Skip to content

Commit 6fbcb76

Browse files
committed
👨‍🌾 Repo initialization
0 parents  commit 6fbcb76

12 files changed

Lines changed: 386 additions & 0 deletions

File tree

.github/workflows/TagBot.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: TagBot
2+
on:
3+
schedule:
4+
- cron: "0 0 * * *" # run once a day
5+
issue_comment:
6+
types:
7+
- created
8+
workflow_dispatch:
9+
jobs:
10+
TagBot:
11+
if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot'
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: JuliaRegistries/TagBot@v1
15+
with:
16+
token: ${{ secrets.GITHUB_TOKEN }}
17+
ssh: ${{ secrets.DOCUMENTER_KEY }}

.github/workflows/ci.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: CI
2+
on:
3+
schedule:
4+
- cron: '0 2 * * *' # Daily at 2 AM UTC (8 PM CST)
5+
push:
6+
branches: [main]
7+
tags: ["*"]
8+
pull_request:
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }}
12+
jobs:
13+
test:
14+
name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }}
15+
runs-on: ${{ matrix.os }}
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
version:
20+
- "1" # Latest Release
21+
os:
22+
- ubuntu-latest
23+
- windows-latest
24+
arch:
25+
- x64
26+
include:
27+
- os: macos-latest
28+
arch: aarch64
29+
version: 1
30+
steps:
31+
- uses: actions/checkout@v4
32+
- uses: julia-actions/setup-julia@v2
33+
with:
34+
version: ${{ matrix.version }}
35+
arch: ${{ matrix.arch }}
36+
- uses: actions/cache@v4
37+
env:
38+
cache-name: cache-artifacts
39+
with:
40+
path: ~/.julia/artifacts
41+
key: ${{ runner.os }}-${{ matrix.arch }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }}
42+
restore-keys: |
43+
${{ runner.os }}-${{ matrix.arch }}-test-${{ env.cache-name }}-
44+
${{ runner.os }}-${{ matrix.arch }}-test-
45+
${{ runner.os }}-${{ matrix.arch }}-
46+
${{ runner.os }}-
47+
- uses: julia-actions/julia-buildpkg@latest
48+
- run: |
49+
git config --global user.name Tester
50+
git config --global user.email te@st.er
51+
- uses: julia-actions/julia-runtest@latest

.gitignore

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
.vscode
2+
# Files generated by invoking Julia with --code-coverage
3+
*.jl.cov
4+
*.jl.*.cov
5+
6+
# Files generated by invoking Julia with --track-allocation
7+
*.jl.mem
8+
9+
# Files generated by MacOS
10+
*.Ds_Store
11+
12+
# System-specific files and directories generated by the BinaryProvider and BinDeps packages
13+
# They contain absolute paths specific to the host computer, and so should not be committed
14+
deps/deps.jl
15+
deps/build.log
16+
deps/downloads/
17+
deps/usr/
18+
deps/src/
19+
20+
# Build artifacts for creating documentation generated by the Documenter package
21+
docs/build/
22+
docs/site/
23+
24+
# File generated by Pkg, the package manager, based on a corresponding Project.toml
25+
# It records a fixed state of all packages used by the project. As such, it should not be
26+
# committed for packages, but should be committed for applications that require a static
27+
# environment.
28+
Manifest.toml

CondaPkg.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
channels = ["conda-forge", "anaconda"]
2+
3+
[deps]
4+
python = ">=3.12,<3.13"
5+
numpy = ">=2.2"
6+
shapely = ">=2.1"
7+
open3d = ">=0.19"
8+
trimesh = ">=4.6"

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Zenan Huo
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Project.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name = "FastPointQuery"
2+
uuid = "b325ad68-3eea-4afa-b43e-4b8e378fd76d"
3+
authors = ["Zenan Huo <zenan.huo@outlook.com>"]
4+
version = "0.1.0"
5+
6+
[deps]
7+
CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab"
8+
LibGEOS = "a90b1aa1-3769-5649-ba7e-abc5a9d163eb"
9+
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
10+
PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d"
11+
12+
[compat]
13+
CondaPkg = "0.2"
14+
LibGEOS = "0.9"
15+
PythonCall = "0.9"
16+
julia = "1.11"
17+
18+
[extras]
19+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
20+
21+
[targets]
22+
test = ["Test"]

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# ***FastPointQuery***
2+
3+
[![CI](https://github.com/LandslideSIM/FastPIP.jl/actions/workflows/ci.yml/badge.svg)](https://github.com/LandslideSIM/FastPointQuery.jl/actions/workflows/ci.yml)
4+
[![Version](https://img.shields.io/badge/version-v0.1.0-pink)]()
5+
6+
7+
> This is a dependency package for [MaterialPointGenerator.jl](https://github.com/LandslideSIM/MaterialPointGenerator.jl), used to separate Python calls for easier management.
8+
9+
## Installation ⚙️
10+
11+
Just type <kbd>]</kbd> in Julia's `REPL`:
12+
13+
```julia
14+
julia> ]
15+
(@1.11) Pkg> add FastPIP
16+
```
17+
18+
## Features ✨
19+
20+
- [x] point-in-polygon
21+
- [x] point-in-polyhedron
22+
- [x] surface normal vectors
23+
24+
## Acknowledgement 👍
25+
26+
This project is sponserd by [Risk Group | Université de Lausanne](https://wp.unil.ch/risk/) and [China Scholarship Council [中国国家留学基金管理委员会]](https://www.csc.edu.cn/).

assets/test2d.stl

11.9 KB
Binary file not shown.

src/FastPointQuery.jl

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
module FastPointQuery
2+
3+
using CondaPkg, LibGEOS, Printf, PythonCall
4+
5+
const np = PythonCall.pynew()
6+
const shapely = PythonCall.pynew()
7+
const o3d = PythonCall.pynew()
8+
const trimesh = PythonCall.pynew()
9+
10+
function __init__()
11+
@info "initializing environment..."
12+
try # import Python modules
13+
PythonCall.pycopy!(np , PythonCall.pyimport("numpy" ))
14+
PythonCall.pycopy!(shapely, PythonCall.pyimport("shapely"))
15+
PythonCall.pycopy!(o3d , PythonCall.pyimport("open3d" ))
16+
PythonCall.pycopy!(trimesh, PythonCall.pyimport("trimesh"))
17+
catch e
18+
@error "Failed to initialize Python ENV" exception=e
19+
end
20+
end
21+
22+
include(joinpath(@__DIR__, "polygon.jl"))
23+
24+
# export structs
25+
export QueryPolygon
26+
# export functions
27+
export get_polygon, pip_query
28+
29+
end

src/polygon.jl

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#==========================================================================================++
2+
| TABLE OF CONTENTS: |
3+
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4+
| struct list: |
5+
| - QueryPolygon |
6+
+------------------------------------------------------------------------------------------+
7+
| function list: |
8+
| - get_polygon (from LibGEOS, need to improve) |
9+
| - pip_query |
10+
+==========================================================================================#
11+
12+
struct QueryPolygon
13+
ju_xy::AbstractMatrix
14+
py_xy::Py
15+
function QueryPolygon(ju_xy::AbstractMatrix, py_xy::Py)
16+
n, m = size(ju_xy)
17+
m >= 3 || error("at least 3 points are required")
18+
n == 2 || error("points must be 2D (2×N array)")
19+
new(ju_xy, py_xy)
20+
end
21+
end
22+
23+
"""
24+
get_polygon(pts; ratio=0.1)
25+
26+
Description:
27+
---
28+
Get the polygon from the given points. `pts` is a 2xN array, and `ratio` is the
29+
parameter for the concave hull. The default value is 0.1.
30+
"""
31+
function get_polygon(points::AbstractArray; ratio::Real=0.1)
32+
# inputs checking
33+
n, m = size(points)
34+
n == 2 || error("points must be a 2xN array")
35+
m >= 3 || error("at least 3 points are required")
36+
ratio > 0 || error("ratio must be positive")
37+
allow_holes = 0
38+
pts = Array{Float64}(points)
39+
40+
# convert to GEOS datatype
41+
gpts = LibGEOS.MultiPoint([pts[:, i] for i in axes(pts, 2)])
42+
ctx = LibGEOS.get_context(gpts)
43+
ptr = LibGEOS.GEOSConcaveHullByLength_r(ctx, gpts, ratio, allow_holes)
44+
conc = LibGEOS.geomFromGEOS(ptr)
45+
conc == C_NULL && error("LibGEOS: Error in GEOSConvexHull")
46+
47+
# convert back to julia datatype
48+
ob_outer = LibGEOS.exteriorRing(conc)
49+
cs_outer = LibGEOS.getCoordSeq(ob_outer)
50+
xs_outer = LibGEOS.getX(cs_outer)
51+
ys_outer = LibGEOS.getY(cs_outer)
52+
xy = vcat(permutedims(xs_outer), permutedims(ys_outer))
53+
54+
return QueryPolygon(xy, shapely.Polygon(xy'))
55+
end
56+
57+
"""
58+
pip_query(polygon::QueryPolygon, px::Real, py::Real; edge::Bool=false)
59+
60+
Description:
61+
---
62+
Determine whether a point [px, py] is inside a polygon. Note to use the function
63+
`get_polygon` to get it; otherwise, it may lead to incorrect results. If the point is on the
64+
edge of the polygon, you can set `edge=true` to check if it is considered as inside.
65+
66+
Example:
67+
---
68+
```julia
69+
polypon_xy = [0 0; 1 0; 1 1; 0 1]' # 2xN array
70+
polygon = get_polygon(polygon_xy, ratio=1) # polygon.py_xy to visualize
71+
particle_in_polygon(polygon, 0, 0) # check points (0, 0) and return false
72+
particle_in_polygon(polygon, 0, 0, edge=true) # check points (0, 0) and return true
73+
```
74+
"""
75+
function pip_query(
76+
polygon::QueryPolygon,
77+
px ::Real,
78+
py ::Real;
79+
edge ::Bool=false
80+
)
81+
func = edge ? shapely.intersects_xy : shapely.contains_xy
82+
return pyconvert(Bool, func(polygon.py_xy, px, py).item())
83+
end
84+
85+
"""
86+
pip_query(polygon::QueryPolygon, points::AbstractMatrix; edge::Bool=false)
87+
88+
Description:
89+
---
90+
Determine whether a set of points is inside a polygon. The input `points` should be a 2xN
91+
array, where each column represents a point (x, y). If `edge` is set to true, it checks if
92+
the points are on the edge of the polygon as well.
93+
94+
Example:
95+
```julia
96+
polygon_xy = [0 0; 1 0; 1 1; 0 1]' # 2xN array
97+
polygon = get_polygon(polygon_xy, ratio=1) # polygon.py_xy to visualize
98+
pip_query(polygon, [0 0; 0.5 0.5; 1 1], edge=true) # returns [true, true, true]
99+
"""
100+
function pip_query(
101+
polygon::QueryPolygon,
102+
points ::AbstractMatrix;
103+
edge ::Bool=false
104+
)
105+
# inputs check for points
106+
n, m = size(points)
107+
m 1 || error("points should have at least 1 point")
108+
n == 2 || error("points should be a 2xN array")
109+
py_points = shapely.points(points')
110+
111+
# check if the particle is inside the polygon
112+
if edge
113+
mask = shapely.covers(polygon.py_xy, py_points)
114+
else
115+
mask = shapely.within(py_points, polygon.py_xy)
116+
end
117+
118+
return pyconvert(Vector{Bool}, mask)
119+
end
120+
121+
"""
122+
pip_query(stl_file::String, points::AbstractMatrix; edge::Bool=false)
123+
124+
Description:
125+
---
126+
Determine whether a set of points is inside a 3D mesh defined by an STL file. The input
127+
`stl_file` should be a valid file path to an STL file, and `points` should be a 2xN array
128+
where each column represents a point (x, y). If `edge` is set to true, it checks if the
129+
points are on the edge of the mesh as well.
130+
131+
Example:
132+
```julia
133+
stl_file = "path/to/your/file.stl"
134+
points = [0 0; 0.5 0.5; 1 1]'
135+
pip_query(stl_file, points, edge=true)
136+
"""
137+
function pip_query(
138+
stl_file::String,
139+
points ::AbstractMatrix;
140+
edge ::Bool=false
141+
)
142+
isfile(stl_file) || error("stl_file must be a valid file path")
143+
size(points, 1) == 2 || error("points must be a 2xN array")
144+
py_points = shapely.points(points')
145+
if edge
146+
@pyexec """
147+
def py_pip(stl_file, py_points, trimesh, shapely):
148+
mesh = trimesh.load(stl_file, force="mesh")
149+
tris2d = mesh.triangles[:, :, :2]
150+
region = shapely.unary_union([shapely.Polygon(t) for t in tris2d])
151+
mask = shapely.covers(region, py_points)
152+
return mask
153+
""" => py_pip
154+
else
155+
@pyexec """
156+
def py_pip(stl_file, py_points, trimesh, shapely):
157+
mesh = trimesh.load(stl_file, force="mesh")
158+
tris2d = mesh.triangles[:, :, :2]
159+
region = shapely.unary_union([shapely.Polygon(t) for t in tris2d])
160+
mask = shapely.within(py_points, region)
161+
return mask
162+
""" => py_pip
163+
end
164+
tmp = py_pip(stl_file, py_points, trimesh, shapely)
165+
return pyconvert(Vector{Bool}, tmp)
166+
end

0 commit comments

Comments
 (0)