//+------------------------------------------------------------------+ //| OMV Panel.mqh | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #ifndef OMV_PANEL_MQH #define OMV_PANEL_MQH #include "OMV Canvas.mqh" #include "OMV Model.mqh" //+------------------------------------------------------------------+ //| Panel size bounds | //+------------------------------------------------------------------+ #define OP_W 660 // Default panel width #define OP_H 440 // Default panel height #define OP_W_MIN 480 // Narrowest the panel may be dragged #define OP_W_MAX 1100 // Widest the panel may be dragged #define OP_H_MIN 320 // Shortest the panel may be dragged #define OP_H_MAX 800 // Tallest the panel may be dragged //+------------------------------------------------------------------+ //| Band heights | //+------------------------------------------------------------------+ #define OP_HEADER_H 44 // Header band holding the title and icons #define OP_STAT_H 48 // Statistics strip under the header #define OP_FOOT_H 34 // Footer band holding the view controls #define OP_GRIP 14 // Side of the square resize grip #define OP_ICON 24 // Square a header or footer icon occupies #define OP_GLYPH 0.26 // Reach of a diagonal glyph across its icon #define OP_BAR_R 0.28 // Reach of a straight glyph across its icon #define OP_FRAME_MS 16 // Shortest gap between two repaints //+------------------------------------------------------------------+ //| Inspector and scrollbars | //+------------------------------------------------------------------+ #define OP_INSPECT_W 230 // Width the inspector occupies when open #define OP_NAME_W 72 // Width of the name column beside a value #define OP_VAL_PAD 5 // Padding inside a value box #define OP_VAL_H 20 // Height of a single line value box #define OP_VAL_GAP 3 // Space between two value boxes #define OP_MAX_ROWS 64 // Value rows the inspector may hold #define OP_MAX_LINES 400 // Lines one row may add when it is opened #define OP_WRAP_MAX 4 // Lines a single value may wrap onto #define OP_BAR 8 // Thickness of a scrollbar #define OP_WHEEL 40 // Pixels one wheel notch moves the graph #define OP_RADIUS 10 // Corner radius of the panel itself #define OP_BTN_R 6 // Corner radius of a button #define OP_PAD 14 // Breathing room around the graph inside its area #define OP_ZOOM_MIN 0.30 // Smallest scale the graph is drawn at #define OP_ZOOM_MAX 2.00 // Largest scale the graph is drawn at //+------------------------------------------------------------------+ //| Hit targets | //+------------------------------------------------------------------+ enum OMV_HIT { OH_NONE = 0, // Pointer is over nothing clickable OH_HEADER, // Header band, starts a move OH_THEME, // Light and dark toggle OH_MIN, // Collapse to the header band OH_RESTORE, // Expand back to the full panel OH_CLOSE, // Remove the panel OH_FIT, // Scale the graph to the canvas OH_VIEW, // Swap between the graph and the neurons OH_ZOOM_OUT, // Step the zoom down OH_ZOOM_IN, // Step the zoom up OH_INSPECT_X, // Close the inspector column OH_GRIP, // Bottom right grip, starts a resize OH_BAR_V, // Vertical scrollbar thumb OH_BAR_H, // Horizontal scrollbar thumb OH_CANVAS, // Graph area, starts a pan OH_NODE, // A drawn node box OH_ROW, // A tensor row inside a node box OH_LINK, // A drawn connection OH_INFO, // An expandable row of the inspector OH_INFO_BAR // The inspector scrollbar thumb }; //+------------------------------------------------------------------+ //| One laid out row of the inspector | //+------------------------------------------------------------------+ struct OmvRow { // Name the row carries on its left string name; // Print the value inside the box string value; // Print these lines once the row is expanded string extra[OP_MAX_LINES]; // Count the lines the row adds when opened int extraCount; // Count the lines the value itself wrapped onto int wraps; // Mark a row that can be expanded bool canOpen; // Mark a row that is currently expanded bool isOpen; // Mark a heading rather than a value bool isHead; // Name the value in the graph this row points at string linkName; // Place the box top edge int y; // Measure the box height int h; }; //+------------------------------------------------------------------+ //| What the inspector is describing | //+------------------------------------------------------------------+ enum OMV_PICK { OP_PICK_NONE = 0, // Nothing chosen OP_PICK_NODE, // A layer of the graph OP_PICK_TENSOR, // A stored weight tensor OP_PICK_LINK, // A connection between two layers OP_PICK_GRAPH, // The graph as a whole OP_PICK_MODEL // The file as a whole }; //+------------------------------------------------------------------+ //| Display mode | //+------------------------------------------------------------------+ enum OMV_MODE { OM_FULL = 0, // Whole panel OM_MIN // Header band only }; //+------------------------------------------------------------------+ //| Resolved theme colors | //+------------------------------------------------------------------+ struct OmvTheme { // Color the panel body uint panel; // Color the header and footer bands uint chrome; // Color the panel outline uint border; // Color the band separators uint divider; // Color the faint canvas grid uint grid; // Color the primary text uint text; // Color the secondary text uint muted; // Color the accent used for the active control uint accent; // Color the compute node fill uint computeFill; // Color the compute node edge uint computeEdge; // Color the activation node fill uint activateFill; // Color the activation node edge uint activateEdge; // Color the structural node fill uint shapeFill; // Color the structural node edge uint shapeEdge; // Color the output node fill uint outputFill; // Color the output node edge uint outputEdge; // Color a connection carrying a positive weight uint linkUp; // Color a connection carrying a negative weight uint linkDown; // Color a neuron circle uint cell; // Color the plain node fill uint plainFill; // Color the plain node edge uint plainEdge; // Color a button face uint btnFace; // Color a button edge uint btnEdge; // Color a hovered button face uint hoverFace; // Color a hovered button edge uint hoverEdge; void Apply(const bool dark); }; //+------------------------------------------------------------------+ //| Resolve every color for the requested theme | //+------------------------------------------------------------------+ void OmvTheme::Apply(const bool dark) { //--- Resolve the dark palette if(dark) { panel = ColorToARGB(C'24,24,27', 255); chrome = ColorToARGB(C'32,32,36', 255); border = ColorToARGB(C'58,58,64', 255); divider = ColorToARGB(C'48,48,53', 255); grid = ColorToARGB(C'38,38,42', 255); text = ColorToARGB(C'232,232,236', 255); muted = ColorToARGB(C'138,138,146', 255); accent = ColorToARGB(C'163,155,242', 255); computeFill = ColorToARGB(C'22,44,68', 255); computeEdge = ColorToARGB(C'88,150,214', 255); activateFill = ColorToARGB(C'62,32,22', 255); activateEdge = ColorToARGB(C'214,120,88', 255); shapeFill = ColorToARGB(C'62,28,44', 255); shapeEdge = ColorToARGB(C'214,110,150', 255); outputFill = ColorToARGB(C'20,54,44', 255); outputEdge = ColorToARGB(C'80,190,152', 255); linkUp = ColorToARGB(C'80,190,152', 255); linkDown = ColorToARGB(C'232,110,110', 255); cell = ColorToARGB(C'150,170,195', 255); plainFill = ColorToARGB(C'42,42,46', 255); plainEdge = ColorToARGB(C'110,110,118', 255); btnFace = ColorToARGB(C'40,40,45', 255); btnEdge = ColorToARGB(C'62,62,68', 255); hoverFace = ColorToARGB(C'52,52,58', 255); hoverEdge = ColorToARGB(C'92,92,100', 255); return; } //--- Resolve the light palette panel = ColorToARGB(C'255,255,255', 255); chrome = ColorToARGB(C'247,246,243', 255); border = ColorToARGB(C'208,206,196', 255); divider = ColorToARGB(C'228,226,219', 255); grid = ColorToARGB(C'241,239,232', 255); text = ColorToARGB(C'44,44,42', 255); muted = ColorToARGB(C'126,124,118', 255); accent = ColorToARGB(C'127,119,221', 255); computeFill = ColorToARGB(C'230,241,251', 255); computeEdge = ColorToARGB(C'55,138,221', 255); activateFill = ColorToARGB(C'250,236,231', 255); activateEdge = ColorToARGB(C'216,90,48', 255); shapeFill = ColorToARGB(C'251,234,240', 255); shapeEdge = ColorToARGB(C'212,83,126', 255); outputFill = ColorToARGB(C'225,245,238', 255); outputEdge = ColorToARGB(C'29,158,117', 255); linkUp = ColorToARGB(C'29,158,117', 255); linkDown = ColorToARGB(C'201,58,58', 255); cell = ColorToARGB(C'92,110,132', 255); plainFill = ColorToARGB(C'241,239,232', 255); plainEdge = ColorToARGB(C'180,178,169', 255); btnFace = ColorToARGB(C'255,255,255', 255); btnEdge = ColorToARGB(C'211,209,199', 255); hoverFace = ColorToARGB(C'247,246,243', 255); hoverEdge = ColorToARGB(C'180,178,169', 255); } //+------------------------------------------------------------------+ //| Box geometry | //+------------------------------------------------------------------+ #define OL_BOX_W 76 // Width of one node box #define OL_HEAD_H 17 // Height of the header strip naming the operator #define OL_ROW_H 13 // Height of one listed weight row #define OL_ROW_PAD 3 // Margin around the listed rows on every side #define OL_BOX_S 19 // Height of a node box carrying a title only #define OL_MAX_ROWS 16 // Weight rows one box may list #define OL_GAP_X 22 // Horizontal space between neighbors on a rank #define OL_GAP_Y 26 // Vertical space where a connection carries a label #define OL_GAP_S 14 // Vertical space where the connections carry none //+------------------------------------------------------------------+ //| Node roles used for coloring | //+------------------------------------------------------------------+ #define OL_ROLE_COMPUTE 0 // Layers holding weights #define OL_ROLE_ACTIVATE 1 // Elementwise activations #define OL_ROLE_SHAPE 2 // Structural operators #define OL_ROLE_OUTPUT 3 // Terminal operators #define OL_ROLE_PLAIN 4 // Anything else #define OL_ROLE_PORT 5 // A graph input or output //+------------------------------------------------------------------+ //| Placed graph node | //+------------------------------------------------------------------+ struct OmvBox { // Index the node this box was placed for, or a negative for a port int node; // Name a port box, empty on a layer box string portName; // Place the left edge in graph space int x; // Place the top edge in graph space int y; // Measure the box width int w; // Measure the box height int h; // Depth this box sits at int rank; // Role that decides the box coloring int role; // Letter the operator calls each listed input string rowLabel[OL_MAX_ROWS]; // Shape text drawn beside each listed input string rowText[OL_MAX_ROWS]; // Index the tensor each listed input names int rowTensor[OL_MAX_ROWS]; // Name the value each listed input reads string rowName[OL_MAX_ROWS]; // Count the rows this box lists int rowCount; }; //+------------------------------------------------------------------+ //| Drawn connection between two boxes | //+------------------------------------------------------------------+ struct OmvEdge { // Index the box the edge leaves int from; // Index the box the edge enters int to; // Name the value the connection carries string label; // Shape text drawn beside the connection string shape; }; //+------------------------------------------------------------------+ //| Neuron view limits | //+------------------------------------------------------------------+ #define ON_MAX_BANDS 32 // Neuron rows the view may stack #define ON_MAX_NEURONS 4096 // Circles the view may draw in total #define ON_MAX_LINES 20000 // Connections the view may draw before thinning #define ON_BAND_GAP 88 // Vertical space between one row and the next #define ON_DOT 11 // Diameter of one neuron at full zoom #define ON_DOT_GAP 8 // Space between two neurons on the same row #define ON_TOP_PAD 24 // Room above the first row for its caption #define ON_RANK_GAP 52 // Space between two rows sharing a depth //+------------------------------------------------------------------+ //| One row of neurons the view draws | //+------------------------------------------------------------------+ struct OmvBand { // Count the neurons standing on this row int count; // Name what produced the row and what was applied to it string label; // Place the row across the arrangement int x; // Place the row down the arrangement int y; // Measure how wide the row of circles runs int w; // Record how far the row sits from the graph input int rank; }; //+------------------------------------------------------------------+ //| One run of connections between two rows | //+------------------------------------------------------------------+ struct OmvWeb { // Name the row the connections leave int from; // Name the row the connections arrive at int to; // Index the tensor holding the weights, or a negative when structural int tensor; // Offset the arriving row when several runs share it int offset; // Mark a weight matrix the operator stores the other way round bool transposed; }; //+------------------------------------------------------------------+ //| Neuron arrangement built from the dense layers | //+------------------------------------------------------------------+ class COmvNeurons { private: // Store the rows of neurons OmvBand m_band[]; // Store the runs of connections OmvWeb m_web[]; // Count the rows held int m_bands; // Count the runs held int m_webs; // Measure the arrangement width int m_width; // Measure the arrangement height int m_height; // Explain why the view cannot be drawn string m_reason; // Report whether the arrangement is drawable bool m_ready; int BandOf(const string name, const string &names[], const int &bands[], const int count) const; int WidthOf(COmvModel &model, const string name) const; int TensorNamed(COmvModel &model, const string name) const; bool IsDense(const string op) const; bool IsUnary(const string op) const; bool IsBinary(const string op) const; bool IsShape(const string op) const; int AddBand(const int count, const string label); public: COmvNeurons(void); bool Build(COmvModel &model); void Clear(void); int Bands(void) const { return m_bands; } int Webs(void) const { return m_webs; } int Width(void) const { return m_width; } int Height(void) const { return m_height; } string Reason(void) const { return m_reason; } bool Ready(void) const { return m_ready; } void BandAt(const int i, OmvBand &out) const { if(i >= 0 && i < m_bands) out = m_band[i]; } void WebAt(const int i, OmvWeb &out) const { if(i >= 0 && i < m_webs) out = m_web[i]; } }; //+------------------------------------------------------------------+ //| Arranges a parsed model into boxes and edges | //+------------------------------------------------------------------+ class COmvLayout { private: // Store the placed boxes OmvBox m_box[]; // Store the drawn edges OmvEdge m_edge[]; // Count the boxes placed int m_boxes; // Count the edges drawn int m_edges; // Measure the arrangement width int m_width; // Measure the arrangement height int m_height; int RoleOf(const string op) const; int ProducerOf(COmvModel &model, const string name) const; bool HasNodeParent(COmvModel &model, const OmvNode &node) const; int ShallowestConsumer(COmvModel &model, const int index, const int &rank[]) const; bool IsWeight(COmvModel &model, const string name) const; void RowsOf(COmvModel &model, const OmvNode &node, OmvBox &box) const; void AddPortBox(const string name, const int rank); void AddEdge(const int from, const int to, const string label, const string shape); string ShapeOf(COmvModel &model, const string name) const; public: COmvLayout(void); void Clear(void); bool Build(COmvModel &model); int Boxes(void) const { return m_boxes; } int Edges(void) const { return m_edges; } int Width(void) const { return m_width; } int Height(void) const { return m_height; } void BoxAt(const int i, OmvBox &out) const { if(i >= 0 && i < m_boxes) out = m_box[i]; } void EdgeAt(const int i, OmvEdge &out) const { if(i >= 0 && i < m_edges) out = m_edge[i]; } }; //+------------------------------------------------------------------+ //| Construct an empty neuron arrangement | //+------------------------------------------------------------------+ COmvNeurons::COmvNeurons(void) { //--- Start with nothing placed Clear(); } //+------------------------------------------------------------------+ //| Drop the arrangement built for a previous model | //+------------------------------------------------------------------+ void COmvNeurons::Clear(void) { //--- Zero the counts that bound both arrays m_bands = 0; m_webs = 0; //--- Release the stored rows and runs ArrayResize(m_band, 0); ArrayResize(m_web, 0); //--- Clear the measurements and the verdict m_width = 0; m_height = 0; m_reason = ""; m_ready = false; } //+------------------------------------------------------------------+ //| Report whether an operator multiplies by a weight matrix | //+------------------------------------------------------------------+ bool COmvNeurons::IsDense(const string op) const { //--- Only these two carry a two dimensional weight between neuron rows return (op == "Gemm" || op == "MatMul"); } //+------------------------------------------------------------------+ //| Report whether an operator works on one value in place | //+------------------------------------------------------------------+ bool COmvNeurons::IsUnary(const string op) const { //--- These change the values on a row without changing how many there are return (op == "Relu" || op == "Tanh" || op == "Sigmoid" || op == "Softmax" || op == "LeakyRelu" || op == "Elu" || op == "Sqrt" || op == "Abs" || op == "Cast" || op == "Identity" || op == "Clip" || op == "Erf"); } //+------------------------------------------------------------------+ //| Report whether an operator pairs a value with a constant | //+------------------------------------------------------------------+ bool COmvNeurons::IsBinary(const string op) const { //--- These scale or shift a row, leaving its neuron count alone return (op == "Mul" || op == "Add" || op == "Sub" || op == "Div" || op == "Pow"); } //+------------------------------------------------------------------+ //| Report whether an operator only rearranges a value | //+------------------------------------------------------------------+ bool COmvNeurons::IsShape(const string op) const { //--- These move the numbers around without creating or destroying any return (op == "Reshape" || op == "Transpose" || op == "Flatten" || op == "Squeeze" || op == "Unsqueeze"); } //+------------------------------------------------------------------+ //| Find the row a named value stands on | //+------------------------------------------------------------------+ int COmvNeurons::BandOf(const string name, const string &names[], const int &bands[], const int count) const { //--- Scan the names already placed for a match for(int i = 0; i < count; i++) if(names[i] == name) return bands[i]; //--- Report that the value stands on no row return -1; } //+------------------------------------------------------------------+ //| Take how many numbers a named value carries | //+------------------------------------------------------------------+ int COmvNeurons::WidthOf(COmvModel &model, const string name) const { //--- A graph input announces its own shape for(int i = 0; i < model.Inputs(); i++) { //--- Fetch this graph input OmvPort port; model.InputAt(i, port); //--- Take the last axis, which is the count of numbers if(port.name == name && port.dims > 0) return port.dim[port.dims - 1]; } //--- A value passing between layers announces its shape when the file says so for(int i = 0; i < model.Inners(); i++) { //--- Fetch this inner value OmvPort port; model.InnerAt(i, port); //--- Take the last axis, which is the count of numbers if(port.name == name && port.dims > 0) return port.dim[port.dims - 1]; } //--- A graph output announces its own shape for(int i = 0; i < model.Outputs(); i++) { //--- Fetch this graph output OmvPort port; model.OutputAt(i, port); //--- Take the last axis, which is the count of numbers if(port.name == name && port.dims > 0) return port.dim[port.dims - 1]; } //--- Report nothing for a value the file never shapes return 0; } //+------------------------------------------------------------------+ //| Find the tensor a name belongs to | //+------------------------------------------------------------------+ int COmvNeurons::TensorNamed(COmvModel &model, const string name) const { //--- Scan every stored tensor for a matching name for(int i = 0; i < model.Tensors(); i++) { //--- Fetch this tensor OmvTensor tensor; model.TensorAt(i, tensor); //--- Report the one that carries the name if(tensor.name == name) return i; } //--- Report that no tensor carries it return -1; } //+------------------------------------------------------------------+ //| Open a new row of neurons | //+------------------------------------------------------------------+ int COmvNeurons::AddBand(const int count, const string label) { //--- Refuse a row beyond what the view may stack if(m_bands >= ON_MAX_BANDS || count <= 0) return -1; //--- Describe the row OmvBand band; band.count = count; band.label = label; band.x = 0; band.y = 0; band.w = 0; band.rank = 0; //--- Append it to the arrangement ArrayResize(m_band, m_bands + 1); m_band[m_bands++] = band; return m_bands - 1; } //+------------------------------------------------------------------+ //| Follow the value names into rows of neurons | //+------------------------------------------------------------------+ bool COmvNeurons::Build(COmvModel &model) { //--- Drop whatever a previous model left behind Clear(); //--- Refuse a graph that exposes nothing to start from if(model.Inputs() <= 0) { m_reason = "the graph exposes no input"; return false; } //--- Track which row each value name stands on string names[]; int bands[]; int named = 0; ArrayResize(names, OMV_MAX_NODES * 2); ArrayResize(bands, OMV_MAX_NODES * 2); //--- Open the first row from the graph input OmvPort first; model.InputAt(0, first); const int firstWide = WidthOf(model, first.name); if(firstWide <= 0) { m_reason = "the graph input carries no shape"; return false; } names[named] = first.name; bands[named] = AddBand(firstWide, first.name); named++; //--- Walk the layers in file order, opening a row wherever weights appear for(int i = 0; i < model.Nodes(); i++) { //--- Fetch this layer OmvNode node; model.NodeAt(i, node); //--- Find which of its inputs already stand on a row int fed[8]; int feds = 0; for(int j = 0; j < node.feedCount && feds < 8; j++) { //--- Keep only the names a previous layer produced const int b = BandOf(node.feeds[j], names, bands, named); if(b >= 0) fed[feds++] = b; } //--- Find a two dimensional weight among its inputs int weight = -1; for(int j = 0; j < node.feedCount; j++) { //--- Take the first stored tensor carrying two axes const int k = TensorNamed(model, node.feeds[j]); if(k < 0) continue; OmvTensor probe; model.TensorAt(k, probe); if(probe.dims == 2) { weight = k; break; } } //--- A dense layer opens a row and connects to it through its weights if(IsDense(node.op) && feds > 0 && weight >= 0) { //--- Take whether the operator stores its weights the other way round bool flipped = false; for(int a = 0; a < node.attrCount; a++) if(node.attrName[a] == "transB" && node.attrValue[a] == "1") flipped = true; //--- Take the outgoing width from whichever axis the storage implies OmvTensor w; model.TensorAt(weight, w); const int made = AddBand(flipped ? w.dim[0] : w.dim[1], node.op); if(made < 0) { m_reason = "the model has more layers than the view can stack"; return false; } //--- Record the run of connections the weight describes OmvWeb web; web.from = fed[0]; web.to = made; web.tensor = weight; web.offset = 0; web.transposed = flipped; ArrayResize(m_web, m_webs + 1); m_web[m_webs++] = web; //--- Stand the produced name on the new row if(node.emitCount > 0 && named < ArraySize(names)) { names[named] = node.emits[0]; bands[named] = made; named++; } continue; } //--- A joining layer opens a row that its sources feed positionally if(node.op == "Concat" && feds >= 2) { //--- Add the widths of every row arriving here int total = 0; for(int j = 0; j < feds; j++) total += m_band[fed[j]].count; const int made = AddBand(total, "Concat"); if(made < 0) { m_reason = "the model has more layers than the view can stack"; return false; } //--- Record one structural run per source, each at its own offset int at = 0; for(int j = 0; j < feds; j++) { //--- Note where this source lands on the joined row OmvWeb web; web.from = fed[j]; web.to = made; web.tensor = -1; web.offset = at; web.transposed = false; ArrayResize(m_web, m_webs + 1); m_web[m_webs++] = web; at += m_band[fed[j]].count; } //--- Stand the produced name on the new row if(node.emitCount > 0 && named < ArraySize(names)) { names[named] = node.emits[0]; bands[named] = made; named++; } continue; } //--- These change the numbers on a row without changing how many there are if((IsUnary(node.op) || IsBinary(node.op) || IsShape(node.op)) && feds > 0) { //--- Stand the produced name on the same row and note what was applied if(node.emitCount > 0 && named < ArraySize(names)) { names[named] = node.emits[0]; bands[named] = fed[0]; named++; } if(IsUnary(node.op) || IsBinary(node.op)) m_band[fed[0]].label += " + " + node.op; continue; } //--- A layer reading only stored tensors touches no row at all if(feds == 0) continue; //--- A dense layer whose weights are computed rather than stored if(IsDense(node.op)) { m_reason = node.op + " takes its weights from another layer, not from a stored tensor"; return false; } //--- Anything else has no neurons to draw, so the view cannot be honest m_reason = "the model uses " + node.op + ", which has no neuron pairs to draw"; return false; } //--- Refuse a graph that produced no weighted connection at all if(m_webs <= 0 || m_bands < 2) { m_reason = "the model has no dense layers"; return false; } //--- Count what the arrangement would cost to draw int neurons = 0; for(int i = 0; i < m_bands; i++) neurons += m_band[i].count; if(neurons > ON_MAX_NEURONS) { m_reason = "the model has " + IntegerToString(neurons) + " neurons to draw"; return false; } //--- Take how deep each row sits by walking the runs until nothing moves for(int pass = 0; pass < m_bands; pass++) { //--- Push every target one step deeper than the source that feeds it bool moved = false; for(int i = 0; i < m_webs; i++) { //--- Deepen the target only when the source now sits deeper const int want = m_band[m_web[i].from].rank + 1; if(m_band[m_web[i].to].rank < want) { m_band[m_web[i].to].rank = want; moved = true; } } //--- Stop as soon as a whole pass changes nothing if(!moved) break; } //--- Measure every row and find how deep the arrangement runs int deepest = 0; for(int i = 0; i < m_bands; i++) { //--- Take the width the circles occupy on this row m_band[i].w = m_band[i].count * ON_DOT + (m_band[i].count - 1) * ON_DOT_GAP; if(m_band[i].rank > deepest) deepest = m_band[i].rank; } //--- Lay every rank across, so rows sharing a depth sit side by side int widest = 0; for(int r = 0; r <= deepest; r++) { //--- Add the widths of every row at this depth int across = 0, seen = 0; for(int i = 0; i < m_bands; i++) if(m_band[i].rank == r) { across += m_band[i].w; seen++; } if(seen > 1) across += (seen - 1) * ON_RANK_GAP; if(across > widest) widest = across; } //--- Place each row against the widest rank, centered as a group for(int r = 0; r <= deepest; r++) { //--- Measure this rank again to know where the group starts int across = 0, seen = 0; for(int i = 0; i < m_bands; i++) if(m_band[i].rank == r) { across += m_band[i].w; seen++; } if(seen > 1) across += (seen - 1) * ON_RANK_GAP; //--- Walk the rank left to right from that starting point int at = (widest - across) / 2; for(int i = 0; i < m_bands; i++) { //--- Place only the rows sitting at this depth if(m_band[i].rank != r) continue; m_band[i].x = at; m_band[i].y = ON_TOP_PAD + r * ON_BAND_GAP; at += m_band[i].w + ON_RANK_GAP; } } m_height = ON_TOP_PAD + deepest * ON_BAND_GAP + ON_DOT; //--- Publish the measurements the panel scales against m_width = widest; m_ready = true; //--- Report the arrangement as built return true; } //+------------------------------------------------------------------+ //| Construct an empty layout | //+------------------------------------------------------------------+ COmvLayout::COmvLayout(void) { //--- Clear every field so a query before building reports empty Clear(); } //+------------------------------------------------------------------+ //| Drop everything the previous build produced | //+------------------------------------------------------------------+ void COmvLayout::Clear(void) { //--- Zero the counts that bound both arrays m_boxes = 0; m_edges = 0; //--- Release the stored geometry ArrayResize(m_box, 0); ArrayResize(m_edge, 0); //--- Zero the overall size m_width = 0; m_height = 0; } //+------------------------------------------------------------------+ //| Classify an operator into a coloring role | //+------------------------------------------------------------------+ int COmvLayout::RoleOf(const string op) const { //--- Group the operators that carry weight matrices if(op == "Gemm" || op == "MatMul" || op == "Conv" || op == "ConvTranspose") return OL_ROLE_COMPUTE; //--- Group the elementwise activations if(op == "Tanh" || op == "Relu" || op == "Sigmoid" || op == "LeakyRelu" || op == "Elu") return OL_ROLE_ACTIVATE; //--- Group the operators that rearrange rather than compute if(op == "Concat" || op == "Reshape" || op == "Transpose" || op == "Split" || op == "Slice") return OL_ROLE_SHAPE; //--- Group the operators that normally end a graph if(op == "Softmax" || op == "LogSoftmax" || op == "Sigmoid") return OL_ROLE_OUTPUT; //--- Leave everything else plain return OL_ROLE_PLAIN; } //+------------------------------------------------------------------+ //| Find the node that emits a given name | //+------------------------------------------------------------------+ int COmvLayout::ProducerOf(COmvModel &model, const string name) const { //--- Scan every node for one that emits this name for(int i = 0; i < model.Nodes(); i++) { //--- Fetch this node OmvNode node; model.NodeAt(i, node); //--- Compare every name it produces for(int j = 0; j < node.emitCount; j++) if(node.emits[j] == name) return i; } //--- Report that no node produces it return -1; } //+------------------------------------------------------------------+ //| Report whether a name belongs to a stored weight tensor | //+------------------------------------------------------------------+ bool COmvLayout::IsWeight(COmvModel &model, const string name) const { //--- Scan every tensor for a matching name for(int i = 0; i < model.Tensors(); i++) { //--- Fetch this tensor OmvTensor tensor; model.TensorAt(i, tensor); //--- Report a match if(tensor.name == name) return true; } //--- Report that no tensor carries this name return false; } //+------------------------------------------------------------------+ //| Report whether any name feeding a node comes from a node | //+------------------------------------------------------------------+ bool COmvLayout::HasNodeParent(COmvModel &model, const OmvNode &node) const { //--- Test every name feeding this node for(int j = 0; j < node.feedCount; j++) { //--- Skip names that are weights rather than node outputs if(IsWeight(model, node.feeds[j])) continue; //--- Report a name that another node produces if(ProducerOf(model, node.feeds[j]) >= 0) return true; } //--- Report that nothing upstream feeds this node return false; } //+------------------------------------------------------------------+ //| Find the shallowest rank among a node's consumers | //+------------------------------------------------------------------+ int COmvLayout::ShallowestConsumer(COmvModel &model, const int index, const int &rank[]) const { //--- Fetch the node whose consumers are wanted OmvNode source; model.NodeAt(index, source); //--- Start above every possible rank int shallowest = -1; //--- Scan every node for one that reads a name this node emits for(int k = 0; k < model.Nodes(); k++) { //--- Fetch the candidate consumer OmvNode target; model.NodeAt(k, target); //--- Compare every name feeding the candidate for(int j = 0; j < target.feedCount; j++) { //--- Skip names that are weights rather than node outputs if(IsWeight(model, target.feeds[j])) continue; //--- Skip names this node does not emit if(ProducerOf(model, target.feeds[j]) != index) continue; //--- Track the shallowest consumer found so far if(shallowest < 0 || rank[k] < shallowest) shallowest = rank[k]; } } //--- Report the shallowest rank, or a negative when nothing consumes it return shallowest; } //+------------------------------------------------------------------+ //| Report the shape a named value carries | //+------------------------------------------------------------------+ string COmvLayout::ShapeOf(COmvModel &model, const string name) const { //--- A graph input announces its own shape for(int i = 0; i < model.Inputs(); i++) { //--- Fetch this graph input OmvPort port; model.InputAt(i, port); //--- Report the shape when the name matches if(port.name == name) return model.DimsText(port.dim, port.dims); } //--- A graph output announces its own shape for(int i = 0; i < model.Outputs(); i++) { //--- Fetch this graph output OmvPort port; model.OutputAt(i, port); //--- Report the shape when the name matches if(port.name == name) return model.DimsText(port.dim, port.dims); } //--- A value passing between layers announces its shape when the file says so for(int i = 0; i < model.Inners(); i++) { //--- Fetch this inner value OmvPort port; model.InnerAt(i, port); //--- Report the shape when the name matches if(port.name == name) return model.DimsText(port.dim, port.dims); } //--- A stored tensor announces its own shape for(int i = 0; i < model.Tensors(); i++) { //--- Fetch this tensor OmvTensor tensor; model.TensorAt(i, tensor); //--- Report the shape when the name matches if(tensor.name == name) return model.DimsText(tensor.dim, tensor.dims); } //--- Report nothing for a value the file never shapes return ""; } //+------------------------------------------------------------------+ //| Place a box standing for a value the graph exposes | //+------------------------------------------------------------------+ void COmvLayout::AddPortBox(const string name, const int rank) { //--- Refuse a box beyond what the arrangement may hold if(m_boxes >= OMV_MAX_NODES) return; //--- Describe the box, which carries a name rather than a layer OmvBox box; box.node = -1; box.portName = name; box.rank = rank; box.role = OL_ROLE_PORT; box.rowCount = 0; box.w = OL_BOX_W; box.h = OL_BOX_S; box.x = (m_width - OL_BOX_W) / 2; box.y = 0; //--- Append it to the arrangement ArrayResize(m_box, m_boxes + 1); m_box[m_boxes++] = box; } //+------------------------------------------------------------------+ //| Record one connection between two placed boxes | //+------------------------------------------------------------------+ void COmvLayout::AddEdge(const int from, const int to, const string label, const string shape) { //--- Refuse a connection between boxes that were never placed if(from < 0 || to < 0 || from >= m_boxes || to >= m_boxes) return; //--- Describe the connection and the value it carries OmvEdge edge; edge.from = from; edge.to = to; edge.label = label; edge.shape = shape; //--- Append it to the arrangement ArrayResize(m_edge, m_edges + 1); m_edge[m_edges++] = edge; } //+------------------------------------------------------------------+ //| List every stored weight an operator reads | //+------------------------------------------------------------------+ void COmvLayout::RowsOf(COmvModel &model, const OmvNode &node, OmvBox &box) const { //--- Start with an empty listing box.rowCount = 0; //--- The operator calls its inputs A, B, C and so on in order const string letters = "ABCDEFGH"; //--- Walk the names feeding this node for(int j = 0; j < node.feedCount && box.rowCount < OL_MAX_ROWS; j++) { //--- Look for a stored tensor carrying this name for(int i = 0; i < model.Tensors(); i++) { //--- Fetch this tensor OmvTensor tensor; model.TensorAt(i, tensor); //--- Skip anything that is not the name being looked up if(tensor.name != node.feeds[j]) continue; //--- Name the input by its position on the operator box.rowLabel[box.rowCount] = StringSubstr(letters, j, 1); //--- Print the shape the way the operator reports it box.rowText[box.rowCount] = model.DimsText(tensor.dim, tensor.dims); box.rowTensor[box.rowCount] = i; box.rowName[box.rowCount] = tensor.name; box.rowCount++; break; } } } //+------------------------------------------------------------------+ //| Arrange every node into ranks and connect them | //+------------------------------------------------------------------+ bool COmvLayout::Build(COmvModel &model) { //--- Drop any arrangement held from a previous model Clear(); //--- Abort when the model holds no nodes const int total = model.Nodes(); if(total <= 0) return false; //--- Start every node at an unknown depth int rank[]; ArrayResize(rank, total); ArrayInitialize(rank, -1); //--- Settle depths by repeated passes so parents always come first for(int pass = 0; pass < total; pass++) { //--- Track whether this pass moved anything bool moved = false; //--- Visit every node for(int i = 0; i < total; i++) { //--- Fetch this node OmvNode node; model.NodeAt(i, node); //--- Take the deepest producer feeding it int deepest = -1; for(int j = 0; j < node.feedCount; j++) { //--- Skip names that are weights rather than node outputs if(IsWeight(model, node.feeds[j])) continue; //--- Find the node emitting this name const int producer = ProducerOf(model, node.feeds[j]); //--- Track the deepest one settled so far if(producer >= 0 && rank[producer] > deepest) deepest = rank[producer]; } //--- Sit one level below the deepest producer const int want = deepest + 1; //--- Push the node down when this pass found it deeper if(want > rank[i]) { rank[i] = want; moved = true; } } //--- Stop once a pass changes nothing if(!moved) break; } //--- Sink a node with no upstream parent to just above its consumer for(int i = 0; i < total; i++) { //--- Fetch this node OmvNode node; model.NodeAt(i, node); //--- Leave a node that something upstream already feeds if(HasNodeParent(model, node)) continue; //--- Find where its shallowest consumer sits const int consumer = ShallowestConsumer(model, i, rank); //--- Drop the node to one rank above that consumer if(consumer > 0 && consumer - 1 > rank[i]) rank[i] = consumer - 1; } //--- Count how many nodes sit on each rank int depth = 0; for(int i = 0; i < total; i++) if(rank[i] > depth) depth = rank[i]; int perRank[]; ArrayResize(perRank, depth + 1); ArrayInitialize(perRank, 0); for(int i = 0; i < total; i++) perRank[rank[i]]++; //--- Find the widest rank so every rank can be centered against it int widest = 1; for(int r = 0; r <= depth; r++) if(perRank[r] > widest) widest = perRank[r]; m_width = widest * OL_BOX_W + (widest - 1) * OL_GAP_X; //--- Track how many boxes each rank has already placed int placed[]; ArrayResize(placed, depth + 1); ArrayInitialize(placed, 0); //--- Find which ranks are entered by a connection that carries a label bool wideGap[]; ArrayResize(wideGap, depth + 2); ArrayInitialize(wideGap, false); for(int i = 0; i < total; i++) { //--- Fetch this node to read the names it takes in OmvNode probe; model.NodeAt(i, probe); //--- Test every name that is not a stored weight for(int j = 0; j < probe.feedCount; j++) { //--- Skip a name the file never shaped, it draws no label if(IsWeight(model, probe.feeds[j])) continue; if(StringLen(ShapeOf(model, probe.feeds[j])) == 0) continue; //--- Widen the gap above the rank this node sits on wideGap[rank[i]] = true; } } //--- A graph output is always entered by a labelled connection if(model.Outputs() > 0) wideGap[depth + 1] = true; //--- Measure the tallest box on every rank so ranks never overlap int rowHigh[]; ArrayResize(rowHigh, depth + 1); ArrayInitialize(rowHigh, OL_BOX_S); for(int i = 0; i < total; i++) { //--- Fetch this node to size the box it will get OmvNode probe; model.NodeAt(i, probe); OmvBox sizing; RowsOf(model, probe, sizing); //--- Take the taller of what this rank already holds and this box const int high = (sizing.rowCount > 0) ? OL_HEAD_H + OL_ROW_PAD * 2 + sizing.rowCount * OL_ROW_H : OL_BOX_S; if(high > rowHigh[rank[i]]) rowHigh[rank[i]] = high; } //--- Place one box per node ArrayResize(m_box, total); for(int i = 0; i < total; i++) { //--- Fetch this node OmvNode node; model.NodeAt(i, node); //--- Describe the box OmvBox box; box.node = i; box.rank = rank[i]; box.role = RoleOf(node.op); RowsOf(model, node, box); box.w = OL_BOX_W; //--- Size the box to the header plus however many rows it lists box.h = (box.rowCount > 0) ? OL_HEAD_H + OL_ROW_PAD * 2 + box.rowCount * OL_ROW_H : OL_BOX_S; //--- Center this rank inside the widest one const int count = perRank[rank[i]]; const int span = count * OL_BOX_W + (count - 1) * OL_GAP_X; const int left = (m_width - span) / 2; //--- Step across the rank by however many boxes it already holds box.x = left + placed[rank[i]] * (OL_BOX_W + OL_GAP_X); box.y = 0; //--- Stack this rank below every taller rank above it for(int r = 0; r < rank[i]; r++) box.y += rowHigh[r] + (wideGap[r + 1] ? OL_GAP_Y : OL_GAP_S); placed[rank[i]]++; //--- Store the placed box m_box[m_boxes++] = box; } //--- Give every graph input a box on a rank of its own above the graph for(int p = 0; p < model.Inputs(); p++) { //--- Fetch this graph input OmvPort port; model.InputAt(p, port); //--- Skip a name that a stored tensor already covers if(IsWeight(model, port.name)) continue; AddPortBox(port.name, -1); } //--- Give every graph output a box on a rank of its own below the graph for(int p = 0; p < model.Outputs(); p++) { //--- Fetch this graph output OmvPort port; model.OutputAt(p, port); AddPortBox(port.name, depth + 1); } //--- Shift every layer box down to leave the input rank its room const int inputGap = wideGap[0] ? OL_GAP_Y : OL_GAP_S; for(int i = 0; i < m_boxes; i++) if(m_box[i].rank >= 0) m_box[i].y += OL_BOX_S + inputGap; //--- Place the output boxes under the deepest rank int deepestY = 0; for(int i = 0; i < m_boxes; i++) if(m_box[i].rank >= 0 && m_box[i].y + m_box[i].h > deepestY) deepestY = m_box[i].y + m_box[i].h; const int outputGap = wideGap[depth + 1] ? OL_GAP_Y : OL_GAP_S; for(int i = 0; i < m_boxes; i++) if(m_box[i].rank > depth) m_box[i].y = deepestY + outputGap; //--- Measure the whole arrangement from the boxes it actually holds m_height = 0; for(int i = 0; i < m_boxes; i++) if(m_box[i].y + m_box[i].h > m_height) m_height = m_box[i].y + m_box[i].h; //--- Connect every graph input box to the layers that read it for(int i = 0; i < m_boxes; i++) { //--- Take only the boxes standing for a graph input if(m_box[i].rank != -1) continue; //--- Find every layer reading that name for(int k = 0; k < total; k++) { //--- Fetch the candidate layer OmvNode target; model.NodeAt(k, target); //--- Compare every name feeding it for(int j = 0; j < target.feedCount; j++) { //--- Skip a name that is not this port if(target.feeds[j] != m_box[i].portName) continue; //--- Find the box the layer was placed in for(int b = 0; b < m_boxes; b++) { //--- Store the connection once the box is found if(m_box[b].node != k) continue; AddEdge(i, b, m_box[i].portName, ShapeOf(model, m_box[i].portName)); break; } } } } //--- Connect the layer that writes each graph output to its box for(int i = 0; i < m_boxes; i++) { //--- Take only the boxes standing for a graph output if(m_box[i].rank <= depth) continue; //--- Find the layer emitting that name const int producer = ProducerOf(model, m_box[i].portName); if(producer < 0) continue; //--- Find the box the layer was placed in for(int b = 0; b < m_boxes; b++) { //--- Store the connection once the box is found if(m_box[b].node != producer) continue; AddEdge(b, i, m_box[i].portName, ShapeOf(model, m_box[i].portName)); break; } } //--- Connect every node to the nodes feeding it for(int i = 0; i < total; i++) { //--- Fetch this node OmvNode node; model.NodeAt(i, node); //--- Draw one edge per producing name for(int j = 0; j < node.feedCount; j++) { //--- Skip names that are weights rather than node outputs if(IsWeight(model, node.feeds[j])) continue; //--- Find the node emitting this name const int producer = ProducerOf(model, node.feeds[j]); //--- Skip a name no node produces if(producer < 0) continue; //--- Find the boxes the two layers were placed in int fromBox = -1, toBox = -1; for(int b = 0; b < m_boxes; b++) { //--- Match each end to its box if(m_box[b].node == producer) fromBox = b; if(m_box[b].node == i) toBox = b; } //--- Skip a pair that was never placed if(fromBox < 0 || toBox < 0) continue; //--- Store the connection and the name it carries AddEdge(fromBox, toBox, node.feeds[j], ShapeOf(model, node.feeds[j])); } } //--- Report success return true; } //+------------------------------------------------------------------+ //| Chart panel showing a parsed model | //+------------------------------------------------------------------+ class COmvPanel { private: // Draw everything through one canvas COmvCanvas m_cv; // Hold the parsed model COmvModel m_model; // Hold the arranged geometry COmvLayout m_layout; // Hold the neuron arrangement COmvNeurons m_cells; // Show the neuron view in place of the graph bool m_neuronView; // Bound the control that switches between the two views int m_rView[4]; // Resolve the active theme colors OmvTheme m_th; // Identify the chart the panel lives on long m_chartId; // Name the bitmap object string m_name; // Name the running program string m_title; // Track whether the canvas exists bool m_created; // Track whether teardown has run bool m_destroyed; // Place the panel left edge int m_x; // Place the panel top edge int m_y; // Measure the panel width int m_w; // Measure the panel height int m_h; // Remember the height before collapsing int m_fullH; // Select the full or collapsed mode int m_mode; // Track whether the dark theme is active bool m_dark; // Track whether the inspector column is open bool m_inspect; // Scale the graph inside its canvas double m_zoom; // Offset the graph horizontally inside its canvas int m_panX; // Offset the graph vertically inside its canvas int m_panY; // Index the node the inspector describes int m_selected; // Index the node under the pointer int m_hoverNode; // Name what kind of thing the inspector describes int m_pickKind; // Index the tensor the inspector describes int m_pickTensor; // Index the connection the inspector describes int m_pickLink; // Index the tensor row under the pointer int m_hoverRow; // Index the connection under the pointer int m_hoverLink; // Bound every drawn tensor row int m_rowRect[][4]; // Name the tensor each drawn row belongs to int m_rowTensor[]; // Count the rows drawn this frame int m_rows; // Remember the route every drawn connection took int m_linkPts[][12]; // Count the points each drawn route holds int m_linkCount[]; // Name the connection each drawn route belongs to int m_linkEdge[]; // Count the connections drawn this frame int m_links; // Hold the rows the inspector lays out OmvRow m_info[]; // Count the rows the inspector holds int m_infos; // Remember which row names the user has opened string m_opened[]; // Count the names held open int m_openCount; // Offset the inspector when its rows run past the column int m_infoScroll; // Measure how tall the laid out rows are int m_infoHigh; // Index the row under the pointer int m_infoHover; // Name the value the pointer is lighting up in the graph string m_litName; // Track whether the pointer sits on a row's expander rather than its body bool m_onExpander; // Bound the inspector scrollbar thumb int m_rInfoBar[4]; // Identify the control under the pointer int m_hover; // Identify the control the press began on int m_press; // Track a header drag bool m_dragging; // Track a grip drag bool m_resizing; // Track a canvas drag bool m_panning; // Track a scrollbar drag int m_barDrag; // Track whether the vertical bar is showing bool m_barV; // Track whether the horizontal bar is showing bool m_barH; // Remember the previous mouse button state bool m_lmbPrev; // Track whether chart scrolling is suppressed bool m_scrollLocked; // Remember whether the chart reported pointer moves before the panel asked bool m_hadMouse; // Remember whether the chart reported wheel turns before the panel asked bool m_hadWheel; // Offset the pointer inside the header at press int m_dragOffX; // Offset the pointer inside the header at press int m_dragOffY; // Offset the grip from the panel corner at press int m_gripOffX; // Offset the grip from the panel corner at press int m_gripOffY; // Remember where a canvas press began, to tell a click from a pan int m_pressX; // Remember where a canvas press began, to tell a click from a pan int m_pressY; // Remember the pointer position at a drag press int m_fromX; // Remember the pointer position at a drag press int m_fromY; // Remember the pan offset at a scrollbar press int m_barFrom; // Stamp the last repaint for the frame cap uint m_lastFrameMs; // Hold the chrome that does not change between frames uint m_bg[]; // Track whether the cached chrome still matches the panel bool m_bgValid; // Keep the drawn view so a hover does not redraw every connection uint m_scene[]; // Remember what the kept scene was drawn for string m_sceneSig; // Track whether the kept scene still matches the view bool m_sceneValid; // Track a frame the cap skipped bool m_dirty; // Describe everything the cached chrome depends on string m_bgSig; // Remember which tooltip the object currently carries string m_tipShown; // Bound the theme button int m_rTheme[4]; // Bound the minimize button int m_rMin[4]; // Bound the close button int m_rClose[4]; // Bound the fit button int m_rFit[4]; // Bound the zoom out button int m_rZoomOut[4]; // Bound the zoom in button int m_rZoomIn[4]; // Bound the inspector close button int m_rInspectX[4]; // Bound the resize grip int m_rGrip[4]; // Bound the vertical scrollbar thumb int m_rBarV[4]; // Bound the horizontal scrollbar thumb int m_rBarH[4]; void InitDefaults(void); void ComputeLayout(void); bool CreateCanvas(void); bool ApplyMode(void); void LockChartScroll(const bool lock); int CanvasLeft(void) const { return 1; } int CanvasTop(void) const { return OP_HEADER_H + OP_STAT_H + 1; } int CanvasRight(void) const { return m_inspect ? m_w - OP_INSPECT_W : m_w - 1; } int CanvasBottom(void) const { return m_h - OP_FOOT_H; } int ViewW(void) const { return CanvasRight() - CanvasLeft() - (m_barV ? OP_BAR : 0); } int ViewH(void) const { return CanvasBottom() - CanvasTop() - (m_barH ? OP_BAR : 0); } int ViewWide(void) const { return m_neuronView ? m_cells.Width() : m_layout.Width(); } int ViewHigh(void) const { return m_neuronView ? m_cells.Height() : m_layout.Height(); } int ScaledW(void) const { return (int)(ViewWide() * m_zoom); } int ScaledH(void) const { return (int)(ViewHigh() * m_zoom); } bool InRect(const int &r[], const int mx, const int my) const { return (r[2] > 0 && mx >= r[0] && mx < r[0] + r[2] && my >= r[1] && my < r[1] + r[3]); } int HitTest(const int mx, const int my); int NodeAtPoint(const int mx, const int my); int RowAtPoint(const int mx, const int my); int LinkAtPoint(const int mx, const int my); int TensorNamed(const string name) const; string TensorNameAt(const int index) const; string Measure(const double value) const; string ArgIn(const string op, const int slot) const; string ArgOut(const string op) const; void AddEndpoint(const string side, const int boxIndex); void FitGraph(void); void SetZoom(const double zoom); void OnWheel(const int mx, const int my, const int delta); void ClampPan(void); uint FillForRole(const int role) const; uint EdgeForRole(const int role) const; string TipFor(const int id) const; void SetTooltip(const string words); string ChromeSignature(void); string SceneSignature(void); void DrawChrome(void); void DrawShell(void); void DrawHeader(void); void DrawStats(void); void DrawGraph(void); void DrawNeurons(void); void SizeBars(void); void DrawBars(void); void AddRow(const string name, const string value, const bool isHead, const string linkName = ""); void AddLine(const string line); void AddValueLines(const string name); void AddTensorValues(const OmvTensor &tensor); void BuildInfo(void); void ToggleRow(const string key); int InfoAtPoint(const int mx, const int my); bool OnExpander(const int mx, const int row) const; int LinkNamed(const string name) const; void DrawInspector(void); void DrawFooterChrome(void); void DrawFooter(void); void DrawViewSwitch(void); void DrawButton(const int &r[], const int id, const string label); void DrawIcon(const int &r[], const int id, const int which); void PaintCapped(void); public: COmvPanel(void) { m_created = false; m_destroyed = false; m_scrollLocked = false; m_hadMouse = false; m_hadWheel = false; } bool Init(const long chartId, const string path); void Destroy(void); void Render(void); void OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam); }; //+------------------------------------------------------------------+ //| Reset every member to a safe starting value | //+------------------------------------------------------------------+ void COmvPanel::InitDefaults(void) { //--- Clear the chart identity m_chartId = 0; m_name = "OMV_Panel"; m_title = ""; m_created = false; m_destroyed = false; //--- Anchor the panel and size it for full mode m_x = 20; m_y = 34; m_w = OP_W; m_h = OP_H; m_fullH = OP_H; m_mode = OM_FULL; //--- Start light with the inspector closed until a node is picked m_dark = false; m_inspect = false; //--- Start the graph unscaled and unshifted m_zoom = 1.0; m_panX = 0; m_panY = 0; m_selected = -1; m_hoverNode = -1; m_neuronView = false; m_pickKind = OP_PICK_NONE; m_pickTensor = -1; m_pickLink = -1; m_hoverRow = -1; m_hoverLink = -1; m_rows = 0; m_links = 0; m_infos = 0; m_openCount = 0; m_litName = ""; m_onExpander = false; m_infoScroll = 0; m_infoHigh = 0; m_infoHover = -1; //--- Clear the interaction state m_hover = OH_NONE; m_press = OH_NONE; m_dragging = false; m_resizing = false; m_panning = false; m_barDrag = OH_NONE; m_barV = false; m_barH = false; m_lmbPrev = false; m_scrollLocked = false; m_dragOffX = 0; m_dragOffY = 0; m_gripOffX = 0; m_gripOffY = 0; m_pressX = 0; m_pressY = 0; m_fromX = 0; m_fromY = 0; m_barFrom = 0; m_lastFrameMs = 0; m_bgValid = false; m_dirty = false; m_bgSig = ""; m_sceneValid = false; m_sceneSig = ""; m_tipShown = ""; } //+------------------------------------------------------------------+ //| Place every control against the current panel size | //+------------------------------------------------------------------+ void COmvPanel::ComputeLayout(void) { //--- Sit the three header icons against the right edge const int iconY = (OP_HEADER_H - OP_ICON) / 2; m_rClose[0] = m_w - 40; m_rClose[1] = iconY; m_rClose[2] = OP_ICON; m_rClose[3] = OP_ICON; m_rMin[0] = m_w - 72; m_rMin[1] = iconY; m_rMin[2] = OP_ICON; m_rMin[3] = OP_ICON; m_rTheme[0] = m_w - 104; m_rTheme[1] = iconY; m_rTheme[2] = OP_ICON; m_rTheme[3] = OP_ICON; //--- Sit the view controls along the footer const int footY = m_h - OP_FOOT_H + (OP_FOOT_H - 22) / 2; m_rFit[0] = 14; m_rFit[1] = footY; m_rFit[2] = 36; m_rFit[3] = 22; m_rZoomOut[0] = 58; m_rZoomOut[1] = footY; m_rZoomOut[2] = 22; m_rZoomOut[3] = 22; m_rZoomIn[0] = 84; m_rZoomIn[1] = footY; m_rZoomIn[2] = 22; m_rZoomIn[3] = 22; //--- Sit the view switch to the right of the zoom readout m_rView[0] = 152; m_rView[1] = footY; m_rView[2] = 62; m_rView[3] = 22; //--- Sit the inspector close button in its top right corner m_rInspectX[0] = m_inspect ? m_w - 30 : 0; m_rInspectX[1] = CanvasTop() + 8; m_rInspectX[2] = m_inspect ? OP_ICON : 0; m_rInspectX[3] = OP_ICON; //--- Sit the grip in the bottom right corner m_rGrip[0] = m_w - OP_GRIP - 2; m_rGrip[1] = m_h - OP_GRIP - 2; m_rGrip[2] = OP_GRIP; m_rGrip[3] = OP_GRIP; } //+------------------------------------------------------------------+ //| Create the bitmap the panel draws into | //+------------------------------------------------------------------+ bool COmvPanel::CreateCanvas(void) { //--- Build the bitmap once at its largest, so its size never changes again if(!m_cv.CreateBitmapLabel(m_chartId, 0, m_name, m_x, m_y, OP_W_MAX, OP_H_MAX, COLOR_FORMAT_ARGB_NORMALIZE)) return false; //--- Pin it to the top left corner of the chart ObjectSetInteger(m_chartId, m_name, OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(m_chartId, m_name, OBJPROP_BACK, false); ObjectSetInteger(m_chartId, m_name, OBJPROP_SELECTABLE, false); ObjectSetInteger(m_chartId, m_name, OBJPROP_HIDDEN, true); ObjectSetInteger(m_chartId, m_name, OBJPROP_ZORDER, 0); ObjectSetInteger(m_chartId, m_name, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER); ObjectSetString(m_chartId, m_name, OBJPROP_TOOLTIP, "\n"); //--- Size the chrome cache to the bitmap, which never changes again ArrayResize(m_bg, m_cv.PixelCount()); ArrayResize(m_scene, m_cv.PixelCount()); m_bgValid = false; m_sceneValid = false; //--- Report that the canvas exists m_created = true; return true; } //+------------------------------------------------------------------+ //| Resize the bitmap to match the current mode | //+------------------------------------------------------------------+ bool COmvPanel::ApplyMode(void) { //--- Take the height the mode calls for, the bitmap itself never changes m_h = (m_mode == OM_MIN) ? OP_HEADER_H : m_fullH; ComputeLayout(); m_bgValid = false; //--- Report success return true; } //+------------------------------------------------------------------+ //| Suppress chart scrolling while the pointer works the panel | //+------------------------------------------------------------------+ void COmvPanel::LockChartScroll(const bool lock) { //--- Skip the call when the state already matches if(lock == m_scrollLocked) return; //--- Toggle scrolling and remember the new state ChartSetInteger(m_chartId, CHART_MOUSE_SCROLL, !lock); m_scrollLocked = lock; } //+------------------------------------------------------------------+ //| Pick the fill color for a node role | //+------------------------------------------------------------------+ uint COmvPanel::FillForRole(const int role) const { //--- Match the role to its fill switch(role) { case OL_ROLE_COMPUTE: return m_th.computeFill; case OL_ROLE_ACTIVATE: return m_th.activateFill; case OL_ROLE_SHAPE: return m_th.shapeFill; case OL_ROLE_OUTPUT: return m_th.outputFill; case OL_ROLE_PORT: return m_th.chrome; } //--- Fall back to the plain fill return m_th.plainFill; } //+------------------------------------------------------------------+ //| Pick the edge color for a node role | //+------------------------------------------------------------------+ uint COmvPanel::EdgeForRole(const int role) const { //--- Match the role to its edge switch(role) { case OL_ROLE_COMPUTE: return m_th.computeEdge; case OL_ROLE_ACTIVATE: return m_th.activateEdge; case OL_ROLE_SHAPE: return m_th.shapeEdge; case OL_ROLE_OUTPUT: return m_th.outputEdge; case OL_ROLE_PORT: return m_th.text; } //--- Fall back to the plain edge return m_th.plainEdge; } //+------------------------------------------------------------------+ //| Name what a control does for its tooltip | //+------------------------------------------------------------------+ string COmvPanel::TipFor(const int id) const { //--- Match the control to the words describing it switch(id) { case OH_HEADER: return "Drag to move"; case OH_THEME: return m_dark ? "Switch to light" : "Switch to dark"; case OH_MIN: return "Collapse"; case OH_RESTORE: return "Expand"; case OH_CLOSE: return "Remove panel"; case OH_FIT: return m_neuronView ? "Fit neurons" : "Fit graph"; case OH_VIEW: //--- Say why the control cannot be used rather than leaving it silent if(!m_neuronView && !m_cells.Ready()) return "Neuron view unavailable: " + m_cells.Reason(); return m_neuronView ? "Back to the graph" : "Show the neurons"; case OH_ZOOM_OUT: return "Zoom out"; case OH_ZOOM_IN: return "Zoom in"; case OH_INSPECT_X: return "Close inspector"; case OH_GRIP: return "Drag to resize"; case OH_BAR_V: return "Scroll vertically"; case OH_BAR_H: return "Scroll horizontally"; case OH_CANVAS: return "Drag to pan, click for model properties"; case OH_ROW: return "Tensor properties"; case OH_LINK: return "Connection properties"; case OH_INFO: //--- Name the half of the row the pointer is actually on if(m_infoHover < 0 || m_infoHover >= m_infos) return ""; if(m_onExpander) return m_info[m_infoHover].isOpen ? "Click to collapse" : "Click to expand"; if(StringLen(m_info[m_infoHover].linkName) > 0) return "Show in graph"; return m_info[m_infoHover].canOpen ? "Click to expand" : ""; case OH_INFO_BAR: return "Scroll properties"; case OH_NODE: return "Node properties"; } //--- Report nothing for anything without a tooltip return ""; } //+------------------------------------------------------------------+ //| Publish the tooltip the terminal shows over the panel | //+------------------------------------------------------------------+ void COmvPanel::SetTooltip(const string words) { //--- A single newline is how the terminal is told to show nothing const string shown = (StringLen(words) > 0) ? words : "\n"; //--- Skip the call when the object already carries this text if(shown == m_tipShown) return; //--- Hand the text to the object the terminal draws the tooltip for ObjectSetString(m_chartId, m_name, OBJPROP_TOOLTIP, shown); m_tipShown = shown; } //+------------------------------------------------------------------+ //| Hold the pan offsets inside what the graph allows | //+------------------------------------------------------------------+ void COmvPanel::ClampPan(void) { //--- Settle which bars are showing before the view is measured SizeBars(); //--- Measure the view and the scaled graph const int viewW = ViewW(), viewH = ViewH(); const int fullW = ScaledW(), fullH = ScaledH(); //--- Center the graph when it fits, otherwise hold it against its margin if(fullW <= viewW - OP_PAD * 2) m_panX = (viewW - fullW) / 2; else { //--- Never pull the graph past the margin on either side if(m_panX > OP_PAD) m_panX = OP_PAD; if(m_panX < viewW - fullW - OP_PAD) m_panX = viewW - fullW - OP_PAD; } //--- Apply the same rule vertically if(fullH <= viewH - OP_PAD * 2) m_panY = (viewH - fullH) / 2; else { //--- Never pull the graph past the margin above or below if(m_panY > OP_PAD) m_panY = OP_PAD; if(m_panY < viewH - fullH - OP_PAD) m_panY = viewH - fullH - OP_PAD; } } //+------------------------------------------------------------------+ //| Scale and center the graph inside the canvas | //+------------------------------------------------------------------+ void COmvPanel::FitGraph(void) { //--- Measure the room the canvas offers once the margin is taken out const int roomW = ViewW() - OP_PAD * 2; const int roomH = ViewH() - OP_PAD * 2; //--- Abort when the canvas or the graph has no size if(roomW <= 0 || roomH <= 0 || ViewWide() <= 0 || ViewHigh() <= 0) return; //--- Take the tighter of the two ratios so nothing overflows const double byWidth = (double)roomW / ViewWide(); const double byHeight = (double)roomH / ViewHigh(); double zoom = MathMin(byWidth, byHeight); //--- Let the neurons fill the canvas, but never enlarge the lettered boxes const double ceiling = m_neuronView ? OP_ZOOM_MAX : 1.0; if(zoom > ceiling) zoom = ceiling; if(zoom < OP_ZOOM_MIN) zoom = OP_ZOOM_MIN; m_zoom = zoom; //--- Center what the zoom produced ClampPan(); } //+------------------------------------------------------------------+ //| Apply a zoom level within the allowed range | //+------------------------------------------------------------------+ void COmvPanel::SetZoom(const double zoom) { //--- Remember where the view center sits in graph space const double cx = (ViewW() * 0.5 - m_panX) / m_zoom; const double cy = (ViewH() * 0.5 - m_panY) / m_zoom; //--- Clamp the request to the readable range double next = zoom; if(next < OP_ZOOM_MIN) next = OP_ZOOM_MIN; if(next > OP_ZOOM_MAX) next = OP_ZOOM_MAX; m_zoom = next; //--- Put the same graph point back under the view center m_panX = (int)(ViewW() * 0.5 - cx * m_zoom); m_panY = (int)(ViewH() * 0.5 - cy * m_zoom); ClampPan(); } //+------------------------------------------------------------------+ //| Handle a wheel turn over the graph | //+------------------------------------------------------------------+ void COmvPanel::OnWheel(const int mx, const int my, const int delta) { //--- Scroll the inspector when the pointer sits over its column if(m_inspect && mx > CanvasRight() && my >= CanvasTop() && my < CanvasBottom()) { //--- Step the listing by one notch m_infoScroll += (delta > 0) ? -OP_WHEEL : OP_WHEEL; //--- Hold the new offset inside the listing before it is drawn if(m_infoScroll < 0) m_infoScroll = 0; PaintCapped(); return; } //--- Ignore a turn that lands outside the graph area if(mx < CanvasLeft() || mx >= CanvasRight() || my < CanvasTop() || my >= CanvasBottom()) return; //--- Step one notch per event, taking only the sign so fine wheels still work const int step = (delta > 0) ? OP_WHEEL : -OP_WHEEL; //--- Scroll the axis that actually has somewhere to go if(m_barV) m_panY += step; else m_panX += step; ClampPan(); PaintCapped(); } //+------------------------------------------------------------------+ //| Write the endpoint line a connection reports | //+------------------------------------------------------------------+ void COmvPanel::AddEndpoint(const string side, const int boxIndex) { //--- Refuse an index no box answers to if(boxIndex < 0 || boxIndex >= m_layout.Boxes()) return; //--- Fetch the box the connection reaches OmvBox box; m_layout.BoxAt(boxIndex, box); //--- A graph port is a value, so it reports its own name and nothing more if(box.node < 0) { AddRow(side, "name: " + box.portName, false, box.portName); return; } //--- A layer reports the operator, then the name it was given OmvNode node; m_model.NodeAt(box.node, node); AddRow(side, "node: " + node.op, false); AddLine("name: " + node.name); } //+------------------------------------------------------------------+ //| Print a number without letting a small one read as zero | //+------------------------------------------------------------------+ string COmvPanel::Measure(const double value) const { //--- Print an exact zero plainly if(value == 0.0) return "0"; //--- Fall back to scientific notation where fixed digits would round to zero if(MathAbs(value) < 0.0000001) return DoubleToString(value, -8); //--- Print anything larger at a width the column can hold return DoubleToString(value, 8); } //+------------------------------------------------------------------+ //| Name the input an operator gives a position | //+------------------------------------------------------------------+ string COmvPanel::ArgIn(const string op, const int slot) const { //--- The general matrix operator names its three operands if(op == "Gemm" || op == "MatMul") return StringSubstr("ABC", slot, 1); //--- These operators name their single operand X if(op == "Relu" || op == "Sigmoid" || op == "LeakyRelu" || op == "Elu" || op == "Sqrt") return "X"; //--- Elementwise pairs name their two operands A and B if(op == "Mul" || op == "Add" || op == "Sub" || op == "Div" || op == "Pow") return StringSubstr("AB", slot, 1); //--- Concat takes a list rather than numbered operands if(op == "Concat") return "inputs"; //--- Everything else names its operand for what it is return "input"; } //+------------------------------------------------------------------+ //| Name the output an operator gives a position | //+------------------------------------------------------------------+ string COmvPanel::ArgOut(const string op) const { //--- The general matrix operator names its result Y if(op == "Gemm" || op == "MatMul" || op == "Relu" || op == "Sigmoid" || op == "LeakyRelu" || op == "Elu" || op == "Sqrt") return "Y"; //--- Elementwise pairs name their result C if(op == "Mul" || op == "Add" || op == "Sub" || op == "Div" || op == "Pow") return "C"; //--- Concat names its result for what it built if(op == "Concat") return "concat_result"; //--- Everything else names its result for what it is return "output"; } //+------------------------------------------------------------------+ //| Name the tensor an index points at | //+------------------------------------------------------------------+ string COmvPanel::TensorNameAt(const int index) const { //--- Refuse an index no tensor answers to if(index < 0 || index >= m_model.Tensors()) return ""; //--- Fetch the tensor and report its name OmvTensor tensor; m_model.TensorAt(index, tensor); return tensor.name; } //+------------------------------------------------------------------+ //| Report whether a point sits on a row's expander | //+------------------------------------------------------------------+ bool COmvPanel::OnExpander(const int mx, const int row) const { //--- Refuse a row that carries no expander if(row < 0 || row >= m_infos || !m_info[row].canOpen) return false; //--- The expander occupies the right end of the value box const int right = m_w - 16 - ((m_infoHigh > (CanvasBottom() - 8) - (CanvasTop() + 44)) ? OP_BAR + 2 : 0); return (mx >= right - 22); } //+------------------------------------------------------------------+ //| Find the first connection carrying a named value | //+------------------------------------------------------------------+ int COmvPanel::LinkNamed(const string name) const { //--- Scan every connection for the one carrying this value for(int i = 0; i < m_layout.Edges(); i++) { //--- Fetch this connection OmvEdge edge; m_layout.EdgeAt(i, edge); //--- Report it once the name matches if(edge.label == name) return i; } //--- Report that nothing carries it return -1; } //+------------------------------------------------------------------+ //| Find the tensor a name belongs to | //+------------------------------------------------------------------+ int COmvPanel::TensorNamed(const string name) const { //--- Scan every stored tensor for a matching name for(int i = 0; i < m_model.Tensors(); i++) { //--- Fetch this tensor OmvTensor tensor; m_model.TensorAt(i, tensor); //--- Report the one that carries the name if(tensor.name == name) return i; } //--- Report that no tensor carries it return -1; } //+------------------------------------------------------------------+ //| Find the tensor row under a panel point | //+------------------------------------------------------------------+ //| Find the tensor row under a panel point | //+------------------------------------------------------------------+ int COmvPanel::RowAtPoint(const int mx, const int my) { //--- Test every row drawn on the last frame for(int i = 0; i < m_rows; i++) if(mx >= m_rowRect[i][0] && mx < m_rowRect[i][0] + m_rowRect[i][2] && my >= m_rowRect[i][1] && my < m_rowRect[i][1] + m_rowRect[i][3]) return i; //--- Report that no row holds the point return -1; } //+------------------------------------------------------------------+ //| Find the connection under a panel point | //+------------------------------------------------------------------+ int COmvPanel::LinkAtPoint(const int mx, const int my) { //--- Take the closest route, so crossing connections cannot be confused int best = -1; double nearest = 6.0; //--- Measure the pointer against every route drawn on the last frame for(int i = 0; i < m_links; i++) { //--- Walk the segments the route was built from for(int s = 0; s + 1 < m_linkCount[i]; s++) { //--- Take this segment const double ax = m_linkPts[i][s * 2], ay = m_linkPts[i][s * 2 + 1]; const double bx = m_linkPts[i][s * 2 + 2], by = m_linkPts[i][s * 2 + 3]; //--- Project the pointer onto it const double dx = bx - ax, dy = by - ay; const double len2 = dx * dx + dy * dy; double along = (len2 > 1e-9) ? ((mx - ax) * dx + (my - ay) * dy) / len2 : 0.0; //--- Clamp the projection to the segment ends if(along < 0.0) along = 0.0; if(along > 1.0) along = 1.0; //--- Measure how far the pointer sits from the segment const double ox = mx - (ax + dx * along), oy = my - (ay + dy * along); const double dist = MathSqrt(ox * ox + oy * oy); //--- Keep the nearest route within reach if(dist < nearest) { nearest = dist; best = i; } } } //--- Report the connection the closest route belongs to return (best >= 0) ? m_linkEdge[best] : -1; } //+------------------------------------------------------------------+ //| Find the node box under a panel point | //+------------------------------------------------------------------+ int COmvPanel::NodeAtPoint(const int mx, const int my) { //--- Convert the point out of canvas space into graph space const double gx = (mx - CanvasLeft() - m_panX) / m_zoom; const double gy = (my - CanvasTop() - m_panY) / m_zoom; //--- Test every placed box for(int i = 0; i < m_layout.Boxes(); i++) { //--- Fetch this box OmvBox box; m_layout.BoxAt(i, box); //--- Report the box that contains the point if(gx >= box.x && gx < box.x + box.w && gy >= box.y && gy < box.y + box.h) return i; } //--- Report that no box holds the point return -1; } //+------------------------------------------------------------------+ //| Identify the control under a panel point | //+------------------------------------------------------------------+ int COmvPanel::HitTest(const int mx, const int my) { //--- Test the header icons before the band behind them if(InRect(m_rTheme, mx, my)) return OH_THEME; if(InRect(m_rMin, mx, my)) return (m_mode == OM_MIN) ? OH_RESTORE : OH_MIN; if(InRect(m_rClose, mx, my)) return OH_CLOSE; //--- Report the header band itself if(my < OP_HEADER_H) return OH_HEADER; //--- Report nothing else while collapsed if(m_mode == OM_MIN) return OH_NONE; //--- Test the grip before the footer behind it if(InRect(m_rGrip, mx, my)) return OH_GRIP; //--- Test the footer controls if(InRect(m_rFit, mx, my)) return OH_FIT; if(InRect(m_rView, mx, my)) return OH_VIEW; if(InRect(m_rZoomOut, mx, my)) return OH_ZOOM_OUT; if(InRect(m_rZoomIn, mx, my)) return OH_ZOOM_IN; //--- Test the inspector close button if(InRect(m_rInspectX, mx, my)) return OH_INSPECT_X; //--- Test the inspector scrollbar before the rows behind it if(InRect(m_rInfoBar, mx, my)) return OH_INFO_BAR; //--- Test the inspector rows, ignoring any that lead nowhere const int infoRow = InfoAtPoint(mx, my); if(infoRow >= 0 && (m_info[infoRow].canOpen || StringLen(m_info[infoRow].linkName) > 0)) return OH_INFO; //--- Test both scrollbar thumbs if(InRect(m_rBarV, mx, my)) return OH_BAR_V; if(InRect(m_rBarH, mx, my)) return OH_BAR_H; //--- Test the graph area last, smallest target first if(mx >= CanvasLeft() && mx < CanvasRight() && my >= CanvasTop() && my < CanvasBottom()) { //--- The neuron view holds no boxes, rows or connections to pick if(m_neuronView) return OH_CANVAS; //--- A tensor row sits inside a node, so it wins over the node if(RowAtPoint(mx, my) >= 0) return OH_ROW; //--- A node wins over the empty canvas if(NodeAtPoint(mx, my) >= 0) return OH_NODE; //--- A connection is the last thing that can be grabbed if(LinkAtPoint(mx, my) >= 0) return OH_LINK; return OH_CANVAS; } //--- Report nothing clickable return OH_NONE; } //+------------------------------------------------------------------+ //| Draw a labeled button | //+------------------------------------------------------------------+ void COmvPanel::DrawButton(const int &r[], const int id, const string label) { //--- Light the button when the pointer sits over it const bool lit = (m_hover == id || m_press == id); //--- Fill and outline the button face m_cv.RoundFill(r[0], r[1], r[2], r[3], OP_BTN_R, lit ? m_th.hoverFace : m_th.btnFace); m_cv.RoundStroke(r[0], r[1], r[2], r[3], OP_BTN_R, 1, lit ? m_th.hoverEdge : m_th.btnEdge); //--- Measure the label, then center it inside the button m_cv.Font(8); const int labelH = m_cv.TextHigh(label); m_cv.TxtC(r[0] + r[2] / 2, r[1] + (r[3] - labelH) / 2, label, m_th.text); } //+------------------------------------------------------------------+ //| Draw an icon button | //+------------------------------------------------------------------+ void COmvPanel::DrawIcon(const int &r[], const int id, const int which) { //--- Skip a control the current mode hides if(r[2] <= 0) return; //--- Light the button when the pointer sits over it const bool lit = (m_hover == id || m_press == id); //--- Fill the face only while lit so the chrome stays clean if(lit) { m_cv.RoundFill(r[0], r[1], r[2], r[3], OP_BTN_R, m_th.hoverFace); m_cv.RoundStroke(r[0], r[1], r[2], r[3], OP_BTN_R, 1, m_th.hoverEdge); } //--- Find the middle of the button and the box the glyph fits const int cx = r[0] + r[2] / 2; const int cy = r[1] + r[3] / 2; const double sz = (double)MathMin(r[2], r[3]) - 4.0; const uint ink = lit ? m_th.text : m_th.muted; //--- Draw the sun that offers the light theme if(which == 0) { //--- Fill the core then lay eight spokes around it m_cv.DiscAA(cx, cy, (int)MathRound(sz * 0.24), ink); for(int i = 0; i < 8; i++) { //--- Step around the circle one eighth at a time const double ang = i * (M_PI / 4.0); const double cs = MathCos(ang), sn = MathSin(ang); m_cv.StrokeAA(cx + cs * sz * 0.36, cy + sn * sz * 0.36, cx + cs * sz * 0.52, cy + sn * sz * 0.52, 2, ink); } return; } //--- Draw the moon that offers the dark theme if(which == 7) { //--- Draw the crescent as one shape so no background bleeds into its rim const double moonR = sz * 0.42; m_cv.CrescentAA(cx, cy, moonR, moonR * 0.58, -moonR * 0.44, moonR * 0.90, ink); return; } //--- Draw the bar that collapses the panel or steps the zoom down if(which == 1 || which == 3) { m_cv.StrokeAA(cx - sz * OP_BAR_R, cy, cx + sz * OP_BAR_R, cy, 2, ink); return; } //--- Draw the cross that removes the panel or closes the inspector if(which == 2 || which == 5) { m_cv.StrokeAA(cx - sz * OP_GLYPH, cy - sz * OP_GLYPH, cx + sz * OP_GLYPH, cy + sz * OP_GLYPH, 2, ink); m_cv.StrokeAA(cx + sz * OP_GLYPH, cy - sz * OP_GLYPH, cx - sz * OP_GLYPH, cy + sz * OP_GLYPH, 2, ink); return; } //--- Draw the plus that steps the zoom up if(which == 4) { m_cv.StrokeAA(cx - sz * OP_BAR_R, cy, cx + sz * OP_BAR_R, cy, 2, ink); m_cv.StrokeAA(cx, cy - sz * OP_BAR_R, cx, cy + sz * OP_BAR_R, 2, ink); return; } //--- Draw the box that restores the panel m_cv.RoundStroke(cx - (int)(sz * OP_GLYPH), cy - (int)(sz * 0.22), (int)(sz * OP_GLYPH * 2), (int)(sz * 0.44), 2, 1, ink); } //+------------------------------------------------------------------+ //| Name what the kept scene was drawn for | //+------------------------------------------------------------------+ string COmvPanel::SceneSignature(void) { //--- Any change here means the drawn view no longer matches the panel return StringFormat("%s|%d|%d|%d|%d|%d|%d|%d|%d|%d|%s", ChromeSignature(), m_panX, m_panY, (int)(m_zoom * 1000), m_selected, m_pickKind, m_hoverNode, m_hoverRow, m_pickTensor, m_pickLink, m_litName); } //+------------------------------------------------------------------+ //| Describe everything the cached chrome depends on | //+------------------------------------------------------------------+ string COmvPanel::ChromeSignature(void) { //--- Any change here means the cached chrome no longer matches the panel return StringFormat("%d|%d|%d|%d|%d|%d|%d|%d|%d", m_w, m_h, m_mode, (int)m_dark, (int)m_inspect, (int)m_neuronView, m_model.Nodes(), m_model.Tensors(), m_model.Parameters()); } //+------------------------------------------------------------------+ //| Draw every band that does not change between frames | //+------------------------------------------------------------------+ void COmvPanel::DrawChrome(void) { //--- Lay the body down first DrawShell(); //--- Draw the bands that never move if(m_mode == OM_FULL) { DrawStats(); DrawFooterChrome(); } //--- Fill the header band under the icons m_cv.RoundFillSel(0, 0, m_w, OP_HEADER_H, OP_RADIUS, m_th.chrome, true, true, m_mode == OM_MIN, m_mode == OM_MIN); //--- Rule the band off only when something sits below it if(m_mode != OM_MIN) m_cv.HRule(1, m_w - 2, OP_HEADER_H - 1, m_th.divider); //--- Outline the whole panel so no band overwrites the edge m_cv.RoundStroke(0, 0, m_w, m_h, OP_RADIUS, 1, m_th.border); } //+------------------------------------------------------------------+ //| Draw the panel body and its outline | //+------------------------------------------------------------------+ void COmvPanel::DrawShell(void) { //--- Clear the whole bitmap, so whatever the panel no longer covers is clear m_cv.NoClip(); m_cv.Erase(0); //--- Fill the body with every corner smoothed m_cv.RoundFill(0, 0, m_w, m_h, OP_RADIUS, m_th.panel); } //+------------------------------------------------------------------+ //| Draw the header title and icons | //+------------------------------------------------------------------+ void COmvPanel::DrawHeader(void) { //--- Draw the eye mark that identifies the viewer m_cv.DiscAA(30, 22, 8, m_th.accent); m_cv.DiscAA(30, 22, 6, m_th.chrome); m_cv.DiscAA(30, 22, 3, m_th.accent); //--- Name the program m_cv.Font(11, true); const int titleRoom = m_rTheme[0] - 48 - 12; string head[]; if(titleRoom > 0 && m_cv.WrapText(m_title, titleRoom, head, 1) > 0) m_cv.TxtL(48, 10, head[0], m_th.text); //--- Describe the loaded file under the name m_cv.Font(8); string subtitle = m_model.GraphName(); if(StringLen(subtitle) == 0) subtitle = "no graph"; string tail[]; const string line = subtitle + " opset " + IntegerToString(m_model.Opset()); if(titleRoom > 0 && m_cv.WrapText(line, titleRoom, tail, 1) > 0) m_cv.TxtL(48, 27, tail[0], m_th.muted); //--- Draw the three header icons DrawIcon(m_rTheme, OH_THEME, m_dark ? 0 : 7); DrawIcon(m_rMin, (m_mode == OM_MIN) ? OH_RESTORE : OH_MIN, (m_mode == OM_MIN) ? 6 : 1); DrawIcon(m_rClose, OH_CLOSE, 2); } //+------------------------------------------------------------------+ //| Draw the statistics strip | //+------------------------------------------------------------------+ void COmvPanel::DrawStats(void) { //--- Fill the strip inside the outline so the graph cannot show through m_cv.FillRect(1, OP_HEADER_H, m_w - 2, OP_HEADER_H + OP_STAT_H, m_th.panel); m_cv.HRule(1, m_w - 2, OP_HEADER_H + OP_STAT_H, m_th.divider); //--- Split the strip into five equal columns const int columns = 5; const int span = m_w / columns; const int top = OP_HEADER_H; //--- Name every column string label[5]; label[0] = "nodes"; label[1] = "parameters"; label[2] = "tensors"; label[3] = "input"; label[4] = "output"; //--- Read the value for every column string shown[5]; shown[0] = IntegerToString(m_model.Nodes()); shown[1] = IntegerToString(m_model.Parameters()); shown[2] = IntegerToString(m_model.Tensors()); //--- Take the first graph input shape OmvPort port; m_model.InputAt(0, port); shown[3] = (m_model.Inputs() > 0) ? m_model.DimsText(port.dim, port.dims) : "-"; //--- Take the first graph output shape m_model.OutputAt(0, port); shown[4] = (m_model.Outputs() > 0) ? m_model.DimsText(port.dim, port.dims) : "-"; //--- Draw every column and the rule between them for(int i = 0; i < columns; i++) { //--- Center the caption and the value in this column const int cx = span * i + span / 2; m_cv.Font(8); m_cv.TxtC(cx, top + 10, label[i], m_th.muted); m_cv.Font(12, true); m_cv.TxtC(cx, top + 24, shown[i], m_th.text); //--- Rule between this column and the previous one if(i > 0) m_cv.VRule(span * i, top + 10, top + OP_STAT_H - 10, m_th.divider); } } //+------------------------------------------------------------------+ //| Draw the neurons and the weights joining them | //+------------------------------------------------------------------+ void COmvPanel::DrawNeurons(void) { //--- Measure the area the arrangement is drawn inside const int left = CanvasLeft(), top = CanvasTop(); const int right = CanvasRight(), bottom = CanvasBottom(); //--- Hold every later stroke inside that area less its margin m_cv.SetClip(left + OP_PAD, top + OP_PAD, right - 1 - (m_barV ? OP_BAR : 0) - OP_PAD, bottom - 1 - (m_barH ? OP_BAR : 0) - OP_PAD); //--- Say why the view is empty when the model cannot be drawn if(!m_cells.Ready()) { //--- Print the reason in the middle of the area m_cv.Font(9); string why[]; const int room = right - left - OP_PAD * 4; if(room > 0 && m_cv.WrapText("Neuron view unavailable: " + m_cells.Reason(), room, why, 1) > 0) m_cv.TxtM((left + right) / 2, (top + bottom) / 2, why[0], m_th.muted); m_cv.NoClip(); return; } //--- Take how far one neuron sits from the next at this zoom const double dot = MathMax(2.0, ON_DOT * m_zoom); const double pitch = (ON_DOT + ON_DOT_GAP) * m_zoom; //--- Draw every run of connections under the neurons themselves for(int i = 0; i < m_cells.Webs(); i++) { //--- Fetch this run and the two rows it joins OmvWeb web; m_cells.WebAt(i, web); OmvBand a, b; m_cells.BandAt(web.from, a); m_cells.BandAt(web.to, b); //--- Place both rows against the current pan and zoom const double ay = top + m_panY + (a.y + ON_DOT) * m_zoom; const double by = top + m_panY + b.y * m_zoom; const double ax = left + m_panX + a.x * m_zoom + dot * 0.5; const double bx = left + m_panX + b.x * m_zoom + dot * 0.5; //--- Draw a structural run as one line per neuron, carrying no weight if(web.tensor < 0) { //--- Join each source neuron to the place it lands on the joined row for(int j = 0; j < a.count; j++) m_cv.WireAA(ax + j * pitch, ay, bx + (web.offset + j) * pitch, by, m_th.divider); continue; } //--- Fetch the weights this run draws OmvTensor w; m_model.TensorAt(web.tensor, w); //--- Scale the color and thickness against the largest weight held const double span = MathMax(MathAbs(w.minValue), MathAbs(w.maxValue)); if(span <= 0.0 || w.kept <= 0) continue; //--- Thin the run when a full matrix would be more lines than we can draw const int lines = a.count * b.count; const double keep = (lines > ON_MAX_LINES) ? (double)ON_MAX_LINES / lines : 1.0; const double floor = (keep < 1.0) ? span * (1.0 - keep) : 0.0; //--- Draw one line per weight, from each source neuron to each target for(int r = 0; r < a.count; r++) for(int c = 0; c < b.count; c++) { //--- Take the weight joining this pair, whichever way it is stored const int at = web.transposed ? c * a.count + r : r * b.count + c; if(at >= w.kept) continue; const double v = w.val[at]; //--- Drop the weakest connections when the run had to be thinned if(MathAbs(v) < floor) continue; //--- Fade the line by how strong the weight is against the largest const double strength = MathAbs(v) / span; const uint ink = (v >= 0.0) ? m_th.linkUp : m_th.linkDown; const uchar alpha = (uchar)(40.0 + 175.0 * strength); //--- Join the two neurons at the color the weight implies m_cv.WireAA(ax + r * pitch, ay, bx + c * pitch, by, (ink & 0x00FFFFFF) | ((uint)alpha << 24)); } } //--- Draw every neuron over the connections that reach it for(int i = 0; i < m_cells.Bands(); i++) { //--- Fetch this row OmvBand band; m_cells.BandAt(i, band); //--- Place it against the current pan and zoom const double y = top + m_panY + band.y * m_zoom; const double x = left + m_panX + band.x * m_zoom; //--- Draw one filled circle per neuron for(int j = 0; j < band.count; j++) m_cv.DiscAA((int)(x + j * pitch + dot * 0.5), (int)(y + dot * 0.5), (int)MathMax(1.0, dot * 0.5), m_th.cell); //--- Name the row over its own centre, so two rows never collide m_cv.Font((int)MathMax(1.0, MathRound(8 * m_zoom))); const string caption = band.label + " " + IntegerToString(band.count); const int high = m_cv.TextHigh(caption); const int wide = m_cv.TextWide(caption); const int padX = (int)MathMax(2.0, 4 * m_zoom); //--- Centre the plate on the row it names const int plateW = wide + padX * 2; const int plateX = (int)(x + (band.w * m_zoom - plateW) / 2); //--- Sit the caption above the row, or below it where there is no room int capY = (int)(y - high - padX * 2); if(capY - padX < CanvasTop() + OP_PAD) capY = (int)(y + dot + padX * 2); //--- Draw the caption while its own plate still sits inside the view if(m_cv.InClip(plateX, capY - padX / 2, plateX + plateW, capY + high + padX / 2)) { //--- Tint the plate rather than hiding what runs behind it const int radius = (int)MathMax(1.0, 3 * m_zoom); m_cv.RoundFill(plateX, capY - padX / 2, plateW, high + padX, radius, (m_th.panel & 0x00FFFFFF) | 0x80000000); m_cv.RoundStroke(plateX, capY - padX / 2, plateW, high + padX, radius, 1, m_th.divider); m_cv.TxtM(plateX + plateW / 2, capY + high / 2, caption, m_th.text); } } //--- Open the clip again for whatever draws next m_cv.NoClip(); } //+------------------------------------------------------------------+ //| Draw the graph inside its canvas area | //+------------------------------------------------------------------+ void COmvPanel::DrawGraph(void) { //--- Measure the area the graph may occupy const int left = CanvasLeft(); const int top = CanvasTop(); const int right = CanvasRight(); const int bottom = CanvasBottom(); //--- Hold every later stroke inside the area less its margin m_cv.SetClip(left + OP_PAD, top + OP_PAD, right - 1 - (m_barV ? OP_BAR : 0) - OP_PAD, bottom - 1 - (m_barH ? OP_BAR : 0) - OP_PAD); //--- Start the pickable lists empty for this frame m_rows = 0; m_links = 0; //--- Report an empty graph rather than drawing nothing if(m_layout.Boxes() <= 0) { m_cv.Font(9); m_cv.TxtC((left + right) / 2, (top + bottom) / 2, "no graph to show", m_th.muted); return; } //--- Draw every edge under the boxes it connects for(int i = 0; i < m_layout.Edges(); i++) { //--- Fetch this edge OmvEdge edge; m_layout.EdgeAt(i, edge); //--- Fetch the two boxes it joins OmvBox a, b; m_layout.BoxAt(edge.from, a); m_layout.BoxAt(edge.to, b); //--- Count how many edges enter the target and which one this is int arrives = 0, ordinal = 0; for(int k = 0; k < m_layout.Edges(); k++) { //--- Fetch the candidate edge OmvEdge other; m_layout.EdgeAt(k, other); //--- Count only the ones landing on the same box if(other.to != edge.to) continue; if(k == i) ordinal = arrives; arrives++; } //--- Count how many edges leave the source and which one this is int departs = 0, place = 0; for(int k = 0; k < m_layout.Edges(); k++) { //--- Fetch the candidate edge OmvEdge other; m_layout.EdgeAt(k, other); //--- Count only the ones leaving the same box if(other.from != edge.from) continue; if(k == i) place = departs; departs++; } //--- Spread both ends so no two routes can share a point const double leaveAt = (double)(place + 1) / (departs + 1); const double share = (double)(ordinal + 1) / (arrives + 1); //--- Leave the bottom of the source box at this edge's own share of it const double x1 = left + m_panX + (a.x + a.w * leaveAt) * m_zoom; const double y1 = top + m_panY + (a.y + a.h) * m_zoom; //--- Enter the top of the target box at this edge's own share of it const double x2 = left + m_panX + (b.x + b.w * share) * m_zoom; const double y2 = top + m_panY + b.y * m_zoom; //--- Skip an edge that falls wholly outside the visible area if(MathMax(y1, y2) < top || MathMin(y1, y2) > bottom) continue; if(MathMax(x1, x2) < left || MathMin(x1, x2) > right) continue; //--- Measure the room each entry point has before the head is sized const double gap = (b.w * m_zoom) / (arrives + 1); //--- Scale the head with the zoom, yielding only where entry points crowd const double head = MathMin(5.0 * m_zoom, gap - 1.0); //--- Only the bend follows the zoom, so the route keeps its shape const double bend = MathMax(4.0, 10.0 * m_zoom); //--- Light every connection carrying the value the pointer is on bool hot = (StringLen(m_litName) > 0 && edge.label == m_litName); //--- Light every connection carrying the value the inspector describes if(!hot && m_pickKind == OP_PICK_LINK && m_pickLink >= 0) { //--- Fetch the chosen connection to read the value it carries OmvEdge chosen; m_layout.EdgeAt(m_pickLink, chosen); hot = (edge.label == chosen.label); } //--- Draw a bent connector so crossings read cleanly m_cv.Elbow(x1, y1, x2, y2, bend, head, hot ? m_th.accent : m_th.muted); //--- Remember the route itself so the pointer can pick the right one ArrayResize(m_linkPts, m_links + 1); ArrayResize(m_linkCount, m_links + 1); ArrayResize(m_linkEdge, m_links + 1); //--- Tie the route to its connection, since skipped edges shift the slots m_linkEdge[m_links] = i; const double midY = (y1 + (y2 - head)) * 0.5; m_linkPts[m_links][0] = (int)x1; m_linkPts[m_links][1] = (int)y1; m_linkPts[m_links][2] = (int)x1; m_linkPts[m_links][3] = (int)midY; m_linkPts[m_links][4] = (int)x2; m_linkPts[m_links][5] = (int)midY; m_linkPts[m_links][6] = (int)x2; m_linkPts[m_links][7] = (int)y2; m_linkCount[m_links] = 4; m_links++; //--- Print the shape the connection carries beside its drop if(StringLen(edge.shape) == 0) continue; //--- Give the label the same room a weight row gets, so both scale alike const int labelH = (int)MathMax(1.0, OL_ROW_H * m_zoom) - 1; const int labelW = (int)MathMax(1.0, OL_BOX_W * m_zoom); m_cv.FitFont(edge.shape, labelW, labelH, 8, 1, false); const int lw = m_cv.TextWide(edge.shape); const int lh = m_cv.TextHigh(edge.shape); const int lx = (int)x1 + 5; const int ly = (int)(y1 + (y2 - y1) * 0.25); //--- Draw it while its own glyphs still sit inside the view if(m_cv.InClip(lx, ly - lh / 2, lx + lw, ly + lh / 2)) m_cv.TxtL(lx, ly - lh / 2, edge.shape, m_th.muted); } //--- Draw every box over the edges for(int i = 0; i < m_layout.Boxes(); i++) { //--- Fetch this box OmvBox box; m_layout.BoxAt(i, box); //--- Convert it out of graph space into canvas space const int bx = left + m_panX + (int)(box.x * m_zoom); const int by = top + m_panY + (int)(box.y * m_zoom); const int bw = (int)(box.w * m_zoom); const int bh = (int)(box.h * m_zoom); //--- Skip a box that falls outside the visible area if(bx + bw < left || bx > right || by + bh < top || by > bottom) continue; //--- Take the caption from the node, or from the port for a port box OmvNode node; string caption = box.portName; if(box.node >= 0) { m_model.NodeAt(box.node, node); caption = node.op; } //--- Fill the box in its role color m_cv.RoundFill(bx, by, bw, bh, 4, FillForRole(box.role)); //--- Give a node holding weights a header strip over a listing body const int headH = (box.rowCount > 0) ? (int)(OL_HEAD_H * m_zoom) : bh; if(box.rowCount > 0) m_cv.RoundFillSel(bx, by, bw, headH, 4, EdgeForRole(box.role), true, true, false, false); //--- A box reads as chosen only while the inspector describes a box const bool chosen = (i == m_selected) && (m_pickKind == OP_PICK_NODE || m_pickKind == OP_PICK_GRAPH); //--- Thicken the outline instead of ringing a marked box const bool marked = (chosen || i == m_hoverNode); m_cv.RoundStroke(bx, by, bw, bh, 4, marked ? 2 : 1, marked ? m_th.accent : EdgeForRole(box.role)); //--- Pick the largest type the header can hold m_cv.FitFont(caption, bw - 6, headH - 2, 11, 1, true); const int titleH = m_cv.TextHigh(caption); //--- Name the operator, light on the strip or in the role color without one const uint titleInk = (box.rowCount > 0) ? FillForRole(box.role) : EdgeForRole(box.role); const int midY = by + headH / 2; const int halfW = m_cv.TextWide(caption) / 2 + 1; //--- Draw the title while its own glyphs still sit inside the view if(m_cv.InClip(bx + bw / 2 - halfW, midY - titleH / 2, bx + bw / 2 + halfW, midY + titleH / 2)) m_cv.TxtM(bx + bw / 2, midY, caption, titleInk); //--- List every weight the node reads, one row each const int rowH = (int)(OL_ROW_H * m_zoom); const int pad = (int)MathMax(1.0, OL_ROW_PAD * m_zoom); for(int r = 0; r < box.rowCount && rowH >= 1; r++) { //--- Place this row inside the body, clear of the header and the floor const int ry = by + headH + pad + r * rowH; if(ry + rowH > by + bh) break; //--- Inset the row by the same margin on every side const int rowX = bx + pad; const int rowW = bw - pad * 2; //--- Remember it so the pointer can pick it ArrayResize(m_rowRect, m_rows + 1); ArrayResize(m_rowTensor, m_rows + 1); m_rowRect[m_rows][0] = rowX; m_rowRect[m_rows][1] = ry; m_rowRect[m_rows][2] = rowW; m_rowRect[m_rows][3] = rowH; m_rowTensor[m_rows] = box.rowTensor[r]; //--- Mark a row the pointer sits on or the inspector describes const bool hot = (m_hoverRow == m_rows) || (m_pickKind == OP_PICK_TENSOR && m_pickTensor == box.rowTensor[r]) || (StringLen(m_litName) > 0 && box.rowName[r] == m_litName); m_rows++; if(hot) m_cv.RoundStroke(rowX, ry, rowW, rowH, 3, 1, m_th.accent); //--- Print the letter the operator calls this input m_cv.FitFont(box.rowText[r], rowW - 8, rowH - 1, 8, 1, false); const int rowTH = m_cv.TextHigh(box.rowText[r]); //--- Draw the row while its own glyphs still sit inside the view if(m_cv.InClip(rowX, ry, rowX + rowW, ry + rowH)) { m_cv.TxtL(rowX + 4, ry + (rowH - rowTH) / 2, box.rowLabel[r], m_th.text); m_cv.TxtR(rowX + rowW - 4, ry + (rowH - rowTH) / 2, box.rowText[r], m_th.muted); } } } //--- Open the clip again for whatever draws next m_cv.NoClip(); } //+------------------------------------------------------------------+ //| Decide which scrollbars the graph currently needs | //+------------------------------------------------------------------+ void COmvPanel::SizeBars(void) { //--- Measure the view as though neither bar were present const int roomW = CanvasRight() - CanvasLeft(); const int roomH = CanvasBottom() - CanvasTop(); //--- Take the graph at the current zoom const int fullW = ScaledW(), fullH = ScaledH(); //--- Decide each bar, allowing for the gutter the other one takes m_barV = (fullH > roomH - OP_PAD * 2); m_barH = (fullW > roomW - OP_PAD * 2 - (m_barV ? OP_BAR : 0)); m_barV = (fullH > roomH - OP_PAD * 2 - (m_barH ? OP_BAR : 0)); } //+------------------------------------------------------------------+ //| Draw whichever scrollbars the graph needs | //+------------------------------------------------------------------+ void COmvPanel::DrawBars(void) { //--- Measure the view and the scaled graph const int viewW = ViewW(), viewH = ViewH(); const int fullW = ScaledW(), fullH = ScaledH(); const int left = CanvasLeft(), top = CanvasTop(); //--- Draw the vertical bar only when the graph is taller than the view if(!m_barV || viewH <= 0) { m_rBarV[2] = 0; m_rBarV[3] = 0; } else { //--- Lay the track down the right edge of the view const int trackX = CanvasRight() - OP_BAR; m_cv.RoundFill(trackX, top, OP_BAR, viewH, OP_BAR / 2, m_th.grid); //--- Size the thumb by how much of the graph is visible int thumbH = (int)((double)viewH * viewH / fullH); if(thumbH < 20) thumbH = 20; //--- Map the whole pan range, margin included, onto the track const int span = fullH - viewH + OP_PAD * 2; double frac = (span > 0) ? (double)(OP_PAD - m_panY) / span : 0.0; if(frac < 0.0) frac = 0.0; if(frac > 1.0) frac = 1.0; //--- Place the thumb inside the track it was given const int thumbY = top + (int)((viewH - thumbH) * frac); //--- Store the thumb so the pointer can grab it m_rBarV[0] = trackX; m_rBarV[1] = thumbY; m_rBarV[2] = OP_BAR; m_rBarV[3] = thumbH; //--- Draw the thumb, lit while the pointer works it const bool lit = (m_hover == OH_BAR_V || m_barDrag == OH_BAR_V); m_cv.RoundFill(trackX, thumbY, OP_BAR, thumbH, OP_BAR / 2, lit ? m_th.accent : m_th.muted); } //--- Draw the horizontal bar only when the graph is wider than the view if(!m_barH || viewW <= 0) { m_rBarH[2] = 0; m_rBarH[3] = 0; return; } //--- Lay the track along the bottom edge of the view const int trackY = CanvasBottom() - OP_BAR; m_cv.RoundFill(left, trackY, viewW, OP_BAR, OP_BAR / 2, m_th.grid); //--- Size the thumb by how much of the graph is visible int thumbW = (int)((double)viewW * viewW / fullW); if(thumbW < 20) thumbW = 20; //--- Map the whole pan range, margin included, onto the track const int span = fullW - viewW + OP_PAD * 2; double frac = (span > 0) ? (double)(OP_PAD - m_panX) / span : 0.0; if(frac < 0.0) frac = 0.0; if(frac > 1.0) frac = 1.0; //--- Place the thumb inside the track it was given const int thumbX = left + (int)((viewW - thumbW) * frac); //--- Store the thumb so the pointer can grab it m_rBarH[0] = thumbX; m_rBarH[1] = trackY; m_rBarH[2] = thumbW; m_rBarH[3] = OP_BAR; //--- Draw the thumb, lit while the pointer works it const bool lit = (m_hover == OH_BAR_H || m_barDrag == OH_BAR_H); m_cv.RoundFill(thumbX, trackY, thumbW, OP_BAR, OP_BAR / 2, lit ? m_th.accent : m_th.muted); } //+------------------------------------------------------------------+ //| Remember or forget that a row is open | //+------------------------------------------------------------------+ void COmvPanel::ToggleRow(const string key) { //--- Forget the key when it is already held open for(int i = 0; i < m_openCount; i++) { //--- Drop it by pulling the last entry into its place if(m_opened[i] != key) continue; m_opened[i] = m_opened[m_openCount - 1]; m_openCount--; ArrayResize(m_opened, m_openCount); return; } //--- Hold the key open when it was not already ArrayResize(m_opened, m_openCount + 1); m_opened[m_openCount++] = key; } //+------------------------------------------------------------------+ //| Append one row to the inspector listing | //+------------------------------------------------------------------+ void COmvPanel::AddRow(const string name, const string value, const bool isHead, const string linkName) { //--- Refuse a row beyond what the listing may hold if(m_infos >= OP_MAX_ROWS) return; //--- Keep a row open across a rebuild by looking its name up bool wasOpen = false; for(int i = 0; i < m_openCount; i++) if(m_opened[i] == name + "|" + value) { wasOpen = true; break; } //--- Describe the row ArrayResize(m_info, m_infos + 1); m_info[m_infos].name = name; m_info[m_infos].value = value; m_info[m_infos].extraCount = 0; m_info[m_infos].wraps = 1; m_info[m_infos].canOpen = false; m_info[m_infos].isOpen = wasOpen; m_info[m_infos].isHead = isHead; m_info[m_infos].linkName = linkName; m_info[m_infos].y = 0; m_info[m_infos].h = 0; m_infos++; } //+------------------------------------------------------------------+ //| Add one line to the row most recently started | //+------------------------------------------------------------------+ void COmvPanel::AddLine(const string line) { //--- Refuse a line before any row has been started if(m_infos <= 0) return; //--- Refuse a line beyond what the row may hold const int last = m_infos - 1; if(m_info[last].extraCount >= OP_MAX_LINES) return; //--- Append the line and mark the row as something that opens m_info[last].extra[m_info[last].extraCount++] = line; m_info[last].canOpen = true; } //+------------------------------------------------------------------+ //| Add the lines a named value opens to | //+------------------------------------------------------------------+ void COmvPanel::AddValueLines(const string name) { //--- A stored weight reports its category before anything else const int k = TensorNamed(name); if(k >= 0) { //--- Fetch the tensor being reported OmvTensor tensor; m_model.TensorAt(k, tensor); AddLine("category: " + tensor.category); AddLine("tensor: " + m_model.TypeName(tensor.dataType) + m_model.DimsList(tensor.dim, tensor.dims)); //--- Say so when the values live outside the file rather than showing none if(tensor.location != "default") AddLine("location: " + tensor.location); //--- Print the weights themselves, wrapped on the last axis AddTensorValues(tensor); return; } //--- A graph port reports the type the graph declares for it for(int p = 0; p < m_model.Inputs(); p++) { //--- Fetch this graph input OmvPort port; m_model.InputAt(p, port); //--- Report its type once the name matches if(port.name != name) continue; AddLine("tensor: " + m_model.TypeName(port.elemType) + m_model.DimsList(port.dim, port.dims)); return; } //--- A value passing between layers reports its type when the file says so for(int p = 0; p < m_model.Inners(); p++) { //--- Fetch this inner value OmvPort port; m_model.InnerAt(p, port); //--- Report its type once the name matches if(port.name != name) continue; AddLine("tensor: " + m_model.TypeName(port.elemType) + m_model.DimsList(port.dim, port.dims)); return; } //--- A graph output reports its type the same way for(int p = 0; p < m_model.Outputs(); p++) { //--- Fetch this graph output OmvPort port; m_model.OutputAt(p, port); //--- Report its type once the name matches if(port.name != name) continue; AddLine("tensor: " + m_model.TypeName(port.elemType) + m_model.DimsList(port.dim, port.dims)); return; } } //+------------------------------------------------------------------+ //| Print a tensor's weights, wrapped on its last axis | //+------------------------------------------------------------------+ void COmvPanel::AddTensorValues(const OmvTensor &tensor) { //--- Print the strings when the tensor holds text rather than numbers if(tensor.texts > 0) { //--- Open the bracket and list one string to a line AddLine("["); for(int s = 0; s < tensor.texts; s++) AddLine(" " + tensor.text[s] + ((s + 1 < tensor.texts) ? "," : "")); AddLine("]"); return; } //--- Nothing to print when the file carried no weights if(tensor.kept <= 0) return; //--- Take the last axis as the length of an innermost run const int inner = (tensor.dims > 0) ? tensor.dim[tensor.dims - 1] : tensor.kept; if(inner <= 0) return; //--- Open the outermost bracket AddLine("["); //--- A one axis tensor is a single run with no nesting const bool nested = (tensor.dims > 1); const string pad = nested ? " " : " "; //--- Walk the weights, one to a line, closing each run as it ends for(int i = 0; i < tensor.kept; i++) { //--- Open a run whenever one begins if(nested && i % inner == 0) AddLine(" ["); //--- Print this weight, with a comma unless it ends its run const bool lastInRun = ((i + 1) % inner == 0) || (i + 1 == tensor.kept); AddLine(pad + DoubleToString(tensor.val[i], 8) + (lastInRun ? "" : ",")); //--- Close a run whenever one ends if(nested && lastInRun) AddLine((i + 1 == tensor.kept) ? " ]" : " ],"); } //--- Say so when the tensor held more than the display keeps if(tensor.values > tensor.kept) AddLine(" ... " + IntegerToString(tensor.values - tensor.kept) + " more"); //--- Close the outermost bracket AddLine("]"); } //+------------------------------------------------------------------+ //| Add the lines a named value opens to | //+------------------------------------------------------------------+ //| Turn the current selection into inspector rows | //+------------------------------------------------------------------+ void COmvPanel::BuildInfo(void) { //--- Start a fresh listing const int held = m_infos; m_infos = 0; //--- Describe a chosen weight tensor if(m_pickKind == OP_PICK_TENSOR && m_pickTensor >= 0) { //--- Fetch the tensor being described OmvTensor tensor; m_model.TensorAt(m_pickTensor, tensor); AddRow("name", tensor.name, false); AddRow("category", tensor.category, false); AddRow("type", m_model.TypeName(tensor.dataType), false); AddRow("shape", m_model.DimsBare(tensor.dim, tensor.dims), false); //--- Say so when the file keeps the values somewhere else if(tensor.location != "default") AddRow("location", tensor.location, false); //--- Say so when the file stored this tensor in pieces if(tensor.segmented) AddRow("segment", "present", false); AddRow("value", IntegerToString(tensor.values), false); AddTensorValues(tensor); AddRow("METRICS", "", true); //--- Each metric reports the type it was measured in const string metricType = m_model.TypeName(tensor.dataType); AddRow("min", Measure(tensor.minValue), false); AddLine("type: " + metricType); AddRow("max", Measure(tensor.maxValue), false); AddLine("type: " + metricType); AddRow("mean", Measure(tensor.meanValue), false); AddLine("type: " + metricType); //--- The spread declares no type, so it carries no opened line AddRow("std", Measure(tensor.stdValue), false); AddRow("sparsity", DoubleToString(tensor.sparsity * 100.0, 1) + "%", false); AddLine("type: percentage"); return; } //--- Describe a chosen connection if(m_pickKind == OP_PICK_LINK && m_pickLink >= 0) { //--- Fetch the connection being described OmvEdge edge; m_layout.EdgeAt(m_pickLink, edge); //--- Name the value and report what it opens to AddRow("name", edge.label, false); AddRow("type", "name: " + edge.label, false, edge.label); AddValueLines(edge.label); //--- List every box that writes the value int writers = 0; for(int e = 0; e < m_layout.Edges(); e++) { //--- Take only the connections carrying the same value OmvEdge other; m_layout.EdgeAt(e, other); if(other.label != edge.label) continue; //--- Report each source once, heading the section first if(writers == 0) AddRow("INPUTS", "", true); writers++; AddEndpoint("from", other.from); break; } //--- List every box that reads the value int readers = 0; for(int e = 0; e < m_layout.Edges(); e++) { //--- Take only the connections carrying the same value OmvEdge other; m_layout.EdgeAt(e, other); if(other.label != edge.label) continue; //--- Report each target, heading the section before the first if(readers == 0) AddRow("OUTPUTS", "", true); readers++; AddEndpoint("to", other.to); } return; } //--- Describe the file as a whole if(m_pickKind == OP_PICK_MODEL) { //--- Report the format the file announces AddRow("format", "ONNX v" + IntegerToString(m_model.IrVersion()), false); //--- Report the tool that wrote it, with its version when it gave one if(StringLen(m_model.Producer()) > 0) { string who = m_model.Producer(); if(StringLen(m_model.ProducerVersion()) > 0) who += " " + m_model.ProducerVersion(); AddRow("producer", who, false); } //--- Report the revision the model gives itself AddRow("version", IntegerToString(m_model.ModelVersion()), false); //--- Report whatever the file says about itself if(StringLen(m_model.DocString()) > 0) AddRow("description", m_model.DocString(), false); //--- Report the domain the model belongs to if(StringLen(m_model.Domain()) > 0) AddRow("domain", m_model.Domain(), false); //--- Report the operator set it imports AddRow("imports", "ai.onnx v" + IntegerToString(m_model.Opset()), false); //--- Report whether the file carries training state beside the model if(m_model.HasTraining()) AddRow("training", "present", false); //--- List every operator the file defines for itself if(m_model.Funcs() > 0) { AddRow("FUNCTIONS", "", true); for(int i = 0; i < m_model.Funcs(); i++) { //--- Name the definition, then say what it holds AddRow(m_model.FuncName(i), m_model.FuncDetail(i), false); } } //--- List every key and value pair the file attached to the model if(m_model.Metas() > 0) { AddRow("METADATA", "", true); for(int i = 0; i < m_model.Metas(); i++) AddRow(m_model.MetaKey(i), m_model.MetaValue(i), false); } return; } //--- Describe the graph as a whole if(m_pickKind == OP_PICK_GRAPH) { //--- Name the graph AddRow("name", m_model.GraphName(), false); //--- Report whatever the file says about the graph if(StringLen(m_model.GraphDoc()) > 0) AddRow("description", m_model.GraphDoc(), false); //--- List every value the graph takes in if(m_model.Inputs() > 0) AddRow("INPUTS", "", true); for(int p = 0; p < m_model.Inputs(); p++) { //--- Fetch this graph input OmvPort port; m_model.InputAt(p, port); //--- Report it with its tensor on the open line AddRow(port.name, "name: " + port.name, false, port.name); AddValueLines(port.name); } //--- List every value the graph gives back if(m_model.Outputs() > 0) AddRow("OUTPUTS", "", true); for(int p = 0; p < m_model.Outputs(); p++) { //--- Fetch this graph output OmvPort port; m_model.OutputAt(p, port); //--- Report it with its tensor on the open line AddRow(port.name, "name: " + port.name, false, port.name); AddValueLines(port.name); } //--- Report what the graph weighs AddRow("METRICS", "", true); AddRow("parameters", IntegerToString(m_model.Parameters()), false); //--- Report how many values the graph annotates for quantization if(m_model.Quants() > 0) AddRow("quantized", IntegerToString(m_model.Quants()), false); //--- List every key and value pair the file attached to the graph if(m_model.GraphMetas() > 0) { AddRow("METADATA", "", true); for(int i = 0; i < m_model.GraphMetas(); i++) AddRow(m_model.GraphMetaKey(i), m_model.GraphMetaValue(i), false); } return; } //--- Describe a chosen layer if(m_pickKind == OP_PICK_NODE && m_selected >= 0 && m_selected < m_layout.Boxes()) { //--- Fetch the box the selection points at OmvBox box; m_layout.BoxAt(m_selected, box); //--- Fetch the node the box was placed for OmvNode node; m_model.NodeAt(box.node, node); AddRow("type", node.op, false); //--- An empty domain means the operator comes from the default set const string domain = (StringLen(node.domain) > 0) ? node.domain : "ai.onnx"; AddRow("module", domain + " v" + IntegerToString(m_model.Opset()), false); AddRow("name", node.name, false); //--- Report the overload the operator was resolved to if(StringLen(node.overload) > 0) AddRow("overload", node.overload, false); //--- Report whatever the file says about this layer if(StringLen(node.docString) > 0) AddRow("description", node.docString, false); //--- Report whatever the operator was configured with if(node.attrCount > 0) { AddRow("ATTRIBUTES", "", true); for(int a = 0; a < node.attrCount; a++) { //--- The Cast target names a type rather than carrying a number const bool isType = (node.op == "Cast" && node.attrName[a] == "to"); const string shown = isType ? m_model.TypeName((int)StringToInteger(node.attrValue[a])) : node.attrValue[a]; //--- Report the value, then the type it opens to AddRow(node.attrName[a], shown, false); AddLine("type: " + (isType ? "DataType" : m_model.AttrType(node.attrKind[a]))); } } AddRow("INPUTS", "", true); //--- Name every input the way the operator names it for(int j = 0; j < node.feedCount; j++) { //--- Report the name, then whatever the value opens to AddRow(ArgIn(node.op, j), "name: " + node.feeds[j], false, node.feeds[j]); AddValueLines(node.feeds[j]); } AddRow("OUTPUTS", "", true); //--- List every name the node writes for(int j = 0; j < node.emitCount; j++) { //--- Report the name, then whatever the value opens to AddRow(ArgOut(node.op), "name: " + node.emits[j], false, node.emits[j]); AddValueLines(node.emits[j]); } return; } //--- Leave the listing empty when nothing is chosen if(held > 0) ArrayResize(m_info, 0); } //+------------------------------------------------------------------+ //| Draw the inspector for the selected node | //+------------------------------------------------------------------+ void COmvPanel::DrawInspector(void) { //--- Skip the column entirely while it is closed if(!m_inspect) return; //--- Fill the column edge to edge so the graph can never show through it const int colX = CanvasRight(); m_cv.FillRect(colX + 1, CanvasTop(), m_w - 2, CanvasBottom() - 1, m_th.panel); m_cv.VRule(colX, CanvasTop(), CanvasBottom() - 1, m_th.divider); //--- Measure the column and the band the rows scroll inside const int left = colX + 16; const int right = m_w - 16; const int top = CanvasTop() + 44; const int bottom = CanvasBottom() - 8; //--- Offer to close the column DrawIcon(m_rInspectX, OH_INSPECT_X, 5); //--- Title the column after whatever it is describing m_cv.Font(8, true); string heading = "PROPERTIES"; if(m_pickKind == OP_PICK_NODE) heading = "NODE PROPERTIES"; else if(m_pickKind == OP_PICK_TENSOR) heading = "TENSOR PROPERTIES"; else if(m_pickKind == OP_PICK_LINK) heading = "CONNECTION PROPERTIES"; else if(m_pickKind == OP_PICK_GRAPH) heading = "GRAPH PROPERTIES"; else if(m_pickKind == OP_PICK_MODEL) heading = "MODEL PROPERTIES"; m_cv.TxtL(left, CanvasTop() + 14, heading, m_th.muted); //--- Lay the rows out for whatever is chosen BuildInfo(); //--- Invite a selection when the listing came back empty if(m_infos <= 0) { m_cv.Font(9); m_cv.TxtL(left, top, "select a node", m_th.muted); m_rInfoBar[2] = 0; m_rInfoBar[3] = 0; return; } //--- Measure every row and stack them m_cv.Font(8); const int lineH = m_cv.TextHigh("Ag"); //--- Assume the scrollbar is present so the width cannot change under us const int valX = left + OP_NAME_W; const int valW = right - valX - OP_BAR - 2 - OP_VAL_PAD * 2; int cursor = 0; for(int i = 0; i < m_infos; i++) { //--- A heading takes a line of its own with space above it if(m_info[i].isHead) { m_info[i].y = cursor + 10; m_info[i].h = lineH + 4; m_info[i].wraps = 1; } else { //--- Take how many lines this value needs at the width it is given string lines[]; m_info[i].wraps = MathMax(1, m_cv.WrapText(m_info[i].value, valW, lines, OP_WRAP_MAX)); //--- Grow the box by every line past the first, and again once opened m_info[i].y = cursor; m_info[i].h = OP_VAL_H + (m_info[i].wraps - 1) * lineH + (m_info[i].isOpen ? m_info[i].extraCount * (lineH + 2) + OP_VAL_PAD : 0); } cursor = m_info[i].y + m_info[i].h + OP_VAL_GAP; } m_infoHigh = cursor; //--- Hold the scroll inside what the rows actually need const int band = bottom - top; if(m_infoHigh <= band) m_infoScroll = 0; else { //--- Never scroll past either end of the listing if(m_infoScroll < 0) m_infoScroll = 0; if(m_infoScroll > m_infoHigh - band) m_infoScroll = m_infoHigh - band; } //--- Hold every row inside the band it scrolls in const bool bar = (m_infoHigh > band); m_cv.SetClip(colX + 1, top, right + (bar ? 0 : 8), bottom); //--- Draw every row that falls inside the band for(int i = 0; i < m_infos; i++) { //--- Place this row against the current scroll const int ry = top + m_info[i].y - m_infoScroll; if(ry + m_info[i].h < top || ry > bottom) continue; //--- Print a heading and move on if(m_info[i].isHead) { //--- Text ignores the clip, so keep it inside the band by hand if(ry >= top && ry + lineH <= bottom) { m_cv.Font(8, true); m_cv.TxtL(left, ry, m_info[i].name, m_th.muted); } continue; } //--- Light a row the pointer is on, or one naming the value being lit const bool hot = (m_infoHover == i) || (StringLen(m_litName) > 0 && m_info[i].linkName == m_litName); m_cv.RoundFill(valX, ry, right - valX - (bar ? OP_BAR + 2 : 0), m_info[i].h, 2, hot ? m_th.hoverFace : m_th.grid); //--- Outline the box while the pointer sits on a row that leads somewhere if(hot && (m_info[i].canOpen || StringLen(m_info[i].linkName) > 0)) m_cv.RoundStroke(valX, ry, right - valX - (bar ? OP_BAR + 2 : 0), m_info[i].h, 2, 1, m_th.accent); //--- Take the baseline the name and the first line share const int firstY = ry + (OP_VAL_H - lineH) / 2; const int valRight = right - (bar ? OP_BAR + 2 : 0) - OP_VAL_PAD; //--- Text ignores the clip, so keep the first line inside the band by hand if(firstY >= top && firstY + lineH <= bottom) { //--- Name the row beside its box m_cv.Font(8); string tag[]; if(m_cv.WrapText(m_info[i].name, OP_NAME_W - 10, tag, 1) > 0) m_cv.TxtR(valX - 8, firstY, tag[0], m_th.muted); } //--- Print the value as the lines it wrapped onto m_cv.Font(8); string shown[]; const int rows = m_cv.WrapText(m_info[i].value, valW, shown, OP_WRAP_MAX); for(int k = 0; k < rows; k++) { //--- Place this line and test it against the band itself const int ly = firstY + k * lineH; if(ly < top || ly + lineH > bottom) continue; m_cv.TxtL(valX + OP_VAL_PAD, ly, shown[k], m_th.text); } //--- Offer the expander on a row that carries a second line if(m_info[i].canOpen && firstY >= top && firstY + lineH <= bottom) { //--- Light the glyph only while the pointer is on it const bool onPlus = (hot && m_onExpander); m_cv.Font(8, true); m_cv.TxtR(valRight, firstY, m_info[i].isOpen ? "-" : "+", onPlus ? m_th.accent : m_th.muted); } //--- Print every line the row carries once it is open if(m_info[i].isOpen && m_info[i].extraCount > 0) { //--- Rule the opened lines off below whatever the value took const int ruleY = ry + OP_VAL_H + (m_info[i].wraps - 1) * lineH; if(ruleY >= top && ruleY <= bottom) m_cv.HRule(valX + 1, valRight, ruleY, m_th.divider); m_cv.Font(8); //--- Stack the lines under the rule, skipping any outside the band for(int e = 0; e < m_info[i].extraCount; e++) { //--- Place this line and test it against the band itself const int ey = ry + OP_VAL_H + (m_info[i].wraps - 1) * lineH + OP_VAL_PAD / 2 + e * (lineH + 2); if(ey < top || ey + lineH > bottom) continue; //--- Hold the line inside the box, marking it when it will not fit string cut[]; if(m_cv.WrapText(m_info[i].extra[e], valW, cut, 1) > 0) m_cv.TxtL(valX + OP_VAL_PAD, ey, cut[0], m_th.muted); } } } m_cv.NoClip(); //--- Hide the scrollbar while every row already fits if(!bar) { m_rInfoBar[2] = 0; m_rInfoBar[3] = 0; return; } //--- Lay the track down the right edge of the column const int trackX = right - OP_BAR; m_cv.RoundFill(trackX, top, OP_BAR, band, OP_BAR / 2, m_th.grid); //--- Size the thumb by how much of the listing is visible int thumbH = (int)((double)band * band / m_infoHigh); if(thumbH < 20) thumbH = 20; //--- Place the thumb by how far the listing has been scrolled const double frac = (double)m_infoScroll / (m_infoHigh - band); const int thumbY = top + (int)((band - thumbH) * frac); //--- Store the thumb so the pointer can grab it m_rInfoBar[0] = trackX; m_rInfoBar[1] = thumbY; m_rInfoBar[2] = OP_BAR; m_rInfoBar[3] = thumbH; //--- Draw the thumb, lit while the pointer works it const bool lit = (m_hover == OH_INFO_BAR || m_barDrag == OH_INFO_BAR); m_cv.RoundFill(trackX, thumbY, OP_BAR, thumbH, OP_BAR / 2, lit ? m_th.accent : m_th.muted); } //+------------------------------------------------------------------+ //| Find the inspector row under a panel point | //+------------------------------------------------------------------+ int COmvPanel::InfoAtPoint(const int mx, const int my) { //--- Ignore anything outside the column if(!m_inspect || mx <= CanvasRight()) return -1; //--- Measure the band the rows scroll inside const int top = CanvasTop() + 44; const int bottom = CanvasBottom() - 8; if(my < top || my > bottom) return -1; //--- Test every laid out row against the current scroll for(int i = 0; i < m_infos; i++) { //--- Skip a heading, it cannot be picked if(m_info[i].isHead) continue; //--- Report the row that holds the point const int ry = top + m_info[i].y - m_infoScroll; if(my >= ry && my < ry + m_info[i].h) return i; } //--- Report that no row holds the point return -1; } //+------------------------------------------------------------------+ //| Draw the footer band that does not change between frames | //+------------------------------------------------------------------+ void COmvPanel::DrawFooterChrome(void) { //--- Fill the band, rounding only the two corners it owns const int top = m_h - OP_FOOT_H; m_cv.RoundFillSel(0, top, m_w, OP_FOOT_H, OP_RADIUS, m_th.chrome, false, false, true, true); m_cv.HRule(1, m_w - 2, top, m_th.divider); } //+------------------------------------------------------------------+ //| Draw the control that swaps between the two views | //+------------------------------------------------------------------+ void COmvPanel::DrawViewSwitch(void) { //--- Name the view the control would take us to const string label = m_neuronView ? "Graph" : "Neurons"; //--- Take whether the control can be used at all const bool live = m_neuronView || m_cells.Ready(); //--- Light the face while the pointer rests on a control that works const bool hot = (m_hover == OH_VIEW && live); m_cv.RoundFill(m_rView[0], m_rView[1], m_rView[2], m_rView[3], 4, hot ? m_th.hoverFace : m_th.btnFace); m_cv.RoundStroke(m_rView[0], m_rView[1], m_rView[2], m_rView[3], 4, 1, live ? m_th.btnEdge : m_th.divider); //--- Name it, in the muted ink when the model cannot be drawn as neurons m_cv.Font(8); m_cv.TxtM(m_rView[0] + m_rView[2] / 2, m_rView[1] + m_rView[3] / 2, label, live ? m_th.text : m_th.divider); } //+------------------------------------------------------------------+ //| Draw the footer controls and readouts | //+------------------------------------------------------------------+ void COmvPanel::DrawFooter(void) { //--- Take the top of the band every control sits in const int top = m_h - OP_FOOT_H; //--- Draw the three view controls DrawButton(m_rFit, OH_FIT, "Fit"); DrawIcon(m_rZoomOut, OH_ZOOM_OUT, 3); DrawIcon(m_rZoomIn, OH_ZOOM_IN, 4); //--- Offer the other view, dimmed when this model has no neurons to draw DrawViewSwitch(); //--- Measure the type once so every footer readout shares a baseline m_cv.Font(8); const int textY = top + OP_FOOT_H / 2; //--- Report the panel size beside the grip, which fixes the right hand edge const string size = IntegerToString(m_w) + " x " + IntegerToString(m_h); m_cv.TxtR(m_w - 26, textY - m_cv.TextHigh(size) / 2, size, m_th.muted); //--- Print the zoom just after the controls it belongs to const int afterZoom = m_rZoomIn[0] + m_rZoomIn[2] + 8; m_cv.TxtL(afterZoom, textY - m_cv.TextHigh("100%") / 2, IntegerToString((int)MathRound(m_zoom * 100)) + "%", m_th.muted); //--- Name the selected node when one is chosen if(m_selected >= 0 && m_selected < m_layout.Boxes()) { //--- Fetch the chosen node OmvBox box; m_layout.BoxAt(m_selected, box); //--- Name a port by itself and a layer by its operator string shown = box.portName; if(box.node >= 0) { OmvNode node; m_model.NodeAt(box.node, node); shown = node.op + " " + node.name; } //--- Sit it between the view switch and the size readout, never over either const int from = m_rView[0] + m_rView[2] + 14; const int room = m_w - 26 - m_cv.TextWide(size) - 14 - from; string cut[]; if(room > 0 && m_cv.WrapText(shown, room, cut, 1) > 0) m_cv.TxtL(from, textY - m_cv.TextHigh(cut[0]) / 2, cut[0], m_th.muted); } //--- Draw the three diagonals that mark the grip const bool lit = (m_hover == OH_GRIP || m_resizing); const uint ink = lit ? m_th.accent : m_th.muted; const int gx = m_rGrip[0] + m_rGrip[2]; const int gy = m_rGrip[1] + m_rGrip[3]; m_cv.StrokeAA(gx - 3, gy - 12, gx - 12, gy - 3, lit ? 2 : 1, ink); m_cv.StrokeAA(gx - 3, gy - 8, gx - 8, gy - 3, lit ? 2 : 1, ink); m_cv.StrokeAA(gx - 3, gy - 4, gx - 4, gy - 3, lit ? 2 : 1, ink); } //+------------------------------------------------------------------+ //| Paint every band in order and publish the bitmap | //+------------------------------------------------------------------+ void COmvPanel::Render(void) { //--- Do nothing without a canvas or after teardown if(m_destroyed || !m_created) return; //--- Rebuild the cached chrome only when something structural moved const string sig = ChromeSignature(); if(sig != m_bgSig) { m_bgSig = sig; m_bgValid = false; } //--- Rebuild the cached scene only when the view itself moved const string scene = SceneSignature(); if(scene != m_sceneSig) { m_sceneSig = scene; m_sceneValid = false; } //--- Put back the whole scene when nothing under the pointer changed it if(m_sceneValid && m_mode == OM_FULL) m_cv.RestoreFrom(m_scene); else { //--- Draw the chrome once and keep it, or put the kept copy back if(!m_bgValid) { DrawChrome(); m_cv.SaveTo(m_bg); m_bgValid = true; } else m_cv.RestoreFrom(m_bg); //--- Draw the view the panel is showing over the cached chrome if(m_mode == OM_FULL) { //--- Keep the finished scene so a hover does not redraw it if(m_neuronView) DrawNeurons(); else DrawGraph(); m_cv.SaveTo(m_scene); m_sceneValid = true; } } //--- Draw everything that answers to the pointer on top of the scene if(m_mode == OM_FULL) { DrawBars(); DrawInspector(); DrawFooter(); } //--- Draw the header icons over the cached band DrawHeader(); //--- Publish the finished bitmap m_cv.Update(); } //+------------------------------------------------------------------+ //| Repaint no faster than the frame cap allows | //+------------------------------------------------------------------+ void COmvPanel::PaintCapped(void) { //--- Read the clock once for this decision const uint now = GetTickCount(); //--- Remember a skipped frame so the next event still lands it if(now - m_lastFrameMs < OP_FRAME_MS) { m_dirty = true; return; } //--- Stamp this frame and paint it m_lastFrameMs = now; m_dirty = false; Render(); ChartRedraw(m_chartId); } //+------------------------------------------------------------------+ //| Build the panel and load the model | //+------------------------------------------------------------------+ bool COmvPanel::Init(const long chartId, const string path) { //--- Reset every member before touching the chart InitDefaults(); m_chartId = chartId; //--- Take the header title from the running program m_title = MQLInfoString(MQL_PROGRAM_NAME); if(StringLen(m_title) == 0) m_title = "ONNX Model Viewer"; //--- Name the bitmap after the program so parts never clash m_name = "OMV_" + m_title; //--- Resolve the starting theme m_th.Apply(m_dark); //--- Read the model before anything is drawn if(!m_model.Load(path)) { Print(m_title, ": ", m_model.Error()); return false; } //--- Arrange what was read m_layout.Build(m_model); //--- Work out whether this model can also be shown as neurons m_cells.Build(m_model); //--- Place every control and build the bitmap ComputeLayout(); if(!CreateCanvas()) return false; //--- Scale the graph to the canvas it now has FitGraph(); //--- Remember how the chart was set before the panel changes it m_hadMouse = (bool)ChartGetInteger(m_chartId, CHART_EVENT_MOUSE_MOVE); m_hadWheel = (bool)ChartGetInteger(m_chartId, CHART_EVENT_MOUSE_WHEEL); //--- Follow the pointer so the panel can be worked ChartSetInteger(m_chartId, CHART_EVENT_MOUSE_MOVE, true); ChartSetInteger(m_chartId, CHART_EVENT_MOUSE_WHEEL, true); //--- Show the first frame Render(); ChartRedraw(m_chartId); //--- Report what was loaded Print(m_title, ": ", path, " -> ", m_model.Nodes(), " nodes, ", m_model.Tensors(), " tensors, ", m_model.Parameters(), " parameters"); return true; } //+------------------------------------------------------------------+ //| Remove the canvas and restore the chart | //+------------------------------------------------------------------+ void COmvPanel::Destroy(void) { //--- Run the teardown only once if(m_destroyed) return; m_destroyed = true; //--- Give scrolling back to the chart LockChartScroll(false); //--- Put the chart's own mouse settings back the way they were found ChartSetInteger(m_chartId, CHART_EVENT_MOUSE_MOVE, m_hadMouse); ChartSetInteger(m_chartId, CHART_EVENT_MOUSE_WHEEL, m_hadWheel); //--- Clear any tooltip left on the object if(m_created) ObjectSetString(m_chartId, m_name, OBJPROP_TOOLTIP, "\n"); //--- Release the bitmap if(m_created) { m_cv.Destroy(); m_created = false; } //--- Release the chrome cache ArrayFree(m_bg); ArrayFree(m_scene); m_bgValid = false; m_sceneValid = false; //--- Release the parsed structures m_layout.Clear(); m_cells.Clear(); m_model.Clear(); } //+------------------------------------------------------------------+ //| Route pointer events into the panel | //+------------------------------------------------------------------+ void COmvPanel::OnEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Ignore events after teardown or before the canvas exists if(m_destroyed || !m_created) return; //--- Route a wheel turn before anything else looks at the pointer if(id == CHARTEVENT_MOUSE_WHEEL) { //--- The position packs into the low and high halves of lparam const int wx = (int)(short)(lparam & 0xFFFF) - m_x; const int wy = (int)(short)((lparam >> 16) & 0xFFFF) - m_y; //--- The terminal hands the wheel delta through dparam unpacked OnWheel(wx, wy, (int)dparam); return; } //--- Handle pointer events only if(id != CHARTEVENT_MOUSE_MOVE) return; //--- Convert the pointer into panel space const int mx = (int)lparam - m_x; const int my = (int)dparam - m_y; const bool down = ((int)StringToInteger(sparam) & 1) == 1; //--- Hold the scroll lock for the whole gesture const bool over = (mx >= 0 && mx < m_w && my >= 0 && my < m_h); LockChartScroll(over || m_dragging || m_resizing || m_panning || m_barDrag != OH_NONE); //--- Identify what sits under the pointer const int hit = over ? HitTest(mx, my) : OH_NONE; //--- Move the panel while a header drag is active if(m_dragging) { //--- End the drag on release if(!down) { m_dragging = false; m_lmbPrev = false; m_press = OH_NONE; LockChartScroll(over); return; } //--- Follow the pointer by the offset captured at press m_x = (int)lparam - m_dragOffX; m_y = (int)dparam - m_dragOffY; ObjectSetInteger(m_chartId, m_name, OBJPROP_XDISTANCE, m_x); ObjectSetInteger(m_chartId, m_name, OBJPROP_YDISTANCE, m_y); ChartRedraw(m_chartId); return; } //--- Resize the panel while a grip drag is active if(m_resizing) { //--- End the resize on release if(!down) { m_resizing = false; m_lmbPrev = false; m_press = OH_NONE; LockChartScroll(over); //--- Land whatever frame the cap skipped m_lastFrameMs = 0; PaintCapped(); return; } //--- Take the size the pointer implies through the captured offset int nw = (int)lparam - m_x + m_gripOffX; int nh = (int)dparam - m_y + m_gripOffY; //--- Clamp both sides to the allowed bounds if(nw < OP_W_MIN) nw = OP_W_MIN; if(nw > OP_W_MAX) nw = OP_W_MAX; if(nh < OP_H_MIN) nh = OP_H_MIN; if(nh > OP_H_MAX) nh = OP_H_MAX; //--- Skip everything when the size did not actually change if(nw == m_w && nh == m_h) return; //--- Take the new size at once, the bitmap behind it never changes m_w = nw; m_h = nh; m_fullH = nh; m_bgValid = false; ComputeLayout(); ClampPan(); //--- Paint through the cap so dragging stays responsive PaintCapped(); return; } //--- Drag the vertical scrollbar while its thumb is held if(m_barDrag == OH_BAR_V) { //--- End the drag on release if(!down) { m_barDrag = OH_NONE; m_lmbPrev = false; m_press = OH_NONE; LockChartScroll(over); //--- Land whatever frame the cap skipped m_lastFrameMs = 0; PaintCapped(); return; } //--- Convert the pointer travel into graph travel const int viewH = ViewH(), fullH = ScaledH(); const int span = fullH - viewH + OP_PAD * 2; if(span > 0 && m_rBarV[3] > 0 && viewH > m_rBarV[3]) { //--- Move the graph by the same fraction the thumb travelled m_panY = m_barFrom - (int)((double)(my - m_fromY) * span / (viewH - m_rBarV[3])); ClampPan(); PaintCapped(); } return; } //--- Drag the horizontal scrollbar while its thumb is held if(m_barDrag == OH_BAR_H) { //--- End the drag on release if(!down) { m_barDrag = OH_NONE; m_lmbPrev = false; m_press = OH_NONE; LockChartScroll(over); //--- Land whatever frame the cap skipped m_lastFrameMs = 0; PaintCapped(); return; } //--- Convert the pointer travel into graph travel const int viewW = ViewW(), fullW = ScaledW(); const int span = fullW - viewW + OP_PAD * 2; if(span > 0 && m_rBarH[2] > 0 && viewW > m_rBarH[2]) { //--- Move the graph by the same fraction the thumb travelled m_panX = m_barFrom - (int)((double)(mx - m_fromX) * span / (viewW - m_rBarH[2])); ClampPan(); PaintCapped(); } return; } //--- Scroll the inspector while its thumb is held if(m_barDrag == OH_INFO_BAR) { //--- End the drag on release if(!down) { m_barDrag = OH_NONE; m_lmbPrev = false; m_press = OH_NONE; LockChartScroll(over); return; } //--- Move the listing by the same fraction the thumb travelled const int band = (CanvasBottom() - 8) - (CanvasTop() + 44); if(m_infoHigh > band && m_rInfoBar[3] > 0 && band > m_rInfoBar[3]) { //--- Scale the movement by how much taller the listing is m_infoScroll = m_barFrom + (int)((double)(my - m_fromY) * (m_infoHigh - band) / (band - m_rInfoBar[3])); PaintCapped(); } return; } //--- Pan the graph while a canvas drag is active if(m_panning) { //--- End the pan on release if(!down) { m_panning = false; m_lmbPrev = false; m_press = OH_NONE; LockChartScroll(over); //--- A press that never moved is a click on the background if(MathAbs(mx - m_pressX) < 3 && MathAbs(my - m_pressY) < 3) { m_pickKind = OP_PICK_MODEL; m_selected = -1; m_inspect = true; ComputeLayout(); ClampPan(); } //--- Land whatever frame the cap skipped m_lastFrameMs = 0; PaintCapped(); return; } //--- Shift the graph by however far the pointer moved m_panX += mx - m_fromX; m_panY += my - m_fromY; m_fromX = mx; m_fromY = my; ClampPan(); PaintCapped(); return; } //--- Track whatever sits under the pointer so it can be highlighted const int overNode = (hit == OH_NODE) ? NodeAtPoint(mx, my) : -1; const int overRow = (hit == OH_ROW) ? RowAtPoint(mx, my) : -1; //--- Take the value a hovered box row lists, so its sidebar row lights too string overRowName = ""; if(overRow >= 0 && overRow < m_rows) overRowName = TensorNameAt(m_rowTensor[overRow]); const int overLink = (hit == OH_LINK) ? LinkAtPoint(mx, my) : -1; //--- Take the value a hovered connection carries, so its siblings light too string overEdgeName = ""; if(overLink >= 0 && overLink < m_layout.Edges()) { //--- Fetch the connection under the pointer OmvEdge lit; m_layout.EdgeAt(overLink, lit); overEdgeName = lit.label; } const int overInfo = (hit == OH_INFO) ? InfoAtPoint(mx, my) : -1; //--- Take the value the hovered row points at, and where on the row we are string overName = (overInfo >= 0) ? m_info[overInfo].linkName : ""; //--- A hovered connection lights by name just as a hovered row does if(StringLen(overName) == 0) overName = overEdgeName; //--- So does a hovered weight row inside a box if(StringLen(overName) == 0) overName = overRowName; const bool overPlus = (overInfo >= 0) && OnExpander(mx, overInfo); //--- Repaint on any hover change, capped to one frame if(hit != m_hover || overNode != m_hoverNode || overRow != m_hoverRow || overLink != m_hoverLink || overInfo != m_infoHover || overName != m_litName || overPlus != m_onExpander) { m_hover = hit; m_hoverNode = overNode; m_hoverRow = overRow; m_hoverLink = overLink; m_infoHover = overInfo; m_litName = overName; m_onExpander = overPlus; //--- Let the terminal draw the tooltip for whatever is hovered SetTooltip(TipFor(hit)); PaintCapped(); } //--- React to the press edge if(down && !m_lmbPrev) { //--- Remember where the press began m_press = hit; //--- Start a header drag if(hit == OH_HEADER) { m_dragging = true; m_dragOffX = (int)lparam - m_x; m_dragOffY = (int)dparam - m_y; } //--- Start a grip drag, keeping the grip under the pointer else if(hit == OH_GRIP) { m_resizing = true; m_gripOffX = (m_x + m_w) - (int)lparam; m_gripOffY = (m_y + m_h) - (int)dparam; } //--- Start a scrollbar drag from where the thumb sits now else if(hit == OH_BAR_V || hit == OH_BAR_H) { m_barDrag = hit; m_fromX = mx; m_fromY = my; m_barFrom = (hit == OH_BAR_V) ? m_panY : m_panX; } //--- Start an inspector scroll from where its thumb sits now else if(hit == OH_INFO_BAR) { m_barDrag = OH_INFO_BAR; m_fromY = my; m_barFrom = m_infoScroll; } //--- Start a canvas pan, remembering where it began else if(hit == OH_CANVAS) { m_panning = true; m_fromX = mx; m_fromY = my; m_pressX = mx; m_pressY = my; } //--- Select the box that was pressed and open the inspector for it else if(hit == OH_NODE) { m_selected = NodeAtPoint(mx, my); //--- A graph port belongs to the graph, so it opens the graph panel OmvBox picked; m_layout.BoxAt(m_selected, picked); m_pickKind = (picked.node < 0) ? OP_PICK_GRAPH : OP_PICK_NODE; m_inspect = true; ComputeLayout(); ClampPan(); PaintCapped(); } //--- Select the weight row that was pressed else if(hit == OH_ROW) { //--- Take the tensor the row names const int row = RowAtPoint(mx, my); if(row >= 0) { m_pickTensor = m_rowTensor[row]; m_pickKind = OP_PICK_TENSOR; m_selected = -1; m_inspect = true; ComputeLayout(); ClampPan(); } PaintCapped(); } //--- Select the connection that was pressed else if(hit == OH_LINK) { m_pickLink = LinkAtPoint(mx, my); m_pickKind = OP_PICK_LINK; m_selected = -1; m_inspect = true; ComputeLayout(); ClampPan(); PaintCapped(); } //--- Light the pressed control else if(hit != OH_NONE) PaintCapped(); } //--- React to the release edge and run the action else if(!down && m_lmbPrev) { //--- Act only when the release lands where the press began const int act = (m_press == hit) ? hit : OH_NONE; m_press = OH_NONE; //--- Swap the theme if(act == OH_THEME) { m_dark = !m_dark; m_th.Apply(m_dark); m_bgValid = false; } //--- Swap the view, but only where the other one can be drawn else if(act == OH_VIEW) { //--- Refuse to leave the graph when the model has no neurons to show if(m_neuronView || m_cells.Ready()) { m_neuronView = !m_neuronView; //--- The neuron view has nothing selectable, so drop what the graph chose if(m_neuronView) { m_selected = -1; m_pickKind = OP_PICK_NONE; m_hoverNode = -1; m_hoverRow = -1; m_hoverLink = -1; m_litName = ""; } //--- The two arrangements differ in size, so refit rather than keep the pan FitGraph(); m_bgValid = false; } } //--- Scale the graph back into the canvas else if(act == OH_FIT) FitGraph(); //--- Step the zoom down else if(act == OH_ZOOM_OUT) SetZoom(m_zoom / 1.25); //--- Step the zoom up else if(act == OH_ZOOM_IN) SetZoom(m_zoom * 1.25); //--- Act on the row that was pressed else if(act == OH_INFO) { //--- Take the row and decide which half of it was hit const int row = InfoAtPoint(mx, my); if(row >= 0 && m_info[row].canOpen && OnExpander(mx, row)) ToggleRow(m_info[row].name + "|" + m_info[row].value); else if(row >= 0 && StringLen(m_info[row].linkName) > 0) { //--- Open the connection the row points at const int link = LinkNamed(m_info[row].linkName); if(link >= 0) { m_pickLink = link; m_pickKind = OP_PICK_LINK; m_selected = -1; m_litName = ""; } } else if(row >= 0 && m_info[row].canOpen) ToggleRow(m_info[row].name + "|" + m_info[row].value); } //--- Close the inspector and give its width back to the graph else if(act == OH_INSPECT_X) { m_inspect = false; m_selected = -1; m_pickKind = OP_PICK_NONE; m_pickTensor = -1; m_pickLink = -1; ComputeLayout(); ClampPan(); } //--- Collapse or restore the panel else if(act == OH_MIN || act == OH_RESTORE) { m_mode = (act == OH_MIN) ? OM_MIN : OM_FULL; m_hover = OH_NONE; ApplyMode(); } //--- Remove the panel else if(act == OH_CLOSE) { Destroy(); ChartRedraw(m_chartId); return; } //--- Repaint whenever an action ran if(act != OH_NONE) { m_lastFrameMs = 0; PaintCapped(); } } //--- Land a frame the cap skipped once enough time has passed if(m_dirty && GetTickCount() - m_lastFrameMs >= OP_FRAME_MS) PaintCapped(); //--- Remember the button state for the next event m_lmbPrev = down; } #endif // OMV_PANEL_MQH //+------------------------------------------------------------------+