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 = {} //nodetypes by string node_types_by_file_extension = {} //used for dropping files in the canvas Nodes: Record = {} //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 = {} // slot types for nodeclass registered_slot_out_types: Record = {} // slot types for nodeclass slot_types_in: string[] = [] // slot types IN slot_types_out: string[] = [] // slot types OUT slot_types_default_in: Record = {} // 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 = {} // 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): 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(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 { 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) ) } } } } }