├── packages ├── examples │ ├── .gitignore │ ├── README.md │ ├── models │ │ └── obj │ │ │ └── female02 │ │ │ ├── 01_-_Default1noCulling.jpg │ │ │ ├── 02_-_Default1noCulling.jpg │ │ │ ├── 03_-_Default1noCulling.jpg │ │ │ ├── readme.txt │ │ │ └── female02.mtl │ ├── scripts │ │ ├── copyAssets.mts │ │ ├── copyAssetsProduction.mts │ │ └── buildWorker.mts │ ├── src │ │ ├── env.d.ts │ │ ├── worker │ │ │ ├── ComWorkerCommon.ts │ │ │ ├── HelloWorldWorker.ts │ │ │ ├── HelloWorldComChannelEndpointWorker.ts │ │ │ ├── TransferableWorkerTest1.ts │ │ │ ├── TransferableWorkerTest2.ts │ │ │ ├── TransferableWorkerTest4.ts │ │ │ ├── HelloWorldThreeWorker.ts │ │ │ ├── InfiniteWorkerInternalGeometry.ts │ │ │ ├── TransferableWorkerTest3.ts │ │ │ ├── InfiniteWorkerExternalGeometry.ts │ │ │ ├── OBJLoaderWorker.ts │ │ │ ├── Com1Worker.ts │ │ │ └── Com2Worker.ts │ │ ├── helloWorld │ │ │ ├── HelloWorldWorkerTask.ts │ │ │ ├── HelloWorldComChannelEndpoint.ts │ │ │ └── HelloWorldWorkerTaskDirector.ts │ │ ├── com │ │ │ └── WorkerCom.ts │ │ ├── transferables │ │ │ └── TransferablesTestbed.ts │ │ └── threejs │ │ │ └── Threejs.ts │ ├── tsconfig.json │ ├── build │ │ ├── vite.config.Com1Worker.ts │ │ ├── vite.config.Com2Worker.ts │ │ ├── vite.config.OBJLoaderWorker.ts │ │ ├── vite.config.HelloWorldWorker.ts │ │ ├── vite.config.HelloWorldThreeWorker.ts │ │ ├── vite.config.TransferableWorkerTest1.ts │ │ ├── vite.config.TransferableWorkerTest2.ts │ │ ├── vite.config.TransferableWorkerTest3.ts │ │ ├── vite.config.TransferableWorkerTest4.ts │ │ ├── vite.config.InfiniteWorkerExternalGeometry.ts │ │ ├── vite.config.InfiniteWorkerInternalGeometry.ts │ │ └── vite.config.HelloWorldComChannelEndpointWorker.ts │ ├── threejs.html │ ├── helloWorldWorkerTask.html │ ├── transferables.html │ ├── helloWorldComChannelEndpoint.html │ ├── helloWorldWorkerTaskDirector.html │ ├── potentially_infinite.html │ ├── main.css │ ├── workerCom.html │ ├── index.html │ ├── LICENSE │ ├── vite.config.production.ts │ └── package.json ├── wtd-three-ext │ ├── src │ │ ├── index.ts │ │ ├── MaterialStore.ts │ │ ├── offscreen │ │ │ └── WorkerEventProxy.ts │ │ ├── MaterialUtils.ts │ │ ├── MaterialsPayload.ts │ │ └── MeshPayload.ts │ ├── tsconfig.json │ ├── vite.bundle.config.ts │ ├── LICENSE │ ├── package.json │ └── README.md └── wtd-core │ ├── tsconfig.json │ ├── tsconfig.test.json │ ├── src │ ├── Payload.ts │ ├── index.ts │ ├── RawPayload.ts │ ├── offscreen │ │ ├── OffscreenPayload.ts │ │ ├── OffscreenWorker.ts │ │ ├── helper.ts │ │ └── MainEventProxy.ts │ ├── DataPayload.ts │ ├── WorkerTaskWorker.ts │ ├── WorkerMessage.ts │ ├── utilities.ts │ ├── WorkerTask.ts │ ├── WorkerTaskDirector.ts │ └── ComChannelEndpoint.ts │ ├── vite.bundle.config.ts │ ├── LICENSE │ ├── package.json │ ├── test │ └── DataPayload.test.ts │ └── README.md ├── .gitignore ├── .gitattributes ├── .gitpod.yml ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── .editorconfig ├── vitest.config.ts ├── tsconfig.build.json ├── docker-compose.yml ├── Dockerfile ├── .github └── workflows │ ├── actions.yml │ └── ghp.yml ├── tsconfig.json ├── LICENSE ├── vite.config.ts ├── index.html ├── package.json ├── CHANGELOG.md ├── .eslintrc.cjs └── README.md /packages/examples/.gitignore: -------------------------------------------------------------------------------- 1 | production 2 | src/worker/external 3 | src/worker/generated 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | docs 4 | bundle 5 | *.tsbuildinfo 6 | *.tgz 7 | .vscode/profile 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set the default behavior, in case people don't have core.autocrlf set. 2 | * text=auto 3 | *.sh text eol=lf 4 | -------------------------------------------------------------------------------- /packages/examples/README.md: -------------------------------------------------------------------------------- 1 | # Worker Task Director Examples 2 | 3 | Worker Task Director Examples. Please refer to main [README.md](../../README.md). 4 | -------------------------------------------------------------------------------- /packages/examples/models/obj/female02/01_-_Default1noCulling.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaisalmen/wtd/HEAD/packages/examples/models/obj/female02/01_-_Default1noCulling.jpg -------------------------------------------------------------------------------- /packages/examples/models/obj/female02/02_-_Default1noCulling.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaisalmen/wtd/HEAD/packages/examples/models/obj/female02/02_-_Default1noCulling.jpg -------------------------------------------------------------------------------- /packages/examples/models/obj/female02/03_-_Default1noCulling.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kaisalmen/wtd/HEAD/packages/examples/models/obj/female02/03_-_Default1noCulling.jpg -------------------------------------------------------------------------------- /packages/examples/models/obj/female02/readme.txt: -------------------------------------------------------------------------------- 1 | Model by Reallusion iClone from Google 3d Warehouse: 2 | 3 | http://sketchup.google.com/3dwarehouse/details?mid=2c6fd128fca34052adc5f5b98d513da1 -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | image: gitpod/workspace-full 2 | 3 | ports: 4 | - port: 23001 5 | onOpen: open-browser 6 | 7 | tasks: 8 | - name: Terminal 9 | init: npm install && npm run build && npm run dev 10 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig", 4 | "dbaeumer.vscode-eslint", 5 | "davidanson.vscode-markdownlint", 6 | "vitest.explorer" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/examples/scripts/copyAssets.mts: -------------------------------------------------------------------------------- 1 | import shell from 'shelljs'; 2 | 3 | shell.mkdir('-p', './src/worker/generated'); 4 | shell.cp('-f', '../../node_modules/wwobjloader2/lib/worker/OBJLoader2Worker*.js', './src/worker/generated'); 5 | -------------------------------------------------------------------------------- /packages/wtd-three-ext/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MaterialUtils.js'; 2 | export * from './MaterialStore.js'; 3 | export * from './MaterialsPayload.js'; 4 | export * from './MeshPayload.js'; 5 | export * from './offscreen/WorkerEventProxy.js'; 6 | -------------------------------------------------------------------------------- /packages/examples/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_APP_TITLE: string 5 | // more env variables... 6 | } 7 | 8 | interface ImportMeta { 9 | readonly env: ImportMetaEnv 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.json] 12 | indent_size = 2 13 | 14 | [*.yml] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /packages/wtd-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "declarationDir": "dist", 7 | "noEmit": false 8 | }, 9 | "include": [ 10 | "src/**/*.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/examples/scripts/copyAssetsProduction.mts: -------------------------------------------------------------------------------- 1 | import shell from 'shelljs'; 2 | 3 | shell.mkdir('-p', './production/worker/generated'); 4 | shell.cp('-f', './src/worker/generated/*.js', './production/worker/generated'); 5 | shell.cp('-f', './models/obj/female02/*.jpg', './production/assets'); 6 | -------------------------------------------------------------------------------- /packages/wtd-core/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "rootDir": "test" 6 | }, 7 | "references": [{ 8 | "path": "./tsconfig.json" 9 | }], 10 | "include": [ 11 | "test/**/*" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, mergeConfig } from 'vitest/config'; 2 | import viteConfig from './vite.config.js'; 3 | 4 | export default defineConfig(configEnv => mergeConfig( 5 | viteConfig(configEnv), 6 | defineConfig({ 7 | test: { 8 | } 9 | }) 10 | )); 11 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "packages/wtd-core/tsconfig.json" }, 5 | { "path": "packages/wtd-core/tsconfig.test.json" }, 6 | { "path": "packages/wtd-three-ext/tsconfig.json" }, 7 | { "path": "packages/examples/tsconfig.json" } 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "version": "2.0.0", 4 | "configurations": [ 5 | { 6 | "type": "chrome", 7 | "request": "launch", 8 | "name": "Chrome", 9 | "url": "http://localhost:23001", 10 | "webRoot": "${workspaceFolder}", 11 | "userDataDir": "${workspaceFolder}/.vscode/profile" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /packages/wtd-three-ext/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "declarationDir": "dist" 7 | }, 8 | "references": [{ 9 | "path": "../wtd-core/tsconfig.json" 10 | }], 11 | "include": [ 12 | "src/**/*.ts" 13 | ], 14 | "exclude": [ 15 | "./dist", 16 | "./node_modules" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | wtddev: 5 | build: 6 | dockerfile: Dockerfile 7 | context: . 8 | ports: 9 | - target: 23001 10 | published: 23001 11 | protocol: tcp 12 | mode: host 13 | volumes: 14 | - ./:/home/devbox/workspace/:rw 15 | command: ["bash", "-c", "npm install && npm run build && npm run dev"] 16 | working_dir: /home/devbox/workspace 17 | container_name: wtddev 18 | -------------------------------------------------------------------------------- /packages/examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "declarationDir": "dist", 7 | "noEmit": false 8 | }, 9 | "references": [{ 10 | "path": "../wtd-core/tsconfig.json", 11 | }, 12 | { 13 | "path": "../wtd-three-ext/tsconfig.json" 14 | }], 15 | "include": [ 16 | "src/**/*.ts" 17 | ], 18 | "exclude": [ 19 | "dist", 20 | "node_modules" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/wtd-core/src/Payload.ts: -------------------------------------------------------------------------------- 1 | export type AssociatedArrayType = { [key: string]: T } 2 | 3 | export interface Payload { 4 | $type: string; 5 | message: unknown; 6 | } 7 | 8 | export interface PayloadHandler { 9 | pack(payload: Payload, transferable: Transferable[], cloneBuffers: boolean): Transferable[]; 10 | unpack(transportObject: Payload, cloneBuffers: boolean): Payload; 11 | } 12 | 13 | export class PayloadRegister { 14 | static handler = new Map(); 15 | } 16 | -------------------------------------------------------------------------------- /packages/examples/build/vite.config.Com1Worker.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | const config = defineConfig({ 5 | build: { 6 | lib: { 7 | entry: path.resolve(__dirname, '../src/worker/Com1Worker.ts'), 8 | name: 'Com1Worker', 9 | fileName: (format) => `Com1Worker-${format}.js`, 10 | formats: ['es', 'iife'] 11 | }, 12 | outDir: 'src/worker/generated', 13 | emptyOutDir: false 14 | } 15 | }); 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /packages/examples/build/vite.config.Com2Worker.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | const config = defineConfig({ 5 | build: { 6 | lib: { 7 | entry: path.resolve(__dirname, '../src/worker/Com2Worker.ts'), 8 | name: 'Com2Worker', 9 | fileName: (format) => `Com2Worker-${format}.js`, 10 | formats: ['es', 'iife'] 11 | }, 12 | outDir: 'src/worker/generated', 13 | emptyOutDir: false 14 | } 15 | }); 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /packages/examples/build/vite.config.OBJLoaderWorker.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | const config = defineConfig({ 5 | build: { 6 | lib: { 7 | entry: path.resolve(__dirname, '../src/worker/OBJLoaderWorker.ts'), 8 | name: 'OBJLoaderWorker', 9 | fileName: (format) => `OBJLoaderWorker-${format}.js`, 10 | formats: ['es', 'iife'] 11 | }, 12 | outDir: 'src/worker/generated', 13 | emptyOutDir: false 14 | } 15 | }); 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /packages/examples/src/worker/ComWorkerCommon.ts: -------------------------------------------------------------------------------- 1 | export const updateText = (params: { 2 | text: string; 3 | width: number; 4 | height: number; 5 | canvas?: HTMLCanvasElement; 6 | log?: boolean; 7 | }) => { 8 | const context = params.canvas?.getContext('2d'); 9 | if (context) { 10 | context.clearRect(0, 0, params.width, params.height); 11 | context.font = '12px Arial'; 12 | context.fillText(params.text, 12, 48); 13 | } 14 | if (params.log === true) { 15 | console.log(params.text); 16 | } 17 | }; 18 | 19 | -------------------------------------------------------------------------------- /packages/examples/build/vite.config.HelloWorldWorker.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | const config = defineConfig({ 5 | build: { 6 | lib: { 7 | entry: path.resolve(__dirname, '../src/worker/HelloWorldWorker.ts'), 8 | name: 'HelloWorldWorker', 9 | fileName: (format) => `HelloWorldWorker-${format}.js`, 10 | formats: ['es', 'iife'] 11 | }, 12 | outDir: 'src/worker/generated', 13 | emptyOutDir: false 14 | } 15 | }); 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /packages/wtd-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './WorkerMessage.js'; 2 | export * from './ComChannelEndpoint.js'; 3 | export * from './WorkerTask.js'; 4 | export * from './WorkerTaskWorker.js'; 5 | export * from './WorkerTaskDirector.js'; 6 | export * from './Payload.js'; 7 | export * from './RawPayload.js'; 8 | export * from './DataPayload.js'; 9 | export * from './utilities.js'; 10 | export * from './offscreen/OffscreenWorker.js'; 11 | export * from './offscreen/OffscreenPayload.js'; 12 | export * from './offscreen/MainEventProxy.js'; 13 | export * from './offscreen/helper.js'; 14 | -------------------------------------------------------------------------------- /packages/examples/build/vite.config.HelloWorldThreeWorker.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | const config = defineConfig({ 5 | build: { 6 | lib: { 7 | entry: path.resolve(__dirname, '../src/worker/HelloWorldThreeWorker.ts'), 8 | name: 'HelloWorldThreeWorker', 9 | fileName: (format) => `HelloWorldThreeWorker-${format}.js`, 10 | formats: ['es', 'iife'] 11 | }, 12 | outDir: 'src/worker/generated', 13 | emptyOutDir: false 14 | } 15 | }); 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /packages/examples/build/vite.config.TransferableWorkerTest1.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | const config = defineConfig({ 5 | build: { 6 | lib: { 7 | entry: path.resolve(__dirname, '../src/worker/TransferableWorkerTest1.ts'), 8 | name: 'TransferableWorkerTest1', 9 | fileName: (format) => `TransferableWorkerTest1-${format}.js`, 10 | formats: ['es', 'iife'] 11 | }, 12 | outDir: 'src/worker/generated', 13 | emptyOutDir: false 14 | } 15 | }); 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /packages/examples/build/vite.config.TransferableWorkerTest2.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | const config = defineConfig({ 5 | build: { 6 | lib: { 7 | entry: path.resolve(__dirname, '../src/worker/TransferableWorkerTest2.ts'), 8 | name: 'TransferableWorkerTest2', 9 | fileName: (format) => `TransferableWorkerTest2-${format}.js`, 10 | formats: ['es', 'iife'] 11 | }, 12 | outDir: 'src/worker/generated', 13 | emptyOutDir: false 14 | } 15 | }); 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /packages/examples/build/vite.config.TransferableWorkerTest3.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | const config = defineConfig({ 5 | build: { 6 | lib: { 7 | entry: path.resolve(__dirname, '../src/worker/TransferableWorkerTest3.ts'), 8 | name: 'TransferableWorkerTest3', 9 | fileName: (format) => `TransferableWorkerTest3-${format}.js`, 10 | formats: ['es', 'iife'] 11 | }, 12 | outDir: 'src/worker/generated', 13 | emptyOutDir: false 14 | } 15 | }); 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /packages/examples/build/vite.config.TransferableWorkerTest4.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | const config = defineConfig({ 5 | build: { 6 | lib: { 7 | entry: path.resolve(__dirname, '../src/worker/TransferableWorkerTest4.ts'), 8 | name: 'TransferableWorkerTest4', 9 | fileName: (format) => `TransferableWorkerTest4-${format}.js`, 10 | formats: ['es', 'iife'] 11 | }, 12 | outDir: 'src/worker/generated', 13 | emptyOutDir: false 14 | } 15 | }); 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /packages/examples/build/vite.config.InfiniteWorkerExternalGeometry.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | const config = defineConfig({ 5 | build: { 6 | lib: { 7 | entry: path.resolve(__dirname, '../src/worker/InfiniteWorkerExternalGeometry.ts'), 8 | name: 'InfiniteWorkerExternalGeometry', 9 | fileName: (format) => `InfiniteWorkerExternalGeometry-${format}.js`, 10 | formats: ['es', 'iife'] 11 | }, 12 | outDir: 'src/worker/generated', 13 | emptyOutDir: false 14 | } 15 | }); 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /packages/examples/build/vite.config.InfiniteWorkerInternalGeometry.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | const config = defineConfig({ 5 | build: { 6 | lib: { 7 | entry: path.resolve(__dirname, '../src/worker/InfiniteWorkerInternalGeometry.ts'), 8 | name: 'InfiniteWorkerInternalGeometry', 9 | fileName: (format) => `InfiniteWorkerInternalGeometry-${format}.js`, 10 | formats: ['es', 'iife'] 11 | }, 12 | outDir: 'src/worker/generated', 13 | emptyOutDir: false 14 | } 15 | }); 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /packages/examples/threejs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WorkerTaskDirector: Three.js 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
WorkerTaskDirector: Three.js
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/examples/build/vite.config.HelloWorldComChannelEndpointWorker.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { defineConfig } from 'vite'; 3 | 4 | const config = defineConfig({ 5 | build: { 6 | lib: { 7 | entry: path.resolve(__dirname, '../src/worker/HelloWorldComChannelEndpointWorker.ts'), 8 | name: 'HelloWorldComChannelEndpointWorker', 9 | fileName: (format) => `HelloWorldComChannelEndpointWorker-${format}.js`, 10 | formats: ['es', 'iife'] 11 | }, 12 | outDir: 'src/worker/generated', 13 | emptyOutDir: false 14 | } 15 | }); 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /packages/examples/helloWorldWorkerTask.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WorkerTask: Hello World 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
WorkerTask: Hello World
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/examples/transferables.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WorkerTaskDirector: Transferables 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
WorkerTaskDirector: Transferables
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.guides.bracketPairs": true, 3 | "editor.formatOnSave": false, 4 | "workbench.editor.revealIfOpen": true, 5 | "[javascript]": { 6 | "editor.formatOnSave": true 7 | }, 8 | "[typescript]": { 9 | "editor.formatOnSave": true 10 | }, 11 | "eslint.codeAction.showDocumentation": { 12 | "enable": true 13 | }, 14 | "eslint.format.enable": true, 15 | "eslint.validate": [ 16 | "javascript", 17 | "typescript" 18 | ], 19 | "[json]": { 20 | "editor.defaultFormatter": "vscode.json-language-features" 21 | }, 22 | "vitest.disableWorkspaceWarning": true 23 | } 24 | -------------------------------------------------------------------------------- /packages/examples/helloWorldComChannelEndpoint.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ComChannelEndpoint: Hello World 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
ComChannelEndpoint: Hello World
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/examples/helloWorldWorkerTaskDirector.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WorkerTaskDirector: Hello World 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
WorkerTaskDirector: Hello World
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/wtd-core/src/RawPayload.ts: -------------------------------------------------------------------------------- 1 | import { Payload } from './Payload.js'; 2 | 3 | export interface RawMessage { 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | raw: any; 6 | } 7 | 8 | export interface RawPayloadAdditions extends Payload { 9 | message: RawMessage; 10 | } 11 | 12 | export class RawPayload implements RawPayloadAdditions { 13 | $type = 'RawPayload'; 14 | message = { 15 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 16 | raw: {} as any 17 | }; 18 | 19 | constructor(raw?: unknown) { 20 | if (raw !== undefined) { 21 | this.message.raw = raw; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | RUN apt update \ 4 | && apt upgrade -y \ 5 | && apt install -y ca-certificates curl gnupg unzip 6 | 7 | RUN mkdir -p /etc/apt/keyrings \ 8 | && curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg 9 | 10 | ARG username=devbox 11 | RUN adduser ${username} && usermod -aG sudo ${username} 12 | 13 | RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list \ 14 | && apt update \ 15 | && apt install -y nodejs 16 | RUN curl https://get.volta.sh | bash 17 | RUN apt autoremove 18 | 19 | WORKDIR /home/devbox/workspace 20 | 21 | -------------------------------------------------------------------------------- /packages/wtd-core/src/offscreen/OffscreenPayload.ts: -------------------------------------------------------------------------------- 1 | import { AssociatedArrayType, Payload } from 'wtd-core'; 2 | 3 | export interface OffscreenPayloadMessage { 4 | drawingSurface?: OffscreenCanvas | HTMLCanvasElement; 5 | width?: number; 6 | height?: number; 7 | pixelRatio?: number; 8 | top?: number; 9 | left?: number; 10 | event?: AssociatedArrayType; 11 | } 12 | 13 | export interface OffscreenPayloadAdditions extends Payload { 14 | message: OffscreenPayloadMessage; 15 | } 16 | 17 | export class OffscreenPayload implements OffscreenPayloadAdditions { 18 | $type = 'OffscreenPayload'; 19 | message: OffscreenPayloadMessage; 20 | 21 | constructor(message: OffscreenPayloadMessage) { 22 | this.message = message; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/examples/potentially_infinite.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WorkerTaskDirector: Potentially Infinite Execution 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
WorkerTaskDirector: Potentially Infinite Execution
13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/wtd-core/vite.bundle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import path from 'path'; 3 | 4 | export default defineConfig({ 5 | build: { 6 | lib: { 7 | entry: path.resolve(__dirname, 'src/index.ts'), 8 | name: 'wtd-core', 9 | fileName: () => 'index.js', 10 | formats: ['es'] 11 | }, 12 | outDir: 'bundle', 13 | emptyOutDir: false, 14 | rollupOptions: { 15 | output: { 16 | inlineDynamicImports: true, 17 | name: 'wtd-core', 18 | exports: 'named', 19 | sourcemap: false, 20 | assetFileNames: (assetInfo) => { 21 | return `assets/${assetInfo.name}`; 22 | } 23 | } 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /.github/workflows/actions.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - "ghp" 7 | tags-ignore: 8 | - "v*.*.*" 9 | 10 | jobs: 11 | build: 12 | name: wtd 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 15 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Volta 21 | uses: volta-cli/action@v4 22 | 23 | - name: Install 24 | run: | 25 | npm ci 26 | 27 | - name: Lint 28 | run: | 29 | npm run lint 30 | 31 | - name: Build 32 | run: | 33 | npm run build 34 | 35 | - name: Test 36 | run: | 37 | npm run test 38 | 39 | # Always check if production build works 40 | - name: Build Production 41 | run: | 42 | npm run build:production 43 | -------------------------------------------------------------------------------- /packages/wtd-core/src/offscreen/OffscreenWorker.ts: -------------------------------------------------------------------------------- 1 | import { WorkerMessage } from '../WorkerMessage.js'; 2 | 3 | export enum OffscreenWorkerCommandRequest { 4 | INIT_OFFSCREEN_CANVAS = 'initOffscreenCanvas', 5 | PROXY_START = 'proxyStart', 6 | PROXY_EVENT = 'proxyEvent', 7 | RESIZE = 'resize' 8 | } 9 | 10 | export enum OffscreenWorkerCommandResponse { 11 | INIT_OFFSCREEN_CANVAS_COMPLETE = 'initOffscreenCanvasComplete', 12 | PROXY_START_COMPLETE = 'proxyStartComplete', 13 | PROXY_EVENT_COMPLETE = 'proxyEventComplete', 14 | RESIZE_COMPLETE = 'resizeComplete' 15 | } 16 | 17 | export interface OffscreenWorker { 18 | initOffscreenCanvas(message: WorkerMessage): void; 19 | proxyStart?(message: WorkerMessage): void; 20 | proxyEvent?(message: WorkerMessage): void; 21 | resize?(message: WorkerMessage): void; 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "lib": [ 7 | "ES2022", 8 | "DOM", 9 | "webworker" 10 | ], 11 | "types": [ 12 | "vite/client" 13 | ], 14 | "useDefineForClassFields": true, 15 | "strict": true, 16 | "composite": true, 17 | "sourceMap": true, 18 | "resolveJsonModule": true, 19 | "esModuleInterop": true, 20 | "emitDeclarationOnly": false, 21 | "declaration": true, 22 | "declarationMap": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "noImplicitReturns": true, 26 | "noImplicitAny": true, 27 | "skipLibCheck": true 28 | }, 29 | "include": [ 30 | "**/src/**/*.ts", 31 | "**/test/**/*.ts" 32 | ], 33 | "exclude": [ 34 | "**/node_modules/**/*", 35 | "**/dist/**/*" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /packages/wtd-three-ext/vite.bundle.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | import { defineConfig } from 'vite'; 3 | import path from 'path'; 4 | 5 | export default defineConfig({ 6 | build: { 7 | lib: { 8 | entry: path.resolve(__dirname, 'src/index.ts'), 9 | name: 'wtd-three-ext', 10 | fileName: () => 'index.js', 11 | formats: ['es'] 12 | }, 13 | outDir: 'bundle', 14 | emptyOutDir: false, 15 | rollupOptions: { 16 | external: ['three'], 17 | output: { 18 | inlineDynamicImports: true, 19 | name: 'wtd-three-ext', 20 | exports: 'named', 21 | sourcemap: false, 22 | assetFileNames: (assetInfo) => { 23 | return `assets/${assetInfo.name}`; 24 | } 25 | } 26 | }, 27 | 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /packages/examples/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background-color: #000; 4 | color: limegreen; 5 | font-family: Monospace; 6 | font-size: 13px; 7 | line-height: 24px; 8 | } 9 | 10 | h1 { 11 | color: rgb(221, 118, 0) 12 | } 13 | 14 | a { 15 | color: limegreen; 16 | text-decoration: none; 17 | } 18 | 19 | a:hover { 20 | text-decoration: underline; 21 | } 22 | 23 | button { 24 | cursor: pointer; 25 | text-transform: uppercase; 26 | } 27 | 28 | canvas { 29 | display: block; 30 | } 31 | 32 | #info { 33 | position: absolute; 34 | top: 0px; 35 | width: 100%; 36 | padding: 10px; 37 | box-sizing: border-box; 38 | text-align: center; 39 | -moz-user-select: none; 40 | -webkit-user-select: none; 41 | -ms-user-select: none; 42 | user-select: none; 43 | pointer-events: none; 44 | z-index: 1; 45 | } 46 | 47 | a, button, input, select { 48 | pointer-events: auto; 49 | } 50 | -------------------------------------------------------------------------------- /packages/examples/workerCom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WorkerTask: Inter-Worker Communication 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
WorkerTask: Inter-Worker Communication
13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 Kai Salmen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /packages/examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples 7 | 8 | 9 | 10 |
11 |

Examples

12 | 13 | ComChannelEndpoint: Hello World 14 |
15 | WorkerTask: Hello World 16 |
17 | WorkerTaskDirector: Hello World 18 |
19 | WorkerTask: Inter-Worker Communication 20 |
21 | WorkerTaskDirector: Transferables 22 |
23 | WorkerTaskDirector: Three.js 24 |
25 | WorkerTaskDirector: Potentially Infinite Execution 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /packages/examples/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 Kai Salmen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /packages/wtd-core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 Kai Salmen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /packages/wtd-three-ext/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 Kai Salmen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import path from 'path'; 3 | 4 | export default defineConfig(({ command }) => { 5 | console.log(`Running: ${command}`); 6 | return { 7 | build: { 8 | rollupOptions: { 9 | input: { 10 | main: path.resolve(__dirname, 'index.html'), 11 | helloWorldWorkerTask: path.resolve(__dirname, 'packages/examples/helloWorldWorkerTask.html'), 12 | helloWorldComChannelEndpoint: path.resolve(__dirname, 'packages/examples/helloWorldComChannelEndpoint.html'), 13 | helloWorldWorkerTaskDirector: path.resolve(__dirname, 'packages/examples/helloWorldWorkerTaskDirector.html'), 14 | transferables: path.resolve(__dirname, 'packages/examples/transferables.html'), 15 | threejs: path.resolve(__dirname, 'packages/examples/threejs.html'), 16 | potentiallyInfinite: path.resolve(__dirname, 'packages/examples/potentially_infinite.html') 17 | } 18 | } 19 | }, 20 | server: { 21 | port: 23001, 22 | host: '0.0.0.0' 23 | }, 24 | }; 25 | }); 26 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Examples 7 | 8 | 9 | 10 |
11 |

Examples

12 | 13 | ComChannelEndpoint: Hello World 14 |
15 | WorkerTask: Hello World 16 |
17 | WorkerTaskDirector: Hello World 18 |
19 | WorkerTask: Inter-Worker Communication 20 |
21 | WorkerTaskDirector: Transferables 22 |
23 | WorkerTaskDirector: Three.js 24 |
25 | WorkerTaskDirector: Potentially Infinite Execution 26 |
27 |
28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /packages/examples/src/worker/HelloWorldWorker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | comRouting, 3 | RawPayload, 4 | WorkerTaskCommandResponse, 5 | WorkerMessage, 6 | WorkerTaskWorker 7 | } from 'wtd-core'; 8 | 9 | export class HelloWorldWorker implements WorkerTaskWorker { 10 | 11 | init(message: WorkerMessage) { 12 | const initComplete = WorkerMessage.createFromExisting(message, { 13 | overrideCmd: WorkerTaskCommandResponse.INIT_COMPLETE 14 | }); 15 | self.postMessage(initComplete); 16 | } 17 | 18 | execute(message: WorkerMessage) { 19 | // burn some time 20 | for (let i = 0; i < 25000000; i++) { 21 | i++; 22 | } 23 | 24 | const rawPayload = new RawPayload({ 25 | hello: 'Hello! I just incremented "i" 25 million times.' 26 | }); 27 | 28 | const execComplete = WorkerMessage.createFromExisting(message, { 29 | overrideCmd: WorkerTaskCommandResponse.EXECUTE_COMPLETE 30 | }); 31 | execComplete.addPayload(rawPayload); 32 | 33 | // no need to pack as there aren't any buffers used 34 | self.postMessage(execComplete); 35 | } 36 | 37 | } 38 | 39 | const worker = new HelloWorldWorker(); 40 | self.onmessage = message => comRouting(worker, message); 41 | -------------------------------------------------------------------------------- /.github/workflows/ghp.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - "ghp" 7 | 8 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | jobs: 15 | build: 16 | name: wtd 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 15 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - name: Volta 25 | uses: volta-cli/action@v4 26 | 27 | - name: Install 28 | run: | 29 | npm ci 30 | 31 | - name: Build 32 | run: | 33 | npm run build 34 | 35 | - name: Build Production 36 | run: | 37 | npm run build:production 38 | 39 | - name: Setup Pages 40 | id: pages 41 | uses: actions/configure-pages@v5 42 | 43 | - name: Upload artifact 44 | uses: actions/upload-pages-artifact@v3 45 | with: 46 | path: ./packages/examples/production/ 47 | 48 | deploy: 49 | needs: build 50 | environment: 51 | name: github-pages 52 | url: ${{ steps.deployment.outputs.page_url }} 53 | runs-on: ubuntu-latest 54 | 55 | steps: 56 | - name: Deploy to GitHub Pages 57 | id: deployment 58 | uses: actions/deploy-pages@v4 59 | -------------------------------------------------------------------------------- /packages/examples/src/worker/HelloWorldComChannelEndpointWorker.ts: -------------------------------------------------------------------------------- 1 | import { RawPayload, WorkerMessage, ComRouter, ComChannelEndpoint } from 'wtd-core'; 2 | 3 | /// 4 | 5 | declare const self: DedicatedWorkerGlobalScope; 6 | 7 | class ExampleComRouterWorker implements ComRouter { 8 | 9 | private endpointFs?: ComChannelEndpoint; 10 | 11 | setComChannelEndpoint(comChannelEndpoint: ComChannelEndpoint): void { 12 | this.endpointFs = comChannelEndpoint; 13 | } 14 | 15 | async hello_world(message: WorkerMessage) { 16 | // burn some time 17 | for (let i = 0; i < 25000000; i++) { 18 | i++; 19 | } 20 | 21 | await this.endpointFs?.sentAnswer({ 22 | message: WorkerMessage.createFromExisting(message, { 23 | overrideCmd: 'hello_world_confirm', 24 | overridePayloads: new RawPayload({ 25 | hello: 'Hello! I just incremented "i" 25 million times.' 26 | }) 27 | }), 28 | awaitAnswer: false 29 | }); 30 | } 31 | } 32 | 33 | new ComChannelEndpoint({ 34 | endpointId: 2000, 35 | endpointConfig: { 36 | $type: 'DirectImplConfig', 37 | impl: self 38 | }, 39 | verbose: true, 40 | endpointName: 'HelloWorldComChannelEndpointWorker' 41 | }).connect(new ExampleComRouterWorker()); 42 | -------------------------------------------------------------------------------- /packages/wtd-core/src/offscreen/helper.ts: -------------------------------------------------------------------------------- 1 | import { WorkerTask } from '../WorkerTask.js'; 2 | import { WorkerMessage } from '../WorkerMessage.js'; 3 | import { OffscreenPayload } from './OffscreenPayload.js'; 4 | import { OffscreenWorkerCommandRequest, OffscreenWorkerCommandResponse } from './OffscreenWorker.js'; 5 | 6 | export const getOffscreenCanvas = (offScreenPayload?: OffscreenPayload) => { 7 | return offScreenPayload ? offScreenPayload.message.drawingSurface as OffscreenCanvas : undefined; 8 | }; 9 | 10 | export const initOffscreenCanvas = async (workerTask: WorkerTask, canvas: HTMLCanvasElement) => { 11 | const offscreenCanvas = canvas.transferControlToOffscreen(); 12 | const offscreenPayloadRenderer = new OffscreenPayload({ 13 | drawingSurface: offscreenCanvas, 14 | width: canvas.clientWidth, 15 | height: canvas.clientHeight, 16 | pixelRatio: window.devicePixelRatio 17 | }); 18 | await workerTask.sentMessage({ 19 | message: WorkerMessage.fromPayload(offscreenPayloadRenderer, OffscreenWorkerCommandRequest.INIT_OFFSCREEN_CANVAS), 20 | transferables: [offscreenCanvas], 21 | awaitAnswer: true, 22 | expectedAnswer: OffscreenWorkerCommandResponse.INIT_OFFSCREEN_CANVAS_COMPLETE 23 | }); 24 | }; 25 | 26 | export const recalcAspectRatio = (canvas: HTMLCanvasElement | OffscreenCanvas, clientWidth: number, clientHeight: number) => { 27 | canvas.width = canvas.height * (clientWidth / clientHeight); 28 | return canvas.width; 29 | }; 30 | -------------------------------------------------------------------------------- /packages/examples/vite.config.production.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import path from 'path'; 3 | 4 | export default defineConfig(({ command }) => { 5 | console.log(`Running: ${command}`); 6 | return { 7 | build: { 8 | target: ['es2022'], 9 | rollupOptions: { 10 | input: { 11 | main: path.resolve(__dirname, 'index.html'), 12 | helloWorldWorkerTask: path.resolve(__dirname, 'helloWorldWorkerTask.html'), 13 | helloWorldComChannelEndpoint: path.resolve(__dirname, 'helloWorldComChannelEndpoint.html'), 14 | helloWorldWorkerTaskDirector: path.resolve(__dirname, 'helloWorldWorkerTaskDirector.html'), 15 | workerCom: path.resolve(__dirname, 'workerCom.html'), 16 | transferables: path.resolve(__dirname, 'transferables.html'), 17 | threejs: path.resolve(__dirname, 'threejs.html'), 18 | potentiallyInfinite: path.resolve(__dirname, 'potentially_infinite.html') 19 | }, 20 | output: { 21 | esModule: true 22 | } 23 | }, 24 | minify: false, 25 | assetsInlineLimit: 128, 26 | outDir: path.resolve(__dirname, 'production'), 27 | emptyOutDir: true, 28 | }, 29 | base: 'https://kaisalmen.github.io/wtd/', 30 | optimizeDeps: { 31 | esbuildOptions: { 32 | target: 'es2022' 33 | } 34 | } 35 | }; 36 | }); 37 | 38 | -------------------------------------------------------------------------------- /packages/wtd-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wtd-core", 3 | "version": "4.0.1", 4 | "license": "MIT", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "default": "./dist/index.js" 13 | }, 14 | "./bundle": { 15 | "types": "./dist/index.d.ts", 16 | "default": "./bundle/index.js" 17 | } 18 | }, 19 | "typesVersions": { 20 | "*": { 21 | ".": [ 22 | "dist/index" 23 | ], 24 | "bundle": [ 25 | "dist/index" 26 | ] 27 | } 28 | }, 29 | "files": [ 30 | "dist", 31 | "bundle", 32 | "src", 33 | "LICENSE", 34 | "README.md" 35 | ], 36 | "scripts": { 37 | "clean": "shx rm -fr *.tsbuildinfo dist bundle", 38 | "doc": "shx rm -fr docs && typedoc --plugin typedoc-plugin-markdown --out docs src/index.ts", 39 | "compile": "tsc -b", 40 | "build:bundle": "vite --config vite.bundle.config.ts build", 41 | "build": "npm run clean && npm run compile && npm run build:bundle" 42 | }, 43 | "volta": { 44 | "node": "20.17.0", 45 | "npm": "10.8.3" 46 | }, 47 | "repository": { 48 | "type": "git", 49 | "url": "https://github.com/kaisalmen/wtd", 50 | "directory": "packages/wtd" 51 | }, 52 | "homepage": "https://github.com/kaisalmen/wtd/blob/main/packages/wtd-core/README.md", 53 | "bugs": "https://github.com/kaisalmen/wtd/issues", 54 | "author": { 55 | "name": "kaisalmen", 56 | "url": "https://www.kaisalmen.de" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/examples/src/worker/TransferableWorkerTest1.ts: -------------------------------------------------------------------------------- 1 | import { 2 | comRouting, 3 | DataPayload, 4 | WorkerTaskCommandResponse, 5 | WorkerMessage, 6 | WorkerTaskWorker 7 | } from 'wtd-core'; 8 | 9 | class TransferableWorkerTest1 implements WorkerTaskWorker { 10 | 11 | init(message: WorkerMessage) { 12 | console.log(`TransferableWorkerTest1#init: name: ${message.name} uuid: ${message.uuid} cmd: ${message.cmd} workerId: ${message.endpointdId}`); 13 | 14 | const initComplete = WorkerMessage.createFromExisting(message, { 15 | overrideCmd: WorkerTaskCommandResponse.INIT_COMPLETE 16 | }); 17 | self.postMessage(initComplete); 18 | } 19 | 20 | execute(message: WorkerMessage) { 21 | console.log(`TransferableWorkerTest1#execute: name: ${message.name} uuid: ${message.uuid} cmd: ${message.cmd} workerId: ${message.endpointdId}`); 22 | 23 | const wm = WorkerMessage.unpack(message, false); 24 | 25 | const dataPayload = new DataPayload(); 26 | dataPayload.message.params = { 27 | data: new Uint32Array(32 * 1024 * 1024) 28 | }; 29 | 30 | const execComplete = WorkerMessage.createFromExisting(wm, { 31 | overrideCmd: WorkerTaskCommandResponse.EXECUTE_COMPLETE 32 | }); 33 | execComplete.addPayload(dataPayload); 34 | 35 | const transferables = WorkerMessage.pack(execComplete.payloads, false); 36 | self.postMessage(execComplete, transferables); 37 | } 38 | 39 | } 40 | 41 | const worker = new TransferableWorkerTest1(); 42 | self.onmessage = message => comRouting(worker, message); 43 | -------------------------------------------------------------------------------- /packages/examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wtd-examples", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "private": "true", 6 | "scripts": { 7 | "clean": "shx rm -fr *.tsbuildinfo dist src/worker/generated", 8 | "doc": "echo 'docs are not generated for examples'", 9 | "compile": "tsc -b", 10 | "build": "npm run clean && npm run script:copy:assets && npm run compile", 11 | "clean:production": "shx rm -fr production", 12 | "script:build:worker": "vite-node ./scripts/buildWorker.mts", 13 | "script:copy:assets": "vite-node ./scripts/copyAssets.mts", 14 | "script:copy:assets:production": "vite-node ./scripts/copyAssetsProduction.mts", 15 | "build:production": "npm run build && npm run script:build:worker && npm run build:production:vite", 16 | "build:production:vite": "npm run clean:production && vite --config vite.config.production.ts build && npm run script:copy:assets:production", 17 | "serve": "http-server ./production" 18 | }, 19 | "dependencies": { 20 | "lil-gui": "~0.19.2", 21 | "three": "~0.169.0", 22 | "wtd-core": "~4.0.1", 23 | "wtd-three-ext": "~4.0.1", 24 | "wwobjloader2": "6.2.1" 25 | }, 26 | "devDependencies": { 27 | "@types/three": "~0.169.0", 28 | "http-server": "~14.1.1" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/kaisalmen/wtd", 33 | "directory": "packages/wtd" 34 | }, 35 | "homepage": "https://github.com/kaisalmen/wtd/blob/main/packages/examples/README.md", 36 | "bugs": "https://github.com/kaisalmen/wtd/issues", 37 | "author": { 38 | "name": "kaisalmen", 39 | "url": "https://www.kaisalmen.de" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/examples/models/obj/female02/female02.mtl: -------------------------------------------------------------------------------- 1 | # Material Count: 6 2 | newmtl FrontColorNoCullingID__01_-_Default1noCulli 3 | Ns 154.901961 4 | Ka 0.000000 0.000000 0.000000 5 | Kd 0.800000 0.800000 0.800000 6 | Ks 0.165000 0.165000 0.165000 7 | Ni 1.000000 8 | d 1.000000 9 | illum 2 10 | map_Kd 01_-_Default1noCulling.jpg 11 | 12 | 13 | newmtl _02_-_Default1noCulli__02_-_Default1noCulli 14 | Ns 154.901961 15 | Ka 0.000000 0.000000 0.000000 16 | Kd 0.640000 0.640000 0.640000 17 | Ks 0.165000 0.165000 0.165000 18 | Ni 1.000000 19 | d 1.000000 20 | illum 2 21 | map_Kd 02_-_Default1noCulling.jpg 22 | 23 | 24 | newmtl _01_-_Default1noCulli__01_-_Default1noCulli 25 | Ns 154.901961 26 | Ka 0.000000 0.000000 0.000000 27 | Kd 0.640000 0.640000 0.640000 28 | Ks 0.165000 0.165000 0.165000 29 | Ni 1.000000 30 | d 1.000000 31 | illum 2 32 | map_Kd 01_-_Default1noCulling.jpg 33 | 34 | 35 | newmtl FrontColorNoCullingID__03_-_Default1noCulli 36 | Ns 154.901961 37 | Ka 0.000000 0.000000 0.000000 38 | Kd 0.800000 0.800000 0.800000 39 | Ks 0.165000 0.165000 0.165000 40 | Ni 1.000000 41 | d 1.000000 42 | illum 2 43 | map_Kd 03_-_Default1noCulling.jpg 44 | 45 | 46 | newmtl _03_-_Default1noCulli__03_-_Default1noCulli 47 | Ns 154.901961 48 | Ka 0.000000 0.000000 0.000000 49 | Kd 0.640000 0.640000 0.640000 50 | Ks 0.165000 0.165000 0.165000 51 | Ni 1.000000 52 | d 1.000000 53 | illum 2 54 | map_Kd 03_-_Default1noCulling.jpg 55 | 56 | 57 | newmtl FrontColorNoCullingID__02_-_Default1noCulli 58 | Ns 154.901961 59 | Ka 0.000000 0.000000 0.000000 60 | Kd 0.800000 0.800000 0.800000 61 | Ks 0.165000 0.165000 0.165000 62 | Ni 1.000000 63 | d 1.000000 64 | illum 2 65 | map_Kd 02_-_Default1noCulling.jpg 66 | 67 | 68 | -------------------------------------------------------------------------------- /packages/examples/src/helloWorld/HelloWorldWorkerTask.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RawPayload, 3 | WorkerTask, 4 | WorkerMessage 5 | } from 'wtd-core'; 6 | 7 | /** 8 | * Hello World example just using the WorkerTask directly without the WorkerTaskDirector 9 | */ 10 | class HelloWorldWorkerTaskExample { 11 | 12 | async run() { 13 | const url = new URL(import.meta.env.DEV ? '../worker/HelloWorldWorker.ts' : '../worker/generated/HelloWorldWorker-es.js', import.meta.url); 14 | const workerTask = new WorkerTask({ 15 | endpointName: 'HelloWorldWorker', 16 | endpointId: 1, 17 | endpointConfig: { 18 | $type: 'WorkerConfigParams', 19 | url, 20 | workerType: 'module', 21 | }, 22 | verbose: true 23 | }); 24 | 25 | try { 26 | // connects the worker callback functions and the WorkerTask 27 | workerTask.connect(); 28 | 29 | const t0 = performance.now(); 30 | // execute without init 31 | const resultExec = await workerTask.executeWorker({ 32 | message: WorkerMessage.createEmpty() 33 | }); 34 | 35 | const rawPayload = resultExec.payloads[0] as RawPayload; 36 | const answer = `Worker said: ${rawPayload.message.raw?.hello}`; 37 | const t1 = performance.now(); 38 | 39 | const msg = `${answer}\nWorker execution has been completed after ${t1 - t0}ms.`; 40 | console.log(msg); 41 | alert(msg); 42 | } catch (e) { 43 | console.error(e); 44 | } 45 | } 46 | } 47 | 48 | const app = new HelloWorldWorkerTaskExample(); 49 | app.run(); 50 | -------------------------------------------------------------------------------- /packages/wtd-core/src/DataPayload.ts: -------------------------------------------------------------------------------- 1 | import { AssociatedArrayType, Payload, PayloadHandler, PayloadRegister } from './Payload.js'; 2 | import { fillTransferables } from './utilities.js'; 3 | 4 | export interface ParameterizedMessage { 5 | params?: AssociatedArrayType; 6 | buffers?: Map; 7 | } 8 | 9 | export interface DataPayloadAdditions extends Payload { 10 | message: ParameterizedMessage; 11 | } 12 | 13 | export class DataPayload implements DataPayloadAdditions { 14 | $type = 'DataPayload'; 15 | message: ParameterizedMessage = { 16 | buffers: new Map(), 17 | params: {} 18 | }; 19 | } 20 | 21 | export class DataPayloadHandler implements PayloadHandler { 22 | 23 | pack(payload: Payload, transferables: Transferable[], cloneBuffers: boolean): Transferable[] { 24 | const dp = payload as DataPayload; 25 | if (dp.message.buffers) { 26 | fillTransferables(dp.message.buffers.values(), transferables, cloneBuffers); 27 | } 28 | return transferables; 29 | } 30 | 31 | unpack(transportObject: Payload, cloneBuffers: boolean): DataPayload { 32 | const dp = transportObject as DataPayload; 33 | const dtp = Object.assign(new DataPayload(), transportObject); 34 | if (dp.message.buffers) { 35 | for (const [name, buffer] of dp.message.buffers.entries()) { 36 | if (dtp.message.buffers) { 37 | dtp.message.buffers.set(name, cloneBuffers ? buffer.slice(0) : buffer); 38 | } 39 | } 40 | } 41 | return dtp; 42 | } 43 | } 44 | 45 | // register the default handler 46 | PayloadRegister.handler.set('DataPayload', new DataPayloadHandler()); 47 | -------------------------------------------------------------------------------- /packages/examples/src/helloWorld/HelloWorldComChannelEndpoint.ts: -------------------------------------------------------------------------------- 1 | import { WorkerMessage, ComChannelEndpoint, RawPayload } from 'wtd-core'; 2 | 3 | /** 4 | * Hello World example just using the ComChannelEndpoint 5 | */ 6 | class HelloWorldComChannelEndpointExample { 7 | 8 | async run() { 9 | const url = new URL(import.meta.env.DEV ? '../worker/HelloWorldComChannelEndpointWorker.ts' : '../worker/generated/HelloWorldComChannelEndpointWorker-es.js', import.meta.url); 10 | const endpoint = new ComChannelEndpoint({ 11 | endpointName: 'HelloWorldWorker', 12 | endpointId: 1, 13 | endpointConfig: { 14 | $type: 'WorkerConfigParams', 15 | url, 16 | workerType: 'module', 17 | }, 18 | verbose: true 19 | }); 20 | endpoint.connect(); 21 | 22 | try { 23 | const t0 = performance.now(); 24 | 25 | const result = await endpoint.sentMessage({ 26 | message: WorkerMessage.createNew({ 27 | cmd: 'hello_world' 28 | }), 29 | awaitAnswer: true, 30 | expectedAnswer: 'hello_world_confirm' 31 | },); 32 | 33 | const rawPayload = result.payloads[0] as RawPayload; 34 | const answer = `Worker said: command: ${result.cmd} message: ${rawPayload.message.raw?.hello}`; 35 | const t1 = performance.now(); 36 | 37 | const msg = `${answer}\nWorker execution has been completed after ${t1 - t0}ms.`; 38 | console.log(msg); 39 | alert(msg); 40 | } catch (e) { 41 | console.error(e); 42 | } 43 | } 44 | } 45 | 46 | const app = new HelloWorldComChannelEndpointExample(); 47 | app.run(); 48 | -------------------------------------------------------------------------------- /packages/wtd-three-ext/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wtd-three-ext", 3 | "version": "4.0.1", 4 | "license": "MIT", 5 | "type": "module", 6 | "main": "./dist/index.js", 7 | "module": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "types": "./dist/index.d.ts", 12 | "default": "./dist/index.js" 13 | }, 14 | "./bundle": { 15 | "types": "./dist/index.d.ts", 16 | "default": "./bundle/index.js" 17 | } 18 | }, 19 | "typesVersions": { 20 | "*": { 21 | ".": [ 22 | "dist/index" 23 | ], 24 | "bundle": [ 25 | "dist/index" 26 | ] 27 | } 28 | }, 29 | "files": [ 30 | "dist", 31 | "bundle", 32 | "src", 33 | "LICENSE", 34 | "README.md" 35 | ], 36 | "scripts": { 37 | "clean": "shx rm -fr *.tsbuildinfo dist bundle", 38 | "doc": "shx rm -fr docs && typedoc --plugin typedoc-plugin-markdown --out docs src/index.ts", 39 | "compile": "tsc -b", 40 | "build:bundle": "vite --config vite.bundle.config.ts build", 41 | "build": "npm run clean && npm run compile && npm run build:bundle" 42 | }, 43 | "volta": { 44 | "node": "20.17.0", 45 | "npm": "10.8.3" 46 | }, 47 | "dependencies": { 48 | "wtd-core": "~4.0.1", 49 | "three": "~0.169.0" 50 | }, 51 | "devDependencies": { 52 | "@types/three": "~0.169.0" 53 | }, 54 | "peerDependencies": { 55 | "three": ">= 0.137.5 < 1" 56 | }, 57 | "repository": { 58 | "type": "git", 59 | "url": "https://github.com/kaisalmen/wtd", 60 | "directory": "packages/wtd" 61 | }, 62 | "homepage": "https://github.com/kaisalmen/wtd/blob/main/packages/wtd-three-ext/README.md", 63 | "bugs": "https://github.com/kaisalmen/wtd/issues", 64 | "author": { 65 | "name": "kaisalmen", 66 | "url": "https://www.kaisalmen.de" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/examples/scripts/buildWorker.mts: -------------------------------------------------------------------------------- 1 | import shell from 'shelljs'; 2 | 3 | shell.rm('-f', './src/worker/generated/HelloWorldWorker*.js'); 4 | shell.exec('vite -c build/vite.config.HelloWorldWorker.ts build'); 5 | 6 | shell.rm('-f', './src/worker/generated/HelloWorldComChannelEndpointWorker*.js'); 7 | shell.exec('vite -c build/vite.config.HelloWorldComChannelEndpointWorker.ts build'); 8 | 9 | shell.rm('-f', './src/worker/generated/Com1Worker*.js'); 10 | shell.exec('vite -c build/vite.config.Com1Worker.ts build'); 11 | 12 | shell.rm('-f', './src/worker/generated/Com2Worker*.js'); 13 | shell.exec('vite -c build/vite.config.Com2Worker.ts build'); 14 | 15 | shell.rm('-f', './src/worker/generated/HelloWorldThreeWorker*.js'); 16 | shell.exec('vite -c build/vite.config.HelloWorldThreeWorker.ts build'); 17 | 18 | shell.rm('-f', './src/worker/generated/InfiniteWorkerExternalGeometry*.js'); 19 | shell.exec('vite -c build/vite.config.InfiniteWorkerExternalGeometry.ts build'); 20 | 21 | shell.rm('-f', './src/worker/generated/InfiniteWorkerInternalGeometry*.js'); 22 | shell.exec('vite -c build/vite.config.InfiniteWorkerInternalGeometry.ts build'); 23 | 24 | shell.rm('-f', './src/worker/generated/OBJLoaderWorker*.js'); 25 | shell.exec('vite -c build/vite.config.OBJLoaderWorker.ts build'); 26 | 27 | shell.rm('-f', './src/worker/generated/TransferableWorkerTest1*.js'); 28 | shell.exec('vite -c build/vite.config.TransferableWorkerTest1.ts build'); 29 | 30 | shell.rm('-f', './src/worker/generated/TransferableWorkerTest2*.js'); 31 | shell.exec('vite -c build/vite.config.TransferableWorkerTest2.ts build'); 32 | 33 | shell.rm('-f', './src/worker/generated/TransferableWorkerTest3*.js'); 34 | shell.exec('vite -c build/vite.config.TransferableWorkerTest3.ts build'); 35 | 36 | shell.rm('-f', './src/worker/generated/TransferableWorkerTest4*.js'); 37 | shell.exec('vite -c build/vite.config.TransferableWorkerTest4.ts build'); 38 | -------------------------------------------------------------------------------- /packages/examples/src/worker/TransferableWorkerTest2.ts: -------------------------------------------------------------------------------- 1 | import { 2 | comRouting, 3 | DataPayload, 4 | WorkerTaskCommandResponse, 5 | WorkerMessage, 6 | WorkerTaskWorker 7 | } from 'wtd-core'; 8 | 9 | class TransferableWorkerTest2 implements WorkerTaskWorker { 10 | 11 | init(message: WorkerMessage) { 12 | console.log(`TransferableWorkerTest2#init: name: ${message.name} uuid: ${message.uuid} cmd: ${message.cmd} workerId: ${message.endpointdId}`); 13 | 14 | const initComplete = WorkerMessage.createFromExisting(message, { 15 | overrideCmd: WorkerTaskCommandResponse.INIT_COMPLETE 16 | }); 17 | self.postMessage(initComplete); 18 | } 19 | 20 | execute(message: WorkerMessage) { 21 | console.log(`TransferableWorkerTest2#execute: name: ${message.name} uuid: ${message.uuid} cmd: ${message.cmd} workerId: ${message.endpointdId}`); 22 | 23 | const wm = WorkerMessage.unpack(message, false); 24 | if (wm.payloads.length === 1) { 25 | const payload = wm.payloads[0] as DataPayload; 26 | if (payload.message.params !== undefined) { 27 | const payloadOut = new DataPayload(); 28 | payloadOut.message.buffers?.set('data', new Uint32Array(32 * 1024 * 1024)); 29 | 30 | const execComplete = WorkerMessage.createFromExisting(wm, { 31 | overrideCmd: WorkerTaskCommandResponse.EXECUTE_COMPLETE 32 | }); 33 | execComplete.name = payload.message.params.name as string; 34 | execComplete.addPayload(payloadOut); 35 | 36 | const transferables = WorkerMessage.pack(execComplete.payloads, false); 37 | self.postMessage(execComplete, transferables); 38 | } 39 | } 40 | } 41 | } 42 | 43 | const worker = new TransferableWorkerTest2(); 44 | self.onmessage = message => comRouting(worker, message); 45 | -------------------------------------------------------------------------------- /packages/examples/src/worker/TransferableWorkerTest4.ts: -------------------------------------------------------------------------------- 1 | import { TorusKnotGeometry } from 'three'; 2 | import { 3 | comRouting, 4 | DataPayload, 5 | WorkerTaskCommandResponse, 6 | WorkerMessage, 7 | WorkerTaskWorker 8 | } from 'wtd-core'; 9 | import { 10 | MeshPayload 11 | } from 'wtd-three-ext'; 12 | 13 | class TransferableWorkerTest4 implements WorkerTaskWorker { 14 | 15 | init(message: WorkerMessage) { 16 | console.log(`TransferableWorkerTest4#init: name: ${message.name} uuid: ${message.uuid} cmd: ${message.cmd} workerId: ${message.endpointdId}`); 17 | message.cmd = WorkerTaskCommandResponse.INIT_COMPLETE; 18 | self.postMessage(message); 19 | } 20 | 21 | execute(message: WorkerMessage) { 22 | console.log(`TransferableWorkerTest4#execute: name: ${message.name} uuid: ${message.uuid} cmd: ${message.cmd} workerId: ${message.endpointdId}`); 23 | 24 | const wm = WorkerMessage.unpack(message, false); 25 | if (wm.payloads.length === 1) { 26 | const payload = wm.payloads[0] as DataPayload; 27 | const bufferGeometry = new TorusKnotGeometry(20, 3, payload.message.params?.segments as number, payload.message.params?.segments as number); 28 | bufferGeometry.name = wm.name; 29 | 30 | const meshPayload = new MeshPayload(); 31 | meshPayload.setBufferGeometry(bufferGeometry, 0); 32 | 33 | const execComplete = WorkerMessage.createFromExisting(wm, { 34 | overrideCmd: WorkerTaskCommandResponse.EXECUTE_COMPLETE 35 | }); 36 | execComplete.addPayload(meshPayload); 37 | 38 | const transferables = WorkerMessage.pack(execComplete.payloads, false); 39 | self.postMessage(execComplete, transferables); 40 | } 41 | } 42 | 43 | } 44 | 45 | const worker = new TransferableWorkerTest4(); 46 | self.onmessage = message => comRouting(worker, message); 47 | -------------------------------------------------------------------------------- /packages/examples/src/worker/HelloWorldThreeWorker.ts: -------------------------------------------------------------------------------- 1 | import { SphereGeometry } from 'three'; 2 | import { 3 | comRouting, 4 | WorkerTaskCommandResponse, 5 | WorkerMessage, 6 | WorkerTaskWorker 7 | } from 'wtd-core'; 8 | import { 9 | MeshPayload 10 | } from 'wtd-three-ext'; 11 | 12 | export class HelloWorlThreedWorker implements WorkerTaskWorker { 13 | 14 | init(message: WorkerMessage) { 15 | console.log(`HelloWorldWorker#init: name: ${message.name} uuid: ${message.uuid} cmd: ${message.cmd} workerId: ${message.endpointdId}`); 16 | 17 | const initComplete = WorkerMessage.createFromExisting(message, { 18 | overrideCmd: WorkerTaskCommandResponse.INIT_COMPLETE 19 | }); 20 | self.postMessage(initComplete); 21 | } 22 | 23 | execute(message: WorkerMessage) { 24 | console.log(`HelloWorldWorker#execute: name: ${message.name} uuid: ${message.uuid} cmd: ${message.cmd} workerId: ${message.endpointdId}`); 25 | 26 | const bufferGeometry = new SphereGeometry(40, 64, 64); 27 | bufferGeometry.name = `${message.name}-${message.uuid}`; 28 | const vertexArray = bufferGeometry.getAttribute('position').array; 29 | for (let i = 0; i < vertexArray.length; i++) { 30 | vertexArray[i] = vertexArray[i] * Math.random() * 0.48; 31 | } 32 | 33 | const meshPayload = new MeshPayload(); 34 | meshPayload.setBufferGeometry(bufferGeometry, 0); 35 | 36 | const execComplete = WorkerMessage.createFromExisting(message, { 37 | overrideCmd: WorkerTaskCommandResponse.EXECUTE_COMPLETE 38 | }); 39 | execComplete.addPayload(meshPayload); 40 | 41 | const transferables = WorkerMessage.pack(execComplete.payloads, false); 42 | self.postMessage(execComplete, transferables); 43 | } 44 | 45 | } 46 | 47 | const worker = new HelloWorlThreedWorker(); 48 | self.onmessage = message => comRouting(worker, message); 49 | -------------------------------------------------------------------------------- /packages/examples/src/helloWorld/HelloWorldWorkerTaskDirector.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RawPayload, 3 | WorkerTaskDirector, 4 | WorkerMessage 5 | } from 'wtd-core'; 6 | 7 | /** 8 | * Hello World example using a module worker 9 | */ 10 | class HelloWorldModuleWorkerExample { 11 | 12 | private workerTaskDirector: WorkerTaskDirector = new WorkerTaskDirector({ 13 | defaultMaxParallelExecutions: 1, 14 | verbose: true 15 | }); 16 | 17 | async run() { 18 | const taskName = 'HelloWorldWorker'; 19 | 20 | // register the module worker 21 | this.workerTaskDirector.registerTask({ 22 | taskName, 23 | endpointConfig: { 24 | $type: 'WorkerConfigParams', 25 | workerType: 'module', 26 | blob: false, 27 | url: new URL(import.meta.env.DEV ? '../worker/HelloWorldWorker.ts' : '../worker/generated/HelloWorldWorker-es.js', import.meta.url), 28 | 29 | } 30 | }); 31 | 32 | try { 33 | // init the director without any payload (worker init without function invocation on worker) 34 | await this.workerTaskDirector.initTaskType(taskName); 35 | 36 | // execute worker without init 37 | const t0 = performance.now(); 38 | const resultExec = await this.workerTaskDirector.enqueueForExecution(taskName, { 39 | message: WorkerMessage.createEmpty(), 40 | }); 41 | 42 | const rawPayload = resultExec.payloads[0] as RawPayload; 43 | const answer = `Worker said: ${rawPayload.message.raw?.hello}`; 44 | const t1 = performance.now(); 45 | 46 | const msg = `${answer}\nWorker execution has been completed after ${t1 - t0}ms.`; 47 | console.log(msg); 48 | alert(msg); 49 | } catch (e) { 50 | console.error(e); 51 | } 52 | } 53 | } 54 | 55 | const app = new HelloWorldModuleWorkerExample(); 56 | app.run(); 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wtd-workspace", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "description": "Worker Task Director Workspace", 6 | "author": { 7 | "name": "kaisalmen", 8 | "url": "https://www.kaisalmen.de" 9 | }, 10 | "private": "true", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/kaisalmen/wtd.git" 15 | }, 16 | "bugs": { 17 | "url": "https://github.com/kaisalmen/wtd/issues" 18 | }, 19 | "scripts": { 20 | "clean": "npm run clean --workspaces", 21 | "lint": "eslint {**/src/**/*.ts,**/src/**/*.tsx,**/test/**/*.ts,**/test/**/*.tsx}", 22 | "lint:fix": "eslint eslint {**/src/**/*.ts,**/src/**/*.tsx,**/test/**/*.ts,**/test/**/*.tsx} --fix", 23 | "doc": "npm run doc --workspaces", 24 | "compile": "tsc --build tsconfig.build.json", 25 | "watch": "tsc --build tsconfig.build.json --watch", 26 | "build": "npm run build --workspaces", 27 | "build:production": "npm run build:production --workspace packages/examples", 28 | "dev": "vite", 29 | "dev:debug": "vite --debug --force", 30 | "release:prepare": "npm run reset:repo && npm ci && npm run lint && npm run build && npm run doc && npm run build:production", 31 | "test": "vitest", 32 | "test:run": "vitest --run", 33 | "reset:repo:dry": "git clean -f -d -x --dry-run", 34 | "reset:repo": "git clean -f -d -x" 35 | }, 36 | "keywords": [], 37 | "homepage": "https://github.com/kaisalmen/wtd#README", 38 | "volta": { 39 | "node": "20.17.0", 40 | "npm": "10.8.3" 41 | }, 42 | "devDependencies": { 43 | "@types/node": "~20.16.10", 44 | "@typescript-eslint/eslint-plugin": "~7.18.0", 45 | "@typescript-eslint/parser": "~7.18.0", 46 | "@vitest/browser": "~2.1.1", 47 | "editorconfig": "~2.0.0", 48 | "eslint": "~8.57.0", 49 | "shx": "~0.3.4", 50 | "typedoc": "~0.26.7", 51 | "typedoc-plugin-markdown": "~4.2.8", 52 | "typescript": "~5.6.2", 53 | "vite": "~5.4.8", 54 | "vitest": "~2.1.1", 55 | "webdriverio": "~9.1.2" 56 | }, 57 | "workspaces": [ 58 | "packages/wtd-core", 59 | "packages/wtd-three-ext", 60 | "packages/examples" 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /packages/wtd-core/src/WorkerTaskWorker.ts: -------------------------------------------------------------------------------- 1 | import { Payload } from './Payload.js'; 2 | import { RawPayload } from './RawPayload.js'; 3 | import { WorkerMessage } from './WorkerMessage.js'; 4 | 5 | export enum WorkerTaskCommandRequest { 6 | INIT = 'init', 7 | INIT_CHANNEL = 'initChannel', 8 | INTERMEDIATE = 'intermediate', 9 | EXECUTE = 'execute', 10 | INTERCOM_INIT = 'interComInit', 11 | INTERCOM_INTERMEDIATE = 'interComIntermediate', 12 | INTERCOM_EXECUTE = 'interComExecute', 13 | } 14 | 15 | export enum WorkerTaskCommandResponse { 16 | INIT_COMPLETE = 'initComplete', 17 | INIT_CHANNEL_COMPLETE = 'initChannelComplete', 18 | INTERMEDIATE_CONFIRM = 'intermediateConfirm', 19 | EXECUTE_COMPLETE = 'executeComplete', 20 | INTERCOM_INIT_COMPLETE = 'interComInitComplete', 21 | INTERCOM_INTERMEDIATE_CONFIRM = 'interComIntermediateConfirm', 22 | INTERCOM_EXECUTE_COMPLETE = 'interComExecuteComplete' 23 | } 24 | 25 | export interface WorkerTaskWorker { 26 | init?(message: WorkerMessage): void; 27 | initChannel?(message: WorkerMessage): void; 28 | intermediate?(message: WorkerMessage): void; 29 | execute?(message: WorkerMessage): void; 30 | } 31 | 32 | export interface InterComWorker { 33 | interComInit?(message: WorkerMessage): void; 34 | interComInitComplete?(message: WorkerMessage): void; 35 | interComIntermediate?(message: WorkerMessage): void; 36 | interComIntermediateConfirm?(message: WorkerMessage): void; 37 | interComExecute?(message: WorkerMessage): void; 38 | interComExecuteComplete?(message: WorkerMessage): void; 39 | } 40 | 41 | export class InterComPortHandler { 42 | 43 | private ports: Map = new Map(); 44 | 45 | registerPort(name: string, payload: Payload | undefined, onmessage: (message: MessageEvent) => void) { 46 | const port = payload ? (payload as RawPayload).message.raw.port as MessagePort : undefined; 47 | if (!port) { 48 | throw new Error(`${payload?.message ?? 'undefined'} is not a RawPayload. Unable to extract a port.`); 49 | } 50 | this.ports.set(name, port); 51 | port.onmessage = onmessage; 52 | } 53 | 54 | postMessageOnPort(target: string, message: WorkerMessage, options?: StructuredSerializeOptions) { 55 | this.ports.get(target)?.postMessage(message, options); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/examples/src/worker/InfiniteWorkerInternalGeometry.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TorusKnotGeometry, 3 | Color, 4 | MeshPhongMaterial 5 | } from 'three'; 6 | import { 7 | comRouting, 8 | WorkerTaskCommandResponse, 9 | WorkerMessage, 10 | WorkerTaskWorker 11 | } from 'wtd-core'; 12 | import { 13 | MaterialUtils, 14 | MeshPayload, 15 | MaterialsPayload, 16 | } from 'wtd-three-ext'; 17 | 18 | class InfiniteWorkerInternalGeometry implements WorkerTaskWorker { 19 | 20 | init(message: WorkerMessage) { 21 | const initComplete = WorkerMessage.createFromExisting(message, { 22 | overrideCmd: WorkerTaskCommandResponse.INIT_COMPLETE 23 | }); 24 | self.postMessage(initComplete); 25 | } 26 | 27 | execute(message: WorkerMessage) { 28 | const bufferGeometry = new TorusKnotGeometry(20, 3, 100, 64); 29 | bufferGeometry.name = 'tmProto' + message.uuid; 30 | 31 | const vertexBA = bufferGeometry.getAttribute('position'); 32 | const vertexArray = vertexBA.array; 33 | for (let i = 0; i < vertexArray.length; i++) { 34 | vertexArray[i] = vertexArray[i] + 10 * (Math.random() - 0.5); 35 | } 36 | 37 | const randArray = new Uint8Array(3); 38 | self.crypto.getRandomValues(randArray); 39 | const color = new Color(); 40 | color.r = randArray[0] / 255; 41 | color.g = randArray[1] / 255; 42 | color.b = randArray[2] / 255; 43 | const material = new MeshPhongMaterial({ color: color }); 44 | 45 | const materialsPayload = new MaterialsPayload(); 46 | MaterialUtils.addMaterial(materialsPayload.message.materials, 'randomColor' + message.uuid, material, false, false); 47 | materialsPayload.cleanMaterials(); 48 | 49 | const meshPayload = new MeshPayload(); 50 | meshPayload.setBufferGeometry(bufferGeometry, 2); 51 | 52 | const execComplete = WorkerMessage.createFromExisting(message, { 53 | overrideCmd: WorkerTaskCommandResponse.EXECUTE_COMPLETE 54 | }); 55 | execComplete.addPayload(meshPayload); 56 | execComplete.addPayload(materialsPayload); 57 | 58 | const transferables = WorkerMessage.pack(execComplete.payloads, false); 59 | self.postMessage(execComplete, transferables); 60 | } 61 | } 62 | 63 | const worker = new InfiniteWorkerInternalGeometry(); 64 | self.onmessage = message => comRouting(worker, message); 65 | -------------------------------------------------------------------------------- /packages/examples/src/worker/TransferableWorkerTest3.ts: -------------------------------------------------------------------------------- 1 | import { BufferGeometry } from 'three'; 2 | import { 3 | comRouting, 4 | DataPayload, 5 | WorkerTaskCommandResponse, 6 | WorkerMessage, 7 | WorkerTaskWorker 8 | } from 'wtd-core'; 9 | import { 10 | MeshPayload, 11 | packGeometryBuffers 12 | } from 'wtd-three-ext'; 13 | 14 | class TransferableWorkerTest3 implements WorkerTaskWorker { 15 | 16 | private context = { 17 | initPayload: undefined as MeshPayload | undefined 18 | }; 19 | 20 | init(message: WorkerMessage) { 21 | console.log(`TransferableWorkerTest3#init: name: ${message.name} uuid: ${message.uuid} cmd: ${message.cmd} workerId: ${message.endpointdId}`); 22 | 23 | const wm = WorkerMessage.unpack(message, false); 24 | if (wm.payloads.length > 0) { 25 | this.context.initPayload = wm.payloads[0] as MeshPayload; 26 | } 27 | 28 | const initComplete = WorkerMessage.createFromExisting(wm, { 29 | overrideCmd: WorkerTaskCommandResponse.INIT_COMPLETE 30 | }); 31 | self.postMessage(initComplete); 32 | } 33 | 34 | execute(message: WorkerMessage) { 35 | console.log(`TransferableWorkerTest3#execute: name: ${message.name} uuid: ${message.uuid} cmd: ${message.cmd} workerId: ${message.endpointdId}`); 36 | 37 | if (this.context.initPayload !== undefined) { 38 | const wm = WorkerMessage.unpack(message, false); 39 | 40 | // just put the buffers into the buffers of a DataPayload 41 | const bufferGeometry = this.context.initPayload.message.bufferGeometry; 42 | const dataPayload = new DataPayload(); 43 | dataPayload.message.params = { 44 | geometry: this.context.initPayload.message.bufferGeometry 45 | }; 46 | if (bufferGeometry !== undefined && dataPayload.message.buffers !== undefined) { 47 | packGeometryBuffers(false, bufferGeometry as BufferGeometry, dataPayload.message.buffers); 48 | } 49 | 50 | const execComplete = WorkerMessage.createFromExisting(wm, { 51 | overrideCmd: WorkerTaskCommandResponse.EXECUTE_COMPLETE 52 | }); 53 | execComplete.addPayload(dataPayload); 54 | 55 | const transferables = WorkerMessage.pack(execComplete.payloads, false); 56 | self.postMessage(execComplete, transferables); 57 | } 58 | } 59 | } 60 | 61 | const worker = new TransferableWorkerTest3(); 62 | self.onmessage = message => comRouting(worker, message); 63 | -------------------------------------------------------------------------------- /packages/examples/src/worker/InfiniteWorkerExternalGeometry.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BufferGeometry 3 | } from 'three'; 4 | import { 5 | WorkerTaskCommandResponse, 6 | WorkerMessage, 7 | WorkerTaskWorker, 8 | comRouting 9 | } from 'wtd-core'; 10 | import { 11 | MeshPayload 12 | } from 'wtd-three-ext'; 13 | 14 | class InfiniteWorkerExternalGeometry implements WorkerTaskWorker { 15 | 16 | private bufferGeometry?: BufferGeometry = undefined; 17 | 18 | init(message: WorkerMessage) { 19 | const wm = WorkerMessage.unpack(message, false); 20 | if (wm.payloads.length > 0) { 21 | this.bufferGeometry = (wm.payloads[0] as MeshPayload).message.bufferGeometry as BufferGeometry; 22 | } 23 | 24 | const initComplete = WorkerMessage.createFromExisting(message, { 25 | overrideCmd: WorkerTaskCommandResponse.INIT_COMPLETE 26 | }); 27 | self.postMessage(initComplete); 28 | } 29 | 30 | execute(message: WorkerMessage) { 31 | if (!this.bufferGeometry) { 32 | self.postMessage(new Error('No initial payload available')); 33 | } else { 34 | // clone before re-using as othewise transferables can not be obtained 35 | const geometry = this.bufferGeometry.clone(); 36 | 37 | geometry.name = 'tmProto' + message.uuid; 38 | 39 | const vertexArray = geometry.getAttribute('position').array; 40 | for (let i = 0; i < vertexArray.length; i++) { 41 | vertexArray[i] = vertexArray[i] + 10 * (Math.random() - 0.5); 42 | } 43 | 44 | const meshPayload = new MeshPayload(); 45 | meshPayload.setBufferGeometry(geometry, 2); 46 | 47 | const randArray = new Uint8Array(3); 48 | self.crypto.getRandomValues(randArray); 49 | meshPayload.message.params = { 50 | color: { 51 | r: randArray[0] / 255, 52 | g: randArray[1] / 255, 53 | b: randArray[2] / 255 54 | } 55 | }; 56 | 57 | const execComplete = WorkerMessage.createFromExisting(message, { 58 | overrideCmd: WorkerTaskCommandResponse.EXECUTE_COMPLETE 59 | }); 60 | execComplete.addPayload(meshPayload); 61 | 62 | const transferables = WorkerMessage.pack(execComplete.payloads, false); 63 | self.postMessage(execComplete, transferables); 64 | } 65 | } 66 | } 67 | 68 | const worker = new InfiniteWorkerExternalGeometry(); 69 | self.onmessage = message => comRouting(worker, message); 70 | -------------------------------------------------------------------------------- /packages/wtd-core/test/DataPayload.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { applyProperties } from 'wtd-core'; 3 | 4 | describe('DataPayload applyProperties Tests', () => { 5 | test('verify empty', () => { 6 | const objToAlter = { 7 | 'alpha': 2 8 | }; 9 | const params = {}; 10 | applyProperties(objToAlter, params, false); 11 | expect(objToAlter).toEqual(objToAlter); 12 | }); 13 | 14 | test('verify props', () => { 15 | const objToAlter = { 16 | 'alpha': 2 17 | }; 18 | const paramsWithProps = { 19 | 'alpha': 5 20 | }; 21 | const objToAlterTarget = { 22 | 'alpha': 5 23 | }; 24 | applyProperties(objToAlter, paramsWithProps, false); 25 | expect(objToAlter).toEqual(objToAlterTarget); 26 | }); 27 | 28 | test('verify props, second level', () => { 29 | const objToAlter = { 30 | 'alpha': 2, 31 | 'beta': undefined 32 | }; 33 | const paramsWithPropsL2 = { 34 | 'alpha': 7, 35 | 'beta': { 36 | 'beta2': true 37 | } 38 | }; 39 | const objToAlterTarget = { 40 | 'alpha': 7, 41 | 'beta': { 42 | 'beta2': true 43 | } 44 | }; 45 | applyProperties(objToAlter, paramsWithPropsL2, false); 46 | expect(objToAlter).toEqual(objToAlterTarget); 47 | }); 48 | 49 | class Class4ApplyProperties { 50 | private coffee: string; 51 | private sugar: number; 52 | private milk: string; 53 | 54 | constructor(coffee: string, sugar: number, milk: string) { 55 | this.coffee = coffee; 56 | this.sugar = sugar; 57 | this.milk = milk; 58 | } 59 | 60 | getCoffee() { 61 | return this.coffee; 62 | } 63 | 64 | getSugar() { 65 | return this.sugar; 66 | } 67 | 68 | getMilk() { 69 | return this.milk; 70 | } 71 | } 72 | 73 | test('verify class', () => { 74 | const testClassRef = new Class4ApplyProperties('colombian', 2, '0ml'); 75 | const testClassAlter = new Class4ApplyProperties('colombian', 2, '0ml'); 76 | const paramsWithProps = { 77 | 'milk': '20ml', 78 | 'sugar': 1 79 | }; 80 | const testClassTarget = new Class4ApplyProperties('colombian', 1, '20ml'); 81 | 82 | applyProperties(testClassAlter, paramsWithProps, false); 83 | expect(testClassAlter).toEqual(testClassTarget); 84 | expect(testClassRef).not.toEqual(testClassAlter); 85 | expect(testClassRef).not.toEqual(testClassTarget); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /packages/wtd-three-ext/src/MaterialStore.ts: -------------------------------------------------------------------------------- 1 | import { MaterialUtils } from './MaterialUtils.js'; 2 | import { 3 | MeshStandardMaterial, 4 | LineBasicMaterial, 5 | PointsMaterial, 6 | Material 7 | } from 'three'; 8 | 9 | export type AssociatedMaterialArrayType = { [key: string]: Material } 10 | 11 | /** 12 | * Helper class around an object storing materials by name. 13 | * Optionally, create and store default materials. 14 | */ 15 | export class MaterialStore { 16 | 17 | private materials: Map; 18 | 19 | /** 20 | * Creates a new {@link MaterialStore}. 21 | * @param {boolean} createDefaultMaterials 22 | */ 23 | constructor(createDefaultMaterials: boolean) { 24 | this.materials = new Map(); 25 | if (createDefaultMaterials) { 26 | const defaultMaterial = new MeshStandardMaterial({ color: 0xDCF1FF }); 27 | defaultMaterial.name = 'defaultMaterial'; 28 | 29 | const defaultVertexColorMaterial = new MeshStandardMaterial({ color: 0xDCF1FF }); 30 | defaultVertexColorMaterial.name = 'defaultVertexColorMaterial'; 31 | defaultVertexColorMaterial.vertexColors = true; 32 | 33 | const defaultLineMaterial = new LineBasicMaterial(); 34 | defaultLineMaterial.name = 'defaultLineMaterial'; 35 | 36 | const defaultPointMaterial = new PointsMaterial({ size: 0.1 }); 37 | defaultPointMaterial.name = 'defaultPointMaterial'; 38 | 39 | this.materials.set(defaultMaterial.name, defaultMaterial); 40 | this.materials.set(defaultVertexColorMaterial.name, defaultVertexColorMaterial); 41 | this.materials.set(defaultLineMaterial.name, defaultLineMaterial); 42 | this.materials.set(defaultPointMaterial.name, defaultPointMaterial); 43 | } 44 | } 45 | 46 | /** 47 | * Set materials loaded by any supplier of an Array of {@link Material}. 48 | * 49 | * @param {Map} newMaterials Object with named {@link Material} 50 | * @param {boolean} forceOverrideExisting boolean Override existing material 51 | */ 52 | addMaterials(newMaterials: Map, forceOverrideExisting: boolean) { 53 | if (newMaterials.size > 0) { 54 | for (const entry of newMaterials.entries()) { 55 | MaterialUtils.addMaterial(this.materials, entry[0], entry[1], forceOverrideExisting === true); 56 | } 57 | } 58 | } 59 | 60 | addMaterialsFromObject(newMaterials: AssociatedMaterialArrayType, forceOverrideExisting: boolean) { 61 | if (Object.keys(newMaterials).length > 0) { 62 | for (const [k, v] of Object.entries(newMaterials)) { 63 | MaterialUtils.addMaterial(this.materials, k, v, forceOverrideExisting === true); 64 | } 65 | } 66 | } 67 | 68 | getMaterials(): Map { 69 | return this.materials; 70 | } 71 | 72 | getMaterial(materialName: string): Material | undefined { 73 | return this.materials.get(materialName); 74 | } 75 | 76 | clearMaterials() { 77 | this.materials.clear(); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /packages/wtd-core/src/WorkerMessage.ts: -------------------------------------------------------------------------------- 1 | import { Payload, PayloadRegister } from './Payload.js'; 2 | 3 | export interface WorkerMessageConfig { 4 | cmd?: WorkerCommand; 5 | name?: string; 6 | endpointdId?: number; 7 | payloads?: Payload[]; 8 | answer?: boolean; 9 | } 10 | 11 | export type WorkerCommand = string | 'unknown'; 12 | 13 | export class WorkerMessage { 14 | cmd: WorkerCommand = 'unknown'; 15 | uuid: string = 'unknown'; 16 | name = 'unnamed'; 17 | endpointdId = 0; 18 | payloads: Payload[] = []; 19 | answer?: boolean = false; 20 | 21 | constructor(config?: WorkerMessageConfig) { 22 | this.cmd = config?.cmd ?? this.cmd; 23 | this.name = config?.name ?? this.name; 24 | this.endpointdId = config?.endpointdId ?? this.endpointdId; 25 | this.answer = config?.answer ?? this.answer; 26 | } 27 | 28 | addPayload(payloads?: Payload[] | Payload) { 29 | if (!payloads) return; 30 | 31 | if (Array.isArray(payloads)) { 32 | this.payloads = this.payloads.concat(payloads); 33 | } else { 34 | this.payloads.push(payloads); 35 | } 36 | } 37 | 38 | static createNew(message: WorkerMessageConfig) { 39 | return new WorkerMessage(message); 40 | } 41 | 42 | static createEmpty() { 43 | return WorkerMessage.createNew({}); 44 | } 45 | 46 | static createFromExisting(message: WorkerMessage, options?: { 47 | overrideCmd?: WorkerCommand, 48 | overrideUuid?: string, 49 | overridePayloads?: Payload | Payload[], 50 | answer?: boolean 51 | }) { 52 | const wm = WorkerMessage.createNew(message); 53 | wm.uuid = message.uuid; 54 | if (options?.overrideCmd !== undefined) { 55 | wm.cmd = options.overrideCmd; 56 | } 57 | if (options?.overrideUuid !== undefined) { 58 | wm.uuid = options.overrideUuid; 59 | } 60 | if (options?.answer !== undefined) { 61 | wm.answer = options.answer; 62 | } 63 | wm.addPayload(options?.overridePayloads); 64 | 65 | return wm; 66 | } 67 | 68 | static pack(payloads?: Payload[], cloneBuffers?: boolean): Transferable[] { 69 | const transferables: Transferable[] = []; 70 | if (payloads) { 71 | for (const payload of payloads) { 72 | const handler = PayloadRegister.handler.get(payload.$type); 73 | handler?.pack(payload, transferables, cloneBuffers === true); 74 | } 75 | } 76 | return transferables; 77 | } 78 | 79 | static unpack(rawMessage: WorkerMessage, cloneBuffers?: boolean) { 80 | const instance = WorkerMessage.createFromExisting(rawMessage, { 81 | overrideUuid: rawMessage.uuid 82 | }); 83 | 84 | for (const payload of rawMessage.payloads) { 85 | const handler = PayloadRegister.handler.get(payload.$type); 86 | instance.addPayload(handler?.unpack(payload, cloneBuffers === true)); 87 | } 88 | return instance; 89 | } 90 | 91 | static fromPayload(payloads: Payload | Payload[], cmd?: WorkerCommand) { 92 | const wm = WorkerMessage.createNew({ 93 | cmd 94 | }); 95 | wm.addPayload(payloads); 96 | return wm; 97 | } 98 | } 99 | 100 | export class WorkerTaskMessage extends WorkerMessage { 101 | 102 | } 103 | -------------------------------------------------------------------------------- /packages/wtd-three-ext/src/offscreen/WorkerEventProxy.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import { 3 | EventDispatcher 4 | } from 'three'; 5 | import { AssociatedArrayType } from 'wtd-core'; 6 | 7 | export const noop = () => { 8 | }; 9 | 10 | export class ElementProxyReceiver extends EventDispatcher { 11 | top = 0; 12 | left = 0; 13 | clientLeft = 0; 14 | clientTop = 0; 15 | pageXOffset = 0; 16 | pageYOffset = 0; 17 | style = {}; 18 | ownerDocument = { 19 | documentElement: {} 20 | }; 21 | offscreenCanvas: OffscreenCanvas = new OffscreenCanvas(100, 100); 22 | 23 | constructor() { 24 | super(); 25 | this.ownerDocument.documentElement = this; 26 | } 27 | 28 | merge(offscreenCanvas: OffscreenCanvas) { 29 | this.offscreenCanvas = offscreenCanvas; 30 | this.width = offscreenCanvas.width; 31 | this.height = offscreenCanvas.height; 32 | this.oncontextlost = offscreenCanvas.oncontextlost; 33 | this.oncontextrestored = offscreenCanvas.oncontextrestored; 34 | } 35 | 36 | oncontextlost: ((this: OffscreenCanvas, ev: Event) => any) | null = null; 37 | oncontextrestored: ((this: OffscreenCanvas, ev: Event) => any) | null = null; 38 | getContext(contextId: any, options?: any): OffscreenCanvasRenderingContext2D | null { 39 | return this.offscreenCanvas.getContext(contextId, options) ?? null; 40 | } 41 | transferToImageBitmap(): ImageBitmap { 42 | return this.offscreenCanvas.transferToImageBitmap(); 43 | } 44 | convertToBlob(options?: ImageEncodeOptions): Promise { 45 | return this.offscreenCanvas.convertToBlob(options); 46 | } 47 | 48 | get height() { 49 | return this.offscreenCanvas.height; 50 | } 51 | 52 | set height(value: number) { 53 | this.offscreenCanvas.height = value; 54 | } 55 | 56 | get width() { 57 | return this.offscreenCanvas.width; 58 | } 59 | 60 | set width(value: number) { 61 | this.offscreenCanvas.width = value; 62 | } 63 | 64 | get clientWidth() { 65 | return this.width; 66 | } 67 | 68 | get clientHeight() { 69 | return this.height; 70 | } 71 | 72 | setPointerCapture(_id: string) { 73 | noop(); 74 | } 75 | 76 | releasePointerCapture(_id: string) { 77 | noop(); 78 | } 79 | 80 | getBoundingClientRect() { 81 | return { 82 | left: this.left, 83 | top: this.top, 84 | width: this.width, 85 | height: this.height, 86 | right: this.left + this.width, 87 | bottom: this.top + this.height, 88 | }; 89 | } 90 | 91 | handleEvent(event: AssociatedArrayType) { 92 | event.preventDefault = noop; 93 | event.stopPropagation = noop; 94 | this.dispatchEvent(event as never); 95 | } 96 | 97 | focus() { 98 | noop(); 99 | } 100 | } 101 | 102 | export const proxyStart = (proxy: ElementProxyReceiver) => { 103 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 104 | self.window = proxy as any; 105 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 106 | (self as any).document = { 107 | addEventListener: proxy.addEventListener.bind(proxy), 108 | removeEventListener: proxy.removeEventListener.bind(proxy), 109 | }; 110 | }; 111 | -------------------------------------------------------------------------------- /packages/wtd-three-ext/src/MaterialUtils.ts: -------------------------------------------------------------------------------- 1 | import { Material } from 'three'; 2 | 3 | export type MaterialCloneInstructionsType = { 4 | materialNameOrg: string, 5 | materialProperties: { 6 | name: string, 7 | vertexColors: number, 8 | flatShading: boolean 9 | } 10 | }; 11 | 12 | /** 13 | * Static functions useful in the context of handling materials. 14 | */ 15 | export class MaterialUtils { 16 | 17 | /** 18 | * Adds the provided material to the provided map of materials if the material does not exists. 19 | * Use force override existing material. 20 | * 21 | * @param {Map} materialsObject 22 | * @param {string} materialName 23 | * @param {Material} material 24 | * @param {boolean} force Enforce addition of provided material 25 | * @param {boolean} [log] Log messages to the console 26 | */ 27 | static addMaterial(materialsObject: Map, materialName: string, material: Material, 28 | force: boolean, log?: boolean) { 29 | let existingMaterial; 30 | // ensure materialName is set 31 | material.name = materialName; 32 | if (!force) { 33 | existingMaterial = materialsObject.get(materialName); 34 | if (existingMaterial) { 35 | if (existingMaterial.uuid !== existingMaterial.uuid) { 36 | if (log === true) console.log('Same material name "' + existingMaterial.name + '" different uuid [' + existingMaterial.uuid + '|' + material.uuid + ']'); 37 | } 38 | } 39 | else { 40 | materialsObject.set(materialName, material); 41 | if (log === true) console.info('Material with name "' + materialName + '" was added.'); 42 | } 43 | } 44 | else { 45 | materialsObject.set(materialName, material); 46 | if (log === true) console.info('Material with name "' + materialName + '" was forcefully overridden.'); 47 | } 48 | } 49 | 50 | /** 51 | * Transforms the named materials object to an object with named jsonified materials. 52 | * 53 | * @param {Map} 54 | * @returns {Map} Map of Materials in JSON representation 55 | */ 56 | static getMaterialsJSON(materialsObject: Map): Map { 57 | const materialsJSON: Map = new Map(); 58 | for (const entry of materialsObject.entries()) { 59 | if (typeof entry[1].toJSON === 'function') { 60 | materialsJSON.set(entry[0], entry[1].toJSON()); 61 | } 62 | } 63 | return materialsJSON; 64 | } 65 | 66 | /** 67 | * Clones a material according the provided instructions. 68 | * 69 | * @param {Map} materials 70 | * @param {MaterialCloneInstructionsType} materialCloneInstruction 71 | * @param {boolean} [log] 72 | */ 73 | static cloneMaterial(materials: Map, materialCloneInstruction: MaterialCloneInstructionsType, log?: boolean): Material | undefined { 74 | 75 | const materialNameOrg = materialCloneInstruction.materialNameOrg; 76 | const materialOrg = materials.get(materialNameOrg); 77 | if (materialOrg) { 78 | const material = materialOrg.clone(); 79 | Object.assign(material, materialCloneInstruction.materialProperties); 80 | MaterialUtils.addMaterial(materials, materialCloneInstruction.materialProperties.name, material, true, log); 81 | return material; 82 | } 83 | else { 84 | if (log === true) console.info('Requested material "' + materialNameOrg + '" is not available!'); 85 | return undefined; 86 | } 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /packages/wtd-core/src/utilities.ts: -------------------------------------------------------------------------------- 1 | import { AssociatedArrayType } from './Payload.js'; 2 | import { RawPayload } from './RawPayload.js'; 3 | import { WorkerTask } from './WorkerTask.js'; 4 | import { WorkerMessage } from './WorkerMessage.js'; 5 | import { WorkerTaskCommandRequest, WorkerTaskCommandResponse } from './WorkerTaskWorker.js'; 6 | 7 | export const fillTransferables = (buffers: IterableIterator, transferables: Transferable[], cloneBuffers: boolean) => { 8 | for (const buffer of buffers) { 9 | if (cloneBuffers) { 10 | transferables.push(buffer.slice(0)); 11 | } else { 12 | if (Object.hasOwn(buffer, 'buffer')) { 13 | transferables.push(buffer); 14 | } 15 | } 16 | } 17 | }; 18 | 19 | /** 20 | * Applies values from parameter object via set functions or via direct assignment. 21 | * 22 | * @param {object} objToAlter The objToAlter instance 23 | * @param {AssociatedArrayType} params The parameter object 24 | * @param {boolean} forceCreation Force the creation of a property 25 | */ 26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 27 | export const applyProperties = (objToAlter: any, params?: AssociatedArrayType, forceCreation?: boolean) => { 28 | if (!params) return; 29 | 30 | for (const [k, v] of Object.entries(params)) { 31 | const funcName = 'set' + k.substring(0, 1).toLocaleUpperCase() + k.substring(1); 32 | 33 | if (Object.prototype.hasOwnProperty.call(objToAlter, funcName) && typeof objToAlter[funcName] === 'function') { 34 | objToAlter[funcName] = v; 35 | } 36 | else if (Object.prototype.hasOwnProperty.call(objToAlter, k) || forceCreation === true) { 37 | objToAlter[k] = v; 38 | } 39 | } 40 | }; 41 | 42 | export const createWorkerBlob = (code: string[]) => { 43 | const simpleWorkerBlob = new Blob(code, { type: 'application/javascript' }); 44 | return window.URL.createObjectURL(simpleWorkerBlob); 45 | }; 46 | 47 | export const initChannel = async (workerOne: WorkerTask, workerTwo: WorkerTask) => { 48 | const channel = new MessageChannel(); 49 | 50 | const promises = []; 51 | const payloadOne = new RawPayload({ 52 | port: channel.port1 53 | }); 54 | promises.push(workerOne.sentMessage({ 55 | message: WorkerMessage.fromPayload(payloadOne, WorkerTaskCommandRequest.INIT_CHANNEL), 56 | transferables: [channel.port1], 57 | awaitAnswer: true, 58 | expectedAnswer: WorkerTaskCommandResponse.INIT_CHANNEL_COMPLETE 59 | })); 60 | 61 | const payloadTwo = new RawPayload({ 62 | port: channel.port2 63 | }); 64 | promises.push(workerTwo.sentMessage({ 65 | message: WorkerMessage.fromPayload(payloadTwo, WorkerTaskCommandRequest.INIT_CHANNEL), 66 | transferables: [channel.port2], 67 | awaitAnswer: true, 68 | expectedAnswer: WorkerTaskCommandResponse.INIT_CHANNEL_COMPLETE 69 | })); 70 | return Promise.all(promises); 71 | }; 72 | 73 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 74 | export const comRouting = (workerImpl: any, message: MessageEvent) => { 75 | const data = (message as MessageEvent).data; 76 | if (Object.hasOwn(data, 'cmd')) { 77 | const wm = (message as MessageEvent).data as WorkerMessage; 78 | const funcName = wm.cmd; 79 | 80 | // only invoke if not flagged as amswer 81 | if (wm.answer === undefined || wm.answer === false) { 82 | if (typeof workerImpl[funcName] === 'function') { 83 | workerImpl[funcName](wm); 84 | } else { 85 | console.warn(`No function "${funcName}" found on workerImpl.`); 86 | } 87 | } 88 | } else { 89 | console.error(`Received: unknown message: ${message}`); 90 | } 91 | }; 92 | -------------------------------------------------------------------------------- /packages/wtd-core/README.md: -------------------------------------------------------------------------------- 1 | # Worker Task Director Library (wtd-core) 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/kaisalmen/wtd/blob/main/LICENSE) 4 | [![wtd](https://github.com/kaisalmen/wtd/actions/workflows/actions.yml/badge.svg)](https://github.com/kaisalmen/wtd/actions/workflows/actions.yml) 5 | [![Github Pages](https://img.shields.io/badge/GitHub-Pages-blue?logo=github)](https://kaisalmen.github.io/wtd) 6 | [![wtd-core version](https://img.shields.io/npm/v/wtd-core?logo=npm&label=wtd-core)](https://www.npmjs.com/package/wtd-core) 7 | 8 | Build applications with workers with less boiler plate code. 9 | 10 | - [Worker Task Director Library (wtd-core)](#worker-task-director-library-wtd-core) 11 | - [Examples](#examples) 12 | - [Usage](#usage) 13 | 14 | ## Examples 15 | 16 | There are multiple examples available demonstarting the features described above (listed from simpler to more advanced): 17 | 18 | - **ComChannelEndpoint: Hello World**: [html](https://github.com/kaisalmen/wtd/blob/main/packages/examples/helloWorldComChannelEndpoint.html), [ts](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/helloWorld/HelloWorldComChannelEndpoint.ts), [worker](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/HelloWorldComChannelEndpointWorker.ts) 19 | - **WorkerTask: Hello World**: [html](https://github.com/kaisalmen/wtd/blob/main/packages/examples/helloWorldWorkerTask.html), [ts](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/helloWorld/HelloWorldWorkerTask.ts), [worker](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/HelloWorldWorker.ts) 20 | - **WorkerTaskDirector: Hello World**: [html](https://github.com/kaisalmen/wtd/blob/main/packages/examples/helloWorldWorkerTaskDirector.html), [ts](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/helloWorld/helloWorldWorkerTaskDirector.ts), [worker](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/HelloWorldWorker.ts) 21 | - **WorkerTask: Inter-Worker Communication**: [html](https://github.com/kaisalmen/wtd/blob/main/packages/examples/workerCom.html), [ts](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/com/WorkerCom.ts), **Worker**: [1](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/Com1Worker.ts) and [2](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/Com2Worker.ts) 22 | 23 | Try out all examples here: 24 | 25 | ## Usage 26 | 27 | This shall give you an idea how you can use module worker with `WorkerTask` (derived from [WorkerTask: Hello World](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/helloWorld/HelloWorldWorkerTask.ts)): 28 | 29 | ```js 30 | // let WorkerTask create the worker 31 | const workerTask = new WorkerTask({ 32 | endpointName, 33 | endpointId: 1, 34 | endpointConfig: { 35 | $type: 'WorkerConfigParams', 36 | url: new URL('./HelloWorldWorker.js', import.meta.url), 37 | workerType: 'module', 38 | }, 39 | verbose: true 40 | }); 41 | 42 | try { 43 | // creates and connects the worker callback functions and the WorkerTask 44 | workerTask.connect(); 45 | 46 | // execute without init and an empty message 47 | const resultExec = await workerTask.executeWorker({ 48 | message: WorkerTaskMessage.createEmpty() 49 | }); 50 | 51 | // once you awaited the resulting WorkerTaskMessage extract the RawPayload 52 | const rawPayload = resultExec.payloads?.[0] as RawPayload; 53 | 54 | // log the hello from the HelloWorldWorker 55 | console.log(`Worker said: ${rawPayload.message.raw?.hello}`); 56 | } catch (e) { 57 | // error handling 58 | console.error(e); 59 | } 60 | ``` 61 | 62 | Further information is found in the main [README](https://github.com/kaisalmen/wtd/blob/main/README.md) of the overall [repository](https://github.com/kaisalmen/wtd). 63 | 64 | All changes are noted in the overall [CHANGELOG](https://github.com/kaisalmen/wtd/blob/main/CHANGELOG.md). 65 | -------------------------------------------------------------------------------- /packages/examples/src/worker/OBJLoaderWorker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Mesh, 3 | Material 4 | } from 'three'; 5 | import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js'; 6 | import { 7 | comRouting, 8 | AssociatedArrayType, 9 | DataPayload, 10 | WorkerTaskCommandResponse, 11 | WorkerMessage, 12 | WorkerTaskWorker 13 | } from 'wtd-core'; 14 | import { 15 | MaterialsPayload, 16 | MaterialUtils, 17 | MeshPayload, 18 | } from 'wtd-three-ext'; 19 | 20 | class OBJLoaderWorker implements WorkerTaskWorker { 21 | 22 | private localData = { 23 | objLoader: undefined as OBJLoader | undefined, 24 | materials: new Map() as Map, 25 | buffer: undefined as ArrayBufferLike | undefined, 26 | objectId: 'unknown' 27 | }; 28 | 29 | init(message: WorkerMessage) { 30 | console.log(`OBJLoaderWorker#init: name: ${message.name} uuid: ${message.uuid} cmd: ${message.cmd} workerId: ${message.endpointdId}`); 31 | 32 | const wm = WorkerMessage.unpack(message, false); 33 | if (wm.payloads.length === 2) { 34 | const dataPayload = wm.payloads[0] as DataPayload; 35 | const materialsPayload = wm.payloads[1] as MaterialsPayload; 36 | 37 | this.localData.buffer = dataPayload.message.buffers?.get('modelData'); 38 | this.localData.materials = materialsPayload.message.materials; 39 | 40 | const initComplete = WorkerMessage.createFromExisting(wm, { 41 | overrideCmd: WorkerTaskCommandResponse.INIT_COMPLETE 42 | }); 43 | self.postMessage(initComplete); 44 | } 45 | } 46 | 47 | execute(message: WorkerMessage) { 48 | console.log(`OBJLoaderWorker#execute: name: ${message.name} uuid: ${message.uuid} cmd: ${message.cmd} workerId: ${message.endpointdId}`); 49 | 50 | this.localData.objLoader = new OBJLoader(); 51 | this.localData.objectId = message.uuid as string; 52 | 53 | const materials: AssociatedArrayType = {}; 54 | materials.create = (name: string) => { 55 | return materials[name]; 56 | }; 57 | for (const [k, v] of Object.entries(this.localData.materials)) { 58 | materials[k] = v; 59 | } 60 | 61 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 62 | // @ts-ignore 63 | this.localData.objLoader.setMaterials(materials as unknown); 64 | 65 | const enc = new TextDecoder('utf-8'); 66 | const meshes = this.localData.objLoader.parse(enc.decode(this.localData.buffer)); 67 | for (let mesh, i = 0; i < meshes.children.length; i++) { 68 | mesh = meshes.children[i] as Mesh; 69 | mesh.name = mesh.name + message.uuid; 70 | 71 | // signal intermediate feedback 72 | const intermediate = WorkerMessage.createFromExisting(message, { 73 | overrideCmd: WorkerTaskCommandResponse.INTERMEDIATE_CONFIRM 74 | }); 75 | 76 | const meshPayload = new MeshPayload(); 77 | meshPayload.setMesh(mesh, 0); 78 | intermediate.addPayload(meshPayload); 79 | 80 | const material = mesh.material; 81 | if (material instanceof Material) { 82 | const materialPayload = new MaterialsPayload(); 83 | MaterialUtils.addMaterial(materialPayload.message.materials, material.name, material, false, false); 84 | intermediate.addPayload(materialPayload); 85 | } 86 | 87 | const transferables = WorkerMessage.pack(intermediate.payloads, false); 88 | self.postMessage(intermediate, transferables); 89 | } 90 | 91 | // signal complete 92 | const execComplete = WorkerMessage.createFromExisting(message, { 93 | overrideCmd: WorkerTaskCommandResponse.EXECUTE_COMPLETE 94 | }); 95 | self.postMessage(execComplete); 96 | } 97 | 98 | } 99 | 100 | const worker = new OBJLoaderWorker(); 101 | self.onmessage = message => comRouting(worker, message); 102 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 4.0.1 - 2024-10-01 4 | 5 | - WorkerMessageDef: Rename answer to expectedAnswer 6 | 7 | ## 4.0.0 - 2024-10-01 8 | 9 | - ComChannelEndpoint has been extracted from WorkerTask 10 | - Now Worker, MessageChannel or DedicatedWorkerGlobalScope can be channel endpoints. Both ends of the communication channel can use the same implementation to send message and await responses if needed. 11 | - Added new example **HelloWorldComChannelEndpoint** 12 | 13 | ## 3.0.0 - 2024-01-05 14 | 15 | - Make the worker lifecylce no longer mandatory if not using `WorkerTaskDirector`. 16 | - Sent message with or without awaiting them. 17 | - `WorkerTask` keeps track of messages that need to be awaited. 18 | - API clean-up and code improvements: 19 | - `WorkerTask` contains async code improvements and it keeps track of outstanding messages (new) 20 | - Use configuration objects instead of long number of arguments 21 | - Move static functions of classes to independent funtions 22 | - Better function and class names 23 | - Added helper functions for creating an OffscreenCanvas and delegating events to the worker. 24 | - Extracted `Payload` from `DataPayload` and created `RawPayload` for supporting plain messages. 25 | - Added offscreen canvas related functionality and utilities: 26 | - Provide framework independent worker and message payload extensions (`OffscreenWorker` and `OffscreenPayload`) (**wtd-core**) 27 | - `MainEventProxy` allows configurable event delegation to a Worker (**wtd-core**) 28 | - `ElementProxyReceiver` can be used to simulate a canvas in a Worker (**wtd-three-ext**) 29 | - Added new example [Inter-Worker Communication](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/com/WorkerCom.ts) that demonstrates communication between workers utilizing message channels. 30 | 31 | ## 2.3.0 - 2023-10-21 32 | 33 | - Added the possiblity to sent intermediate message from main to the worker if the worker is still executing. 34 | - Usage of `await` everwhere instead of `Promise.then()` 35 | - Moved `WorkerTaskWorker` to its own file 36 | - Code Cleanup 37 | - Updated dependencies 38 | 39 | ## 2.2.0 40 | 41 | - `DataPayload#applyProperties` allows any object as input and the params are more relaxed. A first set of uit tests has been introduced. 42 | - Set compiler `target` and `module` to `ES2020`. `moduleResolution` is now `Node16` instead of `Node`, 43 | 44 | ## 2.1.0 45 | 46 | - Export an esm bundle along with raw code 47 | - Use `import type` for importing type definitions 48 | 49 | ## 2.0.0 50 | 51 | - Completely transform the source code from JavaScript to TypeScript 52 | - Switch from snowpack to vitejs 53 | - Clean-up and uncluttering 54 | - Remove all code related to worker online assembly and minification workarounds 55 | - Code is better organized into blocks with specific purpose that are combined to achieve the overall functionality 56 | - Fully rely on module workers. Use vite config to generate standard workers from module workers at build time 57 | - Renamed and split package `three-wtm` into `wtd-core` (core without any dependencies) and `wtd-three-ext` (Payload extension for three.js) 58 | - `WorkerTaskMessage` is now properly defined and provides a header and multiple payloads. Payloads can be extended from the base `DataPayload`/`DataPayloadType` 59 | - `WorkerTask`: Handles worker registration, initialization and execution and can be used independently of the `WorkerTaskDirector` 60 | - `WorkerTaskDirector`: 61 | - It is now possible to define different number of workers for each registered task, execution queue depletion is performed according the configuration (workerExecutionPlans) 62 | - The depletion code has been rewritten and Promise handling has been fixed 63 | 64 | ## 1.1.0 65 | 66 | - Added the possibility to load a non-module worker from a URL. Triggered by https://github.com/kaisalmen/WWOBJLoader/issues/60 67 | - Updated example wtm_helloworld.html to reflect how this is done. 68 | - Updated formatting of files 69 | 70 | ## 1.0.1 71 | 72 | - `three.js` is no longer a **peerDependency**. It is just a **dependency**. 73 | - Updated **devDependencies** to resolve potential security issues. 74 | 75 | ## 1.0.0 76 | 77 | - Initial public release. 78 | -------------------------------------------------------------------------------- /packages/wtd-three-ext/README.md: -------------------------------------------------------------------------------- 1 | # Worker Task Director Three.js Extenstions (wtd-three-ext) 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/kaisalmen/wtd/blob/main/LICENSE) 4 | [![wtd](https://github.com/kaisalmen/wtd/actions/workflows/actions.yml/badge.svg)](https://github.com/kaisalmen/wtd/actions/workflows/actions.yml) 5 | [![Github Pages](https://img.shields.io/badge/GitHub-Pages-blue?logo=github)](https://kaisalmen.github.io/wtd) 6 | [![wtd-three-ext version](https://img.shields.io/npm/v/wtd-three-ext?logo=npm&label=wtd-three-ext)](https://www.npmjs.com/package/wtd-three-ext) 7 | 8 | three.js related extensions of [wtd-core](https://www.npmjs.com/package/wtd-core) and additional three.js related utility functions. 9 | 10 | - [Worker Task Director Three.js Extenstions (wtd-three-ext)](#worker-task-director-threejs-extenstions-wtd-three-ext) 11 | - [Examples](#examples) 12 | - [Usage](#usage) 13 | 14 | ## Examples 15 | 16 | There are multiple examples available demonstarting the features described above (listed from simpler to more advanced): 17 | 18 | - **WorkerTaskDirector: Transferables**: [html](https://github.com/kaisalmen/wtd/blob/main/packages/examples/transferables.html), [ts](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/transferables/TransferablesTestbed.ts), **Worker**: [1](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/TransferableWorkerTest1.ts), [2](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/TransferableWorkerTest2.ts), [3](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/TransferableWorkerTest3.ts), [4](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/TransferableWorkerTest4.ts) 19 | - **WorkerTaskDirector: Three.js**: [html](https://github.com/kaisalmen/wtd/blob/main/packages/examples/threejs.html), [ts](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/threejs/Threejs.ts), **Worker**: [1](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/HelloWorldThreeWorker.ts), [2](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/OBJLoaderWorker.ts) 20 | - **WorkerTaskDirector: Potentially Infinite Execution**: [html](https://github.com/kaisalmen/wtd/blob/main/packages/examples/potentially_infinite.html), [ts](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/infinite/PotentiallyInfiniteExample.ts), **Worker**: [1](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/InfiniteWorkerExternalGeometry.ts), [2](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/InfiniteWorkerInternalGeometry.ts), [3](https://github.com/kaisalmen/WWOBJLoader/blob/main/packages/objloader2/src/worker/OBJLoader2Worker.ts), [4](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/infinite/PotentiallyInfiniteExample.ts#L627-L668) 21 | 22 | Try out all examples here: 23 | 24 | ## Usage 25 | 26 | This shall give you an idea how you can use module worker with `WorkerTask` (derived from [WorkerTask: Hello World](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/helloWorld/HelloWorldWorkerTask.ts)): 27 | 28 | ```js 29 | // let WorkerTask create the worker 30 | const workerTask = new WorkerTask({ 31 | endpointName, 32 | endpointId: 1, 33 | endpointConfig: { 34 | $type: 'WorkerConfigParams', 35 | url: new URL('./HelloWorldWorker.js', import.meta.url), 36 | workerType: 'module', 37 | }, 38 | verbose: true 39 | }); 40 | 41 | try { 42 | // cteates and connects the worker callback functions and the WorkerTask 43 | workerTask.connect(); 44 | 45 | // execute without init and an empty message 46 | const resultExec = await workerTask.executeWorker({ 47 | message: WorkerTaskMessage.createEmpty() 48 | }); 49 | 50 | // once you awaited the resulting WorkerTaskMessage extract the RawPayload 51 | const rawPayload = resultExec.payloads?.[0] as RawPayload; 52 | 53 | // log the hello from the HelloWorldWorker 54 | console.log(`Worker said: ${rawPayload.message.raw?.hello}`); 55 | } catch (e) { 56 | // error handling 57 | console.error(e); 58 | } 59 | ``` 60 | 61 | Further information is found in the main [README](https://github.com/kaisalmen/wtd/blob/main/README.md) of the overall [repository](https://github.com/kaisalmen/wtd). 62 | 63 | All changes are noted in the overall [CHANGELOG](https://github.com/kaisalmen/wtd/blob/main/CHANGELOG.md). 64 | -------------------------------------------------------------------------------- /packages/wtd-core/src/WorkerTask.ts: -------------------------------------------------------------------------------- 1 | import { WorkerMessage } from './WorkerMessage.js'; 2 | import { WorkerTaskCommandRequest, WorkerTaskCommandResponse } from './WorkerTaskWorker.js'; 3 | import { AwaitHandler, ComChannelEndpoint, ComChannelEndpointConfig, WorkerMessageDef } from './ComChannelEndpoint.js'; 4 | 5 | export interface WorkerExecutionDef { 6 | message: WorkerMessage; 7 | onComplete?: (message: WorkerMessage) => void; 8 | onIntermediateConfirm?: (message: WorkerMessage) => void; 9 | transferables?: Transferable[]; 10 | copyTransferables?: boolean; 11 | } 12 | 13 | export class WorkerTask extends ComChannelEndpoint { 14 | 15 | private executing = false; 16 | 17 | constructor(config: ComChannelEndpointConfig) { 18 | super(config); 19 | } 20 | 21 | isWorkerExecuting() { 22 | return this.executing; 23 | } 24 | 25 | markExecuting(executing: boolean) { 26 | this.executing = executing; 27 | } 28 | 29 | connect() { 30 | super.connect(); 31 | 32 | if (this.impl !== undefined && Object.hasOwn(this.impl ?? {}, 'onerror')) { 33 | (this.impl as Worker).onerror = (async (answer) => { 34 | console.log(`Execution Aborted: ${answer.error}`); 35 | Promise.reject(answer); 36 | this.markExecuting(false); 37 | }); 38 | } 39 | } 40 | 41 | async initWorker(def: WorkerMessageDef): Promise { 42 | return new Promise((resolve, reject) => { 43 | if (!this.impl) { 44 | reject(new Error('No worker is available. Aborting...')); 45 | this.markExecuting(false); 46 | } else { 47 | if (this.verbose) { 48 | console.log(`Task: ${this.endpointName}: Waiting for completion of worker init.`); 49 | } 50 | 51 | const message = def.message; 52 | message.cmd = WorkerTaskCommandRequest.INIT; 53 | const transferablesToWorker = this.handleTransferables(def); 54 | 55 | this.updateAwaitHandlers(message, [{ 56 | name: WorkerTaskCommandResponse.INIT_COMPLETE, 57 | resolve: [resolve], 58 | reject: reject, 59 | remove: true, 60 | log: this.verbose, 61 | }]); 62 | this.impl.postMessage(message, transferablesToWorker); 63 | } 64 | }); 65 | } 66 | 67 | async executeWorker(def: WorkerExecutionDef): Promise { 68 | return new Promise((resolve, reject) => { 69 | if (!this.impl) { 70 | reject(new Error('No worker is available. Aborting...')); 71 | this.markExecuting(false); 72 | } else { 73 | this.markExecuting(true); 74 | 75 | const message = def.message; 76 | message.cmd = WorkerTaskCommandRequest.EXECUTE; 77 | const transferablesToWorker = this.handleTransferables(def); 78 | 79 | const awaitHandlers: AwaitHandler[] = []; 80 | const resolveFuncs: Array<(message: WorkerMessage) => void> = []; 81 | if (def.onComplete) { 82 | resolveFuncs.push(def.onComplete); 83 | } 84 | resolveFuncs.push(resolve); 85 | awaitHandlers.push({ 86 | name: WorkerTaskCommandResponse.EXECUTE_COMPLETE, 87 | resolve: resolveFuncs, 88 | reject: reject, 89 | remove: true, 90 | log: this.verbose 91 | }); 92 | 93 | if (typeof def.onIntermediateConfirm === 'function') { 94 | awaitHandlers.push({ 95 | name: WorkerTaskCommandResponse.INTERMEDIATE_CONFIRM, 96 | resolve: [def.onIntermediateConfirm], 97 | reject: reject, 98 | remove: false, 99 | log: this.verbose 100 | }); 101 | } 102 | this.updateAwaitHandlers(message, awaitHandlers); 103 | this.impl.postMessage(message, transferablesToWorker); 104 | } 105 | }); 106 | } 107 | 108 | override removeAwaitHandler(handler: AwaitHandler, wm: WorkerMessage) { 109 | const removed = super.removeAwaitHandler(handler, wm); 110 | if (removed) { 111 | this.markExecuting(false); 112 | } 113 | return removed; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /packages/examples/src/worker/Com1Worker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | comRouting, 3 | getOffscreenCanvas, 4 | InterComPortHandler, 5 | InterComWorker, 6 | OffscreenWorker, 7 | OffscreenWorkerCommandResponse, 8 | RawPayload, 9 | recalcAspectRatio, 10 | WorkerTaskCommandRequest, 11 | WorkerTaskCommandResponse, 12 | WorkerMessage, 13 | WorkerTaskWorker 14 | } from 'wtd-core'; 15 | import { updateText } from './ComWorkerCommon.js'; 16 | import { OffscreenPayload } from 'wtd-core'; 17 | 18 | export class Com1Worker implements WorkerTaskWorker, InterComWorker, OffscreenWorker { 19 | 20 | private icph = new InterComPortHandler(); 21 | private offScreenCanvas?: OffscreenCanvas; 22 | private text = 'none'; 23 | 24 | initChannel(message: WorkerMessage): void { 25 | // register the default com-routing function for inter-worker communication 26 | const payloadPort = message.payloads[0]; 27 | this.icph.registerPort('com2', payloadPort, message => comRouting(this, message)); 28 | 29 | const initChannelComplete = WorkerMessage.createFromExisting(message, { 30 | overrideCmd: WorkerTaskCommandResponse.INIT_CHANNEL_COMPLETE 31 | }); 32 | self.postMessage(initChannelComplete); 33 | } 34 | 35 | initOffscreenCanvas(message: WorkerMessage): void { 36 | const offscreenPayload = message.payloads[0] as OffscreenPayload; 37 | this.offScreenCanvas = getOffscreenCanvas(offscreenPayload); 38 | 39 | const initOffscreenCanvasComplete = WorkerMessage.createFromExisting(message, { 40 | overrideCmd: OffscreenWorkerCommandResponse.INIT_OFFSCREEN_CANVAS_COMPLETE 41 | }); 42 | self.postMessage(initOffscreenCanvasComplete); 43 | } 44 | 45 | resize(message: WorkerMessage) { 46 | const offscreenPayload = message.payloads[0] as OffscreenPayload; 47 | recalcAspectRatio(this.offScreenCanvas!, offscreenPayload.message.width ?? 0, offscreenPayload.message.height ?? 1); 48 | this.updateText(false); 49 | } 50 | 51 | init(message: WorkerMessage): void { 52 | this.text = 'Worker 1: init'; 53 | this.updateText(); 54 | 55 | const initComplete = WorkerMessage.createFromExisting(message, { 56 | overrideCmd: WorkerTaskCommandResponse.INIT_COMPLETE 57 | }); 58 | initComplete.addPayload(new RawPayload({ hello: 'Com1Worker initComplete!' })); 59 | self.postMessage(initComplete); 60 | } 61 | 62 | execute(message: WorkerMessage) { 63 | // send message with cmd 'interComIntermediate' to Com2Worker 64 | const sendWorker2 = WorkerMessage.createFromExisting(message, { 65 | overrideCmd: WorkerTaskCommandRequest.INTERCOM_INTERMEDIATE 66 | }); 67 | const payload = new RawPayload({ hello: 'Hi Worker 2!' }); 68 | sendWorker2.addPayload(payload); 69 | 70 | this.icph.postMessageOnPort('com2', sendWorker2); 71 | } 72 | 73 | interComIntermediate(message: WorkerMessage): void { 74 | const rawPayload = message.payloads[0] as RawPayload; 75 | this.text = `Worker 1: Worker 2 said: ${rawPayload.message.raw.hello}`; 76 | this.updateText(); 77 | 78 | setTimeout(() => { 79 | // after receiving the message from Com2Worker, send interComIntermediateConfirm to worker 2 80 | const intermediateConfirm = WorkerMessage.createFromExisting(message, { 81 | overrideCmd: WorkerTaskCommandResponse.INTERCOM_INTERMEDIATE_CONFIRM 82 | }); 83 | const payload = new RawPayload({ confirmed: 'Hi Worker 2. I confirm!' }); 84 | intermediateConfirm.addPayload(payload); 85 | 86 | this.icph.postMessageOnPort('com2', intermediateConfirm); 87 | }, 2000); 88 | } 89 | 90 | interComIntermediateConfirm(message: WorkerMessage): void { 91 | const rawPayload = message.payloads[0] as RawPayload; 92 | this.text = `Worker 1: Worker 2 confirmed: ${rawPayload.message.raw.confirmed}`; 93 | this.updateText(); 94 | 95 | // after receiving the interComIntermediateConfirm from Com2Worker, send execComplete to main 96 | const execComplete = WorkerMessage.createFromExisting(message, { 97 | overrideCmd: WorkerTaskCommandResponse.EXECUTE_COMPLETE 98 | }); 99 | const payload = new RawPayload({ finished: 'Hi Main. Worker 1 completed!' }); 100 | execComplete.addPayload(payload); 101 | self.postMessage(execComplete); 102 | } 103 | 104 | private updateText(log: boolean = true) { 105 | updateText({ 106 | text: this.text, 107 | width: this.offScreenCanvas?.width ?? 0, 108 | height: this.offScreenCanvas?.height ?? 0, 109 | canvas: this.offScreenCanvas as unknown as HTMLCanvasElement, 110 | log 111 | }); 112 | } 113 | } 114 | 115 | const worker = new Com1Worker(); 116 | self.onmessage = message => comRouting(worker, message); 117 | -------------------------------------------------------------------------------- /packages/examples/src/worker/Com2Worker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | comRouting, 3 | getOffscreenCanvas, 4 | InterComPortHandler, 5 | InterComWorker, 6 | OffscreenWorker, 7 | OffscreenWorkerCommandResponse, 8 | RawPayload, 9 | recalcAspectRatio, 10 | WorkerTaskCommandRequest, 11 | WorkerTaskCommandResponse, 12 | WorkerMessage, 13 | WorkerTaskWorker 14 | } from 'wtd-core'; 15 | import { updateText } from './ComWorkerCommon.js'; 16 | import { OffscreenPayload } from 'wtd-core'; 17 | 18 | export class Com2Worker implements WorkerTaskWorker, InterComWorker, OffscreenWorker { 19 | 20 | private icph = new InterComPortHandler(); 21 | private offScreenCanvas?: OffscreenCanvas; 22 | private text = 'none'; 23 | 24 | initChannel(message: WorkerMessage): void { 25 | // register the default com-routing function for inter-worker communication 26 | const payloadPort = message.payloads[0]; 27 | this.icph.registerPort('com1', payloadPort, message => comRouting(this, message)); 28 | 29 | const initChannelComplete = WorkerMessage.createFromExisting(message, { 30 | overrideCmd: WorkerTaskCommandResponse.INIT_CHANNEL_COMPLETE 31 | }); 32 | self.postMessage(initChannelComplete); 33 | } 34 | 35 | initOffscreenCanvas(message: WorkerMessage): void { 36 | const offscreenPayload = message.payloads[0] as OffscreenPayload; 37 | this.offScreenCanvas = getOffscreenCanvas(offscreenPayload); 38 | 39 | const initOffscreenCanvasComplete = WorkerMessage.createFromExisting(message, { 40 | overrideCmd: OffscreenWorkerCommandResponse.INIT_OFFSCREEN_CANVAS_COMPLETE 41 | }); 42 | self.postMessage(initOffscreenCanvasComplete); 43 | } 44 | 45 | resize(message: WorkerMessage) { 46 | const offscreenPayload = message.payloads[0] as OffscreenPayload; 47 | recalcAspectRatio(this.offScreenCanvas!, offscreenPayload.message.width ?? 0, offscreenPayload.message.height ?? 1); 48 | this.updateText(false); 49 | } 50 | 51 | init(message: WorkerMessage): void { 52 | this.text = 'Worker 2: init'; 53 | this.updateText(); 54 | 55 | const initComplete = WorkerMessage.createFromExisting(message, { 56 | overrideCmd: WorkerTaskCommandResponse.INIT_COMPLETE 57 | }); 58 | initComplete.addPayload(new RawPayload({ hello: 'Com2Worker initComplete!' })); 59 | self.postMessage(initComplete); 60 | } 61 | 62 | execute(message: WorkerMessage) { 63 | // send message with cmd 'interComIntermediate' to Com1Worker 64 | const sendWorker1 = WorkerMessage.createFromExisting(message, { 65 | overrideCmd: WorkerTaskCommandRequest.INTERCOM_INTERMEDIATE 66 | }); 67 | const payload = new RawPayload({ hello: 'Hi Worker 1!' }); 68 | sendWorker1.addPayload(payload); 69 | 70 | this.icph.postMessageOnPort('com1', sendWorker1); 71 | } 72 | 73 | interComIntermediate(message: WorkerMessage): void { 74 | const rawPayload = message.payloads[0] as RawPayload; 75 | this.text = `Worker 2: Worker 1 said: ${rawPayload.message.raw.hello}`; 76 | this.updateText(); 77 | 78 | setTimeout(() => { 79 | // after receiving the message from Com1Worker, send interComIntermediateConfirm to worker 2 80 | const intermediateConfirm = WorkerMessage.createFromExisting(message, { 81 | overrideCmd: WorkerTaskCommandResponse.INTERCOM_INTERMEDIATE_CONFIRM 82 | }); 83 | const payload = new RawPayload({ confirmed: 'Hi Worker 1. I confirm!' }); 84 | intermediateConfirm.addPayload(payload); 85 | 86 | this.icph.postMessageOnPort('com1', intermediateConfirm); 87 | }, 2000); 88 | } 89 | 90 | interComIntermediateConfirm(message: WorkerMessage): void { 91 | const rawPayload = message.payloads[0] as RawPayload; 92 | this.text = `Worker 2: Worker 1 confirmed: ${rawPayload.message.raw.confirmed}`; 93 | this.updateText(); 94 | 95 | // after receiving the interComIntermediateConfirm from Com1Worker, send execComplete to main 96 | const execComplete = WorkerMessage.createFromExisting(message, { 97 | overrideCmd: WorkerTaskCommandResponse.EXECUTE_COMPLETE 98 | }); 99 | const payload = new RawPayload({ finished: 'Hi Main. Worker 2 completed!' }); 100 | execComplete.addPayload(payload); 101 | self.postMessage(execComplete); 102 | } 103 | 104 | private updateText(log: boolean = true) { 105 | updateText({ 106 | text: this.text, 107 | width: this.offScreenCanvas?.width ?? 0, 108 | height: this.offScreenCanvas?.height ?? 0, 109 | canvas: this.offScreenCanvas as unknown as HTMLCanvasElement, 110 | log 111 | }); 112 | } 113 | } 114 | 115 | const worker = new Com2Worker(); 116 | self.onmessage = message => comRouting(worker, message); 117 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | env: { 5 | node: true, 6 | browser: true, 7 | es2020: true 8 | }, 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:@typescript-eslint/recommended' 12 | ], 13 | overrides: [ 14 | ], 15 | parserOptions: { 16 | ecmaVersion: 2020, 17 | sourceType: 'module', 18 | project: ['./tsconfig.json'] 19 | }, 20 | plugins: [ 21 | '@typescript-eslint' 22 | ], 23 | ignorePatterns: [ 24 | '**/{node_modules,dist,lib,out,bin}', 25 | '**/generated/**/*', 26 | '.eslintrc.js' 27 | ], 28 | rules: { 29 | // List of [ESLint rules](https://eslint.org/docs/rules/) 30 | 'arrow-parens': ['off', 'as-needed'], // do not force arrow function parentheses 31 | 'constructor-super': 'error', // checks the correct use of super() in sub-classes 32 | 'dot-notation': 'error', // obj.a instead of obj['a'] when possible 33 | 'eqeqeq': 'error', // ban '==', don't use 'smart' option! 34 | 'guard-for-in': 'error', // needs obj.hasOwnProperty(key) checks 35 | 'new-parens': 'error', // new Error() instead of new Error 36 | 'no-bitwise': 'error', // bitwise operators &, | can be confused with &&, || 37 | 'no-caller': 'error', // ECMAScript deprecated arguments.caller and arguments.callee 38 | 'no-cond-assign': 'error', // assignments if (a = '1') are error-prone 39 | 'no-debugger': 'error', // disallow debugger; statements 40 | 'no-eval': 'error', // eval is considered unsafe 41 | 'no-inner-declarations': 'off', // we need to have 'namespace' functions when using TS 'export =' 42 | 'no-labels': 'error', // GOTO is only used in BASIC ;) 43 | 'no-multiple-empty-lines': ['error', { 'max': 1 }], // two or more empty lines need to be fused to one 44 | 'no-new-wrappers': 'error', // there is no reason to wrap primitve values 45 | 'no-throw-literal': 'error', // only throw Error but no objects {} 46 | 'no-trailing-spaces': 'error', // trim end of lines 47 | 'no-unsafe-finally': 'error', // safe try/catch/finally behavior 48 | 'no-var': 'error', // use const and let instead of var 49 | 'space-before-function-paren': ['error', { // space in function decl: f() vs async () => {} 50 | 'anonymous': 'never', 51 | 'asyncArrow': 'always', 52 | 'named': 'never' 53 | }], 54 | 'semi': [2, 'always'], // Always use semicolons at end of statement 55 | 'quotes': [2, 'single', { 'avoidEscape': true }], // Prefer single quotes 56 | 'use-isnan': 'error', // isNaN(i) Number.isNaN(i) instead of i === NaN 57 | // List of [@typescript-eslint rules](https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#supported-rules) 58 | '@typescript-eslint/adjacent-overload-signatures': 'error', // grouping same method names 59 | '@typescript-eslint/array-type': ['error', { // string[] instead of Array 60 | 'default': 'array-simple' 61 | }], 62 | '@typescript-eslint/ban-types': 'error', // bans types like String in favor of string 63 | '@typescript-eslint/indent': 'error', // consistent indentation 64 | '@typescript-eslint/no-explicit-any': 'error', // don't use :any type 65 | '@typescript-eslint/no-misused-new': 'error', // no constructors for interfaces or new for classes 66 | '@typescript-eslint/no-namespace': 'off', // disallow the use of custom TypeScript modules and namespaces 67 | '@typescript-eslint/no-non-null-assertion': 'off', // allow ! operator 68 | "@typescript-eslint/parameter-properties": "error", // no property definitions in class constructors 69 | '@typescript-eslint/no-unused-vars': ['error', { // disallow Unused Variables 70 | 'argsIgnorePattern': '^_' 71 | }], 72 | '@typescript-eslint/no-var-requires': 'error', // use import instead of require 73 | '@typescript-eslint/prefer-for-of': 'error', // prefer for-of loop over arrays 74 | '@typescript-eslint/prefer-namespace-keyword': 'error', // prefer namespace over module in TypeScript 75 | '@typescript-eslint/triple-slash-reference': 'error', // ban /// , prefer imports 76 | '@typescript-eslint/type-annotation-spacing': 'error', // consistent space around colon ':' 77 | '@typescript-eslint/strict-boolean-expressions': 'error', // Disallow certain types in boolean expressions 78 | '@typescript-eslint/no-unnecessary-condition': 'error' // Disallow conditionals where the type is always truthy or always falsy 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/examples/src/com/WorkerCom.ts: -------------------------------------------------------------------------------- 1 | import { 2 | RawPayload, 3 | WorkerTask, 4 | WorkerMessage, 5 | initChannel, 6 | initOffscreenCanvas, 7 | recalcAspectRatio, 8 | registerResizeHandler 9 | } from 'wtd-core'; 10 | import { updateText } from '../worker/ComWorkerCommon.js'; 11 | 12 | /** 13 | * Hello World example using a classic worker 14 | */ 15 | class HelloWorldStandardWorkerExample { 16 | 17 | private canvasMain: HTMLCanvasElement; 18 | private canvasCom1: HTMLCanvasElement; 19 | private canvasCom2: HTMLCanvasElement; 20 | 21 | constructor() { 22 | this.canvasMain = document.getElementById('main') as HTMLCanvasElement; 23 | this.canvasCom1 = document.getElementById('com1') as HTMLCanvasElement; 24 | this.canvasCom2 = document.getElementById('com2') as HTMLCanvasElement; 25 | 26 | recalcAspectRatio(this.canvasMain, this.canvasMain.clientWidth, this.canvasMain.clientHeight); 27 | recalcAspectRatio(this.canvasCom1, this.canvasCom1.clientWidth, this.canvasCom1.clientHeight); 28 | recalcAspectRatio(this.canvasCom2, this.canvasCom2.clientWidth, this.canvasCom2.clientHeight); 29 | } 30 | 31 | async run() { 32 | const com1Worker = new Worker(new URL(import.meta.env.DEV ? '../worker/Com1Worker.ts' : '../worker/generated/Com1Worker-es.js', import.meta.url), { 33 | type: 'module' 34 | }); 35 | const workerTaskCom1 = new WorkerTask({ 36 | endpointName: 'Com1Worker', 37 | endpointId: 1, 38 | endpointConfig: { 39 | $type: 'DirectImplConfig', 40 | impl: com1Worker 41 | }, 42 | verbose: true 43 | }); 44 | 45 | const com2Worker = new Worker(new URL(import.meta.env.DEV ? '../worker/Com2Worker.ts' : '../worker/generated/Com2Worker-es.js', import.meta.url), { 46 | type: 'module' 47 | }); 48 | const workerTaskCom2 = new WorkerTask({ 49 | endpointName: 'Com2Worker', 50 | endpointId: 2, 51 | endpointConfig: { 52 | $type: 'DirectImplConfig', 53 | impl: com2Worker 54 | }, 55 | verbose: true 56 | }); 57 | 58 | workerTaskCom1.connect(); 59 | workerTaskCom2.connect(); 60 | 61 | try { 62 | await initChannel(workerTaskCom1, workerTaskCom2); 63 | 64 | let promises = []; 65 | promises.push(initOffscreenCanvas(workerTaskCom1, this.canvasCom1)); 66 | promises.push(initOffscreenCanvas(workerTaskCom2, this.canvasCom2)); 67 | await Promise.all(promises); 68 | 69 | registerResizeHandler(workerTaskCom1, this.canvasCom1); 70 | registerResizeHandler(workerTaskCom2, this.canvasCom2); 71 | 72 | updateText({ 73 | text: 'Main: Init', 74 | width: this.canvasMain.clientWidth, 75 | height: this.canvasMain.clientHeight, 76 | canvas: this.canvasMain, 77 | log: true 78 | }); 79 | 80 | promises = []; 81 | promises.push(workerTaskCom1.initWorker({ 82 | message: WorkerMessage.createEmpty() 83 | })); 84 | promises.push(workerTaskCom2.initWorker({ 85 | message: WorkerMessage.createEmpty() 86 | })); 87 | const results = await Promise.all(promises); 88 | const logMsg: string[] = []; 89 | results.forEach(wm => { 90 | const rawPayload = wm.payloads[0] as RawPayload; 91 | logMsg.push(` Worker init feedback: ${rawPayload.message.raw.hello}`); 92 | }); 93 | console.log(`Init results:${logMsg}`); 94 | 95 | const t0 = performance.now(); 96 | setTimeout(async () => { 97 | promises = []; 98 | promises.push(workerTaskCom1.executeWorker({ 99 | message: WorkerMessage.createEmpty() 100 | })); 101 | promises.push(workerTaskCom2.executeWorker({ 102 | message: WorkerMessage.createEmpty() 103 | })); 104 | 105 | const results = await Promise.all(promises); 106 | results.forEach((message: WorkerMessage) => { 107 | console.log('Received final command: ' + message.cmd); 108 | if (message.payloads.length > 0) { 109 | const rawPayload = message.payloads[0] as RawPayload; 110 | console.log(`Worker said onComplete: ${rawPayload.message.raw.finished} `); 111 | } 112 | }); 113 | 114 | const t1 = performance.now(); 115 | const text = `Main: Worker execution has been completed after ${t1 - t0} ms.`; 116 | updateText({ 117 | text, 118 | width: this.canvasMain.clientWidth, 119 | height: this.canvasMain.clientHeight, 120 | canvas: this.canvasMain, 121 | log: true 122 | }); 123 | workerTaskCom1.printAwaitAnswers(); 124 | workerTaskCom2.printAwaitAnswers(); 125 | console.log('Done'); 126 | }, 2000); 127 | } catch (e) { 128 | console.error(e); 129 | } 130 | } 131 | } 132 | 133 | const app = new HelloWorldStandardWorkerExample(); 134 | await app.run(); 135 | -------------------------------------------------------------------------------- /packages/wtd-three-ext/src/MaterialsPayload.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AssociatedArrayType, 3 | ParameterizedMessage, 4 | Payload, 5 | PayloadHandler 6 | } from 'wtd-core'; 7 | import { 8 | DataPayloadHandler, 9 | PayloadRegister, 10 | fillTransferables 11 | } from 'wtd-core'; 12 | import type { 13 | MaterialCloneInstructionsType 14 | } from './MaterialUtils.js'; 15 | import { 16 | MaterialUtils 17 | } from './MaterialUtils.js'; 18 | import { 19 | Material, 20 | MaterialLoader, 21 | Texture 22 | } from 'three'; 23 | 24 | export type MaterialsPayloadAdditions = Payload & { 25 | message: MaterialsPayloadMessageAdditions 26 | }; 27 | 28 | export type MaterialsPayloadMessageAdditions = ParameterizedMessage & { 29 | materials: Map; 30 | materialsJson: Map; 31 | multiMaterialNames: Map; 32 | cloneInstructions: MaterialCloneInstructionsType[]; 33 | } 34 | 35 | export class MaterialsPayload implements MaterialsPayloadAdditions { 36 | 37 | $type = 'MaterialsPayload'; 38 | message: MaterialsPayloadMessageAdditions = { 39 | buffers: new Map(), 40 | params: {}, 41 | materials: new Map(), 42 | materialsJson: new Map(), 43 | multiMaterialNames: new Map(), 44 | cloneInstructions: [] 45 | }; 46 | 47 | /** 48 | * Set an object containing named materials. 49 | * @param {Map} materials 50 | */ 51 | setMaterials(materials: Map): void { 52 | for (const [k, v] of materials.entries()) { 53 | this.message.materials.set(k, v); 54 | } 55 | } 56 | 57 | /** 58 | * Removes all textures and null values from all materials 59 | */ 60 | cleanMaterials(): void { 61 | const clonedMaterials = new Map(); 62 | for (const material of this.message.materials.values()) { 63 | if (typeof material.clone === 'function') { 64 | const clonedMaterial = material.clone(); 65 | clonedMaterials.set(clonedMaterial.name, this.cleanMaterial(clonedMaterial)); 66 | } 67 | } 68 | this.message.materials = clonedMaterials; 69 | } 70 | 71 | private cleanMaterial(material: Material): Material { 72 | const objToAlter = material as unknown as AssociatedArrayType; 73 | for (const [k, v] of Object.entries(objToAlter)) { 74 | if ((v instanceof Texture || v === null) && Object.prototype.hasOwnProperty.call(material, k)) { 75 | objToAlter[k] = undefined; 76 | } 77 | } 78 | return material; 79 | } 80 | 81 | /** 82 | * Tell whether a multi-material was defined 83 | * @return {boolean} 84 | */ 85 | hasMultiMaterial() { 86 | return this.message.multiMaterialNames.size > 0; 87 | } 88 | 89 | /** 90 | * Returns a single material if it is defined or null. 91 | * @return {Material|null} 92 | */ 93 | getSingleMaterial() { 94 | return this.message.materials.size > 0 ? this.message.materials.values().next().value as Material : undefined; 95 | } 96 | 97 | /** 98 | * Adds contained material or multi-material the provided materials object or it clones and adds new materials according clone instructions. 99 | * 100 | * @param {Map} materials 101 | * @param {boolean} log 102 | * 103 | * @return {Material|Material[]|undefined} 104 | */ 105 | processMaterialTransport(materials: Map, log?: boolean) { 106 | for (const cloneInstruction of this.message.cloneInstructions) { 107 | MaterialUtils.cloneMaterial(materials, cloneInstruction, log); 108 | } 109 | if (this.hasMultiMaterial()) { 110 | // multi-material 111 | const outputMaterials: Material[] = []; 112 | for (const [k, v] of this.message.multiMaterialNames.entries()) { 113 | const mat = materials.get(v); 114 | if (mat) { 115 | outputMaterials[k] = mat; 116 | } 117 | } 118 | return outputMaterials; 119 | } 120 | else { 121 | const singleMaterial = this.getSingleMaterial(); 122 | if (singleMaterial) { 123 | const outputMaterial = materials.get(singleMaterial.name); 124 | return outputMaterial ? outputMaterial : singleMaterial; 125 | } 126 | } 127 | return undefined; 128 | } 129 | } 130 | 131 | export class MaterialsPayloadHandler implements PayloadHandler { 132 | 133 | pack(payload: Payload, transferables: Transferable[], cloneBuffers: boolean) { 134 | const mp = payload as MaterialsPayload; 135 | if (mp.message.buffers !== undefined) { 136 | fillTransferables(mp.message.buffers.values(), transferables, cloneBuffers); 137 | } 138 | mp.message.materialsJson = MaterialUtils.getMaterialsJSON(mp.message.materials); 139 | return transferables; 140 | } 141 | 142 | unpack(transportObject: Payload, cloneBuffers: boolean) { 143 | const mp = transportObject as MaterialsPayload; 144 | const materialsPayload = Object.assign(new MaterialsPayload(), transportObject); 145 | new DataPayloadHandler().unpack(mp, cloneBuffers); 146 | 147 | for (const [k, v] of mp.message.multiMaterialNames.entries()) { 148 | materialsPayload.message.multiMaterialNames.set(k, v); 149 | } 150 | 151 | const materialLoader = new MaterialLoader(); 152 | for (const [k, v] of mp.message.materialsJson.entries()) { 153 | materialsPayload.message.materials.set(k, materialLoader.parse(v)); 154 | } 155 | return materialsPayload; 156 | } 157 | } 158 | 159 | // register the Materials related payload handler 160 | PayloadRegister.handler.set('MaterialsPayload', new MaterialsPayloadHandler()); 161 | -------------------------------------------------------------------------------- /packages/wtd-core/src/offscreen/MainEventProxy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Inspired by: 3 | * https://threejs.org/manual/#en/offscreencanvas 4 | * https://jsfiddle.net/greggman/kuLdptmq/17/ 5 | */ 6 | 7 | import { AssociatedArrayType } from '../Payload.js'; 8 | import { WorkerTask } from '../WorkerTask.js'; 9 | import { WorkerMessage } from '../WorkerMessage.js'; 10 | import { OffscreenPayload } from './OffscreenPayload.js'; 11 | import { OffscreenWorkerCommandRequest, OffscreenWorkerCommandResponse } from './OffscreenWorker.js'; 12 | 13 | export const handlePreventDefault = (event: Event) => { 14 | event.preventDefault(); 15 | }; 16 | 17 | export const MouseEventProperties = [ 18 | 'ctrlKey', 19 | 'metaKey', 20 | 'shiftKey', 21 | 'button', 22 | 'pointerType', 23 | 'pointerId', 24 | 'clientX', 25 | 'clientY', 26 | 'pageX', 27 | 'pageY', 28 | ]; 29 | 30 | export const handleMouseEvent = (event: Event, workerTask: WorkerTask, properties?: string[]) => { 31 | const offscreenPayload = extractProperties(event, properties); 32 | workerTask.sentMessage({ 33 | message: WorkerMessage.fromPayload(offscreenPayload, 'proxyEvent') 34 | }); 35 | }; 36 | 37 | export const WheelEventProperties = [ 38 | 'deltaX', 39 | 'deltaY', 40 | ]; 41 | 42 | export const handleWheelEvent = (event: Event, workerTask: WorkerTask, properties?: string[]) => { 43 | const offscreenPayload = extractProperties(event, properties); 44 | workerTask.sentMessage({ 45 | message: WorkerMessage.fromPayload(offscreenPayload, 'proxyEvent') 46 | }); 47 | }; 48 | 49 | export const KeydownEventProperties = [ 50 | 'ctrlKey', 51 | 'altKey', 52 | 'metaKey', 53 | 'shiftKey', 54 | 'code', 55 | ]; 56 | 57 | // The four arrow keys 58 | export const AllowedKeyProperties = [ 59 | 'ArrowLeft', 60 | 'ArrowUp', 61 | 'ArrowRight', 62 | 'ArrowDown', 63 | 'KeyW', 64 | 'KeyA', 65 | 'KeyS', 66 | 'KeyD' 67 | ]; 68 | 69 | export const handleFilteredKeydownEvent = (event: Event, workerTask: WorkerTask, properties?: string[], positiveList?: string[]) => { 70 | const { code } = event as KeyboardEvent; 71 | if (positiveList !== undefined && positiveList.includes(code)) { 72 | const offscreenPayload = extractProperties(event, properties); 73 | workerTask.sentMessage({ 74 | message: WorkerMessage.fromPayload(offscreenPayload, 'proxyEvent') 75 | }); 76 | } 77 | }; 78 | 79 | export const extractProperties = (event: Event, properties?: string[]) => { 80 | const eventTarget = { 81 | type: event.type, 82 | } as AssociatedArrayType; 83 | if (properties) { 84 | for (const name of properties) { 85 | eventTarget[name] = (event as unknown as AssociatedArrayType)[name]; 86 | } 87 | } 88 | return new OffscreenPayload({ 89 | event: eventTarget 90 | }); 91 | }; 92 | 93 | export const handleTouchEvent = (event: Event, workerTask: WorkerTask) => { 94 | const touches = []; 95 | const touchEvent = event as TouchEvent; 96 | 97 | // eslint-disable-next-line @typescript-eslint/prefer-for-of 98 | for (let i = 0; i < touchEvent.touches.length; ++i) { 99 | const touch = touchEvent.touches[i]; 100 | touches.push({ 101 | pageX: touch.pageX, 102 | pageY: touch.pageY, 103 | }); 104 | } 105 | const offscreenPayload = new OffscreenPayload({ 106 | event: { 107 | type: event.type, 108 | touches 109 | } 110 | }); 111 | workerTask.sentMessage({ 112 | message: WorkerMessage.fromPayload(offscreenPayload, OffscreenWorkerCommandRequest.PROXY_EVENT) 113 | }); 114 | }; 115 | 116 | export type HandlingInstructions = { 117 | handler: (event: Event, workerTask: WorkerTask, properties?: string[], positiveList?: string[]) => void; 118 | properties?: string[]; 119 | positiveList?: string[]; 120 | passive?: boolean; 121 | }; 122 | 123 | export const buildDefaultEventHandlingInstructions = (): Map => { 124 | const handlingInstructions: Map = new Map(); 125 | const contextMenuInstruction: HandlingInstructions = { 126 | handler: handlePreventDefault 127 | }; 128 | const mouseInstruction: HandlingInstructions = { 129 | handler: handleMouseEvent, 130 | properties: MouseEventProperties 131 | }; 132 | const wheelInstruction: HandlingInstructions = { 133 | handler: handleWheelEvent, 134 | properties: WheelEventProperties, 135 | passive: true 136 | }; 137 | const keyboardInstruction: HandlingInstructions = { 138 | handler: handleFilteredKeydownEvent, 139 | properties: KeydownEventProperties, 140 | positiveList: AllowedKeyProperties 141 | }; 142 | const touchInstruction: HandlingInstructions = { 143 | handler: handleTouchEvent, 144 | passive: true 145 | }; 146 | 147 | handlingInstructions.set('contextmenu', contextMenuInstruction); 148 | handlingInstructions.set('mousedown', mouseInstruction); 149 | handlingInstructions.set('mousemove', mouseInstruction); 150 | handlingInstructions.set('mouseup', mouseInstruction); 151 | handlingInstructions.set('pointerdown', mouseInstruction); 152 | handlingInstructions.set('pointermove', mouseInstruction); 153 | handlingInstructions.set('pointerup', mouseInstruction); 154 | handlingInstructions.set('wheel', wheelInstruction); 155 | handlingInstructions.set('keydown', keyboardInstruction); 156 | handlingInstructions.set('touchstart', touchInstruction); 157 | handlingInstructions.set('touchmove', touchInstruction); 158 | handlingInstructions.set('touchend', touchInstruction); 159 | return handlingInstructions; 160 | }; 161 | 162 | export const registerCanvas = async (workerTask: WorkerTask, canvas: HTMLCanvasElement, handlingInstructions: Map) => { 163 | canvas.focus(); 164 | 165 | await workerTask.sentMessage({ 166 | message: WorkerMessage.fromPayload(new OffscreenPayload({}), OffscreenWorkerCommandRequest.PROXY_START), 167 | awaitAnswer: true, 168 | expectedAnswer: OffscreenWorkerCommandResponse.PROXY_START_COMPLETE 169 | }); 170 | 171 | for (const [eventName, instruction] of handlingInstructions.entries()) { 172 | if (eventName.startsWith('key')) { 173 | window.addEventListener(eventName, (event: Event) => { 174 | instruction.handler(event, workerTask, instruction.properties, instruction.positiveList); 175 | }, instruction.passive === true ? { passive: true } : undefined); 176 | } else { 177 | canvas.addEventListener(eventName, (event: Event) => { 178 | instruction.handler(event, workerTask, instruction.properties); 179 | }, instruction.passive === true ? { passive: true } : undefined); 180 | } 181 | } 182 | }; 183 | 184 | export const sentResize = (workerTask: WorkerTask, canvas: HTMLCanvasElement) => { 185 | const dataPayload = new OffscreenPayload({ 186 | width: canvas.offsetWidth, 187 | height: canvas.offsetHeight, 188 | pixelRatio: window.devicePixelRatio 189 | }); 190 | workerTask.sentMessage({ 191 | message: WorkerMessage.fromPayload(dataPayload, OffscreenWorkerCommandRequest.RESIZE), 192 | }); 193 | }; 194 | 195 | export const registerResizeHandler = (workerTask: WorkerTask, canvas: HTMLCanvasElement,) => { 196 | window.addEventListener('resize', () => sentResize(workerTask, canvas), false); 197 | }; 198 | -------------------------------------------------------------------------------- /packages/wtd-three-ext/src/MeshPayload.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | AssociatedArrayType, 3 | ParameterizedMessage, 4 | Payload, 5 | PayloadHandler 6 | } from 'wtd-core'; 7 | import { 8 | PayloadRegister, 9 | fillTransferables 10 | } from 'wtd-core'; 11 | import { 12 | Box3, 13 | BufferAttribute, 14 | BufferGeometry, 15 | InterleavedBufferAttribute, 16 | Mesh, 17 | Sphere 18 | } from 'three'; 19 | 20 | export type AssociatedBufferAttributeArrayType = { [key: string]: BufferAttribute | InterleavedBufferAttribute } 21 | 22 | export type MeshPayloadAdditions = Payload & { 23 | message: MeshPayloadMessageAdditions 24 | } 25 | 26 | export type MeshPayloadMessageAdditions = ParameterizedMessage & { 27 | geometryType: GeometryType; 28 | bufferGeometry: BufferGeometry | AssociatedArrayType | undefined; 29 | meshName: string; 30 | } 31 | 32 | export enum GeometryType { 33 | MESH = 0, 34 | LINE = 1, 35 | POINT = 2 36 | } 37 | 38 | export class MeshPayload implements MeshPayloadAdditions { 39 | 40 | $type = 'MeshPayload'; 41 | message: MeshPayloadMessageAdditions = { 42 | params: {}, 43 | buffers: new Map(), 44 | geometryType: GeometryType.MESH, 45 | bufferGeometry: new BufferGeometry(), 46 | meshName: '' 47 | }; 48 | 49 | /** 50 | * Set the {@link BufferGeometry} and geometry type that can be used when a mesh is created. 51 | * 52 | * @param {BufferGeometry} bufferGeometry 53 | * @param {number} geometryType [0=Mesh|1=LineSegments|2=Points] 54 | */ 55 | setBufferGeometry(bufferGeometry: BufferGeometry, geometryType: GeometryType) { 56 | this.message.bufferGeometry = bufferGeometry; 57 | this.message.geometryType = geometryType; 58 | } 59 | 60 | /** 61 | * Sets the mesh and the geometry type [0=Mesh|1=LineSegments|2=Points] 62 | * @param {Mesh} mesh 63 | * @param {number} geometryType 64 | */ 65 | setMesh(mesh: Mesh, geometryType: GeometryType) { 66 | this.message.meshName = mesh.name; 67 | this.setBufferGeometry(mesh.geometry, geometryType); 68 | } 69 | 70 | } 71 | 72 | export class MeshPayloadHandler implements PayloadHandler { 73 | 74 | pack(payload: Payload, transferables: Transferable[], cloneBuffers: boolean) { 75 | const mp = payload as MeshPayload; 76 | if (mp.message.buffers !== undefined) { 77 | packGeometryBuffers(cloneBuffers, mp.message.bufferGeometry as BufferGeometry, mp.message.buffers); 78 | fillTransferables(mp.message.buffers.values(), transferables, cloneBuffers); 79 | } 80 | return transferables; 81 | } 82 | 83 | unpack(transportObject: Payload, cloneBuffers: boolean) { 84 | const mp = transportObject as MeshPayload; 85 | const meshPayload = Object.assign(new MeshPayload(), mp); 86 | if (meshPayload.message.bufferGeometry !== undefined) { 87 | meshPayload.message.bufferGeometry = reconstructBuffer(cloneBuffers, meshPayload.message.bufferGeometry); 88 | } 89 | return meshPayload; 90 | } 91 | } 92 | 93 | export const packGeometryBuffers = (cloneBuffers: boolean, bufferGeometry: BufferGeometry | undefined, buffers: Map) => { 94 | // fast-fail 95 | if (!(bufferGeometry instanceof BufferGeometry)) return; 96 | 97 | const vertexBA = bufferGeometry.getAttribute('position'); 98 | const normalBA = bufferGeometry.getAttribute('normal'); 99 | const uvBA = bufferGeometry.getAttribute('uv'); 100 | const colorBA = bufferGeometry.getAttribute('color'); 101 | const skinIndexBA = bufferGeometry.getAttribute('skinIndex'); 102 | const skinWeightBA = bufferGeometry.getAttribute('skinWeight'); 103 | const indexBA = bufferGeometry.getIndex(); 104 | 105 | addAttributeToBuffers('position', vertexBA, cloneBuffers, buffers); 106 | addAttributeToBuffers('normal', normalBA, cloneBuffers, buffers); 107 | addAttributeToBuffers('uv', uvBA, cloneBuffers, buffers); 108 | addAttributeToBuffers('color', colorBA, cloneBuffers, buffers); 109 | addAttributeToBuffers('skinIndex', skinIndexBA, cloneBuffers, buffers); 110 | addAttributeToBuffers('skinWeight', skinWeightBA, cloneBuffers, buffers); 111 | addAttributeToBuffers('index', indexBA, cloneBuffers, buffers); 112 | }; 113 | 114 | export const addAttributeToBuffers = (name: string, input: BufferAttribute | InterleavedBufferAttribute | null | undefined, 115 | cloneBuffer: boolean, buffers: Map): void => { 116 | if (input !== undefined && input !== null) { 117 | const typedArray = input.array as unknown as ArrayBufferLike; 118 | buffers.set(name, cloneBuffer ? typedArray.slice(0) : typedArray); 119 | } 120 | }; 121 | 122 | export const reconstructBuffer = (cloneBuffers: boolean, transferredGeometry: BufferGeometry | AssociatedArrayType | undefined): BufferGeometry => { 123 | const bufferGeometry = new BufferGeometry(); 124 | 125 | // fast-fail: transferredGeometry is either rubbish or already a bufferGeometry 126 | if (transferredGeometry instanceof BufferGeometry) { 127 | return transferredGeometry; 128 | } 129 | 130 | if (transferredGeometry?.attributes !== undefined) { 131 | const attr = transferredGeometry.attributes as AssociatedBufferAttributeArrayType; 132 | assignAttributeFromTransfered(bufferGeometry, attr.position, 'position', cloneBuffers); 133 | assignAttributeFromTransfered(bufferGeometry, attr.normal, 'normal', cloneBuffers); 134 | assignAttributeFromTransfered(bufferGeometry, attr.uv, 'uv', cloneBuffers); 135 | assignAttributeFromTransfered(bufferGeometry, attr.color, 'color', cloneBuffers); 136 | assignAttributeFromTransfered(bufferGeometry, attr.skinIndex, 'skinIndex', cloneBuffers); 137 | assignAttributeFromTransfered(bufferGeometry, attr.skinWeight, 'skinWeight', cloneBuffers); 138 | } 139 | 140 | // TODO: morphAttributes 141 | 142 | if (transferredGeometry?.index !== null) { 143 | const indexAttr = transferredGeometry?.index as BufferAttribute; 144 | const indexBuffer = cloneBuffers ? indexAttr.array.slice(0) : indexAttr.array; 145 | bufferGeometry.setIndex(new BufferAttribute(indexBuffer, indexAttr.itemSize, indexAttr.normalized)); 146 | } 147 | 148 | const boundingBox = transferredGeometry?.boundingBox; 149 | if (boundingBox !== null) { 150 | bufferGeometry.boundingBox = Object.assign(new Box3(), boundingBox); 151 | } 152 | 153 | const boundingSphere = transferredGeometry?.boundingSphere; 154 | if (boundingSphere !== null) { 155 | bufferGeometry.boundingSphere = Object.assign(new Sphere(), boundingSphere); 156 | } 157 | 158 | bufferGeometry.uuid = transferredGeometry?.uuid as string; 159 | bufferGeometry.name = transferredGeometry?.name as string; 160 | bufferGeometry.groups = transferredGeometry?.groups as Array<{ start: number; count: number; materialIndex?: number | undefined }>; 161 | bufferGeometry.drawRange = transferredGeometry?.drawRange as { start: number; count: number }; 162 | bufferGeometry.userData = transferredGeometry?.userData as AssociatedArrayType; 163 | return bufferGeometry; 164 | }; 165 | 166 | export const assignAttributeFromTransfered = (bufferGeometry: BufferGeometry, input: BufferAttribute | InterleavedBufferAttribute | undefined, 167 | attrName: string, cloneBuffer: boolean): void => { 168 | if (input) { 169 | const arrayLike = cloneBuffer ? input.array.slice(0) : input.array; 170 | bufferGeometry.setAttribute(attrName, new BufferAttribute(arrayLike, input.itemSize, input.normalized)); 171 | } 172 | }; 173 | 174 | // register the Mesh related payload handler 175 | PayloadRegister.handler.set('MeshPayload', new MeshPayloadHandler()); 176 | -------------------------------------------------------------------------------- /packages/wtd-core/src/WorkerTaskDirector.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WorkerMessage 3 | } from './WorkerMessage.js'; 4 | import type { 5 | WorkerExecutionDef 6 | } from './WorkerTask.js'; 7 | import { 8 | WorkerTask 9 | } from './WorkerTask.js'; 10 | import type { WorkerConfig, EndpointConfigDirect, WorkerMessageDef } from './ComChannelEndpoint.js'; 11 | 12 | interface WorkerTaskRuntimeDesc { 13 | workerTasks: Map; 14 | readonly maxParallelExecutions: number; 15 | } 16 | 17 | interface WorkerTaskDirectorConfig { 18 | defaultMaxParallelExecutions?: number; 19 | verbose?: boolean; 20 | } 21 | 22 | /** 23 | * This is only used internally 24 | */ 25 | interface WorkerExecutionPlan extends WorkerExecutionDef { 26 | promiseFunctions?: { 27 | resolve: (message: WorkerMessage) => void, 28 | reject: (error?: Error) => void 29 | }; 30 | } 31 | 32 | export interface WorkerTaskDirectorTaskDef { 33 | taskName: string; 34 | endpointConfig: WorkerConfig | EndpointConfigDirect; 35 | maxParallelExecutions?: number 36 | } 37 | 38 | /** 39 | * Register one to many tasks type to the WorkerTaskDirector. Then init and enqueue a worker based execution by passing 40 | * configuration and buffers. The WorkerTaskDirector allows to execute a maximum number of executions in parallel for 41 | * each registered worker task. 42 | */ 43 | export class WorkerTaskDirector { 44 | 45 | static DEFAULT_MAX_PARALLEL_EXECUTIONS = 4; 46 | 47 | private defaultMaxParallelExecutions: number; 48 | private verbose = false; 49 | private taskTypes: Map; 50 | private workerExecutionPlans: Map; 51 | 52 | constructor(config?: WorkerTaskDirectorConfig) { 53 | this.defaultMaxParallelExecutions = config?.defaultMaxParallelExecutions ?? WorkerTaskDirector.DEFAULT_MAX_PARALLEL_EXECUTIONS; 54 | this.verbose = config?.verbose === true; 55 | this.taskTypes = new Map(); 56 | this.workerExecutionPlans = new Map(); 57 | } 58 | 59 | /** 60 | * Registers functionality for a new task type based on workerRegistration info 61 | * 62 | * @param {string} taskName The name to be used for registration. 63 | * @param {WorkerConfig | WorkerConfigDirect} workerConfig information regarding the worker to be registered 64 | * @param {number} maxParallelExecutions Number of maximum parallel executions allowed 65 | * @return {boolean} Tells if registration is possible (new=true) or if task was already registered (existing=false) 66 | */ 67 | registerTask(workerTaskDirectorDef: WorkerTaskDirectorTaskDef) { 68 | const taskName = workerTaskDirectorDef.taskName; 69 | const allowedToRegister = !this.taskTypes.has(taskName); 70 | if (allowedToRegister) { 71 | const maxParallelExecutions = workerTaskDirectorDef.maxParallelExecutions ?? this.defaultMaxParallelExecutions; 72 | const workerTaskRuntimeDesc: WorkerTaskRuntimeDesc = { 73 | workerTasks: new Map(), 74 | maxParallelExecutions: maxParallelExecutions 75 | }; 76 | this.taskTypes.set(taskName, workerTaskRuntimeDesc); 77 | for (let i = 0; i < maxParallelExecutions; i++) { 78 | workerTaskRuntimeDesc.workerTasks.set(i, new WorkerTask({ 79 | endpointName: taskName, 80 | endpointId: i, 81 | endpointConfig: workerTaskDirectorDef.endpointConfig, 82 | verbose: this.verbose 83 | })); 84 | } 85 | } 86 | return allowedToRegister; 87 | } 88 | 89 | /** 90 | * Provides initialization configuration and transferable objects. 91 | * 92 | * @param {string} taskTypeName The name of the registered task type. 93 | * @param {WorkerMessageDef} [def] Initialization instructions. 94 | */ 95 | async initTaskType(taskTypeName: string, def?: WorkerMessageDef) { 96 | const executions = []; 97 | const workerTaskRuntimeDesc = this.taskTypes.get(taskTypeName); 98 | if (workerTaskRuntimeDesc) { 99 | this.workerExecutionPlans.set(taskTypeName, []); 100 | for (const workerTask of workerTaskRuntimeDesc.workerTasks.values()) { 101 | // only init worker if a def is provided 102 | workerTask.connect(); 103 | if (def) { 104 | executions.push(workerTask.initWorker({ 105 | message: def.message, 106 | transferables: def.transferables, 107 | copyTransferables: def.copyTransferables === true 108 | })); 109 | } 110 | } 111 | } else { 112 | executions.push(Promise.reject()); 113 | } 114 | if (executions.length === 0) { 115 | executions.push(Promise.resolve()); 116 | } 117 | return Promise.all(executions); 118 | } 119 | 120 | /** 121 | * Queues a new task of the given type. Task will not execute until initialization completes. 122 | * 123 | * @param {string} taskTypeName The name of the registered task type. 124 | * @param {WorkerExecutionDef} Defines all the information needed to execute the worker task. 125 | * @return {Promise} 126 | */ 127 | async enqueueForExecution(taskTypeName: string, workerExecutionDef: WorkerExecutionDef): Promise { 128 | const plan = workerExecutionDef as WorkerExecutionPlan; 129 | const promise = new Promise((resolve, reject) => { 130 | plan.promiseFunctions = { 131 | resolve: resolve, 132 | reject: reject 133 | }; 134 | }) as Promise; 135 | 136 | const planForType = this.workerExecutionPlans.get(taskTypeName); 137 | planForType?.push(plan); 138 | this.depleteWorkerExecutionPlans(taskTypeName); 139 | return promise; 140 | } 141 | 142 | private async depleteWorkerExecutionPlans(taskTypeName: string) { 143 | const planForType = this.workerExecutionPlans.get(taskTypeName); 144 | if (planForType?.length === 0) { 145 | if (this.verbose) { 146 | console.log(`No more WorkerExecutionPlans in the queue for: ${taskTypeName}`); 147 | } 148 | return; 149 | } 150 | const plan = planForType?.shift(); 151 | if (plan) { 152 | const workerTaskRuntimeDesc = this.taskTypes.get(taskTypeName); 153 | const workerTask = this.getUnusedWorkerTask(workerTaskRuntimeDesc); 154 | if (workerTask) { 155 | try { 156 | const result = await workerTask.executeWorker(plan); 157 | plan.promiseFunctions?.resolve(result); 158 | this.depleteWorkerExecutionPlans(taskTypeName); 159 | } catch (e) { 160 | plan.promiseFunctions?.reject(new Error('Execution error: ' + e)); 161 | this.depleteWorkerExecutionPlans(taskTypeName); 162 | } 163 | } 164 | else { 165 | planForType?.unshift(plan); 166 | } 167 | } 168 | } 169 | 170 | private getUnusedWorkerTask(workerTaskRuntimeDesc: WorkerTaskRuntimeDesc | undefined) { 171 | if (workerTaskRuntimeDesc) { 172 | for (const workerTask of workerTaskRuntimeDesc.workerTasks.values()) { 173 | if (!workerTask.isWorkerExecuting()) { 174 | return workerTask; 175 | } 176 | } 177 | } 178 | return undefined; 179 | } 180 | 181 | /** 182 | * Destroys all workers and associated resources. 183 | * @return {WorkerTaskDirector} 184 | */ 185 | dispose() { 186 | for (const workerTaskRuntimeDesc of this.taskTypes.values()) { 187 | for (const workerTask of workerTaskRuntimeDesc.workerTasks.values()) { 188 | workerTask.dispose(); 189 | } 190 | } 191 | return this; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /packages/wtd-core/src/ComChannelEndpoint.ts: -------------------------------------------------------------------------------- 1 | import { comRouting } from './utilities.js'; 2 | import { WorkerMessage } from './WorkerMessage.js'; 3 | 4 | export interface WorkerConfig { 5 | $type: 'WorkerConfigParams' 6 | workerType: 'classic' | 'module'; 7 | blob?: boolean; 8 | url: URL | string | undefined; 9 | } 10 | 11 | export interface EndpointConfigDirect { 12 | $type: 'DirectImplConfig'; 13 | impl: Worker | MessagePort | DedicatedWorkerGlobalScope; 14 | } 15 | 16 | export interface AwaitHandler { 17 | name: string; 18 | resolve: Array<(wm: WorkerMessage) => void>; 19 | reject: (error: Error) => void; 20 | remove: boolean; 21 | log: boolean; 22 | } 23 | 24 | export interface WorkerMessageDef { 25 | message: WorkerMessage; 26 | transferables?: Transferable[]; 27 | copyTransferables?: boolean; 28 | expectedAnswer?: string; 29 | awaitAnswer?: boolean; 30 | } 31 | 32 | export interface ComChannelEndpointConfig { 33 | endpointId: number; 34 | endpointConfig: WorkerConfig | EndpointConfigDirect; 35 | verbose?: boolean; 36 | endpointName: string; 37 | } 38 | 39 | export interface ComRouter { 40 | setComChannelEndpoint(comChannelEndpoint: ComChannelEndpoint): void; 41 | } 42 | 43 | export class ComChannelEndpoint { 44 | 45 | protected endpointId: number; 46 | protected endpointName: string; 47 | protected endpointConfig: WorkerConfig | EndpointConfigDirect; 48 | protected verbose = false; 49 | 50 | protected impl?: Worker | MessagePort | DedicatedWorkerGlobalScope; 51 | protected executionCounter = 0; 52 | protected awaitAnswers = new Map(); 53 | 54 | constructor(config: ComChannelEndpointConfig) { 55 | this.endpointId = config.endpointId; 56 | this.endpointConfig = config.endpointConfig; 57 | this.verbose = config.verbose === true; 58 | this.endpointName = config.endpointName; 59 | } 60 | 61 | getImpl() { 62 | return this.impl; 63 | } 64 | 65 | connect(comRoutingHandler?: ComRouter) { 66 | if (this.impl) { 67 | throw new Error('Worker already created. Aborting...'); 68 | } 69 | if (this.endpointConfig.$type === 'DirectImplConfig') { 70 | this.impl = this.endpointConfig.impl; 71 | } else { 72 | if (this.endpointConfig.url !== undefined) { 73 | if (this.endpointConfig.blob === true) { 74 | this.impl = new Worker(this.endpointConfig.url); 75 | } 76 | else { 77 | this.impl = new Worker((this.endpointConfig.url as URL).href, { 78 | type: this.endpointConfig.workerType 79 | }); 80 | } 81 | } 82 | } 83 | 84 | if (!this.impl) { 85 | throw new Error('No valid worker configuration was supplied. Aborting...'); 86 | } 87 | 88 | this.impl.onmessage = (async (message) => { 89 | if (comRoutingHandler !== undefined) { 90 | comRoutingHandler.setComChannelEndpoint(this); 91 | comRouting(comRoutingHandler, message); 92 | } 93 | this.processAwaitHandlerRemoval(message); 94 | }); 95 | this.impl.onmessageerror = (async (msg) => { 96 | console.log(`Received errornuous message: ${msg}`); 97 | Promise.reject(msg); 98 | }); 99 | 100 | if (Object.hasOwn(this.impl ?? {}, 'onerror')) { 101 | (this.impl as Worker).onerror = (async (message) => { 102 | console.log(`Execution Aborted: ${message.error}`); 103 | Promise.reject(message); 104 | }); 105 | } 106 | } 107 | 108 | /** 109 | * This is only possible if the worker is available. 110 | */ 111 | sentMessage(def: WorkerMessageDef): Promise { 112 | if (this.impl === undefined) { 113 | return Promise.reject(new Error('No worker is available. Aborting...')); 114 | } 115 | 116 | return new Promise((resolve, reject) => { 117 | if (this.checkWorker(reject)) { 118 | const message = def.message; 119 | 120 | if (message.cmd === 'unknown' || message.cmd.length === 0) { 121 | throw new Error('No command provided. Aborting...'); 122 | } 123 | const transferablesToWorker = this.handleTransferables(def); 124 | 125 | if (def.awaitAnswer === true) { 126 | if (def.expectedAnswer === undefined) { 127 | reject(new Error('No answer name provided. Aborting...')); 128 | return; 129 | } 130 | this.updateAwaitHandlers(message, [{ 131 | name: def.expectedAnswer, 132 | resolve: [resolve], 133 | reject: reject, 134 | remove: true, 135 | log: this.verbose 136 | }]); 137 | } 138 | this.impl?.postMessage(message, transferablesToWorker); 139 | 140 | if (def.awaitAnswer === false) { 141 | resolve(WorkerMessage.createEmpty()); 142 | } 143 | } 144 | }); 145 | } 146 | 147 | sentAnswer(def: WorkerMessageDef): Promise { 148 | def.message.answer = true; 149 | return this.sentMessage(def); 150 | } 151 | 152 | protected updateAwaitHandlers(wm: WorkerMessage, awaitHandlers: AwaitHandler[]) { 153 | wm.endpointdId = this.endpointId; 154 | wm.uuid = this.buildUuid(); 155 | this.awaitAnswers.set(wm.uuid, awaitHandlers); 156 | } 157 | 158 | protected buildUuid() { 159 | return `${this.endpointId}_${this.executionCounter++}_${Math.floor(Math.random() * 100000000)}`; 160 | } 161 | 162 | protected handleTransferables(def: WorkerMessageDef) { 163 | let transferablesToWorker: Transferable[] = []; 164 | if (def.transferables !== undefined) { 165 | // copy transferables if wanted 166 | if (def.copyTransferables === true) { 167 | for (const transferable of def.transferables) { 168 | transferablesToWorker.push((transferable as ArrayBufferLike).slice(0)); 169 | } 170 | } else { 171 | transferablesToWorker = def.transferables; 172 | } 173 | } 174 | return transferablesToWorker; 175 | } 176 | 177 | processAwaitHandlerRemoval(message: MessageEvent): Promise | void { 178 | const data = (message as MessageEvent).data; 179 | // only process WorkerMessage 180 | if (Object.hasOwn(data, 'cmd')) { 181 | const wm = message.data as WorkerMessage; 182 | const awaitHandlers = this.awaitAnswers.get(wm.uuid); 183 | awaitHandlers?.forEach(handler => this.removeAwaitHandler(handler, wm)); 184 | } else { 185 | console.error(`Received: unknown message: ${message}`); 186 | } 187 | } 188 | 189 | /** 190 | * 191 | * @param handler 192 | * @param wm 193 | * @returns returns if the handler was removed 194 | */ 195 | protected removeAwaitHandler(handler: AwaitHandler, wm: WorkerMessage): boolean { 196 | if (handler.name === wm.cmd) { 197 | if (handler.log === true) { 198 | const completionMsg = `${this.endpointName}: Received: ${wm.cmd} (workerName: ${wm.name}) with uuid: ${wm.uuid}`; 199 | console.log(completionMsg); 200 | } 201 | for (const resolve of handler.resolve) { 202 | resolve(wm); 203 | } 204 | if (handler.remove === true) { 205 | this.awaitAnswers.delete(wm.uuid); 206 | } 207 | return true; 208 | } 209 | return false; 210 | } 211 | 212 | protected checkWorker(reject: (error: Error) => void) { 213 | if (!this.impl) { 214 | reject(new Error('No worker is available. Aborting...')); 215 | return false; 216 | } 217 | return true; 218 | } 219 | 220 | dispose() { 221 | if (this.impl !== undefined) { 222 | if (Object.hasOwn(this.impl, 'terminate')) { 223 | (this.impl as Worker).terminate(); 224 | } else if (Object.hasOwn(this.impl, 'start')) { 225 | (this.impl as MessagePort).close(); 226 | } else { 227 | (this.impl as DedicatedWorkerGlobalScope).close(); 228 | } 229 | } 230 | } 231 | 232 | printAwaitAnswers() { 233 | console.log('awaitAnswers:'); 234 | console.log(this.awaitAnswers); 235 | } 236 | 237 | } 238 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WorkerTask, WorkerTaskDirector and three.js extensions 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/kaisalmen/wtd/blob/main/LICENSE) 4 | [![wtd](https://github.com/kaisalmen/wtd/actions/workflows/actions.yml/badge.svg)](https://github.com/kaisalmen/wtd/actions/workflows/actions.yml) 5 | [![Github Pages](https://img.shields.io/badge/GitHub-Pages-blue?logo=github)](https://kaisalmen.github.io/wtd) 6 | [![wtd-core version](https://img.shields.io/npm/v/wtd-core?logo=npm&label=wtd-core)](https://www.npmjs.com/package/wtd-core) 7 | [![wtd-three-ext version](https://img.shields.io/npm/v/wtd-three-ext?logo=npm&label=wtd-three-ext)](https://www.npmjs.com/package/wtd-three-ext) 8 | [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/kaisalmen/wtd) 9 | 10 | Build applications with workers with less boiler plate code. 11 | 12 | - [WorkerTask, WorkerTaskDirector and three.js extensions](#workertask-workertaskdirector-and-threejs-extensions) 13 | - [Overview](#overview) 14 | - [Examples](#examples) 15 | - [Usage](#usage) 16 | - [Getting Started](#getting-started) 17 | - [WorkerTaskDirector Execution Workflow](#workertaskdirector-execution-workflow) 18 | - [Main Branches](#main-branches) 19 | - [Docs](#docs) 20 | - [History](#history) 21 | 22 | ## Overview 23 | 24 | - [wtd-core](https://www.npmjs.com/package/wtd-core) main features: 25 | - `WorkerTask`: Defines a non-mandatory (**New with v3**) lifecycle and message protocol (`WorkerTaskMessage`) with optional `Payload` for using and re-using workers. Either use `init` and `execute` funtions to follow a basic lifecycle or send various message either awaiting feedback or not. Use a `WorkerTaskWorker` to connect your ESM worker code with the communication routing. `WorkerTask` ensures aynchronous feedback reaches the caller. 26 | - `WorkerTaskManager`: Manages the execution of mutliple `WorkerTask` and multiple instances in parallel. It allows to queue tasks for later execution. This only works when the basic lifecycle is used. 27 | - **New with v3**: Helper functions for creating an OffscreenCanvas and delegating events to the worker. It provides a default configuration, but allows to customize all aspects of the configuration to your specific needs. This work was inspired by [three.js optimization manual](https://threejs.org/manual/#en/offscreencanvas) 28 | - [wtd-three-ext](https://www.npmjs.com/package/wtd-three-ext) main features: 29 | - [three.js](https://github.com/mrdoob/three.js) related extension. It allows to define/extend "enhanced" payloads useful in the context of three.js (e.g. exchange Mesh or Material data). 30 | - **New with v3**: Extension to the OffscreenCanvas functions. It can trick the code running in the worker to think it has a real canvas allowing to re-use the exact same code. This work was also inspired by [three.js optimization manual](https://threejs.org/manual/#en/offscreencanvas) 31 | 32 | ## Examples 33 | 34 | There are multiple examples available demonstarting the features described above (listed from simpler to more advanced): 35 | 36 | - Using [wtd-core](https://www.npmjs.com/package/wtd-core) only: 37 | - **ComChannelEndpoint: Hello World**: [html](https://github.com/kaisalmen/wtd/blob/main/packages/examples/helloWorldComChannelEndpoint.html), [ts](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/helloWorld/HelloWorldComChannelEndpoint.ts), [worker](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/HelloWorldComChannelEndpointWorker.ts) 38 | - **WorkerTask: Hello World**: [html](https://github.com/kaisalmen/wtd/blob/main/packages/examples/helloWorldWorkerTask.html), [ts](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/helloWorld/HelloWorldWorkerTask.ts), [worker](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/HelloWorldWorker.ts) 39 | - **WorkerTaskDirector: Hello World**: [html](https://github.com/kaisalmen/wtd/blob/main/packages/examples/helloWorldWorkerTaskDirector.html), [ts](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/helloWorld/helloWorldWorkerTaskDirector.ts), [worker](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/HelloWorldWorker.ts) 40 | - **WorkerTask: Inter-Worker Communication**: [html](https://github.com/kaisalmen/wtd/blob/main/packages/examples/workerCom.html), [ts](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/com/WorkerCom.ts), **Worker**: [1](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/Com1Worker.ts) and [2](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/Com2Worker.ts) 41 | - Using [wtd-core](https://www.npmjs.com/package/wtd-core) and [wtd-three-ext](https://www.npmjs.com/package/wtd-three-ext): 42 | - **WorkerTaskDirector: Transferables**: [html](https://github.com/kaisalmen/wtd/blob/main/packages/examples/transferables.html), [ts](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/transferables/TransferablesTestbed.ts), **Worker**: [1](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/TransferableWorkerTest1.ts), [2](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/TransferableWorkerTest2.ts), [3](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/TransferableWorkerTest3.ts), [4](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/TransferableWorkerTest4.ts) 43 | - **WorkerTaskDirector: Three.js**: [html](https://github.com/kaisalmen/wtd/blob/main/packages/examples/threejs.html), [ts](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/threejs/Threejs.ts), **Worker**: [1](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/HelloWorldThreeWorker.ts), [2](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/OBJLoaderWorker.ts) 44 | - **WorkerTaskDirector: Potentially Infinite Execution**: [html](https://github.com/kaisalmen/wtd/blob/main/packages/examples/potentially_infinite.html), [ts](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/infinite/PotentiallyInfiniteExample.ts), **Worker**: [1](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/InfiniteWorkerExternalGeometry.ts), [2](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/worker/InfiniteWorkerInternalGeometry.ts), [3](https://github.com/kaisalmen/WWOBJLoader/blob/main/packages/objloader2/src/worker/OBJLoader2Worker.ts), [4](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/infinite/PotentiallyInfiniteExample.ts#L627-L668) 45 | 46 | Try out all examples here: 47 | 48 | ### Usage 49 | 50 | This shall give you an idea how you can use module worker with `WorkerTask` (derived from [WorkerTask: Hello World](https://github.com/kaisalmen/wtd/blob/main/packages/examples/src/helloWorld/HelloWorldWorkerTask.ts)): 51 | 52 | ```js 53 | // let WorkerTask create the worker 54 | const workerTask = new WorkerTask({ 55 | taskName, 56 | workerId: 1, 57 | workerConfig: { 58 | $type: 'WorkerConfigParams', 59 | url: new URL('./HelloWorldWorker.js', import.meta.url), 60 | workerType: 'module', 61 | }, 62 | verbose: true 63 | }); 64 | 65 | try { 66 | // creates and connects the worker callback functions and the WorkerTask 67 | workerTask.connect(); 68 | 69 | // execute without init and an empty message 70 | const resultExec = await workerTask.executeWorker({ 71 | message: WorkerTaskMessage.createEmpty() 72 | }); 73 | 74 | // once you awaited the resulting WorkerTaskMessage extract the RawPayload 75 | const rawPayload = resultExec.payloads?.[0] as RawPayload; 76 | 77 | // log the hello from the HelloWorldWorker 78 | console.log(`Worker said: ${rawPayload.message.raw?.hello}`); 79 | } catch (e) { 80 | // error handling 81 | console.error(e); 82 | } 83 | ``` 84 | 85 | ## Getting Started 86 | 87 | There exist three possibilities: 88 | 89 | - Checkout the repository and run the following to spin up the local Vite dev server: 90 | 91 | ```shell 92 | npm install 93 | npm run build 94 | npm run dev 95 | ``` 96 | 97 | - Press the `Gitpod` button above and start coding and using the examples directly in the browser 98 | - Checkout the repository and use `docker-compose up -d` to spin up local Vite dev server 99 | 100 | Whatever environment you choose to start [Vite](https://vitejs.dev/) is used to serve the code and the examples using it. With this setup you are able to change the code and examples without invoking an additional bundler. Vite ensures all imported npm modules are available if previously installed in local environment (see `npm install`). 101 | 102 | If you run Vite locally you require a `nodejs` and `npm`. The Gitpod and local docker environment ensure all prerequisites are fulfilled. 103 | 104 | In any environment the dev server is reachable on port 23001. 105 | 106 | ## WorkerTaskDirector Execution Workflow 107 | 108 | The following table describes the currently implemented execution workflow of `WorkerTaskDirector`: 109 | 110 | | WorkerTaskDirector (function) | Message cmd + direction | Worker (function) | Comment 111 | | --- | :---: | --- | --- 112 | | `registerTask` | | | 113 | | `initTaskType` | **init ->** | `init` | User of `initTaskType` receives resolved promise after execution completion.
114 | Sending `init` message is Optional 115 | | | **<- initComplete** | | 116 | | `enqueueForExecution` | **exec ->** | `exec` | 117 | | | **<- intermediate** | | Can be sent 0 to n times before **execComplete** 118 | | | **<- execComplete** | | User of `enqueueForExecution` receives resolved promise after execution completion.
119 | Callbacks `onIntermediate` and `onComplete` are used to handle message+payload. 120 | 121 | ## Main Branches 122 | 123 | Main development takes place on branch [main](https://github.com/kaisalmen/wtd/tree/main). 124 | 125 | ## Docs 126 | 127 | Run `npm run doc` to create the markdown documentation in directory **docs** of each package. 128 | 129 | ## History 130 | 131 | The orginal idea of a "TaskManager" was proposed by in Don McCurdy here [three.js issue 18234](https://github.com/mrdoob/three.js/issues/18234) It evolved from [three.js PR 19650](https://github.com/mrdoob/three.js/pull/19650) into this repository. 132 | 133 | With version v2.0.0 the core library [wtd-core](https://github.com/kaisalmen/wtd/blob/main/packages/wtd-core) and the three.js extensions [wtd-three-ext](https://github.com/kaisalmen/wtd/blob/main/packages/wtd-three-ext) were separated into different npm packages [wtd-core](https://www.npmjs.com/package/wtd-core) and [wtd-three-ext](https://www.npmjs.com/package/wtd-three-ext). 134 | 135 | Happy coding! 136 | 137 | Kai 138 | -------------------------------------------------------------------------------- /packages/examples/src/transferables/TransferablesTestbed.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AmbientLight, 3 | BufferGeometry, 4 | Color, 5 | DirectionalLight, 6 | GridHelper, 7 | Mesh, 8 | MeshPhongMaterial, 9 | PerspectiveCamera, 10 | Scene, 11 | TorusGeometry, 12 | Vector3, 13 | WebGLRenderer 14 | } from 'three'; 15 | import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'; 16 | 17 | import { 18 | DataPayload, 19 | WorkerTaskCommandResponse, 20 | WorkerTaskDirector, 21 | WorkerMessage 22 | } from 'wtd-core'; 23 | import { 24 | MeshPayload, 25 | reconstructBuffer 26 | } from 'wtd-three-ext'; 27 | 28 | type CameraDefaults = { 29 | posCamera: Vector3; 30 | posCameraTarget: Vector3; 31 | near: number; 32 | far: number; 33 | fov: number; 34 | }; 35 | 36 | type ExampleTask = { 37 | execute: boolean; 38 | name: string; 39 | sendGeometry: boolean; 40 | url: URL; 41 | segments: number; 42 | } 43 | 44 | class TransferablesTestbed { 45 | 46 | private renderer: WebGLRenderer; 47 | private canvas: HTMLElement; 48 | private scene: Scene = new Scene(); 49 | private camera: PerspectiveCamera; 50 | private cameraTarget: Vector3; 51 | private cameraDefaults: CameraDefaults = { 52 | posCamera: new Vector3(1000.0, 1000.0, 1000.0), 53 | posCameraTarget: new Vector3(0, 0, 0), 54 | near: 0.1, 55 | far: 10000, 56 | fov: 45 57 | }; 58 | private controls: TrackballControls; 59 | private workerTaskDirector: WorkerTaskDirector = new WorkerTaskDirector({ 60 | defaultMaxParallelExecutions: 1, 61 | verbose: true 62 | }); 63 | private tasks: ExampleTask[] = []; 64 | 65 | constructor(elementToBindTo: HTMLElement | null) { 66 | if (elementToBindTo === null) { 67 | throw Error('Bad element HTML given as canvas.'); 68 | } 69 | 70 | this.canvas = elementToBindTo; 71 | this.renderer = new WebGLRenderer({ 72 | canvas: this.canvas, 73 | antialias: true 74 | }); 75 | this.renderer.setClearColor(0x050505); 76 | 77 | this.cameraTarget = this.cameraDefaults.posCameraTarget; 78 | this.camera = new PerspectiveCamera(this.cameraDefaults.fov, this.recalcAspectRatio(), this.cameraDefaults.near, this.cameraDefaults.far); 79 | this.resetCamera(); 80 | 81 | this.controls = new TrackballControls(this.camera, this.renderer.domElement); 82 | 83 | this.tasks.push({ 84 | execute: true, 85 | name: 'TransferableWorkerTest1', 86 | sendGeometry: false, 87 | url: new URL(import.meta.env.DEV ? '../worker/TransferableWorkerTest1.ts' : '../worker/generated/TransferableWorkerTest1-es.js', import.meta.url), 88 | segments: 0 89 | }); 90 | this.tasks.push({ 91 | execute: true, 92 | name: 'TransferableWorkerTest2', 93 | sendGeometry: false, 94 | url: new URL(import.meta.env.DEV ? '../worker/TransferableWorkerTest2.ts' : '../worker/generated/TransferableWorkerTest2-es.js', import.meta.url), 95 | segments: 0 96 | }); 97 | this.tasks.push({ 98 | execute: true, 99 | name: 'TransferableWorkerTest3', 100 | sendGeometry: true, 101 | url: new URL(import.meta.env.DEV ? '../worker/TransferableWorkerTest3.ts' : '../worker/generated/TransferableWorkerTest3-es.js', import.meta.url), 102 | segments: 1024 103 | }); 104 | this.tasks.push({ 105 | execute: true, 106 | name: 'TransferableWorkerTest4', 107 | sendGeometry: false, 108 | url: new URL(import.meta.env.DEV ? '../worker/TransferableWorkerTest4.ts' : '../worker/generated/TransferableWorkerTest4-es.js', import.meta.url), 109 | segments: 1024 110 | }); 111 | 112 | this.renderer = new WebGLRenderer({ 113 | canvas: this.canvas, 114 | antialias: true 115 | }); 116 | this.renderer.setClearColor(0x050505); 117 | 118 | this.scene = new Scene(); 119 | 120 | this.recalcAspectRatio(); 121 | this.camera = new PerspectiveCamera(this.cameraDefaults.fov, this.recalcAspectRatio(), this.cameraDefaults.near, this.cameraDefaults.far); 122 | this.resetCamera(); 123 | this.controls = new TrackballControls(this.camera, this.renderer.domElement); 124 | 125 | const ambientLight = new AmbientLight(0x404040); 126 | const directionalLight1 = new DirectionalLight(0xC0C090); 127 | const directionalLight2 = new DirectionalLight(0xC0C090); 128 | 129 | directionalLight1.position.set(- 100, - 50, 100); 130 | directionalLight2.position.set(100, 50, - 100); 131 | 132 | this.scene.add(directionalLight1); 133 | this.scene.add(directionalLight2); 134 | this.scene.add(ambientLight); 135 | 136 | const helper = new GridHelper(1000, 30, 0xFF4444, 0x404040); 137 | this.scene.add(helper); 138 | } 139 | 140 | resizeDisplayGL() { 141 | this.controls.handleResize(); 142 | this.renderer.setSize(this.canvas.offsetWidth, this.canvas.offsetHeight, false); 143 | this.updateCamera(); 144 | } 145 | 146 | recalcAspectRatio() { 147 | return (this.canvas.offsetHeight === 0) ? 1 : this.canvas.offsetWidth / this.canvas.offsetHeight; 148 | } 149 | 150 | resetCamera() { 151 | this.camera.position.copy(this.cameraDefaults.posCamera); 152 | this.cameraTarget.copy(this.cameraDefaults.posCameraTarget); 153 | this.updateCamera(); 154 | } 155 | 156 | updateCamera() { 157 | this.camera.aspect = this.recalcAspectRatio(); 158 | this.camera.lookAt(this.cameraTarget); 159 | this.camera.updateProjectionMatrix(); 160 | } 161 | 162 | render() { 163 | if (!this.renderer.autoClear) this.renderer.clear(); 164 | this.controls.update(); 165 | this.renderer.render(this.scene, this.camera); 166 | } 167 | 168 | async run() { 169 | console.time('All tasks have been initialized'); 170 | try { 171 | await Promise.all(app.initTasks()); 172 | console.timeEnd('All tasks have been initialized'); 173 | app.executeTasks(); 174 | } catch (e) { 175 | alert(e); 176 | } 177 | } 178 | 179 | /** 180 | * Registers any selected task at the {@link WorkerTaskDirector} and initializes them. 181 | * 182 | * @return {Promise} 183 | */ 184 | private initTasks() { 185 | const awaiting = []; 186 | for (const task of this.tasks) { 187 | awaiting.push(this.initTask(task)); 188 | } 189 | return awaiting; 190 | } 191 | 192 | private initTask(task: ExampleTask) { 193 | // fast-fail: direct resolve a void Promise 194 | if (!task.execute) { 195 | return new Promise((resolve) => { 196 | resolve(); 197 | }); 198 | } 199 | 200 | this.workerTaskDirector.registerTask({ 201 | taskName: task.name, 202 | endpointConfig: { 203 | $type: 'WorkerConfigParams', 204 | workerType: 'module', 205 | blob: false, 206 | url: task.url 207 | } 208 | }); 209 | 210 | const initMessage = WorkerMessage.createNew({ 211 | name: task.name 212 | }); 213 | if (task.sendGeometry) { 214 | const torus = new TorusGeometry(25, 8, 16, 100); 215 | torus.name = 'torus'; 216 | 217 | const meshPayload = new MeshPayload(); 218 | meshPayload.setBufferGeometry(torus, 0); 219 | initMessage.addPayload(meshPayload); 220 | 221 | const transferables = WorkerMessage.pack(initMessage.payloads, false); 222 | return this.workerTaskDirector.initTaskType(initMessage.name!, { 223 | message: initMessage, 224 | transferables, 225 | copyTransferables: true 226 | }); 227 | } 228 | else { 229 | return this.workerTaskDirector.initTaskType(initMessage.name!, { 230 | message: initMessage 231 | }); 232 | } 233 | } 234 | 235 | private async executeTasks() { 236 | console.time('Execute tasks'); 237 | const awaiting = []; 238 | for (const task of this.tasks) { 239 | if (task.execute) { 240 | awaiting.push(this.executeWorker(task)); 241 | } 242 | } 243 | await Promise.all(awaiting); 244 | console.timeEnd('Execute tasks'); 245 | console.log('All worker executions have been completed'); 246 | } 247 | 248 | private async executeWorker(task: ExampleTask) { 249 | const execMessage = WorkerMessage.createNew({ 250 | name: task.name 251 | }); 252 | 253 | const dataPayload = new DataPayload(); 254 | dataPayload.message.params = { 255 | name: task.name, 256 | segments: task.segments 257 | }; 258 | execMessage.addPayload(dataPayload); 259 | const transferables = WorkerMessage.pack(execMessage.payloads, false); 260 | 261 | const result = await this.workerTaskDirector.enqueueForExecution(task.name, { 262 | message: execMessage, 263 | transferables: transferables 264 | }); 265 | 266 | this.processMessage(result); 267 | } 268 | 269 | private processMessage(message: WorkerMessage) { 270 | let wm; 271 | switch (message.cmd) { 272 | case WorkerTaskCommandResponse.EXECUTE_COMPLETE: 273 | console.log(`TransferableTestbed#execComplete: name: ${message.name} uuid: ${message.uuid} cmd: ${message.cmd} workerId: ${message.endpointdId}`); 274 | 275 | wm = WorkerMessage.unpack(message, false); 276 | if (wm.payloads.length === 1) { 277 | 278 | const payload = wm.payloads[0]; 279 | if (payload.$type === 'DataPayload') { 280 | const dataPayload = payload as DataPayload; 281 | if (dataPayload.message.params !== undefined && Object.keys(dataPayload.message.params).length > 0 && 282 | dataPayload.message.params.geometry !== undefined) { 283 | const mesh = new Mesh( 284 | reconstructBuffer(false, dataPayload.message.params.geometry as BufferGeometry), 285 | new MeshPhongMaterial({ color: new Color(0xff0000) }) 286 | ); 287 | mesh.position.set(100, 0, 0); 288 | this.scene.add(mesh); 289 | } 290 | else { 291 | console.log(`${message.name}: Just data`); 292 | } 293 | } 294 | 295 | if (payload.$type === 'MeshPayload') { 296 | const meshPayload = payload as MeshPayload; 297 | if (meshPayload.message.bufferGeometry !== undefined) { 298 | const mesh = new Mesh( 299 | meshPayload.message.bufferGeometry as BufferGeometry, 300 | new MeshPhongMaterial({ color: new Color(0xff0000) }) 301 | ); 302 | this.scene.add(mesh); 303 | } 304 | } 305 | } 306 | break; 307 | 308 | default: 309 | console.error(`${message.uuid}: Received unknown command: ${message.cmd}`); 310 | break; 311 | } 312 | } 313 | } 314 | 315 | const app = new TransferablesTestbed(document.getElementById('example')); 316 | 317 | window.addEventListener('resize', () => app.resizeDisplayGL(), false); 318 | 319 | console.log('Starting initialisation phase...'); 320 | app.resizeDisplayGL(); 321 | 322 | const requestRender = () => { 323 | requestAnimationFrame(requestRender); 324 | app.render(); 325 | }; 326 | requestRender(); 327 | 328 | app.run(); 329 | -------------------------------------------------------------------------------- /packages/examples/src/threejs/Threejs.ts: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | AmbientLight, 4 | BufferGeometry, 5 | Color, 6 | DirectionalLight, 7 | FileLoader, 8 | GridHelper, 9 | Mesh, 10 | MeshPhongMaterial, 11 | PerspectiveCamera, 12 | Scene, 13 | Vector3, 14 | WebGLRenderer 15 | } from 'three'; 16 | import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js'; 17 | 18 | import { 19 | DataPayload, 20 | WorkerTaskCommandResponse, 21 | WorkerTaskDirector, 22 | WorkerMessage 23 | } from 'wtd-core'; 24 | import { 25 | MaterialsPayload, 26 | MaterialStore, 27 | MeshPayload 28 | } from 'wtd-three-ext'; 29 | 30 | export type CameraDefaults = { 31 | posCamera: Vector3; 32 | posCameraTarget: Vector3; 33 | near: number; 34 | far: number; 35 | fov: number; 36 | }; 37 | 38 | /** 39 | * The aim of this example is to show two possible ways how to use the {@link WorkerTaskDirector}: 40 | * - Worker defined inline 41 | * - Wrapper around OBJLoader, so it can be executed as worker 42 | * 43 | * The workers perform the same loading operation over and over again. This is not what you want to do 44 | * in a real-world loading scenario, but it is very helpful to demonstrate that workers executed in 45 | * parallel to main utilizes the CPU. 46 | */ 47 | class WorkerTaskDirectorExample { 48 | 49 | private renderer: WebGLRenderer; 50 | private canvas: HTMLElement; 51 | private scene: Scene = new Scene(); 52 | private camera: PerspectiveCamera; 53 | private cameraTarget: Vector3; 54 | private cameraDefaults: CameraDefaults = { 55 | posCamera: new Vector3(1000.0, 1000.0, 1000.0), 56 | posCameraTarget: new Vector3(0, 0, 0), 57 | near: 0.1, 58 | far: 10000, 59 | fov: 45 60 | }; 61 | private controls: TrackballControls; 62 | private workerTaskDirector: WorkerTaskDirector = new WorkerTaskDirector({ 63 | defaultMaxParallelExecutions: 8, 64 | verbose: false 65 | }); 66 | 67 | private objectsUsed: Map = new Map(); 68 | private tasksToUse: string[] = []; 69 | private materialStore = new MaterialStore(true); 70 | 71 | constructor(elementToBindTo: HTMLElement | null) { 72 | if (elementToBindTo === null) { 73 | throw Error('Bad element HTML given as canvas.'); 74 | } 75 | 76 | this.canvas = elementToBindTo; 77 | this.renderer = new WebGLRenderer({ 78 | canvas: this.canvas, 79 | antialias: true 80 | }); 81 | this.renderer.setClearColor(0x050505); 82 | 83 | this.cameraTarget = this.cameraDefaults.posCameraTarget; 84 | this.camera = new PerspectiveCamera(this.cameraDefaults.fov, this.recalcAspectRatio(), this.cameraDefaults.near, this.cameraDefaults.far); 85 | this.resetCamera(); 86 | 87 | this.controls = new TrackballControls(this.camera, this.renderer.domElement); 88 | 89 | const ambientLight = new AmbientLight(0x404040); 90 | const directionalLight1 = new DirectionalLight(0xC0C090); 91 | const directionalLight2 = new DirectionalLight(0xC0C090); 92 | 93 | directionalLight1.position.set(- 100, - 50, 100); 94 | directionalLight2.position.set(100, 50, - 100); 95 | 96 | this.scene.add(directionalLight1); 97 | this.scene.add(directionalLight2); 98 | this.scene.add(ambientLight); 99 | 100 | const helper = new GridHelper(1000, 30, 0xFF4444, 0x404040); 101 | helper.name = 'grid'; 102 | this.scene.add(helper); 103 | } 104 | 105 | resizeDisplayGL() { 106 | this.controls.handleResize(); 107 | this.renderer.setSize(this.canvas.offsetWidth, this.canvas.offsetHeight, false); 108 | this.updateCamera(); 109 | } 110 | 111 | recalcAspectRatio() { 112 | return (this.canvas.offsetHeight === 0) ? 1 : this.canvas.offsetWidth / this.canvas.offsetHeight; 113 | } 114 | 115 | resetCamera() { 116 | this.camera.position.copy(this.cameraDefaults.posCamera); 117 | this.cameraTarget.copy(this.cameraDefaults.posCameraTarget); 118 | this.updateCamera(); 119 | } 120 | 121 | updateCamera() { 122 | this.camera.aspect = this.recalcAspectRatio(); 123 | this.camera.lookAt(this.cameraTarget); 124 | this.camera.updateProjectionMatrix(); 125 | } 126 | 127 | render() { 128 | if (!this.renderer.autoClear) this.renderer.clear(); 129 | this.controls.update(); 130 | this.renderer.render(this.scene, this.camera); 131 | } 132 | 133 | run() { 134 | this.initTasks(); 135 | } 136 | 137 | /** Registers both workers as tasks at the {@link WorkerTaskDirector} and initializes them. */ 138 | private async initTasks() { 139 | console.time('Init tasks'); 140 | const awaiting: Array> = []; 141 | const helloWorldInitMessage = WorkerMessage.createNew({ 142 | name: 'HelloWorldThreeWorker' 143 | }); 144 | this.workerTaskDirector.registerTask({ 145 | taskName: helloWorldInitMessage.name!, 146 | endpointConfig: { 147 | $type: 'WorkerConfigParams', 148 | workerType: 'module', 149 | blob: false, 150 | url: new URL(import.meta.env.DEV ? '../worker/HelloWorldThreeWorker.ts' : '../worker/generated/HelloWorldThreeWorker-es.js', import.meta.url) 151 | } 152 | }); 153 | this.tasksToUse.push(helloWorldInitMessage.name!); 154 | awaiting.push(this.workerTaskDirector.initTaskType(helloWorldInitMessage.name!, { 155 | message: helloWorldInitMessage 156 | })); 157 | 158 | const objLoaderInitMessage = WorkerMessage.createNew({ 159 | name: 'OBJLoaderdWorker' 160 | }); 161 | this.workerTaskDirector.registerTask({ 162 | taskName: objLoaderInitMessage.name!, 163 | endpointConfig: { 164 | $type: 'WorkerConfigParams', 165 | workerType: 'module', 166 | blob: false, 167 | url: new URL(import.meta.env.DEV ? '../worker/OBJLoaderWorker.ts' : '../worker/generated/OBJLoaderWorker-es.js', import.meta.url) 168 | } 169 | }); 170 | this.tasksToUse.push(objLoaderInitMessage.name!); 171 | 172 | const loadObj = async () => { 173 | const fileLoader = new FileLoader(); 174 | fileLoader.setResponseType('arraybuffer'); 175 | 176 | const objFilename = new URL('../../models/obj/female02/female02_vertex_colors.obj', import.meta.url); 177 | return fileLoader.loadAsync(objFilename as unknown as string); 178 | }; 179 | 180 | awaiting.push(loadObj()); 181 | const result = await Promise.all(awaiting); 182 | console.log('Awaited all required init and data loading.'); 183 | 184 | const objLoaderDataPayload = new DataPayload(); 185 | objLoaderDataPayload.message.buffers?.set('modelData', result[1] as ArrayBufferLike); 186 | 187 | const materialsPayload = new MaterialsPayload(); 188 | materialsPayload.message.materials = this.materialStore.getMaterials(); 189 | materialsPayload.cleanMaterials(); 190 | 191 | objLoaderInitMessage.addPayload(objLoaderDataPayload); 192 | objLoaderInitMessage.addPayload(materialsPayload); 193 | 194 | const transferables = WorkerMessage.pack(objLoaderInitMessage.payloads, false); 195 | await this.workerTaskDirector.initTaskType(objLoaderInitMessage.name!, { 196 | message: objLoaderInitMessage, 197 | transferables, 198 | copyTransferables: true 199 | }); 200 | console.timeEnd('Init tasks'); 201 | await this.executeTasks(); 202 | } 203 | 204 | /** Once all tasks are initialized a 1024 tasks are enqueued for execution by WorkerTaskDirector. */ 205 | private async executeTasks() { 206 | if (this.tasksToUse.length === 0) throw new Error('No Tasks have been selected. Aborting...'); 207 | 208 | console.time('Execute tasks'); 209 | let taskToUseIndex = 0; 210 | const executions = []; 211 | 212 | for (let i = 0; i < 1024; i++) { 213 | const name = this.tasksToUse[taskToUseIndex]; 214 | 215 | const voidPromise = this.workerTaskDirector.enqueueForExecution(name, { 216 | message: WorkerMessage.createEmpty(), 217 | onComplete: (m: WorkerMessage) => { 218 | this.processMessage(m); 219 | }, 220 | onIntermediateConfirm: (m: WorkerMessage) => { 221 | this.processMessage(m); 222 | } 223 | }); 224 | executions.push(voidPromise); 225 | 226 | taskToUseIndex++; 227 | if (taskToUseIndex === this.tasksToUse.length) { 228 | taskToUseIndex = 0; 229 | } 230 | } 231 | await Promise.all(executions); 232 | console.timeEnd('Execute tasks'); 233 | console.log('All worker executions have been completed'); 234 | this.workerTaskDirector.dispose(); 235 | } 236 | 237 | /** 238 | * This method is invoked when {@link WorkerTaskDirector} received a message from a worker. 239 | * @param {object} payload Message received from worker 240 | * @private 241 | */ 242 | private processMessage(message: WorkerMessage) { 243 | const wm = WorkerMessage.unpack(message, false); 244 | switch (wm.cmd) { 245 | case WorkerTaskCommandResponse.INTERMEDIATE_CONFIRM: 246 | case WorkerTaskCommandResponse.EXECUTE_COMPLETE: 247 | if (wm.payloads.length === 1) { 248 | this.buildMesh(wm.uuid, wm.payloads[0] as MeshPayload); 249 | } 250 | else if (wm.payloads.length === 2) { 251 | this.buildMesh(wm.uuid, wm.payloads[0] as MeshPayload, wm.payloads[1] as MaterialsPayload); 252 | } 253 | if (wm.cmd === WorkerTaskCommandResponse.EXECUTE_COMPLETE) { 254 | console.log(`execComplete: name: ${wm.name} uuid: ${wm.uuid} cmd: ${wm.cmd} workerId: ${wm.endpointdId}`); 255 | } 256 | break; 257 | 258 | default: 259 | console.error(`${wm.uuid}: Received unknown command: ${wm.cmd}`); 260 | break; 261 | } 262 | } 263 | 264 | private buildMesh(uuid: string, meshPayload: MeshPayload, materialsPayload?: MaterialsPayload) { 265 | let material; 266 | if (materialsPayload !== undefined) { 267 | material = materialsPayload.processMaterialTransport(this.materialStore.getMaterials(), true); 268 | } else { 269 | const randArray = new Uint8Array(3); 270 | window.crypto.getRandomValues(randArray); 271 | const color = new Color(randArray[0] / 255, randArray[1] / 255, randArray[2] / 255); 272 | material = new MeshPhongMaterial({ color: color }); 273 | } 274 | 275 | if (meshPayload.message.bufferGeometry !== undefined) { 276 | const mesh = new Mesh(meshPayload.message.bufferGeometry as BufferGeometry, material); 277 | this.addMesh(mesh, uuid); 278 | } 279 | } 280 | 281 | /** Add mesh at random position, but keep sub-meshes of an object together, therefore we need */ 282 | private addMesh(mesh: Mesh, uuid: string) { 283 | let pos = this.objectsUsed.get(uuid); 284 | if (!pos) { 285 | // sphere positions 286 | const baseFactor = 750; 287 | pos = new Vector3(baseFactor * Math.random(), baseFactor * Math.random(), baseFactor * Math.random()); 288 | pos.applyAxisAngle(new Vector3(1, 0, 0), 2 * Math.PI * Math.random()); 289 | pos.applyAxisAngle(new Vector3(0, 1, 0), 2 * Math.PI * Math.random()); 290 | pos.applyAxisAngle(new Vector3(0, 0, 1), 2 * Math.PI * Math.random()); 291 | this.objectsUsed.set(uuid, pos); 292 | } 293 | mesh.position.set(pos.x, pos.y, pos.z); 294 | mesh.name = uuid + '' + mesh.name; 295 | this.scene.add(mesh); 296 | } 297 | } 298 | 299 | const app = new WorkerTaskDirectorExample(document.getElementById('example')); 300 | 301 | window.addEventListener('resize', () => app.resizeDisplayGL(), false); 302 | 303 | console.log('Starting initialisation phase...'); 304 | app.resizeDisplayGL(); 305 | 306 | const requestRender = () => { 307 | requestAnimationFrame(requestRender); 308 | app.render(); 309 | }; 310 | requestRender(); 311 | 312 | app.run(); 313 | --------------------------------------------------------------------------------