();
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 |
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 |
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 | [](https://github.com/kaisalmen/wtd/blob/main/LICENSE)
4 | [](https://github.com/kaisalmen/wtd/actions/workflows/actions.yml)
5 | [](https://kaisalmen.github.io/wtd)
6 | [](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 | [](https://github.com/kaisalmen/wtd/blob/main/LICENSE)
4 | [](https://github.com/kaisalmen/wtd/actions/workflows/actions.yml)
5 | [](https://kaisalmen.github.io/wtd)
6 | [](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 | [](https://github.com/kaisalmen/wtd/blob/main/LICENSE)
4 | [](https://github.com/kaisalmen/wtd/actions/workflows/actions.yml)
5 | [](https://kaisalmen.github.io/wtd)
6 | [](https://www.npmjs.com/package/wtd-core)
7 | [](https://www.npmjs.com/package/wtd-three-ext)
8 | [](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 |
--------------------------------------------------------------------------------