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(`\
-
`));
- 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 @@
-
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
+}