From f99fb972b85bb48e8c829102acf26c67316cc73f Mon Sep 17 00:00:00 2001 From: Jan Schopohl Date: Mon, 5 Jan 2026 19:49:50 +0100 Subject: [PATCH 01/12] Added CADability.Avalonia project Added a option to compile without Windows dependencies using the `UiFramework=Avalonia` option. Functionality of the new UI will be added later. --- .../CADability.Avalonia.csproj | 20 +++++++ CADability.Avalonia/CadForm.axaml | 12 ++++ CADability.Avalonia/CadForm.axaml.cs | 12 ++++ CADability.Avalonia/app.manifest | 18 ++++++ CADability/BooleanOperation.cs | 58 ++++++++++--------- CADability/CADability.csproj | 11 ++-- ShapeIt/ChamferEdgesAction.cs | 2 + ShapeIt/FeedbackArrow.cs | 4 +- ShapeIt/MainForm.Designer.cs | 5 +- ShapeIt/MainForm.axaml | 40 +++++++++++++ ShapeIt/MainForm.axaml.cs | 14 +++++ ShapeIt/MainForm.cs | 8 ++- ShapeIt/MateFacesAction.cs | 2 + ShapeIt/ModellingPropertyEntries.cs | 32 +++++----- ShapeIt/ParametricPositionAction.cs | 4 +- ShapeIt/Program.cs | 22 ++++++- ShapeIt/Properties/Settings.Designer.cs | 10 ++-- ShapeIt/RoundEdges.cs | 4 +- ShapeIt/ShapeIt.axaml | 10 ++++ ShapeIt/ShapeIt.axaml.cs | 23 ++++++++ ShapeIt/ShapeIt.csproj | 33 ++++++++++- ShapeIt/ShellExtensions.cs | 6 +- 22 files changed, 284 insertions(+), 66 deletions(-) create mode 100644 CADability.Avalonia/CADability.Avalonia.csproj create mode 100644 CADability.Avalonia/CadForm.axaml create mode 100644 CADability.Avalonia/CadForm.axaml.cs create mode 100644 CADability.Avalonia/app.manifest create mode 100644 ShapeIt/MainForm.axaml create mode 100644 ShapeIt/MainForm.axaml.cs create mode 100644 ShapeIt/ShapeIt.axaml create mode 100644 ShapeIt/ShapeIt.axaml.cs diff --git a/CADability.Avalonia/CADability.Avalonia.csproj b/CADability.Avalonia/CADability.Avalonia.csproj new file mode 100644 index 00000000..62b845b8 --- /dev/null +++ b/CADability.Avalonia/CADability.Avalonia.csproj @@ -0,0 +1,20 @@ + + + net8.0 + enable + app.manifest + true + + + + + + + + + + None + All + + + diff --git a/CADability.Avalonia/CadForm.axaml b/CADability.Avalonia/CadForm.axaml new file mode 100644 index 00000000..698df612 --- /dev/null +++ b/CADability.Avalonia/CadForm.axaml @@ -0,0 +1,12 @@ + + + + + + diff --git a/CADability.Avalonia/CadForm.axaml.cs b/CADability.Avalonia/CadForm.axaml.cs new file mode 100644 index 00000000..109da9c4 --- /dev/null +++ b/CADability.Avalonia/CadForm.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace CADability.Avalonia +{ + public partial class CadForm : UserControl + { + public CadForm() + { + InitializeComponent(); + } + } +} diff --git a/CADability.Avalonia/app.manifest b/CADability.Avalonia/app.manifest new file mode 100644 index 00000000..09474690 --- /dev/null +++ b/CADability.Avalonia/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/CADability/BooleanOperation.cs b/CADability/BooleanOperation.cs index 21587cc2..f0069aea 100644 --- a/CADability/BooleanOperation.cs +++ b/CADability/BooleanOperation.cs @@ -5,7 +5,9 @@ using CADability.Substitutes; using Point = CADability.GeoObject.Point; using MathNet.Numerics; +#if !AVALONIA using Microsoft.VisualStudio.DebuggerVisualizers; +#endif using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -20,8 +22,8 @@ namespace CADability /* This is the attempt to simplify the implementation of BRepIntersection/BRepOperation * There were too many exceptions and special cases in BRepOperation, especially concerning the overlapping faces, both * same oriantation and opposite orientation. I think, we don't need to consider overlapping faces if we consequently intersect faces - * which have edges on faces of the other shell. - * + * which have edges on faces of the other shell. + * * - put all faces (of both shells) in a single octtree * - use the octtree nodes to find pairs of faces (of different shells) which intersect each other and compute the intersection edges * - check whether edges of one face intersect with the other face @@ -49,13 +51,13 @@ namespace CADability */ /// - /// + /// /// public class BooleanOperation : OctTree { private Shell shell1; // the two shells, which are intersected private Shell shell2; // either shell1 and shell2 are set or multipleFaces is set - private List multipleFaces; // the faces, which are intersected. + private List multipleFaces; // the faces, which are intersected. private Dictionary originalToClonedEdges; // points from the original edges to the clones we are working on private Dictionary originalToClonedVertices; // points from the original vertices to the clones we are working on private Dictionary originalToClonedFaces; // points from the original faces to the clones we are working on @@ -80,7 +82,7 @@ public class BooleanOperation : OctTree Dictionary<(Edge edge, Face face), (List vertices, bool curveIsInSurface)> FaceEdgeIntersections; // to avoid duplicate calls to GetFaceEdgeIntersection HashSet commonOverlappingFaces; // Faces, which have been newly created as common parts of overlapping faces - HashSet cancelledfaces; // Faces, which cancel each other, they have the same area but are opposite oriented + HashSet cancelledfaces; // Faces, which cancel each other, they have the same area but are opposite oriented Dictionary> faceToIntersectionEdges; // faces of both shells with their intersection edges Dictionary> faceToCommonFaces; // faces which have overlapping common parts on them Dictionary> edgesToSplit; @@ -572,7 +574,7 @@ from j in Enumerable.Range(i + 1, list.Count - i - 1) // Problme wäre die Genauigkeit, wenn beim BooleanOperation.generateCycles die Richtung genommen wird... if (con1 is InterpolatedDualSurfaceCurve.ProjectedCurve pon1 && con2 is InterpolatedDualSurfaceCurve.ProjectedCurve pon2 && tr is InterpolatedDualSurfaceCurve idsc) - { // con1 und con2 müssen auf tr verweisen, sonst kann man das Face später nicht mit "ReverseOrientation" umdrehen. Dort wird nämlich die + { // con1 und con2 müssen auf tr verweisen, sonst kann man das Face später nicht mit "ReverseOrientation" umdrehen. Dort wird nämlich die // surface verändert, und die muss bei allen Kurven die selbe sein pon1.SetCurve3d(idsc); pon2.SetCurve3d(idsc); @@ -629,7 +631,7 @@ from j in Enumerable.Range(i + 1, list.Count - i - 1) } //TODO: not implemented yet: // if we arrive here with edgeFound==false, there is a tangential intersection which is not an edge on one of the two faces. Now we have two cases: it is either - // a face touching the other face (like a cylinder touches a plane, or it is a real intersection, where one face goes through the other face + // a face touching the other face (like a cylinder touches a plane, or it is a real intersection, where one face goes through the other face // like an S-curve touches and crosses a line in the middle. //we try to find two points close to the middlepoint of the intersection curve where we get stable normals which are not parallel if (normalsCrossedMiddle.Length < 100 * Precision.eps) @@ -1133,7 +1135,7 @@ public enum Operation { union, intersection, difference, clip, connectMultiple, // createEdgeFaceIntersections(); // find intersection of edges with faces from different shells // splitEdges(); // split the edges at the found intersection positions // combineVerticesMultipleFaces(); // combine geometric close vertices - // removeIdenticalOppositeFaces(); // + // removeIdenticalOppositeFaces(); // // createNewEdges(); // populate faceToIntersectionEdges : for each face a list of intersection curves // createInnerFaceIntersections(); // find additional intersection curves where faces intersect, but edges don't intersect (rare) // TrimmIntersectionEdges(); @@ -1832,7 +1834,7 @@ public static (Shell[] upperPart, Shell[] lowerPart) SplitByFace(Shell toSplit, } /// /// Chamfer or bevel the provided edges. the edges must be connected and only two edges may have a common vertex. The edges must build a path. - /// We have two distances from the edge to make chamfers with different angles. All edges must belong to the . + /// We have two distances from the edge to make chamfers with different angles. All edges must belong to the . /// The first distance is on the primary face, the second on the other face (in most cases the distances are equal). /// /// @@ -1882,7 +1884,7 @@ public static Shell ChamferEdges(Face primaryFace, Edge[] edges, double primaryD // List fillets = new List(); // faces, that make the fillet // Dictionary> tangentialIntersectionEdges = new Dictionary>(); // Dictionary> rawFillets = new Dictionary>(); // the axis curve and simple fillets for each edge without joints - // Dictionary> joiningVertices = new Dictionary>(); // for each vertex there may be up to three involved edges + // Dictionary> joiningVertices = new Dictionary>(); // for each vertex there may be up to three involved edges // // 1.: create the simple fillets // foreach (Edge edg in edges) // { @@ -2101,7 +2103,7 @@ public static Shell ChamferEdges(Face primaryFace, Edge[] edges, double primaryD // Shell[] bores = bo.Result(); // if (bores != null && bores.Length == 1) // { - // if (bores[0].Faces.Length == 2) // this should be the case: + // if (bores[0].Faces.Length == 2) // this should be the case: // { // GeoObjectList fcs = bores[0].Decompose(); // if (fcs.Count == 2) @@ -2758,7 +2760,7 @@ public static bool TryFindIntersectingEdges(IEnumerable edges, out Edge e1 } listAB.Add(edge); - // fB → fA + // fB → fA if (!adj.TryGetValue(fB, out var nbrB)) { nbrB = new Dictionary>(); @@ -2800,11 +2802,11 @@ public static bool TryFindIntersectingEdges(IEnumerable edges, out Edge e1 public Shell[] Result() { // this method relies on - // - faceToIntersectionEdges: contains all faces, which intersect with other faces as keys and all the intersection edges, + // - faceToIntersectionEdges: contains all faces, which intersect with other faces as keys and all the intersection edges, // which are produced by other faces on this face as values. // - overlappingFaces and oppositeFaces: they contain pairs of faces, which overlap, with either same or oppostie orientation. - // - // The main algorithm does the following: split (or trimm or cut) a face according to the intersection edges on this face. + // + // The main algorithm does the following: split (or trimm or cut) a face according to the intersection edges on this face. // All edges are oriented, so it is possible to find outer edges and holes. A face might produce zero, one or multiple trimmed faces. // When all faces are trimmed, connect the trimmed faces with the untouched faces. this is the result. // @@ -3167,7 +3169,7 @@ public Shell[] Result() ++dbgc; } #endif - // each outline (only one in most cases) creates a new Face. + // each outline (only one in most cases) creates a new Face. for (int i = 0; i < numOutlines; i++) { Face fc = Face.Construct(); @@ -3199,7 +3201,7 @@ public Shell[] Result() if (operation == Operation.clip) return ClippedParts(trimmedFaces); if (dontReturnShell2) discardedFaces.UnionWith(shell2.Faces); - // Now trimmedFaces contains all faces which are cut by faces of the relative other shell, even those, where the other shell cuts + // Now trimmedFaces contains all faces which are cut by faces of the relative other shell, even those, where the other shell cuts // exactly along existing edges and nothing has been created. // The faces, which have been cut, i.e. faceToIntersectionEdges.Keys, are invalid now, we disconnect all edges from these faces #if DEBUG // show all trimmed faces @@ -3295,7 +3297,7 @@ public Shell[] Result() } } else if (edg.SecondaryFace == null || !trimmedFaces.Contains(edg.SecondaryFace) || !trimmedFaces.Contains(edg.PrimaryFace)) - { // only those edges, which + { // only those edges, which if (trimmedEdges.TryGetValue(dvk, out Edge other)) { if (other == edg) continue; @@ -3451,7 +3453,7 @@ public Shell[] Result() } ConnectOpenEdges(openEdges); // What about "nonManifoldEdges"? - // + // List res = new List(); // the result of this method. // allFaces now contains all the trimmed faces plus the faces, which are (directly or indirectly) connected (via edges) to the trimmed faces List nonManifoldParts = new List(); @@ -4014,7 +4016,7 @@ private List FindLoop(Edge edg, Vertex startVertex, Face onThisFace, HashS if (connected.Count == 0 || intersectionEdgeEndHere) { // (connected.Count == 0): dead end, no connection at endVertex - // intersectionEdgeEndHere: cannot go on, because we are following original edges and crossing at a vertex, + // intersectionEdgeEndHere: cannot go on, because we are following original edges and crossing at a vertex, // where an intersection edge ends. This is not allowed (e.g.: breps4) intersectionEdges.ExceptWith(res); originalEdges.ExceptWith(res); @@ -4339,7 +4341,7 @@ private Dictionary, List>> SortLoopsTopologically(List - /// Takes a set of edges and tries to connect them to closed loops. + /// Takes a set of edges and tries to connect them to closed loops. /// /// work on this set, which will be emptied /// orientation in respect to this face @@ -4448,8 +4450,8 @@ private List> FindPath(HashSet set, Vertex vertex1, Vertex vert } internal static bool SameEdge(Edge e1, Edge e2, double precision) - { // it is assumed that the two edges connect the same vertices - // it is tested whether they have the same geometry (but maybe different directions) + { // it is assumed that the two edges connect the same vertices + // it is tested whether they have the same geometry (but maybe different directions) // (two half circles may connect the same vertices but are not geometrically identical when they describe differnt parts of the same circle) if (e1.Curve3D != null && e2.Curve3D != null) return e1.Curve3D.DistanceTo(e2.Curve3D.PointAt(0.5)) < 10 * precision; // nur precision war zu knapp return false; @@ -4573,7 +4575,7 @@ private void createNewEdges() faceToIntersectionEdges = new Dictionary>(); edgesNotToUse = new HashSet(); // wir haben eine Menge Schnittpunkte, die Face-Paaren zugeordnet sind. Für jedes Face-Paar, welches Schnittpunkte enthält sollen hier die neuen Kanten bestimmt werden - // Probleme dabei sind: + // Probleme dabei sind: // - es ist bei mehr als 2 Schnittpunkten nicht klar, welche Abschnitte dazugehören // - zwei Surfaces können mehr als eine Schnittkurve haben HashSet created = new HashSet(new EdgeComparerByVertexAndFace()); @@ -4745,8 +4747,8 @@ private void createNewEdges() // aus den mehreren Punkten (meist 2) unter Berücksichtigung der Richtungen Kanten erzeugen // bei nicht geschlossenen Kurven sollten immer zwei aufeinanderfolgende Punkte einen gültigen Kurvenabschnitt bilden. // Obwohl auch hierbei ein doppelt auftretenden Punkt ein Problem machen könnte (Fälle sind schwer vorstellbar). - // Im schlimmsten Fall müssten man für alle Abschnitte (nicht nur die geraden) - // Bei geschlossenen Kurven ist nicht klar, welche Punktepaare gültige Kurvenabschnitte erzeugen. + // Im schlimmsten Fall müssten man für alle Abschnitte (nicht nur die geraden) + // Bei geschlossenen Kurven ist nicht klar, welche Punktepaare gültige Kurvenabschnitte erzeugen. if (c3ds[i].IsClosed) { if (item.Key.face1.Area.Contains(crvsOnSurface1[i].StartPoint, false) && item.Key.face2.Area.Contains(crvsOnSurface2[i].StartPoint, false)) @@ -4828,7 +4830,7 @@ private void createNewEdges() // Problme wäre die Genauigkeit, wenn beim BooleanOperation.generateCycles die Richtung genommen wird... if (con1 is InterpolatedDualSurfaceCurve.ProjectedCurve && con2 is InterpolatedDualSurfaceCurve.ProjectedCurve && tr is InterpolatedDualSurfaceCurve) - { // con1 und con2 müssen auf tr verweisen, sonst kann man das Face später nicht mit "ReverseOrientation" umdrehen. Dort wird nämlich die + { // con1 und con2 müssen auf tr verweisen, sonst kann man das Face später nicht mit "ReverseOrientation" umdrehen. Dort wird nämlich die // surface verändert, und die muss bei allen Kurven die selbe sein (con1 as InterpolatedDualSurfaceCurve.ProjectedCurve).SetCurve3d(tr as InterpolatedDualSurfaceCurve); (con2 as InterpolatedDualSurfaceCurve.ProjectedCurve).SetCurve3d(tr as InterpolatedDualSurfaceCurve); @@ -4886,7 +4888,7 @@ private void createNewEdges() HashSet onFace1 = existingEdges.Intersect(item.Key.face1.Edges).ToHashSet(); HashSet onFace2 = existingEdges.Intersect(item.Key.face2.Edges).ToHashSet(); //bool edgFound = false; - //// it was Precision.eps before, but a tangential intersection at "Difference2.cdb.json" failed, which should have been there + //// it was Precision.eps before, but a tangential intersection at "Difference2.cdb.json" failed, which should have been there //// in many cases we are close to an edge on one of the faces or both. //// find this edge and step a little inside into this face //foreach (Edge edg in onFace1) diff --git a/CADability/CADability.csproj b/CADability/CADability.csproj index 71eb2da9..56999239 100644 --- a/CADability/CADability.csproj +++ b/CADability/CADability.csproj @@ -22,21 +22,22 @@ false latest + AVALONIA true - NET4X;TRACE;DEBUG;xTESTNEWCONTEXTMENU, xUSENONPRIODICSURFACES + $(DefineConstants);NET4X;TRACE;DEBUG;xTESTNEWCONTEXTMENU, xUSENONPRIODICSURFACES true - TRACE;xPARALLEL, xTESTNEWCONTEXTMENU, xUSENONPRIODICSURFACES, WEBASSEMBLY + $(DefineConstants);TRACE;xPARALLEL, xTESTNEWCONTEXTMENU, xUSENONPRIODICSURFACES, WEBASSEMBLY true - NET4X;xTESTNEWCONTEXTMENU + $(DefineConstants);NET4X;xTESTNEWCONTEXTMENU True full true @@ -45,7 +46,7 @@ true - xPARALLEL, xTESTNEWCONTEXTMENU + $(DefineConstants);xPARALLEL, xTESTNEWCONTEXTMENU false full true @@ -59,7 +60,7 @@ full - + diff --git a/ShapeIt/ChamferEdgesAction.cs b/ShapeIt/ChamferEdgesAction.cs index 937ed8e7..9bcd31b3 100644 --- a/ShapeIt/ChamferEdgesAction.cs +++ b/ShapeIt/ChamferEdgesAction.cs @@ -3,7 +3,9 @@ using CADability.Actions; using CADability.Attribute; using CADability.GeoObject; +#if !AVALONIA using ExCSS; +#endif using System; using System.Collections.Generic; using System.Linq; diff --git a/ShapeIt/FeedbackArrow.cs b/ShapeIt/FeedbackArrow.cs index c0d8d550..82d9820e 100644 --- a/ShapeIt/FeedbackArrow.cs +++ b/ShapeIt/FeedbackArrow.cs @@ -7,7 +7,9 @@ using System.Threading.Tasks; using static CADability.Projection; using System.Globalization; +#if !AVALONIA using CADability.Forms; +#endif using CADability.Attribute; using CADability.Substitutes; @@ -268,7 +270,7 @@ public static GeoObjectList MakeArcArrow(Shell onThisShell, Axis axis, GeoVector else startArrow.ColorDef = blackParts; if (flags.HasFlag(ArrowFlags.secondRed)) endArrow.ColorDef = redParts; else endArrow.ColorDef = blackParts; - res.Add(startArrow); + res.Add(startArrow); res.Add(endArrow); Text txt = Text.Construct(); diff --git a/ShapeIt/MainForm.Designer.cs b/ShapeIt/MainForm.Designer.cs index 0ced6b6f..676f09c1 100644 --- a/ShapeIt/MainForm.Designer.cs +++ b/ShapeIt/MainForm.Designer.cs @@ -1,4 +1,5 @@ -namespace ShapeIt +#if !AVALONIA +namespace ShapeIt { partial class MainForm { @@ -37,4 +38,4 @@ private void InitializeComponent() #endregion } } - +#endif diff --git a/ShapeIt/MainForm.axaml b/ShapeIt/MainForm.axaml new file mode 100644 index 00000000..df62e729 --- /dev/null +++ b/ShapeIt/MainForm.axaml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ShapeIt/MainForm.axaml.cs b/ShapeIt/MainForm.axaml.cs new file mode 100644 index 00000000..67b21c06 --- /dev/null +++ b/ShapeIt/MainForm.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; +using CADability.Avalonia; + +namespace ShapeIt +{ + + public partial class MainForm : Window + { + public MainForm() + { + InitializeComponent(); + } + } +} diff --git a/ShapeIt/MainForm.cs b/ShapeIt/MainForm.cs index 8345c977..dcb1e666 100644 --- a/ShapeIt/MainForm.cs +++ b/ShapeIt/MainForm.cs @@ -1,4 +1,5 @@ -using CADability; +#if !AVALONIA +using CADability; using CADability.Actions; using CADability.Attribute; using CADability.Forms.NET8; @@ -99,7 +100,7 @@ void FadeOutPictureBox(PictureBox pb) private void ShowLogo() { Control pex = FindControlByName(this, "propertiesExplorer"); - // Create PictureBox + // Create PictureBox logoBox = new PictureBox(); Assembly ThisAssembly = Assembly.GetExecutingAssembly(); using (System.IO.Stream str = ThisAssembly.GetManifestResourceStream("ShapeIt.Resources.ShapeIt2.png")) @@ -531,7 +532,7 @@ protected override void OnLoad(EventArgs e) { base.OnLoad(e); - // this is for recording the session with 1280x720 pixel. + // this is for recording the session with 1280x720 pixel. this.Size = new Size(1294, 727); } @@ -746,3 +747,4 @@ private void Debug() #endif } } +#endif diff --git a/ShapeIt/MateFacesAction.cs b/ShapeIt/MateFacesAction.cs index 546d787e..169e21fd 100644 --- a/ShapeIt/MateFacesAction.cs +++ b/ShapeIt/MateFacesAction.cs @@ -7,7 +7,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +#if !AVALONIA using System.Windows.Forms; +#endif namespace ShapeIt { diff --git a/ShapeIt/ModellingPropertyEntries.cs b/ShapeIt/ModellingPropertyEntries.cs index dadcb2a6..cabecfa2 100644 --- a/ShapeIt/ModellingPropertyEntries.cs +++ b/ShapeIt/ModellingPropertyEntries.cs @@ -14,8 +14,10 @@ using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; +#if !AVALONIA using System.Windows.Forms.Design; using System.Windows.Forms.VisualStyles; +#endif using System.Xml.Linq; using Wintellect.PowerCollections; using static CADability.Projection; @@ -42,7 +44,7 @@ internal class ModellingPropertyEntries : PropertyEntryImpl, ICommandHandler private bool isDragging = false; // the user is dragging a selection rectangle private bool isMoving = false; // the user is moving the selected objects private bool isHotspotMoving = false; // the user is movind a hotspot - private HashSet selectedObjects = new HashSet(); // the currently selected root objects + private HashSet selectedObjects = new HashSet(); // the currently selected root objects private GeoObjectList movingObjects = new GeoObjectList(); // a copy of the selected objects, used for moving private GeoPoint moveObjectsDownPoint; private bool downOnSelectedObjects; @@ -407,7 +409,7 @@ internal bool OnEscape() #endregion #region PropertyEntry implementation - public override PropertyEntryType Flags => PropertyEntryType.Selectable | PropertyEntryType.GroupTitle | PropertyEntryType.Checkable | PropertyEntryType.HasSubEntries; // PropertyEntryType.ContextMenu | + public override PropertyEntryType Flags => PropertyEntryType.Selectable | PropertyEntryType.GroupTitle | PropertyEntryType.Checkable | PropertyEntryType.HasSubEntries; // PropertyEntryType.ContextMenu | public override IPropertyEntry[] SubItems => subEntries.ToArray(); public override string Value { @@ -575,7 +577,7 @@ private string GetCursor(Point location, IView vw) private string resourceIdOfLastSelectedCategory = String.Empty; // what was selected last time? edge, face, solid etc. private void ComposeModellingEntries(GeoObjectList objectsUnderCursor, IView vw, PickArea? pickArea, bool alsoParent = true, bool addRemove = false) - { // a mouse left button up took place. Compose all the entries for objects, which can be handled by + { // a mouse left button up took place. Compose all the entries for objects, which can be handled by // the object(s) under the mouse cursor // what to focus after the selection changed? IPropertyEntry pe = propertyPage.GetCurrentSelection(); @@ -689,7 +691,7 @@ private void ComposeModellingEntries(GeoObjectList objectsUnderCursor, IView vw, } } - // show actions for all vertices, edges, faces and curves in + // show actions for all vertices, edges, faces and curves in // distibute objects into their categories List faces = new List(); List shells = new List(); // only Shells, which are not part of a Solid @@ -860,7 +862,7 @@ private void ComposeModellingEntries(GeoObjectList objectsUnderCursor, IView vw, } else if (edges.Any()) subEntries.AddIfNotNull(GetEdgeProperties(vw, edges.First(), clickBeam)); - // add the menus for Solids + // add the menus for Solids if (solids.Count > 1) { SelectEntry multipleSolids = new SelectEntry("MultipleSolids.Properties", true); @@ -1164,7 +1166,7 @@ private IPropertyEntry[] GetEdgesProperties(List curves) { List res = new List(); List edges = new List(curves.Select(c => (c as IGeoObject).Owner as Edge)); - DirectMenuEntry makeCurves = new DirectMenuEntry("MenuId.EdgesToCurves"); // too bad, no icon yet, + DirectMenuEntry makeCurves = new DirectMenuEntry("MenuId.EdgesToCurves"); // too bad, no icon yet, makeCurves.ExecuteMenu = (frame) => { GeoObjectList allEdgesAsCurves = new GeoObjectList(curves.Select(c => (c as IGeoObject).Clone())); // need to clone, since the curves ARE the actual edges! @@ -1185,7 +1187,7 @@ private IPropertyEntry[] GetEdgesProperties(List curves) return true; }; res.Add(makeCurves); - DirectMenuEntry makeFillet = new DirectMenuEntry("MenuId.Fillet"); // too bad, no icon yet, + DirectMenuEntry makeFillet = new DirectMenuEntry("MenuId.Fillet"); // too bad, no icon yet, makeFillet.ExecuteMenu = (frame) => { cadFrame.ControlCenter.ShowPropertyPage("Action"); @@ -1538,7 +1540,7 @@ private void Clear() if (selection != null) { selection.UnSelected(selection); // to regenerate the feedback display - // and by passing selected as "previousSelected" parameter, they can only regenerate projection dependant feedback + // and by passing selected as "previousSelected" parameter, they can only regenerate projection dependant feedback } subEntries.Clear(); pp.Remove(this); // to reflect this newly composed entry @@ -1672,7 +1674,7 @@ private IPropertyEntry GetCurveProperties(IView vw, ICurve curve, bool suppresRu if ((sphere != null)) { cadFrame.Project.StyleList.GetDefault(Style.EDefaultFor.Solids)?.Apply(sphere); - vw.Model.Add(sphere); // add the sphere to the model + vw.Model.Add(sphere); // add the sphere to the model ComposeModellingEntries(new GeoObjectList(sphere), frame.ActiveView, null); // and show it in the control center } return true; @@ -2021,7 +2023,7 @@ private IPropertyEntry GetSolidProperties(IView vw, Solid sld, List fromFa if (fromBox[i] is Solid solid && sld != solid && solid.GetExtent(0.0).Interferes(sld.GetExtent(0.0))) otherSolids.Add(solid); } if (otherSolids.Count > 0) - { // there are other solids close to this solid, it is not guaranteed that these other solids interfere with this solid + { // there are other solids close to this solid, it is not guaranteed that these other solids interfere with this solid Func ShowThisAndAll = (selected, frame) => { // this is the standard selection behaviour for BRep operations feedback.Clear(); @@ -2376,7 +2378,7 @@ private IPropertyEntry GetEdgeProperties(IView vw, Edge edg, Axis clickBeam) } } DirectMenuEntry mhdist = new DirectMenuEntry("MenuId.Parametrics.DistanceTo"); - // mhdist.Target = new ParametricsDistanceActionOld(edg, selectAction.Frame); + // mhdist.Target = new ParametricsDistanceActionOld(edg, selectAction.Frame); // TODO! edgeMenus.Add(mhdist); @@ -3006,10 +3008,10 @@ private IPropertyEntry GetFeatureProperties(IView vw, IEnumerable featureF } /// /// There is a face pointed at by . We try to find a loop of curves on faces - /// passing through pa. This loop msut be perpendicular to all edges it crosses, so that it could have been used + /// passing through pa. This loop msut be perpendicular to all edges it crosses, so that it could have been used /// in an extrusion operation to build all faces, which are touched by the loop curves. To each loop curve there is a face, - /// on which this curve resides. There may be several (or none) loops and sets of faces as a result. For each result there - /// is also a plane, in which all loop curves reside and which is the plane at which the shell can be split to extent or shrink + /// on which this curve resides. There may be several (or none) loops and sets of faces as a result. For each result there + /// is also a plane, in which all loop curves reside and which is the plane at which the shell can be split to extent or shrink /// the shape along the edges. /// /// @@ -3146,7 +3148,7 @@ internal void OnProjectionChanged() if (selection != null) { selection.Selected(selection); // to regenerate the feedback display - // and by passing selected as "previousSelected" parameter, they can only regenerate projection dependant feedback + // and by passing selected as "previousSelected" parameter, they can only regenerate projection dependant feedback } } diff --git a/ShapeIt/ParametricPositionAction.cs b/ShapeIt/ParametricPositionAction.cs index 450221f6..7178ad24 100644 --- a/ShapeIt/ParametricPositionAction.cs +++ b/ShapeIt/ParametricPositionAction.cs @@ -7,13 +7,15 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +#if !AVALONIA using System.Windows.Forms; +#endif namespace ShapeIt { /// /// This action starts with a object to position. This might be one or more faces (like a feature) and a touching point or axis where to meassure. - /// + /// /// internal class ParametricPositionAction : ConstructAction { diff --git a/ShapeIt/Program.cs b/ShapeIt/Program.cs index 263061b3..0da30cd5 100644 --- a/ShapeIt/Program.cs +++ b/ShapeIt/Program.cs @@ -1,14 +1,33 @@ +#if AVALONIA +using Avalonia; +#endif using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +#if !AVALONIA using System.Windows.Forms; +#endif namespace ShapeIt { internal static class Program { +#if AVALONIA + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main(string[] args) => BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); + +#else /// /// The main entry point for the application. /// @@ -20,5 +39,6 @@ static void Main(string[] args) ApplicationConfiguration.Initialize(); Application.Run(new MainForm(args)); } +#endif } -} \ No newline at end of file +} diff --git a/ShapeIt/Properties/Settings.Designer.cs b/ShapeIt/Properties/Settings.Designer.cs index 35a2d457..901d2cf5 100644 --- a/ShapeIt/Properties/Settings.Designer.cs +++ b/ShapeIt/Properties/Settings.Designer.cs @@ -7,16 +7,17 @@ // the code is regenerated. // //------------------------------------------------------------------------------ +#if !AVALONIA namespace ShapeIt.Properties { - - + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.14.0.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { - + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - + public static Settings Default { get { return defaultInstance; @@ -24,3 +25,4 @@ public static Settings Default { } } } +#endif diff --git a/ShapeIt/RoundEdges.cs b/ShapeIt/RoundEdges.cs index 9710dc2f..b7a83c57 100644 --- a/ShapeIt/RoundEdges.cs +++ b/ShapeIt/RoundEdges.cs @@ -7,7 +7,9 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +#if !AVALONIA using System.Windows.Forms.VisualStyles; +#endif using static ShapeIt.ShellExtensions; namespace ShapeIt @@ -486,7 +488,7 @@ private bool curveIntersectsShell(ICurve curve) #endif Face sweptFace; // how to test for correct orientation of the sweptCircle? - // The normal of the swept circle must point towards the filletAxisCurve, it must be a concave surface + // The normal of the swept circle must point towards the filletAxisCurve, it must be a concave surface GeoPoint facnt = filletAxisCurve.Curve3D.PointAt(0.5); GeoPoint lecnt = leadingEdge.PointAt(0.5); GeoPoint2D facnt2d = sweptCircle.PositionOf(new GeoPoint(facnt, lecnt)); diff --git a/ShapeIt/ShapeIt.axaml b/ShapeIt/ShapeIt.axaml new file mode 100644 index 00000000..75d818b7 --- /dev/null +++ b/ShapeIt/ShapeIt.axaml @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ShapeIt/ShapeIt.axaml.cs b/ShapeIt/ShapeIt.axaml.cs new file mode 100644 index 00000000..925f1093 --- /dev/null +++ b/ShapeIt/ShapeIt.axaml.cs @@ -0,0 +1,23 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace ShapeIt; + +public partial class ShapeIt : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainForm(); + } + + base.OnFrameworkInitializationCompleted(); + } +} diff --git a/ShapeIt/ShapeIt.csproj b/ShapeIt/ShapeIt.csproj index c30ae605..b93912bd 100644 --- a/ShapeIt/ShapeIt.csproj +++ b/ShapeIt/ShapeIt.csproj @@ -1,6 +1,6 @@  - + WinExe net8.0-windows enable @@ -8,6 +8,28 @@ disable + + WinExe + net8.0 + enable + false + disable + true + AVALONIA + + + + + + + + + + None + All + + + True $(DefineConstants);DEBUG @@ -37,11 +59,16 @@ - + + + + + + True @@ -107,4 +134,4 @@ - \ No newline at end of file + diff --git a/ShapeIt/ShellExtensions.cs b/ShapeIt/ShellExtensions.cs index 65bf4b04..dd4369a5 100644 --- a/ShapeIt/ShellExtensions.cs +++ b/ShapeIt/ShellExtensions.cs @@ -15,7 +15,9 @@ using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; +#if !AVALONIA using System.Windows.Forms.VisualStyles; +#endif using Wintellect.PowerCollections; namespace ShapeIt @@ -703,14 +705,14 @@ public static Shell[] GetOffsetNew(this Shell shell, double offset) // or the fillets with the spherical faces. Since each breop operation creates new faces and edges, we attach this information to the user data of the original parts // and retrieve them after the brep operation is done. HashSet<(Edge, Face)> dontIntersect = new HashSet<(Edge, Face)>(); // NOT USED ANY MORE, these pairs will be connected and there is no need to calculate the intersection - // set UserData with the original face and edge references to + // set UserData with the original face and edge references to foreach (Face face in shell.Faces) { // makeparallel faces with the provided offset ISurface offsetSurface = face.Surface.GetOffsetSurface(offset); if (offsetSurface == null) continue; // a sphere, cylinder or torus shrinking to 0 GeoPoint2D cnt = face.Domain.GetCenter(); // if the orentation is reversed (e.g. a cylinder will have a negativ radius) or the surface disappears, don't use it - if (offsetSurface != null) // + if (offsetSurface != null) // { List outline = new List(); foreach (Edge edge in face.OutlineEdges) From c2214795a30f7a96ef4bf0c1d90ab0f6a1edf382 Mon Sep 17 00:00:00 2001 From: Jan Schopohl Date: Sat, 10 Jan 2026 23:28:43 +0100 Subject: [PATCH 02/12] Add Avalonia based OpenGL canvas This adds a PaintToOpenGL class to the CADability.Avalonia project which implements IPaintTo3D. Except for initialisation of OpenGL, there is no functionality yet. --- .../CADability.Avalonia.csproj | 4 + CADability.Avalonia/CadCanvas.axaml | 5 + CADability.Avalonia/CadCanvas.axaml.cs | 20 + CADability.Avalonia/CadForm.axaml | 18 +- CADability.Avalonia/PaintToOpenGL.cs | 417 ++++++++++++++++++ 5 files changed, 456 insertions(+), 8 deletions(-) create mode 100644 CADability.Avalonia/CadCanvas.axaml create mode 100644 CADability.Avalonia/CadCanvas.axaml.cs create mode 100644 CADability.Avalonia/PaintToOpenGL.cs diff --git a/CADability.Avalonia/CADability.Avalonia.csproj b/CADability.Avalonia/CADability.Avalonia.csproj index 62b845b8..7010143d 100644 --- a/CADability.Avalonia/CADability.Avalonia.csproj +++ b/CADability.Avalonia/CADability.Avalonia.csproj @@ -17,4 +17,8 @@ All + + + + diff --git a/CADability.Avalonia/CadCanvas.axaml b/CADability.Avalonia/CadCanvas.axaml new file mode 100644 index 00000000..2579c1b5 --- /dev/null +++ b/CADability.Avalonia/CadCanvas.axaml @@ -0,0 +1,5 @@ + + + diff --git a/CADability.Avalonia/CadCanvas.axaml.cs b/CADability.Avalonia/CadCanvas.axaml.cs new file mode 100644 index 00000000..a9d618a3 --- /dev/null +++ b/CADability.Avalonia/CadCanvas.axaml.cs @@ -0,0 +1,20 @@ +using System; +using System.Numerics; +using Avalonia.Controls; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Controls; +using static Avalonia.OpenGL.GlConsts; + +namespace CADability.Avalonia +{ + public partial class CadCanvas: UserControl + { + public CadCanvas() + { + InitializeComponent(); + // CadCanvasControl canvasControl = new CadCanvasControl(); + PaintToOpenGL openGlControl = new PaintToOpenGL(); + this.Content = openGlControl; + } + } +} diff --git a/CADability.Avalonia/CadForm.axaml b/CADability.Avalonia/CadForm.axaml index 698df612..7e85cbb3 100644 --- a/CADability.Avalonia/CadForm.axaml +++ b/CADability.Avalonia/CadForm.axaml @@ -1,12 +1,14 @@ - - - - + + + + + + + + + diff --git a/CADability.Avalonia/PaintToOpenGL.cs b/CADability.Avalonia/PaintToOpenGL.cs new file mode 100644 index 00000000..e9390d97 --- /dev/null +++ b/CADability.Avalonia/PaintToOpenGL.cs @@ -0,0 +1,417 @@ +using System; +using System.Collections.Generic; +using System.Numerics; +using Avalonia.Controls; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Controls; +using CADability; +using CADability.Attribute; +using CADability.GeoObject; +using CADability.Substitutes; + +using static Avalonia.OpenGL.GlConsts; + +namespace CADability.Avalonia +{ + class PaintToOpenGL : OpenGlControlBase, IPaintTo3D + { + public const int GL_POINTS = 0; + public const int GL_TRUE = 1; + public const int GL_FALSE = 0; + + private int _shaderProgram; + private int _vertexShader; + private int _fragmentShader; + private int _vertexBufferObject; + private int _vertexArrayObject; + private int _indexBufferObject; + + private bool paintSurfaces; + private bool paintEdges; + private bool paintSurfaceEdges; + private bool useLineWidth; + private bool selectMode; + private bool delayText; + private bool delayAll; + private bool triangulateText; + private bool dontRecalcTriangulation; + private bool isBitmap; + private double precision; + private double pixelToWorld; + private Color selectColor; + private PaintCapabilities capabilities; + + private static void GlCheckError(GlInterface gl) + { + int err; + while ((err = gl.GetError()) != GL_NO_ERROR) { + Console.WriteLine(err); + } + } + + private string GetShader(string shader) + { + string data = "#version 330\n"; + if (GlVersion.Type == GlProfileType.OpenGLES) { + data += "precision mediump float;\n"; + } + data += shader; + return data; + } + + private void ConfigureShaders(GlInterface gl) + { + _shaderProgram = gl.CreateProgram(); + + _vertexShader = gl.CreateShader(GL_VERTEX_SHADER); + Console.WriteLine(gl.CompileShaderAndGetError(_vertexShader, VertexShaderSource)); + gl.AttachShader(_shaderProgram, _vertexShader); + + _fragmentShader = gl.CreateShader(GL_FRAGMENT_SHADER); + Console.WriteLine(gl.CompileShaderAndGetError(_fragmentShader, FragmentShaderSource)); + gl.AttachShader(_shaderProgram, _fragmentShader); + + Console.WriteLine(gl.LinkProgramAndGetError(_shaderProgram)); + gl.UseProgram(_shaderProgram); + } + + private unsafe void CreateVertexBuffer(GlInterface gl) + { + Vector3[] vertices = new Vector3[] + { + new Vector3(-1.0f, -1.0f, 0.0f), + new Vector3(1.0f, -1.0f, 0.0f), + new Vector3(0.0f, 1.0f, 0.0f), + }; + + _vertexBufferObject = gl.GenBuffer(); + gl.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject); + + fixed(void * pData = vertices) + gl.BufferData(GL_ARRAY_BUFFER, new IntPtr(sizeof(Vector3) * vertices.Length), + new IntPtr(pData), GL_STATIC_DRAW); + + _vertexArrayObject = gl.GenVertexArray(); + gl.BindVertexArray(_vertexArrayObject); + gl.VertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vector3), IntPtr.Zero); + gl.EnableVertexAttribArray(0); + } + + protected override void OnOpenGlInit(GlInterface gl) + { + base.OnOpenGlInit(gl); + + ConfigureShaders(gl); + CreateVertexBuffer(gl); + + GlCheckError(gl); + } + + protected override void OnOpenGlDeinit(GlInterface gl) + { + base.OnOpenGlDeinit(gl); + + gl.BindBuffer(GL_ARRAY_BUFFER, 0); + gl.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + gl.BindVertexArray(0); + gl.UseProgram(0); + gl.DeleteBuffer(_vertexBufferObject); + gl.DeleteVertexArray(_vertexArrayObject); + gl.DeleteProgram(_shaderProgram); + gl.DeleteShader(_vertexShader); + gl.DeleteShader(_fragmentShader); + + GlCheckError(gl); + } + + protected override void OnOpenGlRender(GlInterface gl, int fb) + { + gl.ClearColor(0.85f, 0.90f, 0.98f, 1.0f); + gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + + gl.Viewport(0, 0, (int)Bounds.Width, (int)Bounds.Height); + gl.DrawArrays(GL_TRIANGLES, 0, new IntPtr(3)); + + GlCheckError(gl); + } + + string VertexShaderSource => GetShader(@" + layout(location = 0) in vec3 Position; + + void main() + { + gl_Position.xyz = Position; + gl_Position.w = 1.0; + } + "); + + string FragmentShaderSource => GetShader(@" + void main() + { + gl_FragColor = vec4(1, 0, 1, 1); + } + "); + + bool IPaintTo3D.PaintSurfaces + { + get { return paintSurfaces; } + } + + bool IPaintTo3D.PaintEdges + { + get { return paintEdges; } + } + + bool IPaintTo3D.PaintSurfaceEdges + { + get { return paintSurfaceEdges; } + set { paintSurfaceEdges = value; } + } + + bool IPaintTo3D.UseLineWidth + { + get { return useLineWidth; } + set { useLineWidth = value; } + } + + double IPaintTo3D.Precision + { + get { return precision; } + set { precision = value; } + } + + double IPaintTo3D.PixelToWorld + { + get { return pixelToWorld; } + } + + bool IPaintTo3D.SelectMode + { + get { return selectMode; } + set { selectMode = value; } + } + + Color IPaintTo3D.SelectColor + { + get { return selectColor; } + set { selectColor = value; } + } + + bool IPaintTo3D.DelayText + { + get { return delayText; } + set { delayText = value; } + } + + bool IPaintTo3D.DelayAll + { + get { return delayAll; } + set { delayAll = value; } + } + + bool IPaintTo3D.TriangulateText + { + get { return triangulateText; } + set { triangulateText = value; } + } + + bool IPaintTo3D.DontRecalcTriangulation + { + get { return dontRecalcTriangulation; } + set { dontRecalcTriangulation = value; } + } + + PaintCapabilities IPaintTo3D.Capabilities + { + get { return capabilities; } + } + + bool IPaintTo3D.IsBitmap + { + get { return isBitmap; } + } + IDisposable IPaintTo3D.FacesBehindEdgesOffset { + get { return null; } + // TODO + } + + void IPaintTo3D.MakeCurrent() + { + + } + void IPaintTo3D.SetColor(Color color, int lockColor = 0) + { + } + void IPaintTo3D.AvoidColor(Color color) + { + + } + void IPaintTo3D.SetLineWidth(LineWidth lineWidth) + { + + } + void IPaintTo3D.SetLinePattern(LinePattern pattern) + { + + } + void IPaintTo3D.Polyline(GeoPoint[] points) + { + + } + void IPaintTo3D.FilledPolyline(GeoPoint[] points) + { + + } + void IPaintTo3D.Points(GeoPoint[] points, float size, PointSymbol pointSymbol) + { + + } + void IPaintTo3D.Triangle(GeoPoint[] vertex, GeoVector[] normals, int[] indextriples) + { + + } + void IPaintTo3D.PrepareText(string fontName, string textString, object fontStyle) + { + + } + void IPaintTo3D.PreparePointSymbol(PointSymbol pointSymbol) + { + + } + void IPaintTo3D.PrepareIcon(object icon) + { + + } + void IPaintTo3D.PrepareBitmap(object bitmap, int xoffset, int yoffset) + { + + } + void IPaintTo3D.PrepareBitmap(object bitmap) + { + + } + void IPaintTo3D.RectangularBitmap(object bitmap, GeoPoint location, GeoVector directionWidth, GeoVector directionHeight) + { + + } + void IPaintTo3D.Text(GeoVector lineDirection, GeoVector glyphDirection, GeoPoint location, string fontName, string textString, object fontStyle, CADability.GeoObject.Text.AlignMode alignment, CADability.GeoObject.Text.LineAlignMode lineAlignment) + { + + } + void IPaintTo3D.List(IPaintTo3DList paintThisList) + { + + } + void IPaintTo3D.SelectedList(IPaintTo3DList paintThisList, int wobbleRadius) + { + + } + void IPaintTo3D.Nurbs(GeoPoint[] poles, double[] weights, double[] knots, int degree) + { + + } + void IPaintTo3D.Line2D(int sx, int sy, int ex, int ey) + { + + } + void IPaintTo3D.Line2D(PointF p1, PointF p2) + { + + } + void IPaintTo3D.FillRect2D(PointF p1, PointF p2) + { + + } + void IPaintTo3D.Point2D(int x, int y) + { + + } + void IPaintTo3D.DisplayIcon(GeoPoint p, object icon) + { + + } + void IPaintTo3D.DisplayBitmap(GeoPoint p, object bitmap) + { + + } + void IPaintTo3D.SetProjection(Projection projection, BoundingCube boundingCube) + { + + } + void IPaintTo3D.Clear(Color background) + { + + } + void IPaintTo3D.Resize(int width, int height) + { + + } + void IPaintTo3D.OpenList(string name = null) + { + + } + IPaintTo3DList IPaintTo3D.CloseList() + { + throw new NotImplementedException(); + } + IPaintTo3DList IPaintTo3D.MakeList(List sublists) + { + throw new NotImplementedException(); + } + void IPaintTo3D.OpenPath() + { + throw new NotSupportedException("OpenGL does not support paths"); + } + void IPaintTo3D.ClosePath(Color color) + { + throw new NotSupportedException("OpenGL does not support paths"); + } + void IPaintTo3D.CloseFigure() + { + throw new NotSupportedException("OpenGL does not support paths"); + } + void IPaintTo3D.Arc(GeoPoint center, GeoVector majorAxis, GeoVector minorAxis, double startParameter, double sweepParameter) + { + throw new NotSupportedException("OpenGL does not support paths"); + } + void IPaintTo3D.FreeUnusedLists() + { + } + void IPaintTo3D.UseZBuffer(bool use) + { + } + void IPaintTo3D.Blending(bool on) + { + } + void IPaintTo3D.FinishPaint() + { + } + void IPaintTo3D.PaintFaces(PaintTo3D.PaintMode paintMode) + { + } + void IPaintTo3D.Dispose() + { + + } + void IPaintTo3D.PushState() + { + + } + void IPaintTo3D.PopState() + { + + } + void IPaintTo3D.PushMultModOp(ModOp insertion) + { + + } + void IPaintTo3D.PopModOp() + { + + } + void IPaintTo3D.SetClip(Rectangle clipRectangle) + { + + } + } +} From 921769ecdb1885b0086d42d4123b5bd8dc815c1a Mon Sep 17 00:00:00 2001 From: Jan Schopohl Date: Fri, 23 Jan 2026 21:43:03 +0100 Subject: [PATCH 03/12] Implement ICanvas in new CadCanvas --- CADability.Avalonia/CadCanvas.axaml.cs | 63 ++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/CADability.Avalonia/CadCanvas.axaml.cs b/CADability.Avalonia/CadCanvas.axaml.cs index a9d618a3..fbade0c7 100644 --- a/CADability.Avalonia/CadCanvas.axaml.cs +++ b/CADability.Avalonia/CadCanvas.axaml.cs @@ -1,14 +1,28 @@ -using System; -using System.Numerics; +using Avalonia; using Avalonia.Controls; using Avalonia.OpenGL; using Avalonia.OpenGL.Controls; +using CADability.GeoObject; +using CADability.Substitutes; +using CADability.UserInterface; +using System; +using System.Numerics; using static Avalonia.OpenGL.GlConsts; namespace CADability.Avalonia { - public partial class CadCanvas: UserControl + public partial class CadCanvas: UserControl, ICanvas { + private static CADability.Substitutes.Rectangle Subst(Rect v) + { + return new Substitutes.Rectangle((int)v.X, (int)v.Y, (int)v.Width, (int)v.Height); + } + + private IPaintTo3D paintTo3D; + private IFrame frame; + private IView view; + private String currentCursor; + public CadCanvas() { InitializeComponent(); @@ -16,5 +30,48 @@ public CadCanvas() PaintToOpenGL openGlControl = new PaintToOpenGL(); this.Content = openGlControl; } + + void ICanvas.Invalidate() {} + + Rectangle ICanvas.ClientRectangle => Subst(base.Bounds); + + IFrame ICanvas.Frame + { + get { return frame; } + } + + string ICanvas.Cursor + { + get { return currentCursor; } + set { currentCursor = value; } //TODO + } + + IPaintTo3D ICanvas.PaintTo3D + { + get { return paintTo3D; } + } + + public event Action OnPaintDone; + + void ICanvas.ShowView(IView toShow) {} + + IView ICanvas.GetView() + { + return view; + } + + Substitutes.Point ICanvas.PointToClient(Substitutes.Point mousePosition) + { + throw new NotImplementedException(); + } + + void ICanvas.ShowContextMenu(MenuWithHandler[] contextMenu, Substitutes.Point viewPosition, System.Action collapsed) {} + + Substitutes.DragDropEffects ICanvas.DoDragDrop(GeoObjectList dragList, Substitutes.DragDropEffects all) + { + throw new NotImplementedException(); + } + + void ICanvas.ShowToolTip(string toDisplay) {} } } From 480d3e6b2fe71ee83f9ea447b16fc1bc3cc40bce Mon Sep 17 00:00:00 2001 From: Jan Schopohl Date: Sun, 1 Feb 2026 22:36:00 +0100 Subject: [PATCH 04/12] Integrate new Avalonia OpenGL renderer into ShapeIt [1/n] --- .../CADability.Avalonia.csproj | 1 + CADability.Avalonia/CadCanvas.axaml.cs | 21 +- CADability.Avalonia/CadControl.axaml | 6 + CADability.Avalonia/CadControl.axaml.cs | 177 ++++ CADability.Avalonia/CadForm.axaml | 14 - CADability.Avalonia/CadForm.axaml.cs | 12 - CADability.Avalonia/CadFrame.cs | 509 +++++++++++ CADability.Avalonia/PaintToOpenGL.cs | 650 ++++++++++++-- CADability/CADability.csproj | 5 + CADability/Frame.cs | 16 +- CADability/ModelView.cs | 18 +- ShapeIt/MainForm.axaml.cs | 14 - ShapeIt/{MainForm.axaml => MainWindow.axaml} | 5 +- ShapeIt/MainWindow.axaml.cs | 792 ++++++++++++++++++ ShapeIt/ShapeIt.axaml.cs | 2 +- ShapeIt/ShapeIt.csproj | 5 + 16 files changed, 2089 insertions(+), 158 deletions(-) create mode 100644 CADability.Avalonia/CadControl.axaml create mode 100644 CADability.Avalonia/CadControl.axaml.cs delete mode 100644 CADability.Avalonia/CadForm.axaml delete mode 100644 CADability.Avalonia/CadForm.axaml.cs create mode 100644 CADability.Avalonia/CadFrame.cs delete mode 100644 ShapeIt/MainForm.axaml.cs rename ShapeIt/{MainForm.axaml => MainWindow.axaml} (87%) create mode 100644 ShapeIt/MainWindow.axaml.cs diff --git a/CADability.Avalonia/CADability.Avalonia.csproj b/CADability.Avalonia/CADability.Avalonia.csproj index 7010143d..ac8fdfeb 100644 --- a/CADability.Avalonia/CADability.Avalonia.csproj +++ b/CADability.Avalonia/CADability.Avalonia.csproj @@ -16,6 +16,7 @@ None All + diff --git a/CADability.Avalonia/CadCanvas.axaml.cs b/CADability.Avalonia/CadCanvas.axaml.cs index fbade0c7..a38d151a 100644 --- a/CADability.Avalonia/CadCanvas.axaml.cs +++ b/CADability.Avalonia/CadCanvas.axaml.cs @@ -18,7 +18,7 @@ private static CADability.Substitutes.Rectangle Subst(Rect v) return new Substitutes.Rectangle((int)v.X, (int)v.Y, (int)v.Width, (int)v.Height); } - private IPaintTo3D paintTo3D; + private PaintToOpenGL paintTo3D; private IFrame frame; private IView view; private String currentCursor; @@ -27,18 +27,14 @@ public CadCanvas() { InitializeComponent(); // CadCanvasControl canvasControl = new CadCanvasControl(); - PaintToOpenGL openGlControl = new PaintToOpenGL(); - this.Content = openGlControl; + paintTo3D = new PaintToOpenGL(1e-6); + this.Content = paintTo3D; } void ICanvas.Invalidate() {} Rectangle ICanvas.ClientRectangle => Subst(base.Bounds); - - IFrame ICanvas.Frame - { - get { return frame; } - } + public IFrame Frame { get; set; } string ICanvas.Cursor { @@ -53,7 +49,14 @@ IPaintTo3D ICanvas.PaintTo3D public event Action OnPaintDone; - void ICanvas.ShowView(IView toShow) {} + void ICanvas.ShowView(IView toShow) + { + view = toShow; + // TODO init paintTo3D here or in constructor? + paintTo3D.View = view; + // TODO view.Connect needed? + view.Connect(this); + } IView ICanvas.GetView() { diff --git a/CADability.Avalonia/CadControl.axaml b/CADability.Avalonia/CadControl.axaml new file mode 100644 index 00000000..2b9d28cf --- /dev/null +++ b/CADability.Avalonia/CadControl.axaml @@ -0,0 +1,6 @@ + + + diff --git a/CADability.Avalonia/CadControl.axaml.cs b/CADability.Avalonia/CadControl.axaml.cs new file mode 100644 index 00000000..9a75af05 --- /dev/null +++ b/CADability.Avalonia/CadControl.axaml.cs @@ -0,0 +1,177 @@ +using Avalonia.Controls; +using CADability.UserInterface; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Text; +using System.Xml; + + +namespace CADability.Avalonia +{ + public partial class CadControl : UserControl, ICommandHandler + { + private CadFrame cadFrame; // the frame, which knows about the views, the ControlCenter (PropertiesExplorer), the menu + // private ProgressForm progressForm; + public CadControl() + { + InitializeComponent(); // makes the cadCanvas and the propertiesExplorer + // KeyPreview = true; // used to filter the escape key (and maybe some more?) + // cadFrame = new CadFrame(propertiesExplorer, cadCanvas, this); + cadFrame = new CadFrame(cadCanvas, this); + // cadFrame.ProgressAction = (show, percent, title) => { this.ProgressForm.ShowProgressBar(show, percent, title); }; + cadCanvas.Frame = cadFrame; + // propertiesExplorer.Frame = cadFrame; + // show this menu in the MainForm + // MenuWithHandler[] mainMenu = MenuResource.LoadMenuDefinition("SDI Menu", true, cadFrame); + // MainMenuStrip = MenuManager.MakeMainMenu(mainMenu); + // Controls.Add(MainMenuStrip); + // cadFrame.FormMenu = MainMenuStrip; + // open an existing Project or create a new one + // ToolBars.CreateOrRestoreToolbars(topToolStripContainer, cadFrame); + // Application.Idle += new EventHandler(OnIdle); // update the toolbars (menus are updated when they popup) + } + /// + /// Resets the main menu of the form. After MenuResource.SetMenuResource has been loaded, the mainmenu in CADability + /// is changed to the new resource. In order to set this menu in the form, call this method. + /// + /// if not null, specifies an additional menue class, which will be created by reflection + // TODO + // public void ResetMainMenu(string debuggerPlaygroundClass) + // { + // MenuWithHandler[] mainMenu = MenuResource.LoadMenuDefinition("SDI Menu", true, cadFrame); + // if (!string.IsNullOrEmpty(debuggerPlaygroundClass)) + // { + // // in the following lines a "DebuggerPlayground" object is created via reflection. This class is a playground to write testcode + // // which is not included in the sources. This is why it is constructed via reflection, there is no need to have this class in the project. + // Type dbgplygnd = Type.GetType(debuggerPlaygroundClass, false); + // if (dbgplygnd != null) + // { + // MethodInfo connect = dbgplygnd.GetMethod("Connect"); + // if (connect != null) mainMenu = connect.Invoke(null, new object[] { cadFrame, mainMenu }) as MenuWithHandler[]; + // } + // } + + // if (this.MainMenuStrip != null) + // { + // this.Controls.Remove(this.MainMenuStrip); + // this.MainMenuStrip.Dispose(); + // } + // MainMenuStrip = MenuManager.MakeMainMenu(mainMenu); + // this.Controls.Add(MainMenuStrip); + // cadFrame.FormMenu = MainMenuStrip; + // } + // Access the components of the MainForm from the CadFrame. + // TODO + // public void SetToolbar(XmlNode definition) + // { + // ToolBars.SetToolBar(topToolStripContainer, cadFrame, definition); + // } + // TODO + // public ProgressForm ProgressForm + // { + // get + // { + // if (progressForm == null) + // { + // progressForm = new ProgressForm + // { + // TopLevel = true, + // Owner = this, + // Visible = false + // }; + // } + // return progressForm; + // } + // } + // public PropertiesExplorer PropertiesExplorer => propertiesExplorer; + public CadCanvas CadCanvas => cadCanvas; + public CadFrame CadFrame => cadFrame; + + // slowdown OnIdle polling: + readonly Stopwatch _idleSw = Stopwatch.StartNew(); + const int MinIdleCheckMs = 250; + bool _idleBusy; + // TODO Avalonia + // private void OnIdle(object sender, EventArgs e) + // { + // if (_idleBusy) return; + // if (_idleSw.ElapsedMilliseconds < MinIdleCheckMs) return; + + // _idleBusy = true; + // _idleSw.Restart(); + // try { ToolBars.UpdateCommandState(topToolStripContainer.TopToolStripPanel); } + // finally { _idleBusy = false; } + // } + // TODO + // protected override void OnClosing(CancelEventArgs e) + // { // maybe we need to save the project + // ToolBars.SaveToolbarPositions(topToolStripContainer); + // Settings.SaveGlobalSettings(); + // ToolStripManager.SaveSettings(this); // save the positions of the toolbars (doesn't work correctly) + // base.OnClosing(e); + // } + // TODO + // protected override void OnFormClosed(FormClosedEventArgs e) + // { + // cadFrame.Dispose(); + // MainMenuStrip.Dispose(); + // this.Dispose(); + // cadCanvas.Dispose(); + // propertiesExplorer.Dispose(); + // topToolStripContainer.Dispose(); + // splitContainer.Dispose(); + // if (progressForm != null) progressForm.Dispose(); + + // MainMenuStrip = null; + // cadFrame = null; + // cadCanvas = null; + // propertiesExplorer = null; + // topToolStripContainer = null; + // splitContainer = null; + // progressForm = null; + + // base.OnFormClosed(e); + // } + // TODO + // protected override bool ProcessCmdKey(ref Message msg, Keys keyData) + // { + // Keys nmKeyData = (Keys)((int)keyData & 0x0FFFF); + // bool preProcess = nmKeyData >= Keys.F1 && nmKeyData <= Keys.F24; + // preProcess = preProcess || (nmKeyData == Keys.Escape); + // preProcess = preProcess || (nmKeyData == Keys.Up) || (nmKeyData == Keys.Down); + // preProcess = preProcess || (nmKeyData == Keys.Tab) || (nmKeyData == Keys.Enter); + // preProcess = preProcess || keyData.HasFlag(Keys.Control) || keyData.HasFlag(Keys.Alt); // menu shortcut + // if (propertiesExplorer.EntryWithTextBox == null) preProcess |= (nmKeyData == Keys.Delete); // the delete key is preferred by the textbox, if there is one + // Substitutes.KeyEventArgs e = new Substitutes.KeyEventArgs((Substitutes.Keys)keyData); + // if (preProcess) + // { + // e.Handled = false; + // cadFrame.PreProcessKeyDown(e); + // if (e.Handled) return true; + // } + // CadFrame.PreProcessKeyDown(e); + // if (e.Handled) return true; + // //if (msg.Msg== 0x0101) // WM_KEYUP + // //{ } + // return base.ProcessCmdKey(ref msg, keyData); + // } + + public virtual bool OnCommand(string MenuId) + { + return false; + } + + public virtual bool OnUpdateCommand(string MenuId, CommandState CommandState) + { + return false; + } + + public virtual void OnSelected(MenuWithHandler selectedMenuItem, bool selected) + { + + } + + } +} diff --git a/CADability.Avalonia/CadForm.axaml b/CADability.Avalonia/CadForm.axaml deleted file mode 100644 index 7e85cbb3..00000000 --- a/CADability.Avalonia/CadForm.axaml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - diff --git a/CADability.Avalonia/CadForm.axaml.cs b/CADability.Avalonia/CadForm.axaml.cs deleted file mode 100644 index 109da9c4..00000000 --- a/CADability.Avalonia/CadForm.axaml.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Avalonia.Controls; - -namespace CADability.Avalonia -{ - public partial class CadForm : UserControl - { - public CadForm() - { - InitializeComponent(); - } - } -} diff --git a/CADability.Avalonia/CadFrame.cs b/CADability.Avalonia/CadFrame.cs new file mode 100644 index 00000000..2d1a107a --- /dev/null +++ b/CADability.Avalonia/CadFrame.cs @@ -0,0 +1,509 @@ +using CADability.GeoObject; +using CADability.UserInterface; +using System; +using System.Collections.Generic; +// using System.Drawing; +using System.Drawing.Printing; +using System.IO; +using System.Threading; +using Action = CADability.Actions.Action; + +namespace CADability.Avalonia +{ + /* TODO: change IFrame for "KernelOnly" KO + * Step by Step implement missing methods (copy implementation from SingleDocumentFrame) + * and remove interface methods from IFrame with "#if KO" ... + */ + /// + /// Implementation of the abstract FrameImpl, doing things, you cannot do in .NET Core + /// + public class CadFrame : FrameImpl, IUIService + { + #region PRIVATE FIELDS + + private ICommandHandler commandHandler; + + private const string ClipFormat = "CADability.GeoObjectList.Json"; + + // Optional: COM-freie Verfügbarkeitsprüfung (robuster im Idle) + // TODO reimplement in Avalonia + // static class NativeClipboard + // { + // [DllImport("user32.dll", CharSet = CharSet.Unicode)] + // private static extern uint RegisterClipboardFormat(string lpszFormat); + + // [DllImport("user32.dll")] + // private static extern bool IsClipboardFormatAvailable(uint format); + + // private static uint _fmtId; + // public static bool HasMyFormat() + // { + // if (_fmtId == 0) _fmtId = RegisterClipboardFormat(ClipFormat); + // return IsClipboardFormatAvailable(_fmtId); + // } + // } + + #endregion PRIVATE FIELDS + + #region PUBLIC PROPERTIES + + /// + /// The parent form menu + /// + // TODO reimplement in Avalonia + // public MenuStrip FormMenu { set; private get; } + + /// + /// Action that delegate the progress. + /// In this way we can use a custom progress ui. + /// + public Action ProgressAction { get; set; } + + #endregion PUBLIC PROPERTIES + + /// + /// Constructor without form dependency. + /// + /// + /// + /// + // TODO reimplement in Avalonia + // public CadFrame(PropertiesExplorer propertiesExplorer, CadCanvas cadCanvas, ICommandHandler commandHandler) + // : base(propertiesExplorer, cadCanvas) + // { + // this.commandHandler = commandHandler; + // } + /// + /// Constructor without form dependency. + /// + /// + /// + // + // TODO add propertyExplorer to parameters and add to base call + // allows cadFrame.ControlCenter to work + public CadFrame(CadCanvas cadCanvas, ICommandHandler commandHandler) + : base(cadCanvas) + { + this.commandHandler = commandHandler; + } + + #region FrameImpl override + + public override bool OnCommand(string MenuId) + { + if (commandHandler != null && commandHandler.OnCommand(MenuId)) return true; + return base.OnCommand(MenuId); + } + + public override bool OnUpdateCommand(string MenuId, CommandState CommandState) + { + if (commandHandler != null && commandHandler.OnUpdateCommand(MenuId, CommandState)) return true; + return base.OnUpdateCommand(MenuId, CommandState); + } + + public override void UpdateMRUMenu(string[] mruFiles) + { + // if (this.FormMenu != null) + // { + // TODO reimplement in Avalonia + // foreach (ToolStripMenuItem mi in this.FormMenu.Items) + // { + // UpdateMRUMenu(mi, mruFiles); + // } + // } + } + + // TODO reimplement in Avalonia + // private void UpdateMRUMenu(ToolStripMenuItem mi, string[] mruFiles) + // { + // if (mi.HasDropDownItems) + // { + // foreach (ToolStripItem tsi in mi.DropDownItems) + // { + // if (tsi is ToolStripMenuItem mmi) UpdateMRUMenu(mmi, mruFiles); + // } + // } + // else + // { + // if (mi is MenuItemWithHandler mid) + // { + // if (mid.Tag is MenuWithHandler mwh) + // { + // string MenuId = mwh.ID; + // if (MenuId.StartsWith("MenuId.File.Mru.File")) + // { + // string filenr = MenuId.Substring("MenuId.File.Mru.File".Length); + // try + // { + // int n = int.Parse(filenr); + // if (n <= mruFiles.Length && n > 0) + // { + // string[] parts = mruFiles[mruFiles.Length - n].Split(';'); + // if (parts.Length > 1) + // mid.Text = parts[0]; + // } + // } + // catch (FormatException) { } + // catch (OverflowException) { } + // } + // } + // } + // } + // } + + #endregion FrameImpl override + + #region IUIService implementation + // TODO reimplement in Avalonia + public override IUIService UIService => this; + GeoObjectList IUIService.GetDataPresent(object data) + { + throw new NotImplementedException(); + } + // { + // if (data is IDataObject idata) + // { + // if (idata.GetDataPresent(System.Windows.Forms.DataFormats.Serializable)) + // { + // return idata.GetData(System.Windows.Forms.DataFormats.Serializable) as GeoObjectList; + // } + // } + // return null; + // } + // Substitutes.Keys IUIService.ModifierKeys => (Substitutes.Keys)Control.ModifierKeys; + // Substitutes.Point IUIService.CurrentMousePosition => Subst(Control.MousePosition); + Substitutes.Keys IUIService.ModifierKeys + { + get { throw new NotImplementedException(); } + } + Substitutes.Point IUIService.CurrentMousePosition + { + get { throw new NotImplementedException(); } + } + + private Substitutes.Point Subst(System.Drawing.Point mousePosition) + { + return new Substitutes.Point(mousePosition.X, mousePosition.Y); + } + + private static Dictionary directories = new Dictionary(); + // TODO reimplement in Avalonia + Substitutes.DialogResult IUIService.ShowOpenFileDlg(string id, string title, string filter, ref int filterIndex, out string fileName) + { + throw new NotImplementedException(); + } + // { + // OpenFileDialog openFileDialog = new OpenFileDialog(); + // openFileDialog.Filter = filter; + // openFileDialog.FilterIndex = filterIndex; + // if (!string.IsNullOrWhiteSpace(title)) openFileDialog.Title = title; + // if (!string.IsNullOrWhiteSpace(id) && directories.TryGetValue(id, out string directory)) + // { + // openFileDialog.InitialDirectory = directory; + // } + // else + // { + // openFileDialog.RestoreDirectory = true; + // } + + // Substitutes.DialogResult res = (Substitutes.DialogResult)openFileDialog.ShowDialog(Application.OpenForms[0]); + // if (res == Substitutes.DialogResult.OK) + // { + // filterIndex = openFileDialog.FilterIndex; + // fileName = openFileDialog.FileName; + // if (!string.IsNullOrWhiteSpace(id)) + // { + // directory = System.IO.Path.GetDirectoryName(fileName); + // directories[id] = directory; + // } + // } + // else + // { + // fileName = null; + // } + // return res; + // } + // TODO reimplement in Avalonia + Substitutes.DialogResult IUIService.ShowSaveFileDlg(string id, string title, string filter, ref int filterIndex, ref string fileName) + { + throw new NotImplementedException(); + } + // { + // SaveFileDialog saveFileDialog = new SaveFileDialog(); + // saveFileDialog.Filter = filter; + // saveFileDialog.FilterIndex = filterIndex; + // if (!string.IsNullOrWhiteSpace(title)) saveFileDialog.Title = title; + // if (!string.IsNullOrWhiteSpace(id) && directories.TryGetValue(id, out string directory)) + // { + // saveFileDialog.InitialDirectory = directory; + // } + // else + // { + // saveFileDialog.RestoreDirectory = true; + // } + // if (!string.IsNullOrWhiteSpace(fileName)) + // { + // saveFileDialog.FileName = fileName; + // } + // Substitutes.DialogResult res = (Substitutes.DialogResult)saveFileDialog.ShowDialog(Application.OpenForms[0]); + // if (res == Substitutes.DialogResult.OK) + // { + // filterIndex = saveFileDialog.FilterIndex; + // fileName = saveFileDialog.FileName; + // if (!string.IsNullOrWhiteSpace(id)) + // { + // directory = System.IO.Path.GetDirectoryName(fileName); + // directories[id] = directory; + // } + // } + // return res; + // } + // TODO reimplement in Avalonia + Substitutes.DialogResult IUIService.ShowMessageBox(string text, string caption, Substitutes.MessageBoxButtons buttons) + { + throw new NotImplementedException(); + } + // { + // return (Substitutes.DialogResult)MessageBox.Show(Application.OpenForms[0], text, caption, (System.Windows.Forms.MessageBoxButtons)buttons); + // } + // TODO reimplement in Avalonia + Substitutes.DialogResult IUIService.ShowColorDialog(ref Substitutes.Color color) + { + throw new NotImplementedException(); + } + // { + // ColorDialog colorDialog = new ColorDialog(); + // colorDialog.Color = Drawing(color); + // Substitutes.DialogResult dlgres = (Substitutes.DialogResult)colorDialog.ShowDialog(Application.OpenForms[0]); + // color = Subst(colorDialog.Color); + // return dlgres; + // } + + private Substitutes.Color Drawing(Substitutes.Color color) + { + return color; + // return Substitutes.Color.FromArgb(color.ToArgb()); + } + + private Substitutes.Color Subst(Substitutes.Color color) // TODO correct argument type + { + return color; + // return Substitutes.Color.FromArgb(color.ToArgb()); + } + + // TODO reimplement in Avalonia + void IUIService.ShowProgressBar(bool show, double percent, string title) + { + throw new NotImplementedException(); + } + // { + // this.ProgressAction?.Invoke(show, percent, title); + // } + /// + /// Returns a bitmap from the specified embeded resource. the name is in the form filename:index + /// + /// + /// + // TODO reimplement in Avalonia + object IUIService.GetBitmap(string name) + { + throw new NotImplementedException(); + } + // { + // string[] parts = name.Split(':'); + // if (parts.Length == 2) + // { + // ImageList il = BitmapTable.GetImageList(parts[0], 15, 15); + // if (il != null) + // { + // try + // { + // int ind = int.Parse(parts[1]); + // return il.Images[ind] as Bitmap; + // } + // catch (FormatException) { } + // catch (OverflowException) { } + // catch (ArgumentOutOfRangeException) { } + // } + // } + // return null; + // } + // TODO reimplement in Avalonia + IPaintTo3D IUIService.CreatePaintInterface(object paintToBitmap, double precision) + { + throw new NotImplementedException(); + } + // { + // PaintToOpenGL paintTo3D = new PaintToOpenGL(precision); + // paintTo3D.Init(paintToBitmap as Bitmap); + // return paintTo3D; + // } + // TODO reimplement in Avalonia + Substitutes.DialogResult IUIService.ShowPageSetupDlg(object printDocument, object pageSettings, out int width, out int height, out bool landscape) + { + throw new NotImplementedException(); + } + // { + // PageSetupDialog psd = new PageSetupDialog(); + // psd.AllowPrinter = true; + // psd.EnableMetric = true; + // psd.Document = (PrintDocument?)printDocument; + // Substitutes.DialogResult res = (Substitutes.DialogResult)(int)psd.ShowDialog(); + // if (res == Substitutes.DialogResult.OK) + // { + // psd.Document.OriginAtMargins = false; + // printDocument = psd.Document; + // width = psd.PageSettings.PaperSize.Width; + // height = psd.PageSettings.PaperSize.Height; + // landscape = psd.PageSettings.Landscape; + // } + // else + // { + // width = height = 0; + // landscape = false; + // } + // return res; + // } + // TODO reimplement in Avalonia + Substitutes.DialogResult IUIService.ShowPrintDlg(object printDocument) + { + throw new NotImplementedException(); + } + // { + // PrintDialog printDialog = new PrintDialog(); + // printDialog.Document = (PrintDocument?)printDocument; + // printDialog.AllowSomePages = false; + // printDialog.AllowCurrentPage = false; + // printDialog.AllowSelection = false; + // printDialog.AllowPrintToFile = false; + // Substitutes.DialogResult res = (Substitutes.DialogResult)printDialog.ShowDialog(); + // if (res == Substitutes.DialogResult.OK) + // { + // printDocument = printDialog.Document; + // } + // return res; + // } + // TODO reimplement in Avalonia + void IUIService.SetClipboardData(GeoObjectList objects, bool copy) + { + throw new NotImplementedException(); + } + // { + // // -> JSON in Byte-Array serialisieren + // byte[] payload; + // using (var ms = new MemoryStream()) + // { + // var js = new JsonSerialize(); + // js.ToStream(ms, objects, closeStream: false); + // payload = ms.ToArray(); + // } + + // // DataObject mit eigenem Format befüllen (ohne Auto-Konvertierung) + // var dob = new DataObject(); + // dob.SetData(ClipFormat, false, payload); + + // // Optional: eine menschenlesbare Text-Notiz (damit Paste in Notepad nicht leer ist) + // dob.SetText($"CADability GeoObjectList ({objects?.Count ?? 0} items)"); + + // // copy=true: Inhalte bleiben erhalten, auch wenn die App endet + // Clipboard.SetDataObject(dob, copy); + // } + // TODO reimplement in Avalonia + object IUIService.GetClipboardData(Type typeOfdata) + { + throw new NotImplementedException(); + } + // { + // try + // { + // // defensiv prüfen, ohne GetDataObject() + // if (!Clipboard.ContainsData(ClipFormat)) + // return null; + + // var data = Clipboard.GetData(ClipFormat); + // if (data is byte[] bytes) + // { + // using var ms = new MemoryStream(bytes); + // var js = new JsonSerialize(); + // return js.FromStream(ms); // -> GeoObjectList + // } + + // // Falls ein Stream geliefert wird (kann je nach Host passieren) + // if (data is Stream s) + // { + // using var ms = new MemoryStream(); + // s.CopyTo(ms); + // ms.Position = 0; + // var js = new JsonSerialize(); + // return js.FromStream(ms); + // } + + // // Worst case: String (sollten wir nicht bekommen, aber behandeln) + // if (data is string json) + // { + // return JsonSerialize.FromString(json); + // } + + // return null; + // } + // catch (ExternalException) + // { + // // Clipboard gerade gelockt o.ä. – einfach "kein Inhalt" signalisieren + // return null; + // } + // } + // TODO reimplement in Avalonia + bool IUIService.HasClipboardData(Type typeOfdata) + { + throw new NotImplementedException(); + } + // { + // try + // { + // // Leichtgewichtige .NET-Variante: + // if (Clipboard.ContainsData(ClipFormat)) return true; + + // // Optional, noch robuster & COM-frei (empfohlen im Idle): + // // return NativeClipboard.HasMyFormat(); + + // return false; + // } + // catch (ExternalException) + // { + // // z. B. wenn gerade ein anderer Prozess das Clipboard geöffnet hat + // return false; + // } + // } + + public Substitutes.FontFamily GetFontFamily(string fontFamilyName) + { + throw new NotImplementedException(); + // if (string.IsNullOrEmpty(fontFamilyName)) return new FontFamilyImpl(); + // else return new FontFamilyImpl(fontFamilyName); + } + + public string[] GetFontFamilies() + { + throw new NotImplementedException(); + // FontFamily[] ffs = FontFamily.Families; + // List result = new List(); + // for (int i = 0; i < ffs.Length; i++) result.Add(ffs[i].Name); + // return result.ToArray(); + } + + // TODO reimplement in Avalonia + event EventHandler IUIService.ApplicationIdle + { + add + { + // Application.Idle += value; + } + + remove + { + // Application.Idle -= value; + } + } + #endregion + } +} diff --git a/CADability.Avalonia/PaintToOpenGL.cs b/CADability.Avalonia/PaintToOpenGL.cs index e9390d97..1df15f43 100644 --- a/CADability.Avalonia/PaintToOpenGL.cs +++ b/CADability.Avalonia/PaintToOpenGL.cs @@ -1,13 +1,17 @@ using System; using System.Collections.Generic; using System.Numerics; +using System.Linq; +using Silk.NET.OpenGL; using Avalonia.Controls; using Avalonia.OpenGL; using Avalonia.OpenGL.Controls; +using Avalonia.Threading; using CADability; using CADability.Attribute; using CADability.GeoObject; using CADability.Substitutes; +using MathNet.Numerics.LinearAlgebra.Single; using static Avalonia.OpenGL.GlConsts; @@ -15,17 +19,28 @@ namespace CADability.Avalonia { class PaintToOpenGL : OpenGlControlBase, IPaintTo3D { - public const int GL_POINTS = 0; - public const int GL_TRUE = 1; - public const int GL_FALSE = 0; - - private int _shaderProgram; - private int _vertexShader; - private int _fragmentShader; - private int _vertexBufferObject; - private int _vertexArrayObject; - private int _indexBufferObject; - + private uint _shaderProgram; + private uint _vertexShader; + private uint _fragmentShader; + private uint _vertexBufferObject; + private uint _vertexArrayObject; + private uint _indexBufferObject; + private int modelViewLocation; + private int projectionLocation; + private int colorLocation; + private int lightPositionLocation; + private int ambientFactorLocation; + + private IView _view; + private GL _gl; // use Silk.NET gl interface, as Avalonia only has incomplete bindings + private GlInterface _aGl; // Avalonia gl interface + private Color _backgroundColor; + + private VertexArrayObject currentVao; + private Dictionary vaos; + + private GeoVector projectionDirection; + private bool isPerspective; private bool paintSurfaces; private bool paintEdges; private bool paintSurfaceEdges; @@ -36,15 +51,26 @@ class PaintToOpenGL : OpenGlControlBase, IPaintTo3D private bool triangulateText; private bool dontRecalcTriangulation; private bool isBitmap; + private bool colorOverride; private double precision; private double pixelToWorld; private Color selectColor; private PaintCapabilities capabilities; - private static void GlCheckError(GlInterface gl) + public PaintToOpenGL(double precision = 1e-6) + { + this.precision = precision; + paintSurfaces = true; + paintEdges = true; + paintSurfaceEdges = true; + selectColor = Substitutes.Color.Yellow; + vaos = new Dictionary(); + } + + private void GlCheckError() { - int err; - while ((err = gl.GetError()) != GL_NO_ERROR) { + GLEnum err; + while ((err = _gl.GetError()) != GLEnum.NoError) { Console.WriteLine(err); } } @@ -59,23 +85,33 @@ private string GetShader(string shader) return data; } - private void ConfigureShaders(GlInterface gl) + private void ConfigureShaders() { - _shaderProgram = gl.CreateProgram(); + _shaderProgram = _gl.CreateProgram(); + + _vertexShader = _gl.CreateShader(ShaderType.VertexShader); + Console.WriteLine(_aGl.CompileShaderAndGetError((int)_vertexShader, VertexShaderSource)); + _gl.AttachShader(_shaderProgram, _vertexShader); - _vertexShader = gl.CreateShader(GL_VERTEX_SHADER); - Console.WriteLine(gl.CompileShaderAndGetError(_vertexShader, VertexShaderSource)); - gl.AttachShader(_shaderProgram, _vertexShader); + _fragmentShader = _gl.CreateShader(ShaderType.FragmentShader); + Console.WriteLine(_aGl.CompileShaderAndGetError((int)_fragmentShader, FragmentShaderSource)); + _gl.AttachShader(_shaderProgram, _fragmentShader); - _fragmentShader = gl.CreateShader(GL_FRAGMENT_SHADER); - Console.WriteLine(gl.CompileShaderAndGetError(_fragmentShader, FragmentShaderSource)); - gl.AttachShader(_shaderProgram, _fragmentShader); + Console.WriteLine(_aGl.LinkProgramAndGetError((int)_shaderProgram)); + _gl.UseProgram(_shaderProgram); - Console.WriteLine(gl.LinkProgramAndGetError(_shaderProgram)); - gl.UseProgram(_shaderProgram); + modelViewLocation = _gl.GetUniformLocation(_shaderProgram, "modelview"); + projectionLocation = _gl.GetUniformLocation(_shaderProgram, "projection"); + colorLocation = _gl.GetUniformLocation(_shaderProgram, "color"); + lightPositionLocation = _gl.GetUniformLocation(_shaderProgram, "lightPosition"); + ambientFactorLocation = _gl.GetUniformLocation(_shaderProgram, "ambientFactor"); + Console.WriteLine("modelViewLocation: " + modelViewLocation); + Console.WriteLine("projectionLocation: " + projectionLocation); + Console.WriteLine("colorLocation: " + colorLocation); + GlCheckError(); } - private unsafe void CreateVertexBuffer(GlInterface gl) + private unsafe void CreateVertexBuffer() { Vector3[] vertices = new Vector3[] { @@ -84,71 +120,97 @@ private unsafe void CreateVertexBuffer(GlInterface gl) new Vector3(0.0f, 1.0f, 0.0f), }; - _vertexBufferObject = gl.GenBuffer(); - gl.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject); + _vertexBufferObject = _gl.GenBuffer(); + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vertexBufferObject); fixed(void * pData = vertices) - gl.BufferData(GL_ARRAY_BUFFER, new IntPtr(sizeof(Vector3) * vertices.Length), - new IntPtr(pData), GL_STATIC_DRAW); + _gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint) (sizeof(Vector3) * vertices.Length), + pData, BufferUsageARB.StaticDraw); - _vertexArrayObject = gl.GenVertexArray(); - gl.BindVertexArray(_vertexArrayObject); - gl.VertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(Vector3), IntPtr.Zero); - gl.EnableVertexAttribArray(0); + _vertexArrayObject = _gl.GenVertexArray(); + _gl.BindVertexArray(_vertexArrayObject); + _gl.VertexAttribPointer(0, 3, GLEnum.Float, false, (uint) sizeof(Vector3), (void*)0); + _gl.EnableVertexAttribArray(0); } protected override void OnOpenGlInit(GlInterface gl) { base.OnOpenGlInit(gl); - ConfigureShaders(gl); - CreateVertexBuffer(gl); + _aGl = gl; + _gl = GL.GetApi(gl.GetProcAddress); - GlCheckError(gl); + ConfigureShaders(); + CreateVertexBuffer(); + + GlCheckError(); } protected override void OnOpenGlDeinit(GlInterface gl) { base.OnOpenGlDeinit(gl); - gl.BindBuffer(GL_ARRAY_BUFFER, 0); - gl.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); - gl.BindVertexArray(0); - gl.UseProgram(0); - gl.DeleteBuffer(_vertexBufferObject); - gl.DeleteVertexArray(_vertexArrayObject); - gl.DeleteProgram(_shaderProgram); - gl.DeleteShader(_vertexShader); - gl.DeleteShader(_fragmentShader); - - GlCheckError(gl); - } - - protected override void OnOpenGlRender(GlInterface gl, int fb) - { - gl.ClearColor(0.85f, 0.90f, 0.98f, 1.0f); - gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - - gl.Viewport(0, 0, (int)Bounds.Width, (int)Bounds.Height); - gl.DrawArrays(GL_TRIANGLES, 0, new IntPtr(3)); + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); + _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, 0); + _gl.BindVertexArray(0); + _gl.UseProgram(0); + _gl.DeleteBuffer(_vertexBufferObject); + _gl.DeleteVertexArray(_vertexArrayObject); + _gl.DeleteProgram(_shaderProgram); + _gl.DeleteShader(_vertexShader); + _gl.DeleteShader(_fragmentShader); + + GlCheckError(); + } + + protected override void OnOpenGlRender(GlInterface _, int fb) + { + if (_view != null) { + Substitutes.PaintEventArgs paintEventArgs = new Substitutes.PaintEventArgs() + { + ClipRectangle = new Substitutes.Rectangle(0, 0, (int)Bounds.Width, (int)Bounds.Height), + // ClipRectangle = new Substitutes.Rectangle((int)Bounds.X, (int)Bounds.Y, (int)Bounds.Width, (int)Bounds.Height), + Graphics = null // TODO should be fine, is there a better solution? + }; + _view.OnPaint(paintEventArgs); + } + GlCheckError(); - GlCheckError(gl); + // disabled redraw for debugging + // Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); } string VertexShaderSource => GetShader(@" - layout(location = 0) in vec3 Position; + layout(location = 0) in vec3 vertex; + layout(location = 1) in vec3 normal; + out vec3 fragmentPosition; + out vec3 fragmentNormal; + uniform mat4 modelview; + uniform mat4 projection; void main() { - gl_Position.xyz = Position; - gl_Position.w = 1.0; + fragmentPosition = vertex; + fragmentNormal = normal; + gl_Position = projection * modelview * vec4(vertex, 1.0); } "); string FragmentShaderSource => GetShader(@" + in vec3 fragmentPosition; + in vec3 fragmentNormal; + layout(location = 0) out vec4 diffuseColor; + uniform vec4 color; + uniform vec3 lightPosition; + uniform float ambientFactor; + vec3 lightColor = vec3(1.0, 1.0, 1.0); + void main() { - gl_FragColor = vec4(1, 0, 1, 1); + vec3 normal = normalize(fragmentNormal); + vec3 lightDirection = normalize(lightPosition - fragmentPosition); + float diffuse = max(dot(normal, lightDirection), 0.0); + diffuseColor = vec4((ambientFactor + diffuse) * lightColor * color.rgb, color.a); } "); @@ -231,44 +293,113 @@ bool IPaintTo3D.IsBitmap get { return isBitmap; } } IDisposable IPaintTo3D.FacesBehindEdgesOffset { - get { return null; } - // TODO + get { throw new NotImplementedException(); } } void IPaintTo3D.MakeCurrent() { + // noop, handled by Avalonia + } + private void SetFragmentColor(Color color) + { + // _gl.Uniform4(colorLocation, (float)color.R / 255f, (float)color.G / 255f, (float)color.B / 255f, (float)color.A / 255f); + _gl.Uniform4(colorLocation, (float)color.R / 255f, (float)color.G / 255f, (float)color.B / 255f, 1.0f); + GlCheckError(); } + void IPaintTo3D.SetColor(Color color, int lockColor = 0) { + Console.WriteLine("Setting color: " + color); + Color res; + if (!colorOverride) + { + if (color.R == _backgroundColor.R && color.G == _backgroundColor.G && color.B == _backgroundColor.B) + { + if (color.R + color.G + color.B < 3 * 128) + { + res = Color.FromArgb(color.A, 255, 255, 255); + } + else + { + res = Color.FromArgb(color.A, 0, 0, 0); + } + } + else + { + res = color; + } + + if (currentVao != null) { + currentVao.Color = res; + } + else + { + this.SetFragmentColor(res); + } + } + if (lockColor == 1) colorOverride = true; + else if (lockColor == -1) colorOverride = false; } void IPaintTo3D.AvoidColor(Color color) { - + _backgroundColor = color; } void IPaintTo3D.SetLineWidth(LineWidth lineWidth) { - + if (lineWidth != null) { + Console.WriteLine("SetLineWidth: " + lineWidth.Width); + } + // throw new NotImplementedException(); } void IPaintTo3D.SetLinePattern(LinePattern pattern) { - + // throw new NotImplementedException(); } - void IPaintTo3D.Polyline(GeoPoint[] points) + unsafe void IPaintTo3D.Polyline(GeoPoint[] points) { + Console.WriteLine("Paint Polyline"); + VertexArrayObject vao = currentVao; + if (vao == null) vao = new VertexArrayObject("single Polyline VAO", _gl); + + vao.addVertices(points, GLEnum.LineStrip, (uint)(3 * sizeof(float))); + + if (currentVao == null) { + vao.Close(); + + (this as IPaintTo3D).List(vao); + } + + GlCheckError(); } + void IPaintTo3D.FilledPolyline(GeoPoint[] points) { - + throw new NotImplementedException(); } void IPaintTo3D.Points(GeoPoint[] points, float size, PointSymbol pointSymbol) { - + throw new NotImplementedException(); } - void IPaintTo3D.Triangle(GeoPoint[] vertex, GeoVector[] normals, int[] indextriples) + unsafe void IPaintTo3D.Triangle(GeoPoint[] vertices, GeoVector[] normals, int[] indextriples) { + VertexArrayObject vao = currentVao; + if (vao == null) vao = new VertexArrayObject("single Triangle VAO", _gl); + + // TODO correctly sort indices depending on direction and enable culling + + Console.WriteLine("normals.Length: " + normals.Length); + vao.addIndexedVertices(vertices, indextriples, GLEnum.Triangles, (uint)(6 * sizeof(float)), normals); + // vao.addIndexedVertices(vertices, indextriples, GLEnum.Triangles, (uint)(3 * sizeof(float))); + + if (currentVao == null) { + vao.Close(); + + (this as IPaintTo3D).List(vao); + } + GlCheckError(); } void IPaintTo3D.PrepareText(string fontName, string textString, object fontStyle) { @@ -284,47 +415,78 @@ void IPaintTo3D.PrepareIcon(object icon) } void IPaintTo3D.PrepareBitmap(object bitmap, int xoffset, int yoffset) { - + throw new NotImplementedException(); } void IPaintTo3D.PrepareBitmap(object bitmap) { - + throw new NotImplementedException(); } void IPaintTo3D.RectangularBitmap(object bitmap, GeoPoint location, GeoVector directionWidth, GeoVector directionHeight) { - + throw new NotImplementedException(); } void IPaintTo3D.Text(GeoVector lineDirection, GeoVector glyphDirection, GeoPoint location, string fontName, string textString, object fontStyle, CADability.GeoObject.Text.AlignMode alignment, CADability.GeoObject.Text.LineAlignMode lineAlignment) { } - void IPaintTo3D.List(IPaintTo3DList paintThisList) + unsafe void IPaintTo3D.List(IPaintTo3DList paintThisList) { + VertexArrayObject vao; + if (paintThisList is VertexArrayObject) { + vao = (VertexArrayObject)paintThisList; + } + else if (!vaos.TryGetValue(paintThisList.Name, out vao)) + { + throw new ApplicationException("paintThisList does not exist: " + paintThisList.Name); + } + Console.WriteLine("IpaintTo3D.List: " + vao.Name); + + if (!vao.IsClosed) throw new ApplicationException("Can only paint closed VAOs"); + + _gl.BindVertexArray(vao.VAO); + if (vao.Color != null) { + this.SetFragmentColor(vao.Color); + } + if (vao.ModelView != null) { + _gl.UniformMatrix4(modelViewLocation, false, vao.ModelView); + // TODO should we reset to identity after drawing? + } + + if (!vao.HasIndices) { + // TODO draw using vao.Segments to prevent line segments between different lines + _gl.DrawArrays(vao.Primitive, 0, vao.FloatCount); + } + else + { + _gl.DrawElements(vao.Primitive, vao.IndicesCount, DrawElementsType.UnsignedInt, (void*)0); + } + + _gl.BindVertexArray(0); } void IPaintTo3D.SelectedList(IPaintTo3DList paintThisList, int wobbleRadius) { - + throw new NotImplementedException(); } void IPaintTo3D.Nurbs(GeoPoint[] poles, double[] weights, double[] knots, int degree) { - + throw new NotImplementedException(); } void IPaintTo3D.Line2D(int sx, int sy, int ex, int ey) { - + throw new NotImplementedException(); } void IPaintTo3D.Line2D(PointF p1, PointF p2) { - + throw new NotImplementedException(); } void IPaintTo3D.FillRect2D(PointF p1, PointF p2) { - + throw new NotImplementedException(); } void IPaintTo3D.Point2D(int x, int y) { - + throw new NotImplementedException(); } void IPaintTo3D.DisplayIcon(GeoPoint p, object icon) { @@ -332,27 +494,128 @@ void IPaintTo3D.DisplayIcon(GeoPoint p, object icon) } void IPaintTo3D.DisplayBitmap(GeoPoint p, object bitmap) { + throw new NotImplementedException(); + } + // helper to get an identity matrix. TODO use library function? + private float[] getIdentity4x4() { + float[] mat = new float[16]; + mat[0] = 1.0f; + mat[1] = 0.0f; + mat[2] = 0.0f; + mat[3] = 0.0f; + mat[4] = 0.0f; + mat[5] = 1.0f; + mat[6] = 0.0f; + mat[7] = 0.0f; + mat[8] = 0.0f; + mat[9] = 0.0f; + mat[10] = 1.0f; + mat[11] = 0.0f; + mat[12] = 0.0f; + mat[13] = 0.0f; + mat[14] = 0.0f; + mat[15] = 1.0f; + return mat; } + void IPaintTo3D.SetProjection(Projection projection, BoundingCube boundingCube) { - + _gl.Viewport(0, 0, (uint)Bounds.Width, (uint)Bounds.Height); + + // TODO correct culling + _gl.Enable(GLEnum.CullFace); + + // the role of the bounding cube is to tell the projection, which z values are relevant + // when the drawing is flat (z always 0) you cannot show temporary objects, which have a positive or negative z value + // e.g. there is a circle and you want to shoe a sphere with the circle as equator. The sphere has positive and negative z values. + // When the model changes, the bounding cube is automatically updated, but temporary objects don't change the bounding cube + // There is no foolproof way to handle this, but at least the most common cases should work when we alway use a equilateral (regular) cube + double size = Math.Max(boundingCube.XDiff, Math.Max(boundingCube.YDiff, boundingCube.ZDiff)); + GeoPoint center = boundingCube.GetCenter(); + BoundingCube boundingCubeEquilateral = new BoundingCube(new GeoPoint(center.x - size / 2, center.y - size / 2, center.z - size / 2), + new GeoPoint(center.x + size / 2, center.y + size / 2, center.z + size / 2)); + + double [,] mm = projection.GetOpenGLProjection(0, (int)Bounds.Width, 0, (int)Bounds.Height, boundingCubeEquilateral); + float [,] mmFloat = new float[,] { + { (float)mm[0, 0], (float)mm[0, 1], (float)mm[0, 2], (float)mm[0, 3]}, + { (float)mm[1, 0], (float)mm[1, 1], (float)mm[1, 2], (float)mm[1, 3]}, + { (float)mm[2, 0], (float)mm[2, 1], (float)mm[2, 2], (float)mm[2, 3]}, + { (float)mm[3, 0], (float)mm[3, 1], (float)mm[3, 2], (float)mm[3, 3]} + }; + Matrix debugMatrix = DenseMatrix.OfArray(mmFloat); + Console.WriteLine(debugMatrix); + float[] pmat = new float[16]; + // ACHTUNG: Matrix ist vertauscht!!! + pmat[0] = (float) mm[0, 0]; + pmat[1] = (float) mm[1, 0]; + pmat[2] = (float) mm[2, 0]; + pmat[3] = (float) mm[3, 0]; + pmat[4] = (float) mm[0, 1]; + pmat[5] = (float) mm[1, 1]; + pmat[6] = (float) mm[2, 1]; + pmat[7] = (float) mm[3, 1]; + pmat[8] = (float) mm[0, 2]; + pmat[9] = (float) mm[1, 2]; + pmat[10] = (float) mm[2, 2]; + pmat[11] = (float) mm[3, 2]; + pmat[12] = (float) mm[0, 3]; + pmat[13] = (float) mm[1, 3]; + pmat[14] = (float) mm[2, 3]; + pmat[15] = (float) mm[3, 3]; + + // GeoVector v = projection.Direction; + projectionDirection = projection.Direction; + isPerspective = projection.IsPerspective; + // TODO use v for lighting position + GeoVector v; + // v = projection.InverseProjection * new GeoVector(0.5, 0.3, -1.0); + v = projection.InverseProjection * new GeoVector(100.0, 300.0, 1000.0); + Console.WriteLine("Light Position: " + v); + pixelToWorld = projection.DeviceToWorldFactor; + _gl.Enable(GLEnum.DepthTest); + + float[] modelViewMat = getIdentity4x4(); + _gl.UniformMatrix4(modelViewLocation, 1, false, modelViewMat); + _gl.UniformMatrix4(projectionLocation, 1, false, pmat); + + _gl.Uniform3(lightPositionLocation, (float)v.x, (float)v.y, (float)v.z); + _gl.Uniform1(ambientFactorLocation, 0.2f); + GlCheckError(); } void IPaintTo3D.Clear(Color background) { + _backgroundColor = background; + _gl.Viewport(0, 0, (uint)Bounds.Width, (uint)Bounds.Height); + _gl.ClearColor(background.R / 255.0f, background.G / 255.0f, background.B / 255.0f, 1.0f); + _gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + GlCheckError(); } void IPaintTo3D.Resize(int width, int height) { - + // TODO save bounds? } - void IPaintTo3D.OpenList(string name = null) + void IPaintTo3D.OpenList(string name) { + if (name == null) throw new ApplicationException("List name cannot be empty."); + if (currentVao != null) throw new ApplicationException("VAOs cannot be nested!"); + + Console.WriteLine("OpenList: " + name); + currentVao = new VertexArrayObject(name, _gl); + // this overwrites any previously existing vao with this name + vaos[name] = currentVao; + GlCheckError(); } IPaintTo3DList IPaintTo3D.CloseList() { - throw new NotImplementedException(); + Console.WriteLine("Close List: " + currentVao.Name); + if (currentVao != null) currentVao.Close(); + VertexArrayObject res = currentVao; + currentVao = null; + GlCheckError(); + return res; } IPaintTo3DList IPaintTo3D.MakeList(List sublists) { @@ -376,18 +639,82 @@ void IPaintTo3D.Arc(GeoPoint center, GeoVector majorAxis, GeoVector minorAxis, d } void IPaintTo3D.FreeUnusedLists() { + throw new NotImplementedException(); } void IPaintTo3D.UseZBuffer(bool use) { + if (use) + { + _gl.Enable(GLEnum.DepthTest); + _gl.DepthFunc(GLEnum.Always); // TODO this makes the test always pass + } + else + { + _gl.Disable(GLEnum.DepthTest); + } } void IPaintTo3D.Blending(bool on) { + // throw new NotImplementedException(); } void IPaintTo3D.FinishPaint() { + // throw new NotImplementedException(); } - void IPaintTo3D.PaintFaces(PaintTo3D.PaintMode paintMode) + unsafe void IPaintTo3D.PaintFaces(PaintTo3D.PaintMode paintMode) { + Console.WriteLine("PaintFaces: " + paintMode); + if (paintMode == PaintTo3D.PaintMode.FacesOnly) + { + if (isPerspective) + { + Console.WriteLine("Perspective Mode"); + } + else + { + // TODO is this a operation used in display list? + // then we would have to save the matrix to VertexBufferObject + Matrix modelViewMat = DenseMatrix.OfArray(new float[,] { + { 1.0f, 0.0f, 0.0f, (float)(2 * precision * projectionDirection.x)}, + { 0.0f, 1.0f, 0.0f, (float)(2 * precision * projectionDirection.y)}, + { 0.0f, 0.0f, 1.0f, (float)(2 * precision * projectionDirection.z)}, + { 0.0f, 0.0f, 0.0f, 1.0f } + }); + Console.WriteLine("Paint Faces, modelView Matrix: " + modelViewMat.ToString()); + + if (currentVao != null) + { + currentVao.ModelView = modelViewMat.ToColumnMajorArray(); + } + else + { + _gl.UniformMatrix4(modelViewLocation, false, modelViewMat.ToColumnMajorArray()); + } + GlCheckError(); + + } + paintSurfaces = true; + paintEdges = false; + } + else if (paintMode == PaintTo3D.PaintMode.CurvesOnly) + { + Matrix identity = DenseMatrix.CreateIdentity(4); + _gl.UniformMatrix4(modelViewLocation, false, identity.ToColumnMajorArray()); + paintSurfaces = false; + paintEdges = true; + } + else if (paintMode == PaintTo3D.PaintMode.All) + { + Matrix identity = DenseMatrix.CreateIdentity(4); + _gl.UniformMatrix4(modelViewLocation, false, identity.ToColumnMajorArray()); + paintSurfaces = true; + paintEdges = true; + } + else + { + throw new NotImplementedException("Invalid paintMode"); + } + GlCheckError(); } void IPaintTo3D.Dispose() { @@ -395,23 +722,168 @@ void IPaintTo3D.Dispose() } void IPaintTo3D.PushState() { - + throw new NotImplementedException(); } void IPaintTo3D.PopState() { - + throw new NotImplementedException(); } void IPaintTo3D.PushMultModOp(ModOp insertion) { - + throw new NotImplementedException(); } void IPaintTo3D.PopModOp() { + throw new NotImplementedException(); + } + void IPaintTo3D.SetClip(Substitutes.Rectangle clipRectangle) + { + throw new NotImplementedException(); + } + public IView View { + set => _view = value; } - void IPaintTo3D.SetClip(Rectangle clipRectangle) + + internal class VertexArrayObject : IPaintTo3DList { + private string name; + private GL _gl; + private uint vertexArrayObject; + private uint vertexBufferObject; + private uint elementBufferObject; + private List vertices; + private List indices; + private GLEnum primitiveType; + private bool hasIndices; + private bool hasNormals; + private bool closed; + private uint floatCount; + private uint indicesCount; + private uint stride; + private Substitutes.Color color; + private float[] modelView; // column major order 4x4 matrix + private List<(uint, uint)> segments; + + public VertexArrayObject(string name, GL gl) + { + this.name = name; + this._gl = gl; + this.closed = false; + this.hasIndices = false; + this.hasNormals = false; + this.primitiveType = GLEnum.False; + this.stride = 0; + vertexArrayObject = _gl.GenVertexArray(); + vertexBufferObject = _gl.GenBuffer(); + elementBufferObject = _gl.GenBuffer(); + vertices = new List(); + indices = new List(); + segments = new List<(uint, uint)>(); + } + + public string Name + { + get { return name; } + set { throw new NotImplementedException(); } + } + public List containedSubLists + { + set { throw new NotImplementedException(); } + } + + public uint VAO => vertexArrayObject; + public uint VBO => vertexBufferObject; + public uint EBO => elementBufferObject; + public GLEnum Primitive => primitiveType; + + public bool HasIndices => hasIndices; + public uint FloatCount => floatCount; + public uint IndicesCount => indicesCount; + public uint Stride => stride; + public bool IsClosed => closed; + public Substitutes.Color Color { set; get; } + public float[] ModelView { set; get; } + + public void addVertices(GeoPoint[] points, GLEnum primitiveType, uint stride, GeoVector[] normals = null) { + if (this.primitiveType != GLEnum.False && primitiveType != this.primitiveType) { + throw new ApplicationException("Only supports one primitive type in a VertexArrayObject"); + } + if (this.stride != 0 && this.stride != stride) { + throw new ApplicationException("Cannot change stride of VertexArrayObject"); + } + if (closed) throw new ApplicationException("Cannot add to closed VertexArrayObject"); + + if (normals == null) + { + this.vertices.AddRange(points.SelectMany( vertex => new float[]{(float)vertex.x, (float)vertex.y, (float)vertex.z})); + } + else { + this.hasNormals = true; + this.vertices.AddRange( + points + .Zip(normals, (p, n) => new float[]{(float)p.x, (float)p.y, (float)p.z, (float)n.x, (float)n.y, (float)n.z}) + .SelectMany(f => f) + ); + } + this.stride = stride; + this.primitiveType = primitiveType; + } + + public void addIndexedVertices(GeoPoint[] points, int[] indices, GLEnum primitiveType, uint stride, GeoVector[] normals = null) { + // only apply an offset if not drawing in segments and offsetting the vertex attrib pointer + uint offset = (uint) (this.vertices.Count / (stride / sizeof(float))); + + this.addVertices(points, primitiveType, stride, normals); + this.indices.AddRange(indices.Select(i => (uint) i + offset)); + this.hasIndices = true; + this.primitiveType = primitiveType; + } + + unsafe public void Close() + { + closed = true; + _gl.BindVertexArray(vertexArrayObject); + _gl.BindBuffer(BufferTargetARB.ArrayBuffer, vertexBufferObject); + float[] verticesArray = vertices.ToArray(); + fixed(float* pData = verticesArray) + _gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint)(sizeof(float) * vertices.Count), + pData, BufferUsageARB.StaticDraw); + + if (hasIndices) { + _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, elementBufferObject); + uint[] indicesArray = indices.ToArray(); + fixed(uint* pIData = indicesArray) + _gl.BufferData(BufferTargetARB.ElementArrayBuffer, (nuint)(sizeof(uint) * indices.Count), + pIData, BufferUsageARB.StaticDraw); + } + + // TODO VertexAttribPointer here or when drawing in List? + _gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, (void*)0); + _gl.EnableVertexAttribArray(0); + + if (this.hasNormals) + { + _gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float))); + _gl.EnableVertexAttribArray(1); + } + + _gl.BindVertexArray(vertexArrayObject); + floatCount = (uint)vertices.Count; + indicesCount = (uint)indices.Count; + vertices.Clear(); + indices.Clear(); + vertices = null; + indices = null; + } + + public void Dispose() + { + _gl.DeleteBuffer(vertexBufferObject); + _gl.DeleteBuffer(elementBufferObject); + _gl.DeleteVertexArray(vertexArrayObject); + } } } } diff --git a/CADability/CADability.csproj b/CADability/CADability.csproj index 56999239..2ada9c72 100644 --- a/CADability/CADability.csproj +++ b/CADability/CADability.csproj @@ -1,4 +1,9 @@  + + false + false + + netstandard2.0 diff --git a/CADability/Frame.cs b/CADability/Frame.cs index 29570e85..b3523338 100644 --- a/CADability/Frame.cs +++ b/CADability/Frame.cs @@ -52,7 +52,7 @@ public class ActiveFrame /// Set to true, if you have processed this command and no further action is required, leave unmodified if CADability should process this command public delegate void ProcessCommandDelegate(string MenuId, ref bool Processed); /// - /// + /// /// /// /// @@ -102,7 +102,7 @@ public interface IFrame /// void RemoveActiveAction(); /// - /// Returns the currently active action. Call to find out more about + /// Returns the currently active action. Call to find out more about /// that action. /// Action ActiveAction { get; } @@ -120,7 +120,7 @@ public interface IFrame /// event ProcessCommandDelegate ProcessCommandEvent; /// - /// Provide a event handler if you want to control the appearnce of commands in the menu or toolbar + /// Provide a event handler if you want to control the appearnce of commands in the menu or toolbar /// (set the enabled and check flags). /// event UpdateCommandDelegate UpdateCommandEvent; @@ -142,7 +142,7 @@ public interface IFrame /// event ProcessContextMenuDelegate ProcessContextMenuEvent; /// - /// Provide a event handler if you want to control the appearance of commands in a context the menu + /// Provide a event handler if you want to control the appearance of commands in a context the menu /// (set the enabled and check flags). /// event UpdateContextMenuDelegate UpdateContextMenuEvent; @@ -178,7 +178,7 @@ public interface IFrame /// Settings GlobalSettings { get; set; } /// - /// Gets the for the provided . First the s settings are + /// Gets the for the provided . First the s settings are /// checked, if it is not defined there, the global settings will be queried. /// /// @@ -615,7 +615,7 @@ public Settings GlobalSettings } } private void OnSettingChanged(string Name, object NewValue) - { + { object o = this.GetSetting(Name); SettingChangedEvent?.Invoke(Name, NewValue); // } @@ -756,7 +756,7 @@ public bool PreProcessMouseWheel(MouseEventArgs e) } public virtual void PreProcessKeyDown(KeyEventArgs e) { - // it is difficult to find an order of processing: + // it is difficult to find an order of processing: // the construct-action processes the enter key, no chance to use it in a list-box for the selection // the property page processes the tab key, no chance to use it in the action for "next modal input field" // we need to replace the KeyEventArgs class by a CADability class anyhow, we could introduce a first-pass, second-pass member @@ -2407,7 +2407,7 @@ private void OnFileOpen(string fileName) { // get a raw model extent to know a precision for the triangulation // parallel triangulate all faces with this precision to show a progress bar - // then zoom total + // then zoom total ModelView mv = FirstModelView; // display the first ModelView if (mv != null) diff --git a/CADability/ModelView.cs b/CADability/ModelView.cs index 5914e349..b771ffcc 100644 --- a/CADability/ModelView.cs +++ b/CADability/ModelView.cs @@ -59,7 +59,7 @@ public interface ICanvas /// IView GetView(); /// - /// + /// /// /// /// @@ -158,7 +158,7 @@ void OnLayerAdded(LayerList sender, Layer added) } #region IShowProperty Members /// - /// Overrides , + /// Overrides , /// returns . /// public override ShowPropertyEntryType EntryType @@ -179,7 +179,7 @@ public override ShowPropertyLabelFlags LabelType } } /// - /// Overrides , + /// Overrides , /// returns the number of subentries in this property view. /// public override int SubEntriesCount @@ -191,7 +191,7 @@ public override int SubEntriesCount } IShowProperty[] subEntries; /// - /// Overrides , + /// Overrides , /// returns the subentries in this property view. /// public override IShowProperty[] SubEntries @@ -1372,7 +1372,7 @@ void IView.Invalidate(PaintBuffer.DrawingAspect aspect, Rectangle ToInvalidate) } void IView.InvalidateAll() { - // ForceInvalidateAll(); // das ist definitiv zuviel, bei FeedbackObjekten wird das aufgerufen und es bräuchte nur ein + // ForceInvalidateAll(); // das ist definitiv zuviel, bei FeedbackObjekten wird das aufgerufen und es bräuchte nur ein // neuzeichnen der Feedback Objekte canvas?.Invalidate(); } @@ -1524,7 +1524,7 @@ public override string LabelText } } /// - /// Overrides , + /// Overrides , /// returns . /// public override ShowPropertyEntryType EntryType @@ -1570,7 +1570,7 @@ public override void EndEdit(bool aborted, bool modified, string newValue) } } /// - /// Overrides , + /// Overrides , /// returns the context menu with the id "MenuId.ModelView". /// (see ) /// @@ -1586,7 +1586,7 @@ public override MenuWithHandler[] ContextMenu IShowProperty[] subEntries; /// - /// Overrides , + /// Overrides , /// returns the number of subentries in this property view. /// public override int SubEntriesCount @@ -1597,7 +1597,7 @@ public override int SubEntriesCount } } /// - /// Overrides , + /// Overrides , /// returns the subentries in this property view. /// public override IShowProperty[] SubEntries diff --git a/ShapeIt/MainForm.axaml.cs b/ShapeIt/MainForm.axaml.cs deleted file mode 100644 index 67b21c06..00000000 --- a/ShapeIt/MainForm.axaml.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Avalonia.Controls; -using CADability.Avalonia; - -namespace ShapeIt -{ - - public partial class MainForm : Window - { - public MainForm() - { - InitializeComponent(); - } - } -} diff --git a/ShapeIt/MainForm.axaml b/ShapeIt/MainWindow.axaml similarity index 87% rename from ShapeIt/MainForm.axaml rename to ShapeIt/MainWindow.axaml index df62e729..9488f414 100644 --- a/ShapeIt/MainForm.axaml +++ b/ShapeIt/MainWindow.axaml @@ -2,7 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ca="using:CADability.Avalonia" Title="ShapeIt" - x:Class="ShapeIt.MainForm"> + x:Class="ShapeIt.MainWindow"> - + + diff --git a/ShapeIt/MainWindow.axaml.cs b/ShapeIt/MainWindow.axaml.cs new file mode 100644 index 00000000..c2d2ab7e --- /dev/null +++ b/ShapeIt/MainWindow.axaml.cs @@ -0,0 +1,792 @@ +using Avalonia.Controls; +using CADability.Avalonia; +using CADability; +using CADability.Actions; +using CADability.Attribute; +using CADability.GeoObject; +using CADability.UserInterface; +using MathNet.Numerics.LinearAlgebra.Factorization; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Diagnostics; +using System.Threading.Tasks; +using System.Reflection; +using System.Linq; +using System.IO; +using System.Xml; +using System.Xml.Linq; +using System.Text; + +namespace ShapeIt +{ + + // TODO move gui related things to CADability.Avalonia.CadForm and inherit from that here? + // or just implement ICommandHandler here? + public partial class MainWindow : Window + { + // public MainForm() + // { + // InitializeComponent(); + // cadFrame = new CadFrame(propertiesExplorer, cadCanvas, this); + // } + // TODO + // create CAD project + // create FrameImpl (or implement own subclass) + // in CadForm (here named CadControl?): create Frame/cadFrame (type x -> FrameImpl -> IFrame) and + // set cadCanvas.Frame = cadFrame (in CadForm) + // + + // currently here because we dont inherit from CadForm + // private CadFrame cadFrame; + + // private PictureBox logoBox; + private ModellingPropertyEntries modellingPropertyEntries; + private DateTime lastSaved; // time, when the current file has been saved the last time, see OnIdle + private bool modifiedSinceLastAutosave = false; + bool projectionChanged = false; // to handle projection changes in OnIdle + bool crashChecked = false; + + // TODO needed? how to do in Avalonia? + // private Control FindControlByName(Control parent, string name) + // { + // foreach (Control child in parent.Controls) + // { + // if (child.Name == name) + // return child; + + // Control found = FindControlByName(child, name); + // if (found != null) + // return found; + // } + + // return null; + // } + + // TODO reimplement in Avalonia + // void FadeOutPictureBox(PictureBox pb) + // { + // var timer = new System.Windows.Forms.Timer(); + // timer.Interval = 50; + // double alpha = 1.0; + + // Image original = pb.Image; + // Bitmap faded = new Bitmap(original.Width, original.Height); + + // timer.Tick += (s, e) => + // { + // alpha -= 0.01; + // if (alpha <= 0) + // { + // timer.Stop(); + // pb.Parent.Controls.Remove(pb); + // //pb.Visible = false; + // pb.Dispose(); + // return; + // } + + // using (Graphics g = Graphics.FromImage(faded)) + // { + // g.Clear(Color.Transparent); + // ColorMatrix matrix = new ColorMatrix + // { + // Matrix33 = (float)alpha // Alpha-Kanal + // }; + // ImageAttributes attributes = new ImageAttributes(); + // attributes.SetColorMatrix(matrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap); + + // g.DrawImage(original, + // new Rectangle(0, 0, faded.Width, faded.Height), + // 0, 0, original.Width, original.Height, + // GraphicsUnit.Pixel, + // attributes); + // } + + // pb.Image = (Image)faded.Clone(); // neues Bild setzen + // }; + + // timer.Start(); + // } + + // TODO reimplement in Avalonia + // private void ShowLogo() + // { + // Control pex = FindControlByName(this, "propertiesExplorer"); + // // Create PictureBox + // logoBox = new PictureBox(); + // Assembly ThisAssembly = Assembly.GetExecutingAssembly(); + // using (System.IO.Stream str = ThisAssembly.GetManifestResourceStream("ShapeIt.Resources.ShapeIt2.png")) + // { + // logoBox.Image = new Bitmap(str); + // } + // logoBox.SizeMode = PictureBoxSizeMode.Zoom; + + // double aspectRatio = (double)logoBox.Image.Height / logoBox.Image.Width; + + // // Zielbreite übernehmen + // int targetWidth = pex.ClientSize.Width - 4; + // int berechneteHoehe = (int)(targetWidth * aspectRatio); + + // // Größe setzen + // logoBox.Size = new Size(targetWidth, berechneteHoehe); + + // // Position am unteren Rand + // logoBox.Location = new Point(2, pex.ClientSize.Height - berechneteHoehe - 2); + + // // Logo zum Ziel-Control hinzufügen + // pex.Controls.Add(logoBox); + // logoBox.BringToFront(); + + // FadeOutPictureBox(logoBox); + + // pex.Resize += (s, e) => + // { + // int newWidth = pex.ClientSize.Width - 4; + // int newHeight = (int)(newWidth * aspectRatio); + // logoBox.Size = new Size(newWidth, newHeight); + // logoBox.Location = new Point(2, pex.ClientSize.Height - newHeight - 2); + // }; + // } + + private string ReadEmbeddedVersion() + { + var asm = typeof(Program).Assembly; + using var s = asm.GetManifestResourceStream("App.Version"); + using var sr = new StreamReader(s!); + return sr.ReadToEnd().Trim(); + } + + // TODO taken from CadForm, as we don't inherit from it + + // public bool OnCommand(string menuId) + // { + // return false; + // } + // public bool OnUpdateCommand(string menuId, CommandState commandState) + // { + // return false; + // } + // public void OnSelected(MenuWithHandler selectedMenu, bool selected) + // { + // } + + // delegate to cadControl (old: cadForm) + // public PropertiesExplorer PropertiesExplorer => propertiesExplorer; + public CadCanvas CadCanvas => cadControl.CadCanvas; + public CadFrame CadFrame => cadControl.CadFrame; + + public MainWindow(string[] args) // TODO inherit from CadForm? : base(args) + { // interpret the command line arguments as a name of a file, which should be opened + + InitializeComponent(); + // cadFrame = new CadFrame(cadCanvas, this); + // ShowLogo(); TODO + // this.Icon = Properties.Resources.Icon; + Assembly ThisAssembly = Assembly.GetExecutingAssembly(); + System.IO.Stream str; + // TODO + // using (str = ThisAssembly.GetManifestResourceStream("ShapeIt.Resources.Icon.ico")) + // { + // this.Icon = new System.Drawing.Icon(str); + // } + + string fileName = ""; + for (int i = 0; i < args.Length; i++) + { + if (!args[i].StartsWith("-")) + { + fileName = args[i]; + break; + } + } + Project toOpen = null; + if (!String.IsNullOrWhiteSpace(fileName)) + { + try + { + toOpen = Project.ReadFromFile(fileName); + Console.WriteLine("Read Project from file: " + fileName); + } + catch { } + } + if (toOpen == null) CadFrame.GenerateNewProject(); + else CadFrame.Project = toOpen; + + string version = ReadEmbeddedVersion(); // version from version.txt + // this.Text = $"ShapeIt with CADability – Version: {version}"; + + if (!Settings.GlobalSettings.ContainsSetting("UserInterface")) + { + Settings UserInterface = new Settings("UserInterface"); + Settings.GlobalSettings.AddSetting("UserInterface", UserInterface); + IntegerProperty ToolbarButtonSize = new IntegerProperty("ToolbarButtonSize", "ToolbarButtonSize"); + ToolbarButtonSize.IntegerValue = 0; + UserInterface.AddSetting("ToolbarButtonSize", ToolbarButtonSize); + IntegerProperty MenuSize = new IntegerProperty("MenuSize", "MenuSize"); + MenuSize.IntegerValue = 0; + UserInterface.AddSetting("MenuSize", MenuSize); + } + Settings.GlobalSettings.SetValue("Construct.3D_Delete2DBase", false); + bool exp = Settings.GlobalSettings.GetBoolValue("Experimental.TestNewContextMenu", false); + bool tst = Settings.GlobalSettings.GetBoolValue("ShapeIt.Initialized", false); + Settings.GlobalSettings.SetValue("ShapeIt.Initialized", true); + CadFrame.FileNameChangedEvent += (name) => + { + // if (string.IsNullOrEmpty(name)) this.Text = "ShapeIt with CADability"; + // else this.Text = "ShapeIt -- " + name; + lastSaved = DateTime.Now; // a new file has been opened + }; + CadFrame.ProjectClosedEvent += OnProjectClosed; + CadFrame.ProjectOpenedEvent += OnProjectOpened; + // CadFrame.UIService.ApplicationIdle += OnIdle; TODO + CadFrame.ViewsChangedEvent += OnViewsChanged; + if (CadFrame.ActiveView != null) OnViewsChanged(CadFrame); + // CadFrame.ControlCenter.RemovePropertyPage("View"); TODO + using (str = ThisAssembly.GetManifestResourceStream("ShapeIt.StringTableDeutsch.xml")) + { + XmlDocument stringXmlDocument = new XmlDocument(); + stringXmlDocument.Load(str); + StringTable.AddStrings(stringXmlDocument); + } + + using (str = ThisAssembly.GetManifestResourceStream("ShapeIt.StringTableEnglish.xml")) + { + XmlDocument stringXmlDocument = new XmlDocument(); + stringXmlDocument.Load(str); + StringTable.AddStrings(stringXmlDocument); + } + using (str = ThisAssembly.GetManifestResourceStream("ShapeIt.MenuResource.xml")) + { + XmlDocument menuDocument = new XmlDocument(); + menuDocument.Load(str); +#if DEBUG + // inject an additional menu "Debug", which we can use to directly execute some debugging code + XmlNode mainMenu = menuDocument.SelectSingleNode("Menus/MainMenu/Popup[@MenuId='MenuId.Extras']"); + if (mainMenu != null) + { + // Create a new MenuItem element. + XmlElement debugMenuItem = menuDocument.CreateElement("MenuItem"); + // Set the MenuId attribute to "MenuId.Debug". + debugMenuItem.SetAttribute("MenuId", "MenuId.Debug"); + // Append the new MenuItem element to the MainMenu node. + mainMenu.AppendChild(debugMenuItem); + } +#endif + XmlNode toolbar = menuDocument.SelectSingleNode("Menus/Popup[@MenuId='Toolbar']"); + // SetToolbar(toolbar); // TODO CadForm + MenuResource.SetMenuResource(menuDocument); + // ResetMainMenu(null); // TODO CadForm + } + + lastSaved = DateTime.Now; + AppDomain.CurrentDomain.UnhandledException += (sender, exobj) => + { + // exobj.ExceptionObject as Exception; + try + { + string path = System.IO.Path.GetTempPath(); + path = System.IO.Path.Combine(path, "ShapeIt"); + DirectoryInfo dirInfo = Directory.CreateDirectory(path); + string currentFileName = CadFrame.Project.FileName; + if (string.IsNullOrEmpty(CadFrame.Project.FileName)) + { + path = System.IO.Path.Combine(path, "crash_" + DateTime.Now.ToString("yyMMddHHmm") + ".cdb.json"); + currentFileName = "unknown"; + } + else + { + string crashFileName = System.IO.Path.GetFileNameWithoutExtension(CadFrame.Project.FileName); + if (crashFileName.EndsWith(".cdb")) crashFileName = System.IO.Path.GetFileNameWithoutExtension(crashFileName); // we usually have two extensions: .cdb.json + path = System.IO.Path.Combine(path, crashFileName + "_X.cdb.json"); + } + CadFrame.Project.WriteToFile(path); + File.WriteAllText(System.IO.Path.Combine(System.IO.Path.GetTempPath(), @"ShapeIt\Crash.txt"), currentFileName + "\n" + path); + } + catch (Exception) { } + ; + }; + // the following installs the property page for modelling. This connects all modelling + // tasks of ShapeIt with CADability + // IPropertyPage modellingPropPage = CadFrame.ControlCenter.AddPropertyPage("Modelling", 6); TODO + // modellingPropertyEntries = new ModellingPropertyEntries(CadFrame); TODO currently crashes + // modellingPropPage.Add(modellingPropertyEntries, false); TODO implement properties explorer implementing IControlCenter + // CadFrame.ControlCenter.ShowPropertyPage("Modelling"); TODO + } + + private void OnViewsChanged(IFrame theFrame) + { + theFrame.ActiveView.Projection.ProjectionChangedEvent -= OnProjectionChanged; // not to double it? + theFrame.ActiveView.Projection.ProjectionChangedEvent += OnProjectionChanged; + } + + private void OnProjectionChanged(Projection sender, EventArgs args) + { + projectionChanged = true; + } + +// // TODO reimplement in Avalonia +// protected override void OnShown(EventArgs e) +// { +// // check for crash +// if (!crashChecked) +// { +// crashChecked = true; +// string crashPath = System.IO.Path.Combine(System.IO.Path.GetTempPath(), @"ShapeIt\Crash.txt"); +// // if (File.Exists(crashPath)) +// // { +// // string[] lines = File.ReadAllLines(System.IO.Path.Combine(System.IO.Path.GetTempPath(), @"ShapeIt\Crash.txt")); +// // if (lines.Length == 2) +// // { +// // string ask = StringTable.GetFormattedString("ShapeIt.RestoreAfterCrash", lines[0]); +// // if (CadFrame.UIService.ShowMessageBox(ask, "ShapeIt", CADability.Substitutes.MessageBoxButtons.YesNo) == CADability.Substitutes.DialogResult.Yes) +// // { +// // CadFrame.Project = Project.ReadFromFile(lines[1]); +// // CadFrame.Project.FileName = lines[0]; + +// // this.Text = "ShapeIt -- " + lines[0]; +// // } +// // } +// // File.Delete(crashPath); +// // } +// #if DEBUG +// AutoDebug(); +// #endif +// } +// base.OnActivated(e); +// } +#if DEBUG + private void AutoDebug() + { + return; + string? filename = @"C:\Users\gerha\Documents\Zeichnungen\RoundEdgesTest2.cdb.json"; + // add code here to be executed automatically upon start in debug mode + // there is no mouse interaction before this code is finished + if (string.IsNullOrEmpty(filename)) + { + string[] mru = MRUFiles.GetMRUFiles(); + if (mru.Length > 0) filename = mru.Last().Split(';')[0]; + } + if (!string.IsNullOrEmpty(filename)) CadFrame.Project = Project.ReadFromFile(filename); + + string command = ""; + List slds = new List(); + Solid operand1 = null, operand2 = null; + List difference = new List(); + List intersection = new List(); + List union = new List(); + List edgeMarkers = new List(); + foreach (CADability.GeoObject.IGeoObject go in CadFrame.Project.GetActiveModel().AllObjects) + { + if (go is CADability.GeoObject.Solid sld) + { + slds.Add(sld); + if (sld.Style != null) + { + if (sld.Style.Name == "Operand1") operand1 = sld; + else if (sld.Style.Name == "Operand2") operand2 = sld; + else if (sld.Style.Name == "Difference") difference.Add(sld); + else if (sld.Style.Name == "Union") union.Add(sld); + else if (sld.Style.Name == "Intersection") intersection.Add(sld); + } + } + if (go is ICurve curve) + { + if (go.Style!=null && go.Style.Name == "EdgeMarker") + { + edgeMarkers.Add(curve); + } + } + if (go is Text txt) + { + command = txt.TextString; // there sould only be one + } + } + if (operand1 != null && operand2 != null) + { + //if (difference.Count > 0) + //{ + // Solid[] sres = NewBooleanOperation.Subtract(operand1, operand2); + // if (sres.Length > 0) + // { + // Project proj = Project.CreateSimpleProject(); + // proj.GetActiveModel().Add(sres); + // proj.WriteToFile("c:\\Temp\\subtract.cdb.json"); + // } + //} + if (command.StartsWith("Difference",StringComparison.OrdinalIgnoreCase)) + { + Solid[] sres = NewBooleanOperation.Subtract(operand1, operand2); + } + if (command.Equals("Union", StringComparison.OrdinalIgnoreCase) || command.Equals("Unite", StringComparison.OrdinalIgnoreCase)) + { + Solid sres = NewBooleanOperation.Unite(operand1, operand2); + } + } + if (slds.Count == 2) + { + //Solid un = NewBooleanOperation.Unite(slds[0], slds[1]); + //Solid[] sld; + //if (slds[0].Volume(0.1) > slds[1].Volume(0.1)) + //{ + // sld = NewBooleanOperation.Subtract(slds[0], slds[1]); + //} + //else + //{ + // sld = NewBooleanOperation.Subtract(slds[1], slds[0]); + //} + //if (sld.Length > 0) + //{ + // Project proj = Project.CreateSimpleProject(); + // proj.GetActiveModel().Add(sld); + // proj.WriteToFile("c:\\Temp\\subtract.cdb.json"); + //} + } + if (edgeMarkers.Count > 0) + { + List edgesToRound = []; + Shell? shellToRound = null; + foreach (Solid s in slds) + { + foreach (Edge edg in s.Shells[0].Edges) + { + foreach (ICurve em in edgeMarkers) + { + if (edg.Curve3D.SameGeometry(em, 0.1)) + { + edgesToRound.Add(edg); + shellToRound = s.Shells[0]; + } + } + } + } + if (command.StartsWith("RoundEdges", StringComparison.OrdinalIgnoreCase)) + { + string[] parts = command.Split(':'); + if (parts.Length==2) + { + double d = double.Parse(parts[1]); + if (d>0) + { + Shell? rounded = shellToRound?.RoundEdges(edgesToRound, d); + } + } + } + if (command.StartsWith("ChamferEdges", StringComparison.OrdinalIgnoreCase)) + { + string[] parts = command.Split(':'); + if (parts.Length == 2) + { + double d = double.Parse(parts[1]); + if (d > 0) + { + Shell? rounded = shellToRound?.ChamferEdges(edgesToRound, d, d); + // CadFrame.Project.GetActiveModel().Add(rounded); + } + } + } + } + } +#endif + + /// + /// Filter the escape key for the modelling property page + /// + /// + /// + /// +// // TODO reimplement in Avalonia + // protected override bool ProcessCmdKey(ref Message msg, Keys keyData) + // { + // Keys nmKeyData = (Keys)((int)keyData & 0x0FFFF); + // CADability.Substitutes.KeyEventArgs e = new CADability.Substitutes.KeyEventArgs((CADability.Substitutes.Keys)keyData); + // if (nmKeyData == Keys.Escape) + // { + // if (modellingPropertyEntries.OnEscape()) return true; + // } + // return base.ProcessCmdKey(ref msg, keyData); + // } + /// + /// Called when CADability is idle. We use it to save the current project data to a temp file in case of a crash + /// + /// + /// + // slowdown OnIdle polling: + readonly Stopwatch _idleSw = Stopwatch.StartNew(); + const int MinIdleCheckMs = 250; + bool _idleBusy; + void OnIdle(object sender, EventArgs e) + { + if (_idleBusy) return; + if (_idleSw.ElapsedMilliseconds < MinIdleCheckMs) return; + + _idleBusy = true; + _idleSw.Restart(); + try + { + if (projectionChanged) + { + projectionChanged = false; + modellingPropertyEntries.OnProjectionChanged(); // to update the feedback objects, which are projection dependant + } + if (modifiedSinceLastAutosave && (DateTime.Now - lastSaved).TotalMinutes > 2) + { + modifiedSinceLastAutosave = false; + string path = System.IO.Path.GetTempPath(); + path = System.IO.Path.Combine(path, "ShapeIt"); + DirectoryInfo dirInfo = Directory.CreateDirectory(path); + string currentFileName = CadFrame.Project.FileName; + if (string.IsNullOrEmpty(CadFrame.Project.FileName)) path = System.IO.Path.Combine(path, "noname.cdb.json"); + else + { + string fileName = System.IO.Path.GetFileNameWithoutExtension(CadFrame.Project.FileName); + if (fileName.EndsWith(".cdb")) fileName = System.IO.Path.GetFileNameWithoutExtension(fileName); // we usually have two extensions: .cdb.json + path = System.IO.Path.Combine(path, fileName + "_.cdb.json"); + } + CadFrame.Project.WriteToFile(path); + CadFrame.Project.FileName = currentFileName; // Project.WriteToFile changes the Project.FileName, restore the current name + lastSaved = DateTime.Now; + } + } + finally { _idleBusy = false; } + } + void OnProjectClosed(Project theProject, IFrame theFrame) + { + // manage autosave OnIdle, remove autosaved files + } + /// + /// When a new or exisiting project has been opened + /// + /// + /// + private void OnProjectOpened(Project theProject, IFrame theFrame) + { + theProject.ProjectModifiedEvent += (Project sender) => + { // register modifications of the project to manage autosave + if (sender == theProject) modifiedSinceLastAutosave = true; + }; + } +// // TODO reimplement in Avalonia + // protected override void OnLoad(EventArgs e) + // { + // base.OnLoad(e); + + // // this is for recording the session with 1280x720 pixel. + // this.Size = new Size(1294, 727); + + // } + /// + /// Give the user a chance to save the modified project + /// + /// +// // TODO reimplement in Avalonia + // protected override void OnFormClosing(FormClosingEventArgs e) + // { + // if (!CadFrame.Project.SaveModified()) e.Cancel = true; + // base.OnFormClosing(e); + // } +// // TODO reimplement in Avalonia +// public override bool OnCommand(string MenuId) +// { +// // forward to modellingPropertyEntries first +// if (modellingPropertyEntries.OnCommand(MenuId)) return true; +// if (MenuId == "MenuId.App.Exit") +// { // this command cannot be handled by CADability.dll +// Application.Exit(); +// return true; +// } +// #if DEBUG +// else if (MenuId == "MenuId.Debug") +// { +// Debug(); +// return true; +// } +// #endif +// else return base.OnCommand(MenuId); +// } +// // TODO reimplement in Avalonia + // public override bool OnUpdateCommand(string MenuId, CommandState CommandState) + // { + // // forward to modellingPropertyEntries first + // if (modellingPropertyEntries.OnUpdateCommand(MenuId, CommandState)) return true; + // return base.OnUpdateCommand(MenuId, CommandState); + // } +// // TODO reimplement in Avalonia + // public override void OnSelected(MenuWithHandler selectedMenuItem, bool selected) + // { + // modellingPropertyEntries.OnSelected(selectedMenuItem, selected); + // base.OnSelected(selectedMenuItem, selected); + // } +#if DEBUG + private static Random rnd = new Random(); + private GeoVector RandomVector(double len) + { + double fx = 1.0, fy = 1.0, fz = 1.0; + if (rnd.NextDouble() < 0.5) fx = -fx; + if (rnd.NextDouble() < 0.5) fy = -fy; + if (rnd.NextDouble() < 0.5) fz = -fz; + return len * (new GeoVector(fx * rnd.NextDouble(), fy * rnd.NextDouble(), fz * rnd.NextDouble())).Normalized; + } + private double RandomDouble(double min, double max) + { + return min + (max - min) * rnd.NextDouble(); + } + /// + /// Here we can add some debug code + /// + private void DebugX() + { + Model model = CadFrame.Project.GetActiveModel(); + Style stl = CadFrame.Project.StyleList.GetDefault(CADability.Attribute.Style.EDefaultFor.Solids); + for (int i = 0; i < 64; i++) + { + double fx = 1.0, fy = 1.0, fz = 1.0; + if ((i & 0x1) != 0) fx = -fx; + if ((i & 0x2) != 0) fy = -fy; + if ((i & 0x4) != 0) fz = -fz; + GeoVector dir = RandomVector(RandomDouble(40, 60)); + GeoPoint center = GeoPoint.Origin + dir; + Solid sld = Make3D.MakeSphere(center, 10 + 20 * rnd.NextDouble()); + ModOp rotate = ModOp.Rotate(center, RandomVector(1.0), SweepAngle.Deg(rnd.NextDouble() * 360.0)); + sld.Modify(rotate); + sld.Style = stl; + model.Add(sld); + } + Solid mainSphere = Make3D.MakeSphere(GeoPoint.Origin, 50); + mainSphere.Style = stl; + model.Add(mainSphere); + } + + + private void DebugY() + { + List slds = new List(); + CADability.GeoObject.Solid sldbig = null; + Face cone = null; + Face upper = null; + Face lower = null; + foreach (CADability.GeoObject.IGeoObject go in CadFrame.Project.GetActiveModel().AllObjects) + { + if (go is CADability.GeoObject.Solid sld) + { + if (sld.Volume(0.1) > 500000) sldbig = sld; + else slds.Add(sld); + } + } + + var rng = new Random(71); + var order = Enumerable.Range(0, slds.Count).ToArray(); + + // Fisher–Yates + for (int i = slds.Count - 1; i > 0; i--) + { + int j = rng.Next(i + 1); + (order[i], order[j]) = (order[j], order[i]); + } + + + if (slds.Count > 1 && sldbig != null) + { + + for (int i = 0; i < slds.Count; i++) + { + Solid[] res = NewBooleanOperation.Subtract(sldbig, slds[order[i]]); + if (res != null && res.Length >= 1) sldbig = res[0]; + else { } + //System.Diagnostics.Debug.WriteLine("Unite " + i.ToString() + " -> " + sldbig.GetExtent(0.0).Size.ToString("F0")); + } + } + } + private void Debug() + { + List slds = new List(); + Solid operand1 = null, operand2 = null; + List difference = new List(); + List intersection = new List(); + List union = new List(); + List edgeMarkers = new List(); + foreach (CADability.GeoObject.IGeoObject go in CadFrame.Project.GetActiveModel().AllObjects) + { + if (go is CADability.GeoObject.Solid sld) + { + slds.Add(sld); + if (sld.Style != null) + { + if (sld.Style.Name == "Operand1") operand1 = sld; + else if (sld.Style.Name == "Operand2") operand2 = sld; + else if (sld.Style.Name == "Difference") difference.Add(sld); + else if (sld.Style.Name == "Union") union.Add(sld); + else if (sld.Style.Name == "Intersection") intersection.Add(sld); + } + } + if (go is ICurve curve) + { + if (go.Style.Name == "EdgeMarker") + { + edgeMarkers.Add(curve); + } + } + } + if (operand1 != null && operand2 != null) + { + if (difference.Count > 0) + { + Solid[] sres = NewBooleanOperation.Subtract(operand1, operand2); + if (sres.Length > 0) + { + Project proj = Project.CreateSimpleProject(); + proj.GetActiveModel().Add(sres); + proj.WriteToFile("c:\\Temp\\subtract.cdb.json"); + } + } + } + if (slds.Count == 2) + { + //Solid un = NewBooleanOperation.Unite(slds[0], slds[1]); + Solid[] sld; + if (slds[0].Volume(0.1) > slds[1].Volume(0.1)) + { + sld = NewBooleanOperation.Subtract(slds[0], slds[1]); + } + else + { + sld = NewBooleanOperation.Subtract(slds[1], slds[0]); + } + //if (sld.Length > 0) + //{ + // Project proj = Project.CreateSimpleProject(); + // proj.GetActiveModel().Add(sld); + // proj.WriteToFile("c:\\Temp\\subtract.cdb.json"); + //} + } + if (edgeMarkers.Count > 0) + { + foreach (Solid s in slds) + { + foreach (Edge edg in s.Shells[0].Edges) + { + foreach (ICurve em in edgeMarkers) + { + if (edg.Curve3D.SameGeometry(em, 0.1)) + { + Shell rounded = s.Shells[0].RoundEdges(new Edge[] { edg }, 2); + } + } + } + } + } + //if (slds.Count == 1) + //{ + // Shell shell = slds[0].Shells[0]; + // foreach (var vtx in shell.Vertices) + // { + // if ((vtx.Position | new GeoPoint(58, 12, 17)) < 3) + // { + // Shell rounded = shell.RoundEdges(vtx.Edges, 2); + // } + // } + //} + } +#endif + } +} diff --git a/ShapeIt/ShapeIt.axaml.cs b/ShapeIt/ShapeIt.axaml.cs index 925f1093..d7ea96c7 100644 --- a/ShapeIt/ShapeIt.axaml.cs +++ b/ShapeIt/ShapeIt.axaml.cs @@ -15,7 +15,7 @@ public override void OnFrameworkInitializationCompleted() { if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { - desktop.MainWindow = new MainForm(); + desktop.MainWindow = new MainWindow(desktop.Args); } base.OnFrameworkInitializationCompleted(); diff --git a/ShapeIt/ShapeIt.csproj b/ShapeIt/ShapeIt.csproj index b93912bd..a42b098e 100644 --- a/ShapeIt/ShapeIt.csproj +++ b/ShapeIt/ShapeIt.csproj @@ -7,6 +7,11 @@ true disable + + false + false + + WinExe From 6d083e9350c6a804aa9ee1fba14ea9c5e070c4ab Mon Sep 17 00:00:00 2001 From: Jan Schopohl Date: Sat, 14 Feb 2026 21:38:19 +0100 Subject: [PATCH 05/12] Properly handle VBOs containing multiple objects --- CADability.Avalonia/PaintToOpenGL.cs | 56 ++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/CADability.Avalonia/PaintToOpenGL.cs b/CADability.Avalonia/PaintToOpenGL.cs index 1df15f43..5ab05058 100644 --- a/CADability.Avalonia/PaintToOpenGL.cs +++ b/CADability.Avalonia/PaintToOpenGL.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Numerics; using System.Linq; +using System.Runtime.InteropServices; using Silk.NET.OpenGL; using Avalonia.Controls; using Avalonia.OpenGL; @@ -454,12 +455,21 @@ unsafe void IPaintTo3D.List(IPaintTo3DList paintThisList) } if (!vao.HasIndices) { - // TODO draw using vao.Segments to prevent line segments between different lines - _gl.DrawArrays(vao.Primitive, 0, vao.FloatCount); + // for simple lines, just drawing the whole VBO with DrawArrays should be fine. + // However, this does not work for line strips + // _gl.DrawArrays(vao.Primitive, 0, vao.FloatCount); + fixed (void* offsets = CollectionsMarshal.AsSpan(vao.SegmentOffsets), counts = CollectionsMarshal.AsSpan(vao.SegmentCounts)) { + _gl.MultiDrawArrays(vao.Primitive, (int*)offsets, (uint*)counts, (uint)vao.SegmentOffsets.Count); + } } else { - _gl.DrawElements(vao.Primitive, vao.IndicesCount, DrawElementsType.UnsignedInt, (void*)0); + // for triangles, we could just use DrawElements and not care about multiple objects + // in one VBO. However, it could be a problem for other primitives and triangle strips + // _gl.DrawElements(vao.Primitive, vao.IndicesCount, DrawElementsType.UnsignedInt, (void*)0); + fixed (void* offsets = vao.OffsetPointers, counts = CollectionsMarshal.AsSpan(vao.SegmentCounts)) { + _gl.MultiDrawElements(vao.Primitive, (uint*)counts, GLEnum.UnsignedInt, (void**)offsets, (uint)vao.SegmentOffsets.Count); + } } _gl.BindVertexArray(0); @@ -519,7 +529,7 @@ private float[] getIdentity4x4() { return mat; } - void IPaintTo3D.SetProjection(Projection projection, BoundingCube boundingCube) + void IPaintTo3D.SetProjection(Projection projection, BoundingBox boundingCube) { _gl.Viewport(0, 0, (uint)Bounds.Width, (uint)Bounds.Height); @@ -533,7 +543,7 @@ void IPaintTo3D.SetProjection(Projection projection, BoundingCube boundingCube) // There is no foolproof way to handle this, but at least the most common cases should work when we alway use a equilateral (regular) cube double size = Math.Max(boundingCube.XDiff, Math.Max(boundingCube.YDiff, boundingCube.ZDiff)); GeoPoint center = boundingCube.GetCenter(); - BoundingCube boundingCubeEquilateral = new BoundingCube(new GeoPoint(center.x - size / 2, center.y - size / 2, center.z - size / 2), + BoundingBox boundingCubeEquilateral = new BoundingBox(new GeoPoint(center.x - size / 2, center.y - size / 2, center.z - size / 2), new GeoPoint(center.x + size / 2, center.y + size / 2, center.z + size / 2)); double [,] mm = projection.GetOpenGLProjection(0, (int)Bounds.Width, 0, (int)Bounds.Height, boundingCubeEquilateral); @@ -763,7 +773,9 @@ internal class VertexArrayObject : IPaintTo3DList private uint stride; private Substitutes.Color color; private float[] modelView; // column major order 4x4 matrix - private List<(uint, uint)> segments; + private List segmentOffsets; + private List segmentCounts; + unsafe private uint*[] offsetPointers; public VertexArrayObject(string name, GL gl) { @@ -779,7 +791,8 @@ public VertexArrayObject(string name, GL gl) elementBufferObject = _gl.GenBuffer(); vertices = new List(); indices = new List(); - segments = new List<(uint, uint)>(); + segmentOffsets = new List(); + segmentCounts = new List(); } public string Name @@ -802,10 +815,14 @@ public List containedSubLists public uint IndicesCount => indicesCount; public uint Stride => stride; public bool IsClosed => closed; + public bool HasNormals => hasNormals; + public List SegmentOffsets => segmentOffsets; + public List SegmentCounts => segmentCounts; + unsafe public uint*[] OffsetPointers => offsetPointers; public Substitutes.Color Color { set; get; } public float[] ModelView { set; get; } - public void addVertices(GeoPoint[] points, GLEnum primitiveType, uint stride, GeoVector[] normals = null) { + public void addVertices(GeoPoint[] points, GLEnum primitiveType, uint stride, GeoVector[] normals = null, bool addSegment = true) { if (this.primitiveType != GLEnum.False && primitiveType != this.primitiveType) { throw new ApplicationException("Only supports one primitive type in a VertexArrayObject"); } @@ -814,6 +831,12 @@ public void addVertices(GeoPoint[] points, GLEnum primitiveType, uint stride, Ge } if (closed) throw new ApplicationException("Cannot add to closed VertexArrayObject"); + if (addSegment) { + int offset = segmentOffsets.LastOrDefault() + (int)segmentCounts.LastOrDefault(); + segmentCounts.Add((uint)points.Length); + segmentOffsets.Add(offset); + } + if (normals == null) { this.vertices.AddRange(points.SelectMany( vertex => new float[]{(float)vertex.x, (float)vertex.y, (float)vertex.z})); @@ -834,7 +857,10 @@ public void addIndexedVertices(GeoPoint[] points, int[] indices, GLEnum primitiv // only apply an offset if not drawing in segments and offsetting the vertex attrib pointer uint offset = (uint) (this.vertices.Count / (stride / sizeof(float))); - this.addVertices(points, primitiveType, stride, normals); + this.addVertices(points, primitiveType, stride, normals, false); + + segmentCounts.Add((uint)indices.Length); + segmentOffsets.Add(this.indices.Count * sizeof(uint)); this.indices.AddRange(indices.Select(i => (uint) i + offset)); this.hasIndices = true; @@ -859,17 +885,23 @@ unsafe public void Close() pIData, BufferUsageARB.StaticDraw); } - // TODO VertexAttribPointer here or when drawing in List? _gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, stride, (void*)0); _gl.EnableVertexAttribArray(0); - if (this.hasNormals) + if (this.HasNormals) { _gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float))); _gl.EnableVertexAttribArray(1); } - _gl.BindVertexArray(vertexArrayObject); + _gl.BindVertexArray(0); + if (this.hasIndices) { + // we have to convert segment offsets to pointers, c# does not support pointers in lists/generic type expressions + offsetPointers = new uint*[this.segmentOffsets.Count]; + for (int i = 0; i < this.segmentOffsets.Count; i++) { + offsetPointers[i] = (uint*)this.segmentOffsets[i]; + } + } floatCount = (uint)vertices.Count; indicesCount = (uint)indices.Count; vertices.Clear(); From a387911e2dcc50dfc34a39ecf83ea51678228c76 Mon Sep 17 00:00:00 2001 From: Jan Schopohl Date: Mon, 16 Feb 2026 01:41:28 +0100 Subject: [PATCH 06/12] Add MenuManager using Avalonia and MarkupExtension for StringTable --- CADability.Avalonia/CadControl.axaml.cs | 2 + CADability.Avalonia/MarkupExtensions.cs | 15 +++++++ CADability.Avalonia/MenuManager.cs | 57 +++++++++++++++++++++++++ ShapeIt/MainWindow.axaml | 27 ++---------- ShapeIt/MainWindow.axaml.cs | 35 +++++++++------ 5 files changed, 100 insertions(+), 36 deletions(-) create mode 100644 CADability.Avalonia/MarkupExtensions.cs create mode 100644 CADability.Avalonia/MenuManager.cs diff --git a/CADability.Avalonia/CadControl.axaml.cs b/CADability.Avalonia/CadControl.axaml.cs index 9a75af05..031714a5 100644 --- a/CADability.Avalonia/CadControl.axaml.cs +++ b/CADability.Avalonia/CadControl.axaml.cs @@ -25,6 +25,8 @@ public CadControl() // propertiesExplorer.Frame = cadFrame; // show this menu in the MainForm // MenuWithHandler[] mainMenu = MenuResource.LoadMenuDefinition("SDI Menu", true, cadFrame); + // MenuManager.MakeMainMenu(mainMenu, dockPanel, this); TODO moved to Main Window + // MainMenuStrip = MenuManager.MakeMainMenu(mainMenu); // Controls.Add(MainMenuStrip); // cadFrame.FormMenu = MainMenuStrip; diff --git a/CADability.Avalonia/MarkupExtensions.cs b/CADability.Avalonia/MarkupExtensions.cs new file mode 100644 index 00000000..2cc8740a --- /dev/null +++ b/CADability.Avalonia/MarkupExtensions.cs @@ -0,0 +1,15 @@ +using CADability.UserInterface; +using System; + +namespace CADability.Avalonia +{ + public class StringTableExtension + { + public string Key { get; set; } = ""; + + public string ProvideValue(IServiceProvider serviceProvider) + { + return StringTable.GetString(Key) ?? Key; + } + } +} diff --git a/CADability.Avalonia/MenuManager.cs b/CADability.Avalonia/MenuManager.cs new file mode 100644 index 00000000..aa7ed22c --- /dev/null +++ b/CADability.Avalonia/MenuManager.cs @@ -0,0 +1,57 @@ +using Avalonia.Controls; +using CADability.UserInterface; +using System; +using System.Windows.Input; + +namespace CADability.Avalonia +{ + internal class MenuCommand : ICommand + { + ICommandHandler handler; + + public MenuCommand(ICommandHandler handler) + { + this.handler = handler; + } + + public event EventHandler CanExecuteChanged; + + public bool CanExecute(object parameter) + { + return true; + } + + public void Execute(object parameter) + { + handler.OnCommand((string)parameter); + } + } + + public static class MenuManager + { + public static void MakeMainMenu(MenuWithHandler[] definition, Menu mainMenu, ICommandHandler handler) + { + foreach (var menuDefinition in definition) { + MenuItem item = new MenuItem { + Header = menuDefinition.Text, + Command = new MenuCommand(handler), + CommandParameter = menuDefinition.ID, + }; + foreach (var subMenuItem in menuDefinition.SubMenus) { + if (subMenuItem.ID == "SEPARATOR") { + item.Items.Add(new Separator()); + } else { + MenuItem subItem = new MenuItem { + Header = subMenuItem.Text, + Command = new MenuCommand(handler), + CommandParameter = subMenuItem.ID, + }; + item.Items.Add(subItem); + } + } + mainMenu.Items.Add(item); + // TODO sub menu items recursive? + } + } + } +} diff --git a/ShapeIt/MainWindow.axaml b/ShapeIt/MainWindow.axaml index 9488f414..a9a9d972 100644 --- a/ShapeIt/MainWindow.axaml +++ b/ShapeIt/MainWindow.axaml @@ -1,34 +1,15 @@ - + - - - - - - - - - - - - - - - - - - - - - - + propertiesExplorer; @@ -276,6 +279,12 @@ public MainWindow(string[] args) // TODO inherit from CadForm? : base(args) XmlNode toolbar = menuDocument.SelectSingleNode("Menus/Popup[@MenuId='Toolbar']"); // SetToolbar(toolbar); // TODO CadForm MenuResource.SetMenuResource(menuDocument); + + // TODO move this to CadControl? + MenuWithHandler[] mainMenuDefinition = MenuResource.LoadMenuDefinition("SDI Menu", true, CadFrame); + // MenuManager.MakeMainMenu(mainMenuDefinition, dockPanel, this); + MenuManager.MakeMainMenu(mainMenuDefinition, mainMenuObject, this); + // ResetMainMenu(null); // TODO CadForm } From cf47c79897cf99d4b443265fdbbf2d2405f0fd34 Mon Sep 17 00:00:00 2001 From: Jan Schopohl Date: Mon, 16 Feb 2026 22:34:24 +0100 Subject: [PATCH 07/12] Remove debugging output and obsolete opengl code --- CADability.Avalonia/PaintToOpenGL.cs | 58 ++-------------------------- 1 file changed, 3 insertions(+), 55 deletions(-) diff --git a/CADability.Avalonia/PaintToOpenGL.cs b/CADability.Avalonia/PaintToOpenGL.cs index 5ab05058..49889a86 100644 --- a/CADability.Avalonia/PaintToOpenGL.cs +++ b/CADability.Avalonia/PaintToOpenGL.cs @@ -23,8 +23,6 @@ class PaintToOpenGL : OpenGlControlBase, IPaintTo3D private uint _shaderProgram; private uint _vertexShader; private uint _fragmentShader; - private uint _vertexBufferObject; - private uint _vertexArrayObject; private uint _indexBufferObject; private int modelViewLocation; private int projectionLocation; @@ -106,34 +104,9 @@ private void ConfigureShaders() colorLocation = _gl.GetUniformLocation(_shaderProgram, "color"); lightPositionLocation = _gl.GetUniformLocation(_shaderProgram, "lightPosition"); ambientFactorLocation = _gl.GetUniformLocation(_shaderProgram, "ambientFactor"); - Console.WriteLine("modelViewLocation: " + modelViewLocation); - Console.WriteLine("projectionLocation: " + projectionLocation); - Console.WriteLine("colorLocation: " + colorLocation); GlCheckError(); } - private unsafe void CreateVertexBuffer() - { - Vector3[] vertices = new Vector3[] - { - new Vector3(-1.0f, -1.0f, 0.0f), - new Vector3(1.0f, -1.0f, 0.0f), - new Vector3(0.0f, 1.0f, 0.0f), - }; - - _vertexBufferObject = _gl.GenBuffer(); - _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vertexBufferObject); - - fixed(void * pData = vertices) - _gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint) (sizeof(Vector3) * vertices.Length), - pData, BufferUsageARB.StaticDraw); - - _vertexArrayObject = _gl.GenVertexArray(); - _gl.BindVertexArray(_vertexArrayObject); - _gl.VertexAttribPointer(0, 3, GLEnum.Float, false, (uint) sizeof(Vector3), (void*)0); - _gl.EnableVertexAttribArray(0); - } - protected override void OnOpenGlInit(GlInterface gl) { base.OnOpenGlInit(gl); @@ -142,7 +115,6 @@ protected override void OnOpenGlInit(GlInterface gl) _gl = GL.GetApi(gl.GetProcAddress); ConfigureShaders(); - CreateVertexBuffer(); GlCheckError(); } @@ -155,8 +127,6 @@ protected override void OnOpenGlDeinit(GlInterface gl) _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, 0); _gl.BindVertexArray(0); _gl.UseProgram(0); - _gl.DeleteBuffer(_vertexBufferObject); - _gl.DeleteVertexArray(_vertexArrayObject); _gl.DeleteProgram(_shaderProgram); _gl.DeleteShader(_vertexShader); _gl.DeleteShader(_fragmentShader); @@ -177,8 +147,7 @@ protected override void OnOpenGlRender(GlInterface _, int fb) } GlCheckError(); - // disabled redraw for debugging - // Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); + Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); } string VertexShaderSource => GetShader(@" @@ -311,7 +280,6 @@ private void SetFragmentColor(Color color) void IPaintTo3D.SetColor(Color color, int lockColor = 0) { - Console.WriteLine("Setting color: " + color); Color res; if (!colorOverride) { @@ -349,7 +317,7 @@ void IPaintTo3D.AvoidColor(Color color) void IPaintTo3D.SetLineWidth(LineWidth lineWidth) { if (lineWidth != null) { - Console.WriteLine("SetLineWidth: " + lineWidth.Width); + // Console.WriteLine("SetLineWidth: " + lineWidth.Width); } // throw new NotImplementedException(); } @@ -359,8 +327,6 @@ void IPaintTo3D.SetLinePattern(LinePattern pattern) } unsafe void IPaintTo3D.Polyline(GeoPoint[] points) { - Console.WriteLine("Paint Polyline"); - VertexArrayObject vao = currentVao; if (vao == null) vao = new VertexArrayObject("single Polyline VAO", _gl); @@ -390,7 +356,6 @@ unsafe void IPaintTo3D.Triangle(GeoPoint[] vertices, GeoVector[] normals, int[] // TODO correctly sort indices depending on direction and enable culling - Console.WriteLine("normals.Length: " + normals.Length); vao.addIndexedVertices(vertices, indextriples, GLEnum.Triangles, (uint)(6 * sizeof(float)), normals); // vao.addIndexedVertices(vertices, indextriples, GLEnum.Triangles, (uint)(3 * sizeof(float))); @@ -440,7 +405,6 @@ unsafe void IPaintTo3D.List(IPaintTo3DList paintThisList) { throw new ApplicationException("paintThisList does not exist: " + paintThisList.Name); } - Console.WriteLine("IpaintTo3D.List: " + vao.Name); if (!vao.IsClosed) throw new ApplicationException("Can only paint closed VAOs"); @@ -547,14 +511,6 @@ void IPaintTo3D.SetProjection(Projection projection, BoundingBox boundingCube) new GeoPoint(center.x + size / 2, center.y + size / 2, center.z + size / 2)); double [,] mm = projection.GetOpenGLProjection(0, (int)Bounds.Width, 0, (int)Bounds.Height, boundingCubeEquilateral); - float [,] mmFloat = new float[,] { - { (float)mm[0, 0], (float)mm[0, 1], (float)mm[0, 2], (float)mm[0, 3]}, - { (float)mm[1, 0], (float)mm[1, 1], (float)mm[1, 2], (float)mm[1, 3]}, - { (float)mm[2, 0], (float)mm[2, 1], (float)mm[2, 2], (float)mm[2, 3]}, - { (float)mm[3, 0], (float)mm[3, 1], (float)mm[3, 2], (float)mm[3, 3]} - }; - Matrix debugMatrix = DenseMatrix.OfArray(mmFloat); - Console.WriteLine(debugMatrix); float[] pmat = new float[16]; // ACHTUNG: Matrix ist vertauscht!!! pmat[0] = (float) mm[0, 0]; @@ -581,7 +537,6 @@ void IPaintTo3D.SetProjection(Projection projection, BoundingBox boundingCube) GeoVector v; // v = projection.InverseProjection * new GeoVector(0.5, 0.3, -1.0); v = projection.InverseProjection * new GeoVector(100.0, 300.0, 1000.0); - Console.WriteLine("Light Position: " + v); pixelToWorld = projection.DeviceToWorldFactor; _gl.Enable(GLEnum.DepthTest); @@ -611,8 +566,6 @@ void IPaintTo3D.OpenList(string name) if (name == null) throw new ApplicationException("List name cannot be empty."); if (currentVao != null) throw new ApplicationException("VAOs cannot be nested!"); - Console.WriteLine("OpenList: " + name); - currentVao = new VertexArrayObject(name, _gl); // this overwrites any previously existing vao with this name vaos[name] = currentVao; @@ -620,7 +573,6 @@ void IPaintTo3D.OpenList(string name) } IPaintTo3DList IPaintTo3D.CloseList() { - Console.WriteLine("Close List: " + currentVao.Name); if (currentVao != null) currentVao.Close(); VertexArrayObject res = currentVao; currentVao = null; @@ -673,24 +625,20 @@ void IPaintTo3D.FinishPaint() } unsafe void IPaintTo3D.PaintFaces(PaintTo3D.PaintMode paintMode) { - Console.WriteLine("PaintFaces: " + paintMode); if (paintMode == PaintTo3D.PaintMode.FacesOnly) { if (isPerspective) { - Console.WriteLine("Perspective Mode"); + throw new NotImplementedException(); } else { - // TODO is this a operation used in display list? - // then we would have to save the matrix to VertexBufferObject Matrix modelViewMat = DenseMatrix.OfArray(new float[,] { { 1.0f, 0.0f, 0.0f, (float)(2 * precision * projectionDirection.x)}, { 0.0f, 1.0f, 0.0f, (float)(2 * precision * projectionDirection.y)}, { 0.0f, 0.0f, 1.0f, (float)(2 * precision * projectionDirection.z)}, { 0.0f, 0.0f, 0.0f, 1.0f } }); - Console.WriteLine("Paint Faces, modelView Matrix: " + modelViewMat.ToString()); if (currentVao != null) { From c4d338c90c9012d284a6022c2bd987da67eee63e Mon Sep 17 00:00:00 2001 From: Jan Schopohl Date: Mon, 16 Feb 2026 23:04:58 +0100 Subject: [PATCH 08/12] Implement recursive menus --- CADability.Avalonia/MenuManager.cs | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/CADability.Avalonia/MenuManager.cs b/CADability.Avalonia/MenuManager.cs index aa7ed22c..0a3924bc 100644 --- a/CADability.Avalonia/MenuManager.cs +++ b/CADability.Avalonia/MenuManager.cs @@ -31,26 +31,21 @@ public static class MenuManager { public static void MakeMainMenu(MenuWithHandler[] definition, Menu mainMenu, ICommandHandler handler) { + CreateMenuItems(definition, mainMenu, handler); + } + + private static void CreateMenuItems(MenuWithHandler[] definition, ItemsControl menu, ICommandHandler handler) + { + if (definition is null) return; + foreach (var menuDefinition in definition) { MenuItem item = new MenuItem { Header = menuDefinition.Text, Command = new MenuCommand(handler), CommandParameter = menuDefinition.ID, }; - foreach (var subMenuItem in menuDefinition.SubMenus) { - if (subMenuItem.ID == "SEPARATOR") { - item.Items.Add(new Separator()); - } else { - MenuItem subItem = new MenuItem { - Header = subMenuItem.Text, - Command = new MenuCommand(handler), - CommandParameter = subMenuItem.ID, - }; - item.Items.Add(subItem); - } - } - mainMenu.Items.Add(item); - // TODO sub menu items recursive? + CreateMenuItems(menuDefinition.SubMenus, item, handler); + menu.Items.Add(item); } } } From fb471ff08f40a4c288a0d3b511a84eb26fb438a7 Mon Sep 17 00:00:00 2001 From: Jan Schopohl Date: Mon, 16 Feb 2026 23:24:15 +0100 Subject: [PATCH 09/12] Improve menu command handling path --- CADability.Avalonia/MenuManager.cs | 2 +- ShapeIt/MainWindow.axaml.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CADability.Avalonia/MenuManager.cs b/CADability.Avalonia/MenuManager.cs index 0a3924bc..9033b5c2 100644 --- a/CADability.Avalonia/MenuManager.cs +++ b/CADability.Avalonia/MenuManager.cs @@ -41,7 +41,7 @@ private static void CreateMenuItems(MenuWithHandler[] definition, ItemsControl m foreach (var menuDefinition in definition) { MenuItem item = new MenuItem { Header = menuDefinition.Text, - Command = new MenuCommand(handler), + Command = new MenuCommand(menuDefinition), CommandParameter = menuDefinition.ID, }; CreateMenuItems(menuDefinition.SubMenus, item, handler); diff --git a/ShapeIt/MainWindow.axaml.cs b/ShapeIt/MainWindow.axaml.cs index cd0e7923..a62081cc 100644 --- a/ShapeIt/MainWindow.axaml.cs +++ b/ShapeIt/MainWindow.axaml.cs @@ -281,7 +281,7 @@ public MainWindow(string[] args) // TODO inherit from CadForm? : base(args) MenuResource.SetMenuResource(menuDocument); // TODO move this to CadControl? - MenuWithHandler[] mainMenuDefinition = MenuResource.LoadMenuDefinition("SDI Menu", true, CadFrame); + MenuWithHandler[] mainMenuDefinition = MenuResource.LoadMenuDefinition("SDI Menu", true, this); // MenuManager.MakeMainMenu(mainMenuDefinition, dockPanel, this); MenuManager.MakeMainMenu(mainMenuDefinition, mainMenuObject, this); From 788b4bcbed6194e078bdbd1ce0dc46744f7e762f Mon Sep 17 00:00:00 2001 From: Jan Schopohl Date: Fri, 20 Feb 2026 02:38:32 +0100 Subject: [PATCH 10/12] Add properties explorer --- CADability.Avalonia/CadControl.axaml | 5 +- CADability.Avalonia/CadControl.axaml.cs | 4 +- CADability.Avalonia/CadFrame.cs | 4 +- CADability.Avalonia/PropertiesExplorer.axaml | 7 + .../PropertiesExplorer.axaml.cs | 83 ++++++++++++ CADability.Avalonia/PropertyPage.axaml | 6 + CADability.Avalonia/PropertyPage.axaml.cs | 128 ++++++++++++++++++ CADability/StringTableDeutsch.xml | 8 +- CADability/StringTableEnglish.xml | 10 +- 9 files changed, 247 insertions(+), 8 deletions(-) create mode 100644 CADability.Avalonia/PropertiesExplorer.axaml create mode 100644 CADability.Avalonia/PropertiesExplorer.axaml.cs create mode 100644 CADability.Avalonia/PropertyPage.axaml create mode 100644 CADability.Avalonia/PropertyPage.axaml.cs diff --git a/CADability.Avalonia/CadControl.axaml b/CADability.Avalonia/CadControl.axaml index 2b9d28cf..14ef72c4 100644 --- a/CADability.Avalonia/CadControl.axaml +++ b/CADability.Avalonia/CadControl.axaml @@ -2,5 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ca="using:CADability.Avalonia" x:Class="CADability.Avalonia.CadControl"> - + + + + diff --git a/CADability.Avalonia/CadControl.axaml.cs b/CADability.Avalonia/CadControl.axaml.cs index 031714a5..a05c1269 100644 --- a/CADability.Avalonia/CadControl.axaml.cs +++ b/CADability.Avalonia/CadControl.axaml.cs @@ -19,7 +19,7 @@ public CadControl() InitializeComponent(); // makes the cadCanvas and the propertiesExplorer // KeyPreview = true; // used to filter the escape key (and maybe some more?) // cadFrame = new CadFrame(propertiesExplorer, cadCanvas, this); - cadFrame = new CadFrame(cadCanvas, this); + cadFrame = new CadFrame(propertiesExplorer, cadCanvas, this); // cadFrame.ProgressAction = (show, percent, title) => { this.ProgressForm.ShowProgressBar(show, percent, title); }; cadCanvas.Frame = cadFrame; // propertiesExplorer.Frame = cadFrame; @@ -87,7 +87,7 @@ public CadControl() // return progressForm; // } // } - // public PropertiesExplorer PropertiesExplorer => propertiesExplorer; + public PropertiesExplorer PropertiesExplorer => propertiesExplorer; public CadCanvas CadCanvas => cadCanvas; public CadFrame CadFrame => cadFrame; diff --git a/CADability.Avalonia/CadFrame.cs b/CADability.Avalonia/CadFrame.cs index 2d1a107a..86c21e73 100644 --- a/CADability.Avalonia/CadFrame.cs +++ b/CADability.Avalonia/CadFrame.cs @@ -81,8 +81,8 @@ public class CadFrame : FrameImpl, IUIService // // TODO add propertyExplorer to parameters and add to base call // allows cadFrame.ControlCenter to work - public CadFrame(CadCanvas cadCanvas, ICommandHandler commandHandler) - : base(cadCanvas) + public CadFrame(PropertiesExplorer propertiesExplorer, CadCanvas cadCanvas, ICommandHandler commandHandler) + : base(propertiesExplorer, cadCanvas) { this.commandHandler = commandHandler; } diff --git a/CADability.Avalonia/PropertiesExplorer.axaml b/CADability.Avalonia/PropertiesExplorer.axaml new file mode 100644 index 00000000..7f6a6050 --- /dev/null +++ b/CADability.Avalonia/PropertiesExplorer.axaml @@ -0,0 +1,7 @@ + + + + diff --git a/CADability.Avalonia/PropertiesExplorer.axaml.cs b/CADability.Avalonia/PropertiesExplorer.axaml.cs new file mode 100644 index 00000000..64bfab07 --- /dev/null +++ b/CADability.Avalonia/PropertiesExplorer.axaml.cs @@ -0,0 +1,83 @@ +using Avalonia.Controls; +using CADability.Substitutes; +using CADability.UserInterface; +using System.Collections.Generic; + +namespace CADability.Avalonia +{ + + public partial class PropertiesExplorer: UserControl, IControlCenter + { + private Dictionary tabPages; + + public PropertiesExplorer() + { + InitializeComponent(); + tabPages = new Dictionary(); + } + + IPropertyPage IControlCenter.AddPropertyPage(string titleId, int iconId) + { + PropertyPage page = new PropertyPage(titleId, iconId, this); + tabPages[titleId] = page; + TabItem tab = new TabItem { + Header = StringTable.GetString(titleId + "TabPage", StringTable.Category.label), // TODO correct string + Content = page, + }; + tabControl.Items.Add(tab); + + return page; + } + + IPropertyPage IControlCenter.ActivePropertyPage => throw new System.NotImplementedException(); + + IFrame IControlCenter.Frame { get; set; } + + IPropertyPage IControlCenter.GetPropertyPage(string titleId) + { + PropertyPage ret; + tabPages.TryGetValue(titleId, out ret); + return ret; + } + + void IControlCenter.DisplayHelp(string helpID) + { + throw new System.NotImplementedException(); + } + + bool IControlCenter.ShowPropertyPage(string titleId) + { + if (tabPages.ContainsKey(titleId)) { + tabControl.SelectedItem = tabPages[titleId]; + return true; + } + return false; + } + + bool IControlCenter.RemovePropertyPage(string titleId) + { + if (tabPages.ContainsKey(titleId)) { + tabControl.Items.Remove(tabPages[titleId]); + tabPages.Remove(titleId); + return true; + } + return false; + } + + void IControlCenter.PreProcessKeyDown(KeyEventArgs e) + { + throw new System.NotImplementedException(); + } + + void IControlCenter.HideEntry(string entryId, bool hide) + { + throw new System.NotImplementedException(); + } + + IPropertyEntry IControlCenter.FindItem(string name) + { + throw new System.NotImplementedException(); + } + } + +} diff --git a/CADability.Avalonia/PropertyPage.axaml b/CADability.Avalonia/PropertyPage.axaml new file mode 100644 index 00000000..5f7c9ee7 --- /dev/null +++ b/CADability.Avalonia/PropertyPage.axaml @@ -0,0 +1,6 @@ + + + diff --git a/CADability.Avalonia/PropertyPage.axaml.cs b/CADability.Avalonia/PropertyPage.axaml.cs new file mode 100644 index 00000000..248db834 --- /dev/null +++ b/CADability.Avalonia/PropertyPage.axaml.cs @@ -0,0 +1,128 @@ +using Avalonia.Controls; +using CADability.UserInterface; + +namespace CADability.Avalonia +{ + public partial class PropertyPage : UserControl, IPropertyPage + { + private PropertiesExplorer propertiesExplorer; + + public PropertyPage(string titleId, int iconId, PropertiesExplorer propExplorer) + { + this.propertiesExplorer = propExplorer; + } + + IPropertyEntry IPropertyPage.Selected { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); } + + IFrame IPropertyPage.Frame => (propertiesExplorer as IControlCenter).Frame; + + IView IPropertyPage.ActiveView => throw new System.NotImplementedException(); + + event PreProcessKeyDown IPropertyPage.OnPreProcessKeyDown + { + add + { + throw new System.NotImplementedException(); + } + + remove + { + throw new System.NotImplementedException(); + } + } + + event SelectionChanged IPropertyPage.OnSelectionChanged + { + add + { + throw new System.NotImplementedException(); + } + + remove + { + throw new System.NotImplementedException(); + } + } + + void IPropertyPage.Add(IPropertyEntry toAdd, bool showOpen) + { + // throw new System.NotImplementedException(); + // TODO + } + + void IPropertyPage.BringToFront() + { + // throw new System.NotImplementedException(); + // TODO + } + + void IPropertyPage.Clear() + { + // throw new System.NotImplementedException(); + // TODO + } + + bool IPropertyPage.ContainsEntry(IPropertyEntry entryWithTextBox) + { + throw new System.NotImplementedException(); + } + + IPropertyEntry IPropertyPage.FindFromHelpLink(string helpResourceID, bool searchTreeAndOpen) + { + throw new System.NotImplementedException(); + } + + IPropertyEntry IPropertyPage.GetCurrentSelection() + { + throw new System.NotImplementedException(); + } + + IFrame IPropertyPage.GetFrame() => (this as IPropertyPage).Frame; + + IPropertyEntry IPropertyPage.GetParent(IShowProperty child) + { + throw new System.NotImplementedException(); + } + + bool IPropertyPage.IsOnTop() + { + throw new System.NotImplementedException(); + } + + bool IPropertyPage.IsOpen(IShowProperty toTest) + { + throw new System.NotImplementedException(); + } + + void IPropertyPage.MakeVisible(IPropertyEntry toShow) + { + throw new System.NotImplementedException(); + } + + void IPropertyPage.OpenSubEntries(IPropertyEntry toOpenOrClose, bool open) + { + throw new System.NotImplementedException(); + } + + void IPropertyPage.Refresh(IPropertyEntry toRefresh) + { + throw new System.NotImplementedException(); + } + + void IPropertyPage.Remove(IPropertyEntry toRemove) + { + // throw new System.NotImplementedException(); + // TODO + } + + void IPropertyPage.SelectEntry(IPropertyEntry toSelect) + { + throw new System.NotImplementedException(); + } + + void IPropertyPage.StartEditLabel(IPropertyEntry ToEdit) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/CADability/StringTableDeutsch.xml b/CADability/StringTableDeutsch.xml index eea1f3ab..d93aee4f 100644 --- a/CADability/StringTableDeutsch.xml +++ b/CADability/StringTableDeutsch.xml @@ -790,21 +790,27 @@ Größe der Hotspots zum Verändern der markierten Objekte (Skalieren, Drehen, Verschieben, Fixpunkt) + Informationen und Eingabemöglichkeiten für die gerade aktive Aktion (z.B. Markieren, Zeichnen, Verändern) + Anzeige und Änderungsmöglichkeit für die Vorgaben der projektbezogenen Systemeinstellungen + Anzeige und Änderungsmöglichkeit für die Vorgaben der globalen Systemeinstellungen + Einstellungen der Darstellung: Layout mit Ausschnitt, Modell mit Projektion, Zeichenebene. + Symbole zur Verwendung im Projekt + Modellierwerkzeuge zum Verändern der Geometrie eines Körpers @@ -6147,4 +6153,4 @@ Daten des regelmäßigen Polygons - \ No newline at end of file + diff --git a/CADability/StringTableEnglish.xml b/CADability/StringTableEnglish.xml index dd161a71..96c24df8 100644 --- a/CADability/StringTableEnglish.xml +++ b/CADability/StringTableEnglish.xml @@ -787,21 +787,27 @@ Size of the handles (hotspots) + Properties and input fields for the active action + Properties or settings for this project + Global properties and settings + Properties or settings for the different views of this project + Symbols available for use in the project + Modelling mode where you can change the geometry of a solid body @@ -6048,7 +6054,7 @@ ??? - + - \ No newline at end of file + From 47a26e6fa3226c308e66da1cba55ebd5149c9dac Mon Sep 17 00:00:00 2001 From: Jan Schopohl Date: Sat, 21 Feb 2026 20:45:58 +0100 Subject: [PATCH 11/12] Add Mouse and Keyboard handling --- CADability.Avalonia/CadCanvas.axaml.cs | 141 +++++++++++++++++- CADability.Avalonia/CadControl.axaml.cs | 66 +++++--- CADability.Avalonia/CadFrame.cs | 21 +-- CADability.Avalonia/PaintToOpenGL.cs | 38 ++--- .../PropertiesExplorer.axaml.cs | 19 ++- CADability.Avalonia/PropertyEntry.axaml | 6 + CADability.Avalonia/PropertyEntry.axaml.cs | 27 ++++ CADability.Avalonia/PropertyPage.axaml.cs | 55 +++---- ShapeIt/MainWindow.axaml.cs | 32 ++-- 9 files changed, 297 insertions(+), 108 deletions(-) create mode 100644 CADability.Avalonia/PropertyEntry.axaml create mode 100644 CADability.Avalonia/PropertyEntry.axaml.cs diff --git a/CADability.Avalonia/CadCanvas.axaml.cs b/CADability.Avalonia/CadCanvas.axaml.cs index a38d151a..84de944d 100644 --- a/CADability.Avalonia/CadCanvas.axaml.cs +++ b/CADability.Avalonia/CadCanvas.axaml.cs @@ -1,7 +1,8 @@ -using Avalonia; +using AvaloniaBase = Avalonia; using Avalonia.Controls; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Controls; +using AvaloniaKeyEventArgs = Avalonia.Input.KeyEventArgs; +using Avalonia.Input; +using Avalonia.Interactivity; using CADability.GeoObject; using CADability.Substitutes; using CADability.UserInterface; @@ -13,7 +14,7 @@ namespace CADability.Avalonia { public partial class CadCanvas: UserControl, ICanvas { - private static CADability.Substitutes.Rectangle Subst(Rect v) + private static CADability.Substitutes.Rectangle Subst(AvaloniaBase.Rect v) { return new Substitutes.Rectangle((int)v.X, (int)v.Y, (int)v.Width, (int)v.Height); } @@ -22,13 +23,18 @@ private static CADability.Substitutes.Rectangle Subst(Rect v) private IFrame frame; private IView view; private String currentCursor; + public Substitutes.Point CurrentMousePosition { get; private set; } + public Substitutes.Keys ModifierKeys { get; private set; } public CadCanvas() { InitializeComponent(); // CadCanvasControl canvasControl = new CadCanvasControl(); paintTo3D = new PaintToOpenGL(1e-6); + paintTo3D.CadCanvas = this; this.Content = paintTo3D; + this.CurrentMousePosition = new Substitutes.Point(0, 0); + this.ModifierKeys = Substitutes.Keys.None; } void ICanvas.Invalidate() {} @@ -49,11 +55,16 @@ IPaintTo3D ICanvas.PaintTo3D public event Action OnPaintDone; + public void OnPaint(PaintEventArgs e) + { + view.OnPaint(e); + OnPaintDone?.Invoke(this); + } + void ICanvas.ShowView(IView toShow) { view = toShow; // TODO init paintTo3D here or in constructor? - paintTo3D.View = view; // TODO view.Connect needed? view.Connect(this); } @@ -65,10 +76,13 @@ IView ICanvas.GetView() Substitutes.Point ICanvas.PointToClient(Substitutes.Point mousePosition) { - throw new NotImplementedException(); + // should already be client coordinates with avalonia + return mousePosition; } - void ICanvas.ShowContextMenu(MenuWithHandler[] contextMenu, Substitutes.Point viewPosition, System.Action collapsed) {} + void ICanvas.ShowContextMenu(MenuWithHandler[] contextMenu, Substitutes.Point viewPosition, System.Action collapsed) { + throw new NotImplementedException(); + } Substitutes.DragDropEffects ICanvas.DoDragDrop(GeoObjectList dragList, Substitutes.DragDropEffects all) { @@ -76,5 +90,118 @@ Substitutes.DragDropEffects ICanvas.DoDragDrop(GeoObjectList dragList, Substitut } void ICanvas.ShowToolTip(string toDisplay) {} + + protected override void OnLoaded(RoutedEventArgs e) + { + var topLevel = TopLevel.GetTopLevel(this)!; + topLevel.KeyDown += OnKeyDown; + topLevel.KeyUp += OnKeyUp; + topLevel.PointerPressed += OnPointerPressed; + topLevel.PointerReleased += OnPointerReleased; + topLevel.PointerEntered += OnPointerEntered; + topLevel.PointerExited += OnPointerExited; + topLevel.PointerMoved += OnPointerMoved; + topLevel.PointerWheelChanged += OnPointerWheel; + base.OnLoaded(e); + } + + private Substitutes.Point Subst(AvaloniaBase.Point mousePosition) + { + return new Substitutes.Point((int)mousePosition.X, (int)mousePosition.Y); + } + + private Substitutes.MouseButtons SubstButton(PointerEventArgs e) + { + if (e is PointerReleasedEventArgs) { + var mb = (e as PointerReleasedEventArgs)?.InitialPressMouseButton; + if (Enum.TryParse(mb.ToString(), out Substitutes.MouseButtons res)) return res; + + } else { + if (e.Properties.IsLeftButtonPressed) return Substitutes.MouseButtons.Left; + if (e.Properties.IsRightButtonPressed) return Substitutes.MouseButtons.Right; + if (e.Properties.IsMiddleButtonPressed) return Substitutes.MouseButtons.Middle; + if (e.Properties.IsXButton1Pressed) return Substitutes.MouseButtons.XButton1; + if (e.Properties.IsXButton2Pressed) return Substitutes.MouseButtons.XButton2; + } + return Substitutes.MouseButtons.None; + } + + private CADability.Substitutes.MouseEventArgs Subst(PointerEventArgs v) + { + int clicks = (v is PointerPressedEventArgs) ? (v as PointerPressedEventArgs).ClickCount : 0; + int delta = (v is PointerWheelEventArgs) ? (int)((v as PointerWheelEventArgs).Delta.Y*10) : 0; + return new Substitutes.MouseEventArgs() + { + Button = SubstButton(v), + Clicks = clicks, + X = (int)v.GetPosition(this).X, + Y = (int)v.GetPosition(this).Y, + Delta = delta, + Location = new Substitutes.Point((int)v.GetPosition(this).X, (int)v.GetPosition(this).Y) + }; + } + + private Substitutes.Keys Subst(KeyModifiers key) + { + if (key == KeyModifiers.Meta) { + return Keys.LWin; + } + // TODO check if this works with multiple modifiers + return Enum.TryParse(key.ToString(), out Substitutes.Keys res) ? res : Substitutes.Keys.None; + } + + private void OnKeyDown(object sender, AvaloniaKeyEventArgs e) + { + ModifierKeys = Subst(e.KeyModifiers); + base.OnKeyDown(e); + } + + private void OnKeyUp(object sender, AvaloniaKeyEventArgs e) + { + ModifierKeys = Subst(e.KeyModifiers); + base.OnKeyUp(e); + } + + private void OnPointerPressed(object sender, PointerPressedEventArgs e) + { + ModifierKeys = Subst(e.KeyModifiers); + view?.OnMouseDown(Subst(e)); + base.OnPointerPressed(e); + } + + private void OnPointerReleased(object sender, PointerReleasedEventArgs e) + { + ModifierKeys = Subst(e.KeyModifiers); + view?.OnMouseUp(Subst(e)); + base.OnPointerReleased(e); + } + + private void OnPointerEntered(object sender, PointerEventArgs e) + { + view?.OnMouseEnter(e); + base.OnPointerEntered(e); + } + + private void OnPointerExited(object sender, PointerEventArgs e) + { + view?.OnMouseLeave(e); + base.OnPointerEntered(e); + } + + private void OnPointerMoved(object sender, PointerEventArgs e) + { + ModifierKeys = Subst(e.KeyModifiers); + CurrentMousePosition = Subst(e.GetPosition(this)); + view?.OnMouseMove(Subst(e)); + base.OnPointerMoved(e); + } + + private void OnPointerWheel(object sender, PointerWheelEventArgs e) + { + ModifierKeys = Subst(e.KeyModifiers); + view?.OnMouseWheel(Subst(e)); + base.OnPointerWheelChanged(e); + } + // TODO more mouse handling } } diff --git a/CADability.Avalonia/CadControl.axaml.cs b/CADability.Avalonia/CadControl.axaml.cs index a05c1269..b5ec8a75 100644 --- a/CADability.Avalonia/CadControl.axaml.cs +++ b/CADability.Avalonia/CadControl.axaml.cs @@ -1,4 +1,6 @@ using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; using CADability.UserInterface; using System; using System.Collections.Generic; @@ -22,7 +24,7 @@ public CadControl() cadFrame = new CadFrame(propertiesExplorer, cadCanvas, this); // cadFrame.ProgressAction = (show, percent, title) => { this.ProgressForm.ShowProgressBar(show, percent, title); }; cadCanvas.Frame = cadFrame; - // propertiesExplorer.Frame = cadFrame; + propertiesExplorer.Frame = cadFrame; // show this menu in the MainForm // MenuWithHandler[] mainMenu = MenuResource.LoadMenuDefinition("SDI Menu", true, cadFrame); // MenuManager.MakeMainMenu(mainMenu, dockPanel, this); TODO moved to Main Window @@ -136,29 +138,45 @@ public CadControl() // base.OnFormClosed(e); // } - // TODO - // protected override bool ProcessCmdKey(ref Message msg, Keys keyData) - // { - // Keys nmKeyData = (Keys)((int)keyData & 0x0FFFF); - // bool preProcess = nmKeyData >= Keys.F1 && nmKeyData <= Keys.F24; - // preProcess = preProcess || (nmKeyData == Keys.Escape); - // preProcess = preProcess || (nmKeyData == Keys.Up) || (nmKeyData == Keys.Down); - // preProcess = preProcess || (nmKeyData == Keys.Tab) || (nmKeyData == Keys.Enter); - // preProcess = preProcess || keyData.HasFlag(Keys.Control) || keyData.HasFlag(Keys.Alt); // menu shortcut - // if (propertiesExplorer.EntryWithTextBox == null) preProcess |= (nmKeyData == Keys.Delete); // the delete key is preferred by the textbox, if there is one - // Substitutes.KeyEventArgs e = new Substitutes.KeyEventArgs((Substitutes.Keys)keyData); - // if (preProcess) - // { - // e.Handled = false; - // cadFrame.PreProcessKeyDown(e); - // if (e.Handled) return true; - // } - // CadFrame.PreProcessKeyDown(e); - // if (e.Handled) return true; - // //if (msg.Msg== 0x0101) // WM_KEYUP - // //{ } - // return base.ProcessCmdKey(ref msg, keyData); - // } + + protected override void OnLoaded(RoutedEventArgs e) + { + var topLevel = TopLevel.GetTopLevel(this)!; + topLevel.KeyDown += OnKeyDown; + base.OnLoaded(e); + } + + private Substitutes.Keys Subst(Key key) + { + return Enum.TryParse(key.ToString(), out Substitutes.Keys res) ? res : Substitutes.Keys.None; + } + + private void OnKeyDown(object sender, KeyEventArgs keyEvent) + { + Key key = keyEvent.Key; + bool preProcess = key >= Key.F1 && key <= Key.F24; + preProcess = preProcess || (key == Key.Escape); + preProcess = preProcess || (key == Key.Up) || (key == Key.Down); + preProcess = preProcess || (key == Key.Tab) || (key == Key.Enter); + preProcess = preProcess || keyEvent.KeyModifiers.HasFlag(KeyModifiers.Control) || keyEvent.KeyModifiers.HasFlag(KeyModifiers.Alt); // menu shortcut + if (propertiesExplorer.EntryWithTextBox == null) preProcess |= (key == Key.Delete); // the delete key is preferred by the textbox, if there is one + Substitutes.KeyEventArgs e = new Substitutes.KeyEventArgs(Subst(key)); + if (preProcess) + { + e.Handled = false; + cadFrame.PreProcessKeyDown(e); + if (e.Handled) { + keyEvent.Handled = true; + return; + } + } + CadFrame.PreProcessKeyDown(e); + if (e.Handled) { + keyEvent.Handled = true; + return; + } + base.OnKeyDown(keyEvent); + } public virtual bool OnCommand(string MenuId) { diff --git a/CADability.Avalonia/CadFrame.cs b/CADability.Avalonia/CadFrame.cs index 86c21e73..4cf33b27 100644 --- a/CADability.Avalonia/CadFrame.cs +++ b/CADability.Avalonia/CadFrame.cs @@ -1,3 +1,5 @@ +using AvaloniaBase = Avalonia; +using Avalonia.Controls; using CADability.GeoObject; using CADability.UserInterface; using System; @@ -22,6 +24,7 @@ public class CadFrame : FrameImpl, IUIService #region PRIVATE FIELDS private ICommandHandler commandHandler; + private CadCanvas cadCanvas; private const string ClipFormat = "CADability.GeoObjectList.Json"; @@ -79,12 +82,12 @@ public class CadFrame : FrameImpl, IUIService /// /// // - // TODO add propertyExplorer to parameters and add to base call // allows cadFrame.ControlCenter to work public CadFrame(PropertiesExplorer propertiesExplorer, CadCanvas cadCanvas, ICommandHandler commandHandler) : base(propertiesExplorer, cadCanvas) { this.commandHandler = commandHandler; + this.cadCanvas = cadCanvas; } #region FrameImpl override @@ -170,21 +173,9 @@ GeoObjectList IUIService.GetDataPresent(object data) // } // return null; // } - // Substitutes.Keys IUIService.ModifierKeys => (Substitutes.Keys)Control.ModifierKeys; - // Substitutes.Point IUIService.CurrentMousePosition => Subst(Control.MousePosition); - Substitutes.Keys IUIService.ModifierKeys - { - get { throw new NotImplementedException(); } - } - Substitutes.Point IUIService.CurrentMousePosition - { - get { throw new NotImplementedException(); } - } - private Substitutes.Point Subst(System.Drawing.Point mousePosition) - { - return new Substitutes.Point(mousePosition.X, mousePosition.Y); - } + Substitutes.Keys IUIService.ModifierKeys => cadCanvas.ModifierKeys; + Substitutes.Point IUIService.CurrentMousePosition => cadCanvas.CurrentMousePosition; private static Dictionary directories = new Dictionary(); // TODO reimplement in Avalonia diff --git a/CADability.Avalonia/PaintToOpenGL.cs b/CADability.Avalonia/PaintToOpenGL.cs index 49889a86..eb461032 100644 --- a/CADability.Avalonia/PaintToOpenGL.cs +++ b/CADability.Avalonia/PaintToOpenGL.cs @@ -30,7 +30,6 @@ class PaintToOpenGL : OpenGlControlBase, IPaintTo3D private int lightPositionLocation; private int ambientFactorLocation; - private IView _view; private GL _gl; // use Silk.NET gl interface, as Avalonia only has incomplete bindings private GlInterface _aGl; // Avalonia gl interface private Color _backgroundColor; @@ -56,6 +55,8 @@ class PaintToOpenGL : OpenGlControlBase, IPaintTo3D private Color selectColor; private PaintCapabilities capabilities; + public CadCanvas CadCanvas { get; set; } + public PaintToOpenGL(double precision = 1e-6) { this.precision = precision; @@ -136,15 +137,13 @@ protected override void OnOpenGlDeinit(GlInterface gl) protected override void OnOpenGlRender(GlInterface _, int fb) { - if (_view != null) { - Substitutes.PaintEventArgs paintEventArgs = new Substitutes.PaintEventArgs() - { - ClipRectangle = new Substitutes.Rectangle(0, 0, (int)Bounds.Width, (int)Bounds.Height), - // ClipRectangle = new Substitutes.Rectangle((int)Bounds.X, (int)Bounds.Y, (int)Bounds.Width, (int)Bounds.Height), - Graphics = null // TODO should be fine, is there a better solution? - }; - _view.OnPaint(paintEventArgs); - } + Substitutes.PaintEventArgs paintEventArgs = new Substitutes.PaintEventArgs() + { + ClipRectangle = new Substitutes.Rectangle(0, 0, (int)Bounds.Width, (int)Bounds.Height), + // ClipRectangle = new Substitutes.Rectangle((int)Bounds.X, (int)Bounds.Y, (int)Bounds.Width, (int)Bounds.Height), + Graphics = null // TODO should be fine, is there a better solution? + }; + CadCanvas?.OnPaint(paintEventArgs); GlCheckError(); Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); @@ -440,7 +439,8 @@ unsafe void IPaintTo3D.List(IPaintTo3DList paintThisList) } void IPaintTo3D.SelectedList(IPaintTo3DList paintThisList, int wobbleRadius) { - throw new NotImplementedException(); + // throw new NotImplementedException(); + (this as IPaintTo3D).List(paintThisList); // TODO select color } void IPaintTo3D.Nurbs(GeoPoint[] poles, double[] weights, double[] knots, int degree) { @@ -680,29 +680,29 @@ void IPaintTo3D.Dispose() } void IPaintTo3D.PushState() { - throw new NotImplementedException(); + // throw new NotImplementedException(); + // TODO do we have to do something here? new vao? } void IPaintTo3D.PopState() { - throw new NotImplementedException(); + // throw new NotImplementedException(); + // TODO do we have to do something here? new vao? } void IPaintTo3D.PushMultModOp(ModOp insertion) { - throw new NotImplementedException(); + // throw new NotImplementedException(); + // TODO do we have to do something here? new vao? } void IPaintTo3D.PopModOp() { - throw new NotImplementedException(); + // throw new NotImplementedException(); + // TODO do we have to do something here? new vao? } void IPaintTo3D.SetClip(Substitutes.Rectangle clipRectangle) { throw new NotImplementedException(); } - public IView View { - set => _view = value; - } - internal class VertexArrayObject : IPaintTo3DList { private string name; diff --git a/CADability.Avalonia/PropertiesExplorer.axaml.cs b/CADability.Avalonia/PropertiesExplorer.axaml.cs index 64bfab07..684e7eaf 100644 --- a/CADability.Avalonia/PropertiesExplorer.axaml.cs +++ b/CADability.Avalonia/PropertiesExplorer.axaml.cs @@ -29,9 +29,10 @@ IPropertyPage IControlCenter.AddPropertyPage(string titleId, int iconId) return page; } - IPropertyPage IControlCenter.ActivePropertyPage => throw new System.NotImplementedException(); + public IPropertyPage ActivePropertyPage => (tabControl.SelectedItem as PropertyPage); - IFrame IControlCenter.Frame { get; set; } + public IFrame Frame { get; set; } + public IPropertyEntry EntryWithTextBox { get; private set; } IPropertyPage IControlCenter.GetPropertyPage(string titleId) { @@ -66,7 +67,19 @@ bool IControlCenter.RemovePropertyPage(string titleId) void IControlCenter.PreProcessKeyDown(KeyEventArgs e) { - throw new System.NotImplementedException(); + switch (e.KeyData) { + // case Keys.Tab: // TODO use ctrl to switch tabs + // case Keys.Enter: + // if (EntryWithTextBox != null) + // { + // // TODO end editing of text box + // // EntryWithTextBox.EndEdit(false, textBox.Modified, textBox.Text); + // } + + } + if (!e.SuppressKeyPress) { + (ActivePropertyPage as PropertyPage)?.PreProcessKeyDown(e); + } } void IControlCenter.HideEntry(string entryId, bool hide) diff --git a/CADability.Avalonia/PropertyEntry.axaml b/CADability.Avalonia/PropertyEntry.axaml new file mode 100644 index 00000000..b43d537f --- /dev/null +++ b/CADability.Avalonia/PropertyEntry.axaml @@ -0,0 +1,6 @@ + + + diff --git a/CADability.Avalonia/PropertyEntry.axaml.cs b/CADability.Avalonia/PropertyEntry.axaml.cs new file mode 100644 index 00000000..406a7c7f --- /dev/null +++ b/CADability.Avalonia/PropertyEntry.axaml.cs @@ -0,0 +1,27 @@ +using Avalonia.Controls; +using CADability.UserInterface; + +namespace CADability.Avalonia +{ + public partial class PropertyEntry: UserControl + { + private IPropertyEntry prop; + + public PropertyEntry(IPropertyEntry prop) + { + InitializeComponent(); + this.prop = prop; + if (prop.Flags.HasFlag(PropertyEntryType.LabelEditable)) { + TextBox text = new TextBox { + Text = prop.Label, + }; + this.panel.Children.Add(text); + } else { + TextBlock text = new TextBlock() { + Text = prop.Label, + }; + this.panel.Children.Add(text); + } + } + } +} diff --git a/CADability.Avalonia/PropertyPage.axaml.cs b/CADability.Avalonia/PropertyPage.axaml.cs index 248db834..315f4c4c 100644 --- a/CADability.Avalonia/PropertyPage.axaml.cs +++ b/CADability.Avalonia/PropertyPage.axaml.cs @@ -1,15 +1,25 @@ using Avalonia.Controls; using CADability.UserInterface; +using System; +using System.Collections.Generic; namespace CADability.Avalonia { public partial class PropertyPage : UserControl, IPropertyPage { private PropertiesExplorer propertiesExplorer; + private string TitleId { get; } + private Dictionary properties; + + public event PreProcessKeyDown OnPreProcessKeyDown; + public event SelectionChanged OnSelectionChanged; public PropertyPage(string titleId, int iconId, PropertiesExplorer propExplorer) { + InitializeComponent(); this.propertiesExplorer = propExplorer; + this.TitleId = titleId; + properties = new Dictionary(); } IPropertyEntry IPropertyPage.Selected { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); } @@ -18,48 +28,29 @@ public PropertyPage(string titleId, int iconId, PropertiesExplorer propExplorer) IView IPropertyPage.ActiveView => throw new System.NotImplementedException(); - event PreProcessKeyDown IPropertyPage.OnPreProcessKeyDown + public void PreProcessKeyDown(Substitutes.KeyEventArgs e) { - add - { - throw new System.NotImplementedException(); - } - - remove - { - throw new System.NotImplementedException(); - } - } - - event SelectionChanged IPropertyPage.OnSelectionChanged - { - add - { - throw new System.NotImplementedException(); - } - - remove - { - throw new System.NotImplementedException(); - } + OnPreProcessKeyDown?.Invoke(e); } void IPropertyPage.Add(IPropertyEntry toAdd, bool showOpen) { - // throw new System.NotImplementedException(); - // TODO + Console.WriteLine("Label: " + toAdd.Label + " Index: " + toAdd.Index); + PropertyEntry prop = new PropertyEntry(toAdd); + properties[toAdd] = prop; + panel.Children.Add(prop); + // TODO select if showOpen } void IPropertyPage.BringToFront() { - // throw new System.NotImplementedException(); - // TODO + (propertiesExplorer as IControlCenter).ShowPropertyPage(this.TitleId); } void IPropertyPage.Clear() { - // throw new System.NotImplementedException(); - // TODO + panel.Children.Clear(); + properties.Clear(); } bool IPropertyPage.ContainsEntry(IPropertyEntry entryWithTextBox) @@ -111,8 +102,10 @@ void IPropertyPage.Refresh(IPropertyEntry toRefresh) void IPropertyPage.Remove(IPropertyEntry toRemove) { - // throw new System.NotImplementedException(); - // TODO + if (properties.ContainsKey(toRemove)) { + PropertyEntry res = properties[toRemove]; + panel.Children.Remove(res); + } } void IPropertyPage.SelectEntry(IPropertyEntry toSelect) diff --git a/ShapeIt/MainWindow.axaml.cs b/ShapeIt/MainWindow.axaml.cs index a62081cc..cca23523 100644 --- a/ShapeIt/MainWindow.axaml.cs +++ b/ShapeIt/MainWindow.axaml.cs @@ -1,4 +1,5 @@ using Avalonia.Controls; +using Avalonia.Input; using CADability.Avalonia; using CADability; using CADability.Actions; @@ -161,6 +162,7 @@ private string ReadEmbeddedVersion() public bool OnCommand(string menuId) { Console.WriteLine("MainWindow.OnCommand(" + menuId + ")"); + if (modellingPropertyEntries.OnCommand(menuId)) return true; if (menuId == "MenuId.App.Exit") { Close(); } @@ -168,10 +170,11 @@ public bool OnCommand(string menuId) } public bool OnUpdateCommand(string menuId, CommandState commandState) { - return false; + throw new NotImplementedException(); } public void OnSelected(MenuWithHandler selectedMenu, bool selected) { + throw new NotImplementedException(); } // delegate to cadControl (old: cadForm) @@ -183,7 +186,6 @@ public MainWindow(string[] args) // TODO inherit from CadForm? : base(args) { // interpret the command line arguments as a name of a file, which should be opened InitializeComponent(); - // cadFrame = new CadFrame(cadCanvas, this); // ShowLogo(); TODO // this.Icon = Properties.Resources.Icon; Assembly ThisAssembly = Assembly.GetExecutingAssembly(); @@ -233,6 +235,7 @@ public MainWindow(string[] args) // TODO inherit from CadForm? : base(args) Settings.GlobalSettings.SetValue("Construct.3D_Delete2DBase", false); bool exp = Settings.GlobalSettings.GetBoolValue("Experimental.TestNewContextMenu", false); bool tst = Settings.GlobalSettings.GetBoolValue("ShapeIt.Initialized", false); + // TODO do we need to load colorSettings here? Settings.GlobalSettings.SetValue("ShapeIt.Initialized", true); CadFrame.FileNameChangedEvent += (name) => { @@ -245,7 +248,7 @@ public MainWindow(string[] args) // TODO inherit from CadForm? : base(args) // CadFrame.UIService.ApplicationIdle += OnIdle; TODO CadFrame.ViewsChangedEvent += OnViewsChanged; if (CadFrame.ActiveView != null) OnViewsChanged(CadFrame); - // CadFrame.ControlCenter.RemovePropertyPage("View"); TODO + CadFrame.ControlCenter.RemovePropertyPage("View"); using (str = ThisAssembly.GetManifestResourceStream("ShapeIt.StringTableDeutsch.xml")) { XmlDocument stringXmlDocument = new XmlDocument(); @@ -317,10 +320,10 @@ public MainWindow(string[] args) // TODO inherit from CadForm? : base(args) }; // the following installs the property page for modelling. This connects all modelling // tasks of ShapeIt with CADability - // IPropertyPage modellingPropPage = CadFrame.ControlCenter.AddPropertyPage("Modelling", 6); TODO - // modellingPropertyEntries = new ModellingPropertyEntries(CadFrame); TODO currently crashes - // modellingPropPage.Add(modellingPropertyEntries, false); TODO implement properties explorer implementing IControlCenter - // CadFrame.ControlCenter.ShowPropertyPage("Modelling"); TODO + IPropertyPage modellingPropPage = CadFrame.ControlCenter.AddPropertyPage("Modelling", 6); + modellingPropertyEntries = new ModellingPropertyEntries(CadFrame); + modellingPropPage.Add(modellingPropertyEntries, false); + CadFrame.ControlCenter.ShowPropertyPage("Modelling"); } private void OnViewsChanged(IFrame theFrame) @@ -331,7 +334,8 @@ private void OnViewsChanged(IFrame theFrame) private void OnProjectionChanged(Projection sender, EventArgs args) { - projectionChanged = true; + // projectionChanged = true; + modellingPropertyEntries.OnProjectionChanged(); // TODO move to OnIdle? } // // TODO reimplement in Avalonia @@ -504,7 +508,6 @@ private void AutoDebug() /// /// /// -// // TODO reimplement in Avalonia // protected override bool ProcessCmdKey(ref Message msg, Keys keyData) // { // Keys nmKeyData = (Keys)((int)keyData & 0x0FFFF); @@ -515,6 +518,17 @@ private void AutoDebug() // } // return base.ProcessCmdKey(ref msg, keyData); // } + protected override void OnKeyDown(KeyEventArgs keyEvent) + { + Console.WriteLine("Process Key: " + keyEvent.Key); + if (keyEvent.Key == Key.Escape) { + if (modellingPropertyEntries.OnEscape()) { + keyEvent.Handled = true; + return; + } + } + base.OnKeyDown(keyEvent); + } /// /// Called when CADability is idle. We use it to save the current project data to a temp file in case of a crash /// From 05b8347dde03c908df0d1a562aa45fde7676f03d Mon Sep 17 00:00:00 2001 From: Jan Schopohl Date: Mon, 23 Feb 2026 23:18:50 +0100 Subject: [PATCH 12/12] Add basic properties to proterty pages, still WIP --- CADability.Avalonia/CadControl.axaml.cs | 3 +- CADability.Avalonia/PropertyEntry.axaml | 5 +- CADability.Avalonia/PropertyEntry.axaml.cs | 61 ++++++++++++++++++---- CADability.Avalonia/PropertyPage.axaml.cs | 34 +++++++++--- 4 files changed, 84 insertions(+), 19 deletions(-) diff --git a/CADability.Avalonia/CadControl.axaml.cs b/CADability.Avalonia/CadControl.axaml.cs index b5ec8a75..a39af0c7 100644 --- a/CADability.Avalonia/CadControl.axaml.cs +++ b/CADability.Avalonia/CadControl.axaml.cs @@ -19,12 +19,13 @@ public partial class CadControl : UserControl, ICommandHandler public CadControl() { InitializeComponent(); // makes the cadCanvas and the propertiesExplorer + propertiesExplorer.Frame = cadFrame; + // KeyPreview = true; // used to filter the escape key (and maybe some more?) // cadFrame = new CadFrame(propertiesExplorer, cadCanvas, this); cadFrame = new CadFrame(propertiesExplorer, cadCanvas, this); // cadFrame.ProgressAction = (show, percent, title) => { this.ProgressForm.ShowProgressBar(show, percent, title); }; cadCanvas.Frame = cadFrame; - propertiesExplorer.Frame = cadFrame; // show this menu in the MainForm // MenuWithHandler[] mainMenu = MenuResource.LoadMenuDefinition("SDI Menu", true, cadFrame); // MenuManager.MakeMainMenu(mainMenu, dockPanel, this); TODO moved to Main Window diff --git a/CADability.Avalonia/PropertyEntry.axaml b/CADability.Avalonia/PropertyEntry.axaml index b43d537f..9b0f5d62 100644 --- a/CADability.Avalonia/PropertyEntry.axaml +++ b/CADability.Avalonia/PropertyEntry.axaml @@ -2,5 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:ca="using:CADability.Avalonia" x:Class="CADability.Avalonia.PropertyEntry"> - + + + + diff --git a/CADability.Avalonia/PropertyEntry.axaml.cs b/CADability.Avalonia/PropertyEntry.axaml.cs index 406a7c7f..73568b73 100644 --- a/CADability.Avalonia/PropertyEntry.axaml.cs +++ b/CADability.Avalonia/PropertyEntry.axaml.cs @@ -1,27 +1,70 @@ using Avalonia.Controls; +using Avalonia.Layout; +using GridPanel = Avalonia.Controls.Grid; using CADability.UserInterface; namespace CADability.Avalonia { public partial class PropertyEntry: UserControl { - private IPropertyEntry prop; + private IPropertyPage parent; + private bool showOpen; + public IPropertyEntry Prop { get; private set; } - public PropertyEntry(IPropertyEntry prop) + public PropertyEntry(IPropertyEntry prop, IPropertyPage parent, bool showOpen) { InitializeComponent(); - this.prop = prop; - if (prop.Flags.HasFlag(PropertyEntryType.LabelEditable)) { - TextBox text = new TextBox { + this.Prop = prop; + this.parent = parent; + this.showOpen = showOpen; + (parent as PropertyPage).AddToHash(this); + createProp(prop); + if (showOpen) { + createSubEntries(prop); + } + } + + private void createProp(IPropertyEntry prop) + { + prop.Added(parent); + prop.Parent = parent; + TextBlock text = new TextBlock() { + Text = prop.Label, + [GridPanel.ColumnProperty] = 0, + }; + this.panel.Children.Add(text); + + if (prop.Flags.HasFlag(PropertyEntryType.ValueEditable)) { + TextBox value = new TextBox { Text = prop.Label, + HorizontalAlignment = HorizontalAlignment.Right, + [GridPanel.ColumnProperty] = 1, }; - this.panel.Children.Add(text); + this.panel.Children.Add(value); } else { - TextBlock text = new TextBlock() { - Text = prop.Label, + TextBlock value = new TextBlock() { + Text = prop.Value, + HorizontalAlignment = HorizontalAlignment.Right, + [GridPanel.ColumnProperty] = 1, }; - this.panel.Children.Add(text); + this.panel.Children.Add(value); + } + + } + + private void createSubEntries(IPropertyEntry prop) + { + if (prop.SubItems == null) return; + foreach (IPropertyEntry subProp in prop.SubItems) { + PropertyEntry subEntry = new PropertyEntry(subProp, parent, subProp.IsOpen); + this.childPanel.Children.Add(subEntry); } } + + public void Refresh() + { + // TODO + // check if subproperties have been opened? + } } } diff --git a/CADability.Avalonia/PropertyPage.axaml.cs b/CADability.Avalonia/PropertyPage.axaml.cs index 315f4c4c..9abc2d0f 100644 --- a/CADability.Avalonia/PropertyPage.axaml.cs +++ b/CADability.Avalonia/PropertyPage.axaml.cs @@ -24,22 +24,26 @@ public PropertyPage(string titleId, int iconId, PropertiesExplorer propExplorer) IPropertyEntry IPropertyPage.Selected { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); } - IFrame IPropertyPage.Frame => (propertiesExplorer as IControlCenter).Frame; + public IFrame Frame => (propertiesExplorer as IControlCenter).Frame; - IView IPropertyPage.ActiveView => throw new System.NotImplementedException(); + public IView ActiveView => Frame.ActiveView; public void PreProcessKeyDown(Substitutes.KeyEventArgs e) { OnPreProcessKeyDown?.Invoke(e); } + public void AddToHash(PropertyEntry toAdd) + { + properties[toAdd.Prop] = toAdd; + } + void IPropertyPage.Add(IPropertyEntry toAdd, bool showOpen) { Console.WriteLine("Label: " + toAdd.Label + " Index: " + toAdd.Index); - PropertyEntry prop = new PropertyEntry(toAdd); - properties[toAdd] = prop; + + PropertyEntry prop = new PropertyEntry(toAdd, this, showOpen); panel.Children.Add(prop); - // TODO select if showOpen } void IPropertyPage.BringToFront() @@ -65,7 +69,8 @@ IPropertyEntry IPropertyPage.FindFromHelpLink(string helpResourceID, bool search IPropertyEntry IPropertyPage.GetCurrentSelection() { - throw new System.NotImplementedException(); + // throw new System.NotImplementedException(); + return null; // TODO } IFrame IPropertyPage.GetFrame() => (this as IPropertyPage).Frame; @@ -97,20 +102,33 @@ void IPropertyPage.OpenSubEntries(IPropertyEntry toOpenOrClose, bool open) void IPropertyPage.Refresh(IPropertyEntry toRefresh) { - throw new System.NotImplementedException(); + if (!properties.ContainsKey(toRefresh)) return; + + properties[toRefresh].Refresh(); + // PropertyEntry oldProp = properties[toRefresh]; + // // For now, just recreate it. Can be improved later. + // PropertyEntry prop = new PropertyEntry(toRefresh, this, toRefresh.IsOpen); + // properties[toRefresh] = prop; + // panel.Children.Insert(panel.Children.IndexOf(oldProp), prop); + // panel.Children.Remove(oldProp); } void IPropertyPage.Remove(IPropertyEntry toRemove) { if (properties.ContainsKey(toRemove)) { PropertyEntry res = properties[toRemove]; + if (!panel.Children.Contains(res)) { + // TODO recursive removal + throw new NotImplementedException(); + } panel.Children.Remove(res); } } void IPropertyPage.SelectEntry(IPropertyEntry toSelect) { - throw new System.NotImplementedException(); + // throw new System.NotImplementedException(); + // TODO focus that entry } void IPropertyPage.StartEditLabel(IPropertyEntry ToEdit)