Files
ComfyUI_frontend/src/LiteGraphGlobal.ts
filtered c0e8b33716 Snap everything to grid (#315)
* Implement snap to grid

- Moves positioning logic to LGraph
- Simplifies code
- Adds Pointer API to alt-clone node
- Removes always_round_positions, replaced by always snap to grid (default size is 1 when always snapping)

Fix refator error

* Fix group items snapped without group

* Allow snapping of all items

- Add snapToGrid to Positionable
- Impl. on all types
- Deprecated: alignToGrid is now a wrapper

* Fix test import alias, update expectations

* Prevent desync of before / after change events

Adds ability to perform late binding of finally() during drag start.

* nit - Refactor

* Fix unwanted snap on node/group add

* nit - Doc

* Add shift key state tracking for snap to grid

Private impl., no state API as yet.

* Add snap guides rendering

Nodes, reroutes

* Optimisation - reroute rendering

 Fixes exponential redraw

* Add snap guidelines for groups
2024-11-18 10:12:20 -05:00

948 lines
36 KiB
TypeScript

import { LGraph } from "./LGraph"
import { LLink } from "./LLink"
import { LGraphGroup } from "./LGraphGroup"
import { DragAndScale } from "./DragAndScale"
import { LGraphCanvas } from "./LGraphCanvas"
import { ContextMenu } from "./ContextMenu"
import { CurveEditor } from "./CurveEditor"
import { LGraphEventMode, LinkDirection, LinkRenderType, NodeSlotType, RenderShape, TitleMode } from "./types/globalEnums"
import { LGraphNode } from "./LGraphNode"
import { SlotShape, SlotDirection, SlotType, LabelPosition } from "./draw"
import type { Dictionary, ISlotType, Rect } from "./interfaces"
import { distance, isInsideRectangle, overlapBounding } from "./measure"
/**
* 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 = "box"
NODE_BOX_OUTLINE_COLOR = "#FFF"
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_TEXT_COLOR = "#DDD"
WIDGET_SECONDARY_TEXT_COLOR = "#999"
LINK_COLOR = "#9A9"
// TODO: This is a workaround until LGraphCanvas.link_type_colors is no longer static.
static DEFAULT_EVENT_LINK_COLOR = "#A86"
EVENT_LINK_COLOR = "#A86"
CONNECTING_LINK_COLOR = "#AFA"
MAX_NUMBER_OF_NODES = 10000 //avoid infinite loops
DEFAULT_POSITION = [100, 100] //default node position
VALID_SHAPES = ["default", "box", "round", "card"] //,"circle"
//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
GRID_SHAPE = RenderShape.GRID // intended for slot arrays
//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.
EVENT = -1 as const //for outputs
ACTION = -1 as const //for inputs
NODE_MODES = ["Always", "On Event", "Never", "On Trigger"] // helper, will add "On Request" and more in the future
NODE_MODES_COLORS = ["#666", "#422", "#333", "#224", "#626"] // use with node_box_coloured_by_mode
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
LINK_RENDER_MODES = ["Straight", "Linear", "Spline"] // helper
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
VERTICAL_LAYOUT = "vertical" // arrange nodes vertically
proxy = null //used to redirect calls
node_images_path = ""
debug = false
catch_exceptions = true
throw_errors = true
allow_scripts = false //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
registered_node_types: Record<string, typeof LGraphNode> = {} //nodetypes by string
node_types_by_file_extension = {} //used for dropping files in the canvas
Nodes: Record<string, typeof LGraphNode> = {} //node types by classname
Globals = {} //used to store vars between graphs
searchbox_extras = {} //used to add extra features to the search box
auto_sort_node_types = false // [true!] If set to true, will automatically sort node types / categories in the context menus
node_box_coloured_when_on = false // [true!] this make the nodes box (top left circle) coloured when triggered (execute/action), visual feedback
node_box_coloured_by_mode = false // [true!] nodebox based on node mode, visual feedback
dialog_close_on_mouse_leave = false // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false
dialog_close_on_mouse_leave_delay = 500
shift_click_do_break_link_from = false // [false!] prefer false if results too easy to break links - implement with ALT or TODO custom keys
click_do_break_link_to = false // [false!]prefer false, way too easy to break links
ctrl_alt_click_do_break_link = true // [true!] who accidentally ctrl-alt-clicks on an in/output? nobody! that's who!
snaps_for_comfy = true // [true!] snaps links when dragging connections over valid targets
snap_highlights_node = true // [true!] renders a partial border to highlight when a dragged link is snapped to a node
search_hide_on_mouse_leave = true // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false
search_filter_enabled = false // [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_show_all_on_open = true // [true!] opens the results list when opening the search widget
auto_load_slot_types = false // [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]
// set these values if not using auto_load_slot_types
registered_slot_in_types: Record<string, { nodes: string[] }> = {} // slot types for nodeclass
registered_slot_out_types: Record<string, { nodes: string[] }> = {} // slot types for nodeclass
slot_types_in: string[] = [] // slot types IN
slot_types_out: string[] = [] // slot types OUT
slot_types_default_in: Record<string, 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_out: 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
alt_drag_do_clone_nodes = false // [true!] very handy, ALT click to clone and drag the new node
do_add_triggers_slots = 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
allow_multi_output_for_events = true // [false!] being events, it is strongly reccomended to use them sequentially, one by one
middle_click_slot_add_default_node = false //[true!] allows to create and connect a ndoe clicking with the third button (wheel)
release_link_on_empty_shows_menu = false //[true!] dragging a link to empty space will open a menu, add from list, search or defaults
pointerevents_method = "pointer" // "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now)
// TODO implement pointercancel, gotpointercapture, lostpointercapture, (pointerover, pointerout if necessary)
ctrl_shift_v_paste_connect_unselected_outputs = true //[true!] allows ctrl + shift + v to paste nodes with the outputs of the unselected nodes connected with the inputs of the newly pasted nodes
// 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
// TODO: Remove legacy accessors
LGraph = LGraph
LLink = LLink
LGraphNode = LGraphNode
LGraphGroup = LGraphGroup
DragAndScale = DragAndScale
LGraphCanvas = LGraphCanvas
ContextMenu = ContextMenu
CurveEditor = CurveEditor
onNodeTypeRegistered?(type: string, base_class: typeof LGraphNode): void
onNodeTypeReplaced?(type: string, base_class: typeof LGraphNode, prev: unknown): void
// Avoid circular dependency from original single-module
static {
LGraphCanvas.link_type_colors = {
"-1": LiteGraphGlobal.DEFAULT_EVENT_LINK_COLOR,
number: "#AAA",
node: "#DCA"
}
}
constructor() {
//timer that works everywhere
if (typeof performance != "undefined") {
this.getTime = performance.now.bind(performance)
} else if (typeof Date != "undefined" && Date.now) {
this.getTime = Date.now.bind(Date)
} else if (typeof process != "undefined") {
this.getTime = function () {
const t = process.hrtime()
return t[0] * 0.001 + t[1] * 1e-6
}
} else {
this.getTime = function () {
return new Date().getTime()
}
}
}
/**
* Register a node class so it can be listed when the user wants to create a new one
* @param {String} type name of the node and path
* @param {Class} 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) {
base_class.prototype[i] ||= LGraphNode.prototype[i]
}
const prev = this.registered_node_types[type]
if (prev) {
console.log("replacing node type: " + type)
}
if (!Object.prototype.hasOwnProperty.call(base_class.prototype, "shape")) {
Object.defineProperty(base_class.prototype, "shape", {
set(this: LGraphNode, v: RenderShape | "default" | "box" | "round" | "circle" | "card") {
switch (v) {
case "default":
delete this._shape
break
case "box":
this._shape = RenderShape.BOX
break
case "round":
this._shape = RenderShape.ROUND
break
case "circle":
this._shape = RenderShape.CIRCLE
break
case "card":
this._shape = RenderShape.CARD
break
default:
this._shape = v
}
},
get() {
return this._shape
},
enumerable: true,
configurable: true
})
//used to know which nodes to create when dragging files to the canvas
if (base_class.supported_extensions) {
for (const i in base_class.supported_extensions) {
const ext = base_class.supported_extensions[i]
if (ext && typeof ext === "string") {
this.node_types_by_file_extension[ext.toLowerCase()] = base_class
}
}
}
}
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 {String|Object} 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: " + type
delete this.registered_node_types[base_class.type]
const name = base_class.constructor.name
if (name) delete this.Nodes[name]
}
/**
* Save a slot type and his node
* @param {String|Object} type name of the node or the node constructor itself
* @param {String} 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 i = 0; i < allTypes.length; ++i) {
let slotType = allTypes[i]
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()
}
}
}
/**
* Create a new nodetype by passing a function, it wraps it with a proper class and generates inputs according to the parameters of the function.
* Useful to wrap simple methods that do not require properties, and that only process some input to generate an output.
* @param {String} name node name with namespace (p.e.: 'math/sum')
* @param {Function} func
* @param {Array} param_types [optional] an array containing the type of every parameter, otherwise parameters will accept any type
* @param {String} return_type [optional] string with the return type, otherwise it will be generic
* @param {Object} properties [optional] properties to be configurable
*/
wrapFunctionAsNode(
name: string,
func: (...args: any) => any,
param_types: string[],
return_type: string,
properties: unknown
) {
const params = Array(func.length)
let code = ""
const names = this.getParameterNames(func)
for (let i = 0; i < names.length; ++i) {
code += `this.addInput('${names[i]}',${param_types && param_types[i] ? `'${param_types[i]}'` : "0"});\n`
}
code += `this.addOutput('out',${return_type ? `'${return_type}'` : 0});\n`
if (properties) code += `this.properties = ${JSON.stringify(properties)};\n`
const classobj = Function(code)
// @ts-ignore
classobj.title = name.split("/").pop()
// @ts-ignore
classobj.desc = "Generated from " + func.name
classobj.prototype.onExecute = function onExecute() {
for (let i = 0; i < params.length; ++i) {
params[i] = this.getInputData(i)
}
const r = func.apply(this, params)
this.setOutputData(0, r)
}
// @ts-expect-error Required to make this kludge work
this.registerNodeType(name, classobj)
}
/**
* Removes all previously registered node's types
*/
clearRegisteredTypes(): void {
this.registered_node_types = {}
this.node_types_by_file_extension = {}
this.Nodes = {}
this.searchbox_extras = {}
}
/**
* Adds this method to all nodetypes, existing and to be created
* (You can add it to LGraphNode.prototype but then existing node types wont have it)
* @param {Function} func
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
addNodeMethod(name: string, func: Function): void {
LGraphNode.prototype[name] = func
for (const i in this.registered_node_types) {
const type = this.registered_node_types[i]
//keep old in case of replacing
if (type.prototype[name]) type.prototype["_" + name] = type.prototype[name]
type.prototype[name] = func
}
}
/**
* Create a node of a given type with a name. The node is not attached to any graph yet.
* @param {String} type full name of the node class. p.e. "math/sin"
* @param {String} name a name to distinguish from other nodes
* @param {Object} options to set options
*/
createNode(type: string, title?: string, options?: Dictionary<unknown>): LGraphNode {
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 (err) {
console.error(err)
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.concat()
node.mode ||= LGraphEventMode.ALWAYS
//extra options
if (options) {
for (const i in options) {
node[i] = options[i]
}
}
// callback
node.onNodeCreated?.()
return node
}
/**
* Returns a registered node type with a given name
* @param {String} type full name of the node class. p.e. "math/sin"
* @return {Class} the node class
*/
getNodeType(type: string): typeof LGraphNode {
return this.registered_node_types[type]
}
/**
* Returns a list of node types matching one category
* @param {String} category category name
* @return {Array} array with all the node classes
*/
getNodeTypesInCategory(category: string, filter: any) {
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)
}
}
if (this.auto_sort_node_types) {
r.sort(function (a, b) {
return a.title.localeCompare(b.title)
})
}
return r
}
/**
* Returns a list with all the node type categories
* @param {String} filter only nodes with ctor.filter equal can be shown
* @return {Array} array with all the names of the categories
*/
getNodeTypesCategories(filter: string): string[] {
const categories = { "": 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 this.auto_sort_node_types ? result.sort() : 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 (let i = 0; i < tmp.length; i++) {
script_files.push(tmp[i])
}
const docHeadObj = document.getElementsByTagName("head")[0]
folder_wildcard = document.location.href + folder_wildcard
for (let i = 0; i < script_files.length; i++) {
const src = script_files[i].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.appendChild(dynamicScript)
docHeadObj.removeChild(script_files[i])
} catch (err) {
if (this.throw_errors) throw err
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
cloneObject<T extends object>(obj: T, target?: T): T {
if (obj == null) return null
const r = JSON.parse(JSON.stringify(obj))
if (!target) return r
for (const i in r) {
target[i] = r[i]
}
return target
}
/*
* https://gist.github.com/jed/982883?permalink_comment_id=852670#gistcomment-852670
*/
uuidv4(): string {
// @ts-ignore
return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, a => (a ^ Math.random() * 16 >> a / 4).toString(16))
}
/**
* Returns if the types of two slots are compatible (taking into account wildcards, etc)
* @param {String} type_a output
* @param {String} type_b input
* @return {Boolean} 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.indexOf(",") == -1 && type_b.indexOf(",") == -1)
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 (let i = 0; i < supported_types_a.length; ++i) {
for (let j = 0; j < supported_types_b.length; ++j) {
if (this.isValidConnection(supported_types_a[i], supported_types_b[j]))
return true
}
}
return false
}
/**
* Register a string in the search box so when the user types it it will recommend this node
* @param {String} node_type the node recommended
* @param {String} description text to show next to it
* @param {Object} data it could contain info of how the node should be configured
* @return {Boolean} true if they can be connected
*/
registerSearchboxExtra(node_type: any, description: string, data: any): void {
this.searchbox_extras[description.toLowerCase()] = {
type: node_type,
desc: description,
data: data
}
}
/**
* Wrapper to load files (from url using fetch or from file using FileReader)
* @param {String|File|Blob} url the url of the file (or the file itself)
* @param {String} type an string to know how to fetch it: "text","arraybuffer","json","blob"
* @param {Function} on_complete callback(data)
* @param {Function} on_error in case of an error
* @return {FileReader|Promise} returns the object used to
*/
fetchFile(url: string | URL | Request | Blob, type: string, on_complete: (data: string | ArrayBuffer) => void, on_error: (error: unknown) => void): void | Promise<void> {
if (!url) return null
type = type || "text"
if (typeof url === "string") {
if (url.substr(0, 4) == "http" && this.proxy)
url = this.proxy + url.substr(url.indexOf(":") + 3)
return fetch(url)
.then(function (response) {
if (!response.ok)
throw new Error("File not found") //it will be catch below
if (type == "arraybuffer")
return response.arrayBuffer()
else if (type == "text" || type == "string")
return response.text()
else if (type == "json")
return response.json()
else if (type == "blob")
return response.blob()
})
.then(function (data: string | ArrayBuffer): void {
on_complete?.(data)
})
.catch(function (error) {
console.error("error fetching file:", url)
on_error?.(error)
})
} else if (url instanceof File || url instanceof Blob) {
const reader = new FileReader()
reader.onload = function (e) {
let v = e.target.result
if (type == "json")
// @ts-ignore
v = JSON.parse(v)
on_complete?.(v)
}
if (type == "arraybuffer")
return reader.readAsArrayBuffer(url)
else if (type == "text" || type == "json")
return reader.readAsText(url)
else if (type == "blob")
return reader.readAsBinaryString(url)
}
return null
}
//used to create nodes from wrapping functions
getParameterNames(func: (...args: any) => any): string[] {
return (func + "")
.replace(/[/][/].*$/gm, "") // strip single-line comments
.replace(/\s+/g, "") // strip white space
.replace(/[/][*][^/*]*[*][/]/g, "") // strip multi-line comments /**/
.split("){", 1)[0]
.replace(/^[^(]*[(]/, "") // extract the parameters
.replace(/=[^,]+/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
compareObjects(a: object, b: object): boolean {
for (const i in a) {
if (a[i] != b[i]) return false
}
return true
}
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 result = []
for (let i = 0; i < elements.length; i++) {
result.push(elements[i])
}
for (let i = 0; i < result.length; i++) {
if (result[i].close) {
result[i].close()
} else if (result[i].parentNode) {
result[i].parentNode.removeChild(result[i])
}
}
}
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)
)
}
}
}
}
}