From 9049499525179ed12c218d9b74fd3aea2a63b82d Mon Sep 17 00:00:00 2001 From: Zdenek Borovec Date: Sun, 1 Sep 2024 13:10:19 +0200 Subject: [PATCH] core functionality --- .gitignore | 400 +++++++++++++++++++++++++++++++++++++ BooleanOperation.cs | 216 ++++++++++++++++++++ Boprs.csproj | 63 ++++++ Boprs.sln | 25 +++ Commands.cs | 115 +++++++++++ LICENSE | 37 ++++ Properties/AssemblyInfo.cs | 36 ++++ README.md | 5 + Region.cs | 175 ++++++++++++++++ SweepEdge.cs | 399 ++++++++++++++++++++++++++++++++++++ SweepLine.cs | 128 ++++++++++++ SweepVertex.cs | 126 ++++++++++++ Utils.cs | 134 +++++++++++++ 13 files changed, 1859 insertions(+) create mode 100644 .gitignore create mode 100644 BooleanOperation.cs create mode 100644 Boprs.csproj create mode 100644 Boprs.sln create mode 100644 Commands.cs create mode 100644 LICENSE create mode 100644 Properties/AssemblyInfo.cs create mode 100644 README.md create mode 100644 Region.cs create mode 100644 SweepEdge.cs create mode 100644 SweepLine.cs create mode 100644 SweepVertex.cs create mode 100644 Utils.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca1c7a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,400 @@ +# ---> VisualStudio +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + diff --git a/BooleanOperation.cs b/BooleanOperation.cs new file mode 100644 index 0000000..bc4166c --- /dev/null +++ b/BooleanOperation.cs @@ -0,0 +1,216 @@ +#if ACAD24 +using Autodesk.AutoCAD.EditorInput; +using Autodesk.AutoCAD.Geometry; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.Colors; +using AcAp = Autodesk.AutoCAD.ApplicationServices; +#elif BCAD +using Bricscad.EditorInput; +using Teigha.Geometry; +using Teigha.DatabaseServices; +using Teigha.Colors; +#endif +using System; +using System.Collections.Generic; + +namespace Boprs +{ + /// + /// Type of the boolean operation. + /// + public enum BoprType + { + /// + /// Union. + /// + UNITE, + /// + /// Intersection. + /// + INTERSECT, + /// + /// Difference. + /// + SUBTRACT, + /// + /// Exclusive or. + /// + EXCLUSIVE, + } + + /// + /// Boolean operation on two regions. + /// + public class BooleanOperation + { + /// + /// Resulting region from the operation. + /// + public Region Result { get; private set; } + + /// + /// Subdivide any intersecting edges such that there are no more edge intersections and tag their Type property. + /// + /// + /// + /// This method will empty the sortedVertices list. + /// + /// + /// Lexico-graphically sorted vertices of the edges to subdivide. + /// Processed edges. + private List ProcessEdges(List sortedVertices) + { + // Sweepline to keep track of edges. + SweepLine sweepLine = new SweepLine(); + + // Processed edges will be moved here. + List processed = new List(); + + // Go through all the sorted vertices, test for intersections when appropriate and, + // if needed, subdivide the edges. + while (sortedVertices.Count != 0) + { + SweepVertex acVx = sortedVertices[0]; + sortedVertices.RemoveAt(0); + + if (acVx.IsLeft()) + { + sweepLine.Add(acVx.Edge, true); + + SweepEdge prevEdge = sweepLine.PrevEdge(acVx.Edge); + SweepEdge nextEdge = sweepLine.NextEdge(acVx.Edge); + + acVx.Edge.SetInsideOther(prevEdge); + + if (prevEdge != null) + { + List newVertices = acVx.Edge.TrySubdivideBy(prevEdge); + + foreach (SweepVertex vertex in newVertices) + { + Utils.InsertVertexSorted(sortedVertices, vertex); + } + } + if (nextEdge != null) + { + List newVertices = acVx.Edge.TrySubdivideBy(nextEdge); + + foreach (SweepVertex vertex in newVertices) + { + Utils.InsertVertexSorted(sortedVertices, vertex); + } + } + } + else + { + SweepEdge prevEdge = sweepLine.PrevEdge(acVx.Edge); + SweepEdge nextEdge = sweepLine.NextEdge(acVx.Edge); + + sweepLine.Remove(acVx.Edge); + processed.Add(acVx.Edge); + + if (prevEdge != null && nextEdge != null) + { + List newVertices = prevEdge.TrySubdivideBy(nextEdge); + + foreach (SweepVertex vertex in newVertices) + { + Utils.InsertVertexSorted(sortedVertices, vertex); + } + } + } + } + + return processed; + } + + /// + /// Should a given edge be included in the result polygon? + /// + /// Edge to evaluate. + /// Subject region of this operation. + /// Type of the boolean operation. + /// true if the edge should be included in the result, false if not. + private bool IsInResult(SweepEdge edge, Region subject, BoprType boprType) + { + switch (edge.Type) + { + case EdgeType.NORMAL: + switch (boprType) + { + case BoprType.UNITE: + return !edge.InsideOther; + case BoprType.INTERSECT: + return edge.InsideOther; + case BoprType.SUBTRACT: + return (edge.ParentRegion == subject && !edge.InsideOther) || + (edge.ParentRegion != subject && edge.InsideOther); + case BoprType.EXCLUSIVE: + return true; + } + break; + case EdgeType.OVERLAP_SAME: + return boprType == BoprType.INTERSECT || boprType == BoprType.UNITE; + case EdgeType.OVERLAP_DIFFERENT: + return boprType == BoprType.SUBTRACT; + case EdgeType.OVERLAP_SILENT: + return false; + } + return false; // Just to avoid compiler warning. + } + + /// + /// Build the result polygon from processed edges. + /// + /// Input edges. + /// Subject region of this operation. + /// Type of boolean operation. + /// Built result region. + private Region BuildResult(List edges, Region subject, BoprType boprType) + { + List chosenSegments = new List(); + + foreach(SweepEdge edge in edges) + { + if(IsInResult(edge, subject, boprType)) + { + chosenSegments.Add(new LineSegment2d(edge.LeftVertex.Point, edge.RightVertex.Point)); + } + } + + return Region.FromSegments(chosenSegments); + } + + /// + /// Compute a boolean operation on two regions. + /// + /// Subject region. + /// Clip region. + /// Boolean operation type. + public BooleanOperation(Region subject, Region clip, BoprType type) + { + List sortedVerts = new List(); + + foreach (LineSegment2d seg in subject.GetSegments()) + { + SweepEdge edge = new SweepEdge(seg.StartPoint, seg.EndPoint); + + sortedVerts.Add(edge.LeftVertex); + sortedVerts.Add(edge.RightVertex); + } + foreach (LineSegment2d seg in clip.GetSegments()) + { + SweepEdge edge = new SweepEdge(seg.StartPoint, seg.EndPoint); + + sortedVerts.Add(edge.LeftVertex); + sortedVerts.Add(edge.RightVertex); + } + + sortedVerts.Sort(); + + List processed = ProcessEdges(sortedVerts); + + Result = BuildResult(processed, subject, type); + } + } +} diff --git a/Boprs.csproj b/Boprs.csproj new file mode 100644 index 0000000..4edefaa --- /dev/null +++ b/Boprs.csproj @@ -0,0 +1,63 @@ + + + + + Debug + AnyCPU + {12366D09-2514-4EF3-9918-253B46077A4D} + Library + Properties + Boprs + Boprs + v4.8 + 512 + true + + + true + full + false + bin\Debug\ + TRACE;DEBUG;ACAD24 + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\..\..\..\..\Program Files\Autodesk\AutoCAD 2024\accoremgd.dll + + + ..\..\..\..\..\Program Files\Autodesk\AutoCAD 2024\acdbmgd.dll + + + ..\..\..\..\..\Program Files\Autodesk\AutoCAD 2024\acmgd.dll + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Boprs.sln b/Boprs.sln new file mode 100644 index 0000000..bae7aa2 --- /dev/null +++ b/Boprs.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.10.34928.147 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Boprs", "Boprs.csproj", "{12366D09-2514-4EF3-9918-253B46077A4D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {12366D09-2514-4EF3-9918-253B46077A4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {12366D09-2514-4EF3-9918-253B46077A4D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {12366D09-2514-4EF3-9918-253B46077A4D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {12366D09-2514-4EF3-9918-253B46077A4D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {36F3AEF0-3759-4E30-8DA3-A47688241BCE} + EndGlobalSection +EndGlobal diff --git a/Commands.cs b/Commands.cs new file mode 100644 index 0000000..5887c38 --- /dev/null +++ b/Commands.cs @@ -0,0 +1,115 @@ +#if ACAD24 +using Autodesk.AutoCAD.Runtime; +using Autodesk.AutoCAD.EditorInput; +using Autodesk.AutoCAD.Geometry; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.Colors; +using AcAp = Autodesk.AutoCAD.ApplicationServices; +#elif BCAD +using Teigha.Runtime; +using Bricscad.EditorInput; +using Teigha.Geometry; +using Teigha.DatabaseServices; +using Teigha.Colors; +#endif +using System; +using System.Collections.Generic; +using Boprs; + +#if DEBUG +[assembly: CommandClass(typeof(Commands))] +namespace Boprs +{ + internal class Commands + { + private Region UserCreateRegion(string msg) + { + AcAp.Document acDoc = AcAp.Application.DocumentManager.MdiActiveDocument; + Database acDb = acDoc.Database; + + acDoc.Editor.WriteMessage("\n" + msg); + + PromptSelectionResult res = acDoc.Editor.GetSelection(); + + if (res.Status != PromptStatus.OK || res.Value.Count == 0) + { + return null; + } + + List segs = new List(); + + using (Transaction acTrans = acDb.TransactionManager.StartTransaction()) + { + foreach (ObjectId asObjId in res.Value.GetObjectIds()) + { + Polyline asPoly = (Polyline)acTrans.GetObject(asObjId, OpenMode.ForWrite); + + using (DBObjectCollection exploded = new DBObjectCollection()) + { + asPoly.Explode(exploded); + + foreach(DBObject dBObject in exploded) + { + Line asLine = (Line)dBObject; + segs.Add(new LineSegment2d(asLine.StartPoint.To2d(), asLine.EndPoint.To2d())); + } + } + } + acTrans.Commit(); + } + + return Region.FromSegments(segs); + } + + private BoprType UserChooseBoprType() + { + AcAp.Document acDoc = AcAp.Application.DocumentManager.MdiActiveDocument; + + PromptKeywordOptions keyPrompt = new PromptKeywordOptions("\nBoolean operation type: "); + keyPrompt.Keywords.Add("Unite"); + keyPrompt.Keywords.Add("Intersect"); + keyPrompt.Keywords.Add("Subtract"); + keyPrompt.Keywords.Add("eXclusive"); + keyPrompt.AllowNone = false; + + PromptResult keywRes = acDoc.Editor.GetKeywords(keyPrompt); + + switch (keywRes.StringResult.ToLower()) + { + case "unite": + return BoprType.UNITE; + case "intersect": + return BoprType.INTERSECT; + case "subtract": + return BoprType.SUBTRACT; + case "exclusive": + return BoprType.EXCLUSIVE; + default: + return BoprType.INTERSECT; + } + } + + [CommandMethod("boprtest")] + public void Subdivide() + { + AcAp.Document acDoc = AcAp.Application.DocumentManager.MdiActiveDocument; + + Region subject = UserCreateRegion("Enter polylines for first polygon:"); + + Region clip = UserCreateRegion("Enter polylines for second polygon:"); + + if(subject == null || clip == null) + { + acDoc.Editor.WriteMessage("\nOne of the polyline selections was empty."); + return; + } + + BoprType type = UserChooseBoprType(); + + BooleanOperation bopr = new BooleanOperation(subject, clip, type); + + bopr.Result.Draw(); + } + } +} +#endif \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e582330 --- /dev/null +++ b/LICENSE @@ -0,0 +1,37 @@ +3-Clause BSD NON-AI License + +Copyright 2024 Zdenek Borovec + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +4. The source code and the binary form, and any modifications made to them + may not be used for the purpose of training or improving machine learning + algorithms, including but not limited to artificial intelligence, + natural language processing, or data mining. This condition applies + to any derivatives, modifications, or updates based on the Software code. + Any usage of the source code or the binary form in an AI-training dataset + is considered a breach of this License. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..aba560d --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Boprs")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("DEK")] +[assembly: AssemblyProduct("Boprs")] +[assembly: AssemblyCopyright("Copyright © DEK 2024")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("12366d09-2514-4ef3-9918-253b46077a4d")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/README.md b/README.md new file mode 100644 index 0000000..bca2435 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# Boprs + +Boolean operation library for .NET framework 4.8, written in C#. Default bindings for AutoCAD (pre-v25) and BricsCAD. + +Implements the algorithm described in the "A simple algorithm for Boolean operations on polygons" paper by F. Martínez et al. \ No newline at end of file diff --git a/Region.cs b/Region.cs new file mode 100644 index 0000000..02fff67 --- /dev/null +++ b/Region.cs @@ -0,0 +1,175 @@ +#if ACAD24 +using Autodesk.AutoCAD.EditorInput; +using Autodesk.AutoCAD.Geometry; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.Colors; +using AcAp = Autodesk.AutoCAD.ApplicationServices; +#elif BCAD +using Bricscad.EditorInput; +using Teigha.Geometry; +using Teigha.DatabaseServices; +using Teigha.Colors; +#endif +using System; +using System.Collections.Generic; + +namespace Boprs +{ + /// + /// Geometric region used for boolean operations calculations. + /// + public class Region + { + /// + /// Vertices belonging to this polygon. + /// + private List Vertices { get; } + + /// + /// Subdivide my edges, set their inOut tags and validate their non-colinearity. + /// + private void ValidateEdges() + { + // Lexico-graphically sort my vertices. + Vertices.Sort(); + + // Sweepline to keep track of edges. + SweepLine sweepLine = new SweepLine(); + + // Current number of vertices in the list. + // Will be increased if subdivision occurs. + int numVertices = Vertices.Count; + + // Go through all the sorted vertices, test for intersections when appropriate and, + // if needed, subdivide the edges. + for (int i = 0; i < numVertices; i++) + { + SweepVertex acVx = Vertices[i]; + + if (acVx.IsLeft()) + { + sweepLine.Add(acVx.Edge, false); + + SweepEdge prevEdge = sweepLine.PrevEdge(acVx.Edge); + SweepEdge nextEdge = sweepLine.NextEdge(acVx.Edge); + + acVx.Edge.SetTransitionInside(prevEdge); + + if (prevEdge != null) + { + List newVertices = acVx.Edge.TrySubdivideBy(prevEdge); + + foreach (SweepVertex vertex in newVertices) + { + Utils.InsertVertexSorted(Vertices, vertex); + numVertices++; + } + } + if (nextEdge != null) + { + List newVertices = acVx.Edge.TrySubdivideBy(nextEdge); + + foreach (SweepVertex vertex in newVertices) + { + Utils.InsertVertexSorted(Vertices, vertex); + numVertices++; + } + } + } + else + { + SweepEdge prevEdge = sweepLine.PrevEdge(acVx.Edge); + SweepEdge nextEdge = sweepLine.NextEdge(acVx.Edge); + + sweepLine.Remove(acVx.Edge); + + if (prevEdge != null && nextEdge != null) + { + List newVertices = prevEdge.TrySubdivideBy(nextEdge); + + foreach (SweepVertex vertex in newVertices) + { + Utils.InsertVertexSorted(Vertices, vertex); + numVertices++; + } + } + } + } + } + + /// + /// Get a copy of the regions vertex list. + /// + /// Copy of the regions vertex list. + internal List GetSegments() + { + List segments = new List(); + + foreach(SweepVertex sweepVertex in Vertices) + { + if (sweepVertex.IsLeft()) + { + segments.Add( + new LineSegment2d(sweepVertex.Edge.LeftVertex.Point, sweepVertex.Edge.RightVertex.Point)); + } + } + + return segments; + } + + /// + /// Instantiate a region with edges defined by a list of given line segments. + /// + /// + /// + public static Region FromSegments(List segments) + { + Region region = new Region(); + + foreach(LineSegment2d segment in segments) + { + SweepEdge newEdge = new SweepEdge(segment.StartPoint, segment.EndPoint); + region.Vertices.Add(newEdge.LeftVertex); + region.Vertices.Add(newEdge.RightVertex); + newEdge.ParentRegion = region; + } + + region.ValidateEdges(); + + return region; + } + + /// + /// Constructor. + /// + private Region() + { + Vertices = new List(); + } + +#if DEBUG + internal void Draw() + { + Erase(); + + foreach (SweepVertex vert in Vertices) + { + if (vert.IsLeft()) + { + vert.Edge.Draw(); + } + } + } + internal void Erase() + { + foreach (SweepVertex vert in Vertices) + { + if (vert.IsLeft()) + { + vert.Edge.Erase(); + } + } + } +#endif + } +} diff --git a/SweepEdge.cs b/SweepEdge.cs new file mode 100644 index 0000000..8373bfe --- /dev/null +++ b/SweepEdge.cs @@ -0,0 +1,399 @@ +#if ACAD24 +using Autodesk.AutoCAD.EditorInput; +using Autodesk.AutoCAD.Geometry; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.Colors; +using AcAp = Autodesk.AutoCAD.ApplicationServices; +#elif BCAD +using Bricscad.EditorInput; +using Teigha.Geometry; +using Teigha.DatabaseServices; +using Teigha.Colors; +#endif +using System; +using System.Collections.Generic; + +namespace Boprs +{ + /// + /// Type the edge can be. + /// + internal enum EdgeType + { + /// + /// Normal edge, nothing special. + /// + NORMAL, + /// + /// Edge overlaps with an edge on another region, + /// both represent the same transition (in or out) of their respective regions. + /// + OVERLAP_SAME, + /// + /// Edge overlaps with an edge on another region, + /// both represent a different transition (in or out) of their respective regions. + /// + OVERLAP_DIFFERENT, + /// + /// Edge overlaps with an edge on another region, it will handle the situation. + /// + OVERLAP_SILENT, + } + + /// + /// Line segment (edge) used while a sweep line is processing a boolean operation. + /// + internal class SweepEdge + { + /// + /// String representation of the edge. + /// + public override string ToString() + { + return ($"({LeftVertex} -- {RightVertex})"); + } + + /// + /// Comparison of edges. + /// + /// Other edge to compare. + /// X param at which to make the comparison. + /// -1 if this is below obj, 1 if this is above obj, 0 if collinear. + public int CompareTo(SweepEdge other, double xParam) + { + // Get the Y values of the edges at the given X coordinate + // If the edge is vertical, the Y value of its left (lower) endpoint is its Y value for this comparison. + double myY = IsVertical() ? LeftVertex.Point.Y : + Utils.YatX(LeftVertex.Point, RightVertex.Point, xParam); + + double otherY = other.IsVertical() ? other.LeftVertex.Point.Y : + Utils.YatX(other.LeftVertex.Point, other.RightVertex.Point, xParam); + + // If the edges are not intersecting at this X, order them as expected. + if (!Utils.DoubleEquals(myY, otherY)) + { + return myY > otherY ? 1 : -1; + } + // If the edges do intersect here, and one is vertical, the non-vertical one is "lower". + if (IsVertical() != other.IsVertical()) + { + return IsVertical() ? 1 : -1; + } + // If they are not collinear, the one leading downwards from here is "lower". + if (Utils.SignedArea(LeftVertex.Point, RightVertex.Point, other.RightVertex.Point) < 0) + { + return 1; + } + if (Utils.SignedArea(LeftVertex.Point, RightVertex.Point, other.RightVertex.Point) > 0) + { + return -1; + } + // If they are collinear, return 0; + return 0; + } + + /// + /// Left (or bottom) vertex of the edge. + /// + internal SweepVertex LeftVertex { get; set; } + + /// + /// Right (or top) vertex of the edge. + /// + internal SweepVertex RightVertex { get; set; } + + /// + /// Does this edge signify a transition from the outside to the inside of its region? + /// + internal bool TransitionInside { get; private set; } + + /// + /// Is this edge inside the other polygon? + /// + internal bool InsideOther { get; private set; } + + /// + /// Type of the edge. + /// + internal EdgeType Type { get; private set; } + + /// + /// Region I belong to. + /// + internal Region ParentRegion { get; set; } + + /// + /// Is this edge vertical? + /// + /// true if this vedge is vertical, false if not. + internal bool IsVertical() + { + return Utils.DoubleEquals(LeftVertex.Point.X, RightVertex.Point.X); + } + + /// + /// Set the TransitionInside flag for this edge. + /// + /// Edge preceeding this one in the region construction sweep line. + internal void SetTransitionInside(SweepEdge prevEdge) + { + if(prevEdge == null) + { + TransitionInside = true; + } + else if(IsVertical()) + { + TransitionInside = prevEdge.TransitionInside; + } + else + { + TransitionInside = !prevEdge.TransitionInside; + } + } + + /// + /// Set the InsideOther flag for this edge. + /// + /// Edge preceeding this one in the bopr processing sweep line. + internal void SetInsideOther(SweepEdge prevEdge) + { + AcAp.Document acDoc = AcAp.Application.DocumentManager.MdiActiveDocument; + + if (prevEdge == null) + { + InsideOther = false; + } + else if (ParentRegion == prevEdge.ParentRegion) + { + InsideOther = prevEdge.InsideOther; + } + else + { + InsideOther = prevEdge.TransitionInside; + } + } + + /// + /// Create new edge going from pt to my right vertex and make pt my new right vertex. + /// + /// Point at which to subdivide. + /// List of the two newly created vertices. + internal List SubdivideAt(Point2d pt) + { + // Create the new vertices. + SweepVertex newLeft = new SweepVertex(pt); + SweepVertex newRight = new SweepVertex(pt); + + // Create new edge from the point to my right vertex and assign it as the edge of its vertices. + SweepEdge newEdge = new SweepEdge(newLeft, RightVertex); + newEdge.TransitionInside = TransitionInside; + newEdge.ParentRegion = ParentRegion; + newLeft.Edge = newEdge; + RightVertex.Edge = newEdge; + + // Set the point as my new right vertex and me as its edge. + RightVertex = newRight; + newRight.Edge = this; + + // Return the newly created vertices. + return new List() { newLeft, newRight }; + } + + /// + /// Intersect this edge with the other, if there is an intersection, subdivide them. + /// + /// Edge to try intersecting with. + /// Unsorted list of newly created verticies. + internal List TrySubdivideBy(SweepEdge otherEdge) + { + // Get the linesegment representation of this and the other edge. + LineSegment2d myLine = new LineSegment2d(LeftVertex.Point, RightVertex.Point); + LineSegment2d otherLine = new LineSegment2d(otherEdge.LeftVertex.Point, otherEdge.RightVertex.Point); + + // Get the potential intersection between the two edges. + CurveCurveIntersector2d intersector = new CurveCurveIntersector2d(myLine, otherLine); + + // If the intersection was on the endpoints of both segments, return empty list. + // Right vertices get processed before left ones, so connected left and right vertices + // will not communicate. + if (intersector.NumberOfIntersectionPoints == 1) + { + List newVertices = new List(); + Point2d intPt = intersector.GetIntersectionPoint(0); + + // If the point is inside of either edge (not on its end point), subdivide the edge. + if (!(Utils.DoubleEquals(intPt.GetDistanceTo(LeftVertex.Point), 0) || + Utils.DoubleEquals(intPt.GetDistanceTo(RightVertex.Point), 0))) + { + newVertices.AddRange(SubdivideAt(intPt)); + } + if (!(Utils.DoubleEquals(intPt.GetDistanceTo(otherEdge.LeftVertex.Point), 0) || + Utils.DoubleEquals(intPt.GetDistanceTo(otherEdge.RightVertex.Point), 0))) + { + newVertices.AddRange(otherEdge.SubdivideAt(intPt)); + } + + // Return the newly created vertices. + return newVertices; + } + + // Overlap + if (myLine.Overlaps(otherLine)) + { + List newVertices = new List(); + + // The edges share the left vertex + if (Utils.DoubleEquals(LeftVertex.Point.GetDistanceTo(otherEdge.LeftVertex.Point), 0)) + { + EdgeType et = (TransitionInside == otherEdge.TransitionInside) ? + EdgeType.OVERLAP_SAME : EdgeType.OVERLAP_DIFFERENT; + + // The edges share the right vertex as well, set the edge type for both this and the other + // edge and return an empty list. + if (Utils.DoubleEquals(RightVertex.Point.GetDistanceTo(otherEdge.RightVertex.Point), 0)) + { + Type = et; + otherEdge.Type = EdgeType.OVERLAP_SILENT; + + return newVertices; + } + // One of the edges carries on farther, clip it to be fully overlapping, + // set the edge type for this and other edge and create a new edge from the clipped off part. + else + { + SweepEdge longer = RightVertex.CompareTo(otherEdge.RightVertex) > 0 ? this : otherEdge; + SweepEdge shorter = (this == longer) ? otherEdge : this; + + newVertices.AddRange(longer.SubdivideAt(shorter.RightVertex.Point)); + + Type = et; + otherEdge.Type = EdgeType.OVERLAP_SILENT; + + return newVertices; + } + } + // One edge starts earlier than the other. Find it and create new edge from the point it + // intersects the other edge, those two will negtiate how to handle the situation later. + else + { + SweepEdge earlier = LeftVertex.CompareTo(otherEdge.LeftVertex) < 0 ? this : otherEdge; + SweepEdge later = (this == earlier) ? otherEdge : this; + + newVertices.AddRange(earlier.SubdivideAt(later.LeftVertex.Point)); + + return newVertices; + } + } + + // If there was no intersection, return empty list + return new List(); + } + + /// + /// Constructor. + /// + /// Endpoint 1. + /// EndPoint 2. + internal SweepEdge(Point2d pt1, Point2d pt2) + { + if (Utils.DoubleEquals(pt1.GetDistanceTo(pt2), 0)) + { + throw new ArgumentException("Edge end points cannot be the same."); + } + + SweepVertex v1 = new SweepVertex(pt1); + SweepVertex v2 = new SweepVertex(pt2); + + v1.Edge = this; + v2.Edge = this; + Type = EdgeType.NORMAL; + + if (v1.CompareTo(v2) < 0) + { + LeftVertex = v1; + RightVertex = v2; + } + else + { + LeftVertex = v2; + RightVertex = v1; + } + } + + internal SweepEdge(SweepVertex v1, SweepVertex v2) + { + Type = EdgeType.NORMAL; + + if (v1.CompareTo(v2) < 0) + { + LeftVertex = v1; + RightVertex = v2; + } + else + { + LeftVertex = v2; + RightVertex = v1; + } + } + +#if DEBUG + internal ObjectId ObjectId { get; private set; } + + internal void Draw(Color color = null) + { + AcAp.Document acDoc = AcAp.Application.DocumentManager.MdiActiveDocument; + Database acDb = acDoc.Database; + + Erase(); + + Color lineColor = color; + + if (color == null) + { + if (Type == EdgeType.NORMAL) + { + lineColor = TransitionInside ? + Color.FromRgb(32, 255, 32) : Color.FromRgb(255, 32, 32); + } + else + { + lineColor = Type == EdgeType.OVERLAP_SAME ? + Color.FromRgb(32, 32, 255) : Color.FromRgb(255, 255, 32); + } + } + + using (Transaction acTrans = acDb.TransactionManager.StartTransaction()) + { + // Open the BlockTableRecord + BlockTable acBlkTbl = (BlockTable)acTrans.GetObject(acDb.BlockTableId, OpenMode.ForRead); + BlockTableRecord acBlkTblRec = + (BlockTableRecord)acTrans.GetObject(acBlkTbl[BlockTableRecord.ModelSpace], OpenMode.ForWrite); + + Line asLine = new Line(LeftVertex.Point.To3d(), RightVertex.Point.To3d()); + asLine.Color = lineColor; + acBlkTblRec.AppendEntity(asLine); + acTrans.AddNewlyCreatedDBObject(asLine, true); + + acTrans.Commit(); + + ObjectId = asLine.ObjectId; + } + } + + internal void Erase() + { + AcAp.Document acDoc = AcAp.Application.DocumentManager.MdiActiveDocument; + Database acDb = acDoc.Database; + + if (!ObjectId.IsNull && !ObjectId.IsEffectivelyErased) + { + using (Transaction acTrans = acDb.TransactionManager.StartTransaction()) + { + acTrans.GetObject(ObjectId, OpenMode.ForWrite).Erase(); + acTrans.Commit(); + } + } + } +#endif + } +} diff --git a/SweepLine.cs b/SweepLine.cs new file mode 100644 index 0000000..1435e4c --- /dev/null +++ b/SweepLine.cs @@ -0,0 +1,128 @@ +#if ACAD24 +using Autodesk.AutoCAD.EditorInput; +using Autodesk.AutoCAD.Geometry; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.Colors; +using AcAp = Autodesk.AutoCAD.ApplicationServices; +#elif BCAD +using Bricscad.EditorInput; +using Teigha.Geometry; +using Teigha.DatabaseServices; +using Teigha.Colors; +#endif +using System; +using System.Collections.Generic; + +namespace Boprs +{ + /// + /// Line to sweep through sorted vertices, keeps track of currently intersecting edges. + /// + internal class SweepLine + { + /// + /// Edges the line is currently intersecting. + /// + /// TODO: Change this into a BST when optimizing. + private List IntersectingEdges { get; } + + /// + /// Start tracking an edge identified by its left vertex. + /// + /// Edge to start tracking. + /// + /// If false and the edge is collinear with one of the edges in the sweep line, + /// an exception will get thrown. + /// + internal void Add(SweepEdge edge, bool allowCollinearity) + { + int i = 0; + + if (IntersectingEdges.Contains(edge)) + { + throw new Exception("inserting contained edge"); + } + + // Go through all the edges, find my index for insertion. + for(; i < IntersectingEdges.Count; i++) + { + int comparison = edge.CompareTo(IntersectingEdges[i], edge.LeftVertex.Point.X); + // If we are below the current edge, that is the correct index. + if (comparison < 0) + { + break; + } + // If edges are collinear and it is not allowed, throw an exception + else if(!allowCollinearity && comparison == 0) + { + throw new Exception($"Collinear edges detected. [{edge} & {IntersectingEdges[i]}]"); + } + // If we are above the last edge, set inserttion index after it. + else if (i == IntersectingEdges.Count) + { + i++; + } + } + + // Insert the edge to the correct place. + IntersectingEdges.Insert(i, edge); + } + + /// + /// Remove an edge from being tracked by the sweep line. + /// + /// Edge to remove. + internal void Remove(SweepEdge edge) + { + IntersectingEdges.Remove(edge); + } + + /// + /// Returns the edge below curr, or null if curr is the lowest. + /// + /// Current edge. + /// Edge below curr, or null. + internal SweepEdge PrevEdge(SweepEdge curr) + { + // Get the index of the current edge + int currIdx = IntersectingEdges.IndexOf(curr); + + // If the current edge isn't the lowest, return the edge below it. + if(currIdx != 0) + { + return IntersectingEdges[currIdx - 1]; + } + + // Otherwise return null. + return null; + } + + /// + /// Returns the edge above curr, or null if curr is the highest. + /// + /// Current edge. + /// Edge above curr, or null. + internal SweepEdge NextEdge(SweepEdge curr) + { + // Get the index of the current edge + int currIdx = IntersectingEdges.IndexOf(curr); + + // If the current edge isn't the highest, return the edge above it. + if (currIdx != IntersectingEdges.Count - 1) + { + return IntersectingEdges[currIdx + 1]; + } + + // Otherwise return null. + return null; + } + + /// + /// Constructor. + /// + internal SweepLine() + { + IntersectingEdges = new List(); + } + } +} diff --git a/SweepVertex.cs b/SweepVertex.cs new file mode 100644 index 0000000..ff80d0c --- /dev/null +++ b/SweepVertex.cs @@ -0,0 +1,126 @@ +#if ACAD24 +using Autodesk.AutoCAD.EditorInput; +using Autodesk.AutoCAD.Geometry; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.Colors; +using AcAp = Autodesk.AutoCAD.ApplicationServices; +#elif BCAD +using Bricscad.EditorInput; +using Teigha.Geometry; +using Teigha.DatabaseServices; +using Teigha.Colors; +#endif +using System; +using System.Collections.Generic; + +namespace Boprs +{ + /// + /// End point of an edge used while sweep line is processing a boolean operation. + /// + internal class SweepVertex : IComparable + { + /// + /// String representation of the vertex. + /// + public override string ToString() + { + return ($"({Point.X}, {Point.Y})"); + } + + /// + /// Comparison of vertices. + /// + /// Vertex to compare. + /// + /// -1 if this is to the left of obj, 1 if this is to the right of obj, 0 if same. + /// + public int CompareTo(object obj) + { + if (obj is SweepVertex other) + { + // Different x coordinates + if (Point.X > other.Point.X) + { + return 1; + } + // Different x coordinates + if (Point.X < other.Point.X) + { + return -1; + } + // Different points, but same x-coordinate. The event with lower y-coordinate is processed first. + if (Point.Y != other.Point.Y) + { + return Point.Y > other.Point.Y ? 1 : -1; + } + // Same point, but one is a left endpoint and the other a right endpoint. + // The right endpoint is processed first. + if (IsLeft() != other.IsLeft()) + { + return IsLeft() ? 1 : -1; + } + // One of our edges is vertical, the non-vertical one gets processed first. + if (Edge.IsVertical() != other.Edge.IsVertical()) + { + return Edge.IsVertical() ? 1 : -1; + } + // Left vertex common, the downward leading edge gets processed first. + if (IsLeft()) + { + if (Utils.SignedArea(Point, Edge.RightVertex.Point, other.Edge.RightVertex.Point) < 0) + { + return 1; + } + if (Utils.SignedArea(Point, Edge.RightVertex.Point, other.Edge.RightVertex.Point) > 0) + { + return -1; + } + } + // Right vertex is common, the upward leading edge gets processed first. + if (!IsLeft()) + { + if (Utils.SignedArea(Point, Edge.RightVertex.Point, other.Edge.RightVertex.Point) < 0) + { + return -1; + } + if (Utils.SignedArea(Point, Edge.RightVertex.Point, other.Edge.RightVertex.Point) > 0) + { + return 1; + } + } + // Colinear edges + return 0; + } + throw new ArgumentException("obj is not SweepEvent."); + } + + /// + /// Position of the vertex, use + /// + internal Point2d Point { get; } + + /// + /// Edge this vertex belongs to. + /// + internal SweepEdge Edge { get; set; } + + /// + /// Is this the left vertex of my parent edge? + /// + /// true if this is the left(or bottom) edge of my parent, false if not. + internal bool IsLeft() + { + return this == Edge.LeftVertex; + } + + /// + /// Constructor. + /// + /// Position of the vertex. + internal SweepVertex (Point2d point) + { + Point = point; + } + } +} diff --git a/Utils.cs b/Utils.cs new file mode 100644 index 0000000..fb3b12b --- /dev/null +++ b/Utils.cs @@ -0,0 +1,134 @@ +#if ACAD24 +using Autodesk.AutoCAD.EditorInput; +using Autodesk.AutoCAD.Geometry; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.Colors; +using AcAp = Autodesk.AutoCAD.ApplicationServices; +#elif BCAD +using Bricscad.EditorInput; +using Teigha.Geometry; +using Teigha.DatabaseServices; +using Teigha.Colors; +#endif +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Boprs +{ + /// + /// Non=specific utility functions. + /// + internal static class Utils + { + /// + /// Largest difference between two doubles before they are considered equal. + /// + internal const double DOUBLEPREC = 1e-8; + + /// + /// Signed area of the triangle (pt1, pt2, pt3). + /// + /// Point 1 + /// Point 2 + /// Point 3 + /// Signed area of the triangle (pt1, pt2, pt3). + internal static double SignedArea(Point2d pt1, Point2d pt2, Point2d pt3) + { + return (pt1.X - pt3.X) * (pt2.Y - pt3.Y) - (pt2.X - pt3.X) * (pt1.Y - pt3.Y); + } + + /// + /// Are the two doubles close enough to be almost equal? + /// + /// First double to compare. + /// Second double to compare. + /// true if doubles are almost equal, false if doubles are not equal. + internal static bool DoubleEquals(double a, double b) + { + return Math.Abs(a - b) < DOUBLEPREC; + } + + /// + /// Get the value Y at a given coordinate X for line going through stPt and endPt. + /// + /// Start point of the line. + /// End point of the line. + /// X param. + /// Y value at X param for line going through the 2 points. + internal static double YatX(Point2d stPt, Point2d endPt, double x) + { + double m = (endPt.Y - stPt.Y) / (endPt.X - stPt.X); + double b = stPt.Y - (m * stPt.X); + return m * x + b; + } + + /// + /// Insert a new vertex inbto a sorted list of vertices such that it remains sorted. + /// + /// Sorted list of vertices. + /// Vertex to insert. + internal static void InsertVertexSorted(List vertices, SweepVertex newElement) + { + int i = 0; + + // Go through all the vertices, find my index for insertion. + for (; i < vertices.Count; i++) + { + // If we are left of the current edge, that is the correct index. + if (newElement.CompareTo(vertices[i]) < 0) + { + break; + } + // If we are to the right of the last vertex, set inserttion index after it. + else if (i == vertices.Count - 1) + { + i++; + } + } + // Insert the vertex to the correct place. + vertices.Insert(i, newElement); + } + + internal static bool Overlaps(this LineSegment2d me, LineSegment2d other) + { + if (!me.IsColinearTo(other)) + { + return false; + } + Line2d commonLine = me.GetLine(); + + double a = commonLine.GetParameterOf(me.StartPoint); + double b = commonLine.GetParameterOf(me.EndPoint); + double c = commonLine.GetParameterOf(other.StartPoint); + double d = commonLine.GetParameterOf(other.EndPoint); + + if(DoubleEquals(b, c) || DoubleEquals(a, d)) + { + return false; + } + if (b < c || d < a) + { + return false; + } + + return true; + } + + /// + /// Convert a 3d point to 2d. + /// + internal static Point2d To2d(this Point3d pt) + { + return new Point2d(pt.X, pt.Y); + } + + /// + /// Convert a 2d point to 3d. + /// + internal static Point3d To3d(this Point2d pt) + { + return new Point3d(pt.X, pt.Y, 0); + } + } +}