4386 lines
172 KiB
MQL5
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
|
|
//+------------------------------------------------------------------+
|