English Русский
preview
MQL5-Handelswerkzeuge (Teil 24): Verbesserte Tiefenwahrnehmung mit 3D-Kurven, Pan-Modus und ViewCube-Navigation

MQL5-Handelswerkzeuge (Teil 24): Verbesserte Tiefenwahrnehmung mit 3D-Kurven, Pan-Modus und ViewCube-Navigation

MetaTrader 5Handelssysteme |
22 5
Allan Munene Mutiiria
Allan Munene Mutiiria

Einführung

Sie verfügen zwar über eine dreidimensionale Visualisierung der Binomialverteilung mit Dreh- und Zoomfunktionen, jedoch ohne eine räumlich präzise Kurve, ohne die Möglichkeit, den Fokuspunkt der Szene zu verschieben, und ohne schnelle Möglichkeit, zur Standardausrichtung zurückzukehren. Das Untersuchen der Formen der PMF erfordert daher ständiges manuelles Drehen. Außerhalb des Zentrums liegende Bereiche lassen sich nur schwer erreichen, und die Rückkehr zur Standardansicht bedeutet, die Ansicht gewissermaßen blind manuell zurückzusetzen. Dieser Artikel richtet sich an Entwickler der Programmiersprache MetaQuotes Language 5 (MQL5) sowie an algorithmische Händler, die interaktive dreidimensionale Statistik-Tools um intuitive Navigationselemente erweitern möchten, um tiefgreifendere Wahrscheinlichkeitsanalysen durchzuführen.

In unserem vorherigen Artikel (Teil 23) haben wir Direct3D in den MQL5-Viewer der Binomialverteilung integriert und damit umschaltbare 2D-/3D-Modi sowie Kamerasteuerung für Drehung, Zoom und automatische Anpassung ermöglicht. In Teil 24 erweitern wir das Tool um eine segmentierte dreidimensionale Kurve zur besseren Tiefenwahrnehmung der PMF, integrieren einen Pan-Modus zum Verschieben des Kamerazielpunkts und implementieren einen interaktiven ViewCube mit Hover-Zonen und animierten Kameraübergängen für schnelle Orientierungswechsel. Wir werden die folgenden Themen behandeln:

  1. Die 3D-Kurve, den Pan-Modus und das ViewCube-Framework verstehen
  2. Implementierung in MQL5
  3. Backtests
  4. Schlussfolgerung

Am Ende verfügen Sie über ein erweitertes MQL5-Tool mit erweiterter 3D-Navigation für die Verteilungsanalyse, das Sie ganz nach Ihren Wünschen anpassen können – legen wir los!


Die 3D-Kurve, den Pan-Modus und das ViewCube-Framework verstehen

Die Darstellung der PMF als flache, projizierte Linie im dreidimensionalen Raum verliert die Tiefe, die die dreidimensionale Darstellung so wertvoll macht – eine segmentierte, röhrenförmige Kurve, die aus orientierten Box-Primitiven aufgebaut ist, die an aufeinanderfolgende Datenpunkte ausgerichtet sind, verleiht der Kurve eine greifbare räumliche Präsenz in der Szene, sodass Höhenunterschiede zwischen den Bins in der Perspektive natürlich zur Geltung kommen. Der Pan-Modus behebt eine andere Einschränkung: Durch Drehen und Zoomen bleibt die Szene an einem festen Zielpunkt verankert, sodass außermittige Verteilungen oder eine große Anzahl von Versuchen den relevanten Bereich an den Rand verschieben; durch die Verschiebung des Kamerazielpunkts mittels kamerarelativer Vektorarithmetik können wir die gesamte Szene erkunden, ohne den aktuellen Blickwinkel oder die aktuelle Zoomstufe zu verändern. Der ViewCube ist ein kompaktes Orientierungs-Widget, das die aktuelle Drehung der Kamera in Echtzeit widerspiegelt. Er ist in anklickbare Flächen, Kanten und Ecken unterteilt, die jeweils einem vordefinierten Winkelpaar zugeordnet sind und einen sanft interpolierten Übergang anstelle eines sofortigen Sprungs auslösen.

Nutzen Sie diese räumlich präzise dargestellte Kurve im Handel zusammen mit dem Histogramm, um zu beurteilen, ob die Spitze der PMF mit dem Bin der höchsten Frequenz übereinstimmt, was auf ein gut kalibriertes Modell hindeutet. Richten Sie den Fokus zunächst auf die Verteilungsschwänze mit geringer Wahrscheinlichkeit, um diese genauer zu untersuchen, bevor Sie Positionen in der Nähe der Extremwerte festlegen. Verwenden Sie die Draufsicht des ViewCubes, um die Barhöhen aller Bins gleichzeitig zu vergleichen, und die Vorderansicht, um die Werte der einzelnen Bars übersichtlich abzulesen.

Wir werden das Tool erweitern, indem wir boxbasierte Kurvensegmente erstellen, die so skaliert und gedreht werden, dass sie eine durchgehende Röhre bilden. Außerdem werden wir eine Pan-Logik unter Verwendung kamerarelativer Vektorberechnungen implementieren und den ViewCube mit projizierten Scheitelpunkten, Flächensortierung für die korrekte Zeichenreihenfolge sowie bilinearer Interpolation für unterteilte Hover-Zonen rendern. Außerdem werden wir zeitgesteuerte Animationen für Ansichtsübergänge, überblendete Pixel-Overlays für Symbole und den Würfel sowie die Ereignisbehandlung für Klicks und Hover-Ereignisse integrieren, um die Benutzeroberfläche vollständig responsiv zu gestalten. Werfen Sie unten einen Blick auf unsere Ziele.

3D-KURVE, PAN-MODUS UND VIEWCUBE-FRAMEWORK


Implementierung in MQL5

Erweiterung von Eingaben, Konstanten und Klassenelementen zur Unterstützung von Pan-Modus und ViewCube

Um den neuen Pan-Modus und das ViewCube-Widget zu unterstützen, erweitern wir zunächst die Eingaben um einen Schalter für den ViewCube-Hintergrund, definieren Konstanten, die die Größe von Symbolen und Würfeln steuern, führen neue geschützte Member-Variablen in der Visualizer-Klasse für Hover-Zustände, Kurvensegmente, das Verschieben und die Animationssteuerung ein, initialisieren diese alle im Konstruktor und passen den Destruktor an, um das neue Kurvensegment-Array zusammen mit den bestehenden Szenenobjekten sauber freizugeben.

//+------------------------------------------------------------------+
//|    Canvas Graphing PART 3.2 - Statistical Distributions (3D).mq5 |
//|                           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"
#property version   "1.00"
#property strict

input group "=== VIEW CUBE SETTINGS ==="
input bool             showViewCubeBackground = false;       // Show View Cube Background Panel

//--- Add new constants for pan icon and view cube dimensions

const int PAN_ICON_SIZE     = 24;   // Size of the pan mode toggle icon
const int PAN_ICON_MARGIN   = 6;    // Margin around the pan icon
const int VCUBE_SIZE        = 70;   // Pixel size of the view cube widget
const int VCUBE_MARGIN      = 6;    // Margin around the view cube widget

//--- Add new member variables in the "DistributionVisualizer" class for hovering states, curve segments, panning, animation, and view cube.

//+------------------------------------------------------------------+
//| Distribution visualization window class                          |
//+------------------------------------------------------------------+
class DistributionVisualizer
  {
protected:

   bool              m_isHoveringPanIcon;     // True when mouse is over the pan mode icon
   bool              m_isHoveringViewCube;    // True when mouse is over the view cube widget
   int               m_lastMouseX;            // Last recorded mouse X coordinate
   int               m_lastMouseY;            // Last recorded mouse Y coordinate
   int               m_previousMouseButtonState; // Mouse button state on previous event

   ViewModeType      m_currentViewMode;       // Active view mode (2D or 3D)
   bool              m_are3DObjectsCreated;   // True once 3D scene objects have been built

   CDXBox            m_histogramBars[];       // Array of 3D boxes representing histogram bars
   CDXBox            m_groundPlane;           // 3D box used as the ground reference plane
   CDXBox            m_axisX;                 // 3D box representing the X axis
   CDXBox            m_axisY;                 // 3D box representing the Y axis
   CDXBox            m_axisZ;                 // 3D box representing the Z axis
   CDXBox            m_curveSegments[];       // Array of 3D boxes representing PMF curve tube segments
   int               m_curveSegmentCount;     // Number of active curve segment boxes

   double            m_cameraDistance;        // Distance from camera to scene target
   double            m_cameraAngleX;          // Camera elevation angle (radians)
   double            m_cameraAngleY;          // Camera azimuth angle (radians)
   int               m_mouse3DStartX;         // Mouse X when 3D orbit drag began
   int               m_mouse3DStartY;         // Mouse Y when 3D orbit drag began
   bool              m_isRotating3D;          // True while user is orbiting the 3D scene
   bool              m_panMode;               // True when pan mode is active (vs orbit mode)
   bool              m_isPanning;             // True while a pan drag is in progress
   int               m_panStartX;             // Mouse X when pan drag began
   int               m_panStartY;             // Mouse Y when pan drag began
   DXVector3         m_viewTarget;            // World-space point the camera looks at

   double            m_sampleData[];                  // Raw binomial sample values
   double            m_histogramIntervals[];           // Histogram bin centre positions
   double            m_histogramFrequencies[];         // Scaled frequency per histogram bin
   double            m_theoreticalXValues[];           // X values for the theoretical PMF curve
   double            m_theoreticalYValues[];           // Y values for the theoretical PMF curve
   double            m_minDataValue;                   // Minimum value in data range
   double            m_maxDataValue;                   // Maximum value in data range
   double            m_maxFrequency;                   // Peak raw frequency across all bins
   double            m_maxTheoreticalValue;            // Peak theoretical PMF value
   bool              m_isDataLoaded;                   // True once distribution data is ready

   double            m_sampleMean;                     // Computed sample mean
   double            m_sampleStandardDeviation;        // Computed sample standard deviation
   double            m_sampleSkewness;                 // Computed sample skewness
   double            m_sampleKurtosis;                 // Computed sample excess kurtosis
   double            m_percentile25;                   // 25th percentile (Q1)
   double            m_percentile50;                   // 50th percentile (median)
   double            m_percentile75;                   // 75th percentile (Q3)
   double            m_confidenceInterval95Lower;      // Lower bound of 95% confidence interval
   double            m_confidenceInterval95Upper;      // Upper bound of 95% confidence interval
   double            m_confidenceInterval99Lower;      // Lower bound of 99% confidence interval
   double            m_confidenceInterval99Upper;      // Upper bound of 99% confidence interval

   double            m_targetAngleX;          // Target camera elevation angle for animation
   double            m_targetAngleY;          // Target camera azimuth angle for animation
   bool              m_isAnimating;           // True while a camera snap animation is running
   int               m_animSteps;             // Total number of animation interpolation steps
   int               m_animStep;              // Current animation step index
   double            m_animStartAngleX;       // Camera elevation angle at animation start
   double            m_animStartAngleY;       // Camera azimuth angle at animation start
   string            m_vcubeHoverZone;        // Name of the view cube zone under the cursor
   int               m_vcubeCenterX;          // Screen X of the view cube widget centre
   int               m_vcubeCenterY;          // Screen Y of the view cube widget centre

public:
   //+------------------------------------------------------------------+
   //| Initialise all member variables to safe defaults                 |
   //+------------------------------------------------------------------+
   DistributionVisualizer(void)
     {
      //--- Reset canvas hover flag
      m_isHoveringCanvas = false;
      //--- Reset header hover flag
      m_isHoveringHeader = false;
      //--- Reset resize zone hover flag
      m_isHoveringResizeZone = false;
      //--- Reset switch icon hover flag
      m_isHoveringSwitchIcon = false;
      //--- Reset pan icon hover flag
      m_isHoveringPanIcon = false;
      //--- Reset view cube hover flag
      m_isHoveringViewCube = false;
      //--- Reset last mouse X
      m_lastMouseX = 0;
      //--- Reset last mouse Y
      m_lastMouseY = 0;
      //--- Reset previous mouse button state
      m_previousMouseButtonState = 0;

      //--- Set view mode from input
      m_currentViewMode = viewMode;
      //--- Mark 3D objects as not yet created
      m_are3DObjectsCreated = false;

      //--- Set camera distance from input
      m_cameraDistance = initialCameraDistance;
      //--- Set camera elevation angle from input
      m_cameraAngleX = initialCameraAngleX;
      //--- Set camera azimuth angle from input
      m_cameraAngleY = initialCameraAngleY;
      //--- Reset 3D orbit drag origin X
      m_mouse3DStartX = -1;
      //--- Reset 3D orbit drag origin Y
      m_mouse3DStartY = -1;
      //--- Mark 3D orbit as inactive
      m_isRotating3D = false;
      //--- Start in orbit mode (not pan)
      m_panMode = false;
      //--- Mark pan drag as inactive
      m_isPanning = false;
      //--- Reset pan drag origin X
      m_panStartX = 0;
      //--- Reset pan drag origin Y
      m_panStartY = 0;
      //--- Initialise view target at scene origin
      m_viewTarget = DXVector3(0.0f, 0.0f, 0.0f);

      //--- Reset data range minimum
      m_minDataValue = 0.0;
      //--- Reset data range maximum
      m_maxDataValue = 0.0;
      //--- Reset peak histogram frequency
      m_maxFrequency = 0.0;
      //--- Reset peak theoretical PMF value
      m_maxTheoreticalValue = 0.0;
      //--- Mark data as not yet loaded
      m_isDataLoaded = false;

      //--- Reset sample mean
      m_sampleMean = 0.0;
      //--- Reset standard deviation
      m_sampleStandardDeviation = 0.0;
      //--- Reset skewness
      m_sampleSkewness = 0.0;
      //--- Reset kurtosis
      m_sampleKurtosis = 0.0;
      //--- Reset 25th percentile
      m_percentile25 = 0.0;
      //--- Reset median
      m_percentile50 = 0.0;
      //--- Reset 75th percentile
      m_percentile75 = 0.0;
      //--- Reset 95% CI lower bound
      m_confidenceInterval95Lower = 0.0;
      //--- Reset 95% CI upper bound
      m_confidenceInterval95Upper = 0.0;
      //--- Reset 99% CI lower bound
      m_confidenceInterval99Lower = 0.0;
      //--- Reset 99% CI upper bound
      m_confidenceInterval99Upper = 0.0;

      //--- Reset animation target elevation
      m_targetAngleX = 0.0;
      //--- Reset animation target azimuth
      m_targetAngleY = 0.0;
      //--- Mark animation as inactive
      m_isAnimating = false;
      //--- Set default animation step count
      m_animSteps = 20;
      //--- Reset current animation step
      m_animStep = 0;
      //--- Reset animation start elevation
      m_animStartAngleX = 0.0;
      //--- Reset animation start azimuth
      m_animStartAngleY = 0.0;
      //--- Clear view cube hover zone
      m_vcubeHoverZone = "";
      //--- Reset view cube centre X
      m_vcubeCenterX = 0;
      //--- Reset view cube centre Y
      m_vcubeCenterY = 0;
      //--- Reset curve segment count
      m_curveSegmentCount = 0;
     }

   //--- Update destructor to shutdown new curve segments array

   //+------------------------------------------------------------------+
   //| Release all 3D scene objects on destruction                      |
   //+------------------------------------------------------------------+
   ~DistributionVisualizer(void)
     {
      //--- Get total number of histogram bar boxes
      int count = ArraySize(m_histogramBars);
      //--- Release DirectX resources for every histogram bar
      for(int i = 0; i < count; i++)
         m_histogramBars[i].Shutdown();
      //--- Release DirectX resources for every curve tube segment
      for(int i = 0; i < m_curveSegmentCount; i++)
         m_curveSegments[i].Shutdown();
      //--- Release ground plane DirectX resources
      m_groundPlane.Shutdown();
      //--- Release X axis DirectX resources
      m_axisX.Shutdown();
      //--- Release Y axis DirectX resources
      m_axisY.Shutdown();
      //--- Release Z axis DirectX resources
      m_axisZ.Shutdown();
     }
   }

Zunächst fügen wir eine neue Eingabegruppe === VIEW CUBE SETTINGS === hinzu, bei der showViewCubeBackground standardmäßig auf false gesetzt ist, sodass wir einen Hintergrund für die ViewCube-Überlagerung ein- und ausschalten können. Als Nächstes definieren wir Konstanten für das Pan-Icon und den ViewCube: PAN_ICON_SIZE und PAN_ICON_MARGIN für die Abmessungen der Pan-Schaltfläche, VCUBE_SIZE und VCUBE_MARGIN für die Platzierung des ViewCubes. Im geschützten Bereich der Klasse DistributionVisualizer definieren wir Mitglieder für erweiterte Interaktionen: m_isHoveringPanIcon und m_isHoveringViewCube zur Hover-Erkennung, das Array m_curveSegments und m_curveSegmentCount zur Verwaltung von 3D-Kurven, die Booleschen Werte m_panMode und m_isPanning zusammen mit m_panStartX und m_panStartY für den Pan-Status, m_viewTarget als DXVector3 für den Kamerafokus, m_targetAngleX und m_targetAngleY für Animationsziele, m_isAnimating mit m_animSteps, m_animStep, m_animStartAngleX und m_animStartAngleY für flüssige Übergänge, m_vcubeHoverZone (Zeichenkette) für erkannte Würfelbereiche sowie m_vcubeCenterX und m_vcubeCenterY für die Würfelpositionierung.

Im Konstruktor initialisieren wir folgende Variablen: Wir setzen m_isHoveringPanIcon und m_isHoveringViewCube auf false, m_curveSegmentCount auf 0, m_panMode und m_isPanning auf false, m_panStartX und m_panStartY auf 0, m_viewTarget auf den Ursprungsvektor, die Zielwinkel auf 0,0, m_isAnimating auf false, m_animSteps auf 20, m_animStep auf 0, Startwinkel auf 0,0, m_vcubeHoverZone auf eine leere Zeichenkette und die Würfelmittelpunkte auf 0. Wir passen den Destruktor so an, dass die Elemente des neuen Arrays m_curveSegments freigegeben werden, indem wir eine Schleife über m_curveSegmentCount durchlaufen und für jedes Element die Funktion Shutdown aufrufen – zusätzlich zu den bereits vorhandenen Bereinigungsmaßnahmen für Bars, Ebenen und Achsen. Nachdem dies erledigt ist, passen wir die Initialisierung des dreidimensionalen Kontexts wie folgt an, damit die neue Member-Variable verwendet wird. Zur besseren Übersichtlichkeit haben wir die konkrete Änderung hervorgehoben.

Aktualisierung der Initialisierung des dreidimensionalen Kontexts für dynamische Kamerazielpunkte

Die einzige Änderung besteht darin, den an ViewTargetSet übergebenen fest codierten Ursprungsvektor durch die Membervariable m_viewTarget zu ersetzen, sodass Pan-Operationen, die m_viewTarget aktualisieren, sofort berücksichtigt werden, wenn die Kamera neu positioniert wird.

//+------------------------------------------------------------------+
//| Set up projection, lighting and initial camera for 3D rendering  |
//+------------------------------------------------------------------+
bool initialize3DContext()
  {
   //--- Set perspective projection matrix with 30-degree FOV
   m_mainCanvas.ProjectionMatrixSet((float)(DX_PI / 6.0),
                                    (float)m_currentWidth / (float)m_currentHeight,
                                    0.1f, 1000.0f);
   //--- Point the camera at the current view target
   m_mainCanvas.ViewTargetSet(m_viewTarget);
   //--- Define world up direction as positive Y
   m_mainCanvas.ViewUpDirectionSet(DXVector3(0.0f, 1.0f, 0.0f));
   //--- Set directional light colour to near-white
   m_mainCanvas.LightColorSet(DXColor(1.0f, 1.0f, 1.0f, 0.9f));
   //--- Set ambient light colour for soft fill
   m_mainCanvas.AmbientColorSet(DXColor(0.6f, 0.6f, 0.6f, 0.5f));

   //--- Auto-fit camera if enabled and data is already available
   if(autoFitCamera && m_isDataLoaded)
      autoFitCameraPosition();
   //--- Recompute and apply the camera transform
   updateCameraPosition();

   Print("SUCCESS: 3D context initialized");
   return true;
  }

Nachdem der Kontext aktualisiert wurde, erstellen wir nun die dreidimensionalen Kurvensegmente, um die flache, projizierte Spline-Kurve zu ersetzen.

Erstellen der dreidimensionalen Kurvensegmente

Anstatt die PMF als flache Linie in die Szene zu projizieren, erstellen wir eine Reihe ausgerichteter Box-Primitive, die eine durchgehende Röhre bilden, der an aufeinanderfolgende theoretische Datenpunkte ausgerichtet ist. Dadurch erhält die Kurve in der dreidimensionalen Szene räumliche Präsenz, sodass sich die Höhenunterschiede zwischen den Bins in der Perspektive natürlich darstellen.

//+------------------------------------------------------------------+
//| Allocate one DXBox tube segment per PMF curve interval           |
//+------------------------------------------------------------------+
bool create3DCurveSegments()
  {
   //--- Get the total number of PMF sample points
   int numPoints = ArraySize(m_theoreticalXValues);
   Print("DEBUG: create3DCurveSegments called, numPoints=", numPoints);
   //--- Need at least two points to form a segment
   if(numPoints < 2) return false;

   //--- Shutdown any previously created segments before rebuilding
   for(int i = 0; i < m_curveSegmentCount; i++)
      m_curveSegments[i].Shutdown();

   //--- One segment per adjacent pair of PMF points
   m_curveSegmentCount = numPoints - 1;
   ArrayResize(m_curveSegments, m_curveSegmentCount);

   //--- Decompose PMF curve colour into RGB byte components
   uchar cr = (uchar)((theoreticalCurveColor)       & 0xFF);
   uchar cg = (uchar)((theoreticalCurveColor >> 8)  & 0xFF);
   uchar cb = (uchar)((theoreticalCurveColor >> 16) & 0xFF);

   //--- Create and configure each tube segment box
   for(int i = 0; i < m_curveSegmentCount; i++)
     {
      //--- Create unit box; actual transform applied in update step
      if(!m_curveSegments[i].Create(m_mainCanvas.DXDispatcher(), m_mainCanvas.InputScene(),
                                    DXVector3(0.0f, -0.5f, -0.5f),
                                    DXVector3(1.0f,  0.5f,  0.5f)))
        {
         Print("ERROR: Failed to create curve segment ", i);
         return false;
        }
      //--- Set the segment diffuse colour from the curve colour input
      m_curveSegments[i].DiffuseColorSet(DXColor(cr / 255.0f, cg / 255.0f, cb / 255.0f, 1.0f));
      //--- Add a specular highlight to the tube surface
      m_curveSegments[i].SpecularColorSet(DXColor(0.3f, 0.3f, 0.3f, 0.5f));
      //--- Set specular shininess exponent
      m_curveSegments[i].SpecularPowerSet(32.0f);
      //--- Add a faint self-emission for visibility in shadowed areas
      m_curveSegments[i].EmissionColorSet(DXColor(cr / 255.0f * 0.25f,
                                                   cg / 255.0f * 0.25f,
                                                   cb / 255.0f * 0.25f, 1.0f));
      //--- Register the segment with the 3D scene
      m_mainCanvas.ObjectAdd(GetPointer(m_curveSegments[i]));
     }

   Print("DEBUG: Created ", m_curveSegmentCount, " curve segments successfully");
   return true;
  }

Hier definieren wir die Funktion create3DCurveSegments, um eine Reihe von 3D-Box-Primitiven zu erzeugen, die eine röhrenförmige Darstellung der theoretischen Wahrscheinlichkeitsmassenfunktionskurve bilden. In diesem Zusammenhang bedeutet die rohrförmige Darstellung, die Kurve als eine Abfolge miteinander verbundener, boxbasierter Segmente darzustellen, die zusammen einem durchgehenden Rohr ähneln und der Kurve in der 3D-Szene Tiefe sowie eine fließende visuelle Kontinuität verleihen. Die rohrförmige Darstellung ist die beste und einfachste Methode, die uns für diese Präsentation eingefallen ist. Sie können jeden beliebigen Ansatz wählen. Zunächst ermitteln wir die Anzahl der theoretischen Punkte mit ArraySize in m_theoreticalXValues und geben eine Debug-Meldung aus. Wenn weniger als zwei Punkte vorhanden sind, wird false zurückgegeben, da keine Segmente erstellt werden können. Wir durchlaufen das Array m_curveSegments, um alle vorhandenen Segmente mit Shutdown zu schließen, setzen m_curveSegmentCount auf die Anzahl der Punkte minus eins und passen die Größe des Arrays mit der Funktion ArrayResize an.

Als Nächstes werden die RGB-Komponenten mittels Bitoperationen aus theoreticalCurveColor extrahiert. In einer Schleife, die für jedes Segment durchlaufen wird, rufen wir die Methode Create für die Box auf, wobei wir den DX-Dispatcher, die Eingabeszene und die Vektorabmessungen für eine zylinderartige Einheitsform übergeben; Fehler werden durch eine Fehlermeldung und die Rückgabe von false behandelt. Wir legen die Materialeigenschaften fest: diffuse Farbe mit DiffuseColorSet unter Verwendung von normiertem RGB, Spiegelung über SpecularColorSet für den Glanz, Stärke mit SpecularPowerSet auf 32,0f und Emission mit einer leichten Tönung für das Leuchten mithilfe von EmissionColorSet. Anschließend fügen wir jedes Segment mit ObjectAdd unter Übergabe eines Zeigers zur Zeichenfläche hinzu, geben eine Debug-Meldung aus, und true zurück, wenn der Vorgang erfolgreich war.

Bei diesem segmentierten Ansatz wird die Kurve durch zusammenhängende Röhren approximiert, wobei jedes Kästchen später so transformiert wird, dass es den Kurvenpunkten entspricht. Dies ermöglicht Rotation und Tiefenwahrnehmung ohne komplexe Geometrie, was entscheidend ist, um fließende Wahrscheinlichkeitsübergänge in drei Dimensionen zu visualisieren und gleichzeitig eine hohe Leistung zu gewährleisten. Nachdem die Segmente erstellt sind, müssen wir sie nun so positionieren und ausrichten, dass sie mit den tatsächlichen Kurvendaten übereinstimmen.

Aktualisierung der Transformationen von Kurvensegmenten, um den Datenpunkten zu folgen

Da die Segmentboxen als einfache Grundelemente erstellt wurden, berechnet diese Funktion für jede einzelne eine benutzerdefinierte Transformationsmatrix und wendet diese an: Dabei wird das Element auf die richtige Länge gestreckt, so gedreht, dass es mit der Richtung zwischen aufeinanderfolgenden Punkten der Wahrscheinlichkeitsmassenfunktion übereinstimmt, und an der richtigen Position in der dreidimensionalen Szene platziert.

//+------------------------------------------------------------------+
//| Reposition and orient every PMF curve tube segment to match data |
//+------------------------------------------------------------------+
void update3DCurveSegments()
  {
   //--- Abort if data or segments are not ready
   if(!m_isDataLoaded || m_curveSegmentCount == 0) return;

   //--- Compute data ranges for world-space mapping
   double rangeX = m_maxDataValue - m_minDataValue;
   double rangeY = m_maxTheoreticalValue;
   if(rangeX == 0) rangeX = 1;
   if(rangeY == 0) rangeY = 1;

   //--- Match the total scene width used for the histogram bars
   float totalWidth  = 30.0f;
   float offsetX     = -totalWidth / 2.0f;
   float barSpacing  = totalWidth / (float)histogramCells;
   float bw          = barSpacing * 0.8f;
   //--- Tube radius controls the 3D thickness of each curve segment
   float tubeRadius  = 0.2f;
   //--- Place the curve in front of the histogram bars along Z
   float zPos        = bw / 2.0f + 0.5f;

   //--- Limit debug logging to the first call only
   static bool firstDebug = true;

   //--- Build and apply a custom transform matrix for each segment
   for(int i = 0; i < m_curveSegmentCount; i++)
     {
      //--- Map PMF X and Y values to 3D world space coordinates
      float x1 = offsetX + (float)((m_theoreticalXValues[i]     - m_minDataValue) / rangeX * totalWidth);
      float y1 = (float)(m_theoreticalYValues[i]     / rangeY * 15.0);
      float x2 = offsetX + (float)((m_theoreticalXValues[i + 1] - m_minDataValue) / rangeX * totalWidth);
      float y2 = (float)(m_theoreticalYValues[i + 1] / rangeY * 15.0);

      //--- Compute the segment direction vector components
      float dx = x2 - x1;
      float dy = y2 - y1;
      //--- Compute segment length for normalisation
      float segLen = (float)MathSqrt(dx * dx + dy * dy);
      if(segLen < 0.0001f) segLen = 0.0001f;
      //--- Normalise the direction vector
      float dirX = dx / segLen;
      float dirY = dy / segLen;

      //--- Log the first segment only for diagnostics
      if(firstDebug && i == 0)
         Print("DEBUG curve seg0: x1=", x1, " y1=", y1, " x2=", x2, " y2=", y2,
               " len=", segLen, " z=", zPos);

      //--- Build a custom transform that scales, rotates and translates the unit box
      //--- The transform rows encode: [right, up, depth, translation]
      DXMatrix transform;
      transform.m[0][0] = dirX * segLen;      transform.m[0][1] = dirY * segLen;      transform.m[0][2] = 0.0f;         transform.m[0][3] = 0.0f;
      transform.m[1][0] = -dirY * tubeRadius; transform.m[1][1] = dirX * tubeRadius;  transform.m[1][2] = 0.0f;         transform.m[1][3] = 0.0f;
      transform.m[2][0] = 0.0f;               transform.m[2][1] = 0.0f;               transform.m[2][2] = tubeRadius;   transform.m[2][3] = 0.0f;
      transform.m[3][0] = x1;                 transform.m[3][1] = y1;                 transform.m[3][2] = zPos;         transform.m[3][3] = 1.0f;

      //--- Apply the combined transform to this curve segment
      m_curveSegments[i].TransformMatrixSet(transform);
     }
   //--- Suppress first-call debug logging after the first pass
   firstDebug = false;
  }

Hier definieren wir die Funktion update3DCurveSegments, um jedes 3D-Box-Segment, das die Kurve der PMF darstellt, dynamisch zu positionieren und auszurichten, sodass es den Datenpunkten folgt und durch seine röhrenförmige Erscheinung eine verbesserte Tiefenwirkung erzeugt. Wir kehren vorzeitig zurück, wenn keine Daten geladen oder keine Segmente vorhanden sind, berechnen dann die X- und Y-Bereiche mit minimalen Sicherheitsabständen, legen eine Gesamtbreite fest, die dem Histogramm für die Ausrichtung entspricht, und leiten daraus den Versatz, den Barsabstand, die Breite, den Röhrenradius sowie eine Z-Position leicht vor den Bars für die Tiefenstaffelung ab. In einer Schleife über m_curveSegmentCount skalieren wir die Koordinaten x1, y1, x2 und y2 so, dass sie in den 3D-Raum bis zu einer Höhe von 15,0f passen, berechnen den Richtungsvektor aus den durch die Segmentlänge normierten Deltas (mit einem winzigen Mindestwert, um eine Division durch Null zu vermeiden) und geben optional beim ersten Aufruf mithilfe eines statischen Flags Debug-Informationen für das erste Segment aus.

Um jede Einheitsbox in ein orientiertes Rohr umzuwandeln, konstruieren wir eine DXMatrix-Transformation: Zeile 0 streckt die Box entlang der Richtung um die Länge length, Zeile 1 dreht sie senkrecht um den Radius thickness, Zeile 2 legt den Tiefenradius fest und Zeile 3 verschiebt sie an die Startposition bei zPos; anschließend wenden wir sie mit TransformMatrixSet an, um eine präzise Platzierung zu gewährleisten. Abschließend setzen wir das Debug-Flag nach dem ersten Durchlauf zurück und schließen damit die Aktualisierung ab, durch die flache Kurvendaten in eine drehbare dreidimensionale Struktur umgewandelt werden, wodurch die Wahrnehmbarkeit von Wahrscheinlichkeitsschwankungen über die gesamte Verteilung hinweg verbessert wird. Nachdem nun sowohl die Erstellungs- als auch die Aktualisierungsfunktion fertig sind, binden wir sie wie folgt in den bestehenden Szenenaufbau ein.

Einbindung der Erstellung und Aktualisierung in die Szenen-Pipeline

Drei gezielte Ergänzungen verbinden die neuen Kurvenfunktionen mit dem bestehenden Aufbau: Segmente werden bei der anfänglichen Objektkonstruktion erstellt, sofern bereits Daten geladen sind; sie werden beim Wechsel in den dreidimensionalen Modus neu erstellt, falls noch keine vorhanden sind, und bei jedem Neuladen der Verteilungsdaten zusammen mit den Histogramm-Bars aktualisiert.

//--- Update "create3DObjects" to attempt creating curve segments if data is loaded

//--- Attempt to create PMF curve tube segments (non-fatal if it fails)
if(m_isDataLoaded && !create3DCurveSegments())
   Print("WARNING: Failed to create 3D curve segments");

//--- Update "setup3DMode" to create and update curve segments if needed

//--- Build curve segments if they were not created during init
if(m_isDataLoaded && m_curveSegmentCount == 0)
   create3DCurveSegments();

//--- Update "loadDistributionData" to update curve segments in 3D mode

if(m_curveSegmentCount == 0)
   create3DCurveSegments();
//--- Reposition every curve segment in 3D space
update3DCurveSegments();

Nach dem Kompilieren werden die dreidimensionalen Kurvensegmente wie folgt neben den Histogramm-Bars angezeigt.

3D-KURVENSEGMENTE

Nachdem die Kurve erstellt ist, fügen wir nun das Pan-Symbol-Overlay hinzu, mit dem der Benutzer den Pan-Modus direkt in der dreidimensionalen Ansicht ein- und ausschalten kann.

Das Symbol zum Umschalten des Pan-Modus zeichnen

Um dem Benutzer eine sichtbare Steuerung zur Aktivierung des Pan-Modus zu bieten, zeigen wir ein kreisförmiges Symbol an, das direkt unter der Modusumschalttaste positioniert ist. Das Symbol färbt sich grün, wenn der Pan-Modus aktiv ist, wird beim Darüberfahren mit der Maus dunkler und zeigt ein aus überblendeten Pixeln bestehendes Kreuz-mit-Pfeilen-Symbol an, um darauf hinzuweisen, dass es zum Verschieben durch Ziehen dient.

//+------------------------------------------------------------------+
//| Draw the pan mode toggle button overlaid on the 3D canvas        |
//+------------------------------------------------------------------+
void drawPanIconOverlay()
  {
   //--- Pan icon is only drawn in 3D mode
   if(m_currentViewMode != VIEW_3D_MODE) return;

   //--- Compute icon position below the header on the right side
   int iconX = m_currentWidth  - PAN_ICON_SIZE - PAN_ICON_MARGIN;
   int iconY = HEADER_BAR_HEIGHT + PAN_ICON_MARGIN;
   int cx    = iconX + PAN_ICON_SIZE / 2;
   int cy    = iconY + PAN_ICON_SIZE / 2;
   int rad   = PAN_ICON_SIZE / 2;

   //--- Compute icon background colour based on interaction state
   color iconBgColor;
   if(m_panMode)
      iconBgColor = clrGreen;                         // Active pan mode: green
   else if(m_isHoveringPanIcon)
      iconBgColor = DarkenColor(themeColor, 0.1);     // Hover: slightly darker
   else
      iconBgColor = LightenColor(themeColor, 0.5);    // Default: lighter shade

   uint argbBg     = ColorToARGB(iconBgColor, 220);
   uint argbBorder = ColorToARGB(themeColor, 255);
   uint argbWhite  = ColorToARGB(clrWhite, 255);

   //--- Fill the circular icon background
   for(int py = cy - rad; py <= cy + rad; py++)
      for(int px = cx - rad; px <= cx + rad; px++)
        {
         int dx = px - cx;
         int dy = py - cy;
         if(dx * dx + dy * dy <= rad * rad)
            blendPixelSet(m_mainCanvas, px, py, argbBg);
        }

   //--- Draw the circular icon border ring
   for(int py = cy - rad; py <= cy + rad; py++)
      for(int px = cx - rad; px <= cx + rad; px++)
        {
         int dx = px - cx;
         int dy = py - cy;
         int distSq = dx * dx + dy * dy;
         if(distSq <= rad * rad && distSq >= (rad - 1) * (rad - 1))
            blendPixelSet(m_mainCanvas, px, py, argbBorder);
        }

   //--- Draw the four-arrow pan icon symbol in white
   int arrLen  = 4;
   int arrHead = 2;
   //--- Draw the horizontal crosshair bar
   for(int i = -arrLen; i <= arrLen; i++)
      blendPixelSet(m_mainCanvas, cx + i, cy, argbWhite);
   //--- Draw the vertical crosshair bar
   for(int i = -arrLen; i <= arrLen; i++)
      blendPixelSet(m_mainCanvas, cx, cy + i, argbWhite);
   //--- Draw the four arrowheads at each end of the crosshair
   for(int i = 0; i <= arrHead; i++)
     {
      blendPixelSet(m_mainCanvas, cx + arrLen - i, cy - i, argbWhite); // Right arrow top
      blendPixelSet(m_mainCanvas, cx + arrLen - i, cy + i, argbWhite); // Right arrow bottom
      blendPixelSet(m_mainCanvas, cx - arrLen + i, cy - i, argbWhite); // Left arrow top
      blendPixelSet(m_mainCanvas, cx - arrLen + i, cy + i, argbWhite); // Left arrow bottom
      blendPixelSet(m_mainCanvas, cx - i, cy - arrLen + i, argbWhite); // Up arrow left
      blendPixelSet(m_mainCanvas, cx + i, cy - arrLen + i, argbWhite); // Up arrow right
      blendPixelSet(m_mainCanvas, cx - i, cy + arrLen - i, argbWhite); // Down arrow left
      blendPixelSet(m_mainCanvas, cx + i, cy + arrLen - i, argbWhite); // Down arrow right
     }
  }

Wir definieren die Funktion drawPanIconOverlay, um ein kreisförmiges Umschalt-Symbol für den Pan-Modus in der 3D-Ansicht darzustellen, das unterhalb der Kopfzeile positioniert wird, wobei wir Konstanten wie PAN_ICON_SIZE und PAN_ICON_MARGIN verwenden. Wir wählen die Hintergrundfarbe aus: grün, wenn m_panMode aktiv ist, eine abgedunkelte Version von themeColor beim Überfahren mit der Maus mithilfe von DarkenColor oder andernfalls eine aufgehellte Version über LightenColor, und wandeln sie anschließend mit teilweiser Deckkraft in ARGB um. Um die Schaltfläche zu erstellen, durchlaufen wir einen quadratischen Bereich in einer Schleife und mischen die Pixel innerhalb des Radius mit blendPixelSet für die Füllung sowie separat für den Rand, indem wir den äußeren Ring überprüfen und dabei das ARGB-Thema für den Umriss verwenden.

Für die Symbolgrafiken legen wir die Pfeillänge und die Größe der Pfeilspitzen fest, zeichnen mit blendPixelSet in weißem ARGB horizontale und vertikale Linien durch die Mitte und fügen an jedem Ende dreieckige Pfeilspitzen hinzu, indem wir versetzte Pixel überblenden, wodurch ein Kreuz mit Zeigern entsteht, das eine intuitive Pan-Anzeige bildet. Dieses Overlay bietet visuelles Feedback und Zugriff auf Umschaltfunktionen. Es wird nach dem Rendern der dreidimensionalen Szene gezeichnet, sodass es darüber liegt, ohne die DirectX-Ausgabe zu beeinträchtigen. Beim Aufruf der Funktion wird das Pan-Overlay wie folgt dargestellt.

PAN-OVERLAY

Nachdem das Pan-Overlay eingeblendet ist, erstellen wir nun das ViewCube-Overlay, das die Kameraausrichtung widerspiegelt und anklickbare Navigationsbereiche bereitstellt.

Projektion und Darstellung der ViewCube-Überlagerung

Der ViewCube wird als zweidimensionale Überlagerung dargestellt, indem die acht Eckpunkte eines Einheitswürfels durch die aktuellen Kamerawinkel projiziert werden, wobei seine sechs Flächen für eine korrekte Okklusion von hinten nach vorne sortiert werden, wobei jede sichtbare Fläche in ein 3×3-Raster aus anklickbaren Teilbereichen mit bilinearer Interpolation unterteilt wird, jeder Teilbereich mit einer auf der Helligkeit basierenden Schattierung gefüllt wird und farbige Achsenlinien vom projizierten Ursprung als Orientierungshilfe gezeichnet werden.

//+------------------------------------------------------------------+
//| Project a 3D view cube vertex into 2D screen coordinates         |
//+------------------------------------------------------------------+
void vcubeProject(double x, double y, double z, double angX, double angY, int &sx, int &sy)
  {
   //--- Pre-compute trigonometric values for both rotation angles
   double cosAX = MathCos(angX), sinAX = MathSin(angX);
   double cosAY = MathCos(angY), sinAY = MathSin(angY);
   //--- Rotate the point around the X axis (elevation)
   double y2 = y * cosAX - z * sinAX;
   double z2 = y * sinAX + z * cosAX;
   //--- Rotate the result around the Y axis (azimuth)
   double x2 = x * cosAY + z2 * sinAY;
   //--- Apply a fixed orthographic scale relative to the cube widget size
   double scale = VCUBE_SIZE * 0.30;
   //--- Map to screen pixels relative to the view cube centre
   sx = m_vcubeCenterX + (int)(x2 * scale);
   sy = m_vcubeCenterY - (int)(y2 * scale);
  }

//+------------------------------------------------------------------+
//| Draw and label the interactive 3D view cube overlay widget       |
//+------------------------------------------------------------------+
void drawViewCubeOverlay()
  {
   //--- View cube is only drawn in 3D mode
   if(m_currentViewMode != VIEW_3D_MODE) return;

   //--- Compute the view cube bounding area below the pan icon
   int areaX       = m_currentWidth  - VCUBE_SIZE - VCUBE_MARGIN;
   int areaY       = HEADER_BAR_HEIGHT + PAN_ICON_MARGIN + PAN_ICON_SIZE + VCUBE_MARGIN;
   m_vcubeCenterX  = areaX + VCUBE_SIZE / 2;
   m_vcubeCenterY  = areaY + VCUBE_SIZE / 2;

   //--- Optionally draw a semi-transparent background panel behind the cube
   if(showViewCubeBackground)
     {
      uint argbPanelBg     = ColorToARGB(LightenColor(themeColor, 0.92), 200);
      uint argbPanelBorder = ColorToARGB(themeColor, 200);
      //--- Fill the background panel
      for(int py = areaY; py < areaY + VCUBE_SIZE; py++)
         for(int px = areaX; px < areaX + VCUBE_SIZE; px++)
            blendPixelSet(m_mainCanvas, px, py, argbPanelBg);
      //--- Draw top and bottom borders
      for(int px = areaX; px < areaX + VCUBE_SIZE; px++)
        {
         blendPixelSet(m_mainCanvas, px, areaY, argbPanelBorder);
         blendPixelSet(m_mainCanvas, px, areaY + VCUBE_SIZE - 1, argbPanelBorder);
        }
      //--- Draw left and right borders
      for(int py = areaY; py < areaY + VCUBE_SIZE; py++)
        {
         blendPixelSet(m_mainCanvas, areaX, py, argbPanelBorder);
         blendPixelSet(m_mainCanvas, areaX + VCUBE_SIZE - 1, py, argbPanelBorder);
        }
     }

   //--- Use the current camera angles as the cube orientation
   double angX = m_cameraAngleX;
   double angY = m_cameraAngleY;

   //--- Define the 8 vertices of the unit cube in local space
   double verts[8][3];
   verts[0][0] = -1; verts[0][1] = -1; verts[0][2] = -1;
   verts[1][0] =  1; verts[1][1] = -1; verts[1][2] = -1;
   verts[2][0] =  1; verts[2][1] =  1; verts[2][2] = -1;
   verts[3][0] = -1; verts[3][1] =  1; verts[3][2] = -1;
   verts[4][0] = -1; verts[4][1] = -1; verts[4][2] =  1;
   verts[5][0] =  1; verts[5][1] = -1; verts[5][2] =  1;
   verts[6][0] =  1; verts[6][1] =  1; verts[6][2] =  1;
   verts[7][0] = -1; verts[7][1] =  1; verts[7][2] =  1;

   //--- Project all 8 vertices to screen space
   int sv[8][2];
   for(int i = 0; i < 8; i++)
      vcubeProject(verts[i][0], verts[i][1], verts[i][2], angX, angY, sv[i][0], sv[i][1]);

   //--- Define the 6 faces by their vertex indices (counter-clockwise)
   int faces[6][4];
   faces[0][0]=0; faces[0][1]=1; faces[0][2]=2; faces[0][3]=3; // Front
   faces[1][0]=5; faces[1][1]=4; faces[1][2]=7; faces[1][3]=6; // Back
   faces[2][0]=3; faces[2][1]=2; faces[2][2]=6; faces[2][3]=7; // Top
   faces[3][0]=0; faces[3][1]=1; faces[3][2]=5; faces[3][3]=4; // Bottom
   faces[4][0]=1; faces[4][1]=5; faces[4][2]=6; faces[4][3]=2; // Right
   faces[5][0]=4; faces[5][1]=0; faces[5][2]=3; faces[5][3]=7; // Left

   //--- Face label strings for text overlay
   string faceNames[6];
   faceNames[0] = "Front";
   faceNames[1] = "Back";
   faceNames[2] = "Top";
   faceNames[3] = "Bottom";
   faceNames[4] = "Right";
   faceNames[5] = "Left";

   //--- Face outward normals for back-face culling
   double faceNormals[6][3];
   faceNormals[0][0]= 0; faceNormals[0][1]= 0; faceNormals[0][2]=-1;
   faceNormals[1][0]= 0; faceNormals[1][1]= 0; faceNormals[1][2]= 1;
   faceNormals[2][0]= 0; faceNormals[2][1]= 1; faceNormals[2][2]= 0;
   faceNormals[3][0]= 0; faceNormals[3][1]=-1; faceNormals[3][2]= 0;
   faceNormals[4][0]= 1; faceNormals[4][1]= 0; faceNormals[4][2]= 0;
   faceNormals[5][0]=-1; faceNormals[5][1]= 0; faceNormals[5][2]= 0;

   //--- Camera direction vector for depth sorting and back-face culling
   double camDir[3];
   camDir[0] =  MathCos(angX) * MathSin(angY);
   camDir[1] = -MathSin(angX);
   camDir[2] = -MathCos(angX) * MathCos(angY);

   //--- Sort faces back-to-front using their projected depth (painter's algorithm)
   int    faceOrder[6];
   double faceDepth[6];
   for(int i = 0; i < 6; i++)
     {
      faceOrder[i] = i;
      double cx = 0, cy2 = 0, cz = 0;
      //--- Compute the face centre as the average of its four vertices
      for(int j = 0; j < 4; j++)
        {
         cx  += verts[faces[i][j]][0];
         cy2 += verts[faces[i][j]][1];
         cz  += verts[faces[i][j]][2];
        }
      cx /= 4.0; cy2 /= 4.0; cz /= 4.0;
      //--- Project the centre onto the camera direction for depth
      faceDepth[i] = cx * camDir[0] + cy2 * camDir[1] + cz * camDir[2];
     }
   //--- Bubble-sort the face order from farthest to nearest
   for(int i = 0; i < 5; i++)
      for(int j = i + 1; j < 6; j++)
         if(faceDepth[faceOrder[i]] > faceDepth[faceOrder[j]])
           {
            int tmp = faceOrder[i];
            faceOrder[i] = faceOrder[j];
            faceOrder[j] = tmp;
           }

   //--- Assign a distinct base colour to each face for identification
   color faceColors[6];
   faceColors[0] = clrCornflowerBlue; // Front
   faceColors[1] = clrSteelBlue;      // Back
   faceColors[2] = clrLimeGreen;      // Top
   faceColors[3] = clrDarkGreen;      // Bottom
   faceColors[4] = clrOrangeRed;      // Right
   faceColors[5] = clrDarkOrange;     // Left

   //--- Sub-face zone names for all 6 faces (3x3 grid per face)
   string faceSubNames[6][3][3];
   // Front face sub-zones
   faceSubNames[0][0][0] = "BottomFrontLeft";   faceSubNames[0][1][0] = "BottomFront";   faceSubNames[0][2][0] = "BottomFrontRight";
   faceSubNames[0][0][1] = "FrontLeft";          faceSubNames[0][1][1] = "Front";         faceSubNames[0][2][1] = "FrontRight";
   faceSubNames[0][0][2] = "TopFrontLeft";       faceSubNames[0][1][2] = "TopFront";      faceSubNames[0][2][2] = "TopFrontRight";
   // Back face sub-zones
   faceSubNames[1][0][0] = "BottomBackLeft";     faceSubNames[1][1][0] = "BottomBack";    faceSubNames[1][2][0] = "BottomBackRight";
   faceSubNames[1][0][1] = "BackLeft";           faceSubNames[1][1][1] = "Back";          faceSubNames[1][2][1] = "BackRight";
   faceSubNames[1][0][2] = "TopBackLeft";        faceSubNames[1][1][2] = "TopBack";       faceSubNames[1][2][2] = "TopBackRight";
   // Top face sub-zones
   faceSubNames[2][0][0] = "TopFrontLeft";       faceSubNames[2][1][0] = "TopFront";      faceSubNames[2][2][0] = "TopFrontRight";
   faceSubNames[2][0][1] = "TopLeft";            faceSubNames[2][1][1] = "Top";           faceSubNames[2][2][1] = "TopRight";
   faceSubNames[2][0][2] = "TopBackLeft";        faceSubNames[2][1][2] = "TopBack";       faceSubNames[2][2][2] = "TopBackRight";
   // Bottom face sub-zones
   faceSubNames[3][0][0] = "BottomFrontLeft";   faceSubNames[3][1][0] = "BottomFront";   faceSubNames[3][2][0] = "BottomFrontRight";
   faceSubNames[3][0][1] = "BottomLeft";         faceSubNames[3][1][1] = "Bottom";        faceSubNames[3][2][1] = "BottomRight";
   faceSubNames[3][0][2] = "BottomBackLeft";     faceSubNames[3][1][2] = "BottomBack";    faceSubNames[3][2][2] = "BottomBackRight";
   // Right face sub-zones
   faceSubNames[4][0][0] = "BottomFrontRight";  faceSubNames[4][1][0] = "BottomRight";   faceSubNames[4][2][0] = "BottomBackRight";
   faceSubNames[4][0][1] = "FrontRight";         faceSubNames[4][1][1] = "Right";         faceSubNames[4][2][1] = "BackRight";
   faceSubNames[4][0][2] = "TopFrontRight";      faceSubNames[4][1][2] = "TopRight";      faceSubNames[4][2][2] = "TopBackRight";
   // Left face sub-zones
   faceSubNames[5][0][0] = "BottomFrontLeft";   faceSubNames[5][1][0] = "BottomLeft";    faceSubNames[5][2][0] = "BottomBackLeft";
   faceSubNames[5][0][1] = "FrontLeft";          faceSubNames[5][1][1] = "Left";          faceSubNames[5][2][1] = "BackLeft";
   faceSubNames[5][0][2] = "TopFrontLeft";       faceSubNames[5][1][2] = "TopLeft";       faceSubNames[5][2][2] = "TopBackLeft";

   //--- Shrink factor creates a small visible gap between sub-face tiles
   double shrink = 0.03;

   //--- Render visible faces in back-to-front order
   for(int fi = 0; fi < 6; fi++)
     {
      int f = faceOrder[fi];
      //--- Compute the dot product to cull back-facing faces
      double dot = faceNormals[f][0] * camDir[0] +
                   faceNormals[f][1] * camDir[1] +
                   faceNormals[f][2] * camDir[2];
      if(dot >= 0) continue;

      //--- Compute Lambert diffuse brightness from the dot product
      double brightness = MathAbs(dot);
      brightness = 0.4 + 0.6 * brightness;

      //--- Apply brightness to the base face colour
      color fc = faceColors[f];
      uchar fr_base = (uchar)MathMin(255, (int)(((fc >> 16) & 0xFF) * brightness));
      uchar fg_base = (uchar)MathMin(255, (int)(((fc >> 8)  & 0xFF) * brightness));
      uchar fb_base = (uchar)MathMin(255, (int)(( fc        & 0xFF) * brightness));

      uchar alpha = 180;

      //--- Get the four projected screen vertices for this face
      int fx[4], fy[4];
      for(int j = 0; j < 4; j++)
        {
         fx[j] = sv[faces[f][j]][0];
         fy[j] = sv[faces[f][j]][1];
        }

      //--- Draw each 3x3 sub-face tile using bilinear interpolation
      for(int i = 0; i < 3; i++)
         for(int j = 0; j < 3; j++)
           {
            //--- Compute the UV bounds for this sub-tile with shrink margin
            double u1 = i / 3.0 + shrink;
            double u2 = (i + 1) / 3.0 - shrink;
            double v1 = j / 3.0 + shrink;
            double v2 = (j + 1) / 3.0 - shrink;
            if(u1 >= u2 || v1 >= v2) continue;

            //--- Compute the four screen-space corners of this sub-tile
            int sub_fx[4], sub_fy[4];
            sub_fx[0] = (int)bilinear(fx[0], fx[1], fx[3], fx[2], u1, v1);
            sub_fy[0] = (int)bilinear(fy[0], fy[1], fy[3], fy[2], u1, v1);
            sub_fx[1] = (int)bilinear(fx[0], fx[1], fx[3], fx[2], u2, v1);
            sub_fy[1] = (int)bilinear(fy[0], fy[1], fy[3], fy[2], u2, v1);
            sub_fx[2] = (int)bilinear(fx[0], fx[1], fx[3], fx[2], u2, v2);
            sub_fy[2] = (int)bilinear(fy[0], fy[1], fy[3], fy[2], u2, v2);
            sub_fx[3] = (int)bilinear(fx[0], fx[1], fx[3], fx[2], u1, v2);
            sub_fy[3] = (int)bilinear(fy[0], fy[1], fy[3], fy[2], u1, v2);

            //--- Look up the zone name for this sub-tile
            string subZone  = faceSubNames[f][i][j];
            //--- Check if this tile is the one currently hovered
            bool isHovered = (m_vcubeHoverZone == subZone);

            //--- Copy base colour channels for potential brightening
            uchar fr = fr_base;
            uchar fg = fg_base;
            uchar fb = fb_base;
            //--- Increase alpha and brighten colour when hovered
            uchar subAlpha = isHovered ? (uchar)240 : alpha;
            if(isHovered)
              {
               fr = (uchar)MathMin(255, fr + 40);
               fg = (uchar)MathMin(255, fg + 40);
               fb = (uchar)MathMin(255, fb + 40);
              }
            //--- Pack ARGB colour for this sub-tile
            uint argbFace = ((uint)subAlpha << 24) | ((uint)fr << 16) | ((uint)fg << 8) | (uint)fb;
            //--- Fill the sub-tile quad with the computed colour
            fillQuad(sub_fx, sub_fy, argbFace);
           }

      //--- Draw the four black outline edges of the face
      uint argbEdge = ColorToARGB(clrBlack, 200);
      for(int j = 0; j < 4; j++)
        {
         int next = (j + 1) % 4;
         m_mainCanvas.LineAA(fx[j], fy[j], fx[next], fy[next], argbEdge);
        }

      //--- Compute the face screen-space centre for text placement
      int fcx = (fx[0] + fx[1] + fx[2] + fx[3]) / 4;
      int fcy = (fy[0] + fy[1] + fy[2] + fy[3]) / 4;

      //--- Draw the face name label only for well-lit faces
      if(brightness > 0.5)
        {
         m_mainCanvas.FontSet("Arial Bold", 7);
         uint textCol = ColorToARGB(clrWhite, 220);
         m_mainCanvas.TextOut(fcx, fcy - 4, faceNames[f], textCol, TA_CENTER);
        }
     }

   //--- Draw the three coordinate axis indicators on the view cube
   uint argbAxis;
   int  ox, oy;
   vcubeProject(0, 0, 0, angX, angY, ox, oy);

   //--- Draw the X axis indicator and label
   int axEnd;
   argbAxis = ColorToARGB(clrRed, 220);
   vcubeProject(1.4, 0, 0, angX, angY, axEnd, oy);
   m_mainCanvas.LineAA(ox, oy, axEnd, oy, argbAxis);
   m_mainCanvas.FontSet("Arial Bold", 7);
   m_mainCanvas.TextOut(axEnd + 2, oy - 4, "X", ColorToARGB(clrRed, 255), TA_LEFT);

   //--- Draw the Y axis indicator and label
   int dummy;
   argbAxis = ColorToARGB(clrGreen, 220);
   vcubeProject(0, 1.4, 0, angX, angY, dummy, axEnd);
   m_mainCanvas.LineAA(ox, oy, dummy, axEnd, argbAxis);
   m_mainCanvas.TextOut(dummy + 2, axEnd - 4, "Y", ColorToARGB(clrGreen, 255), TA_LEFT);

   //--- Draw the Z axis indicator and label
   argbAxis = ColorToARGB(clrBlue, 220);
   vcubeProject(0, 0, 1.4, angX, angY, axEnd, dummy);
   m_mainCanvas.LineAA(ox, oy, axEnd, dummy, argbAxis);
   m_mainCanvas.TextOut(axEnd + 2, dummy - 4, "Z", ColorToARGB(clrBlue, 255), TA_LEFT);
  }

//+------------------------------------------------------------------+
//| Bilinearly interpolate between four corner values                |
//+------------------------------------------------------------------+
double bilinear(double p00, double p10, double p01, double p11, double u, double v)
  {
   //--- Compute the weighted blend of the four corner values
   return (1 - u) * (1 - v) * p00 + u * (1 - v) * p10 +
          (1 - u) *      v  * p01 + u *       v  * p11;
  }

//+------------------------------------------------------------------+
//| Scanline-fill a convex quadrilateral with the given ARGB colour  |
//+------------------------------------------------------------------+
void fillQuad(int &px[], int &py[], uint clr)
  {
   //--- Determine the vertical span of the quad
   int minY = py[0], maxY = py[0];
   for(int i = 1; i < 4; i++)
     {
      if(py[i] < minY) minY = py[i];
      if(py[i] > maxY) maxY = py[i];
     }

   //--- Scanline-fill the quad row by row
   for(int y = minY; y <= maxY; y++)
     {
      int minX = 99999, maxX = -99999;
      //--- Find the left and right intersection X for this scanline
      for(int i = 0; i < 4; i++)
        {
         int j  = (i + 1) % 4;
         int y1 = py[i], y2 = py[j];
         int x1 = px[i], x2 = px[j];
         //--- Process edges that cross this scanline
         if((y1 <= y && y2 >= y) || (y2 <= y && y1 >= y))
           {
            if(y1 == y2)
              {
               //--- Horizontal edge: include both endpoints
               if(x1 < minX) minX = x1;
               if(x2 < minX) minX = x2;
               if(x1 > maxX) maxX = x1;
               if(x2 > maxX) maxX = x2;
              }
            else
              {
               //--- Non-horizontal edge: interpolate the X intersection
               int ix = x1 + (y - y1) * (x2 - x1) / (y2 - y1);
               if(ix < minX) minX = ix;
               if(ix > maxX) maxX = ix;
              }
           }
        }
      //--- Paint every pixel on this scanline row
      for(int x = minX; x <= maxX; x++)
         blendPixelSet(m_mainCanvas, x, y, clr);
     }
  }

Zunächst definieren wir die Funktion vcubeProject, um einen 3D-Punkt (x, y, z) anhand der Drehwinkel angX und angY auf 2D-Bildschirmkoordinaten (sx, sy) zu projizieren und so eine isometrische Ansicht für den ViewCube zu simulieren. Wir berechnen Kosinus- und Sinuswerte mit MathCos und MathSin, drehen um die X-Achse, indem wir y und z in y2 und z2 umwandeln, anschließend um die Y-Achse, indem wir x mit z2 auf x2 anpassen, skalieren mit einem aus VCUBE_SIZE abgeleiteten Faktor und verschieben die Werte um m_vcubeCenterX und m_vcubeCenterY, um sie zu zentrieren, wodurch eine genaue 2D-Abbildung der Eckpunkte des Würfels ermöglicht wird.

Um den interaktiven ViewCube im 3D-Modus als Overlay darzustellen, implementieren wir die Funktion drawViewCubeOverlay und positionieren sie mithilfe von Konstanten wie VCUBE_SIZE und VCUBE_MARGIN unterhalb des Pan-Icons, und legen die Mittelpunkte m_vcubeCenterX und m_vcubeCenterY fest. Wenn showViewCubeBackground auf true gesetzt ist, mischen wir ein halbtransparentes Feld mit blendPixelSet für die Füllung und die Ränder unter Verwendung einer aufgehellten Version von themeColor. Wir synchronisieren die Winkel mit der aktuellen Kamera, definieren die Eckpunkte des Würfels als Array von Double-Werten und projizieren jeden einzelnen mit vcubeProject auf Ganzzahlpaare in sv. Wir legen Flächenindizes, Namen und Normalen fest, berechnen den Kamerarichtungsvektor aus den Winkeln, ermitteln die Tiefe jeder Fläche durch Mittelwertbildung der projizierten Mittelpunkte und das Skalarprodukt mit den Normalen und sortieren die faceOrder nach aufsteigender Tiefe, um die Darstellung von hinten nach vorne zu gewährleisten und so die Okklusion zu berücksichtigen. Bei den Farben weisen wir den Flächen unterschiedliche Farben zu und passen die Helligkeit anhand des absoluten Skalarprodukts für die Schattierung an.

Mit einem Verkleinerungsfaktor für Unterteilungen durchlaufen wir pro Fläche ein 3x3-Raster, berechnen die Koordinaten der Teilfläche unter Verwendung von bilinear, prüfen, ob der Mauszeiger über einer SubZone aus dem Array faceSubNames schwebt (ein großes statisches 6×3×3-Zeichenfolgenarray, das Zonen wie TopFrontLeft definiert), hellen die Fläche auf und erhöhen den Alpha-Wert, wenn der Mauszeiger darüber schwebt, setzen ARGB zusammen und füllen mit fillQuad. Wir fügen mithilfe von LineAA-Schleifen schwarze Ränder hinzu, zentrieren Beschriftungen auf helleren Flächen mit TextOut und einer kleinen, fetten Schriftart und projizieren und zeichnen dann farbige Achsenlinien vom Ursprung aus mit den Beschriftungen X, Y und Z zur Orientierung.

Wir definieren die bilineare Funktion zur Interpolation von 2D-Quad-Werten am Punkt (u, v) zwischen den Eckpunkten p00 und p11 unter Verwendung gewichteter Summen, was eine glatte Unterteilung im ViewCube ermöglicht und so eine präzise Hover-Erkennung ohne gezackte Kanten gewährleistet. Abschließend definieren wir die Funktion fillQuad, um beliebige Vierecke anhand von px- und py-Arrays zu rasterisieren und auszufüllen, wobei die Min-/Max-Y-Werte ermittelt werden, horizontale Linien abtasten, um über Kantenkreuzungspunkte die Min-/Max-X-Werte zu berechnen (wobei horizontale Kanten separat behandelt werden), und jedes Pixel in der Zeile mit blendPixelSet mischen, wodurch eine einheitliche Flächenwiedergabe in der Würfelüberlagerung ermöglicht wird. Wenn wir diese Funktion aufrufen, wird der ViewCube wie folgt dargestellt.

VIEWCUBE – RENDERING

Nachdem der ViewCube nun korrekt dargestellt wird, implementieren wir nun die Interaktionslogik für die Zonenerkennung, die Klickverarbeitung und die Kameraanimation.

Erkennen von Hover-Zonen und Verarbeiten von Klicks auf den ViewCube

Bei der Zonenerkennung werden die Mittelpunkte aller Flächen, Kanten und Ecken projiziert und der projizierte Punkt ermittelt, der innerhalb eines zonenspezifischen Abstandsschwellenwerts am nächsten am Cursor liegt; das Ergebnis wird in m_vcubeHoverZone gespeichert. Ein Klick auf einen der erkannten Bereiche ordnet diesem Bereichsnamen ein Zielwinkelpaar zu und startet eine zeitgesteuerte Animation in diese Richtung.

//+------------------------------------------------------------------+
//| Detect which face, edge or corner of the view cube is hovered    |
//+------------------------------------------------------------------+
void detectViewCubeZone(int localX, int localY)
  {
   //--- Use the current camera angles for the cube orientation
   double angX = m_cameraAngleX;
   double angY = m_cameraAngleY;

   //--- Compute the camera direction for back-face filtering
   double camDir[3];
   camDir[0] =  MathCos(angX) * MathSin(angY);
   camDir[1] = -MathSin(angX);
   camDir[2] = -MathCos(angX) * MathCos(angY);

   //--- Face name and normal lookup tables
   string faceNames[6];
   faceNames[0] = "Front"; faceNames[1] = "Back"; faceNames[2] = "Top";
   faceNames[3] = "Bottom"; faceNames[4] = "Right"; faceNames[5] = "Left";

   double faceNormals[6][3];
   faceNormals[0][0]= 0; faceNormals[0][1]= 0; faceNormals[0][2]=-1;
   faceNormals[1][0]= 0; faceNormals[1][1]= 0; faceNormals[1][2]= 1;
   faceNormals[2][0]= 0; faceNormals[2][1]= 1; faceNormals[2][2]= 0;
   faceNormals[3][0]= 0; faceNormals[3][1]=-1; faceNormals[3][2]= 0;
   faceNormals[4][0]= 1; faceNormals[4][1]= 0; faceNormals[4][2]= 0;
   faceNormals[5][0]=-1; faceNormals[5][1]= 0; faceNormals[5][2]= 0;

   //--- Face centre lookup table
   double faceCenters[6][3];
   faceCenters[0][0]= 0; faceCenters[0][1]= 0; faceCenters[0][2]=-1;
   faceCenters[1][0]= 0; faceCenters[1][1]= 0; faceCenters[1][2]= 1;
   faceCenters[2][0]= 0; faceCenters[2][1]= 1; faceCenters[2][2]= 0;
   faceCenters[3][0]= 0; faceCenters[3][1]=-1; faceCenters[3][2]= 0;
   faceCenters[4][0]= 1; faceCenters[4][1]= 0; faceCenters[4][2]= 0;
   faceCenters[5][0]=-1; faceCenters[5][1]= 0; faceCenters[5][2]= 0;

   //--- Initialise the best match distance and clear the zone
   double bestDist = 99999;
   m_vcubeHoverZone = "";

   //--- Check each visible face centre against the cursor position
   for(int i = 0; i < 6; i++)
     {
      //--- Skip back-facing faces (they cannot be clicked)
      double dot = faceNormals[i][0] * camDir[0] +
                   faceNormals[i][1] * camDir[1] +
                   faceNormals[i][2] * camDir[2];
      if(dot >= 0) continue;
      int sx, sy;
      vcubeProject(faceCenters[i][0], faceCenters[i][1], faceCenters[i][2], angX, angY, sx, sy);
      double dx = localX - sx;
      double dy = localY - sy;
      double d  = MathSqrt(dx * dx + dy * dy);
      //--- Update the closest zone within a 15-pixel radius
      if(d < 15 && d < bestDist)
        {
         bestDist = d;
         m_vcubeHoverZone = faceNames[i];
        }
     }

   //--- Edge midpoint lookup tables
   double edgeCenters[12][3];
   string edgeNames[12];
   edgeCenters[0][0]= 0;  edgeCenters[0][1]= 1;  edgeCenters[0][2]=-1;  edgeNames[0]  = "TopFront";
   edgeCenters[1][0]= 0;  edgeCenters[1][1]= 1;  edgeCenters[1][2]= 1;  edgeNames[1]  = "TopBack";
   edgeCenters[2][0]= 1;  edgeCenters[2][1]= 1;  edgeCenters[2][2]= 0;  edgeNames[2]  = "TopRight";
   edgeCenters[3][0]=-1;  edgeCenters[3][1]= 1;  edgeCenters[3][2]= 0;  edgeNames[3]  = "TopLeft";
   edgeCenters[4][0]= 0;  edgeCenters[4][1]=-1;  edgeCenters[4][2]=-1;  edgeNames[4]  = "BottomFront";
   edgeCenters[5][0]= 0;  edgeCenters[5][1]=-1;  edgeCenters[5][2]= 1;  edgeNames[5]  = "BottomBack";
   edgeCenters[6][0]= 1;  edgeCenters[6][1]=-1;  edgeCenters[6][2]= 0;  edgeNames[6]  = "BottomRight";
   edgeCenters[7][0]=-1;  edgeCenters[7][1]=-1;  edgeCenters[7][2]= 0;  edgeNames[7]  = "BottomLeft";
   edgeCenters[8][0]= 1;  edgeCenters[8][1]= 0;  edgeCenters[8][2]=-1;  edgeNames[8]  = "FrontRight";
   edgeCenters[9][0]=-1;  edgeCenters[9][1]= 0;  edgeCenters[9][2]=-1;  edgeNames[9]  = "FrontLeft";
   edgeCenters[10][0]= 1; edgeCenters[10][1]= 0; edgeCenters[10][2]= 1; edgeNames[10] = "BackRight";
   edgeCenters[11][0]=-1; edgeCenters[11][1]= 0; edgeCenters[11][2]= 1; edgeNames[11] = "BackLeft";

   //--- Check each edge midpoint against the cursor position (10-pixel radius)
   for(int i = 0; i < 12; i++)
     {
      int sx, sy;
      vcubeProject(edgeCenters[i][0], edgeCenters[i][1], edgeCenters[i][2], angX, angY, sx, sy);
      double dx = localX - sx;
      double dy = localY - sy;
      double d  = MathSqrt(dx * dx + dy * dy);
      if(d < 10 && d < bestDist)
        {
         bestDist = d;
         m_vcubeHoverZone = edgeNames[i];
        }
     }

   //--- Corner position and name lookup tables
   double cornerCenters[8][3];
   string cornerNames[8];
   cornerCenters[0][0]=-1; cornerCenters[0][1]= 1; cornerCenters[0][2]=-1; cornerNames[0]="TopFrontLeft";
   cornerCenters[1][0]= 1; cornerCenters[1][1]= 1; cornerCenters[1][2]=-1; cornerNames[1]="TopFrontRight";
   cornerCenters[2][0]= 1; cornerCenters[2][1]= 1; cornerCenters[2][2]= 1; cornerNames[2]="TopBackRight";
   cornerCenters[3][0]=-1; cornerCenters[3][1]= 1; cornerCenters[3][2]= 1; cornerNames[3]="TopBackLeft";
   cornerCenters[4][0]=-1; cornerCenters[4][1]=-1; cornerCenters[4][2]=-1; cornerNames[4]="BottomFrontLeft";
   cornerCenters[5][0]= 1; cornerCenters[5][1]=-1; cornerCenters[5][2]=-1; cornerNames[5]="BottomFrontRight";
   cornerCenters[6][0]= 1; cornerCenters[6][1]=-1; cornerCenters[6][2]= 1; cornerNames[6]="BottomBackRight";
   cornerCenters[7][0]=-1; cornerCenters[7][1]=-1; cornerCenters[7][2]= 1; cornerNames[7]="BottomBackLeft";

   //--- Check each corner against the cursor position (8-pixel radius)
   for(int i = 0; i < 8; i++)
     {
      int sx, sy;
      vcubeProject(cornerCenters[i][0], cornerCenters[i][1], cornerCenters[i][2], angX, angY, sx, sy);
      double dx = localX - sx;
      double dy = localY - sy;
      double d  = MathSqrt(dx * dx + dy * dy);
      if(d < 8 && d < bestDist)
        {
         bestDist = d;
         m_vcubeHoverZone = cornerNames[i];
        }
     }
  }

//+------------------------------------------------------------------+
//| Snap the camera to the orientation indicated by a cube zone click|
//+------------------------------------------------------------------+
void handleViewCubeClick(int mouseX, int mouseY)
  {
   //--- Abort if no valid zone is hovered
   if(m_vcubeHoverZone == "") return;

   //--- Diagonal tilt angle used for isometric corner/edge views
   double isoTilt = MathArctan(1.0 / MathSqrt(2.0));
   //--- Near-vertical angle used for top/bottom face views
   double nearPole = DX_PI / 2.0 - 0.0001;

   //--- Look up the target camera angles for the clicked zone
   double tX = 0, tY = 0;
   if     (m_vcubeHoverZone == "Front")              { tX = 0.0;        tY = 0.0; }
   else if(m_vcubeHoverZone == "Back")               { tX = 0.0;        tY = DX_PI; }
   else if(m_vcubeHoverZone == "Top")                { tX = nearPole;   tY = 0.0; }
   else if(m_vcubeHoverZone == "Bottom")             { tX = -nearPole;  tY = 0.0; }
   else if(m_vcubeHoverZone == "Right")              { tX = 0.0;        tY =  DX_PI / 2.0; }
   else if(m_vcubeHoverZone == "Left")               { tX = 0.0;        tY = -DX_PI / 2.0; }
   else if(m_vcubeHoverZone == "TopFront")           { tX = isoTilt;    tY = 0.0; }
   else if(m_vcubeHoverZone == "TopBack")            { tX = isoTilt;    tY = DX_PI; }
   else if(m_vcubeHoverZone == "TopRight")           { tX = isoTilt;    tY =  DX_PI / 2.0; }
   else if(m_vcubeHoverZone == "TopLeft")            { tX = isoTilt;    tY = -DX_PI / 2.0; }
   else if(m_vcubeHoverZone == "BottomFront")        { tX = -isoTilt;   tY = 0.0; }
   else if(m_vcubeHoverZone == "BottomBack")         { tX = -isoTilt;   tY = DX_PI; }
   else if(m_vcubeHoverZone == "BottomRight")        { tX = -isoTilt;   tY =  DX_PI / 2.0; }
   else if(m_vcubeHoverZone == "BottomLeft")         { tX = -isoTilt;   tY = -DX_PI / 2.0; }
   else if(m_vcubeHoverZone == "FrontRight")         { tX = 0.0;        tY =  DX_PI / 4.0; }
   else if(m_vcubeHoverZone == "FrontLeft")          { tX = 0.0;        tY = -DX_PI / 4.0; }
   else if(m_vcubeHoverZone == "BackRight")          { tX = 0.0;        tY =  DX_PI * 3.0 / 4.0; }
   else if(m_vcubeHoverZone == "BackLeft")           { tX = 0.0;        tY = -DX_PI * 3.0 / 4.0; }
   else if(m_vcubeHoverZone == "TopFrontRight")      { tX = isoTilt;    tY =  DX_PI / 4.0; }
   else if(m_vcubeHoverZone == "TopFrontLeft")       { tX = isoTilt;    tY = -DX_PI / 4.0; }
   else if(m_vcubeHoverZone == "TopBackRight")       { tX = isoTilt;    tY =  DX_PI * 3.0 / 4.0; }
   else if(m_vcubeHoverZone == "TopBackLeft")        { tX = isoTilt;    tY = -DX_PI * 3.0 / 4.0; }
   else if(m_vcubeHoverZone == "BottomFrontRight")   { tX = -isoTilt;   tY =  DX_PI / 4.0; }
   else if(m_vcubeHoverZone == "BottomFrontLeft")    { tX = -isoTilt;   tY = -DX_PI / 4.0; }
   else if(m_vcubeHoverZone == "BottomBackRight")    { tX = -isoTilt;   tY =  DX_PI * 3.0 / 4.0; }
   else if(m_vcubeHoverZone == "BottomBackLeft")     { tX = -isoTilt;   tY = -DX_PI * 3.0 / 4.0; }
   else return;

   //--- Store the target angles for the animation
   m_targetAngleX    = tX;
   m_targetAngleY    = tY;
   //--- Record the current angles as the animation start
   m_animStartAngleX = m_cameraAngleX;
   m_animStartAngleY = m_cameraAngleY;
   //--- Reset and start the animation
   m_animStep        = 0;
   m_animSteps       = 20;
   m_isAnimating     = true;
   //--- Start the millisecond timer to drive the animation
   EventSetMillisecondTimer(30);
  }

Wir definieren die Funktion detectViewCubeZone, um die jeweilige Unterzone (Fläche, Kante oder Ecke) unter dem Mauszeiger in lokalen Koordinaten zu identifizieren und so eine interaktive Rückmeldung zum ViewCube zu ermöglichen. Wir synchronisieren die Winkel mit der aktuellen Kamera, berechnen den Kamerarichtungsvektor mithilfe von MathCos und MathSin, definieren Arrays für Flächennamen, Normalen und Mittelpunkte als feste Eigenschaften des Würfels und initialisieren bestDist mit einem hohen Wert, während wir m_vcubeHoverZone zurücksetzen. Beim Durchlaufen der Schleife über die Flächen lassen wir die nach hinten gerichteten Flächen anhand des Skalarprodukts mit den Normalen aus, projizieren die Mittelpunkte mit vcubeProject, berechnen den euklidischen Abstand zum Cursor und aktualisieren die Zone, wenn der Abstand weniger als 15 Einheiten beträgt.

Ebenso gilt für 12 Kanten mit vordefinierten Mittelpunkten und Bezeichnungen wie TopFront, die innerhalb von 10 Einheiten überprüft werden, sowie für 8 Ecken wie TopFrontLeft innerhalb von 8 Einheiten, wobei der kleinste Abstand priorisiert wird, um feinkörnige Hover-Bereiche für eine präzise Navigation genau zu erkennen. Diese Zonenerkennung ist für eine benutzerfreundliche Interaktion von entscheidender Bedeutung, da sie den Würfel anhand einfacher Abstandsschwellenwerte ohne Raycasting in anklickbare Bereiche unterteilt und so eine schnelle Identifizierung von Orientierungszielen unter Berücksichtigung der perspektivischen Projektion ermöglicht.

Um auf Klicks zu reagieren, implementieren wir die Funktion handleViewCubeClick, die vorzeitig zurückkehrt, wenn keine Zone vorhanden ist. Wir berechnen die isometrische Neigung vorab mit MathArctan auf 1/sqrt(2) für 35-Grad-Ansichten und einem Wert nahe dem Pol von PI/2 und ordnen dann m_vcubeHoverZone über eine umfangreiche bedingte Kette den Zielwinkeln tX und tY für Standardausrichtungen zu (z. B. Front bei (0,0), Top am Nahepol auf der X-Achse, Kanten bei PI/4-Varianten, Ecken bei isoTilt in Kombination mit Quadranten). Wir setzen m_targetAngleX und m_targetAngleY, erfassen die aktuellen Winkel zu Beginn der Animation, setzen den Schritt auf 0, wobei m_animSteps auf 20 gesetzt ist, aktivieren m_isAnimating, und starten einen 30-ms-Timer mit EventSetMillisecondTimer, um flüssige Übergänge zu gewährleisten. Dieser Klick-Handler ermöglicht ein intuitives Zurücksetzen der Ansicht durch Animationen zu vordefinierten Winkeln und verbessert so die Benutzerfreundlichkeit bei der 3D-Navigation, da keine abrupten Sprünge auftreten. Nachdem der Klick-Handler eingerichtet ist, fügen wir nun den Animations-Tick hinzu und passen den Maus-Ereignis-Handler an, damit die Interaktionen beim Verschieben und Anzeigen des Würfels korrekt weitergeleitet werden.

Animieren von Kameraübergängen und Weiterleiten von Mausereignissen für Verschieben und ViewCube

Der Animations-Tick verschiebt die Kamerawinkel in jedem Zeitintervall mithilfe einer Smooth-Step-Kurve, sodass die Übergänge auf natürliche Weise verlangsamt werden. Der Ereignis-Handler der Maus wird erweitert, um die Hover-Zustände des Pan-Icons und des ViewCubes zu erfassen, je nach m_panMode zwischen Rotation und Pan-Ziehen zu unterscheiden und Linksklick-Ereignisse an den ViewCube-Handler oder die Pan-Umschaltfunktion weiterzuleiten, bevor die bestehende Logik für Ziehen und Größenänderung zum Tragen kommt.

//+------------------------------------------------------------------+
//| Advance the camera snap animation by one timer tick              |
//+------------------------------------------------------------------+
void tickAnimation()
  {
   //--- Abort if no animation is running
   if(!m_isAnimating) return;

   //--- Advance to the next animation step
   m_animStep++;
   //--- Compute normalised interpolation parameter [0, 1]
   double t = (double)m_animStep / (double)m_animSteps;
   //--- Apply smoothstep easing for a natural deceleration curve
   t = t * t * (3.0 - 2.0 * t);

   //--- Interpolate the elevation angle toward the target
   m_cameraAngleX = m_animStartAngleX + (m_targetAngleX - m_animStartAngleX) * t;
   //--- Interpolate the azimuth angle toward the target
   m_cameraAngleY = m_animStartAngleY + (m_targetAngleY - m_animStartAngleY) * t;

   //--- Snap to the exact target on the final step
   if(m_animStep >= m_animSteps)
     {
      m_cameraAngleX = m_targetAngleX;
      m_cameraAngleY = m_targetAngleY;
      m_isAnimating = false;
     }

   //--- Re-render and push the frame to the chart
   renderVisualization();
   ChartRedraw();
  }

//--- Update "handleMouseEvent" with new hover checks, panning logic, and view cube handling


bool previousPanHoverState    = m_isHoveringPanIcon;
bool previousVCubeHoverState  = m_isHoveringViewCube;
string previousVCubeZone      = m_vcubeHoverZone;

//--- Update all hover flags for the current cursor position
m_isHoveringCanvas = (mouseX >= m_currentPositionX &&
                      mouseX <= m_currentPositionX + m_currentWidth &&
                      mouseY >= m_currentPositionY &&
                      mouseY <= m_currentPositionY + m_currentHeight);
m_isHoveringHeader      = isMouseOverHeaderBar(mouseX, mouseY);
m_isHoveringSwitchIcon  = isMouseOverSwitchIcon(mouseX, mouseY);
m_isHoveringPanIcon     = isMouseOverPanIcon(mouseX, mouseY);
m_isHoveringResizeZone  = isMouseInResizeZone(mouseX, mouseY, m_hoverResizeMode);

//--- Update view cube hover only in 3D mode
if(m_currentViewMode == VIEW_3D_MODE)
   m_isHoveringViewCube = isMouseOverViewCube(mouseX, mouseY);
else
   m_isHoveringViewCube = false;

//--- Determine if any hover state change requires a redraw
bool needRedraw = (previousHoverState       != m_isHoveringCanvas      ||
                   previousHeaderHoverState != m_isHoveringHeader      ||
                   previousResizeHoverState != m_isHoveringResizeZone  ||
                   previousSwitchHoverState != m_isHoveringSwitchIcon  ||
                   previousPanHoverState    != m_isHoveringPanIcon     ||
                   previousVCubeHoverState  != m_isHoveringViewCube    ||
                   previousVCubeZone        != m_vcubeHoverZone);

//--- Handle 3D orbit and pan drags when over the canvas body (not header or cube)
if(m_currentViewMode == VIEW_3D_MODE && m_isHoveringCanvas &&
   !m_isHoveringHeader && !m_isHoveringViewCube)
  {
   //--- Orbit mode: rotate the camera around the target
   if(!m_panMode)
     {
      //--- Begin orbit on fresh left-button press
      if(mouseState == 1 && m_previousMouseButtonState == 0)
        {
         m_isRotating3D  = true;
         m_mouse3DStartX = mouseX;
         m_mouse3DStartY = mouseY;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
        }
      //--- Continue orbit while button is held
      else if(mouseState == 1 && m_previousMouseButtonState == 1 && m_isRotating3D)
        {
         //--- Update azimuth proportional to horizontal mouse delta
         m_cameraAngleY += (mouseX - m_mouse3DStartX) / 300.0;
         //--- Update elevation proportional to vertical mouse delta
         m_cameraAngleX += (mouseY - m_mouse3DStartY) / 300.0;
         //--- Clamp elevation to avoid gimbal lock at poles
         if(m_cameraAngleX < -DX_PI * 0.499) m_cameraAngleX = -DX_PI * 0.499;
         if(m_cameraAngleX >  DX_PI * 0.499) m_cameraAngleX =  DX_PI * 0.499;
         //--- Update anchor for next delta computation
         m_mouse3DStartX = mouseX;
         m_mouse3DStartY = mouseY;
         //--- Cancel any running snap animation
         m_isAnimating = false;
         needRedraw = true;
        }
      //--- End orbit on button release
      else if(mouseState == 0 && m_previousMouseButtonState == 1)
        {
         m_isRotating3D = false;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
        }
     }
   else
     {
      //--- Pan mode: translate the camera target in the view plane
      if(mouseState == 1 && m_previousMouseButtonState == 0)
        {
         //--- Begin pan drag
         m_isPanning = true;
         m_panStartX = mouseX;
         m_panStartY = mouseY;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, false);
        }
      else if(mouseState == 1 && m_previousMouseButtonState == 1 && m_isPanning)
        {
         //--- Compute mouse delta since last event
         int deltaX = mouseX - m_panStartX;
         int deltaY = mouseY - m_panStartY;

         //--- Reconstruct the camera position vector in world space
         DXVector4 camera(0.0f, 0.0f, (float)-m_cameraDistance, 1.0f);
         DXMatrix rotX;
         DXMatrixRotationX(rotX, (float)m_cameraAngleX);
         DXVec4Transform(camera, camera, rotX);
         DXMatrix rotY;
         DXMatrixRotationY(rotY, (float)m_cameraAngleY);
         DXVec4Transform(camera, camera, rotY);
         DXVector3 cameraPos(camera.x, camera.y, camera.z);

         //--- Compute and normalise the forward direction toward the target
         DXVector3 forward;
         DXVec3Subtract(forward, m_viewTarget, cameraPos);
         float len = DXVec3Length(forward);
         if(len > 0.0f) DXVec3Scale(forward, forward, 1.0f / len);

         //--- Compute the camera right vector via cross product
         DXVector3 worldUp(0.0f, 1.0f, 0.0f);
         DXVector3 right;
         DXVec3Cross(right, worldUp, forward);
         len = DXVec3Length(right);
         if(len > 0.0f) DXVec3Scale(right, right, 1.0f / len);

         //--- Compute the camera up vector orthogonal to forward and right
         DXVector3 camUp;
         DXVec3Cross(camUp, forward, right);
         len = DXVec3Length(camUp);
         if(len > 0.0f) DXVec3Scale(camUp, camUp, 1.0f / len);

         //--- Scale the pan movement proportional to camera distance
         float panFactor = (float)m_cameraDistance / 500.0f;
         DXVector3 moveRight;
         DXVec3Scale(moveRight, right,  (float)(-deltaX) * panFactor);
         DXVector3 moveUp;
         DXVec3Scale(moveUp,   camUp,   (float)( deltaY) * panFactor);

         //--- Translate the view target by the computed pan offset
         DXVector3 temp;
         DXVec3Add(temp,        m_viewTarget, moveRight);
         DXVec3Add(m_viewTarget, temp,        moveUp);

         //--- Update pan anchor for next delta computation
         m_panStartX = mouseX;
         m_panStartY = mouseY;
         needRedraw = true;
        }
      //--- End pan drag on button release
      else if(mouseState == 0 && m_previousMouseButtonState == 1)
        {
         m_isPanning = false;
         ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
        }
     }
  }

//--- In the mouse down block:

//--- Handle view cube face/edge/corner click in 3D mode
if(m_isHoveringViewCube && m_currentViewMode == VIEW_3D_MODE && m_vcubeHoverZone != "")
  {
   handleViewCubeClick(mouseX, mouseY);
   needRedraw = true;
   m_previousMouseButtonState = mouseState;
   return;
  }
//--- Toggle pan mode when the pan icon is clicked
else if(m_isHoveringPanIcon && m_currentViewMode == VIEW_3D_MODE)
  {
   m_panMode = !m_panMode;
   needRedraw = true;
   m_previousMouseButtonState = mouseState;
   return;
  }

//+------------------------------------------------------------------+
//| Return true when the cursor is over the pan mode toggle icon     |
//+------------------------------------------------------------------+
bool isMouseOverPanIcon(int mouseX, int mouseY)
  {
   //--- Pan icon is only visible in 3D mode
   if(m_currentViewMode != VIEW_3D_MODE) return false;
   //--- Align pan icon with the right edge below the header
   int iconX = m_currentPositionX + m_currentWidth - PAN_ICON_SIZE - PAN_ICON_MARGIN;
   int iconY = m_currentPositionY + HEADER_BAR_HEIGHT + PAN_ICON_MARGIN;
   //--- Return true if the cursor falls within the icon bounding box
   return (mouseX >= iconX && mouseX <= iconX + PAN_ICON_SIZE &&
           mouseY >= iconY && mouseY <= iconY + PAN_ICON_SIZE);
  }

//+------------------------------------------------------------------+
//| Return true when the cursor is over the view cube widget         |
//+------------------------------------------------------------------+
bool isMouseOverViewCube(int mouseX, int mouseY)
  {
   //--- View cube is only visible in 3D mode
   if(m_currentViewMode != VIEW_3D_MODE) return false;
   //--- Compute the view cube bounding area below the pan icon
   int cubeAreaX = m_currentPositionX + m_currentWidth - VCUBE_SIZE - VCUBE_MARGIN;
   int cubeAreaY = m_currentPositionY + HEADER_BAR_HEIGHT + PAN_ICON_MARGIN + PAN_ICON_SIZE + VCUBE_MARGIN;
   int cubeAreaW = VCUBE_SIZE;
   int cubeAreaH = VCUBE_SIZE;

   //--- Check if the cursor is within the bounding box
   if(mouseX >= cubeAreaX && mouseX <= cubeAreaX + cubeAreaW &&
      mouseY >= cubeAreaY && mouseY <= cubeAreaY + cubeAreaH)
     {
      //--- Detect the specific cube face, edge or corner under the cursor
      detectViewCubeZone(mouseX - m_currentPositionX, mouseY - m_currentPositionY);
      return (m_vcubeHoverZone != "");
     }
   //--- Cursor is outside the view cube; clear hover zone
   m_vcubeHoverZone = "";
   return false;
  }

//+------------------------------------------------------------------+
//| Deactivate pan mode and trigger a redraw                        |
//+------------------------------------------------------------------+
void exitPanMode()
  {
   //--- Only act if pan mode is currently active
   if(m_panMode)
     {
      m_panMode = false;
      renderVisualization();
      ChartRedraw();
     }
  }

Um die Subzone, über der sich der Mauszeiger befindet, anhand lokaler Koordinaten auf dem ViewCube zu lokalisieren, definieren wir die Funktion detectViewCubeZone. Wir richten die Winkel auf die aktuelle Kamera aus, berechnen den Richtungsvektor mit MathCos und MathSin und definieren Arrays für Flächennamen, Normalen und Mittelpunkte als Würfelflächen. Ausgehend von der größten Entfernung und einer leeren m_vcubeHoverZone durchlaufen wir die Flächen in einer Schleife, überspringen die nach hinten gerichteten Flächen anhand des Skalarprodukts, projizieren die Mittelpunkte mit vcubeProject, berechnen den Abstand zum Cursor mit MathSqrt und aktualisieren den Wert, wenn die Entfernung weniger als 15 Einheiten beträgt. Wir wiederholen diesen Vorgang für 12 Kanten mit vordefinierten Mittelpunkten und Bezeichnungen wie TopFront, wobei wir einen Bereich von 10 Einheiten prüfen, sowie für 8 Ecken wie TopFrontLeft innerhalb von 8 Einheiten, wobei wir den nächstgelegenen Punkt auswählen, um eine detaillierte Interaktion zu ermöglichen.

In handleMouseEvent fügen wir frühere Zustände für Pan- und ViewCube-Hover-Ereignisse hinzu, einschließlich der Zone, aktualisieren m_isHoveringPanIcon über isMouseOverPanIcon und m_isHoveringViewCube über isMouseOverViewCube (Zurücksetzen, wenn nicht 3D), und beziehen diese in die needRedraw-Prüfungen ein. Bei 3D-Canvas-Hover ohne Header oder Würfel: Wenn m_panMode deaktiviert ist, die Rotationslogik beibehalten; wenn aktiviert, bei Betätigung m_isPanning auf true setzen und das Scrollen deaktivieren; beim Ziehen die Deltas berechnen, den Kameravektor durch Rotationen transformieren, die normierten Vektoren forward und right über DXVec3Cross und DXVec3Length ableiten, camUp analog berechnen, Bewegungen anhand des Pan-Faktors aus der Entfernung skalieren, mit DXVec3Add zu m_viewTarget addieren, Startpunkte aktualisieren und Neuzeichnung markieren; beim Loslassen Pan zurücksetzen und Scrollen aktivieren. Im Druckblock: Wenn der Mauszeiger im 3D-Modus über dem ViewCube mit Zone steht, handleViewCubeClick aufrufen, Neuzeichnung setzen, Status aktualisieren, zurückkehren; wenn der Mauszeiger im 3D-Modus über dem Pan-Icon steht, m_panMode umschalten, Neuzeichnung setzen, Status aktualisieren, zurückkehren.

Wir definieren die Funktion isMouseOverPanIcon, um zu erkennen, ob der Mauszeiger über dem Umschaltelement des Pan-Modus schwebt. Die Funktion gibt false zurück, wenn es sich nicht um 3D handelt; andernfalls berechnet sie die Begrenzungen des Symbols anhand der Position und der Konstanten und prüft, ob sich die Maus innerhalb dieser Begrenzungen befindet. Als Nächstes prüft isMouseOverViewCube, ob der 3D-Modus aktiv ist, oder gibt false zurück, berechnet die Fläche anhand der Position und der Ränder; befindet sich die Maus innerhalb dieses Bereichs, ruft die Funktion detectViewCubeZone mit lokalen Versätzen auf, gibt true zurück, wenn die Zone festgelegt ist, andernfalls setzt sie die Zone auf leer zurück und gibt false zurück. Um das Verschieben zu deaktivieren, definieren wir exitPanMode, das – sofern m_panMode aktiv ist – diesen zurücksetzt, mit renderVisualization neu rendert und das Diagramm neu zeichnet, sodass das Beenden über die Escape-Taste gemäß der Ereignisbehandlung möglich ist. Um die Animationen zu aktivieren, rufen wir die Funktion animations im Timer-Ereignis-Handler auf.

Anschluss des Timers, Deinitialisierung und Ereignisbehandler für Charts

Im letzten Schritt wird der Animations-Tick mit dem Timer-Ereignis-Handler verknüpft, die Timer-Bereinigung zur Deinitialisierung hinzugefügt und der Diagramm-Ereignis-Handler um eine Tastaturverzweigung erweitert, die bei Drücken der Escape-Taste exitPanMode aufruft.

//+------------------------------------------------------------------+
//| Advance the camera snap animation on each timer tick             |
//+------------------------------------------------------------------+
void OnTimer()
  {
   //--- Delegate to the visualizer's per-tick animation updater
   if(distributionVisualizer != NULL)
      distributionVisualizer.tickAnimation();
  }

//--- Add "EventKillTimer()" in "OnDeinit"

//+------------------------------------------------------------------+
//| Release all resources when the EA is removed                     |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   //--- Stop the animation timer
   EventKillTimer();
   //--- Destroy the visualizer object if it still exists
   if(distributionVisualizer != NULL)
     {
      delete distributionVisualizer;
      distributionVisualizer = NULL;
     }
   //--- Redraw the chart to remove the canvas bitmap object
   ChartRedraw();
   Print("Distribution window deinitialized");
  }

//--- Update "OnChartEvent" to handle ESC key for exiting pan mode
//--- We added this for enhanced simplicity


//+------------------------------------------------------------------+
//| Route chart mouse, wheel and key events to the visualizer        |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
  {
   //--- Abort if the visualizer has not been initialised
   if(distributionVisualizer == NULL) return;

   //--- Handle mouse move and button events
   if(id == CHARTEVENT_MOUSE_MOVE)
     {
      int mouseX     = (int)lparam;       // Horizontal cursor position in pixels
      int mouseY     = (int)dparam;       // Vertical cursor position in pixels
      int mouseState = (int)sparam;       // Bitmask of pressed mouse buttons

      distributionVisualizer.handleMouseEvent(mouseX, mouseY, mouseState);
     }

   //--- Handle mouse wheel scroll events
   if(id == CHARTEVENT_MOUSE_WHEEL)
     {
      //--- Unpack cursor X from the low 16 bits of lparam
      int mouseX = (int)(short) lparam;
      //--- Unpack cursor Y from the high 16 bits of lparam
      int mouseY = (int)(short)(lparam >> 16);
      distributionVisualizer.handleMouseWheel(mouseX, mouseY, dparam);
     }

   //--- Handle Escape key press to exit pan mode
   if(id == CHARTEVENT_KEYDOWN && lparam == 27)
      distributionVisualizer.exitPanMode();
  }

Hier definieren wir die Ereignisbehandlung durch OnTimer, um Animationen in regelmäßigen Abständen voranzutreiben. Wir prüfen, ob distributionVisualizer nicht NULL ist, und rufen dann tickAnimation dafür auf, um den Übergang des ViewCubes oder andere zeitgesteuerte Effekte zu aktualisieren. In OnDeinit fügen wir EventKillTimer hinzu, um den Timer anzuhalten und sicherzustellen, dass nach der Deinitialisierung keine Ereignisse mehr bestehen bleiben. Wir erweitern OnChartEvent um die Tastenbehandlung: Wenn id den Wert CHARTEVENT_KEYDOWN hat und lparam den Wert 27 (ESC-Taste) annimmt, wird die Funktion exitPanMode des Visualizers aufgerufen, um den Pan-Modus zu deaktivieren, falls dieser aktiv ist. Damit wird eine Tastenkombination zur Vereinfachung bereitgestellt. Damit ist die Implementierung abgeschlossen. Nun muss das System noch getestet werden; darauf wird im nächsten Abschnitt eingegangen.


Backtests

Wir haben die Tests durchgeführt, und unten sehen Sie die kompilierte Visualisierung in einer einzigen GIF-Animation.

BACKTEST-GIF

Während der Tests stimmte die segmentierte dreidimensionale Kurve über unterschiedliche Versuchsanzahlen hinweg stets mit den Spitzenwerten der Histogramm-Bars überein; im Pan-Modus wurde der Kamerazielpunkt sanft verschoben, ohne die aktuelle Drehung oder den Zoomfaktor zu stören, und durch Klicks auf den ViewCube wurde die Kamera für alle getesteten Flächen-, Kanten- und Eckbereiche sauber in die korrekte Standardausrichtung animiert.


Schlussfolgerung

Zusammenfassend lässt sich sagen, dass wir den dreidimensionalen Viewer für die Binomialverteilung in MQL5 erweitert haben, indem wir eine segmentierte, röhrenförmige Kurve zur räumlich präzise Visualisierung der PMF hinzugefügt, einen Pan-Modus zur kamerarelativen Verschiebung des Kamerazielpunkts integriert und einen interaktiven ViewCube mit Hover-Zonen für Flächen, Kanten und Ecken implementiert haben, die die Kamera in Standardausrichtungen animieren. Die Umsetzung umfasste die Erstellung von Kurvensegmenten auf Basis von Box-Primitive und die Ausrichtung mittels Transformationsmatrizen, die bilineare Unterteilung zur Erkennung von Hover-Bereichen, zeitgesteuerte, sanfte Kameraübergänge sowie eine überarbeitete Ereignisbehandlung für Klicks, Panning und Tastaturkürzel. Nach der Lektüre dieses Artikels können Sie:

  • Verwenden Sie die dreidimensionale Kurve, um visuell zu überprüfen, ob der Peak der Wahrscheinlichkeitsmassenfunktion mit dem Histogramm-Bin der höchsten Frequenz übereinstimmt, und so die Modellanpassung direkt aus der Darstellung heraus zu erkennen, ohne in den zweidimensionalen Modus wechseln zu müssen
  • Aktivieren Sie den Pan-Modus, um den Fokus der Szene auf Bereiche mit geringer Wahrscheinlichkeit oder weit außerhalb der Mitte liegende Versuchsbereiche zu verlagern, ohne dabei Ihren aktuellen Drehwinkel oder Zoomfaktor zu verlieren.
  • Klicken Sie auf eine beliebige Fläche, Kante oder Ecke des ViewCubes, um die Kamera in eine Standardausrichtung zu bewegen – dabei dient die Draufsicht zum gleichzeitigen Vergleich aller Barhöhen und die Vorderansicht zum übersichtlichen Ablesen einzelner Bins.

In den nächsten Teilen werden wir uns mit den statistischen 2D-Verteilungen befassen und weitere Verteilungen grafisch darstellen. Bleiben Sie dran!

Übersetzt aus dem Englischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/en/articles/21335

Letzte Kommentare | Zur Diskussion im Händlerforum (5)
BeeXXI Corporation
Nikolai Semko | 15 Juni 2026 in 11:50
Ich sehe keine 3D-Kurven
Ich sehe 2D-Kurven in einer dreidimensionalen Darstellung
Allan Munene Mutiiria
Allan Munene Mutiiria | 15 Juni 2026 in 13:41
Nikolai Semko #:
Ich kann die 3D-Kurven nicht sehen
Ich kann 2D-Kurven in der 3D-Ansicht sehen
Vielen Dank für das freundliche Feedback.

Derzeit positionieren wir die 3D-Kurvensegmente erst einen Schritt nach dem Rendern der Szene, sodass sie im ersten statischen Bild noch nicht an der richtigen Stelle sind. Führen Sie einfach einmal eine Drehbewegung (Linksklick und Ziehen) oder eine Zoom-Geste (Scrollrad) über die Arbeitsfläche aus – dadurch wird ein erneutes Rendern erzwungen und die Kurve wird angezeigt.

Wenn du möchtest, dass die Kurve sofort gerendert wird, sobald du in die 3D-Ansicht wechselst, verschiebe einfach die Funktion zum Rendern der Kurve vor den Render-Aufruf. Siehe unten:

Vorher:

   //+------------------------------------------------------------------+
   //| Die 3D-Szene rendern und 2D-UI-Elemente darüberlegen            |
   //+------------------------------------------------------------------+
   void render3DVisualization()
     {
      //--- Abbruch, falls die Daten nicht geladen wurden
      if(!m_isDataLoaded) return;

      //--- Die Ansichts- und Lichttransformationen für dieses Bild neu berechnen
      updateCameraPosition();
      //--- Alle 3D-Histogrammbalken entsprechend den aktuellen Daten neu positionieren
      update3DHistogramBars();

      //--- Die konfigurierte Hintergrundfarbe für den „Clear“-Durchlauf verwenden
      color bgColor     = backgroundTopColor;
      uint  bgColorArgb = ColorToARGB(bgColor, 255);
      //--- Farb- und Tiefenpuffer löschen, anschließend die 3D-Szene rendern
      m_mainCanvas.Render(DX_CLEAR_COLOR | DX_CLEAR_DEPTH, bgColorArgb);

      //--- Den 2D-Rahmen über das 3D-Rendering legen
      if(showBorderFrame)
         draw3DBorder();
      //--- Die Kopfzeile über das gerenderte 3D-Bild legen
      drawHeaderBarOn3D();
      //--- Das Statistikfenster und die Legende einblenden, sofern aktiviert
      if(showStatistics)
        {
         drawStatisticsPanelOn3D();
         drawLegendOn3D();
        }
      //--- Alle PMF-Kurvenrohrsegmente für diesen Frame neu positionieren
      update3DCurveSegments(); // THIS IS AFTER THE SCENE RENDERS, SO THE 3D SEGMENTS DELAY RENDERING UNTIL INTERACTION. SEE THE CANVAS RENDER ABOVE IN YELLOW
      //--- Das Symbol für den Umschaltknopf des Schwenkmodus zeichnen
      drawPanIconOverlay();
      //--- Das interaktive View-Cube-Widget zeichnen
      drawViewCubeOverlay();
      //--- Die Anzeige für den Größenänderungsgriff beim Überfahren mit der Maus zeichnen
      if(m_isHoveringResizeZone && enableResizing)
         drawResizeIndicatorOn3D();
      //--- Den Pixelpuffer an das Diagrammobjekt übergeben
      m_mainCanvas.Update();
     }

Nachher:

   //+------------------------------------------------------------------+
   //| Die 3D-Szene rendern und 2D-UI-Elemente darüberlegen            |
   //+------------------------------------------------------------------+
   void render3DVisualization()
     {
      //--- Abbruch, falls die Daten nicht geladen wurden
      if(!m_isDataLoaded) return;

      //--- Die Ansichts- und Lichttransformationen für dieses Bild neu berechnen
      updateCameraPosition();
      //--- Alle 3D-Histogrammbalken entsprechend den aktuellen Daten neu positionieren
      update3DHistogramBars();
      //--- Alle PMF-Kurvenrohrsegmente VOR dem Rendern neu positionieren
      update3DCurveSegments(); // JUST MOVE THE FUNCTION BEFORE THE 3D RENDER FOR INSTANT RENDERING, TO BE COUPLED WITH THE 3D BARS

      //--- Die konfigurierte Hintergrundfarbe für den „Clear“-Durchlauf verwenden
      color bgColor     = backgroundTopColor;
      uint  bgColorArgb = ColorToARGB(bgColor, 255);
      //--- Farb- und Tiefenpuffer löschen, anschließend die 3D-Szene rendern
      m_mainCanvas.Render(DX_CLEAR_COLOR | DX_CLEAR_DEPTH, bgColorArgb);

      //--- Den 2D-Rahmen über das 3D-Rendering legen
      if(showBorderFrame)
         draw3DBorder();
      //--- Die Kopfzeile über das gerenderte 3D-Bild legen
      drawHeaderBarOn3D();
      //--- Das Statistikfenster und die Legende einblenden, sofern aktiviert
      if(showStatistics)
        {
         drawStatisticsPanelOn3D();
         drawLegendOn3D();
        }
      //--- Das Symbol für den Umschaltknopf des Schwenkmodus zeichnen
      drawPanIconOverlay();
      //--- Das interaktive View-Cube-Widget zeichnen
      drawViewCubeOverlay();
      //--- Die Anzeige für den Größenänderungsgriff beim Überfahren mit der Maus zeichnen
      if(m_isHoveringResizeZone && enableResizing)
         drawResizeIndicatorOn3D();
      //--- Den Pixelpuffer an das Diagrammobjekt übergeben
      m_mainCanvas.Update();
     }

Danke, dass du das bemerkt hast. Ich hoffe, das hilft weiter. Danke.

Dmitriy Skub
Dmitriy Skub | 15 Juni 2026 in 14:20
Nikolai Semko #:
Ich sehe keine 3D-Kurven
Ich sehe 2D-Kurven in der dreidimensionalen Darstellung
Da stimme ich zu.
BeeXXI Corporation
Nikolai Semko | 15 Juni 2026 in 14:52
Allan Munene Mutiiria #:
Vielen Dank für Ihre Rückmeldung.

Derzeit werden die Segmente der 3D-Kurve einen Schritt später platziert, als die Szene gerendert wird, weshalb sie im ersten statischen Bild noch nicht zu sehen sind. Führen Sie einfach einmal eine Drehung (Ziehen mit der linken Maustaste) oder eine Skalierung (mit dem Scrollrad) über der Leinwand durch – dies löst ein erneutes Rendern aus, und die Kurve wird angezeigt.

Wenn Sie möchten, dass die Kurve beim Wechsel in den 3D-Modus sofort gerendert wird, verschieben Sie die Funktion zum Rendern der Kurve einfach vor den Renderaufruf. Siehe unten:

Vorher:

Nachher:

Vielen Dank für den Hinweis. Ich hoffe, das hilft weiter. Vielen Dank.

Sie haben mich nicht verstanden.
Genauer gesagt, haben Sie mich falsch verstanden.
Sie haben keine 3D-Kurven, sondern nur 2D-Kurven. Es ist wichtig, die korrekte Terminologie zu verwenden, insbesondere in Artikeln.

Hier ist ein Beispiel für eine 3D-Kurve. Wenn sich eine Kurve im dreidimensionalen Raum befindet, ergibt die Projektion einer solchen Kurve auf eine beliebige Ebene niemals eine gerade Linie, wie in Ihrem Fall.

Allan Munene Mutiiria
Allan Munene Mutiiria | 15 Juni 2026 in 15:17
Nikolai Semko #:
Du hast mich nicht verstanden.
Oder besser gesagt: Du hast mich nicht richtig verstanden.
Du hast keine 3D-Kurven, sondern nur 2D-Kurven. Es ist wichtig, die korrekte Terminologie zu verwenden, insbesondere in Artikeln.

Hier ist ein Beispiel für eine 3D-Kurve. Befindet sich eine Kurve im dreidimensionalen Raum, ergibt die Projektion einer solchen Kurve auf eine beliebige Ebene niemals eine Gerade, wie in deinem Fall

Jetzt verstehe ich, was du meinst. Einverstanden; du hast Recht, was die Terminologie angeht. Streng genommen handelt es sich um eine ebene Kurve (festes Z; nur die Anzahl der Erfolge und die Wahrscheinlichkeit variieren), es ist also keine 3D-Kurve im eigentlichen Sinne, und ich verstehe deinen Einwand.

Die Absicht war, die PMF als reales Objekt in der 3D-Szene darzustellen; drehbar, mit Röhrengeometrie, neben den 3D-Histogrammbalken platziert, sodass sie beim Rendern mit diesen ausgerichtet bleibt und man beide gemeinsam im selben Raum verfolgen kann. „3D“ war also im Sinne einer Darstellung in der 3D-Ansicht gemeint und nicht als echte Raumkurve. Danke für den Hinweis.

Larry Williams’ Marktgeheimnisse (Teil 15): Handel mit Hidden-Smash Day-Umkehrungen unter Berücksichtigung des Marktkontexts Larry Williams’ Marktgeheimnisse (Teil 15): Handel mit Hidden-Smash Day-Umkehrungen unter Berücksichtigung des Marktkontexts
Entwickeln Sie einen MQL5-Expert Advisor, der die Umkehrmuster nach Larry Williams’ Hidden-Smash-Day automatisiert. Er liest bestätigte Signale eines benutzerdefinierten Indikators aus, wendet Kontextfilter an (Supertrend-Ausrichtung und optionale Handelstag-Regeln) und steuert das Risiko mithilfe von Stop-Loss-Modellen, die auf der Smash-Bar-Struktur oder dem ATR sowie einer festen oder risikobasierten Positionsgröße basieren. Das Ergebnis ist ein reproduzierbares Framework, das für Tests und Erweiterungen bereit ist.
PD-Arrays meistern: Optimierung des Handels mit Ungleichgewichten in PD-Arrays PD-Arrays meistern: Optimierung des Handels mit Ungleichgewichten in PD-Arrays
Dieser Artikel befasst sich mit einem spezialisierten, trendfolgenden EA, der deutlich machen soll, wie sich Setups, die sich aus Ungleichgewichten in Premium-/Discount-Bereichen (PD-Arrays) ergeben, definieren und nutzen lassen. In diesem Artikel wird ein EA ausführlich vorgestellt, der speziell für Trader entwickelt wurde, die PD-Arrays und Ungleichgewichte als Einstiegskriterien für ihre Trades und Handelsentscheidungen optimieren und nutzen möchten. Außerdem wird untersucht, wie sich Premium- und Discount-Zonen korrekt ermitteln und charakterisieren lassen und wie man sie jeweils validieren und nutzen kann, wenn sie unter den entsprechenden Marktbedingungen auftreten, um so die sich aus solchen Szenarien ergebenden Chancen bestmöglich zu nutzen.
Entwicklung eines dynamischen Multi-Pair-EA (Teil 7): Paarübergreifende Korrelationsabbildung für die Handelsfilterung in Echtzeit Entwicklung eines dynamischen Multi-Pair-EA (Teil 7): Paarübergreifende Korrelationsabbildung für die Handelsfilterung in Echtzeit
In diesem Abschnitt werden wir eine Echtzeit-Korrelationsmatrix in einen Expert Advisor für mehrere Symbole integrieren, um redundante oder risikobehaftete Trades zu vermeiden. Durch die dynamische Messung der Wechselbeziehungen zwischen Symbolen filtert der EA Einstiegssignale heraus, die im Widerspruch zur bestehenden Exposition stehen, wodurch die Ausgewogenheit des Portfolios verbessert, das systemische Risiko verringert und die Qualität der Trades insgesamt verbessert wird.
Entwicklung eines Toolkits zur Price-Action-Analyse (Teil 63): Automatisierung der Erkennung von steigenden und fallenden Keilen in MQL5 Entwicklung eines Toolkits zur Price-Action-Analyse (Teil 63): Automatisierung der Erkennung von steigenden und fallenden Keilen in MQL5
In diesem Teil der Reihe Entwicklung des Price-Action-Analyse-Toolkits entwickeln wir einen MQL5-Indikator, der steigende und fallende Keilformationen automatisch in Echtzeit erkennt. Das System überprüft Pivot-Strukturen, validiert die Konvergenz der Begrenzungslinien mathematisch, verhindert überlappende Formationen und überwacht die Bedingungen für Ausbruch und Scheitern mit präziser visueller Rückmeldung. Diese auf einer klaren, objektorientierten Architektur basierende Implementierung wandelt die subjektive Erkennung von Keilformationen in eine strukturierte, zustandsabhängige Analysekomponente um, die darauf ausgelegt ist, eine disziplinierte Price-Action-Analyse zu unterstützen.