5155 lines
160 KiB
C#
5155 lines
160 KiB
C#
/*****************************************************************************
|
|
|
|
This class has been written by Elmü (elmue@gmx.de)
|
|
|
|
Check if you have the latest version on:
|
|
https://www.codeproject.com/Articles/5293980/Graph3D-A-Windows-Forms-Render-Control-in-Csharp
|
|
|
|
=======================================
|
|
|
|
IMPORTANT:
|
|
This class has been written by a software developer with 45 years programming exprience.
|
|
This class is optimized for the highest possible speed in every line of it's code.
|
|
If you found a fork of this code on Github or elsewhere you do NOT have the original high quality code!
|
|
This code with 5000 lines is extremely complex and there is a very high risk that a beginner has
|
|
broken this code by modifying it without properly understanding it.
|
|
|
|
=======================================
|
|
|
|
NAMING CONVENTIONS which allow to see the type of a variable immediately without having to jump to the variable definition:
|
|
|
|
cName for class definitions
|
|
tName for type definitions
|
|
eName for enum definitions
|
|
kName for "konstruct" (struct) definitions (letter 's' already used for string)
|
|
delName for delegate definitions
|
|
|
|
b_Name for bool
|
|
c_Name for Char, also Color
|
|
d_Name for double
|
|
e_Name for enum variables
|
|
f_Name for function delegates, also float
|
|
i_Name for instances of classes
|
|
k_Name for "konstructs" (struct) (letter 's' already used for string)
|
|
r_Name for Rectangle
|
|
s_Name for strings
|
|
o_Name for objects
|
|
|
|
s8_Name for signed 8 Bit (sbyte)
|
|
s16_Name for signed 16 Bit (short)
|
|
s32_Name for signed 32 Bit (int)
|
|
s64_Name for signed 64 Bit (long)
|
|
u8_Name for unsigned 8 Bit (byte)
|
|
u16_Name for unsigned 16 bit (ushort)
|
|
u32_Name for unsigned 32 Bit (uint)
|
|
u64_Name for unsigned 64 Bit (ulong)
|
|
|
|
An additional "m" is prefixed for all member variables (e.g. ms_String)
|
|
|
|
*****************************************************************************/
|
|
|
|
// Print render speed to Trace output
|
|
// ATTENTION: The times in the very first trace output are always wrong because of JIT compilation delays.
|
|
#if DEBUG
|
|
// #define DEBUG_SPEED
|
|
#endif
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.ComponentModel;
|
|
using System.Diagnostics;
|
|
using System.Drawing;
|
|
using System.Drawing.Drawing2D;
|
|
using System.Globalization;
|
|
using System.Text;
|
|
using System.Windows.Forms;
|
|
|
|
namespace Plot3D
|
|
{
|
|
/// <summary>
|
|
/// ATTENTION: This class is not thread safe. Call all functions only from the GUI thread or use Control.Invoke()
|
|
/// </summary>
|
|
public class Editor3D : UserControl
|
|
{
|
|
#region enums
|
|
|
|
public enum eColorScheme
|
|
{
|
|
Autumn = 0,
|
|
Cool,
|
|
Copper,
|
|
Hot,
|
|
Hsv,
|
|
Monochrome,
|
|
Pink,
|
|
Rainbow_Sweep, // This creates a 100% cyclic rainbow with 1536 colors. The end color is the same as the start color.
|
|
Rainbow_Bright, // This creates a rainbow without magenta with 1024 colors. It goes from blue to red.
|
|
Rainbow_Dark, // This is similar to RainbowBright, but darker and only with 64 colors.
|
|
Spring,
|
|
Summer,
|
|
Winter,
|
|
Green
|
|
}
|
|
|
|
public enum eRaster
|
|
{
|
|
Off, // turn off coordinate system
|
|
MainAxes, // draw only solid main axes for X,Y,Z
|
|
Raster, // draw additional thin raster lines
|
|
Labels, // draw additional labels in quadrant 3
|
|
}
|
|
|
|
/// <summary>
|
|
/// If a function has an asymetric range for X and Y as demo "Callback" a separate
|
|
/// normalization would always lead to a square X,Y pane which would be a distortion for the
|
|
/// relation between X and Y values. MaintainXY guarantees that the relation between X and Y
|
|
/// values is maintained. MaintainXYZ additionally guarantees that the relation between X, Y
|
|
/// and Z values is maintained.
|
|
/// </summary>
|
|
public enum eNormalize
|
|
{
|
|
Separate, // Normalize X,Y,Z separately (use this for discrete values)
|
|
MaintainXY, // Normalize X,Y without changing their relation (use this for math functions)
|
|
MaintainXYZ, // Normalize X,Y,Z without changing their relation (use this for math functions)
|
|
}
|
|
|
|
public enum eScatterShape
|
|
{
|
|
// 0 is invalid
|
|
Circle = 1,
|
|
|
|
Square = 2,
|
|
Triangle = 3,
|
|
// Star = 4, you can implement your own shapes here
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used internally for coordinate system
|
|
/// </summary>
|
|
public enum eCoord
|
|
{
|
|
// for axis in coordinate system
|
|
X = 0,
|
|
|
|
Y = 1,
|
|
Z = 2,
|
|
Invalid,
|
|
}
|
|
|
|
/// <summary>
|
|
/// These flags define which axes are allowed to be mirrored. For user objects option "All"
|
|
/// is used. Coordinate system axes and raster lines are mirrored individually.
|
|
/// </summary>
|
|
[FlagsAttribute]
|
|
public enum eMirror
|
|
{
|
|
None = 0,
|
|
X = 1,
|
|
Y = 2,
|
|
Z = 4,
|
|
XY = X | Y,
|
|
XZ = X | Z,
|
|
YZ = Y | Z,
|
|
All = X | Y | Z,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mouse operations
|
|
/// </summary>
|
|
public enum eMouseAction
|
|
{
|
|
None = 0,
|
|
Move, // Move the coordinate system with the mouse
|
|
Theta, // Elevate the coordinate system with the mouse
|
|
Phi, // Rotate the coordinate system with the mouse
|
|
ThetaAndPhi, // Elevate + Rotate the coordinate system with the mouse
|
|
Rho, // Zoom in / out
|
|
SelectObj, // Select a 3D object or call the selection callback function if defined
|
|
Callback, // Call the selection callback function if defined
|
|
}
|
|
|
|
public enum eSelEvent
|
|
{
|
|
MouseDown, // Inform selection callback function that the pre-defined mouse button goes down
|
|
MouseDrag, // Inform selection callback function that the mouse is moved while the pre-defined mouse button is down
|
|
MouseUp, // Inform selection callback function that the pre-defined mouse button goes up
|
|
}
|
|
|
|
public enum ePolygonMode
|
|
{
|
|
Fill, // Fill polygons with Brush
|
|
Lines, // Draw only polygon border lines
|
|
}
|
|
|
|
/// <summary>
|
|
/// This enum defines of which type is a cObject3D
|
|
/// </summary>
|
|
public enum eObjType
|
|
{
|
|
Point,
|
|
Line,
|
|
Shape,
|
|
Polygon,
|
|
}
|
|
|
|
/// <summary>
|
|
/// These flags are used to filter the 3D objects that are selected.
|
|
/// </summary>
|
|
[FlagsAttribute]
|
|
public enum eSelType
|
|
{
|
|
Line = 0x1,
|
|
Shape = 0x2,
|
|
Polygon = 0x4,
|
|
All = 0x7,
|
|
}
|
|
|
|
[FlagsAttribute]
|
|
public enum eTooltip
|
|
{
|
|
Off = 0x0, // Tooltip is disabled
|
|
UserText = 0x1, // Show user defined tooltip text that has been set in cPoint3D.Tooltip
|
|
Coord = 0x2, // Show coordinates X,Y,Z of cPoint3D
|
|
All = 0x3, // Show all
|
|
}
|
|
|
|
// This enum is to get the maximum speed out of your CPU. Re-calculation is done only if required.
|
|
[FlagsAttribute]
|
|
private enum eRecalculate
|
|
{
|
|
Nothing = 0x0, // repaint objects after changed selection --> recalculate nothing
|
|
Objects = 0x1, // Projection, Brush, LineWidth,... has changed --> recalculate 3D objects
|
|
CoordSystem = 0x2, // The coordinate system must be recalculated --> recalculate Min/Max and Coord System
|
|
AddRemove = 0x4, // Draw Objects have been added or removed --> refresh lists and recalculate all
|
|
}
|
|
|
|
/// <summary>
|
|
/// These are the possible return values of the Selection callback. See description of
|
|
/// SelectionCallback() at the end of this class.
|
|
/// </summary>
|
|
public enum eInvalidate
|
|
{
|
|
NoChange, // The callback has not modified anything --> do nothing.
|
|
Invalidate, // Calls Invalidate() to redraw what is required depending on the flags in me_Recalculate.
|
|
CoordSystem, // The coordinate system will be recalculated, then Invalidate() is called.
|
|
// Use this option after moving a 3D object with the mouse.
|
|
}
|
|
|
|
/// <summary>
|
|
/// This defines with which mouse button the user controls rotation and elevation
|
|
/// </summary>
|
|
public enum eMouseCtrl
|
|
{
|
|
L_Theta_R_Phi, // Left mouse button vertical: Theta, Right mouse button horizontal: Phi
|
|
L_Theta_L_Phi, // Left mouse button vertical: Theta, Left mouse button horizontal: Phi
|
|
M_Theta_M_Phi, // Middle mouse button vertical: Theta, Middle mouse button horizontal: Phi
|
|
}
|
|
|
|
/// <summary>
|
|
/// 3D object properties that the user may change are stored in the Undo Buffer
|
|
/// </summary>
|
|
public enum eUndoProp
|
|
{
|
|
// cObject3D
|
|
Tag,
|
|
|
|
Selected,
|
|
CanSelect,
|
|
|
|
// cPoint3D
|
|
X,
|
|
|
|
Y,
|
|
Z,
|
|
Tooltip,
|
|
|
|
// cLine3D
|
|
Pen,
|
|
|
|
Width,
|
|
|
|
// cShape3D
|
|
Shape,
|
|
|
|
Radius,
|
|
|
|
// cShape3D + cPolygon3D
|
|
Brush,
|
|
}
|
|
|
|
public enum eLegendPos
|
|
{
|
|
BottomLeft, // The main axis legends are drawn in the bottom left corner of the graph pane. (recommended)
|
|
AxisEnd, // The main axis legends are drawn at the end of the axis and rotate with the graph. (not recommended)
|
|
}
|
|
|
|
#endregion enums
|
|
|
|
// ================== PUBLIC =================
|
|
|
|
#region cObject3D
|
|
|
|
/// <summary>
|
|
/// Base class for cPoint3D, cShape3D, cLine3D, cPloygon3D
|
|
/// </summary>
|
|
public abstract class cObject3D
|
|
{
|
|
public Editor3D mi_Inst;
|
|
protected Object mo_Tag;
|
|
protected bool mb_Selected;
|
|
protected bool mb_CanSelect = true;
|
|
protected cPoint3D[] mi_Points; // This instance must never be replaced by a new array!
|
|
protected int ms32_Col = -1;
|
|
protected int ms32_Row = -1;
|
|
|
|
public virtual void Restore(eUndoProp e_Property, Object o_Value)
|
|
{
|
|
switch (e_Property)
|
|
{
|
|
case eUndoProp.Tag: mo_Tag = o_Value; break;
|
|
case eUndoProp.Selected: mb_Selected = (bool)o_Value; break;
|
|
case eUndoProp.CanSelect: mb_CanSelect = (bool)o_Value; break;
|
|
default: throw new ArgumentException();
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Gets the type of this cObject3D
|
|
/// </summary>
|
|
public virtual eObjType ObjType
|
|
{
|
|
get { throw new NotImplementedException(); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// 1 point for cPoint3D 1 point for cShape3D 2 points for cLine3D n points for cPolygon3D
|
|
/// </summary>
|
|
public cPoint3D[] Points
|
|
{
|
|
get { return mi_Points; }
|
|
// set must NOT be allowed here !!!!
|
|
}
|
|
|
|
/// <summary>
|
|
/// The column in the grid. This is only valid for Polygons and Scatter circles created
|
|
/// by cSurfaceData, otherwise -1
|
|
/// </summary>
|
|
public int Column
|
|
{
|
|
get { return ms32_Col; } // the grid column cannot be changed
|
|
}
|
|
|
|
/// <summary>
|
|
/// The row in the grid. This is only valid for Polygons and Scatter circles created by
|
|
/// cSurfaceData, otherwise -1
|
|
/// </summary>
|
|
public int Row
|
|
{
|
|
get { return ms32_Row; } // the grid row cannot be changed
|
|
}
|
|
|
|
/// <summary>
|
|
/// Here you can store your private data which is passed in Selection.Callback to your code.
|
|
/// </summary>
|
|
public Object Tag
|
|
{
|
|
get { return mo_Tag; }
|
|
set
|
|
{
|
|
if (mo_Tag == value)
|
|
return;
|
|
|
|
if (mi_Inst != null)
|
|
mi_Inst.mi_UndoBuffer.Backup(this, eUndoProp.Tag, mo_Tag, value);
|
|
|
|
mo_Tag = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The draw object has been selected by ALT + click
|
|
/// </summary>
|
|
public virtual bool Selected
|
|
{
|
|
get { return mb_Selected; }
|
|
set
|
|
{
|
|
if (mb_Selected == value)
|
|
return;
|
|
|
|
if (mi_Inst != null)
|
|
mi_Inst.mi_UndoBuffer.Backup(this, eUndoProp.Selected, mb_Selected, value);
|
|
|
|
mb_Selected = value;
|
|
// me_Recalculate needs no change here
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Defines if the user is allowed to select this object
|
|
/// </summary>
|
|
public virtual bool CanSelect
|
|
{
|
|
get { return mb_CanSelect; }
|
|
set
|
|
{
|
|
if (mb_CanSelect == value)
|
|
return;
|
|
|
|
if (mi_Inst != null)
|
|
mi_Inst.mi_UndoBuffer.Backup(this, eUndoProp.CanSelect, mb_CanSelect, value);
|
|
|
|
mb_CanSelect = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Move the object in the 3D space
|
|
/// </summary>
|
|
public void Move(double d_DeltaX, double d_DeltaY, double d_DeltaZ)
|
|
{
|
|
foreach (cPoint3D i_Point in mi_Points)
|
|
{
|
|
i_Point.X += d_DeltaX;
|
|
i_Point.Y += d_DeltaY;
|
|
i_Point.Z += d_DeltaZ;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion cObject3D
|
|
|
|
#region cPoint3D
|
|
|
|
public class cPoint3D : cObject3D
|
|
{
|
|
private double md_X;
|
|
private double md_Y;
|
|
private double md_Z;
|
|
private String ms_Tooltip;
|
|
|
|
public override void Restore(eUndoProp e_Property, Object o_Value)
|
|
{
|
|
switch (e_Property)
|
|
{
|
|
case eUndoProp.X: md_X = (double)o_Value; break;
|
|
case eUndoProp.Y: md_Y = (double)o_Value; break;
|
|
case eUndoProp.Z: md_Z = (double)o_Value; break;
|
|
case eUndoProp.Tooltip: ms_Tooltip = (String)o_Value; break;
|
|
default: base.Restore(e_Property, o_Value); break;
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Gets the type of this cObject3D
|
|
/// </summary>
|
|
public override eObjType ObjType
|
|
{
|
|
get { return eObjType.Point; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// 3D coordinate
|
|
/// </summary>
|
|
public double X
|
|
{
|
|
get { return md_X; }
|
|
set
|
|
{
|
|
if (md_X == value)
|
|
return;
|
|
|
|
if (mi_Inst != null)
|
|
{
|
|
mi_Inst.mi_UndoBuffer.Backup(this, eUndoProp.X, md_X, value);
|
|
mi_Inst.me_Recalculate |= eRecalculate.Objects;
|
|
|
|
if (value < mi_Inst.mi_Bounds.X.Min || value > mi_Inst.mi_Bounds.X.Max)
|
|
mi_Inst.me_Recalculate |= eRecalculate.CoordSystem;
|
|
}
|
|
md_X = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 3D coordinate
|
|
/// </summary>
|
|
public double Y
|
|
{
|
|
get { return md_Y; }
|
|
set
|
|
{
|
|
if (md_Y == value)
|
|
return;
|
|
|
|
if (mi_Inst != null)
|
|
{
|
|
mi_Inst.mi_UndoBuffer.Backup(this, eUndoProp.Y, md_Y, value);
|
|
mi_Inst.me_Recalculate |= eRecalculate.Objects;
|
|
|
|
if (value < mi_Inst.mi_Bounds.Y.Min || value > mi_Inst.mi_Bounds.Y.Max)
|
|
mi_Inst.me_Recalculate |= eRecalculate.CoordSystem;
|
|
}
|
|
md_Y = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 3D coordinate
|
|
/// </summary>
|
|
public double Z
|
|
{
|
|
get { return md_Z; }
|
|
set
|
|
{
|
|
if (md_Z == value)
|
|
return;
|
|
|
|
if (mi_Inst != null)
|
|
{
|
|
mi_Inst.mi_UndoBuffer.Backup(this, eUndoProp.Z, md_Z, value);
|
|
mi_Inst.me_Recalculate |= eRecalculate.Objects;
|
|
|
|
if (value < mi_Inst.mi_Bounds.Z.Min || value > mi_Inst.mi_Bounds.Z.Max)
|
|
mi_Inst.me_Recalculate |= eRecalculate.CoordSystem;
|
|
}
|
|
md_Z = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Optional tooltip text to be displayed when the mouse is over this point
|
|
/// </summary>
|
|
public String Tooltip
|
|
{
|
|
get { return ms_Tooltip; }
|
|
set
|
|
{
|
|
if (ms_Tooltip == value)
|
|
return;
|
|
|
|
if (mi_Inst != null)
|
|
mi_Inst.mi_UndoBuffer.Backup(this, eUndoProp.Tooltip, ms_Tooltip, value);
|
|
|
|
ms_Tooltip = value;
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Constructor s_ToolTip is displayed when eTooltip.UserText is enabled In o_Tag you
|
|
/// can store any data that you need when the Selection callback is called after the
|
|
/// user has selected a point.
|
|
/// </summary>
|
|
public cPoint3D(double d_X, double d_Y, double d_Z, String s_ToolTip = null, Object o_Tag = null)
|
|
{
|
|
md_X = d_X;
|
|
md_Y = d_Y;
|
|
md_Z = d_Z;
|
|
mo_Tag = o_Tag;
|
|
mi_Points = new cPoint3D[] { this };
|
|
|
|
if (s_ToolTip != null)
|
|
ms_Tooltip = s_ToolTip.Trim();
|
|
}
|
|
|
|
// =================== used for coordinate system ===================
|
|
|
|
public cPoint3D Clone()
|
|
{
|
|
return new cPoint3D(md_X, md_Y, md_Z, ms_Tooltip, Tag);
|
|
}
|
|
|
|
public bool CoordEquals(cPoint3D i_Point)
|
|
{
|
|
return md_X == i_Point.md_X && md_Y == i_Point.md_Y && md_Z == i_Point.md_Z;
|
|
}
|
|
|
|
public double GetValue(eCoord e_Coord)
|
|
{
|
|
switch (e_Coord)
|
|
{
|
|
case eCoord.X: return md_X;
|
|
case eCoord.Y: return md_Y;
|
|
case eCoord.Z: return md_Z;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
public void SetValue(eCoord e_Coord, double d_Value)
|
|
{
|
|
switch (e_Coord)
|
|
{
|
|
case eCoord.X: X = d_Value; break;
|
|
case eCoord.Y: Y = d_Value; break;
|
|
case eCoord.Z: Z = d_Value; break;
|
|
}
|
|
}
|
|
|
|
// For debugging in Visual Studio
|
|
public override string ToString()
|
|
{
|
|
return String.Format("cPoint3D (X={0}, Y={1}, Z={2})", FormatDouble(md_X), FormatDouble(md_Y), FormatDouble(md_Z));
|
|
}
|
|
}
|
|
|
|
#endregion cPoint3D
|
|
|
|
#region cLine3D
|
|
|
|
public class cLine3D : cObject3D
|
|
{
|
|
private Pen mi_Pen;
|
|
private int ms32_Width;
|
|
private int ms32_Parts;
|
|
|
|
public override void Restore(eUndoProp e_Property, Object o_Value)
|
|
{
|
|
switch (e_Property)
|
|
{
|
|
case eUndoProp.Pen: mi_Pen = (Pen)o_Value; break;
|
|
case eUndoProp.Width: ms32_Width = (int)o_Value; break;
|
|
default: base.Restore(e_Property, o_Value); break;
|
|
// ms32_Parts is constant
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Gets the type of this cObject3D
|
|
/// </summary>
|
|
public override eObjType ObjType
|
|
{
|
|
get { return eObjType.Line; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The line width in pixels
|
|
/// </summary>
|
|
public int Width
|
|
{
|
|
get { return ms32_Width; }
|
|
set
|
|
{
|
|
if (ms32_Width == value)
|
|
return;
|
|
|
|
if (mi_Inst != null)
|
|
{
|
|
mi_Inst.mi_UndoBuffer.Backup(this, eUndoProp.Width, ms32_Width, value);
|
|
mi_Inst.me_Recalculate |= eRecalculate.Objects;
|
|
}
|
|
ms32_Width = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// If Pen is null, a Pen from the ColorScheme will be used. The width of the Pen does
|
|
/// not matter. It will be set to the property Width. IMPORTANT: If you use the
|
|
/// UndoBuffer you must never modify any property of the Pen assigned here. If you
|
|
/// externally change for example Pen.Color the Undo will not work correctly. Always
|
|
/// assign a different instance of Pen to avoid this.
|
|
/// </summary>
|
|
public Pen Pen
|
|
{
|
|
get { return mi_Pen; }
|
|
set
|
|
{
|
|
if (mi_Pen == value)
|
|
return;
|
|
|
|
if (mi_Inst != null)
|
|
{
|
|
mi_Inst.mi_UndoBuffer.Backup(this, eUndoProp.Pen, mi_Pen, value);
|
|
mi_Inst.me_Recalculate |= eRecalculate.Objects;
|
|
}
|
|
mi_Pen = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parts of different color in multi-color lines
|
|
/// </summary>
|
|
public int ColorParts
|
|
{
|
|
get { return ms32_Parts; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor IMPORTANT: If you use the UndoBuffer you must never modify any property
|
|
/// of the Pen assigned here. If you externally change for example Pen.Color the Undo
|
|
/// will not work correctly. Always assign a different instance of Pen to cLine3D.Pen to
|
|
/// avoid this.
|
|
/// </summary>
|
|
public cLine3D(cPoint3D i_Start, cPoint3D i_End, int s32_Width, Pen i_Pen, int s32_Parts, Object o_Tag = null)
|
|
{
|
|
mi_Points = new cPoint3D[] { i_Start, i_End };
|
|
ms32_Width = s32_Width;
|
|
mi_Pen = i_Pen;
|
|
ms32_Parts = s32_Parts;
|
|
mo_Tag = o_Tag;
|
|
}
|
|
|
|
// For debugging in Visual Studio
|
|
public override string ToString()
|
|
{
|
|
return "cLine3D from " + mi_Points[0] + " to " + mi_Points[1];
|
|
}
|
|
}
|
|
|
|
#endregion cLine3D
|
|
|
|
#region cShape3D
|
|
|
|
public class cShape3D : cObject3D
|
|
{
|
|
private eScatterShape me_Shape;
|
|
private int ms32_Radius;
|
|
private Brush mi_Brush;
|
|
|
|
public override void Restore(eUndoProp e_Property, Object o_Value)
|
|
{
|
|
switch (e_Property)
|
|
{
|
|
case eUndoProp.Shape: me_Shape = (eScatterShape)o_Value; break;
|
|
case eUndoProp.Radius: ms32_Radius = (int)o_Value; break;
|
|
case eUndoProp.Brush: mi_Brush = (Brush)o_Value; break;
|
|
default: base.Restore(e_Property, o_Value); break;
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Gets the type of this cObject3D
|
|
/// </summary>
|
|
public override eObjType ObjType
|
|
{
|
|
get { return eObjType.Shape; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The type of shape to be painted
|
|
/// </summary>
|
|
public eScatterShape Shape
|
|
{
|
|
get { return me_Shape; }
|
|
set
|
|
{
|
|
if (me_Shape == value)
|
|
return;
|
|
|
|
// Circle and rectangle are drawn by the framework --> no recalculation required.
|
|
if (mi_Inst != null)
|
|
{
|
|
mi_Inst.mi_UndoBuffer.Backup(this, eUndoProp.Shape, me_Shape, value);
|
|
|
|
if (value != eScatterShape.Circle && value != eScatterShape.Square)
|
|
mi_Inst.me_Recalculate |= eRecalculate.Objects;
|
|
}
|
|
me_Shape = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The radius of the circle, square or triangle
|
|
/// </summary>
|
|
public int Radius
|
|
{
|
|
get { return ms32_Radius; }
|
|
set
|
|
{
|
|
if (ms32_Radius == value)
|
|
return;
|
|
|
|
if (mi_Inst != null)
|
|
{
|
|
mi_Inst.mi_UndoBuffer.Backup(this, eUndoProp.Radius, ms32_Radius, value);
|
|
mi_Inst.me_Recalculate |= eRecalculate.Objects;
|
|
}
|
|
ms32_Radius = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The color of the Shape or null to use a color from the ColorScheme IMPORTANT: If you
|
|
/// use the UndoBuffer you must never modify any property of the Brush assigned here. If
|
|
/// you externally change for example SolidBrush.Color the Undo will not work correctly.
|
|
/// Always assign a different instance of Brush to avoid this.
|
|
/// </summary>
|
|
public Brush Brush
|
|
{
|
|
get { return mi_Brush; }
|
|
set
|
|
{
|
|
if (mi_Brush == value)
|
|
return;
|
|
|
|
if (mi_Inst != null)
|
|
{
|
|
mi_Inst.mi_UndoBuffer.Backup(this, eUndoProp.Brush, mi_Brush, value);
|
|
mi_Inst.me_Recalculate |= eRecalculate.Objects;
|
|
}
|
|
mi_Brush = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The shape is selected
|
|
/// </summary>
|
|
public override bool Selected
|
|
{
|
|
get { return mi_Points[0].Selected; }
|
|
set { mi_Points[0].Selected = value; } // me_Recalculate needs no change
|
|
}
|
|
|
|
/// <summary>
|
|
/// Defines if the user is allowed to select this shape
|
|
/// </summary>
|
|
public override bool CanSelect
|
|
{
|
|
get { return mi_Points[0].CanSelect; }
|
|
set { mi_Points[0].CanSelect = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor IMPORTANT: If you use the UndoBuffer you must never modify any property
|
|
/// of the Brush assigned here. If you externally change for example SolidBrush.Color
|
|
/// the Undo will not work correctly. Always assign a different instance of Brush to
|
|
/// cShape3D.Brush to avoid this.
|
|
/// </summary>
|
|
public cShape3D(int s32_Col, int s32_Row, cPoint3D i_Point, eScatterShape e_Shape, int s32_Radius, Brush i_Brush, Object o_Tag = null)
|
|
{
|
|
ms32_Col = s32_Col;
|
|
ms32_Row = s32_Row;
|
|
i_Point.Tag = o_Tag;
|
|
mi_Points = new cPoint3D[] { i_Point };
|
|
me_Shape = e_Shape;
|
|
ms32_Radius = s32_Radius;
|
|
mi_Brush = i_Brush;
|
|
mo_Tag = o_Tag;
|
|
}
|
|
|
|
// For debugging in Visual Studio
|
|
public override string ToString()
|
|
{
|
|
return "cShape3D " + me_Shape + " at " + mi_Points[0];
|
|
}
|
|
}
|
|
|
|
#endregion cShape3D
|
|
|
|
#region cPolygon3D
|
|
|
|
public class cPolygon3D : cObject3D
|
|
{
|
|
private Brush mi_Brush;
|
|
|
|
public override void Restore(eUndoProp e_Property, Object o_Value)
|
|
{
|
|
switch (e_Property)
|
|
{
|
|
case eUndoProp.Brush: mi_Brush = (Brush)o_Value; break;
|
|
default: base.Restore(e_Property, o_Value); break;
|
|
// ms32_Col is constant ms32_Row is constant
|
|
}
|
|
}
|
|
|
|
// --------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Gets the type of this cObject3D
|
|
/// </summary>
|
|
public override eObjType ObjType
|
|
{
|
|
get { return eObjType.Polygon; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The color of the Polygon or null to use a color from the ColorScheme IMPORTANT: If
|
|
/// you use the UndoBuffer you must never modify any property of the Brush assigned
|
|
/// here. If you externally change for example SolidBrush.Color the Undo will not work
|
|
/// correctly. Always assign a different instance of Brush to avoid this.
|
|
/// </summary>
|
|
public Brush Brush
|
|
{
|
|
get { return mi_Brush; }
|
|
set
|
|
{
|
|
if (mi_Brush == value)
|
|
return;
|
|
|
|
if (mi_Inst != null)
|
|
{
|
|
mi_Inst.mi_UndoBuffer.Backup(this, eUndoProp.Brush, mi_Brush, value);
|
|
mi_Inst.me_Recalculate |= eRecalculate.Objects;
|
|
}
|
|
mi_Brush = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor IMPORTANT: If you use the UndoBuffer you must never modify any property
|
|
/// of the Brush assigned here. If you externally change for example SolidBrush.Color
|
|
/// the Undo will not work correctly. Always assign a different instance of Brush to
|
|
/// cPolygon3D.Brush to avoid this.
|
|
/// </summary>
|
|
public cPolygon3D(int s32_Col, int s32_Row, Brush i_Brush, params cPoint3D[] i_Points)
|
|
{
|
|
if (i_Points.Length < 3)
|
|
throw new ArgumentException("At least 3 points are required to draw a polygon.");
|
|
|
|
mi_Points = i_Points;
|
|
mi_Brush = i_Brush;
|
|
ms32_Col = s32_Col;
|
|
ms32_Row = s32_Row;
|
|
}
|
|
|
|
// For debugging in Visual Studio
|
|
public override string ToString()
|
|
{
|
|
return "cPolygon3D (" + mi_Points.Length + " points)";
|
|
}
|
|
}
|
|
|
|
#endregion cPolygon3D
|
|
|
|
// ---------------------
|
|
|
|
#region cAxis
|
|
|
|
/// <summary>
|
|
/// Used for the main axes and raster lines of the coordinate system
|
|
/// </summary>
|
|
[Serializable]
|
|
[TypeConverter(typeof(ExpandableObjectConverter))]
|
|
public class cAxis
|
|
{
|
|
private Editor3D mi_Inst;
|
|
private eCoord me_Coord;
|
|
private Pen mi_RasterPen; // Raster lines (brigther)
|
|
private Pen mi_AxisPen; // Main coordinate axis (darker)
|
|
private SolidBrush mi_LegendBrush; // Label text
|
|
private String ms_LegendText;
|
|
private bool mb_Mirror;
|
|
private bool mb_IncludeZero;
|
|
|
|
public cAxis(Editor3D i_Inst, eCoord e_Coord, Color c_Color)
|
|
{
|
|
mi_Inst = i_Inst;
|
|
me_Coord = e_Coord;
|
|
Color = c_Color;
|
|
Reset();
|
|
}
|
|
|
|
public void Reset()
|
|
{
|
|
Debug.Assert(!mi_Inst.InvokeRequired); // Call only from GUI thread
|
|
ms_LegendText = null;
|
|
mb_Mirror = false;
|
|
mb_IncludeZero = me_Coord == eCoord.Z;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The color of the axis lines and the legend. This change will become visible the next
|
|
/// time you call Invalidate() Not modified by Reset()
|
|
/// </summary>
|
|
public Color Color
|
|
{
|
|
get { return mi_AxisPen.Color; }
|
|
set
|
|
{
|
|
Debug.Assert(!mi_Inst.InvokeRequired); // Call only from GUI thread
|
|
mi_LegendBrush = new SolidBrush(value);
|
|
mi_AxisPen = new Pen(value, 3);
|
|
mi_RasterPen = new Pen(BrightenColor(value), 1);
|
|
mi_Inst.me_Recalculate |= eRecalculate.CoordSystem;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internally used to draw raster lines (brighter)
|
|
/// </summary>
|
|
[Browsable(false)]
|
|
public Pen RasterPen
|
|
{
|
|
get { return mi_RasterPen; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internally used to draw main coordinates (darker)
|
|
/// </summary>
|
|
[Browsable(false)]
|
|
public Pen AxisPen
|
|
{
|
|
get { return mi_AxisPen; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Internally used for label text
|
|
/// </summary>
|
|
[Browsable(false)]
|
|
public SolidBrush LegendBrush
|
|
{
|
|
get { return mi_LegendBrush; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Here you can add an optional legend which is displayed for the axis. With the
|
|
/// property Editor3D.LegendPos you can define where the legend is drawn. If the string
|
|
/// is null or empty, no legend will be displayed for this axis. This change will become
|
|
/// visible the next time you call Invalidate()
|
|
/// </summary>
|
|
public String LegendText
|
|
{
|
|
get { return ms_LegendText; }
|
|
set { ms_LegendText = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// In the default rotation angle the axis values are normally increasing from right to
|
|
/// left (X), back to front (Y) and bottom to top (Z). If Mirror = true they will
|
|
/// decrease instead of increase. This change will become visible the next time you call Invalidate()
|
|
/// </summary>
|
|
public bool Mirror
|
|
{
|
|
get { return mb_Mirror; }
|
|
set
|
|
{
|
|
if (mb_Mirror == value)
|
|
return;
|
|
|
|
if (me_Coord == eCoord.Z)
|
|
throw new NotImplementedException("Mirroring the Z axis is not implemented because there are multiple issues and it does not make sense to draw the Z values reverse.");
|
|
|
|
Debug.Assert(!mi_Inst.InvokeRequired); // Call only from GUI thread
|
|
mb_Mirror = value;
|
|
mi_Inst.me_Recalculate |= eRecalculate.CoordSystem | eRecalculate.Objects;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Example: If the axis has values from 4 to 10 and IncludeZero = false --> the
|
|
/// coordinate axis has a range from 4 to 10 IncludeZero = true --> the coordinate
|
|
/// axis has a range from 0 to 10 (default)
|
|
/// ATTENTION: This setting is ignored if the coordinate system is not drawn (eRaster.Off)
|
|
/// This change will become visible the next time you call Invalidate()
|
|
/// </summary>
|
|
public bool IncludeZero
|
|
{
|
|
get { return mb_IncludeZero; }
|
|
set
|
|
{
|
|
if (mb_IncludeZero == value)
|
|
return;
|
|
|
|
Debug.Assert(!mi_Inst.InvokeRequired); // Call only from GUI thread
|
|
mb_IncludeZero = value;
|
|
mi_Inst.me_Recalculate |= eRecalculate.CoordSystem | eRecalculate.Objects;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion cAxis
|
|
|
|
#region cSelection
|
|
|
|
/// <summary>
|
|
/// This class controls the user selection of points, lines, shapes and polygons
|
|
/// </summary>
|
|
[Serializable]
|
|
[TypeConverter(typeof(ExpandableObjectConverter))]
|
|
public class cSelection
|
|
{
|
|
private Editor3D mi_Inst;
|
|
private bool mb_Enabled;
|
|
private bool mb_MultiSel;
|
|
private bool mb_SinglePoints;
|
|
private delSelectHandler mf_Callback;
|
|
private Color mc_HighlightColor = Color.Empty;
|
|
private Brush mi_HighlightBrush;
|
|
private Pen mi_HighlightPen;
|
|
|
|
public cSelection(Editor3D i_Inst)
|
|
{
|
|
mi_Inst = i_Inst;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This property defines if the user is allowed to select 3D objects. The selection
|
|
/// will only be visible if HighlightColor has also been set. The callback will only be
|
|
/// called if Callback has also been assigned.
|
|
/// </summary>
|
|
public bool Enabled
|
|
{
|
|
get { return mb_Enabled; }
|
|
set { mb_Enabled = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// This defines the color with which selected draw objects / points are painted. If you
|
|
/// pass Color.Empty draw objects will not be highlighted although they are selected!
|
|
/// This change will become visible the next time you call Invalidate()
|
|
/// </summary>
|
|
public Color HighlightColor
|
|
{
|
|
set
|
|
{
|
|
if (value.A > 0)
|
|
{
|
|
mi_HighlightBrush = new SolidBrush(value);
|
|
mi_HighlightPen = new Pen(value);
|
|
mi_HighlightPen.StartCap = LineCap.Round;
|
|
mi_HighlightPen.EndCap = LineCap.Round;
|
|
}
|
|
else
|
|
{
|
|
mi_HighlightBrush = null;
|
|
mi_HighlightPen = null;
|
|
}
|
|
mc_HighlightColor = value;
|
|
}
|
|
get
|
|
{
|
|
return mc_HighlightColor;
|
|
}
|
|
}
|
|
|
|
[Browsable(false)]
|
|
public Pen HighlightPen
|
|
{
|
|
get
|
|
{
|
|
if (mb_Enabled) return mi_HighlightPen;
|
|
else return null;
|
|
}
|
|
}
|
|
|
|
[Browsable(false)]
|
|
public Brush HighlightBrush
|
|
{
|
|
get
|
|
{
|
|
if (mb_Enabled) return mi_HighlightBrush;
|
|
else return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// true --> allow selection of multiple 3D objects at once false --> allow only
|
|
/// selection of a single 3D object at a time This setting will be ignored when a
|
|
/// Callback is assigned. The callback is responsible for any selection changes!
|
|
/// </summary>
|
|
public bool MultiSelect
|
|
{
|
|
get { return mb_MultiSel; }
|
|
set { mb_MultiSel = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enables selection of single points. true --> the user can only select single
|
|
/// points of a polygon or the end points of a line. false --> the user can only
|
|
/// select an entire polygon or an entire line. For scatter shapes this setting does not
|
|
/// matter because a scatter shape corresponds to one point.
|
|
/// IMPORTANT: Selecting entire polygons makes only sense if Fill mode is used.
|
|
/// Otherwise polygons are transparent and a click would hit the background behind the
|
|
/// polygon, so this setting is ignored for polygons in Line mode. This change will
|
|
/// become visible the next time you call Invalidate()
|
|
/// </summary>
|
|
public bool SinglePoints
|
|
{
|
|
get { return mb_SinglePoints; }
|
|
set
|
|
{
|
|
mb_SinglePoints = value;
|
|
|
|
// A mix of selected lines or polygons and selected points is possible but the
|
|
// user will be confused.
|
|
DeSelectAll();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// IMPORTANT: Read the detailed comment of function SelectionCallback() at the end of
|
|
/// this class. You can set null here to turn off the callback.
|
|
/// </summary>
|
|
[Browsable(false)]
|
|
public delSelectHandler Callback
|
|
{
|
|
set { mf_Callback = value; }
|
|
get { return mf_Callback; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns selected cLine3D, cShape3D or cPoygon3D objects. Multiple enums can be
|
|
/// combined. Example: GetSelectedObjects(eSelType.Line | eSelType.Shape)
|
|
/// NOTE: For Shape3D the selection of the shape itself and it's point is always the same.
|
|
/// </summary>
|
|
public cObject3D[] GetSelectedObjects(eSelType e_SelType)
|
|
{
|
|
Debug.Assert(!mi_Inst.InvokeRequired); // Call only from GUI thread
|
|
|
|
List<cObject3D> i_List = new List<cObject3D>();
|
|
foreach (cDrawObj i_Object in mi_Inst.mi_UserObjects)
|
|
{
|
|
if (!i_Object.Selected || (i_Object.SelType & e_SelType) == 0)
|
|
continue;
|
|
|
|
i_List.Add(i_Object.mi_Object3D);
|
|
}
|
|
return i_List.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the points that the user may select individually if Selection.SinglePoints =
|
|
/// true and gets all points of a Line3D or Polygon3D if it is selected. Multiple enums
|
|
/// can be combined. Example: GetSelectedPoints(eSelType.Line | eSelType.Shape)
|
|
/// NOTE: The returned array contains only unique points.
|
|
/// NOTE: For Shape3D the selection of the shape itself and it's point is always the same.
|
|
/// </summary>
|
|
public cPoint3D[] GetSelectedPoints(eSelType e_SelType)
|
|
{
|
|
Debug.Assert(!mi_Inst.InvokeRequired); // Call only from GUI thread
|
|
|
|
List<cPoint3D> i_Unique = new List<cPoint3D>();
|
|
|
|
foreach (cDrawObj i_Object in mi_Inst.mi_UserObjects)
|
|
{
|
|
if ((i_Object.SelType & e_SelType) == 0)
|
|
continue;
|
|
|
|
// If the object itself is selected return all it's points, no matter if the
|
|
// points are selected or not.
|
|
if (i_Object.Selected)
|
|
{
|
|
foreach (cPoint i_Point in i_Object.mi_Points)
|
|
{
|
|
if (!i_Unique.Contains(i_Point.mi_P3D))
|
|
i_Unique.Add(i_Point.mi_P3D);
|
|
}
|
|
}
|
|
else // Object not selected --> check if single points of the object are selected
|
|
{
|
|
foreach (cPoint i_Point in i_Object.mi_Points)
|
|
{
|
|
if (i_Point.mi_P3D.Selected &&
|
|
!i_Unique.Contains(i_Point.mi_P3D))
|
|
i_Unique.Add(i_Point.mi_P3D);
|
|
}
|
|
}
|
|
}
|
|
return i_Unique.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove the selection from all draw objects This change will become visible the next
|
|
/// time you call Invalidate()
|
|
/// </summary>
|
|
public void DeSelectAll()
|
|
{
|
|
Debug.Assert(!mi_Inst.InvokeRequired); // Call only from GUI thread
|
|
|
|
foreach (cDrawObj i_Obj in mi_Inst.mi_UserObjects)
|
|
{
|
|
i_Obj.Selected = false;
|
|
|
|
foreach (cPoint i_Point in i_Obj.mi_Points)
|
|
{
|
|
i_Point.mi_P3D.Selected = false;
|
|
}
|
|
}
|
|
|
|
mi_Inst.mi_UndoBuffer.Store();
|
|
}
|
|
}
|
|
|
|
#endregion cSelection
|
|
|
|
#region cUndoBuffer
|
|
|
|
/// <summary>
|
|
/// This public base class is exposed to the user. The implementation is in cUndoImpl.
|
|
/// </summary>
|
|
[Serializable]
|
|
[TypeConverter(typeof(ExpandableObjectConverter))]
|
|
public abstract class cUndoBuffer
|
|
{
|
|
/// <summary>
|
|
/// Enable the Undo Buffer only if you need the Undo functionality. By default Undo /
|
|
/// Redo is disabled.
|
|
/// </summary>
|
|
public virtual bool Enabled
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Clears the Undo Buffer.
|
|
/// </summary>
|
|
public virtual void Clear()
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Undo the last user operation. return false if Undo is not available. This is also
|
|
/// called when hitting CTRL + Z while the 3D Editor has the keyboard focus.
|
|
/// </summary>
|
|
public virtual bool Undo()
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Redo the last user operation. return false if Redo is not available. This is also
|
|
/// called when hitting CTRL + Y while the 3D Editor has the keyboard focus.
|
|
/// </summary>
|
|
public virtual bool Redo()
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
}
|
|
|
|
#endregion cUndoBuffer
|
|
|
|
#region cColorScheme
|
|
|
|
/// <summary>
|
|
/// Pens and Brushes are GDI+ objects which must not be created on the fly in Draw() For
|
|
/// speed optimization these are created only once and stored in this class.
|
|
/// </summary>
|
|
public class cColorScheme
|
|
{
|
|
// ========================= STATIC ===========================
|
|
|
|
public static Color[] GetSchema(eColorScheme e_Scheme)
|
|
{
|
|
Byte[,] u8_RGB;
|
|
switch (e_Scheme)
|
|
{
|
|
case eColorScheme.Green: return new Color[] { Color.Green };
|
|
case eColorScheme.Rainbow_Sweep: return CalcRainbow(6); // all colors, also magenta
|
|
case eColorScheme.Rainbow_Bright: return CalcRainbow(4); // from red to blue, no magenta
|
|
case eColorScheme.Monochrome: return new Color[] { Color.Goldenrod };
|
|
case eColorScheme.Autumn: u8_RGB = new byte[,] { { 255, 0, 0 }, { 255, 4, 0 }, { 255, 8, 0 }, { 255, 12, 0 }, { 255, 16, 0 }, { 255, 20, 0 }, { 255, 24, 0 }, { 255, 28, 0 }, { 255, 32, 0 }, { 255, 36, 0 }, { 255, 40, 0 }, { 255, 45, 0 }, { 255, 49, 0 }, { 255, 53, 0 }, { 255, 57, 0 }, { 255, 61, 0 }, { 255, 65, 0 }, { 255, 69, 0 }, { 255, 73, 0 }, { 255, 77, 0 }, { 255, 81, 0 }, { 255, 85, 0 }, { 255, 89, 0 }, { 255, 93, 0 }, { 255, 97, 0 }, { 255, 101, 0 }, { 255, 105, 0 }, { 255, 109, 0 }, { 255, 113, 0 }, { 255, 117, 0 }, { 255, 121, 0 }, { 255, 125, 0 }, { 255, 130, 0 }, { 255, 134, 0 }, { 255, 138, 0 }, { 255, 142, 0 }, { 255, 146, 0 }, { 255, 150, 0 }, { 255, 154, 0 }, { 255, 158, 0 }, { 255, 162, 0 }, { 255, 166, 0 }, { 255, 170, 0 }, { 255, 174, 0 }, { 255, 178, 0 }, { 255, 182, 0 }, { 255, 186, 0 }, { 255, 190, 0 }, { 255, 194, 0 }, { 255, 198, 0 }, { 255, 202, 0 }, { 255, 206, 0 }, { 255, 210, 0 }, { 255, 215, 0 }, { 255, 219, 0 }, { 255, 223, 0 }, { 255, 227, 0 }, { 255, 231, 0 }, { 255, 235, 0 }, { 255, 239, 0 }, { 255, 243, 0 }, { 255, 247, 0 }, { 255, 251, 0 }, { 255, 255, 0 } }; break;
|
|
case eColorScheme.Cool: u8_RGB = new byte[,] { { 0, 255, 255 }, { 4, 251, 255 }, { 8, 247, 255 }, { 12, 243, 255 }, { 16, 239, 255 }, { 20, 235, 255 }, { 24, 231, 255 }, { 28, 227, 255 }, { 32, 223, 255 }, { 36, 219, 255 }, { 40, 215, 255 }, { 45, 210, 255 }, { 49, 206, 255 }, { 53, 202, 255 }, { 57, 198, 255 }, { 61, 194, 255 }, { 65, 190, 255 }, { 69, 186, 255 }, { 73, 182, 255 }, { 77, 178, 255 }, { 81, 174, 255 }, { 85, 170, 255 }, { 89, 166, 255 }, { 93, 162, 255 }, { 97, 158, 255 }, { 101, 154, 255 }, { 105, 150, 255 }, { 109, 146, 255 }, { 113, 142, 255 }, { 117, 138, 255 }, { 121, 134, 255 }, { 125, 130, 255 }, { 130, 125, 255 }, { 134, 121, 255 }, { 138, 117, 255 }, { 142, 113, 255 }, { 146, 109, 255 }, { 150, 105, 255 }, { 154, 101, 255 }, { 158, 97, 255 }, { 162, 93, 255 }, { 166, 89, 255 }, { 170, 85, 255 }, { 174, 81, 255 }, { 178, 77, 255 }, { 182, 73, 255 }, { 186, 69, 255 }, { 190, 65, 255 }, { 194, 61, 255 }, { 198, 57, 255 }, { 202, 53, 255 }, { 206, 49, 255 }, { 210, 45, 255 }, { 215, 40, 255 }, { 219, 36, 255 }, { 223, 32, 255 }, { 227, 28, 255 }, { 231, 24, 255 }, { 235, 20, 255 }, { 239, 16, 255 }, { 243, 12, 255 }, { 247, 8, 255 }, { 251, 4, 255 }, { 255, 0, 255 } }; break;
|
|
case eColorScheme.Copper: u8_RGB = new byte[,] { { 0, 0, 0 }, { 5, 3, 2 }, { 10, 6, 4 }, { 15, 9, 6 }, { 20, 13, 8 }, { 25, 16, 10 }, { 30, 19, 12 }, { 35, 22, 14 }, { 40, 25, 16 }, { 46, 28, 18 }, { 51, 32, 20 }, { 56, 35, 22 }, { 61, 38, 24 }, { 66, 41, 26 }, { 71, 44, 28 }, { 76, 47, 30 }, { 81, 51, 32 }, { 86, 54, 34 }, { 91, 57, 36 }, { 96, 60, 38 }, { 101, 63, 40 }, { 106, 66, 42 }, { 111, 70, 44 }, { 116, 73, 46 }, { 121, 76, 48 }, { 126, 79, 50 }, { 132, 82, 52 }, { 137, 85, 54 }, { 142, 89, 56 }, { 147, 92, 58 }, { 152, 95, 60 }, { 157, 98, 62 }, { 162, 101, 64 }, { 167, 104, 66 }, { 172, 108, 68 }, { 177, 111, 70 }, { 182, 114, 72 }, { 187, 117, 75 }, { 192, 120, 77 }, { 197, 123, 79 }, { 202, 126, 81 }, { 207, 130, 83 }, { 212, 133, 85 }, { 218, 136, 87 }, { 223, 139, 89 }, { 228, 142, 91 }, { 233, 145, 93 }, { 238, 149, 95 }, { 243, 152, 97 }, { 248, 155, 99 }, { 253, 158, 101 }, { 255, 161, 103 }, { 255, 164, 105 }, { 255, 168, 107 }, { 255, 171, 109 }, { 255, 174, 111 }, { 255, 177, 113 }, { 255, 180, 115 }, { 255, 183, 117 }, { 255, 187, 119 }, { 255, 190, 121 }, { 255, 193, 123 }, { 255, 196, 125 }, { 255, 199, 127 } }; break;
|
|
case eColorScheme.Hot: u8_RGB = new byte[,] { { 11, 0, 0 }, { 21, 0, 0 }, { 32, 0, 0 }, { 43, 0, 0 }, { 53, 0, 0 }, { 64, 0, 0 }, { 74, 0, 0 }, { 85, 0, 0 }, { 96, 0, 0 }, { 106, 0, 0 }, { 117, 0, 0 }, { 128, 0, 0 }, { 138, 0, 0 }, { 149, 0, 0 }, { 159, 0, 0 }, { 170, 0, 0 }, { 181, 0, 0 }, { 191, 0, 0 }, { 202, 0, 0 }, { 213, 0, 0 }, { 223, 0, 0 }, { 234, 0, 0 }, { 244, 0, 0 }, { 255, 0, 0 }, { 255, 11, 0 }, { 255, 21, 0 }, { 255, 32, 0 }, { 255, 43, 0 }, { 255, 53, 0 }, { 255, 64, 0 }, { 255, 74, 0 }, { 255, 85, 0 }, { 255, 96, 0 }, { 255, 106, 0 }, { 255, 117, 0 }, { 255, 128, 0 }, { 255, 138, 0 }, { 255, 149, 0 }, { 255, 159, 0 }, { 255, 170, 0 }, { 255, 181, 0 }, { 255, 191, 0 }, { 255, 202, 0 }, { 255, 213, 0 }, { 255, 223, 0 }, { 255, 234, 0 }, { 255, 244, 0 }, { 255, 255, 0 }, { 255, 255, 16 }, { 255, 255, 32 }, { 255, 255, 48 }, { 255, 255, 64 }, { 255, 255, 80 }, { 255, 255, 96 }, { 255, 255, 112 }, { 255, 255, 128 }, { 255, 255, 143 }, { 255, 255, 159 }, { 255, 255, 175 }, { 255, 255, 191 }, { 255, 255, 207 }, { 255, 255, 223 }, { 255, 255, 239 }, { 255, 255, 255 } }; break;
|
|
case eColorScheme.Hsv: u8_RGB = new byte[,] { { 255, 0, 0 }, { 255, 24, 0 }, { 255, 48, 0 }, { 255, 72, 0 }, { 255, 96, 0 }, { 255, 120, 0 }, { 255, 143, 0 }, { 255, 167, 0 }, { 255, 191, 0 }, { 255, 215, 0 }, { 255, 239, 0 }, { 247, 255, 0 }, { 223, 255, 0 }, { 199, 255, 0 }, { 175, 255, 0 }, { 151, 255, 0 }, { 128, 255, 0 }, { 104, 255, 0 }, { 80, 255, 0 }, { 56, 255, 0 }, { 32, 255, 0 }, { 8, 255, 0 }, { 0, 255, 16 }, { 0, 255, 40 }, { 0, 255, 64 }, { 0, 255, 88 }, { 0, 255, 112 }, { 0, 255, 135 }, { 0, 255, 159 }, { 0, 255, 183 }, { 0, 255, 207 }, { 0, 255, 231 }, { 0, 255, 255 }, { 0, 231, 255 }, { 0, 207, 255 }, { 0, 183, 255 }, { 0, 159, 255 }, { 0, 135, 255 }, { 0, 112, 255 }, { 0, 88, 255 }, { 0, 64, 255 }, { 0, 40, 255 }, { 0, 16, 255 }, { 8, 0, 255 }, { 32, 0, 255 }, { 56, 0, 255 }, { 80, 0, 255 }, { 104, 0, 255 }, { 128, 0, 255 }, { 151, 0, 255 }, { 175, 0, 255 }, { 199, 0, 255 }, { 223, 0, 255 }, { 247, 0, 255 }, { 255, 0, 239 }, { 255, 0, 215 }, { 255, 0, 191 }, { 255, 0, 167 }, { 255, 0, 143 }, { 255, 0, 120 }, { 255, 0, 96 }, { 255, 0, 72 }, { 255, 0, 48 }, { 255, 0, 24 } }; break;
|
|
case eColorScheme.Rainbow_Dark: u8_RGB = new byte[,] { { 0, 0, 143 }, { 0, 0, 159 }, { 0, 0, 175 }, { 0, 0, 191 }, { 0, 0, 207 }, { 0, 0, 223 }, { 0, 0, 239 }, { 0, 0, 255 }, { 0, 16, 255 }, { 0, 32, 255 }, { 0, 48, 255 }, { 0, 64, 255 }, { 0, 80, 255 }, { 0, 96, 255 }, { 0, 112, 255 }, { 0, 128, 255 }, { 0, 143, 255 }, { 0, 159, 255 }, { 0, 175, 255 }, { 0, 191, 255 }, { 0, 207, 255 }, { 0, 223, 255 }, { 0, 239, 255 }, { 0, 255, 255 }, { 16, 255, 239 }, { 32, 255, 223 }, { 48, 255, 207 }, { 64, 255, 191 }, { 80, 255, 175 }, { 96, 255, 159 }, { 112, 255, 143 }, { 128, 255, 128 }, { 143, 255, 112 }, { 159, 255, 96 }, { 175, 255, 80 }, { 191, 255, 64 }, { 207, 255, 48 }, { 223, 255, 32 }, { 239, 255, 16 }, { 255, 255, 0 }, { 255, 239, 0 }, { 255, 223, 0 }, { 255, 207, 0 }, { 255, 191, 0 }, { 255, 175, 0 }, { 255, 159, 0 }, { 255, 143, 0 }, { 255, 128, 0 }, { 255, 112, 0 }, { 255, 96, 0 }, { 255, 80, 0 }, { 255, 64, 0 }, { 255, 48, 0 }, { 255, 32, 0 }, { 255, 16, 0 }, { 255, 0, 0 }, { 239, 0, 0 }, { 223, 0, 0 }, { 207, 0, 0 }, { 191, 0, 0 }, { 175, 0, 0 }, { 159, 0, 0 }, { 143, 0, 0 }, { 128, 0, 0 } }; break;
|
|
case eColorScheme.Pink: u8_RGB = new byte[,] { { 30, 0, 0 }, { 50, 26, 26 }, { 64, 37, 37 }, { 75, 45, 45 }, { 85, 52, 52 }, { 94, 59, 59 }, { 102, 64, 64 }, { 110, 69, 69 }, { 117, 74, 74 }, { 123, 79, 79 }, { 130, 83, 83 }, { 136, 87, 87 }, { 141, 91, 91 }, { 147, 95, 95 }, { 152, 98, 98 }, { 157, 102, 102 }, { 162, 105, 105 }, { 167, 108, 108 }, { 172, 111, 111 }, { 176, 114, 114 }, { 181, 117, 117 }, { 185, 120, 120 }, { 189, 123, 123 }, { 194, 126, 126 }, { 195, 132, 129 }, { 197, 138, 131 }, { 199, 144, 134 }, { 201, 149, 136 }, { 202, 154, 139 }, { 204, 159, 141 }, { 206, 164, 144 }, { 207, 169, 146 }, { 209, 174, 148 }, { 211, 178, 151 }, { 212, 183, 153 }, { 214, 187, 155 }, { 216, 191, 157 }, { 217, 195, 160 }, { 219, 199, 162 }, { 220, 203, 164 }, { 222, 207, 166 }, { 223, 211, 168 }, { 225, 215, 170 }, { 226, 218, 172 }, { 228, 222, 174 }, { 229, 225, 176 }, { 231, 229, 178 }, { 232, 232, 180 }, { 234, 234, 185 }, { 235, 235, 191 }, { 237, 237, 196 }, { 238, 238, 201 }, { 240, 240, 206 }, { 241, 241, 211 }, { 243, 243, 216 }, { 244, 244, 221 }, { 245, 245, 225 }, { 247, 247, 230 }, { 248, 248, 234 }, { 250, 250, 238 }, { 251, 251, 243 }, { 252, 252, 247 }, { 254, 254, 251 }, { 255, 255, 255 } }; break;
|
|
case eColorScheme.Spring: u8_RGB = new byte[,] { { 255, 0, 255 }, { 255, 4, 251 }, { 255, 8, 247 }, { 255, 12, 243 }, { 255, 16, 239 }, { 255, 20, 235 }, { 255, 24, 231 }, { 255, 28, 227 }, { 255, 32, 223 }, { 255, 36, 219 }, { 255, 40, 215 }, { 255, 45, 210 }, { 255, 49, 206 }, { 255, 53, 202 }, { 255, 57, 198 }, { 255, 61, 194 }, { 255, 65, 190 }, { 255, 69, 186 }, { 255, 73, 182 }, { 255, 77, 178 }, { 255, 81, 174 }, { 255, 85, 170 }, { 255, 89, 166 }, { 255, 93, 162 }, { 255, 97, 158 }, { 255, 101, 154 }, { 255, 105, 150 }, { 255, 109, 146 }, { 255, 113, 142 }, { 255, 117, 138 }, { 255, 121, 134 }, { 255, 125, 130 }, { 255, 130, 125 }, { 255, 134, 121 }, { 255, 138, 117 }, { 255, 142, 113 }, { 255, 146, 109 }, { 255, 150, 105 }, { 255, 154, 101 }, { 255, 158, 97 }, { 255, 162, 93 }, { 255, 166, 89 }, { 255, 170, 85 }, { 255, 174, 81 }, { 255, 178, 77 }, { 255, 182, 73 }, { 255, 186, 69 }, { 255, 190, 65 }, { 255, 194, 61 }, { 255, 198, 57 }, { 255, 202, 53 }, { 255, 206, 49 }, { 255, 210, 45 }, { 255, 215, 40 }, { 255, 219, 36 }, { 255, 223, 32 }, { 255, 227, 28 }, { 255, 231, 24 }, { 255, 235, 20 }, { 255, 239, 16 }, { 255, 243, 12 }, { 255, 247, 8 }, { 255, 251, 4 }, { 255, 255, 0 } }; break;
|
|
case eColorScheme.Summer: u8_RGB = new byte[,] { { 0, 128, 102 }, { 4, 130, 102 }, { 8, 132, 102 }, { 12, 134, 102 }, { 16, 136, 102 }, { 20, 138, 102 }, { 24, 140, 102 }, { 28, 142, 102 }, { 32, 144, 102 }, { 36, 146, 102 }, { 40, 148, 102 }, { 45, 150, 102 }, { 49, 152, 102 }, { 53, 154, 102 }, { 57, 156, 102 }, { 61, 158, 102 }, { 65, 160, 102 }, { 69, 162, 102 }, { 73, 164, 102 }, { 77, 166, 102 }, { 81, 168, 102 }, { 85, 170, 102 }, { 89, 172, 102 }, { 93, 174, 102 }, { 97, 176, 102 }, { 101, 178, 102 }, { 105, 180, 102 }, { 109, 182, 102 }, { 113, 184, 102 }, { 117, 186, 102 }, { 121, 188, 102 }, { 125, 190, 102 }, { 130, 192, 102 }, { 134, 194, 102 }, { 138, 196, 102 }, { 142, 198, 102 }, { 146, 200, 102 }, { 150, 202, 102 }, { 154, 204, 102 }, { 158, 206, 102 }, { 162, 208, 102 }, { 166, 210, 102 }, { 170, 212, 102 }, { 174, 215, 102 }, { 178, 217, 102 }, { 182, 219, 102 }, { 186, 221, 102 }, { 190, 223, 102 }, { 194, 225, 102 }, { 198, 227, 102 }, { 202, 229, 102 }, { 206, 231, 102 }, { 210, 233, 102 }, { 215, 235, 102 }, { 219, 237, 102 }, { 223, 239, 102 }, { 227, 241, 102 }, { 231, 243, 102 }, { 235, 245, 102 }, { 239, 247, 102 }, { 243, 249, 102 }, { 247, 251, 102 }, { 251, 253, 102 }, { 255, 255, 102 } }; break;
|
|
case eColorScheme.Winter: u8_RGB = new byte[,] { { 0, 0, 255 }, { 0, 4, 253 }, { 0, 8, 251 }, { 0, 12, 249 }, { 0, 16, 247 }, { 0, 20, 245 }, { 0, 24, 243 }, { 0, 28, 241 }, { 0, 32, 239 }, { 0, 36, 237 }, { 0, 40, 235 }, { 0, 45, 233 }, { 0, 49, 231 }, { 0, 53, 229 }, { 0, 57, 227 }, { 0, 61, 225 }, { 0, 65, 223 }, { 0, 69, 221 }, { 0, 73, 219 }, { 0, 77, 217 }, { 0, 81, 215 }, { 0, 85, 213 }, { 0, 89, 210 }, { 0, 93, 208 }, { 0, 97, 206 }, { 0, 101, 204 }, { 0, 105, 202 }, { 0, 109, 200 }, { 0, 113, 198 }, { 0, 117, 196 }, { 0, 121, 194 }, { 0, 125, 192 }, { 0, 130, 190 }, { 0, 134, 188 }, { 0, 138, 186 }, { 0, 142, 184 }, { 0, 146, 182 }, { 0, 150, 180 }, { 0, 154, 178 }, { 0, 158, 176 }, { 0, 162, 174 }, { 0, 166, 172 }, { 0, 170, 170 }, { 0, 174, 168 }, { 0, 178, 166 }, { 0, 182, 164 }, { 0, 186, 162 }, { 0, 190, 160 }, { 0, 194, 158 }, { 0, 198, 156 }, { 0, 202, 154 }, { 0, 206, 152 }, { 0, 210, 150 }, { 0, 215, 148 }, { 0, 219, 146 }, { 0, 223, 144 }, { 0, 227, 142 }, { 0, 231, 140 }, { 0, 235, 138 }, { 0, 239, 136 }, { 0, 243, 134 }, { 0, 247, 132 }, { 0, 251, 130 }, { 0, 255, 128 } }; break;
|
|
default:
|
|
throw new ArgumentException("Invalid enum");
|
|
}
|
|
|
|
Color[] c_Schema = new Color[u8_RGB.GetLength(0)];
|
|
for (int i = 0; i < c_Schema.Length; i++)
|
|
{
|
|
c_Schema[i] = Color.FromArgb(u8_RGB[i, 0], u8_RGB[i, 1], u8_RGB[i, 2]);
|
|
}
|
|
return c_Schema;
|
|
}
|
|
|
|
/// <summary>
|
|
/// s32_Sweeps = 4 --> Colors from blue to red, not including magenta (1024 colors)
|
|
/// s32_Sweeps = 6 --> Complete sweep with all rainbow colors, also magenta (1536 colors)
|
|
/// </summary>
|
|
private static Color[] CalcRainbow(int s32_Sweeps)
|
|
{
|
|
Color[] c_Colors = new Color[s32_Sweeps * 256];
|
|
int R, G, B, P = 0;
|
|
for (int L = 0; L < s32_Sweeps; L++)
|
|
{
|
|
for (int E = 0; E < 256; E++)
|
|
{
|
|
switch (L)
|
|
{
|
|
case 0: R = 0; G = E; B = 255; break; // Blue...Cyan
|
|
case 1: R = 0; G = 255; B = 255 - E; break; // Cyan...Green
|
|
case 2: R = E; G = 255; B = 0; break; // Green...Yellow
|
|
case 3: R = 255; G = 255 - E; B = 0; break; // Yellow...Red
|
|
case 4: R = 255; G = 0; B = E; break; // Red...Magenta
|
|
case 5: R = 255 - E; G = 0; B = 255; break; // Magenta...Blue
|
|
default: throw new ArgumentException();
|
|
}
|
|
c_Colors[P++] = Color.FromArgb(255, R, G, B);
|
|
}
|
|
}
|
|
return c_Colors;
|
|
}
|
|
|
|
// ========================= MEMBER ===========================
|
|
|
|
private Brush[] mi_Brushes;
|
|
private Pen[] mi_Pens;
|
|
|
|
public int ColorCount
|
|
{
|
|
get { return mi_Brushes.Length; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor 1
|
|
/// </summary>
|
|
public cColorScheme(eColorScheme e_Scheme)
|
|
: this(GetSchema(e_Scheme))
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor 2 If you want to draw the entire graph with only one color, you can pass
|
|
/// a single Color here.
|
|
/// </summary>
|
|
public cColorScheme(params Color[] c_Colors)
|
|
{
|
|
if (c_Colors.Length == 0)
|
|
throw new ArgumentException("A ColorScheme needs at least one color.");
|
|
|
|
mi_Brushes = new SolidBrush[c_Colors.Length];
|
|
mi_Pens = new Pen[c_Colors.Length];
|
|
|
|
for (int i = 0; i < c_Colors.Length; i++)
|
|
{
|
|
mi_Brushes[i] = new SolidBrush(c_Colors[i]);
|
|
mi_Pens[i] = new Pen(c_Colors[i], 1); // the width of the Pen will be modified later.
|
|
}
|
|
}
|
|
|
|
public Brush GetBrush(int s32_Index)
|
|
{
|
|
s32_Index = Math.Max(0, s32_Index);
|
|
return mi_Brushes[s32_Index % mi_Brushes.Length];
|
|
}
|
|
|
|
public Pen GetPen(int s32_Index)
|
|
{
|
|
s32_Index = Math.Max(0, s32_Index);
|
|
return mi_Pens[s32_Index % mi_Pens.Length];
|
|
}
|
|
|
|
public int CalcIndex(double d_FactorZ)
|
|
{
|
|
if (double.IsNaN(d_FactorZ))
|
|
return -1;
|
|
|
|
d_FactorZ = Math.Min(1.0, d_FactorZ);
|
|
d_FactorZ = Math.Max(0.0, d_FactorZ);
|
|
|
|
// d_FactorZ is a value between 0.0 and 1.0
|
|
return (int)(d_FactorZ * (mi_Brushes.Length - 1));
|
|
}
|
|
}
|
|
|
|
#endregion cColorScheme
|
|
|
|
#region cUserInput
|
|
|
|
public class cUserInput
|
|
{
|
|
private MouseButtons me_MouseButton;
|
|
private Keys me_Modifiers;
|
|
private eMouseAction me_Action;
|
|
private Cursor mi_Cursor;
|
|
|
|
/// <summary>
|
|
/// A unique identifier for the combination of mouse button and modifier(s) Keys.Shift =
|
|
/// 0x00010000 Keys.Control = 0x00020000 Keys.Alt = 0x00040000 MouseButtons.Left =
|
|
/// 0x00100000 MouseButtons.Right = 0x00200000 MouseButtons.Middle = 0x00400000
|
|
/// MouseButtons.XButton1 = 0x00800000 MouseButtons.XButton2 = 0x01000000
|
|
/// </summary>
|
|
public int UID
|
|
{
|
|
get { return (int)me_MouseButton | (int)me_Modifiers; }
|
|
}
|
|
|
|
public MouseButtons MouseButton
|
|
{
|
|
get { return me_MouseButton; }
|
|
}
|
|
|
|
public Keys Modifiers
|
|
{
|
|
get { return me_Modifiers; }
|
|
}
|
|
|
|
public eMouseAction Action
|
|
{
|
|
get { return me_Action; }
|
|
}
|
|
|
|
public Cursor Cursor
|
|
{
|
|
get { return mi_Cursor; }
|
|
}
|
|
|
|
public cUserInput(MouseButtons e_MouseButton, Keys e_Modifiers, eMouseAction e_Action, Cursor i_Cursor = null)
|
|
{
|
|
Debug.Assert((e_Modifiers & Keys.KeyCode) == 0, "Invalid parameter e_Modifiers");
|
|
Debug.Assert(e_MouseButton != MouseButtons.None, "Invalid parameter e_MouseButton");
|
|
Debug.Assert(e_Action != eMouseAction.None, "Invalid parameter e_Action");
|
|
|
|
me_MouseButton = e_MouseButton;
|
|
me_Modifiers = e_Modifiers;
|
|
me_Action = e_Action;
|
|
mi_Cursor = i_Cursor;
|
|
|
|
if (mi_Cursor == null)
|
|
{
|
|
switch (e_Action)
|
|
{
|
|
case eMouseAction.ThetaAndPhi: mi_Cursor = Cursors.SizeAll; break;
|
|
case eMouseAction.Theta: mi_Cursor = Cursors.NoMoveVert; break;
|
|
case eMouseAction.Phi: mi_Cursor = Cursors.NoMoveHoriz; break;
|
|
case eMouseAction.Rho: mi_Cursor = Cursors.SizeNS; break;
|
|
case eMouseAction.Move: mi_Cursor = Cursors.NoMove2D; break;
|
|
default: mi_Cursor = Cursors.Arrow; break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// For debugging in Visual Studio
|
|
/// </summary>
|
|
public override string ToString()
|
|
{
|
|
return String.Format("MouseButton: {0}, Modifiers: {1} --> Action: {2}", me_MouseButton, me_Modifiers, me_Action);
|
|
}
|
|
}
|
|
|
|
#endregion cUserInput
|
|
|
|
// ---------------------
|
|
|
|
#region cRenderData
|
|
|
|
/// <summary>
|
|
/// Base class for cSurfaceData, cScatterData, cLineData, cPolygonData
|
|
/// </summary>
|
|
public abstract class cRenderData
|
|
{
|
|
public virtual void AddDrawObjects(Editor3D i_Inst)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The width and color of the Pen may be modified later. So the immutable framework
|
|
/// collection like Pens.Black,... cannot be used here.
|
|
/// </summary>
|
|
protected static void CheckPenMutable(Pen i_Pen, cColorScheme i_ColorScheme)
|
|
{
|
|
if (i_Pen != null && i_ColorScheme != null)
|
|
{
|
|
try { i_Pen.Color = Color.BlanchedAlmond; }
|
|
catch { throw new ArgumentException("To use a color scheme create a new Pen. Do not use the immutable Pens.XYZ collection."); }
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion cRenderData
|
|
|
|
#region cSurfaceData
|
|
|
|
public class cSurfaceData : cRenderData
|
|
{
|
|
private bool mb_Fill;
|
|
private bool mb_Missing;
|
|
private int ms32_Cols;
|
|
private int ms32_Rows;
|
|
private int ms32_Radius;
|
|
private Pen mi_Pen;
|
|
private cPoint3D[,] mi_PointArray;
|
|
private cPolygon3D[,] mi_PolygonArray;
|
|
private cColorScheme mi_ColorScheme;
|
|
|
|
public cColorScheme ColorScheme
|
|
{
|
|
get { return mi_ColorScheme; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The count of points in one column of the surface grid
|
|
/// </summary>
|
|
public int Cols
|
|
{
|
|
get { return ms32_Cols; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The count of points in one row of the surface grid
|
|
/// </summary>
|
|
public int Rows
|
|
{
|
|
get { return ms32_Rows; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fill Mode: ------------ Polygons are filled with a color from the ColorScheme. If
|
|
/// you want only one color, set a ColorScheme which contains only one color. The Pen is
|
|
/// used to draw the thin lines between the polygons (mostly black, 1 pixel) If Pen is
|
|
/// null, no lines are drawn.
|
|
///
|
|
/// Line Mode: ------------ Only the border lines of the polygons are drawn. The Pen is
|
|
/// used to draw these lines. The Pen's color and width will be modified.
|
|
///
|
|
/// Missing Mode: -------------- s32_Radius > 0 allows missing points. s32_Radius
|
|
/// defines the radius of cicles that represent points which have not enough neigbours
|
|
/// to draw a polygon.
|
|
/// </summary>
|
|
public cSurfaceData(int s32_Cols, int s32_Rows, ePolygonMode e_Mode, Pen i_Pen, cColorScheme i_ColorScheme,
|
|
int s32_Radius = 0)
|
|
{
|
|
if (s32_Cols < 3 || s32_Rows < 3)
|
|
throw new ArgumentException("cSurfaceData needs at least 3 columns and 3 rows");
|
|
|
|
if (e_Mode == ePolygonMode.Fill)
|
|
{
|
|
if (i_ColorScheme == null)
|
|
throw new ArgumentException("In Fill mode you must specify a ColorScheme");
|
|
|
|
// The border pen is allowed to be immutable. It will not be changed.
|
|
}
|
|
else // Lines
|
|
{
|
|
if (i_Pen == null)
|
|
throw new ArgumentException("In Line mode you must specify a Line Pen");
|
|
|
|
CheckPenMutable(i_Pen, i_ColorScheme);
|
|
}
|
|
|
|
mb_Fill = e_Mode == ePolygonMode.Fill;
|
|
mb_Missing = s32_Radius > 0;
|
|
ms32_Radius = s32_Radius;
|
|
ms32_Cols = s32_Cols;
|
|
ms32_Rows = s32_Rows;
|
|
mi_Pen = i_Pen;
|
|
mi_ColorScheme = i_ColorScheme;
|
|
mi_PointArray = new cPoint3D[s32_Cols, s32_Rows];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Here you can set a callback function which will be called with X,Y to calculate the
|
|
/// Z values of the points.
|
|
/// </summary>
|
|
public void ExecuteFunction(delRendererFunction f_Function, PointF k_Start, PointF k_End)
|
|
{
|
|
mb_Missing = false;
|
|
|
|
double d_StepX = (k_End.X - k_Start.X) / (ms32_Cols - 1);
|
|
double d_StepY = (k_End.Y - k_Start.Y) / (ms32_Rows - 1);
|
|
|
|
for (int C = 0; C < ms32_Cols; C++)
|
|
{
|
|
double d_X = k_Start.X + d_StepX * C;
|
|
|
|
for (int R = 0; R < ms32_Rows; R++)
|
|
{
|
|
double d_Y = k_Start.Y + d_StepY * R;
|
|
double d_Z = f_Function(d_X, d_Y);
|
|
|
|
SetPointAt(C, R, new cPoint3D(d_X, d_Y, d_Z));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// IMPORTANT: The X coordinate of the point must be related to the column. The Y
|
|
/// coordinate of the point must be related to the row.
|
|
/// </summary>
|
|
public void SetPointAt(int s32_Column, int s32_Row, cPoint3D i_Point3D)
|
|
{
|
|
if (mi_PolygonArray != null)
|
|
throw new Exception("You cannot call cSurfaceData.SetPointAt() or ExecuteFunction() anymore after calling GetPolygonAt() "
|
|
+ "or Editor3D.AddRenderData(). To modify a point after the polygons have been created call "
|
|
+ "GetPointAt() and modify the X,Y,Z values of the returned point.");
|
|
|
|
mi_PointArray[s32_Column, s32_Row] = i_Point3D;
|
|
}
|
|
|
|
/// <summary>
|
|
/// ATTENTION: In mode 'Missing' null may be returned or polygons with only 3 corners!
|
|
/// </summary>
|
|
public cPoint3D GetPointAt(int s32_Column, int s32_Row)
|
|
{
|
|
return mi_PointArray[s32_Column, s32_Row];
|
|
}
|
|
|
|
/// <summary>
|
|
/// ATTENTION: The polygons have one row less than cSurfaceData.Rows and one column less
|
|
/// than cSurfaceData.Cols
|
|
/// </summary>
|
|
public cPolygon3D GetPolygonAt(int s32_Column, int s32_Row)
|
|
{
|
|
CreatePolygons();
|
|
return mi_PolygonArray[s32_Column, s32_Row];
|
|
}
|
|
|
|
private void CreatePolygons()
|
|
{
|
|
if (mi_PolygonArray != null)
|
|
return;
|
|
|
|
cPolygon3D[,] i_TempArr = new cPolygon3D[ms32_Cols - 1, ms32_Rows - 1];
|
|
|
|
List<cPoint3D> i_Valid = new List<cPoint3D>();
|
|
for (int C = 0; C < ms32_Cols - 1; C++)
|
|
{
|
|
for (int R = 0; R < ms32_Rows - 1; R++)
|
|
{
|
|
i_Valid.Clear();
|
|
if (mi_PointArray[C, R] != null) i_Valid.Add(mi_PointArray[C, R]);
|
|
if (mi_PointArray[C, R + 1] != null) i_Valid.Add(mi_PointArray[C, R + 1]);
|
|
if (mi_PointArray[C + 1, R + 1] != null) i_Valid.Add(mi_PointArray[C + 1, R + 1]);
|
|
if (mi_PointArray[C + 1, R] != null) i_Valid.Add(mi_PointArray[C + 1, R]);
|
|
|
|
if (i_Valid.Count < 4 && !mb_Missing)
|
|
throw new Exception("cSurfaceData: You must call cSurfaceData.SetPointAt() for all points!");
|
|
|
|
if (i_Valid.Count < 3)
|
|
continue; // A polygon needs at least 3 points
|
|
|
|
i_TempArr[C, R] = new cPolygon3D(C, R, null, i_Valid.ToArray());
|
|
}
|
|
}
|
|
|
|
mi_PolygonArray = i_TempArr;
|
|
}
|
|
|
|
// =============================================================================
|
|
|
|
/// <summary>
|
|
/// Called from AddRenderData()
|
|
/// </summary>
|
|
public override void AddDrawObjects(Editor3D i_Inst)
|
|
{
|
|
CreatePolygons();
|
|
|
|
bool b_Added = false;
|
|
List<cPoint3D> i_Used = new List<cPoint3D>();
|
|
|
|
foreach (cPolygon3D i_Poly3D in mi_PolygonArray)
|
|
{
|
|
if (i_Poly3D == null)
|
|
continue;
|
|
|
|
i_Inst.AddDrawObject(new cPolygon(mb_Fill, i_Poly3D, mi_Pen, mi_ColorScheme));
|
|
b_Added = true;
|
|
|
|
foreach (cPoint3D i_Point3D in i_Poly3D.Points)
|
|
{
|
|
if (!i_Used.Contains(i_Point3D))
|
|
i_Used.Add(i_Point3D);
|
|
}
|
|
}
|
|
|
|
// Add all the remaining points as Scatter circles that are not part of a polygon.
|
|
for (int C = 0; C < ms32_Cols; C++)
|
|
{
|
|
for (int R = 0; R < ms32_Rows; R++)
|
|
{
|
|
cPoint3D i_Point3D = mi_PointArray[C, R];
|
|
if (i_Point3D == null || i_Used.Contains(i_Point3D))
|
|
continue;
|
|
|
|
cShape3D i_Shape3D = new cShape3D(C, R, i_Point3D, eScatterShape.Circle, ms32_Radius, null);
|
|
i_Inst.AddDrawObject(new cShape(i_Shape3D, mi_ColorScheme));
|
|
b_Added = true;
|
|
}
|
|
}
|
|
|
|
if (!b_Added)
|
|
throw new Exception("You cannot draw a completely empty SurfaceData");
|
|
}
|
|
}
|
|
|
|
#endregion cSurfaceData
|
|
|
|
#region cScatterData
|
|
|
|
public class cScatterData : cRenderData
|
|
{
|
|
private List<cShape3D> mi_Shapes3D = new List<cShape3D>();
|
|
private cColorScheme mi_ColorScheme;
|
|
|
|
public cShape3D[] AllShapes
|
|
{
|
|
get { return mi_Shapes3D.ToArray(); }
|
|
}
|
|
|
|
public cColorScheme ColorScheme
|
|
{
|
|
get { return mi_ColorScheme; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor If all Scatter shapes contain a valid Brush, you can pass i_ColorScheme
|
|
/// == null here
|
|
/// </summary>
|
|
public cScatterData(cColorScheme i_ColorScheme)
|
|
{
|
|
mi_ColorScheme = i_ColorScheme;
|
|
}
|
|
|
|
public cScatterData()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// s32_Radius defines the size of the shape and i_Brush the color
|
|
/// </summary>
|
|
public cShape3D AddShape(cPoint3D i_Point, eScatterShape e_Shape, int s32_Radius, Brush i_Brush, Object o_Tag = null)
|
|
{
|
|
cShape3D i_Shape3D = new cShape3D(-1, -1, i_Point, e_Shape, s32_Radius, i_Brush, o_Tag);
|
|
mi_Shapes3D.Add(i_Shape3D);
|
|
return i_Shape3D;
|
|
}
|
|
|
|
// =============================================================================
|
|
|
|
/// <summary>
|
|
/// Called from AddRenderData()
|
|
/// </summary>
|
|
public override void AddDrawObjects(Editor3D i_Inst)
|
|
{
|
|
foreach (cShape3D i_Shape3D in mi_Shapes3D)
|
|
{
|
|
i_Inst.AddDrawObject(new cShape(i_Shape3D, mi_ColorScheme));
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion cScatterData
|
|
|
|
#region cLineData
|
|
|
|
public class cLineData : cRenderData
|
|
{
|
|
private List<cLine3D> mi_Lines3D = new List<cLine3D>();
|
|
private cColorScheme mi_ColorScheme;
|
|
|
|
public cLine3D[] AllLines
|
|
{
|
|
get { return mi_Lines3D.ToArray(); }
|
|
}
|
|
|
|
public cColorScheme ColorScheme
|
|
{
|
|
get { return mi_ColorScheme; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor If you use only solid lines and specify a valid Pen, you can pass
|
|
/// i_ColorScheme == null here
|
|
/// </summary>
|
|
public cLineData(cColorScheme i_ColorScheme)
|
|
{
|
|
mi_ColorScheme = i_ColorScheme;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a line which will be drawn entirely in one color.
|
|
/// </summary>
|
|
public cLine3D AddSolidLine(cPoint3D i_Start, cPoint3D i_End, int s32_Width, Pen i_Pen, Object o_Tag = null)
|
|
{
|
|
CheckPenMutable(i_Pen, mi_ColorScheme);
|
|
|
|
cLine3D i_Line3D = new cLine3D(i_Start, i_End, s32_Width, i_Pen, 1, o_Tag);
|
|
mi_Lines3D.Add(i_Line3D);
|
|
return i_Line3D;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a line which will appear with multiple colors of the ColorScheme by drawing it
|
|
/// in multiple parts. If s32_Parts = 50, the line is rendered in 50 parts where each
|
|
/// part has it's own color depending on the Z coordinate.
|
|
/// </summary>
|
|
public cLine3D AddMultiColorLine(int s32_Parts, cPoint3D i_Start, cPoint3D i_End, int s32_Width, Pen i_Pen, Object o_Tag = null)
|
|
{
|
|
if (s32_Parts < 3)
|
|
throw new ArgumentException("Multi color lines require at least 3 parts");
|
|
|
|
if (mi_ColorScheme == null)
|
|
throw new Exception("To create a multi-color line you must specify a ColorScheme");
|
|
|
|
CheckPenMutable(i_Pen, mi_ColorScheme);
|
|
|
|
cLine3D i_Line3D = new cLine3D(i_Start, i_End, s32_Width, i_Pen, s32_Parts, o_Tag);
|
|
mi_Lines3D.Add(i_Line3D);
|
|
return i_Line3D;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates connected lines from the points in the given order
|
|
/// </summary>
|
|
public cLine3D[] AddConnectedLines(List<cPoint3D> i_Points, int s32_Width, Pen i_Pen)
|
|
{
|
|
CheckPenMutable(i_Pen, mi_ColorScheme);
|
|
|
|
List<cLine3D> i_NewLines = new List<cLine3D>();
|
|
|
|
cPoint3D i_Prev = null;
|
|
for (int i = 0; i < i_Points.Count; i++)
|
|
{
|
|
cPoint3D i_Point = i_Points[i];
|
|
if (i_Prev != null)
|
|
{
|
|
cLine3D i_Line3D = new cLine3D(i_Prev, i_Point, s32_Width, i_Pen, 1);
|
|
i_NewLines.Add(i_Line3D);
|
|
mi_Lines3D.Add(i_Line3D);
|
|
}
|
|
i_Prev = i_Point;
|
|
}
|
|
return i_NewLines.ToArray();
|
|
}
|
|
|
|
// =============================================================================
|
|
|
|
/// <summary>
|
|
/// Called from AddRenderData()
|
|
/// </summary>
|
|
public override void AddDrawObjects(Editor3D i_Inst)
|
|
{
|
|
foreach (cLine3D i_Line3D in mi_Lines3D)
|
|
{
|
|
i_Inst.AddDrawObject(new cLine(i_Line3D, mi_ColorScheme));
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion cLineData
|
|
|
|
#region cPolygonData
|
|
|
|
public class cPolygonData : cRenderData
|
|
{
|
|
private bool mb_Fill;
|
|
private Pen mi_Pen;
|
|
private cColorScheme mi_ColorScheme;
|
|
private List<cPolygon3D> mi_Polygons3D = new List<cPolygon3D>();
|
|
|
|
public cPolygon3D[] AllPolygons
|
|
{
|
|
get { return mi_Polygons3D.ToArray(); }
|
|
}
|
|
|
|
public cColorScheme ColorScheme
|
|
{
|
|
get { return mi_ColorScheme; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fill Mode: ------------ Polygons are filled with a color from the ColorScheme. If
|
|
/// you want only one color, set a ColorScheme which contains only one color. The Pen is
|
|
/// used to draw the thin lines between the polygons (mostly black, 1 pixel) If Pen is
|
|
/// null, no lines are drawn.
|
|
///
|
|
/// Line Mode: ------------ Only the border lines of the polygons are drawn. The Pen is
|
|
/// used to draw these lines. The Pen's color and width will be modified.
|
|
/// </summary>
|
|
public cPolygonData(ePolygonMode e_Mode, Pen i_Pen, cColorScheme i_ColorScheme)
|
|
{
|
|
if (e_Mode == ePolygonMode.Lines)
|
|
{
|
|
if (i_Pen == null)
|
|
throw new ArgumentException("In Line mode you must specify a Line Pen");
|
|
|
|
CheckPenMutable(i_Pen, i_ColorScheme);
|
|
}
|
|
|
|
mb_Fill = e_Mode == ePolygonMode.Fill;
|
|
mi_Pen = i_Pen;
|
|
mi_ColorScheme = i_ColorScheme;
|
|
}
|
|
|
|
/// <summary>
|
|
/// In contrast to other drawing libraries (like WPF or Direct3D) you can add polygons
|
|
/// of any dimension here. A polygon can have any amount of corners (minimum 3). The
|
|
/// Brush can be specified in Fill mode. To use a Brush from the ColorScheme set i_Brush
|
|
/// = null.
|
|
/// </summary>
|
|
public cPolygon3D AddPolygon(Brush i_Brush, params cPoint3D[] i_Points3D)
|
|
{
|
|
cPolygon3D i_Polygon3D = new cPolygon3D(-1, -1, i_Brush, i_Points3D);
|
|
mi_Polygons3D.Add(i_Polygon3D);
|
|
return i_Polygon3D;
|
|
}
|
|
|
|
// =============================================================================
|
|
|
|
/// <summary>
|
|
/// Called from AddRenderData()
|
|
/// </summary>
|
|
public override void AddDrawObjects(Editor3D i_Inst)
|
|
{
|
|
foreach (cPolygon3D i_Polygon3D in mi_Polygons3D)
|
|
{
|
|
i_Inst.AddDrawObject(new cPolygon(mb_Fill, i_Polygon3D, mi_Pen, mi_ColorScheme));
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion cPolygonData
|
|
|
|
#region cMessgData
|
|
|
|
public class cMessgData
|
|
{
|
|
private String ms_Text;
|
|
private Brush mi_Brush;
|
|
private int ms32_PosX;
|
|
private int ms32_PosY;
|
|
private Font mi_Font;
|
|
private SizeF mk_Size;
|
|
|
|
/// <summary>
|
|
/// Here you can change the text without loading all the render objects again. The
|
|
/// change will become visible the next time you call Invalidate()
|
|
/// </summary>
|
|
public String Text
|
|
{
|
|
set
|
|
{
|
|
ms_Text = value;
|
|
mk_Size = SizeF.Empty;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Here you can change the text color without loading all the render objects again. The
|
|
/// change will become visible the next time you call Invalidate()
|
|
/// </summary>
|
|
public Color TextColor
|
|
{
|
|
set { mi_Brush = new SolidBrush(value); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// If X is negative, it is displayed right aligned at X pixels from the right If Y is
|
|
/// negative, it is displayed bottom aligned at Y pixels from the bottom
|
|
/// </summary>
|
|
public cMessgData(String s_Text, int X, int Y, Color c_Color,
|
|
FontStyle e_FontStyle = FontStyle.Bold,
|
|
int s32_FontSize = 9,
|
|
String s_FontFace = "Tahoma")
|
|
{
|
|
ms_Text = s_Text;
|
|
ms32_PosX = X;
|
|
ms32_PosY = Y;
|
|
mi_Brush = new SolidBrush(c_Color);
|
|
mi_Font = new Font(s_FontFace, s32_FontSize, e_FontStyle);
|
|
}
|
|
|
|
public void Draw(Graphics i_Graph, Rectangle k_Client)
|
|
{
|
|
if (String.IsNullOrEmpty(ms_Text))
|
|
return;
|
|
|
|
float X = ms32_PosX;
|
|
float Y = ms32_PosY;
|
|
|
|
if (X < 0 || Y < 0)
|
|
{
|
|
// Speed optimization: Measure the size only once.
|
|
if (mk_Size.IsEmpty)
|
|
mk_Size = i_Graph.MeasureString(ms_Text, mi_Font);
|
|
|
|
if (X < 0) X += k_Client.Width - mk_Size.Width;
|
|
if (Y < 0) Y += k_Client.Height - mk_Size.Height;
|
|
}
|
|
|
|
i_Graph.DrawString(ms_Text, mi_Font, mi_Brush, X, Y);
|
|
}
|
|
}
|
|
|
|
#endregion cMessgData
|
|
|
|
// ================= PRIVATE =================
|
|
|
|
#region cPoint2D
|
|
|
|
/// <summary>
|
|
/// This class represents a point in the 2D space, in pixels.
|
|
/// </summary>
|
|
private class cPoint2D
|
|
{
|
|
public double md_X;
|
|
public double md_Y;
|
|
|
|
public cPoint2D()
|
|
{
|
|
}
|
|
|
|
public cPoint2D(double X, double Y)
|
|
{
|
|
md_X = X;
|
|
md_Y = Y;
|
|
}
|
|
|
|
public cPoint2D Clone()
|
|
{
|
|
return new cPoint2D(md_X, md_Y);
|
|
}
|
|
|
|
public PointF Coord
|
|
{
|
|
get { return new PointF((float)md_X, (float)md_Y); }
|
|
}
|
|
|
|
public bool IsValid
|
|
{
|
|
get
|
|
{
|
|
// The screen will always be smaller than 9999 pixels
|
|
return (!Double.IsNaN(md_X) && Math.Abs(md_X) < 9999.9 &&
|
|
!Double.IsNaN(md_Y) && Math.Abs(md_Y) < 9999.9);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Use the good old Pythagoras to calculate the pixel distance between this point and
|
|
/// X, Y.
|
|
/// </summary>
|
|
public int CalcDistanceTo(int X, int Y)
|
|
{
|
|
int s32_DiffX = (int)md_X - X;
|
|
int s32_DiffY = (int)md_Y - Y;
|
|
return (int)Math.Sqrt(s32_DiffX * s32_DiffX + s32_DiffY * s32_DiffY);
|
|
}
|
|
|
|
// For debugging in Visual Studio
|
|
public override string ToString()
|
|
{
|
|
return String.Format("cPoint2D (X={0}, Y={1})", FormatDouble(md_X), FormatDouble(md_Y));
|
|
}
|
|
}
|
|
|
|
#endregion cPoint2D
|
|
|
|
#region cPoint
|
|
|
|
/// <summary>
|
|
/// This class contains the 3D point and it's projection into the 2D space.
|
|
/// </summary>
|
|
private class cPoint
|
|
{
|
|
public cPoint3D mi_P3D;
|
|
public cPoint2D mi_P2D;
|
|
public int ms32_RadiusTip; // if 0 --> no tooltip
|
|
|
|
public cPoint(double d_X, double d_Y, double d_Z)
|
|
{
|
|
mi_P3D = new cPoint3D(d_X, d_Y, d_Z);
|
|
mi_P2D = new cPoint2D();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The radius defines at which distance of the mouse from the 2D point the tooltip pops
|
|
/// up. Radius = 0 --> no tooltip
|
|
/// </summary>
|
|
public cPoint(cPoint3D i_Point3D, int s32_RadiusTip)
|
|
{
|
|
mi_P3D = i_Point3D;
|
|
mi_P2D = new cPoint2D();
|
|
|
|
if (s32_RadiusTip > 0)
|
|
ms32_RadiusTip = s32_RadiusTip + TOOLTIP_RADIUS;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Projects the 3D coordinates into the 2D space (pixels on the screen).
|
|
/// </summary>
|
|
public void Project3D(Editor3D i_Inst, eMirror e_Mirror)
|
|
{
|
|
mi_P2D = i_Inst.mi_Transform.Project3D(mi_P3D, e_Mirror);
|
|
}
|
|
|
|
/// <summary>
|
|
/// For Debugging in Visual Studio
|
|
/// </summary>
|
|
public override string ToString()
|
|
{
|
|
return String.Format("cPoint [{0} = {1}]", mi_P3D, mi_P2D);
|
|
}
|
|
}
|
|
|
|
#endregion cPoint
|
|
|
|
#region cTooltip
|
|
|
|
private class cTooltip
|
|
{
|
|
private Editor3D mi_Inst = null;
|
|
private eTooltip me_Mode = eTooltip.All;
|
|
private ToolTip mi_Tooltip = new ToolTip();
|
|
private List<cPoint> mi_Points = new List<cPoint>();
|
|
private cPoint mi_Last = null;
|
|
|
|
public eTooltip Mode
|
|
{
|
|
get { return me_Mode; }
|
|
set { me_Mode = value; }
|
|
}
|
|
|
|
// Constructor
|
|
public cTooltip(Editor3D i_Inst)
|
|
{
|
|
mi_Inst = i_Inst;
|
|
|
|
mi_Tooltip.AutoPopDelay = 30000; // The maximum that Windows allows are 32.767 seconds (0x7FFF milliseconds)
|
|
mi_Tooltip.InitialDelay = 50;
|
|
mi_Tooltip.ReshowDelay = 50;
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
mi_Points.Clear();
|
|
mi_Last = null;
|
|
Hide();
|
|
}
|
|
|
|
public void AddPoint(cPoint i_Point)
|
|
{
|
|
// ATTENTION adding a && !mi_Points.Contains(i_Point) here would make this function
|
|
// 40 times slower! The more points are already in mi_Points, the slower it would
|
|
// become. Several points will be added multiple times here, but this does not
|
|
// affect the functioning of the tooltip.
|
|
if (i_Point != null && i_Point.ms32_RadiusTip > 0)
|
|
mi_Points.Add(i_Point);
|
|
}
|
|
|
|
public void Hide()
|
|
{
|
|
mi_Tooltip.Hide(mi_Inst);
|
|
}
|
|
|
|
public void OnMouseMove(MouseEventArgs e)
|
|
{
|
|
if (me_Mode == eTooltip.Off)
|
|
{
|
|
Hide();
|
|
return;
|
|
}
|
|
|
|
int s32_MouseX = e.X - mi_Inst.mi_Mouse.mk_OffMove.X - mi_Inst.mi_Mouse.mk_OffCoord.X;
|
|
int s32_MouseY = e.Y - mi_Inst.mi_Mouse.mk_OffMove.Y - mi_Inst.mi_Mouse.mk_OffCoord.Y;
|
|
|
|
int s32_MinDist = int.MaxValue;
|
|
cPoint i_Nearest = null;
|
|
foreach (cPoint i_Point in mi_Points)
|
|
{
|
|
int s32_Dist = i_Point.mi_P2D.CalcDistanceTo(s32_MouseX, s32_MouseY);
|
|
if (s32_Dist < s32_MinDist)
|
|
{
|
|
s32_MinDist = s32_Dist;
|
|
i_Nearest = i_Point;
|
|
}
|
|
}
|
|
|
|
if (i_Nearest != null && s32_MinDist < i_Nearest.ms32_RadiusTip)
|
|
{
|
|
if (mi_Last == i_Nearest)
|
|
return; // The mouse is still over the same point
|
|
|
|
mi_Last = i_Nearest;
|
|
|
|
String s_TT = "";
|
|
if ((me_Mode & eTooltip.Coord) > 0)
|
|
s_TT = String.Format("X = {0}\nY = {1}\nZ = {2}\n", FormatDouble(i_Nearest.mi_P3D.X),
|
|
FormatDouble(i_Nearest.mi_P3D.Y),
|
|
FormatDouble(i_Nearest.mi_P3D.Z));
|
|
|
|
if ((me_Mode & eTooltip.UserText) > 0 && i_Nearest.mi_P3D.Tooltip != null)
|
|
s_TT += i_Nearest.mi_P3D.Tooltip;
|
|
|
|
s_TT = s_TT.Trim();
|
|
if (s_TT.Length > 0)
|
|
{
|
|
mi_Tooltip.Show(s_TT, mi_Inst, e.X + 10, e.Y + 10);
|
|
return;
|
|
}
|
|
}
|
|
|
|
mi_Last = null;
|
|
Hide();
|
|
}
|
|
}
|
|
|
|
#endregion cTooltip
|
|
|
|
#region cBackup
|
|
|
|
private class cBackup
|
|
{
|
|
public Dictionary<eUndoProp, Object[]> mi_Changes = new Dictionary<eUndoProp, Object[]>();
|
|
|
|
public void StoreProperty(eUndoProp e_Property, Object o_OldValue, Object o_NewValue)
|
|
{
|
|
Object[] o_Values;
|
|
if (mi_Changes.TryGetValue(e_Property, out o_Values))
|
|
{
|
|
// The property has changed multiple times (dragging) --> store only the last value
|
|
o_Values[1] = o_NewValue;
|
|
}
|
|
else
|
|
{
|
|
// The property has changed for the first time
|
|
mi_Changes.Add(e_Property, new Object[] { o_OldValue, o_NewValue });
|
|
}
|
|
}
|
|
|
|
public void Restore(cObject3D i_Object3D, bool b_Undo)
|
|
{
|
|
foreach (KeyValuePair<eUndoProp, Object[]> i_Pair in mi_Changes)
|
|
{
|
|
if (b_Undo) i_Object3D.Restore(i_Pair.Key, i_Pair.Value[0]); // Undo: old value
|
|
else i_Object3D.Restore(i_Pair.Key, i_Pair.Value[1]); // Redo: new value
|
|
}
|
|
}
|
|
|
|
// for debugging in Visual Studio
|
|
public override string ToString()
|
|
{
|
|
StringBuilder i_Dbg = new StringBuilder();
|
|
foreach (eUndoProp e_Property in mi_Changes.Keys)
|
|
{
|
|
if (i_Dbg.Length > 0) i_Dbg.Append(", ");
|
|
i_Dbg.Append(e_Property);
|
|
}
|
|
return "cBackup Properties: " + i_Dbg.ToString();
|
|
}
|
|
}
|
|
|
|
#endregion cBackup
|
|
|
|
#region cUndoEntry
|
|
|
|
private class cUndoEntry
|
|
{
|
|
public Dictionary<cObject3D, cBackup> mi_Backups = new Dictionary<cObject3D, cBackup>();
|
|
public List<cDrawObj> mi_Added = new List<cDrawObj>();
|
|
public List<cDrawObj> mi_Removed = new List<cDrawObj>();
|
|
|
|
public bool IsEmpty
|
|
{
|
|
get { return mi_Backups.Count == 0 && mi_Added.Count == 0 && mi_Removed.Count == 0; }
|
|
}
|
|
|
|
// for debugging in Visual Studio
|
|
public override string ToString()
|
|
{
|
|
return String.Format("Backups: {0}, Added: {1}, Removed: {2}", (mi_Backups == null) ? 0 : mi_Backups.Count,
|
|
(mi_Added == null) ? 0 : mi_Added.Count,
|
|
(mi_Removed == null) ? 0 : mi_Removed.Count);
|
|
}
|
|
}
|
|
|
|
#endregion cUndoEntry
|
|
|
|
#region cUndoImpl
|
|
|
|
/// <summary>
|
|
/// This private class stores the changes after each edit operation that the user has made.
|
|
/// </summary>
|
|
private class cUndoImpl : cUndoBuffer
|
|
{
|
|
private Editor3D mi_Inst;
|
|
private int ms32_UndoIdx = 0; // Points to the next Undo item in mi_UndoList
|
|
private int ms32_RedoIdx = 0; // Points to the next Redo item in mi_UndoList
|
|
private bool mb_Enabled = false; // The Undo Buffer is by default disabled
|
|
private bool mb_DoInit = false; // After Clear() execute Init() once.
|
|
private bool mb_Pending = false; // User actions are pending that have not yet been stored
|
|
private cUndoEntry mi_CurUndo = new cUndoEntry();
|
|
private List<cUndoEntry> mi_UndoList = new List<cUndoEntry>();
|
|
|
|
/// <summary>
|
|
/// Do not enable the Undo buffer if you don't need the Undo functionality. By default
|
|
/// it is disabled.
|
|
/// </summary>
|
|
public override bool Enabled
|
|
{
|
|
get { return mb_Enabled; }
|
|
set
|
|
{
|
|
Clear();
|
|
mb_Enabled = value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
public cUndoImpl(Editor3D i_Inst)
|
|
{
|
|
mi_Inst = i_Inst;
|
|
}
|
|
|
|
public override void Clear()
|
|
{
|
|
ms32_UndoIdx = 0;
|
|
ms32_RedoIdx = 0;
|
|
mb_DoInit = true;
|
|
mb_Pending = false;
|
|
mi_CurUndo = new cUndoEntry();
|
|
mi_UndoList.Clear();
|
|
}
|
|
|
|
/// <summary>
|
|
/// This must only be called from Editor3D.Render() ! The first time calling
|
|
/// Invalidate() after Clear() --> clear all draw objets that have been added.
|
|
/// Otherwise all added objects would be stored. (This class stores only user changes.)
|
|
/// </summary>
|
|
public void Init()
|
|
{
|
|
if (mb_DoInit)
|
|
{
|
|
Clear();
|
|
mb_DoInit = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when any public property of a 3D object was changed (e.g. X,Y,Z coordinates
|
|
/// of a cPoint3D). These changes will be stored when Store() is called.
|
|
/// </summary>
|
|
public void Backup(cObject3D i_Object3D, eUndoProp e_Property, Object o_OldValue, Object o_NewValue)
|
|
{
|
|
if (!mb_Enabled)
|
|
return;
|
|
|
|
cBackup i_Backup;
|
|
if (!mi_CurUndo.mi_Backups.TryGetValue(i_Object3D, out i_Backup))
|
|
{
|
|
i_Backup = new cBackup();
|
|
mi_CurUndo.mi_Backups.Add(i_Object3D, i_Backup);
|
|
}
|
|
|
|
i_Backup.StoreProperty(e_Property, o_OldValue, o_NewValue);
|
|
mb_Pending = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the user removes a draw object. These changes will be stored when
|
|
/// Store() is called.
|
|
/// </summary>
|
|
public void DrawObjectRemoved(cDrawObj i_DrawObject)
|
|
{
|
|
if (!mb_Enabled)
|
|
return;
|
|
|
|
mi_CurUndo.mi_Removed.Add(i_DrawObject);
|
|
mb_Pending = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the user adds a draw object. These changes will be stored when Store()
|
|
/// is called.
|
|
/// </summary>
|
|
public void DrawObjectAdded(cDrawObj i_DrawObject)
|
|
{
|
|
if (!mb_Enabled)
|
|
return;
|
|
|
|
mi_CurUndo.mi_Added.Add(i_DrawObject);
|
|
mb_Pending = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Store the changes on user objects in the Undo buffer on mouse-up.
|
|
/// </summary>
|
|
public void Store()
|
|
{
|
|
if (!mb_Enabled || !mb_Pending || mi_CurUndo.IsEmpty)
|
|
return;
|
|
|
|
// Remove any entries in the Undo Buffer behind the current index
|
|
int s32_Remove = mi_UndoList.Count - ms32_UndoIdx - 1;
|
|
if (s32_Remove > 0)
|
|
mi_UndoList.RemoveRange(ms32_UndoIdx + 1, s32_Remove);
|
|
|
|
mi_UndoList.Add(mi_CurUndo);
|
|
mi_CurUndo = new cUndoEntry();
|
|
|
|
ms32_UndoIdx = mi_UndoList.Count - 1;
|
|
ms32_RedoIdx = mi_UndoList.Count - 1;
|
|
mb_Pending = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is also called when hitting CTRL + Z while the 3D Editor has the keyboard focus
|
|
/// </summary>
|
|
public override bool Undo()
|
|
{
|
|
Debug.Assert(!mi_Inst.InvokeRequired); // Call only from GUI thread
|
|
|
|
if (!mb_Enabled || mi_UndoList.Count == 0 || ms32_UndoIdx < 0)
|
|
return false;
|
|
|
|
Restore(true, ms32_UndoIdx);
|
|
|
|
ms32_RedoIdx = ms32_UndoIdx;
|
|
ms32_UndoIdx--;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is also called when hitting CTRL + Y while the 3D Editor has the keyboard focus
|
|
/// </summary>
|
|
public override bool Redo()
|
|
{
|
|
Debug.Assert(!mi_Inst.InvokeRequired); // Call only from GUI thread
|
|
|
|
if (!mb_Enabled || mi_UndoList.Count == 0 || ms32_RedoIdx >= mi_UndoList.Count)
|
|
return false;
|
|
|
|
Restore(false, ms32_RedoIdx);
|
|
|
|
ms32_UndoIdx = ms32_RedoIdx;
|
|
ms32_RedoIdx++;
|
|
return true;
|
|
}
|
|
|
|
private void Restore(bool b_Undo, int s32_Index)
|
|
{
|
|
s32_Index = Math.Max(s32_Index, 0);
|
|
s32_Index = Math.Min(s32_Index, mi_UndoList.Count - 1);
|
|
|
|
cUndoEntry i_UndoItem = mi_UndoList[s32_Index];
|
|
|
|
foreach (cDrawObj i_Removed in i_UndoItem.mi_Removed)
|
|
{
|
|
if (b_Undo) mi_Inst.mi_UserObjects.Add(i_Removed);
|
|
else mi_Inst.mi_UserObjects.Remove(i_Removed);
|
|
|
|
mi_Inst.me_Recalculate |= eRecalculate.AddRemove | eRecalculate.Objects | eRecalculate.CoordSystem;
|
|
}
|
|
|
|
foreach (cDrawObj i_Added in i_UndoItem.mi_Added)
|
|
{
|
|
if (b_Undo) mi_Inst.mi_UserObjects.Remove(i_Added);
|
|
else mi_Inst.mi_UserObjects.Add(i_Added);
|
|
|
|
mi_Inst.me_Recalculate |= eRecalculate.AddRemove | eRecalculate.Objects | eRecalculate.CoordSystem;
|
|
}
|
|
|
|
foreach (KeyValuePair<cObject3D, cBackup> i_Pair in i_UndoItem.mi_Backups)
|
|
{
|
|
i_Pair.Value.Restore(i_Pair.Key, b_Undo);
|
|
|
|
mi_Inst.me_Recalculate |= eRecalculate.Objects | eRecalculate.CoordSystem;
|
|
}
|
|
|
|
mi_Inst.Invalidate();
|
|
}
|
|
}
|
|
|
|
#endregion cUndoImpl
|
|
|
|
// ----- DrawObjects -----
|
|
|
|
#region cDrawObj
|
|
|
|
/// <summary>
|
|
/// Base class for cLine, cShape, cPolygon
|
|
/// </summary>
|
|
private abstract class cDrawObj : IComparable
|
|
{
|
|
public Editor3D mi_Inst;
|
|
public SmoothingMode me_SmoothMode;
|
|
public cPoint[] mi_Points;
|
|
public cObject3D mi_Object3D; // This is a cLine3D, cShape3D or cPolygon3D
|
|
public double md_Sort; // sorting is important. Always draw first back, then front objects.
|
|
public bool mb_IsAxis; // This is a line from the coordinate system
|
|
protected double md_AvrgZ; // 3D center of the Z coordinates of all points in this object
|
|
protected bool mb_IsValid;
|
|
protected eMirror me_Mirror;
|
|
|
|
// --------------------------------------------------
|
|
|
|
/// <summary>
|
|
/// Set after conversion 3D --> 2D. If invalid coordinates are found (NaN or >
|
|
/// 9999) this returns false. If projection results in lines of thousands of pixels
|
|
/// length, the drawing will become extremely slow. Do not draw lines or polygons
|
|
/// outside the screen area.
|
|
/// </summary>
|
|
public bool IsValid
|
|
{
|
|
get { return mb_IsValid; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The object is selected
|
|
/// </summary>
|
|
public bool Selected
|
|
{
|
|
get { return mi_Object3D.Selected; }
|
|
set { mi_Object3D.Selected = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// The type of this object
|
|
/// </summary>
|
|
public virtual eSelType SelType
|
|
{
|
|
get { throw new NotImplementedException(); }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates colors from color scheme
|
|
/// </summary>
|
|
public virtual void ProcessColors()
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates the 2D screen coordinates for each 3D point.
|
|
/// </summary>
|
|
public virtual void Project3D()
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draw into Graphics
|
|
/// </summary>
|
|
public virtual void Render(Graphics i_Graph)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if a user click at mouse point X, Y matches this draw object
|
|
/// </summary>
|
|
public virtual cObject3D MatchesPoint2D(int X, int Y)
|
|
{
|
|
throw new NotImplementedException();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Uses the center of a draw object (e.g. the middle of a line) to calculate in which
|
|
/// order the draw objects are rendered. This is called for all user defined objects,
|
|
/// but not for the coordinate system which has it's own logic.
|
|
/// </summary>
|
|
public void CalcSortOrder()
|
|
{
|
|
Debug.Assert(!mb_IsAxis); // axes have their own value for md_Sort
|
|
|
|
double d_AvrgX = 0.0;
|
|
double d_AvrgY = 0.0;
|
|
md_AvrgZ = 0.0;
|
|
|
|
foreach (cPoint i_Point in mi_Points)
|
|
{
|
|
d_AvrgX += i_Point.mi_P3D.X;
|
|
d_AvrgY += i_Point.mi_P3D.Y;
|
|
md_AvrgZ += i_Point.mi_P3D.Z;
|
|
}
|
|
|
|
d_AvrgX /= mi_Points.Length;
|
|
d_AvrgY /= mi_Points.Length;
|
|
md_AvrgZ /= mi_Points.Length;
|
|
|
|
if (mi_Object3D.Row > -1) // only Polygons and Scatter circles created by cSurfaceData
|
|
{
|
|
int X = mi_Object3D.Column + 1;
|
|
int Y = mi_Object3D.Row + 1;
|
|
|
|
// Mirror axis values increasing / decreasing
|
|
if (mi_Inst.AxisX.Mirror && (me_Mirror & eMirror.X) > 0) X = 5000 - mi_Object3D.Column;
|
|
if (mi_Inst.AxisY.Mirror && (me_Mirror & eMirror.Y) > 0) Y = 5000 - mi_Object3D.Row;
|
|
|
|
// In case of a surface grid the Z value must be ignored because sorting is
|
|
// ALWAYS based on the position in the grid. Using the Z value here may even
|
|
// result in wrong sort order.
|
|
md_Sort = mi_Inst.mi_Transform.ProjectXY(X, Y);
|
|
}
|
|
else
|
|
{
|
|
double X = d_AvrgX;
|
|
double Y = d_AvrgY;
|
|
double Z = md_AvrgZ;
|
|
|
|
// Mirror axis values increasing / decreasing
|
|
if (mi_Inst.AxisX.Mirror && (me_Mirror & eMirror.X) > 0) X = mi_Inst.mi_Bounds.X.Max - (X - mi_Inst.mi_Bounds.X.Min);
|
|
if (mi_Inst.AxisY.Mirror && (me_Mirror & eMirror.Y) > 0) Y = mi_Inst.mi_Bounds.Y.Max - (Y - mi_Inst.mi_Bounds.Y.Min);
|
|
if (mi_Inst.AxisZ.Mirror && (me_Mirror & eMirror.Z) > 0) Z = mi_Inst.mi_Bounds.Z.Max - (Z - mi_Inst.mi_Bounds.Z.Min);
|
|
|
|
// In case of any other 3D object the Z value must also be included in the
|
|
// calculation to avoid artifacts. Demo Sphere shows that the Z value is
|
|
// required if you move Theta to an extreme.
|
|
X = (X - mi_Inst.mi_Transform.mi_Center3D.X) * mi_Inst.mi_Transform.md_NormalizeX;
|
|
Y = (Y - mi_Inst.mi_Transform.mi_Center3D.Y) * mi_Inst.mi_Transform.md_NormalizeY;
|
|
Z = (Z - mi_Inst.mi_Transform.mi_Center3D.Z) * mi_Inst.mi_Transform.md_NormalizeZ;
|
|
|
|
md_Sort = mi_Inst.mi_Transform.ProjectXY(X, Y, Z);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used for sorting all DrawObjects from back to front
|
|
/// </summary>
|
|
int IComparable.CompareTo(Object o_Comp)
|
|
{
|
|
return md_Sort.CompareTo(((cDrawObj)o_Comp).md_Sort);
|
|
}
|
|
}
|
|
|
|
#endregion cDrawObj
|
|
|
|
#region cLine
|
|
|
|
private class cLine : cDrawObj
|
|
{
|
|
private cLine3D mi_Line3D; // object passed to and from the user
|
|
private Pen mi_Pen;
|
|
private Brush mi_Brush; // assigned to Pen
|
|
private float mf_LineWidth; // Linewidth with zoom factor
|
|
private float mf_SelSize; // size of selection points
|
|
|
|
// -------- coordinate axes --------
|
|
public double md_Angle; // needed to calculate current rotation quadrant of coordinate axis
|
|
|
|
public eCoord me_Line = eCoord.Invalid; // main coordinate in coordinate direction
|
|
public eCoord me_Offset = eCoord.Invalid; // secondary coordinate in coordinate direction
|
|
public String ms_Label; // Label for axis
|
|
|
|
// ---------- multicolor -----------
|
|
private cPoint[] mi_ColorPoints; // all points on the line that are drawn separately
|
|
|
|
private Brush[] mi_ColorBrushes; // all Brushes which are assigned to the Pen
|
|
private cColorScheme mi_ColorScheme;
|
|
|
|
public override eSelType SelType
|
|
{
|
|
get { return eSelType.Line; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor 1 for coordinate system. LineWidth is always 1.
|
|
/// </summary>
|
|
public cLine(Editor3D i_Inst, eCoord e_Line, eCoord e_Offset, eMirror e_Mirror)
|
|
{
|
|
if (e_Line == e_Offset) mi_Pen = i_Inst.mi_Axis[(int)e_Line].AxisPen; // Main axis
|
|
else mi_Pen = i_Inst.mi_Axis[(int)e_Offset].RasterPen; // Raster line
|
|
|
|
mi_Inst = i_Inst;
|
|
me_Line = e_Line;
|
|
me_Offset = e_Offset;
|
|
mi_Brush = mi_Pen.Brush;
|
|
mi_Points = new cPoint[2];
|
|
mi_Points[0] = new cPoint(0, 0, 0);
|
|
mi_Points[1] = new cPoint(0, 0, 0);
|
|
mb_IsAxis = true;
|
|
me_Mirror = e_Mirror;
|
|
me_SmoothMode = SmoothingMode.AntiAlias;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor 2 for user lines if i_Line3D.Pen == null --> Pen from ColorScheme is
|
|
/// used if i_Line3D.Pen != null --> ColorScheme will be ignored, even if a
|
|
/// ColorScheme is specified An indvidual LineWidth can be defined in each cLine3D.
|
|
/// </summary>
|
|
public cLine(cLine3D i_Line3D, cColorScheme i_ColorScheme)
|
|
{
|
|
if (i_Line3D.Pen == null && i_ColorScheme == null)
|
|
throw new ArgumentException("You must specify a Pen or a ColorScheme");
|
|
|
|
mi_ColorScheme = i_ColorScheme;
|
|
mi_Line3D = i_Line3D;
|
|
mi_Object3D = i_Line3D;
|
|
mi_Pen = i_Line3D.Pen; // get user's Pen or null
|
|
mi_Points = new cPoint[2];
|
|
mi_Points[0] = new cPoint(i_Line3D.Points[0], 1);
|
|
mi_Points[1] = new cPoint(i_Line3D.Points[1], 1);
|
|
mb_IsAxis = false;
|
|
me_Mirror = eMirror.All;
|
|
me_SmoothMode = SmoothingMode.AntiAlias;
|
|
|
|
if (mi_Pen != null)
|
|
{
|
|
// The original Brush must be stored separately because the same Pen may be used
|
|
// for multiple instances of cLine Changing the color of one line would affect
|
|
// all the others.
|
|
mi_Brush = mi_Pen.Brush;
|
|
}
|
|
else
|
|
{
|
|
mi_Pen = new Pen(Brushes.Black); // color and width will be changed below
|
|
mi_Brush = null; // Brush will be taken from colorscheme
|
|
}
|
|
mi_Pen.StartCap = LineCap.Round;
|
|
mi_Pen.EndCap = LineCap.Round;
|
|
|
|
// ---------- multi color ---------
|
|
|
|
if (i_Line3D.ColorParts > 1)
|
|
{
|
|
mi_ColorBrushes = new Brush[i_Line3D.ColorParts];
|
|
mi_ColorPoints = new cPoint[i_Line3D.ColorParts];
|
|
}
|
|
}
|
|
|
|
public override void ProcessColors()
|
|
{
|
|
Debug.Assert(!mb_IsAxis); // axes have their own color
|
|
|
|
// If the user has changed the Pen --> use the new Pen and it's Brush
|
|
if (mi_Line3D.Pen != null)
|
|
{
|
|
mi_Pen = mi_Line3D.Pen;
|
|
mi_Brush = mi_Pen.Brush;
|
|
|
|
mi_Pen.StartCap = LineCap.Round;
|
|
mi_Pen.EndCap = LineCap.Round;
|
|
}
|
|
|
|
if (mi_ColorPoints != null) // multicolor line
|
|
{
|
|
double d_X = mi_Points[0].mi_P3D.X;
|
|
double d_Y = mi_Points[0].mi_P3D.Y;
|
|
double d_Z = mi_Points[0].mi_P3D.Z;
|
|
|
|
double d_DeltaX = (mi_Points[1].mi_P3D.X - d_X) / mi_ColorPoints.Length;
|
|
double d_DeltaY = (mi_Points[1].mi_P3D.Y - d_Y) / mi_ColorPoints.Length;
|
|
double d_DeltaZ = (mi_Points[1].mi_P3D.Z - d_Z) / mi_ColorPoints.Length;
|
|
|
|
mi_ColorPoints[0] = mi_Points[0]; // Set Start Point
|
|
|
|
cPoint i_Prev = mi_Points[0];
|
|
for (int i = 1; i < mi_ColorPoints.Length; i++)
|
|
{
|
|
d_X += d_DeltaX;
|
|
d_Y += d_DeltaY;
|
|
d_Z += d_DeltaZ;
|
|
|
|
cPoint i_Point = new cPoint(new cPoint3D(d_X, d_Y, d_Z), 0);
|
|
|
|
double d_AvrgZ = (i_Prev.mi_P3D.Z + i_Point.mi_P3D.Z) / 2.0;
|
|
double d_FactorZ = mi_Inst.mi_Bounds.CalcFactorZ(d_AvrgZ);
|
|
int s32_Index = mi_ColorScheme.CalcIndex(d_FactorZ);
|
|
mi_ColorBrushes[i] = mi_ColorScheme.GetBrush(s32_Index);
|
|
mi_ColorPoints[i] = i_Point;
|
|
|
|
i_Prev = i_Point;
|
|
}
|
|
|
|
mi_ColorPoints[mi_ColorPoints.Length - 1] = mi_Points[1]; // Set End Point
|
|
}
|
|
else // solid line
|
|
{
|
|
if (mi_ColorScheme != null)
|
|
{
|
|
// Load missing Pen from ColorScheme which the user has not specified.
|
|
double d_FactorZ = mi_Inst.mi_Bounds.CalcFactorZ(md_AvrgZ);
|
|
int s32_Index = mi_ColorScheme.CalcIndex(d_FactorZ);
|
|
mi_Brush = mi_ColorScheme.GetBrush(s32_Index);
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void Project3D()
|
|
{
|
|
cPoint[] i_PointArr = (mi_ColorPoints != null) ? mi_ColorPoints : mi_Points;
|
|
foreach (cPoint i_Point in i_PointArr)
|
|
{
|
|
i_Point.Project3D(mi_Inst, me_Mirror);
|
|
}
|
|
|
|
mb_IsValid = mi_Points[0].mi_P2D.IsValid && mi_Points[1].mi_P2D.IsValid;
|
|
|
|
if (!mb_IsAxis)
|
|
{
|
|
mf_LineWidth = (float)(mi_Line3D.Width * mi_Inst.mi_Transform.md_Zoom);
|
|
// Diameter of circle for selected points
|
|
mf_SelSize = (float)(Math.Max(6, mi_Line3D.Width * 2) * mi_Inst.mi_Transform.md_Zoom);
|
|
}
|
|
}
|
|
|
|
public override void Render(Graphics i_Graph)
|
|
{
|
|
// b_LineSel depends on the selection of the line only. It does not matter if an end
|
|
// point is selected or not.
|
|
bool b_LineSel = !mb_IsAxis && Selected && mi_Inst.mi_Selection.HighlightPen != null;
|
|
|
|
if (mi_ColorPoints == null || b_LineSel) // draw solid line
|
|
{
|
|
Pen i_DrawPen;
|
|
if (b_LineSel)
|
|
{
|
|
i_DrawPen = mi_Inst.mi_Selection.HighlightPen;
|
|
}
|
|
else
|
|
{
|
|
i_DrawPen = mi_Pen;
|
|
i_DrawPen.Brush = mi_Brush; // mi_Brush = null for multicolor lines!
|
|
}
|
|
|
|
// Axis lines have always 1 pixel width
|
|
if (!mb_IsAxis)
|
|
i_DrawPen.Width = mf_LineWidth;
|
|
|
|
i_Graph.DrawLine(i_DrawPen, mi_Points[0].mi_P2D.Coord, mi_Points[1].mi_P2D.Coord);
|
|
}
|
|
else // multi color
|
|
{
|
|
mi_Pen.Width = mf_LineWidth;
|
|
|
|
cPoint i_Prev = mi_ColorPoints[0];
|
|
for (int i = 1; i < mi_ColorPoints.Length; i++)
|
|
{
|
|
cPoint i_Point = mi_ColorPoints[i];
|
|
mi_Pen.Brush = mi_ColorBrushes[i];
|
|
i_Graph.DrawLine(mi_Pen, i_Prev.mi_P2D.Coord, i_Point.mi_P2D.Coord);
|
|
|
|
i_Prev = i_Point;
|
|
}
|
|
}
|
|
|
|
// Draw circle for selected points
|
|
foreach (cPoint i_Point in mi_Points)
|
|
{
|
|
if (!i_Point.mi_P3D.Selected)
|
|
continue;
|
|
|
|
float X = (float)i_Point.mi_P2D.md_X - mf_SelSize / 2.0f;
|
|
float Y = (float)i_Point.mi_P2D.md_Y - mf_SelSize / 2.0f;
|
|
i_Graph.FillEllipse(mi_Inst.mi_Selection.HighlightBrush, X, Y, mf_SelSize, mf_SelSize);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if a user click at X, Y matches this draw object
|
|
/// </summary>
|
|
public override cObject3D MatchesPoint2D(int X, int Y)
|
|
{
|
|
if (mb_IsAxis)
|
|
return null; // do not allow to select axis lines
|
|
|
|
int s32_MaxDist = Math.Max(1, (int)mi_Pen.Width / 2) + SELECT_RADIUS;
|
|
|
|
if (mi_Inst.Selection.SinglePoints)
|
|
{
|
|
foreach (cPoint i_Point in mi_Points)
|
|
{
|
|
if (i_Point.mi_P2D.CalcDistanceTo(X, Y) <= s32_MaxDist)
|
|
return i_Point.mi_P3D;
|
|
}
|
|
}
|
|
else // select entire line
|
|
{
|
|
if (IsPointOnLine(mi_Points[0].mi_P2D, mi_Points[1].mi_P2D, X, Y, s32_MaxDist))
|
|
return mi_Line3D;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// ---------------- Coord System ---------------
|
|
|
|
/// <summary>
|
|
/// Used while creating coordinate system Check if 2 lines have the same coordinates.
|
|
/// </summary>
|
|
public bool CoordEquals(cLine i_Line)
|
|
{
|
|
return mi_Points[0].mi_P3D.CoordEquals(i_Line.mi_Points[0].mi_P3D) &&
|
|
mi_Points[1].mi_P3D.CoordEquals(i_Line.mi_Points[1].mi_P3D);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used while creating coordinate system Calculate the angle of the 3 main axes on the
|
|
/// screen in a range from 0 to 360 degree.
|
|
/// </summary>
|
|
public void CalcAngle2D()
|
|
{
|
|
double d_DX = mi_Points[1].mi_P2D.md_X - mi_Points[0].mi_P2D.md_X;
|
|
double d_DY = mi_Points[1].mi_P2D.md_Y - mi_Points[0].mi_P2D.md_Y;
|
|
md_Angle = Math.Atan2(d_DY, d_DX) * 180.0 / Math.PI;
|
|
if (md_Angle < 0.0) md_Angle += 360.0;
|
|
}
|
|
|
|
// For debugging in Visual Studio
|
|
public override string ToString()
|
|
{
|
|
String s_Dbg = String.Format("cLine from {0} to {1}", mi_Points[0], mi_Points[1]);
|
|
if (mb_IsAxis)
|
|
s_Dbg += String.Format(" (Axis {0}, {1})", me_Line, me_Offset);
|
|
|
|
return s_Dbg;
|
|
}
|
|
}
|
|
|
|
#endregion cLine
|
|
|
|
#region cShape
|
|
|
|
private class cShape : cDrawObj
|
|
{
|
|
private float mf_Radius; // radius of shape adapted with Zoom factor
|
|
private float mf_Diameter; // diameter of shape adapted with Zoom factor
|
|
private PointF mk_TopLeft; // top left corner in screen coordinates for all types of shapes
|
|
private PointF[] mk_Polygon; // used for triangles or any future user objects
|
|
private Brush mi_Brush;
|
|
private cShape3D mi_Shape3D;
|
|
private cColorScheme mi_ColorScheme;
|
|
|
|
public override eSelType SelType
|
|
{
|
|
get { return eSelType.Shape; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
public cShape(cShape3D i_Shape3D, cColorScheme i_ColorScheme)
|
|
{
|
|
if (i_Shape3D.Brush == null && i_ColorScheme == null)
|
|
throw new ArgumentException("You must specify a Brush or a ColorScheme");
|
|
|
|
mi_Shape3D = i_Shape3D;
|
|
mi_Object3D = i_Shape3D;
|
|
mi_Points = new cPoint[1];
|
|
mi_Points[0] = new cPoint(i_Shape3D.Points[0], i_Shape3D.Radius);
|
|
mi_ColorScheme = i_ColorScheme;
|
|
me_SmoothMode = SmoothingMode.AntiAlias;
|
|
me_Mirror = eMirror.All;
|
|
}
|
|
|
|
public override void ProcessColors()
|
|
{
|
|
// If the user has specified an individual brush for this Shape --> always use it
|
|
mi_Brush = mi_Shape3D.Brush;
|
|
|
|
// Otherwise use Brush from ColorScheme
|
|
if (mi_Brush == null)
|
|
{
|
|
double d_FactorZ = mi_Inst.mi_Bounds.CalcFactorZ(md_AvrgZ);
|
|
int s32_Index = mi_ColorScheme.CalcIndex(d_FactorZ);
|
|
mi_Brush = mi_ColorScheme.GetBrush(s32_Index);
|
|
}
|
|
}
|
|
|
|
public override void Project3D()
|
|
{
|
|
mi_Points[0].Project3D(mi_Inst, me_Mirror);
|
|
|
|
mb_IsValid = mi_Points[0].mi_P2D.IsValid;
|
|
mf_Radius = (float)(mi_Shape3D.Radius * mi_Inst.mi_Transform.md_Zoom);
|
|
mf_Diameter = mf_Radius * 2.0f;
|
|
|
|
// Move coordinate from center to upper left corner of circle
|
|
mk_TopLeft = mi_Points[0].mi_P2D.Coord;
|
|
mk_TopLeft.X -= mf_Radius;
|
|
mk_TopLeft.Y -= mf_Radius;
|
|
|
|
switch (mi_Shape3D.Shape)
|
|
{
|
|
case eScatterShape.Triangle:
|
|
mk_Polygon = new PointF[3];
|
|
// top center
|
|
mk_Polygon[0].X = mk_TopLeft.X + mf_Radius;
|
|
mk_Polygon[0].Y = mk_TopLeft.Y;
|
|
// bottom left
|
|
mk_Polygon[1].X = mk_TopLeft.X;
|
|
mk_Polygon[1].Y = mk_TopLeft.Y + mf_Diameter;
|
|
// bottom right
|
|
mk_Polygon[2].X = mk_TopLeft.X + mf_Diameter;
|
|
mk_Polygon[2].Y = mk_TopLeft.Y + mf_Diameter;
|
|
break;
|
|
|
|
// case eScatterShape.Star: Here you can implement your own shapes break;
|
|
}
|
|
}
|
|
|
|
public override void Render(Graphics i_Graph)
|
|
{
|
|
bool b_DrawSel = Selected && mi_Inst.mi_Selection.HighlightBrush != null;
|
|
Brush i_DrawBrush = b_DrawSel ? mi_Inst.mi_Selection.HighlightBrush : mi_Brush;
|
|
|
|
switch (mi_Shape3D.Shape)
|
|
{
|
|
case eScatterShape.Circle:
|
|
i_Graph.FillEllipse(i_DrawBrush, mk_TopLeft.X, mk_TopLeft.Y, mf_Diameter, mf_Diameter);
|
|
break;
|
|
|
|
case eScatterShape.Square:
|
|
i_Graph.FillRectangle(i_DrawBrush, mk_TopLeft.X, mk_TopLeft.Y, mf_Diameter, mf_Diameter);
|
|
break;
|
|
|
|
default:
|
|
i_Graph.FillPolygon(i_DrawBrush, mk_Polygon);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if a user click at X, Y matches this draw object
|
|
/// </summary>
|
|
public override cObject3D MatchesPoint2D(int X, int Y)
|
|
{
|
|
int s32_MaxDist = (int)mf_Radius + SELECT_RADIUS;
|
|
|
|
if (mi_Points[0].mi_P2D.CalcDistanceTo(X, Y) <= s32_MaxDist)
|
|
return mi_Shape3D;
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// For Debugging in Visual Studio
|
|
/// </summary>
|
|
public override string ToString()
|
|
{
|
|
return String.Format("cShape {0} at {1}, Diameter {2}", mi_Shape3D.Shape, mi_Points[0], FormatDouble(mf_Diameter));
|
|
}
|
|
}
|
|
|
|
#endregion cShape
|
|
|
|
#region cPolygon
|
|
|
|
private class cPolygon : cDrawObj
|
|
{
|
|
private bool mb_Fill; // Fill / Line mode
|
|
private float mf_SelSize; // size of selection points
|
|
private PointF[] mk_Screen; // the 2D polygon corner points in screen coordinates
|
|
private int ms32_OrgWidth; // original line width for Line Pen
|
|
private float mf_LineWidth; // zoomed line width for Line Pen
|
|
private Pen mi_LinePen; // used in Line mode
|
|
private Pen mi_BorderPen; // used in Fill mode (not zoomed)
|
|
private Brush mi_Brush; // used in Fill mode
|
|
private cPolygon3D mi_Polygon3D;
|
|
private cColorScheme mi_ColorScheme;
|
|
|
|
public override eSelType SelType
|
|
{
|
|
get { return eSelType.Polygon; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
public cPolygon(bool b_Fill, cPolygon3D i_Polygon3D, Pen i_Pen, cColorScheme i_ColorScheme)
|
|
{
|
|
mi_Points = new cPoint[i_Polygon3D.Points.Length];
|
|
for (int i = 0; i < mi_Points.Length; i++)
|
|
{
|
|
mi_Points[i] = new cPoint(i_Polygon3D.Points[i], 1);
|
|
}
|
|
|
|
mb_Fill = b_Fill;
|
|
mi_Polygon3D = i_Polygon3D;
|
|
mi_Object3D = i_Polygon3D;
|
|
mi_ColorScheme = i_ColorScheme;
|
|
mk_Screen = new PointF[mi_Points.Length];
|
|
me_Mirror = eMirror.All;
|
|
|
|
if (b_Fill) mi_BorderPen = i_Pen;
|
|
else mi_LinePen = i_Pen;
|
|
|
|
if (i_Pen != null)
|
|
ms32_OrgWidth = (int)i_Pen.Width;
|
|
|
|
// Drawing polygon border lines with antialias makes them very thick and black. Do
|
|
// not smooth the lines when the polygons are filled with color and the lines are
|
|
// only separators. But use smooth mode if only the lines are drawn.
|
|
me_SmoothMode = (b_Fill) ? SmoothingMode.None : SmoothingMode.AntiAlias;
|
|
}
|
|
|
|
public override void ProcessColors()
|
|
{
|
|
// If the user has specified an individual brush for this Polygon --> always use it
|
|
if (mi_Polygon3D.Brush != null)
|
|
{
|
|
mi_Brush = mi_Polygon3D.Brush;
|
|
}
|
|
else if (mi_ColorScheme != null) // ColorScheme is never null in Fill mode
|
|
{
|
|
double d_FactorZ = mi_Inst.mi_Bounds.CalcFactorZ(md_AvrgZ);
|
|
int s32_Index = mi_ColorScheme.CalcIndex(d_FactorZ);
|
|
mi_Brush = mi_ColorScheme.GetBrush(s32_Index); // used for Fill and assigned to LinePen
|
|
}
|
|
}
|
|
|
|
public override void Project3D()
|
|
{
|
|
foreach (cPoint i_Point in mi_Points)
|
|
{
|
|
i_Point.Project3D(mi_Inst, me_Mirror);
|
|
}
|
|
|
|
// Line width for Line mode
|
|
mf_LineWidth = (float)(ms32_OrgWidth * mi_Inst.mi_Transform.md_Zoom);
|
|
|
|
// Diameter of circle for selected points
|
|
mf_SelSize = (float)(Math.Max(6, ms32_OrgWidth * 2) * mi_Inst.mi_Transform.md_Zoom);
|
|
|
|
mb_IsValid = true;
|
|
for (int i = 0; i < mi_Points.Length; i++)
|
|
{
|
|
if (mi_Points[i].mi_P2D.IsValid)
|
|
mk_Screen[i] = mi_Points[i].mi_P2D.Coord;
|
|
else
|
|
mb_IsValid = false;
|
|
}
|
|
}
|
|
|
|
public override void Render(Graphics i_Graph)
|
|
{
|
|
// Fill polygon with solid color
|
|
if (mb_Fill)
|
|
{
|
|
Brush i_FillBrush = mi_Brush;
|
|
if (Selected && mi_Inst.mi_Selection.HighlightBrush != null)
|
|
i_FillBrush = mi_Inst.mi_Selection.HighlightBrush;
|
|
|
|
i_Graph.FillPolygon(i_FillBrush, mk_Screen);
|
|
}
|
|
|
|
Pen i_DrawPen;
|
|
if (mb_Fill)
|
|
{
|
|
i_DrawPen = mi_BorderPen;
|
|
}
|
|
else // Line mode
|
|
{
|
|
i_DrawPen = mi_LinePen;
|
|
i_DrawPen.Width = mf_LineWidth;
|
|
|
|
if (mi_Brush != null)
|
|
i_DrawPen.Brush = mi_Brush;
|
|
}
|
|
|
|
// Fill mode --> draw thin black border lines around the polygons Line mode --> draw
|
|
// thicker lines around transparent polygons
|
|
if (i_DrawPen != null)
|
|
{
|
|
// ATTENTION: Graphics.DrawPolygon() with a Pen > 1 pixel is buggy in the .NET
|
|
// framework (artifacts)! The lines must be drawn one by one manually here.
|
|
int T = mk_Screen.Length - 1;
|
|
for (int F = 0; F < mk_Screen.Length; F++)
|
|
{
|
|
i_Graph.DrawLine(i_DrawPen, mk_Screen[F], mk_Screen[T]);
|
|
T = F;
|
|
}
|
|
}
|
|
|
|
// Draw selected points
|
|
foreach (cPoint i_Point in mi_Points)
|
|
{
|
|
if (!i_Point.mi_P3D.Selected)
|
|
continue;
|
|
|
|
float X = (float)i_Point.mi_P2D.md_X - mf_SelSize / 2.0f;
|
|
float Y = (float)i_Point.mi_P2D.md_Y - mf_SelSize / 2.0f;
|
|
i_Graph.FillEllipse(mi_Inst.mi_Selection.HighlightBrush, X, Y, mf_SelSize, mf_SelSize);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if a user click at X, Y matches this draw object
|
|
/// </summary>
|
|
public override cObject3D MatchesPoint2D(int X, int Y)
|
|
{
|
|
// ATTENTION: Selecting entire polygons makes only sense with eSurfaceMode.Fill In
|
|
// line mode polygons are transparent and a click into the polygon would go to the background.
|
|
if (!mb_Fill || mi_Inst.mi_Selection.SinglePoints)
|
|
{
|
|
int s32_MaxDist = (int)mf_SelSize / 2 + SELECT_RADIUS;
|
|
foreach (cPoint i_Point in mi_Points)
|
|
{
|
|
if (i_Point.mi_P2D.CalcDistanceTo(X, Y) <= s32_MaxDist)
|
|
return i_Point.mi_P3D;
|
|
}
|
|
}
|
|
else // select entire polygon
|
|
{
|
|
// Detect if the point is inside the polygon. Here SELECT_RADIUS is ignored. But
|
|
// the user must only click into the middle of the polygon, which is easier than
|
|
// clicking a thin line.
|
|
bool b_Result = false;
|
|
int k = mi_Points.Length - 1;
|
|
for (int i = 0; i < mi_Points.Length; i++)
|
|
{
|
|
cPoint2D i_Point1 = mi_Points[i].mi_P2D;
|
|
cPoint2D i_Point2 = mi_Points[k].mi_P2D;
|
|
|
|
if (i_Point1.md_Y < Y && i_Point2.md_Y >= Y ||
|
|
i_Point2.md_Y < Y && i_Point1.md_Y >= Y)
|
|
{
|
|
if (i_Point1.md_X + (Y - i_Point1.md_Y) /
|
|
(i_Point2.md_Y - i_Point1.md_Y) *
|
|
(i_Point2.md_X - i_Point1.md_X) < X)
|
|
{
|
|
b_Result = !b_Result;
|
|
}
|
|
}
|
|
k = i;
|
|
}
|
|
if (b_Result)
|
|
return mi_Polygon3D;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// For debugging in Visual Studio
|
|
/// </summary>
|
|
public override string ToString()
|
|
{
|
|
return String.Format("cPolygon ({0} points)", mk_Screen.Length);
|
|
}
|
|
}
|
|
|
|
#endregion cPolygon
|
|
|
|
// ----- Math Stuff ------
|
|
|
|
#region cMouse
|
|
|
|
private class cMouse
|
|
{
|
|
public eMouseAction me_Action; // left mouse button action
|
|
public Point mk_LastPos; // last mouse location
|
|
public Point mk_OffMove; // Mouse offset after moving the graph with the mouse
|
|
public Point mk_OffCoord; // Offset caused by labels in coordinate system
|
|
public TrackBar mi_TrackRho; // Rho trackbar (optional)
|
|
public TrackBar mi_TrackTheta; // Theta trackbar (optional)
|
|
public TrackBar mi_TrackPhi; // Phi trackbar (optional)
|
|
public double md_Rho = VALUES_RHO.Default;
|
|
public double md_Theta = VALUES_THETA.Default;
|
|
public double md_Phi = VALUES_PHI.Default;
|
|
|
|
public void AssignTrackbar(eMouseAction e_Trackbar, TrackBar i_Trackbar, EventHandler i_OnScroll)
|
|
{
|
|
if (i_Trackbar == null)
|
|
return;
|
|
|
|
cDefault i_Default = null;
|
|
switch (e_Trackbar)
|
|
{
|
|
case eMouseAction.Rho:
|
|
i_Default = VALUES_RHO;
|
|
mi_TrackRho = i_Trackbar;
|
|
break;
|
|
|
|
case eMouseAction.Theta:
|
|
i_Default = VALUES_THETA;
|
|
mi_TrackTheta = i_Trackbar;
|
|
break;
|
|
|
|
case eMouseAction.Phi:
|
|
i_Default = VALUES_PHI;
|
|
mi_TrackPhi = i_Trackbar;
|
|
break;
|
|
}
|
|
|
|
i_Trackbar.Minimum = (int)i_Default.Min;
|
|
i_Trackbar.Maximum = (int)i_Default.Max;
|
|
i_Trackbar.Value = (int)i_Default.Default;
|
|
i_Trackbar.Scroll += i_OnScroll;
|
|
}
|
|
|
|
/// <summary>
|
|
/// User has moved the TrackBar
|
|
/// </summary>
|
|
public void OnTrackBarScroll()
|
|
{
|
|
if (mi_TrackRho != null) md_Rho = mi_TrackRho.Value;
|
|
if (mi_TrackTheta != null) md_Theta = mi_TrackTheta.Value;
|
|
if (mi_TrackPhi != null) md_Phi = mi_TrackPhi.Value;
|
|
}
|
|
|
|
public bool OnMouseWheel(int s32_Delta)
|
|
{
|
|
if (me_Action != eMouseAction.None)
|
|
return false;
|
|
|
|
me_Action = eMouseAction.Rho;
|
|
OnMouseMove(0, s32_Delta / 10);
|
|
me_Action = eMouseAction.None;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// User has dragged the mouse over the 3D control
|
|
/// </summary>
|
|
public void OnMouseMove(int s32_DiffX, int s32_DiffY)
|
|
{
|
|
if (me_Action == eMouseAction.Rho)
|
|
{
|
|
md_Rho += s32_DiffY * VALUES_RHO.MouseFactor;
|
|
SetRho(md_Rho);
|
|
}
|
|
if (me_Action == eMouseAction.Theta || me_Action == eMouseAction.ThetaAndPhi)
|
|
{
|
|
md_Theta -= s32_DiffY * VALUES_THETA.MouseFactor;
|
|
SetTheta(md_Theta);
|
|
}
|
|
if (me_Action == eMouseAction.Phi || me_Action == eMouseAction.ThetaAndPhi)
|
|
{
|
|
md_Phi -= s32_DiffX * VALUES_PHI.MouseFactor;
|
|
SetPhi(md_Phi);
|
|
}
|
|
}
|
|
|
|
public void SetRho(double d_Rho)
|
|
{
|
|
md_Rho = d_Rho;
|
|
md_Rho = Math.Max(md_Rho, VALUES_RHO.Min);
|
|
md_Rho = Math.Min(md_Rho, VALUES_RHO.Max);
|
|
if (mi_TrackRho != null)
|
|
mi_TrackRho.Value = (int)md_Rho;
|
|
}
|
|
|
|
public void SetTheta(double d_Theta)
|
|
{
|
|
md_Theta = d_Theta;
|
|
md_Theta = Math.Max(md_Theta, VALUES_THETA.Min);
|
|
md_Theta = Math.Min(md_Theta, VALUES_THETA.Max);
|
|
if (mi_TrackTheta != null)
|
|
mi_TrackTheta.Value = (int)md_Theta;
|
|
}
|
|
|
|
public void SetPhi(double d_Phi)
|
|
{
|
|
md_Phi = d_Phi;
|
|
while (md_Phi > 360.0) md_Phi -= 360.0; // continuous rotation
|
|
while (md_Phi < 0.0) md_Phi += 360.0; // continuous rotation
|
|
if (mi_TrackPhi != null)
|
|
mi_TrackPhi.Value = (int)md_Phi;
|
|
}
|
|
}
|
|
|
|
#endregion cMouse
|
|
|
|
#region cRange
|
|
|
|
private class cRange
|
|
{
|
|
private double md_Min, md_Max;
|
|
|
|
public double Min
|
|
{
|
|
get { return md_Min; }
|
|
}
|
|
|
|
public double Max
|
|
{
|
|
get { return md_Max; }
|
|
}
|
|
|
|
public double Range
|
|
{
|
|
get { return md_Max - md_Min; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
public cRange(double d_Min, double d_Max, bool b_IncludeZero, eRaster e_Raster)
|
|
{
|
|
md_Min = d_Min;
|
|
md_Max = d_Max;
|
|
|
|
if (md_Max == md_Min)
|
|
{
|
|
md_Min -= 1.0;
|
|
md_Max += 1.0;
|
|
}
|
|
|
|
if (e_Raster == eRaster.Off)
|
|
return;
|
|
|
|
if (b_IncludeZero)
|
|
{
|
|
md_Min = Math.Min(0.0, md_Min);
|
|
md_Max = Math.Max(0.0, md_Max);
|
|
}
|
|
|
|
// Add 10 % excess to all axes
|
|
if (md_Min < 0.0 || (md_Min > 0.0 && !b_IncludeZero)) md_Min -= Math.Abs(md_Min) * AXIS_EXCESS;
|
|
if (md_Max > 0.0 || (md_Max < 0.0 && !b_IncludeZero)) md_Max += Math.Abs(md_Max) * AXIS_EXCESS;
|
|
}
|
|
}
|
|
|
|
#endregion cRange
|
|
|
|
#region cBounds
|
|
|
|
private class cBounds
|
|
{
|
|
private Editor3D mi_Inst;
|
|
private cRange mi_RangeX;
|
|
private cRange mi_RangeY;
|
|
private cRange mi_RangeZ;
|
|
private double md_MinX, md_MaxX, md_MinY, md_MaxY, md_MinZ, md_MaxZ;
|
|
|
|
public cRange X
|
|
{
|
|
get { return mi_RangeX; }
|
|
}
|
|
|
|
public cRange Y
|
|
{
|
|
get { return mi_RangeY; }
|
|
}
|
|
|
|
public cRange Z
|
|
{
|
|
get { return mi_RangeZ; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
public cBounds(Editor3D i_Inst)
|
|
{
|
|
mi_Inst = i_Inst;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Also assigns mi_Inst to all draw objects
|
|
/// </summary>
|
|
public void Calculate()
|
|
{
|
|
md_MinX = double.PositiveInfinity;
|
|
md_MaxX = double.NegativeInfinity;
|
|
md_MinY = double.PositiveInfinity;
|
|
md_MaxY = double.NegativeInfinity;
|
|
md_MinZ = double.PositiveInfinity;
|
|
md_MaxZ = double.NegativeInfinity;
|
|
|
|
foreach (cDrawObj i_DrawObj in mi_Inst.mi_UserObjects)
|
|
{
|
|
i_DrawObj.mi_Inst = mi_Inst;
|
|
i_DrawObj.mi_Object3D.mi_Inst = mi_Inst;
|
|
|
|
foreach (cPoint i_Point in i_DrawObj.mi_Points)
|
|
{
|
|
cPoint3D i_Point3D = i_Point.mi_P3D;
|
|
i_Point3D.mi_Inst = mi_Inst;
|
|
|
|
md_MinX = Math.Min(md_MinX, i_Point3D.X);
|
|
md_MaxX = Math.Max(md_MaxX, i_Point3D.X);
|
|
md_MinY = Math.Min(md_MinY, i_Point3D.Y);
|
|
md_MaxY = Math.Max(md_MaxY, i_Point3D.Y);
|
|
md_MinZ = Math.Min(md_MinZ, i_Point3D.Z);
|
|
md_MaxZ = Math.Max(md_MaxZ, i_Point3D.Z);
|
|
}
|
|
}
|
|
|
|
mi_RangeX = new cRange(md_MinX, md_MaxX, mi_Inst.AxisX.IncludeZero, mi_Inst.me_Raster);
|
|
mi_RangeY = new cRange(md_MinY, md_MaxY, mi_Inst.AxisY.IncludeZero, mi_Inst.me_Raster);
|
|
mi_RangeZ = new cRange(md_MinZ, md_MaxZ, mi_Inst.AxisZ.IncludeZero, mi_Inst.me_Raster);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used to get the color from the ColorScheme
|
|
/// </summary>
|
|
public double CalcFactorZ(double d_Value)
|
|
{
|
|
return (d_Value - md_MinZ) / (md_MaxZ - md_MinZ);
|
|
}
|
|
}
|
|
|
|
#endregion cBounds
|
|
|
|
#region cQuadrant
|
|
|
|
private class cQuadrant
|
|
{
|
|
public double md_SortXY; // Sort order of raster in area XY (red)
|
|
public double md_SortXZ; // Sort order of X axis and raster in area XZ (blue)
|
|
public double md_SortYZ; // Sort order of Y axis and raster in area YZ (green)
|
|
public int ms32_Quadrant;
|
|
public bool mb_BottomView;
|
|
|
|
public void Calculate(double d_Phi, cLine i_AxisX, cLine i_AxisY, cLine i_AxisZ)
|
|
{
|
|
// Split rotation into 4 sections (0...3) which increment every 90° starting at 45°
|
|
int s32_Section45 = (int)d_Phi + 45;
|
|
if (s32_Section45 > 360) s32_Section45 -= 360;
|
|
s32_Section45 = Math.Min(3, s32_Section45 / 90);
|
|
|
|
// Theta elevation lets the camera watch the graph from the top or bottom
|
|
switch (s32_Section45)
|
|
{
|
|
case 0: mb_BottomView = i_AxisX.md_Angle < 180.0; break;
|
|
case 1: mb_BottomView = i_AxisY.md_Angle < 180.0; break;
|
|
case 2: mb_BottomView = i_AxisX.md_Angle > 180.0; break;
|
|
case 3: mb_BottomView = i_AxisY.md_Angle > 180.0; break;
|
|
}
|
|
|
|
// The quadrant changes when the 2D transformed Z axis is in line with the X or Y axis
|
|
if (mb_BottomView)
|
|
{
|
|
switch (s32_Section45)
|
|
{
|
|
case 0: ms32_Quadrant = i_AxisX.md_Angle + 180.0 < i_AxisZ.md_Angle ? 1 : 0; break;
|
|
case 1: ms32_Quadrant = i_AxisY.md_Angle + 180.0 < i_AxisZ.md_Angle ? 2 : 1; break;
|
|
case 2: ms32_Quadrant = i_AxisX.md_Angle < i_AxisZ.md_Angle ? 3 : 2; break;
|
|
case 3: ms32_Quadrant = i_AxisY.md_Angle < i_AxisZ.md_Angle ? 0 : 3; break;
|
|
}
|
|
}
|
|
else // Top View
|
|
{
|
|
switch (s32_Section45)
|
|
{
|
|
case 0: ms32_Quadrant = i_AxisX.md_Angle > i_AxisZ.md_Angle ? 1 : 0; break;
|
|
case 1: ms32_Quadrant = i_AxisY.md_Angle > i_AxisZ.md_Angle ? 2 : 1; break;
|
|
case 2: ms32_Quadrant = i_AxisX.md_Angle + 180.0 > i_AxisZ.md_Angle ? 3 : 2; break;
|
|
case 3: ms32_Quadrant = i_AxisY.md_Angle + 180.0 > i_AxisZ.md_Angle ? 0 : 3; break;
|
|
}
|
|
}
|
|
|
|
md_SortXY = (mb_BottomView) ? 99999.9 : -99999.9;
|
|
md_SortXZ = (ms32_Quadrant == 1 || ms32_Quadrant == 2) ? 99999.9 : -99999.9;
|
|
md_SortYZ = (ms32_Quadrant == 0 || ms32_Quadrant == 1) ? 99999.9 : -99999.9;
|
|
|
|
i_AxisX.md_Sort = md_SortXZ;
|
|
i_AxisY.md_Sort = md_SortYZ;
|
|
i_AxisZ.md_Sort = (ms32_Quadrant == 3) ? -99999.9 : 99999.9;
|
|
|
|
// Debug.WriteLine(String.Format("Section: {0} Quadrant: {1}", s32_Section45, ms32_Quadrant));
|
|
}
|
|
}
|
|
|
|
#endregion cQuadrant
|
|
|
|
#region cTransform
|
|
|
|
private class cTransform
|
|
{
|
|
// Camera distance. Smaller values result in ugly stretched egdes when rotating.
|
|
private const double DISTANCE = 0.55;
|
|
|
|
private double md_sf; // sf = sinus fi
|
|
private double md_st; // st = sinus theta
|
|
private double md_cf; // cf = cosinus fi
|
|
private double md_ct; // ct = cosinus theta
|
|
private double md_Rho;
|
|
|
|
// ----------------
|
|
private double md_FactX;
|
|
|
|
private double md_OffsX;
|
|
private double md_FactY;
|
|
private double md_OffsY;
|
|
private double md_Resize = 1.0;
|
|
|
|
// ----------------
|
|
public cPoint3D mi_Center3D = new cPoint3D(0, 0, 0);
|
|
|
|
public double md_NormalizeX;
|
|
public double md_NormalizeY;
|
|
public double md_NormalizeZ;
|
|
public double md_Zoom;
|
|
|
|
// ----------------
|
|
private Size mk_InitialSize = Size.Empty;
|
|
|
|
private Editor3D mi_Inst;
|
|
|
|
public cTransform(Editor3D i_Inst)
|
|
{
|
|
mi_Inst = i_Inst;
|
|
}
|
|
|
|
public void SetCoefficients(cMouse i_Mouse)
|
|
{
|
|
md_Rho = i_Mouse.md_Rho; // Distance of viewer (zoom)
|
|
double d_Theta = i_Mouse.md_Theta * Math.PI / 180.0; // Height of viewer (elevation)
|
|
double d_Phi = (i_Mouse.md_Phi - 180.0) * Math.PI / 180.0; // Rotation around center (-Pi ... +Pi)
|
|
|
|
// Speed optimization: precalculate factors
|
|
md_sf = Math.Sin(d_Phi);
|
|
md_cf = Math.Cos(d_Phi);
|
|
md_st = Math.Sin(d_Theta); // Theta = 0...pi --> st = 0 .. 1 .. 0
|
|
md_ct = Math.Cos(d_Theta); // Theta = 0...pi --> ct = 1 .. 0 .. -1
|
|
|
|
CalcZoom();
|
|
mi_Inst.me_Recalculate |= eRecalculate.CoordSystem | eRecalculate.Objects;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The initial size is needed to calculate the user resizing factor. To assure that it
|
|
/// is correct it must be set when the control has already been created. Then it will be
|
|
/// the size that was defined in Visual Studio Form Designer.
|
|
/// </summary>
|
|
public void SetInitialSize(Size k_Size)
|
|
{
|
|
mk_InitialSize = k_Size;
|
|
SetSize(k_Size);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The control has been resized. This may be called with an invalid size before the
|
|
/// control is created!
|
|
/// </summary>
|
|
public void SetSize(Size k_Size) // Control.ClientSize
|
|
{
|
|
if (mk_InitialSize == Size.Empty)
|
|
return;
|
|
|
|
double d_Width = k_Size.Width * 0.0254 / 96.0; // 0.0254 meter = 1 inch. Screen has 96 DPI
|
|
double d_Height = k_Size.Height * 0.0254 / 96.0;
|
|
|
|
// linear transformation coefficients
|
|
md_FactX = k_Size.Width / d_Width;
|
|
md_FactY = -k_Size.Height / d_Height;
|
|
|
|
md_OffsX = md_FactX * d_Width / 2.0;
|
|
md_OffsY = -md_FactY * d_Height / 2.0;
|
|
|
|
// -----------------------------------
|
|
|
|
double d_ResizeX = (double)k_Size.Width / mk_InitialSize.Width;
|
|
double d_ResizeY = (double)k_Size.Height / mk_InitialSize.Height;
|
|
md_Resize = Math.Min(d_ResizeX, d_ResizeY);
|
|
|
|
md_FactX *= md_Resize;
|
|
md_FactY *= md_Resize;
|
|
|
|
CalcZoom();
|
|
mi_Inst.me_Recalculate |= eRecalculate.CoordSystem | eRecalculate.Objects;
|
|
}
|
|
|
|
// Required for correct painting order of polygons (always from back to front)
|
|
public double ProjectXY(double X, double Y, double Z = 0.0)
|
|
{
|
|
return X * md_cf + Y * md_sf + Z * md_ct;
|
|
}
|
|
|
|
// Used to convert mouse movements back into the 3D space depending on the current
|
|
// rotation angle
|
|
public double ReverseProject(double X, double Y, double Z)
|
|
{
|
|
if (mi_Inst.AxisX.Mirror) X = -X;
|
|
if (mi_Inst.AxisY.Mirror) Y = -Y;
|
|
if (mi_Inst.AxisZ.Mirror) Z = -Z;
|
|
|
|
// If Theta has the correct range from 10 to 170 degree --> Sinus(Theta) will never
|
|
// become zero. This can only happen if VALUES_THETA has been manipulated to invalid
|
|
// Min/Max values.
|
|
double d_Divide = Math.Max(0.1, md_st);
|
|
return (-X * md_sf + Y * md_cf + Z / d_Divide) / md_Zoom;
|
|
}
|
|
|
|
/// <summary>
|
|
/// This approximates a zoom factor that depends on Rho and the resize window factor.
|
|
/// Used to adapt the size of lines, shapes and selected points.
|
|
/// </summary>
|
|
private void CalcZoom()
|
|
{
|
|
md_Zoom = md_Resize * (1800.0 / (md_Rho + 300));
|
|
}
|
|
|
|
// Performs projection. Calculates 2D screen coordinates from 3D point.
|
|
public cPoint2D Project3D(cPoint3D i_Point3D, eMirror e_Mirror)
|
|
{
|
|
double X = i_Point3D.X;
|
|
double Y = i_Point3D.Y;
|
|
double Z = i_Point3D.Z;
|
|
|
|
// Mirror axis values increasing / decreasing
|
|
if (mi_Inst.AxisX.Mirror && (e_Mirror & eMirror.X) > 0) X = mi_Inst.mi_Bounds.X.Max - (X - mi_Inst.mi_Bounds.X.Min);
|
|
if (mi_Inst.AxisY.Mirror && (e_Mirror & eMirror.Y) > 0) Y = mi_Inst.mi_Bounds.Y.Max - (Y - mi_Inst.mi_Bounds.Y.Min);
|
|
if (mi_Inst.AxisZ.Mirror && (e_Mirror & eMirror.Z) > 0) Z = mi_Inst.mi_Bounds.Z.Max - (Z - mi_Inst.mi_Bounds.Z.Min);
|
|
|
|
X = (X - mi_Center3D.X) * md_NormalizeX;
|
|
Y = (Y - mi_Center3D.Y) * md_NormalizeY;
|
|
Z = (Z - mi_Center3D.Z) * md_NormalizeZ;
|
|
|
|
// 3D coordinates with center point in the middle of the screen X positive to the
|
|
// right, X negative to the left Y positive to the top, Y negative to the bottom
|
|
double xn = -md_sf * X + md_cf * Y;
|
|
double yn = -md_cf * md_ct * X - md_sf * md_ct * Y + md_st * Z;
|
|
double zn = -md_cf * md_st * X - md_sf * md_st * Y - md_ct * Z + md_Rho;
|
|
|
|
zn = Math.Max(zn, 0.01); // avoid division by zero
|
|
|
|
// Thales' theorem
|
|
cPoint2D i_Point2D = new cPoint2D(xn * DISTANCE / zn, yn * DISTANCE / zn);
|
|
|
|
i_Point2D.md_X = i_Point2D.md_X * md_FactX + md_OffsX;
|
|
i_Point2D.md_Y = i_Point2D.md_Y * md_FactY + md_OffsY;
|
|
return i_Point2D;
|
|
}
|
|
}
|
|
|
|
#endregion cTransform
|
|
|
|
#region cDefault
|
|
|
|
/// <summary>
|
|
/// Stores defauls for Rho, Theta, Phi
|
|
/// </summary>
|
|
private class cDefault
|
|
{
|
|
public readonly double Min;
|
|
public readonly double Max;
|
|
public readonly double Default;
|
|
public readonly double MouseFactor;
|
|
|
|
public cDefault(double d_Min, double d_Max, double d_Default, double d_MouseFactor)
|
|
{
|
|
Min = d_Min;
|
|
Max = d_Max;
|
|
Default = d_Default;
|
|
MouseFactor = d_MouseFactor;
|
|
}
|
|
}
|
|
|
|
#endregion cDefault
|
|
|
|
// Limits and default values for mouse actions and trackbars.
|
|
// ATTENTION: It is strongly recommended not to change the MIN, MAX values. The mouse factor
|
|
// defines how much mouse movement you need for a change. A movement of mouse by approx 1000
|
|
// pixels on the screen results in getting from Min to Max or vice versa.
|
|
private static readonly cDefault VALUES_RHO = new cDefault(300, 1800, 1350, 2);
|
|
|
|
private static readonly cDefault VALUES_THETA = new cDefault(10, 170, 70, 0.25); // degree
|
|
private static readonly cDefault VALUES_PHI = new cDefault(0, 360, 230, 0.4); // degree (continuous rotation)
|
|
|
|
// The coordinate axes are 10 % longer than the bounds of the X,Y,Z values
|
|
private const double AXIS_EXCESS = 0.1;
|
|
|
|
// For any strange reason the graph is not centered vertically
|
|
private const int VERT_OFFSET = -30;
|
|
|
|
// The maximum distance between mouse pointer and a 2D point to display the tooltip
|
|
private const int TOOLTIP_RADIUS = 6;
|
|
|
|
// The maximum distance between mouse pointer and a 2D point to allow a match when selecting
|
|
// a 3D object.
|
|
private const int SELECT_RADIUS = 3;
|
|
|
|
// Calculate 3-dimensional Z value from X,Y values
|
|
public delegate double delRendererFunction(double X, double Y);
|
|
|
|
// IMPORTANT: Read the detailed comment of function SelectionCallback() at the end of this class.
|
|
public delegate eInvalidate delSelectHandler(eSelEvent e_Event, Keys e_Modifiers, int s32_DeltaX, int s32_DeltaY, cObject3D i_Object);
|
|
|
|
private Pen[] mi_BorderPens = new Pen[2];
|
|
private SolidBrush mi_TopLegendBrush = null;
|
|
private eRaster me_Raster = eRaster.Labels;
|
|
private cAxis[] mi_Axis = new cAxis[3];
|
|
private cMouse mi_Mouse = new cMouse();
|
|
private List<cMessgData> mi_MessageData = new List<cMessgData>();
|
|
private eRecalculate me_Recalculate = eRecalculate.Nothing;
|
|
private eNormalize me_Normalize = eNormalize.Separate;
|
|
private eLegendPos me_LegendPos = eLegendPos.BottomLeft;
|
|
private List<cLine> mi_AxisLines = new List<cLine>(); // 0, 3, or 45 axis lines of coordinate system
|
|
private List<cDrawObj> mi_UserObjects = new List<cDrawObj>(); // Draw objects from the user (cLine, cShape, cPolygon)
|
|
private List<cDrawObj> mi_AllObjects = new List<cDrawObj>(); // mi_UserObjects + mi_AxisLines
|
|
private cQuadrant mi_Quadrant = new cQuadrant();
|
|
private Dictionary<int, cUserInput> mi_UserInputs = new Dictionary<int, cUserInput>();
|
|
private cUndoImpl mi_UndoBuffer;
|
|
private cTransform mi_Transform;
|
|
private cBounds mi_Bounds;
|
|
private cTooltip mi_Tooltip;
|
|
private cSelection mi_Selection;
|
|
private cObject3D mi_DragObject;
|
|
|
|
#region Properties
|
|
|
|
/// <summary>
|
|
/// See comment of enum eTooltip. This property can also be set in the Visual Studio Designer
|
|
/// </summary>
|
|
public eTooltip TooltipMode
|
|
{
|
|
get { return mi_Tooltip.Mode; }
|
|
set { mi_Tooltip.Mode = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// See comment of enum eLegendPos. This property can also be set in the Visual Studio Designer
|
|
/// </summary>
|
|
public eLegendPos LegendPos
|
|
{
|
|
get { return me_LegendPos; }
|
|
set { me_LegendPos = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// See comment of enum eNormalize. This change will become visible the next time you call Invalidate()
|
|
/// </summary>
|
|
public eNormalize Normalize
|
|
{
|
|
get { return me_Normalize; }
|
|
set
|
|
{
|
|
if (me_Normalize != value)
|
|
{
|
|
me_Normalize = value;
|
|
me_Recalculate |= eRecalculate.CoordSystem | eRecalculate.Objects;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// See comment of enum eRaster This property can also be set in the Visual Studio Designer
|
|
/// This change will become visible the next time you call Invalidate()
|
|
/// </summary>
|
|
public eRaster Raster
|
|
{
|
|
set
|
|
{
|
|
Debug.Assert(!InvokeRequired); // Call only from GUI thread
|
|
|
|
if (me_Raster != value)
|
|
{
|
|
me_Raster = value;
|
|
me_Recalculate |= eRecalculate.CoordSystem | eRecalculate.Objects;
|
|
}
|
|
}
|
|
get
|
|
{
|
|
return me_Raster;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the border color when the 3D Editor does not have the keyboard focus Setting
|
|
/// BorderColor = Color.Empty turns off the border This change will become visible the next
|
|
/// time you call Invalidate() This property can also be set in the Visual Studio Designer
|
|
/// </summary>
|
|
public Color BorderColorNormal
|
|
{
|
|
set
|
|
{
|
|
Debug.Assert(!InvokeRequired); // Call only from GUI thread
|
|
|
|
if (value.A > 0) mi_BorderPens[0] = new Pen(value, 1);
|
|
else mi_BorderPens[0] = null; // transparent color
|
|
}
|
|
get
|
|
{
|
|
if (mi_BorderPens[0] != null) return mi_BorderPens[0].Color;
|
|
else return Color.Empty;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the border color when the 3D Editor has the keyboard focus Setting BorderColorFocus
|
|
/// = Color.Empty turns off the highlighting on focus. This change will become visible the
|
|
/// next time you call Invalidate() This property can also be set in the Visual Studio Designer
|
|
/// </summary>
|
|
public Color BorderColorFocus
|
|
{
|
|
set
|
|
{
|
|
Debug.Assert(!InvokeRequired); // Call only from GUI thread
|
|
|
|
if (value.A > 0) mi_BorderPens[1] = new Pen(value, 1);
|
|
else mi_BorderPens[1] = mi_BorderPens[0];
|
|
}
|
|
get
|
|
{
|
|
if (mi_BorderPens[1] != null) return mi_BorderPens[1].Color;
|
|
else return BorderColorNormal;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Show a legend with Rotation, Elevation and Distance at the top left Setting LegendColor
|
|
/// = Color.Empty turns off the top legend This property can also be set in the Visual
|
|
/// Studio Designer This change will become visible the next time you call Invalidate()
|
|
/// </summary>
|
|
public Color TopLegendColor
|
|
{
|
|
set
|
|
{
|
|
Debug.Assert(!InvokeRequired); // Call only from GUI thread
|
|
|
|
mi_TopLegendBrush = new SolidBrush(value);
|
|
}
|
|
get
|
|
{
|
|
if (mi_TopLegendBrush != null) return mi_TopLegendBrush.Color;
|
|
else return Color.Empty;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// returns the total count of loaded draw objects (lines, shapes and polygons)
|
|
/// </summary>
|
|
[Browsable(false)]
|
|
public String ObjectStatistics
|
|
{
|
|
get
|
|
{
|
|
int s32_Lines = 0;
|
|
int s32_Shapes = 0;
|
|
int s32_Polygons = 0;
|
|
foreach (cDrawObj i_Obj in mi_UserObjects)
|
|
{
|
|
if (i_Obj is cLine) s32_Lines++;
|
|
if (i_Obj is cShape) s32_Shapes++;
|
|
if (i_Obj is cPolygon) s32_Polygons++;
|
|
}
|
|
StringBuilder i_Out = new StringBuilder();
|
|
if (s32_Lines > 0) i_Out.Append(s32_Lines + " Lines, ");
|
|
if (s32_Shapes > 0) i_Out.Append(s32_Shapes + " Shapes, ");
|
|
if (s32_Polygons > 0) i_Out.Append(s32_Polygons + " Polygons, ");
|
|
return i_Out.ToString().TrimEnd(' ', ',');
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// See comments for class cAxis The properties of the class cAxis can be expanded in the
|
|
/// Visual Studio designer
|
|
/// </summary>
|
|
[TypeConverter(typeof(ExpandableObjectConverter))]
|
|
public cAxis AxisX
|
|
{
|
|
get { return mi_Axis[(int)eCoord.X]; }
|
|
}
|
|
|
|
[TypeConverter(typeof(ExpandableObjectConverter))]
|
|
public cAxis AxisY
|
|
{
|
|
get { return mi_Axis[(int)eCoord.Y]; }
|
|
}
|
|
|
|
[TypeConverter(typeof(ExpandableObjectConverter))]
|
|
public cAxis AxisZ
|
|
{
|
|
get { return mi_Axis[(int)eCoord.Z]; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// This property controls if and how the user can select draw objects / points The
|
|
/// properties of the class cSelection can be expanded in the Visual Studio designer
|
|
/// </summary>
|
|
[TypeConverter(typeof(ExpandableObjectConverter))]
|
|
public cSelection Selection
|
|
{
|
|
get { return mi_Selection; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// This property contains the public methods for the Undo / Redo buffer The property
|
|
/// 'Enabled' can be expanded in the Visual Studio designer
|
|
/// </summary>
|
|
[TypeConverter(typeof(ExpandableObjectConverter))]
|
|
public cUndoBuffer UndoBuffer
|
|
{
|
|
get { return mi_UndoBuffer; }
|
|
}
|
|
|
|
#endregion Properties
|
|
|
|
/// <summary>
|
|
/// b_ResetOffset = true --> reset the offset that the user has created with SHIFT +
|
|
/// moving the 3D object This change will become visible the next time you call Invalidate()
|
|
/// </summary>
|
|
public void SetCoefficients(double d_Rho, double d_Theta, double d_Phi, bool b_ResetOffset = true)
|
|
{
|
|
Debug.Assert(!InvokeRequired); // Call only from GUI thread
|
|
|
|
mi_Mouse.SetRho(d_Rho);
|
|
mi_Mouse.SetTheta(d_Theta);
|
|
mi_Mouse.SetPhi(d_Phi);
|
|
|
|
if (b_ResetOffset)
|
|
{
|
|
mi_Mouse.mk_OffMove.X = 0;
|
|
mi_Mouse.mk_OffMove.Y = 0;
|
|
}
|
|
|
|
mi_Transform.SetCoefficients(mi_Mouse);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convert mouse movement in 2D space back into the 3D space depending on the current
|
|
/// rotation angle and Min/Max values.
|
|
/// </summary>
|
|
public cPoint3D ReverseProject(int s32_MouseX, int s32_MouseY)
|
|
{
|
|
double d_FactX = mi_Transform.ReverseProject(mi_Bounds.X.Range, 0.0, 0.0);
|
|
double d_FactY = mi_Transform.ReverseProject(0.0, mi_Bounds.Y.Range, 0.0);
|
|
double d_FactZ = mi_Transform.ReverseProject(0.0, 0.0, mi_Bounds.Z.Range);
|
|
|
|
return new cPoint3D(d_FactX * s32_MouseX / 300.0,
|
|
d_FactY * s32_MouseX / 300.0,
|
|
d_FactZ * s32_MouseY / 300.0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Trackbars are optional for user interaction. If this function is never called
|
|
/// thetrackbars are not used.
|
|
/// </summary>
|
|
public void AssignTrackBars(TrackBar i_Rho, TrackBar i_Theta, TrackBar i_Phi)
|
|
{
|
|
Debug.Assert(!InvokeRequired); // Call only from GUI thread
|
|
|
|
mi_Mouse.AssignTrackbar(eMouseAction.Rho, i_Rho, new EventHandler(OnTrackbarScroll));
|
|
mi_Mouse.AssignTrackbar(eMouseAction.Theta, i_Theta, new EventHandler(OnTrackbarScroll));
|
|
mi_Mouse.AssignTrackbar(eMouseAction.Phi, i_Phi, new EventHandler(OnTrackbarScroll));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load one of the three pre-defined input control patterns
|
|
/// </summary>
|
|
public void SetUserInputs(eMouseCtrl e_MouseCtrl)
|
|
{
|
|
List<cUserInput> i_Inputs = new List<cUserInput>();
|
|
|
|
switch (e_MouseCtrl)
|
|
{
|
|
case eMouseCtrl.L_Theta_R_Phi:
|
|
i_Inputs.Add(new cUserInput(MouseButtons.Left, Keys.None, eMouseAction.Theta));
|
|
i_Inputs.Add(new cUserInput(MouseButtons.Right, Keys.None, eMouseAction.Phi));
|
|
break;
|
|
|
|
case eMouseCtrl.L_Theta_L_Phi:
|
|
i_Inputs.Add(new cUserInput(MouseButtons.Left, Keys.None, eMouseAction.ThetaAndPhi));
|
|
break;
|
|
|
|
case eMouseCtrl.M_Theta_M_Phi:
|
|
i_Inputs.Add(new cUserInput(MouseButtons.Middle, Keys.None, eMouseAction.ThetaAndPhi));
|
|
break;
|
|
}
|
|
|
|
i_Inputs.Add(new cUserInput(MouseButtons.Left, Keys.Control, eMouseAction.Rho));
|
|
i_Inputs.Add(new cUserInput(MouseButtons.Left, Keys.Shift, eMouseAction.Move));
|
|
i_Inputs.Add(new cUserInput(MouseButtons.Left, Keys.Alt, eMouseAction.SelectObj));
|
|
i_Inputs.Add(new cUserInput(MouseButtons.Left, Keys.Alt | Keys.Control, eMouseAction.Callback));
|
|
i_Inputs.Add(new cUserInput(MouseButtons.Left, Keys.Alt | Keys.Shift, eMouseAction.Callback));
|
|
|
|
SetUserInputs(i_Inputs.ToArray());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Load fully user defined input control patterns. Each user input must define a unique
|
|
/// combination of mouse button and modifier key(s).
|
|
/// </summary>
|
|
public void SetUserInputs(cUserInput[] i_Inputs)
|
|
{
|
|
mi_UserInputs.Clear();
|
|
foreach (cUserInput i_Input in i_Inputs)
|
|
{
|
|
// throws if same UID has already been added
|
|
mi_UserInputs.Add(i_Input.UID, i_Input);
|
|
}
|
|
}
|
|
|
|
// ==================================================================================
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
public Editor3D()
|
|
{
|
|
this.DoubleBuffered = true; // 启用双缓冲
|
|
// avoid flicker
|
|
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
|
|
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
|
|
|
|
mi_Bounds = new cBounds(this);
|
|
mi_Transform = new cTransform(this);
|
|
mi_Tooltip = new cTooltip(this);
|
|
mi_Selection = new cSelection(this);
|
|
mi_UndoBuffer = new cUndoImpl(this);
|
|
|
|
// Load the default colors
|
|
BackColor = Color.White;
|
|
|
|
mi_Axis[(int)eCoord.X] = new cAxis(this, eCoord.X, Color.DarkBlue);
|
|
mi_Axis[(int)eCoord.Y] = new cAxis(this, eCoord.Y, Color.DarkGreen);
|
|
mi_Axis[(int)eCoord.Z] = new cAxis(this, eCoord.Z, Color.DarkRed);
|
|
|
|
mi_BorderPens[0] = new Pen(Color.FromArgb(255, 0xB4, 0xB4, 0xB4), 1); // normal border: bright gray
|
|
mi_BorderPens[1] = new Pen(Color.FromArgb(255, 0x33, 0x99, 0xFF), 1); // focused border: bright cyan
|
|
mi_TopLegendBrush = new SolidBrush(Color.FromArgb(255, 0xC8, 0xC8, 0x96)); // beige
|
|
|
|
mi_Transform.SetCoefficients(mi_Mouse);
|
|
SetUserInputs(eMouseCtrl.L_Theta_R_Phi);
|
|
}
|
|
|
|
// ==================================================================================
|
|
|
|
/// <summary>
|
|
/// Removes all content from the control. This change will become visible the next time you
|
|
/// call Invalidate()
|
|
/// </summary>
|
|
public void Clear()
|
|
{
|
|
Debug.Assert(!InvokeRequired); // Call only from GUI thread
|
|
|
|
mi_MessageData.Clear();
|
|
mi_UserObjects.Clear();
|
|
mi_AxisLines.Clear();
|
|
mi_AllObjects.Clear();
|
|
mi_UndoBuffer.Clear();
|
|
AxisX.Reset();
|
|
AxisY.Reset();
|
|
AxisZ.Reset();
|
|
mi_Mouse.mk_OffMove = Point.Empty;
|
|
mi_Mouse.mk_OffCoord = Point.Empty;
|
|
me_Recalculate = eRecalculate.Nothing;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a message to be shown, even if no 3D data is loaded. Messages which are null are
|
|
/// allowed, they will be skipped. This change will become visible the next time you call Invalidate()
|
|
/// </summary>
|
|
public void AddMessageData(params cMessgData[] i_Messages)
|
|
{
|
|
Debug.Assert(!InvokeRequired); // Call only from GUI thread
|
|
|
|
foreach (cMessgData i_Mesg in i_Messages)
|
|
{
|
|
if (i_Mesg != null)
|
|
mi_MessageData.Add(i_Mesg);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Here you can add cSurfaceData, cScatterData, cPolygonData RenderData which are null are
|
|
/// allowed, they will be skipped. This change will become visible the next time you call Invalidate()
|
|
/// </summary>
|
|
public void AddRenderData(params cRenderData[] i_Render)
|
|
{
|
|
Debug.Assert(!InvokeRequired); // Call only from GUI thread
|
|
|
|
foreach (cRenderData i_Data in i_Render)
|
|
{
|
|
if (i_Data != null)
|
|
{
|
|
i_Data.AddDrawObjects(this);
|
|
me_Recalculate |= eRecalculate.AddRemove | eRecalculate.CoordSystem | eRecalculate.Objects;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is called from cRenderData.AddDrawObjects()
|
|
/// </summary>
|
|
private void AddDrawObject(cDrawObj i_Obj)
|
|
{
|
|
mi_UserObjects.Add(i_Obj);
|
|
mi_UndoBuffer.DrawObjectAdded(i_Obj);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes one or multiple 3D objects: cLine3D, cShape3D or cPolygon3D.
|
|
/// ATTENTION: It is not possible to remove a cPoint3D which is part of one or multiple Lines/Shapes/Polygons
|
|
/// This change will become visible the next time you call Invalidate()
|
|
/// </summary>
|
|
public void RemoveObjects(params cObject3D[] i_Objects)
|
|
{
|
|
Debug.Assert(!InvokeRequired); // Call only from GUI thread
|
|
|
|
foreach (cObject3D i_Object3D in i_Objects)
|
|
{
|
|
if (i_Object3D is cPoint3D)
|
|
throw new ArgumentException("You cannot remove a single point. Remove the 3D object that contains the point instead.");
|
|
|
|
int s32_Found = -1;
|
|
for (int D = 0; D < mi_UserObjects.Count; D++)
|
|
{
|
|
if (mi_UserObjects[D].mi_Object3D == i_Object3D)
|
|
{
|
|
s32_Found = D;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (s32_Found < 0)
|
|
{
|
|
Debug.Assert(false, "There is something wrong in your code. You are removing an object that does not exist.");
|
|
}
|
|
else
|
|
{
|
|
cDrawObj i_DrawObj = mi_UserObjects[s32_Found];
|
|
mi_UserObjects.RemoveAt(s32_Found);
|
|
mi_UndoBuffer.DrawObjectRemoved(i_DrawObj);
|
|
me_Recalculate |= eRecalculate.AddRemove | eRecalculate.CoordSystem | eRecalculate.Objects;
|
|
}
|
|
}
|
|
|
|
mi_UndoBuffer.Store();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Search the object at the coordinate X, Y relative to to the upper left corner of the
|
|
/// control. b_OnlyCanSelect = true --> return only objects that can be selected by the
|
|
/// user b_OnlyCanSelect = false --> return any object at the given location
|
|
/// </summary>
|
|
public cObject3D FindObjectAt(int X, int Y, bool b_OnlyCanSelect)
|
|
{
|
|
X -= (mi_Mouse.mk_OffMove.X + mi_Mouse.mk_OffCoord.X);
|
|
Y -= (mi_Mouse.mk_OffMove.Y + mi_Mouse.mk_OffCoord.Y);
|
|
|
|
// Search in reverse order. Last drawn objects are in foreground.
|
|
// ATTENTION: mi_UserObjects cannot be used here because it is not sorted --> background
|
|
// polygons would be found.
|
|
for (int i = mi_AllObjects.Count - 1; i >= 0; i--)
|
|
{
|
|
cDrawObj i_Draw = mi_AllObjects[i];
|
|
|
|
if (i_Draw.mi_Object3D == null)
|
|
continue; // a coordinate system line
|
|
|
|
if (b_OnlyCanSelect && !i_Draw.mi_Object3D.CanSelect)
|
|
continue; // user selection disabled for this object
|
|
|
|
// MatchesPoint2D() returns a cPoint3D, cLine3D, cShape3D or cPolygon3D.
|
|
cObject3D i_Found = i_Draw.MatchesPoint2D(X, Y);
|
|
if (i_Found != null)
|
|
return i_Found;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ========================================== PRIVATE ============================================
|
|
|
|
/// <summary>
|
|
/// This function normalizes the 3D ranges for the X,Y,Z coordinates. Otherwise a 3D range
|
|
/// of X,Y from -10 to +10 will appear much smaller than a range from -100 to +100. It
|
|
/// adapts the values so that rotation (phi) goes through the center of the X, Y pane.
|
|
/// </summary>
|
|
private void NormalizeRanges()
|
|
{
|
|
double d_RangeX = mi_Bounds.X.Range;
|
|
double d_RangeY = mi_Bounds.Y.Range;
|
|
double d_RangeZ = mi_Bounds.Z.Range;
|
|
|
|
switch (me_Normalize)
|
|
{
|
|
case eNormalize.MaintainXY:
|
|
double d_RangeXY = (d_RangeX + d_RangeY) / 2.0; // average
|
|
d_RangeX = d_RangeXY;
|
|
d_RangeY = d_RangeXY;
|
|
break;
|
|
|
|
case eNormalize.MaintainXYZ:
|
|
double d_RangeXYZ = (d_RangeX + d_RangeY + d_RangeZ) / 3.0; // average
|
|
d_RangeX = d_RangeXYZ;
|
|
d_RangeY = d_RangeXYZ;
|
|
d_RangeZ = d_RangeXYZ;
|
|
break;
|
|
}
|
|
|
|
mi_Transform.md_NormalizeX = 250.0 / d_RangeX; // Ranges will never be zero.
|
|
mi_Transform.md_NormalizeY = 250.0 / d_RangeY;
|
|
mi_Transform.md_NormalizeZ = 250.0 / d_RangeZ;
|
|
|
|
mi_Transform.mi_Center3D.X = (mi_Bounds.X.Max + mi_Bounds.X.Min) / 2.0; // average
|
|
mi_Transform.mi_Center3D.Y = (mi_Bounds.Y.Max + mi_Bounds.Y.Min) / 2.0;
|
|
mi_Transform.mi_Center3D.Z = (mi_Bounds.Z.Max + mi_Bounds.Z.Min) / 2.0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fills mi_AxisLines with 3 main axis and 42 raster lines
|
|
/// </summary>
|
|
private void CreateCoordinateSystem(Graphics i_Graph)
|
|
{
|
|
mi_Mouse.mk_OffCoord = new Point(0, VERT_OFFSET);
|
|
mi_AxisLines.Clear();
|
|
|
|
if (me_Raster == eRaster.Off)
|
|
return;
|
|
|
|
cLine i_MainAxisX = new cLine(this, eCoord.X, eCoord.X, eMirror.Z);
|
|
mi_AxisLines.Add(i_MainAxisX);
|
|
|
|
i_MainAxisX.mi_Points[0].mi_P3D.X = mi_Bounds.X.Min;
|
|
i_MainAxisX.mi_Points[1].mi_P3D.X = mi_Bounds.X.Max;
|
|
// ------------
|
|
i_MainAxisX.mi_Points[0].mi_P3D.Y = mi_Bounds.Y.Min; // X axis at minimum Y position
|
|
i_MainAxisX.mi_Points[1].mi_P3D.Y = mi_Bounds.Y.Min; // X axis at minimum Y position
|
|
|
|
// ---------------------------------------------------
|
|
|
|
cLine i_MainAxisY = new cLine(this, eCoord.Y, eCoord.Y, eMirror.Z);
|
|
mi_AxisLines.Add(i_MainAxisY);
|
|
|
|
i_MainAxisY.mi_Points[0].mi_P3D.Y = mi_Bounds.Y.Min;
|
|
i_MainAxisY.mi_Points[1].mi_P3D.Y = mi_Bounds.Y.Max;
|
|
// ------------
|
|
i_MainAxisY.mi_Points[0].mi_P3D.X = mi_Bounds.X.Min; // Y axis at minimum X position
|
|
i_MainAxisY.mi_Points[1].mi_P3D.X = mi_Bounds.X.Min; // Y axis at minimum X position
|
|
// ------------
|
|
if (mi_Bounds.Z.Min < 0.0 && mi_Bounds.Z.Max > 0.0)
|
|
i_MainAxisY.ms_Label = "0"; // label for Z value zero (red)
|
|
|
|
// ---------------------------------------------------
|
|
|
|
cLine i_MainAxisZ = new cLine(this, eCoord.Z, eCoord.Z, eMirror.None);
|
|
mi_AxisLines.Add(i_MainAxisZ);
|
|
|
|
i_MainAxisZ.mi_Points[0].mi_P3D.Z = mi_Bounds.Z.Min;
|
|
i_MainAxisZ.mi_Points[1].mi_P3D.Z = mi_Bounds.Z.Max;
|
|
// ------------
|
|
i_MainAxisZ.mi_Points[0].mi_P3D.X = mi_Bounds.X.Min; // Z axis start at minimum X position
|
|
i_MainAxisZ.mi_Points[1].mi_P3D.X = mi_Bounds.X.Min; // Z axis start at minimum X position
|
|
i_MainAxisZ.mi_Points[0].mi_P3D.Y = mi_Bounds.Y.Min; // Z axis start at minimum Y position
|
|
i_MainAxisZ.mi_Points[1].mi_P3D.Y = mi_Bounds.Y.Min; // Z axis start at minimum Y position
|
|
|
|
// ---------------------------------------------------
|
|
|
|
foreach (cLine i_Axis in mi_AxisLines)
|
|
{
|
|
i_Axis.Project3D();
|
|
i_Axis.CalcAngle2D(); // required to calculate the quadrant
|
|
}
|
|
|
|
// Calculate currently visible quadrant
|
|
mi_Quadrant.Calculate(mi_Mouse.md_Phi, i_MainAxisX, i_MainAxisY, i_MainAxisZ);
|
|
|
|
// Add raster lines in 6 different directions
|
|
if (me_Raster >= eRaster.Raster)
|
|
{
|
|
for (int A = 0; A < 6; A++) // iterate Axes X,Y,Z twice
|
|
{
|
|
int F = A;
|
|
int S = A;
|
|
|
|
// Combine X+Y, Y+Z, Z+X, Y+X, Z+Y, X+Z
|
|
if (A >= 3) F++;
|
|
else S++;
|
|
|
|
eCoord e_First = (eCoord)(F % 3);
|
|
eCoord e_Second = (eCoord)(S % 3);
|
|
|
|
// Define which mirror operations are allowed for this raster line
|
|
eMirror e_Mirror = eMirror.None;
|
|
if (e_Second == eCoord.X) e_Mirror = eMirror.X;
|
|
if (e_Second == eCoord.Y) e_Mirror = eMirror.Y;
|
|
if (e_Second == eCoord.Z) e_Mirror = eMirror.Z;
|
|
|
|
cLine i_FirstLine = mi_AxisLines[(int)e_First]; // Main axis
|
|
cLine i_SecndLine = mi_AxisLines[(int)e_Second]; // Main axis
|
|
|
|
double d_SecndStart = i_SecndLine.mi_Points[0].mi_P3D.GetValue(e_Second);
|
|
double d_SecndEnd = i_SecndLine.mi_Points[1].mi_P3D.GetValue(e_Second);
|
|
|
|
// Distance between raster lines
|
|
double d_Interval = CalculateInterval(d_SecndEnd - d_SecndStart);
|
|
|
|
int s32_Start = (int)(d_SecndStart / d_Interval) - 1;
|
|
int s32_End = (int)(d_SecndEnd / d_Interval) + 1;
|
|
|
|
for (int L = s32_Start; L < s32_End; L++) // iterate raster lines
|
|
{
|
|
double d_Offset = d_Interval * L;
|
|
|
|
if (d_Offset < d_SecndStart || d_Offset > d_SecndEnd)
|
|
continue;
|
|
|
|
cLine i_Raster = new cLine(this, e_First, e_Second, e_Mirror);
|
|
|
|
i_Raster.mi_Points[0].mi_P3D = i_FirstLine.mi_Points[0].mi_P3D.Clone();
|
|
i_Raster.mi_Points[1].mi_P3D = i_FirstLine.mi_Points[1].mi_P3D.Clone();
|
|
|
|
i_Raster.mi_Points[0].mi_P3D.SetValue(e_Second, d_Offset);
|
|
i_Raster.mi_Points[1].mi_P3D.SetValue(e_Second, d_Offset);
|
|
|
|
i_Raster.ms_Label = FormatDouble(d_Offset);
|
|
|
|
// Do not draw the raster line at value zero if it equals the main axis and
|
|
// the axis is not mirrored
|
|
if (L == 0 && !mi_Axis[(int)e_Second].Mirror && i_Raster.CoordEquals(mi_AxisLines[(int)e_First]))
|
|
continue;
|
|
|
|
if ((e_First == eCoord.X && e_Second == eCoord.Z) || // Blue
|
|
(e_First == eCoord.Z && e_Second == eCoord.X))
|
|
{
|
|
i_Raster.md_Sort = mi_Quadrant.md_SortXZ;
|
|
}
|
|
else if ((e_First == eCoord.Z && e_Second == eCoord.Y) || // Green
|
|
(e_First == eCoord.Y && e_Second == eCoord.Z))
|
|
{
|
|
i_Raster.md_Sort = mi_Quadrant.md_SortYZ;
|
|
}
|
|
else // X + Y Red
|
|
{
|
|
i_Raster.md_Sort = mi_Quadrant.md_SortXY;
|
|
|
|
// Special case: XY raster lines must be shifted down to negative end of
|
|
// Z axis
|
|
i_Raster.mi_Points[0].mi_P3D.Z = i_MainAxisZ.mi_Points[0].mi_P3D.Z;
|
|
i_Raster.mi_Points[1].mi_P3D.Z = i_MainAxisZ.mi_Points[0].mi_P3D.Z;
|
|
}
|
|
|
|
i_Raster.Project3D();
|
|
mi_AxisLines.Add(i_Raster);
|
|
} // for (L)
|
|
} // for (A)
|
|
} // if (Raster)
|
|
|
|
// Remove the green and blue main axes if Z value zero is outside the visible range
|
|
if (mi_Bounds.Z.Min > 0.0 || mi_Bounds.Z.Max < 0.0)
|
|
{
|
|
mi_AxisLines.Remove(i_MainAxisX);
|
|
mi_AxisLines.Remove(i_MainAxisY);
|
|
}
|
|
|
|
// Move the graph to the left when labels are enabled
|
|
if (me_Raster == eRaster.Labels)
|
|
{
|
|
int s32_LabelWidth = 0;
|
|
foreach (cLine i_Line in mi_AxisLines)
|
|
{
|
|
if (i_Line.me_Line == eCoord.Y && i_Line.me_Offset == eCoord.Z)
|
|
{
|
|
SizeF k_Size = i_Graph.MeasureString(i_Line.ms_Label, Font);
|
|
s32_LabelWidth = Math.Max(s32_LabelWidth, (int)k_Size.Width);
|
|
}
|
|
}
|
|
mi_Mouse.mk_OffCoord.X -= s32_LabelWidth / 2;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Makes a color brigther
|
|
/// </summary>
|
|
private static Color BrightenColor(Color c_Color)
|
|
{
|
|
int s32_Red = c_Color.R + (255 - c_Color.R) / 2;
|
|
int s32_Green = c_Color.G + (255 - c_Color.G) / 2;
|
|
int s32_Blue = c_Color.B + (255 - c_Color.B) / 2;
|
|
|
|
return Color.FromArgb(255, s32_Red, s32_Green, s32_Blue);
|
|
}
|
|
|
|
/// <summary>
|
|
/// returns intervals of 0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, etc... The count of intervals
|
|
/// which fit into the range is always between 5 and 10
|
|
/// </summary>
|
|
private static double CalculateInterval(double d_Range)
|
|
{
|
|
double d_Factor = Math.Pow(10.0, Math.Floor(Math.Log10(d_Range)));
|
|
if (d_Range / d_Factor >= 5.0)
|
|
return d_Factor;
|
|
else if (d_Range / (d_Factor / 2.0) >= 5.0)
|
|
return d_Factor / 2.0;
|
|
else
|
|
return d_Factor / 5.0;
|
|
}
|
|
|
|
// md_Label = 123.000 --> display "123" md_Label = 15.700 --> display "15.7" md_Label =
|
|
// 4.260 --> display "4.26" md_Label = 0.834 --> display "0.834"
|
|
public static String FormatDouble(double d_Label)
|
|
{
|
|
return d_Label.ToString("0.000", CultureInfo.InvariantCulture).TrimEnd('0').TrimEnd('.');
|
|
}
|
|
|
|
/// <summary>
|
|
/// For debugging drawing speed.
|
|
/// ATTENTION: The first time the delays are wrong because of JIT compilation delays.
|
|
/// </summary>
|
|
private static void FormatStopwatch(String s_Measure, Stopwatch i_Watch, StringBuilder i_Debug)
|
|
{
|
|
double d_Elapsed = (double)i_Watch.ElapsedTicks / TimeSpan.TicksPerMillisecond;
|
|
i_Debug.AppendLine(s_Measure.PadRight(17) + d_Elapsed.ToString("0.000") + " ms");
|
|
i_Watch.Restart();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the 2D point X,Y lies on the line between i_Start and i_End within s32_MaxDist
|
|
/// All coordinates in pixels.
|
|
/// </summary>
|
|
private static bool IsPointOnLine(cPoint2D i_Start, cPoint2D i_End, int X, int Y, int s32_MaxDist)
|
|
{
|
|
double d_DeltaX = i_End.md_X - i_Start.md_X;
|
|
double d_DeltaY = i_End.md_Y - i_Start.md_Y;
|
|
|
|
int s32_StepsX = Math.Abs((int)d_DeltaX);
|
|
int s32_StepsY = Math.Abs((int)d_DeltaY);
|
|
int s32_Steps = Math.Max(s32_StepsX, s32_StepsY);
|
|
|
|
d_DeltaX /= s32_Steps;
|
|
d_DeltaY /= s32_Steps;
|
|
|
|
cPoint2D i_Point = i_Start.Clone();
|
|
for (int S = 0; S <= s32_Steps; S++)
|
|
{
|
|
if (i_Point.CalcDistanceTo(X, Y) <= s32_MaxDist)
|
|
return true;
|
|
|
|
i_Point.md_X += d_DeltaX;
|
|
i_Point.md_Y += d_DeltaY;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// =================================== DRAWING =====================================
|
|
|
|
protected override void OnPaintBackground(PaintEventArgs e)
|
|
{
|
|
// Background is painted flickerless in OnPaint()
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is invoked by Invalidate() when the GUI thread becomes idle.
|
|
/// </summary>
|
|
protected override void OnPaint(PaintEventArgs e)
|
|
{
|
|
base.OnPaint(e);
|
|
// Stupidly the .NET framework draws a red cross if any exception occurres in OnPaint()
|
|
try
|
|
{
|
|
Render(e.Graphics);
|
|
}
|
|
catch (Exception Ex)
|
|
{
|
|
e.Graphics.ResetTransform();
|
|
e.Graphics.Clear(Color.DarkRed);
|
|
e.Graphics.DrawString(Ex.Message + "\n" + Ex.StackTrace, new Font("Verdana", 8, FontStyle.Bold), Brushes.White, 10, 10);
|
|
return;
|
|
}
|
|
|
|
DrawBorder(e.Graphics);
|
|
}
|
|
|
|
public Bitmap GetScreenshot()
|
|
{
|
|
Debug.Assert(!InvokeRequired); // Call only from GUI thread
|
|
|
|
Bitmap i_Bmp = new Bitmap(ClientSize.Width, ClientSize.Height);
|
|
using (Graphics i_Graph = Graphics.FromImage(i_Bmp))
|
|
{
|
|
Render(i_Graph);
|
|
}
|
|
return i_Bmp;
|
|
}
|
|
|
|
private void Render(Graphics i_Graph)
|
|
{
|
|
i_Graph.Clear(BackColor);
|
|
|
|
foreach (cMessgData i_Mesg in mi_MessageData)
|
|
{
|
|
i_Mesg.Draw(i_Graph, ClientRectangle);
|
|
}
|
|
|
|
// If there are no 3D objects --> only show the messages. Do not show an empty
|
|
// coordinate system.
|
|
if (mi_UserObjects.Count == 0)
|
|
return;
|
|
|
|
mi_UndoBuffer.Init();
|
|
|
|
#if DEBUG_SPEED
|
|
StringBuilder i_Debug = new StringBuilder();
|
|
Stopwatch i_Watch = new Stopwatch();
|
|
i_Debug.Append("--------------------------\n");
|
|
i_Watch.Start();
|
|
#endif
|
|
|
|
// Speed optimization: Add points to tooltip only if required
|
|
if ((me_Recalculate & eRecalculate.AddRemove) > 0)
|
|
{
|
|
mi_Tooltip.Clear();
|
|
foreach (cDrawObj i_Object in mi_UserObjects)
|
|
{
|
|
foreach (cPoint i_Point in i_Object.mi_Points)
|
|
{
|
|
mi_Tooltip.AddPoint(i_Point);
|
|
}
|
|
}
|
|
|
|
#if DEBUG_SPEED
|
|
FormatStopwatch("Add Tooltip: ", i_Watch, i_Debug);
|
|
#endif
|
|
}
|
|
|
|
// Speed optimization: Calculate coordinate system only if required
|
|
if ((me_Recalculate & eRecalculate.CoordSystem) > 0)
|
|
{
|
|
// Calculate Min/Max for all UserObjects, assign mi_Inst to all user objects.
|
|
mi_Bounds.Calculate();
|
|
|
|
// Calculate factors for transformation
|
|
NormalizeRanges();
|
|
|
|
// Fills mi_AxisLines with 3 main axis and 42 raster lines
|
|
CreateCoordinateSystem(i_Graph);
|
|
|
|
#if DEBUG_SPEED
|
|
FormatStopwatch("Coord System: ", i_Watch, i_Debug);
|
|
#endif
|
|
}
|
|
|
|
// Speed optimization: Calculate 3D objects only if required
|
|
if ((me_Recalculate & eRecalculate.Objects) > 0)
|
|
{
|
|
foreach (cDrawObj i_Object in mi_UserObjects)
|
|
{
|
|
// This must not be called for axes
|
|
i_Object.CalcSortOrder();
|
|
|
|
// This must not be called for axes reload Pens, Brushes from User objects or
|
|
// from ColorScheme
|
|
i_Object.ProcessColors();
|
|
|
|
// Project 3D --> 2D, calculate line width, shape radius,...
|
|
i_Object.Project3D();
|
|
}
|
|
|
|
#if DEBUG_SPEED
|
|
FormatStopwatch("Prepare Objects: ", i_Watch, i_Debug);
|
|
#endif
|
|
}
|
|
|
|
// Speed optimization: Merge lists only if at least one of them has changed
|
|
if ((me_Recalculate & (eRecalculate.AddRemove | eRecalculate.CoordSystem)) > 0)
|
|
{
|
|
mi_AllObjects.Clear();
|
|
mi_AllObjects.AddRange(mi_AxisLines);
|
|
mi_AllObjects.AddRange(mi_UserObjects);
|
|
|
|
#if DEBUG_SPEED
|
|
FormatStopwatch("Merge Lists: ", i_Watch, i_Debug);
|
|
#endif
|
|
}
|
|
|
|
// Speed optimization: sort draw objects only if required
|
|
if ((me_Recalculate & (eRecalculate.AddRemove | eRecalculate.CoordSystem | eRecalculate.Objects)) > 0)
|
|
{
|
|
// Sort draw order from background to foreground
|
|
mi_AllObjects.Sort();
|
|
|
|
#if DEBUG_SPEED
|
|
FormatStopwatch("Sort List: ", i_Watch, i_Debug);
|
|
#endif
|
|
}
|
|
|
|
me_Recalculate = eRecalculate.Nothing;
|
|
|
|
// ---------------------------------------------------
|
|
|
|
// Draw axis legends in bottom left corner
|
|
if (me_LegendPos == eLegendPos.BottomLeft)
|
|
{
|
|
int X = 4;
|
|
int Y = ClientSize.Height - Font.Height - 4;
|
|
for (int i = 2; i >= 0; i--)
|
|
{
|
|
if (String.IsNullOrEmpty(mi_Axis[i].LegendText))
|
|
continue;
|
|
|
|
String s_Disp = String.Format("{0}: {1}", (eCoord)i, mi_Axis[i].LegendText);
|
|
i_Graph.DrawString(s_Disp, Font, mi_Axis[i].LegendBrush, X, Y);
|
|
Y -= Font.Height;
|
|
}
|
|
}
|
|
|
|
// Draw rotation legends at top
|
|
if (mi_TopLegendBrush != null)
|
|
{
|
|
String[] s_Legend = new String[] { "Rotation:", "Elevation:", "Distance:" };
|
|
String[] s_Value = new String[] { String.Format("{0:+#;-#;0}°", (int)mi_Mouse.md_Phi),
|
|
String.Format("{0:+#;-#;0}°", (int)mi_Mouse.md_Theta),
|
|
String.Format("{0}", (int)mi_Mouse.md_Rho) };
|
|
|
|
SizeF k_Size = i_Graph.MeasureString(s_Legend[1], Font); // measure the widest string
|
|
int X = 4;
|
|
int Y = 3;
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
i_Graph.DrawString(s_Legend[i], Font, mi_TopLegendBrush, X, Y);
|
|
i_Graph.DrawString(s_Value[i], Font, mi_TopLegendBrush, X + k_Size.Width, Y);
|
|
Y += Font.Height;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------
|
|
|
|
// Set X, Y offset which user has set by mouse dragging with SHIFT key pressed
|
|
i_Graph.TranslateTransform(mi_Mouse.mk_OffMove.X + mi_Mouse.mk_OffCoord.X,
|
|
mi_Mouse.mk_OffMove.Y + mi_Mouse.mk_OffCoord.Y);
|
|
|
|
SmoothingMode e_Smooth = SmoothingMode.Invalid;
|
|
|
|
foreach (cDrawObj i_DrawObj in mi_AllObjects)
|
|
{
|
|
if (!i_DrawObj.IsValid)
|
|
continue; // avoid overflow exception or hanging
|
|
|
|
if (e_Smooth != i_DrawObj.me_SmoothMode) // avoid unneccessary calls into GDI+ (speed optimization)
|
|
{
|
|
e_Smooth = i_DrawObj.me_SmoothMode;
|
|
i_Graph.SmoothingMode = i_DrawObj.me_SmoothMode;
|
|
}
|
|
|
|
// Draw Line, Shape, Polygon
|
|
i_DrawObj.Render(i_Graph);
|
|
|
|
// Draw labels and legends
|
|
cLine i_Line = i_DrawObj as cLine;
|
|
if (i_Line != null &&
|
|
i_Line.me_Line != eCoord.Invalid &&
|
|
mi_Quadrant.mb_BottomView == false && // no label in bottom view
|
|
mi_Quadrant.ms32_Quadrant == 3) // showing labels makes sense only in quadrant 3
|
|
{
|
|
bool b_Legend = false;
|
|
|
|
// Draw axis legends at end of of main axis
|
|
if (me_LegendPos == eLegendPos.AxisEnd && i_Line.me_Line == i_Line.me_Offset)
|
|
{
|
|
cAxis i_Axis = mi_Axis[(int)i_Line.me_Line];
|
|
if (!String.IsNullOrEmpty(i_Axis.LegendText))
|
|
{
|
|
StringFormat i_Align = new StringFormat();
|
|
PointF k_Pos = i_Line.mi_Points[1].mi_P2D.Coord;
|
|
switch (i_Line.me_Line)
|
|
{
|
|
case eCoord.X:
|
|
k_Pos.X += (float)mi_Transform.ProjectXY(5, -5);
|
|
k_Pos.Y += (float)mi_Transform.ProjectXY(5, -Font.Height / 2 - 2);
|
|
i_Align.Alignment = StringAlignment.Far;
|
|
break;
|
|
|
|
case eCoord.Y:
|
|
k_Pos.X += 5;
|
|
k_Pos.Y -= Font.Height / 2;
|
|
break;
|
|
|
|
case eCoord.Z:
|
|
k_Pos.X -= 5;
|
|
k_Pos.Y -= Font.Height + 5;
|
|
break;
|
|
}
|
|
|
|
i_Graph.DrawString(i_Axis.LegendText, Font, i_Axis.LegendBrush, k_Pos, i_Align);
|
|
b_Legend = true; // do not draw a label if a legend has already been drawn (see Demo Sphere)
|
|
}
|
|
}
|
|
|
|
// Draw labels of raster lines
|
|
if (me_Raster == eRaster.Labels && !b_Legend && !String.IsNullOrEmpty(i_Line.ms_Label))
|
|
{
|
|
Brush i_Brush = null;
|
|
StringFormat i_Align = new StringFormat();
|
|
PointF k_Pos = i_Line.mi_Points[1].mi_P2D.Coord;
|
|
|
|
if (i_Line.me_Line == eCoord.Y)
|
|
{
|
|
if (i_Line.me_Offset == eCoord.X)
|
|
{
|
|
k_Pos.X += (float)mi_Transform.ProjectXY(5, -5);
|
|
k_Pos.Y += (float)mi_Transform.ProjectXY(-Font.Height / 2, 5);
|
|
i_Brush = AxisX.LegendBrush;
|
|
}
|
|
else // Y (Main axis) and Z (Raster)
|
|
{
|
|
k_Pos.X += 5;
|
|
k_Pos.Y -= Font.Height / 2;
|
|
i_Brush = AxisZ.LegendBrush;
|
|
}
|
|
}
|
|
else if (i_Line.me_Line == eCoord.X && i_Line.me_Offset == eCoord.Y)
|
|
{
|
|
k_Pos.X += (float)mi_Transform.ProjectXY(5, -5);
|
|
k_Pos.Y += (float)mi_Transform.ProjectXY(5, -Font.Height / 2);
|
|
i_Align.Alignment = StringAlignment.Far;
|
|
i_Brush = AxisY.LegendBrush;
|
|
}
|
|
|
|
if (i_Brush != null)
|
|
i_Graph.DrawString(i_Line.ms_Label, Font, i_Brush, k_Pos, i_Align);
|
|
}
|
|
} // if (Line != null)
|
|
} // foreach (cDrawObj)
|
|
|
|
#if DEBUG_SPEED
|
|
FormatStopwatch("Render Objects: ", i_Watch, i_Debug);
|
|
Debug.Print(i_Debug.ToString().TrimEnd());
|
|
#endif
|
|
}
|
|
|
|
// ============================================================================
|
|
|
|
protected override void OnCreateControl()
|
|
{
|
|
base.OnCreateControl();
|
|
|
|
// This control draws it's own border. See DrawBorder()
|
|
BorderStyle = BorderStyle.None;
|
|
|
|
// This is the size of the control defined in Visual Studio Form Designer
|
|
mi_Transform.SetInitialSize(ClientSize);
|
|
}
|
|
|
|
protected override void OnSizeChanged(EventArgs e)
|
|
{
|
|
base.OnSizeChanged(e);
|
|
|
|
// This may be called with an invalid size before the control is created!
|
|
mi_Transform.SetSize(ClientSize);
|
|
Invalidate(); // Windows will call OnPaint() when the GUI thread becomes idle.
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is only called when the user moves the trackbar, not when TrackBar.Value is set programmatically.
|
|
/// </summary>
|
|
private void OnTrackbarScroll(object sender, EventArgs e)
|
|
{
|
|
mi_Mouse.OnTrackBarScroll();
|
|
mi_Transform.SetCoefficients(mi_Mouse);
|
|
Invalidate(); // Windows will call OnPaint() when the GUI thread becomes idle.
|
|
}
|
|
|
|
// --------------------------------------------
|
|
|
|
protected override void OnGotFocus(EventArgs e)
|
|
{
|
|
base.OnGotFocus(e);
|
|
DrawBorder(null);
|
|
}
|
|
|
|
protected override void OnLostFocus(EventArgs e)
|
|
{
|
|
base.OnLostFocus(e);
|
|
DrawBorder(null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Draw a one pixel border around the control which may change color when the control has
|
|
/// the focus.
|
|
/// </summary>
|
|
private void DrawBorder(Graphics i_Graphics)
|
|
{
|
|
Pen i_Pen = mi_BorderPens[Focused ? 1 : 0];
|
|
if (i_Pen != null)
|
|
{
|
|
BorderStyle = BorderStyle.None;
|
|
|
|
if (i_Graphics == null)
|
|
i_Graphics = Graphics.FromHwnd(Handle);
|
|
|
|
i_Graphics.ResetTransform();
|
|
Rectangle r_Rect = ClientRectangle;
|
|
i_Graphics.DrawRectangle(i_Pen, r_Rect.X, r_Rect.Y, r_Rect.Width - 1, r_Rect.Height - 1);
|
|
}
|
|
}
|
|
|
|
// ============================== MOUSE =====================================
|
|
|
|
protected override void OnMouseDown(MouseEventArgs e)
|
|
{
|
|
base.OnMouseDown(e);
|
|
|
|
mi_Tooltip.Hide();
|
|
mi_Mouse.mk_LastPos = e.Location;
|
|
|
|
if (mi_AllObjects.Count == 0)
|
|
return;
|
|
|
|
int s32_UID = (int)Control.ModifierKeys | (int)e.Button;
|
|
|
|
cUserInput i_Input;
|
|
if (mi_UserInputs.TryGetValue(s32_UID, out i_Input))
|
|
{
|
|
switch (i_Input.Action)
|
|
{
|
|
case eMouseAction.SelectObj:
|
|
case eMouseAction.Callback:
|
|
OnSelMouseDown(e.X, e.Y, i_Input);
|
|
break;
|
|
|
|
default:
|
|
mi_Mouse.me_Action = i_Input.Action;
|
|
Cursor = i_Input.Cursor;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
protected override void OnMouseMove(MouseEventArgs e)
|
|
{
|
|
base.OnMouseMove(e);
|
|
|
|
int s32_DeltaX = e.X - mi_Mouse.mk_LastPos.X;
|
|
int s32_DeltaY = e.Y - mi_Mouse.mk_LastPos.Y;
|
|
mi_Mouse.mk_LastPos = e.Location;
|
|
|
|
switch (mi_Mouse.me_Action)
|
|
{
|
|
case eMouseAction.Move:
|
|
mi_Tooltip.Hide();
|
|
mi_Mouse.mk_OffMove.X += s32_DeltaX;
|
|
mi_Mouse.mk_OffMove.Y += s32_DeltaY;
|
|
Invalidate(); // Windows will call OnPaint() when the GUI thread becomes idle.
|
|
break;
|
|
|
|
case eMouseAction.Rho:
|
|
case eMouseAction.Theta:
|
|
case eMouseAction.Phi:
|
|
case eMouseAction.ThetaAndPhi:
|
|
mi_Tooltip.Hide();
|
|
mi_Mouse.OnMouseMove(s32_DeltaX, s32_DeltaY);
|
|
mi_Transform.SetCoefficients(mi_Mouse);
|
|
Invalidate(); // Windows will call OnPaint() when the GUI thread becomes idle.
|
|
break;
|
|
|
|
case eMouseAction.SelectObj:
|
|
case eMouseAction.Callback:
|
|
int s32_UID = (int)Control.ModifierKeys | (int)e.Button;
|
|
cUserInput i_Input;
|
|
if (mi_UserInputs.TryGetValue(s32_UID, out i_Input))
|
|
{
|
|
if (i_Input.Action == mi_Mouse.me_Action)
|
|
{
|
|
// Mouse.Y coordinates have the zero point at top left Editor3D
|
|
// coordinates have the zero point at bottom left --> negate Y
|
|
SelectionCallback(eSelEvent.MouseDrag, i_Input.Modifiers, s32_DeltaX, -s32_DeltaY, mi_DragObject);
|
|
}
|
|
else
|
|
{
|
|
// The modifier keys have changed --> abort sending events to the callback
|
|
OnMouseExit();
|
|
}
|
|
}
|
|
break;
|
|
|
|
case eMouseAction.None:
|
|
mi_Tooltip.OnMouseMove(e);
|
|
break;
|
|
}
|
|
}
|
|
|
|
protected override void OnMouseUp(MouseEventArgs e)
|
|
{
|
|
base.OnMouseUp(e);
|
|
OnMouseExit();
|
|
}
|
|
|
|
protected override void OnMouseLeave(EventArgs e)
|
|
{
|
|
base.OnMouseLeave(e);
|
|
OnMouseExit();
|
|
}
|
|
|
|
private void OnMouseExit()
|
|
{
|
|
mi_Tooltip.Hide();
|
|
Cursor = Cursors.Arrow;
|
|
|
|
switch (mi_Mouse.me_Action)
|
|
{
|
|
case eMouseAction.SelectObj:
|
|
case eMouseAction.Callback:
|
|
SelectionCallback(eSelEvent.MouseUp, Keys.None, 0, 0, mi_DragObject);
|
|
break;
|
|
}
|
|
|
|
mi_DragObject = null;
|
|
mi_Mouse.me_Action = eMouseAction.None;
|
|
|
|
// Store any pending user changes on mouse-up
|
|
mi_UndoBuffer.Store();
|
|
}
|
|
|
|
protected override void OnMouseWheel(MouseEventArgs e)
|
|
{
|
|
base.OnMouseWheel(e);
|
|
mi_Tooltip.Hide();
|
|
|
|
if (mi_Mouse.OnMouseWheel(e.Delta))
|
|
{
|
|
mi_Transform.SetCoefficients(mi_Mouse);
|
|
Invalidate(); // Windows will call OnPaint() when the GUI thread becomes idle.
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Select 3D object or call selection callback function
|
|
/// </summary>
|
|
private void OnSelMouseDown(int X, int Y, cUserInput i_Input)
|
|
{
|
|
if (!mi_Selection.Enabled)
|
|
return;
|
|
|
|
cObject3D i_Found = FindObjectAt(X, Y, true);
|
|
|
|
if (mi_Selection.Callback != null)
|
|
{
|
|
// Start dragging even if i_Found == null
|
|
mi_DragObject = i_Found;
|
|
mi_Mouse.me_Action = i_Input.Action;
|
|
Cursor = i_Input.Cursor;
|
|
|
|
SelectionCallback(eSelEvent.MouseDown, i_Input.Modifiers, 0, 0, mi_DragObject);
|
|
return;
|
|
}
|
|
|
|
// No callback assigned --> toggle selection of 3D object.
|
|
if (i_Found != null && i_Input.Action == eMouseAction.SelectObj)
|
|
{
|
|
// Not multiselect --> remove all current selections
|
|
if (!mi_Selection.MultiSelect)
|
|
mi_Selection.DeSelectAll();
|
|
|
|
i_Found.Selected = !i_Found.Selected; // toggle selection
|
|
|
|
Invalidate(); // Windows will call OnPaint() when the GUI thread becomes idle.
|
|
}
|
|
}
|
|
|
|
// ============================== KEYBOARD =====================================
|
|
|
|
protected override void OnKeyDown(KeyEventArgs e)
|
|
{
|
|
base.OnKeyDown(e);
|
|
|
|
if (e.Control && !e.Alt && !e.Shift)
|
|
{
|
|
switch (e.KeyCode)
|
|
{
|
|
case Keys.Y: mi_UndoBuffer.Redo(); break; // CTRL + Y --> Redo
|
|
case Keys.Z: mi_UndoBuffer.Undo(); break; // CTRL + Z --> Undo
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========================== SELECTION CALLBACK ===============================
|
|
|
|
/// <summary>
|
|
/// Selection.Callback is called on the mouse events Down, Move and Up if cUserInput.Action
|
|
/// = Callback or SelectObj The callback must never throw an exception. i_Object may be
|
|
/// cPoint3D if Selection.SinglePoints = true i_Object may be cShape3D, cLine3D, cPolygon3D
|
|
/// if Selection.SinglePoints = false i_Object may be null if the user has clicked a
|
|
/// location without a 3D object. In this case the callback can call
|
|
/// Selection.GetSelectedObjects() / GetSelectedPoints() to obtain the previous selections.
|
|
/// The callback is responsible for selecting / deselecting the desired objects. If the
|
|
/// callback does not change the selection status, the 3D object will never be selected /
|
|
/// deselected. The callback is allowed to show a MessageBox to the user.
|
|
/// </summary>
|
|
private void SelectionCallback(eSelEvent e_Event, Keys e_Modifiers, int s32_DeltaX, int s32_DeltaY, cObject3D i_Object)
|
|
{
|
|
if (!mi_Selection.Enabled || mi_Selection.Callback == null)
|
|
return;
|
|
try
|
|
{
|
|
eInvalidate e_Invalidate = mi_Selection.Callback(e_Event, e_Modifiers, s32_DeltaX, s32_DeltaY, i_Object);
|
|
|
|
if (e_Invalidate == eInvalidate.CoordSystem)
|
|
me_Recalculate |= eRecalculate.CoordSystem;
|
|
|
|
if (e_Invalidate != eInvalidate.NoChange)
|
|
Invalidate(); // Windows will call OnPaint() when the GUI thread becomes idle.
|
|
}
|
|
catch (Exception Ex)
|
|
{
|
|
MessageBox.Show(TopLevelControl, "Your callback function has crashed:\n\n" + Ex.Message + "\n\n" + Ex.StackTrace,
|
|
"Bug Alarm", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
|
}
|
|
}
|
|
}
|
|
} |