/// <summary> /// Checks if the node is in the middle of two lines that are moving in the same /// vertical direction. /// /// This is done for edge case detection in _CleanProjectIntersections(), where vertical /// "elbows" can cause. /// </summary> /// <param name="bn">The node to check for vertical "elbowness."</param> /// <returns>If true, the point at bn is the corner of a formed elbow - i.e., where the /// previous segment will travel vertical in one direction, and then afterwards the node /// the segment travels in the opposite vertical direction. /// </returns> private static bool _IsVerticalPoint(BNode bn) { // There's a small optimization we can do by just calculating the Y // instead of vectors. Vector2 ptPrev, ptNext; BNode.PathBridge pbNext = bn.GetPathBridgeInfo(); BNode.PathBridge pbPrev = bn.prev.GetPathBridgeInfo(); if (pbNext.pathType == BNode.PathType.Line) { ptNext = bn.next.Pos - bn.Pos; } else { float a, b, c, d; Utils.GetBezierDerivativeWeights(0.0f, out a, out b, out c, out d); Vector2 pt0 = bn.prev.Pos; Vector2 pt1 = bn.prev.Pos + pbNext.prevTanOut; Vector2 pt2 = bn.Pos + pbNext.nextTanIn; Vector2 pt3 = bn.Pos; ptNext = a * pt0 + b * pt1 + c * pt2 + d * pt2; if (ptNext.y == 0.0f) { float lroot = 1.0f; float ra, rb; int r = Utils.GetRoots1DCubic(pt0.y, pt1.y, pt2.y, pt3.y, out ra, out rb); for (int i = 0; i < r; ++i) { if (i == 0) { lroot = Mathf.Min(ra, lroot); } else if (i == 1) { lroot = Mathf.Min(rb, lroot); } } Utils.GetBezierWeights(lroot, out a, out b, out c, out d); Vector2 rpt = a * pt0 + b * pt1 + c * pt2 + d * pt3; ptNext = rpt - bn.Pos; } } if (bn.prev.IsLine() == true) { ptPrev = bn.Pos - bn.prev.Pos; } else { float a, b, c, d; Utils.GetBezierDerivativeWeights(1.0f, out a, out b, out c, out d); Vector2 pt0 = bn.prev.Pos; Vector2 pt1 = bn.prev.Pos + pbPrev.prevTanOut; Vector2 pt2 = bn.Pos + pbPrev.nextTanIn; Vector2 pt3 = bn.Pos; ptPrev = a * pt0 + b * pt1 + c * pt2 + d * pt3; if (ptPrev.y == 0.0f) { float lroot = 0.0f; float ra, rb; int r = Utils.GetRoots1DCubic(pt0.y, pt1.y, pt2.y, pt3.y, out ra, out rb); for (int i = 0; i < r; ++i) { if (i == 0) { lroot = Mathf.Max(ra, lroot); } else if (i == 1) { lroot = Mathf.Max(rb, lroot); } } Utils.GetBezierWeights(lroot, out a, out b, out c, out d); Vector2 rpt = a * pt0 + b * pt1 + c * pt2 + d * pt3; ptPrev = bn.Pos - rpt; } } if (ptPrev.y == 0.0f || ptNext.y == 0.0f) { return(false); } return(Mathf.Sign(ptPrev.y) != Mathf.Sign(ptNext.y)); }
/// <summary> /// Subdivide a child node into multiple parts. /// </summary> /// <remarks>Not reliable, to be replaced later with De Casteljau's algorithm.</remarks> /// <param name="targ">The node path to subdivide.</param> /// <param name="lambda">The interpolation location between (0.0, 1.0) to subdivide.</param> /// <returns>The node create during the subdivision process. It will be connected right /// after the targ node. If targ does not have a next node, it does not represent a segment /// and cannot be subdivided; returning in a null return.</returns> public BNode Subdivide(BNode targ, float lambda = 0.5f) { if (targ.parent != this) { return(null); } BNode.PathBridge pb = targ.GetPathBridgeInfo(); if (pb.pathType == BNode.PathType.None) { return(null); } BNode bn = null; if (pb.pathType == BNode.PathType.Line) { bn = new BNode( this, Vector2.Lerp(targ.Pos, targ.next.Pos, lambda)); bn.UseTanIn = false; bn.UseTanOut = false; } else if (pb.pathType == BNode.PathType.BezierCurve) { BNode.SubdivideInfo sdi = targ.GetSubdivideInfo(lambda); bn = new BNode( this, sdi.subPos, sdi.subIn, sdi.subOut); targ.next.SetTangentDisconnected(); targ.SetTangentDisconnected(); bn.UseTanIn = true; bn.UseTanOut = true; targ.TanOut = sdi.prevOut; targ.next.TanIn = sdi.nextIn; } if (bn != null) { bn.next = targ.next; bn.prev = targ; bn.next.prev = bn; bn.prev.next = bn; // this.nodes.Add(bn); bn.FlagDirty(); targ.FlagDirty(); return(bn); } return(null); }
/// <summary> /// Given a set of collisions, convert them into a datastructure better suited for /// reflow boolean operations. /// </summary> /// <param name="collisions">The set of collision information to reorganize.</param> /// <returns>The collision information, reorganized into a form better suited for /// reflow boolean operations.</returns> public static Dictionary <Utils.NodeTPos, BNode.SubdivideInfo> SliceCollisionInfo(List <Utils.BezierSubdivSample> collisions) { Dictionary <Utils.NodeTPos, BNode.SubdivideInfo> ret = new Dictionary <Utils.NodeTPos, BNode.SubdivideInfo>(); Dictionary <BNode, HashSet <float> > subdivLocs = new Dictionary <BNode, HashSet <float> >(); // Get all the unique subdivision locations for both parts of // each collision. foreach (Utils.BezierSubdivSample bss in collisions) { HashSet <float> hsA; if (subdivLocs.TryGetValue(bss.a.node, out hsA) == false) { hsA = new HashSet <float>(); subdivLocs.Add(bss.a.node, hsA); } hsA.Add(bss.a.lEst); HashSet <float> hsB; if (subdivLocs.TryGetValue(bss.b.node, out hsB) == false) { hsB = new HashSet <float>(); subdivLocs.Add(bss.b.node, hsB); } hsB.Add(bss.b.lEst); } foreach (KeyValuePair <BNode, HashSet <float> > kvp in subdivLocs) { BNode node = kvp.Key; List <float> subs = new List <float>(kvp.Value); subs.Sort(); if (node.UseTanOut == false && node.next.UseTanIn == false) { // The scale isn't useful, but the direction is, for // winding purposes later. Vector2 outTan = (node.next.Pos - node.Pos); for (int i = 0; i < subs.Count; ++i) { // Linear subdivide, the easiest to do Vector2 loc = Vector2.Lerp(node.Pos, node.next.Pos, subs[i]); BNode.SubdivideInfo si = new BNode.SubdivideInfo(); // The tangents aren't really relevant for shaping the path, but // can be useful for calculating windings when making decisions for // boolean operations. si.prevOut = outTan; si.nextIn = -outTan; si.subPos = loc; si.subOut = outTan; si.subIn = -outTan; si.windTangent = outTan; // ret.Add(new Utils.NodeTPos(node, subs[i]), si); } } else { float lm = 0.0f; BNode.PathBridge pb = node.GetPathBridgeInfo(); Vector2 pt0 = node.Pos; Vector2 pt1 = node.Pos + pb.prevTanOut; Vector2 pt2 = node.next.Pos + pb.nextTanIn; Vector2 pt3 = node.next.Pos; List <Vector2> subSpots = new List <Vector2>(); // Breaking the cubic Bezier down into multiple parts is going to be // quite a bit more difficult because every subdivision changes the // curvature between tangent neighbors - so we have to incrementally // crawl and update tangents with respect to recent changes we're making. subSpots.Add(pt0); for (int i = 0; i < subs.Count; ++i) { float curT = subs[i]; float realT = (curT - lm) / (1.0f - lm); Vector2 p00 = Vector2.Lerp(pt0, pt1, realT); Vector2 p01 = Vector2.Lerp(pt1, pt2, realT); Vector2 p02 = Vector2.Lerp(pt2, pt3, realT); // Vector2 p10 = Vector2.Lerp(p00, p01, realT); Vector2 p11 = Vector2.Lerp(p01, p02, realT); // Vector2 npos = Vector2.Lerp(p10, p11, realT); // Record some important parts of the tangent, we're focused on what's // before the point, because what comes after could still be subject // to modification. subSpots.Add(p00); subSpots.Add(p10); subSpots.Add(npos); // And update our info for iteration. lm = curT; pt0 = npos; pt1 = p11; pt2 = p02; } subSpots.Add(pt1); subSpots.Add(pt2); subSpots.Add(pt3); for (int i = 0; i < subs.Count; ++i) { int idx = 3 + i * 3; BNode.SubdivideInfo si = new BNode.SubdivideInfo(); si.subPos = subSpots[idx]; si.subIn = subSpots[idx - 1] - si.subPos; si.subOut = subSpots[idx + 1] - si.subPos; si.prevOut = subSpots[idx - 2] - subSpots[idx - 3]; si.nextIn = subSpots[idx + 2] - subSpots[idx + 3]; si.windTangent = si.subOut; ret.Add(new Utils.NodeTPos(node, subs[i]), si); } } } return(ret); }
/// <summary> /// Unity inspector function. /// </summary> public override void OnInspectorGUI() { if (drawKnots == false) { this.selectedNodes.Clear(); } base.OnInspectorGUI(); drawKnots = EditorGUILayout.Toggle("Draw Knots", drawKnots); BernyTest t = (BernyTest)this.target; if (t == null || t.curveDocument == null) { return; } this.showCurveIDs = GUILayout.Toggle(this.showCurveIDs, "Show Curve IDs"); if (GUILayout.Button("Test Validity") == true) { t.curveDocument.TestValidity(); } if (GUILayout.Button("Fill") == true) { t.UpdateFillsForAll(BernyTest.FillType.Filled, this.strokeWidth); } if (GUILayout.Button("Fill Outline") == true) { t.UpdateFillsForAll(BernyTest.FillType.Outlined, this.strokeWidth); } if (GUILayout.Button("Fill Outlined") == true) { t.UpdateFillsForAll(BernyTest.FillType.FilledAndOutlined, this.strokeWidth); } this.strokeWidth = EditorGUILayout.Slider("Stroke Width", this.strokeWidth, 0.001f, 1.0f); GUILayout.BeginHorizontal(); GUI.color = Color.green; if (GUILayout.Button("Select All") == true) { this.selectedNodes = new HashSet <BNode>(t.curveDocument.EnumerateNodes()); } GUI.color = Color.white; if (GUILayout.Button("Deselect All") == true) { this.selectedNodes.Clear(); } GUILayout.EndHorizontal(); if (GUILayout.Button("Scan Selected Intersections") == true) { this.ScanSelectedIntersections(t.curveDocument); } GUILayout.Space(20.0f); string [] files = new string[] { "Ven", "TriVen", "CircAnCirc", "Complex", "Complex2", "Edges", "ShapeCircle", "ShapeEllipse", "ShapeLine", "ShapePolygon", "ShapePolyline", "ShapeRect" }; foreach (string f in files) { GUILayout.BeginHorizontal(); if (GUILayout.Button("LOAD " + f) == true) { t.curveDocument.Clear(); SVGSerializer.Load("TestSamples/" + f + ".svg", t.curveDocument, true); t.curveDocument.FlushDirty(); } if (GUILayout.Button("...", GUILayout.Width(30.0f)) == true) { System.Diagnostics.Process.Start("TestSamples\\" + f + ".svg"); } GUILayout.EndHorizontal(); } GUILayout.Space(20.0f); this.infAmt = EditorGUILayout.FloatField("Inflation Amt", this.infAmt); if (GUILayout.Button("Inflate") == true) { foreach (Layer l in t.curveDocument.Layers()) { foreach (BShape bs in l.shapes) { foreach (BLoop bl in bs.loops) { bl.Deinflect(); bl.Inflate(this.infAmt); } } } } if (GUILayout.Button("Edgeify") == true) { foreach (BLoop bl in t.curveDocument.EnumerateLoops()) { bl.Deinflect(); PxPre.Berny.Operators.Edgify(bl, this.infAmt); } } GUILayout.Space(20.0f); GUI.color = Color.red; if (GUILayout.Button("Clear") == true) { t.curveDocument.Clear(); t.ClearFills(); } GUI.color = Color.white; if (GUILayout.Button("Save SVG") == true) { SVGSerializer.Save("TestSave.svg", t.curveDocument); } if (GUILayout.Button("Load SVG") == true) { t.curveDocument.Clear(); SVGSerializer.Load("TestSave.svg", t.curveDocument); } GUILayout.Space(20.0f); if (this.selectedNodes.Count > 0) { if (GUILayout.Button("Reverse Windings") == true) { HashSet <BLoop> loops = new HashSet <BLoop>(); foreach (BNode bn in this.selectedNodes) { loops.Add(bn.parent); } foreach (BLoop loop in loops) { if (loop == null) { continue; } loop.Reverse(); } } GUI.color = Color.red; GUILayout.BeginHorizontal(); if (GUILayout.Button("Delete Selected") == true) { foreach (BNode selNode in this.selectedNodes) { selNode.parent.RemoveNode(selNode); } this.selectedNodes.Clear(); this.movedTangent = null; } if (GUILayout.Button("Disconnect Selected") == true) { foreach (BNode selNode in this.selectedNodes) { selNode.Disconnect(true); } } if (GUILayout.Button("Detach Selected") == true) { foreach (BNode selNode in this.selectedNodes) { selNode.Detach(); } } GUILayout.EndHorizontal(); GUI.color = Color.white; if (GUILayout.Button("Expand Islands") == true) { HashSet <BNode> toScan = new HashSet <BNode>(this.selectedNodes); while (toScan.Count > 0) { BNode bnCut = Utils.GetFirstInHash(toScan); foreach (BNode bnT in bnCut.Travel()) { toScan.Remove(bnT); this.selectedNodes.Add(bnT); } } } if (GUILayout.Button("Connect") == true) { if (this.selectedNodes.Count != 2) { return; } List <BNode> selList = new List <BNode>(this.selectedNodes); selList[0].parent.ConnectNodes(selList[0], selList[1]); } if (GUILayout.Button("Subdivide Selected") == true) { HashSet <BNode> subbed = new HashSet <BNode>(); foreach (BNode selNode in this.selectedNodes) { BNode bsubed = selNode.Subdivide(0.5f); if (bsubed != null) { subbed.Add(bsubed); } } if (subbed.Count > 0) { foreach (BNode bn in subbed) { this.selectedNodes.Add(bn); } this.RecalculateSelectionCentroid(); } } GUILayout.BeginHorizontal(); if (GUILayout.Button("Round Selected") == true) { foreach (BNode selNode in this.selectedNodes) { selNode.Round(); } } if (GUILayout.Button("Smooth") == true) { foreach (BNode selNode in this.selectedNodes) { selNode.SetTangentSmooth(); } } if (GUILayout.Button("Symmetrize") == true) { foreach (BNode selNode in this.selectedNodes) { selNode.SetTangentsSymmetry(); } } if (GUILayout.Button("Disconnect") == true) { foreach (BNode selNode in this.selectedNodes) { selNode.SetTangentDisconnected(); } } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); if (GUILayout.Button("Enable Inputs") == true) { foreach (BNode selNode in this.selectedNodes) { selNode.UseTanIn = true; } } if (GUILayout.Button("Enable Outputs") == true) { foreach (BNode selNode in this.selectedNodes) { selNode.UseTanOut = true; } } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); if (GUILayout.Button("Disable Inputs") == true) { foreach (BNode selNode in this.selectedNodes) { selNode.UseTanIn = false; } } if (GUILayout.Button("Disable Outputs") == true) { foreach (BNode selNode in this.selectedNodes) { selNode.UseTanOut = false; } } GUILayout.EndHorizontal(); if (GUILayout.Button("Islands") == true) { HashSet <BLoop> foundLoops = new HashSet <BLoop>(); foreach (BNode bn in this.selectedNodes) { foundLoops.Add(bn.parent); } foreach (BLoop bl in foundLoops) { int island = bl.CalculateIslands(); for (int i = 0; i < island - 1; ++i) { bl.ExtractIsland(bl.nodes[0]); } } } GUILayout.BeginHorizontal(); if (GUILayout.Button("Winding Simple") == true) { if (this.selectedNodes.Count > 0) { BNode bn = Utils.GetFirstInHash(this.selectedNodes); float w = bn.parent.CalculateWindingSimple(bn, true); Debug.Log("Simple winding of " + w.ToString()); } } if (GUILayout.Button("Winding Samples") == true) { BNode bn = Utils.GetFirstInHash(this.selectedNodes); float w = bn.parent.CalculateWindingSamples(bn, true); Debug.Log("Simple winding of " + w.ToString()); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); if (GUILayout.Button("Calculate ArcLength") == true) { HashSet <BLoop> loops = new HashSet <BLoop>(); foreach (BNode bn in this.selectedNodes) { loops.Add(bn.parent); } foreach (BLoop loop in loops) { float len = loop.CalculateArclen(); Debug.Log("Calculated arclen of " + len.ToString()); } } if (GUILayout.Button("Calculate SampleLen") == true) { HashSet <BLoop> loops = new HashSet <BLoop>(); foreach (BNode bn in this.selectedNodes) { loops.Add(bn.parent); } foreach (BLoop loop in loops) { float len = loop.CalculateSampleLens(); Debug.Log("Calculated sample len of " + len.ToString()); } } GUILayout.EndHorizontal(); if (GUILayout.Button("Calculate BBoxes") == true) { BoundsMM2 total = BoundsMM2.GetInifiniteRegion(); foreach (BNode bn in this.selectedNodes) { if (bn.next == null) { continue; } BoundsMM2 b2 = bn.GetBounds(); Debug.Log($"Node found to have bounds of region min {{{b2.min.x}, {b2.min.y}}} - and max {{{b2.max.x}, {b2.max.y} }}"); total.Union(b2); } Debug.Log($"Total selected bounds of region min {{{total.min.x}, {total.min.y}}} - and max {{{total.max.x}, {total.max.y} }}"); } if (GUILayout.Button("Split into Thirds") == true) { Vector2 pt0, pt1, pt2, pt3; foreach (BNode bn in this.selectedNodes) { if (bn.next == null) { continue; } Utils.SubdivideBezier(bn.Pos, bn.Pos + bn.TanOut, bn.next.Pos + bn.next.TanIn, bn.next.Pos, out pt0, out pt1, out pt2, out pt3, 0.1f, 0.9f); bn.Pos = pt0; bn.TanOut = (pt1 - pt0); bn.next.TanIn = (pt2 - pt3); bn.next.Pos = pt3; } } GUILayout.Space(20.0f); this.intersectTestStart = EditorGUILayout.Vector2Field("Intersection Start", this.intersectTestStart); this.intersectTextEnd = EditorGUILayout.Vector2Field("Intersection End", this.intersectTextEnd); if (GUILayout.Button("Line Intersection Test") == true) { this.intersectionPreviews.Clear(); foreach (BNode node in this.selectedNodes) { List <float> curveOuts = new List <float>(); if (node.next == null) { continue; } BNode.PathBridge pb = node.GetPathBridgeInfo(); Vector2 pt0 = node.Pos; Vector2 pt1 = node.Pos + pb.prevTanOut; Vector2 pt2 = node.next.Pos + pb.nextTanIn; Vector2 pt3 = node.next.Pos; int cols = Utils.IntersectLine( curveOuts, null, pt0, pt1, pt2, pt3, this.intersectTestStart, this.intersectTextEnd); for (int i = 0; i < cols; ++i) { float intLam = curveOuts[i]; float a, b, c, d; Utils.GetBezierWeights(intLam, out a, out b, out c, out d); this.intersectionPreviews.Add(a * pt0 + b * pt1 + c * pt2 + d * pt3); } } if (this.intersectionPreviews.Count == 0) { Debug.Log("No collisions found"); } else { Debug.Log($"{this.intersectionPreviews.Count} Collisions found"); } } GUILayout.BeginHorizontal(); if (GUILayout.Button("Wind Sel Back") == true) { List <BNode> bnsel = new List <BNode>(this.selectedNodes); this.selectedNodes.Clear(); foreach (BNode bn in bnsel) { if (bn.prev != null) { this.selectedNodes.Add(bn.prev); } } } if (GUILayout.Button("Wind Sel Fwd") == true) { List <BNode> bnsel = new List <BNode>(this.selectedNodes); this.selectedNodes.Clear(); foreach (BNode bn in bnsel) { if (bn.next != null) { this.selectedNodes.Add(bn.next); } } } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); if (GUILayout.Button("Test Union") == true) { BLoop srcLoop = null; List <BLoop> loops = Boolean.GetUniqueLoopsInEncounteredOrder(out srcLoop, this.selectedNodes); // If loops is filled, srcLoop should be non-null if (loops.Count > 0) { BNode filler; Boolean.Union(srcLoop, out filler, loops.ToArray()); } } if (GUILayout.Button("Union Trace") == true) { List <BLoop> loops = Boolean.GetUniqueLoopsInEncounteredOrder(this.selectedNodes); Boolean.TraceUnion(loops, loops[0], null, true); } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); if (GUILayout.Button("Test Difference") == true) { List <BLoop> loops = Boolean.GetUniqueLoopsInEncounteredOrder(this.selectedNodes); if (loops.Count >= 2) { BNode filler; Boolean.Difference(loops[0], loops[1], out filler); } } if (GUILayout.Button("Difference Trace") == true) { List <BLoop> loops = Boolean.GetUniqueLoopsInEncounteredOrder(this.selectedNodes); if (loops.Count >= 2) { Boolean.TraceDifference( loops[0], loops[1], loops[0], true); } } GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); if (GUILayout.Button("Test Intersection") == true) { List <BLoop> loops = Boolean.GetUniqueLoopsInEncounteredOrder(this.selectedNodes); if (loops.Count >= 2) { BNode filler; Boolean.Intersection(loops[0], loops[1], out filler, true); } } if (GUILayout.Button("Intersection Trace") == true) { List <BLoop> loops = Boolean.GetUniqueLoopsInEncounteredOrder(this.selectedNodes); if (loops.Count >= 2) { Boolean.TraceIntersection( loops[0], loops[1], loops[0], null, true); } } GUILayout.EndHorizontal(); if (GUILayout.Button("Test Exclusion") == true) { List <BLoop> loops = Boolean.GetUniqueLoopsInEncounteredOrder(this.selectedNodes); if (loops.Count >= 2) { Boolean.Exclusion(loops[0], loops[1], true); } } if (GUILayout.Button("Bridge") == true) { HashSet <BLoop> loops = new HashSet <BLoop>(); List <BLoop> ol = new List <BLoop>(); foreach (BNode bn in this.selectedNodes) { if (loops.Add(bn.parent) == true) { ol.Add(bn.parent); } } if (ol.Count > 0) { BLoop blTarg = ol[0]; if (ol.Count >= 2) { //Boolean.Union(blTarg, ol[1]); ol[1].DumpInto(blTarg); } List <BNode> islands = blTarg.GetIslands(IslandTypeRequest.Closed); if (islands.Count >= 2) { List <BNode> segmentsA = new List <BNode>(islands[0].Travel()); List <BNode> segmentsB = new List <BNode>(islands[1].Travel()); BNode innerBridgeSeg, outerBridgeSeg; float innerBridgeT, outerBridgeT; BNode.FindBridge( segmentsA, segmentsB, out innerBridgeSeg, out outerBridgeSeg, out innerBridgeT, out outerBridgeT); if (outerBridgeSeg != null) { // We have what we need for a connection. BNode.MakeBridge(innerBridgeSeg, innerBridgeT, outerBridgeSeg, outerBridgeT); } } } } } textToCreate = GUILayout.TextField( textToCreate, GUILayout.ExpandWidth(true), GUILayout.Height(100.0f)); bridgeFonts = GUILayout.Toggle(bridgeFonts, "Bridge Font"); if (GUILayout.Button("Create Text") == true) { List <BShape> charShapes = PxPre.Berny.Text.GenerateString( t.curveDocument.GetFirstLayer(), Vector2.zero, t.typeface, 1.0f, textToCreate); if (bridgeFonts == true) { foreach (BShape bs in charShapes) { Text.BridgeGlyph(bs); } } } if (GUILayout.Button("Bridge Font Shapes") == true) { foreach (BShape shape in t.curveDocument.EnumerateShapes()) { Text.BridgeGlyph(shape); } } if (GUILayout.Button("Clean") == true) { t.curveDocument.Clean(); } if (t.curveDocument.IsDirty() == true) { t.curveDocument.FlushDirty(); } }