mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
Add ESLint, pre-commit hook & format all files (#319)
* Add ESLint config * Add ESLint packages * Add prettier config * Fix ESLint package version * Format all files * Format static assets * Format project root config * Add pre-commit code formatting Formats .css & .js files automatically. If any .ts or .mts files are staged, the entire project is type-checked. Packages: - lint-staged - husky - prettier
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
/build
|
|
||||||
56
.eslintrc.js
56
.eslintrc.js
@@ -1,56 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es2021": true,
|
|
||||||
"node": true,
|
|
||||||
"jest/globals": true
|
|
||||||
},
|
|
||||||
"extends": "eslint:recommended",
|
|
||||||
"overrides": [
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": "latest",
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"plugins": ["jest"],
|
|
||||||
"globals": {
|
|
||||||
"gl": true,
|
|
||||||
"GL": true,
|
|
||||||
"LS": true,
|
|
||||||
"Uint8Array": true,
|
|
||||||
"Uint32Array": true,
|
|
||||||
"Float32Array": true,
|
|
||||||
"LGraphCanvas": true,
|
|
||||||
"LGraph": true,
|
|
||||||
"LGraphNode": true,
|
|
||||||
"LiteGraph": true,
|
|
||||||
"LGraphTexture": true,
|
|
||||||
"Mesh": true,
|
|
||||||
"Shader": true,
|
|
||||||
"enableWebGLCanvas": true,
|
|
||||||
"vec2": true,
|
|
||||||
"vec3": true,
|
|
||||||
"vec4": true,
|
|
||||||
"DEG2RAD": true,
|
|
||||||
"isPowerOfTwo": true,
|
|
||||||
"cloneCanvas": true,
|
|
||||||
"createCanvas": true,
|
|
||||||
"hex2num": true,
|
|
||||||
"colorToString": true,
|
|
||||||
"showElement": true,
|
|
||||||
"quat": true,
|
|
||||||
"AudioSynth": true,
|
|
||||||
"SillyClient": true
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"no-console": "off",
|
|
||||||
"no-empty": "warn",
|
|
||||||
"no-redeclare": "warn",
|
|
||||||
"no-inner-declarations": "warn",
|
|
||||||
"no-constant-condition": "warn",
|
|
||||||
"no-unused-vars": "warn",
|
|
||||||
"no-mixed-spaces-and-tabs": "warn",
|
|
||||||
"no-unreachable": "warn",
|
|
||||||
"curly": ["warn", "all"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
5
.husky/pre-commit
Normal file
5
.husky/pre-commit
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
if [[ "$OS" == "Windows_NT" ]]; then
|
||||||
|
npx.cmd lint-staged
|
||||||
|
else
|
||||||
|
npx lint-staged
|
||||||
|
fi
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"semi": true,
|
"semi": false,
|
||||||
"tabWidth": 2
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"overrides": [{ "files": "*.ts", "options": { "requirePragma": true } }],
|
||||||
|
"arrowParens": "avoid"
|
||||||
}
|
}
|
||||||
|
|||||||
158
eslint.config.js
Normal file
158
eslint.config.js
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import globals from "globals"
|
||||||
|
import eslint from "@eslint/js"
|
||||||
|
import tseslint from "typescript-eslint"
|
||||||
|
import stylistic from "@stylistic/eslint-plugin"
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ files: ["**/*.{js,mjs,ts,mts}"] },
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
stylistic.configs.customize({
|
||||||
|
quotes: "double",
|
||||||
|
braceStyle: "1tbs",
|
||||||
|
commaDangle: "always-multiline",
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: { ...globals.browser },
|
||||||
|
parserOptions: {
|
||||||
|
projectService: {
|
||||||
|
allowDefaultProject: [
|
||||||
|
"eslint.config.js",
|
||||||
|
"lint-staged.config.js",
|
||||||
|
"vite.config.mts",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: ["./dist/**/*"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// TODO: Update when TypeScript has been cleaned
|
||||||
|
"prefer-spread": "off",
|
||||||
|
"no-empty": "off",
|
||||||
|
"no-prototype-builtins": "off",
|
||||||
|
"no-var": "warn",
|
||||||
|
"no-fallthrough": "off",
|
||||||
|
|
||||||
|
"no-empty-pattern": ["error", { allowObjectPatternsAsParameters: true }],
|
||||||
|
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"@typescript-eslint/no-this-alias": "off",
|
||||||
|
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-argument": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-call": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-return": "off",
|
||||||
|
|
||||||
|
"@typescript-eslint/no-base-to-string": "off",
|
||||||
|
"@typescript-eslint/restrict-plus-operands": "off",
|
||||||
|
"@typescript-eslint/no-implied-eval": "off",
|
||||||
|
"@typescript-eslint/unbound-method": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-enum-comparison": "off",
|
||||||
|
"@typescript-eslint/no-for-in-array": "off",
|
||||||
|
"@typescript-eslint/only-throw-error": "off",
|
||||||
|
"@typescript-eslint/no-duplicate-type-constituents": "off",
|
||||||
|
"@typescript-eslint/no-empty-object-type": "off",
|
||||||
|
|
||||||
|
// "@typescript-eslint/prefer-readonly-parameter-types": "error",
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
// "@typescript-eslint/no-unsafe-function-type": "off",
|
||||||
|
|
||||||
|
"@stylistic/max-len": [
|
||||||
|
"warn",
|
||||||
|
{ code: 100, comments: 130, ignoreStrings: true },
|
||||||
|
],
|
||||||
|
|
||||||
|
// "@stylistic/multiline-comment-style": ["warn", "starred-block"],
|
||||||
|
"@stylistic/curly-newline": [
|
||||||
|
"warn",
|
||||||
|
{ consistent: true, multiline: true },
|
||||||
|
],
|
||||||
|
"@stylistic/object-curly-newline": [
|
||||||
|
"warn",
|
||||||
|
{ consistent: true, multiline: true },
|
||||||
|
],
|
||||||
|
// "@stylistic/object-property-newline": ["warn", { allowAllPropertiesOnSameLine: true }],
|
||||||
|
// "@stylistic/object-property-newline": "warn",
|
||||||
|
"@stylistic/one-var-declaration-per-line": "warn",
|
||||||
|
|
||||||
|
"@stylistic/array-bracket-newline": ["warn", { multiline: true }],
|
||||||
|
"@stylistic/array-element-newline": [
|
||||||
|
"warn",
|
||||||
|
{ consistent: true, multiline: true },
|
||||||
|
],
|
||||||
|
|
||||||
|
"@stylistic/function-paren-newline": ["warn", "multiline-arguments"],
|
||||||
|
"@stylistic/newline-per-chained-call": "warn",
|
||||||
|
|
||||||
|
"@stylistic/array-bracket-spacing": "warn",
|
||||||
|
"@stylistic/arrow-parens": "warn",
|
||||||
|
"@stylistic/arrow-spacing": "warn",
|
||||||
|
"@stylistic/block-spacing": "warn",
|
||||||
|
"@stylistic/brace-style": "warn",
|
||||||
|
"@stylistic/comma-dangle": "warn",
|
||||||
|
"@stylistic/comma-spacing": "warn",
|
||||||
|
"@stylistic/comma-style": "warn",
|
||||||
|
"@stylistic/computed-property-spacing": "warn",
|
||||||
|
"@stylistic/dot-location": "warn",
|
||||||
|
"@stylistic/eol-last": "warn",
|
||||||
|
"@stylistic/indent": ["warn", 2, { VariableDeclarator: "first" }],
|
||||||
|
"@stylistic/indent-binary-ops": "warn",
|
||||||
|
"@stylistic/key-spacing": "warn",
|
||||||
|
"@stylistic/keyword-spacing": "warn",
|
||||||
|
"@stylistic/lines-between-class-members": "warn",
|
||||||
|
"@stylistic/max-statements-per-line": "warn",
|
||||||
|
"@stylistic/member-delimiter-style": "warn",
|
||||||
|
"@stylistic/multiline-ternary": "warn",
|
||||||
|
"@stylistic/new-parens": "warn",
|
||||||
|
"@stylistic/no-extra-parens": "warn",
|
||||||
|
"@stylistic/no-floating-decimal": "warn",
|
||||||
|
"@stylistic/no-mixed-operators": "warn",
|
||||||
|
"@stylistic/no-mixed-spaces-and-tabs": "warn",
|
||||||
|
"@stylistic/no-multi-spaces": "warn",
|
||||||
|
"@stylistic/no-multiple-empty-lines": "warn",
|
||||||
|
"@stylistic/no-tabs": "warn",
|
||||||
|
"@stylistic/no-trailing-spaces": "warn",
|
||||||
|
"@stylistic/no-whitespace-before-property": "warn",
|
||||||
|
"@stylistic/object-curly-spacing": "warn",
|
||||||
|
"@stylistic/operator-linebreak": [
|
||||||
|
"warn",
|
||||||
|
"after",
|
||||||
|
{ overrides: { "?": "before", ":": "before" } },
|
||||||
|
],
|
||||||
|
"@stylistic/padded-blocks": "warn",
|
||||||
|
"@stylistic/quote-props": "warn",
|
||||||
|
"@stylistic/quotes": "warn",
|
||||||
|
"@stylistic/rest-spread-spacing": "warn",
|
||||||
|
"@stylistic/semi": "warn",
|
||||||
|
"@stylistic/semi-spacing": "warn",
|
||||||
|
"@stylistic/semi-style": ["warn", "first"],
|
||||||
|
"@stylistic/space-before-blocks": "warn",
|
||||||
|
"@stylistic/space-before-function-paren": "warn",
|
||||||
|
"@stylistic/space-in-parens": "warn",
|
||||||
|
"@stylistic/space-infix-ops": "warn",
|
||||||
|
"@stylistic/space-unary-ops": "warn",
|
||||||
|
"@stylistic/spaced-comment": "warn",
|
||||||
|
"@stylistic/template-curly-spacing": "warn",
|
||||||
|
"@stylistic/template-tag-spacing": "warn",
|
||||||
|
"@stylistic/type-annotation-spacing": "warn",
|
||||||
|
"@stylistic/type-generic-spacing": "warn",
|
||||||
|
"@stylistic/type-named-tuple-spacing": "warn",
|
||||||
|
"@stylistic/wrap-iife": "warn",
|
||||||
|
"@stylistic/yield-star-spacing": "warn",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-unused-vars": "error",
|
||||||
|
},
|
||||||
|
files: ["test/**/*.ts"],
|
||||||
|
},
|
||||||
|
)
|
||||||
17
lint-staged.config.js
Normal file
17
lint-staged.config.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export default {
|
||||||
|
"*.css": stagedFiles => `prettier --write ${stagedFiles.join(" ")}`,
|
||||||
|
|
||||||
|
"*.js": stagedFiles => prettierAndEslint(stagedFiles),
|
||||||
|
|
||||||
|
"*.{ts,mts}": stagedFiles => [
|
||||||
|
...prettierAndEslint(stagedFiles),
|
||||||
|
`tsc --noEmit`,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
function prettierAndEslint(fileNames) {
|
||||||
|
return [
|
||||||
|
`prettier --write ${fileNames.join(" ")}`,
|
||||||
|
`eslint --fix ${fileNames.join(" ")}`,
|
||||||
|
]
|
||||||
|
}
|
||||||
2219
package-lock.json
generated
2219
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -41,10 +41,19 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/Comfy-Org/litegraph.js",
|
"homepage": "https://github.com/Comfy-Org/litegraph.js",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.14.0",
|
||||||
|
"@stylistic/eslint-plugin": "^2.10.1",
|
||||||
|
"@types/eslint__js": "^8.42.3",
|
||||||
"@types/node": "^22.1.0",
|
"@types/node": "^22.1.0",
|
||||||
|
"eslint": "^9.14.0",
|
||||||
|
"globals": "^15.12.0",
|
||||||
|
"husky": "^9.1.7",
|
||||||
"jsdom": "^25.0.1",
|
"jsdom": "^25.0.1",
|
||||||
|
"lint-staged": "^15.2.10",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.6.3",
|
"typescript": "^5.6.3",
|
||||||
|
"typescript-eslint": "^8.14.0",
|
||||||
"vite": "^5.3.4",
|
"vite": "^5.3.4",
|
||||||
"vite-plugin-dts": "^4.3.0",
|
"vite-plugin-dts": "^4.3.0",
|
||||||
"vitest": "^2.1.4"
|
"vitest": "^2.1.4"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -29,10 +29,12 @@ export class CanvasPointer {
|
|||||||
static get maxClickDrift() {
|
static get maxClickDrift() {
|
||||||
return this.#maxClickDrift
|
return this.#maxClickDrift
|
||||||
}
|
}
|
||||||
|
|
||||||
static set maxClickDrift(value) {
|
static set maxClickDrift(value) {
|
||||||
this.#maxClickDrift = value
|
this.#maxClickDrift = value
|
||||||
this.#maxClickDrift2 = value * value
|
this.#maxClickDrift2 = value * value
|
||||||
}
|
}
|
||||||
|
|
||||||
static #maxClickDrift = 6
|
static #maxClickDrift = 6
|
||||||
/** {@link maxClickDrift} squared. Used to calculate click drift without `sqrt`. */
|
/** {@link maxClickDrift} squared. Used to calculate click drift without `sqrt`. */
|
||||||
static #maxClickDrift2 = this.#maxClickDrift ** 2
|
static #maxClickDrift2 = this.#maxClickDrift ** 2
|
||||||
@@ -107,6 +109,7 @@ export class CanvasPointer {
|
|||||||
get finally() {
|
get finally() {
|
||||||
return this.#finally
|
return this.#finally
|
||||||
}
|
}
|
||||||
|
|
||||||
set finally(value) {
|
set finally(value) {
|
||||||
try {
|
try {
|
||||||
this.#finally?.()
|
this.#finally?.()
|
||||||
@@ -114,6 +117,7 @@ export class CanvasPointer {
|
|||||||
this.#finally = value
|
this.#finally = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#finally?: () => unknown
|
#finally?: () => unknown
|
||||||
|
|
||||||
constructor(element: Element) {
|
constructor(element: Element) {
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import type { IContextMenuOptions, IContextMenuValue } from "./interfaces"
|
|||||||
import { LiteGraph } from "./litegraph"
|
import { LiteGraph } from "./litegraph"
|
||||||
|
|
||||||
interface ContextMenuDivElement extends HTMLDivElement {
|
interface ContextMenuDivElement extends HTMLDivElement {
|
||||||
value?: IContextMenuValue | string
|
value?: IContextMenuValue | string
|
||||||
onclick_callback?: never
|
onclick_callback?: never
|
||||||
closing_timer?: number
|
closing_timer?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContextMenu {
|
export interface ContextMenu {
|
||||||
constructor: new (...args: ConstructorParameters<typeof ContextMenu>) => ContextMenu
|
constructor: new (...args: ConstructorParameters<typeof ContextMenu>) => ContextMenu
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,354 +24,379 @@ export interface ContextMenu {
|
|||||||
* - event: you can pass a MouseEvent, this way the ContextMenu appears in that position
|
* - event: you can pass a MouseEvent, this way the ContextMenu appears in that position
|
||||||
*/
|
*/
|
||||||
export class ContextMenu {
|
export class ContextMenu {
|
||||||
options?: IContextMenuOptions
|
options?: IContextMenuOptions
|
||||||
parentMenu?: ContextMenu
|
parentMenu?: ContextMenu
|
||||||
root: ContextMenuDivElement
|
root: ContextMenuDivElement
|
||||||
current_submenu?: ContextMenu
|
current_submenu?: ContextMenu
|
||||||
lock?: boolean
|
lock?: boolean
|
||||||
|
|
||||||
// TODO: Interface for values requires functionality change - currently accepts an array of strings, functions, objects, nulls, or undefined.
|
// TODO: Interface for values requires functionality change - currently accepts
|
||||||
constructor(values: (IContextMenuValue | string)[], options: IContextMenuOptions) {
|
// an array of strings, functions, objects, nulls, or undefined.
|
||||||
options ||= {}
|
constructor(values: (IContextMenuValue | string)[], options: IContextMenuOptions) {
|
||||||
this.options = options
|
options ||= {}
|
||||||
|
this.options = options
|
||||||
|
|
||||||
//to link a menu with its parent
|
// to link a menu with its parent
|
||||||
const parent = options.parentMenu
|
const parent = options.parentMenu
|
||||||
if (parent) {
|
if (parent) {
|
||||||
if (!(parent instanceof ContextMenu)) {
|
if (!(parent instanceof ContextMenu)) {
|
||||||
console.error("parentMenu must be of class ContextMenu, ignoring it")
|
console.error("parentMenu must be of class ContextMenu, ignoring it")
|
||||||
options.parentMenu = null
|
options.parentMenu = null
|
||||||
} else {
|
} else {
|
||||||
this.parentMenu = parent
|
this.parentMenu = parent
|
||||||
this.parentMenu.lock = true
|
this.parentMenu.lock = true
|
||||||
this.parentMenu.current_submenu = this
|
this.parentMenu.current_submenu = this
|
||||||
}
|
}
|
||||||
if (parent.options?.className === "dark") {
|
if (parent.options?.className === "dark") {
|
||||||
options.className = "dark"
|
options.className = "dark"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
//use strings because comparing classes between windows doesnt work
|
|
||||||
const eventClass = options.event
|
|
||||||
? options.event.constructor.name
|
|
||||||
: null
|
|
||||||
if (eventClass !== "MouseEvent" &&
|
|
||||||
eventClass !== "CustomEvent" &&
|
|
||||||
eventClass !== "PointerEvent") {
|
|
||||||
console.error(`Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it. (${eventClass})`)
|
|
||||||
options.event = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const root: ContextMenuDivElement = document.createElement("div")
|
|
||||||
let classes = "litegraph litecontextmenu litemenubar-panel"
|
|
||||||
if (options.className) classes += " " + options.className
|
|
||||||
root.className = classes
|
|
||||||
root.style.minWidth = "100"
|
|
||||||
root.style.minHeight = "100"
|
|
||||||
// TODO: Fix use of timer in place of events
|
|
||||||
root.style.pointerEvents = "none"
|
|
||||||
setTimeout(function () {
|
|
||||||
root.style.pointerEvents = "auto"
|
|
||||||
}, 100) //delay so the mouse up event is not caught by this element
|
|
||||||
|
|
||||||
//this prevents the default context browser menu to open in case this menu was created when pressing right button
|
|
||||||
LiteGraph.pointerListenerAdd(root, "up",
|
|
||||||
function (e: MouseEvent) {
|
|
||||||
//console.log("pointerevents: ContextMenu up root prevent");
|
|
||||||
e.preventDefault()
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
true
|
|
||||||
)
|
|
||||||
root.addEventListener(
|
|
||||||
"contextmenu",
|
|
||||||
function (e: MouseEvent) {
|
|
||||||
//right button
|
|
||||||
if (e.button != 2) return false
|
|
||||||
e.preventDefault()
|
|
||||||
return false
|
|
||||||
},
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
LiteGraph.pointerListenerAdd(root, "down",
|
|
||||||
(e: MouseEvent) => {
|
|
||||||
//console.log("pointerevents: ContextMenu down");
|
|
||||||
if (e.button == 2) {
|
|
||||||
this.close()
|
|
||||||
e.preventDefault()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
function on_mouse_wheel(e: WheelEvent) {
|
|
||||||
const pos = parseInt(root.style.top)
|
|
||||||
root.style.top =
|
|
||||||
(pos + e.deltaY * options.scroll_speed).toFixed() + "px"
|
|
||||||
e.preventDefault()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.scroll_speed) {
|
|
||||||
options.scroll_speed = 0.1
|
|
||||||
}
|
|
||||||
|
|
||||||
root.addEventListener("wheel", on_mouse_wheel, true)
|
|
||||||
|
|
||||||
this.root = root
|
|
||||||
|
|
||||||
//title
|
|
||||||
if (options.title) {
|
|
||||||
const element = document.createElement("div")
|
|
||||||
element.className = "litemenu-title"
|
|
||||||
element.innerHTML = options.title
|
|
||||||
root.appendChild(element)
|
|
||||||
}
|
|
||||||
|
|
||||||
//entries
|
|
||||||
for (let i = 0; i < values.length; i++) {
|
|
||||||
const value = values[i]
|
|
||||||
let name = Array.isArray(values) ? value : String(i)
|
|
||||||
|
|
||||||
if (typeof name !== "string") {
|
|
||||||
name = name != null
|
|
||||||
? name.content === undefined ? String(name) : name.content
|
|
||||||
: name as null | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addItem(name, value, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
LiteGraph.pointerListenerAdd(root, "enter", function () {
|
|
||||||
if (root.closing_timer) {
|
|
||||||
clearTimeout(root.closing_timer)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
//insert before checking position
|
|
||||||
const ownerDocument = (options.event?.target as Node).ownerDocument
|
|
||||||
const root_document = ownerDocument || document
|
|
||||||
|
|
||||||
if (root_document.fullscreenElement)
|
|
||||||
root_document.fullscreenElement.appendChild(root)
|
|
||||||
else
|
|
||||||
root_document.body.appendChild(root)
|
|
||||||
|
|
||||||
//compute best position
|
|
||||||
let left = options.left || 0
|
|
||||||
let top = options.top || 0
|
|
||||||
if (options.event) {
|
|
||||||
left = options.event.clientX - 10
|
|
||||||
top = options.event.clientY - 10
|
|
||||||
if (options.title) top -= 20
|
|
||||||
|
|
||||||
if (parent) {
|
|
||||||
const rect = parent.root.getBoundingClientRect()
|
|
||||||
left = rect.left + rect.width
|
|
||||||
}
|
|
||||||
|
|
||||||
const body_rect = document.body.getBoundingClientRect()
|
|
||||||
const root_rect = root.getBoundingClientRect()
|
|
||||||
if (body_rect.height == 0)
|
|
||||||
console.error("document.body height is 0. That is dangerous, set html,body { height: 100%; }")
|
|
||||||
|
|
||||||
if (body_rect.width && left > body_rect.width - root_rect.width - 10)
|
|
||||||
left = body_rect.width - root_rect.width - 10
|
|
||||||
if (body_rect.height && top > body_rect.height - root_rect.height - 10)
|
|
||||||
top = body_rect.height - root_rect.height - 10
|
|
||||||
}
|
|
||||||
|
|
||||||
root.style.left = left + "px"
|
|
||||||
root.style.top = top + "px"
|
|
||||||
|
|
||||||
if (options.scale)
|
|
||||||
root.style.transform = `scale(${options.scale})`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addItem(name: string, value: IContextMenuValue | string, options: IContextMenuOptions): HTMLElement {
|
// use strings because comparing classes between windows doesnt work
|
||||||
options ||= {}
|
const eventClass = options.event
|
||||||
|
? options.event.constructor.name
|
||||||
const element: ContextMenuDivElement = document.createElement("div")
|
: null
|
||||||
element.className = "litemenu-entry submenu"
|
if (
|
||||||
|
eventClass !== "MouseEvent" &&
|
||||||
let disabled = false
|
eventClass !== "CustomEvent" &&
|
||||||
|
eventClass !== "PointerEvent"
|
||||||
if (value === null) {
|
) {
|
||||||
element.classList.add("separator")
|
console.error(`Event passed to ContextMenu is not of type MouseEvent or CustomEvent. Ignoring it. (${eventClass})`)
|
||||||
} else {
|
options.event = null
|
||||||
if (typeof value === "string") {
|
|
||||||
element.innerHTML = name
|
|
||||||
} else {
|
|
||||||
element.innerHTML = value?.title ?? name
|
|
||||||
|
|
||||||
if (value.disabled) {
|
|
||||||
disabled = true
|
|
||||||
element.classList.add("disabled")
|
|
||||||
element.setAttribute("aria-disabled", "true")
|
|
||||||
}
|
|
||||||
if (value.submenu || value.has_submenu) {
|
|
||||||
element.classList.add("has_submenu")
|
|
||||||
element.setAttribute("aria-haspopup", "true")
|
|
||||||
element.setAttribute("aria-expanded", "false")
|
|
||||||
}
|
|
||||||
if (value.className)
|
|
||||||
element.className += " " + value.className
|
|
||||||
}
|
|
||||||
element.value = value
|
|
||||||
element.setAttribute("role", "menuitem")
|
|
||||||
|
|
||||||
if (typeof value === "function") {
|
|
||||||
element.dataset["value"] = name
|
|
||||||
element.onclick_callback = value
|
|
||||||
} else {
|
|
||||||
element.dataset["value"] = String(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.root.appendChild(element)
|
|
||||||
if (!disabled) element.addEventListener("click", inner_onclick)
|
|
||||||
if (!disabled && options.autoopen)
|
|
||||||
LiteGraph.pointerListenerAdd(element, "enter", inner_over)
|
|
||||||
|
|
||||||
const setAriaExpanded = () => {
|
|
||||||
const entries = this.root.querySelectorAll("div.litemenu-entry.has_submenu")
|
|
||||||
if (entries) {
|
|
||||||
for (let i = 0; i < entries.length; i++) {
|
|
||||||
entries[i].setAttribute("aria-expanded", "false")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
element.setAttribute("aria-expanded", "true")
|
|
||||||
}
|
|
||||||
|
|
||||||
function inner_over(this: ContextMenuDivElement, e: MouseEvent) {
|
|
||||||
const value = this.value
|
|
||||||
if (!value || !(value as IContextMenuValue).has_submenu) return
|
|
||||||
|
|
||||||
//if it is a submenu, autoopen like the item was clicked
|
|
||||||
inner_onclick.call(this, e)
|
|
||||||
setAriaExpanded()
|
|
||||||
}
|
|
||||||
|
|
||||||
//menu option clicked
|
|
||||||
const that = this
|
|
||||||
function inner_onclick(this: ContextMenuDivElement, e: MouseEvent) {
|
|
||||||
const value = this.value
|
|
||||||
let close_parent = true
|
|
||||||
|
|
||||||
that.current_submenu?.close(e)
|
|
||||||
if ((value as IContextMenuValue)?.has_submenu || (value as IContextMenuValue)?.submenu) setAriaExpanded()
|
|
||||||
|
|
||||||
//global callback
|
|
||||||
if (options.callback) {
|
|
||||||
const r = options.callback.call(
|
|
||||||
this,
|
|
||||||
value,
|
|
||||||
options,
|
|
||||||
e,
|
|
||||||
that,
|
|
||||||
options.node
|
|
||||||
)
|
|
||||||
if (r === true) close_parent = false
|
|
||||||
}
|
|
||||||
|
|
||||||
//special cases
|
|
||||||
if (typeof value === "object") {
|
|
||||||
if (value.callback &&
|
|
||||||
!options.ignore_item_callbacks &&
|
|
||||||
value.disabled !== true) {
|
|
||||||
//item callback
|
|
||||||
const r = value.callback.call(
|
|
||||||
this,
|
|
||||||
value,
|
|
||||||
options,
|
|
||||||
e,
|
|
||||||
that,
|
|
||||||
options.extra
|
|
||||||
)
|
|
||||||
if (r === true) close_parent = false
|
|
||||||
}
|
|
||||||
if (value.submenu) {
|
|
||||||
if (!value.submenu.options)
|
|
||||||
throw "ContextMenu submenu needs options"
|
|
||||||
|
|
||||||
new that.constructor(value.submenu.options, {
|
|
||||||
callback: value.submenu.callback,
|
|
||||||
event: e,
|
|
||||||
parentMenu: that,
|
|
||||||
ignore_item_callbacks: value.submenu.ignore_item_callbacks,
|
|
||||||
title: value.submenu.title,
|
|
||||||
extra: value.submenu.extra,
|
|
||||||
autoopen: options.autoopen
|
|
||||||
})
|
|
||||||
close_parent = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (close_parent && !that.lock)
|
|
||||||
that.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return element
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close(e?: MouseEvent, ignore_parent_menu?: boolean): void {
|
const root: ContextMenuDivElement = document.createElement("div")
|
||||||
this.root.parentNode?.removeChild(this.root)
|
let classes = "litegraph litecontextmenu litemenubar-panel"
|
||||||
if (this.parentMenu && !ignore_parent_menu) {
|
if (options.className) classes += " " + options.className
|
||||||
this.parentMenu.lock = false
|
root.className = classes
|
||||||
this.parentMenu.current_submenu = null
|
root.style.minWidth = "100"
|
||||||
if (e === undefined) {
|
root.style.minHeight = "100"
|
||||||
this.parentMenu.close()
|
// TODO: Fix use of timer in place of events
|
||||||
} else if (e &&
|
root.style.pointerEvents = "none"
|
||||||
!ContextMenu.isCursorOverElement(e, this.parentMenu.root)) {
|
setTimeout(function () {
|
||||||
ContextMenu.trigger(this.parentMenu.root, LiteGraph.pointerevents_method + "leave", e)
|
root.style.pointerEvents = "auto"
|
||||||
}
|
}, 100) // delay so the mouse up event is not caught by this element
|
||||||
}
|
|
||||||
this.current_submenu?.close(e, true)
|
|
||||||
|
|
||||||
if (this.root.closing_timer)
|
// this prevents the default context browser menu to open in case this menu was created when pressing right button
|
||||||
clearTimeout(this.root.closing_timer)
|
LiteGraph.pointerListenerAdd(
|
||||||
}
|
root,
|
||||||
|
"up",
|
||||||
//this code is used to trigger events easily (used in the context menu mouseleave
|
function (e: MouseEvent) {
|
||||||
static trigger(element: HTMLDivElement, event_name: string, params: MouseEvent, origin?: unknown): CustomEvent {
|
// console.log("pointerevents: ContextMenu up root prevent");
|
||||||
const evt = document.createEvent("CustomEvent")
|
e.preventDefault()
|
||||||
evt.initCustomEvent(event_name, true, true, params) //canBubble, cancelable, detail
|
return true
|
||||||
// @ts-expect-error
|
},
|
||||||
evt.srcElement = origin
|
true,
|
||||||
if (element.dispatchEvent) element.dispatchEvent(evt)
|
)
|
||||||
// @ts-expect-error
|
root.addEventListener(
|
||||||
else if (element.__events) element.__events.dispatchEvent(evt)
|
"contextmenu",
|
||||||
//else nothing seems binded here so nothing to do
|
function (e: MouseEvent) {
|
||||||
return evt
|
// right button
|
||||||
}
|
if (e.button != 2) return false
|
||||||
|
e.preventDefault()
|
||||||
//returns the top most menu
|
|
||||||
getTopMenu(): ContextMenu {
|
|
||||||
return this.options.parentMenu
|
|
||||||
? this.options.parentMenu.getTopMenu()
|
|
||||||
: this
|
|
||||||
}
|
|
||||||
|
|
||||||
getFirstEvent(): MouseEvent {
|
|
||||||
return this.options.parentMenu
|
|
||||||
? this.options.parentMenu.getFirstEvent()
|
|
||||||
: this.options.event
|
|
||||||
}
|
|
||||||
|
|
||||||
static isCursorOverElement(event: MouseEvent, element: HTMLDivElement): boolean {
|
|
||||||
const left = event.clientX
|
|
||||||
const top = event.clientY
|
|
||||||
const rect = element.getBoundingClientRect()
|
|
||||||
if (!rect) return false
|
|
||||||
|
|
||||||
if (top > rect.top &&
|
|
||||||
top < rect.top + rect.height &&
|
|
||||||
left > rect.left &&
|
|
||||||
left < rect.left + rect.width) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
|
LiteGraph.pointerListenerAdd(
|
||||||
|
root,
|
||||||
|
"down",
|
||||||
|
(e: MouseEvent) => {
|
||||||
|
// console.log("pointerevents: ContextMenu down");
|
||||||
|
if (e.button == 2) {
|
||||||
|
this.close()
|
||||||
|
e.preventDefault()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
|
function on_mouse_wheel(e: WheelEvent) {
|
||||||
|
const pos = parseInt(root.style.top)
|
||||||
|
root.style.top = (pos + e.deltaY * options.scroll_speed).toFixed() + "px"
|
||||||
|
e.preventDefault()
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!options.scroll_speed) {
|
||||||
|
options.scroll_speed = 0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
root.addEventListener("wheel", on_mouse_wheel, true)
|
||||||
|
|
||||||
|
this.root = root
|
||||||
|
|
||||||
|
// title
|
||||||
|
if (options.title) {
|
||||||
|
const element = document.createElement("div")
|
||||||
|
element.className = "litemenu-title"
|
||||||
|
element.innerHTML = options.title
|
||||||
|
root.appendChild(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
// entries
|
||||||
|
for (let i = 0; i < values.length; i++) {
|
||||||
|
const value = values[i]
|
||||||
|
let name = Array.isArray(values) ? value : String(i)
|
||||||
|
|
||||||
|
if (typeof name !== "string") {
|
||||||
|
name = name != null
|
||||||
|
? name.content === undefined ? String(name) : name.content
|
||||||
|
: name as null | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
this.addItem(name, value, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
LiteGraph.pointerListenerAdd(root, "enter", function () {
|
||||||
|
if (root.closing_timer) {
|
||||||
|
clearTimeout(root.closing_timer)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// insert before checking position
|
||||||
|
const ownerDocument = (options.event?.target as Node).ownerDocument
|
||||||
|
const root_document = ownerDocument || document
|
||||||
|
|
||||||
|
if (root_document.fullscreenElement)
|
||||||
|
root_document.fullscreenElement.appendChild(root)
|
||||||
|
else
|
||||||
|
root_document.body.appendChild(root)
|
||||||
|
|
||||||
|
// compute best position
|
||||||
|
let left = options.left || 0
|
||||||
|
let top = options.top || 0
|
||||||
|
if (options.event) {
|
||||||
|
left = options.event.clientX - 10
|
||||||
|
top = options.event.clientY - 10
|
||||||
|
if (options.title) top -= 20
|
||||||
|
|
||||||
|
if (parent) {
|
||||||
|
const rect = parent.root.getBoundingClientRect()
|
||||||
|
left = rect.left + rect.width
|
||||||
|
}
|
||||||
|
|
||||||
|
const body_rect = document.body.getBoundingClientRect()
|
||||||
|
const root_rect = root.getBoundingClientRect()
|
||||||
|
if (body_rect.height == 0)
|
||||||
|
console.error("document.body height is 0. That is dangerous, set html,body { height: 100%; }")
|
||||||
|
|
||||||
|
if (body_rect.width && left > body_rect.width - root_rect.width - 10)
|
||||||
|
left = body_rect.width - root_rect.width - 10
|
||||||
|
if (body_rect.height && top > body_rect.height - root_rect.height - 10)
|
||||||
|
top = body_rect.height - root_rect.height - 10
|
||||||
|
}
|
||||||
|
|
||||||
|
root.style.left = left + "px"
|
||||||
|
root.style.top = top + "px"
|
||||||
|
|
||||||
|
if (options.scale) root.style.transform = `scale(${options.scale})`
|
||||||
|
}
|
||||||
|
|
||||||
|
addItem(
|
||||||
|
name: string,
|
||||||
|
value: IContextMenuValue | string,
|
||||||
|
options: IContextMenuOptions,
|
||||||
|
): HTMLElement {
|
||||||
|
options ||= {}
|
||||||
|
|
||||||
|
const element: ContextMenuDivElement = document.createElement("div")
|
||||||
|
element.className = "litemenu-entry submenu"
|
||||||
|
|
||||||
|
let disabled = false
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
element.classList.add("separator")
|
||||||
|
} else {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
element.innerHTML = name
|
||||||
|
} else {
|
||||||
|
element.innerHTML = value?.title ?? name
|
||||||
|
|
||||||
|
if (value.disabled) {
|
||||||
|
disabled = true
|
||||||
|
element.classList.add("disabled")
|
||||||
|
element.setAttribute("aria-disabled", "true")
|
||||||
|
}
|
||||||
|
if (value.submenu || value.has_submenu) {
|
||||||
|
element.classList.add("has_submenu")
|
||||||
|
element.setAttribute("aria-haspopup", "true")
|
||||||
|
element.setAttribute("aria-expanded", "false")
|
||||||
|
}
|
||||||
|
if (value.className) element.className += " " + value.className
|
||||||
|
}
|
||||||
|
element.value = value
|
||||||
|
element.setAttribute("role", "menuitem")
|
||||||
|
|
||||||
|
if (typeof value === "function") {
|
||||||
|
element.dataset["value"] = name
|
||||||
|
element.onclick_callback = value
|
||||||
|
} else {
|
||||||
|
element.dataset["value"] = String(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.root.appendChild(element)
|
||||||
|
if (!disabled) element.addEventListener("click", inner_onclick)
|
||||||
|
if (!disabled && options.autoopen)
|
||||||
|
LiteGraph.pointerListenerAdd(element, "enter", inner_over)
|
||||||
|
|
||||||
|
const setAriaExpanded = () => {
|
||||||
|
const entries = this.root.querySelectorAll("div.litemenu-entry.has_submenu")
|
||||||
|
if (entries) {
|
||||||
|
for (let i = 0; i < entries.length; i++) {
|
||||||
|
entries[i].setAttribute("aria-expanded", "false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
element.setAttribute("aria-expanded", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
function inner_over(this: ContextMenuDivElement, e: MouseEvent) {
|
||||||
|
const value = this.value
|
||||||
|
if (!value || !(value as IContextMenuValue).has_submenu) return
|
||||||
|
|
||||||
|
// if it is a submenu, autoopen like the item was clicked
|
||||||
|
inner_onclick.call(this, e)
|
||||||
|
setAriaExpanded()
|
||||||
|
}
|
||||||
|
|
||||||
|
// menu option clicked
|
||||||
|
const that = this
|
||||||
|
function inner_onclick(this: ContextMenuDivElement, e: MouseEvent) {
|
||||||
|
const value = this.value
|
||||||
|
let close_parent = true
|
||||||
|
|
||||||
|
that.current_submenu?.close(e)
|
||||||
|
if (
|
||||||
|
(value as IContextMenuValue)?.has_submenu ||
|
||||||
|
(value as IContextMenuValue)?.submenu
|
||||||
|
) {
|
||||||
|
setAriaExpanded()
|
||||||
|
}
|
||||||
|
|
||||||
|
// global callback
|
||||||
|
if (options.callback) {
|
||||||
|
const r = options.callback.call(
|
||||||
|
this,
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
e,
|
||||||
|
that,
|
||||||
|
options.node,
|
||||||
|
)
|
||||||
|
if (r === true) close_parent = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// special cases
|
||||||
|
if (typeof value === "object") {
|
||||||
|
if (
|
||||||
|
value.callback &&
|
||||||
|
!options.ignore_item_callbacks &&
|
||||||
|
value.disabled !== true
|
||||||
|
) {
|
||||||
|
// item callback
|
||||||
|
const r = value.callback.call(
|
||||||
|
this,
|
||||||
|
value,
|
||||||
|
options,
|
||||||
|
e,
|
||||||
|
that,
|
||||||
|
options.extra,
|
||||||
|
)
|
||||||
|
if (r === true) close_parent = false
|
||||||
|
}
|
||||||
|
if (value.submenu) {
|
||||||
|
if (!value.submenu.options) throw "ContextMenu submenu needs options"
|
||||||
|
|
||||||
|
new that.constructor(value.submenu.options, {
|
||||||
|
callback: value.submenu.callback,
|
||||||
|
event: e,
|
||||||
|
parentMenu: that,
|
||||||
|
ignore_item_callbacks: value.submenu.ignore_item_callbacks,
|
||||||
|
title: value.submenu.title,
|
||||||
|
extra: value.submenu.extra,
|
||||||
|
autoopen: options.autoopen,
|
||||||
|
})
|
||||||
|
close_parent = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (close_parent && !that.lock) that.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
|
||||||
|
close(e?: MouseEvent, ignore_parent_menu?: boolean): void {
|
||||||
|
this.root.parentNode?.removeChild(this.root)
|
||||||
|
if (this.parentMenu && !ignore_parent_menu) {
|
||||||
|
this.parentMenu.lock = false
|
||||||
|
this.parentMenu.current_submenu = null
|
||||||
|
if (e === undefined) {
|
||||||
|
this.parentMenu.close()
|
||||||
|
} else if (e && !ContextMenu.isCursorOverElement(e, this.parentMenu.root)) {
|
||||||
|
ContextMenu.trigger(
|
||||||
|
this.parentMenu.root,
|
||||||
|
LiteGraph.pointerevents_method + "leave",
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.current_submenu?.close(e, true)
|
||||||
|
|
||||||
|
if (this.root.closing_timer) clearTimeout(this.root.closing_timer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// this code is used to trigger events easily (used in the context menu mouseleave
|
||||||
|
static trigger(
|
||||||
|
element: HTMLDivElement,
|
||||||
|
event_name: string,
|
||||||
|
params: MouseEvent,
|
||||||
|
origin?: unknown,
|
||||||
|
): CustomEvent {
|
||||||
|
const evt = document.createEvent("CustomEvent")
|
||||||
|
evt.initCustomEvent(event_name, true, true, params) // canBubble, cancelable, detail
|
||||||
|
// @ts-expect-error
|
||||||
|
evt.srcElement = origin
|
||||||
|
if (element.dispatchEvent) element.dispatchEvent(evt)
|
||||||
|
// @ts-expect-error
|
||||||
|
else if (element.__events) element.__events.dispatchEvent(evt)
|
||||||
|
// else nothing seems binded here so nothing to do
|
||||||
|
return evt
|
||||||
|
}
|
||||||
|
|
||||||
|
// returns the top most menu
|
||||||
|
getTopMenu(): ContextMenu {
|
||||||
|
return this.options.parentMenu
|
||||||
|
? this.options.parentMenu.getTopMenu()
|
||||||
|
: this
|
||||||
|
}
|
||||||
|
|
||||||
|
getFirstEvent(): MouseEvent {
|
||||||
|
return this.options.parentMenu
|
||||||
|
? this.options.parentMenu.getFirstEvent()
|
||||||
|
: this.options.event
|
||||||
|
}
|
||||||
|
|
||||||
|
static isCursorOverElement(
|
||||||
|
event: MouseEvent,
|
||||||
|
element: HTMLDivElement,
|
||||||
|
): boolean {
|
||||||
|
const left = event.clientX
|
||||||
|
const top = event.clientY
|
||||||
|
const rect = element.getBoundingClientRect()
|
||||||
|
if (!rect) return false
|
||||||
|
|
||||||
|
if (
|
||||||
|
top > rect.top &&
|
||||||
|
top < rect.top + rect.height &&
|
||||||
|
left > rect.left &&
|
||||||
|
left < rect.left + rect.width
|
||||||
|
) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,172 +2,190 @@ import type { Point, Rect } from "./interfaces"
|
|||||||
import { clamp, LGraphCanvas } from "./litegraph"
|
import { clamp, LGraphCanvas } from "./litegraph"
|
||||||
import { distance } from "./measure"
|
import { distance } from "./measure"
|
||||||
|
|
||||||
//used by some widgets to render a curve editor
|
// used by some widgets to render a curve editor
|
||||||
|
|
||||||
export class CurveEditor {
|
export class CurveEditor {
|
||||||
points: Point[]
|
points: Point[]
|
||||||
selected: number
|
selected: number
|
||||||
nearest: number
|
nearest: number
|
||||||
size: Rect
|
size: Rect
|
||||||
must_update: boolean
|
must_update: boolean
|
||||||
margin: number
|
margin: number
|
||||||
_nearest: number
|
_nearest: number
|
||||||
|
|
||||||
constructor(points: Point[]) {
|
constructor(points: Point[]) {
|
||||||
this.points = points
|
this.points = points
|
||||||
this.selected = -1
|
this.selected = -1
|
||||||
this.nearest = -1
|
this.nearest = -1
|
||||||
this.size = null //stores last size used
|
this.size = null // stores last size used
|
||||||
this.must_update = true
|
this.must_update = true
|
||||||
this.margin = 5
|
this.margin = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
static sampleCurve(f: number, points: Point[]): number {
|
||||||
|
if (!points) return
|
||||||
|
|
||||||
|
for (let i = 0; i < points.length - 1; ++i) {
|
||||||
|
const p = points[i]
|
||||||
|
const pn = points[i + 1]
|
||||||
|
if (pn[0] < f) continue
|
||||||
|
|
||||||
|
const r = pn[0] - p[0]
|
||||||
|
if (Math.abs(r) < 0.00001) return p[1]
|
||||||
|
|
||||||
|
const local_f = (f - p[0]) / r
|
||||||
|
return p[1] * (1.0 - local_f) + pn[1] * local_f
|
||||||
}
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
static sampleCurve(f: number, points: Point[]): number {
|
draw(
|
||||||
if (!points)
|
ctx: CanvasRenderingContext2D,
|
||||||
return
|
size: Rect,
|
||||||
for (let i = 0; i < points.length - 1; ++i) {
|
graphcanvas?: LGraphCanvas,
|
||||||
const p = points[i]
|
background_color?: string,
|
||||||
const pn = points[i + 1]
|
line_color?: string,
|
||||||
if (pn[0] < f)
|
inactive = false,
|
||||||
continue
|
): void {
|
||||||
const r = (pn[0] - p[0])
|
const points = this.points
|
||||||
if (Math.abs(r) < 0.00001)
|
if (!points) return
|
||||||
return p[1]
|
|
||||||
const local_f = (f - p[0]) / r
|
this.size = size
|
||||||
return p[1] * (1.0 - local_f) + pn[1] * local_f
|
const w = size[0] - this.margin * 2
|
||||||
}
|
const h = size[1] - this.margin * 2
|
||||||
return 0
|
|
||||||
|
line_color = line_color || "#666"
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(this.margin, this.margin)
|
||||||
|
|
||||||
|
if (background_color) {
|
||||||
|
ctx.fillStyle = "#111"
|
||||||
|
ctx.fillRect(0, 0, w, h)
|
||||||
|
ctx.fillStyle = "#222"
|
||||||
|
ctx.fillRect(w * 0.5, 0, 1, h)
|
||||||
|
ctx.strokeStyle = "#333"
|
||||||
|
ctx.strokeRect(0, 0, w, h)
|
||||||
}
|
}
|
||||||
|
ctx.strokeStyle = line_color
|
||||||
draw(ctx: CanvasRenderingContext2D, size: Rect, graphcanvas?: LGraphCanvas, background_color?: string, line_color?: string, inactive = false): void {
|
if (inactive) ctx.globalAlpha = 0.5
|
||||||
const points = this.points
|
ctx.beginPath()
|
||||||
if (!points)
|
for (let i = 0; i < points.length; ++i) {
|
||||||
return
|
const p = points[i]
|
||||||
this.size = size
|
ctx.lineTo(p[0] * w, (1.0 - p[1]) * h)
|
||||||
const w = size[0] - this.margin * 2
|
}
|
||||||
const h = size[1] - this.margin * 2
|
ctx.stroke()
|
||||||
|
ctx.globalAlpha = 1
|
||||||
line_color = line_color || "#666"
|
if (!inactive)
|
||||||
|
for (let i = 0; i < points.length; ++i) {
|
||||||
ctx.save()
|
const p = points[i]
|
||||||
ctx.translate(this.margin, this.margin)
|
ctx.fillStyle = this.selected == i
|
||||||
|
? "#FFF"
|
||||||
if (background_color) {
|
: this.nearest == i ? "#DDD" : "#AAA"
|
||||||
ctx.fillStyle = "#111"
|
|
||||||
ctx.fillRect(0, 0, w, h)
|
|
||||||
ctx.fillStyle = "#222"
|
|
||||||
ctx.fillRect(w * 0.5, 0, 1, h)
|
|
||||||
ctx.strokeStyle = "#333"
|
|
||||||
ctx.strokeRect(0, 0, w, h)
|
|
||||||
}
|
|
||||||
ctx.strokeStyle = line_color
|
|
||||||
if (inactive)
|
|
||||||
ctx.globalAlpha = 0.5
|
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
for (let i = 0; i < points.length; ++i) {
|
ctx.arc(p[0] * w, (1.0 - p[1]) * h, 2, 0, Math.PI * 2)
|
||||||
const p = points[i]
|
ctx.fill()
|
||||||
ctx.lineTo(p[0] * w, (1.0 - p[1]) * h)
|
}
|
||||||
}
|
ctx.restore()
|
||||||
ctx.stroke()
|
}
|
||||||
ctx.globalAlpha = 1
|
|
||||||
if (!inactive)
|
// localpos is mouse in curve editor space
|
||||||
for (let i = 0; i < points.length; ++i) {
|
onMouseDown(localpos: Point, graphcanvas: LGraphCanvas): boolean {
|
||||||
const p = points[i]
|
const points = this.points
|
||||||
ctx.fillStyle = this.selected == i ? "#FFF" : (this.nearest == i ? "#DDD" : "#AAA")
|
if (!points) return
|
||||||
ctx.beginPath()
|
if (localpos[1] < 0) return
|
||||||
ctx.arc(p[0] * w, (1.0 - p[1]) * h, 2, 0, Math.PI * 2)
|
|
||||||
ctx.fill()
|
// this.captureInput(true);
|
||||||
}
|
const w = this.size[0] - this.margin * 2
|
||||||
ctx.restore()
|
const h = this.size[1] - this.margin * 2
|
||||||
|
const x = localpos[0] - this.margin
|
||||||
|
const y = localpos[1] - this.margin
|
||||||
|
const pos: Point = [x, y]
|
||||||
|
const max_dist = 30 / graphcanvas.ds.scale
|
||||||
|
// search closer one
|
||||||
|
this.selected = this.getCloserPoint(pos, max_dist)
|
||||||
|
// create one
|
||||||
|
if (this.selected == -1) {
|
||||||
|
const point: Point = [x / w, 1 - y / h]
|
||||||
|
points.push(point)
|
||||||
|
points.sort(function (a, b) {
|
||||||
|
return a[0] - b[0]
|
||||||
|
})
|
||||||
|
this.selected = points.indexOf(point)
|
||||||
|
this.must_update = true
|
||||||
}
|
}
|
||||||
|
if (this.selected != -1) return true
|
||||||
|
}
|
||||||
|
|
||||||
//localpos is mouse in curve editor space
|
onMouseMove(localpos: Point, graphcanvas: LGraphCanvas): void {
|
||||||
onMouseDown(localpos: Point, graphcanvas: LGraphCanvas): boolean {
|
const points = this.points
|
||||||
const points = this.points
|
if (!points) return
|
||||||
if (!points)
|
|
||||||
return
|
|
||||||
if (localpos[1] < 0)
|
|
||||||
return
|
|
||||||
|
|
||||||
//this.captureInput(true);
|
const s = this.selected
|
||||||
const w = this.size[0] - this.margin * 2
|
if (s < 0) return
|
||||||
const h = this.size[1] - this.margin * 2
|
|
||||||
const x = localpos[0] - this.margin
|
|
||||||
const y = localpos[1] - this.margin
|
|
||||||
const pos: Point = [x, y]
|
|
||||||
const max_dist = 30 / graphcanvas.ds.scale
|
|
||||||
//search closer one
|
|
||||||
this.selected = this.getCloserPoint(pos, max_dist)
|
|
||||||
//create one
|
|
||||||
if (this.selected == -1) {
|
|
||||||
const point: Point = [x / w, 1 - y / h]
|
|
||||||
points.push(point)
|
|
||||||
points.sort(function (a, b) { return a[0] - b[0] })
|
|
||||||
this.selected = points.indexOf(point)
|
|
||||||
this.must_update = true
|
|
||||||
}
|
|
||||||
if (this.selected != -1)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
onMouseMove(localpos: Point, graphcanvas: LGraphCanvas): void {
|
const x = (localpos[0] - this.margin) / (this.size[0] - this.margin * 2)
|
||||||
const points = this.points
|
const y = (localpos[1] - this.margin) / (this.size[1] - this.margin * 2)
|
||||||
if (!points)
|
const curvepos: Point = [
|
||||||
return
|
localpos[0] - this.margin,
|
||||||
const s = this.selected
|
localpos[1] - this.margin,
|
||||||
if (s < 0)
|
]
|
||||||
return
|
const max_dist = 30 / graphcanvas.ds.scale
|
||||||
const x = (localpos[0] - this.margin) / (this.size[0] - this.margin * 2)
|
this._nearest = this.getCloserPoint(curvepos, max_dist)
|
||||||
const y = (localpos[1] - this.margin) / (this.size[1] - this.margin * 2)
|
const point = points[s]
|
||||||
const curvepos: Point = [(localpos[0] - this.margin), (localpos[1] - this.margin)]
|
if (point) {
|
||||||
const max_dist = 30 / graphcanvas.ds.scale
|
const is_edge_point = s == 0 || s == points.length - 1
|
||||||
this._nearest = this.getCloserPoint(curvepos, max_dist)
|
if (
|
||||||
const point = points[s]
|
!is_edge_point &&
|
||||||
if (point) {
|
(localpos[0] < -10 ||
|
||||||
const is_edge_point = s == 0 || s == points.length - 1
|
localpos[0] > this.size[0] + 10 ||
|
||||||
if (!is_edge_point && (localpos[0] < -10 || localpos[0] > this.size[0] + 10 || localpos[1] < -10 || localpos[1] > this.size[1] + 10)) {
|
localpos[1] < -10 ||
|
||||||
points.splice(s, 1)
|
localpos[1] > this.size[1] + 10)
|
||||||
this.selected = -1
|
) {
|
||||||
return
|
points.splice(s, 1)
|
||||||
}
|
|
||||||
if (!is_edge_point) //not edges
|
|
||||||
point[0] = clamp(x, 0, 1)
|
|
||||||
else
|
|
||||||
point[0] = s == 0 ? 0 : 1
|
|
||||||
point[1] = 1.0 - clamp(y, 0, 1)
|
|
||||||
points.sort(function (a, b) { return a[0] - b[0] })
|
|
||||||
this.selected = points.indexOf(point)
|
|
||||||
this.must_update = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Former params: localpos, graphcanvas
|
|
||||||
onMouseUp(): boolean {
|
|
||||||
this.selected = -1
|
this.selected = -1
|
||||||
return false
|
return
|
||||||
|
}
|
||||||
|
// not edges
|
||||||
|
if (!is_edge_point) point[0] = clamp(x, 0, 1)
|
||||||
|
else point[0] = s == 0 ? 0 : 1
|
||||||
|
point[1] = 1.0 - clamp(y, 0, 1)
|
||||||
|
points.sort(function (a, b) {
|
||||||
|
return a[0] - b[0]
|
||||||
|
})
|
||||||
|
this.selected = points.indexOf(point)
|
||||||
|
this.must_update = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getCloserPoint(pos: Point, max_dist: number): number {
|
// Former params: localpos, graphcanvas
|
||||||
const points = this.points
|
onMouseUp(): boolean {
|
||||||
if (!points)
|
this.selected = -1
|
||||||
return -1
|
return false
|
||||||
max_dist = max_dist || 30
|
}
|
||||||
const w = (this.size[0] - this.margin * 2)
|
|
||||||
const h = (this.size[1] - this.margin * 2)
|
getCloserPoint(pos: Point, max_dist: number): number {
|
||||||
const num = points.length
|
const points = this.points
|
||||||
const p2: Point = [0, 0]
|
if (!points) return -1
|
||||||
let min_dist = 1000000
|
|
||||||
let closest = -1
|
max_dist = max_dist || 30
|
||||||
for (let i = 0; i < num; ++i) {
|
const w = this.size[0] - this.margin * 2
|
||||||
const p = points[i]
|
const h = this.size[1] - this.margin * 2
|
||||||
p2[0] = p[0] * w
|
const num = points.length
|
||||||
p2[1] = (1.0 - p[1]) * h
|
const p2: Point = [0, 0]
|
||||||
const dist = distance(pos, p2)
|
let min_dist = 1000000
|
||||||
if (dist > min_dist || dist > max_dist)
|
let closest = -1
|
||||||
continue
|
|
||||||
closest = i
|
for (let i = 0; i < num; ++i) {
|
||||||
min_dist = dist
|
const p = points[i]
|
||||||
}
|
p2[0] = p[0] * w
|
||||||
return closest
|
p2[1] = (1.0 - p[1]) * h
|
||||||
|
const dist = distance(pos, p2)
|
||||||
|
if (dist > min_dist || dist > max_dist) continue
|
||||||
|
|
||||||
|
closest = i
|
||||||
|
min_dist = dist
|
||||||
}
|
}
|
||||||
|
return closest
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,252 +4,247 @@ import { LiteGraph } from "./litegraph"
|
|||||||
import { isInRect } from "./measure"
|
import { isInRect } from "./measure"
|
||||||
|
|
||||||
export interface DragAndScaleState {
|
export interface DragAndScaleState {
|
||||||
offset: Point
|
offset: Point
|
||||||
scale: number
|
scale: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DragAndScale {
|
export class DragAndScale {
|
||||||
/**
|
/**
|
||||||
* The state of this DragAndScale instance.
|
* The state of this DragAndScale instance.
|
||||||
*
|
*
|
||||||
* Implemented as a POCO that can be proxied without side-effects.
|
* Implemented as a POCO that can be proxied without side-effects.
|
||||||
*/
|
*/
|
||||||
state: DragAndScaleState
|
state: DragAndScaleState
|
||||||
|
|
||||||
/** Maximum scale (zoom in) */
|
/** Maximum scale (zoom in) */
|
||||||
max_scale: number
|
max_scale: number
|
||||||
/** Minimum scale (zoom out) */
|
/** Minimum scale (zoom out) */
|
||||||
min_scale: number
|
min_scale: number
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
last_mouse: Point
|
last_mouse: Point
|
||||||
element?: HTMLCanvasElement
|
element?: HTMLCanvasElement
|
||||||
visible_area: Rect32
|
visible_area: Rect32
|
||||||
_binded_mouse_callback
|
_binded_mouse_callback
|
||||||
dragging?: boolean
|
dragging?: boolean
|
||||||
viewport?: Rect
|
viewport?: Rect
|
||||||
|
|
||||||
onredraw?(das: DragAndScale): void
|
onredraw?(das: DragAndScale): void
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
onmouse?(e: unknown): boolean
|
onmouse?(e: unknown): boolean
|
||||||
|
|
||||||
get offset(): Point {
|
get offset(): Point {
|
||||||
return this.state.offset
|
return this.state.offset
|
||||||
|
}
|
||||||
|
|
||||||
|
set offset(value: Point) {
|
||||||
|
this.state.offset = value
|
||||||
|
}
|
||||||
|
|
||||||
|
get scale(): number {
|
||||||
|
return this.state.scale
|
||||||
|
}
|
||||||
|
|
||||||
|
set scale(value: number) {
|
||||||
|
this.state.scale = value
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(element?: HTMLCanvasElement, skip_events?: boolean) {
|
||||||
|
this.state = {
|
||||||
|
offset: new Float32Array([0, 0]),
|
||||||
|
scale: 1,
|
||||||
}
|
}
|
||||||
set offset(value: Point) {
|
this.max_scale = 10
|
||||||
this.state.offset = value
|
this.min_scale = 0.1
|
||||||
|
this.onredraw = null
|
||||||
|
this.enabled = true
|
||||||
|
this.last_mouse = [0, 0]
|
||||||
|
this.element = null
|
||||||
|
this.visible_area = new Float32Array(4)
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
this.element = element
|
||||||
|
if (!skip_events) {
|
||||||
|
this.bindEvents(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated Has not been kept up to date */
|
||||||
|
bindEvents(element: Node): void {
|
||||||
|
this.last_mouse = new Float32Array(2)
|
||||||
|
|
||||||
|
this._binded_mouse_callback = this.onMouse.bind(this)
|
||||||
|
|
||||||
|
LiteGraph.pointerListenerAdd(element, "down", this._binded_mouse_callback)
|
||||||
|
LiteGraph.pointerListenerAdd(element, "move", this._binded_mouse_callback)
|
||||||
|
LiteGraph.pointerListenerAdd(element, "up", this._binded_mouse_callback)
|
||||||
|
|
||||||
|
element.addEventListener("mousewheel", this._binded_mouse_callback, false)
|
||||||
|
element.addEventListener("wheel", this._binded_mouse_callback, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
computeVisibleArea(viewport: Rect): void {
|
||||||
|
if (!this.element) {
|
||||||
|
this.visible_area[0] = this.visible_area[1] = this.visible_area[2] = this.visible_area[3] = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let width = this.element.width
|
||||||
|
let height = this.element.height
|
||||||
|
let startx = -this.offset[0]
|
||||||
|
let starty = -this.offset[1]
|
||||||
|
if (viewport) {
|
||||||
|
startx += viewport[0] / this.scale
|
||||||
|
starty += viewport[1] / this.scale
|
||||||
|
width = viewport[2]
|
||||||
|
height = viewport[3]
|
||||||
|
}
|
||||||
|
const endx = startx + width / this.scale
|
||||||
|
const endy = starty + height / this.scale
|
||||||
|
this.visible_area[0] = startx
|
||||||
|
this.visible_area[1] = starty
|
||||||
|
this.visible_area[2] = endx - startx
|
||||||
|
this.visible_area[3] = endy - starty
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated Has not been kept up to date */
|
||||||
|
onMouse(e: CanvasMouseEvent) {
|
||||||
|
if (!this.enabled) {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
get scale(): number {
|
const canvas = this.element
|
||||||
return this.state.scale
|
const rect = canvas.getBoundingClientRect()
|
||||||
}
|
const x = e.clientX - rect.left
|
||||||
set scale(value: number) {
|
const y = e.clientY - rect.top
|
||||||
this.state.scale = value
|
// FIXME: "canvasx" / y are not referenced anywhere - wrong case
|
||||||
|
// @ts-expect-error Incorrect case
|
||||||
|
e.canvasx = x
|
||||||
|
// @ts-expect-error Incorrect case
|
||||||
|
e.canvasy = y
|
||||||
|
e.dragging = this.dragging
|
||||||
|
|
||||||
|
const is_inside = !this.viewport || isInRect(x, y, this.viewport)
|
||||||
|
|
||||||
|
let ignore = false
|
||||||
|
if (this.onmouse) {
|
||||||
|
ignore = this.onmouse(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(element?: HTMLCanvasElement, skip_events?: boolean) {
|
if (e.type == LiteGraph.pointerevents_method + "down" && is_inside) {
|
||||||
this.state = {
|
this.dragging = true
|
||||||
offset: new Float32Array([0, 0]),
|
LiteGraph.pointerListenerRemove(canvas, "move", this._binded_mouse_callback)
|
||||||
scale: 1
|
LiteGraph.pointerListenerAdd(document, "move", this._binded_mouse_callback)
|
||||||
|
LiteGraph.pointerListenerAdd(document, "up", this._binded_mouse_callback)
|
||||||
|
} else if (e.type == LiteGraph.pointerevents_method + "move") {
|
||||||
|
if (!ignore) {
|
||||||
|
const deltax = x - this.last_mouse[0]
|
||||||
|
const deltay = y - this.last_mouse[1]
|
||||||
|
if (this.dragging) {
|
||||||
|
this.mouseDrag(deltax, deltay)
|
||||||
}
|
}
|
||||||
this.max_scale = 10
|
}
|
||||||
this.min_scale = 0.1
|
} else if (e.type == LiteGraph.pointerevents_method + "up") {
|
||||||
this.onredraw = null
|
this.dragging = false
|
||||||
this.enabled = true
|
LiteGraph.pointerListenerRemove(document, "move", this._binded_mouse_callback)
|
||||||
this.last_mouse = [0, 0]
|
LiteGraph.pointerListenerRemove(document, "up", this._binded_mouse_callback)
|
||||||
this.element = null
|
LiteGraph.pointerListenerAdd(canvas, "move", this._binded_mouse_callback)
|
||||||
this.visible_area = new Float32Array(4)
|
} else if (
|
||||||
|
is_inside &&
|
||||||
|
(e.type == "mousewheel" || e.type == "wheel" || e.type == "DOMMouseScroll")
|
||||||
|
) {
|
||||||
|
// @ts-expect-error Deprecated
|
||||||
|
e.eventType = "mousewheel"
|
||||||
|
// @ts-expect-error Deprecated
|
||||||
|
if (e.type == "wheel") e.wheel = -e.deltaY
|
||||||
|
// @ts-expect-error Deprecated
|
||||||
|
else e.wheel = e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60
|
||||||
|
|
||||||
if (element) {
|
// from stack overflow
|
||||||
this.element = element
|
// @ts-expect-error Deprecated
|
||||||
if (!skip_events) {
|
e.delta = e.wheelDelta
|
||||||
this.bindEvents(element)
|
// @ts-expect-error Deprecated
|
||||||
}
|
? e.wheelDelta / 40
|
||||||
}
|
: e.deltaY
|
||||||
|
? -e.deltaY / 3
|
||||||
|
: 0
|
||||||
|
// @ts-expect-error Deprecated
|
||||||
|
this.changeDeltaScale(1.0 + e.delta * 0.05)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated Has not been kept up to date */
|
this.last_mouse[0] = x
|
||||||
bindEvents(element: Node): void {
|
this.last_mouse[1] = y
|
||||||
this.last_mouse = new Float32Array(2)
|
|
||||||
|
|
||||||
this._binded_mouse_callback = this.onMouse.bind(this)
|
if (is_inside) {
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LiteGraph.pointerListenerAdd(element, "down", this._binded_mouse_callback)
|
toCanvasContext(ctx: CanvasRenderingContext2D): void {
|
||||||
LiteGraph.pointerListenerAdd(element, "move", this._binded_mouse_callback)
|
ctx.scale(this.scale, this.scale)
|
||||||
LiteGraph.pointerListenerAdd(element, "up", this._binded_mouse_callback)
|
ctx.translate(this.offset[0], this.offset[1])
|
||||||
|
}
|
||||||
|
|
||||||
element.addEventListener(
|
convertOffsetToCanvas(pos: Point): Point {
|
||||||
"mousewheel",
|
return [
|
||||||
this._binded_mouse_callback,
|
(pos[0] + this.offset[0]) * this.scale,
|
||||||
false
|
(pos[1] + this.offset[1]) * this.scale,
|
||||||
)
|
]
|
||||||
element.addEventListener("wheel", this._binded_mouse_callback, false)
|
}
|
||||||
|
|
||||||
|
convertCanvasToOffset(pos: Point, out?: Point): Point {
|
||||||
|
out = out || [0, 0]
|
||||||
|
out[0] = pos[0] / this.scale - this.offset[0]
|
||||||
|
out[1] = pos[1] / this.scale - this.offset[1]
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated Has not been kept up to date */
|
||||||
|
mouseDrag(x: number, y: number): void {
|
||||||
|
this.offset[0] += x / this.scale
|
||||||
|
this.offset[1] += y / this.scale
|
||||||
|
|
||||||
|
this.onredraw?.(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
changeScale(value: number, zooming_center?: Point): void {
|
||||||
|
if (value < this.min_scale) {
|
||||||
|
value = this.min_scale
|
||||||
|
} else if (value > this.max_scale) {
|
||||||
|
value = this.max_scale
|
||||||
}
|
}
|
||||||
|
|
||||||
computeVisibleArea(viewport: Rect): void {
|
if (value == this.scale) return
|
||||||
if (!this.element) {
|
if (!this.element) return
|
||||||
this.visible_area[0] = this.visible_area[1] = this.visible_area[2] = this.visible_area[3] = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let width = this.element.width
|
|
||||||
let height = this.element.height
|
|
||||||
let startx = -this.offset[0]
|
|
||||||
let starty = -this.offset[1]
|
|
||||||
if (viewport) {
|
|
||||||
startx += viewport[0] / this.scale
|
|
||||||
starty += viewport[1] / this.scale
|
|
||||||
width = viewport[2]
|
|
||||||
height = viewport[3]
|
|
||||||
}
|
|
||||||
const endx = startx + width / this.scale
|
|
||||||
const endy = starty + height / this.scale
|
|
||||||
this.visible_area[0] = startx
|
|
||||||
this.visible_area[1] = starty
|
|
||||||
this.visible_area[2] = endx - startx
|
|
||||||
this.visible_area[3] = endy - starty
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @deprecated Has not been kept up to date */
|
const rect = this.element.getBoundingClientRect()
|
||||||
onMouse(e: CanvasMouseEvent) {
|
if (!rect) return
|
||||||
if (!this.enabled) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const canvas = this.element
|
zooming_center = zooming_center || [rect.width * 0.5, rect.height * 0.5]
|
||||||
const rect = canvas.getBoundingClientRect()
|
const center = this.convertCanvasToOffset(zooming_center)
|
||||||
const x = e.clientX - rect.left
|
this.scale = value
|
||||||
const y = e.clientY - rect.top
|
if (Math.abs(this.scale - 1) < 0.01) this.scale = 1
|
||||||
// FIXME: "canvasx" / y are not referenced anywhere - wrong case
|
|
||||||
// @ts-expect-error Incorrect case
|
|
||||||
e.canvasx = x
|
|
||||||
// @ts-expect-error Incorrect case
|
|
||||||
e.canvasy = y
|
|
||||||
e.dragging = this.dragging
|
|
||||||
|
|
||||||
const is_inside = !this.viewport || isInRect(x, y, this.viewport)
|
const new_center = this.convertCanvasToOffset(zooming_center)
|
||||||
|
const delta_offset = [
|
||||||
|
new_center[0] - center[0],
|
||||||
|
new_center[1] - center[1],
|
||||||
|
]
|
||||||
|
|
||||||
let ignore = false
|
this.offset[0] += delta_offset[0]
|
||||||
if (this.onmouse) {
|
this.offset[1] += delta_offset[1]
|
||||||
ignore = this.onmouse(e)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.type == LiteGraph.pointerevents_method + "down" && is_inside) {
|
this.onredraw?.(this)
|
||||||
this.dragging = true
|
}
|
||||||
LiteGraph.pointerListenerRemove(canvas, "move", this._binded_mouse_callback)
|
|
||||||
LiteGraph.pointerListenerAdd(document, "move", this._binded_mouse_callback)
|
|
||||||
LiteGraph.pointerListenerAdd(document, "up", this._binded_mouse_callback)
|
|
||||||
} else if (e.type == LiteGraph.pointerevents_method + "move") {
|
|
||||||
if (!ignore) {
|
|
||||||
const deltax = x - this.last_mouse[0]
|
|
||||||
const deltay = y - this.last_mouse[1]
|
|
||||||
if (this.dragging) {
|
|
||||||
this.mouseDrag(deltax, deltay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (e.type == LiteGraph.pointerevents_method + "up") {
|
|
||||||
this.dragging = false
|
|
||||||
LiteGraph.pointerListenerRemove(document, "move", this._binded_mouse_callback)
|
|
||||||
LiteGraph.pointerListenerRemove(document, "up", this._binded_mouse_callback)
|
|
||||||
LiteGraph.pointerListenerAdd(canvas, "move", this._binded_mouse_callback)
|
|
||||||
} else if (is_inside &&
|
|
||||||
(e.type == "mousewheel" ||
|
|
||||||
e.type == "wheel" ||
|
|
||||||
e.type == "DOMMouseScroll")) {
|
|
||||||
// @ts-expect-error Deprecated
|
|
||||||
e.eventType = "mousewheel"
|
|
||||||
// @ts-expect-error Deprecated
|
|
||||||
if (e.type == "wheel") e.wheel = -e.deltaY
|
|
||||||
// @ts-expect-error Deprecated
|
|
||||||
else e.wheel = e.wheelDeltaY != null ? e.wheelDeltaY : e.detail * -60
|
|
||||||
|
|
||||||
//from stack overflow
|
changeDeltaScale(value: number, zooming_center?: Point): void {
|
||||||
// @ts-expect-error Deprecated
|
this.changeScale(this.scale * value, zooming_center)
|
||||||
e.delta = e.wheelDelta
|
}
|
||||||
// @ts-expect-error Deprecated
|
|
||||||
? e.wheelDelta / 40
|
|
||||||
: e.deltaY
|
|
||||||
? -e.deltaY / 3
|
|
||||||
: 0
|
|
||||||
// @ts-expect-error Deprecated
|
|
||||||
this.changeDeltaScale(1.0 + e.delta * 0.05)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.last_mouse[0] = x
|
reset(): void {
|
||||||
this.last_mouse[1] = y
|
this.scale = 1
|
||||||
|
this.offset[0] = 0
|
||||||
if (is_inside) {
|
this.offset[1] = 0
|
||||||
e.preventDefault()
|
}
|
||||||
e.stopPropagation()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toCanvasContext(ctx: CanvasRenderingContext2D): void {
|
|
||||||
ctx.scale(this.scale, this.scale)
|
|
||||||
ctx.translate(this.offset[0], this.offset[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
convertOffsetToCanvas(pos: Point): Point {
|
|
||||||
return [
|
|
||||||
(pos[0] + this.offset[0]) * this.scale,
|
|
||||||
(pos[1] + this.offset[1]) * this.scale
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
convertCanvasToOffset(pos: Point, out?: Point): Point {
|
|
||||||
out = out || [0, 0]
|
|
||||||
out[0] = pos[0] / this.scale - this.offset[0]
|
|
||||||
out[1] = pos[1] / this.scale - this.offset[1]
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @deprecated Has not been kept up to date */
|
|
||||||
mouseDrag(x: number, y: number): void {
|
|
||||||
this.offset[0] += x / this.scale
|
|
||||||
this.offset[1] += y / this.scale
|
|
||||||
|
|
||||||
this.onredraw?.(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
changeScale(value: number, zooming_center?: Point): void {
|
|
||||||
if (value < this.min_scale) {
|
|
||||||
value = this.min_scale
|
|
||||||
} else if (value > this.max_scale) {
|
|
||||||
value = this.max_scale
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value == this.scale) return
|
|
||||||
if (!this.element) return
|
|
||||||
|
|
||||||
const rect = this.element.getBoundingClientRect()
|
|
||||||
if (!rect) return
|
|
||||||
|
|
||||||
zooming_center = zooming_center || [
|
|
||||||
rect.width * 0.5,
|
|
||||||
rect.height * 0.5
|
|
||||||
]
|
|
||||||
const center = this.convertCanvasToOffset(zooming_center)
|
|
||||||
this.scale = value
|
|
||||||
if (Math.abs(this.scale - 1) < 0.01) this.scale = 1
|
|
||||||
|
|
||||||
const new_center = this.convertCanvasToOffset(zooming_center)
|
|
||||||
const delta_offset = [
|
|
||||||
new_center[0] - center[0],
|
|
||||||
new_center[1] - center[1]
|
|
||||||
]
|
|
||||||
|
|
||||||
this.offset[0] += delta_offset[0]
|
|
||||||
this.offset[1] += delta_offset[1]
|
|
||||||
|
|
||||||
this.onredraw?.(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
changeDeltaScale(value: number, zooming_center?: Point): void {
|
|
||||||
this.changeScale(this.scale * value, zooming_center)
|
|
||||||
}
|
|
||||||
|
|
||||||
reset(): void {
|
|
||||||
this.scale = 1
|
|
||||||
this.offset[0] = 0
|
|
||||||
this.offset[1] = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
3123
src/LGraph.ts
3123
src/LGraph.ts
File diff suppressed because it is too large
Load Diff
@@ -4,23 +4,23 @@ export enum BadgePosition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface LGraphBadgeOptions {
|
export interface LGraphBadgeOptions {
|
||||||
text: string;
|
text: string
|
||||||
fgColor?: string;
|
fgColor?: string
|
||||||
bgColor?: string;
|
bgColor?: string
|
||||||
fontSize?: number;
|
fontSize?: number
|
||||||
padding?: number;
|
padding?: number
|
||||||
height?: number;
|
height?: number
|
||||||
cornerRadius?: number;
|
cornerRadius?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LGraphBadge {
|
export class LGraphBadge {
|
||||||
text: string;
|
text: string
|
||||||
fgColor: string;
|
fgColor: string
|
||||||
bgColor: string;
|
bgColor: string
|
||||||
fontSize: number;
|
fontSize: number
|
||||||
padding: number;
|
padding: number
|
||||||
height: number;
|
height: number
|
||||||
cornerRadius: number;
|
cornerRadius: number
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
text,
|
text,
|
||||||
@@ -31,27 +31,27 @@ export class LGraphBadge {
|
|||||||
height = 20,
|
height = 20,
|
||||||
cornerRadius = 5,
|
cornerRadius = 5,
|
||||||
}: LGraphBadgeOptions) {
|
}: LGraphBadgeOptions) {
|
||||||
this.text = text;
|
this.text = text
|
||||||
this.fgColor = fgColor;
|
this.fgColor = fgColor
|
||||||
this.bgColor = bgColor;
|
this.bgColor = bgColor
|
||||||
this.fontSize = fontSize;
|
this.fontSize = fontSize
|
||||||
this.padding = padding;
|
this.padding = padding
|
||||||
this.height = height;
|
this.height = height
|
||||||
this.cornerRadius = cornerRadius;
|
this.cornerRadius = cornerRadius
|
||||||
}
|
}
|
||||||
|
|
||||||
get visible() {
|
get visible() {
|
||||||
return this.text.length > 0;
|
return this.text.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
getWidth(ctx: CanvasRenderingContext2D) {
|
getWidth(ctx: CanvasRenderingContext2D) {
|
||||||
if (!this.visible) return 0;
|
if (!this.visible) return 0
|
||||||
|
|
||||||
ctx.save();
|
ctx.save()
|
||||||
ctx.font = `${this.fontSize}px sans-serif`;
|
ctx.font = `${this.fontSize}px sans-serif`
|
||||||
const textWidth = ctx.measureText(this.text).width;
|
const textWidth = ctx.measureText(this.text).width
|
||||||
ctx.restore();
|
ctx.restore()
|
||||||
return textWidth + this.padding * 2;
|
return textWidth + this.padding * 2
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(
|
draw(
|
||||||
@@ -59,32 +59,32 @@ export class LGraphBadge {
|
|||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
): void {
|
): void {
|
||||||
if (!this.visible) return;
|
if (!this.visible) return
|
||||||
|
|
||||||
ctx.save();
|
ctx.save()
|
||||||
ctx.font = `${this.fontSize}px sans-serif`;
|
ctx.font = `${this.fontSize}px sans-serif`
|
||||||
const badgeWidth = this.getWidth(ctx);
|
const badgeWidth = this.getWidth(ctx)
|
||||||
const badgeX = 0;
|
const badgeX = 0
|
||||||
|
|
||||||
// Draw badge background
|
// Draw badge background
|
||||||
ctx.fillStyle = this.bgColor;
|
ctx.fillStyle = this.bgColor
|
||||||
ctx.beginPath();
|
ctx.beginPath()
|
||||||
if (ctx.roundRect) {
|
if (ctx.roundRect) {
|
||||||
ctx.roundRect(x + badgeX, y, badgeWidth, this.height, this.cornerRadius);
|
ctx.roundRect(x + badgeX, y, badgeWidth, this.height, this.cornerRadius)
|
||||||
} else {
|
} else {
|
||||||
// Fallback for browsers that don't support roundRect
|
// Fallback for browsers that don't support roundRect
|
||||||
ctx.rect(x + badgeX, y, badgeWidth, this.height);
|
ctx.rect(x + badgeX, y, badgeWidth, this.height)
|
||||||
}
|
}
|
||||||
ctx.fill();
|
ctx.fill()
|
||||||
|
|
||||||
// Draw badge text
|
// Draw badge text
|
||||||
ctx.fillStyle = this.fgColor;
|
ctx.fillStyle = this.fgColor
|
||||||
ctx.fillText(
|
ctx.fillText(
|
||||||
this.text,
|
this.text,
|
||||||
x + badgeX + this.padding,
|
x + badgeX + this.padding,
|
||||||
y + this.height - this.padding
|
y + this.height - this.padding,
|
||||||
);
|
)
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
15535
src/LGraphCanvas.ts
15535
src/LGraphCanvas.ts
File diff suppressed because it is too large
Load Diff
@@ -1,306 +1,328 @@
|
|||||||
import type { IContextMenuValue, IPinnable, Point, Positionable, Size } from "./interfaces"
|
import type {
|
||||||
|
IContextMenuValue,
|
||||||
|
IPinnable,
|
||||||
|
Point,
|
||||||
|
Positionable,
|
||||||
|
Size,
|
||||||
|
} from "./interfaces"
|
||||||
import type { LGraph } from "./LGraph"
|
import type { LGraph } from "./LGraph"
|
||||||
import type { ISerialisedGroup } from "./types/serialisation"
|
import type { ISerialisedGroup } from "./types/serialisation"
|
||||||
import { LiteGraph } from "./litegraph"
|
import { LiteGraph } from "./litegraph"
|
||||||
import { LGraphCanvas } from "./LGraphCanvas"
|
import { LGraphCanvas } from "./LGraphCanvas"
|
||||||
import { containsCentre, containsRect, isInRectangle, isPointInRect, createBounds, snapPoint } from "./measure"
|
import {
|
||||||
|
containsCentre,
|
||||||
|
containsRect,
|
||||||
|
isInRectangle,
|
||||||
|
isPointInRect,
|
||||||
|
createBounds,
|
||||||
|
snapPoint,
|
||||||
|
} from "./measure"
|
||||||
import { LGraphNode } from "./LGraphNode"
|
import { LGraphNode } from "./LGraphNode"
|
||||||
import { RenderShape, TitleMode } from "./types/globalEnums"
|
import { RenderShape, TitleMode } from "./types/globalEnums"
|
||||||
|
|
||||||
export interface IGraphGroupFlags extends Record<string, unknown> {
|
export interface IGraphGroupFlags extends Record<string, unknown> {
|
||||||
pinned?: true
|
pinned?: true
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LGraphGroup implements Positionable, IPinnable {
|
export class LGraphGroup implements Positionable, IPinnable {
|
||||||
static minWidth = 140
|
static minWidth = 140
|
||||||
static minHeight = 80
|
static minHeight = 80
|
||||||
static resizeLength = 10
|
static resizeLength = 10
|
||||||
static padding = 4
|
static padding = 4
|
||||||
static defaultColour = '#335'
|
static defaultColour = "#335"
|
||||||
|
|
||||||
id: number
|
id: number
|
||||||
color: string
|
color: string
|
||||||
title: string
|
title: string
|
||||||
font?: string
|
font?: string
|
||||||
font_size: number = LiteGraph.DEFAULT_GROUP_FONT || 24
|
font_size: number = LiteGraph.DEFAULT_GROUP_FONT || 24
|
||||||
_bounding: Float32Array = new Float32Array([10, 10, LGraphGroup.minWidth, LGraphGroup.minHeight])
|
_bounding: Float32Array = new Float32Array([
|
||||||
_pos: Point = this._bounding.subarray(0, 2)
|
10,
|
||||||
_size: Size = this._bounding.subarray(2, 4)
|
10,
|
||||||
/** @deprecated See {@link _children} */
|
LGraphGroup.minWidth,
|
||||||
_nodes: LGraphNode[] = []
|
LGraphGroup.minHeight,
|
||||||
_children: Set<Positionable> = new Set()
|
])
|
||||||
graph: LGraph | null = null
|
_pos: Point = this._bounding.subarray(0, 2)
|
||||||
flags: IGraphGroupFlags = {}
|
_size: Size = this._bounding.subarray(2, 4)
|
||||||
selected?: boolean
|
/** @deprecated See {@link _children} */
|
||||||
|
_nodes: LGraphNode[] = []
|
||||||
|
_children: Set<Positionable> = new Set()
|
||||||
|
graph: LGraph | null = null
|
||||||
|
flags: IGraphGroupFlags = {}
|
||||||
|
selected?: boolean
|
||||||
|
|
||||||
constructor(title?: string, id?: number) {
|
constructor(title?: string, id?: number) {
|
||||||
// TODO: Object instantiation pattern requires too much boilerplate and null checking. ID should be passed in via constructor.
|
// TODO: Object instantiation pattern requires too much boilerplate and null checking. ID should be passed in via constructor.
|
||||||
this.id = id ?? -1
|
this.id = id ?? -1
|
||||||
this.title = title || "Group"
|
this.title = title || "Group"
|
||||||
this.color = LGraphCanvas.node_colors.pale_blue
|
this.color = LGraphCanvas.node_colors.pale_blue
|
||||||
? LGraphCanvas.node_colors.pale_blue.groupcolor
|
? LGraphCanvas.node_colors.pale_blue.groupcolor
|
||||||
: "#AAA"
|
: "#AAA"
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Position of the group, as x,y co-ordinates in graph space */
|
||||||
|
get pos() {
|
||||||
|
return this._pos
|
||||||
|
}
|
||||||
|
|
||||||
|
set pos(v) {
|
||||||
|
if (!v || v.length < 2) return
|
||||||
|
|
||||||
|
this._pos[0] = v[0]
|
||||||
|
this._pos[1] = v[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Size of the group, as width,height in graph units */
|
||||||
|
get size() {
|
||||||
|
return this._size
|
||||||
|
}
|
||||||
|
|
||||||
|
set size(v) {
|
||||||
|
if (!v || v.length < 2) return
|
||||||
|
|
||||||
|
this._size[0] = Math.max(LGraphGroup.minWidth, v[0])
|
||||||
|
this._size[1] = Math.max(LGraphGroup.minHeight, v[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
get boundingRect() {
|
||||||
|
return this._bounding
|
||||||
|
}
|
||||||
|
|
||||||
|
get nodes() {
|
||||||
|
return this._nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
get titleHeight() {
|
||||||
|
return this.font_size * 1.4
|
||||||
|
}
|
||||||
|
|
||||||
|
get children(): ReadonlySet<Positionable> {
|
||||||
|
return this._children
|
||||||
|
}
|
||||||
|
|
||||||
|
get pinned() {
|
||||||
|
return !!this.flags.pinned
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevents the group being accidentally moved or resized by mouse interaction.
|
||||||
|
* Toggles pinned state if no value is provided.
|
||||||
|
**/
|
||||||
|
pin(value?: boolean): void {
|
||||||
|
const newState = value === undefined ? !this.pinned : value
|
||||||
|
|
||||||
|
if (newState) this.flags.pinned = true
|
||||||
|
else delete this.flags.pinned
|
||||||
|
}
|
||||||
|
|
||||||
|
unpin(): void {
|
||||||
|
this.pin(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(o: ISerialisedGroup): void {
|
||||||
|
this.id = o.id
|
||||||
|
this.title = o.title
|
||||||
|
this._bounding.set(o.bounding)
|
||||||
|
this.color = o.color
|
||||||
|
this.flags = o.flags || this.flags
|
||||||
|
if (o.font_size) this.font_size = o.font_size
|
||||||
|
}
|
||||||
|
|
||||||
|
serialize(): ISerialisedGroup {
|
||||||
|
const b = this._bounding
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
title: this.title,
|
||||||
|
bounding: [...b],
|
||||||
|
color: this.color,
|
||||||
|
font_size: this.font_size,
|
||||||
|
flags: this.flags,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the group on the canvas
|
||||||
|
* @param {LGraphCanvas} graphCanvas
|
||||||
|
* @param {CanvasRenderingContext2D} ctx
|
||||||
|
*/
|
||||||
|
draw(graphCanvas: LGraphCanvas, ctx: CanvasRenderingContext2D): void {
|
||||||
|
const { padding, resizeLength, defaultColour } = LGraphGroup
|
||||||
|
const font_size = this.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE
|
||||||
|
|
||||||
|
const [x, y] = this._pos
|
||||||
|
const [width, height] = this._size
|
||||||
|
|
||||||
|
// Titlebar
|
||||||
|
ctx.globalAlpha = 0.25 * graphCanvas.editor_alpha
|
||||||
|
ctx.fillStyle = this.color || defaultColour
|
||||||
|
ctx.strokeStyle = this.color || defaultColour
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.rect(x + 0.5, y + 0.5, width, font_size * 1.4)
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
// Group background, border
|
||||||
|
ctx.fillStyle = this.color
|
||||||
|
ctx.strokeStyle = this.color
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.rect(x + 0.5, y + 0.5, width, height)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.globalAlpha = graphCanvas.editor_alpha
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
// Resize marker
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(x + width, y + height)
|
||||||
|
ctx.lineTo(x + width - resizeLength, y + height)
|
||||||
|
ctx.lineTo(x + width, y + height - resizeLength)
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
// Title
|
||||||
|
ctx.font = font_size + "px Arial"
|
||||||
|
ctx.textAlign = "left"
|
||||||
|
ctx.fillText(this.title + (this.pinned ? "📌" : ""), x + padding, y + font_size)
|
||||||
|
|
||||||
|
if (LiteGraph.highlight_selected_group && this.selected) {
|
||||||
|
graphCanvas.drawSelectionBounding(ctx, this._bounding, {
|
||||||
|
shape: RenderShape.BOX,
|
||||||
|
title_height: this.titleHeight,
|
||||||
|
title_mode: TitleMode.NORMAL_TITLE,
|
||||||
|
fgcolor: this.color,
|
||||||
|
padding,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resize(width: number, height: number): boolean {
|
||||||
|
if (this.pinned) return false
|
||||||
|
|
||||||
|
this._size[0] = Math.max(LGraphGroup.minWidth, width)
|
||||||
|
this._size[1] = Math.max(LGraphGroup.minHeight, height)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
move(deltaX: number, deltaY: number, skipChildren: boolean = false): void {
|
||||||
|
if (this.pinned) return
|
||||||
|
|
||||||
|
this._pos[0] += deltaX
|
||||||
|
this._pos[1] += deltaY
|
||||||
|
if (skipChildren === true) return
|
||||||
|
|
||||||
|
for (const item of this._children) {
|
||||||
|
item.move(deltaX, deltaY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
snapToGrid(snapTo: number): boolean {
|
||||||
|
return this.pinned ? false : snapPoint(this.pos, snapTo)
|
||||||
|
}
|
||||||
|
|
||||||
|
recomputeInsideNodes(): void {
|
||||||
|
const { nodes, reroutes, groups } = this.graph
|
||||||
|
const children = this._children
|
||||||
|
this._nodes.length = 0
|
||||||
|
children.clear()
|
||||||
|
|
||||||
|
// Move nodes we overlap the centre point of
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (containsCentre(this._bounding, node.boundingRect)) {
|
||||||
|
this._nodes.push(node)
|
||||||
|
children.add(node)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Position of the group, as x,y co-ordinates in graph space */
|
// Move reroutes we overlap the centre point of
|
||||||
get pos() {
|
for (const reroute of reroutes.values()) {
|
||||||
return this._pos
|
if (isPointInRect(reroute.pos, this._bounding))
|
||||||
}
|
children.add(reroute)
|
||||||
set pos(v) {
|
|
||||||
if (!v || v.length < 2) return
|
|
||||||
|
|
||||||
this._pos[0] = v[0]
|
|
||||||
this._pos[1] = v[1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Size of the group, as width,height in graph units */
|
// Move groups we wholly contain
|
||||||
get size() {
|
for (const group of groups) {
|
||||||
return this._size
|
if (containsRect(this._bounding, group._bounding))
|
||||||
}
|
children.add(group)
|
||||||
set size(v) {
|
|
||||||
if (!v || v.length < 2) return
|
|
||||||
|
|
||||||
this._size[0] = Math.max(LGraphGroup.minWidth, v[0])
|
|
||||||
this._size[1] = Math.max(LGraphGroup.minHeight, v[1])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get boundingRect() {
|
groups.sort((a, b) => {
|
||||||
return this._bounding
|
if (a === this) {
|
||||||
}
|
return children.has(b) ? -1 : 0
|
||||||
|
} else if (b === this) {
|
||||||
|
return children.has(a) ? 1 : 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
get nodes() {
|
/**
|
||||||
return this._nodes
|
* Resizes and moves the group to neatly fit all given {@link objects}.
|
||||||
}
|
* @param objects All objects that should be inside the group
|
||||||
|
* @param padding Value in graph units to add to all sides of the group. Default: 10
|
||||||
|
*/
|
||||||
|
resizeTo(objects: Iterable<Positionable>, padding: number = 10): void {
|
||||||
|
const boundingBox = createBounds(objects, padding)
|
||||||
|
if (boundingBox === null) return
|
||||||
|
|
||||||
get titleHeight() {
|
this.pos[0] = boundingBox[0]
|
||||||
return this.font_size * 1.4
|
this.pos[1] = boundingBox[1] - this.titleHeight
|
||||||
}
|
this.size[0] = boundingBox[2]
|
||||||
|
this.size[1] = boundingBox[3] + this.titleHeight
|
||||||
|
}
|
||||||
|
|
||||||
get children(): ReadonlySet<Positionable> {
|
/**
|
||||||
return this._children
|
* Add nodes to the group and adjust the group's position and size accordingly
|
||||||
}
|
* @param {LGraphNode[]} nodes - The nodes to add to the group
|
||||||
|
* @param {number} [padding=10] - The padding around the group
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
addNodes(nodes: LGraphNode[], padding: number = 10): void {
|
||||||
|
if (!this._nodes && nodes.length === 0) return
|
||||||
|
this.resizeTo([...this.children, ...this._nodes, ...nodes], padding)
|
||||||
|
}
|
||||||
|
|
||||||
get pinned() {
|
getMenuOptions(): IContextMenuValue[] {
|
||||||
return !!this.flags.pinned
|
return [
|
||||||
}
|
{
|
||||||
|
content: this.pinned ? "Unpin" : "Pin",
|
||||||
|
callback: () => {
|
||||||
|
if (this.pinned) this.unpin()
|
||||||
|
else this.pin()
|
||||||
|
this.setDirtyCanvas(false, true)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{ content: "Title", callback: LGraphCanvas.onShowPropertyEditor },
|
||||||
|
{
|
||||||
|
content: "Color",
|
||||||
|
has_submenu: true,
|
||||||
|
callback: LGraphCanvas.onMenuNodeColors,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
content: "Font size",
|
||||||
|
property: "font_size",
|
||||||
|
type: "Number",
|
||||||
|
callback: LGraphCanvas.onShowPropertyEditor,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{ content: "Remove", callback: LGraphCanvas.onMenuNodeRemove },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
isPointInTitlebar(x: number, y: number): boolean {
|
||||||
* Prevents the group being accidentally moved or resized by mouse interaction.
|
const b = this.boundingRect
|
||||||
* Toggles pinned state if no value is provided.
|
return isInRectangle(x, y, b[0], b[1], b[2], this.titleHeight)
|
||||||
**/
|
}
|
||||||
pin(value?: boolean): void {
|
|
||||||
const newState = value === undefined ? !this.pinned : value
|
|
||||||
|
|
||||||
if (newState) this.flags.pinned = true
|
isInResize(x: number, y: number): boolean {
|
||||||
else delete this.flags.pinned
|
const b = this.boundingRect
|
||||||
}
|
const right = b[0] + b[2]
|
||||||
|
const bottom = b[1] + b[3]
|
||||||
|
|
||||||
unpin(): void {
|
return (
|
||||||
this.pin(false)
|
x < right &&
|
||||||
}
|
y < bottom &&
|
||||||
|
x - right + (y - bottom) > -LGraphGroup.resizeLength
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
configure(o: ISerialisedGroup): void {
|
isPointInside = LGraphNode.prototype.isPointInside
|
||||||
this.id = o.id
|
setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas
|
||||||
this.title = o.title
|
|
||||||
this._bounding.set(o.bounding)
|
|
||||||
this.color = o.color
|
|
||||||
this.flags = o.flags || this.flags
|
|
||||||
if (o.font_size) this.font_size = o.font_size
|
|
||||||
}
|
|
||||||
|
|
||||||
serialize(): ISerialisedGroup {
|
|
||||||
const b = this._bounding
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
title: this.title,
|
|
||||||
bounding: [...b],
|
|
||||||
color: this.color,
|
|
||||||
font_size: this.font_size,
|
|
||||||
flags: this.flags,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Draws the group on the canvas
|
|
||||||
* @param {LGraphCanvas} graphCanvas
|
|
||||||
* @param {CanvasRenderingContext2D} ctx
|
|
||||||
*/
|
|
||||||
draw(graphCanvas: LGraphCanvas, ctx: CanvasRenderingContext2D): void {
|
|
||||||
const { padding, resizeLength, defaultColour } = LGraphGroup
|
|
||||||
const font_size = this.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE
|
|
||||||
|
|
||||||
const [x, y] = this._pos
|
|
||||||
const [width, height] = this._size
|
|
||||||
|
|
||||||
// Titlebar
|
|
||||||
ctx.globalAlpha = 0.25 * graphCanvas.editor_alpha
|
|
||||||
ctx.fillStyle = this.color || defaultColour
|
|
||||||
ctx.strokeStyle = this.color || defaultColour
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.rect(x + 0.5, y + 0.5, width, font_size * 1.4)
|
|
||||||
ctx.fill()
|
|
||||||
|
|
||||||
// Group background, border
|
|
||||||
ctx.fillStyle = this.color
|
|
||||||
ctx.strokeStyle = this.color
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.rect(x + 0.5, y + 0.5, width, height)
|
|
||||||
ctx.fill()
|
|
||||||
ctx.globalAlpha = graphCanvas.editor_alpha
|
|
||||||
ctx.stroke()
|
|
||||||
|
|
||||||
// Resize marker
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.moveTo(x + width, y + height)
|
|
||||||
ctx.lineTo(x + width - resizeLength, y + height)
|
|
||||||
ctx.lineTo(x + width, y + height - resizeLength)
|
|
||||||
ctx.fill()
|
|
||||||
|
|
||||||
// Title
|
|
||||||
ctx.font = font_size + "px Arial"
|
|
||||||
ctx.textAlign = "left"
|
|
||||||
ctx.fillText(this.title + (this.pinned ? "📌" : ""), x + padding, y + font_size)
|
|
||||||
|
|
||||||
if (LiteGraph.highlight_selected_group && this.selected) {
|
|
||||||
graphCanvas.drawSelectionBounding(ctx, this._bounding, {
|
|
||||||
shape: RenderShape.BOX,
|
|
||||||
title_height: this.titleHeight,
|
|
||||||
title_mode: TitleMode.NORMAL_TITLE,
|
|
||||||
fgcolor: this.color,
|
|
||||||
padding,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resize(width: number, height: number): boolean {
|
|
||||||
if (this.pinned) return false
|
|
||||||
|
|
||||||
this._size[0] = Math.max(LGraphGroup.minWidth, width)
|
|
||||||
this._size[1] = Math.max(LGraphGroup.minHeight, height)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
move(deltaX: number, deltaY: number, skipChildren: boolean = false): void {
|
|
||||||
if (this.pinned) return
|
|
||||||
|
|
||||||
this._pos[0] += deltaX
|
|
||||||
this._pos[1] += deltaY
|
|
||||||
if (skipChildren === true) return
|
|
||||||
|
|
||||||
for (const item of this._children) {
|
|
||||||
item.move(deltaX, deltaY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritdoc */
|
|
||||||
snapToGrid(snapTo: number): boolean {
|
|
||||||
return this.pinned ? false : snapPoint(this.pos, snapTo)
|
|
||||||
}
|
|
||||||
|
|
||||||
recomputeInsideNodes(): void {
|
|
||||||
const { nodes, reroutes, groups } = this.graph
|
|
||||||
const children = this._children
|
|
||||||
this._nodes.length = 0
|
|
||||||
children.clear()
|
|
||||||
|
|
||||||
// Move nodes we overlap the centre point of
|
|
||||||
for (const node of nodes) {
|
|
||||||
if (containsCentre(this._bounding, node.boundingRect)) {
|
|
||||||
this._nodes.push(node)
|
|
||||||
children.add(node)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move reroutes we overlap the centre point of
|
|
||||||
for (const reroute of reroutes.values()) {
|
|
||||||
if (isPointInRect(reroute.pos, this._bounding))
|
|
||||||
children.add(reroute)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move groups we wholly contain
|
|
||||||
for (const group of groups) {
|
|
||||||
if (containsRect(this._bounding, group._bounding))
|
|
||||||
children.add(group)
|
|
||||||
}
|
|
||||||
|
|
||||||
groups.sort((a, b) => {
|
|
||||||
if (a === this) {
|
|
||||||
return children.has(b) ? -1 : 0
|
|
||||||
} else if (b === this) {
|
|
||||||
return children.has(a) ? 1 : 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resizes and moves the group to neatly fit all given {@link objects}.
|
|
||||||
* @param objects All objects that should be inside the group
|
|
||||||
* @param padding Value in graph units to add to all sides of the group. Default: 10
|
|
||||||
*/
|
|
||||||
resizeTo(objects: Iterable<Positionable>, padding: number = 10): void {
|
|
||||||
const boundingBox = createBounds(objects, padding)
|
|
||||||
if (boundingBox === null) return
|
|
||||||
|
|
||||||
this.pos[0] = boundingBox[0]
|
|
||||||
this.pos[1] = boundingBox[1] - this.titleHeight
|
|
||||||
this.size[0] = boundingBox[2]
|
|
||||||
this.size[1] = boundingBox[3] + this.titleHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add nodes to the group and adjust the group's position and size accordingly
|
|
||||||
* @param {LGraphNode[]} nodes - The nodes to add to the group
|
|
||||||
* @param {number} [padding=10] - The padding around the group
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
addNodes(nodes: LGraphNode[], padding: number = 10): void {
|
|
||||||
if (!this._nodes && nodes.length === 0) return
|
|
||||||
this.resizeTo([...this.children, ...this._nodes, ...nodes], padding)
|
|
||||||
}
|
|
||||||
|
|
||||||
getMenuOptions(): IContextMenuValue[] {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
content: this.pinned ? "Unpin" : "Pin",
|
|
||||||
callback: () => {
|
|
||||||
if (this.pinned) this.unpin()
|
|
||||||
else this.pin()
|
|
||||||
this.setDirtyCanvas(false, true)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
{ content: "Title", callback: LGraphCanvas.onShowPropertyEditor },
|
|
||||||
{
|
|
||||||
content: "Color",
|
|
||||||
has_submenu: true,
|
|
||||||
callback: LGraphCanvas.onMenuNodeColors
|
|
||||||
},
|
|
||||||
{
|
|
||||||
content: "Font size",
|
|
||||||
property: "font_size",
|
|
||||||
type: "Number",
|
|
||||||
callback: LGraphCanvas.onShowPropertyEditor
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
{ content: "Remove", callback: LGraphCanvas.onMenuNodeRemove }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
isPointInTitlebar(x: number, y: number): boolean {
|
|
||||||
const b = this.boundingRect
|
|
||||||
return isInRectangle(x, y, b[0], b[1], b[2], this.titleHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
isInResize(x: number, y: number): boolean {
|
|
||||||
const b = this.boundingRect
|
|
||||||
const right = b[0] + b[2]
|
|
||||||
const bottom = b[1] + b[3]
|
|
||||||
|
|
||||||
return x < right
|
|
||||||
&& y < bottom
|
|
||||||
&& (x - right) + (y - bottom) > -LGraphGroup.resizeLength
|
|
||||||
}
|
|
||||||
|
|
||||||
isPointInside = LGraphNode.prototype.isPointInside
|
|
||||||
setDirtyCanvas = LGraphNode.prototype.setDirtyCanvas
|
|
||||||
}
|
}
|
||||||
|
|||||||
4916
src/LGraphNode.ts
4916
src/LGraphNode.ts
File diff suppressed because it is too large
Load Diff
311
src/LLink.ts
311
src/LLink.ts
@@ -1,152 +1,193 @@
|
|||||||
import type { CanvasColour, LinkNetwork, ISlotType, LinkSegment } from "./interfaces"
|
import type {
|
||||||
|
CanvasColour,
|
||||||
|
LinkNetwork,
|
||||||
|
ISlotType,
|
||||||
|
LinkSegment,
|
||||||
|
} from "./interfaces"
|
||||||
import type { NodeId } from "./LGraphNode"
|
import type { NodeId } from "./LGraphNode"
|
||||||
import type { Reroute, RerouteId } from "./Reroute"
|
import type { Reroute, RerouteId } from "./Reroute"
|
||||||
import type { Serialisable, SerialisableLLink } from "./types/serialisation"
|
import type { Serialisable, SerialisableLLink } from "./types/serialisation"
|
||||||
|
|
||||||
export type LinkId = number
|
export type LinkId = number
|
||||||
|
|
||||||
export type SerialisedLLinkArray = [id: LinkId, origin_id: NodeId, origin_slot: number, target_id: NodeId, target_slot: number, type: ISlotType]
|
export type SerialisedLLinkArray = [
|
||||||
|
id: LinkId,
|
||||||
|
origin_id: NodeId,
|
||||||
|
origin_slot: number,
|
||||||
|
target_id: NodeId,
|
||||||
|
target_slot: number,
|
||||||
|
type: ISlotType,
|
||||||
|
]
|
||||||
|
|
||||||
//this is the class in charge of storing link information
|
// this is the class in charge of storing link information
|
||||||
export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||||
/** Link ID */
|
/** Link ID */
|
||||||
id: LinkId
|
id: LinkId
|
||||||
parentId?: RerouteId
|
parentId?: RerouteId
|
||||||
type: ISlotType
|
type: ISlotType
|
||||||
/** Output node ID */
|
/** Output node ID */
|
||||||
origin_id: NodeId
|
origin_id: NodeId
|
||||||
/** Output slot index */
|
/** Output slot index */
|
||||||
origin_slot: number
|
origin_slot: number
|
||||||
/** Input node ID */
|
/** Input node ID */
|
||||||
target_id: NodeId
|
target_id: NodeId
|
||||||
/** Input slot index */
|
/** Input slot index */
|
||||||
target_slot: number
|
target_slot: number
|
||||||
|
|
||||||
data?: number | string | boolean | { toToolTip?(): string }
|
data?: number | string | boolean | { toToolTip?(): string }
|
||||||
_data?: unknown
|
_data?: unknown
|
||||||
/** Centre point of the link, calculated during render only - can be inaccurate */
|
/** Centre point of the link, calculated during render only - can be inaccurate */
|
||||||
_pos: Float32Array
|
_pos: Float32Array
|
||||||
/** @todo Clean up - never implemented in comfy. */
|
/** @todo Clean up - never implemented in comfy. */
|
||||||
_last_time?: number
|
_last_time?: number
|
||||||
/** The last canvas 2D path that was used to render this link */
|
/** The last canvas 2D path that was used to render this link */
|
||||||
path?: Path2D
|
path?: Path2D
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
_centreAngle?: number
|
_centreAngle?: number
|
||||||
|
|
||||||
#color?: CanvasColour
|
#color?: CanvasColour
|
||||||
/** Custom colour for this link only */
|
/** Custom colour for this link only */
|
||||||
public get color(): CanvasColour { return this.#color }
|
public get color(): CanvasColour {
|
||||||
public set color(value: CanvasColour) {
|
return this.#color
|
||||||
this.#color = value === "" ? null : value
|
}
|
||||||
|
|
||||||
|
public set color(value: CanvasColour) {
|
||||||
|
this.#color = value === "" ? null : value
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
id: LinkId,
|
||||||
|
type: ISlotType,
|
||||||
|
origin_id: NodeId,
|
||||||
|
origin_slot: number,
|
||||||
|
target_id: NodeId,
|
||||||
|
target_slot: number,
|
||||||
|
parentId?: RerouteId,
|
||||||
|
) {
|
||||||
|
this.id = id
|
||||||
|
this.type = type
|
||||||
|
this.origin_id = origin_id
|
||||||
|
this.origin_slot = origin_slot
|
||||||
|
this.target_id = target_id
|
||||||
|
this.target_slot = target_slot
|
||||||
|
this.parentId = parentId
|
||||||
|
|
||||||
|
this._data = null
|
||||||
|
this._pos = new Float32Array(2) // center
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use {@link LLink.create} */
|
||||||
|
static createFromArray(data: SerialisedLLinkArray): LLink {
|
||||||
|
return new LLink(data[0], data[5], data[1], data[2], data[3], data[4])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LLink static factory: creates a new LLink from the provided data.
|
||||||
|
* @param data Serialised LLink data to create the link from
|
||||||
|
* @returns A new LLink
|
||||||
|
*/
|
||||||
|
static create(data: SerialisableLLink): LLink {
|
||||||
|
return new LLink(
|
||||||
|
data.id,
|
||||||
|
data.type,
|
||||||
|
data.origin_id,
|
||||||
|
data.origin_slot,
|
||||||
|
data.target_id,
|
||||||
|
data.target_slot,
|
||||||
|
data.parentId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all reroutes from the output slot to this segment. If this segment is a reroute, it will be the last element.
|
||||||
|
* @returns An ordered array of all reroutes from the node output to
|
||||||
|
* this reroute or the reroute before it. Otherwise, an empty array.
|
||||||
|
*/
|
||||||
|
static getReroutes(
|
||||||
|
network: LinkNetwork,
|
||||||
|
linkSegment: LinkSegment,
|
||||||
|
): Reroute[] {
|
||||||
|
return network.reroutes.get(linkSegment.parentId)
|
||||||
|
?.getReroutes() ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the reroute in the chain after the provided reroute ID.
|
||||||
|
* @param network The network this link belongs to
|
||||||
|
* @param linkSegment The starting point of the search (input side).
|
||||||
|
* Typically the LLink object itself, but can be any link segment.
|
||||||
|
* @param rerouteId The matching reroute will have this set as its {@link parentId}.
|
||||||
|
* @returns The reroute that was found, `undefined` if no reroute was found, or `null` if an infinite loop was detected.
|
||||||
|
*/
|
||||||
|
static findNextReroute(
|
||||||
|
network: LinkNetwork,
|
||||||
|
linkSegment: LinkSegment,
|
||||||
|
rerouteId: RerouteId,
|
||||||
|
): Reroute | null | undefined {
|
||||||
|
return network.reroutes.get(linkSegment.parentId)
|
||||||
|
?.findNextReroute(rerouteId)
|
||||||
|
}
|
||||||
|
|
||||||
|
configure(o: LLink | SerialisedLLinkArray) {
|
||||||
|
if (Array.isArray(o)) {
|
||||||
|
this.id = o[0]
|
||||||
|
this.origin_id = o[1]
|
||||||
|
this.origin_slot = o[2]
|
||||||
|
this.target_id = o[3]
|
||||||
|
this.target_slot = o[4]
|
||||||
|
this.type = o[5]
|
||||||
|
} else {
|
||||||
|
this.id = o.id
|
||||||
|
this.type = o.type
|
||||||
|
this.origin_id = o.origin_id
|
||||||
|
this.origin_slot = o.origin_slot
|
||||||
|
this.target_id = o.target_id
|
||||||
|
this.target_slot = o.target_slot
|
||||||
|
this.parentId = o.parentId
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor(id: LinkId, type: ISlotType, origin_id: NodeId, origin_slot: number, target_id: NodeId, target_slot: number, parentId?: RerouteId) {
|
/**
|
||||||
this.id = id
|
* Disconnects a link and removes it from the graph, cleaning up any reroutes that are no longer used
|
||||||
this.type = type
|
* @param network The container (LGraph) where reroutes should be updated
|
||||||
this.origin_id = origin_id
|
* @param keepReroutes If `true`, reroutes will not be garbage collected.
|
||||||
this.origin_slot = origin_slot
|
*/
|
||||||
this.target_id = target_id
|
disconnect(network: LinkNetwork, keepReroutes?: boolean): void {
|
||||||
this.target_slot = target_slot
|
const reroutes = LLink.getReroutes(network, this)
|
||||||
this.parentId = parentId
|
|
||||||
|
|
||||||
this._data = null
|
for (const reroute of reroutes) {
|
||||||
this._pos = new Float32Array(2) //center
|
reroute.linkIds.delete(this.id)
|
||||||
|
if (!keepReroutes && !reroute.linkIds.size)
|
||||||
|
network.reroutes.delete(reroute.id)
|
||||||
}
|
}
|
||||||
|
network.links.delete(this.id)
|
||||||
|
}
|
||||||
|
|
||||||
/** @deprecated Use {@link LLink.create} */
|
/**
|
||||||
static createFromArray(data: SerialisedLLinkArray): LLink {
|
* @deprecated Prefer {@link LLink.asSerialisable} (returns an object, not an array)
|
||||||
return new LLink(data[0], data[5], data[1], data[2], data[3], data[4])
|
* @returns An array representing this LLink
|
||||||
}
|
*/
|
||||||
|
serialize(): SerialisedLLinkArray {
|
||||||
/**
|
return [
|
||||||
* LLink static factory: creates a new LLink from the provided data.
|
this.id,
|
||||||
* @param data Serialised LLink data to create the link from
|
this.origin_id,
|
||||||
* @returns A new LLink
|
this.origin_slot,
|
||||||
*/
|
this.target_id,
|
||||||
static create(data: SerialisableLLink): LLink {
|
this.target_slot,
|
||||||
return new LLink(data.id, data.type, data.origin_id, data.origin_slot, data.target_id, data.target_slot, data.parentId)
|
this.type,
|
||||||
}
|
]
|
||||||
|
}
|
||||||
/**
|
|
||||||
* Gets all reroutes from the output slot to this segment. If this segment is a reroute, it will be the last element.
|
asSerialisable(): SerialisableLLink {
|
||||||
* @returns An ordered array of all reroutes from the node output to this reroute or the reroute before it. Otherwise, an empty array.
|
const copy: SerialisableLLink = {
|
||||||
*/
|
id: this.id,
|
||||||
static getReroutes(network: LinkNetwork, linkSegment: LinkSegment): Reroute[] {
|
origin_id: this.origin_id,
|
||||||
return network.reroutes.get(linkSegment.parentId)
|
origin_slot: this.origin_slot,
|
||||||
?.getReroutes() ?? []
|
target_id: this.target_id,
|
||||||
}
|
target_slot: this.target_slot,
|
||||||
|
type: this.type,
|
||||||
/**
|
|
||||||
* Finds the reroute in the chain after the provided reroute ID.
|
|
||||||
* @param network The network this link belongs to
|
|
||||||
* @param linkSegment The starting point of the search (input side). Typically the LLink object itself, but can be any link segment.
|
|
||||||
* @param rerouteId The matching reroute will have this set as its {@link parentId}.
|
|
||||||
* @returns The reroute that was found, `undefined` if no reroute was found, or `null` if an infinite loop was detected.
|
|
||||||
*/
|
|
||||||
static findNextReroute(network: LinkNetwork, linkSegment: LinkSegment, rerouteId: RerouteId): Reroute | null | undefined {
|
|
||||||
return network.reroutes.get(linkSegment.parentId)
|
|
||||||
?.findNextReroute(rerouteId)
|
|
||||||
}
|
|
||||||
|
|
||||||
configure(o: LLink | SerialisedLLinkArray) {
|
|
||||||
if (Array.isArray(o)) {
|
|
||||||
this.id = o[0]
|
|
||||||
this.origin_id = o[1]
|
|
||||||
this.origin_slot = o[2]
|
|
||||||
this.target_id = o[3]
|
|
||||||
this.target_slot = o[4]
|
|
||||||
this.type = o[5]
|
|
||||||
} else {
|
|
||||||
this.id = o.id
|
|
||||||
this.type = o.type
|
|
||||||
this.origin_id = o.origin_id
|
|
||||||
this.origin_slot = o.origin_slot
|
|
||||||
this.target_id = o.target_id
|
|
||||||
this.target_slot = o.target_slot
|
|
||||||
this.parentId = o.parentId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disconnects a link and removes it from the graph, cleaning up any reroutes that are no longer used
|
|
||||||
* @param network The container (LGraph) where reroutes should be updated
|
|
||||||
* @param keepReroutes If `true`, reroutes will not be garbage collected.
|
|
||||||
*/
|
|
||||||
disconnect(network: LinkNetwork, keepReroutes?: boolean): void {
|
|
||||||
const reroutes = LLink.getReroutes(network, this)
|
|
||||||
|
|
||||||
for (const reroute of reroutes) {
|
|
||||||
reroute.linkIds.delete(this.id)
|
|
||||||
if (!keepReroutes && !reroute.linkIds.size) network.reroutes.delete(reroute.id)
|
|
||||||
}
|
|
||||||
network.links.delete(this.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Prefer {@link LLink.asSerialisable} (returns an object, not an array)
|
|
||||||
* @returns An array representing this LLink
|
|
||||||
*/
|
|
||||||
serialize(): SerialisedLLinkArray {
|
|
||||||
return [
|
|
||||||
this.id,
|
|
||||||
this.origin_id,
|
|
||||||
this.origin_slot,
|
|
||||||
this.target_id,
|
|
||||||
this.target_slot,
|
|
||||||
this.type
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
asSerialisable(): SerialisableLLink {
|
|
||||||
const copy: SerialisableLLink = {
|
|
||||||
id: this.id,
|
|
||||||
origin_id: this.origin_id,
|
|
||||||
origin_slot: this.origin_slot,
|
|
||||||
target_id: this.target_id,
|
|
||||||
target_slot: this.target_slot,
|
|
||||||
type: this.type
|
|
||||||
}
|
|
||||||
if (this.parentId) copy.parentId = this.parentId
|
|
||||||
return copy
|
|
||||||
}
|
}
|
||||||
|
if (this.parentId) copy.parentId = this.parentId
|
||||||
|
return copy
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,55 +1,61 @@
|
|||||||
/** Temporary workaround until downstream consumers migrate to Map. A brittle wrapper with many flaws, but should be fine for simple maps using int indexes. */
|
/**
|
||||||
|
* Temporary workaround until downstream consumers migrate to Map.
|
||||||
|
* A brittle wrapper with many flaws, but should be fine for simple maps using int indexes.
|
||||||
|
*/
|
||||||
export class MapProxyHandler<V> implements ProxyHandler<Map<number | string, V>> {
|
export class MapProxyHandler<V> implements ProxyHandler<Map<number | string, V>> {
|
||||||
getOwnPropertyDescriptor(target: Map<number | string, V>, p: string | symbol): PropertyDescriptor | undefined {
|
getOwnPropertyDescriptor(
|
||||||
const value = this.get(target, p)
|
target: Map<number | string, V>,
|
||||||
if (value) return {
|
p: string | symbol,
|
||||||
configurable: true,
|
): PropertyDescriptor | undefined {
|
||||||
enumerable: true,
|
const value = this.get(target, p)
|
||||||
value
|
if (value) return {
|
||||||
}
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
value,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
has(target: Map<number | string, V>, p: string | symbol): boolean {
|
has(target: Map<number | string, V>, p: string | symbol): boolean {
|
||||||
if (typeof p === "symbol") return false
|
if (typeof p === "symbol") return false
|
||||||
|
|
||||||
const int = parseInt(p, 10)
|
const int = parseInt(p, 10)
|
||||||
return target.has(!isNaN(int) ? int : p)
|
return target.has(!isNaN(int) ? int : p)
|
||||||
}
|
}
|
||||||
|
|
||||||
ownKeys(target: Map<number | string, V>): ArrayLike<string | symbol> {
|
ownKeys(target: Map<number | string, V>): ArrayLike<string | symbol> {
|
||||||
return [...target.keys()].map(x => String(x))
|
return [...target.keys()].map(x => String(x))
|
||||||
}
|
}
|
||||||
|
|
||||||
get(target: Map<number | string, V>, p: string | symbol): any {
|
get(target: Map<number | string, V>, p: string | symbol): any {
|
||||||
// Workaround does not support link IDs of "values", "entries", "constructor", etc.
|
// Workaround does not support link IDs of "values", "entries", "constructor", etc.
|
||||||
if (p in target) return Reflect.get(target, p, target)
|
if (p in target) return Reflect.get(target, p, target)
|
||||||
if (typeof p === "symbol") return
|
if (typeof p === "symbol") return
|
||||||
|
|
||||||
const int = parseInt(p, 10)
|
const int = parseInt(p, 10)
|
||||||
return target.get(!isNaN(int) ? int : p)
|
return target.get(!isNaN(int) ? int : p)
|
||||||
}
|
}
|
||||||
|
|
||||||
set(target: Map<number | string, V>, p: string | symbol, newValue: any): boolean {
|
set(target: Map<number | string, V>, p: string | symbol, newValue: any): boolean {
|
||||||
if (typeof p === "symbol") return false
|
if (typeof p === "symbol") return false
|
||||||
|
|
||||||
const int = parseInt(p, 10)
|
const int = parseInt(p, 10)
|
||||||
target.set(!isNaN(int) ? int : p, newValue)
|
target.set(!isNaN(int) ? int : p, newValue)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteProperty(target: Map<number | string, V>, p: string | symbol): boolean {
|
deleteProperty(target: Map<number | string, V>, p: string | symbol): boolean {
|
||||||
return target.delete(p as number | string)
|
return target.delete(p as number | string)
|
||||||
}
|
}
|
||||||
|
|
||||||
static bindAllMethods(map: Map<any, any>): void {
|
static bindAllMethods(map: Map<any, any>): void {
|
||||||
map.clear = map.clear.bind(map)
|
map.clear = map.clear.bind(map)
|
||||||
map.delete = map.delete.bind(map)
|
map.delete = map.delete.bind(map)
|
||||||
map.forEach = map.forEach.bind(map)
|
map.forEach = map.forEach.bind(map)
|
||||||
map.get = map.get.bind(map)
|
map.get = map.get.bind(map)
|
||||||
map.has = map.has.bind(map)
|
map.has = map.has.bind(map)
|
||||||
map.set = map.set.bind(map)
|
map.set = map.set.bind(map)
|
||||||
map.entries = map.entries.bind(map)
|
map.entries = map.entries.bind(map)
|
||||||
map.keys = map.keys.bind(map)
|
map.keys = map.keys.bind(map)
|
||||||
map.values = map.values.bind(map)
|
map.values = map.values.bind(map)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
562
src/Reroute.ts
562
src/Reroute.ts
@@ -1,4 +1,11 @@
|
|||||||
import type { CanvasColour, LinkSegment, LinkNetwork, Point, Positionable, ReadOnlyRect } from "./interfaces"
|
import type {
|
||||||
|
CanvasColour,
|
||||||
|
LinkSegment,
|
||||||
|
LinkNetwork,
|
||||||
|
Point,
|
||||||
|
Positionable,
|
||||||
|
ReadOnlyRect,
|
||||||
|
} from "./interfaces"
|
||||||
import { LLink, type LinkId } from "./LLink"
|
import { LLink, type LinkId } from "./LLink"
|
||||||
import type { SerialisableReroute, Serialisable } from "./types/serialisation"
|
import type { SerialisableReroute, Serialisable } from "./types/serialisation"
|
||||||
import { distance } from "./measure"
|
import { distance } from "./measure"
|
||||||
@@ -8,285 +15,302 @@ export type RerouteId = number
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an additional point on the graph that a link path will travel through. Used for visual organisation only.
|
* Represents an additional point on the graph that a link path will travel through. Used for visual organisation only.
|
||||||
*
|
*
|
||||||
* Requires no disposal or clean up.
|
* Requires no disposal or clean up.
|
||||||
* Stores only primitive values (IDs) to reference other items in its network, and a `WeakRef` to a {@link LinkNetwork} to resolve them.
|
* Stores only primitive values (IDs) to reference other items in its network,
|
||||||
|
* and a `WeakRef` to a {@link LinkNetwork} to resolve them.
|
||||||
*/
|
*/
|
||||||
export class Reroute implements Positionable, LinkSegment, Serialisable<SerialisableReroute> {
|
export class Reroute implements Positionable, LinkSegment, Serialisable<SerialisableReroute> {
|
||||||
static radius: number = 10
|
static radius: number = 10
|
||||||
|
|
||||||
#malloc = new Float32Array(8)
|
#malloc = new Float32Array(8)
|
||||||
|
|
||||||
/** The network this reroute belongs to. Contains all valid links and reroutes. */
|
/** The network this reroute belongs to. Contains all valid links and reroutes. */
|
||||||
#network: WeakRef<LinkNetwork>
|
#network: WeakRef<LinkNetwork>
|
||||||
|
|
||||||
#parentId?: RerouteId
|
#parentId?: RerouteId
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
public get parentId(): RerouteId {
|
public get parentId(): RerouteId {
|
||||||
return this.#parentId
|
return this.#parentId
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Ignores attempts to create an infinite loop. @inheritdoc */
|
||||||
|
public set parentId(value: RerouteId) {
|
||||||
|
if (value === this.id) return
|
||||||
|
if (this.getReroutes() === null) return
|
||||||
|
this.#parentId = value
|
||||||
|
}
|
||||||
|
|
||||||
|
#pos = this.#malloc.subarray(0, 2)
|
||||||
|
/** @inheritdoc */
|
||||||
|
get pos(): Point {
|
||||||
|
return this.#pos
|
||||||
|
}
|
||||||
|
|
||||||
|
set pos(value: Point) {
|
||||||
|
if (!(value?.length >= 2))
|
||||||
|
throw new TypeError("Reroute.pos is an x,y point, and expects an indexable with at least two values.")
|
||||||
|
this.#pos[0] = value[0]
|
||||||
|
this.#pos[1] = value[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
get boundingRect(): ReadOnlyRect {
|
||||||
|
const { radius } = Reroute
|
||||||
|
const [x, y] = this.#pos
|
||||||
|
return [x - radius, y - radius, 2 * radius, 2 * radius]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
selected?: boolean
|
||||||
|
|
||||||
|
/** The ID ({@link LLink.id}) of every link using this reroute */
|
||||||
|
linkIds: Set<LinkId>
|
||||||
|
|
||||||
|
/** The averaged angle of every link through this reroute. */
|
||||||
|
otherAngle: number = 0
|
||||||
|
|
||||||
|
/** Cached cos */
|
||||||
|
cos: number = 0
|
||||||
|
sin: number = 0
|
||||||
|
|
||||||
|
/** Bezier curve control point for the "target" (input) side of the link */
|
||||||
|
controlPoint: Point = this.#malloc.subarray(4, 6)
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
path?: Path2D
|
||||||
|
/** @inheritdoc */
|
||||||
|
_centreAngle?: number
|
||||||
|
/** @inheritdoc */
|
||||||
|
_pos: Float32Array = this.#malloc.subarray(6, 8)
|
||||||
|
|
||||||
|
/** Colour of the first link that rendered this reroute */
|
||||||
|
_colour?: CanvasColour
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to ensure reroute angles are only executed once per frame.
|
||||||
|
* @todo Calculate on change instead.
|
||||||
|
*/
|
||||||
|
#lastRenderTime: number = -Infinity
|
||||||
|
#buffer: Point = this.#malloc.subarray(2, 4)
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
get origin_id(): NodeId | undefined {
|
||||||
|
// if (!this.linkIds.size) return this.#network.deref()?.reroutes.get(this.parentId)
|
||||||
|
return this.#network.deref()
|
||||||
|
?.links.get(this.linkIds.values().next().value)
|
||||||
|
?.origin_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
get origin_slot(): number | undefined {
|
||||||
|
return this.#network.deref()
|
||||||
|
?.links.get(this.linkIds.values().next().value)
|
||||||
|
?.origin_slot
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialises a new link reroute object.
|
||||||
|
* @param id Unique identifier for this reroute
|
||||||
|
* @param network The network of links this reroute belongs to. Internally converted to a WeakRef.
|
||||||
|
* @param pos Position in graph coordinates
|
||||||
|
* @param linkIds Link IDs ({@link LLink.id}) of all links that use this reroute
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
public readonly id: RerouteId,
|
||||||
|
network: LinkNetwork,
|
||||||
|
pos?: Point,
|
||||||
|
parentId?: RerouteId,
|
||||||
|
linkIds?: Iterable<LinkId>,
|
||||||
|
) {
|
||||||
|
this.#network = new WeakRef(network)
|
||||||
|
this.update(parentId, pos, linkIds)
|
||||||
|
this.linkIds ??= new Set()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a new parentId to the reroute, and optinoally a new position and linkId.
|
||||||
|
* Primarily used for deserialisation.
|
||||||
|
* @param parentId The ID of the reroute prior to this reroute, or
|
||||||
|
* `undefined` if it is the first reroute connected to a nodes output
|
||||||
|
* @param pos The position of this reroute
|
||||||
|
* @param linkIds All link IDs that pass through this reroute
|
||||||
|
*/
|
||||||
|
update(
|
||||||
|
parentId: RerouteId | undefined,
|
||||||
|
pos?: Point,
|
||||||
|
linkIds?: Iterable<LinkId>,
|
||||||
|
): void {
|
||||||
|
this.parentId = parentId
|
||||||
|
if (pos) this.pos = pos
|
||||||
|
if (linkIds) this.linkIds = new Set(linkIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the linkIds this reroute has. Removes broken links.
|
||||||
|
* @param links Collection of valid links
|
||||||
|
* @returns true if any links remain after validation
|
||||||
|
*/
|
||||||
|
validateLinks(links: Map<LinkId, LLink>): boolean {
|
||||||
|
const { linkIds } = this
|
||||||
|
for (const linkId of linkIds) {
|
||||||
|
if (!links.get(linkId)) linkIds.delete(linkId)
|
||||||
}
|
}
|
||||||
/** Ignores attempts to create an infinite loop. @inheritdoc */
|
return linkIds.size > 0
|
||||||
public set parentId(value: RerouteId) {
|
}
|
||||||
if (value === this.id) return
|
|
||||||
if (this.getReroutes() === null) return
|
/**
|
||||||
this.#parentId = value
|
* Retrieves an ordered array of all reroutes from the node output.
|
||||||
|
* @param visited Internal. A set of reroutes that this function
|
||||||
|
* has already visited whilst recursing up the chain.
|
||||||
|
* @returns An ordered array of all reroutes from the node output to this reroute, inclusive.
|
||||||
|
* `null` if an infinite loop is detected.
|
||||||
|
* `undefined` if the reroute chain or {@link LinkNetwork} are invalid.
|
||||||
|
*/
|
||||||
|
getReroutes(visited = new Set<Reroute>()): Reroute[] | null | undefined {
|
||||||
|
// No parentId - last in the chain
|
||||||
|
if (this.#parentId === undefined) return [this]
|
||||||
|
// Invalid chain - looped
|
||||||
|
if (visited.has(this)) return null
|
||||||
|
visited.add(this)
|
||||||
|
|
||||||
|
const parent = this.#network.deref()?.reroutes.get(this.#parentId)
|
||||||
|
// Invalid parent (or network) - drop silently to recover
|
||||||
|
if (!parent) {
|
||||||
|
this.#parentId = undefined
|
||||||
|
return [this]
|
||||||
}
|
}
|
||||||
|
|
||||||
#pos = this.#malloc.subarray(0, 2)
|
const reroutes = parent.getReroutes(visited)
|
||||||
/** @inheritdoc */
|
reroutes?.push(this)
|
||||||
get pos(): Point {
|
return reroutes
|
||||||
return this.#pos
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal. Called by {@link LLink.findNextReroute}. Not intended for use by itself.
|
||||||
|
* @param withParentId The rerouteId to look for
|
||||||
|
* @param visited A set of reroutes that have already been visited
|
||||||
|
* @returns The reroute that was found, `undefined` if no reroute was found, or `null` if an infinite loop was detected.
|
||||||
|
*/
|
||||||
|
findNextReroute(
|
||||||
|
withParentId: RerouteId,
|
||||||
|
visited = new Set<Reroute>(),
|
||||||
|
): Reroute | null | undefined {
|
||||||
|
if (this.#parentId === withParentId) return this
|
||||||
|
if (visited.has(this)) return null
|
||||||
|
visited.add(this)
|
||||||
|
|
||||||
|
return this.#network
|
||||||
|
.deref()
|
||||||
|
?.reroutes.get(this.#parentId)
|
||||||
|
?.findNextReroute(withParentId, visited)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
move(deltaX: number, deltaY: number) {
|
||||||
|
this.#pos[0] += deltaX
|
||||||
|
this.#pos[1] += deltaY
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
snapToGrid(snapTo: number): boolean {
|
||||||
|
if (!snapTo) return false
|
||||||
|
|
||||||
|
const { pos } = this
|
||||||
|
pos[0] = snapTo * Math.round(pos[0] / snapTo)
|
||||||
|
pos[1] = snapTo * Math.round(pos[1] / snapTo)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateAngle(lastRenderTime: number, network: LinkNetwork, linkStart: Point): void {
|
||||||
|
// Ensure we run once per render
|
||||||
|
if (!(lastRenderTime > this.#lastRenderTime)) return
|
||||||
|
this.#lastRenderTime = lastRenderTime
|
||||||
|
|
||||||
|
const { links } = network
|
||||||
|
const { linkIds, id } = this
|
||||||
|
const angles: number[] = []
|
||||||
|
let sum = 0
|
||||||
|
for (const linkId of linkIds) {
|
||||||
|
const link = links.get(linkId)
|
||||||
|
// Remove the linkId or just ignore?
|
||||||
|
if (!link) continue
|
||||||
|
|
||||||
|
const pos = LLink.findNextReroute(network, link, id)?.pos ??
|
||||||
|
network.getNodeById(link.target_id)
|
||||||
|
?.getConnectionPos(true, link.target_slot, this.#buffer)
|
||||||
|
if (!pos) continue
|
||||||
|
|
||||||
|
// TODO: Store points/angles, check if changed, skip calcs.
|
||||||
|
const angle = Math.atan2(pos[1] - this.#pos[1], pos[0] - this.#pos[0])
|
||||||
|
angles.push(angle)
|
||||||
|
sum += angle
|
||||||
}
|
}
|
||||||
set pos(value: Point) {
|
if (!angles.length) return
|
||||||
if (!(value?.length >= 2)) throw new TypeError("Reroute.pos is an x,y point, and expects an indexable with at least two values.")
|
|
||||||
this.#pos[0] = value[0]
|
sum /= angles.length
|
||||||
this.#pos[1] = value[1]
|
|
||||||
|
const originToReroute = Math.atan2(
|
||||||
|
this.#pos[1] - linkStart[1],
|
||||||
|
this.#pos[0] - linkStart[0],
|
||||||
|
)
|
||||||
|
let diff = (originToReroute - sum) * 0.5
|
||||||
|
if (Math.abs(diff) > Math.PI * 0.5) diff += Math.PI
|
||||||
|
const dist = Math.min(80, distance(linkStart, this.#pos) * 0.25)
|
||||||
|
|
||||||
|
// Store results
|
||||||
|
const originDiff = originToReroute - diff
|
||||||
|
const cos = Math.cos(originDiff)
|
||||||
|
const sin = Math.sin(originDiff)
|
||||||
|
|
||||||
|
this.otherAngle = originDiff
|
||||||
|
this.cos = cos
|
||||||
|
this.sin = sin
|
||||||
|
this.controlPoint[0] = dist * -cos
|
||||||
|
this.controlPoint[1] = dist * -sin
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the reroute on the canvas.
|
||||||
|
* @param ctx Canvas context to draw on
|
||||||
|
* @param colour Reroute colour (typically link colour)
|
||||||
|
*
|
||||||
|
* @remarks Leaves {@link ctx}.fillStyle, strokeStyle, and lineWidth dirty (perf.).
|
||||||
|
*/
|
||||||
|
draw(ctx: CanvasRenderingContext2D): void {
|
||||||
|
const { pos } = this
|
||||||
|
ctx.fillStyle = this._colour
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(pos[0], pos[1], Reroute.radius, 0, 2 * Math.PI)
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.strokeStyle = "rgb(0,0,0,0.5)"
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
ctx.fillStyle = "#ffffff55"
|
||||||
|
ctx.strokeStyle = "rgb(0,0,0,0.3)"
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(pos[0], pos[1], 8, 0, 2 * Math.PI)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.stroke()
|
||||||
|
|
||||||
|
if (this.selected) {
|
||||||
|
ctx.strokeStyle = "#fff"
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(pos[0], pos[1], 12, 0, 2 * Math.PI)
|
||||||
|
ctx.stroke()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
get boundingRect(): ReadOnlyRect {
|
asSerialisable(): SerialisableReroute {
|
||||||
const { radius } = Reroute
|
return {
|
||||||
const [x, y] = this.#pos
|
id: this.id,
|
||||||
return [x - radius, y - radius, 2 * radius, 2 * radius]
|
parentId: this.parentId,
|
||||||
}
|
pos: [this.pos[0], this.pos[1]],
|
||||||
|
linkIds: [...this.linkIds],
|
||||||
/** @inheritdoc */
|
|
||||||
selected?: boolean
|
|
||||||
|
|
||||||
/** The ID ({@link LLink.id}) of every link using this reroute */
|
|
||||||
linkIds: Set<LinkId>
|
|
||||||
|
|
||||||
/** The averaged angle of every link through this reroute. */
|
|
||||||
otherAngle: number = 0
|
|
||||||
|
|
||||||
/** Cached cos */
|
|
||||||
cos: number = 0
|
|
||||||
sin: number = 0
|
|
||||||
|
|
||||||
/** Bezier curve control point for the "target" (input) side of the link */
|
|
||||||
controlPoint: Point = this.#malloc.subarray(4, 6)
|
|
||||||
|
|
||||||
/** @inheritdoc */
|
|
||||||
path?: Path2D
|
|
||||||
/** @inheritdoc */
|
|
||||||
_centreAngle?: number
|
|
||||||
/** @inheritdoc */
|
|
||||||
_pos: Float32Array = this.#malloc.subarray(6, 8)
|
|
||||||
|
|
||||||
/** Colour of the first link that rendered this reroute */
|
|
||||||
_colour?: CanvasColour
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to ensure reroute angles are only executed once per frame.
|
|
||||||
* @todo Calculate on change instead.
|
|
||||||
*/
|
|
||||||
#lastRenderTime: number = -Infinity
|
|
||||||
#buffer: Point = this.#malloc.subarray(2, 4)
|
|
||||||
|
|
||||||
/** @inheritdoc */
|
|
||||||
get origin_id(): NodeId | undefined {
|
|
||||||
// if (!this.linkIds.size) return this.#network.deref()?.reroutes.get(this.parentId)
|
|
||||||
return this.#network.deref()
|
|
||||||
?.links.get(this.linkIds.values().next().value)
|
|
||||||
?.origin_id
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritdoc */
|
|
||||||
get origin_slot(): number | undefined {
|
|
||||||
return this.#network.deref()
|
|
||||||
?.links.get(this.linkIds.values().next().value)
|
|
||||||
?.origin_slot
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialises a new link reroute object.
|
|
||||||
* @param id Unique identifier for this reroute
|
|
||||||
* @param network The network of links this reroute belongs to. Internally converted to a WeakRef.
|
|
||||||
* @param pos Position in graph coordinates
|
|
||||||
* @param linkIds Link IDs ({@link LLink.id}) of all links that use this reroute
|
|
||||||
*/
|
|
||||||
constructor(
|
|
||||||
public readonly id: RerouteId,
|
|
||||||
network: LinkNetwork,
|
|
||||||
pos?: Point,
|
|
||||||
parentId?: RerouteId,
|
|
||||||
linkIds?: Iterable<LinkId>
|
|
||||||
) {
|
|
||||||
this.#network = new WeakRef(network)
|
|
||||||
this.update(parentId, pos, linkIds)
|
|
||||||
this.linkIds ??= new Set()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies a new parentId to the reroute, and optinoally a new position and linkId.
|
|
||||||
* Primarily used for deserialisation.
|
|
||||||
* @param parentId The ID of the reroute prior to this reroute, or `undefined` if it is the first reroute connected to a nodes output
|
|
||||||
* @param pos The position of this reroute
|
|
||||||
* @param linkIds All link IDs that pass through this reroute
|
|
||||||
*/
|
|
||||||
update(parentId: RerouteId | undefined, pos?: Point, linkIds?: Iterable<LinkId>): void {
|
|
||||||
this.parentId = parentId
|
|
||||||
if (pos) this.pos = pos
|
|
||||||
if (linkIds) this.linkIds = new Set(linkIds)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates the linkIds this reroute has. Removes broken links.
|
|
||||||
* @param links Collection of valid links
|
|
||||||
* @returns true if any links remain after validation
|
|
||||||
*/
|
|
||||||
validateLinks(links: Map<LinkId, LLink>): boolean {
|
|
||||||
const { linkIds } = this
|
|
||||||
for (const linkId of linkIds) {
|
|
||||||
if (!links.get(linkId)) linkIds.delete(linkId)
|
|
||||||
}
|
|
||||||
return linkIds.size > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves an ordered array of all reroutes from the node output.
|
|
||||||
* @param visited Internal. A set of reroutes that this function has already visited whilst recursing up the chain.
|
|
||||||
* @returns An ordered array of all reroutes from the node output to this reroute, inclusive.
|
|
||||||
* `null` if an infinite loop is detected.
|
|
||||||
* `undefined` if the reroute chain or {@link LinkNetwork} are invalid.
|
|
||||||
*/
|
|
||||||
getReroutes(visited = new Set<Reroute>()): Reroute[] | null | undefined {
|
|
||||||
// No parentId - last in the chain
|
|
||||||
if (this.#parentId === undefined) return [this]
|
|
||||||
// Invalid chain - looped
|
|
||||||
if (visited.has(this)) return null
|
|
||||||
visited.add(this)
|
|
||||||
|
|
||||||
const parent = this.#network.deref()?.reroutes.get(this.#parentId)
|
|
||||||
// Invalid parent (or network) - drop silently to recover
|
|
||||||
if (!parent) {
|
|
||||||
this.#parentId = undefined
|
|
||||||
return [this]
|
|
||||||
}
|
|
||||||
|
|
||||||
const reroutes = parent.getReroutes(visited)
|
|
||||||
reroutes?.push(this)
|
|
||||||
return reroutes
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Internal. Called by {@link LLink.findNextReroute}. Not intended for use by itself.
|
|
||||||
* @param withParentId The rerouteId to look for
|
|
||||||
* @param visited A set of reroutes that have already been visited
|
|
||||||
* @returns The reroute that was found, `undefined` if no reroute was found, or `null` if an infinite loop was detected.
|
|
||||||
*/
|
|
||||||
findNextReroute(withParentId: RerouteId, visited = new Set<Reroute>()): Reroute | null | undefined {
|
|
||||||
if (this.#parentId === withParentId) return this
|
|
||||||
if (visited.has(this)) return null
|
|
||||||
visited.add(this)
|
|
||||||
|
|
||||||
return this.#network.deref()
|
|
||||||
?.reroutes.get(this.#parentId)
|
|
||||||
?.findNextReroute(withParentId, visited)
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritdoc */
|
|
||||||
move(deltaX: number, deltaY: number) {
|
|
||||||
this.#pos[0] += deltaX
|
|
||||||
this.#pos[1] += deltaY
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritdoc */
|
|
||||||
snapToGrid(snapTo: number): boolean {
|
|
||||||
if (!snapTo) return false
|
|
||||||
|
|
||||||
const { pos } = this
|
|
||||||
pos[0] = snapTo * Math.round(pos[0] / snapTo)
|
|
||||||
pos[1] = snapTo * Math.round(pos[1] / snapTo)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
calculateAngle(lastRenderTime: number, network: LinkNetwork, linkStart: Point): void {
|
|
||||||
// Ensure we run once per render
|
|
||||||
if (!(lastRenderTime > this.#lastRenderTime)) return
|
|
||||||
this.#lastRenderTime = lastRenderTime
|
|
||||||
|
|
||||||
const { links } = network
|
|
||||||
const { linkIds, id } = this
|
|
||||||
const angles: number[] = []
|
|
||||||
let sum = 0
|
|
||||||
for (const linkId of linkIds) {
|
|
||||||
const link = links.get(linkId)
|
|
||||||
// Remove the linkId or just ignore?
|
|
||||||
if (!link) continue
|
|
||||||
|
|
||||||
const pos = LLink.findNextReroute(network, link, id)?.pos ??
|
|
||||||
network.getNodeById(link.target_id)
|
|
||||||
?.getConnectionPos(true, link.target_slot, this.#buffer)
|
|
||||||
if (!pos) continue
|
|
||||||
|
|
||||||
// TODO: Store points/angles, check if changed, skip calcs.
|
|
||||||
const angle = Math.atan2(pos[1] - this.#pos[1], pos[0] - this.#pos[0])
|
|
||||||
angles.push(angle)
|
|
||||||
sum += angle
|
|
||||||
}
|
|
||||||
if (!angles.length) return
|
|
||||||
|
|
||||||
sum /= angles.length
|
|
||||||
|
|
||||||
const originToReroute = Math.atan2(this.#pos[1] - linkStart[1], this.#pos[0] - linkStart[0])
|
|
||||||
let diff = (originToReroute - sum) * 0.5
|
|
||||||
if (Math.abs(diff) > Math.PI * 0.5) diff += Math.PI
|
|
||||||
const dist = Math.min(80, distance(linkStart, this.#pos) * 0.25)
|
|
||||||
|
|
||||||
// Store results
|
|
||||||
const originDiff = originToReroute - diff
|
|
||||||
const cos = Math.cos(originDiff)
|
|
||||||
const sin = Math.sin(originDiff)
|
|
||||||
|
|
||||||
this.otherAngle = originDiff
|
|
||||||
this.cos = cos
|
|
||||||
this.sin = sin
|
|
||||||
this.controlPoint[0] = dist * -cos
|
|
||||||
this.controlPoint[1] = dist * -sin
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renders the reroute on the canvas.
|
|
||||||
* @param ctx Canvas context to draw on
|
|
||||||
* @param colour Reroute colour (typically link colour)
|
|
||||||
*
|
|
||||||
* @remarks Leaves {@link ctx}.fillStyle, strokeStyle, and lineWidth dirty (perf.).
|
|
||||||
*/
|
|
||||||
draw(ctx: CanvasRenderingContext2D): void {
|
|
||||||
const { pos } = this
|
|
||||||
ctx.fillStyle = this._colour
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.arc(pos[0], pos[1], Reroute.radius, 0, 2 * Math.PI)
|
|
||||||
ctx.fill()
|
|
||||||
|
|
||||||
ctx.lineWidth = 1
|
|
||||||
ctx.strokeStyle = "rgb(0,0,0,0.5)"
|
|
||||||
ctx.stroke()
|
|
||||||
|
|
||||||
ctx.fillStyle = "#ffffff55"
|
|
||||||
ctx.strokeStyle = "rgb(0,0,0,0.3)"
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.arc(pos[0], pos[1], 8, 0, 2 * Math.PI)
|
|
||||||
ctx.fill()
|
|
||||||
ctx.stroke()
|
|
||||||
|
|
||||||
if (this.selected) {
|
|
||||||
ctx.strokeStyle = "#fff"
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.arc(pos[0], pos[1], 12, 0, 2 * Math.PI)
|
|
||||||
ctx.stroke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @inheritdoc */
|
|
||||||
asSerialisable(): SerialisableReroute {
|
|
||||||
return {
|
|
||||||
id: this.id,
|
|
||||||
parentId: this.parentId,
|
|
||||||
pos: [this.pos[0], this.pos[1]],
|
|
||||||
linkIds: [...this.linkIds]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
98
src/draw.ts
98
src/draw.ts
@@ -1,4 +1,4 @@
|
|||||||
import type { Vector2 } from "./litegraph";
|
import type { Vector2 } from "./litegraph"
|
||||||
import type { INodeSlot } from "./interfaces"
|
import type { INodeSlot } from "./interfaces"
|
||||||
import { LinkDirection, RenderShape } from "./types/globalEnums"
|
import { LinkDirection, RenderShape } from "./types/globalEnums"
|
||||||
|
|
||||||
@@ -42,44 +42,44 @@ export function drawSlot(
|
|||||||
do_stroke = false,
|
do_stroke = false,
|
||||||
highlight = false,
|
highlight = false,
|
||||||
}: {
|
}: {
|
||||||
label_color?: string;
|
label_color?: string
|
||||||
label_position?: LabelPosition;
|
label_position?: LabelPosition
|
||||||
horizontal?: boolean;
|
horizontal?: boolean
|
||||||
low_quality?: boolean;
|
low_quality?: boolean
|
||||||
render_text?: boolean;
|
render_text?: boolean
|
||||||
do_stroke?: boolean;
|
do_stroke?: boolean
|
||||||
highlight?: boolean;
|
highlight?: boolean
|
||||||
} = {}
|
} = {},
|
||||||
) {
|
) {
|
||||||
// Save the current fillStyle and strokeStyle
|
// Save the current fillStyle and strokeStyle
|
||||||
const originalFillStyle = ctx.fillStyle;
|
const originalFillStyle = ctx.fillStyle
|
||||||
const originalStrokeStyle = ctx.strokeStyle;
|
const originalStrokeStyle = ctx.strokeStyle
|
||||||
const originalLineWidth = ctx.lineWidth;
|
const originalLineWidth = ctx.lineWidth
|
||||||
|
|
||||||
const slot_type = slot.type as SlotType;
|
const slot_type = slot.type as SlotType
|
||||||
const slot_shape = (
|
const slot_shape = (
|
||||||
slot_type === SlotType.Array ? SlotShape.Grid : slot.shape
|
slot_type === SlotType.Array ? SlotShape.Grid : slot.shape
|
||||||
) as SlotShape;
|
) as SlotShape
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath()
|
||||||
let doStroke = do_stroke;
|
let doStroke = do_stroke
|
||||||
let doFill = true;
|
let doFill = true
|
||||||
|
|
||||||
if (slot_type === SlotType.Event || slot_shape === SlotShape.Box) {
|
if (slot_type === SlotType.Event || slot_shape === SlotShape.Box) {
|
||||||
if (horizontal) {
|
if (horizontal) {
|
||||||
ctx.rect(pos[0] - 5 + 0.5, pos[1] - 8 + 0.5, 10, 14);
|
ctx.rect(pos[0] - 5 + 0.5, pos[1] - 8 + 0.5, 10, 14)
|
||||||
} else {
|
} else {
|
||||||
ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10);
|
ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10)
|
||||||
}
|
}
|
||||||
} else if (slot_shape === SlotShape.Arrow) {
|
} else if (slot_shape === SlotShape.Arrow) {
|
||||||
ctx.moveTo(pos[0] + 8, pos[1] + 0.5);
|
ctx.moveTo(pos[0] + 8, pos[1] + 0.5)
|
||||||
ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5);
|
ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5)
|
||||||
ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5);
|
ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5)
|
||||||
ctx.closePath();
|
ctx.closePath()
|
||||||
} else if (slot_shape === SlotShape.Grid) {
|
} else if (slot_shape === SlotShape.Grid) {
|
||||||
const gridSize = 3;
|
const gridSize = 3
|
||||||
const cellSize = 2;
|
const cellSize = 2
|
||||||
const spacing = 3;
|
const spacing = 3
|
||||||
|
|
||||||
for (let x = 0; x < gridSize; x++) {
|
for (let x = 0; x < gridSize; x++) {
|
||||||
for (let y = 0; y < gridSize; y++) {
|
for (let y = 0; y < gridSize; y++) {
|
||||||
@@ -87,59 +87,59 @@ export function drawSlot(
|
|||||||
pos[0] - 4 + x * spacing,
|
pos[0] - 4 + x * spacing,
|
||||||
pos[1] - 4 + y * spacing,
|
pos[1] - 4 + y * spacing,
|
||||||
cellSize,
|
cellSize,
|
||||||
cellSize
|
cellSize,
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
doStroke = false;
|
doStroke = false
|
||||||
} else {
|
} else {
|
||||||
// Default rendering for circle, hollow circle.
|
// Default rendering for circle, hollow circle.
|
||||||
if (low_quality) {
|
if (low_quality) {
|
||||||
ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8);
|
ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8)
|
||||||
} else {
|
} else {
|
||||||
let radius: number;
|
let radius: number
|
||||||
if (slot_shape === SlotShape.HollowCircle) {
|
if (slot_shape === SlotShape.HollowCircle) {
|
||||||
doFill = false;
|
doFill = false
|
||||||
doStroke = true;
|
doStroke = true
|
||||||
ctx.lineWidth = 3;
|
ctx.lineWidth = 3
|
||||||
ctx.strokeStyle = ctx.fillStyle;
|
ctx.strokeStyle = ctx.fillStyle
|
||||||
radius = highlight ? 4 : 3;
|
radius = highlight ? 4 : 3
|
||||||
} else {
|
} else {
|
||||||
// Normal circle
|
// Normal circle
|
||||||
radius = highlight ? 5 : 4;
|
radius = highlight ? 5 : 4
|
||||||
}
|
}
|
||||||
ctx.arc(pos[0], pos[1], radius, 0, Math.PI * 2);
|
ctx.arc(pos[0], pos[1], radius, 0, Math.PI * 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (doFill) ctx.fill();
|
if (doFill) ctx.fill()
|
||||||
if (!low_quality && doStroke) ctx.stroke();
|
if (!low_quality && doStroke) ctx.stroke()
|
||||||
|
|
||||||
// render slot label
|
// render slot label
|
||||||
if (render_text) {
|
if (render_text) {
|
||||||
const text = slot.label != null ? slot.label : slot.name;
|
const text = slot.label != null ? slot.label : slot.name
|
||||||
if (text) {
|
if (text) {
|
||||||
// TODO: Finish impl. Highlight text on mouseover unless we're connecting links.
|
// TODO: Finish impl. Highlight text on mouseover unless we're connecting links.
|
||||||
ctx.fillStyle = label_color;
|
ctx.fillStyle = label_color
|
||||||
|
|
||||||
if (label_position === LabelPosition.Right) {
|
if (label_position === LabelPosition.Right) {
|
||||||
if (horizontal || slot.dir == LinkDirection.UP) {
|
if (horizontal || slot.dir == LinkDirection.UP) {
|
||||||
ctx.fillText(text, pos[0], pos[1] - 10);
|
ctx.fillText(text, pos[0], pos[1] - 10)
|
||||||
} else {
|
} else {
|
||||||
ctx.fillText(text, pos[0] + 10, pos[1] + 5);
|
ctx.fillText(text, pos[0] + 10, pos[1] + 5)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (horizontal || slot.dir == LinkDirection.DOWN) {
|
if (horizontal || slot.dir == LinkDirection.DOWN) {
|
||||||
ctx.fillText(text, pos[0], pos[1] - 8);
|
ctx.fillText(text, pos[0], pos[1] - 8)
|
||||||
} else {
|
} else {
|
||||||
ctx.fillText(text, pos[0] - 10, pos[1] + 5);
|
ctx.fillText(text, pos[0] - 10, pos[1] + 5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore the original fillStyle and strokeStyle
|
// Restore the original fillStyle and strokeStyle
|
||||||
ctx.fillStyle = originalFillStyle;
|
ctx.fillStyle = originalFillStyle
|
||||||
ctx.strokeStyle = originalStrokeStyle;
|
ctx.strokeStyle = originalStrokeStyle
|
||||||
ctx.lineWidth = originalLineWidth;
|
ctx.lineWidth = originalLineWidth
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,15 +8,15 @@ export type Dictionary<T> = { [key: string]: T }
|
|||||||
|
|
||||||
/** Allows all properties to be null. The same as `Partial<T>`, but adds null instead of undefined. */
|
/** Allows all properties to be null. The same as `Partial<T>`, but adds null instead of undefined. */
|
||||||
export type NullableProperties<T> = {
|
export type NullableProperties<T> = {
|
||||||
[P in keyof T]: T[P] | null
|
[P in keyof T]: T[P] | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CanvasColour = string | CanvasGradient | CanvasPattern
|
export type CanvasColour = string | CanvasGradient | CanvasPattern
|
||||||
|
|
||||||
/** An object containing a set of child objects */
|
/** An object containing a set of child objects */
|
||||||
export interface Parent<TChild> {
|
export interface Parent<TChild> {
|
||||||
/** All objects owned by the parent object. */
|
/** All objects owned by the parent object. */
|
||||||
readonly children?: ReadonlySet<TChild>
|
readonly children?: ReadonlySet<TChild>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,42 +25,42 @@ export interface Parent<TChild> {
|
|||||||
* May contain other {@link Positionable} objects.
|
* May contain other {@link Positionable} objects.
|
||||||
*/
|
*/
|
||||||
export interface Positionable extends Parent<Positionable> {
|
export interface Positionable extends Parent<Positionable> {
|
||||||
id: NodeId | RerouteId | number
|
id: NodeId | RerouteId | number
|
||||||
/** Position in graph coordinates. Default: 0,0 */
|
/** Position in graph coordinates. Default: 0,0 */
|
||||||
pos: Point
|
pos: Point
|
||||||
/** true if this object is part of the selection, otherwise false. */
|
/** true if this object is part of the selection, otherwise false. */
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
|
|
||||||
/** See {@link IPinnable.pinned} */
|
/** See {@link IPinnable.pinned} */
|
||||||
readonly pinned?: boolean
|
readonly pinned?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds a delta to the current position.
|
* Adds a delta to the current position.
|
||||||
* @param deltaX X value to add to current position
|
* @param deltaX X value to add to current position
|
||||||
* @param deltaY Y value to add to current position
|
* @param deltaY Y value to add to current position
|
||||||
* @param skipChildren If true, any child objects like group contents will not be moved
|
* @param skipChildren If true, any child objects like group contents will not be moved
|
||||||
*/
|
*/
|
||||||
move(deltaX: number, deltaY: number, skipChildren?: boolean): void
|
move(deltaX: number, deltaY: number, skipChildren?: boolean): void
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Snaps this item to a grid.
|
* Snaps this item to a grid.
|
||||||
*
|
*
|
||||||
* Position values are rounded to the nearest multiple of {@link snapTo}.
|
* Position values are rounded to the nearest multiple of {@link snapTo}.
|
||||||
* @param snapTo The size of the grid to align to
|
* @param snapTo The size of the grid to align to
|
||||||
* @returns `true` if it moved, or `false` if the snap was rejected (e.g. `pinned`)
|
* @returns `true` if it moved, or `false` if the snap was rejected (e.g. `pinned`)
|
||||||
*/
|
*/
|
||||||
snapToGrid(snapTo: number): boolean
|
snapToGrid(snapTo: number): boolean
|
||||||
|
|
||||||
/**
|
|
||||||
* Cached position & size as `x, y, width, height`.
|
|
||||||
* @readonly See {@link move}
|
|
||||||
*/
|
|
||||||
readonly boundingRect: ReadOnlyRect
|
|
||||||
|
|
||||||
/** Called whenever the item is selected */
|
/**
|
||||||
onSelected?(): void
|
* Cached position & size as `x, y, width, height`.
|
||||||
/** Called whenever the item is deselected */
|
* @readonly See {@link move}
|
||||||
onDeselected?(): void
|
*/
|
||||||
|
readonly boundingRect: ReadOnlyRect
|
||||||
|
|
||||||
|
/** Called whenever the item is selected */
|
||||||
|
onSelected?(): void
|
||||||
|
/** Called whenever the item is deselected */
|
||||||
|
onDeselected?(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,52 +69,55 @@ export interface Positionable extends Parent<Positionable> {
|
|||||||
* Prevents the object being accidentally moved or resized by mouse interaction.
|
* Prevents the object being accidentally moved or resized by mouse interaction.
|
||||||
*/
|
*/
|
||||||
export interface IPinnable {
|
export interface IPinnable {
|
||||||
pinned: boolean
|
pinned: boolean
|
||||||
pin(value?: boolean): void
|
pin(value?: boolean): void
|
||||||
unpin(): void
|
unpin(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contains a list of links, reroutes, and nodes.
|
* Contains a list of links, reroutes, and nodes.
|
||||||
*/
|
*/
|
||||||
export interface LinkNetwork {
|
export interface LinkNetwork {
|
||||||
links: Map<LinkId, LLink>
|
links: Map<LinkId, LLink>
|
||||||
reroutes: Map<RerouteId, Reroute>
|
reroutes: Map<RerouteId, Reroute>
|
||||||
getNodeById(id: NodeId): LGraphNode | null
|
getNodeById(id: NodeId): LGraphNode | null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Contains a cached 2D canvas path and a centre point, with an optional forward angle. */
|
/** Contains a cached 2D canvas path and a centre point, with an optional forward angle. */
|
||||||
export interface LinkSegment {
|
export interface LinkSegment {
|
||||||
/** Link / reroute ID */
|
/** Link / reroute ID */
|
||||||
readonly id: LinkId | RerouteId
|
readonly id: LinkId | RerouteId
|
||||||
/** The {@link id} of the reroute that this segment starts from (output side), otherwise `undefined`. */
|
/** The {@link id} of the reroute that this segment starts from (output side), otherwise `undefined`. */
|
||||||
readonly parentId?: RerouteId
|
readonly parentId?: RerouteId
|
||||||
|
|
||||||
/** The last canvas 2D path that was used to render this segment */
|
/** The last canvas 2D path that was used to render this segment */
|
||||||
path?: Path2D
|
path?: Path2D
|
||||||
/** Centre point of the {@link path}. Calculated during render only - can be inaccurate */
|
/** Centre point of the {@link path}. Calculated during render only - can be inaccurate */
|
||||||
readonly _pos: Float32Array
|
readonly _pos: Float32Array
|
||||||
/** Y-forward along the {@link path} from its centre point, in radians. `undefined` if using circles for link centres. Calculated during render only - can be inaccurate. */
|
/** Y-forward along the {@link path} from its centre point, in radians.
|
||||||
_centreAngle?: number
|
* `undefined` if using circles for link centres.
|
||||||
|
* Calculated during render only - can be inaccurate.
|
||||||
|
*/
|
||||||
|
_centreAngle?: number
|
||||||
|
|
||||||
/** Output node ID */
|
/** Output node ID */
|
||||||
readonly origin_id: NodeId
|
readonly origin_id: NodeId
|
||||||
/** Output slot index */
|
/** Output slot index */
|
||||||
readonly origin_slot: number
|
readonly origin_slot: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IInputOrOutput {
|
export interface IInputOrOutput {
|
||||||
// If an input, this will be defined
|
// If an input, this will be defined
|
||||||
input?: INodeInputSlot
|
input?: INodeInputSlot
|
||||||
// If an output, this will be defined
|
// If an output, this will be defined
|
||||||
output?: INodeOutputSlot
|
output?: INodeOutputSlot
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFoundSlot extends IInputOrOutput {
|
export interface IFoundSlot extends IInputOrOutput {
|
||||||
// Slot index
|
// Slot index
|
||||||
slot: number
|
slot: number
|
||||||
// Centre point of the rendered slot connection
|
// Centre point of the rendered slot connection
|
||||||
link_pos: Point
|
link_pos: Point
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A point represented as `[x, y]` co-ordinates */
|
/** A point represented as `[x, y]` co-ordinates */
|
||||||
@@ -133,13 +136,31 @@ export type Rect = ArRect | Float32Array | Float64Array
|
|||||||
export type Rect32 = Float32Array
|
export type Rect32 = Float32Array
|
||||||
|
|
||||||
/** A point represented as `[x, y]` co-ordinates that will not be modified */
|
/** A point represented as `[x, y]` co-ordinates that will not be modified */
|
||||||
export type ReadOnlyPoint = readonly [x: number, y: number] | ReadOnlyTypedArray<Float32Array> | ReadOnlyTypedArray<Float64Array>
|
export type ReadOnlyPoint =
|
||||||
/** A rectangle starting at top-left coordinates `[x, y, width, height]` that will not be modified */
|
| readonly [x: number, y: number]
|
||||||
export type ReadOnlyRect = readonly [x: number, y: number, width: number, height: number] | ReadOnlyTypedArray<Float32Array> | ReadOnlyTypedArray<Float64Array>
|
| ReadOnlyTypedArray<Float32Array>
|
||||||
|
| ReadOnlyTypedArray<Float64Array>
|
||||||
|
|
||||||
|
/** A rectangle starting at top-left coordinates `[x, y, width, height]` that will not be modified */
|
||||||
|
export type ReadOnlyRect =
|
||||||
|
| readonly [x: number, y: number, width: number, height: number]
|
||||||
|
| ReadOnlyTypedArray<Float32Array>
|
||||||
|
| ReadOnlyTypedArray<Float64Array>
|
||||||
|
|
||||||
|
type TypedArrays =
|
||||||
|
| Int8Array
|
||||||
|
| Uint8Array
|
||||||
|
| Uint8ClampedArray
|
||||||
|
| Int16Array
|
||||||
|
| Uint16Array
|
||||||
|
| Int32Array
|
||||||
|
| Uint32Array
|
||||||
|
| Float32Array
|
||||||
|
| Float64Array
|
||||||
|
|
||||||
type TypedArrays = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array
|
|
||||||
type TypedBigIntArrays = BigInt64Array | BigUint64Array
|
type TypedBigIntArrays = BigInt64Array | BigUint64Array
|
||||||
type ReadOnlyTypedArray<T extends TypedArrays | TypedBigIntArrays> = Omit<T, "fill" | "copyWithin" | "reverse" | "set" | "sort" | "subarray">
|
type ReadOnlyTypedArray<T extends TypedArrays | TypedBigIntArrays> =
|
||||||
|
Omit<T, "fill" | "copyWithin" | "reverse" | "set" | "sort" | "subarray">
|
||||||
|
|
||||||
/** Union of property names that are of type Match */
|
/** Union of property names that are of type Match */
|
||||||
export type KeysOfType<T, Match> = { [P in keyof T]: T[P] extends Match ? P : never }[keyof T]
|
export type KeysOfType<T, Match> = { [P in keyof T]: T[P] extends Match ? P : never }[keyof T]
|
||||||
@@ -151,99 +172,105 @@ export type PickByType<T, Match> = { [P in keyof T]: Extract<T[P], Match> }
|
|||||||
export type MethodNames<T> = KeysOfType<T, ((...args: any) => any) | undefined>
|
export type MethodNames<T> = KeysOfType<T, ((...args: any) => any) | undefined>
|
||||||
|
|
||||||
export interface IBoundaryNodes {
|
export interface IBoundaryNodes {
|
||||||
top: LGraphNode
|
top: LGraphNode
|
||||||
right: LGraphNode
|
right: LGraphNode
|
||||||
bottom: LGraphNode
|
bottom: LGraphNode
|
||||||
left: LGraphNode
|
left: LGraphNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Direction = "top" | "bottom" | "left" | "right"
|
export type Direction = "top" | "bottom" | "left" | "right"
|
||||||
|
|
||||||
export interface IOptionalSlotData<TSlot extends INodeInputSlot | INodeOutputSlot> {
|
export interface IOptionalSlotData<TSlot extends INodeInputSlot | INodeOutputSlot> {
|
||||||
content: string
|
content: string
|
||||||
value: TSlot
|
value: TSlot
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ISlotType = number | string
|
export type ISlotType = number | string
|
||||||
|
|
||||||
export interface INodeSlot {
|
export interface INodeSlot {
|
||||||
name: string
|
name: string
|
||||||
type: ISlotType
|
type: ISlotType
|
||||||
dir?: LinkDirection
|
dir?: LinkDirection
|
||||||
removable?: boolean
|
removable?: boolean
|
||||||
shape?: RenderShape
|
shape?: RenderShape
|
||||||
not_subgraph_input?: boolean
|
not_subgraph_input?: boolean
|
||||||
color_off?: CanvasColour
|
color_off?: CanvasColour
|
||||||
color_on?: CanvasColour
|
color_on?: CanvasColour
|
||||||
label?: string
|
label?: string
|
||||||
locked?: boolean
|
locked?: boolean
|
||||||
nameLocked?: boolean
|
nameLocked?: boolean
|
||||||
pos?: Point
|
pos?: Point
|
||||||
widget?: unknown
|
widget?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INodeFlags {
|
export interface INodeFlags {
|
||||||
skip_repeated_outputs?: boolean
|
skip_repeated_outputs?: boolean
|
||||||
allow_interaction?: boolean
|
allow_interaction?: boolean
|
||||||
pinned?: boolean
|
pinned?: boolean
|
||||||
collapsed?: boolean
|
collapsed?: boolean
|
||||||
/** Configuration setting for {@link LGraphNode.connectInputToOutput} */
|
/** Configuration setting for {@link LGraphNode.connectInputToOutput} */
|
||||||
keepAllLinksOnBypass?: boolean
|
keepAllLinksOnBypass?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INodeInputSlot extends INodeSlot {
|
export interface INodeInputSlot extends INodeSlot {
|
||||||
link: LinkId | null
|
link: LinkId | null
|
||||||
not_subgraph_input?: boolean
|
not_subgraph_input?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INodeOutputSlot extends INodeSlot {
|
export interface INodeOutputSlot extends INodeSlot {
|
||||||
links: LinkId[] | null
|
links: LinkId[] | null
|
||||||
_data?: unknown
|
_data?: unknown
|
||||||
slot_index?: number
|
slot_index?: number
|
||||||
not_subgraph_output?: boolean
|
not_subgraph_output?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Links */
|
/** Links */
|
||||||
export interface ConnectingLink extends IInputOrOutput {
|
export interface ConnectingLink extends IInputOrOutput {
|
||||||
node: LGraphNode
|
node: LGraphNode
|
||||||
slot: number
|
slot: number
|
||||||
pos: Point
|
pos: Point
|
||||||
direction?: LinkDirection
|
direction?: LinkDirection
|
||||||
afterRerouteId?: RerouteId
|
afterRerouteId?: RerouteId
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IContextMenuBase {
|
interface IContextMenuBase {
|
||||||
title?: string
|
title?: string
|
||||||
className?: string
|
className?: string
|
||||||
callback?(value?: unknown, options?: unknown, event?: MouseEvent, previous_menu?: ContextMenu, node?: LGraphNode): void | boolean
|
callback?(
|
||||||
|
value?: unknown,
|
||||||
|
options?: unknown,
|
||||||
|
event?: MouseEvent,
|
||||||
|
previous_menu?: ContextMenu,
|
||||||
|
node?: LGraphNode,
|
||||||
|
): void | boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** ContextMenu */
|
/** ContextMenu */
|
||||||
export interface IContextMenuOptions extends IContextMenuBase {
|
export interface IContextMenuOptions extends IContextMenuBase {
|
||||||
ignore_item_callbacks?: boolean
|
ignore_item_callbacks?: boolean
|
||||||
parentMenu?: ContextMenu
|
parentMenu?: ContextMenu
|
||||||
event?: MouseEvent
|
event?: MouseEvent
|
||||||
extra?: unknown
|
extra?: unknown
|
||||||
scroll_speed?: number
|
scroll_speed?: number
|
||||||
left?: number
|
left?: number
|
||||||
top?: number
|
top?: number
|
||||||
scale?: number
|
scale?: number
|
||||||
node?: LGraphNode
|
node?: LGraphNode
|
||||||
autoopen?: boolean
|
autoopen?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IContextMenuValue extends IContextMenuBase {
|
export interface IContextMenuValue extends IContextMenuBase {
|
||||||
value?: string
|
value?: string
|
||||||
content: string
|
content: string
|
||||||
has_submenu?: boolean
|
has_submenu?: boolean
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
submenu?: IContextMenuSubmenu
|
submenu?: IContextMenuSubmenu
|
||||||
property?: string
|
property?: string
|
||||||
type?: string
|
type?: string
|
||||||
slot?: IFoundSlot
|
slot?: IFoundSlot
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IContextMenuSubmenu extends IContextMenuOptions {
|
export interface IContextMenuSubmenu extends IContextMenuOptions {
|
||||||
options: ConstructorParameters<typeof ContextMenu>[0]
|
options: ConstructorParameters<typeof ContextMenu>[0]
|
||||||
}
|
}
|
||||||
|
|||||||
152
src/litegraph.ts
152
src/litegraph.ts
@@ -1,5 +1,25 @@
|
|||||||
import type { Point, ConnectingLink } from "./interfaces"
|
import type { Point, ConnectingLink } from "./interfaces"
|
||||||
import type { INodeSlot, INodeInputSlot, INodeOutputSlot, CanvasColour, Direction, IBoundaryNodes, IContextMenuOptions, IContextMenuValue, IFoundSlot, IInputOrOutput, INodeFlags, IOptionalSlotData, ISlotType, KeysOfType, MethodNames, PickByType, Rect, Rect32, Size } from "./interfaces"
|
import type {
|
||||||
|
INodeSlot,
|
||||||
|
INodeInputSlot,
|
||||||
|
INodeOutputSlot,
|
||||||
|
CanvasColour,
|
||||||
|
Direction,
|
||||||
|
IBoundaryNodes,
|
||||||
|
IContextMenuOptions,
|
||||||
|
IContextMenuValue,
|
||||||
|
IFoundSlot,
|
||||||
|
IInputOrOutput,
|
||||||
|
INodeFlags,
|
||||||
|
IOptionalSlotData,
|
||||||
|
ISlotType,
|
||||||
|
KeysOfType,
|
||||||
|
MethodNames,
|
||||||
|
PickByType,
|
||||||
|
Rect,
|
||||||
|
Rect32,
|
||||||
|
Size,
|
||||||
|
} from "./interfaces"
|
||||||
import type { SlotShape, LabelPosition, SlotDirection, SlotType } from "./draw"
|
import type { SlotShape, LabelPosition, SlotDirection, SlotType } from "./draw"
|
||||||
import type { IWidget } from "./types/widgets"
|
import type { IWidget } from "./types/widgets"
|
||||||
import type { RenderShape, TitleMode } from "./types/globalEnums"
|
import type { RenderShape, TitleMode } from "./types/globalEnums"
|
||||||
@@ -18,19 +38,53 @@ import { CurveEditor } from "./CurveEditor"
|
|||||||
import { LGraphBadge, BadgePosition } from "./LGraphBadge"
|
import { LGraphBadge, BadgePosition } from "./LGraphBadge"
|
||||||
|
|
||||||
export const LiteGraph = new LiteGraphGlobal()
|
export const LiteGraph = new LiteGraphGlobal()
|
||||||
export { LGraph, LGraphCanvas, LGraphCanvasState, DragAndScale, LGraphNode, LGraphGroup, LLink, ContextMenu, CurveEditor }
|
export {
|
||||||
export { INodeSlot, INodeInputSlot, INodeOutputSlot, ConnectingLink, CanvasColour, Direction, IBoundaryNodes, IContextMenuOptions, IContextMenuValue, IFoundSlot, IInputOrOutput, INodeFlags, IOptionalSlotData, ISlotType, KeysOfType, MethodNames, PickByType, Rect, Rect32, Size }
|
LGraph,
|
||||||
|
LGraphCanvas,
|
||||||
|
LGraphCanvasState,
|
||||||
|
DragAndScale,
|
||||||
|
LGraphNode,
|
||||||
|
LGraphGroup,
|
||||||
|
LLink,
|
||||||
|
ContextMenu,
|
||||||
|
CurveEditor,
|
||||||
|
}
|
||||||
|
export {
|
||||||
|
INodeSlot,
|
||||||
|
INodeInputSlot,
|
||||||
|
INodeOutputSlot,
|
||||||
|
ConnectingLink,
|
||||||
|
CanvasColour,
|
||||||
|
Direction,
|
||||||
|
IBoundaryNodes,
|
||||||
|
IContextMenuOptions,
|
||||||
|
IContextMenuValue,
|
||||||
|
IFoundSlot,
|
||||||
|
IInputOrOutput,
|
||||||
|
INodeFlags,
|
||||||
|
IOptionalSlotData,
|
||||||
|
ISlotType,
|
||||||
|
KeysOfType,
|
||||||
|
MethodNames,
|
||||||
|
PickByType,
|
||||||
|
Rect,
|
||||||
|
Rect32,
|
||||||
|
Size,
|
||||||
|
}
|
||||||
export { IWidget }
|
export { IWidget }
|
||||||
export { LGraphBadge, BadgePosition }
|
export { LGraphBadge, BadgePosition }
|
||||||
export { SlotShape, LabelPosition, SlotDirection, SlotType }
|
export { SlotShape, LabelPosition, SlotDirection, SlotType }
|
||||||
export { EaseFunction, LinkMarkerShape } from "./types/globalEnums"
|
export { EaseFunction, LinkMarkerShape } from "./types/globalEnums"
|
||||||
export type { SerialisableGraph, SerialisableLLink } from "./types/serialisation"
|
export type {
|
||||||
|
SerialisableGraph,
|
||||||
|
SerialisableLLink,
|
||||||
|
} from "./types/serialisation"
|
||||||
export { CanvasPointer } from "./CanvasPointer"
|
export { CanvasPointer } from "./CanvasPointer"
|
||||||
export { createBounds } from "./measure"
|
export { createBounds } from "./measure"
|
||||||
|
|
||||||
export function clamp(v: number, a: number, b: number): number {
|
export function clamp(v: number, a: number, b: number): number {
|
||||||
return a > v ? a : b < v ? b : v
|
return a > v ? a : b < v ? b : v
|
||||||
};
|
}
|
||||||
|
|
||||||
// Load legacy polyfills
|
// Load legacy polyfills
|
||||||
loadPolyfills()
|
loadPolyfills()
|
||||||
@@ -44,68 +98,72 @@ export type Vector2 = Point
|
|||||||
export type Vector4 = [number, number, number, number]
|
export type Vector4 = [number, number, number, number]
|
||||||
|
|
||||||
export interface IContextMenuItem {
|
export interface IContextMenuItem {
|
||||||
content: string
|
content: string
|
||||||
callback?: ContextMenuEventListener
|
callback?: ContextMenuEventListener
|
||||||
/** Used as innerHTML for extra child element */
|
/** Used as innerHTML for extra child element */
|
||||||
title?: string
|
title?: string
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
has_submenu?: boolean
|
has_submenu?: boolean
|
||||||
submenu?: {
|
submenu?: {
|
||||||
options: IContextMenuItem[]
|
options: IContextMenuItem[]
|
||||||
} & IContextMenuOptions
|
} & IContextMenuOptions
|
||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ContextMenuEventListener = (
|
export type ContextMenuEventListener = (
|
||||||
value: IContextMenuItem,
|
value: IContextMenuItem,
|
||||||
options: IContextMenuOptions,
|
options: IContextMenuOptions,
|
||||||
event: MouseEvent,
|
event: MouseEvent,
|
||||||
parentMenu: ContextMenu | undefined,
|
parentMenu: ContextMenu | undefined,
|
||||||
node: LGraphNode
|
node: LGraphNode,
|
||||||
) => boolean | void
|
) => boolean | void
|
||||||
|
|
||||||
export interface LinkReleaseContext {
|
export interface LinkReleaseContext {
|
||||||
node_to?: LGraphNode
|
node_to?: LGraphNode
|
||||||
node_from?: LGraphNode
|
node_from?: LGraphNode
|
||||||
slot_from: INodeSlot
|
slot_from: INodeSlot
|
||||||
type_filter_in?: string
|
type_filter_in?: string
|
||||||
type_filter_out?: string
|
type_filter_out?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LinkReleaseContextExtended {
|
export interface LinkReleaseContextExtended {
|
||||||
links: ConnectingLink[]
|
links: ConnectingLink[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated Confirm no downstream consumers, then remove. */
|
/** @deprecated Confirm no downstream consumers, then remove. */
|
||||||
export type LiteGraphCanvasEventType = "empty-release" | "empty-double-click" | "group-double-click"
|
export type LiteGraphCanvasEventType =
|
||||||
|
| "empty-release"
|
||||||
|
| "empty-double-click"
|
||||||
|
| "group-double-click"
|
||||||
|
|
||||||
export interface LiteGraphCanvasEvent extends CustomEvent<CanvasEventDetail> { }
|
export interface LiteGraphCanvasEvent extends CustomEvent<CanvasEventDetail> {}
|
||||||
|
|
||||||
export interface LiteGraphCanvasGroupEvent extends CustomEvent<{
|
export interface LiteGraphCanvasGroupEvent
|
||||||
|
extends CustomEvent<{
|
||||||
subType: "group-double-click"
|
subType: "group-double-click"
|
||||||
originalEvent: MouseEvent
|
originalEvent: MouseEvent
|
||||||
group: LGraphGroup
|
group: LGraphGroup
|
||||||
}> { }
|
}> {}
|
||||||
|
|
||||||
/** https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#lgraphnode */
|
/** https://github.com/jagenjo/litegraph.js/blob/master/guides/README.md#lgraphnode */
|
||||||
|
|
||||||
export interface LGraphNodeConstructor<T extends LGraphNode = LGraphNode> {
|
export interface LGraphNodeConstructor<T extends LGraphNode = LGraphNode> {
|
||||||
title?: string
|
title?: string
|
||||||
type?: string
|
type?: string
|
||||||
size?: Size
|
size?: Size
|
||||||
min_height?: number
|
min_height?: number
|
||||||
slot_start_y?: number
|
slot_start_y?: number
|
||||||
widgets_info?: any
|
widgets_info?: any
|
||||||
collapsable?: boolean
|
collapsable?: boolean
|
||||||
color?: string
|
color?: string
|
||||||
bgcolor?: string
|
bgcolor?: string
|
||||||
shape?: RenderShape
|
shape?: RenderShape
|
||||||
title_mode?: TitleMode
|
title_mode?: TitleMode
|
||||||
title_color?: string
|
title_color?: string
|
||||||
title_text_color?: string
|
title_text_color?: string
|
||||||
keepAllLinksOnBypass: boolean
|
keepAllLinksOnBypass: boolean
|
||||||
nodeData: any
|
nodeData: any
|
||||||
new(): T
|
new (): T
|
||||||
}
|
}
|
||||||
|
|
||||||
// End backwards compat
|
// End backwards compat
|
||||||
|
|||||||
357
src/measure.ts
357
src/measure.ts
@@ -1,4 +1,10 @@
|
|||||||
import type { Point, Positionable, ReadOnlyPoint, ReadOnlyRect, Rect } from "./interfaces"
|
import type {
|
||||||
|
Point,
|
||||||
|
Positionable,
|
||||||
|
ReadOnlyPoint,
|
||||||
|
ReadOnlyRect,
|
||||||
|
Rect,
|
||||||
|
} from "./interfaces"
|
||||||
import { LinkDirection } from "./types/globalEnums"
|
import { LinkDirection } from "./types/globalEnums"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8,9 +14,9 @@ import { LinkDirection } from "./types/globalEnums"
|
|||||||
* @returns Distance between point {@link a} & {@link b}
|
* @returns Distance between point {@link a} & {@link b}
|
||||||
*/
|
*/
|
||||||
export function distance(a: ReadOnlyPoint, b: ReadOnlyPoint): number {
|
export function distance(a: ReadOnlyPoint, b: ReadOnlyPoint): number {
|
||||||
return Math.sqrt(
|
return Math.sqrt(
|
||||||
(b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1])
|
(b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,12 +29,12 @@ export function distance(a: ReadOnlyPoint, b: ReadOnlyPoint): number {
|
|||||||
* @returns Distance2 (squared) between point [{@link x1}, {@link y1}] & [{@link x2}, {@link y2}]
|
* @returns Distance2 (squared) between point [{@link x1}, {@link y1}] & [{@link x2}, {@link y2}]
|
||||||
*/
|
*/
|
||||||
export function dist2(x1: number, y1: number, x2: number, y2: number): number {
|
export function dist2(x1: number, y1: number, x2: number, y2: number): number {
|
||||||
return ((x2 - x1) * (x2 - x1)) + ((y2 - y1) * (y2 - y1))
|
return ((x2 - x1) * (x2 - x1)) + ((y2 - y1) * (y2 - y1))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether a point is inside a rectangle.
|
* Determines whether a point is inside a rectangle.
|
||||||
*
|
*
|
||||||
* Otherwise identical to {@link isInsideRectangle}, it also returns `true` if `x` equals `left` or `y` equals `top`.
|
* Otherwise identical to {@link isInsideRectangle}, it also returns `true` if `x` equals `left` or `y` equals `top`.
|
||||||
* @param x Point x
|
* @param x Point x
|
||||||
* @param y Point y
|
* @param y Point y
|
||||||
@@ -38,11 +44,18 @@ export function dist2(x1: number, y1: number, x2: number, y2: number): number {
|
|||||||
* @param height Rect height
|
* @param height Rect height
|
||||||
* @returns `true` if the point is inside the rect, otherwise `false`
|
* @returns `true` if the point is inside the rect, otherwise `false`
|
||||||
*/
|
*/
|
||||||
export function isInRectangle(x: number, y: number, left: number, top: number, width: number, height: number): boolean {
|
export function isInRectangle(
|
||||||
return x >= left
|
x: number,
|
||||||
&& x < left + width
|
y: number,
|
||||||
&& y >= top
|
left: number,
|
||||||
&& y < top + height
|
top: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
): boolean {
|
||||||
|
return x >= left &&
|
||||||
|
x < left + width &&
|
||||||
|
y >= top &&
|
||||||
|
y < top + height
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,10 +65,10 @@ export function isInRectangle(x: number, y: number, left: number, top: number, w
|
|||||||
* @returns `true` if the point is inside the rect, otherwise `false`
|
* @returns `true` if the point is inside the rect, otherwise `false`
|
||||||
*/
|
*/
|
||||||
export function isPointInRect(point: ReadOnlyPoint, rect: ReadOnlyRect): boolean {
|
export function isPointInRect(point: ReadOnlyPoint, rect: ReadOnlyRect): boolean {
|
||||||
return point[0] >= rect[0]
|
return point[0] >= rect[0] &&
|
||||||
&& point[0] < rect[0] + rect[2]
|
point[0] < rect[0] + rect[2] &&
|
||||||
&& point[1] >= rect[1]
|
point[1] >= rect[1] &&
|
||||||
&& point[1] < rect[1] + rect[3]
|
point[1] < rect[1] + rect[3]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,20 +79,20 @@ export function isPointInRect(point: ReadOnlyPoint, rect: ReadOnlyRect): boolean
|
|||||||
* @returns `true` if the point is inside the rect, otherwise `false`
|
* @returns `true` if the point is inside the rect, otherwise `false`
|
||||||
*/
|
*/
|
||||||
export function isInRect(x: number, y: number, rect: ReadOnlyRect): boolean {
|
export function isInRect(x: number, y: number, rect: ReadOnlyRect): boolean {
|
||||||
return x >= rect[0]
|
return x >= rect[0] &&
|
||||||
&& x < rect[0] + rect[2]
|
x < rect[0] + rect[2] &&
|
||||||
&& y >= rect[1]
|
y >= rect[1] &&
|
||||||
&& y < rect[1] + rect[3]
|
y < rect[1] + rect[3]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether a point (`x, y`) is inside a rectangle.
|
* Determines whether a point (`x, y`) is inside a rectangle.
|
||||||
*
|
*
|
||||||
* This is the original litegraph implementation. It returns `false` if `x` is equal to `left`, or `y` is equal to `top`.
|
* This is the original litegraph implementation. It returns `false` if `x` is equal to `left`, or `y` is equal to `top`.
|
||||||
* @deprecated
|
* @deprecated
|
||||||
* Use {@link isInRectangle} to match inclusive of top left.
|
* Use {@link isInRectangle} to match inclusive of top left.
|
||||||
* This function returns a false negative when an integer point (e.g. pixel) is on the leftmost or uppermost edge of a rectangle.
|
* This function returns a false negative when an integer point (e.g. pixel) is on the leftmost or uppermost edge of a rectangle.
|
||||||
*
|
*
|
||||||
* @param x Point x
|
* @param x Point x
|
||||||
* @param y Point y
|
* @param y Point y
|
||||||
* @param left Rect x
|
* @param left Rect x
|
||||||
@@ -88,11 +101,18 @@ export function isInRect(x: number, y: number, rect: ReadOnlyRect): boolean {
|
|||||||
* @param height Rect height
|
* @param height Rect height
|
||||||
* @returns `true` if the point is inside the rect, otherwise `false`
|
* @returns `true` if the point is inside the rect, otherwise `false`
|
||||||
*/
|
*/
|
||||||
export function isInsideRectangle(x: number, y: number, left: number, top: number, width: number, height: number): boolean {
|
export function isInsideRectangle(
|
||||||
return left < x
|
x: number,
|
||||||
&& left + width > x
|
y: number,
|
||||||
&& top < y
|
left: number,
|
||||||
&& top + height > y
|
top: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
): boolean {
|
||||||
|
return left < x &&
|
||||||
|
left + width > x &&
|
||||||
|
top < y &&
|
||||||
|
top + height > y
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,8 +123,8 @@ export function isInsideRectangle(x: number, y: number, left: number, top: numbe
|
|||||||
* @returns `true` if the point is roughly inside the octagon centred on 0,0 with specified radius
|
* @returns `true` if the point is roughly inside the octagon centred on 0,0 with specified radius
|
||||||
*/
|
*/
|
||||||
export function isSortaInsideOctagon(x: number, y: number, radius: number): boolean {
|
export function isSortaInsideOctagon(x: number, y: number, radius: number): boolean {
|
||||||
const sum = Math.min(radius, Math.abs(x)) + Math.min(radius, Math.abs(y))
|
const sum = Math.min(radius, Math.abs(x)) + Math.min(radius, Math.abs(y))
|
||||||
return sum < radius * 0.75
|
return sum < radius * 0.75
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,17 +134,17 @@ export function isSortaInsideOctagon(x: number, y: number, radius: number): bool
|
|||||||
* @returns `true` if rectangles overlap, otherwise `false`
|
* @returns `true` if rectangles overlap, otherwise `false`
|
||||||
*/
|
*/
|
||||||
export function overlapBounding(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
|
export function overlapBounding(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
|
||||||
const aRight = a[0] + a[2]
|
const aRight = a[0] + a[2]
|
||||||
const aBottom = a[1] + a[3]
|
const aBottom = a[1] + a[3]
|
||||||
const bRight = b[0] + b[2]
|
const bRight = b[0] + b[2]
|
||||||
const bBottom = b[1] + b[3]
|
const bBottom = b[1] + b[3]
|
||||||
|
|
||||||
return a[0] > bRight
|
return a[0] > bRight ||
|
||||||
|| a[1] > bBottom
|
a[1] > bBottom ||
|
||||||
|| aRight < b[0]
|
aRight < b[0] ||
|
||||||
|| aBottom < b[1]
|
aBottom < b[1]
|
||||||
? false
|
? false
|
||||||
: true
|
: true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -134,9 +154,9 @@ export function overlapBounding(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
|
|||||||
* @returns `true` if {@link a} contains most of {@link b}, otherwise `false`
|
* @returns `true` if {@link a} contains most of {@link b}, otherwise `false`
|
||||||
*/
|
*/
|
||||||
export function containsCentre(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
|
export function containsCentre(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
|
||||||
const centreX = b[0] + (b[2] * 0.5)
|
const centreX = b[0] + (b[2] * 0.5)
|
||||||
const centreY = b[1] + (b[3] * 0.5)
|
const centreY = b[1] + (b[3] * 0.5)
|
||||||
return isInRect(centreX, centreY, a)
|
return isInRect(centreX, centreY, a)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -146,21 +166,21 @@ export function containsCentre(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
|
|||||||
* @returns `true` if {@link a} wholly contains {@link b}, otherwise `false`
|
* @returns `true` if {@link a} wholly contains {@link b}, otherwise `false`
|
||||||
*/
|
*/
|
||||||
export function containsRect(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
|
export function containsRect(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
|
||||||
const aRight = a[0] + a[2]
|
const aRight = a[0] + a[2]
|
||||||
const aBottom = a[1] + a[3]
|
const aBottom = a[1] + a[3]
|
||||||
const bRight = b[0] + b[2]
|
const bRight = b[0] + b[2]
|
||||||
const bBottom = b[1] + b[3]
|
const bBottom = b[1] + b[3]
|
||||||
|
|
||||||
const identical = a[0] === b[0]
|
const identical = a[0] === b[0] &&
|
||||||
&& a[1] === b[1]
|
a[1] === b[1] &&
|
||||||
&& aRight === bRight
|
aRight === bRight &&
|
||||||
&& aBottom === bBottom
|
aBottom === bBottom
|
||||||
|
|
||||||
return !identical
|
return !identical &&
|
||||||
&& a[0] <= b[0]
|
a[0] <= b[0] &&
|
||||||
&& a[1] <= b[1]
|
a[1] <= b[1] &&
|
||||||
&& aRight >= bRight
|
aRight >= bRight &&
|
||||||
&& aBottom >= bBottom
|
aBottom >= bBottom
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -169,99 +189,117 @@ export function containsRect(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
|
|||||||
* @param direction Direction to add the offset to
|
* @param direction Direction to add the offset to
|
||||||
* @param out The {@link Point} to add the offset to
|
* @param out The {@link Point} to add the offset to
|
||||||
*/
|
*/
|
||||||
export function addDirectionalOffset(amount: number, direction: LinkDirection, out: Point): void {
|
export function addDirectionalOffset(
|
||||||
switch (direction) {
|
amount: number,
|
||||||
case LinkDirection.LEFT:
|
direction: LinkDirection,
|
||||||
out[0] -= amount
|
out: Point,
|
||||||
return
|
): void {
|
||||||
case LinkDirection.RIGHT:
|
switch (direction) {
|
||||||
out[0] += amount
|
case LinkDirection.LEFT:
|
||||||
return
|
out[0] -= amount
|
||||||
case LinkDirection.UP:
|
return
|
||||||
out[1] -= amount
|
case LinkDirection.RIGHT:
|
||||||
return
|
out[0] += amount
|
||||||
case LinkDirection.DOWN:
|
return
|
||||||
out[1] += amount
|
case LinkDirection.UP:
|
||||||
return
|
out[1] -= amount
|
||||||
// LinkDirection.CENTER: Nothing to do.
|
return
|
||||||
}
|
case LinkDirection.DOWN:
|
||||||
|
out[1] += amount
|
||||||
|
return
|
||||||
|
// LinkDirection.CENTER: Nothing to do.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rotates an offset in 90° increments.
|
* Rotates an offset in 90° increments.
|
||||||
*
|
*
|
||||||
* Swaps/flips axis values of a 2D vector offset - effectively rotating {@link offset} by 90°
|
* Swaps/flips axis values of a 2D vector offset - effectively rotating
|
||||||
|
* {@link offset} by 90°
|
||||||
* @param offset The zero-based offset to rotate
|
* @param offset The zero-based offset to rotate
|
||||||
* @param from Direction to rotate from
|
* @param from Direction to rotate from
|
||||||
* @param to Direction to rotate to
|
* @param to Direction to rotate to
|
||||||
*/
|
*/
|
||||||
export function rotateLink(offset: Point, from: LinkDirection, to: LinkDirection): void {
|
export function rotateLink(
|
||||||
let x: number
|
offset: Point,
|
||||||
let y: number
|
from: LinkDirection,
|
||||||
|
to: LinkDirection,
|
||||||
|
): void {
|
||||||
|
let x: number
|
||||||
|
let y: number
|
||||||
|
|
||||||
// Normalise to left
|
// Normalise to left
|
||||||
switch (from) {
|
switch (from) {
|
||||||
case to:
|
case to:
|
||||||
case LinkDirection.CENTER:
|
case LinkDirection.CENTER:
|
||||||
case LinkDirection.NONE:
|
case LinkDirection.NONE:
|
||||||
// Nothing to do
|
// Nothing to do
|
||||||
return
|
return
|
||||||
|
|
||||||
case LinkDirection.LEFT:
|
case LinkDirection.LEFT:
|
||||||
x = offset[0]
|
x = offset[0]
|
||||||
y = offset[1]
|
y = offset[1]
|
||||||
break
|
break
|
||||||
case LinkDirection.RIGHT:
|
case LinkDirection.RIGHT:
|
||||||
x = -offset[0]
|
x = -offset[0]
|
||||||
y = -offset[1]
|
y = -offset[1]
|
||||||
break
|
break
|
||||||
case LinkDirection.UP:
|
case LinkDirection.UP:
|
||||||
x = -offset[1]
|
x = -offset[1]
|
||||||
y = offset[0]
|
y = offset[0]
|
||||||
break
|
break
|
||||||
case LinkDirection.DOWN:
|
case LinkDirection.DOWN:
|
||||||
x = offset[1]
|
x = offset[1]
|
||||||
y = -offset[0]
|
y = -offset[0]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply new direction
|
// Apply new direction
|
||||||
switch (to) {
|
switch (to) {
|
||||||
case LinkDirection.CENTER:
|
case LinkDirection.CENTER:
|
||||||
case LinkDirection.NONE:
|
case LinkDirection.NONE:
|
||||||
// Nothing to do
|
// Nothing to do
|
||||||
return
|
return
|
||||||
|
|
||||||
case LinkDirection.LEFT:
|
case LinkDirection.LEFT:
|
||||||
offset[0] = x
|
offset[0] = x
|
||||||
offset[1] = y
|
offset[1] = y
|
||||||
break
|
break
|
||||||
case LinkDirection.RIGHT:
|
case LinkDirection.RIGHT:
|
||||||
offset[0] = -x
|
offset[0] = -x
|
||||||
offset[1] = -y
|
offset[1] = -y
|
||||||
break
|
break
|
||||||
case LinkDirection.UP:
|
case LinkDirection.UP:
|
||||||
offset[0] = y
|
offset[0] = y
|
||||||
offset[1] = -x
|
offset[1] = -x
|
||||||
break
|
break
|
||||||
case LinkDirection.DOWN:
|
case LinkDirection.DOWN:
|
||||||
offset[0] = -y
|
offset[0] = -y
|
||||||
offset[1] = x
|
offset[1] = x
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a point is to to the left or right of a line.
|
* Check if a point is to to the left or right of a line.
|
||||||
* Project a line from lineStart -> lineEnd. Determine if point is to the left or right of that projection.
|
* Project a line from lineStart -> lineEnd. Determine if point is to the left
|
||||||
|
* or right of that projection.
|
||||||
* {@link https://www.geeksforgeeks.org/orientation-3-ordered-points/}
|
* {@link https://www.geeksforgeeks.org/orientation-3-ordered-points/}
|
||||||
* @param lineStart The start point of the line
|
* @param lineStart The start point of the line
|
||||||
* @param lineEnd The end point of the line
|
* @param lineEnd The end point of the line
|
||||||
* @param point The point to check
|
* @param point The point to check
|
||||||
* @returns 0 if all three points are in a straight line, a negative value if point is to the left of the projected line, or positive if the point is to the right
|
* @returns 0 if all three points are in a straight line, a negative value if
|
||||||
|
* point is to the left of the projected line, or positive if the point is to
|
||||||
|
* the right
|
||||||
*/
|
*/
|
||||||
export function getOrientation(lineStart: ReadOnlyPoint, lineEnd: ReadOnlyPoint, x: number, y: number): number {
|
export function getOrientation(
|
||||||
return ((lineEnd[1] - lineStart[1]) * (x - lineEnd[0])) - ((lineEnd[0] - lineStart[0]) * (y - lineEnd[1]))
|
lineStart: ReadOnlyPoint,
|
||||||
|
lineEnd: ReadOnlyPoint,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
): number {
|
||||||
|
return ((lineEnd[1] - lineStart[1]) * (x - lineEnd[0])) -
|
||||||
|
((lineEnd[0] - lineStart[0]) * (y - lineEnd[1]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -274,42 +312,45 @@ export function getOrientation(lineStart: ReadOnlyPoint, lineEnd: ReadOnlyPoint,
|
|||||||
* @param t Time: factor of distance to travel along the curve (e.g 0.25 is 25% along the curve)
|
* @param t Time: factor of distance to travel along the curve (e.g 0.25 is 25% along the curve)
|
||||||
*/
|
*/
|
||||||
export function findPointOnCurve(
|
export function findPointOnCurve(
|
||||||
out: Point,
|
out: Point,
|
||||||
a: ReadOnlyPoint,
|
a: ReadOnlyPoint,
|
||||||
b: ReadOnlyPoint,
|
b: ReadOnlyPoint,
|
||||||
controlA: ReadOnlyPoint,
|
controlA: ReadOnlyPoint,
|
||||||
controlB: ReadOnlyPoint,
|
controlB: ReadOnlyPoint,
|
||||||
t: number = 0.5,
|
t: number = 0.5,
|
||||||
): void {
|
): void {
|
||||||
const iT = 1 - t
|
const iT = 1 - t
|
||||||
|
|
||||||
const c1 = iT * iT * iT
|
const c1 = iT * iT * iT
|
||||||
const c2 = 3 * (iT * iT) * t
|
const c2 = 3 * (iT * iT) * t
|
||||||
const c3 = 3 * iT * (t * t)
|
const c3 = 3 * iT * (t * t)
|
||||||
const c4 = t * t * t
|
const c4 = t * t * t
|
||||||
|
|
||||||
out[0] = (c1 * a[0]) + (c2 * controlA[0]) + (c3 * controlB[0]) + (c4 * b[0])
|
out[0] = (c1 * a[0]) + (c2 * controlA[0]) + (c3 * controlB[0]) + (c4 * b[0])
|
||||||
out[1] = (c1 * a[1]) + (c2 * controlA[1]) + (c3 * controlB[1]) + (c4 * b[1])
|
out[1] = (c1 * a[1]) + (c2 * controlA[1]) + (c3 * controlB[1]) + (c4 * b[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createBounds(objects: Iterable<Positionable>, padding: number = 10): ReadOnlyRect | null {
|
export function createBounds(
|
||||||
const bounds = new Float32Array([Infinity, Infinity, -Infinity, -Infinity])
|
objects: Iterable<Positionable>,
|
||||||
|
padding: number = 10,
|
||||||
|
): ReadOnlyRect | null {
|
||||||
|
const bounds = new Float32Array([Infinity, Infinity, -Infinity, -Infinity])
|
||||||
|
|
||||||
for (const obj of objects) {
|
for (const obj of objects) {
|
||||||
const rect = obj.boundingRect
|
const rect = obj.boundingRect
|
||||||
bounds[0] = Math.min(bounds[0], rect[0])
|
bounds[0] = Math.min(bounds[0], rect[0])
|
||||||
bounds[1] = Math.min(bounds[1], rect[1])
|
bounds[1] = Math.min(bounds[1], rect[1])
|
||||||
bounds[2] = Math.max(bounds[2], rect[0] + rect[2])
|
bounds[2] = Math.max(bounds[2], rect[0] + rect[2])
|
||||||
bounds[3] = Math.max(bounds[3], rect[1] + rect[3])
|
bounds[3] = Math.max(bounds[3], rect[1] + rect[3])
|
||||||
}
|
}
|
||||||
if (!bounds.every(x => isFinite(x))) return null
|
if (!bounds.every(x => isFinite(x))) return null
|
||||||
|
|
||||||
return [
|
return [
|
||||||
bounds[0] - padding,
|
bounds[0] - padding,
|
||||||
bounds[1] - padding,
|
bounds[1] - padding,
|
||||||
bounds[2] - bounds[0] + (2 * padding),
|
bounds[2] - bounds[0] + (2 * padding),
|
||||||
bounds[3] - bounds[1] + (2 * padding)
|
bounds[3] - bounds[1] + (2 * padding),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -320,9 +361,9 @@ export function createBounds(objects: Iterable<Positionable>, padding: number =
|
|||||||
* @remarks `NaN` propagates through this function and does not affect return value.
|
* @remarks `NaN` propagates through this function and does not affect return value.
|
||||||
*/
|
*/
|
||||||
export function snapPoint(pos: Point | Rect, snapTo: number): boolean {
|
export function snapPoint(pos: Point | Rect, snapTo: number): boolean {
|
||||||
if (!snapTo) return false
|
if (!snapTo) return false
|
||||||
|
|
||||||
pos[0] = snapTo * Math.round(pos[0] / snapTo)
|
pos[0] = snapTo * Math.round(pos[0] / snapTo)
|
||||||
pos[1] = snapTo * Math.round(pos[1] / snapTo)
|
pos[1] = snapTo * Math.round(pos[1] / snapTo)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
144
src/polyfills.ts
144
src/polyfills.ts
@@ -1,85 +1,85 @@
|
|||||||
|
// API *************************************************
|
||||||
//API *************************************************
|
// like rect but rounded corners
|
||||||
//like rect but rounded corners
|
|
||||||
export function loadPolyfills() {
|
export function loadPolyfills() {
|
||||||
if (typeof (window) != "undefined" && window.CanvasRenderingContext2D && !window.CanvasRenderingContext2D.prototype.roundRect) {
|
if (
|
||||||
|
typeof window != "undefined" &&
|
||||||
|
window.CanvasRenderingContext2D &&
|
||||||
|
!window.CanvasRenderingContext2D.prototype.roundRect
|
||||||
|
) {
|
||||||
// @ts-expect-error Slightly broken polyfill - radius_low not impl. anywhere
|
// @ts-expect-error Slightly broken polyfill - radius_low not impl. anywhere
|
||||||
window.CanvasRenderingContext2D.prototype.roundRect = function (
|
window.CanvasRenderingContext2D.prototype.roundRect = function (
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
w,
|
w,
|
||||||
h,
|
h,
|
||||||
radius,
|
radius,
|
||||||
radius_low
|
radius_low,
|
||||||
) {
|
) {
|
||||||
let top_left_radius = 0;
|
let top_left_radius = 0
|
||||||
let top_right_radius = 0;
|
let top_right_radius = 0
|
||||||
let bottom_left_radius = 0;
|
let bottom_left_radius = 0
|
||||||
let bottom_right_radius = 0;
|
let bottom_right_radius = 0
|
||||||
|
|
||||||
if (radius === 0) {
|
if (radius === 0) {
|
||||||
this.rect(x, y, w, h);
|
this.rect(x, y, w, h)
|
||||||
return;
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (radius_low === undefined) radius_low = radius
|
||||||
|
|
||||||
|
// make it compatible with official one
|
||||||
|
if (radius != null && radius.constructor === Array) {
|
||||||
|
if (radius.length == 1)
|
||||||
|
top_left_radius = top_right_radius = bottom_left_radius = bottom_right_radius = radius[0]
|
||||||
|
else if (radius.length == 2) {
|
||||||
|
top_left_radius = bottom_right_radius = radius[0]
|
||||||
|
top_right_radius = bottom_left_radius = radius[1]
|
||||||
|
} else if (radius.length == 4) {
|
||||||
|
top_left_radius = radius[0]
|
||||||
|
top_right_radius = radius[1]
|
||||||
|
bottom_left_radius = radius[2]
|
||||||
|
bottom_right_radius = radius[3]
|
||||||
|
} else {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// old using numbers
|
||||||
|
top_left_radius = radius || 0
|
||||||
|
top_right_radius = radius || 0
|
||||||
|
bottom_left_radius = radius_low || 0
|
||||||
|
bottom_right_radius = radius_low || 0
|
||||||
|
}
|
||||||
|
|
||||||
if (radius_low === undefined)
|
// top right
|
||||||
radius_low = radius;
|
this.moveTo(x + top_left_radius, y)
|
||||||
|
this.lineTo(x + w - top_right_radius, y)
|
||||||
|
this.quadraticCurveTo(x + w, y, x + w, y + top_right_radius)
|
||||||
|
|
||||||
//make it compatible with official one
|
// bottom right
|
||||||
if (radius != null && radius.constructor === Array) {
|
this.lineTo(x + w, y + h - bottom_right_radius)
|
||||||
if (radius.length == 1)
|
this.quadraticCurveTo(
|
||||||
top_left_radius = top_right_radius = bottom_left_radius = bottom_right_radius = radius[0];
|
x + w,
|
||||||
else if (radius.length == 2) {
|
y + h,
|
||||||
top_left_radius = bottom_right_radius = radius[0];
|
x + w - bottom_right_radius,
|
||||||
top_right_radius = bottom_left_radius = radius[1];
|
y + h,
|
||||||
}
|
)
|
||||||
else if (radius.length == 4) {
|
|
||||||
top_left_radius = radius[0];
|
|
||||||
top_right_radius = radius[1];
|
|
||||||
bottom_left_radius = radius[2];
|
|
||||||
bottom_right_radius = radius[3];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else //old using numbers
|
|
||||||
{
|
|
||||||
top_left_radius = radius || 0;
|
|
||||||
top_right_radius = radius || 0;
|
|
||||||
bottom_left_radius = radius_low || 0;
|
|
||||||
bottom_right_radius = radius_low || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
//top right
|
// bottom left
|
||||||
this.moveTo(x + top_left_radius, y);
|
this.lineTo(x + bottom_right_radius, y + h)
|
||||||
this.lineTo(x + w - top_right_radius, y);
|
this.quadraticCurveTo(x, y + h, x, y + h - bottom_left_radius)
|
||||||
this.quadraticCurveTo(x + w, y, x + w, y + top_right_radius);
|
|
||||||
|
|
||||||
//bottom right
|
// top left
|
||||||
this.lineTo(x + w, y + h - bottom_right_radius);
|
this.lineTo(x, y + bottom_left_radius)
|
||||||
this.quadraticCurveTo(
|
this.quadraticCurveTo(x, y, x + top_left_radius, y)
|
||||||
x + w,
|
}
|
||||||
y + h,
|
} // if
|
||||||
x + w - bottom_right_radius,
|
|
||||||
y + h
|
|
||||||
);
|
|
||||||
|
|
||||||
//bottom left
|
if (typeof window != "undefined" && !window["requestAnimationFrame"]) {
|
||||||
this.lineTo(x + bottom_right_radius, y + h);
|
|
||||||
this.quadraticCurveTo(x, y + h, x, y + h - bottom_left_radius);
|
|
||||||
|
|
||||||
//top left
|
|
||||||
this.lineTo(x, y + bottom_left_radius);
|
|
||||||
this.quadraticCurveTo(x, y, x + top_left_radius, y);
|
|
||||||
};
|
|
||||||
}//if
|
|
||||||
|
|
||||||
if (typeof window != "undefined" && !window["requestAnimationFrame"]) {
|
|
||||||
window.requestAnimationFrame =
|
window.requestAnimationFrame =
|
||||||
// @ts-expect-error Legacy code
|
// @ts-expect-error Legacy code
|
||||||
window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame ||
|
window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame ||
|
||||||
function (callback) {
|
function (callback) {
|
||||||
window.setTimeout(callback, 1000 / 60);
|
window.setTimeout(callback, 1000 / 60)
|
||||||
};
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
* @returns String(value) or null
|
* @returns String(value) or null
|
||||||
*/
|
*/
|
||||||
export function stringOrNull(value: unknown): string | null {
|
export function stringOrNull(value: unknown): string | null {
|
||||||
return value == null ? null : String(value)
|
return value == null ? null : String(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -13,5 +13,5 @@ export function stringOrNull(value: unknown): string | null {
|
|||||||
* @returns String(value) or ""
|
* @returns String(value) or ""
|
||||||
*/
|
*/
|
||||||
export function stringOrEmpty(value: unknown): string {
|
export function stringOrEmpty(value: unknown): string {
|
||||||
return value == null ? "" : String(value)
|
return value == null ? "" : String(value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,77 +9,84 @@ import type { LGraphGroup } from "../LGraphGroup"
|
|||||||
|
|
||||||
/** For Canvas*Event - adds graph space co-ordinates (property names are shipped) */
|
/** For Canvas*Event - adds graph space co-ordinates (property names are shipped) */
|
||||||
export interface ICanvasPosition {
|
export interface ICanvasPosition {
|
||||||
/** X co-ordinate of the event, in graph space (NOT canvas space) */
|
/** X co-ordinate of the event, in graph space (NOT canvas space) */
|
||||||
canvasX: number
|
canvasX: number
|
||||||
/** Y co-ordinate of the event, in graph space (NOT canvas space) */
|
/** Y co-ordinate of the event, in graph space (NOT canvas space) */
|
||||||
canvasY: number
|
canvasY: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** For Canvas*Event */
|
/** For Canvas*Event */
|
||||||
export interface IDeltaPosition {
|
export interface IDeltaPosition {
|
||||||
deltaX: number
|
deltaX: number
|
||||||
deltaY: number
|
deltaY: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LegacyMouseEvent {
|
interface LegacyMouseEvent {
|
||||||
/** @deprecated Part of DragAndScale mouse API - incomplete / not maintained */
|
/** @deprecated Part of DragAndScale mouse API - incomplete / not maintained */
|
||||||
dragging?: boolean
|
dragging?: boolean
|
||||||
click_time?: number
|
click_time?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** PointerEvent with canvasX/Y and deltaX/Y properties */
|
/** PointerEvent with canvasX/Y and deltaX/Y properties */
|
||||||
export interface CanvasPointerEvent extends PointerEvent, CanvasMouseEvent { }
|
export interface CanvasPointerEvent extends PointerEvent, CanvasMouseEvent {}
|
||||||
|
|
||||||
/** MouseEvent with canvasX/Y and deltaX/Y properties */
|
/** MouseEvent with canvasX/Y and deltaX/Y properties */
|
||||||
export interface CanvasMouseEvent extends MouseEvent, Readonly<ICanvasPosition>, Readonly<IDeltaPosition>, LegacyMouseEvent { }
|
export interface CanvasMouseEvent extends
|
||||||
|
MouseEvent,
|
||||||
|
Readonly<ICanvasPosition>,
|
||||||
|
Readonly<IDeltaPosition>,
|
||||||
|
LegacyMouseEvent {}
|
||||||
|
|
||||||
/** DragEvent with canvasX/Y and deltaX/Y properties */
|
/** DragEvent with canvasX/Y and deltaX/Y properties */
|
||||||
export interface CanvasDragEvent extends DragEvent, ICanvasPosition, IDeltaPosition { }
|
export interface CanvasDragEvent extends
|
||||||
|
DragEvent,
|
||||||
|
ICanvasPosition,
|
||||||
|
IDeltaPosition {}
|
||||||
|
|
||||||
export type CanvasEventDetail =
|
export type CanvasEventDetail =
|
||||||
GenericEventDetail
|
| GenericEventDetail
|
||||||
| DragggingCanvasEventDetail
|
| DragggingCanvasEventDetail
|
||||||
| ReadOnlyEventDetail
|
| ReadOnlyEventDetail
|
||||||
| GroupDoubleClickEventDetail
|
| GroupDoubleClickEventDetail
|
||||||
| EmptyDoubleClickEventDetail
|
| EmptyDoubleClickEventDetail
|
||||||
| ConnectingWidgetLinkEventDetail
|
| ConnectingWidgetLinkEventDetail
|
||||||
| EmptyReleaseEventDetail
|
| EmptyReleaseEventDetail
|
||||||
|
|
||||||
export interface GenericEventDetail {
|
export interface GenericEventDetail {
|
||||||
subType: "before-change" | "after-change"
|
subType: "before-change" | "after-change"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OriginalEvent {
|
export interface OriginalEvent {
|
||||||
originalEvent: CanvasPointerEvent,
|
originalEvent: CanvasPointerEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmptyReleaseEventDetail extends OriginalEvent {
|
export interface EmptyReleaseEventDetail extends OriginalEvent {
|
||||||
subType: "empty-release",
|
subType: "empty-release"
|
||||||
linkReleaseContext: LinkReleaseContextExtended,
|
linkReleaseContext: LinkReleaseContextExtended
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConnectingWidgetLinkEventDetail {
|
export interface ConnectingWidgetLinkEventDetail {
|
||||||
subType: "connectingWidgetLink"
|
subType: "connectingWidgetLink"
|
||||||
link: ConnectingLink
|
link: ConnectingLink
|
||||||
node: LGraphNode
|
node: LGraphNode
|
||||||
widget: IWidget
|
widget: IWidget
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmptyDoubleClickEventDetail extends OriginalEvent {
|
export interface EmptyDoubleClickEventDetail extends OriginalEvent {
|
||||||
subType: "empty-double-click"
|
subType: "empty-double-click"
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupDoubleClickEventDetail extends OriginalEvent {
|
export interface GroupDoubleClickEventDetail extends OriginalEvent {
|
||||||
subType: "group-double-click"
|
subType: "group-double-click"
|
||||||
group: LGraphGroup
|
group: LGraphGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DragggingCanvasEventDetail {
|
export interface DragggingCanvasEventDetail {
|
||||||
subType: "dragging-canvas"
|
subType: "dragging-canvas"
|
||||||
draggingCanvas: boolean
|
draggingCanvas: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReadOnlyEventDetail {
|
export interface ReadOnlyEventDetail {
|
||||||
subType: "read-only"
|
subType: "read-only"
|
||||||
readOnly: boolean
|
readOnly: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,92 +1,92 @@
|
|||||||
/** Node slot type - input or output */
|
/** Node slot type - input or output */
|
||||||
export enum NodeSlotType {
|
export enum NodeSlotType {
|
||||||
INPUT = 1,
|
INPUT = 1,
|
||||||
OUTPUT = 2,
|
OUTPUT = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Shape that an object will render as - used by nodes and slots */
|
/** Shape that an object will render as - used by nodes and slots */
|
||||||
export enum RenderShape {
|
export enum RenderShape {
|
||||||
/** Rectangle with square corners */
|
/** Rectangle with square corners */
|
||||||
BOX = 1,
|
BOX = 1,
|
||||||
/** Rounded rectangle */
|
/** Rounded rectangle */
|
||||||
ROUND = 2,
|
ROUND = 2,
|
||||||
/** Circle is circle */
|
/** Circle is circle */
|
||||||
CIRCLE = 3,
|
CIRCLE = 3,
|
||||||
/** Two rounded corners: top left & bottom right */
|
/** Two rounded corners: top left & bottom right */
|
||||||
CARD = 4,
|
CARD = 4,
|
||||||
/** Slot shape: Arrow */
|
/** Slot shape: Arrow */
|
||||||
ARROW = 5,
|
ARROW = 5,
|
||||||
/** Slot shape: Grid */
|
/** Slot shape: Grid */
|
||||||
GRID = 6,
|
GRID = 6,
|
||||||
/** Slot shape: Hollow circle */
|
/** Slot shape: Hollow circle */
|
||||||
HollowCircle = 7,
|
HollowCircle = 7,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Bit flags used to indicate what the pointer is currently hovering over. */
|
/** Bit flags used to indicate what the pointer is currently hovering over. */
|
||||||
export enum CanvasItem {
|
export enum CanvasItem {
|
||||||
/** No items / none */
|
/** No items / none */
|
||||||
Nothing = 0,
|
Nothing = 0,
|
||||||
/** At least one node */
|
/** At least one node */
|
||||||
Node = 1 << 0,
|
Node = 1 << 0,
|
||||||
/** At least one group */
|
/** At least one group */
|
||||||
Group = 1 << 1,
|
Group = 1 << 1,
|
||||||
/** A reroute (not its path) */
|
/** A reroute (not its path) */
|
||||||
Reroute = 1 << 2,
|
Reroute = 1 << 2,
|
||||||
/** The path of a link */
|
/** The path of a link */
|
||||||
Link = 1 << 3,
|
Link = 1 << 3,
|
||||||
/** A resize in the bottom-right corner */
|
/** A resize in the bottom-right corner */
|
||||||
ResizeSe = 1 << 4,
|
ResizeSe = 1 << 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The direction that a link point will flow towards - e.g. horizontal outputs are right by default */
|
/** The direction that a link point will flow towards - e.g. horizontal outputs are right by default */
|
||||||
export enum LinkDirection {
|
export enum LinkDirection {
|
||||||
NONE = 0,
|
NONE = 0,
|
||||||
UP = 1,
|
UP = 1,
|
||||||
DOWN = 2,
|
DOWN = 2,
|
||||||
LEFT = 3,
|
LEFT = 3,
|
||||||
RIGHT = 4,
|
RIGHT = 4,
|
||||||
CENTER = 5,
|
CENTER = 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The path calculation that links follow */
|
/** The path calculation that links follow */
|
||||||
export enum LinkRenderType {
|
export enum LinkRenderType {
|
||||||
HIDDEN_LINK = -1,
|
HIDDEN_LINK = -1,
|
||||||
/** Juts out from the input & output a little @see LinkDirection, then a straight line between them */
|
/** Juts out from the input & output a little @see LinkDirection, then a straight line between them */
|
||||||
STRAIGHT_LINK = 0,
|
STRAIGHT_LINK = 0,
|
||||||
/** 90° angles, clean and box-like */
|
/** 90° angles, clean and box-like */
|
||||||
LINEAR_LINK = 1,
|
LINEAR_LINK = 1,
|
||||||
/** Smooth curved links - default */
|
/** Smooth curved links - default */
|
||||||
SPLINE_LINK = 2,
|
SPLINE_LINK = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The marker in the middle of a link */
|
/** The marker in the middle of a link */
|
||||||
export enum LinkMarkerShape {
|
export enum LinkMarkerShape {
|
||||||
/** Do not display markers */
|
/** Do not display markers */
|
||||||
None = 0,
|
None = 0,
|
||||||
/** Circles (default) */
|
/** Circles (default) */
|
||||||
Circle = 1,
|
Circle = 1,
|
||||||
/** Directional arrows */
|
/** Directional arrows */
|
||||||
Arrow = 2,
|
Arrow = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum TitleMode {
|
export enum TitleMode {
|
||||||
NORMAL_TITLE = 0,
|
NORMAL_TITLE = 0,
|
||||||
NO_TITLE = 1,
|
NO_TITLE = 1,
|
||||||
TRANSPARENT_TITLE = 2,
|
TRANSPARENT_TITLE = 2,
|
||||||
AUTOHIDE_TITLE = 3,
|
AUTOHIDE_TITLE = 3,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LGraphEventMode {
|
export enum LGraphEventMode {
|
||||||
ALWAYS = 0,
|
ALWAYS = 0,
|
||||||
ON_EVENT = 1,
|
ON_EVENT = 1,
|
||||||
NEVER = 2,
|
NEVER = 2,
|
||||||
ON_TRIGGER = 3,
|
ON_TRIGGER = 3,
|
||||||
BYPASS = 4,
|
BYPASS = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum EaseFunction {
|
export enum EaseFunction {
|
||||||
LINEAR = "linear",
|
LINEAR = "linear",
|
||||||
EASE_IN_QUAD = "easeInQuad",
|
EASE_IN_QUAD = "easeInQuad",
|
||||||
EASE_OUT_QUAD = "easeOutQuad",
|
EASE_OUT_QUAD = "easeOutQuad",
|
||||||
EASE_IN_OUT_QUAD = "easeInOutQuad",
|
EASE_IN_OUT_QUAD = "easeInOutQuad",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import type { ISlotType, Dictionary, INodeFlags, INodeInputSlot, INodeOutputSlot, Point, Size } from "../interfaces"
|
import type {
|
||||||
|
ISlotType,
|
||||||
|
Dictionary,
|
||||||
|
INodeFlags,
|
||||||
|
INodeInputSlot,
|
||||||
|
INodeOutputSlot,
|
||||||
|
Point,
|
||||||
|
Size,
|
||||||
|
} from "../interfaces"
|
||||||
import type { LGraph, LGraphState } from "../LGraph"
|
import type { LGraph, LGraphState } from "../LGraph"
|
||||||
import type { IGraphGroupFlags, LGraphGroup } from "../LGraphGroup"
|
import type { IGraphGroupFlags, LGraphGroup } from "../LGraphGroup"
|
||||||
import type { LGraphNode, NodeId } from "../LGraphNode"
|
import type { LGraphNode, NodeId } from "../LGraphNode"
|
||||||
@@ -12,45 +20,45 @@ import type { RenderShape } from "./globalEnums"
|
|||||||
* An object that implements custom pre-serialization logic via {@link Serialisable.asSerialisable}.
|
* An object that implements custom pre-serialization logic via {@link Serialisable.asSerialisable}.
|
||||||
*/
|
*/
|
||||||
export interface Serialisable<SerialisableObject> {
|
export interface Serialisable<SerialisableObject> {
|
||||||
/**
|
/**
|
||||||
* Prepares this object for serialization.
|
* Prepares this object for serialization.
|
||||||
* Creates a partial shallow copy of itself, with only the properties that should be serialised.
|
* Creates a partial shallow copy of itself, with only the properties that should be serialised.
|
||||||
* @returns An object that can immediately be serialized to JSON.
|
* @returns An object that can immediately be serialized to JSON.
|
||||||
*/
|
*/
|
||||||
asSerialisable(): SerialisableObject
|
asSerialisable(): SerialisableObject
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SerialisableGraph {
|
export interface SerialisableGraph {
|
||||||
/** Schema version. @remarks Version bump should add to const union, which is used to narrow type during deserialise. */
|
/** Schema version. @remarks Version bump should add to const union, which is used to narrow type during deserialise. */
|
||||||
version: 0 | 1
|
version: 0 | 1
|
||||||
config: LGraph["config"]
|
config: LGraph["config"]
|
||||||
state: LGraphState
|
state: LGraphState
|
||||||
groups?: ISerialisedGroup[]
|
groups?: ISerialisedGroup[]
|
||||||
nodes?: ISerialisedNode[]
|
nodes?: ISerialisedNode[]
|
||||||
links?: SerialisableLLink[]
|
links?: SerialisableLLink[]
|
||||||
reroutes?: SerialisableReroute[]
|
reroutes?: SerialisableReroute[]
|
||||||
extra?: Record<any, any>
|
extra?: Record<any, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Serialised LGraphNode */
|
/** Serialised LGraphNode */
|
||||||
export interface ISerialisedNode {
|
export interface ISerialisedNode {
|
||||||
title?: string
|
title?: string
|
||||||
id: NodeId
|
id: NodeId
|
||||||
type?: string
|
type?: string
|
||||||
pos?: Point
|
pos?: Point
|
||||||
size?: Size
|
size?: Size
|
||||||
flags?: INodeFlags
|
flags?: INodeFlags
|
||||||
order?: number
|
order?: number
|
||||||
mode?: number
|
mode?: number
|
||||||
outputs?: INodeOutputSlot[]
|
outputs?: INodeOutputSlot[]
|
||||||
inputs?: INodeInputSlot[]
|
inputs?: INodeInputSlot[]
|
||||||
properties?: Dictionary<unknown>
|
properties?: Dictionary<unknown>
|
||||||
shape?: RenderShape
|
shape?: RenderShape
|
||||||
boxcolor?: string
|
boxcolor?: string
|
||||||
color?: string
|
color?: string
|
||||||
bgcolor?: string
|
bgcolor?: string
|
||||||
showAdvanced?: boolean
|
showAdvanced?: boolean
|
||||||
widgets_values?: TWidgetValue[]
|
widgets_values?: TWidgetValue[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,66 +66,72 @@ export interface ISerialisedNode {
|
|||||||
* Maintained for backwards compat
|
* Maintained for backwards compat
|
||||||
*/
|
*/
|
||||||
export type ISerialisedGraph<
|
export type ISerialisedGraph<
|
||||||
TNode = ReturnType<LGraphNode["serialize"]>,
|
TNode = ReturnType<LGraphNode["serialize"]>,
|
||||||
TLink = ReturnType<LLink["serialize"]>,
|
TLink = ReturnType<LLink["serialize"]>,
|
||||||
TGroup = ReturnType<LGraphGroup["serialize"]>
|
TGroup = ReturnType<LGraphGroup["serialize"]>,
|
||||||
> = {
|
> = {
|
||||||
last_node_id: NodeId
|
last_node_id: NodeId
|
||||||
last_link_id: number
|
last_link_id: number
|
||||||
nodes: TNode[]
|
nodes: TNode[]
|
||||||
links: TLink[]
|
links: TLink[]
|
||||||
groups: TGroup[]
|
groups: TGroup[]
|
||||||
config: LGraph["config"]
|
config: LGraph["config"]
|
||||||
version: typeof LiteGraph.VERSION
|
version: typeof LiteGraph.VERSION
|
||||||
extra?: Record<any, any>
|
extra?: Record<any, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Serialised LGraphGroup */
|
/** Serialised LGraphGroup */
|
||||||
export interface ISerialisedGroup {
|
export interface ISerialisedGroup {
|
||||||
id: number
|
id: number
|
||||||
title: string
|
title: string
|
||||||
bounding: number[]
|
bounding: number[]
|
||||||
color: string
|
color: string
|
||||||
font_size: number
|
font_size: number
|
||||||
flags?: IGraphGroupFlags
|
flags?: IGraphGroupFlags
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TClipboardLink = [targetRelativeIndex: number, originSlot: number, nodeRelativeIndex: number, targetSlot: number, targetNodeId: NodeId]
|
export type TClipboardLink = [
|
||||||
|
targetRelativeIndex: number,
|
||||||
|
originSlot: number,
|
||||||
|
nodeRelativeIndex: number,
|
||||||
|
targetSlot: number,
|
||||||
|
targetNodeId: NodeId,
|
||||||
|
]
|
||||||
|
|
||||||
/** Items copied from the canvas */
|
/** Items copied from the canvas */
|
||||||
export interface ClipboardItems {
|
export interface ClipboardItems {
|
||||||
nodes?: ISerialisedNode[]
|
nodes?: ISerialisedNode[]
|
||||||
groups?: ISerialisedGroup[]
|
groups?: ISerialisedGroup[]
|
||||||
reroutes?: SerialisableReroute[]
|
reroutes?: SerialisableReroute[]
|
||||||
links?: SerialisableLLink[]
|
links?: SerialisableLLink[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** */
|
/** */
|
||||||
export interface IClipboardContents {
|
export interface IClipboardContents {
|
||||||
nodes?: ISerialisedNode[]
|
nodes?: ISerialisedNode[]
|
||||||
links?: TClipboardLink[]
|
links?: TClipboardLink[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SerialisableReroute {
|
export interface SerialisableReroute {
|
||||||
id: RerouteId
|
id: RerouteId
|
||||||
parentId?: RerouteId
|
parentId?: RerouteId
|
||||||
pos: Point
|
pos: Point
|
||||||
linkIds: LinkId[]
|
linkIds: LinkId[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SerialisableLLink {
|
export interface SerialisableLLink {
|
||||||
/** Link ID */
|
/** Link ID */
|
||||||
id: LinkId
|
id: LinkId
|
||||||
/** Output node ID */
|
/** Output node ID */
|
||||||
origin_id: NodeId
|
origin_id: NodeId
|
||||||
/** Output slot index */
|
/** Output slot index */
|
||||||
origin_slot: number
|
origin_slot: number
|
||||||
/** Input node ID */
|
/** Input node ID */
|
||||||
target_id: NodeId
|
target_id: NodeId
|
||||||
/** Input slot index */
|
/** Input slot index */
|
||||||
target_slot: number
|
target_slot: number
|
||||||
/** Data type of the link */
|
/** Data type of the link */
|
||||||
type: ISlotType
|
type: ISlotType
|
||||||
/** ID of the last reroute (from input to output) that this link passes through, otherwise `undefined` */
|
/** ID of the last reroute (from input to output) that this link passes through, otherwise `undefined` */
|
||||||
parentId?: RerouteId
|
parentId?: RerouteId
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,26 +3,26 @@ import type { LGraphCanvas, LGraphNode } from "../litegraph"
|
|||||||
import type { CanvasMouseEvent } from "./events"
|
import type { CanvasMouseEvent } from "./events"
|
||||||
|
|
||||||
export interface IWidgetOptions<TValue = unknown> extends Record<string, unknown> {
|
export interface IWidgetOptions<TValue = unknown> extends Record<string, unknown> {
|
||||||
on?: string
|
on?: string
|
||||||
off?: string
|
off?: string
|
||||||
max?: number
|
max?: number
|
||||||
min?: number
|
min?: number
|
||||||
slider_color?: CanvasColour
|
slider_color?: CanvasColour
|
||||||
marker_color?: CanvasColour
|
marker_color?: CanvasColour
|
||||||
precision?: number
|
precision?: number
|
||||||
read_only?: boolean
|
read_only?: boolean
|
||||||
step?: number
|
step?: number
|
||||||
y?: number
|
y?: number
|
||||||
multiline?: boolean
|
multiline?: boolean
|
||||||
// TODO: Confirm this
|
// TODO: Confirm this
|
||||||
property?: string
|
property?: string
|
||||||
|
|
||||||
hasOwnProperty?(arg0: string): any
|
hasOwnProperty?(arg0: string): any
|
||||||
// values?(widget?: IWidget, node?: LGraphNode): any
|
// values?(widget?: IWidget, node?: LGraphNode): any
|
||||||
values?: TValue[]
|
values?: TValue[]
|
||||||
callback?: IWidget["callback"]
|
callback?: IWidget["callback"]
|
||||||
|
|
||||||
onHide?(widget: IWidget): void
|
onHide?(widget: IWidget): void
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,52 +34,61 @@ export interface IWidgetOptions<TValue = unknown> extends Record<string, unknown
|
|||||||
* Recommend declaration merging any properties that use IWidget (e.g. {@link LGraphNode.widgets}) with a new type alias.
|
* Recommend declaration merging any properties that use IWidget (e.g. {@link LGraphNode.widgets}) with a new type alias.
|
||||||
* @see ICustomWidget
|
* @see ICustomWidget
|
||||||
*/
|
*/
|
||||||
export type IWidget = IBooleanWidget | INumericWidget | IStringWidget | IMultilineStringWidget | IComboWidget | ICustomWidget
|
export type IWidget =
|
||||||
|
| IBooleanWidget
|
||||||
|
| INumericWidget
|
||||||
|
| IStringWidget
|
||||||
|
| IMultilineStringWidget
|
||||||
|
| IComboWidget
|
||||||
|
| ICustomWidget
|
||||||
|
|
||||||
export interface IBooleanWidget extends IBaseWidget {
|
export interface IBooleanWidget extends IBaseWidget {
|
||||||
type?: "toggle"
|
type?: "toggle"
|
||||||
value: boolean
|
value: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Any widget that uses a numeric backing */
|
/** Any widget that uses a numeric backing */
|
||||||
export interface INumericWidget extends IBaseWidget {
|
export interface INumericWidget extends IBaseWidget {
|
||||||
type?: "slider" | "number"
|
type?: "slider" | "number"
|
||||||
value: number
|
value: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A combo-box widget (dropdown, select, etc) */
|
/** A combo-box widget (dropdown, select, etc) */
|
||||||
export interface IComboWidget extends IBaseWidget {
|
export interface IComboWidget extends IBaseWidget {
|
||||||
type?: "combo"
|
type?: "combo"
|
||||||
value: string | number
|
value: string | number
|
||||||
options: IWidgetOptions<string>
|
options: IWidgetOptions<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IStringWidgetType = IStringWidget["type"] | IMultilineStringWidget["type"]
|
export type IStringWidgetType = IStringWidget["type"] | IMultilineStringWidget["type"]
|
||||||
|
|
||||||
/** A widget with a string value */
|
/** A widget with a string value */
|
||||||
export interface IStringWidget extends IBaseWidget {
|
export interface IStringWidget extends IBaseWidget {
|
||||||
type?: "string" | "text" | "button"
|
type?: "string" | "text" | "button"
|
||||||
value: string
|
value: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A widget with a string value and a multiline text input */
|
/** A widget with a string value and a multiline text input */
|
||||||
export interface IMultilineStringWidget<TElement extends HTMLElement = HTMLTextAreaElement> extends IBaseWidget {
|
export interface IMultilineStringWidget<TElement extends HTMLElement = HTMLTextAreaElement> extends
|
||||||
type?: "multiline"
|
IBaseWidget {
|
||||||
value: string
|
|
||||||
|
|
||||||
/** HTML textarea element */
|
type?: "multiline"
|
||||||
element?: TElement
|
value: string
|
||||||
|
|
||||||
|
/** HTML textarea element */
|
||||||
|
element?: TElement
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A custom widget - accepts any value and has no built-in special handling */
|
/** A custom widget - accepts any value and has no built-in special handling */
|
||||||
export interface ICustomWidget<TElement extends HTMLElement = HTMLElement> extends IBaseWidget<TElement> {
|
export interface ICustomWidget<TElement extends HTMLElement = HTMLElement> extends
|
||||||
type?: "custom"
|
IBaseWidget<TElement> {
|
||||||
value: string | object
|
|
||||||
|
|
||||||
element?: TElement
|
type?: "custom"
|
||||||
|
value: string | object
|
||||||
|
|
||||||
|
element?: TElement
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valid widget types. TS cannot provide easily extensible type safety for this at present.
|
* Valid widget types. TS cannot provide easily extensible type safety for this at present.
|
||||||
* Override linkedWidgets[]
|
* Override linkedWidgets[]
|
||||||
@@ -93,35 +102,47 @@ export type TWidgetValue = IWidget["value"]
|
|||||||
* @see IWidget
|
* @see IWidget
|
||||||
*/
|
*/
|
||||||
export interface IBaseWidget<TElement extends HTMLElement = HTMLElement> {
|
export interface IBaseWidget<TElement extends HTMLElement = HTMLElement> {
|
||||||
linkedWidgets?: IWidget[]
|
linkedWidgets?: IWidget[]
|
||||||
|
|
||||||
options: IWidgetOptions
|
options: IWidgetOptions
|
||||||
marker?: number
|
marker?: number
|
||||||
label?: string
|
label?: string
|
||||||
clicked?: boolean
|
clicked?: boolean
|
||||||
name?: string
|
name?: string
|
||||||
/** Widget type (see {@link TWidgetType}) */
|
/** Widget type (see {@link TWidgetType}) */
|
||||||
type?: TWidgetType
|
type?: TWidgetType
|
||||||
value?: TWidgetValue
|
value?: TWidgetValue
|
||||||
y?: number
|
y?: number
|
||||||
last_y?: number
|
last_y?: number
|
||||||
width?: number
|
width?: number
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
|
||||||
hidden?: boolean
|
|
||||||
advanced?: boolean
|
|
||||||
|
|
||||||
tooltip?: string
|
hidden?: boolean
|
||||||
|
advanced?: boolean
|
||||||
|
|
||||||
/** HTML widget element */
|
tooltip?: string
|
||||||
element?: TElement
|
|
||||||
|
|
||||||
// TODO: Confirm this format
|
/** HTML widget element */
|
||||||
callback?(value: any, canvas?: LGraphCanvas, node?: LGraphNode, pos?: Point, e?: CanvasMouseEvent): void
|
element?: TElement
|
||||||
onRemove?(): void
|
|
||||||
beforeQueued?(): void
|
|
||||||
|
|
||||||
mouse?(event: CanvasMouseEvent, arg1: number[], node: LGraphNode): boolean
|
// TODO: Confirm this format
|
||||||
draw?(ctx: CanvasRenderingContext2D, node: LGraphNode, widget_width: number, y: number, H: number): void
|
callback?(
|
||||||
computeSize?(width: number): Size
|
value: any,
|
||||||
|
canvas?: LGraphCanvas,
|
||||||
|
node?: LGraphNode,
|
||||||
|
pos?: Point,
|
||||||
|
e?: CanvasMouseEvent,
|
||||||
|
): void
|
||||||
|
onRemove?(): void
|
||||||
|
beforeQueued?(): void
|
||||||
|
|
||||||
|
mouse?(event: CanvasMouseEvent, arg1: number[], node: LGraphNode): boolean
|
||||||
|
draw?(
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
node: LGraphNode,
|
||||||
|
widget_width: number,
|
||||||
|
y: number,
|
||||||
|
H: number,
|
||||||
|
): void
|
||||||
|
computeSize?(width: number): Size
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,34 +4,35 @@ import type { LGraphNode } from "../LGraphNode"
|
|||||||
/**
|
/**
|
||||||
* Finds the nodes that are farthest in all four directions, representing the boundary of the nodes.
|
* Finds the nodes that are farthest in all four directions, representing the boundary of the nodes.
|
||||||
* @param nodes The nodes to check the edges of
|
* @param nodes The nodes to check the edges of
|
||||||
* @returns An object listing the furthest node (edge) in all four directions. `null` if no nodes were supplied or the first node was falsy.
|
* @returns An object listing the furthest node (edge) in all four directions.
|
||||||
|
* `null` if no nodes were supplied or the first node was falsy.
|
||||||
*/
|
*/
|
||||||
export function getBoundaryNodes(nodes: LGraphNode[]): IBoundaryNodes | null {
|
export function getBoundaryNodes(nodes: LGraphNode[]): IBoundaryNodes | null {
|
||||||
const valid = nodes?.find(x => x)
|
const valid = nodes?.find(x => x)
|
||||||
if (!valid) return null
|
if (!valid) return null
|
||||||
|
|
||||||
let top = valid
|
let top = valid
|
||||||
let right = valid
|
let right = valid
|
||||||
let bottom = valid
|
let bottom = valid
|
||||||
let left = valid
|
let left = valid
|
||||||
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (!node) continue
|
if (!node) continue
|
||||||
const [x, y] = node.pos
|
const [x, y] = node.pos
|
||||||
const [width, height] = node.size
|
const [width, height] = node.size
|
||||||
|
|
||||||
if (y < top.pos[1]) top = node
|
if (y < top.pos[1]) top = node
|
||||||
if (x + width > right.pos[0] + right.size[0]) right = node
|
if (x + width > right.pos[0] + right.size[0]) right = node
|
||||||
if (y + height > bottom.pos[1] + bottom.size[1]) bottom = node
|
if (y + height > bottom.pos[1] + bottom.size[1]) bottom = node
|
||||||
if (x < left.pos[0]) left = node
|
if (x < left.pos[0]) left = node
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
top,
|
top,
|
||||||
right,
|
right,
|
||||||
bottom,
|
bottom,
|
||||||
left
|
left,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,30 +41,30 @@ export function getBoundaryNodes(nodes: LGraphNode[]): IBoundaryNodes | null {
|
|||||||
* @param horizontal If true, distributes along the horizontal plane. Otherwise, the vertical plane.
|
* @param horizontal If true, distributes along the horizontal plane. Otherwise, the vertical plane.
|
||||||
*/
|
*/
|
||||||
export function distributeNodes(nodes: LGraphNode[], horizontal?: boolean): void {
|
export function distributeNodes(nodes: LGraphNode[], horizontal?: boolean): void {
|
||||||
const nodeCount = nodes?.length
|
const nodeCount = nodes?.length
|
||||||
if (!(nodeCount > 1)) return
|
if (!(nodeCount > 1)) return
|
||||||
|
|
||||||
const index = horizontal ? 0 : 1
|
const index = horizontal ? 0 : 1
|
||||||
|
|
||||||
let total = 0
|
let total = 0
|
||||||
let highest = -Infinity
|
let highest = -Infinity
|
||||||
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
total += node.size[index]
|
total += node.size[index]
|
||||||
|
|
||||||
const high = node.pos[index] + node.size[index]
|
const high = node.pos[index] + node.size[index]
|
||||||
if (high > highest) highest = high
|
if (high > highest) highest = high
|
||||||
}
|
}
|
||||||
const sorted = [...nodes].sort((a, b) => a.pos[index] - b.pos[index])
|
const sorted = [...nodes].sort((a, b) => a.pos[index] - b.pos[index])
|
||||||
const lowest = sorted[0].pos[index]
|
const lowest = sorted[0].pos[index]
|
||||||
|
|
||||||
const gap = ((highest - lowest) - total) / (nodeCount - 1)
|
const gap = (highest - lowest - total) / (nodeCount - 1)
|
||||||
let startAt = lowest
|
let startAt = lowest
|
||||||
for (let i = 0; i < nodeCount; i++) {
|
for (let i = 0; i < nodeCount; i++) {
|
||||||
const node = sorted[i]
|
const node = sorted[i]
|
||||||
node.pos[index] = startAt + (gap * i)
|
node.pos[index] = startAt + gap * i
|
||||||
startAt += node.size[index]
|
startAt += node.size[index]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,34 +73,33 @@ export function distributeNodes(nodes: LGraphNode[], horizontal?: boolean): void
|
|||||||
* @param direction The edge to align nodes on
|
* @param direction The edge to align nodes on
|
||||||
* @param align_to The node to align all other nodes to. If undefined, the farthest node will be used.
|
* @param align_to The node to align all other nodes to. If undefined, the farthest node will be used.
|
||||||
*/
|
*/
|
||||||
export function alignNodes(nodes: LGraphNode[], direction: Direction, align_to?: LGraphNode): void {
|
export function alignNodes(
|
||||||
if (!nodes) return
|
nodes: LGraphNode[],
|
||||||
|
direction: Direction,
|
||||||
|
align_to?: LGraphNode,
|
||||||
|
): void {
|
||||||
|
if (!nodes) return
|
||||||
|
|
||||||
const boundary = align_to === undefined
|
const boundary = align_to === undefined
|
||||||
? getBoundaryNodes(nodes)
|
? getBoundaryNodes(nodes)
|
||||||
: {
|
: { top: align_to, right: align_to, bottom: align_to, left: align_to }
|
||||||
top: align_to,
|
|
||||||
right: align_to,
|
|
||||||
bottom: align_to,
|
|
||||||
left: align_to
|
|
||||||
}
|
|
||||||
|
|
||||||
if (boundary === null) return
|
if (boundary === null) return
|
||||||
|
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
switch (direction) {
|
switch (direction) {
|
||||||
case "right":
|
case "right":
|
||||||
node.pos[0] = boundary.right.pos[0] + boundary.right.size[0] - node.size[0]
|
node.pos[0] = boundary.right.pos[0] + boundary.right.size[0] - node.size[0]
|
||||||
break
|
break
|
||||||
case "left":
|
case "left":
|
||||||
node.pos[0] = boundary.left.pos[0]
|
node.pos[0] = boundary.left.pos[0]
|
||||||
break
|
break
|
||||||
case "top":
|
case "top":
|
||||||
node.pos[1] = boundary.top.pos[1]
|
node.pos[1] = boundary.top.pos[1]
|
||||||
break
|
break
|
||||||
case "bottom":
|
case "bottom":
|
||||||
node.pos[1] = boundary.bottom.pos[1] + boundary.bottom.size[1] - node.size[1]
|
node.pos[1] = boundary.bottom.pos[1] + boundary.bottom.size[1] - node.size[1]
|
||||||
break
|
break
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,21 +3,21 @@ import { LGraphNode } from "@/LGraphNode"
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a flat set of all positionable items by recursively iterating through all child items.
|
* Creates a flat set of all positionable items by recursively iterating through all child items.
|
||||||
*
|
*
|
||||||
* Does not include or recurse into pinned items.
|
* Does not include or recurse into pinned items.
|
||||||
* @param items The original set of items to iterate through
|
* @param items The original set of items to iterate through
|
||||||
* @returns All unpinned items in the original set, and recursively, their children
|
* @returns All unpinned items in the original set, and recursively, their children
|
||||||
*/
|
*/
|
||||||
export function getAllNestedItems(items: ReadonlySet<Positionable>): Set<Positionable> {
|
export function getAllNestedItems(items: ReadonlySet<Positionable>): Set<Positionable> {
|
||||||
const allItems = new Set<Positionable>()
|
const allItems = new Set<Positionable>()
|
||||||
items?.forEach(x => addRecursively(x, allItems))
|
items?.forEach(x => addRecursively(x, allItems))
|
||||||
return allItems
|
return allItems
|
||||||
|
|
||||||
function addRecursively(item: Positionable, flatSet: Set<Positionable>): void {
|
function addRecursively(item: Positionable, flatSet: Set<Positionable>): void {
|
||||||
if (flatSet.has(item) || item.pinned) return
|
if (flatSet.has(item) || item.pinned) return
|
||||||
flatSet.add(item)
|
flatSet.add(item)
|
||||||
item.children?.forEach(x => addRecursively(x, flatSet))
|
item.children?.forEach(x => addRecursively(x, flatSet))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,7 +26,7 @@ export function getAllNestedItems(items: ReadonlySet<Positionable>): Set<Positio
|
|||||||
* @returns The first node found in {@link items}, otherwise `undefined`
|
* @returns The first node found in {@link items}, otherwise `undefined`
|
||||||
*/
|
*/
|
||||||
export function findFirstNode(items: Iterable<Positionable>): LGraphNode | undefined {
|
export function findFirstNode(items: Iterable<Positionable>): LGraphNode | undefined {
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
if (item instanceof LGraphNode) return item
|
if (item instanceof LGraphNode) return item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
/// <reference types='vitest' />
|
/// <reference types='vitest' />
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from "vite"
|
||||||
import path from 'path'
|
import path from "path"
|
||||||
import dts from 'vite-plugin-dts'
|
import dts from "vite-plugin-dts"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: path.resolve(__dirname, 'src/litegraph'),
|
entry: path.resolve(__dirname, "src/litegraph"),
|
||||||
name: 'litegraph.js',
|
name: "litegraph.js",
|
||||||
fileName: (format) => `litegraph.${format}.js`,
|
fileName: format => `litegraph.${format}.js`,
|
||||||
formats: ['es', 'umd']
|
formats: ["es", "umd"],
|
||||||
},
|
},
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
target: ['es2022'],
|
target: ["es2022"],
|
||||||
},
|
},
|
||||||
esbuild: {
|
esbuild: {
|
||||||
minifyIdentifiers: false,
|
minifyIdentifiers: false,
|
||||||
@@ -20,18 +20,18 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
dts({
|
dts({
|
||||||
entryRoot: 'src',
|
entryRoot: "src",
|
||||||
insertTypesEntry: true,
|
insertTypesEntry: true,
|
||||||
include: ['src/**/*.ts'],
|
include: ["src/**/*.ts"],
|
||||||
outDir: 'dist',
|
outDir: "dist",
|
||||||
aliasesExclude: ['@'],
|
aliasesExclude: ["@"],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: { '@': '/src' },
|
alias: { "@": "/src" },
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
alias: { '@/': path.resolve(__dirname, './src/') },
|
alias: { "@/": path.resolve(__dirname, "./src/") },
|
||||||
environment: 'jsdom',
|
environment: "jsdom",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user