diff --git a/e2e/pages/mask/sprite-mask.html b/e2e/pages/mask/sprite-mask.html index 1a970242..b51e38b1 100644 --- a/e2e/pages/mask/sprite-mask.html +++ b/e2e/pages/mask/sprite-mask.html @@ -592,32 +592,66 @@ container15.addChild(triangleMask); videoWrapper3.mask = triangleMask; - // 全ビデオの読み込みと再生開始を待機 + // 全ビデオのcompleteイベントを待機 const allVideos = [video1, video2, video3]; - const waitForVideos = () => { + await Promise.all(allVideos.map(video => { return new Promise((resolve) => { - const checkLoaded = () => { - // loadedかつ再生中(paused=false)を確認 - if (allVideos.every(v => v.loaded && !v.paused)) { - // フレーム描画を待つために複数フレーム待機 - let frameCount = 0; - const waitFrames = () => { - frameCount++; - if (frameCount >= 10) { - setTimeout(resolve, 1000); - } else { - requestAnimationFrame(waitFrames); - } - }; - requestAnimationFrame(waitFrames); - } else { - requestAnimationFrame(checkLoaded); - } + if (video.loaded && !video.paused) { + resolve(); + } else { + video.addEventListener("complete", () => resolve()); + } + }); + })); + + // 固定フレームのためにシークして一時停止 + const SEEK_TIME = 1.0; + await Promise.all(allVideos.map(video => { + return new Promise((resolve) => { + if (!video.$videoElement) { + resolve(); + return; + } + + const onSeeked = () => { + video.$videoElement.removeEventListener("seeked", onSeeked); + video.pause(); + resolve(); }; - checkLoaded(); + + video.$videoElement.addEventListener("seeked", onSeeked); + video.seek(SEEK_TIME); }); + })); + + // changedフラグを更新してレンダリングを強制 + const applyChanges = (displayObject) => { + displayObject.changed = true; + let parent = displayObject.parent; + while (parent && !parent.changed) { + parent.changed = true; + parent = parent.parent; + } }; - await waitForVideos(); + + // レンダリング安定化のため数フレーム待機 + await new Promise((resolve) => { + let frameCount = 0; + const waitFrames = () => { + allVideos.forEach((video) => { + if (video.$videoElement && video.$videoElement.readyState >= 2) { + applyChanges(video); + } + }); + frameCount++; + if (frameCount >= 10) { + resolve(); + } else { + requestAnimationFrame(waitFrames); + } + }; + requestAnimationFrame(waitFrames); + }); window.__E2E_RENDER_COMPLETE__ = true; }); diff --git a/e2e/pages/shape/cache-as-bitmap.html b/e2e/pages/shape/cache-as-bitmap.html new file mode 100644 index 00000000..f91f6446 --- /dev/null +++ b/e2e/pages/shape/cache-as-bitmap.html @@ -0,0 +1,161 @@ + + + + + + Next2D E2E - Shape cacheAsBitmap + + + + + + + diff --git a/e2e/pages/sprite/cache-as-bitmap-hit.html b/e2e/pages/sprite/cache-as-bitmap-hit.html new file mode 100644 index 00000000..737268ae --- /dev/null +++ b/e2e/pages/sprite/cache-as-bitmap-hit.html @@ -0,0 +1,104 @@ + + + + + + Next2D E2E - Sprite cacheAsBitmap cache HIT test + + + + + + + diff --git a/e2e/pages/sprite/cache-as-bitmap-yflip.html b/e2e/pages/sprite/cache-as-bitmap-yflip.html new file mode 100644 index 00000000..f4bf0338 --- /dev/null +++ b/e2e/pages/sprite/cache-as-bitmap-yflip.html @@ -0,0 +1,80 @@ + + + + + + Next2D E2E - Sprite cacheAsBitmap Y-flip test + + + + + + + diff --git a/e2e/pages/sprite/cache-as-bitmap.html b/e2e/pages/sprite/cache-as-bitmap.html new file mode 100644 index 00000000..f4b52be2 --- /dev/null +++ b/e2e/pages/sprite/cache-as-bitmap.html @@ -0,0 +1,92 @@ + + + + + + Next2D E2E - Sprite cacheAsBitmap + + + + + + + diff --git a/e2e/pages/textfield/cache-as-bitmap.html b/e2e/pages/textfield/cache-as-bitmap.html new file mode 100644 index 00000000..02d5002b --- /dev/null +++ b/e2e/pages/textfield/cache-as-bitmap.html @@ -0,0 +1,166 @@ + + + + + + Next2D E2E - TextField cacheAsBitmap + + + + + + + diff --git a/e2e/pages/textfield/html-text.html b/e2e/pages/textfield/html-text.html new file mode 100644 index 00000000..a1be44cd --- /dev/null +++ b/e2e/pages/textfield/html-text.html @@ -0,0 +1,321 @@ + + + + + + Next2D E2E - TextField htmlText + + + + + + + diff --git a/e2e/snapshots/webgl/mask.spec.ts-snapshots/mask-sprite-webgl-darwin.png b/e2e/snapshots/webgl/mask.spec.ts-snapshots/mask-sprite-webgl-darwin.png index 0f6267b8..b6be7e67 100644 Binary files a/e2e/snapshots/webgl/mask.spec.ts-snapshots/mask-sprite-webgl-darwin.png and b/e2e/snapshots/webgl/mask.spec.ts-snapshots/mask-sprite-webgl-darwin.png differ diff --git a/e2e/snapshots/webgl/shape.spec.ts-snapshots/cache-as-bitmap-webgl-darwin.png b/e2e/snapshots/webgl/shape.spec.ts-snapshots/cache-as-bitmap-webgl-darwin.png new file mode 100644 index 00000000..ab46ec4a Binary files /dev/null and b/e2e/snapshots/webgl/shape.spec.ts-snapshots/cache-as-bitmap-webgl-darwin.png differ diff --git a/e2e/snapshots/webgl/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-hit-webgl-darwin.png b/e2e/snapshots/webgl/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-hit-webgl-darwin.png new file mode 100644 index 00000000..2bcc6dc4 Binary files /dev/null and b/e2e/snapshots/webgl/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-hit-webgl-darwin.png differ diff --git a/e2e/snapshots/webgl/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-webgl-darwin.png b/e2e/snapshots/webgl/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-webgl-darwin.png new file mode 100644 index 00000000..93d2edeb Binary files /dev/null and b/e2e/snapshots/webgl/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-webgl-darwin.png differ diff --git a/e2e/snapshots/webgl/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-yflip-webgl-darwin.png b/e2e/snapshots/webgl/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-yflip-webgl-darwin.png new file mode 100644 index 00000000..fc0372b8 Binary files /dev/null and b/e2e/snapshots/webgl/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-yflip-webgl-darwin.png differ diff --git a/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-basic-webgl-darwin.png b/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-basic-webgl-darwin.png index 89d2d6ac..878a0b40 100644 Binary files a/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-basic-webgl-darwin.png and b/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-basic-webgl-darwin.png differ diff --git a/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-cache-as-bitmap-webgl-darwin.png b/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-cache-as-bitmap-webgl-darwin.png new file mode 100644 index 00000000..e9b77f05 Binary files /dev/null and b/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-cache-as-bitmap-webgl-darwin.png differ diff --git a/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-html-text-webgl-darwin.png b/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-html-text-webgl-darwin.png new file mode 100644 index 00000000..59287e4f Binary files /dev/null and b/e2e/snapshots/webgl/textfield.spec.ts-snapshots/textfield-html-text-webgl-darwin.png differ diff --git a/e2e/snapshots/webgpu/filter.spec.ts-snapshots/filter-quality-webgpu-darwin.png b/e2e/snapshots/webgpu/filter.spec.ts-snapshots/filter-quality-webgpu-darwin.png index 8962a4f6..69c64611 100644 Binary files a/e2e/snapshots/webgpu/filter.spec.ts-snapshots/filter-quality-webgpu-darwin.png and b/e2e/snapshots/webgpu/filter.spec.ts-snapshots/filter-quality-webgpu-darwin.png differ diff --git a/e2e/snapshots/webgpu/mask.spec.ts-snapshots/mask-sprite-webgpu-darwin.png b/e2e/snapshots/webgpu/mask.spec.ts-snapshots/mask-sprite-webgpu-darwin.png index 5c854810..5c11d5dc 100644 Binary files a/e2e/snapshots/webgpu/mask.spec.ts-snapshots/mask-sprite-webgpu-darwin.png and b/e2e/snapshots/webgpu/mask.spec.ts-snapshots/mask-sprite-webgpu-darwin.png differ diff --git a/e2e/snapshots/webgpu/shape.spec.ts-snapshots/cache-as-bitmap-webgpu-darwin.png b/e2e/snapshots/webgpu/shape.spec.ts-snapshots/cache-as-bitmap-webgpu-darwin.png new file mode 100644 index 00000000..cd88dacf Binary files /dev/null and b/e2e/snapshots/webgpu/shape.spec.ts-snapshots/cache-as-bitmap-webgpu-darwin.png differ diff --git a/e2e/snapshots/webgpu/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-hit-webgpu-darwin.png b/e2e/snapshots/webgpu/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-hit-webgpu-darwin.png new file mode 100644 index 00000000..7ee67f61 Binary files /dev/null and b/e2e/snapshots/webgpu/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-hit-webgpu-darwin.png differ diff --git a/e2e/snapshots/webgpu/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-webgpu-darwin.png b/e2e/snapshots/webgpu/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-webgpu-darwin.png new file mode 100644 index 00000000..54699bad Binary files /dev/null and b/e2e/snapshots/webgpu/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-webgpu-darwin.png differ diff --git a/e2e/snapshots/webgpu/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-yflip-webgpu-darwin.png b/e2e/snapshots/webgpu/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-yflip-webgpu-darwin.png new file mode 100644 index 00000000..7761da76 Binary files /dev/null and b/e2e/snapshots/webgpu/sprite.spec.ts-snapshots/sprite-cache-as-bitmap-yflip-webgpu-darwin.png differ diff --git a/e2e/snapshots/webgpu/textfield.spec.ts-snapshots/textfield-basic-webgpu-darwin.png b/e2e/snapshots/webgpu/textfield.spec.ts-snapshots/textfield-basic-webgpu-darwin.png index 577bb11f..2ef3d02c 100644 Binary files a/e2e/snapshots/webgpu/textfield.spec.ts-snapshots/textfield-basic-webgpu-darwin.png and b/e2e/snapshots/webgpu/textfield.spec.ts-snapshots/textfield-basic-webgpu-darwin.png differ diff --git a/e2e/snapshots/webgpu/textfield.spec.ts-snapshots/textfield-cache-as-bitmap-webgpu-darwin.png b/e2e/snapshots/webgpu/textfield.spec.ts-snapshots/textfield-cache-as-bitmap-webgpu-darwin.png new file mode 100644 index 00000000..8ea829db Binary files /dev/null and b/e2e/snapshots/webgpu/textfield.spec.ts-snapshots/textfield-cache-as-bitmap-webgpu-darwin.png differ diff --git a/e2e/snapshots/webgpu/textfield.spec.ts-snapshots/textfield-html-text-webgpu-darwin.png b/e2e/snapshots/webgpu/textfield.spec.ts-snapshots/textfield-html-text-webgpu-darwin.png new file mode 100644 index 00000000..59e8d578 Binary files /dev/null and b/e2e/snapshots/webgpu/textfield.spec.ts-snapshots/textfield-html-text-webgpu-darwin.png differ diff --git a/e2e/tests/shape.spec.ts b/e2e/tests/shape.spec.ts index 59f1a75d..ad14977c 100644 --- a/e2e/tests/shape.spec.ts +++ b/e2e/tests/shape.spec.ts @@ -106,4 +106,13 @@ test.describe("Shape描画テスト", () => { await expect(page).toHaveScreenshot("graphics-clone.png"); }); }); + + test.describe("cacheAsBitmap", () => { + test("Matrix指定によるビットマップキャッシュ(等倍・2倍・親スケール・回転・ネスト)", async ({ page }) => { + await page.goto("/e2e/pages/shape/cache-as-bitmap.html"); + await waitForCanvas(page); + + await expect(page).toHaveScreenshot("cache-as-bitmap.png"); + }); + }); }); diff --git a/e2e/tests/sprite.spec.ts b/e2e/tests/sprite.spec.ts index 52637afe..e5e0d926 100644 --- a/e2e/tests/sprite.spec.ts +++ b/e2e/tests/sprite.spec.ts @@ -25,4 +25,25 @@ test.describe("Sprite テスト", () => { maxDiffPixels: 15000 }); }); + + test("Sprite cacheAsBitmap(コンテナビットマップキャッシュ)", async ({ page }) => { + await page.goto("/e2e/pages/sprite/cache-as-bitmap.html"); + await waitForCanvas(page); + + await expect(page).toHaveScreenshot("sprite-cache-as-bitmap.png"); + }); + + test("Sprite cacheAsBitmap cache hit(キャッシュ再利用テスト)", async ({ page }) => { + await page.goto("/e2e/pages/sprite/cache-as-bitmap-hit.html"); + await waitForCanvas(page); + + await expect(page).toHaveScreenshot("sprite-cache-as-bitmap-hit.png"); + }); + + test("Sprite cacheAsBitmap Y-flip(Y軸反転検出テスト)", async ({ page }) => { + await page.goto("/e2e/pages/sprite/cache-as-bitmap-yflip.html"); + await waitForCanvas(page); + + await expect(page).toHaveScreenshot("sprite-cache-as-bitmap-yflip.png"); + }); }); diff --git a/e2e/tests/textfield.spec.ts b/e2e/tests/textfield.spec.ts index 465e3789..1971a9d4 100644 --- a/e2e/tests/textfield.spec.ts +++ b/e2e/tests/textfield.spec.ts @@ -57,4 +57,18 @@ test.describe("TextFieldテスト", () => { await expect(page).toHaveScreenshot("textfield-auto-font-size.png"); }); + + test("htmlText(全タグ: b, i, u, font, span, p, div, br + スタイル属性)", async ({ page }) => { + await page.goto("/e2e/pages/textfield/html-text.html"); + await waitForCanvas(page); + + await expect(page).toHaveScreenshot("textfield-html-text.png"); + }); + + test("cacheAsBitmap - Matrix指定によるビットマップキャッシュ(等倍・2倍・親スケール・回転・ネスト)", async ({ page }) => { + await page.goto("/e2e/pages/textfield/cache-as-bitmap.html"); + await waitForCanvas(page); + + await expect(page).toHaveScreenshot("textfield-cache-as-bitmap.png"); + }); }); diff --git a/package-lock.json b/package-lock.json index 58a56364..8a9d7603 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,44 +1,36 @@ { "name": "@next2d/player", - "version": "3.0.5", + "version": "3.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@next2d/player", - "version": "3.0.5", + "version": "3.1.0", "license": "MIT", "workspaces": [ "packages/*" ], - "dependencies": { - "fflate": "^0.8.2", - "htmlparser2": "^10.1.0" - }, "devDependencies": { - "@eslint/eslintrc": "^3.3.4", + "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", - "@playwright/test": "^1.58.2", - "@rollup/plugin-commonjs": "^29.0.0", + "@playwright/test": "^1.59.0", + "@rollup/plugin-commonjs": "^29.0.2", "@rollup/plugin-node-resolve": "^16.0.3", - "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.3.0", - "@types/node": "^25.3.3", - "@typescript-eslint/eslint-plugin": "^8.56.1", - "@typescript-eslint/parser": "^8.56.1", - "@vitest/web-worker": "^4.0.18", + "@typescript-eslint/eslint-plugin": "^8.58.0", + "@typescript-eslint/parser": "^8.58.0", "@webgpu/types": "^0.1.69", - "eslint": "^10.0.2", + "eslint": "^10.1.0", "eslint-plugin-unused-imports": "^4.4.1", - "globals": "^17.3.0", - "jsdom": "^28.1.0", - "rollup": "^4.59.0", - "tslib": "^2.8.1", - "typescript": "^5.9.3", - "vite": "^7.3.1", - "vitest": "^4.0.18", - "vitest-webgl-canvas-mock": "^1.1.0", - "xml2js": "^0.6.2" + "globals": "^17.4.0", + "jsdom": "^29.0.1", + "rollup": "^4.60.1", + "typescript": "^6.0.2", + "vite": "^8.0.3", + "vitest": "^4.1.2", + "vitest-webgl-canvas-mock": "^1.1.0" }, "funding": { "type": "github", @@ -62,17 +54,10 @@ "@next2d/webgpu": "file:packages/webgpu" } }, - "node_modules/@acemir/cssom": { - "version": "0.9.31", - "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.31.tgz", - "integrity": "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==", - "dev": true, - "license": "MIT" - }, "node_modules/@asamuzakjp/css-color": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", - "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.1.tgz", + "integrity": "sha512-iGWN8E45Ws0XWx3D44Q1t6vX2LqhCKcwfmwBYCDsFrYFS6m4q/Ks61L2veETaLv+ckDC6+dTETJoaAAb7VjLiw==", "dev": true, "license": "MIT", "dependencies": { @@ -80,24 +65,27 @@ "@csstools/css-color-parser": "^4.0.2", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.2.6" + "lru-cache": "^11.2.7" }, "engines": { "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", - "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.4.tgz", + "integrity": "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==", "dev": true, "license": "MIT", "dependencies": { "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", - "css-tree": "^3.1.0", + "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.2.6" + "lru-cache": "^11.2.7" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/nwsapi": { @@ -216,9 +204,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.28.tgz", - "integrity": "sha512-1NRf1CUBjnr3K7hu8BLxjQrKCxEe8FP/xmPTenAxCRZWVLbmGotkFvG9mfNpjA6k7Bw1bw4BilZq9cu19RA5pg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.2.tgz", + "integrity": "sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA==", "dev": true, "funding": [ { @@ -230,7 +218,15 @@ "url": "https://opencollective.com/csstools" } ], - "license": "MIT-0" + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } }, "node_modules/@csstools/css-tokenizer": { "version": "4.0.0", @@ -252,446 +248,41 @@ "node": ">=20.19.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "peer": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@eslint-community/eslint-utils": { @@ -724,15 +315,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.2.tgz", - "integrity": "sha512-YF+fE6LV4v5MGWRGj7G404/OZzGNepVF8fxk7jqmqo3lrza7a0uUcDnROGRBG1WFC1omYUS/Wp1f42i0M+3Q3A==", + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.2", + "@eslint/object-schema": "^3.0.3", "debug": "^4.3.1", - "minimatch": "^10.2.1" + "minimatch": "^10.2.4" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" @@ -749,9 +340,9 @@ } }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -762,13 +353,13 @@ } }, "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -778,22 +369,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", - "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.0" + "@eslint/core": "^1.1.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz", - "integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -804,9 +395,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.4.tgz", - "integrity": "sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { @@ -817,7 +408,7 @@ "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.3", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -862,9 +453,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.2.tgz", - "integrity": "sha512-HOy56KJt48Bx8KmJ+XGQNSUMT/6dZee/M54XyUyuvTvPXJmsERRvBchsUVx1UMe1WwIH49XLAczNC7V2INsuUw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -872,13 +463,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.0.tgz", - "integrity": "sha512-bIZEUzOI1jkhviX2cp5vNyXQc6olzb2ohewQubuYlMXZ2Q/XjBO0x0XhGPvc9fjSIiUN0vw+0hq53BJ4eQSJKQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.0", + "@eslint/core": "^1.1.1", "levn": "^0.4.1" }, "engines": { @@ -886,9 +477,9 @@ } }, "node_modules/@exodus/bytes": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.14.1.tgz", - "integrity": "sha512-OhkBFWI6GcRMUroChZiopRiSp2iAMvEBK47NhJooDqz1RERO4QuZIZnjP63TXX8GAiLABkYmX+fuQsdJ1dd2QQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", "dev": true, "license": "MIT", "engines": { @@ -1005,6 +596,25 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@next2d/cache": { "resolved": "packages/cache", "link": true @@ -1065,14 +675,24 @@ "resolved": "packages/webgpu", "link": true }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@playwright/test": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", - "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "version": "1.59.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.0.tgz", + "integrity": "sha512-TOA5sTLd49rTDaZpYpvCQ9hGefHQq/OYOyCVnGqS2mjMfX+lGZv2iddIJd0I48cfxqSPttS9S3OuLKyylHcO1w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.58.2" + "playwright": "1.59.0" }, "bin": { "playwright": "cli.js" @@ -1081,10 +701,290 @@ "node": ">=18" } }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/plugin-commonjs": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.0.tgz", - "integrity": "sha512-U2YHaxR2cU/yAiwKJtJRhnyLk7cifnQw0zUpISsocBDoHDJn+HTV74ABqnwr5bEgWUwFZC9oFL6wLe21lHu5eQ==", + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-29.0.2.tgz", + "integrity": "sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==", "dev": true, "license": "MIT", "dependencies": { @@ -1134,18 +1034,18 @@ } }, "node_modules/@rollup/plugin-terser": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", - "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", + "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", "dev": true, "license": "MIT", "dependencies": { - "serialize-javascript": "^6.0.1", + "serialize-javascript": "^7.0.3", "smob": "^1.0.0", "terser": "^5.17.4" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" }, "peerDependencies": { "rollup": "^2.0.0||^3.0.0||^4.0.0" @@ -1207,9 +1107,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", "cpu": [ "arm" ], @@ -1221,9 +1121,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", "cpu": [ "arm64" ], @@ -1235,9 +1135,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", "cpu": [ "arm64" ], @@ -1249,9 +1149,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", "cpu": [ "x64" ], @@ -1263,9 +1163,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", "cpu": [ "arm64" ], @@ -1277,9 +1177,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", "cpu": [ "x64" ], @@ -1291,13 +1191,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1305,13 +1208,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1319,13 +1225,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1333,13 +1242,16 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1347,13 +1259,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1361,13 +1276,16 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", "cpu": [ "loong64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1375,13 +1293,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1389,13 +1310,16 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1403,13 +1327,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1417,13 +1344,16 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", "cpu": [ "riscv64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1431,13 +1361,16 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1445,13 +1378,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1459,13 +1395,16 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1473,9 +1412,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", "cpu": [ "x64" ], @@ -1487,9 +1426,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", "cpu": [ "arm64" ], @@ -1501,9 +1440,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", "cpu": [ "arm64" ], @@ -1515,9 +1454,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", "cpu": [ "ia32" ], @@ -1529,9 +1468,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", "cpu": [ "x64" ], @@ -1543,9 +1482,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", "cpu": [ "x64" ], @@ -1563,6 +1502,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -1598,19 +1548,9 @@ "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.3.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", - "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.18.0" - } + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" }, "node_modules/@types/resolve": { "version": "1.20.2", @@ -1620,20 +1560,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", - "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz", + "integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/type-utils": "8.56.1", - "@typescript-eslint/utils": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/type-utils": "8.58.0", + "@typescript-eslint/utils": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1643,9 +1583,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.56.1", + "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -1659,16 +1599,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", - "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz", + "integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "engines": { @@ -1680,18 +1620,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", - "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz", + "integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.1", - "@typescript-eslint/types": "^8.56.1", + "@typescript-eslint/tsconfig-utils": "^8.58.0", + "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "engines": { @@ -1702,18 +1642,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", - "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz", + "integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1" + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1724,9 +1664,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", - "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz", + "integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==", "dev": true, "license": "MIT", "engines": { @@ -1737,21 +1677,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", - "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz", + "integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0", + "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1762,13 +1702,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", - "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz", + "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==", "dev": true, "license": "MIT", "engines": { @@ -1780,21 +1720,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", - "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz", + "integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.56.1", - "@typescript-eslint/tsconfig-utils": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/project-service": "8.58.0", + "@typescript-eslint/tsconfig-utils": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1804,7 +1744,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { @@ -1818,9 +1758,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1831,13 +1771,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -1847,16 +1787,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", - "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz", + "integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1" + "@typescript-eslint/scope-manager": "8.58.0", + "@typescript-eslint/types": "8.58.0", + "@typescript-eslint/typescript-estree": "8.58.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1867,17 +1807,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", - "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "version": "8.58.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz", + "integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1902,31 +1842,31 @@ } }, "node_modules/@vitest/expect": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", - "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.0.0", + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "chai": "^6.2.1", - "tinyrainbow": "^3.0.3" + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", - "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.0.18", + "@vitest/spy": "4.1.2", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1935,7 +1875,7 @@ }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -1957,26 +1897,26 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", - "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.0.3" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", - "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.0.18", + "@vitest/utils": "4.1.2", "pathe": "^2.0.3" }, "funding": { @@ -1984,13 +1924,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", - "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.0.18", + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1999,9 +1940,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", - "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", "dev": true, "license": "MIT", "funding": { @@ -2009,33 +1950,18 @@ } }, "node_modules/@vitest/utils": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", - "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.0.18", - "tinyrainbow": "^3.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/web-worker": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/@vitest/web-worker/-/web-worker-4.0.18.tgz", - "integrity": "sha512-h9MiAI3nQNVeEH8Tn1p9CwJGmXPJPUTGhzcuQalIk+6fqIazqUDVzDi+NUKrpK6sQKgSa4MonhyDThhtZqH+cA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", "dev": true, "license": "MIT", "dependencies": { - "obug": "^2.1.1" + "@vitest/pretty-format": "4.1.2", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "4.0.18" } }, "node_modules/@webgpu/types": { @@ -2068,16 +1994,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -2130,9 +2046,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, "license": "MIT", "dependencies": { @@ -2194,6 +2110,13 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2210,14 +2133,14 @@ } }, "node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, "license": "MIT", "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" @@ -2230,22 +2153,6 @@ "dev": true, "license": "MIT" }, - "node_modules/cssstyle": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.1.0.tgz", - "integrity": "sha512-Ml4fP2UT2K3CUBQnVlbdV/8aFDdlY69E+YnwJM+3VUWl08S3J8c8aRuJqCkD9Py8DHZ7zNNvsfKl8psocHZEFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^5.0.0", - "@csstools/css-syntax-patches-for-csstree": "^1.0.28", - "css-tree": "^3.1.0", - "lru-cache": "^11.2.6" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/data-urls": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", @@ -2302,77 +2209,21 @@ "node": ">=0.10.0" } }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" + "node": ">=8" } }, "node_modules/entities": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", - "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -2382,54 +2233,12 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2444,18 +2253,18 @@ } }, "node_modules/eslint": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.2.tgz", - "integrity": "sha512-uYixubwmqJZH+KLVYIVKY1JQt7tysXhtj21WSvjcSmU5SVNzMus1bgLe+pAt816yQ8opKfheVVoPLqvVMGejYw==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", + "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.2", - "@eslint/config-helpers": "^0.5.2", - "@eslint/core": "^1.1.0", - "@eslint/plugin-kit": "^0.6.0", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.3", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -2464,9 +2273,9 @@ "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.1", + "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", - "espree": "^11.1.1", + "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -2477,7 +2286,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.1", + "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -2516,9 +2325,9 @@ } }, "node_modules/eslint-scope": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.1.tgz", - "integrity": "sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2558,9 +2367,9 @@ } }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2584,9 +2393,9 @@ } }, "node_modules/eslint/node_modules/espree": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", - "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -2602,13 +2411,13 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -2750,12 +2559,6 @@ } } }, - "node_modules/fflate": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", - "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", - "license": "MIT" - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2801,9 +2604,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -2846,9 +2649,9 @@ } }, "node_modules/globals": { - "version": "17.3.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz", - "integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", "dev": true, "license": "MIT", "engines": { @@ -2884,53 +2687,6 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/htmlparser2": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", - "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "entities": "^7.0.1" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3031,110 +2787,383 @@ "@types/estree": "*" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz", + "integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@asamuzakjp/dom-selector": "^7.0.3", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.24.5", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], "dev": true, - "license": "ISC" + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jsdom": { - "version": "28.1.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", - "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@acemir/cssom": "^0.9.31", - "@asamuzakjp/dom-selector": "^6.8.1", - "@bramus/specificity": "^2.4.2", - "@exodus/bytes": "^1.11.0", - "cssstyle": "^6.0.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.6", - "is-potential-custom-element-name": "^1.0.1", - "parse5": "^8.0.0", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.0", - "undici": "^7.21.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0", - "xml-name-validator": "^5.0.0" - }, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": ">= 12.0.0" }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">= 0.8.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/locate-path": { @@ -3154,9 +3183,9 @@ } }, "node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -3174,9 +3203,9 @@ } }, "node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true, "license": "CC0-1.0" }, @@ -3323,19 +3352,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3378,9 +3394,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -3391,13 +3407,13 @@ } }, "node_modules/playwright": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", - "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "version": "1.59.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.0.tgz", + "integrity": "sha512-wihGScriusvATUxmhfENxg0tj1vHEFeIwxlnPFKQTOQVd7aG08mUfvvniRP/PtQOC+2Bs52kBOC/Up1jTXeIbw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.58.2" + "playwright-core": "1.59.0" }, "bin": { "playwright": "cli.js" @@ -3410,9 +3426,9 @@ } }, "node_modules/playwright-core": { - "version": "1.58.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", - "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "version": "1.59.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.0.tgz", + "integrity": "sha512-PW/X/IoZ6BMUUy8rpwHEZ8Kc0IiLIkgKYGNFaMs5KmQhcfLILNx9yCQD0rnWeWfz1PNeqcFP1BsihQhDOBCwZw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3423,9 +3439,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { @@ -3471,16 +3487,6 @@ "node": ">=6" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -3522,10 +3528,44 @@ "node": ">=4" } }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "dev": true, "license": "MIT", "dependencies": { @@ -3539,65 +3579,34 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/sax": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", - "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=11.0.0" - } - }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -3625,13 +3634,13 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", + "integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==", "dev": true, "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=20.0.0" } }, "node_modules/shebang-command": { @@ -3713,9 +3722,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", "dev": true, "license": "MIT" }, @@ -3753,9 +3762,9 @@ "license": "MIT" }, "node_modules/terser": { - "version": "5.46.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", - "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { @@ -3779,9 +3788,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", "dev": true, "license": "MIT", "engines": { @@ -3806,9 +3815,9 @@ } }, "node_modules/tinyrainbow": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", - "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -3816,29 +3825,29 @@ } }, "node_modules/tldts": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.23.tgz", - "integrity": "sha512-ASdhgQIBSay0R/eXggAkQ53G4nTJqTXqC2kbaBbdDwM7SkjyZyO0OaaN1/FH7U/yCeqOHDwFO5j8+Os/IS1dXw==", + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", + "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.0.23" + "tldts-core": "^7.0.27" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.23", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.23.tgz", - "integrity": "sha512-0g9vrtDQLrNIiCj22HSe9d4mLVG3g5ph5DZ8zCKBr4OtrspmNB6ss7hVyzArAeE88ceZocIEGkyW1Ime7fxPtQ==", + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", + "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", "dev": true, "license": "MIT" }, "node_modules/tough-cookie": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", - "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3862,9 +3871,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -3879,7 +3888,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "optional": true }, "node_modules/type-check": { "version": "0.4.0", @@ -3895,9 +3905,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -3909,22 +3919,15 @@ } }, "node_modules/undici": { - "version": "7.22.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.22.0.tgz", - "integrity": "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz", + "integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==", "dev": true, "license": "MIT", "engines": { "node": ">=20.18.1" } }, - "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, - "license": "MIT" - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3936,17 +3939,16 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "bin": { @@ -3963,9 +3965,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -3978,13 +3981,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -4026,31 +4032,31 @@ } }, "node_modules/vitest": { - "version": "4.0.18", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", - "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.0.18", - "@vitest/mocker": "4.0.18", - "@vitest/pretty-format": "4.0.18", - "@vitest/runner": "4.0.18", - "@vitest/snapshot": "4.0.18", - "@vitest/spy": "4.0.18", - "@vitest/utils": "4.0.18", - "es-module-lexer": "^1.7.0", - "expect-type": "^1.2.2", + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", - "std-env": "^3.10.0", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.0.3", - "vite": "^6.0.0 || ^7.0.0", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { @@ -4066,12 +4072,13 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.0.18", - "@vitest/browser-preview": "4.0.18", - "@vitest/browser-webdriverio": "4.0.18", - "@vitest/ui": "4.0.18", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { @@ -4100,6 +4107,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, @@ -4215,30 +4225,6 @@ "node": ">=18" } }, - "node_modules/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "dev": true, - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", @@ -4348,9 +4334,6 @@ "name": "@next2d/text", "version": "*", "license": "MIT", - "dependencies": { - "htmlparser2": "^10.0.0" - }, "peerDependencies": { "@next2d/cache": "file:../cache", "@next2d/display": "file:../display", diff --git a/package.json b/package.json index 25e85e3f..b47b277d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@next2d/player", - "version": "3.0.5", + "version": "3.1.0", "description": "Experience the fast and beautiful anti-aliased rendering of WebGL. You can create rich, interactive graphics, cross-platform applications and games without worrying about browser or device compatibility.", "author": "Toshiyuki Ienaga (https://github.com/ienaga/)", "license": "MIT", @@ -47,34 +47,26 @@ "type": "github", "url": "https://github.com/sponsors/Next2D" }, - "dependencies": { - "fflate": "^0.8.2", - "htmlparser2": "^10.1.0" - }, "devDependencies": { - "@eslint/eslintrc": "^3.3.4", + "@eslint/eslintrc": "^3.3.5", "@eslint/js": "^10.0.1", - "@playwright/test": "^1.58.2", - "@rollup/plugin-commonjs": "^29.0.0", + "@playwright/test": "^1.59.0", + "@rollup/plugin-commonjs": "^29.0.2", "@rollup/plugin-node-resolve": "^16.0.3", - "@rollup/plugin-terser": "^0.4.4", + "@rollup/plugin-terser": "^1.0.0", "@rollup/plugin-typescript": "^12.3.0", - "@types/node": "^25.3.3", - "@typescript-eslint/eslint-plugin": "^8.56.1", - "@typescript-eslint/parser": "^8.56.1", - "@vitest/web-worker": "^4.0.18", + "@typescript-eslint/eslint-plugin": "^8.58.0", + "@typescript-eslint/parser": "^8.58.0", "@webgpu/types": "^0.1.69", - "eslint": "^10.0.2", + "eslint": "^10.1.0", "eslint-plugin-unused-imports": "^4.4.1", - "globals": "^17.3.0", - "jsdom": "^28.1.0", - "rollup": "^4.59.0", - "tslib": "^2.8.1", - "typescript": "^5.9.3", - "vite": "^7.3.1", - "vitest": "^4.0.18", - "vitest-webgl-canvas-mock": "^1.1.0", - "xml2js": "^0.6.2" + "globals": "^17.4.0", + "jsdom": "^29.0.1", + "rollup": "^4.60.1", + "typescript": "^6.0.2", + "vite": "^8.0.3", + "vitest": "^4.1.2", + "vitest-webgl-canvas-mock": "^1.1.0" }, "peerDependencies": { "@next2d/cache": "file:packages/cache", diff --git a/packages/display/src/DisplayObject.test.ts b/packages/display/src/DisplayObject.test.ts new file mode 100644 index 00000000..3263e429 --- /dev/null +++ b/packages/display/src/DisplayObject.test.ts @@ -0,0 +1,94 @@ +import { DisplayObject } from "./DisplayObject"; +import { Matrix } from "@next2d/geom"; +import { describe, expect, it } from "vitest"; + +describe("DisplayObject cacheAsBitmap test", () => +{ + it("default value is null", () => + { + const displayObject = new DisplayObject(); + expect(displayObject.cacheAsBitmap).toBe(null); + }); + + it("setting Matrix marks changed", () => + { + const displayObject = new DisplayObject(); + displayObject.changed = false; + + expect(displayObject.changed).toBe(false); + + const matrix = new Matrix(2, 0, 0, 2, 0, 0); + displayObject.cacheAsBitmap = matrix; + + expect(displayObject.cacheAsBitmap).toBe(matrix); + expect(displayObject.changed).toBe(true); + }); + + it("setting null when already null does not mark changed", () => + { + const displayObject = new DisplayObject(); + displayObject.changed = false; + + expect(displayObject.changed).toBe(false); + expect(displayObject.cacheAsBitmap).toBe(null); + + displayObject.cacheAsBitmap = null; + + expect(displayObject.changed).toBe(false); + }); + + it("setting to null after Matrix marks changed", () => + { + const displayObject = new DisplayObject(); + const matrix = new Matrix(2, 0, 0, 2, 0, 0); + displayObject.cacheAsBitmap = matrix; + displayObject.changed = false; + + expect(displayObject.changed).toBe(false); + + displayObject.cacheAsBitmap = null; + + expect(displayObject.cacheAsBitmap).toBe(null); + expect(displayObject.changed).toBe(true); + }); + + it("setting same Matrix instance does not mark changed", () => + { + const displayObject = new DisplayObject(); + const matrix = new Matrix(2, 0, 0, 2, 0, 0); + displayObject.cacheAsBitmap = matrix; + displayObject.changed = false; + + expect(displayObject.changed).toBe(false); + + displayObject.cacheAsBitmap = matrix; + + expect(displayObject.changed).toBe(false); + }); + + it("propagates changed to parent", () => + { + const displayObject = new DisplayObject(); + const parent = new DisplayObject(); + displayObject.parent = parent; + + parent.changed = false; + displayObject.changed = false; + + displayObject.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + + expect(displayObject.changed).toBe(true); + expect(parent.changed).toBe(true); + }); + + it("ignores invalid values (non-Matrix, non-null)", () => + { + const displayObject = new DisplayObject(); + displayObject.changed = false; + + displayObject.cacheAsBitmap = "invalid" as any; + + expect(displayObject.cacheAsBitmap).toBe(null); + expect(displayObject.changed).toBe(false); + }); +}); diff --git a/packages/display/src/DisplayObject.ts b/packages/display/src/DisplayObject.ts index 12a9aea2..a76d230d 100644 --- a/packages/display/src/DisplayObject.ts +++ b/packages/display/src/DisplayObject.ts @@ -8,10 +8,10 @@ import type { MovieClip } from "./MovieClip"; import type { ISprite } from "./interface/ISprite"; import type { ColorTransform, - Matrix, Rectangle, Point } from "@next2d/geom"; +import { Matrix } from "@next2d/geom"; import { EventDispatcher } from "@next2d/events"; import { execute as displayObjectApplyChangesService } from "./DisplayObject/service/DisplayObjectApplyChangesService"; import { execute as displayObjectConcatenatedMatrixUseCase } from "./DisplayObject/usecase/DisplayObjectConcatenatedMatrixUseCase"; @@ -397,6 +397,21 @@ export class DisplayObject extends EventDispatcher */ private _$visible: boolean; + /** + * @description ビットマップキャッシュ用のMatrix。nullでない場合、指定Matrix × stageのrendererScaleで + * Shape/TextFieldのベクター描画をキャッシュし、ステージのリサイズがあるまで再利用します。 + * 先祖のMatrixの影響を受けず、キャッシュの品質は指定Matrixとstageスケールのみで決定されます。 + * Matrix for bitmap caching. When not null, caches Shape/TextField vector rendering + * at the specified Matrix × stage rendererScale, reusing until stage resize. + * Cache quality is determined only by the specified Matrix and stage scale, + * independent of ancestor transforms. + * + * @type {Matrix | null} + * @default null + * @private + */ + private _$cacheAsBitmap: Matrix | null; + /** * @description 表示オブジェクト単位の変数を保持するマップ * Map that holds variables for display objects @@ -428,6 +443,16 @@ export class DisplayObject extends EventDispatcher */ public parent: ISprite | null; + /** + * @description キャッシュする際の指定Matrix、nullの場合は通常のMatrixで描画 + * Specified Matrix for caching, if null, draw with the normal Matrix + * + * @member {Matrix | null} + * @default null + * @public + */ + public cacheTransform: Matrix | null = null; + /** * @constructor * @public @@ -474,6 +499,7 @@ export class DisplayObject extends EventDispatcher this.$blendMode = null; this._$visible = true; + this._$cacheAsBitmap = null; this._$scale9Grid = null; this._$variables = null; @@ -726,9 +752,15 @@ export class DisplayObject extends EventDispatcher */ get scaleX (): number { - return this.$scaleX === null + const base = this.$scaleX === null ? displayObjectGetScaleXUseCase(this) : this.$scaleX; + + if (this._$cacheAsBitmap) { + const m = this._$cacheAsBitmap.rawData; + return base * Math.sqrt(m[0] * m[0] + m[1] * m[1]); + } + return base; } set scaleX (scale_x: number) { @@ -745,9 +777,15 @@ export class DisplayObject extends EventDispatcher */ get scaleY (): number { - return this.$scaleY === null + const base = this.$scaleY === null ? displayObjectGetScaleYUseCase(this) : this.$scaleY; + + if (this._$cacheAsBitmap) { + const m = this._$cacheAsBitmap.rawData; + return base * Math.sqrt(m[2] * m[2] + m[3] * m[3]); + } + return base; } set scaleY (scale_y: number) { @@ -775,6 +813,35 @@ export class DisplayObject extends EventDispatcher displayObjectApplyChangesService(this); } + /** + * @description ビットマップキャッシュ用のMatrix。nullでない場合、指定Matrix × stageのrendererScaleで + * Shape/TextFieldのベクター描画をキャッシュし、ステージのリサイズがあるまで再利用します。 + * 先祖のMatrixの影響を受けず、キャッシュの品質は指定Matrixとstageスケールのみで決定されます。 + * Matrix for bitmap caching. When not null, caches Shape/TextField vector rendering + * at the specified Matrix × stage rendererScale, reusing until stage resize. + * Cache quality is determined only by the specified Matrix and stage scale, + * independent of ancestor transforms. + * + * @member {Matrix | null} + * @default null + * @public + */ + get cacheAsBitmap (): Matrix | null + { + return this._$cacheAsBitmap; + } + set cacheAsBitmap (cache_as_bitmap: Matrix | null) + { + if (cache_as_bitmap !== null && !(cache_as_bitmap instanceof Matrix)) { + return ; + } + if (this._$cacheAsBitmap === cache_as_bitmap) { + return ; + } + this._$cacheAsBitmap = cache_as_bitmap; + displayObjectApplyChangesService(this); + } + /** * @description 表示オブジェクトの幅を示します(ピクセル単位)。 * Indicates the width of the display object, in pixels. diff --git a/packages/display/src/DisplayObject/usecase/DisplayObjectHitTestPointUseCase.ts b/packages/display/src/DisplayObject/usecase/DisplayObjectHitTestPointUseCase.ts index d03c534f..23dbf390 100644 --- a/packages/display/src/DisplayObject/usecase/DisplayObjectHitTestPointUseCase.ts +++ b/packages/display/src/DisplayObject/usecase/DisplayObjectHitTestPointUseCase.ts @@ -18,7 +18,9 @@ import { import { $MATRIX_ARRAY_IDENTITY, $poolBoundsArray, - $colorContext + $colorContext, + $getFloat32Array6, + $poolFloat32Array6 } from "../../DisplayObjectUtil"; /** @@ -108,14 +110,34 @@ export const execute = ( const rawBounds = displayObjectGetRawBoundsUseCase(display_object); const martix = displayObjectGetRawMatrixUseCase(display_object); + + // cacheAsBitmap倍率をhitTest用のmatrixに適用 + const cacheMatrix = display_object.cacheAsBitmap; + let hitMatrix = martix ? martix : $MATRIX_ARRAY_IDENTITY; + let scaledMatrix: Float32Array | null = null; + if (cacheMatrix && hitMatrix) { + const m = cacheMatrix.rawData; + const csx = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + const csy = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + scaledMatrix = $getFloat32Array6( + hitMatrix[0] * csx, hitMatrix[1] * csx, + hitMatrix[2] * csy, hitMatrix[3] * csy, + hitMatrix[4], hitMatrix[5] + ); + hitMatrix = scaledMatrix; + } + const bounds = displayObjectCalcBoundsMatrixService( rawBounds[0], rawBounds[1], rawBounds[2], rawBounds[3], - martix ? martix : $MATRIX_ARRAY_IDENTITY + hitMatrix ); // pool $poolBoundsArray(rawBounds); + if (scaledMatrix) { + $poolFloat32Array6(scaledMatrix); + } const rectangle = new Rectangle( bounds[0], bounds[1], diff --git a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerCalcBoundsMatrixUseCase.ts b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerCalcBoundsMatrixUseCase.ts index b645f749..ce0f0982 100644 --- a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerCalcBoundsMatrixUseCase.ts +++ b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerCalcBoundsMatrixUseCase.ts @@ -10,7 +10,9 @@ import { execute as videoCalcBoundsMatrixUseCase } from "../../Video/usecase/Vid import { execute as textFieldCalcBoundsMatrixUseCase } from "../../TextField/usecase/TextFieldCalcBoundsMatrixUseCase"; import { $getBoundsArray, - $poolBoundsArray + $poolBoundsArray, + $getFloat32Array6, + $poolFloat32Array6 } from "../../DisplayObjectUtil"; /** @@ -32,7 +34,27 @@ export const execute = ( return $getBoundsArray(0, 0, 0, 0); } - const rawMatrix = displayObjectGetRawMatrixUseCase(display_object_container); + let rawMatrix = displayObjectGetRawMatrixUseCase(display_object_container); + + // cacheAsBitmap倍率をrawMatrixに適用(ShapeCalcBoundsMatrixUseCaseと同様) + const cacheMatrix = display_object_container.cacheAsBitmap; + let scaledMatrix: Float32Array | null = null; + if (cacheMatrix) { + const m = cacheMatrix.rawData; + const csx = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + const csy = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + if (rawMatrix) { + scaledMatrix = $getFloat32Array6( + rawMatrix[0] * csx, rawMatrix[1] * csx, + rawMatrix[2] * csy, rawMatrix[3] * csy, + rawMatrix[4], rawMatrix[5] + ); + } else { + scaledMatrix = $getFloat32Array6(csx, 0, 0, csy, 0, 0); + } + rawMatrix = scaledMatrix; + } + const tMatrix = rawMatrix ? matrix ? Matrix.multiply(matrix, rawMatrix) @@ -84,5 +106,9 @@ export const execute = ( $poolBoundsArray(bounds); } + if (scaledMatrix) { + $poolFloat32Array6(scaledMatrix); + } + return $getBoundsArray(xMin, yMin, xMax, yMax); }; \ No newline at end of file diff --git a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.test.ts b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.test.ts index cdaa8ed7..ff0808f2 100644 --- a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.test.ts +++ b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.test.ts @@ -1,8 +1,12 @@ import { execute } from "./DisplayObjectContainerGenerateRenderQueueUseCase"; import { describe, expect, it } from "vitest"; import { renderQueue } from "@next2d/render-queue"; +import { $cacheStore } from "@next2d/cache"; import { MovieClip } from "../../MovieClip"; +import { Shape } from "../../Shape"; import { $RENDERER_CONTAINER_TYPE } from "../../DisplayObjectUtil"; +import { Matrix } from "@next2d/geom"; +import { stage } from "../../Stage"; describe("DisplayObjectContainerGenerateRenderQueueUseCase.js test", () => { @@ -38,4 +42,307 @@ describe("DisplayObjectContainerGenerateRenderQueueUseCase.js test", () => renderQueue.buffer.fill(0); renderQueue.offset = 0; }); + + it("cacheAsBitmap null の場合は通常パスを使用", () => + { + const movieClip = new MovieClip(); + movieClip.addChild(new MovieClip()); + movieClip.cacheAsBitmap = null; + + const matrix = new Float32Array([1, 0, 0, 1, 0, 0]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + renderQueue.offset = 0; + renderQueue.buffer.fill(0); + + execute(movieClip, [], matrix, colorTransform, 0, 0); + + // 通常パス: visible=1, CONTAINER_TYPE, blendMode(11=normal), useLayer=0 + expect(renderQueue.buffer[0]).toBe(1); + expect(renderQueue.buffer[1]).toBe($RENDERER_CONTAINER_TYPE); + expect(renderQueue.buffer[2]).toBe(11); + expect(renderQueue.buffer[3]).toBe(0); // useLayer=0(no filter, normal blend) + + renderQueue.buffer.fill(0); + renderQueue.offset = 0; + }); + + it("cacheAsBitmap 設定時に cache miss で専用形式を生成", () => + { + const movieClip = new MovieClip(); + const childShape = new Shape(); + movieClip.addChild(childShape); + + movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + stage.rendererScale = 1; + + const matrix = new Float32Array([1, 0, 0, 1, 0, 0]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + // キャッシュをクリア + $cacheStore.removeById(`${movieClip.instanceId}`); + + renderQueue.offset = 0; + renderQueue.buffer.fill(0); + + execute(movieClip, [], matrix, colorTransform, 800, 600); + + // visible=1, CONTAINER_TYPE + expect(renderQueue.buffer[0]).toBe(1); + expect(renderQueue.buffer[1]).toBe($RENDERER_CONTAINER_TYPE); + // blendMode (normal=11) + expect(renderQueue.buffer[2]).toBe(11); + // useLayer=1 + expect(renderQueue.buffer[3]).toBe(1); + + // layerType=2 (cacheAsBitmap), cacheHit=0 + expect(renderQueue.buffer[6]).toBe(2); + expect(renderQueue.buffer[7]).toBe(0); + + // instanceId + expect(renderQueue.buffer[8]).toBe(movieClip.instanceId); + + // filterBounds (フィルターなし: すべて0) + expect(renderQueue.buffer[10]).toBe(0); + expect(renderQueue.buffer[11]).toBe(0); + expect(renderQueue.buffer[12]).toBe(0); + expect(renderQueue.buffer[13]).toBe(0); + + // renderScaleX, renderScaleY + expect(renderQueue.buffer[14]).toBe(1); // renderScaleX + expect(renderQueue.buffer[15]).toBe(1); // renderScaleY + // parent matrix a, b, c, d + expect(renderQueue.buffer[16]).toBe(1); // matrix[0] + expect(renderQueue.buffer[17]).toBe(0); // matrix[1] + expect(renderQueue.buffer[18]).toBe(0); // matrix[2] + expect(renderQueue.buffer[19]).toBe(1); // matrix[3] + + // メインスレッドのキャッシュストアにキャッシュキーが設定される + const cacheKey = $cacheStore.generateKeys(1, 1, colorTransform[7]); + const cached = $cacheStore.get( + `${movieClip.instanceId}`, + "bitmapKey" + ); + expect(cached).toBe(cacheKey); + + // cleanup + $cacheStore.removeById(`${movieClip.instanceId}`); + movieClip.cacheAsBitmap = null; + renderQueue.buffer.fill(0); + renderQueue.offset = 0; + }); + + it("cacheAsBitmap cache hit で子要素をスキップ", () => + { + const movieClip = new MovieClip(); + const childShape = new Shape(); + movieClip.addChild(childShape); + + movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + stage.rendererScale = 1; + + const matrix = new Float32Array([1, 0, 0, 1, 0, 0]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + // キャッシュストアにキャッシュキーとローカルバウンズを事前設定(cache hitを模擬) + const cacheKey = $cacheStore.generateKeys(1, 1, colorTransform[7]); + $cacheStore.set(`${movieClip.instanceId}`, "bitmapKey", cacheKey); + $cacheStore.set(`${movieClip.instanceId}`, "bLocalBounds", new Float32Array([0, 0, 100, 80])); + + renderQueue.offset = 0; + renderQueue.buffer.fill(0); + + const offsetBefore = renderQueue.offset; + execute(movieClip, [], matrix, colorTransform, 800, 600); + const offsetAfter = renderQueue.offset; + + // visible=1, CONTAINER_TYPE + expect(renderQueue.buffer[0]).toBe(1); + expect(renderQueue.buffer[1]).toBe($RENDERER_CONTAINER_TYPE); + // blendMode (normal=11) + expect(renderQueue.buffer[2]).toBe(11); + // useLayer=1 + expect(renderQueue.buffer[3]).toBe(1); + + // layerType=2 (cacheAsBitmap), cacheHit=1 + expect(renderQueue.buffer[6]).toBe(2); + expect(renderQueue.buffer[7]).toBe(1); + + // instanceId + expect(renderQueue.buffer[8]).toBe(movieClip.instanceId); + + // cache hit の場合、子要素のデータは含まれない(offsetが小さい) + expect(offsetAfter - offsetBefore).toBeLessThan(35); + + // cleanup + $cacheStore.removeById(`${movieClip.instanceId}`); + movieClip.cacheAsBitmap = null; + renderQueue.buffer.fill(0); + renderQueue.offset = 0; + }); + + it("cacheAsBitmap: 子要素変更後もキャッシュが維持される", () => + { + const movieClip = new MovieClip(); + const childShape = new Shape(); + movieClip.addChild(childShape); + + movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + stage.rendererScale = 1; + + const matrix = new Float32Array([1, 0, 0, 1, 0, 0]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + const cacheKey = $cacheStore.generateKeys(1, 1, colorTransform[7]); + + // キャッシュヒット状態を作る + $cacheStore.set(`${movieClip.instanceId}`, "bitmapKey", cacheKey); + $cacheStore.set(`${movieClip.instanceId}`, "bLocalBounds", new Float32Array([0, 0, 100, 80])); + + // 子要素を変更 + movieClip.addChild(new Shape()); + + renderQueue.offset = 0; + renderQueue.buffer.fill(0); + + execute(movieClip, [], matrix, colorTransform, 800, 600); + + // layerType=2 (cacheAsBitmap), cacheHit=1 → キャッシュが維持されている + expect(renderQueue.buffer[6]).toBe(2); + expect(renderQueue.buffer[7]).toBe(1); + + // cleanup + $cacheStore.removeById(`${movieClip.instanceId}`); + movieClip.cacheAsBitmap = null; + renderQueue.buffer.fill(0); + renderQueue.offset = 0; + }); + + it("cacheAsBitmap: cache miss時にmatrix a/b/c/dがrenderScaleを使用(rendererScaleを含む)", () => + { + const movieClip = new MovieClip(); + const childShape = new Shape(); + movieClip.addChild(childShape); + + movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + // rendererScale=2でテスト(parentScaleではなくrendererScaleを使用) + stage.rendererScale = 2; + + const matrix = new Float32Array([2, 0, 0, 2, 100, 50]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + $cacheStore.removeById(`${movieClip.instanceId}`); + + renderQueue.offset = 0; + renderQueue.buffer.fill(0); + + execute(movieClip, [], matrix, colorTransform, 800, 600); + + // renderScaleX, renderScaleY = 2, parent matrix a,b,c,d + expect(renderQueue.buffer[14]).toBe(2); // renderScaleX + expect(renderQueue.buffer[15]).toBe(2); // renderScaleY + expect(renderQueue.buffer[16]).toBe(2); // matrix[0] + expect(renderQueue.buffer[17]).toBe(0); // matrix[1] + + // cleanup + $cacheStore.removeById(`${movieClip.instanceId}`); + movieClip.cacheAsBitmap = null; + renderQueue.buffer.fill(0); + renderQueue.offset = 0; + stage.rendererScale = 1; + }); + + it("cacheAsBitmap: cache hit時にもrenderScaleを使用", () => + { + const movieClip = new MovieClip(); + const childShape = new Shape(); + movieClip.addChild(childShape); + + movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + // rendererScale=2でテスト + stage.rendererScale = 2; + + const matrix = new Float32Array([2, 0, 0, 2, 100, 50]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + // renderScale = cacheScale(1) * ownScale(1) * rendererScale(2) = 2 + const cacheKey = $cacheStore.generateKeys(2, 2, colorTransform[7]); + $cacheStore.set( + `${movieClip.instanceId}`, + "bitmapKey", cacheKey + ); + // HIT高速パスに必要なローカルバウンズ + $cacheStore.set( + `${movieClip.instanceId}`, + "bLocalBounds", + new Float32Array([0, 0, 100, 80]) + ); + + renderQueue.offset = 0; + renderQueue.buffer.fill(0); + + execute(movieClip, [], matrix, colorTransform, 800, 600); + + // cache hit + expect(renderQueue.buffer[7]).toBe(1); + + // renderScaleX, renderScaleY = cacheScale(1) * ownScale(1) * rendererScale(2) = 2 + expect(renderQueue.buffer[14]).toBe(2); // renderScaleX + expect(renderQueue.buffer[15]).toBe(2); // renderScaleY + // parent matrix a, b, c, d + expect(renderQueue.buffer[16]).toBe(2); // matrix[0] = parentScale + expect(renderQueue.buffer[17]).toBe(0); // matrix[1] + expect(renderQueue.buffer[18]).toBe(0); // matrix[2] + expect(renderQueue.buffer[19]).toBe(2); // matrix[3] = parentScale + + // cleanup + $cacheStore.removeById(`${movieClip.instanceId}`); + movieClip.cacheAsBitmap = null; + stage.rendererScale = 1; + renderQueue.buffer.fill(0); + renderQueue.offset = 0; + }); + + it("cacheAsBitmap: rendererScale変更でキャッシュキーが変わる", () => + { + const movieClip = new MovieClip(); + movieClip.addChild(new Shape()); + + movieClip.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + // 実パイプラインでは$renderMatrixにrendererScaleが含まれるため + // matrixパラメータもrendererScaleを反映する必要がある + + // rendererScale=1 でmatrix=[1,0,0,1]($renderMatrixシミュレート) + stage.rendererScale = 1; + const matrix1 = new Float32Array([1, 0, 0, 1, 0, 0]); + const cacheKey1 = $cacheStore.generateKeys(1, 1, colorTransform[7]); + $cacheStore.set( + `${movieClip.instanceId}`, + "bitmapKey", cacheKey1 + ); + + // rendererScale=2 でmatrix=[2,0,0,2]($renderMatrixがscale=2に変更) + stage.rendererScale = 2; + const matrix2 = new Float32Array([2, 0, 0, 2, 0, 0]); + const cacheKey2 = $cacheStore.generateKeys(2, 2, colorTransform[7]); + expect(cacheKey1).not.toBe(cacheKey2); + + renderQueue.offset = 0; + renderQueue.buffer.fill(0); + + execute(movieClip, [], matrix2, colorTransform, 800, 600); + + // cache miss(cacheHit=0): parentScaleX=2(rendererScale含む)→ キャッシュキー変更 + expect(renderQueue.buffer[7]).toBe(0); + + // cleanup + stage.rendererScale = 1; + $cacheStore.removeById(`${movieClip.instanceId}`); + movieClip.cacheAsBitmap = null; + renderQueue.buffer.fill(0); + renderQueue.offset = 0; + }); }); \ No newline at end of file diff --git a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.ts b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.ts index 8d536d70..396045ab 100644 --- a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.ts +++ b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerGenerateRenderQueueUseCase.ts @@ -14,6 +14,7 @@ import { execute as displayObjectContainerGenerateClipQueueUseCase } from "../.. import { execute as displayObjectBlendToNumberService } from "../../DisplayObject/service/DisplayObjectBlendToNumberService"; import { execute as displayObjectContainerGetLayerBoundsUseCase } from "./DisplayObjectContainerGetLayerBoundsUseCase"; import { execute as displayObjectContainerCalcBoundsMatrixUseCase } from "../../DisplayObjectContainer/usecase/DisplayObjectContainerCalcBoundsMatrixUseCase"; +import { stage } from "../../Stage"; import { renderQueue } from "@next2d/render-queue"; import { $cacheStore } from "@next2d/cache"; import { @@ -22,7 +23,8 @@ import { $poolBoundsArray, $RENDERER_CONTAINER_TYPE, $getFloat32Array8, - $getFloat32Array6 + $getFloat32Array6, + $poolFloat32Array6 } from "../../DisplayObjectUtil"; import { ColorTransform, @@ -114,157 +116,404 @@ export const execute =

( const blendMode = display_object_container.blendMode; renderQueue.push(displayObjectBlendToNumberService(blendMode)); - // filters - const filters = display_object_container.filters; - if (filters) { - const filterKey = $cacheStore.generateFilterKeys( - tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3] + // cacheAsBitmap: フィルターキャッシュ形式でコンテナ全体をビットマップキャッシュ + // キャッシュテクスチャは親のスケールを含むサイズで描画し、 + // コンポジットは setTransform(1,0,0,1,x,y) で1:1描画されるため正しい画面サイズになる + // 親の移動はキャッシュヒット(位置だけ更新)、スケール変更はキャッシュミス(再描画) + const cacheMatrix = display_object_container.cacheAsBitmap; + if (cacheMatrix) { + + const m = cacheMatrix.rawData; + const cacheScaleX = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + const cacheScaleY = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + + const ownScaleX = rawMatrix + ? Math.sqrt(rawMatrix[0] * rawMatrix[0] + rawMatrix[1] * rawMatrix[1]) + : 1; + const ownScaleY = rawMatrix + ? Math.sqrt(rawMatrix[2] * rawMatrix[2] + rawMatrix[3] * rawMatrix[3]) + : 1; + + // Shape/TextFieldと同様にstage.rendererScaleを使用 + // matrixから抽出すると親のアニメーション(スケール変動)でキーが揺れて + // 毎フレームcache MISSが発生する原因になる + const rendererScale = stage.rendererScale; + + // コンポジットスケール = cacheScale × ownScale × rendererScale + const renderScaleX = cacheScaleX * ownScaleX * rendererScale; + const renderScaleY = cacheScaleY * ownScaleY * rendererScale; + const xRounded = Math.round(renderScaleX * 100) / 100; + const yRounded = Math.round(renderScaleY * 100) / 100; + + const bitmapCacheKey = $cacheStore.generateKeys( + xRounded, yRounded, tColorTransform[7] ); - const filterCache = $cacheStore.get( + + // 固定プロパティ名で最新のキーのみ保存(古いキーが蓄積されないようにする) + // レンダラーも最新のfKeyのみ保持するため、キーの一致を保証 + const bitmapCache = $cacheStore.get( `${display_object_container.instanceId}`, - `${filterKey}` - ); + "bitmapKey" + ) === bitmapCacheKey; + + // コンテナ自身のフィルター境界を収集 + const cacheFilters = display_object_container.filters; + const cacheFilterBounds = $getBoundsArray(0, 0, 0, 0); + if (cacheFilters) { + for (let idx = 0; idx < cacheFilters.length; idx++) { + const filter = cacheFilters[idx]; + if (!filter || !filter.canApplyFilter()) { + continue; + } + filter.getBounds(cacheFilterBounds); + } + } + + if (bitmapCache) { + + // cacheAsBitmap cache hit: 子要素走査をスキップして高速パス + // Shapeと同様に親matrixをレンダラーに渡し、描画時にcacheScaleで補正 + const cachedLocalBounds = $cacheStore.get( + `${display_object_container.instanceId}`, "bLocalBounds" + ) as Float32Array | null; + + if (cachedLocalBounds) { + // ローカルバウンズ原点のスクリーン座標を計算(Shapeと同じ方式) + const localOriginX = cachedLocalBounds[0] / (cacheScaleX * rendererScale); + const localOriginY = cachedLocalBounds[1] / (cacheScaleY * rendererScale); + const screenX = matrix[0] * localOriginX + matrix[2] * localOriginY + matrix[4]; + const screenY = matrix[1] * localOriginX + matrix[3] * localOriginY + matrix[5]; + + // Shapeと同様にmatrixにcacheScaleを乗算して送る + // レンダラーで matrix/renderScale → cacheScale成分が反映される + renderQueue.push(1, + 0, 0, // layerWidth/Height: HIT時は未使用 + 2, 1, display_object_container.instanceId, bitmapCacheKey, + cacheFilterBounds[0], cacheFilterBounds[1], cacheFilterBounds[2], cacheFilterBounds[3], + renderScaleX, renderScaleY, + matrix[0] * cacheScaleX, matrix[1] * cacheScaleX, + matrix[2] * cacheScaleY, matrix[3] * cacheScaleY, + screenX, screenY, + tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], + tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7] + ); - let updated = false; - const params = []; - const bounds = $getBoundsArray(0, 0, 0, 0); - for (let idx = 0; idx < filters.length; idx++) { + $poolBoundsArray(cacheFilterBounds); - const filter = filters[idx]; - if (!filter || !filter.canApplyFilter()) { - continue; + if (tColorTransform !== color_transform) { + ColorTransform.release(tColorTransform); + } + if (tMatrix !== matrix) { + Matrix.release(tMatrix); + } + return ; } + // cachedLocalBoundsがない場合はMISSにフォールスルー + } - // フィルターが更新されたかをチェック - if (filter.$updated) { - updated = true; + // cacheAsBitmap cache miss: 初回描画 + // 親matrixのスケール成分のみを含むローカル空間でキャッシュを生成する + // (親の位置・回転は含まず、スケールのみ反映してテクスチャサイズを画面と一致させる) + + // フィルターパラメータを収集 + const cacheFilterParams: number[] = []; + if (cacheFilters) { + for (let idx = 0; idx < cacheFilters.length; idx++) { + const filter = cacheFilters[idx]; + if (!filter || !filter.canApplyFilter()) { + continue; + } + const buffer = filter.toNumberArray(); + for (let idx = 0; idx < buffer.length; idx += 4096) { + cacheFilterParams.push(...buffer.subarray(idx, idx + 4096)); + } } - filter.$updated = false; + } - filter.getBounds(bounds); + // キャッシュ描画用の親matrix: cacheScale × rendererScale(対角行列) + const cacheParentMatrix = $getFloat32Array6( + cacheScaleX * rendererScale, 0, + 0, cacheScaleY * rendererScale, + 0, 0 + ); + const localLayerBounds = displayObjectContainerGetLayerBoundsUseCase( + display_object_container, cacheParentMatrix + ); - const buffer = filter.toNumberArray(); + const localLayerWidth = Math.ceil(Math.abs(localLayerBounds[2] - localLayerBounds[0])); + const localLayerHeight = Math.ceil(Math.abs(localLayerBounds[3] - localLayerBounds[1])); + + // Shapeと同じ方式でスクリーン座標を計算 + const localOriginX = localLayerBounds[0] / (cacheScaleX * rendererScale); + const localOriginY = localLayerBounds[1] / (cacheScaleY * rendererScale); + const missScreenX = matrix[0] * localOriginX + matrix[2] * localOriginY + matrix[4]; + const missScreenY = matrix[1] * localOriginX + matrix[3] * localOriginY + matrix[5]; + + renderQueue.push( + 1, + localLayerWidth, localLayerHeight, + 2, 0, display_object_container.instanceId, bitmapCacheKey, + cacheFilterBounds[0], cacheFilterBounds[1], cacheFilterBounds[2], cacheFilterBounds[3], + renderScaleX, renderScaleY, + matrix[0] * cacheScaleX, matrix[1] * cacheScaleX, + matrix[2] * cacheScaleY, matrix[3] * cacheScaleY, + missScreenX, missScreenY, + tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], + tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7], + cacheFilterParams.length + ); + if (cacheFilterParams.length > 0) { + renderQueue.set(new Float32Array(cacheFilterParams)); + } - for (let idx = 0; idx < buffer.length; idx += 4096) { - params.push(...buffer.subarray(idx, idx + 4096)); - } + // 子要素の描画マトリクスをローカル空間に切り替え(親matrixの位置・回転を除外) + // cacheParentMatrixにrawMatrixを乗算し、layerBoundsオフセットで原点を調整 + let localMatrix: Float32Array; + if (rawMatrix) { + localMatrix = Matrix.multiply(cacheParentMatrix, rawMatrix); + } else { + localMatrix = $getFloat32Array6( + cacheParentMatrix[0], 0, + 0, cacheParentMatrix[3], + 0, 0 + ); } - const useFilfer = params.length > 0; - if (useFilfer) { + const localTx = localMatrix[4] - localLayerBounds[0]; + const localTy = localMatrix[5] - localLayerBounds[1]; - // 子の変更があった場合は親のフラグが立っているので更新 - if (!updated) { - updated = display_object_container.changed; - } + if (tMatrix !== matrix) { + Matrix.release(tMatrix); + } + tMatrix = $getFloat32Array6( + localMatrix[0], localMatrix[1], + localMatrix[2], localMatrix[3], + localTx, localTy + ); + + if (rawMatrix) { + Matrix.release(localMatrix); + } else { + $poolFloat32Array6(localMatrix); + } + Matrix.release(cacheParentMatrix); + + if (tColorTransform !== color_transform) { + ColorTransform.release(tColorTransform); + } + tColorTransform = $getFloat32Array8(1, 1, 1, 1, 0, 0, 0, 0); - const layerBounds = displayObjectContainerGetLayerBoundsUseCase( - display_object_container, matrix + // HIT時の高速パス用にローカルバウンズをキャッシュ(poolする前に保存) + $cacheStore.set( + `${display_object_container.instanceId}`, + "bLocalBounds", + new Float32Array([localLayerBounds[0], localLayerBounds[1], localLayerBounds[2], localLayerBounds[3]]) + ); + + $poolBoundsArray(localLayerBounds); + $poolBoundsArray(cacheFilterBounds); + + $cacheStore.set( + `${display_object_container.instanceId}`, + "bitmapKey", bitmapCacheKey + ); + + } else { + + // filters + const filters = display_object_container.filters; + if (filters) { + const filterKey = $cacheStore.generateFilterKeys( + tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3] + ); + const filterCache = $cacheStore.get( + `${display_object_container.instanceId}`, + `${filterKey}` ); - if (filterCache) { + let updated = false; + const params = []; + const bounds = $getBoundsArray(0, 0, 0, 0); + for (let idx = 0; idx < filters.length; idx++) { + + const filter = filters[idx]; + if (!filter || !filter.canApplyFilter()) { + continue; + } + + // フィルターが更新されたかをチェック + if (filter.$updated) { + updated = true; + } + filter.$updated = false; + + filter.getBounds(bounds); - // キャッシュがあって、変更がなければキャッシュを使用 + const buffer = filter.toNumberArray(); + + for (let idx = 0; idx < buffer.length; idx += 4096) { + params.push(...buffer.subarray(idx, idx + 4096)); + } + } + + const useFilfer = params.length > 0; + if (useFilfer) { + + // 子の変更があった場合は親のフラグが立っているので更新 if (!updated) { - renderQueue.push(1, - Math.ceil(Math.abs(layerBounds[2] - layerBounds[0])), - Math.ceil(Math.abs(layerBounds[3] - layerBounds[1])), - 1, 1, display_object_container.instanceId, filterKey, - bounds[0], bounds[1], bounds[2], bounds[3], - tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerBounds[0], layerBounds[1], - tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], - tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7] - ); + updated = display_object_container.changed; + } - $poolBoundsArray(layerBounds); - $poolBoundsArray(bounds); + const layerBounds = displayObjectContainerGetLayerBoundsUseCase( + display_object_container, matrix + ); - if (tColorTransform !== color_transform) { - ColorTransform.release(tColorTransform); - } - if (tMatrix !== matrix) { - Matrix.release(tMatrix); + if (filterCache) { + + // キャッシュがあって、変更がなければキャッシュを使用 + if (!updated) { + renderQueue.push(1, + Math.ceil(Math.abs(layerBounds[2] - layerBounds[0])), + Math.ceil(Math.abs(layerBounds[3] - layerBounds[1])), + 1, 1, display_object_container.instanceId, filterKey, + bounds[0], bounds[1], bounds[2], bounds[3], + tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerBounds[0], layerBounds[1], + tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], + tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7] + ); + + $poolBoundsArray(layerBounds); + $poolBoundsArray(bounds); + + if (tColorTransform !== color_transform) { + ColorTransform.release(tColorTransform); + } + if (tMatrix !== matrix) { + Matrix.release(tMatrix); + } + return ; } - return ; + + // どこかで変更があったので、キャッシュを削除 + $cacheStore.removeById(`${display_object_container.instanceId}`); } - // どこかで変更があったので、キャッシュを削除 - $cacheStore.removeById(`${display_object_container.instanceId}`); - } + renderQueue.push( + 1, + Math.ceil(Math.abs(layerBounds[2] - layerBounds[0])), + Math.ceil(Math.abs(layerBounds[3] - layerBounds[1])), + 1, 0, display_object_container.instanceId, filterKey, + bounds[0], bounds[1], bounds[2], bounds[3], + tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerBounds[0], layerBounds[1], + tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], + tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7], + params.length + ); + renderQueue.set(new Float32Array(params)); - renderQueue.push( - 1, - Math.ceil(Math.abs(layerBounds[2] - layerBounds[0])), - Math.ceil(Math.abs(layerBounds[3] - layerBounds[1])), - 1, 0, display_object_container.instanceId, filterKey, - bounds[0], bounds[1], bounds[2], bounds[3], - tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerBounds[0], layerBounds[1], - tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], - tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7], - params.length - ); - renderQueue.set(new Float32Array(params)); + const fa0 = tMatrix[0]; + const fa1 = tMatrix[1]; + const fa2 = tMatrix[2]; + const fa3 = tMatrix[3]; + const faTx = tMatrix[4] - layerBounds[0]; + const faTy = tMatrix[5] - layerBounds[1]; - const fa0 = tMatrix[0]; - const fa1 = tMatrix[1]; - const fa2 = tMatrix[2]; - const fa3 = tMatrix[3]; - const faTx = tMatrix[4] - layerBounds[0]; - const faTy = tMatrix[5] - layerBounds[1]; + if (tMatrix !== matrix) { + Matrix.release(tMatrix); + } + tMatrix = $getFloat32Array6(fa0, fa1, fa2, fa3, faTx, faTy); - if (tMatrix !== matrix) { - Matrix.release(tMatrix); - } - tMatrix = $getFloat32Array6(fa0, fa1, fa2, fa3, faTx, faTy); + if (tColorTransform !== color_transform) { + ColorTransform.release(tColorTransform); + } - if (tColorTransform !== color_transform) { - ColorTransform.release(tColorTransform); - } + tColorTransform = $getFloat32Array8(1, 1, 1, 1, 0, 0, 0, 0); - tColorTransform = $getFloat32Array8(1, 1, 1, 1, 0, 0, 0, 0); + $poolBoundsArray(layerBounds); - $poolBoundsArray(layerBounds); + $cacheStore.set( + `${display_object_container.instanceId}`, + `${filterKey}`, true + ); - $cacheStore.set( - `${display_object_container.instanceId}`, - `${filterKey}`, true - ); + } else { + if (blendMode === "normal") { + renderQueue.push(0); + } else { + + // ブレンドモードのみのLayerモード + const layerBounds = displayObjectContainerCalcBoundsMatrixUseCase( + display_object_container, + matrix + ); + const layerXMin = layerBounds[0]; + const layerYMin = layerBounds[1]; + + renderQueue.push( + 1, + Math.ceil(Math.abs(layerBounds[2] - layerXMin)), + Math.ceil(Math.abs(layerBounds[3] - layerYMin)), + 0, // not use filter, + tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerXMin, layerYMin, + tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], + tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7] + ); + + const a0 = tMatrix[0]; + const a1 = tMatrix[1]; + const a2 = tMatrix[2]; + const a3 = tMatrix[3]; + const adjustedTx1 = tMatrix[4] - layerXMin; + const adjustedTy1 = tMatrix[5] - layerYMin; + + if (tMatrix !== matrix) { + Matrix.release(tMatrix); + } + tMatrix = $getFloat32Array6(a0, a1, a2, a3, adjustedTx1, adjustedTy1); + $poolBoundsArray(layerBounds); + + if (tColorTransform !== color_transform) { + ColorTransform.release(tColorTransform); + } + tColorTransform = $getFloat32Array8(1, 1, 1, 1, 0, 0, 0, 0); + } + } + + $poolBoundsArray(bounds); } else { if (blendMode === "normal") { renderQueue.push(0); } else { - - // ブレンドモードのみのLayerモード const layerBounds = displayObjectContainerCalcBoundsMatrixUseCase( display_object_container, matrix ); - const layerXMin = layerBounds[0]; - const layerYMin = layerBounds[1]; + const layerXMin2 = layerBounds[0]; + const layerYMin2 = layerBounds[1]; renderQueue.push( 1, - Math.ceil(Math.abs(layerBounds[2] - layerXMin)), - Math.ceil(Math.abs(layerBounds[3] - layerYMin)), + Math.ceil(Math.abs(layerBounds[2] - layerXMin2)), + Math.ceil(Math.abs(layerBounds[3] - layerYMin2)), 0, // not use filter, - tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerXMin, layerYMin, + tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerXMin2, layerYMin2, tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7] ); - const a0 = tMatrix[0]; - const a1 = tMatrix[1]; - const a2 = tMatrix[2]; - const a3 = tMatrix[3]; - const adjustedTx1 = tMatrix[4] - layerXMin; - const adjustedTy1 = tMatrix[5] - layerYMin; + const b0 = tMatrix[0]; + const b1 = tMatrix[1]; + const b2 = tMatrix[2]; + const b3 = tMatrix[3]; + const adjustedTx2 = tMatrix[4] - layerXMin2; + const adjustedTy2 = tMatrix[5] - layerYMin2; if (tMatrix !== matrix) { Matrix.release(tMatrix); } - tMatrix = $getFloat32Array6(a0, a1, a2, a3, adjustedTx1, adjustedTy1); + tMatrix = $getFloat32Array6(b0, b1, b2, b3, adjustedTx2, adjustedTy2); $poolBoundsArray(layerBounds); if (tColorTransform !== color_transform) { @@ -274,48 +523,7 @@ export const execute =

( } } - $poolBoundsArray(bounds); - } else { - if (blendMode === "normal") { - renderQueue.push(0); - } else { - const layerBounds = displayObjectContainerCalcBoundsMatrixUseCase( - display_object_container, - matrix - ); - - const layerXMin2 = layerBounds[0]; - const layerYMin2 = layerBounds[1]; - - renderQueue.push( - 1, - Math.ceil(Math.abs(layerBounds[2] - layerXMin2)), - Math.ceil(Math.abs(layerBounds[3] - layerYMin2)), - 0, // not use filter, - tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], layerXMin2, layerYMin2, - tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], - tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7] - ); - - const b0 = tMatrix[0]; - const b1 = tMatrix[1]; - const b2 = tMatrix[2]; - const b3 = tMatrix[3]; - const adjustedTx2 = tMatrix[4] - layerXMin2; - const adjustedTy2 = tMatrix[5] - layerYMin2; - - if (tMatrix !== matrix) { - Matrix.release(tMatrix); - } - tMatrix = $getFloat32Array6(b0, b1, b2, b3, adjustedTx2, adjustedTy2); - $poolBoundsArray(layerBounds); - - if (tColorTransform !== color_transform) { - ColorTransform.release(tColorTransform); - } - tColorTransform = $getFloat32Array8(1, 1, 1, 1, 0, 0, 0, 0); - } - } + } // end cacheAsBitmap else // mask const maskDisplayObject = display_object_container.mask; diff --git a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerMouseHitUseCase.ts b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerMouseHitUseCase.ts index 357c8d07..5a8e393a 100644 --- a/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerMouseHitUseCase.ts +++ b/packages/display/src/DisplayObjectContainer/usecase/DisplayObjectContainerMouseHitUseCase.ts @@ -15,7 +15,9 @@ import { $getArray, $poolArray, $getMap, - $poolMap + $poolMap, + $getFloat32Array6, + $poolFloat32Array6 } from "../../DisplayObjectUtil"; /** @@ -44,11 +46,35 @@ export const execute =

=> { if (object.type === "zlib") { - await new Promise((resolve): void => + await new Promise((resolve, reject): void => { $unzipWorker.onmessage = (event: MessageEvent): void => { + if (event.data && event.data.error) { + reject(new Error(event.data.error)); + return; + } loaderBuildService(loader, event.data as IAnimationToolData); resolve(); }; + $unzipWorker.onerror = (event: ErrorEvent): void => + { + reject(new Error(event.message)); + }; + const buffer: Uint8Array = new Uint8Array(object.buffer); $unzipWorker.postMessage(buffer, [buffer.buffer]); }); diff --git a/packages/display/src/Loader/worker/ZlibInflateWorker.ts b/packages/display/src/Loader/worker/ZlibInflateWorker.ts index 6653e1d6..f8f98825 100644 --- a/packages/display/src/Loader/worker/ZlibInflateWorker.ts +++ b/packages/display/src/Loader/worker/ZlibInflateWorker.ts @@ -1,26 +1,416 @@ "use strict"; -import { decompressSync } from "fflate"; +/** + * @description fflate非依存の高速 zlib/DEFLATE 解凍ワーカー (RFC 1950 / RFC 1951) + * High-performance zlib/DEFLATE decompression worker without fflate dependency. + */ + +const _$LEN_BASE = new Uint16Array([ + 3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31, + 35,43,51,59,67,83,99,115,131,163,195,227,258 +]); +const _$LEN_EXTRA = new Uint8Array([ + 0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2, + 3,3,3,3,4,4,4,4,5,5,5,5,0 +]); +const _$DIST_BASE = new Uint16Array([ + 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193, + 257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577 +]); +const _$DIST_EXTRA = new Uint8Array([ + 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6, + 7,7,8,8,9,9,10,10,11,11,12,12,13,13 +]); +const _$CL_ORDER = new Uint8Array([ + 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 +]); + +const _$MASK = new Uint32Array(17); +for (let i = 1; i < 17; i++) { _$MASK[i] = (1 << i) - 1 } + +const _$FIXED_LIT_CL = new Uint8Array(288); +for (let i = 0; i <= 143; i++) { _$FIXED_LIT_CL[i] = 8 } +for (let i = 144; i <= 255; i++) { _$FIXED_LIT_CL[i] = 9 } +for (let i = 256; i <= 279; i++) { _$FIXED_LIT_CL[i] = 7 } +for (let i = 280; i <= 287; i++) { _$FIXED_LIT_CL[i] = 8 } + +const _$FIXED_DIST_CL = new Uint8Array(32).fill(5); + +const _$decoder = new TextDecoder(); + +let _$outBuf = new Uint8Array(65536); + +const _$blCount = new Uint16Array(16); +const _$nextCode = new Uint16Array(16); +const _$clLens = new Uint8Array(19); +const _$codeLens = new Uint8Array(320); + +let _$dynClTbl: Uint32Array = new Uint32Array(128); +let _$dynLitTbl: Uint32Array = new Uint32Array(2048); +let _$dynDistTbl: Uint32Array = new Uint32Array(1024); + +/** + * @description canonical Huffmanルックアップテーブルを構築する。 + * 各エントリは (symbol << 4) | codeLength の形式で格納される。 + * Builds a canonical Huffman lookup table. Each entry stores (symbol << 4) | codeLength. + * + * @param {Uint8Array} codeLens - シンボルごとのコード長配列 + * @param {number} n - codeLensの有効要素数 + * @param {Uint32Array} [reuse] - 再利用するテーブルバッファ(GC回避用) + * @return {[Uint32Array, number]} [テーブル, 最大コード長] + * @method + * @private + */ +const _$buildTable = (codeLens: Uint8Array, n: number, reuse?: Uint32Array): [Uint32Array, number] => +{ + let max = 0; + _$blCount.fill(0); + for (let i = 0; i < n; i++) { + const l = codeLens[i]; + if (l) { + _$blCount[l]++; + if (l > max) { max = l } + } + } + if (!max) { + if (reuse) { + reuse[0] = 0; + return [reuse, 1]; + } + return [new Uint32Array(2), 1]; + } + + _$nextCode.fill(0); + for (let b = 1, c = 0; b <= max; b++) { + c = c + _$blCount[b - 1] << 1; + _$nextCode[b] = c; + } + + const size = 1 << max; + let tbl: Uint32Array; + if (reuse && reuse.length >= size) { + tbl = reuse; + tbl.fill(0, 0, size); + } else { + tbl = new Uint32Array(size); + } + + for (let s = 0; s < n; s++) { + const l = codeLens[s]; + if (!l) { continue } + let c = _$nextCode[l]++; + let r = 0; + for (let i = 0; i < l; i++) { + r = r << 1 | c & 1; + c >>= 1; + } + const entry = s << 4 | l; + for (let j = r; j < size; j += 1 << l) { + tbl[j] = entry; + } + } + + return [tbl, max]; +}; + +const [_$FIXED_LIT_TBL, _$FIXED_LIT_BITS] = _$buildTable(_$FIXED_LIT_CL, 288); +const [_$FIXED_DIST_TBL, _$FIXED_DIST_BITS] = _$buildTable(_$FIXED_DIST_CL, 32); +const _$FIXED_LIT_MASK = _$MASK[_$FIXED_LIT_BITS]; +const _$FIXED_DIST_MASK = _$MASK[_$FIXED_DIST_BITS]; + +let _$src: Uint8Array; +let _$pos: number; +let _$buf: number; +let _$cnt: number; + +/** + * @description ビットストリームからnビットを読み取って返す。 + * Reads n bits from the bit stream and returns the value. + * + * @param {number} n - 読み取るビット数 + * @return {number} 読み取った値 + * @method + * @private + */ +const _$bits = (n: number): number => +{ + while (_$cnt < n) { + _$buf |= _$src[_$pos++] << _$cnt; + _$cnt += 8; + } + const v = _$buf & _$MASK[n]; + _$buf >>>= n; + _$cnt -= n; + return v; +}; + +/** + * @description Huffmanルックアップテーブルから1シンボルをデコードする。 + * Decodes one symbol from the Huffman lookup table. + * + * @param {Uint32Array} tbl - Huffmanルックアップテーブル + * @param {number} maxBits - テーブルの最大コード長 + * @param {number} mask - ビットマスク ((1 << maxBits) - 1) + * @return {number} デコードされたシンボル値 + * @method + * @private + */ +const _$huf = (tbl: Uint32Array, maxBits: number, mask: number): number => +{ + while (_$cnt < maxBits) { + _$buf |= _$src[_$pos++] << _$cnt; + _$cnt += 8; + } + const e = tbl[_$buf & mask]; + _$buf >>>= e & 0xF; + _$cnt -= e & 0xF; + return e >> 4; +}; + +/** + * @description DEFLATEストリームを解凍する (RFC 1951)。 + * stored / fixed Huffman / dynamic Huffman の全ブロックタイプに対応。 + * Decompresses a DEFLATE stream. Supports all block types: stored, fixed and dynamic Huffman. + * + * @param {Uint8Array} data - 圧縮データ + * @param {number} offset - DEFLATEストリームの開始オフセット + * @return {Uint8Array} 解凍されたバイト列 + * @method + * @private + */ +const _$inflate = (data: Uint8Array, offset: number): Uint8Array => +{ + _$src = data; + _$pos = offset; + _$buf = 0; + _$cnt = 0; + + const estimatedSize = Math.max(data.length * 6, 4096); + if (_$outBuf.length < estimatedSize) { + _$outBuf = new Uint8Array(estimatedSize); + } + let out = _$outBuf; + let op = 0; + + let fin = 0; + while (!fin) { + fin = _$bits(1); + const bt = _$bits(2); + + if (bt === 0) { + const skip = _$cnt & 7; + _$buf >>>= skip; + _$cnt -= skip; + const len = _$bits(16); + _$bits(16); + + if (op + len > out.length) { + let sz = out.length; + while (sz < op + len) { sz <<= 1 } + const nb = new Uint8Array(sz); + nb.set(out); + out = nb; + } + + out.set(_$src.subarray(_$pos, _$pos + len), op); + _$pos += len; + op += len; + + } else if (bt === 3) { + throw new Error("Invalid DEFLATE block type"); + + } else { + let lt: Uint32Array, lm: number, lb: number; + let dt: Uint32Array, dm: number, db: number; + + if (bt === 1) { + lt = _$FIXED_LIT_TBL; + lb = _$FIXED_LIT_BITS; + lm = _$FIXED_LIT_MASK; + dt = _$FIXED_DIST_TBL; + db = _$FIXED_DIST_BITS; + dm = _$FIXED_DIST_MASK; + } else { + const hlit = _$bits(5) + 257; + const hdist = _$bits(5) + 1; + const hclen = _$bits(4) + 4; + + _$clLens.fill(0); + for (let i = 0; i < hclen; i++) { + _$clLens[_$CL_ORDER[i]] = _$bits(3); + } + let clb: number; + [_$dynClTbl, clb] = _$buildTable(_$clLens, 19, _$dynClTbl); + const clm = _$MASK[clb]; + + const total = hlit + hdist; + _$codeLens.fill(0, 0, total); + for (let i = 0; i < total;) { + const s = _$huf(_$dynClTbl, clb, clm); + if (s < 16) { + _$codeLens[i++] = s; + } else if (s === 16) { + const p = _$codeLens[i - 1]; + for (let r = _$bits(2) + 3; r > 0; r--) { _$codeLens[i++] = p } + } else if (s === 17) { + i += _$bits(3) + 3; + } else { + i += _$bits(7) + 11; + } + } + + [_$dynLitTbl, lb] = _$buildTable(_$codeLens.subarray(0, hlit), hlit, _$dynLitTbl); + lm = _$MASK[lb]; + lt = _$dynLitTbl; + [_$dynDistTbl, db] = _$buildTable(_$codeLens.subarray(hlit, total), hdist, _$dynDistTbl); + dm = _$MASK[db]; + dt = _$dynDistTbl; + } + + for (;;) { + + while (_$cnt < lb) { + _$buf |= _$src[_$pos++] << _$cnt; + _$cnt += 8; + } + const le = lt[_$buf & lm]; + const sl = le & 0xF; + _$buf >>>= sl; + _$cnt -= sl; + const sym = le >> 4; + + if (sym < 256) { + if (op >= out.length) { + const nb = new Uint8Array(out.length << 1); + nb.set(out); + out = nb; + } + out[op++] = sym; + + } else if (sym === 256) { + break; + + } else { + const li = sym - 257; + let length = _$LEN_BASE[li]; + const le2 = _$LEN_EXTRA[li]; + if (le2) { + while (_$cnt < le2) { + _$buf |= _$src[_$pos++] << _$cnt; + _$cnt += 8; + } + length += _$buf & _$MASK[le2]; + _$buf >>>= le2; + _$cnt -= le2; + } + + while (_$cnt < db) { + _$buf |= _$src[_$pos++] << _$cnt; + _$cnt += 8; + } + const de = dt[_$buf & dm]; + const dl = de & 0xF; + _$buf >>>= dl; + _$cnt -= dl; + const di = de >> 4; + + let dist = _$DIST_BASE[di]; + const de2 = _$DIST_EXTRA[di]; + if (de2) { + while (_$cnt < de2) { + _$buf |= _$src[_$pos++] << _$cnt; + _$cnt += 8; + } + dist += _$buf & _$MASK[de2]; + _$buf >>>= de2; + _$cnt -= de2; + } + + if (op + length > out.length) { + let sz = out.length; + while (sz < op + length) { sz <<= 1 } + const nb = new Uint8Array(sz); + nb.set(out); + out = nb; + } + + const sp = op - dist; + if (dist === 1) { + out.fill(out[sp], op, op + length); + op += length; + } else if (dist >= length) { + out.copyWithin(op, sp, sp + length); + op += length; + } else { + out.copyWithin(op, sp, sp + dist); + let copied = dist; + while (copied < length) { + const chunk = Math.min(copied, length - copied); + out.copyWithin(op + copied, op, op + chunk); + copied += chunk; + } + op += length; + } + } + } + } + } + + _$outBuf = out; + + return out.subarray(0, op); +}; /** - * @description zilbの圧縮されたデータを解凍します。 - * Unzips zlib-compressed data. + * @description zlibラッパー付きデータを解凍する (RFC 1950)。 + * zlibヘッダーを検出した場合は2バイトスキップし、それ以外は生DEFLATEとして処理する。 + * Decompresses zlib-wrapped data. Skips the 2-byte zlib header if detected, + * otherwise treats input as raw DEFLATE. * - * @param {MessageEvent} event + * @param {Uint8Array} input - zlib圧縮データまたは生DEFLATEストリーム + * @return {Uint8Array} 解凍されたバイト列 + * @method + * @private + */ +const _$zlibDecompress = (input: Uint8Array): Uint8Array => +{ + const cmf = input[0]; + const flg = input[1]; + + const isZlib = (cmf & 0x0F) === 8 + && (cmf * 256 + flg) % 31 === 0 + && !(flg & 0x20); + + return _$inflate(input, isZlib ? 2 : 0); +}; + +/** + * @description zlibの圧縮されたデータを解凍し、JSONとしてパースして返すワーカーエントリポイント。 + * Worker entry point that decompresses zlib data, decodes as URI-encoded string, + * parses as JSON and posts the result back. + * + * @param {MessageEvent} event - 圧縮データ (Uint8Array) を含むメッセージイベント * @return {void} * @method * @public */ self.addEventListener("message", (event: MessageEvent): void => { - const buffer = decompressSync(event.data); + try { - let json = ""; - for (let idx: number = 0; idx < buffer.length; idx += 4096) { - json += String.fromCharCode(...buffer.subarray(idx, idx + 4096)); - } + const buffer = _$zlibDecompress(event.data); + + self.postMessage(JSON.parse( + decodeURIComponent(_$decoder.decode(buffer)) + )); + + } catch (e) { - self.postMessage(JSON.parse(decodeURIComponent(json))); + self.postMessage({ + "error": e instanceof Error ? e.message : "Unknown decompression error" + }); + + } }); export default {}; \ No newline at end of file diff --git a/packages/display/src/Shape/usecase/ShapeCalcBoundsMatrixUseCase.ts b/packages/display/src/Shape/usecase/ShapeCalcBoundsMatrixUseCase.ts index d8691b7e..84a5f3d7 100644 --- a/packages/display/src/Shape/usecase/ShapeCalcBoundsMatrixUseCase.ts +++ b/packages/display/src/Shape/usecase/ShapeCalcBoundsMatrixUseCase.ts @@ -3,7 +3,11 @@ import { Matrix } from "@next2d/geom"; import { execute as displayObjectCalcBoundsMatrixService } from "../../DisplayObject/service/DisplayObjectCalcBoundsMatrixService"; import { execute as shapeGetRawBoundsService } from "../service/ShapeGetRawBoundsService"; import { execute as displayObjectGetRawMatrixUseCase } from "../../DisplayObject/usecase/DisplayObjectGetRawMatrixUseCase"; -import { $poolBoundsArray } from "../../DisplayObjectUtil"; +import { + $poolBoundsArray, + $getFloat32Array6, + $poolFloat32Array6 +} from "../../DisplayObjectUtil"; /** * @description Shapeの描画範囲を計算します。 @@ -19,7 +23,27 @@ export const execute = (shape: Shape, matrix: Float32Array | null = null): Float { const rawBounds = shapeGetRawBoundsService(shape); - const rawMatrix = displayObjectGetRawMatrixUseCase(shape); + let rawMatrix = displayObjectGetRawMatrixUseCase(shape); + + // cacheAsBitmap倍率をrawMatrixに適用 + const cacheMatrix = shape.cacheAsBitmap; + let scaledMatrix: Float32Array | null = null; + if (cacheMatrix) { + const m = cacheMatrix.rawData; + const csx = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + const csy = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + if (rawMatrix) { + scaledMatrix = $getFloat32Array6( + rawMatrix[0] * csx, rawMatrix[1] * csx, + rawMatrix[2] * csy, rawMatrix[3] * csy, + rawMatrix[4], rawMatrix[5] + ); + } else { + scaledMatrix = $getFloat32Array6(csx, 0, 0, csy, 0, 0); + } + rawMatrix = scaledMatrix; + } + if (!rawMatrix) { if (matrix) { const calcBounds = displayObjectCalcBoundsMatrixService( @@ -41,6 +65,9 @@ export const execute = (shape: Shape, matrix: Float32Array | null = null): Float matrix ? Matrix.multiply(matrix, rawMatrix) : rawMatrix ); + if (scaledMatrix) { + $poolFloat32Array6(scaledMatrix); + } $poolBoundsArray(rawBounds); return calcBounds; diff --git a/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts b/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts index ccdac919..c3af0def 100644 --- a/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts +++ b/packages/display/src/Shape/usecase/ShapeGenerateRenderQueueUseCase.ts @@ -92,10 +92,10 @@ export const execute = ( tMatrix ); - const xMin = bounds[0]; - const yMin = bounds[1]; - const xMax = bounds[2]; - const yMax = bounds[3]; + let xMin = bounds[0]; + let yMin = bounds[1]; + let xMax = bounds[2]; + let yMax = bounds[3]; $poolBoundsArray(bounds); const width = Math.ceil(Math.abs(xMax - xMin)); @@ -156,20 +156,67 @@ export const execute = ( } } - const xScale = Math.sqrt( - tMatrix[0] * tMatrix[0] - + tMatrix[1] * tMatrix[1] - ); - - const yScale = Math.sqrt( - tMatrix[2] * tMatrix[2] - + tMatrix[3] * tMatrix[3] - ); + // cacheAsBitmap: 指定Matrix × 自身のスケール × stageのrendererScaleでキャッシュ品質を決定 + // 1.0基準: Matrix(1,0,0,1)はdisplayObjectの等倍スケールを意味する + const cacheMatrix = shape.cacheAsBitmap; + let renderXScale: number; + let renderYScale: number; + let cacheScaleX = 1; + let cacheScaleY = 1; + if (cacheMatrix) { + const m = cacheMatrix.rawData; + cacheScaleX = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + cacheScaleY = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + + const ownScaleX = rawMatrix + ? Math.sqrt(rawMatrix[0] * rawMatrix[0] + rawMatrix[1] * rawMatrix[1]) + : 1; + const ownScaleY = rawMatrix + ? Math.sqrt(rawMatrix[2] * rawMatrix[2] + rawMatrix[3] * rawMatrix[3]) + : 1; + + renderXScale = cacheScaleX * ownScaleX * stage.rendererScale; + renderYScale = cacheScaleY * ownScaleY * stage.rendererScale; + + // cacheMatrix倍率をスクリーン座標のboundsにも反映 + if (cacheScaleX !== 1 || cacheScaleY !== 1) { + const modMatrix = $getFloat32Array6( + tMatrix[0] * cacheScaleX, tMatrix[1] * cacheScaleX, + tMatrix[2] * cacheScaleY, tMatrix[3] * cacheScaleY, + tMatrix[4], tMatrix[5] + ); + const modBounds = displayObjectCalcBoundsMatrixService( + graphics.xMin, graphics.yMin, + graphics.xMax, graphics.yMax, + modMatrix + ); + xMin = modBounds[0]; + yMin = modBounds[1]; + xMax = modBounds[2]; + yMax = modBounds[3]; + $poolBoundsArray(modBounds); + $poolFloat32Array6(modMatrix); + } + } else { + renderXScale = Math.sqrt( + tMatrix[0] * tMatrix[0] + + tMatrix[1] * tMatrix[1] + ); + renderYScale = Math.sqrt( + tMatrix[2] * tMatrix[2] + + tMatrix[3] * tMatrix[3] + ); + } - const xScaleRounded = Math.round(xScale * 100) / 100; - const yScaleRounded = Math.round(yScale * 100) / 100; + const xScaleRounded = Math.round(renderXScale * 100) / 100; + const yScaleRounded = Math.round(renderYScale * 100) / 100; - if (!shape.isBitmap + if (cacheMatrix && shape.cacheKey + && shape.cacheParams[0] === xScaleRounded + && shape.cacheParams[1] === yScaleRounded + ) { + // cacheAsBitmap: スケール未変更のためキャッシュキーを維持 + } else if (!shape.isBitmap && !shape.cacheKey || shape.cacheParams[0] !== xScaleRounded || shape.cacheParams[1] !== yScaleRounded @@ -188,15 +235,17 @@ export const execute = ( // rennder on renderQueue.pushShapeBuffer( 1, $RENDERER_SHAPE_TYPE, - tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], tMatrix[4], tMatrix[5], + tMatrix[0] * cacheScaleX, tMatrix[1] * cacheScaleX, + tMatrix[2] * cacheScaleY, tMatrix[3] * cacheScaleY, + tMatrix[4], tMatrix[5], tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7], xMin, yMin, xMax, yMax, graphics.xMin, graphics.yMin, graphics.xMax, graphics.yMax, - +isGridEnabled, +isDrawable, +shape.isBitmap, + +isGridEnabled, +isDrawable, shape.isBitmap ? 1 : cacheMatrix ? 2 : 0, +shape.uniqueKey, cacheKey, - xScale, yScale, + renderXScale, renderYScale, shape.instanceId // フィルターキャッシュ用のユニークキー ); diff --git a/packages/display/src/Shape/usecase/ShapeHitTestUseCase.ts b/packages/display/src/Shape/usecase/ShapeHitTestUseCase.ts index ea8d0320..c7072439 100644 --- a/packages/display/src/Shape/usecase/ShapeHitTestUseCase.ts +++ b/packages/display/src/Shape/usecase/ShapeHitTestUseCase.ts @@ -3,6 +3,10 @@ import type { Shape } from "../../Shape"; import { Matrix } from "@next2d/geom"; import { execute as displayObjectGetRawMatrixUseCase } from "../../DisplayObject/usecase/DisplayObjectGetRawMatrixUseCase"; import { execute as graphicsHitTestService } from "../../Graphics/service/GraphicsHitTestService"; +import { + $getFloat32Array6, + $poolFloat32Array6 +} from "../../DisplayObjectUtil"; /** * @description Shape のヒット判定 @@ -31,11 +35,35 @@ export const execute = ( return false; } - const rawMatrix = displayObjectGetRawMatrixUseCase(shape); + let rawMatrix = displayObjectGetRawMatrixUseCase(shape); + + // cacheAsBitmap倍率をrawMatrixに適用 + const cacheMatrix = shape.cacheAsBitmap; + let scaledMatrix: Float32Array | null = null; + if (cacheMatrix) { + const m = cacheMatrix.rawData; + const csx = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + const csy = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + if (rawMatrix) { + scaledMatrix = $getFloat32Array6( + rawMatrix[0] * csx, rawMatrix[1] * csx, + rawMatrix[2] * csy, rawMatrix[3] * csy, + rawMatrix[4], rawMatrix[5] + ); + } else { + scaledMatrix = $getFloat32Array6(csx, 0, 0, csy, 0, 0); + } + rawMatrix = scaledMatrix; + } + const tMatrix = rawMatrix ? Matrix.multiply(matrix, rawMatrix) : matrix; + if (scaledMatrix) { + $poolFloat32Array6(scaledMatrix); + } + hit_context.beginPath(); hit_context.setTransform( tMatrix[0], tMatrix[1], tMatrix[2], diff --git a/packages/display/src/TextField/usecase/TextFieldCalcBoundsMatrixUseCase.ts b/packages/display/src/TextField/usecase/TextFieldCalcBoundsMatrixUseCase.ts index 26e4efa9..3d2e1188 100644 --- a/packages/display/src/TextField/usecase/TextFieldCalcBoundsMatrixUseCase.ts +++ b/packages/display/src/TextField/usecase/TextFieldCalcBoundsMatrixUseCase.ts @@ -3,7 +3,11 @@ import { Matrix } from "@next2d/geom"; import { execute as displayObjectCalcBoundsMatrixService } from "../../DisplayObject/service/DisplayObjectCalcBoundsMatrixService"; import { execute as textFieldGetRawBoundsService } from "../service/TextFieldGetRawBoundsService"; import { execute as displayObjectGetRawMatrixUseCase } from "../../DisplayObject/usecase/DisplayObjectGetRawMatrixUseCase"; -import { $poolBoundsArray } from "../../DisplayObjectUtil"; +import { + $poolBoundsArray, + $getFloat32Array6, + $poolFloat32Array6 +} from "../../DisplayObjectUtil"; /** * @description TextFieldの描画範囲を計算します。 @@ -19,7 +23,27 @@ export const execute = (text_field: TextField, matrix: Float32Array | null = nul { const rawBounds = textFieldGetRawBoundsService(text_field); - const rawMatrix = displayObjectGetRawMatrixUseCase(text_field); + let rawMatrix = displayObjectGetRawMatrixUseCase(text_field); + + // cacheAsBitmap倍率をrawMatrixに適用 + const cacheMatrix = (text_field as any).cacheAsBitmap; + let scaledMatrix: Float32Array | null = null; + if (cacheMatrix) { + const m = cacheMatrix.rawData; + const csx = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + const csy = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + if (rawMatrix) { + scaledMatrix = $getFloat32Array6( + rawMatrix[0] * csx, rawMatrix[1] * csx, + rawMatrix[2] * csy, rawMatrix[3] * csy, + rawMatrix[4], rawMatrix[5] + ); + } else { + scaledMatrix = $getFloat32Array6(csx, 0, 0, csy, 0, 0); + } + rawMatrix = scaledMatrix; + } + if (!rawMatrix) { if (matrix) { const calcBounds = displayObjectCalcBoundsMatrixService( @@ -41,6 +65,9 @@ export const execute = (text_field: TextField, matrix: Float32Array | null = nul matrix ? Matrix.multiply(matrix, rawMatrix) : rawMatrix ); + if (scaledMatrix) { + $poolFloat32Array6(scaledMatrix); + } $poolBoundsArray(rawBounds); return calcBounds; diff --git a/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts b/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts index 31bacd25..3196a30c 100644 --- a/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts +++ b/packages/display/src/TextField/usecase/TextFieldGenerateRenderQueueUseCase.ts @@ -6,13 +6,16 @@ import { execute as displayObjectBlendToNumberService } from "../../DisplayObjec import { execute as displayObjectGenerateHashService } from "../../DisplayObject/service/DisplayObjectGenerateHashService"; import { $cacheStore } from "@next2d/cache"; import { renderQueue } from "@next2d/render-queue"; +import { stage } from "../../Stage"; import { $clamp, $RENDERER_TEXT_TYPE, $getArray, $poolArray, $poolBoundsArray, - $getBoundsArray + $getBoundsArray, + $getFloat32Array6, + $poolFloat32Array6 } from "../../DisplayObjectUtil"; import { ColorTransform, @@ -85,10 +88,10 @@ export const execute = ( tMatrix ); - const xMin = bounds[0]; - const yMin = bounds[1]; - const xMax = bounds[2]; - const yMax = bounds[3]; + let xMin = bounds[0]; + let yMin = bounds[1]; + let xMax = bounds[2]; + let yMax = bounds[3]; $poolBoundsArray(bounds); const width = Math.ceil(Math.abs(xMax - xMin)); @@ -148,20 +151,67 @@ export const execute = ( } } - const xScale = Math.sqrt( - tMatrix[0] * tMatrix[0] - + tMatrix[1] * tMatrix[1] - ); - - const yScale = Math.sqrt( - tMatrix[2] * tMatrix[2] - + tMatrix[3] * tMatrix[3] - ); + // cacheAsBitmap: 指定Matrix × 自身のスケール × stageのrendererScaleでキャッシュ品質を決定 + // 1.0基準: Matrix(1,0,0,1)はdisplayObjectの等倍スケールを意味する + const cacheMatrix = (text_field as any).cacheAsBitmap; + let renderXScale: number; + let renderYScale: number; + let cacheScaleX = 1; + let cacheScaleY = 1; + if (cacheMatrix) { + const m = cacheMatrix.rawData; + cacheScaleX = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + cacheScaleY = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + + const ownScaleX = rawMatrix + ? Math.sqrt(rawMatrix[0] * rawMatrix[0] + rawMatrix[1] * rawMatrix[1]) + : 1; + const ownScaleY = rawMatrix + ? Math.sqrt(rawMatrix[2] * rawMatrix[2] + rawMatrix[3] * rawMatrix[3]) + : 1; + + renderXScale = cacheScaleX * ownScaleX * stage.rendererScale; + renderYScale = cacheScaleY * ownScaleY * stage.rendererScale; + + // cacheMatrix倍率をスクリーン座標のboundsにも反映 + if (cacheScaleX !== 1 || cacheScaleY !== 1) { + const modMatrix = $getFloat32Array6( + tMatrix[0] * cacheScaleX, tMatrix[1] * cacheScaleX, + tMatrix[2] * cacheScaleY, tMatrix[3] * cacheScaleY, + tMatrix[4], tMatrix[5] + ); + const modBounds = displayObjectCalcBoundsMatrixService( + text_field.xMin, text_field.yMin, + text_field.xMax, text_field.yMax, + modMatrix + ); + xMin = modBounds[0]; + yMin = modBounds[1]; + xMax = modBounds[2]; + yMax = modBounds[3]; + $poolBoundsArray(modBounds); + $poolFloat32Array6(modMatrix); + } + } else { + renderXScale = Math.sqrt( + tMatrix[0] * tMatrix[0] + + tMatrix[1] * tMatrix[1] + ); + renderYScale = Math.sqrt( + tMatrix[2] * tMatrix[2] + + tMatrix[3] * tMatrix[3] + ); + } - const xScaleRounded = Math.round(xScale * 100) / 100; - const yScaleRounded = Math.round(yScale * 100) / 100; + const xScaleRounded = Math.round(renderXScale * 100) / 100; + const yScaleRounded = Math.round(renderYScale * 100) / 100; - if (text_field.changed + if (cacheMatrix && text_field.cacheKey + && text_field.cacheParams[0] === xScaleRounded + && text_field.cacheParams[1] === yScaleRounded + ) { + // cacheAsBitmap: スケール未変更のためキャッシュキーを維持 + } else if (text_field.changed && !text_field.cacheKey || text_field.cacheParams[0] !== xScaleRounded || text_field.cacheParams[1] !== yScaleRounded @@ -178,14 +228,16 @@ export const execute = ( // rennder on renderQueue.pushTextFieldBuffer( 1, $RENDERER_TEXT_TYPE, - tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], tMatrix[4], tMatrix[5], + tMatrix[0] * cacheScaleX, tMatrix[1] * cacheScaleX, + tMatrix[2] * cacheScaleY, tMatrix[3] * cacheScaleY, + tMatrix[4], tMatrix[5], tColorTransform[0], tColorTransform[1], tColorTransform[2], tColorTransform[3], tColorTransform[4], tColorTransform[5], tColorTransform[6], tColorTransform[7], xMin, yMin, xMax, yMax, text_field.xMin, text_field.yMin, text_field.xMax, text_field.yMax, - +text_field.uniqueKey, cacheKey, +text_field.changed, - xScale, yScale, + +text_field.uniqueKey, cacheKey, +text_field.changed | (cacheMatrix ? 2 : 0), + renderXScale, renderYScale, text_field.instanceId // フィルターキャッシュ用のユニークキー ); diff --git a/packages/display/src/TextField/usecase/TextFieldHitTestUseCase.ts b/packages/display/src/TextField/usecase/TextFieldHitTestUseCase.ts index 986be8ea..c6f51ab8 100644 --- a/packages/display/src/TextField/usecase/TextFieldHitTestUseCase.ts +++ b/packages/display/src/TextField/usecase/TextFieldHitTestUseCase.ts @@ -2,6 +2,10 @@ import type { IPlayerHitObject } from "../../interface/IPlayerHitObject"; import type { TextField } from "@next2d/text"; import { Matrix } from "@next2d/geom"; import { execute as displayObjectGetRawMatrixUseCase } from "../../DisplayObject/usecase/DisplayObjectGetRawMatrixUseCase"; +import { + $getFloat32Array6, + $poolFloat32Array6 +} from "../../DisplayObjectUtil"; /** * @description TextField のヒット判定 @@ -28,11 +32,35 @@ export const execute = ( return false; } - const rawMatrix = displayObjectGetRawMatrixUseCase(text_field); + let rawMatrix = displayObjectGetRawMatrixUseCase(text_field); + + // cacheAsBitmap倍率をrawMatrixに適用 + const cacheMatrix = (text_field as any).cacheAsBitmap; + let scaledMatrix: Float32Array | null = null; + if (cacheMatrix) { + const m = cacheMatrix.rawData; + const csx = Math.sqrt(m[0] * m[0] + m[1] * m[1]); + const csy = Math.sqrt(m[2] * m[2] + m[3] * m[3]); + if (rawMatrix) { + scaledMatrix = $getFloat32Array6( + rawMatrix[0] * csx, rawMatrix[1] * csx, + rawMatrix[2] * csy, rawMatrix[3] * csy, + rawMatrix[4], rawMatrix[5] + ); + } else { + scaledMatrix = $getFloat32Array6(csx, 0, 0, csy, 0, 0); + } + rawMatrix = scaledMatrix; + } + const tMatrix = rawMatrix ? Matrix.multiply(matrix, rawMatrix) : matrix; + if (scaledMatrix) { + $poolFloat32Array6(scaledMatrix); + } + hit_context.setTransform( tMatrix[0], tMatrix[1], tMatrix[2], tMatrix[3], tMatrix[4], tMatrix[5] diff --git a/packages/media/src/Sound/service/SoundDecodeService.test.ts b/packages/media/src/Sound/service/SoundDecodeService.test.ts new file mode 100644 index 00000000..251ad72d --- /dev/null +++ b/packages/media/src/Sound/service/SoundDecodeService.test.ts @@ -0,0 +1,26 @@ +import { execute } from "./SoundDecodeService"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../../MediaUtil", () => ({ + "$getAudioContext": vi.fn(() => ({ + "decodeAudioData": vi.fn(() => Promise.resolve({ "length": 100 })) + })) +})); + +describe("SoundDecodeService.js test", () => { + + it("execute test case1 - empty buffer returns undefined", async () => + { + const emptyBuffer = new ArrayBuffer(0); + const result = await execute(emptyBuffer); + expect(result).toBeUndefined(); + }); + + it("execute test case2 - successful decode", async () => + { + const buffer = new ArrayBuffer(10); + new Uint8Array(buffer).set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + const result = await execute(buffer); + expect(result).toEqual({ "length": 100 }); + }); +}); diff --git a/packages/media/src/Video/usecase/VideoBuildFromCharacterUseCase.test.ts b/packages/media/src/Video/usecase/VideoBuildFromCharacterUseCase.test.ts new file mode 100644 index 00000000..a472cded --- /dev/null +++ b/packages/media/src/Video/usecase/VideoBuildFromCharacterUseCase.test.ts @@ -0,0 +1,77 @@ +import { execute } from "./VideoBuildFromCharacterUseCase"; +import { describe, expect, it, vi } from "vitest"; + +describe("VideoBuildFromCharacterUseCase.js test", () => { + + it("execute test case1 - build video from character with buffer", () => + { + const mockVideo = { + "loop": false, + "autoPlay": false, + "videoWidth": 0, + "videoHeight": 0, + "volume": 0, + "src": "" + } as any; + + const character = { + "loop": true, + "autoPlay": true, + "bounds": { "xMax": 320, "yMax": 240 }, + "volume": 0.8, + "buffer": [0, 1, 2, 3], + "videoData": null + } as any; + + // Mock URL.createObjectURL + const originalCreateObjectURL = URL.createObjectURL; + URL.createObjectURL = vi.fn(() => "blob:mock-url"); + + execute(mockVideo, character); + + expect(mockVideo.loop).toBe(true); + expect(mockVideo.autoPlay).toBe(true); + expect(mockVideo.videoWidth).toBe(320); + expect(mockVideo.videoHeight).toBe(240); + expect(mockVideo.volume).toBe(0.8); + expect(mockVideo.src).toBe("blob:mock-url"); + expect(character.videoData).toBeInstanceOf(Uint8Array); + expect(character.buffer).toBeNull(); + + URL.createObjectURL = originalCreateObjectURL; + }); + + it("execute test case2 - reuse existing videoData", () => + { + const mockVideo = { + "loop": false, + "autoPlay": false, + "videoWidth": 0, + "videoHeight": 0, + "volume": 0, + "src": "" + } as any; + + const existingVideoData = new Uint8Array([10, 20, 30]); + const character = { + "loop": false, + "autoPlay": false, + "bounds": { "xMax": 640, "yMax": 480 }, + "volume": 1.0, + "buffer": [5, 6, 7], + "videoData": existingVideoData + } as any; + + const originalCreateObjectURL = URL.createObjectURL; + URL.createObjectURL = vi.fn(() => "blob:mock-url-2"); + + execute(mockVideo, character); + + expect(character.videoData).toBe(existingVideoData); + expect(character.buffer).not.toBeNull(); + expect(mockVideo.videoWidth).toBe(640); + expect(mockVideo.videoHeight).toBe(480); + + URL.createObjectURL = originalCreateObjectURL; + }); +}); diff --git a/packages/renderer/src/Command/service/CommandRemoveCacheService.test.ts b/packages/renderer/src/Command/service/CommandRemoveCacheService.test.ts new file mode 100644 index 00000000..877b6efd --- /dev/null +++ b/packages/renderer/src/Command/service/CommandRemoveCacheService.test.ts @@ -0,0 +1,70 @@ +import { execute } from "./CommandRemoveCacheService"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("@next2d/cache", () => { + const store = new Map>(); + return { + "$cacheStore": { + "has": vi.fn((key: string) => store.has(key)), + "getById": vi.fn((key: string) => store.get(key)), + "removeById": vi.fn((key: string) => store.delete(key)), + "_store": store + } + }; +}); + +vi.mock("../../RendererUtil", () => ({ + "$context": { + "removeNode": vi.fn() + } +})); + +describe("CommandRemoveCacheService.js test", () => { + + it("execute test case1 - remove existing cache entries", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + const { $context } = await import("../../RendererUtil"); + + const node1 = { "id": 1 }; + const node2 = { "id": 2 }; + const cache = new Map(); + cache.set("a", node1); + cache.set("b", node2); + + ($cacheStore as any)._store.set("1", cache); + + const keys = new Float32Array([1]); + execute(keys); + + expect($cacheStore.has).toHaveBeenCalledWith("1"); + expect($cacheStore.getById).toHaveBeenCalledWith("1"); + expect($context.removeNode).toHaveBeenCalledTimes(2); + expect($cacheStore.removeById).toHaveBeenCalledWith("1"); + }); + + it("execute test case2 - skip non-existing cache keys", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + + vi.mocked($cacheStore.has).mockReturnValue(false as any); + + const keys = new Float32Array([999]); + execute(keys); + + expect($cacheStore.has).toHaveBeenCalledWith("999"); + expect($cacheStore.getById).not.toHaveBeenCalledWith("999"); + }); + + it("execute test case3 - empty keys array", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + + vi.mocked($cacheStore.has).mockClear(); + + const keys = new Float32Array([]); + execute(keys); + + expect($cacheStore.has).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerClipRenderUseCase.test.ts b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerClipRenderUseCase.test.ts new file mode 100644 index 00000000..ca8f7aa8 --- /dev/null +++ b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerClipRenderUseCase.test.ts @@ -0,0 +1,77 @@ +import { execute } from "./DisplayObjectContainerClipRenderUseCase"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../../Shape/usecase/ShapeClipRenderUseCase", () => ({ + "execute": vi.fn((render_queue: Float32Array, index: number) => { + // Skip matrix(6) + isGridEnabled(1) + length(1) + commands(n) + index += 6; // matrix + const isGrid = Boolean(render_queue[index++]); + if (isGrid) { index += 24; } + const len = render_queue[index++]; + index += len; + return index; + }) +})); + +describe("DisplayObjectContainerClipRenderUseCase.js test", () => { + + it("execute test case1 - empty container", () => + { + const renderQueue = new Float32Array([0]); // length = 0 + const result = execute(renderQueue, 0); + expect(result).toBe(1); + }); + + it("execute test case2 - shape clip child", async () => + { + const shapeClipMod = await import("../../Shape/usecase/ShapeClipRenderUseCase"); + vi.mocked(shapeClipMod.execute).mockClear(); + + // length(1) + type(1) + shape data + const data: number[] = []; + data.push(1); // 1 child + data.push(0x01); // type = shape + // matrix (6) + data.push(1, 0, 0, 1, 0, 0); + // isGridEnabled + data.push(0); + // command length + data.push(2); + // commands + data.push(9, 12); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0); + + expect(shapeClipMod.execute).toHaveBeenCalledTimes(1); + expect(result).toBe(data.length); + }); + + it("execute test case3 - nested container clip", () => + { + // outer: length=1, type=container(0x00) + // inner: length=0 + const data: number[] = []; + data.push(1); // 1 child + data.push(0x00); // type = container + data.push(0); // inner length = 0 + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0); + + expect(result).toBe(data.length); + }); + + it("execute test case4 - unknown type is skipped", () => + { + const data: number[] = []; + data.push(1); // 1 child + data.push(0x02); // type = text (not handled in clip) + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0); + + // Should consume length + type but nothing else + expect(result).toBe(2); + }); +}); diff --git a/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.test.ts b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.test.ts new file mode 100644 index 00000000..5b75a1e9 --- /dev/null +++ b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.test.ts @@ -0,0 +1,265 @@ +import { execute } from "./DisplayObjectContainerRenderUseCase"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../../RendererUtil", () => ({ + "$context": { + "drawArraysInstanced": vi.fn(), + "save": vi.fn(), + "restore": vi.fn(), + "beginMask": vi.fn(), + "endMask": vi.fn(), + "leaveMask": vi.fn(), + "setMaskBounds": vi.fn(), + "containerBeginLayer": vi.fn(), + "containerEndLayer": vi.fn(), + "containerBeginAtlasNode": vi.fn(() => ({ "x": 0, "y": 0, "w": 100, "h": 80, "index": 0 })), + "containerEndAtlasNode": vi.fn(), + "containerDrawCachedFilter": vi.fn(), + "removeNode": vi.fn(), + "setTransform": vi.fn(), + "drawDisplayObject": vi.fn(), + "globalAlpha": 1, + "imageSmoothingEnabled": true, + "globalCompositeOperation": "normal" + } +})); + +vi.mock("@next2d/cache", () => ({ + "$cacheStore": { + "get": vi.fn(() => null), + "set": vi.fn() + } +})); + +vi.mock("../../Shape/usecase/ShapeRenderUseCase", () => ({ + "execute": vi.fn((_rq: Float32Array, index: number) => index) +})); + +vi.mock("../../Shape/usecase/ShapeClipRenderUseCase", () => ({ + "execute": vi.fn((_rq: Float32Array, index: number) => index) +})); + +vi.mock("../../TextField/usecase/TextFieldRenderUseCase", () => ({ + "execute": vi.fn((_rq: Float32Array, index: number) => index) +})); + +vi.mock("../../Video/usecase/VideoRenderUseCase", () => ({ + "execute": vi.fn((_rq: Float32Array, index: number) => index) +})); + +vi.mock("./DisplayObjectContainerClipRenderUseCase", () => ({ + "execute": vi.fn((_rq: Float32Array, index: number) => index) +})); + +vi.mock("../../DisplayObject/service/DisplayObjectGetBlendModeService", () => ({ + "execute": vi.fn(() => "normal") +})); + +describe("DisplayObjectContainerRenderUseCase.js test", () => { + + it("execute test case1 - simple container no layer, no mask, no children", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.containerBeginLayer).mockClear(); + vi.mocked($context.containerEndLayer).mockClear(); + + const data: number[] = []; + // blendMode + data.push(0); + // useLayer = false + data.push(0); + // useMaskDisplayObject = false + data.push(0); + // children length = 0 + data.push(0); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, null); + + expect($context.containerBeginLayer).not.toHaveBeenCalled(); + expect($context.containerEndLayer).not.toHaveBeenCalled(); + expect(result).toBe(data.length); + }); + + it("execute test case2 - container with useLayer and blend only", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.containerBeginLayer).mockClear(); + vi.mocked($context.containerEndLayer).mockClear(); + + const data: number[] = []; + // blendMode + data.push(0); + // useLayer = true + data.push(1); + // layerWidth, layerHeight + data.push(800, 600); + // useFilter = false + data.push(0); + // matrix (6) + data.push(1, 0, 0, 1, 0, 0); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // useMaskDisplayObject = false + data.push(0); + // children length = 0 + data.push(0); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, null); + + expect($context.containerBeginLayer).toHaveBeenCalledWith(800, 600); + expect($context.containerEndLayer).toHaveBeenCalledTimes(1); + expect(result).toBe(data.length); + }); + + it("execute test case3 - container with filter cache hit returns early", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.containerDrawCachedFilter).mockClear(); + + const data: number[] = []; + // blendMode + data.push(0); + // useLayer = true + data.push(1); + // layerWidth, layerHeight + data.push(800, 600); + // useFilter = true + data.push(1); + // filterCache = true + data.push(1); + // uniqueKey + data.push(42); + // filterKey + data.push(99); + // filterBounds (4) + data.push(-10, -10, 110, 110); + // matrix (6) + data.push(1, 0, 0, 1, 0, 0); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, null); + + expect($context.containerDrawCachedFilter).toHaveBeenCalledTimes(1); + expect(result).toBe(data.length); + }); + + it("execute test case4 - cacheAsBitmap cache hit returns early", async () => + { + const { $context } = await import("../../RendererUtil"); + const { $cacheStore } = await import("@next2d/cache"); + vi.mocked($context.containerBeginLayer).mockClear(); + vi.mocked($context.containerEndLayer).mockClear(); + vi.mocked($context.drawDisplayObject).mockClear(); + vi.mocked($context.setTransform).mockClear(); + + // キャッシュされたノードを返すようにモック + const mockNode = { "x": 0, "y": 0, "w": 100, "h": 80, "index": 0 }; + vi.mocked($cacheStore.get).mockReturnValueOnce(mockNode as any); + + const data: number[] = []; + // blendMode + data.push(0); + // useLayer = true + data.push(1); + // layerWidth, layerHeight + data.push(0, 0); + // layerType = 2 (cacheAsBitmap) + data.push(2); + // cacheHit = true + data.push(1); + // uniqueKey (instanceId) + data.push(10); + // cacheKey + data.push(500); + // filterBounds (4) + data.push(0, 0, 100, 80); + // renderScaleX, renderScaleY + data.push(1, 1); + // parent matrix a, b, c, d, screenX, screenY + data.push(1, 0, 0, 1, 30, 40); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, null); + + // cache hit → drawDisplayObject でアトラスから描画して即return + expect($context.drawDisplayObject).toHaveBeenCalledTimes(1); + // matrix/renderScale = 1/1 → setTransform(1, 0, 0, 1, 30, 40) + expect($context.setTransform).toHaveBeenCalledWith(1, 0, 0, 1, 30, 40); + // containerBeginLayer/EndLayerは呼ばれない(子要素の描画不要) + expect($context.containerBeginLayer).not.toHaveBeenCalled(); + expect($context.containerEndLayer).not.toHaveBeenCalled(); + expect(result).toBe(data.length); + }); + + it("execute test case5 - cacheAsBitmap cache miss processes children and caches", async () => + { + const { $context } = await import("../../RendererUtil"); + const { $cacheStore } = await import("@next2d/cache"); + vi.mocked($context.containerBeginLayer).mockClear(); + vi.mocked($context.containerEndLayer).mockClear(); + vi.mocked($context.containerBeginAtlasNode).mockClear(); + vi.mocked($context.containerEndAtlasNode).mockClear(); + vi.mocked($context.drawDisplayObject).mockClear(); + vi.mocked($context.setTransform).mockClear(); + vi.mocked($context.removeNode).mockClear(); + vi.mocked($cacheStore.get).mockReturnValue(null as any); + vi.mocked($cacheStore.set).mockClear(); + + const mockNode = { "x": 0, "y": 0, "w": 100, "h": 80, "index": 0 }; + vi.mocked($context.containerBeginAtlasNode).mockReturnValueOnce(mockNode as any); + + const data: number[] = []; + // blendMode + data.push(0); + // useLayer = true + data.push(1); + // layerWidth, layerHeight + data.push(100, 80); + // layerType = 2 (cacheAsBitmap) + data.push(2); + // cacheHit = false + data.push(0); + // uniqueKey (instanceId) + data.push(10); + // cacheKey + data.push(500); + // filterBounds (4) + data.push(0, 0, 100, 80); + // renderScaleX, renderScaleY + data.push(1, 1); + // parent matrix a, b, c, d, screenX, screenY + data.push(1, 0, 0, 1, 30, 40); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // paramsLength = 0 (no filter params) + data.push(0); + // useMaskDisplayObject = false + data.push(0); + // children length = 0 + data.push(0); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, null); + + // cache miss → containerBeginAtlasNode → 子要素描画 → containerEndAtlasNode + expect($context.containerBeginAtlasNode).toHaveBeenCalledWith(100, 80); + expect($context.containerEndAtlasNode).toHaveBeenCalledTimes(1); + // containerBeginLayer/containerEndLayer は呼ばれない(直接アトラス描画) + expect($context.containerBeginLayer).not.toHaveBeenCalled(); + expect($context.containerEndLayer).not.toHaveBeenCalled(); + // ノードをキャッシュに保存 + expect($cacheStore.set).toHaveBeenCalledWith("10", "bNode", mockNode); + expect($cacheStore.set).toHaveBeenCalledWith("10", "bKey", "500"); + // アトラスからインスタンス描画 + expect($context.drawDisplayObject).toHaveBeenCalledTimes(1); + // matrix/renderScale = 1/1 → setTransform(1, 0, 0, 1, 30, 40) + expect($context.setTransform).toHaveBeenCalledWith(1, 0, 0, 1, 30, 40); + expect(result).toBe(data.length); + }); +}); diff --git a/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.ts b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.ts index 04e29959..fa08b0c9 100644 --- a/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.ts +++ b/packages/renderer/src/DisplayObjectContainer/usecase/DisplayObjectContainerRenderUseCase.ts @@ -1,4 +1,6 @@ import { $context } from "../../RendererUtil"; +import { $cacheStore } from "@next2d/cache"; +import type { Node } from "@next2d/texture-packer"; import { execute as shapeRenderUseCase } from "../../Shape/usecase/ShapeRenderUseCase"; import { execute as shapeClipRenderUseCase } from "../../Shape/usecase/ShapeClipRenderUseCase"; import { execute as textFieldRenderUseCase } from "../../TextField/usecase/TextFieldRenderUseCase"; @@ -35,8 +37,11 @@ export const execute = ( let layerHeight = 0; let useFilter = false; + let useCacheAsBitmap = false; let uniqueKey = ""; let filterKey = ""; + let cacheRenderScaleX = 1; + let cacheRenderScaleY = 1; let filterBounds: Float32Array | null = null; let filterParams: Float32Array | null = null; let matrix: Float32Array | null = null; @@ -46,9 +51,57 @@ export const execute = ( layerWidth = render_queue[index++]; layerHeight = render_queue[index++]; - useFilter = Boolean(render_queue[index++]); + const layerType = render_queue[index++]; // 0=blend, 1=filter, 2=cacheAsBitmap + useFilter = layerType === 1; + useCacheAsBitmap = layerType === 2; - if (useFilter) { + if (useCacheAsBitmap) { + + // cacheAsBitmapパス + const cacheHit = Boolean(render_queue[index++]); + uniqueKey = `${render_queue[index++]}`; + filterKey = `${render_queue[index++]}`; + + // フィルター境界を読み取り + filterBounds = render_queue.subarray(index, index + 4); + index += 4; + + // renderScale(キャッシュ描画時のスケール) + cacheRenderScaleX = render_queue[index++]; + cacheRenderScaleY = render_queue[index++]; + + // 親matrix + スクリーン座標 (a, b, c, d, screenX, screenY) + matrix = render_queue.subarray(index, index + 6); + index += 6; + + colorTransform = render_queue.subarray(index, index + 8); + index += 8; + + if (cacheHit) { + // キャッシュ済み: Shapeと同様にmatrix/renderScaleで描画 + const cachedNode = $cacheStore.get(uniqueKey, "bNode") as Node; + if (cachedNode) { + $context.globalAlpha = Math.min(Math.max(0, colorTransform[3] + colorTransform[7] / 255), 1); + $context.globalCompositeOperation = blendMode; + $context.setTransform( + matrix[0] / cacheRenderScaleX, matrix[1] / cacheRenderScaleX, + matrix[2] / cacheRenderScaleY, matrix[3] / cacheRenderScaleY, + matrix[4], matrix[5] + ); + $context.drawDisplayObject( + cachedNode, + filterBounds[0], filterBounds[1], + filterBounds[2], filterBounds[3], + colorTransform + ); + } + return index; + } + + // 初回描画: フィルターパラメータをスキップ(アトラスパスでは不使用) + index += render_queue[index] + 1; + + } else if (useFilter) { // フィルターパス: filterCache/uniqueKey/filterKey を読む const filterCache = Boolean(render_queue[index++]); uniqueKey = `${render_queue[index++]}`; @@ -95,7 +148,11 @@ export const execute = ( } // コンテナのフィルター/ブレンド用にレイヤーを開始 - if (useLayer) { + let cacheNode: Node | null = null; + if (useCacheAsBitmap) { + // cacheAsBitmap: temp FBO作成→子要素描画→アトラスノードへコピー + cacheNode = $context.containerBeginAtlasNode(layerWidth, layerHeight); + } else if (useLayer) { $context.containerBeginLayer(layerWidth, layerHeight); } @@ -244,7 +301,33 @@ export const execute = ( } // コンテナのフィルター/ブレンド結果をメインに合成 - if (useLayer) { + if (cacheNode) { + // cacheAsBitmap: アトラス描画終了→キャッシュ→インスタンス描画 + $context.containerEndAtlasNode(); + + // 古いノードを解放 + const oldNode = $cacheStore.get(uniqueKey, "bNode") as Node; + if (oldNode) { + $context.removeNode(oldNode); + } + $cacheStore.set(uniqueKey, "bNode", cacheNode); + $cacheStore.set(uniqueKey, "bKey", filterKey); + + // アトラスからインスタンス描画(Shapeと同様にmatrix/renderScaleで描画) + $context.globalAlpha = Math.min(Math.max(0, colorTransform![3] + colorTransform![7] / 255), 1); + $context.globalCompositeOperation = blendMode; + $context.setTransform( + matrix![0] / cacheRenderScaleX, matrix![1] / cacheRenderScaleX, + matrix![2] / cacheRenderScaleY, matrix![3] / cacheRenderScaleY, + matrix![4], matrix![5] + ); + $context.drawDisplayObject( + cacheNode, + filterBounds![0], filterBounds![1], + filterBounds![2], filterBounds![3], + colorTransform! + ); + } else if (useLayer) { $context.containerEndLayer( blendMode, matrix!, colorTransform, useFilter, filterBounds, filterParams, diff --git a/packages/renderer/src/Shape/service/ShapeCommandService.test.ts b/packages/renderer/src/Shape/service/ShapeCommandService.test.ts new file mode 100644 index 00000000..4cab1305 --- /dev/null +++ b/packages/renderer/src/Shape/service/ShapeCommandService.test.ts @@ -0,0 +1,192 @@ +import { execute } from "./ShapeCommandService"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../../RendererUtil", () => ({ + "$context": { + "beginPath": vi.fn(), + "moveTo": vi.fn(), + "lineTo": vi.fn(), + "quadraticCurveTo": vi.fn(), + "bezierCurveTo": vi.fn(), + "arc": vi.fn(), + "closePath": vi.fn(), + "fillStyle": vi.fn(), + "strokeStyle": vi.fn(), + "fill": vi.fn(), + "stroke": vi.fn(), + "gradientFill": vi.fn(), + "gradientStroke": vi.fn(), + "bitmapFill": vi.fn(), + "bitmapStroke": vi.fn(), + "thickness": 0, + "caps": 0, + "joints": 0, + "miterLimit": 0 + }, + "$getArray": vi.fn(() => []) +})); + +describe("ShapeCommandService.js test", () => { + + it("execute test case1 - BEGIN_PATH command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.beginPath).mockClear(); + + const commands = new Float32Array([9]); // BEGIN_PATH = 9 + execute(commands); + + expect($context.beginPath).toHaveBeenCalledTimes(1); + }); + + it("execute test case2 - MOVE_TO command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.moveTo).mockClear(); + + const commands = new Float32Array([0, 10, 20]); // MOVE_TO = 0 + execute(commands); + + expect($context.moveTo).toHaveBeenCalledWith(10, 20); + }); + + it("execute test case3 - LINE_TO command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.lineTo).mockClear(); + + const commands = new Float32Array([2, 30, 40]); // LINE_TO = 2 + execute(commands); + + expect($context.lineTo).toHaveBeenCalledWith(30, 40); + }); + + it("execute test case4 - CURVE_TO command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.quadraticCurveTo).mockClear(); + + const commands = new Float32Array([1, 10, 20, 30, 40]); // CURVE_TO = 1 + execute(commands); + + expect($context.quadraticCurveTo).toHaveBeenCalledWith(10, 20, 30, 40); + }); + + it("execute test case5 - CUBIC command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.bezierCurveTo).mockClear(); + + const commands = new Float32Array([3, 1, 2, 3, 4, 5, 6]); // CUBIC = 3 + execute(commands); + + expect($context.bezierCurveTo).toHaveBeenCalledWith(1, 2, 3, 4, 5, 6); + }); + + it("execute test case6 - ARC command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.arc).mockClear(); + + const commands = new Float32Array([4, 50, 60, 25]); // ARC = 4 + execute(commands); + + expect($context.arc).toHaveBeenCalledWith(50, 60, 25); + }); + + it("execute test case7 - FILL_STYLE and END_FILL commands", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.fillStyle).mockClear(); + vi.mocked($context.fill).mockClear(); + + // FILL_STYLE(5) r g b a, END_FILL(7) + const commands = new Float32Array([5, 255, 128, 0, 255, 7]); + execute(commands); + + expect($context.fillStyle).toHaveBeenCalledWith(1, 128 / 255, 0, 1); + expect($context.fill).toHaveBeenCalledTimes(1); + }); + + it("execute test case8 - FILL_STYLE skipped in clip mode", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.fillStyle).mockClear(); + + const commands = new Float32Array([5, 255, 128, 0, 255]); + execute(commands, true); + + expect($context.fillStyle).not.toHaveBeenCalled(); + }); + + it("execute test case9 - STROKE_STYLE command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.strokeStyle).mockClear(); + + // STROKE_STYLE(6) thickness caps joints miterLimit r g b a + const commands = new Float32Array([6, 2, 1, 0, 10, 255, 0, 0, 255]); + execute(commands); + + expect($context.thickness).toBe(2); + expect($context.caps).toBe(1); + expect($context.joints).toBe(0); + expect($context.miterLimit).toBe(10); + expect($context.strokeStyle).toHaveBeenCalledWith(1, 0, 0, 1); + }); + + it("execute test case10 - STROKE_STYLE skipped in clip mode", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.strokeStyle).mockClear(); + + const commands = new Float32Array([6, 2, 1, 0, 10, 255, 0, 0, 255]); + execute(commands, true); + + expect($context.strokeStyle).not.toHaveBeenCalled(); + }); + + it("execute test case11 - END_STROKE command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.stroke).mockClear(); + + const commands = new Float32Array([8]); // END_STROKE = 8 + execute(commands); + + expect($context.stroke).toHaveBeenCalledTimes(1); + }); + + it("execute test case12 - END_STROKE skipped in clip mode", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.stroke).mockClear(); + + const commands = new Float32Array([8]); // END_STROKE = 8 + execute(commands, true); + + expect($context.stroke).not.toHaveBeenCalled(); + }); + + it("execute test case13 - CLOSE_PATH command", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.closePath).mockClear(); + + const commands = new Float32Array([12]); // CLOSE_PATH = 12 + execute(commands); + + expect($context.closePath).toHaveBeenCalledTimes(1); + }); + + it("execute test case14 - empty commands", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.beginPath).mockClear(); + + const commands = new Float32Array([]); + execute(commands); + + expect($context.beginPath).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/renderer/src/Shape/usecase/ShapeClipRenderUseCase.test.ts b/packages/renderer/src/Shape/usecase/ShapeClipRenderUseCase.test.ts new file mode 100644 index 00000000..9025a36c --- /dev/null +++ b/packages/renderer/src/Shape/usecase/ShapeClipRenderUseCase.test.ts @@ -0,0 +1,86 @@ +import { execute } from "./ShapeClipRenderUseCase"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("../../RendererUtil", () => ({ + "$context": { + "reset": vi.fn(), + "setTransform": vi.fn(), + "useGrid": vi.fn(), + "clip": vi.fn() + } +})); + +vi.mock("../service/ShapeCommandService", () => ({ + "execute": vi.fn() +})); + +describe("ShapeClipRenderUseCase.js test", () => { + + it("execute test case1 - basic clip without grid", async () => + { + const { $context } = await import("../../RendererUtil"); + const shapeCommandService = await import("../service/ShapeCommandService"); + + vi.mocked($context.reset).mockClear(); + vi.mocked($context.setTransform).mockClear(); + vi.mocked($context.useGrid).mockClear(); + vi.mocked($context.clip).mockClear(); + vi.mocked(shapeCommandService.execute).mockClear(); + + // matrix(6) + isGridEnabled(1) + length(1) + commands(3) + const renderQueue = new Float32Array([ + 1, 0, 0, 1, 10, 20, // matrix + 0, // isGridEnabled = false + 3, // command length + 9, 0, 5 // commands (BEGIN_PATH, MOVE_TO partial) + ]); + + const resultIndex = execute(renderQueue, 0); + + expect($context.reset).toHaveBeenCalledTimes(1); + expect($context.setTransform).toHaveBeenCalledWith(1, 0, 0, 1, 10, 20); + expect($context.useGrid).toHaveBeenCalledWith(null); + expect(shapeCommandService.execute).toHaveBeenCalledTimes(1); + expect($context.clip).toHaveBeenCalledTimes(1); + expect(resultIndex).toBe(11); + }); + + it("execute test case2 - clip with grid enabled", async () => + { + const { $context } = await import("../../RendererUtil"); + const shapeCommandService = await import("../service/ShapeCommandService"); + + vi.mocked($context.reset).mockClear(); + vi.mocked($context.setTransform).mockClear(); + vi.mocked($context.useGrid).mockClear(); + vi.mocked($context.clip).mockClear(); + vi.mocked(shapeCommandService.execute).mockClear(); + + // matrix(6) + isGridEnabled(1) + gridData(24) + length(1) + commands(2) + const data = new Float32Array(6 + 1 + 24 + 1 + 2); + let idx = 0; + // matrix + data[idx++] = 2; data[idx++] = 0; data[idx++] = 0; + data[idx++] = 2; data[idx++] = 5; data[idx++] = 10; + // isGridEnabled = true + data[idx++] = 1; + // grid data (24 values) + for (let i = 0; i < 24; i++) { + data[idx++] = i; + } + // command length + data[idx++] = 2; + // commands + data[idx++] = 9; data[idx++] = 12; + + const resultIndex = execute(data, 0); + + expect($context.setTransform).toHaveBeenCalledWith(2, 0, 0, 2, 5, 10); + expect($context.useGrid).toHaveBeenCalledTimes(1); + const gridArg = vi.mocked($context.useGrid).mock.calls[0][0]; + expect(gridArg).not.toBeNull(); + expect(gridArg!.length).toBe(28); + expect($context.clip).toHaveBeenCalledTimes(1); + expect(resultIndex).toBe(idx); + }); +}); diff --git a/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.test.ts b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.test.ts new file mode 100644 index 00000000..d9e8e232 --- /dev/null +++ b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.test.ts @@ -0,0 +1,127 @@ +import { execute } from "./ShapeRenderUseCase"; +import { describe, expect, it, vi } from "vitest"; + +const mockNode = { "x": 0, "y": 0, "w": 100, "h": 100 }; + +vi.mock("@next2d/cache", () => ({ + "$cacheStore": { + "get": vi.fn(() => mockNode), + "set": vi.fn(), + "has": vi.fn(() => false) + } +})); + +vi.mock("../../RendererUtil", () => ({ + "$context": { + "reset": vi.fn(), + "setTransform": vi.fn(), + "useGrid": vi.fn(), + "createNode": vi.fn(() => mockNode), + "beginNodeRendering": vi.fn(), + "endNodeRendering": vi.fn(), + "drawFill": vi.fn(), + "drawPixels": vi.fn(), + "drawDisplayObject": vi.fn(), + "drawArraysInstanced": vi.fn(), + "bind": vi.fn(), + "applyFilter": vi.fn(), + "currentAttachmentObject": null, + "atlasAttachmentObject": null, + "globalAlpha": 1, + "imageSmoothingEnabled": true, + "globalCompositeOperation": "normal" + } +})); + +vi.mock("../service/ShapeCommandService", () => ({ + "execute": vi.fn() +})); + +vi.mock("../../DisplayObject/service/DisplayObjectGetBlendModeService", () => ({ + "execute": vi.fn(() => "normal") +})); + +describe("ShapeRenderUseCase.js test", () => { + + it("execute test case1 - render with cache hit", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.drawDisplayObject).mockClear(); + vi.mocked($context.setTransform).mockClear(); + + // Build render queue for cached shape + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 50, 60); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 100, 100); + // baseBounds xMin, yMin, xMax, yMax + data.push(0, 0, 100, 100); + // isGridEnabled, isDrawable, isBitmap + data.push(0, 1, 0); + // uniqueKey, cacheKey + data.push(1, 0); + // xScale, yScale + data.push(1, 1); + // filterKey + data.push(1); + // hasCache = 1 (cached) + data.push(1); + // blendMode + data.push(0); + // useFilter = 0 + data.push(0); + + const renderQueue = new Float32Array(data); + const resultIndex = execute(renderQueue, 0); + + expect($context.drawDisplayObject).toHaveBeenCalledTimes(1); + expect(resultIndex).toBe(data.length); + }); + + it("execute test case2 - render with filter", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.applyFilter).mockClear(); + + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 50, 60); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 100, 100); + // baseBounds + data.push(0, 0, 100, 100); + // isGridEnabled, isDrawable, isBitmap + data.push(0, 1, 0); + // uniqueKey, cacheKey + data.push(2, 0); + // xScale, yScale + data.push(1, 1); + // filterKey + data.push(2); + // hasCache = 1 + data.push(1); + // blendMode + data.push(0); + // useFilter = 1 + data.push(1); + // updated + data.push(1); + // filterBounds (4) + data.push(-5, -5, 110, 110); + // filter params length + data.push(3); + // filter params + data.push(1, 2, 1); + + const renderQueue = new Float32Array(data); + const resultIndex = execute(renderQueue, 0); + + expect($context.applyFilter).toHaveBeenCalledTimes(1); + expect(resultIndex).toBe(data.length); + }); +}); diff --git a/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts index 923fd353..26a00d74 100644 --- a/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts +++ b/packages/renderer/src/Shape/usecase/ShapeRenderUseCase.ts @@ -33,7 +33,9 @@ export const execute = (render_queue: Float32Array, index: number): number => const isGridEnabled = Boolean(render_queue[index++]); const isDrawable = Boolean(render_queue[index++]); - const isBitmap = Boolean(render_queue[index++]); + const renderMode = render_queue[index++]; // 0=vector, 1=bitmap, 2=cacheAsBitmap + const isBitmap = renderMode === 1; + const isCacheAsBitmap = renderMode === 2; // cache uniqueKey const uniqueKey = `${render_queue[index++]}`; @@ -196,6 +198,24 @@ export const execute = (render_queue: Float32Array, index: number): number => matrix[4], matrix[5] ); + $context.drawDisplayObject( + node, + bounds[0], bounds[1], bounds[2], bounds[3], + colorTransform + ); + } else if (isCacheAsBitmap) { + + // cacheAsBitmap: Bitmapと同様の描画パスで、cacheScaleを補正 + // baseBounds原点(xMin,yMin)のスクリーン座標をtranslationに反映 + const screenX = matrix[0] * xMin + matrix[2] * yMin + matrix[4]; + const screenY = matrix[1] * xMin + matrix[3] * yMin + matrix[5]; + + $context.setTransform( + matrix[0] / xScale, matrix[1] / xScale, + matrix[2] / yScale, matrix[3] / yScale, + screenX, screenY + ); + $context.drawDisplayObject( node, bounds[0], bounds[1], bounds[2], bounds[3], diff --git a/packages/renderer/src/TextField/service/TextFiledGetAlignOffsetService.test.ts b/packages/renderer/src/TextField/service/TextFiledGetAlignOffsetService.test.ts new file mode 100644 index 00000000..44bdb89f --- /dev/null +++ b/packages/renderer/src/TextField/service/TextFiledGetAlignOffsetService.test.ts @@ -0,0 +1,205 @@ +import { execute } from "./TextFiledGetAlignOffsetService"; +import { describe, expect, it } from "vitest"; + +describe("TextFiledGetAlignOffsetService.js test", () => { + + it("execute test case1 - left align returns leftMargin", () => + { + const textData = { + "widthTable": [100] + } as any; + + const textObject = { + "line": 0, + "textFormat": { + "align": "left", + "leftMargin": 5, + "rightMargin": 0 + } + } as any; + + const textSetting = { + "wordWrap": true, + "rawWidth": 200, + "autoSize": "left" + } as any; + + const result = execute(textData, textObject, textSetting); + expect(result).toBe(5); + }); + + it("execute test case2 - center align", () => + { + const textData = { + "widthTable": [100] + } as any; + + const textObject = { + "line": 0, + "textFormat": { + "align": "center", + "leftMargin": 0, + "rightMargin": 0 + } + } as any; + + const textSetting = { + "wordWrap": true, + "rawWidth": 200, + "autoSize": "none" + } as any; + + const result = execute(textData, textObject, textSetting); + expect(result).toBe(Math.max(0, 200 / 2 - 0 - 0 - 100 / 2 - 2)); + }); + + it("execute test case3 - right align", () => + { + const textData = { + "widthTable": [100] + } as any; + + const textObject = { + "line": 0, + "textFormat": { + "align": "right", + "leftMargin": 0, + "rightMargin": 0 + } + } as any; + + const textSetting = { + "wordWrap": true, + "rawWidth": 200, + "autoSize": "none" + } as any; + + const result = execute(textData, textObject, textSetting); + expect(result).toBe(Math.max(0, 200 - 0 - 100 - 0 - 4)); + }); + + it("execute test case4 - autoSize center", () => + { + const textData = { + "widthTable": [80] + } as any; + + const textObject = { + "line": 0, + "textFormat": { + "align": "left", + "leftMargin": 0, + "rightMargin": 0 + } + } as any; + + const textSetting = { + "wordWrap": true, + "rawWidth": 200, + "autoSize": "center" + } as any; + + const result = execute(textData, textObject, textSetting); + expect(result).toBe(Math.max(0, 200 / 2 - 0 - 0 - 80 / 2 - 2)); + }); + + it("execute test case5 - autoSize right", () => + { + const textData = { + "widthTable": [80] + } as any; + + const textObject = { + "line": 0, + "textFormat": { + "align": "left", + "leftMargin": 0, + "rightMargin": 0 + } + } as any; + + const textSetting = { + "wordWrap": true, + "rawWidth": 200, + "autoSize": "right" + } as any; + + const result = execute(textData, textObject, textSetting); + expect(result).toBe(Math.max(0, 200 - 0 - 80 - 0 - 4)); + }); + + it("execute test case6 - lineWidth exceeds rawWidth without wordWrap", () => + { + const textData = { + "widthTable": [300] + } as any; + + const textObject = { + "line": 0, + "textFormat": { + "align": "center", + "leftMargin": 10, + "rightMargin": 5 + } + } as any; + + const textSetting = { + "wordWrap": false, + "rawWidth": 200, + "autoSize": "none" + } as any; + + const result = execute(textData, textObject, textSetting); + expect(result).toBe(10); + }); + + it("execute test case7 - missing line in widthTable defaults to 0", () => + { + const textData = { + "widthTable": [100] + } as any; + + const textObject = { + "line": 5, + "textFormat": { + "align": "left", + "leftMargin": 0, + "rightMargin": 0 + } + } as any; + + const textSetting = { + "wordWrap": true, + "rawWidth": 200, + "autoSize": "none" + } as any; + + const result = execute(textData, textObject, textSetting); + expect(result).toBe(0); + }); + + it("execute test case8 - margins with center align", () => + { + const textData = { + "widthTable": [100] + } as any; + + const textObject = { + "line": 0, + "textFormat": { + "align": "center", + "leftMargin": 10, + "rightMargin": 10 + } + } as any; + + const textSetting = { + "wordWrap": true, + "rawWidth": 300, + "autoSize": "none" + } as any; + + const result = execute(textData, textObject, textSetting); + expect(result).toBe(Math.max(0, 300 / 2 - 10 - 10 - 100 / 2 - 2)); + }); +}); diff --git a/packages/renderer/src/TextField/usecase/TextFieldDrawOffscreenCanvasUseCase.test.ts b/packages/renderer/src/TextField/usecase/TextFieldDrawOffscreenCanvasUseCase.test.ts new file mode 100644 index 00000000..7f6e7c04 --- /dev/null +++ b/packages/renderer/src/TextField/usecase/TextFieldDrawOffscreenCanvasUseCase.test.ts @@ -0,0 +1,199 @@ +import { execute } from "./TextFieldDrawOffscreenCanvasUseCase"; +import { describe, expect, it, vi, beforeAll } from "vitest"; + +vi.mock("../../RendererUtil", () => ({ + "$intToRGBA": vi.fn((color: number) => ({ + "R": (color >> 16) & 0xFF, + "G": (color >> 8) & 0xFF, + "B": color & 0xFF, + "A": 1 + })) +})); + +vi.mock("../service/TextFiledGetAlignOffsetService", () => ({ + "execute": vi.fn(() => 0) +})); + +vi.mock("../service/TextFieldGenerateFontStyleService", () => ({ + "execute": vi.fn(() => "12px Arial") +})); + +// OffscreenCanvas mock with full 2D context +beforeAll(() => { + const originalOffscreenCanvas = globalThis.OffscreenCanvas; + (globalThis as any).OffscreenCanvas = class MockOffscreenCanvas { + width: number; + height: number; + constructor(w: number, h: number) { + this.width = w; + this.height = h; + } + getContext() { + return { + "fillRect": vi.fn(), + "beginPath": vi.fn(), + "moveTo": vi.fn(), + "lineTo": vi.fn(), + "quadraticCurveTo": vi.fn(), + "closePath": vi.fn(), + "isPointInPath": vi.fn(() => false), + "fill": vi.fn(), + "stroke": vi.fn(), + "save": vi.fn(), + "restore": vi.fn(), + "clip": vi.fn(), + "setTransform": vi.fn(), + "fillText": vi.fn(), + "strokeText": vi.fn(), + "rect": vi.fn(), + "font": "", + "fillStyle": "", + "strokeStyle": "", + "lineWidth": 1 + }; + } + }; + + return () => { + (globalThis as any).OffscreenCanvas = originalOffscreenCanvas; + }; +}); + +describe("TextFieldDrawOffscreenCanvasUseCase.js test", () => { + + it("execute test case1 - returns canvas with null text_data", () => + { + const textSetting = { + "width": 200, + "height": 100, + "background": false, + "border": false, + "backgroundColor": 0xFFFFFF, + "borderColor": 0x000000, + "autoSize": "none", + "stopIndex": -1, + "scrollX": 0, + "scrollY": 0, + "textWidth": 200, + "textHeight": 100, + "rawWidth": 200, + "rawHeight": 100, + "focusIndex": -1, + "selectIndex": -1, + "focusVisible": false, + "thickness": 0, + "thicknessColor": 0, + "wordWrap": false, + "defaultColor": 0, + "defaultSize": 12 + } as any; + + const canvas = execute(null, textSetting, 1, 1); + expect(canvas).toBeInstanceOf(OffscreenCanvas); + expect(canvas.width).toBe(200); + expect(canvas.height).toBe(100); + }); + + it("execute test case2 - draws background and border", () => + { + const textSetting = { + "width": 300, + "height": 150, + "background": true, + "border": true, + "backgroundColor": 0xFF0000, + "borderColor": 0x00FF00, + "autoSize": "none", + "stopIndex": -1, + "scrollX": 0, + "scrollY": 0, + "textWidth": 300, + "textHeight": 150, + "rawWidth": 300, + "rawHeight": 150, + "focusIndex": -1, + "selectIndex": -1, + "focusVisible": false, + "thickness": 0, + "thicknessColor": 0, + "wordWrap": false, + "defaultColor": 0, + "defaultSize": 12 + } as any; + + const canvas = execute(null, textSetting, 1, 1); + expect(canvas).toBeInstanceOf(OffscreenCanvas); + }); + + it("execute test case3 - renders text objects", () => + { + const textData = { + "textTable": [ + { + "mode": "wrap", + "line": 0, + "w": 0, + "h": 14, + "y": 0, + "text": "", + "textFormat": { + "align": "left", + "leftMargin": 0, + "rightMargin": 0, + "color": 0x000000, + "underline": false + } + }, + { + "mode": "text", + "line": 0, + "w": 50, + "h": 14, + "y": 12, + "text": "Hello", + "textFormat": { + "align": "left", + "leftMargin": 0, + "rightMargin": 0, + "color": 0x000000, + "underline": false + } + } + ], + "lineTable": [], + "widthTable": [50], + "heightTable": [16], + "ascentTable": [12] + } as any; + + const textSetting = { + "width": 200, + "height": 100, + "background": false, + "border": false, + "backgroundColor": 0xFFFFFF, + "borderColor": 0x000000, + "autoSize": "none", + "stopIndex": -1, + "scrollX": 0, + "scrollY": 0, + "textWidth": 200, + "textHeight": 100, + "rawWidth": 200, + "rawHeight": 100, + "focusIndex": -1, + "selectIndex": -1, + "focusVisible": false, + "thickness": 0, + "thicknessColor": 0, + "wordWrap": false, + "defaultColor": 0, + "defaultSize": 12 + } as any; + + const canvas = execute(textData, textSetting, 1, 1); + expect(canvas).toBeInstanceOf(OffscreenCanvas); + expect(canvas.width).toBe(200); + expect(canvas.height).toBe(100); + }); +}); diff --git a/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.test.ts b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.test.ts new file mode 100644 index 00000000..4ff21fb6 --- /dev/null +++ b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.test.ts @@ -0,0 +1,154 @@ +import { execute } from "./TextFieldRenderUseCase"; +import { describe, expect, it, vi } from "vitest"; + +const mockNode = { "x": 0, "y": 0, "w": 100, "h": 50 }; + +vi.mock("@next2d/cache", () => ({ + "$cacheStore": { + "get": vi.fn(() => mockNode), + "set": vi.fn(), + "has": vi.fn(() => false) + } +})); + +vi.mock("../../RendererUtil", () => ({ + "$context": { + "reset": vi.fn(), + "setTransform": vi.fn(), + "createNode": vi.fn(() => mockNode), + "beginNodeRendering": vi.fn(), + "endNodeRendering": vi.fn(), + "drawElement": vi.fn(), + "drawDisplayObject": vi.fn(), + "bind": vi.fn(), + "applyFilter": vi.fn(), + "currentAttachmentObject": null, + "atlasAttachmentObject": null, + "globalAlpha": 1, + "imageSmoothingEnabled": true, + "globalCompositeOperation": "normal" + } +})); + +vi.mock("./TextFieldDrawOffscreenCanvasUseCase", () => ({ + "execute": vi.fn(() => new OffscreenCanvas(100, 50)) +})); + +vi.mock("../../DisplayObject/service/DisplayObjectGetBlendModeService", () => ({ + "execute": vi.fn(() => "normal") +})); + +describe("TextFieldRenderUseCase.js test", () => { + + it("execute test case1 - render with cache hit", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.drawDisplayObject).mockClear(); + + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 10, 20); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 100, 50); + // baseBounds (4) + data.push(0, 0, 100, 50); + // uniqueKey, cacheKey + data.push(1, 0); + // changed + data.push(0); + // xScale, yScale + data.push(1, 1); + // filterKey + data.push(1); + // hasCache = 1 + data.push(1); + // blendMode + data.push(0); + // useFilter = 0 + data.push(0); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0); + + expect($context.drawDisplayObject).toHaveBeenCalledTimes(1); + expect(result).toBe(data.length); + }); + + it("execute test case2 - render with filter", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.applyFilter).mockClear(); + + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 10, 20); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 100, 50); + // baseBounds (4) + data.push(0, 0, 100, 50); + // uniqueKey, cacheKey + data.push(5, 0); + // changed + data.push(1); + // xScale, yScale + data.push(1, 1); + // filterKey + data.push(5); + // hasCache = 1 + data.push(1); + // blendMode + data.push(0); + // useFilter = 1 + data.push(1); + // updated + data.push(1); + // filterBounds (4) + data.push(-5, -5, 110, 60); + // filter params length + data.push(3); + // filter params + data.push(1, 2, 1); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0); + + expect($context.applyFilter).toHaveBeenCalledTimes(1); + expect(result).toBe(data.length); + }); + + it("execute test case3 - cache miss returns early when no node", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + vi.mocked($cacheStore.get).mockReturnValueOnce(null as any); + + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 10, 20); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 100, 50); + // baseBounds (4) + data.push(0, 0, 100, 50); + // uniqueKey, cacheKey + data.push(99, 0); + // changed + data.push(0); + // xScale, yScale + data.push(1, 1); + // filterKey + data.push(99); + // hasCache = 1 + data.push(1); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0); + + // Should return early at hasCache position + 1 + expect(result).toBe(data.length); + }); +}); diff --git a/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts index 52cf396f..540be8f6 100644 --- a/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts +++ b/packages/renderer/src/TextField/usecase/TextFieldRenderUseCase.ts @@ -43,8 +43,10 @@ export const execute = (render_queue: Float32Array, index: number): number => const uniqueKey = `${render_queue[index++]}`; const cacheKey = render_queue[index++]; - // text state - const changed = Boolean(render_queue[index++]); + // text state (bit 0 = changed, bit 1 = cacheAsBitmap) + const changedFlag = render_queue[index++]; + const changed = Boolean(changedFlag & 1); + const isCacheAsBitmap = Boolean(changedFlag & 2); const xScale = render_queue[index++]; const yScale = render_queue[index++]; @@ -187,29 +189,46 @@ export const execute = (render_queue: Float32Array, index: number): number => $context.imageSmoothingEnabled = true; $context.globalCompositeOperation = displayObjectGetBlendModeService(blendMode); - const radianX = Math.atan2(matrix[1], matrix[0]); - const radianY = Math.atan2(-matrix[2], matrix[3]); - if (radianX || radianY) { + if (isCacheAsBitmap) { - const tx = xMin * xScale; - const ty = yMin * yScale; - - const cosX = Math.cos(radianX); - const sinX = Math.sin(radianX); - const cosY = Math.cos(radianY); - const sinY = Math.sin(radianY); + // cacheAsBitmap: Bitmapと同様の描画パスで、cacheScaleを補正 + // baseBounds原点(xMin,yMin)のスクリーン座標をtranslationに反映 + const screenX = matrix[0] * xMin + matrix[2] * yMin + matrix[4]; + const screenY = matrix[1] * xMin + matrix[3] * yMin + matrix[5]; $context.setTransform( - cosX, sinX, -sinY, cosY, - tx * cosX - ty * sinY + matrix[4], - tx * sinX + ty * cosY + matrix[5] + matrix[0] / xScale, matrix[1] / xScale, + matrix[2] / yScale, matrix[3] / yScale, + screenX, screenY ); } else { - $context.setTransform(1, 0, 0, 1, - bounds[0], bounds[1] - ); + const radianX = Math.atan2(matrix[1], matrix[0]); + const radianY = Math.atan2(-matrix[2], matrix[3]); + if (radianX || radianY) { + + const tx = xMin * xScale; + const ty = yMin * yScale; + + const cosX = Math.cos(radianX); + const sinX = Math.sin(radianX); + const cosY = Math.cos(radianY); + const sinY = Math.sin(radianY); + + $context.setTransform( + cosX, sinX, -sinY, cosY, + tx * cosX - ty * sinY + matrix[4], + tx * sinX + ty * cosY + matrix[5] + ); + + } else { + + $context.setTransform(1, 0, 0, 1, + bounds[0], bounds[1] + ); + + } } diff --git a/packages/renderer/src/Video/usecase/VideoRenderUseCase.test.ts b/packages/renderer/src/Video/usecase/VideoRenderUseCase.test.ts new file mode 100644 index 00000000..c90b2ad8 --- /dev/null +++ b/packages/renderer/src/Video/usecase/VideoRenderUseCase.test.ts @@ -0,0 +1,187 @@ +import { execute } from "./VideoRenderUseCase"; +import { describe, expect, it, vi } from "vitest"; + +const mockNode = { "x": 0, "y": 0, "w": 320, "h": 240 }; + +vi.mock("@next2d/cache", () => ({ + "$cacheStore": { + "get": vi.fn(() => mockNode), + "set": vi.fn(), + "has": vi.fn(() => false) + } +})); + +vi.mock("../../RendererUtil", () => ({ + "$context": { + "reset": vi.fn(), + "setTransform": vi.fn(), + "createNode": vi.fn(() => mockNode), + "beginNodeRendering": vi.fn(), + "endNodeRendering": vi.fn(), + "drawElement": vi.fn(), + "drawDisplayObject": vi.fn(), + "bind": vi.fn(), + "applyFilter": vi.fn(), + "currentAttachmentObject": null, + "atlasAttachmentObject": null, + "globalAlpha": 1, + "imageSmoothingEnabled": true, + "globalCompositeOperation": "normal" + } +})); + +vi.mock("../../DisplayObject/service/DisplayObjectGetBlendModeService", () => ({ + "execute": vi.fn(() => "normal") +})); + +describe("VideoRenderUseCase.js test", () => { + + it("execute test case1 - render with cache hit", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.drawDisplayObject).mockClear(); + vi.mocked($context.setTransform).mockClear(); + + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 50, 60); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 320, 240); + // baseBounds (4) + data.push(0, 0, 320, 240); + // uniqueKey + data.push(1); + // changed + data.push(0); + // filterKey + data.push(1); + // hasCache = 1 + data.push(1); + // blendMode + data.push(0); + // useFilter = 0 + data.push(0); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, null); + + expect($context.drawDisplayObject).toHaveBeenCalledTimes(1); + expect($context.setTransform).toHaveBeenCalledWith(1, 0, 0, 1, 50, 60); + expect(result).toBe(data.length); + }); + + it("execute test case2 - render with filter", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.applyFilter).mockClear(); + + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 50, 60); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 320, 240); + // baseBounds (4) + data.push(0, 0, 320, 240); + // uniqueKey + data.push(3); + // changed + data.push(1); + // filterKey + data.push(3); + // hasCache = 1 + data.push(1); + // blendMode + data.push(0); + // useFilter = 1 + data.push(1); + // updated + data.push(1); + // filterBounds (4) + data.push(-5, -5, 330, 250); + // filter params length + data.push(3); + // filter params + data.push(1, 2, 1); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, null); + + expect($context.applyFilter).toHaveBeenCalledTimes(1); + expect(result).toBe(data.length); + }); + + it("execute test case3 - no cache, with image bitmap", async () => + { + const { $context } = await import("../../RendererUtil"); + vi.mocked($context.drawElement).mockClear(); + vi.mocked($context.beginNodeRendering).mockClear(); + vi.mocked($context.endNodeRendering).mockClear(); + + const mockBitmap = { "width": 320, "height": 240 } as unknown as ImageBitmap; + + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 50, 60); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 320, 240); + // baseBounds (4) + data.push(0, 0, 320, 240); + // uniqueKey + data.push(10); + // changed + data.push(1); + // filterKey + data.push(10); + // hasCache = 0 + data.push(0); + // hasNode = 0 + data.push(0); + // blendMode + data.push(0); + // useFilter = 0 + data.push(0); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, [mockBitmap]); + + expect($context.beginNodeRendering).toHaveBeenCalledTimes(1); + expect($context.drawElement).toHaveBeenCalledWith(mockNode, mockBitmap, true); + expect($context.endNodeRendering).toHaveBeenCalledTimes(1); + expect(result).toBe(data.length); + }); + + it("execute test case4 - cache miss returns early when no node found", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + vi.mocked($cacheStore.get).mockReturnValueOnce(null as any); + + const data: number[] = []; + // matrix (6) + data.push(1, 0, 0, 1, 0, 0); + // colorTransform (8) + data.push(1, 1, 1, 1, 0, 0, 0, 0); + // bounds (4) + data.push(0, 0, 320, 240); + // baseBounds (4) + data.push(0, 0, 320, 240); + // uniqueKey + data.push(99); + // changed + data.push(0); + // filterKey + data.push(99); + // hasCache = 1 + data.push(1); + + const renderQueue = new Float32Array(data); + const result = execute(renderQueue, 0, null); + + expect(result).toBe(data.length); + }); +}); diff --git a/packages/text/package.json b/packages/text/package.json index 7a7e11fb..dee4c87c 100644 --- a/packages/text/package.json +++ b/packages/text/package.json @@ -23,9 +23,6 @@ "type": "git", "url": "git+https://github.com/Next2D/Player.git" }, - "dependencies": { - "htmlparser2": "^10.0.0" - }, "peerDependencies": { "@next2d/display": "file:../display", "@next2d/geom": "file:../geom", diff --git a/packages/text/src/TextParser/service/TextParserHtmlParserService.test.ts b/packages/text/src/TextParser/service/TextParserHtmlParserService.test.ts new file mode 100644 index 00000000..dc32b56a --- /dev/null +++ b/packages/text/src/TextParser/service/TextParserHtmlParserService.test.ts @@ -0,0 +1,160 @@ +import type { IHtmlElementNode, IHtmlTextNode } from "../../interface/IHtmlNode"; +import { execute } from "./TextParserHtmlParserService"; +import { describe, expect, it } from "vitest"; + +describe("TextParserHtmlParserService.ts test", () => +{ + it("empty string returns empty array", () => + { + const result = execute(""); + expect(result.length).toBe(0); + }); + + it("plain text without tags", () => + { + const result = execute("hello world"); + expect(result.length).toBe(1); + expect(result[0].type).toBe("text"); + expect((result[0] as IHtmlTextNode).value).toBe("hello world"); + }); + + it("single tag with text", () => + { + const result = execute("bold"); + expect(result.length).toBe(1); + + const el = result[0] as IHtmlElementNode; + expect(el.type).toBe("element"); + expect(el.tagName).toBe("B"); + expect(el.children.length).toBe(1); + expect((el.children[0] as IHtmlTextNode).value).toBe("bold"); + }); + + it("nested tags", () => + { + const result = execute("

text

"); + expect(result.length).toBe(1); + + const p = result[0] as IHtmlElementNode; + expect(p.tagName).toBe("P"); + expect(p.children.length).toBe(1); + + const b = p.children[0] as IHtmlElementNode; + expect(b.tagName).toBe("B"); + expect(b.children.length).toBe(1); + expect((b.children[0] as IHtmlTextNode).value).toBe("text"); + }); + + it("tag with attributes (double quotes)", () => + { + const result = execute('text'); + expect(result.length).toBe(1); + + const font = result[0] as IHtmlElementNode; + expect(font.tagName).toBe("FONT"); + expect(font.attributes.length).toBe(3); + expect(font.attributes[0]).toEqual({ name: "face", value: "Arial" }); + expect(font.attributes[1]).toEqual({ name: "size", value: "12" }); + expect(font.attributes[2]).toEqual({ name: "color", value: "#FF0000" }); + }); + + it("tag with attributes (single quotes)", () => + { + const result = execute("text"); + const font = result[0] as IHtmlElementNode; + expect(font.attributes[0]).toEqual({ name: "face", value: "Arial" }); + }); + + it("style attribute", () => + { + const result = execute('text'); + const span = result[0] as IHtmlElementNode; + expect(span.tagName).toBe("SPAN"); + expect(span.attributes.length).toBe(1); + expect(span.attributes[0].name).toBe("style"); + expect(span.attributes[0].value).toBe("font-size: 14px; color: red;"); + }); + + it("self-closing br tag", () => + { + const result = execute("a
b"); + expect(result.length).toBe(3); + expect((result[0] as IHtmlTextNode).value).toBe("a"); + expect((result[1] as IHtmlElementNode).tagName).toBe("BR"); + expect((result[1] as IHtmlElementNode).children.length).toBe(0); + expect((result[2] as IHtmlTextNode).value).toBe("b"); + }); + + it("self-closing br/ tag", () => + { + const result = execute("a
b"); + expect(result.length).toBe(3); + expect((result[1] as IHtmlElementNode).tagName).toBe("BR"); + expect((result[1] as IHtmlElementNode).children.length).toBe(0); + }); + + it("tag name is case-insensitive", () => + { + const result = execute("text"); + const el = result[0] as IHtmlElementNode; + expect(el.tagName).toBe("FONT"); + }); + + it("complex HTML matching existing test input", () => + { + const htmlText = `

+ + +
+えお順 +

`; + const cleaned = htmlText.trim().replace(/\r?\n/g, "").replace(/\t/g, ""); + const result = execute(cleaned); + + expect(result.length).toBe(1); + + const p = result[0] as IHtmlElementNode; + expect(p.tagName).toBe("P"); + + // children:
えお順 + const children = p.children; + + // + const u = children.find((n) => n.type === "element" && n.tagName === "U") as IHtmlElementNode; + expect(u).toBeDefined(); + expect((u.children[0] as IHtmlTextNode).value).toBe("あ"); + + // + const b = children.find((n) => n.type === "element" && n.tagName === "B") as IHtmlElementNode; + expect(b).toBeDefined(); + expect((b.children[0] as IHtmlTextNode).value).toBe("い"); + + // + const i = children.find((n) => n.type === "element" && n.tagName === "I") as IHtmlElementNode; + expect(i).toBeDefined(); + expect((i.children[0] as IHtmlTextNode).value).toBe("う"); + + //
+ const br = children.find((n) => n.type === "element" && n.tagName === "BR") as IHtmlElementNode; + expect(br).toBeDefined(); + expect(br.children.length).toBe(0); + }); + + it("div tag with align attribute", () => + { + const result = execute('
text
'); + const div = result[0] as IHtmlElementNode; + expect(div.tagName).toBe("DIV"); + expect(div.attributes[0]).toEqual({ name: "align", value: "center" }); + expect((div.children[0] as IHtmlTextNode).value).toBe("text"); + }); + + it("multiple sibling tags", () => + { + const result = execute("abc"); + expect(result.length).toBe(3); + expect((result[0] as IHtmlElementNode).tagName).toBe("U"); + expect((result[1] as IHtmlElementNode).tagName).toBe("B"); + expect((result[2] as IHtmlElementNode).tagName).toBe("I"); + }); +}); diff --git a/packages/text/src/TextParser/service/TextParserHtmlParserService.ts b/packages/text/src/TextParser/service/TextParserHtmlParserService.ts new file mode 100644 index 00000000..74334d96 --- /dev/null +++ b/packages/text/src/TextParser/service/TextParserHtmlParserService.ts @@ -0,0 +1,371 @@ +import type { IAttributeObject } from "../../interface/IAttributeObject"; +import type { IHtmlNode } from "../../interface/IHtmlNode"; + +/** + * @description モジュールレベルのパーサ状態(クロージャ生成を回避) + * Module-level parser state (avoids closure allocation) + * + * @type {string} + * @private + */ +let _$html: string = ""; + +/** + * @description 現在の解析位置 + * Current parse position + * + * @type {number} + * @private + */ +let _$pos: number = 0; + +/** + * @description HTML文字列の長さ(キャッシュ) + * Cached length of the HTML string + * + * @type {number} + * @private + */ +let _$len: number = 0; + +/** + * @description void要素(BR等)の空children共有インスタンス + * 再利用によりアロケーションを回避する + * Shared empty children array for void elements (e.g. BR) + * Reused across calls to avoid allocation + * + * @type {IHtmlNode[]} + * @const + * @private + */ +const $EMPTY_CHILDREN: IHtmlNode[] = []; + +/** + * @description 軽量HTMLパーサ — TextField用の限定HTMLサブセットを1-passで解析 + * 対応タグ: B, I, U, P, BR, DIV, FONT, SPAN + * 対応属性: align, face, size, color, style, letterSpacing, + * leading, leftMargin, rightMargin, underline, bold, italic + * Lightweight HTML parser — single-pass parse for TextField HTML subset + * + * @param {string} html - 解析対象のHTML文字列 / HTML string to parse + * @return {IHtmlNode[]} 解析結果のノード配列 / Array of parsed nodes + * @method + * @protected + */ +export const execute = (html: string): IHtmlNode[] => +{ + _$html = html; + _$pos = 0; + _$len = html.length; + + const result = $parseChildren(""); + + _$html = ""; + + return result; +}; + +/** + * @description 子ノードを再帰的に解析する + * parentTagに一致する閉じタグを検出すると、そのスコープの解析を終了して返却する + * Recursively parse child nodes. + * Returns when a closing tag matching parent_tag is found. + * + * @param {string} parent_tag - 親要素のタグ名(大文字)。ルートの場合は空文字列 + * Parent element tag name (uppercase). Empty string for root. + * @return {IHtmlNode[]} 解析された子ノード配列 / Array of parsed child nodes + * @private + */ +const $parseChildren = (parent_tag: string): IHtmlNode[] => +{ + const nodes: IHtmlNode[] = []; + + while (_$pos < _$len) { + + const ltIdx = _$html.indexOf("<", _$pos); + + if (ltIdx === -1) { + if (_$pos < _$len) { + nodes[nodes.length] = { + "type": "text", + "value": _$html.substring(_$pos) + }; + _$pos = _$len; + } + break; + } + + if (ltIdx > _$pos) { + nodes[nodes.length] = { + "type": "text", + "value": _$html.substring(_$pos, ltIdx) + }; + } + + _$pos = ltIdx + 1; + + if (_$pos >= _$len) { + break; + } + + // closing tag: '", _$pos); + if (gtIdx === -1) { + _$pos = _$len; + break; + } + + if (parent_tag && $matchesUpperCase(_$pos, gtIdx, parent_tag)) { + _$pos = gtIdx + 1; + return nodes; + } + + _$pos = gtIdx + 1; + continue; + } + + const tagStart = _$pos; + while (_$pos < _$len) { + const c = _$html.charCodeAt(_$pos); + if (c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) { + break; + } + _$pos++; + } + const tagName = $toUpperCase(tagStart, _$pos); + + const attributes = $parseAttributes(); + + // self-closing '/>' + let selfClosing = false; + if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x2F) { + selfClosing = true; + _$pos++; + } + + if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x3E) { + _$pos++; + } + + if (selfClosing || tagName === "BR") { + nodes[nodes.length] = { + "type": "element", + "tagName": tagName, + "attributes": attributes, + "children": $EMPTY_CHILDREN + }; + } else { + nodes[nodes.length] = { + "type": "element", + "tagName": tagName, + "attributes": attributes, + "children": $parseChildren(tagName) + }; + } + } + + return nodes; +}; + +/** + * @description 現在位置から開始タグの属性を解析して IAttributeObject[] として返す + * 属性形式: name="value", name='value', name=value, name (boolean) + * '>' または '/' に到達した時点で解析を終了する + * Parse attributes from current position and return as IAttributeObject[]. + * Supports: name="value", name='value', name=value, name (boolean). + * Stops when '>' or '/' is encountered. + * + * @return {IAttributeObject[]} 解析された属性オブジェクトの配列 / Array of parsed attribute objects + * @private + */ +const $parseAttributes = (): IAttributeObject[] => +{ + const attrs: IAttributeObject[] = []; + + while (_$pos < _$len) { + + while (_$pos < _$len) { + const c = _$html.charCodeAt(_$pos); + if (c !== 0x20 && c !== 0x09) { + break; + } + _$pos++; + } + + if (_$pos >= _$len) { + break; + } + + const ch = _$html.charCodeAt(_$pos); + + // '>' or '/' + if (ch === 0x3E || ch === 0x2F) { + break; + } + + const nameStart = _$pos; + while (_$pos < _$len) { + const c = _$html.charCodeAt(_$pos); + if (c === 0x3D || c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) { + break; + } + _$pos++; + } + + if (_$pos === nameStart) { + break; + } + + const name = _$html.substring(nameStart, _$pos); + + while (_$pos < _$len) { + const c = _$html.charCodeAt(_$pos); + if (c !== 0x20 && c !== 0x09) { + break; + } + _$pos++; + } + + if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x3D) { + _$pos++; + + while (_$pos < _$len) { + const c = _$html.charCodeAt(_$pos); + if (c !== 0x20 && c !== 0x09) { + break; + } + _$pos++; + } + + let value: string; + const quote = _$html.charCodeAt(_$pos); + + if (quote === 0x22 || quote === 0x27) { + _$pos++; + const closeIdx = _$html.indexOf( + quote === 0x22 ? "\"" : "'", _$pos + ); + if (closeIdx === -1) { + value = _$html.substring(_$pos); + _$pos = _$len; + } else { + value = _$html.substring(_$pos, closeIdx); + _$pos = closeIdx + 1; + } + } else { + // unquoted value + const valStart = _$pos; + while (_$pos < _$len) { + const c = _$html.charCodeAt(_$pos); + if (c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) { + break; + } + _$pos++; + } + value = _$html.substring(valStart, _$pos); + } + + attrs[attrs.length] = { "name": name, "value": value }; + } else { + attrs[attrs.length] = { "name": name, "value": true }; + } + } + + return attrs; +}; + +/** + * @description 閉じタグ名を文字列生成せずにparentTag(大文字)と照合する + * _$html[start..end) の範囲を1文字ずつ大文字変換しながら比較する + * ゼロアロケーションで一致判定を行い、GC圧を回避する + * Compare closing tag name against parent_tag (uppercase) without string allocation. + * Converts each character to uppercase via charCode and compares in-place. + * Zero-allocation comparison to avoid GC pressure. + * + * @param {number} start - 照合開始位置('' の位置)/ End index (position of '>') + * @param {string} tag - 比較対象の親タグ名(大文字)/ Parent tag name to match (uppercase) + * @return {boolean} 一致する場合true / True if the closing tag matches + * @private + */ +const $matchesUpperCase = ( + start: number, + end: number, + tag: string +): boolean => +{ + while (start < end) { + const c = _$html.charCodeAt(start); + if (c !== 0x20 && c !== 0x09) { + break; + } + start++; + } + + while (end > start) { + const c = _$html.charCodeAt(end - 1); + if (c !== 0x20 && c !== 0x09) { + break; + } + end--; + } + + const tagLen = tag.length; + if (end - start !== tagLen) { + return false; + } + + for (let i = 0; i < tagLen; i++) { + let c = _$html.charCodeAt(start + i); + if (c >= 0x61 && c <= 0x7A) { + c -= 0x20; + } + if (c !== tag.charCodeAt(i)) { + return false; + } + } + + return true; +}; + +/** + * @description _$html[start..end) の部分文字列を大文字化して返す + * 1-2文字はString.fromCharCodeで直接生成(短いタグ名の最速パス) + * 3文字以上はsubstring().toUpperCase()にフォールバック + * Convert _$html[start..end) substring to uppercase. + * 1-2 char tags use String.fromCharCode (fastest path for short tag names). + * 3+ chars fall back to substring().toUpperCase(). + * + * @param {number} start - 開始位置 / Start index + * @param {number} end - 終了位置 / End index (exclusive) + * @return {string} 大文字化されたタグ名 / Uppercased tag name + * @private + */ +const $toUpperCase = (start: number, end: number): string => +{ + const length = end - start; + + // 1文字タグ (B, I, U, P) — 最頻出パスを最速処理 + if (length === 1) { + let c = _$html.charCodeAt(start); + if (c >= 0x61 && c <= 0x7A) { + c -= 0x20; + } + return String.fromCharCode(c); + } + + // 2文字タグ (BR, DIV先頭判定高速化) + if (length === 2) { + let c0 = _$html.charCodeAt(start); + let c1 = _$html.charCodeAt(start + 1); + if (c0 >= 0x61 && c0 <= 0x7A) { c0 -= 0x20 } + if (c1 >= 0x61 && c1 <= 0x7A) { c1 -= 0x20 } + return String.fromCharCode(c0, c1); + } + + // 3-4文字タグ (DIV, FONT, SPAN) + return _$html.substring(start, end).toUpperCase(); +}; diff --git a/packages/text/src/TextParser/usecase/TextParserParseHtmlTextUseCase.ts b/packages/text/src/TextParser/usecase/TextParserParseHtmlTextUseCase.ts index cec8df3b..1906071a 100644 --- a/packages/text/src/TextParser/usecase/TextParserParseHtmlTextUseCase.ts +++ b/packages/text/src/TextParser/usecase/TextParserParseHtmlTextUseCase.ts @@ -1,7 +1,6 @@ import type { TextFormat } from "../../TextFormat"; import type { IOptions } from "../../interface/IOptions"; import { TextData } from "../../TextData"; -import { parseDocument } from "htmlparser2"; import { execute as textParserCreateNewLineUseCase } from "./TextParserCreateNewLineUseCase"; import { execute as textParserAdjustmentHeightService } from "../service/TextParserAdjustmentHeightService"; import { execute as textParserParseTagUseCase } from "./TextParserParseTagUseCase"; @@ -44,7 +43,7 @@ export const execute = ( textParserCreateNewLineUseCase(textData, textFormat); textParserParseTagUseCase( - parseDocument(htmlText), textFormat, textData, options + htmlText, textFormat, textData, options ); textParserAdjustmentHeightService(textData); diff --git a/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.test.ts b/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.test.ts index 760d50f2..b37afda6 100644 --- a/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.test.ts +++ b/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.test.ts @@ -1,7 +1,6 @@ import { execute } from "./TextParserParseTagUseCase"; import { TextData } from "../../TextData"; import { TextFormat } from "../../TextFormat"; -import { parseDocument } from "htmlparser2"; import { describe, expect, it } from "vitest"; describe("TextParserParseTagUseCase.js test", () => @@ -33,7 +32,7 @@ describe("TextParserParseTagUseCase.js test", () => えお順

`; - execute(parseDocument(htmlText.trim().replace(/\r?\n/g, "").replace(/\t/g, "")), textFormat, textData, { + execute(htmlText.trim().replace(/\r?\n/g, "").replace(/\t/g, ""), textFormat, textData, { "width": 200, "multiline": true, "wordWrap": true, diff --git a/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.ts b/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.ts index 98f71c0a..81c863fe 100644 --- a/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.ts +++ b/packages/text/src/TextParser/usecase/TextParserParseTagUseCase.ts @@ -1,16 +1,41 @@ import type { TextFormat } from "../../TextFormat"; import type { TextData } from "../../TextData"; import type { IOptions } from "../../interface/IOptions"; -import type { Document, Element, ChildNode } from "domhandler"; +import type { IAttributeObject } from "../../interface/IAttributeObject"; +import type { ITextFormatAlign } from "../../interface/ITextFormatAlign"; import { execute as textParserParseTextUseCase } from "./TextParserParseTextUseCase"; import { execute as textParserCreateNewLineUseCase } from "./TextParserCreateNewLineUseCase"; -import { execute as textParserSetAttributesUseCase } from "./TextParserSetAttributesUseCase"; +import { execute as textParserParseStyleService } from "../service/TextParserParseStyleService"; +import { $toColorInt } from "../../TextUtil"; /** - * @description タグを解析してTextDataとTextFormatを設定 - * Analyze tags and set TextData and TextFormat + * @description 数値タグID — 文字列生成を完全排除 + * Numeric tag IDs — eliminates all tag string allocation + */ +const $TAG_NONE: number = 0; +const $TAG_B: number = 1; +const $TAG_I: number = 2; +const $TAG_U: number = 3; +const $TAG_P: number = 4; +const $TAG_BR: number = 5; +const $TAG_DIV: number = 6; +const $TAG_FONT: number = 7; +const $TAG_SPAN: number = 8; + +/** + * @description モジュールレベルのパーサ状態 + * Module-level parser state + */ +let _$html: string = ""; +let _$pos: number = 0; +let _$len: number = 0; + +/** + * @description HTMLタグを直接解析してTextDataを構築 + * 中間ツリーなし、タグ名文字列なし、clone()なし、属性オブジェクトなし + * Parse HTML directly — no tree, no tag strings, no clone, no attr objects * - * @param {Document} document + * @param {string} html * @param {TextFormat} text_format * @param {TextData} text_data * @param {IOptions} options @@ -19,68 +44,550 @@ import { execute as textParserSetAttributesUseCase } from "./TextParserSetAttrib * @protected */ export const execute = ( - document: Document, + html: string, + text_format: TextFormat, + text_data: TextData, + options: IOptions +): void => { + + _$html = html; + _$pos = 0; + _$len = html.length; + + $processChildren($TAG_NONE, text_format, text_data, options); + + _$html = ""; +}; + +/** + * @description 子要素をストリーミング解析・処理(再帰) + * save/restoreでTextFormatをスタック管理 — clone()ゼロ + * Stream-parse children with stack-based format — zero clone() + */ +const $processChildren = ( + parent_tag_id: number, text_format: TextFormat, text_data: TextData, options: IOptions ): void => { - for (let idx = 0; idx < document.children.length; ++idx) { + while (_$pos < _$len) { - const node = document.children[idx] as ChildNode; + const ltIdx: number = _$html.indexOf("<", _$pos); + + if (ltIdx === -1) { + textParserParseTextUseCase( + _$html.substring(_$pos), text_format, text_data, options + ); + _$pos = _$len; + break; + } - if (node.nodeType === 3) { + if (ltIdx > _$pos) { + textParserParseTextUseCase( + _$html.substring(_$pos, ltIdx), text_format, text_data, options + ); + } - textParserParseTextUseCase(node.nodeValue || "", text_format, text_data, options); + _$pos = ltIdx + 1; + if (_$pos >= _$len) { + break; + } + // closing tag: '", _$pos); + if (gtIdx === -1) { + _$pos = _$len; + break; + } + if (parent_tag_id !== $TAG_NONE && $identifyTag(_$pos, gtIdx) === parent_tag_id) { + _$pos = gtIdx + 1; + return; + } + _$pos = gtIdx + 1; continue; + } + const tagStart: number = _$pos; + while (_$pos < _$len) { + const c: number = _$html.charCodeAt(_$pos); + if (c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) { + break; + } + _$pos++; } + const tagId: number = $identifyTag(tagStart, _$pos); - const textFormat = text_format.clone(); - switch ((node as Element).name.toUpperCase()) { + switch (tagId) { - case "DIV": // div tag - case "P": // p tag - textParserSetAttributesUseCase((node as Element).attributes, textFormat, options); + case $TAG_B: { + const saved: boolean | null = text_format.bold; + text_format.bold = true; + $skipToClose(); + $processChildren(tagId, text_format, text_data, options); + text_format.bold = saved; + continue; + } - if (options.multiline) { - textParserCreateNewLineUseCase(text_data, textFormat); + case $TAG_I: { + const saved: boolean | null = text_format.italic; + text_format.italic = true; + $skipToClose(); + $processChildren(tagId, text_format, text_data, options); + text_format.italic = saved; + continue; + } + + case $TAG_U: { + const saved: boolean | null = text_format.underline; + text_format.underline = true; + $skipToClose(); + $processChildren(tagId, text_format, text_data, options); + text_format.underline = saved; + continue; + } + + case $TAG_DIV: + case $TAG_P: + case $TAG_FONT: + case $TAG_SPAN: { + const sFont: string | null = text_format.font; + const sSize: number | null = text_format.size; + const sColor: number | null = text_format.color; + const sBold: boolean | null = text_format.bold; + const sItalic: boolean | null = text_format.italic; + const sUnderline: boolean | null = text_format.underline; + const sAlign: ITextFormatAlign | null = text_format.align; + const sLeftMargin: number | null = text_format.leftMargin; + const sRightMargin: number | null = text_format.rightMargin; + const sLeading: number | null = text_format.leading; + const sLetterSpacing: number | null = text_format.letterSpacing; + + $applyAttributesInline(text_format, options); + $finishOpenTag(); + + if ((tagId === $TAG_P || tagId === $TAG_DIV) && options.multiline) { + textParserCreateNewLineUseCase(text_data, text_format); } - execute(node as Document, textFormat, text_data, options); + $processChildren(tagId, text_format, text_data, options); + + text_format.font = sFont; + text_format.size = sSize; + text_format.color = sColor; + text_format.bold = sBold; + text_format.italic = sItalic; + text_format.underline = sUnderline; + text_format.align = sAlign; + text_format.leftMargin = sLeftMargin; + text_format.rightMargin = sRightMargin; + text_format.leading = sLeading; + text_format.letterSpacing = sLetterSpacing; + continue; + } + + case $TAG_BR: + $skipToClose(); + if (options.multiline) { + textParserCreateNewLineUseCase(text_data, text_format); + } + continue; + default: + $skipToClose(); continue; - case "U": // underline - textFormat.underline = true; + } + } +}; + +/** + * @description '>' までスキップ(V8 SIMD最適化のindexOf使用) + * Skip to '>' using SIMD-optimized indexOf + */ +const $skipToClose = (): void => +{ + const idx: number = _$html.indexOf(">", _$pos); + _$pos = idx === -1 ? _$len : idx + 1; +}; + +/** + * @description 属性パース後のタグ末尾処理 + * Handle end of opening tag after attribute parsing + */ +const $finishOpenTag = (): void => +{ + if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x2F) { + _$pos++; + } + if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x3E) { + _$pos++; + } +}; + +/** + * @description 属性をインラインで解析・直接適用 + * IAttributeObject配列・オブジェクト・属性名文字列を一切生成しない + * Parse and apply attributes inline — zero arrays, objects, name strings + */ +const $applyAttributesInline = ( + text_format: TextFormat, + options: IOptions +): void => { + + for (;;) { + + while (_$pos < _$len) { + const c: number = _$html.charCodeAt(_$pos); + if (c !== 0x20 && c !== 0x09) { break; + } + _$pos++; + } + + if (_$pos >= _$len) { + break; + } - case "B": // bold - textFormat.bold = true; + const ch: number = _$html.charCodeAt(_$pos); + if (ch === 0x3E || ch === 0x2F) { + break; + } + + const nameStart: number = _$pos; + while (_$pos < _$len) { + const c: number = _$html.charCodeAt(_$pos); + if (c === 0x3D || c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) { break; + } + _$pos++; + } + + if (_$pos === nameStart) { + break; + } + + const name_len: number = _$pos - nameStart; + const name_c0: number = _$html.charCodeAt(nameStart); - case "I": // italic - textFormat.italic = true; + while (_$pos < _$len) { + const c: number = _$html.charCodeAt(_$pos); + if (c !== 0x20 && c !== 0x09) { break; + } + _$pos++; + } + + if (_$pos < _$len && _$html.charCodeAt(_$pos) === 0x3D) { + _$pos++; + + while (_$pos < _$len) { + const c: number = _$html.charCodeAt(_$pos); + if (c !== 0x20 && c !== 0x09) { + break; + } + _$pos++; + } + + let value: string; + const quote: number = _$html.charCodeAt(_$pos); + + if (quote === 0x22 || quote === 0x27) { + _$pos++; + const closeIdx: number = _$html.indexOf( + quote === 0x22 ? "\"" : "'", _$pos + ); + if (closeIdx === -1) { + value = _$html.substring(_$pos); + _$pos = _$len; + } else { + value = _$html.substring(_$pos, closeIdx); + _$pos = closeIdx + 1; + } + } else { + const valStart: number = _$pos; + while (_$pos < _$len) { + const c: number = _$html.charCodeAt(_$pos); + if (c === 0x3E || c === 0x2F || c === 0x20 || c === 0x09) { + break; + } + _$pos++; + } + value = _$html.substring(valStart, _$pos); + } + + $applyAttribute(name_len, name_c0, value, text_format, options); + } else { + $applyBooleanAttribute(name_len, name_c0, text_format); + } + } +}; + +/** + * @description 属性名をcharCodeで識別して値を直接適用 + * Identify attribute name by charCode and apply value directly + * + * name lengths: face(4), size(4), bold(4), style(5), align(5), color(5), + * italic(6), leading(7), underline(9), leftMargin(10), + * rightMargin(11), letterSpacing(14) + */ +const $applyAttribute = ( + name_len: number, + name_c0: number, + value: string, + text_format: TextFormat, + options: IOptions +): void => { + + switch (name_len) { + + case 4: + // face(0x66/0x46) | size(0x73/0x53) | bold(0x62/0x42) + if (name_c0 === 0x66 || name_c0 === 0x46) { + text_format.font = value; + } else if (name_c0 === 0x73 || name_c0 === 0x53) { + text_format.size = +value; + if (options.subFontSize) { + text_format.size -= options.subFontSize; + if (1 > text_format.size) { + text_format.size = 1; + } + } + } else if (name_c0 === 0x62 || name_c0 === 0x42) { + text_format.bold = true; + } + break; + + case 5: + // color(0x63/0x43) | style(0x73/0x53) | align(0x61/0x41) + if (name_c0 === 0x63 || name_c0 === 0x43) { + text_format.color = $toColorInt(value); + } else if (name_c0 === 0x73 || name_c0 === 0x53) { + $applyStyleAttributes( + textParserParseStyleService(value), + text_format, options + ); + } else if (name_c0 === 0x61 || name_c0 === 0x41) { + text_format.align = value as ITextFormatAlign; + } + break; + + case 6: + // italic(0x69/0x49) + if (name_c0 === 0x69 || name_c0 === 0x49) { + text_format.italic = true; + } + break; + + case 7: + // leading(0x6C/0x4C) + if (name_c0 === 0x6C || name_c0 === 0x4C) { + text_format.leading = parseInt(value); + } + break; + + case 9: + // underline(0x75/0x55) + if (name_c0 === 0x75 || name_c0 === 0x55) { + text_format.underline = true; + } + break; - case "FONT": // FONT tag - case "SPAN": // SPAN tag - textParserSetAttributesUseCase((node as Element).attributes, textFormat, options); + case 10: + // leftMargin(0x6C/0x4C) + if (name_c0 === 0x6C || name_c0 === 0x4C) { + text_format.leftMargin = parseInt(value); + } + break; + + case 11: + // rightMargin(0x72/0x52) + if (name_c0 === 0x72 || name_c0 === 0x52) { + text_format.rightMargin = parseInt(value); + } + break; + + case 14: + // letterSpacing(0x6C/0x4C) + if (name_c0 === 0x6C || name_c0 === 0x4C) { + text_format.letterSpacing = parseInt(value); + } + break; + + default: + break; + + } +}; + +/** + * @description 値なし属性をcharCodeで識別して直接適用 + * Apply boolean (no-value) attributes by charCode + */ +const $applyBooleanAttribute = ( + name_len: number, + name_c0: number, + text_format: TextFormat +): void => { + if (name_len === 4 && (name_c0 === 0x62 || name_c0 === 0x42)) { + text_format.bold = true; + } else if (name_len === 6 && (name_c0 === 0x69 || name_c0 === 0x49)) { + text_format.italic = true; + } else if (name_len === 9 && (name_c0 === 0x75 || name_c0 === 0x55)) { + text_format.underline = true; + } +}; + +/** + * @description style属性の解析結果をtext_formatに直接適用 + * Apply parsed style attributes directly to text_format + */ +const $applyStyleAttributes = ( + attributes: IAttributeObject[], + text_format: TextFormat, + options: IOptions +): void => { + for (let idx: number = 0; idx < attributes.length; ++idx) { + const attr: IAttributeObject = attributes[idx]; + switch (attr.name) { + + case "face": + text_format.font = attr.value as string; break; - case "BR": - if (!options.multiline) { - continue; + case "size": + text_format.size = +attr.value; + if (options.subFontSize) { + text_format.size -= options.subFontSize; + if (1 > text_format.size) { + text_format.size = 1; + } } - textParserCreateNewLineUseCase(text_data, textFormat); + break; + + case "color": + text_format.color = $toColorInt(attr.value); + break; + + case "align": + text_format.align = attr.value as ITextFormatAlign; + break; + + case "letterSpacing": + text_format.letterSpacing = parseInt(attr.value as string); + break; + + case "leading": + text_format.leading = parseInt(attr.value as string); + break; + + case "leftMargin": + text_format.leftMargin = parseInt(attr.value as string); + break; + + case "rightMargin": + text_format.rightMargin = parseInt(attr.value as string); + break; + + case "underline": + text_format.underline = true; + break; + + case "bold": + text_format.bold = true; + break; + + case "italic": + text_format.italic = true; break; default: break; } + } +}; + +/** + * @description 文字列未生成でタグを数値IDに変換 + * Zero-allocation tag identification via character comparison + */ +const $identifyTag = (start: number, end: number): number => +{ + while (start < end) { + const c: number = _$html.charCodeAt(start); + if (c !== 0x20 && c !== 0x09) { + break; + } + start++; + } + while (end > start) { + const c: number = _$html.charCodeAt(end - 1); + if (c !== 0x20 && c !== 0x09) { + break; + } + end--; + } + + const length: number = end - start; + + if (length === 1) { + let c: number = _$html.charCodeAt(start); + if (c >= 0x61) { c -= 0x20 } + if (c === 0x42) { return $TAG_B } // B + if (c === 0x49) { return $TAG_I } // I + if (c === 0x55) { return $TAG_U } // U + if (c === 0x50) { return $TAG_P } // P + return $TAG_NONE; + } + + if (length === 2) { + let c0: number = _$html.charCodeAt(start); + let c1: number = _$html.charCodeAt(start + 1); + if (c0 >= 0x61) { c0 -= 0x20 } + if (c1 >= 0x61) { c1 -= 0x20 } + if (c0 === 0x42 && c1 === 0x52) { return $TAG_BR } // BR + return $TAG_NONE; + } + + if (length === 3) { + let c0: number = _$html.charCodeAt(start); + if (c0 >= 0x61) { c0 -= 0x20 } + if (c0 === 0x44) { // D + let c1: number = _$html.charCodeAt(start + 1); + let c2: number = _$html.charCodeAt(start + 2); + if (c1 >= 0x61) { c1 -= 0x20 } + if (c2 >= 0x61) { c2 -= 0x20 } + if (c1 === 0x49 && c2 === 0x56) { return $TAG_DIV } // DIV + } + return $TAG_NONE; + } - execute(node as Document, textFormat, text_data, options); + if (length === 4) { + let c0: number = _$html.charCodeAt(start); + if (c0 >= 0x61) { c0 -= 0x20 } + if (c0 === 0x46) { // F + let c1: number = _$html.charCodeAt(start + 1); + let c2: number = _$html.charCodeAt(start + 2); + let c3: number = _$html.charCodeAt(start + 3); + if (c1 >= 0x61) { c1 -= 0x20 } + if (c2 >= 0x61) { c2 -= 0x20 } + if (c3 >= 0x61) { c3 -= 0x20 } + if (c1 === 0x4F && c2 === 0x4E && c3 === 0x54) { return $TAG_FONT } // FONT + } + if (c0 === 0x53) { // S + let c1: number = _$html.charCodeAt(start + 1); + let c2: number = _$html.charCodeAt(start + 2); + let c3: number = _$html.charCodeAt(start + 3); + if (c1 >= 0x61) { c1 -= 0x20 } + if (c2 >= 0x61) { c2 -= 0x20 } + if (c3 >= 0x61) { c3 -= 0x20 } + if (c1 === 0x50 && c2 === 0x41 && c3 === 0x4E) { return $TAG_SPAN } // SPAN + } + return $TAG_NONE; } + + return $TAG_NONE; }; \ No newline at end of file diff --git a/packages/text/src/interface/IHtmlNode.ts b/packages/text/src/interface/IHtmlNode.ts new file mode 100644 index 00000000..a136e1d6 --- /dev/null +++ b/packages/text/src/interface/IHtmlNode.ts @@ -0,0 +1,27 @@ +import type { IAttributeObject } from "./IAttributeObject"; + +/** + * @description テキストノード + * Text node + */ +export interface IHtmlTextNode { + readonly type: "text"; + readonly value: string; +} + +/** + * @description 要素ノード + * Element node + */ +export interface IHtmlElementNode { + readonly type: "element"; + readonly tagName: string; + readonly attributes: IAttributeObject[]; + readonly children: IHtmlNode[]; +} + +/** + * @description HTMLノード共用型 + * HTML node union type + */ +export type IHtmlNode = IHtmlTextNode | IHtmlElementNode; diff --git a/packages/webgl/src/Context.ts b/packages/webgl/src/Context.ts index 058892c7..f53b295d 100644 --- a/packages/webgl/src/Context.ts +++ b/packages/webgl/src/Context.ts @@ -47,6 +47,7 @@ import { execute as contextStrokeUseCase } from "./Context/usecase/ContextStroke import { execute as contextApplyFilterUseCase } from "./Context/usecase/ContextApplyFilterUseCase"; import { execute as contextContainerBeginLayerUseCase } from "./Context/usecase/ContextContainerBeginLayerUseCase"; import { execute as contextContainerEndLayerUseCase } from "./Context/usecase/ContextContainerEndLayerUseCase"; +import { execute as contextContainerEndAtlasNodeUseCase } from "./Context/usecase/ContextContainerEndAtlasNodeUseCase"; import { execute as contextContainerDrawCachedFilterUseCase } from "./Context/usecase/ContextContainerDrawCachedFilterUseCase"; import { execute as contextUpdateTransferBoundsService } from "./Context/service/ContextUpdateTransferBoundsService"; import { execute as contextDrawFillUseCase } from "./Context/usecase/ContextDrawFillUseCase"; @@ -95,6 +96,7 @@ export class Context public joints: number; public miterLimit: number; public newDrawState: boolean = false; + private readonly _pendingAtlasNodes: Node[] = []; constructor ( gl: WebGL2RenderingContext, @@ -509,6 +511,26 @@ export class Context ); } + containerBeginAtlasNode (width: number, height: number): Node + { + this.drawArraysInstanced(); + + // アトラスノードを確保(後でコピー先として使用) + const node = atlasManagerCreateNodeService(width, height); + this._pendingAtlasNodes.push(node); + + // temp FBOを作成して子要素の描画先として設定 + contextContainerBeginLayerUseCase(width, height); + + return node; + } + + containerEndAtlasNode (): void + { + const node = this._pendingAtlasNodes.pop()!; + contextContainerEndAtlasNodeUseCase(node); + } + containerDrawCachedFilter ( blend_mode: IBlendMode, matrix: Float32Array, diff --git a/packages/webgl/src/Context/usecase/ContextContainerBeginLayerUseCase.test.ts b/packages/webgl/src/Context/usecase/ContextContainerBeginLayerUseCase.test.ts new file mode 100644 index 00000000..92c2d069 --- /dev/null +++ b/packages/webgl/src/Context/usecase/ContextContainerBeginLayerUseCase.test.ts @@ -0,0 +1,75 @@ +import { execute, $containerLayerStack } from "./ContextContainerBeginLayerUseCase"; +import { describe, expect, it, vi, beforeEach } from "vitest"; + +vi.mock("../../WebGLUtil", () => ({ + "$context": { + "drawArraysInstanced": vi.fn(), + "$mainAttachmentObject": { "width": 800, "height": 600, "label": "main" }, + "bind": vi.fn() + } +})); + +vi.mock("../../FrameBufferManager/usecase/FrameBufferManagerGetAttachmentObjectUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600, "label": "layer" })) +})); + +describe("ContextContainerBeginLayerUseCase.js test", () => { + + beforeEach(() => + { + $containerLayerStack.length = 0; + }); + + it("execute test case1 - pushes current main to stack", async () => + { + const { $context } = await import("../../WebGLUtil"); + const originalMain = $context.$mainAttachmentObject; + + execute(800, 600); + + expect($containerLayerStack.length).toBe(1); + expect($containerLayerStack[0]).toBe(originalMain); + }); + + it("execute test case2 - flushes instanced draw before switching", async () => + { + const { $context } = await import("../../WebGLUtil"); + vi.mocked($context.drawArraysInstanced).mockClear(); + + execute(800, 600); + + expect($context.drawArraysInstanced).toHaveBeenCalledTimes(1); + }); + + it("execute test case3 - sets new layer attachment as main", async () => + { + const { $context } = await import("../../WebGLUtil"); + + execute(400, 300); + + expect($context.$mainAttachmentObject).toEqual({ "width": 800, "height": 600, "label": "layer" }); + }); + + it("execute test case4 - binds layer attachment", async () => + { + const { $context } = await import("../../WebGLUtil"); + vi.mocked($context.bind).mockClear(); + + execute(800, 600); + + expect($context.bind).toHaveBeenCalledTimes(1); + }); + + it("execute test case5 - multiple layers stack correctly", async () => + { + const { $context } = await import("../../WebGLUtil"); + + $context.$mainAttachmentObject = { "label": "main1" } as any; + execute(800, 600); + + $context.$mainAttachmentObject = { "label": "main2" } as any; + execute(400, 300); + + expect($containerLayerStack.length).toBe(2); + }); +}); diff --git a/packages/webgl/src/Context/usecase/ContextContainerDrawCachedFilterUseCase.test.ts b/packages/webgl/src/Context/usecase/ContextContainerDrawCachedFilterUseCase.test.ts new file mode 100644 index 00000000..a709cfd7 --- /dev/null +++ b/packages/webgl/src/Context/usecase/ContextContainerDrawCachedFilterUseCase.test.ts @@ -0,0 +1,93 @@ +import { execute } from "./ContextContainerDrawCachedFilterUseCase"; +import { describe, expect, it, vi } from "vitest"; + +vi.mock("@next2d/cache", () => ({ + "$cacheStore": { + "get": vi.fn() + } +})); + +vi.mock("../../WebGLUtil", () => ({ + "$context": { + "drawArraysInstanced": vi.fn(), + "reset": vi.fn(), + "globalCompositeOperation": "normal" + }, + "$devicePixelRatio": 1 +})); + +vi.mock("../../Blend/usecase/BlendDrawFilterToMainUseCase", () => ({ + "execute": vi.fn() +})); + +describe("ContextContainerDrawCachedFilterUseCase.js test", () => { + + it("execute test case1 - returns early if cached key does not match", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + const blendMod = await import("../../Blend/usecase/BlendDrawFilterToMainUseCase"); + + vi.mocked($cacheStore.get).mockImplementation((key: string, prop: string) => { + if (prop === "fKey") { return "old_key"; } + return null; + }); + vi.mocked(blendMod.execute).mockClear(); + + const matrix = new Float32Array([1, 0, 0, 1, 10, 20]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + const filterBounds = new Float32Array([-5, -5, 110, 110]); + + execute("normal", matrix, colorTransform, filterBounds, "1", "new_key"); + + expect(blendMod.execute).not.toHaveBeenCalled(); + }); + + it("execute test case2 - returns early if no texture object", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + const blendMod = await import("../../Blend/usecase/BlendDrawFilterToMainUseCase"); + + vi.mocked($cacheStore.get).mockImplementation((_key: string, prop: string) => { + if (prop === "fKey") { return "match_key"; } + if (prop === "fTexture") { return null; } + return null; + }); + vi.mocked(blendMod.execute).mockClear(); + + const matrix = new Float32Array([1, 0, 0, 1, 10, 20]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + const filterBounds = new Float32Array([-5, -5, 110, 110]); + + execute("normal", matrix, colorTransform, filterBounds, "1", "match_key"); + + expect(blendMod.execute).not.toHaveBeenCalled(); + }); + + it("execute test case3 - draws cached filter when key matches", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + const { $context } = await import("../../WebGLUtil"); + const blendMod = await import("../../Blend/usecase/BlendDrawFilterToMainUseCase"); + + const mockTexture = { "width": 100, "height": 100 }; + vi.mocked($cacheStore.get).mockImplementation((_key: string, prop: string) => { + if (prop === "fKey") { return "valid_key"; } + if (prop === "fTexture") { return mockTexture; } + return null; + }); + vi.mocked($context.drawArraysInstanced).mockClear(); + vi.mocked($context.reset).mockClear(); + vi.mocked(blendMod.execute).mockClear(); + + const matrix = new Float32Array([1, 0, 0, 1, 10, 20]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + const filterBounds = new Float32Array([-5, -5, 110, 110]); + + execute("add", matrix, colorTransform, filterBounds, "1", "valid_key"); + + expect($context.drawArraysInstanced).toHaveBeenCalledTimes(1); + expect($context.reset).toHaveBeenCalledTimes(1); + expect($context.globalCompositeOperation).toBe("add"); + expect(blendMod.execute).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/webgl/src/Context/usecase/ContextContainerEndAtlasNodeUseCase.ts b/packages/webgl/src/Context/usecase/ContextContainerEndAtlasNodeUseCase.ts new file mode 100644 index 00000000..30ef2256 --- /dev/null +++ b/packages/webgl/src/Context/usecase/ContextContainerEndAtlasNodeUseCase.ts @@ -0,0 +1,79 @@ +import type { IAttachmentObject } from "../../interface/IAttachmentObject"; +import type { ITextureObject } from "../../interface/ITextureObject"; +import type { Node } from "@next2d/texture-packer"; +import { $containerLayerStack } from "./ContextContainerBeginLayerUseCase"; +import { execute as frameBufferManagerReleaseAttachmentObjectUseCase } from "../../FrameBufferManager/usecase/FrameBufferManagerReleaseAttachmentObjectUseCase"; +import { execute as variantsBlendMatrixTextureShaderService } from "../../Shader/Variants/Blend/service/VariantsBlendMatrixTextureShaderService"; +import { execute as shaderManagerSetMatrixTextureWithColorTransformUniformService } from "../../Shader/ShaderManager/service/ShaderManagerSetMatrixTextureWithColorTransformUniformService"; +import { execute as textureManagerBind0UseCase } from "../../TextureManager/usecase/TextureManagerBind0UseCase"; +import { execute as shaderManagerDrawTextureUseCase } from "../../Shader/ShaderManager/usecase/ShaderManagerDrawTextureUseCase"; +import { execute as textureManagerReleaseTextureObjectUseCase } from "../../TextureManager/usecase/TextureManagerReleaseTextureObjectUseCase"; +import { execute as blendOperationUseCase } from "../../Blend/usecase/BlendOperationUseCase"; +import { $context } from "../../WebGLUtil"; +import { $getAtlasAttachmentObject } from "../../AtlasManager"; + +/** + * @description コンテナ用のカラー変換(恒等変換) + * Identity color transform for container copy + * + * @type {Float32Array} + * @private + */ +const $identityColorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + +/** + * @description コンテナのアトラスノードへの描画を終了し、 + * temp FBOの内容をアトラスのノード領域にコピーします。 + * End container atlas node rendering, + * copy temp FBO contents to atlas node region. + * + * @param {Node} node + * @return {void} + * @method + * @protected + */ +export const execute = (node: Node): void => +{ + // temp FBOへの描画をフラッシュ + $context.drawArraysInstanced(); + + const tempAttachment = $context.$mainAttachmentObject as IAttachmentObject; + const textureObject = tempAttachment.texture as ITextureObject; + + // mainを復元($containerLayerStackから) + $context.$mainAttachmentObject = $containerLayerStack.pop() as IAttachmentObject; + + // temp FBOを解放(テクスチャは保持してアトラスコピーに使用) + frameBufferManagerReleaseAttachmentObjectUseCase(tempAttachment, false); + + if (textureObject) { + // アトラスにバインド + const atlas = $getAtlasAttachmentObject() as IAttachmentObject; + $context.bind(atlas); + + // ノード領域を設定(scissor + clear + transfer bounds登録) + $context.beginNodeRendering(node); + + // ブレンドモード設定(premultiplied alpha用: ONE, ONE_MINUS_SRC_ALPHA) + blendOperationUseCase("normal"); + + // テクスチャをノード位置に描画(Y軸反転: WebGLはbottom-left原点) + const offsetY = atlas.height - node.y - node.h; + $context.setTransform(1, 0, 0, 1, node.x, offsetY); + const shaderManager = variantsBlendMatrixTextureShaderService(false); + shaderManagerSetMatrixTextureWithColorTransformUniformService( + shaderManager, $identityColorTransform, node.w, node.h + ); + textureManagerBind0UseCase(textureObject); + shaderManagerDrawTextureUseCase(shaderManager); + + // ノード描画終了 + $context.endNodeRendering(); + + // テクスチャを解放 + textureManagerReleaseTextureObjectUseCase(textureObject); + } + + // mainをバインド + $context.bind($context.$mainAttachmentObject as IAttachmentObject); +}; diff --git a/packages/webgl/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts b/packages/webgl/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts new file mode 100644 index 00000000..5164173b --- /dev/null +++ b/packages/webgl/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts @@ -0,0 +1,122 @@ +import { execute } from "./ContextContainerEndLayerUseCase"; +import { describe, expect, it, vi, beforeEach } from "vitest"; + +vi.mock("@next2d/cache", () => ({ + "$cacheStore": { + "set": vi.fn(), + "get": vi.fn() + } +})); + +vi.mock("../../WebGLUtil", () => ({ + "$context": { + "drawArraysInstanced": vi.fn(), + "$mainAttachmentObject": { "width": 800, "height": 600, "label": "layer" }, + "bind": vi.fn(), + "reset": vi.fn(), + "globalCompositeOperation": "normal" + }, + "$devicePixelRatio": 1 +})); + +vi.mock("./ContextContainerBeginLayerUseCase", () => ({ + "$containerLayerStack": [] as any[] +})); + +vi.mock("../../FrameBufferManager/usecase/FrameBufferManagerGetTextureFromBoundsUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600, "textureId": 1 })) +})); + +vi.mock("../../FrameBufferManager/usecase/FrameBufferManagerReleaseAttachmentObjectUseCase", () => ({ + "execute": vi.fn() +})); + +vi.mock("../../Blend/usecase/BlendDrawFilterToMainUseCase", () => ({ + "execute": vi.fn() +})); + +vi.mock("../../TextureManager/usecase/TextureManagerReleaseTextureObjectUseCase", () => ({ + "execute": vi.fn() +})); + +vi.mock("../../Filter/BlurFilter/usecase/FilterApplyBlurFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600, "textureId": 2 })) +})); +vi.mock("../../Filter/BevelFilter/usecase/FilterApplyBevelFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600 })) +})); +vi.mock("../../Filter/ColorMatrixFilter/usecase/FilterApplyColorMatrixFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600 })) +})); +vi.mock("../../Filter/ConvolutionFilter/usecase/FilterApplyConvolutionFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600 })) +})); +vi.mock("../../Filter/DisplacementMapFilter/usecase/FilterApplyDisplacementMapFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600 })) +})); +vi.mock("../../Filter/DropShadowFilter/usecase/FilterApplyDropShadowFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600 })) +})); +vi.mock("../../Filter/GlowFilter/usecase/FilterApplyGlowFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600 })) +})); +vi.mock("../../Filter/GradientBevelFilter/usecase/FilterApplyGradientBevelFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600 })) +})); +vi.mock("../../Filter/GradientGlowFilter/usecase/FilterApplyGradientGlowFilterUseCase", () => ({ + "execute": vi.fn(() => ({ "width": 800, "height": 600 })) +})); +vi.mock("../../Filter", () => ({ + "$offset": { "x": 0, "y": 0 } +})); + +describe("ContextContainerEndLayerUseCase.js test", () => { + + beforeEach(async () => + { + vi.clearAllMocks(); + const { $containerLayerStack } = await import("./ContextContainerBeginLayerUseCase"); + $containerLayerStack.length = 0; + $containerLayerStack.push({ "width": 800, "height": 600, "label": "main" } as any); + + const { $context } = await import("../../WebGLUtil"); + $context.$mainAttachmentObject = { "width": 800, "height": 600, "label": "layer" } as any; + }); + + it("execute test case1 - blend only (no filter)", async () => + { + const { $context } = await import("../../WebGLUtil"); + const blendMod = await import("../../Blend/usecase/BlendDrawFilterToMainUseCase"); + const releaseMod = await import("../../TextureManager/usecase/TextureManagerReleaseTextureObjectUseCase"); + + const matrix = new Float32Array([1, 0, 0, 1, 10, 20]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + execute("normal", matrix, colorTransform, false, null, null, "", ""); + + expect($context.drawArraysInstanced).toHaveBeenCalledTimes(1); + expect($context.reset).toHaveBeenCalledTimes(1); + expect(blendMod.execute).toHaveBeenCalledTimes(1); + expect(releaseMod.execute).toHaveBeenCalledTimes(1); + expect($context.bind).toHaveBeenCalled(); + }); + + it("execute test case2 - with filter (blur)", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + const blendMod = await import("../../Blend/usecase/BlendDrawFilterToMainUseCase"); + const blurMod = await import("../../Filter/BlurFilter/usecase/FilterApplyBlurFilterUseCase"); + + const matrix = new Float32Array([1, 0, 0, 1, 10, 20]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + const filterBounds = new Float32Array([-5, -5, 110, 110]); + // BlurFilter: type=1, blurX=4, blurY=4, quality=1 + const filterParams = new Float32Array([1, 4, 4, 1]); + + execute("normal", matrix, colorTransform, true, filterBounds, filterParams, "uk1", "fk1"); + + expect(blurMod.execute).toHaveBeenCalledTimes(1); + expect($cacheStore.set).toHaveBeenCalledWith("uk1", "fKey", "fk1"); + expect(blendMod.execute).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/webgpu/src/AtlasManager.test.ts b/packages/webgpu/src/AtlasManager.test.ts index daa5c803..a8e48e2d 100644 --- a/packages/webgpu/src/AtlasManager.test.ts +++ b/packages/webgpu/src/AtlasManager.test.ts @@ -5,12 +5,8 @@ import { $getAtlasAttachmentObjects, $setAtlasAttachmentObject, $getAtlasAttachmentObject, - $hasAtlasAttachmentObject, $rootNodes, - $setAtlasTexture, - $getAtlasTexture, $getActiveTransferBounds, - $getActiveAllTransferBounds, $clearTransferBounds, $setCurrentAtlasIndex, $getCurrentAtlasIndex, @@ -91,31 +87,6 @@ describe("AtlasManager", () => }); }); - describe("$hasAtlasAttachmentObject", () => - { - it("should return false when no attachment exists", () => - { - expect($hasAtlasAttachmentObject()).toBe(false); - }); - - it("should return true when attachment exists", () => - { - const mockAttachment = createMockAttachment(1, 512, 512); - $setAtlasAttachmentObject(mockAttachment); - - expect($hasAtlasAttachmentObject()).toBe(true); - }); - - it("should return false at non-existing index", () => - { - const mockAttachment = createMockAttachment(1, 512, 512); - $setAtlasAttachmentObject(mockAttachment); - - $setActiveAtlasIndex(5); - expect($hasAtlasAttachmentObject()).toBe(false); - }); - }); - describe("$rootNodes", () => { it("should be empty array after reset", () => @@ -133,44 +104,6 @@ describe("AtlasManager", () => }); }); - describe("$setAtlasTexture / $getAtlasTexture", () => - { - it("should set and get atlas texture", () => - { - const mockTexture = { - "id": 1, - "width": 4096, - "height": 4096, - "area": 4096 * 4096, - "smooth": true, - "resource": {}, - "view": {} - } as any; - - $setAtlasTexture(mockTexture); - - expect($getAtlasTexture()).toBe(mockTexture); - }); - - it("should allow setting to null", () => - { - const mockTexture = { - "id": 1, - "width": 4096, - "height": 4096, - "area": 4096 * 4096, - "smooth": true, - "resource": {}, - "view": {} - } as any; - - $setAtlasTexture(mockTexture); - $setAtlasTexture(null); - - expect($getAtlasTexture()).toBeNull(); - }); - }); - describe("$getActiveTransferBounds", () => { it("should create transfer bounds at index", () => @@ -209,28 +142,6 @@ describe("AtlasManager", () => }); }); - describe("$getActiveAllTransferBounds", () => - { - it("should create all transfer bounds at index", () => - { - const bounds = $getActiveAllTransferBounds(0); - - expect(bounds).toBeInstanceOf(Float32Array); - expect(bounds.length).toBe(4); - }); - - it("should initialize with max/min values", () => - { - const bounds = $getActiveAllTransferBounds(0); - - // Float32Array stores Number.MAX_VALUE as Infinity - expect(bounds[0]).toBe(Infinity); - expect(bounds[1]).toBe(Infinity); - expect(bounds[2]).toBe(-Infinity); - expect(bounds[3]).toBe(-Infinity); - }); - }); - describe("$clearTransferBounds", () => { it("should reset transfer bounds to initial values", () => @@ -250,22 +161,19 @@ describe("AtlasManager", () => expect(bounds[3]).toBe(-Infinity); }); - it("should reset all transfer bounds arrays", () => + it("should reset multiple transfer bounds arrays", () => { const bounds0 = $getActiveTransferBounds(0); const bounds1 = $getActiveTransferBounds(1); - const allBounds0 = $getActiveAllTransferBounds(0); bounds0[0] = 50; bounds1[0] = 60; - allBounds0[0] = 70; $clearTransferBounds(); // Float32Array stores Number.MAX_VALUE as Infinity expect(bounds0[0]).toBe(Infinity); expect(bounds1[0]).toBe(Infinity); - expect(allBounds0[0]).toBe(Infinity); }); }); @@ -348,7 +256,7 @@ describe("AtlasManager", () => $resetAtlas(); // Attachment should be removed - expect($hasAtlasAttachmentObject()).toBe(false); + expect($getAtlasAttachmentObjects().length).toBe(0); }); it("should clear transfer bounds", () => diff --git a/packages/webgpu/src/AtlasManager.ts b/packages/webgpu/src/AtlasManager.ts index 9bfd2cb1..f53c9050 100644 --- a/packages/webgpu/src/AtlasManager.ts +++ b/packages/webgpu/src/AtlasManager.ts @@ -1,43 +1,105 @@ import type { IAttachmentObject } from "./interface/IAttachmentObject"; -import type { ITextureObject } from "./interface/ITextureObject"; import type { TexturePacker } from "@next2d/texture-packer"; +/** + * @description テクスチャアトラス境界の初期最大値 + * Initial maximum value for texture atlas bounds + * @type {number} + */ const $MAX_VALUE: number = Number.MAX_VALUE; + +/** + * @description テクスチャアトラス境界の初期最小値 + * Initial minimum value for texture atlas bounds + * @type {number} + */ const $MIN_VALUE: number = -Number.MAX_VALUE; +/** + * @description 現在アクティブなアトラスのインデックス + * Index of the currently active atlas + * @type {number} + */ let $activeAtlasIndex: number = 0; +/** + * @description アクティブなアトラスインデックスを設定する + * Set the active atlas index + * @param {number} index - アトラスインデックス / atlas index + * @return {void} + */ export const $setActiveAtlasIndex = (index: number): void => { $activeAtlasIndex = index; }; +/** + * @description アクティブなアトラスインデックスを取得する + * Get the active atlas index + * @return {number} + */ export const $getActiveAtlasIndex = (): number => { return $activeAtlasIndex; }; +/** + * @description アトラスのアタッチメントオブジェクト配列 + * Array of atlas attachment objects + * @type {IAttachmentObject[]} + */ const $atlasAttachmentObjects: IAttachmentObject[] = []; +/** + * @description アトラスのアタッチメントオブジェクト配列を取得する + * Get the array of atlas attachment objects + * @return {IAttachmentObject[]} + */ export const $getAtlasAttachmentObjects = (): IAttachmentObject[] => { return $atlasAttachmentObjects; }; +/** + * @description アクティブなインデックスにアタッチメントオブジェクトを設定する + * Set an attachment object at the active atlas index + * @param {IAttachmentObject} attachment_object - アタッチメントオブジェクト / attachment object + * @return {void} + */ export const $setAtlasAttachmentObject = (attachment_object: IAttachmentObject): void => { $atlasAttachmentObjects[$activeAtlasIndex] = attachment_object; }; +/** + * @description アトラス生成関数の型定義 + * Type definition for atlas creator function + */ type AtlasCreator = (index: number) => IAttachmentObject; +/** + * @description アトラス生成関数の参照 + * Atlas creator function reference + * @type {AtlasCreator | null} + */ let $atlasCreator: AtlasCreator | null = null; +/** + * @description アトラス生成関数を設定する + * Set the atlas creator function + * @param {AtlasCreator} creator - アトラス生成関数 / atlas creator function + * @return {void} + */ export const $setAtlasCreator = (creator: AtlasCreator): void => { $atlasCreator = creator; }; +/** + * @description アクティブなインデックスのアタッチメントオブジェクトを取得する(未作成の場合はcreatorで生成) + * Get the attachment object at the active index (creates via creator if not yet created) + * @return {IAttachmentObject | null} + */ export const $getAtlasAttachmentObject = (): IAttachmentObject | null => { if (!($activeAtlasIndex in $atlasAttachmentObjects)) { @@ -51,6 +113,12 @@ export const $getAtlasAttachmentObject = (): IAttachmentObject | null => return $atlasAttachmentObjects[$activeAtlasIndex]; }; +/** + * @description 指定インデックスのアタッチメントオブジェクトを取得する + * Get the attachment object at a specified index + * @param {number} index - アトラスインデックス / atlas index + * @return {IAttachmentObject | null} + */ export const $getAtlasAttachmentObjectByIndex = (index: number): IAttachmentObject | null => { if (!(index in $atlasAttachmentObjects)) { @@ -59,27 +127,26 @@ export const $getAtlasAttachmentObjectByIndex = (index: number): IAttachmentObje return $atlasAttachmentObjects[index]; }; -export const $hasAtlasAttachmentObject = (): boolean => -{ - return $activeAtlasIndex in $atlasAttachmentObjects; -}; - +/** + * @description テクスチャパッカーのルートノード配列 + * Array of root nodes for texture packing + * @type {TexturePacker[]} + */ export const $rootNodes: TexturePacker[] = []; -export let $atlasTexture: ITextureObject | null = null; - -export const $setAtlasTexture = (texture_object: ITextureObject | null): void => -{ - $atlasTexture = texture_object; -}; - -export const $getAtlasTexture = (): ITextureObject | null => -{ - return $atlasTexture; -}; - +/** + * @description アトラスごとの転送領域配列 + * Array of transfer bounds per atlas + * @type {Float32Array[]} + */ const $transferBounds: Float32Array[] = []; +/** + * @description 指定インデックスのアクティブな転送領域を取得する(未作成の場合は初期化) + * Get the active transfer bounds at the specified index (initializes if not yet created) + * @param {number} index - アトラスインデックス / atlas index + * @return {Float32Array} + */ export const $getActiveTransferBounds = (index: number): Float32Array => { if (!(index in $transferBounds)) { @@ -93,21 +160,11 @@ export const $getActiveTransferBounds = (index: number): Float32Array => return $transferBounds[index]; }; -const $allTransferBounds: Float32Array[] = []; - -export const $getActiveAllTransferBounds = (index: number): Float32Array => -{ - if (!(index in $allTransferBounds)) { - $allTransferBounds[index] = new Float32Array([ - $MAX_VALUE, - $MAX_VALUE, - $MIN_VALUE, - $MIN_VALUE - ]); - } - return $allTransferBounds[index]; -}; - +/** + * @description 全ての転送領域を初期値にリセットする + * Reset all transfer bounds to their initial values + * @return {void} + */ export const $clearTransferBounds = (): void => { for (let idx = 0; idx < $transferBounds.length; ++idx) { @@ -119,30 +176,41 @@ export const $clearTransferBounds = (): void => bounds[0] = bounds[1] = $MAX_VALUE; bounds[2] = bounds[3] = $MIN_VALUE; } - - for (let idx = 0; idx < $allTransferBounds.length; ++idx) { - const bounds = $allTransferBounds[idx]; - if (!bounds) { - continue; - } - - bounds[0] = bounds[1] = $MAX_VALUE; - bounds[2] = bounds[3] = $MIN_VALUE; - } }; +/** + * @description 現在処理中のアトラスインデックス + * Index of the currently processed atlas + * @type {number} + */ let $currentAtlasIndex: number = 0; +/** + * @description 現在のアトラスインデックスを設定する + * Set the current atlas index + * @param {number} index - アトラスインデックス / atlas index + * @return {void} + */ export const $setCurrentAtlasIndex = (index: number): void => { $currentAtlasIndex = index; }; +/** + * @description 現在のアトラスインデックスを取得する + * Get the current atlas index + * @return {number} + */ export const $getCurrentAtlasIndex = (): number => { return $currentAtlasIndex; }; +/** + * @description アトラスの全状態をリセットする(テクスチャリソースの破棄を含む) + * Reset all atlas state including destroying texture resources + * @return {void} + */ export const $resetAtlas = (): void => { $rootNodes.length = 0; diff --git a/packages/webgpu/src/AttachmentManager.test.ts b/packages/webgpu/src/AttachmentManager.test.ts index 2db615c6..d47a1f18 100644 --- a/packages/webgpu/src/AttachmentManager.test.ts +++ b/packages/webgpu/src/AttachmentManager.test.ts @@ -27,16 +27,6 @@ vi.mock("./AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase", "execute": vi.fn() })); -vi.mock("./AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService", () => ({ - "execute": vi.fn((attachment, r, g, b, a, loadOp) => ({ - "colorAttachments": [{ - "view": attachment.texture?.view, - "clearValue": { r, g, b, a }, - "loadOp": loadOp, - "storeOp": "store" - }] - })) -})); describe("AttachmentManager", () => { @@ -65,13 +55,6 @@ describe("AttachmentManager", () => expect(manager).toBeDefined(); }); - it("should initialize with null current attachment", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - - expect(manager.getCurrentAttachment()).toBeNull(); - }); }); describe("getAttachmentObject", () => @@ -119,71 +102,6 @@ describe("AttachmentManager", () => }); }); - describe("bindAttachment", () => - { - it("should set current attachment", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - const attachment = manager.getAttachmentObject(100, 100); - - manager.bindAttachment(attachment); - - expect(manager.getCurrentAttachment()).toBe(attachment); - }); - }); - - describe("getCurrentAttachment", () => - { - it("should return null before binding", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - - expect(manager.getCurrentAttachment()).toBeNull(); - }); - - it("should return bound attachment", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - const attachment = manager.getAttachmentObject(100, 100); - - manager.bindAttachment(attachment); - - expect(manager.getCurrentAttachment()).toBe(attachment); - }); - }); - - describe("currentAttachmentObject", () => - { - it("should return same as getCurrentAttachment", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - const attachment = manager.getAttachmentObject(100, 100); - - manager.bindAttachment(attachment); - - expect(manager.currentAttachmentObject).toBe(manager.getCurrentAttachment()); - }); - }); - - describe("unbindAttachment", () => - { - it("should set current attachment to null", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - const attachment = manager.getAttachmentObject(100, 100); - - manager.bindAttachment(attachment); - manager.unbindAttachment(); - - expect(manager.getCurrentAttachment()).toBeNull(); - }); - }); - describe("releaseAttachment", () => { it("should release attachment back to pool", () => @@ -196,51 +114,6 @@ describe("AttachmentManager", () => }); }); - describe("createRenderPassDescriptor", () => - { - it("should create descriptor with clear color", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - const attachment = manager.getAttachmentObject(100, 100); - - const descriptor = manager.createRenderPassDescriptor( - attachment, 0.5, 0.5, 0.5, 1.0, "clear" - ); - - expect(descriptor.colorAttachments).toBeDefined(); - expect((descriptor.colorAttachments as any)[0].clearValue).toEqual({ - "r": 0.5, "g": 0.5, "b": 0.5, "a": 1.0 - }); - }); - - it("should use clear as default loadOp", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - const attachment = manager.getAttachmentObject(100, 100); - - const descriptor = manager.createRenderPassDescriptor( - attachment, 0, 0, 0, 0 - ); - - expect((descriptor.colorAttachments as any)[0].loadOp).toBe("clear"); - }); - - it("should accept load as loadOp", () => - { - const device = createMockDevice(); - const manager = new AttachmentManager(device); - const attachment = manager.getAttachmentObject(100, 100); - - const descriptor = manager.createRenderPassDescriptor( - attachment, 0, 0, 0, 0, "load" - ); - - expect((descriptor.colorAttachments as any)[0].loadOp).toBe("load"); - }); - }); - describe("dispose", () => { it("should not throw when disposing empty manager", () => diff --git a/packages/webgpu/src/AttachmentManager.ts b/packages/webgpu/src/AttachmentManager.ts index 107ab1ba..95d2c1fd 100644 --- a/packages/webgpu/src/AttachmentManager.ts +++ b/packages/webgpu/src/AttachmentManager.ts @@ -4,8 +4,11 @@ import type { IColorBufferObject } from "./interface/IColorBufferObject"; import type { IStencilBufferObject } from "./interface/IStencilBufferObject"; import { execute as attachmentManagerGetAttachmentObjectUseCase } from "./AttachmentManager/usecase/AttachmentManagerGetAttachmentObjectUseCase"; import { execute as attachmentManagerReleaseAttachmentUseCase } from "./AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase"; -import { execute as attachmentManagerCreateRenderPassDescriptorService } from "./AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService"; +/** + * @description アタッチメントリソースのプール管理クラス + * Pool manager class for attachment resources + */ export class AttachmentManager { private device: GPUDevice; @@ -14,8 +17,12 @@ export class AttachmentManager private colorBufferPool: IColorBufferObject[]; private stencilBufferPool: IStencilBufferObject[]; private idCounter: { attachmentId: number; textureId: number; stencilId: number }; - private currentAttachment: IAttachmentObject | null; + /** + * @description AttachmentManagerのコンストラクタ + * Constructor for AttachmentManager + * @param {GPUDevice} device - WebGPUデバイス / WebGPU device + */ constructor(device: GPUDevice) { this.device = device; @@ -24,9 +31,16 @@ export class AttachmentManager this.colorBufferPool = []; this.stencilBufferPool = []; this.idCounter = { "attachmentId": 0, "textureId": 0, "stencilId": 0 }; - this.currentAttachment = null; } + /** + * @description プールからアタッチメントオブジェクトを取得する + * Get an attachment object from the pool + * @param {number} width - テクスチャの幅 / Texture width + * @param {number} height - テクスチャの高さ / Texture height + * @param {boolean} [msaa=false] - MSAAを有効にするか / Whether to enable MSAA + * @return {IAttachmentObject} + */ getAttachmentObject( width: number, height: number, @@ -46,26 +60,12 @@ export class AttachmentManager ); } - bindAttachment(attachment: IAttachmentObject): void - { - this.currentAttachment = attachment; - } - - getCurrentAttachment(): IAttachmentObject | null - { - return this.currentAttachment; - } - - get currentAttachmentObject(): IAttachmentObject | null - { - return this.currentAttachment; - } - - unbindAttachment(): void - { - this.currentAttachment = null; - } - + /** + * @description アタッチメントをプールに返却する + * Release an attachment back to the pool + * @param {IAttachmentObject} attachment - 返却するアタッチメント / Attachment to release + * @return {void} + */ releaseAttachment(attachment: IAttachmentObject): void { attachmentManagerReleaseAttachmentUseCase( @@ -77,25 +77,11 @@ export class AttachmentManager ); } - createRenderPassDescriptor( - attachment: IAttachmentObject, - r: number, - g: number, - b: number, - a: number, - loadOp: GPULoadOp = "clear" - ): GPURenderPassDescriptor - { - return attachmentManagerCreateRenderPassDescriptorService( - attachment, - r, - g, - b, - a, - loadOp - ); - } - + /** + * @description 全リソースを破棄する + * Dispose all resources + * @return {void} + */ dispose(): void { for (const pool of this.texturePool.values()) { diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateAttachmentObjectService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateAttachmentObjectService.ts index 5ca9deca..48b06f0a 100644 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateAttachmentObjectService.ts +++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateAttachmentObjectService.ts @@ -4,16 +4,16 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject"; * @description 新しいアタッチメントオブジェクトを作成 * Create a new attachment object * - * @param {{ attachmentId: number }} idCounter + * @param {{ attachmentId: number }} id_counter - ID管理カウンタ * @return {IAttachmentObject} * @method * @protected */ export const execute = ( - idCounter: { attachmentId: number } + id_counter: { attachmentId: number } ): IAttachmentObject => { return { - "id": idCounter.attachmentId++, + "id": id_counter.attachmentId++, "width": 0, "height": 0, "clipLevel": 0, diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateColorBufferService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateColorBufferService.ts index 38cc6932..8a21184f 100644 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateColorBufferService.ts +++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateColorBufferService.ts @@ -5,10 +5,10 @@ import type { IStencilBufferObject } from "../../interface/IStencilBufferObject" * @description カラーバッファを新規作成 * Create a new color buffer * - * @param {GPUDevice} device - * @param {number} width - * @param {number} height - * @param {IStencilBufferObject} stencil + * @param {GPUDevice} device - GPUデバイス + * @param {number} width - バッファ幅 + * @param {number} height - バッファ高さ + * @param {IStencilBufferObject} stencil - 関連するステンシルバッファ * @return {IColorBufferObject} * @method * @protected diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.test.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.test.ts deleted file mode 100644 index 7cc3038c..00000000 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { describe, it, expect } from "vitest"; -import type { IAttachmentObject } from "../../interface/IAttachmentObject"; -import { execute } from "./AttachmentManagerCreateRenderPassDescriptorService"; - -describe("AttachmentManagerCreateRenderPassDescriptorService", () => -{ - const createMockAttachment = ( - hasColorView: boolean = true, - hasTextureView: boolean = false, - hasStencil: boolean = false - ): IAttachmentObject => ({ - "id": 1, - "width": 256, - "height": 256, - "color": hasColorView ? { "view": { "label": "colorView" } } : null, - "texture": hasTextureView ? { "view": { "label": "textureView" } } : null, - "stencil": hasStencil ? { "view": { "label": "stencilView" } } : null - } as unknown as IAttachmentObject); - - describe("color attachments", () => - { - it("should use color.view when available", () => - { - const attachment = createMockAttachment(true, false, false); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.colorAttachments[0].view).toEqual({ "label": "colorView" }); - }); - - it("should fallback to texture.view when color.view is not available", () => - { - const attachment = createMockAttachment(false, true, false); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.colorAttachments[0].view).toEqual({ "label": "textureView" }); - }); - - it("should throw error when no color view available", () => - { - const attachment = createMockAttachment(false, false, false); - - expect(() => execute(attachment, 0, 0, 0, 1, "clear")).toThrow( - "No color view available for render pass" - ); - }); - - it("should set clearValue with provided RGBA values", () => - { - const attachment = createMockAttachment(true); - - const result = execute(attachment, 0.5, 0.6, 0.7, 0.8, "clear"); - - expect(result.colorAttachments[0].clearValue).toEqual({ - "r": 0.5, - "g": 0.6, - "b": 0.7, - "a": 0.8 - }); - }); - - it("should set loadOp to provided value", () => - { - const attachment = createMockAttachment(true); - - const result = execute(attachment, 0, 0, 0, 1, "load"); - - expect(result.colorAttachments[0].loadOp).toBe("load"); - }); - - it("should default loadOp to clear", () => - { - const attachment = createMockAttachment(true); - - const result = execute(attachment, 0, 0, 0, 1); - - expect(result.colorAttachments[0].loadOp).toBe("clear"); - }); - - it("should set storeOp to store", () => - { - const attachment = createMockAttachment(true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.colorAttachments[0].storeOp).toBe("store"); - }); - }); - - describe("depth stencil attachment", () => - { - it("should include depthStencilAttachment when stencil is available", () => - { - const attachment = createMockAttachment(true, false, true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment).toBeDefined(); - }); - - it("should not include depthStencilAttachment when stencil is not available", () => - { - const attachment = createMockAttachment(true, false, false); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment).toBeUndefined(); - }); - - it("should set stencil view correctly", () => - { - const attachment = createMockAttachment(true, false, true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment?.view).toEqual({ "label": "stencilView" }); - }); - - it("should set depth clear value to 1.0", () => - { - const attachment = createMockAttachment(true, false, true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment?.depthClearValue).toBe(1.0); - }); - - it("should set stencil clear value to 0", () => - { - const attachment = createMockAttachment(true, false, true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment?.stencilClearValue).toBe(0); - }); - - it("should set depthLoadOp to clear", () => - { - const attachment = createMockAttachment(true, false, true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment?.depthLoadOp).toBe("clear"); - }); - - it("should set depthStoreOp to store", () => - { - const attachment = createMockAttachment(true, false, true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment?.depthStoreOp).toBe("store"); - }); - - it("should set stencilLoadOp to clear", () => - { - const attachment = createMockAttachment(true, false, true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment?.stencilLoadOp).toBe("clear"); - }); - - it("should set stencilStoreOp to store", () => - { - const attachment = createMockAttachment(true, false, true); - - const result = execute(attachment, 0, 0, 0, 1, "clear"); - - expect(result.depthStencilAttachment?.stencilStoreOp).toBe("store"); - }); - }); -}); diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.ts deleted file mode 100644 index 77628233..00000000 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateRenderPassDescriptorService.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { IAttachmentObject } from "../../interface/IAttachmentObject"; - -const $clearValue: GPUColorDict = { "r": 0, "g": 0, "b": 0, "a": 0 }; -const $colorAttachment: GPURenderPassColorAttachment = { - "view": null as unknown as GPUTextureView, - "loadOp": "clear", - "storeOp": "store", - "clearValue": $clearValue -}; -const $depthStencilAttachment: GPURenderPassDepthStencilAttachment = { - "view": null as unknown as GPUTextureView, - "depthLoadOp": "clear", - "depthStoreOp": "store", - "depthClearValue": 1.0, - "stencilLoadOp": "clear", - "stencilStoreOp": "store", - "stencilClearValue": 0 -}; -const $descriptor: GPURenderPassDescriptor = { - "colorAttachments": [$colorAttachment] -}; - -/** - * @description レンダーパスディスクリプタを作成(プリアロケート再利用) - */ -export const execute = ( - attachment: IAttachmentObject, - r: number, - g: number, - b: number, - a: number, - loadOp: GPULoadOp = "clear" -): GPURenderPassDescriptor => { - const colorView = attachment.color?.view ?? attachment.texture?.view; - if (!colorView) { - throw new Error("No color view available for render pass"); - } - $colorAttachment.view = colorView; - $colorAttachment.loadOp = loadOp; - $clearValue.r = r; - $clearValue.g = g; - $clearValue.b = b; - $clearValue.a = a; - if (attachment.stencil?.view) { - $depthStencilAttachment.view = attachment.stencil.view; - $descriptor.depthStencilAttachment = $depthStencilAttachment; - } else { - $descriptor.depthStencilAttachment = undefined; - } - return $descriptor; -}; diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateStencilBufferService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateStencilBufferService.ts index 367d1639..3cf886ff 100644 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateStencilBufferService.ts +++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateStencilBufferService.ts @@ -4,10 +4,10 @@ import type { IStencilBufferObject } from "../../interface/IStencilBufferObject" * @description ステンシルバッファを新規作成 * Create a new stencil buffer * - * @param {GPUDevice} device - * @param {number} width - * @param {number} height - * @param {{ stencilId: number }} idCounter + * @param {GPUDevice} device - GPUデバイス + * @param {number} width - バッファ幅 + * @param {number} height - バッファ高さ + * @param {{ stencilId: number }} id_counter - ID管理カウンタ * @return {IStencilBufferObject} * @method * @protected @@ -16,7 +16,7 @@ export const execute = ( device: GPUDevice, width: number, height: number, - idCounter: { stencilId: number } + id_counter: { stencilId: number } ): IStencilBufferObject => { const texture = device.createTexture({ "size": { width, height }, @@ -25,7 +25,7 @@ export const execute = ( }); return { - "id": idCounter.stencilId++, + "id": id_counter.stencilId++, "resource": texture, "view": texture.createView(), width, diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateTextureObjectService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateTextureObjectService.ts index 5b03d7d7..115ca073 100644 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateTextureObjectService.ts +++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerCreateTextureObjectService.ts @@ -4,11 +4,11 @@ import type { ITextureObject } from "../../interface/ITextureObject"; * @description テクスチャオブジェクトを新規作成 * Create a new texture object * - * @param {GPUDevice} device - * @param {number} width - * @param {number} height - * @param {boolean} smooth - * @param {{ textureId: number }} idCounter + * @param {GPUDevice} device - GPUデバイス + * @param {number} width - テクスチャ幅 + * @param {number} height - テクスチャ高さ + * @param {boolean} smooth - スムーズフィルタリングの有効フラグ + * @param {{ textureId: number }} id_counter - ID管理カウンタ * @return {ITextureObject} * @method * @protected @@ -18,7 +18,7 @@ export const execute = ( width: number, height: number, smooth: boolean, - idCounter: { textureId: number } + id_counter: { textureId: number } ): ITextureObject => { const texture = device.createTexture({ "size": { width, height }, @@ -32,7 +32,7 @@ export const execute = ( const view = texture.createView(); return { - "id": idCounter.textureId++, + "id": id_counter.textureId++, "resource": texture, view, width, diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetColorBufferService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetColorBufferService.ts index d43ed5c0..07e571e6 100644 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetColorBufferService.ts +++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetColorBufferService.ts @@ -6,27 +6,27 @@ import { execute as attachmentManagerCreateColorBufferService } from "./Attachme * @description カラーバッファを取得(プールから再利用または新規作成) * Get color buffer from pool or create new one * - * @param {GPUDevice} device - * @param {IColorBufferObject[]} colorBufferPool - * @param {number} width - * @param {number} height - * @param {IStencilBufferObject} stencil + * @param {GPUDevice} device - GPUデバイス + * @param {IColorBufferObject[]} color_buffer_pool - カラーバッファプール + * @param {number} width - バッファ幅 + * @param {number} height - バッファ高さ + * @param {IStencilBufferObject} stencil - 関連するステンシルバッファ * @return {IColorBufferObject} * @method * @protected */ export const execute = ( device: GPUDevice, - colorBufferPool: IColorBufferObject[], + color_buffer_pool: IColorBufferObject[], width: number, height: number, stencil: IStencilBufferObject ): IColorBufferObject => { // プールから適切なサイズのものを検索 - for (let i = 0; i < colorBufferPool.length; i++) { - const buffer = colorBufferPool[i]; + for (let i = 0; i < color_buffer_pool.length; i++) { + const buffer = color_buffer_pool[i]; if (buffer.width >= width && buffer.height >= height) { - colorBufferPool.splice(i, 1); + color_buffer_pool.splice(i, 1); buffer.stencil = stencil; buffer.dirty = false; return buffer; diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetStencilBufferService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetStencilBufferService.ts index b49cc1c4..70b5fdfb 100644 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetStencilBufferService.ts +++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetStencilBufferService.ts @@ -5,32 +5,32 @@ import { execute as attachmentManagerCreateStencilBufferService } from "./Attach * @description ステンシルバッファを取得(プールから再利用または新規作成) * Get stencil buffer from pool or create new one * - * @param {GPUDevice} device - * @param {IStencilBufferObject[]} stencilBufferPool - * @param {number} width - * @param {number} height - * @param {{ stencilId: number }} idCounter + * @param {GPUDevice} device - GPUデバイス + * @param {IStencilBufferObject[]} stencil_buffer_pool - ステンシルバッファプール + * @param {number} width - バッファ幅 + * @param {number} height - バッファ高さ + * @param {{ stencilId: number }} id_counter - ID管理カウンタ * @return {IStencilBufferObject} * @method * @protected */ export const execute = ( device: GPUDevice, - stencilBufferPool: IStencilBufferObject[], + stencil_buffer_pool: IStencilBufferObject[], width: number, height: number, - idCounter: { stencilId: number } + id_counter: { stencilId: number } ): IStencilBufferObject => { // プールから適切なサイズのものを検索 - for (let i = 0; i < stencilBufferPool.length; i++) { - const buffer = stencilBufferPool[i]; + for (let i = 0; i < stencil_buffer_pool.length; i++) { + const buffer = stencil_buffer_pool[i]; if (buffer.width >= width && buffer.height >= height) { - stencilBufferPool.splice(i, 1); + stencil_buffer_pool.splice(i, 1); buffer.dirty = false; return buffer; } } // 新規作成 - return attachmentManagerCreateStencilBufferService(device, width, height, idCounter); + return attachmentManagerCreateStencilBufferService(device, width, height, id_counter); }; diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetTextureService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetTextureService.ts index 300464bf..3c9f5608 100644 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetTextureService.ts +++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerGetTextureService.ts @@ -5,34 +5,34 @@ import { execute as attachmentManagerCreateTextureObjectService } from "./Attach * @description テクスチャオブジェクトを取得(プールから再利用または新規作成) * Get texture object from pool or create new one * - * @param {GPUDevice} device - * @param {Map} texturePool - * @param {number} width - * @param {number} height - * @param {boolean} smooth - * @param {{ textureId: number }} idCounter + * @param {GPUDevice} device - GPUデバイス + * @param {Map} texture_pool - テクスチャプール + * @param {number} width - テクスチャ幅 + * @param {number} height - テクスチャ高さ + * @param {boolean} smooth - スムーズフィルタリングの有効フラグ + * @param {{ textureId: number }} id_counter - ID管理カウンタ * @return {ITextureObject} * @method * @protected */ export const execute = ( device: GPUDevice, - texturePool: Map, + texture_pool: Map, width: number, height: number, smooth: boolean, - idCounter: { textureId: number } + id_counter: { textureId: number } ): ITextureObject => { const key = `${width}x${height}_${smooth ? "smooth" : "nearest"}`; // プールから再利用 - if (texturePool.has(key)) { - const pool = texturePool.get(key)!; + if (texture_pool.has(key)) { + const pool = texture_pool.get(key)!; if (pool.length > 0) { return pool.pop()!; } } // 新規作成 - return attachmentManagerCreateTextureObjectService(device, width, height, smooth, idCounter); + return attachmentManagerCreateTextureObjectService(device, width, height, smooth, id_counter); }; diff --git a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerReleaseTextureService.ts b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerReleaseTextureService.ts index f202edda..0627bdc4 100644 --- a/packages/webgpu/src/AttachmentManager/service/AttachmentManagerReleaseTextureService.ts +++ b/packages/webgpu/src/AttachmentManager/service/AttachmentManagerReleaseTextureService.ts @@ -4,21 +4,21 @@ import type { ITextureObject } from "../../interface/ITextureObject"; * @description テクスチャをプールに返却 * Release texture back to pool * - * @param {Map} texturePool - * @param {ITextureObject} textureObject + * @param {Map} texture_pool - テクスチャプール + * @param {ITextureObject} texture_object - 返却するテクスチャオブジェクト * @return {void} * @method * @protected */ export const execute = ( - texturePool: Map, - textureObject: ITextureObject + texture_pool: Map, + texture_object: ITextureObject ): void => { - const key = `${textureObject.width}x${textureObject.height}_${textureObject.smooth ? "smooth" : "nearest"}`; + const key = `${texture_object.width}x${texture_object.height}_${texture_object.smooth ? "smooth" : "nearest"}`; - if (!texturePool.has(key)) { - texturePool.set(key, []); + if (!texture_pool.has(key)) { + texture_pool.set(key, []); } - texturePool.get(key)!.push(textureObject); + texture_pool.get(key)!.push(texture_object); }; diff --git a/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerGetAttachmentObjectUseCase.ts b/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerGetAttachmentObjectUseCase.ts index a6fb60ae..85a614ec 100644 --- a/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerGetAttachmentObjectUseCase.ts +++ b/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerGetAttachmentObjectUseCase.ts @@ -11,34 +11,34 @@ import { execute as attachmentManagerGetTextureService } from "../service/Attach * @description アタッチメントオブジェクトを取得 * Get attachment object * - * @param {GPUDevice} device - * @param {IAttachmentObject[]} attachmentPool - * @param {Map} texturePool - * @param {IColorBufferObject[]} colorBufferPool - * @param {IStencilBufferObject[]} stencilBufferPool - * @param {number} width - * @param {number} height - * @param {boolean} msaa - * @param {{ attachmentId: number, textureId: number, stencilId: number }} idCounter + * @param {GPUDevice} device - GPUデバイス + * @param {IAttachmentObject[]} attachment_pool - アタッチメントプール + * @param {Map} texture_pool - テクスチャプール + * @param {IColorBufferObject[]} color_buffer_pool - カラーバッファプール + * @param {IStencilBufferObject[]} stencil_buffer_pool - ステンシルバッファプール + * @param {number} width - バッファ幅 + * @param {number} height - バッファ高さ + * @param {boolean} msaa - MSAA有効フラグ + * @param {{ attachmentId: number, textureId: number, stencilId: number }} id_counter - ID管理カウンタ * @return {IAttachmentObject} * @method * @protected */ export const execute = ( device: GPUDevice, - attachmentPool: IAttachmentObject[], - texturePool: Map, - colorBufferPool: IColorBufferObject[], - stencilBufferPool: IStencilBufferObject[], + attachment_pool: IAttachmentObject[], + texture_pool: Map, + color_buffer_pool: IColorBufferObject[], + stencil_buffer_pool: IStencilBufferObject[], width: number, height: number, msaa: boolean, - idCounter: { attachmentId: number; textureId: number; stencilId: number } + id_counter: { attachmentId: number; textureId: number; stencilId: number } ): IAttachmentObject => { // プールから再利用 - const attachment = attachmentPool.length > 0 - ? attachmentPool.pop()! - : attachmentManagerCreateAttachmentObjectService(idCounter); + const attachment = attachment_pool.length > 0 + ? attachment_pool.pop()! + : attachmentManagerCreateAttachmentObjectService(id_counter); // サイズとフラグを更新 attachment.width = width; @@ -50,16 +50,16 @@ export const execute = ( // ステンシルバッファを取得または作成 const stencil = attachmentManagerGetStencilBufferService( device, - stencilBufferPool, + stencil_buffer_pool, width, height, - idCounter + id_counter ); // カラーバッファを取得または作成(ステンシルを参照) const color = attachmentManagerGetColorBufferService( device, - colorBufferPool, + color_buffer_pool, width, height, stencil @@ -70,11 +70,11 @@ export const execute = ( // テクスチャを取得 const texture = attachmentManagerGetTextureService( device, - texturePool, + texture_pool, width, height, true, - idCounter + id_counter ); attachment.texture = texture; diff --git a/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase.ts b/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase.ts index 6987a867..551edda5 100644 --- a/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase.ts +++ b/packages/webgpu/src/AttachmentManager/usecase/AttachmentManagerReleaseAttachmentUseCase.ts @@ -8,40 +8,40 @@ import { execute as attachmentManagerReleaseTextureService } from "../service/At * @description アタッチメントを解放してプールに返却 * Release attachment and return to pool * - * @param {IAttachmentObject[]} attachmentPool - * @param {Map} texturePool - * @param {IColorBufferObject[]} colorBufferPool - * @param {IStencilBufferObject[]} stencilBufferPool - * @param {IAttachmentObject} attachment + * @param {IAttachmentObject[]} attachment_pool - アタッチメントプール + * @param {Map} texture_pool - テクスチャプール + * @param {IColorBufferObject[]} color_buffer_pool - カラーバッファプール + * @param {IStencilBufferObject[]} stencil_buffer_pool - ステンシルバッファプール + * @param {IAttachmentObject} attachment - 解放するアタッチメント * @return {void} * @method * @protected */ export const execute = ( - attachmentPool: IAttachmentObject[], - texturePool: Map, - colorBufferPool: IColorBufferObject[], - stencilBufferPool: IStencilBufferObject[], + attachment_pool: IAttachmentObject[], + texture_pool: Map, + color_buffer_pool: IColorBufferObject[], + stencil_buffer_pool: IStencilBufferObject[], attachment: IAttachmentObject ): void => { // テクスチャをプールに返却 if (attachment.texture) { - attachmentManagerReleaseTextureService(texturePool, attachment.texture); + attachmentManagerReleaseTextureService(texture_pool, attachment.texture); attachment.texture = null; } // カラーバッファをプールに返却 if (attachment.color) { - colorBufferPool.push(attachment.color); + color_buffer_pool.push(attachment.color); attachment.color = null; } // ステンシルバッファをプールに返却 if (attachment.stencil) { - stencilBufferPool.push(attachment.stencil); + stencil_buffer_pool.push(attachment.stencil); attachment.stencil = null; } // アタッチメントをプールに返却 - attachmentPool.push(attachment); + attachment_pool.push(attachment); }; diff --git a/packages/webgpu/src/BezierConverter/BezierConverter.ts b/packages/webgpu/src/BezierConverter/BezierConverter.ts index 80c670a4..942221e4 100644 --- a/packages/webgpu/src/BezierConverter/BezierConverter.ts +++ b/packages/webgpu/src/BezierConverter/BezierConverter.ts @@ -10,11 +10,16 @@ * * これにより品質を維持しながら不要な計算を削減。 */ +/** + * @description 三次ベジェ曲線を適応的に二次ベジェ曲線群に変換する関数 + * Function to adaptively convert cubic bezier to quadratic bezier segments + */ export { - execute as adaptiveCubicToQuad, - calculateAdaptiveThreshold + execute as adaptiveCubicToQuad } from "./usecase/BezierConverterAdaptiveCubicToQuadUseCase"; -export type { IQuadraticSegment } from "../interface/IQuadraticSegment"; -export { execute as calculateFlatness } from "./service/BezierConverterCalculateFlatnessService"; -export { execute as splitCubic } from "./service/BezierConverterSplitCubicService"; +/** + * @description 二次ベジェ曲線セグメントのインターフェース + * Interface for quadratic bezier curve segment + */ +export type { IQuadraticSegment } from "../interface/IQuadraticSegment"; diff --git a/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.test.ts b/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.test.ts index c31e4d84..42a04cce 100644 --- a/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.test.ts +++ b/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { execute, calculateAdaptiveThreshold } from "./BezierConverterAdaptiveCubicToQuadUseCase"; +import { execute } from "./BezierConverterAdaptiveCubicToQuadUseCase"; describe("BezierConverterAdaptiveCubicToQuadUseCase", () => { @@ -83,38 +83,3 @@ describe("BezierConverterAdaptiveCubicToQuadUseCase", () => } }); }); - -describe("calculateAdaptiveThreshold", () => -{ - it("should return smaller threshold for larger scale", () => - { - const threshold1 = calculateAdaptiveThreshold(1.0); - const threshold2 = calculateAdaptiveThreshold(2.0); - - expect(threshold2).toBeLessThan(threshold1); - }); - - it("should return larger threshold for smaller scale", () => - { - const threshold1 = calculateAdaptiveThreshold(1.0); - const threshold2 = calculateAdaptiveThreshold(0.5); - - expect(threshold2).toBeGreaterThan(threshold1); - }); - - it("should clamp to minimum threshold", () => - { - // 非常に大きなスケールでも最小値を下回らない - // 最小値は0.0625(0.25px squared) - const threshold = calculateAdaptiveThreshold(100.0); - expect(threshold).toBeGreaterThanOrEqual(0.0625); - }); - - it("should clamp to maximum threshold", () => - { - // 非常に小さなスケールでも最大値を超えない - // 最大値は4.0(2px squared) - const threshold = calculateAdaptiveThreshold(0.01); - expect(threshold).toBeLessThanOrEqual(4.0); - }); -}); diff --git a/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.ts b/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.ts index 0072c066..aecbeb74 100644 --- a/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.ts +++ b/packages/webgpu/src/BezierConverter/usecase/BezierConverterAdaptiveCubicToQuadUseCase.ts @@ -32,7 +32,7 @@ const MAX_RECURSION_DEPTH = 8; * @param {IPoint} p1 - 制御点1 * @param {IPoint} p2 - 制御点2 * @param {IPoint} p3 - 終点 - * @param {number} flatnessThreshold - フラットネス閾値(オプション) + * @param {number} flatness_threshold - フラットネス閾値(オプション) * @return {IQuadraticSegment[]} 二次ベジェ曲線のセグメント配列 */ export const execute = ( @@ -40,12 +40,21 @@ export const execute = ( p1: IPoint, p2: IPoint, p3: IPoint, - flatnessThreshold: number = DEFAULT_FLATNESS_THRESHOLD + flatness_threshold: number = DEFAULT_FLATNESS_THRESHOLD ): IQuadraticSegment[] => { const result: IQuadraticSegment[] = []; - // 再帰的に分割を行う内部関数 + /** + * @description 再帰的に三次ベジェ曲線を分割し、二次ベジェ曲線に近似する内部関数 + * Internal recursive function that subdivides cubic bezier and approximates with quadratic bezier + * @param {IPoint} start - 始点 + * @param {IPoint} ctrl1 - 制御点1 + * @param {IPoint} ctrl2 - 制御点2 + * @param {IPoint} end - 終点 + * @param {number} depth - 現在の再帰深度 + * @return {void} + */ const subdivide = ( start: IPoint, ctrl1: IPoint, @@ -58,7 +67,7 @@ export const execute = ( const flatness = calculateFlatness(start, ctrl1, ctrl2, end); // フラットネスが閾値以下、または最大深度に達した場合は近似 - if (flatness <= flatnessThreshold || depth >= MAX_RECURSION_DEPTH) { + if (flatness <= flatness_threshold || depth >= MAX_RECURSION_DEPTH) { // 三次ベジェを二次ベジェに近似 // WebGL版と同じ: 分割後は単純に2つの制御点の中点を使用 const ctrl: IPoint = { @@ -99,27 +108,3 @@ export const execute = ( return result; }; - -/** - * @description スケールに応じたフラットネス閾値を計算 - * Calculate flatness threshold based on scale - * - * ズームレベルが高い場合は高品質な近似が必要。 - * スケール = sqrt(matrix[0]^2 + matrix[1]^2) などで計算可能。 - * - * @param {number} scale - 現在のスケール - * @return {number} 調整されたフラットネス閾値 - */ -export const calculateAdaptiveThreshold = (scale: number): number => { - // スケールが大きい場合は閾値を小さくして高品質に - // スケールが小さい場合は閾値を大きくしてパフォーマンス優先 - const baseThreshold = DEFAULT_FLATNESS_THRESHOLD; - - // スケールの逆数に比例した閾値 - // 最小値と最大値を設定して極端な値を防ぐ - const adjustedThreshold = baseThreshold / (scale * scale); - - // 閾値の範囲を制限(0.0625〜4.0) - // 0.0625 = 0.25px squared, 4.0 = 2px squared - return Math.max(0.0625, Math.min(4.0, adjustedThreshold)); -}; diff --git a/packages/webgpu/src/Blend.test.ts b/packages/webgpu/src/Blend.test.ts index 53359038..3d80fa6a 100644 --- a/packages/webgpu/src/Blend.test.ts +++ b/packages/webgpu/src/Blend.test.ts @@ -2,8 +2,6 @@ import { describe, it, expect, beforeEach } from "vitest"; import { $setCurrentBlendMode, $currentBlendMode, - $setFuncCode, - $funcCode, $getBlendState } from "./Blend"; @@ -12,7 +10,6 @@ describe("Blend", () => beforeEach(() => { $setCurrentBlendMode("normal"); - $setFuncCode(0); }); describe("blend mode", () => @@ -42,24 +39,6 @@ describe("Blend", () => }); }); - describe("func code", () => - { - it("should default to 0", () => - { - $setFuncCode(0); - expect($funcCode).toBe(0); - }); - - it("should set and get func code", () => - { - $setFuncCode(123); - expect($funcCode).toBe(123); - - $setFuncCode(456); - expect($funcCode).toBe(456); - }); - }); - describe("$getBlendState", () => { it("should return normal blend state", () => diff --git a/packages/webgpu/src/Blend.ts b/packages/webgpu/src/Blend.ts index 2dfe18c8..e0ae0925 100644 --- a/packages/webgpu/src/Blend.ts +++ b/packages/webgpu/src/Blend.ts @@ -3,20 +3,33 @@ import type { IBlendState } from "./interface/IBlendState"; export type { IBlendState }; +/** + * @description 現在のブレンドモード + * The current blend mode used for rendering + * + * @type {IBlendMode} + */ export let $currentBlendMode: IBlendMode = "normal"; -export let $funcCode: number = 0; - +/** + * @description 現在のブレンドモードを設定する + * Set the current blend mode + * + * @param {IBlendMode} blend_mode - ブレンドモード / blend mode to apply + * @return {void} + */ export const $setCurrentBlendMode = (blend_mode: IBlendMode): void => { $currentBlendMode = blend_mode; }; -export const $setFuncCode = (code: number): void => -{ - $funcCode = code; -}; - +/** + * @description 指定されたブレンドモードに対応するWebGPUブレンドステートを返す + * Returns the WebGPU blend state configuration for the given blend mode + * + * @param {IBlendMode} mode - ブレンドモード / blend mode + * @return {IBlendState} + */ export const $getBlendState = (mode: IBlendMode): IBlendState => { switch (mode) { diff --git a/packages/webgpu/src/Blend/BlendInstancedManager.ts b/packages/webgpu/src/Blend/BlendInstancedManager.ts index 8d9438d7..6aad5917 100644 --- a/packages/webgpu/src/Blend/BlendInstancedManager.ts +++ b/packages/webgpu/src/Blend/BlendInstancedManager.ts @@ -8,28 +8,37 @@ import { $context } from "../WebGPUUtil"; /** * @description シンプルなブレンドモード(インスタンス描画可能) + * Simple blend modes that support instanced rendering + * @type {ReadonlySet} */ -const SIMPLE_BLEND_MODES: ReadonlySet = new Set([ +const $SIMPLE_BLEND_MODES: ReadonlySet = new Set([ "normal", "layer", "add", "screen", "alpha", "erase", "copy" ]); /** * @description 複雑なブレンドモード描画キュー + * Queue for complex blend mode draw items + * @type {IComplexBlendItem[]} */ const $complexBlendQueue: IComplexBlendItem[] = []; /** * @description Float32Array(8) プール(color_transform 用) + * Object pool for Float32Array(8) used by color transforms + * @type {Float32Array[]} */ const $ct8Pool: Float32Array[] = []; /** * @description Float32Array(9) プール(matrix 用) + * Object pool for Float32Array(9) used by matrices + * @type {Float32Array[]} */ const $m9Pool: Float32Array[] = []; /** * @description 複雑なブレンドモードの描画キューを取得 + * Returns the queue of complex blend mode draw items * @return {IComplexBlendItem[]} */ export const getComplexBlendQueue = (): IComplexBlendItem[] => @@ -38,7 +47,8 @@ export const getComplexBlendQueue = (): IComplexBlendItem[] => }; /** - * @description 複雑なブレンドモードの描画キューをクリア + * @description 複雑なブレンドモードの描画キューをクリアし、プールへ返却する + * Clears the complex blend queue and returns arrays to their pools * @return {void} */ export const clearComplexBlendQueue = (): void => @@ -54,37 +64,40 @@ export const clearComplexBlendQueue = (): void => /** * @description インスタンスシェーダーマネージャーのキャッシュ - * @private + * Cache map for instanced shader managers + * @type {Map} */ -const shaderManagers = new Map(); +const $shaderManagers = new Map(); /** - * @description インスタンスシェーダーマネージャーを取得 + * @description インスタンスシェーダーマネージャーを取得(なければ生成) + * Gets or creates the instanced shader manager * @return {ShaderInstancedManager} */ export const getInstancedShaderManager = (): ShaderInstancedManager => { const key = "blend_instanced"; - if (!shaderManagers.has(key)) { - shaderManagers.set(key, new ShaderInstancedManager()); + if (!$shaderManagers.has(key)) { + $shaderManagers.set(key, new ShaderInstancedManager()); } - return shaderManagers.get(key)!; + return $shaderManagers.get(key)!; }; /** - * @description DisplayObject単体の描画をインスタンス配列に追加 - * @param {Node} node - * @param {number} x_min - * @param {number} y_min - * @param {number} x_max - * @param {number} y_max - * @param {Float32Array} color_transform - * @param {Float32Array} matrix - * @param {string} blend_mode - * @param {number} viewport_width - * @param {number} viewport_height - * @param {number} render_max_size - * @param {number} global_alpha + * @description DisplayObject単体の描画をインスタンス配列に追加する + * Adds a single DisplayObject's draw data to the instanced array + * @param {Node} node - テクスチャアトラスノード / Texture atlas node + * @param {number} x_min - バウンディングボックス左端 / Bounding box left edge + * @param {number} y_min - バウンディングボックス上端 / Bounding box top edge + * @param {number} x_max - バウンディングボックス右端 / Bounding box right edge + * @param {number} y_max - バウンディングボックス下端 / Bounding box bottom edge + * @param {Float32Array} color_transform - カラートランスフォーム配列 / Color transform array + * @param {Float32Array} matrix - 変換行列配列 / Transformation matrix array + * @param {string} blend_mode - ブレンドモード名 / Blend mode name + * @param {number} viewport_width - ビューポート幅 / Viewport width + * @param {number} viewport_height - ビューポート高さ / Viewport height + * @param {number} render_max_size - レンダーテクスチャ最大サイズ / Render texture max size + * @param {number} global_alpha - グローバルアルファ値 / Global alpha value * @return {void} */ export const addDisplayObjectToInstanceArray = ( @@ -112,7 +125,7 @@ export const addDisplayObjectToInstanceArray = ( const ct6 = color_transform[6] / 255; const ct7 = 0; - if (SIMPLE_BLEND_MODES.has(blend_mode)) { + if ($SIMPLE_BLEND_MODES.has(blend_mode)) { // ブレンドモードまたはアトラスインデックスが変わった場合 if ($currentBlendMode !== blend_mode || $getCurrentAtlasIndex() !== node.index) { // 異なるブレンドモード/アトラスになるので、切り替え前にバッチを描画 diff --git a/packages/webgpu/src/Blend/service/BlendAddService.test.ts b/packages/webgpu/src/Blend/service/BlendAddService.test.ts deleted file mode 100644 index 02261765..00000000 --- a/packages/webgpu/src/Blend/service/BlendAddService.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { execute } from "./BlendAddService"; -import { $setFuncCode, $funcCode } from "../../Blend"; - -describe("BlendAddService", () => -{ - beforeEach(() => - { - // Reset to non-add state - $setFuncCode(0); - }); - - it("should return true when func code is different", () => - { - $setFuncCode(0); - - const result = execute(); - - expect(result).toBe(true); - }); - - it("should set func code to 101 (add)", () => - { - $setFuncCode(0); - - execute(); - - expect($funcCode).toBe(101); - }); - - it("should return false when already set to add (101)", () => - { - $setFuncCode(101); - - const result = execute(); - - expect(result).toBe(false); - }); - - it("should not change func code when already 101", () => - { - $setFuncCode(101); - - execute(); - - expect($funcCode).toBe(101); - }); - - it("should return true when changing from another mode", () => - { - $setFuncCode(301); // screen mode - - const result = execute(); - - expect(result).toBe(true); - expect($funcCode).toBe(101); - }); - - it("should return true when changing from normal mode", () => - { - $setFuncCode(613); // normal mode - - const result = execute(); - - expect(result).toBe(true); - }); -}); diff --git a/packages/webgpu/src/Blend/service/BlendAddService.ts b/packages/webgpu/src/Blend/service/BlendAddService.ts deleted file mode 100644 index 07affca6..00000000 --- a/packages/webgpu/src/Blend/service/BlendAddService.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - $setFuncCode, - $funcCode -} from "../../Blend"; - -export const execute = (): boolean => -{ - if ($funcCode !== 101) { - $setFuncCode(101); - return true; - } - return false; -}; diff --git a/packages/webgpu/src/Blend/service/BlendAlphaService.test.ts b/packages/webgpu/src/Blend/service/BlendAlphaService.test.ts deleted file mode 100644 index 56edb947..00000000 --- a/packages/webgpu/src/Blend/service/BlendAlphaService.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { execute } from "./BlendAlphaService"; -import { $setFuncCode, $funcCode } from "../../Blend"; - -describe("BlendAlphaService", () => -{ - beforeEach(() => - { - $setFuncCode(0); - }); - - it("should return true when func code is different", () => - { - $setFuncCode(0); - - const result = execute(); - - expect(result).toBe(true); - }); - - it("should set func code to 401 (alpha)", () => - { - $setFuncCode(0); - - execute(); - - expect($funcCode).toBe(401); - }); - - it("should return false when already set to alpha (401)", () => - { - $setFuncCode(401); - - const result = execute(); - - expect(result).toBe(false); - }); - - it("should not change func code when already 401", () => - { - $setFuncCode(401); - - execute(); - - expect($funcCode).toBe(401); - }); - - it("should return true when changing from normal mode", () => - { - $setFuncCode(613); // normal mode - - const result = execute(); - - expect(result).toBe(true); - expect($funcCode).toBe(401); - }); -}); diff --git a/packages/webgpu/src/Blend/service/BlendAlphaService.ts b/packages/webgpu/src/Blend/service/BlendAlphaService.ts deleted file mode 100644 index 7f5617aa..00000000 --- a/packages/webgpu/src/Blend/service/BlendAlphaService.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - $setFuncCode, - $funcCode -} from "../../Blend"; - -export const execute = (): boolean => -{ - if ($funcCode !== 401) { - $setFuncCode(401); - return true; - } - return false; -}; diff --git a/packages/webgpu/src/Blend/service/BlendEraseService.test.ts b/packages/webgpu/src/Blend/service/BlendEraseService.test.ts deleted file mode 100644 index 196dd414..00000000 --- a/packages/webgpu/src/Blend/service/BlendEraseService.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { execute } from "./BlendEraseService"; -import { $setFuncCode, $funcCode } from "../../Blend"; - -describe("BlendEraseService", () => -{ - beforeEach(() => - { - $setFuncCode(0); - }); - - it("should return true when func code is different", () => - { - $setFuncCode(0); - - const result = execute(); - - expect(result).toBe(true); - }); - - it("should set func code to 501 (erase)", () => - { - $setFuncCode(0); - - execute(); - - expect($funcCode).toBe(501); - }); - - it("should return false when already set to erase (501)", () => - { - $setFuncCode(501); - - const result = execute(); - - expect(result).toBe(false); - }); - - it("should not change func code when already 501", () => - { - $setFuncCode(501); - - execute(); - - expect($funcCode).toBe(501); - }); - - it("should return true when changing from screen mode", () => - { - $setFuncCode(301); // screen mode - - const result = execute(); - - expect(result).toBe(true); - expect($funcCode).toBe(501); - }); -}); diff --git a/packages/webgpu/src/Blend/service/BlendEraseService.ts b/packages/webgpu/src/Blend/service/BlendEraseService.ts deleted file mode 100644 index 86ee87fd..00000000 --- a/packages/webgpu/src/Blend/service/BlendEraseService.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - $setFuncCode, - $funcCode -} from "../../Blend"; - -export const execute = (): boolean => -{ - if ($funcCode !== 501) { - $setFuncCode(501); - return true; - } - return false; -}; diff --git a/packages/webgpu/src/Blend/service/BlendGetStateService.test.ts b/packages/webgpu/src/Blend/service/BlendGetStateService.test.ts deleted file mode 100644 index dde24480..00000000 --- a/packages/webgpu/src/Blend/service/BlendGetStateService.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { execute } from "./BlendGetStateService"; - -describe("BlendGetStateService", () => -{ - describe("normal blend mode", () => - { - it("should return correct blend state for normal mode", () => - { - const result = execute("normal"); - - expect(result.color.srcFactor).toBe("one"); - expect(result.color.dstFactor).toBe("one-minus-src-alpha"); - expect(result.color.operation).toBe("add"); - expect(result.alpha.srcFactor).toBe("one"); - expect(result.alpha.dstFactor).toBe("one-minus-src-alpha"); - expect(result.alpha.operation).toBe("add"); - }); - }); - - describe("add blend mode", () => - { - it("should return correct blend state for add mode", () => - { - const result = execute("add"); - - expect(result.color.srcFactor).toBe("one"); - expect(result.color.dstFactor).toBe("one"); - expect(result.color.operation).toBe("add"); - expect(result.alpha.srcFactor).toBe("one"); - expect(result.alpha.dstFactor).toBe("one-minus-src-alpha"); - }); - }); - - describe("screen blend mode", () => - { - it("should return correct blend state for screen mode", () => - { - const result = execute("screen"); - - expect(result.color.srcFactor).toBe("one-minus-dst"); - expect(result.color.dstFactor).toBe("one"); - expect(result.color.operation).toBe("add"); - }); - }); - - describe("alpha blend mode", () => - { - it("should return correct blend state for alpha mode", () => - { - const result = execute("alpha"); - - expect(result.color.srcFactor).toBe("zero"); - expect(result.color.dstFactor).toBe("src-alpha"); - expect(result.color.operation).toBe("add"); - expect(result.alpha.srcFactor).toBe("zero"); - expect(result.alpha.dstFactor).toBe("src-alpha"); - }); - }); - - describe("erase blend mode", () => - { - it("should return correct blend state for erase mode", () => - { - const result = execute("erase"); - - expect(result.color.srcFactor).toBe("zero"); - expect(result.color.dstFactor).toBe("one-minus-src-alpha"); - expect(result.color.operation).toBe("add"); - expect(result.alpha.srcFactor).toBe("zero"); - expect(result.alpha.dstFactor).toBe("one-minus-src-alpha"); - }); - }); - - describe("copy blend mode", () => - { - it("should return correct blend state for copy mode", () => - { - const result = execute("copy"); - - expect(result.color.srcFactor).toBe("one"); - expect(result.color.dstFactor).toBe("zero"); - expect(result.color.operation).toBe("add"); - expect(result.alpha.srcFactor).toBe("one"); - expect(result.alpha.dstFactor).toBe("zero"); - }); - }); - - describe("default behavior", () => - { - it("should return normal state for unknown mode", () => - { - const result = execute("unknown" as any); - - expect(result.color.srcFactor).toBe("one"); - expect(result.color.dstFactor).toBe("one-minus-src-alpha"); - }); - }); -}); diff --git a/packages/webgpu/src/Blend/service/BlendGetStateService.ts b/packages/webgpu/src/Blend/service/BlendGetStateService.ts deleted file mode 100644 index 5f8535eb..00000000 --- a/packages/webgpu/src/Blend/service/BlendGetStateService.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { IBlendMode } from "../../interface/IBlendMode"; -import type { IBlendState } from "../../Blend"; -import { $getBlendState } from "../../Blend"; - -/** - * @description ブレンドモードからWebGPUブレンドステートを取得するサービス - * Service to get WebGPU blend state from blend mode - * - * @param {IBlendMode} mode - * @return {IBlendState} - * @method - * @protected - */ -export const execute = (mode: IBlendMode): IBlendState => -{ - return $getBlendState(mode); -}; diff --git a/packages/webgpu/src/Blend/service/BlendOneZeroService.test.ts b/packages/webgpu/src/Blend/service/BlendOneZeroService.test.ts deleted file mode 100644 index 4b4083fe..00000000 --- a/packages/webgpu/src/Blend/service/BlendOneZeroService.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { execute } from "./BlendOneZeroService"; -import { $setFuncCode, $funcCode } from "../../Blend"; - -describe("BlendOneZeroService", () => -{ - beforeEach(() => - { - $setFuncCode(0); - }); - - it("should return true when func code is different", () => - { - $setFuncCode(100); - - const result = execute(); - - expect(result).toBe(true); - }); - - it("should set func code to 10 (copy/one-zero)", () => - { - $setFuncCode(100); - - execute(); - - expect($funcCode).toBe(10); - }); - - it("should return false when already set to one-zero (10)", () => - { - $setFuncCode(10); - - const result = execute(); - - expect(result).toBe(false); - }); - - it("should not change func code when already 10", () => - { - $setFuncCode(10); - - execute(); - - expect($funcCode).toBe(10); - }); - - it("should return true when changing from normal mode", () => - { - $setFuncCode(613); // normal mode - - const result = execute(); - - expect(result).toBe(true); - expect($funcCode).toBe(10); - }); -}); diff --git a/packages/webgpu/src/Blend/service/BlendOneZeroService.ts b/packages/webgpu/src/Blend/service/BlendOneZeroService.ts deleted file mode 100644 index 108cac13..00000000 --- a/packages/webgpu/src/Blend/service/BlendOneZeroService.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - $setFuncCode, - $funcCode -} from "../../Blend"; - -export const execute = (): boolean => -{ - if ($funcCode !== 10) { - $setFuncCode(10); - return true; - } - return false; -}; diff --git a/packages/webgpu/src/Blend/service/BlendResetService.test.ts b/packages/webgpu/src/Blend/service/BlendResetService.test.ts deleted file mode 100644 index c9ccf537..00000000 --- a/packages/webgpu/src/Blend/service/BlendResetService.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { execute } from "./BlendResetService"; -import { $setFuncCode, $funcCode } from "../../Blend"; - -describe("BlendResetService", () => -{ - beforeEach(() => - { - $setFuncCode(0); - }); - - it("should return true when func code is not 613 (normal)", () => - { - $setFuncCode(101); // add mode - - const result = execute(); - - expect(result).toBe(true); - }); - - it("should set func code to 613 (normal)", () => - { - $setFuncCode(101); // add mode - - execute(); - - expect($funcCode).toBe(613); - }); - - it("should return false when already set to normal (613)", () => - { - $setFuncCode(613); - - const result = execute(); - - expect(result).toBe(false); - }); - - it("should not change func code when already 613", () => - { - $setFuncCode(613); - - execute(); - - expect($funcCode).toBe(613); - }); - - it("should return true when resetting from screen mode", () => - { - $setFuncCode(301); // screen mode - - const result = execute(); - - expect(result).toBe(true); - expect($funcCode).toBe(613); - }); - - it("should return true when resetting from erase mode", () => - { - $setFuncCode(501); // erase mode - - const result = execute(); - - expect(result).toBe(true); - expect($funcCode).toBe(613); - }); - - it("should return true when resetting from one-zero mode", () => - { - $setFuncCode(10); // one-zero (copy) mode - - const result = execute(); - - expect(result).toBe(true); - expect($funcCode).toBe(613); - }); -}); diff --git a/packages/webgpu/src/Blend/service/BlendResetService.ts b/packages/webgpu/src/Blend/service/BlendResetService.ts deleted file mode 100644 index aff3f081..00000000 --- a/packages/webgpu/src/Blend/service/BlendResetService.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - $setFuncCode, - $funcCode -} from "../../Blend"; - -export const execute = (): boolean => -{ - if ($funcCode !== 613) { - $setFuncCode(613); - return true; - } - return false; -}; diff --git a/packages/webgpu/src/Blend/service/BlendScreenService.test.ts b/packages/webgpu/src/Blend/service/BlendScreenService.test.ts deleted file mode 100644 index ad266f30..00000000 --- a/packages/webgpu/src/Blend/service/BlendScreenService.test.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { execute } from "./BlendScreenService"; -import { $setFuncCode, $funcCode } from "../../Blend"; - -describe("BlendScreenService", () => -{ - beforeEach(() => - { - $setFuncCode(0); - }); - - it("should return true when func code is different", () => - { - $setFuncCode(0); - - const result = execute(); - - expect(result).toBe(true); - }); - - it("should set func code to 301 (screen)", () => - { - $setFuncCode(0); - - execute(); - - expect($funcCode).toBe(301); - }); - - it("should return false when already set to screen (301)", () => - { - $setFuncCode(301); - - const result = execute(); - - expect(result).toBe(false); - }); - - it("should not change func code when already 301", () => - { - $setFuncCode(301); - - execute(); - - expect($funcCode).toBe(301); - }); - - it("should return true when changing from add mode", () => - { - $setFuncCode(101); // add mode - - const result = execute(); - - expect(result).toBe(true); - expect($funcCode).toBe(301); - }); -}); diff --git a/packages/webgpu/src/Blend/service/BlendScreenService.ts b/packages/webgpu/src/Blend/service/BlendScreenService.ts deleted file mode 100644 index 53acf82c..00000000 --- a/packages/webgpu/src/Blend/service/BlendScreenService.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { - $setFuncCode, - $funcCode -} from "../../Blend"; - -export const execute = (): boolean => -{ - if ($funcCode !== 301) { - $setFuncCode(301); - return true; - } - return false; -}; diff --git a/packages/webgpu/src/Blend/service/BlendSetModeService.test.ts b/packages/webgpu/src/Blend/service/BlendSetModeService.test.ts deleted file mode 100644 index b8b0d3f5..00000000 --- a/packages/webgpu/src/Blend/service/BlendSetModeService.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { describe, it, expect, beforeEach } from "vitest"; -import { execute } from "./BlendSetModeService"; -import { $currentBlendMode, $setCurrentBlendMode } from "../../Blend"; - -describe("BlendSetModeService", () => -{ - beforeEach(() => - { - $setCurrentBlendMode("normal"); - }); - - it("should set blend mode to normal", () => - { - execute("normal"); - - expect($currentBlendMode).toBe("normal"); - }); - - it("should set blend mode to add", () => - { - execute("add"); - - expect($currentBlendMode).toBe("add"); - }); - - it("should set blend mode to screen", () => - { - execute("screen"); - - expect($currentBlendMode).toBe("screen"); - }); - - it("should set blend mode to alpha", () => - { - execute("alpha"); - - expect($currentBlendMode).toBe("alpha"); - }); - - it("should set blend mode to erase", () => - { - execute("erase"); - - expect($currentBlendMode).toBe("erase"); - }); - - it("should set blend mode to copy", () => - { - execute("copy"); - - expect($currentBlendMode).toBe("copy"); - }); - - it("should change mode from one to another", () => - { - execute("add"); - expect($currentBlendMode).toBe("add"); - - execute("screen"); - expect($currentBlendMode).toBe("screen"); - - execute("normal"); - expect($currentBlendMode).toBe("normal"); - }); -}); diff --git a/packages/webgpu/src/Blend/service/BlendSetModeService.ts b/packages/webgpu/src/Blend/service/BlendSetModeService.ts deleted file mode 100644 index 3162d9f5..00000000 --- a/packages/webgpu/src/Blend/service/BlendSetModeService.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { IBlendMode } from "../../interface/IBlendMode"; -import { $setCurrentBlendMode } from "../../Blend"; - -export const execute = (mode: IBlendMode): void => -{ - $setCurrentBlendMode(mode); -}; diff --git a/packages/webgpu/src/Blend/usecase/BlendApplyComplexBlendUseCase.ts b/packages/webgpu/src/Blend/usecase/BlendApplyComplexBlendUseCase.ts index 8c7e5c5f..4f4c4ecc 100644 --- a/packages/webgpu/src/Blend/usecase/BlendApplyComplexBlendUseCase.ts +++ b/packages/webgpu/src/Blend/usecase/BlendApplyComplexBlendUseCase.ts @@ -4,11 +4,15 @@ import { ShaderSource } from "../../Shader/ShaderSource"; /** * @description プリアロケートされた uniform データ (12 floats = 48 bytes) + * Pre-allocated uniform data array (12 floats = 48 bytes) + * @type {Float32Array} */ const $uniform12 = new Float32Array(12); /** * @description プリアロケートされた BindGroupEntry 配列 (4 bindings) + * Pre-allocated BindGroupEntry array (4 bindings) + * @type {GPUBindGroupEntry[]} */ const $entries4: GPUBindGroupEntry[] = [ { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, @@ -18,21 +22,28 @@ const $entries4: GPUBindGroupEntry[] = [ ]; /** - * @description 複雑なブレンドモードを適用 + * @description 複雑なブレンドモードを適用し、ブレンド結果のアタッチメントを返す + * Applies a complex blend mode and returns the resulting attachment + * @param {IAttachmentObject} src_attachment - ソースアタッチメント / Source attachment + * @param {IAttachmentObject} dst_attachment - デスティネーションアタッチメント / Destination attachment + * @param {string} blend_mode - ブレンドモード名 / Blend mode name + * @param {Float32Array} color_transform - カラートランスフォーム配列 / Color transform array + * @param {IFilterConfig} config - フィルター設定 / Filter configuration + * @return {IAttachmentObject} */ export const execute = ( - srcAttachment: IAttachmentObject, - dstAttachment: IAttachmentObject, - blendMode: string, - colorTransform: Float32Array, + src_attachment: IAttachmentObject, + dst_attachment: IAttachmentObject, + blend_mode: string, + color_transform: Float32Array, config: IFilterConfig ): IAttachmentObject => { const { device, commandEncoder, frameBufferManager, pipelineManager, textureManager } = config; // 出力サイズは両方の大きい方を使用 - const width = Math.max(srcAttachment.width, dstAttachment.width); - const height = Math.max(srcAttachment.height, dstAttachment.height); + const width = Math.max(src_attachment.width, dst_attachment.width); + const height = Math.max(src_attachment.height, dst_attachment.height); // 出力アタッチメントを作成 const destAttachment = frameBufferManager.createTemporaryAttachment(width, height); @@ -42,9 +53,9 @@ export const execute = ( const bindGroupLayout = pipelineManager.getBindGroupLayout("complex_blend"); if (!pipeline || !bindGroupLayout) { - console.error(`[WebGPU ComplexBlend] Pipeline not found for blend mode: ${blendMode}`); + console.error(`[WebGPU ComplexBlend] Pipeline not found for blend mode: ${blend_mode}`); // フォールバック: srcをそのまま返す - return srcAttachment; + return src_attachment; } // サンプラーを作成 @@ -55,15 +66,15 @@ export const execute = ( // addColor: vec4 (16 bytes) // blendMode: f32 + padding: vec3 (16 bytes) // Total: 48 bytes - const blendModeIndex = ShaderSource.getBlendModeIndex(blendMode); - $uniform12[0] = colorTransform[0]; - $uniform12[1] = colorTransform[1]; - $uniform12[2] = colorTransform[2]; - $uniform12[3] = colorTransform[3]; - $uniform12[4] = colorTransform[4]; - $uniform12[5] = colorTransform[5]; - $uniform12[6] = colorTransform[6]; - $uniform12[7] = colorTransform[7]; + const blendModeIndex = ShaderSource.getBlendModeIndex(blend_mode); + $uniform12[0] = color_transform[0]; + $uniform12[1] = color_transform[1]; + $uniform12[2] = color_transform[2]; + $uniform12[3] = color_transform[3]; + $uniform12[4] = color_transform[4]; + $uniform12[5] = color_transform[5]; + $uniform12[6] = color_transform[6]; + $uniform12[7] = color_transform[7]; $uniform12[8] = blendModeIndex; $uniform12[9] = 0; $uniform12[10] = 0; @@ -82,8 +93,8 @@ export const execute = ( // バインドグループを作成 ($entries4[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries4[1].resource = sampler; - $entries4[2].resource = dstAttachment.texture!.view; - $entries4[3].resource = srcAttachment.texture!.view; + $entries4[2].resource = dst_attachment.texture!.view; + $entries4[3].resource = src_attachment.texture!.view; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries4 diff --git a/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.test.ts b/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.test.ts deleted file mode 100644 index 3d74804c..00000000 --- a/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { execute } from "./BlendOperationUseCase"; -import { describe, expect, it, beforeEach } from "vitest"; -import { $setFuncCode } from "../../Blend"; - -describe("BlendOperationUseCase.ts method test", () => -{ - beforeEach(() => - { - // Reset func code before each test - $setFuncCode(0); - }); - - it("test case - add blend mode", () => - { - const changed = execute("add"); - expect(changed).toBe(true); - - // Second call should return false (no change) - const changed2 = execute("add"); - expect(changed2).toBe(false); - }); - - it("test case - screen blend mode", () => - { - const changed = execute("screen"); - expect(changed).toBe(true); - - const changed2 = execute("screen"); - expect(changed2).toBe(false); - }); - - it("test case - alpha blend mode", () => - { - const changed = execute("alpha"); - expect(changed).toBe(true); - - const changed2 = execute("alpha"); - expect(changed2).toBe(false); - }); - - it("test case - erase blend mode", () => - { - const changed = execute("erase"); - expect(changed).toBe(true); - - const changed2 = execute("erase"); - expect(changed2).toBe(false); - }); - - it("test case - copy blend mode", () => - { - const changed = execute("copy"); - expect(changed).toBe(true); - - const changed2 = execute("copy"); - expect(changed2).toBe(false); - }); - - it("test case - normal blend mode (default)", () => - { - const changed = execute("normal"); - expect(changed).toBe(true); - - const changed2 = execute("normal"); - expect(changed2).toBe(false); - }); - - it("test case - switching between modes", () => - { - execute("add"); - const changed = execute("screen"); - expect(changed).toBe(true); - - const changed2 = execute("normal"); - expect(changed2).toBe(true); - }); -}); diff --git a/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.ts b/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.ts deleted file mode 100644 index d7101eb0..00000000 --- a/packages/webgpu/src/Blend/usecase/BlendOperationUseCase.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { IBlendMode } from "../../interface/IBlendMode"; -import { execute as blendAddService } from "../service/BlendAddService"; -import { execute as blendResetService } from "../service/BlendResetService"; -import { execute as blendScreenService } from "../service/BlendScreenService"; -import { execute as blendAlphaService } from "../service/BlendAlphaService"; -import { execute as blendEraseService } from "../service/BlendEraseService"; -import { execute as blendOneZeroService } from "../service/BlendOneZeroService"; - -/** - * @description 設定されたブレンドモードへ切り替える - * Switch to the set blend mode - * - * @param {IBlendMode} operation - * @return {boolean} ブレンドモードが変更されたかどうか - * @method - * @protected - */ -export const execute = (operation: IBlendMode): boolean => -{ - switch (operation) { - - case "add": - return blendAddService(); - - case "screen": - return blendScreenService(); - - case "alpha": - return blendAlphaService(); - - case "erase": - return blendEraseService(); - - case "copy": - return blendOneZeroService(); - - default: - return blendResetService(); - - } -}; diff --git a/packages/webgpu/src/BufferManager.test.ts b/packages/webgpu/src/BufferManager.test.ts index a557244a..cef33579 100644 --- a/packages/webgpu/src/BufferManager.test.ts +++ b/packages/webgpu/src/BufferManager.test.ts @@ -59,13 +59,6 @@ vi.mock("./BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase", () = }) })); -vi.mock("./BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase", () => ({ - "execute": vi.fn((pool, buffer) => { - const entry = pool.find((e: any) => e.buffer === buffer); - if (entry) entry.inUse = false; - }) -})); - vi.mock("./BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase", () => ({ "execute": vi.fn() })); @@ -110,121 +103,6 @@ describe("BufferManager", () => expect(manager).toBeDefined(); }); - it("should initialize with zero frame number", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - expect(manager.getFrameNumber()).toBe(0); - }); - - it("should initialize with empty pool stats", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - const stats = manager.getPoolStats(); - expect(stats.vertexPoolSize).toBe(0); - expect(stats.uniformPoolSize).toBe(0); - }); - }); - - describe("createVertexBuffer", () => - { - it("should create vertex buffer with data", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - const data = new Float32Array([1, 2, 3, 4]); - - const buffer = manager.createVertexBuffer("test", data); - - expect(buffer).toBeDefined(); - expect(device.createBuffer).toHaveBeenCalled(); - }); - - it("should store buffer by name", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - const data = new Float32Array([1, 2, 3]); - - manager.createVertexBuffer("myBuffer", data); - const retrieved = manager.getVertexBuffer("myBuffer"); - - expect(retrieved).toBeDefined(); - }); - }); - - describe("createUniformBuffer", () => - { - it("should create uniform buffer with specified size", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - const buffer = manager.createUniformBuffer("uniforms", 64); - - expect(buffer).toBeDefined(); - expect(device.createBuffer).toHaveBeenCalled(); - }); - - it("should store buffer by name", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.createUniformBuffer("myUniforms", 128); - const retrieved = manager.getUniformBuffer("myUniforms"); - - expect(retrieved).toBeDefined(); - }); - }); - - describe("updateUniformBuffer", () => - { - it("should write data to existing buffer", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - const data = new Float32Array([1, 2, 3, 4]); - - manager.createUniformBuffer("uniforms", 64); - manager.updateUniformBuffer("uniforms", data); - - expect(device.queue.writeBuffer).toHaveBeenCalled(); - }); - - it("should not throw when buffer does not exist", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - const data = new Float32Array([1, 2, 3, 4]); - - expect(() => manager.updateUniformBuffer("nonexistent", data)).not.toThrow(); - }); - }); - - describe("getVertexBuffer", () => - { - it("should return undefined for non-existent buffer", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - expect(manager.getVertexBuffer("nonexistent")).toBeUndefined(); - }); - }); - - describe("getUniformBuffer", () => - { - it("should return undefined for non-existent buffer", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - expect(manager.getUniformBuffer("nonexistent")).toBeUndefined(); - }); }); describe("createRectVertices", () => @@ -253,28 +131,6 @@ describe("BufferManager", () => expect(buffer).toBeDefined(); }); - it("should update pool stats", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.acquireVertexBuffer(256); - - const stats = manager.getPoolStats(); - expect(stats.vertexPoolSize).toBe(1); - }); - }); - - describe("releaseVertexBuffer", () => - { - it("should release buffer back to pool", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - const buffer = manager.acquireVertexBuffer(256); - - expect(() => manager.releaseVertexBuffer(buffer)).not.toThrow(); - }); }); describe("acquireUniformBuffer", () => @@ -289,85 +145,18 @@ describe("BufferManager", () => expect(buffer).toBeDefined(); }); - it("should update pool stats", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.acquireUniformBuffer(64); - - const stats = manager.getPoolStats(); - expect(stats.uniformPoolSize).toBe(1); - }); - }); - - describe("releaseUniformBuffer", () => - { - it("should release buffer back to pool", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - const buffer = manager.acquireUniformBuffer(64); - - expect(() => manager.releaseUniformBuffer(buffer)).not.toThrow(); - }); - }); - - describe("destroyBuffer", () => - { - it("should destroy vertex buffer by name", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.createVertexBuffer("test", new Float32Array([1, 2, 3])); - manager.destroyBuffer("test"); - - expect(manager.getVertexBuffer("test")).toBeUndefined(); - }); - - it("should destroy uniform buffer by name", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.createUniformBuffer("test", 64); - manager.destroyBuffer("test"); - - expect(manager.getUniformBuffer("test")).toBeUndefined(); - }); }); describe("dispose", () => { - it("should clear all buffers", () => + it("should not throw when disposing", () => { const device = createMockDevice(); const manager = new BufferManager(device); - manager.createVertexBuffer("v1", new Float32Array([1, 2, 3])); - manager.createUniformBuffer("u1", 64); - - manager.dispose(); - - expect(manager.getVertexBuffer("v1")).toBeUndefined(); - expect(manager.getUniformBuffer("u1")).toBeUndefined(); + expect(() => manager.dispose()).not.toThrow(); }); - it("should reset pool stats", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.acquireVertexBuffer(256); - manager.acquireUniformBuffer(64); - - manager.dispose(); - - const stats = manager.getPoolStats(); - expect(stats.vertexPoolSize).toBe(0); - expect(stats.uniformPoolSize).toBe(0); - }); }); describe("acquireStorageBuffer", () => @@ -382,29 +171,6 @@ describe("BufferManager", () => expect(buffer).toBeDefined(); }); - it("should update storage pool stats", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.acquireStorageBuffer(1024); - - const stats = manager.getStoragePoolStats(); - expect(stats.storagePoolSize).toBe(1); - expect(stats.storagePoolInUse).toBe(1); - }); - }); - - describe("releaseStorageBuffer", () => - { - it("should release storage buffer", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - const buffer = manager.acquireStorageBuffer(1024); - - expect(() => manager.releaseStorageBuffer(buffer)).not.toThrow(); - }); }); describe("writeStorageBuffer", () => @@ -432,35 +198,18 @@ describe("BufferManager", () => manager.acquireStorageBuffer(256); manager.acquireStorageBuffer(512); - manager.releaseAllStorageBuffers(); - - const stats = manager.getStoragePoolStats(); - expect(stats.storagePoolInUse).toBe(0); + expect(() => manager.releaseAllStorageBuffers()).not.toThrow(); }); }); describe("clearFrameBuffers", () => { - it("should clear named buffers", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.createVertexBuffer("frame", new Float32Array([1, 2, 3])); - manager.clearFrameBuffers(); - - expect(manager.getVertexBuffer("frame")).toBeUndefined(); - }); - - it("should increment frame number", () => + it("should not throw when clearing", () => { const device = createMockDevice(); const manager = new BufferManager(device); - const beforeFrame = manager.getFrameNumber(); - manager.clearFrameBuffers(); - - expect(manager.getFrameNumber()).toBe(beforeFrame + 1); + expect(() => manager.clearFrameBuffers()).not.toThrow(); }); it("should release all storage buffers", () => @@ -469,34 +218,7 @@ describe("BufferManager", () => const manager = new BufferManager(device); manager.acquireStorageBuffer(256); - manager.clearFrameBuffers(); - - const stats = manager.getStoragePoolStats(); - expect(stats.storagePoolInUse).toBe(0); - }); - }); - - describe("getOrCreateIndirectBuffer", () => - { - it("should create indirect buffer on first call", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - const buffer = manager.getOrCreateIndirectBuffer(6, 10); - - expect(buffer).toBeDefined(); - }); - - it("should return same buffer on subsequent calls", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - const buffer1 = manager.getOrCreateIndirectBuffer(6, 10); - const buffer2 = manager.getOrCreateIndirectBuffer(6, 20); - - expect(buffer1).toBe(buffer2); + expect(() => manager.clearFrameBuffers()).not.toThrow(); }); }); @@ -515,18 +237,4 @@ describe("BufferManager", () => }); }); - describe("getFrameNumber", () => - { - it("should track frame count", () => - { - const device = createMockDevice(); - const manager = new BufferManager(device); - - manager.clearFrameBuffers(); - manager.clearFrameBuffers(); - manager.clearFrameBuffers(); - - expect(manager.getFrameNumber()).toBe(3); - }); - }); }); diff --git a/packages/webgpu/src/BufferManager.ts b/packages/webgpu/src/BufferManager.ts index 4b78913d..e037d27d 100644 --- a/packages/webgpu/src/BufferManager.ts +++ b/packages/webgpu/src/BufferManager.ts @@ -5,15 +5,13 @@ import { execute as bufferManagerAcquireUniformBufferUseCase } from "./BufferMan import { execute as bufferManagerReleaseVertexBufferService } from "./BufferManager/service/BufferManagerReleaseVertexBufferService"; import { execute as bufferManagerReleaseUniformBufferService } from "./BufferManager/service/BufferManagerReleaseUniformBufferService"; import { execute as bufferManagerAcquireStorageBufferUseCase } from "./BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase"; -import { execute as releaseStorageBufferUseCase } from "./BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase"; import { execute as cleanupStorageBuffersUseCase } from "./BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase"; import { execute as bufferManagerCreateIndirectBufferService } from "./BufferManager/service/BufferManagerCreateIndirectBufferService"; import { execute as updateIndirectBuffer } from "./BufferManager/service/BufferManagerUpdateIndirectBufferService"; /** - * @description Dynamic Uniform Buffer Allocator - * 1フレーム内の全fill uniform データを1本の大バッファにサブアロケートし、 - * BindGroup作成を1回に削減する。 + * @description 動的Uniformバッファアロケータ。1フレーム内の全uniformデータを1本の大バッファにサブアロケートし、BindGroup作成を1回に削減 + * Dynamic Uniform Buffer Allocator. Sub-allocates all uniform data within a frame into a single large buffer, reducing BindGroup creation to once */ export class DynamicUniformAllocator { @@ -27,6 +25,12 @@ export class DynamicUniformAllocator private stagingFloat32: Float32Array; private dirtyEnd: number = 0; + /** + * @description コンストラクタ。GPUデバイスとバッファ容量を設定 + * Constructor. Sets up GPU device and buffer capacity + * @param {GPUDevice} device - WebGPUデバイス + * @param {number} capacity - 初期バッファ容量(バイト単位、デフォルト: 65536) + */ constructor (device: GPUDevice, capacity: number = 65536) { this.device = device; @@ -36,8 +40,9 @@ export class DynamicUniformAllocator } /** - * @description フレーム開始時にオフセットをリセット - * 前フレームの旧バッファを安全に破棄(submit済みのため) + * @description フレーム開始時にオフセットをリセットし、前フレームの旧バッファを安全に破棄 + * Reset offset at frame start and safely destroy old buffers from previous frame + * @return {void} */ resetFrame (): void { @@ -52,6 +57,8 @@ export class DynamicUniformAllocator /** * @description バッファを取得(遅延生成) + * Get buffer with lazy initialization + * @return {GPUBuffer} GPUバッファ */ getBuffer (): GPUBuffer { @@ -65,10 +72,10 @@ export class DynamicUniformAllocator } /** - * @description uniform データをCPUステージングバッファにコピーし、アライメント済みオフセットを返す - * 実際のGPU書き込みはflush()で一括実行される - * @param data - 書き込むデータ - * @return アライメント済みオフセット(バイト単位) + * @description uniformデータをCPUステージングバッファにコピーし、アライメント済みオフセットを返す。実際のGPU書き込みはflush()で一括実行 + * Copy uniform data to CPU staging buffer and return aligned offset. Actual GPU write is batched in flush() + * @param {Float32Array} data - 書き込むデータ + * @return {number} アライメント済みオフセット(バイト単位) */ allocate (data: Float32Array): number { @@ -118,8 +125,9 @@ export class DynamicUniformAllocator } /** - * @description ステージングバッファの内容をGPUバッファに一括書き込み - * submit前に1回だけ呼び出す + * @description ステージングバッファの内容をGPUバッファに一括書き込み。submit前に1回だけ呼び出す + * Flush staging buffer content to GPU buffer in bulk. Call once before submit + * @return {void} */ flush (): void { @@ -129,6 +137,11 @@ export class DynamicUniformAllocator } } + /** + * @description バッファを破棄してリソースを解放 + * Dispose buffers and release resources + * @return {void} + */ dispose (): void { if (this.buffer) { @@ -143,15 +156,16 @@ export class DynamicUniformAllocator } } +/** + * @description GPUバッファの管理クラス。頂点・ユニフォーム・ストレージ・インダイレクトバッファのプール管理と再利用を提供 + * GPU buffer management class. Provides pooling and reuse for vertex, uniform, storage, and indirect buffers + */ export class BufferManager { private device: GPUDevice; - private vertexBuffers: Map; - private uniformBuffers: Map; private vertexBufferBuckets: Map; private uniformBufferBuckets: Map; private storageBufferPool: IPooledStorageBuffer[]; - private indirectBuffer: GPUBuffer | null; private indirectBufferPool: GPUBuffer[]; private frameIndirectBuffers: GPUBuffer[]; private frameNumber: number; @@ -160,15 +174,17 @@ export class BufferManager private frameUniformPoolBuffers: GPUBuffer[]; readonly dynamicUniform: DynamicUniformAllocator; + /** + * @description コンストラクタ。GPUデバイスを設定し、各種バッファプールを初期化 + * Constructor. Sets up GPU device and initializes buffer pools + * @param {GPUDevice} device - WebGPUデバイス + */ constructor (device: GPUDevice) { this.device = device; - this.vertexBuffers = new Map(); - this.uniformBuffers = new Map(); this.vertexBufferBuckets = new Map(); this.uniformBufferBuckets = new Map(); this.storageBufferPool = []; - this.indirectBuffer = null; this.indirectBufferPool = []; this.frameIndirectBuffers = []; this.frameNumber = 0; @@ -178,78 +194,51 @@ export class BufferManager this.dynamicUniform = new DynamicUniformAllocator(device); } - createVertexBuffer (name: string, data: Float32Array): GPUBuffer - { - const buffer = this.device.createBuffer({ - "size": data.byteLength, - "usage": GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, - "mappedAtCreation": true - }); - - new Float32Array(buffer.getMappedRange()).set(data); - buffer.unmap(); - - this.vertexBuffers.set(name, buffer); - return buffer; - } - - createUniformBuffer (name: string, size: number): GPUBuffer - { - const buffer = this.device.createBuffer({ - "size": size, - "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST - }); - - this.uniformBuffers.set(name, buffer); - return buffer; - } - - updateUniformBuffer (name: string, data: Float32Array): void - { - const buffer = this.uniformBuffers.get(name); - if (buffer) { - this.device.queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, data.byteLength); - } - } - - getVertexBuffer (name: string): GPUBuffer | undefined - { - return this.vertexBuffers.get(name); - } - - getUniformBuffer (name: string): GPUBuffer | undefined - { - return this.uniformBuffers.get(name); - } - + /** + * @description 矩形の頂点データを作成 + * Create rect vertices data + * @param {number} x - X座標 + * @param {number} y - Y座標 + * @param {number} width - 幅 + * @param {number} height - 高さ + * @return {Float32Array} 矩形の頂点データ + */ createRectVertices (x: number, y: number, width: number, height: number): Float32Array { return bufferManagerCreateRectVerticesService(x, y, width, height); } - acquireVertexBuffer (requiredSize: number, data?: Float32Array): GPUBuffer + /** + * @description プールから頂点バッファを取得(または新規作成) + * Acquire vertex buffer from pool or create new one + * @param {number} required_size - 必要なバイトサイズ + * @param {Float32Array} [data] - 初期データ + * @return {GPUBuffer} 取得された頂点バッファ + */ + acquireVertexBuffer (required_size: number, data?: Float32Array): GPUBuffer { const buffer = bufferManagerAcquireVertexBufferUseCase( this.device, this.vertexBufferBuckets, - requiredSize, + required_size, data ); this.frameVertexPoolBuffers.push(buffer); return buffer; } - releaseVertexBuffer (buffer: GPUBuffer): void - { - bufferManagerReleaseVertexBufferService(this.vertexBufferBuckets, buffer); - } - - acquireUniformBuffer (requiredSize: number): GPUBuffer + /** + * @description プールからユニフォームバッファを取得(または新規作成) + * Acquire uniform buffer from pool or create new one + * @param {number} required_size - 必要なバイトサイズ + * @return {GPUBuffer} 取得されたユニフォームバッファ + */ + acquireUniformBuffer (required_size: number): GPUBuffer { const buffer = bufferManagerAcquireUniformBufferUseCase( this.device, this.uniformBufferBuckets, - requiredSize + required_size ); this.frameUniformPoolBuffers.push(buffer); return buffer; @@ -257,51 +246,26 @@ export class BufferManager /** * @description Uniform Bufferの取得と書き込みを一括で行うヘルパー - * acquireUniformBuffer + writeBuffer の2ステップを1呼び出しに統合 - * @param data - 書き込むデータ - * @param byteLength - 書き込みバイト数(省略時はdata.byteLength) - * @return GPUBuffer + * Helper to acquire and write uniform buffer in one call, combining acquireUniformBuffer + writeBuffer + * @param {Float32Array} data - 書き込むデータ + * @param {number} [byte_length] - 書き込みバイト数(省略時はdata.byteLength) + * @return {GPUBuffer} 取得されたユニフォームバッファ */ - acquireAndWriteUniformBuffer (data: Float32Array, byteLength?: number): GPUBuffer + acquireAndWriteUniformBuffer (data: Float32Array, byte_length?: number): GPUBuffer { - const writeBytes = byteLength ?? data.byteLength; + const writeBytes = byte_length ?? data.byteLength; const buffer = this.acquireUniformBuffer(writeBytes); this.device.queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, writeBytes); return buffer; } - releaseUniformBuffer (buffer: GPUBuffer): void - { - bufferManagerReleaseUniformBufferService(this.uniformBufferBuckets, buffer); - } - - destroyBuffer (name: string): void - { - const vertexBuffer = this.vertexBuffers.get(name); - if (vertexBuffer) { - vertexBuffer.destroy(); - this.vertexBuffers.delete(name); - } - - const uniformBuffer = this.uniformBuffers.get(name); - if (uniformBuffer) { - uniformBuffer.destroy(); - this.uniformBuffers.delete(name); - } - } - + /** + * @description 全バッファを破棄してリソースを解放 + * Dispose all buffers and release resources + * @return {void} + */ dispose (): void { - for (const buffer of this.vertexBuffers.values()) { - buffer.destroy(); - } - this.vertexBuffers.clear(); - - for (const buffer of this.uniformBuffers.values()) { - buffer.destroy(); - } - this.uniformBuffers.clear(); - for (const bucket of this.vertexBufferBuckets.values()) { for (const buffer of bucket) { buffer.destroy(); @@ -321,11 +285,6 @@ export class BufferManager } this.storageBufferPool = []; - if (this.indirectBuffer) { - this.indirectBuffer.destroy(); - this.indirectBuffer = null; - } - for (const buffer of this.indirectBufferPool) { buffer.destroy(); } @@ -347,34 +306,13 @@ export class BufferManager this.dynamicUniform.dispose(); } - getPoolStats (): { vertexPoolSize: number; uniformPoolSize: number } - { - let vertexCount = 0; - for (const bucket of this.vertexBufferBuckets.values()) { - vertexCount += bucket.length; - } - let uniformCount = 0; - for (const bucket of this.uniformBufferBuckets.values()) { - uniformCount += bucket.length; - } - return { - "vertexPoolSize": vertexCount, - "uniformPoolSize": uniformCount - }; - } - + /** + * @description フレーム内で使用したバッファをクリアし、プールに返却 + * Clear frame buffers and return them to pool + * @return {void} + */ clearFrameBuffers (): void { - for (const buffer of this.vertexBuffers.values()) { - buffer.destroy(); - } - this.vertexBuffers.clear(); - - for (const buffer of this.uniformBuffers.values()) { - buffer.destroy(); - } - this.uniformBuffers.clear(); - // フレーム内で取得したプールバッファをプールに返却 for (const buffer of this.frameVertexPoolBuffers) { bufferManagerReleaseVertexBufferService(this.vertexBufferBuckets, buffer); @@ -403,6 +341,11 @@ export class BufferManager } } + /** + * @description 全てのStorage Bufferを未使用状態に戻す + * Mark all storage buffers as not in use + * @return {void} + */ releaseAllStorageBuffers (): void { for (const entry of this.storageBufferPool) { @@ -410,82 +353,77 @@ export class BufferManager } } - acquireStorageBuffer (requiredSize: number): GPUBuffer + /** + * @description プールからStorage Bufferを取得(または新規作成) + * Acquire storage buffer from pool or create new one + * @param {number} required_size - 必要なバイトサイズ + * @return {GPUBuffer} 取得されたStorage Buffer + */ + acquireStorageBuffer (required_size: number): GPUBuffer { return bufferManagerAcquireStorageBufferUseCase( this.device, this.storageBufferPool, - requiredSize, + required_size, this.frameNumber ); } - releaseStorageBuffer (buffer: GPUBuffer): void - { - releaseStorageBufferUseCase(this.storageBufferPool, buffer); - } - + /** + * @description Storage Bufferにデータを書き込む + * Write data to storage buffer + * @param {GPUBuffer} buffer - 書き込み先バッファ + * @param {Float32Array | Uint32Array} data - 書き込むデータ + * @return {void} + */ writeStorageBuffer (buffer: GPUBuffer, data: Float32Array | Uint32Array): void { this.device.queue.writeBuffer(buffer, 0, data.buffer, data.byteOffset, data.byteLength); } - getOrCreateIndirectBuffer ( - vertexCount: number, - instanceCount: number, - firstVertex: number = 0, - firstInstance: number = 0 - ): GPUBuffer { - if (!this.indirectBuffer) { - this.indirectBuffer = bufferManagerCreateIndirectBufferService( - this.device, - vertexCount, - instanceCount, - firstVertex, - firstInstance - ); - } else { - updateIndirectBuffer( - this.device, - this.indirectBuffer, - vertexCount, - instanceCount, - firstVertex, - firstInstance - ); - } - return this.indirectBuffer; - } - + /** + * @description 新しいIndirect Bufferを作成(プールから再利用または新規作成) + * Create new indirect buffer (reuse from pool or create new) + * @param {number} vertex_count - 頂点数 + * @param {number} instance_count - インスタンス数 + * @param {number} first_vertex - 開始頂点インデックス + * @param {number} first_instance - 開始インスタンスインデックス + * @return {GPUBuffer} 作成されたIndirect Buffer + */ createIndirectBuffer ( - vertexCount: number, - instanceCount: number, - firstVertex: number = 0, - firstInstance: number = 0 + vertex_count: number, + instance_count: number, + first_vertex: number = 0, + first_instance: number = 0 ): GPUBuffer { let buffer = this.indirectBufferPool.pop(); if (buffer) { updateIndirectBuffer( this.device, buffer, - vertexCount, - instanceCount, - firstVertex, - firstInstance + vertex_count, + instance_count, + first_vertex, + first_instance ); } else { buffer = bufferManagerCreateIndirectBufferService( this.device, - vertexCount, - instanceCount, - firstVertex, - firstInstance + vertex_count, + instance_count, + first_vertex, + first_instance ); } this.frameIndirectBuffers.push(buffer); return buffer; } + /** + * @description 単位矩形(0,0,1,1)の頂点バッファを取得(遅延生成) + * Get unit rect (0,0,1,1) vertex buffer with lazy creation + * @return {GPUBuffer} 単位矩形の頂点バッファ + */ getUnitRectBuffer (): GPUBuffer { if (!this.unitRectBuffer) { @@ -501,17 +439,4 @@ export class BufferManager return this.unitRectBuffer; } - getFrameNumber (): number - { - return this.frameNumber; - } - - getStoragePoolStats (): { storagePoolSize: number; storagePoolInUse: number } - { - const inUse = this.storageBufferPool.filter((e) => e.inUse).length; - return { - "storagePoolSize": this.storageBufferPool.length, - "storagePoolInUse": inUse - }; - } } diff --git a/packages/webgpu/src/BufferManager/service/BufferManagerReleaseUniformBufferService.ts b/packages/webgpu/src/BufferManager/service/BufferManagerReleaseUniformBufferService.ts index ea30eb88..c04b8bad 100644 --- a/packages/webgpu/src/BufferManager/service/BufferManagerReleaseUniformBufferService.ts +++ b/packages/webgpu/src/BufferManager/service/BufferManagerReleaseUniformBufferService.ts @@ -4,7 +4,7 @@ * @type {number} * @const */ -const MAX_BUCKET_SIZE: number = 32; +const $MAX_BUCKET_SIZE: number = 32; /** * @description ユニフォームバッファをプールに返却 @@ -29,7 +29,7 @@ export const execute = ( buckets.set(size, bucket); } - if (bucket.length >= MAX_BUCKET_SIZE) { + if (bucket.length >= $MAX_BUCKET_SIZE) { // バケットが満杯の場合、このバッファを破棄 buffer.destroy(); return; diff --git a/packages/webgpu/src/BufferManager/service/BufferManagerReleaseVertexBufferService.ts b/packages/webgpu/src/BufferManager/service/BufferManagerReleaseVertexBufferService.ts index 359e5922..a546046b 100644 --- a/packages/webgpu/src/BufferManager/service/BufferManagerReleaseVertexBufferService.ts +++ b/packages/webgpu/src/BufferManager/service/BufferManagerReleaseVertexBufferService.ts @@ -4,7 +4,7 @@ * @type {number} * @const */ -const MAX_BUCKET_SIZE: number = 32; +const $MAX_BUCKET_SIZE: number = 32; /** * @description 頂点バッファをプールに返却 @@ -29,7 +29,7 @@ export const execute = ( buckets.set(size, bucket); } - if (bucket.length >= MAX_BUCKET_SIZE) { + if (bucket.length >= $MAX_BUCKET_SIZE) { // バケットが満杯の場合、このバッファを破棄 buffer.destroy(); return; diff --git a/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.test.ts b/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.test.ts new file mode 100644 index 00000000..17443da7 --- /dev/null +++ b/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.test.ts @@ -0,0 +1,74 @@ +import { execute } from "./BufferManagerUpdateIndirectBufferService"; +import { describe, expect, it, vi } from "vitest"; + +describe("BufferManagerUpdateIndirectBufferService.js test", () => { + + it("execute test case1", () => + { + let writtenData: Uint32Array | null = null; + let writtenOffset: number = -1; + + const mockBuffer = {} as unknown as GPUBuffer; + const mockDevice = { + "queue": { + "writeBuffer": vi.fn((buffer: GPUBuffer, offset: number, data: Uint32Array) => { + expect(buffer).toBe(mockBuffer); + writtenOffset = offset; + writtenData = new Uint32Array(data); + }) + } + } as unknown as GPUDevice; + + execute(mockDevice, mockBuffer, 12, 3, 0, 0); + + expect(mockDevice.queue.writeBuffer).toHaveBeenCalledTimes(1); + expect(writtenOffset).toBe(0); + expect(writtenData).not.toBeNull(); + expect(writtenData![0]).toBe(12); + expect(writtenData![1]).toBe(3); + expect(writtenData![2]).toBe(0); + expect(writtenData![3]).toBe(0); + }); + + it("execute test case2 - custom first_vertex and first_instance", () => + { + let writtenData: Uint32Array | null = null; + + const mockBuffer = {} as unknown as GPUBuffer; + const mockDevice = { + "queue": { + "writeBuffer": vi.fn((_buffer: GPUBuffer, _offset: number, data: Uint32Array) => { + writtenData = new Uint32Array(data); + }) + } + } as unknown as GPUDevice; + + execute(mockDevice, mockBuffer, 6, 1, 5, 10); + + expect(writtenData![0]).toBe(6); + expect(writtenData![1]).toBe(1); + expect(writtenData![2]).toBe(5); + expect(writtenData![3]).toBe(10); + }); + + it("execute test case3 - default parameters", () => + { + let writtenData: Uint32Array | null = null; + + const mockBuffer = {} as unknown as GPUBuffer; + const mockDevice = { + "queue": { + "writeBuffer": vi.fn((_buffer: GPUBuffer, _offset: number, data: Uint32Array) => { + writtenData = new Uint32Array(data); + }) + } + } as unknown as GPUDevice; + + execute(mockDevice, mockBuffer, 100, 50); + + expect(writtenData![0]).toBe(100); + expect(writtenData![1]).toBe(50); + expect(writtenData![2]).toBe(0); + expect(writtenData![3]).toBe(0); + }); +}); diff --git a/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.ts b/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.ts index 1ddec2ad..43fcf90f 100644 --- a/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.ts +++ b/packages/webgpu/src/BufferManager/service/BufferManagerUpdateIndirectBufferService.ts @@ -8,6 +8,7 @@ * @param {number} instance_count - インスタンス数 * @param {number} first_vertex - 開始頂点インデックス * @param {number} first_instance - 開始インスタンスインデックス + * @return {void} */ export const execute = ( device: GPUDevice, diff --git a/packages/webgpu/src/BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase.test.ts b/packages/webgpu/src/BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase.test.ts index 2801ffc0..881f9766 100644 --- a/packages/webgpu/src/BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase.test.ts +++ b/packages/webgpu/src/BufferManager/usecase/BufferManagerAcquireStorageBufferUseCase.test.ts @@ -24,9 +24,21 @@ vi.mock("../service/BufferManagerCreateStorageBufferService", () => ({ })); import { execute } from "./BufferManagerAcquireStorageBufferUseCase"; -import { execute as releaseStorageBuffer } from "./BufferManagerReleaseStorageBufferUseCase"; import { execute as cleanupStorageBuffers } from "./BufferManagerCleanupStorageBuffersUseCase"; +/** + * @description テスト用ヘルパー: Storage Bufferをプールに返却 + * Test helper: Release storage buffer back to pool + */ +const releaseStorageBuffer = (pool: IPooledStorageBuffer[], buffer: GPUBuffer): void => { + for (const entry of pool) { + if (entry.buffer === buffer) { + entry.inUse = false; + return; + } + } +}; + describe("BufferManagerAcquireStorageBufferUseCase", () => { let mockDevice: GPUDevice; @@ -95,26 +107,6 @@ describe("BufferManagerAcquireStorageBufferUseCase", () => }); }); - describe("releaseStorageBuffer", () => - { - it("should mark buffer as not in use", () => - { - const buffer = execute(mockDevice, pool, 1024, 0); - expect(pool[0].inUse).toBe(true); - - releaseStorageBuffer(pool, buffer); - expect(pool[0].inUse).toBe(false); - }); - - it("should handle buffer not in pool", () => - { - const fakeBuffer = {} as GPUBuffer; - - // Should not throw - expect(() => releaseStorageBuffer(pool, fakeBuffer)).not.toThrow(); - }); - }); - describe("cleanupStorageBuffers", () => { it("should remove old unused buffers", () => diff --git a/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.test.ts b/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.test.ts new file mode 100644 index 00000000..4e572c54 --- /dev/null +++ b/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.test.ts @@ -0,0 +1,63 @@ +import { execute } from "./BufferManagerCleanupStorageBuffersUseCase"; +import { describe, expect, it, vi } from "vitest"; + +describe("BufferManagerCleanupStorageBuffersUseCase.js test", () => { + + it("execute test case1 - should remove old unused buffers", () => + { + const destroyFn = vi.fn(); + const pool = [ + { "buffer": { "destroy": destroyFn }, "inUse": false, "lastUsedFrame": 0, "size": 256 }, + { "buffer": { "destroy": vi.fn() }, "inUse": true, "lastUsedFrame": 0, "size": 256 }, + { "buffer": { "destroy": vi.fn() }, "inUse": false, "lastUsedFrame": 50, "size": 256 } + ] as any; + + execute(pool, 100, 60); + + expect(pool.length).toBe(2); + expect(destroyFn).toHaveBeenCalledTimes(1); + }); + + it("execute test case2 - should not remove in-use buffers", () => + { + const pool = [ + { "buffer": { "destroy": vi.fn() }, "inUse": true, "lastUsedFrame": 0, "size": 256 }, + { "buffer": { "destroy": vi.fn() }, "inUse": true, "lastUsedFrame": 10, "size": 256 } + ] as any; + + execute(pool, 100, 60); + + expect(pool.length).toBe(2); + }); + + it("execute test case3 - should not remove recently used buffers", () => + { + const pool = [ + { "buffer": { "destroy": vi.fn() }, "inUse": false, "lastUsedFrame": 95, "size": 256 } + ] as any; + + execute(pool, 100, 60); + + expect(pool.length).toBe(1); + }); + + it("execute test case4 - empty pool", () => + { + const pool: any[] = []; + execute(pool, 100, 60); + expect(pool.length).toBe(0); + }); + + it("execute test case5 - default max_age", () => + { + const destroyFn = vi.fn(); + const pool = [ + { "buffer": { "destroy": destroyFn }, "inUse": false, "lastUsedFrame": 0, "size": 256 } + ] as any; + + execute(pool, 61); + + expect(pool.length).toBe(0); + expect(destroyFn).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.ts b/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.ts index 6304374c..6cb69ac7 100644 --- a/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.ts +++ b/packages/webgpu/src/BufferManager/usecase/BufferManagerCleanupStorageBuffersUseCase.ts @@ -9,6 +9,7 @@ import type { IPooledStorageBuffer } from "../../interface/IStorageBufferConfig" * @param {IPooledStorageBuffer[]} pool - Storage Bufferプール * @param {number} current_frame - 現在のフレーム番号 * @param {number} max_age - 最大保持フレーム数 + * @return {void} */ export const execute = ( pool: IPooledStorageBuffer[], diff --git a/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.ts b/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.ts deleted file mode 100644 index da94118e..00000000 --- a/packages/webgpu/src/BufferManager/usecase/BufferManagerReleaseStorageBufferUseCase.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { IPooledStorageBuffer } from "../../interface/IStorageBufferConfig"; - -/** - * @description Storage Bufferをプールに返却 - * Release Storage Buffer back to pool - * - * @param {IPooledStorageBuffer[]} pool - Storage Bufferプール - * @param {GPUBuffer} buffer - 返却するバッファ - */ -export const execute = ( - pool: IPooledStorageBuffer[], - buffer: GPUBuffer -): void => { - - for (const entry of pool) { - if (entry.buffer === buffer) { - entry.inUse = false; - return; - } - } -}; diff --git a/packages/webgpu/src/Compute/ComputePipelineManager.test.ts b/packages/webgpu/src/Compute/ComputePipelineManager.test.ts deleted file mode 100644 index 62ba9cda..00000000 --- a/packages/webgpu/src/Compute/ComputePipelineManager.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; - -// Mock GPUShaderStage -const GPUShaderStage = { - COMPUTE: 0x04 -}; -(globalThis as any).GPUShaderStage = GPUShaderStage; - -describe("ComputePipelineManager", () => -{ - // Create a mock implementation for testing without the actual class - class MockComputePipelineManager - { - private pipelines: Map; - private bindGroupLayouts: Map; - - constructor (_device: GPUDevice) - { - this.pipelines = new Map(); - this.bindGroupLayouts = new Map(); - - // Initialize mock pipelines - this.pipelines.set("blur_compute_horizontal", { "label": "blur_compute_horizontal" }); - this.pipelines.set("blur_compute_vertical", { "label": "blur_compute_vertical" }); - - // Initialize mock bind group layouts - this.bindGroupLayouts.set("blur_compute", { "label": "blur_compute_bind_group_layout" }); - } - - getPipeline (name: string): any - { - return this.pipelines.get(name); - } - - getBindGroupLayout (name: string): any - { - return this.bindGroupLayouts.get(name); - } - - destroy (): void - { - this.pipelines.clear(); - this.bindGroupLayouts.clear(); - } - } - - const createMockDevice = (): GPUDevice => - { - return { - "createShaderModule": vi.fn(() => ({ "label": "mockShaderModule" })), - "createBindGroupLayout": vi.fn(() => ({ "label": "mockBindGroupLayout" })), - "createPipelineLayout": vi.fn(() => ({ "label": "mockPipelineLayout" })), - "createComputePipeline": vi.fn(() => ({ "label": "mockComputePipeline" })) - } as unknown as GPUDevice; - }; - - beforeEach(() => - { - vi.clearAllMocks(); - }); - - describe("constructor", () => - { - it("should create instance with device", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - expect(manager).toBeDefined(); - }); - - it("should initialize blur pipelines", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - expect(manager.getPipeline("blur_compute_horizontal")).toBeDefined(); - expect(manager.getPipeline("blur_compute_vertical")).toBeDefined(); - }); - }); - - describe("getPipeline", () => - { - it("should return horizontal blur pipeline", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - const pipeline = manager.getPipeline("blur_compute_horizontal"); - - expect(pipeline).toBeDefined(); - expect(pipeline.label).toBe("blur_compute_horizontal"); - }); - - it("should return vertical blur pipeline", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - const pipeline = manager.getPipeline("blur_compute_vertical"); - - expect(pipeline).toBeDefined(); - expect(pipeline.label).toBe("blur_compute_vertical"); - }); - - it("should return undefined for non-existent pipeline", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - expect(manager.getPipeline("nonexistent")).toBeUndefined(); - }); - }); - - describe("getBindGroupLayout", () => - { - it("should return blur compute bind group layout", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - const layout = manager.getBindGroupLayout("blur_compute"); - - expect(layout).toBeDefined(); - }); - - it("should return undefined for non-existent layout", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - expect(manager.getBindGroupLayout("nonexistent")).toBeUndefined(); - }); - }); - - describe("destroy", () => - { - it("should clear pipelines", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - manager.destroy(); - - expect(manager.getPipeline("blur_compute_horizontal")).toBeUndefined(); - expect(manager.getPipeline("blur_compute_vertical")).toBeUndefined(); - }); - - it("should clear bind group layouts", () => - { - const device = createMockDevice(); - const manager = new MockComputePipelineManager(device); - - manager.destroy(); - - expect(manager.getBindGroupLayout("blur_compute")).toBeUndefined(); - }); - }); -}); diff --git a/packages/webgpu/src/Compute/ComputePipelineManager.ts b/packages/webgpu/src/Compute/ComputePipelineManager.ts deleted file mode 100644 index 46a873d9..00000000 --- a/packages/webgpu/src/Compute/ComputePipelineManager.ts +++ /dev/null @@ -1,322 +0,0 @@ -/** - * @description Compute Pipeline Manager - * Compute Shaderパイプラインの管理 - * - * Compute Shaderは並列処理に最適で、フィルター処理を高速化。 - * 特に大きなブラー半径(64+)の場合、20-35%の高速化が期待できる。 - * - * @class - */ -export class ComputePipelineManager -{ - private device: GPUDevice; - private pipelines: Map; - private bindGroupLayouts: Map; - - /** - * @constructor - * @param {GPUDevice} device - WebGPU device - */ - constructor (device: GPUDevice) - { - this.device = device; - this.pipelines = new Map(); - this.bindGroupLayouts = new Map(); - - this.initializeBlurPipelines(); - } - - /** - * @description ブラー用Compute Pipelineを初期化 - * @private - */ - private initializeBlurPipelines (): void - { - // ブラーCompute Shader用のBindGroupLayoutを作成 - const blurBindGroupLayout = this.device.createBindGroupLayout({ - "label": "blur_compute_bind_group_layout", - "entries": [ - { - // 入力テクスチャ - "binding": 0, - "visibility": GPUShaderStage.COMPUTE, - "texture": { - "sampleType": "float" - } - }, - { - // 出力テクスチャ(Storage Texture) - "binding": 1, - "visibility": GPUShaderStage.COMPUTE, - "storageTexture": { - "access": "write-only", - "format": "rgba8unorm" - } - }, - { - // パラメータ(方向、ブラー半径など) - "binding": 2, - "visibility": GPUShaderStage.COMPUTE, - "buffer": { - "type": "uniform" - } - } - ] - }); - - this.bindGroupLayouts.set("blur_compute", blurBindGroupLayout); - - // 水平/垂直ブラーパイプラインを作成 - // 同じシェーダーを使用し、方向はuniformで制御 - this.createBlurComputePipeline("blur_compute_horizontal"); - this.createBlurComputePipeline("blur_compute_vertical"); - - // 共有メモリ版(大半径用) - this.createBlurComputePipeline("blur_compute_shared_horizontal", true); - this.createBlurComputePipeline("blur_compute_shared_vertical", true); - } - - /** - * @description ブラーCompute Pipelineを作成 - * @param {string} name - パイプライン名 - * @private - */ - private createBlurComputePipeline (name: string, useSharedMemory: boolean = false): void - { - const shaderModule = this.device.createShaderModule({ - "label": `${name}_shader`, - "code": useSharedMemory ? this.getSharedBlurComputeShaderCode() : this.getBlurComputeShaderCode() - }); - - const pipelineLayout = this.device.createPipelineLayout({ - "label": `${name}_layout`, - "bindGroupLayouts": [this.bindGroupLayouts.get("blur_compute")!] - }); - - const pipeline = this.device.createComputePipeline({ - "label": name, - "layout": pipelineLayout, - "compute": { - "module": shaderModule, - "entryPoint": "main" - } - }); - - this.pipelines.set(name, pipeline); - } - - /** - * @description ブラーCompute Shaderコードを生成 - * ボックスブラー(均一加重平均)を使用。Fragment Shaderと同一出力。 - * @return {string} WGSLシェーダーコード - * @private - */ - private getBlurComputeShaderCode (): string - { - return ` -struct BlurParams { - direction: vec2, // (1,0) or (0,1) - radius: f32, // ブラー半径 - fraction: f32, // 端ピクセルのブレンド割合 - texSize: vec2, // テクスチャサイズ - samples: f32, // サンプル数 - padding: f32, // パディング(16バイトアライメント) -} - -@group(0) @binding(0) var inputTexture: texture_2d; -@group(0) @binding(1) var outputTexture: texture_storage_2d; -@group(0) @binding(2) var params: BlurParams; - -const WORKGROUP_SIZE: u32 = 16u; - -@compute @workgroup_size(16, 16, 1) -fn main( - @builtin(global_invocation_id) globalId: vec3 -) { - let texSize = vec2(u32(params.texSize.x), u32(params.texSize.y)); - let radius = i32(params.radius); - - let outCoord = globalId.xy; - - if (outCoord.x >= texSize.x || outCoord.y >= texSize.y) { - return; - } - - let direction = vec2(i32(params.direction.x), i32(params.direction.y)); - let samples = params.samples; - let fraction = params.fraction; - - var color = vec4(0.0); - - for (var i = -radius; i <= radius; i = i + 1) { - var sampleCoord = vec2(outCoord) + direction * i; - - sampleCoord.x = clamp(sampleCoord.x, 0, i32(texSize.x) - 1); - sampleCoord.y = clamp(sampleCoord.y, 0, i32(texSize.y) - 1); - - let sample = textureLoad(inputTexture, vec2(sampleCoord), 0); - - // 端ピクセルにfraction重みを適用(Fragment Shaderと同じロジック) - if (i == -radius || i == radius) { - color = color + sample * fraction; - } else { - color = color + sample; - } - } - - color = color / samples; - - textureStore(outputTexture, outCoord, color); -} -`; - } - - /** - * @description 共有メモリ版ブラーCompute Shaderコードを生成 - * ワークグループ共有メモリでテクスチャ読み込みの重複を排除。 - * radius >= 8 で通常版より高速。 - * @return {string} WGSLシェーダーコード - * @private - */ - private getSharedBlurComputeShaderCode (): string - { - return ` -struct BlurParams { - direction: vec2, - radius: f32, - fraction: f32, - texSize: vec2, - samples: f32, - padding: f32, -} - -@group(0) @binding(0) var inputTexture: texture_2d; -@group(0) @binding(1) var outputTexture: texture_storage_2d; -@group(0) @binding(2) var params: BlurParams; - -const TILE: u32 = 16u; -const MAX_APRON: u32 = 24u; -const SHARED_W: u32 = TILE + 2u * MAX_APRON; - -var tile: array, ${(16 + 2 * 24) * 16}>; - -@compute @workgroup_size(16, 16, 1) -fn main( - @builtin(global_invocation_id) globalId: vec3, - @builtin(local_invocation_id) localId: vec3, - @builtin(workgroup_id) workgroupId: vec3 -) { - let texSize = vec2(u32(params.texSize.x), u32(params.texSize.y)); - let radius = u32(params.radius); - let apron = min(radius, MAX_APRON); - let isHorizontal = params.direction.x > 0.5; - let fraction = params.fraction; - let samples = params.samples; - - let threadIdx = localId.x + localId.y * TILE; - let totalThreads = TILE * TILE; - - if (isHorizontal) { - let sharedWidth = TILE + 2u * apron; - let baseX = workgroupId.x * TILE; - let y = globalId.y; - let clampedY = clamp(y, 0u, max(texSize.y, 1u) - 1u); - - // 全スレッドが協調ロード(範囲外スレッドもclampされた座標でロード) - var idx = threadIdx; - loop { - if (idx >= sharedWidth) { break; } - let gx = i32(baseX) + i32(idx) - i32(apron); - let cx = u32(clamp(gx, 0, i32(max(texSize.x, 1u)) - 1)); - tile[localId.y * SHARED_W + idx] = textureLoad(inputTexture, vec2(cx, clampedY), 0); - idx += totalThreads; - } - - // 全スレッドがバリアに到達(早期returnなし) - workgroupBarrier(); - - // 範囲内のスレッドのみ出力 - let outX = globalId.x; - if (outX < texSize.x && y < texSize.y) { - let iRadius = i32(radius); - var color = vec4(0.0); - for (var i = -iRadius; i <= iRadius; i = i + 1) { - let tileIdx = i32(localId.x) + i32(apron) + i; - let s = tile[localId.y * SHARED_W + u32(clamp(tileIdx, 0, i32(sharedWidth) - 1))]; - if (i == -iRadius || i == iRadius) { - color += s * fraction; - } else { - color += s; - } - } - textureStore(outputTexture, vec2(outX, y), color / samples); - } - } else { - let sharedHeight = TILE + 2u * apron; - let baseY = workgroupId.y * TILE; - let x = globalId.x; - let clampedX = clamp(x, 0u, max(texSize.x, 1u) - 1u); - - // 全スレッドが協調ロード(範囲外スレッドもclampされた座標でロード) - var idx = threadIdx; - loop { - if (idx >= sharedHeight) { break; } - let gy = i32(baseY) + i32(idx) - i32(apron); - let cy = u32(clamp(gy, 0, i32(max(texSize.y, 1u)) - 1)); - tile[idx * TILE + localId.x] = textureLoad(inputTexture, vec2(clampedX, cy), 0); - idx += totalThreads; - } - - // 全スレッドがバリアに到達(早期returnなし) - workgroupBarrier(); - - // 範囲内のスレッドのみ出力 - let outY = globalId.y; - if (x < texSize.x && outY < texSize.y) { - let iRadius = i32(radius); - var color = vec4(0.0); - for (var i = -iRadius; i <= iRadius; i = i + 1) { - let tileIdx = i32(localId.y) + i32(apron) + i; - let s = tile[u32(clamp(tileIdx, 0, i32(sharedHeight) - 1)) * TILE + localId.x]; - if (i == -iRadius || i == iRadius) { - color += s * fraction; - } else { - color += s; - } - } - textureStore(outputTexture, vec2(x, outY), color / samples); - } - } -} -`; - } - - /** - * @description パイプラインを取得 - * @param {string} name - パイプライン名 - * @return {GPUComputePipeline | undefined} - */ - getPipeline (name: string): GPUComputePipeline | undefined - { - return this.pipelines.get(name); - } - - /** - * @description BindGroupLayoutを取得 - * @param {string} name - レイアウト名 - * @return {GPUBindGroupLayout | undefined} - */ - getBindGroupLayout (name: string): GPUBindGroupLayout | undefined - { - return this.bindGroupLayouts.get(name); - } - - /** - * @description リソースを破棄 - */ - destroy (): void - { - this.pipelines.clear(); - this.bindGroupLayouts.clear(); - } -} diff --git a/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.test.ts b/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.test.ts deleted file mode 100644 index dd859f05..00000000 --- a/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.test.ts +++ /dev/null @@ -1,318 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import type { IAttachmentObject } from "../../interface/IAttachmentObject"; -import type { ComputePipelineManager } from "../ComputePipelineManager"; -import { execute } from "./ComputeExecuteBlurService"; - -// Mock GPUBufferUsage -const GPUBufferUsage = { - UNIFORM: 0x0040, - COPY_DST: 0x0008 -}; -(globalThis as any).GPUBufferUsage = GPUBufferUsage; - -describe("ComputeExecuteBlurService", () => -{ - const createMockDevice = () => - { - const mockBuffer = { "label": "mockBuffer" }; - return { - "createBuffer": vi.fn(() => mockBuffer), - "createBindGroup": vi.fn(() => ({ "label": "mockBindGroup" })), - "queue": { - "writeBuffer": vi.fn() - } - } as unknown as GPUDevice; - }; - - const createMockCommandEncoder = () => - { - const mockComputePass = { - "setPipeline": vi.fn(), - "setBindGroup": vi.fn(), - "dispatchWorkgroups": vi.fn(), - "end": vi.fn() - }; - return { - "beginComputePass": vi.fn(() => mockComputePass), - "_mockComputePass": mockComputePass - } as unknown as GPUCommandEncoder & { _mockComputePass: any }; - }; - - const createMockComputePipelineManager = (hasPipeline: boolean = true) => - { - return { - "getPipeline": vi.fn(() => hasPipeline ? { "label": "mockPipeline" } : null), - "getBindGroupLayout": vi.fn(() => hasPipeline ? { "label": "mockLayout" } : null) - } as unknown as ComputePipelineManager; - }; - - const createMockAttachment = (width: number, height: number): IAttachmentObject => - { - return { - "id": 1, - "width": width, - "height": height, - "clipLevel": 0, - "msaa": false, - "mask": false, - "color": null, - "texture": { - "id": 1, - "width": width, - "height": height, - "area": width * height, - "smooth": true, - "resource": {} as GPUTexture, - "view": { "label": "mockView" } as unknown as GPUTextureView - }, - "stencil": null, - "msaaTexture": null, - "msaaStencil": null - }; - }; - - beforeEach(() => - { - vi.spyOn(console, "error").mockImplementation(() => {}); - }); - - describe("pipeline selection", () => - { - it("should use horizontal pipeline when isHorizontal is true", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(pipelineManager.getPipeline).toHaveBeenCalledWith("blur_compute_horizontal"); - }); - - it("should use vertical pipeline when isHorizontal is false", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, false, 8); - - expect(pipelineManager.getPipeline).toHaveBeenCalledWith("blur_compute_vertical"); - }); - - it("should return early when pipeline not found", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(false); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(commandEncoder.beginComputePass).not.toHaveBeenCalled(); - }); - }); - - describe("parameter buffer", () => - { - it("should create uniform buffer with correct usage", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(device.createBuffer).toHaveBeenCalledWith( - expect.objectContaining({ - "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST - }) - ); - }); - - it("should write parameter data to buffer", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(device.queue.writeBuffer).toHaveBeenCalled(); - }); - - it("should set horizontal direction vector when isHorizontal is true", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - // Check the params passed to writeBuffer - const writeBufferCall = (device.queue.writeBuffer as ReturnType).mock.calls[0]; - const params = writeBufferCall[2] as Float32Array; - expect(params[0]).toBe(1.0); // direction.x - expect(params[1]).toBe(0.0); // direction.y - }); - - it("should set vertical direction vector when isHorizontal is false", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, false, 8); - - const writeBufferCall = (device.queue.writeBuffer as ReturnType).mock.calls[0]; - const params = writeBufferCall[2] as Float32Array; - expect(params[0]).toBe(0.0); // direction.x - expect(params[1]).toBe(1.0); // direction.y - }); - }); - - describe("bind group", () => - { - it("should create bind group with correct layout", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(pipelineManager.getBindGroupLayout).toHaveBeenCalledWith("blur_compute"); - }); - - it("should create bind group with source and dest textures", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(device.createBindGroup).toHaveBeenCalled(); - }); - }); - - describe("compute pass", () => - { - it("should begin compute pass with correct label for horizontal", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(commandEncoder.beginComputePass).toHaveBeenCalledWith({ - "label": "blur_compute_pass_h" - }); - }); - - it("should begin compute pass with correct label for vertical", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, false, 8); - - expect(commandEncoder.beginComputePass).toHaveBeenCalledWith({ - "label": "blur_compute_pass_v" - }); - }); - - it("should set pipeline", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(commandEncoder._mockComputePass.setPipeline).toHaveBeenCalled(); - }); - - it("should set bind group", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(commandEncoder._mockComputePass.setBindGroup).toHaveBeenCalledWith(0, expect.anything()); - }); - - it("should end compute pass", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - expect(commandEncoder._mockComputePass.end).toHaveBeenCalled(); - }); - }); - - describe("workgroup dispatch", () => - { - it("should calculate correct workgroup count for 256x256", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(256, 256); - const dest = createMockAttachment(256, 256); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - // 256 / 16 = 16 workgroups in each dimension - expect(commandEncoder._mockComputePass.dispatchWorkgroups).toHaveBeenCalledWith(16, 16, 1); - }); - - it("should round up workgroup count for non-aligned size", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const pipelineManager = createMockComputePipelineManager(); - const source = createMockAttachment(100, 100); - const dest = createMockAttachment(100, 100); - - execute(device, commandEncoder, pipelineManager, source, dest, true, 8); - - // ceil(100 / 16) = 7 workgroups in each dimension - expect(commandEncoder._mockComputePass.dispatchWorkgroups).toHaveBeenCalledWith(7, 7, 1); - }); - }); -}); diff --git a/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.ts b/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.ts deleted file mode 100644 index 1026baa3..00000000 --- a/packages/webgpu/src/Compute/service/ComputeExecuteBlurService.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { IAttachmentObject } from "../../interface/IAttachmentObject"; -import type { ComputePipelineManager } from "../ComputePipelineManager"; - -/** - * @description プリアロケートされたFloat32Array (サイズ8) - */ -const $params8 = new Float32Array(8); - -/** - * @description プリアロケートされたBindGroupEntry配列 (バインディング3つ) - */ -const $computeEntries3: GPUBindGroupEntry[] = [ - { "binding": 0, "resource": null as unknown as GPUTextureView }, - { "binding": 1, "resource": null as unknown as GPUTextureView }, - { "binding": 2, "resource": { "buffer": null as unknown as GPUBuffer } } -]; - -/** - * @description プリアロケートされたComputePassDescriptor - */ -const $labelH: GPUComputePassDescriptor = { "label": "blur_compute_pass_h" }; -const $labelV: GPUComputePassDescriptor = { "label": "blur_compute_pass_v" }; - -/** - * @description Compute Shaderでブラーを実行(ボックスブラー) - * Execute box blur using Compute Shader - * - * Fragment Shaderと同一のボックスブラーアルゴリズムを使用。 - * - * @param {GPUDevice} device - WebGPU device - * @param {GPUCommandEncoder} commandEncoder - コマンドエンコーダー - * @param {ComputePipelineManager} computePipelineManager - Compute Pipeline Manager - * @param {IAttachmentObject} source - 入力アタッチメント - * @param {IAttachmentObject} dest - 出力アタッチメント - * @param {boolean} isHorizontal - 水平ブラーかどうか - * @param {number} blur - ブラー量(bufferBlurX/Y相当) - * @param {object} [bufferManager] - バッファマネージャー(プール化用) - * @return {void} - */ -export const execute = ( - device: GPUDevice, - commandEncoder: GPUCommandEncoder, - computePipelineManager: ComputePipelineManager, - source: IAttachmentObject, - dest: IAttachmentObject, - isHorizontal: boolean, - blur: number, - bufferManager?: { acquireAndWriteUniformBuffer(data: Float32Array, byteLength?: number): GPUBuffer } -): void => { - - // radius 8~24 の場合は共有メモリ版を使用(MAX_APRON=24の制限) - const halfBlur = Math.ceil(blur * 0.5); - const useShared = halfBlur >= 8 && halfBlur <= 24; - const pipelineName = useShared - ? isHorizontal ? "blur_compute_shared_horizontal" : "blur_compute_shared_vertical" - : isHorizontal ? "blur_compute_horizontal" : "blur_compute_vertical"; - const pipeline = computePipelineManager.getPipeline(pipelineName); - const bindGroupLayout = computePipelineManager.getBindGroupLayout("blur_compute"); - - if (!pipeline || !bindGroupLayout) { - return; - } - - // ボックスブラーパラメータ(Fragment ShaderのcalculateDirectionalBlurParamsと同一ロジック) - const fraction = 1 - (halfBlur - blur * 0.5); - const samples = 1 + blur; - - $params8[0] = isHorizontal ? 1.0 : 0.0; // direction.x - $params8[1] = isHorizontal ? 0.0 : 1.0; // direction.y - $params8[2] = halfBlur; // radius (halfBlur) - $params8[3] = fraction; // fraction - $params8[4] = source.width; // texSize.x - $params8[5] = source.height; // texSize.y - $params8[6] = samples; // samples - $params8[7] = 0.0; // padding - - const paramsBuffer = bufferManager - ? bufferManager.acquireAndWriteUniformBuffer($params8) - : (() => { - const buf = device.createBuffer({ - "size": $params8.byteLength, - "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST - }); - device.queue.writeBuffer(buf, 0, $params8); - return buf; - })(); - - $computeEntries3[0].resource = source.texture!.view; - $computeEntries3[1].resource = dest.texture!.view; - ($computeEntries3[2].resource as GPUBufferBinding).buffer = paramsBuffer; - const bindGroup = device.createBindGroup({ - "layout": bindGroupLayout, - "entries": $computeEntries3 - }); - - const computePass = commandEncoder.beginComputePass(isHorizontal ? $labelH : $labelV); - - computePass.setPipeline(pipeline); - computePass.setBindGroup(0, bindGroup); - - const workgroupsX = Math.ceil(dest.width / 16); - const workgroupsY = Math.ceil(dest.height / 16); - - computePass.dispatchWorkgroups(workgroupsX, workgroupsY, 1); - computePass.end(); -}; diff --git a/packages/webgpu/src/Context.test.ts b/packages/webgpu/src/Context.test.ts index a3bc7133..1dd90849 100644 --- a/packages/webgpu/src/Context.test.ts +++ b/packages/webgpu/src/Context.test.ts @@ -211,15 +211,6 @@ describe("Context", () => }); }); - describe("clearRect", () => - { - it("should be a no-op in WebGPU (clear happens at render pass start)", () => - { - // clearRect does nothing in WebGPU - clear is done at render pass start - expect(() => context.clearRect(0, 0, 100, 100)).not.toThrow(); - }); - }); - describe("fillStyle", () => { it("should update fill style when set", () => @@ -422,7 +413,7 @@ describe("Context", () => vi.spyOn(context["pipelineManager"], "getPipeline").mockReturnValue({} as GPURenderPipeline); // Mock buffer manager - vi.spyOn(context["bufferManager"], "createVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer); + vi.spyOn(context["bufferManager"], "acquireVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer); const mockNode = { "x": 100, "y": 200, "w": 50, "h": 30 }; @@ -464,7 +455,7 @@ describe("Context", () => "depthStencilAttachment": { "view": {}, "stencilLoadOp": "clear", "stencilStoreOp": "store" } } as unknown as GPURenderPassDescriptor); vi.spyOn(context["pipelineManager"], "getPipeline").mockReturnValue({} as GPURenderPipeline); - vi.spyOn(context["bufferManager"], "createVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer); + vi.spyOn(context["bufferManager"], "acquireVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer); const mockNode = { "x": 0, "y": 0, "w": 100, "h": 100 }; @@ -502,7 +493,7 @@ describe("Context", () => "depthStencilAttachment": { "view": {}, "stencilLoadOp": "clear", "stencilStoreOp": "store" } } as unknown as GPURenderPassDescriptor); vi.spyOn(context["pipelineManager"], "getPipeline").mockReturnValue({} as GPURenderPipeline); - vi.spyOn(context["bufferManager"], "createVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer); + vi.spyOn(context["bufferManager"], "acquireVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer); const mockNode = { "x": 0, "y": 0, "w": 10, "h": 10 }; const mockPixels = new Uint8Array(10 * 10 * 4); @@ -559,7 +550,7 @@ describe("Context", () => "depthStencilAttachment": { "view": {}, "stencilLoadOp": "clear", "stencilStoreOp": "store" } } as unknown as GPURenderPassDescriptor); vi.spyOn(context["pipelineManager"], "getPipeline").mockReturnValue({} as GPURenderPipeline); - vi.spyOn(context["bufferManager"], "createVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer); + vi.spyOn(context["bufferManager"], "acquireVertexBuffer").mockReturnValue(mockBuffer as unknown as GPUBuffer); // Mock copyExternalImageToTexture mockQueue.copyExternalImageToTexture = vi.fn(); diff --git a/packages/webgpu/src/Context.ts b/packages/webgpu/src/Context.ts index 3e80c193..5c47fa79 100644 --- a/packages/webgpu/src/Context.ts +++ b/packages/webgpu/src/Context.ts @@ -9,9 +9,7 @@ import { PathCommand } from "./PathCommand"; import { BufferManager } from "./BufferManager"; import { TextureManager } from "./TextureManager"; import { FrameBufferManager } from "./FrameBufferManager"; -import { AttachmentManager } from "./AttachmentManager"; import { PipelineManager } from "./Shader/PipelineManager"; -import { ComputePipelineManager } from "./Compute/ComputePipelineManager"; import { $rootNodes, $resetAtlas, @@ -76,16 +74,19 @@ import { execute as contextContainerEndLayerUseCase } from "./Context/usecase/Co /** * @description スワップチェーン転送用のIdentity UV定数: scale=(1,1), offset=(0,0) + * Identity UV constant for swap-chain transfer: scale=(1,1), offset=(0,0) */ const $IDENTITY_UV = new Float32Array([1.0, 1.0, 0.0, 0.0]); /** * @description save()/restore()用の Float32Array プール + * Float32Array pool for save()/restore() operations */ const $matrixPool: Float32Array[] = []; /** * @description leaveMask() 用フルスクリーンメッシュ定数 + * Full-screen mesh constant for leaveMask() */ const $FULLSCREEN_MESH = new Float32Array([ // Triangle 1: (0,0), (1,0), (0,1) @@ -100,6 +101,7 @@ const $FULLSCREEN_MESH = new Float32Array([ /** * @description clearNodeArea() 用クワッド頂点定数 + * Quad vertex constant for clearNodeArea() */ const $QUAD_VERTICES = new Float32Array([ 0, 0, // 左上 @@ -112,11 +114,19 @@ const $QUAD_VERTICES = new Float32Array([ /** * @description containerDrawCachedFilter() 用 CT uniform プリアロケート + * Pre-allocated CT uniform for containerDrawCachedFilter() */ const $ctUniform8 = new Float32Array(8); +/** + * @description copyTempToAtlasNode() 用 uniform プリアロケート (scale=1,-1, offset=0,1) + * Pre-allocated uniform for atlas node copy with Y-flip + */ +const $atlasNodeCopyUniform = new Float32Array([1, -1, 0, 1]); + /** * @description fill() 用 uniform プリアロケート (color + matrix = 16 floats = 64 bytes) + * Pre-allocated uniform for fill() (color + matrix = 16 floats = 64 bytes) */ const $fillUniform16 = new Float32Array(16); @@ -187,97 +197,132 @@ const $msaaDescriptor: GPURenderPassDescriptor = { */ export class Context { + /** @description 変換行列スタック / Transform matrix stack */ public readonly $stack: Float32Array[]; + /** @description 現在の2D変換行列 / Current 2D transform matrix */ public readonly $matrix: Float32Array; + /** @description 背景クリア色R / Background clear color R */ public $clearColorR: number; + /** @description 背景クリア色G / Background clear color G */ public $clearColorG: number; + /** @description 背景クリア色B / Background clear color B */ public $clearColorB: number; + /** @description 背景クリア色A / Background clear color A */ public $clearColorA: number; + /** @description メインアタッチメントオブジェクト / Main attachment object */ public $mainAttachmentObject: IAttachmentObject | null; + /** @description アタッチメントオブジェクトのスタック / Attachment object stack */ public readonly $stackAttachmentObject: IAttachmentObject[]; + /** @description グローバルアルファ値 / Global alpha value */ public globalAlpha: number; + /** @description グローバル合成操作 / Global composite operation */ public globalCompositeOperation: IBlendMode; + /** @description 画像スムージングの有効/無効 / Whether image smoothing is enabled */ public imageSmoothingEnabled: boolean; + /** @description 塗りつぶしスタイル / Fill style color */ public $fillStyle: Float32Array; + /** @description 線スタイル / Stroke style color */ public $strokeStyle: Float32Array; + /** @description マスク描画範囲 / Mask drawing bounds */ public readonly maskBounds: IBounds; + /** @description 線の太さ / Line thickness */ public thickness: number; + /** @description 線端の形状 / Line cap style */ public caps: number; + /** @description 線の結合スタイル / Line joint style */ public joints: number; + /** @description マイターリミット / Miter limit */ public miterLimit: number; + /** @description GPUデバイス / GPU device instance */ private device: GPUDevice; + /** @description GPUキャンバスコンテキスト / GPU canvas context */ private canvasContext: GPUCanvasContext; + /** @description 優先テクスチャフォーマット / Preferred texture format */ private preferredFormat: GPUTextureFormat; + /** @description コマンドエンコーダー / Command encoder */ private commandEncoder: GPUCommandEncoder | null = null; + /** @description レンダーパスエンコーダー / Render pass encoder */ private renderPassEncoder: GPURenderPassEncoder | null = null; - // Main canvas texture (for final display) - acquired once per frame + /** @description メインキャンバステクスチャ(最終表示用、フレームごとに1回取得) / Main canvas texture (for final display, acquired once per frame) */ private mainTexture: GPUTexture | null = null; + /** @description メインキャンバステクスチャビュー / Main canvas texture view */ private mainTextureView: GPUTextureView | null = null; + /** @description フレーム開始済みフラグ / Whether the frame has been started */ private frameStarted: boolean = false; - // フレームごとの一時テクスチャ(endFrame()でdestroy) + /** @description フレームごとの一時テクスチャ(endFrame()でdestroy) / Per-frame temporary textures (destroyed in endFrame()) */ private frameTextures: GPUTexture[] = []; - // フレームごとのプール管理テクスチャ(endFrame()でプールに返却) + /** @description フレームごとのプール管理テクスチャ(endFrame()でプールに返却) / Per-frame pooled textures (returned to pool in endFrame()) */ private pooledTextures: GPUTexture[] = []; - // フレームごとのレンダーテクスチャプール管理(endFrame()でプールに返却) + /** @description フレームごとのレンダーテクスチャプール管理(endFrame()でプールに返却) / Per-frame render texture pool (returned to pool in endFrame()) */ private pooledRenderTextures: GPUTexture[] = []; - // Current rendering target (could be main or atlas) + /** @description 現在のレンダーターゲット(メインまたはアトラス) / Current render target (could be main or atlas) */ private currentRenderTarget: GPUTextureView | null = null; - // Current viewport size (WebGL版と同じ: アトラス描画時はアトラスサイズを使用) + /** @description 現在のビューポート幅(アトラス描画時はアトラスサイズ) / Current viewport width (atlas size during atlas rendering) */ private viewportWidth: number = 0; + /** @description 現在のビューポート高さ(アトラス描画時はアトラスサイズ) / Current viewport height (atlas size during atlas rendering) */ private viewportHeight: number = 0; + /** @description パスコマンド / Path command handler */ private pathCommand: PathCommand; + /** @description バッファマネージャー / Buffer manager */ private bufferManager: BufferManager; + /** @description テクスチャマネージャー / Texture manager */ private textureManager: TextureManager; + /** @description フレームバッファマネージャー / Frame buffer manager */ private frameBufferManager: FrameBufferManager; + /** @description パイプラインマネージャー / Pipeline manager */ private pipelineManager: PipelineManager; - private computePipelineManager: ComputePipelineManager; - private attachmentManager: AttachmentManager; + /** @description 新しい描画状態フラグ / New draw state flag */ public newDrawState: boolean = false; - // コンテナレイヤースタック(フィルター/ブレンド用) + /** @description cacheAsBitmap用の保留中アトラスノードスタック / Pending atlas nodes stack for cacheAsBitmap */ + private readonly _pendingAtlasNodes: Node[] = []; + + /** @description コンテナレイヤースタック(フィルター/ブレンド用) / Container layer stack (for filter/blend) */ private readonly $containerLayerStack: IAttachmentObject[] = []; + /** @description コンテナレイヤーのコンテンツサイズ / Container layer content sizes */ private containerLayerContentSizes: { width: number; height: number }[] = []; - // マスク描画モードフラグ(beginMask〜endMask間でtrue) + /** @description マスク描画モードフラグ(beginMask〜endMask間でtrue) / Mask drawing mode flag (true between beginMask and endMask) */ private inMaskMode: boolean = false; - // ノード領域クリア済みフラグ(beginNodeRendering〜endNodeRendering間で使用) + /** @description ノード領域クリア済みフラグ(beginNodeRendering〜endNodeRendering間で使用) / Node area cleared flag (used between beginNodeRendering and endNodeRendering) */ private nodeAreaCleared: boolean = false; - // 現在のノードのシザー範囲(クリア後に戻すため) + /** @description 現在のノードのシザー範囲(クリア後に戻すため) / Current node scissor rect (to restore after clearing) */ private currentNodeScissor: { x: number; y: number; w: number; h: number } | null = null; - // アトラスレンダーパス統合: 同一アトラスへの連続描画でパスを再利用 + /** @description アトラスレンダーパス統合: 同一アトラスへの連続描画でパスを再利用 / Atlas render pass integration: reuse pass for consecutive draws to the same atlas */ private nodeRenderPassAtlasIndex: number = -1; - // Dynamic Uniform BindGroup(fill/stencilパイプライン共有、フレームごとに1回作成) + /** @description Dynamic Uniform BindGroup(fill/stencilパイプライン共有、フレームごとに1回作成) / Dynamic Uniform BindGroup (shared by fill/stencil pipelines, created once per frame) */ private fillDynamicBindGroup: GPUBindGroup | null = null; + /** @description Dynamic Uniform BindGroupのバッファ / Dynamic Uniform BindGroup buffer */ private fillDynamicBindGroupBuffer: GPUBuffer | null = null; - // clearNodeArea() 用頂点バッファキャッシュ + /** @description clearNodeArea() 用頂点バッファキャッシュ / Vertex buffer cache for clearNodeArea() */ private nodeClearQuadBuffer: GPUBuffer | null = null; - // Storage Buffer + Indirect Drawing を使用するかどうか + /** @description Storage Buffer + Indirect Drawing を使用するかどうか / Whether to use Storage Buffer + Indirect Drawing */ private useOptimizedInstancing: boolean = true; - // リサイズ後にcanvasContextの再設定が必要かどうか - // ($resizeComplete()でcanvas.width/heightが設定された後、ensureMainTexture()でconfigure()を呼ぶ) + /** @description リサイズ後にcanvasContextの再設定が必要かどうか / Whether canvasContext reconfiguration is needed after resize */ private $needsReconfigure: boolean = false; - // Hot Path 用の事前割り当てバッファ + /** @description Hot Path 用の事前割り当てバッファ / Pre-allocated buffer for hot path */ private readonly $uniformData8 = new Float32Array(8); + /** @description Hot Path 用の事前割り当てシザーレクト / Pre-allocated scissor rect for hot path */ private readonly $scissorRect: { "x": number; "y": number; "w": number; "h": number } = { "x": 0, "y": 0, "w": 0, "h": 0 }; - // フィルター/コンテナレイヤー用のプリアロケートされた設定オブジェクト + /** @description フィルター/コンテナレイヤー用のプリアロケートされた設定オブジェクト / Pre-allocated config object for filter/container layers */ private readonly $filterConfig: { device: GPUDevice; commandEncoder: GPUCommandEncoder; @@ -286,10 +331,18 @@ export class Context pipelineManager: PipelineManager; textureManager: TextureManager; mainAttachment?: IAttachmentObject; - computePipelineManager: ComputePipelineManager; frameTextures: GPUTexture[]; }; + /** + * @description WebGPUコンテキストを初期化する + * Initialize the WebGPU context + * + * @param {GPUDevice} device - GPUデバイス / GPU device instance + * @param {GPUCanvasContext} canvas_context - GPUキャンバスコンテキスト / GPU canvas context + * @param {GPUTextureFormat} preferred_format - 優先テクスチャフォーマット / Preferred texture format + * @param {number} device_pixel_ratio - デバイスピクセル比 / Device pixel ratio + */ constructor ( device: GPUDevice, canvas_context: GPUCanvasContext, @@ -356,8 +409,6 @@ export class Context this.pipelineManager = new PipelineManager(device, preferred_format); // 遅延パイプライン群を即座に先行作成(初回アクセス時のレイテンシ解消) this.pipelineManager.preloadLazyGroups(); - this.computePipelineManager = new ComputePipelineManager(device); - this.attachmentManager = new AttachmentManager(device); // グラデーションLUT共有アタッチメントにGPUDeviceを設定 $setGradientLUTDevice(device); @@ -383,7 +434,6 @@ export class Context "frameBufferManager": this.frameBufferManager, "pipelineManager": this.pipelineManager, "textureManager": this.textureManager, - "computePipelineManager": this.computePipelineManager, "frameTextures": this.frameTextures }; @@ -393,6 +443,9 @@ export class Context /** * @description 転送範囲をリセット(フレーム開始) + * Reset transfer bounds (frame start) + * + * @return {void} */ clearTransferBounds (): void { @@ -403,6 +456,13 @@ export class Context /** * @description 背景色を更新 + * Update the background color + * + * @param {number} red - 赤色成分 / Red component + * @param {number} green - 緑色成分 / Green component + * @param {number} blue - 青色成分 / Blue component + * @param {number} alpha - アルファ成分 / Alpha component + * @return {void} */ updateBackgroundColor (red: number, green: number, blue: number, alpha: number): void { @@ -414,6 +474,9 @@ export class Context /** * @description 背景色で塗りつぶす(メインアタッチメント) + * Fill with background color (main attachment) + * + * @return {void} */ fillBackgroundColor (): void { @@ -469,6 +532,12 @@ export class Context /** * @description メインcanvasのサイズを変更 + * Resize the main canvas + * + * @param {number} width - 新しい幅 / New width + * @param {number} height - 新しい高さ / New height + * @param {boolean} cache_clear - キャッシュをクリアするか / Whether to clear cache + * @return {void} */ resize (width: number, height: number, cache_clear: boolean = true): void { @@ -574,18 +643,11 @@ export class Context this.bind(this.$mainAttachmentObject); } - /** - * @description 指定範囲をクリアする - */ - clearRect (_x: number, _y: number, _w: number, _h: number): void - { - // WebGPU clear rect implementation - // WebGPUではclearはレンダーパス開始時に行うため、ここでは何もしない - // 実際のクリアはbeginNodeRenderingやbeginFrameで行われる - } - /** * @description 現在の2D変換行列を保存 + * Save the current 2D transform matrix + * + * @return {void} */ save (): void { @@ -596,6 +658,9 @@ export class Context /** * @description 2D変換行列を復元 + * Restore the 2D transform matrix + * + * @return {void} */ restore (): void { @@ -608,6 +673,15 @@ export class Context /** * @description 2D変換行列を設定 + * Set the 2D transform matrix + * + * @param {number} a - 水平スケール / Horizontal scale + * @param {number} b - 垂直スキュー / Vertical skew + * @param {number} c - 水平スキュー / Horizontal skew + * @param {number} d - 垂直スケール / Vertical scale + * @param {number} e - 水平移動 / Horizontal translation + * @param {number} f - 垂直移動 / Vertical translation + * @return {void} */ setTransform ( a: number, b: number, c: number, @@ -623,6 +697,15 @@ export class Context /** * @description 現在の2D変換行列に対して乗算を行います + * Multiply the current 2D transform matrix + * + * @param {number} a - 水平スケール / Horizontal scale + * @param {number} b - 垂直スキュー / Vertical skew + * @param {number} c - 水平スキュー / Horizontal skew + * @param {number} d - 垂直スケール / Vertical scale + * @param {number} e - 水平移動 / Horizontal translation + * @param {number} f - 垂直移動 / Vertical translation + * @return {void} */ transform ( a: number, b: number, c: number, @@ -641,6 +724,9 @@ export class Context /** * @description コンテキストの値を初期化する + * Reset all context values to their initial state + * + * @return {void} */ reset (): void { @@ -654,6 +740,9 @@ export class Context /** * @description パスを開始 + * Begin a new path + * + * @return {void} */ beginPath (): void { @@ -662,6 +751,11 @@ export class Context /** * @description パスを移動 + * Move the path to the specified point + * + * @param {number} x - X座標 / X coordinate + * @param {number} y - Y座標 / Y coordinate + * @return {void} */ moveTo (x: number, y: number): void { @@ -670,6 +764,11 @@ export class Context /** * @description パスを線で結ぶ + * Draw a line to the specified point + * + * @param {number} x - X座標 / X coordinate + * @param {number} y - Y座標 / Y coordinate + * @return {void} */ lineTo (x: number, y: number): void { @@ -678,6 +777,13 @@ export class Context /** * @description 二次ベジェ曲線を描画 + * Draw a quadratic Bézier curve + * + * @param {number} cx - 制御点X / Control point X + * @param {number} cy - 制御点Y / Control point Y + * @param {number} x - 終点X / End point X + * @param {number} y - 終点Y / End point Y + * @return {void} */ quadraticCurveTo (cx: number, cy: number, x: number, y: number): void { @@ -686,6 +792,13 @@ export class Context /** * @description 塗りつぶしスタイルを設定 + * Set the fill style color + * + * @param {number} red - 赤色成分 / Red component + * @param {number} green - 緑色成分 / Green component + * @param {number} blue - 青色成分 / Blue component + * @param {number} alpha - アルファ成分 / Alpha component + * @return {void} */ fillStyle (red: number, green: number, blue: number, alpha: number): void { @@ -697,6 +810,13 @@ export class Context /** * @description 線のスタイルを設定 + * Set the stroke style color + * + * @param {number} red - 赤色成分 / Red component + * @param {number} green - 緑色成分 / Green component + * @param {number} blue - 青色成分 / Blue component + * @param {number} alpha - アルファ成分 / Alpha component + * @return {void} */ strokeStyle (red: number, green: number, blue: number, alpha: number): void { @@ -708,6 +828,9 @@ export class Context /** * @description パスを閉じる + * Close the current path + * + * @return {void} */ closePath (): void { @@ -716,6 +839,12 @@ export class Context /** * @description 円弧を描画 + * Draw an arc + * + * @param {number} x - 中心X / Center X + * @param {number} y - 中心Y / Center Y + * @param {number} radius - 半径 / Radius + * @return {void} */ arc (x: number, y: number, radius: number): void { @@ -724,6 +853,15 @@ export class Context /** * @description 3次ベジェ曲線を描画 + * Draw a cubic Bézier curve + * + * @param {number} cx1 - 第1制御点X / First control point X + * @param {number} cy1 - 第1制御点Y / First control point Y + * @param {number} cx2 - 第2制御点X / Second control point X + * @param {number} cy2 - 第2制御点Y / Second control point Y + * @param {number} x - 終点X / End point X + * @param {number} y - 終点Y / End point Y + * @return {void} */ bezierCurveTo (cx1: number, cy1: number, cx2: number, cy2: number, x: number, y: number): void { @@ -732,7 +870,10 @@ export class Context /** * @description 描画メソッド共通: レンダーパスの確保とノード領域クリア + * Common drawing method: ensure render pass and clear node area * fill(), stroke(), gradientFill(), bitmapFill(), gradientStroke(), bitmapStroke() で使用 + * + * @return {void} */ private ensureFillRenderPass (): void { @@ -854,6 +995,9 @@ export class Context /** * @description 塗りつぶしを実行(Loop-Blinn方式対応) + * Execute fill operation (with Loop-Blinn support) + * + * @return {void} */ fill (): void { @@ -901,6 +1045,9 @@ export class Context /** * @description Dynamic Uniform BindGroupを取得(フレーム内で初回呼び出し時に作成) + * Get or create the Dynamic Uniform BindGroup (created on first call within a frame) + * + * @return {GPUBindGroup} Dynamic Uniform BindGroup */ private getOrCreateFillDynamicBindGroup(): GPUBindGroup { @@ -927,14 +1074,28 @@ export class Context /** * @description fill/stroke用のcolor/matrix uniformを書き込む - * FillUniforms構造体: color(vec4) + matrix0(vec4) + matrix1(vec4) + matrix2(vec4) = 64 bytes - * @return Dynamic Uniform Buffer内のアライメント済みオフセット + * Write color/matrix uniform for fill/stroke operations + * FillUniforms構造体: color(vec4) + matrix0(vec4) + matrix1(vec4) + matrix2(vec4) = 64 bytes + * + * @param {number} red - 赤色成分 / Red component + * @param {number} green - 緑色成分 / Green component + * @param {number} blue - 青色成分 / Blue component + * @param {number} alpha - アルファ成分 / Alpha component + * @param {number} a - 変換行列a / Transform matrix a + * @param {number} b - 変換行列b / Transform matrix b + * @param {number} c - 変換行列c / Transform matrix c + * @param {number} d - 変換行列d / Transform matrix d + * @param {number} tx - 変換行列tx / Transform matrix tx + * @param {number} ty - 変換行列ty / Transform matrix ty + * @param {number} viewport_width - ビューポート幅 / Viewport width + * @param {number} viewport_height - ビューポート高さ / Viewport height + * @return {number} Dynamic Uniform Buffer内のアライメント済みオフセット / Aligned offset in the Dynamic Uniform Buffer */ private writeFillUniform( red: number, green: number, blue: number, alpha: number, a: number, b: number, c: number, d: number, tx: number, ty: number, - viewportWidth: number, viewportHeight: number + viewport_width: number, viewport_height: number ): number { // color @@ -943,18 +1104,18 @@ export class Context $fillUniform16[2] = blue; $fillUniform16[3] = alpha; // matrix0 (a, b, 0, pad) — ビューポート正規化 - $fillUniform16[4] = a / viewportWidth; - $fillUniform16[5] = b / viewportHeight; + $fillUniform16[4] = a / viewport_width; + $fillUniform16[5] = b / viewport_height; $fillUniform16[6] = 0; $fillUniform16[7] = 0; // matrix1 (c, d, 0, pad) - $fillUniform16[8] = c / viewportWidth; - $fillUniform16[9] = d / viewportHeight; + $fillUniform16[8] = c / viewport_width; + $fillUniform16[9] = d / viewport_height; $fillUniform16[10] = 0; $fillUniform16[11] = 0; // matrix2 (tx, ty, 1, pad) - $fillUniform16[12] = tx / viewportWidth; - $fillUniform16[13] = ty / viewportHeight; + $fillUniform16[12] = tx / viewport_width; + $fillUniform16[13] = ty / viewport_height; $fillUniform16[14] = 1; $fillUniform16[15] = 0; @@ -963,53 +1124,75 @@ export class Context /** * @description 2パスステンシルフィル(アトラス用) + * Two-pass stencil fill (for atlas) + * + * @param {GPUBuffer} vertex_buffer - 頂点バッファ / Vertex buffer + * @param {number} vertex_count - 頂点数 / Vertex count + * @param {GPUBindGroup} bind_group - バインドグループ / Bind group + * @param {number} uniform_offset - ユニフォームオフセット / Uniform offset + * @return {void} */ private fillWithStencil( - vertexBuffer: GPUBuffer, - vertexCount: number, - bindGroup: GPUBindGroup, - uniformOffset: number + vertex_buffer: GPUBuffer, + vertex_count: number, + bind_group: GPUBindGroup, + uniform_offset: number ): void { contextFillWithStencilService( this.renderPassEncoder!, this.pipelineManager, - vertexBuffer, - vertexCount, - bindGroup, - uniformOffset + vertex_buffer, + vertex_count, + bind_group, + uniform_offset ); } /** * @description 2パスステンシルフィル(メインキャンバス用) + * Two-pass stencil fill (for main canvas) + * + * @param {GPUBuffer} vertex_buffer - 頂点バッファ / Vertex buffer + * @param {number} vertex_count - 頂点数 / Vertex count + * @param {GPUBindGroup} bind_group - バインドグループ / Bind group + * @param {number} uniform_offset - ユニフォームオフセット / Uniform offset + * @return {void} */ private fillWithStencilMain( - vertexBuffer: GPUBuffer, - vertexCount: number, - bindGroup: GPUBindGroup, - uniformOffset: number + vertex_buffer: GPUBuffer, + vertex_count: number, + bind_group: GPUBindGroup, + uniform_offset: number ): void { contextFillWithStencilMainService( this.renderPassEncoder!, this.pipelineManager, - vertexBuffer, - vertexCount, - bindGroup, - uniformOffset + vertex_buffer, + vertex_count, + bind_group, + uniform_offset ); } /** * @description 単純なフィル(ステンシルなし、キャンバス描画用) + * Simple fill (no stencil, for canvas rendering) + * + * @param {GPUBuffer} vertex_buffer - 頂点バッファ / Vertex buffer + * @param {number} vertex_count - 頂点数 / Vertex count + * @param {boolean} use_stencil_pipeline - ステンシルパイプラインを使用するか / Whether to use stencil pipeline + * @param {GPUBindGroup} bind_group - バインドグループ / Bind group + * @param {number} uniform_offset - ユニフォームオフセット / Uniform offset + * @return {void} */ private fillSimple( - vertexBuffer: GPUBuffer, - vertexCount: number, - useStencilPipeline: boolean, - bindGroup: GPUBindGroup, - uniformOffset: number + vertex_buffer: GPUBuffer, + vertex_count: number, + use_stencil_pipeline: boolean, + bind_group: GPUBindGroup, + uniform_offset: number ): void { const clipLevel = this.$mainAttachmentObject?.clipLevel ?? 1; @@ -1017,63 +1200,22 @@ export class Context contextFillSimpleService( this.renderPassEncoder!, this.pipelineManager, - vertexBuffer, - vertexCount, - bindGroup, - uniformOffset, - !!this.currentRenderTarget, - useStencilPipeline, + vertex_buffer, + vertex_count, + bind_group, + uniform_offset, + this.currentRenderTarget, + use_stencil_pipeline, clipLevel ); } - /** - * @description オフスクリーンアタッチメントにバインド - * WebGL: FrameBufferManagerBindAttachmentObjectService - */ - bindAttachment(attachment: IAttachmentObject): void - { - this.attachmentManager.bindAttachment(attachment); - - // 現在のレンダーターゲットをオフスクリーンに切り替え - // color?.view または texture?.view を使用 - const view = attachment.color?.view ?? attachment.texture?.view; - if (view) { - this.currentRenderTarget = view; - } - } - - /** - * @description メインキャンバスにバインド - * WebGL: FrameBufferManagerUnBindAttachmentObjectService - */ - unbindAttachment(): void - { - this.attachmentManager.unbindAttachment(); - this.currentRenderTarget = null; - } - - /** - * @description アタッチメントオブジェクトを取得 - * WebGL: FrameBufferManagerGetAttachmentObjectUseCase - */ - getAttachmentObject(width: number, height: number, msaa: boolean = false): IAttachmentObject - { - return this.attachmentManager.getAttachmentObject(width, height, msaa); - } - - /** - * @description アタッチメントオブジェクトを解放 - * WebGL: FrameBufferManagerReleaseAttachmentObjectUseCase - */ - releaseAttachment(attachment: IAttachmentObject): void - { - this.attachmentManager.releaseAttachment(attachment); - } - /** * @description 線の描画を実行(WebGL版と同じ仕様) + * Execute stroke drawing (same specification as WebGL version) * WebGL版と同様に、ストロークを塗りとして描画する + * + * @return {void} */ stroke (): void { @@ -1124,6 +1266,15 @@ export class Context /** * @description グラデーションの塗りつぶしを実行 + * Execute gradient fill + * + * @param {number} type - グラデーションタイプ / Gradient type + * @param {number[]} stops - カラーストップ配列 / Color stop array + * @param {Float32Array} matrix - グラデーション変換行列 / Gradient transform matrix + * @param {number} spread - スプレッドメソッド / Spread method + * @param {number} interpolation - 補間方法 / Interpolation method + * @param {number} focal - 焦点距離 / Focal point ratio + * @return {void} */ gradientFill ( type: number, @@ -1148,7 +1299,7 @@ export class Context const useStencilPipeline = useMainStencil; // アトラスへの描画かどうか - const useAtlasTarget = !!this.currentRenderTarget; + const useAtlasTarget = this.currentRenderTarget; const lutTexture = contextGradientFillUseCase( this.device, @@ -1181,6 +1332,15 @@ export class Context /** * @description ビットマップの塗りつぶしを実行 + * Execute bitmap fill + * + * @param {Uint8Array} pixels - ピクセルデータ / Pixel data + * @param {Float32Array} matrix - ビットマップ変換行列 / Bitmap transform matrix + * @param {number} width - ビットマップ幅 / Bitmap width + * @param {number} height - ビットマップ高さ / Bitmap height + * @param {boolean} repeat - 繰り返しフラグ / Repeat flag + * @param {boolean} smooth - スムージングフラグ / Smoothing flag + * @return {void} */ bitmapFill ( pixels: Uint8Array, @@ -1224,7 +1384,7 @@ export class Context smooth, this.viewportWidth, this.viewportHeight, - !!this.currentRenderTarget, + this.currentRenderTarget, !!useStencilPipeline, clipLevel ); @@ -1241,6 +1401,15 @@ export class Context /** * @description グラデーション線の描画を実行 + * Execute gradient stroke drawing + * + * @param {number} type - グラデーションタイプ / Gradient type + * @param {number[]} stops - カラーストップ配列 / Color stop array + * @param {Float32Array} matrix - グラデーション変換行列 / Gradient transform matrix + * @param {number} spread - スプレッドメソッド / Spread method + * @param {number} interpolation - 補間方法 / Interpolation method + * @param {number} focal - 焦点距離 / Focal point ratio + * @return {void} */ gradientStroke ( type: number, @@ -1282,7 +1451,7 @@ export class Context focal, this.viewportWidth, this.viewportHeight, - !!this.currentRenderTarget, + this.currentRenderTarget, useStencilPipeline ); @@ -1298,6 +1467,15 @@ export class Context /** * @description ビットマップ線の描画を実行 + * Execute bitmap stroke drawing + * + * @param {Uint8Array} pixels - ピクセルデータ / Pixel data + * @param {Float32Array} matrix - ビットマップ変換行列 / Bitmap transform matrix + * @param {number} width - ビットマップ幅 / Bitmap width + * @param {number} height - ビットマップ高さ / Bitmap height + * @param {boolean} repeat - 繰り返しフラグ / Repeat flag + * @param {boolean} smooth - スムージングフラグ / Smoothing flag + * @return {void} */ bitmapStroke ( pixels: Uint8Array, @@ -1339,7 +1517,7 @@ export class Context smooth, this.viewportWidth, this.viewportHeight, - !!this.currentRenderTarget, + this.currentRenderTarget, useStencilPipeline ); @@ -1355,8 +1533,11 @@ export class Context /** * @description マスク処理を実行 + * Execute mask clipping operation * WebGL版と同様にステンシルバッファを使用したクリッピング * メインアタッチメントとアトラス両方でマスク処理をサポート + * + * @return {void} */ clip (): void { @@ -1425,6 +1606,10 @@ export class Context /** * @description アタッチメントオブジェクトをバインド + * Bind an attachment object + * + * @param {IAttachmentObject} attachment_object - バインドするアタッチメント / Attachment to bind + * @return {void} */ bind (attachment_object: IAttachmentObject): void { @@ -1437,8 +1622,11 @@ export class Context /** * @description 現在のアタッチメントオブジェクトを取得 + * Get the current attachment object * アトラスがバインドされていない場合はメインアタッチメントを返す * When no atlas is bound, returns the main attachment + * + * @return {IAttachmentObject | null} 現在のアタッチメント / Current attachment */ get currentAttachmentObject (): IAttachmentObject | null { @@ -1450,6 +1638,9 @@ export class Context /** * @description アトラス専用のアタッチメントオブジェクトを取得 + * Get the atlas-specific attachment object + * + * @return {IAttachmentObject | null} アトラスアタッチメント / Atlas attachment */ get atlasAttachmentObject (): IAttachmentObject | null { @@ -1458,6 +1649,10 @@ export class Context /** * @description グリッドの描画データをセット + * Set grid drawing data + * + * @param {Float32Array | null} grid_data - グリッドデータ / Grid data + * @return {void} */ useGrid (grid_data: Float32Array | null): void { @@ -1466,7 +1661,11 @@ export class Context /** * @description 指定のノード範囲で描画を開始(アトラステクスチャへの描画) + * Begin rendering for the specified node region (drawing to atlas texture) * 2パスステンシルフィル対応: ステンシルバッファ付きレンダーパスを使用 + * + * @param {Node} node - 描画対象ノード / Target node for rendering + * @return {void} */ beginNodeRendering (node: Node): void { @@ -1558,7 +1757,10 @@ export class Context /** * @description ノード領域がまだクリアされていない場合にクリアを実行 + * Clear the node area if it has not been cleared yet * 最初の描画操作(fill, gradientFill, gradientStroke等)で呼び出される + * + * @return {void} */ private ensureNodeAreaCleared (): void { @@ -1569,7 +1771,10 @@ export class Context /** * @description ノード領域をクリア(透明色 + ステンシル=0) + * Clear the node area (transparent color + stencil=0) * WebGL版の gl.clear(COLOR_BUFFER_BIT | STENCIL_BUFFER_BIT) と同等 + * + * @return {void} */ private clearNodeArea (): void { @@ -1615,7 +1820,10 @@ export class Context /** * @description 指定のノード範囲で描画を終了 + * End rendering for the current node region * レンダーパスは終了しない(次のbeginNodeRenderingで再利用するため) + * + * @return {void} */ endNodeRendering (): void { @@ -1634,6 +1842,9 @@ export class Context /** * @description 塗りの描画を実行 + * Execute fill drawing + * + * @return {void} */ drawFill (): void { @@ -1650,6 +1861,15 @@ export class Context /** * @description インスタンスを描画 + * Draw a display object instance + * + * @param {Node} node - 描画対象ノード / Target node + * @param {number} x_min - バウンディングボックス左端 / Bounding box left + * @param {number} y_min - バウンディングボックス上端 / Bounding box top + * @param {number} x_max - バウンディングボックス右端 / Bounding box right + * @param {number} y_max - バウンディングボックス下端 / Bounding box bottom + * @param {Float32Array} color_transform - カラー変換パラメータ / Color transform parameters + * @return {void} */ drawDisplayObject ( node: Node, @@ -1681,10 +1901,11 @@ export class Context * @description インスタンス配列を描画 * Draw instanced arrays * - * useOptimizedInstancingがtrueの場合、Storage BufferとIndirect Drawingを使用。 - * - Storage Buffer: メモリアロケーション削減、CPU負荷15-25%軽減 - * - Indirect Drawing: CPU-GPUオーバーヘッド5-15%削減 + * useOptimizedInstancingがtrueの場合、Storage BufferとIndirect Drawingを使用。 + * - Storage Buffer: メモリアロケーション削減、CPU負荷15-25%軽減 + * - Indirect Drawing: CPU-GPUオーバーヘッド5-15%削減 * + * @return {void} */ drawArraysInstanced (): void { @@ -1741,28 +1962,11 @@ export class Context this.processComplexBlendQueue(); } - /** - * @description 最適化インスタンス描画の有効/無効を設定 - * Enable or disable optimized instancing - * - */ - setOptimizedInstancing (enabled: boolean): void - { - this.useOptimizedInstancing = enabled; - } - - /** - * @description 最適化インスタンス描画が有効かどうか - * Whether optimized instancing is enabled - * - */ - isOptimizedInstancingEnabled (): boolean - { - return this.useOptimizedInstancing; - } - /** * @description 複雑なブレンドモードのキューを処理 + * Process the complex blend mode queue + * + * @return {void} */ private processComplexBlendQueue (): void { @@ -1783,6 +1987,9 @@ export class Context /** * @description インスタンス配列をクリア + * Clear instanced arrays + * + * @return {void} */ clearArraysInstanced (): void { @@ -1793,8 +2000,13 @@ export class Context /** * @description ピクセルバッファをNodeの指定箇所に転送 + * Transfer pixel buffer to the specified position of the Node * WebGPUでは、Shapeのシェーダーが-ndc.yでY軸反転しているため、 * Bitmapも同じ方向になるよう画像を上下反転して書き込む + * + * @param {Node} node - 描画対象ノード / Target node + * @param {Uint8Array} pixels - ピクセルデータ / Pixel data + * @return {void} */ drawPixels (node: Node, pixels: Uint8Array): void { @@ -1845,6 +2057,14 @@ export class Context /** * @description 一時テクスチャ経由でピクセルデータをMSAAテクスチャに描画 + * Draw pixel data to MSAA texture via a temporary texture + * + * @param {IAttachmentObject} attachment - アタッチメントオブジェクト / Attachment object + * @param {Node} node - 描画対象ノード / Target node + * @param {Uint8Array} pixels - ピクセルデータ / Pixel data + * @param {number} width - 幅 / Width + * @param {number} height - 高さ / Height + * @return {void} */ private drawPixelsToMsaa ( attachment: IAttachmentObject, @@ -1933,10 +2153,16 @@ export class Context /** * @description OffscreenCanvasをNodeの指定箇所に転送 + * Transfer OffscreenCanvas to the specified position of the Node * WebGPUでは、Shapeのシェーダーが-ndc.yでY軸反転しているため、 * Bitmapも同じ方向になるよう画像を上下反転して書き込む + * + * @param {Node} node - 描画対象ノード / Target node + * @param {OffscreenCanvas | ImageBitmap} element - 描画要素 / Element to draw + * @param {boolean} flip_y - Y軸反転フラグ / Y-axis flip flag + * @return {void} */ - drawElement (node: Node, element: OffscreenCanvas | ImageBitmap, flipY: boolean = false): void + drawElement (node: Node, element: OffscreenCanvas | ImageBitmap, flip_y: boolean = false): void { // WebGPU draw element // OffscreenCanvasまたはImageBitmapをアトラステクスチャに描画 @@ -1960,14 +2186,23 @@ export class Context // MSAAが有効な場合は一時テクスチャ経由でMSAAテクスチャに直接描画 // MSAAが無効な場合もシェーダー経由で描画(WebGLと同じ処理フロー) if (attachment.msaa && attachment.msaaTexture?.view) { - this.drawElementToMsaa(attachment, node, element, width, height, flipY); + this.drawElementToMsaa(attachment, node, element, width, height, flip_y); } else { - this.drawElementToTexture(attachment, node, element, width, height, flipY); + this.drawElementToTexture(attachment, node, element, width, height, flip_y); } } /** * @description 一時テクスチャ経由でMSAAテクスチャに直接描画 + * Draw to MSAA texture directly via a temporary texture + * + * @param {IAttachmentObject} attachment - アタッチメントオブジェクト / Attachment object + * @param {Node} node - 描画対象ノード / Target node + * @param {OffscreenCanvas | ImageBitmap} element - 描画要素 / Element to draw + * @param {number} width - 幅 / Width + * @param {number} height - 高さ / Height + * @param {boolean} flip_y - Y軸反転フラグ / Y-axis flip flag + * @return {void} */ private drawElementToMsaa ( attachment: IAttachmentObject, @@ -1975,7 +2210,7 @@ export class Context element: OffscreenCanvas | ImageBitmap, width: number, height: number, - flipY: boolean + flip_y: boolean ): void { // 一時テクスチャをプールから取得 @@ -1984,7 +2219,7 @@ export class Context this.device.queue.copyExternalImageToTexture( { "source": element as ImageBitmap, - "flipY": flipY + "flipY": flip_y }, { "texture": tempTexture, @@ -2057,6 +2292,15 @@ export class Context /** * @description 一時テクスチャ経由で通常テクスチャに描画(非MSAA版) + * Draw to a regular texture via a temporary texture (non-MSAA version) + * + * @param {IAttachmentObject} attachment - アタッチメントオブジェクト / Attachment object + * @param {Node} node - 描画対象ノード / Target node + * @param {OffscreenCanvas | ImageBitmap} element - 描画要素 / Element to draw + * @param {number} width - 幅 / Width + * @param {number} height - 高さ / Height + * @param {boolean} flip_y - Y軸反転フラグ / Y-axis flip flag + * @return {void} */ private drawElementToTexture ( attachment: IAttachmentObject, @@ -2064,7 +2308,7 @@ export class Context element: OffscreenCanvas | ImageBitmap, width: number, height: number, - flipY: boolean + flip_y: boolean ): void { // 一時テクスチャをプールから取得 @@ -2075,7 +2319,7 @@ export class Context this.device.queue.copyExternalImageToTexture( { "source": element as ImageBitmap, - "flipY": flipY + "flipY": flip_y }, { "texture": tempTexture, @@ -2148,6 +2392,20 @@ export class Context /** * @description フィルターを適用 + * Apply filter effects + * + * @param {Node} node - 描画対象ノード / Target node + * @param {string} _unique_key - ユニークキー / Unique key + * @param {boolean} _updated - 更新フラグ / Updated flag + * @param {number} width - 幅 / Width + * @param {number} height - 高さ / Height + * @param {boolean} _is_bitmap - ビットマップかどうか / Whether it is a bitmap + * @param {Float32Array} matrix - 変換行列 / Transform matrix + * @param {Float32Array} color_transform - カラー変換パラメータ / Color transform parameters + * @param {IBlendMode} blend_mode - ブレンドモード / Blend mode + * @param {Float32Array} bounds - バウンディングボックス / Bounding box + * @param {Float32Array} params - フィルターパラメータ / Filter parameters + * @return {void} */ applyFilter ( node: Node, @@ -2203,6 +2461,9 @@ export class Context * @description コンテナのフィルター/ブレンド用のレイヤーを開始 * Begin a container layer for filter/blend processing * + * @param {number} width - レイヤー幅 / Layer width + * @param {number} height - レイヤー高さ / Layer height + * @return {void} */ containerBeginLayer (width: number, height: number): void { @@ -2243,14 +2504,15 @@ export class Context * @description コンテナのフィルター/ブレンド用レイヤーを終了し、結果を元のメインに合成 * End the container layer and composite the result back to the original main * - * @param {IBlendMode} blend_mode - * @param {Float32Array} matrix - * @param {Float32Array | null} color_transform - * @param {boolean} use_filter - * @param {Float32Array | null} filter_bounds - * @param {Float32Array | null} filter_params - * @param {string} unique_key - * @param {string} filter_key + * @param {IBlendMode} blend_mode - ブレンドモード / Blend mode + * @param {Float32Array} matrix - 変換行列 / Transform matrix + * @param {Float32Array | null} color_transform - カラー変換パラメータ / Color transform parameters + * @param {boolean} use_filter - フィルター使用フラグ / Whether to use filter + * @param {Float32Array | null} filter_bounds - フィルターバウンド / Filter bounds + * @param {Float32Array | null} filter_params - フィルターパラメータ / Filter parameters + * @param {string} unique_key - ユニークキー / Unique key + * @param {string} filter_key - フィルターキー / Filter key + * @return {void} */ containerEndLayer ( blend_mode: IBlendMode, @@ -2303,16 +2565,157 @@ export class Context this.bind(this.$mainAttachmentObject); } + /** + * @description cacheAsBitmap: temp FBO作成→子要素描画開始 + * Begin container cacheAsBitmap: create temp bgra8unorm FBO for children, + * allocate atlas node for later copy + * + * @param {number} width - ノード幅 / Node width + * @param {number} height - ノード高さ / Node height + * @return {Node} + */ + containerBeginAtlasNode (width: number, height: number): Node + { + this.drawArraysInstanced(); + + // アトラスノードを確保(後でコピー先として使用) + const node = this.createNode(width, height); + this._pendingAtlasNodes.push(node); + + // mainをスタックに保存 + const mainAttachment = this.$mainAttachmentObject as IAttachmentObject; + this.$containerLayerStack.push(mainAttachment); + + // 既存のレンダーパスを終了 + if (this.renderPassEncoder) { + this.renderPassEncoder.end(); + this.renderPassEncoder = null; + } + + // bgra8unormのtemp FBOを作成(全パイプラインが互換) + const tempAttachment = this.frameBufferManager.createAttachment( + "container_layer", width, height, mainAttachment.msaa, true + ); + + this.$mainAttachmentObject = tempAttachment; + this.bind(tempAttachment); + + return node; + } + + /** + * @description cacheAsBitmap: temp FBO→アトラスノードへコピー + * End container cacheAsBitmap: copy temp FBO content to atlas node, + * release temp FBO + * + * @return {void} + */ + containerEndAtlasNode (): void + { + this.drawArraysInstanced(); + + // 既存のレンダーパスを終了(temp FBOのMSAAリゾルブが実行される) + if (this.renderPassEncoder) { + this.renderPassEncoder.end(); + this.renderPassEncoder = null; + } + + this.ensureCommandEncoder(); + + const tempAttachment = this.$mainAttachmentObject as IAttachmentObject; + const node = this._pendingAtlasNodes.pop()!; + + // mainを復元 + this.$mainAttachmentObject = this.$containerLayerStack.pop() as IAttachmentObject; + + // temp FBO → アトラスノードへコピー + this.copyTempToAtlasNode(tempAttachment, node); + + // temp FBOをリリース + this.frameBufferManager.releaseTemporaryAttachment(tempAttachment); + + // メインをバインド + this.bind(this.$mainAttachmentObject); + } + + /** + * @description temp FBOの内容をアトラスノード領域にコピー + * Copy temp FBO content to the atlas node region using texture_copy pipeline + * + * @param {IAttachmentObject} temp_attachment - コピー元のtemp FBO / Source temp FBO + * @param {Node} node - コピー先のアトラスノード / Destination atlas node + * @return {void} + */ + private copyTempToAtlasNode (temp_attachment: IAttachmentObject, node: Node): void + { + const atlas = $getAtlasAttachmentObjectByIndex(node.index) || $getAtlasAttachmentObject(); + if (!atlas || !atlas.texture || !temp_attachment.texture) { + return; + } + + // アトラスはrgba8unormフォーマット(FrameBufferManagerCreateAttachmentUseCase参照) + // atlas_*テクスチャはcopyExternalImageToTextureとの互換性のためrgba8unormで作成される + const useMsaa = atlas.msaa && atlas.msaaTexture?.view; + const pipelineName = useMsaa ? "texture_copy_rgba8_msaa" : "texture_copy_rgba8"; + const pipeline = this.pipelineManager.getPipeline(pipelineName); + const bindGroupLayout = this.pipelineManager.getBindGroupLayout("texture_copy"); + if (!pipeline || !bindGroupLayout) { + return; + } + + // uniform: temp FBO全体をサンプリング(Y軸反転してアトラスに格納) + // アトラスのShape描画はFillVertexのyFlipSign=1.0によりY反転で格納されるため、 + // cacheAsBitmapのコピーも同じ規則に合わせてY反転する + // UV.y = texCoord.y * scaleY + offsetY = texCoord.y * (-1) + 1 = 1 - texCoord.y + const uniformBuffer = this.bufferManager.acquireAndWriteUniformBuffer($atlasNodeCopyUniform); + + // サンプラーとソーステクスチャ(MSAAリゾルブ済みのテクスチャ) + const sampler = this.textureManager.createSampler("container_atlas_copy", false); + const srcView = temp_attachment.texture.view; + + const bindGroup = this.device.createBindGroup({ + "layout": bindGroupLayout, + "entries": [ + { "binding": 0, "resource": { "buffer": uniformBuffer } }, + { "binding": 1, "resource": sampler }, + { "binding": 2, "resource": srcView } + ] + }); + + // アトラスのノード領域にレンダーパスを作成 + const colorView = useMsaa ? atlas.msaaTexture!.view : atlas.texture.view; + const resolveTarget = useMsaa ? atlas.texture.view : null; + + const renderPassDescriptor = this.frameBufferManager.createRenderPassDescriptor( + colorView, 0, 0, 0, 0, "load", resolveTarget + ); + + const passEncoder = this.commandEncoder!.beginRenderPass(renderPassDescriptor); + + // ビューポートとシザーでノード領域に制限 + passEncoder.setViewport(node.x, node.y, node.w, node.h, 0, 1); + passEncoder.setScissorRect(node.x, node.y, node.w, node.h); + + passEncoder.setPipeline(pipeline); + passEncoder.setBindGroup(0, bindGroup); + passEncoder.draw(6, 1, 0, 0); + passEncoder.end(); + + // アトラスレンダーパスインデックスをリセット(次のbeginNodeRenderingで新規作成させる) + this.nodeRenderPassAtlasIndex = -1; + } + /** * @description キャッシュされたコンテナフィルターテクスチャをメインに描画 * Draw a cached container filter texture to the main attachment * - * @param {IBlendMode} blend_mode - * @param {Float32Array} matrix - * @param {Float32Array} color_transform - * @param {Float32Array} filter_bounds - * @param {string} unique_key - * @param {string} filter_key + * @param {IBlendMode} blend_mode - ブレンドモード / Blend mode + * @param {Float32Array} matrix - 変換行列 / Transform matrix + * @param {Float32Array} color_transform - カラー変換パラメータ / Color transform parameters + * @param {Float32Array} filter_bounds - フィルターバウンド / Filter bounds + * @param {string} unique_key - ユニークキー / Unique key + * @param {string} filter_key - フィルターキー / Filter key + * @return {void} */ containerDrawCachedFilter ( blend_mode: IBlendMode, @@ -2482,6 +2885,9 @@ export class Context /** * @description メインテクスチャを確保(フレーム開始時に一度だけgetCurrentTexture呼び出し) + * Ensure the main texture is acquired (calls getCurrentTexture once per frame) + * + * @return {void} */ private ensureMainTexture(): void { @@ -2503,6 +2909,9 @@ export class Context /** * @description 現在の描画ターゲットのテクスチャビューを取得 + * Get the texture view of the current render target + * + * @return {GPUTextureView} 現在のテクスチャビュー / Current texture view */ private getCurrentTextureView(): GPUTextureView { @@ -2518,6 +2927,9 @@ export class Context /** * @description コマンドエンコーダーが存在することを保証 + * Ensure the command encoder exists + * + * @return {void} */ private ensureCommandEncoder(): void { @@ -2531,6 +2943,9 @@ export class Context /** * @description フレーム開始(レンダリング開始前に呼ぶ) + * Begin a new frame (call before rendering starts) + * + * @return {void} */ beginFrame(): void { @@ -2547,6 +2962,10 @@ export class Context /** * @description フレームごとのプール管理テクスチャを追加(endFrame()でプールに返却) + * Add a pooled texture for the current frame (returned to pool in endFrame()) + * + * @param {GPUTexture} texture - プール管理テクスチャ / Pooled texture + * @return {void} */ addFrameTexture (texture: GPUTexture): void { @@ -2555,6 +2974,9 @@ export class Context /** * @description フレーム終了とコマンド送信(レンダリング完了後に呼ぶ) + * End the frame and submit commands (call after rendering is complete) + * + * @return {void} */ endFrame(): void { @@ -2626,6 +3048,9 @@ export class Context /** * @description コマンドを送信(後方互換性のため残す) + * Submit commands (kept for backward compatibility) + * + * @return {void} */ submit (): void { @@ -2634,7 +3059,12 @@ export class Context /** * @description ノードを作成 + * Create a node in the texture atlas * アトラスがいっぱいの場合は新しいアトラスを作成して再試行 + * + * @param {number} width - ノード幅 / Node width + * @param {number} height - ノード高さ / Node height + * @return {Node} 作成されたノード / Created node */ createNode (width: number, height: number): Node { @@ -2660,6 +3090,10 @@ export class Context /** * @description ノードを削除 + * Remove a node from the texture atlas + * + * @param {Node} node - 削除対象ノード / Node to remove + * @return {void} */ removeNode (node: Node): void { @@ -2674,7 +3108,10 @@ export class Context /** * @description フレームバッファの描画情報をキャンバスに転送 + * Transfer frame buffer contents to the canvas * スワップチェーンはCopyDstをサポートしないため、レンダーパスでブリット + * + * @return {void} */ transferMainCanvas (): void { @@ -2743,6 +3180,11 @@ export class Context /** * @description ImageBitmapを生成 + * Create an ImageBitmap from the current rendering result + * + * @param {number} width - 画像幅 / Image width + * @param {number} height - 画像高さ / Image height + * @return {Promise} 生成されたImageBitmap / Created ImageBitmap */ async createImageBitmap (width: number, height: number): Promise { @@ -2865,6 +3307,7 @@ export class Context * @description マスク描画の開始準備 * Prepare to start drawing the mask * + * @return {void} */ beginMask(): void { @@ -2932,10 +3375,11 @@ export class Context * @description マスクの描画範囲を設定 * Set the mask drawing bounds * - * @param {number} x_min - * @param {number} y_min - * @param {number} x_max - * @param {number} y_max + * @param {number} x_min - 最小X座標 / Minimum X coordinate + * @param {number} y_min - 最小Y座標 / Minimum Y coordinate + * @param {number} x_max - 最大X座標 / Maximum X coordinate + * @param {number} y_max - 最大Y座標 / Maximum Y coordinate + * @return {void} */ setMaskBounds( x_min: number, @@ -2950,6 +3394,7 @@ export class Context * @description マスクの描画を終了 * End mask drawing * + * @return {void} */ endMask(): void { @@ -2967,8 +3412,9 @@ export class Context /** * @description マスクの終了処理 - * Mask end processing + * Mask end processing (leave the mask) * + * @return {void} */ leaveMask(): void { diff --git a/packages/webgpu/src/Context/service/ContextComputeBitmapMatrixService.ts b/packages/webgpu/src/Context/service/ContextComputeBitmapMatrixService.ts index 1d0f48b2..1b4cd789 100644 --- a/packages/webgpu/src/Context/service/ContextComputeBitmapMatrixService.ts +++ b/packages/webgpu/src/Context/service/ContextComputeBitmapMatrixService.ts @@ -1,3 +1,10 @@ +/** + * @description ビットマップ行列とコンテキスト行列からテクスチャマッピング用の逆行列を計算する + * Computes the inverse matrix for texture mapping from bitmap and context matrices + * @param {Float32Array} bitmap_matrix ビットマップ変換行列 / Bitmap transformation matrix + * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix + * @return {Float32Array} 列優先形式の3x3逆行列 / Column-major 3x3 inverse matrix + */ export const execute = (bitmap_matrix: Float32Array, context_matrix: Float32Array): Float32Array => { // ビットマップ行列 [a, b, c, d, tx, ty] const ba = bitmap_matrix[0]; diff --git a/packages/webgpu/src/Context/service/ContextComputeGradientMatrixService.ts b/packages/webgpu/src/Context/service/ContextComputeGradientMatrixService.ts index fb90e4e3..d3142308 100644 --- a/packages/webgpu/src/Context/service/ContextComputeGradientMatrixService.ts +++ b/packages/webgpu/src/Context/service/ContextComputeGradientMatrixService.ts @@ -1,15 +1,23 @@ +/** + * @description グラデーション行列からグラデーション描画用の逆行列とリニアポイントを計算する + * Computes inverse matrix and linear points for gradient rendering from gradient matrix + * @param {Float32Array} gradient_matrix グラデーション変換行列 / Gradient transformation matrix + * @param {Float32Array} _context_matrix コンテキスト変換行列(未使用) / Context transformation matrix (unused) + * @param {number} type グラデーションタイプ (0: linear, 1: radial) / Gradient type (0: linear, 1: radial) + * @return {{ inverseMatrix: Float32Array; linearPoints: Float32Array | null }} 逆行列とリニアポイント / Inverse matrix and linear points + */ export const execute = ( - gradientMatrix: Float32Array, - _contextMatrix: Float32Array, + gradient_matrix: Float32Array, + _context_matrix: Float32Array, type: number ): { inverseMatrix: Float32Array; linearPoints: Float32Array | null } => { // グラデーション行列 - const ga = gradientMatrix[0]; - const gb = gradientMatrix[1]; - const gc = gradientMatrix[2]; - const gd = gradientMatrix[3]; - const gtx = gradientMatrix[4]; - const gty = gradientMatrix[5]; + const ga = gradient_matrix[0]; + const gb = gradient_matrix[1]; + const gc = gradient_matrix[2]; + const gd = gradient_matrix[3]; + const gtx = gradient_matrix[4]; + const gty = gradient_matrix[5]; if (type === 0) { // === Linear gradient === diff --git a/packages/webgpu/src/Context/service/ContextFillSimpleService.ts b/packages/webgpu/src/Context/service/ContextFillSimpleService.ts index b6c48365..397e605c 100644 --- a/packages/webgpu/src/Context/service/ContextFillSimpleService.ts +++ b/packages/webgpu/src/Context/service/ContextFillSimpleService.ts @@ -1,6 +1,20 @@ import type { PipelineManager } from "../../Shader/PipelineManager"; import { $isMaskDrawing, $getMaskStencilReference } from "../../Mask"; +/** + * @description シンプルなフィル描画を実行する + * Executes simple fill rendering + * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {GPUBuffer} vertex_buffer 頂点バッファ / Vertex buffer + * @param {number} vertex_count 頂点数 / Vertex count + * @param {GPUBindGroup} bind_group バインドグループ / Bind group + * @param {number} uniform_offset ユニフォームオフセット / Uniform offset + * @param {boolean} use_atlas_target アトラスターゲット使用フラグ / Whether to use atlas target + * @param {boolean} use_stencil_pipeline ステンシルパイプライン使用フラグ / Whether to use stencil pipeline + * @param {number} _clip_level クリップレベル(未使用) / Clip level (unused) + * @return {void} + */ export const execute = ( render_pass_encoder: GPURenderPassEncoder, pipeline_manager: PipelineManager, diff --git a/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.test.ts b/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.test.ts new file mode 100644 index 00000000..8be54d7c --- /dev/null +++ b/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.test.ts @@ -0,0 +1,103 @@ +import { execute } from "./ContextFillWithStencilMainService"; +import { describe, expect, it, vi } from "vitest"; + +describe("ContextFillWithStencilMainService.js test", () => { + + const createMockRenderPassEncoder = () => ({ + "setPipeline": vi.fn(), + "setVertexBuffer": vi.fn(), + "setStencilReference": vi.fn(), + "setBindGroup": vi.fn(), + "draw": vi.fn() + }) as unknown as GPURenderPassEncoder; + + const createMockPipelineManager = (hasWrite = true, hasFill = true) => ({ + "getPipeline": vi.fn((name: string) => { + if (name === "stencil_write_main" && hasWrite) { + return { "label": "stencil_write_main" }; + } + if (name === "stencil_fill_main" && hasFill) { + return { "label": "stencil_fill_main" }; + } + return null; + }) + }) as any; + + it("execute test case1 - stencil write pass", () => + { + const encoder = createMockRenderPassEncoder(); + const pipelineManager = createMockPipelineManager(); + const vertexBuffer = {} as unknown as GPUBuffer; + const bindGroup = {} as unknown as GPUBindGroup; + + execute(encoder, pipelineManager, vertexBuffer, 12, bindGroup, 0); + + expect(pipelineManager.getPipeline).toHaveBeenCalledWith("stencil_write_main"); + expect(encoder.setStencilReference).toHaveBeenCalledWith(0); + expect(encoder.setVertexBuffer).toHaveBeenCalledWith(0, vertexBuffer); + }); + + it("execute test case2 - stencil fill pass", () => + { + const encoder = createMockRenderPassEncoder(); + const pipelineManager = createMockPipelineManager(); + const vertexBuffer = {} as unknown as GPUBuffer; + const bindGroup = {} as unknown as GPUBindGroup; + + execute(encoder, pipelineManager, vertexBuffer, 12, bindGroup, 0); + + expect(pipelineManager.getPipeline).toHaveBeenCalledWith("stencil_fill_main"); + }); + + it("execute test case3 - both passes execute in order", () => + { + const encoder = createMockRenderPassEncoder(); + const pipelineManager = createMockPipelineManager(); + const vertexBuffer = {} as unknown as GPUBuffer; + const bindGroup = {} as unknown as GPUBindGroup; + + execute(encoder, pipelineManager, vertexBuffer, 12, bindGroup, 0); + + expect(pipelineManager.getPipeline).toHaveBeenCalledTimes(2); + expect(encoder.setPipeline).toHaveBeenCalledTimes(2); + expect(encoder.draw).toHaveBeenCalledTimes(2); + expect(encoder.draw).toHaveBeenCalledWith(12, 1, 0, 0); + }); + + it("execute test case4 - bind group with dynamic offset", () => + { + const encoder = createMockRenderPassEncoder(); + const pipelineManager = createMockPipelineManager(); + const vertexBuffer = {} as unknown as GPUBuffer; + const bindGroup = {} as unknown as GPUBindGroup; + + execute(encoder, pipelineManager, vertexBuffer, 6, bindGroup, 256); + + expect(encoder.setBindGroup).toHaveBeenCalledWith(0, bindGroup, [256]); + }); + + it("execute test case5 - no write pipeline available", () => + { + const encoder = createMockRenderPassEncoder(); + const pipelineManager = createMockPipelineManager(false, true); + const vertexBuffer = {} as unknown as GPUBuffer; + const bindGroup = {} as unknown as GPUBindGroup; + + execute(encoder, pipelineManager, vertexBuffer, 12, bindGroup, 0); + + // write pass skipped, but fill pass should still execute + expect(encoder.draw).toHaveBeenCalledTimes(1); + }); + + it("execute test case6 - no pipelines available", () => + { + const encoder = createMockRenderPassEncoder(); + const pipelineManager = createMockPipelineManager(false, false); + const vertexBuffer = {} as unknown as GPUBuffer; + const bindGroup = {} as unknown as GPUBindGroup; + + execute(encoder, pipelineManager, vertexBuffer, 12, bindGroup, 0); + + expect(encoder.draw).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.ts b/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.ts index ce39f071..35ab516a 100644 --- a/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.ts +++ b/packages/webgpu/src/Context/service/ContextFillWithStencilMainService.ts @@ -1,5 +1,16 @@ import type { PipelineManager } from "../../Shader/PipelineManager"; +/** + * @description メインキャンバス向けのステンシル書き込みとフィル描画を2パスで実行する + * Executes two-pass stencil write and fill rendering for the main canvas + * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {GPUBuffer} vertex_buffer 頂点バッファ / Vertex buffer + * @param {number} vertex_count 頂点数 / Vertex count + * @param {GPUBindGroup} bind_group バインドグループ / Bind group + * @param {number} uniform_offset ユニフォームオフセット / Uniform offset + * @return {void} + */ export const execute = ( render_pass_encoder: GPURenderPassEncoder, pipeline_manager: PipelineManager, diff --git a/packages/webgpu/src/Context/service/ContextFillWithStencilService.ts b/packages/webgpu/src/Context/service/ContextFillWithStencilService.ts index d6a68ae9..b64cbe5c 100644 --- a/packages/webgpu/src/Context/service/ContextFillWithStencilService.ts +++ b/packages/webgpu/src/Context/service/ContextFillWithStencilService.ts @@ -1,5 +1,16 @@ import type { PipelineManager } from "../../Shader/PipelineManager"; +/** + * @description アトラスターゲット向けのステンシル書き込みとフィル描画を2パスで実行する + * Executes two-pass stencil write and fill rendering for the atlas target + * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {GPUBuffer} vertex_buffer 頂点バッファ / Vertex buffer + * @param {number} vertex_count 頂点数 / Vertex count + * @param {GPUBindGroup} bind_group バインドグループ / Bind group + * @param {number} uniform_offset ユニフォームオフセット / Uniform offset + * @return {void} + */ export const execute = ( render_pass_encoder: GPURenderPassEncoder, pipeline_manager: PipelineManager, diff --git a/packages/webgpu/src/Context/usecase/ContextApplyFilterUseCase.ts b/packages/webgpu/src/Context/usecase/ContextApplyFilterUseCase.ts index 37b789c3..4f17dd6c 100644 --- a/packages/webgpu/src/Context/usecase/ContextApplyFilterUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextApplyFilterUseCase.ts @@ -17,36 +17,90 @@ import { execute as filterApplyGradientBevelFilterUseCase } from "../../Filter/G import { execute as filterApplyGradientGlowFilterUseCase } from "../../Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase"; import { execute as filterApplyDisplacementMapFilterUseCase } from "../../Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase"; import { execute as blendApplyComplexBlendUseCase } from "../../Blend/usecase/BlendApplyComplexBlendUseCase"; - +import { + $isMaskTestEnabled, + $getMaskStencilReference +} from "../../Mask"; + +/** + * @description ユニフォームデータの事前確保配列(4要素) + * Pre-allocated uniform data array (4 elements) + */ const $uniform4 = new Float32Array(4); +/** + * @description ユニフォームデータの事前確保配列A(6要素) + * Pre-allocated uniform data array A (6 elements) + */ const $uniform6a = new Float32Array(6); +/** + * @description ユニフォームデータの事前確保配列B(6要素) + * Pre-allocated uniform data array B (6 elements) + */ const $uniform6b = new Float32Array(6); +/** + * @description ユニフォームデータの事前確保配列(8要素) + * Pre-allocated uniform data array (8 elements) + */ const $uniform8 = new Float32Array(8); +/** + * @description ユニフォームデータの事前確保配列(12要素) + * Pre-allocated uniform data array (12 elements) + */ const $uniform12 = new Float32Array(12); +/** + * @description ユニフォームデータの事前確保配列(20要素) + * Pre-allocated uniform data array (20 elements) + */ const $uniform20 = new Float32Array(20); // プリアロケート BindGroup Entry 配列 +/** + * @description バインドグループエントリの事前確保配列 + * Pre-allocated bind group entry array + */ const $entries3: GPUBindGroupEntry[] = [ { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, { "binding": 1, "resource": null as unknown as GPUSampler }, { "binding": 2, "resource": null as unknown as GPUTextureView } ]; -const SIMPLE_BLEND_MODES: ReadonlySet = new Set([ +/** + * @description シンプルなブレンドモードのセット + * Set of simple blend modes + */ +const $SIMPLE_BLEND_MODES: ReadonlySet = new Set([ "normal", "layer", "add", "screen", "alpha", "erase", "copy" ] as IBlendMode[]); -const Y_FLIP_UNIFORM = new Float32Array([1, -1, 0, 1]); - -const isIdentityColorTransform = (ct: Float32Array): boolean => { +/** + * @description Y軸反転用ユニフォームデータ + * Uniform data for Y-axis flip + */ +const $Y_FLIP_UNIFORM = new Float32Array([1, -1, 0, 1]); + +/** + * @description カラートランスフォームが恒等変換かどうかを判定する + * Checks whether the color transform is an identity transform + * @param {Float32Array} ct カラートランスフォーム配列 / Color transform array + * @return {boolean} 恒等変換の場合true / True if identity transform + */ +const $isIdentityColorTransform = (ct: Float32Array): boolean => { return ct[0] === 1 && ct[1] === 1 && ct[2] === 1 && ct[3] === 1 && ct[4] === 0 && ct[5] === 0 && ct[6] === 0 && ct[7] === 0; }; -const applyColorTransform = ( +/** + * @description フィルター結果にカラートランスフォームを適用する + * Applies color transform to the filter result + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {IAttachmentObject} attachment ソースアタッチメント / Source attachment + * @param {Float32Array} color_transform カラートランスフォーム配列 / Color transform array + * @return {IAttachmentObject} カラートランスフォーム適用後のアタッチメント / Attachment with color transform applied + */ +const $applyColorTransform = ( config: ILocalFilterConfig, attachment: IAttachmentObject, - colorTransform: Float32Array + color_transform: Float32Array ): IAttachmentObject => { const ctAttachment = config.frameBufferManager.createTemporaryAttachment( attachment.width, attachment.height @@ -61,13 +115,13 @@ const applyColorTransform = ( // uniform: mul(vec4) + add(vec4) = 32 bytes // add値は0-255スケールの生値をそのまま渡す(WebGLのフィルターCTパスと同じ) - $uniform8[0] = colorTransform[0]; - $uniform8[1] = colorTransform[1]; - $uniform8[2] = colorTransform[2]; - $uniform8[3] = colorTransform[3]; - $uniform8[4] = colorTransform[4]; - $uniform8[5] = colorTransform[5]; - $uniform8[6] = colorTransform[6]; + $uniform8[0] = color_transform[0]; + $uniform8[1] = color_transform[1]; + $uniform8[2] = color_transform[2]; + $uniform8[3] = color_transform[3]; + $uniform8[4] = color_transform[4]; + $uniform8[5] = color_transform[5]; + $uniform8[6] = color_transform[6]; $uniform8[7] = 0; const uniformBuffer = config.bufferManager.acquireAndWriteUniformBuffer($uniform8); @@ -94,7 +148,15 @@ const applyColorTransform = ( return ctAttachment; }; -const getTextureFromNode = ( +/** + * @description アトラスノードからテクスチャを取得する + * Extracts texture from an atlas node + * @param {Node} node テクスチャパッカーノード / Texture packer node + * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder + * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager + * @return {IAttachmentObject} 取得したアタッチメント / Extracted attachment + */ +const $getTextureFromNode = ( node: Node, command_encoder: GPUCommandEncoder, frame_buffer_manager: FrameBufferManager @@ -122,19 +184,36 @@ const getTextureFromNode = ( } ); } else { - console.error("[WebGPU Filter] getTextureFromNode: FAILED - missing atlas or textures"); + console.error("[WebGPU Filter] $getTextureFromNode: FAILED - missing atlas or textures"); } return attachment; }; -const isSimpleBlendMode = (blendMode: IBlendMode): boolean => { - return SIMPLE_BLEND_MODES.has(blendMode); +/** + * @description ブレンドモードがシンプルかどうかを判定する + * Checks whether the blend mode is a simple blend mode + * @param {IBlendMode} blend_mode ブレンドモード / Blend mode + * @return {boolean} シンプルブレンドモードの場合true / True if simple blend mode + */ +const $isSimpleBlendMode = (blend_mode: IBlendMode): boolean => { + return $SIMPLE_BLEND_MODES.has(blend_mode); }; -const copyMainAttachmentRegion = ( +/** + * @description メインアタッチメントの矩形領域をコピーする + * Copies a rectangular region from the main attachment + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment + * @param {number} x X座標 / X coordinate + * @param {number} y Y座標 / Y coordinate + * @param {number} width 幅 / Width + * @param {number} height 高さ / Height + * @return {IAttachmentObject} コピーされたアタッチメント / Copied attachment + */ +const $copyMainAttachmentRegion = ( config: ILocalFilterConfig, - mainAttachment: IAttachmentObject, + main_attachment: IAttachmentObject, x: number, y: number, width: number, @@ -147,15 +226,15 @@ const copyMainAttachmentRegion = ( const pipeline = config.pipelineManager.getPipeline("complex_blend_copy"); const bindGroupLayout = config.pipelineManager.getBindGroupLayout("texture_copy"); - if (!pipeline || !bindGroupLayout || !mainAttachment.texture || !dstAttachment.texture) { + if (!pipeline || !bindGroupLayout || !main_attachment.texture || !dstAttachment.texture) { return dstAttachment; } // ユニフォームバッファ: scale (vec2) + offset (vec2) - const scaleX = width / mainAttachment.width; - const scaleY = height / mainAttachment.height; - const offsetX = x / mainAttachment.width; - const offsetY = y / mainAttachment.height; + const scaleX = width / main_attachment.width; + const scaleY = height / main_attachment.height; + const offsetX = x / main_attachment.width; + const offsetY = y / main_attachment.height; $uniform4[0] = scaleX; $uniform4[1] = scaleY; @@ -167,7 +246,7 @@ const copyMainAttachmentRegion = ( ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; - $entries3[2].resource = mainAttachment.texture.view; + $entries3[2].resource = main_attachment.texture.view; const bindGroup = config.device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries3 @@ -188,31 +267,41 @@ const copyMainAttachmentRegion = ( return dstAttachment; }; -const drawBlendResultToMain = ( +/** + * @description ブレンド結果をメインアタッチメントに描画する + * Draws blend result to the main attachment + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {IAttachmentObject} src_attachment ソースアタッチメント / Source attachment + * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment + * @param {number} x X座標 / X coordinate + * @param {number} y Y座標 / Y coordinate + * @return {void} + */ +const $drawBlendResultToMain = ( config: ILocalFilterConfig, - srcAttachment: IAttachmentObject, - mainAttachment: IAttachmentObject, + src_attachment: IAttachmentObject, + main_attachment: IAttachmentObject, x: number, y: number ): void => { // フィルター+複雑なブレンド用のパイプライン(Y軸反転あり)を使用 // MSAA有効時はMSAA版パイプラインを使用してmsaaTextureに描画→texture.viewにresolve - const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view; + const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view; const pipelineName = useMsaa ? "filter_complex_blend_output_msaa" : "filter_complex_blend_output"; const pipeline = config.pipelineManager.getPipeline(pipelineName); const bindGroupLayout = config.pipelineManager.getBindGroupLayout("positioned_texture"); - if (!pipeline || !bindGroupLayout || !srcAttachment.texture || !mainAttachment.texture) { + if (!pipeline || !bindGroupLayout || !src_attachment.texture || !main_attachment.texture) { return; } // ユニフォームデータ: offset, size, viewport, padding $uniform8[0] = x; $uniform8[1] = y; - $uniform8[2] = srcAttachment.width; - $uniform8[3] = srcAttachment.height; - $uniform8[4] = mainAttachment.width; - $uniform8[5] = mainAttachment.height; + $uniform8[2] = src_attachment.width; + $uniform8[3] = src_attachment.height; + $uniform8[4] = main_attachment.width; + $uniform8[5] = main_attachment.height; $uniform8[6] = 0; $uniform8[7] = 0; const uniformBuffer = config.bufferManager.acquireAndWriteUniformBuffer($uniform8); @@ -221,7 +310,7 @@ const drawBlendResultToMain = ( ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; - $entries3[2].resource = srcAttachment.texture.view; + $entries3[2].resource = src_attachment.texture.view; const bindGroup = config.device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries3 @@ -229,8 +318,8 @@ const drawBlendResultToMain = ( // メインアタッチメントへの描画(loadで既存内容を保持) // MSAA有効時はmsaaTextureに描画してtexture.viewにresolve - const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture.view; - const resolveTarget = useMsaa ? mainAttachment.texture.view : null; + const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture.view; + const resolveTarget = useMsaa ? main_attachment.texture.view : null; const renderPassDescriptor = config.frameBufferManager.createRenderPassDescriptor( colorView, 0, 0, 0, 0, @@ -245,7 +334,20 @@ const drawBlendResultToMain = ( passEncoder.end(); }; -const drawFilterToMain = ( +/** + * @description フィルター適用結果をメインキャンバスに描画する + * Draws filter result to the main canvas + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {IAttachmentObject} filter_attachment フィルターアタッチメント / Filter attachment + * @param {Float32Array} color_transform カラートランスフォーム配列 / Color transform array + * @param {IBlendMode} blend_mode ブレンドモード / Blend mode + * @param {number} x X座標 / X coordinate + * @param {number} y Y座標 / Y coordinate + * @param {GPUTextureView} _main_texture_view メインテクスチャビュー(未使用) / Main texture view (unused) + * @param {BufferManager} _buffer_manager バッファマネージャ(未使用) / Buffer manager (unused) + * @return {void} + */ +const $drawFilterToMain = ( config: ILocalFilterConfig, filter_attachment: IAttachmentObject, color_transform: Float32Array, @@ -300,32 +402,42 @@ const drawFilterToMain = ( } // シンプルなブレンドモードの場合 - if (isSimpleBlendMode(blend_mode)) { + if ($isSimpleBlendMode(blend_mode)) { // MSAA有効時はMSAA版パイプラインを使用してmsaaTextureに描画→texture.viewにresolve const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view; + // マスクが有効かチェック + const isMasked = $isMaskTestEnabled(); + const useStencil = isMasked + && (mainAttachment.msaaStencil?.view || mainAttachment.stencil?.view); + // ブレンドモードに応じたパイプラインを選択 let pipelineName: string; - switch (blend_mode) { - case "add": - pipelineName = useMsaa ? "filter_output_add_msaa" : "filter_output_add"; - break; - case "screen": - pipelineName = useMsaa ? "filter_output_screen_msaa" : "filter_output_screen"; - break; - case "alpha": - pipelineName = useMsaa ? "filter_output_alpha_msaa" : "filter_output_alpha"; - break; - case "erase": - pipelineName = useMsaa ? "filter_output_erase_msaa" : "filter_output_erase"; - break; - case "copy": - pipelineName = useMsaa ? "texture_copy_bgra_msaa" : "texture_copy_bgra"; - break; - default: - // normal, layer - pipelineName = useMsaa ? "filter_output_msaa" : "filter_output"; - break; + if (useStencil) { + // マスク有効時はステンシルテスト付きパイプラインを使用 + pipelineName = useMsaa ? "filter_output_masked_msaa" : "filter_output_masked"; + } else { + switch (blend_mode) { + case "add": + pipelineName = useMsaa ? "filter_output_add_msaa" : "filter_output_add"; + break; + case "screen": + pipelineName = useMsaa ? "filter_output_screen_msaa" : "filter_output_screen"; + break; + case "alpha": + pipelineName = useMsaa ? "filter_output_alpha_msaa" : "filter_output_alpha"; + break; + case "erase": + pipelineName = useMsaa ? "filter_output_erase_msaa" : "filter_output_erase"; + break; + case "copy": + pipelineName = useMsaa ? "texture_copy_bgra_msaa" : "texture_copy_bgra"; + break; + default: + // normal, layer + pipelineName = useMsaa ? "filter_output_msaa" : "filter_output"; + break; + } } let pipeline = config.pipelineManager.getPipeline(pipelineName); @@ -365,12 +477,27 @@ const drawFilterToMain = ( // MSAA有効時はmsaaTextureに描画してtexture.viewにresolve const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture.view; const resolveTarget = useMsaa ? mainAttachment.texture.view : null; - const renderPassDescriptor = config.frameBufferManager.createRenderPassDescriptor( - colorView, - 0, 0, 0, 0, - "load", - resolveTarget - ); + + let renderPassDescriptor: GPURenderPassDescriptor; + if (useStencil) { + // マスク有効時はステンシル付きレンダーパスを使用 + const stencilView = useMsaa && mainAttachment.msaaStencil?.view + ? mainAttachment.msaaStencil.view : mainAttachment.stencil!.view; + renderPassDescriptor = config.frameBufferManager.createStencilRenderPassDescriptor( + colorView, + stencilView, + "load", + "load", + resolveTarget + ); + } else { + renderPassDescriptor = config.frameBufferManager.createRenderPassDescriptor( + colorView, + 0, 0, 0, 0, + "load", + resolveTarget + ); + } // Viewportはfloat値でサブピクセル精度を維持(WebGLのsetTransform相当) // ScissorはGPUIntegerCoordinate必須のため整数化し、viewport領域を包含する @@ -391,6 +518,10 @@ const drawFilterToMain = ( const passEncoder = config.commandEncoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); + // マスク有効時はステンシル参照値を設定 + if (useStencil) { + passEncoder.setStencilReference($getMaskStencilReference()); + } passEncoder.setViewport(vpX, vpY, vpW, vpH, 0, 1); passEncoder.setScissorRect(scissorX, scissorY, scissorW, scissorH); passEncoder.draw(6, 1, 0, 0); @@ -398,7 +529,7 @@ const drawFilterToMain = ( } else { // 複雑なブレンドモード(multiply, overlay, darken, lighten, hardlight等) // 1. メインアタッチメントから描画先の矩形をコピー - const dstAttachment = copyMainAttachmentRegion( + const dstAttachment = $copyMainAttachmentRegion( config, mainAttachment, drawX, drawY, drawWidth, drawHeight ); @@ -431,7 +562,7 @@ const drawFilterToMain = ( ); // 4. 結果をメインアタッチメントに描画 - drawBlendResultToMain( + $drawBlendResultToMain( config, blendedAttachment, mainAttachment, @@ -445,6 +576,23 @@ const drawFilterToMain = ( } }; +/** + * @description フィルターを適用してメインキャンバスに描画する + * Applies filters and draws the result to the main canvas + * @param {Node} node テクスチャパッカーノード / Texture packer node + * @param {number} width 幅 / Width + * @param {number} height 高さ / Height + * @param {boolean} is_bitmap ビットマップフラグ / Whether the source is a bitmap + * @param {Float32Array} matrix 変換行列 / Transformation matrix + * @param {Float32Array} color_transform カラートランスフォーム配列 / Color transform array + * @param {IBlendMode} blend_mode ブレンドモード / Blend mode + * @param {Float32Array} bounds バウンディングボックス / Bounding box + * @param {Float32Array} params フィルターパラメータ配列 / Filter parameters array + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {GPUTextureView} main_texture_view メインテクスチャビュー / Main texture view + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @return {void} + */ export const execute = ( node: Node, width: number, @@ -464,7 +612,7 @@ export const execute = ( $offset.y = 0; // ノードからテクスチャを取得 - let filterAttachment = getTextureFromNode(node, config.commandEncoder, config.frameBufferManager); + let filterAttachment = $getTextureFromNode(node, config.commandEncoder, config.frameBufferManager); // アトラスのY反転を補正 // WebGPUではアトラスに描画する際にY軸が反転して格納される: @@ -480,7 +628,7 @@ export const execute = ( const sampler = config.textureManager.createSampler("filter_flip_sampler", false); // scale=(1, -1), offset=(0, 1) で UV.y = texCoord.y * (-1) + 1 = 1 - texCoord.y - const uniformBuffer = config.bufferManager.acquireAndWriteUniformBuffer(Y_FLIP_UNIFORM); + const uniformBuffer = config.bufferManager.acquireAndWriteUniformBuffer($Y_FLIP_UNIFORM); ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; @@ -906,8 +1054,8 @@ export const execute = ( // ColorTransformが恒等変換でない場合、フィルター結果に適用 // WebGL版と同じ: フィルターチェーン適用後、メイン描画前にColorTransformを適用 - if (!isIdentityColorTransform(color_transform)) { - const ctAttachment = applyColorTransform(config, filterAttachment, color_transform); + if (!$isIdentityColorTransform(color_transform)) { + const ctAttachment = $applyColorTransform(config, filterAttachment, color_transform); config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); filterAttachment = ctAttachment; } @@ -921,7 +1069,7 @@ export const execute = ( const drawX = -offsetX + xMin + matrix[4]; const drawY = -offsetY + yMin + matrix[5]; - drawFilterToMain( + $drawFilterToMain( config, filterAttachment, color_transform, diff --git a/packages/webgpu/src/Context/usecase/ContextBitmapFillUseCase.ts b/packages/webgpu/src/Context/usecase/ContextBitmapFillUseCase.ts index a92aaa51..501550ce 100644 --- a/packages/webgpu/src/Context/usecase/ContextBitmapFillUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextBitmapFillUseCase.ts @@ -9,20 +9,67 @@ import { $getMaskStencilReference } from "../../Mask"; +/** + * @description ビットマップサンプラーのキャッシュ + * Cache for bitmap samplers + */ const $bitmapSamplerCache = new Map(); +/** + * @description ユニフォームデータの事前確保配列(32要素) + * Pre-allocated uniform data array (32 elements) + */ const $uniformData32 = new Float32Array(32); +/** + * @description ステンシルデータの事前確保配列(16要素) + * Pre-allocated stencil data array (16 elements) + */ const $stencilData16 = new Float32Array(16); +/** + * @description ステンシル用動的バインドグループのキャッシュ + * Cached dynamic bind group for stencil operations + */ let $stencilDynamicBindGroup: GPUBindGroup | null = null; +/** + * @description ステンシル用動的バッファのキャッシュ + * Cached dynamic buffer for stencil operations + */ let $stencilDynamicBuffer: GPUBuffer | null = null; +/** + * @description バインドグループエントリの事前確保配列 + * Pre-allocated bind group entry array + */ const $entries3: GPUBindGroupEntry[] = [ { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, { "binding": 1, "resource": null as unknown as GPUSampler }, { "binding": 2, "resource": null as unknown as GPUTextureView } ]; +/** + * @description ビットマップフィル描画を実行する + * Executes bitmap fill rendering + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {IPath[]} path_vertices パス頂点配列 / Path vertices array + * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix + * @param {Float32Array} fill_style フィルスタイル(RGBA) / Fill style (RGBA) + * @param {Uint8Array} pixels ピクセルデータ / Pixel data + * @param {Float32Array} bitmap_matrix ビットマップ変換行列 / Bitmap transformation matrix + * @param {number} width テクスチャ幅 / Texture width + * @param {number} height テクスチャ高さ / Texture height + * @param {boolean} repeat リピート有無 / Whether to repeat + * @param {boolean} smooth スムーズフィルタ有無 / Whether to use smooth filtering + * @param {number} viewport_width ビューポート幅 / Viewport width + * @param {number} viewport_height ビューポート高さ / Viewport height + * @param {boolean} use_atlas_target アトラスターゲット使用フラグ / Whether to use atlas target + * @param {boolean} use_stencil_pipeline ステンシルパイプライン使用フラグ / Whether to use stencil pipeline + * @param {number} _clip_level クリップレベル(未使用) / Clip level (unused) + * @return {GPUTexture | null} ビットマップテクスチャまたはnull / Bitmap texture or null + */ export const execute = ( device: GPUDevice, render_pass_encoder: GPURenderPassEncoder, diff --git a/packages/webgpu/src/Context/usecase/ContextBitmapStrokeUseCase.ts b/packages/webgpu/src/Context/usecase/ContextBitmapStrokeUseCase.ts index eaed4048..6a4a3cf6 100644 --- a/packages/webgpu/src/Context/usecase/ContextBitmapStrokeUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextBitmapStrokeUseCase.ts @@ -5,16 +5,51 @@ import { execute as meshBitmapStrokeGenerateUseCase } from "../../Mesh/usecase/M import { execute as contextComputeBitmapMatrixService } from "../service/ContextComputeBitmapMatrixService"; import { $acquireFillTexture, $releaseFillTexture } from "../../FillTexturePool"; +/** + * @description ビットマップサンプラーのキャッシュ + * Cache for bitmap samplers + */ const $bitmapSamplerCache = new Map(); +/** + * @description ユニフォームデータの事前確保配列(32要素) + * Pre-allocated uniform data array (32 elements) + */ const $uniformData32 = new Float32Array(32); +/** + * @description バインドグループエントリの事前確保配列 + * Pre-allocated bind group entry array + */ const $entries3: GPUBindGroupEntry[] = [ { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, { "binding": 1, "resource": null as unknown as GPUSampler }, { "binding": 2, "resource": null as unknown as GPUTextureView } ]; +/** + * @description ビットマップストローク描画を実行する + * Executes bitmap stroke rendering + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {IPath[]} vertices パス頂点配列 / Path vertices array + * @param {number} thickness ストローク太さ / Stroke thickness + * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix + * @param {Float32Array} stroke_style ストロークスタイル(RGBA) / Stroke style (RGBA) + * @param {Uint8Array} pixels ピクセルデータ / Pixel data + * @param {Float32Array} bitmap_matrix ビットマップ変換行列 / Bitmap transformation matrix + * @param {number} width テクスチャ幅 / Texture width + * @param {number} height テクスチャ高さ / Texture height + * @param {boolean} repeat リピート有無 / Whether to repeat + * @param {boolean} smooth スムーズフィルタ有無 / Whether to use smooth filtering + * @param {number} viewport_width ビューポート幅 / Viewport width + * @param {number} viewport_height ビューポート高さ / Viewport height + * @param {boolean} use_atlas_target アトラスターゲット使用フラグ / Whether to use atlas target + * @param {boolean} use_stencil_pipeline ステンシルパイプライン使用フラグ / Whether to use stencil pipeline + * @return {GPUTexture | null} ビットマップテクスチャまたはnull / Bitmap texture or null + */ export const execute = ( device: GPUDevice, render_pass_encoder: GPURenderPassEncoder, diff --git a/packages/webgpu/src/Context/usecase/ContextClipUseCase.ts b/packages/webgpu/src/Context/usecase/ContextClipUseCase.ts index c86eb906..bb5f6641 100644 --- a/packages/webgpu/src/Context/usecase/ContextClipUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextClipUseCase.ts @@ -6,8 +6,27 @@ import { execute as meshFillGenerateUseCase } from "../../Mesh/usecase/MeshFillG import { execute as maskUnionMaskService } from "../../Mask/service/MaskUnionMaskService"; import { $clipLevels } from "../../Mask"; +/** + * @description クリップ用ユニフォームデータの事前確保配列(16要素) + * Pre-allocated uniform data array for clipping (16 elements) + */ const $clipUniform16 = new Float32Array(16); +/** + * @description クリップ(マスク)描画を実行する + * Executes clip (mask) rendering + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {IAttachmentObject} current_attachment 現在のアタッチメント / Current attachment + * @param {IPath[]} path_vertices パス頂点配列 / Path vertices array + * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix + * @param {Float32Array} fill_style フィルスタイル(RGBA) / Fill style (RGBA) + * @param {number} global_alpha グローバルアルファ値 / Global alpha value + * @param {boolean} is_main_attachment メインアタッチメントフラグ / Whether this is the main attachment + * @return {void} + */ export const execute = ( device: GPUDevice, render_pass_encoder: GPURenderPassEncoder, diff --git a/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts b/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts new file mode 100644 index 00000000..1c558ee8 --- /dev/null +++ b/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.test.ts @@ -0,0 +1,231 @@ +import { execute } from "./ContextContainerEndLayerUseCase"; +import { describe, expect, it, vi, beforeEach } from "vitest"; + +const mockAttachment = { + "width": 800, + "height": 600, + "texture": { "view": {} }, + "msaa": false, + "msaaTexture": null +}; + +const mockMainAttachment = { + "width": 800, + "height": 600, + "texture": { "view": {} }, + "msaa": false, + "msaaTexture": null +}; + +const mockPassEncoder = { + "setPipeline": vi.fn(), + "setBindGroup": vi.fn(), + "setViewport": vi.fn(), + "setScissorRect": vi.fn(), + "draw": vi.fn(), + "end": vi.fn() +}; + +vi.mock("@next2d/cache", () => ({ + "$cacheStore": { + "set": vi.fn(), + "get": vi.fn() + } +})); + +vi.mock("../../Filter/FilterOffset", () => ({ + "$offset": { "x": 0, "y": 0 } +})); + +vi.mock("../../WebGPUUtil", () => ({ + "WebGPUUtil": { + "getDevicePixelRatio": vi.fn(() => 1) + } +})); + +vi.mock("../../Filter/BlurFilter/FilterApplyBlurFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Filter/ColorMatrixFilter/FilterApplyColorMatrixFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Filter/GlowFilter/FilterApplyGlowFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Filter/DropShadowFilter/FilterApplyDropShadowFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Filter/BevelFilter/FilterApplyBevelFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Filter/GradientBevelFilter/FilterApplyGradientBevelFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase", () => ({ + "execute": vi.fn((_att: any) => mockAttachment) +})); +vi.mock("../../Blend/usecase/BlendApplyComplexBlendUseCase", () => ({ + "execute": vi.fn(() => mockAttachment) +})); + +describe("ContextContainerEndLayerUseCase.js test", () => { + + const createMockConfig = () => ({ + "device": { + "createBindGroup": vi.fn(() => ({})) + }, + "commandEncoder": { + "beginRenderPass": vi.fn(() => mockPassEncoder) + }, + "bufferManager": { + "acquireAndWriteUniformBuffer": vi.fn(() => ({})) + }, + "frameBufferManager": { + "createTemporaryAttachment": vi.fn(() => ({ + ...mockAttachment, + "texture": { "view": {} } + })), + "releaseTemporaryAttachment": vi.fn(), + "createRenderPassDescriptor": vi.fn(() => ({})) + }, + "pipelineManager": { + "getPipeline": vi.fn(() => ({})), + "getBindGroupLayout": vi.fn(() => ({})) + }, + "textureManager": { + "createSampler": vi.fn(() => ({})) + }, + "frameTextures": [] + }) as any; + + const createMockBufferManager = () => ({ + "acquireAndWriteUniformBuffer": vi.fn(() => ({})) + }) as any; + + beforeEach(() => + { + vi.clearAllMocks(); + mockPassEncoder.setPipeline.mockClear(); + mockPassEncoder.setBindGroup.mockClear(); + mockPassEncoder.draw.mockClear(); + mockPassEncoder.end.mockClear(); + }); + + it("execute test case1 - blend only (no filter)", () => + { + const config = createMockConfig(); + const bufferManager = createMockBufferManager(); + + const matrix = new Float32Array([1, 0, 0, 1, 10, 20]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + + execute( + mockAttachment as any, + mockMainAttachment as any, + "temp", + "normal", + matrix, + colorTransform, + false, null, null, + "", "", + 800, 600, + config, + bufferManager + ); + + // Should copy region and draw to main + expect(config.frameBufferManager.createTemporaryAttachment).toHaveBeenCalled(); + expect(config.frameBufferManager.releaseTemporaryAttachment).toHaveBeenCalled(); + }); + + it("execute test case2 - with filter (blur)", () => + { + const config = createMockConfig(); + const bufferManager = createMockBufferManager(); + + const matrix = new Float32Array([1, 0, 0, 1, 10, 20]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + const filterBounds = new Float32Array([-5, -5, 110, 110]); + // BlurFilter: type=1, blurX=4, blurY=4, quality=1 + const filterParams = new Float32Array([1, 4, 4, 1]); + + execute( + mockAttachment as any, + mockMainAttachment as any, + "temp", + "normal", + matrix, + colorTransform, + true, filterBounds, filterParams, + "uk1", "fk1", + 800, 600, + config, + bufferManager + ); + + expect(config.frameBufferManager.createTemporaryAttachment).toHaveBeenCalled(); + }); + + it("execute test case3 - non-identity color transform in blend-only mode", () => + { + const config = createMockConfig(); + const bufferManager = createMockBufferManager(); + + const matrix = new Float32Array([1, 0, 0, 1, 10, 20]); + // Non-identity color transform + const colorTransform = new Float32Array([0.5, 0.5, 0.5, 1, 10, 10, 10, 0]); + + execute( + mockAttachment as any, + mockMainAttachment as any, + "temp", + "normal", + matrix, + colorTransform, + false, null, null, + "", "", + 800, 600, + config, + bufferManager + ); + + // Should apply color transform (extra createTemporaryAttachment call) + expect(config.frameBufferManager.createTemporaryAttachment).toHaveBeenCalled(); + expect(config.frameBufferManager.releaseTemporaryAttachment).toHaveBeenCalled(); + }); + + it("execute test case4 - caches filter result with uniqueKey", async () => + { + const { $cacheStore } = await import("@next2d/cache"); + const config = createMockConfig(); + const bufferManager = createMockBufferManager(); + + const matrix = new Float32Array([1, 0, 0, 1, 0, 0]); + const colorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); + const filterBounds = new Float32Array([0, 0, 100, 100]); + const filterParams = new Float32Array([1, 2, 2, 1]); // BlurFilter + + execute( + mockAttachment as any, + mockMainAttachment as any, + "temp", + "normal", + matrix, + colorTransform, + true, filterBounds, filterParams, + "cacheKey", "filterKey", + 800, 600, + config, + bufferManager + ); + + expect($cacheStore.set).toHaveBeenCalledWith("cacheKey", "fKey", "filterKey"); + expect($cacheStore.set).toHaveBeenCalledWith("cacheKey", "fTexture", expect.anything()); + }); +}); diff --git a/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts b/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts index e33c8475..8ad48391 100644 --- a/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextContainerEndLayerUseCase.ts @@ -16,24 +16,54 @@ import { execute as filterApplyGradientGlowFilterUseCase } from "../../Filter/Gr import { execute as filterApplyDisplacementMapFilterUseCase } from "../../Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase"; import { execute as blendApplyComplexBlendUseCase } from "../../Blend/usecase/BlendApplyComplexBlendUseCase"; +/** + * @description ユニフォームデータの事前確保配列(4要素) + * Pre-allocated uniform data array (4 elements) + */ const $uniform4 = new Float32Array(4); +/** + * @description ユニフォームデータの事前確保配列(8要素) + * Pre-allocated uniform data array (8 elements) + */ const $uniform8 = new Float32Array(8); +/** + * @description ユニフォームデータの事前確保配列(20要素) + * Pre-allocated uniform data array (20 elements) + */ const $uniform20 = new Float32Array(20); // プリアロケート BindGroup Entry 配列 +/** + * @description バインドグループエントリの事前確保配列 + * Pre-allocated bind group entry array + */ const $entries3: GPUBindGroupEntry[] = [ { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, { "binding": 1, "resource": null as unknown as GPUSampler }, { "binding": 2, "resource": null as unknown as GPUTextureView } ]; -const SIMPLE_BLEND_MODES: ReadonlySet = new Set([ +/** + * @description シンプルなブレンドモードのセット + * Set of simple blend modes + */ +const $SIMPLE_BLEND_MODES: ReadonlySet = new Set([ "normal", "layer", "add", "screen", "alpha", "erase", "copy" ] as IBlendMode[]); +/** + * @description 恒等カラートランスフォーム + * Identity color transform + */ const $identityColorTransform = new Float32Array([1, 1, 1, 1, 0, 0, 0, 0]); -const isIdentityColorTransform = (ct: Float32Array | null): boolean => { +/** + * @description カラートランスフォームが恒等変換かどうかを判定する + * Checks whether the color transform is an identity transform + * @param {Float32Array | null} ct カラートランスフォーム配列 / Color transform array + * @return {boolean} 恒等変換の場合true / True if identity transform + */ +const $isIdentityColorTransform = (ct: Float32Array | null): boolean => { if (!ct) { return true; } @@ -41,10 +71,18 @@ const isIdentityColorTransform = (ct: Float32Array | null): boolean => { && ct[4] === 0 && ct[5] === 0 && ct[6] === 0 && ct[7] === 0; }; -const applyColorTransform = ( +/** + * @description アタッチメントにカラートランスフォームを適用する + * Applies color transform to an attachment + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {IAttachmentObject} attachment ソースアタッチメント / Source attachment + * @param {Float32Array} color_transform カラートランスフォーム配列 / Color transform array + * @return {IAttachmentObject} カラートランスフォーム適用後のアタッチメント / Attachment with color transform applied + */ +const $applyColorTransform = ( config: ILocalFilterConfig, attachment: IAttachmentObject, - colorTransform: Float32Array + color_transform: Float32Array ): IAttachmentObject => { const ctAttachment = config.frameBufferManager.createTemporaryAttachment( attachment.width, attachment.height @@ -57,13 +95,13 @@ const applyColorTransform = ( return attachment; } - $uniform8[0] = colorTransform[0]; - $uniform8[1] = colorTransform[1]; - $uniform8[2] = colorTransform[2]; - $uniform8[3] = colorTransform[3]; - $uniform8[4] = colorTransform[4]; - $uniform8[5] = colorTransform[5]; - $uniform8[6] = colorTransform[6]; + $uniform8[0] = color_transform[0]; + $uniform8[1] = color_transform[1]; + $uniform8[2] = color_transform[2]; + $uniform8[3] = color_transform[3]; + $uniform8[4] = color_transform[4]; + $uniform8[5] = color_transform[5]; + $uniform8[6] = color_transform[6]; $uniform8[7] = 0; const uniformBuffer = config.bufferManager.acquireAndWriteUniformBuffer($uniform8); @@ -90,9 +128,20 @@ const applyColorTransform = ( return ctAttachment; }; -const copyRegionToFilterAttachment = ( +/** + * @description ソースアタッチメントの領域をフィルター用アタッチメントにコピーする + * Copies a region from source attachment to a filter attachment + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {IAttachmentObject} src_attachment ソースアタッチメント / Source attachment + * @param {number} x X座標 / X coordinate + * @param {number} y Y座標 / Y coordinate + * @param {number} width 幅 / Width + * @param {number} height 高さ / Height + * @return {IAttachmentObject} コピーされたアタッチメント / Copied attachment + */ +const $copyRegionToFilterAttachment = ( config: ILocalFilterConfig, - srcAttachment: IAttachmentObject, + src_attachment: IAttachmentObject, x: number, y: number, width: number, @@ -101,21 +150,20 @@ const copyRegionToFilterAttachment = ( const dstAttachment = config.frameBufferManager.createTemporaryAttachment(width, height); - const pipeline = config.pipelineManager.getPipeline("complex_blend_copy"); + // texture_copy_rgba8 (BlurFilterVertex, yFlipTexCoord=true) を使用 + const pipeline = config.pipelineManager.getPipeline("texture_copy_rgba8"); const bindGroupLayout = config.pipelineManager.getBindGroupLayout("texture_copy"); - if (!pipeline || !bindGroupLayout || !srcAttachment.texture || !dstAttachment.texture) { + if (!pipeline || !bindGroupLayout || !src_attachment.texture || !dstAttachment.texture) { return dstAttachment; } - const scaleX = width / srcAttachment.width; - const offsetX = x / srcAttachment.width; - - // ComplexBlendCopyVertexはOpenGL座標系のtexCoord(Y軸反転)を使用するため、 - // UV uniformでY反転を補正して正しい向きの出力を得る - // texCoord.y=1(fb上端) → uv.y=y/H(ソース上端), texCoord.y=0(fb下端) → uv.y=(y+h)/H(ソース下端) - const scaleY = -(height / srcAttachment.height); - const offsetY = (y + height) / srcAttachment.height; + // BlurFilterVertex (yFlipTexCoord=true): + // texCoord.y=0(fb上端) → uv.y=y/H, texCoord.y=1(fb下端) → uv.y=(y+h)/H + const scaleX = width / src_attachment.width; + const scaleY = height / src_attachment.height; + const offsetX = x / src_attachment.width; + const offsetY = y / src_attachment.height; $uniform4[0] = scaleX; $uniform4[1] = scaleY; @@ -126,7 +174,7 @@ const copyRegionToFilterAttachment = ( const sampler = config.textureManager.createSampler("container_copy_sampler", false); ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; - $entries3[2].resource = srcAttachment.texture.view; + $entries3[2].resource = src_attachment.texture.view; const bindGroup = config.device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries3 @@ -145,35 +193,47 @@ const copyRegionToFilterAttachment = ( return dstAttachment; }; -const drawFilterResultToMain = ( +/** + * @description フィルター結果をメインアタッチメントに描画する + * Draws filter result to the main attachment + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {IAttachmentObject} filter_attachment フィルターアタッチメント / Filter attachment + * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment + * @param {IBlendMode} blend_mode ブレンドモード / Blend mode + * @param {number} x X座標 / X coordinate + * @param {number} y Y座標 / Y coordinate + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @return {void} + */ +const $drawFilterResultToMain = ( config: ILocalFilterConfig, - filterAttachment: IAttachmentObject, - mainAttachment: IAttachmentObject, - blendMode: IBlendMode, + filter_attachment: IAttachmentObject, + main_attachment: IAttachmentObject, + blend_mode: IBlendMode, x: number, y: number, - bufferManager: BufferManager + buffer_manager: BufferManager ): void => { - if (!mainAttachment.texture || !filterAttachment.texture) { + if (!main_attachment.texture || !filter_attachment.texture) { return; } // WebGLと同じサブピクセル精度を維持するため、Math.floorを使用しない let drawX = x; let drawY = y; - let drawWidth = filterAttachment.width; - let drawHeight = filterAttachment.height; + let drawWidth = filter_attachment.width; + let drawHeight = filter_attachment.height; let uvOffsetX = 0; let uvOffsetY = 0; if (drawX < 0) { - uvOffsetX = -drawX / filterAttachment.width; + uvOffsetX = -drawX / filter_attachment.width; drawWidth += drawX; drawX = 0; } if (drawY < 0) { - uvOffsetY = -drawY / filterAttachment.height; + uvOffsetY = -drawY / filter_attachment.height; drawHeight += drawY; drawY = 0; } @@ -182,8 +242,8 @@ const drawFilterResultToMain = ( return; } - const mainWidth = mainAttachment.width; - const mainHeight = mainAttachment.height; + const mainWidth = main_attachment.width; + const mainHeight = main_attachment.height; if (drawX + drawWidth > mainWidth) { drawWidth = mainWidth - drawX; } @@ -191,12 +251,12 @@ const drawFilterResultToMain = ( drawHeight = mainHeight - drawY; } - if (SIMPLE_BLEND_MODES.has(blendMode)) { + if ($SIMPLE_BLEND_MODES.has(blend_mode)) { - const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view; + const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view; let pipelineName: string; - switch (blendMode) { + switch (blend_mode) { case "add": pipelineName = useMsaa ? "filter_output_add_msaa" : "filter_output_add"; break; @@ -226,24 +286,24 @@ const drawFilterResultToMain = ( const sampler = config.textureManager.createSampler("container_output_sampler", true); - const uvScaleX = drawWidth / filterAttachment.width; - const uvScaleY = drawHeight / filterAttachment.height; + const uvScaleX = drawWidth / filter_attachment.width; + const uvScaleY = drawHeight / filter_attachment.height; $uniform4[0] = uvScaleX; $uniform4[1] = uvScaleY; $uniform4[2] = uvOffsetX; $uniform4[3] = uvOffsetY; - const uniformBuffer = bufferManager.acquireAndWriteUniformBuffer($uniform4); + const uniformBuffer = buffer_manager.acquireAndWriteUniformBuffer($uniform4); ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; - $entries3[2].resource = filterAttachment.texture.view; + $entries3[2].resource = filter_attachment.texture.view; const bindGroup = config.device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries3 }); - const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture.view; - const resolveTarget = useMsaa ? mainAttachment.texture.view : null; + const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture.view; + const resolveTarget = useMsaa ? main_attachment.texture.view : null; const renderPassDescriptor = config.frameBufferManager.createRenderPassDescriptor( colorView, 0, 0, 0, 0, "load", resolveTarget ); @@ -273,8 +333,8 @@ const drawFilterResultToMain = ( } else { // 複雑なブレンドモード - const dstAttachment = copyRegionToFilterAttachment( - config, mainAttachment, drawX, drawY, drawWidth, drawHeight + const dstAttachment = $copyRegionToFilterAttachment( + config, main_attachment, drawX, drawY, drawWidth, drawHeight ); $uniform8[0] = $identityColorTransform[0]; @@ -287,7 +347,7 @@ const drawFilterResultToMain = ( $uniform8[7] = 0; const blendedAttachment = blendApplyComplexBlendUseCase( - filterAttachment, dstAttachment, blendMode, $uniform8, { + filter_attachment, dstAttachment, blend_mode, $uniform8, { "device": config.device, "commandEncoder": config.commandEncoder, "bufferManager": config.bufferManager, @@ -299,22 +359,22 @@ const drawFilterResultToMain = ( ); // 結果をメインに描画 - const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view; + const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view; const resultPipelineName = useMsaa ? "filter_complex_blend_output_msaa" : "filter_complex_blend_output"; const resultPipeline = config.pipelineManager.getPipeline(resultPipelineName); const resultLayout = config.pipelineManager.getBindGroupLayout("positioned_texture"); - if (resultPipeline && resultLayout && blendedAttachment.texture && mainAttachment.texture) { + if (resultPipeline && resultLayout && blendedAttachment.texture && main_attachment.texture) { $uniform8[0] = drawX; $uniform8[1] = drawY; $uniform8[2] = blendedAttachment.width; $uniform8[3] = blendedAttachment.height; - $uniform8[4] = mainAttachment.width; - $uniform8[5] = mainAttachment.height; + $uniform8[4] = main_attachment.width; + $uniform8[5] = main_attachment.height; $uniform8[6] = 0; $uniform8[7] = 0; - const uniformBuffer = bufferManager.acquireAndWriteUniformBuffer($uniform8); + const uniformBuffer = buffer_manager.acquireAndWriteUniformBuffer($uniform8); const sampler = config.textureManager.createSampler("container_blend_output_sampler", false); @@ -326,8 +386,8 @@ const drawFilterResultToMain = ( "entries": $entries3 }); - const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture.view; - const resolveTarget = useMsaa ? mainAttachment.texture.view : null; + const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture.view; + const resolveTarget = useMsaa ? main_attachment.texture.view : null; const renderPassDescriptor = config.frameBufferManager.createRenderPassDescriptor( colorView, 0, 0, 0, 0, "load", resolveTarget ); @@ -344,11 +404,21 @@ const drawFilterResultToMain = ( } }; -const applyFilterChain = ( - filterAttachment: IAttachmentObject, +/** + * @description フィルターチェーンをアタッチメントに適用する + * Applies a chain of filters to an attachment + * @param {IAttachmentObject} filter_attachment フィルターアタッチメント / Filter attachment + * @param {Float32Array} matrix 変換行列 / Transformation matrix + * @param {Float32Array} params フィルターパラメータ配列 / Filter parameters array + * @param {number} device_pixel_ratio デバイスピクセル比 / Device pixel ratio + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @return {IAttachmentObject} フィルター適用後のアタッチメント / Attachment with filters applied + */ +const $applyFilterChain = ( + filter_attachment: IAttachmentObject, matrix: Float32Array, params: Float32Array, - devicePixelRatio: number, + device_pixel_ratio: number, config: ILocalFilterConfig ): IAttachmentObject => { @@ -363,30 +433,30 @@ const applyFilterChain = ( case 0: // BevelFilter { const newAtt = filterApplyBevelFilterUseCase( - filterAttachment, matrix, + filter_attachment, matrix, params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], Boolean(params[idx++]), - devicePixelRatio, config + device_pixel_ratio, config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; case 1: // BlurFilter { const newAtt = filterApplyBlurFilterUseCase( - filterAttachment, matrix, + filter_attachment, matrix, params[idx++], params[idx++], params[idx++], - devicePixelRatio, config + device_pixel_ratio, config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; @@ -413,12 +483,12 @@ const applyFilterChain = ( $uniform20[18] = params[idx++]; $uniform20[19] = params[idx++]; const newAtt = filterApplyColorMatrixFilterUseCase( - filterAttachment, $uniform20, config + filter_attachment, $uniform20, config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; @@ -433,17 +503,17 @@ const applyFilterChain = ( } const newAtt = filterApplyConvolutionFilterUseCase( - filterAttachment, + filter_attachment, matrixX, matrixY, convMatrix, params[idx++], params[idx++], Boolean(params[idx++]), Boolean(params[idx++]), params[idx++], params[idx++], config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; @@ -456,50 +526,49 @@ const applyFilterChain = ( } const newAtt = filterApplyDisplacementMapFilterUseCase( - filterAttachment, matrix, + filter_attachment, matrix, dmBuffer, params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], - devicePixelRatio, config + device_pixel_ratio, config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; case 5: // DropShadowFilter { const newAtt = filterApplyDropShadowFilterUseCase( - filterAttachment, matrix, + filter_attachment, matrix, params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], Boolean(params[idx++]), Boolean(params[idx++]), Boolean(params[idx++]), - devicePixelRatio, config + device_pixel_ratio, config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; case 6: // GlowFilter { const newAtt = filterApplyGlowFilterUseCase( - filterAttachment, matrix, + filter_attachment, matrix, params[idx++], params[idx++], params[idx++], params[idx++], - params[idx++], params[idx++], - Boolean(params[idx++]), Boolean(params[idx++]), - devicePixelRatio, config + params[idx++], params[idx++], Boolean(params[idx++]), Boolean(params[idx++]), + device_pixel_ratio, config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; @@ -521,16 +590,16 @@ const applyFilterChain = ( for (let i = 0; i < gbRatiosLen; i++) { gbRatios[i] = params[idx++] } const newAtt = filterApplyGradientBevelFilterUseCase( - filterAttachment, matrix, + filter_attachment, matrix, gbDist, gbAngle, gbColors, gbAlphas, gbRatios, params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], Boolean(params[idx++]), - devicePixelRatio, config + device_pixel_ratio, config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; @@ -552,43 +621,63 @@ const applyFilterChain = ( for (let i = 0; i < ggRatiosLen; i++) { ggRatios[i] = params[idx++] } const newAtt = filterApplyGradientGlowFilterUseCase( - filterAttachment, matrix, + filter_attachment, matrix, ggDist, ggAngle, ggColors, ggAlphas, ggRatios, params[idx++], params[idx++], params[idx++], params[idx++], params[idx++], Boolean(params[idx++]), - devicePixelRatio, config + device_pixel_ratio, config ); - if (filterAttachment !== newAtt) { - config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); + if (filter_attachment !== newAtt) { + config.frameBufferManager.releaseTemporaryAttachment(filter_attachment); } - filterAttachment = newAtt; + filter_attachment = newAtt; } break; } } - return filterAttachment; + return filter_attachment; }; +/** + * @description コンテナレイヤーの終了処理を実行する(フィルター適用+ブレンド+メインへの描画) + * Executes container layer end processing (filter application + blending + drawing to main) + * @param {IAttachmentObject} temp_attachment 一時アタッチメント / Temporary attachment + * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment + * @param {string} _temp_name 一時名(未使用) / Temporary name (unused) + * @param {IBlendMode} blend_mode ブレンドモード / Blend mode + * @param {Float32Array} matrix 変換行列 / Transformation matrix + * @param {Float32Array | null} color_transform カラートランスフォーム配列 / Color transform array + * @param {boolean} use_filter フィルター使用フラグ / Whether to use filter + * @param {Float32Array | null} filter_bounds フィルターバウンディングボックス / Filter bounding box + * @param {Float32Array | null} params フィルターパラメータ配列 / Filter parameters array + * @param {string} unique_key ユニークキー / Unique key + * @param {string} filter_key フィルターキー / Filter key + * @param {number} _content_width コンテンツ幅(未使用) / Content width (unused) + * @param {number} _content_height コンテンツ高さ(未使用) / Content height (unused) + * @param {ILocalFilterConfig} config フィルター設定 / Filter configuration + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @return {void} + */ export const execute = ( - tempAttachment: IAttachmentObject, - mainAttachment: IAttachmentObject, - _tempName: string, - blendMode: IBlendMode, + temp_attachment: IAttachmentObject, + main_attachment: IAttachmentObject, + _temp_name: string, + blend_mode: IBlendMode, matrix: Float32Array, - colorTransform: Float32Array | null, - useFilter: boolean, - filterBounds: Float32Array | null, + color_transform: Float32Array | null, + use_filter: boolean, + filter_bounds: Float32Array | null, params: Float32Array | null, - uniqueKey: string, - filterKey: string, - _contentWidth: number, - _contentHeight: number, + unique_key: string, + filter_key: string, + _content_width: number, + _content_height: number, config: ILocalFilterConfig, - bufferManager: BufferManager + buffer_manager: BufferManager ): void => { - if (useFilter && matrix && filterBounds && params) { + if (use_filter && matrix && filter_bounds && params) { // containerEndLayerが呼ばれる=ディスプレイレイヤーがコンテンツ変更を検出して再レンダリングを要求 // 常に新鮮なテクスチャを抽出してフィルターを適用する @@ -597,26 +686,26 @@ export const execute = ( // WebGL版と同じ: レイヤー全体をフィルター用にコピー // レイヤーはコンテンツサイズで作成され、childrenは相対座標で描画されているため // (0, 0, layerWidth, layerHeight) = コンテンツ全体 - let filterAttachment = copyRegionToFilterAttachment( - config, tempAttachment, - 0, 0, tempAttachment.width, tempAttachment.height + let filterAttachment = $copyRegionToFilterAttachment( + config, temp_attachment, + 0, 0, temp_attachment.width, temp_attachment.height ); // 一時アタッチメントを遅延解放(コマンドバッファsubmit後に解放) // destroyAttachmentは即座にGPUテクスチャを破棄するため、 // コマンドエンコーダに記録済みのレンダーパスが参照するテクスチャが無効になる - config.frameBufferManager.releaseTemporaryAttachment(tempAttachment); + config.frameBufferManager.releaseTemporaryAttachment(temp_attachment); // フィルターチェーンを適用 const devicePixelRatio = WebGPUUtil.getDevicePixelRatio(); - filterAttachment = applyFilterChain( + filterAttachment = $applyFilterChain( filterAttachment, matrix, params, devicePixelRatio, config ); // キャッシュに保存 - if (uniqueKey) { - $cacheStore.set(uniqueKey, "fKey", filterKey); - $cacheStore.set(uniqueKey, "fTexture", filterAttachment); + if (unique_key) { + $cacheStore.set(unique_key, "fKey", filter_key); + $cacheStore.set(unique_key, "fTexture", filterAttachment); } // フィルター結果をメインに描画 @@ -625,23 +714,23 @@ export const execute = ( // キャッシュにはフィルター結果のみ保存(CTは毎フレーム適用する) let drawAttachment = filterAttachment; let ctAttachment: IAttachmentObject | null = null; - if (!isIdentityColorTransform(colorTransform)) { - ctAttachment = applyColorTransform(config, filterAttachment, colorTransform!); + if (!$isIdentityColorTransform(color_transform)) { + ctAttachment = $applyColorTransform(config, filterAttachment, color_transform!); drawAttachment = ctAttachment; } const scaleX = Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1]); const scaleY = Math.sqrt(matrix[2] * matrix[2] + matrix[3] * matrix[3]); - const boundsXMin = filterBounds[0] * (scaleX / devicePixelRatio); - const boundsYMin = filterBounds[1] * (scaleY / devicePixelRatio); + const boundsXMin = filter_bounds[0] * (scaleX / devicePixelRatio); + const boundsYMin = filter_bounds[1] * (scaleY / devicePixelRatio); // WebGL版と同じ: boundsXMin + matrix[4] で絶対位置 const drawX = boundsXMin + matrix[4]; const drawY = boundsYMin + matrix[5]; - drawFilterResultToMain( - config, drawAttachment, mainAttachment, - blendMode, drawX, drawY, bufferManager + $drawFilterResultToMain( + config, drawAttachment, main_attachment, + blend_mode, drawX, drawY, buffer_manager ); // CT一時アタッチメントを解放 @@ -649,7 +738,7 @@ export const execute = ( config.frameBufferManager.releaseTemporaryAttachment(ctAttachment); } // キャッシュされていないフィルター結果のみ解放 - if (!uniqueKey) { + if (!unique_key) { config.frameBufferManager.releaseTemporaryAttachment(filterAttachment); } } @@ -657,25 +746,25 @@ export const execute = ( } else { // ブレンドのみ:レイヤー全体をフィルター用にコピーしてメインに描画 - let fullAttachment = copyRegionToFilterAttachment( - config, tempAttachment, - 0, 0, tempAttachment.width, tempAttachment.height + let fullAttachment = $copyRegionToFilterAttachment( + config, temp_attachment, + 0, 0, temp_attachment.width, temp_attachment.height ); // 一時アタッチメントを遅延解放(コマンドバッファsubmit後に解放) - config.frameBufferManager.releaseTemporaryAttachment(tempAttachment); + config.frameBufferManager.releaseTemporaryAttachment(temp_attachment); // ColorTransformが恒等変換でない場合、適用 - if (!isIdentityColorTransform(colorTransform)) { - const ctAttachment = applyColorTransform(config, fullAttachment, colorTransform!); + if (!$isIdentityColorTransform(color_transform)) { + const ctAttachment = $applyColorTransform(config, fullAttachment, color_transform!); config.frameBufferManager.releaseTemporaryAttachment(fullAttachment); fullAttachment = ctAttachment; } // WebGL版と同じ: matrix[4], matrix[5] = layerBounds の絶対位置に描画 - drawFilterResultToMain( - config, fullAttachment, mainAttachment, - blendMode, matrix[4], matrix[5], bufferManager + $drawFilterResultToMain( + config, fullAttachment, main_attachment, + blend_mode, matrix[4], matrix[5], buffer_manager ); config.frameBufferManager.releaseTemporaryAttachment(fullAttachment); diff --git a/packages/webgpu/src/Context/usecase/ContextDrawArraysInstancedUseCase.ts b/packages/webgpu/src/Context/usecase/ContextDrawArraysInstancedUseCase.ts index 74b4980c..bd0051b4 100644 --- a/packages/webgpu/src/Context/usecase/ContextDrawArraysInstancedUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextDrawArraysInstancedUseCase.ts @@ -13,9 +13,50 @@ import { } from "../../Mask"; import { $getAtlasAttachmentObject } from "../../AtlasManager"; +/** + * @description キャッシュ済みバインドグループ + * Cached bind group + */ let $cachedBindGroup: GPUBindGroup | null = null; +/** + * @description キャッシュ済みアトラステクスチャビュー + * Cached atlas texture view + */ let $cachedAtlasView: GPUTextureView | null = null; +/** + * @description ブレンドモードに応じたインスタンスパイプライン名を返す + */ +const $getPipelineName = (mode: IBlendMode): string => { + switch (mode) { + case "add": + return "instanced_add"; + case "screen": + return "instanced_screen"; + case "alpha": + return "instanced_alpha"; + case "erase": + return "instanced_erase"; + case "copy": + return "instanced_copy"; + default: + return "instanced"; + } +}; + +/** + * @description インスタンス描画を実行する + * Executes instanced array drawing + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder + * @param {GPURenderPassEncoder | null} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager + * @param {TextureManager} texture_manager テクスチャマネージャ / Texture manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @return {GPURenderPassEncoder | null} レンダーパスエンコーダまたはnull / Render pass encoder or null + */ export const execute = ( device: GPUDevice, command_encoder: GPUCommandEncoder, @@ -44,27 +85,7 @@ export const execute = ( // 現在のブレンドモードを取得 const blendMode: IBlendMode = $currentBlendMode; - // ブレンドモードに応じたパイプライン名を生成 - // simpleBlendModes: normal, layer, add, screen, alpha, erase, copy - const getPipelineName = (mode: IBlendMode): string => { - switch (mode) { - case "add": - return "instanced_add"; - case "screen": - return "instanced_screen"; - case "alpha": - return "instanced_alpha"; - case "erase": - return "instanced_erase"; - case "copy": - return "instanced_copy"; - default: - // normal, layer - return "instanced"; - } - }; - - const pipelineName = getPipelineName(blendMode); + const pipelineName = $getPipelineName(blendMode); const normalPipeline = pipeline_manager.getPipeline(pipelineName); const maskedPipeline = pipeline_manager.getPipeline("instanced_masked"); diff --git a/packages/webgpu/src/Context/usecase/ContextDrawIndirectUseCase.ts b/packages/webgpu/src/Context/usecase/ContextDrawIndirectUseCase.ts index b51c01b6..314b552a 100644 --- a/packages/webgpu/src/Context/usecase/ContextDrawIndirectUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextDrawIndirectUseCase.ts @@ -13,31 +13,54 @@ import { } from "../../Mask"; import { $getAtlasAttachmentObject } from "../../AtlasManager"; +/** + * @description キャッシュ済みバインドグループ + * Cached bind group + */ let $cachedBindGroup: GPUBindGroup | null = null; +/** + * @description キャッシュ済みアトラステクスチャビュー + * Cached atlas texture view + */ let $cachedAtlasView: GPUTextureView | null = null; +/** + * @description Indirect描画を使用したインスタンス描画を実行する + * Executes instanced drawing with indirect draw support + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder + * @param {GPURenderPassEncoder | null} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager + * @param {TextureManager} texture_manager テクスチャマネージャ / Texture manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {boolean} use_indirect Indirect描画使用フラグ / Whether to use indirect drawing + * @param {boolean} use_storage_buffer StorageBuffer使用フラグ / Whether to use storage buffer + * @return {GPURenderPassEncoder | null} レンダーパスエンコーダまたはnull / Render pass encoder or null + */ export const execute = ( device: GPUDevice, - commandEncoder: GPUCommandEncoder, - renderPassEncoder: GPURenderPassEncoder | null, - mainAttachment: IAttachmentObject, - bufferManager: BufferManager, - frameBufferManager: FrameBufferManager, - textureManager: TextureManager, - pipelineManager: PipelineManager, - useIndirect: boolean = true, - useStorageBuffer: boolean = true + command_encoder: GPUCommandEncoder, + render_pass_encoder: GPURenderPassEncoder | null, + main_attachment: IAttachmentObject, + buffer_manager: BufferManager, + frame_buffer_manager: FrameBufferManager, + texture_manager: TextureManager, + pipeline_manager: PipelineManager, + use_indirect: boolean = true, + use_storage_buffer: boolean = true ): GPURenderPassEncoder | null => { const shaderManager = getInstancedShaderManager(); if (shaderManager.count === 0) { - return renderPassEncoder; + return render_pass_encoder; } // 既存のレンダーパスを終了 - if (renderPassEncoder) { - renderPassEncoder.end(); - renderPassEncoder = null; + if (render_pass_encoder) { + render_pass_encoder.end(); + render_pass_encoder = null; } const isMasked = $isMaskTestEnabled(); @@ -66,11 +89,11 @@ export const execute = ( }; const pipelineName = getPipelineName(blendMode); - const normalPipeline = pipelineManager.getPipeline(pipelineName); - const maskedPipeline = pipelineManager.getPipeline("instanced_masked"); + const normalPipeline = pipeline_manager.getPipeline(pipelineName); + const maskedPipeline = pipeline_manager.getPipeline("instanced_masked"); const useStencil = isMasked && maskedPipeline - && (mainAttachment.msaaStencil?.view || mainAttachment.stencil?.view); + && (main_attachment.msaaStencil?.view || main_attachment.stencil?.view); const pipeline = useStencil ? maskedPipeline : normalPipeline; @@ -84,32 +107,32 @@ export const execute = ( if (useStencil) { // MSAA対応 - const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view; - const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture!.view; - const stencilView = useMsaa && mainAttachment.msaaStencil?.view - ? mainAttachment.msaaStencil.view : mainAttachment.stencil!.view; - const resolveTarget = useMsaa ? mainAttachment.texture!.view : null; + const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view; + const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture!.view; + const stencilView = useMsaa && main_attachment.msaaStencil?.view + ? main_attachment.msaaStencil.view : main_attachment.stencil!.view; + const resolveTarget = useMsaa ? main_attachment.texture!.view : null; - const renderPassDescriptor = frameBufferManager.createStencilRenderPassDescriptor( + const renderPassDescriptor = frame_buffer_manager.createStencilRenderPassDescriptor( colorView, stencilView, "load", "load", resolveTarget ); - passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder = command_encoder.beginRenderPass(renderPassDescriptor); } else { // 通常のレンダーパス(MSAA対応) - const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view; - const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture!.view; - const resolveTarget = useMsaa ? mainAttachment.texture!.view : null; - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( + const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view; + const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture!.view; + const resolveTarget = useMsaa ? main_attachment.texture!.view : null; + const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor( colorView, 0, 0, 0, 0, "load", resolveTarget ); - passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + passEncoder = command_encoder.beginRenderPass(renderPassDescriptor); } passEncoder.setPipeline(pipeline); @@ -127,22 +150,22 @@ export const execute = ( // インスタンスバッファを作成または取得 let instanceBuffer: GPUBuffer; - if (useStorageBuffer) { + if (use_storage_buffer) { // Storage Buffer最適化: プールから再利用してメモリアロケーション削減 // Storage BufferはVERTEXフラグ付きで作成されているため、setVertexBufferで使用可能 - instanceBuffer = bufferManager.acquireStorageBuffer(instanceData.byteLength); - bufferManager.writeStorageBuffer(instanceBuffer, instanceData); + instanceBuffer = buffer_manager.acquireStorageBuffer(instanceData.byteLength); + buffer_manager.writeStorageBuffer(instanceBuffer, instanceData); } else { // 従来方式: プールから再利用 - instanceBuffer = bufferManager.acquireVertexBuffer(instanceData.byteLength, instanceData); + instanceBuffer = buffer_manager.acquireVertexBuffer(instanceData.byteLength, instanceData); } // 頂点バッファ(矩形)を取得(キャッシュ済み) - const vertexBuffer = bufferManager.getUnitRectBuffer(); + const vertexBuffer = buffer_manager.getUnitRectBuffer(); // アトラステクスチャをバインド(複数アトラス対応) // AtlasManagerから取得、フォールバックとしてFrameBufferManagerから取得 - const atlasAttachment = $getAtlasAttachmentObject() || frameBufferManager.getAttachment("atlas"); + const atlasAttachment = $getAtlasAttachmentObject() || frame_buffer_manager.getAttachment("atlas"); if (!atlasAttachment) { console.error("[WebGPU] Atlas attachment not found"); passEncoder.end(); @@ -150,9 +173,9 @@ export const execute = ( } // アトラス用サンプラーを取得(キャッシュ済み) - const sampler = textureManager.createSampler("atlas_instanced_sampler", false); + const sampler = texture_manager.createSampler("atlas_instanced_sampler", false); - const bindGroupLayout = pipelineManager.getBindGroupLayout("instanced"); + const bindGroupLayout = pipeline_manager.getBindGroupLayout("instanced"); if (!bindGroupLayout) { console.error("[WebGPU] Instanced bind group layout not found"); passEncoder.end(); @@ -183,13 +206,13 @@ export const execute = ( passEncoder.setVertexBuffer(1, instanceBuffer); passEncoder.setBindGroup(0, $cachedBindGroup); - if (useIndirect) { + if (use_indirect) { // Indirect Drawing: CPU-GPU間のオーバーヘッドを削減 // 注意: 1フレーム内で複数回呼び出される場合があるため、 // 毎回新しいIndirect Bufferを作成する必要がある // (共有バッファを使うとqueue.writeBufferの更新が全てGPU実行前に行われ、 // 全てのdrawIndirectが最後の更新値を使用してしまう) - const indirectBuffer = bufferManager.createIndirectBuffer( + const indirectBuffer = buffer_manager.createIndirectBuffer( 6, // vertexCount (2 triangles = 6 vertices) shaderManager.count, // instanceCount 0, // firstVertex diff --git a/packages/webgpu/src/Context/usecase/ContextGradientFillUseCase.ts b/packages/webgpu/src/Context/usecase/ContextGradientFillUseCase.ts index ccb5073c..f22e9550 100644 --- a/packages/webgpu/src/Context/usecase/ContextGradientFillUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextGradientFillUseCase.ts @@ -11,20 +11,67 @@ import { $getMaskStencilReference } from "../../Mask"; +/** + * @description グラデーションサンプラーのキャッシュ + * Cached gradient sampler + */ let $gradientSampler: GPUSampler | null = null; +/** + * @description ユニフォームデータの事前確保配列(36要素) + * Pre-allocated uniform data array (36 elements) + */ const $uniformData36 = new Float32Array(36); +/** + * @description ステンシルデータの事前確保配列(16要素) + * Pre-allocated stencil data array (16 elements) + */ const $stencilData16 = new Float32Array(16); +/** + * @description ステンシル用動的バインドグループのキャッシュ + * Cached dynamic bind group for stencil operations + */ let $stencilDynamicBindGroup: GPUBindGroup | null = null; +/** + * @description ステンシル用動的バッファのキャッシュ + * Cached dynamic buffer for stencil operations + */ let $stencilDynamicBuffer: GPUBuffer | null = null; +/** + * @description バインドグループエントリの事前確保配列 + * Pre-allocated bind group entry array + */ const $entries3: GPUBindGroupEntry[] = [ { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, { "binding": 1, "resource": null as unknown as GPUSampler }, { "binding": 2, "resource": null as unknown as GPUTextureView } ]; +/** + * @description グラデーションフィル描画を実行する + * Executes gradient fill rendering + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {IPath[]} path_vertices パス頂点配列 / Path vertices array + * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix + * @param {Float32Array} fill_style フィルスタイル(RGBA) / Fill style (RGBA) + * @param {number} type グラデーションタイプ / Gradient type + * @param {number[]} stops グラデーションストップ配列 / Gradient stops array + * @param {Float32Array} gradient_matrix グラデーション変換行列 / Gradient transformation matrix + * @param {number} spread スプレッドモード / Spread mode + * @param {number} interpolation 補間モード / Interpolation mode + * @param {number} focal 焦点距離 / Focal point + * @param {number} viewport_width ビューポート幅 / Viewport width + * @param {number} viewport_height ビューポート高さ / Viewport height + * @param {boolean} use_atlas_target アトラスターゲット使用フラグ / Whether to use atlas target + * @param {boolean} use_stencil_pipeline ステンシルパイプライン使用フラグ / Whether to use stencil pipeline + * @param {number} _clip_level クリップレベル(未使用) / Clip level (unused) + * @return {GPUTexture | null} LUTテクスチャまたはnull / LUT texture or null + */ export const execute = ( device: GPUDevice, render_pass_encoder: GPURenderPassEncoder, diff --git a/packages/webgpu/src/Context/usecase/ContextGradientStrokeUseCase.ts b/packages/webgpu/src/Context/usecase/ContextGradientStrokeUseCase.ts index f4a35384..3cd2789b 100644 --- a/packages/webgpu/src/Context/usecase/ContextGradientStrokeUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextGradientStrokeUseCase.ts @@ -7,16 +7,51 @@ import { execute as contextComputeGradientMatrixService } from "../service/Conte import { $getLUTFromCache, $putLUTToCache } from "../../Gradient/GradientLUTCache"; import { $acquireFillTexture } from "../../FillTexturePool"; +/** + * @description グラデーションサンプラーのキャッシュ + * Cached gradient sampler + */ let $gradientSampler: GPUSampler | null = null; +/** + * @description ユニフォームデータの事前確保配列(36要素) + * Pre-allocated uniform data array (36 elements) + */ const $uniformData36 = new Float32Array(36); +/** + * @description バインドグループエントリの事前確保配列 + * Pre-allocated bind group entry array + */ const $entries3: GPUBindGroupEntry[] = [ { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, { "binding": 1, "resource": null as unknown as GPUSampler }, { "binding": 2, "resource": null as unknown as GPUTextureView } ]; +/** + * @description グラデーションストローク描画を実行する + * Executes gradient stroke rendering + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPURenderPassEncoder} render_pass_encoder レンダーパスエンコーダ / Render pass encoder + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {IPath[]} vertices パス頂点配列 / Path vertices array + * @param {number} thickness ストローク太さ / Stroke thickness + * @param {Float32Array} context_matrix コンテキスト変換行列 / Context transformation matrix + * @param {Float32Array} stroke_style ストロークスタイル(RGBA) / Stroke style (RGBA) + * @param {number} type グラデーションタイプ / Gradient type + * @param {number[]} stops グラデーションストップ配列 / Gradient stops array + * @param {Float32Array} gradient_matrix グラデーション変換行列 / Gradient transformation matrix + * @param {number} spread スプレッドモード / Spread mode + * @param {number} interpolation 補間モード / Interpolation mode + * @param {number} focal 焦点距離 / Focal point + * @param {number} viewport_width ビューポート幅 / Viewport width + * @param {number} viewport_height ビューポート高さ / Viewport height + * @param {boolean} use_atlas_target アトラスターゲット使用フラグ / Whether to use atlas target + * @param {boolean} use_stencil_pipeline ステンシルパイプライン使用フラグ / Whether to use stencil pipeline + * @return {GPUTexture | null} LUTテクスチャまたはnull / LUT texture or null + */ export const execute = ( device: GPUDevice, render_pass_encoder: GPURenderPassEncoder, diff --git a/packages/webgpu/src/Context/usecase/ContextProcessComplexBlendQueueUseCase.ts b/packages/webgpu/src/Context/usecase/ContextProcessComplexBlendQueueUseCase.ts index c3717047..95d86aca 100644 --- a/packages/webgpu/src/Context/usecase/ContextProcessComplexBlendQueueUseCase.ts +++ b/packages/webgpu/src/Context/usecase/ContextProcessComplexBlendQueueUseCase.ts @@ -8,135 +8,201 @@ import { execute as blendApplyComplexBlendUseCase } from "../../Blend/usecase/Bl import { $getAtlasAttachmentObject } from "../../AtlasManager"; // プリアロケート配列 +/** + * @description ユニフォームデータの事前確保配列(4要素) + * Pre-allocated uniform data array (4 elements) + */ const $uniform4 = new Float32Array(4); +/** + * @description ユニフォームデータの事前確保配列(6要素) + * Pre-allocated uniform data array (6 elements) + */ const $uniform6 = new Float32Array(6); +/** + * @description ユニフォームデータの事前確保配列(8要素) + * Pre-allocated uniform data array (8 elements) + */ const $uniform8 = new Float32Array(8); +/** + * @description ユニフォームデータの事前確保配列(12要素) + * Pre-allocated uniform data array (12 elements) + */ const $uniform12 = new Float32Array(12); // プリアロケート BindGroup Entry 配列 +/** + * @description バインドグループエントリの事前確保配列 + * Pre-allocated bind group entry array + */ const $entries3: GPUBindGroupEntry[] = [ { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, { "binding": 1, "resource": null as unknown as GPUSampler }, { "binding": 2, "resource": null as unknown as GPUTextureView } ]; -const copyTextureRegionViaRenderPass = ( +/** + * @description レンダーパスを使用してテクスチャ領域をコピーする + * Copies a texture region via render pass + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder + * @param {GPUTextureView} src_view ソーステクスチャビュー / Source texture view + * @param {IAttachmentObject} dst_attachment デスティネーションアタッチメント / Destination attachment + * @param {number} src_x ソースX座標 / Source X coordinate + * @param {number} src_y ソースY座標 / Source Y coordinate + * @param {number} src_width ソース幅 / Source width + * @param {number} src_height ソース高さ / Source height + * @param {number} copy_width コピー幅 / Copy width + * @param {number} copy_height コピー高さ / Copy height + * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager + * @param {TextureManager} texture_manager テクスチャマネージャ / Texture manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @return {void} + */ +const $copyTextureRegionViaRenderPass = ( device: GPUDevice, - commandEncoder: GPUCommandEncoder, - srcView: GPUTextureView, - dstAttachment: IAttachmentObject, - srcX: number, - srcY: number, - srcWidth: number, - srcHeight: number, - copyWidth: number, - copyHeight: number, - frameBufferManager: FrameBufferManager, - textureManager: TextureManager, - pipelineManager: PipelineManager, - bufferManager: BufferManager + command_encoder: GPUCommandEncoder, + src_view: GPUTextureView, + dst_attachment: IAttachmentObject, + src_x: number, + src_y: number, + src_width: number, + src_height: number, + copy_width: number, + copy_height: number, + frame_buffer_manager: FrameBufferManager, + texture_manager: TextureManager, + pipeline_manager: PipelineManager, + buffer_manager: BufferManager ): void => { - const pipeline = pipelineManager.getPipeline("complex_blend_copy"); - const bindGroupLayout = pipelineManager.getBindGroupLayout("texture_copy"); + const pipeline = pipeline_manager.getPipeline("complex_blend_copy"); + const bindGroupLayout = pipeline_manager.getBindGroupLayout("texture_copy"); if (!pipeline || !bindGroupLayout) { return; } - $uniform4[0] = copyWidth / srcWidth; - $uniform4[1] = copyHeight / srcHeight; - $uniform4[2] = srcX / srcWidth; - $uniform4[3] = srcY / srcHeight; - const uniformBuffer = bufferManager.acquireAndWriteUniformBuffer($uniform4); + $uniform4[0] = copy_width / src_width; + $uniform4[1] = copy_height / src_height; + $uniform4[2] = src_x / src_width; + $uniform4[3] = src_y / src_height; + const uniformBuffer = buffer_manager.acquireAndWriteUniformBuffer($uniform4); - const sampler = textureManager.createSampler("complex_blend_copy_sampler", false); + const sampler = texture_manager.createSampler("complex_blend_copy_sampler", false); ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; - $entries3[2].resource = srcView; + $entries3[2].resource = src_view; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries3 }); - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( - dstAttachment.texture!.view, + const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor( + dst_attachment.texture!.view, 0, 0, 0, 0, "clear" ); - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.draw(6, 1, 0, 0); passEncoder.end(); }; -const drawToMainAttachment = ( +/** + * @description ブレンド結果をメインアタッチメントに描画する + * Draws blend result to the main attachment + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder + * @param {IAttachmentObject} src_attachment ソースアタッチメント / Source attachment + * @param {IAttachmentObject} main_attachment メインアタッチメント / Main attachment + * @param {number} dst_x デスティネーションX座標 / Destination X coordinate + * @param {number} dst_y デスティネーションY座標 / Destination Y coordinate + * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager + * @param {TextureManager} texture_manager テクスチャマネージャ / Texture manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @return {void} + */ +const $drawToMainAttachment = ( device: GPUDevice, - commandEncoder: GPUCommandEncoder, - srcAttachment: IAttachmentObject, - mainAttachment: IAttachmentObject, - dstX: number, - dstY: number, - frameBufferManager: FrameBufferManager, - textureManager: TextureManager, - pipelineManager: PipelineManager, - bufferManager: BufferManager + command_encoder: GPUCommandEncoder, + src_attachment: IAttachmentObject, + main_attachment: IAttachmentObject, + dst_x: number, + dst_y: number, + frame_buffer_manager: FrameBufferManager, + texture_manager: TextureManager, + pipeline_manager: PipelineManager, + buffer_manager: BufferManager ): void => { - const useMsaa = mainAttachment.msaa && mainAttachment.msaaTexture?.view; + const useMsaa = main_attachment.msaa && main_attachment.msaaTexture?.view; const pipelineName = useMsaa ? "complex_blend_output_msaa" : "complex_blend_output"; - const pipeline = pipelineManager.getPipeline(pipelineName); - const bindGroupLayout = pipelineManager.getBindGroupLayout("positioned_texture"); + const pipeline = pipeline_manager.getPipeline(pipelineName); + const bindGroupLayout = pipeline_manager.getBindGroupLayout("positioned_texture"); if (!pipeline || !bindGroupLayout) { return; } - $uniform8[0] = dstX; - $uniform8[1] = dstY; - $uniform8[2] = srcAttachment.width; - $uniform8[3] = srcAttachment.height; - $uniform8[4] = mainAttachment.width; - $uniform8[5] = mainAttachment.height; + $uniform8[0] = dst_x; + $uniform8[1] = dst_y; + $uniform8[2] = src_attachment.width; + $uniform8[3] = src_attachment.height; + $uniform8[4] = main_attachment.width; + $uniform8[5] = main_attachment.height; $uniform8[6] = 0; $uniform8[7] = 0; - const uniformBuffer = bufferManager.acquireAndWriteUniformBuffer($uniform8); + const uniformBuffer = buffer_manager.acquireAndWriteUniformBuffer($uniform8); - const sampler = textureManager.createSampler("complex_blend_output_sampler", false); + const sampler = texture_manager.createSampler("complex_blend_output_sampler", false); ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; - $entries3[2].resource = srcAttachment.texture!.view; + $entries3[2].resource = src_attachment.texture!.view; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries3 }); - const colorView = useMsaa ? mainAttachment.msaaTexture!.view : mainAttachment.texture!.view; - const resolveTarget = useMsaa ? mainAttachment.texture!.view : null; - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( + const colorView = useMsaa ? main_attachment.msaaTexture!.view : main_attachment.texture!.view; + const resolveTarget = useMsaa ? main_attachment.texture!.view : null; + const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor( colorView, 0, 0, 0, 0, "load", resolveTarget ); - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.draw(6, 1, 0, 0); passEncoder.end(); }; +/** + * @description 複雑なブレンドモードキューを処理する + * Processes the complex blend mode queue + * @param {GPUDevice} device GPUデバイス / GPU device + * @param {GPUCommandEncoder} command_encoder コマンドエンコーダ / Command encoder + * @param {IAttachmentObject | null} main_attachment メインアタッチメント / Main attachment + * @param {FrameBufferManager} frame_buffer_manager フレームバッファマネージャ / Frame buffer manager + * @param {TextureManager} texture_manager テクスチャマネージャ / Texture manager + * @param {PipelineManager} pipeline_manager パイプラインマネージャ / Pipeline manager + * @param {BufferManager} buffer_manager バッファマネージャ / Buffer manager + * @return {void} + */ export const execute = ( device: GPUDevice, - commandEncoder: GPUCommandEncoder, - mainAttachment: IAttachmentObject | null, - frameBufferManager: FrameBufferManager, - textureManager: TextureManager, - pipelineManager: PipelineManager, - bufferManager: BufferManager + command_encoder: GPUCommandEncoder, + main_attachment: IAttachmentObject | null, + frame_buffer_manager: FrameBufferManager, + texture_manager: TextureManager, + pipeline_manager: PipelineManager, + buffer_manager: BufferManager ): void => { const queue = getComplexBlendQueue(); @@ -144,12 +210,12 @@ export const execute = ( return; } - if (!mainAttachment || !mainAttachment.texture) { + if (!main_attachment || !main_attachment.texture) { clearComplexBlendQueue(); return; } - const atlasAttachment = $getAtlasAttachmentObject() || frameBufferManager.getAttachment("atlas"); + const atlasAttachment = $getAtlasAttachmentObject() || frame_buffer_manager.getAttachment("atlas"); if (!atlasAttachment || !atlasAttachment.texture) { clearComplexBlendQueue(); return; @@ -168,7 +234,7 @@ export const execute = ( const dstX = Math.max(0, Math.floor(matrix[6])); const dstY = Math.max(0, Math.floor(matrix[7])); - if (dstX >= mainAttachment.width || dstY >= mainAttachment.height) { + if (dstX >= main_attachment.width || dstY >= main_attachment.height) { continue; } @@ -177,8 +243,8 @@ export const execute = ( const blendWidth = hasScale ? width : node.w; const blendHeight = hasScale ? height : node.h; - const clippedWidth = Math.min(blendWidth, mainAttachment.width - dstX); - const clippedHeight = Math.min(blendHeight, mainAttachment.height - dstY); + const clippedWidth = Math.min(blendWidth, main_attachment.width - dstX); + const clippedHeight = Math.min(blendHeight, main_attachment.height - dstY); if (clippedWidth <= 0 || clippedHeight <= 0) { continue; } @@ -187,10 +253,10 @@ export const execute = ( let srcAttachment: IAttachmentObject; if (hasScale) { - srcAttachment = frameBufferManager.createTemporaryAttachment(blendWidth, blendHeight); + srcAttachment = frame_buffer_manager.createTemporaryAttachment(blendWidth, blendHeight); - const scalePipeline = pipelineManager.getPipeline("complex_blend_scale"); - const scaleBindGroupLayout = pipelineManager.getBindGroupLayout("texture_scale"); + const scalePipeline = pipeline_manager.getPipeline("complex_blend_scale"); + const scaleBindGroupLayout = pipeline_manager.getBindGroupLayout("texture_scale"); if (scalePipeline && scaleBindGroupLayout) { const halfW = blendWidth / 2; @@ -205,8 +271,8 @@ export const execute = ( $uniform6[4] = -halfNodeW * matrix[0] - halfNodeH * matrix[3] + halfW; $uniform6[5] = -halfNodeW * matrix[1] - halfNodeH * matrix[4] + halfH; - const originalAttachment = frameBufferManager.createTemporaryAttachment(node.w, node.h); - commandEncoder.copyTextureToTexture( + const originalAttachment = frame_buffer_manager.createTemporaryAttachment(node.w, node.h); + command_encoder.copyTextureToTexture( { "texture": atlasAttachment.texture.resource, "origin": { "x": node.x, "y": node.y, "z": 0 } @@ -230,9 +296,9 @@ export const execute = ( $uniform12[9] = blendHeight; $uniform12[10] = 0; $uniform12[11] = 0; - const uniformBuffer = bufferManager.acquireAndWriteUniformBuffer($uniform12, 48); + const uniformBuffer = buffer_manager.acquireAndWriteUniformBuffer($uniform12, 48); - const sampler = textureManager.createSampler("scale_sampler", true); + const sampler = texture_manager.createSampler("scale_sampler", true); ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; $entries3[2].resource = originalAttachment.texture!.view; @@ -241,21 +307,21 @@ export const execute = ( "entries": $entries3 }); - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( + const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor( srcAttachment.texture!.view, 0, 0, 0, 0, "clear" ); - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(scalePipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.draw(6, 1, 0, 0); passEncoder.end(); - frameBufferManager.releaseTemporaryAttachment(originalAttachment); + frame_buffer_manager.releaseTemporaryAttachment(originalAttachment); } else { - commandEncoder.copyTextureToTexture( + command_encoder.copyTextureToTexture( { "texture": atlasAttachment.texture.resource, "origin": { "x": node.x, "y": node.y, "z": 0 } @@ -268,8 +334,8 @@ export const execute = ( ); } } else { - srcAttachment = frameBufferManager.createTemporaryAttachment(blendWidth, blendHeight); - commandEncoder.copyTextureToTexture( + srcAttachment = frame_buffer_manager.createTemporaryAttachment(blendWidth, blendHeight); + command_encoder.copyTextureToTexture( { "texture": atlasAttachment.texture.resource, "origin": { "x": node.x, "y": node.y, "z": 0 } @@ -283,23 +349,23 @@ export const execute = ( } // 2. デスティネーションテクスチャを作成(メインからレンダーパスでコピー) - const dstAttachment = frameBufferManager.createTemporaryAttachment(blendWidth, blendHeight); + const dstAttachment = frame_buffer_manager.createTemporaryAttachment(blendWidth, blendHeight); - copyTextureRegionViaRenderPass( + $copyTextureRegionViaRenderPass( device, - commandEncoder, - mainAttachment.texture.view, + command_encoder, + main_attachment.texture.view, dstAttachment, dstX, dstY, - mainAttachment.width, - mainAttachment.height, + main_attachment.width, + main_attachment.height, blendWidth, blendHeight, - frameBufferManager, - textureManager, - pipelineManager, - bufferManager + frame_buffer_manager, + texture_manager, + pipeline_manager, + buffer_manager ); // 3. カラートランスフォームを準備(add値は生値) @@ -319,34 +385,34 @@ export const execute = ( blend_mode, $uniform8, { - device, - commandEncoder, - bufferManager, - frameBufferManager, - pipelineManager, - textureManager, + "device": device, + "commandEncoder": command_encoder, + "bufferManager": buffer_manager, + "frameBufferManager": frame_buffer_manager, + "pipelineManager": pipeline_manager, + "textureManager": texture_manager, "frameTextures": [] } ); // 5. 結果をメインアタッチメントに描画 - drawToMainAttachment( + $drawToMainAttachment( device, - commandEncoder, + command_encoder, blendedAttachment, - mainAttachment, + main_attachment, dstX, dstY, - frameBufferManager, - textureManager, - pipelineManager, - bufferManager + frame_buffer_manager, + texture_manager, + pipeline_manager, + buffer_manager ); // 6. 一時テクスチャを解放 - frameBufferManager.releaseTemporaryAttachment(srcAttachment); - frameBufferManager.releaseTemporaryAttachment(dstAttachment); - frameBufferManager.releaseTemporaryAttachment(blendedAttachment); + frame_buffer_manager.releaseTemporaryAttachment(srcAttachment); + frame_buffer_manager.releaseTemporaryAttachment(dstAttachment); + frame_buffer_manager.releaseTemporaryAttachment(blendedAttachment); } clearComplexBlendQueue(); diff --git a/packages/webgpu/src/FillTexturePool.ts b/packages/webgpu/src/FillTexturePool.ts index 75e75797..d6c977bb 100644 --- a/packages/webgpu/src/FillTexturePool.ts +++ b/packages/webgpu/src/FillTexturePool.ts @@ -1,6 +1,16 @@ -// GPUTexture → GPUTextureView キャッシュ(createView()呼び出し削減) +/** + * @description GPUTexture → GPUTextureView キャッシュ(createView()呼び出し削減) + * GPUTexture to GPUTextureView cache to reduce createView() calls + * @type {WeakMap} + */ const $viewCache = new WeakMap(); +/** + * @description キャッシュからビューを取得、なければ生成してキャッシュに保存 + * Get view from cache, or create and cache a new one + * @param {GPUTexture} texture + * @return {GPUTextureView} + */ export const $getOrCreateView = (texture: GPUTexture): GPUTextureView => { let view = $viewCache.get(texture); if (!view) { @@ -10,15 +20,44 @@ export const $getOrCreateView = (texture: GPUTexture): GPUTextureView => { return view; }; -// GPUTextureUsage.TEXTURE_BINDING(0x04) | GPUTextureUsage.COPY_DST(0x02) = 0x06 -const FILL_TEXTURE_USAGE = 0x06; +/** + * @description 塗りテクスチャ用のGPUTextureUsageフラグ + * GPUTextureUsage flags for fill textures + * TEXTURE_BINDING(0x04) | COPY_DST(0x02) = 0x06 + * @type {number} + */ +const $FILL_TEXTURE_USAGE = 0x06; -// GPUTextureUsage.TEXTURE_BINDING(0x04) | GPUTextureUsage.COPY_DST(0x02) | GPUTextureUsage.RENDER_ATTACHMENT(0x10) = 0x16 -const RENDER_TEXTURE_USAGE = 0x16; +/** + * @description レンダーテクスチャ用のGPUTextureUsageフラグ + * GPUTextureUsage flags for render textures + * TEXTURE_BINDING(0x04) | COPY_DST(0x02) | RENDER_ATTACHMENT(0x10) = 0x16 + * @type {number} + */ +const $RENDER_TEXTURE_USAGE = 0x16; +/** + * @description 塗りテクスチャのオブジェクトプール + * Object pool for fill textures + * @type {Map} + */ const $pool: Map = new Map(); + +/** + * @description レンダーテクスチャのオブジェクトプール + * Object pool for render textures + * @type {Map} + */ const $renderPool: Map = new Map(); +/** + * @description プールから塗りテクスチャを取得、なければ新規作成 + * Acquire a fill texture from the pool, or create a new one + * @param {GPUDevice} device + * @param {number} width + * @param {number} height + * @return {GPUTexture} + */ export const $acquireFillTexture = (device: GPUDevice, width: number, height: number): GPUTexture => { const key = `${width}_${height}`; @@ -29,10 +68,16 @@ export const $acquireFillTexture = (device: GPUDevice, width: number, height: nu return device.createTexture({ "size": { width, height }, "format": "rgba8unorm", - "usage": FILL_TEXTURE_USAGE + "usage": $FILL_TEXTURE_USAGE }); }; +/** + * @description 塗りテクスチャをプールに返却 + * Release a fill texture back to the pool + * @param {GPUTexture} texture + * @return {void} + */ export const $releaseFillTexture = (texture: GPUTexture): void => { const key = `${texture.width}_${texture.height}`; @@ -44,6 +89,14 @@ export const $releaseFillTexture = (texture: GPUTexture): void => list.push(texture); }; +/** + * @description プールからレンダーテクスチャを取得、なければ新規作成 + * Acquire a render texture from the pool, or create a new one + * @param {GPUDevice} device + * @param {number} width + * @param {number} height + * @return {GPUTexture} + */ export const $acquireRenderTexture = (device: GPUDevice, width: number, height: number): GPUTexture => { const key = `${width}_${height}`; @@ -54,10 +107,16 @@ export const $acquireRenderTexture = (device: GPUDevice, width: number, height: return device.createTexture({ "size": { width, height }, "format": "rgba8unorm", - "usage": RENDER_TEXTURE_USAGE + "usage": $RENDER_TEXTURE_USAGE }); }; +/** + * @description レンダーテクスチャをプールに返却 + * Release a render texture back to the pool + * @param {GPUTexture} texture + * @return {void} + */ export const $releaseRenderTexture = (texture: GPUTexture): void => { const key = `${texture.width}_${texture.height}`; @@ -69,6 +128,11 @@ export const $releaseRenderTexture = (texture: GPUTexture): void => list.push(texture); }; +/** + * @description 全テクスチャプールを破棄してクリア + * Destroy and clear all texture pools + * @return {void} + */ export const $clearFillTexturePool = (): void => { for (const [, list] of $pool) { diff --git a/packages/webgpu/src/Filter/BevelFilter/FilterApplyBevelFilterUseCase.ts b/packages/webgpu/src/Filter/BevelFilter/FilterApplyBevelFilterUseCase.ts index 845fc0b6..9a6f0a8c 100644 --- a/packages/webgpu/src/Filter/BevelFilter/FilterApplyBevelFilterUseCase.ts +++ b/packages/webgpu/src/Filter/BevelFilter/FilterApplyBevelFilterUseCase.ts @@ -1,13 +1,9 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject"; import type { IFilterConfig } from "../../interface/IFilterConfig"; import { $offset } from "../FilterOffset"; +import { DEG_TO_RAD, intToPremultipliedRGBA } from "../FilterUtil"; import { execute as filterApplyBlurFilterUseCase } from "../BlurFilter/FilterApplyBlurFilterUseCase"; -/** - * @description 度からラジアンへの変換係数 - */ -const DEG_TO_RAD: number = Math.PI / 180; - /** * @description プリアロケートされたFloat32Array */ @@ -33,39 +29,48 @@ const $entries4: GPUBindGroupEntry[] = [ { "binding": 3, "resource": null as unknown as GPUTextureView } ]; -/** - * @description 32bit整数からRGB値を抽出(プリマルチプライドアルファ対応) - */ -const intToRGBA = (color: number, alpha: number): [number, number, number, number] => { - const r = (color >> 16 & 0xFF) / 255 * alpha; - const g = (color >> 8 & 0xFF) / 255 * alpha; - const b = (color & 0xFF) / 255 * alpha; - return [r, g, b, alpha]; -}; - /** * @description ベベルフィルターを適用 - * WebGL版と同様に、erase前処理で差分テクスチャを作成してからブラーを適用 + * Apply bevel filter * + * WebGL版と同様に、erase前処理で差分テクスチャを作成してからブラーを適用 * UV変換方式で元テクスチャとブラーテクスチャを直接サンプリング。 * 合成時のcopyTextureToTextureと一時テクスチャを使用しない最適化版。 + * + * @param {IAttachmentObject} source_attachment - 入力テクスチャ + * @param {Float32Array} matrix - 変換行列 + * @param {number} distance - ベベルの距離 + * @param {number} angle - ベベルの角度(度) + * @param {number} highlight_color - ハイライト色 (32bit整数) + * @param {number} highlight_alpha - ハイライトアルファ + * @param {number} shadow_color - シャドウ色 (32bit整数) + * @param {number} shadow_alpha - シャドウアルファ + * @param {number} blur_x - X方向ブラー量 + * @param {number} blur_y - Y方向ブラー量 + * @param {number} strength - ベベル強度 + * @param {number} quality - クオリティ + * @param {number} type - タイプ (0: full, 1: inner, 2: outer) + * @param {boolean} knockout - ノックアウトモード + * @param {number} device_pixel_ratio - デバイスピクセル比 + * @param {IFilterConfig} config - WebGPUリソース設定 + * @return {IAttachmentObject} - フィルター適用後のアタッチメント */ export const execute = ( - sourceAttachment: IAttachmentObject, + source_attachment: IAttachmentObject, matrix: Float32Array, distance: number, angle: number, - highlightColor: number, - highlightAlpha: number, - shadowColor: number, - shadowAlpha: number, - blurX: number, - blurY: number, + highlight_color: number, + highlight_alpha: number, + shadow_color: number, + shadow_alpha: number, + blur_x: number, + blur_y: number, strength: number, quality: number, type: number, knockout: boolean, - devicePixelRatio: number, + device_pixel_ratio: number, config: IFilterConfig ): IAttachmentObject => { @@ -74,8 +79,8 @@ export const execute = ( // 元のオフセットを保存 const baseOffsetX = $offset.x; const baseOffsetY = $offset.y; - const baseWidth = sourceAttachment.width; - const baseHeight = sourceAttachment.height; + const baseWidth = source_attachment.width; + const baseHeight = source_attachment.height; // スケールを計算 const xScale = Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1]); @@ -83,8 +88,8 @@ export const execute = ( // オフセットを計算(WebGL版と同じ) const radian = angle * DEG_TO_RAD; - const x = Math.cos(radian) * distance * (xScale / devicePixelRatio); - const y = Math.sin(radian) * distance * (yScale / devicePixelRatio); + const x = Math.cos(radian) * distance * (xScale / device_pixel_ratio); + const y = Math.sin(radian) * distance * (yScale / device_pixel_ratio); // === Erase前処理:差分テクスチャを作成 === const eraseAttachment = frameBufferManager.createTemporaryAttachment(baseWidth, baseHeight); @@ -92,7 +97,7 @@ export const execute = ( // Step 1: ソーステクスチャを元の位置にコピー(erase前処理のcopyTextureToTextureは残す) commandEncoder.copyTextureToTexture( { - "texture": sourceAttachment.texture!.resource, + "texture": source_attachment.texture!.resource, "origin": { "x": 0, "y": 0, "z": 0 } }, { @@ -136,7 +141,7 @@ export const execute = ( ($entries3[0].resource as GPUBufferBinding).buffer = eraseUniformBuffer; $entries3[1].resource = eraseSampler; - $entries3[2].resource = sourceAttachment.texture!.view; + $entries3[2].resource = source_attachment.texture!.view; const eraseBindGroup = device.createBindGroup({ "layout": eraseBindGroupLayout, "entries": $entries3 @@ -156,8 +161,8 @@ export const execute = ( // === 差分テクスチャにブラーを適用 === const blurAttachment = filterApplyBlurFilterUseCase( eraseAttachment, matrix, - blurX, blurY, quality, - devicePixelRatio, config + blur_x, blur_y, quality, + device_pixel_ratio, config ); // eraseアタッチメントを解放 @@ -207,7 +212,7 @@ export const execute = ( if (!pipeline || !bindGroupLayout) { console.error("[WebGPU BevelFilter] Pipeline not found"); frameBufferManager.releaseTemporaryAttachment(blurAttachment); - return sourceAttachment; + return source_attachment; } // サンプラーを作成 @@ -220,8 +225,8 @@ export const execute = ( // baseScale, baseOffset (16 bytes) // blurScale, blurOffset (16 bytes) // Total: 80 bytes → 16 floats + 4 floats = 20 floats (80 bytes) - const [hr, hg, hb, ha] = intToRGBA(highlightColor, highlightAlpha); - const [sr, sg, sb, sa] = intToRGBA(shadowColor, shadowAlpha); + const [hr, hg, hb, ha] = intToPremultipliedRGBA(highlight_color, highlight_alpha); + const [sr, sg, sb, sa] = intToPremultipliedRGBA(shadow_color, shadow_alpha); $uniform20[0] = hr; $uniform20[1] = hg; @@ -258,7 +263,7 @@ export const execute = ( ($entries4[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries4[1].resource = sampler; $entries4[2].resource = blurAttachment.texture!.view; - $entries4[3].resource = sourceAttachment.texture!.view; + $entries4[3].resource = source_attachment.texture!.view; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries4 diff --git a/packages/webgpu/src/Filter/BevelFilterShader.test.ts b/packages/webgpu/src/Filter/BevelFilterShader.test.ts deleted file mode 100644 index ec27301e..00000000 --- a/packages/webgpu/src/Filter/BevelFilterShader.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { getBevelFilterFragmentShader, getBevelFilterShaderKey } from "./BevelFilterShader"; - -describe("BevelFilterShader", () => -{ - describe("getBevelFilterFragmentShader", () => - { - it("should return a valid WGSL shader string for full type", () => - { - const shader = getBevelFilterFragmentShader("full", false, false); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should return a valid shader for inner type", () => - { - const shader = getBevelFilterFragmentShader("inner", false, false); - - expect(shader).toContain("filterColor * baseAlpha"); - }); - - it("should return a valid shader for outer type", () => - { - const shader = getBevelFilterFragmentShader("outer", false, false); - - expect(shader).toContain("filterColor * (1.0 - baseAlpha)"); - }); - - it("should contain @vertex attribute", () => - { - const shader = getBevelFilterFragmentShader("full", false, false); - - expect(shader).toContain("@vertex"); - }); - - it("should contain @fragment attribute", () => - { - const shader = getBevelFilterFragmentShader("full", false, false); - - expect(shader).toContain("@fragment"); - }); - - it("should define BevelUniforms struct", () => - { - const shader = getBevelFilterFragmentShader("full", false, false); - - expect(shader).toContain("struct BevelUniforms"); - }); - - it("should include highlight and shadow colors", () => - { - const shader = getBevelFilterFragmentShader("full", false, false); - - expect(shader).toContain("highlightColor"); - expect(shader).toContain("shadowColor"); - }); - - it("should handle knockout mode", () => - { - const shader = getBevelFilterFragmentShader("full", true, false); - - expect(shader).toContain("finalColor"); - }); - - it("should include gradient texture binding when isGradient is true", () => - { - const shader = getBevelFilterFragmentShader("full", false, true); - - expect(shader).toContain("gradientTexture"); - }); - - it("should not include gradient texture binding when isGradient is false", () => - { - const shader = getBevelFilterFragmentShader("full", false, false); - - expect(shader).not.toContain("gradientTexture"); - }); - - it("should use gradient LUT when isGradient is true", () => - { - const shader = getBevelFilterFragmentShader("full", false, true); - - expect(shader).toContain("gradientCoord"); - }); - }); - - describe("getBevelFilterShaderKey", () => - { - it("should generate unique key for full type", () => - { - const key = getBevelFilterShaderKey("full", false, false); - - expect(key).toBe("bevel_full_nko_ng"); - }); - - it("should generate unique key for inner type with knockout", () => - { - const key = getBevelFilterShaderKey("inner", true, false); - - expect(key).toBe("bevel_inner_ko_ng"); - }); - - it("should generate unique key for outer type with gradient", () => - { - const key = getBevelFilterShaderKey("outer", false, true); - - expect(key).toBe("bevel_outer_nko_g"); - }); - - it("should generate different keys for different configurations", () => - { - const key1 = getBevelFilterShaderKey("full", false, false); - const key2 = getBevelFilterShaderKey("full", true, false); - const key3 = getBevelFilterShaderKey("full", false, true); - - expect(key1).not.toBe(key2); - expect(key1).not.toBe(key3); - expect(key2).not.toBe(key3); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/BevelFilterShader.ts b/packages/webgpu/src/Filter/BevelFilterShader.ts deleted file mode 100644 index 5c390985..00000000 --- a/packages/webgpu/src/Filter/BevelFilterShader.ts +++ /dev/null @@ -1,118 +0,0 @@ -export const getBevelFilterFragmentShader = ( - type: string, - knockout: boolean, - isGradient: boolean -): string => { - const isInner = type === "inner"; - const isOuter = type === "outer"; - - const gradientBinding = isGradient ? ` -@group(0) @binding(4) var gradientTexture: texture_2d;` : ""; - - const colorCalculation = isGradient ? ` - let gradientCoord = vec2(blurAlpha, 0.5); - var filterColor = textureSample(gradientTexture, sourceSampler, gradientCoord); -` : ` - let highlightWeight = clamp(blurAlpha * 2.0, 0.0, 1.0); - let shadowWeight = clamp((1.0 - blurAlpha) * 2.0, 0.0, 1.0); - var filterColor = uniforms.highlightColor * highlightWeight + uniforms.shadowColor * shadowWeight; -`; - - let typeProcessing = ""; - if (isInner) { - typeProcessing = ` - let baseAlpha = textureSample(baseTexture, sourceSampler, baseTexCoord).a; - filterColor = filterColor * baseAlpha; - ${knockout ? "let finalColor = filterColor;" : "let finalColor = mix(baseColor, filterColor, filterColor.a);"} -`; - } else if (isOuter) { - typeProcessing = ` - let baseAlpha = textureSample(baseTexture, sourceSampler, baseTexCoord).a; - filterColor = filterColor * (1.0 - baseAlpha); - ${knockout ? "let finalColor = filterColor;" : "let finalColor = filterColor + baseColor * (1.0 - filterColor.a);"} -`; - } else { - typeProcessing = knockout ? ` - let finalColor = filterColor; -` : ` - let finalColor = filterColor + baseColor * (1.0 - filterColor.a); -`; - } - - return ` -struct BevelUniforms { - blurTexCoordScale: vec2, - blurTexCoordOffset: vec2, - baseTexCoordScale: vec2, - baseTexCoordOffset: vec2, - strength: f32, - _pad1: f32, - _pad2: f32, - _pad3: f32, - highlightColor: vec4, - shadowColor: vec4, -} - -@group(0) @binding(0) var uniforms: BevelUniforms; -@group(0) @binding(1) var sourceSampler: sampler; -@group(0) @binding(2) var blurTexture: texture_2d; -@group(0) @binding(3) var baseTexture: texture_2d; -${gradientBinding} - -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, -} - -@vertex -fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { - var positions = array, 6>( - vec2(-1.0, -1.0), - vec2(1.0, -1.0), - vec2(-1.0, 1.0), - vec2(-1.0, 1.0), - vec2(1.0, -1.0), - vec2(1.0, 1.0) - ); - - var texCoords = array, 6>( - vec2(0.0, 1.0), - vec2(1.0, 1.0), - vec2(0.0, 0.0), - vec2(0.0, 0.0), - vec2(1.0, 1.0), - vec2(1.0, 0.0) - ); - - var output: VertexOutput; - output.position = vec4(positions[vertexIndex], 0.0, 1.0); - output.texCoord = texCoords[vertexIndex]; - return output; -} - -@fragment -fn fs_main(input: VertexOutput) -> @location(0) vec4 { - let blurTexCoord = input.texCoord * uniforms.blurTexCoordScale + uniforms.blurTexCoordOffset; - let baseTexCoord = input.texCoord * uniforms.baseTexCoordScale + uniforms.baseTexCoordOffset; - - let blurColor = textureSample(blurTexture, sourceSampler, blurTexCoord); - var blurAlpha = blurColor.a * uniforms.strength; - blurAlpha = clamp(blurAlpha, 0.0, 1.0); - - let baseColor = textureSample(baseTexture, sourceSampler, baseTexCoord); - - ${colorCalculation} - ${typeProcessing} - - return finalColor; -} -`; -}; - -export const getBevelFilterShaderKey = ( - type: string, - knockout: boolean, - isGradient: boolean -): string => { - return `bevel_${type}_${knockout ? "ko" : "nko"}_${isGradient ? "g" : "ng"}`; -}; diff --git a/packages/webgpu/src/Filter/BitmapFilterShader.test.ts b/packages/webgpu/src/Filter/BitmapFilterShader.test.ts deleted file mode 100644 index 332c8f13..00000000 --- a/packages/webgpu/src/Filter/BitmapFilterShader.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { getBitmapFilterFragmentShader, getBitmapFilterShaderKey } from "./BitmapFilterShader"; - -describe("BitmapFilterShader", () => -{ - describe("getBitmapFilterFragmentShader", () => - { - it("should return a valid WGSL shader string", () => - { - const shader = getBitmapFilterFragmentShader(true, true, true, "full", false, true, false); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = getBitmapFilterFragmentShader(true, true, true, "full", false, true, false); - - expect(shader).toContain("@vertex"); - }); - - it("should contain @fragment attribute", () => - { - const shader = getBitmapFilterFragmentShader(true, true, true, "full", false, true, false); - - expect(shader).toContain("@fragment"); - }); - - it("should define BitmapFilterUniforms struct", () => - { - const shader = getBitmapFilterFragmentShader(true, true, true, "full", false, true, false); - - expect(shader).toContain("struct BitmapFilterUniforms"); - }); - - it("should include base scale/offset when transformsBase is true", () => - { - const shader = getBitmapFilterFragmentShader(true, false, true, "full", false, false, false); - - expect(shader).toContain("baseScale"); - expect(shader).toContain("baseOffset"); - }); - - it("should include blur scale/offset when transformsBlur is true", () => - { - const shader = getBitmapFilterFragmentShader(false, true, true, "full", false, false, false); - - expect(shader).toContain("blurScale"); - expect(shader).toContain("blurOffset"); - }); - - it("should include strength when appliesStrength is true", () => - { - const shader = getBitmapFilterFragmentShader(false, false, true, "full", false, true, false); - - expect(shader).toContain("strength"); - }); - - it("should include color for glow mode", () => - { - const shader = getBitmapFilterFragmentShader(false, false, true, "full", false, false, false); - - expect(shader).toContain("color"); - }); - - it("should include highlight/shadow colors for bevel mode", () => - { - const shader = getBitmapFilterFragmentShader(false, false, false, "full", false, false, false); - - expect(shader).toContain("highlightColor"); - expect(shader).toContain("shadowColor"); - }); - - it("should include gradient texture when isGradient is true", () => - { - const shader = getBitmapFilterFragmentShader(false, false, true, "full", false, false, true); - - expect(shader).toContain("gradientTexture"); - }); - - it("should handle inner type", () => - { - const shader = getBitmapFilterFragmentShader(true, true, true, "inner", false, true, false); - - expect(shader).toContain("blur"); - }); - - it("should handle outer type", () => - { - const shader = getBitmapFilterFragmentShader(true, true, true, "outer", false, true, false); - - expect(shader).toContain("blur"); - }); - - it("should handle knockout mode", () => - { - const shader = getBitmapFilterFragmentShader(true, true, true, "full", true, true, false); - - expect(shader).toBeDefined(); - }); - - it("should include isInside helper function", () => - { - const shader = getBitmapFilterFragmentShader(true, true, true, "full", false, true, false); - - expect(shader).toContain("fn isInside"); - }); - }); - - describe("getBitmapFilterShaderKey", () => - { - it("should generate unique key for glow configuration", () => - { - const key = getBitmapFilterShaderKey(true, true, true, "full", false, true, false); - - expect(key).toBe("bitmap_yygfullnsso"); - }); - - it("should generate unique key for bevel configuration", () => - { - const key = getBitmapFilterShaderKey(true, true, false, "full", false, true, false); - - expect(key).toBe("bitmap_yybfullnsso"); - }); - - it("should generate different keys for different configurations", () => - { - const key1 = getBitmapFilterShaderKey(true, true, true, "full", false, true, false); - const key2 = getBitmapFilterShaderKey(true, true, true, "inner", false, true, false); - const key3 = getBitmapFilterShaderKey(true, true, true, "full", true, true, false); - - expect(key1).not.toBe(key2); - expect(key1).not.toBe(key3); - }); - - it("should include gradient flag in key", () => - { - const keyNonGradient = getBitmapFilterShaderKey(true, true, true, "full", false, true, false); - const keyGradient = getBitmapFilterShaderKey(true, true, true, "full", false, true, true); - - expect(keyNonGradient).toContain("so"); - expect(keyGradient).toContain("gr"); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/BitmapFilterShader.ts b/packages/webgpu/src/Filter/BitmapFilterShader.ts deleted file mode 100644 index 2e6fe223..00000000 --- a/packages/webgpu/src/Filter/BitmapFilterShader.ts +++ /dev/null @@ -1,231 +0,0 @@ -export const getBitmapFilterFragmentShader = ( - transformsBase: boolean, - transformsBlur: boolean, - isGlow: boolean, - type: string, - knockout: boolean, - appliesStrength: boolean, - isGradient: boolean -): string => { - const isInner = type === "inner"; - - let textureBindingIndex = 2; - const blurTextureBinding = textureBindingIndex++; - const baseTextureBinding = transformsBase ? textureBindingIndex++ : -1; - const gradientTextureBinding = isGradient ? textureBindingIndex++ : -1; - - let uniformsStruct = `struct BitmapFilterUniforms { -`; - if (transformsBase) { - uniformsStruct += ` baseScale: vec2, - baseOffset: vec2, -`; - } - if (transformsBlur) { - uniformsStruct += ` blurScale: vec2, - blurOffset: vec2, -`; - } - if (appliesStrength) { - uniformsStruct += ` strength: f32, - _padStrength: vec3, -`; - } - if (!isGradient) { - if (isGlow) { - uniformsStruct += ` color: vec4, -`; - } else { - uniformsStruct += ` highlightColor: vec4, - shadowColor: vec4, -`; - } - } - uniformsStruct += "}"; - - let textureBindings = ` -@group(0) @binding(0) var uniforms: BitmapFilterUniforms; -@group(0) @binding(1) var sourceSampler: sampler; -@group(0) @binding(${blurTextureBinding}) var blurTexture: texture_2d;`; - - if (transformsBase) { - textureBindings += ` -@group(0) @binding(${baseTextureBinding}) var baseTexture: texture_2d;`; - } - if (isGradient) { - textureBindings += ` -@group(0) @binding(${gradientTextureBinding}) var gradientTexture: texture_2d;`; - } - - let baseStatement = ""; - if (transformsBase) { - baseStatement = ` - let baseScale = uniforms.baseScale; - let baseOffset = uniforms.baseOffset; - let uv = input.texCoord * baseScale - baseOffset; - let base = mix(vec4(0.0), textureSample(baseTexture, sourceSampler, uv), isInside(uv));`; - } - - let blurStatement = ""; - if (transformsBlur) { - blurStatement = ` - let blurScale = uniforms.blurScale; - let blurOffset = uniforms.blurOffset; - let st = input.texCoord * blurScale - blurOffset; - var blur = mix(vec4(0.0), textureSample(blurTexture, sourceSampler, st), isInside(st));`; - } else { - blurStatement = ` - var blur = textureSample(blurTexture, sourceSampler, input.texCoord);`; - } - - let colorStatement = ""; - if (isGlow) { - if (isInner) { - colorStatement += ` - blur.a = 1.0 - blur.a;`; - } - if (appliesStrength) { - colorStatement += ` - let strength = uniforms.strength; - blur.a = clamp(blur.a * strength, 0.0, 1.0);`; - } - if (isGradient) { - colorStatement += ` - blur = textureSample(gradientTexture, sourceSampler, vec2(blur.a, 0.5));`; - } else { - colorStatement += ` - let color = uniforms.color; - blur = color * blur.a;`; - } - } else { - if (transformsBlur) { - colorStatement += ` - let pq = (vec2(1.0) - input.texCoord) * blurScale - blurOffset; - let blur2 = mix(vec4(0.0), textureSample(blurTexture, sourceSampler, pq), isInside(pq));`; - } else { - colorStatement += ` - let blur2 = textureSample(blurTexture, sourceSampler, vec2(1.0) - input.texCoord);`; - } - colorStatement += ` - var highlightAlpha = blur.a - blur2.a; - var shadowAlpha = blur2.a - blur.a;`; - - if (appliesStrength) { - colorStatement += ` - let strength = uniforms.strength; - highlightAlpha = highlightAlpha * strength; - shadowAlpha = shadowAlpha * strength;`; - } - - colorStatement += ` - highlightAlpha = clamp(highlightAlpha, 0.0, 1.0); - shadowAlpha = clamp(shadowAlpha, 0.0, 1.0);`; - - if (isGradient) { - colorStatement += ` - blur = textureSample(gradientTexture, sourceSampler, vec2( - 0.5019607843137255 - 0.5019607843137255 * shadowAlpha + 0.4980392156862745 * highlightAlpha, - 0.5 - ));`; - } else { - colorStatement += ` - let highlightColor = uniforms.highlightColor; - let shadowColor = uniforms.shadowColor; - blur = highlightColor * highlightAlpha + shadowColor * shadowAlpha;`; - } - } - - let modeExpression = ""; - switch (type) { - case "outer": - modeExpression = knockout - ? "blur - blur * base.a" - : "base + blur - blur * base.a"; - break; - case "full": - modeExpression = knockout - ? "blur" - : "base - base * blur.a + blur"; - break; - case "inner": - default: - modeExpression = "blur"; - break; - } - - const needsBase = transformsBase || (type === "outer" || type === "full" && !knockout); - let baseDecl = ""; - if (needsBase && !transformsBase) { - baseDecl = ` - let base = vec4(0.0);`; - } - - return ` -${uniformsStruct} -${textureBindings} - -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, -} - -fn isInside(uv: vec2) -> f32 { - let inside = step(vec2(0.0), uv) * step(uv, vec2(1.0)); - return inside.x * inside.y; -} - -@vertex -fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { - var positions = array, 6>( - vec2(-1.0, -1.0), - vec2(1.0, -1.0), - vec2(-1.0, 1.0), - vec2(-1.0, 1.0), - vec2(1.0, -1.0), - vec2(1.0, 1.0) - ); - - var texCoords = array, 6>( - vec2(0.0, 1.0), - vec2(1.0, 1.0), - vec2(0.0, 0.0), - vec2(0.0, 0.0), - vec2(1.0, 1.0), - vec2(1.0, 0.0) - ); - - var output: VertexOutput; - output.position = vec4(positions[vertexIndex], 0.0, 1.0); - output.texCoord = texCoords[vertexIndex]; - return output; -} - -@fragment -fn fs_main(input: VertexOutput) -> @location(0) vec4 { - ${baseDecl} - ${baseStatement} - ${blurStatement} - ${colorStatement} - - return ${modeExpression}; -} -`; -}; - -export const getBitmapFilterShaderKey = ( - transformsBase: boolean, - transformsBlur: boolean, - isGlow: boolean, - type: string, - knockout: boolean, - appliesStrength: boolean, - isGradient: boolean -): string => { - const key1 = transformsBase ? "y" : "n"; - const key2 = transformsBlur ? "y" : "n"; - const key3 = isGlow ? "g" : "b"; - const key4 = knockout ? "k" : "n"; - const key5 = appliesStrength ? "s" : "n"; - const key6 = isGradient ? "gr" : "so"; - return `bitmap_${key1}${key2}${key3}${type}${key4}${key5}${key6}`; -}; diff --git a/packages/webgpu/src/Filter/BlurFilter/FilterApplyBlurFilterUseCase.ts b/packages/webgpu/src/Filter/BlurFilter/FilterApplyBlurFilterUseCase.ts index 84f7b231..67a97c91 100644 --- a/packages/webgpu/src/Filter/BlurFilter/FilterApplyBlurFilterUseCase.ts +++ b/packages/webgpu/src/Filter/BlurFilter/FilterApplyBlurFilterUseCase.ts @@ -2,8 +2,6 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject"; import type { IFilterConfig } from "../../interface/IFilterConfig"; import { $offset } from "../FilterOffset"; import { calculateBlurParams, calculateDirectionalBlurParams } from "../BlurFilterUseCase"; -import { shouldUseComputeShader } from "./service/BlurFilterComputeShaderService"; -import { execute as executeBlurCompute } from "../../Compute/service/ComputeExecuteBlurService"; /** * @description プリアロケートされたFloat32Array (サイズ4) @@ -23,29 +21,29 @@ const $entries3: GPUBindGroupEntry[] = [ * @description ブラーフィルターを適用 * Apply blur filter * - * @param {IAttachmentObject} sourceAttachment - 入力テクスチャ(アタッチメント) + * @param {IAttachmentObject} source_attachment - 入力テクスチャ(アタッチメント) * @param {Float32Array} matrix - 変換行列 - * @param {number} blurX - X方向のブラー量 - * @param {number} blurY - Y方向のブラー量 + * @param {number} blur_x - X方向のブラー量 + * @param {number} blur_y - Y方向のブラー量 * @param {number} quality - クオリティ (1-15) - * @param {number} devicePixelRatio - デバイスピクセル比 + * @param {number} device_pixel_ratio - デバイスピクセル比 * @param {IFilterConfig} config - WebGPUリソース設定 * @return {IAttachmentObject} - フィルター適用後のアタッチメント */ export const execute = ( - sourceAttachment: IAttachmentObject, + source_attachment: IAttachmentObject, matrix: Float32Array, - blurX: number, - blurY: number, + blur_x: number, + blur_y: number, quality: number, - devicePixelRatio: number, + device_pixel_ratio: number, config: IFilterConfig ): IAttachmentObject => { const { device, commandEncoder, frameBufferManager, pipelineManager, textureManager } = config; // ブラーパラメータを計算 - const blurParams = calculateBlurParams(matrix, blurX, blurY, quality, devicePixelRatio); + const blurParams = calculateBlurParams(matrix, blur_x, blur_y, quality, device_pixel_ratio); const { baseBlurX, baseBlurY, offsetX, offsetY, bufferScaleX, bufferScaleY } = blurParams; // オフセットを更新 @@ -53,8 +51,8 @@ export const execute = ( $offset.y += offsetY; // ブラー用バッファサイズを計算 - const width = sourceAttachment.width + offsetX * 2; - const height = sourceAttachment.height + offsetY * 2; + const width = source_attachment.width + offsetX * 2; + const height = source_attachment.height + offsetY * 2; const bufferWidth = Math.ceil(width * bufferScaleX); const bufferHeight = Math.ceil(height * bufferScaleY); @@ -68,7 +66,7 @@ export const execute = ( // ソーステクスチャをattachment0にコピー(スケーリング付き) copyTextureToAttachment( device, commandEncoder, frameBufferManager, pipelineManager, - sourceAttachment, attachment0, sampler, + source_attachment, attachment0, sampler, bufferScaleX, bufferScaleY, offsetX * bufferScaleX, offsetY * bufferScaleY, config.bufferManager @@ -78,53 +76,33 @@ export const execute = ( const bufferBlurX = baseBlurX * bufferScaleX; const bufferBlurY = baseBlurY * bufferScaleY; - // Compute Shaderを使用すべきか判定 - const useCompute = config.computePipelineManager - && shouldUseComputeShader(baseBlurX, baseBlurY, bufferWidth, bufferHeight); - // ブラーパスを実行 const attachments = [attachment0, attachment1]; let attachmentIndex = 0; for (let q = 0; q < quality; ++q) { // 水平ブラー - if (blurX > 0) { + if (blur_x > 0) { const srcIndex = attachmentIndex; attachmentIndex = (attachmentIndex + 1) % 2; - if (useCompute) { - executeBlurCompute( - device, commandEncoder, config.computePipelineManager!, - attachments[srcIndex], attachments[attachmentIndex], - true, bufferBlurX, config.bufferManager - ); - } else { - applyDirectionalBlur( - device, commandEncoder, frameBufferManager, pipelineManager, - attachments[srcIndex], attachments[attachmentIndex], sampler, - true, bufferBlurX, config.bufferManager - ); - } + applyDirectionalBlur( + device, commandEncoder, frameBufferManager, pipelineManager, + attachments[srcIndex], attachments[attachmentIndex], sampler, + true, bufferBlurX, config.bufferManager + ); } // 垂直ブラー - if (blurY > 0) { + if (blur_y > 0) { const srcIndex = attachmentIndex; attachmentIndex = (attachmentIndex + 1) % 2; - if (useCompute) { - executeBlurCompute( - device, commandEncoder, config.computePipelineManager!, - attachments[srcIndex], attachments[attachmentIndex], - false, bufferBlurY, config.bufferManager - ); - } else { - applyDirectionalBlur( - device, commandEncoder, frameBufferManager, pipelineManager, - attachments[srcIndex], attachments[attachmentIndex], sampler, - false, bufferBlurY, config.bufferManager - ); - } + applyDirectionalBlur( + device, commandEncoder, frameBufferManager, pipelineManager, + attachments[srcIndex], attachments[attachmentIndex], sampler, + false, bufferBlurY, config.bufferManager + ); } } @@ -138,7 +116,6 @@ export const execute = ( upscaleTexture( device, commandEncoder, frameBufferManager, pipelineManager, resultAttachment, finalAttachment, sampler, - 1 / bufferScaleX, 1 / bufferScaleY, config.bufferManager ); @@ -158,31 +135,39 @@ export const execute = ( /** * @description テクスチャをアタッチメントにコピー(オフセット位置に配置、スケーリング対応) + * Copy texture to attachment with offset placement and scaling support * - * @param source - ソーステクスチャ - * @param dest - デストテクスチャ(ソースより大きい) - * @param bufferScaleX - X方向のバッファスケール - * @param bufferScaleY - Y方向のバッファスケール - * @param pixelOffsetX - デスト内でのX方向オフセット(ピクセル単位、スケーリング済み) - * @param pixelOffsetY - デスト内でのY方向オフセット(ピクセル単位、スケーリング済み) + * @param {GPUDevice} device - GPUデバイス + * @param {GPUCommandEncoder} command_encoder - コマンドエンコーダー + * @param {IFilterConfig["frameBufferManager"]} frame_buffer_manager - フレームバッファマネージャー + * @param {IFilterConfig["pipelineManager"]} pipeline_manager - パイプラインマネージャー + * @param {IAttachmentObject} source - ソーステクスチャ + * @param {IAttachmentObject} dest - デストテクスチャ(ソースより大きい) + * @param {GPUSampler} sampler - サンプラー + * @param {number} buffer_scale_x - X方向のバッファスケール + * @param {number} buffer_scale_y - Y方向のバッファスケール + * @param {number} pixel_offset_x - デスト内でのX方向オフセット(ピクセル単位、スケーリング済み) + * @param {number} pixel_offset_y - デスト内でのY方向オフセット(ピクセル単位、スケーリング済み) + * @param {IFilterConfig["bufferManager"]} [buffer_manager] - バッファマネージャー + * @return {void} */ const copyTextureToAttachment = ( device: GPUDevice, - commandEncoder: GPUCommandEncoder, - frameBufferManager: IFilterConfig["frameBufferManager"], - pipelineManager: IFilterConfig["pipelineManager"], + command_encoder: GPUCommandEncoder, + frame_buffer_manager: IFilterConfig["frameBufferManager"], + pipeline_manager: IFilterConfig["pipelineManager"], source: IAttachmentObject, dest: IAttachmentObject, sampler: GPUSampler, - bufferScaleX: number, - bufferScaleY: number, - pixelOffsetX: number, - pixelOffsetY: number, - bufferManager?: IFilterConfig["bufferManager"] + buffer_scale_x: number, + buffer_scale_y: number, + pixel_offset_x: number, + pixel_offset_y: number, + buffer_manager?: IFilterConfig["bufferManager"] ): void => { // texture_copy_rgba8を使用し、ビューポートでオフセットを制御 - const pipeline = pipelineManager.getPipeline("texture_copy_rgba8"); - const bindGroupLayout = pipelineManager.getBindGroupLayout("texture_copy"); + const pipeline = pipeline_manager.getPipeline("texture_copy_rgba8"); + const bindGroupLayout = pipeline_manager.getBindGroupLayout("texture_copy"); if (!pipeline || !bindGroupLayout) { console.error("[WebGPU BlurFilter] texture_copy_rgba8 pipeline not found"); @@ -190,8 +175,8 @@ const copyTextureToAttachment = ( } // デスト内でのソース描画サイズ(スケーリング後) - const scaledSourceWidth = source.width * bufferScaleX; - const scaledSourceHeight = source.height * bufferScaleY; + const scaledSourceWidth = source.width * buffer_scale_x; + const scaledSourceHeight = source.height * buffer_scale_y; // シェーダー: uv = texCoord * scale + offset // ソース全体をサンプリングするので scale = 1, offset = 0 @@ -205,13 +190,13 @@ const copyTextureToAttachment = ( $uniform4[1] = scaleY; $uniform4[2] = offsetX; $uniform4[3] = offsetY; - const uniformBuffer = bufferManager - ? bufferManager.acquireAndWriteUniformBuffer($uniform4) + const uniformBuffer = buffer_manager + ? buffer_manager.acquireAndWriteUniformBuffer($uniform4) : device.createBuffer({ "size": $uniform4.byteLength, "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); - if (!bufferManager) { + if (!buffer_manager) { device.queue.writeBuffer(uniformBuffer, 0, $uniform4); } @@ -223,22 +208,22 @@ const copyTextureToAttachment = ( "entries": $entries3 }); - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( + const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor( dest.texture!.view, 0, 0, 0, 0, "clear" ); - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); // ビューポートを設定してオフセット位置に描画 passEncoder.setViewport( - pixelOffsetX, pixelOffsetY, + pixel_offset_x, pixel_offset_y, scaledSourceWidth, scaledSourceHeight, 0, 1 ); passEncoder.setScissorRect( - Math.floor(pixelOffsetX), Math.floor(pixelOffsetY), + Math.floor(pixel_offset_x), Math.floor(pixel_offset_y), Math.ceil(scaledSourceWidth), Math.ceil(scaledSourceHeight) ); @@ -249,21 +234,34 @@ const copyTextureToAttachment = ( /** * @description 方向ブラーを適用 + * Apply directional blur pass + * + * @param {GPUDevice} device - GPUデバイス + * @param {GPUCommandEncoder} command_encoder - コマンドエンコーダー + * @param {IFilterConfig["frameBufferManager"]} frame_buffer_manager - フレームバッファマネージャー + * @param {IFilterConfig["pipelineManager"]} pipeline_manager - パイプラインマネージャー + * @param {IAttachmentObject} source - ソーステクスチャ + * @param {IAttachmentObject} dest - デストテクスチャ + * @param {GPUSampler} sampler - サンプラー + * @param {boolean} is_horizontal - 水平方向かどうか + * @param {number} blur - ブラー量 + * @param {IFilterConfig["bufferManager"]} [buffer_manager] - バッファマネージャー + * @return {void} */ const applyDirectionalBlur = ( device: GPUDevice, - commandEncoder: GPUCommandEncoder, - frameBufferManager: IFilterConfig["frameBufferManager"], - pipelineManager: IFilterConfig["pipelineManager"], + command_encoder: GPUCommandEncoder, + frame_buffer_manager: IFilterConfig["frameBufferManager"], + pipeline_manager: IFilterConfig["pipelineManager"], source: IAttachmentObject, dest: IAttachmentObject, sampler: GPUSampler, - isHorizontal: boolean, + is_horizontal: boolean, blur: number, - bufferManager?: IFilterConfig["bufferManager"] + buffer_manager?: IFilterConfig["bufferManager"] ): void => { const params = calculateDirectionalBlurParams( - isHorizontal, blur, + is_horizontal, blur, source.width, source.height ); @@ -271,8 +269,8 @@ const applyDirectionalBlur = ( // halfBlurに対応するパイプラインを取得(1〜16の範囲でクランプ) const clampedHalfBlur = Math.max(1, Math.min(16, halfBlur)); - const pipeline = pipelineManager.getPipeline(`blur_filter_${clampedHalfBlur}`); - const bindGroupLayout = pipelineManager.getBindGroupLayout("blur_filter"); + const pipeline = pipeline_manager.getPipeline(`blur_filter_${clampedHalfBlur}`); + const bindGroupLayout = pipeline_manager.getBindGroupLayout("blur_filter"); if (!pipeline || !bindGroupLayout) { console.error(`[WebGPU BlurFilter] blur_filter_${clampedHalfBlur} pipeline not found`); @@ -284,13 +282,13 @@ const applyDirectionalBlur = ( $uniform4[1] = offsetY; $uniform4[2] = fraction; $uniform4[3] = samples; - const uniformBuffer = bufferManager - ? bufferManager.acquireAndWriteUniformBuffer($uniform4) + const uniformBuffer = buffer_manager + ? buffer_manager.acquireAndWriteUniformBuffer($uniform4) : device.createBuffer({ "size": $uniform4.byteLength, "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); - if (!bufferManager) { + if (!buffer_manager) { device.queue.writeBuffer(uniformBuffer, 0, $uniform4); } @@ -302,11 +300,11 @@ const applyDirectionalBlur = ( "entries": $entries3 }); - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( + const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor( dest.texture!.view, 0, 0, 0, 0, "clear" ); - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.draw(6, 1, 0, 0); @@ -316,22 +314,31 @@ const applyDirectionalBlur = ( /** * @description テクスチャをアップスケール(ソース全体をデスト全体にマッピング) + * Upscale texture by mapping entire source to entire destination + * + * @param {GPUDevice} device - GPUデバイス + * @param {GPUCommandEncoder} command_encoder - コマンドエンコーダー + * @param {IFilterConfig["frameBufferManager"]} frame_buffer_manager - フレームバッファマネージャー + * @param {IFilterConfig["pipelineManager"]} pipeline_manager - パイプラインマネージャー + * @param {IAttachmentObject} source - ソーステクスチャ + * @param {IAttachmentObject} dest - デストテクスチャ + * @param {GPUSampler} sampler - サンプラー + * @param {IFilterConfig["bufferManager"]} [buffer_manager] - バッファマネージャー + * @return {void} */ const upscaleTexture = ( device: GPUDevice, - commandEncoder: GPUCommandEncoder, - frameBufferManager: IFilterConfig["frameBufferManager"], - pipelineManager: IFilterConfig["pipelineManager"], + command_encoder: GPUCommandEncoder, + frame_buffer_manager: IFilterConfig["frameBufferManager"], + pipeline_manager: IFilterConfig["pipelineManager"], source: IAttachmentObject, dest: IAttachmentObject, sampler: GPUSampler, - _scaleX: number, - _scaleY: number, - bufferManager?: IFilterConfig["bufferManager"] + buffer_manager?: IFilterConfig["bufferManager"] ): void => { // temp_アタッチメントはrgba8unormフォーマットなので、texture_copy_rgba8パイプラインを使用 - const pipeline = pipelineManager.getPipeline("texture_copy_rgba8"); - const bindGroupLayout = pipelineManager.getBindGroupLayout("texture_copy"); + const pipeline = pipeline_manager.getPipeline("texture_copy_rgba8"); + const bindGroupLayout = pipeline_manager.getBindGroupLayout("texture_copy"); if (!pipeline || !bindGroupLayout) { console.error("[WebGPU BlurFilter] texture_copy_rgba8 pipeline not found"); @@ -345,13 +352,13 @@ const upscaleTexture = ( $uniform4[1] = 1; $uniform4[2] = 0; $uniform4[3] = 0; - const uniformBuffer = bufferManager - ? bufferManager.acquireAndWriteUniformBuffer($uniform4) + const uniformBuffer = buffer_manager + ? buffer_manager.acquireAndWriteUniformBuffer($uniform4) : device.createBuffer({ "size": $uniform4.byteLength, "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }); - if (!bufferManager) { + if (!buffer_manager) { device.queue.writeBuffer(uniformBuffer, 0, $uniform4); } @@ -363,11 +370,11 @@ const upscaleTexture = ( "entries": $entries3 }); - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( + const renderPassDescriptor = frame_buffer_manager.createRenderPassDescriptor( dest.texture!.view, 0, 0, 0, 0, "clear" ); - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + const passEncoder = command_encoder.beginRenderPass(renderPassDescriptor); passEncoder.setPipeline(pipeline); passEncoder.setBindGroup(0, bindGroup); passEncoder.draw(6, 1, 0, 0); diff --git a/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.test.ts b/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.test.ts deleted file mode 100644 index 073925d7..00000000 --- a/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.test.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import type { IAttachmentObject } from "../../../interface/IAttachmentObject"; -import type { IFilterConfig } from "../../../interface/IFilterConfig"; -import type { ComputePipelineManager } from "../../../Compute/ComputePipelineManager"; -import { execute, shouldUseComputeShader } from "./BlurFilterComputeShaderService"; - -// Mock the compute blur service -vi.mock("../../../Compute/service/ComputeExecuteBlurService", () => ({ - "execute": vi.fn() -})); - -import { execute as mockExecuteBlurCompute } from "../../../Compute/service/ComputeExecuteBlurService"; - -describe("BlurFilterComputeShaderService", () => -{ - const createMockAttachment = (width: number = 256, height: number = 256): IAttachmentObject => - { - return { - "id": 1, - "width": width, - "height": height, - "clipLevel": 0, - "msaa": false, - "mask": false, - "color": null, - "texture": { - "id": 1, - "width": width, - "height": height, - "area": width * height, - "smooth": true, - "resource": {} as GPUTexture, - "view": {} as GPUTextureView - }, - "stencil": null, - "msaaTexture": null, - "msaaStencil": null - }; - }; - - const createMockDevice = () => - { - return {} as GPUDevice; - }; - - const createMockCommandEncoder = () => - { - return {} as GPUCommandEncoder; - }; - - const createMockComputePipelineManager = () => - { - return {} as ComputePipelineManager; - }; - - const createMockConfig = () => - { - return {} as IFilterConfig; - }; - - beforeEach(() => - { - vi.clearAllMocks(); - }); - - describe("execute", () => - { - it("should call executeBlurCompute with correct parameters", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const computePipelineManager = createMockComputePipelineManager(); - const config = createMockConfig(); - const source = createMockAttachment(); - const dest = createMockAttachment(); - - execute(device, commandEncoder, computePipelineManager, config, source, dest, true, 16); - - expect(mockExecuteBlurCompute).toHaveBeenCalledWith( - device, - commandEncoder, - computePipelineManager, - source, - dest, - true, - 8 // radius = ceil(16 / 2) = 8 - ); - }); - - it("should calculate radius as ceil of blur / 2", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const computePipelineManager = createMockComputePipelineManager(); - const config = createMockConfig(); - const source = createMockAttachment(); - const dest = createMockAttachment(); - - execute(device, commandEncoder, computePipelineManager, config, source, dest, false, 15); - - expect(mockExecuteBlurCompute).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - false, - 8 // radius = ceil(15 / 2) = 8 - ); - }); - - it("should pass isHorizontal = true for horizontal blur", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const computePipelineManager = createMockComputePipelineManager(); - const config = createMockConfig(); - const source = createMockAttachment(); - const dest = createMockAttachment(); - - execute(device, commandEncoder, computePipelineManager, config, source, dest, true, 10); - - expect(mockExecuteBlurCompute).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - true, - expect.any(Number) - ); - }); - - it("should pass isHorizontal = false for vertical blur", () => - { - const device = createMockDevice(); - const commandEncoder = createMockCommandEncoder(); - const computePipelineManager = createMockComputePipelineManager(); - const config = createMockConfig(); - const source = createMockAttachment(); - const dest = createMockAttachment(); - - execute(device, commandEncoder, computePipelineManager, config, source, dest, false, 10); - - expect(mockExecuteBlurCompute).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - false, - expect.any(Number) - ); - }); - }); - - describe("shouldUseComputeShader", () => - { - describe("blur threshold", () => - { - it("should return true when blur >= 4 and size >= 128", () => - { - const result = shouldUseComputeShader(4, 4, 128, 128); - - expect(result).toBe(true); - }); - - it("should return true when blurX >= 4 (using max)", () => - { - const result = shouldUseComputeShader(5, 2, 128, 128); - - expect(result).toBe(true); - }); - - it("should return true when blurY >= 4 (using max)", () => - { - const result = shouldUseComputeShader(2, 5, 128, 128); - - expect(result).toBe(true); - }); - - it("should return false when both blurs < 4", () => - { - const result = shouldUseComputeShader(3, 3, 128, 128); - - expect(result).toBe(false); - }); - - it("should return false when max blur < 4", () => - { - const result = shouldUseComputeShader(2, 3, 512, 512); - - expect(result).toBe(false); - }); - }); - - describe("size threshold", () => - { - it("should return true when min size >= 128", () => - { - const result = shouldUseComputeShader(10, 10, 128, 128); - - expect(result).toBe(true); - }); - - it("should return true when width >= 128 and height > 128", () => - { - const result = shouldUseComputeShader(10, 10, 128, 512); - - expect(result).toBe(true); - }); - - it("should return true when height >= 128 and width > 128", () => - { - const result = shouldUseComputeShader(10, 10, 512, 128); - - expect(result).toBe(true); - }); - - it("should return false when width < 128", () => - { - const result = shouldUseComputeShader(10, 10, 100, 512); - - expect(result).toBe(false); - }); - - it("should return false when height < 128", () => - { - const result = shouldUseComputeShader(10, 10, 512, 100); - - expect(result).toBe(false); - }); - - it("should return false when both dimensions < 128", () => - { - const result = shouldUseComputeShader(20, 20, 100, 100); - - expect(result).toBe(false); - }); - }); - - describe("edge cases", () => - { - it("should return true at exact thresholds", () => - { - const result = shouldUseComputeShader(4, 0, 128, 128); - - expect(result).toBe(true); - }); - - it("should return false just below blur threshold", () => - { - const result = shouldUseComputeShader(3.9, 3.9, 128, 128); - - expect(result).toBe(false); - }); - - it("should return false just below size threshold", () => - { - const result = shouldUseComputeShader(10, 10, 127, 127); - - expect(result).toBe(false); - }); - - it("should return false when only one condition is met", () => - { - // Large blur but small size - expect(shouldUseComputeShader(20, 20, 100, 100)).toBe(false); - - // Large size but small blur - expect(shouldUseComputeShader(3, 3, 1024, 1024)).toBe(false); - }); - - it("should handle zero blur values", () => - { - const result = shouldUseComputeShader(0, 0, 512, 512); - - expect(result).toBe(false); - }); - - it("should handle large values", () => - { - const result = shouldUseComputeShader(100, 100, 4096, 4096); - - expect(result).toBe(true); - }); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.ts b/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.ts deleted file mode 100644 index 7cfbace5..00000000 --- a/packages/webgpu/src/Filter/BlurFilter/service/BlurFilterComputeShaderService.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { IAttachmentObject } from "../../../interface/IAttachmentObject"; -import type { IFilterConfig } from "../../../interface/IFilterConfig"; -import type { ComputePipelineManager } from "../../../Compute/ComputePipelineManager"; -import { execute as executeBlurCompute } from "../../../Compute/service/ComputeExecuteBlurService"; - -/** - * @description Compute Shaderでブラーパスを実行 - * Apply blur pass using Compute Shader - * - * Fragment Shaderベースの従来実装と比較して: - * - 並列処理による高速化(大きな半径で20-35%) - * - 共有メモリを活用したメモリアクセス最適化 - * - ワークグループ内でのデータ共有 - * - * @param {GPUDevice} device - WebGPU device - * @param {GPUCommandEncoder} commandEncoder - コマンドエンコーダー - * @param {ComputePipelineManager} computePipelineManager - Compute Pipeline Manager - * @param {IFilterConfig} config - フィルター設定 - * @param {IAttachmentObject} source - 入力アタッチメント - * @param {IAttachmentObject} dest - 出力アタッチメント - * @param {boolean} isHorizontal - 水平ブラーかどうか - * @param {number} blur - ブラー量 - * @return {void} - */ -export const execute = ( - device: GPUDevice, - commandEncoder: GPUCommandEncoder, - computePipelineManager: ComputePipelineManager, - _config: IFilterConfig, - source: IAttachmentObject, - dest: IAttachmentObject, - isHorizontal: boolean, - blur: number -): void => { - - // ブラー半径を計算(ブラー量の半分) - const radius = Math.ceil(blur / 2); - - // Compute Shaderでブラーを実行 - executeBlurCompute( - device, - commandEncoder, - computePipelineManager, - source, - dest, - isHorizontal, - radius - ); -}; - -/** - * @description Compute Shaderを使用すべきかどうか判定 - * Determine whether to use Compute Shader - * - * 以下の条件でCompute Shaderを使用: - * - ブラー半径が大きい(8以上) - * - テクスチャサイズが十分大きい(256x256以上) - * - * 小さなブラー半径では Fragment Shader の方が効率的な場合がある。 - * - * @param {number} blurX - X方向のブラー量 - * @param {number} blurY - Y方向のブラー量 - * @param {number} width - テクスチャ幅 - * @param {number} height - テクスチャ高さ - * @return {boolean} Compute Shaderを使用すべきかどうか - */ -export const shouldUseComputeShader = ( - blurX: number, - blurY: number, - width: number, - height: number -): boolean => { - - // ブラー半径のしきい値 - const BLUR_THRESHOLD = 4; - - // テクスチャサイズのしきい値 - const SIZE_THRESHOLD = 128; - - const maxBlur = Math.max(blurX, blurY); - const minSize = Math.min(width, height); - - return maxBlur >= BLUR_THRESHOLD && minSize >= SIZE_THRESHOLD; -}; diff --git a/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.test.ts b/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.test.ts deleted file mode 100644 index 9d8d9053..00000000 --- a/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.test.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import type { IAttachmentObject } from "../../../interface/IAttachmentObject"; -import type { IFilterConfig } from "../../../interface/IFilterConfig"; -import type { ComputePipelineManager } from "../../../Compute/ComputePipelineManager"; -import { execute } from "./FilterApplyBlurComputeUseCase"; - -// Mock GPUBufferUsage -const GPUBufferUsage = { - UNIFORM: 0x40, - COPY_DST: 0x08 -}; -(globalThis as any).GPUBufferUsage = GPUBufferUsage; - -// Mock offset - use object that will be imported -vi.mock("../../FilterOffset", () => ({ - "$offset": { "x": 0, "y": 0 } -})); - -import { $offset } from "../../FilterOffset"; - -// Mock calculateBlurParams -const mockCalculateBlurParams = vi.fn(); -vi.mock("../../BlurFilterUseCase", () => ({ - "calculateBlurParams": (...args: any[]) => mockCalculateBlurParams(...args) -})); - -// Mock BlurFilterComputeShaderService -const mockBlurComputeService = vi.fn(); -const mockShouldUseComputeShader = vi.fn(); -vi.mock("../service/BlurFilterComputeShaderService", () => ({ - "execute": (...args: any[]) => mockBlurComputeService(...args), - "shouldUseComputeShader": (...args: any[]) => mockShouldUseComputeShader(...args) -})); - -// Mock FilterApplyBlurFilterUseCase (fragment fallback) -const mockExecuteFragmentBlur = vi.fn(); -vi.mock("../FilterApplyBlurFilterUseCase", () => ({ - "execute": (...args: any[]) => mockExecuteFragmentBlur(...args) -})); - -describe("FilterApplyBlurComputeUseCase", () => -{ - const createMockAttachment = (width: number = 100, height: number = 100): IAttachmentObject => - { - return { - "id": 1, - "width": width, - "height": height, - "clipLevel": 0, - "texture": { - "resource": { "label": "mockTexture" } as unknown as GPUTexture, - "view": { "label": "mockTextureView" } as unknown as GPUTextureView - } - } as IAttachmentObject; - }; - - const createMockConfig = (): IFilterConfig => - { - const mockPassEncoder = { - "setPipeline": vi.fn(), - "setBindGroup": vi.fn(), - "setViewport": vi.fn(), - "setScissorRect": vi.fn(), - "draw": vi.fn(), - "end": vi.fn() - }; - - return { - "device": { - "createBuffer": vi.fn(() => ({ "label": "mockBuffer" })), - "queue": { "writeBuffer": vi.fn() }, - "createBindGroup": vi.fn(() => ({ "label": "mockBindGroup" })) - } as unknown as GPUDevice, - "commandEncoder": { - "beginRenderPass": vi.fn(() => mockPassEncoder) - } as unknown as GPUCommandEncoder, - "frameBufferManager": { - "createTemporaryAttachment": vi.fn((w: number, h: number) => createMockAttachment(w, h)), - "releaseTemporaryAttachment": vi.fn(), - "createRenderPassDescriptor": vi.fn(() => ({ - "colorAttachments": [{ "view": {}, "loadOp": "clear", "storeOp": "store" }] - })) - }, - "pipelineManager": { - "getPipeline": vi.fn(() => ({ "label": "mockPipeline" })), - "getBindGroupLayout": vi.fn(() => ({ "label": "mockLayout" })) - }, - "textureManager": { - "createSampler": vi.fn(() => ({ "label": "mockSampler" })) - } - } as unknown as IFilterConfig; - }; - - const createMockComputePipelineManager = (): ComputePipelineManager => - { - return { - "getPipeline": vi.fn(() => ({ "label": "mockComputePipeline" })), - "getBindGroupLayout": vi.fn(() => ({ "label": "mockComputeLayout" })) - } as unknown as ComputePipelineManager; - }; - - beforeEach(() => - { - vi.clearAllMocks(); - $offset.x = 0; - $offset.y = 0; - vi.spyOn(console, "error").mockImplementation(() => {}); - - // Default mock implementations - mockCalculateBlurParams.mockReturnValue({ - "baseBlurX": 16, - "baseBlurY": 16, - "offsetX": 32, - "offsetY": 32, - "bufferScaleX": 1, - "bufferScaleY": 1 - }); - }); - - describe("compute shader decision", () => - { - it("should use fragment shader when compute is not appropriate", () => - { - mockShouldUseComputeShader.mockReturnValue(false); - const expectedResult = createMockAttachment(200, 200); - mockExecuteFragmentBlur.mockReturnValue(expectedResult); - - const sourceAttachment = createMockAttachment(); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - const result = execute( - sourceAttachment, - matrix, - 4, // small blurX - 4, // small blurY - 1, - 1, - config, - computePipelineManager - ); - - expect(mockShouldUseComputeShader).toHaveBeenCalled(); - expect(mockExecuteFragmentBlur).toHaveBeenCalled(); - expect(result).toBe(expectedResult); - }); - - it("should use compute shader when appropriate", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - mockCalculateBlurParams.mockReturnValue({ - "baseBlurX": 32, - "baseBlurY": 32, - "offsetX": 64, - "offsetY": 64, - "bufferScaleX": 1, - "bufferScaleY": 1 - }); - - const sourceAttachment = createMockAttachment(); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - execute( - sourceAttachment, - matrix, - 32, - 32, - 1, - 1, - config, - computePipelineManager - ); - - expect(mockShouldUseComputeShader).toHaveBeenCalled(); - expect(mockExecuteFragmentBlur).not.toHaveBeenCalled(); - expect(mockBlurComputeService).toHaveBeenCalled(); - }); - }); - - describe("blur parameters", () => - { - it("should calculate blur parameters correctly", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - - const sourceAttachment = createMockAttachment(100, 100); - const matrix = new Float32Array([2, 0, 0, 0, 2, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - execute( - sourceAttachment, - matrix, - 16, - 16, - 3, - 2, - config, - computePipelineManager - ); - - expect(mockCalculateBlurParams).toHaveBeenCalledWith( - matrix, - 16, - 16, - 3, - 2 - ); - }); - - it("should update offset based on blur parameters", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - mockCalculateBlurParams.mockReturnValue({ - "baseBlurX": 16, - "baseBlurY": 16, - "offsetX": 20, - "offsetY": 25, - "bufferScaleX": 1, - "bufferScaleY": 1 - }); - - const sourceAttachment = createMockAttachment(); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - execute( - sourceAttachment, - matrix, - 16, - 16, - 1, - 1, - config, - computePipelineManager - ); - - expect($offset.x).toBe(20); - expect($offset.y).toBe(25); - }); - }); - - describe("multi-pass blur", () => - { - it("should perform horizontal and vertical passes for each quality level", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - - const sourceAttachment = createMockAttachment(); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - execute( - sourceAttachment, - matrix, - 16, - 16, - 3, // quality = 3 - 1, - config, - computePipelineManager - ); - - // 3 quality passes * 2 directions (horizontal + vertical) = 6 calls - expect(mockBlurComputeService).toHaveBeenCalledTimes(6); - }); - - it("should skip horizontal pass when blurX is 0", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - mockCalculateBlurParams.mockReturnValue({ - "baseBlurX": 0, - "baseBlurY": 16, - "offsetX": 0, - "offsetY": 32, - "bufferScaleX": 1, - "bufferScaleY": 1 - }); - - const sourceAttachment = createMockAttachment(); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - execute( - sourceAttachment, - matrix, - 0, - 16, - 1, - 1, - config, - computePipelineManager - ); - - // Only vertical passes - expect(mockBlurComputeService).toHaveBeenCalledTimes(1); - // Should be called with horizontal=false - expect(mockBlurComputeService).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - false, // horizontal = false (vertical pass) - expect.any(Number) - ); - }); - - it("should skip vertical pass when blurY is 0", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - mockCalculateBlurParams.mockReturnValue({ - "baseBlurX": 16, - "baseBlurY": 0, - "offsetX": 32, - "offsetY": 0, - "bufferScaleX": 1, - "bufferScaleY": 1 - }); - - const sourceAttachment = createMockAttachment(); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - execute( - sourceAttachment, - matrix, - 16, - 0, - 1, - 1, - config, - computePipelineManager - ); - - // Only horizontal passes - expect(mockBlurComputeService).toHaveBeenCalledTimes(1); - // Should be called with horizontal=true - expect(mockBlurComputeService).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - expect.anything(), - true, // horizontal = true - expect.any(Number) - ); - }); - }); - - describe("buffer management", () => - { - it("should create temporary attachments for ping-pong buffer", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - - const sourceAttachment = createMockAttachment(100, 100); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - execute( - sourceAttachment, - matrix, - 16, - 16, - 1, - 1, - config, - computePipelineManager - ); - - // Should create 2 temporary attachments for ping-pong - expect(config.frameBufferManager.createTemporaryAttachment).toHaveBeenCalledTimes(2); - }); - - it("should release unused buffer after processing", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - - const sourceAttachment = createMockAttachment(100, 100); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - execute( - sourceAttachment, - matrix, - 16, - 16, - 1, - 1, - config, - computePipelineManager - ); - - // Should release the unused ping-pong buffer - expect(config.frameBufferManager.releaseTemporaryAttachment).toHaveBeenCalled(); - }); - }); - - describe("return value", () => - { - it("should return result attachment after compute blur", () => - { - mockShouldUseComputeShader.mockReturnValue(true); - - const sourceAttachment = createMockAttachment(100, 100); - const matrix = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]); - const config = createMockConfig(); - const computePipelineManager = createMockComputePipelineManager(); - - const result = execute( - sourceAttachment, - matrix, - 16, - 16, - 1, - 1, - config, - computePipelineManager - ); - - expect(result).toBeDefined(); - expect(result.texture).toBeDefined(); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.ts b/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.ts deleted file mode 100644 index 9639a3a8..00000000 --- a/packages/webgpu/src/Filter/BlurFilter/usecase/FilterApplyBlurComputeUseCase.ts +++ /dev/null @@ -1,292 +0,0 @@ -import type { IAttachmentObject } from "../../../interface/IAttachmentObject"; -import type { IFilterConfig } from "../../../interface/IFilterConfig"; -import type { ComputePipelineManager } from "../../../Compute/ComputePipelineManager"; -import { $offset } from "../../FilterOffset"; -import { calculateBlurParams } from "../../BlurFilterUseCase"; -import { - execute as blurComputeService, - shouldUseComputeShader -} from "../service/BlurFilterComputeShaderService"; -import { execute as executeFragmentBlur } from "../FilterApplyBlurFilterUseCase"; - -/** - * @description プリアロケートされたFloat32Array (サイズ4) - */ -const $uniform4 = new Float32Array(4); - -/** - * @description プリアロケートされたBindGroupEntry配列 (バインディング3つ) - */ -const $entries3: GPUBindGroupEntry[] = [ - { "binding": 0, "resource": { "buffer": null as unknown as GPUBuffer } }, - { "binding": 1, "resource": null as unknown as GPUSampler }, - { "binding": 2, "resource": null as unknown as GPUTextureView } -]; - -/** - * @description Compute Shaderを使用したブラーフィルター - * Apply blur filter using Compute Shader - * - * Fragment Shaderベースの従来実装と比較して: - * - 大きなブラー半径で20-35%高速化 - * - 並列処理による効率的なテクスチャサンプリング - * - 共有メモリを活用したメモリアクセス最適化 - * - * 小さなブラー半径(8未満)では従来のFragment Shaderを使用。 - * - * @param {IAttachmentObject} sourceAttachment - 入力テクスチャ - * @param {Float32Array} matrix - 変換行列 - * @param {number} blurX - X方向のブラー量 - * @param {number} blurY - Y方向のブラー量 - * @param {number} quality - クオリティ (1-15) - * @param {number} devicePixelRatio - デバイスピクセル比 - * @param {IFilterConfig} config - WebGPUリソース設定 - * @param {ComputePipelineManager} computePipelineManager - Compute Pipeline Manager - * @return {IAttachmentObject} - フィルター適用後のアタッチメント - */ -export const execute = ( - sourceAttachment: IAttachmentObject, - matrix: Float32Array, - blurX: number, - blurY: number, - quality: number, - devicePixelRatio: number, - config: IFilterConfig, - computePipelineManager: ComputePipelineManager -): IAttachmentObject => { - - const { device, commandEncoder, frameBufferManager, pipelineManager, textureManager } = config; - - // ブラーパラメータを計算 - const blurParams = calculateBlurParams(matrix, blurX, blurY, quality, devicePixelRatio); - const { baseBlurX, baseBlurY, offsetX, offsetY, bufferScaleX, bufferScaleY } = blurParams; - - // オフセットを更新 - $offset.x += offsetX; - $offset.y += offsetY; - - // ブラー用バッファサイズを計算 - const width = sourceAttachment.width + offsetX * 2; - const height = sourceAttachment.height + offsetY * 2; - const bufferWidth = Math.ceil(width * bufferScaleX); - const bufferHeight = Math.ceil(height * bufferScaleY); - - // Compute Shaderを使用すべきか判定 - const useCompute = shouldUseComputeShader(baseBlurX, baseBlurY, bufferWidth, bufferHeight); - - if (!useCompute) { - // 小さなブラーは従来のFragment Shaderを使用 - // FilterApplyBlurFilterUseCaseにフォールバック - return executeFragmentBlur( - sourceAttachment, - matrix, - blurX, - blurY, - quality, - devicePixelRatio, - config - ); - } - - // ピンポンバッファ用の一時アタッチメントを作成 - const attachment0 = frameBufferManager.createTemporaryAttachment(bufferWidth, bufferHeight); - const attachment1 = frameBufferManager.createTemporaryAttachment(bufferWidth, bufferHeight); - - // サンプラーを作成(線形補間) - const sampler = textureManager.createSampler("blur_compute_sampler", true); - - // ソーステクスチャをattachment0にコピー - copyTextureToAttachment( - device, commandEncoder, frameBufferManager, pipelineManager, - sourceAttachment, attachment0, sampler, - bufferScaleX, bufferScaleY, - offsetX * bufferScaleX, offsetY * bufferScaleY, - config.bufferManager - ); - - // バッファスケールを考慮したブラー値 - const bufferBlurX = baseBlurX * bufferScaleX; - const bufferBlurY = baseBlurY * bufferScaleY; - - // Compute Shaderでブラーパスを実行 - const attachments = [attachment0, attachment1]; - let attachmentIndex = 0; - - for (let q = 0; q < quality; ++q) { - // 水平ブラー - if (blurX > 0) { - const srcIndex = attachmentIndex; - attachmentIndex = (attachmentIndex + 1) % 2; - - blurComputeService( - device, commandEncoder, computePipelineManager, config, - attachments[srcIndex], attachments[attachmentIndex], - true, bufferBlurX - ); - } - - // 垂直ブラー - if (blurY > 0) { - const srcIndex = attachmentIndex; - attachmentIndex = (attachmentIndex + 1) % 2; - - blurComputeService( - device, commandEncoder, computePipelineManager, config, - attachments[srcIndex], attachments[attachmentIndex], - false, bufferBlurY - ); - } - } - - // 結果のアタッチメント - let resultAttachment = attachments[attachmentIndex]; - - // バッファスケールが1でない場合は元のサイズにアップスケール - if (bufferScaleX !== 1 || bufferScaleY !== 1) { - const finalAttachment = frameBufferManager.createTemporaryAttachment(width, height); - - upscaleTexture( - device, commandEncoder, frameBufferManager, pipelineManager, - resultAttachment, finalAttachment, sampler, - config.bufferManager - ); - - // ピンポンバッファを解放 - frameBufferManager.releaseTemporaryAttachment(attachment0); - frameBufferManager.releaseTemporaryAttachment(attachment1); - - resultAttachment = finalAttachment; - } else { - // 使わなかったバッファを解放 - const unusedIndex = (attachmentIndex + 1) % 2; - frameBufferManager.releaseTemporaryAttachment(attachments[unusedIndex]); - } - - return resultAttachment; -}; - -/** - * @description テクスチャをアタッチメントにコピー - */ -const copyTextureToAttachment = ( - device: GPUDevice, - commandEncoder: GPUCommandEncoder, - frameBufferManager: IFilterConfig["frameBufferManager"], - pipelineManager: IFilterConfig["pipelineManager"], - source: IAttachmentObject, - dest: IAttachmentObject, - sampler: GPUSampler, - bufferScaleX: number, - bufferScaleY: number, - pixelOffsetX: number, - pixelOffsetY: number, - bufferManager?: IFilterConfig["bufferManager"] -): void => { - const pipeline = pipelineManager.getPipeline("texture_copy_rgba8"); - const bindGroupLayout = pipelineManager.getBindGroupLayout("texture_copy"); - - if (!pipeline || !bindGroupLayout) { - console.error("[WebGPU BlurCompute] texture_copy_rgba8 pipeline not found"); - return; - } - - const scaledSourceWidth = source.width * bufferScaleX; - const scaledSourceHeight = source.height * bufferScaleY; - - $uniform4[0] = 1; - $uniform4[1] = 1; - $uniform4[2] = 0; - $uniform4[3] = 0; - const uniformBuffer = bufferManager - ? bufferManager.acquireAndWriteUniformBuffer($uniform4) - : device.createBuffer({ - "size": $uniform4.byteLength, - "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST - }); - if (!bufferManager) { - device.queue.writeBuffer(uniformBuffer, 0, $uniform4); - } - - ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; - $entries3[1].resource = sampler; - $entries3[2].resource = source.texture!.view; - const bindGroup = device.createBindGroup({ - "layout": bindGroupLayout, - "entries": $entries3 - }); - - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( - dest.texture!.view, 0, 0, 0, 0, "clear" - ); - - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); - passEncoder.setPipeline(pipeline); - passEncoder.setBindGroup(0, bindGroup); - - passEncoder.setViewport( - pixelOffsetX, pixelOffsetY, - scaledSourceWidth, scaledSourceHeight, - 0, 1 - ); - passEncoder.setScissorRect( - Math.floor(pixelOffsetX), Math.floor(pixelOffsetY), - Math.ceil(scaledSourceWidth), Math.ceil(scaledSourceHeight) - ); - - passEncoder.draw(6, 1, 0, 0); - passEncoder.end(); -}; - -/** - * @description テクスチャをアップスケール - */ -const upscaleTexture = ( - device: GPUDevice, - commandEncoder: GPUCommandEncoder, - frameBufferManager: IFilterConfig["frameBufferManager"], - pipelineManager: IFilterConfig["pipelineManager"], - source: IAttachmentObject, - dest: IAttachmentObject, - sampler: GPUSampler, - bufferManager?: IFilterConfig["bufferManager"] -): void => { - const pipeline = pipelineManager.getPipeline("texture_copy_rgba8"); - const bindGroupLayout = pipelineManager.getBindGroupLayout("texture_copy"); - - if (!pipeline || !bindGroupLayout) { - console.error("[WebGPU BlurCompute] texture_copy_rgba8 pipeline not found"); - return; - } - - $uniform4[0] = 1; - $uniform4[1] = 1; - $uniform4[2] = 0; - $uniform4[3] = 0; - const uniformBuffer = bufferManager - ? bufferManager.acquireAndWriteUniformBuffer($uniform4) - : device.createBuffer({ - "size": $uniform4.byteLength, - "usage": GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST - }); - if (!bufferManager) { - device.queue.writeBuffer(uniformBuffer, 0, $uniform4); - } - - ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; - $entries3[1].resource = sampler; - $entries3[2].resource = source.texture!.view; - const bindGroup = device.createBindGroup({ - "layout": bindGroupLayout, - "entries": $entries3 - }); - - const renderPassDescriptor = frameBufferManager.createRenderPassDescriptor( - dest.texture!.view, 0, 0, 0, 0, "clear" - ); - - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); - passEncoder.setPipeline(pipeline); - passEncoder.setBindGroup(0, bindGroup); - passEncoder.draw(6, 1, 0, 0); - passEncoder.end(); -}; diff --git a/packages/webgpu/src/Filter/BlurFilterShader.test.ts b/packages/webgpu/src/Filter/BlurFilterShader.test.ts deleted file mode 100644 index 09e1a83c..00000000 --- a/packages/webgpu/src/Filter/BlurFilterShader.test.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { BlurFilterShader } from "./BlurFilterShader"; - -describe("BlurFilterShader", () => -{ - describe("getVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = BlurFilterShader.getVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = BlurFilterShader.getVertexShader(); - - expect(shader).toContain("@vertex"); - }); - - it("should contain main function", () => - { - const shader = BlurFilterShader.getVertexShader(); - - expect(shader).toContain("fn main"); - }); - - it("should define VertexInput struct", () => - { - const shader = BlurFilterShader.getVertexShader(); - - expect(shader).toContain("struct VertexInput"); - }); - - it("should define VertexOutput struct", () => - { - const shader = BlurFilterShader.getVertexShader(); - - expect(shader).toContain("struct VertexOutput"); - }); - - it("should include position and texCoord attributes", () => - { - const shader = BlurFilterShader.getVertexShader(); - - expect(shader).toContain("@location(0) position"); - expect(shader).toContain("@location(1) texCoord"); - }); - }); - - describe("getHorizontalBlurShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlurFilterShader.getHorizontalBlurShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlurFilterShader.getHorizontalBlurShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should define BlurUniforms struct", () => - { - const shader = BlurFilterShader.getHorizontalBlurShader(); - - expect(shader).toContain("struct BlurUniforms"); - }); - - it("should include blur parameters", () => - { - const shader = BlurFilterShader.getHorizontalBlurShader(); - - expect(shader).toContain("blurSize"); - expect(shader).toContain("textureWidth"); - }); - - it("should use textureWidth for horizontal sampling", () => - { - const shader = BlurFilterShader.getHorizontalBlurShader(); - - expect(shader).toContain("1.0 / uniforms.textureWidth"); - }); - - it("should include texture sampling", () => - { - const shader = BlurFilterShader.getHorizontalBlurShader(); - - expect(shader).toContain("textureSample"); - }); - }); - - describe("getVerticalBlurShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlurFilterShader.getVerticalBlurShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlurFilterShader.getVerticalBlurShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should define BlurUniforms struct", () => - { - const shader = BlurFilterShader.getVerticalBlurShader(); - - expect(shader).toContain("struct BlurUniforms"); - }); - - it("should use textureHeight for vertical sampling", () => - { - const shader = BlurFilterShader.getVerticalBlurShader(); - - expect(shader).toContain("1.0 / uniforms.textureHeight"); - }); - - it("should include y-axis offset calculation", () => - { - const shader = BlurFilterShader.getVerticalBlurShader(); - - expect(shader).toContain("input.texCoord.y + offset"); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/BlurFilterShader.ts b/packages/webgpu/src/Filter/BlurFilterShader.ts deleted file mode 100644 index c61ba3aa..00000000 --- a/packages/webgpu/src/Filter/BlurFilterShader.ts +++ /dev/null @@ -1,115 +0,0 @@ -export class BlurFilterShader -{ - static getVertexShader(): string - { - return /* wgsl */` - struct VertexInput { - @location(0) position: vec2, - @location(1) texCoord: vec2, - } - - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - @vertex - fn main(input: VertexInput) -> VertexOutput { - var output: VertexOutput; - output.position = vec4(input.position, 0.0, 1.0); - output.texCoord = input.texCoord; - return output; - } - `; - } - - static getHorizontalBlurShader(): string - { - return /* wgsl */` - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - struct BlurUniforms { - blurSize: f32, - textureWidth: f32, - textureHeight: f32, - _padding: f32, - } - - @group(0) @binding(0) var uniforms: BlurUniforms; - @group(0) @binding(1) var textureSampler: sampler; - @group(0) @binding(2) var textureData: texture_2d; - - @fragment - fn main(input: VertexOutput) -> @location(0) vec4 { - let texelSize = 1.0 / uniforms.textureWidth; - var color = vec4(0.0); - let blurRadius = i32(uniforms.blurSize); - - var totalWeight = 0.0; - - for (var i = -blurRadius; i <= blurRadius; i++) { - let offset = f32(i) * texelSize; - let weight = 1.0 - abs(f32(i)) / f32(blurRadius + 1); - - let sampleCoord = vec2( - input.texCoord.x + offset, - input.texCoord.y - ); - - color += textureSample(textureData, textureSampler, sampleCoord) * weight; - totalWeight += weight; - } - - return color / totalWeight; - } - `; - } - - static getVerticalBlurShader(): string - { - return /* wgsl */` - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - struct BlurUniforms { - blurSize: f32, - textureWidth: f32, - textureHeight: f32, - _padding: f32, - } - - @group(0) @binding(0) var uniforms: BlurUniforms; - @group(0) @binding(1) var textureSampler: sampler; - @group(0) @binding(2) var textureData: texture_2d; - - @fragment - fn main(input: VertexOutput) -> @location(0) vec4 { - let texelSize = 1.0 / uniforms.textureHeight; - var color = vec4(0.0); - let blurRadius = i32(uniforms.blurSize); - - var totalWeight = 0.0; - - for (var i = -blurRadius; i <= blurRadius; i++) { - let offset = f32(i) * texelSize; - let weight = 1.0 - abs(f32(i)) / f32(blurRadius + 1); - - let sampleCoord = vec2( - input.texCoord.x, - input.texCoord.y + offset - ); - - color += textureSample(textureData, textureSampler, sampleCoord) * weight; - totalWeight += weight; - } - - return color / totalWeight; - } - `; - } -} diff --git a/packages/webgpu/src/Filter/BlurFilterUseCase.ts b/packages/webgpu/src/Filter/BlurFilterUseCase.ts index 2954d3d1..5735d330 100644 --- a/packages/webgpu/src/Filter/BlurFilterUseCase.ts +++ b/packages/webgpu/src/Filter/BlurFilterUseCase.ts @@ -6,24 +6,27 @@ /** * @description ブラー計算用のステップ値 * Step values for blur calculation + * @type {number[]} */ -const BLUR_STEP: number[] = [0.5, 1.05, 1.4, 1.55, 1.75, 1.9, 2, 2.15, 2.2, 2.3, 2.5, 3, 3, 3.5, 3.5]; +const $BLUR_STEP: number[] = [0.5, 1.05, 1.4, 1.55, 1.75, 1.9, 2, 2.15, 2.2, 2.3, 2.5, 3, 3, 3.5, 3.5]; /** * @description ブラーフィルターパラメータを計算 - * @param {Float32Array} matrix - 変換行列 - * @param {number} blurX - X方向のブラー量 - * @param {number} blurY - Y方向のブラー量 - * @param {number} quality - クオリティ (1-15) - * @param {number} devicePixelRatio - デバイスピクセル比 + * Calculate blur filter parameters + * + * @param {Float32Array} matrix - 変換行列 + * @param {number} blur_x - X方向のブラー量 + * @param {number} blur_y - Y方向のブラー量 + * @param {number} quality - クオリティ (1-15) + * @param {number} device_pixel_ratio - デバイスピクセル比 * @return {object} */ export const calculateBlurParams = ( matrix: Float32Array, - blurX: number, - blurY: number, + blur_x: number, + blur_y: number, quality: number, - devicePixelRatio: number + device_pixel_ratio: number ): { baseBlurX: number; baseBlurY: number; @@ -35,10 +38,10 @@ export const calculateBlurParams = ( const xScale = Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1]); const yScale = Math.sqrt(matrix[2] * matrix[2] + matrix[3] * matrix[3]); - const baseBlurX = blurX * (xScale / devicePixelRatio); - const baseBlurY = blurY * (yScale / devicePixelRatio); + const baseBlurX = blur_x * (xScale / device_pixel_ratio); + const baseBlurY = blur_y * (yScale / device_pixel_ratio); - const step = BLUR_STEP[Math.min(quality - 1, BLUR_STEP.length - 1)]; + const step = $BLUR_STEP[Math.min(quality - 1, $BLUR_STEP.length - 1)]; const offsetX = Math.round(baseBlurX * step); const offsetY = Math.round(baseBlurY * step); @@ -78,17 +81,19 @@ export const calculateBlurParams = ( /** * @description 方向ブラーのパラメータを計算 - * @param {boolean} isHorizontal - 水平方向かどうか - * @param {number} blur - ブラー量 - * @param {number} textureWidth - テクスチャ幅 - * @param {number} textureHeight - テクスチャ高さ + * Calculate directional blur parameters + * + * @param {boolean} is_horizontal - 水平方向かどうか + * @param {number} blur - ブラー量 + * @param {number} texture_width - テクスチャ幅 + * @param {number} texture_height - テクスチャ高さ * @return {object} */ export const calculateDirectionalBlurParams = ( - isHorizontal: boolean, + is_horizontal: boolean, blur: number, - textureWidth: number, - textureHeight: number + texture_width: number, + texture_height: number ): { offsetX: number; offsetY: number; @@ -101,8 +106,8 @@ export const calculateDirectionalBlurParams = ( const samples = 1 + blur; // テクセルオフセットを計算 - const offsetX = isHorizontal ? 1 / textureWidth : 0; - const offsetY = isHorizontal ? 0 : 1 / textureHeight; + const offsetX = is_horizontal ? 1 / texture_width : 0; + const offsetY = is_horizontal ? 0 : 1 / texture_height; return { offsetX, diff --git a/packages/webgpu/src/Filter/ColorMatrixFilter/FilterApplyColorMatrixFilterUseCase.ts b/packages/webgpu/src/Filter/ColorMatrixFilter/FilterApplyColorMatrixFilterUseCase.ts index b9ef9282..3ae6c859 100644 --- a/packages/webgpu/src/Filter/ColorMatrixFilter/FilterApplyColorMatrixFilterUseCase.ts +++ b/packages/webgpu/src/Filter/ColorMatrixFilter/FilterApplyColorMatrixFilterUseCase.ts @@ -19,13 +19,13 @@ const $entries3: GPUBindGroupEntry[] = [ * @description カラーマトリックスフィルターを適用 * Apply color matrix filter * - * @param {IAttachmentObject} sourceAttachment - 入力テクスチャ(アタッチメント) + * @param {IAttachmentObject} source_attachment - 入力テクスチャ(アタッチメント) * @param {Float32Array} matrix - 4x5カラーマトリックス (20 floats) * @param {IFilterConfig} config - WebGPUリソース設定 * @return {IAttachmentObject} - フィルター適用後のアタッチメント */ export const execute = ( - sourceAttachment: IAttachmentObject, + source_attachment: IAttachmentObject, matrix: Float32Array, config: IFilterConfig ): IAttachmentObject => { @@ -34,8 +34,8 @@ export const execute = ( // 出力アタッチメントを作成 const destAttachment = frameBufferManager.createTemporaryAttachment( - sourceAttachment.width, - sourceAttachment.height + source_attachment.width, + source_attachment.height ); const pipeline = pipelineManager.getPipeline("color_matrix_filter"); @@ -43,7 +43,7 @@ export const execute = ( if (!pipeline || !bindGroupLayout) { console.error("[WebGPU ColorMatrixFilter] Pipeline not found"); - return sourceAttachment; + return source_attachment; } // サンプラーを作成 @@ -93,7 +93,7 @@ export const execute = ( // バインドグループを作成 ($entries3[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries3[1].resource = sampler; - $entries3[2].resource = sourceAttachment.texture!.view; + $entries3[2].resource = source_attachment.texture!.view; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries3 diff --git a/packages/webgpu/src/Filter/ColorMatrixFilterShader.test.ts b/packages/webgpu/src/Filter/ColorMatrixFilterShader.test.ts deleted file mode 100644 index 672e57af..00000000 --- a/packages/webgpu/src/Filter/ColorMatrixFilterShader.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { ColorMatrixFilterShader } from "./ColorMatrixFilterShader"; - -describe("ColorMatrixFilterShader", () => -{ - describe("getVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = ColorMatrixFilterShader.getVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = ColorMatrixFilterShader.getVertexShader(); - - expect(shader).toContain("@vertex"); - }); - - it("should define VertexInput struct", () => - { - const shader = ColorMatrixFilterShader.getVertexShader(); - - expect(shader).toContain("struct VertexInput"); - }); - - it("should define VertexOutput struct", () => - { - const shader = ColorMatrixFilterShader.getVertexShader(); - - expect(shader).toContain("struct VertexOutput"); - }); - }); - - describe("getFragmentShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = ColorMatrixFilterShader.getFragmentShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = ColorMatrixFilterShader.getFragmentShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should define ColorMatrixUniforms struct", () => - { - const shader = ColorMatrixFilterShader.getFragmentShader(); - - expect(shader).toContain("struct ColorMatrixUniforms"); - }); - - it("should include matrix uniform", () => - { - const shader = ColorMatrixFilterShader.getFragmentShader(); - - expect(shader).toContain("matrix: mat4x4"); - }); - - it("should include offset uniform", () => - { - const shader = ColorMatrixFilterShader.getFragmentShader(); - - expect(shader).toContain("offset: vec4"); - }); - - it("should apply matrix transformation", () => - { - const shader = ColorMatrixFilterShader.getFragmentShader(); - - expect(shader).toContain("uniforms.matrix * color"); - }); - - it("should apply offset", () => - { - const shader = ColorMatrixFilterShader.getFragmentShader(); - - expect(shader).toContain("+ uniforms.offset"); - }); - - it("should clamp result to 0-1 range", () => - { - const shader = ColorMatrixFilterShader.getFragmentShader(); - - expect(shader).toContain("clamp"); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/ColorMatrixFilterShader.ts b/packages/webgpu/src/Filter/ColorMatrixFilterShader.ts deleted file mode 100644 index ed0108c2..00000000 --- a/packages/webgpu/src/Filter/ColorMatrixFilterShader.ts +++ /dev/null @@ -1,55 +0,0 @@ -export class ColorMatrixFilterShader -{ - static getFragmentShader(): string - { - return /* wgsl */` - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - struct ColorMatrixUniforms { - matrix: mat4x4, - offset: vec4, - } - - @group(0) @binding(0) var uniforms: ColorMatrixUniforms; - @group(0) @binding(1) var textureSampler: sampler; - @group(0) @binding(2) var textureData: texture_2d; - - @fragment - fn main(input: VertexOutput) -> @location(0) vec4 { - var color = textureSample(textureData, textureSampler, input.texCoord); - - var result = uniforms.matrix * color + uniforms.offset; - - result = clamp(result, vec4(0.0), vec4(1.0)); - - return result; - } - `; - } - - static getVertexShader(): string - { - return /* wgsl */` - struct VertexInput { - @location(0) position: vec2, - @location(1) texCoord: vec2, - } - - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - @vertex - fn main(input: VertexInput) -> VertexOutput { - var output: VertexOutput; - output.position = vec4(input.position, 0.0, 1.0); - output.texCoord = input.texCoord; - return output; - } - `; - } -} diff --git a/packages/webgpu/src/Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase.ts b/packages/webgpu/src/Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase.ts index 57bee8ff..f5d8bb0e 100644 --- a/packages/webgpu/src/Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase.ts +++ b/packages/webgpu/src/Filter/ConvolutionFilter/FilterApplyConvolutionFilterUseCase.ts @@ -1,6 +1,7 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject"; import type { IFilterConfig } from "../../interface/IFilterConfig"; import { ShaderSource } from "../../Shader/ShaderSource"; +import { intToStraightRGBA } from "../FilterUtil"; /** * @description プリアロケートされたBindGroupEntry配列 (バインディング3つ) @@ -11,16 +12,6 @@ const $entries3: GPUBindGroupEntry[] = [ { "binding": 2, "resource": null as unknown as GPUTextureView } ]; -/** - * @description 32bit整数からRGB値を抽出 - */ -const intToRGBA = (color: number, alpha: number): [number, number, number, number] => { - const r = (color >> 16 & 0xFF) / 255; - const g = (color >> 8 & 0xFF) / 255; - const b = (color & 0xFF) / 255; - return [r, g, b, alpha]; -}; - /** * @description パイプラインキャッシュ(キー: matrixX,matrixY,preserveAlpha,clamp) */ @@ -31,15 +22,29 @@ const $pipelineCache = new Map -{ - describe("getConvolutionFilterFragmentShader", () => - { - it("should return a valid WGSL shader string", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("@vertex"); - }); - - it("should contain @fragment attribute", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("@fragment"); - }); - - it("should define ConvolutionUniforms struct", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("struct ConvolutionUniforms"); - }); - - it("should include rcpSize uniform", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("rcpSize: vec2"); - }); - - it("should include rcpDivisor uniform", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("rcpDivisor: f32"); - }); - - it("should include bias uniform", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("bias: f32"); - }); - - it("should include substituteColor uniform", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("substituteColor: vec4"); - }); - - it("should include matrix array", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("matrix: array"); - }); - - it("should generate correct matrix size for 3x3", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - // 3x3 = 9 elements, ceil(9/4) = 3 - expect(shader).toContain("array, 3>"); - }); - - it("should generate correct matrix size for 5x5", () => - { - const shader = getConvolutionFilterFragmentShader(5, 5, true, true); - // 5x5 = 25 elements, ceil(25/4) = 7 - expect(shader).toContain("array, 7>"); - }); - - it("should include isInside helper function", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("fn isInside"); - }); - - it("should include getMatrixWeight helper function", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("fn getMatrixWeight"); - }); - - it("should include getWeightedColor helper function", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("fn getWeightedColor"); - }); - - it("should preserve alpha when preserveAlpha is true", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("result.a = textureSample(sourceTexture, sourceSampler, input.texCoord).a"); - }); - - it("should not preserve alpha when preserveAlpha is false", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, false, true); - - expect(shader).not.toContain("result.a = textureSample(sourceTexture, sourceSampler, input.texCoord).a"); - }); - - it("should include substituteColor handling when clamp is false", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, false); - - expect(shader).toContain("substituteColor"); - expect(shader).toContain("mix(substituteColor, color, isInside(uv))"); - }); - - it("should not include substituteColor handling when clamp is true", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - // Should still have substituteColor in uniforms but not the mix statement - expect(shader).not.toContain("mix(substituteColor, color, isInside(uv))"); - }); - - it("should clamp result values", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("clamp(result * rcpDivisor + bias"); - }); - - it("should premultiply result", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("result.rgb * result.a"); - }); - - it("should unpremultiply color for processing", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - expect(shader).toContain("color.rgb / max(0.0001, color.a)"); - }); - - it("should generate 9 getWeightedColor calls for 3x3 matrix", () => - { - const shader = getConvolutionFilterFragmentShader(3, 3, true, true); - - let count = 0; - for (let i = 0; i < 9; i++) { - if (shader.includes(`getWeightedColor(${i}`)) { - count++; - } - } - - expect(count).toBe(9); - }); - - it("should handle asymmetric matrix sizes", () => - { - const shader = getConvolutionFilterFragmentShader(5, 3, true, true); - // 5x3 = 15 elements, ceil(15/4) = 4 - expect(shader).toContain("array, 4>"); - }); - }); - - describe("getConvolutionFilterShaderKey", () => - { - it("should generate unique key for 3x3 with preserveAlpha and clamp", () => - { - const key = getConvolutionFilterShaderKey(3, 3, true, true); - - expect(key).toBe("convolution_3x3_pa_c"); - }); - - it("should generate unique key for 5x5 without preserveAlpha and without clamp", () => - { - const key = getConvolutionFilterShaderKey(5, 5, false, false); - - expect(key).toBe("convolution_5x5_npa_nc"); - }); - - it("should include matrix dimensions in key", () => - { - const key = getConvolutionFilterShaderKey(7, 3, true, true); - - expect(key).toContain("7x3"); - }); - - it("should include preserveAlpha flag in key", () => - { - const keyWithPA = getConvolutionFilterShaderKey(3, 3, true, true); - const keyWithoutPA = getConvolutionFilterShaderKey(3, 3, false, true); - - expect(keyWithPA).toContain("_pa_"); - expect(keyWithoutPA).toContain("_npa_"); - }); - - it("should include clamp flag in key", () => - { - const keyWithClamp = getConvolutionFilterShaderKey(3, 3, true, true); - const keyWithoutClamp = getConvolutionFilterShaderKey(3, 3, true, false); - - expect(keyWithClamp).toContain("_c"); - expect(keyWithoutClamp).toContain("_nc"); - }); - - it("should generate different keys for different configurations", () => - { - const key1 = getConvolutionFilterShaderKey(3, 3, true, true); - const key2 = getConvolutionFilterShaderKey(3, 3, false, true); - const key3 = getConvolutionFilterShaderKey(3, 3, true, false); - const key4 = getConvolutionFilterShaderKey(5, 5, true, true); - - expect(key1).not.toBe(key2); - expect(key1).not.toBe(key3); - expect(key1).not.toBe(key4); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/ConvolutionFilterShader.ts b/packages/webgpu/src/Filter/ConvolutionFilterShader.ts deleted file mode 100644 index e112bb40..00000000 --- a/packages/webgpu/src/Filter/ConvolutionFilterShader.ts +++ /dev/null @@ -1,130 +0,0 @@ -export const getConvolutionFilterFragmentShader = ( - matrixX: number, - matrixY: number, - preserveAlpha: boolean, - clamp: boolean -): string => { - const halfX = Math.floor(matrixX * 0.5); - const halfY = Math.floor(matrixY * 0.5); - const size = matrixX * matrixY; - - let matrixStatement = ""; - for (let idx = 0; idx < size; idx++) { - matrixStatement += ` - result = result + getWeightedColor(${idx}, getMatrixWeight(${idx}));`; - } - - const preserveAlphaStatement = preserveAlpha - ? "result.a = textureSample(sourceTexture, sourceSampler, input.texCoord).a;" - : ""; - - const clampStatement = clamp - ? "" - : ` - let substituteColor = uniforms.substituteColor; - color = mix(substituteColor, color, isInside(uv));`; - - return ` -struct ConvolutionUniforms { - rcpSize: vec2, - rcpDivisor: f32, - bias: f32, - substituteColor: vec4, - matrix: array, ${Math.ceil(size / 4)}>, -} - -@group(0) @binding(0) var uniforms: ConvolutionUniforms; -@group(0) @binding(1) var sourceSampler: sampler; -@group(0) @binding(2) var sourceTexture: texture_2d; - -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, -} - -fn isInside(uv: vec2) -> f32 { - let inside = step(vec2(0.0), uv) * step(uv, vec2(1.0)); - return inside.x * inside.y; -} - -fn getMatrixWeight(index: i32) -> f32 { - let vecIndex = index / 4; - let component = index % 4; - let vec = uniforms.matrix[vecIndex]; - - if (component == 0) { return vec.x; } - else if (component == 1) { return vec.y; } - else if (component == 2) { return vec.z; } - else { return vec.w; } -} - -fn getWeightedColor(i: i32, weight: f32) -> vec4 { - let rcpSize = uniforms.rcpSize; - - let iDivX = i / ${matrixX}; - let iModX = i - ${matrixX} * iDivX; - let offset = vec2(f32(iModX - ${halfX}), f32(${halfY} - iDivX)); - var uv = input.texCoord + offset * rcpSize; - - var color = textureSample(sourceTexture, sourceSampler, uv); - color = vec4(color.rgb / max(0.0001, color.a), color.a); - ${clampStatement} - - return color * weight; -} - -var input: VertexOutput; - -@vertex -fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { - var positions = array, 6>( - vec2(-1.0, -1.0), - vec2(1.0, -1.0), - vec2(-1.0, 1.0), - vec2(-1.0, 1.0), - vec2(1.0, -1.0), - vec2(1.0, 1.0) - ); - - var texCoords = array, 6>( - vec2(0.0, 1.0), - vec2(1.0, 1.0), - vec2(0.0, 0.0), - vec2(0.0, 0.0), - vec2(1.0, 1.0), - vec2(1.0, 0.0) - ); - - var output: VertexOutput; - output.position = vec4(positions[vertexIndex], 0.0, 1.0); - output.texCoord = texCoords[vertexIndex]; - return output; -} - -@fragment -fn fs_main(fragInput: VertexOutput) -> @location(0) vec4 { - input = fragInput; - - let rcpDivisor = uniforms.rcpDivisor; - let bias = uniforms.bias; - - var result = vec4(0.0); - ${matrixStatement} - - result = clamp(result * rcpDivisor + bias, vec4(0.0), vec4(1.0)); - ${preserveAlphaStatement} - - result = vec4(result.rgb * result.a, result.a); - return result; -} -`; -}; - -export const getConvolutionFilterShaderKey = ( - matrixX: number, - matrixY: number, - preserveAlpha: boolean, - clamp: boolean -): string => { - return `convolution_${matrixX}x${matrixY}_${preserveAlpha ? "pa" : "npa"}_${clamp ? "c" : "nc"}`; -}; diff --git a/packages/webgpu/src/Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase.ts b/packages/webgpu/src/Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase.ts index 2f44d640..2b89d782 100644 --- a/packages/webgpu/src/Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase.ts +++ b/packages/webgpu/src/Filter/DisplacementMapFilter/FilterApplyDisplacementMapFilterUseCase.ts @@ -1,6 +1,7 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject"; import type { IFilterConfig } from "../../interface/IFilterConfig"; import { ShaderSource } from "../../Shader/ShaderSource"; +import { intToPremultipliedRGBA } from "../FilterUtil"; /** * @description プリアロケートされたFloat32Array (サイズ12: 最大48バイト) @@ -17,16 +18,6 @@ const $entries4: GPUBindGroupEntry[] = [ { "binding": 3, "resource": null as unknown as GPUTextureView } ]; -/** - * @description 32bit整数からRGB値を抽出(プリマルチプライドアルファ) - */ -const intToRGBA = (color: number, alpha: number): [number, number, number, number] => { - const r = (color >> 16 & 0xFF) / 255 * alpha; - const g = (color >> 8 & 0xFF) / 255 * alpha; - const b = (color & 0xFF) / 255 * alpha; - return [r, g, b, alpha]; -}; - /** * @description パイプラインキャッシュ(キー: componentX,componentY,mode) */ @@ -37,57 +28,76 @@ const $pipelineCache = new Map { const { device, commandEncoder, frameBufferManager, textureManager } = config; - const width = sourceAttachment.width; - const height = sourceAttachment.height; + const width = source_attachment.width; + const height = source_attachment.height; // WebGL版と同じ: baseWidth/baseHeightはビットマップサイズを使用 - const baseWidth = bitmapWidth; - const baseHeight = bitmapHeight; + const baseWidth = bitmap_width; + const baseHeight = bitmap_height; // 出力アタッチメントを作成 const destAttachment = frameBufferManager.createTemporaryAttachment(width, height); // マップテクスチャを作成 const mapTexture = device.createTexture({ - "size": { "width": bitmapWidth, "height": bitmapHeight }, + "size": { "width": bitmap_width, "height": bitmap_height }, "format": "rgba8unorm", "usage": GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST }); device.queue.writeTexture( { "texture": mapTexture }, - bitmapBuffer.buffer, - { "bytesPerRow": bitmapWidth * 4, "offset": bitmapBuffer.byteOffset }, - { "width": bitmapWidth, "height": bitmapHeight } + bitmap_buffer.buffer, + { "bytesPerRow": bitmap_width * 4, "offset": bitmap_buffer.byteOffset }, + { "width": bitmap_width, "height": bitmap_height } ); // パイプラインをキャッシュから取得または作成 - const cacheKey = `${componentX},${componentY},${mode}`; + const cacheKey = `${component_x},${component_y},${mode}`; let cached = $pipelineCache.get(cacheKey); if (!cached) { const fragmentShaderCode = ShaderSource.getDisplacementMapFilterFragmentShader( - componentX, componentY, mode + component_x, component_y, mode ); const vertexShaderModule = device.createShaderModule({ @@ -171,16 +181,16 @@ export const execute = ( const uniformSize = needsSubstituteColor ? 48 : 32; // uvToStScale - $uniform12[0] = baseWidth / bitmapWidth; - $uniform12[1] = baseHeight / bitmapHeight; + $uniform12[0] = baseWidth / bitmap_width; + $uniform12[1] = baseHeight / bitmap_height; // uvToStOffset - $uniform12[2] = mapPointX / bitmapWidth; - $uniform12[3] = (baseHeight - bitmapHeight - mapPointY) / bitmapHeight; + $uniform12[2] = map_point_x / bitmap_width; + $uniform12[3] = (baseHeight - bitmap_height - map_point_y) / bitmap_height; // scale - $uniform12[4] = scaleX / baseWidth; - $uniform12[5] = scaleY / baseHeight; + $uniform12[4] = scale_x / baseWidth; + $uniform12[5] = scale_y / baseHeight; // padding $uniform12[6] = 0; @@ -188,7 +198,7 @@ export const execute = ( // substituteColor (mode === 1 の場合) if (needsSubstituteColor) { - const [r, g, b, a] = intToRGBA(color, alpha); + const [r, g, b, a] = intToPremultipliedRGBA(color, alpha); $uniform12[8] = r; $uniform12[9] = g; $uniform12[10] = b; @@ -208,7 +218,7 @@ export const execute = ( // バインドグループを作成 ($entries4[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries4[1].resource = sampler; - $entries4[2].resource = sourceAttachment.texture!.view; + $entries4[2].resource = source_attachment.texture!.view; $entries4[3].resource = mapTexture.createView(); const bindGroup = device.createBindGroup({ "layout": cached.bindGroupLayout, diff --git a/packages/webgpu/src/Filter/DisplacementMapFilterShader.test.ts b/packages/webgpu/src/Filter/DisplacementMapFilterShader.test.ts deleted file mode 100644 index 0400ad10..00000000 --- a/packages/webgpu/src/Filter/DisplacementMapFilterShader.test.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { getDisplacementMapFilterFragmentShader, getDisplacementMapFilterShaderKey } from "./DisplacementMapFilterShader"; - -describe("DisplacementMapFilterShader", () => -{ - describe("getDisplacementMapFilterFragmentShader", () => - { - it("should return a valid WGSL shader string", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("@vertex"); - }); - - it("should contain @fragment attribute", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("@fragment"); - }); - - it("should define DisplacementMapUniforms struct", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("struct DisplacementMapUniforms"); - }); - - it("should include uvToStScale uniform", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("uvToStScale: vec2"); - }); - - it("should include uvToStOffset uniform", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("uvToStOffset: vec2"); - }); - - it("should include scale uniform", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("scale: vec2"); - }); - - it("should include mapTexture binding", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("var mapTexture: texture_2d"); - }); - - it("should include sourceTexture binding", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("var sourceTexture: texture_2d"); - }); - - it("should include isInside helper function", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("fn isInside"); - }); - - // Component channel tests - it("should use mapColor.r for componentX = 1 (RED)", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("mapColor.r"); - }); - - it("should use mapColor.g for componentX = 2 (GREEN)", () => - { - const shader = getDisplacementMapFilterFragmentShader(2, 1, 0); - - expect(shader).toContain("vec2(mapColor.g, mapColor.r)"); - }); - - it("should use mapColor.b for componentX = 4 (BLUE)", () => - { - const shader = getDisplacementMapFilterFragmentShader(4, 1, 0); - - expect(shader).toContain("vec2(mapColor.b, mapColor.r)"); - }); - - it("should use mapColor.a for componentX = 8 (ALPHA)", () => - { - const shader = getDisplacementMapFilterFragmentShader(8, 1, 0); - - expect(shader).toContain("vec2(mapColor.a, mapColor.r)"); - }); - - it("should use 0.5 for unknown component value", () => - { - const shader = getDisplacementMapFilterFragmentShader(99, 99, 0); - - expect(shader).toContain("vec2(0.5, 0.5)"); - }); - - // Mode tests - it("should handle mode 0 (direct sampling)", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("let sourceColor = textureSample(sourceTexture, sourceSampler, uv)"); - expect(shader).not.toContain("substituteColor"); - }); - - it("should include substituteColor for mode 1", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 1); - - expect(shader).toContain("substituteColor: vec4"); - expect(shader).toContain("mix(substituteColor"); - }); - - it("should handle mode 2 (wrap/repeat)", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 2); - - expect(shader).toContain("fract(uv)"); - }); - - it("should handle mode 3 (axis fallback)", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 3); - - expect(shader).toContain("fallbackUv"); - }); - - it("should calculate offset from map color", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("let offset = vec2"); - expect(shader).toContain("- 0.5"); - }); - - it("should calculate displaced UV", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("let uv = input.texCoord + offset * scale"); - }); - - it("should mix original and displaced color based on map bounds", () => - { - const shader = getDisplacementMapFilterFragmentShader(1, 2, 0); - - expect(shader).toContain("mix(originalColor, sourceColor, isInside(st))"); - }); - }); - - describe("getDisplacementMapFilterShaderKey", () => - { - it("should generate unique key for component combination", () => - { - const key = getDisplacementMapFilterShaderKey(1, 2, 0); - - expect(key).toBe("displacement_1_2_0"); - }); - - it("should include all component and mode values", () => - { - const key = getDisplacementMapFilterShaderKey(4, 8, 2); - - expect(key).toBe("displacement_4_8_2"); - }); - - it("should generate different keys for different configurations", () => - { - const key1 = getDisplacementMapFilterShaderKey(1, 2, 0); - const key2 = getDisplacementMapFilterShaderKey(2, 1, 0); - const key3 = getDisplacementMapFilterShaderKey(1, 2, 1); - const key4 = getDisplacementMapFilterShaderKey(4, 8, 3); - - expect(key1).not.toBe(key2); - expect(key1).not.toBe(key3); - expect(key1).not.toBe(key4); - }); - - it("should include componentX in key", () => - { - const key = getDisplacementMapFilterShaderKey(4, 2, 0); - - expect(key).toContain("_4_"); - }); - - it("should include componentY in key", () => - { - const key = getDisplacementMapFilterShaderKey(1, 8, 0); - - expect(key).toContain("_8_"); - }); - - it("should include mode in key", () => - { - const key = getDisplacementMapFilterShaderKey(1, 2, 3); - - expect(key).toContain("_3"); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/DisplacementMapFilterShader.ts b/packages/webgpu/src/Filter/DisplacementMapFilterShader.ts deleted file mode 100644 index 3a3f51d3..00000000 --- a/packages/webgpu/src/Filter/DisplacementMapFilterShader.ts +++ /dev/null @@ -1,130 +0,0 @@ -const getComponentExpression = (component: number): string => { - switch (component) { - case 1: - return "mapColor.r"; - case 2: - return "mapColor.g"; - case 4: - return "mapColor.b"; - case 8: - return "mapColor.a"; - default: - return "0.5"; - } -}; - -const getModeStatement = (mode: number): string => { - switch (mode) { - case 0: - return ` - let sourceColor = textureSample(sourceTexture, sourceSampler, uv);`; - - case 1: - return ` - let substituteColor = uniforms.substituteColor; - let sourceColor = mix(substituteColor, textureSample(sourceTexture, sourceSampler, uv), isInside(uv));`; - - case 3: - return ` - let fallbackUv = mix(input.texCoord, uv, step(abs(uv - vec2(0.5)), vec2(0.5))); - let sourceColor = textureSample(sourceTexture, sourceSampler, fallbackUv);`; - - case 2: - default: - return ` - let sourceColor = textureSample(sourceTexture, sourceSampler, fract(uv));`; - } -}; - -export const getDisplacementMapFilterFragmentShader = ( - componentX: number, - componentY: number, - mode: number -): string => { - const cx = getComponentExpression(componentX); - const cy = getComponentExpression(componentY); - const modeStatement = getModeStatement(mode); - - const hasSubstituteColor = mode === 1; - - return ` -struct DisplacementMapUniforms { - uvToStScale: vec2, - uvToStOffset: vec2, - scale: vec2, - _pad: vec2, -${hasSubstituteColor ? " substituteColor: vec4," : ""} -} - -@group(0) @binding(0) var uniforms: DisplacementMapUniforms; -@group(0) @binding(1) var sourceSampler: sampler; -@group(0) @binding(2) var sourceTexture: texture_2d; -@group(0) @binding(3) var mapTexture: texture_2d; - -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, -} - -fn isInside(uv: vec2) -> f32 { - let inside = step(vec2(0.0), uv) * step(uv, vec2(1.0)); - return inside.x * inside.y; -} - -var input: VertexOutput; - -@vertex -fn vs_main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { - var positions = array, 6>( - vec2(-1.0, -1.0), - vec2(1.0, -1.0), - vec2(-1.0, 1.0), - vec2(-1.0, 1.0), - vec2(1.0, -1.0), - vec2(1.0, 1.0) - ); - - var texCoords = array, 6>( - vec2(0.0, 1.0), - vec2(1.0, 1.0), - vec2(0.0, 0.0), - vec2(0.0, 0.0), - vec2(1.0, 1.0), - vec2(1.0, 0.0) - ); - - var output: VertexOutput; - output.position = vec4(positions[vertexIndex], 0.0, 1.0); - output.texCoord = texCoords[vertexIndex]; - return output; -} - -@fragment -fn fs_main(fragInput: VertexOutput) -> @location(0) vec4 { - input = fragInput; - - let uvToStScale = uniforms.uvToStScale; - let uvToStOffset = uniforms.uvToStOffset; - let scale = uniforms.scale; - - let st = input.texCoord * uvToStScale - uvToStOffset; - let mapColor = textureSample(mapTexture, sourceSampler, st); - - let offset = vec2(${cx}, ${cy}) - 0.5; - let uv = input.texCoord + offset * scale; - - ${modeStatement} - - let originalColor = textureSample(sourceTexture, sourceSampler, input.texCoord); - return mix(originalColor, sourceColor, isInside(st)); -} -`; -}; - -export const getDisplacementMapFilterShaderKey = ( - componentX: number, - componentY: number, - mode: number -): string => { - return `displacement_${componentX}_${componentY}_${mode}`; -}; diff --git a/packages/webgpu/src/Filter/DropShadowFilter/FilterApplyDropShadowFilterUseCase.ts b/packages/webgpu/src/Filter/DropShadowFilter/FilterApplyDropShadowFilterUseCase.ts index b6a73dab..841c4054 100644 --- a/packages/webgpu/src/Filter/DropShadowFilter/FilterApplyDropShadowFilterUseCase.ts +++ b/packages/webgpu/src/Filter/DropShadowFilter/FilterApplyDropShadowFilterUseCase.ts @@ -1,13 +1,9 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject"; import type { IFilterConfig } from "../../interface/IFilterConfig"; import { $offset } from "../FilterOffset"; +import { DEG_TO_RAD, intToPremultipliedRGBA } from "../FilterUtil"; import { execute as filterApplyBlurFilterUseCase } from "../BlurFilter/FilterApplyBlurFilterUseCase"; -/** - * @description 度からラジアンへの変換係数 - */ -const DEG_TO_RAD: number = Math.PI / 180; - /** * @description プリアロケートされたFloat32Array (サイズ16) */ @@ -23,52 +19,42 @@ const $entries4: GPUBindGroupEntry[] = [ { "binding": 3, "resource": null as unknown as GPUTextureView } ]; -/** - * @description 32bit整数からRGB値を抽出(プリマルチプライドアルファ対応) - */ -const intToRGBA = (color: number, alpha: number): [number, number, number, number] => { - const r = (color >> 16 & 0xFF) / 255 * alpha; - const g = (color >> 8 & 0xFF) / 255 * alpha; - const b = (color & 0xFF) / 255 * alpha; - return [r, g, b, alpha]; -}; - /** * @description ドロップシャドウフィルターを適用 * Apply drop shadow filter * - * @param {IAttachmentObject} sourceAttachment - 入力テクスチャ + * @param {IAttachmentObject} source_attachment - 入力テクスチャ * @param {Float32Array} matrix - 変換行列 * @param {number} distance - シャドウの距離 * @param {number} angle - シャドウの角度(度) * @param {number} color - シャドウ色 (32bit整数) * @param {number} alpha - アルファ - * @param {number} blurX - X方向ブラー量 - * @param {number} blurY - Y方向ブラー量 + * @param {number} blur_x - X方向ブラー量 + * @param {number} blur_y - Y方向ブラー量 * @param {number} strength - シャドウ強度 * @param {number} quality - クオリティ * @param {boolean} inner - インナーシャドウ * @param {boolean} knockout - ノックアウトモード - * @param {boolean} hideObject - 元オブジェクトを隠す - * @param {number} devicePixelRatio - デバイスピクセル比 - * @param {IDropShadowConfig} config - WebGPUリソース設定 + * @param {boolean} hide_object - 元オブジェクトを隠す + * @param {number} device_pixel_ratio - デバイスピクセル比 + * @param {IFilterConfig} config - WebGPUリソース設定 * @return {IAttachmentObject} - フィルター適用後のアタッチメント */ export const execute = ( - sourceAttachment: IAttachmentObject, + source_attachment: IAttachmentObject, matrix: Float32Array, distance: number, angle: number, color: number, alpha: number, - blurX: number, - blurY: number, + blur_x: number, + blur_y: number, strength: number, quality: number, inner: boolean, knockout: boolean, - hideObject: boolean, - devicePixelRatio: number, + hide_object: boolean, + device_pixel_ratio: number, config: IFilterConfig ): IAttachmentObject => { @@ -77,14 +63,14 @@ export const execute = ( // 元のオフセットを保存 const baseOffsetX = $offset.x; const baseOffsetY = $offset.y; - const baseWidth = sourceAttachment.width; - const baseHeight = sourceAttachment.height; + const baseWidth = source_attachment.width; + const baseHeight = source_attachment.height; // ブラーフィルターを適用 const blurAttachment = filterApplyBlurFilterUseCase( - sourceAttachment, matrix, - blurX, blurY, quality, - devicePixelRatio, config + source_attachment, matrix, + blur_x, blur_y, quality, + device_pixel_ratio, config ); const blurWidth = blurAttachment.width; @@ -101,8 +87,8 @@ export const execute = ( // シャドウのオフセットを計算 const radian = angle * DEG_TO_RAD; - const shadowX = Math.cos(radian) * distance * (xScale / devicePixelRatio); - const shadowY = Math.sin(radian) * distance * (yScale / devicePixelRatio); + const shadowX = Math.cos(radian) * distance * (xScale / device_pixel_ratio); + const shadowY = Math.sin(radian) * distance * (yScale / device_pixel_ratio); // 出力キャンバスのサイズを計算 const w = inner ? baseWidth : blurWidth + Math.max(0, Math.abs(shadowX) - offsetDiffX); @@ -124,11 +110,11 @@ export const execute = ( // タイプとノックアウト状態を決定 const isInner = inner; let isKnockout = knockout; - let isHideObject = hideObject; + let isHideObject = hide_object; if (inner) { - isKnockout = knockout || hideObject; - } else if (!knockout && hideObject) { + isKnockout = knockout || hide_object; + } else if (!knockout && hide_object) { // フルモード(シャドウのみ表示) isKnockout = true; isHideObject = true; @@ -144,7 +130,7 @@ export const execute = ( if (!pipeline || !bindGroupLayout) { console.error("[WebGPU DropShadowFilter] Pipeline not found"); frameBufferManager.releaseTemporaryAttachment(blurAttachment); - return sourceAttachment; + return source_attachment; } // サンプラーを作成 @@ -161,7 +147,7 @@ export const execute = ( // knockout: f32 (4 bytes) // hideObject: f32 (4 bytes) // Total: 64 bytes - const [r, g, b, a] = intToRGBA(color, alpha); + const [r, g, b, a] = intToPremultipliedRGBA(color, alpha); // WebGL版と同じUV変換方式: // uv = texCoord * scale - offset @@ -208,7 +194,7 @@ export const execute = ( ($entries4[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries4[1].resource = sampler; $entries4[2].resource = blurAttachment.texture!.view; - $entries4[3].resource = sourceAttachment.texture!.view; + $entries4[3].resource = source_attachment.texture!.view; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries4 diff --git a/packages/webgpu/src/Filter/DropShadowFilterShader.test.ts b/packages/webgpu/src/Filter/DropShadowFilterShader.test.ts deleted file mode 100644 index 9604fb9b..00000000 --- a/packages/webgpu/src/Filter/DropShadowFilterShader.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { DropShadowFilterShader } from "./DropShadowFilterShader"; - -describe("DropShadowFilterShader", () => -{ - describe("getVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = DropShadowFilterShader.getVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = DropShadowFilterShader.getVertexShader(); - - expect(shader).toContain("@vertex"); - }); - - it("should define VertexInput and VertexOutput structs", () => - { - const shader = DropShadowFilterShader.getVertexShader(); - - expect(shader).toContain("struct VertexInput"); - expect(shader).toContain("struct VertexOutput"); - }); - }); - - describe("getFragmentShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = DropShadowFilterShader.getFragmentShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = DropShadowFilterShader.getFragmentShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should define DropShadowUniforms struct", () => - { - const shader = DropShadowFilterShader.getFragmentShader(); - - expect(shader).toContain("struct DropShadowUniforms"); - }); - - it("should include shadow parameters", () => - { - const shader = DropShadowFilterShader.getFragmentShader(); - - expect(shader).toContain("shadowColor"); - expect(shader).toContain("distance"); - expect(shader).toContain("angle"); - expect(shader).toContain("strength"); - }); - - it("should include inner shadow option", () => - { - const shader = DropShadowFilterShader.getFragmentShader(); - - expect(shader).toContain("inner"); - }); - - it("should include knockout option", () => - { - const shader = DropShadowFilterShader.getFragmentShader(); - - expect(shader).toContain("knockout"); - }); - - it("should include hideObject option", () => - { - const shader = DropShadowFilterShader.getFragmentShader(); - - expect(shader).toContain("hideObject"); - }); - - it("should calculate shadow offset using angle", () => - { - const shader = DropShadowFilterShader.getFragmentShader(); - - expect(shader).toContain("cos(radian)"); - expect(shader).toContain("sin(radian)"); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/DropShadowFilterShader.ts b/packages/webgpu/src/Filter/DropShadowFilterShader.ts deleted file mode 100644 index 14160fa7..00000000 --- a/packages/webgpu/src/Filter/DropShadowFilterShader.ts +++ /dev/null @@ -1,97 +0,0 @@ -export class DropShadowFilterShader -{ - static getFragmentShader(): string - { - return /* wgsl */` - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - struct DropShadowUniforms { - shadowColor: vec4, - offset: vec2, - distance: f32, - angle: f32, - strength: f32, - inner: f32, - knockout: f32, - hideObject: f32, - } - - @group(0) @binding(0) var uniforms: DropShadowUniforms; - @group(0) @binding(1) var textureSampler: sampler; - @group(0) @binding(2) var textureData: texture_2d; - - @fragment - fn main(input: VertexOutput) -> @location(0) vec4 { - var originalColor = textureSample(textureData, textureSampler, input.texCoord); - - let radian = uniforms.angle * 3.14159265 / 180.0; - let offsetX = cos(radian) * uniforms.distance / 100.0; - let offsetY = sin(radian) * uniforms.distance / 100.0; - - let shadowCoord = vec2( - input.texCoord.x + offsetX, - input.texCoord.y + offsetY - ); - - var shadowAlpha = textureSample(textureData, textureSampler, shadowCoord).a; - - var shadowColor = vec4( - uniforms.shadowColor.rgb, - shadowAlpha * uniforms.shadowColor.a * uniforms.strength - ); - - if (uniforms.inner > 0.5) { - let alpha = originalColor.a; - shadowColor.a *= alpha; - - if (uniforms.knockout > 0.5) { - return shadowColor; - } else { - return mix(shadowColor, originalColor, alpha); - } - } else { - if (uniforms.hideObject > 0.5) { - return shadowColor * (1.0 - originalColor.a); - } else if (uniforms.knockout > 0.5) { - return shadowColor; - } else { - let combinedAlpha = originalColor.a + shadowColor.a * (1.0 - originalColor.a); - if (combinedAlpha > 0.0) { - let rgb = (originalColor.rgb * originalColor.a + - shadowColor.rgb * shadowColor.a * (1.0 - originalColor.a)) / combinedAlpha; - return vec4(rgb, combinedAlpha); - } else { - return vec4(0.0); - } - } - } - } - `; - } - - static getVertexShader(): string - { - return /* wgsl */` - struct VertexInput { - @location(0) position: vec2, - @location(1) texCoord: vec2, - } - - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - @vertex - fn main(input: VertexInput) -> VertexOutput { - var output: VertexOutput; - output.position = vec4(input.position, 0.0, 1.0); - output.texCoord = input.texCoord; - return output; - } - `; - } -} diff --git a/packages/webgpu/src/Filter/FilterGradientLUTCache.ts b/packages/webgpu/src/Filter/FilterGradientLUTCache.ts index f2fd19d7..1722dfc2 100644 --- a/packages/webgpu/src/Filter/FilterGradientLUTCache.ts +++ b/packages/webgpu/src/Filter/FilterGradientLUTCache.ts @@ -15,6 +15,8 @@ let $filterGradientAttachment: IAttachmentObject | null = null; /** * @description GPUDeviceの参照 + * Reference to GPUDevice + * @type {GPUDevice | null} * @private */ let $device: GPUDevice | null = null; diff --git a/packages/webgpu/src/Filter/FilterUtil.ts b/packages/webgpu/src/Filter/FilterUtil.ts new file mode 100644 index 00000000..7dc5995d --- /dev/null +++ b/packages/webgpu/src/Filter/FilterUtil.ts @@ -0,0 +1,35 @@ +/** + * @description 度数法からラジアンへの変換係数 + * Conversion factor from degrees to radians + */ +export const DEG_TO_RAD: number = Math.PI / 180; + +/** + * @description 32bit整数カラーからプリマルチプライドアルファRGBA値を抽出 + * Extract premultiplied alpha RGBA values from 32bit integer color + * + * @param {number} color - 32bit整数カラー値 + * @param {number} alpha - アルファ値 (0-1) + * @return {[number, number, number, number]} - [r, g, b, a] (プリマルチプライドアルファ) + */ +export const intToPremultipliedRGBA = (color: number, alpha: number): [number, number, number, number] => { + const r = (color >> 16 & 0xFF) / 255 * alpha; + const g = (color >> 8 & 0xFF) / 255 * alpha; + const b = (color & 0xFF) / 255 * alpha; + return [r, g, b, alpha]; +}; + +/** + * @description 32bit整数カラーからストレートRGBA値を抽出 + * Extract straight (non-premultiplied) RGBA values from 32bit integer color + * + * @param {number} color - 32bit整数カラー値 + * @param {number} alpha - アルファ値 (0-1) + * @return {[number, number, number, number]} - [r, g, b, a] (ストレート) + */ +export const intToStraightRGBA = (color: number, alpha: number): [number, number, number, number] => { + const r = (color >> 16 & 0xFF) / 255; + const g = (color >> 8 & 0xFF) / 255; + const b = (color & 0xFF) / 255; + return [r, g, b, alpha]; +}; diff --git a/packages/webgpu/src/Filter/GlowFilter/FilterApplyGlowFilterUseCase.ts b/packages/webgpu/src/Filter/GlowFilter/FilterApplyGlowFilterUseCase.ts index fb27e5f6..4120f9eb 100644 --- a/packages/webgpu/src/Filter/GlowFilter/FilterApplyGlowFilterUseCase.ts +++ b/packages/webgpu/src/Filter/GlowFilter/FilterApplyGlowFilterUseCase.ts @@ -1,6 +1,7 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject"; import type { IFilterConfig } from "../../interface/IFilterConfig"; import { $offset } from "../FilterOffset"; +import { intToPremultipliedRGBA } from "../FilterUtil"; import { execute as filterApplyBlurFilterUseCase } from "../BlurFilter/FilterApplyBlurFilterUseCase"; /** @@ -18,16 +19,6 @@ const $entries4: GPUBindGroupEntry[] = [ { "binding": 3, "resource": null as unknown as GPUTextureView } ]; -/** - * @description 32bit整数からRGB値を抽出(プリマルチプライドアルファ対応) - */ -const intToRGBA = (color: number, alpha: number): [number, number, number, number] => { - const r = (color >> 16 & 0xFF) / 255 * alpha; - const g = (color >> 8 & 0xFF) / 255 * alpha; - const b = (color & 0xFF) / 255 * alpha; - return [r, g, b, alpha]; -}; - /** * @description グローフィルターを適用 * Apply glow filter @@ -35,32 +26,32 @@ const intToRGBA = (color: number, alpha: number): [number, number, number, numbe * UV変換方式で元テクスチャとブラーテクスチャを直接サンプリング。 * copyTextureToTextureと一時テクスチャを使用しない最適化版。 * - * @param {IAttachmentObject} sourceAttachment - 入力テクスチャ + * @param {IAttachmentObject} source_attachment - 入力テクスチャ * @param {Float32Array} matrix - 変換行列 * @param {number} color - グロー色 (32bit整数) * @param {number} alpha - アルファ - * @param {number} blurX - X方向ブラー量 - * @param {number} blurY - Y方向ブラー量 + * @param {number} blur_x - X方向ブラー量 + * @param {number} blur_y - Y方向ブラー量 * @param {number} strength - グロー強度 * @param {number} quality - クオリティ * @param {boolean} inner - インナーグロー * @param {boolean} knockout - ノックアウトモード - * @param {number} devicePixelRatio - デバイスピクセル比 + * @param {number} device_pixel_ratio - デバイスピクセル比 * @param {IFilterConfig} config - WebGPUリソース設定 * @return {IAttachmentObject} - フィルター適用後のアタッチメント */ export const execute = ( - sourceAttachment: IAttachmentObject, + source_attachment: IAttachmentObject, matrix: Float32Array, color: number, alpha: number, - blurX: number, - blurY: number, + blur_x: number, + blur_y: number, strength: number, quality: number, inner: boolean, knockout: boolean, - devicePixelRatio: number, + device_pixel_ratio: number, config: IFilterConfig ): IAttachmentObject => { @@ -69,14 +60,13 @@ export const execute = ( // 元のオフセットを保存 const baseOffsetX = $offset.x; const baseOffsetY = $offset.y; - const baseWidth = sourceAttachment.width; - const baseHeight = sourceAttachment.height; + const baseWidth = source_attachment.width; + const baseHeight = source_attachment.height; - // ブラーフィルターを適用(元テクスチャを保持) const blurAttachment = filterApplyBlurFilterUseCase( - sourceAttachment, matrix, - blurX, blurY, quality, - devicePixelRatio, config + source_attachment, matrix, + blur_x, blur_y, quality, + device_pixel_ratio, config ); const blurWidth = blurAttachment.width; @@ -120,7 +110,7 @@ export const execute = ( if (!pipeline || !bindGroupLayout) { console.error("[WebGPU GlowFilter] Pipeline not found"); frameBufferManager.releaseTemporaryAttachment(blurAttachment); - return sourceAttachment; + return source_attachment; } // サンプラーを作成 @@ -132,7 +122,7 @@ export const execute = ( // blurScale: vec2, blurOffset: vec2 (16 bytes) // strength: f32, inner: f32, knockout: f32, _padding: f32 (16 bytes) // Total: 64 bytes - const [r, g, b, a] = intToRGBA(color, alpha); + const [r, g, b, a] = intToPremultipliedRGBA(color, alpha); $uniform16[0] = r; $uniform16[1] = g; $uniform16[2] = b; @@ -164,7 +154,7 @@ export const execute = ( ($entries4[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries4[1].resource = sampler; $entries4[2].resource = blurAttachment.texture!.view; - $entries4[3].resource = sourceAttachment.texture!.view; + $entries4[3].resource = source_attachment.texture!.view; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, "entries": $entries4 diff --git a/packages/webgpu/src/Filter/GlowFilterShader.test.ts b/packages/webgpu/src/Filter/GlowFilterShader.test.ts deleted file mode 100644 index 92067859..00000000 --- a/packages/webgpu/src/Filter/GlowFilterShader.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { GlowFilterShader } from "./GlowFilterShader"; - -describe("GlowFilterShader", () => -{ - describe("getVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = GlowFilterShader.getVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = GlowFilterShader.getVertexShader(); - - expect(shader).toContain("@vertex"); - }); - - it("should define VertexInput struct", () => - { - const shader = GlowFilterShader.getVertexShader(); - - expect(shader).toContain("struct VertexInput"); - }); - - it("should define VertexOutput struct", () => - { - const shader = GlowFilterShader.getVertexShader(); - - expect(shader).toContain("struct VertexOutput"); - }); - }); - - describe("getFragmentShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = GlowFilterShader.getFragmentShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = GlowFilterShader.getFragmentShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should define GlowUniforms struct", () => - { - const shader = GlowFilterShader.getFragmentShader(); - - expect(shader).toContain("struct GlowUniforms"); - }); - - it("should include glow parameters", () => - { - const shader = GlowFilterShader.getFragmentShader(); - - expect(shader).toContain("glowColor"); - expect(shader).toContain("strength"); - expect(shader).toContain("inner"); - expect(shader).toContain("knockout"); - }); - - it("should handle inner glow mode", () => - { - const shader = GlowFilterShader.getFragmentShader(); - - expect(shader).toContain("uniforms.inner"); - }); - - it("should handle knockout mode", () => - { - const shader = GlowFilterShader.getFragmentShader(); - - expect(shader).toContain("uniforms.knockout"); - }); - - it("should include texture sampling", () => - { - const shader = GlowFilterShader.getFragmentShader(); - - expect(shader).toContain("textureSample"); - }); - }); -}); diff --git a/packages/webgpu/src/Filter/GlowFilterShader.ts b/packages/webgpu/src/Filter/GlowFilterShader.ts deleted file mode 100644 index 6a19f62a..00000000 --- a/packages/webgpu/src/Filter/GlowFilterShader.ts +++ /dev/null @@ -1,70 +0,0 @@ -export class GlowFilterShader -{ - static getFragmentShader(): string - { - return /* wgsl */` - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - struct GlowUniforms { - glowColor: vec4, - strength: f32, - inner: f32, - knockout: f32, - _padding: f32, - } - - @group(0) @binding(0) var uniforms: GlowUniforms; - @group(0) @binding(1) var textureSampler: sampler; - @group(0) @binding(2) var textureData: texture_2d; - - @fragment - fn main(input: VertexOutput) -> @location(0) vec4 { - var originalColor = textureSample(textureData, textureSampler, input.texCoord); - - let alpha = originalColor.a; - - var glowColor = uniforms.glowColor * uniforms.strength * alpha; - - if (uniforms.inner > 0.5) { - if (uniforms.knockout > 0.5) { - return glowColor; - } else { - return mix(originalColor, glowColor, alpha); - } - } else { - if (uniforms.knockout > 0.5) { - return vec4(glowColor.rgb, glowColor.a * (1.0 - alpha)); - } else { - return originalColor + glowColor; - } - } - } - `; - } - - static getVertexShader(): string - { - return /* wgsl */` - struct VertexInput { - @location(0) position: vec2, - @location(1) texCoord: vec2, - } - - struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, - } - - @vertex - fn main(input: VertexInput) -> VertexOutput { - var output: VertexOutput; - output.position = vec4(input.position, 0.0, 1.0); - output.texCoord = input.texCoord; - return output; - } - `; - } -} diff --git a/packages/webgpu/src/Filter/GradientBevelFilter/FilterApplyGradientBevelFilterUseCase.ts b/packages/webgpu/src/Filter/GradientBevelFilter/FilterApplyGradientBevelFilterUseCase.ts index 4096a0b5..a7776e85 100644 --- a/packages/webgpu/src/Filter/GradientBevelFilter/FilterApplyGradientBevelFilterUseCase.ts +++ b/packages/webgpu/src/Filter/GradientBevelFilter/FilterApplyGradientBevelFilterUseCase.ts @@ -3,11 +3,7 @@ import type { IFilterConfig } from "../../interface/IFilterConfig"; import { $offset } from "../FilterOffset"; import { execute as filterApplyBlurFilterUseCase } from "../BlurFilter/FilterApplyBlurFilterUseCase"; import { generateFilterGradientLUT } from "../../Gradient/GradientLUTGenerator"; - -/** - * @description 度からラジアンへの変換係数 - */ -const DEG_TO_RAD: number = Math.PI / 180; +import { DEG_TO_RAD } from "../FilterUtil"; /** * @description プリアロケートされたFloat32Array @@ -44,38 +40,38 @@ const $entries5: GPUBindGroupEntry[] = [ * 2. ベベルベースにブラー適用 * 3. UV変換方式で最終合成(isInsideでハード境界クリッピング) * - * @param {IAttachmentObject} sourceAttachment - 入力テクスチャ + * @param {IAttachmentObject} source_attachment - 入力テクスチャ * @param {Float32Array} matrix - 変換行列 * @param {number} distance - ベベルの距離 * @param {number} angle - ベベルの角度(度) * @param {Float32Array} colors - 色配列 * @param {Float32Array} alphas - アルファ配列 * @param {Float32Array} ratios - 比率配列 - * @param {number} blurX - X方向ブラー量 - * @param {number} blurY - Y方向ブラー量 + * @param {number} blur_x - X方向ブラー量 + * @param {number} blur_y - Y方向ブラー量 * @param {number} strength - ベベル強度 * @param {number} quality - クオリティ * @param {number} type - タイプ (0: full, 1: inner, 2: outer) * @param {boolean} knockout - ノックアウトモード - * @param {number} devicePixelRatio - デバイスピクセル比 - * @param {IGradientBevelConfig} config - WebGPUリソース設定 + * @param {number} device_pixel_ratio - デバイスピクセル比 + * @param {IFilterConfig} config - WebGPUリソース設定 * @return {IAttachmentObject} - フィルター適用後のアタッチメント */ export const execute = ( - sourceAttachment: IAttachmentObject, + source_attachment: IAttachmentObject, matrix: Float32Array, distance: number, angle: number, colors: Float32Array, alphas: Float32Array, ratios: Float32Array, - blurX: number, - blurY: number, + blur_x: number, + blur_y: number, strength: number, quality: number, type: number, knockout: boolean, - devicePixelRatio: number, + device_pixel_ratio: number, config: IFilterConfig ): IAttachmentObject => { @@ -84,8 +80,8 @@ export const execute = ( // 元のオフセットを保存 const baseOffsetX = $offset.x; const baseOffsetY = $offset.y; - const baseWidth = sourceAttachment.width; - const baseHeight = sourceAttachment.height; + const baseWidth = source_attachment.width; + const baseHeight = source_attachment.height; // 変換行列からスケールを取得 const xScale = Math.sqrt(matrix[0] * matrix[0] + matrix[1] * matrix[1]); @@ -93,8 +89,8 @@ export const execute = ( // ベベルのオフセットを計算 const radian = angle * DEG_TO_RAD; - const x = Math.cos(radian) * distance * (xScale / devicePixelRatio); - const y = Math.sin(radian) * distance * (yScale / devicePixelRatio); + const x = Math.cos(radian) * distance * (xScale / device_pixel_ratio); + const y = Math.sin(radian) * distance * (yScale / device_pixel_ratio); // ===== Step 1: ベベルベーステクスチャ作成 ===== // WebGL版と同じ: original * (1 - shifted_original.a) @@ -104,7 +100,7 @@ export const execute = ( if (!bevelBasePipeline || !bevelBaseLayout) { console.error("[WebGPU GradientBevelFilter] bevel_base pipeline not found"); - return sourceAttachment; + return source_attachment; } const bevelBaseAttachment = frameBufferManager.createTemporaryAttachment(baseWidth, baseHeight); @@ -128,7 +124,7 @@ export const execute = ( ($entries3[0].resource as GPUBufferBinding).buffer = bevelBaseUniformBuffer; $entries3[1].resource = bevelBaseSampler; - $entries3[2].resource = sourceAttachment.texture!.view; + $entries3[2].resource = source_attachment.texture!.view; const bevelBaseBindGroup = device.createBindGroup({ "layout": bevelBaseLayout, "entries": $entries3 @@ -148,8 +144,8 @@ export const execute = ( // WebGL版と同じ: bevelBaseをブラーする(元テクスチャではなく) const blurAttachment = filterApplyBlurFilterUseCase( bevelBaseAttachment, matrix, - blurX, blurY, quality, - devicePixelRatio, config + blur_x, blur_y, quality, + device_pixel_ratio, config ); // ベベルベースは不要になったので解放 @@ -223,7 +219,7 @@ export const execute = ( if (!pipeline || !bindGroupLayout) { console.error("[WebGPU GradientBevelFilter] Pipeline not found"); frameBufferManager.releaseTemporaryAttachment(blurAttachment); - return sourceAttachment; + return source_attachment; } const sampler = textureManager.createSampler("gradient_bevel_sampler", true); @@ -256,7 +252,7 @@ export const execute = ( ($entries5[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries5[1].resource = sampler; $entries5[2].resource = blurAttachment.texture!.view; - $entries5[3].resource = sourceAttachment.texture!.view; + $entries5[3].resource = source_attachment.texture!.view; $entries5[4].resource = lutView; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, diff --git a/packages/webgpu/src/Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase.ts b/packages/webgpu/src/Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase.ts index 58512293..521a1bf4 100644 --- a/packages/webgpu/src/Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase.ts +++ b/packages/webgpu/src/Filter/GradientGlowFilter/FilterApplyGradientGlowFilterUseCase.ts @@ -3,11 +3,7 @@ import type { IFilterConfig } from "../../interface/IFilterConfig"; import { $offset } from "../FilterOffset"; import { execute as filterApplyBlurFilterUseCase } from "../BlurFilter/FilterApplyBlurFilterUseCase"; import { generateFilterGradientLUT } from "../../Gradient/GradientLUTGenerator"; - -/** - * @description 度からラジアンへの変換係数 - */ -const DEG_TO_RAD: number = Math.PI / 180; +import { DEG_TO_RAD } from "../FilterUtil"; /** * @description プリアロケートされたFloat32Array (サイズ12) @@ -34,38 +30,38 @@ const $entries5: GPUBindGroupEntry[] = [ * 2. グラデーションLUT生成(専用テクスチャ) * 3. UV変換方式で最終合成(isInsideでハード境界クリッピング) * - * @param {IAttachmentObject} sourceAttachment - 入力テクスチャ + * @param {IAttachmentObject} source_attachment - 入力テクスチャ * @param {Float32Array} matrix - 変換行列 * @param {number} distance - グローの距離 * @param {number} angle - グローの角度(度) * @param {Float32Array} colors - 色配列 * @param {Float32Array} alphas - アルファ配列 * @param {Float32Array} ratios - 比率配列 - * @param {number} blurX - X方向ブラー量 - * @param {number} blurY - Y方向ブラー量 + * @param {number} blur_x - X方向ブラー量 + * @param {number} blur_y - Y方向ブラー量 * @param {number} strength - グロー強度 * @param {number} quality - クオリティ * @param {number} type - タイプ (0: full, 1: inner, 2: outer) * @param {boolean} knockout - ノックアウトモード - * @param {number} devicePixelRatio - デバイスピクセル比 + * @param {number} device_pixel_ratio - デバイスピクセル比 * @param {IFilterConfig} config - WebGPUリソース設定 * @return {IAttachmentObject} - フィルター適用後のアタッチメント */ export const execute = ( - sourceAttachment: IAttachmentObject, + source_attachment: IAttachmentObject, matrix: Float32Array, distance: number, angle: number, colors: Float32Array, alphas: Float32Array, ratios: Float32Array, - blurX: number, - blurY: number, + blur_x: number, + blur_y: number, strength: number, quality: number, type: number, knockout: boolean, - devicePixelRatio: number, + device_pixel_ratio: number, config: IFilterConfig ): IAttachmentObject => { @@ -74,14 +70,14 @@ export const execute = ( // 元のオフセットを保存 const baseOffsetX = $offset.x; const baseOffsetY = $offset.y; - const baseWidth = sourceAttachment.width; - const baseHeight = sourceAttachment.height; + const baseWidth = source_attachment.width; + const baseHeight = source_attachment.height; // ブラーフィルターを適用 const blurAttachment = filterApplyBlurFilterUseCase( - sourceAttachment, matrix, - blurX, blurY, quality, - devicePixelRatio, config + source_attachment, matrix, + blur_x, blur_y, quality, + device_pixel_ratio, config ); const blurWidth = blurAttachment.width; @@ -98,8 +94,8 @@ export const execute = ( // グローのオフセットを計算 const radian = angle * DEG_TO_RAD; - const x = Math.cos(radian) * distance * (xScale / devicePixelRatio); - const y = Math.sin(radian) * distance * (yScale / devicePixelRatio); + const x = Math.cos(radian) * distance * (xScale / device_pixel_ratio); + const y = Math.sin(radian) * distance * (yScale / device_pixel_ratio); // ===== WebGL版と同じサイズ・位置計算 ===== const isInner = type === 1; @@ -158,7 +154,7 @@ export const execute = ( if (!pipeline || !bindGroupLayout) { console.error("[WebGPU GradientGlowFilter] Pipeline not found"); frameBufferManager.releaseTemporaryAttachment(blurAttachment); - return sourceAttachment; + return source_attachment; } const sampler = textureManager.createSampler("gradient_glow_sampler", true); @@ -191,7 +187,7 @@ export const execute = ( ($entries5[0].resource as GPUBufferBinding).buffer = uniformBuffer; $entries5[1].resource = sampler; $entries5[2].resource = blurAttachment.texture!.view; - $entries5[3].resource = sourceAttachment.texture!.view; + $entries5[3].resource = source_attachment.texture!.view; $entries5[4].resource = lutView; const bindGroup = device.createBindGroup({ "layout": bindGroupLayout, diff --git a/packages/webgpu/src/FrameBufferManager.test.ts b/packages/webgpu/src/FrameBufferManager.test.ts index c621d6cd..f5cccf76 100644 --- a/packages/webgpu/src/FrameBufferManager.test.ts +++ b/packages/webgpu/src/FrameBufferManager.test.ts @@ -356,22 +356,6 @@ describe("FrameBufferManager", () => }); }); - describe("resizeAttachment", () => - { - it("should destroy old and create new attachment", () => - { - const device = createMockDevice(); - const manager = new FrameBufferManager(device, "bgra8unorm"); - const oldAttachment = manager.createAttachment("test", 100, 100); - - const newAttachment = manager.resizeAttachment("test", 200, 200); - - expect(oldAttachment.texture!.resource.destroy).toHaveBeenCalled(); - expect(newAttachment.width).toBe(200); - expect(newAttachment.height).toBe(200); - }); - }); - describe("createTemporaryAttachment", () => { it("should create temporary attachment", () => diff --git a/packages/webgpu/src/FrameBufferManager.ts b/packages/webgpu/src/FrameBufferManager.ts index 632edbb4..f99bf029 100644 --- a/packages/webgpu/src/FrameBufferManager.ts +++ b/packages/webgpu/src/FrameBufferManager.ts @@ -6,6 +6,10 @@ import { execute as frameBufferManagerCreateRenderPassDescriptorService } from " import { execute as frameBufferManagerCreateStencilRenderPassDescriptorService } from "./FrameBufferManager/service/FrameBufferManagerCreateStencilRenderPassDescriptorService"; import { TexturePool } from "./TexturePool"; +/** + * @description フレームバッファとアタッチメントの管理クラス + * Manager class for frame buffers and attachments + */ export class FrameBufferManager { private device: GPUDevice; @@ -16,6 +20,12 @@ export class FrameBufferManager private texturePool: TexturePool; private pendingReleases: IAttachmentObject[] = []; + /** + * @description FrameBufferManagerのコンストラクタ + * Constructor for FrameBufferManager + * @param {GPUDevice} device - WebGPUデバイス / WebGPU device + * @param {GPUTextureFormat} format - テクスチャフォーマット / Texture format + */ constructor(device: GPUDevice, format: GPUTextureFormat) { this.device = device; @@ -26,11 +36,26 @@ export class FrameBufferManager this.texturePool = new TexturePool(device); } + /** + * @description フレームの開始処理を行う + * Begin a new frame + * @return {void} + */ beginFrame(): void { this.texturePool.beginFrame(); } + /** + * @description 新しいアタッチメントを作成する + * Create a new attachment + * @param {string} name - アタッチメント名 / Attachment name + * @param {number} width - テクスチャの幅 / Texture width + * @param {number} height - テクスチャの高さ / Texture height + * @param {boolean} [msaa=false] - MSAAを有効にするか / Whether to enable MSAA + * @param {boolean} [mask=false] - マスクを有効にするか / Whether to enable mask + * @return {IAttachmentObject} + */ createAttachment( name: string, width: number, @@ -52,49 +77,94 @@ export class FrameBufferManager ); } + /** + * @description 名前でアタッチメントを取得する + * Get an attachment by name + * @param {string} name - アタッチメント名 / Attachment name + * @return {IAttachmentObject | undefined} + */ getAttachment(name: string): IAttachmentObject | undefined { return this.attachments.get(name); } + /** + * @description 現在のアタッチメントを設定する + * Set the current attachment + * @param {IAttachmentObject | null} attachment - 設定するアタッチメント / Attachment to set + * @return {void} + */ setCurrentAttachment(attachment: IAttachmentObject | null): void { this.currentAttachment = attachment; } + /** + * @description 現在のアタッチメントを取得する + * Get the current attachment + * @return {IAttachmentObject | null} + */ getCurrentAttachment(): IAttachmentObject | null { return this.currentAttachment; } + /** + * @description レンダーパスディスクリプタを作成する + * Create a render pass descriptor + * @param {GPUTextureView} view - カラーテクスチャビュー / Color texture view + * @param {number} [r=0] - クリアカラーR値 / Clear color R value + * @param {number} [g=0] - クリアカラーG値 / Clear color G value + * @param {number} [b=0] - クリアカラーB値 / Clear color B value + * @param {number} [a=0] - クリアカラーA値 / Clear color A value + * @param {GPULoadOp} [load_op="clear"] - ロードオペレーション / Load operation + * @param {GPUTextureView|null} [resolve_target=null] - MSAAリゾルブターゲット / MSAA resolve target + * @return {GPURenderPassDescriptor} + */ createRenderPassDescriptor( view: GPUTextureView, r: number = 0, g: number = 0, b: number = 0, a: number = 0, - loadOp: GPULoadOp = "clear", - resolveTarget: GPUTextureView | null = null + load_op: GPULoadOp = "clear", + resolve_target: GPUTextureView | null = null ): GPURenderPassDescriptor { - return frameBufferManagerCreateRenderPassDescriptorService(view, r, g, b, a, loadOp, resolveTarget); + return frameBufferManagerCreateRenderPassDescriptorService(view, r, g, b, a, load_op, resolve_target); } + /** + * @description ステンシル付きレンダーパスディスクリプタを作成する + * Create a render pass descriptor with stencil + * @param {GPUTextureView} color_view - カラーテクスチャビュー / Color texture view + * @param {GPUTextureView} stencil_view - ステンシルテクスチャビュー / Stencil texture view + * @param {GPULoadOp} [color_load_op="load"] - カラーロードオペレーション / Color load operation + * @param {GPULoadOp} [stencil_load_op="clear"] - ステンシルロードオペレーション / Stencil load operation + * @param {GPUTextureView|null} [resolve_target=null] - MSAAリゾルブターゲット / MSAA resolve target + * @return {GPURenderPassDescriptor} + */ createStencilRenderPassDescriptor( - colorView: GPUTextureView, - stencilView: GPUTextureView, - colorLoadOp: GPULoadOp = "load", - stencilLoadOp: GPULoadOp = "clear", - resolveTarget: GPUTextureView | null = null + color_view: GPUTextureView, + stencil_view: GPUTextureView, + color_load_op: GPULoadOp = "load", + stencil_load_op: GPULoadOp = "clear", + resolve_target: GPUTextureView | null = null ): GPURenderPassDescriptor { return frameBufferManagerCreateStencilRenderPassDescriptorService( - colorView, - stencilView, - colorLoadOp, - stencilLoadOp, - resolveTarget + color_view, + stencil_view, + color_load_op, + stencil_load_op, + resolve_target ); } + /** + * @description アタッチメントを破棄する + * Destroy an attachment by name + * @param {string} name - アタッチメント名 / Attachment name + * @return {void} + */ destroyAttachment(name: string): void { const attachment = this.attachments.get(name); @@ -109,12 +179,13 @@ export class FrameBufferManager } } - resizeAttachment(name: string, width: number, height: number): IAttachmentObject - { - this.destroyAttachment(name); - return this.createAttachment(name, width, height); - } - + /** + * @description 一時的なアタッチメントを作成する + * Create a temporary attachment + * @param {number} width - テクスチャの幅 / Texture width + * @param {number} height - テクスチャの高さ / Texture height + * @return {IAttachmentObject} + */ createTemporaryAttachment(width: number, height: number): IAttachmentObject { const name = `temp_${this.idCounter.nextId}`; @@ -155,6 +226,12 @@ export class FrameBufferManager return attachment; } + /** + * @description 一時アタッチメントをリリースキューに追加する + * Add a temporary attachment to the release queue + * @param {IAttachmentObject} attachment - リリースするアタッチメント / Attachment to release + * @return {void} + */ releaseTemporaryAttachment(attachment: IAttachmentObject): void { frameBufferManagerReleaseTemporaryAttachmentUseCase( @@ -164,6 +241,11 @@ export class FrameBufferManager ); } + /** + * @description 保留中のリリースを実行する + * Flush all pending releases + * @return {void} + */ flushPendingReleases(): void { for (const att of this.pendingReleases) { @@ -183,6 +265,11 @@ export class FrameBufferManager this.pendingReleases = []; } + /** + * @description 全リソースを破棄する + * Dispose all resources + * @return {void} + */ dispose(): void { for (const attachment of this.attachments.values()) { diff --git a/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerCreateRenderPassDescriptorService.ts b/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerCreateRenderPassDescriptorService.ts index d3355a92..13d10558 100644 --- a/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerCreateRenderPassDescriptorService.ts +++ b/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerCreateRenderPassDescriptorService.ts @@ -1,16 +1,45 @@ +/** + * @description クリアカラー値のプリアロケートオブジェクト + * Pre-allocated clear color value object + * @type {GPUColorDict} + */ const $clearValue: GPUColorDict = { "r": 0, "g": 0, "b": 0, "a": 0 }; + +/** + * @description カラーアタッチメントのプリアロケートオブジェクト + * Pre-allocated color attachment object + * @type {GPURenderPassColorAttachment} + */ const $colorAttachment: GPURenderPassColorAttachment = { "view": null as unknown as GPUTextureView, "clearValue": $clearValue, "loadOp": "clear", "storeOp": "store" }; + +/** + * @description レンダーパス記述子のプリアロケートオブジェクト + * Pre-allocated render pass descriptor object + * @type {GPURenderPassDescriptor} + */ const $descriptor: GPURenderPassDescriptor = { "colorAttachments": [$colorAttachment] }; /** * @description レンダーパス記述子を作成(プリアロケート再利用) + * Create render pass descriptor (pre-allocated reuse) + * + * @param {GPUTextureView} view - レンダーターゲットのテクスチャビュー + * @param {number} r - クリアカラーの赤成分 + * @param {number} g - クリアカラーの緑成分 + * @param {number} b - クリアカラーの青成分 + * @param {number} a - クリアカラーのアルファ成分 + * @param {GPULoadOp} load_op - ロード操作 + * @param {GPUTextureView | null} resolve_target - MSAAリゾルブターゲット + * @return {GPURenderPassDescriptor} + * @method + * @protected */ export const execute = ( view: GPUTextureView, @@ -18,15 +47,15 @@ export const execute = ( g: number = 0, b: number = 0, a: number = 0, - loadOp: GPULoadOp = "clear", - resolveTarget: GPUTextureView | null = null + load_op: GPULoadOp = "clear", + resolve_target: GPUTextureView | null = null ): GPURenderPassDescriptor => { $colorAttachment.view = view; $clearValue.r = r; $clearValue.g = g; $clearValue.b = b; $clearValue.a = a; - $colorAttachment.loadOp = loadOp; - $colorAttachment.resolveTarget = resolveTarget ?? undefined; + $colorAttachment.loadOp = load_op; + $colorAttachment.resolveTarget = resolve_target ?? undefined; return $descriptor; }; diff --git a/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerCreateStencilRenderPassDescriptorService.ts b/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerCreateStencilRenderPassDescriptorService.ts index 0a592389..cb3bff17 100644 --- a/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerCreateStencilRenderPassDescriptorService.ts +++ b/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerCreateStencilRenderPassDescriptorService.ts @@ -1,16 +1,39 @@ +/** + * @description クリアカラー値のプリアロケートオブジェクト + * Pre-allocated clear color value object + * @type {GPUColorDict} + */ const $clearValue: GPUColorDict = { "r": 0, "g": 0, "b": 0, "a": 0 }; + +/** + * @description カラーアタッチメントのプリアロケートオブジェクト + * Pre-allocated color attachment object + * @type {GPURenderPassColorAttachment} + */ const $colorAttachment: GPURenderPassColorAttachment = { "view": null as unknown as GPUTextureView, "clearValue": $clearValue, "loadOp": "load", "storeOp": "store" }; + +/** + * @description 深度ステンシルアタッチメントのプリアロケートオブジェクト + * Pre-allocated depth stencil attachment object + * @type {GPURenderPassDepthStencilAttachment} + */ const $depthStencilAttachment: GPURenderPassDepthStencilAttachment = { "view": null as unknown as GPUTextureView, "stencilClearValue": 0, "stencilLoadOp": "clear", "stencilStoreOp": "store" }; + +/** + * @description ステンシル付きレンダーパス記述子のプリアロケートオブジェクト + * Pre-allocated render pass descriptor with stencil + * @type {GPURenderPassDescriptor} + */ const $descriptor: GPURenderPassDescriptor = { "colorAttachments": [$colorAttachment], "depthStencilAttachment": $depthStencilAttachment @@ -18,18 +41,28 @@ const $descriptor: GPURenderPassDescriptor = { /** * @description ステンシル付きレンダーパス記述子を作成(プリアロケート再利用) + * Create render pass descriptor with stencil (pre-allocated reuse) + * + * @param {GPUTextureView} color_view - カラーアタッチメントのテクスチャビュー + * @param {GPUTextureView} stencil_view - ステンシルアタッチメントのテクスチャビュー + * @param {GPULoadOp} color_load_op - カラーのロード操作 + * @param {GPULoadOp} stencil_load_op - ステンシルのロード操作 + * @param {GPUTextureView | null} resolve_target - MSAAリゾルブターゲット + * @return {GPURenderPassDescriptor} + * @method + * @protected */ export const execute = ( - colorView: GPUTextureView, - stencilView: GPUTextureView, - colorLoadOp: GPULoadOp = "load", - stencilLoadOp: GPULoadOp = "clear", - resolveTarget: GPUTextureView | null = null + color_view: GPUTextureView, + stencil_view: GPUTextureView, + color_load_op: GPULoadOp = "load", + stencil_load_op: GPULoadOp = "clear", + resolve_target: GPUTextureView | null = null ): GPURenderPassDescriptor => { - $colorAttachment.view = colorView; - $colorAttachment.loadOp = colorLoadOp; - $colorAttachment.resolveTarget = resolveTarget ?? undefined; - $depthStencilAttachment.view = stencilView; - $depthStencilAttachment.stencilLoadOp = stencilLoadOp; + $colorAttachment.view = color_view; + $colorAttachment.loadOp = color_load_op; + $colorAttachment.resolveTarget = resolve_target ?? undefined; + $depthStencilAttachment.view = stencil_view; + $depthStencilAttachment.stencilLoadOp = stencil_load_op; return $descriptor; }; diff --git a/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerFlushPendingReleasesService.test.ts b/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerFlushPendingReleasesService.test.ts deleted file mode 100644 index f5abde1a..00000000 --- a/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerFlushPendingReleasesService.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import type { IAttachmentObject } from "../../interface/IAttachmentObject"; -import { execute } from "./FrameBufferManagerFlushPendingReleasesService"; - -describe("FrameBufferManagerFlushPendingReleasesService", () => -{ - const createMockAttachment = (hasTexture: boolean, hasStencil: boolean): IAttachmentObject => - { - return { - "texture": hasTexture ? { - "resource": { "destroy": vi.fn() } - } : null, - "stencil": hasStencil ? { - "resource": { "destroy": vi.fn() } - } : null - } as unknown as IAttachmentObject; - }; - - it("should destroy texture resources", () => - { - const attachment = createMockAttachment(true, false); - const pendingReleases = [attachment]; - - execute(pendingReleases); - - expect(attachment.texture!.resource.destroy).toHaveBeenCalled(); - }); - - it("should destroy stencil resources", () => - { - const attachment = createMockAttachment(false, true); - const pendingReleases = [attachment]; - - execute(pendingReleases); - - expect(attachment.stencil!.resource.destroy).toHaveBeenCalled(); - }); - - it("should destroy both texture and stencil resources", () => - { - const attachment = createMockAttachment(true, true); - const pendingReleases = [attachment]; - - execute(pendingReleases); - - expect(attachment.texture!.resource.destroy).toHaveBeenCalled(); - expect(attachment.stencil!.resource.destroy).toHaveBeenCalled(); - }); - - it("should handle attachment without texture", () => - { - const attachment = createMockAttachment(false, true); - const pendingReleases = [attachment]; - - expect(() => execute(pendingReleases)).not.toThrow(); - }); - - it("should handle attachment without stencil", () => - { - const attachment = createMockAttachment(true, false); - const pendingReleases = [attachment]; - - expect(() => execute(pendingReleases)).not.toThrow(); - }); - - it("should handle empty pending releases array", () => - { - const pendingReleases: IAttachmentObject[] = []; - - expect(() => execute(pendingReleases)).not.toThrow(); - }); - - it("should process multiple attachments", () => - { - const attachment1 = createMockAttachment(true, true); - const attachment2 = createMockAttachment(true, false); - const attachment3 = createMockAttachment(false, true); - const pendingReleases = [attachment1, attachment2, attachment3]; - - execute(pendingReleases); - - expect(attachment1.texture!.resource.destroy).toHaveBeenCalled(); - expect(attachment1.stencil!.resource.destroy).toHaveBeenCalled(); - expect(attachment2.texture!.resource.destroy).toHaveBeenCalled(); - expect(attachment3.stencil!.resource.destroy).toHaveBeenCalled(); - }); - - it("should call destroy exactly once per resource", () => - { - const attachment = createMockAttachment(true, true); - const pendingReleases = [attachment]; - - execute(pendingReleases); - - expect(attachment.texture!.resource.destroy).toHaveBeenCalledTimes(1); - expect(attachment.stencil!.resource.destroy).toHaveBeenCalledTimes(1); - }); -}); diff --git a/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerFlushPendingReleasesService.ts b/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerFlushPendingReleasesService.ts deleted file mode 100644 index 847a77a5..00000000 --- a/packages/webgpu/src/FrameBufferManager/service/FrameBufferManagerFlushPendingReleasesService.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { IAttachmentObject } from "../../interface/IAttachmentObject"; - -/** - * @description フレーム終了時に保留中のテクスチャを解放 - * Release pending textures at end of frame (after submit) - * - * @param {IAttachmentObject[]} pendingReleases - * @return {void} - * @method - * @protected - */ -export const execute = (pendingReleases: IAttachmentObject[]): void => { - for (const att of pendingReleases) { - if (att.texture) { - att.texture.resource.destroy(); - } - if (att.stencil) { - att.stencil.resource.destroy(); - } - } -}; diff --git a/packages/webgpu/src/FrameBufferManager/usecase/FrameBufferManagerCreateAttachmentUseCase.ts b/packages/webgpu/src/FrameBufferManager/usecase/FrameBufferManagerCreateAttachmentUseCase.ts index a17d0596..ab5a96ba 100644 --- a/packages/webgpu/src/FrameBufferManager/usecase/FrameBufferManagerCreateAttachmentUseCase.ts +++ b/packages/webgpu/src/FrameBufferManager/usecase/FrameBufferManagerCreateAttachmentUseCase.ts @@ -7,15 +7,15 @@ import { $samples } from "../../WebGPUUtil"; * @description アタッチメントオブジェクトを作成 * Create attachment object * - * @param {GPUDevice} device - * @param {GPUTextureFormat} format - * @param {Map} attachments - * @param {string} name - * @param {number} width - * @param {number} height - * @param {boolean} msaa - * @param {boolean} mask - * @param {{ nextId: number, textureId: number, stencilId: number }} idCounter + * @param {GPUDevice} device - GPUデバイス + * @param {GPUTextureFormat} format - テクスチャフォーマット + * @param {Map} attachments - アタッチメント管理マップ + * @param {string} name - アタッチメント名 + * @param {number} width - テクスチャ幅 + * @param {number} height - テクスチャ高さ + * @param {boolean} msaa - MSAA有効フラグ + * @param {boolean} mask - マスク有効フラグ + * @param {{ nextId: number, textureId: number, stencilId: number }} id_counter - ID管理カウンタ * @return {IAttachmentObject} * @method * @protected @@ -29,7 +29,7 @@ export const execute = ( height: number, msaa: boolean, mask: boolean, - idCounter: { nextId: number; textureId: number; stencilId: number } + id_counter: { nextId: number; textureId: number; stencilId: number } ): IAttachmentObject => { // アトラスかどうか判定(atlas, atlas_0, atlas_1, ...) const isAtlas = name === "atlas" || name.startsWith("atlas_"); @@ -57,7 +57,7 @@ export const execute = ( // ITextureObject形式で格納(解決先テクスチャ) const texture: ITextureObject = { - "id": idCounter.textureId++, + "id": id_counter.textureId++, "resource": gpuTexture, "view": textureView, width, @@ -78,7 +78,7 @@ export const execute = ( const msaaTextureView = msaaGpuTexture.createView(); msaaTexture = { - "id": idCounter.textureId++, + "id": id_counter.textureId++, "resource": msaaGpuTexture, "view": msaaTextureView, width, @@ -103,7 +103,7 @@ export const execute = ( const stencilView = stencilTexture.createView(); stencil = { - "id": idCounter.stencilId++, + "id": id_counter.stencilId++, "resource": stencilTexture, "view": stencilView, width, @@ -123,7 +123,7 @@ export const execute = ( const msaaStencilView = msaaStencilTexture.createView(); msaaStencil = { - "id": idCounter.stencilId++, + "id": id_counter.stencilId++, "resource": msaaStencilTexture, "view": msaaStencilView, width, @@ -135,7 +135,7 @@ export const execute = ( } const attachment: IAttachmentObject = { - "id": idCounter.nextId++, + "id": id_counter.nextId++, width, height, "clipLevel": 0, diff --git a/packages/webgpu/src/FrameBufferManager/usecase/FrameBufferManagerReleaseTemporaryAttachmentUseCase.ts b/packages/webgpu/src/FrameBufferManager/usecase/FrameBufferManagerReleaseTemporaryAttachmentUseCase.ts index cb110ebd..71022778 100644 --- a/packages/webgpu/src/FrameBufferManager/usecase/FrameBufferManagerReleaseTemporaryAttachmentUseCase.ts +++ b/packages/webgpu/src/FrameBufferManager/usecase/FrameBufferManagerReleaseTemporaryAttachmentUseCase.ts @@ -5,16 +5,16 @@ import type { IAttachmentObject } from "../../interface/IAttachmentObject"; * Releases a temporary attachment after filter processing * テクスチャは即座に破棄せず、フレーム終了時に遅延解放します * - * @param {Map} attachments - * @param {IAttachmentObject[]} pendingReleases - * @param {IAttachmentObject} attachment + * @param {Map} attachments - アタッチメント管理マップ + * @param {IAttachmentObject[]} pending_releases - 遅延解放キュー + * @param {IAttachmentObject} attachment - 解放するアタッチメント * @return {void} * @method * @protected */ export const execute = ( attachments: Map, - pendingReleases: IAttachmentObject[], + pending_releases: IAttachmentObject[], attachment: IAttachmentObject ): void => { // 名前を検索して削除(Map から削除するが、テクスチャは破棄しない) @@ -22,7 +22,7 @@ export const execute = ( if (att.id === attachment.id) { attachments.delete(name); // フレーム終了時に遅延解放するためキューに追加 - pendingReleases.push(att); + pending_releases.push(att); break; } } diff --git a/packages/webgpu/src/Gradient/GradientLUTCache.ts b/packages/webgpu/src/Gradient/GradientLUTCache.ts index ad05cad5..11d884b5 100644 --- a/packages/webgpu/src/Gradient/GradientLUTCache.ts +++ b/packages/webgpu/src/Gradient/GradientLUTCache.ts @@ -16,6 +16,8 @@ const $gradientAttachmentObjects: Map = new Map(); /** * @description GPUDeviceの参照 + * Reference to the GPUDevice + * @type {GPUDevice | null} * @private */ let $device: GPUDevice | null = null; @@ -114,18 +116,50 @@ export const $clearGradientAttachmentObjects = (): void => // === Gradient LUT テクスチャキャッシュ === +/** + * @description グラデーションLUTキャッシュエントリのインターフェース + * Interface for gradient LUT cache entry + */ interface IGradientLUTEntry { texture: GPUTexture; view: GPUTextureView; lastUsedFrame: number; } +/** + * @description キー文字列からLUTエントリへのキャッシュマップ + * Cache map from key string to LUT entry + * @type {Map} + * @private + */ const $lutCache: Map = new Map(); + +/** + * @description 現在のフレーム番号(TTL計算に使用) + * Current frame number used for TTL calculation + * @type {number} + * @private + */ let $currentFrame: number = 0; + +/** + * @description LUTキャッシュのTTL(フレーム数) + * TTL for LUT cache in number of frames + * @type {number} + * @constant + * @private + */ const $LUT_TTL: number = 60; /** - * @description グラデーションLUTのキャッシュキーを生成 + * @description グラデーションLUTのキャッシュキーを生成する + * Build a cache key string for a gradient LUT + * + * @param {number[]} stops - グラデーションストップ配列 / Gradient stop array + * @param {number} spread - スプレッドメソッド / Spread method + * @param {number} interpolation - 補間方法 / Interpolation method + * @return {string} キャッシュキー文字列 / Cache key string + * @private */ const $buildLUTKey = ( stops: number[], @@ -137,7 +171,15 @@ const $buildLUTKey = ( }; /** - * @description キャッシュからLUTテクスチャを取得。ヒットしなければnullを返す。 + * @description キャッシュからLUTテクスチャを取得する。ヒットしなければnullを返す。 + * Retrieve a LUT texture from the cache. Returns null on cache miss. + * + * @param {number[]} stops - グラデーションストップ配列 / Gradient stop array + * @param {number} spread - スプレッドメソッド / Spread method + * @param {number} interpolation - 補間方法 / Interpolation method + * @return {IGradientLUTEntry | null} キャッシュエントリまたはnull / Cache entry or null + * @method + * @protected */ export const $getLUTFromCache = ( stops: number[], @@ -155,7 +197,17 @@ export const $getLUTFromCache = ( }; /** - * @description LUTテクスチャをキャッシュに格納 + * @description LUTテクスチャをキャッシュに格納する + * Store a LUT texture into the cache + * + * @param {number[]} stops - グラデーションストップ配列 / Gradient stop array + * @param {number} spread - スプレッドメソッド / Spread method + * @param {number} interpolation - 補間方法 / Interpolation method + * @param {GPUTexture} texture - GPUテクスチャ / GPU texture + * @param {GPUTextureView} view - GPUテクスチャビュー / GPU texture view + * @return {void} + * @method + * @protected */ export const $putLUTToCache = ( stops: number[], @@ -174,7 +226,12 @@ export const $putLUTToCache = ( }; /** - * @description フレーム終了時にTTL超過エントリを解放 + * @description フレーム終了時にTTL超過エントリを解放する + * Release entries that exceed TTL at the end of each frame + * + * @return {void} + * @method + * @protected */ export const $cleanupLUTCache = (): void => { @@ -188,7 +245,12 @@ export const $cleanupLUTCache = (): void => }; /** - * @description 全LUTキャッシュを破棄 + * @description 全LUTキャッシュを破棄する + * Destroy and clear the entire LUT cache + * + * @return {void} + * @method + * @protected */ export const $clearLUTCache = (): void => { diff --git a/packages/webgpu/src/Gradient/GradientLUTGenerator.ts b/packages/webgpu/src/Gradient/GradientLUTGenerator.ts index 58fbb4a8..ba23563d 100644 --- a/packages/webgpu/src/Gradient/GradientLUTGenerator.ts +++ b/packages/webgpu/src/Gradient/GradientLUTGenerator.ts @@ -4,30 +4,38 @@ */ /** - * @description ストップ数に応じた適応解像度を取得 - * @param {number} stopsLength - * @return {number} + * @description ストップ数に応じた適応解像度を取得する + * Get adaptive resolution based on the number of gradient stops + * + * @param {number} stops_length - ストップ数 / Number of gradient stops + * @return {number} 解像度 (256, 512, or 1024) / Resolution + * @method + * @protected */ -export const getAdaptiveResolution = (stopsLength: number): number => +export const getAdaptiveResolution = (stops_length: number): number => { - if (stopsLength <= 4) { + if (stops_length <= 4) { return 256; } - if (stopsLength <= 8) { + if (stops_length <= 8) { return 512; } return 1024; }; /** - * @description グラデーションLUTテクスチャデータを生成 + * @description グラデーションLUTテクスチャデータを生成する + * Generate gradient LUT texture data. * stops配列: [offset, R, G, B, A, offset, R, G, B, A, ...] * 注意: R, G, B, A は 0-255 範囲 * LUTは0-1の範囲の色を生成し、spread処理はシェーダー側で行う - * @param {number[]} stops - グラデーションストップ配列 - * @param {number} _spread - スプレッドメソッド(未使用、シェーダー側で処理) - * @param {number} interpolation - 補間方法 (0: linearRGB, 1: RGB) ※WebGL互換 - * @return {Uint8Array} + * + * @param {number[]} stops - グラデーションストップ配列 / Gradient stop array + * @param {number} _spread - スプレッドメソッド(未使用、シェーダー側で処理)/ Spread method (unused, handled by shader) + * @param {number} interpolation - 補間方法 (0: linearRGB, 1: RGB) / Interpolation method (WebGL compatible) + * @return {Uint8Array} LUTテクスチャデータ / LUT texture data + * @method + * @protected */ export const generateGradientLUT = ( stops: number[], @@ -48,7 +56,7 @@ export const generateGradientLUT = ( const t = i / (resolution - 1); // 色を補間(色は0-255範囲で返される) - const color = interpolateColor(stops, t, interpolation); + const color = $interpolateColor(stops, t, interpolation); // WebGL版と同じ: プリマルチプライドアルファは適用しない // LUTにはストレート(非プリマルチプライド)の色を格納 @@ -67,14 +75,18 @@ export const generateGradientLUT = ( }; /** - * @description 色を補間 + * @description 色を補間する + * Interpolate color between gradient stops. * 色は0-255範囲で入力され、0-255範囲で出力される - * @param {number[]} stops - * @param {number} t - * @param {number} interpolation - 0: linearRGB, 1: RGB(WebGL互換) - * @return {{ r: number, g: number, b: number, a: number }} + * Colors are input in 0-255 range and output in 0-255 range. + * + * @param {number[]} stops - グラデーションストップ配列 / Gradient stop array + * @param {number} t - 補間位置 (0-1) / Interpolation position (0-1) + * @param {number} interpolation - 補間方法 (0: linearRGB, 1: RGB) / Interpolation method (WebGL compatible) + * @return {{ r: number, g: number, b: number, a: number }} 補間された色 / Interpolated color + * @private */ -const interpolateColor = ( +const $interpolateColor = ( stops: number[], t: number, interpolation: number @@ -135,57 +147,76 @@ const interpolateColor = ( // linearRGB補間(ガンマ補正) // 0-255 → 0-1に正規化してからリニア変換 return { - "r": linearToSRGB(lerp(sRGBToLinear(startR / 255), sRGBToLinear(endR / 255), localT)) * 255, - "g": linearToSRGB(lerp(sRGBToLinear(startG / 255), sRGBToLinear(endG / 255), localT)) * 255, - "b": linearToSRGB(lerp(sRGBToLinear(startB / 255), sRGBToLinear(endB / 255), localT)) * 255, - "a": lerp(startA, endA, localT) + "r": $linearToSRGB($lerp($sRGBToLinear(startR / 255), $sRGBToLinear(endR / 255), localT)) * 255, + "g": $linearToSRGB($lerp($sRGBToLinear(startG / 255), $sRGBToLinear(endG / 255), localT)) * 255, + "b": $linearToSRGB($lerp($sRGBToLinear(startB / 255), $sRGBToLinear(endB / 255), localT)) * 255, + "a": $lerp(startA, endA, localT) }; } // RGB補間(リニア、デフォルト)- 0-255範囲でそのまま補間 return { - "r": lerp(startR, endR, localT), - "g": lerp(startG, endG, localT), - "b": lerp(startB, endB, localT), - "a": lerp(startA, endA, localT) + "r": $lerp(startR, endR, localT), + "g": $lerp(startG, endG, localT), + "b": $lerp(startB, endB, localT), + "a": $lerp(startA, endA, localT) }; }; /** - * @description 線形補間 + * @description 線形補間を行う + * Perform linear interpolation between two values + * + * @param {number} a - 開始値 / Start value + * @param {number} b - 終了値 / End value + * @param {number} t - 補間係数 (0-1) / Interpolation factor (0-1) + * @return {number} 補間結果 / Interpolated result + * @private */ -const lerp = (a: number, b: number, t: number): number => +const $lerp = (a: number, b: number, t: number): number => { return a + (b - a) * t; }; /** - * @description sRGBからリニアへ変換(入力: 0-1正規化値) + * @description sRGBからリニア色空間へ変換する(入力: 0-1正規化値) + * Convert from sRGB to linear color space (input: 0-1 normalized value). * WebGL版と同じガンマ値 2.23333333 を使用 + * + * @param {number} value - sRGB色空間の正規化値 (0-1) / Normalized value in sRGB color space + * @return {number} リニア色空間の値 / Value in linear color space + * @private */ -const sRGBToLinear = (value: number): number => +const $sRGBToLinear = (value: number): number => { - // WebGL版と同じ簡易ガンマ補正 return Math.pow(value, 2.23333333); }; /** - * @description リニアからsRGBへ変換(出力: 0-1正規化値) + * @description リニア色空間からsRGBへ変換する(出力: 0-1正規化値) + * Convert from linear color space to sRGB (output: 0-1 normalized value). * WebGL版と同じガンマ値 0.45454545 (= 1/2.2) を使用 + * + * @param {number} value - リニア色空間の値 / Value in linear color space + * @return {number} sRGB色空間の正規化値 / Normalized value in sRGB color space + * @private */ -const linearToSRGB = (value: number): number => +const $linearToSRGB = (value: number): number => { - // WebGL版と同じ簡易ガンマ補正 return Math.pow(value, 0.45454545); }; /** - * @description フィルター用グラデーションLUTテクスチャデータを生成 + * @description フィルター用グラデーションLUTテクスチャデータを生成する + * Generate gradient LUT texture data for filters. * ratios, colors, alphas配列から1D LUTを生成 - * @param {Float32Array} ratios - 比率配列 (0-255) - * @param {Float32Array} colors - 色配列 (32bit整数) - * @param {Float32Array} alphas - アルファ配列 (0-1) - * @return {Uint8Array} + * + * @param {Float32Array} ratios - 比率配列 (0-255) / Ratio array (0-255) + * @param {Float32Array} colors - 色配列 (32bit整数) / Color array (32-bit integers) + * @param {Float32Array} alphas - アルファ配列 (0-1) / Alpha array (0-1) + * @return {Uint8Array} LUTテクスチャデータ / LUT texture data + * @method + * @protected */ export const generateFilterGradientLUT = ( ratios: Float32Array, @@ -234,10 +265,10 @@ export const generateFilterGradientLUT = ( localT = (t - start.offset) / (end.offset - start.offset); } - const r = lerp(start.r, end.r, localT); - const g = lerp(start.g, end.g, localT); - const b = lerp(start.b, end.b, localT); - const a = lerp(start.a, end.a, localT); + const r = $lerp(start.r, end.r, localT); + const g = $lerp(start.g, end.g, localT); + const b = $lerp(start.b, end.b, localT); + const a = $lerp(start.a, end.a, localT); // プリマルチプライドアルファで書き込み const offset = i * 4; diff --git a/packages/webgpu/src/Mask.test.ts b/packages/webgpu/src/Mask.test.ts index 15310a93..292383df 100644 --- a/packages/webgpu/src/Mask.test.ts +++ b/packages/webgpu/src/Mask.test.ts @@ -6,9 +6,6 @@ import { $isMaskTestEnabled, $setMaskStencilReference, $getMaskStencilReference, - $pushMaskAttachment, - $popMaskAttachment, - $hasMaskAttachment, $clipBounds, $clipLevels, $resetMaskState @@ -72,37 +69,6 @@ describe("Mask", () => }); }); - describe("mask attachment stack", () => - { - it("should default to empty", () => - { - expect($hasMaskAttachment()).toBe(false); - }); - - it("should push and pop attachments", () => - { - const attachment1 = { "id": 1 }; - const attachment2 = { "id": 2 }; - - $pushMaskAttachment(attachment1); - expect($hasMaskAttachment()).toBe(true); - - $pushMaskAttachment(attachment2); - expect($hasMaskAttachment()).toBe(true); - - expect($popMaskAttachment()).toBe(attachment2); - expect($hasMaskAttachment()).toBe(true); - - expect($popMaskAttachment()).toBe(attachment1); - expect($hasMaskAttachment()).toBe(false); - }); - - it("should return undefined when popping empty stack", () => - { - expect($popMaskAttachment()).toBeUndefined(); - }); - }); - describe("clip bounds and levels", () => { it("should be Maps", () => @@ -137,7 +103,6 @@ describe("Mask", () => $setMaskDrawing(true); $setMaskTestEnabled(true); $setMaskStencilReference(10); - $pushMaskAttachment({ "id": 1 }); $clipBounds.set(1, new Float32Array([0, 0, 100, 100])); $clipLevels.set(1, 5); @@ -148,7 +113,6 @@ describe("Mask", () => expect($isMaskDrawing()).toBe(false); expect($isMaskTestEnabled()).toBe(false); expect($getMaskStencilReference()).toBe(0); - expect($hasMaskAttachment()).toBe(false); expect($clipBounds.size).toBe(0); expect($clipLevels.size).toBe(0); }); diff --git a/packages/webgpu/src/Mask.ts b/packages/webgpu/src/Mask.ts index 5551a197..bdcdce6b 100644 --- a/packages/webgpu/src/Mask.ts +++ b/packages/webgpu/src/Mask.ts @@ -1,66 +1,123 @@ +/** + * @description マスク描画中かどうかの状態フラグ + * Flag indicating whether mask drawing is in progress + * + * @type {boolean} + */ let $maskDrawingState: boolean = false; +/** + * @description マスク描画状態を設定する + * Set the mask drawing state + * + * @param {boolean} state - マスク描画中かどうか / whether mask drawing is active + * @return {void} + */ export const $setMaskDrawing = (state: boolean): void => { $maskDrawingState = state; }; +/** + * @description マスク描画中かどうかを返す + * Returns whether mask drawing is currently active + * + * @return {boolean} + */ export const $isMaskDrawing = (): boolean => { return $maskDrawingState; }; +/** + * @description マスクテストが有効かどうかのフラグ + * Flag indicating whether mask (stencil) testing is enabled + * + * @type {boolean} + */ let $maskTestEnabled: boolean = false; +/** + * @description マスクステンシル参照値 + * Mask stencil reference value used for stencil comparison + * + * @type {number} + */ let $maskStencilReference: number = 0; +/** + * @description マスクテストの有効/無効を設定する + * Enable or disable mask (stencil) testing + * + * @param {boolean} enabled - 有効にするかどうか / whether to enable + * @return {void} + */ export const $setMaskTestEnabled = (enabled: boolean): void => { $maskTestEnabled = enabled; }; +/** + * @description マスクテストが有効かどうかを返す + * Returns whether mask (stencil) testing is enabled + * + * @return {boolean} + */ export const $isMaskTestEnabled = (): boolean => { return $maskTestEnabled; }; +/** + * @description マスクステンシル参照値を設定する + * Set the mask stencil reference value + * + * @param {number} value - ステンシル参照値 / stencil reference value + * @return {void} + */ export const $setMaskStencilReference = (value: number): void => { $maskStencilReference = value; }; +/** + * @description マスクステンシル参照値を取得する + * Get the current mask stencil reference value + * + * @return {number} + */ export const $getMaskStencilReference = (): number => { return $maskStencilReference; }; -const $maskAttachmentStack: any[] = []; - -export const $pushMaskAttachment = (attachment: any): void => -{ - $maskAttachmentStack.push(attachment); -}; - -export const $popMaskAttachment = (): any => -{ - return $maskAttachmentStack.pop(); -}; - -export const $hasMaskAttachment = (): boolean => -{ - return $maskAttachmentStack.length > 0; -}; - +/** + * @description クリップ境界のマップ(キー: ID、値: バウンディングボックス) + * Map of clip bounds (key: ID, value: bounding box as Float32Array) + * + * @type {Map} + */ export const $clipBounds: Map = new Map(); +/** + * @description クリップレベルのマップ(キー: ID、値: クリップ深度) + * Map of clip levels (key: ID, value: clip depth) + * + * @type {Map} + */ export const $clipLevels: Map = new Map(); +/** + * @description マスク関連の全状態をリセットする + * Reset all mask-related state to initial values + * + * @return {void} + */ export const $resetMaskState = (): void => { $maskDrawingState = false; $maskTestEnabled = false; $maskStencilReference = 0; - $maskAttachmentStack.length = 0; $clipBounds.clear(); $clipLevels.clear(); }; diff --git a/packages/webgpu/src/Mask/service/MaskUnionMaskService.ts b/packages/webgpu/src/Mask/service/MaskUnionMaskService.ts index eedd97e3..cf63f14e 100644 --- a/packages/webgpu/src/Mask/service/MaskUnionMaskService.ts +++ b/packages/webgpu/src/Mask/service/MaskUnionMaskService.ts @@ -3,20 +3,12 @@ import type { PipelineManager } from "../../Shader/PipelineManager"; import type { IAttachmentObject } from "../../interface/IAttachmentObject"; /** - * @description マスクの合成処理(ネストされたマスク対応) - * WebGL版と同様に、レベル7を超えたステンシルビットをマージする + * @description フルスクリーン矩形の頂点データ(4 floats/vertex: position + bezier) + * Full-screen rectangle vertex data (4 floats/vertex: position + bezier) * - * @param {GPUDevice} device - * @param {GPURenderPassEncoder} renderPassEncoder - * @param {BufferManager} bufferManager - * @param {PipelineManager} pipelineManager - * @param {IAttachmentObject} currentAttachment - * @return {void} - * @method - * @protected + * @type {Float32Array} + * @constant */ - -// フルスクリーン矩形(4 floats/vertex: position + bezier) const $rectVertices = new Float32Array([ // Triangle 1 -1, -1, 0.5, 0.5, @@ -28,7 +20,13 @@ const $rectVertices = new Float32Array([ 1, 1, 0.5, 0.5 ]); -// FillUniforms: identity matrix + white color (NDC座標なので変換不要) +/** + * @description FillUniformsデータ: 恒等行列と白色(NDC座標なので変換不要) + * FillUniforms data: identity matrix + white color (no transform needed for NDC coordinates) + * + * @type {Float32Array} + * @constant + */ const $uniformData16 = new Float32Array([ 1, 1, 1, 1, // color: white 0.5, 0, 0, 0, // matrix0: (0.5, 0, 0, pad) → identity-like for NDC passthrough @@ -36,27 +34,42 @@ const $uniformData16 = new Float32Array([ 0.5, 0.5, 1, 0 // matrix2: (0.5, 0.5, 1, pad) ]); +/** + * @description マスクの合成処理(ネストされたマスク対応) + * Union mask processing for nested masks. + * WebGL版と同様に、レベル7を超えたステンシルビットをマージする + * Merges stencil bits exceeding level 7, same as WebGL version. + * + * @param {GPUDevice} device - GPUデバイス / GPU device + * @param {GPURenderPassEncoder} render_pass_encoder - レンダーパスエンコーダ / Render pass encoder + * @param {BufferManager} buffer_manager - バッファマネージャ / Buffer manager + * @param {PipelineManager} pipeline_manager - パイプラインマネージャ / Pipeline manager + * @param {IAttachmentObject} current_attachment - 現在のアタッチメントオブジェクト / Current attachment object + * @return {void} + * @method + * @protected + */ export const execute = ( device: GPUDevice, - renderPassEncoder: GPURenderPassEncoder, - bufferManager: BufferManager, - pipelineManager: PipelineManager, - currentAttachment: IAttachmentObject + render_pass_encoder: GPURenderPassEncoder, + buffer_manager: BufferManager, + pipeline_manager: PipelineManager, + current_attachment: IAttachmentObject ): void => { - if (!currentAttachment) { + if (!current_attachment) { return; } - const clipLevel = currentAttachment.clipLevel; + const clipLevel = current_attachment.clipLevel; const mask = 1 << clipLevel - 1; - const vertexBuffer = bufferManager.acquireVertexBuffer($rectVertices.byteLength, $rectVertices); + const vertexBuffer = buffer_manager.acquireVertexBuffer($rectVertices.byteLength, $rectVertices); // Dynamic Uniform Bufferにデータを書き込み - const uniformOffset = bufferManager.dynamicUniform.allocate($uniformData16); + const uniformOffset = buffer_manager.dynamicUniform.allocate($uniformData16); // Dynamic BindGroupを取得 - const layout = pipelineManager.getBindGroupLayout("fill_dynamic"); + const layout = pipeline_manager.getBindGroupLayout("fill_dynamic"); if (!layout) { return; } @@ -65,29 +78,29 @@ export const execute = ( "entries": [{ "binding": 0, "resource": { - "buffer": bufferManager.dynamicUniform.getBuffer(), + "buffer": buffer_manager.dynamicUniform.getBuffer(), "size": 256 } }] }); // === Pass 1: ステンシルビットのマージ === - const mergePipeline = pipelineManager.getPipeline(`mask_union_merge_${clipLevel}`); + const mergePipeline = pipeline_manager.getPipeline(`mask_union_merge_${clipLevel}`); if (mergePipeline) { - renderPassEncoder.setPipeline(mergePipeline); - renderPassEncoder.setStencilReference(mask); - renderPassEncoder.setVertexBuffer(0, vertexBuffer); - renderPassEncoder.setBindGroup(0, bindGroup, [uniformOffset]); - renderPassEncoder.draw(6, 1, 0, 0); + render_pass_encoder.setPipeline(mergePipeline); + render_pass_encoder.setStencilReference(mask); + render_pass_encoder.setVertexBuffer(0, vertexBuffer); + render_pass_encoder.setBindGroup(0, bindGroup, [uniformOffset]); + render_pass_encoder.draw(6, 1, 0, 0); } // === Pass 2: 上位ビットのクリア === - const clearPipeline = pipelineManager.getPipeline(`mask_union_clear_${clipLevel}`); + const clearPipeline = pipeline_manager.getPipeline(`mask_union_clear_${clipLevel}`); if (clearPipeline) { - renderPassEncoder.setPipeline(clearPipeline); - renderPassEncoder.setStencilReference(0); - renderPassEncoder.setVertexBuffer(0, vertexBuffer); - renderPassEncoder.setBindGroup(0, bindGroup, [uniformOffset]); - renderPassEncoder.draw(6, 1, 0, 0); + render_pass_encoder.setPipeline(clearPipeline); + render_pass_encoder.setStencilReference(0); + render_pass_encoder.setVertexBuffer(0, vertexBuffer); + render_pass_encoder.setBindGroup(0, bindGroup, [uniformOffset]); + render_pass_encoder.draw(6, 1, 0, 0); } }; diff --git a/packages/webgpu/src/Mask/usecase/MaskBindUseCase.test.ts b/packages/webgpu/src/Mask/usecase/MaskBindUseCase.test.ts deleted file mode 100644 index 64ed13e1..00000000 --- a/packages/webgpu/src/Mask/usecase/MaskBindUseCase.test.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { execute } from "./MaskBindUseCase"; - -// Mock Mask module -const mockIsMaskDrawing = vi.fn(() => false); -const mockSetMaskDrawing = vi.fn(); - -vi.mock("../../Mask", () => ({ - "$isMaskDrawing": () => mockIsMaskDrawing(), - "$setMaskDrawing": (value: boolean) => mockSetMaskDrawing(value) -})); - -// Mock MaskEndMaskService -const mockMaskEndMaskService = vi.fn(); -vi.mock("../service/MaskEndMaskService", () => ({ - "execute": () => mockMaskEndMaskService() -})); - -describe("MaskBindUseCase", () => -{ - beforeEach(() => - { - vi.clearAllMocks(); - mockIsMaskDrawing.mockReturnValue(false); - }); - - describe("mask binding", () => - { - it("should set mask drawing to true when mask=true and not already drawing", () => - { - mockIsMaskDrawing.mockReturnValue(false); - - execute(true); - - expect(mockSetMaskDrawing).toHaveBeenCalledWith(true); - expect(mockMaskEndMaskService).toHaveBeenCalled(); - }); - - it("should not change state when mask=true and already drawing", () => - { - mockIsMaskDrawing.mockReturnValue(true); - - execute(true); - - expect(mockSetMaskDrawing).not.toHaveBeenCalled(); - expect(mockMaskEndMaskService).not.toHaveBeenCalled(); - }); - }); - - describe("mask unbinding", () => - { - it("should set mask drawing to false when mask=false and currently drawing", () => - { - mockIsMaskDrawing.mockReturnValue(true); - - execute(false); - - expect(mockSetMaskDrawing).toHaveBeenCalledWith(false); - }); - - it("should not change state when mask=false and not drawing", () => - { - mockIsMaskDrawing.mockReturnValue(false); - - execute(false); - - expect(mockSetMaskDrawing).not.toHaveBeenCalled(); - }); - }); - - describe("mask end service", () => - { - it("should call mask end service when transitioning from not drawing to drawing", () => - { - mockIsMaskDrawing.mockReturnValue(false); - - execute(true); - - expect(mockMaskEndMaskService).toHaveBeenCalled(); - }); - - it("should not call mask end service when mask=false", () => - { - mockIsMaskDrawing.mockReturnValue(true); - - execute(false); - - expect(mockMaskEndMaskService).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/webgpu/src/Mask/usecase/MaskBindUseCase.ts b/packages/webgpu/src/Mask/usecase/MaskBindUseCase.ts deleted file mode 100644 index f652fb4f..00000000 --- a/packages/webgpu/src/Mask/usecase/MaskBindUseCase.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { execute as maskEndMaskService } from "../service/MaskEndMaskService"; -import { - $isMaskDrawing, - $setMaskDrawing -} from "../../Mask"; - -/** - * @description マスクOn/Offに合わせたバインド処理 - * Binding process according to mask On/Off - * - * @param {boolean} mask - * @return {void} - * @method - * @protected - */ -export const execute = (mask: boolean): void => -{ - if (!mask && $isMaskDrawing()) { - $setMaskDrawing(false); - } else if (mask && !$isMaskDrawing()) { - $setMaskDrawing(true); - maskEndMaskService(); - } -}; diff --git a/packages/webgpu/src/Mesh/service/MeshFillGenerateService.ts b/packages/webgpu/src/Mesh/service/MeshFillGenerateService.ts index 89bb9515..9913385c 100644 --- a/packages/webgpu/src/Mesh/service/MeshFillGenerateService.ts +++ b/packages/webgpu/src/Mesh/service/MeshFillGenerateService.ts @@ -10,10 +10,10 @@ import type { IPath } from "../../interface/IPath"; * * color/matrixはuniform bufferで供給される * - * @param {IPath} vertex - * @param {Float32Array} buffer - * @param {number} index - 現在の頂点インデックス - * @return {number} 新しい頂点インデックス + * @param {IPath} vertex - 頂点パスデータ / Vertex path data + * @param {Float32Array} buffer - 出力先バッファ / Output buffer + * @param {number} index - 現在の頂点インデックス / Current vertex index + * @return {number} 新しい頂点インデックス / New vertex index * @method * @protected */ diff --git a/packages/webgpu/src/Mesh/service/MeshLerpService.test.ts b/packages/webgpu/src/Mesh/service/MeshLerpService.test.ts deleted file mode 100644 index 3cf53c46..00000000 --- a/packages/webgpu/src/Mesh/service/MeshLerpService.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { execute } from "./MeshLerpService"; -import { describe, expect, it } from "vitest"; - -describe("MeshLerpService.ts method test", () => -{ - it("test case - lerp at t=0 returns first point", () => - { - const pointA = { x: 0, y: 0 }; - const pointB = { x: 10, y: 10 }; - - const result = execute(pointA, pointB, 0); - - expect(result.x).toBe(0); - expect(result.y).toBe(0); - }); - - it("test case - lerp at t=1 returns second point", () => - { - const pointA = { x: 0, y: 0 }; - const pointB = { x: 10, y: 10 }; - - const result = execute(pointA, pointB, 1); - - expect(result.x).toBe(10); - expect(result.y).toBe(10); - }); - - it("test case - lerp at t=0.5 returns midpoint", () => - { - const pointA = { x: 0, y: 0 }; - const pointB = { x: 10, y: 20 }; - - const result = execute(pointA, pointB, 0.5); - - expect(result.x).toBe(5); - expect(result.y).toBe(10); - }); - - it("test case - lerp at t=0.25", () => - { - const pointA = { x: 0, y: 0 }; - const pointB = { x: 100, y: 200 }; - - const result = execute(pointA, pointB, 0.25); - - expect(result.x).toBe(25); - expect(result.y).toBe(50); - }); -}); diff --git a/packages/webgpu/src/Mesh/service/MeshLerpService.ts b/packages/webgpu/src/Mesh/service/MeshLerpService.ts deleted file mode 100644 index eb5164aa..00000000 --- a/packages/webgpu/src/Mesh/service/MeshLerpService.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { IPoint } from "../../interface/IPoint"; - -/** - * @description 線形補間 - * Linear interpolation - * - * @param {IPoint} pointA - * @param {IPoint} pointB - * @param {number} t - * @return {IPoint} - * @method - * @protected - */ -export const execute = ( - pointA: IPoint, - pointB: IPoint, - t: number -): IPoint => { - return { - "x": pointA.x + (pointB.x - pointA.x) * t, - "y": pointA.y + (pointB.y - pointA.y) * t - }; -}; diff --git a/packages/webgpu/src/Mesh/service/MeshStrokeFillGenerateService.ts b/packages/webgpu/src/Mesh/service/MeshStrokeFillGenerateService.ts index a2520667..deae055c 100644 --- a/packages/webgpu/src/Mesh/service/MeshStrokeFillGenerateService.ts +++ b/packages/webgpu/src/Mesh/service/MeshStrokeFillGenerateService.ts @@ -10,10 +10,10 @@ import type { IPath } from "../../interface/IPath"; * * color/matrixはuniform bufferで供給される * - * @param {IPath} vertex - * @param {Float32Array} buffer - * @param {number} index - 現在の頂点インデックス - * @return {number} 新しい頂点インデックス + * @param {IPath} vertex - 頂点パスデータ / Vertex path data + * @param {Float32Array} buffer - 出力先バッファ / Output buffer + * @param {number} index - 現在の頂点インデックス / Current vertex index + * @return {number} 新しい頂点インデックス / New vertex index * @method * @protected */ diff --git a/packages/webgpu/src/Mesh/usecase/MeshBitmapStrokeGenerateUseCase.ts b/packages/webgpu/src/Mesh/usecase/MeshBitmapStrokeGenerateUseCase.ts index 0a44eb67..220c0a70 100644 --- a/packages/webgpu/src/Mesh/usecase/MeshBitmapStrokeGenerateUseCase.ts +++ b/packages/webgpu/src/Mesh/usecase/MeshBitmapStrokeGenerateUseCase.ts @@ -8,6 +8,13 @@ import { execute as meshStrokeFillGenerateService } from "../service/MeshStrokeF */ let $meshTempBuffer: Float32Array = new Float32Array(32); +/** + * @description 2のべき乗に切り上げる + * Round up to the next power of two + * + * @param {number} v - 切り上げ対象の値 / Value to round up + * @return {number} 2のべき乗の値 / Next power of two + */ const $upperPowerOfTwo = (v: number): number => { v--; @@ -24,9 +31,9 @@ const $upperPowerOfTwo = (v: number): number => * @description ビットマップストローク用のメッシュを生成する * Generate a mesh for bitmap stroke * - * @param {IPath[]} vertices - * @param {number} thickness - * @return {IMeshResult} + * @param {IPath[]} vertices - パス頂点配列 / Array of path vertices + * @param {number} thickness - 線の太さ / Line thickness + * @return {IMeshResult} メッシュ結果 / Mesh result * @method * @protected */ diff --git a/packages/webgpu/src/Mesh/usecase/MeshFillGenerateUseCase.ts b/packages/webgpu/src/Mesh/usecase/MeshFillGenerateUseCase.ts index 420fad5b..17fdfcf7 100644 --- a/packages/webgpu/src/Mesh/usecase/MeshFillGenerateUseCase.ts +++ b/packages/webgpu/src/Mesh/usecase/MeshFillGenerateUseCase.ts @@ -7,6 +7,13 @@ import { execute as meshFillGenerateService } from "../service/MeshFillGenerateS */ let $meshTempBuffer: Float32Array = new Float32Array(32); +/** + * @description 2のべき乗に切り上げる + * Round up to the next power of two + * + * @param {number} v - 切り上げ対象の値 / Value to round up + * @return {number} 2のべき乗の値 / Next power of two + */ const $upperPowerOfTwo = (v: number): number => { v--; @@ -23,8 +30,8 @@ const $upperPowerOfTwo = (v: number): number => * @description 塗りのメッシュを生成する * Generate a fill mesh * - * @param {IPath[]} vertices - * @return {IMeshResult} + * @param {IPath[]} vertices - パス頂点配列 / Array of path vertices + * @return {IMeshResult} メッシュ結果 / Mesh result * @method * @protected */ diff --git a/packages/webgpu/src/Mesh/usecase/MeshGradientStrokeGenerateUseCase.ts b/packages/webgpu/src/Mesh/usecase/MeshGradientStrokeGenerateUseCase.ts index 2ab5b446..5877ab6c 100644 --- a/packages/webgpu/src/Mesh/usecase/MeshGradientStrokeGenerateUseCase.ts +++ b/packages/webgpu/src/Mesh/usecase/MeshGradientStrokeGenerateUseCase.ts @@ -8,6 +8,13 @@ import { execute as meshStrokeFillGenerateService } from "../service/MeshStrokeF */ let $meshTempBuffer: Float32Array = new Float32Array(32); +/** + * @description 2のべき乗に切り上げる + * Round up to the next power of two + * + * @param {number} v - 切り上げ対象の値 / Value to round up + * @return {number} 2のべき乗の値 / Next power of two + */ const $upperPowerOfTwo = (v: number): number => { v--; @@ -24,9 +31,9 @@ const $upperPowerOfTwo = (v: number): number => * @description グラデーションストローク用のメッシュを生成する * Generate a mesh for gradient stroke * - * @param {IPath[]} vertices - * @param {number} thickness - * @return {IMeshResult} + * @param {IPath[]} vertices - パス頂点配列 / Array of path vertices + * @param {number} thickness - 線の太さ / Line thickness + * @return {IMeshResult} メッシュ結果 / Mesh result * @method * @protected */ diff --git a/packages/webgpu/src/Mesh/usecase/MeshSplitQuadraticBezierUseCase.test.ts b/packages/webgpu/src/Mesh/usecase/MeshSplitQuadraticBezierUseCase.test.ts deleted file mode 100644 index 66a2443d..00000000 --- a/packages/webgpu/src/Mesh/usecase/MeshSplitQuadraticBezierUseCase.test.ts +++ /dev/null @@ -1,247 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { execute } from "./MeshSplitQuadraticBezierUseCase"; - -describe("MeshSplitQuadraticBezierUseCase", () => -{ - describe("basic splitting", () => - { - it("should split a simple quadratic bezier at t=0.5", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 50, y: 100 }; - const end = { x: 100, y: 0 }; - - const result = execute(start, control, end, 0.5); - - expect(result).toHaveLength(2); - expect(result[0]).toHaveLength(3); - expect(result[1]).toHaveLength(3); - }); - - it("should use default t=0.5 when not specified", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 50, y: 100 }; - const end = { x: 100, y: 0 }; - - const result = execute(start, control, end); - - expect(result).toHaveLength(2); - }); - - it("should preserve start point in left sub-curve", () => - { - const start = { x: 10, y: 20 }; - const control = { x: 50, y: 100 }; - const end = { x: 100, y: 0 }; - - const result = execute(start, control, end, 0.5); - - expect(result[0][0]).toEqual(start); - }); - - it("should preserve end point in right sub-curve", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 50, y: 100 }; - const end = { x: 90, y: 30 }; - - const result = execute(start, control, end, 0.5); - - expect(result[1][2]).toEqual(end); - }); - - it("should share the split point between curves", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 50, y: 100 }; - const end = { x: 100, y: 0 }; - - const result = execute(start, control, end, 0.5); - - // M01 (split point) is last of left and first of right - expect(result[0][2]).toEqual(result[1][0]); - }); - }); - - describe("split at t=0.5", () => - { - it("should compute correct intermediate points", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 100, y: 200 }; - const end = { x: 200, y: 0 }; - - const result = execute(start, control, end, 0.5); - - // M0 = lerp(P0, P1, 0.5) = (50, 100) - // M1 = lerp(P1, P2, 0.5) = (150, 100) - // M01 = lerp(M0, M1, 0.5) = (100, 100) - - // Left curve: [P0, M0, M01] = [(0,0), (50,100), (100,100)] - expect(result[0][0]).toEqual({ x: 0, y: 0 }); - expect(result[0][1]).toEqual({ x: 50, y: 100 }); - expect(result[0][2]).toEqual({ x: 100, y: 100 }); - - // Right curve: [M01, M1, P2] = [(100,100), (150,100), (200,0)] - expect(result[1][0]).toEqual({ x: 100, y: 100 }); - expect(result[1][1]).toEqual({ x: 150, y: 100 }); - expect(result[1][2]).toEqual({ x: 200, y: 0 }); - }); - }); - - describe("split at different t values", () => - { - it("should correctly split at t=0.25", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 100, y: 0 }; - const end = { x: 100, y: 100 }; - - const result = execute(start, control, end, 0.25); - - // M0 = lerp(P0, P1, 0.25) = (25, 0) - // M1 = lerp(P1, P2, 0.25) = (100, 25) - // M01 = lerp(M0, M1, 0.25) = (25 + (100-25)*0.25, 0 + (25-0)*0.25) = (43.75, 6.25) - - expect(result[0][1].x).toBeCloseTo(25, 5); - expect(result[0][1].y).toBeCloseTo(0, 5); - - expect(result[1][1].x).toBeCloseTo(100, 5); - expect(result[1][1].y).toBeCloseTo(25, 5); - }); - - it("should correctly split at t=0.75", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 100, y: 0 }; - const end = { x: 100, y: 100 }; - - const result = execute(start, control, end, 0.75); - - // M0 = lerp(P0, P1, 0.75) = (75, 0) - // M1 = lerp(P1, P2, 0.75) = (100, 75) - - expect(result[0][1].x).toBeCloseTo(75, 5); - expect(result[0][1].y).toBeCloseTo(0, 5); - - expect(result[1][1].x).toBeCloseTo(100, 5); - expect(result[1][1].y).toBeCloseTo(75, 5); - }); - - it("should handle t=0 (split at start)", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 50, y: 100 }; - const end = { x: 100, y: 0 }; - - const result = execute(start, control, end, 0); - - // At t=0, split point is at start - expect(result[0][2]).toEqual(start); - expect(result[1][0]).toEqual(start); - }); - - it("should handle t=1 (split at end)", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 50, y: 100 }; - const end = { x: 100, y: 0 }; - - const result = execute(start, control, end, 1); - - // At t=1, split point is at end - expect(result[0][2]).toEqual(end); - expect(result[1][0]).toEqual(end); - }); - }); - - describe("edge cases", () => - { - it("should handle horizontal line (control on line)", () => - { - const start = { x: 0, y: 50 }; - const control = { x: 50, y: 50 }; - const end = { x: 100, y: 50 }; - - const result = execute(start, control, end, 0.5); - - // All y values should remain 50 - expect(result[0][0].y).toBe(50); - expect(result[0][1].y).toBe(50); - expect(result[0][2].y).toBe(50); - expect(result[1][1].y).toBe(50); - expect(result[1][2].y).toBe(50); - }); - - it("should handle vertical line", () => - { - const start = { x: 50, y: 0 }; - const control = { x: 50, y: 50 }; - const end = { x: 50, y: 100 }; - - const result = execute(start, control, end, 0.5); - - // All x values should remain 50 - expect(result[0][0].x).toBe(50); - expect(result[0][1].x).toBe(50); - expect(result[0][2].x).toBe(50); - expect(result[1][1].x).toBe(50); - expect(result[1][2].x).toBe(50); - }); - - it("should handle single point curve", () => - { - const point = { x: 50, y: 50 }; - - const result = execute(point, point, point, 0.5); - - // All points should be the same - expect(result[0][0]).toEqual(point); - expect(result[0][1]).toEqual(point); - expect(result[0][2]).toEqual(point); - expect(result[1][0]).toEqual(point); - expect(result[1][1]).toEqual(point); - expect(result[1][2]).toEqual(point); - }); - - it("should handle negative coordinates", () => - { - const start = { x: -100, y: -100 }; - const control = { x: 0, y: 100 }; - const end = { x: 100, y: -100 }; - - const result = execute(start, control, end, 0.5); - - expect(result).toHaveLength(2); - expect(result[0][0]).toEqual(start); - expect(result[1][2]).toEqual(end); - }); - - it("should handle very small t value", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 50, y: 100 }; - const end = { x: 100, y: 0 }; - - const result = execute(start, control, end, 0.001); - - // Split point should be much closer to start than to end - // At t=0.001, we expect very small values - expect(result[0][2].x).toBeLessThan(1); - expect(result[0][2].y).toBeLessThan(1); - }); - - it("should handle very large coordinates", () => - { - const start = { x: 0, y: 0 }; - const control = { x: 100000, y: 200000 }; - const end = { x: 200000, y: 0 }; - - const result = execute(start, control, end, 0.5); - - expect(result[0][1].x).toBeCloseTo(50000, 0); - expect(result[0][1].y).toBeCloseTo(100000, 0); - }); - }); -}); diff --git a/packages/webgpu/src/Mesh/usecase/MeshSplitQuadraticBezierUseCase.ts b/packages/webgpu/src/Mesh/usecase/MeshSplitQuadraticBezierUseCase.ts deleted file mode 100644 index dba9c744..00000000 --- a/packages/webgpu/src/Mesh/usecase/MeshSplitQuadraticBezierUseCase.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { IPoint } from "../../interface/IPoint"; -import { execute as meshLerpService } from "../service/MeshLerpService"; - -/** - * @description 二次ベジェ曲線を分割する - * Split a quadratic Bezier curve - * - * @param {IPoint} start_point - * @param {IPoint} control_point - * @param {IPoint} end_point - * @param {number} [t = 0.5] - * @return {Array} - * @method - * @protected - */ -export const execute = ( - start_point: IPoint, - control_point: IPoint, - end_point: IPoint, - t: number = 0.5 -): Array => { - - // 二次ベジエ曲線の分割 - // M0 = lerp(P0, P1, t) - // M1 = lerp(P1, P2, t) - // M01 = lerp(M0, M1, t) - const M0 = meshLerpService(start_point, control_point, t); - const M1 = meshLerpService(control_point, end_point, t); - const M01 = meshLerpService(M0, M1, t); - - // 左サブ (0...t): [P0, M0, M01] - // 右サブ (t...1): [M01, M1, P2] - return [ - [start_point, M0, M01], - [M01, M1, end_point] - ]; -}; diff --git a/packages/webgpu/src/Mesh/usecase/MeshStrokeFillGenerateUseCase.test.ts b/packages/webgpu/src/Mesh/usecase/MeshStrokeFillGenerateUseCase.test.ts deleted file mode 100644 index c83389b8..00000000 --- a/packages/webgpu/src/Mesh/usecase/MeshStrokeFillGenerateUseCase.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { describe, it, expect } from "vitest"; -import type { IPath } from "../../interface/IPath"; -import { execute } from "./MeshStrokeFillGenerateUseCase"; - -describe("MeshStrokeFillGenerateUseCase", () => -{ - describe("basic mesh generation", () => - { - it("should return IMeshResult with buffer and indexCount", () => - { - const vertices: IPath[] = [[ - 0, 0, false, - 100, 0, false, - 100, 100, false, - 0, 0, false - ]]; - - const result = execute(vertices); - - expect(result).toHaveProperty("buffer"); - expect(result).toHaveProperty("indexCount"); - expect(result.buffer).toBeInstanceOf(Float32Array); - }); - - it("should generate 4 floats per vertex", () => - { - const vertices: IPath[] = [[ - 0, 0, false, - 100, 0, false, - 100, 100, false, - 0, 0, false - ]]; - - const result = execute(vertices); - - // 2 triangles * 3 vertices = 6 vertices - // 6 vertices * 4 floats = 24 - expect(result.buffer.length).toBe(6 * 4); - expect(result.indexCount).toBe(6); - }); - }); - - describe("bezier coordinates", () => - { - it("should always set bezier to (0.5, 0.5) for stroke fill", () => - { - const vertices: IPath[] = [[ - 0, 0, false, - 100, 0, false, - 100, 100, false, - 0, 0, false - ]]; - - const result = execute(vertices); - - // Check bezier coordinates for all vertices - for (let i = 0; i < result.indexCount; i++) { - const offset = i * 4; - expect(result.buffer[offset + 2]).toBe(0.5); // bezier.u - expect(result.buffer[offset + 3]).toBe(0.5); // bezier.v - } - }); - - it("should set bezier to (0.5, 0.5) even for curve paths", () => - { - const vertices: IPath[] = [[ - 0, 0, false, - 50, 50, true, // control point - 100, 0, false, - 0, 0, false - ]]; - - const result = execute(vertices); - - // All bezier coordinates should be (0.5, 0.5) - for (let i = 0; i < result.indexCount; i++) { - const offset = i * 4; - expect(result.buffer[offset + 2]).toBe(0.5); - expect(result.buffer[offset + 3]).toBe(0.5); - } - }); - }); - - describe("multiple paths", () => - { - it("should handle multiple paths", () => - { - const vertices: IPath[] = [ - [0, 0, false, 100, 0, false, 100, 100, false, 0, 0, false], - [200, 200, false, 300, 200, false, 300, 300, false, 200, 200, false] - ]; - - const result = execute(vertices); - - // 2 paths * 2 triangles * 3 vertices = 12 vertices - expect(result.indexCount).toBe(12); - }); - }); - - describe("empty input", () => - { - it("should handle empty vertices array", () => - { - const vertices: IPath[] = []; - - const result = execute(vertices); - - expect(result.buffer.length).toBe(0); - expect(result.indexCount).toBe(0); - }); - - it("should handle path with insufficient points", () => - { - const vertices: IPath[] = [[ - 0, 0, false, - 100, 0, false - ]]; - - const result = execute(vertices); - - expect(result.buffer.length).toBe(0); - expect(result.indexCount).toBe(0); - }); - }); -}); diff --git a/packages/webgpu/src/Mesh/usecase/MeshStrokeFillGenerateUseCase.ts b/packages/webgpu/src/Mesh/usecase/MeshStrokeFillGenerateUseCase.ts deleted file mode 100644 index 9b1784e1..00000000 --- a/packages/webgpu/src/Mesh/usecase/MeshStrokeFillGenerateUseCase.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { IPath } from "../../interface/IPath"; -import type { IMeshResult } from "../../interface/IMeshResult"; -import { execute as meshStrokeFillGenerateService } from "../service/MeshStrokeFillGenerateService"; - -/** - * @description メッシュ生成用の再利用可能な一時バッファ(GC回避) - */ -let $meshTempBuffer: Float32Array = new Float32Array(32); - -const $upperPowerOfTwo = (v: number): number => -{ - v--; - v |= v >> 1; - v |= v >> 2; - v |= v >> 4; - v |= v >> 8; - v |= v >> 16; - v++; - return v; -}; - -/** - * @description ストローク塗りつぶし用のメッシュを生成する - * Generate a stroke fill mesh - * - * 頂点フォーマット(4 floats per vertex): - * - position: x, y (2 floats) - * - bezier: u, v (2 floats) - 常に (0.5, 0.5) - * - * color/matrixはuniform bufferで供給される - * - * @param {IPath[]} vertices - * @return {IMeshResult} - * @method - * @protected - */ -export const execute = ( - vertices: IPath[] -): IMeshResult => { - - // 頂点数を計算(各パスの三角形数 × 3) - let totalVertices = 0; - for (const vertex of vertices) { - const length = vertex.length - 5; - for (let idx = 3; idx < length; idx += 3) { - totalVertices += 3; - } - } - - // バッファを確保(4 floats per vertex、再利用可能バッファ) - const requiredSize = totalVertices * 4; - if ($meshTempBuffer.length < requiredSize) { - $meshTempBuffer = new Float32Array($upperPowerOfTwo(requiredSize)); - } - const buffer = $meshTempBuffer; - - let index = 0; - for (const vertex of vertices) { - index = meshStrokeFillGenerateService( - vertex, - buffer, - index - ); - } - - return { - "buffer": buffer.subarray(0, index * 4), - "indexCount": index - }; -}; diff --git a/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.test.ts b/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.test.ts index d1c60792..35f52f10 100644 --- a/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.test.ts +++ b/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.test.ts @@ -1,10 +1,8 @@ import { describe, it, expect, vi } from "vitest"; import type { IPath } from "../../interface/IPath"; -import type { IPoint } from "../../interface/IPoint"; import { generateStrokeOutline, - generateStrokeMesh, - generateStrokeMeshFromPoints + generateStrokeMesh } from "./MeshStrokeGenerateUseCase"; // Mock $context @@ -182,69 +180,4 @@ describe("MeshStrokeGenerateUseCase", () => }); }); - describe("generateStrokeMeshFromPoints", () => - { - it("should return Float32Array", () => - { - const paths: IPoint[][] = [[ - { "x": 0, "y": 0 }, - { "x": 100, "y": 0 } - ]]; - - const result = generateStrokeMeshFromPoints(paths, 10); - - expect(result).toBeInstanceOf(Float32Array); - }); - - it("should generate triangles for line segment", () => - { - const paths: IPoint[][] = [[ - { "x": 0, "y": 0 }, - { "x": 100, "y": 0 } - ]]; - - const result = generateStrokeMeshFromPoints(paths, 10); - - // 2 triangles * 3 vertices * 4 floats = 24 - expect(result.length).toBe(24); - }); - - it("should skip paths with single point", () => - { - const paths: IPoint[][] = [[ - { "x": 0, "y": 0 } - ]]; - - const result = generateStrokeMeshFromPoints(paths, 10); - - expect(result.length).toBe(0); - }); - - it("should handle multi-segment path", () => - { - const paths: IPoint[][] = [[ - { "x": 0, "y": 0 }, - { "x": 100, "y": 0 }, - { "x": 100, "y": 100 } - ]]; - - const result = generateStrokeMeshFromPoints(paths, 10); - - // 2 line segments * 24 floats each = 48 - expect(result.length).toBe(48); - }); - - it("should handle multiple separate paths", () => - { - const paths: IPoint[][] = [ - [{ "x": 0, "y": 0 }, { "x": 100, "y": 0 }], - [{ "x": 200, "y": 200 }, { "x": 300, "y": 200 }] - ]; - - const result = generateStrokeMeshFromPoints(paths, 10); - - // 2 paths * 1 line segment each * 24 floats = 48 - expect(result.length).toBe(48); - }); - }); }); diff --git a/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.ts b/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.ts index 04ed9b4b..9e4aee37 100644 --- a/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.ts +++ b/packages/webgpu/src/Mesh/usecase/MeshStrokeGenerateUseCase.ts @@ -4,26 +4,50 @@ import { $context } from "../../WebGPUUtil"; /** * @description Canvas 2Dコンテキスト(点が矩形内にあるか判定用) + * Canvas 2D context for point-in-rectangle testing */ -const canvas = new OffscreenCanvas(1, 1); -const $canvasContext = canvas.getContext("2d") as OffscreenCanvasRenderingContext2D; +const $canvas: OffscreenCanvas = new OffscreenCanvas(1, 1); + +/** + * @description 矩形内の点判定用2Dコンテキスト + * 2D context used for point-in-path detection + */ +const $canvasContext = $canvas.getContext("2d") as OffscreenCanvasRenderingContext2D; /** * @description 再利用可能なPointオブジェクト(GC回避) + * Reusable Point objects to avoid GC overhead */ const $startPoint: IPoint = { "x": 0, "y": 0 }; + +/** + * @description 再利用可能な制御点オブジェクト(GC回避) + * Reusable control point object to avoid GC overhead + */ const $controlPoint: IPoint = { "x": 0, "y": 0 }; + +/** + * @description 再利用可能な終了点オブジェクト(GC回避) + * Reusable end point object to avoid GC overhead + */ const $endPoint: IPoint = { "x": 0, "y": 0 }; + +/** + * @description 再利用可能な前の点オブジェクト(GC回避) + * Reusable previous point object to avoid GC overhead + */ const $prevPoint: IPoint = { "x": 0, "y": 0 }; /** * @description 法線ベクトルを計算(WebGL版のMeshCalculateNormalVectorServiceと同じ) - * @param {number} x - 方向ベクトルのx成分 - * @param {number} y - 方向ベクトルのy成分 - * @param {number} thickness - 線の太さ(半分の値) - * @return {IPoint} + * Calculate the normal vector (same as WebGL MeshCalculateNormalVectorService) + * + * @param {number} x - 方向ベクトルのx成分 / X component of direction vector + * @param {number} y - 方向ベクトルのy成分 / Y component of direction vector + * @param {number} thickness - 線の太さ(半分の値) / Half line thickness + * @return {IPoint} 法線ベクトル / Normal vector */ -const calculateNormalVector = (x: number, y: number, thickness: number): IPoint => +const $calculateNormalVector = (x: number, y: number, thickness: number): IPoint => { const magnitude = Math.sqrt(x * x + y * y); if (magnitude === 0) { @@ -37,16 +61,26 @@ const calculateNormalVector = (x: number, y: number, thickness: number): IPoint /** * @description 線形補間(lerp) + * Linear interpolation between two points + * + * @param {IPoint} p0 - 始点 / Start point + * @param {IPoint} p1 - 終点 / End point + * @param {number} t - 補間パラメータ(0〜1) / Interpolation parameter (0-1) + * @return {IPoint} 補間された点 / Interpolated point */ -const lerp = (p0: IPoint, p1: IPoint, t: number): IPoint => ({ +const $lerp = (p0: IPoint, p1: IPoint, t: number): IPoint => ({ "x": p0.x + (p1.x - p0.x) * t, "y": p0.y + (p1.y - p0.y) * t }); /** * @description ベクトルの正規化 + * Normalize a vector to unit length + * + * @param {IPoint} point - 正規化する点 / Point to normalize + * @return {IPoint} 正規化された点 / Normalized point */ -const normalize = (point: IPoint): IPoint => { +const $normalize = (point: IPoint): IPoint => { const length = Math.sqrt(point.x * point.x + point.y * point.y); return length === 0 ? { "x": 0, "y": 0 } @@ -55,8 +89,15 @@ const normalize = (point: IPoint): IPoint => { /** * @description 二次ベジェ曲線上の座標を計算 + * Calculate a point on a quadratic Bezier curve + * + * @param {number} t - 曲線パラメータ(0〜1) / Curve parameter (0-1) + * @param {IPoint} s0 - 始点 / Start point + * @param {IPoint} s1 - 制御点 / Control point + * @param {IPoint} s2 - 終点 / End point + * @return {IPoint} 曲線上の座標 / Point on the curve */ -const getQuadraticBezierPoint = ( +const $getQuadraticBezierPoint = ( t: number, s0: IPoint, s1: IPoint, @@ -68,8 +109,15 @@ const getQuadraticBezierPoint = ( /** * @description 二次ベジェ曲線上の接線ベクトルを計算 + * Calculate the tangent vector on a quadratic Bezier curve + * + * @param {number} t - 曲線パラメータ(0〜1) / Curve parameter (0-1) + * @param {IPoint} s0 - 始点 / Start point + * @param {IPoint} s1 - 制御点 / Control point + * @param {IPoint} s2 - 終点 / End point + * @return {IPoint} 接線ベクトル / Tangent vector */ -const getQuadraticBezierTangent = ( +const $getQuadraticBezierTangent = ( t: number, s0: IPoint, s1: IPoint, @@ -81,16 +129,23 @@ const getQuadraticBezierTangent = ( /** * @description 二次ベジェ曲線を分割 + * Split a quadratic Bezier curve at a given parameter + * + * @param {IPoint} s0 - 始点 / Start point + * @param {IPoint} s1 - 制御点 / Control point + * @param {IPoint} s2 - 終点 / End point + * @param {number} t - 分割パラメータ / Split parameter + * @return {Array} 分割された2つの曲線 / Two split curves */ -const splitQuadraticBezier = ( +const $splitQuadraticBezier = ( s0: IPoint, s1: IPoint, s2: IPoint, t: number = 0.5 ): Array => { - const M0 = lerp(s0, s1, t); - const M1 = lerp(s1, s2, t); - const M01 = lerp(M0, M1, t); + const M0 = $lerp(s0, s1, t); + const M1 = $lerp(s1, s2, t); + const M01 = $lerp(M0, M1, t); return [ [s0, M0, M01], [M01, M1, s2] @@ -99,8 +154,15 @@ const splitQuadraticBezier = ( /** * @description ベジェ曲線を複数回分割 + * Split a Bezier curve multiple times + * + * @param {IPoint} s0 - 始点 / Start point + * @param {IPoint} s1 - 制御点 / Control point + * @param {IPoint} s2 - 終点 / End point + * @param {number} n - 分割回数 / Number of splits + * @return {Array} 分割されたセグメント配列 / Array of split segments */ -const splitBezierMultipleTimes = ( +const $splitBezierMultipleTimes = ( s0: IPoint, s1: IPoint, s2: IPoint, @@ -110,7 +172,7 @@ const splitBezierMultipleTimes = ( for (let i = 0; i < n; i++) { const newSegments: Array = []; for (const seg of segments) { - const splitted = splitQuadraticBezier(seg[0], seg[1], seg[2], 0.5); + const splitted = $splitQuadraticBezier(seg[0], seg[1], seg[2], 0.5); newSegments.push(splitted[0], splitted[1]); } segments = newSegments; @@ -120,8 +182,15 @@ const splitBezierMultipleTimes = ( /** * @description 2次ベジェのオフセットを計算 + * Calculate offset of a quadratic Bezier curve + * + * @param {IPoint} s0 - 始点 / Start point + * @param {IPoint} s1 - 制御点 / Control point + * @param {IPoint} s2 - 終点 / End point + * @param {number} offset - オフセット距離 / Offset distance + * @return {IPoint[]} オフセットされた点配列 / Array of offset points */ -const approximateOffsetQuadratic = ( +const $approximateOffsetQuadratic = ( s0: IPoint, s1: IPoint, s2: IPoint, @@ -131,9 +200,9 @@ const approximateOffsetQuadratic = ( const newPoints: IPoint[] = []; for (const t of tValues) { - const pos = getQuadraticBezierPoint(t, s0, s1, s2); - const tan = getQuadraticBezierTangent(t, s0, s1, s2); - const n = normalize({ "x": -tan.y, "y": tan.x }); + const pos = $getQuadraticBezierPoint(t, s0, s1, s2); + const tan = $getQuadraticBezierTangent(t, s0, s1, s2); + const n = $normalize({ "x": -tan.y, "y": tan.x }); newPoints.push({ "x": pos.x + n.x * offset, "y": pos.y + n.y * offset @@ -144,22 +213,29 @@ const approximateOffsetQuadratic = ( /** * @description カーブの矩形を計算(WebGL版のMeshCalculateCurveRectangleUseCaseと同じ) + * Calculate curve rectangle (same as WebGL MeshCalculateCurveRectangleUseCase) + * + * @param {IPoint} start_point - 始点 / Start point + * @param {IPoint} control_point - 制御点 / Control point + * @param {IPoint} end_point - 終点 / End point + * @param {number} thickness - 線の太さ(半分の値) / Half line thickness + * @return {IPath} カーブ矩形パス / Curve rectangle path */ -const calculateCurveRectangle = ( - startPoint: IPoint, - controlPoint: IPoint, - endPoint: IPoint, +const $calculateCurveRectangle = ( + start_point: IPoint, + control_point: IPoint, + end_point: IPoint, thickness: number ): IPath => { // WebGL版と同じ分割数(5回分割 = 32セグメント) - const segments = splitBezierMultipleTimes(startPoint, controlPoint, endPoint, 5); + const segments = $splitBezierMultipleTimes(start_point, control_point, end_point, 5); const leftCurves: Array = []; const rightCurves: Array = []; for (const seg of segments) { - leftCurves.push(approximateOffsetQuadratic(seg[0], seg[1], seg[2], +thickness)); - rightCurves.push(approximateOffsetQuadratic(seg[0], seg[1], seg[2], -thickness)); + leftCurves.push($approximateOffsetQuadratic(seg[0], seg[1], seg[2], +thickness)); + rightCurves.push($approximateOffsetQuadratic(seg[0], seg[1], seg[2], -thickness)); } // セグメント間の連続性を確保:各セグメントの終点を次のセグメントの始点に強制一致 @@ -208,42 +284,44 @@ const calculateCurveRectangle = ( /** * @description 直線の矩形を計算(WebGL版のMeshCalculateLineRectangleUseCaseと同じ) - * @param {IPoint} startPoint - 開始点 - * @param {IPoint} endPoint - 終了点 - * @param {number} thickness - 線の太さ(半分の値) - * @return {IPath} 矩形パス + * Calculate line rectangle (same as WebGL MeshCalculateLineRectangleUseCase) + * + * @param {IPoint} start_point - 開始点 / Start point + * @param {IPoint} end_point - 終了点 / End point + * @param {number} thickness - 線の太さ(半分の値) / Half line thickness + * @return {IPath} 矩形パス / Rectangle path */ -const calculateLineRectangle = ( - startPoint: IPoint, - endPoint: IPoint, +const $calculateLineRectangle = ( + start_point: IPoint, + end_point: IPoint, thickness: number ): IPath => { const vector: IPoint = { - "x": endPoint.x - startPoint.x, - "y": endPoint.y - startPoint.y + "x": end_point.x - start_point.x, + "y": end_point.y - start_point.y }; - const normal = calculateNormalVector(vector.x, vector.y, thickness); + const normal = $calculateNormalVector(vector.x, vector.y, thickness); const shiftedUpStart: IPoint = { - "x": startPoint.x + normal.x, - "y": startPoint.y + normal.y + "x": start_point.x + normal.x, + "y": start_point.y + normal.y }; const shiftedUpEnd: IPoint = { - "x": endPoint.x + normal.x, - "y": endPoint.y + normal.y + "x": end_point.x + normal.x, + "y": end_point.y + normal.y }; const shiftedDownEnd: IPoint = { - "x": endPoint.x - normal.x, - "y": endPoint.y - normal.y + "x": end_point.x - normal.x, + "y": end_point.y - normal.y }; const shiftedDownStart: IPoint = { - "x": startPoint.x - normal.x, - "y": startPoint.y - normal.y + "x": start_point.x - normal.x, + "y": start_point.y - normal.y }; return [ @@ -257,9 +335,16 @@ const calculateLineRectangle = ( /** * @description メッシュのパスの中で指定座標が含まれる線を探す + * Find paths overlapping with specified coordinates * WebGL版のMeshFindOverlappingPathsServiceと同じ + * + * @param {number} x - 中心x座標 / Center x coordinate + * @param {number} y - 中心y座標 / Center y coordinate + * @param {number} r - 半径 / Radius + * @param {IPath} paths - パスデータ / Path data + * @return {number[]} 重複する座標配列 / Array of overlapping coordinates */ -const findOverlappingPaths = ( +const $findOverlappingPaths = ( x: number, y: number, r: number, @@ -293,9 +378,14 @@ const findOverlappingPaths = ( /** * @description 矩形内に含まれてない座標を返却 + * Return coordinates that are not inside the rectangle * WebGL版のMeshIsPointInsideRectangleServiceと同じ + * + * @param {number[]} points - 判定対象の座標配列 / Array of points to test + * @param {IPath} rectangle - 矩形パス / Rectangle path + * @return {number[] | null} 矩形外の座標またはnull / Point outside rectangle or null */ -const findPointOutsideRectangle = ( +const $findPointOutsideRectangle = ( points: number[], rectangle: IPath ): number[] | null => { @@ -340,19 +430,27 @@ const findPointOutsideRectangle = ( /** * @description ベベル結合を生成(WebGL版のMeshGenerateCalculateBevelJoinUseCaseと同じ) + * Generate bevel join (same as WebGL MeshGenerateCalculateBevelJoinUseCase) + * + * @param {number} x - 結合点のx座標 / Join point x coordinate + * @param {number} y - 結合点のy座標 / Join point y coordinate + * @param {number} r - 半径 / Radius + * @param {IPath[]} rectangles - 矩形パス配列 / Array of rectangle paths + * @param {boolean} is_last - 最後の結合かどうか / Whether this is the last join + * @return {void} */ -const generateBevelJoin = ( +const $generateBevelJoin = ( x: number, y: number, r: number, rectangles: IPath[], - isLast: boolean = false + is_last: boolean = false ): void => { // WebGL版と同じ: isLastフラグでインデックスを切り替え - const indexA = isLast ? 0 : rectangles.length - 1; - const indexB = isLast ? rectangles.length - 1 : rectangles.length - 2; - const pathsA = findOverlappingPaths(x, y, r, rectangles[indexA]); - const pathsB = findOverlappingPaths(x, y, r, rectangles[indexB]); + const indexA = is_last ? 0 : rectangles.length - 1; + const indexB = is_last ? rectangles.length - 1 : rectangles.length - 2; + const pathsA = $findOverlappingPaths(x, y, r, rectangles[indexA]); + const pathsB = $findOverlappingPaths(x, y, r, rectangles[indexB]); // パスが並行であれば終了 if (pathsA[0] === pathsB[0] && pathsA[1] === pathsB[1] @@ -361,12 +459,12 @@ const generateBevelJoin = ( return; } - const pointA = findPointOutsideRectangle(pathsA, rectangles[indexB]); + const pointA = $findPointOutsideRectangle(pathsA, rectangles[indexB]); if (!pointA) { return; } - const pointB = findPointOutsideRectangle(pathsB, rectangles[indexA]); + const pointB = $findPointOutsideRectangle(pathsB, rectangles[indexA]); if (!pointB) { return; } @@ -381,26 +479,34 @@ const generateBevelJoin = ( /** * @description ラウンド結合を生成(WebGL版のMeshGenerateCalculateRoundJoinUseCaseと同じ) + * Generate round join (same as WebGL MeshGenerateCalculateRoundJoinUseCase) + * + * @param {number} x - 結合点のx座標 / Join point x coordinate + * @param {number} y - 結合点のy座標 / Join point y coordinate + * @param {number} r - 半径 / Radius + * @param {IPath[]} rectangles - 矩形パス配列 / Array of rectangle paths + * @param {boolean} is_last - 最後の結合かどうか / Whether this is the last join + * @return {void} */ -const generateRoundJoin = ( +const $generateRoundJoin = ( x: number, y: number, r: number, rectangles: IPath[], - isLast: boolean = false + is_last: boolean = false ): void => { // WebGL版と同じ: isLastフラグでインデックスを切り替え - const indexA = isLast ? 0 : rectangles.length - 1; - const indexB = isLast ? rectangles.length - 1 : rectangles.length - 2; - const pathsA = findOverlappingPaths(x, y, r, rectangles[indexA]); - const pathsB = findOverlappingPaths(x, y, r, rectangles[indexB]); + const indexA = is_last ? 0 : rectangles.length - 1; + const indexB = is_last ? rectangles.length - 1 : rectangles.length - 2; + const pathsA = $findOverlappingPaths(x, y, r, rectangles[indexA]); + const pathsB = $findOverlappingPaths(x, y, r, rectangles[indexB]); - const pointA = findPointOutsideRectangle(pathsA, rectangles[indexB]); + const pointA = $findPointOutsideRectangle(pathsA, rectangles[indexB]); if (!pointA) { return; } - const pointB = findPointOutsideRectangle(pathsB, rectangles[indexA]); + const pointB = $findPointOutsideRectangle(pathsB, rectangles[indexA]); if (!pointB) { return; } @@ -432,19 +538,28 @@ const generateRoundJoin = ( /** * @description マイター結合を生成(WebGL版のMeshGenerateCalculateMiterJoinUseCaseと同じ) + * Generate miter join (same as WebGL MeshGenerateCalculateMiterJoinUseCase) + * + * @param {IPoint} start_point - 結合点 / Join point + * @param {IPoint} end_point - 終了点 / End point + * @param {IPoint} prev_point - 前の点 / Previous point + * @param {number} r - 半径 / Radius + * @param {IPath[]} rectangles - 矩形パス配列 / Array of rectangle paths + * @param {boolean} is_last - 最後の結合かどうか / Whether this is the last join + * @return {void} */ -const generateMiterJoin = ( - startPoint: IPoint, - endPoint: IPoint, - prevPoint: IPoint, +const $generateMiterJoin = ( + start_point: IPoint, + end_point: IPoint, + prev_point: IPoint, r: number, rectangles: IPath[], - isLast: boolean = false + is_last: boolean = false ): void => { - const indexA = isLast ? 0 : rectangles.length - 1; - const indexB = isLast ? rectangles.length - 1 : rectangles.length - 2; - const pathsA = findOverlappingPaths(startPoint.x, startPoint.y, r, rectangles[indexA]); - const pathsB = findOverlappingPaths(startPoint.x, startPoint.y, r, rectangles[indexB]); + const indexA = is_last ? 0 : rectangles.length - 1; + const indexB = is_last ? rectangles.length - 1 : rectangles.length - 2; + const pathsA = $findOverlappingPaths(start_point.x, start_point.y, r, rectangles[indexA]); + const pathsB = $findOverlappingPaths(start_point.x, start_point.y, r, rectangles[indexB]); // パスが並行であれば終了 if (pathsA[0] === pathsB[0] && pathsA[1] === pathsB[1] @@ -453,26 +568,26 @@ const generateMiterJoin = ( return; } - const pointA = findPointOutsideRectangle(pathsA, rectangles[indexB]); + const pointA = $findPointOutsideRectangle(pathsA, rectangles[indexB]); if (!pointA) { return; } - const pointB = findPointOutsideRectangle(pathsB, rectangles[indexA]); + const pointB = $findPointOutsideRectangle(pathsB, rectangles[indexA]); if (!pointB) { return; } - const aVx = endPoint.x - startPoint.x; - const aVy = endPoint.y - startPoint.y; + const aVx = end_point.x - start_point.x; + const aVy = end_point.y - start_point.y; const lengthA = Math.hypot(aVx, aVy); const normalizeA = { "x": aVx / lengthA, "y": aVy / lengthA }; - const bVx = prevPoint.x - startPoint.x; - const bVy = prevPoint.y - startPoint.y; + const bVx = prev_point.x - start_point.x; + const bVy = prev_point.y - start_point.y; const lengthB = Math.hypot(bVx, bVy); const normalizeB = { "x": bVx / lengthB, @@ -485,7 +600,7 @@ const generateMiterJoin = ( const denom = d1x * d2y - d1y * d2x; if (denom === 0) { rectangles.splice(-1, 0, [ - startPoint.x, startPoint.y, false, + start_point.x, start_point.y, false, pointA[0], pointA[1], false, pointB[0], pointB[1], false ]); @@ -498,10 +613,10 @@ const generateMiterJoin = ( const iy = pointA[1] + t * d1y; rectangles.splice(-1, 0, [ - startPoint.x, startPoint.y, false, + start_point.x, start_point.y, false, pointA[0], pointA[1], false, ix, iy, false, - startPoint.x, startPoint.y, false, + start_point.x, start_point.y, false, pointB[0], pointB[1], false, ix, iy, false ]); @@ -509,8 +624,14 @@ const generateMiterJoin = ( /** * @description ラウンドキャップを生成(WebGL版のMeshGenerateCalculateRoundCapServiceと同じ) + * Generate round cap (same as WebGL MeshGenerateCalculateRoundCapService) + * + * @param {IPath} vertices - パス頂点 / Path vertices + * @param {number} thickness - 線の太さ(半分の値) / Half line thickness + * @param {IPath[]} rectangles - 矩形パス配列 / Array of rectangle paths + * @return {void} */ -const generateRoundCap = ( +const $generateRoundCap = ( vertices: IPath, thickness: number, rectangles: IPath[] @@ -558,8 +679,14 @@ const generateRoundCap = ( /** * @description スクエアキャップを生成(WebGL版のMeshGenerateCalculateSquareCapServiceと同じ) + * Generate square cap (same as WebGL MeshGenerateCalculateSquareCapService) + * + * @param {IPath} vertices - パス頂点 / Path vertices + * @param {number} thickness - 線の太さ(半分の値) / Half line thickness + * @param {IPath[]} rectangles - 矩形パス配列 / Array of rectangle paths + * @return {void} */ -const generateSquareCap = ( +const $generateSquareCap = ( vertices: IPath, thickness: number, rectangles: IPath[] @@ -621,9 +748,9 @@ const generateSquareCap = ( * @description 線の外周を算出して塗りのフォーマットで返却(WebGL版と同じ) * Calculate the outer circumference of the line and return it in the format of the fill * - * @param {IPath} vertices - パス頂点 [x, y, isCurve, ...] - * @param {number} thickness - 線の太さ(半分の値) - * @return {IPath[]} パス配列 + * @param {IPath} vertices - パス頂点 [x, y, isCurve, ...] / Path vertices + * @param {number} thickness - 線の太さ(半分の値) / Half line thickness + * @return {IPath[]} パス配列 / Array of paths */ export const generateStrokeOutline = (vertices: IPath, thickness: number): IPath[] => { @@ -660,11 +787,11 @@ export const generateStrokeOutline = (vertices: IPath, thickness: number): IPath endPoint.y = y; if (vertices[idx - 1] as boolean) { rectangles.push( - calculateCurveRectangle(startPoint, controlPoint, endPoint, thickness) + $calculateCurveRectangle(startPoint, controlPoint, endPoint, thickness) ); } else { rectangles.push( - calculateLineRectangle(startPoint, endPoint, thickness) + $calculateLineRectangle(startPoint, endPoint, thickness) ); } @@ -672,7 +799,7 @@ export const generateStrokeOutline = (vertices: IPath, thickness: number): IPath switch ($context.joints) { case 0: // bevel - generateBevelJoin( + $generateBevelJoin( startPoint.x, startPoint.y, thickness, rectangles ); break; @@ -680,14 +807,14 @@ export const generateStrokeOutline = (vertices: IPath, thickness: number): IPath case 1: // miter prevPoint.x = vertices[idx - 6] as number; prevPoint.y = vertices[idx - 5] as number; - generateMiterJoin( + $generateMiterJoin( startPoint, endPoint, prevPoint, thickness, rectangles ); break; case 2: // round - generateRoundJoin( + $generateRoundJoin( startPoint.x, startPoint.y, thickness, rectangles ); break; @@ -715,7 +842,7 @@ export const generateStrokeOutline = (vertices: IPath, thickness: number): IPath switch ($context.joints) { case 0: // bevel - generateBevelJoin( + $generateBevelJoin( startX, startY, thickness, rectangles, true ); break; @@ -727,14 +854,14 @@ export const generateStrokeOutline = (vertices: IPath, thickness: number): IPath endPoint.y = vertices[4] as number; prevPoint.x = vertices[vertices.length - 6] as number; prevPoint.y = vertices[vertices.length - 5] as number; - generateMiterJoin( + $generateMiterJoin( startPoint, endPoint, prevPoint, thickness, rectangles, true ); break; case 2: // round - generateRoundJoin( + $generateRoundJoin( startX, startY, thickness, rectangles, true ); break; @@ -749,13 +876,13 @@ export const generateStrokeOutline = (vertices: IPath, thickness: number): IPath switch ($context.caps) { case 1: // round - generateRoundCap( + $generateRoundCap( vertices, thickness, rectangles ); break; case 2: // square - generateSquareCap( + $generateSquareCap( vertices, thickness, rectangles ); break; @@ -771,9 +898,11 @@ export const generateStrokeOutline = (vertices: IPath, thickness: number): IPath /** * @description ストロークメッシュを生成(WebGL版のMeshStrokeGenerateUseCaseと同じ) - * @param {IPath[]} vertices - パス頂点配列 - * @param {number} thickness - 線の太さ(フル値、内部で/2される) - * @return {IPath[]} + * Generate stroke mesh (same as WebGL MeshStrokeGenerateUseCase) + * + * @param {IPath[]} vertices - パス頂点配列 / Array of path vertices + * @param {number} thickness - 線の太さ(フル値、内部で/2される) / Full line thickness (halved internally) + * @return {IPath[]} 塗りフォーマットのパス配列 / Array of fill-format paths */ export const generateStrokeMesh = (vertices: IPath[], thickness: number): IPath[] => { @@ -793,59 +922,3 @@ export const generateStrokeMesh = (vertices: IPath[], thickness: number): IPath[ return fillVertices; }; -/** - * @description IPoint[][]形式からストロークメッシュを生成(後方互換用) - * @param {IPoint[][]} paths - パス配列 - * @param {number} thickness - 線の太さ - * @return {Float32Array} - */ -export const generateStrokeMeshFromPoints = (paths: IPoint[][], thickness: number): Float32Array => -{ - const triangles: number[] = []; - - // WebGL版と同じ: 内部で半分にする - const halfThickness = thickness / 2; - - for (const path of paths) { - if (path.length < 2) { continue } - - // 各線分に対して矩形を生成 - for (let i = 0; i < path.length - 1; i++) { - const startPoint = path[i]; - const endPoint = path[i + 1]; - - const vector: IPoint = { - "x": endPoint.x - startPoint.x, - "y": endPoint.y - startPoint.y - }; - - const normal = calculateNormalVector(vector.x, vector.y, halfThickness); - - // 矩形の4頂点 - const p0x = startPoint.x + normal.x; - const p0y = startPoint.y + normal.y; - const p1x = endPoint.x + normal.x; - const p1y = endPoint.y + normal.y; - const p2x = endPoint.x - normal.x; - const p2y = endPoint.y - normal.y; - const p3x = startPoint.x - normal.x; - const p3y = startPoint.y - normal.y; - - // Triangle 1: p0, p1, p2 - triangles.push( - p0x, p0y, 0, 0, - p1x, p1y, 0, 0, - p2x, p2y, 0, 0 - ); - - // Triangle 2: p0, p2, p3 - triangles.push( - p0x, p0y, 0, 0, - p2x, p2y, 0, 0, - p3x, p3y, 0, 0 - ); - } - } - - return new Float32Array(triangles); -}; diff --git a/packages/webgpu/src/PathCommand.test.ts b/packages/webgpu/src/PathCommand.test.ts index fdba0c1b..95ba80b8 100644 --- a/packages/webgpu/src/PathCommand.test.ts +++ b/packages/webgpu/src/PathCommand.test.ts @@ -18,8 +18,8 @@ describe("PathCommand", () => pathCommand.lineTo(30, 40); pathCommand.beginPath(); - const paths = pathCommand.getAllPaths(); - expect(paths.length).toBe(0); + const vertices = pathCommand.getVerticesForStroke(); + expect(vertices.length).toBe(0); }); }); @@ -28,11 +28,12 @@ describe("PathCommand", () => it("should start a new path", () => { pathCommand.moveTo(10, 20); - const path = pathCommand.getCurrentPath(); + pathCommand.lineTo(30, 40); + const vertices = pathCommand.getVerticesForStroke(); - expect(path.length).toBe(1); - expect(path[0].x).toBe(10); - expect(path[0].y).toBe(20); + expect(vertices.length).toBe(1); + expect(vertices[0][0]).toBe(10); + expect(vertices[0][1]).toBe(20); }); it("should save previous path if long enough", () => @@ -42,9 +43,10 @@ describe("PathCommand", () => pathCommand.lineTo(10, 10); pathCommand.lineTo(0, 10); pathCommand.moveTo(100, 100); + pathCommand.lineTo(110, 100); - const paths = pathCommand.getAllPaths(); - expect(paths.length).toBe(2); + const vertices = pathCommand.getVerticesForStroke(); + expect(vertices.length).toBe(2); }); }); @@ -55,19 +57,23 @@ describe("PathCommand", () => pathCommand.moveTo(0, 0); pathCommand.lineTo(10, 20); - const path = pathCommand.getCurrentPath(); - expect(path.length).toBe(2); - expect(path[1].x).toBe(10); - expect(path[1].y).toBe(20); + const vertices = pathCommand.getVerticesForStroke(); + expect(vertices.length).toBe(1); + // Path: [0, 0, false, 10, 20, false] = 6 elements + expect(vertices[0].length).toBe(6); + expect(vertices[0][3]).toBe(10); + expect(vertices[0][4]).toBe(20); }); it("should ignore same point", () => { pathCommand.moveTo(10, 20); pathCommand.lineTo(10, 20); + pathCommand.lineTo(30, 40); - const path = pathCommand.getCurrentPath(); - expect(path.length).toBe(1); + const vertices = pathCommand.getVerticesForStroke(); + // moveTo + lineTo(same) + lineTo = 2 unique points (6 elements) + expect(vertices[0].length).toBe(6); }); it("should add multiple lines", () => @@ -77,8 +83,9 @@ describe("PathCommand", () => pathCommand.lineTo(10, 10); pathCommand.lineTo(0, 10); - const path = pathCommand.getCurrentPath(); - expect(path.length).toBe(4); + const vertices = pathCommand.getVerticesForStroke(); + // 4 points * 3 elements = 12 + expect(vertices[0].length).toBe(12); }); }); @@ -91,7 +98,6 @@ describe("PathCommand", () => const vertices = pathCommand.getVerticesForStroke(); expect(vertices.length).toBe(1); - // Path format: [x, y, isCurve, ...] // Should have: start(0,0,false), control(50,100,true), end(100,0,false) expect(vertices[0].length).toBe(9); }); @@ -104,9 +110,9 @@ describe("PathCommand", () => pathCommand.moveTo(0, 0); pathCommand.bezierCurveTo(10, 50, 90, 50, 100, 0); - const path = pathCommand.getCurrentPath(); + const vertices = pathCommand.getVerticesForStroke(); // Should have at least start point and some segments - expect(path.length).toBeGreaterThan(1); + expect(vertices[0].length).toBeGreaterThan(3); }); }); @@ -117,10 +123,9 @@ describe("PathCommand", () => pathCommand.moveTo(150, 100); pathCommand.arc(100, 100, 50); - const path = pathCommand.getCurrentPath(); + const vertices = pathCommand.getVerticesForStroke(); // Adaptive tessellation produces variable segment count - // 4 cubic bezier curves, each converted to quadratic segments - expect(path.length).toBeGreaterThan(1); + expect(vertices[0].length).toBeGreaterThan(3); }); it("should be centered at specified position", () => @@ -128,10 +133,10 @@ describe("PathCommand", () => pathCommand.moveTo(150, 100); pathCommand.arc(100, 100, 50); - const path = pathCommand.getCurrentPath(); - // First point should be at (100+50, 100) = (150, 100) from moveTo - expect(path[0].x).toBeCloseTo(150, 1); - expect(path[0].y).toBeCloseTo(100, 1); + const vertices = pathCommand.getVerticesForStroke(); + // First point should be at (150, 100) from moveTo + expect(vertices[0][0]).toBeCloseTo(150, 1); + expect(vertices[0][1]).toBeCloseTo(100, 1); }); }); @@ -144,10 +149,11 @@ describe("PathCommand", () => pathCommand.lineTo(100, 100); pathCommand.closePath(); - const path = pathCommand.getCurrentPath(); - const lastPoint = path[path.length - 1]; - expect(lastPoint.x).toBe(0); - expect(lastPoint.y).toBe(0); + const vertices = pathCommand.getVerticesForStroke(); + const path = vertices[0]; + // Last point should be (0, 0) + expect(path[path.length - 3]).toBe(0); + expect(path[path.length - 2]).toBe(0); }); it("should not add duplicate point if already at start", () => @@ -155,43 +161,15 @@ describe("PathCommand", () => pathCommand.moveTo(0, 0); pathCommand.lineTo(100, 0); pathCommand.lineTo(0, 0); - const lengthBefore = pathCommand.getCurrentPath().length; + const lengthBefore = pathCommand.getVerticesForStroke()[0].length; pathCommand.closePath(); - const lengthAfter = pathCommand.getCurrentPath().length; + const lengthAfter = pathCommand.getVerticesForStroke()[0].length; expect(lengthAfter).toBe(lengthBefore); }); }); - describe("generateVertices", () => - { - it("should generate triangle vertices for simple triangle", () => - { - pathCommand.moveTo(0, 0); - pathCommand.lineTo(100, 0); - pathCommand.lineTo(50, 100); - pathCommand.closePath(); - - const vertices = pathCommand.generateVertices(); - // 1 triangle = 6 values (3 points * 2 coords) - expect(vertices.length).toBeGreaterThanOrEqual(6); - }); - - it("should generate triangles using fan triangulation", () => - { - pathCommand.moveTo(0, 0); - pathCommand.lineTo(100, 0); - pathCommand.lineTo(100, 100); - pathCommand.lineTo(0, 100); - pathCommand.closePath(); - - const vertices = pathCommand.generateVertices(); - // Square with 5 points (including close) should generate multiple triangles - expect(vertices.length).toBeGreaterThan(6); - }); - }); - - describe("getAllPaths", () => + describe("getVerticesForStroke", () => { it("should return all paths", () => { @@ -205,25 +183,11 @@ describe("PathCommand", () => pathCommand.lineTo(110, 110); pathCommand.lineTo(100, 110); - const paths = pathCommand.getAllPaths(); - expect(paths.length).toBe(2); - expect(paths[0].length).toBe(4); - expect(paths[1].length).toBe(4); - }); - }); - - describe("setScale", () => - { - it("should adjust flatness threshold based on scale", () => - { - pathCommand.setScale(2.0); - // We can't directly access the private threshold, - // but we can verify the bezierCurveTo still works - pathCommand.moveTo(0, 0); - pathCommand.bezierCurveTo(10, 50, 90, 50, 100, 0); - - const path = pathCommand.getCurrentPath(); - expect(path.length).toBeGreaterThan(1); + const vertices = pathCommand.getVerticesForStroke(); + expect(vertices.length).toBe(2); + // Each path has 4 points * 3 elements = 12 + expect(vertices[0].length).toBe(12); + expect(vertices[1].length).toBe(12); }); }); @@ -235,11 +199,8 @@ describe("PathCommand", () => pathCommand.lineTo(30, 40); pathCommand.reset(); - const paths = pathCommand.getAllPaths(); - expect(paths.length).toBe(0); - - const currentPath = pathCommand.getCurrentPath(); - expect(currentPath.length).toBe(0); + const vertices = pathCommand.getVerticesForStroke(); + expect(vertices.length).toBe(0); }); }); }); diff --git a/packages/webgpu/src/PathCommand.ts b/packages/webgpu/src/PathCommand.ts index 52da4079..5d548eb2 100644 --- a/packages/webgpu/src/PathCommand.ts +++ b/packages/webgpu/src/PathCommand.ts @@ -1,9 +1,6 @@ import type { IPoint } from "./interface/IPoint"; import type { IPath } from "./interface/IPath"; -import { - adaptiveCubicToQuad, - calculateAdaptiveThreshold -} from "./BezierConverter/BezierConverter"; +import { adaptiveCubicToQuad } from "./BezierConverter/BezierConverter"; /** * @description WebGPU用パスコマンド(WebGL互換形式) @@ -15,11 +12,40 @@ import { */ export class PathCommand { + /** + * @description 現在のパスデータ + * Current path data array + */ private $currentPath: IPath; + + /** + * @description 確定済みパスの配列 + * Array of finalized path vertices + */ private $vertices: IPath[]; + + /** + * @description 現在のX座標 + * Current X coordinate + */ private $currentX: number; + + /** + * @description 現在のY座標 + * Current Y coordinate + */ private $currentY: number; + + /** + * @description サブパスの開始X座標 + * Start X coordinate of current sub-path + */ private $startX: number; + + /** + * @description サブパスの開始Y座標 + * Start Y coordinate of current sub-path + */ private $startY: number; /** @@ -114,17 +140,6 @@ export class PathCommand */ private $flatnessThreshold: number = 0.25; - /** - * @description フラットネス閾値を設定 - * Set flatness threshold for adaptive bezier tessellation - * @param {number} scale - 現在のスケール(行列のスケール成分) - * @return {void} - */ - setScale(scale: number): void - { - this.$flatnessThreshold = calculateAdaptiveThreshold(scale); - } - /** * @description 三次ベジェ曲線を二次ベジェ曲線に適応的に近似 * Adaptively approximate cubic bezier with quadratic beziers @@ -250,90 +265,6 @@ export class PathCommand return vertices; } - /** - * @description パスから頂点配列を生成(従来互換用・単純なfan triangulation) - * @return {Float32Array} - */ - generateVertices(): Float32Array - { - const vertices = this.$getVertices; - const triangles: number[] = []; - - for (const path of vertices) { - if (path.length < 9) { continue } // 最低3点(9要素)必要 - - // 点を抽出 - const points: IPoint[] = []; - for (let i = 0; i < path.length; i += 3) { - points.push({ "x": path[i] as number, "y": path[i + 1] as number }); - } - - // Fan triangulation - for (let i = 1; i < points.length - 1; i++) { - triangles.push( - points[0].x, points[0].y, - points[i].x, points[i].y, - points[i + 1].x, points[i + 1].y - ); - } - } - - return new Float32Array(triangles); - } - - /** - * @description 現在のパスを取得(ストローク用) - * @return {IPoint[]} - */ - getCurrentPath(): IPoint[] - { - const points: IPoint[] = []; - for (let i = 0; i < this.$currentPath.length; i += 3) { - points.push({ - "x": this.$currentPath[i] as number, - "y": this.$currentPath[i + 1] as number - }); - } - return points; - } - - /** - * @description すべてのパスを取得(ストローク用) - * @return {IPoint[][]} - */ - getAllPaths(): IPoint[][] - { - const allPaths: IPoint[][] = []; - - for (const path of this.$vertices) { - const points: IPoint[] = []; - for (let i = 0; i < path.length; i += 3) { - points.push({ - "x": path[i] as number, - "y": path[i + 1] as number - }); - } - if (points.length > 0) { - allPaths.push(points); - } - } - - if (this.$currentPath.length >= 3) { - const points: IPoint[] = []; - for (let i = 0; i < this.$currentPath.length; i += 3) { - points.push({ - "x": this.$currentPath[i] as number, - "y": this.$currentPath[i + 1] as number - }); - } - if (points.length > 0) { - allPaths.push(points); - } - } - - return allPaths; - } - /** * @description WebGL互換形式でパスを取得(ストローク用) * [x, y, isCurve, x, y, isCurve, ...] 形式 diff --git a/packages/webgpu/src/SamplerCache.test.ts b/packages/webgpu/src/SamplerCache.test.ts deleted file mode 100644 index ff829ec7..00000000 --- a/packages/webgpu/src/SamplerCache.test.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { - SamplerCache, - initSamplerCache, - getSamplerCache, - clearSamplerCache -} from "./SamplerCache"; - -// Mock the service modules -vi.mock("./SamplerCache/service/SamplerCacheGetOrCreateService", () => ({ - "execute": vi.fn((device, cache, minFilter, magFilter, addressModeU, addressModeV) => { - const key = `${minFilter}_${magFilter}_${addressModeU}_${addressModeV}`; - if (!cache.has(key)) { - const sampler = { "label": key } as unknown as GPUSampler; - cache.set(key, sampler); - } - return cache.get(key); - }) -})); - -vi.mock("./SamplerCache/service/SamplerCacheCreateCommonSamplersService", () => ({ - "execute": vi.fn((device, cache) => { - // Pre-create common samplers - cache.set("linear_linear_clamp-to-edge_clamp-to-edge", { "label": "linearClamp" }); - cache.set("nearest_nearest_clamp-to-edge_clamp-to-edge", { "label": "nearestClamp" }); - cache.set("linear_linear_repeat_repeat", { "label": "linearRepeat" }); - cache.set("nearest_nearest_repeat_repeat", { "label": "nearestRepeat" }); - }) -})); - -describe("SamplerCache", () => -{ - const createMockDevice = (): GPUDevice => - { - return { - "createSampler": vi.fn(() => ({ "label": "mockSampler" })) - } as unknown as GPUDevice; - }; - - beforeEach(() => - { - vi.clearAllMocks(); - clearSamplerCache(); - }); - - describe("SamplerCache class", () => - { - it("should create instance with device", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - expect(cache).toBeDefined(); - }); - - it("should pre-create common samplers on initialization", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const stats = cache.getStats(); - expect(stats.size).toBe(4); - }); - - describe("getOrCreate", () => - { - it("should get existing sampler from cache", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler1 = cache.getOrCreate("linear", "linear", "clamp-to-edge", "clamp-to-edge"); - const sampler2 = cache.getOrCreate("linear", "linear", "clamp-to-edge", "clamp-to-edge"); - - expect(sampler1).toBe(sampler2); - }); - - it("should create new sampler for new combination", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const initialSize = cache.getStats().size; - - cache.getOrCreate("linear", "nearest", "mirror-repeat", "clamp-to-edge"); - - expect(cache.getStats().size).toBe(initialSize + 1); - }); - }); - - describe("getLinearClamp", () => - { - it("should return linear clamp sampler", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler = cache.getLinearClamp(); - - expect(sampler).toBeDefined(); - }); - - it("should return same instance on multiple calls", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler1 = cache.getLinearClamp(); - const sampler2 = cache.getLinearClamp(); - - expect(sampler1).toBe(sampler2); - }); - }); - - describe("getNearestClamp", () => - { - it("should return nearest clamp sampler", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler = cache.getNearestClamp(); - - expect(sampler).toBeDefined(); - }); - }); - - describe("getLinearRepeat", () => - { - it("should return linear repeat sampler", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler = cache.getLinearRepeat(); - - expect(sampler).toBeDefined(); - }); - }); - - describe("getNearestRepeat", () => - { - it("should return nearest repeat sampler", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler = cache.getNearestRepeat(); - - expect(sampler).toBeDefined(); - }); - }); - - describe("getBySmoothRepeat", () => - { - it("should return linear clamp for smooth=true, repeat=false", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler = cache.getBySmoothRepeat(true, false); - const linearClamp = cache.getLinearClamp(); - - expect(sampler).toBe(linearClamp); - }); - - it("should return nearest clamp for smooth=false, repeat=false", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler = cache.getBySmoothRepeat(false, false); - const nearestClamp = cache.getNearestClamp(); - - expect(sampler).toBe(nearestClamp); - }); - - it("should return linear repeat for smooth=true, repeat=true", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler = cache.getBySmoothRepeat(true, true); - const linearRepeat = cache.getLinearRepeat(); - - expect(sampler).toBe(linearRepeat); - }); - - it("should return nearest repeat for smooth=false, repeat=true", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const sampler = cache.getBySmoothRepeat(false, true); - const nearestRepeat = cache.getNearestRepeat(); - - expect(sampler).toBe(nearestRepeat); - }); - }); - - describe("getStats", () => - { - it("should return cache size", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - const stats = cache.getStats(); - - expect(stats).toHaveProperty("size"); - expect(typeof stats.size).toBe("number"); - }); - }); - - describe("dispose", () => - { - it("should clear cache", () => - { - const device = createMockDevice(); - const cache = new SamplerCache(device); - - cache.dispose(); - - expect(cache.getStats().size).toBe(0); - }); - }); - }); - - describe("global functions", () => - { - describe("initSamplerCache", () => - { - it("should initialize global cache", () => - { - const device = createMockDevice(); - - initSamplerCache(device); - - expect(getSamplerCache()).not.toBeNull(); - }); - }); - - describe("getSamplerCache", () => - { - it("should return cache after initialization", () => - { - const device = createMockDevice(); - initSamplerCache(device); - - expect(getSamplerCache()).toBeInstanceOf(SamplerCache); - }); - }); - - describe("clearSamplerCache", () => - { - it("should dispose cache", () => - { - const device = createMockDevice(); - initSamplerCache(device); - const cache = getSamplerCache(); - - clearSamplerCache(); - - expect(cache!.getStats().size).toBe(0); - }); - - it("should not throw when cache is null", () => - { - expect(() => clearSamplerCache()).not.toThrow(); - }); - }); - }); -}); diff --git a/packages/webgpu/src/SamplerCache.ts b/packages/webgpu/src/SamplerCache.ts deleted file mode 100644 index c67cd168..00000000 --- a/packages/webgpu/src/SamplerCache.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { execute as samplerCacheGetOrCreateService } from "./SamplerCache/service/SamplerCacheGetOrCreateService"; -import { execute as samplerCacheCreateCommonSamplersService } from "./SamplerCache/service/SamplerCacheCreateCommonSamplersService"; - -export class SamplerCache -{ - private device: GPUDevice; - private cache: Map; - - constructor(device: GPUDevice) - { - this.device = device; - this.cache = new Map(); - - samplerCacheCreateCommonSamplersService(device, this.cache); - } - - getOrCreate( - minFilter: GPUFilterMode, - magFilter: GPUFilterMode, - addressModeU: GPUAddressMode, - addressModeV: GPUAddressMode - ): GPUSampler { - return samplerCacheGetOrCreateService( - this.device, - this.cache, - minFilter, - magFilter, - addressModeU, - addressModeV - ); - } - - getLinearClamp(): GPUSampler - { - return this.getOrCreate("linear", "linear", "clamp-to-edge", "clamp-to-edge"); - } - - getNearestClamp(): GPUSampler - { - return this.getOrCreate("nearest", "nearest", "clamp-to-edge", "clamp-to-edge"); - } - - getLinearRepeat(): GPUSampler - { - return this.getOrCreate("linear", "linear", "repeat", "repeat"); - } - - getNearestRepeat(): GPUSampler - { - return this.getOrCreate("nearest", "nearest", "repeat", "repeat"); - } - - getBySmoothRepeat(smooth: boolean, repeat: boolean): GPUSampler - { - const filter: GPUFilterMode = smooth ? "linear" : "nearest"; - const addressMode: GPUAddressMode = repeat ? "repeat" : "clamp-to-edge"; - return this.getOrCreate(filter, filter, addressMode, addressMode); - } - - getStats(): { size: number } - { - return { - "size": this.cache.size - }; - } - - dispose(): void - { - this.cache.clear(); - } -} - -let $samplerCache: SamplerCache | null = null; - -export const initSamplerCache = (device: GPUDevice): void => -{ - $samplerCache = new SamplerCache(device); -}; - -export const getSamplerCache = (): SamplerCache | null => -{ - return $samplerCache; -}; - -export const clearSamplerCache = (): void => -{ - if ($samplerCache) { - $samplerCache.dispose(); - } -}; diff --git a/packages/webgpu/src/SamplerCache/service/SamplerCacheCreateCommonSamplersService.test.ts b/packages/webgpu/src/SamplerCache/service/SamplerCacheCreateCommonSamplersService.test.ts deleted file mode 100644 index 3f74a905..00000000 --- a/packages/webgpu/src/SamplerCache/service/SamplerCacheCreateCommonSamplersService.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { execute } from "./SamplerCacheCreateCommonSamplersService"; - -describe("SamplerCacheCreateCommonSamplersService", () => -{ - const createMockDevice = () => - { - let samplerId = 0; - return { - "createSampler": vi.fn(() => ({ "id": ++samplerId })) - } as unknown as GPUDevice; - }; - - it("should create 5 common samplers", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - - expect(cache.size).toBe(5); - }); - - it("should create linear clamp sampler", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - - expect(cache.has("linear_linear_clamp-to-edge_clamp-to-edge")).toBe(true); - }); - - it("should create nearest clamp sampler", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - - expect(cache.has("nearest_nearest_clamp-to-edge_clamp-to-edge")).toBe(true); - }); - - it("should create linear repeat sampler", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - - expect(cache.has("linear_linear_repeat_repeat")).toBe(true); - }); - - it("should create nearest repeat sampler", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - - expect(cache.has("nearest_nearest_repeat_repeat")).toBe(true); - }); - - it("should create linear mirror repeat sampler", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - - expect(cache.has("linear_linear_mirror-repeat_mirror-repeat")).toBe(true); - }); - - it("should not overwrite existing samplers", () => - { - const device = createMockDevice(); - const cache = new Map(); - const existingSampler = { "id": "existing" } as unknown as GPUSampler; - cache.set("linear_linear_clamp-to-edge_clamp-to-edge", existingSampler); - - execute(device, cache); - - expect(cache.get("linear_linear_clamp-to-edge_clamp-to-edge")).toBe(existingSampler); - }); - - it("should call createSampler with correct parameters", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - - // Verify first call (linear clamp) - expect(device.createSampler).toHaveBeenCalledWith({ - "minFilter": "linear", - "magFilter": "linear", - "addressModeU": "clamp-to-edge", - "addressModeV": "clamp-to-edge" - }); - - // Verify nearest clamp was called - expect(device.createSampler).toHaveBeenCalledWith({ - "minFilter": "nearest", - "magFilter": "nearest", - "addressModeU": "clamp-to-edge", - "addressModeV": "clamp-to-edge" - }); - }); - - it("should only call createSampler 5 times for empty cache", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - - expect(device.createSampler).toHaveBeenCalledTimes(5); - }); - - it("should be idempotent when called multiple times", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache); - const sizeAfterFirst = cache.size; - const callsAfterFirst = (device.createSampler as any).mock.calls.length; - - execute(device, cache); - - expect(cache.size).toBe(sizeAfterFirst); - expect(device.createSampler).toHaveBeenCalledTimes(callsAfterFirst); - }); -}); diff --git a/packages/webgpu/src/SamplerCache/service/SamplerCacheCreateCommonSamplersService.ts b/packages/webgpu/src/SamplerCache/service/SamplerCacheCreateCommonSamplersService.ts deleted file mode 100644 index c7b27a3b..00000000 --- a/packages/webgpu/src/SamplerCache/service/SamplerCacheCreateCommonSamplersService.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { execute as samplerCacheGenerateKeyService } from "./SamplerCacheGenerateKeyService"; - -/** - * @description 頻繁に使用されるサンプラーを事前に作成 - * Pre-create commonly used samplers - * - * @param {GPUDevice} device - * @param {Map} cache - * @return {void} - * @method - * @protected - */ -export const execute = ( - device: GPUDevice, - cache: Map -): void => { - const createAndCache = ( - minFilter: GPUFilterMode, - magFilter: GPUFilterMode, - addressModeU: GPUAddressMode, - addressModeV: GPUAddressMode - ): void => { - const key = samplerCacheGenerateKeyService(minFilter, magFilter, addressModeU, addressModeV); - - if (!cache.has(key)) { - const sampler = device.createSampler({ - minFilter, - magFilter, - addressModeU, - addressModeV - }); - cache.set(key, sampler); - } - }; - - // リニアクランプ(最も一般的) - createAndCache("linear", "linear", "clamp-to-edge", "clamp-to-edge"); - - // ニアレストクランプ - createAndCache("nearest", "nearest", "clamp-to-edge", "clamp-to-edge"); - - // リニアリピート - createAndCache("linear", "linear", "repeat", "repeat"); - - // ニアレストリピート - createAndCache("nearest", "nearest", "repeat", "repeat"); - - // リニアミラーリピート - createAndCache("linear", "linear", "mirror-repeat", "mirror-repeat"); -}; diff --git a/packages/webgpu/src/SamplerCache/service/SamplerCacheGenerateKeyService.test.ts b/packages/webgpu/src/SamplerCache/service/SamplerCacheGenerateKeyService.test.ts deleted file mode 100644 index f36950b6..00000000 --- a/packages/webgpu/src/SamplerCache/service/SamplerCacheGenerateKeyService.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { execute } from "./SamplerCacheGenerateKeyService"; - -describe("SamplerCacheGenerateKeyService", () => -{ - it("should generate key with all linear filters", () => - { - const result = execute("linear", "linear", "clamp-to-edge", "clamp-to-edge"); - expect(result).toBe("linear_linear_clamp-to-edge_clamp-to-edge"); - }); - - it("should generate key with all nearest filters", () => - { - const result = execute("nearest", "nearest", "repeat", "repeat"); - expect(result).toBe("nearest_nearest_repeat_repeat"); - }); - - it("should generate key with mixed filters", () => - { - const result = execute("linear", "nearest", "clamp-to-edge", "repeat"); - expect(result).toBe("linear_nearest_clamp-to-edge_repeat"); - }); - - it("should generate unique keys for different configurations", () => - { - const key1 = execute("linear", "linear", "clamp-to-edge", "clamp-to-edge"); - const key2 = execute("nearest", "nearest", "clamp-to-edge", "clamp-to-edge"); - const key3 = execute("linear", "linear", "repeat", "repeat"); - - expect(key1).not.toBe(key2); - expect(key1).not.toBe(key3); - expect(key2).not.toBe(key3); - }); - - it("should generate same key for same configuration", () => - { - const key1 = execute("linear", "nearest", "repeat", "mirror-repeat"); - const key2 = execute("linear", "nearest", "repeat", "mirror-repeat"); - - expect(key1).toBe(key2); - }); - - it("should handle mirror-repeat address mode", () => - { - const result = execute("linear", "linear", "mirror-repeat", "mirror-repeat"); - expect(result).toBe("linear_linear_mirror-repeat_mirror-repeat"); - }); - - it("should differentiate address modes U and V", () => - { - const key1 = execute("linear", "linear", "repeat", "clamp-to-edge"); - const key2 = execute("linear", "linear", "clamp-to-edge", "repeat"); - - expect(key1).not.toBe(key2); - }); -}); diff --git a/packages/webgpu/src/SamplerCache/service/SamplerCacheGenerateKeyService.ts b/packages/webgpu/src/SamplerCache/service/SamplerCacheGenerateKeyService.ts deleted file mode 100644 index 06c33e10..00000000 --- a/packages/webgpu/src/SamplerCache/service/SamplerCacheGenerateKeyService.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * @description サンプラーのキーを生成 - * Generate sampler cache key - * - * @param {GPUFilterMode} minFilter - * @param {GPUFilterMode} magFilter - * @param {GPUAddressMode} addressModeU - * @param {GPUAddressMode} addressModeV - * @return {string} - * @method - * @protected - */ -export const execute = ( - minFilter: GPUFilterMode, - magFilter: GPUFilterMode, - addressModeU: GPUAddressMode, - addressModeV: GPUAddressMode -): string => { - return `${minFilter}_${magFilter}_${addressModeU}_${addressModeV}`; -}; diff --git a/packages/webgpu/src/SamplerCache/service/SamplerCacheGetOrCreateService.test.ts b/packages/webgpu/src/SamplerCache/service/SamplerCacheGetOrCreateService.test.ts deleted file mode 100644 index 1f77798e..00000000 --- a/packages/webgpu/src/SamplerCache/service/SamplerCacheGetOrCreateService.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { execute } from "./SamplerCacheGetOrCreateService"; - -describe("SamplerCacheGetOrCreateService", () => -{ - const createMockDevice = () => - { - return { - "createSampler": vi.fn((descriptor) => ({ ...descriptor, "label": "mock-sampler" })) - } as unknown as GPUDevice; - }; - - it("should return cached sampler if exists", () => - { - const device = createMockDevice(); - const cache = new Map(); - const existingSampler = { "label": "existing" } as unknown as GPUSampler; - - // Pre-populate cache with correct key format (underscore separated) - cache.set("linear_linear_clamp-to-edge_clamp-to-edge", existingSampler); - - const result = execute(device, cache, "linear", "linear", "clamp-to-edge", "clamp-to-edge"); - - expect(result).toBe(existingSampler); - expect(device.createSampler).not.toHaveBeenCalled(); - }); - - it("should create new sampler if not cached", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache, "linear", "linear", "clamp-to-edge", "clamp-to-edge"); - - expect(device.createSampler).toHaveBeenCalledWith({ - "minFilter": "linear", - "magFilter": "linear", - "addressModeU": "clamp-to-edge", - "addressModeV": "clamp-to-edge" - }); - }); - - it("should cache newly created sampler", () => - { - const device = createMockDevice(); - const cache = new Map(); - - const result = execute(device, cache, "nearest", "nearest", "repeat", "repeat"); - - expect(cache.has("nearest_nearest_repeat_repeat")).toBe(true); - expect(cache.get("nearest_nearest_repeat_repeat")).toBe(result); - }); - - it("should generate correct cache key", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache, "linear", "nearest", "repeat", "mirror-repeat"); - - expect(cache.has("linear_nearest_repeat_mirror-repeat")).toBe(true); - }); - - it("should return same sampler for same parameters", () => - { - const device = createMockDevice(); - const cache = new Map(); - - const result1 = execute(device, cache, "linear", "linear", "clamp-to-edge", "clamp-to-edge"); - const result2 = execute(device, cache, "linear", "linear", "clamp-to-edge", "clamp-to-edge"); - - expect(result1).toBe(result2); - expect(device.createSampler).toHaveBeenCalledTimes(1); - }); - - it("should create different samplers for different parameters", () => - { - const device = createMockDevice(); - const cache = new Map(); - - const result1 = execute(device, cache, "linear", "linear", "clamp-to-edge", "clamp-to-edge"); - const result2 = execute(device, cache, "nearest", "nearest", "repeat", "repeat"); - - expect(result1).not.toBe(result2); - expect(device.createSampler).toHaveBeenCalledTimes(2); - }); - - it("should handle all filter modes", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache, "linear", "linear", "clamp-to-edge", "clamp-to-edge"); - execute(device, cache, "nearest", "nearest", "clamp-to-edge", "clamp-to-edge"); - - expect(cache.size).toBe(2); - }); - - it("should handle all address modes", () => - { - const device = createMockDevice(); - const cache = new Map(); - - execute(device, cache, "linear", "linear", "clamp-to-edge", "clamp-to-edge"); - execute(device, cache, "linear", "linear", "repeat", "repeat"); - execute(device, cache, "linear", "linear", "mirror-repeat", "mirror-repeat"); - - expect(cache.size).toBe(3); - }); -}); diff --git a/packages/webgpu/src/SamplerCache/service/SamplerCacheGetOrCreateService.ts b/packages/webgpu/src/SamplerCache/service/SamplerCacheGetOrCreateService.ts deleted file mode 100644 index a0d54691..00000000 --- a/packages/webgpu/src/SamplerCache/service/SamplerCacheGetOrCreateService.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { execute as samplerCacheGenerateKeyService } from "./SamplerCacheGenerateKeyService"; - -/** - * @description サンプラーを取得または作成 - * Get or create sampler - * - * @param {GPUDevice} device - * @param {Map} cache - * @param {GPUFilterMode} minFilter - * @param {GPUFilterMode} magFilter - * @param {GPUAddressMode} addressModeU - * @param {GPUAddressMode} addressModeV - * @return {GPUSampler} - * @method - * @protected - */ -export const execute = ( - device: GPUDevice, - cache: Map, - minFilter: GPUFilterMode, - magFilter: GPUFilterMode, - addressModeU: GPUAddressMode, - addressModeV: GPUAddressMode -): GPUSampler => { - const key = samplerCacheGenerateKeyService(minFilter, magFilter, addressModeU, addressModeV); - - const cached = cache.get(key); - if (cached) { - return cached; - } - - const sampler = device.createSampler({ - minFilter, - magFilter, - addressModeU, - addressModeV - }); - - cache.set(key, sampler); - return sampler; -}; diff --git a/packages/webgpu/src/Shader/BlendModeShader.test.ts b/packages/webgpu/src/Shader/BlendModeShader.test.ts deleted file mode 100644 index 6e8664c7..00000000 --- a/packages/webgpu/src/Shader/BlendModeShader.test.ts +++ /dev/null @@ -1,365 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { BlendModeShader } from "./BlendModeShader"; - -describe("BlendModeShader", () => -{ - describe("getVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = BlendModeShader.getVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = BlendModeShader.getVertexShader(); - - expect(shader).toContain("@vertex"); - }); - - it("should define VertexInput struct", () => - { - const shader = BlendModeShader.getVertexShader(); - - expect(shader).toContain("struct VertexInput"); - }); - - it("should define VertexOutput struct", () => - { - const shader = BlendModeShader.getVertexShader(); - - expect(shader).toContain("struct VertexOutput"); - }); - - it("should include position in VertexInput", () => - { - const shader = BlendModeShader.getVertexShader(); - - expect(shader).toContain("@location(0) position: vec2"); - }); - - it("should include texCoord in VertexInput", () => - { - const shader = BlendModeShader.getVertexShader(); - - expect(shader).toContain("@location(1) texCoord: vec2"); - }); - - it("should output position with @builtin(position)", () => - { - const shader = BlendModeShader.getVertexShader(); - - expect(shader).toContain("@builtin(position) position: vec4"); - }); - }); - - describe("getMultiplyShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlendModeShader.getMultiplyShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlendModeShader.getMultiplyShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should define BlendUniforms struct", () => - { - const shader = BlendModeShader.getMultiplyShader(); - - expect(shader).toContain("struct BlendUniforms"); - }); - - it("should include colorTransform uniform", () => - { - const shader = BlendModeShader.getMultiplyShader(); - - expect(shader).toContain("colorTransform: vec4"); - }); - - it("should include addColor uniform", () => - { - const shader = BlendModeShader.getMultiplyShader(); - - expect(shader).toContain("addColor: vec4"); - }); - - it("should have two texture bindings for dst and src", () => - { - const shader = BlendModeShader.getMultiplyShader(); - - expect(shader).toContain("var texture0: texture_2d"); - expect(shader).toContain("var texture1: texture_2d"); - }); - - it("should implement multiply blend formula", () => - { - const shader = BlendModeShader.getMultiplyShader(); - - expect(shader).toContain("src * dst"); - }); - }); - - describe("getScreenShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlendModeShader.getScreenShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlendModeShader.getScreenShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should define BlendUniforms struct", () => - { - const shader = BlendModeShader.getScreenShader(); - - expect(shader).toContain("struct BlendUniforms"); - }); - - it("should implement screen blend formula", () => - { - const shader = BlendModeShader.getScreenShader(); - expect(shader).toContain("srcRgb + dstRgb - srcRgb * dstRgb"); - }); - }); - - describe("getLightenShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlendModeShader.getLightenShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlendModeShader.getLightenShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should implement lighten blend using max", () => - { - const shader = BlendModeShader.getLightenShader(); - - expect(shader).toContain("max(srcRgb, dstRgb)"); - }); - - it("should handle zero alpha cases", () => - { - const shader = BlendModeShader.getLightenShader(); - - expect(shader).toContain("if (src.a == 0.0) { return dst; }"); - expect(shader).toContain("if (dst.a == 0.0) { return src; }"); - }); - - it("should unpremultiply colors for blending", () => - { - const shader = BlendModeShader.getLightenShader(); - - expect(shader).toContain("srcRgb = src.rgb / src.a"); - expect(shader).toContain("dstRgb = dst.rgb / dst.a"); - }); - }); - - describe("getDarkenShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlendModeShader.getDarkenShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlendModeShader.getDarkenShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should implement darken blend using min", () => - { - const shader = BlendModeShader.getDarkenShader(); - - expect(shader).toContain("min(srcRgb, dstRgb)"); - }); - - it("should handle zero alpha cases", () => - { - const shader = BlendModeShader.getDarkenShader(); - - expect(shader).toContain("if (src.a == 0.0) { return dst; }"); - expect(shader).toContain("if (dst.a == 0.0) { return src; }"); - }); - }); - - describe("getOverlayShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlendModeShader.getOverlayShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlendModeShader.getOverlayShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should implement branchless overlay blend with step on dst", () => - { - const shader = BlendModeShader.getOverlayShader(); - - expect(shader).toContain("step(vec3(0.5), dstRgb)"); - expect(shader).toContain("mix(lo, hi, s)"); - }); - }); - - describe("getHardLightShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlendModeShader.getHardLightShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlendModeShader.getHardLightShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should implement branchless hard light blend with step on src", () => - { - const shader = BlendModeShader.getHardLightShader(); - - expect(shader).toContain("step(vec3(0.5), srcRgb)"); - expect(shader).toContain("mix(lo, hi, s)"); - }); - }); - - describe("getDifferenceShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlendModeShader.getDifferenceShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlendModeShader.getDifferenceShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should implement difference blend using abs", () => - { - const shader = BlendModeShader.getDifferenceShader(); - - expect(shader).toContain("abs(srcRgb - dstRgb)"); - }); - }); - - describe("getSubtractShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = BlendModeShader.getSubtractShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = BlendModeShader.getSubtractShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should implement subtract blend (dst - src)", () => - { - const shader = BlendModeShader.getSubtractShader(); - - expect(shader).toContain("dstRgb - srcRgb"); - }); - - it("should handle zero alpha cases", () => - { - const shader = BlendModeShader.getSubtractShader(); - - expect(shader).toContain("if (src.a == 0.0) { return dst; }"); - expect(shader).toContain("if (dst.a == 0.0) { return src; }"); - }); - }); - - describe("common shader properties", () => - { - const shaders = [ - { name: "Multiply", fn: BlendModeShader.getMultiplyShader }, - { name: "Screen", fn: BlendModeShader.getScreenShader }, - { name: "Lighten", fn: BlendModeShader.getLightenShader }, - { name: "Darken", fn: BlendModeShader.getDarkenShader }, - { name: "Overlay", fn: BlendModeShader.getOverlayShader }, - { name: "HardLight", fn: BlendModeShader.getHardLightShader }, - { name: "Difference", fn: BlendModeShader.getDifferenceShader }, - { name: "Subtract", fn: BlendModeShader.getSubtractShader } - ]; - - shaders.forEach(({ name, fn }) => - { - it(`${name} shader should include textureSample calls`, () => - { - const shader = fn(); - - expect(shader).toContain("textureSample"); - }); - - it(`${name} shader should apply color transform`, () => - { - const shader = fn(); - - expect(shader).toContain("uniforms.colorTransform"); - }); - - it(`${name} shader should have sampler binding`, () => - { - const shader = fn(); - - expect(shader).toContain("var sampler0: sampler"); - }); - }); - }); -}); diff --git a/packages/webgpu/src/Shader/BlendModeShader.ts b/packages/webgpu/src/Shader/BlendModeShader.ts deleted file mode 100644 index f709fdb6..00000000 --- a/packages/webgpu/src/Shader/BlendModeShader.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { BlendModeVertex } from "./wgsl/vertex/FilterVertex"; -import { - MultiplyBlendFragment, - ScreenBlendFragment, - LightenBlendFragment, - DarkenBlendFragment, - OverlayBlendFragment, - HardLightBlendFragment, - DifferenceBlendFragment, - SubtractBlendFragment -} from "./wgsl/fragment/BlendFragment"; - -/** - * @description WebGPU用ブレンドモードシェーダー - * Blend mode shaders for WebGPU - */ -export class BlendModeShader -{ - /** - * @description ブレンドモード用の頂点シェーダー - * @return {string} - */ - static getVertexShader(): string - { - return BlendModeVertex; - } - - /** - * @description Multiplyブレンド用のフラグメントシェーダー - * @return {string} - */ - static getMultiplyShader(): string - { - return MultiplyBlendFragment; - } - - /** - * @description Screenブレンド用のフラグメントシェーダー - * @return {string} - */ - static getScreenShader(): string - { - return ScreenBlendFragment; - } - - /** - * @description Lightenブレンド用のフラグメントシェーダー - * @return {string} - */ - static getLightenShader(): string - { - return LightenBlendFragment; - } - - /** - * @description Darkenブレンド用のフラグメントシェーダー - * @return {string} - */ - static getDarkenShader(): string - { - return DarkenBlendFragment; - } - - /** - * @description Overlayブレンド用のフラグメントシェーダー - * @return {string} - */ - static getOverlayShader(): string - { - return OverlayBlendFragment; - } - - /** - * @description Hard Lightブレンド用のフラグメントシェーダー - * @return {string} - */ - static getHardLightShader(): string - { - return HardLightBlendFragment; - } - - /** - * @description Differenceブレンド用のフラグメントシェーダー - * @return {string} - */ - static getDifferenceShader(): string - { - return DifferenceBlendFragment; - } - - /** - * @description Subtractブレンド用のフラグメントシェーダー - * @return {string} - */ - static getSubtractShader(): string - { - return SubtractBlendFragment; - } -} diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.test.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.test.ts deleted file mode 100644 index d8d3922d..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { execute } from "./GradientLUTCalculateResolutionService"; -import { describe, expect, it } from "vitest"; - -describe("GradientLUTCalculateResolutionService.ts method test", () => -{ - it("test case - 2 stops returns 64", () => - { - const result = execute(2); - - expect(result).toBe(64); - }); - - it("test case - 3 stops returns 128", () => - { - const result = execute(3); - - expect(result).toBe(128); - }); - - it("test case - 4 stops returns 128", () => - { - const result = execute(4); - - expect(result).toBe(128); - }); - - it("test case - 5 stops returns 256", () => - { - const result = execute(5); - - expect(result).toBe(256); - }); - - it("test case - 8 stops returns 256", () => - { - const result = execute(8); - - expect(result).toBe(256); - }); - - it("test case - 9 stops returns 512", () => - { - const result = execute(9); - - expect(result).toBe(512); - }); - - it("test case - respects minResolution parameter", () => - { - const result = execute(2, 128); - - expect(result).toBe(128); - }); - - it("test case - respects maxResolution parameter", () => - { - const result = execute(10, 64, 256); - - expect(result).toBe(256); - }); -}); diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.ts deleted file mode 100644 index cbf2193a..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTCalculateResolutionService.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @description グラデーションストップ数に応じた適応的解像度を計算 - * Calculate adaptive resolution based on number of gradient stops - * - * @param {number} stopsCount - グラデーションストップの数 - * @param {number} [minResolution=64] - 最小解像度 - * @param {number} [maxResolution=512] - 最大解像度 - * @return {number} - * @method - * @protected - */ -export const execute = ( - stopsCount: number, - minResolution: number = 64, - maxResolution: number = 512 -): number => { - - // ストップ数に応じて解像度を調整 - // 2ストップ: 64px - // 3-4ストップ: 128px - // 5-8ストップ: 256px - // 9以上: 512px - if (stopsCount <= 2) { - return Math.max(minResolution, 64); - } - - if (stopsCount <= 4) { - return Math.min(maxResolution, Math.max(minResolution, 128)); - } - - if (stopsCount <= 8) { - return Math.min(maxResolution, Math.max(minResolution, 256)); - } - - return maxResolution; -}; diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTGeneratePixelsService.test.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTGeneratePixelsService.test.ts deleted file mode 100644 index d4f808f0..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTGeneratePixelsService.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { execute } from "./GradientLUTGeneratePixelsService"; -import type { IGradientStop } from "./GradientLUTParseStopsService"; -import { describe, expect, it } from "vitest"; - -describe("GradientLUTGeneratePixelsService.ts method test", () => -{ - it("test case - empty stops returns empty array", () => - { - const stops: IGradientStop[] = []; - - const result = execute(stops, 64, 0); - - expect(result.length).toBe(64 * 4); - expect(result.every(v => v === 0)).toBe(true); - }); - - it("test case - single stop fills with same color", () => - { - const stops: IGradientStop[] = [ - { ratio: 0.5, r: 1, g: 0, b: 0, a: 1 } - ]; - - const result = execute(stops, 4, 0); - - expect(result.length).toBe(16); - // 全ピクセルが同じ色(赤) - for (let i = 0; i < 4; i++) { - expect(result[i * 4]).toBe(255); // r - expect(result[i * 4 + 1]).toBe(0); // g - expect(result[i * 4 + 2]).toBe(0); // b - expect(result[i * 4 + 3]).toBe(255); // a - } - }); - - it("test case - two stops gradient from red to blue", () => - { - const stops: IGradientStop[] = [ - { ratio: 0, r: 1, g: 0, b: 0, a: 1 }, - { ratio: 1, r: 0, g: 0, b: 1, a: 1 } - ]; - - const result = execute(stops, 3, 0); - - // ピクセル0: 赤 - expect(result[0]).toBe(255); - expect(result[1]).toBe(0); - expect(result[2]).toBe(0); - - // ピクセル1: 紫(中間) - expect(result[4]).toBe(128); - expect(result[5]).toBe(0); - expect(result[6]).toBe(128); - - // ピクセル2: 青 - expect(result[8]).toBe(0); - expect(result[9]).toBe(0); - expect(result[10]).toBe(255); - }); - - it("test case - alpha channel interpolation", () => - { - const stops: IGradientStop[] = [ - { ratio: 0, r: 1, g: 1, b: 1, a: 0 }, - { ratio: 1, r: 1, g: 1, b: 1, a: 1 } - ]; - - const result = execute(stops, 3, 0); - - // ピクセル0: alpha=0 - expect(result[3]).toBe(0); - - // ピクセル1: alpha=0.5 - expect(result[7]).toBe(128); - - // ピクセル2: alpha=1 - expect(result[11]).toBe(255); - }); - - it("test case - three stops gradient", () => - { - const stops: IGradientStop[] = [ - { ratio: 0, r: 1, g: 0, b: 0, a: 1 }, // 赤 - { ratio: 0.5, r: 0, g: 1, b: 0, a: 1 }, // 緑 - { ratio: 1, r: 0, g: 0, b: 1, a: 1 } // 青 - ]; - - const result = execute(stops, 5, 0); - - // ピクセル0: 赤 - expect(result[0]).toBe(255); - expect(result[1]).toBe(0); - expect(result[2]).toBe(0); - - // ピクセル2: 緑 - expect(result[8]).toBe(0); - expect(result[9]).toBe(255); - expect(result[10]).toBe(0); - - // ピクセル4: 青 - expect(result[16]).toBe(0); - expect(result[17]).toBe(0); - expect(result[18]).toBe(255); - }); -}); diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTGeneratePixelsService.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTGeneratePixelsService.ts deleted file mode 100644 index 30dea70a..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTGeneratePixelsService.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { IGradientStop } from "../../../interface/IGradientStop"; -import { execute as gradientLUTInterpolateColorService } from "./GradientLUTInterpolateColorService"; - -/** - * @description グラデーションLUTのピクセルデータを生成 - * Generate pixel data for gradient LUT - * - * @param {IGradientStop[]} stops - ソート済みのグラデーションストップ - * @param {number} resolution - LUTの解像度(ピクセル数) - * @param {number} interpolation - 0: RGB, 1: Linear RGB - * @return {Uint8Array} - * @method - * @protected - */ -export const execute = ( - stops: IGradientStop[], - resolution: number, - interpolation: number -): Uint8Array => { - - const pixels = new Uint8Array(resolution * 4); - - if (stops.length === 0) { - return pixels; - } - - if (stops.length === 1) { - // 単一ストップの場合は全体を同じ色で塗る - const stop = stops[0]; - for (let i = 0; i < resolution; i++) { - const offset = i * 4; - pixels[offset] = Math.round(stop.r * 255); - pixels[offset + 1] = Math.round(stop.g * 255); - pixels[offset + 2] = Math.round(stop.b * 255); - pixels[offset + 3] = Math.round(stop.a * 255); - } - return pixels; - } - - for (let i = 0; i < resolution; i++) { - - const ratio = i / (resolution - 1); - - // 該当するストップ区間を見つける - let startStopIndex = 0; - for (let j = 0; j < stops.length - 1; j++) { - if (ratio >= stops[j].ratio && ratio <= stops[j + 1].ratio) { - startStopIndex = j; - break; - } - if (ratio > stops[j + 1].ratio) { - startStopIndex = j + 1; - } - } - - const startStop = stops[startStopIndex]; - const endStop = stops[Math.min(startStopIndex + 1, stops.length - 1)]; - - // 区間内での補間係数を計算 - let t = 0; - const rangeWidth = endStop.ratio - startStop.ratio; - if (rangeWidth > 0) { - t = (ratio - startStop.ratio) / rangeWidth; - t = Math.max(0, Math.min(1, t)); - } - - // 色を補間 - const color = gradientLUTInterpolateColorService( - startStop, endStop, t, interpolation - ); - - const offset = i * 4; - pixels[offset] = Math.round(color.r * 255); - pixels[offset + 1] = Math.round(color.g * 255); - pixels[offset + 2] = Math.round(color.b * 255); - pixels[offset + 3] = Math.round(color.a * 255); - } - - return pixels; -}; diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.test.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.test.ts deleted file mode 100644 index 6a54bb51..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { execute } from "./GradientLUTInterpolateColorService"; -import type { IGradientStop } from "./GradientLUTParseStopsService"; -import { describe, expect, it } from "vitest"; - -describe("GradientLUTInterpolateColorService.ts method test", () => -{ - it("test case - interpolate at t=0 returns start color (RGB mode)", () => - { - const startStop: IGradientStop = { ratio: 0, r: 1, g: 0, b: 0, a: 1 }; - const endStop: IGradientStop = { ratio: 1, r: 0, g: 0, b: 1, a: 1 }; - - const result = execute(startStop, endStop, 0, 0); - - expect(result.r).toBe(1); - expect(result.g).toBe(0); - expect(result.b).toBe(0); - expect(result.a).toBe(1); - }); - - it("test case - interpolate at t=1 returns end color (RGB mode)", () => - { - const startStop: IGradientStop = { ratio: 0, r: 1, g: 0, b: 0, a: 1 }; - const endStop: IGradientStop = { ratio: 1, r: 0, g: 0, b: 1, a: 1 }; - - const result = execute(startStop, endStop, 1, 0); - - expect(result.r).toBe(0); - expect(result.g).toBe(0); - expect(result.b).toBe(1); - expect(result.a).toBe(1); - }); - - it("test case - interpolate at t=0.5 returns midpoint (RGB mode)", () => - { - const startStop: IGradientStop = { ratio: 0, r: 0, g: 0, b: 0, a: 0 }; - const endStop: IGradientStop = { ratio: 1, r: 1, g: 1, b: 1, a: 1 }; - - const result = execute(startStop, endStop, 0.5, 0); - - expect(result.r).toBe(0.5); - expect(result.g).toBe(0.5); - expect(result.b).toBe(0.5); - expect(result.a).toBe(0.5); - }); - - it("test case - interpolate with Linear RGB mode (interpolation=1)", () => - { - const startStop: IGradientStop = { ratio: 0, r: 0, g: 0, b: 0, a: 0 }; - const endStop: IGradientStop = { ratio: 1, r: 1, g: 1, b: 1, a: 1 }; - - const result = execute(startStop, endStop, 0.5, 1); - - // Linear RGB補間では結果が異なる - expect(result.r).toBeGreaterThan(0); - expect(result.r).toBeLessThan(1); - expect(result.a).toBe(0.5); // アルファは常に線形補間 - }); - - it("test case - alpha is always linearly interpolated", () => - { - const startStop: IGradientStop = { ratio: 0, r: 1, g: 1, b: 1, a: 0 }; - const endStop: IGradientStop = { ratio: 1, r: 1, g: 1, b: 1, a: 1 }; - - const resultRGB = execute(startStop, endStop, 0.5, 0); - const resultLinear = execute(startStop, endStop, 0.5, 1); - - expect(resultRGB.a).toBe(0.5); - expect(resultLinear.a).toBe(0.5); - }); -}); diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.ts deleted file mode 100644 index 8180844f..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTInterpolateColorService.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { IGradientStop } from "../../../interface/IGradientStop"; - -/** - * @description 2つのストップ間で色を補間 - * Interpolate color between two stops - * - * @param {IGradientStop} startStop - * @param {IGradientStop} endStop - * @param {number} t - 補間係数 (0-1) - * @param {number} interpolation - 0: RGB, 1: Linear RGB - * @return {{ r: number, g: number, b: number, a: number }} - * @method - * @protected - */ -export const execute = ( - startStop: IGradientStop, - endStop: IGradientStop, - t: number, - interpolation: number -): { r: number; g: number; b: number; a: number } => { - - let r: number; - let g: number; - let b: number; - - if (interpolation === 1) { - // Linear RGB補間(ガンマ補正あり) - const sr = Math.pow(startStop.r, 2.2); - const sg = Math.pow(startStop.g, 2.2); - const sb = Math.pow(startStop.b, 2.2); - const er = Math.pow(endStop.r, 2.2); - const eg = Math.pow(endStop.g, 2.2); - const eb = Math.pow(endStop.b, 2.2); - - r = Math.pow(sr + (er - sr) * t, 1 / 2.2); - g = Math.pow(sg + (eg - sg) * t, 1 / 2.2); - b = Math.pow(sb + (eb - sb) * t, 1 / 2.2); - } else { - // 通常のRGB補間 - r = startStop.r + (endStop.r - startStop.r) * t; - g = startStop.g + (endStop.g - startStop.g) * t; - b = startStop.b + (endStop.b - startStop.b) * t; - } - - const a = startStop.a + (endStop.a - startStop.a) * t; - - return { r, g, b, a }; -}; diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTParseStopsService.test.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTParseStopsService.test.ts deleted file mode 100644 index ffa725a9..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTParseStopsService.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { execute } from "./GradientLUTParseStopsService"; -import { describe, expect, it } from "vitest"; - -describe("GradientLUTParseStopsService.ts method test", () => -{ - it("test case - parse single stop", () => - { - const stops = [0.5, 1, 0, 0, 1]; // ratio=0.5, r=1, g=0, b=0, a=1 - - const result = execute(stops); - - expect(result.length).toBe(1); - expect(result[0].ratio).toBe(0.5); - expect(result[0].r).toBe(1); - expect(result[0].g).toBe(0); - expect(result[0].b).toBe(0); - expect(result[0].a).toBe(1); - }); - - it("test case - parse multiple stops", () => - { - const stops = [ - 0, 1, 0, 0, 1, // ratio=0, red - 1, 0, 0, 1, 1 // ratio=1, blue - ]; - - const result = execute(stops); - - expect(result.length).toBe(2); - expect(result[0].ratio).toBe(0); - expect(result[1].ratio).toBe(1); - }); - - it("test case - sorts stops by ratio", () => - { - const stops = [ - 1, 0, 0, 1, 1, // ratio=1 - 0.5, 0, 1, 0, 1, // ratio=0.5 - 0, 1, 0, 0, 1 // ratio=0 - ]; - - const result = execute(stops); - - expect(result.length).toBe(3); - expect(result[0].ratio).toBe(0); - expect(result[1].ratio).toBe(0.5); - expect(result[2].ratio).toBe(1); - }); - - it("test case - empty stops", () => - { - const stops: number[] = []; - - const result = execute(stops); - - expect(result.length).toBe(0); - }); -}); diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTParseStopsService.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTParseStopsService.ts deleted file mode 100644 index 6f62628b..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/service/GradientLUTParseStopsService.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { IGradientStop } from "../../../interface/IGradientStop"; - -/** - * @description グラデーションストップ配列をパースしてソート - * Parse and sort gradient stops array - * - * @param {number[]} stops - [ratio, r, g, b, a, ratio, r, g, b, a, ...] - * @return {IGradientStop[]} - * @method - * @protected - */ -export const execute = (stops: number[]): IGradientStop[] => -{ - const gradientStops: IGradientStop[] = []; - - for (let i = 0; i < stops.length; i += 5) { - gradientStops.push({ - "ratio": stops[i], - "r": stops[i + 1], - "g": stops[i + 2], - "b": stops[i + 3], - "a": stops[i + 4] - }); - } - - // ストップポイントをratio順にソート - gradientStops.sort((a, b) => a.ratio - b.ratio); - - return gradientStops; -}; diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/usecase/GradientLUTGenerateDataUseCase.test.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/usecase/GradientLUTGenerateDataUseCase.test.ts deleted file mode 100644 index 6d0c7884..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/usecase/GradientLUTGenerateDataUseCase.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { execute } from "./GradientLUTGenerateDataUseCase"; -import { describe, expect, it } from "vitest"; - -describe("GradientLUTGenerateDataUseCase.ts method test", () => -{ - it("test case - generates LUT data for simple gradient", () => - { - const stops = [ - 0, 1, 0, 0, 1, // ratio=0, red - 1, 0, 0, 1, 1 // ratio=1, blue - ]; - - const result = execute(stops, 0); - - expect(result.pixels).toBeInstanceOf(Uint8Array); - expect(result.resolution).toBe(64); // 2 stops = 64px - expect(result.pixels.length).toBe(64 * 4); - }); - - it("test case - resolution adapts to stop count", () => - { - // 5ストップのグラデーション - const stops = [ - 0, 1, 0, 0, 1, - 0.25, 1, 1, 0, 1, - 0.5, 0, 1, 0, 1, - 0.75, 0, 1, 1, 1, - 1, 0, 0, 1, 1 - ]; - - const result = execute(stops, 0); - - expect(result.resolution).toBe(256); // 5 stops = 256px - }); - - it("test case - first pixel is start color", () => - { - const stops = [ - 0, 1, 0.5, 0.25, 1, // ratio=0 - 1, 0, 0, 1, 1 // ratio=1 - ]; - - const result = execute(stops, 0); - - expect(result.pixels[0]).toBe(255); // r - expect(result.pixels[1]).toBe(128); // g (0.5 * 255) - expect(result.pixels[2]).toBe(64); // b (0.25 * 255) - expect(result.pixels[3]).toBe(255); // a - }); - - it("test case - last pixel is end color", () => - { - const stops = [ - 0, 1, 0, 0, 1, - 1, 0.5, 0.25, 1, 0.5 // ratio=1 - ]; - - const result = execute(stops, 0); - - const lastIndex = (result.resolution - 1) * 4; - expect(result.pixels[lastIndex]).toBe(128); // r (0.5 * 255) - expect(result.pixels[lastIndex + 1]).toBe(64); // g (0.25 * 255) - expect(result.pixels[lastIndex + 2]).toBe(255); // b - expect(result.pixels[lastIndex + 3]).toBe(128); // a (0.5 * 255) - }); - - it("test case - respects custom resolution limits", () => - { - const stops = [ - 0, 1, 0, 0, 1, - 1, 0, 0, 1, 1 - ]; - - const result = execute(stops, 0, 128, 128); - - expect(result.resolution).toBe(128); - }); - - it("test case - handles unsorted stops", () => - { - const stops = [ - 1, 0, 0, 1, 1, // ratio=1 (end) - 0, 1, 0, 0, 1 // ratio=0 (start) - ]; - - const result = execute(stops, 0); - - // 内部でソートされるので、最初のピクセルは赤になるはず - expect(result.pixels[0]).toBe(255); // r - expect(result.pixels[1]).toBe(0); // g - expect(result.pixels[2]).toBe(0); // b - }); - - it("test case - white gradient with varying alpha (0xffffff alpha 1.0 to 0.6)", () => - { - // Issue: 0xffffff (white) colors and transparency/alpha gradients were not displaying - const stops = [ - 0, 1, 1, 1, 1, // ratio=0, white with alpha=1.0 - 1, 1, 1, 1, 0.6 // ratio=1, white with alpha=0.6 - ]; - - const result = execute(stops, 0); - - // First pixel: white with full alpha - expect(result.pixels[0]).toBe(255); // r - expect(result.pixels[1]).toBe(255); // g - expect(result.pixels[2]).toBe(255); // b - expect(result.pixels[3]).toBe(255); // a (1.0) - - // Last pixel: white with 60% alpha - const lastIndex = (result.resolution - 1) * 4; - expect(result.pixels[lastIndex]).toBe(255); // r - expect(result.pixels[lastIndex + 1]).toBe(255); // g - expect(result.pixels[lastIndex + 2]).toBe(255); // b - expect(result.pixels[lastIndex + 3]).toBe(153); // a (0.6 * 255 = 153) - }); - - it("test case - alpha-only gradient (same color, different alphas)", () => - { - const stops = [ - 0, 0.8, 0.4, 0.2, 1, // ratio=0, color with alpha=1.0 - 1, 0.8, 0.4, 0.2, 0 // ratio=1, same color with alpha=0.0 - ]; - - const result = execute(stops, 0); - - // First pixel: full alpha - expect(result.pixels[3]).toBe(255); // a (1.0) - - // Last pixel: zero alpha (fully transparent) - const lastIndex = (result.resolution - 1) * 4; - expect(result.pixels[lastIndex + 3]).toBe(0); // a (0.0) - - // Middle pixel should have ~50% alpha - const midIndex = Math.floor(result.resolution / 2) * 4; - expect(result.pixels[midIndex + 3]).toBeGreaterThan(100); - expect(result.pixels[midIndex + 3]).toBeLessThan(156); - }); -}); diff --git a/packages/webgpu/src/Shader/GradientLUTGenerator/usecase/GradientLUTGenerateDataUseCase.ts b/packages/webgpu/src/Shader/GradientLUTGenerator/usecase/GradientLUTGenerateDataUseCase.ts deleted file mode 100644 index f9659e38..00000000 --- a/packages/webgpu/src/Shader/GradientLUTGenerator/usecase/GradientLUTGenerateDataUseCase.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { IGradientLUTData } from "../../../interface/IGradientLUTData"; -import { execute as gradientLUTParseStopsService } from "../service/GradientLUTParseStopsService"; -import { execute as gradientLUTCalculateResolutionService } from "../service/GradientLUTCalculateResolutionService"; -import { execute as gradientLUTGeneratePixelsService } from "../service/GradientLUTGeneratePixelsService"; - -/** - * @description グラデーションLUTのピクセルデータを生成 - * Generate gradient LUT pixel data - * - * @param {number[]} stops - [ratio, r, g, b, a, ratio, r, g, b, a, ...] - * @param {number} interpolation - 0: RGB, 1: Linear RGB - * @param {number} [minResolution=64] - 最小解像度 - * @param {number} [maxResolution=512] - 最大解像度 - * @return {IGradientLUTData} - * @method - * @protected - */ -export const execute = ( - stops: number[], - interpolation: number, - minResolution: number = 64, - maxResolution: number = 512 -): IGradientLUTData => { - - // ストップ配列をパースしてソート - const parsedStops = gradientLUTParseStopsService(stops); - - // ストップ数に応じた解像度を計算 - const resolution = gradientLUTCalculateResolutionService( - parsedStops.length, - minResolution, - maxResolution - ); - - // ピクセルデータを生成 - const pixels = gradientLUTGeneratePixelsService( - parsedStops, - resolution, - interpolation - ); - - return { pixels, resolution }; -}; diff --git a/packages/webgpu/src/Shader/PipelineManager.ts b/packages/webgpu/src/Shader/PipelineManager.ts index fdfcdb78..9f0990ca 100644 --- a/packages/webgpu/src/Shader/PipelineManager.ts +++ b/packages/webgpu/src/Shader/PipelineManager.ts @@ -1,7 +1,11 @@ import { ShaderSource } from "./ShaderSource"; import { $samples } from "../WebGPUUtil"; -const VERTEX_BUFFER_LAYOUT_4F: GPUVertexBufferLayout = { +/** + * @description 4フロートストライドの頂点バッファレイアウト(position: float32x2, uv: float32x2) + * Vertex buffer layout with 4-float stride (position: float32x2, uv: float32x2) + */ +const $VERTEX_BUFFER_LAYOUT_4F: GPUVertexBufferLayout = { "arrayStride": 4 * 4, "attributes": [ { "shaderLocation": 0, "offset": 0, "format": "float32x2" }, @@ -9,7 +13,11 @@ const VERTEX_BUFFER_LAYOUT_4F: GPUVertexBufferLayout = { ] }; -const BLEND_PREMULTIPLIED_ALPHA: GPUBlendState = { +/** + * @description プリマルチプライドアルファのブレンドステート + * Premultiplied alpha blend state for standard compositing + */ +const $BLEND_PREMULTIPLIED_ALPHA: GPUBlendState = { "color": { "srcFactor": "one", "dstFactor": "one-minus-src-alpha", @@ -22,16 +30,54 @@ const BLEND_PREMULTIPLIED_ALPHA: GPUBlendState = { } }; +/** + * @description WebGPUレンダーパイプラインの管理クラス。パイプラインとバインドグループレイアウトの生成・キャッシュを行う + * Manager class for WebGPU render pipelines. Creates, caches, and manages pipelines and bind group layouts + */ export class PipelineManager { + /** + * @description GPUデバイスの参照 + * Reference to the GPU device + */ private device: GPUDevice; + /** + * @description 出力テクスチャフォーマット + * Output texture format + */ private format: GPUTextureFormat; + /** + * @description パイプライン名からGPURenderPipelineへのキャッシュマップ + * Cache map from pipeline name to GPURenderPipeline + */ private pipelines: Map; + /** + * @description バインドグループレイアウト名からGPUBindGroupLayoutへのキャッシュマップ + * Cache map from layout name to GPUBindGroupLayout + */ private bindGroupLayouts: Map; + /** + * @description MSAAサンプル数 + * MSAA sample count + */ private sampleCount: number; + /** + * @description シェーダーモジュールのキャッシュ + * Shader module cache by key + */ private shaderModuleCache: Map = new Map(); + /** + * @description フィルター用バインドグループレイアウトキャッシュ(テクスチャ数別) + * Filter bind group layout cache indexed by texture count + */ private filterBindGroupLayouts: Map = new Map(); + /** + * @description PipelineManagerのコンストラクタ。GPUデバイスとフォーマットを設定し、パイプラインを初期化する + * Construct PipelineManager. Sets GPU device, format, and initializes pipelines + * @param {GPUDevice} device - GPUデバイス / GPU device instance + * @param {GPUTextureFormat} format - テクスチャフォーマット / Texture format for output + */ constructor(device: GPUDevice, format: GPUTextureFormat) { this.device = device; @@ -43,6 +89,13 @@ export class PipelineManager this.initialize(); } + /** + * @description シェーダーモジュールをキャッシュから取得、または新規作成する + * Get a shader module from cache, or create and cache a new one + * @param {string} key - キャッシュキー / Cache key + * @param {string} code - WGSLシェーダーコード / WGSL shader source code + * @return {GPUShaderModule} シェーダーモジュール / The shader module + */ private getOrCreateShaderModule(key: string, code: string): GPUShaderModule { let module = this.shaderModuleCache.get(key); @@ -53,6 +106,10 @@ export class PipelineManager return module; } + /** + * @description 初期パイプライン群を一括作成する + * Create all initial render pipelines + */ private initialize(): void { this.createFillPipeline(); @@ -69,11 +126,19 @@ export class PipelineManager this.createNodeClearPipeline(); } + /** + * @description 初期化済みの遅延グループ名セット + * Set of initialized lazy group names + */ private lazyInitGroups: Set = new Set(); + /** + * @description パイプライン名から遅延初期化グループ名への読み取り専用マップ + * Read-only map from pipeline name to lazy initialization group name + */ private readonly lazyGroupMap: ReadonlyMap = new Map([ ...Array.from({ "length": 16 }, (_, i): [string, string] => [`blur_filter_${i + 1}`, "blur_filter"]), ["blur_filter", "blur_filter"], - ["texture_copy", "texture_copy"], ["texture_copy_rgba8", "texture_copy"], ["color_transform", "texture_copy"], ["y_flip_color_transform", "texture_copy"], + ["texture_copy", "texture_copy"], ["texture_copy_rgba8", "texture_copy"], ["texture_copy_rgba8_msaa", "texture_copy"], ["color_transform", "texture_copy"], ["y_flip_color_transform", "texture_copy"], ["texture_erase", "texture_copy"], ["blur_texture_copy", "texture_copy"], ["filter_blend", "texture_copy"], ["texture_copy_bgra", "texture_copy"], ["filter_output", "texture_copy"], ["filter_output_add", "texture_copy"], @@ -82,6 +147,7 @@ export class PipelineManager ["filter_output_msaa", "texture_copy"], ["filter_output_add_msaa", "texture_copy"], ["filter_output_screen_msaa", "texture_copy"], ["filter_output_alpha_msaa", "texture_copy"], ["filter_output_erase_msaa", "texture_copy"], + ["filter_output_masked", "texture_copy"], ["filter_output_masked_msaa", "texture_copy"], ["positioned_texture", "texture_copy"], ["positioned_texture_rgba", "texture_copy"], ["bitmap_render_msaa", "texture_copy"], ["bitmap_render", "texture_copy"], ["texture_scale", "texture_copy"], ["texture_scale_blend", "texture_copy"], @@ -98,6 +164,11 @@ export class PipelineManager ["filter_complex_blend_output_msaa", "complex_blend"] ]); + /** + * @description 指定された名前に対応する遅延初期化グループをまだ初期化されていなければ初期化する + * Ensure the lazy initialization group for the given name is initialized + * @param {string} name - パイプライン名 / Pipeline name to look up its lazy group + */ private ensureLazyGroup(name: string): void { const group = this.lazyGroupMap.get(name); @@ -125,6 +196,10 @@ export class PipelineManager } } + /** + * @description すべての遅延初期化グループを事前にロードする + * Preload all lazy initialization groups eagerly + */ preloadLazyGroups(): void { const groups = ["blur_filter", "texture_copy", "bitmap_sync", "filter", "complex_blend"]; @@ -133,6 +208,10 @@ export class PipelineManager } } + /** + * @description 塗りつぶし描画用パイプラインを作成する(RGBA/BGRA/ステンシル対応) + * Create fill render pipelines (RGBA, BGRA, and stencil variants) + */ private createFillPipeline(): void { // Dynamic Offset対応のBindGroupLayout(fill + stencil共有) @@ -156,8 +235,8 @@ export class PipelineManager const fragmentShaderModule = this.getOrCreateShaderModule("fillFragment", ShaderSource.getFillFragmentShader()); - const vertexBufferLayout = VERTEX_BUFFER_LAYOUT_4F; - const blendState = BLEND_PREMULTIPLIED_ALPHA; + const vertexBufferLayout = $VERTEX_BUFFER_LAYOUT_4F; + const blendState = $BLEND_PREMULTIPLIED_ALPHA; const pipelineRGBA = this.device.createRenderPipeline({ "layout": pipelineLayout, "vertex": { @@ -272,9 +351,13 @@ export class PipelineManager this.pipelines.set("fill_bgra_stencil", pipelineBGRAStencil); } + /** + * @description ステンシル塗りつぶし用パイプラインを作成する(書き込み・塗りつぶし・アトラス・メイン・マスク対応) + * Create stencil fill pipelines (write, fill, atlas, main, and masked variants) + */ private createStencilFillPipelines(): void { - const vertexBufferLayout = VERTEX_BUFFER_LAYOUT_4F; + const vertexBufferLayout = $VERTEX_BUFFER_LAYOUT_4F; // fill_dynamicレイアウトを共有(hasDynamicOffset: true) const dynamicLayout = this.bindGroupLayouts.get("fill_dynamic")!; @@ -624,9 +707,13 @@ export class PipelineManager this.pipelines.set("stencil_fill_masked", stencilFillMaskedPipeline); } + /** + * @description クリッピング用パイプラインを作成する(レベル別の書き込み・クリア) + * Create clip pipelines (level-based write and clear variants) + */ private createClipPipeline(): void { - const vertexBufferLayout = VERTEX_BUFFER_LAYOUT_4F; + const vertexBufferLayout = $VERTEX_BUFFER_LAYOUT_4F; const dynamicLayout = this.bindGroupLayouts.get("fill_dynamic")!; const clipPipelineLayout = this.device.createPipelineLayout({ "bindGroupLayouts": [dynamicLayout] @@ -769,9 +856,13 @@ export class PipelineManager } } + /** + * @description マスク合成用パイプラインを作成する(レベル別のマージ・クリア) + * Create mask union pipelines (level-based merge and clear variants) + */ private createMaskUnionPipelines(): void { - const vertexBufferLayout = VERTEX_BUFFER_LAYOUT_4F; + const vertexBufferLayout = $VERTEX_BUFFER_LAYOUT_4F; const dynamicLayout = this.bindGroupLayouts.get("fill_dynamic")!; const maskUnionPipelineLayout = this.device.createPipelineLayout({ "bindGroupLayouts": [dynamicLayout] @@ -863,6 +954,10 @@ export class PipelineManager } } + /** + * @description マスク描画用パイプラインを作成する + * Create mask render pipeline + */ private createMaskPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -934,6 +1029,10 @@ export class PipelineManager this.pipelines.set("mask", pipeline); } + /** + * @description 基本描画パイプラインを作成する(RGBA/BGRA) + * Create basic render pipelines (RGBA and BGRA variants) + */ private createBasicPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -970,7 +1069,7 @@ export class PipelineManager ] }; - const blendState = BLEND_PREMULTIPLIED_ALPHA; + const blendState = $BLEND_PREMULTIPLIED_ALPHA; const pipelineRGBA = this.device.createRenderPipeline({ "layout": pipelineLayout, "vertex": { @@ -1020,6 +1119,10 @@ export class PipelineManager this.pipelines.set("basic_bgra", pipelineBGRA); } + /** + * @description テクスチャ描画用パイプラインを作成する + * Create texture render pipeline + */ private createTexturePipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -1101,6 +1204,10 @@ export class PipelineManager this.pipelines.set("texture", pipeline); } + /** + * @description インスタンス描画用パイプラインを作成する(ブレンドバリアント・マスク対応) + * Create instanced render pipelines (blend variants and masked) + */ private createInstancedPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -1278,18 +1385,7 @@ export class PipelineManager "entryPoint": "main", "targets": [{ "format": this.format, - "blend": { - "color": { - "srcFactor": "one", - "dstFactor": "one-minus-src-alpha", - "operation": "add" - }, - "alpha": { - "srcFactor": "one", - "dstFactor": "one-minus-src-alpha", - "operation": "add" - } - } + "blend": $BLEND_PREMULTIPLIED_ALPHA }] }, "primitive": { @@ -1320,6 +1416,10 @@ export class PipelineManager this.pipelines.set("instanced_masked", maskedPipeline); } + /** + * @description グラデーション塗りつぶし用パイプラインを作成する(RGBA/BGRA/ステンシル対応) + * Create gradient fill pipelines (RGBA, BGRA, stencil, and stroke variants) + */ private createGradientPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -1357,8 +1457,8 @@ export class PipelineManager const stencilFragmentShaderModule = this.getOrCreateShaderModule("gradientFillStencilFragment", ShaderSource.getGradientFillStencilFragmentShader()); this.gradientStencilFragmentShaderModule = stencilFragmentShaderModule; - const vertexBufferLayout = VERTEX_BUFFER_LAYOUT_4F; - const blendState = BLEND_PREMULTIPLIED_ALPHA; + const vertexBufferLayout = $VERTEX_BUFFER_LAYOUT_4F; + const blendState = $BLEND_PREMULTIPLIED_ALPHA; const pipelineRGBA = this.device.createRenderPipeline({ "label": "gradient_fill_no_stencil_pipeline", "layout": pipelineLayout, @@ -1674,6 +1774,10 @@ export class PipelineManager this.pipelines.set("gradient_fill_bgra_stencil", pipelineBGRAStencil); } + /** + * @description ビットマップ塗りつぶし用パイプラインを作成する(RGBA/BGRA/ステンシル・ストローク対応) + * Create bitmap fill pipelines (RGBA, BGRA, stencil, and stroke variants) + */ private createBitmapFillPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -1706,8 +1810,8 @@ export class PipelineManager const fragmentShaderModule = this.getOrCreateShaderModule("bitmapFillFragment", ShaderSource.getBitmapFillFragmentShader()); - const vertexBufferLayout = VERTEX_BUFFER_LAYOUT_4F; - const blendState = BLEND_PREMULTIPLIED_ALPHA; + const vertexBufferLayout = $VERTEX_BUFFER_LAYOUT_4F; + const blendState = $BLEND_PREMULTIPLIED_ALPHA; const pipelineRGBA = this.device.createRenderPipeline({ "layout": pipelineLayout, "vertex": { @@ -1907,6 +2011,10 @@ export class PipelineManager this.pipelines.set("bitmap_fill_bgra_stencil", pipelineBGRAStencil); } + /** + * @description ブレンド描画用パイプラインを作成する(デュアルテクスチャブレンド) + * Create blend render pipeline for dual-texture blending + */ private createBlendPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -2003,6 +2111,10 @@ export class PipelineManager this.pipelines.set("blend", pipeline); } + /** + * @description ブラーフィルター用パイプラインを作成する(halfBlur 1〜16のバリアント) + * Create blur filter pipelines (halfBlur 1-16 variants) + */ private createBlurFilterPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -2070,6 +2182,10 @@ export class PipelineManager } } + /** + * @description テクスチャコピー用パイプラインを作成する(各種ブレンド・フィルター出力・MSAA対応) + * Create texture copy pipelines (various blend modes, filter output, MSAA variants) + */ private createTextureCopyPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -2121,6 +2237,12 @@ export class PipelineManager this.pipelines.set("texture_copy_rgba8", this.createFullscreenQuadPipeline( pipelineLayout, vertexShaderModule, fragmentShaderModule, "rgba8unorm", BLEND_REPLACE )); + if (this.sampleCount > 1) { + const copyBlendRgba8: GPUBlendState = { "color": { "srcFactor": "one", "dstFactor": "zero", "operation": "add" }, "alpha": { "srcFactor": "one", "dstFactor": "zero", "operation": "add" } }; + this.pipelines.set("texture_copy_rgba8_msaa", this.createFullscreenQuadPipeline( + pipelineLayout, vertexShaderModule, fragmentShaderModule, "rgba8unorm", copyBlendRgba8, this.sampleCount + )); + } const colorTransformFragmentModule = this.getOrCreateShaderModule("colorTransformFragment", ShaderSource.getColorTransformFragmentShader()); this.pipelines.set("color_transform", this.createFullscreenQuadPipeline( pipelineLayout, vertexShaderModule, colorTransformFragmentModule, "rgba8unorm", BLEND_REPLACE @@ -2158,6 +2280,35 @@ export class PipelineManager pipelineLayout, vertexShaderModule, filterOutputShaderModule, this.format, blend )); } + + // マスク付きフィルター出力パイプライン(ステンシルテスト付き) + const filterMaskedStencil: GPUDepthStencilState = { + "format": "stencil8", + "stencilFront": { + "compare": "equal", + "failOp": "keep", + "depthFailOp": "keep", + "passOp": "keep" + }, + "stencilBack": { + "compare": "equal", + "failOp": "keep", + "depthFailOp": "keep", + "passOp": "keep" + }, + "stencilReadMask": 0xFF, + "stencilWriteMask": 0x00 + }; + this.pipelines.set("filter_output_masked", this.createFullscreenQuadPipeline( + pipelineLayout, vertexShaderModule, filterOutputShaderModule, this.format, BLEND_ALPHA, undefined, filterMaskedStencil + )); + if (this.sampleCount > 1) { + const normalBlendForMask: GPUBlendState = { "color": { "srcFactor": "one", "dstFactor": "one-minus-src-alpha", "operation": "add" }, "alpha": { "srcFactor": "one", "dstFactor": "one-minus-src-alpha", "operation": "add" } }; + this.pipelines.set("filter_output_masked_msaa", this.createFullscreenQuadPipeline( + pipelineLayout, vertexShaderModule, filterOutputShaderModule, this.format, normalBlendForMask, this.sampleCount, filterMaskedStencil + )); + } + if (this.sampleCount > 1) { const copyBlend: GPUBlendState = { "color": { "srcFactor": "one", "dstFactor": "zero", "operation": "add" }, "alpha": { "srcFactor": "one", "dstFactor": "zero", "operation": "add" } }; this.pipelines.set("texture_copy_bgra_msaa", this.createFullscreenQuadPipeline( @@ -2177,6 +2328,10 @@ export class PipelineManager this.createTextureScalePipeline(); } + /** + * @description 位置指定テクスチャ描画用パイプラインを作成する(RGBA/ビットマップレンダー対応) + * Create positioned texture pipelines (RGBA, bitmap render, MSAA variants) + */ private createPositionedTexturePipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -2371,6 +2526,10 @@ export class PipelineManager this.pipelines.set("bitmap_render", pipelineNonMsaa); } + /** + * @description テクスチャスケーリング用パイプラインを作成する + * Create texture scale pipelines + */ private createTextureScalePipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -2470,6 +2629,10 @@ export class PipelineManager this.pipelines.set("texture_scale_blend", blendPipeline); } + /** + * @description ビットマップ同期描画用パイプラインを作成する(MSAA 4x) + * Create bitmap sync render pipeline (MSAA 4x) + */ private createBitmapSyncPipeline(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -2535,6 +2698,10 @@ export class PipelineManager this.pipelines.set("bitmap_sync", pipeline); } + /** + * @description カラーマトリクスフィルター・ベベル・グロー・ドロップシャドウ等のフィルターパイプラインを作成する + * Create filter pipelines (color matrix, bevel, glow, drop shadow, gradient glow/bevel) + */ private createColorMatrixFilterPipeline(): void { const BLEND_REPLACE: GPUBlendState = { @@ -2554,6 +2721,10 @@ export class PipelineManager this.createFilterPipelineWithLayout("gradient_bevel_filter", ShaderSource.getGradientBevelFilterFragmentShader(), 3, BLEND_ALPHA); } + /** + * @description 複合ブレンド用パイプラインを作成する + * Create complex blend pipelines + */ private createComplexBlendPipelines(): void { const bindGroupLayout = this.device.createBindGroupLayout({ @@ -2626,6 +2797,10 @@ export class PipelineManager this.createComplexBlendOutputPipeline(); } + /** + * @description 複合ブレンドのコピー・スケール用パイプラインを作成する + * Create complex blend copy and scale pipelines + */ private createComplexBlendCopyPipeline(): void { const BLEND_REPLACE: GPUBlendState = { @@ -2652,6 +2827,10 @@ export class PipelineManager } } + /** + * @description 複合ブレンドの出力用パイプラインを作成する(MSAA対応含む) + * Create complex blend output pipelines (including MSAA variants) + */ private createComplexBlendOutputPipeline(): void { const bindGroupLayout = this.bindGroupLayouts.get("positioned_texture"); @@ -2685,56 +2864,76 @@ export class PipelineManager } } + /** + * @description 指定されたフラグメントシェーダーとテクスチャ数でフィルターパイプラインを作成する + * Create a filter pipeline with the specified fragment shader and texture count + * @param {string} name - パイプライン名 / Pipeline name + * @param {string} fragment_shader_code - フラグメントシェーダーコード / Fragment shader WGSL source code + * @param {number} texture_count - テクスチャバインディング数 / Number of texture bindings + * @param {GPUBlendState} blend - ブレンドステート / Blend state configuration + */ private createFilterPipelineWithLayout( name: string, - fragmentShaderCode: string, - textureCount: number, + fragment_shader_code: string, + texture_count: number, blend: GPUBlendState ): void { - let bindGroupLayout = this.filterBindGroupLayouts.get(textureCount); + let bindGroupLayout = this.filterBindGroupLayouts.get(texture_count); if (!bindGroupLayout) { const entries: GPUBindGroupLayoutEntry[] = [ { "binding": 0, "visibility": GPUShaderStage.FRAGMENT, "buffer": { "type": "uniform" } }, { "binding": 1, "visibility": GPUShaderStage.FRAGMENT, "sampler": {} } ]; - for (let i = 0; i < textureCount; i++) { + for (let i = 0; i < texture_count; i++) { entries.push({ "binding": 2 + i, "visibility": GPUShaderStage.FRAGMENT, "texture": {} }); } bindGroupLayout = this.device.createBindGroupLayout({ "entries": entries }); - this.filterBindGroupLayouts.set(textureCount, bindGroupLayout); + this.filterBindGroupLayouts.set(texture_count, bindGroupLayout); } this.bindGroupLayouts.set(name, bindGroupLayout); const pipelineLayout = this.device.createPipelineLayout({ "bindGroupLayouts": [bindGroupLayout] }); const vertexShaderModule = this.getOrCreateShaderModule("blurFilterVertex", ShaderSource.getBlurFilterVertexShader()); - const fragmentShaderModule = this.getOrCreateShaderModule(`filter_${name}`, fragmentShaderCode); + const fragmentShaderModule = this.getOrCreateShaderModule(`filter_${name}`, fragment_shader_code); this.pipelines.set(name, this.createFullscreenQuadPipeline( pipelineLayout, vertexShaderModule, fragmentShaderModule, "rgba8unorm", blend )); } + /** + * @description フルスクリーンクアッド描画用の汎用パイプラインを作成する + * Create a generic fullscreen quad render pipeline + * @param {GPUPipelineLayout} pipeline_layout - パイプラインレイアウト / Pipeline layout + * @param {GPUShaderModule} vertex_module - 頂点シェーダーモジュール / Vertex shader module + * @param {GPUShaderModule} fragment_module - フラグメントシェーダーモジュール / Fragment shader module + * @param {GPUTextureFormat} format - テクスチャフォーマット / Target texture format + * @param {GPUBlendState} blend - ブレンドステート / Blend state configuration + * @param {number} multisample_count - MSAAサンプル数(任意) / Optional MSAA sample count + * @param {GPUDepthStencilState} depth_stencil - 深度ステンシルステート(任意) / Optional depth-stencil state + * @return {GPURenderPipeline} レンダーパイプライン / The created render pipeline + */ private createFullscreenQuadPipeline( - pipelineLayout: GPUPipelineLayout, - vertexModule: GPUShaderModule, - fragmentModule: GPUShaderModule, + pipeline_layout: GPUPipelineLayout, + vertex_module: GPUShaderModule, + fragment_module: GPUShaderModule, format: GPUTextureFormat, blend: GPUBlendState, - multisampleCount?: number, - depthStencil?: GPUDepthStencilState + multisample_count?: number, + depth_stencil?: GPUDepthStencilState ): GPURenderPipeline { const descriptor: GPURenderPipelineDescriptor = { - "layout": pipelineLayout, + "layout": pipeline_layout, "vertex": { - "module": vertexModule, + "module": vertex_module, "entryPoint": "main", "buffers": [] }, "fragment": { - "module": fragmentModule, + "module": fragment_module, "entryPoint": "main", "targets": [{ "format": format, "blend": blend }] }, @@ -2744,17 +2943,23 @@ export class PipelineManager } }; - if (multisampleCount && multisampleCount > 1) { - descriptor.multisample = { "count": multisampleCount }; + if (multisample_count && multisample_count > 1) { + descriptor.multisample = { "count": multisample_count }; } - if (depthStencil) { - descriptor.depthStencil = depthStencil; + if (depth_stencil) { + descriptor.depthStencil = depth_stencil; } return this.device.createRenderPipeline(descriptor); } + /** + * @description 名前からレンダーパイプラインを取得する(遅延初期化を含む) + * Get a render pipeline by name, initializing lazy groups if needed + * @param {string} name - パイプライン名 / Pipeline name + * @return {GPURenderPipeline | undefined} パイプラインまたはundefined / The pipeline or undefined + */ getPipeline(name: string): GPURenderPipeline | undefined { let pipeline = this.pipelines.get(name); @@ -2766,15 +2971,18 @@ export class PipelineManager } /** - * @description フィルターパイプラインのoverride定数バリアントを取得 - * GPU warp divergenceを排除するコンパイル時分岐特殊化 + * @description フィルターパイプラインのoverride定数バリアントを取得する。GPU warp divergenceを排除するコンパイル時分岐特殊化 + * Get a filter pipeline variant with override constants. Compile-time branch specialization to eliminate GPU warp divergence + * @param {string} base_name - ベースパイプライン名 / Base pipeline name + * @param {Record} constants - オーバーライド定数 / Override constant values + * @return {GPURenderPipeline | undefined} パイプラインまたはundefined / The pipeline or undefined */ - getFilterPipeline(baseName: string, constants: Record): GPURenderPipeline | undefined + getFilterPipeline(base_name: string, constants: Record): GPURenderPipeline | undefined { // キャッシュキーを生成 const keys = Object.keys(constants).sort(); const suffix = keys.map((k) => `${k}${constants[k]}`).join("_"); - const cacheKey = `${baseName}_${suffix}`; + const cacheKey = `${base_name}_${suffix}`; let pipeline = this.pipelines.get(cacheKey); if (pipeline) { @@ -2782,14 +2990,14 @@ export class PipelineManager } // ベースグループのロードを確保 - this.ensureLazyGroup(baseName); + this.ensureLazyGroup(base_name); - const fragmentModule = this.shaderModuleCache.get(`filter_${baseName}`); + const fragmentModule = this.shaderModuleCache.get(`filter_${base_name}`); const vertexModule = this.shaderModuleCache.get("blurFilterVertex"); - const bindGroupLayout = this.bindGroupLayouts.get(baseName); + const bindGroupLayout = this.bindGroupLayouts.get(base_name); if (!fragmentModule || !vertexModule || !bindGroupLayout) { - return this.pipelines.get(baseName); + return this.pipelines.get(base_name); } const pipelineLayout = this.device.createPipelineLayout({ @@ -2826,57 +3034,85 @@ export class PipelineManager } /** - * @description グラデーションタイプとスプレッドモードに応じた特殊化パイプラインを取得 - * override定数でGPU warp divergenceを排除 + * @description グラデーションタイプとスプレッドモードに応じた特殊化パイプラインを取得する。override定数でGPU warp divergenceを排除 + * Get a gradient pipeline specialized by gradient type and spread mode. Uses override constants to eliminate GPU warp divergence + * @param {string} base_name - ベースパイプライン名 / Base pipeline name + * @param {number} gradient_type - グラデーションタイプ / Gradient type identifier + * @param {number} spread_mode - スプレッドモード / Spread mode identifier + * @return {GPURenderPipeline | undefined} パイプラインまたはundefined / The pipeline or undefined */ - getGradientPipeline(baseName: string, gradientType: number, spreadMode: number): GPURenderPipeline | undefined + getGradientPipeline(base_name: string, gradient_type: number, spread_mode: number): GPURenderPipeline | undefined { - const key = `${baseName}_t${gradientType}s${spreadMode}`; + const key = `${base_name}_t${gradient_type}s${spread_mode}`; let pipeline = this.pipelines.get(key); if (pipeline) { return pipeline; } if (!this.gradientPipelineLayout) { - return this.getPipeline(baseName); + return this.getPipeline(base_name); } // ベースパイプラインと同じ構成でoverride定数を変えて作成 - pipeline = this.createGradientVariant(baseName, gradientType, spreadMode); + pipeline = this.createGradientVariant(base_name, gradient_type, spread_mode); if (pipeline) { this.pipelines.set(key, pipeline); return pipeline; } // フォールバック: デフォルト定数のベースパイプラインを使用 - return this.getPipeline(baseName); + return this.getPipeline(base_name); } + /** + * @description グラデーション用パイプラインレイアウト + * Pipeline layout for gradient pipelines + */ private gradientPipelineLayout: GPUPipelineLayout | null = null; + /** + * @description グラデーション用頂点シェーダーモジュール + * Vertex shader module for gradient pipelines + */ private gradientVertexShaderModule: GPUShaderModule | null = null; + /** + * @description グラデーション用フラグメントシェーダーモジュール + * Fragment shader module for gradient pipelines + */ private gradientFragmentShaderModule: GPUShaderModule | null = null; + /** + * @description グラデーション用ステンシルフラグメントシェーダーモジュール + * Stencil fragment shader module for gradient pipelines + */ private gradientStencilFragmentShaderModule: GPUShaderModule | null = null; - private createGradientVariant(baseName: string, gradientType: number, spreadMode: number): GPURenderPipeline | undefined + /** + * @description グラデーションバリアントパイプラインを作成する(override定数による特殊化) + * Create a gradient variant pipeline with override constants for specialization + * @param {string} base_name - ベースパイプライン名 / Base pipeline name + * @param {number} gradient_type - グラデーションタイプ / Gradient type identifier + * @param {number} spread_mode - スプレッドモード / Spread mode identifier + * @return {GPURenderPipeline | undefined} パイプラインまたはundefined / The pipeline or undefined + */ + private createGradientVariant(base_name: string, gradient_type: number, spread_mode: number): GPURenderPipeline | undefined { if (!this.gradientPipelineLayout) { return undefined; } const constants = { - "GRADIENT_TYPE": gradientType, - "SPREAD_MODE": spreadMode + "GRADIENT_TYPE": gradient_type, + "SPREAD_MODE": spread_mode }; - const vertexBufferLayout = VERTEX_BUFFER_LAYOUT_4F; - const blendState = BLEND_PREMULTIPLIED_ALPHA; + const vertexBufferLayout = $VERTEX_BUFFER_LAYOUT_4F; + const blendState = $BLEND_PREMULTIPLIED_ALPHA; // ベース名からパイプライン構成を決定 - const isStencilFragment = baseName.includes("stencil_atlas") || baseName === "gradient_fill_stencil_main"; + const isStencilFragment = base_name.includes("stencil_atlas") || base_name === "gradient_fill_stencil_main"; const fragModule = isStencilFragment ? this.gradientStencilFragmentShaderModule! : this.gradientFragmentShaderModule!; - const isBGRA = baseName.includes("bgra") || baseName === "gradient_fill_stencil_main"; + const isBGRA = base_name.includes("bgra") || base_name === "gradient_fill_stencil_main"; const format: GPUTextureFormat = isBGRA ? this.format : "rgba8unorm"; - const needsYFlip = baseName.includes("bgra") || baseName === "gradient_fill_stencil_main"; + const needsYFlip = base_name.includes("bgra") || base_name === "gradient_fill_stencil_main"; const vertexConstants: Record = {}; if (needsYFlip) { @@ -2886,7 +3122,7 @@ export class PipelineManager let depthStencil: GPUDepthStencilState | undefined; let sampleCount = this.sampleCount; - if (baseName.includes("stroke")) { + if (base_name.includes("stroke")) { depthStencil = { "format": "stencil8", "stencilFront": { "compare": "always", "failOp": "keep", "depthFailOp": "keep", "passOp": "keep" }, @@ -2894,7 +3130,7 @@ export class PipelineManager "stencilReadMask": 0x00, "stencilWriteMask": 0x00 }; - } else if (baseName === "gradient_fill_stencil" || baseName === "gradient_fill_stencil_atlas") { + } else if (base_name === "gradient_fill_stencil" || base_name === "gradient_fill_stencil_atlas") { depthStencil = { "format": "stencil8", "stencilFront": { "compare": "not-equal", "failOp": "keep", "depthFailOp": "zero", "passOp": "zero" }, @@ -2902,7 +3138,7 @@ export class PipelineManager "stencilReadMask": 0xFF, "stencilWriteMask": 0xFF }; - } else if (baseName === "gradient_fill_stencil_main") { + } else if (base_name === "gradient_fill_stencil_main") { depthStencil = { "format": "stencil8", "stencilFront": { "compare": "not-equal", "failOp": "keep", "depthFailOp": "zero", "passOp": "zero" }, @@ -2911,7 +3147,7 @@ export class PipelineManager "stencilWriteMask": 0xFF }; sampleCount = 1; - } else if (baseName === "gradient_fill_bgra_stencil") { + } else if (base_name === "gradient_fill_bgra_stencil") { depthStencil = { "format": "stencil8", "stencilFront": { "compare": "equal", "failOp": "keep", "depthFailOp": "keep", "passOp": "keep" }, @@ -2919,7 +3155,7 @@ export class PipelineManager "stencilReadMask": 0xFF, "stencilWriteMask": 0x00 }; - } else if (baseName === "gradient_fill_bgra_no_msaa") { + } else if (base_name === "gradient_fill_bgra_no_msaa") { sampleCount = 1; } @@ -2948,6 +3184,12 @@ export class PipelineManager return this.device.createRenderPipeline(descriptor); } + /** + * @description 名前からバインドグループレイアウトを取得する(遅延初期化を含む) + * Get a bind group layout by name, initializing lazy groups if needed + * @param {string} name - バインドグループレイアウト名 / Bind group layout name + * @return {GPUBindGroupLayout | undefined} レイアウトまたはundefined / The layout or undefined + */ getBindGroupLayout(name: string): GPUBindGroupLayout | undefined { let layout = this.bindGroupLayouts.get(name); @@ -2958,6 +3200,10 @@ export class PipelineManager return layout; } + /** + * @description ノードクリア用パイプラインを作成する(カラーとステンシルの同時クリア) + * Create node clear pipeline for simultaneous color and stencil clear + */ private createNodeClearPipeline(): void { const vertexBufferLayout: GPUVertexBufferLayout = { @@ -3025,6 +3271,10 @@ export class PipelineManager this.pipelines.set("node_clear_atlas", nodeClearPipeline); } + /** + * @description すべてのパイプライン・レイアウト・シェーダーモジュールを解放する + * Dispose all pipelines, layouts, and shader module caches + */ dispose(): void { this.pipelines.clear(); diff --git a/packages/webgpu/src/Shader/ShaderInstancedManager.ts b/packages/webgpu/src/Shader/ShaderInstancedManager.ts index b2d3d233..9627c0a2 100644 --- a/packages/webgpu/src/Shader/ShaderInstancedManager.ts +++ b/packages/webgpu/src/Shader/ShaderInstancedManager.ts @@ -2,16 +2,33 @@ import { renderQueue } from "@next2d/render-queue"; /** * @description WebGPU用インスタンスシェーダーマネージャー + * WebGPU instanced shader manager for batch rendering */ export class ShaderInstancedManager { + /** + * @description 現在のインスタンス描画カウント + * Current instance draw count + * + * @type {number} + */ public count: number; + /** + * @description ShaderInstancedManagerを初期化する + * Initialize ShaderInstancedManager + */ constructor() { this.count = 0; } + /** + * @description インスタンスカウントとレンダーキューオフセットをリセットする + * Reset instance count and render queue offset + * + * @return {void} + */ clear(): void { this.count = renderQueue.offset = 0; diff --git a/packages/webgpu/src/Shader/ShaderSource.test.ts b/packages/webgpu/src/Shader/ShaderSource.test.ts index 2959403f..0fa42ca8 100644 --- a/packages/webgpu/src/Shader/ShaderSource.test.ts +++ b/packages/webgpu/src/Shader/ShaderSource.test.ts @@ -46,32 +46,6 @@ describe("ShaderSource", () => }); }); - describe("getFillMainVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = ShaderSource.getFillMainVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = ShaderSource.getFillMainVertexShader(); - - expect(shader).toContain("@vertex"); - }); - - it("should return same shader as non-Main variant (uses @override yFlipSign)", () => - { - const mainShader = ShaderSource.getFillMainVertexShader(); - const atlasShader = ShaderSource.getFillVertexShader(); - - expect(mainShader).toBe(atlasShader); - }); - }); - describe("getFillFragmentShader", () => { it("should return a valid WGSL fragment shader string", () => @@ -122,32 +96,6 @@ describe("ShaderSource", () => }); }); - describe("getStencilWriteMainVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = ShaderSource.getStencilWriteMainVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = ShaderSource.getStencilWriteMainVertexShader(); - - expect(shader).toContain("@vertex"); - }); - - it("should return same shader as non-Main variant (uses @override yFlipSign)", () => - { - const mainShader = ShaderSource.getStencilWriteMainVertexShader(); - const atlasShader = ShaderSource.getStencilWriteVertexShader(); - - expect(mainShader).toBe(atlasShader); - }); - }); - describe("getStencilWriteFragmentShader", () => { it("should return a valid WGSL fragment shader string", () => @@ -264,32 +212,6 @@ describe("ShaderSource", () => }); }); - describe("getBasicMainVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = ShaderSource.getBasicMainVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = ShaderSource.getBasicMainVertexShader(); - - expect(shader).toContain("@vertex"); - }); - - it("should return same shader as non-Main variant (uses @override yFlipSign)", () => - { - const mainShader = ShaderSource.getBasicMainVertexShader(); - const atlasShader = ShaderSource.getBasicVertexShader(); - - expect(mainShader).toBe(atlasShader); - }); - }); - describe("getBasicFragmentShader", () => { it("should return a valid WGSL fragment shader string", () => @@ -402,24 +324,6 @@ describe("ShaderSource", () => }); }); - describe("getGradientFillMainVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = ShaderSource.getGradientFillMainVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = ShaderSource.getGradientFillMainVertexShader(); - - expect(shader).toContain("@vertex"); - }); - }); - describe("getGradientFillFragmentShader", () => { it("should return a valid WGSL fragment shader string", () => @@ -445,24 +349,6 @@ describe("ShaderSource", () => }); }); - describe("getGradientFragmentShader", () => - { - it("should return a valid WGSL fragment shader string", () => - { - const shader = ShaderSource.getGradientFragmentShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = ShaderSource.getGradientFragmentShader(); - - expect(shader).toContain("@fragment"); - }); - }); - describe("getBitmapFillVertexShader", () => { it("should return a valid WGSL vertex shader string", () => @@ -481,24 +367,6 @@ describe("ShaderSource", () => }); }); - describe("getBitmapFillMainVertexShader", () => - { - it("should return a valid WGSL vertex shader string", () => - { - const shader = ShaderSource.getBitmapFillMainVertexShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @vertex attribute", () => - { - const shader = ShaderSource.getBitmapFillMainVertexShader(); - - expect(shader).toContain("@vertex"); - }); - }); - describe("getBitmapFillFragmentShader", () => { it("should return a valid WGSL fragment shader string", () => @@ -844,46 +712,6 @@ describe("ShaderSource", () => }); }); - describe("getComplexBlendFragmentShader", () => - { - it("should return a valid WGSL fragment shader (unified)", () => - { - const shader = ShaderSource.getComplexBlendFragmentShader(); - - expect(typeof shader).toBe("string"); - expect(shader.length).toBeGreaterThan(0); - }); - - it("should contain @fragment attribute", () => - { - const shader = ShaderSource.getComplexBlendFragmentShader(); - - expect(shader).toContain("@fragment"); - }); - - it("should include blend function", () => - { - const shader = ShaderSource.getComplexBlendFragmentShader(); - - expect(shader).toContain("fn blend"); - }); - - it("should support step-based blend modes (lighten/darken)", () => - { - const shader = ShaderSource.getComplexBlendFragmentShader(); - - expect(shader).toContain("step(srcRgb, dstRgb)"); - expect(shader).toContain("step(dstRgb, srcRgb)"); - }); - - it("should include blendMode uniform", () => - { - const shader = ShaderSource.getComplexBlendFragmentShader(); - - expect(shader).toContain("blendMode"); - }); - }); - describe("getDisplacementMapFilterFragmentShader", () => { it("should return a valid WGSL fragment shader string", () => @@ -1024,18 +852,13 @@ describe("ShaderSource", () => { const vertexShaders = [ { name: "getFillVertexShader", fn: () => ShaderSource.getFillVertexShader() }, - { name: "getFillMainVertexShader", fn: () => ShaderSource.getFillMainVertexShader() }, { name: "getStencilWriteVertexShader", fn: () => ShaderSource.getStencilWriteVertexShader() }, - { name: "getStencilWriteMainVertexShader", fn: () => ShaderSource.getStencilWriteMainVertexShader() }, { name: "getStencilFillVertexShader", fn: () => ShaderSource.getStencilFillVertexShader() }, { name: "getMaskVertexShader", fn: () => ShaderSource.getMaskVertexShader() }, { name: "getBasicVertexShader", fn: () => ShaderSource.getBasicVertexShader() }, - { name: "getBasicMainVertexShader", fn: () => ShaderSource.getBasicMainVertexShader() }, { name: "getInstancedVertexShader", fn: () => ShaderSource.getInstancedVertexShader() }, { name: "getGradientFillVertexShader", fn: () => ShaderSource.getGradientFillVertexShader() }, - { name: "getGradientFillMainVertexShader", fn: () => ShaderSource.getGradientFillMainVertexShader() }, { name: "getBitmapFillVertexShader", fn: () => ShaderSource.getBitmapFillVertexShader() }, - { name: "getBitmapFillMainVertexShader", fn: () => ShaderSource.getBitmapFillMainVertexShader() }, { name: "getBlurFilterVertexShader", fn: () => ShaderSource.getBlurFilterVertexShader() }, { name: "getNodeClearVertexShader", fn: () => ShaderSource.getNodeClearVertexShader() }, { name: "getPositionedTextureVertexShader", fn: () => ShaderSource.getPositionedTextureVertexShader() } @@ -1050,7 +873,6 @@ describe("ShaderSource", () => { name: "getTextureFragmentShader", fn: () => ShaderSource.getTextureFragmentShader() }, { name: "getInstancedFragmentShader", fn: () => ShaderSource.getInstancedFragmentShader() }, { name: "getGradientFillFragmentShader", fn: () => ShaderSource.getGradientFillFragmentShader() }, - { name: "getGradientFragmentShader", fn: () => ShaderSource.getGradientFragmentShader() }, { name: "getBitmapFillFragmentShader", fn: () => ShaderSource.getBitmapFillFragmentShader() }, { name: "getBlendFragmentShader", fn: () => ShaderSource.getBlendFragmentShader() }, { name: "getTextureCopyFragmentShader", fn: () => ShaderSource.getTextureCopyFragmentShader() }, diff --git a/packages/webgpu/src/Shader/ShaderSource.ts b/packages/webgpu/src/Shader/ShaderSource.ts index 824c5f7f..094c55fd 100644 --- a/packages/webgpu/src/Shader/ShaderSource.ts +++ b/packages/webgpu/src/Shader/ShaderSource.ts @@ -12,7 +12,7 @@ import { StencilWriteFragment, StencilFillFragment } from "./wgsl/fragment/Stenc import { MaskFragment } from "./wgsl/fragment/MaskFragment"; import { BasicFragment, TextureFragment } from "./wgsl/fragment/BasicFragment"; import { InstancedFragment } from "./wgsl/fragment/InstancedFragment"; -import { GradientFillFragment, GradientFillStencilFragment, GradientFragment } from "./wgsl/fragment/GradientFragment"; +import { GradientFillFragment, GradientFillStencilFragment } from "./wgsl/fragment/GradientFragment"; import { BitmapFillFragment } from "./wgsl/fragment/BitmapFragment"; import { TextureCopyFragment, @@ -36,156 +36,264 @@ import { } from "./wgsl/fragment/EffectFragment"; import { WgslIsInside, WgslVertexOutput } from "./wgsl/common/SharedWgsl"; +/** + * @description WebGPU用シェーダーソース管理クラス + * WebGPU shader source management class providing all vertex and fragment shaders + */ export class ShaderSource { + /** + * @description 塗り用頂点シェーダーを取得する + * Get fill vertex shader + * + * @return {string} + */ static getFillVertexShader (): string { return FillVertex; } - static getFillMainVertexShader (): string - { - return FillVertex; - } - + /** + * @description 塗り用フラグメントシェーダーを取得する + * Get fill fragment shader + * + * @return {string} + */ static getFillFragmentShader (): string { return FillFragment; } + /** + * @description ステンシル書き込み用頂点シェーダーを取得する + * Get stencil write vertex shader + * + * @return {string} + */ static getStencilWriteVertexShader (): string { return StencilWriteVertex; } - static getStencilWriteMainVertexShader (): string - { - return StencilWriteVertex; - } - + /** + * @description ステンシル書き込み用フラグメントシェーダーを取得する + * Get stencil write fragment shader + * + * @return {string} + */ static getStencilWriteFragmentShader (): string { return StencilWriteFragment; } + /** + * @description ステンシル塗り用頂点シェーダーを取得する + * Get stencil fill vertex shader + * + * @return {string} + */ static getStencilFillVertexShader (): string { return StencilFillVertex; } - static getStencilFillMainVertexShader (): string - { - return StencilFillVertex; - } - + /** + * @description ステンシル塗り用フラグメントシェーダーを取得する + * Get stencil fill fragment shader + * + * @return {string} + */ static getStencilFillFragmentShader (): string { return StencilFillFragment; } + /** + * @description マスク用頂点シェーダーを取得する + * Get mask vertex shader + * + * @return {string} + */ static getMaskVertexShader (): string { return MaskVertex; } + /** + * @description マスク用フラグメントシェーダーを取得する + * Get mask fragment shader + * + * @return {string} + */ static getMaskFragmentShader (): string { return MaskFragment; } + /** + * @description 基本頂点シェーダーを取得する + * Get basic vertex shader + * + * @return {string} + */ static getBasicVertexShader (): string { return BasicVertex; } - static getBasicMainVertexShader (): string - { - return BasicVertex; - } - + /** + * @description 基本フラグメントシェーダーを取得する + * Get basic fragment shader + * + * @return {string} + */ static getBasicFragmentShader (): string { return BasicFragment; } + /** + * @description テクスチャフラグメントシェーダーを取得する + * Get texture fragment shader + * + * @return {string} + */ static getTextureFragmentShader (): string { return TextureFragment; } + /** + * @description インスタンス描画用頂点シェーダーを取得する + * Get instanced vertex shader + * + * @return {string} + */ static getInstancedVertexShader (): string { return InstancedVertex; } + /** + * @description インスタンス描画用フラグメントシェーダーを取得する + * Get instanced fragment shader + * + * @return {string} + */ static getInstancedFragmentShader (): string { return InstancedFragment; } + /** + * @description グラデーション塗り用頂点シェーダーを取得する + * Get gradient fill vertex shader + * + * @return {string} + */ static getGradientFillVertexShader (): string { return GradientFillVertex; } - static getGradientFillMainVertexShader (): string - { - return GradientFillVertex; - } - + /** + * @description グラデーション塗り用フラグメントシェーダーを取得する + * Get gradient fill fragment shader + * + * @return {string} + */ static getGradientFillFragmentShader (): string { return GradientFillFragment; } + /** + * @description ステンシル用グラデーション塗りフラグメントシェーダーを取得する + * Get gradient fill stencil fragment shader + * + * @return {string} + */ static getGradientFillStencilFragmentShader (): string { return GradientFillStencilFragment; } - static getGradientFragmentShader (): string - { - return GradientFragment; - } - + /** + * @description ビットマップ塗り用頂点シェーダーを取得する + * Get bitmap fill vertex shader + * + * @return {string} + */ static getBitmapFillVertexShader (): string { return BitmapFillVertex; } - static getBitmapFillMainVertexShader (): string - { - return BitmapFillVertex; - } - + /** + * @description ビットマップ塗り用フラグメントシェーダーを取得する + * Get bitmap fill fragment shader + * + * @return {string} + */ static getBitmapFillFragmentShader (): string { return BitmapFillFragment; } + /** + * @description ブレンド用フラグメントシェーダーを取得する + * Get blend fragment shader + * + * @return {string} + */ static getBlendFragmentShader (): string { return BlendGenericFragment; } + /** + * @description ブラーフィルター用頂点シェーダーを取得する + * Get blur filter vertex shader + * + * @return {string} + */ static getBlurFilterVertexShader (): string { return BlurFilterVertex; } + /** + * @description ビットマップ同期用頂点シェーダーを取得する + * Get bitmap sync vertex shader + * + * @return {string} + */ static getBitmapSyncVertexShader (): string { return BitmapSyncVertex; } + /** + * @description ビットマップ同期用フラグメントシェーダーを取得する + * Get bitmap sync fragment shader + * + * @return {string} + */ static getBitmapSyncFragmentShader (): string { return BitmapSyncFragment; } - static getBlurFilterFragmentShader (halfBlur: number): string + /** + * @description ブラーフィルター用フラグメントシェーダーを生成する + * Generate blur filter fragment shader + * + * @param {number} half_blur - ブラーの半径値 + * @return {string} + */ + static getBlurFilterFragmentShader (half_blur: number): string { - const halfBlurFixed = halfBlur.toFixed(1); + const halfBlurFixed = half_blur.toFixed(1); return /* wgsl */` ${WgslVertexOutput} @@ -218,76 +326,158 @@ fn main(input: VertexOutput) -> @location(0) vec4 { `; } + /** + * @description テクスチャコピー用フラグメントシェーダーを取得する + * Get texture copy fragment shader + * + * @return {string} + */ static getTextureCopyFragmentShader (): string { return TextureCopyFragment; } + /** + * @description ブラー用テクスチャコピーフラグメントシェーダーを取得する + * Get blur texture copy fragment shader + * + * @return {string} + */ static getBlurTextureCopyFragmentShader (): string { return BlurTextureCopyFragment; } + /** + * @description フィルター出力用フラグメントシェーダーを取得する + * Get filter output fragment shader + * + * @return {string} + */ static getFilterOutputFragmentShader (): string { return FilterOutputFragment; } + /** + * @description カラー変換フラグメントシェーダーを取得する + * Get color transform fragment shader + * + * @return {string} + */ static getColorTransformFragmentShader (): string { return ColorTransformFragment; } + /** + * @description Y軸反転付きカラー変換フラグメントシェーダーを取得する + * Get Y-flip color transform fragment shader + * + * @return {string} + */ static getYFlipColorTransformFragmentShader (): string { return YFlipColorTransformFragment; } + /** + * @description カラーマトリクスフィルターフラグメントシェーダーを取得する + * Get color matrix filter fragment shader + * + * @return {string} + */ static getColorMatrixFilterFragmentShader (): string { return ColorMatrixFilterFragment; } + /** + * @description グローフィルターフラグメントシェーダーを取得する + * Get glow filter fragment shader + * + * @return {string} + */ static getGlowFilterFragmentShader (): string { return GlowFilterFragment; } + /** + * @description ドロップシャドウフィルターフラグメントシェーダーを取得する + * Get drop shadow filter fragment shader + * + * @return {string} + */ static getDropShadowFilterFragmentShader (): string { return DropShadowFilterFragment; } + /** + * @description グラデーショングローフィルターフラグメントシェーダーを取得する + * Get gradient glow filter fragment shader + * + * @return {string} + */ static getGradientGlowFilterFragmentShader (): string { return GradientGlowFilterFragment; } + /** + * @description グラデーションベベルフィルターフラグメントシェーダーを取得する + * Get gradient bevel filter fragment shader + * + * @return {string} + */ static getGradientBevelFilterFragmentShader (): string { return GradientBevelFilterFragment; } + /** + * @description ベベルフィルターフラグメントシェーダーを取得する + * Get bevel filter fragment shader + * + * @return {string} + */ static getBevelFilterFragmentShader (): string { return BevelFilterFragment; } + /** + * @description ベベルフィルターベース処理フラグメントシェーダーを取得する + * Get bevel filter base fragment shader + * + * @return {string} + */ static getBevelBaseFragmentShader (): string { return BevelBaseFragment; } + /** + * @description コンボリューション(畳み込み)フィルターフラグメントシェーダーを生成する + * Generate convolution filter fragment shader + * + * @param {number} matrix_x - コンボリューション行列のX次元サイズ + * @param {number} matrix_y - コンボリューション行列のY次元サイズ + * @param {boolean} [preserve_alpha=true] - 元のアルファ値を保持するかどうか + * @param {boolean} [clamp=true] - UV座標を範囲内にクランプするかどうか + * @return {string} + */ static getConvolutionFilterFragmentShader ( - matrixX: number, - matrixY: number, - preserveAlpha: boolean = true, + matrix_x: number, + matrix_y: number, + preserve_alpha: boolean = true, clamp: boolean = true ): string { - const halfX = Math.floor(matrixX * 0.5); - const halfY = Math.floor(matrixY * 0.5); - const size = matrixX * matrixY; + const halfX = Math.floor(matrix_x * 0.5); + const halfY = Math.floor(matrix_y * 0.5); + const size = matrix_x * matrix_y; let matrixStatement = ""; for (let idx = 0; idx < size; idx++) { @@ -295,7 +485,7 @@ fn main(input: VertexOutput) -> @location(0) vec4 { result = result + getWeightedColor(${idx}, getMatrixWeight(${idx}));`; } - const preserveAlphaStatement = preserveAlpha + const preserveAlphaStatement = preserve_alpha ? "result.a = textureSample(sourceTexture, sourceSampler, input.texCoord).a;" : ""; @@ -334,8 +524,8 @@ fn getMatrixWeight(index: i32) -> f32 { fn getWeightedColor(i: i32, weight: f32) -> vec4 { let rcpSize = uniforms.rcpSize; - let iDivX = i / ${matrixX}; - let iModX = i - ${matrixX} * iDivX; + let iDivX = i / ${matrix_x}; + let iModX = i - ${matrix_x} * iDivX; let offset = vec2(f32(iModX - ${halfX}), f32(${halfY} - iDivX)); var uv = input.texCoord + offset * rcpSize; var color = textureSample(sourceTexture, sourceSampler, uv); @@ -385,14 +575,16 @@ fn fs_main(fragInput: VertexOutput) -> @location(0) vec4 { `; } - static getComplexBlendFragmentShader (): string - { - return ShaderSource.getUnifiedComplexBlendFragmentShader(); - } - - static getBlendModeIndex (blendMode: string): number + /** + * @description ブレンドモード名からインデックスを取得する + * Get blend mode index from blend mode name + * + * @param {string} blend_mode - ブレンドモード名 + * @return {number} + */ + static getBlendModeIndex (blend_mode: string): number { - switch (blendMode) { + switch (blend_mode) { case "subtract": return 0; case "multiply": return 1; case "lighten": return 2; @@ -405,6 +597,12 @@ fn fs_main(fragInput: VertexOutput) -> @location(0) vec4 { } } + /** + * @description 統合複合ブレンドフラグメントシェーダーを取得する + * Get unified complex blend fragment shader + * + * @return {string} + */ static getUnifiedComplexBlendFragmentShader (): string { return /* wgsl */` @@ -498,16 +696,25 @@ fn main(input: VertexOutput) -> @location(0) vec4 { `; } + /** + * @description ディスプレースメントマップフィルターフラグメントシェーダーを生成する + * Generate displacement map filter fragment shader + * + * @param {number} component_x - X方向の色コンポーネント (1:R, 2:G, 4:B, 8:A) + * @param {number} component_y - Y方向の色コンポーネント (1:R, 2:G, 4:B, 8:A) + * @param {number} mode - マッピングモード (0:wrap, 1:color, 2:repeat, 3:clamp) + * @return {string} + */ static getDisplacementMapFilterFragmentShader ( - componentX: number, - componentY: number, + component_x: number, + component_y: number, mode: number ): string { let cx: string; let cy: string; - switch (componentX) { + switch (component_x) { case 1: cx = "mapColor.r"; break; @@ -525,7 +732,7 @@ fn main(input: VertexOutput) -> @location(0) vec4 { break; } - switch (componentY) { + switch (component_y) { case 1: cy = "mapColor.r"; break; @@ -617,56 +824,122 @@ fn main(input: VertexOutput) -> @location(0) vec4 { `; } + /** + * @description ノードクリア用頂点シェーダーを取得する + * Get node clear vertex shader + * + * @return {string} + */ static getNodeClearVertexShader (): string { return NodeClearVertex; } + /** + * @description ノードクリア用フラグメントシェーダーを取得する + * Get node clear fragment shader + * + * @return {string} + */ static getNodeClearFragmentShader (): string { return NodeClearFragment; } + /** + * @description 位置指定テクスチャ用頂点シェーダーを取得する + * Get positioned texture vertex shader + * + * @return {string} + */ static getPositionedTextureVertexShader (): string { return PositionedTextureVertex; } + /** + * @description テクスチャスケール用頂点シェーダーを取得する + * Get texture scale vertex shader + * + * @return {string} + */ static getTextureScaleVertexShader (): string { return TextureScaleVertex; } + /** + * @description テクスチャスケールブレンド用頂点シェーダーを取得する + * Get texture scale blend vertex shader + * + * @return {string} + */ static getTextureScaleBlendVertexShader (): string { return TextureScaleBlendVertex; } + /** + * @description 複合ブレンドスケール用頂点シェーダーを取得する + * Get complex blend scale vertex shader + * + * @return {string} + */ static getComplexBlendScaleVertexShader (): string { return ComplexBlendScaleVertex; } + /** + * @description 複合ブレンド用頂点シェーダーを取得する + * Get complex blend vertex shader + * + * @return {string} + */ static getComplexBlendVertexShader (): string { return ComplexBlendVertex; } + /** + * @description 複合ブレンドコピー用頂点シェーダーを取得する + * Get complex blend copy vertex shader + * + * @return {string} + */ static getComplexBlendCopyVertexShader (): string { return ComplexBlendCopyVertex; } + /** + * @description 複合ブレンド出力用頂点シェーダーを取得する + * Get complex blend output vertex shader + * + * @return {string} + */ static getComplexBlendOutputVertexShader (): string { return ComplexBlendOutputVertex; } + /** + * @description フィルター複合ブレンド出力用頂点シェーダーを取得する + * Get filter complex blend output vertex shader + * + * @return {string} + */ static getFilterComplexBlendOutputVertexShader (): string { return FilterComplexBlendOutputVertex; } + /** + * @description 位置指定テクスチャ用フラグメントシェーダーを取得する + * Get positioned texture fragment shader + * + * @return {string} + */ static getPositionedTextureFragmentShader (): string { return PositionedTextureFragment; diff --git a/packages/webgpu/src/Shader/wgsl/common/SharedWgsl.ts b/packages/webgpu/src/Shader/wgsl/common/SharedWgsl.ts index 3e81858f..46c090d1 100644 --- a/packages/webgpu/src/Shader/wgsl/common/SharedWgsl.ts +++ b/packages/webgpu/src/Shader/wgsl/common/SharedWgsl.ts @@ -1,9 +1,23 @@ +/** + * @description UV座標が0〜1の範囲内にあるかを判定するWGSLヘルパー関数 + * WGSL helper function that checks if UV coordinates are within the 0-1 range + * + * @type {string} + * @constant + */ export const WgslIsInside = ` fn isInside(uv: vec2) -> f32 { let s = step(vec2(0.0), uv) * step(uv, vec2(1.0)); return s.x * s.y; }`; +/** + * @description フルスクリーン四角形の頂点座標定義(NDC空間) + * Full-screen quad vertex positions definition in NDC space + * + * @type {string} + * @constant + */ export const WgslFullscreenPositions = ` const positions = array, 6>( vec2(-1.0, -1.0), @@ -14,6 +28,13 @@ export const WgslFullscreenPositions = ` vec2( 1.0, 1.0) );`; +/** + * @description 単位四角形の頂点座標定義(0〜1空間) + * Unit quad vertex positions definition in 0-1 space + * + * @type {string} + * @constant + */ export const WgslUnitQuadVertices = ` const vertices = array, 6>( vec2(0.0, 0.0), @@ -24,18 +45,15 @@ export const WgslUnitQuadVertices = ` vec2(1.0, 1.0) );`; +/** + * @description 標準的な頂点シェーダー出力構造体のWGSL定義 + * WGSL definition of the standard vertex shader output struct + * + * @type {string} + * @constant + */ export const WgslVertexOutput = ` struct VertexOutput { @builtin(position) position: vec4, @location(0) texCoord: vec2, }`; - -export const WgslFullscreenTexCoords = ` - const texCoords = array, 6>( - vec2(0.0, 1.0), - vec2(1.0, 1.0), - vec2(0.0, 0.0), - vec2(0.0, 0.0), - vec2(1.0, 1.0), - vec2(1.0, 0.0) - );`; diff --git a/packages/webgpu/src/Shader/wgsl/fragment/BasicFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/BasicFragment.ts index 4b2982e0..d1632b23 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/BasicFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/BasicFragment.ts @@ -1,3 +1,10 @@ +/** + * @description 頂点カラーをそのまま出力する基本フラグメントシェーダー + * Basic fragment shader that outputs vertex color directly + * + * @type {string} + * @constant + */ export const BasicFragment = /* wgsl */` struct VertexOutput { @builtin(position) position: vec4, @@ -11,6 +18,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description テクスチャサンプリングと頂点カラーを乗算するフラグメントシェーダー + * Fragment shader that multiplies texture sampling with vertex color + * + * @type {string} + * @constant + */ export const TextureFragment = /* wgsl */` struct VertexOutput { @builtin(position) position: vec4, diff --git a/packages/webgpu/src/Shader/wgsl/fragment/BitmapFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/BitmapFragment.ts index 82cbc744..a46dc26c 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/BitmapFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/BitmapFragment.ts @@ -1,3 +1,10 @@ +/** + * @description ビットマップ塗りフラグメントシェーダー(ベジェクリッピング・リピート対応) + * Bitmap fill fragment shader with bezier clipping and repeat support + * + * @type {string} + * @constant + */ export const BitmapFillFragment = /* wgsl */` struct VertexOutput { @builtin(position) position: vec4, diff --git a/packages/webgpu/src/Shader/wgsl/fragment/BlendFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/BlendFragment.ts deleted file mode 100644 index b3cf048d..00000000 --- a/packages/webgpu/src/Shader/wgsl/fragment/BlendFragment.ts +++ /dev/null @@ -1,87 +0,0 @@ -/** - * @description ブレンドシェーダー共通ヘッダー - */ -const BLEND_HEADER = /* wgsl */`struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, -} - -struct BlendUniforms { - colorTransform: vec4, - addColor: vec4, -} - -@group(0) @binding(0) var uniforms: BlendUniforms; -@group(0) @binding(1) var sampler0: sampler; -@group(0) @binding(2) var texture0: texture_2d; -@group(0) @binding(3) var texture1: texture_2d; -`; - -/** - * @description alpha guard 付きブレンドシェーダーを生成 - */ -const createBlendFragment = (blendLogic: string): string => - BLEND_HEADER + /* wgsl */` -@fragment -fn main(input: VertexOutput) -> @location(0) vec4 { - var src = textureSampleLevel(texture1, sampler0, input.texCoord, 0); - var dst = textureSampleLevel(texture0, sampler0, input.texCoord, 0); - if (src.a == 0.0) { return dst; } - if (dst.a == 0.0) { return src; } - src = src * uniforms.colorTransform + vec4(uniforms.addColor.rgb, 0.0); - let a = src - src * dst.a; - let b = dst - dst * src.a; - var srcRgb = src.rgb / src.a; - var dstRgb = dst.rgb / dst.a; -${blendLogic} - c = vec4(c.rgb * c.a, c.a); - return a + b + c; -} -`; - -export const MultiplyBlendFragment = BLEND_HEADER + /* wgsl */` -@fragment -fn main(input: VertexOutput) -> @location(0) vec4 { - var src = textureSampleLevel(texture1, sampler0, input.texCoord, 0); - var dst = textureSampleLevel(texture0, sampler0, input.texCoord, 0); - src = src * uniforms.colorTransform + vec4(uniforms.addColor.rgb, 0.0); - let a = src - src * dst.a; - let b = dst - dst * src.a; - let c = src * dst; - return a + b + c; -} -`; - -export const ScreenBlendFragment = createBlendFragment( - " var c = vec4(srcRgb + dstRgb - srcRgb * dstRgb, src.a * dst.a);" -); - -export const LightenBlendFragment = createBlendFragment( - " var c = vec4(max(srcRgb, dstRgb), src.a * dst.a);" -); - -export const DarkenBlendFragment = createBlendFragment( - " var c = vec4(min(srcRgb, dstRgb), src.a * dst.a);" -); - -export const OverlayBlendFragment = createBlendFragment( - ` let s = step(vec3(0.5), dstRgb); - let lo = 2.0 * srcRgb * dstRgb; - let hi = 1.0 - 2.0 * (1.0 - srcRgb) * (1.0 - dstRgb); - var c = vec4(mix(lo, hi, s), src.a * dst.a);` -); - -export const HardLightBlendFragment = createBlendFragment( - ` let s = step(vec3(0.5), srcRgb); - let lo = 2.0 * srcRgb * dstRgb; - let hi = 1.0 - 2.0 * (1.0 - srcRgb) * (1.0 - dstRgb); - var c = vec4(mix(lo, hi, s), src.a * dst.a);` -); - -export const DifferenceBlendFragment = createBlendFragment( - " var c = vec4(abs(srcRgb - dstRgb), src.a * dst.a);" -); - -export const SubtractBlendFragment = createBlendFragment( - " var c = vec4(max(dstRgb - srcRgb, vec3(0.0)), src.a * dst.a);" -); diff --git a/packages/webgpu/src/Shader/wgsl/fragment/EffectFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/EffectFragment.ts index 5c94d6e8..cb4ea66e 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/EffectFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/EffectFragment.ts @@ -1,5 +1,12 @@ import { WgslIsInside, WgslVertexOutput } from "../common/SharedWgsl"; +/** + * @description グローフィルター用フラグメントシェーダー(内側・外側・ノックアウト対応) + * Glow filter fragment shader with inner, outer, and knockout support + * + * @type {string} + * @constant + */ export const GlowFilterFragment = /* wgsl */` ${WgslVertexOutput} @@ -55,6 +62,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description ドロップシャドウフィルター用フラグメントシェーダー(内側・外側・ノックアウト・非表示対応) + * Drop shadow filter fragment shader with inner, outer, knockout, and hide-object support + * + * @type {string} + * @constant + */ export const DropShadowFilterFragment = /* wgsl */` ${WgslVertexOutput} @@ -114,6 +128,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description グラデーショングローフィルター用フラグメントシェーダー(LUTベース) + * Gradient glow filter fragment shader using LUT + * + * @type {string} + * @constant + */ export const GradientGlowFilterFragment = /* wgsl */` ${WgslVertexOutput} @@ -172,6 +193,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description グラデーションベベルフィルター用フラグメントシェーダー(LUTベース) + * Gradient bevel filter fragment shader using LUT + * + * @type {string} + * @constant + */ export const GradientBevelFilterFragment = /* wgsl */` ${WgslVertexOutput} @@ -240,6 +268,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description ベベルフィルター用フラグメントシェーダー(ハイライト・シャドウカラー指定) + * Bevel filter fragment shader with highlight and shadow color specification + * + * @type {string} + * @constant + */ export const BevelFilterFragment = /* wgsl */` ${WgslVertexOutput} @@ -307,6 +342,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description ベベルフィルターのベース処理用フラグメントシェーダー(オフセット差分計算) + * Bevel filter base fragment shader for offset difference calculation + * + * @type {string} + * @constant + */ export const BevelBaseFragment = /* wgsl */` ${WgslVertexOutput} diff --git a/packages/webgpu/src/Shader/wgsl/fragment/FillFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/FillFragment.ts index d10cd208..a6342582 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/FillFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/FillFragment.ts @@ -1,3 +1,10 @@ +/** + * @description ベジェ曲線ベースのアンチエイリアス付き塗りフラグメントシェーダー + * Bezier curve-based fill fragment shader with anti-aliasing + * + * @type {string} + * @constant + */ export const FillFragment = /* wgsl */` struct FragmentInput { @builtin(position) position: vec4, diff --git a/packages/webgpu/src/Shader/wgsl/fragment/FilterFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/FilterFragment.ts index a74d025b..89c2974e 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/FilterFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/FilterFragment.ts @@ -1,5 +1,12 @@ import { WgslVertexOutput } from "../common/SharedWgsl"; +/** + * @description テクスチャコピー用フラグメントシェーダー(スケール・オフセット付き) + * Texture copy fragment shader with scale and offset + * + * @type {string} + * @constant + */ export const TextureCopyFragment = /* wgsl */` ${WgslVertexOutput} @@ -19,6 +26,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description ブラー用テクスチャコピーフラグメントシェーダー(境界クランプ付き) + * Blur texture copy fragment shader with boundary clamping + * + * @type {string} + * @constant + */ export const BlurTextureCopyFragment = /* wgsl */` ${WgslVertexOutput} @@ -41,6 +55,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description フィルター出力用フラグメントシェーダー(境界チェック付きコピー) + * Filter output fragment shader with boundary-checked copy + * + * @type {string} + * @constant + */ export const FilterOutputFragment = /* wgsl */` ${WgslVertexOutput} @@ -63,6 +84,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description カラー変換フラグメントシェーダー(乗算・加算カラー適用) + * Color transform fragment shader with multiply and add color application + * + * @type {string} + * @constant + */ export const ColorTransformFragment = /* wgsl */` ${WgslVertexOutput} @@ -87,6 +115,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description Y軸反転付きカラー変換フラグメントシェーダー + * Y-flip color transform fragment shader + * + * @type {string} + * @constant + */ export const YFlipColorTransformFragment = /* wgsl */` ${WgslVertexOutput} @@ -114,6 +149,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description カラーマトリクスフィルター用フラグメントシェーダー + * Color matrix filter fragment shader + * + * @type {string} + * @constant + */ export const ColorMatrixFilterFragment = /* wgsl */` ${WgslVertexOutput} @@ -139,6 +181,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description ノードクリア用フラグメントシェーダー(透明色出力) + * Node clear fragment shader that outputs transparent color + * + * @type {string} + * @constant + */ export const NodeClearFragment = /* wgsl */` @fragment fn main() -> @location(0) vec4 { @@ -146,6 +195,13 @@ fn main() -> @location(0) vec4 { } `; +/** + * @description 位置指定テクスチャサンプリング用フラグメントシェーダー + * Positioned texture sampling fragment shader + * + * @type {string} + * @constant + */ export const PositionedTextureFragment = /* wgsl */` ${WgslVertexOutput} @@ -158,6 +214,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description ビットマップ同期用フラグメントシェーダー(テクスチャ直接サンプリング) + * Bitmap sync fragment shader with direct texture sampling + * + * @type {string} + * @constant + */ export const BitmapSyncFragment = /* wgsl */` ${WgslVertexOutput} @@ -170,6 +233,13 @@ fn main(input: VertexOutput) -> @location(0) vec4 { } `; +/** + * @description 汎用ブレンドフラグメントシェーダー(Normal/Multiply/Screen/Add) + * Generic blend fragment shader supporting Normal, Multiply, Screen, and Add modes + * + * @type {string} + * @constant + */ export const BlendGenericFragment = /* wgsl */` struct VertexOutput { @builtin(position) position: vec4, diff --git a/packages/webgpu/src/Shader/wgsl/fragment/GradientFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/GradientFragment.ts index 53742de4..dfa1bc35 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/GradientFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/GradientFragment.ts @@ -1,4 +1,11 @@ -const GradientUniformsAndSpread = ` +/** + * @description グラデーションのUniform定義とスプレッドモード処理のWGSLコード + * WGSL code for gradient uniform definitions and spread mode handling + * + * @type {string} + * @constant + */ +const $GradientUniformsAndSpread = ` struct GradientUniforms { inverseMatrix: mat3x3, gradientType: f32, @@ -26,7 +33,14 @@ fn applySpread(t: f32) -> f32 { } `; -const GradientCalculation = ` +/** + * @description 線形・放射グラデーションのt値計算WGSLコード + * WGSL code for calculating t value in linear and radial gradients + * + * @type {string} + * @constant + */ +const $GradientCalculation = ` var t: f32; if (GRADIENT_TYPE == 0u) { let a = gradient.linearPoints.xy; @@ -69,6 +83,13 @@ const GradientCalculation = ` let gradientColor = textureSampleLevel(gradientTexture, gradientSampler, vec2(t, 0.5), 0); `; +/** + * @description グラデーション塗りフラグメントシェーダー(頂点カラー乗算付き) + * Gradient fill fragment shader with vertex color multiplication + * + * @type {string} + * @constant + */ export const GradientFillFragment = /* wgsl */` struct VertexOutput { @builtin(position) position: vec4, @@ -76,17 +97,24 @@ struct VertexOutput { @location(1) bezier: vec2, @location(2) color: vec4, } -${GradientUniformsAndSpread} +${$GradientUniformsAndSpread} @fragment fn main(input: VertexOutput) -> @location(0) vec4 { let p = input.v_uv; -${GradientCalculation} +${$GradientCalculation} let result = gradientColor * input.color; return vec4(result.rgb * result.a, result.a); } `; +/** + * @description ステンシル用グラデーション塗りフラグメントシェーダー + * Gradient fill fragment shader for stencil rendering + * + * @type {string} + * @constant + */ export const GradientFillStencilFragment = /* wgsl */` struct VertexOutput { @builtin(position) position: vec4, @@ -94,28 +122,35 @@ struct VertexOutput { @location(1) bezier: vec2, @location(2) color: vec4, } -${GradientUniformsAndSpread} +${$GradientUniformsAndSpread} @fragment fn main(input: VertexOutput) -> @location(0) vec4 { let p = input.v_uv; -${GradientCalculation} +${$GradientCalculation} return vec4(gradientColor.rgb * gradientColor.a, gradientColor.a); } `; +/** + * @description テクスチャ座標ベースのグラデーションフラグメントシェーダー + * Texture coordinate-based gradient fragment shader + * + * @type {string} + * @constant + */ export const GradientFragment = /* wgsl */` struct VertexOutput { @builtin(position) position: vec4, @location(0) texCoord: vec2, @location(1) color: vec4, } -${GradientUniformsAndSpread} +${$GradientUniformsAndSpread} @fragment fn main(input: VertexOutput) -> @location(0) vec4 { let p = input.texCoord; -${GradientCalculation} +${$GradientCalculation} let result = gradientColor * input.color; return vec4(result.rgb * result.a, result.a); } diff --git a/packages/webgpu/src/Shader/wgsl/fragment/InstancedFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/InstancedFragment.ts index c78d99fc..2991d147 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/InstancedFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/InstancedFragment.ts @@ -1,3 +1,10 @@ +/** + * @description インスタンス描画用フラグメントシェーダー(カラー変換付き) + * Instanced rendering fragment shader with color transform + * + * @type {string} + * @constant + */ export const InstancedFragment = /* wgsl */` struct VertexOutput { @builtin(position) position: vec4, diff --git a/packages/webgpu/src/Shader/wgsl/fragment/MaskFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/MaskFragment.ts index 00a04701..75f479e3 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/MaskFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/MaskFragment.ts @@ -1,3 +1,10 @@ +/** + * @description ベジェ曲線ベースのマスク用フラグメントシェーダー + * Bezier curve-based mask fragment shader + * + * @type {string} + * @constant + */ export const MaskFragment = /* wgsl */` struct FragmentInput { @location(0) bezier: vec2, diff --git a/packages/webgpu/src/Shader/wgsl/fragment/StencilFragment.ts b/packages/webgpu/src/Shader/wgsl/fragment/StencilFragment.ts index 3547cd01..8820e4a0 100644 --- a/packages/webgpu/src/Shader/wgsl/fragment/StencilFragment.ts +++ b/packages/webgpu/src/Shader/wgsl/fragment/StencilFragment.ts @@ -1,3 +1,10 @@ +/** + * @description ステンシル書き込み用フラグメントシェーダー(ベジェ曲線アンチエイリアス) + * Stencil write fragment shader with bezier curve anti-aliasing + * + * @type {string} + * @constant + */ export const StencilWriteFragment = /* wgsl */` struct FragmentInput { @builtin(position) position: vec4, @@ -20,6 +27,13 @@ fn main(input: FragmentInput) -> @location(0) vec4 { } `; +/** + * @description ステンシル塗り用フラグメントシェーダー(プリマルチプライドアルファ出力) + * Stencil fill fragment shader with premultiplied alpha output + * + * @type {string} + * @constant + */ export const StencilFillFragment = /* wgsl */` struct FragmentInput { @builtin(position) position: vec4, diff --git a/packages/webgpu/src/Shader/wgsl/vertex/BasicVertex.ts b/packages/webgpu/src/Shader/wgsl/vertex/BasicVertex.ts index e2dcdd91..7dce4ecc 100644 --- a/packages/webgpu/src/Shader/wgsl/vertex/BasicVertex.ts +++ b/packages/webgpu/src/Shader/wgsl/vertex/BasicVertex.ts @@ -1,3 +1,10 @@ +/** + * @description 基本頂点シェーダー(行列変換・プリマルチプライドカラー出力) + * Basic vertex shader with matrix transform and premultiplied color output + * + * @type {string} + * @constant + */ export const BasicVertex = /* wgsl */` override yFlipSign: f32 = 1.0; diff --git a/packages/webgpu/src/Shader/wgsl/vertex/BitmapVertex.ts b/packages/webgpu/src/Shader/wgsl/vertex/BitmapVertex.ts index e90d921e..94235007 100644 --- a/packages/webgpu/src/Shader/wgsl/vertex/BitmapVertex.ts +++ b/packages/webgpu/src/Shader/wgsl/vertex/BitmapVertex.ts @@ -1,3 +1,10 @@ +/** + * @description ビットマップ塗り用頂点シェーダー(ワールド座標出力付き) + * Bitmap fill vertex shader with world position output + * + * @type {string} + * @constant + */ export const BitmapFillVertex = /* wgsl */` override yFlipSign: f32 = 1.0; diff --git a/packages/webgpu/src/Shader/wgsl/vertex/FillVertex.ts b/packages/webgpu/src/Shader/wgsl/vertex/FillVertex.ts index a24f8660..90237ace 100644 --- a/packages/webgpu/src/Shader/wgsl/vertex/FillVertex.ts +++ b/packages/webgpu/src/Shader/wgsl/vertex/FillVertex.ts @@ -1,3 +1,10 @@ +/** + * @description 塗り用頂点シェーダー(ベジェ曲線パラメータ付き行列変換) + * Fill vertex shader with matrix transform and bezier parameters + * + * @type {string} + * @constant + */ export const FillVertex = /* wgsl */` override yFlipSign: f32 = 1.0; diff --git a/packages/webgpu/src/Shader/wgsl/vertex/FilterVertex.ts b/packages/webgpu/src/Shader/wgsl/vertex/FilterVertex.ts index 204d76ad..504e3f96 100644 --- a/packages/webgpu/src/Shader/wgsl/vertex/FilterVertex.ts +++ b/packages/webgpu/src/Shader/wgsl/vertex/FilterVertex.ts @@ -1,6 +1,13 @@ import { WgslFullscreenPositions, WgslUnitQuadVertices, WgslVertexOutput } from "../common/SharedWgsl"; -const createFullscreenQuadVertex = (yFlipTexCoord: boolean): string => /* wgsl */` +/** + * @description フルスクリーン四角形の頂点シェーダーを生成する + * Generate a full-screen quad vertex shader + * + * @param {boolean} y_flip_tex_coord - テクスチャY座標を反転するかどうか + * @return {string} + */ +const $createFullscreenQuadVertex = (y_flip_tex_coord: boolean): string => /* wgsl */` ${WgslVertexOutput} @vertex @@ -8,12 +15,12 @@ fn main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { var output: VertexOutput; ${WgslFullscreenPositions} var texCoords = array, 6>( - vec2(0.0, ${yFlipTexCoord ? "1.0" : "0.0"}), - vec2(1.0, ${yFlipTexCoord ? "1.0" : "0.0"}), - vec2(0.0, ${yFlipTexCoord ? "0.0" : "1.0"}), - vec2(0.0, ${yFlipTexCoord ? "0.0" : "1.0"}), - vec2(1.0, ${yFlipTexCoord ? "1.0" : "0.0"}), - vec2(1.0, ${yFlipTexCoord ? "0.0" : "1.0"}) + vec2(0.0, ${y_flip_tex_coord ? "1.0" : "0.0"}), + vec2(1.0, ${y_flip_tex_coord ? "1.0" : "0.0"}), + vec2(0.0, ${y_flip_tex_coord ? "0.0" : "1.0"}), + vec2(0.0, ${y_flip_tex_coord ? "0.0" : "1.0"}), + vec2(1.0, ${y_flip_tex_coord ? "1.0" : "0.0"}), + vec2(1.0, ${y_flip_tex_coord ? "0.0" : "1.0"}) ); output.position = vec4(positions[vertexIndex], 0.0, 1.0); output.texCoord = texCoords[vertexIndex]; @@ -21,10 +28,40 @@ ${WgslFullscreenPositions} } `; -export const BlurFilterVertex = createFullscreenQuadVertex(true); -export const ComplexBlendVertex = createFullscreenQuadVertex(false); -export const ComplexBlendCopyVertex = createFullscreenQuadVertex(false); +/** + * @description ブラーフィルター用フルスクリーン頂点シェーダー(Y軸テクスチャ反転) + * Full-screen vertex shader for blur filter with Y-axis texture flip + * + * @type {string} + * @constant + */ +export const BlurFilterVertex = $createFullscreenQuadVertex(true); +/** + * @description 複合ブレンド用フルスクリーン頂点シェーダー + * Full-screen vertex shader for complex blend + * + * @type {string} + * @constant + */ +export const ComplexBlendVertex = $createFullscreenQuadVertex(false); + +/** + * @description 複合ブレンドコピー用フルスクリーン頂点シェーダー + * Full-screen vertex shader for complex blend copy + * + * @type {string} + * @constant + */ +export const ComplexBlendCopyVertex = $createFullscreenQuadVertex(false); + +/** + * @description ノードクリア用頂点シェーダー(NDC変換のみ) + * Node clear vertex shader with NDC transform only + * + * @type {string} + * @constant + */ export const NodeClearVertex = /* wgsl */` struct VertexInput { @location(0) position: vec2, @@ -43,6 +80,13 @@ fn main(input: VertexInput) -> VertexOutput { } `; +/** + * @description 位置指定テクスチャ描画用頂点シェーダー(ビューポート変換付き) + * Positioned texture rendering vertex shader with viewport transform + * + * @type {string} + * @constant + */ export const PositionedTextureVertex = /* wgsl */` struct PositionUniforms { offset: vec2, @@ -72,6 +116,13 @@ ${WgslUnitQuadVertices} } `; +/** + * @description ビットマップ同期用頂点シェーダー(ノード矩形ベース配置) + * Bitmap sync vertex shader with node rectangle-based positioning + * + * @type {string} + * @constant + */ export const BitmapSyncVertex = /* wgsl */` struct BitmapSyncUniforms { nodeRect: vec4, @@ -102,27 +153,14 @@ ${WgslUnitQuadVertices} } `; -export const BlendModeVertex = /* wgsl */` -struct VertexInput { - @location(0) position: vec2, - @location(1) texCoord: vec2, -} - -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) texCoord: vec2, -} - -@vertex -fn main(input: VertexInput) -> VertexOutput { - var output: VertexOutput; - output.position = vec4(input.position, 0.0, 1.0); - output.texCoord = input.texCoord; - return output; -} -`; - -const ScaleUniformsAndStruct = ` +/** + * @description スケール変換用Uniform定義と頂点出力構造体のWGSLコード + * WGSL code for scale transform uniform definitions and vertex output struct + * + * @type {string} + * @constant + */ +const $ScaleUniformsAndStruct = ` struct ScaleUniforms { matrix: vec4, translate: vec2, @@ -139,7 +177,14 @@ struct VertexOutput { @group(0) @binding(0) var uniforms: ScaleUniforms; `; -const ScaleTransformBody = ` +/** + * @description スケール変換の頂点位置計算処理のWGSLコード + * WGSL code for scale transform vertex position calculation + * + * @type {string} + * @constant + */ +const $ScaleTransformBody = ` var pos = vertex * uniforms.srcSize; let a = uniforms.matrix.x; let b = uniforms.matrix.y; @@ -154,25 +199,62 @@ const ScaleTransformBody = ` output.position = vec4(position.x, -position.y, 0.0, 1.0); `; -const createScaleVertex = (yFlipTexCoord: boolean): string => /* wgsl */` -${ScaleUniformsAndStruct} +/** + * @description スケール変換用頂点シェーダーを生成する + * Generate a scale transform vertex shader + * + * @param {boolean} y_flip_tex_coord - テクスチャY座標を反転するかどうか + * @return {string} + */ +const $createScaleVertex = (y_flip_tex_coord: boolean): string => /* wgsl */` +${$ScaleUniformsAndStruct} @vertex fn main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { var output: VertexOutput; ${WgslUnitQuadVertices} let vertex = vertices[vertexIndex]; - output.texCoord = ${yFlipTexCoord ? "vec2(vertex.x, 1.0 - vertex.y)" : "vertex"}; -${ScaleTransformBody} + output.texCoord = ${y_flip_tex_coord ? "vec2(vertex.x, 1.0 - vertex.y)" : "vertex"}; +${$ScaleTransformBody} return output; } `; -export const TextureScaleVertex = createScaleVertex(false); -export const TextureScaleBlendVertex = createScaleVertex(true); -export const ComplexBlendScaleVertex = createScaleVertex(false); +/** + * @description テクスチャスケール用頂点シェーダー + * Texture scale vertex shader + * + * @type {string} + * @constant + */ +export const TextureScaleVertex = $createScaleVertex(false); + +/** + * @description ブレンド用テクスチャスケール頂点シェーダー(Y軸反転) + * Texture scale vertex shader for blend with Y-axis flip + * + * @type {string} + * @constant + */ +export const TextureScaleBlendVertex = $createScaleVertex(true); -const createOutputVertex = (yFlipTexCoord: boolean): string => /* wgsl */` +/** + * @description 複合ブレンドスケール用頂点シェーダー + * Complex blend scale vertex shader + * + * @type {string} + * @constant + */ +export const ComplexBlendScaleVertex = $createScaleVertex(false); + +/** + * @description 出力用頂点シェーダーを生成する(位置指定・ビューポート変換) + * Generate an output vertex shader with positioning and viewport transform + * + * @param {boolean} y_flip_tex_coord - テクスチャY座標を反転するかどうか + * @return {string} + */ +const $createOutputVertex = (y_flip_tex_coord: boolean): string => /* wgsl */` struct PositionUniforms { offset: vec2, size: vec2, @@ -192,7 +274,7 @@ fn main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput { var output: VertexOutput; ${WgslUnitQuadVertices} let vertex = vertices[vertexIndex]; - output.texCoord = ${yFlipTexCoord ? "vec2(vertex.x, 1.0 - vertex.y)" : "vertex"}; + output.texCoord = ${y_flip_tex_coord ? "vec2(vertex.x, 1.0 - vertex.y)" : "vertex"}; var position = vertex * uniforms.size + uniforms.offset; position = position / uniforms.viewport; position = position * 2.0 - 1.0; @@ -201,5 +283,20 @@ ${WgslUnitQuadVertices} } `; -export const ComplexBlendOutputVertex = createOutputVertex(false); -export const FilterComplexBlendOutputVertex = createOutputVertex(true); +/** + * @description 複合ブレンド出力用頂点シェーダー + * Complex blend output vertex shader + * + * @type {string} + * @constant + */ +export const ComplexBlendOutputVertex = $createOutputVertex(false); + +/** + * @description フィルター複合ブレンド出力用頂点シェーダー(Y軸反転) + * Filter complex blend output vertex shader with Y-axis flip + * + * @type {string} + * @constant + */ +export const FilterComplexBlendOutputVertex = $createOutputVertex(true); diff --git a/packages/webgpu/src/Shader/wgsl/vertex/GradientVertex.ts b/packages/webgpu/src/Shader/wgsl/vertex/GradientVertex.ts index 313654d8..86b30f79 100644 --- a/packages/webgpu/src/Shader/wgsl/vertex/GradientVertex.ts +++ b/packages/webgpu/src/Shader/wgsl/vertex/GradientVertex.ts @@ -1,3 +1,10 @@ +/** + * @description グラデーション塗り用頂点シェーダー(逆行列によるUV座標計算) + * Gradient fill vertex shader with inverse matrix UV coordinate calculation + * + * @type {string} + * @constant + */ export const GradientFillVertex = /* wgsl */` override yFlipSign: f32 = 1.0; diff --git a/packages/webgpu/src/Shader/wgsl/vertex/InstancedVertex.ts b/packages/webgpu/src/Shader/wgsl/vertex/InstancedVertex.ts index 86cf39fb..c0a43452 100644 --- a/packages/webgpu/src/Shader/wgsl/vertex/InstancedVertex.ts +++ b/packages/webgpu/src/Shader/wgsl/vertex/InstancedVertex.ts @@ -1,3 +1,10 @@ +/** + * @description インスタンス描画用頂点シェーダー(インスタンスごとの変換・カラー) + * Instanced rendering vertex shader with per-instance transform and color + * + * @type {string} + * @constant + */ export const InstancedVertex = /* wgsl */` struct VertexInput { @location(0) position: vec2, diff --git a/packages/webgpu/src/Shader/wgsl/vertex/MaskVertex.ts b/packages/webgpu/src/Shader/wgsl/vertex/MaskVertex.ts index 21328b8c..a710846a 100644 --- a/packages/webgpu/src/Shader/wgsl/vertex/MaskVertex.ts +++ b/packages/webgpu/src/Shader/wgsl/vertex/MaskVertex.ts @@ -1,3 +1,10 @@ +/** + * @description マスク用頂点シェーダー(ベジェ曲線パラメータ付き行列変換) + * Mask vertex shader with matrix transform and bezier parameters + * + * @type {string} + * @constant + */ export const MaskVertex = /* wgsl */` struct VertexInput { @location(0) position: vec2, diff --git a/packages/webgpu/src/Shader/wgsl/vertex/StencilVertex.ts b/packages/webgpu/src/Shader/wgsl/vertex/StencilVertex.ts index 729e8055..b1e2aee9 100644 --- a/packages/webgpu/src/Shader/wgsl/vertex/StencilVertex.ts +++ b/packages/webgpu/src/Shader/wgsl/vertex/StencilVertex.ts @@ -1,3 +1,10 @@ +/** + * @description ステンシル書き込み用頂点シェーダー(ベジェ曲線パラメータ付き) + * Stencil write vertex shader with bezier parameters + * + * @type {string} + * @constant + */ export const StencilWriteVertex = /* wgsl */` override yFlipSign: f32 = 1.0; @@ -32,6 +39,13 @@ fn main(input: VertexInput) -> VertexOutput { } `; +/** + * @description ステンシル塗り用頂点シェーダー(カラー出力付き) + * Stencil fill vertex shader with color output + * + * @type {string} + * @constant + */ export const StencilFillVertex = /* wgsl */` override yFlipSign: f32 = 1.0; diff --git a/packages/webgpu/src/TextureManager.test.ts b/packages/webgpu/src/TextureManager.test.ts index 3bdc1914..3b4ccbc2 100644 --- a/packages/webgpu/src/TextureManager.test.ts +++ b/packages/webgpu/src/TextureManager.test.ts @@ -9,7 +9,7 @@ const GPUTextureUsage = { }; (globalThis as any).GPUTextureUsage = GPUTextureUsage; -// Mock service and usecase modules +// Mock service module vi.mock("./TextureManager/service/TextureManagerInitializeSamplersService", () => ({ "execute": vi.fn((device, samplers) => { samplers.set("default", { "label": "defaultSampler" }); @@ -18,32 +18,6 @@ vi.mock("./TextureManager/service/TextureManagerInitializeSamplersService", () = }) })); -vi.mock("./TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase", () => ({ - "execute": vi.fn((device, textures, name, pixels, width, height) => { - const texture = { - "width": width, - "height": height, - "destroy": vi.fn(), - "createView": vi.fn() - }; - textures.set(name, texture); - return texture; - }) -})); - -vi.mock("./TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase", () => ({ - "execute": vi.fn((device, textures, name, imageBitmap) => { - const texture = { - "width": imageBitmap.width, - "height": imageBitmap.height, - "destroy": vi.fn(), - "createView": vi.fn() - }; - textures.set(name, texture); - return texture; - }) -})); - describe("TextureManager", () => { const createMockDevice = (): GPUDevice => @@ -57,10 +31,7 @@ describe("TextureManager", () => })), "createSampler": vi.fn((descriptor) => ({ "label": `sampler-${descriptor.magFilter}` - })), - "queue": { - "writeTexture": vi.fn() - } + })) } as unknown as GPUDevice; }; @@ -84,8 +55,9 @@ describe("TextureManager", () => const device = createMockDevice(); const manager = new TextureManager(device); - // Default sampler should be initialized - expect(manager.getSampler("default")).toBeDefined(); + // Verify pre-initialized sampler is returned by createSampler + const sampler = manager.createSampler("default"); + expect(sampler).toEqual({ "label": "defaultSampler" }); }); }); @@ -142,80 +114,6 @@ describe("TextureManager", () => }); }); - describe("createTextureFromPixels", () => - { - it("should create texture from pixel data", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - const pixels = new Uint8Array(64 * 64 * 4); - - const texture = manager.createTextureFromPixels("pixelTex", pixels, 64, 64); - - expect(texture).toBeDefined(); - }); - - it("should store texture by name", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - const pixels = new Uint8Array(32 * 32 * 4); - - manager.createTextureFromPixels("pixels", pixels, 32, 32); - - expect(manager.getTexture("pixels")).toBeDefined(); - }); - }); - - describe("createTextureFromImageBitmap", () => - { - it("should create texture from ImageBitmap", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - const imageBitmap = { "width": 128, "height": 128 } as ImageBitmap; - - const texture = manager.createTextureFromImageBitmap("bitmapTex", imageBitmap); - - expect(texture).toBeDefined(); - }); - - it("should store texture by name", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - const imageBitmap = { "width": 256, "height": 256 } as ImageBitmap; - - manager.createTextureFromImageBitmap("bitmap", imageBitmap); - - expect(manager.getTexture("bitmap")).toBeDefined(); - }); - }); - - describe("updateTexture", () => - { - it("should update existing texture with pixel data", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - const pixels = new Uint8Array(64 * 64 * 4); - - manager.createTexture("test", 64, 64); - manager.updateTexture("test", pixels, 64, 64); - - expect(device.queue.writeTexture).toHaveBeenCalled(); - }); - - it("should not throw when texture does not exist", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - const pixels = new Uint8Array(64 * 64 * 4); - - expect(() => manager.updateTexture("nonexistent", pixels, 64, 64)).not.toThrow(); - }); - }); - describe("getTexture", () => { it("should return undefined for non-existent texture", () => @@ -227,25 +125,6 @@ describe("TextureManager", () => }); }); - describe("getSampler", () => - { - it("should return initialized sampler", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - - expect(manager.getSampler("default")).toBeDefined(); - }); - - it("should return undefined for non-existent sampler", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - - expect(manager.getSampler("nonexistent")).toBeUndefined(); - }); - }); - describe("createSampler", () => { it("should create new sampler", () => @@ -305,32 +184,10 @@ describe("TextureManager", () => const device = createMockDevice(); const manager = new TextureManager(device); - manager.createSampler("newSampler", true); - - expect(manager.getSampler("newSampler")).toBeDefined(); - }); - }); - - describe("destroyTexture", () => - { - it("should destroy texture by name", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - - const texture = manager.createTexture("test", 128, 128); - manager.destroyTexture("test"); + const created = manager.createSampler("newSampler", true); + const retrieved = manager.createSampler("newSampler"); - expect(texture.destroy).toHaveBeenCalled(); - expect(manager.getTexture("test")).toBeUndefined(); - }); - - it("should not throw when texture does not exist", () => - { - const device = createMockDevice(); - const manager = new TextureManager(device); - - expect(() => manager.destroyTexture("nonexistent")).not.toThrow(); + expect(retrieved).toBe(created); }); }); @@ -369,7 +226,10 @@ describe("TextureManager", () => manager.createSampler("custom", true); manager.dispose(); - expect(manager.getSampler("custom")).toBeUndefined(); + // After dispose, createSampler should create a new sampler instead of returning the old one + const sampler = manager.createSampler("custom", true); + expect(device.createSampler).toHaveBeenCalled(); + expect(sampler).toBeDefined(); }); }); }); diff --git a/packages/webgpu/src/TextureManager.ts b/packages/webgpu/src/TextureManager.ts index ac588f53..ae6aadc8 100644 --- a/packages/webgpu/src/TextureManager.ts +++ b/packages/webgpu/src/TextureManager.ts @@ -1,13 +1,20 @@ import { execute as textureManagerInitializeSamplersService } from "./TextureManager/service/TextureManagerInitializeSamplersService"; -import { execute as textureManagerCreateTextureFromPixelsUseCase } from "./TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase"; -import { execute as textureManagerCreateTextureFromImageBitmapUseCase } from "./TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase"; +/** + * @description テクスチャとサンプラーの管理クラス + * Manager class for textures and samplers + */ export class TextureManager { private device: GPUDevice; private textures: Map; private samplers: Map; + /** + * @description TextureManagerのコンストラクタ + * Constructor for TextureManager + * @param {GPUDevice} device - WebGPUデバイス / WebGPU device + */ constructor (device: GPUDevice) { this.device = device; @@ -17,6 +24,15 @@ export class TextureManager textureManagerInitializeSamplersService(device, this.samplers); } + /** + * @description 新しいGPUテクスチャを作成する + * Create a new GPU texture + * @param {string} name - テクスチャ名 / Texture name + * @param {number} width - テクスチャの幅 / Texture width + * @param {number} height - テクスチャの高さ / Texture height + * @param {GPUTextureFormat} [format="rgba8unorm"] - テクスチャフォーマット / Texture format + * @return {GPUTexture} + */ createTexture ( name: string, width: number, @@ -35,59 +51,24 @@ export class TextureManager return texture; } - createTextureFromPixels ( - name: string, - pixels: Uint8Array, - width: number, - height: number - ): GPUTexture { - return textureManagerCreateTextureFromPixelsUseCase( - this.device, - this.textures, - name, - pixels, - width, - height - ); - } - - createTextureFromImageBitmap (name: string, imageBitmap: ImageBitmap): GPUTexture - { - return textureManagerCreateTextureFromImageBitmapUseCase( - this.device, - this.textures, - name, - imageBitmap - ); - } - - updateTexture ( - name: string, - pixels: Uint8Array, - width: number, - height: number - ): void { - const texture = this.textures.get(name); - if (texture) { - this.device.queue.writeTexture( - { texture }, - pixels.buffer, - { "bytesPerRow": width * 4, "offset": pixels.byteOffset }, - { width, height } - ); - } - } - + /** + * @description 名前でテクスチャを取得する + * Get a texture by name + * @param {string} name - テクスチャ名 / Texture name + * @return {GPUTexture | undefined} + */ getTexture (name: string): GPUTexture | undefined { return this.textures.get(name); } - getSampler (name: string): GPUSampler | undefined - { - return this.samplers.get(name); - } - + /** + * @description サンプラーを作成する(既存の場合は返却) + * Create a sampler (returns existing if found) + * @param {string} name - サンプラー名 / Sampler name + * @param {boolean} [smooth=true] - スムージングを有効にするか / Whether to enable smoothing + * @return {GPUSampler} + */ createSampler (name: string, smooth: boolean = true): GPUSampler { const existing = this.samplers.get(name); @@ -107,15 +88,11 @@ export class TextureManager return sampler; } - destroyTexture (name: string): void - { - const texture = this.textures.get(name); - if (texture) { - texture.destroy(); - this.textures.delete(name); - } - } - + /** + * @description 全テクスチャとサンプラーを破棄する + * Dispose all textures and samplers + * @return {void} + */ dispose (): void { for (const texture of this.textures.values()) { diff --git a/packages/webgpu/src/TextureManager/service/TextureManagerInitializeSamplersService.ts b/packages/webgpu/src/TextureManager/service/TextureManagerInitializeSamplersService.ts index 2f62e0ac..3510bfa3 100644 --- a/packages/webgpu/src/TextureManager/service/TextureManagerInitializeSamplersService.ts +++ b/packages/webgpu/src/TextureManager/service/TextureManagerInitializeSamplersService.ts @@ -2,8 +2,8 @@ * @description サンプラーを初期化 * Initialize samplers * - * @param {GPUDevice} device - * @param {Map} samplers + * @param {GPUDevice} device - GPUデバイス + * @param {Map} samplers - サンプラー管理マップ * @return {void} * @method * @protected diff --git a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.test.ts b/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.test.ts deleted file mode 100644 index d3f147a3..00000000 --- a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { execute } from "./TextureManagerCreateTextureFromImageBitmapUseCase"; - -// Mock GPUTextureUsage -const GPUTextureUsage = { - TEXTURE_BINDING: 0x04, - COPY_DST: 0x02, - RENDER_ATTACHMENT: 0x10 -}; -(globalThis as any).GPUTextureUsage = GPUTextureUsage; - -describe("TextureManagerCreateTextureFromImageBitmapUseCase", () => -{ - const createMockDevice = () => - { - const mockTexture = { "label": "mockTexture" }; - return { - "createTexture": vi.fn(() => mockTexture), - "queue": { - "copyExternalImageToTexture": vi.fn() - }, - "_mockTexture": mockTexture - } as unknown as GPUDevice & { _mockTexture: any }; - }; - - const createMockImageBitmap = (width: number = 100, height: number = 100): ImageBitmap => - { - return { width, height } as unknown as ImageBitmap; - }; - - describe("texture creation", () => - { - it("should create texture with ImageBitmap dimensions", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(512, 256); - - execute(device, textures, "test", imageBitmap); - - expect(device.createTexture).toHaveBeenCalledWith( - expect.objectContaining({ - "size": { "width": 512, "height": 256 } - }) - ); - }); - - it("should create texture with rgba8unorm format", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(); - - execute(device, textures, "test", imageBitmap); - - expect(device.createTexture).toHaveBeenCalledWith( - expect.objectContaining({ - "format": "rgba8unorm" - }) - ); - }); - - it("should create texture with correct usage flags", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(); - - execute(device, textures, "test", imageBitmap); - - const expectedUsage = - GPUTextureUsage.TEXTURE_BINDING | - GPUTextureUsage.COPY_DST | - GPUTextureUsage.RENDER_ATTACHMENT; - - expect(device.createTexture).toHaveBeenCalledWith( - expect.objectContaining({ - "usage": expectedUsage - }) - ); - }); - }); - - describe("image copy", () => - { - it("should copy ImageBitmap to texture", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(); - - execute(device, textures, "test", imageBitmap); - - expect(device.queue.copyExternalImageToTexture).toHaveBeenCalled(); - }); - - it("should use ImageBitmap as source with flipY", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(); - - execute(device, textures, "test", imageBitmap); - - expect(device.queue.copyExternalImageToTexture).toHaveBeenCalledWith( - { "source": imageBitmap, "flipY": true }, - expect.anything(), - expect.anything() - ); - }); - - it("should copy to created texture with premultipliedAlpha", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(); - - execute(device, textures, "test", imageBitmap); - - expect(device.queue.copyExternalImageToTexture).toHaveBeenCalledWith( - expect.anything(), - expect.objectContaining({ - "texture": (device as any)._mockTexture, - "premultipliedAlpha": true - }), - expect.anything() - ); - }); - - it("should use ImageBitmap dimensions for copy size", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(320, 240); - - execute(device, textures, "test", imageBitmap); - - expect(device.queue.copyExternalImageToTexture).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - { "width": 320, "height": 240 } - ); - }); - }); - - describe("texture storage", () => - { - it("should add texture to map with name", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(); - - execute(device, textures, "myBitmapTexture", imageBitmap); - - expect(textures.has("myBitmapTexture")).toBe(true); - expect(textures.get("myBitmapTexture")).toBe((device as any)._mockTexture); - }); - - it("should overwrite existing texture with same name", () => - { - const device = createMockDevice(); - const textures = new Map(); - const existingTexture = { "label": "existing" } as unknown as GPUTexture; - textures.set("test", existingTexture); - const imageBitmap = createMockImageBitmap(); - - execute(device, textures, "test", imageBitmap); - - expect(textures.get("test")).toBe((device as any)._mockTexture); - expect(textures.get("test")).not.toBe(existingTexture); - }); - }); - - describe("return value", () => - { - it("should return created texture", () => - { - const device = createMockDevice(); - const textures = new Map(); - const imageBitmap = createMockImageBitmap(); - - const result = execute(device, textures, "test", imageBitmap); - - expect(result).toBe((device as any)._mockTexture); - }); - }); -}); diff --git a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.ts b/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.ts deleted file mode 100644 index 9fe1f821..00000000 --- a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromImageBitmapUseCase.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * @description ImageBitmapからテクスチャを作成 - * Create texture from ImageBitmap - * - * @param {GPUDevice} device - * @param {Map} textures - * @param {string} name - * @param {ImageBitmap} image_bitmap - * @return {GPUTexture} - * @method - * @protected - */ -export const execute = ( - device: GPUDevice, - textures: Map, - name: string, - image_bitmap: ImageBitmap -): GPUTexture => { - const texture = device.createTexture({ - "size": { "width": image_bitmap.width, "height": image_bitmap.height }, - "format": "rgba8unorm", - "usage": GPUTextureUsage.TEXTURE_BINDING | - GPUTextureUsage.COPY_DST | - GPUTextureUsage.RENDER_ATTACHMENT - }); - - device.queue.copyExternalImageToTexture( - { - "source": image_bitmap, - "flipY": true - }, - { - texture, - "premultipliedAlpha": true - }, - { "width": image_bitmap.width, "height": image_bitmap.height } - ); - - textures.set(name, texture); - return texture; -}; diff --git a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.test.ts b/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.test.ts deleted file mode 100644 index 66e4cb5f..00000000 --- a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.test.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { describe, it, expect, vi, beforeEach } from "vitest"; -import { execute } from "./TextureManagerCreateTextureFromPixelsUseCase"; - -// Mock GPUTextureUsage -const GPUTextureUsage = { - TEXTURE_BINDING: 0x04, - COPY_DST: 0x02, - RENDER_ATTACHMENT: 0x10 -}; -(globalThis as any).GPUTextureUsage = GPUTextureUsage; - -describe("TextureManagerCreateTextureFromPixelsUseCase", () => -{ - const createMockDevice = () => - { - const mockTexture = { "label": "mockTexture" }; - return { - "createTexture": vi.fn(() => mockTexture), - "queue": { - "writeTexture": vi.fn() - }, - "_mockTexture": mockTexture - } as unknown as GPUDevice & { _mockTexture: any }; - }; - - describe("texture creation", () => - { - it("should create texture with correct size", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(256 * 128 * 4); - - execute(device, textures, "test", pixels, 256, 128); - - expect(device.createTexture).toHaveBeenCalledWith( - expect.objectContaining({ - "size": { "width": 256, "height": 128 } - }) - ); - }); - - it("should create texture with rgba8unorm format", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(100 * 100 * 4); - - execute(device, textures, "test", pixels, 100, 100); - - expect(device.createTexture).toHaveBeenCalledWith( - expect.objectContaining({ - "format": "rgba8unorm" - }) - ); - }); - - it("should create texture with correct usage flags", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(100 * 100 * 4); - - execute(device, textures, "test", pixels, 100, 100); - - const expectedUsage = - GPUTextureUsage.TEXTURE_BINDING | - GPUTextureUsage.COPY_DST | - GPUTextureUsage.RENDER_ATTACHMENT; - - expect(device.createTexture).toHaveBeenCalledWith( - expect.objectContaining({ - "usage": expectedUsage - }) - ); - }); - }); - - describe("pixel data writing", () => - { - it("should write pixel data to texture", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(100 * 100 * 4); - - execute(device, textures, "test", pixels, 100, 100); - - expect(device.queue.writeTexture).toHaveBeenCalled(); - }); - - it("should write to texture with correct target", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(100 * 100 * 4); - - execute(device, textures, "test", pixels, 100, 100); - - expect(device.queue.writeTexture).toHaveBeenCalledWith( - { "texture": (device as any)._mockTexture }, - expect.anything(), - expect.anything(), - expect.anything() - ); - }); - - it("should write with correct bytesPerRow", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(256 * 128 * 4); - - execute(device, textures, "test", pixels, 256, 128); - - expect(device.queue.writeTexture).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.objectContaining({ - "bytesPerRow": 256 * 4 // width * 4 bytes per pixel - }), - expect.anything() - ); - }); - - it("should write with correct extent", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(320 * 240 * 4); - - execute(device, textures, "test", pixels, 320, 240); - - expect(device.queue.writeTexture).toHaveBeenCalledWith( - expect.anything(), - expect.anything(), - expect.anything(), - { "width": 320, "height": 240 } - ); - }); - }); - - describe("texture storage", () => - { - it("should add texture to map with name", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(100 * 100 * 4); - - execute(device, textures, "myTexture", pixels, 100, 100); - - expect(textures.has("myTexture")).toBe(true); - expect(textures.get("myTexture")).toBe((device as any)._mockTexture); - }); - - it("should overwrite existing texture with same name", () => - { - const device = createMockDevice(); - const textures = new Map(); - const existingTexture = { "label": "existing" } as unknown as GPUTexture; - textures.set("test", existingTexture); - const pixels = new Uint8Array(100 * 100 * 4); - - execute(device, textures, "test", pixels, 100, 100); - - expect(textures.get("test")).toBe((device as any)._mockTexture); - expect(textures.get("test")).not.toBe(existingTexture); - }); - }); - - describe("return value", () => - { - it("should return created texture", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(100 * 100 * 4); - - const result = execute(device, textures, "test", pixels, 100, 100); - - expect(result).toBe((device as any)._mockTexture); - }); - }); - - describe("byte offset handling", () => - { - it("should pass pixel buffer to writeTexture", () => - { - const device = createMockDevice(); - const textures = new Map(); - const pixels = new Uint8Array(100 * 100 * 4); - - execute(device, textures, "test", pixels, 100, 100); - - expect(device.queue.writeTexture).toHaveBeenCalledWith( - expect.anything(), - pixels.buffer, - expect.objectContaining({ - "offset": 0 - }), - expect.anything() - ); - }); - }); -}); diff --git a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.ts b/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.ts deleted file mode 100644 index 9cf07105..00000000 --- a/packages/webgpu/src/TextureManager/usecase/TextureManagerCreateTextureFromPixelsUseCase.ts +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @description ピクセルデータからテクスチャを作成 - * Create texture from pixel data - * - * @param {GPUDevice} device - * @param {Map} textures - * @param {string} name - * @param {Uint8Array} pixels - * @param {number} width - * @param {number} height - * @return {GPUTexture} - * @method - * @protected - */ -export const execute = ( - device: GPUDevice, - textures: Map, - name: string, - pixels: Uint8Array, - width: number, - height: number -): GPUTexture => { - const texture = device.createTexture({ - "size": { width, height }, - "format": "rgba8unorm", - "usage": GPUTextureUsage.TEXTURE_BINDING | - GPUTextureUsage.COPY_DST | - GPUTextureUsage.RENDER_ATTACHMENT - }); - - device.queue.writeTexture( - { texture }, - pixels.buffer, - { "bytesPerRow": width * 4, "offset": pixels.byteOffset }, - { width, height } - ); - - textures.set(name, texture); - return texture; -}; diff --git a/packages/webgpu/src/TexturePool.test.ts b/packages/webgpu/src/TexturePool.test.ts index e3dfc9e7..65c2cbf1 100644 --- a/packages/webgpu/src/TexturePool.test.ts +++ b/packages/webgpu/src/TexturePool.test.ts @@ -1,9 +1,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import { - TexturePool, - initTexturePool, - getTexturePool, - clearTexturePool + TexturePool } from "./TexturePool"; // Mock GPUTextureUsage @@ -105,7 +102,6 @@ describe("TexturePool", () => beforeEach(() => { vi.clearAllMocks(); - clearTexturePool(); }); describe("TexturePool class", () => @@ -118,17 +114,6 @@ describe("TexturePool", () => expect(pool).toBeDefined(); }); - it("should initialize with empty stats", () => - { - const device = createMockDevice(); - const pool = new TexturePool(device); - - const stats = pool.getStats(); - expect(stats.total).toBe(0); - expect(stats.inUse).toBe(0); - expect(stats.available).toBe(0); - }); - describe("beginFrame", () => { it("should increment frame counter", () => @@ -172,18 +157,6 @@ describe("TexturePool", () => expect(texture.height).toBe(256); }); - it("should update stats when acquiring", () => - { - const device = createMockDevice(); - const pool = new TexturePool(device); - - pool.acquire(128, 128); - - const stats = pool.getStats(); - expect(stats.total).toBe(1); - expect(stats.inUse).toBe(1); - }); - it("should reuse released texture with same dimensions", () => { const device = createMockDevice(); @@ -230,17 +203,14 @@ describe("TexturePool", () => describe("release", () => { - it("should mark texture as available", () => + it("should not throw when releasing", () => { const device = createMockDevice(); const pool = new TexturePool(device); const texture = pool.acquire(256, 256); - pool.release(texture); - const stats = pool.getStats(); - expect(stats.inUse).toBe(0); - expect(stats.available).toBe(1); + expect(() => pool.release(texture)).not.toThrow(); }); it("should allow reuse after release", () => @@ -250,28 +220,9 @@ describe("TexturePool", () => const texture1 = pool.acquire(256, 256); pool.release(texture1); + const texture2 = pool.acquire(256, 256); - const stats = pool.getStats(); - expect(stats.available).toBe(1); - }); - }); - - describe("getStats", () => - { - it("should return accurate pool statistics", () => - { - const device = createMockDevice(); - const pool = new TexturePool(device); - - pool.acquire(128, 128); - pool.acquire(256, 256); - const tex3 = pool.acquire(512, 512); - pool.release(tex3); - - const stats = pool.getStats(); - expect(stats.total).toBe(3); - expect(stats.inUse).toBe(2); - expect(stats.available).toBe(1); + expect(texture1).toBe(texture2); }); }); @@ -291,7 +242,7 @@ describe("TexturePool", () => expect(tex2.destroy).toHaveBeenCalled(); }); - it("should reset pool to empty", () => + it("should not throw after dispose", () => { const device = createMockDevice(); const pool = new TexturePool(device); @@ -301,54 +252,7 @@ describe("TexturePool", () => pool.dispose(); - const stats = pool.getStats(); - expect(stats.total).toBe(0); - }); - }); - }); - - describe("global functions", () => - { - describe("initTexturePool", () => - { - it("should initialize global pool", () => - { - const device = createMockDevice(); - - initTexturePool(device); - - expect(getTexturePool()).not.toBeNull(); - }); - }); - - describe("getTexturePool", () => - { - it("should return pool after initialization", () => - { - const device = createMockDevice(); - initTexturePool(device); - - expect(getTexturePool()).toBeInstanceOf(TexturePool); - }); - }); - - describe("clearTexturePool", () => - { - it("should dispose pool", () => - { - const device = createMockDevice(); - initTexturePool(device); - const pool = getTexturePool(); - const tex = pool!.acquire(128, 128); - - clearTexturePool(); - - expect(tex.destroy).toHaveBeenCalled(); - }); - - it("should not throw when pool is null", () => - { - expect(() => clearTexturePool()).not.toThrow(); + expect(() => pool.acquire(64, 64)).not.toThrow(); }); }); }); diff --git a/packages/webgpu/src/TexturePool.ts b/packages/webgpu/src/TexturePool.ts index 65891cbf..5e3e2c2a 100644 --- a/packages/webgpu/src/TexturePool.ts +++ b/packages/webgpu/src/TexturePool.ts @@ -5,13 +5,17 @@ import { execute as texturePoolCleanupService } from "./TexturePool/service/Text /** * @description プールの最大サイズ + * Maximum pool size for texture reuse + * @type {number} */ -const MAX_POOL_SIZE = 32; +const $MAX_POOL_SIZE = 32; /** * @description キャッシュのクリーンアップ閾値(フレーム数) + * Cache cleanup threshold in frames (3 seconds at 60FPS) + * @type {number} */ -const CACHE_CLEANUP_THRESHOLD = 180; // 3秒(60FPS想定) +const $CACHE_CLEANUP_THRESHOLD = 180; /** * @description テクスチャプールマネージャー(Power-of-2バケット版) @@ -23,13 +27,38 @@ const CACHE_CLEANUP_THRESHOLD = 180; // 3秒(60FPS想定) */ export class TexturePool { + /** + * @description WebGPUデバイスの参照 + * Reference to the WebGPU device + * @type {GPUDevice} + */ private device: GPUDevice; + + /** + * @description Power-of-2バケットによるテクスチャプール + * Texture pool organized by power-of-2 buckets + * @type {ITexturePoolBuckets} + */ private buckets: ITexturePoolBuckets; + + /** + * @description 現在のフレーム番号 + * Current frame number for LRU tracking + * @type {number} + */ private currentFrame: number; + + /** + * @description プール内のテクスチャ総数 + * Total count of textures in the pool + * @type {number[]} + */ private totalCount: number[]; /** - * @param {GPUDevice} device + * @description テクスチャプールを生成する + * Create a new texture pool instance + * @param {GPUDevice} device - WebGPUデバイス / WebGPU device * @constructor */ constructor(device: GPUDevice) @@ -41,7 +70,8 @@ export class TexturePool } /** - * @description フレーム開始時に呼び出し + * @description フレーム開始時に呼び出し、定期的にプールをクリーンアップする + * Called at the beginning of each frame; periodically cleans up the pool * @return {void} */ beginFrame(): void @@ -50,16 +80,17 @@ export class TexturePool // 定期的にプールをクリーンアップ(LRU回収) if (this.currentFrame % 60 === 0) { - texturePoolCleanupService(this.buckets, this.currentFrame, CACHE_CLEANUP_THRESHOLD, this.totalCount); + texturePoolCleanupService(this.buckets, this.currentFrame, $CACHE_CLEANUP_THRESHOLD, this.totalCount); } } /** - * @description テクスチャを取得または作成 - * @param {number} width - テクスチャの幅 - * @param {number} height - テクスチャの高さ - * @param {GPUTextureFormat} format - テクスチャフォーマット - * @param {GPUTextureUsageFlags} usage - テクスチャ使用フラグ + * @description テクスチャを取得または作成する + * Acquire a texture from the pool or create a new one + * @param {number} width - テクスチャの幅 / texture width + * @param {number} height - テクスチャの高さ / texture height + * @param {GPUTextureFormat} format - テクスチャフォーマット / texture format + * @param {GPUTextureUsageFlags} usage - テクスチャ使用フラグ / texture usage flags * @return {GPUTexture} */ acquire( @@ -78,14 +109,15 @@ export class TexturePool format, usage, this.currentFrame, - MAX_POOL_SIZE, + $MAX_POOL_SIZE, this.totalCount ); } /** - * @description テクスチャをプールに返却 - * @param {GPUTexture} texture - 返却するテクスチャ + * @description テクスチャをプールに返却する + * Release a texture back to the pool for reuse + * @param {GPUTexture} texture - 返却するテクスチャ / texture to release * @return {void} */ release(texture: GPUTexture): void @@ -94,33 +126,8 @@ export class TexturePool } /** - * @description プール統計を取得 - * @return {{ total: number, inUse: number, available: number }} - */ - getStats(): { total: number; inUse: number; available: number } - { - let inUse = 0; - let available = 0; - - for (const bucket of this.buckets.values()) { - for (const entry of bucket) { - if (entry.inUse) { - inUse++; - } else { - available++; - } - } - } - - return { - "total": this.totalCount[0], - inUse, - available - }; - } - - /** - * @description 解放 + * @description 全テクスチャを破棄しプールを解放する + * Destroy all textures and dispose of the pool * @return {void} */ dispose(): void @@ -134,38 +141,3 @@ export class TexturePool this.totalCount[0] = 0; } } - -/** - * @description グローバルテクスチャプールインスタンス - */ -let $texturePool: TexturePool | null = null; - -/** - * @description テクスチャプールを初期化 - * @param {GPUDevice} device - * @return {void} - */ -export const initTexturePool = (device: GPUDevice): void => -{ - $texturePool = new TexturePool(device); -}; - -/** - * @description テクスチャプールを取得 - * @return {TexturePool | null} - */ -export const getTexturePool = (): TexturePool | null => -{ - return $texturePool; -}; - -/** - * @description テクスチャプールをクリア - * @return {void} - */ -export const clearTexturePool = (): void => -{ - if ($texturePool) { - $texturePool.dispose(); - } -}; diff --git a/packages/webgpu/src/TexturePool/service/TexturePoolCleanupService.ts b/packages/webgpu/src/TexturePool/service/TexturePoolCleanupService.ts index cb2ede79..5dd180a8 100644 --- a/packages/webgpu/src/TexturePool/service/TexturePoolCleanupService.ts +++ b/packages/webgpu/src/TexturePool/service/TexturePoolCleanupService.ts @@ -4,21 +4,21 @@ import type { ITexturePoolBuckets } from "../../interface/IPooledTexture"; * @description 古いプールエントリをクリーンアップ(バケットMap版 LRU回収) * Cleanup old pool entries (bucket Map version, LRU eviction) * - * @param {ITexturePoolBuckets} buckets - * @param {number} currentFrame + * @param {ITexturePoolBuckets} buckets - テクスチャプールバケット + * @param {number} current_frame - 現在のフレーム番号 * @param {number} threshold - フレーム数閾値 - * @param {number[]} totalCount - [0]に現在の合計数を格納 + * @param {number[]} total_count - [0]に現在の合計数を格納 * @return {void} * @method * @protected */ export const execute = ( buckets: ITexturePoolBuckets, - currentFrame: number, + current_frame: number, threshold: number, - totalCount: number[] + total_count: number[] ): void => { - const frameThreshold = currentFrame - threshold; + const frameThreshold = current_frame - threshold; for (const [key, bucket] of buckets) { for (let i = bucket.length - 1; i >= 0; i--) { @@ -26,7 +26,7 @@ export const execute = ( if (!entry.inUse && entry.lastUsedFrame < frameThreshold) { entry.texture.destroy(); bucket.splice(i, 1); - totalCount[0]--; + total_count[0]--; } } if (bucket.length === 0) { diff --git a/packages/webgpu/src/TexturePool/service/TexturePoolEvictOldestService.test.ts b/packages/webgpu/src/TexturePool/service/TexturePoolEvictOldestService.test.ts deleted file mode 100644 index 25cc4071..00000000 --- a/packages/webgpu/src/TexturePool/service/TexturePoolEvictOldestService.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from "vitest"; -import type { IPooledTexture } from "../../interface/IPooledTexture"; -import { execute } from "./TexturePoolEvictOldestService"; - -describe("TexturePoolEvictOldestService", () => -{ - let pool: IPooledTexture[]; - - const createMockEntry = ( - lastUsedFrame: number, - inUse: boolean = false - ): IPooledTexture => ({ - "texture": { - "destroy": vi.fn() - } as unknown as GPUTexture, - "width": 256, - "height": 256, - "format": "rgba8unorm" as GPUTextureFormat, - inUse, - lastUsedFrame - }); - - beforeEach(() => - { - pool = []; - }); - - it("should evict the oldest unused entry", () => - { - const oldest = createMockEntry(10, false); - const middle = createMockEntry(50, false); - const recent = createMockEntry(100, false); - pool.push(oldest, middle, recent); - - execute(pool); - - expect(pool.length).toBe(2); - expect(pool).not.toContain(oldest); - expect(oldest.texture.destroy).toHaveBeenCalled(); - }); - - it("should skip entries that are in use", () => - { - const oldestInUse = createMockEntry(10, true); - const oldestNotInUse = createMockEntry(50, false); - pool.push(oldestInUse, oldestNotInUse); - - execute(pool); - - expect(pool.length).toBe(1); - expect(pool[0]).toBe(oldestInUse); - expect(oldestInUse.texture.destroy).not.toHaveBeenCalled(); - expect(oldestNotInUse.texture.destroy).toHaveBeenCalled(); - }); - - it("should handle empty pool", () => - { - expect(() => execute(pool)).not.toThrow(); - expect(pool.length).toBe(0); - }); - - it("should not evict anything if all entries are in use", () => - { - pool.push( - createMockEntry(10, true), - createMockEntry(50, true), - createMockEntry(100, true) - ); - - execute(pool); - - expect(pool.length).toBe(3); - }); - - it("should only evict one entry per call", () => - { - pool.push( - createMockEntry(10, false), - createMockEntry(20, false), - createMockEntry(30, false) - ); - - execute(pool); - expect(pool.length).toBe(2); - - execute(pool); - expect(pool.length).toBe(1); - }); - - it("should call destroy on evicted texture", () => - { - const entry = createMockEntry(10, false); - pool.push(entry); - - execute(pool); - - expect(entry.texture.destroy).toHaveBeenCalledTimes(1); - }); - - it("should correctly identify oldest among multiple unused", () => - { - const recent = createMockEntry(100, false); - const oldest = createMockEntry(5, false); - const middle = createMockEntry(50, false); - pool.push(recent, oldest, middle); - - execute(pool); - - expect(pool.length).toBe(2); - expect(pool).not.toContain(oldest); - expect(pool).toContain(recent); - expect(pool).toContain(middle); - }); -}); diff --git a/packages/webgpu/src/TexturePool/service/TexturePoolEvictOldestService.ts b/packages/webgpu/src/TexturePool/service/TexturePoolEvictOldestService.ts deleted file mode 100644 index 34184e61..00000000 --- a/packages/webgpu/src/TexturePool/service/TexturePoolEvictOldestService.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { IPooledTexture } from "../../interface/IPooledTexture"; - -/** - * @description 最も古い未使用エントリを削除 - * Evict the oldest unused pool entry - * - * @param {IPooledTexture[]} pool - * @return {void} - * @method - * @protected - */ -export const execute = (pool: IPooledTexture[]): void => { - let oldestIndex = -1; - let oldestFrame = Infinity; - - for (let i = 0; i < pool.length; i++) { - const entry = pool[i]; - if (!entry.inUse && entry.lastUsedFrame < oldestFrame) { - oldestFrame = entry.lastUsedFrame; - oldestIndex = i; - } - } - - if (oldestIndex >= 0) { - pool[oldestIndex].texture.destroy(); - pool.splice(oldestIndex, 1); - } -}; diff --git a/packages/webgpu/src/TexturePool/service/TexturePoolReleaseService.ts b/packages/webgpu/src/TexturePool/service/TexturePoolReleaseService.ts index 15d796ef..1a0e14e1 100644 --- a/packages/webgpu/src/TexturePool/service/TexturePoolReleaseService.ts +++ b/packages/webgpu/src/TexturePool/service/TexturePoolReleaseService.ts @@ -4,9 +4,9 @@ import type { ITexturePoolBuckets } from "../../interface/IPooledTexture"; * @description テクスチャをプールに返却(バケットMap版) * Release texture back to pool (bucket Map version) * - * @param {ITexturePoolBuckets} buckets - * @param {GPUTexture} texture - * @param {number} currentFrame + * @param {ITexturePoolBuckets} buckets - テクスチャプールバケット + * @param {GPUTexture} texture - 返却するテクスチャ + * @param {number} current_frame - 現在のフレーム番号 * @return {void} * @method * @protected @@ -14,13 +14,13 @@ import type { ITexturePoolBuckets } from "../../interface/IPooledTexture"; export const execute = ( buckets: ITexturePoolBuckets, texture: GPUTexture, - currentFrame: number + current_frame: number ): void => { for (const bucket of buckets.values()) { for (let i = 0; i < bucket.length; i++) { if (bucket[i].texture === texture) { bucket[i].inUse = false; - bucket[i].lastUsedFrame = currentFrame; + bucket[i].lastUsedFrame = current_frame; return; } } diff --git a/packages/webgpu/src/TexturePool/usecase/TexturePoolAcquireUseCase.ts b/packages/webgpu/src/TexturePool/usecase/TexturePoolAcquireUseCase.ts index 97ca9558..0c337983 100644 --- a/packages/webgpu/src/TexturePool/usecase/TexturePoolAcquireUseCase.ts +++ b/packages/webgpu/src/TexturePool/usecase/TexturePoolAcquireUseCase.ts @@ -2,10 +2,11 @@ import type { IPooledTexture, ITexturePoolBuckets } from "../../interface/IPoole /** * @description バケットキーを生成(exactサイズ + フォーマット) + * Build bucket key from exact size and format * - * @param {number} width - * @param {number} height - * @param {GPUTextureFormat} format + * @param {number} width - テクスチャ幅 + * @param {number} height - テクスチャ高さ + * @param {GPUTextureFormat} format - テクスチャフォーマット * @return {string} */ const buildKey = (width: number, height: number, format: GPUTextureFormat): string => @@ -17,15 +18,15 @@ const buildKey = (width: number, height: number, format: GPUTextureFormat): stri * @description テクスチャを取得または作成(バケットMap検索) * Acquire texture from pool or create new one (bucket Map lookup) * - * @param {GPUDevice} device - * @param {ITexturePoolBuckets} buckets - * @param {number} width - * @param {number} height - * @param {GPUTextureFormat} format - * @param {GPUTextureUsageFlags} usage - * @param {number} currentFrame - * @param {number} maxPoolSize - * @param {number[]} totalCount - [0]に現在の合計数を格納 + * @param {GPUDevice} device - GPUデバイス + * @param {ITexturePoolBuckets} buckets - テクスチャプールバケット + * @param {number} width - テクスチャ幅 + * @param {number} height - テクスチャ高さ + * @param {GPUTextureFormat} format - テクスチャフォーマット + * @param {GPUTextureUsageFlags} usage - テクスチャ使用フラグ + * @param {number} current_frame - 現在のフレーム番号 + * @param {number} max_pool_size - プールの最大サイズ + * @param {number[]} total_count - [0]に現在の合計数を格納 * @return {GPUTexture} * @method * @protected @@ -37,9 +38,9 @@ export const execute = ( height: number, format: GPUTextureFormat, usage: GPUTextureUsageFlags, - currentFrame: number, - maxPoolSize: number, - totalCount: number[] + current_frame: number, + max_pool_size: number, + total_count: number[] ): GPUTexture => { const key = buildKey(width, height, format); @@ -50,14 +51,14 @@ export const execute = ( const entry = bucket[i]; if (!entry.inUse) { entry.inUse = true; - entry.lastUsedFrame = currentFrame; + entry.lastUsedFrame = current_frame; return entry.texture; } } } // プールが満杯なら最も古い未使用エントリを削除(LRU回収) - if (totalCount[0] >= maxPoolSize) { + if (total_count[0] >= max_pool_size) { let oldestFrame = Infinity; let oldestKey = ""; let oldestIdx = -1; @@ -80,7 +81,7 @@ export const execute = ( if (bEntries.length === 0) { buckets.delete(oldestKey); } - totalCount[0]--; + total_count[0]--; } } @@ -96,7 +97,7 @@ export const execute = ( width, height, format, - "lastUsedFrame": currentFrame, + "lastUsedFrame": current_frame, "inUse": true }; @@ -105,7 +106,7 @@ export const execute = ( } else { buckets.set(key, [entry]); } - totalCount[0]++; + total_count[0]++; return texture; }; diff --git a/packages/webgpu/src/WebGPUUtil.test.ts b/packages/webgpu/src/WebGPUUtil.test.ts index c81c8672..95ac398a 100644 --- a/packages/webgpu/src/WebGPUUtil.test.ts +++ b/packages/webgpu/src/WebGPUUtil.test.ts @@ -1,7 +1,6 @@ import { describe, it, expect, beforeEach } from "vitest"; import { $samples, - $setSamples, WebGPUUtil, $context, $setContext, @@ -13,18 +12,9 @@ describe("WebGPUUtil", () => { describe("$samples", () => { - it("should default to 1", () => + it("should have default value", () => { - $setSamples(1); - expect($samples).toBe(1); - }); - - it("should set samples", () => - { - $setSamples(4); - expect($samples).toBe(4); - - $setSamples(1); // Reset + expect(typeof $samples).toBe("number"); }); }); diff --git a/packages/webgpu/src/WebGPUUtil.ts b/packages/webgpu/src/WebGPUUtil.ts index d56298c4..b9e99aff 100644 --- a/packages/webgpu/src/WebGPUUtil.ts +++ b/packages/webgpu/src/WebGPUUtil.ts @@ -4,37 +4,52 @@ * * @type {number} * @default 4 - * @protected - * - * @note WebGL版と同じくMSAA 4xをデフォルトで有効化 - * 曲線のアンチエイリアス品質向上のため */ -export let $samples: number = 4; +export const $samples: number = 4; /** - * @description 描画のサンプリング数を変更 - * Change the number of samples for drawing - * - * @param {number} samples - * @return {void} - * @method - * @protected + * @description WebGPU描画ユーティリティクラス + * Utility class for WebGPU rendering operations */ -export const $setSamples = (samples: number): void => -{ - $samples = samples; -}; - export class WebGPUUtil { + /** + * @description GPUデバイスインスタンス + * GPU device instance + * + * @type {GPUDevice | null} + */ private static device: GPUDevice | null = null; + + /** + * @description デバイスピクセル比率 + * Device pixel ratio for high-DPI rendering + * + * @type {number} + */ private static devicePixelRatio: number = 1; + + /** + * @description レンダリング最大サイズ(テクスチャアトラス用) + * Maximum render size for texture atlas + * + * @type {number} + */ private static renderMaxSize: number = 8192; + + /** + * @description Float32Array(4) のオブジェクトプール + * Object pool for Float32Array(4) instances + * + * @type {Float32Array[]} + */ private static float32Array4Pool: Float32Array[] = []; /** - * @description Set GPUDevice - * @param {GPUDevice} gpu_device + * @description GPUデバイスを設定する + * Set the GPUDevice instance + * + * @param {GPUDevice} gpu_device - GPUデバイス / GPU device * @return {void} */ public static setDevice(gpu_device: GPUDevice): void @@ -43,7 +58,9 @@ export class WebGPUUtil } /** - * @description Get GPUDevice + * @description GPUデバイスを取得する + * Get the GPUDevice instance + * * @return {GPUDevice} */ public static getDevice(): GPUDevice @@ -55,8 +72,10 @@ export class WebGPUUtil } /** - * @description Set device pixel ratio - * @param {number} ratio + * @description デバイスピクセル比率を設定する + * Set the device pixel ratio + * + * @param {number} ratio - ピクセル比率 / pixel ratio * @return {void} */ public static setDevicePixelRatio(ratio: number): void @@ -65,7 +84,9 @@ export class WebGPUUtil } /** - * @description Get device pixel ratio + * @description デバイスピクセル比率を取得する + * Get the device pixel ratio + * * @return {number} */ public static getDevicePixelRatio(): number @@ -74,8 +95,10 @@ export class WebGPUUtil } /** - * @description Set render max size - * @param {number} size + * @description レンダリング最大サイズを設定する + * Set the maximum render size + * + * @param {number} size - 最大サイズ / maximum size in pixels * @return {void} */ public static setRenderMaxSize(size: number): void @@ -84,7 +107,9 @@ export class WebGPUUtil } /** - * @description Get render max size (for atlas) + * @description レンダリング最大サイズを取得する(アトラス用) + * Get the maximum render size (for texture atlas) + * * @return {number} */ public static getRenderMaxSize(): number @@ -93,8 +118,10 @@ export class WebGPUUtil } /** - * @description Create Float32Array - * @param {number} length + * @description 指定長のFloat32Arrayを生成する + * Create a new Float32Array with the specified length + * + * @param {number} length - 配列の長さ / array length * @return {Float32Array} */ public static createFloat32Array(length: number): Float32Array @@ -103,8 +130,10 @@ export class WebGPUUtil } /** - * @description Create generic array - * @return {Array} + * @description 汎用の空配列を生成する + * Create a new empty generic array + * + * @return {T[]} */ public static createArray(): T[] { @@ -112,7 +141,9 @@ export class WebGPUUtil } /** - * @description Get Float32Array(4) from pool + * @description Float32Array(4) をプールから取得する(なければ新規作成) + * Get a Float32Array(4) from the pool, or create a new one + * * @return {Float32Array} */ public static getFloat32Array4(): Float32Array @@ -123,8 +154,10 @@ export class WebGPUUtil } /** - * @description Return Float32Array(4) to pool - * @param {Float32Array} array + * @description Float32Array(4) をプールに返却する + * Return a Float32Array(4) to the pool for reuse + * + * @param {Float32Array} array - 返却する配列 / array to return * @return {void} */ public static poolFloat32Array4(array: Float32Array): void @@ -137,12 +170,18 @@ export class WebGPUUtil /** * @description グローバルコンテキスト(WebGLUtilの$contextに相当) + * Global context instance (equivalent to $context in WebGLUtil) + * + * @type {any} */ export let $context: any = null; /** - * @description コンテキストを設定 - * @param {any} context + * @description グローバルコンテキストを設定する + * Set the global context instance + * + * @param {any} context - コンテキスト / context instance + * @return {void} */ export const $setContext = (context: any): void => { @@ -150,7 +189,9 @@ export const $setContext = (context: any): void => }; /** - * @description Float32Array(4) をプールから取得 + * @description Float32Array(4) をプールから取得する + * Get a Float32Array(4) from the pool + * * @return {Float32Array} */ export const $getFloat32Array4 = (): Float32Array => @@ -159,8 +200,11 @@ export const $getFloat32Array4 = (): Float32Array => }; /** - * @description Float32Array(4) をプールに返却 - * @param {Float32Array} array + * @description Float32Array(4) をプールに返却する + * Return a Float32Array(4) to the pool + * + * @param {Float32Array} array - 返却する配列 / array to return + * @return {void} */ export const $poolFloat32Array4 = (array: Float32Array): void => { diff --git a/packages/webgpu/src/interface/IAttachmentObject.ts b/packages/webgpu/src/interface/IAttachmentObject.ts index 7a3fcc10..6d6e4206 100644 --- a/packages/webgpu/src/interface/IAttachmentObject.ts +++ b/packages/webgpu/src/interface/IAttachmentObject.ts @@ -11,14 +11,50 @@ import type { IStencilBufferObject } from "./IStencilBufferObject"; */ export interface IAttachmentObject { + /** + * @description アタッチメントの一意な識別子 + * Unique identifier for the attachment + */ id: number; + /** + * @description アタッチメントの幅(ピクセル) + * Width of the attachment in pixels + */ width: number; + /** + * @description アタッチメントの高さ(ピクセル) + * Height of the attachment in pixels + */ height: number; + /** + * @description 現在のクリップ(マスク)ネストレベル + * Current clip (mask) nesting level + */ clipLevel: number; + /** + * @description MSAAが有効かどうか + * Whether MSAA is enabled + */ msaa: boolean; + /** + * @description マスクモードが有効かどうか + * Whether mask mode is enabled + */ mask: boolean; + /** + * @description カラーバッファオブジェクト + * Color buffer object + */ color: IColorBufferObject | null; + /** + * @description テクスチャオブジェクト + * Texture object + */ texture: ITextureObject | null; + /** + * @description ステンシルバッファオブジェクト + * Stencil buffer object + */ stencil: IStencilBufferObject | null; /** * @description MSAAテクスチャ(sampleCount > 1 の場合に使用) diff --git a/packages/webgpu/src/interface/IBlendMode.ts b/packages/webgpu/src/interface/IBlendMode.ts index 95fe0434..2370ad76 100644 --- a/packages/webgpu/src/interface/IBlendMode.ts +++ b/packages/webgpu/src/interface/IBlendMode.ts @@ -1,3 +1,10 @@ +/** + * @description ブレンドモードの型定義 + * Blend mode type definition + * + * 描画オブジェクトに適用可能なブレンドモードを定義します。 + * Defines the blend modes that can be applied to display objects. + */ export type IBlendMode = | "normal" | "layer" diff --git a/packages/webgpu/src/interface/IBlendState.ts b/packages/webgpu/src/interface/IBlendState.ts index ba9a54d1..1c52dff3 100644 --- a/packages/webgpu/src/interface/IBlendState.ts +++ b/packages/webgpu/src/interface/IBlendState.ts @@ -3,6 +3,14 @@ * WebGPU blend state definitions */ export interface IBlendState { + /** + * @description カラーチャンネルのブレンド設定 + * Blend component configuration for the color channel + */ color: GPUBlendComponent; + /** + * @description アルファチャンネルのブレンド設定 + * Blend component configuration for the alpha channel + */ alpha: GPUBlendComponent; } diff --git a/packages/webgpu/src/interface/IBounds.ts b/packages/webgpu/src/interface/IBounds.ts index 762980e7..9d83e462 100644 --- a/packages/webgpu/src/interface/IBounds.ts +++ b/packages/webgpu/src/interface/IBounds.ts @@ -1,7 +1,30 @@ +/** + * @description バウンディングボックスのインターフェース + * Bounding box interface + * + * 描画オブジェクトの矩形領域を最小・最大座標で定義します。 + * Defines a rectangular area of a display object using min/max coordinates. + */ export interface IBounds { + /** + * @description X軸の最小値 + * Minimum value on the X axis + */ xMin: number; + /** + * @description Y軸の最小値 + * Minimum value on the Y axis + */ yMin: number; + /** + * @description X軸の最大値 + * Maximum value on the X axis + */ xMax: number; + /** + * @description Y軸の最大値 + * Maximum value on the Y axis + */ yMax: number; } diff --git a/packages/webgpu/src/interface/ICachedBindGroup.ts b/packages/webgpu/src/interface/ICachedBindGroup.ts deleted file mode 100644 index 4910cbf3..00000000 --- a/packages/webgpu/src/interface/ICachedBindGroup.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @description キャッシュされたBindGroup - * Cached bind group interface - */ -export interface ICachedBindGroup { - bindGroup: GPUBindGroup; - lastUsedFrame: number; -} diff --git a/packages/webgpu/src/interface/IColorBufferObject.ts b/packages/webgpu/src/interface/IColorBufferObject.ts index 4565f6be..e014555f 100644 --- a/packages/webgpu/src/interface/IColorBufferObject.ts +++ b/packages/webgpu/src/interface/IColorBufferObject.ts @@ -9,11 +9,39 @@ import type { IStencilBufferObject } from "./IStencilBufferObject"; */ export interface IColorBufferObject { + /** + * @description GPUテクスチャリソース + * GPU texture resource + */ resource: GPUTexture; + /** + * @description テクスチャビュー(レンダーパスへのアタッチ用) + * Texture view for attaching to render passes + */ view: GPUTextureView; + /** + * @description 対応するステンシルバッファオブジェクト + * Associated stencil buffer object + */ stencil: IStencilBufferObject; + /** + * @description カラーバッファの幅(ピクセル) + * Width of the color buffer in pixels + */ width: number; + /** + * @description カラーバッファの高さ(ピクセル) + * Height of the color buffer in pixels + */ height: number; + /** + * @description バッファの面積(width × height) + * Area of the buffer (width × height) + */ area: number; + /** + * @description バッファが変更されたかどうか + * Whether the buffer has been modified + */ dirty: boolean; } diff --git a/packages/webgpu/src/interface/IComplexBlendItem.ts b/packages/webgpu/src/interface/IComplexBlendItem.ts index ac27a265..35569d4f 100644 --- a/packages/webgpu/src/interface/IComplexBlendItem.ts +++ b/packages/webgpu/src/interface/IComplexBlendItem.ts @@ -5,16 +5,64 @@ import type { Node } from "@next2d/texture-packer"; * Complex blend mode rendering queue */ export interface IComplexBlendItem { + /** + * @description テクスチャアトラスのノード + * Texture atlas node + */ node: Node; + /** + * @description 描画領域のX最小座標 + * Minimum X coordinate of the rendering area + */ x_min: number; + /** + * @description 描画領域のY最小座標 + * Minimum Y coordinate of the rendering area + */ y_min: number; + /** + * @description 描画領域のX最大座標 + * Maximum X coordinate of the rendering area + */ x_max: number; + /** + * @description 描画領域のY最大座標 + * Maximum Y coordinate of the rendering area + */ y_max: number; + /** + * @description カラー変換配列 + * Color transform array + */ color_transform: Float32Array; + /** + * @description 変換行列 + * Transformation matrix + */ matrix: Float32Array; + /** + * @description ブレンドモード名 + * Blend mode name + */ blend_mode: string; + /** + * @description ビューポートの幅 + * Viewport width + */ viewport_width: number; + /** + * @description ビューポートの高さ + * Viewport height + */ viewport_height: number; + /** + * @description レンダリング最大サイズ + * Maximum render size + */ render_max_size: number; + /** + * @description グローバル透明度 + * Global alpha transparency + */ global_alpha: number; } diff --git a/packages/webgpu/src/interface/IFilterConfig.ts b/packages/webgpu/src/interface/IFilterConfig.ts index f4381878..6f788e07 100644 --- a/packages/webgpu/src/interface/IFilterConfig.ts +++ b/packages/webgpu/src/interface/IFilterConfig.ts @@ -1,17 +1,32 @@ import type { IAttachmentObject } from "./IAttachmentObject"; -import type { ComputePipelineManager } from "../Compute/ComputePipelineManager"; /** * @description フィルター処理の共通設定 * Common filter processing configuration */ export interface IFilterConfig { + /** + * @description GPUデバイス + * GPU device instance + */ device: GPUDevice; + /** + * @description GPUコマンドエンコーダー + * GPU command encoder + */ commandEncoder: GPUCommandEncoder; + /** + * @description ユニフォームバッファの管理インターフェース + * Uniform buffer management interface + */ bufferManager?: { acquireUniformBuffer(requiredSize: number): GPUBuffer; acquireAndWriteUniformBuffer(data: Float32Array, byteLength?: number): GPUBuffer; }; + /** + * @description フレームバッファの管理インターフェース + * Frame buffer management interface + */ frameBufferManager: { createTemporaryAttachment(width: number, height: number): IAttachmentObject; releaseTemporaryAttachment(attachment: IAttachmentObject): void; @@ -21,14 +36,25 @@ export interface IFilterConfig { loadOp: GPULoadOp ): GPURenderPassDescriptor; }; + /** + * @description パイプラインの管理インターフェース + * Pipeline management interface + */ pipelineManager: { getPipeline(name: string): GPURenderPipeline | undefined; getFilterPipeline(baseName: string, constants: Record): GPURenderPipeline | undefined; getBindGroupLayout(name: string): GPUBindGroupLayout | undefined; }; + /** + * @description テクスチャの管理インターフェース + * Texture management interface + */ textureManager: { createSampler(name: string, smooth: boolean): GPUSampler; }; - computePipelineManager?: ComputePipelineManager; + /** + * @description フレームテクスチャの配列 + * Array of frame textures + */ frameTextures: GPUTexture[]; } diff --git a/packages/webgpu/src/interface/IGradientLUTData.ts b/packages/webgpu/src/interface/IGradientLUTData.ts deleted file mode 100644 index 2213b222..00000000 --- a/packages/webgpu/src/interface/IGradientLUTData.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @description グラデーションLUTデータを生成する結果型 - * Result type for generated gradient LUT data - */ -export interface IGradientLUTData { - pixels: Uint8Array; - resolution: number; -} diff --git a/packages/webgpu/src/interface/IGradientStop.ts b/packages/webgpu/src/interface/IGradientStop.ts index 86176eae..d6cc6ec0 100644 --- a/packages/webgpu/src/interface/IGradientStop.ts +++ b/packages/webgpu/src/interface/IGradientStop.ts @@ -3,9 +3,29 @@ * Gradient stop type definition */ export interface IGradientStop { + /** + * @description グラデーション位置(0.0〜1.0) + * Gradient position ratio (0.0 to 1.0) + */ ratio: number; + /** + * @description 赤チャンネル値(0〜255) + * Red channel value (0 to 255) + */ r: number; + /** + * @description 緑チャンネル値(0〜255) + * Green channel value (0 to 255) + */ g: number; + /** + * @description 青チャンネル値(0〜255) + * Blue channel value (0 to 255) + */ b: number; + /** + * @description アルファチャンネル値(0〜255) + * Alpha channel value (0 to 255) + */ a: number; } diff --git a/packages/webgpu/src/interface/ILocalFilterConfig.ts b/packages/webgpu/src/interface/ILocalFilterConfig.ts index 986d9b1d..1abd6e3d 100644 --- a/packages/webgpu/src/interface/ILocalFilterConfig.ts +++ b/packages/webgpu/src/interface/ILocalFilterConfig.ts @@ -3,20 +3,50 @@ import type { BufferManager } from "../BufferManager"; import type { FrameBufferManager } from "../FrameBufferManager"; import type { PipelineManager } from "../Shader/PipelineManager"; import type { TextureManager } from "../TextureManager"; -import type { ComputePipelineManager } from "../Compute/ComputePipelineManager"; /** * @description フィルター適用時のローカル設定(ContextApplyFilterUseCase用) * Local filter configuration for ContextApplyFilterUseCase */ export interface ILocalFilterConfig { + /** + * @description GPUデバイス + * GPU device instance + */ device: GPUDevice; + /** + * @description GPUコマンドエンコーダー + * GPU command encoder + */ commandEncoder: GPUCommandEncoder; + /** + * @description バッファマネージャー + * Buffer manager instance + */ bufferManager: BufferManager; + /** + * @description フレームバッファマネージャー + * Frame buffer manager instance + */ frameBufferManager: FrameBufferManager; + /** + * @description パイプラインマネージャー + * Pipeline manager instance + */ pipelineManager: PipelineManager; + /** + * @description テクスチャマネージャー + * Texture manager instance + */ textureManager: TextureManager; + /** + * @description メインのアタッチメントオブジェクト(任意) + * Main attachment object (optional) + */ mainAttachment?: IAttachmentObject; - computePipelineManager?: ComputePipelineManager; + /** + * @description フレームテクスチャの配列 + * Array of frame textures + */ frameTextures: GPUTexture[]; } diff --git a/packages/webgpu/src/interface/IMeshResult.ts b/packages/webgpu/src/interface/IMeshResult.ts index 934ad3ef..1bd68431 100644 --- a/packages/webgpu/src/interface/IMeshResult.ts +++ b/packages/webgpu/src/interface/IMeshResult.ts @@ -3,6 +3,14 @@ * Common interface for mesh generation results */ export interface IMeshResult { + /** + * @description 頂点データバッファ + * Vertex data buffer + */ buffer: Float32Array; + /** + * @description インデックスの数(描画する三角形の頂点数) + * Number of indices (vertex count for drawing triangles) + */ indexCount: number; } diff --git a/packages/webgpu/src/interface/IPoint.ts b/packages/webgpu/src/interface/IPoint.ts index a5980d9a..981f0d24 100644 --- a/packages/webgpu/src/interface/IPoint.ts +++ b/packages/webgpu/src/interface/IPoint.ts @@ -1,5 +1,17 @@ +/** + * @description 2D座標点のインターフェース + * 2D coordinate point interface + */ export interface IPoint { + /** + * @description X座標 + * X coordinate + */ x: number; + /** + * @description Y座標 + * Y coordinate + */ y: number; } diff --git a/packages/webgpu/src/interface/IPooledBuffer.ts b/packages/webgpu/src/interface/IPooledBuffer.ts deleted file mode 100644 index b73254af..00000000 --- a/packages/webgpu/src/interface/IPooledBuffer.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * @description プールされたバッファのエントリ - * Pooled buffer entry - */ -export interface IPooledBuffer { - buffer: GPUBuffer; - size: number; -} diff --git a/packages/webgpu/src/interface/IPooledTexture.ts b/packages/webgpu/src/interface/IPooledTexture.ts index c129337e..4a1e0d1e 100644 --- a/packages/webgpu/src/interface/IPooledTexture.ts +++ b/packages/webgpu/src/interface/IPooledTexture.ts @@ -3,11 +3,35 @@ * Pooled texture interface */ export interface IPooledTexture { + /** + * @description GPUテクスチャリソース + * GPU texture resource + */ texture: GPUTexture; + /** + * @description テクスチャの幅(ピクセル) + * Width of the texture in pixels + */ width: number; + /** + * @description テクスチャの高さ(ピクセル) + * Height of the texture in pixels + */ height: number; + /** + * @description テクスチャフォーマット + * Texture format + */ format: GPUTextureFormat; + /** + * @description 最後に使用されたフレーム番号 + * Last frame number when this texture was used + */ lastUsedFrame: number; + /** + * @description 使用中フラグ + * Whether this texture is currently in use + */ inUse: boolean; } diff --git a/packages/webgpu/src/interface/IQuadraticSegment.ts b/packages/webgpu/src/interface/IQuadraticSegment.ts index 84476bdd..35527b2e 100644 --- a/packages/webgpu/src/interface/IQuadraticSegment.ts +++ b/packages/webgpu/src/interface/IQuadraticSegment.ts @@ -5,6 +5,14 @@ import type { IPoint } from "./IPoint"; * Quadratic bezier segment approximation */ export interface IQuadraticSegment { + /** + * @description 制御点 + * Control point + */ ctrl: IPoint; + /** + * @description 終点 + * End point + */ end: IPoint; } diff --git a/packages/webgpu/src/interface/IRectangleInfo.ts b/packages/webgpu/src/interface/IRectangleInfo.ts deleted file mode 100644 index 60a983d5..00000000 --- a/packages/webgpu/src/interface/IRectangleInfo.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { IPoint } from "./IPoint"; -import type { IPath } from "./IPath"; - -/** - * @description 矩形の情報を保持する型 - * Rectangle info type for stroke generation - */ -export interface IRectangleInfo { - path: IPath; - startUp: IPoint; - startDown: IPoint; - endUp: IPoint; - endDown: IPoint; -} diff --git a/packages/webgpu/src/interface/IStencilBufferObject.ts b/packages/webgpu/src/interface/IStencilBufferObject.ts index c3f7ac7b..431ec929 100644 --- a/packages/webgpu/src/interface/IStencilBufferObject.ts +++ b/packages/webgpu/src/interface/IStencilBufferObject.ts @@ -7,11 +7,39 @@ */ export interface IStencilBufferObject { + /** + * @description ステンシルバッファの一意な識別子 + * Unique identifier for the stencil buffer + */ id: number; + /** + * @description GPUテクスチャリソース + * GPU texture resource + */ resource: GPUTexture; + /** + * @description テクスチャビュー(レンダーパスへのアタッチ用) + * Texture view for attaching to render passes + */ view: GPUTextureView; + /** + * @description ステンシルバッファの幅(ピクセル) + * Width of the stencil buffer in pixels + */ width: number; + /** + * @description ステンシルバッファの高さ(ピクセル) + * Height of the stencil buffer in pixels + */ height: number; + /** + * @description バッファの面積(width × height) + * Area of the buffer (width × height) + */ area: number; + /** + * @description バッファが変更されたかどうか + * Whether the buffer has been modified + */ dirty: boolean; } diff --git a/packages/webgpu/src/interface/IStorageBufferConfig.ts b/packages/webgpu/src/interface/IStorageBufferConfig.ts index edb3657f..10467f1c 100644 --- a/packages/webgpu/src/interface/IStorageBufferConfig.ts +++ b/packages/webgpu/src/interface/IStorageBufferConfig.ts @@ -5,16 +5,19 @@ export interface IStorageBufferConfig { /** * @description バッファサイズ(バイト) + * Buffer size in bytes */ size: number; /** - * @description 使用目的 + * @description 使用目的フラグ + * Usage flags for the buffer */ usage: GPUBufferUsageFlags; /** * @description ラベル(デバッグ用) + * Label for debugging purposes */ label?: string; } @@ -26,21 +29,25 @@ export interface IStorageBufferConfig { export interface IPooledStorageBuffer { /** * @description GPUバッファ + * GPU buffer instance */ buffer: GPUBuffer; /** * @description バッファサイズ(バイト) + * Buffer size in bytes */ size: number; /** * @description 使用中フラグ + * Whether this buffer is currently in use */ inUse: boolean; /** * @description 最後に使用されたフレーム番号 + * Last frame number when this buffer was used */ lastUsedFrame: number; } diff --git a/packages/webgpu/src/interface/ITextureObject.ts b/packages/webgpu/src/interface/ITextureObject.ts index 8ee1d5e3..032bc703 100644 --- a/packages/webgpu/src/interface/ITextureObject.ts +++ b/packages/webgpu/src/interface/ITextureObject.ts @@ -7,11 +7,39 @@ */ export interface ITextureObject { + /** + * @description テクスチャの一意な識別子 + * Unique identifier for the texture + */ id: number; + /** + * @description GPUテクスチャリソース + * GPU texture resource + */ resource: GPUTexture; + /** + * @description テクスチャビュー(シェーダーバインド用) + * Texture view for shader binding + */ view: GPUTextureView; + /** + * @description テクスチャの幅(ピクセル) + * Width of the texture in pixels + */ width: number; + /** + * @description テクスチャの高さ(ピクセル) + * Height of the texture in pixels + */ height: number; + /** + * @description テクスチャの面積(width × height) + * Area of the texture (width × height) + */ area: number; + /** + * @description スムージング(バイリニアフィルタリング)が有効かどうか + * Whether smoothing (bilinear filtering) is enabled + */ smooth: boolean; } diff --git a/specs/cn/display-object.md b/specs/cn/display-object.md index 27371142..efec3f6f 100644 --- a/specs/cn/display-object.md +++ b/specs/cn/display-object.md @@ -44,6 +44,7 @@ DisplayObject 是 Next2D Player 中所有显示对象的基类。 | `scaleX` | number | 从参考点应用的对象水平缩放值 | | `scaleY` | number | 从参考点应用的对象垂直缩放值 | | `visible` | boolean | 显示对象是否可见(默认:true) | +| `cacheAsBitmap` | Matrix \| null | 位图缓存用的 Matrix。**仅可设置缩放值(a, d)**(b, c, tx, ty 将被忽略)。以 1.0 为基准,应用于 displayObject 自身的 scaleX/scaleY。缓存质量 = 指定 Matrix × 自身缩放 × 舞台缩放。缓存时不受祖先 Matrix 影响,但绘制时应用祖先 Matrix。命中测试、宽度和高度基于矢量。**适用对象:Shape、TextField、Sprite、MovieClip**(Video 因图像尺寸固定而不适用)(默认:null) | | `x` | number | 相对于父 DisplayObjectContainer 本地坐标的 X 坐标 | | `y` | number | 相对于父 DisplayObjectContainer 本地坐标的 Y 坐标 | @@ -158,6 +159,108 @@ const state = displayObject.getGlobalVariable("gameState"); displayObject.clearGlobalVariable(); // 清除全部 ``` +### cacheAsBitmap 示例 + +`cacheAsBitmap` 将矢量绘制或容器缓存为位图,从第二帧开始重复使用缓存纹理,从而提高性能。 + +**适用类:** +- `Shape` — 缓存矢量绘制 +- `TextField` — 缓存文本渲染 +- `Sprite` — 将容器及其所有子元素一起缓存 +- `MovieClip` — 将容器及其所有子元素一起缓存 + +> ⚠️ 不适用于 `Video`。由于 Video 具有固定的图像尺寸,缓存没有效果。 + +**Matrix 限制:** +Matrix 中仅可设置缩放值(a, d)。旋转(b, c)和平移(tx, ty)将被忽略。 + +```typescript +// ✅ 正确用法(仅设置缩放) +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); // 1 倍 +shape.cacheAsBitmap = new Matrix(2, 0, 0, 2, 0, 0); // 2 倍质量 + +// ❌ 旋转/平移值将被忽略 +shape.cacheAsBitmap = new Matrix(1, 0.5, 0.5, 1, 100, 200); // b, c, tx, ty 被忽略 +``` + +**使用场景:** + +1. **高速动画** — 高速移动的对象视觉清晰度低,缓存导致的画质降低几乎不可察觉,同时能显著提高性能 +2. **静态背景和 UI 元素** — 缓存不变的 UI 元素(面板、装饰、图标等)可消除每帧重绘成本 +3. **复杂矢量绘制** — 缓存路径较多的复杂 Shape 可大幅降低绘制开销 + +```typescript +const { Shape, Sprite } = next2d.display; +const { Matrix } = next2d.geom; + +// 以 1 倍比例缓存(1.0 基准 = 相对于 displayObject 自身的 scaleX/scaleY) +const shape = new Shape(); +shape.graphics.beginFill(0xFF0000).drawCircle(50, 50, 40).endFill(); +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + +// 以 2 倍分辨率缓存(自身缩放的 2 倍质量) +const hqShape = new Shape(); +hqShape.graphics.beginFill(0x00FF00).drawRect(0, 0, 100, 80).endFill(); +hqShape.cacheAsBitmap = new Matrix(2, 0, 0, 2, 0, 0); + +// scaleX/scaleY 会被反映(缓存质量 = Matrix × 自身缩放 × 舞台缩放) +shape.scaleX = 2; // 缓存质量: 1 × 2 × stageScale +shape.scaleY = 2; + +// 父级缩放不影响缓存质量(但绘制时会应用) +const container = new Sprite(); +container.scaleX = 3; +container.scaleY = 3; +container.addChild(shape); + +// 命中测试、宽度和高度基于矢量 +const bounds = shape.getBounds(shape); // 返回矢量边界 + +// 禁用缓存 +shape.cacheAsBitmap = null; +``` + +### 在 DisplayObjectContainer 上使用 cacheAsBitmap + +您也可以在 `Sprite` 和 `MovieClip` 等 `DisplayObjectContainer` 子类上设置 `cacheAsBitmap`。 +容器的所有子元素将被渲染到单个纹理中并缓存,在后续帧中重复使用。 + +```typescript +const { Shape, Sprite, MovieClip } = next2d.display; +const { Matrix } = next2d.geom; + +// 包含多个子元素的 Sprite +const sprite = new Sprite(); +const rect = new Shape(); +rect.graphics.beginFill(0xFF0000).drawRect(0, 0, 100, 80).endFill(); +sprite.addChild(rect); +const circle = new Shape(); +circle.graphics.beginFill(0x00FF00).drawCircle(50, 40, 30).endFill(); +sprite.addChild(circle); + +// 将整个容器缓存为位图 +sprite.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + +// 同样适用于 MovieClip +const mc = new MovieClip(); +mc.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + +// 禁用缓存(下一帧恢复正常渲染) +sprite.cacheAsBitmap = null; +``` + +**缓存行为:** +- 当对象或其祖先的缩放发生变化时,缓存将失效并在下一帧重新生成 +- 位置更改(x, y)会保留缓存 — 仅更新绘制位置 +- 在缩放频繁变化的动画期间,缓存每帧都会重新生成,因此缓存在动画完成后的静态状态下最为有效 + +**注意事项:** +- **Matrix 中仅可设置缩放值**(旋转和平移将被忽略) +- **不适用于 Video**(固定尺寸的图像数据) +- 缓存期间,子元素的更改(添加/删除/属性更改)不会反映在屏幕上 +- 当 `stage.rendererScale` 更改时,缓存会自动失效 +- 同时设置 `filter` 和 `cacheAsBitmap` 时,`cacheAsBitmap` 优先 + ## 相关 - [MovieClip](/cn/reference/player/movie-clip) diff --git a/specs/cn/shape.md b/specs/cn/shape.md index f9150f79..1d90fc61 100644 --- a/specs/cn/shape.md +++ b/specs/cn/shape.md @@ -231,7 +231,8 @@ stage.addChild(frontShape); ```javascript // 将复杂形状缓存为位图 -shape.cacheAsBitmap = true; +const { Matrix } = next2d.geom; +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); ``` ## Graphics 类 diff --git a/specs/en/display-object.md b/specs/en/display-object.md index f8faec70..6c9157f8 100644 --- a/specs/en/display-object.md +++ b/specs/en/display-object.md @@ -44,6 +44,7 @@ DisplayObject is the base class for all display objects in Next2D Player. | `scaleX` | number | Horizontal scale value of the object applied from the reference point | | `scaleY` | number | Vertical scale value of the object applied from the reference point | | `visible` | boolean | Whether the display object is visible (default: true) | +| `cacheAsBitmap` | Matrix \| null | Matrix for bitmap caching. **Only scale values (a, d) can be set** (b, c, tx, ty are ignored). 1.0-based, applied relative to the displayObject's own scaleX/scaleY. Cache quality = specified Matrix × own scale × stage scale. Independent of ancestor transforms for caching, but ancestor transforms are applied when drawing. Hit tests, width, and height remain vector-based. **Applicable to: Shape, TextField, Sprite, MovieClip** (not applicable to Video as it has fixed image dimensions) (default: null) | | `x` | number | X coordinate relative to the local coordinates of the parent DisplayObjectContainer | | `y` | number | Y coordinate relative to the local coordinates of the parent DisplayObjectContainer | @@ -158,6 +159,108 @@ const state = displayObject.getGlobalVariable("gameState"); displayObject.clearGlobalVariable(); // Clear all ``` +### cacheAsBitmap Example + +`cacheAsBitmap` caches vector drawings or containers as bitmaps, improving performance by reusing the cached texture from the second frame onward. + +**Applicable classes:** +- `Shape` — Cache vector drawings +- `TextField` — Cache text rendering +- `Sprite` — Cache containers and all their children together +- `MovieClip` — Cache containers and all their children together + +> ⚠️ Not applicable to `Video`. Since Video has fixed image dimensions, caching provides no benefit. + +**Matrix restriction:** +Only scale values (a, d) can be set in the Matrix. Rotation (b, c) and translation (tx, ty) are ignored. + +```typescript +// ✅ Correct usage (scale only) +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); // 1x scale +shape.cacheAsBitmap = new Matrix(2, 0, 0, 2, 0, 0); // 2x quality + +// ❌ Rotation/translation values are ignored +shape.cacheAsBitmap = new Matrix(1, 0.5, 0.5, 1, 100, 200); // b, c, tx, ty ignored +``` + +**Use cases:** + +1. **Fast-moving animations** — Objects moving at high speed have low visual clarity, so quality loss from caching is barely noticeable while providing significant performance gains +2. **Static backgrounds and UI elements** — Caching unchanging UI elements (panels, decorations, icons) eliminates per-frame redraw costs +3. **Complex vector drawings** — Caching Shapes with many paths dramatically reduces drawing overhead + +```typescript +const { Shape, Sprite } = next2d.display; +const { Matrix } = next2d.geom; + +// Cache at 1x scale (1.0-based = relative to displayObject's own scaleX/scaleY) +const shape = new Shape(); +shape.graphics.beginFill(0xFF0000).drawCircle(50, 50, 40).endFill(); +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + +// Cache at 2x resolution (2x the object's own scale quality) +const hqShape = new Shape(); +hqShape.graphics.beginFill(0x00FF00).drawRect(0, 0, 100, 80).endFill(); +hqShape.cacheAsBitmap = new Matrix(2, 0, 0, 2, 0, 0); + +// scaleX/scaleY are reflected (cache quality = Matrix × own scale × stage scale) +shape.scaleX = 2; // Cache quality: 1 × 2 × stageScale +shape.scaleY = 2; + +// Parent scale does not affect cache quality (but is applied when drawing) +const container = new Sprite(); +container.scaleX = 3; +container.scaleY = 3; +container.addChild(shape); + +// Hit tests, width, and height remain vector-based +const bounds = shape.getBounds(shape); // Returns vector bounds + +// Disable caching +shape.cacheAsBitmap = null; +``` + +### cacheAsBitmap on DisplayObjectContainer + +You can also set `cacheAsBitmap` on `DisplayObjectContainer` subclasses such as `Sprite` and `MovieClip`. +All child elements are rendered into a single texture and cached, reused on subsequent frames. + +```typescript +const { Shape, Sprite, MovieClip } = next2d.display; +const { Matrix } = next2d.geom; + +// Sprite with multiple children +const sprite = new Sprite(); +const rect = new Shape(); +rect.graphics.beginFill(0xFF0000).drawRect(0, 0, 100, 80).endFill(); +sprite.addChild(rect); +const circle = new Shape(); +circle.graphics.beginFill(0x00FF00).drawCircle(50, 40, 30).endFill(); +sprite.addChild(circle); + +// Cache the entire container as a bitmap +sprite.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + +// Also applicable to MovieClip +const mc = new MovieClip(); +mc.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + +// Disable caching (resumes normal rendering next frame) +sprite.cacheAsBitmap = null; +``` + +**Cache behavior:** +- When the scale of the object or its ancestors changes, the cache is invalidated and regenerated on the next frame +- Position changes (x, y) preserve the cache — only the drawing position is updated +- During animations with frequent scale changes, the cache is regenerated every frame, so caching is most effective after animations complete and the object is static + +**Notes:** +- **Only scale values can be set in the Matrix** (rotation and translation are ignored) +- **Not applicable to Video** (fixed-size image data) +- While cached, changes to children (add/remove/property changes) are not reflected on screen +- Cache is automatically invalidated when `stage.rendererScale` changes +- When both `filter` and `cacheAsBitmap` are set, `cacheAsBitmap` takes priority + ## Related - [MovieClip](/en/reference/player/movie-clip) diff --git a/specs/en/shape.md b/specs/en/shape.md index 0e25d3fd..bc33819f 100644 --- a/specs/en/shape.md +++ b/specs/en/shape.md @@ -231,7 +231,8 @@ stage.addChild(frontShape); ```javascript // Cache complex shapes as bitmap -shape.cacheAsBitmap = true; +const { Matrix } = next2d.geom; +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); ``` ## Graphics Class diff --git a/specs/ja/display-object.md b/specs/ja/display-object.md index 93ab9486..1d1c38cf 100644 --- a/specs/ja/display-object.md +++ b/specs/ja/display-object.md @@ -44,6 +44,7 @@ DisplayObjectは、Next2D Playerにおける全ての表示オブジェクトの | `scaleX` | number | 基準点から適用されるオブジェクトの水平スケール値 | | `scaleY` | number | 基準点から適用されるオブジェクトの垂直スケール値 | | `visible` | boolean | 表示オブジェクトが可視かどうか(デフォルト: true) | +| `cacheAsBitmap` | Matrix \| null | ビットマップキャッシュ用のMatrix。**スケール値(a, d)のみ設定可能**(b, c, tx, tyは無視されます)。1.0基準でdisplayObjectのscaleX/scaleYに適用される。キャッシュ品質 = 指定Matrix × 自身のスケール × stageスケール。先祖のMatrixの影響は受けないが、描画時には先祖のMatrixが適用される。ヒットテスト・幅・高さはベクター基準。**適用対象: Shape, TextField, Sprite, MovieClip**(Videoは固定サイズのため適用不可)(デフォルト: null) | | `x` | number | 親DisplayObjectContainerのローカル座標を基準にしたX座標 | | `y` | number | 親DisplayObjectContainerのローカル座標を基準にしたY座標 | @@ -158,6 +159,108 @@ const state = displayObject.getGlobalVariable("gameState"); displayObject.clearGlobalVariable(); // 全てクリア ``` +### cacheAsBitmapの例 + +`cacheAsBitmap`はベクター描画やコンテナをビットマップとしてキャッシュし、2回目以降の描画でキャッシュを再利用することでパフォーマンスを向上させるプロパティです。 + +**適用対象クラス:** +- `Shape` — ベクター描画のキャッシュ +- `TextField` — テキスト描画のキャッシュ +- `Sprite` — コンテナとその子要素をまとめてキャッシュ +- `MovieClip` — コンテナとその子要素をまとめてキャッシュ + +> ⚠️ `Video`には適用できません。Videoは画像サイズが固定されているため、キャッシュの効果がありません。 + +**Matrixの制限:** +Matrixにはスケール値(a, d)のみ設定できます。回転(b, c)や移動(tx, ty)は無視されます。 + +```typescript +// ✅ 正しい使い方(スケールのみ設定) +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); // 等倍 +shape.cacheAsBitmap = new Matrix(2, 0, 0, 2, 0, 0); // 2倍品質 + +// ❌ 回転・移動の値は無視される +shape.cacheAsBitmap = new Matrix(1, 0.5, 0.5, 1, 100, 200); // b, c, tx, tyは無視 +``` + +**利用用途の例:** + +1. **速度の速いアニメーション** — 高速で動くオブジェクトは視認性が低いため、キャッシュによる画質低下が目立たず、パフォーマンス向上が期待できます +2. **静的な背景やUI部品** — 変化しないUI要素(パネル、装飾、アイコンなど)をキャッシュすることで毎フレームの再描画コストを削減できます +3. **複雑なベクター描画** — パスが多い複雑なShapeをキャッシュすることで描画負荷を大幅に軽減できます + +```typescript +const { Shape, Sprite } = next2d.display; +const { Matrix } = next2d.geom; + +// 等倍でキャッシュ(1.0基準 = displayObjectのscaleX/scaleYに対する等倍) +const shape = new Shape(); +shape.graphics.beginFill(0xFF0000).drawCircle(50, 50, 40).endFill(); +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + +// 2倍の解像度でキャッシュ(自身のスケールの2倍品質) +const hqShape = new Shape(); +hqShape.graphics.beginFill(0x00FF00).drawRect(0, 0, 100, 80).endFill(); +hqShape.cacheAsBitmap = new Matrix(2, 0, 0, 2, 0, 0); + +// scaleX/scaleYが反映される(キャッシュ品質 = Matrix × 自身のスケール × stageスケール) +shape.scaleX = 2; // キャッシュ品質: 1 × 2 × stageScale +shape.scaleY = 2; + +// 親のスケールはキャッシュ品質に影響しない(描画位置には反映される) +const container = new Sprite(); +container.scaleX = 3; +container.scaleY = 3; +container.addChild(shape); + +// ヒットテスト・幅・高さはベクター基準 +const bounds = shape.getBounds(shape); // ベクターの境界を返す + +// キャッシュを解除 +shape.cacheAsBitmap = null; +``` + +### DisplayObjectContainerでのcacheAsBitmap + +`Sprite`や`MovieClip`などの`DisplayObjectContainer`に対しても`cacheAsBitmap`を設定できます。 +コンテナの全子要素をまとめてテクスチャにキャッシュし、以降のフレームではキャッシュされたテクスチャを再利用します。 + +```typescript +const { Shape, Sprite, MovieClip } = next2d.display; +const { Matrix } = next2d.geom; + +// 複数の子要素を持つSprite +const sprite = new Sprite(); +const rect = new Shape(); +rect.graphics.beginFill(0xFF0000).drawRect(0, 0, 100, 80).endFill(); +sprite.addChild(rect); +const circle = new Shape(); +circle.graphics.beginFill(0x00FF00).drawCircle(50, 40, 30).endFill(); +sprite.addChild(circle); + +// コンテナ全体をビットマップキャッシュ +sprite.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + +// MovieClipにも同様に適用可能 +const mc = new MovieClip(); +mc.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); + +// キャッシュを解除(次フレームから通常描画に戻る) +sprite.cacheAsBitmap = null; +``` + +**キャッシュの動作:** +- 先祖や自身のスケールが変更されるとキャッシュが無効化され、次フレームで再生成されます +- 位置(x, y)の変更ではキャッシュは維持され、描画位置のみ更新されます +- スケールが頻繁に変化するアニメーション中はキャッシュが毎フレーム再生成されるため、アニメーション完了後の静的な状態で最も効果を発揮します + +**注意事項:** +- **Matrixにはスケール値のみ設定可能**です(回転・移動は無視されます) +- **Videoには適用できません**(固定サイズの画像データのため) +- キャッシュ中は子要素の変更(追加・削除・プロパティ変更)が画面に反映されません +- `stage.rendererScale`が変更されるとキャッシュが自動的に無効化されます +- `filter`と`cacheAsBitmap`を同時に設定した場合、`cacheAsBitmap`が優先されます + ## 関連項目 - [MovieClip](/ja/reference/player/movie-clip) diff --git a/specs/ja/shape.md b/specs/ja/shape.md index d3d46a17..68aa12c5 100644 --- a/specs/ja/shape.md +++ b/specs/ja/shape.md @@ -231,7 +231,8 @@ stage.addChild(frontShape); ```typescript // 複雑な図形をビットマップとしてキャッシュ -shape.cacheAsBitmap = true; +const { Matrix } = next2d.geom; +shape.cacheAsBitmap = new Matrix(1, 0, 0, 1, 0, 0); ``` ## Graphics クラス diff --git a/src/index.ts b/src/index.ts index 593287b6..dec5f198 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import { Next2D } from "@next2d/core"; if (!("next2d" in window)) { - console.log("%c Next2D Player %c 3.0.5 %c https://next2d.app", + console.log("%c Next2D Player %c 3.1.0 %c https://next2d.app", "color: #fff; background: #5f5f5f", "color: #fff; background: #4bc729", "");