From e9e698c570cebb2d11e4795f6ae5b5fe2c8629cc Mon Sep 17 00:00:00 2001 From: Luke Parry Date: Sun, 17 Dec 2023 00:07:20 +0000 Subject: [PATCH] Pyclipr Fixes for addPaths which were not correctly offsetting for polygon regions. Updated example for clipper offset + visualisation --- .github/workflows/pythonpublish.yml | 5 ++- CHANGELOG.md | 14 ++++++++ README.rst | 6 ++-- examples/example.py | 27 ++++++++++------ python/pyclipr/module.cpp | 50 +++++++++++++++++++++-------- setup.py | 2 +- 6 files changed, 77 insertions(+), 27 deletions(-) diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml index 4b92ca0..0101563 100644 --- a/.github/workflows/pythonpublish.yml +++ b/.github/workflows/pythonpublish.yml @@ -14,7 +14,10 @@ jobs: strategy: matrix: python-version: [3.7, 3.8, 3.9, '3.10', '3.11', '3.12'] - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest, macos-13, windows-latest] + exclude: + - os: ubuntu-latest + python-version: '3.12' steps: - uses: actions/checkout@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 247e1c8..d8265f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,20 @@ All notable changes to this project will be documented in this file. ### Changed + +## [0.1.5] - 2023-16-12 + +### Added + +### Fixed + +- In `ClipperOffset::addPaths`, paths of each polygon are seperately constructed into a list of paths to ensure that these are correctly imported into ClipperOffset. +- Scale factor has been applied when importing paths + +### Changed + + + ## [0.1.4] - 2023-11-12 Update release for PyClipr and updated dependencies to ClipperLib2 v.1.2. and Eigen 3.4.0 - dependent on diff --git a/README.rst b/README.rst index 865da1a..2eadac9 100644 --- a/README.rst +++ b/README.rst @@ -81,7 +81,9 @@ by using either `execute` or `execute2` methods, respectively. po.scaleFactor = int(1000) # add the path - ensuring to use Polygon for the endType argument - po.addPath(np.array(path), pyclipr.JoinType.Miter, pyclipr.EndType.Polygon) + # addPaths is required when working with polygon - this is a list of correctly orientated paths for exterior + # and interior holes + po.addPaths([np.array(path)], pyclipr.JoinType.Miter, pyclipr.EndType.Polygon) # Apply the offsetting operation using a delta. offsetSquare = po.execute(10.0) @@ -149,4 +151,4 @@ by using either `execute` or `execute2` methods, respectively. plt.fill(out[0][:, 0], out[0][:, 1], facecolor='#75507b') # Plot the open path intersection - plt.plot(openPathsC[0][:,0], openPathsC[0][:,1],color='#222', linewidth=1.0, linestyle='dashed', marker='.',markersize=20.0) \ No newline at end of file + plt.plot(openPathsC[0][:,0], openPathsC[0][:,1],color='#222', linewidth=1.0, linestyle='dashed', marker='.',markersize=20.0) diff --git a/examples/example.py b/examples/example.py index 4a21452..2b2351d 100644 --- a/examples/example.py +++ b/examples/example.py @@ -4,8 +4,11 @@ import pyclipr # Tuple definition of a path -path = [(0.0, 0.), (0, 105.1234), (100, 105.1234), (100, 0), (0, 0)] -path2 = [(1.0, 1.0), (1.0, 50), (100, 50), (100, 1.0), (1.0, 1.0)] +path = np.array([(0.0, 0.), (0, 105.1234), (100, 105.1234), (100, 0), (0, 0)]) +path2 = np.array([(10.0, 10.0), (1.0, 50), (50.0, 50), (50., 10.0), (10.0, 10.0)]) +path2 = np.flipud(path2) + +path3 = np.array([(-10.0, -10.0), (70.0, -10), (70.0, 70), (-10., 70.0), (-10.0, -10.0)]) """ Create an offsetting PyClipr tool @@ -14,16 +17,20 @@ # Set the scale factor to convert to internal integer representation # Note the default is set to 1000 units -po.scaleFactor = int(1000) +po.scaleFactor = int(1e4) # add the path - ensuring to use Polygon for the endType argument -po.addPath(np.array(path), pyclipr.JoinType.Miter, pyclipr.EndType.Polygon) - +po.addPaths([path, path2], pyclipr.JoinType.Miter, pyclipr.EndType.Polygon) """ Apply the offsetting operation using a delta/offset NoteL this automatically scaled interally scaled within pyclipr.ClipperOffset """ -offsetSquare = po.execute(10.0) +offsetOut = po.execute(-1.0) + +import pyslm.visualise +handle = pyslm.visualise.plotPolygon([path, path2]) +pyslm.visualise.plotPolygon(offsetOut, handle=handle, lineColor='r') + """ Create a Pyclipr clipping tool @@ -35,10 +42,10 @@ Add the paths to the clipping object. Ensure the subject and clip arguments are set to differentiate the paths during the Boolean operation. The final argument specifies if the path is open. """ -pc.addPaths(offsetSquare, pyclipr.Subject) +pc.addPaths(offsetOut, pyclipr.Subject) # Note it is possible to add a path as a numpy array (nx2) or as a list of tuples -pc.addPath(np.array(path2), pyclipr.Clip) +pc.addPath(np.array(path3), pyclipr.Clip) """ Perform Polygon Clipping @@ -76,7 +83,7 @@ pc2.addPath(((40, -10), (50, 130)), pyclipr.Subject, True) # The clipping object is usually set to the Polygon -pc2.addPaths(offsetSquare, pyclipr.Clip, False) +pc2.addPaths(offsetOut, pyclipr.Clip, False) """ Test the return types for open path clipping with option enabled. When The returnOpenPaths argument is set to True @@ -100,7 +107,7 @@ plt.fill(pathPoly[:, 0], pathPoly[:, 1], 'b', alpha=0.1, linewidth=1.0, linestyle='dashed', edgecolor='#000') # Plot the offset square -plt.fill(offsetSquare[0][:, 0], offsetSquare[0][:, 1], +plt.fill(offsetOut[0][:, 0], offsetOut[0][:, 1], linewidth=1.0, linestyle='dashed', edgecolor='#333', facecolor='none') # Plot the intersection diff --git a/python/pyclipr/module.cpp b/python/pyclipr/module.cpp index 07b2aae..99b8944 100644 --- a/python/pyclipr/module.cpp +++ b/python/pyclipr/module.cpp @@ -27,7 +27,6 @@ typedef Eigen::Matrix EigenVec2d; typedef Eigen::Matrix EigenVec3d; - static void myZCB(const Clipper2Lib::Point64& e1bot, const Clipper2Lib::Point64& e1top, const Clipper2Lib::Point64& e2bot, const Clipper2Lib::Point64& e2top, Clipper2Lib::Point64& pt) { @@ -112,7 +111,7 @@ void applyScaleFactor(const Clipper2Lib::PolyPath64 & polyPath, { Clipper2Lib::Path64 path = polyPath[i]->Polygon(); Clipper2Lib::PolyPathD pathD; - newPath.SetScale(1.0/scaleFactor); + newPath.SetScale(1.0 / double(scaleFactor)); auto newChild = newPath.AddChild(path); @@ -194,8 +193,8 @@ class Clipper : public Clipper2Lib::Clipper64 { EigenVec1d eigPathZ(path.size(), 1); for (uint64_t i=0; i < path.size(); i++) { - eigPath(i,0) = double(path[i].x) / scaleFactor; - eigPath(i,1) = double(path[i].y) / scaleFactor; + eigPath(i,0) = double(path[i].x) / double(scaleFactor); + eigPath(i,1) = double(path[i].y) / double(scaleFactor); if(returnZ) eigPathZ(i, 0) = path[i].z; @@ -221,8 +220,8 @@ class Clipper : public Clipper2Lib::Clipper64 { EigenVec2d eigPath(path.size(), 2); for (uint64_t i=0; i(); - if(path.shape(1) == 2) { for(uint64_t i=0; i < path.shape(0); i++) p.push_back(Clipper2Lib::Point64(r(i,0) * scaleFactor, r(i,1) * scaleFactor)); @@ -340,9 +338,35 @@ class ClipperOffset : public Clipper2Lib::ClipperOffset { const Clipper2Lib::EndType endType = Clipper2Lib::EndType::Polygon) { - for(auto path : paths) - addPath(path, joinType, endType); + std::vector closedPaths; + + for(auto path : paths) { + + Clipper2Lib::Path64 p; + + if (path.ndim() != 2) + throw std::runtime_error("Number of dimensions must be two"); + + if (!(path.shape(1) == 2 || path.shape(1) == 3)) + throw std::runtime_error("Path must be nx2, or nx3"); + // Resize the path list + p.reserve(path.shape(0)); + + auto r = path.unchecked<2>(); + + if(path.shape(1) == 2) { + for(uint64_t i=0; i < path.shape(0); i++) + p.push_back(Clipper2Lib::Point64(r(i,0) * scaleFactor, r(i,1) * scaleFactor)); + } else { + for(uint64_t i=0; i < path.shape(0); i++) + p.push_back(Clipper2Lib::Point64(r(i,0) * scaleFactor, r(i,1) * scaleFactor, r(i,2))); + } + + closedPaths.push_back(p); + + } + this->AddPaths(closedPaths, joinType, endType); } void clear() { this->Clear(); } @@ -360,8 +384,8 @@ class ClipperOffset : public Clipper2Lib::ClipperOffset { EigenVec2d eigPath(path.size(), 2); for (uint64_t i=0; i