Skip to content

Commit 4606dc9

Browse files
authored
Add Cone primitive (#257)
* add Cone primitive * add tests * fix docs ci * fix normals * update test * clean up some code duplication * add comment on nvertices choice * add docs * fix typos
1 parent 290e220 commit 4606dc9

File tree

6 files changed

+205
-15
lines changed

6 files changed

+205
-15
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
token: ${{ secrets.CODECOV_TOKEN }}
4444
docs:
4545
name: Documentation
46-
runs-on: ubuntu-20.04
46+
runs-on: ubuntu-latest
4747
env:
4848
JULIA_PKG_SERVER: ""
4949
steps:

docs/src/primitives.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ GeometryBasics comes with a few predefined primitives:
99

1010
#### HyperRectangle
1111

12-
A `Rect{D, T} = HyperRectangle{D, T}` is a D-dimensional axis-aligned
12+
A `Rect{D, T} = HyperRectangle{D, T}` is a D-dimensional axis-aligned
1313
hyperrectangle defined by an origin and a size.
1414

1515
```@repl rects
@@ -33,7 +33,7 @@ Shorthands:
3333

3434
#### Sphere and Circle
3535

36-
`Circle` and `Sphere` are the 2 and 3 dimensional variants of `HyperSphere`.
36+
`Circle` and `Sphere` are the 2 and 3 dimensional variants of `HyperSphere`.
3737
They are defined by an origin and a radius.
3838
While you can technically create a HyperSphere of any dimension, decomposition
3939
is only defined in 2D and 3D.
@@ -54,23 +54,34 @@ The coordinates of Circle are defined in anti-clockwise order.
5454

5555
A `Cylinder` is a 3D shape defined by two points and a radius.
5656

57-
5857
```@setup cylinder
5958
using GeometryBasics
6059
```
6160
```@repl cylinder
6261
c = Cylinder(Point3f(-1, 0, 0), Point3f(0, 0, 1), 0.3f0) # start point, end point, radius
6362
```
6463

65-
Cylinder supports normals an Tessellation, but currently no texture coordinates.
64+
Cylinder supports normals and Tessellation, but currently no texture coordinates.
65+
66+
#### Cone
67+
68+
A `Cone` is also defined by two points and a radius, but the radius decreases to 0 from the start point to the tip.
69+
70+
```@setup cone
71+
using GeometryBasics
72+
```
73+
```@repl cone
74+
c = Cone(Point3f(-1, 0, 0), Point3f(0, 0, 1), 0.3f0) # start point, tip point, radius
75+
```
76+
77+
Cone supports normals and Tessellation, but currently no texture coordinates.
6678

6779
#### Pyramid
6880

6981
`Pyramid` corresponds to a pyramid shape with a square base and four triangles
7082
coming together into a sharp point.
7183
It is defined by by the center point of the base, its height and its width.
7284

73-
7485
```@setup pyramid
7586
using GeometryBasics
7687
```
@@ -132,7 +143,7 @@ end
132143
```
133144

134145
To connect these points into a mesh, we need to generate a set of faces.
135-
The faces of a parallelepiped are parallelograms, which we can describe with `QuadFace`.
146+
The faces of a parallelepiped are parallelograms, which we can describe with `QuadFace`.
136147
Here we should be conscious of the winding direction of faces.
137148
They are often used to determine the front vs the backside of a (2D) face.
138149
For example GeometryBasics normal generation and OpenGL's backface culling assume a counter-clockwise winding direction to correspond to a front-facing face.
@@ -187,7 +198,7 @@ function GeometryBasics.texturecoordinates(::Parallelepiped{T}) where {T}
187198
uvs = [Vec2f(x, y) for x in range(0, 1, length=4) for y in range(0, 1, 3)]
188199
fs = QuadFace{Int}[
189200
(1, 2, 5, 4), (2, 3, 6, 5),
190-
(4, 5, 8, 7), (5, 6, 9, 8),
201+
(4, 5, 8, 7), (5, 6, 9, 8),
191202
(7, 8, 11, 10), (8, 9, 12, 11)
192203
]
193204
return FaceView(uvs, fs)

src/GeometryBasics.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ include("primitives/spheres.jl")
1717
include("primitives/cylinders.jl")
1818
include("primitives/pyramids.jl")
1919
include("primitives/particles.jl")
20+
include("primitives/Cone.jl")
2021

2122
include("interfaces.jl")
2223
include("viewtypes.jl")
@@ -57,7 +58,7 @@ export triangle_mesh, triangle_mesh, uv_mesh
5758
export uv_mesh, normal_mesh, uv_normal_mesh
5859

5960
export height, origin, radius, width, widths
60-
export HyperSphere, Circle, Sphere
61+
export HyperSphere, Circle, Sphere, Cone
6162
export Cylinder, Pyramid, extremity
6263
export HyperRectangle, Rect, Rect2, Rect3, Recti, Rect2i, Rect3i, Rectf, Rect2f, Rect3f, Rectd, Rect2d, Rect3d, RectT
6364
export before, during, meets, overlaps, intersects, finishes

src/primitives/Cone.jl

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""
2+
Cone{T}(origin::Point3, tip::Point3, radius)
3+
4+
A Cone is a cylinder where one end has a radius of 0. It is defined by an
5+
`origin` with a finite `radius` which linearly decreases to 0 at the `tip`.
6+
"""
7+
struct Cone{T} <: GeometryPrimitive{3, T}
8+
origin::Point3{T}
9+
tip::Point3{T}
10+
radius::T
11+
end
12+
13+
function Cone(origin::Point3{T1}, tip::Point3{T2}, radius::T3) where {T1, T2, T3}
14+
T = promote_type(T1, T2, T3)
15+
return Cone{T}(origin, tip, radius)
16+
end
17+
18+
origin(c::Cone) = c.origin
19+
extremity(c::Cone) = c.tip
20+
radius(c::Cone) = c.radius
21+
height(c::Cone) = norm(c.tip - c.origin)
22+
direction(c::Cone) = (c.tip .- c.origin) ./ height(c)
23+
24+
# Note:
25+
# nvertices is matched with Cylinder, where each end has half the vertices. That
26+
# results in less than nvertices for Cone, but allows a Cylinder and a Cone to
27+
# be seamless matched with the same `nvertices`
28+
29+
function coordinates(c::Cone{T}, nvertices=30) where {T}
30+
nvertices += isodd(nvertices)
31+
nhalf = div(nvertices, 2)
32+
33+
R = cylinder_rotation_matrix(direction(c))
34+
step = 2pi / nhalf
35+
36+
ps = Vector{Point3{T}}(undef, nhalf + 2)
37+
for i in 1:nhalf
38+
phi = (i-1) * step
39+
ps[i] = R * Point3{T}(c.radius * cos(phi), c.radius * sin(phi), 0) + c.origin
40+
end
41+
ps[end-1] = c.tip
42+
ps[end] = c.origin
43+
44+
return ps
45+
end
46+
47+
function normals(c::Cone, nvertices = 30)
48+
nvertices += isodd(nvertices)
49+
nhalf = div(nvertices, 2)
50+
51+
R = cylinder_rotation_matrix(direction(c))
52+
step = 2pi / nhalf
53+
54+
ns = Vector{Vec3f}(undef, nhalf + 2)
55+
# shell at origin
56+
# normals are angled in z direction due to change in radius (from radius to 0)
57+
# This can be calculated from triangles
58+
z = radius(c) / height(c)
59+
norm = 1.0 / sqrt(1 + z*z)
60+
for i in 1:nhalf
61+
phi = (i-1) * step
62+
ns[i] = R * (norm * Vec3f(cos(phi), sin(phi), z))
63+
end
64+
65+
# tip - this is undefined / should be all ring angles at once
66+
# for rendering it is useful to define this as Vec3f(0), because tip normal
67+
# has no useful value to contribute to the interpolated fragment normal
68+
ns[end-1] = Vec3f(0)
69+
70+
# cap
71+
ns[end] = Vec3f(normalize(c.origin - c.tip))
72+
73+
faces = Vector{GLTriangleFace}(undef, nvertices)
74+
75+
# shell
76+
for i in 1:nhalf
77+
faces[i] = GLTriangleFace(i, mod1(i+1, nhalf), nhalf+1)
78+
end
79+
80+
# cap
81+
for i in 1:nhalf
82+
faces[i+nhalf] = GLTriangleFace(nhalf + 2)
83+
end
84+
85+
return FaceView(ns, faces)
86+
end
87+
88+
function faces(::Cone, facets=30)
89+
nvertices = facets + isodd(facets)
90+
nhalf = div(nvertices, 2)
91+
92+
faces = Vector{GLTriangleFace}(undef, nvertices)
93+
94+
# shell
95+
for i in 1:nhalf
96+
faces[i] = GLTriangleFace(i, mod1(i+1, nhalf), nhalf+1)
97+
end
98+
99+
# cap
100+
for i in 1:nhalf
101+
faces[i+nhalf] = GLTriangleFace(i, mod1(i+1, nhalf), nhalf+2)
102+
end
103+
104+
return faces
105+
end

src/primitives/cylinders.jl

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,15 @@ radius(c::Cylinder) = c.r
2121
height(c::Cylinder) = norm(c.extremity - c.origin)
2222
direction(c::Cylinder) = (c.extremity .- c.origin) ./ height(c)
2323

24-
function rotation(c::Cylinder{T}) where {T}
25-
d3 = direction(c)
24+
"""
25+
cylinder_rotation_matrix(direction::VecTypes{3})
26+
27+
Creates a basis transformation matrix `R` that maps the third dimension to the
28+
given `direction` and the first and second to orthogonal directions. This allows
29+
you to encode a rotation around `direction` in the first two components and
30+
transform it with `R * rotated_point`.
31+
"""
32+
function cylinder_rotation_matrix(d3::VecTypes{3, T}) where {T}
2633
u = Vec{3, T}(d3[1], d3[2], d3[3])
2734
if abs(u[1]) > 0 || abs(u[2]) > 0
2835
v = Vec{3, T}(u[2], -u[1], T(0))
@@ -39,9 +46,9 @@ function coordinates(c::Cylinder{T}, nvertices=30) where {T}
3946
nvertices += isodd(nvertices)
4047
nhalf = div(nvertices, 2)
4148

42-
R = rotation(c)
49+
R = cylinder_rotation_matrix(direction(c))
4350
step = 2pi / nhalf
44-
51+
4552
ps = Vector{Point3{T}}(undef, nvertices + 2)
4653
for i in 1:nhalf
4754
phi = (i-1) * step
@@ -61,9 +68,9 @@ function normals(c::Cylinder, nvertices = 30)
6168
nvertices += isodd(nvertices)
6269
nhalf = div(nvertices, 2)
6370

64-
R = rotation(c)
71+
R = cylinder_rotation_matrix(direction(c))
6572
step = 2pi / nhalf
66-
73+
6774
ns = Vector{Vec3f}(undef, nhalf + 2)
6875
for i in 1:nhalf
6976
phi = (i-1) * step

test/geometrytypes.jl

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,4 +696,70 @@ end
696696
@test all(getindex.(Ref(mp), 1:10) .== ps1)
697697
@test size(mp) == (10, ) # TODO: Does this make sense?
698698
@test length(mp) == 10
699+
end
700+
701+
@testset "Cone" begin
702+
@testset "constructors" begin
703+
v1 = rand(Point{3,Float64})
704+
v2 = rand(Point{3,Float64})
705+
R = rand()
706+
s = Cone(v1, v2, R)
707+
@test typeof(s) == Cone{Float64}
708+
@test origin(s) == v1
709+
@test extremity(s) == v2
710+
@test radius(s) == R
711+
@test height(s) == norm(v2 - v1)
712+
@test isapprox(direction(s), (v2 - v1) ./ norm(v2 .- v1))
713+
end
714+
715+
@testset "decompose" begin
716+
v1 = Point{3,Float64}(1, 2, 3)
717+
v2 = Point{3,Float64}(4, 5, 6)
718+
R = 5.0
719+
s = Cone(v1, v2, R)
720+
positions = Point{3,Float64}[
721+
(4.535533905932738, -1.5355339059327373, 3.0),
722+
(3.0412414523193148, 4.041241452319315, -1.0824829046386295),
723+
(-2.535533905932737, 5.535533905932738, 2.9999999999999996),
724+
(-1.0412414523193152, -0.04124145231931431, 7.0824829046386295),
725+
(4, 5, 6),
726+
(1, 2, 3)
727+
]
728+
729+
@test decompose(Point3{Float64}, Tessellation(s, 8)) positions
730+
731+
_faces = TriangleFace[
732+
(1,2,5), (2,3,5), (3,4,5), (4,1,5),
733+
(1,2,6), (2,3,6), (3,4,6), (4,1,6)]
734+
735+
@test _faces == decompose(TriangleFace{Int}, Tessellation(s, 8))
736+
737+
m = triangle_mesh(Tessellation(s, 8))
738+
@test m === triangle_mesh(m)
739+
@test GeometryBasics.faces(m) == decompose(GLTriangleFace, _faces)
740+
@test GeometryBasics.coordinates(m) positions
741+
742+
m = normal_mesh(s) # just test that it works without explicit resolution parameter
743+
@test hasproperty(m, :position)
744+
@test hasproperty(m, :normal)
745+
@test faces(m) isa AbstractVector{GLTriangleFace}
746+
747+
ns = Vec{3, Float32}[
748+
(0.90984505, -0.10920427, 0.40032038),
749+
(0.6944946, 0.6944946, -0.18802801),
750+
(-0.10920427, 0.90984505, 0.40032038),
751+
(0.106146194, 0.106146194, 0.9886688),
752+
(0.0, 0.0, 0.0),
753+
(-0.57735026, -0.57735026, -0.57735026),
754+
]
755+
fs = [
756+
GLTriangleFace(1, 2, 5), GLTriangleFace(2, 3, 5), GLTriangleFace(3, 4, 5), GLTriangleFace(4, 1, 5),
757+
GLTriangleFace(6, 6, 6), GLTriangleFace(6, 6, 6), GLTriangleFace(6, 6, 6), GLTriangleFace(6, 6, 6)
758+
]
759+
760+
@test FaceView(ns, fs) == decompose_normals(Tessellation(s, 8))
761+
762+
muv = uv_mesh(s)
763+
@test !hasproperty(muv, :uv) # not defined yet
764+
end
699765
end

0 commit comments

Comments
 (0)