package zui; using zui.GraphicsExtension; @:access(zui.Zui) class Nodes { public var nodesDrag = false; public var nodesSelected: Array = []; public var panX = 0.0; public var panY = 0.0; public var zoom = 1.0; public var uiw = 0; public var uih = 0; public var _inputStarted = false; public var colorPickerCallback: kha.Color->Void = null; var scaleFactor = 1.0; var ELEMENT_H = 25; var dragged = false; var moveOnTop: TNode = null; var linkDrag: TNodeLink = null; var isNewLink = false; var snapFromId = -1; var snapToId = -1; var snapSocket = 0; var snapX = 0.0; var snapY = 0.0; var handle = new Zui.Handle(); static var elementsBaked = false; static var socketImage: kha.Image = null; static var socketReleased = false; static var clipboard = ""; static var boxSelect = false; static var boxSelectX = 0; static var boxSelectY = 0; static inline var maxButtons = 9; public static var excludeRemove: Array = []; // No removal for listed node types public static var onLinkDrag: TNodeLink->Bool->Void = null; public static var onHeaderReleased: TNode->Void = null; public static var onSocketReleased: TNodeSocket->Void = null; public static var onCanvasReleased: Void->Void = null; public static var onNodeRemove: TNode->Void = null; public static var onCanvasControl: Void->CanvasControl = null; // Pan, zoom #if zui_translate public static dynamic function tr(id: String, vars: Map = null) { return id; } #else public static inline function tr(id: String, vars: Map = null) { return id; } #end public function new() {} public inline function SCALE(): Float { return scaleFactor * zoom; } public inline function PAN_X(): Float { var zoomPan = (1.0 - zoom) * uiw / 2.5; return panX * SCALE() + zoomPan; } public inline function PAN_Y(): Float { var zoomPan = (1.0 - zoom) * uih / 2.5; return panY * SCALE() + zoomPan; } public inline function LINE_H(): Int { return Std.int(ELEMENT_H * SCALE()); } function BUTTONS_H(node: TNode): Int { var h = 0.0; for (but in node.buttons) { if (but.type == "RGBA") h += 102 * SCALE() + LINE_H() * 5; // Color wheel + controls else if (but.type == "VECTOR") h += LINE_H() * 4; else if (but.type == "CUSTOM") h += LINE_H() * but.height; else h += LINE_H(); } return Std.int(h); } function OUTPUTS_H(sockets: Array, length: Null = null): Int { var h = 0.0; for (i in 0...(length == null ? sockets.length : length)) { h += LINE_H(); } return Std.int(h); } function INPUTS_H(canvas: TNodeCanvas, sockets: Array, length: Null = null): Int { var h = 0.0; for (i in 0...(length == null ? sockets.length : length)) { if (sockets[i].type == "VECTOR" && sockets[i].display == 1 && !inputLinked(canvas, sockets[i].node_id, i)) h += LINE_H() * 4; else h += LINE_H(); } return Std.int(h); } inline function NODE_H(canvas: TNodeCanvas, node: TNode): Int { return Std.int(LINE_H() * 1.2 + INPUTS_H(canvas, node.inputs) + OUTPUTS_H(node.outputs) + BUTTONS_H(node)); } inline function NODE_W(node: TNode): Int { return Std.int((node.width != null ? node.width : 140) * SCALE()); } inline function NODE_X(node: TNode): Float { return node.x * SCALE() + PAN_X(); } inline function NODE_Y(node: TNode): Float { return node.y * SCALE() + PAN_Y(); } inline function INPUT_Y(canvas: TNodeCanvas, sockets: Array, pos: Int): Int { return Std.int(LINE_H() * 1.62) + INPUTS_H(canvas, sockets, pos); } inline function OUTPUT_Y(sockets: Array, pos: Int): Int { return Std.int(LINE_H() * 1.62) + OUTPUTS_H(sockets, pos); } public inline function p(f: Float): Float { return f * SCALE(); } public function getNode(nodes: Array, id: Int): TNode { for (node in nodes) if (node.id == id) return node; return null; } var nodeId = -1; public function getNodeId(nodes: Array): Int { if (nodeId == -1) for (n in nodes) if (nodeId < n.id) nodeId = n.id; return ++nodeId; } public function getLinkId(links: Array): Int { var id = 0; for (l in links) if (l.id >= id) id = l.id + 1; return id; } public function getSocketId(nodes: Array): Int { var id = 0; for (n in nodes) { for (s in n.inputs) if (s.id >= id) id = s.id + 1; for (s in n.outputs) if (s.id >= id) id = s.id + 1; } return id; } function inputLinked(canvas: TNodeCanvas, node_id: Int, i: Int): Bool { for (l in canvas.links) if (l.to_id == node_id && l.to_socket == i) return true; return false; } function bakeElements(ui: Zui) { ui.g.end(); elementsBaked = true; socketImage = kha.Image.createRenderTarget(24, 24); var g = socketImage.g2; g.begin(true, 0x00000000); g.color = 0xff000000; zui.GraphicsExtension.fillCircle(g, 12, 12, 12); g.color = 0xffffffff; zui.GraphicsExtension.fillCircle(g, 12, 12, 9); g.end(); ui.g.begin(false); } public function nodeCanvas(ui: Zui, canvas: TNodeCanvas) { if (!elementsBaked) bakeElements(ui); var wx = ui._windowX; var wy = ui._windowY; var _inputEnabled = ui.inputEnabled; ui.inputEnabled = _inputEnabled && popupCommands == null; var controls = onCanvasControl != null ? onCanvasControl() : { panX: ui.inputDownR ? ui.inputDX : 0.0, panY: ui.inputDownR ? ui.inputDY : 0.0, zoom: -ui.inputWheelDelta / 10.0 }; socketReleased = false; // Pan canvas if (ui.inputEnabled && (controls.panX != 0.0 || controls.panY != 0.0)) { panX += controls.panX / SCALE(); panY += controls.panY / SCALE(); } // Zoom canvas if (ui.inputEnabled && controls.zoom != 0.0) { zoom += controls.zoom; if (zoom < 0.1) zoom = 0.1; else if (zoom > 1.0) zoom = 1.0; zoom = Math.round(zoom * 10) / 10; uiw = ui._w; uih = ui._h; } scaleFactor = ui.SCALE(); ELEMENT_H = ui.t.ELEMENT_H + 2; ui.setScale(SCALE()); // Apply zoomed scale ui.elementsBaked = true; ui.g.font = ui.ops.font; ui.g.fontSize = ui.fontSize; for (link in canvas.links) { var from = getNode(canvas.nodes, link.from_id); var to = getNode(canvas.nodes, link.to_id); var fromX = from == null ? ui.inputX : wx + NODE_X(from) + NODE_W(from); var fromY = from == null ? ui.inputY : wy + NODE_Y(from) + OUTPUT_Y(from.outputs, link.from_socket); var toX = to == null ? ui.inputX : wx + NODE_X(to); var toY = to == null ? ui.inputY : wy + NODE_Y(to) + INPUT_Y(canvas, to.inputs, link.to_socket) + OUTPUTS_H(to.outputs) + BUTTONS_H(to); // Cull var left = toX > fromX ? fromX : toX; var right = toX > fromX ? toX : fromX; var top = toY > fromY ? fromY : toY; var bottom = toY > fromY ? toY : fromY; if (right < 0 || left > wx + ui._windowW || bottom < 0 || top > wy + ui._windowH) { continue; } // Snap to nearest socket if (linkDrag == link) { if (snapFromId != -1) { fromX = snapX; fromY = snapY; } if (snapToId != -1) { toX = snapX; toY = snapY; } snapFromId = snapToId = -1; for (node in canvas.nodes) { var inps = node.inputs; var outs = node.outputs; var nodeh = NODE_H(canvas, node); var rx = wx + NODE_X(node) - LINE_H() / 2; var ry = wy + NODE_Y(node) - LINE_H() / 2; var rw = NODE_W(node) + LINE_H(); var rh = nodeh + LINE_H(); if (ui.getInputInRect(rx, ry, rw, rh)) { if (from == null && node.id != to.id) { // Snap to output for (i in 0...outs.length) { var sx = wx + NODE_X(node) + NODE_W(node); var sy = wy + NODE_Y(node) + OUTPUT_Y(outs, i); var rx = sx - LINE_H() / 2; var ry = sy - LINE_H() / 2; if (ui.getInputInRect(rx, ry, LINE_H(), LINE_H())) { snapX = sx; snapY = sy; snapFromId = node.id; snapSocket = i; break; } } } else if (to == null && node.id != from.id) { // Snap to input for (i in 0...inps.length) { var sx = wx + NODE_X(node); var sy = wy + NODE_Y(node) + INPUT_Y(canvas, inps, i) + OUTPUTS_H(outs) + BUTTONS_H(node); var rx = sx - LINE_H() / 2; var ry = sy - LINE_H() / 2; if (ui.getInputInRect(rx, ry, LINE_H(), LINE_H())) { snapX = sx; snapY = sy; snapToId = node.id; snapSocket = i; break; } } } } } } var selected = false; for (n in nodesSelected) { if (link.from_id == n.id || link.to_id == n.id) { selected = true; break; } } drawLink(ui, fromX - wx, fromY - wy, toX - wx, toY - wy, selected); } for (node in canvas.nodes) { // Cull if (NODE_X(node) > ui._windowW || NODE_X(node) + NODE_W(node) < 0 || NODE_Y(node) > ui._windowH || NODE_Y(node) + NODE_H(canvas, node) < 0) { if (!isSelected(node)) continue; } var inps = node.inputs; var outs = node.outputs; // Drag node var nodeh = NODE_H(canvas, node); if (ui.inputEnabled && ui.getInputInRect(wx + NODE_X(node) - LINE_H() / 2, wy + NODE_Y(node), NODE_W(node) + LINE_H(), LINE_H())) { if (ui.inputStarted) { if (ui.isShiftDown || ui.isCtrlDown) { // Add to selection or deselect isSelected(node) ? nodesSelected.remove(node) : nodesSelected.push(node); } else if (nodesSelected.length <= 1) { // Selecting single node, otherwise wait for input release nodesSelected = [node]; } moveOnTop = node; // Place selected node on top nodesDrag = true; dragged = false; } else if (ui.inputReleased && !ui.isShiftDown && !ui.isCtrlDown && !dragged) { // No drag performed, select single node nodesSelected = [node]; if (onHeaderReleased != null) { onHeaderReleased(node); } } } if (ui.inputStarted && ui.getInputInRect(wx + NODE_X(node) - LINE_H() / 2, wy + NODE_Y(node) - LINE_H() / 2, NODE_W(node) + LINE_H(), nodeh + LINE_H())) { // Check sockets if (linkDrag == null) { for (i in 0...outs.length) { var sx = wx + NODE_X(node) + NODE_W(node); var sy = wy + NODE_Y(node) + OUTPUT_Y(outs, i); if (ui.getInputInRect(sx - LINE_H() / 2, sy - LINE_H() / 2, LINE_H(), LINE_H())) { // New link from output var l: TNodeLink = { id: getLinkId(canvas.links), from_id: node.id, from_socket: i, to_id: -1, to_socket: -1 }; canvas.links.push(l); linkDrag = l; isNewLink = true; break; } } } if (linkDrag == null) { for (i in 0...inps.length) { var sx = wx + NODE_X(node); var sy = wy + NODE_Y(node) + INPUT_Y(canvas, inps, i) + OUTPUTS_H(outs) + BUTTONS_H(node); if (ui.getInputInRect(sx - LINE_H() / 2, sy - LINE_H() / 2, LINE_H(), LINE_H())) { // Already has a link - disconnect for (l in canvas.links) { if (l.to_id == node.id && l.to_socket == i) { l.to_id = l.to_socket = -1; linkDrag = l; isNewLink = false; break; } } if (linkDrag != null) break; // New link from input var l: TNodeLink = { id: getLinkId(canvas.links), from_id: -1, from_socket: -1, to_id: node.id, to_socket: i }; canvas.links.push(l); linkDrag = l; isNewLink = true; break; } } } } else if (ui.inputReleased) { if (snapToId != -1) { // Connect to input // Force single link per input for (l in canvas.links) { if (l.to_id == snapToId && l.to_socket == snapSocket) { canvas.links.remove(l); break; } } linkDrag.to_id = snapToId; linkDrag.to_socket = snapSocket; ui.changed = true; } else if (snapFromId != -1) { // Connect to output linkDrag.from_id = snapFromId; linkDrag.from_socket = snapSocket; ui.changed = true; } else if (linkDrag != null) { // Remove dragged link canvas.links.remove(linkDrag); ui.changed = true; if (onLinkDrag != null) { onLinkDrag(linkDrag, isNewLink); } } snapToId = snapFromId = -1; linkDrag = null; nodesDrag = false; } if (nodesDrag && isSelected(node) && !ui.inputDownR) { if (ui.inputDX != 0 || ui.inputDY != 0) { dragged = true; node.x += Std.int(ui.inputDX / SCALE()); node.y += Std.int(ui.inputDY / SCALE()); } } drawNode(ui, node, canvas); } if (onCanvasReleased != null && ui.inputEnabled && (ui.inputReleased || ui.inputReleasedR) && !socketReleased) { onCanvasReleased(); } if (boxSelect) { ui.g.color = 0x223333dd; ui.g.fillRect(boxSelectX, boxSelectY, ui.inputX - boxSelectX - ui._windowX, ui.inputY - boxSelectY - ui._windowY); ui.g.color = 0x773333dd; ui.g.drawRect(boxSelectX, boxSelectY, ui.inputX - boxSelectX - ui._windowX, ui.inputY - boxSelectY - ui._windowY); ui.g.color = 0xffffffff; } if (ui.inputEnabled && ui.inputStarted && !ui.isAltDown && linkDrag == null && !nodesDrag && !ui.changed) { boxSelect = true; boxSelectX = Std.int(ui.inputX - ui._windowX); boxSelectY = Std.int(ui.inputY - ui._windowY); } else if (boxSelect && !ui.inputDown) { boxSelect = false; var nodes: Array = []; var left = boxSelectX; var top = boxSelectY; var right = Std.int(ui.inputX - ui._windowX); var bottom = Std.int(ui.inputY - ui._windowY); if (left > right) { var t = left; left = right; right = t; } if (top > bottom) { var t = top; top = bottom; bottom = t; } for (n in canvas.nodes) { if (NODE_X(n) + NODE_W(n) > left && NODE_X(n) < right && NODE_Y(n) + NODE_H(canvas, n) > top && NODE_Y(n) < bottom) { nodes.push(n); } } (ui.isShiftDown || ui.isCtrlDown) ? for (n in nodes) nodesSelected.push(n) : nodesSelected = nodes; } // Place selected node on top if (moveOnTop != null) { canvas.nodes.remove(moveOnTop); canvas.nodes.push(moveOnTop); moveOnTop = null; } // Node copy & paste var cutSelected = false; if (Zui.isCopy) { var copyNodes: Array = []; for (n in nodesSelected) { if (excludeRemove.indexOf(n.type) >= 0) continue; copyNodes.push(n); } var copyLinks: Array = []; for (l in canvas.links) { var from = getNode(nodesSelected, l.from_id); var to = getNode(nodesSelected, l.to_id); if (from != null && excludeRemove.indexOf(from.type) == -1 && to != null && excludeRemove.indexOf(to.type) == -1) { copyLinks.push(l); } } if (copyNodes.length > 0) { var copyCanvas: TNodeCanvas = { name: canvas.name, nodes: copyNodes, links: copyLinks }; clipboard = haxe.Json.stringify(copyCanvas); } cutSelected = Zui.isCut; } if (Zui.isPaste && !ui.isTyping) { var pasteCanvas: TNodeCanvas = null; // Clipboard can contain non-json data try { pasteCanvas = haxe.Json.parse(clipboard); } catch(_) {} if (pasteCanvas != null) { for (l in pasteCanvas.links) { // Assign unique link id l.id = getLinkId(canvas.links); canvas.links.push(l); } var offsetX = Std.int((Std.int(ui.inputX / ui.SCALE()) * SCALE() - wx - PAN_X()) / SCALE()) - pasteCanvas.nodes[pasteCanvas.nodes.length - 1].x; var offsetY = Std.int((Std.int(ui.inputY / ui.SCALE()) * SCALE() - wy - PAN_Y()) / SCALE()) - pasteCanvas.nodes[pasteCanvas.nodes.length - 1].y; for (n in pasteCanvas.nodes) { // Assign unique node id var old_id = n.id; n.id = getNodeId(canvas.nodes); for (soc in n.inputs) { soc.id = getSocketId(canvas.nodes); soc.node_id = n.id; } for (soc in n.outputs) { soc.id = getSocketId(canvas.nodes); soc.node_id = n.id; } for (l in pasteCanvas.links) { if (l.from_id == old_id) l.from_id = n.id; else if (l.to_id == old_id) l.to_id = n.id; } n.x += offsetX; n.y += offsetY; canvas.nodes.push(n); } nodesDrag = true; nodesSelected = pasteCanvas.nodes; ui.changed = true; } } // Select all nodes if (ui.isCtrlDown && ui.key == kha.input.KeyCode.A && !ui.isTyping) { nodesSelected = []; for (n in canvas.nodes) nodesSelected.push(n); } // Node removal if (ui.inputEnabled && (ui.isBackspaceDown || ui.isDeleteDown || cutSelected) && !ui.isTyping) { var i = nodesSelected.length - 1; while (i >= 0) { var n = nodesSelected[i--]; if (excludeRemove.indexOf(n.type) >= 0) continue; removeNode(n, canvas); ui.changed = true; } } ui.setScale(scaleFactor); // Restore non-zoomed scale ui.elementsBaked = true; ui.inputEnabled = _inputEnabled; if (popupCommands != null) { ui._x = popupX; ui._y = popupY; ui._w = popupW; ui.fill(-6, -6, ui._w / ui.SCALE() + 12, popupH + 12, ui.t.ACCENT_SELECT_COL); ui.fill(-5, -5, ui._w / ui.SCALE() + 10, popupH + 10, ui.t.SEPARATOR_COL); popupCommands(ui); var hide = (ui.inputStarted || ui.inputStartedR) && (ui.inputX - wx < popupX - 6 || ui.inputX - wx > popupX + popupW + 6 || ui.inputY - wy < popupY - 6 || ui.inputY - wy > popupY + popupH * ui.SCALE() + 6); if (hide || ui.isEscapeDown) { popupCommands = null; } } } // Retrieve combo items for buttons of type ENUM public static var enumTexts: String->Array = null; inline function isSelected(node: TNode): Bool { return nodesSelected.indexOf(node) >= 0; } public function drawNode(ui: Zui, node: TNode, canvas: TNodeCanvas) { var wx = ui._windowX; var wy = ui._windowY; var uiX = ui._x; var uiY = ui._y; var uiW = ui._w; var w = NODE_W(node); var g = ui.g; var h = NODE_H(canvas, node); var nx = NODE_X(node); var ny = NODE_Y(node); var text = tr(node.name); var lineh = LINE_H(); // Disallow input if node is overlapped by another node _inputStarted = ui.inputStarted; if (ui.inputStarted) { for (i in (canvas.nodes.indexOf(node) + 1)...canvas.nodes.length) { var n = canvas.nodes[i]; if (NODE_X(n) < ui.inputX - ui._windowX && NODE_X(n) + NODE_W(n) > ui.inputX - ui._windowX && NODE_Y(n) < ui.inputY - ui._windowY && NODE_Y(n) + NODE_H(canvas, n) > ui.inputY - ui._windowY) { ui.inputStarted = false; break; } } } // Outline g.color = isSelected(node) ? ui.t.LABEL_COL : ui.t.CONTEXT_COL; g.fillRect(nx - 1, ny - 1, w + 2, h + 2); // Body g.color = ui.t.WINDOW_BG_COL; g.fillRect(nx, ny, w, h); // Header line g.color = node.color; g.fillRect(nx, ny + lineh - p(3), w, p(3)); // Title g.color = ui.t.LABEL_COL; var textw = g.font.width(ui.fontSize, text); g.drawString(text, nx + p(10), ny + p(6)); ui._x = nx; // Use the whole line for hovering and not just the drawn string. ui._y = ny; ui._w = w; if (ui.getHover(lineh) && node.tooltip != null) ui.tooltip(tr(node.tooltip)); ny += lineh * 0.5; // Outputs for (out in node.outputs) { ny += lineh; g.color = out.color; g.drawScaledImage(socketImage, nx + w - p(6), ny - p(3), p(12), p(12)); } ny -= lineh * node.outputs.length; g.color = ui.t.LABEL_COL; for (out in node.outputs) { ny += lineh; var strw = ui.ops.font.width(ui.fontSize, tr(out.name)); g.drawString(tr(out.name), nx + w - strw - p(12), ny - p(3)); ui._x = nx; ui._y = ny; ui._w = w; if (ui.getHover(lineh) && out.tooltip != null) ui.tooltip(tr(out.tooltip)); if (onSocketReleased != null && ui.inputEnabled && (ui.inputReleased || ui.inputReleasedR)) { if (ui.inputX > wx + nx && ui.inputX < wx + nx + w && ui.inputY > wy + ny && ui.inputY < wy + ny + lineh) { onSocketReleased(out); socketReleased = true; } } } // Buttons var nhandle = handle.nest(node.id); ny -= lineh / 3; // Fix align for (buti in 0...node.buttons.length) { var but = node.buttons[buti]; if (but.type == "RGBA") { ny += lineh; // 18 + 2 separator ui._x = nx; ui._y = ny; ui._w = w; var val: kha.arrays.Float32Array = node.outputs[but.output].default_value; nhandle.color = kha.Color.fromFloats(val[0], val[1], val[2]); Ext.colorWheel(ui, nhandle, false, null, null, true, function () { colorPickerCallback = function (color: kha.Color) { node.outputs[but.output].default_value[0] = color.R; node.outputs[but.output].default_value[1] = color.G; node.outputs[but.output].default_value[2] = color.B; ui.changed = true; }; }); val[0] = nhandle.color.R; val[1] = nhandle.color.G; val[2] = nhandle.color.B; } else if (but.type == "VECTOR") { ny += lineh; ui._x = nx; ui._y = ny; ui._w = w; var min = but.min != null ? but.min : 0.0; var max = but.max != null ? but.max : 1.0; var textOff = ui.t.TEXT_OFFSET; ui.t.TEXT_OFFSET = 6; ui.text(tr(but.name)); if (ui.isHovered && but.tooltip != null) ui.tooltip(tr(but.tooltip)); var val: kha.arrays.Float32Array = but.default_value; val[0] = ui.slider(nhandle.nest(buti).nest(0, {value: val[0]}), "X", min, max, true, 100, true, Left); if (ui.isHovered && but.tooltip != null) ui.tooltip(tr(but.tooltip)); val[1] = ui.slider(nhandle.nest(buti).nest(1, {value: val[1]}), "Y", min, max, true, 100, true, Left); if (ui.isHovered && but.tooltip != null) ui.tooltip(tr(but.tooltip)); val[2] = ui.slider(nhandle.nest(buti).nest(2, {value: val[2]}), "Z", min, max, true, 100, true, Left); if (ui.isHovered && but.tooltip != null) ui.tooltip(tr(but.tooltip)); ui.t.TEXT_OFFSET = textOff; if (but.output != null) node.outputs[but.output].default_value = but.default_value; ny += lineh * 3; } else if (but.type == "VALUE") { ny += lineh; ui._x = nx; ui._y = ny; ui._w = w; var soc = node.outputs[but.output]; var min = but.min != null ? but.min : 0.0; var max = but.max != null ? but.max : 1.0; var prec = but.precision != null ? but.precision : 100.0; var textOff = ui.t.TEXT_OFFSET; ui.t.TEXT_OFFSET = 6; soc.default_value = ui.slider(nhandle.nest(buti, {value: soc.default_value}), "Value", min, max, true, prec, true, Left); if (ui.isHovered && but.tooltip != null) ui.tooltip(tr(but.tooltip)); ui.t.TEXT_OFFSET = textOff; } else if (but.type == "STRING") { ny += lineh; ui._x = nx; ui._y = ny; ui._w = w; var soc = but.output != null ? node.outputs[but.output] : null; but.default_value = ui.textInput(nhandle.nest(buti, {text: soc != null ? soc.default_value : but.default_value != null ? but.default_value : ""}), tr(but.name)); if (soc != null) soc.default_value = but.default_value; if (ui.isHovered && but.tooltip != null) ui.tooltip(tr(but.tooltip)); } else if (but.type == "ENUM") { ny += lineh; ui._x = nx; ui._y = ny; ui._w = w; var texts = Std.isOfType(but.data, Array) ? [for (s in cast(but.data, Array)) tr(s)] : enumTexts(node.type); var buthandle = nhandle.nest(buti); buthandle.position = but.default_value; but.default_value = ui.combo(buthandle, texts, tr(but.name)); if (ui.isHovered && but.tooltip != null) ui.tooltip(tr(but.tooltip)); } else if (but.type == "BOOL") { ny += lineh; ui._x = nx; ui._y = ny; ui._w = w; but.default_value = ui.check(nhandle.nest(buti, {selected: but.default_value}), tr(but.name)); if (ui.isHovered && but.tooltip != null) ui.tooltip(tr(but.tooltip)); } else if (but.type == "CUSTOM") { // Calls external function for custom button drawing ny += lineh; ui._x = nx; ui._y = ny; ui._w = w; var dot = but.name.lastIndexOf("."); // TNodeButton.name specifies external function path var fn = Reflect.field(Type.resolveClass(but.name.substr(0, dot)), but.name.substr(dot + 1)); fn(ui, this, node); ny += lineh * (but.height - 1); // TNodeButton.height specifies vertical button size } } ny += lineh / 3; // Fix align // Inputs for (i in 0...node.inputs.length) { var inp = node.inputs[i]; ny += lineh; g.color = inp.color; g.drawScaledImage(socketImage, nx - p(6), ny - p(3), p(12), p(12)); var isLinked = inputLinked(canvas, node.id, i); if (!isLinked && inp.type == "VALUE") { ui._x = nx + p(6); ui._y = ny - p(9); ui._w = Std.int(w - p(6)); var soc = inp; var min = soc.min != null ? soc.min : 0.0; var max = soc.max != null ? soc.max : 1.0; var prec = soc.precision != null ? soc.precision : 100.0; var textOff = ui.t.TEXT_OFFSET; ui.t.TEXT_OFFSET = 6; soc.default_value = ui.slider(nhandle.nest(maxButtons).nest(i, {value: soc.default_value}), tr(inp.name), min, max, true, prec, true, Left); if (ui.isHovered && inp.tooltip != null) ui.tooltip(tr(inp.tooltip)); ui.t.TEXT_OFFSET = textOff; } else if (!isLinked && inp.type == "STRING") { ui._x = nx + p(6); ui._y = ny - p(9); ui._w = Std.int(w - p(6)); var soc = inp; var textOff = ui.t.TEXT_OFFSET; ui.t.TEXT_OFFSET = 6; soc.default_value = ui.textInput(nhandle.nest(maxButtons).nest(i, {text: soc.default_value}), tr(inp.name), Left); if (ui.isHovered && inp.tooltip != null) ui.tooltip(tr(inp.tooltip)); ui.t.TEXT_OFFSET = textOff; } else if (!isLinked && inp.type == "RGBA") { g.color = ui.t.LABEL_COL; g.drawString(tr(inp.name), nx + p(12), ny - p(3)); ui._x = nx; ui._y = ny; ui._w = w; if (ui.getHover(lineh) && inp.tooltip != null) ui.tooltip(tr(inp.tooltip)); var soc = inp; g.color = 0xff000000; g.fillRect(nx + w - p(38), ny - p(6), p(36), p(18)); var val: kha.arrays.Float32Array = soc.default_value; g.color = kha.Color.fromFloats(val[0], val[1], val[2]); var rx = nx + w - p(37); var ry = ny - p(5); var rw = p(34); var rh = p(16); g.fillRect(rx, ry, rw, rh); var ix = ui.inputX - wx; var iy = ui.inputY - wy; if (ui.inputStarted && ix > rx && iy > ry && ix < rx + rw && iy < ry + rh) { _inputStarted = ui.inputStarted = false; rgbaPopup(ui, nhandle, soc.default_value, Std.int(rx), Std.int(ry + ui.ELEMENT_H())); } } else if (!isLinked && inp.type == "VECTOR" && inp.display == 1) { g.color = ui.t.LABEL_COL; g.drawString(tr(inp.name), nx + p(12), ny - p(3)); ui._x = nx; ui._y = ny; ui._w = w; if (ui.getHover(lineh) && inp.tooltip != null) ui.tooltip(tr(inp.tooltip)); ny += lineh / 2; ui._y = ny; var min = inp.min != null ? inp.min : 0.0; var max = inp.max != null ? inp.max : 1.0; var textOff = ui.t.TEXT_OFFSET; ui.t.TEXT_OFFSET = 6; var val: kha.arrays.Float32Array = inp.default_value; val[0] = ui.slider(nhandle.nest(maxButtons).nest(i).nest(0, {value: val[0]}), "X", min, max, true, 100, true, Left); if (ui.isHovered && inp.tooltip != null) ui.tooltip(tr(inp.tooltip)); val[1] = ui.slider(nhandle.nest(maxButtons).nest(i).nest(1, {value: val[1]}), "Y", min, max, true, 100, true, Left); if (ui.isHovered && inp.tooltip != null) ui.tooltip(tr(inp.tooltip)); val[2] = ui.slider(nhandle.nest(maxButtons).nest(i).nest(2, {value: val[2]}), "Z", min, max, true, 100, true, Left); if (ui.isHovered && inp.tooltip != null) ui.tooltip(tr(inp.tooltip)); ui.t.TEXT_OFFSET = textOff; ny += lineh * 2.5; } else { g.color = ui.t.LABEL_COL; g.drawString(tr(inp.name), nx + p(12), ny - p(3)); ui._x = nx; ui._y = ny; ui._w = w; if (ui.getHover(lineh) && inp.tooltip != null) ui.tooltip(tr(inp.tooltip)); } if (onSocketReleased != null && ui.inputEnabled && (ui.inputReleased || ui.inputReleasedR)) { if (ui.inputX > wx + nx && ui.inputX < wx + nx + w && ui.inputY > wy + ny && ui.inputY < wy + ny + lineh) { onSocketReleased(inp); socketReleased = true; } } } ui._x = uiX; ui._y = uiY; ui._w = uiW; ui.inputStarted = _inputStarted; } public function rgbaPopup(ui: Zui, nhandle: zui.Zui.Handle, val: kha.arrays.Float32Array, x: Int, y: Int) { popup(x, y, Std.int(140 * scaleFactor), Std.int(ui.t.ELEMENT_H * 10), function(ui: Zui) { nhandle.color = kha.Color.fromFloats(val[0], val[1], val[2]); Ext.colorWheel(ui, nhandle, false, null, true, function () { colorPickerCallback = function (color: kha.Color) { val[0] = color.R; val[1] = color.G; val[2] = color.B; ui.changed = true; }; }); val[0] = nhandle.color.R; val[1] = nhandle.color.G; val[2] = nhandle.color.B; }); } public function drawLink(ui: Zui, x1: Float, y1: Float, x2: Float, y2: Float, highlight: Bool = false) { var g = ui.g; var c1: kha.Color = ui.t.LABEL_COL; var c2: kha.Color = ui.t.ACCENT_SELECT_COL; g.color = highlight ? kha.Color.fromBytes(c1.Rb, c1.Gb, c1.Bb, 210) : kha.Color.fromBytes(c2.Rb, c2.Gb, c2.Bb, 210); if (ui.t.LINK_STYLE == Line) { g.drawLine(x1, y1, x2, y2, 1.0); g.color = highlight ? kha.Color.fromBytes(c1.Rb, c1.Gb, c1.Bb, 150) : kha.Color.fromBytes(c2.Rb, c2.Gb, c2.Bb, 150); // AA g.drawLine(x1 + 0.5, y1, x2 + 0.5, y2, 1.0); g.drawLine(x1 - 0.5, y1, x2 - 0.5, y2, 1.0); g.drawLine(x1, y1 + 0.5, x2, y2 + 0.5, 1.0); g.drawLine(x1, y1 - 0.5, x2, y2 - 0.5, 1.0); } else if (ui.t.LINK_STYLE == CubicBezier) { g.drawCubicBezier([x1, x1 + Math.abs(x1 - x2) / 2, x2 - Math.abs(x1 - x2) / 2, x2], [y1, y1, y2, y2], 30, highlight ? 2.0 : 1.0); } } public function removeNode(n: TNode, canvas: TNodeCanvas) { if (n == null) return; var i = 0; while (i < canvas.links.length) { var l = canvas.links[i]; if (l.from_id == n.id || l.to_id == n.id) { canvas.links.splice(i, 1); } else i++; } canvas.nodes.remove(n); if (onNodeRemove != null) { onNodeRemove(n); } } var popupX = 0; var popupY = 0; var popupW = 0; var popupH = 0; var popupCommands: Zui->Void = null; function popup(x: Int, y: Int, w: Int, h: Int, commands: Zui->Void) { popupX = x; popupY = y; popupW = w; popupH = h; popupCommands = commands; } } typedef CanvasControl = { var panX: Float; var panY: Float; var zoom: Float; } typedef TNodeCanvas = { var name: String; var nodes: Array; var links: Array; } typedef TNode = { var id: Int; var name: String; var type: String; var x: Float; var y: Float; var inputs: Array; var outputs: Array; var buttons: Array; var color: Int; @:optional var width: Null; @:optional var tooltip: String; } typedef TNodeSocket = { var id: Int; var node_id: Int; var name: String; var type: String; var color: Int; var default_value: Dynamic; @:optional var min: Null; @:optional var max: Null; @:optional var precision: Null; @:optional var display: Null; @:optional var tooltip: String; } typedef TNodeLink = { var id: Int; var from_id: Int; var from_socket: Int; var to_id: Int; var to_socket: Int; } typedef TNodeButton = { var name: String; var type: String; @:optional var output: Null; @:optional var default_value: Dynamic; @:optional var data: Dynamic; @:optional var min: Null; @:optional var max: Null; @:optional var precision: Null; @:optional var height: Null; @:optional var tooltip: String; }