Article-24337-Working-With-.../OMV Panel.mqh

4386 lines
172 KiB
MQL5

//+------------------------------------------------------------------+
//| 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
//+------------------------------------------------------------------+