mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-23 00:04:06 +00:00
Currently, when node defs are refreshed (e.g., by pressing "r" shortcut), every single node def is logged, which is useless when there's hundred of nodes and clutters console. This PR changes to only log those messages when in debug mode.
841 lines
26 KiB
TypeScript
841 lines
26 KiB
TypeScript
import type { Dictionary, ISlotType, Rect, WhenNullish } from "./interfaces"
|
|
|
|
import { InputIndicators } from "./canvas/InputIndicators"
|
|
import { ContextMenu } from "./ContextMenu"
|
|
import { CurveEditor } from "./CurveEditor"
|
|
import { DragAndScale } from "./DragAndScale"
|
|
import { LabelPosition, SlotDirection, SlotShape, SlotType } from "./draw"
|
|
import { LGraph } from "./LGraph"
|
|
import { LGraphCanvas } from "./LGraphCanvas"
|
|
import { LGraphGroup } from "./LGraphGroup"
|
|
import { LGraphNode } from "./LGraphNode"
|
|
import { LLink } from "./LLink"
|
|
import { distance, isInsideRectangle, overlapBounding } from "./measure"
|
|
import { Reroute } from "./Reroute"
|
|
import {
|
|
LGraphEventMode,
|
|
LinkDirection,
|
|
LinkRenderType,
|
|
NodeSlotType,
|
|
RenderShape,
|
|
TitleMode,
|
|
} from "./types/globalEnums"
|
|
import { createUuidv4 } from "./utils/uuid"
|
|
|
|
/**
|
|
* The Global Scope. It contains all the registered node classes.
|
|
*/
|
|
export class LiteGraphGlobal {
|
|
// Enums
|
|
SlotShape = SlotShape
|
|
SlotDirection = SlotDirection
|
|
SlotType = SlotType
|
|
LabelPosition = LabelPosition
|
|
|
|
/** Used in serialised graphs at one point. */
|
|
VERSION = 0.4 as const
|
|
|
|
CANVAS_GRID_SIZE = 10
|
|
|
|
NODE_TITLE_HEIGHT = 30
|
|
NODE_TITLE_TEXT_Y = 20
|
|
NODE_SLOT_HEIGHT = 20
|
|
NODE_WIDGET_HEIGHT = 20
|
|
NODE_WIDTH = 140
|
|
NODE_MIN_WIDTH = 50
|
|
NODE_COLLAPSED_RADIUS = 10
|
|
NODE_COLLAPSED_WIDTH = 80
|
|
NODE_TITLE_COLOR = "#999"
|
|
NODE_SELECTED_TITLE_COLOR = "#FFF"
|
|
NODE_TEXT_SIZE = 14
|
|
NODE_TEXT_COLOR = "#AAA"
|
|
NODE_TEXT_HIGHLIGHT_COLOR = "#EEE"
|
|
NODE_SUBTEXT_SIZE = 12
|
|
NODE_DEFAULT_COLOR = "#333"
|
|
NODE_DEFAULT_BGCOLOR = "#353535"
|
|
NODE_DEFAULT_BOXCOLOR = "#666"
|
|
NODE_DEFAULT_SHAPE = RenderShape.ROUND
|
|
NODE_BOX_OUTLINE_COLOR = "#FFF"
|
|
NODE_ERROR_COLOUR = "#E00"
|
|
DEFAULT_SHADOW_COLOR = "rgba(0,0,0,0.5)"
|
|
DEFAULT_GROUP_FONT = 24
|
|
DEFAULT_GROUP_FONT_SIZE?: any
|
|
|
|
WIDGET_BGCOLOR = "#222"
|
|
WIDGET_OUTLINE_COLOR = "#666"
|
|
WIDGET_ADVANCED_OUTLINE_COLOR = "rgba(56, 139, 253, 0.8)"
|
|
WIDGET_TEXT_COLOR = "#DDD"
|
|
WIDGET_SECONDARY_TEXT_COLOR = "#999"
|
|
|
|
LINK_COLOR = "#9A9"
|
|
EVENT_LINK_COLOR = "#A86"
|
|
CONNECTING_LINK_COLOR = "#AFA"
|
|
|
|
/** avoid infinite loops */
|
|
MAX_NUMBER_OF_NODES = 10_000
|
|
/** default node position */
|
|
DEFAULT_POSITION = [100, 100]
|
|
/** ,"circle" */
|
|
VALID_SHAPES = ["default", "box", "round", "card"] satisfies ("default" | Lowercase<keyof typeof RenderShape>)[]
|
|
ROUND_RADIUS = 8
|
|
|
|
// shapes are used for nodes but also for slots
|
|
BOX_SHAPE = RenderShape.BOX
|
|
ROUND_SHAPE = RenderShape.ROUND
|
|
CIRCLE_SHAPE = RenderShape.CIRCLE
|
|
CARD_SHAPE = RenderShape.CARD
|
|
ARROW_SHAPE = RenderShape.ARROW
|
|
/** intended for slot arrays */
|
|
GRID_SHAPE = RenderShape.GRID
|
|
|
|
// enums
|
|
INPUT = NodeSlotType.INPUT
|
|
OUTPUT = NodeSlotType.OUTPUT
|
|
|
|
// TODO: -1 can lead to ambiguity in JS; these should be updated to a more explicit constant or Symbol.
|
|
/** for outputs */
|
|
EVENT = -1 as const
|
|
/** for inputs */
|
|
ACTION = -1 as const
|
|
|
|
/** helper, will add "On Request" and more in the future */
|
|
NODE_MODES = ["Always", "On Event", "Never", "On Trigger"]
|
|
/** use with node_box_coloured_by_mode */
|
|
NODE_MODES_COLORS = ["#666", "#422", "#333", "#224", "#626"]
|
|
ALWAYS = LGraphEventMode.ALWAYS
|
|
ON_EVENT = LGraphEventMode.ON_EVENT
|
|
NEVER = LGraphEventMode.NEVER
|
|
ON_TRIGGER = LGraphEventMode.ON_TRIGGER
|
|
|
|
UP = LinkDirection.UP
|
|
DOWN = LinkDirection.DOWN
|
|
LEFT = LinkDirection.LEFT
|
|
RIGHT = LinkDirection.RIGHT
|
|
CENTER = LinkDirection.CENTER
|
|
|
|
/** helper */
|
|
LINK_RENDER_MODES = ["Straight", "Linear", "Spline"]
|
|
HIDDEN_LINK = LinkRenderType.HIDDEN_LINK
|
|
STRAIGHT_LINK = LinkRenderType.STRAIGHT_LINK
|
|
LINEAR_LINK = LinkRenderType.LINEAR_LINK
|
|
SPLINE_LINK = LinkRenderType.SPLINE_LINK
|
|
|
|
NORMAL_TITLE = TitleMode.NORMAL_TITLE
|
|
NO_TITLE = TitleMode.NO_TITLE
|
|
TRANSPARENT_TITLE = TitleMode.TRANSPARENT_TITLE
|
|
AUTOHIDE_TITLE = TitleMode.AUTOHIDE_TITLE
|
|
|
|
/** arrange nodes vertically */
|
|
VERTICAL_LAYOUT = "vertical"
|
|
|
|
/** used to redirect calls */
|
|
proxy = null
|
|
node_images_path = ""
|
|
|
|
debug = false
|
|
catch_exceptions = true
|
|
throw_errors = true
|
|
/** if set to true some nodes like Formula would be allowed to evaluate code that comes from unsafe sources (like node configuration), which could lead to exploits */
|
|
allow_scripts = false
|
|
/** nodetypes by string */
|
|
registered_node_types: Record<string, typeof LGraphNode> = {}
|
|
/** @deprecated used for dropping files in the canvas. It appears the code that enables this was removed, but the object remains and is references by built-in drag drop. */
|
|
node_types_by_file_extension: Record<string, { type: string }> = {}
|
|
/** node types by classname */
|
|
Nodes: Record<string, typeof LGraphNode> = {}
|
|
/** used to store vars between graphs */
|
|
Globals = {}
|
|
|
|
/** @deprecated Unused and will be deleted. */
|
|
searchbox_extras: Dictionary<unknown> = {}
|
|
|
|
/** [true!] this make the nodes box (top left circle) coloured when triggered (execute/action), visual feedback */
|
|
node_box_coloured_when_on = false
|
|
/** [true!] nodebox based on node mode, visual feedback */
|
|
node_box_coloured_by_mode = false
|
|
|
|
/** [false on mobile] better true if not touch device, TODO add an helper/listener to close if false */
|
|
dialog_close_on_mouse_leave = false
|
|
dialog_close_on_mouse_leave_delay = 500
|
|
|
|
/** [false!] prefer false if results too easy to break links - implement with ALT or TODO custom keys */
|
|
shift_click_do_break_link_from = false
|
|
/** [false!]prefer false, way too easy to break links */
|
|
click_do_break_link_to = false
|
|
/** [true!] who accidentally ctrl-alt-clicks on an in/output? nobody! that's who! */
|
|
ctrl_alt_click_do_break_link = true
|
|
/** [true!] snaps links when dragging connections over valid targets */
|
|
snaps_for_comfy = true
|
|
/** [true!] renders a partial border to highlight when a dragged link is snapped to a node */
|
|
snap_highlights_node = true
|
|
|
|
/**
|
|
* If `true`, items always snap to the grid - modifier keys are ignored.
|
|
* When {@link snapToGrid} is falsy, a value of `1` is used.
|
|
* Default: `false`
|
|
*/
|
|
alwaysSnapToGrid?: boolean
|
|
|
|
/**
|
|
* When set to a positive number, when nodes are moved their positions will
|
|
* be rounded to the nearest multiple of this value. Half up.
|
|
* Default: `undefined`
|
|
* @todo Not implemented - see {@link LiteGraph.CANVAS_GRID_SIZE}
|
|
*/
|
|
snapToGrid?: number
|
|
|
|
/** [false on mobile] better true if not touch device, TODO add an helper/listener to close if false */
|
|
search_hide_on_mouse_leave = true
|
|
/**
|
|
* [true!] enable filtering slots type in the search widget
|
|
* !requires auto_load_slot_types or manual set registered_slot_[in/out]_types and slot_types_[in/out]
|
|
*/
|
|
search_filter_enabled = false
|
|
/** [true!] opens the results list when opening the search widget */
|
|
search_show_all_on_open = true
|
|
|
|
/**
|
|
* [if want false, use true, run, get vars values to be statically set, than disable]
|
|
* nodes types and nodeclass association with node types need to be calculated,
|
|
* if dont want this, calculate once and set registered_slot_[in/out]_types and slot_types_[in/out]
|
|
*/
|
|
auto_load_slot_types = false
|
|
|
|
// set these values if not using auto_load_slot_types
|
|
/** slot types for nodeclass */
|
|
registered_slot_in_types: Record<string, { nodes: string[] }> = {}
|
|
/** slot types for nodeclass */
|
|
registered_slot_out_types: Record<string, { nodes: string[] }> = {}
|
|
/** slot types IN */
|
|
slot_types_in: string[] = []
|
|
/** slot types OUT */
|
|
slot_types_out: string[] = []
|
|
/**
|
|
* specify for each IN slot type a(/many) default node(s), use single string, array, or object
|
|
* (with node, title, parameters, ..) like for search
|
|
*/
|
|
slot_types_default_in: Record<string, string[]> = {}
|
|
/**
|
|
* specify for each OUT slot type a(/many) default node(s), use single string, array, or object
|
|
* (with node, title, parameters, ..) like for search
|
|
*/
|
|
slot_types_default_out: Record<string, string[]> = {}
|
|
|
|
/** [true!] very handy, ALT click to clone and drag the new node */
|
|
alt_drag_do_clone_nodes = false
|
|
|
|
/**
|
|
* [true!] will create and connect event slots when using action/events connections,
|
|
* !WILL CHANGE node mode when using onTrigger (enable mode colors), onExecuted does not need this
|
|
*/
|
|
do_add_triggers_slots = false
|
|
|
|
/** [false!] being events, it is strongly reccomended to use them sequentially, one by one */
|
|
allow_multi_output_for_events = true
|
|
|
|
/** [true!] allows to create and connect a ndoe clicking with the third button (wheel) */
|
|
middle_click_slot_add_default_node = false
|
|
|
|
/** [true!] dragging a link to empty space will open a menu, add from list, search or defaults */
|
|
release_link_on_empty_shows_menu = false
|
|
|
|
/** "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now) */
|
|
pointerevents_method = "pointer"
|
|
|
|
/**
|
|
* [true!] allows ctrl + shift + v to paste nodes with the outputs of the unselected nodes connected
|
|
* with the inputs of the newly pasted nodes
|
|
*/
|
|
ctrl_shift_v_paste_connect_unselected_outputs = true
|
|
|
|
// if true, all newly created nodes/links will use string UUIDs for their id fields instead of integers.
|
|
// use this if you must have node IDs that are unique across all graphs and subgraphs.
|
|
use_uuids = false
|
|
|
|
// Whether to highlight the bounding box of selected groups
|
|
highlight_selected_group = true
|
|
|
|
/** Whether to scale context with the graph when zooming in. Zooming out never makes context menus smaller. */
|
|
context_menu_scaling = false
|
|
|
|
// TODO: Remove legacy accessors
|
|
LGraph = LGraph
|
|
LLink = LLink
|
|
LGraphNode = LGraphNode
|
|
LGraphGroup = LGraphGroup
|
|
DragAndScale = DragAndScale
|
|
LGraphCanvas = LGraphCanvas
|
|
ContextMenu = ContextMenu
|
|
CurveEditor = CurveEditor
|
|
Reroute = Reroute
|
|
InputIndicators = InputIndicators
|
|
|
|
onNodeTypeRegistered?(type: string, base_class: typeof LGraphNode): void
|
|
onNodeTypeReplaced?(type: string, base_class: typeof LGraphNode, prev: unknown): void
|
|
|
|
/**
|
|
* Register a node class so it can be listed when the user wants to create a new one
|
|
* @param type name of the node and path
|
|
* @param base_class class containing the structure of a node
|
|
*/
|
|
registerNodeType(type: string, base_class: typeof LGraphNode): void {
|
|
if (!base_class.prototype)
|
|
throw "Cannot register a simple object, it must be a class with a prototype"
|
|
base_class.type = type
|
|
|
|
if (this.debug) console.log("Node registered:", type)
|
|
|
|
const classname = base_class.name
|
|
|
|
const pos = type.lastIndexOf("/")
|
|
base_class.category = type.substring(0, pos)
|
|
|
|
base_class.title ||= classname
|
|
|
|
// extend class
|
|
for (const i in LGraphNode.prototype) {
|
|
// @ts-expect-error #576 This functionality is deprecated and should be removed.
|
|
base_class.prototype[i] ||= LGraphNode.prototype[i]
|
|
}
|
|
|
|
const prev = this.registered_node_types[type]
|
|
if (prev && this.debug) {
|
|
console.log("replacing node type:", type)
|
|
}
|
|
|
|
this.registered_node_types[type] = base_class
|
|
if (base_class.constructor.name) this.Nodes[classname] = base_class
|
|
|
|
this.onNodeTypeRegistered?.(type, base_class)
|
|
if (prev) this.onNodeTypeReplaced?.(type, base_class, prev)
|
|
|
|
// warnings
|
|
if (base_class.prototype.onPropertyChange)
|
|
console.warn(`LiteGraph node class ${type} has onPropertyChange method, it must be called onPropertyChanged with d at the end`)
|
|
|
|
// TODO one would want to know input and ouput :: this would allow through registerNodeAndSlotType to get all the slots types
|
|
if (this.auto_load_slot_types) new base_class(base_class.title || "tmpnode")
|
|
}
|
|
|
|
/**
|
|
* removes a node type from the system
|
|
* @param type name of the node or the node constructor itself
|
|
*/
|
|
unregisterNodeType(type: string | typeof LGraphNode): void {
|
|
const base_class = typeof type === "string"
|
|
? this.registered_node_types[type]
|
|
: type
|
|
if (!base_class) throw `node type not found: ${String(type)}`
|
|
|
|
delete this.registered_node_types[String(base_class.type)]
|
|
|
|
const name = base_class.constructor.name
|
|
if (name) delete this.Nodes[name]
|
|
}
|
|
|
|
/**
|
|
* Save a slot type and his node
|
|
* @param type name of the node or the node constructor itself
|
|
* @param slot_type name of the slot type (variable type), eg. string, number, array, boolean, ..
|
|
*/
|
|
registerNodeAndSlotType(
|
|
type: LGraphNode,
|
|
slot_type: ISlotType,
|
|
out?: boolean,
|
|
): void {
|
|
out ||= false
|
|
// @ts-expect-error Confirm this function no longer supports string types - base_class should always be an instance not a constructor.
|
|
const base_class = typeof type === "string" && this.registered_node_types[type] !== "anonymous"
|
|
? this.registered_node_types[type]
|
|
: type
|
|
|
|
// @ts-expect-error Confirm this function no longer supports string types - base_class should always be an instance not a constructor.
|
|
const class_type = base_class.constructor.type
|
|
|
|
let allTypes = []
|
|
if (typeof slot_type === "string") {
|
|
allTypes = slot_type.split(",")
|
|
} else if (slot_type == this.EVENT || slot_type == this.ACTION) {
|
|
allTypes = ["_event_"]
|
|
} else {
|
|
allTypes = ["*"]
|
|
}
|
|
|
|
for (let slotType of allTypes) {
|
|
if (slotType === "") slotType = "*"
|
|
|
|
const registerTo = out
|
|
? "registered_slot_out_types"
|
|
: "registered_slot_in_types"
|
|
if (this[registerTo][slotType] === undefined)
|
|
this[registerTo][slotType] = { nodes: [] }
|
|
if (!this[registerTo][slotType].nodes.includes(class_type))
|
|
this[registerTo][slotType].nodes.push(class_type)
|
|
|
|
// check if is a new type
|
|
const types = out
|
|
? this.slot_types_out
|
|
: this.slot_types_in
|
|
if (!types.includes(slotType.toLowerCase())) {
|
|
types.push(slotType.toLowerCase())
|
|
types.sort()
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes all previously registered node's types
|
|
*/
|
|
clearRegisteredTypes(): void {
|
|
this.registered_node_types = {}
|
|
this.node_types_by_file_extension = {}
|
|
this.Nodes = {}
|
|
this.searchbox_extras = {}
|
|
}
|
|
|
|
/**
|
|
* Create a node of a given type with a name. The node is not attached to any graph yet.
|
|
* @param type full name of the node class. p.e. "math/sin"
|
|
* @param title a name to distinguish from other nodes
|
|
* @param options to set options
|
|
*/
|
|
createNode(
|
|
type: string,
|
|
title?: string,
|
|
options?: Dictionary<unknown>,
|
|
): LGraphNode | null {
|
|
const base_class = this.registered_node_types[type]
|
|
if (!base_class) {
|
|
if (this.debug) console.log(`GraphNode type "${type}" not registered.`)
|
|
return null
|
|
}
|
|
|
|
title = title || base_class.title || type
|
|
|
|
let node = null
|
|
|
|
if (this.catch_exceptions) {
|
|
try {
|
|
node = new base_class(title)
|
|
} catch (error) {
|
|
console.error(error)
|
|
return null
|
|
}
|
|
} else {
|
|
node = new base_class(title)
|
|
}
|
|
|
|
node.type = type
|
|
|
|
if (!node.title && title) node.title = title
|
|
node.properties ||= {}
|
|
node.properties_info ||= []
|
|
node.flags ||= {}
|
|
// call onresize?
|
|
node.size ||= node.computeSize()
|
|
node.pos ||= [this.DEFAULT_POSITION[0], this.DEFAULT_POSITION[1]]
|
|
node.mode ||= LGraphEventMode.ALWAYS
|
|
|
|
// extra options
|
|
if (options) {
|
|
for (const i in options) {
|
|
// @ts-expect-error #577 Requires interface
|
|
node[i] = options[i]
|
|
}
|
|
}
|
|
|
|
// callback
|
|
node.onNodeCreated?.()
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* Returns a registered node type with a given name
|
|
* @param type full name of the node class. p.e. "math/sin"
|
|
* @returns the node class
|
|
*/
|
|
getNodeType(type: string): typeof LGraphNode {
|
|
return this.registered_node_types[type]
|
|
}
|
|
|
|
/**
|
|
* Returns a list of node types matching one category
|
|
* @param category category name
|
|
* @returns array with all the node classes
|
|
*/
|
|
getNodeTypesInCategory(category: string, filter?: string) {
|
|
const r = []
|
|
for (const i in this.registered_node_types) {
|
|
const type = this.registered_node_types[i]
|
|
if (type.filter != filter) continue
|
|
|
|
if (category == "") {
|
|
if (type.category == null) r.push(type)
|
|
} else if (type.category == category) {
|
|
r.push(type)
|
|
}
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
/**
|
|
* Returns a list with all the node type categories
|
|
* @param filter only nodes with ctor.filter equal can be shown
|
|
* @returns array with all the names of the categories
|
|
*/
|
|
getNodeTypesCategories(filter?: string): string[] {
|
|
const categories: Dictionary<number> = { "": 1 }
|
|
for (const i in this.registered_node_types) {
|
|
const type = this.registered_node_types[i]
|
|
if (type.category && !type.skip_list) {
|
|
if (type.filter != filter) continue
|
|
|
|
categories[type.category] = 1
|
|
}
|
|
}
|
|
const result = []
|
|
for (const i in categories) {
|
|
result.push(i)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// debug purposes: reloads all the js scripts that matches a wildcard
|
|
reloadNodes(folder_wildcard: string): void {
|
|
const tmp = document.getElementsByTagName("script")
|
|
// weird, this array changes by its own, so we use a copy
|
|
const script_files = []
|
|
for (const element of tmp) {
|
|
script_files.push(element)
|
|
}
|
|
|
|
const docHeadObj = document.getElementsByTagName("head")[0]
|
|
folder_wildcard = document.location.href + folder_wildcard
|
|
|
|
for (const script_file of script_files) {
|
|
const src = script_file.src
|
|
if (!src || src.substr(0, folder_wildcard.length) != folder_wildcard)
|
|
continue
|
|
|
|
try {
|
|
if (this.debug) console.log("Reloading:", src)
|
|
const dynamicScript = document.createElement("script")
|
|
dynamicScript.type = "text/javascript"
|
|
dynamicScript.src = src
|
|
docHeadObj.append(dynamicScript)
|
|
script_file.remove()
|
|
} catch (error) {
|
|
if (this.throw_errors) throw error
|
|
if (this.debug) console.log("Error while reloading", src)
|
|
}
|
|
}
|
|
|
|
if (this.debug) console.log("Nodes reloaded")
|
|
}
|
|
|
|
// separated just to improve if it doesn't work
|
|
/** @deprecated Prefer {@link structuredClone} */
|
|
cloneObject<T extends object | undefined | null>(obj: T, target?: T): WhenNullish<T, null> {
|
|
if (obj == null) return null as WhenNullish<T, null>
|
|
|
|
const r = JSON.parse(JSON.stringify(obj))
|
|
if (!target) return r
|
|
|
|
for (const i in r) {
|
|
// @ts-expect-error deprecated
|
|
target[i] = r[i]
|
|
}
|
|
return target
|
|
}
|
|
|
|
/** @see {@link createUuidv4} @inheritdoc */
|
|
uuidv4 = createUuidv4
|
|
|
|
/**
|
|
* Returns if the types of two slots are compatible (taking into account wildcards, etc)
|
|
* @param type_a output
|
|
* @param type_b input
|
|
* @returns true if they can be connected
|
|
*/
|
|
isValidConnection(type_a: ISlotType, type_b: ISlotType): boolean {
|
|
if (type_a == "" || type_a === "*") type_a = 0
|
|
if (type_b == "" || type_b === "*") type_b = 0
|
|
// If generic in/output, matching types (valid for triggers), or event/action types
|
|
if (
|
|
!type_a ||
|
|
!type_b ||
|
|
type_a == type_b ||
|
|
(type_a == this.EVENT && type_b == this.ACTION)
|
|
) {
|
|
return true
|
|
}
|
|
|
|
// Enforce string type to handle toLowerCase call (-1 number not ok)
|
|
type_a = String(type_a)
|
|
type_b = String(type_b)
|
|
type_a = type_a.toLowerCase()
|
|
type_b = type_b.toLowerCase()
|
|
|
|
// For nodes supporting multiple connection types
|
|
if (!type_a.includes(",") && !type_b.includes(","))
|
|
return type_a == type_b
|
|
|
|
// Check all permutations to see if one is valid
|
|
const supported_types_a = type_a.split(",")
|
|
const supported_types_b = type_b.split(",")
|
|
for (const a of supported_types_a) {
|
|
for (const b of supported_types_b) {
|
|
if (this.isValidConnection(a, b))
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// used to create nodes from wrapping functions
|
|
getParameterNames(func: (...args: any) => any): string[] {
|
|
return String(func)
|
|
.replaceAll(/\/\/.*$/gm, "") // strip single-line comments
|
|
.replaceAll(/\s+/g, "") // strip white space
|
|
.replaceAll(/\/\*[^*/]*\*\//g, "") // strip multi-line comments /**/
|
|
.split("){", 1)[0]
|
|
.replace(/^[^(]*\(/, "") // extract the parameters
|
|
.replaceAll(/=[^,]+/g, "") // strip any ES6 defaults
|
|
.split(",")
|
|
.filter(Boolean) // split & filter [""]
|
|
}
|
|
|
|
/* helper for interaction: pointer, touch, mouse Listeners
|
|
used by LGraphCanvas DragAndScale ContextMenu */
|
|
pointerListenerAdd(oDOM: Node, sEvIn: string, fCall: (e: Event) => boolean | void, capture = false): void {
|
|
if (!oDOM || !oDOM.addEventListener || !sEvIn || typeof fCall !== "function") return
|
|
|
|
let sMethod = this.pointerevents_method
|
|
let sEvent = sEvIn
|
|
|
|
// UNDER CONSTRUCTION
|
|
// convert pointerevents to touch event when not available
|
|
if (sMethod == "pointer" && !window.PointerEvent) {
|
|
console.warn("sMethod=='pointer' && !window.PointerEvent")
|
|
console.log(`Converting pointer[${sEvent}] : down move up cancel enter TO touchstart touchmove touchend, etc ..`)
|
|
switch (sEvent) {
|
|
case "down": {
|
|
sMethod = "touch"
|
|
sEvent = "start"
|
|
break
|
|
}
|
|
case "move": {
|
|
sMethod = "touch"
|
|
// sEvent = "move";
|
|
break
|
|
}
|
|
case "up": {
|
|
sMethod = "touch"
|
|
sEvent = "end"
|
|
break
|
|
}
|
|
case "cancel": {
|
|
sMethod = "touch"
|
|
// sEvent = "cancel";
|
|
break
|
|
}
|
|
case "enter": {
|
|
console.log("debug: Should I send a move event?") // ???
|
|
break
|
|
}
|
|
// case "over": case "out": not used at now
|
|
default: {
|
|
console.warn(`PointerEvent not available in this browser ? The event ${sEvent} would not be called`)
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (sEvent) {
|
|
// @ts-expect-error
|
|
// both pointer and move events
|
|
case "down": case "up": case "move": case "over": case "out": case "enter":
|
|
{
|
|
oDOM.addEventListener(sMethod + sEvent, fCall, capture)
|
|
}
|
|
// @ts-expect-error
|
|
// only pointerevents
|
|
case "leave": case "cancel": case "gotpointercapture": case "lostpointercapture":
|
|
{
|
|
if (sMethod != "mouse") {
|
|
return oDOM.addEventListener(sMethod + sEvent, fCall, capture)
|
|
}
|
|
}
|
|
// not "pointer" || "mouse"
|
|
default:
|
|
return oDOM.addEventListener(sEvent, fCall, capture)
|
|
}
|
|
}
|
|
|
|
pointerListenerRemove(oDOM: Node, sEvent: string, fCall: (e: Event) => boolean | void, capture = false): void {
|
|
if (!oDOM || !oDOM.removeEventListener || !sEvent || typeof fCall !== "function") return
|
|
|
|
switch (sEvent) {
|
|
// @ts-expect-error
|
|
// both pointer and move events
|
|
case "down": case "up": case "move": case "over": case "out": case "enter":
|
|
{
|
|
if (this.pointerevents_method == "pointer" || this.pointerevents_method == "mouse") {
|
|
oDOM.removeEventListener(this.pointerevents_method + sEvent, fCall, capture)
|
|
}
|
|
}
|
|
// @ts-expect-error
|
|
// only pointerevents
|
|
case "leave": case "cancel": case "gotpointercapture": case "lostpointercapture":
|
|
{
|
|
if (this.pointerevents_method == "pointer") {
|
|
return oDOM.removeEventListener(this.pointerevents_method + sEvent, fCall, capture)
|
|
}
|
|
}
|
|
// not "pointer" || "mouse"
|
|
default:
|
|
return oDOM.removeEventListener(sEvent, fCall, capture)
|
|
}
|
|
}
|
|
|
|
getTime(): number {
|
|
return performance.now()
|
|
}
|
|
|
|
distance = distance
|
|
|
|
colorToString(c: [number, number, number, number]): string {
|
|
return (
|
|
`rgba(${
|
|
Math.round(c[0] * 255).toFixed()
|
|
},${
|
|
Math.round(c[1] * 255).toFixed()
|
|
},${
|
|
Math.round(c[2] * 255).toFixed()
|
|
},${
|
|
c.length == 4 ? c[3].toFixed(2) : "1.0"
|
|
})`
|
|
)
|
|
}
|
|
|
|
isInsideRectangle = isInsideRectangle
|
|
|
|
// [minx,miny,maxx,maxy]
|
|
growBounding(bounding: Rect, x: number, y: number): void {
|
|
if (x < bounding[0]) {
|
|
bounding[0] = x
|
|
} else if (x > bounding[2]) {
|
|
bounding[2] = x
|
|
}
|
|
|
|
if (y < bounding[1]) {
|
|
bounding[1] = y
|
|
} else if (y > bounding[3]) {
|
|
bounding[3] = y
|
|
}
|
|
}
|
|
|
|
overlapBounding = overlapBounding
|
|
|
|
// point inside bounding box
|
|
isInsideBounding(p: number[], bb: number[][]): boolean {
|
|
if (
|
|
p[0] < bb[0][0] ||
|
|
p[1] < bb[0][1] ||
|
|
p[0] > bb[1][0] ||
|
|
p[1] > bb[1][1]
|
|
) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Convert a hex value to its decimal value - the inputted hex must be in the
|
|
// format of a hex triplet - the kind we use for HTML colours. The function
|
|
// will return an array with three values.
|
|
hex2num(hex: string): number[] {
|
|
if (hex.charAt(0) == "#") {
|
|
hex = hex.slice(1)
|
|
// Remove the '#' char - if there is one.
|
|
}
|
|
hex = hex.toUpperCase()
|
|
const hex_alphabets = "0123456789ABCDEF"
|
|
const value = new Array(3)
|
|
let k = 0
|
|
let int1, int2
|
|
for (let i = 0; i < 6; i += 2) {
|
|
int1 = hex_alphabets.indexOf(hex.charAt(i))
|
|
int2 = hex_alphabets.indexOf(hex.charAt(i + 1))
|
|
value[k] = int1 * 16 + int2
|
|
k++
|
|
}
|
|
return value
|
|
}
|
|
|
|
// Give a array with three values as the argument and the function will return
|
|
// the corresponding hex triplet.
|
|
num2hex(triplet: number[]): string {
|
|
const hex_alphabets = "0123456789ABCDEF"
|
|
let hex = "#"
|
|
let int1, int2
|
|
for (let i = 0; i < 3; i++) {
|
|
int1 = triplet[i] / 16
|
|
int2 = triplet[i] % 16
|
|
|
|
hex += hex_alphabets.charAt(int1) + hex_alphabets.charAt(int2)
|
|
}
|
|
return hex
|
|
}
|
|
|
|
closeAllContextMenus(ref_window: Window = window): void {
|
|
const elements = [...ref_window.document.querySelectorAll(".litecontextmenu")]
|
|
if (!elements.length) return
|
|
|
|
for (const element of elements) {
|
|
if ("close" in element && typeof element.close === "function") {
|
|
element.close()
|
|
} else {
|
|
element.remove()
|
|
}
|
|
}
|
|
}
|
|
|
|
extendClass(target: any, origin: any): void {
|
|
for (const i in origin) {
|
|
// copy class properties
|
|
if (target.hasOwnProperty(i)) continue
|
|
target[i] = origin[i]
|
|
}
|
|
|
|
if (origin.prototype) {
|
|
// copy prototype properties
|
|
for (const i in origin.prototype) {
|
|
// only enumerable
|
|
if (!origin.prototype.hasOwnProperty(i)) continue
|
|
|
|
// avoid overwriting existing ones
|
|
if (target.prototype.hasOwnProperty(i)) continue
|
|
|
|
// copy getters
|
|
if (origin.prototype.__lookupGetter__(i)) {
|
|
target.prototype.__defineGetter__(
|
|
i,
|
|
origin.prototype.__lookupGetter__(i),
|
|
)
|
|
} else {
|
|
target.prototype[i] = origin.prototype[i]
|
|
}
|
|
|
|
// and setters
|
|
if (origin.prototype.__lookupSetter__(i)) {
|
|
target.prototype.__defineSetter__(
|
|
i,
|
|
origin.prototype.__lookupSetter__(i),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|