diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b36d3a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.vscode +/node_modules +/_docs +/_types +/module.zip +/script.js +/script.js.map diff --git a/README.md b/README.md index 6370eaf..23fda9b 100644 --- a/README.md +++ b/README.md @@ -5,314 +5,6 @@ # Limits (Foundry VTT Module) -This module allows you to define the maximum range of light, sight, and sound within the scene, drawings, templates, and tiles. +This module allows you to limit the range of sight, light, darkness, and sound within regions. -![config](images/config.png) - -```js -object.document.setFlag("limits", { - light: { - enabled: true, - range: 0, - }, - sight: { - basicSight: { - enabled: true, - range: 0, - }, - seeAll: { - enabled: true, - range: 30, - }, - // ... - }, - sound: { - enabled: false, - range: null, // Infinity - }, -}); -``` - -## Macros - -### D&D 5e - -Recommended modules: - -- [Vision 5e](https://foundryvtt.com/packages/vision-5e) -- [Walled Templates](https://foundryvtt.com/packages/walledtemplates) - -#### Darkness spell - -```js -// === Darkness spell template === - -if (!game.modules.get("limits")?.active) { - ui.notifications.warn("The Limits module is not enabled!"); -} - -const templateData = { - t: CONST.MEASURED_TEMPLATE_TYPES.CIRCLE, - distance: 15, - fillColor: "#000000", - flags: { - limits: { - // Block sight-based senses except for Devil's Sight and Truesight - sight: { - basicSight: { enabled: true, range: 0 }, // Darkvision - ghostlyGaze: { enabled: true, range: 0 }, // Ghostly Gaze (Vision 5e) - lightPerception: { enabled: true, range: 0 }, // Light Perception (Vision 5e) - }, - // Block light - light: { enabled: true, range: 0 }, - }, - }, -}; - -// Walled Templates (optional) -if (game.modules.get("walledtemplates")?.active) { - templateData.flags.walledtemplates = { - wallsBlock: "recurse", // blocked by walls and spreads around corners - wallRestriction: "move", - }; -} - -const template = ( - await new dnd5e.canvas.AbilityTemplate( - new CONFIG.MeasuredTemplate.documentClass(templateData, { - parent: canvas.scene, - }) - ).drawPreview() -).at(0); - -// Sequencer + JB2A Assets (optional) -if ( - game.modules.get("sequencer")?.active && - (game.modules.get("JB2A_DnD5e")?.active || - game.modules.get("jb2a_patreon")?.active) -) { - new Sequence() - .effect() - .persist(true) - .file("jb2a.darkness.black") - .opacity(0.5) - .attachTo(template) - .scaleToObject((template.distance + 2.5) / template.distance) - .xray(true) - .aboveLighting() - .mask(game.modules.get("walledtemplates")?.active ? [template] : []) - .play(); -} -``` - -#### Hunger of Hadar spell - -```js -// === Hunger of Hadar spell template === - -if (!game.modules.get("limits")?.active) { - ui.notifications.warn("The Limits module is not enabled!"); -} - -const templateData = { - t: CONST.MEASURED_TEMPLATE_TYPES.CIRCLE, - distance: 20, - fillColor: "#000000", - flags: { - limits: { - // Block sight-based senses - sight: { - basicSight: { enabled: true, range: 0 }, // Darkvision - devilsSight: { enabled: true, range: 0 }, // Devil's Sight (Vision 5e) - ghostlyGaze: { enabled: true, range: 0 }, // Ghostly Gaze (Vision 5e) - lightPerception: { enabled: true, range: 0 }, // Light Perception (Vision 5e) - seeAll: { enabled: true, range: 0 }, // Truesight - }, - // Block light - light: { enabled: true, range: 0 }, - }, - }, -}; - -// Walled Templates (optional) -if (game.modules.get("walledtemplates")?.active) { - templateData.flags.walledtemplates = { - wallsBlock: "walled", // blocked by walls and does not spread around corners - wallRestriction: "move", - }; -} - -const template = ( - await new dnd5e.canvas.AbilityTemplate( - new CONFIG.MeasuredTemplate.documentClass(templateData, { - parent: canvas.scene, - }) - ).drawPreview() -).at(0); - -// Sequencer + JB2A Assets (optional) -if ( - game.modules.get("sequencer")?.active && - (game.modules.get("JB2A_DnD5e")?.active || - game.modules.get("jb2a_patreon")?.active) -) { - new Sequence() - .effect() - .persist(true) - .file("jb2a.darkness.black") - .opacity(0.5) - .attachTo(template) - .scaleToObject((template.distance + 2.5) / template.distance) - .xray(true) - .aboveLighting() - .mask(game.modules.get("walledtemplates")?.active ? [template] : []) - .zIndex(0) - .effect() - .persist(true) - .file("jb2a.arms_of_hadar.dark_purple") - .opacity(0.5) - .attachTo(template) - .scaleToObject((template.distance + 2.5) / template.distance) - .xray(true) - .aboveLighting() - .mask(game.modules.get("walledtemplates")?.active ? [template] : []) - .zIndex(1) - .play(); -} -``` - -#### Fog Cloud spell - -```js -// === Fog Cloud Spell Template === - -if (!game.modules.get("limits")?.active) { - ui.notifications.warn("The Limits module is not enabled!"); -} - -const spellLevel = 1; -const templateData = { - t: CONST.MEASURED_TEMPLATE_TYPES.CIRCLE, - distance: 20 * spellLevel, - fillColor: "#ffffff", - flags: { - limits: { - // Block sight-based senses - sight: { - basicSight: { enabled: true, range: 0 }, // Darkvision - devilsSight: { enabled: true, range: 0 }, // Devil's Sight (Vision 5e) - ghostlyGaze: { enabled: true, range: 0 }, // Ghostly Gaze (Vision 5e) - lightPerception: { enabled: true, range: 0 }, // Light Perception (Vision 5e) - seeAll: { enabled: true, range: 0 }, // Truesight - }, - }, - }, -}; - -// Walled Templates (optional) -if (game.modules.get("walledtemplates")?.active) { - templateData.flags.walledtemplates = { - wallsBlock: "recurse", // blocked by walls and spreads around corners - wallRestriction: "move", - }; -} - -const template = ( - await new dnd5e.canvas.AbilityTemplate( - new CONFIG.MeasuredTemplate.documentClass(templateData, { - parent: canvas.scene, - }) - ).drawPreview() -).at(0); - -// Sequencer + JB2A Assets (optional) -if ( - game.modules.get("sequencer")?.active && - (game.modules.get("JB2A_DnD5e")?.active || - game.modules.get("jb2a_patreon")?.active) -) { - new Sequence() - .effect() - .persist(true) - .file("jb2a.fog_cloud.01.white") - .opacity(0.5) - .attachTo(template) - .scaleToObject((template.distance + 2.5) / template.distance) - .xray(true) - .aboveLighting() - .mask(game.modules.get("walledtemplates")?.active ? [template] : []) - .play(); -} -``` - -#### Silence spell - -```js -// === Silence spell template === - -if (!game.modules.get("limits")?.active) { - ui.notifications.warn("The Limits module is not enabled!"); -} - -const templateData = { - t: CONST.MEASURED_TEMPLATE_TYPES.CIRCLE, - distance: 20, - fillColor: "#7fffff", - flags: { - limits: { - // Block hearing - sight: { - hearing: { enabled: true, range: 0 }, // Hearing (Vision 5e) - }, - // Block sound - sound: { enabled: true, range: 0 }, - }, - }, -}; - -// Walled Templates (optional) -if (game.modules.get("walledtemplates")?.active) { - templateData.flags.walledtemplates = { - wallsBlock: "walled", // blocked by walls and does not spread around corners - wallRestriction: "move", - }; -} - -const template = ( - await new dnd5e.canvas.AbilityTemplate( - new CONFIG.MeasuredTemplate.documentClass(templateData, { - parent: canvas.scene, - }) - ).drawPreview() -).at(0); - -// Sequencer + JB2A Assets (optional) -if ( - game.modules.get("sequencer")?.active && - (game.modules.get("JB2A_DnD5e")?.active || - game.modules.get("jb2a_patreon")?.active) -) { - new Sequence() - .effect() - .persist(true) - .file("jb2a.energy_field.02.below.blue") - .opacity(0.25) - .attachTo(template) - .scaleToObject((template.distance + 2.5) / template.distance) - .xray(true) - .aboveLighting() - .mask(game.modules.get("walledtemplates")?.active ? [template] : []) - .zIndex(0) - .effect() - .persist(true) - .file("jb2a.energy_field.02.above.blue") - .opacity(0.25) - .attachTo(template) - .scaleToObject((template.distance + 2.5) / template.distance) - .xray(true) - .aboveLighting() - .mask(game.modules.get("walledtemplates")?.active ? [template] : []) - .zIndex(1) - .play(); -} -``` + diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..d782d58 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,61 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import stylistic from "@stylistic/eslint-plugin"; + +export default [ + { + ignores: ["_docs", "_types", "script.js"], + }, + pluginJs.configs.recommended, + stylistic.configs.customize({ + indent: 4, + quotes: "double", + semi: true, + jsx: false, + arrowParens: "always", + braceStyle: "1tbs", + blockSpacing: true, + quoteProps: "consistent-as-needed", + commaDangle: "always-multiline", + }), + { + languageOptions: { + ecmaVersion: "latest", + globals: { + ...globals.browser, + canvas: "readonly", + CONFIG: "readonly", + CONST: "readonly", + DetectionMode: "readonly", + foundry: "readonly", + game: "readonly", + Hooks: "readonly", + libWrapper: "readonly", + PIXI: "readonly", + }, + }, + rules: { + "no-unused-vars": ["error", { + vars: "all", + args: "none", + argsIgnorePattern: "^_", + caughtErrors: "all", + caughtErrorsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + ignoreRestSiblings: false, + reportUsedIgnorePattern: false, + }], + "@stylistic/padding-line-between-statements": [ + "error", + { blankLine: "always", prev: "*", next: ["block-like", "break", "class", "continue", "function", "return"] }, + { blankLine: "always", prev: ["block-like", "class", "function"], next: "*" }, + { blankLine: "always", prev: "expression", next: ["const", "let", "var"] }, + { blankLine: "always", prev: ["const", "let", "var"], next: "expression" }, + { blankLine: "never", prev: ["break", "continue", "return"], next: "*" }, + { blankLine: "never", prev: "*", next: "case" }, + ], + "@stylistic/no-mixed-operators": "off", + "@stylistic/no-multiple-empty-lines": ["error", { max: 1, maxBOF: 0, maxEOF: 0 }], + }, + }, +]; diff --git a/images/config.png b/images/config.png index 19b8779..4f9486d 100644 Binary files a/images/config.png and b/images/config.png differ diff --git a/images/demo.png b/images/demo.png new file mode 100644 index 0000000..9691fb9 Binary files /dev/null and b/images/demo.png differ diff --git a/lang/en.json b/lang/en.json index b6a7f1e..efa885d 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,5 +1,53 @@ { - "LIMITS.ConfigureLimits": "Configure Limits", - "LIMITS.LimitsLabel": "Limits", - "LIMITS.LimitsHint": "Configure the maximum range of light, sight, and sound." + "LIMITS": { + "label": "Limit Range", + "FIELDS": { + "sight": { + "label": "Sight", + "hint": "Configure the detection modes that this limit applies to." + }, + "light": { + "label": "Light", + "hint": "Configure whether this limit applies to light sources." + }, + "darkness": { + "label": "Darkness", + "hint": "Configure whether this limit applies to darkness sources." + }, + "sound": { + "label": "Sound", + "hint": "Configure whether this limit applies to sound sources." + }, + "range": { + "label": "Range", + "hint": "Configure the range of this limit." + }, + "mode": { + "label": "Mode", + "hint": "Configure how the range of this limit affects the effective range." + }, + "priority": { + "label": "Priority", + "hint": "Configure the order in which limits are applied to determine the effective range." + } + }, + "MODES": { + "STACK": { + "label": "Stack", + "hint": "The effective range is reduced further by this limit." + }, + "UPGRADE": { + "label": "Upgrade", + "hint": "The effective range is increased to the range of this limit." + }, + "DOWNGRADE": { + "label": "Downgrade", + "hint": "The effective range is decreased to the range of this limit." + }, + "OVERRIDE": { + "label": "Override", + "hint": "The effective range is overridden by the range of this limit." + } + } + } } diff --git a/module.json b/module.json index bdcdd2c..5795c2f 100644 --- a/module.json +++ b/module.json @@ -1,44 +1,41 @@ { - "id": "limits", - "title": "Limits", - "description": "Light, sight, and sound limits.", - "authors": [ - { - "name": "dev7355608", - "email": "dev7355608@gmail.com" - } - ], - "version": "1.1.0", - "compatibility": { - "minimum": "11", - "verified": "11" - }, - "esmodules": [ - "scripts/index.js" - ], - "styles": [ - "styles/config.css" - ], - "languages": [ - { - "lang": "en", - "name": "English", - "path": "lang/en.json" - } - ], - "relationships": { - "requires": [ - { - "id": "lib-wrapper", - "type": "module" - } - ] - }, - "url": "https://github.com/dev7355608/limits", - "manifest": "https://github.com/dev7355608/limits/releases/latest/download/module.json", - "download": "https://github.com/dev7355608/limits/releases/download/v1.1.0/module.zip", - "changelog": "https://github.com/dev7355608/limits/releases/tag/v1.1.0", - "bugs": "https://github.com/dev7355608/limits/issues", - "readme": "https://raw.githubusercontent.com/dev7355608/limits/main/README.md", - "license": "https://raw.githubusercontent.com/dev7355608/limits/main/LICENSE" + "id": "limits", + "title": "Limits", + "description": "Limit the range of sight, light, darkness, and sound within regions.", + "authors": [ + { + "name": "dev7355608", + "email": "dev7355608@gmail.com" + } + ], + "version": "2.0.0", + "compatibility": { + "minimum": "12", + "verified": "12" + }, + "documentTypes": { + "RegionBehavior": { + "limitRange": {} + } + }, + "scripts": [ + "script.js" + ], + "styles": [ + "style.css" + ], + "languages": [ + { + "lang": "en", + "name": "English", + "path": "lang/en.json" + } + ], + "url": "https://github.com/dev7355608/limits", + "manifest": "https://github.com/dev7355608/limits/releases/latest/download/module.json", + "download": "https://github.com/dev7355608/limits/releases/download/v2.0.0/module.zip", + "changelog": "https://github.com/dev7355608/limits/releases/tag/v2.0.0", + "bugs": "https://github.com/dev7355608/limits/issues", + "readme": "https://raw.githubusercontent.com/dev7355608/limits/main/README.md", + "license": "https://raw.githubusercontent.com/dev7355608/limits/main/LICENSE" } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..421dee9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3588 @@ +{ + "name": "limits", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "limits", + "license": "MIT", + "devDependencies": { + "@eslint/js": "^9.12.0", + "@rollup/plugin-terser": "^0.4.4", + "@stylistic/eslint-plugin": "^2.9.0", + "archiver": "^7.0.1", + "eslint": "^9.12.0", + "globals": "^15.11.0", + "rimraf": "^6.0.1", + "rollup": "^4.24.0", + "typedoc": "^0.26.9" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/core": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", + "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.12.0.tgz", + "integrity": "sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", + "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", + "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", + "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.0", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.22.0.tgz", + "integrity": "sha512-S8sMe4q71TJAW+qG93s5VaiihujRK6rqDFqBnxqvga/3LvqHEnxqBIOPkt//IdXVtHkQWKu4nOQNk0uBGicU7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-javascript": "1.22.0", + "@shikijs/engine-oniguruma": "1.22.0", + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.3" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.22.0.tgz", + "integrity": "sha512-AeEtF4Gcck2dwBqCFUKYfsCq0s+eEbCEbkUuFou53NZ0sTGnJnJ/05KHQFZxpii5HMXbocV9URYVowOP2wH5kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", + "oniguruma-to-js": "0.4.3" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.22.0.tgz", + "integrity": "sha512-5iBVjhu/DYs1HB0BKsRRFipRrD7rqjxlWTj4F2Pf+nQSPqc3kcyqFFeZXnBMzDf0HdqaFVvhDRAGiYNvyLP+Mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0" + } + }, + "node_modules/@shikijs/types": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.22.0.tgz", + "integrity": "sha512-Fw/Nr7FGFhlQqHfxzZY8Cwtwk5E9nKDUgeLjZgt3UuhcM3yJR9xj3ZGNravZZok8XmEZMiYkSMTPlPkULB8nww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz", + "integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stylistic/eslint-plugin": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.9.0.tgz", + "integrity": "sha512-OrDyFAYjBT61122MIY1a3SfEgy3YCMgt2vL4eoPmvTwDBwyQhAXurxNQznlRD/jESNfYWfID8Ej+31LljvF7Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.8.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "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/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz", + "integrity": "sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.1.tgz", + "integrity": "sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz", + "integrity": "sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.1.tgz", + "integrity": "sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz", + "integrity": "sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/archiver-utils/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/archiver-utils/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/b4a": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", + "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz", + "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "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", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz", + "integrity": "sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.6.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.12.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz", + "integrity": "sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz", + "integrity": "sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "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": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "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/jackspeak": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "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/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.1.tgz", + "integrity": "sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/oniguruma-to-js": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/oniguruma-to-js/-/oniguruma-to-js-0.4.3.tgz", + "integrity": "sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "regex": "^4.3.2" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "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/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true, + "license": "MIT" + }, + "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/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/regex": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/regex/-/regex-4.3.3.tgz", + "integrity": "sha512-r/AadFO7owAq1QJVeZ/nq9jNS1vyZt+6t1p/E59B56Rn2GCya+gr1KSyOzNL/er+r+B7phv5jG2xU2Nz1YkmJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "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", + "dependencies": { + "queue-microtask": "^1.2.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/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shiki": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.22.0.tgz", + "integrity": "sha512-/t5LlhNs+UOKQCYBtl5ZsH/Vclz73GIqT2yQsCBygr8L/ppTdmpL4w3kPLoZJbMKVWtoG77Ue1feOjZfDxvMkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.22.0", + "@shikijs/engine-javascript": "1.22.0", + "@shikijs/engine-oniguruma": "1.22.0", + "@shikijs/types": "1.22.0", + "@shikijs/vscode-textmate": "^9.3.0", + "@types/hast": "^3.0.4" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "dev": true, + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/streamx": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz", + "integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/terser": { + "version": "5.34.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz", + "integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/text-decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.0.tgz", + "integrity": "sha512-n1yg1mOj9DNpk3NeZOx7T6jchTbyJS3i3cucbNN6FcdPriMZx7NsgrGpWWdWZZGxD7ES1XB+3uoqHMgOKaN+fg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typedoc": { + "version": "0.26.9", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.26.9.tgz", + "integrity": "sha512-Rc7QpWL7EtmrT8yxV0GmhOR6xHgFnnhphbD9Suti3fz3um7ZOrou6q/g9d6+zC5PssTLZmjaW4Upmzv8T1rCcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "shiki": "^1.16.2", + "yaml": "^2.5.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7bd9290 --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "limits", + "description": "Limits (Foundry VTT Module)", + "author": { + "name": "dev7355608", + "email": "dev7355608@gmail.com", + "url": "https://github.com/dev7355608" + }, + "license": "MIT", + "homepage": "https://github.com/dev7355608/limits", + "bugs": { + "url": "https://github.com/dev7355608/limits/issues", + "email": "dev7355608@gmail.com" + }, + "private": true, + "scripts": { + "build": "rimraf module.zip script.js script.js.map && rollup -c", + "clean": "rimraf _docs _types module.zip script.js script.js.map", + "docs": "rimraf _docs && typedoc", + "lint": "eslint", + "lint:fix": "eslint --fix", + "types": "rimraf _types && tsc", + "watch": "rollup -c -w --environment BUILD:development" + }, + "devDependencies": { + "@eslint/js": "^9.12.0", + "@rollup/plugin-terser": "^0.4.4", + "@stylistic/eslint-plugin": "^2.9.0", + "archiver": "^7.0.1", + "eslint": "^9.12.0", + "globals": "^15.11.0", + "rimraf": "^6.0.1", + "rollup": "^4.24.0", + "typedoc": "^0.26.9" + } +} diff --git a/rollup.config.mjs b/rollup.config.mjs new file mode 100644 index 0000000..b00739c --- /dev/null +++ b/rollup.config.mjs @@ -0,0 +1,78 @@ +import archiver from "archiver"; +import fs from "fs"; +import process from "process"; +import terser from "@rollup/plugin-terser"; + +const isDevelopment = process.env.BUILD === "development"; + +export default { + input: "scripts/_index.mjs", + output: { + file: "script.js", + format: "iife", + sourcemap: true, + generatedCode: "es2015", + plugins: [terser({ + ecma: 2023, + compress: { + booleans: false, + comparisons: true, + conditionals: false, + drop_console: isDevelopment ? false : ["assert"], + drop_debugger: !isDevelopment, + ecma: 2023, + join_vars: !isDevelopment, + keep_classnames: true, + keep_fargs: true, + keep_fnames: isDevelopment, + keep_infinity: true, + lhs_constants: !isDevelopment, + passes: 2, + sequences: false, + typeofs: false, + }, + mangle: isDevelopment ? false : { keep_classnames: true, keep_fnames: false }, + format: { + ascii_only: true, + beautify: isDevelopment, + comments: false, + keep_numbers: true, + }, + keep_classnames: true, + keep_fnames: isDevelopment, + })], + }, + plugins: [{ + closeBundle() { + if (isDevelopment) { + return; + } + + const start = Date.now(); + const output = fs.createWriteStream("module.zip"); + const archive = archiver("zip", { zlib: { level: 9 } }); + + output.on("close", function () { + console.log(`\x1b[32mcreated \x1b[1mmodule.zip\x1b[0m\x1b[32m in \x1b[1m${Date.now() - start}ms\x1b[0m`); + }); + + archive.on("warning", function (error) { + throw error; + }); + + archive.on("error", function (error) { + throw error; + }); + + archive.pipe(output); + + for (const name of ["module.json", "script.js", "script.js.map", "style.css", "LICENSE"]) { + archive.append(fs.createReadStream(name), { name }); + } + + archive.directory("lang", "lang"); + + archive.finalize(); + }, + }], +}; diff --git a/scripts/_index.mjs b/scripts/_index.mjs new file mode 100644 index 0000000..b9fb3e3 --- /dev/null +++ b/scripts/_index.mjs @@ -0,0 +1,39 @@ +import { PointDarknessSourceMixin, PointLightSourceMixin, PointSoundSourceMixin, PointVisionSourceMixin } from "./canvas/sources/_module.mjs"; +import LimitRangeRegionBehaviorType from "./data/region-behavior.mjs"; + +Hooks.once("init", () => { + const type = "limits.limitRange"; + + CONFIG.RegionBehavior.dataModels[type] = LimitRangeRegionBehaviorType; + CONFIG.RegionBehavior.typeIcons[type] = "fa-solid fa-eye-low-vision"; + CONFIG.RegionBehavior.typeLabels[type] = "LIMITS.label"; + + Hooks.once("setup", () => { + Hooks.once("canvasInit", () => { + CONFIG.Canvas.visionSourceClass = PointVisionSourceMixin(CONFIG.Canvas.visionSourceClass); + CONFIG.Canvas.lightSourceClass = PointLightSourceMixin(CONFIG.Canvas.lightSourceClass); + CONFIG.Canvas.darknessSourceClass = PointDarknessSourceMixin(CONFIG.Canvas.darknessSourceClass); + CONFIG.Canvas.soundSourceClass = PointSoundSourceMixin(CONFIG.Canvas.soundSourceClass); + + if (game.modules.get("lib-wrapper")?.active) { + libWrapper.register( + "limits", + "DetectionMode.prototype._testPoint", + function (wrapped, visionSource, mode, target, test) { + return wrapped(visionSource, mode, target, test) + && visionSource._testLimit(mode, test.point, test.elevation); + }, + libWrapper.WRAPPER, + { perf_mode: libWrapper.PERF_FAST }, + ); + } else { + const testPoint = DetectionMode.prototype._testPoint; + + DetectionMode.prototype._testPoint = function (visionSource, mode, target, test) { + return testPoint.call(this, visionSource, mode, target, test) + && visionSource._testLimit(mode, test.point, test.elevation); + }; + } + }); + }); +}); diff --git a/scripts/_module.mjs b/scripts/_module.mjs new file mode 100644 index 0000000..adda2a6 --- /dev/null +++ b/scripts/_module.mjs @@ -0,0 +1,6 @@ +export { default as Limits } from "./limits.mjs"; + +export * as canvas from "./canvas/_module.mjs"; +export * as const from "./const.mjs"; +export * as data from "./data/_module.mjs"; +export * as raycast from "./raycast/_module.mjs"; diff --git a/scripts/canvas/_module.mjs b/scripts/canvas/_module.mjs new file mode 100644 index 0000000..2f6b687 --- /dev/null +++ b/scripts/canvas/_module.mjs @@ -0,0 +1,2 @@ +export * as geometry from "./geometry/_module.mjs"; +export * as sources from "./sources/_module.mjs"; diff --git a/scripts/canvas/geometry/_module.mjs b/scripts/canvas/geometry/_module.mjs new file mode 100644 index 0000000..aaff600 --- /dev/null +++ b/scripts/canvas/geometry/_module.mjs @@ -0,0 +1,2 @@ +export { default as PointSourcePolygonConstraint } from "./constraint.mjs"; +export { default as computeQuadrantBounds } from "./quadrants.mjs"; diff --git a/scripts/canvas/geometry/constraint.mjs b/scripts/canvas/geometry/constraint.mjs new file mode 100644 index 0000000..1798ed5 --- /dev/null +++ b/scripts/canvas/geometry/constraint.mjs @@ -0,0 +1,541 @@ +import * as raycast from "../../raycast/_module.mjs"; +import computeQuadrantBounds from "./quadrants.mjs"; + +/** + * The constraint for a polygon given a space. + */ +export default class PointSourcePolygonConstraint extends PIXI.Polygon { + /** + * Apply the constraint given by the space to the polygon. + * @overload + * @param {foundry.canvas.geometry.shapes.PointSourcePolygon} polygon - The polygon that is to be constrained. + * @param {raycast.Space} space - The space. + * @returns {boolean} Was the polygon constrained? + */ + /** + * Apply the constraint given by the space to the polygon. + * @overload + * @param {foundry.canvas.geometry.shapes.PointSourcePolygon} polygon - The polygon that is to be constrained. + * @param {raycast.Space} space - The space. + * @param {boolean} clone - Clone before constraining? + * @returns {foundry.canvas.geometry.shapes.PointSourcePolygon} The constrained polygon. + */ + static apply(polygon, space, clone) { + const constraint = new this(polygon, space); + + if (!constraint.isEnveloping) { + const intersection = constraint.intersectPolygon(polygon, { scalingFactor: 100 }); + + if (clone) { + const origin = polygon.origin; + const config = { ...polygon.config, boundaryShapes: [...polygon.config.boundaryShapes] }; + + polygon = new polygon.constructor(); + polygon.origin = origin; + polygon.config = config; + } + + polygon.points = intersection.points; + polygon.bounds = polygon.getBounds(); + } + + polygon.config.boundaryShapes.push(constraint); + + return clone === undefined ? !constraint.isEnveloping : polygon; + } + + /** + * @param {foundry.canvas.geometry.shapes.PointSourcePolygon} polygon - The polygon that the constraint is computed for. + * @param {raycast.Space} space - The space. + * @protected + */ + constructor(polygon, space) { + super(); + + const { x: originX, y: originY } = this.#origin = polygon.origin; + const elevation = polygon.config.source?.elevation ?? 0.0; + const originZ = elevation * canvas.dimensions.distancePixels; + const externalRadius = this.#externalRadius = polygon.config.externalRadius; + const { left: minX, right: maxX, top: minY, bottom: maxY } = this.#sourceBounds = polygon.bounds; + const { minDistance, maxDistance } = this.#space0 = space.crop(minX, minY, originZ, maxX, maxY, originZ); + + if (minDistance === maxDistance) { + const maxRadius = externalRadius + maxDistance; + + if (maxRadius < polygon.config.radius) { + this.#addCircleSegment(maxRadius, 0.0); + this.#addCircleSegment(maxRadius, Math.PI * 0.5); + this.#addCircleSegment(maxRadius, Math.PI); + this.#addCircleSegment(maxRadius, Math.PI * 1.5); + } + } else { + this.#quadrantBounds = computeQuadrantBounds(originX, originY, polygon.points); + + const ray = raycast.Ray.create() + .setOrigin(originX, originY, originZ) + .setRange(externalRadius, polygon.config.radius); + + this.#computePoints(ray); + } + + if (this.#enveloping) { + this.points.length = 0; + this.points.push( + minX, minY, + maxX, minY, + maxX, maxY, + minX, maxY, + ); + } else { + this.#closePoints(); + } + } + + /** @type {{ x: number, y: number }} */ + #origin; + + /** @type {number} */ + #externalRadius; + + /** @type {PIXI.Rectangle} */ + #sourceBounds; + + /** @type {[x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number] | null} */ + #quadrantBounds = null; + + /** @type {raycast.Space | null} */ + #space0 = null; + + /** @type {raycast.Space | null} */ + #space1 = null; + + /** @type {raycast.Space | null} */ + #space2 = null; + + /** @type {raycast.Space | null} */ + #space3 = null; + + /** @type {raycast.Space | null} */ + #space4 = null; + + /** @type {boolean} */ + #enveloping = true; + + /** + * Is the constraint enveloping the polygon it was computed for? + * @type {boolean} + */ + get isEnveloping() { + return this.#enveloping; + } + + /** @type {true} */ + get isPositive() { + return true; + } + + /** + * Compute the constraint. + * @param {raycast.Ray} ray + */ + #computePoints(ray) { + const { x, y } = this.#origin; + let [x0, y0, x1, y1, x2, y2, x3, y3] = this.#quadrantBounds; + + if (x < x0 && y < y0) { + const space1 = this.#space1 = this.#space0.crop(x, y, -Infinity, x0, y0, Infinity); + const { minDistance, maxDistance } = space1; + const maxRadius = this.#externalRadius + maxDistance; + + if (minDistance === maxDistance) { + if (maxRadius < Math.hypot(x0 - x, y0 - y)) { + this.#addCircleSegment(maxRadius, 0.0); + } else { + this.#addPoint(x0, y); + this.points.push(x0, y0, x, y0); + } + } else { + x0 = Math.min(x0, x + maxRadius); + y0 = Math.min(y0, y + maxRadius); + + ray.setSpace(space1); + this.#castRays(ray, x0, y, x0, y0); + this.#castRays(ray, x0, y0, x, y0); + } + } else { + this.#addPoint(x0, y); + this.#addPoint(x0, y0); + this.#addPoint(x, y0); + } + + if (x1 < x && y < y1) { + const space2 = this.#space2 = this.#space0.crop(x1, y, -Infinity, x, y1, Infinity); + const { minDistance, maxDistance } = space2; + const maxRadius = this.#externalRadius + maxDistance; + + if (minDistance === maxDistance) { + if (maxRadius < Math.hypot(x - x1, y1 - y)) { + this.#addCircleSegment(maxRadius, Math.PI * 0.5); + } else { + this.#addPoint(x, y1); + this.points.push(x1, y1, x1, y); + } + } else { + x1 = Math.max(x1, x - maxRadius); + y1 = Math.min(y1, y + maxRadius); + + ray.setSpace(space2); + this.#castRays(ray, x, y1, x1, y1); + this.#castRays(ray, x1, y1, x1, y); + } + } else { + this.#addPoint(x, y1); + this.#addPoint(x1, y1); + this.#addPoint(x1, y); + } + + if (x2 < x && y2 < y) { + const space3 = this.#space3 = this.#space0.crop(x2, y2, -Infinity, x, y, Infinity); + const { minDistance, maxDistance } = space3; + const maxRadius = this.#externalRadius + maxDistance; + + if (minDistance === maxDistance) { + if (maxRadius < Math.hypot(x - x2, y - y2)) { + this.#addCircleSegment(maxRadius, Math.PI); + } else { + this.#addPoint(x2, y); + this.points.push(x2, y2, x, y2); + } + } else { + x2 = Math.max(x2, x - maxRadius); + y2 = Math.max(y2, y - maxRadius); + + ray.setSpace(space3); + this.#castRays(ray, x2, y, x2, y2); + this.#castRays(ray, x2, y2, x, y2); + } + } else { + this.#addPoint(x2, y); + this.#addPoint(x2, y2); + this.#addPoint(x, y2); + } + + if (x < x3 && y3 < y) { + const space4 = this.#space4 = this.#space0.crop(x, y3, -Infinity, x3, y, Infinity); + const { minDistance, maxDistance } = space4; + const maxRadius = this.#externalRadius + maxDistance; + + if (minDistance === maxDistance) { + if (maxRadius < Math.hypot(x3 - x, y - y3)) { + this.#addCircleSegment(maxRadius, Math.PI * 1.5); + } else { + this.#addPoint(x, y3); + this.points.push(x3, y3, x3, y); + } + } else { + x3 = Math.min(x3, x + maxRadius); + y3 = Math.max(y3, y - maxRadius); + + ray.setSpace(space4); + this.#castRays(ray, x, y3, x3, y3); + this.#castRays(ray, x3, y3, x3, y); + } + } else { + this.#addPoint(x, y3); + this.#addPoint(x3, y3); + this.#addPoint(x3, y); + } + } + + /** + * Add a circle segment to the constraint. + * @param {number} centerX - The x-coordiante of the origin. + * @param {number} originY - The y-coordinate of the origin. + * @param {number} radius - The radius. + * @param {number} startAngle - The start angle. + */ + #addCircleSegment(radius, startAngle) { + this.#enveloping = false; + + const { x: centerX, y: centerY } = this.#origin; + + if (radius === 0.0) { + this.#addPoint(centerX, centerY); + + return; + } + + this.#addPoint( + centerX + Math.cos(startAngle) * radius, + centerY + Math.sin(startAngle) * radius, + ); + + const deltaAngle = Math.PI * 0.5; + const points = this.points; + + if (radius < canvas.dimensions.maxR) { + const epsilon = 1.0; // PIXI.Circle.approximateVertexDensity + const numSteps = Math.ceil(deltaAngle / Math.sqrt(2.0 * epsilon / radius) - 1e-3); + const angleStep = deltaAngle / numSteps; + + for (let i = 1; i <= numSteps; i++) { + const a = startAngle + angleStep * i; + + points.push( + centerX + Math.cos(a) * radius, + centerY + Math.sin(a) * radius, + ); + } + } else { + const halfDeltaAngle = deltaAngle * 0.5; + const midAngle = startAngle + halfDeltaAngle; + const stopAngle = startAngle + deltaAngle; + const radiusMid = radius / Math.cos(halfDeltaAngle); + + points.push( + centerX + Math.cos(midAngle) * radiusMid, + centerY + Math.sin(midAngle) * radiusMid, + centerX + Math.cos(stopAngle) * radius, + centerY + Math.sin(stopAngle) * radius, + ); + } + } + + /** + * Cast rays in the given quadrant. + * @param {raycast.Ray} ray + * @param {number} c0x + * @param {number} c0y + * @param {number} c1x + * @param {number} c1y + */ + #castRays(ray, c0x, c0y, c1x, c1y) { + const { originX: x, originY: y, originZ: z } = ray; + const precision = canvas.dimensions.size * 0.0825; + const precision2 = precision * precision; + const c0dx = c0x - x; + const c0dy = c0y - y; + const t0 = ray.setTarget(c0x, c0y, z).elapsedTime; + + if (t0 < 1.0) { + this.#enveloping = false; + } + + const r0x = x + t0 * c0dx; + const r0y = y + t0 * c0dy; + + this.#addPoint(r0x, r0y); + + const c1dx = c1x - x; + const c1dy = c1y - y; + const t1 = ray.setTarget(c1x, c1y, z).elapsedTime; + const r1x = x + t1 * c1dx; + const r1y = y + t1 * c1dy; + let cdx = c1x - c0x; + let cdy = c1y - c0y; + const cdd = Math.sqrt(cdx * cdx + cdy * cdy); + + cdx /= cdd; + cdy /= cdd; + + const u0n = cdx * c0dx + cdy * c0dy; + const ndx = c0dx - u0n * cdx; + const ndy = c0dy - u0n * cdy; + let ndd = ndx * ndx + ndy * ndy; + + if (ndd > 1e-6) { + ndd /= Math.sqrt(ndd); + + const pdx = cdx * ndd * 0.5; + const pdy = cdy * ndd * 0.5; + const u1n = cdx * c1dx + cdy * c1dy; + const c0dd = Math.sqrt(c0dx * c0dx + c0dy * c0dy); + const c1dd = Math.sqrt(c1dx * c1dx + c1dy * c1dy); + const fu0 = Math.log((u0n + c0dd) / ndd); // Math.asinh(u0n / ndd) + const fu1 = Math.log((u1n + c1dd) / ndd); // Math.asinh(u1n / ndd) + const dfu = fu1 - fu0; + const fuk = Math.ceil(Math.abs(dfu * (ndd / precision))); // Math.asinh(precision / ndd) + const fud = dfu / fuk; + + const recur = (i0, x0, y0, i2, x2, y2) => { + if (!(i2 - i0 > 1)) { + return; + } + + const dx02 = x0 - x2; + const dy02 = y0 - y2; + const dd02 = dx02 * dx02 + dy02 * dy02; + + if (dd02 <= precision2) { + return; + } + + const i1 = (i0 + i2) >> 1; + let u = Math.exp(fu0 + i1 * fud) - 1.0; + + u += u / (u + 1.0); // Math.sinh(fu0 + i1 * fud) + + const dx = ndx + u * pdx; + const dy = ndy + u * pdy; + const t1 = ray.setTarget(x + dx, y + dy, z).elapsedTime; + const x1 = x + t1 * dx; + const y1 = y + t1 * dy; + + recur(i0, x0, y0, i1, x1, y1); + + if (t1 < 1.0) { + this.#enveloping = false; + } + + this.#addPoint(x1, y1); + + recur(i1, x1, y1, i2, x2, y2); + }; + + recur(0, r0x, r0y, fuk, r1x, r1y); + } + + if (t1 < 1.0) { + this.#enveloping = false; + } + + this.#addPoint(r1x, r1y); + } + + /** + * Add a point to the constraint. + * @param {number} x - The x-coordinate. + * @param {number} y - The y-coordinate. + */ + #addPoint(x, y) { + const points = this.points; + const m = points.length; + + if (m >= 4) { + let x3 = points[m - 4]; + let y3 = points[m - 3]; + let x2 = points[m - 2]; + let y2 = points[m - 1]; + let x1 = x; + let y1 = y; + + if (Math.abs(x1 - x2) > Math.abs(y1 - y2)) { + if ((x1 > x2) !== (x1 < x3)) { + if ((x2 > x1) === (x2 < x3)) { + [x1, y1, x2, y2] = [x2, y2, x1, y1]; + } else { + [x1, y1, x2, y2, x3, y3] = [x3, y3, x1, y1, x2, y2]; + } + } + } else { + if ((y1 > y2) !== (y1 < y3)) { + if ((y2 > y1) === (y2 < y3)) { + [x1, y1, x2, y2] = [x2, y2, x1, y1]; + } else { + [x1, y1, x2, y2, x3, y3] = [x3, y3, x1, y1, x2, y2]; + } + } + } + + const a = y2 - y3; + const b = x3 - x2; + const c = a * (x1 - x2) + b * (y1 - y2); + + if ((c * c) / (a * a + b * b) > 0.0625) { + points.push(x, y); + } else { + const dx = points[m - 4] - x; + const dy = points[m - 3] - y; + + points.length -= 2; + + if (dx * dx + dy * dy > 0.0625) { + points.push(x, y); + } + } + } else if (m === 2) { + const dx = points[m - 2] - x; + const dy = points[m - 1] - y; + + if (dx * dx + dy * dy > 0.0625) { + points.push(x, y); + } + } else { + points.push(x, y); + } + } + + /** + * Close the points of the constraint. + */ + #closePoints() { + const points = this.points; + + if (points.length < 6) { + points.length = 0; + + return; + } + + const [x1, y1, x2, y2] = points; + + this.#addPoint(x1, y1); + this.#addPoint(x2, y2); + + const m = points.length; + + [points[0], points[1], points[2], points[3]] = [points[m - 4], points[m - 3], points[m - 2], points[m - 1]]; + points.length -= 4; + } + + /** + * Visualize the polygon for debugging. + */ + visualize() { + const dg = canvas.controls.debug; + + dg.clear(); + + for (const [i, space] of [this.#space1, this.#space2, this.#space3, this.#space4].entries()) { + if (!space || (space.minDistance < space.maxDistance)) { + continue; + } + + let minX = this.#origin.x; + let minY = this.#origin.y; + let maxX = this.#quadrantBounds[i * 2]; + let maxY = this.#quadrantBounds[i * 2 + 1]; + + if (minX > maxX) { + [minX, maxX] = [maxX, minX]; + } + + if (minY > maxY) { + [minY, maxY] = [maxY, minY]; + } + + dg.lineStyle(0); + dg.beginFill(0x00FF00, 0.2); + dg.drawRect(minX, minY, maxX - minX, maxY - minY); + dg.endFill(); + } + + dg.lineStyle(2, 0x0000FF); + dg.drawPolygon(this.points); + + dg.lineStyle(2, 0xFFFF00, 0.7); + + if (this.#quadrantBounds) { + const { x, y } = this.#origin; + const [q0x, q0y, q1x, q1y, q2x, q2y, q3x, q3y] = this.#quadrantBounds; + + dg.drawPolygon([q0x, q0y, x, q0y, x, q1y, q1x, q1y, q1x, y, q2x, y, q2x, q2y, x, q2y, x, q3y, q3x, q3y, q3x, y, q0x, y]); + } else { + dg.beginFill(0x00FF00, 0.2); + dg.drawShape(this.#sourceBounds); + dg.endFill(); + } + + dg.lineStyle(0); + } +} diff --git a/scripts/canvas/geometry/quadrants.mjs b/scripts/canvas/geometry/quadrants.mjs new file mode 100644 index 0000000..5213b07 --- /dev/null +++ b/scripts/canvas/geometry/quadrants.mjs @@ -0,0 +1,203 @@ +/** + * @param {number} originX - The x-coordinate of the origin. + * @param {number} originY - The y-coordinate of the origin. + * @param {number[]} points - The points of the polygon (`[x0, y0, x1, y1, x2, y2, ...]`). + * @returns {[x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number]} + */ +export default function computeQuadrantBounds(originX, originY, points) { + let q0x = originX; + let q1x = originX; + let q2x = originX; + let q3x = originX; + let q0y = originY; + let q1y = originY; + let q2y = originY; + let q3y = originY; + let x1, y1, q1; + let i = 0; + const m = points.length; + + for (; i < m; i += 2) { + x1 = points[i]; + y1 = points[i + 1]; + + if (y1 > originY) { + q1 = x1 >= originX ? 0 : 1; + + break; + } + + if (y1 < originY) { + q1 = x1 <= originX ? 2 : 3; + + break; + } + + if (x1 !== originX) { + q1 = x1 <= originX ? 1 : 3; + + break; + } + } + + if (i < m) { + const i0 = i = (i + 2) % m; + + for (; ;) { + const x2 = points[i]; + const y2 = points[i + 1]; + let q2; + + if (y2 > originY) { + q2 = x2 >= originX ? 0 : 1; + } else if (y2 < originY) { + q2 = x2 <= originX ? 2 : 3; + } else if (x2 !== originX) { + q2 = x2 <= originX ? 1 : 3; + } else { + q2 = q1; + } + + if (q2 !== q1) { + let s; + + switch (q1) { + case 0: + case 2: + if (x2 !== x1) { + s = (originX - x1) / (x2 - x1); + x1 = originX; + y1 = y1 * (1 - s) + y2 * s; + } else { + s = 0; + x1 = originX; + y1 = originY; + } + + break; + case 1: + case 3: + if (y2 !== y1) { + s = (originY - y1) / (y2 - y1); + x1 = x1 * (1 - s) + x2 * s; + y1 = originY; + } else { + s = 0; + x1 = originX; + y1 = originY; + } + + break; + } + + switch (q1) { + case 0: + if (s !== 0) { + q0x = max(q0x, x1); + q0y = max(q0y, y1); + } + + q1x = min(q1x, x1); + q1y = max(q1y, y1); + + break; + case 1: + if (s !== 0) { + q1x = min(q1x, x1); + q1y = max(q1y, y1); + } + + q2x = min(q2x, x1); + q2y = min(q2y, y1); + + break; + case 2: + if (s !== 0) { + q2x = min(q2x, x1); + q2y = min(q2y, y1); + } + + q3x = max(q3x, x1); + q3y = min(q3y, y1); + + break; + case 3: + if (s !== 0) { + q3x = max(q3x, x1); + q3y = min(q3y, y1); + } + + q0x = max(q0x, x1); + q0y = max(q0y, y1); + + break; + } + + q1 = (q1 + 1) % 4; + } else { + switch (q2) { + case 0: + if (x1 !== originX || x2 !== originX) { + q0x = max(q0x, x2); + q0y = max(q0y, y2); + } + + break; + case 1: + if (y1 !== originY || y2 !== originY) { + q1x = min(q1x, x2); + q1y = max(q1y, y2); + } + + break; + case 2: + if (x1 !== originX || x2 !== originX) { + q2x = min(q2x, x2); + q2y = min(q2y, y2); + } + + break; + case 3: + if (y1 !== originY || y2 !== originY) { + q3x = max(q3x, x2); + q3y = min(q3y, y2); + } + + break; + } + + i = (i + 2) % m; + + if (i === i0) { + break; + } + + x1 = x2; + y1 = y2; + q1 = q2; + } + } + } + + return [q0x, q0y, q1x, q1y, q2x, q2y, q3x, q3y]; +} + +/** + * Minimum. + * @param {number} x + * @param {number} y + * @returns {number} + */ +function min(x, y) { + return x < y ? x : y; +} + +/** + * Maximum. + * @param {number} x + * @param {number} y + * @returns {number} + */ +function max(x, y) { + return x > y ? x : y; +} diff --git a/scripts/canvas/sources/_module.mjs b/scripts/canvas/sources/_module.mjs new file mode 100644 index 0000000..5783913 --- /dev/null +++ b/scripts/canvas/sources/_module.mjs @@ -0,0 +1,5 @@ +export { default as PointSourceRayCaster } from "./caster.mjs"; +export { default as PointDarknessSourceMixin } from "./darkness.mjs"; +export { default as PointLightSourceMixin } from "./light.mjs"; +export { default as PointSoundSourceMixin } from "./sound.mjs"; +export { default as PointVisionSourceMixin } from "./vision.mjs"; diff --git a/scripts/canvas/sources/caster.mjs b/scripts/canvas/sources/caster.mjs new file mode 100644 index 0000000..dee98ac --- /dev/null +++ b/scripts/canvas/sources/caster.mjs @@ -0,0 +1,165 @@ +import * as raycast from "../../raycast/_module.mjs"; + +export default class PointSourceRayCaster { + /** + * @param {raycast.Ray} [ray] - The ray. + */ + constructor(ray) { + /** + * @type {raycast.Ray} + * @readonly + */ + this.ray = ray ?? raycast.Ray.create(); + } + + /** + * @type {raycast.Space} + * @readonly + */ + space = raycast.Space.EMPTY; + + /** + * @type {Readonly<[ + * raycast.Space | null, + * raycast.Space | null, + * raycast.Space | null, + * raycast.Space | null, + * raycast.Space | null, + * raycast.Space | null, + * raycast.Space | null, + * raycast.Space | null + * ]>} + * @readonly + */ + #octants = [null, null, null, null, null, null, null, null]; + + /** + * @type {boolean} + * @readonly + */ + initialized = false; + + /** + * Initialize the caster. + * @param {raycast.Space} space - The space. + * @param {number} originX - The x-coordinate of the origin of the ray. + * @param {number} originY - The y-coordinate of the origin of the ray. + * @param {number} originZ - The z-coordinate of the origin of the ray. + * @param {number} minRange - The minimum range of the ray. + * @param {number} maxRange - The maximum range of the ray. + */ + initialize(space, originX, originY, originZ, minRange, maxRange) { + this.ray.setSpace(raycast.Space.EMPTY).setOrigin(originX, originY, originZ).setRange(minRange, maxRange); + + this.space = space; + + for (let i = 0; i < 8; i++) { + this.#octants[i] = null; + } + + this.initialized = true; + } + + /** + * Reset the caster. + */ + reset() { + this.ray.reset(); + this.space = raycast.Space.EMPTY; + + for (let i = 0; i < 8; i++) { + this.#octants[i] = null; + } + + this.initialized = false; + } + + /** + * Cast the ray. + * @param {number} targetX - The x-coordinate of the target of the ray. + * @param {number} targetY - The y-coordinate of the target of the ray. + * @param {number} targetZ - The z-coordinate of the target of the ray. + * @returns {raycast.Ray} The ray of this instance. + */ + castRay(targetX, targetY, targetZ) { + return this.ray.setSpace(this.#getOctant(targetX, targetY, targetZ)).setTarget(targetX, targetY, targetZ); + } + + /** + * Get the octant. + * @param {number} targetX - The x-coordinate of the target of the ray. + * @param {number} targetY - The y-coordinate of the target of the ray. + * @param {number} targetZ - The z-coordinate of the target of the ray. + * @returns {raycast.Space} The octant. + */ + #getOctant(targetX, targetY, targetZ) { + const { originX, originY, originZ } = this.ray; + const index = (originX < targetX ? 1 : 0) | (originY < targetY ? 2 : 0) | (originZ < targetZ ? 4 : 0); + let octant = this.#octants[index]; + + if (!octant) { + let minX = originX; + let maxX = originX; + let minY = originY; + let maxY = originY; + let minZ = originZ; + let maxZ = originZ; + + switch (index) { + case 0: + minX = -Infinity; + minY = -Infinity; + minZ = -Infinity; + + break; + case 1: + maxX = Infinity; + minY = -Infinity; + minZ = -Infinity; + + break; + case 2: + minX = -Infinity; + maxY = Infinity; + minZ = -Infinity; + + break; + case 3: + maxX = Infinity; + maxY = Infinity; + minZ = -Infinity; + + break; + case 4: + minX = -Infinity; + minY = -Infinity; + maxZ = Infinity; + + break; + case 5: + maxX = Infinity; + minY = -Infinity; + maxZ = Infinity; + + break; + case 6: + minX = -Infinity; + maxY = Infinity; + maxZ = Infinity; + + break; + case 7: + maxX = Infinity; + maxY = Infinity; + maxZ = Infinity; + + break; + } + + octant = this.space.crop(minX, minY, minZ, maxX, maxY, maxZ); + this.#octants[index] = octant; + } + + return octant; + } +} diff --git a/scripts/canvas/sources/darkness.mjs b/scripts/canvas/sources/darkness.mjs new file mode 100644 index 0000000..b11a16f --- /dev/null +++ b/scripts/canvas/sources/darkness.mjs @@ -0,0 +1,27 @@ +import Limits from "../../limits.mjs"; +import PointSourcePolygonConstraint from "../geometry/constraint.mjs"; + +/** + * @param {typeof foundry.canvas.sources.PointDarknessSource} PointDarknessSource + * @returns {typeof foundry.canvas.sources.PointDarknessSource} + */ +export const PointDarknessSourceMixin = (PointDarknessSource) => class extends PointDarknessSource { + /** @override */ + _createShapes() { + super._createShapes(); + + if (this._visualShape) { + if (PointSourcePolygonConstraint.apply(this._visualShape, Limits.darkness)) { + const { x, y, radius } = this.data; + const circle = new PIXI.Circle(x, y, radius); + const density = PIXI.Circle.approximateVertexDensity(radius); + + this.shape = this._visualShape.applyConstraint(circle, { density, scalingFactor: 100 }); + } + } else { + PointSourcePolygonConstraint.apply(this.shape, Limits.darkness); + } + } +}; + +export default PointDarknessSourceMixin; diff --git a/scripts/canvas/sources/light.mjs b/scripts/canvas/sources/light.mjs new file mode 100644 index 0000000..3bcf2fb --- /dev/null +++ b/scripts/canvas/sources/light.mjs @@ -0,0 +1,17 @@ +import Limits from "../../limits.mjs"; +import PointSourcePolygonConstraint from "../geometry/constraint.mjs"; + +/** + * @param {typeof foundry.canvas.sources.PointLightSource} PointLightSource + * @returns {typeof foundry.canvas.sources.PointLightSource} + */ +export const PointLightSourceMixin = (PointLightSource) => class extends PointLightSource { + /** @override */ + _createShapes() { + super._createShapes(); + + PointSourcePolygonConstraint.apply(this.shape, Limits.light); + } +}; + +export default PointLightSourceMixin; diff --git a/scripts/canvas/sources/sound.mjs b/scripts/canvas/sources/sound.mjs new file mode 100644 index 0000000..96f3b0e --- /dev/null +++ b/scripts/canvas/sources/sound.mjs @@ -0,0 +1,42 @@ +import Limits from "../../limits.mjs"; +import PointSourcePolygonConstraint from "../geometry/constraint.mjs"; +import PointSourceRayCaster from "./caster.mjs"; + +/** + * @param {typeof foundry.canvas.sources.PointSoundSource} PointSoundSource + * @returns {typeof foundry.canvas.sources.PointSoundSource} + */ +export const PointSoundSourceMixin = (PointSoundSource) => class extends PointSoundSource { + /** + * @type {PointSourceRayCaster} + * @readonly + */ + #caster = new PointSourceRayCaster(); + + /** @override */ + _createShapes() { + super._createShapes(); + + PointSourcePolygonConstraint.apply(this.shape, Limits.sound); + + const { x, y, elevation } = this.data; + const z = elevation * canvas.dimensions.distancePixels; + const { left: minX, right: maxX, top: minY, bottom: maxY } = this.shape.bounds; + const space = Limits.sound.crop(minX, minY, z, maxX, maxY, z); + + this.#caster.initialize(space, x, y, z, 0.0, Infinity); + } + + /** @override */ + getVolumeMultiplier(listener, options) { + let volume = super.getVolumeMultiplier(listener, options); + + if (volume > 0.0) { + volume *= this.#caster.castRay(listener.x, listener.y, this.#caster.ray.originZ).remainingEnergy; + } + + return volume; + } +}; + +export default PointSoundSourceMixin; diff --git a/scripts/canvas/sources/vision.mjs b/scripts/canvas/sources/vision.mjs new file mode 100644 index 0000000..f05efd4 --- /dev/null +++ b/scripts/canvas/sources/vision.mjs @@ -0,0 +1,70 @@ +import Limits from "../../limits.mjs"; +import * as raycast from "../../raycast/_module.mjs"; +import PointSourcePolygonConstraint from "../geometry/constraint.mjs"; +import PointSourceRayCaster from "./caster.mjs"; + +/** + * @param {typeof foundry.canvas.sources.PointVisionSource} PointVisionSource + * @returns {typeof foundry.canvas.sources.PointVisionSource} + */ +export const PointVisionSourceMixin = (PointVisionSource) => class extends PointVisionSource { + /** + * @type {{ [mode: string]: PointSourceRayCaster }}} + * @readonly + */ + #casters = ((ray) => Object.fromEntries(Object.keys(Limits.sight).map((mode) => [mode, new PointSourceRayCaster(ray)])))(raycast.Ray.create()); + + /** @override */ + _createShapes() { + super._createShapes(); + + this.shape = PointSourcePolygonConstraint.apply(this.shape, Limits.sight[this.data.detectionMode ?? "basicSight"], this.shape === this.los); + this.light = PointSourcePolygonConstraint.apply(this.light, Limits.sight.lightPerception, this.light === this.los); + + for (const mode in this.#casters) { + this.#casters[mode].reset(); + } + } + + /** + * Test whether the ray hits the target. + * @param {foundry.types.TokenDetectionMode} - The detection mode data. + * @param {{ x: number, y: number }} point - The target point. + * @param {number} elevation - The target elevation. + * @returns {boolean} Does the ray hit the target? + * @internal + */ + _testLimit(mode, point, elevation) { + const caster = this.#casters[mode.id]; + + if (!caster.initialized) { + const { x, y, elevation, externalRadius } = this.data; + const z = elevation * canvas.dimensions.distancePixels; + const radius = this.object.getLightRadius(mode.range); + let minX = x - radius; + let minY = y - radius; + let maxX = x + radius; + let maxY = y + radius; + let bounds; + + if (mode.id === "lightPerception") { + bounds = this.los.bounds; + } else { + bounds = this.los.config.useInnerBounds ? canvas.dimensions.sceneRect : canvas.dimensions.rect; + } + + minX = Math.max(minX, bounds.left); + minY = Math.max(minY, bounds.top); + maxX = Math.min(maxX, bounds.right); + maxY = Math.min(maxY, bounds.bottom); + + const space = Limits.sight[mode.id].crop(minX, minY, -Infinity, maxX, maxY, Infinity); + + caster.initialize(space, x, y, z, externalRadius, Infinity); + } + + return caster.castRay(point.x, point.y, elevation * canvas.dimensions.distancePixels).targetHit; + } +}; + +export default PointVisionSourceMixin; diff --git a/scripts/config.mjs b/scripts/config.mjs deleted file mode 100644 index fbefff7..0000000 --- a/scripts/config.mjs +++ /dev/null @@ -1,230 +0,0 @@ -import { MODULE_ID } from "./const.mjs"; -import { VolumeData } from "./data/models.mjs"; - -export class LimitsConfig extends DocumentSheet { - /** @override */ - static _getInheritanceChain() { - return []; - } - - /** @override */ - static name = "LimitsConfig"; - - /** @override */ - static get defaultOptions() { - return foundry.utils.mergeObject(super.defaultOptions, { - classes: ["sheet", "token-sheet"], - id: `${MODULE_ID}-config`, - template: `modules/${MODULE_ID}/templates/config.hbs`, - width: 440, - height: "auto" - }); - } - - /** - * @param {DocumentSheet} sheet - * @param {string[]} selectors - * @param {string} position - * @param {(html: string) => string} [wrap] - */ - static inject(sheet, selectors, position, wrap = (s) => s) { - let element; - - for (const selector of selectors) { - element = element ? element.closest(selector) : sheet.element[0].querySelector(selector); - } - - element.insertAdjacentHTML(position, wrap(`\ -
- -
- -
-

${game.i18n.localize("LIMITS.LimitsHint")}

-
`)); - sheet.form.querySelector(`button[name="flags.${MODULE_ID}"]`) - .addEventListener("click", event => { - event.preventDefault(); - - new LimitsConfig(sheet.object).render(true); - }); - - sheet.options.height = "auto"; - sheet.position.height = "auto"; - sheet.setPosition(sheet.position); - } - - /** @override */ - get id() { - return `${this.constructor.name}-${this.document.uuid.replace(/\./g, "-")}`; - } - - /** @override */ - get title() { - const name = this.document.name || game.i18n.localize(this.document.constructor.metadata.label); - - return `${game.i18n.localize("LIMITS.ConfigureLimits")}: ${name}`; - } - - /** @override */ - getData(options) { - const baseData = {}; - const data = foundry.utils.mergeObject(baseData, super.getData(options)); - - if (!this._sight) { - const limits = new VolumeData(data.data.flags[MODULE_ID] ?? {}); - - this._sight = []; - - for (const mode of Object.values(CONFIG.Canvas.detectionModes)) { - if (mode.tokenConfig && mode.id in limits.sight) { - this._sight.push({ - id: mode.id, - label: game.i18n.localize(mode.label), - enabled: limits.sight[mode.id]?.enabled, - range: limits.sight[mode.id]?.range - }); - } - } - - this._sight.sort((a, b) => a.label.localeCompare(b.label, game.i18n.lang)); - this._light = foundry.utils.deepClone(limits.light); - this._sound = foundry.utils.deepClone(limits.sound); - - if (this._sight.length === 0) { - this._sight.push({ id: "", range: null, enabled: true }); - } - } - - const scene = this.document instanceof Scene - ? this.document - : this.document.parent instanceof Scene - ? this.document.parent : null; - - return foundry.utils.mergeObject( - data, - { - sight: this._sight, - light: this._light, - sound: this._sound, - detectionModes: Object.values(CONFIG.Canvas.detectionModes).filter(m => m.tokenConfig), - gridUnits: scene?.grid.units || game.system.gridUnits || game.i18n.localize("GridUnits"), - submitText: game.i18n.localize("Save Changes"), - classPrefix: MODULE_ID - } - ); - } - - /** @override */ - render(force = false, options = {}) { - return super.render(force, options); - } - - /** @override */ - activateListeners(html) { - html.find('button[type="reset"]').click(this._onResetForm.bind(this)); - html.find(".action-button").click(this._onClickActionButton.bind(this)); - - return super.activateListeners(html); - } - - /** @param {PointerEvent} event */ - _onClickActionButton(event) { - event.preventDefault(); - const button = event.currentTarget; - const action = button.dataset.action; - game.tooltip.deactivate(); - - switch (action) { - case "addDetectionMode": - this._sight.push({ id: "", range: null, enabled: true }); - break; - case "removeDetectionMode": - this._sight.splice(button.closest(".detection-mode").dataset.index, 1); - - if (this._sight.length === 0) { - this._sight.push({ id: "", range: null, enabled: true }); - } - break; - } - - this.render(); - } - - /** @override */ - async _onChangeInput(event) { - await super._onChangeInput(event); - - const name = event.currentTarget.name; - - if (name.startsWith("sight.")) { - const i = event.currentTarget.closest(".detection-mode").dataset.index; - const limit = this._sight[i]; - - limit.id = this.form.querySelector(`[name="sight.${i}.id"]`).value; - limit.range = this.form.querySelector(`[name="sight.${i}.range"]`).value; - limit.enabled = this.form.querySelector(`[name="sight.${i}.enabled"]`).checked; - } else if (name.startsWith("light.")) { - const limit = this._light; - - limit.range = this.form.querySelector(`[name="light.range"]`).value; - limit.enabled = this.form.querySelector(`[name="light.enabled"]`).checked; - } else if (name.startsWith("sound.")) { - const limit = this._sound; - - limit.range = this.form.querySelector(`[name="sound.range"]`).value; - limit.enabled = this.form.querySelector(`[name="sound.enabled"]`).checked; - } - } - - /** @param {PointerEvent} event */ - _onResetForm(event) { - event.preventDefault(); - - this._sight = [{ id: "", range: null, enabled: true }]; - this._light.enabled = false; - this._light.range = null; - this._sound.enabled = false; - this._sound.range = null; - this.render(); - } - - /** @override */ - _getSubmitData(updateData = {}) { - const formData = foundry.utils.expandObject(super._getSubmitData(updateData)); - - formData.sight = Object.fromEntries(Object.values(formData.sight ?? []) - .filter(({ id }) => id).map(({ id, range, enabled }) => [id, { range, enabled }])); - - return foundry.utils.flattenObject({ flags: { [MODULE_ID]: formData } }); - } - - /** @override */ - async _updateObject(event, formData) { - formData = foundry.utils.expandObject(formData); - - const flags = this.document.toObject().flags[MODULE_ID] ?? {}; - - if (foundry.utils.isEmpty(flags)) { - const updateData = foundry.utils.expandObject(formData).flags[MODULE_ID]; - - if (!updateData.light.enabled && updateData.light.range == null - && Object.values(updateData.sight).every((mode) => !mode.enabled && mode.range == null) - && !updateData.sound.enabled && updateData.sound.range == null) { - return; - } - } - - for (const id of Object.keys(flags.sight ?? {})) { - if (!foundry.utils.hasProperty(formData, `flags.${MODULE_ID}.sight.${id}`) - && CONFIG.Canvas.detectionModes[id]?.tokenConfig) { - formData[`flags.${MODULE_ID}.sight.-=${id}`] = null; - } - } - - return this.document.update(formData, { render: false }); - } -} diff --git a/scripts/const.mjs b/scripts/const.mjs index 68314f0..27dc5ef 100644 --- a/scripts/const.mjs +++ b/scripts/const.mjs @@ -1 +1,24 @@ -export const MODULE_ID = "limits"; +/** + * @enum {number & {}} + */ +export const MODES = Object.freeze({ + /** + * Stack. + */ + STACK: 0, + + /** + * Upgrade. + */ + UPGRADE: 1, + + /** + * Downgrade. + */ + DOWNGRADE: 2, + + /** + * Override. + */ + OVERRIDE: 3, +}); diff --git a/scripts/constraint.mjs b/scripts/constraint.mjs deleted file mode 100644 index 39c0502..0000000 --- a/scripts/constraint.mjs +++ /dev/null @@ -1,675 +0,0 @@ -import { VolumeCollection } from "./volume.mjs"; - -export class Constraint extends PIXI.Polygon { - #rayCaster0 = null; - #rayCaster1 = null; - #rayCaster2 = null; - #rayCaster3 = null; - #rayCaster4 = null; - #minX = 0; - #minY = 0; - #minZ = 0; - #maxX = 0; - #maxY = 0; - #maxZ = 0; - #maxD = 0; - #constrain = false; - - /** - * @param {PointSourcePolygon} polygon - * @param {string} sense - * @param {object} [constraintOptions] - * @returns {PointSourcePolygon} - */ - static apply(polygon, sense, constraintOptions) { - const contraint = new this(polygon, sense); - - if (contraint.envelops) { - polygon.config.boundaryShapes.push(contraint); - } else { - polygon = polygon.applyConstraint(contraint, constraintOptions); - } - - return polygon; - } - - /** - * @param {PointSourcePolygon} polygon - * @param {string} sense - * @protected - */ - constructor(polygon, sense) { - super(); - - const origin = polygon.origin; - const config = polygon.config; - const source = config.source; - - /** - * @type {{x: number, y: number, z: number }} - * @readonly - */ - this.origin = { - x: origin.x, - y: origin.y, - z: source ? source.elevation * canvas.dimensions.distancePixels : 0 - }; - /** - * @type {number} - * @readonly - */ - this.radius = config.radius; - /** - * @type {number} - * @readonly - */ - this.externalRadius = config.externalRadius; - /** - * @type {string} - * @readonly - */ - this.sense = sense; - - this.#compute(polygon); - } - - /** - * @type {boolean} - * @readonly - */ - get envelops() { - return !this.#constrain; - } - - /** - * @param {PointSourcePolygon} polygon - */ - #compute(polygon) { - const { x: ox, y: oy, z: oz } = this.origin; - const minR = this.externalRadius; - const maxR = this.radius; - const bounds = polygon.bounds; - let minX = bounds.left; - let minY = bounds.top; - let maxX = bounds.right; - let maxY = bounds.bottom; - let minZ = this.#minZ = oz; - let maxZ = this.#maxZ = oz; - - // TODO: Do we need maxR? - const rayCaster0 = this.#rayCaster0 = VolumeCollection.instance.createRayCaster( - this.sense, minR, maxR, minX, minY, minZ, maxX, maxY, maxZ - ); - - rayCaster0.setOrigin(ox, oy, oz); - - const maxD = rayCaster0.maxD; - - this.#minX = minX = Math.max(minX, ox - maxD); - this.#minY = minY = Math.max(minY, oy - maxD); - this.#maxX = maxX = Math.min(maxX, ox + maxD); - this.#maxY = maxY = Math.min(maxY, oy + maxD); - - const points = polygon.points; - - if (rayCaster0.minD === maxD) { - this.#maxD = maxD; - - if (maxD < maxR) { - this.#addCircleSegment(maxD, 0); - this.#addCircleSegment(maxD, Math.PI * 0.5); - this.#addCircleSegment(maxD, Math.PI); - this.#addCircleSegment(maxD, Math.PI * 1.5); - } - } else { - this.#maxD = 0; - - const m = points.length; - let px0, py0, px1, py1, px2, py2, px3, py3; - - px0 = px1 = px2 = px3 = ox; - py0 = py1 = py2 = py3 = oy; - - let i = 0; - let x1, y1, q1; - - for (; i < m; i += 2) { - x1 = points[i]; - y1 = points[i + 1]; - - if (y1 > oy) { - q1 = x1 >= ox ? 0 : 1; - - break; - } - - if (y1 < oy) { - q1 = x1 <= ox ? 2 : 3; - - break; - } - - if (x1 !== ox) { - q1 = x1 <= ox ? 1 : 3; - - break; - } - } - - if (i < m) { - const i0 = i = (i + 2) % m; - - for (; ;) { - const x2 = points[i]; - const y2 = points[i + 1]; - let q2; - - if (y2 > oy) { - q2 = x2 >= ox ? 0 : 1; - } else if (y2 < oy) { - q2 = x2 <= ox ? 2 : 3; - } else if (x2 !== ox) { - q2 = x2 <= ox ? 1 : 3; - } else { - q2 = q1; - } - - if (q2 !== q1) { - let s; - - switch (q1) { - case 0: - case 2: - if (x2 !== x1) { - s = (ox - x1) / (x2 - x1); - x1 = ox; - y1 = y1 * (1 - s) + y2 * s; - } else { - s = 0; - x1 = ox; - y1 = oy; - } - - break; - case 1: - case 3: - if (y2 !== y1) { - s = (oy - y1) / (y2 - y1); - x1 = x1 * (1 - s) + x2 * s; - y1 = oy; - } else { - s = 0; - x1 = ox; - y1 = oy; - } - - break; - } - - switch (q1) { - case 0: - if (s !== 0) { - px0 = Math.max(px0, x1); - py0 = Math.max(py0, y1); - } - - px1 = Math.min(px1, x1); - py1 = Math.max(py1, y1); - - break; - case 1: - if (s !== 0) { - px1 = Math.min(px1, x1); - py1 = Math.max(py1, y1); - } - - px2 = Math.min(px2, x1); - py2 = Math.min(py2, y1); - - break; - case 2: - if (s !== 0) { - px2 = Math.min(px2, x1); - py2 = Math.min(py2, y1); - } - - px3 = Math.max(px3, x1); - py3 = Math.min(py3, y1); - - break; - case 3: - if (s !== 0) { - px3 = Math.max(px3, x1); - py3 = Math.min(py3, y1); - } - - px0 = Math.max(px0, x1); - py0 = Math.max(py0, y1); - - break; - } - - q1 = (q1 + 1) % 4; - } else { - switch (q2) { - case 0: - if (x1 !== ox || x2 !== ox) { - px0 = Math.max(px0, x2); - py0 = Math.max(py0, y2); - } - - break; - case 1: - if (y1 !== oy || y2 !== oy) { - px1 = Math.min(px1, x2); - py1 = Math.max(py1, y2); - } - - break; - case 2: - if (x1 !== ox || x2 !== ox) { - px2 = Math.min(px2, x2); - py2 = Math.min(py2, y2); - } - - break; - case 3: - if (y1 !== oy || y2 !== oy) { - px3 = Math.max(px3, x2); - py3 = Math.min(py3, y2); - } - - break; - } - - i = (i + 2) % m; - - if (i === i0) { - break; - } - - x1 = x2; - y1 = y2; - q1 = q2; - } - } - } - - px0 = Math.min(px0, maxX); - px3 = Math.min(px3, maxX); - py0 = Math.min(py0, maxY); - py1 = Math.min(py1, maxY); - px1 = Math.max(px1, minX); - px2 = Math.max(px2, minX); - py2 = Math.max(py2, minY); - py3 = Math.max(py3, minY); - - if (ox < px0 && oy < py0) { - const rayCaster1 = this.#rayCaster1 = rayCaster0.crop(ox, oy, minZ, px0, py0, maxZ); - const { minD, maxD } = rayCaster1; - - this.#maxD = Math.max(this.#maxD, maxD); - - px0 = Math.min(px0, ox + maxD); - py0 = Math.min(py0, oy + maxD); - - if (minD === maxD) { - if (maxD < Math.hypot(px0 - ox, py0 - oy)) { - this.#addCircleSegment(maxD, 0); - } else { - this.#addPoint(px0, oy); - this.points.push(px0, py0, ox, py0); - } - } else { - this.#castRays(rayCaster1, px0, oy, px0, py0); - this.#castRays(rayCaster1, px0, py0, ox, py0); - } - } else { - this.#rayCaster1 = null; - this.#addPoint(px0, oy); - this.#addPoint(px0, py0); - this.#addPoint(ox, py0); - } - - if (px1 < ox && oy < py1) { - const rayCaster2 = this.#rayCaster2 = rayCaster0.crop(px1, oy, minZ, ox, py1, maxZ); - const { minD, maxD } = rayCaster2; - - this.#maxD = Math.max(this.#maxD, maxD); - - px1 = Math.max(px1, ox - maxD); - py1 = Math.min(py1, oy + maxD); - - if (minD === maxD) { - if (maxD < Math.hypot(ox - px1, py1 - oy)) { - this.#addCircleSegment(maxD, Math.PI * 0.5); - } else { - this.#addPoint(ox, py1); - this.points.push(px1, py1, px1, oy); - } - } else { - this.#castRays(rayCaster2, ox, py1, px1, py1); - this.#castRays(rayCaster2, px1, py1, px1, oy); - } - } else { - this.#rayCaster2 = null; - this.#addPoint(ox, py1); - this.#addPoint(px1, py1); - this.#addPoint(px1, oy); - } - - if (px2 < ox && py2 < oy) { - const rayCaster3 = this.#rayCaster3 = rayCaster0.crop(px2, py2, minZ, ox, oy, maxZ); - const { minD, maxD } = rayCaster3; - - this.#maxD = Math.max(this.#maxD, maxD); - - px2 = Math.max(px2, ox - maxD); - py2 = Math.max(py2, oy - maxD); - - if (minD === maxD) { - if (maxD < Math.hypot(ox - px2, oy - py2)) { - this.#addCircleSegment(maxD, Math.PI); - } else { - this.#addPoint(px2, oy); - this.points.push(px2, py2, ox, py2); - } - } else { - this.#castRays(rayCaster3, px2, oy, px2, py2); - this.#castRays(rayCaster3, px2, py2, ox, py2); - } - } else { - this.#rayCaster3 = null; - this.#addPoint(px2, oy); - this.#addPoint(px2, py2); - this.#addPoint(ox, py2); - } - - if (ox < px3 && py3 < oy) { - const rayCaster4 = this.#rayCaster4 = rayCaster0.crop(ox, py3, minZ, px3, oy, maxZ); - const { minD, maxD } = rayCaster4; - - this.#maxD = Math.max(this.#maxD, maxD); - - px3 = Math.min(px3, ox + maxD); - py3 = Math.max(py3, oy - maxD); - - if (minD === maxD) { - if (maxD < Math.hypot(px3 - ox, oy - py3)) { - this.#addCircleSegment(maxD, Math.PI * 1.5); - } else { - this.#addPoint(ox, py3); - this.points.push(px3, py3, px3, oy); - } - } else { - this.#castRays(rayCaster4, ox, py3, px3, py3); - this.#castRays(rayCaster4, px3, py3, px3, oy); - } - } else { - this.#rayCaster4 = null; - this.#addPoint(ox, py3); - this.#addPoint(px3, py3); - this.#addPoint(px3, oy); - } - } - - if (this.#constrain) { - this.#closePoints(); - } else { - this.points.length = 0; - this.points.push( - minX, minY, - maxX, minY, - maxX, maxY, - minX, maxY - ); - } - } - - visualize() { - const dg = canvas.controls.debug; - - dg.lineStyle(8, 0xFF00FF, 1.0) - .beginFill(0xFF00FF, 0.0) - .drawRect(this.#minX, this.#minY, this.#maxX - this.#minX, this.#maxY - this.#minY) - .endFill(); - dg.lineStyle(4, 0xFFFF00, 1.0); - - for (const rayCaster of [this.#rayCaster1, this.#rayCaster2, this.#rayCaster3, this.#rayCaster4]) { - if (!rayCaster) { - continue; - } - - dg.beginFill(0xFFFF00, rayCaster.minD < rayCaster.maxD ? 0.25 : 0.0) - .drawRect( - rayCaster.minX, - rayCaster.minY, - rayCaster.maxX - rayCaster.minX, - rayCaster.maxY - rayCaster.minY - ) - .endFill(); - } - - dg.lineStyle(4, 0xFF0000, 1.0) - .beginFill(0xFF0000, 0.0) - .drawPolygon(this.points) - .endFill(); - } - - #addCircleSegment(radius, aStart, aDelta = Math.PI * 0.5) { - this.#constrain = true; - - const { x, y } = this.origin; - - if (radius === 0) { - this.#addPoint(x, y); - - return; - } - - this.#addPoint( - x + Math.cos(aStart) * radius, - y + Math.sin(aStart) * radius - ); - - const points = this.points; - - if (radius < canvas.dimensions.maxR) { - const epsilon = 1; // PIXI.Circle.approximateVertexDensity - const nStep = Math.ceil(aDelta / Math.sqrt(2 * epsilon / radius) - 1e-3); - const aStep = aDelta / nStep; - - for (let i = 1; i <= nStep; i++) { - const a = aStart + aStep * i; - - points.push( - x + Math.cos(a) * radius, - y + Math.sin(a) * radius, - ); - } - } else { - const aStep = aDelta * 0.5; - const aMid = aStart + aStep; - const aStop = aStart + aDelta; - const radiusMid = radius / Math.cos(aStep); - - points.push( - x + Math.cos(aMid) * radiusMid, - y + Math.sin(aMid) * radiusMid, - x + Math.cos(aStop) * radius, - y + Math.sin(aStop) * radius - ); - } - } - - #castRays(rayCaster, c0x, c0y, c1x, c1y) { - const { x, y, z } = this.origin; - - rayCaster.setOrigin(x, y, z); - - const precision = canvas.dimensions.size / 10; - const precision2 = precision * precision; - const c0dx = c0x - x; - const c0dy = c0y - y; - const t0 = rayCaster.setTarget(c0x, c0y, z).castRay().elapsedTime; - - if (t0 < 1) { - this.#constrain = true; - } - - const r0x = x + t0 * c0dx; - const r0y = y + t0 * c0dy; - - this.#addPoint(r0x, r0y); - - const c1dx = c1x - x; - const c1dy = c1y - y; - const t1 = rayCaster.setTarget(c1x, c1y, z).castRay().elapsedTime; - const r1x = x + t1 * c1dx; - const r1y = y + t1 * c1dy; - let cdx = c1x - c0x; - let cdy = c1y - c0y; - const cdd = Math.sqrt(cdx * cdx + cdy * cdy); - - cdx /= cdd; - cdy /= cdd; - - const u0n = cdx * c0dx + cdy * c0dy; - const ndx = c0dx - u0n * cdx; - const ndy = c0dy - u0n * cdy; - let ndd = ndx * ndx + ndy * ndy; - - if (ndd > 1e-6) { - ndd /= Math.sqrt(ndd); - - const pdx = cdx * ndd * 0.5; - const pdy = cdy * ndd * 0.5; - const u1n = cdx * c1dx + cdy * c1dy; - const c0dd = Math.sqrt(c0dx * c0dx + c0dy * c0dy); - const c1dd = Math.sqrt(c1dx * c1dx + c1dy * c1dy); - const fu0 = Math.log((u0n + c0dd) / ndd); // Math.asinh(u0n / ndd) - const fu1 = Math.log((u1n + c1dd) / ndd); // Math.asinh(u1n / ndd) - const dfu = fu1 - fu0; - const fuk = Math.ceil(Math.abs(dfu * (ndd / precision))); // Math.asinh(precision / ndd) - const fud = dfu / fuk; - - const recur = (i0, x0, y0, i2, x2, y2) => { - if (!(i2 - i0 > 1)) { - return; - } - - const dx02 = x0 - x2; - const dy02 = y0 - y2; - const dd02 = dx02 * dx02 + dy02 * dy02; - - if (dd02 <= precision2) { - return; - } - - const i1 = (i0 + i2) >> 1; - let u = Math.exp(fu0 + i1 * fud) - 1; u += u / (u + 1); // Math.sinh(fu0 + i1 * fud) - const dx = ndx + u * pdx; - const dy = ndy + u * pdy; - const t1 = rayCaster.setTarget(x + dx, y + dy, z).castRay().elapsedTime; // TODO: optimize? - const x1 = x + t1 * dx; - const y1 = y + t1 * dy; - - recur(i0, x0, y0, i1, x1, y1); - - if (t1 < 1) { - this.#constrain = true; - } - - this.#addPoint(x1, y1); - - recur(i1, x1, y1, i2, x2, y2); - }; - - recur(0, r0x, r0y, fuk, r1x, r1y); - } - - if (t1 < 1) { - this.#constrain = true; - } - - this.#addPoint(r1x, r1y); - } - - #addPoint(x, y) { - const points = this.points; - const m = points.length; - - if (m >= 4) { - let x3 = points[m - 4]; - let y3 = points[m - 3]; - let x2 = points[m - 2]; - let y2 = points[m - 1]; - let x1 = x; - let y1 = y; - - if (Math.abs(x1 - x2) > Math.abs(y1 - y2)) { - if ((x1 > x2) !== (x1 < x3)) { - if ((x2 > x1) === (x2 < x3)) { - [x1, y1, x2, y2] = [x2, y2, x1, y1]; - } else { - [x1, y1, x2, y2, x3, y3] = [x3, y3, x1, y1, x2, y2]; - } - } - } else { - if ((y1 > y2) !== (y1 < y3)) { - if ((y2 > y1) === (y2 < y3)) { - [x1, y1, x2, y2] = [x2, y2, x1, y1]; - } else { - [x1, y1, x2, y2, x3, y3] = [x3, y3, x1, y1, x2, y2]; - } - } - } - - const a = y2 - y3; - const b = x3 - x2; - const c = a * (x1 - x2) + b * (y1 - y2); - - if ((c * c) / (a * a + b * b) > 0.0625) { - points.push(x, y); - } else { - const dx = points[m - 4] - x; - const dy = points[m - 3] - y; - - points.length -= 2; - - if (dx * dx + dy * dy > 0.0625) { - points.push(x, y); - } - } - } else if (m === 2) { - const dx = points[m - 2] - x; - const dy = points[m - 1] - y; - - if (dx * dx + dy * dy > 0.0625) { - points.push(x, y); - } - } else { - points.push(x, y); - } - } - - #closePoints() { - const points = this.points; - - if (points.length < 6) { - points.length = 0; - - return; - } - - const [x1, y1, x2, y2] = points; - - this.#addPoint(x1, y1); - this.#addPoint(x2, y2); - - const m = points.length; - - [points[0], points[1], points[2], points[3]] = [points[m - 4], points[m - 3], points[m - 2], points[m - 1]]; - points.length -= 4; - } -} diff --git a/scripts/data/_module.mjs b/scripts/data/_module.mjs new file mode 100644 index 0000000..ae2076a --- /dev/null +++ b/scripts/data/_module.mjs @@ -0,0 +1,3 @@ +export { default as LimitRangeRegionBehaviorType } from "./region-behavior.mjs"; + +export * as fields from "./fields/_module.mjs"; diff --git a/scripts/data/fields.mjs b/scripts/data/fields.mjs deleted file mode 100644 index 9842b3a..0000000 --- a/scripts/data/fields.mjs +++ /dev/null @@ -1,224 +0,0 @@ -export class BitmaskField extends foundry.data.fields.DataField { - /** @override */ - static get _defaults() { - return foundry.utils.mergeObject(super._defaults, { - initial: 0, - nullable: false - }); - } - - /** @override */ - _cast(value) { - return Number(value); - } - - /** @override */ - _cleanType(value, options) { - value = super._cleanType(value, options); - - if (typeof value !== "number") { - return value; - } - - return value | 0; - } - - /** @override */ - _validateType(value) { - if (value !== (value | 0)) { - throw new Error("must be a signed 32-bit integer"); - } - } -} - -export class MapField extends foundry.data.fields.DataField { - /** - * @param {DataField} element - * @param {DataFieldOptions} [options] - */ - constructor(element, options) { - super(options); - - this.element = this.constructor._validateElementType(element); - } - - /** @override */ - static get _defaults() { - return foundry.utils.mergeObject(super._defaults, { - required: true, - nullable: false, - initial: () => ({}) - }); - } - - /** @override */ - static recursive = true; - - static _validateElementType(element) { - if (!(element instanceof foundry.data.fields.DataField)) { - throw new Error(`${this.name} must have a DataField as its contained element`); - } - - return element; - } - - /** @override */ - _cast(value) { - return typeof value === "object" ? value : {}; - } - - /** @override */ - _cleanType(value, options = {}) { - options.source = options.source || value; - - for (const name in value) { - value[name] = this.element.clean(value[name], options); - } - - return value; - } - - /** @override */ - initialize(value, model, options = {}) { - if (!value) { - return value; - } - - const data = {}; - - for (const name in value) { - const v = this.element.initialize(value[name], model, options); - - if (this.element.readonly) { - Object.defineProperty(data, name, { value: v, writable: false }); - } else if (typeof v === "function" && !v.prototype) { - Object.defineProperty(data, name, { get: v, set() { }, configurable: true }); - } else { - data[name] = v; - } - } - - return data; - } - - /** @override */ - _validateType(data, options = {}) { - if (!(data instanceof Object)) { - throw new Error("must be an object"); - } - - options.source = options.source || data; - - const schemaFailure = new foundry.data.validation.DataModelValidationFailure(); - - for (const key in data) { - const value = data[key]; - const failure = this.element.validate(value, options); - - if (failure) { - schemaFailure.fields[key] = failure; - - if (!failure.unresolved && failure.fallback) { - continue; - } - - if (options.fallback) { - const initial = this.element.getInitialValue(options.source); - - if (this.element.validate(initial, { source: options.source }) === undefined) { - data[key] = initial; - failure.fallback = initial; - } else { - failure.unresolved = schemaFailure.unresolved = true; - } - } else { - failure.unresolved = schemaFailure.unresolved = true; - } - } - } - - if (!foundry.utils.isEmpty(schemaFailure.fields)) { - return schemaFailure; - } - } - - /** @override */ - _validateModel(changes, options = {}) { - options.source = options.source || changes; - - if (!changes) { - return; - } - - for (const name in changes) { - const change = changes[name]; - - if (change && this.element.constructor.recursive) { - this.element._validateModel(change, options); - } - } - } - - /** @override */ - toObject(value) { - if (value == null) { - return value; - } - - const data = {}; - - for (const name in value) { - data[name] = this.element.toObject(value[name]); - } - - return data; - } - - /** @override */ - apply(fn, data = {}, options = {}) { - const results = {}; - - for (const key in data) { - const r = this.element.apply(fn, data[key], options); - - if (!options.filter || !foundry.utils.isEmpty(r)) { - results[key] = r; - } - } - - return results; - } -} - -export class VariantDataField extends foundry.data.fields.TypeDataField { - /** - * The data models of this variant data type. - * @type {{[type: string]: DataModel}} - */ - #dataModels; - - /** - * @param {{[type: string]: DataModel}} dataModels - * @param {DataFieldOptions} [options] - */ - constructor(dataModels, options) { - super(foundry.abstract.Document, options); - - this.#dataModels = Object.freeze({ ...dataModels }); - } - - /** @override */ - static getModelProvider(model) { - return null; - } - - /** @override */ - getModelForType(type) { - return this.#dataModels[type] ?? null; - } - - /** @override */ - getInitialValue(data) { - return this.getModelForType(data.type)?.cleanData() ?? {}; - } -} diff --git a/scripts/data/fields/_module.mjs b/scripts/data/fields/_module.mjs new file mode 100644 index 0000000..d67329e --- /dev/null +++ b/scripts/data/fields/_module.mjs @@ -0,0 +1,4 @@ +export { default as ModeField } from "./mode.mjs"; +export { default as PriorityField } from "./priority.mjs"; +export { default as RangeField } from "./range.mjs"; +export { default as SightField } from "./sight.mjs"; diff --git a/scripts/data/fields/mode.mjs b/scripts/data/fields/mode.mjs new file mode 100644 index 0000000..afbac81 --- /dev/null +++ b/scripts/data/fields/mode.mjs @@ -0,0 +1,34 @@ +import { MODES } from "../../const.mjs"; + +export default class ModeField extends foundry.data.fields.NumberField { + constructor() { + super({ + required: true, + nullable: false, + initial: MODES.DOWNGRADE, + choices: Object.values(MODES), + }); + } + + /** @override */ + _toInput(config) { + if (config.value === undefined) { + config.value = this.getInitialValue({}); + } + + config.options = Object.entries(MODES).map(([key, value]) => ({ value, label: `LIMITS.MODES.${key}.label` })); + config.localize = true; + config.sort = false; + config.dataset ??= {}; + config.dataset.dtype = "Number"; + + const select = foundry.applications.fields.createSelectInput(config); + const modes = foundry.utils.invertObject(MODES); + + select.dataset.tooltip = `LIMITS.MODES.${modes[select.value]}.hint`; + select.dataset.tooltipDirection = "UP"; + select.setAttribute("onchange", `game.tooltip.deactivate(); this.dataset.tooltip = "LIMITS.MODES." + (${JSON.stringify(modes)})[this.value] + ".hint";`); + + return select; + } +} diff --git a/scripts/data/fields/priority.mjs b/scripts/data/fields/priority.mjs new file mode 100644 index 0000000..aebeac2 --- /dev/null +++ b/scripts/data/fields/priority.mjs @@ -0,0 +1,24 @@ +export default class PriorityField extends foundry.data.fields.NumberField { + constructor() { + super({ + required: true, + nullable: false, + integer: true, + min: -2147483648, + max: 2147483647, + initial: 0, + }); + } + + /** @override */ + _toInput(config) { + Object.assign(config, { + min: this.min, + max: this.max, + step: 1, + placeholder: "0", + }); + + return foundry.applications.fields.createNumberInput(config); + } +} diff --git a/scripts/data/fields/range.mjs b/scripts/data/fields/range.mjs new file mode 100644 index 0000000..dad835a --- /dev/null +++ b/scripts/data/fields/range.mjs @@ -0,0 +1,28 @@ +export default class RangeField extends foundry.data.fields.NumberField { + constructor() { + super({ required: true, nullable: true, min: 0, step: 0.01 }); + } + + /** @override */ + toFormGroup(groupConfig = {}, inputConfig) { + groupConfig.units ??= "GridUnits"; + + return super.toFormGroup(groupConfig, inputConfig); + } + + /** @override */ + _toInput(config) { + Object.assign(config, { + min: this.min, + max: this.max, + step: this.step, + placeholder: "\uF534", + }); + + const input = foundry.applications.fields.createNumberInput(config); + + input.classList.add("placeholder-fa-solid", "limits--placeholder-font-size-12"); + + return input; + } +} diff --git a/scripts/data/fields/sight.mjs b/scripts/data/fields/sight.mjs new file mode 100644 index 0000000..d0bd660 --- /dev/null +++ b/scripts/data/fields/sight.mjs @@ -0,0 +1,38 @@ +export default class SightField extends foundry.data.fields.SetField { + constructor() { + super(new foundry.data.fields.StringField({ required: true, nullable: false, blank: false })); + } + + /** @override */ + _cleanType(value, options) { + value = super._cleanType(value, options); + value.sort(); + + const n = value.length; + let k = 0; + + for (let i = 0; i + 1 < n; i++) { + if (value[i] === value[i + 1]) { + k++; + } else if (k !== 0) { + value[i - k] = value[i]; + } + } + + if (k !== 0) { + value[n - 1 - k] = value[n - 1]; + value.length -= k; + } + + return value; + } + + /** @override */ + _toInput(config) { + config.options = Object.entries(CONFIG.Canvas.detectionModes).map(([value, { label }]) => ({ value, label })); + config.localize = true; + config.sort = true; + + return foundry.applications.fields.createMultiSelectInput(config); + } +} diff --git a/scripts/data/models.mjs b/scripts/data/models.mjs deleted file mode 100644 index 95a12c3..0000000 --- a/scripts/data/models.mjs +++ /dev/null @@ -1,107 +0,0 @@ -import { BitmaskField, MapField, VariantDataField } from "./fields.mjs"; - -export class FigureData extends foundry.abstract.DataModel { - /** @override */ - static defineSchema() { - const schema = {}; - - schema.x = new foundry.data.fields.NumberField({ required: true, nullable: false, initial: 0 }); - schema.y = new foundry.data.fields.NumberField({ required: true, nullable: false, initial: 0 }); - schema.rotation = new foundry.data.fields.AngleField({ required: true }); - schema.shape = new foundry.data.fields.EmbeddedDataField(foundry.data.ShapeData, { required: true }); - schema.bezierFactor = new foundry.data.fields.AlphaField({ required: false, initial: 0, max: 0.5 }); - schema.texture = new foundry.data.TextureData({ required: true }); - schema.mask = new BitmaskField({ required: true, initial: -1 }); - - return schema; - } -} - -export class BoundaryData extends foundry.abstract.DataModel { - /** @override */ - static defineSchema() { - const schema = {}; - - schema.type = new foundry.data.fields.StringField({ required: true }); - schema.data = new VariantDataField({ - cylinder: CylinderData, - sphere: SphereData - }); - schema.mask = new BitmaskField({ required: true, initial: -1 }); - - return schema; - } -} - -export class CylinderData extends foundry.abstract.DataModel { - /** @override */ - static defineSchema() { - const schema = {}; - - schema.base = new foundry.data.fields.ArrayField( - new foundry.data.fields.EmbeddedDataField(FigureData, { required: true }), - { required: true } - ); - schema.bottom = new foundry.data.fields.NumberField({ required: true, nullable: true, initial: null }); - schema.top = new foundry.data.fields.NumberField({ required: true, nullable: true, initial: null }); - - return schema; - } -} - -export class SphereData extends foundry.abstract.DataModel { - /** @override */ - static defineSchema() { - const schema = {}; - - schema.x = new foundry.data.fields.NumberField({ required: true, nullable: false, initial: 0 }); - schema.y = new foundry.data.fields.NumberField({ required: true, nullable: false, initial: 0 }); - schema.radius = new foundry.data.fields.NumberField({ required: true, nullable: false, initial: 0 }); - - return schema; - } -} - -export class VolumeData extends foundry.abstract.DataModel { - /** @override */ - static defineSchema() { - const rangeField = () => new foundry.data.fields.SchemaField({ - enabled: new foundry.data.fields.BooleanField({ required: true }), - range: new foundry.data.fields.NumberField({ - required: true, - initial: null, - nullable: true, - min: 0, - step: 0.01 - }) - }); - const schema = {}; - - schema.hidden = new foundry.data.fields.BooleanField({ required: true }); - schema.boundaries = new foundry.data.fields.ArrayField( - new foundry.data.fields.EmbeddedDataField(BoundaryData, { required: true }), - { required: true } - ); - schema.priority = new foundry.data.fields.NumberField({ - required: true, - nullable: false, - integer: true, - initial: 0, - min: Number.MIN_SAFE_INTEGER, - max: Number.MAX_SAFE_INTEGER - }); - schema.mode = new foundry.data.fields.NumberField({ - required: true, - nullable: false, - integer: true, - initial: 0, - min: 0, - max: 4 - }); - schema.light = rangeField(); - schema.sight = new MapField(rangeField()); - schema.sound = rangeField(); - - return schema; - } -} diff --git a/scripts/data/region-behavior.mjs b/scripts/data/region-behavior.mjs new file mode 100644 index 0000000..8caaf21 --- /dev/null +++ b/scripts/data/region-behavior.mjs @@ -0,0 +1,78 @@ +import Limits from "../limits.mjs"; +import ModeField from "./fields/mode.mjs"; +import PriorityField from "./fields/priority.mjs"; +import RangeField from "./fields/range.mjs"; +import SightField from "./fields/sight.mjs"; + +/** + * The "Limit Range" Region Behavior. + * @sealed + */ +export default class LimitRangeRegionBehaviorType extends foundry.data.regionBehaviors.RegionBehaviorType { + /** + * @type {string[]} + * @override + */ + static LOCALIZATION_PREFIXES = ["LIMITS"]; + + /** + * @returns {Record}} + * @override + */ + static defineSchema() { + return { + sight: new SightField(), + light: new foundry.data.fields.BooleanField(), + darkness: new foundry.data.fields.BooleanField(), + sound: new foundry.data.fields.BooleanField(), + range: new RangeField(), + mode: new ModeField(), + priority: new PriorityField(), + }; + } + + /** + * @type {Record Promise>} + * @override + */ + static events = { + [CONST.REGION_EVENTS.BEHAVIOR_STATUS]: onBehaviorStatus, + [CONST.REGION_EVENTS.REGION_BOUNDARY]: onRegionBoundary, + }; + + /** + * @param {object} changed + * @param {object} options + * @param {string} userId + * @override + */ + _onUpdate(changed, options, userId) { + super._onUpdate(changed, options, userId); + + if ("system" in changed && this.parent.viewed) { + Limits._onBehaviorSystemChanged(this.parent); + } + } +} + +/** + * @this LimitRangeRegionBehaviorType + * @param {foundry.types.RegionEvent} event - The Region event. + */ +function onBehaviorStatus(event) { + if (event.data.viewed === true) { + Limits._onBehaviorViewed(this.parent); + } else if (event.data.viewed === false) { + Limits._onBehaviorUnviewed(this.parent); + } +} + +/** + * @this LimitRangeRegionBehaviorType + * @param {foundry.types.RegionEvent} event - The Region event. + */ +function onRegionBoundary(event) { + if (this.parent.viewed) { + Limits._onBehaviorBoundaryChanged(this.parent); + } +} diff --git a/scripts/extension.mjs b/scripts/extension.mjs deleted file mode 100644 index ca9c8d5..0000000 --- a/scripts/extension.mjs +++ /dev/null @@ -1,61 +0,0 @@ -import { LimitsConfig } from "./config.mjs"; -import { MODULE_ID } from "./const.mjs"; -import { VolumeData } from "./data/models.mjs"; -import { Volume, VolumeCollection } from "./volume.mjs"; - -/** - * @abstract - */ -export class Extension { - /** - * @param {Document} document - * @returns {object} - */ - getFlags(document) { - return foundry.utils.deepClone(document.flags[MODULE_ID]); - } - - /** - * @param {Document} document - * @param {VolumeData} data - * @abstract - */ - prepareVolumeData(document, data) { - throw new Error("Not implemented"); - } - - /** - * @param {Document} document - * @param {boolean} [deleted=false] - */ - updateVolume(document, deleted = false) { - const id = document.uuid; - let volume = VolumeCollection.instance.get(id); - let flags; - - if (!deleted && !foundry.utils.isEmpty(flags = this.getFlags(document))) { - if (!volume) { - volume = new Volume(id); - VolumeCollection.instance.set(id, volume); - } - - const data = new VolumeData(flags); - - this.prepareVolumeData(document, data); - volume.update(data.toObject()); - } else if (volume) { - volume.destroy(); - VolumeCollection.instance.delete(id); - } - } - - /** - * @param {DocumentSheet} sheet - * @param {string[]} selectors - * @param {string} position - * @param {(html: string) => string} [wrap] - */ - injectConfig(sheet, selectors, position, wrap) { - LimitsConfig.inject(sheet, selectors, position, wrap); - } -} diff --git a/scripts/extensions/drawing.mjs b/scripts/extensions/drawing.mjs deleted file mode 100644 index c2f6a80..0000000 --- a/scripts/extensions/drawing.mjs +++ /dev/null @@ -1,68 +0,0 @@ -import { Extension } from "../extension.mjs"; - -export class DrawingExtension extends Extension { - /** @override */ - registerHooks() { - Hooks.on("drawDrawing", (object) => { - if (object.isPreview) { - return; - } - - this.updateVolume(object.document); - }); - - Hooks.on("updateDrawing", (document) => { - if (!document.rendered) { - return; - } - - this.updateVolume(document); - }); - - Hooks.on("destroyDrawing", (object) => { - if (object.isPreview) { - return; - } - - this.updateVolume(object.document, true); - }); - - Hooks.on("renderDrawingConfig", (sheet) => { - if (sheet.options.configureDefault) { - return; - } - - this.injectConfig(sheet, [`.tab[data-tab="position"]`], "beforeend"); - }); - } - - /** @override */ - prepareVolumeData(document, data) { - let bottom = null; - let top = null; - - if (game.modules.get("levels")?.active) { - bottom = document.flags.levels?.rangeBottom ?? null; - top = document.flags.levels?.rangeTop ?? null; - } - - data.updateSource({ - hidden: document.hidden, - mode: 4, - boundaries: [{ - type: "cylinder", - data: { - base: [{ - x: document.x, - y: document.y, - rotation: document.rotation, - shape: document.shape, - bezierFactor: document.bezierFactor, - }], - bottom, - top - } - }] - }); - } -} diff --git a/scripts/extensions/scene.mjs b/scripts/extensions/scene.mjs deleted file mode 100644 index 193644d..0000000 --- a/scripts/extensions/scene.mjs +++ /dev/null @@ -1,47 +0,0 @@ -import { Extension } from "../extension.mjs"; - -export class SceneExtension extends Extension { - /** @override */ - registerHooks() { - Hooks.on("canvasReady", () => { - this.updateVolume(canvas.scene); - }); - - Hooks.on("updateScene", (scene) => { - if (!scene.isView) { - return; - } - - this.updateVolume(scene); - }); - - Hooks.on("canvasTearDown", () => { - this.updateVolume(canvas.scene, true); - }); - - Hooks.on("renderSceneConfig", (sheet) => { - this.injectConfig(sheet, [`.tab[data-tab="lighting"]`], "beforeend", (html) => `
${html}`); - }); - } - - /** @override */ - prepareVolumeData(scene, data) { - data.updateSource({ - mode: 4, - boundaries: [{ - type: "cylinder", - data: { - base: [{ - x: 0, - y: 0, - shape: { - type: "r", - width: scene.dimensions.width, - height: scene.dimensions.height - } - }] - } - }] - }); - } -} diff --git a/scripts/extensions/template.mjs b/scripts/extensions/template.mjs deleted file mode 100644 index 324a6dd..0000000 --- a/scripts/extensions/template.mjs +++ /dev/null @@ -1,108 +0,0 @@ -import { Shape } from "../utils/shape.js"; -import { Extension } from "../extension.mjs"; - -export class TemplateExtension extends Extension { - /** @override */ - registerHooks() { - Hooks.on("drawMeasuredTemplate", (object) => { - if (object.isPreview) { - return; - } - - this.updateVolume(object.document); - }); - - Hooks.on("updateMeasuredTemplate", (document) => { - if (!document.rendered) { - return; - } - - this.updateVolume(document); - }); - - Hooks.on("refreshMeasuredTemplate", (object, flags) => { - if (object.isPreview || !flags.refreshShape) { - return; - } - - this.updateVolume(object.document); - }); - - Hooks.on("destroyMeasuredTemplate", (object) => { - if (object.isPreview) { - return; - } - - this.updateVolume(object.document, true); - }); - - Hooks.on("renderMeasuredTemplateConfig", (sheet) => { - this.injectConfig(sheet, [`button[type="submit"]`], "beforebegin"); - }); - } - - /** @override */ - prepareVolumeData(document, data) { - const shape = document.object.shape; - let base; - - if (shape instanceof PIXI.Rectangle) { - base = { - x: document.x, - y: document.y, - shape: { - type: "r", - width: shape.width, - height: shape.height - } - }; - } else if (shape instanceof PIXI.Circle) { - base = { - x: document.x - shape.radius, - y: document.y - shape.radius, - shape: { - type: "e", - width: shape.radius * 2, - height: shape.radius * 2 - } - }; - } else if (shape instanceof PIXI.Ellipse) { - base = { - x: document.x - shape.width, - y: document.y - shape.height, - shape: { - type: "e", - width: shape.width * 2, - height: shape.height * 2, - } - }; - } else if (shape instanceof PIXI.Polygon) { - base = { - x: document.x, - y: document.y, - shape: { - type: "p", - points: shape.points - } - }; - } else if (shape instanceof PIXI.RoundedRectangle) { - base = { - x: document.x, - y: document.y, - shape: { - type: "p", - points: Shape.from(shape).contour - } - }; - } - - data.updateSource({ - hidden: document.hidden, - mode: 4, - boundaries: base ? [{ - type: "cylinder", - data: { base: [base] } - }] : [] - }); - } -} diff --git a/scripts/extensions/tile.mjs b/scripts/extensions/tile.mjs deleted file mode 100644 index 6d7b46a..0000000 --- a/scripts/extensions/tile.mjs +++ /dev/null @@ -1,87 +0,0 @@ -import { Extension } from "../extension.mjs"; - -export class TileExtension extends Extension { - /** @override */ - registerHooks() { - Hooks.on("drawTile", (object) => { - if (object.isPreview) { - return; - } - - this.updateVolume(object.document); - }); - - Hooks.on("updateTile", (document) => { - if (!document.rendered) { - return; - } - - this.#updateVolume(document); - }); - - Hooks.on("destroyTile", (object) => { - if (object.isPreview) { - return; - } - - this.updateVolume(object.document, true); - }); - - Hooks.on("renderTileConfig", (sheet) => { - this.injectConfig(sheet, [`.tab[data-tab="basic"]`], "beforeend"); - }); - } - - /** - * @param {Document} document - * @param {boolean} [deleted=false] - */ - async #updateVolume(document, deleted) { - if (deleted || !document.texture.src || getTexture(document.texture.src)) { - this.updateVolume(document, deleted); - } else { - loadTexture(document.texture.src).then(() => this.updateVolume(document)); - } - } - - /** @override */ - prepareVolumeData(document, data) { - let bottom = null; - let top = null; - - if (document.overhead && game.modules.get("levels")?.active) { - bottom = document.flags.levels?.rangeBottom ?? null; - top = document.flags.levels?.rangeTop ?? null; - } - - data.updateSource({ - hidden: document.hidden, - mode: 4, - boundaries: [{ - type: "cylinder", - data: { - base: [{ - x: document.x, - y: document.y, - rotation: document.rotation, - shape: { - type: "r", - width: document.width, - height: document.height - }, - texture: { - src: document.texture.src, - scaleX: document.texture.scaleX, - scaleY: document.texture.scaleY, - rotation: document.texture.rotation, - offsetX: document.texture.offsetX, - offsetY: document.texture.offsetY - } - }], - bottom, - top - } - }] - }); - } -} diff --git a/scripts/index.js b/scripts/index.js deleted file mode 100644 index 4a57da6..0000000 --- a/scripts/index.js +++ /dev/null @@ -1,22 +0,0 @@ -import { DrawingExtension } from "./extensions/drawing.mjs"; -import { SceneExtension } from "./extensions/scene.mjs"; -import { TemplateExtension } from "./extensions/template.mjs"; -import { TileExtension } from "./extensions/tile.mjs"; - -const extensions = [ - new DrawingExtension(), - new TemplateExtension(), - new SceneExtension(), - new TileExtension() -]; - -Hooks.once("init", () => { - for (const extension of extensions) { - extension.registerHooks(); - } -}); - -import "./patches/light.mjs"; -import "./patches/sight.mjs"; -import "./patches/sound.mjs"; - diff --git a/scripts/limits.mjs b/scripts/limits.mjs new file mode 100644 index 0000000..93a1ed5 --- /dev/null +++ b/scripts/limits.mjs @@ -0,0 +1,410 @@ +import * as raycast from "./raycast/_module.mjs"; + +/** + * @sealed + */ +export default class Limits { + /** + * @type {Readonly<{ [mode: string]: raycast.Space }>} + * @readonly + */ + static sight = {}; + + /** @type {Limits} */ + static #light; + + /** + * @type {raycast.Space} + */ + static get light() { + return this.#light.#getSpace(); + } + + /** @type {Limits} */ + static #darkness; + + /** + * @type {raycast.Space} + */ + static get darkness() { + return this.#darkness.#getSpace(); + } + + /** @type {Limits} */ + static #sound; + + /** + * @type {raycast.Space} + */ + static get sound() { + return this.#sound.#getSpace(); + } + + static { + Hooks.once("init", () => { + Hooks.once("setup", () => { + Hooks.once("canvasInit", () => { + this.sight = {}; + + for (const id in CONFIG.Canvas.detectionModes) { + const limit = new SightLimits(id); + + Object.defineProperty(this.sight, id, { + get: limit.#getSpace.bind(limit), + enumerable: true, + }); + } + + Object.freeze(this.sight); + + this.#light = new LightLimits(); + this.#darkness = new DarknessLimits(); + this.#sound = new SoundLimits(); + }); + }); + }); + } + + /** @type {Map} */ + static #geometries = new Map(); + + /** + * Get the geometry of the RegionDocument. + * @param {foundry.documents.RegionDocument} region - The RegionDocument. + * @returns {raycast.Geometry} The geometry. + */ + static #getGeometry(region) { + let geometry = this.#geometries.get(region); + + if (!geometry) { + const distancePixels = canvas.dimensions.distancePixels; + let shape; + + if (region.shapes.length === 1) { + const data = region.shapes[0]; + + switch (data.type) { + case "rectangle": + if (data.rotation === 0) { + shape = raycast.shapes.Bounds.create({ + minX: data.x, + minY: data.y, + maxX: data.x + data.width, + maxY: data.y + data.height, + }); + } else { + shape = raycast.shapes.Rectangle.create({ + centerX: data.x + data.width / 2, + centerY: data.y + data.height / 2, + width: data.width / 2, + height: data.height / 2, + rotation: Math.toRadians(data.rotation), + }); + } + + break; + case "circle": + shape = raycast.shapes.Circle.create({ + centerX: data.x, + centerY: data.y, + radius: data.radius, + }); + + break; + case "ellipse": + if (data.radiusX === data.radiusY) { + shape = raycast.shapes.Circle.create({ + centerX: data.x, + centerY: data.y, + radius: data.radiusX, + }); + } else { + shape = raycast.shapes.Ellipse.create({ + centerX: data.x, + centerY: data.y, + radiusX: data.radiusX, + radiusY: data.radiusY, + rotation: Math.toRadians(data.rotation), + }); + } + + break; + } + } + + geometry = raycast.Geometry.create({ + boundaries: [raycast.boundaries.Region.create({ + shapes: shape ? [shape] : region.object.polygons.map((polygon) => raycast.shapes.Polygon.create({ points: polygon.points })), + bottom: region.object.bottom * distancePixels - 1e-8, + top: region.object.top * distancePixels + 1e-8, + })], + }); + this.#geometries.set(region, geometry); + } + + return geometry; + } + + /** + * Destroy the geometry of the RegionDocument. + * @param {foundry.documents.RegionDocument} region - The RegionDocument. + */ + static #destroyGeometry(region) { + if (this.#geometries.delete(region)) { + for (const behavior of region.behaviors) { + if (this.#volumes.delete(behavior)) { + for (const instance of this.#instances) { + if (instance.#behaviors.has(behavior)) { + instance.#space = null; + instance._updatePerception(); + } + } + } + } + } + } + + static { + Hooks.on("updateRegion", (document, changed) => { + if (document.object && ("shapes" in changed || "elevation" in changed)) { + this.#destroyGeometry(document); + } + }); + + Hooks.on("destroyRegion", (region) => { + this.#destroyGeometry(region.document); + }); + } + + /** @type {Map} */ + static #volumes = new Map(); + + /** + * Get the volume of the RegionBehavior. + * @param {foundry.documents.RegionBehavior} behavior - The RegionBehavior. + * @returns {raycast.Volume} The volume. + */ + static #getVolume(behavior) { + let volume = this.#volumes.get(behavior); + + if (!volume) { + const geometry = this.#getGeometry(behavior.parent); + const { priority, mode, range } = behavior.system; + const cost = 1.0 / ((range ?? Infinity) * canvas.dimensions.distancePixels); + + volume = raycast.Volume.create({ geometry, priority, mode, cost }); + this.#volumes.set(behavior, volume); + } + + return volume; + } + + /** @type {Limits[]} */ + static #instances = []; + + /** + * @internal + * @ignore + */ + constructor() { + Limits.#instances.push(this); + } + + /** @type {Set} */ + #behaviors = new Set(); + + /** @type {raycast.Space | null} */ + #space = null; + + /** + * Get the space. + * @returns {raycast.Space} + */ + #getSpace() { + let space = this.#space; + + if (!space) { + const volumes = []; + + for (const behavior of this.#behaviors) { + volumes.push(Limits.#getVolume(behavior)); + } + + space = raycast.Space.create({ volumes }); + this.#space = space; + } + + return space; + } + + /** + * Called when the RegionBehavior is viewed. + * @param {foundry.documents.RegionBehavior} behavior - The RegionBehavior. + * @internal + * @ignore + */ + static _onBehaviorViewed(behavior) { + for (const instance of this.#instances) { + if (instance.#behaviors.has(behavior)) { + continue; + } + + if (instance._hasBehavior(behavior)) { + instance.#behaviors.add(behavior); + instance.#space = null; + instance._updatePerception(); + } + } + } + + /** + * Called when the RegionBehavior is unviewed. + * @param {foundry.documents.RegionBehavior} behavior - The RegionBehavior. + * @internal + * @ignore + */ + static _onBehaviorUnviewed(behavior) { + this.#volumes.delete(behavior); + + for (const instance of this.#instances) { + if (instance.#behaviors.delete(behavior)) { + instance.#space = null; + instance._updatePerception(); + } + } + } + + /** + * Called when the RegionBehavior's Region shape is changed and the RegionBehavior is viewed. + * @param {foundry.documents.RegionBehavior} behavior - The RegionBehavior. + * @internal + * @ignore + */ + static _onBehaviorBoundaryChanged(behavior) { + this.#volumes.delete(behavior); + + for (const instance of this.#instances) { + if (instance.#behaviors.has(behavior)) { + instance.#space = null; + instance._updatePerception(); + } + } + } + + /** + * Called when the RegionBehavior's system data changed and the RegionBehavior is viewed. + * @param {foundry.documents.RegionBehavior} behavior - The RegionBehavior. + * @internal + * @ignore + */ + static _onBehaviorSystemChanged(behavior) { + this.#volumes.delete(behavior); + + for (const instance of this.#instances) { + if (instance._hasBehavior(behavior)) { + instance.#behaviors.add(behavior); + instance.#space = null; + instance._updatePerception(); + } else if (instance.#behaviors.has(behavior)) { + instance.#behaviors.delete(behavior); + instance.#space = null; + instance._updatePerception(); + } + } + } + + /** + * @param {foundry.documents.RegionBehavior} behavior - The RegionBehavior. + * @returns {boolean} + * @protected + * @abstract + */ + _hasBehavior(behavior) { + return false; + } + + /** + * Update perception. + * @protected + * @abstract + */ + _updatePerception() { } +} + +/** + * @internal + * @ignore + */ +class SightLimits extends Limits { + /** + * @param {string} id - The detection mode ID. + */ + constructor(id) { + super(); + + /** + * The detection mode ID. + * @type {string} + * @readonly + */ + this.id = id; + } + + /** @override */ + _hasBehavior(behavior) { + return behavior.system.sight.has(this.id); + } + + /** @override */ + _updatePerception() { + canvas.perception.update({ initializeVision: true }); + } +} + +/** + * @internal + * @ignore + */ +class LightLimits extends Limits { + /** @override */ + _hasBehavior(behavior) { + return behavior.system.light; + } + + /** @override */ + _updatePerception() { + canvas.perception.update({ initializeLightSources: true }); + } +} + +/** + * @internal + * @ignore + */ +class DarknessLimits extends Limits { + /** @override */ + _hasBehavior(behavior) { + return behavior.system.darkness; + } + + /** @override */ + _updatePerception() { + canvas.perception.update({ initializeDarknessSources: true, initializeLightSources: true }); + } +} + +/** + * @internal + * @ignore + */ +class SoundLimits extends Limits { + /** @override */ + _hasBehavior(behavior) { + return behavior.system.sound; + } + + /** @override */ + _updatePerception() { + canvas.perception.update({ initializeSounds: true }); + } +} diff --git a/scripts/patches/light.mjs b/scripts/patches/light.mjs deleted file mode 100644 index 6cb5dc4..0000000 --- a/scripts/patches/light.mjs +++ /dev/null @@ -1,14 +0,0 @@ -import { MODULE_ID } from "../const.mjs"; -import { Constraint } from "./../constraint.mjs"; - -Hooks.once("libWrapper.Ready", () => { - libWrapper.register( - MODULE_ID, - "LightSource.prototype._createPolygon", - function (wrapped, ...args) { - return Constraint.apply(wrapped(...args), "light", { scalingFactor: 100 }); - }, - libWrapper.WRAPPER, - { perf_mode: libWrapper.PERF_FAST } - ); -}); diff --git a/scripts/patches/sight.mjs b/scripts/patches/sight.mjs deleted file mode 100644 index d12ed39..0000000 --- a/scripts/patches/sight.mjs +++ /dev/null @@ -1,107 +0,0 @@ -import { MODULE_ID } from "../const.mjs"; -import { Constraint } from "./../constraint.mjs"; -import { VolumeCollection } from "./../volume.mjs"; - -function _testPoint(wrapped, visionSource, mode, target, test) { - if (!wrapped(visionSource, mode, target, test)) { - return false; - } - - const visionSourceZ = visionSource.elevation * canvas.dimensions.distancePixels; - const point = test.point; - - return VolumeCollection.instance.castRay( - `sight.${mode.id}`, - visionSource.object.externalRadius, - visionSource.x, visionSource.y, visionSourceZ, - point.x, point.y, point.z ?? visionSourceZ - ).targetHit; -} - -Hooks.once("libWrapper.Ready", () => { - libWrapper.register( - MODULE_ID, - "DetectionMode.prototype._testPoint", - _testPoint, - libWrapper.WRAPPER, - { perf_mode: libWrapper.PERF_FAST } - ); - - if (VisionSource.prototype._createLightPolygon) { - libWrapper.register( - MODULE_ID, - "VisionSource.prototype._createRestrictedPolygon", - function (wrapped, ...args) { - return Constraint.apply( - wrapped(...args), - `sight.${this.detectionMode?.id ?? DetectionMode.BASIC_MODE_ID}`, - { scalingFactor: 100 } - ); - }, - libWrapper.WRAPPER, - { perf_mode: libWrapper.PERF_FAST } - ); - - libWrapper.register( - MODULE_ID, - "VisionSource.prototype._createLightPolygon", - function (wrapped, ...args) { - return Constraint.apply( - wrapped(...args), - `sight.${DetectionMode.LIGHT_MODE_ID}`, - { scalingFactor: 100 } - ); - }, - libWrapper.WRAPPER, - { perf_mode: libWrapper.PERF_FAST } - ); - } else { - libWrapper.register( - MODULE_ID, - "DetectionModeBasicSight.prototype._testPoint", - _testPoint, - libWrapper.WRAPPER, - { perf_mode: libWrapper.PERF_FAST } - ); - - const los = Symbol("los"); - - libWrapper.register( - MODULE_ID, - "VisionSource.prototype._createRestrictedPolygon", - function (wrapped, ...args) { - [this.los, this[los]] = [Constraint.apply( - this.los, - `sight.${DetectionMode.BASIC_MODE_ID}`, - { scalingFactor: 100 } - ), this.los]; - - const fov = wrapped(...args); - - [this.los, this[los]] = [this[los], this.los]; - - return fov; - }, - libWrapper.WRAPPER, - { perf_mode: libWrapper.PERF_FAST } - ); - - libWrapper.register( - MODULE_ID, - "CanvasVisibility.prototype.refreshVisibility", - function (wrapped, ...args) { - for (const visionSource of canvas.effects.visionSources) { - [visionSource.los, visionSource[los]] = [visionSource[los], visionSource.los]; - } - - wrapped(...args); - - for (const visionSource of canvas.effects.visionSources) { - [visionSource.los, visionSource[los]] = [visionSource[los], visionSource.los]; - } - }, - libWrapper.WRAPPER, - { perf_mode: libWrapper.PERF_FAST } - ); - } -}); diff --git a/scripts/patches/sound.mjs b/scripts/patches/sound.mjs deleted file mode 100644 index bc19783..0000000 --- a/scripts/patches/sound.mjs +++ /dev/null @@ -1,56 +0,0 @@ -import { MODULE_ID } from "../const.mjs"; -import { Constraint } from "./../constraint.mjs"; -import { VolumeCollection } from "./../volume.mjs"; - -Hooks.once("libWrapper.Ready", () => { - libWrapper.register( - MODULE_ID, - "SoundSource.prototype._createPolygon", - function (wrapped, ...args) { - return Constraint.apply(wrapped(...args), "sound", { scalingFactor: 100 }); - }, - libWrapper.WRAPPER, - { perf_mode: libWrapper.PERF_FAST } - ); - - libWrapper.register( - MODULE_ID, - "SoundsLayer.prototype._syncPositions", - function (listeners, options) { - if (!this.placeables.length || game.audio.locked) return; - const sounds = {}; - for (let sound of this.placeables) { - const p = sound.document.path; - const r = sound.radius; - if (!p) continue; - - // Track one audible object per unique sound path - if (!(p in sounds)) sounds[p] = { path: p, audible: false, volume: 0, sound }; - const s = sounds[p]; - if (!sound.isAudible) continue; // The sound may not be currently audible - - // Determine whether the sound is audible, and its greatest audible volume - for (let l of listeners) { - if (!sound.source.active || !sound.source.shape?.contains(l.x, l.y)) continue; - s.audible = true; - let volume = sound.document.volume; - if (sound.document.easing) { - const soundZ = sound.source.elevation * canvas.dimensions.distancePixels; - const distance = Math.hypot(l.x - sound.x, l.y - sound.y, (l.z ?? soundZ) - soundZ); - const remainingEnergy = VolumeCollection.instance.castRay( - "sound", 0, sound.x, sound.y, soundZ, l.x, l.y, l.z ?? soundZ).remainingEnergy; - volume *= this._getEasingVolume(distance, r) * remainingEnergy; - } - if (!s.volume || (volume > s.volume)) s.volume = volume; - } - } - - // For each audible sound, sync at the target volume - for (let s of Object.values(sounds)) { - s.sound.sync(s.audible, s.volume, options); - } - }, - libWrapper.OVERRIDE, - { perf_mode: libWrapper.PERF_FAST } - ); -}); diff --git a/scripts/raycast/_module.mjs b/scripts/raycast/_module.mjs new file mode 100644 index 0000000..ea0c303 --- /dev/null +++ b/scripts/raycast/_module.mjs @@ -0,0 +1,13 @@ +export { default as Boundary } from "./boundary.mjs"; +export { default as Geometry } from "./geometry.mjs"; +export { default as Hit } from "./hit.mjs"; +export { default as Mode } from "./mode.mjs"; +export { default as Ray } from "./ray.mjs"; +export { default as Cast } from "./cast.mjs"; +export { default as Shape } from "./shape.mjs"; +export { default as Space } from "./space.mjs"; +export { default as Volume } from "./volume.mjs"; + +export * as boundaries from "./boundaries/_module.mjs"; +export * as shapes from "./shapes/_module.mjs"; +export * as types from "./_types.mjs"; diff --git a/scripts/raycast/_types.mjs b/scripts/raycast/_types.mjs new file mode 100644 index 0000000..bf63992 --- /dev/null +++ b/scripts/raycast/_types.mjs @@ -0,0 +1,9 @@ +/** + * @typedef {number & {}} int31 + * A 31-bit integer. + */ + +/** + * @typedef {number & {}} int32 + * A 32-bit integer. + */ diff --git a/scripts/raycast/boundaries/_module.mjs b/scripts/raycast/boundaries/_module.mjs new file mode 100644 index 0000000..aba980e --- /dev/null +++ b/scripts/raycast/boundaries/_module.mjs @@ -0,0 +1,2 @@ +export { default as Region } from "./region.mjs"; +export { default as Universe } from "./universe.mjs"; diff --git a/scripts/raycast/boundaries/region.mjs b/scripts/raycast/boundaries/region.mjs new file mode 100644 index 0000000..c160522 --- /dev/null +++ b/scripts/raycast/boundaries/region.mjs @@ -0,0 +1,218 @@ +import Boundary from "../boundary.mjs"; +import { max, min } from "../math.mjs"; +import Shape from "../shape.mjs"; +import Universe from "./universe.mjs"; + +/** + * @import { int32 } from "../_types.mjs"; + */ + +/** + * @sealed + */ +export default class Region extends Boundary { + /** + * @param {object} args + * @param {Shape[]} args.shapes - The shapes (nonempty). + * @param {number} [args.bottom=-Infinity] - The bottom (minimum z-coordinate). + * @param {number} [args.top=Infinity] - The top (maximum z-coordinate). + * @param {int32} [args.mask=-1] - The bit mask (nonzero 32-bit). + * @param {int32} [args.state=-1] - The bit state (32-bit). + * @returns {Region} The region. + */ + static create({ shapes, bottom = -Infinity, top = Infinity, mask = -1, state = -1 }) { + console.assert(Array.isArray(shapes)); + console.assert(shapes.every((shape) => shape instanceof Shape && shape.mask !== 0)); + console.assert(shapes.length !== 0); + console.assert(typeof bottom === "number"); + console.assert(typeof top === "number"); + console.assert(bottom <= top); + console.assert(mask === (mask | 0) && mask !== 0); + console.assert(state === (state | 0)); + + return new Region(shapes.toSorted(compareShapesByType), bottom + 0.0, top + 0.0, mask | 0, state | 0); + } + + /** + * @param {Shape[]} shapes - The shapes (nonempty). + * @param {number} bottom - The bottom (minimum z-coordinate). + * @param {number} top - The top (maximum z-coordinate). + * @param {int32} mask - The bit mask (nonzero 32-bit integer). + * @param {int32} state - The bit state (32-bit integer). + * @private + * @ignore + */ + constructor(shapes, bottom, top, mask, state) { + super(mask, state); + + /** + * The shapes. + * @type {ReadonlyArray} + * @readonly + */ + this.shapes = shapes; + + /** + * The bottom (minimum z-coordinate). + * @type {number} + * @readonly + */ + this.bottom = bottom; + + /** + * The top (maximum z-coordinate). + * @type {number} + * @readonly + */ + this.top = top; + } + + /** @inheritDoc */ + get isUnbounded() { + return this.state === 0 && this.shapes.length === 0; + } + + /** + * @param {number} minX - The minimum x-coordinate. + * @param {number} minY - The minimum y-coordinate. + * @param {number} minZ - The minimum z-coordinate. + * @param {number} maxX - The maximum x-coordinate. + * @param {number} maxY - The maximum y-coordinate. + * @param {number} maxZ - The maximum z-coordinate. + * @returns {Boundary} The cropped boundary. + * @inheritDoc + */ + crop(minX, minY, minZ, maxX, maxY, maxZ) { + if (max(this.bottom, minZ) > min(this.top, maxZ)) { + return Universe.EMPTY; + } + + const shapes = this.shapes; + const numShapes = shapes.length; + let croppedState = this.state; + + for (let shapeIndex = 0; shapeIndex < numShapes; shapeIndex++) { + const shape = shapes[shapeIndex]; + const result = shape.testBounds(minX, minY, maxX, maxY); + + if (result < 0) { + continue; + } + + if (result > 0) { + croppedState ^= shape.mask; + + continue; + } + + CROPPED_SHAPES.push(shape); + } + + if (CROPPED_SHAPES.length === numShapes) { + CROPPED_SHAPES.length = 0; + + return this; + } + + const { bottom, top } = this; + + if (CROPPED_SHAPES.length === 0) { + if (bottom <= minZ && maxZ <= top) { + croppedState ^= 1 << 31; + } + + return croppedState === 0 ? Universe.get(this.mask) : Universe.EMPTY; + } + + const croppedShapes = CROPPED_SHAPES.slice(0); + + CROPPED_SHAPES.length = 0; + + return new Region(croppedShapes, bottom, top, this.mask, croppedState); + } + + /** + * @param {Cast} cast - The cast. + * @inheritDoc + */ + computeHits(cast) { + const { originZ, invDirectionZ } = cast; + + if (invDirectionZ !== Infinity) { + const time1 = (this.bottom - originZ) * invDirectionZ; + + if (time1 > 0.0) { + cast.addHit(time1, 1 << 31); + } + + const time2 = (this.top - originZ) * invDirectionZ; + + if (time2 > 0.0) { + cast.addHit(time2, 1 << 31); + } + } else if (this.bottom <= originZ && originZ <= this.top) { + cast.addHit(Infinity, 1 << 31); + } + + const shapes = this.shapes; + const numShapes = shapes.length; + const { directionX, directionY } = cast; + + if (directionX !== 0.0 || directionY !== 0.0) { + for (let shapeIndex = 0; shapeIndex < numShapes; shapeIndex++) { + const shape = shapes[shapeIndex]; + + shape.computeHits(cast); + } + } else { + const { originX, originY } = cast; + + for (let shapeIndex = 0; shapeIndex < numShapes; shapeIndex++) { + const shape = shapes[shapeIndex]; + + if (shape.containsPoint(originX, originY)) { + cast.addHit(Infinity, shape.mask); + } + } + } + } +} + +/** + * The array for cropped shapes. + * @type {Shape[]} + */ +const CROPPED_SHAPES = []; + +/** + * The shape type to ID map. + * @type {Map} + */ +const SHAPE_TYPE_IDS = new Map(); + +/** + * Get the ID of the shape's type. + * @param {Shape} shape - The shape. + * @returns {int32} The shape type ID. + */ +function getShapeTypeID(shape) { + const shapeType = shape.constructor; + let id = SHAPE_TYPE_IDS.get(shapeType); + + if (id === undefined) { + id = SHAPE_TYPE_IDS.size; + SHAPE_TYPE_IDS.set(shapeType, id); + } + + return id; +} + +/** + * Compare two shapes by type. + * @param {Shape} shape1 - The first shape. + * @param {Shape} shape2 - The second shape. + * @returns {number} + */ +function compareShapesByType(shape1, shape2) { + return getShapeTypeID(shape1) - getShapeTypeID(shape2); +} diff --git a/scripts/raycast/boundaries/universe.mjs b/scripts/raycast/boundaries/universe.mjs new file mode 100644 index 0000000..b701e0b --- /dev/null +++ b/scripts/raycast/boundaries/universe.mjs @@ -0,0 +1,77 @@ +import Boundary from "../boundary.mjs"; + +/** + * @import { int32 } from "../_types.mjs"; + */ + +/** + * @sealed + */ +export default class Universe extends Boundary { + /** + * The empty boundless boundary. + * @type {Universe} + * @readonly + */ + static EMPTY = new Universe(0); + + /** + * Get the boundless boundary for the given mask. + * @param {int32} mask - The bit mask (32-bit integer). + * @returns {Universe} The boundless boundary. + */ + static get(mask) { + let boundary = CACHE.get(mask); + + if (!boundary) { + console.assert(mask === (mask | 0)); + + boundary = new Universe(mask); + CACHE.set(mask, boundary); + } + + return boundary; + } + + /** + * @param {int32} mask - The bit mask (32-bit integer). + * @private + * @ignore + */ + constructor(mask) { + super(mask, 0); + } + + /** @inheritDoc */ + get isUnbounded() { + return true; + } + + /** @inheritDoc */ + get isEmpty() { + return this.mask === 0; + } + + /** + * @param {number} minX - The minimum x-coordinate. + * @param {number} minY - The minimum y-coordinate. + * @param {number} minZ - The minimum z-coordinate. + * @param {number} maxX - The maximum x-coordinate. + * @param {number} maxY - The maximum y-coordinate. + * @param {number} maxZ - The maximum z-coordinate. + * @returns {Boundary} The cropped boundary. + * @inheritDoc + */ + crop(minX, minY, minZ, maxX, maxY, maxZ) { + return this; + } + + /** + * @param {Cast} cast - The cast. + * @inheritDoc + */ + computeHits(cast) { } +} + +/** @type {Map} */ +const CACHE = new Map([[0, Universe.EMPTY]]); diff --git a/scripts/raycast/boundary.mjs b/scripts/raycast/boundary.mjs new file mode 100644 index 0000000..3eb5f77 --- /dev/null +++ b/scripts/raycast/boundary.mjs @@ -0,0 +1,78 @@ +/** + * @import { int32 } from "./_types.mjs"; + * @import Geometry from "./geometry.mjs"; + * @import Cast from "./cast.mjs"; + */ + +/** + * The boundary of a {@link Geometry}. + * @abstract + */ +export default class Boundary { + /** + * @param {int32} mask - The bit mask (32-bit). + * @param {int32} state - The bit state (32-bit). + */ + constructor(mask, state) { + /** + * The bit mask of the boundary (32-bit). + * @type {int32} + * @readonly + */ + this.mask = mask; + + /** + * The initial state of the ray relative to the interior of the boundary. + * @type {int32} + * @readonly + */ + this.state = state; + + /** + * The current state of the ray relative to the interior of the boundary. + * If zero, the ray is currently inside the interior enclosed by the boundary. + * @type {int32} + * @internal + */ + this._state = 0; + } + + /** + * Is this boundary unbounded w.r.t. to the bounding box of the space? + * @type {boolean} + */ + get isUnbounded() { + return false; + } + + /** + * Can this boundary be discarded as it wouldn't affect rays at all? + * @type {boolean} + * @sealed + */ + get isEmpty() { + return this.mask === 0 || this.state !== 0 && this.isUnbounded; + } + + /** + * Crop the boundary w.r.t. to the bounding box of the space. + * @param {number} minX - The minimum x-coordinate. + * @param {number} minY - The minimum y-coordinate. + * @param {number} minZ - The minimum z-coordinate. + * @param {number} maxX - The maximum x-coordinate. + * @param {number} maxY - The maximum y-coordinate. + * @param {number} maxZ - The maximum z-coordinate. + * @returns {Boundary} The cropped boundary. + * @abstract + */ + crop(minX, minY, minZ, maxX, maxY, maxZ) { + return this; + } + + /** + * Compute the hits of the boundary with the ray. + * @param {Cast} cast - The cast. + * @abstract + */ + computeHits(cast) { } +} diff --git a/scripts/raycast/cast.mjs b/scripts/raycast/cast.mjs new file mode 100644 index 0000000..fd9c773 --- /dev/null +++ b/scripts/raycast/cast.mjs @@ -0,0 +1,344 @@ +import Hit from "./hit.mjs"; + +/** + * @import { int32 } from "./_types.mjs"; + * @import Boundary from "./boundary.mjs"; + * @import Geometry from "./geometry.mjs"; + * @import Ray from "./ray.mjs"; + */ + +/** + * @sealed + */ +export default class Cast { + /** + * @returns {Cast} The cast. + */ + static create() { + return new Cast(64); + } + + /** + * @param {number} numHits - Initial number of allocated hits. + * @internal + * @ignore + */ + constructor(numHits) { + const hits = this._hits; + + for (let j = 0; j < numHits; j++) { + hits.push(new Hit()); + } + } + + /** + * The x-coordinate of the origin of the ray. + * @type {number} + * @readonly + */ + originX = 0.0; + + /** + * The y-coordinate of the origin of the ray. + * @type {number} + * @readonly + */ + originY = 0.0; + + /** + * The z-coordinate of the origin of the ray. + * @type {number} + * @readonly + */ + originZ = 0.0; + + /** + * The x-coordinate of the direction of the ray. + * @type {number} + * @readonly + */ + directionX = 0.0; + + /** + * The y-coordinate of the direction of the ray. + * @type {number} + * @readonly + */ + directionY = 0.0; + + /** + * The z-coordinate of the direction of the ray. + * @type {number} + * @readonly + */ + directionZ = 0.0; + + /** + * The inverse x-coordinate of the direction of the ray. + * @type {number} + * @readonly + */ + invDirectionX = Infinity; + + /** + * The inverse y-coordinate of the direction of the ray. + * @type {number} + * @readonly + */ + invDirectionY = Infinity; + + /** + * The inverse z-coordinate of the direction of the ray. + * @type {number} + * @readonly + */ + invDirectionZ = Infinity; + + /** + * The pool of hits. + * @type {Hit[]} + * @readonly + * @private + * @ignore + */ + _hits = []; + + /** + * The number of hits of the pool that are currently used. + * @type {number} + * @private + * @ignore + */ + _hitsUsed = 0; + + /** + * The number of hits remaining in the heap. + * @type {number} + * @private + * @ignore + */ + _hitsRemaining = 0; + + /** + * The current geometry instance. + * @type {Geometry | null} + * @private + * @ignore + */ + _geometry = null; + + /** + * The current boundary instance. + * @type {Boundary | null} + * @private + * @ignore + */ + _boundary = null; + + /** + * Record a hit with the boundary of the geometry. + * This function can be called only during {@link Cast#computeHits}. + * @param {number} time - The time the ray hits the boundary of the geometry (nonnegative). + * @param {int32} mask - The bit mask indicating which parts of the boundary were hit (nonzero 32-bit integer). + */ + addHit(time, mask) { + const boundary = this._boundary; + + boundary._state ^= mask; + + if (time > 1.0) { + return; + } + + const hits = this._hits; + const i = this._hitsUsed++; + + if (i === hits.length) { + for (let j = i; j > 0; j--) { + hits.push(new Hit()); + } + } + + const hit = hits[i]; + + hit.geometry = this._geometry; + hit.boundary = boundary; + hit.time = time; + hit.mask = mask; + } + + /** + * Compute the hits of the ray. + * Resets the cast before computing the hits. + * @param {Ray} ray - The ray. + */ + computeHits(ray) { + this.reset(); + + CAST_ID++; + + const originX = (ray.originX + 6755399441055744.0) - 6755399441055744.0; + const originY = (ray.originY + 6755399441055744.0) - 6755399441055744.0; + const originZ = (ray.originZ + 6755399441055744.0) - 6755399441055744.0; + const targetX = (ray.targetX + 6755399441055744.0) - 6755399441055744.0; + const targetY = (ray.targetY + 6755399441055744.0) - 6755399441055744.0; + const targetZ = (ray.targetZ + 6755399441055744.0) - 6755399441055744.0; + const directionX = targetX - originX; + const directionY = targetY - originY; + const directionZ = targetZ - originZ; + + this.originX = originX; + this.originY = originY; + this.originZ = originZ; + this.directionX = directionX; + this.directionY = directionY; + this.directionZ = directionZ; + this.invDirectionX = 1.0 / directionX; + this.invDirectionY = 1.0 / directionY; + this.invDirectionZ = 1.0 / directionZ; + + const { minRange, maxRange, targetDistance } = ray; + + if (minRange < targetDistance) { + this._hits[this._hitsUsed++].time = minRange / targetDistance; + } + + if (maxRange < targetDistance) { + this._hits[this._hitsUsed++].time = maxRange / targetDistance; + } + + const volumes = ray.space.volumes; + const numVolumes = volumes.length; + + for (let volumeIndex = 0; volumeIndex < numVolumes; volumeIndex++) { + const geometry = volumes[volumeIndex].geometry; + + if (geometry._castId === CAST_ID) { + continue; + } + + geometry._castId = CAST_ID; + + this._geometry = geometry; + + const boundaries = geometry.boundaries; + const numBoundaries = boundaries.length; + let state = geometry.state; + + for (let boundaryIndex = 0; boundaryIndex < numBoundaries; boundaryIndex++) { + const boundary = boundaries[boundaryIndex]; + + this._boundary = boundary; + + boundary._state = boundary.state; + boundary.computeHits(this); + + if (boundary._state === 0) { + state ^= boundary.mask; + } + } + + geometry._state = state; + } + + this._geometry = null; + this._boundary = null; + + const hits = this._hits; + const numHits = this._hitsUsed; + + this._hitsRemaining = numHits; + + for (let i = numHits >> 1; i--;) { + siftDown(hits, numHits, hits[i], i); + } + } + + /** + * Get the next this that needs to be processed. + * @returns {Hit} The next hit if there is still one. The returned hit is owned by the cast and becomes invalid once the cast is reset. + */ + nextHit() { + const hits = this._hits; + let numHits = this._hitsRemaining; + + if (numHits === 0) { + return; + } + + numHits--; + this._hitsRemaining = numHits; + + const nextHit = hits[0]; + + if (numHits !== 0) { + const lastHit = hits[numHits]; + + hits[numHits] = nextHit; + siftDown(hits, numHits, lastHit, 0); + } + + return nextHit; + } + + /** + * Reset the cast. + * Invalidates all hit instances. + */ + reset() { + const hits = this._hits; + const numHits = this._hitsUsed; + + this._hitsUsed = 0; + this._hitsRemaining = 0; + + for (let i = 0; i < numHits; i++) { + const hit = hits[i]; + + hit.geometry = null; + hit.boundary = null; + } + } +} + +/** + * The last cast ID. + * @type {int32} + */ +let CAST_ID = 0; + +/** + * Sift down the hit. + * @param {Hit[]} hits - The hits. + * @param {number} n - The number of hits. + * @param {Hit} hit - The hit. + * @param {number} i - The current index of the hit. + */ +function siftDown(hits, n, hit, i) { + for (; ;) { + const r = i + 1 << 1; + const l = r - 1; + let j = i; + let h = hit; + let temp; + + if (l < n && (temp = hits[l]).time < h.time) { + h = temp; + j = l; + } + + if (r < n && (temp = hits[r]).time < h.time) { + h = temp; + j = r; + } + + if (j === i) { + break; + } + + hits[i] = h; + i = j; + } + + hits[i] = hit; +} diff --git a/scripts/raycast/geometry.mjs b/scripts/raycast/geometry.mjs new file mode 100644 index 0000000..b4e0a97 --- /dev/null +++ b/scripts/raycast/geometry.mjs @@ -0,0 +1,210 @@ +import Boundary from "./boundary.mjs"; + +/** + * @import { int32 } from "./_types.mjs"; + * @import Volume from "./volume.mjs"; + */ + +/** + * The next geometry ID. + * @type {int32} + */ +let ID = 0; + +/** + * The geometry of a {@link Volume}. + * @sealed + */ +export default class Geometry { + /** + * An empty geometry. + * @type {Geometry} + * @readonly + */ + static EMPTY = new Geometry([], -1); + + /** + * An unbounded geometry. + * @type {Geometry} + * @readonly + */ + static UNBOUNDED = new Geometry([], 0); + + /** + * @param {object} args + * @param {Boundary[]} args.boundaries - The boundaries. + * @param {int32} [args.state=-1] - The bit state (32-bit integer). + * @returns {Geometry} The geometry. + */ + static create({ boundaries, state = -1 }) { + console.assert(Array.isArray(boundaries)); + console.assert(boundaries.every((boundary) => boundary instanceof Boundary && boundary.mask !== 0)); + console.assert(state === (state | 0)); + + return new Geometry(boundaries.toSorted(compareBoundariesByType), state); + } + + /** + * @param {Boundary[]} boundaries - The boundaries. + * @param {int32} state - The bit state (32-bit integer). + * @private + * @ignore + */ + constructor(boundaries, state) { + /** + * The boundaries. + * @type {ReadonlyArray} + * @readonly + */ + this.boundaries = boundaries; + + /** + * The initial state of the ray relative to this geometry. + * @type {int32} + * @readonly + */ + this.state = state; + + /** + * The current state of the ray relative to this geometry. + * If zero, the ray is currently inside the geometry. + * @type {int32} + * @internal + * @ignore + */ + this._state = 0; + + /** + * The ID of the geometry. + * @type {int32} + * @readonly + * @internal + * @ignore + */ + this._id = ID++; + + /** + * The ray cast ID, which used to track whether hits were already computed for this geometry. + * Also used to determine whether the cropped geometry of this geometry was already created; + * in this case negative number are used. + * @type {int32} + * @internal + * @ignore + */ + this._castId = 0; + } + + /** + * Is this geometry unbounded w.r.t. the space? + * @type {boolean} + */ + get isUnbounded() { + return this.state === 0 && this.boundaries.length === 0; + } + + /** + * Can this geometry be discarded as it wouldn't affect rays at all? + * @type {boolean} + */ + get isEmpty() { + return this.state !== 0 && this.boundaries.length === 0; + } + + /** + * Crop the geometry w.r.t. to the bounding box of the space. + * @param {number} minX - The minimum x-coordinate. + * @param {number} minY - The minimum y-coordinate. + * @param {number} minZ - The minimum z-coordinate. + * @param {number} maxX - The maximum x-coordinate. + * @param {number} maxY - The maximum y-coordinate. + * @param {number} maxZ - The maximum z-coordinate. + * @returns {Geometry} The cropped geometry. + */ + crop(minX, minY, minZ, maxX, maxY, maxZ) { + const boundaries = this.boundaries; + const numBoundaries = boundaries.length; + let croppedState = this.state; + + for (let boundaryIndex = 0; boundaryIndex < numBoundaries; boundaryIndex++) { + const boundary = boundaries[boundaryIndex]; + const croppedBoundary = boundary.crop(minX, minY, minZ, maxX, maxY, maxZ); + + if (croppedBoundary.isUnbounded) { + if (croppedBoundary.state === 0) { + croppedState ^= croppedBoundary.mask; + } + + continue; + } + + CROPPED_BOUNDARIES.push(croppedBoundary); + } + + if (CROPPED_BOUNDARIES.length === 0) { + return croppedState === 0 ? Geometry.UNBOUNDED : Geometry.EMPTY; + } + + if (CROPPED_BOUNDARIES.length === numBoundaries) { + let cropped = false; + + for (let boundaryIndex = 0; boundaryIndex < numBoundaries; boundaryIndex++) { + if (CROPPED_BOUNDARIES[boundaryIndex] !== boundaries[boundaryIndex]) { + cropped = true; + + break; + } + } + + if (!cropped) { + CROPPED_BOUNDARIES.length = 0; + + return this; + } + } + + const croppedBoundaries = CROPPED_BOUNDARIES.slice(0); + + CROPPED_BOUNDARIES.length = 0; + + return new Geometry(croppedBoundaries, croppedState); + } +} + +/** + * The array for cropped boundaries. + * @type {Boundary[]} + */ +const CROPPED_BOUNDARIES = []; + +/** + * The boundary type ID map. + * @type {Map} + */ +const BOUNDARY_TYPE_IDS = new Map(); + +/** + * Get the ID of the boundary's type. + * @param {Boundary} boundary - The boundary. + * @returns {int32} The boundary type ID. + */ +function getBoundaryTypeID(boundary) { + const boundaryType = boundary.constructor; + let id = BOUNDARY_TYPE_IDS.get(boundaryType); + + if (id === undefined) { + id = BOUNDARY_TYPE_IDS.size; + BOUNDARY_TYPE_IDS.set(boundaryType, id); + } + + return id; +} + +/** + * Compare two boundaries by type. + * @param {Boundary} boundary1 - The first boundary. + * @param {Boundary} boundary2 - The second boundary. + * @returns {number} + */ +function compareBoundariesByType(boundary1, boundary2) { + return getBoundaryTypeID(boundary1) - getBoundaryTypeID(boundary2); +} diff --git a/scripts/raycast/hit.mjs b/scripts/raycast/hit.mjs new file mode 100644 index 0000000..a816fc5 --- /dev/null +++ b/scripts/raycast/hit.mjs @@ -0,0 +1,39 @@ +/** + * @import { int32 } from "./_types.mjs"; + * @import Boundary from "./boundary.mjs"; + * @import Geometry from "./geometry.mjs"; + */ + +/** + * @sealed + * @hideconstructor + */ +export default class Hit { + /** + * The geometry that was hit. + * @type {Geometry | null} + * @readonly + */ + geometry = null; + + /** + * The boundary that was hit. + * @type {Boundary | null} + * @readonly + */ + boundary = null; + + /** + * The time the ray hits the boundary of the geometry. + * @type {number} + * @readonly + */ + time = 0.0; + + /** + * The bit mask indicating which part of the boundary were hit (32-bit). + * @type {int32} + * @readonly + */ + mask = 0; +} diff --git a/scripts/raycasting/math.mjs b/scripts/raycast/math.mjs similarity index 85% rename from scripts/raycasting/math.mjs rename to scripts/raycast/math.mjs index 9b5d9c9..2728b02 100644 --- a/scripts/raycasting/math.mjs +++ b/scripts/raycast/math.mjs @@ -3,6 +3,8 @@ * @param {number} x * @param {number} y * @returns {number} + * @internal + * @ignore */ export function min(x, y) { return x < y ? x : y; @@ -13,6 +15,8 @@ export function min(x, y) { * @param {number} x * @param {number} y * @returns {number} + * @internal + * @ignore */ export function max(x, y) { return x > y ? x : y; diff --git a/scripts/raycast/mode.mjs b/scripts/raycast/mode.mjs new file mode 100644 index 0000000..f705c00 --- /dev/null +++ b/scripts/raycast/mode.mjs @@ -0,0 +1,26 @@ +/** + * @enum {number & {}} + */ +export const Mode = Object.freeze({ + /** + * Add. + */ + ADD: 0, + + /** + * Minimize. + */ + MINIMIZE: 1, + + /** + * Maximize. + */ + MAXIMIZE: 2, + + /** + * Override. + */ + OVERRIDE: 3, +}); + +export default Mode; diff --git a/scripts/raycast/ray.mjs b/scripts/raycast/ray.mjs new file mode 100644 index 0000000..d1a46c0 --- /dev/null +++ b/scripts/raycast/ray.mjs @@ -0,0 +1,485 @@ +import Cast from "./cast.mjs"; +import { max, min } from "./math.mjs"; +import Space from "./space.mjs"; + +/** + * @import { int32 } from "./_types.mjs"; + */ + +/** + * @sealed + * @hideconstructor + */ +export default class Ray { + /** + * @returns {Ray} + */ + static create() { + return new Ray(); + } + + /** + * The space. + * @type {Space} + * @readonly + */ + space = Space.EMPTY; + + /** + * The minimum range. + * @type {number} + * @readonly + */ + minRange = 0.0; + + /** + * The maximum range. + * @type {number} + * @readonly + */ + maxRange = Infinity; + + /** + * The x-coordinate of the current origin. + * @type {number} + * @readonly + */ + originX = 0.0; + + /** + * The y-coordinate of the current origin. + * @type {number} + * @readonly + */ + originY = 0.0; + + /** + * The z-coordinate of the current origin. + * @type {number} + * @readonly + */ + originZ = 0.0; + + /** + * The x-coordinate of the current target. + * @type {number} + * @readonly + */ + targetX = 0.0; + + /** + * The y-coordinate of the current target. + * @type {number} + * @readonly + */ + targetY = 0.0; + + /** + * The z-coordinate of the current target. + * @type {number} + * @readonly + */ + targetZ = 0.0; + + /** + * The x-coordinate of the direction of the ray. + * @type {number} + */ + get directionX() { + return this.targetX - this.originX; + } + + /** + * The y-coordinate of the direction of the ray. + * @type {number} + */ + get directionY() { + return this.targetY - this.originY; + } + + /** + * The z-coordinate of the direction of the ray. + * @type {number} + */ + get directionZ() { + return this.targetZ - this.originZ; + } + + /** + * The distance from the origin to the target. + * @type {number} + */ + get targetDistance() { + let targetDistance = this._targetDistance; + + if (targetDistance < 0.0) { + targetDistance = this._targetDistance = Math.hypot(this.directionX, this.directionY, this.directionZ); + } + + return targetDistance; + } + + /** + * @type {number} + * @private + * @ignore + */ + _targetDistance = 0.0; + + /** + * Did the ray hit the target? + * @type {boolean} + */ + get targetHit() { + if (this._targetHit === 0) { + this._cast(false, false); + } + + return this._targetHit > 0; + } + + /** + * @type {-1|0|1} + * @private + * @ignore + */ + _targetHit = 0; + + /** + * The time that elapsed before the ray reached its destination. + * @type {number} + */ + get elapsedTime() { + if (this._elapsedTime < 0.0) { + this._cast(true, false); + } + + return this._elapsedTime; + } + + /** + * @type {number} + * @private + * @ignore + */ + _elapsedTime = -1.0; + + /** + * The remaining energy of the ray when it reached its destination. + * @type {number} + */ + get remainingEnergy() { + if (this._remainingEnergy < 0.0) { + this._cast(false, true); + } + + return this._remainingEnergy; + } + + /** + * @type {number} + * @private + * @ignore + */ + _remainingEnergy = -1.0; + + /** + * The distance that the ray travelled before it reached its destination. + * @type {number} + */ + get distanceTravelled() { + return this.targetDistance * this.elapsedTime; + } + + /** + * The x-coordinate of the destination of the curreny ray. + * @type {number} + */ + get destinationX() { + return this.originX + this.directionX * this.elapsedTime; + } + + /** + * The y-coordinate of the destination of the curreny ray. + * @type {number} + */ + get destinationY() { + return this.originY + this.directionY * this.elapsedTime; + } + + /** + * The z-coordinate of the destination of the curreny ray. + * @type {number} + */ + get destinationZ() { + return this.originZ + this.directionZ * this.elapsedTime; + } + + /** + * Set the space of the ray. + * @param {Space} space - The space. + * @returns {this} + */ + setSpace(space) { + if (this.space !== space) { + this.space = space; + + this._targetDistance = -1.0; + this._targetHit = 0; + this._elapsedTime = -1.0; + this._remainingEnergy = -1.0; + } + + return this; + } + + /** + * Set the ranges of the ray. + * @param {number} minRange - The minimum range within no energy is consumed. + * @param {number} maxRange - The maximum range that the ray can travel. + * @returns {this} + */ + setRange(minRange, maxRange) { + this.minRange = minRange; + this.maxRange = max(maxRange, minRange); + + this._targetDistance = -1.0; + this._targetHit = 0; + this._elapsedTime = -1.0; + this._remainingEnergy = -1.0; + + return this; + } + + /** + * Set the origin for the ray. + * @param {number} originX - The x-coordinate of the origin. + * @param {number} originY - The y-coordinate of the origin. + * @param {number} originZ - The z-coordinate of the origin. + * @returns {this} + */ + setOrigin(originX, originY, originZ) { + this.originX = originX; + this.originY = originY; + this.originZ = originZ; + + this._targetDistance = -1.0; + this._targetHit = 0; + this._elapsedTime = -1.0; + this._remainingEnergy = -1.0; + + return this; + } + + /** + * Set the target for the next ray casts. + * @param {number} targetX - The x-coordinate of the target. + * @param {number} targetY - The y-coordinate of the target. + * @param {number} targetZ - The z-coordinate of the target. + * @returns {this} + */ + setTarget(targetX, targetY, targetZ) { + this.targetX = targetX; + this.targetY = targetY; + this.targetZ = targetZ; + + this._targetDistance = -1.0; + this._targetHit = 0; + this._elapsedTime = -1.0; + this._remainingEnergy = -1.0; + + return this; + } + + /** + * Reset the ray. + */ + reset() { + this.space = Space.EMPTY; + this.minRange = 0.0; + this.maxRange = Infinity; + this.targetX = 0.0; + this.targetY = 0.0; + this.targetZ = 0.0; + this.originX = 0.0; + this.originY = 0.0; + this.originZ = 0.0; + + this._targetDistance = -1.0; + this._targetHit = 0; + this._elapsedTime = -1.0; + this._remainingEnergy = -1.0; + } + + /** + * Cast the ray from the origin to the target point. + * @param {boolean} computeElapsedTime - Compute the elapsed time of the ray. + * @param {boolean} computeRemainingEnergy - Compute the remaining energy of the ray. + * @private + * @ignore + */ + _cast(computeElapsedTime, computeRemainingEnergy) { + const { space, minRange, targetDistance } = this; + + if (targetDistance - minRange < space.minDistance + 0.5 / 256.0) { + this._targetHit = 1; + this._elapsedTime = 1.0; + + if (targetDistance <= minRange) { + this._remainingEnergy = 1.0; + + return; + } + + if (!computeRemainingEnergy) { + return; + } + } + + if (!computeElapsedTime && targetDistance > space.maxDistance + 0.5 / 256.0) { + this._targetHit = -1; + this._remainingEnergy = 0.0; + + return; + } + + CAST.computeHits(this); + + let stage = 0; + let currentTime = 0.0; + let currentCost = 0.0; + let remainingEnergy = 1.0 / targetDistance; + const almostZeroEnergy = remainingEnergy * 1e-12; + + for (; ;) { + const hit = CAST.nextHit(); + + if (!hit) { + break; + } + + const hitGeometry = hit.geometry; + + if (hitGeometry) { + const hitBoundary = hit.boundary; + const hitBoundaryState = hitBoundary._state; + + if ((hitBoundary._state = hitBoundaryState ^ hit.mask) !== 0 && hitBoundaryState !== 0) { + continue; + } + + const hitGeometryState = hitGeometry._state; + + if ((hitGeometry._state = hitGeometryState ^ hitBoundary.mask) !== 0 && hitGeometryState !== 0) { + continue; + } + } else { + stage++; + } + + const hitTime = hit.time; + const deltaTime = hitTime - currentTime; + const requiredEnergy = deltaTime > 0.0 ? deltaTime * min(currentCost, 256.0) : 0.0; + + if (remainingEnergy <= requiredEnergy) { + break; + } + + remainingEnergy -= requiredEnergy; + + currentTime = hitTime; + + if (stage === 0) { + currentCost = 0.0; + } else if (stage === 1) { + currentCost = calculateCost(this.space); + } else { + currentCost = 0.0; + + if (remainingEnergy <= almostZeroEnergy) { + remainingEnergy = 0.0; + } + + break; + } + + if (remainingEnergy <= almostZeroEnergy) { + remainingEnergy = 0.0; + + break; + } + } + + if (currentCost !== 0) { + const requiredEnergy = currentTime < 1.0 ? (1.0 - currentTime) * currentCost : 0.0; + + currentTime = min(currentTime + remainingEnergy / currentCost, 1.0); + remainingEnergy -= requiredEnergy; + + if (remainingEnergy <= almostZeroEnergy) { + remainingEnergy = 0.0; + } + } else if (remainingEnergy !== 0.0) { + currentTime = 1.0; + } + + if (currentTime * targetDistance > targetDistance - 0.5 / 256.0) { + currentTime = 1.0; + } + + this._targetHit = currentTime === 1.0 ? 1 : -1; + this._elapsedTime = currentTime; + this._remainingEnergy = min(remainingEnergy * targetDistance, 1.0); + + CAST.reset(); + } +} + +/** + * @type {Cast} + * @internal + * @ignore + */ +const CAST = new Cast(1024); + +/** + * Calculate the current energy cost. + * @param {Space} space - The space. + * @returns {number} The current energy cost. + */ +function calculateCost(space) { + let calculatedCost = 0.0; + const volumes = space.volumes; + const numVolumes = volumes.length; + + for (let volumeIndex = 0; volumeIndex < numVolumes; volumeIndex++) { + const volume = volumes[volumeIndex]; + + if (volume.geometry._state !== 0) { + continue; + } + + const cost = volume.cost; + + switch (volume.mode) { + case 0: + calculatedCost = max(calculatedCost + cost, 0.0); + + break; + case 1: + calculatedCost = min(calculatedCost, cost); + + break; + case 2: + calculatedCost = max(calculatedCost, cost); + + break; + case 3: + calculatedCost = cost; + + break; + } + } + + return calculatedCost; +} diff --git a/scripts/raycast/shape.mjs b/scripts/raycast/shape.mjs new file mode 100644 index 0000000..3ae1ba1 --- /dev/null +++ b/scripts/raycast/shape.mjs @@ -0,0 +1,54 @@ +/** + * @import { int31 } from "./_types.mjs"; + * @import Cast from "./cast.mjs"; + * @import Region from "./boundaries/region.mjs"; + */ + +/** + * The shape of a {@link Region}. + * @abstract + */ +export default class Shape { + /** + * @param {int31} mask - The mask (nonzero 31-bit integer). + */ + constructor(mask) { + /** + * The bit mask of the shape (31-bit). + * @type {int31} + * @readonly + */ + this.mask = mask; + } + + /** + * Test whether the shape contains (1) or not intersects (-1) the bounding box. + * @param {number} minX - The minimum x-coordinate. + * @param {number} minY - The minimum y-coordinate. + * @param {number} maxX - The maximum x-coordinate. + * @param {number} maxY - The maximum y-coordinate. + * @returns {-1|0|1} If 1, then the bounding box is contained in the shape. If -1, if the bounding box does not intersect with the shape. + */ + testBounds(minX, minY, maxX, maxY) { + return 0; + } + + /** + * Test whether the shape contains the point. + * @param {number} x - The x-coordinate of the point. + * @param {number} y - The y-coordinate of the point. + * @returns {boolean} True if the shape contains the point. + * @abstract + */ + containsPoint(x, y) { + return false; + } + + /** + * Compute the hits of the shape with the ray. + * This function is called only with nonzero x/y-direction. + * @param {Cast} cast - The cast. + * @abstract + */ + computeHits(cast) { } +} diff --git a/scripts/raycast/shapes/_module.mjs b/scripts/raycast/shapes/_module.mjs new file mode 100644 index 0000000..4a8dc0f --- /dev/null +++ b/scripts/raycast/shapes/_module.mjs @@ -0,0 +1,6 @@ +export { default as Bounds } from "./bounds.mjs"; +export { default as Circle } from "./circle.mjs"; +export { default as Ellipse } from "./ellipse.mjs"; +export { default as Polygon } from "./polygon.mjs"; +export { default as Rectangle } from "./rectangle.mjs"; +export { default as Tile } from "./tile.mjs"; diff --git a/scripts/raycast/shapes/bounds.mjs b/scripts/raycast/shapes/bounds.mjs new file mode 100644 index 0000000..0fe432f --- /dev/null +++ b/scripts/raycast/shapes/bounds.mjs @@ -0,0 +1,155 @@ +import Shape from "../shape.mjs"; +import { max, min } from "../math.mjs"; + +/** + * @import { int31 } from "../_types.mjs"; + * @import Cast from "../cast.mjs"; + */ + +/** + * @sealed + * @hideconstructor + */ +export default class Bounds extends Shape { + /** + * @param {object} args + * @param {number} args.minX - The minimum x-coordinate (finite). + * @param {number} args.minY - The minimum y-coordinate (finite). + * @param {number} args.maxX - The maximum x-coordinate (finite). + * @param {number} args.maxY - The maximum y-coordinate (finite). + * @param {int31} [args.mask=0x7FFFFFFF] - The mask (nonzero 31-bit integer). + * @returns {Bounds} The bounds. + */ + static create({ minX, minY, maxX, maxY, mask = 0x7FFFFFFF }) { + console.assert(typeof minX === "number"); + console.assert(typeof minY === "number"); + console.assert(typeof maxX === "number"); + console.assert(typeof maxY === "number"); + console.assert(Number.isFinite(minX)); + console.assert(Number.isFinite(minY)); + console.assert(Number.isFinite(maxX)); + console.assert(Number.isFinite(maxY)); + console.assert(minX < maxX); + console.assert(minY < maxY); + console.assert(mask === (mask & 0x7FFFFFFF) && mask !== 0); + + return new Bounds(mask | 0, minX + 0.0, minY + 0.0, maxX + 0.0, maxY + 0.0); + } + + /** + * @param {int31} mask - The mask (nonzero 31-bit integer). + * @param {number} minX - The minimum x-coordinate (finite). + * @param {number} minY - The minimum y-coordinate (finite). + * @param {number} maxX - The maximum x-coordinate (finite). + * @param {number} maxY - The maximum y-coordinate (finite). + * @private + * @ignore + */ + constructor(mask, minX, minY, maxX, maxY) { + super(mask); + + /** + * The minimum x-coordinate (finite). + * @type {number} + * @readonly + * @private + * @ignore + */ + this._minX = minX; + + /** + * The minimum y-coordinate (finite). + * @type {number} + * @readonly + * @private + * @ignore + */ + this._minY = minY; + + /** + * The maximum x-coordinate (finite). + * @type {number} + * @readonly + * @private + * @ignore + */ + this._maxX = maxX; + + /** + * The maximum y-coordinate (finite). + * @type {number} + * @readonly + * @private + * @ignore + */ + this._maxY = maxY; + } + + /** + * @param {number} minX - The minimum x-coordinate. + * @param {number} minY - The minimum y-coordinate. + * @param {number} maxX - The maximum x-coordinate. + * @param {number} maxY - The maximum y-coordinate. + * @returns {-1|0|1} If 1, then the bounding box is contained in the shape. If -1, if the bounding box does not intersect with the shape. + * @inheritDoc + */ + testBounds(minX, minY, maxX, maxY) { + const x0 = this._minX; + const x1 = this._maxX; + + if (max(x0, minX) > min(x1, maxX)) { + return -1; + } + + const y0 = this._minY; + const y1 = this._maxY; + + if (max(y0, minY) > min(y1, maxY)) { + return -1; + } + + if (x0 > minX || maxX > x1 || y0 > minY || maxY > y1) { + return 0; + } + + return 1; + } + + /** + * @param {number} x - The x-coordinate of the point. + * @param {number} y - The y-coordinate of the point. + * @returns {boolean} True if the shape contains the point. + * @inheritDoc + */ + containsPoint(x, y) { + return this._minX <= x && x <= this._maxX && this._minY <= y && y <= this._maxY; + } + + /** + * @param {Cast} cast - The cast. + * @inheritDoc + */ + computeHits(cast) { + const { originX, originY, invDirectionX, invDirectionY } = cast; + + let t1 = (this._minX - originX) * invDirectionX; + let t2 = (this._maxX - originX) * invDirectionX; + let time1 = min(max(t1, 0.0), max(t2, 0.0)); + let time2 = max(min(t1, Infinity), min(t2, Infinity)); + + t1 = (this._minY - originY) * invDirectionY; + t2 = (this._maxY - originY) * invDirectionY; + time1 = min(max(t1, time1), max(t2, time1)); + time2 = max(min(t1, time2), min(t2, time2)); + + if (time1 <= min(time2, 1.0)) { + const mask = this.mask; + + if (time1 > 0) { + cast.addHit(time1, mask); + } + + cast.addHit(time2, mask); + } + } +} diff --git a/scripts/raycast/shapes/circle.mjs b/scripts/raycast/shapes/circle.mjs new file mode 100644 index 0000000..7287202 --- /dev/null +++ b/scripts/raycast/shapes/circle.mjs @@ -0,0 +1,192 @@ +import Shape from "../shape.mjs"; + +/** + * @import { int31 } from "../_types.mjs"; + * @import Cast from "../cast.mjs"; + */ + +/** + * @sealed + */ +export default class Circle extends Shape { + /** + * @param {object} args + * @param {number} args.centerX - The x-coordinate of the center. + * @param {number} args.centerY - The y-coordinate of the center. + * @param {number} args.radius - The radius (positive). + * @param {int31} [args.mask=0x7FFFFFFF] - The mask (nonzero 31-bit integer). + * @returns {Circle} The circle. + */ + static create({ centerX, centerY, radius, mask = 0x7FFFFFFF }) { + console.assert(typeof centerX === "number"); + console.assert(typeof centerY === "number"); + console.assert(typeof radius === "number"); + console.assert(Number.isFinite(centerX)); + console.assert(Number.isFinite(centerY)); + console.assert(Number.isFinite(radius)); + console.assert(radius > 0); + console.assert(mask === (mask & 0x7FFFFFFF) && mask !== 0); + + return new Circle(mask | 0, centerX + 0.0, centerY + 0.0, radius + 0.0); + } + + /** + * @param {int31} mask - The mask (nonzero 31-bit integer). + * @param {number} centerX - The x-coordinate of the center. + * @param {number} centerY - The y-coordinate of the center. + * @param {number} radius - The radius (finite, positive). + * @private + * @ignore + */ + constructor(mask, centerX, centerY, radius) { + super(mask); + + /** + * The x-coordinate of the center. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._centerX = centerX; + + /** + * The y-coordinate of the center. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._centerY = centerY; + + /** + * The radius. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._radius = radius; + } + + /** + * @param {number} minX - The minimum x-coordinate. + * @param {number} minY - The minimum y-coordinate. + * @param {number} maxX - The maximum x-coordinate. + * @param {number} maxY - The maximum y-coordinate. + * @returns {-1|0|1} If 1, then the bounding box is contained in the shape. If -1, if the bounding box does not intersect with the shape. + * @inheritDoc + */ + testBounds(minX, minY, maxX, maxY) { + const radius = this._radius; + const centerX = this._centerX; + + if (centerX + radius < minX || maxX < centerX - radius) { + return -1; + } + + const centerY = this._centerY; + + if (centerY + radius < minY || maxY < centerY - radius) { + return -1; + } + + if (centerX - radius > minX || maxX > centerX + radius || centerY - radius > minY || maxY > centerY + radius) { + return 0; + } + + const radiusSquared = radius * radius; + + let x = minX - centerX; + let y = minY - centerY; + + if (x * x + y * y > radiusSquared) { + return 0; + } + + x = maxX - centerX; + y = minY - centerY; + + if (x * x + y * y > radiusSquared) { + return 0; + } + + x = maxX - centerX; + y = maxY - centerY; + + if (x * x + y * y > radiusSquared) { + return 0; + } + + x = minX - centerX; + y = maxY - centerY; + + if (x * x + y * y > radiusSquared) { + return 0; + } + + return 1; + } + + /** + * @param {number} x - The x-coordinate of the point. + * @param {number} y - The y-coordinate of the point. + * @returns {boolean} True if the shape contains the point. + * @inheritDoc + */ + containsPoint(x, y) { + x -= this._centerX; + y -= this._centerY; + + const radius = this._radius; + + return x * x + y * y <= radius * radius; + } + + /** + * @param {Cast} cast - The cast. + * @inheritDoc + */ + computeHits(cast) { + const { originX, originY, directionX, directionY } = cast; + const invRadius = 1.0 / this._radius; + const x = (originX - this._centerX) * invRadius; + const y = (originY - this._centerY) * invRadius; + const dx = directionX * invRadius; + const dy = directionY * invRadius; + const a = dx * dx + dy * dy; + const b = dx * x + dy * y; + const c = x * x + y * y - 1; + + let time1 = 0.0; + let time2 = 0.0; + + if (c !== 0.0) { + const d = b * b - a * c; + + if (d <= 1e-6) { + return; + } + + const f = Math.sqrt(d); + + if (b !== 0.0) { + time1 = (-b - Math.sign(b) * f) / a; + time2 = c / (a * time1); + } else { + time1 = f / a; + time2 = -time1; + } + } else { + time2 = -b / a; + } + + if (time1 > 0.0) { + cast.addHit(time1, this.mask); + } + + if (time2 > 0.0) { + cast.addHit(time2, this.mask); + } + } +} diff --git a/scripts/raycast/shapes/ellipse.mjs b/scripts/raycast/shapes/ellipse.mjs new file mode 100644 index 0000000..5a1e1c9 --- /dev/null +++ b/scripts/raycast/shapes/ellipse.mjs @@ -0,0 +1,288 @@ +import Shape from "../shape.mjs"; +import { max, min } from "../math.mjs"; + +/** + * @import { int31 } from "../_types.mjs"; + * @import Cast from "../cast.mjs"; + */ + +/** + * @sealed + */ +export default class Ellipse extends Shape { + /** + * @param {object} args + * @param {number} args.centerX - The x-coordinate of the center. + * @param {number} args.centerY - The y-coordinate of the center. + * @param {number} args.radiusX - The x-radius (finite, positive). + * @param {number} args.radiusY - The y-radius (finite, positive). + * @param {number} [args.rotation=0.0] - The rotation in radians. + * @param {int31} [args.mask=0x7FFFFFFF] - The mask (nonzero 31-bit integer). + * @returns {Ellipse} The ellipse. + */ + static create({ centerX, centerY, radiusX, radiusY, rotation = 0.0, mask = 0x7FFFFFFF }) { + console.assert(typeof centerX === "number"); + console.assert(typeof centerY === "number"); + console.assert(typeof radiusX === "number"); + console.assert(typeof radiusY === "number"); + console.assert(typeof rotation === "number"); + console.assert(Number.isFinite(centerX)); + console.assert(Number.isFinite(centerY)); + console.assert(Number.isFinite(radiusX)); + console.assert(Number.isFinite(radiusY)); + console.assert(Number.isFinite(rotation)); + console.assert(radiusX > 0); + console.assert(radiusY > 0); + console.assert(mask === (mask & 0x7FFFFFFF) && mask !== 0); + + return new Ellipse(mask | 0, centerX + 0.0, centerY + 0.0, radiusX + 0.0, radiusY + 0.0, rotation + 0.0); + } + + /** + * @param {int31} mask - The mask (nonzero 31-bit integer). + * @param {number} centerX - The x-coordinate of the center. + * @param {number} centerY - The y-coordinate of the center. + * @param {number} radiusX - The x-radius (positive). + * @param {number} radiusY - The y-radius (positive). + * @param {number} rotation - The rotation in radians. + * @private + * @ignore + */ + constructor(mask, centerX, centerY, radiusX, radiusY, rotation) { + super(mask); + + const cos = Math.cos(rotation); + const sin = Math.sin(rotation); + const deltaX = Math.hypot(radiusX * cos, radiusY * sin); + const deltaY = Math.hypot(radiusX * sin, radiusY * cos); + const scaleX = cos / radiusX; + const skewX = -sin / radiusY; + const skewY = sin / radiusX; + const scaleY = cos / radiusY; + + /** + * The minimum x-coordinate. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._minX = centerX - deltaX; + + /** + * The minimum y-coordinate. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._minY = centerY - deltaY; + + /** + * The maximum x-coordinate. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._maxX = centerX + deltaX; + + /** + * The maximum y-coordinate. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._maxY = centerY + deltaY; + + /** + * The x-scale of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._scaleX = scaleX; + + /** + * The x-skew of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._skewX = skewX; + + /** + * The y-skew of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._skewY = skewY; + + /** + * The y-scale of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._scaleY = scaleY; + + /** + * The x-translation of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._translationX = -(centerX * scaleX + centerY * skewY); + + /** + * The y-translation of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._translationY = -(centerX * skewX + centerY * scaleY); + } + + /** + * @param {number} minX - The minimum x-coordinate. + * @param {number} minY - The minimum y-coordinate. + * @param {number} maxX - The maximum x-coordinate. + * @param {number} maxY - The maximum y-coordinate. + * @returns {-1|0|1} If 1, then the bounding box is contained in the shape. If -1, if the bounding box does not intersect with the shape. + * @inheritDoc + */ + testBounds(minX, minY, maxX, maxY) { + const x0 = this._minX; + const x1 = this._maxX; + + if (max(x0, minX) > min(x1, maxX)) { + return -1; + } + + const y0 = this._minY; + const y1 = this._maxY; + + if (max(y0, minY) > min(y1, maxY)) { + return -1; + } + + if (x0 > minX || maxX > x1 || y0 > minY || maxY > y1) { + return 0; + } + + const ma = this._scaleX; + const mb = this._skewX; + const mc = this._skewY; + const md = this._scaleY; + const mx = this._translationX; + const my = this._translationY; + + let x = ma * minX + mc * minY + mx; + let y = mb * minX + md * minY + my; + + if (x * x + y * y > 1.0) { + return 0; + } + + x = ma * maxX + mc * minY + mx; + y = mb * maxX + md * minY + my; + + if (x * x + y * y > 1.0) { + return 0; + } + + x = ma * maxX + mc * maxY + mx; + y = mb * maxX + md * maxY + my; + + if (x * x + y * y > 1.0) { + return 0; + } + + x = ma * minX + mc * maxY + mx; + y = mb * minX + md * maxY + my; + + if (x * x + y * y > 1.0) { + return 0; + } + + return 1; + } + + /** + * @param {number} x - The x-coordinate of the point. + * @param {number} y - The y-coordinate of the point. + * @returns {boolean} True if the shape contains the point. + * @inheritDoc + */ + containsPoint(x, y) { + const ma = this._scaleX; + const mb = this._skewX; + const mc = this._skewY; + const md = this._scaleY; + const mx = this._translationX; + const my = this._translationY; + const x0 = ma * x + mc * y + mx; + const y0 = mb * x + md * y + my; + + return x0 * x0 + y0 * y0 <= 1.0; + } + + /** + * @param {Cast} cast - The cast. + * @inheritDoc + */ + computeHits(cast) { + const { originX, originY, directionX, directionY } = cast; + const ma = this._scaleX; + const mb = this._skewX; + const mc = this._skewY; + const md = this._scaleY; + const mx = this._translationX; + const my = this._translationY; + const x = ma * originX + mc * originY + mx; + const y = mb * originX + md * originY + my; + const dx = ma * directionX + mc * directionY; + const dy = mb * directionX + md * directionY; + const a = dx * dx + dy * dy; + const b = dx * x + dy * y; + const c = x * x + y * y - 1.0; + let time1, time2; + + if (c !== 0.0) { + const d = b * b - a * c; + + if (d <= 1e-6) { + return; + } + + const f = Math.sqrt(d); + + if (b !== 0.0) { + time1 = (-b - Math.sign(b) * f) / a; + time2 = c / (a * time1); + } else { + time1 = f / a; + time2 = -time1; + } + } else { + time1 = 0.0; + time2 = -b / a; + } + + if (time1 > 0.0) { + cast.addHit(time1, this.mask); + } + + if (time2 > 0.0) { + cast.addHit(time2, this.mask); + } + } +} diff --git a/scripts/raycast/shapes/polygon.mjs b/scripts/raycast/shapes/polygon.mjs new file mode 100644 index 0000000..90d3cfd --- /dev/null +++ b/scripts/raycast/shapes/polygon.mjs @@ -0,0 +1,274 @@ +import Shape from "../shape.mjs"; +import { max, min } from "../math.mjs"; + +/** + * @import { int31 } from "../_types.mjs"; + * @import Cast from "../cast.mjs"; + */ + +/** + * @sealed + */ +export default class Polygon extends Shape { + /** + * @param {object} args + * @param {number} args.points - The points of the polygon (`[x0, y0, x1, y1, x2, y2, ...]`). + * @param {int31} [args.mask=0x7FFFFFFF] - The mask (nonzero 31-bit integer). + * @returns {Polygon} The polygon. + */ + static create({ points, mask = 0x7FFFFFFF }) { + console.assert(Array.isArray(points)); + console.assert(points.every((v) => typeof v === "number" && Number.isFinite(v))); + console.assert(points.length >= 6); + console.assert(points.length % 2 === 0); + console.assert(points.some((v, i) => i % 2 === 0 && v !== points[0])); + console.assert(points.some((v, i) => i % 2 === 1 && v !== points[1])); + console.assert(mask === (mask & 0x7FFFFFFF) && mask !== 0); + + const n = points.length; + const roundedPoints = new Float64Array(n); + + for (let i = 0; i < n; i++) { + roundedPoints[i] = (points[i] + 6755399441055744.0) - 6755399441055744.0; + } + + return new Polygon(mask | 0, roundedPoints); + } + + /** + * @param {int31} mask - The mask (nonzero 31-bit integer). + * @param {Float64Array} points - The points of the polygon (`[x0, y0, x1, y1, x2, y2, ...]`). + * @private + * @ignore + */ + constructor(mask, points) { + super(mask); + + const n = points.length; + let minX = points[0]; + let minY = points[1]; + let maxX = minX; + let maxY = minY; + + for (let i = 2; i < n; i += 2) { + const x = points[i]; + const y = points[i + 1]; + + minX = min(minX, x); + minY = min(minY, y); + maxX = max(maxX, x); + maxY = max(maxY, y); + } + + /** + * The points of the polygon. + * @type {Float64Array} + * @readonly + * @private + * @ignore + */ + this._points = points; + + /** + * The minimum x-coordinate. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._minX = minX; + + /** + * The minimum y-coordinate. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._minY = minY; + + /** + * The maximum x-coordinate. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._maxX = maxX; + + /** + * The maximum y-coordinate. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._maxY = maxY; + } + + /** + * @param {number} minX - The minimum x-coordinate. + * @param {number} minY - The minimum y-coordinate. + * @param {number} maxX - The maximum x-coordinate. + * @param {number} maxY - The maximum y-coordinate. + * @returns {-1|0|1} If 1, then the bounding box is contained in the shape. If -1, if the bounding box does not intersect with the shape. + * @inheritDoc + */ + testBounds(minX, minY, maxX, maxY) { + const x0 = this._minX; + const x1 = this._maxX; + + if (max(x0, minX) > min(x1, maxX)) { + return -1; + } + + const y0 = this._minY; + const y1 = this._maxY; + + if (max(y0, minY) > min(y1, maxY)) { + return -1; + } + + if (x0 > minX || maxX > x1 || y0 > minY || maxY > y1) { + return 0; + } + + const centerX = (minX + maxX) * 0.5; + const centerY = (minY + maxY) * 0.5; + const points = this._points; + const n = points.length; + let centerInside = false; + + for (let i = 0, x0 = points[n - 2], y0 = points[n - 1]; i < n; i += 2) { + const x1 = points[i]; + const y1 = points[i + 1]; + + if ((y1 > centerY) !== (y0 > centerY) && centerX < (x0 - x1) * ((centerY - y1) / (y0 - y1)) + x1) { + centerInside = !centerInside; + } + + x0 = x1; + y0 = y1; + } + + if (!centerInside) { + return 0; + } + + for (let i = 0, x0 = points[n - 2], y0 = points[n - 1]; i < n; i += 2) { + const x1 = points[i]; + const y1 = points[i + 1]; + const px = 1.0 / (x1 - x0); + const py = 1.0 / (y1 - y0); + + let t1 = (minX - x0) * px; + let t2 = (maxX - x0) * px; + let time1 = min(max(t1, 0.0), max(t2, 0.0)); + let time2 = max(min(t1, Infinity), min(t2, Infinity)); + + t1 = (minY - y0) * py; + t2 = (maxY - y0) * py; + time1 = min(max(t1, time1), max(t2, time1)); + time2 = max(min(t1, time2), min(t2, time2)); + + if (time1 <= min(time2, 1.0)) { + return 0; + } + + x0 = x1; + y0 = y1; + } + + return 1; + } + + /** + * @param {number} x - The x-coordinate of the point. + * @param {number} y - The y-coordinate of the point. + * @returns {boolean} True if the shape contains the point. + * @inheritDoc + */ + containsPoint(x, y) { + if (x < this._minX || x > this._maxX || y < this._minY || y > this._maxY) { + return false; + } + + const points = this._points; + const n = points.length; + let inside = false; + + for (let i = 0, x0 = points[n - 2], y0 = points[n - 1]; i < n; i += 2) { + const x1 = points[i]; + const y1 = points[i + 1]; + + if ((y1 > y) !== (y0 > y) && x < (x0 - x1) * ((y - y1) / (y0 - y1)) + x1) { + inside = !inside; + } + + x0 = x1; + y0 = y1; + } + + return inside; + } + + /** + * @param {Cast} cast - The cast. + * @inheritDoc + */ + computeHits(cast) { + const { originX, originY, invDirectionX, invDirectionY } = cast; + + let t1 = (this._minX - originX) * invDirectionX; + let t2 = (this._maxX - originX) * invDirectionX; + let time1 = min(max(t1, 0.0), max(t2, 0.0)); + let time2 = max(min(t1, Infinity), min(t2, Infinity)); + + t1 = (this._minY - originY) * invDirectionY; + t2 = (this._maxY - originY) * invDirectionY; + time1 = min(max(t1, time1), max(t2, time1)); + time2 = max(min(t1, time2), min(t2, time2)); + + if (time1 > min(time2, 1.0)) { + return; + } + + const { directionX, directionY } = cast; + const points = this._points; + const n = points.length; + let i = 0; + let x0 = points[n - 2]; + let y0 = points[n - 1]; + + do { + const x1 = points[i++]; + const y1 = points[i++]; + const dx = x1 - x0; + const dy = y1 - y0; + const q = directionX * dy - directionY * dx; + + while (q !== 0.0) { + const ox = x0 - originX; + const oy = y0 - originY; + const u = (ox * directionY - oy * directionX) / q; + + if (u < 0.0 || u > 1.0 || u === 0.0 && q > 0.0 || u === 1.0 && q < 0.0) { + break; + } + + const time = (ox * dy - oy * dx) / q; + + if (time <= 0.0) { + break; + } + + cast.addHit(time, this.mask); + + break; + } + + x0 = x1; + y0 = y1; + } while (i !== n); + } +} diff --git a/scripts/raycast/shapes/rectangle.mjs b/scripts/raycast/shapes/rectangle.mjs new file mode 100644 index 0000000..892d3ee --- /dev/null +++ b/scripts/raycast/shapes/rectangle.mjs @@ -0,0 +1,319 @@ +import Shape from "../shape.mjs"; +import { max, min } from "../math.mjs"; + +/** + * @import { int31 } from "../_types.mjs"; + * @import Cast from "../cast.mjs"; + */ + +/** + * @sealed + */ +export default class Rectangle extends Shape { + /** + * @param {object} args + * @param {number} args.centerX - The x-coordinate of the center. + * @param {number} args.centerY - The y-coordinate of the center. + * @param {number} args.width - The width (finite, positive). + * @param {number} args.height - The height (finite, positive). + * @param {number} [args.rotation=0.0] - The rotation in radians. + * @param {int31} [args.mask=0x7FFFFFFF] - The mask (nonzero 31-bit integer). + * @returns {Rectangle} The rectangle. + */ + static create({ centerX, centerY, width, height, rotation = 0.0, mask = 0x7FFFFFFF }) { + console.assert(typeof centerX === "number"); + console.assert(typeof centerY === "number"); + console.assert(typeof width === "number"); + console.assert(typeof height === "number"); + console.assert(typeof rotation === "number"); + console.assert(Number.isFinite(centerX)); + console.assert(Number.isFinite(centerY)); + console.assert(Number.isFinite(width)); + console.assert(Number.isFinite(height)); + console.assert(Number.isFinite(rotation)); + console.assert(width > 0); + console.assert(height > 0); + console.assert(mask === (mask & 0x7FFFFFFF) && mask !== 0); + + return new Rectangle(mask | 0, centerX + 0.0, centerY + 0.0, width + 0.0, height + 0.0, rotation + 0.0); + } + + /** + * @param {int31} mask - The mask (nonzero 31-bit integer). + * @param {number} centerX - The x-coordinate of the center. + * @param {number} centerY - The y-coordinate of the center. + * @param {number} width - The width (finite, positive). + * @param {number} height - The height (finite, positive). + * @param {number} rotation - The rotation in radians. + * @private + * @ignore + */ + constructor(mask, centerX, centerY, width, height, rotation) { + super(mask); + + const cos = Math.cos(rotation); + const sin = Math.sin(rotation); + const l = -width * 0.5; + const r = -l; + const t = -height * 0.5; + const b = -t; + const x0 = cos * l - sin * t; + const x1 = cos * r - sin * t; + const x2 = cos * r - sin * b; + const x3 = cos * l - sin * b; + const minX = Math.min(x0, x1, x2, x3) + centerX; + const maxX = Math.max(x0, x1, x2, x3) + centerX; + const y0 = sin * l + cos * t; + const y1 = sin * r + cos * t; + const y2 = sin * r + cos * b; + const y3 = sin * l + cos * b; + const minY = Math.min(y0, y1, y2, y3) + centerY; + const maxY = Math.max(y0, y1, y2, y3) + centerY; + const scaleX = cos / width; + const skewX = -sin / height; + const skewY = sin / width; + const scaleY = cos / height; + + /** + * The minimum x-coordinate. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._minX = minX; + + /** + * The minimum y-coordinate. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._minY = minY; + + /** + * The maximum x-coordinate. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._maxX = maxX; + + /** + * The maximum y-coordinate. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._maxY = maxY; + + /** + * The x-scale of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._scaleX = scaleX; + + /** + * The x-skew of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._skewX = skewX; + + /** + * The y-skew of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._skewY = skewY; + + /** + * The y-scale of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._scaleY = scaleY; + + /** + * The x-translation of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._translationX = 0.5 - (centerX * scaleX + centerY * skewY); + + /** + * The y-translation of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._translationY = 0.5 - (centerX * skewX + centerY * scaleY); + } + + /** + * @param {number} minX - The minimum x-coordinate. + * @param {number} minY - The minimum y-coordinate. + * @param {number} maxX - The maximum x-coordinate. + * @param {number} maxY - The maximum y-coordinate. + * @returns {-1|0|1} If 1, then the bounding box is contained in the shape. If -1, if the bounding box does not intersect with the shape. + * @inheritDoc + */ + testBounds(minX, minY, maxX, maxY) { + const x0 = this._minX; + const x1 = this._maxX; + + if (max(x0, minX) > min(x1, maxX)) { + return -1; + } + + const y0 = this._minY; + const y1 = this._maxY; + + if (max(y0, minY) > min(y1, maxY)) { + return -1; + } + + if (x0 > minX || maxX > x1 || y0 > minY || maxY > y1) { + return 0; + } + + const ma = this._scaleX; + const mc = this._skewY; + const mx = this._translationX; + let x = ma * minX + mc * minY + mx; + + if (x < 0 || x > 1) { + return 0; + } + + x = ma * maxX + mc * minY + mx; + + if (x < 0 || x > 1) { + return 0; + } + + x = ma * maxX + mc * maxY + mx; + + if (x < 0 || x > 1) { + return 0; + } + + x = ma * minX + mc * maxY + mx; + + if (x < 0 || x > 1) { + return 0; + } + + const mb = this._skewX; + const md = this._scaleY; + const my = this._translationY; + let y = mb * minX + md * minY + my; + + if (y < 0 || y > 1) { + return 0; + } + + y = mb * maxX + md * minY + my; + + if (y < 0 || y > 1) { + return 0; + } + + y = mb * maxX + md * maxY + my; + + if (y < 0 || y > 1) { + return 0; + } + + y = mb * minX + md * maxY + my; + + if (y < 0 || y > 1) { + return 0; + } + + return 1; + } + + /** + * @param {number} x - The x-coordinate of the point. + * @param {number} y - The y-coordinate of the point. + * @returns {boolean} True if the shape contains the point. + * @inheritDoc + */ + containsPoint(x, y) { + const ma = this._scaleX; + const mc = this._skewY; + const mx = this._translationX; + const x0 = ma * x + mc * y + mx; + + if (x0 < 0 || x0 > 1) { + return false; + } + + const mb = this._skewX; + const md = this._scaleY; + const my = this._translationY; + const y0 = mb * x + md * y + my; + + if (y0 < 0 || y0 > 1) { + return false; + } + + return true; + } + + /** + * @param {Cast} cast - The cast. + * @inheritDoc + */ + computeHits(cast) { + const { originX, originY, directionX, directionY } = cast; + const ma = this._scaleX; + const mb = this._skewX; + const mc = this._skewY; + const md = this._scaleY; + const mx = this._translationX; + const my = this._translationY; + const x = ma * originX + mc * originY + mx; + const y = mb * originX + md * originY + my; + const dx = ma * directionX + mc * directionY; + const dy = mb * directionX + md * directionY; + const px = -1.0 / dx; + const py = -1.0 / dy; + + let t1 = x * px; + let t2 = (x - 1.0) * px; + let time1 = min(max(t1, 0.0), max(t2, 0.0)); + let time2 = max(min(t1, Infinity), min(t2, Infinity)); + + t1 = y * py; + t2 = (y - 1.0) * py; + time1 = min(max(t1, time1), max(t2, time1)); + time2 = max(min(t1, time2), min(t2, time2)); + + if (time1 <= min(time2, 1.0)) { + const mask = this.mask; + + if (time1 > 0) { + cast.addHit(time1, mask); + } + + cast.addHit(time2, mask); + } + } +} diff --git a/scripts/raycast/shapes/tile.mjs b/scripts/raycast/shapes/tile.mjs new file mode 100644 index 0000000..0dddb08 --- /dev/null +++ b/scripts/raycast/shapes/tile.mjs @@ -0,0 +1,494 @@ +import Shape from "../shape.mjs"; +import { max, min } from "../math.mjs"; + +/** + * @import { int31 } from "../_types.mjs"; + * @import Cast from "../cast.mjs"; + */ + +/** + * @sealed + */ +export default class Tile extends Shape { + /** + * @param {object} args + * @param {number} args.centerX - The x-coordinate of the center. + * @param {number} args.centerY - The y-coordinate of the center. + * @param {number} args.width - The width (finite, positive). + * @param {number} args.height - The height (finite, positive). + * @param {number} [args.rotation=0.0] - The rotation in radians. + * @param {{ + * data: (number | boolean)[], + * offset?: number, + * stride?: number, + * width: number, + * height: number, + * minX?: number, + * minY?: number, + * maxX?: number, + * maxY?: number, + * threshold?: number + * }} args.texture - The texture. + * @param {int31} [args.mask=0x7FFFFFFF] - The mask (nonzero 31-bit integer). + * @returns {Tile} The tile. + */ + static create({ centerX, centerY, width, height, rotation = 0.0, texture, mask = 0x7FFFFFFF }) { + console.assert(typeof centerX === "number"); + console.assert(typeof centerY === "number"); + console.assert(typeof width === "number"); + console.assert(typeof height === "number"); + console.assert(typeof rotation === "number"); + console.assert(Number.isFinite(centerX)); + console.assert(Number.isFinite(centerY)); + console.assert(Number.isFinite(width)); + console.assert(Number.isFinite(height)); + console.assert(Number.isFinite(rotation)); + console.assert(width > 0); + console.assert(height > 0); + console.assert(texture !== null && typeof texture === "object"); + console.assert(Array.isArray(texture.data) || ArrayBuffer.isView(texture.data) && !(texture.data instanceof DataView)); + console.assert(texture.data.every((v) => (typeof v === "number" || typeof v === "boolean") && v >= 0)); + console.assert(texture.offset === undefined || typeof texture.offset === "number" && Number.isInteger(texture.offset) && texture.offset >= 0); + console.assert(texture.stride === undefined || typeof texture.stride === "number" && Number.isInteger(texture.stride) && texture.stride > 0); + console.assert(typeof texture.width === "number" && Number.isInteger(texture.width) && texture.width > 0); + console.assert(typeof texture.height === "number" && Number.isInteger(texture.height) && texture.height > 0); + console.assert(texture.minX === undefined || typeof texture.minX === "number" && Number.isInteger(texture.minX) && texture.minX >= 0); + console.assert(texture.minY === undefined || typeof texture.minY === "number" && Number.isInteger(texture.minY) && texture.minY >= 0); + console.assert(texture.maxX === undefined || typeof texture.maxX === "number" && Number.isInteger(texture.maxX) && texture.maxX > (texture.minX ?? 0)); + console.assert(texture.maxY === undefined || typeof texture.maxY === "number" && Number.isInteger(texture.maxY) && texture.maxY > (texture.minY ?? 0)); + console.assert(texture.threshold === undefined || typeof texture.threshold === "number"); + console.assert(mask === (mask & 0x7FFFFFFF) && mask !== 0); + + return new Tile(mask | 0, centerX + 0.0, centerY + 0.0, width + 0.0, height + 0.0, rotation + 0.0, texture); + } + + /** + * @param {int31} mask - The mask (nonzero 31-bit integer). + * @param {number} centerX - The x-coordinate of the center. + * @param {number} centerY - The y-coordinate of the center. + * @param {number} width - The width (finite, positive). + * @param {number} height - The height (finite, positive). + * @param {number} rotation - The rotation in radians. + * @param {{ + * data: (number | boolean)[], + * offset?: number, + * stride?: number, + * width: number, + * height: number, + * minX?: number, + * minY?: number, + * maxX?: number, + * maxY?: number, + * threshold?: number + * }} texture - The texture. + * @private + * @ignore + */ + constructor(mask, centerX, centerY, width, height, rotation, texture) { + super(mask); + + const textureWidth = texture.width; + const textureHeight = texture.height; + const textureMinX = texture.minX ?? 0; + const textureMinY = texture.minY ?? 0; + const textureMaxX = texture.maxX ?? textureWidth; + const textureMaxY = texture.maxY ?? textureHeight; + const textureScaleX = textureWidth / width; + const textureScaleY = textureHeight / height; + const cos = Math.cos(rotation); + const sin = Math.sin(rotation); + const halfWidth = width * 0.5; + const halfHeight = height * 0.5; + const l = textureMinX / textureScaleX - halfWidth; + const r = textureMaxX / textureScaleX - halfWidth; + const t = textureMinY / textureScaleY - halfHeight; + const b = textureMaxY / textureScaleY - halfHeight; + const x0 = cos * l - sin * t; + const x1 = cos * r - sin * t; + const x2 = cos * r - sin * b; + const x3 = cos * l - sin * b; + const minX = Math.min(x0, x1, x2, x3) + centerX; + const maxX = Math.max(x0, x1, x2, x3) + centerX; + const y0 = sin * l + cos * t; + const y1 = sin * r + cos * t; + const y2 = sin * r + cos * b; + const y3 = sin * l + cos * b; + const minY = Math.min(y0, y1, y2, y3) + centerY; + const maxY = Math.max(y0, y1, y2, y3) + centerY; + let scaleX = cos; + let skewX = -sin; + let skewY = sin; + let scaleY = cos; + let translationX = halfWidth - (centerX * scaleX + centerY * skewY); + let translationY = halfHeight - (centerX * skewX + centerY * scaleY); + + scaleX *= textureScaleX; + skewX *= textureScaleY; + skewY *= textureScaleX; + scaleY *= textureScaleY; + translationX *= textureScaleX; + translationY *= textureScaleY; + translationX += 1.0 - textureMinX; + translationY += 1.0 - textureMinY; + + const textureStrideX = texture.stride ?? 1; + const textureStrideY = textureWidth * textureStrideX; + const field = sdf( + texture.data, + texture.offset ?? 0, + textureStrideX, + textureStrideY, + textureMinX, + textureMinY, + textureMaxX, + textureMaxY, + texture.threshold ?? Number.MIN_VALUE, + ); + + for (let i = 0, n = field.length; i < n; i++) { + const signedDistance = field[i]; + + field[i] = Math.sign(signedDistance) * max(Math.abs(signedDistance) - 1.0, 0.5); + } + + /** + * The minimum x-coordinate. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._minX = minX; + + /** + * The minimum y-coordinate. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._minY = minY; + + /** + * The maximum x-coordinate. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._maxX = maxX; + + /** + * The maximum y-coordinate. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._maxY = maxY; + + /** + * The width. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._width = (textureMaxX - textureMinX + 2) | 0; + + /** + * The height. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._height = (textureMaxY - textureMinY + 2) | 0; + + /** + * The x-scale of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._scaleX = scaleX; + + /** + * The x-skew of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._skewX = skewX; + + /** + * The y-skew of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._skewY = skewY; + + /** + * The y-scale of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._scaleY = scaleY; + + /** + * The x-translation of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._translationX = translationX; + + /** + * The y-translation of the matrix. + * @type {number} + * @readonly + * @private + * @ignore + */ + this._translationY = translationY; + + /** + * The signed distance field. + * @type {Float64Array} + * @readonly + * @private + * @ignore + */ + this._field = field; + } + + /** + * @param {number} minX - The minimum x-coordinate. + * @param {number} minY - The minimum y-coordinate. + * @param {number} maxX - The maximum x-coordinate. + * @param {number} maxY - The maximum y-coordinate. + * @returns {-1|0|1} If 1, then the bounding box is contained in the shape. If -1, if the bounding box does not intersect with the shape. + * @inheritDoc + */ + testBounds(minX, minY, maxX, maxY) { + return max(this._minX, minX) > min(this._maxX, maxX) || max(this._minY, minY) > min(this._maxY, maxY) ? -1 : 0; + } + + /** + * @param {number} x - The x-coordinate of the point. + * @param {number} y - The y-coordinate of the point. + * @returns {boolean} True if the shape contains the point. + * @inheritDoc + */ + containsPoint(x, y) { + const ma = this._scaleX; + const mc = this._skewY; + const mx = this._translationX; + const x0 = ma * x + mc * y + mx; + + if (x0 < 0.0) { + return false; + } + + const w = this._width; + + if (x0 >= w) { + return false; + } + + const mb = this._skewX; + const md = this._scaleY; + const my = this._translationY; + const y0 = mb * x + md * y + my; + + if (y0 < 0.0 || y0 >= this._height) { + return false; + } + + return this._field[(y0 | 0) * w + (x0 | 0)] < 0.0; + } + + /** + * @param {Cast} cast - The cast. + * @inheritDoc + */ + computeHits(cast) { + const { originX, originY, directionX, directionY } = cast; + const w = this._width; + const h = this._height; + const ma = this._scaleX; + const mb = this._skewX; + const mc = this._skewY; + const md = this._scaleY; + const mx = this._translationX; + const my = this._translationY; + let x = ma * originX + mc * originY + mx; + let y = mb * originX + md * originY + my; + const dx = ma * directionX + mc * directionY; + const dy = mb * directionX + md * directionY; + const px = 1.0 / dx; + const py = 1.0 / dy; + + let t1 = (1.0 - x) * px; + let t2 = (w - 1.0 - x) * px; + let time1 = min(max(t1, 0.0), max(t2, 0.0)); + let time2 = max(min(t1, Infinity), min(t2, Infinity)); + + t1 = (1.0 - y) * py; + t2 = (h - 1.0 - y) * py; + time1 = min(max(t1, time1), max(t2, time1)); + time2 = max(min(t1, time2), min(t2, time2)); + + if (time1 <= min(time2, 1.0)) { + const field = this._field; + let inside = false; + + if (time1 <= 0.0) { + time1 = 0.0; + inside = field[(y | 0) * w + (x | 0)] < 0.0; + } + + const invMagnitude = 1.0 / Math.sqrt(dx * dx + dy * dy); + + do { + const signedDistance = field[(y + dy * time1 | 0) * w + (x + dx * time1 | 0)] * invMagnitude; + + if (inside !== signedDistance < 0.0) { + inside = !inside; + + cast.addHit(time1, this.mask); + } + + time1 += Math.abs(signedDistance); + } while (time1 <= time2); + + if (inside) { + cast.addHit(time2, this.mask); + } + } + } +} + +/** + * The value representing infinity. Used by {@link edt}. + * @type {number} + */ +const EDT_INF = 1e20; + +/** + * Generate the 2D Euclidean signed distance field. + * @param {(number | boolean)[]} data - The elements. + * @param {number} offset - The offset of the first element in `data`. + * @param {number} strideX - The distance between consecutive elements in a row of `data`. + * @param {number} strideY - The distance between consecutive elements in a column of `data`. + * @param {number} minX - The minimum x-coordinate of the rectangle. + * @param {number} minY - The minimum y-coordinate of the rectangle. + * @param {number} maxX - The maximum x-coordinate of the rectangle. + * @param {number} maxY - The maximum x-coordinate of the rectangle. + * @param {number} threshold - The threshold that needs to be met or exceeded for a pixel to be inner. + * @returns {Float64Array} - The signed distance field with a 1 pixel padding. + */ +function sdf(data, offset, strideX, strideY, minX, minY, maxX, maxY, threshold) { + const width = maxX - minX + 2; + const height = maxY - minY + 2; + const size = width * height; + const capacity = Math.max(width, height); + const temp = new ArrayBuffer(8 * size + 20 * capacity + 8); + const inner = new Float64Array(temp, 0.0, size); + const outer = new Float64Array(size).fill(EDT_INF); + + for (let y = minY, j = width + 1; y < maxY; y++, j += 2) { + for (let x = minX; x < maxX; x++, j++) { + const a = data[offset + x * strideX + y * strideY]; + + if (a >= threshold) { + inner[j] = EDT_INF; + outer[j] = 0.0; + } + } + } + + const f = new Float64Array(temp, inner.byteLength, capacity); + const z = new Float64Array(temp, f.byteOffset + f.byteLength, capacity + 1); + const v = new Int32Array(temp, z.byteOffset + z.byteLength, capacity); + + edt(inner, width, height, f, v, z); + edt(outer, width, height, f, v, z); + + for (let i = 0; i < size; i++) { + outer[i] = Math.sqrt(outer[i]) - Math.sqrt(inner[i]); + } + + return outer; +} + +/** + * 2D Euclidean squared distance transform by Felzenszwalb & Huttenlocher. + * @param {Float64Array} grid - The grid. + * @param {number} width - The width of the grid. + * @param {number} height - The height of the grid. + * @param {Float64Array} f - The temporary source data, which returns the y of the parabola vertex at x. + * @param {Int32Array} v - The temporary used to store x-coordinates of parabola vertices. + * @param {Float64Array} z - The temporary used to store x-coordinates of parabola intersections. + */ +function edt(grid, width, height, f, v, z) { + for (let x = 0; x < width; x++) { + edt1d(grid, x, width, height, f, v, z); + } + + for (let y = 0; y < height; y++) { + edt1d(grid, y * width, 1, width, f, v, z); + } +} + +/** + * 1D squared distance transform. Used by {@link edt}. + * @param {Float64Array} grid - The grid. + * @param {number} offset - The offset. + * @param {number} stride - The stride. + * @param {number} length - The length. + * @param {Float64Array} f - The temporary source data, which returns the y of the parabola vertex at x. + * @param {Int32Array} v - The temporary used to store x-coordinates of parabola vertices. + * @param {Float64Array} z - The temporary used to store x-coordinates of parabola intersections. + */ +function edt1d(grid, offset, stride, length, f, v, z) { + f[0] = grid[offset]; + v[0] = 0; + z[0] = -EDT_INF; + z[1] = EDT_INF; + + for (let q = 1, k = 0, s = 0; q < length; q++) { + f[q] = grid[offset + q * stride]; + + const q2 = q * q; + + do { + const r = v[k]; + + s = (f[q] - f[r] + q2 - r * r) / (q - r) * 0.5; + } while (s <= z[k] && k--); + + k++; + v[k] = q; + z[k] = s; + z[k + 1] = EDT_INF; + } + + for (let q = 0, k = 0; q < length; q++) { + while (z[k + 1] < q) { + k++; + } + + const r = v[k]; + const qr = q - r; + + grid[offset + q * stride] = f[r] + qr * qr; + } +} diff --git a/scripts/raycast/space.mjs b/scripts/raycast/space.mjs new file mode 100644 index 0000000..1cb784f --- /dev/null +++ b/scripts/raycast/space.mjs @@ -0,0 +1,336 @@ +import { max, min } from "./math.mjs"; +import Volume from "./volume.mjs"; + +/** + * @import Geometry from "./geometry.mjs"; + */ + +/** + * @sealed + */ +export default class Space { + /** + * An empty space. + * @type {Space} + * @readonly + */ + static EMPTY = new Space([]); + + /** + * @param {object} args + * @param {Volume[]} args.volumes - The volumes. + * @param {number} [args.minX=-Infinity] - The minimum x-coordinate. + * @param {number} [args.minY=-Infinity] - The minimum y-coordinate. + * @param {number} [args.minZ=-Infinity] - The minimum z-coordinate. + * @param {number} [args.maxX=Infinity] - The maximum x-coordinate. + * @param {number} [args.maxY=Infinity] - The maximum y-coordinate. + * @param {number} [args.maxZ=Infinity] - The maximum z-coordinate. + * @returns {Space} The space. + */ + static create({ volumes, minX = -Infinity, minY = -Infinity, minZ = -Infinity, maxX = Infinity, maxY = Infinity, maxZ = Infinity }) { + console.assert(Array.isArray(volumes)); + console.assert(volumes.every((volume) => volume instanceof Volume)); + console.assert(typeof minX === "number"); + console.assert(typeof minY === "number"); + console.assert(typeof minZ === "number"); + console.assert(typeof maxX === "number"); + console.assert(typeof maxY === "number"); + console.assert(typeof maxZ === "number"); + console.assert(minX <= maxX); + console.assert(minY <= maxY); + console.assert(minZ <= maxZ); + + return new Space(initializeVolumes(volumes.toSorted(compareVolumesByPriority), minX, minY, minZ, maxX, maxY, maxZ)); + } + + /** + * @param {Volume[]} volumes - The volumes. + * @private + * @ignore + */ + constructor(volumes) { + const [minCost, maxCost] = calculateCostEstimates(volumes); + + /** + * The volumes. + * @type {ReadonlyArray} + * @readonly + */ + this.volumes = volumes; + + /** + * The estimated minimum energy cost anywhere in the space. + * @type {number} + * @readonly + */ + this.minCost = minCost; + + /** + * The estimated maximum energy cost anywhere in the space. + * @type {number} + * @readonly + */ + this.maxCost = maxCost; + + /** + * The estimated minimum distance a ray can travel anywhere in the space. + * @type {number} + * @readonly + */ + this.minDistance = 1.0 / maxCost; + + /** + * The estimated maximum distance a ray can travel anywhere in the space. + * @type {number} + * @readonly + */ + this.maxDistance = 1.0 / minCost; + } + + /** + * Crop the space w.r.t. the given bounding box. + * @param {number} minX - The minimum x-coordinate. + * @param {number} minY - The minimum y-coordinate. + * @param {number} minZ - The minimum z-coordinate. + * @param {number} maxX - The maximum x-coordinate. + * @param {number} maxY - The maximum y-coordinate. + * @param {number} maxZ - The maximum z-coordinate. + * @returns {Space} The cropped space. + */ + crop(minX, minY, minZ, maxX, maxY, maxZ) { + const volumes = initializeVolumes(this.volumes, minX, minY, minZ, maxX, maxY, maxZ); + + if (volumes.length === 0) { + return Space.EMPTY; + } + + if (volumes === this.volumes) { + return this; + } + + return new Space(volumes); + } +} + +/** + * The array for cropped volumes. + * @type {Volume[]} + */ +const CROPPED_VOLUMES = []; + +/** + * The array for cropped geometries. + * @type {Geometry[]} + */ +const CROPPED_GEOMETRIES = []; + +/** + * Compare two volumes by priority. + * @param {Volume} volume1 - The first volume. + * @param {Volume} volume2 - The second volume. + * @returns {number} + */ +function compareVolumesByPriority(volume1, volume2) { + return volume1.priority - volume2.priority || volume1.geometry._id - volume2.geometry._id; +} + +/** + * Initialize this volumes by cropping them to the given the bounding box of the space. + * Discard unnecessary volumes and identify volumes that envelop the bounding box of the space, + * which are marked to be skipped by the ray intersection test. + * @param {Volume[]} volumes - The volumes. + * @param {number} minX - The minimum x-coordinate. + * @param {number} minY - The minimum y-coordinate. + * @param {number} minZ - The minimum z-coordinate. + * @param {number} maxX - The maximum x-coordinate. + * @param {number} maxY - The maximum y-coordinate. + * @param {number} maxZ - The maximum z-coordinate. + * @returns {Volume[]} The cropped volumes that haven't been discarded. + */ +function initializeVolumes(volumes, minX, minY, minZ, maxX, maxY, maxZ) { + const numVolumes = volumes.length; + + for (let volumeIndex = 0; volumeIndex < numVolumes; volumeIndex++) { + const volume = volumes[volumeIndex]; + + volume.geometry._castId = 0; + } + + for (let volumeIndex = 0; volumeIndex < numVolumes; volumeIndex++) { + const volume = volumes[volumeIndex]; + + switch (volume.mode) { + case 0: + if (volume.cost === 0.0) { + continue; + } + + break; + case 1: + if (volume.cost === Infinity) { + continue; + } + + break; + case 2: + if (volume.cost === 0.0) { + continue; + } + + break; + } + + const geometry = volume.geometry; + let croppedGeometry; + + if (geometry._castId === 0) { + croppedGeometry = geometry.crop(minX, minY, minZ, maxX, maxY, maxZ); + geometry._castId = ~CROPPED_VOLUMES.length; + CROPPED_GEOMETRIES.push(croppedGeometry); + } else { + croppedGeometry = CROPPED_GEOMETRIES[~geometry._castId]; + } + + if (croppedGeometry.isEmpty) { + continue; + } + + const croppedVolume = croppedGeometry === geometry ? volume : new Volume(croppedGeometry, volume.priority, volume.mode, volume.cost); + + CROPPED_VOLUMES.push(croppedVolume); + } + + CROPPED_GEOMETRIES.length = 0; + + if (CROPPED_VOLUMES.length === volumes.length) { + let cropped = false; + + for (let volumeIndex = 0; volumeIndex < numVolumes; volumeIndex++) { + if (CROPPED_VOLUMES[volumeIndex] !== volumes[volumeIndex]) { + cropped = true; + + break; + } + + if (!cropped) { + CROPPED_VOLUMES.length = 0; + + return volumes; + } + } + } + + for (let volumeIndex = CROPPED_VOLUMES.length - 1; volumeIndex >= 0; volumeIndex--) { + const croppedVolume = CROPPED_VOLUMES[volumeIndex]; + + if (!croppedVolume.geometry.isUnbounded) { + continue; + } + + const mode = croppedVolume.mode; + + if (mode === 0) { + if (!Number.isFinite(croppedVolume.cost)) { + const croppedVolumes = CROPPED_VOLUMES.slice(0, volumeIndex + (croppedVolume.cost <= 0 ? 1 : 0)); + + CROPPED_VOLUMES.length = 0; + + return croppedVolumes; + } + } else if (mode === 1) { + if (croppedVolume.cost === 0.0) { + const croppedVolumes = CROPPED_VOLUMES.slice(volumeIndex + 1); + + CROPPED_VOLUMES.length = 0; + + return croppedVolumes; + } + } else if (mode === 2) { + if (croppedVolume.cost === Infinity) { + const croppedVolumes = CROPPED_VOLUMES.slice(volumeIndex); + + CROPPED_VOLUMES.length = 0; + + return croppedVolumes; + } + } else if (mode === 3) { + const croppedVolumes = CROPPED_VOLUMES.slice(volumeIndex + (croppedVolume.cost === 0.0 ? 1 : 0)); + + CROPPED_VOLUMES.length = 0; + + return croppedVolumes; + } + } + + const croppedVolumes = CROPPED_VOLUMES.slice(0); + + CROPPED_VOLUMES.length = 0; + + return croppedVolumes; +} + +/** + * Estimate the minimum and maximum energy cost and distance travelled anywhere in the space. + * @param {Volume[]} volumes - The volumes. + * @returns {[minCost: number, maxCost: number]} The estimates of the minimum and maximum cost. + */ +function calculateCostEstimates(volumes) { + let minCost = 0.0; + let maxCost = 0.0; + const numVolumes = volumes.length; + + for (let volumeIndex = 0; volumeIndex < numVolumes; volumeIndex++) { + const volume = volumes[volumeIndex]; + const cost = volume.cost; + + if (volume.geometry.isUnbounded) { + switch (volume.mode) { + case 0: + minCost = max(minCost + cost, 0.0); + maxCost = max(maxCost + cost, 0.0); + + break; + case 1: + minCost = min(minCost, cost); + maxCost = min(maxCost, cost); + + break; + case 2: + minCost = max(minCost, cost); + maxCost = max(maxCost, cost); + + break; + case 3: + minCost = maxCost = cost; + + break; + } + } else { + switch (volume.mode) { + case 0: + if (cost >= 0.0) { + maxCost += cost; + } else { + minCost = max(minCost + cost, 0.0); + } + + break; + case 1: + minCost = min(minCost, cost); + + break; + case 2: + maxCost = max(maxCost, cost); + + break; + case 3: + minCost = min(minCost, cost); + maxCost = max(maxCost, cost); + + break; + } + } + } + + return [minCost, maxCost]; +} diff --git a/scripts/raycast/volume.mjs b/scripts/raycast/volume.mjs new file mode 100644 index 0000000..0d0cb85 --- /dev/null +++ b/scripts/raycast/volume.mjs @@ -0,0 +1,67 @@ +import Geometry from "./geometry.mjs"; +import Mode from "./mode.mjs"; + +/** + * @import { int32 } from "./_types.mjs"; + */ + +/** + * @sealed + */ +export default class Volume { + /** + * @param {object} args + * @param {Geometry} args.geometry - The geometry. + * @param {int32} [args.priority=0] - The priority. + * @param {Mode} args.mode - The mode used in the energy calculation. + * @param {number} args.cost - The energy cost. + * @returns {Volume} The volume. + */ + static create({ geometry, priority = 0, mode, cost }) { + console.assert(geometry instanceof Geometry); + console.assert(priority === (priority | 0)); + console.assert(mode === (mode | 0) && Object.values(Mode).includes(mode)); + console.assert(mode === Mode.ADD || cost >= 0.0); + console.assert(typeof cost === "number"); + + return new Volume(geometry, priority | 0, mode | 0, cost + 0.0); + } + + /** + * @param {Geometry} geometry - The geometry. + * @param {int32} priority - The priority. + * @param {Mode} mode - The mode used in the energy calculation. + * @param {number} cost - The energy cost. + * @private + * @ignore + */ + constructor(geometry, priority, mode, cost) { + /** + * The geometry. + * @type {Geometry} + * @readonly + */ + this.geometry = geometry; + + /** + * The priority. + * @type {int32} + * @readonly + */ + this.priority = priority; + + /** + * The mode. + * @type {Mode} + * @readonly + */ + this.mode = mode; + + /** + * The energy cost. + * @type {number} + * @readonly + */ + this.cost = cost; + } +} diff --git a/scripts/raycasting/_index.mjs b/scripts/raycasting/_index.mjs deleted file mode 100644 index 0598ebf..0000000 --- a/scripts/raycasting/_index.mjs +++ /dev/null @@ -1,7 +0,0 @@ -export * as boundaries from "./boundaries/_index.mjs"; -export { Boundary } from "./boundary.mjs"; -export { Caster } from "./caster.mjs"; -export { Figure } from "./figure.mjs"; -export * as figures from "./figures/_index.mjs"; -export { Operation } from "./operation.mjs"; -export { Volume } from "./volume.mjs"; diff --git a/scripts/raycasting/boundaries/_index.mjs b/scripts/raycasting/boundaries/_index.mjs deleted file mode 100644 index 11f8a22..0000000 --- a/scripts/raycasting/boundaries/_index.mjs +++ /dev/null @@ -1 +0,0 @@ -export * from "./cylinder.mjs"; diff --git a/scripts/raycasting/boundaries/cylinder.mjs b/scripts/raycasting/boundaries/cylinder.mjs deleted file mode 100644 index 84536f4..0000000 --- a/scripts/raycasting/boundaries/cylinder.mjs +++ /dev/null @@ -1,183 +0,0 @@ -import { Hit } from "../hit.mjs"; -import { max, min } from "../math.mjs"; -import { Boundary } from "../boundary.mjs" - -export class Cylinder extends Boundary { - /** - * The figures of the base. - * @type {Figure[]} - * @readonly - */ - #base; - - /** - * The bottom (minimum z-coordinate). - * @type {number} - * @readonly - */ - #bottom; - - /** - * The top (maximum z-coordinate). - * @type {number} - * @readonly - */ - #top; - - /** - * @param {object} args - * @param {Figure[]} args.base - The figures of the base. - * @param {number|null} [args.bottom=-Infinity] - The bottom (minimum z-coordinate). - * @param {number|null} [args.top=Infinity] - The top (maximum z-coordinate). - * @param {number} [args.mask] - The bit mask (32-bit). - */ - constructor({ base, bottom, top, mask }) { - super(mask); - - this.#base = base; - this.#bottom = bottom ?? -Infinity; - this.#top = top ?? Infinity; - } - - /** @type {number} */ - #state = -1; - - /** @override */ - clone() { - const clone = new this.constructor({ - base: Array.from(this.#base), - bottom: this.#bottom, - top: this.#top, - mask: this.mask - }); - - clone.#state = this.#state; - - return clone; - } - - /** @override */ - initialize(minX, minY, minZ, maxX, maxY, maxZ) { - const figures = this.#base; - - if (this.mask === 0 || !(max(this.#bottom, minZ) <= min(this.#top, maxZ))) { - figures.length = 0; - - return false; - } - - for (let figureIndex = figures.length - 1; figureIndex >= 0; figureIndex--) { - const figure = figures[figureIndex]; - - if (figure.mask === 0 || !figure.intersectsBounds(minX, minY, maxX, maxY)) { - figures[figureIndex] = figures[figures.length - 1]; - figures.length--; - } - } - - let state = this.#state; - - if (this.#bottom <= minZ && this.#top >= maxZ) { - state ^= 1 << 31; - } - - if (Number.isFinite(minX) && Number.isFinite(minY) && Number.isFinite(maxX) && Number.isFinite(maxY)) { - for (let figureIndex = figures.length - 1; figureIndex >= 0; figureIndex--) { - const figure = figures[figureIndex]; - - if (figure.containsBounds(minX, minY, maxX, maxY)) { - figures[figureIndex] = figures[figures.length - 1]; - figures.length--; - state ^= figure.mask; - } - } - } - - this.#state = state; - - if ((state & 0x7FFFFFFF) !== 0 && figures.length === 0) { - return false; - } - - if (state === 0 && figures.length === 0) { - this.envelops = true; - } - - return true; - } - - /** @override */ - computeHits(originX, originY, originZ, velocityX, velocityY, velocityZ, hitQueue, volumeIndex, boundaryIndex) { - let state = this.#state; - let computeFigureHits; - const envelopsZ = (state & 1 << 31) === 0; - - if (!envelopsZ) { - const invVelocityZ = 1 / velocityZ; - - const t1 = (this.#bottom - originZ) * invVelocityZ; - const t2 = (this.#top - originZ) * invVelocityZ; - const time1 = min(max(t1, 0), max(t2, 0)); - const time2 = max(min(t1, Infinity), min(t2, Infinity)); - - if (time1 <= time2 && time2 > 0) { - if (time1 >= 0) { - if (time1 < 1) { - hitQueue?.push(new Hit(time1, volumeIndex, boundaryIndex, 1 << 31)); - } - - state ^= 1 << 31; - } - - if (time2 >= 0) { - if (time2 < 1) { - hitQueue?.push(new Hit(time2, volumeIndex, boundaryIndex, 1 << 31)); - } - - state ^= 1 << 31; - } - - computeFigureHits = true; - } - } else { - computeFigureHits = true; - } - - if (computeFigureHits) { - if (velocityX === 0 && velocityY === 0) { - velocityX = velocityY = 1; - hitQueue = null; - } - - const invVelocityX = 1 / velocityX; - const invVelocityY = 1 / velocityY; - - const figures = this.#base; - const numFigures = figures.length; - - for (let figureIndex = 0; figureIndex < numFigures; figureIndex++) { - const figure = figures[figureIndex]; - - let t1 = (figure.minX - originX) * invVelocityX; - let t2 = (figure.maxX - originX) * invVelocityX; - let time1 = min(max(t1, 0), max(t2, 0)); - let time2 = max(min(t1, Infinity), min(t2, Infinity)); - - t1 = (figure.minY - originY) * invVelocityY; - t2 = (figure.maxY - originY) * invVelocityY; - time1 = min(max(t1, time1), max(t2, time1)); - time2 = max(min(t1, time2), min(t2, time2)); - - if (time1 > time2 || time2 <= 0) { - continue; - } - - state ^= figure.computeHits(originX, originY, velocityX, velocityY, hitQueue, volumeIndex, boundaryIndex); - } - } - - this.state = state; - - return state === 0 ? this.mask : 0; - } -} diff --git a/scripts/raycasting/boundary.mjs b/scripts/raycasting/boundary.mjs deleted file mode 100644 index 14b2315..0000000 --- a/scripts/raycasting/boundary.mjs +++ /dev/null @@ -1,70 +0,0 @@ -/** - * @abstract - */ -export class Boundary { - /** - * @param {number} [mask=-1] - The bit mask (32-bit). - */ - constructor(mask) { - /** - * The bit mask of the boundary (32-bit). - * @type {number} - * @readonly - */ - this.mask = mask ?? -1; - /** - * The current state of the ray relative to the interior of the boundary. - * If zero, the ray is currently inside the interior enclosed by the boundary. - * @type {number} - */ - this.state = 0; - /** - * Skip hits computation? True if rays cannot leave the interior enclosed by this boundary. - * @type {boolean} - * @readonly - */ - this.envelops = false; - } - - /** - * Clone this volume. - * @returns {Volume} - * @abstract - */ - clone() { - throw new Error("Not implemented"); - } - - /** - * Initialize this boundary given the bounding box of the ray caster. - * @param {number} minX - The minimum x-coordinate. - * @param {number} minY - The minimum y-coordinate. - * @param {number} minZ - The minimum z-coordinate. - * @param {number} maxX - The maximum x-coordinate. - * @param {number} maxY - The maximum y-coordinate. - * @param {number} maxZ - The maximum z-coordinate. - * @returns {boolean} Returns false if the boundary can be discarded. - * @abstract - */ - initialize(minX, minY, minZ, maxX, maxY, maxZ) { - throw new Error("Not implemented"); - } - - /** - * Compute the hits of the boundary with the ray. - * @param {number} originX - The x-origin of the ray. - * @param {number} originY - The y-origin of the ray. - * @param {number} originZ - The y-origin of the ray. - * @param {number} velocityX - The x-velocity of the ray. - * @param {number} velocityY - The y-velocity of the ray. - * @param {number} velocityZ - The y-velocity of the ray. - * @param {Hit[]|null} hitQueue - The hit queue. - * @param {number} volumeIndex - The index of the volume. - * @param {number} boundaryIndex - The index of the boundary. - * @returns {number} The mask that encodes whether the ray originates in the interior enclosed by the boundary. - * @abstract - */ - computeHits(originX, originY, originZ, velocityX, velocityY, velocityZ, hitQueue, volumeIndex, boundaryIndex) { - throw new Error("Not implemented"); - } -} diff --git a/scripts/raycasting/caster.mjs b/scripts/raycasting/caster.mjs deleted file mode 100644 index cc0c1c2..0000000 --- a/scripts/raycasting/caster.mjs +++ /dev/null @@ -1,743 +0,0 @@ -import { Hit } from "./hit.mjs"; -import { max, min } from "./math.mjs"; -import { Volume } from "./volume.mjs"; - -/** - * The ray caster. - */ -export class Caster { - /** - * The x-coordinate of the current origin. - * @type {number} - * @readonly - */ - originX = 0; - - /** - * The y-coordinate of the current origin. - * @type {number} - * @readonly - */ - originY = 0; - - /** - * The z-coordinate of the current origin. - * @type {number} - * @readonly - */ - originZ = 0; - - /** - * The x-coordinate of the current target. - * @type {number} - * @readonly - */ - targetX = 0; - - /** - * The y-coordinate of the current target. - * @type {number} - * @readonly - */ - targetY = 0; - - /** - * The z-coordinate of the current target. - * @type {number} - * @readonly - */ - targetZ = 0; - - /** - * The hits. - * @type {Hit[]} - */ - #hits = []; - - /** - * @param {Volume[]} volumes - The volumes. - * @param {number} [minR] - The minimum range. - * @param {number} [maxR] - The maximum range. - * @param {number} [minX] - The minimum x-coordinate. - * @param {number} [minY] - The minimum y-coordinate. - * @param {number} [minZ] - The minimum z-coordinate. - * @param {number} [maxX] - The maximum x-coordinate. - * @param {number} [maxY] - The maximum y-coordinate. - * @param {number} [maxZ] - The maximum z-coordinate. - */ - constructor(volumes, minR, maxR, minX, minY, minZ, maxX, maxY, maxZ) { - /** - * The minimum range. - * @type {number} - * @readonly - */ - this.minR = minR ??= 0; - /** - * The maximum range. - * @type {number} - * @readonly - */ - this.maxR = maxR = max(maxR ?? Infinity, minR); - /** - * The minimum x-coordinate. - * @type {number} - * @readonly - */ - this.minX = minX = Math.floor((minX ?? -Infinity) * 256) / 256; - /** - * The minimum y-coordinate. - * @type {number} - * @readonly - */ - this.minY = minY = Math.floor((minY ?? -Infinity) * 256) / 256; - /** - * The minimum z-coordinate. - * @type {number} - * @readonly - */ - this.minZ = minZ = Math.floor((minZ ?? -Infinity) * 256) / 256; - /** - * The maximum x-coordinate. - * @type {number} - * @readonly - */ - this.maxX = maxX = Math.ceil((maxX ?? +Infinity) * 256) / 256; - /** - * The maximum y-coordinate. - * @type {number} - * @readonly - */ - this.maxY = maxY = Math.ceil((maxY ?? +Infinity) * 256) / 256; - /** - * The maximum z-coordinate. - * @type {number} - * @readonly - */ - this.maxZ = maxZ = Math.ceil((maxZ ?? +Infinity) * 256) / 256; - /** - * The volumes. - * @type {Volume[]} - * @readonly - */ - this.volumes = Caster.#initializeVolumes(volumes, minX, minY, minZ, maxX, maxY, maxZ); - - this.#estimateDistances(); - } - - /** - * Create an optimized ray caster restricted to the specified bounds. - * @param {number} minX - The minimum x-coordinate. - * @param {number} minY - The minimum y-coordinate. - * @param {number} minZ - The minimum z-coordinate. - * @param {number} maxX - The maximum x-coordinate. - * @param {number} maxY - The maximum y-coordinate. - * @param {number} maxZ - The maximum z-coordinate. - * @returns {Caster} The new ray caster restricted to the bounds. - */ - crop(minX, minY, minZ, maxX, maxY, maxZ) { - minX = max(minX ?? -Infinity, this.minX); - minY = max(minY ?? -Infinity, this.minY); - minZ = max(minZ ?? -Infinity, this.minZ); - maxX = min(maxX ?? +Infinity, this.maxX); - maxY = min(maxY ?? +Infinity, this.maxY); - maxZ = min(maxZ ?? +Infinity, this.maxZ); - - return new Caster(this.volumes.map((v) => v.clone()), this.minR, this.maxR, minX, minY, minZ, maxX, maxY, maxZ); - } - - /** - * Initialize this volumes given the bounding box of the ray caster. - * Remove unnecessary volumes and identify volumes that contain the bounds of the ray caster, - * which are marked to be skipped by the ray intersection test. Sort volumes by priority. - * @param {Volume[]} volumes - The volumes. - * @param {number} minX - The minimum x-coordinate. - * @param {number} minY - The minimum y-coordinate. - * @param {number} minZ - The minimum z-coordinate. - * @param {number} maxX - The maximum x-coordinate. - * @param {number} maxY - The maximum y-coordinate. - * @param {number} maxZ - The maximum z-coordinate. - * @returns {Volume[]} - */ - static #initializeVolumes(volumes, minX, minY, minZ, maxX, maxY, maxZ) { - const numVolumes = volumes.length; - let numDiscardedVolumes = 0; - - for (let volumeIndex = 0; volumeIndex < numVolumes; volumeIndex++) { - const volume = volumes[volumeIndex]; - - if (!volume.initialize(minX, minY, minZ, maxX, maxY, maxZ)) { - volumes[volumeIndex] = null; - numDiscardedVolumes++; - } - } - - volumes.sort((v1, v2) => !v1 - !v2 || (v1 ? v1.priority - v2.priority : 0)); - volumes.length -= numDiscardedVolumes; - - for (let volumeIndex = volumes.length - 1; volumeIndex >= 0; volumeIndex--) { - const volume = volumes[volumeIndex]; - - if (volume.envelops) { - const operation = volume.operation; - - if (operation === 0 - || (operation === 1 || operation === 2 || operation === 4) && volume.cost === Infinity - || operation === 3 && volume.cost === 0) { - volumes.splice(0, volumeIndex); // TODO - - break; - } - } - } - - return volumes; - } - - /** - * Estimate the minimum and maximum ranges that rays can travel. - */ - #estimateDistances() { - const maxR = this.maxR; - let maxD = Math.min( - maxR, - Math.hypot( - this.maxX - this.minX, - this.maxY - this.minY, - this.maxZ - this.minZ - ) - ); - - const volumes = this.volumes; - - if (maxD === 0 || volumes.length === 0) { - this.minD = this.maxD = maxD; - - return; - } - - let minEnergyCost = 0; - let maxEnergyCost = 0; - - for (const volume of volumes) { - const energyCost = volume.cost; - - if (volume.envelops) { - switch (volume.operation) { - case 0: - minEnergyCost = maxEnergyCost = energyCost; - break; - case 1: - minEnergyCost += energyCost; - maxEnergyCost += energyCost; - break; - case 2: - minEnergyCost = max(minEnergyCost - energyCost, 0); - maxEnergyCost = max(maxEnergyCost - energyCost, 0); - break; - case 3: - minEnergyCost = min(minEnergyCost, energyCost); - maxEnergyCost = min(maxEnergyCost, energyCost); - break; - case 4: - minEnergyCost = max(minEnergyCost, energyCost); - maxEnergyCost = max(maxEnergyCost, energyCost); - break; - } - } else { - switch (volume.operation) { - case 0: - minEnergyCost = min(minEnergyCost, energyCost); - maxEnergyCost = max(maxEnergyCost, energyCost); - break; - case 1: - maxEnergyCost += energyCost; - break; - case 2: - minEnergyCost = max(minEnergyCost - energyCost, 0); - break; - case 3: - minEnergyCost = min(minEnergyCost, energyCost); - break; - case 4: - maxEnergyCost = max(maxEnergyCost, energyCost); - break; - } - } - } - - /** - * The maximum distance a ray can travel. - * @type {number} - * @readonly - */ - this.maxD = maxD = min(this.minR + 1 / minEnergyCost, maxD); - /** - * The minimum distance a ray can travel. - * @type {number} - * @readonly - */ - this.minD = min(this.minR + 1 / maxEnergyCost, maxD); - } - - /** - * Set the origin for the next ray casts. - * @param {number} originX - The x-coordinate of the origin. - * @param {number} originY - The y-coordinate of the origin. - * @param {number} originZ - The z-coordinate of the origin. - * @returns {this} - */ - setOrigin(originX, originY, originZ) { - this.originX = Math.round(originX * 256) / 256; - this.originY = Math.round(originY * 256) / 256; - this.originZ = Math.round(originZ * 256) / 256; - - return this; - } - - /** - * Set the target for the next ray casts. - * @param {number} targetX - The x-coordinate of the target. - * @param {number} targetY - The y-coordinate of the target. - * @param {number} targetZ - The z-coordinate of the target. - * @returns {this} - */ - setTarget(targetX, targetY, targetZ) { - this.targetX = Math.round(targetX * 256) / 256; - this.targetY = Math.round(targetY * 256) / 256; - this.targetZ = Math.round(targetZ * 256) / 256; - - return this; - } - - /** - * Cast a ray from the origin to the target point. - * @returns {this} - */ - castRay() { - this.targetDistance = Math.hypot( - this.velocityX = this.targetX - this.originX, - this.velocityY = this.targetY - this.originY, - this.velocityZ = this.targetZ - this.originZ - ); - - this.#targetHit = undefined; - this.#elapsedTime = undefined; - this.#distanceTravelled = undefined; - this.#remainingEnergy = undefined; - this.#destinationX = undefined; - this.#destinationY = undefined; - this.#destinationZ = undefined; - - return this; - } - - /** - * Cast a ray from the origin to the target point. - * @param {boolean} computeElapsedTime - Compute the elapsed time of the ray. - * @param {boolean} computeRemainingEnergy - Compute the remaining energy of the ray. - */ - #castRay(computeElapsedTime, computeRemainingEnergy) { - const targetDistance = this.targetDistance; - - if (targetDistance < this.minD + 0.5 / 256) { - this.#targetHit = true; - this.#elapsedTime = 1; - this.#distanceTravelled = targetDistance; - - if (targetDistance <= this.minR) { - this.#remainingEnergy = 1; - - return; - } - - if (!computeRemainingEnergy) { - return; - } - } - - if (!computeElapsedTime && targetDistance > this.maxD + 0.5 / 256) { - this.#targetHit = false; - this.#remainingEnergy = 0; - - return; - } - - this.#initializeHits(targetDistance); - - const originX = this.originX; - const originY = this.originY; - const originZ = this.originZ; - const velocityX = this.velocityX; - const velocityY = this.velocityY; - const velocityZ = this.velocityZ; - - this.#computeHits(originX, originY, originZ, velocityX, velocityY, velocityZ); - this.#heapifyHits(); - - const volumes = this.volumes; - let travelStage = 0; - let currentTime = 0; - let currentEnergyCost = 0; - let remainingEnergy = 1 / targetDistance; - const almostZeroEnergy = remainingEnergy * 1e-12; - - for (let hit; hit = this.#nextHit();) { - const hitTime = hit.time; - const hitVolumeIndex = hit.volumeIndex; - - if (hitVolumeIndex >= 0) { - const hitVolume = volumes[hitVolumeIndex]; - const hitBoundary = hitVolume.boundaries[hit.boundaryIndex]; - const hitBoundaryState = hitBoundary.state; - - if ((hitBoundary.state ^= hit.boundaryMask) !== 0 && hitBoundaryState !== 0) { - continue; - } - - const hitVolumeState = hitVolume.state; - - if ((hitVolume.state ^= hitBoundary.mask) !== 0 && hitVolumeState !== 0) { - continue; - } - } else { - travelStage++; - } - - const deltaTime = hitTime - currentTime; - const requiredEnergy = deltaTime > 0 ? deltaTime * min(currentEnergyCost, 256) : 0; - - if (remainingEnergy <= requiredEnergy) { - this.#hits.length = 0; - - break; - } - - currentTime = hitTime; - currentEnergyCost = this.#calculateEnergyCost(travelStage); - remainingEnergy -= requiredEnergy; - - if (remainingEnergy <= almostZeroEnergy) { - remainingEnergy = 0; - this.#hits.length = 0; - - break; - } - } - - if (currentEnergyCost !== 0) { - const requiredEnergy = currentTime < 1 ? (1 - currentTime) * currentEnergyCost : 0; - - currentTime = min(currentTime + remainingEnergy / currentEnergyCost, 1); - remainingEnergy -= requiredEnergy; - - if (remainingEnergy <= almostZeroEnergy) { - remainingEnergy = 0; - } - } else if (remainingEnergy !== 0) { - currentTime = 1; - } - - if (currentTime * targetDistance > targetDistance - 0.5 / 256) { - currentTime = 1; - } - - this.#targetHit = currentTime === 1; - this.#elapsedTime = currentTime; - this.#remainingEnergy = min(remainingEnergy * targetDistance, 1); - } - - /** - * The x-coordinate of the velocity of the ray. - * @type {number} - * @readonly - */ - velocityX = 0; - - /** - * The y-coordinate of the velocity of the ray. - * @type {number} - * @readonly - */ - velocityY = 0; - - /** - * The z-coordinate of the velocity of the ray. - * @type {number} - * @readonly - */ - velocityZ = 0; - - /** - * The distance from the origin to the target. - * @type {number} - * @readonly - */ - targetDistance = 0; - - - /** @type {boolean|undefined} */ - #targetHit; - - /** - * Did the ray hit the target? - * @type {boolean} - * @readonly - */ - get targetHit() { - if (this.#targetHit === undefined) { - this.#castRay(false, false); - } - - return this.#targetHit; - } - - /** @type {number|undefined} */ - #elapsedTime; - - /** - * The time that elapsed before the ray reached its destination. - * @type {number} - * @readonly - */ - get elapsedTime() { - if (this.#elapsedTime === undefined) { - this.#castRay(true, false); - } - - return this.#elapsedTime; - } - - /** @type {number|undefined} */ - #distanceTravelled; - - /** - * The distance that the ray travelled before it reached its destination. - * @type {number} - * @readonly - */ - get distanceTravelled() { - return this.#distanceTravelled ??= this.targetDistance * this.elapsedTime; - } - - /** @type {number|undefined} */ - #remainingEnergy; - - /** - * The remaining energy of the ray when it reached its destination. - * @type {number} - * @readonly - */ - get remainingEnergy() { - if (this.#remainingEnergy === undefined) { - this.#castRay(false, true); - } - - return this.#remainingEnergy; - } - - #computeDestination() { - const elapsedTime = this.elapsedTime; - - this.#destinationX = this.originX + this.velocityX * elapsedTime; - this.#destinationY = this.originY + this.velocityY * elapsedTime; - this.#destinationZ = this.originZ + this.velocityZ * elapsedTime; - } - - /** @type {number|undefined} */ - #destinationX; - - /** - * The x-coordinate of the destination of the curreny ray. - * @type {number} - * @readonly - */ - get destinationX() { - if (this.#destinationX === undefined) { - this.#computeDestination(); - } - - return this.#destinationX; - } - - /** @type {number|undefined} */ - #destinationY; - - /** - * The y-coordinate of the destination of the curreny ray. - * @type {number} - * @readonly - */ - get destinationY() { - if (this.#destinationY === undefined) { - this.#computeDestination(); - } - - return this.#destinationY; - } - - /** @type {number|undefined} */ - #destinationZ; - - /** - * The z-coordinate of the destination of the curreny ray. - * @type {number} - * @readonly - */ - get destinationZ() { - if (this.#destinationZ === undefined) { - this.#computeDestination(); - } - - return this.#destinationZ; - } - - /** - * Initialize the hits. - * @param {number} targetDistance - The distance from the origin of the ray to the target. - */ - #initializeHits(targetDistance) { - if (this.minR < targetDistance) { - this.#hits.push(new Hit(this.minR / targetDistance, -1, -1, 0)); - } - - if (this.maxR < targetDistance) { - this.#hits.push(new Hit(this.maxR / targetDistance, -1, -1, 0)); - } - } - - /** - * Compute the hits of all volumes with the ray. - * @param {number} originX - The x-origin of the ray. - * @param {number} originY - The y-origin of the ray. - * @param {number} originZ - The z-origin of the ray. - * @param {number} velocityX - The x-velocity of the ray. - * @param {number} velocityY - The y-velocity of the ray. - * @param {number} velocityZ - The z-velocity of the ray. - */ - #computeHits(originX, originY, originZ, velocityX, velocityY, velocityZ) { - const volumes = this.volumes; - const numVolumes = volumes.length; - const hitQueue = this.#hits; - - for (let volumeIndex = 0; volumeIndex < numVolumes; volumeIndex++) { - const volume = volumes[volumeIndex]; - - if (volume.envelops) { - continue; - } - - volume.computeHits(originX, originY, originZ, velocityX, velocityY, velocityZ, hitQueue, volumeIndex); - } - } - - /** - * Heapify hits. - */ - #heapifyHits() { - const hits = this.#hits; - - for (let i = hits.length >> 1; i--;) { - this.#siftDownHit(hits[i], i); - } - } - - /** - * Get the next this that needs to be processed. - * @returns {Hit} The next hit. - */ - #nextHit() { - const hits = this.#hits; - const numHits = hits.length; - - if (!numHits) { - return; - } - - const nextHit = hits[0]; - const lastHit = hits.pop(); - - if (numHits > 1) { - this.#siftDownHit(lastHit, 0); - } - - return nextHit; - } - - /** - * Sift down the hit. - * @param {Hit} hit - The hit. - * @param {number} i - The current index of the hit. - * @returns {number} The new index of the hit. - */ - #siftDownHit(hit, i) { - const hits = this.#hits; - const numHits = hits.length; - - for (; ;) { - const r = i + 1 << 1; - const l = r - 1; - let j = i; - let h = hit - let tmp; - - if (l < numHits && (tmp = hits[l]).time < h.time) { - j = l; - h = tmp; - } - - if (r < numHits && (tmp = hits[r]).time < h.time) { - j = r; - h = tmp; - } - - if (j === i) { - break; - } - - hits[i] = h; - i = j; - } - - hits[i] = hit; - - return i; - } - - /** - * Compute the current energy cost based on the active senses. - * @param {number} travelStage - The travel stage: 0 for <=minR, 1 for >minR and <=maxR, 2 otherwise. - * @returns {number} The current energy cost. - */ - #calculateEnergyCost(travelStage) { - if (travelStage === 0) { - return 0; - } - - if (travelStage === 2) { - return Infinity; - } - - let computedEnergyCost = 0; - const volumes = this.volumes; - - for (let volumeIndex = 0, numVolumes = volumes.length; volumeIndex < numVolumes; volumeIndex++) { - const volume = volumes[volumeIndex]; - - if (volume.state !== 0) { - continue; - } - - const energyCost = volume.cost; - - switch (volume.operation) { - case 0: computedEnergyCost = energyCost; break; - case 1: computedEnergyCost += energyCost; break; - case 2: computedEnergyCost = max(computedEnergyCost - energyCost, 0); break; - case 3: computedEnergyCost = min(computedEnergyCost, energyCost); break; - case 4: computedEnergyCost = max(computedEnergyCost, energyCost); break; - } - } - - return computedEnergyCost; - } -} diff --git a/scripts/raycasting/figure.mjs b/scripts/raycasting/figure.mjs deleted file mode 100644 index 53ec8ae..0000000 --- a/scripts/raycasting/figure.mjs +++ /dev/null @@ -1,88 +0,0 @@ -import { max, min } from "./math.mjs"; - -/** - * @abstract - */ -export class Figure { - /** - * @param {number} minX - The minimum x-coordinate. - * @param {number} minY - The minimum y-coordinate. - * @param {number} maxX - The maximum x-coordinate. - * @param {number} maxY - The maximum y-coordinate. - * @param {number} [mask] - The mask (31-bit integer). - */ - constructor(minX, minY, maxX, maxY, mask) { - /** - * The minimum x-coordinate. - * @type {number} - * @readonly - */ - this.minX = minX; - /** - * The minimum y-coordinate. - * @type {number} - * @readonly - */ - this.minY = minY; - /** - * The maximum x-coordinate. - * @type {number} - * @readonly - */ - this.maxX = maxX; - /** - * The maximum y-coordinate. - * @type {number} - * @readonly - */ - this.maxY = maxY; - /** - * The bit mask of the figure (31-bit). - * @type {number} - * @readonly - */ - this.mask = (mask ?? -1) & 0x7FFFFFFF; - } - - /** - * Test whether this figure intersects the bounding box. - * @param {number} minX - The minimum x-coordinate. - * @param {number} minY - The minimum y-coordinate. - * @param {number} maxX - The maximum x-coordinate. - * @param {number} maxY - The maximum y-coordinate. - * @returns {boolean} False if the bounding box does not intersect with the figure. - */ - intersectsBounds(minX, minY, maxX, maxY) { - return max(minX, this.minX) <= min(maxX, this.maxX) - && max(minY, this.minY) <= min(maxY, this.maxY); - } - - /** - * Test whether the figure contains the bounding box. - * @param {number} minX - The minimum x-coordinate. - * @param {number} minY - The minimum y-coordinate. - * @param {number} maxX - The maximum x-coordinate. - * @param {number} maxY - The maximum y-coordinate. - * @returns {boolean} True if the bounding box to contains the figure. - * @abstract - */ - containsBounds(minX, minY, maxX, maxY) { - return false; - } - - /** - * Compute the hits of the figure with the ray. - * @param {number} originX - The x-origin of the ray. - * @param {number} originY - The y-origin of the ray. - * @param {number} velocityX - The x-velocity of the ray. - * @param {number} velocityY - The y-velocity of the ray. - * @param {Hit[]|null} hitQueue - The hit queue. - * @param {number} volumeIndex - The index of the volume. - * @param {number} boundaryIndex - The index of the boundary. - * @returns {number} The state that encodes whether the ray originates in the figure. - * @abstract - */ - computeHits(originX, originY, velocityX, velocityY, hitQueue, volumeIndex, boundaryIndex) { - throw new Error("Not implemented"); - } -} diff --git a/scripts/raycasting/figures/_index.mjs b/scripts/raycasting/figures/_index.mjs deleted file mode 100644 index 5a9d832..0000000 --- a/scripts/raycasting/figures/_index.mjs +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./circle.mjs"; -export * from "./ellipse.mjs"; -export * from "./polygon.mjs"; -export * from "./rectangle.mjs"; -export * from "./tile.mjs"; diff --git a/scripts/raycasting/figures/circle.mjs b/scripts/raycasting/figures/circle.mjs deleted file mode 100644 index 4f4c780..0000000 --- a/scripts/raycasting/figures/circle.mjs +++ /dev/null @@ -1,134 +0,0 @@ -import { Hit } from "../hit.mjs"; -import { Figure } from "../figure.mjs"; - -export class Circle extends Figure { - /** - * The x-coordinate of the center. - * @type {number} - */ - #centerX; - - /** - * The y-coordinate of the center. - * @type {number} - */ - #centerY; - - /** - * The radius. - * @type {number} - */ - #radius; - - /** - * @param {object} args - * @param {number} args.centerX - * @param {number} args.centerY - * @param {number} args.radius - * @param {number} [args.mask] - */ - constructor({ centerX, centerY, radius, mask }) { - const minX = centerX - radius; - const minY = centerY - radius; - const maxX = centerX + radius; - const maxY = centerY + radius; - - super(minX, minY, maxX, maxY, mask); - - this.#centerX = centerX; - this.#centerY = centerY; - this.#radius = radius; - } - - /** @override */ - containsBounds(minX, minY, maxX, maxY) { - const cx = this.#centerX; - const cy = this.#centerY; - const r = this.#radius; - const rr = r * r; - let x, y; - - x = minX - cx; - y = minY - cy; - - if (x * x + y * y > rr) { - return false; - } - - x = maxX - cx; - y = minY - cy; - - if (x * x + y * y > rr) { - return false; - } - - x = maxX - cx; - y = maxY - cy; - - if (x * x + y * y > rr) { - return false; - } - - x = minX - cx; - y = maxY - cy; - - if (x * x + y * y > rr) { - return false; - } - - return true; - } - - /** @override */ - computeHits(originX, originY, velocityX, velocityY, hitQueue, volumeIndex, boundaryIndex) { - const ir = 1 / this.#radius; - const x = (originX - this.#centerX) * ir; - const y = (originY - this.#centerY) * ir; - const dx = velocityX * ir; - const dy = velocityY * ir; - const a = dx * dx + dy * dy; - const b = dx * x + dy * y; - const c = x * x + y * y - 1; - let time1, time2; - let state = 0; - - if (c !== 0) { - const d = b * b - a * c; - - if (d <= 1e-6) { - return state; - } - - const f = Math.sqrt(d); - - if (b !== 0) { - time1 = (-b - Math.sign(b) * f) / a; - time2 = c / (a * time1); - } else { - time1 = f / a; - time2 = -time1; - } - } else { - time1 = 0; - time2 = -b / a; - } - - if (time1 > 0) { - if (time1 < 1) { - hitQueue?.push(new Hit(time1, volumeIndex, boundaryIndex, this.mask)); - } - - state ^= this.mask; - } - - if (time2 > 0) { - if (time2 < 1) { - hitQueue?.push(new Hit(time2, volumeIndex, boundaryIndex, this.mask)); - } - - state ^= this.mask; - } - - return state; - } -} diff --git a/scripts/raycasting/figures/ellipse.mjs b/scripts/raycasting/figures/ellipse.mjs deleted file mode 100644 index 526cba7..0000000 --- a/scripts/raycasting/figures/ellipse.mjs +++ /dev/null @@ -1,130 +0,0 @@ -import { Hit } from "../hit.mjs"; -import { Figure } from "../figure.mjs"; - -export class Ellipse extends Figure { - /** - * The transform matrix. - * @type {Float64Array} - */ - #matrix = new Float64Array(6); - - /** - * @param {object} args - * @param {number} args.centerX - * @param {number} args.centerY - * @param {number} args.radiusX - * @param {number} args.radiusY - * @param {number} [args.rotation=0] - * @param {number} [args.mask] - */ - constructor({ centerX, centerY, radiusX, radiusY, rotation = 0, mask }) { - const cos = Math.cos(rotation); - const sin = Math.sin(rotation); - const deltaX = Math.hypot(radiusX * cos, radiusY * sin); - const deltaY = Math.hypot(radiusX * sin, radiusY * cos); - const minX = centerX - deltaX; - const minY = centerY - deltaY; - const maxX = centerX + deltaX; - const maxY = centerY + deltaY; - - super(minX, minY, maxX, maxY, mask); - - const matrix = this.#matrix; - const m0 = matrix[0] = cos / radiusX; - const m1 = matrix[1] = -sin / radiusY; - const m2 = matrix[2] = sin / radiusX; - const m3 = matrix[3] = cos / radiusY; - - matrix[4] = -(centerX * m0 + centerY * m2); - matrix[5] = -(centerX * m1 + centerY * m3); - } - - /** @override */ - containsBounds(minX, minY, maxX, maxY) { - const [ta, tb, tc, td, tx, ty] = this.#matrix; - let x, y; - - x = ta * minX + tc * minY + tx; - y = tb * minX + td * minY + ty; - - if (x * x + y * y > 1) { - return false; - } - - x = ta * maxX + tc * minY + tx; - y = tb * maxX + td * minY + ty; - - if (x * x + y * y > 1) { - return false; - } - - x = ta * maxX + tc * maxY + tx; - y = tb * maxX + td * maxY + ty; - - if (x * x + y * y > 1) { - return false; - } - - x = ta * minX + tc * maxY + tx; - y = tb * minX + td * maxY + ty; - - if (x * x + y * y > 1) { - return false; - } - - return true; - } - - /** @override */ - computeHits(originX, originY, velocityX, velocityY, hitQueue, volumeIndex, boundaryIndex) { - const [ta, tb, tc, td, tx, ty] = this.#matrix; - const x = ta * originX + tc * originY + tx; - const y = tb * originX + td * originY + ty; - const dx = ta * velocityX + tc * velocityY; - const dy = tb * velocityX + td * velocityY; - const a = dx * dx + dy * dy; - const b = dx * x + dy * y; - const c = x * x + y * y - 1; - let time1, time2; - let state = 0; - - if (c !== 0) { - const d = b * b - a * c; - - if (d <= 1e-6) { - return state; - } - - const f = Math.sqrt(d); - - if (b !== 0) { - time1 = (-b - Math.sign(b) * f) / a; - time2 = c / (a * time1); - } else { - time1 = f / a; - time2 = -time1; - } - } else { - time1 = 0; - time2 = -b / a; - } - - if (time1 > 0) { - if (time1 < 1) { - hitQueue?.push(new Hit(time1, volumeIndex, boundaryIndex, this.mask)); - } - - state ^= this.mask; - } - - if (time2 > 0) { - if (time2 < 1) { - hitQueue?.push(new Hit(time2, volumeIndex, boundaryIndex, this.mask)); - } - - state ^= this.mask; - } - - return state; - } -} diff --git a/scripts/raycasting/figures/polygon.mjs b/scripts/raycasting/figures/polygon.mjs deleted file mode 100644 index 16b8500..0000000 --- a/scripts/raycasting/figures/polygon.mjs +++ /dev/null @@ -1,152 +0,0 @@ -import { Hit } from "../hit.mjs"; -import { max, min } from "../math.mjs"; -import { Figure } from "../figure.mjs"; - -export class Polygon extends Figure { - /** - * The points of the polygon. - * @type {Float64Array} - */ - #points; - - /** - * @param {object} args - * @param {number[]} args.points - * @param {number} [args.mask] - */ - constructor({ points, mask }) { - const n = points.length; - const p = new Float64Array(n); - let minX; - let minY; - let maxX; - let maxY; - - if (n > 0) { - minX = p[0] = Math.round(points[0] * 256) / 256; - minY = p[1] = Math.round(points[1] * 256) / 256; - maxX = minX; - maxY = minY; - - for (let i = 2; i < n; i += 2) { - const x = p[i] = Math.round(points[i] * 256) / 256; - const y = p[i + 1] = Math.round(points[i + 1] * 256) / 256; - - minX = min(minX, x); - minY = min(minY, y); - maxX = max(maxX, x); - maxY = max(maxY, y); - } - } else { - minX = minY = maxX = maxY = 0; - } - - super(minX, minY, maxX, maxY, mask); - - /** - * The points of the polygon. - * @type {Float64Array} - * @readonly - */ - this.#points = p; - } - - /** @override */ - containsBounds(minX, minY, maxX, maxY) { - const points = this.#points; - const n = points.length; - const centerX = (minX + maxX) / 2; - const centerY = (minY + maxY) / 2; - let centerInside = false; - - for (let i = 0, x0 = points[n - 2], y0 = points[n - 1]; i < n; i += 2) { - const x1 = points[i]; - const y1 = points[i + 1]; - - if ((y1 > centerY) !== (y0 > centerY) - && centerX < (x0 - x1) * ((centerY - y1) / (y0 - y1)) + x1) { - centerInside = !centerInside; - } - - x0 = x1; - y0 = y1; - } - - if (!centerInside) { - return false; - } - - for (let i = 0, x0 = points[n - 2], y0 = points[n - 1]; i < n; i += 2) { - const x1 = points[i]; - const y1 = points[i + 1]; - const px = 1 / (x1 - x0); - const py = 1 / (y1 - y0); - - let t1 = (minX - x0) * px; - let t2 = (maxX - x0) * px; - let time1 = min(max(t1, 0), max(t2, 0)); - let time2 = max(min(t1, Infinity), min(t2, Infinity)); - - t1 = (minY - y0) * py; - t2 = (maxY - y0) * py; - time1 = min(max(t1, time1), max(t2, time1)); - time2 = max(min(t1, time2), min(t2, time2)); - - if (time1 <= time2 && time1 < 1 && time2 > 0) { - return false; - } - - x0 = x1; - y0 = y1; - } - - return true; - } - - /** @override */ - computeHits(originX, originY, velocityX, velocityY, hitQueue, volumeIndex, boundaryIndex) { - const points = this.#points; - const m = points.length; - let i = 0; - let x0 = points[m - 2]; - let y0 = points[m - 1]; - let state = 0; - - do { - const x1 = points[i++]; - const y1 = points[i++]; - const dx = x1 - x0; - const dy = y1 - y0; - const q = velocityX * dy - velocityY * dx; - - while (q !== 0) { - const ox = x0 - originX; - const oy = y0 - originY; - const u = (ox * velocityY - oy * velocityX) / q; - - if (u < 0 || u > 1 || u === 0 && q > 0 || u === 1 && q < 0) { - break; - } - - const time = (ox * dy - oy * dx) / q; - - if (time <= 0) { - break; - } - - if (time < 1) { - hitQueue?.push(new Hit(time, volumeIndex, boundaryIndex, this.mask)); - } - - state ^= this.mask; - - break; - } - - x0 = x1; - y0 = y1; - } while (i !== m); - - return state; - } -} diff --git a/scripts/raycasting/figures/rectangle.mjs b/scripts/raycasting/figures/rectangle.mjs deleted file mode 100644 index a2ec6e8..0000000 --- a/scripts/raycasting/figures/rectangle.mjs +++ /dev/null @@ -1,149 +0,0 @@ -import { Hit } from "../hit.mjs"; -import { max, min } from "../math.mjs"; -import { Figure } from "../figure.mjs"; - -export class Rectangle extends Figure { - /** - * The transform matrix. - * @type {Float64Array} - */ - #matrix = new Float64Array(6); - - /** - * @param {object} args - * @param {number} args.centerX - * @param {number} args.centerY - * @param {number} args.width - * @param {number} args.height - * @param {number} [args.rotation=0] - * @param {number} [args.mask] - */ - constructor({ centerX, centerY, width, height, rotation = 0, mask }) { - const cos = Math.cos(rotation); - const sin = Math.sin(rotation); - const l = -width / 2; - const r = -l; - const t = -height / 2; - const b = -t; - const x0 = cos * l - sin * t; - const x1 = cos * r - sin * t; - const x2 = cos * r - sin * b; - const x3 = cos * l - sin * b; - const minX = Math.min(x0, x1, x2, x3) + centerX; - const maxX = Math.max(x0, x1, x2, x3) + centerX; - const y0 = sin * l + cos * t; - const y1 = sin * r + cos * t; - const y2 = sin * r + cos * b; - const y3 = sin * l + cos * b; - const minY = Math.min(y0, y1, y2, y3) + centerY; - const maxY = Math.max(y0, y1, y2, y3) + centerY; - - super(minX, minY, maxX, maxY, mask); - - const matrix = this.#matrix; - const m0 = matrix[0] = cos / width; - const m1 = matrix[1] = -sin / height; - const m2 = matrix[2] = sin / width; - const m3 = matrix[3] = cos / height; - - matrix[4] = 0.5 - (centerX * m0 + centerY * m2); - matrix[5] = 0.5 - (centerX * m1 + centerY * m3); - } - - /** @override */ - containsBounds(minX, minY, maxX, maxY) { - const [ta, tb, tc, td, tx, ty] = this.#matrix; - let x, y; - - x = ta * minX + tc * minY + tx; - - if (x < 0 || x > 1) { - return false; - } - - x = ta * maxX + tc * minY + tx; - - if (x < 0 || x > 1) { - return false; - } - - x = ta * maxX + tc * maxY + tx; - - if (x < 0 || x > 1) { - return false; - } - - x = ta * minX + tc * maxY + tx; - - if (x < 0 || x > 1) { - return false; - } - - y = tb * minX + td * minY + ty; - - if (y < 0 || y > 1) { - return false; - } - - y = tb * maxX + td * minY + ty; - - if (y < 0 || y > 1) { - return false; - } - - y = tb * maxX + td * maxY + ty; - - if (y < 0 || y > 1) { - return false; - } - - y = tb * minX + td * maxY + ty; - - if (y < 0 || y > 1) { - return false; - } - - return true; - } - - /** @override */ - computeHits(originX, originY, velocityX, velocityY, hitQueue, volumeIndex, boundaryIndex) { - const [ta, tb, tc, td, tx, ty] = this.#matrix; - const x = ta * originX + tc * originY + tx; - const y = tb * originX + td * originY + ty; - const dx = ta * velocityX + tc * velocityY; - const dy = tb * velocityX + td * velocityY; - const px = -1 / dx; - const py = -1 / dy; - - let t1 = x * px; - let t2 = (x - 1) * px; - let time1 = min(max(t1, 0), max(t2, 0)); - let time2 = max(min(t1, Infinity), min(t2, Infinity)); - - t1 = y * py; - t2 = (y - 1) * py; - time1 = min(max(t1, time1), max(t2, time1)); - time2 = max(min(t1, time2), min(t2, time2)); - - let state = 0; - - if (time1 <= time2 && time1 < 1 && time2 > 0) { - if (time1 <= 0) { - state = this.mask; - } - - if (hitQueue) { - if (time1 > 0) { - hitQueue.push(new Hit(time1, volumeIndex, boundaryIndex, this.mask)); - } - - if (time2 < 1) { - hitQueue.push(new Hit(time2, volumeIndex, boundaryIndex, this.mask)); - } - } - } - - return state; - } -} diff --git a/scripts/raycasting/figures/tile.mjs b/scripts/raycasting/figures/tile.mjs deleted file mode 100644 index 66b99a2..0000000 --- a/scripts/raycasting/figures/tile.mjs +++ /dev/null @@ -1,308 +0,0 @@ -import { Hit } from "../hit.mjs"; -import { max, min } from "../math.mjs"; -import { Figure } from "../figure.mjs"; - -export class Tile extends Figure { - /** - * The transform matrix. - * @type {Float64Array} - */ - #matrix = new Float64Array(6); - - /** - * The width. - * @type {number} - */ - #width; - - /** - * The height. - * @type {number} - */ - #height; - - /** - * The signed distance field. - * @type {Float64Array} - */ - #field; - - /** - * @param {object} args - * @param {number} args.centerX - * @param {number} args.centerY - * @param {number} args.width - * @param {number} args.height - * @param {number} [args.rotation=0] - * @param {{ - * pixels: (number|boolean)[], - * offset: number, - * stride: number, - * width: number, - * height: number, - * minX: number, - * minY: number, - * maxX: number, - * maxY: number, - * threshold: number - * }} args.texture - * @param {number} [args.mask] - */ - constructor({ centerX, centerY, width, height, rotation = 0, texture, mask }) { - const textureWidth = texture.width; - const textureHeight = texture.height; - const textureMinX = texture.minX ?? 0; - const textureMinY = texture.minY ?? 0; - const textureMaxX = texture.maxX ?? textureWidth; - const textureMaxY = texture.maxY ?? textureHeight; - const textureScaleX = textureWidth / width; - const textureScaleY = textureHeight / height; - const cos = Math.cos(rotation); - const sin = Math.sin(rotation); - const halfWidth = width / 2; - const halfHeight = height / 2; - const l = textureMinX / textureScaleX - halfWidth; - const r = textureMaxX / textureScaleX - halfWidth; - const t = textureMinY / textureScaleY - halfHeight; - const b = textureMaxY / textureScaleY - halfHeight; - const x0 = cos * l - sin * t; - const x1 = cos * r - sin * t; - const x2 = cos * r - sin * b; - const x3 = cos * l - sin * b; - const minX = Math.min(x0, x1, x2, x3) + centerX; - const maxX = Math.max(x0, x1, x2, x3) + centerX; - const y0 = sin * l + cos * t; - const y1 = sin * r + cos * t; - const y2 = sin * r + cos * b; - const y3 = sin * l + cos * b; - const minY = Math.min(y0, y1, y2, y3) + centerY; - const maxY = Math.max(y0, y1, y2, y3) + centerY; - - super(minX, minY, maxX, maxY, mask); - - this.#width = textureMaxX - textureMinX + 2; - this.#height = textureMaxY - textureMinY + 2; - - const matrix = this.#matrix; - const m0 = matrix[0] = cos; - const m1 = matrix[1] = -sin; - const m2 = matrix[2] = sin; - const m3 = matrix[3] = cos; - - matrix[4] = halfWidth - (centerX * m0 + centerY * m2); - matrix[5] = halfHeight - (centerX * m1 + centerY * m3); - - matrix[0] *= textureScaleX; - matrix[1] *= textureScaleY; - matrix[2] *= textureScaleX; - matrix[3] *= textureScaleY; - matrix[4] *= textureScaleX; - matrix[5] *= textureScaleY; - matrix[4] += 1 - textureMinX; - matrix[5] += 1 - textureMinY; - - const textureStrideX = texture.stride ?? 1; - const textureStrideY = textureWidth * textureStrideX; - const signedDistanceField = this.#field = sdf( - texture.pixels, - texture.offset ?? 0, - textureStrideX, - textureStrideY, - textureMinX, - textureMinY, - textureMaxX, - textureMaxY, - texture.threshold ?? 0 - ); - - for (let i = 0, n = signedDistanceField.length; i < n; i++) { - const signedDistance = signedDistanceField[i]; - - signedDistanceField[i] = Math.sign(signedDistance) - * max(Math.abs(signedDistance) - 1, 0.5); - } - } - - /** @override */ - computeHits(originX, originY, velocityX, velocityY, hitQueue, volumeIndex, boundaryIndex) { - const [ta, tb, tc, td, tx, ty] = this.#matrix; - const w = this.#width; - const h = this.#height; - let x = ta * originX + tc * originY + tx; - let y = tb * originX + td * originY + ty; - const dx = ta * velocityX + tc * velocityY; - const dy = tb * velocityX + td * velocityY; - const px = 1 / dx; - const py = 1 / dy; - - let t1 = (1 - x) * px; - let t2 = (w - 1 - x) * px; - let time1 = min(max(t1, 0), max(t2, 0)); - let time2 = max(min(t1, Infinity), min(t2, Infinity)); - - t1 = (1 - y) * py; - t2 = (h - 1 - y) * py; - time1 = min(max(t1, time1), max(t2, time1)); - time2 = max(min(t1, time2), min(t2, time2)); - - let state = 0; - - if (time1 <= time2 && time1 < 1 && time2 > 0) { - const f = this.#field; - let inside; - - if (time1 <= 0) { - time1 = 0; - inside = f[(y | 0) * w + (x | 0)] < 0; - - if (inside) { - state = this.mask; - } - } else { - inside = false; - } - - if (hitQueue) { - const invTravelDistance = 1 / Math.sqrt(dx * dx + dy * dy); - - do { - const signedDistance = f[(y + dy * time1 | 0) * w + (x + dx * time1 | 0)] - * invTravelDistance; - - if (inside !== signedDistance < 0) { - inside = !inside; - - if (time1 < 1) { - hitQueue.push(new Hit(time1, volumeIndex, boundaryIndex, this.mask)); - } - } - - time1 += Math.abs(signedDistance); - } while (time1 <= time2); - - if (inside && time2 <= 1) { - hitQueue.push(new Hit(time2, volumeIndex, boundaryIndex, this.mask)); - } - } - } - - return state; - } -} - -/** - * The value representing infinity. Used by {@link edt}. - * @type {number} - */ -const EDT_INF = 1e20; - -/** - * Generate the 2D Euclidean signed distance field. - * @param {(number|boolean)[]} data - The elements. - * @param {number} offset - The offset of the first element in `data`. - * @param {number} strideX - The distance between consecutive elements in a row of `data`. - * @param {number} strideY - The distance between consecutive elements in a column of `data`. - * @param {number} minX - The minimum x-coordinate of the rectangle. - * @param {number} minY - The minimum y-coordinate of the rectangle. - * @param {number} maxX - The maximum x-coordinate of the rectangle. - * @param {number} maxY - The maximum x-coordinate of the rectangle. - * @param {number} [threshold=0] - The threshold that needs to be exceeded for a pixel to be inner. - * @returns {Float64Array} - The signed distance field with a 1 pixel padding. - */ -function sdf(data, offset, strideX, strideY, minX, minY, maxX, maxY, threshold = 0) { - const width = maxX - minX + 2; - const height = maxY - minY + 2; - const size = width * height; - const capacity = Math.max(width, height); - const temp = new ArrayBuffer(8 * size + 20 * capacity + 8); - const inner = new Float64Array(temp, 0, size); - const outer = new Float64Array(size).fill(EDT_INF); - - for (let y = minY, j = width + 1; y < maxY; y++, j += 2) { - for (let x = minX; x < maxX; x++, j++) { - const a = data[offset + x * strideX + y * strideY]; - - if (a > threshold) { - inner[j] = EDT_INF; - outer[j] = 0; - } - } - } - - const f = new Float64Array(temp, inner.byteLength, capacity); - const z = new Float64Array(temp, f.byteOffset + f.byteLength, capacity + 1); - const v = new Int32Array(temp, z.byteOffset + z.byteLength, capacity); - - edt(inner, width, height, f, v, z); - edt(outer, width, height, f, v, z); - - for (let i = 0; i < size; i++) { - outer[i] = Math.sqrt(outer[i]) - Math.sqrt(inner[i]); - } - - return outer; -} - -/** - * 2D Euclidean squared distance transform by Felzenszwalb & Huttenlocher. - * @param {Float64Array} grid - The grid. - * @param {number} width - The width of the grid. - * @param {number} height - The height of the grid. - * @param {Float64Array} f - The temporary source data, which returns the y of the parabola vertex at x. - * @param {Int32Array} v - The temporary used to store x-coordinates of parabola vertices. - * @param {Float64Array} z - The temporary used to store x-coordinates of parabola intersections. - */ -function edt(grid, width, height, f, v, z) { - for (let x = 0; x < width; x++) { - edt1d(grid, x, width, height, f, v, z); - } - - for (let y = 0; y < height; y++) { - edt1d(grid, y * width, 1, width, f, v, z); - } -} - -/** - * 1D squared distance transform. Used by {@link edt}. - * @param {Float64Array} grid - The grid. - * @param {number} offset - The offset. - * @param {number} stride - The stride. - * @param {number} length - The length. - * @param {Float64Array} f - The temporary source data, which returns the y of the parabola vertex at x. - * @param {Int32Array} v - The temporary used to store x-coordinates of parabola vertices. - * @param {Float64Array} z - The temporary used to store x-coordinates of parabola intersections. - */ -function edt1d(grid, offset, stride, length, f, v, z) { - f[0] = grid[offset]; - v[0] = 0; - z[0] = -EDT_INF; - z[1] = EDT_INF; - - for (let q = 1, k = 0, s = 0; q < length; q++) { - f[q] = grid[offset + q * stride]; - - const q2 = q * q; - - do { - const r = v[k]; - - s = (f[q] - f[r] + q2 - r * r) / (q - r) * 0.5; - } while (s <= z[k] && k--); - - k++; - v[k] = q; - z[k] = s; - z[k + 1] = EDT_INF; - } - - for (let q = 0, k = 0; q < length; q++) { - while (z[k + 1] < q) { - k++; - } - - const r = v[k]; - const qr = q - r; - - grid[offset + q * stride] = f[r] + qr * qr; - } -} diff --git a/scripts/raycasting/hit.mjs b/scripts/raycasting/hit.mjs deleted file mode 100644 index 7233bdd..0000000 --- a/scripts/raycasting/hit.mjs +++ /dev/null @@ -1,37 +0,0 @@ -/** - * The hit of a ray with a volume. - */ -export class Hit { - /** - * @param {number} time - The time the ray hits the volume. - * @param {number} volumeIndex - The index of the volume that was hit. - * @param {number} boundaryIndex - The index of the boundary that was hit. - * @param {number} boundaryMask - The bit mask indicating which part of the boundary were hit. - */ - constructor(time, volumeIndex, boundaryIndex, boundaryMask) { - /** - * The time the ray hits the volume. - * @type {number} - * @readonly - */ - this.time = time; - /** - * The index of the volume that was hit. - * @type {number} - * @readonly - */ - this.volumeIndex = volumeIndex; - /** - * The index of the boundary that was hit. - * @type {number} - * @readonly - */ - this.boundaryIndex = boundaryIndex; - /** - * The bit mask indicating which part of the boundary were hit. - * @type {number} - * @readonly - */ - this.boundaryMask = boundaryMask; - } -} diff --git a/scripts/raycasting/operation.mjs b/scripts/raycasting/operation.mjs deleted file mode 100644 index 537e462..0000000 --- a/scripts/raycasting/operation.mjs +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @enum {number} - */ -export const Operation = Object.freeze({ - SET: 0, - ADD: 1, - SUB: 2, - MIN: 3, - MAX: 4 -}); diff --git a/scripts/raycasting/volume.mjs b/scripts/raycasting/volume.mjs deleted file mode 100644 index ad836e6..0000000 --- a/scripts/raycasting/volume.mjs +++ /dev/null @@ -1,155 +0,0 @@ -/** - * The volume used by {@link RayCaster}. - */ -export class Volume { - /** - * @param {Boundary[]} boundaries - The boundaries. - * @param {number} priority - The priority. - * @param {Operation} operation - The operation used in the energy calculation. - * @param {number} cost - The energy cost. - */ - constructor(boundaries, priority, operation, cost) { - /** - * The boundaries. - * @type {Boundary[]} - * @readonly - */ - this.boundaries = boundaries; - /** - * The priority. - * @type {number} - * @readonly - */ - this.priority = priority; - /** - * The operation. - * @type {Operation} - * @readonly - */ - this.operation = operation; - /** - * The energy cost. - * @type {number} - * @readonly - */ - this.cost = cost; - /** - * The current state of the ray relative to this volume. - * If zero, the ray is currently inside the volume. - * @type {number} - */ - this.state = 0; - /** - * Skip hits computation? True if rays cannot leave this volume. - * @type {boolean} - * @readonly - */ - this.envelops = false; - } - - /** @type {number} */ - #state = -1; - - /** - * Clone this volume. - * @returns {Volume} - */ - clone() { - const volume = new this.constructor(this.boundaries.map((o) => o.clone()), this.priority, this.operation, this.cost); - - volume.#state = this.#state; - - return volume; - } - - /** - * Initialize this volume given the bounding box of the ray caster. - * @param {number} minX - The minimum x-coordinate. - * @param {number} minY - The minimum y-coordinate. - * @param {number} minZ - The minimum z-coordinate. - * @param {number} maxX - The maximum x-coordinate. - * @param {number} maxY - The maximum y-coordinate. - * @param {number} maxZ - The maximum z-coordinate. - * @returns {boolean} Returns false if the volume can be discarded. - */ - initialize(minX, minY, minZ, maxX, maxY, maxZ) { - const boundaries = this.boundaries; - - for (let boundaryIndex = boundaries.length - 1; boundaryIndex >= 0; boundaryIndex--) { - const boundary = boundaries[boundaryIndex]; - - if (!boundary.initialize(minX, minY, minZ, maxX, maxY, maxZ)) { - boundaries[boundaryIndex] = boundaries[boundaries.length - 1]; - boundaries.length--; - } - } - - let state = this.#state; - - for (let boundaryIndex = boundaries.length - 1; boundaryIndex >= 0; boundaryIndex--) { - const boundary = boundaries[boundaryIndex]; - - if (boundary.envelops) { - boundaries[boundaryIndex] = boundaries[boundaries.length - 1]; - boundaries.length--; - state ^= boundary.mask; - } - } - - this.#state = state; - - if (state !== 0 && boundaries.length === 0) { - return false; - } - - if (state === 0 && boundaries.length === 0) { - this.envelops = true; - } - - const operation = this.operation; - - if (operation === 1 || operation === 2) { - if (this.cost === 0) { - return false; - } - } else if (operation === 3) { - if (this.cost === Infinity) { - return false; - } - } else if (operation === 4) { - if (this.cost === 0) { - return false; - } - } - - return true; - } - - /** - * Compute the hits of the volume with the ray. - * @param {number} originX - The x-origin of the ray. - * @param {number} originY - The y-origin of the ray. - * @param {number} originZ - The y-origin of the ray. - * @param {number} velocityX - The x-velocity of the ray. - * @param {number} velocityY - The y-velocity of the ray. - * @param {number} velocityZ - The y-velocity of the ray. - * @param {Hit[]|null} hitQueue - The hit queue. - * @param {number} volumeIndex - The index of the volume. - * @returns {number} The state that encodes whether the ray originates in the volume. - */ - computeHits(originX, originY, originZ, velocityX, velocityY, velocityZ, hitQueue, volumeIndex) { - let state = this.#state; - const boundaries = this.boundaries; - const numBoundaries = boundaries.length; - - for (let boundaryIndex = 0; boundaryIndex < numBoundaries; boundaryIndex++) { - const boundary = boundaries[boundaryIndex]; - - state ^= boundary.computeHits(originX, originY, originZ, velocityX, velocityY, velocityZ, hitQueue, volumeIndex, boundaryIndex); - } - - this.state = state; - - return state; - } -} diff --git a/scripts/utils/shape.js b/scripts/utils/shape.js deleted file mode 100644 index e7ab549..0000000 --- a/scripts/utils/shape.js +++ /dev/null @@ -1,1418 +0,0 @@ -export class Shape { - /** - * The resolution (precision). - * @type {number} - * @readonly - */ - static RESOLUTION = 256; - - /** - * Create from PIXI shape. - * @param {PIXI.Rectangle|PIXI.RoundedRectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon} shape - The shape. - * @param {PIXI.Matrix} [transform] - The transform. - * @returns {Shape} - */ - static from(shape, transform) { - if (shape instanceof Shape) { - if (transform && shape.transform) { - transform = transform.clone().append(shape.transform); - } else { - transform = transform ?? shape.transform; - } - - if (!transform) { - return shape; - } - - shape = shape.shape; - } - - return new this(shape, transform); - } - - /** - * Create from Clipper path. - * @param {{X: number, Y: number}[]} path - The Clipper path. - * @param {number} [resolution=Shape.RESOLUTION] - The resolution. - * @returns {Shape} - */ - static fromClipper(path, resolution = Shape.RESOLUTION) { - return this.from(this.createPolygonFromClipper(path, resolution)); - } - - /** - * The shape. - * @type {PIXI.Rectangle|PIXI.RoundedRectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon} - * @readonly - */ - shape; - - /** - * The transform. - * @type {PIXI.Matrix} - * @readonly - */ - transform; - - /** - * The bounds. - * @type {PIXI.Rectangle} - */ - #bounds; - - /** - * The contour. - * @type {number[]} - */ - #contour; - - /** - * The area. - * @type {number} - */ - #area; - - /** - * Not simple (`0`), weakly simple (`1`), or strictly simple (`2`). - * @type {0|1|2} - */ - #simple; - - /** - * @param {PIXI.Rectangle|PIXI.RoundedRectangle|PIXI.Circle|PIXI.Ellipse|PIXI.Polygon} shape - The shape. - * @param {PIXI.Matrix} [transform] - The transform. - */ - constructor(shape, transform) { - const originalShape = shape; - - { - const type = shape.type; - - if (type === PIXI.SHAPES.RECT || type === PIXI.SHAPES.RREC) { - if (shape.width < 0 || shape.height < 0) { - shape = new PIXI.Rectangle(shape.x, shape.y, Math.max(shape.width, 0), Math.max(shape.height, 0)); - } - } else if (type === PIXI.SHAPES.ELIP) { - if (shape.width < 0 || shape.height < 0) { - shape = new PIXI.Ellipse(shape.x, shape.y, Math.max(shape.width, 0), Math.max(shape.height, 0)); - } - } else if (type === PIXI.SHAPES.CIRC) { - if (shape.radius < 0) { - shape = new PIXI.Circle(shape.x, shape.y, 0); - } - } - } - - { - const type = shape.type; - - if (transform && type !== PIXI.SHAPES.POLY) { - const { a, b, c, d, tx, ty } = transform; - const bc0 = Math.abs(b) < 1e-4 && Math.abs(c) < 1e-4; - - if (bc0 || Math.abs(a) < 1e-4 && Math.abs(d) < 1e-4) { - if (type === PIXI.SHAPES.RECT) { - shape = new PIXI.Rectangle(shape.x, shape.y, shape.width, shape.height); - transform = null; - } else if (type === PIXI.SHAPES.RREC) { - if (bc0 && a === d || !bc0 && b === c) { - shape = new PIXI.RoundedRectangle(shape.x, shape.y, shape.width, shape.height, shape.radius); - transform = null; - } - } else if (type === PIXI.SHAPES.CIRC) { - shape = new PIXI.Ellipse(shape.x, shape.y, shape.radius, shape.radius); - transform = null; - } else if (type === PIXI.SHAPES.ELIP) { - shape = new PIXI.Ellipse(shape.x, shape.y, shape.width, shape.height); - transform = null; - } - - if (!transform) { - const { x, y, width, height } = shape; - - if (bc0) { - shape.x = x * a + tx; - shape.y = y * d + ty; - shape.width = width * a; - shape.height = height * d; - } else { - shape.x = y * c + tx; - shape.y = x * b + ty; - shape.width = height * c; - shape.height = width * b; - } - - if (shape.type === PIXI.SHAPES.RECT || shape.type === PIXI.SHAPES.RREC) { - const x = shape.width >= 0 ? shape.x : shape.x + shape.width; - const y = shape.height >= 0 ? shape.y : shape.y + shape.height; - - shape.x = x; - shape.y = y; - } - - shape.width = Math.abs(shape.width); - shape.height = Math.abs(shape.height); - } - } else if (Math.abs(a * b + c * d) < 1e-4) { - if (type === PIXI.SHAPES.CIRC) { - const radius = shape.radius; - - shape = new PIXI.Ellipse(shape.x, shape.y, radius, radius); - transform = null; - } else if (type === PIXI.SHAPES.ELIP) { - if (shape.width === shape.height) { - const radius = shape.width; - - shape = new PIXI.Ellipse(shape.x, shape.y, radius, radius); - transform = null; - } - } else if (type === PIXI.SHAPES.RREC) { - const { width, height } = shape; - - if (shape.radius >= Math.max(width, height) / 2) { - const radius = Math.min(width, height) / 2; - - shape = new PIXI.Ellipse(shape.x + width / 2, shape.y + height / 2, radius, radius); - transform = null; - } - } - - if (!transform) { - const { x, y } = shape; - const radius = shape.width; - - shape.x = x * a + y * c + tx; - shape.y = x * b + y * d + ty; - shape.width = radius * Math.sqrt(a * a + c * c); - shape.height = radius * Math.sqrt(b * b + d * d); - } - } - } - } - - { - const type = shape.type; - - if (type === PIXI.SHAPES.RREC) { - const { width, height } = shape; - const radius = Math.min(shape.radius, Math.min(width, height) / 2); - - if (radius <= 0) { - shape = new PIXI.Rectangle(shape.x, shape.y, width, height); - } else if (radius === Math.max(width, height) / 2) { - shape = new PIXI.Circle(shape.x + width / 2, shape.y + height / 2, radius); - } else if (radius !== shape.radius) { - shape = new PIXI.RoundedRectangle(shape.x, shape.y, width, height, radius); - } - - this.#simple = 2; - } else if (type === PIXI.SHAPES.ELIP) { - const { width, height } = shape; - - if (width === height) { - shape = new PIXI.Circle(shape.x, shape.y, width); - } - - this.#simple = 2; - } else if (type === PIXI.SHAPES.POLY) { - shape = new PIXI.Polygon(Array.from(shape.points)); - - if (transform) { - const points = shape.points; - const m = points.length; - const { a, b, c, d, tx, ty } = transform; - - for (let i = 0; i < m; i += 2) { - const x = points[i]; - const y = points[i + 1]; - - points[i] = a * x + c * y + tx; - points[i + 1] = b * x + d * y + ty; - } - - transform = null; - } - } else if (type === PIXI.SHAPES.RECT) { - if (transform) { - const x1 = shape.x; - const y1 = shape.y - const x2 = x1 + shape.width; - const y2 = y1 + shape.height; - - shape = new PIXI.Polygon(x1, y1, x2, y1, x2, y2, x1, y2); - - const points = shape.points; - const { a, b, c, d, tx, ty } = transform; - - for (let i = 0; i < 8; i += 2) { - const x = points[i]; - const y = points[i + 1]; - - points[i] = a * x + c * y + tx; - points[i + 1] = b * x + d * y + ty; - } - - transform = null; - } else { - this.#simple = 2; - } - } else { - this.#simple = 2; - } - } - - if (shape.type === PIXI.SHAPES.POLY) { - const points = shape.points; - - Shape.roundPolygon(points); - Shape.dedupePolygon(points); - Shape.cleanPolygon(points); - - let m = points.length; - let area = 0; - - for (let i = 0, x1 = points[m - 2], y1 = points[m - 1]; i < m; i += 2) { - const x2 = points[i]; - const y2 = points[i + 1]; - - area += (x2 - x1) * (y2 + y1); - - x1 = x2; - y1 = y2; - } - - this.#area = Math.abs(area) / 2; - - if (area === 0) { - points.length = 0; - } else if (area > 0) { - const n = m / 2; - - for (let i = n + n % 2; i < m; i += 2) { - const j1 = m - i - 2; - const j2 = m - i - 1; - const j3 = i; - const j4 = i + 1; - - [points[j1], points[j3]] = [points[j3], points[j1]]; - [points[j2], points[j4]] = [points[j4], points[j2]]; - } - } - } - - if (shape === originalShape) { - const type = shape.type; - - if (type === PIXI.SHAPES.RECT) { - shape = new PIXI.Rectangle(shape.x, shape.y, shape.width, shape.height); - } else if (type === PIXI.SHAPES.RREC) { - shape = new PIXI.RoundedRectangle(shape.x, shape.y, shape.width, shape.height, shape.radius); - } else if (type === PIXI.SHAPES.CIRC) { - shape = new PIXI.Circle(shape.x, shape.y, shape.radius); - } else { // PIXI.SHAPES.ELIP - shape = new PIXI.Ellipse(shape.x, shape.y, shape.width, shape.height); - } - } - - this.shape = shape; - this.transform = transform ? transform.clone() : null; - } - - /** - * Convert to Clipper path. - * @param {number} [resolution=Shape.RESOLUTION] - The resolution. - * @returns {{X: number, Y: number}[]} - */ - toClipper(resolution = Shape.RESOLUTION) { - return Shape.createClipperPathFromPolygon(this.contour, resolution); - } - - /** - * The bounds. - * @type {PIXI.Rectangle} - * @readonly - */ - get bounds() { - let bounds = this.#bounds; - - if (!bounds) { - bounds = this.#bounds = this.#computeBounds(); - } - - return bounds; - } - - /** - * The contour. - * @type {number[]} - * @readonly - */ - get contour() { - let contour = this.#contour; - - if (!contour) { - contour = this.#contour = this.#generateContour(); - } - - return contour; - } - - /** - * The area. - * @type {number} - * @readonly - */ - get area() { - this.contour; - - return this.#area; - } - - /** - * Is weakly simple? - * @type {boolean} - * @readonly - */ - get weaklySimple() { - if (this.#simple === undefined) { - this.#simple = Shape.#isSimplePolygon(this.contour); - } - - return this.#simple >= 1; - } - - /** - * Is strictly simple? - * @type {boolean} - * @readonly - */ - get strictlySimple() { - if (this.#simple === undefined) { - this.#simple = Shape.#isSimplePolygon(this.contour); - } - - return this.#simple === 2; - } - - /** - * Test whether the point is contained this shape. - * @param {{x: number, y: number}} point - The point. - * @returns {boolean} True if and only if the point is contained. - */ - containsPoint(point) { - const shape = this.shape; - let transform; - let { x, y } = point; - - if (shape.type === PIXI.SHAPES.POLY) { - if (!this.bounds.contains(x, y)) { - return false; - } - } else if (transform = this.transform) { - const { a, b, c, d, tx, ty } = transform; - const id = a * d - b * c; - const x2 = x - tx; - const y2 = y - ty; - - x = (d * x2 - c * y2) / id; - y = (a * y2 - b * x2) / id; - } - - return shape.contains(x, y); - } - - /** - * Test whether the circle is contained this shape. - * @param {{x: number, y: number}} point - The center point of the circle. - * @param {number} [radius=0] - The radius of the circle. - * @returns {boolean} True if and only if the circle is contained. - */ - containsCircle(point, radius) { - if (!(radius > 0)) { - return this.containsPoint(point); - } - - const shape = this.shape; - const type = shape.type; - let { x, y } = point; - const radius2 = radius * radius; - - if (this.transform || type === PIXI.SHAPES.POLY || type === PIXI.SHAPES.ELIP || type === PIXI.SHAPES.RREC) { - const bounds = this.bounds; - const xmin = bounds.x; - const ymin = bounds.y; - const xmax = xmin + bounds.width; - const ymax = ymin + bounds.height; - - if (x < xmin + radius || x > xmax - radius || y < ymin + radius || y > ymax - radius) { - return false; - } - - if (type === PIXI.SHAPES.POLY) { - if (!shape.contains(x, y)) { - return false; - } - } else { - if (!this.containsPoint(point)) { - return false; - } - } - - const points = shape.points ?? this.contour; - const m = points.length; - - for (let i = 0, x1 = points[m - 2], y1 = points[m - 1]; i < m; i += 2) { - const x2 = points[i]; - const y2 = points[i + 1]; - - const dx = x - x1; - const dy = y - y1; - const nx = x2 - x1; - const ny = y2 - y1; - const t = Math.min(Math.max((dx * nx + dy * ny) / (nx * nx + ny * ny), 0), 1); - const x3 = t * nx - dx; - const y3 = t * ny - dy; - - if (x3 * x3 + y3 * y3 < radius2) { - return false; - } - - x1 = x2; - y1 = y2; - } - - return true; - } - - if (type === PIXI.SHAPES.RECT) { - const xmin = shape.x; - const ymin = shape.y; - const xmax = xmin + shape.width; - const ymax = ymin + shape.height; - - return x >= xmin + radius && x <= xmax - radius && y >= ymin + radius && y <= ymax - radius; - } else { // type === PIXI.SHAPES.CIRC - const dx = x - shape.x; - const dy = y - shape.y; - const r = shape.radius; - - if (r < radius) { - return false; - } - - return dx * dx + dy * dy <= radius2 + (r - 2 * radius) * r; - } - } - - /** - * Test whether the circle intersects this shape. - * @param {{x: number, y: number}} point - The center point of the circle. - * @param {number} [radius=0] - The radius of the circle. - * @returns {boolean} True if and only if the circle intersects. - */ - intersectsCircle(point, radius) { - if (!(radius > 0)) { - return this.containsPoint(point); - } - - const shape = this.shape; - const type = shape.type; - let { x, y } = point; - const radius2 = radius * radius; - - if (this.transform || type === PIXI.SHAPES.POLY || type === PIXI.SHAPES.ELIP || type === PIXI.SHAPES.RREC) { - const bounds = this.bounds; - const xmin = bounds.x; - const ymin = bounds.y; - const xmax = xmin + bounds.width; - const ymax = ymin + bounds.height; - - if (x <= xmin - radius || x >= xmax + radius || y <= ymin - radius || y >= ymax + radius) { - return false; - } - - if (type === PIXI.SHAPES.POLY) { - if (bounds.contains(x, y) && shape.contains(x, y)) { - return true; - } - } else { - if (this.containsPoint(point)) { - return true; - } - } - - const points = shape.points ?? this.contour; - const m = points.length; - - for (let i = 0, x1 = points[m - 2], y1 = points[m - 1]; i < m; i += 2) { - const x2 = points[i]; - const y2 = points[i + 1]; - - const dx = x - x1; - const dy = y - y1; - const nx = x2 - x1; - const ny = y2 - y1; - const t = Math.min(Math.max((dx * nx + dy * ny) / (nx * nx + ny * ny), 0), 1); - const x3 = t * nx - dx; - const y3 = t * ny - dy; - - if (x3 * x3 + y3 * y3 < radius2) { - return true; - } - - x1 = x2; - y1 = y2; - } - - return false; - } - - if (type === PIXI.SHAPES.RECT) { - const xmin = shape.x; - const ymin = shape.y; - const xmax = xmin + shape.width; - const ymax = ymin + shape.height; - - if (x <= xmin - radius || x >= xmax + radius || y <= ymin - radius || y >= ymax + radius) { - return false; - } - - let x1; - let y1; - - if (x < xmin) { - if (y < ymin) { - x1 = xmin; - y1 = ymin; - } else if (y > ymax) { - x1 = xmin; - y1 = ymax; - } else { - return true; - } - } else if (x > xmax) { - if (y < ymin) { - x1 = xmax; - y1 = ymin; - } else if (y > ymax) { - x1 = xmax; - y1 = ymax; - } else { - return true; - } - } else { - return true; - } - - const dx = x - x1; - const dy = y - y1; - - return dx * dx + dy * dy < radius2; - } else { // type === PIXI.SHAPES.CIRC - const dx = x - shape.x; - const dy = y - shape.y; - const r = shape.radius; - - return dx * dx + dy * dy < radius2 + (r + 2 * radius) * r; - } - } - - /** - * Test whether the line segment is contained this shape. - * @param {{x: number, y: number}} point1 - The first point of the line segment. - * @param {{x: number, y: number}} point2 - The second point of the line segment. - * @returns {boolean} True if and only if the line segment is contained. - */ - containsLineSegment(point1, point2) { - const shape = this.shape; - - if (!(this.containsPoint(point1) && this.containsPoint(point2))) { - return false; - } - - if (shape.type !== PIXI.SHAPES.POLY) { - return true; - } - - const ax = point1.x; - const ay = point1.y; - const bx = point2.x; - const by = point2.y; - const points = shape.points; - const m = points.length; - - for (let i = 0, x1 = points[m - 2], y1 = points[m - 1], d1 = (ay - y1) * (bx - x1) - (ax - x1) * (by - y1); i < m; i += 2) { - const x2 = points[i]; - const y2 = points[i + 1]; - const d2 = (ay - y2) * (bx - x2) - (ax - x2) * (by - y2); - - if ((d1 !== 0 || d2 !== 0) && d1 * d2 <= 0) { - const d3 = (y1 - ay) * (x2 - ax) - (x1 - ax) * (y2 - ay); - const d4 = (y1 - by) * (x2 - bx) - (x1 - bx) * (y2 - by); - - if (d3 * d4 <= 0) { - return false; - } - } - - x1 = x2; - y1 = y2; - d1 = d2; - } - - const mx = (ax + bx) / 2; - const my = (ay + by) / 2; - - return shape.contains(mx, my); - } - - /** - * Test whether the line segment intersects this shape. - * @param {{x: number, y: number}} point1 - The first point of the line segment. - * @param {{x: number, y: number}} point2 - The second point of the line segment. - * @returns {boolean} True if and only if the line segment intersects. - */ - intersectsLineSegment(point1, point2) { - const { left, right, top, bottom } = this.bounds; - const ax = point1.x; - const ay = point1.y; - const bx = point2.x; - const by = point2.y; - - const dx = 1 / (bx - ax); - const tx1 = (left - ax) * dx; - const tx2 = (right - ax) * dx; - - let tmin = Math.min(tx1, tx2); - let tmax = Math.max(tx1, tx2); - - const dy = 1 / (by - ay); - const ty1 = (top - ay) * dy; - const ty2 = (bottom - ay) * dy; - - tmin = Math.max(tmin, Math.min(ty1, ty2)); - tmax = Math.min(tmax, Math.max(ty1, ty2)); - - if (tmin >= 1 || tmax <= Math.max(0, tmin)) { - return false; - } - - if (this.containsPoint(point1) || this.containsPoint(point2)) { - return true; - } - - const points = this.contour; - const m = points.length; - - for (let i = 0, x1 = points[m - 2], y1 = points[m - 1], d1 = (ay - y1) * (bx - x1) - (ax - x1) * (by - y1); i < m; i += 2) { - const x2 = points[i]; - const y2 = points[i + 1]; - const d2 = (ay - y2) * (bx - x2) - (ax - x2) * (by - y2); - - if ((d1 !== 0 || d2 !== 0) && d1 * d2 <= 0) { - const d3 = (y1 - ay) * (x2 - ax) - (x1 - ax) * (y2 - ay); - const d4 = (y1 - by) * (x2 - bx) - (x1 - bx) * (y2 - by); - - if (d3 * d4 <= 0) { - return true; - } - } - - x1 = x2; - y1 = y2; - d1 = d2; - } - - return false; - } - - /** - * Compute the bounds. - * @returns {PIXI.Rectangle} The computed bounds. - */ - #computeBounds() { - const shape = this.shape; - const type = shape.type; - const transform = this.transform; - const bounds = new PIXI.Rectangle(); - - if (type === PIXI.SHAPES.POLY) { - const points = shape.points; - const m = points.length; - - if (m >= 6) { - let minX = points[0]; - let minY = points[1]; - let maxX = minX; - let maxY = minY; - - for (let i = 2; i < m; i += 2) { - const x = points[i]; - const y = points[i + 1]; - - if (minX > x) { - minX = x; - } else if (maxX < x) { - maxX = x; - } - - if (minY > y) { - minY = y; - } else if (maxY < y) { - maxY = y; - } - } - - bounds.x = minX; - bounds.y = minY; - bounds.width = maxX - minX; - bounds.height = maxY - minY; - } - } else { - if (!transform) { - if (type === PIXI.SHAPES.RECT) { - bounds.copyFrom(shape); - } else if (type === PIXI.SHAPES.RREC) { - bounds.x = shape.x; - bounds.y = shape.y; - bounds.width = shape.width; - bounds.height = shape.height; - } else if (type === PIXI.SHAPES.CIRC) { - const radius = shape.radius; - - bounds.x = shape.x - radius; - bounds.y = shape.y - radius; - bounds.width = radius * 2; - bounds.height = radius * 2; - } else { // type === PIXI.SHAPES.ELIP - const { width, height } = shape; - - bounds.x = shape.x - width; - bounds.y = shape.y - height; - bounds.width = width * 2; - bounds.height = height * 2; - } - } else { - const { a, b, c, d, tx, ty } = transform; - - if (shape.type === PIXI.SHAPES.RREC) { - const radius = shape.radius; - - const s = Math.atan2(c, a); - const t = Math.atan2(d, b); - const w = Math.abs(a * Math.cos(s) + c * Math.sin(s)) * radius; - const h = Math.abs(b * Math.cos(t) + d * Math.sin(t)) * radius; - - const x1 = shape.x + radius; - const y1 = shape.y + radius; - const x2 = x1 + shape.width - radius * 2; - const y2 = y1 + shape.height - radius * 2; - - const ltx = a * x1 + c * y1; - const lty = b * x1 + d * y1; - const lbx = a * x1 + c * y2; - const lby = b * x1 + d * y2; - const rtx = a * x2 + c * y1; - const rty = b * x2 + d * y1; - const rbx = a * x2 + c * y2; - const rby = b * x2 + d * y2; - - const minX = Math.min(ltx, lbx, rtx, rbx) - w; - const minY = Math.min(lty, lby, rty, rby) - h; - const maxX = Math.max(ltx, lbx, rtx, rbx) + w; - const maxY = Math.max(lty, lby, rty, rby) + h; - - bounds.x = minX + tx; - bounds.y = minY + ty; - bounds.width = maxX - minX; - bounds.height = maxY - minY; - } else { // shape.type === PIXI.SHAPES.CIRC || type === PIXI.SHAPES.ELIP - const { x, y } = shape; - let rx, ry; - - if (type === PIXI.SHAPES.CIRC) { - rx = ry = shape.radius; - } else { - rx = shape.width; - ry = shape.height; - } - - const s = Math.atan2(c * ry, a * rx); - const t = Math.atan2(d * ry, b * rx); - const w = Math.abs(a * rx * Math.cos(s) + c * ry * Math.sin(s)); - const h = Math.abs(b * rx * Math.cos(t) + d * ry * Math.sin(t)); - - bounds.x = a * x + c * y + tx - w; - bounds.y = b * x + d * y + ty - h; - bounds.width = w * 2; - bounds.height = h * 2; - } - } - } - - const resolution = Shape.RESOLUTION; - - bounds.x = Math.floor(bounds.x * resolution) / resolution; - bounds.y = Math.floor(bounds.y * resolution) / resolution; - bounds.width = Math.ceil((bounds.x + bounds.width) * resolution) / resolution - bounds.x; - bounds.height = Math.ceil((bounds.y + bounds.height) * resolution) / resolution - bounds.y; - - return bounds; - } - - /** - * Generate the contour. - * @returns {number[]} The generated contour. - */ - #generateContour() { - const shape = this.shape; - const type = shape.type; - - if (type === PIXI.SHAPES.RECT) { - const resolution = Shape.RESOLUTION; - const x0 = Math.round(shape.x * resolution) / resolution; - const y0 = Math.round(shape.y * resolution) / resolution; - const w = Math.round((shape.x + shape.width) * resolution) / resolution - x0; - const h = Math.round((shape.y + shape.height) * resolution) / resolution - y0; - const x1 = x0 + w; - const y1 = y0 + h; - - let points; - - if (w > 0 && h > 0) { - points = new Array(8); - points[0] = points[6] = x0; - points[1] = points[3] = y0; - points[2] = points[4] = x1; - points[5] = points[7] = y1; - } else { - points = []; - } - - this.#area = w * h; - - return points; - } - - if (type === PIXI.SHAPES.POLY) { - return shape.points; - } - - const transform = this.transform; - - let x, y; - let dx, dy; - let rx, ry; - - if (shape.type === PIXI.SHAPES.RREC) { - const w = shape.width / 2; - const h = shape.height / 2; - - x = shape.x + w; - y = shape.y + h; - rx = ry = shape.radius; - dx = w - rx; - dy = h - ry; - } else { - x = shape.x; - y = shape.y; - - if (shape.type === PIXI.SHAPES.CIRC) { - rx = ry = shape.radius; - } else { - rx = shape.width; - ry = shape.height; - } - - dx = 0; - dy = 0; - } - - let sx = rx; - let sy = ry; - - if (transform) { - const { a, b, c, d } = transform; - - sx *= Math.sqrt(a * a + c * c); - sy *= Math.sqrt(b * b + d * d); - } - - if (!(sx >= 0 && sy >= 0 && dx >= 0 && dy >= 0)) { - this.#area = 0; - - return []; - } - - const n = Math.ceil(Math.sqrt((sx + sy) / 2)); - let m = n * 8 + (dx ? 4 : 0) + (dy ? 4 : 0); - const points = new Array(m); - - if (m === 0) { - this.#area = 0; - - return points; - } - - if (n === 0) { - if (dx > 0 && dy > 0) { - points.length = 8; - points[0] = points[6] = x + dx; - points[1] = points[3] = y + dy; - points[2] = points[4] = x - dx; - points[5] = points[7] = y - dy; - - this.#area = dx * dy * 4; - } else { - points.length = 0; - - this.#area = 0; - } - - return points; - } - - let j1 = 0; - let j2 = n * 4 + (dx ? 2 : 0) + 2; - let j3 = j2; - let j4 = m; - - { - const x0 = dx + rx; - const y0 = dy; - const x1 = x + x0; - const x2 = x - x0; - const y1 = y + y0; - - points[j1++] = x1; - points[j1++] = y1; - points[--j2] = y1; - points[--j2] = x2; - - if (dy) { - const y2 = y - y0; - - points[j3++] = x2; - points[j3++] = y2; - points[--j4] = y2; - points[--j4] = x1; - } - } - - for (let i = 1; i < n; i++) { - const a = Math.PI / 2 * (i / n); - const x0 = dx + Math.cos(a) * rx; - const y0 = dy + Math.sin(a) * ry; - const x1 = x + x0; - const x2 = x - x0; - const y1 = y + y0; - const y2 = y - y0; - - points[j1++] = x1; - points[j1++] = y1; - points[--j2] = y1; - points[--j2] = x2; - points[j3++] = x2; - points[j3++] = y2; - points[--j4] = y2; - points[--j4] = x1; - } - - { - const x0 = dx; - const y0 = dy + ry; - const x1 = x + x0; - const x2 = x - x0; - const y1 = y + y0; - const y2 = y - y0; - - points[j1++] = x1; - points[j1++] = y1; - points[--j4] = y2; - points[--j4] = x1; - - if (dx) { - points[j1++] = x2; - points[j1++] = y1; - points[--j4] = y2; - points[--j4] = x2; - } - } - - if (transform) { - const { a, b, c, d, tx, ty } = transform; - - for (let i = 0; i < m; i += 2) { - const x = points[i]; - const y = points[i + 1]; - - points[i] = a * x + c * y + tx; - points[i + 1] = b * x + d * y + ty; - } - } - - Shape.roundPolygon(points); - Shape.dedupePolygon(points); - Shape.cleanPolygon(points); - - m = points.length; - - if (m === 0) { - return points; - } - - let area = 0; - - for (let i = 0, x1 = points[m - 2], y1 = points[m - 1]; i < m; i += 2) { - const x2 = points[i]; - const y2 = points[i + 1]; - - area += (x2 - x1) * (y2 + y1); - - x1 = x2; - y1 = y2; - } - - this.#area = Math.abs(area) / 2; - - if (area === 0) { - points.length = 0; - } else if (area > 0) { - const n = m / 2; - - for (let i = n + n % 2; i < m; i += 2) { - const j1 = m - i - 2; - const j2 = m - i - 1; - const j3 = i; - const j4 = i + 1; - - [points[j1], points[j3]] = [points[j3], points[j1]]; - [points[j2], points[j4]] = [points[j4], points[j2]]; - } - } - - return points; - } - - /** - * Create polygon from Clipper path. - * @param {{X: number, Y: number}[]} path - The Clipper path. - * @param {number} [resolution=Shape.RESOLUTION] - The resolution. - * @returns {Shape} - */ - static createPolygonFromClipper(path, resolution = Shape.RESOLUTION) { - const n = path.length; - const points = new Array(n << 1); - - resolution = 1 / resolution; - - for (let i = 0; i < n; i++) { - const point = path[i]; - - points[(i << 1)] = point.X * resolution; - points[(i << 1) + 1] = point.Y * resolution; - } - - return new PIXI.Polygon(points); - } - - /** - * Create polygon from Clipper path. - * @param {{X: number, Y: number}[]} path - The Clipper path. - * @param {number} [resolution=Shape.RESOLUTION] - The resolution. - * @returns {Shape} - */ - static createClipperPathFromPolygon(path, resolution = Shape.RESOLUTION) { - const n = path.length; - const points = new Array(n << 1); - - resolution = 1 / resolution; - - for (let i = 0; i < n; i++) { - const point = path[i]; - - points[(i << 1)] = point.X * resolution; - points[(i << 1) + 1] = point.Y * resolution; - } - - return new PIXI.Polygon(points); - } - /** - * Create Clipper path from polygon. - * @param {PIXI.Polygon|number[]} points - The polygon or points. - * @param {number} [resolution=Shape.RESOLUTION] - The resolution. - * @returns {{X: number, Y: number}[]} - */ - static createClipperPathFromPolygon(polygon, resolution = Shape.RESOLUTION) { - const points = polygon.points ?? polygon; - const m = points.length; - const path = new Array(m >> 1); - - for (let i = 0; i < m; i += 2) { - path[i >> 1] = { - X: Math.round(points[i] * resolution), - Y: Math.round(points[i + 1] * resolution) - }; - } - - return path; - } - /** - * Round the points of the polygon (in-place). - * @param {PIXI.Polygon|number[]} points - The polygon or points. - * @param {number} [resolution=Shape.RESOLUTION] - The resolution. - * @returns {PIXI.Polygon|number[]} The input polygon or points. - */ - static roundPolygon(points, resolution = Shape.RESOLUTION) { - const polygon = points; - - points = polygon.points ?? points; - - const m = points.length; - - for (let i = 0; i < m; i++) { - points[i] = Math.round(points[i] * resolution) / resolution; - } - - return polygon; - } - - /** - * Dedupe the points of the polygon (in-place). - * @param {PIXI.Polygon|number[]} points - The polygon or points. - * @returns {PIXI.Polygon|number[]} The input polygon or points. - */ - static dedupePolygon(points) { - const polygon = points; - - points = polygon.points ?? points; - - while (points.length !== 0 && points[0] === points[points.length - 2] && points[1] === points[points.length - 1]) { - points.length -= 2; - } - - let k = 0; - - for (let i = 0, k = 0; i + 2 < points.length; i += 2) { - const x = points[i]; - const y = points[i + 1]; - - if (x === points[i + 2] && y === points[i + 3]) { - k += 2; - } else if (k !== 0) { - points[i - k] = x; - points[i - k + 1] = y; - } - } - - points.length -= k; - - return polygon; - } - - /** - * Clean the points of the polygon (in-place). - * @param {PIXI.Polygon|number[]} points - The polygon or points. - * @param {number} [resolution=Shape.RESOLUTION] - The resolution. - * @returns {PIXI.Polygon|number[]} The input polygon or points. - */ - static cleanPolygon(points, resolution = Shape.RESOLUTION) { - const polygon = points; - - points = polygon.points ?? points; - - const m = points.length; - - if (m < 6) { - points.length = 0; - - return points; - } - - let path = new Array(m / 2); - - for (let j = 0; j < m; j += 2) { - const x = Math.round(points[j] * resolution); - const y = Math.round(points[j + 1] * resolution); - - path[j >> 1] = new ClipperLib.IntPoint(x, y); - } - - path = ClipperLib.Clipper.CleanPolygon(path); - - const n = path.length; - - points.length = n << 1; - - for (let i = 0; i < n; i++) { - const point = path[i]; - - points[(i << 1)] = point.X / resolution; - points[(i << 1) + 1] = point.Y / resolution; - } - - return polygon; - } - - /** - * Smooth the points of the polygon (in-place). - * @param {PIXI.Polygon|number[]} points - The polygon or points. - * @param {number} [factor=0.5] - The smoothing factor. - * @returns {PIXI.Polygon|number[]} The input polygon or points. - */ - static smoothPolygon(points, factor = 0.5) { - const polygon = points; - - points = polygon.points ?? points; - - if (points.length >= 6 && factor !== 0) { - const first = points.slice(0, 2); - const last = points.slice(-2); - const path = points.concat(points.slice(0, 4)); - - let previous = first; - let current = path.slice(2, 4); - let cp0 = getBezierControlPoints(factor, last, previous, current).next_cp0; - - points.length = 0; - points.push(first[0], first[1]); - - for (let i = 4; i < path.length; i += 2) { - const next = [path[i], path[i + 1]]; - const bp = getBezierControlPoints(factor, previous, current, next); - const cp1 = bp.cp1; - - PIXI.graphicsUtils.BezierUtils.curveTo(cp0.x, cp0.y, cp1.x, cp1.y, current[0], current[1], points); - - previous = current; - current = next; - cp0 = bp.next_cp0; - } - - points.length -= 2; - } - - return polygon; - } - - /** - * Is the polygon weakly simple? - * @param {PIXI.Polygon|number[]} points - The polygon or points. - * @returns {boolean} True if and only if the polygon is weakly simple. - */ - static isWeaklySimplePolygon(points) { - return this.#isSimplePolygon(points) >= 1; - } - - /** - * Is the polygon strictly simple? - * @param {PIXI.Polygon|number[]} points - The polygon or points. - * @returns {boolean} True if and only if the polygon is strictly simple. - */ - static isStrictlySimplePolygon(points) { - return this.#isSimplePolygon(points) === 2; - } - - /** - * Determine whether the polygon is not simple (`0`), weakly simple (`1`), or strictly simple (`2`). - * @param {PIXI.Polygon|number[]} points - The polygon or points. - * @returns {0|1|2} Not simple (`0`), weakly simple (`1`), or strictly simple (`2`). - */ - static #isSimplePolygon(points) { - points = points instanceof Array ? points : points.points; - - const m = points.length; - - for (let i = 2; i < m; i += 2) { - const x1 = points[i - 2]; - const y1 = points[i - 1]; - const x2 = points[i]; - const y2 = points[i + 1]; - - for (let j = i + 2; j < (i > 2 ? m : m - 2); j += 2) { - const x3 = points[j]; - const y3 = points[j + 1]; - const x4 = points[(j + 2) % m]; - const y4 = points[(j + 3) % m]; - - const d1 = (y1 - y3) * (x2 - x3) - (x1 - x3) * (y2 - y3); - const d2 = (y1 - y4) * (x2 - x4) - (x1 - x4) * (y2 - y4); - - if (d1 * d2 < 0) { - const d3 = (y3 - y1) * (x4 - x1) - (x3 - x1) * (y4 - y1); - const d4 = (y3 - y2) * (x4 - x2) - (x3 - x2) * (y4 - y2); - - if (d3 * d4 < 0) { - return 0; - } - } - } - } - - for (let i = 0; i < m; i += 2) { - const x1 = points[i]; - const y1 = points[i + 1]; - - for (let j = i + 2; j < m; j += 2) { - const x2 = points[j]; - const y2 = points[j + 1]; - - if (x1 === x2 && y1 === y2) { - return 1; - } - } - } - - for (let i = 0; i < m; i += 2) { - const x0 = points[i]; - const y0 = points[i + 1]; - - for (let j = 0; j < m; j += 2) { - if (i === j || i === (j + 2) % m) { - continue; - } - - const x1 = points[j]; - const y1 = points[j + 1]; - const x2 = points[(j + 2) % m]; - const y2 = points[(j + 3) % m]; - - const d1 = (y0 - y1) * (x2 - x1) - (x0 - x1) * (y2 - y1); - - if (d1 === 0) { - const d2 = (x0 - x1) * (x2 - x1) + (y0 - y1) * (y2 - y1); - - if (d2 >= 0) { - const d3 = (x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1); - - if (d2 <= d3) { - return 1; - } - } - } - } - } - - return 2; - } -} - -function getBezierControlPoints(factor, previous, point, next) { - const vector = { x: next[0] - previous[0], y: next[1] - previous[1] }; - const preDist = Math.hypot(previous[0] - point[0], previous[1] - point[1]); - const postDist = Math.hypot(next[0] - point[0], next[1] - point[1]); - const dist = preDist + postDist; - const cp0d = dist === 0 ? 0 : factor * (preDist / dist); - const cp1d = dist === 0 ? 0 : factor * (postDist / dist); - - return { - cp1: { - x: point[0] - (vector.x * cp0d), - y: point[1] - (vector.y * cp0d) - }, - next_cp0: { - x: point[0] + (vector.x * cp1d), - y: point[1] + (vector.y * cp1d) - } - }; -} diff --git a/scripts/volume.mjs b/scripts/volume.mjs deleted file mode 100644 index 3449f73..0000000 --- a/scripts/volume.mjs +++ /dev/null @@ -1,428 +0,0 @@ -import { VolumeData } from "./data/models.mjs"; -import * as raycasting from "./raycasting/_index.mjs"; -import { Shape } from "./utils/shape.js"; - -export class Volume { - /** - * The ID. - * @type {string} - * @readonly - */ - id; - - /** - * The data. - * @type {VolumeData} - * @readonly - */ - data = new VolumeData({ hidden: true }); - - /** @type {raycasting.Boundary[]|null} */ - #boundaries = null; - - /** @type {{[sense: string]: number}} */ - #limits = {}; - - /** - * @param {string} id - */ - constructor(id) { - this.id = id; - } - - /** - * @param {VolumeData} data - * @returns {boolean} - */ - update(data) { - const changes = this.data.updateSource(data); - - if (foundry.utils.isEmpty(changes)) { - return false; - } - - const isActive = !this.data.hidden; - const isOrWasActive = isActive || "hidden" in changes; - let changedLimits; - - if ("light" in changes || "sight" in changes || "sound" in changes) { - changedLimits = this.#updateLimits(); - } else { - changedLimits = {}; - } - - if (isActive && !foundry.utils.isEmpty(this.#limits)) { - if (!this.#boundaries || "boundaries" in changes) { - this.#updateBoundaries(); - } - } else if ("boundaries" in changes) { - this.#boundaries = null; - } - - if (!isOrWasActive) { - return true; - } - - const commonChanges = ["hidden", "boundaries", "priority", "mode"].some((key) => key in changes); - const limits = foundry.utils.expandObject(this.#limits); - - changedLimits = foundry.utils.expandObject(changedLimits); - - canvas.perception.update({ - initializeLighting: commonChanges && "light" in limits || "light" in changedLimits, - initializeVision: commonChanges && "sight" in limits || "sight" in changedLimits, - initializeSounds: commonChanges && "sound" in limits || "sound" in changedLimits - }); - - return true; - } - - destroy() { - if (this.hidden) { - return; - } - - const limits = foundry.utils.expandObject(this.#limits); - - canvas.perception.update({ - initializeLighting: "light" in limits, - initializeVision: "sight" in limits, - initializeSounds: "sound" in limits - }); - } - - /** - * Returns a ray caster volume for the type and subtype. - * @param {string} sense - The sense. - * @returns {Volume|undefined} The ray caster volume. - * @internal - */ - _createRayVolume(sense) { - if (this.data.hidden) { - return; - } - - const limit = this.#limits[sense]; - - if (limit === undefined) { - return; - } - - const energyCost = 1 / limit; - - return new raycasting.Volume(this.#boundaries.map((o) => o.clone()), this.data.priority, this.data.mode, energyCost); - } - - #updateBoundaries() { - this.#boundaries = []; - - for (const boundaryData of this.data.boundaries) { - if (boundaryData.mask === 0) { - continue; - } - - let boundary; - - switch (boundaryData.type) { - case "cylinder": - boundary = Volume.#createCylinder(boundaryData); - break; - } - - if (!boundary) { - continue; - } - - this.#boundaries.push(boundary); - } - } - - /** - * @returns {{[sense: string]: number}} - */ - #updateLimits() { - const oldLimits = this.#limits; - - this.#limits = {}; - - this.#updateLimit("light", this.data.light); - - for (const [mode, limit] of Object.entries(this.data.sight)) { - this.#updateLimit(`sight.${mode}`, limit) - } - - this.#updateLimit("sound", this.data.sound); - - const changedLimits = {}; - - for (const [sense, oldLimit] of Object.entries(oldLimits)) { - const limit = this.#limits[sense]; - - if (oldLimit !== limit) { - changedLimits[sense] = limit ?? null; - } - } - - for (const [sense, limit] of Object.entries(this.#limits)) { - if (oldLimits[sense] !== limit) { - changedLimits[sense] = limit; - } - } - - return changedLimits; - } - - /** - * @param {string} sense - * @param {{enabled: boolean, range: number|null}|null|undefined} limit - */ - #updateLimit(sense, limit) { - if (!limit?.enabled) { - return; - } - - const distancePixels = canvas.dimensions.distancePixels; - const range = Number.isFinite(limit.range) ? limit.range * distancePixels : Infinity; - let skip = false; - - switch (this.data.mode) { - case raycasting.Operation.ADD: - case raycasting.Operation.SUB: - case raycasting.Operation.MAX: - skip = range === Infinity; - break; - case raycasting.Operation.MIN: - skip = range === 0; - break; - } - - if (skip) { - return; - } - - this.#limits[sense] = range; - } - - /** - * @param {ObjectData} data - * @returns {raycasting.boundaries.Cylinder|undefined} - */ - static #createCylinder(data) { - const cylinderData = data.data; - const base = []; - - for (const figureData of cylinderData.base) { - if ((figureData.mask & 0x7FFFFFFF) === 0) { - continue; - } - - let figure; - - switch (figureData.shape.type) { - case "r": - figure = Volume.#createRectangle(figureData); - break; - case "e": - figure = Volume.#createEllipse(figureData); - break; - case "p": - figure = Volume.#createPolygon(figureData); - break; - } - - if (!figure) { - continue; - } - - base.push(figure); - } - - if (base.length === 0) { - return; - } - - const distancePixels = canvas.dimensions.distancePixels; - const bottom = (cylinderData.bottom ?? -Infinity) * distancePixels; - const top = (cylinderData.top ?? Infinity) * distancePixels; - - return new raycasting.boundaries.Cylinder({ base, bottom, top, mask: data.mask }); - } - - /** - * @param {FigureData} data - * @returns {raycasting.figures.Rectangle|raycasting.figures.Tile|undefined} - */ - static #createRectangle(data) { - let { x, y, rotation, shape: { width, height }, mask } = data; - - if (!(width > 0 && height > 0)) { - return; - } - - const centerX = x + width / 2; - const centerY = y + height / 2; - - rotation = Math.toRadians(rotation ?? 0); - - const { src, scaleX, scaleY } = data.texture; - - if (!src) { - return new raycasting.figures.Rectangle({ centerX, centerY, width, height, rotation, mask }); - } - - if (scaleX === 0 || scaleY === 0) { - return; - } - - width *= scaleX; - height *= scaleY; - - const texture = getTexture(src); - - if (!texture) { - return; - } - - const { pixels, aw, ah, minX, minY, maxX, maxY } = new TileMesh( - { occlusion: { mode: CONST.OCCLUSION_MODES.FADE } }, texture)._textureData; - - return new raycasting.figures.Tile({ - centerX, centerY, width, height, rotation, mask, - texture: { pixels, width: aw, height: ah, minX, minY, maxX, maxY, threshold: 0.75 * 255 } - }); - } - - /** - * @param {FigureData} data - * @returns {raycasting.figures.Circle|raycasting.figures.Ellipse|undefined} - */ - static #createEllipse(data) { - let { x, y, rotation, shape: { width, height }, mask } = data; - - if (!(width > 0 && height > 0)) { - return; - } - - const radiusX = width / 2; - const radiusY = height / 2; - const centerX = x + radiusX; - const centerY = y + radiusY; - - if (radiusX === radiusY) { - return new raycasting.figures.Circle({ centerX, centerY, radius: radiusX, mask }); - } - - rotation = Math.toRadians(rotation ?? 0); - - return new raycasting.figures.Ellipse({ centerX, centerY, radiusX, radiusY, rotation, mask }); - } - - /** - * @param {FigureData} data - * @returns {raycasting.figures.Polygon|undefined} - */ - static #createPolygon(data) { - const { x, y, rotation, shape: { width, height, points }, bezierFactor, mask } = data; - - if (points.length < 6) { - return; - } - - const polygon = new PIXI.Polygon(Array.from(points)); - - Shape.dedupePolygon(polygon); - Shape.smoothPolygon(polygon, bezierFactor ?? 0); - - const transform = new PIXI.Matrix() - .translate(-(width ?? 0) / 2, -(height ?? 0) / 2) - .rotate(Math.toRadians(rotation ?? 0)) - .translate(x + (width ?? 0) / 2, y + (height ?? 0) / 2); - const shape = Shape.from(polygon, transform); - - if (shape.contour.length < 6) { - return; - } - - return new raycasting.figures.Polygon({ points: shape.contour, mask }); - } -} - -export class VolumeCollection extends foundry.utils.Collection { - /** - * @type {VolumeCollection} - * @readonly - */ - static instance = new VolumeCollection(); - - /** - * Returns a ray caster for this type and subtype restricted to the specified bounds and range. - * @param {string} sense - The sense. - * @param {number} minR - The minimum range. - * @param {number} maxR - The maximum range. - * @param {number} [minX] - The minimum x-coordinate. - * @param {number} [minY] - The minimum y-coordinate. - * @param {number} [minZ] - The minimum z-coordinate. - * @param {number} [maxX] - The maximum x-coordinate. - * @param {number} [maxY] - The maximum y-coordinate. - * @param {number} [maxZ] - The maximum z-coordinate. - * @returns {raycasting.Caster} The new ray caster restricted to the bounds and range. - */ - createRayCaster(sense, minR, maxR, minX, minY, minZ, maxX, maxY, maxZ) { - const volumes = []; - - for (const value of this.values()) { - const volume = value._createRayVolume(sense); - - if (volume) { - volumes.push(volume); - } - } - - return new raycasting.Caster(volumes, minR, maxR, minX, minY, minZ, maxX, maxY, maxZ); - } - - /** - * Returns an optimized ray caster, which the ray from the origin to the target was cast with. - * @param {string} sense - * @param {number} minR - The minimum range of the ray. - * @param {number} originX - The x-coordinate of the origin of the ray. - * @param {number} originY - The y-coordinate of the origin of the ray. - * @param {number} originZ - The z-coordinate of the origin of the ray. - * @param {number} targetX - The x-coordinate of the target of the ray. - * @param {number} targetY - The y-coordinate of the target of the ray. - * @param {number} targetZ - The z-coordinate of the target of the ray. - * @returns {raycasting.Caster} The ray caster that was used to cast the ray. - */ - castRay(sense, minR, originX, originY, originZ, targetX, targetY, targetZ) { - let minX, minY, minZ, maxX, maxY, maxZ; - - if (originX <= targetX) { - minX = originX; - maxX = targetX; - } else { - minX = targetX; - maxX = originX; - } - - if (originY <= targetY) { - minY = originY; - maxY = targetY; - } else { - minY = targetY; - maxY = originY; - } - - if (originZ <= targetZ) { - minZ = originZ; - maxZ = targetZ; - } else { - minZ = targetZ; - maxZ = originZ; - } - - return this.createRayCaster(sense, minR, Infinity, minX, minY, minZ, maxX, maxY, maxZ) - .setOrigin(originX, originY, originZ) - .setTarget(targetX, targetY, targetZ) - .castRay(); - } -} diff --git a/style.css b/style.css new file mode 100644 index 0000000..0882dd7 --- /dev/null +++ b/style.css @@ -0,0 +1,3 @@ +input.limits--placeholder-font-size-12::placeholder { + font-size: var(--font-size-12); +} diff --git a/styles/config.css b/styles/config.css deleted file mode 100644 index 3bc7b85..0000000 --- a/styles/config.css +++ /dev/null @@ -1,65 +0,0 @@ -form .form-group .form-fields button.limits--button { - font-size: var(--font-size-12); - flex: 1; -} - -form.limits--form>fieldset { - margin: 0 0 6px 0; -} - -.detection-modes.limits--detection-modes { - margin: 0; -} - -.detection-modes.limits--detection-modes .detection-mode { - margin: 0 0 0.25rem; - align-items: center; -} - -.detection-modes.limits--detection-modes header.detection-mode .detection-mode-id { - font-weight: normal; - font-size: var(--font-size-12); - color: var(--color-text-dark-secondary); -} - -.detection-modes.limits--detection-modes header.detection-mode .detection-mode-range { - font-weight: normal; - font-size: var(--font-size-12); - color: var(--color-text-dark-secondary); -} - -.detection-modes.limits--detection-modes header.detection-mode .detection-mode-enabled { - font-weight: normal; - font-size: var(--font-size-12); - color: var(--color-text-dark-secondary); -} - -.detection-modes.limits--detection-modes header.detection-mode .detection-mode-range { - display: inline-block !important; -} - -.detection-modes.limits--detection-modes .detection-mode .detection-mode-range { - margin-right: 0; - display: flex; - flex-direction: row; - flex-wrap: nowrap; - align-items: center; -} - -.detection-modes.limits--detection-modes .detection-mode .limits--detection-units { - font-size: var(--font-size-12); - color: var(--color-text-dark-secondary); - flex: 0; -} - -form.limits--form input[type="number"].limits--range::placeholder { - -moz-osx-font-smoothing: grayscale; - -webkit-font-smoothing: antialiased; - font-family: "Font Awesome 6 Pro"; - font-weight: 900; - font-style: normal; - font-variant: normal; - line-height: 1; - text-rendering: auto; - font-size: var(--font-size-12); -} diff --git a/templates/config.hbs b/templates/config.hbs deleted file mode 100644 index 8602248..0000000 --- a/templates/config.hbs +++ /dev/null @@ -1,76 +0,0 @@ -
-
- {{localize "Sight"}} -
-
-
{{localize "TOKEN.DetectionMode"}}
-
{{localize "TOKEN.DetectionRange"}} ({{gridUnits}})
-
{{localize "TOKEN.DetectionEnabled"}}
-
- - - -
-
- - {{#each sight as |mode i|}} -
-
- -
-
- -
-
- -
-
- - - -
-
- {{/each}} -
-
- -
- {{localize "Light"}} -
-
- - - - -
-
-
- -
- {{localize "Sound"}} -
-
- - - - -
-
-
- - -
diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..fd302fd --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "include": [ + "scripts/**/*.mjs" + ], + "compilerOptions": { + "strict": true, + "allowJs": true, + "checkJs": false, + "declaration": true, + "emitDeclarationOnly": true, + "lib": [ + "ES2023" + ], + "module": "esnext", + "target": "esnext", + "outDir": "_types", + "noUnusedLocals": false, + "noUnusedParameters": false + } +} diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000..9455992 --- /dev/null +++ b/typedoc.json @@ -0,0 +1,9 @@ +{ + "name": "Limits (Foundry VTT Module)", + "entryPoints": [ + "scripts/_module.mjs" + ], + "out": "_docs", + "disableSources": true, + "excludeExternals": true +}