mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 22:37:32 +00:00
The fallback option was added in https://github.com/Comfy-Org/litegraph.js/pull/358. So far no code is using this legacy fallback option. Removing it now.
848 lines
26 KiB
TypeScript
848 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) {
|
|
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): void {
|
|
ref_window = ref_window || window
|
|
|
|
const elements = ref_window.document.querySelectorAll(".litecontextmenu")
|
|
if (!elements.length) return
|
|
|
|
const results = []
|
|
for (const element of elements) {
|
|
results.push(element)
|
|
}
|
|
|
|
for (const result of results) {
|
|
if ("close" in result && typeof result.close === "function") {
|
|
result.close()
|
|
} else if (result.parentNode) {
|
|
result.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),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|