From be5c6ba9896f66de2bae08fd5cdf12f7c8a50279 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Tue, 22 Feb 2022 14:40:03 +0100 Subject: [PATCH 01/58] get rid of StaticArrays --- Project.toml | 1 + src/GeometryBasics.jl | 21 +-- src/basic_types.jl | 18 +-- src/deprecated.jl | 30 ---- src/fixed_arrays.jl | 258 ++++++++++++++++++++++++----------- src/lines.jl | 2 +- src/mat.jl | 157 +++++++++++++++++++++ src/meshes.jl | 75 +--------- src/offsetintegers.jl | 2 +- src/precompile.jl | 39 ------ src/primitives/cylinders.jl | 22 +-- src/primitives/rectangles.jl | 4 +- src/viewtypes.jl | 2 +- test/geometrytypes.jl | 1 - test/runtests.jl | 35 ++--- 15 files changed, 377 insertions(+), 290 deletions(-) delete mode 100644 src/deprecated.jl create mode 100644 src/mat.jl delete mode 100644 src/precompile.jl diff --git a/Project.toml b/Project.toml index c9651cda..5055bc48 100644 --- a/Project.toml +++ b/Project.toml @@ -7,6 +7,7 @@ version = "0.4.1" EarCut_jll = "5ae413db-bbd1-5e63-b57d-d24a61df00f5" IterTools = "c8e1da08-722c-5040-9ed9-7db0dc04731e" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index fb60f3a8..2fd0e12c 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -1,10 +1,12 @@ module GeometryBasics -using StaticArrays, Tables, StructArrays, IterTools, LinearAlgebra +using Tables, StructArrays, IterTools, LinearAlgebra using EarCut_jll +import Base: * using Base: @propagate_inbounds +include("mat.jl") include("fixed_arrays.jl") include("offsetintegers.jl") include("basic_types.jl") @@ -24,8 +26,6 @@ include("triangulation.jl") include("lines.jl") include("boundingboxes.jl") -include("deprecated.jl") - export AbstractGeometry, GeometryPrimitive export Mat, Point, Vec export LineFace, Polytope, Line, NgonFace, convert_simplex @@ -43,18 +43,12 @@ export PolygonMeta, MultiPointMeta, MultiLineStringMeta, MeshMeta, LineStringMet export decompose, coordinates, faces, normals, decompose_uv, decompose_normals, texturecoordinates export Tesselation, pointmeta, Normal, UV, UVW -export GLTriangleFace, GLNormalMesh3D, GLPlainTriangleMesh, GLUVMesh3D, GLUVNormalMesh3D -export AbstractMesh, Mesh, TriangleMesh -export GLNormalMesh2D, PlainTriangleMesh +export AbstractMesh, Mesh export MetaT, meta_table # all the different predefined mesh types # Note: meshes can contain arbitrary meta information, -export AbstractMesh, TriangleMesh, PlainMesh, GLPlainMesh, GLPlainMesh2D, GLPlainMesh3D -export UVMesh, GLUVMesh, GLUVMesh2D, GLUVMesh3D -export NormalMesh, GLNormalMesh, GLNormalMesh2D, GLNormalMesh3D -export NormalUVMesh, GLNormalUVMesh, GLNormalUVMesh2D, GLNormalUVMesh3D -export NormalUVWMesh, GLNormalUVWMesh, GLNormalUVWMesh2D, GLNormalUVWMesh3D +export AbstractMesh, TriangleMesh, PlainMesh # mesh creation functions export triangle_mesh, triangle_mesh, uv_mesh @@ -70,9 +64,4 @@ export max_dist_dim, max_euclidean, max_euclideansq, min_dist_dim, min_euclidean export min_euclideansq, minmax_dist_dim, minmax_euclidean, minmax_euclideansq export self_intersections, split_intersections -if Base.VERSION >= v"1.4.2" - include("precompile.jl") - _precompile_() -end - end # module diff --git a/src/basic_types.jl b/src/basic_types.jl index 16aa8047..38b4238d 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -13,26 +13,20 @@ Note That `Polytope{N} where N == 3` denotes a Triangle both as a Simplex or Ngo abstract type Polytope{Dim,T} <: AbstractGeometry{Dim,T} end abstract type AbstractPolygon{Dim,T} <: Polytope{Dim,T} end -abstract type AbstractPoint{Dim,T} <: StaticVector{Dim,T} end abstract type AbstractFace{N,T} <: StaticVector{N,T} end abstract type AbstractSimplexFace{N,T} <: AbstractFace{N,T} end abstract type AbstractNgonFace{N,T} <: AbstractFace{N,T} end abstract type AbstractSimplex{Dim,N,T} <: StaticVector{Dim,T} end -""" -Face index, connecting points to form a simplex -""" @fixed_vector SimplexFace AbstractSimplexFace + const TetrahedronFace{T} = SimplexFace{4,T} Face(::Type{<:SimplexFace{N}}, ::Type{T}) where {N,T} = SimplexFace{N,T} -""" -Face index, connecting points to form an Ngon -""" - @fixed_vector NgonFace AbstractNgonFace + const LineFace{T} = NgonFace{2,T} const TriangleFace{T} = NgonFace{3,T} const QuadFace{T} = NgonFace{4,T} @@ -58,14 +52,14 @@ Fixed Size Polygon, e.g. """ struct Ngon{Dim,T<:Real,N,Point<:AbstractPoint{Dim,T}} <: AbstractPolygon{Dim,T} - points::SVector{N,Point} + points::Vec{N,Point} end const NNgon{N} = Ngon{Dim,T,N,P} where {Dim,T,P} function (::Type{<:NNgon{N}})(points::Vararg{P,N}) where {P<:AbstractPoint{Dim,T}, N} where {Dim,T} - return Ngon{Dim,T,N,P}(SVector(points)) + return Ngon{Dim,T,N,P}(Vec(points)) end Base.show(io::IO, x::NNgon{N}) where {N} = print(io, "Ngon{$N}(", join(x, ", "), ")") @@ -138,7 +132,7 @@ to allow embedding in higher-order spaces by parameterizing on `T`. """ struct Simplex{Dim,T<:Real,N,Point<:AbstractPoint{Dim,T}} <: Polytope{Dim,T} - points::SVector{N,Point} + points::Vec{N,Point} end const NSimplex{N} = Simplex{Dim,T,N,P} where {Dim,T,P} @@ -151,7 +145,7 @@ coordinates(x::Simplex) = x.points function (::Type{<:NSimplex{N}})(points::Vararg{P,N}) where {P<:AbstractPoint{Dim,T}, N} where {Dim,T} - return Simplex{Dim,T,N,P}(SVector(points)) + return Simplex{Dim,T,N,P}(Vec(points)) end # Base Array interface diff --git a/src/deprecated.jl b/src/deprecated.jl deleted file mode 100644 index 54336aa0..00000000 --- a/src/deprecated.jl +++ /dev/null @@ -1,30 +0,0 @@ -using Base: @deprecate_binding - -# Types ...f0 renamed to ...f -@deprecate_binding Vecf0 Vecf -@deprecate_binding Pointf0 Pointf -for i in 1:4 - for T in [:Point, :Vec] - oldname = Symbol("$T$(i)f0") - newname = Symbol("$T$(i)f") - @eval begin - @deprecate_binding $oldname $newname - end - end - oldname = Symbol("Mat$(i)f0") - newname = Symbol("Mat$(i)f") - @eval begin - @deprecate_binding $oldname $newname - end -end - -# Rect types -@deprecate_binding Rect2D Rect2 -@deprecate_binding Rect3D Rect3 -@deprecate_binding FRect Rectf -@deprecate_binding FRect2D Rect2f -@deprecate_binding FRect3D Rect3f -@deprecate_binding IRect Recti -@deprecate_binding IRect2D Rect2i -@deprecate_binding IRect3D Rect3i -@deprecate_binding TRect RectT diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index c312b829..90d27840 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -1,127 +1,220 @@ -function unit(::Type{T}, i::Integer) where {T <: StaticVector} - tup = ntuple(Val(length(T))) do j - return ifelse(i == j, 1, 0) - end - return T(tup) -end +using LinearAlgebra +import Random +import Base: setindex -macro fixed_vector(name, parent) +abstract type StaticVector{N, T} end + +macro fixed_vector(VecT, SuperT) expr = quote - struct $(name){S,T} <: $(parent){S,T} - data::NTuple{S,T} + struct $(VecT){N, T} <: $(SuperT){N, T} + data::NTuple{N,T} - function $(name){S,T}(x::NTuple{S,T}) where {S,T} - return new{S,T}(x) + function $(VecT){N,T}(x::NTuple{N,T}) where {N,T} + return new{N,T}(x) end - function $(name){S,T}(x::NTuple{S,Any}) where {S,T} - return new{S,T}(StaticArrays.convert_ntuple(T, x)) + function $(VecT){N,T}(x::NTuple{N,Any}) where {N,T} + return new{N,T}(convert(NTuple{N, T}, x)) end + $(VecT){1, T}(x::Tuple{T}) where {T} = new{1, T}(x) end - size_or(::Type{$(name)}, or) = or - eltype_or(::Type{$(name)}, or) = or - eltype_or(::Type{$(name){S,T} where S}, or) where {T} = T - eltype_or(::Type{$(name){S,T} where T}, or) where {S} = or - eltype_or(::Type{$(name){S,T}}, or) where {S,T} = T - - size_or(::Type{$(name){S,T} where S}, or) where {T} = or - size_or(::Type{$(name){S,T} where T}, or) where {S} = Size{(S,)}() - size_or(::Type{$(name){S,T}}, or) where {S,T} = (S,) - # Array constructor - function $(name){S}(x::AbstractVector{T}) where {S,T} - @assert S <= length(x) - return $(name){S,T}(ntuple(i -> x[i], Val(S))) + $(VecT)(x::AbstractVector) = error("You need to supply size of vector") + $(VecT){N}(x::AbstractVector{T}) where {N,T} = $(VecT){N,T}(x) + function $(VecT){N,T1}(x::AbstractVector{T2}) where {N,T1,T2} + @assert N <= length(x) + return $(VecT){N,T1}(ntuple(i -> convert(T1, x[i]), N)) end - function $(name){S,T1}(x::AbstractVector{T2}) where {S,T1,T2} - @assert S <= length(x) - return $(name){S,T1}(ntuple(i -> convert(T1, x[i]), Val(S))) + # StaticVector conversion + $(VecT)(x::StaticVector{N, T}) where {N,T} = $(VecT){N, T}(x) + $(VecT){N}(x::StaticVector{N2, T}) where {N,N2,T} = $(VecT){N,T}(x) + function $(VecT){N1,T1}(x::StaticVector{N2, T2}) where {N1,T1,N2,T2} + @assert N1 <= N2 + return $(VecT){N1,T1}(ntuple(i -> convert(T1, x[i]), N1)) end - - function $(name){S,T}(x) where {S,T} - return $(name){S,T}(ntuple(i -> convert(T, x), Val(S))) + function $(VecT){1,T1}(x::StaticVector{N2, T2}) where {T1,N2,T2} + @assert 1 <= N2 + return $(VecT){1,T1}(ntuple(i -> convert(T1, x[i]), 1)) end - $(name){S}(x::T) where {S,T} = $(name){S,T}(ntuple(i -> x, Val(S))) - $(name){1,T}(x::T) where {T} = $(name){1,T}((x,)) - $(name)(x::NTuple{S}) where {S} = $(name){S}(x) - function $(name)(x::T) where {S,T <: Tuple{Vararg{Any,S}}} - return $(name){S,StaticArrays.promote_tuple_eltype(T)}(x) + # repeat + $(VecT){N}(x::T) where {N,T<:Number} = $(VecT){N, T}(x) + function $(VecT){N,T}(x::Number) where {N,T} + return $(VecT){N,T}(ntuple(i -> convert(T, x), N)) end - function $(name){S}(x::T) where {S,T <: Tuple} - return $(name){S,StaticArrays.promote_tuple_eltype(T)}(x) - end - $(name){S,T}(x::StaticVector) where {S,T} = $(name){S,T}(Tuple(x)) + $(VecT){1, T}(x) where {T} = $(VecT){1, T}((x,)) + $(VecT){1, T}(x::Tuple{Any}) where T = $(VecT){1, T}((T(x[1]),)) + $(VecT)(x::Tuple) = $(VecT)(promote(x...)) + $(VecT){N}(x::Tuple) where {N} = $(VecT){N}(promote(x...)) + $(VecT){N, T}(x::Tuple) where {N,T} = $(VecT){N,T}(convert(NTuple{N,T}, x)) - @generated function (::Type{$(name){S,T}})(x::$(name)) where {S,T} - idx = [:(x[$i]) for i in 1:S] - return quote - $($(name)){S,T}($(idx...)) - end + $(VecT)(x::NTuple{N, T}) where {N, T} = $(VecT){N,T}(x) + $(VecT){N}(x::NTuple{N, T}) where {N, T} = $(VecT){N,T}(x) + + $(VecT)(x::Vararg{<:Any,N}) where {N} = $(VecT){N}(x) + $(VecT)(x::Vararg{T,N}) where {T,N} = $(VecT){N,T}(x) + + $(VecT){N}(x::Vararg{<:Any,N}) where {N} = $(VecT){N}(x) + $(VecT){N}(x::Vararg{T,N}) where {T,N} = $(VecT){N,T}(x) + + $(VecT){N, T}(x::Vararg{<:Any,N}) where {T,N} = $(VecT){N,T}(x) + $(VecT){N, T1}(x::Vararg{T2,N}) where {T1,T2,N} = $(VecT){N, T1}(x) + + Base.convert(::Type{$(VecT){N,T}}, x) where {N,T} = $(VecT){N,T}(x) + Base.convert(::Type{$(VecT){N}}, x) where {N} = $(VecT){N}(x) + Base.convert(::Type{$(VecT){N}}, x::$(VecT){N}) where {N} = x + Base.convert(::Type{$(VecT){N,T}}, x::$(VecT){N,T}) where {N,T} = x + + function Base.convert(::Type{$(VecT){N,T}}, x::NTuple{N,T}) where {N,T} + return $(VecT){N,T}(x) + end + function Base.convert(::Type{$(VecT){N,T}}, x::Tuple) where {N,T} + return $(VecT){N,T}(convert(NTuple{N,T}, x)) end + Base.convert(::Type{$(VecT)}, x::Tuple) = $(VecT)(x) - @generated function Base.convert(::Type{$(name){S,T}}, x::$(name)) where {S,T} - idx = [:(x[$i]) for i in 1:S] - return quote - $($(name)){S,T}($(idx...)) - end + Base.@propagate_inbounds function Base.getindex(v::$(VecT){N,T}, i::Int) where {N,T} + return v.data[i] + end + Base.setindex(c::$(VecT){N, T}, v, i::Integer) where {N,T} = $(VecT){N,T}(Base.setindex(c.data, v, i)) + Base.@propagate_inbounds function Base.getindex(a::AbstractArray{T}, idx::$(VecT){N, <:Integer}) where {N,T} + return $(VecT){N,T}(map(i-> a[i], idx)) end - @generated function (::Type{SV})(x::StaticVector) where {SV <: $(name)} - len = size_or(SV, size(x))[1] - return if length(x) == len - :(SV(Tuple(x))) - elseif length(x) > len - elems = [:(x[$i]) for i in 1:len] - :(SV($(Expr(:tuple, elems...)))) - else - error("Static Vector too short: $x, target type: $SV") - end + Base.@propagate_inbounds function Base.getindex(a::StaticVector{N1, T}, idx::$(VecT){N, <:Integer}) where {N,N1,T} + return $(VecT){N,T}(map(i-> a[i], idx)) end - Base.@pure StaticArrays.Size(::Type{$(name){S,Any}}) where {S} = Size(S) - Base.@pure StaticArrays.Size(::Type{$(name){S,T}}) where {S,T} = Size(S) + Base.Tuple(v::$(VecT)) = v.data - Base.@propagate_inbounds function Base.getindex(v::$(name){S,T}, i::Int) where {S,T} - return v.data[i] + + function Base.broadcasted(f, a::AbstractArray{T}, b::$(VecT)) where {T <: $(VecT)} + return Base.broadcasted(f, a, (b,)) end - Base.Tuple(v::$(name)) = v.data - function Base.convert(::Type{$(name){S,T}}, x::NTuple{S,T}) where {S,T} - return $(name){S,T}(x) + function Base.broadcasted(f, a::$(VecT), b::$(VecT)) + return $(VecT)(map(f, a.data, b.data)) end - function Base.convert(::Type{$(name){S,T}}, x::Tuple) where {S,T} - return $(name){S,T}(convert(NTuple{S,T}, x)) + + Base.broadcasted(f, a::$(VecT), b) = $(VecT)(f.(a.data, b)) + Base.broadcasted(f, a, b::$(VecT)) = $(VecT)(f.(a, b.data)) + + Base.map(f, b::$(VecT)) = $(VecT)(map(f, b.data)) + + (*)(a::Mat{M, N, T1}, b::$(VecT){O, T2}) where {T1, T2, M, N, O} = throw(DimensionMismatch("$N != $O in $(typeof(a)) and $(typeof(b))")) + + # vector * (row vector) + @generated function *(a::$VecT{N, T1}, b::Mat{1, M, T2}) where {T1, T2, N, M} + elements = Expr(:tuple, [Expr(:tuple, [:(a[$i] * b[$j]) for i in 1:N]...) for j in 1:M]...) + return :($($(VecT))($elements)) end - @generated function StaticArrays.similar_type(::Type{SV}, ::Type{T}, - s::Size{S}) where {SV <: $(name),T,S} - return if length(S) === 1 - $(name){S[1],T} + # matrix * vector + @generated function *(a::Mat{M, N, T1}, b::$VecT{N, T2}) where {T1, T2, M, N} + total_terms = M*N + if total_terms <= 64 + # Full unrolling + elements = Expr(:tuple, [Expr(:call, :+, [:(a[$i,$k]*b[$k]) for k = 1:N]...) for i in 1:M]...) else - StaticArrays.default_similar_type(T, s(), Val{length(S)}) + # Expand as a bunch of dot products + elements = Expr(:tuple, [:(bilindot($($(VecT))(row(a,$i)),b)) for i in 1:M]...) end + return :($($(VecT))($elements)) end - Base.:(*)(a::$name, b::$name) = a .* b - function Base.broadcasted(f, a::AbstractArray{T}, b::$name) where {T <: $name} - return Base.broadcasted(f, a, (b,)) + Base.:(*)(a::$VecT, b::$VecT) = a .* b + Base.:(*)(a::Number, b::$VecT) = a .* b + Base.:(*)(a::$VecT, b::Number) = a .* b + + Base.:(+)(a::$VecT, b::$VecT) = a .+ b + Base.:(+)(a::Number, b::$VecT) = a .+ b + Base.:(+)(a::$VecT, b::Number) = a .+ b + + Base.:(-)(a::$VecT) = (-).(a) + Base.:(-)(a::$VecT, b::$VecT) = a .- b + Base.:(-)(a::Number, b::$VecT) = a .- b + Base.:(-)(a::$VecT, b::Number) = a .- b + + function Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{$(VecT){N,T}}) where {N,T} + $(VecT){N,T}(ntuple(i-> rand(rng, T), N)) + end + function Random.randn(rng::Random.AbstractRNG, ::Type{$(VecT){N,T}}) where {N,T} + $(VecT){N,T}(ntuple(i-> randn(rng, T), N)) + end + function LinearAlgebra.cross(a::$(VecT){3}, b::$(VecT){3}) + @inbounds elements = (a[2]*b[3]-a[3]*b[2], + a[3]*b[1]-a[1]*b[3], + a[1]*b[2]-a[2]*b[1]) + return $(VecT)(elements) end end return esc(expr) end +LinearAlgebra.cross(a::StaticVector{2}, b::StaticVector{2}) = a[1]*b[2]-a[2]*b[1] +LinearAlgebra.norm(a::StaticVector) = sqrt(dot(a,a)) +LinearAlgebra.normalize(a::StaticVector) = a ./ norm(a) + +Base.eltype(::StaticVector{N, T}) where {N, T} = T +Base.eltype(::Type{<: StaticVector{N, T}}) where {N, T} = T + +Base.size(::StaticVector{N}) where {N} = (N,) +Base.length(::StaticVector{N}) where {N} = N +Base.length(::Type{<: StaticVector{N}}) where {N} = N +function Base.iterate(A::StaticVector, i=1) + i - 1 < length(A) ? (A[i], i + 1) : nothing +end + +function unit(::Type{T}, i::Integer) where {T <: StaticVector} + tup = ntuple(Val(length(T))) do j + return ifelse(i == j, 1, 0) + end + return T(tup) +end + +Base.zero(::Type{V}) where {V <:StaticVector} = V(0) + +function Base.:(==)(a::StaticVector{N}, b::StaticVector{N}) where N + for i in 1:N + a[i] == b[i] || return false + end + return true +end + +function Base.isapprox(a::StaticVector{N}, b::StaticVector{N}; kw...) where N + for i in 1:N + isapprox(a[i], b[i]; kw...) || return false + end + return true +end + +@generated function Base.transpose(b::StaticVector{N,T}) where {N,T} + expr = [:(transpose(b[$i])) for i=1:N] + return quote + Mat{1,N,T}($(expr...)) + end +end + +# Since we don't inherit from AbstractArray, some extra functions need to be overloaded +LinearAlgebra.promote_leaf_eltypes(x::StaticVector{N, T}) where {N,T} = T + abstract type AbstractPoint{Dim,T} <: StaticVector{Dim,T} end + @fixed_vector Point AbstractPoint @fixed_vector Vec StaticVector -const Mat = SMatrix -const VecTypes{N,T} = Union{StaticVector{N,T},NTuple{N,T}} -const Vecf{N} = Vec{N,Float32} + +Base.broadcasted(f, a::Point, b::GeometryBasics.Vec) = Vec(f.(a.data, b.data)) +Base.broadcasted(f, a::Vec, b::Point) = Vec(f.(a.data, b.data)) + +Base.:(+)(a::Vec{N}, b::Point{N}) where {N} = Point{N}(a.data .+ b.data) + +const VecTypes{N,T} = Union{StaticVector{N,T}, NTuple{N,T}} +const Vecf{N} = Vec{N, Float32} const Pointf{N} = Point{N,Float32} Base.isnan(p::Union{AbstractPoint,Vec}) = any(x -> isnan(x), p) @@ -138,8 +231,9 @@ for i in 1:4 end name = Symbol("Mat$i") namef = Symbol("Mat$(i)f") + namef = Symbol("Mat$(i)f") @eval begin - const $name{T} = $Mat{$i,$i,T,$(i * i)} + const $name{T} = $Mat{$i,$i,T} const $namef = $name{Float32} export $name export $namef diff --git a/src/lines.jl b/src/lines.jl index c89ae8da..ae891fb8 100644 --- a/src/lines.jl +++ b/src/lines.jl @@ -9,7 +9,7 @@ function intersects(a::Line{2,T1}, b::Line{2,T2}) where {T1,T2} T = promote_type(T1, T2) v1, v2 = a v3, v4 = b - MT = Mat{2,2,T,4} + MT = Mat{2,2,T} p0 = zero(Point2{T}) verticalA = v1[1] == v2[1] diff --git a/src/mat.jl b/src/mat.jl new file mode 100644 index 00000000..c5a204c0 --- /dev/null +++ b/src/mat.jl @@ -0,0 +1,157 @@ +import LinearAlgebra: inv, det +import Random + +struct Mat{Row, Column, T, L} <: AbstractMatrix{T} + values::NTuple{L, T} + function Mat{R, C, T}(values::NTuple{L, T}) where {R, C, T, L} + @assert L == R * C "$R * $C needs to be $L" + return new{R, C, T, L}(values) + end + function Mat{R, C, T, L}(values::NTuple{L, T}) where {R, C, T, L} + @assert L == R * C "$R * $C needs to be $L" + return new{R, C, T, L}(values) + end +end + +Base.size(::Mat{R, C}) where {R, C} = (R, C) +Base.getindex(mat::Mat{R, C}, i) where {R, C} = mat.values[i] +Base.IndexStyle(::Mat)= Base.IndexLinear() + +function Mat{C, R, T}(::LinearAlgebra.UniformScaling) where {C, R, T} + idx = CartesianIndices((R, C)) + data = ntuple(C * R) do i + ci, ri = Tuple(idx[i]) + return ci === ri ? T(1) : T(0) + end + return Mat{R, C, T}(data) +end + +function Mat{C, R, T, L}(::LinearAlgebra.UniformScaling) where {C, R, T, L} + idx = CartesianIndices((R, C)) + data = ntuple(C * R) do i + ci, ri = Tuple(idx[i]) + return ci === ri ? T(1) : T(0) + end + return Mat{R, C, T, L}(data) +end + +Mat{C, R, T}(args...) where {C, R, T} = Mat{C, R, T}(args) +Mat{C}(args...) where {C} = Mat{C, C}(args) +Mat{C}(arg) where {C} = Mat{C, C}(arg) +Mat{C, R}(x::Tuple) where {C, R} = Mat{C, R}(promote(x...)) +Mat{C, R, T}(x::Tuple) where {C, R, T} = Mat{C, R, T}(convert(NTuple{length(x), T}, x)) +Mat{C, R}(x::NTuple{L, T}) where {C, R, L, T} = Mat{C, R, T}(x) +Mat{C, R, T1}(x::NTuple{L, T2}) where {C, R, L, T1, T2} = Mat{C, R, T1}(convert(NTuple{L, T1}, x)) + +Mat{C, R}(x::AbstractMatrix{T}) where {C, R, T} = Mat{C, R, T}(x) +Mat{C, R, T}(x::AbstractMatrix) where {C, R, T} = Mat{C, R, T}(ntuple(i-> convert(T, x[i]), C*R)) + +# Matrix products +# General shape mismatched versions are errors +(*)(a::Mat{M, N, T1}, b::Mat{O, K, T2}) where {T1, T2, M, N, O, K} = throw(DimensionMismatch("$N != $O in $(typeof(a)) and $(typeof(b))")) + +# matrix * matrix +@generated function *(a::Mat{M, N, T1}, b::Mat{N, P, T2}) where {T1, T2, M, N, P} + elements = Expr(:tuple) + for j in 1:P + for i in 1:M + plus_expr = Expr(:call, :+, (:(a[$i,$k]*b[$k,$j]) for k = 1:N)...) + push!(elements.args, plus_expr) + end + end + :(Mat{$M, $P}($elements)) +end + +@inline det(A::Mat{1, 1, T}) where {T} = @inbounds return ( A[1] ) +@inline det(A::Mat{2, 2, T}) where {T} = @inbounds return ( A[1,1]*A[2,2] - A[1,2]*A[2,1]) +@inline det(A::Mat{3, 3, T}) where {T} = @inbounds return ( + A[1,1]*(A[2,2]*A[3,3]-A[2,3]*A[3,2]) - + A[1,2]*(A[2,1]*A[3,3]-A[2,3]*A[3,1]) + + A[1,3]*(A[2,1]*A[3,2]-A[2,2]*A[3,1]) +) + +det(A::Mat{4, 4, T}) where {T} = @inbounds return ( + A[13] * A[10] * A[7] * A[4] - A[9] * A[14] * A[7] * A[4] - + A[13] * A[6] * A[11] * A[4] + A[5] * A[14] * A[11] * A[4] + + A[9] * A[6] * A[15] * A[4] - A[5] * A[10] * A[15] * A[4] - + A[13] * A[10] * A[3] * A[8] + A[9] * A[14] * A[3] * A[8] + + A[13] * A[2] * A[11] * A[8] - A[1] * A[14] * A[11] * A[8] - + A[9] * A[2] * A[15] * A[8] + A[1] * A[10] * A[15] * A[8] + + A[13] * A[6] * A[3] * A[12] - A[5] * A[14] * A[3] * A[12] - + A[13] * A[2] * A[7] * A[12] + A[1] * A[14] * A[7] * A[12] + + A[5] * A[2] * A[15] * A[12] - A[1] * A[6] * A[15] * A[12] - + A[9] * A[6] * A[3] * A[16] + A[5] * A[10] * A[3] * A[16] + + A[9] * A[2] * A[7] * A[16] - A[1] * A[10] * A[7] * A[16] - + A[5] * A[2] * A[11] * A[16] + A[1] * A[6] * A[11] * A[16] +) + +det(A::Mat) = det(Matrix(A)) + +inv(A::Mat{1, 1, T, 1}) where T = @inbounds return Mat{1, 1, T, 1}(inv(A[1])) +function inv(A::Mat{2, 2, T, L}) where {T, L} + determinant = det(A) + @inbounds return Mat{2, 2, T}( + (A[2,2] /determinant, -A[2,1]/determinant), + (-A[1,2]/determinant, A[1,1] /determinant) + ) +end +function inv(A::Mat{3, 3, T, L}) where {T, L} + determinant = det(A) + @inbounds return Mat{3, 3, T}( + (A[2,2]*A[3,3]-A[2,3]*A[3,2]) /determinant, + -(A[2,1]*A[3,3]-A[2,3]*A[3,1])/determinant, + (A[2,1]*A[3,2]-A[2,2]*A[3,1]) /determinant, + + -(A[1,2]*A[3,3]-A[1,3]*A[3,2])/determinant, + (A[1,1]*A[3,3]-A[1,3]*A[3,1]) /determinant, + -(A[1,1]*A[3,2]-A[1,2]*A[3,1])/determinant, + + (A[1,2]*A[2,3]-A[1,3]*A[2,2]) /determinant, + -(A[1,1]*A[2,3]-A[1,3]*A[2,1])/determinant, + (A[1,1]*A[2,2]-A[1,2]*A[2,1]) /determinant + ) +end + +function inv(A::Mat{4, 4, T, L}) where {T, L} + determinant = det(A) + @inbounds return Mat{4, 4, T}( + (A[2,3]*A[3,4]*A[4,2] - A[2,4]*A[3,3]*A[4,2] + A[2,4]*A[3,2]*A[4,3] - A[2,2]*A[3,4]*A[4,3] - A[2,3]*A[3,2]*A[4,4] + A[2,2]*A[3,3]*A[4,4]) / determinant, + (A[2,4]*A[3,3]*A[4,1] - A[2,3]*A[3,4]*A[4,1] - A[2,4]*A[3,1]*A[4,3] + A[2,1]*A[3,4]*A[4,3] + A[2,3]*A[3,1]*A[4,4] - A[2,1]*A[3,3]*A[4,4]) / determinant, + (A[2,2]*A[3,4]*A[4,1] - A[2,4]*A[3,2]*A[4,1] + A[2,4]*A[3,1]*A[4,2] - A[2,1]*A[3,4]*A[4,2] - A[2,2]*A[3,1]*A[4,4] + A[2,1]*A[3,2]*A[4,4]) / determinant, + (A[2,3]*A[3,2]*A[4,1] - A[2,2]*A[3,3]*A[4,1] - A[2,3]*A[3,1]*A[4,2] + A[2,1]*A[3,3]*A[4,2] + A[2,2]*A[3,1]*A[4,3] - A[2,1]*A[3,2]*A[4,3]) / determinant, + + (A[1,4]*A[3,3]*A[4,2] - A[1,3]*A[3,4]*A[4,2] - A[1,4]*A[3,2]*A[4,3] + A[1,2]*A[3,4]*A[4,3] + A[1,3]*A[3,2]*A[4,4] - A[1,2]*A[3,3]*A[4,4]) / determinant, + (A[1,3]*A[3,4]*A[4,1] - A[1,4]*A[3,3]*A[4,1] + A[1,4]*A[3,1]*A[4,3] - A[1,1]*A[3,4]*A[4,3] - A[1,3]*A[3,1]*A[4,4] + A[1,1]*A[3,3]*A[4,4]) / determinant, + (A[1,4]*A[3,2]*A[4,1] - A[1,2]*A[3,4]*A[4,1] - A[1,4]*A[3,1]*A[4,2] + A[1,1]*A[3,4]*A[4,2] + A[1,2]*A[3,1]*A[4,4] - A[1,1]*A[3,2]*A[4,4]) / determinant, + (A[1,2]*A[3,3]*A[4,1] - A[1,3]*A[3,2]*A[4,1] + A[1,3]*A[3,1]*A[4,2] - A[1,1]*A[3,3]*A[4,2] - A[1,2]*A[3,1]*A[4,3] + A[1,1]*A[3,2]*A[4,3]) / determinant, + + (A[1,3]*A[2,4]*A[4,2] - A[1,4]*A[2,3]*A[4,2] + A[1,4]*A[2,2]*A[4,3] - A[1,2]*A[2,4]*A[4,3] - A[1,3]*A[2,2]*A[4,4] + A[1,2]*A[2,3]*A[4,4]) / determinant, + (A[1,4]*A[2,3]*A[4,1] - A[1,3]*A[2,4]*A[4,1] - A[1,4]*A[2,1]*A[4,3] + A[1,1]*A[2,4]*A[4,3] + A[1,3]*A[2,1]*A[4,4] - A[1,1]*A[2,3]*A[4,4]) / determinant, + (A[1,2]*A[2,4]*A[4,1] - A[1,4]*A[2,2]*A[4,1] + A[1,4]*A[2,1]*A[4,2] - A[1,1]*A[2,4]*A[4,2] - A[1,2]*A[2,1]*A[4,4] + A[1,1]*A[2,2]*A[4,4]) / determinant, + (A[1,3]*A[2,2]*A[4,1] - A[1,2]*A[2,3]*A[4,1] - A[1,3]*A[2,1]*A[4,2] + A[1,1]*A[2,3]*A[4,2] + A[1,2]*A[2,1]*A[4,3] - A[1,1]*A[2,2]*A[4,3]) / determinant, + + (A[1,4]*A[2,3]*A[3,2] - A[1,3]*A[2,4]*A[3,2] - A[1,4]*A[2,2]*A[3,3] + A[1,2]*A[2,4]*A[3,3] + A[1,3]*A[2,2]*A[3,4] - A[1,2]*A[2,3]*A[3,4]) / determinant, + (A[1,3]*A[2,4]*A[3,1] - A[1,4]*A[2,3]*A[3,1] + A[1,4]*A[2,1]*A[3,3] - A[1,1]*A[2,4]*A[3,3] - A[1,3]*A[2,1]*A[3,4] + A[1,1]*A[2,3]*A[3,4]) / determinant, + (A[1,4]*A[2,2]*A[3,1] - A[1,2]*A[2,4]*A[3,1] - A[1,4]*A[2,1]*A[3,2] + A[1,1]*A[2,4]*A[3,2] + A[1,2]*A[2,1]*A[3,4] - A[1,1]*A[2,2]*A[3,4]) / determinant, + (A[1,2]*A[2,3]*A[3,1] - A[1,3]*A[2,2]*A[3,1] + A[1,3]*A[2,1]*A[3,2] - A[1,1]*A[2,3]*A[3,2] - A[1,2]*A[2,1]*A[3,3] + A[1,1]*A[2,2]*A[3,3]) / determinant + ) +end + +inv(A::Mat) = typeof(A)(inv(Matrix(A))) + +function Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{<: Mat{R, C, T}}) where {R,C,T} + return Mat{R, C, T}(ntuple(i-> rand(rng, T), R*C)) +end + +function Random.randn(rng::Random.AbstractRNG, ::Type{<: Mat{R, C, T}}) where {R,C,T} + return Mat{R, C, T}(ntuple(i-> randn(rng, T), R*C)) +end + +function Base.transpose(a::Mat{R, C, T}) where {R, C, T} + idx = CartesianIndices((R, C)) + data = ntuple(R * C) do i + ci, ri = Tuple(idx[i]) + return a[ri, ci] + end + Mat{R, C, T}(data) +end diff --git a/src/meshes.jl b/src/meshes.jl index d7d41ffa..9c0bbed5 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -1,4 +1,7 @@ const FaceMesh{Dim,T,Element} = Mesh{Dim,T,Element,<:FaceView{Element}} +const GLTriangleElement = Triangle{3,Float32} +const GLTriangleFace = TriangleFace{GLIndex} + coordinates(mesh::FaceMesh) = coordinates(getfield(mesh, :simplices)) faces(mesh::FaceMesh) = faces(getfield(mesh, :simplices)) @@ -14,78 +17,6 @@ function normals(mesh::AbstractMesh) return nothing end -const GLTriangleElement = Triangle{3,Float32} -const GLTriangleFace = TriangleFace{GLIndex} -const PointWithUV{Dim,T} = PointMeta{Dim,T,Point{Dim,T},(:uv,),Tuple{Vec{2,T}}} -const PointWithNormal{Dim,T} = PointMeta{Dim,T,Point{Dim,T},(:normals,),Tuple{Vec{3,T}}} -const PointWithUVNormal{Dim,T} = PointMeta{Dim,T,Point{Dim,T},(:normals, :uv), - Tuple{Vec{3,T},Vec{2,T}}} -const PointWithUVWNormal{Dim,T} = PointMeta{Dim,T,Point{Dim,T},(:normals, :uvw), - Tuple{Vec{3,T},Vec{3,T}}} - -""" - TriangleMesh{Dim, T, PointType} - -Abstract Mesh with triangle elements of eltype `T`. -""" -const TriangleMesh{Dim,T,PointType} = AbstractMesh{TriangleP{Dim,T,PointType}} - -""" - PlainMesh{Dim, T} - -Triangle mesh with no meta information (just points + triangle faces) -""" -const PlainMesh{Dim,T} = TriangleMesh{Dim,T,Point{Dim,T}} -const GLPlainMesh{Dim} = PlainMesh{Dim,Float32} -const GLPlainMesh2D = GLPlainMesh{2} -const GLPlainMesh3D = GLPlainMesh{3} - -""" - UVMesh{Dim, T} - -PlainMesh with texture coordinates meta at each point. -`uvmesh.uv isa AbstractVector{Vec2f}` -""" -const UVMesh{Dim,T} = TriangleMesh{Dim,T,PointWithUV{Dim,T}} -const GLUVMesh{Dim} = UVMesh{Dim,Float32} -const GLUVMesh2D = UVMesh{2} -const GLUVMesh3D = UVMesh{3} - -""" - NormalMesh{Dim, T} - -PlainMesh with normals meta at each point. -`normalmesh.normals isa AbstractVector{Vec3f}` -""" -const NormalMesh{Dim,T} = TriangleMesh{Dim,T,PointWithNormal{Dim,T}} -const GLNormalMesh{Dim} = NormalMesh{Dim,Float32} -const GLNormalMesh2D = GLNormalMesh{2} -const GLNormalMesh3D = GLNormalMesh{3} - -""" - NormalUVMesh{Dim, T} - -PlainMesh with normals and uv meta at each point. -`normalmesh.normals isa AbstractVector{Vec3f}` -`normalmesh.uv isa AbstractVector{Vec2f}` -""" -const NormalUVMesh{Dim,T} = TriangleMesh{Dim,T,PointWithUVNormal{Dim,T}} -const GLNormalUVMesh{Dim} = NormalUVMesh{Dim,Float32} -const GLNormalUVMesh2D = GLNormalUVMesh{2} -const GLNormalUVMesh3D = GLNormalUVMesh{3} - -""" - NormalUVWMesh{Dim, T} - -PlainMesh with normals and uvw (texture coordinates in 3D) meta at each point. -`normalmesh.normals isa AbstractVector{Vec3f}` -`normalmesh.uvw isa AbstractVector{Vec3f}` -""" -const NormalUVWMesh{Dim,T} = TriangleMesh{Dim,T,PointWithUVWNormal{Dim,T}} -const GLNormalUVWMesh{Dim} = NormalUVWMesh{Dim,Float32} -const GLNormalUVWMesh2D = GLNormalUVWMesh{2} -const GLNormalUVWMesh3D = GLNormalUVWMesh{3} - function decompose_triangulate_fallback(primitive::Meshable; pointtype, facetype) positions = decompose(pointtype, primitive) faces = decompose(facetype, primitive) diff --git a/src/offsetintegers.jl b/src/offsetintegers.jl index 12c567be..c0043c53 100644 --- a/src/offsetintegers.jl +++ b/src/offsetintegers.jl @@ -20,7 +20,7 @@ raw(x::Integer) = x value(x::OffsetInteger{O,T}) where {O,T} = raw(x) - O value(x::Integer) = x -function show(io::IO, oi::OffsetInteger) +function Base.show(io::IO, oi::OffsetInteger) return print(io, "|$(raw(oi)) (indexes as $(value(oi))|") end diff --git a/src/precompile.jl b/src/precompile.jl deleted file mode 100644 index f7b1c52b..00000000 --- a/src/precompile.jl +++ /dev/null @@ -1,39 +0,0 @@ -macro warnpcfail(ex::Expr) - modl = __module__ - file = __source__.file === nothing ? "?" : String(__source__.file) - line = __source__.line - quote - $(esc(ex)) || @warn """precompile directive - $($(Expr(:quote, ex))) - failed. Please report an issue in $($modl) (after checking for duplicates) or remove this directive.""" _file=$file _line=$line - end -end - -function _precompile_() - ccall(:jl_generating_output, Cint, ()) == 1 || return nothing - @warnpcfail precompile(HyperRectangle{2,Float32}, (Int, Int, Int, Int)) - @warnpcfail precompile(==, (HyperRectangle{2,Float32}, HyperRectangle{2,Float32})) - @warnpcfail precompile(normal_mesh, (Tesselation{3,Float32,Cylinder{3,Float32},1},)) - @warnpcfail precompile(normal_mesh, (Tesselation{3,Float32,HyperSphere{3,Float32},1},)) - @warnpcfail precompile(normal_mesh, (HyperSphere{3,Float32},)) - @warnpcfail precompile(Tuple{typeof(*),SMatrix{4, 4, Float32, 16},HyperRectangle{3, Float32}}) # time: 0.11091917 - @warnpcfail precompile(Tuple{typeof(coordinates),HyperRectangle{2, Float32},Tuple{Int64, Int64}}) # time: 0.08693867 - @warnpcfail precompile(union, (HyperRectangle{3, Float32}, HyperRectangle{3, Float32})) - @warnpcfail precompile(Tuple{typeof(decompose),Type{Point{2, Float32}},HyperRectangle{2, Float32}}) # time: 0.026609203 - @warnpcfail precompile(Tuple{Type{HyperRectangle{3, Float32}},HyperRectangle{2, Float32}}) # time: 0.023717888 - @warnpcfail precompile(Tuple{typeof(+),HyperRectangle{3, Float32},Point{3, Float32}}) # time: 0.006633118 - @warnpcfail precompile(Tuple{Type{Rect2{T} where T},Float32,Float32,Float32,Float32}) # time: 0.001636267 - @warnpcfail precompile(Tuple{typeof(*),HyperRectangle{2, Float32},Float32}) # time: 0.001057589 - - if Base.VERSION >= v"1.6.0-DEV.1083" - @warnpcfail precompile(triangle_mesh, (Polygon{2, Float32, Point2f, LineString{2, Float32, Point2f, - Base.ReinterpretArray{Line{2, Float32}, 1, Tuple{Point2f, Point2f}, TupleView{Tuple{Point2f, Point2f}, 2, 1, Vector{Point2f}}, false}}, - Vector{LineString{2, Float32, Point2f, Base.ReinterpretArray{Line{2, Float32}, 1, Tuple{Point2f, Point2f}, TupleView{Tuple{Point2f, Point2f}, 2, 1, Vector{Point2f}}, false}}}},)) - else - @warnpcfail precompile(triangle_mesh, (Polygon{2, Float32, Point2f, LineString{2, Float32, Point2f, - Base.ReinterpretArray{Line{2, Float32}, 1, Tuple{Point2f, Point2f}, TupleView{Tuple{Point2f, Point2f}, 2, 1, Vector{Point2f}}}}, - Vector{LineString{2, Float32, Point2f, Base.ReinterpretArray{Line{2, Float32}, 1, Tuple{Point2f, Point2f}, TupleView{Tuple{Point2f, Point2f}, 2, 1, Vector{Point2f}}}}}},)) - end - - @warnpcfail precompile(split_intersections, (Vector{Point2f},)) -end diff --git a/src/primitives/cylinders.jl b/src/primitives/cylinders.jl index 9804cf53..e9467ad2 100644 --- a/src/primitives/cylinders.jl +++ b/src/primitives/cylinders.jl @@ -28,24 +28,24 @@ direction(c::Cylinder{N,T}) where {N,T} = (c.extremity .- c.origin) ./ height(c) function rotation(c::Cylinder{2,T}) where {T} d2 = direction(c) - u = @SVector [d2[1], d2[2], T(0)] - v = @MVector [u[2], -u[1], T(0)] - normalize!(v) - return hcat(v, u, @SVector T[0, 0, 1]) + u = Vec{3, T}(d2[1], d2[2], T(0)) + v = Vec{3, T}(u[2], -u[1], T(0)) + v = normalize(v) + return Mat{3, 3, T}(v..., u..., 0, 0, 1) end function rotation(c::Cylinder{3,T}) where {T} d3 = direction(c) - u = @SVector [d3[1], d3[2], d3[3]] + u = Vec{3, T}(d3[1], d3[2], d3[3]) if abs(u[1]) > 0 || abs(u[2]) > 0 - v = @MVector [u[2], -u[1], T(0)] + v = Vec{3, T}(u[2], -u[1], T(0)) else - v = @MVector [T(0), -u[3], u[2]] + v = Vec{3, T}(T(0), -u[3], u[2]) end - normalize!(v) - w = @SVector [u[2] * v[3] - u[3] * v[2], -u[1] * v[3] + u[3] * v[1], - u[1] * v[2] - u[2] * v[1]] - return hcat(v, w, u) + v = normalize(v) + w = Vec{3, T}(u[2] * v[3] - u[3] * v[2], -u[1] * v[3] + u[3] * v[1], + u[1] * v[2] - u[2] * v[1]) + return Mat{3, 3, T}(v..., w..., u...) end function coordinates(c::Cylinder{2,T}, nvertices=(2, 2)) where {T} diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index 7c24c444..7594c063 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -177,8 +177,8 @@ split(b::Rect, axis, value::Number) = _split(b, axis, value) function _split(b::H, axis, value) where {H<:Rect} bmin = minimum(b) bmax = maximum(b) - b1max = setindex(bmax, value, axis) - b2min = setindex(bmin, value, axis) + b1max = Base.setindex(bmax, value, axis) + b2min = Base.setindex(bmin, value, axis) return H(bmin, b1max - bmin), H(b2min, bmax - b2min) end diff --git a/src/viewtypes.jl b/src/viewtypes.jl index b700b6ea..3fbd30bb 100644 --- a/src/viewtypes.jl +++ b/src/viewtypes.jl @@ -96,7 +96,7 @@ end columns = ntuple(N) do i return view(points, ((i - 1) * seglen + 1):(i * seglen)) end - return StructArray{Point{N,T}}(columns) + return P.(columns...) else error("Dim 1 or 2 must be equal to the point dimension!") end diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 8985b512..958c09f5 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -74,7 +74,6 @@ using Test, GeometryBasics @test GeometryBasics.faces(m) == faces @test GeometryBasics.coordinates(m) ≈ positions m = normal_mesh(s)# just test that it works without explicit resolution parameter - @test m isa GLNormalMesh muv = uv_mesh(s) @test Rect(Point.(texturecoordinates(muv))) == Rect2f(Vec2f(0), Vec2f(1.0)) diff --git a/test/runtests.jl b/test/runtests.jl index fb576457..19bdf7cd 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,10 +1,9 @@ -using Test, Random, StructArrays, Tables, StaticArrays, OffsetArrays +using Test, Random, StructArrays, OffsetArrays using GeometryBasics using LinearAlgebra using GeometryBasics: attributes @testset "GeometryBasics" begin - @testset "algorithms" begin cube = Rect(Vec3f(-0.5), Vec3f(1)) cube_faces = decompose(TriangleFace{Int}, faces(cube)) @@ -39,7 +38,7 @@ end @testset "per vertex attributes" begin points = rand(Point{3, Float64}, 8) tfaces = TetrahedronFace{Int}[(1, 2, 3, 4), (5, 6, 7, 8)] - normals = rand(SVector{3, Float64}, 8) + normals = rand(Vec{3, Float64}, 8) stress = LinRange(0, 1, 8) mesh = Mesh(meta(points, normals = normals, stress = stress), tfaces) @@ -111,7 +110,6 @@ end end @testset "point with metadata" begin p = Point(1.1, 2.2) - @test p isa AbstractVector{Float64} pm = PointMeta(1.1, 2.2; a=1, b=2) p1 = Point(2.2, 3.6) p2 = [p, p1] @@ -190,7 +188,6 @@ end @testset "MetaT{Point}" begin p = Point(1.1, 2.2) - @test p isa AbstractVector{Float64} pm = MetaT(Point(1.1, 2.2); a=1, b=2) p1 = Point(2.2, 3.6) p2 = [p, p1] @@ -245,7 +242,7 @@ end @testset "per vertex attributes" begin points = rand(Point{3, Float64}, 8) tfaces = TetrahedronFace{Int}[(1, 2, 3, 4), (5, 6, 7, 8)] - normals = rand(SVector{3, Float64}, 8) + normals = rand(Vec{3, Float64}, 8) stress = LinRange(0, 1, 8) mesh_nometa = Mesh(points, tfaces) mesh = MetaT(mesh_nometa, normals = normals, stress = stress) @@ -410,10 +407,6 @@ end meshuv = Mesh(meta(points; uv=uv), tfaces) meshuvnormal = Mesh(meta(points; normals=normals, uv=uv), tfaces) - @test mesh isa GLPlainMesh - @test meshuv isa GLUVMesh3D - @test meshuvnormal isa GLNormalUVMesh3D - t = Tesselation(Rect2f(0, 0, 2, 2), (30, 30)) m = GeometryBasics.mesh(t, pointtype=Point3f, facetype=QuadFace{Int}) m2 = GeometryBasics.mesh(m, facetype=QuadFace{GLIndex}) @@ -472,28 +465,28 @@ end primitive = Sphere(Point3f(0), 1) m_normal = normal_mesh(primitive) - @test normals(m_normal) isa Vector{Vec3f} + @test GeometryBasics.normals(m_normal) isa Vector{Vec3f} primitive = Rect2(0, 0, 1, 1) m_normal = normal_mesh(primitive) - @test normals(m_normal) isa Vector{Vec3f} + @test GeometryBasics.normals(m_normal) isa Vector{Vec3f} primitive = Rect3(0, 0, 0, 1, 1, 1) m_normal = normal_mesh(primitive) - @test normals(m_normal) isa Vector{Vec3f} + @test GeometryBasics.normals(m_normal) isa Vector{Vec3f} points = decompose(Point2f, Circle(Point2f(0), 1)) tmesh = triangle_mesh(points) - @test normals(tmesh) == nothing + @test GeometryBasics.normals(tmesh) == nothing m = GeometryBasics.mesh(Sphere(Point3f(0), 1)) - @test normals(m) == nothing + @test GeometryBasics.normals(m) == nothing m_normals = pointmeta(m, Normal()) - @test normals(m_normals) isa Vector{Vec3f} + @test GeometryBasics.normals(m_normals) isa Vector{Vec3f} @test texturecoordinates(m) == nothing r2 = Rect2(0.0, 0.0, 1.0, 1.0) - @test iterate(texturecoordinates(r2)) == ((0.0, 1.0), ((0.0, 2), (1.0, 2))) + @test collect(texturecoordinates(r2)) == [(0.0, 1.0), (1.0, 1.0), (0.0, 0.0), (1.0, 0.0)] r3 = Rect3(0.0, 0.0, 1.0, 1.0, 2.0, 2.0) - @test iterate(texturecoordinates(r3)) == ([0, 0, 0], 2) + @test first(texturecoordinates(r3)) == Vec3(0, 0, 0) uv = decompose_uv(m) @test Rect(Point.(uv)) == Rect(0, 0, 1, 1) @@ -596,13 +589,11 @@ end @test coordinates(m) === decompose(Point{3, Float64}, m) tmesh = triangle_mesh(m) - @test tmesh isa GLPlainMesh @test coordinates(tmesh) === decompose(Point3f, tmesh) nmesh = normal_mesh(m) - @test nmesh isa GLNormalMesh @test metafree(coordinates(nmesh)) === decompose(Point3f, nmesh) - @test normals(nmesh) === decompose_normals(nmesh) + @test GeometryBasics.normals(nmesh) === decompose_normals(nmesh) m = GeometryBasics.mesh(s, pointtype=Point3f) @test m isa Mesh{3, Float32} @@ -715,4 +706,4 @@ end include("fixed_arrays.jl") end -end # testset "GeometryBasics" +end From 505c89be0c91d74282797535d5845fd7e61ae60e Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Tue, 22 Feb 2022 15:08:31 +0100 Subject: [PATCH 02/58] alias must be fully typed --- src/fixed_arrays.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index 90d27840..d41dff7f 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -233,7 +233,7 @@ for i in 1:4 namef = Symbol("Mat$(i)f") namef = Symbol("Mat$(i)f") @eval begin - const $name{T} = $Mat{$i,$i,T} + const $name{T} = $Mat{$i,$i,T, $(i*i)} const $namef = $name{Float32} export $name export $namef From 88f6a12df10cc0cddef5c5483ddfaecd37cfe6aa Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Tue, 22 Feb 2022 17:05:29 +0100 Subject: [PATCH 03/58] more fixes --- src/GeometryBasics.jl | 5 +++++ src/basic_types.jl | 1 - src/fixed_arrays.jl | 2 ++ src/mat.jl | 10 ++++++++++ src/precompiles.jl | 6 ++++++ src/triangulation.jl | 21 ++++++++++----------- 6 files changed, 33 insertions(+), 12 deletions(-) create mode 100644 src/precompiles.jl diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index 2fd0e12c..69c13569 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -64,4 +64,9 @@ export max_dist_dim, max_euclidean, max_euclideansq, min_dist_dim, min_euclidean export min_euclideansq, minmax_dist_dim, minmax_euclidean, minmax_euclideansq export self_intersections, split_intersections +if Base.VERSION >= v"1.4.2" + include("precompiles.jl") + _precompile_() +end + end # module diff --git a/src/basic_types.jl b/src/basic_types.jl index 38b4238d..c6123f72 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -19,7 +19,6 @@ abstract type AbstractNgonFace{N,T} <: AbstractFace{N,T} end abstract type AbstractSimplex{Dim,N,T} <: StaticVector{Dim,T} end - @fixed_vector SimplexFace AbstractSimplexFace const TetrahedronFace{T} = SimplexFace{4,T} diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index d41dff7f..034f1467 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -100,6 +100,7 @@ macro fixed_vector(VecT, SuperT) return $(VecT)(map(f, a.data, b.data)) end + Base.broadcasted(f, a::$(VecT)) = $(VecT)(f.(a.data)) Base.broadcasted(f, a::$(VecT), b) = $(VecT)(f.(a.data, b)) Base.broadcasted(f, a, b::$(VecT)) = $(VecT)(f.(a, b.data)) @@ -207,6 +208,7 @@ abstract type AbstractPoint{Dim,T} <: StaticVector{Dim,T} end @fixed_vector Point AbstractPoint @fixed_vector Vec StaticVector +Base.lastindex(::StaticVector{N}) where N = N Base.broadcasted(f, a::Point, b::GeometryBasics.Vec) = Vec(f.(a.data, b.data)) Base.broadcasted(f, a::Vec, b::Point) = Vec(f.(a.data, b.data)) diff --git a/src/mat.jl b/src/mat.jl index c5a204c0..dbb875ec 100644 --- a/src/mat.jl +++ b/src/mat.jl @@ -15,6 +15,16 @@ end Base.size(::Mat{R, C}) where {R, C} = (R, C) Base.getindex(mat::Mat{R, C}, i) where {R, C} = mat.values[i] + +function Base.getindex(mat::Mat{R, C, T}, r::Vec{NR}, c::Vec{NC}) where {R, C, NR, NC, T} + idx = CartesianIndices((NR, NC)) + data = ntuple(NR * NC) do i + ri, ci = Tuple(idx[i]) + return mat[r[ri], c[ci]] + end + return Mat{NR, NC, T}(data) +end + Base.IndexStyle(::Mat)= Base.IndexLinear() function Mat{C, R, T}(::LinearAlgebra.UniformScaling) where {C, R, T} diff --git a/src/precompiles.jl b/src/precompiles.jl new file mode 100644 index 00000000..df41b1e6 --- /dev/null +++ b/src/precompiles.jl @@ -0,0 +1,6 @@ + +function _precompile_() + ccall(:jl_generating_output, Cint, ()) == 1 || return nothing + Point2f(0.5, 0.1) in Triangle(Point2f(0), Point2f(0.5, 1), Point2f(1, 0)) + decompose(GLTriangleFace, [Point2f(0), Point2f(0.5, 1), Point2f(1, 0)]) +end diff --git a/src/triangulation.jl b/src/triangulation.jl index 94a10eb6..7bc8f931 100644 --- a/src/triangulation.jl +++ b/src/triangulation.jl @@ -68,23 +68,22 @@ Determine if a point is inside of a triangle. """ function Base.in(P::T, triangle::Triangle) where {T<:AbstractPoint} A, B, C = coordinates(triangle) - a = C .- B - b = A .- C - c = B .- A - ap = P .- A - bp = P .- B - cp = P .- C + ap = A .- P + bp = B .- P + cp = C .- P - a_bp = a[1] * bp[2] - a[2] * bp[1] - c_ap = c[1] * ap[2] - c[2] * ap[1] - b_cp = b[1] * cp[2] - b[2] * cp[1] + u = cross(bp, cp) + v = cross(cp, ap) + w = cross(ap, bp) t0 = zero(eltype(T)) - - return ((a_bp >= t0) && (b_cp >= t0) && (c_ap >= t0)) + dot(u, v) < t0 && return false + dot(u, w) < t0 && return false + return true end + function snip(contour::AbstractVector{<:AbstractPoint}, u, v, w, n, V) A = contour[V[u]] B = contour[V[v]] From 13854cc8eac417e44984b587e55a8b4a601e7325 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Tue, 22 Feb 2022 17:33:40 +0100 Subject: [PATCH 04/58] fix order --- src/fixed_arrays.jl | 11 +++++++++++ src/mat.jl | 8 -------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index 034f1467..94a6fb7c 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -203,11 +203,22 @@ end # Since we don't inherit from AbstractArray, some extra functions need to be overloaded LinearAlgebra.promote_leaf_eltypes(x::StaticVector{N, T}) where {N,T} = T + + abstract type AbstractPoint{Dim,T} <: StaticVector{Dim,T} end @fixed_vector Point AbstractPoint @fixed_vector Vec StaticVector +function Base.getindex(mat::Mat{R, C, T}, r::Vec{NR}, c::Vec{NC}) where {R, C, NR, NC, T} + idx = CartesianIndices((NR, NC)) + data = ntuple(NR * NC) do i + ri, ci = Tuple(idx[i]) + return mat[r[ri], c[ci]] + end + return Mat{NR, NC, T}(data) +end + Base.lastindex(::StaticVector{N}) where N = N Base.broadcasted(f, a::Point, b::GeometryBasics.Vec) = Vec(f.(a.data, b.data)) diff --git a/src/mat.jl b/src/mat.jl index dbb875ec..6f4c0fd7 100644 --- a/src/mat.jl +++ b/src/mat.jl @@ -16,14 +16,6 @@ end Base.size(::Mat{R, C}) where {R, C} = (R, C) Base.getindex(mat::Mat{R, C}, i) where {R, C} = mat.values[i] -function Base.getindex(mat::Mat{R, C, T}, r::Vec{NR}, c::Vec{NC}) where {R, C, NR, NC, T} - idx = CartesianIndices((NR, NC)) - data = ntuple(NR * NC) do i - ri, ci = Tuple(idx[i]) - return mat[r[ri], c[ci]] - end - return Mat{NR, NC, T}(data) -end Base.IndexStyle(::Mat)= Base.IndexLinear() From 46bebc351dcd84c7ebea075384e3b9dca3e2951c Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 2 Mar 2022 17:13:23 +0100 Subject: [PATCH 05/58] more fixes --- src/fixed_arrays.jl | 4 ++++ src/mat.jl | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index 94a6fb7c..db9b9215 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -164,8 +164,11 @@ Base.eltype(::StaticVector{N, T}) where {N, T} = T Base.eltype(::Type{<: StaticVector{N, T}}) where {N, T} = T Base.size(::StaticVector{N}) where {N} = (N,) +Base.size(::Type{<: StaticVector{N}}) where {N} = (N,) Base.length(::StaticVector{N}) where {N} = N Base.length(::Type{<: StaticVector{N}}) where {N} = N +Base.ndims(::Type{<: StaticVector}) = 1 + function Base.iterate(A::StaticVector, i=1) i - 1 < length(A) ? (A[i], i + 1) : nothing end @@ -199,6 +202,7 @@ end Mat{1,N,T}($(expr...)) end end +Base.reverse(x::P) where P <: StaticVector = P(reverse(x.data)) # Since we don't inherit from AbstractArray, some extra functions need to be overloaded LinearAlgebra.promote_leaf_eltypes(x::StaticVector{N, T}) where {N,T} = T diff --git a/src/mat.jl b/src/mat.jl index 6f4c0fd7..0838de8f 100644 --- a/src/mat.jl +++ b/src/mat.jl @@ -14,6 +14,8 @@ struct Mat{Row, Column, T, L} <: AbstractMatrix{T} end Base.size(::Mat{R, C}) where {R, C} = (R, C) +Base.size(::Type{<: Mat{R, C}}) where {R, C} = (R, C) +Base.ndims(::Type{<: Mat}) = 2 Base.getindex(mat::Mat{R, C}, i) where {R, C} = mat.values[i] @@ -38,6 +40,7 @@ function Mat{C, R, T, L}(::LinearAlgebra.UniformScaling) where {C, R, T, L} end Mat{C, R, T}(args...) where {C, R, T} = Mat{C, R, T}(args) +Mat{C, R, T, L}(args...) where {C, R, T, L} = Mat{C, R, T}(args) Mat{C}(args...) where {C} = Mat{C, C}(args) Mat{C}(arg) where {C} = Mat{C, C}(arg) Mat{C, R}(x::Tuple) where {C, R} = Mat{C, R}(promote(x...)) @@ -48,6 +51,8 @@ Mat{C, R, T1}(x::NTuple{L, T2}) where {C, R, L, T1, T2} = Mat{C, R, T1}(convert( Mat{C, R}(x::AbstractMatrix{T}) where {C, R, T} = Mat{C, R, T}(x) Mat{C, R, T}(x::AbstractMatrix) where {C, R, T} = Mat{C, R, T}(ntuple(i-> convert(T, x[i]), C*R)) +Base.convert(::Type{Mat{C, R, T, L}}, from::Mat{C, R}) where {C, R, T, L} = Mat{C, R, T}(from.values) + # Matrix products # General shape mismatched versions are errors (*)(a::Mat{M, N, T1}, b::Mat{O, K, T2}) where {T1, T2, M, N, O, K} = throw(DimensionMismatch("$N != $O in $(typeof(a)) and $(typeof(b))")) From ea0db94c6784a34771c150eb98b8d89d248ed553 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 16 Mar 2022 11:06:58 +0100 Subject: [PATCH 06/58] simplify fixed array --- src/GeometryBasics.jl | 1 - src/basic_types.jl | 200 +++++++++++++----------------------- src/boundingboxes.jl | 2 +- src/fixed_arrays.jl | 125 +++++++++------------- src/geometry_primitives.jl | 4 +- src/interfaces.jl | 6 +- src/lines.jl | 8 +- src/mat.jl | 23 +++++ src/meshes.jl | 4 +- src/offsetintegers.jl | 20 +--- src/primitives/cylinders.jl | 2 +- src/primitives/spheres.jl | 2 +- src/triangulation.jl | 22 ++-- src/viewtypes.jl | 9 +- test/runtests.jl | 2 +- 15 files changed, 175 insertions(+), 255 deletions(-) diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index 69c13569..7cf70776 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -6,7 +6,6 @@ import Base: * using Base: @propagate_inbounds -include("mat.jl") include("fixed_arrays.jl") include("offsetintegers.jl") include("basic_types.jl") diff --git a/src/basic_types.jl b/src/basic_types.jl index c6123f72..7e638fd0 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -49,16 +49,14 @@ Fixed Size Polygon, e.g. - N = 5 : Pentagon - ... """ -struct Ngon{Dim,T<:Real,N,Point<:AbstractPoint{Dim,T}} <: AbstractPolygon{Dim,T} - - points::Vec{N,Point} +struct Ngon{Dim, T<:Real, N} <: AbstractPolygon{Dim,T} + points::NTuple{N, Point{Dim, T}} end -const NNgon{N} = Ngon{Dim,T,N,P} where {Dim,T,P} +const NNgon{N} = Ngon{Dim,T,N} where {Dim,T} -function (::Type{<:NNgon{N}})(points::Vararg{P,N}) where {P<:AbstractPoint{Dim,T}, - N} where {Dim,T} - return Ngon{Dim,T,N,P}(Vec(points)) +function (::Type{<: NNgon{N}})(points::Vararg{Point{Dim,T}, N}) where {N,Dim,T} + return Ngon{Dim,T,N}(points) end Base.show(io::IO, x::NNgon{N}) where {N} = print(io, "Ngon{$N}(", join(x, ", "), ")") @@ -71,7 +69,7 @@ Base.length(::NNgon{N}) where {N} = N """ The Ngon Polytope element type when indexing an array of points with a SimplexFace """ -function Polytope(P::Type{<:AbstractPoint{Dim,T}}, +function Polytope(P::Type{Point{Dim,T}}, ::Type{<:AbstractNgonFace{N,IT}}) where {N,Dim,T,IT} return Ngon{Dim,T,N,P} end @@ -79,38 +77,30 @@ end """ The fully concrete Ngon type, when constructed from a point type! """ -function Polytope(::Type{<:NNgon{N}}, P::Type{<:AbstractPoint{NDim,T}}) where {N,NDim,T} - return Ngon{NDim,T,N,P} +function Polytope(::Type{<:NNgon{N}}, P::Type{Point{NDim,T}}) where {N,NDim,T} + return Ngon{NDim,T,N} end -const LineP{Dim,T,P<:AbstractPoint{Dim,T}} = Ngon{Dim,T,2,P} -const Line{Dim,T} = LineP{Dim,T,Point{Dim,T}} +const Line{Dim,T} = Ngon{Dim,T,2} # Simplex{D, T, 3} & Ngon{D, T, 3} are both representing a triangle. # Since Ngon is supposed to be flat and a triangle is flat, lets prefer Ngon # for triangle: -const TriangleP{Dim,T,P<:AbstractPoint{Dim,T}} = Ngon{Dim,T,3,P} -const Triangle{Dim,T} = TriangleP{Dim,T,Point{Dim,T}} +const Triangle{Dim,T} = Ngon{Dim,T,3} const Triangle3d{T} = Triangle{3,T} -Base.show(io::IO, x::TriangleP) = print(io, "Triangle(", join(x, ", "), ")") -Base.summary(io::IO, x::Type{<:TriangleP}) = print(io, "Triangle") +Base.show(io::IO, x::Triangle) = print(io, "Triangle(", join(x, ", "), ")") -const Quadrilateral{Dim,T} = Ngon{Dim,T,4,P} where {P<:AbstractPoint{Dim,T}} +const Quadrilateral{Dim,T} = Ngon{Dim,T,4} Base.show(io::IO, x::Quadrilateral) = print(io, "Quad(", join(x, ", "), ")") -Base.summary(io::IO, x::Type{<:Quadrilateral}) = print(io, "Quad") -function coordinates(lines::AbstractArray{LineP{Dim,T,PointType}}) where {Dim,T,PointType} - return if lines isa Base.ReinterpretArray - return coordinates(lines.parent) - else - result = PointType[] - for line in lines - append!(result, coordinates(line)) - end - return result +function coordinates(lines::AbstractArray{Line{Dim,T}}) where {Dim,T} + result = Point{Dim, T}[] + for line in lines + append!(result, coordinates(line)) end + return result end """ @@ -129,22 +119,19 @@ This is for a simpler implementation. It applies to infinite dimensions. The structure of this type is designed to allow embedding in higher-order spaces by parameterizing on `T`. """ -struct Simplex{Dim,T<:Real,N,Point<:AbstractPoint{Dim,T}} <: Polytope{Dim,T} - - points::Vec{N,Point} +struct Simplex{Dim,T<:Real,N} <: Polytope{Dim,T} + points::NTuple{N,Point{Dim,T}} end -const NSimplex{N} = Simplex{Dim,T,N,P} where {Dim,T,P} -const TetrahedronP{T,P<:AbstractPoint{3,T}} = Simplex{3,T,4,P} -const Tetrahedron{T} = TetrahedronP{T,Point{3,T}} +const NSimplex{N} = Simplex{Dim,T,N} where {Dim,T} +const Tetrahedron{T} = Simplex{3,T,4} -Base.show(io::IO, x::TetrahedronP) = print(io, "Tetrahedron(", join(x, ", "), ")") +Base.show(io::IO, x::Tetrahedron) = print(io, "Tetrahedron(", join(x, ", "), ")") coordinates(x::Simplex) = x.points -function (::Type{<:NSimplex{N}})(points::Vararg{P,N}) where {P<:AbstractPoint{Dim,T}, - N} where {Dim,T} - return Simplex{Dim,T,N,P}(Vec(points)) +function (::Type{<:NSimplex{N}})(points::Vararg{Point{Dim,T},N}) where {Dim,T,N} + return Simplex{Dim,T,N}(points) end # Base Array interface @@ -154,26 +141,24 @@ Base.length(::NSimplex{N}) where {N} = N """ The Simplex Polytope element type when indexing an array of points with a SimplexFace """ -function Polytope(P::Type{<:AbstractPoint{Dim,T}}, - ::Type{<:AbstractSimplexFace{N}}) where {N,Dim,T} +function Polytope(P::Type{Point{Dim,T}}, ::Type{<:AbstractSimplexFace{N}}) where {N,Dim,T} return Simplex{Dim,T,N,P} end """ The fully concrete Simplex type, when constructed from a point type! """ -function Polytope(::Type{<:NSimplex{N}}, P::Type{<:AbstractPoint{NDim,T}}) where {N,NDim,T} +function Polytope(::Type{<:NSimplex{N}}, P::Type{Point{NDim,T}}) where {N,NDim,T} return Simplex{NDim,T,N,P} end -Base.show(io::IO, x::LineP) = print(io, "Line(", x[1], " => ", x[2], ")") +Base.show(io::IO, x::Line) = print(io, "Line(", x[1], " => ", x[2], ")") """ - LineString(points::AbstractVector{<:AbstractPoint}) + LineString(points::AbstractVector{<:Point}) A LineString is a geometry of connected line segments """ -struct LineString{Dim,T<:Real,P<:AbstractPoint,V<:AbstractVector{<:LineP{Dim,T,P}}} <: - AbstractVector{LineP{Dim,T,P}} +struct LineString{Dim,T<:Real,V<:AbstractVector{Line{Dim,T}}} points::V end @@ -183,12 +168,12 @@ Base.copy(x::LineString) = LineString(copy(x.points)) Base.size(x::LineString) = size(getfield(x, :points)) Base.getindex(x::LineString, i) = getindex(getfield(x, :points), i) -function LineString(points::AbstractVector{LineP{Dim,T,P}}) where {Dim,T,P} - return LineString{Dim,T,P,typeof(points)}(points) +function LineString(points::AbstractVector{Line{Dim,T}}) where {Dim,T} + return LineString{Dim,T,typeof(points)}(points) end """ - LineString(points::AbstractVector{<: AbstractPoint}, skip = 1) + LineString(points::AbstractVector{<: Point}, skip = 1) Creates a LineString from a vector of points. With `skip == 1`, the default, it will connect the line like this: @@ -198,22 +183,21 @@ linestring = LineString(points) @assert linestring == LineString([a => b, b => c, c => d]) ``` """ -function LineString(points::AbstractVector{<:AbstractPoint}, skip=1) - return LineString(connect(points, LineP, skip)) +function LineString(points::AbstractVector{<:Point}, skip=1) + return LineString(connect(points, Line, skip)) end -function LineString(points::AbstractVector{<:Pair{P,P}}) where {P<:AbstractPoint{N,T}} where {N, - T} - return LineString(reinterpret(LineP{N,T,P}, points)) +function LineString(points::AbstractVector{<:Pair{Point{N,T},Point{N,T}}}) where {N, T} + return LineString(reinterpret(Line{N,T}, points)) end -function LineString(points::AbstractVector{<:AbstractPoint}, +function LineString(points::AbstractVector{<:Point}, faces::AbstractVector{<:LineFace}) return LineString(connect(points, faces)) end """ - LineString(points::AbstractVector{<: AbstractPoint}, indices::AbstractVector{<: Integer}, skip = 1) + LineString(points::AbstractVector{<: Point}, indices::AbstractVector{<: Integer}, skip = 1) Creates a LineString from a vector of points and an index list. With `skip == 1`, the default, it will connect the line like this: @@ -231,7 +215,7 @@ linestring = LineString(points, faces, 2) @assert linestring == LineString([a => b, c => d]) ``` """ -function LineString(points::AbstractVector{<:AbstractPoint}, +function LineString(points::AbstractVector{<:Point}, indices::AbstractVector{<:Integer}, skip=1) faces = connect(indices, LineFace, skip) return LineString(points, faces) @@ -239,10 +223,10 @@ end """ Polygon(exterior::AbstractVector{<:Point}) - Polygon(exterior::AbstractVector{<:Point}, interiors::Vector{<:AbstractVector{<:AbstractPoint}}) + Polygon(exterior::AbstractVector{<:Point}, interiors::Vector{<:AbstractVector{<:Point}}) """ -struct Polygon{Dim,T<:Real,P<:AbstractPoint{Dim,T},L<:AbstractVector{<:LineP{Dim,T,P}}, +struct Polygon{Dim,T<:Real,L<:AbstractVector{Point{Dim,T}}, V<:AbstractVector{L}} <: AbstractPolygon{Dim,T} exterior::L interiors::V @@ -254,35 +238,26 @@ function Base.:(==)(a::Polygon, b::Polygon) return (a.exterior == b.exterior) && (a.interiors == b.interiors) end -function Polygon(exterior::E, - interiors::AbstractVector{E}) where {E<:AbstractVector{LineP{Dim,T,P}}} where {Dim, - T, - P} - return Polygon{Dim,T,P,typeof(exterior),typeof(interiors)}(exterior, interiors) +function Polygon( + exterior::AbstractVector{Point{Dim,T}}, + interiors::AbstractVector{AbstractVector{Point{Dim,T}}}) where {Dim, T} + return Polygon{Dim,T,typeof(exterior),typeof(interiors)}(exterior, interiors) end -Polygon(exterior::L) where {L<:AbstractVector{<:LineP}} = Polygon(exterior, L[]) +Polygon(exterior::AbstractVector{Point{N, T}}) where {N, T} = Polygon(exterior, Vector{Point{N, T}}[]) -function Polygon(exterior::AbstractVector{P}, - skip::Int=1) where {P<:AbstractPoint{Dim,T}} where {Dim,T} - return Polygon(LineString(exterior, skip)) -end - -function Polygon(exterior::AbstractVector{P}, faces::AbstractVector{<:Integer}, - skip::Int=1) where {P<:AbstractPoint{Dim,T}} where {Dim,T} +function Polygon(exterior::AbstractVector{Point{Dim, T}}, faces::AbstractVector{<:Integer}, + skip::Int=1) where {Dim,T} return Polygon(LineString(exterior, faces, skip)) end -function Polygon(exterior::AbstractVector{P}, - faces::AbstractVector{<:LineFace}) where {P<:AbstractPoint{Dim,T}} where {Dim, - T} +function Polygon(exterior::AbstractVector{Point{Dim,T}}, + faces::AbstractVector{<:LineFace}) where {Dim, T} return Polygon(LineString(exterior, faces)) end -function Polygon(exterior::AbstractVector{P}, - interior::AbstractVector{<:AbstractVector{P}}) where {P<:AbstractPoint{Dim, - T}} where {Dim, - T} +function Polygon(exterior::AbstractVector{Point{Dim, T}}, + interior::AbstractVector{<:AbstractVector{Point{Dim, T}}}) where {Dim,T} ext = LineString(exterior) # We need to take extra care for empty interiors, since # if we just map over it, it won't infer the element type correctly! @@ -291,12 +266,12 @@ function Polygon(exterior::AbstractVector{P}, return Polygon(ext, int) end -function coordinates(polygon::Polygon{N,T,PointType}) where {N,T,PointType} +function coordinates(polygon::Polygon{N,T}) where {N,T} exterior = coordinates(polygon.exterior) if isempty(polygon.interiors) return exterior else - result = PointType[] + result = Point{N, T}[] append!(result, exterior) foreach(x -> append!(result, coordinates(x)), polygon.interiors) return result @@ -306,47 +281,39 @@ end """ MultiPolygon(polygons::AbstractPolygon) """ -struct MultiPolygon{Dim,T<:Real,Element<:AbstractPolygon{Dim,T}, - A<:AbstractVector{Element}} <: AbstractVector{Element} +struct MultiPolygon{Dim,T<:Real,A<:AbstractVector{<:AbstractPolygon{Dim,T}}} polygons::A end -function MultiPolygon(polygons::AbstractVector{P}; - kw...) where {P<:AbstractPolygon{Dim,T}} where {Dim,T} - return MultiPolygon(meta(polygons; kw...)) +function MultiPolygon(polygons::AbstractVector{<: AbstractPolygon{Dim,T}}) where {Dim,T} + return MultiPolygon(polygons) end Base.getindex(mp::MultiPolygon, i) = mp.polygons[i] Base.size(mp::MultiPolygon) = size(mp.polygons) -struct MultiLineString{Dim,T<:Real,Element<:LineString{Dim,T},A<:AbstractVector{Element}} <: - AbstractVector{Element} - +struct MultiLineString{Dim,T<:Real,A<:AbstractVector{LineString{Dim,T}}} linestrings::A end -function MultiLineString(linestrings::AbstractVector{L}; - kw...) where {L<:AbstractVector{LineP{Dim,T,P}}} where {Dim,T,P} - return MultiLineString(meta(linestrings; kw...)) +function MultiLineString(linestrings::AbstractVector{L}) where {L<:AbstractVector{Line{Dim,T}}} where {Dim,T} + return MultiLineString(linestrings) end Base.getindex(ms::MultiLineString, i) = ms.linestrings[i] Base.size(ms::MultiLineString) = size(ms.linestrings) """ - MultiPoint(points::AbstractVector{AbstractPoint}) + MultiPoint(points::AbstractVector{<: Point}) A collection of points """ -struct MultiPoint{Dim,T<:Real,P<:AbstractPoint{Dim,T},A<:AbstractVector{P}} <: - AbstractVector{P} - +struct MultiPoint{Dim,T<:Real,A<:AbstractVector{Point{Dim,T}}} points::A end -function MultiPoint(points::AbstractVector{P}; - kw...) where {P<:AbstractPoint{Dim,T}} where {Dim,T} - return MultiPoint(meta(points; kw...)) +function MultiPoint(points::AbstractVector{Point{Dim,T}}) where {Dim,T} + return MultiPoint(points) end Base.getindex(mpt::MultiPoint, i) = mpt.points[i] @@ -359,36 +326,16 @@ An abstract mesh is a collection of Polytope elements (Simplices / Ngons). The connections are defined via faces(mesh), the coordinates of the elements are returned by coordinates(mesh). Arbitrary meta information can be attached per point or per face """ -abstract type AbstractMesh{Element<:Polytope} <: AbstractVector{Element} end +abstract type AbstractMesh{Element} end """ Mesh <: AbstractVector{Element} The concrete AbstractMesh implementation. """ -struct Mesh{Dim,T<:Number,Element<:Polytope{Dim,T},V<:AbstractVector{Element}} <: +struct Mesh{Dim,T<:Number, Element, V<:AbstractVector{Point{Dim, T}}, C} <: AbstractMesh{Element} - simplices::V # usually a FaceView, to connect a set of points via a set of faces. -end - -Tables.schema(mesh::Mesh) = Tables.schema(getfield(mesh, :simplices)) - -function Base.getproperty(mesh::Mesh, name::Symbol) - if name === :position - # a mesh always has position defined by coordinates... - return metafree(coordinates(mesh)) - else - return getproperty(getfield(mesh, :simplices), name) - end -end - -function Base.propertynames(mesh::Mesh) - names = propertynames(getfield(mesh, :simplices)) - if :position in names - return names - else - # a mesh always has positions! - return (names..., :position) - end + vertices::V + connectivity::C end function Base.summary(io::IO, ::Mesh{Dim,T,Element}) where {Dim,T,Element} @@ -397,19 +344,16 @@ function Base.summary(io::IO, ::Mesh{Dim,T,Element}) where {Dim,T,Element} return print(io, "}") end -Base.size(mesh::Mesh) = size(getfield(mesh, :simplices)) -Base.getindex(mesh::Mesh, i::Integer) = getfield(mesh, :simplices)[i] - function Mesh(elements::AbstractVector{<:Polytope{Dim,T}}) where {Dim,T} - return Mesh{Dim,T,eltype(elements),typeof(elements)}(elements) + return Mesh{Dim,T,eltype(elements),typeof(elements)}(elements, nothing) end -function Mesh(points::AbstractVector{<:AbstractPoint}, +function Mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:AbstractFace}) - return Mesh(connect(points, faces)) + return Mesh(points, faces) end -function Mesh(points::AbstractVector{<:AbstractPoint}, faces::AbstractVector{<:Integer}, +function Mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Integer}, facetype=TriangleFace, skip=1) return Mesh(connect(points, connect(faces, facetype, skip))) end diff --git a/src/boundingboxes.jl b/src/boundingboxes.jl index fa19d296..e2deeb32 100644 --- a/src/boundingboxes.jl +++ b/src/boundingboxes.jl @@ -5,7 +5,7 @@ end """ Construct a HyperRectangle enclosing all points. """ -function Rect{N1,T1}(geometry::AbstractArray{PT}) where {N1,T1,PT<:AbstractPoint} +function Rect{N1,T1}(geometry::AbstractArray{PT}) where {N1,T1,PT<:Point} N2, T2 = length(PT), eltype(PT) @assert N1 >= N2 vmin = Point{N2,T2}(typemax(T2)) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index db9b9215..d4df208f 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -4,6 +4,7 @@ import Random import Base: setindex abstract type StaticVector{N, T} end +function similar_type end macro fixed_vector(VecT, SuperT) expr = quote @@ -77,83 +78,60 @@ macro fixed_vector(VecT, SuperT) end Base.convert(::Type{$(VecT)}, x::Tuple) = $(VecT)(x) - Base.@propagate_inbounds function Base.getindex(v::$(VecT){N,T}, i::Int) where {N,T} - return v.data[i] - end - Base.setindex(c::$(VecT){N, T}, v, i::Integer) where {N,T} = $(VecT){N,T}(Base.setindex(c.data, v, i)) - Base.@propagate_inbounds function Base.getindex(a::AbstractArray{T}, idx::$(VecT){N, <:Integer}) where {N,T} - return $(VecT){N,T}(map(i-> a[i], idx)) - end - - Base.@propagate_inbounds function Base.getindex(a::StaticVector{N1, T}, idx::$(VecT){N, <:Integer}) where {N,N1,T} - return $(VecT){N,T}(map(i-> a[i], idx)) - end - - Base.Tuple(v::$(VecT)) = v.data - - - function Base.broadcasted(f, a::AbstractArray{T}, b::$(VecT)) where {T <: $(VecT)} - return Base.broadcasted(f, a, (b,)) - end + @inline similar_type(::$(VecT){N, T}, n::Integer) where {N, T} = $(VecT){n, T} + @inline similar_type(::$(VecT){N}, ::Type{T}) where {N, T} = $(VecT){N, T} + @inline similar_type(::$(VecT), ::Integer, ::Type{T}) where {N, T} = $(VecT){N, T} + @inline similar_type(::$(VecT)) = $(VecT) function Base.broadcasted(f, a::$(VecT), b::$(VecT)) return $(VecT)(map(f, a.data, b.data)) end + function LinearAlgebra.cross(a::$(VecT){3}, b::$(VecT){3}) + @inbounds elements = (a[2]*b[3]-a[3]*b[2], + a[3]*b[1]-a[1]*b[3], + a[1]*b[2]-a[2]*b[1]) + return $(VecT)(elements) + end + end + return esc(expr) +end - Base.broadcasted(f, a::$(VecT)) = $(VecT)(f.(a.data)) - Base.broadcasted(f, a::$(VecT), b) = $(VecT)(f.(a.data, b)) - Base.broadcasted(f, a, b::$(VecT)) = $(VecT)(f.(a, b.data)) +Base.broadcasted(f, a::StaticVector) = similar_type(a)(f.(a.data)) +Base.broadcasted(::typeof(+), a::StaticVector, b::OneTo{Int64}) = similar_type(a)((a.data .+ b)) +Base.broadcasted(f, a::StaticVector, b) = similar_type(a)(f.(a.data, b)) +Base.broadcasted(f, a, b::StaticVector) = similar_type(b)(f.(a, b.data)) - Base.map(f, b::$(VecT)) = $(VecT)(map(f, b.data)) +Base.@propagate_inbounds function Base.getindex(a::AbstractArray{T}, idx::StaticVector{N, <:Integer}) where {N,T} + return similar_type(idx, N, T)(map(i-> a[i], idx)) +end - (*)(a::Mat{M, N, T1}, b::$(VecT){O, T2}) where {T1, T2, M, N, O} = throw(DimensionMismatch("$N != $O in $(typeof(a)) and $(typeof(b))")) +Base.map(f, b::StaticVector) = similar_type(b)(map(f, b.data)) - # vector * (row vector) - @generated function *(a::$VecT{N, T1}, b::Mat{1, M, T2}) where {T1, T2, N, M} - elements = Expr(:tuple, [Expr(:tuple, [:(a[$i] * b[$j]) for i in 1:N]...) for j in 1:M]...) - return :($($(VecT))($elements)) - end +function Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{V}) where V <: StaticVector{N,T} where {N, T} + V(ntuple(i-> rand(rng, T), N)) +end +function Random.randn(rng::Random.AbstractRNG, ::Type{V}) where V <: StaticVector{N,T} where {N, T} + V(ntuple(i-> randn(rng, T), N)) +end - # matrix * vector - @generated function *(a::Mat{M, N, T1}, b::$VecT{N, T2}) where {T1, T2, M, N} - total_terms = M*N - if total_terms <= 64 - # Full unrolling - elements = Expr(:tuple, [Expr(:call, :+, [:(a[$i,$k]*b[$k]) for k = 1:N]...) for i in 1:M]...) - else - # Expand as a bunch of dot products - elements = Expr(:tuple, [:(bilindot($($(VecT))(row(a,$i)),b)) for i in 1:M]...) - end - return :($($(VecT))($elements)) - end +Base.@propagate_inbounds function Base.getindex(v::StaticVector{N,T}, i::Int) where {N,T} + return v.data[i] +end - Base.:(*)(a::$VecT, b::$VecT) = a .* b - Base.:(*)(a::Number, b::$VecT) = a .* b - Base.:(*)(a::$VecT, b::Number) = a .* b +Base.setindex(c::V, v, i::Integer) where {V <: StaticVector} = V(Base.setindex(c.data, v, i)) - Base.:(+)(a::$VecT, b::$VecT) = a .+ b - Base.:(+)(a::Number, b::$VecT) = a .+ b - Base.:(+)(a::$VecT, b::Number) = a .+ b +Base.@propagate_inbounds function Base.getindex(a::StaticVector{N1, T}, idx::StaticVector{N, <:Integer}) where {N,N1,T} + return similar_type(idx, N, T)(map(i-> a[i], idx)) +end - Base.:(-)(a::$VecT) = (-).(a) - Base.:(-)(a::$VecT, b::$VecT) = a .- b - Base.:(-)(a::Number, b::$VecT) = a .- b - Base.:(-)(a::$VecT, b::Number) = a .- b +Base.:(-)(a::StaticVector) = (-).(a) - function Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{$(VecT){N,T}}) where {N,T} - $(VecT){N,T}(ntuple(i-> rand(rng, T), N)) - end - function Random.randn(rng::Random.AbstractRNG, ::Type{$(VecT){N,T}}) where {N,T} - $(VecT){N,T}(ntuple(i-> randn(rng, T), N)) - end - function LinearAlgebra.cross(a::$(VecT){3}, b::$(VecT){3}) - @inbounds elements = (a[2]*b[3]-a[3]*b[2], - a[3]*b[1]-a[1]*b[3], - a[1]*b[2]-a[2]*b[1]) - return $(VecT)(elements) - end +for op in [:(Base.:(*)), :(Base.:(+)), :(Base.:(-))] + @eval begin + ($op)(a::StaticVector, b::StaticVector) = a .* b + ($op)(a::Number, b::StaticVector) = a .* b + ($op)(a::StaticVector, b::Number) = a .* b end - return esc(expr) end LinearAlgebra.cross(a::StaticVector{2}, b::StaticVector{2}) = a[1]*b[2]-a[2]*b[1] @@ -207,33 +185,22 @@ Base.reverse(x::P) where P <: StaticVector = P(reverse(x.data)) # Since we don't inherit from AbstractArray, some extra functions need to be overloaded LinearAlgebra.promote_leaf_eltypes(x::StaticVector{N, T}) where {N,T} = T - - -abstract type AbstractPoint{Dim,T} <: StaticVector{Dim,T} end - -@fixed_vector Point AbstractPoint +@fixed_vector Point StaticVector @fixed_vector Vec StaticVector -function Base.getindex(mat::Mat{R, C, T}, r::Vec{NR}, c::Vec{NC}) where {R, C, NR, NC, T} - idx = CartesianIndices((NR, NC)) - data = ntuple(NR * NC) do i - ri, ci = Tuple(idx[i]) - return mat[r[ri], c[ci]] - end - return Mat{NR, NC, T}(data) -end Base.lastindex(::StaticVector{N}) where N = N -Base.broadcasted(f, a::Point, b::GeometryBasics.Vec) = Vec(f.(a.data, b.data)) +Base.broadcasted(f, a::Point, b::Vec) = Vec(f.(a.data, b.data)) Base.broadcasted(f, a::Vec, b::Point) = Vec(f.(a.data, b.data)) - Base.:(+)(a::Vec{N}, b::Point{N}) where {N} = Point{N}(a.data .+ b.data) const VecTypes{N,T} = Union{StaticVector{N,T}, NTuple{N,T}} const Vecf{N} = Vec{N, Float32} const Pointf{N} = Point{N,Float32} -Base.isnan(p::Union{AbstractPoint,Vec}) = any(x -> isnan(x), p) +Base.isnan(p::Union{Point,Vec}) = any(x -> isnan(x), p) + +include("mat.jl") for i in 1:4 for T in [:Point, :Vec] diff --git a/src/geometry_primitives.jl b/src/geometry_primitives.jl index b2ae6d42..295c74a4 100644 --- a/src/geometry_primitives.jl +++ b/src/geometry_primitives.jl @@ -105,12 +105,12 @@ normals{VT,FD,FT,FO}(vertices::Vector{Point{3, VT}}, ``` Compute all vertex normals. """ -function normals(vertices::AbstractVector{<:AbstractPoint{3,T}}, faces::AbstractVector{F}; +function normals(vertices::AbstractVector{Point{3,T}}, faces::AbstractVector{F}; normaltype=Vec{3,T}) where {T,F<:NgonFace} return normals(vertices, faces, normaltype) end -function normals(vertices::AbstractVector{<:AbstractPoint{3,T}}, faces::AbstractVector{F}, +function normals(vertices::AbstractVector{Point{3,T}}, faces::AbstractVector{F}, ::Type{N}) where {T,F<:NgonFace,N} normals_result = zeros(N, length(vertices)) for face in faces diff --git a/src/interfaces.jl b/src/interfaces.jl index c9905fcb..f2ee1f7d 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -4,7 +4,7 @@ Returns the edges/vertices/coordinates of a geometry. Is allowed to return lazy Use `decompose(ConcretePointType, geometry)` to get `Vector{ConcretePointType}` with `ConcretePointType` to be something like `Point{3, Float32}`. """ -function coordinates(points::AbstractVector{<:AbstractPoint}) +function coordinates(points::AbstractVector{<:Point}) return points end @@ -87,7 +87,7 @@ end # Types that can be converted to a mesh via the functions below const Meshable{Dim,T} = Union{Tesselation{Dim,T},Mesh{Dim,T},AbstractPolygon{Dim,T}, GeometryPrimitive{Dim,T}, - AbstractVector{<:AbstractPoint{Dim,T}}} + AbstractVector{Point{Dim,T}}} struct UV{T} end UV(::Type{T}) where {T} = UV{T}() @@ -105,7 +105,7 @@ function decompose(::Type{F}, primitive) where {F<:AbstractFace} return collect_with_eltype(F, f) end -function decompose(::Type{P}, primitive) where {P<:AbstractPoint} +function decompose(::Type{P}, primitive) where {P<:Point} return collect_with_eltype(P, metafree(coordinates(primitive))) end diff --git a/src/lines.jl b/src/lines.jl index ae891fb8..1cd70360 100644 --- a/src/lines.jl +++ b/src/lines.jl @@ -79,11 +79,11 @@ function consecutive_pairs(arr) end """ - self_intersections(points::AbstractVector{AbstractPoint}) + self_intersections(points::AbstractVector{<:Point}) Finds all self intersections of polygon `points` """ -function self_intersections(points::AbstractVector{<:AbstractPoint}) +function self_intersections(points::AbstractVector{<:Point}) sections = similar(points, 0) intersections = Int[] @@ -109,12 +109,12 @@ function self_intersections(points::AbstractVector{<:AbstractPoint}) end """ - split_intersections(points::AbstractVector{AbstractPoint}) + split_intersections(points::AbstractVector{<: Point}) Splits polygon `points` into it's self intersecting parts. Only 1 intersection is handled right now. """ -function split_intersections(points::AbstractVector{<:AbstractPoint}) +function split_intersections(points::AbstractVector{<:Point}) intersections, sections = self_intersections(points) return if isempty(intersections) return [points] diff --git a/src/mat.jl b/src/mat.jl index 0838de8f..b2730a77 100644 --- a/src/mat.jl +++ b/src/mat.jl @@ -162,3 +162,26 @@ function Base.transpose(a::Mat{R, C, T}) where {R, C, T} end Mat{R, C, T}(data) end + +(*)(a::Mat{M, N, T1}, b::StaticVector{O, T2}) where {T1, T2, M, N, O} = throw(DimensionMismatch("$N != $O in $(typeof(a)) and $(typeof(b))")) + +# vector * (row vector) +@generated function *(a::StaticVector{N, T1}, b::Mat{1, M, T2}) where {T1, T2, N, M} + elements = Expr(:tuple, [Expr(:tuple, [:(a[$i] * b[$j]) for i in 1:N]...) for j in 1:M]...) + return :(similar_type(a)($elements)) +end + +# matrix * vector +@generated function *(a::Mat{M, N, T1}, b::StaticVector{N, T2}) where {T1, T2, M, N} + elements = Expr(:tuple, [Expr(:call, :+, [:(a[$i,$k]*b[$k]) for k = 1:N]...) for i in 1:M]...) + return :(similar_type(b)($elements)) +end + +function Base.getindex(mat::Mat{R, C, T}, r::Vec{NR}, c::Vec{NC}) where {R, C, NR, NC, T} + idx = CartesianIndices((NR, NC)) + data = ntuple(NR * NC) do i + ri, ci = Tuple(idx[i]) + return mat[r[ri], c[ci]] + end + return Mat{NR, NC, T}(data) +end diff --git a/src/meshes.jl b/src/meshes.jl index 9c0bbed5..899cd9c6 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -80,7 +80,7 @@ end Create a mesh from a polygon given as a vector of points, using triangulation. """ function mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace, - normaltype=nothing) where {P<:AbstractPoint{2}} + normaltype=nothing) where {P<:Point{2}} return mesh(Polygon(polygon); pointtype=pointtype, facetype=facetype, normaltype=normaltype) @@ -115,7 +115,7 @@ function uv_normal_mesh(primitive::Meshable{N}) where {N} facetype=GLTriangleFace) end -function normal_mesh(points::AbstractVector{<:AbstractPoint}, +function normal_mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:AbstractFace}) _points = decompose(Point3f, points) _faces = decompose(GLTriangleFace, faces) diff --git a/src/offsetintegers.jl b/src/offsetintegers.jl index c0043c53..8eeacdd8 100644 --- a/src/offsetintegers.jl +++ b/src/offsetintegers.jl @@ -1,4 +1,3 @@ - """ OffsetInteger{O, T} @@ -6,7 +5,7 @@ OffsetInteger type mainly for indexing. * `O` - The offset relative to Julia arrays. This helps reduce copying when communicating with 0-indexed systems such as OpenGL. """ -struct OffsetInteger{O,T<:Integer} <: Integer +struct OffsetInteger{O,T<:Integer} i::T OffsetInteger{O,T}(x::Integer) where {O,T<:Integer} = new{O,T}(T(x + O)) end @@ -37,22 +36,11 @@ OffsetInteger{O}(x::OffsetInteger) where {O} = OffsetInteger{O,eltype(x)}(x) # This constructor has a massive method invalidation as a consequence, # and doesn't seem to be needed, so let's remove it! -Base.convert(::Type{IT}, x::OffsetInteger) where {IT<:Integer} = IT(value(x)) - -Base.promote_rule(::Type{IT}, ::Type{<:OffsetInteger}) where {IT<:Integer} = IT - -function Base.promote_rule(::Type{OffsetInteger{O1,T1}}, - ::Type{OffsetInteger{O2,T2}}) where {O1,O2,T1<:Integer, - T2<:Integer} - return OffsetInteger{pure_max(O1, O2),promote_type(T1, T2)} -end - -Base.@pure pure_max(x1, x2) = x1 > x2 ? x1 : x2 - # Need to convert to Int here because of: https://github.com/JuliaLang/julia/issues/35038 -Base.to_index(I::OffsetInteger) = convert(Int, raw(OneIndex(I))) -Base.to_index(I::OffsetInteger{0}) = convert(Int, raw(I)) +Base.to_index(idx::OffsetInteger) = convert(Int, raw(OneIndex(idx))) +Base.convert(::Type{IT}, x::OffsetInteger) where {IT<:Integer} = IT(value(x)) +Base.convert(::Type{O}, x::Integer) where {O<:OffsetInteger} = O(x) # basic operators for op in (:(-), :abs) @eval Base.$op(x::T) where {T<:OffsetInteger} = T($(op)(value(x))) diff --git a/src/primitives/cylinders.jl b/src/primitives/cylinders.jl index e9467ad2..adba8b8b 100644 --- a/src/primitives/cylinders.jl +++ b/src/primitives/cylinders.jl @@ -56,7 +56,7 @@ function coordinates(c::Cylinder{2,T}, nvertices=(2, 2)) where {T} return (M * (to_pointn(Point3{T}, point) .- vo) .+ vo for point in points) end -function faces(sphere::Cylinder{2}, nvertices=(2, 2)) +function faces(::Cylinder{2}, nvertices=(2, 2)) return faces(Rect(0, 0, 1, 1), nvertices) end diff --git a/src/primitives/spheres.jl b/src/primitives/spheres.jl index de7c59b6..342ca9b1 100644 --- a/src/primitives/spheres.jl +++ b/src/primitives/spheres.jl @@ -32,7 +32,7 @@ origin(c::HyperSphere) = c.center Base.minimum(c::HyperSphere{N,T}) where {N,T} = Vec{N,T}(origin(c)) - Vec{N,T}(radius(c)) Base.maximum(c::HyperSphere{N,T}) where {N,T} = Vec{N,T}(origin(c)) + Vec{N,T}(radius(c)) -function Base.in(x::AbstractPoint, c::HyperSphere) +function Base.in(x::Point, c::HyperSphere) return norm(origin(c) - x) ≤ radius(c) end diff --git a/src/triangulation.jl b/src/triangulation.jl index 7bc8f931..28e359ac 100644 --- a/src/triangulation.jl +++ b/src/triangulation.jl @@ -13,35 +13,35 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. =# """ - area(vertices::AbstractVector{AbstractPoint{3}}, face::TriangleFace) + area(vertices::AbstractVector{Point{3}}, face::TriangleFace) Calculate the area of one triangle. """ -function area(vertices::AbstractVector{<:AbstractPoint{3,VT}}, +function area(vertices::AbstractVector{<:Point{3,VT}}, face::TriangleFace{FT}) where {VT,FT} v1, v2, v3 = vertices[face] return 0.5 * norm(orthogonal_vector(v1, v2, v3)) end """ - area(vertices::AbstractVector{AbstractPoint{3}}, faces::AbstractVector{TriangleFace}) + area(vertices::AbstractVector{Point{3}}, faces::AbstractVector{TriangleFace}) Calculate the area of all triangles. """ -function area(vertices::AbstractVector{<:AbstractPoint{3,VT}}, +function area(vertices::AbstractVector{Point{3,VT}}, faces::AbstractVector{TriangleFace{FT}}) where {VT,FT} return sum(x -> area(vertices, x), faces) end """ - area(contour::AbstractVector{AbstractPoint}}) + area(contour::AbstractVector{Point}}) Calculate the area of a polygon. For 2D points, the oriented area is returned (negative when the points are oriented clockwise). """ -function area(contour::AbstractVector{<:AbstractPoint{2,T}}) where {T} +function area(contour::AbstractVector{Point{2,T}}) where {T} length(contour) < 3 && return zero(T) A = zero(T) p = lastindex(contour) @@ -52,7 +52,7 @@ function area(contour::AbstractVector{<:AbstractPoint{2,T}}) where {T} return A * T(0.5) end -function area(contour::AbstractVector{<:AbstractPoint{3,T}}) where {T} +function area(contour::AbstractVector{Point{3,T}}) where {T} A = zero(eltype(contour)) o = first(contour) for i in (firstindex(contour) + 1):(lastindex(contour) - 1) @@ -66,7 +66,7 @@ end Determine if a point is inside of a triangle. """ -function Base.in(P::T, triangle::Triangle) where {T<:AbstractPoint} +function Base.in(P::T, triangle::Triangle) where {T<:Point} A, B, C = coordinates(triangle) ap = A .- P @@ -84,7 +84,7 @@ function Base.in(P::T, triangle::Triangle) where {T<:AbstractPoint} end -function snip(contour::AbstractVector{<:AbstractPoint}, u, v, w, n, V) +function snip(contour::AbstractVector{<:Point}, u, v, w, n, V) A = contour[V[u]] B = contour[V[v]] C = contour[V[w]] @@ -104,14 +104,14 @@ function snip(contour::AbstractVector{<:AbstractPoint}, u, v, w, n, V) end """ - decompose(facetype, contour::AbstractArray{<:AbstractPoint}) + decompose(facetype, contour::AbstractArray{<:Point}) Triangulate a Polygon without hole. Returns a Vector{`facetype`} defining indexes into `contour`. """ function decompose(::Type{FaceType}, - points::AbstractArray{P}) where {P<:AbstractPoint,FaceType<:AbstractFace} + points::AbstractArray{P}) where {P<:Point,FaceType<:AbstractFace} #= allocate and initialize list of Vertices in polygon =# result = FaceType[] diff --git a/src/viewtypes.jl b/src/viewtypes.jl index 3fbd30bb..cd7cbff2 100644 --- a/src/viewtypes.jl +++ b/src/viewtypes.jl @@ -56,13 +56,13 @@ function TupleView{N,M}(x::AbstractVector{T}; connect=false) where {T,N,M} return TupleView{NTuple{N,T},N,M,typeof(x)}(x, connect) end -@inline function connected_line(points::AbstractVector{<:AbstractPoint{N}}, +@inline function connected_line(points::AbstractVector{<:Point{N}}, skip=N) where {N} return connect(points, Line, skip) end """ - connect(points::AbstractVector{<: AbstractPoint}, P::Type{<: Polytope{N}}, skip::Int = N) + connect(points::AbstractVector{<: Point}, P::Type{<: Polytope{N}}, skip::Int = N) Creates a view that connects a number of points to a Polytope `P`. Between each polytope, `skip` elements are skipped untill the next starts. @@ -73,7 +73,7 @@ x == [Line(Point(1, 2), Point(3, 4)), Line(Point(5, 6), Point(7, 8))] """ @inline function connect(points::AbstractVector{Point}, P::Type{<:Polytope{N,T} where {N,T}}, - skip::Int=length(P)) where {Point <: AbstractPoint} + skip::Int=length(P)) where {Point <: Point} return reinterpret(Polytope(P, Point), TupleView{length(P),skip}(points)) end @@ -87,8 +87,7 @@ end return reinterpret(Face(P, T), TupleView{N,skip}(points)) end -@inline function connect(points::AbstractMatrix{T}, - P::Type{<:AbstractPoint{N}}) where {T <: Real,N} +@inline function connect(points::AbstractMatrix{T}, P::Type{<:Point{N}}) where {T <: Real, N} return if size(points, 1) === N return reinterpret(Point{N,T}, points) elseif size(points, 2) === N diff --git a/test/runtests.jl b/test/runtests.jl index 19bdf7cd..0c08a33a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -235,7 +235,7 @@ end #= So mesh works differently for MetaT - since `MetaT{Point}` not subtyped to `AbstractPoint` + since `MetaT{Point}` not subtyped to `Point` =# @testset "MetaT{Mesh}" begin From c1ea315a3ad9c7591b7772cb68de2a993be18241 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 16 Mar 2022 11:07:16 +0100 Subject: [PATCH 07/58] remove meta types --- src/GeometryBasics.jl | 18 +-- src/meshes.jl | 68 +--------- src/metadata.jl | 305 ------------------------------------------ src/viewtypes.jl | 58 -------- 4 files changed, 9 insertions(+), 440 deletions(-) delete mode 100644 src/metadata.jl diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index 7cf70776..bebe8a66 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -1,6 +1,6 @@ module GeometryBasics -using Tables, StructArrays, IterTools, LinearAlgebra +using IterTools, LinearAlgebra using EarCut_jll import Base: * @@ -17,7 +17,6 @@ include("primitives/pyramids.jl") include("primitives/particles.jl") include("interfaces.jl") -include("metadata.jl") include("viewtypes.jl") include("geometry_primitives.jl") include("meshes.jl") @@ -30,24 +29,19 @@ export Mat, Point, Vec export LineFace, Polytope, Line, NgonFace, convert_simplex export LineString, AbstractPolygon, Polygon, MultiPoint, MultiLineString, MultiPolygon export Simplex, connect, Triangle, NSimplex, Tetrahedron -export QuadFace, metafree, coordinates, TetrahedronFace -export TupleView, SimplexFace, Mesh, meta -export Triangle, TriangleP +export QuadFace, coordinates, TetrahedronFace +export TupleView, SimplexFace, Mesh +export Triangle export AbstractFace, TriangleFace, QuadFace, GLTriangleFace export OffsetInteger, ZeroIndex, OneIndex, GLIndex -export FaceView, SimpleFaceView -export AbstractPoint, PointMeta, PointWithUV -export PolygonMeta, MultiPointMeta, MultiLineStringMeta, MeshMeta, LineStringMeta, - MultiPolygonMeta export decompose, coordinates, faces, normals, decompose_uv, decompose_normals, texturecoordinates -export Tesselation, pointmeta, Normal, UV, UVW +export Tesselation, Normal, UV, UVW export AbstractMesh, Mesh -export MetaT, meta_table # all the different predefined mesh types # Note: meshes can contain arbitrary meta information, -export AbstractMesh, TriangleMesh, PlainMesh +export AbstractMesh # mesh creation functions export triangle_mesh, triangle_mesh, uv_mesh diff --git a/src/meshes.jl b/src/meshes.jl index 899cd9c6..6a2d81de 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -1,21 +1,9 @@ -const FaceMesh{Dim,T,Element} = Mesh{Dim,T,Element,<:FaceView{Element}} + const GLTriangleElement = Triangle{3,Float32} const GLTriangleFace = TriangleFace{GLIndex} - -coordinates(mesh::FaceMesh) = coordinates(getfield(mesh, :simplices)) -faces(mesh::FaceMesh) = faces(getfield(mesh, :simplices)) - -function texturecoordinates(mesh::AbstractMesh) - hasproperty(mesh, :uv) && return mesh.uv - hasproperty(mesh, :uvw) && return mesh.uvw - return nothing -end - -function normals(mesh::AbstractMesh) - hasproperty(mesh, :normals) && return mesh.normals - return nothing -end +coordinates(mesh::Mesh) = mesh.vertices +faces(mesh::Mesh) = mesh.connectivity function decompose_triangulate_fallback(primitive::Meshable; pointtype, facetype) positions = decompose(pointtype, primitive) @@ -169,53 +157,3 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) return Mesh(ps, fs) end end - -""" - pointmeta(mesh::Mesh; meta_data...) - -Attaches metadata to the coordinates of a mesh -""" -function pointmeta(mesh::Mesh; meta_data...) - points = coordinates(mesh) - attr = attributes(points) - delete!(attr, :position) # position == metafree(points) - # delete overlapping attributes so we can replace with `meta_data` - foreach(k -> delete!(attr, k), keys(meta_data)) - return Mesh(meta(metafree(points); attr..., meta_data...), faces(mesh)) -end - -function pointmeta(mesh::Mesh, uv::UV) - return pointmeta(mesh; uv=decompose(uv, mesh)) -end - -function pointmeta(mesh::Mesh, normal::Normal) - return pointmeta(mesh; normals=decompose(normal, mesh)) -end - -""" - pop_pointmeta(mesh::Mesh, property::Symbol) -Remove `property` from point metadata. -Returns the new mesh, and the property! -""" -function pop_pointmeta(mesh::Mesh, property::Symbol) - points = coordinates(mesh) - attr = attributes(points) - delete!(attr, :position) # position == metafree(points) - # delete overlapping attributes so we can replace with `meta_data` - m = pop!(attr, property) - return Mesh(meta(metafree(points); attr...), faces(mesh)), m -end - -""" - facemeta(mesh::Mesh; meta_data...) - -Attaches metadata to the faces of a mesh -""" -function facemeta(mesh::Mesh; meta_data...) - return Mesh(coordinates(mesh), meta(faces(mesh); meta_data...)) -end - -function attributes(hasmeta::Mesh) - return Dict{Symbol,Any}((name => getproperty(hasmeta, name) - for name in propertynames(hasmeta))) -end diff --git a/src/metadata.jl b/src/metadata.jl deleted file mode 100644 index c8e3378b..00000000 --- a/src/metadata.jl +++ /dev/null @@ -1,305 +0,0 @@ -#= -Helper functions that works around the fact, that there is no generic -Table interface for this functionality. Once this is in e.g. Tables.jl, -it should be removed from GeometryBasics! -=# - -""" - attributes(hasmeta) -Returns all attributes of meta as a Dict{Symbol, Any}. -Needs to be overloaded, and returns empty dict for non overloaded types! -Gets overloaded by default for all Meta types. -""" -function attributes(hasmeta) - return Dict{Symbol,Any}() -end - -function attributes(hasmeta::StructArray) - return Dict{Symbol,Any}((name => getproperty(hasmeta, name) - for name in propertynames(hasmeta))) -end - -""" - getcolumns(t, colnames::Symbol...) - -Gets a column from any Array like (Table/AbstractArray). -For AbstractVectors, a column will be the field names of the element type. -""" -function getcolumns(tablelike, colnames::Symbol...) - return getproperty.((tablelike,), colnames) -end - -getcolumn(t, colname::Symbol) = getcolumns(t, colname)[1] - -""" - MetaType(::Type{T}) - -Returns the Meta Type corresponding to `T` -E.g: -```julia -MetaType(Point) == PointMeta -``` -""" -MetaType(::Type{T}) where {T} = error("No Meta Type for $T") - -""" - MetaFree(::Type{T}) - -Returns the original type containing no metadata for `T` -E.g: -```julia -MetaFree(PointMeta) == Point -``` -""" -MetaFree(::Type{T}) where {T} = error("No meta free Type for $T") - -""" - meta(x::MetaObject) - -Returns the metadata of `x` -""" -meta(x::T) where {T} = error("$T has no meta!") - -metafree(x::T) where {T} = x - -macro meta_type(name, mainfield, supertype, params...) - MetaName = Symbol("$(name)Meta") - field = QuoteNode(mainfield) - NoParams = Symbol("$(MetaName)NoParams") - - params_sym = map(params) do param - param isa Symbol && return param - param isa Expr && param.head == :(<:) && return param.args[1] - return error("Unsupported type parameter: $(param)") - end - - expr = quote - struct $MetaName{$(params...),Typ<:$supertype{$(params_sym...)},Names,Types} <: - $supertype{$(params_sym...)} - main::Typ - meta::NamedTuple{Names,Types} - end - - const $NoParams{Typ,Names,Types} = $MetaName{$(params_sym...),Typ,Names, - Types} where {$(params_sym...)} - - function Base.getproperty(x::$MetaName{$(params_sym...),Typ,Names,Types}, - field::Symbol) where {$(params...),Typ,Names,Types} - field === $field && return getfield(x, :main) - field === :main && return getfield(x, :main) - Base.sym_in(field, Names) && return getfield(getfield(x, :meta), field) - return error("Field $field not part of Element") - end - - function GeometryBasics.MetaType(XX::Type{<:$supertype{$(params_sym...)} where {$(params...)}}) - return $MetaName - end - - function GeometryBasics.MetaType(ST::Type{<:$supertype{$(params_sym...)}}, - ::Type{NamedTuple{Names,Types}}) where {$(params...), - Names, - Types} - return $MetaName{$(params_sym...),ST,Names,Types} - end - - GeometryBasics.MetaFree(::Type{<:$MetaName{$(params_sym...),Typ}}) where {$(params_sym...), Typ<:$supertype{$(params_sym...)} } = Typ - GeometryBasics.MetaFree(::Type{<:$MetaName}) = $name - GeometryBasics.metafree(x::$MetaName) = getfield(x, :main) - GeometryBasics.metafree(x::AbstractVector{<:$MetaName}) = getproperty(x, $field) - GeometryBasics.meta(x::$MetaName) = getfield(x, :meta) - GeometryBasics.meta(x::AbstractVector{<:$MetaName}) = getproperty(x, :meta) - - function GeometryBasics.meta(main::$supertype{$(params_sym...)}; - meta...) where {$(params...)} - isempty(meta) && return elements # no meta to add! - return $MetaName(main; meta...) - end - - function GeometryBasics.meta(elements::AbstractVector{XX}; - meta...) where {XX<:$supertype{$(params_sym...)}} where {$(params...)} - isempty(meta) && return elements # no meta to add! - n = length(elements) - for (k, v) in meta - if v isa AbstractVector - mn = length(v) - mn != n && error("Metadata array needs to have same length as data. - Found $(n) data items, and $mn metadata items") - else - error("Metadata needs to be an array with the same length as data items. Found: $(typeof(v))") - end - end - nt = values(meta) - # get the first element to get the per element named tuple type - ElementNT = typeof(map(first, nt)) - - return StructArray{MetaType(XX, ElementNT)}(($(mainfield)=elements, nt...)) - end - - function GeometryBasics.attributes(hasmeta::$MetaName) - return Dict{Symbol,Any}((name => getproperty(hasmeta, name) - for name in propertynames(hasmeta))) - end - - function (MT::Type{<:$MetaName})(args...; meta...) - nt = values(meta) - obj = MetaFree(MT)(args...) - return MT(obj, nt) - end - - function (MT::Type{<:$MetaName})(main::$(name); meta...) - nt = values(meta) - return MT(main, nt) - end - - function Base.propertynames(::$MetaName{$(params_sym...),Typ,Names,Types}) where {$(params...), - Typ, - Names, - Types} - return ($field, Names...) - end - - function StructArrays.component(x::$MetaName{$(params_sym...),Typ,Names,Types}, - field::Symbol) where {$(params...),Typ,Names,Types} - return getproperty(x, field) - end - - function StructArrays.staticschema(::Type{$MetaName{$(params_sym...),Typ,Names, - Types}}) where {$(params...), - Typ,Names,Types} - return NamedTuple{($field, Names...),Base.tuple_type_cons(Typ, Types)} - end - - function StructArrays.createinstance(::Type{$MetaName{$(params_sym...),Typ,Names, - Types}}, metafree, - args...) where {$(params...),Typ,Names,Types} - return $MetaName(metafree, NamedTuple{Names,Types}(args)) - end - end - return esc(expr) -end - -@meta_type(Point, position, AbstractPoint, Dim, T) -Base.getindex(x::PointMeta, idx::Int) = getindex(metafree(x), idx) - -@meta_type(NgonFace, ngon, AbstractNgonFace, N, T) -Base.getindex(x::NgonFaceMeta, idx::Int) = getindex(metafree(x), idx) - -@meta_type(SimplexFace, simplex, AbstractSimplexFace, N, T) -Base.getindex(x::SimplexFaceMeta, idx::Int) = getindex(metafree(x), idx) - -@meta_type(Polygon, polygon, AbstractPolygon, N, T) - -@meta_type(LineString, lines, AbstractVector, P <: Line) -Base.getindex(x::LineStringMeta, idx::Int) = getindex(metafree(x), idx) -Base.size(x::LineStringMeta) = size(metafree(x)) - -@meta_type(MultiPoint, points, AbstractVector, P <: AbstractPoint) -Base.getindex(x::MultiPointMeta, idx::Int) = getindex(metafree(x), idx) -Base.size(x::MultiPointMeta) = size(metafree(x)) - -@meta_type(MultiLineString, linestrings, AbstractVector, P <: LineString) -Base.getindex(x::MultiLineStringMeta, idx::Int) = getindex(metafree(x), idx) -Base.size(x::MultiLineStringMeta) = size(metafree(x)) - -@meta_type(MultiPolygon, polygons, AbstractVector, P <: Polygon) -Base.getindex(x::MultiPolygonMeta, idx::Int) = getindex(metafree(x), idx) -Base.size(x::MultiPolygonMeta) = size(metafree(x)) - -@meta_type(Mesh, mesh, AbstractMesh, Element <: Polytope) -Base.getindex(x::MeshMeta, idx::Int) = getindex(metafree(x), idx) -Base.size(x::MeshMeta) = size(metafree(x)) - -""" - - MetaT(geometry, meta::NamedTuple) - MetaT(geometry; meta...) - -Returns a `MetaT` that holds a geometry and its metadata - -`MetaT` acts the same as `Meta` method. -The difference lies in the fact that it is designed to handle -geometries and metadata of different/heterogeneous types. - -eg: While a Point MetaGeometry is a `PointMeta`, the MetaT representation is `MetaT{Point}` -The downside being it's not subtyped to `AbstractPoint` like a `PointMeta` is. - -Example: -```julia -julia> MetaT(Point(1, 2), city = "Mumbai") -MetaT{Point{2,Int64},(:city,),Tuple{String}}([1, 2], (city = "Mumbai",)) -``` -""" -struct MetaT{T,Names,Types} - main::T - meta::NamedTuple{Names,Types} -end - -MetaT(x; kwargs...) = MetaT(x, values(kwargs)) - -""" - - metafree(x::MetaT) - metafree(x::Array{MetaT}) - -Free the MetaT from metadata -i.e. returns the geometry/array of geometries -""" -function metafree(x::MetaT) - return getfield(x, :main) -end -metafree(x::AbstractVector{<:MetaT}) = map(metafree, x) - -""" - - meta(x::MetaT) - meta(x::Array{MetaT}) - -Returns the metadata of a `MetaT` -""" -function meta(x::MetaT) - return getfield(x, :meta) -end -meta(x::AbstractVector{<:MetaT}) = map(meta, x) - -# helper methods -function Base.getproperty(x::MetaT, field::Symbol) - return if field == :main - metafree(x) - elseif field == :meta - meta(x) - else - getproperty(meta(x), field) - end -end - -Base.propertynames(x::MetaT) = (:main, propertynames(meta(x))...) -getnamestypes(::Type{MetaT{T,Names,Types}}) where {T,Names,Types} = (T, Names, Types) - -# explicitly give the "schema" of the object to StructArrays -function StructArrays.staticschema(::Type{F}) where {F<:MetaT} - T, names, types = getnamestypes(F) - return NamedTuple{(:main, names...),Base.tuple_type_cons(T, types)} -end - -# generate an instance of MetaT type -function StructArrays.createinstance(::Type{F}, x, args...) where {F<:MetaT} - T, names, types = getnamestypes(F) - return MetaT(x, NamedTuple{names,types}(args)) -end - -""" -Puts an iterable of MetaT's into a StructArray -""" -function meta_table(iter) - cols = Tables.columntable(iter) - return meta_table(first(cols), Base.tail(cols)) -end - -function meta_table(main, meta::NamedTuple{names}) where {names} - eltypes = Tuple{map(eltype, values(meta))...} - F = MetaT{eltype(main),names,eltypes} - return StructArray{F}(; main=main, meta...) -end - -Base.getindex(x::MetaT, idx::Int) = getindex(metafree(x), idx) -Base.size(x::MetaT) = size(metafree(x)) diff --git a/src/viewtypes.jl b/src/viewtypes.jl index cd7cbff2..360a0c1c 100644 --- a/src/viewtypes.jl +++ b/src/viewtypes.jl @@ -100,61 +100,3 @@ end error("Dim 1 or 2 must be equal to the point dimension!") end end - -""" - FaceView{Element, Point, Face, P, F} - -FaceView enables to link one array of points via a face array, to generate one -abstract array of elements. -E.g., this becomes possible: -``` -x = FaceView(rand(Point3f, 10), TriangleFace[(1, 2, 3), (2, 4, 5), ...]) -x[1] isa Triangle == true -x isa AbstractVector{<: Triangle} == true -# This means we can use it as a mesh: -Mesh(x) # should just work! -# Can also be used for Points: - -linestring = FaceView(points, LineFace[...]) -Polygon(linestring) -``` -""" -struct FaceView{Element,Point <: AbstractPoint,Face <: AbstractFace,P <: AbstractVector{Point},F <: AbstractVector{Face}} <: AbstractVector{Element} - elements::P - faces::F -end - -const SimpleFaceView{Dim,T,NFace,IndexType,PointType <: AbstractPoint{Dim,T},FaceType <: AbstractFace{NFace,IndexType}} = FaceView{Ngon{Dim,T,NFace,PointType},PointType,FaceType,Vector{PointType},Vector{FaceType}} - -function Base.getproperty(faceview::FaceView, name::Symbol) - return getproperty(getfield(faceview, :elements), name) -end - -function Base.propertynames(faceview::FaceView) - return propertynames(getfield(faceview, :elements)) -end - -Tables.schema(faceview::FaceView) = Tables.schema(getfield(faceview, :elements)) - -Base.size(faceview::FaceView) = size(getfield(faceview, :faces)) - -@propagate_inbounds function Base.getindex(x::FaceView{Element}, i) where {Element} - return Element(map(idx -> coordinates(x)[idx], faces(x)[i])) -end - -@propagate_inbounds function Base.setindex!(x::FaceView{Element}, element::Element, - i) where {Element} - face = faces(x)[i] - for (i, f) in enumerate(face) # TODO unroll!? - coordinates(x)[face[i]] = element[i] - end - return element -end - -function connect(points::AbstractVector{P}, - faces::AbstractVector{F}) where {P <: AbstractPoint,F <: AbstractFace} - return FaceView{Polytope(P, F),P,F,typeof(points),typeof(faces)}(points, faces) -end - -coordinates(mesh::FaceView) = getfield(mesh, :elements) -faces(mesh::FaceView) = getfield(mesh, :faces) From 924bd2ee5d57dd9e306df96516dd79738b5d3e86 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 16 Mar 2022 16:28:24 +0100 Subject: [PATCH 08/58] try to get mesh working --- src/basic_types.jl | 72 +++++++++++++++++++++++----------- src/fixed_arrays.jl | 19 ++++----- src/geometry_primitives.jl | 15 +++---- src/interfaces.jl | 13 ++++--- src/meshes.jl | 80 ++++++++------------------------------ src/viewtypes.jl | 19 +++++---- test/runtests.jl | 36 +++++------------ 7 files changed, 107 insertions(+), 147 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 7e638fd0..3edaa2d2 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -3,7 +3,7 @@ Abstract Geometry in R{Dim} with Number type T """ abstract type AbstractGeometry{Dim,T<:Number} end abstract type GeometryPrimitive{Dim,T} <: AbstractGeometry{Dim,T} end -Base.ndims(x::AbstractGeometry{Dim}) where {Dim} = Dim +Base.ndims(::AbstractGeometry{Dim}) where {Dim} = Dim """ Geometry made of N connected points. Connected as one flat geometry, it makes a Ngon / Polygon. @@ -19,6 +19,10 @@ abstract type AbstractNgonFace{N,T} <: AbstractFace{N,T} end abstract type AbstractSimplex{Dim,N,T} <: StaticVector{Dim,T} end +@propagate_inbounds function Base.getindex(points::AbstractVector{P}, face::F) where {P<: Point, F <: AbstractFace} + Polytope(P, F)(map(i-> points[i], face.data)) +end + @fixed_vector SimplexFace AbstractSimplexFace const TetrahedronFace{T} = SimplexFace{4,T} @@ -29,6 +33,7 @@ Face(::Type{<:SimplexFace{N}}, ::Type{T}) where {N,T} = SimplexFace{N,T} const LineFace{T} = NgonFace{2,T} const TriangleFace{T} = NgonFace{3,T} const QuadFace{T} = NgonFace{4,T} +const GLTriangleFace = TriangleFace{GLIndex} function Base.show(io::IO, x::TriangleFace{T}) where {T} return print(io, "TriangleFace(", join(x, ", "), ")") @@ -69,9 +74,9 @@ Base.length(::NNgon{N}) where {N} = N """ The Ngon Polytope element type when indexing an array of points with a SimplexFace """ -function Polytope(P::Type{Point{Dim,T}}, +function Polytope(::Type{Point{Dim,T}}, ::Type{<:AbstractNgonFace{N,IT}}) where {N,Dim,T,IT} - return Ngon{Dim,T,N,P} + return Ngon{Dim,T,N} end """ @@ -88,6 +93,7 @@ const Line{Dim,T} = Ngon{Dim,T,2} # for triangle: const Triangle{Dim,T} = Ngon{Dim,T,3} const Triangle3d{T} = Triangle{3,T} +const GLTriangleElement = Triangle{3,Float32} Base.show(io::IO, x::Triangle) = print(io, "Triangle(", join(x, ", "), ")") @@ -141,15 +147,15 @@ Base.length(::NSimplex{N}) where {N} = N """ The Simplex Polytope element type when indexing an array of points with a SimplexFace """ -function Polytope(P::Type{Point{Dim,T}}, ::Type{<:AbstractSimplexFace{N}}) where {N,Dim,T} - return Simplex{Dim,T,N,P} +function Polytope(::Type{Point{Dim,T}}, ::Type{<:AbstractSimplexFace{N}}) where {N,Dim,T} + return Simplex{Dim,T,N} end """ The fully concrete Simplex type, when constructed from a point type! """ function Polytope(::Type{<:NSimplex{N}}, P::Type{Point{NDim,T}}) where {N,NDim,T} - return Simplex{NDim,T,N,P} + return Simplex{NDim,T,N} end Base.show(io::IO, x::Line) = print(io, "Line(", x[1], " => ", x[2], ")") @@ -326,34 +332,54 @@ An abstract mesh is a collection of Polytope elements (Simplices / Ngons). The connections are defined via faces(mesh), the coordinates of the elements are returned by coordinates(mesh). Arbitrary meta information can be attached per point or per face """ -abstract type AbstractMesh{Element} end +abstract type AbstractMesh{Dim, T} <: AbstractGeometry{Dim, T} end """ - Mesh <: AbstractVector{Element} -The concrete AbstractMesh implementation. + Mesh <: AbstractMesh{Element} +The concrete AbstractMesh type. """ -struct Mesh{Dim,T<:Number, Element, V<:AbstractVector{Point{Dim, T}}, C} <: - AbstractMesh{Element} +struct Mesh{Dim, T<:Number, V<:AbstractVector{Point{Dim, T}}, C <: AbstractVector{<: AbstractFace}} <: AbstractMesh{Dim, T} vertices::V connectivity::C end +coordinates(mesh::Mesh) = mesh.vertices +faces(mesh::Mesh) = mesh.connectivity +Base.getindex(mesh::Mesh, i::Integer) = mesh.vertices[mesh.connectivity[i]] +Base.length(mesh::Mesh) = length(mesh.connectivity) -function Base.summary(io::IO, ::Mesh{Dim,T,Element}) where {Dim,T,Element} - print(io, "Mesh{$Dim, $T, ") - summary(io, Element) - return print(io, "}") -end - -function Mesh(elements::AbstractVector{<:Polytope{Dim,T}}) where {Dim,T} - return Mesh{Dim,T,eltype(elements),typeof(elements)}(elements, nothing) +function Base.iterate(mesh::Mesh, i=1) + return i - 1 < length(mesh) ? (mesh[i], i + 1) : nothing end -function Mesh(points::AbstractVector{<:Point}, - faces::AbstractVector{<:AbstractFace}) - return Mesh(points, faces) +function Mesh(points::AbstractVector{Point{Dim, T}}, + faces::AbstractVector{<:AbstractFace}) where {Dim, T} + return Mesh{Dim, T, }(points, faces) end function Mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Integer}, facetype=TriangleFace, skip=1) - return Mesh(connect(points, connect(faces, facetype, skip))) + return Mesh(points, connect(faces, facetype, skip)) +end + +struct MetaMesh{Dim, T, M <: AbstractMesh{Dim, T}, Names, Types} <: AbstractMesh{Dim, T} + mesh::M + meta::NamedTuple{Names, Types} + function MetaMesh(mesh::AbstractMesh{Dim, T}, meta::NamedTuple{Names, Types}) where {Dim, T, Names, Types} + new{Dim, T, typeof(mesh), Names, Types}(mesh, meta) + end +end + +function MetaMesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:AbstractFace}; meta...) + MetaMesh(Mesh(points, faces), values(meta)) end + +function MetaMesh(m::AbstractMesh; meta...) + MetaMesh(m, values(meta)) +end + +@inline Base.hasproperty(mesh::MetaMesh, field::Symbol) = hasproperty(getfield(mesh, :meta), field) +@inline Base.getproperty(mesh::MetaMesh, field::Symbol) = getproperty(getfield(mesh, :meta), field) +coordinates(mesh::MetaMesh) = coordinates(getfield(mesh, :mesh)) +faces(mesh::MetaMesh) = faces(getfield(mesh, :mesh)) +normals(mesh::MetaMesh) = hasproperty(mesh, :normals) ? mesh.normals : nothing +texturecoordinates(mesh::MetaMesh) = hasproperty(mesh, :uv) ? mesh.uv : nothing diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index d4df208f..dcee301b 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -80,7 +80,7 @@ macro fixed_vector(VecT, SuperT) @inline similar_type(::$(VecT){N, T}, n::Integer) where {N, T} = $(VecT){n, T} @inline similar_type(::$(VecT){N}, ::Type{T}) where {N, T} = $(VecT){N, T} - @inline similar_type(::$(VecT), ::Integer, ::Type{T}) where {N, T} = $(VecT){N, T} + @inline similar_type(::$(VecT), n::Integer, ::Type{T}) where {N, T} = $(VecT){n, T} @inline similar_type(::$(VecT)) = $(VecT) function Base.broadcasted(f, a::$(VecT), b::$(VecT)) @@ -97,14 +97,10 @@ macro fixed_vector(VecT, SuperT) end Base.broadcasted(f, a::StaticVector) = similar_type(a)(f.(a.data)) -Base.broadcasted(::typeof(+), a::StaticVector, b::OneTo{Int64}) = similar_type(a)((a.data .+ b)) +Base.broadcasted(::typeof(+), a::StaticVector, b::Base.OneTo{Int64}) = similar_type(a)((a.data .+ b)) Base.broadcasted(f, a::StaticVector, b) = similar_type(a)(f.(a.data, b)) Base.broadcasted(f, a, b::StaticVector) = similar_type(b)(f.(a, b.data)) -Base.@propagate_inbounds function Base.getindex(a::AbstractArray{T}, idx::StaticVector{N, <:Integer}) where {N,T} - return similar_type(idx, N, T)(map(i-> a[i], idx)) -end - Base.map(f, b::StaticVector) = similar_type(b)(map(f, b.data)) function Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{V}) where V <: StaticVector{N,T} where {N, T} @@ -126,11 +122,13 @@ end Base.:(-)(a::StaticVector) = (-).(a) -for op in [:(Base.:(*)), :(Base.:(+)), :(Base.:(-))] +import Base: *, +, - + +for op in [:*, :+, :-] @eval begin - ($op)(a::StaticVector, b::StaticVector) = a .* b - ($op)(a::Number, b::StaticVector) = a .* b - ($op)(a::StaticVector, b::Number) = a .* b + ($op)(a::StaticVector, b::StaticVector) = Base.broadcasted($(op), a, b) + ($op)(a::Number, b::StaticVector) = Base.broadcasted($(op), a, b) + ($op)(a::StaticVector, b::Number) = Base.broadcasted($(op), a, b) end end @@ -188,7 +186,6 @@ LinearAlgebra.promote_leaf_eltypes(x::StaticVector{N, T}) where {N,T} = T @fixed_vector Point StaticVector @fixed_vector Vec StaticVector - Base.lastindex(::StaticVector{N}) where N = N Base.broadcasted(f, a::Point, b::Vec) = Vec(f.(a.data, b.data)) diff --git a/src/geometry_primitives.jl b/src/geometry_primitives.jl index 295c74a4..89cfcc5e 100644 --- a/src/geometry_primitives.jl +++ b/src/geometry_primitives.jl @@ -92,8 +92,8 @@ end The unnormalized normal of three vertices. """ function orthogonal_vector(v1, v2, v3) - a = v2 - v1 - b = v3 - v1 + a = v2 .- v1 + b = v3 .- v1 return cross(a, b) end @@ -110,16 +110,17 @@ function normals(vertices::AbstractVector{Point{3,T}}, faces::AbstractVector{F}; return normals(vertices, faces, normaltype) end -function normals(vertices::AbstractVector{Point{3,T}}, faces::AbstractVector{F}, - ::Type{N}) where {T,F<:NgonFace,N} - normals_result = zeros(N, length(vertices)) +function normals(vertices::AbstractVector{<:Point{3}}, faces::AbstractVector{F}, + ::Type{NormalType}) where {F<:NgonFace,NormalType} + normals_result = zeros(NormalType, length(vertices)) + @show NormalType for face in faces - v = metafree.(vertices[face]) + v = vertices[face] # we can get away with two edges since faces are planar. n = orthogonal_vector(v[1], v[2], v[3]) for i in 1:length(F) fi = face[i] - normals_result[fi] = normals_result[fi] + n + normals_result[fi] = normals_result[fi] .+ n end end normals_result .= normalize.(normals_result) diff --git a/src/interfaces.jl b/src/interfaces.jl index f2ee1f7d..c350c542 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -106,15 +106,15 @@ function decompose(::Type{F}, primitive) where {F<:AbstractFace} end function decompose(::Type{P}, primitive) where {P<:Point} - return collect_with_eltype(P, metafree(coordinates(primitive))) + return collect_with_eltype(P, coordinates(primitive)) end function decompose(::Type{Point}, primitive::Meshable{Dim,T}) where {Dim,T} - return collect_with_eltype(Point{Dim,T}, metafree(coordinates(primitive))) + return collect_with_eltype(Point{Dim,T}, coordinates(primitive)) end function decompose(::Type{Point}, primitive::LineString{Dim,T}) where {Dim,T} - return collect_with_eltype(Point{Dim,T}, metafree(coordinates(primitive))) + return collect_with_eltype(Point{Dim,T}, coordinates(primitive)) end function decompose(::Type{T}, primitive) where {T} @@ -128,7 +128,7 @@ decompose_normals(primitive) = decompose(Normal(), primitive) function decompose(NT::Normal{T}, primitive) where {T} n = normals(primitive) if n === nothing - return collect_with_eltype(T, normals(coordinates(primitive), faces(primitive))) + return collect_with_eltype(T, normals(decompose(Point, primitive), faces(primitive), T)) end return collect_with_eltype(T, n) end @@ -141,7 +141,7 @@ function decompose(UVT::Union{UV{T},UVW{T}}, primitive) where {T} # If the primitive doesn't even have coordinates, we're out of options and return # nothing, indicating that texturecoordinates aren't implemented positions = decompose(Point, primitive) - positions === nothing && return nothing + isnothing(positions) && return nothing # Let this overlord do the work return decompose(UVT, positions) end @@ -153,8 +153,9 @@ function decompose(UVT::Union{UV{T},UVW{T}}, N = length(T) positions_nd = decompose(Point{N,eltype(T)}, positions) bb = Rect(positions_nd) # Make sure we get this as points + mini, w = minimum(bb), widths(bb) return map(positions_nd) do p - return T((p .- minimum(bb)) ./ widths(bb)) + return T((p .- mini) ./ w) end end diff --git a/src/meshes.jl b/src/meshes.jl index 6a2d81de..621f5ee6 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -1,11 +1,4 @@ - -const GLTriangleElement = Triangle{3,Float32} -const GLTriangleFace = TriangleFace{GLIndex} - -coordinates(mesh::Mesh) = mesh.vertices -faces(mesh::Mesh) = mesh.connectivity - -function decompose_triangulate_fallback(primitive::Meshable; pointtype, facetype) +function decompose_triangulate_fallback(primitive::Meshable, pointtype, facetype) positions = decompose(pointtype, primitive) faces = decompose(facetype, primitive) # If faces returns nothing for primitive, we try to triangulate! @@ -29,36 +22,9 @@ Note, that this can be an `Int` or `Tuple{Int, Int}``, when the primitive is gri It also only losely correlates to the number of vertices, depending on the algorithm used. #TODO: find a better number here! """ -function mesh(primitive::Meshable; pointtype=Point, facetype=GLTriangleFace, uv=nothing, - normaltype=nothing) - - positions, faces = decompose_triangulate_fallback(primitive; pointtype=pointtype, facetype=facetype) - - # We want to preserve any existing attributes! - attrs = attributes(primitive) - # Make sure this doesn't contain position, we'll add position explicitely via meta! - delete!(attrs, :position) - - if uv !== nothing - # this may overwrite an existing :uv, but will only create a copy - # if it has a different eltype, otherwise it should replace it - # with exactly the same instance - which is what we want here - attrs[:uv] = decompose(UV(uv), primitive) - end - - if normaltype !== nothing - primitive_normals = normals(primitive) - if primitive_normals !== nothing - attrs[:normals] = decompose(normaltype, primitive_normals) - else - # Normals not implemented for primitive, so we calculate them! - n = normals(positions, faces; normaltype=normaltype) - if n !== nothing # ok jeez, this is a 2d mesh which cant have normals - attrs[:normals] = n - end - end - end - return Mesh(meta(positions; attrs...), faces) +function mesh(primitive::Meshable; pointtype=Point, facetype=GLTriangleFace) + positions, faces = decompose_triangulate_fallback(primitive, pointtype, facetype) + return Mesh(positions, faces) end """ @@ -74,49 +40,35 @@ function mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace, normaltype=normaltype) end -function mesh(polygon::AbstractPolygon{Dim,T}; pointtype=Point{Dim,T}, - facetype=GLTriangleFace, normaltype=nothing) where {Dim,T} - - positions, faces = decompose_triangulate_fallback(polygon; pointtype=pointtype, facetype=facetype) - - if normaltype !== nothing - n = normals(positions, faces; normaltype=normaltype) - positions = meta(positions; normals=n) - end +function mesh(polygon::AbstractPolygon{Dim,T}) where {Dim,T} + positions, faces = decompose_triangulate_fallback(polygon; pointtype=Point{Dim, T}, facetype=GLTriangleFace) return Mesh(positions, faces) end -function triangle_mesh(primitive::Meshable{N}; nvertices=nothing) where {N} - if nvertices !== nothing - @warn("nvertices argument deprecated. Wrap primitive in `Tesselation(primitive, nvertices)`") - primitive = Tesselation(primitive, nvertices) - end - return mesh(primitive; pointtype=Point{N,Float32}, facetype=GLTriangleFace) +function triangle_mesh(primitive::Meshable{N}) where {N} + return Mesh(decompose(Point{N,Float32}, primitive), decompose(GLTriangleFace, primitive)) end function uv_mesh(primitive::Meshable{N,T}) where {N,T} - return mesh(primitive; pointtype=Point{N,Float32}, uv=Vec2f, facetype=GLTriangleFace) + m = triangle_mesh(primitive) + return MetaMesh(m, (uv=decompose_uv(primitive),)) end function uv_normal_mesh(primitive::Meshable{N}) where {N} - return mesh(primitive; pointtype=Point{N,Float32}, uv=Vec2f, normaltype=Vec3f, - facetype=GLTriangleFace) + m = triangle_mesh(primitive) + return MetaMesh(m, (uv=decompose_uv(primitive), normals=decompose_normals(primitive))) end function normal_mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:AbstractFace}) _points = decompose(Point3f, points) _faces = decompose(GLTriangleFace, faces) - return Mesh(meta(_points; normals=normals(_points, _faces)), _faces) + return MetaMesh(Mesh(_points, _faces), (normals=normals(_points, _faces),)) end -function normal_mesh(primitive::Meshable{N}; nvertices=nothing) where {N} - if nvertices !== nothing - @warn("nvertices argument deprecated. Wrap primitive in `Tesselation(primitive, nvertices)`") - primitive = Tesselation(primitive, nvertices) - end - return mesh(primitive; pointtype=Point{N,Float32}, normaltype=Vec3f, - facetype=GLTriangleFace) +function normal_mesh(primitive::Meshable{N}) where {N} + m = triangle_mesh(primitive) + return MetaMesh(m, (normals=decompose_normals(primitive),)) end """ diff --git a/src/viewtypes.jl b/src/viewtypes.jl index 360a0c1c..82c8e553 100644 --- a/src/viewtypes.jl +++ b/src/viewtypes.jl @@ -56,7 +56,7 @@ function TupleView{N,M}(x::AbstractVector{T}; connect=false) where {T,N,M} return TupleView{NTuple{N,T},N,M,typeof(x)}(x, connect) end -@inline function connected_line(points::AbstractVector{<:Point{N}}, +function connected_line(points::AbstractVector{<:Point{N}}, skip=N) where {N} return connect(points, Line, skip) end @@ -71,23 +71,22 @@ Example: x = connect(Point[(1, 2), (3, 4), (5, 6), (7, 8)], Line, 2) x == [Line(Point(1, 2), Point(3, 4)), Line(Point(5, 6), Point(7, 8))] """ -@inline function connect(points::AbstractVector{Point}, +function connect(points::AbstractVector{Point}, P::Type{<:Polytope{N,T} where {N,T}}, skip::Int=length(P)) where {Point <: Point} - return reinterpret(Polytope(P, Point), TupleView{length(P),skip}(points)) + return map(Polytope(P, Point), TupleView{length(P),skip}(points)) end -@inline function connect(points::AbstractVector{T}, P::Type{<:Point{N}}, - skip::Int=N) where {T <: Real,N} - return reinterpret(Point{N,T}, TupleView{N,skip}(points)) +function connect(points::AbstractVector{T}, ::Type{<:Point{N}}, skip::Int=N) where {T <: Real,N} + return map(Point{N,T}, TupleView{N,skip}(points)) end -@inline function connect(points::AbstractVector{T}, P::Type{<:AbstractFace{N}}, - skip::Int=N) where {T <: Real,N} - return reinterpret(Face(P, T), TupleView{N,skip}(points)) +function connect(indices::AbstractVector{T}, P::Type{<:AbstractFace{N}}, + skip::Int=N) where {T <: Integer, N} + return map(Face(P, T), TupleView{N, skip}(indices)) end -@inline function connect(points::AbstractMatrix{T}, P::Type{<:Point{N}}) where {T <: Real, N} +function connect(points::AbstractMatrix{T}, P::Type{<:Point{N}}) where {T <: Real, N} return if size(points, 1) === N return reinterpret(Point{N,T}, points) elseif size(points, 2) === N diff --git a/test/runtests.jl b/test/runtests.jl index 0c08a33a..71740067 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -304,21 +304,6 @@ end end end - @testset "face views" begin - numbers = [1, 2, 3, 4, 5, 6] - points = connect(numbers, Point{2}) - faces = connect([1, 2, 3], TriangleFace) - triangles = connect(points, faces) - @test triangles == [Triangle(Point(1, 2), Point(3, 4), Point(5, 6))] - x = Point{3}(1.0) - triangles = connect([x], [TriangleFace(1, 1, 1)]) - @test triangles == [Triangle(x, x, x)] - points = connect([1, 2, 3, 4, 5, 6, 7, 8], Point{2}) - faces = connect([1, 2, 3, 4], SimplexFace{4}) - triangles = connect(points, faces) - @test triangles == [Tetrahedron(points...)] - end - @testset "reinterpret" begin numbers = collect(reshape(1:6, 2, 3)) points = reinterpret(Point{2, Int}, numbers) @@ -383,36 +368,35 @@ end end @testset "Mesh" begin - numbers = [1, 2, 3, 4, 5, 6] points = connect(numbers, Point{2}) mesh = Mesh(points, [1,2,3]) - @test mesh == [Triangle(points...)] + @test faces(mesh) == [TriangleFace(1, 2, 3)] x = Point{3}(1.0) mesh = Mesh([x], [TriangleFace(1, 1, 1)]) - @test mesh == [Triangle(x, x, x)] + @test coordinates(mesh) == [x] + @test faces(mesh) == [TriangleFace(1, 1, 1)] points = connect([1, 2, 3, 4, 5, 6, 7, 8], Point{2}) - faces = connect([1, 2, 3, 4], SimplexFace{4}) - mesh = Mesh(points, faces) - @test mesh == [Tetrahedron(points...)] + f = connect([1, 2, 3, 4], SimplexFace{4}) + mesh = Mesh(points, f) + @test collect(mesh) == [Tetrahedron(points...)] points = rand(Point3f, 8) tfaces = [GLTriangleFace(1, 2, 3), GLTriangleFace(5, 6, 7)] normals = rand(Vec3f, 8) uv = rand(Vec2f, 8) mesh = Mesh(points, tfaces) - meshuv = Mesh(meta(points; uv=uv), tfaces) - meshuvnormal = Mesh(meta(points; normals=normals, uv=uv), tfaces) - + meshuv = MetaMesh(points, tfaces; uv=uv) + meshuvnormal = MetaMesh(points, tfaces; normals=normals, uv=uv) t = Tesselation(Rect2f(0, 0, 2, 2), (30, 30)) - m = GeometryBasics.mesh(t, pointtype=Point3f, facetype=QuadFace{Int}) + + m = GeometryBasics.mesh(t; pointtype=Point3f, facetype=QuadFace{Int}) m2 = GeometryBasics.mesh(m, facetype=QuadFace{GLIndex}) @test GeometryBasics.faces(m2) isa Vector{QuadFace{GLIndex}} @test GeometryBasics.coordinates(m2) isa Vector{Point3f} - end @testset "Multi geometries" begin From a8f3293d834ee6b5306fcabb3b57ba6dd970cbc5 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Thu, 17 Mar 2022 20:47:40 +0100 Subject: [PATCH 09/58] remove all the things!! --- src/GeometryBasics.jl | 2 +- src/basic_types.jl | 125 +----- src/fixed_arrays.jl | 5 +- src/geometry_primitives.jl | 2 +- src/interfaces.jl | 55 +-- src/meshes.jl | 100 +++-- src/offsetintegers.jl | 27 +- src/primitive = Circle(Point2f(0), 1f0).jl | 45 +++ src/primitives/spheres.jl | 12 +- src/triangulation.jl | 7 +- test/fixed_arrays.jl | 1 - test/runtests.jl | 436 ++------------------- 12 files changed, 226 insertions(+), 591 deletions(-) create mode 100644 src/primitive = Circle(Point2f(0), 1f0).jl diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index bebe8a66..2c4af96c 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -27,7 +27,7 @@ include("boundingboxes.jl") export AbstractGeometry, GeometryPrimitive export Mat, Point, Vec export LineFace, Polytope, Line, NgonFace, convert_simplex -export LineString, AbstractPolygon, Polygon, MultiPoint, MultiLineString, MultiPolygon +export AbstractPolygon, Polygon export Simplex, connect, Triangle, NSimplex, Tetrahedron export QuadFace, coordinates, TetrahedronFace export TupleView, SimplexFace, Mesh diff --git a/src/basic_types.jl b/src/basic_types.jl index 3edaa2d2..7ac39edf 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -159,73 +159,6 @@ function Polytope(::Type{<:NSimplex{N}}, P::Type{Point{NDim,T}}) where {N,NDim,T end Base.show(io::IO, x::Line) = print(io, "Line(", x[1], " => ", x[2], ")") -""" - LineString(points::AbstractVector{<:Point}) - -A LineString is a geometry of connected line segments -""" -struct LineString{Dim,T<:Real,V<:AbstractVector{Line{Dim,T}}} - points::V -end - -coordinates(x::LineString) = coordinates(x.points) - -Base.copy(x::LineString) = LineString(copy(x.points)) -Base.size(x::LineString) = size(getfield(x, :points)) -Base.getindex(x::LineString, i) = getindex(getfield(x, :points), i) - -function LineString(points::AbstractVector{Line{Dim,T}}) where {Dim,T} - return LineString{Dim,T,typeof(points)}(points) -end - -""" - LineString(points::AbstractVector{<: Point}, skip = 1) - -Creates a LineString from a vector of points. -With `skip == 1`, the default, it will connect the line like this: -```julia -points = Point[a, b, c, d] -linestring = LineString(points) -@assert linestring == LineString([a => b, b => c, c => d]) -``` -""" -function LineString(points::AbstractVector{<:Point}, skip=1) - return LineString(connect(points, Line, skip)) -end - -function LineString(points::AbstractVector{<:Pair{Point{N,T},Point{N,T}}}) where {N, T} - return LineString(reinterpret(Line{N,T}, points)) -end - -function LineString(points::AbstractVector{<:Point}, - faces::AbstractVector{<:LineFace}) - return LineString(connect(points, faces)) -end - -""" - LineString(points::AbstractVector{<: Point}, indices::AbstractVector{<: Integer}, skip = 1) - -Creates a LineString from a vector of points and an index list. -With `skip == 1`, the default, it will connect the line like this: - - -```julia -points = Point[a, b, c, d]; faces = [1, 2, 3, 4] -linestring = LineString(points, faces) -@assert linestring == LineString([a => b, b => c, c => d]) -``` -To make a segmented line, set skip to 2 -```julia -points = Point[a, b, c, d]; faces = [1, 2, 3, 4] -linestring = LineString(points, faces, 2) -@assert linestring == LineString([a => b, c => d]) -``` -""" -function LineString(points::AbstractVector{<:Point}, - indices::AbstractVector{<:Integer}, skip=1) - faces = connect(indices, LineFace, skip) - return LineString(points, faces) -end """ Polygon(exterior::AbstractVector{<:Point}) @@ -264,12 +197,10 @@ end function Polygon(exterior::AbstractVector{Point{Dim, T}}, interior::AbstractVector{<:AbstractVector{Point{Dim, T}}}) where {Dim,T} - ext = LineString(exterior) - # We need to take extra care for empty interiors, since # if we just map over it, it won't infer the element type correctly! - int = typeof(ext)[] - foreach(x -> push!(int, LineString(x)), interior) - return Polygon(ext, int) + int = typeof(exterior)[] + foreach(x -> push!(int, x), interior) + return Polygon(exterior, int) end function coordinates(polygon::Polygon{N,T}) where {N,T} @@ -284,47 +215,6 @@ function coordinates(polygon::Polygon{N,T}) where {N,T} end end -""" - MultiPolygon(polygons::AbstractPolygon) -""" -struct MultiPolygon{Dim,T<:Real,A<:AbstractVector{<:AbstractPolygon{Dim,T}}} - polygons::A -end - -function MultiPolygon(polygons::AbstractVector{<: AbstractPolygon{Dim,T}}) where {Dim,T} - return MultiPolygon(polygons) -end - -Base.getindex(mp::MultiPolygon, i) = mp.polygons[i] -Base.size(mp::MultiPolygon) = size(mp.polygons) - -struct MultiLineString{Dim,T<:Real,A<:AbstractVector{LineString{Dim,T}}} - linestrings::A -end - -function MultiLineString(linestrings::AbstractVector{L}) where {L<:AbstractVector{Line{Dim,T}}} where {Dim,T} - return MultiLineString(linestrings) -end - -Base.getindex(ms::MultiLineString, i) = ms.linestrings[i] -Base.size(ms::MultiLineString) = size(ms.linestrings) - -""" - MultiPoint(points::AbstractVector{<: Point}) - -A collection of points -""" -struct MultiPoint{Dim,T<:Real,A<:AbstractVector{Point{Dim,T}}} - points::A -end - -function MultiPoint(points::AbstractVector{Point{Dim,T}}) where {Dim,T} - return MultiPoint(points) -end - -Base.getindex(mpt::MultiPoint, i) = mpt.points[i] -Base.size(mpt::MultiPoint) = size(mpt.points) - """ AbstractMesh @@ -379,7 +269,12 @@ end @inline Base.hasproperty(mesh::MetaMesh, field::Symbol) = hasproperty(getfield(mesh, :meta), field) @inline Base.getproperty(mesh::MetaMesh, field::Symbol) = getproperty(getfield(mesh, :meta), field) -coordinates(mesh::MetaMesh) = coordinates(getfield(mesh, :mesh)) -faces(mesh::MetaMesh) = faces(getfield(mesh, :mesh)) +@inline Base.propertynames(mesh::MetaMesh) = propertynames(getfield(mesh, :meta)) + +coordinates(mesh::MetaMesh) = coordinates(Mesh(mesh)) +faces(mesh::MetaMesh) = faces(Mesh(mesh)) normals(mesh::MetaMesh) = hasproperty(mesh, :normals) ? mesh.normals : nothing texturecoordinates(mesh::MetaMesh) = hasproperty(mesh, :uv) ? mesh.uv : nothing + +meta(mesh::MetaMesh) = getfield(mesh, :meta) +Mesh(mesh::MetaMesh) = getfield(mesh, :mesh) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index dcee301b..cf81c887 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -78,7 +78,7 @@ macro fixed_vector(VecT, SuperT) end Base.convert(::Type{$(VecT)}, x::Tuple) = $(VecT)(x) - @inline similar_type(::$(VecT){N, T}, n::Integer) where {N, T} = $(VecT){n, T} + @inline similar_type(::$(VecT){N, T}, n::Integer) where {N, T} = $(VecT){n} @inline similar_type(::$(VecT){N}, ::Type{T}) where {N, T} = $(VecT){N, T} @inline similar_type(::$(VecT), n::Integer, ::Type{T}) where {N, T} = $(VecT){n, T} @inline similar_type(::$(VecT)) = $(VecT) @@ -99,7 +99,8 @@ end Base.broadcasted(f, a::StaticVector) = similar_type(a)(f.(a.data)) Base.broadcasted(::typeof(+), a::StaticVector, b::Base.OneTo{Int64}) = similar_type(a)((a.data .+ b)) Base.broadcasted(f, a::StaticVector, b) = similar_type(a)(f.(a.data, b)) -Base.broadcasted(f, a, b::StaticVector) = similar_type(b)(f.(a, b.data)) +Base.broadcasted(f, a, b::StaticVector{N}) where N = similar_type(b, N)(f.(a, b.data)) +Base.broadcasted(f, a::AbstractArray, b::StaticVector{N}) where N = f.(a, (b,)) Base.map(f, b::StaticVector) = similar_type(b)(map(f, b.data)) diff --git a/src/geometry_primitives.jl b/src/geometry_primitives.jl index 89cfcc5e..a0da242d 100644 --- a/src/geometry_primitives.jl +++ b/src/geometry_primitives.jl @@ -9,6 +9,7 @@ end ## # conversion & decompose convert_simplex(::Type{T}, x::T) where {T} = (x,) +convert_simplex(::Type{Vec{N, T}}, x::Vec{N, T}) where {N, T} = x function convert_simplex(NFT::Type{NgonFace{N,T1}}, f::Union{NgonFace{N,T2}}) where {T1,T2,N} @@ -113,7 +114,6 @@ end function normals(vertices::AbstractVector{<:Point{3}}, faces::AbstractVector{F}, ::Type{NormalType}) where {F<:NgonFace,NormalType} normals_result = zeros(NormalType, length(vertices)) - @show NormalType for face in faces v = vertices[face] # we can get away with two edges since faces are planar. diff --git a/src/interfaces.jl b/src/interfaces.jl index c350c542..5b083834 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -53,7 +53,7 @@ For grid based tesselation, you can also use a tuple: rect = Rect2(0, 0, 1, 1) Tesselation(rect, (5, 5)) """ -struct Tesselation{Dim,T,Primitive,NGrid} +struct Tesselation{Dim,T,Primitive,NGrid} <: AbstractGeometry{Dim, T} primitive::Primitive nvertices::NTuple{NGrid,Int} end @@ -84,11 +84,6 @@ end # Dispatch type to make `decompose(UV{Vec2f}, primitive)` work # and to pass through tesselation information -# Types that can be converted to a mesh via the functions below -const Meshable{Dim,T} = Union{Tesselation{Dim,T},Mesh{Dim,T},AbstractPolygon{Dim,T}, - GeometryPrimitive{Dim,T}, - AbstractVector{Point{Dim,T}}} - struct UV{T} end UV(::Type{T}) where {T} = UV{T}() UV() = UV(Vec2f) @@ -99,21 +94,30 @@ struct Normal{T} end Normal(::Type{T}) where {T} = Normal{T}() Normal() = Normal(Vec3f) -function decompose(::Type{F}, primitive) where {F<:AbstractFace} +function decompose(::Type{F}, primitive::AbstractGeometry) where {F<:AbstractFace} f = faces(primitive) - f === nothing && return nothing + if isnothing(f) + if ndims(primitive) == 2 + # if 2d, we can fallback to Polygon triangulation + return decompose(F, Polygon(decompose(Point, primitive))) + else + return nothing + end + end return collect_with_eltype(F, f) end -function decompose(::Type{P}, primitive) where {P<:Point} - return collect_with_eltype(P, coordinates(primitive)) +function decompose(::Type{F}, f::AbstractVector) where {F<:AbstractFace} + fs = faces(f) + isnothing(fs) && error("No faces defined for $(typeof(f))") + return collect_with_eltype(F, fs) end -function decompose(::Type{Point}, primitive::Meshable{Dim,T}) where {Dim,T} - return collect_with_eltype(Point{Dim,T}, coordinates(primitive)) +function decompose(::Type{P}, primitive) where {P<:Point} + return collect_with_eltype(P, coordinates(primitive)) end -function decompose(::Type{Point}, primitive::LineString{Dim,T}) where {Dim,T} +function decompose(::Type{Point}, primitive::AbstractGeometry{Dim,T}) where {Dim,T} return collect_with_eltype(Point{Dim,T}, coordinates(primitive)) end @@ -125,10 +129,18 @@ decompose_uv(primitive) = decompose(UV(), primitive) decompose_uvw(primitive) = decompose(UVW(), primitive) decompose_normals(primitive) = decompose(Normal(), primitive) -function decompose(NT::Normal{T}, primitive) where {T} - n = normals(primitive) - if n === nothing - return collect_with_eltype(T, normals(decompose(Point, primitive), faces(primitive), T)) +function decompose(::Normal{T}, primitive) where {T} + _n = normals(primitive) + if isnothing(_n) + # For 3D primitives, we can calculate the normals from the vertices + faces + if ndims(primitive) == 3 + n = normals(decompose(Point, primitive), faces(primitive), T) + else + points = decompose(Point, primitive) + n = (Vec3f(0, 0, 1) for p in points) + end + else + n = _n end return collect_with_eltype(T, n) end @@ -148,8 +160,7 @@ function decompose(UVT::Union{UV{T},UVW{T}}, primitive) where {T} return collect_with_eltype(T, uv) end -function decompose(UVT::Union{UV{T},UVW{T}}, - positions::AbstractVector{<:VecTypes}) where {T} +function decompose(::Union{UV{T},UVW{T}}, positions::AbstractVector{<:VecTypes}) where {T} N = length(T) positions_nd = decompose(Point{N,eltype(T)}, positions) bb = Rect(positions_nd) # Make sure we get this as points @@ -158,9 +169,3 @@ function decompose(UVT::Union{UV{T},UVW{T}}, return T((p .- mini) ./ w) end end - -# Stay backward compatible: - -function decompose(::Type{T}, primitive::Meshable, nvertices) where {T} - return decompose(T, Tesselation(primitive, nvertices)) -end diff --git a/src/meshes.jl b/src/meshes.jl index 621f5ee6..8d03347e 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -1,14 +1,3 @@ -function decompose_triangulate_fallback(primitive::Meshable, pointtype, facetype) - positions = decompose(pointtype, primitive) - faces = decompose(facetype, primitive) - # If faces returns nothing for primitive, we try to triangulate! - if faces === nothing - # triangulation.jl - faces = decompose(facetype, positions) - end - return positions, faces -end - """ mesh(primitive::GeometryPrimitive; pointtype=Point, facetype=GLTriangle, @@ -22,9 +11,21 @@ Note, that this can be an `Int` or `Tuple{Int, Int}``, when the primitive is gri It also only losely correlates to the number of vertices, depending on the algorithm used. #TODO: find a better number here! """ -function mesh(primitive::Meshable; pointtype=Point, facetype=GLTriangleFace) - positions, faces = decompose_triangulate_fallback(primitive, pointtype, facetype) - return Mesh(positions, faces) +function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleFace) + positions = decompose(pointtype, primitive) + _f = faces(primitive) + # If faces returns nothing for primitive, we try to triangulate! + if isnothing(_f) + if eltype(positions) <: Point2 + # triangulation.jl + f = decompose(facetype, positions) + else + error("No triangulation for $(typeof(primitive))") + end + else + f = decompose(facetype, _f) + end + return Mesh(positions, f) end """ @@ -33,30 +34,26 @@ end Create a mesh from a polygon given as a vector of points, using triangulation. """ -function mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace, - normaltype=nothing) where {P<:Point{2}} - - return mesh(Polygon(polygon); pointtype=pointtype, facetype=facetype, - normaltype=normaltype) +function mesh(polygon::AbstractVector{P}; pointtype=P) where {P<:Point{2}} + return mesh(Polygon(polygon); pointtype=pointtype) end -function mesh(polygon::AbstractPolygon{Dim,T}) where {Dim,T} - positions, faces = decompose_triangulate_fallback(polygon; pointtype=Point{Dim, T}, facetype=GLTriangleFace) - return Mesh(positions, faces) +function triangle_mesh(primitive::AbstractGeometry{N}) where {N} + return mesh(primitive; pointtype=Point{N, Float32}) end -function triangle_mesh(primitive::Meshable{N}) where {N} - return Mesh(decompose(Point{N,Float32}, primitive), decompose(GLTriangleFace, primitive)) +function triangle_mesh(primitive::AbstractVector{<: Point}) + return mesh(Polygon(primitive)) end -function uv_mesh(primitive::Meshable{N,T}) where {N,T} +function uv_mesh(primitive::AbstractGeometry{N,T}) where {N,T} m = triangle_mesh(primitive) return MetaMesh(m, (uv=decompose_uv(primitive),)) end -function uv_normal_mesh(primitive::Meshable{N}) where {N} +function uv_normal_mesh(primitive::AbstractGeometry{N}) where {N} m = triangle_mesh(primitive) - return MetaMesh(m, (uv=decompose_uv(primitive), normals=decompose_normals(primitive))) + return MetaMesh(m, (uv=decompose_uv(primitive), normals=decompose_normals(m))) end function normal_mesh(points::AbstractVector{<:Point}, @@ -66,9 +63,9 @@ function normal_mesh(points::AbstractVector{<:Point}, return MetaMesh(Mesh(_points, _faces), (normals=normals(_points, _faces),)) end -function normal_mesh(primitive::Meshable{N}) where {N} +function normal_mesh(primitive::AbstractGeometry{N}) where {N} m = triangle_mesh(primitive) - return MetaMesh(m, (normals=decompose_normals(primitive),)) + return MetaMesh(m, (normals=decompose_normals(m),)) end """ @@ -109,3 +106,48 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) return Mesh(ps, fs) end end + +function map_coordinates(f, mesh::Mesh) + result = copy(mesh) + map_coordinates!(f, result) + return result +end + +function map_coordinates(f, mesh::MetaMesh) + result = copy(Mesh(mesh)) + map_coordinates!(f, result) + return MetaMesh(result, meta(mesh)) +end + +function map_coordinates!(f, mesh::AbstractMesh) + points = coordinates(mesh) + map!(f, points, points) + return mesh +end + +function add_meta(mesh::MetaMesh; kw...) + return MetaMesh(Mesh(mesh), (; meta(mesh)..., kw...)) +end + +function add_meta(mesh::Mesh; kw...) + return MetaMesh(mesh, (; meta(mesh)..., kw...)) +end + +# I didn't find a simple way to remove a field from a namedtuple in a type stable way without +# a generated function.. +@generated function pop(nt::NamedTuple{Names, Values}, ::Val{name}) where {Names, Values, name} + if !(name in Names) + return :(throw(Base.KeyError($(QuoteNode(name))))) + else + names = filter(x-> x !== name, Names) + nt = map(names) do name + :($name = nt.$(name)) + end + return :((; $(nt...)), nt.$(name)) + end +end + +function pop_meta(mesh::MetaMesh, name::Symbol) + new_meta, value = pop(meta(mesh), Val(name)) + return MetaMesh(mesh, new_meta), value +end diff --git a/src/offsetintegers.jl b/src/offsetintegers.jl index 8eeacdd8..d743c0d0 100644 --- a/src/offsetintegers.jl +++ b/src/offsetintegers.jl @@ -19,8 +19,24 @@ raw(x::Integer) = x value(x::OffsetInteger{O,T}) where {O,T} = raw(x) - O value(x::Integer) = x -function Base.show(io::IO, oi::OffsetInteger) - return print(io, "|$(raw(oi)) (indexes as $(value(oi))|") +function to_subscript(io::IO, x::Int) + if x in 0:9 + print(io, Char(0x2080+x)) + elseif x < 0 + print(io, Char(0x208B)); to_subscript(io, abs(x)) + else # positive + more than one digit + for d in reverse(digits(x)) + to_subscript(io, d) + end + end +end + +function Base.show(io::IO, oi::OffsetInteger{O}) where {O} + o = O < 0 ? "ₒ₊" : "ₒ₋" + print(io, "<$(value(oi))$o") + to_subscript(io, abs(O)) + print(io, ">") + return end Base.eltype(::Type{OffsetInteger{O,T}}) where {O,T} = T @@ -40,7 +56,9 @@ OffsetInteger{O}(x::OffsetInteger) where {O} = OffsetInteger{O,eltype(x)}(x) Base.to_index(idx::OffsetInteger) = convert(Int, raw(OneIndex(idx))) Base.convert(::Type{IT}, x::OffsetInteger) where {IT<:Integer} = IT(value(x)) +Base.convert(::Type{IT}, x::OffsetInteger) where {IT<:OffsetInteger} = IT(value(x)) Base.convert(::Type{O}, x::Integer) where {O<:OffsetInteger} = O(x) + # basic operators for op in (:(-), :abs) @eval Base.$op(x::T) where {T<:OffsetInteger} = T($(op)(value(x))) @@ -56,8 +74,11 @@ end for op in (:(==), :(>=), :(<=), :(<), :(>), :sub_with_overflow) @eval begin - @inline function Base.$op(x::OffsetInteger{O}, y::OffsetInteger{O}) where {O} + function Base.$op(x::OffsetInteger{O}, y::OffsetInteger{O}) where {O} return $op(x.i, y.i) end + Base.$op(x::OffsetInteger, y::OffsetInteger) = $op(value(x), value(y)) + Base.$op(x::OffsetInteger, y::Integer) = $op(value(x), y) + Base.$op(x::Integer, y::OffsetInteger) = $op(x, value(y)) end end diff --git a/src/primitive = Circle(Point2f(0), 1f0).jl b/src/primitive = Circle(Point2f(0), 1f0).jl new file mode 100644 index 00000000..f7cfe517 --- /dev/null +++ b/src/primitive = Circle(Point2f(0), 1f0).jl @@ -0,0 +1,45 @@ +m = GeometryBasics.normal_mesh(Sphere(Point3f(0), 1f0)) +@test decompose_uv(m) isa Vector{Vec2f} + +o, extr, r = Point2f(1, 2), Point2f(3, 4), 5.0f0 +s = Cylinder(o, extr, r) +positions = Point{3,Float32}[(-0.7677671, 3.767767, 0.0), + (2.767767, 0.23223293, 0.0), + (0.23223293, 4.767767, 0.0), + (3.767767, 1.2322329, 0.0), (1.2322329, 5.767767, 0.0), + (4.767767, 2.232233, 0.0)] +@test decompose(Point3f, Tesselation(s, (2, 3))) ≈ positions + +FT = TriangleFace{Int} +f = FT[(1, 2, 4), (1, 4, 3), (3, 4, 6), (3, 6, 5)] +@test f == decompose(FT, Tesselation(s, (2, 3))) + +v1 = Point{3,Float64}(1, 2, 3) +v2 = Point{3,Float64}(4, 5, 6) +R = 5.0 +s = Cylinder(v1, v2, R) +positions = Point{3,Float64}[(4.535533905932738, -1.5355339059327373, 3.0), + (7.535533905932738, 1.4644660940672627, 6.0), + (3.0412414523193148, 4.041241452319315, + -1.0824829046386295), + (6.041241452319315, 7.041241452319315, + 1.9175170953613705), + (-2.535533905932737, 5.535533905932738, + 2.9999999999999996), + (0.46446609406726314, 8.535533905932738, 6.0), + (-1.0412414523193152, -0.04124145231931431, + 7.0824829046386295), + (1.9587585476806848, 2.9587585476806857, + 10.08248290463863), (1, 2, 3), (4, 5, 6)] + +@test decompose(Point3{Float64}, Tesselation(s, 8)) ≈ positions + +f = TriangleFace{Int}[(3, 2, 1), (4, 2, 3), (5, 4, 3), (6, 4, 5), (7, 6, 5), + (8, 6, 7), (1, 8, 7), (2, 8, 1), (3, 1, 9), (2, 4, 10), + (5, 3, 9), (4, 6, 10), (7, 5, 9), (6, 8, 10), (1, 7, 9), + (8, 2, 10)] +@test f == decompose(TriangleFace{Int}, Tesselation(s, 8)) + +m = triangle_mesh(Tesselation(s, 8)) + +@test GeometryBasics.faces(m) == f diff --git a/src/primitives/spheres.jl b/src/primitives/spheres.jl index 342ca9b1..0eb2fea0 100644 --- a/src/primitives/spheres.jl +++ b/src/primitives/spheres.jl @@ -44,10 +44,10 @@ end function coordinates(s::Circle, nvertices=64) rad = radius(s) inner(fi) = Point(rad * sin(fi + pi), rad * cos(fi + pi)) .+ origin(s) - return (inner(fi) for fi in LinRange(0, 2pi, nvertices)) + return [inner(fi) for fi in LinRange(0, 2pi, nvertices)] end -function texturecoordinates(s::Circle, nvertices=64) +function texturecoordinates(::Circle, nvertices=64) return coordinates(Circle(Point2f(0.5), 0.5f0), nvertices) end @@ -55,18 +55,18 @@ function coordinates(s::Sphere, nvertices=24) θ = LinRange(0, pi, nvertices) φ = LinRange(0, 2pi, nvertices) inner(θ, φ) = Point(cos(φ) * sin(θ), sin(φ) * sin(θ), cos(θ)) .* s.r .+ s.center - return ivec((inner(θ, φ) for θ in θ, φ in φ)) + return [inner(θ, φ) for φ in φ for θ in θ] end -function texturecoordinates(s::Sphere, nvertices=24) +function texturecoordinates(::Sphere, nvertices=24) ux = LinRange(0, 1, nvertices) return ivec(((φ, θ) for θ in reverse(ux), φ in ux)) end -function faces(sphere::Sphere, nvertices=24) +function faces(::Sphere, nvertices=24) return faces(Rect(0, 0, 1, 1), (nvertices, nvertices)) end -function normals(s::Sphere{T}, nvertices=24) where {T} +function normals(::Sphere{T}, nvertices=24) where {T} return coordinates(Sphere(Point{3,T}(0), 1), nvertices) end diff --git a/src/triangulation.jl b/src/triangulation.jl index 28e359ac..dfbfca9d 100644 --- a/src/triangulation.jl +++ b/src/triangulation.jl @@ -110,10 +110,9 @@ Triangulate a Polygon without hole. Returns a Vector{`facetype`} defining indexes into `contour`. """ -function decompose(::Type{FaceType}, - points::AbstractArray{P}) where {P<:Point,FaceType<:AbstractFace} +function decompose(::Type{F}, points::AbstractVector{<:Point}) where {F<:AbstractFace} #= allocate and initialize list of Vertices in polygon =# - result = FaceType[] + result = F[] # the algorithm doesn't like closed contours contour = if isapprox(last(points), first(points)) @@ -159,7 +158,7 @@ function decompose(::Type{FaceType}, b = V[v] c = V[w] #= output Triangle =# - push!(result, convert_simplex(FaceType, TriangleFace(a, b, c))...) + push!(result, convert_simplex(F, TriangleFace(a, b, c))...) #= remove v from remaining polygon =# s = v t = v + 1 diff --git a/test/fixed_arrays.jl b/test/fixed_arrays.jl index 497ee829..7a8698b4 100644 --- a/test/fixed_arrays.jl +++ b/test/fixed_arrays.jl @@ -8,7 +8,6 @@ end @testset "broadcast" begin @testset for T in (Vec, Point) x = [T(2, 3), T(7, 3)] - @test [T(4, 9), T(14, 9)] == x .* T(2, 3) @test [T(4, 6), T(9, 6)] == x .+ T(2, 3) @test [T(0, 0), T(5, 0)] == x .- T(2, 3) diff --git a/test/runtests.jl b/test/runtests.jl index 71740067..b2baa753 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,7 +1,7 @@ -using Test, Random, StructArrays, OffsetArrays +using Test, Random, OffsetArrays using GeometryBasics using LinearAlgebra -using GeometryBasics: attributes +using GeometryBasics: MetaMesh, add_meta, pop_meta @testset "GeometryBasics" begin @testset "algorithms" begin @@ -24,239 +24,35 @@ using GeometryBasics: attributes points3d = Point3f[(0,0,0), (0,0,1), (0,1,1)] @test area(OffsetArray(points3d, -2)) ≈ 0.5 - - pm2d = [PointMeta(0.0, 0.0, a=:d), PointMeta(0.0, 1.0, a=:e), PointMeta(1.0, 0.0, a=:f)] - @test area(pm2d) ≈ -0.5 - - pm3d = [PointMeta(0.0, 0.0, 0.0, a=:d), PointMeta(0.0, 1.0, 0.0, a=:e), PointMeta(1.0, 0.0, 0.0, a=:f)] - @test_broken area(pm3d) ≈ 0.5 # Currently broken as zero(PointMeta(0.0, 0.0, 0.0, a=:d)) fails end @testset "embedding metadata" begin @testset "Meshes" begin - @testset "per vertex attributes" begin points = rand(Point{3, Float64}, 8) tfaces = TetrahedronFace{Int}[(1, 2, 3, 4), (5, 6, 7, 8)] normals = rand(Vec{3, Float64}, 8) stress = LinRange(0, 1, 8) - mesh = Mesh(meta(points, normals = normals, stress = stress), tfaces) + mesh = MetaMesh(points, tfaces; normals = normals, stress = stress) - @test hasproperty(coordinates(mesh), :stress) - @test hasproperty(coordinates(mesh), :normals) - @test coordinates(mesh).stress === stress - @test coordinates(mesh).normals === normals - @test coordinates(mesh).normals === normals + @test hasproperty(mesh, :stress) + @test hasproperty(mesh, :normals) + @test mesh.stress === stress + @test mesh.normals === normals + @test mesh.normals === normals @test GeometryBasics.faces(mesh) === tfaces - @test propertynames(coordinates(mesh)) == (:position, :normals, :stress) - - end - - @testset "per face attributes" begin - - # Construct a cube out of Quads - points = Point{3, Float64}[ - (0.0, 0.0, 0.0), (2.0, 0.0, 0.0), - (2.0, 2.0, 0.0), (0.0, 2.0, 0.0), - (0.0, 0.0, 12.0), (2.0, 0.0, 12.0), - (2.0, 2.0, 12.0), (0.0, 2.0, 12.0) - ] - - facets = QuadFace{Cint}[ - 1:4, - 5:8, - [1,5,6,2], - [2,6,7,3], - [3, 7, 8, 4], - [4, 8, 5, 1] - ] - - markers = Cint[-1, -2, 0, 0, 0, 0] - # attach some additional information to our faces! - mesh = Mesh(points, meta(facets, markers = markers)) - @test hasproperty(GeometryBasics.faces(mesh), :markers) - # test with === to assert we're not doing any copies - @test GeometryBasics.faces(mesh).markers === markers - @test coordinates(mesh) === points - @test metafree(GeometryBasics.faces(mesh)) === facets - + @test propertynames(mesh) == (:normals, :stress) end - - end - - @testset "polygon with metadata" begin - polys = [Polygon(rand(Point{2, Float32}, 20)) for i in 1:10] - pnames = [randstring(4) for i in 1:10] - numbers = LinRange(0.0, 1.0, 10) - bin = rand(Bool, 10) - # create a polygon - poly = PolygonMeta(polys[1], name = pnames[1], value = numbers[1], category = bin[1]) - # create a MultiPolygon with the right type & meta information! - multipoly = MultiPolygonMeta(polys, name = pnames, value = numbers, category = bin) - @test multipoly isa AbstractVector - @test poly isa GeometryBasics.AbstractPolygon - - @test GeometryBasics.getcolumn(poly, :name) == pnames[1] - @test GeometryBasics.MetaFree(PolygonMeta) == Polygon - - @test GeometryBasics.getcolumn(multipoly, :name) == pnames - @test GeometryBasics.MetaFree(MultiPolygonMeta) == MultiPolygon - - meta_p = meta(polys[1], boundingbox=Rect(0, 0, 2, 2)) - @test meta_p.boundingbox === Rect(0, 0, 2, 2) - @test metafree(meta_p) === polys[1] - attributes(meta_p) == Dict{Symbol, Any}(:boundingbox => meta_p.boundingbox, - :polygon => polys[1]) - end - @testset "point with metadata" begin - p = Point(1.1, 2.2) - pm = PointMeta(1.1, 2.2; a=1, b=2) - p1 = Point(2.2, 3.6) - p2 = [p, p1] - @test coordinates(p2) == p2 - @test meta(pm) === (a=1, b=2) - @test metafree(pm) === p - @test propertynames(pm) == (:position, :a, :b) - @test GeometryBasics.MetaFree(typeof(pm)) == Point{2,Float64} - @test_broken zero(pm) == [0, 0] - end - - @testset "MultiPoint with metadata" begin - p = collect(Point{2, Float64}(x, x+1) for x in 1:5) - @test p isa AbstractVector - mpm = MultiPointMeta(p, a=1, b=2) - @test coordinates(mpm) == mpm - @test meta(mpm) === (a=1, b=2) - @test metafree(mpm) == p - @test propertynames(mpm) == (:points, :a, :b) - end - - @testset "LineString with metadata" begin - linestring = LineStringMeta(Point{2, Int}[(10, 10), (20, 20), (10, 40)], a = 1, b = 2) - @test linestring isa AbstractVector - @test meta(linestring) === (a = 1, b = 2) - @test metafree(linestring) == linestring - @test propertynames(linestring) == (:lines, :a, :b) - end - - @testset "MultiLineString with metadata" begin - linestring1 = LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]) - linestring2 = LineString(Point{2, Int}[(40, 40), (30, 30), (40, 20), (30, 10)]) - multilinestring = MultiLineString([linestring1, linestring2]) - multilinestringmeta = MultiLineStringMeta([linestring1, linestring2]; boundingbox = Rect(1.0, 1.0, 2.0, 2.0)) - @test multilinestringmeta isa AbstractVector - @test meta(multilinestringmeta) === (boundingbox = Rect(1.0, 1.0, 2.0, 2.0),) - @test metafree(multilinestringmeta) == multilinestring - @test propertynames(multilinestringmeta) == (:linestrings, :boundingbox) end @testset "Mesh with metadata" begin m = triangle_mesh(Sphere(Point3f(0), 1)) - m_meta = MeshMeta(m; boundingbox=Rect(1.0, 1.0, 2.0, 2.0)) - @test meta(m_meta) === (boundingbox = Rect(1.0, 1.0, 2.0, 2.0),) - @test metafree(m_meta) === m - @test propertynames(m_meta) == (:mesh, :boundingbox) + m_meta = MetaMesh(m; boundingbox=Rect(1.0, 1.0, 2.0, 2.0)) + @test m_meta.boundingbox === Rect(1.0, 1.0, 2.0, 2.0) + @test propertynames(m_meta) == (:boundingbox,) end end -@testset "embedding MetaT" begin - @testset "MetaT{Polygon}" begin - polys = [Polygon(rand(Point{2, Float32}, 20)) for i in 1:10] - multipol = MultiPolygon(polys) - pnames = [randstring(4) for i in 1:10] - numbers = LinRange(0.0, 1.0, 10) - bin = rand(Bool, 10) - # create a polygon - poly = MetaT(polys[1], name = pnames[1], value = numbers[1], category = bin[1]) - # create a MultiPolygon with the right type & meta information! - multipoly = MetaT(multipol, name = pnames, value = numbers, category = bin) - @test multipoly isa MetaT - @test poly isa MetaT - - @test GeometryBasics.getcolumn(poly, :name) == pnames[1] - @test GeometryBasics.getcolumn(multipoly, :name) == pnames - - meta_p = MetaT(polys[1], boundingbox=Rect(0, 0, 2, 2)) - @test meta_p.boundingbox === Rect(0, 0, 2, 2) - @test GeometryBasics.metafree(meta_p) == polys[1] - @test GeometryBasics.metafree(poly) == polys[1] - @test GeometryBasics.metafree(multipoly) == multipol - @test GeometryBasics.meta(meta_p) == (boundingbox = GeometryBasics.HyperRectangle{2,Int64}([0, 0], [2, 2]),) - @test GeometryBasics.meta(poly) == (name = pnames[1], value = 0.0, category = bin[1]) - @test GeometryBasics.meta(multipoly) == (name = pnames, value = numbers, category = bin) - end - - @testset "MetaT{Point}" begin - p = Point(1.1, 2.2) - pm = MetaT(Point(1.1, 2.2); a=1, b=2) - p1 = Point(2.2, 3.6) - p2 = [p, p1] - @test coordinates(p2) == p2 - @test pm.meta === (a=1, b=2) - @test pm.main === p - @test propertynames(pm) == (:main, :a, :b) - @test GeometryBasics.metafree(pm) == p - @test GeometryBasics.meta(pm) == (a = 1, b = 2) - end - - @testset "MetaT{MultiPoint}" begin - p = collect(Point{2, Float64}(x, x+1) for x in 1:5) - @test p isa AbstractVector - mpm = MetaT(MultiPoint(p); a=1, b=2) - @test coordinates(mpm.main) == Point{2, Float64}[(x, x+1) for x in 1:5] - @test mpm.meta === (a=1, b=2) - @test mpm.main == p - @test propertynames(mpm) == (:main, :a, :b) - @test GeometryBasics.metafree(mpm) == p - @test GeometryBasics.meta(mpm) == (a = 1, b = 2) - end - - @testset "MetaT{LineString}" begin - linestring = MetaT(LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]), a = 1, b = 2) - @test linestring isa MetaT - @test linestring.meta === (a = 1, b = 2) - @test propertynames(linestring) == (:main, :a, :b) - @test GeometryBasics.metafree(linestring) == LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]) - @test GeometryBasics.meta(linestring) == (a = 1, b = 2) - end - - @testset "MetaT{MultiLineString}" begin - linestring1 = LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]) - linestring2 = LineString(Point{2, Int}[(40, 40), (30, 30), (40, 20), (30, 10)]) - multilinestring = MultiLineString([linestring1, linestring2]) - multilinestringmeta = MetaT(MultiLineString([linestring1, linestring2]); boundingbox = Rect(1.0, 1.0, 2.0, 2.0)) - @test multilinestringmeta isa MetaT - @test multilinestringmeta.meta === (boundingbox = Rect(1.0, 1.0, 2.0, 2.0),) - @test multilinestringmeta.main == multilinestring - @test propertynames(multilinestringmeta) == (:main, :boundingbox) - @test GeometryBasics.metafree(multilinestringmeta) == multilinestring - @test GeometryBasics.meta(multilinestringmeta) == (boundingbox = GeometryBasics.HyperRectangle{2,Float64}([1.0, 1.0], [2.0, 2.0]),) - end - - #= - So mesh works differently for MetaT - since `MetaT{Point}` not subtyped to `Point` - =# - - @testset "MetaT{Mesh}" begin - @testset "per vertex attributes" begin - points = rand(Point{3, Float64}, 8) - tfaces = TetrahedronFace{Int}[(1, 2, 3, 4), (5, 6, 7, 8)] - normals = rand(Vec{3, Float64}, 8) - stress = LinRange(0, 1, 8) - mesh_nometa = Mesh(points, tfaces) - mesh = MetaT(mesh_nometa, normals = normals, stress = stress) - - @test hasproperty(mesh, :stress) - @test hasproperty(mesh, :normals) - @test mesh.stress == stress - @test mesh.normals == normals - @test GeometryBasics.faces(mesh.main) == tfaces - @test propertynames(mesh) == (:main, :normals, :stress) - end - end -end - @testset "view" begin @testset "TupleView" begin x = [1, 2, 3, 4, 5, 6] @@ -315,56 +111,10 @@ end end @testset "constructors" begin - @testset "LineFace" begin - - points = connect([1, 2, 3, 4, 5, 6], Point{2}) - linestring = LineString(points) - @test linestring == [Line(points[1], points[2]), Line(points[2], points[3])] - - points = rand(Point{2, Float64}, 4) - linestring = LineString(points, 2) - @test linestring == [Line(points[1], points[2]), Line(points[3], points[4])] - - linestring = LineString([points[1] => points[2], points[2] => points[3]]) - @test linestring == [Line(points[1], points[2]), Line(points[2], points[3])] - - faces = [1, 2, 3] - linestring = LineString(points, faces) - @test linestring == LineString([points[1] => points[2], points[2] => points[3]]) - a, b, c, d = Point(1, 2), Point(3, 4), Point(5, 6), Point(7, 8) - points = [a, b, c, d]; faces = [1, 2, 3, 4] - linestring = LineString(points, faces, 2) - @test linestring == LineString([a => b, c => d]) - - faces = [LineFace(1, 2) - , LineFace(3, 4)] - linestring = LineString(points, faces) - @test linestring == LineString([a => b, c => d]) - end - @testset "Polygon" begin - points = connect([1, 2, 3, 4, 5, 6], Point{2}) polygon = Polygon(points) - @test polygon == Polygon(LineString(points)) - - points = rand(Point{2, Float64}, 4) - linestring = LineString(points, 2) - @test Polygon(points, 2) == Polygon(linestring) - - faces = [1, 2, 3] - polygon = Polygon(points, faces) - @test polygon == Polygon(LineString(points, faces)) - - a, b, c, d = Point(1, 2), Point(3, 4), Point(5, 6), Point(7, 8) - points = [a, b, c, d]; faces = [1, 2, 3, 4] - polygon = Polygon(points, faces, 2) - @test polygon == Polygon(LineString(points, faces, 2)) - - faces = [LineFace(1, 2), LineFace(3, 4)] - polygon = Polygon(points, faces) - @test polygon == Polygon(LineString(points, faces)) - @test ndims(polygon) === 2 + @test polygon == Polygon(points) end @testset "Mesh" begin @@ -386,11 +136,11 @@ end points = rand(Point3f, 8) tfaces = [GLTriangleFace(1, 2, 3), GLTriangleFace(5, 6, 7)] - normals = rand(Vec3f, 8) + ns = rand(Vec3f, 8) uv = rand(Vec2f, 8) mesh = Mesh(points, tfaces) meshuv = MetaMesh(points, tfaces; uv=uv) - meshuvnormal = MetaMesh(points, tfaces; normals=normals, uv=uv) + meshuvnormal = MetaMesh(points, tfaces; normals=ns, uv=uv) t = Tesselation(Rect2f(0, 0, 2, 2), (30, 30)) m = GeometryBasics.mesh(t; pointtype=Point3f, facetype=QuadFace{Int}) @@ -398,37 +148,6 @@ end @test GeometryBasics.faces(m2) isa Vector{QuadFace{GLIndex}} @test GeometryBasics.coordinates(m2) isa Vector{Point3f} end - - @testset "Multi geometries" begin - # coordinates from https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry#Geometric_objects - points = Point{2, Int}[(10, 40), (40, 30), (20, 20), (30, 10)] - multipoint = MultiPoint(points) - @test size(multipoint) === size(points) - @test multipoint[3] === points[3] - - linestring1 = LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]) - linestring2 = LineString(Point{2, Int}[(40, 40), (30, 30), (40, 20), (30, 10)]) - multilinestring = MultiLineString([linestring1, linestring2]) - @test size(multilinestring) === (2,) - @test multilinestring[1] === linestring1 - @test multilinestring[2] === linestring2 - - polygon11 = Polygon(Point{2, Int}[(30, 20), (45, 40), (10, 40), (30, 20)]) - polygon12 = Polygon(Point{2, Int}[(15, 5), (40, 10), (10, 20), (5, 10), (15, 5)]) - multipolygon1 = MultiPolygon([polygon11, polygon12]) - @test size(multipolygon1) === (2,) - @test multipolygon1[1] === polygon11 - @test multipolygon1[2] === polygon12 - - polygon21 = Polygon(Point{2, Int}[(40, 40), (20, 45), (45, 30), (40, 40)]) - polygon22 = Polygon(LineString(Point{2, Int}[(20, 35), (10, 30), (10, 10), (30, 5), (45, 20), (20, 35)]), - [LineString(Point{2, Int}[(30, 20), (20, 15), (20, 25), (30, 20)])]) - multipolygon2 = MultiPolygon([polygon21, polygon22]) - @test size(multipolygon2) === (2,) - @test multipolygon2[1] === polygon21 - @test multipolygon2[2] === polygon22 - end - end @testset "decompose/triangulation" begin @@ -463,8 +182,8 @@ end m = GeometryBasics.mesh(Sphere(Point3f(0), 1)) @test GeometryBasics.normals(m) == nothing - m_normals = pointmeta(m, Normal()) - @test GeometryBasics.normals(m_normals) isa Vector{Vec3f} + m_normals = decompose_normals(m) + @test m_normals isa Vector{Vec3f} @test texturecoordinates(m) == nothing r2 = Rect2(0.0, 0.0, 1.0, 1.0) @@ -477,54 +196,29 @@ end points = decompose(Point2f, Circle(Point2f(0), 1)) m = GeometryBasics.mesh(points) @test coordinates(m) === points - - linestring = LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]) - pts = Point{2, Int}[(10, 10), (20, 20), (10, 40)] - linestring = LineString(pts) - pts_decomp = decompose(Point{2, Int}, linestring) - @test pts === pts_decomp - - pts_ext = Point{2, Int}[(5, 1), (3, 3), (4, 8), (1, 2), (5, 1)] - ls_ext = LineString(pts_ext) - pts_int1 = Point{2, Int}[(2, 2), (3, 8),(5, 6), (3, 4), (2, 2)] - ls_int1 = LineString(pts_int1) - pts_int2 = Point{2, Int}[(3, 2), (4, 5),(6, 1), (1, 4), (3, 2)] - ls_int2 = LineString(pts_int2) - poly_ext = Polygon(ls_ext) - poly_ext_int = Polygon(ls_ext, [ls_int1, ls_int2]) - @test decompose(Point{2, Int}, poly_ext) == pts_ext - @test decompose(Point{2, Int}, poly_ext_int) == [pts_ext..., pts_int1..., pts_int2...] -end - -@testset "mesh" begin - primitive = Triangle(Point2f(0), Point2f(1), Point2f(1,0)) - m = GeometryBasics.mesh(primitive) - @test length(faces(m)) == 1 end @testset "convert mesh + meta" begin m = uv_normal_mesh(Circle(Point2f(0), 1f0)) - # for 2D primitives we dont actually calculate normals - @test !hasproperty(m, :normals) + # For 2d primitives normal is just the upvector + m.normals == [Vec3f(0, 0, 1) for p in coordinates(m)] end @testset "convert mesh + meta" begin m = uv_normal_mesh(Rect3f(Vec3f(-1), Vec3f(1, 2, 3))) - m_normal = normal_mesh(m) + m_normal = add_meta(m; normals = decompose_normals(m)) # make sure we don't loose the uv @test hasproperty(m_normal, :uv) - @test m == m_normal # Make sure we don't create any copies - @test m.position === m_normal.position - @test m.normals === m_normal.normals + @test coordinates(m) === coordinates(m_normal) + @test m.normals == m_normal.normals @test m.uv === m_normal.uv - m = GeometryBasics.mesh(Rect3f(Vec3f(-1), Vec3f(1, 2, 3)); - uv=Vec2{Float64}, normaltype=Vec3{Float64}, pointtype=Point3{Float64}) - m_normal = normal_mesh(m) + m = uv_mesh(Rect3f(Vec3f(-1), Vec3f(1, 2, 3))) + m_normal = add_meta(m, normals = decompose_normals(m)) @test hasproperty(m_normal, :uv) - @test m.position !== m_normal.position - @test m.normals !== m_normal.normals + @test coordinates(m) === coordinates(m_normal) + @test decompose_normals(m) == normals(m_normal) # uv stays untouched, since we don't specify the element type in normalmesh @test m.uv === m_normal.uv end @@ -532,35 +226,22 @@ end @testset "modifying meta" begin xx = rand(10) points = rand(Point3f, 10) - m = GeometryBasics.Mesh(meta(points, xx=xx), GLTriangleFace[(1,2,3), (3,4,5)]) + m = MetaMesh(points, GLTriangleFace[(1,2,3), (3,4,5)]; xx=xx) color = rand(10) - m = pointmeta(m; color=color) + m = add_meta(m; color=color) @test hasproperty(m, :xx) @test hasproperty(m, :color) - @test_throws ErrorException GeometryBasics.MetaType(Simplex) - @test_throws ErrorException GeometryBasics.MetaFree(Simplex) - @test m.xx === xx @test m.color === color - m, colpopt = GeometryBasics.pop_pointmeta(m, :color) - m, xxpopt = GeometryBasics.pop_pointmeta(m, :xx) + m, colpopt = GeometryBasics.pop_meta(m, :color) + m, xxpopt = GeometryBasics.pop_meta(m, :xx) - @test propertynames(m) == (:position,) + @test propertynames(m) == () @test colpopt === color @test xxpopt === xx - - @testset "creating meta" begin - x = Point3f[(1,3,4)] - # no meta gets added, so should stay the same - @test meta(x) === x - @test meta(x, value=[1]).position === x - end - pos = Point2f[(10, 2)] - m = Mesh(meta(pos, uv=[Vec2f(1, 1)]), [GLTriangleFace(1, 1, 1)]) - @test m.position === pos end @testset "mesh conversion" begin @@ -576,7 +257,7 @@ end @test coordinates(tmesh) === decompose(Point3f, tmesh) nmesh = normal_mesh(m) - @test metafree(coordinates(nmesh)) === decompose(Point3f, nmesh) + @test coordinates(nmesh) === decompose(Point3f, nmesh) @test GeometryBasics.normals(nmesh) === decompose_normals(nmesh) m = GeometryBasics.mesh(s, pointtype=Point3f) @@ -613,10 +294,7 @@ end @test typeof(x) == OffsetInteger{0,Int64} x1 = OffsetInteger{0}(2) - @test GeometryBasics.pure_max(x, x1) == x1 - @test promote_rule(typeof(x), typeof(x1)) == OffsetInteger{0,Int64} x2 = 1 - @test promote_rule(typeof(x2), typeof(x1)) == Int64 @test Base.to_index(x1) == 2 @test -(x1) == OffsetInteger{0,Int64}(-2) @test abs(x1) == OffsetInteger{0,Int64}(2) @@ -632,56 +310,6 @@ end @test <(x, x1) end -@testset "MetaT and heterogeneous data" begin - ls = [LineString([Point(i, (i+1)^2/6), Point(i*0.86,i+5), Point(i/3, i/7)]) for i in 1:10] - mls = MultiLineString([LineString([Point(i+1, (i)^2/6), Point(i*0.75,i+8), Point(i/2.5, i/6.79)]) for i in 5:10]) - poly = Polygon(Point{2, Int}[(40, 40), (20, 45), (45, 30), (40, 40)]) - geom = [ls..., mls, poly] - prop = Any[(country_states = "India$(i)", rainfall = (i*9)/2) for i in 1:11] - push!(prop, (country_states = 12, rainfall = 1000)) # a pinch of heterogeneity - - feat = [MetaT(i, j) for (i,j) = zip(geom, prop)] - sa = meta_table(feat) - - @test nameof(eltype(feat)) == :MetaT - @test eltype(sa) === MetaT{Any,(:country_states, :rainfall),Tuple{Any,Float64}} - @test propertynames(sa) === (:main, :country_states, :rainfall) - @test getproperty(sa, :country_states) isa Array{Any} - @test getproperty(sa, :main) == geom - - maintype, metanames, metatype = GeometryBasics.getnamestypes(typeof(feat[1])) - @test (metanames, metatype) == ((:country_states, :rainfall), Tuple{String,Float64}) - - - @test StructArrays.createinstance(typeof(feat[1]), LineString([Point(1, (2)^2/6), Point(1*0.86,6), Point(1/3, 1/7)]), "Mumbai", 100) isa typeof(feat[1]) - - @test Base.getindex(feat[1], 1) isa Line - @test Base.size(feat[1]) == (2,) -end - -@testset "StructArrays integration" begin - pt = meta(Point(0.0, 0.0), color="red", alpha=0.1) - @test StructArrays.component(pt, :position) == Point(0.0, 0.0) - @test StructArrays.component(pt, :color) == "red" - @test StructArrays.component(pt, :alpha) == 0.1 - @test StructArrays.staticschema(typeof(pt)) == - NamedTuple{(:position, :color, :alpha), Tuple{Point2{Float64}, String, Float64}} - @test StructArrays.createinstance(typeof(pt), Point(0.0, 0.0), "red", 0.1) == pt - - s = StructArray([pt, pt]) - @test StructArrays.components(s) == ( - position = [Point(0.0, 0.0), Point(0.0, 0.0)], - color = ["red", "red"], - alpha = [0.1, 0.1] - ) - s[2] = meta(Point(0.1, 0.1), color="blue", alpha=0.3) - @test StructArrays.components(s) == ( - position = [Point(0.0, 0.0), Point(0.1, 0.1)], - color = ["red", "blue"], - alpha = [0.1, 0.3] - ) -end - @testset "Tests from GeometryTypes" begin include("geometrytypes.jl") end From 834fb125726fd40107b59e393a9efabbf7188496 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Thu, 17 Mar 2022 21:47:24 +0100 Subject: [PATCH 10/58] more tests --- Project.toml | 9 +---- src/basic_types.jl | 5 ++- src/meshes.jl | 2 +- src/primitive = Circle(Point2f(0), 1f0).jl | 45 ---------------------- test/meshes.jl | 33 ++++++++++++++++ test/runtests.jl | 5 +++ 6 files changed, 45 insertions(+), 54 deletions(-) delete mode 100644 src/primitive = Circle(Point2f(0), 1f0).jl create mode 100644 test/meshes.jl diff --git a/Project.toml b/Project.toml index 2a68b39e..a39c12d1 100644 --- a/Project.toml +++ b/Project.toml @@ -8,21 +8,16 @@ EarCut_jll = "5ae413db-bbd1-5e63-b57d-d24a61df00f5" IterTools = "c8e1da08-722c-5040-9ed9-7db0dc04731e" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" -StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" -Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" [compat] IterTools = "1.3.0" -StaticArrays = "0.12, 1.0" -StructArrays = "0.6" -Tables = "0.2, 1" julia = "1.3" [extras] OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [targets] -test = ["Test", "Random", "OffsetArrays"] +test = ["Test", "Random", "StaticArrays", "OffsetArrays"] diff --git a/src/basic_types.jl b/src/basic_types.jl index 7ac39edf..2906646d 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -95,11 +95,13 @@ const Triangle{Dim,T} = Ngon{Dim,T,3} const Triangle3d{T} = Triangle{3,T} const GLTriangleElement = Triangle{3,Float32} +faces(x::Ngon{Dim, T, N}) where {Dim, T, N} = [NgonFace{N, Int}(ntuple(identity, N))] + Base.show(io::IO, x::Triangle) = print(io, "Triangle(", join(x, ", "), ")") const Quadrilateral{Dim,T} = Ngon{Dim,T,4} -Base.show(io::IO, x::Quadrilateral) = print(io, "Quad(", join(x, ", "), ")") +Base.show(io::IO, x::Quadrilateral) = print(io, "Quadrilateral(", join(x, ", "), ")") function coordinates(lines::AbstractArray{Line{Dim,T}}) where {Dim,T} result = Point{Dim, T}[] @@ -236,6 +238,7 @@ coordinates(mesh::Mesh) = mesh.vertices faces(mesh::Mesh) = mesh.connectivity Base.getindex(mesh::Mesh, i::Integer) = mesh.vertices[mesh.connectivity[i]] Base.length(mesh::Mesh) = length(mesh.connectivity) +Base.:(==)(a::Mesh, b::Mesh) = coordinates(a) == coordinates(b) && faces(a) == faces(b) function Base.iterate(mesh::Mesh, i=1) return i - 1 < length(mesh) ? (mesh[i], i + 1) : nothing diff --git a/src/meshes.jl b/src/meshes.jl index 8d03347e..a7cdd39d 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -92,7 +92,7 @@ end function Base.merge(meshes::AbstractVector{<:Mesh}) return if isempty(meshes) - error("No meshes to merge") + return Mesh(Point3f[], GLTriangleFace[]) elseif length(meshes) == 1 return meshes[1] else diff --git a/src/primitive = Circle(Point2f(0), 1f0).jl b/src/primitive = Circle(Point2f(0), 1f0).jl deleted file mode 100644 index f7cfe517..00000000 --- a/src/primitive = Circle(Point2f(0), 1f0).jl +++ /dev/null @@ -1,45 +0,0 @@ -m = GeometryBasics.normal_mesh(Sphere(Point3f(0), 1f0)) -@test decompose_uv(m) isa Vector{Vec2f} - -o, extr, r = Point2f(1, 2), Point2f(3, 4), 5.0f0 -s = Cylinder(o, extr, r) -positions = Point{3,Float32}[(-0.7677671, 3.767767, 0.0), - (2.767767, 0.23223293, 0.0), - (0.23223293, 4.767767, 0.0), - (3.767767, 1.2322329, 0.0), (1.2322329, 5.767767, 0.0), - (4.767767, 2.232233, 0.0)] -@test decompose(Point3f, Tesselation(s, (2, 3))) ≈ positions - -FT = TriangleFace{Int} -f = FT[(1, 2, 4), (1, 4, 3), (3, 4, 6), (3, 6, 5)] -@test f == decompose(FT, Tesselation(s, (2, 3))) - -v1 = Point{3,Float64}(1, 2, 3) -v2 = Point{3,Float64}(4, 5, 6) -R = 5.0 -s = Cylinder(v1, v2, R) -positions = Point{3,Float64}[(4.535533905932738, -1.5355339059327373, 3.0), - (7.535533905932738, 1.4644660940672627, 6.0), - (3.0412414523193148, 4.041241452319315, - -1.0824829046386295), - (6.041241452319315, 7.041241452319315, - 1.9175170953613705), - (-2.535533905932737, 5.535533905932738, - 2.9999999999999996), - (0.46446609406726314, 8.535533905932738, 6.0), - (-1.0412414523193152, -0.04124145231931431, - 7.0824829046386295), - (1.9587585476806848, 2.9587585476806857, - 10.08248290463863), (1, 2, 3), (4, 5, 6)] - -@test decompose(Point3{Float64}, Tesselation(s, 8)) ≈ positions - -f = TriangleFace{Int}[(3, 2, 1), (4, 2, 3), (5, 4, 3), (6, 4, 5), (7, 6, 5), - (8, 6, 7), (1, 8, 7), (2, 8, 1), (3, 1, 9), (2, 4, 10), - (5, 3, 9), (4, 6, 10), (7, 5, 9), (6, 8, 10), (1, 7, 9), - (8, 2, 10)] -@test f == decompose(TriangleFace{Int}, Tesselation(s, 8)) - -m = triangle_mesh(Tesselation(s, 8)) - -@test GeometryBasics.faces(m) == f diff --git a/test/meshes.jl b/test/meshes.jl new file mode 100644 index 00000000..56cca5c9 --- /dev/null +++ b/test/meshes.jl @@ -0,0 +1,33 @@ +@testset "Meshing a single triangle sometimes returns an empty mesh" begin + ϕ = (sqrt(5)+1)/2 + p,q,r = Point(ϕ,0,+1),Point(1,ϕ,0),Point(ϕ,0,-1) + m = triangle_mesh(Triangle(p,q,r)) + @test m isa Mesh + @test faces(m) == [TriangleFace(1, 2, 3)] +end + +@testset "Heterogenous faces" begin + # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/142 + f = [TriangleFace(1, 2, 3), QuadFace(1, 2, 3, 4)] + p = Point2f[(0, 1), (1, 2), (3, 4), (4, 5)] + m = Mesh(p, f) + @test collect(m) == [Triangle(p[1], p[2], p[3]), GeometryBasics.Quadrilateral(p[1], p[2], p[3], p[4])] +end + +@testset "Heterogenous faces" begin + # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/142 + f = [TriangleFace(1, 2, 3), QuadFace(1, 2, 3, 4)] + p = Point2f[(0, 1), (1, 2), (3, 4), (4, 5)] + m = Mesh(p, f) + @test collect(m) == [Triangle(p[1], p[2], p[3]), GeometryBasics.Quadrilateral(p[1], p[2], p[3], p[4])] +end + +@testset "Ambiguous NgonFace constructors" begin + # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/151 + t = TriangleFace(SA[0.4, 0.2, 0.55]) +end + +@testset "Merge empty vector of meshes" begin + # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/136 + merge(Mesh[]) == Mesh(Point3f[], GLTriangleFace[]) +end diff --git a/test/runtests.jl b/test/runtests.jl index b2baa753..146d8e94 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,7 @@ using Test, Random, OffsetArrays using GeometryBasics using LinearAlgebra +using StaticArrays using GeometryBasics: MetaMesh, add_meta, pop_meta @testset "GeometryBasics" begin @@ -318,4 +319,8 @@ end include("fixed_arrays.jl") end +@testset "Some mesh issues" begin + include("meshes.jl") +end + end From c52937e3592bf1014fcfd565f819672cc7baa06f Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Thu, 17 Mar 2022 21:50:47 +0100 Subject: [PATCH 11/58] remove 1.3 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b35cfb5f..59fb4d51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: version: - - '1.3' + - '1.6' - '1.7' os: - ubuntu-latest From 857ae2d60325312cfff17e67b79c1b7317effc29 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Thu, 17 Mar 2022 21:51:27 +0100 Subject: [PATCH 12/58] drop 1.3 --- .github/workflows/ci.yml | 2 +- Project.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59fb4d51..1beefbe1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: version: - - '1.6' + - '1.5' - '1.7' os: - ubuntu-latest diff --git a/Project.toml b/Project.toml index a39c12d1..ac0e521a 100644 --- a/Project.toml +++ b/Project.toml @@ -11,7 +11,7 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" [compat] IterTools = "1.3.0" -julia = "1.3" +julia = "1.5" [extras] OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" From 848e281ba5dbedd35b290ca34d3ce6b5ff7d454a Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Fri, 18 Mar 2022 13:55:38 +0100 Subject: [PATCH 13/58] add imports and get overload --- src/GeometryBasics.jl | 5 +++-- src/meshes.jl | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index 2c4af96c..a8062598 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -30,14 +30,15 @@ export LineFace, Polytope, Line, NgonFace, convert_simplex export AbstractPolygon, Polygon export Simplex, connect, Triangle, NSimplex, Tetrahedron export QuadFace, coordinates, TetrahedronFace -export TupleView, SimplexFace, Mesh +export TupleView, SimplexFace export Triangle export AbstractFace, TriangleFace, QuadFace, GLTriangleFace export OffsetInteger, ZeroIndex, OneIndex, GLIndex export decompose, coordinates, faces, normals, decompose_uv, decompose_normals, texturecoordinates export Tesselation, Normal, UV, UVW -export AbstractMesh, Mesh +export AbstractMesh, Mesh, MetaMesh +export add_meta, pop_meta # all the different predefined mesh types # Note: meshes can contain arbitrary meta information, diff --git a/src/meshes.jl b/src/meshes.jl index a7cdd39d..ae77d901 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -151,3 +151,9 @@ function pop_meta(mesh::MetaMesh, name::Symbol) new_meta, value = pop(meta(mesh), Val(name)) return MetaMesh(mesh, new_meta), value end + + +function Base.get(f, mesh::MetaMesh, key::Symbol) + hasproperty(mesh, key) && return getproperty(mesh, key) + return f() +end From e58d235ad4a9a493ce89a19ce439c9b87cd1ae73 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Thu, 16 Jun 2022 12:09:45 +0200 Subject: [PATCH 14/58] add getindex for faces for vec too --- src/basic_types.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 2906646d..193274c6 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -20,7 +20,11 @@ abstract type AbstractNgonFace{N,T} <: AbstractFace{N,T} end abstract type AbstractSimplex{Dim,N,T} <: StaticVector{Dim,T} end @propagate_inbounds function Base.getindex(points::AbstractVector{P}, face::F) where {P<: Point, F <: AbstractFace} - Polytope(P, F)(map(i-> points[i], face.data)) + return Polytope(P, F)(map(i-> points[i], face.data)) +end + +@propagate_inbounds function Base.getindex(points::AbstractVector{P}, face::F) where {P<: Vec, F <: AbstractFace} + return map(i-> points[i], face.data) end @fixed_vector SimplexFace AbstractSimplexFace From 9e9bf34bfcdabc0c2a1565aa32ee85b244fcae9c Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Mon, 20 Jun 2022 20:46:40 +0200 Subject: [PATCH 15/58] fixes --- src/basic_types.jl | 4 ++-- src/meshes.jl | 14 ++++++++++++++ src/offsetintegers.jl | 12 +++++++++++- src/viewtypes.jl | 7 +------ 4 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 193274c6..d98d34cb 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -23,8 +23,8 @@ abstract type AbstractSimplex{Dim,N,T} <: StaticVector{Dim,T} end return Polytope(P, F)(map(i-> points[i], face.data)) end -@propagate_inbounds function Base.getindex(points::AbstractVector{P}, face::F) where {P<: Vec, F <: AbstractFace} - return map(i-> points[i], face.data) +@propagate_inbounds function Base.getindex(elements::AbstractVector, face::F) where {F <: AbstractFace} + return map(i-> elements[i], face.data) end @fixed_vector SimplexFace AbstractSimplexFace diff --git a/src/meshes.jl b/src/meshes.jl index ae77d901..ba725134 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -107,6 +107,20 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) end end +function Base.merge(meshes::AbstractVector{T}) where T <: MetaMesh + isempty(meshes) && return T(Point3f[], GLTriangleFace[]) + big_mesh = merge(map(Mesh, meshes)) + big_meta = deepcopy(meta(meshes[1])) + for mesh in Iterators.drop(meshes, 1) + mm = meta(mesh) + for (k, v) in pairs(mm) + append!(big_meta[k], v) + end + end + return MetaMesh(big_mesh, big_meta) +end + + function map_coordinates(f, mesh::Mesh) result = copy(mesh) map_coordinates!(f, result) diff --git a/src/offsetintegers.jl b/src/offsetintegers.jl index d743c0d0..c921f1dd 100644 --- a/src/offsetintegers.jl +++ b/src/offsetintegers.jl @@ -5,7 +5,7 @@ OffsetInteger type mainly for indexing. * `O` - The offset relative to Julia arrays. This helps reduce copying when communicating with 0-indexed systems such as OpenGL. """ -struct OffsetInteger{O,T<:Integer} +struct OffsetInteger{O,T<:Integer} <: Integer i::T OffsetInteger{O,T}(x::Integer) where {O,T<:Integer} = new{O,T}(T(x + O)) end @@ -82,3 +82,13 @@ for op in (:(==), :(>=), :(<=), :(<), :(>), :sub_with_overflow) Base.$op(x::Integer, y::OffsetInteger) = $op(x, value(y)) end end + +Base.promote_rule(::Type{IT}, ::Type{<:OffsetInteger}) where {IT<:Integer} = IT + +function Base.promote_rule(::Type{OffsetInteger{O1,T1}}, + ::Type{OffsetInteger{O2,T2}}) where {O1,O2,T1<:Integer, + T2<:Integer} + return OffsetInteger{pure_max(O1, O2),promote_type(T1, T2)} +end + +Base.@pure pure_max(x1, x2) = x1 > x2 ? x1 : x2 diff --git a/src/viewtypes.jl b/src/viewtypes.jl index 82c8e553..c0932f2f 100644 --- a/src/viewtypes.jl +++ b/src/viewtypes.jl @@ -56,11 +56,6 @@ function TupleView{N,M}(x::AbstractVector{T}; connect=false) where {T,N,M} return TupleView{NTuple{N,T},N,M,typeof(x)}(x, connect) end -function connected_line(points::AbstractVector{<:Point{N}}, - skip=N) where {N} - return connect(points, Line, skip) -end - """ connect(points::AbstractVector{<: Point}, P::Type{<: Polytope{N}}, skip::Int = N) @@ -83,7 +78,7 @@ end function connect(indices::AbstractVector{T}, P::Type{<:AbstractFace{N}}, skip::Int=N) where {T <: Integer, N} - return map(Face(P, T), TupleView{N, skip}(indices)) + return collect(reinterpret(Face(P, T), TupleView{N, skip}(indices))) end function connect(points::AbstractMatrix{T}, P::Type{<:Point{N}}) where {T <: Real, N} From 635150eca80de9766904bc2578a12a34b20a84e8 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Mon, 20 Jun 2022 20:58:03 +0200 Subject: [PATCH 16/58] bring back connect --- src/viewtypes.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/viewtypes.jl b/src/viewtypes.jl index c0932f2f..41937a39 100644 --- a/src/viewtypes.jl +++ b/src/viewtypes.jl @@ -94,3 +94,7 @@ function connect(points::AbstractMatrix{T}, P::Type{<:Point{N}}) where {T <: Rea error("Dim 1 or 2 must be equal to the point dimension!") end end + +function connect(elements::AbstractVector, faces::AbstractVector{<: AbstractFace}) + return [elements[i] for f in faces for i in f] +end From 991baccd3c388eec387fa4c1955301efb8e30196 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Tue, 21 Jun 2022 11:59:10 +0200 Subject: [PATCH 17/58] add / for e.g. mean(points) --- src/fixed_arrays.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index cf81c887..ff0fcadc 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -123,9 +123,9 @@ end Base.:(-)(a::StaticVector) = (-).(a) -import Base: *, +, - +import Base: *, +, -, / -for op in [:*, :+, :-] +for op in [:*, :+, :-, :/] @eval begin ($op)(a::StaticVector, b::StaticVector) = Base.broadcasted($(op), a, b) ($op)(a::Number, b::StaticVector) = Base.broadcasted($(op), a, b) From 90aed0e9c2c7c488c8a7edeefc7be5bfdab39705 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Tue, 21 Jun 2022 14:23:16 +0200 Subject: [PATCH 18/58] small improvements --- src/basic_types.jl | 5 +++-- src/fixed_arrays.jl | 1 + src/interfaces.jl | 4 ++++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index d98d34cb..a39a8cdb 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -270,8 +270,8 @@ function MetaMesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Abstr MetaMesh(Mesh(points, faces), values(meta)) end -function MetaMesh(m::AbstractMesh; meta...) - MetaMesh(m, values(meta)) +function MetaMesh(m::AbstractMesh; kw...) + MetaMesh(m, merge(meta(m), values(kw))) end @inline Base.hasproperty(mesh::MetaMesh, field::Symbol) = hasproperty(getfield(mesh, :meta), field) @@ -283,5 +283,6 @@ faces(mesh::MetaMesh) = faces(Mesh(mesh)) normals(mesh::MetaMesh) = hasproperty(mesh, :normals) ? mesh.normals : nothing texturecoordinates(mesh::MetaMesh) = hasproperty(mesh, :uv) ? mesh.uv : nothing +meta(@nospecialize(m)) = NamedTuple() meta(mesh::MetaMesh) = getfield(mesh, :meta) Mesh(mesh::MetaMesh) = getfield(mesh, :mesh) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index ff0fcadc..eb8c3607 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -195,6 +195,7 @@ Base.:(+)(a::Vec{N}, b::Point{N}) where {N} = Point{N}(a.data .+ b.data) const VecTypes{N,T} = Union{StaticVector{N,T}, NTuple{N,T}} const Vecf{N} = Vec{N, Float32} +const PointT{T} = Point{N,T} where N const Pointf{N} = Point{N,Float32} Base.isnan(p::Union{Point,Vec}) = any(x -> isnan(x), p) diff --git a/src/interfaces.jl b/src/interfaces.jl index 5b083834..f0235bb7 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -121,6 +121,10 @@ function decompose(::Type{Point}, primitive::AbstractGeometry{Dim,T}) where {Dim return collect_with_eltype(Point{Dim,T}, coordinates(primitive)) end +function decompose(::Type{PointT{T}}, primitive::AbstractGeometry{Dim}) where {Dim, T} + return collect_with_eltype(Point{Dim,T}, coordinates(primitive)) +end + function decompose(::Type{T}, primitive) where {T} return collect_with_eltype(T, primitive) end From ffa5e0866a957989a48f8ca97829ccd280e69cb1 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 22 Jun 2022 15:33:26 +0200 Subject: [PATCH 19/58] introduce TriangleMesh --- src/interfaces.jl | 2 +- src/meshes.jl | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/interfaces.jl b/src/interfaces.jl index f0235bb7..b0b5a11e 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -141,7 +141,7 @@ function decompose(::Normal{T}, primitive) where {T} n = normals(decompose(Point, primitive), faces(primitive), T) else points = decompose(Point, primitive) - n = (Vec3f(0, 0, 1) for p in points) + n = [T(0, 0, 1) for p in points] end else n = _n diff --git a/src/meshes.jl b/src/meshes.jl index ba725134..ced98476 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -28,6 +28,9 @@ function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleF return Mesh(positions, f) end +const SimpleMesh{N, T, FT} = Mesh{N, T, Vector{Point{N, T}}, Vector{FT}} +const TriangleMesh{N} = SimpleMesh{N, Float32, GLTriangleFace} + """ mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace, normaltype=nothing) @@ -38,11 +41,11 @@ function mesh(polygon::AbstractVector{P}; pointtype=P) where {P<:Point{2}} return mesh(Polygon(polygon); pointtype=pointtype) end -function triangle_mesh(primitive::AbstractGeometry{N}) where {N} +function triangle_mesh(primitive::AbstractGeometry{N})::TriangleMesh{N} where {N} return mesh(primitive; pointtype=Point{N, Float32}) end -function triangle_mesh(primitive::AbstractVector{<: Point}) +function triangle_mesh(primitive::AbstractVector{<: Point2})::TriangleMesh{2} return mesh(Polygon(primitive)) end From cba8ec858c043d9936f6821f788007d735550f74 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 22 Jun 2022 17:31:48 +0200 Subject: [PATCH 20/58] fix constructor and print Mesh more nicely --- src/basic_types.jl | 2 +- src/meshes.jl | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index a39a8cdb..c394324a 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -271,7 +271,7 @@ function MetaMesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Abstr end function MetaMesh(m::AbstractMesh; kw...) - MetaMesh(m, merge(meta(m), values(kw))) + MetaMesh(Mesh(m), merge(meta(m), values(kw))) end @inline Base.hasproperty(mesh::MetaMesh, field::Symbol) = hasproperty(getfield(mesh, :meta), field) diff --git a/src/meshes.jl b/src/meshes.jl index ced98476..fa0a78cd 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -174,3 +174,30 @@ function Base.get(f, mesh::MetaMesh, key::Symbol) hasproperty(mesh, key) && return getproperty(mesh, key) return f() end + +function Base.show(io::IO, ::MIME"text/plain", mesh::Mesh{N, T}) where {N, T} + FT = eltype(faces(mesh)) + println(io, "Mesh{$N, $T, $(FT)}") + println(io, " vertices: ", length(coordinates(mesh))) + println(io, " faces: ", length(faces(mesh)), " $(FT)") +end + +function Base.show(io::IO, mesh::Mesh{N, T}) where {N, T} + FT = eltype(faces(mesh)) + print(io, "Mesh{$N, $T, $(FT)}(...)") +end + +function Base.show(io::IO, ::MIME"text/plain", mesh::MetaMesh{N, T}) where {N, T} + FT = eltype(faces(mesh)) + println(io, "MetaMesh{$N, $T, $(FT)}") + println(io, " vertices: ", length(coordinates(mesh))) + println(io, " faces: ", length(faces(mesh)), " $(FT)") + for (k, v) in pairs(meta(mesh)) + println(io, " ", k, ": ", length(v), " $(eltype(v))") + end +end + +function Base.show(io::IO, mesh::MetaMesh{N, T}) where {N, T} + FT = eltype(faces(mesh)) + println(io, "MetaMesh{$N, $T, $(FT)}($(join(keys(meta(mesh)), ", ")))") +end From 1238b052fc08f696c1ca3ef7e7cee91e560025fb Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Mon, 8 Aug 2022 16:32:36 +0200 Subject: [PATCH 21/58] add zero for value --- src/fixed_arrays.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index eb8c3607..66960499 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -158,6 +158,7 @@ function unit(::Type{T}, i::Integer) where {T <: StaticVector} end Base.zero(::Type{V}) where {V <:StaticVector} = V(0) +Base.zero(::V) where {V <:StaticVector} = zero(V) function Base.:(==)(a::StaticVector{N}, b::StaticVector{N}) where N for i in 1:N From a7fba750293d30abd04008bcc9956c8817010f2e Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Thu, 15 Sep 2022 17:17:05 +0200 Subject: [PATCH 22/58] fix tests --- src/GeometryBasics.jl | 3 +- src/basic_types.jl | 74 +++++++++++++++++++++++-------------------- src/fixed_arrays.jl | 9 +++--- src/geointerface.jl | 16 +++++----- test/geointerface.jl | 5 +-- test/meshes.jl | 3 +- test/runtests.jl | 7 ++-- 7 files changed, 63 insertions(+), 54 deletions(-) diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index 5e27787c..e8ea1635 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -30,7 +30,8 @@ include("geointerface.jl") export AbstractGeometry, GeometryPrimitive export Mat, Point, Vec export LineFace, Polytope, Line, NgonFace, convert_simplex -export AbstractPolygon, Polygon +export LineString, MultiLineString, MultiPoint +export AbstractPolygon, Polygon, MultiPolygon export Simplex, connect, Triangle, NSimplex, Tetrahedron export QuadFace, coordinates, TetrahedronFace export TupleView, SimplexFace diff --git a/src/basic_types.jl b/src/basic_types.jl index 142bc938..67a8deb6 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -171,13 +171,12 @@ Base.show(io::IO, x::Line) = print(io, "Line(", x[1], " => ", x[2], ")") Polygon(exterior::AbstractVector{<:Point}, interiors::Vector{<:AbstractVector{<:Point}}) """ -struct Polygon{Dim,T<:Real,L<:AbstractVector{Point{Dim,T}}, - V<:AbstractVector{L}} <: AbstractPolygon{Dim,T} - exterior::L - interiors::V +struct Polygon{Dim,T<:Real} <: AbstractPolygon{Dim,T} + exterior::Vector{Point{Dim, T}} + interiors::Vector{Vector{Point{Dim, T}}} end -Base.copy(x::Polygon) = Polygon(copy(x.exterior), copy(x.interiors)) +Base.copy(x::Polygon) = Polygon(copy(x.exterior), deepcopy(x.interiors)) function Base.:(==)(a::Polygon, b::Polygon) return (a.exterior == b.exterior) && (a.interiors == b.interiors) @@ -186,27 +185,24 @@ end function Polygon( exterior::AbstractVector{Point{Dim,T}}, interiors::AbstractVector{AbstractVector{Point{Dim,T}}}) where {Dim, T} - return Polygon{Dim,T,typeof(exterior),typeof(interiors)}(exterior, interiors) + tov(v) = convert(Vector{Point{Dim, T}}, v) + return Polygon{Dim,T}(tov(exterior), map(tov, interiors)) end Polygon(exterior::AbstractVector{Point{N, T}}) where {N, T} = Polygon(exterior, Vector{Point{N, T}}[]) -function Polygon(exterior::AbstractVector{Point{Dim, T}}, faces::AbstractVector{<:Integer}, - skip::Int=1) where {Dim,T} - return Polygon(LineString(exterior, faces, skip)) -end - function Polygon(exterior::AbstractVector{Point{Dim,T}}, faces::AbstractVector{<:LineFace}) where {Dim, T} return Polygon(LineString(exterior, faces)) end -function Polygon(exterior::AbstractVector{Point{Dim, T}}, - interior::AbstractVector{<:AbstractVector{Point{Dim, T}}}) where {Dim,T} - # if we just map over it, it won't infer the element type correctly! - int = typeof(exterior)[] - foreach(x -> push!(int, x), interior) - return Polygon(exterior, int) +function Polygon(exterior::AbstractGeometry{Dim, T}, interior::AbstractVector=[]) where {Dim, T} + to_p(v) = decompose(Point{Dim, T}, v) + int = Vector{Point{Dim, T}}[] + for i in interior + push!(int, to_p(i)) + end + return Polygon(to_p(exterior), int) end function coordinates(polygon::Polygon{N,T}) where {N,T} @@ -224,49 +220,56 @@ end """ MultiPolygon(polygons::AbstractPolygon) """ -struct MultiPolygon{Dim,T<:Real,Element<:AbstractPolygon{Dim,T}, - A<:AbstractVector{Element}} <: AbstractVector{Element} - polygons::A +struct MultiPolygon{Dim, T<:Real} <: AbstractGeometry{Dim, T} + polygons::Vector{<:AbstractPolygon{Dim,T}} end -function MultiPolygon(polygons::AbstractVector{P}; - kw...) where {P<:AbstractPolygon{Dim,T}} where {Dim,T} - return MultiPolygon(meta(polygons; kw...)) +function MultiPolygon(polygons::AbstractVector{<:AbstractPolygon{Dim,T}}) where {Dim,T} + return MultiPolygon(convert(Vector{eltype(polygons)}, polygons)) end Base.getindex(mp::MultiPolygon, i) = mp.polygons[i] Base.size(mp::MultiPolygon) = size(mp.polygons) +Base.length(mp::MultiPolygon) = length(mp.polygons) + +""" + LineString(points::AbstractVector{<:Point}) +A LineString is a geometry of connected line segments +""" +struct LineString{Dim, T<:Real} <: AbstractGeometry{Dim, T} + points::Vector{Point{Dim, T}} +end +Base.length(ls::LineString) = length(coordinates(ls)) +coordinates(ls::LineString) = ls.points -struct MultiLineString{Dim,T<:Real,Element<:LineString{Dim,T},A<:AbstractVector{Element}} <: - AbstractVector{Element} - linestrings::A +struct MultiLineString{Dim, T<:Real} <: AbstractGeometry{Dim, T} + linestrings::Vector{LineString{Dim, T}} end -function MultiLineString(linestrings::AbstractVector{L}; - kw...) where {L<:AbstractVector{LineP{Dim,T,P}}} where {Dim,T,P} - return MultiLineString(meta(linestrings; kw...)) +function MultiLineString(linestrings::AbstractVector{L}) where {L<:LineString} + return MultiLineString(convert(Vector{L}, linestrings)) end Base.getindex(ms::MultiLineString, i) = ms.linestrings[i] Base.size(ms::MultiLineString) = size(ms.linestrings) +Base.length(mpt::MultiLineString) = length(mpt.linestrings) """ MultiPoint(points::AbstractVector{AbstractPoint}) A collection of points """ -struct MultiPoint{Dim,T<:Real,P<:AbstractPoint{Dim,T},A<:AbstractVector{P}} <: - AbstractVector{P} - points::A +struct MultiPoint{Dim,T<:Real} <: AbstractGeometry{Dim, T} + points::Vector{Point{Dim, T}} end -function MultiPoint(points::AbstractVector{P}; - kw...) where {P<:AbstractPoint{Dim,T}} where {Dim,T} - return MultiPoint(meta(points; kw...)) +function MultiPoint(points::AbstractVector{Point{Dim, T}}) where {Dim,T} + return MultiPoint(convert(Vector{Point{Dim, T}}, points)) end Base.getindex(mpt::MultiPoint, i) = mpt.points[i] Base.size(mpt::MultiPoint) = size(mpt.points) +Base.length(mpt::MultiPoint) = length(mpt.points) """ AbstractMesh @@ -333,3 +336,4 @@ texturecoordinates(mesh::MetaMesh) = hasproperty(mesh, :uv) ? mesh.uv : nothing meta(@nospecialize(m)) = NamedTuple() meta(mesh::MetaMesh) = getfield(mesh, :meta) Mesh(mesh::MetaMesh) = getfield(mesh, :mesh) +Mesh(mesh::Mesh) = mesh diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index 66960499..6f31a69d 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -56,13 +56,13 @@ macro fixed_vector(VecT, SuperT) $(VecT)(x::NTuple{N, T}) where {N, T} = $(VecT){N,T}(x) $(VecT){N}(x::NTuple{N, T}) where {N, T} = $(VecT){N,T}(x) - $(VecT)(x::Vararg{<:Any,N}) where {N} = $(VecT){N}(x) + $(VecT)(x::Vararg{Any,N}) where {N} = $(VecT){N}(x) $(VecT)(x::Vararg{T,N}) where {T,N} = $(VecT){N,T}(x) - $(VecT){N}(x::Vararg{<:Any,N}) where {N} = $(VecT){N}(x) + $(VecT){N}(x::Vararg{Any,N}) where {N} = $(VecT){N}(x) $(VecT){N}(x::Vararg{T,N}) where {T,N} = $(VecT){N,T}(x) - $(VecT){N, T}(x::Vararg{<:Any,N}) where {T,N} = $(VecT){N,T}(x) + $(VecT){N, T}(x::Vararg{Any,N}) where {T,N} = $(VecT){N,T}(x) $(VecT){N, T1}(x::Vararg{T2,N}) where {T1,T2,N} = $(VecT){N, T1}(x) Base.convert(::Type{$(VecT){N,T}}, x) where {N,T} = $(VecT){N,T}(x) @@ -70,6 +70,7 @@ macro fixed_vector(VecT, SuperT) Base.convert(::Type{$(VecT){N}}, x::$(VecT){N}) where {N} = x Base.convert(::Type{$(VecT){N,T}}, x::$(VecT){N,T}) where {N,T} = x + function Base.convert(::Type{$(VecT){N,T}}, x::NTuple{N,T}) where {N,T} return $(VecT){N,T}(x) end @@ -80,7 +81,7 @@ macro fixed_vector(VecT, SuperT) @inline similar_type(::$(VecT){N, T}, n::Integer) where {N, T} = $(VecT){n} @inline similar_type(::$(VecT){N}, ::Type{T}) where {N, T} = $(VecT){N, T} - @inline similar_type(::$(VecT), n::Integer, ::Type{T}) where {N, T} = $(VecT){n, T} + @inline similar_type(::$(VecT), n::Integer, ::Type{T}) where {T} = $(VecT){n, T} @inline similar_type(::$(VecT)) = $(VecT) function Base.broadcasted(f, a::$(VecT), b::$(VecT)) diff --git a/src/geointerface.jl b/src/geointerface.jl index 0b73b284..0619f0af 100644 --- a/src/geointerface.jl +++ b/src/geointerface.jl @@ -2,9 +2,9 @@ GeoInterface.isgeometry(::Type{<:AbstractGeometry}) = true GeoInterface.isgeometry(::Type{<:AbstractFace}) = true -GeoInterface.isgeometry(::Type{<:AbstractPoint}) = true +GeoInterface.isgeometry(::Type{<:Point}) = true GeoInterface.isgeometry(::Type{<:AbstractVector{<:AbstractGeometry}}) = true -GeoInterface.isgeometry(::Type{<:AbstractVector{<:AbstractPoint}}) = true +GeoInterface.isgeometry(::Type{<:AbstractVector{<:Point}}) = true GeoInterface.isgeometry(::Type{<:AbstractVector{<:LineString}}) = true GeoInterface.isgeometry(::Type{<:AbstractVector{<:AbstractPolygon}}) = true GeoInterface.isgeometry(::Type{<:AbstractVector{<:AbstractFace}}) = true @@ -30,7 +30,7 @@ GeoInterface.getcoord(::PointTrait, g::Point, i::Int) = g[i] GeoInterface.ngeom(::LineTrait, g::Line) = length(g) GeoInterface.getgeom(::LineTrait, g::Line, i::Int) = g[i] -GeoInterface.ngeom(::LineStringTrait, g::LineString) = length(g) + 1 # n line segments + 1 +GeoInterface.ngeom(::LineStringTrait, g::LineString) = length(g) # n line segments + 1 function GeoInterface.getgeom(::LineStringTrait, g::LineString, i::Int) return GeometryBasics.coordinates(g)[i] end @@ -38,8 +38,8 @@ end GeoInterface.ngeom(::PolygonTrait, g::Polygon) = length(g.interiors) + 1 # +1 for exterior function GeoInterface.getgeom(::PolygonTrait, g::Polygon, - i::Int)::typeof(g.exterior) - return i > 1 ? g.interiors[i - 1] : g.exterior + i::Int) + return i > 1 ? LineString(g.interiors[i - 1]) : LineString(g.exterior) end GeoInterface.ngeom(::MultiPointTrait, g::MultiPoint) = length(g) @@ -57,7 +57,7 @@ GeoInterface.ngeom(::MultiPolygonTrait, g::MultiPolygon) = length(g) GeoInterface.getgeom(::MultiPolygonTrait, g::MultiPolygon, i::Int) = g[i] function GeoInterface.ncoord(::AbstractGeometryTrait, - ::Simplex{Dim,T,N,P}) where {Dim,T,N,P} + ::Simplex{Dim,T,N}) where {Dim,T,N} return Dim end function GeoInterface.ncoord(::AbstractGeometryTrait, @@ -65,11 +65,11 @@ function GeoInterface.ncoord(::AbstractGeometryTrait, return Dim end function GeoInterface.ngeom(::AbstractGeometryTrait, - ::Simplex{Dim,T,N,P}) where {Dim,T,N,P} + ::Simplex{Dim,T,N}) where {Dim,T,N} return N end GeoInterface.ngeom(::PolygonTrait, ::Ngon) = 1 # can't have any holes -GeoInterface.getgeom(::PolygonTrait, g::Ngon, _) = LineString(g.points) +GeoInterface.getgeom(::PolygonTrait, g::Ngon, _) = LineString([g.points...]) function GeoInterface.ncoord(::PolyhedralSurfaceTrait, ::Mesh{Dim,T,E,V} where {Dim,T,E,V}) diff --git a/test/geointerface.jl b/test/geointerface.jl index b70d63fc..1a1c67f7 100644 --- a/test/geointerface.jl +++ b/test/geointerface.jl @@ -81,8 +81,9 @@ end @test point_gb === Point{2, Float64}(30.1, 10.1) @test point_3d_gb === Point{3, Float64}(30.1, 10.1, 5.1) @test linestring_gb isa LineString - @test length(linestring_gb) == 2 - @test eltype(linestring_gb) == Line{2, Float64} + # TODO, what should we do exactly with linestrings? + # @test length(linestring_gb) == 2 + # @test eltype(linestring_gb) == Line{2, Float64} @test polygon_gb isa Polygon @test isempty(polygon_gb.interiors) @test polygon_hole_gb isa Polygon diff --git a/test/meshes.jl b/test/meshes.jl index 56cca5c9..192c7400 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -24,7 +24,8 @@ end @testset "Ambiguous NgonFace constructors" begin # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/151 - t = TriangleFace(SA[0.4, 0.2, 0.55]) + # Currently no StaticVector support + # t = TriangleFace(SA[0.4, 0.2, 0.55]) end @testset "Merge empty vector of meshes" begin diff --git a/test/runtests.jl b/test/runtests.jl index b369b4ad..2769c372 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -52,7 +52,7 @@ end m_meta = MetaMesh(m; boundingbox=Rect(1.0, 1.0, 2.0, 2.0)) @test m_meta.boundingbox === Rect(1.0, 1.0, 2.0, 2.0) @test propertynames(m_meta) == (:boundingbox,) - end + end end @testset "view" begin @@ -220,7 +220,7 @@ end m_normal = add_meta(m, normals = decompose_normals(m)) @test hasproperty(m_normal, :uv) @test coordinates(m) === coordinates(m_normal) - @test decompose_normals(m) == normals(m_normal) + @test decompose_normals(m) == GeometryBasics.normals(m_normal) # uv stays untouched, since we don't specify the element type in normalmesh @test m.uv === m_normal.uv end @@ -331,6 +331,7 @@ end using Aqua # Aqua tests # Intervals brings a bunch of ambiquities unfortunately -Aqua.test_all(GeometryBasics; ambiguities=false) +# seems like we also run into https://github.com/JuliaTesting/Aqua.jl/issues/86 +Aqua.test_all(GeometryBasics; ambiguities=false, unbound_args=false) end # testset "GeometryBasics" From 785d940782c8cda16bdd45ea2275ba517ea70c4b Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Fri, 16 Sep 2022 14:55:27 +0200 Subject: [PATCH 23/58] polys --- src/primitives/rectangles.jl | 3 +-- test/polygons.jl | 28 ++++++++++++++++++++++++++++ test/runtests.jl | 6 ------ 3 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 test/polygons.jl diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index cf000a7e..65370ccc 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -526,8 +526,7 @@ end function coordinates(rect::Rect2, nvertices=(2, 2)) mini, maxi = extrema(rect) - xrange, yrange = LinRange.(mini, maxi, nvertices) - return ivec(((x, y) for x in xrange, y in yrange)) + return [mini, Point(mini[1], maxi[2]), maxi, Point(maxi[1], mini[2])] end function texturecoordinates(rect::Rect2, nvertices=(2, 2)) diff --git a/test/polygons.jl b/test/polygons.jl new file mode 100644 index 00000000..898a6f30 --- /dev/null +++ b/test/polygons.jl @@ -0,0 +1,28 @@ +@testset "Polygon" begin + @testset "from points" begin + points = connect([1, 2, 3, 4, 5, 6], PointPoint2f(2)) + polygon = Polygon(points) + @test polygon == Polygon(points) + end + +end + +rect = Rect2f(0, 0, 1, 1) +hole = Circle(Point2f(0.5), 0.2) +poly2 = Polygon(decompose(Point2f, rect), [decompose(Point2f, hole)]) +poly1 = Polygon(rect, [hole]) +@test poly1 == poly2 +@test poly.exterior == decompose(Point2f, rect) +@test poly.interiors == [decompose(Point2f, hole)] + +faces(poly1) + +GeometryBasics.earcut_triangulate([poly.exterior[[1, 2, 3, 4, 1]]]) + +poly = [ + [Point2f(100, 0), Point2f(100, 100), Point2f(0, 100), Point2f(0, 0)], + # Following polylines define holes. + [Point2f(75, 25), Point2f(75, 75), Point2f(25, 75), Point2f(25, 25)] +] + +GeometryBasics.earcut_triangulate(poly) diff --git a/test/runtests.jl b/test/runtests.jl index 2769c372..f21a1f8e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -113,12 +113,6 @@ end end @testset "constructors" begin - @testset "Polygon" begin - points = connect([1, 2, 3, 4, 5, 6], Point{2}) - polygon = Polygon(points) - @test polygon == Polygon(points) - end - @testset "Mesh" begin numbers = [1, 2, 3, 4, 5, 6] points = connect(numbers, Point{2}) From 113812f248ed8701227efe221cfd94d8e6f3809c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 30 Aug 2024 21:50:32 +0200 Subject: [PATCH 24/58] fix some compile errors --- src/fixed_arrays.jl | 9 +++++---- src/meshes.jl | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index 388037d6..e811de97 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -8,7 +8,7 @@ function similar_type end macro fixed_vector(name_parent) @assert name_parent.head == :(=) - name, parent = name_parent.args + VecT, SuperT = name_parent.args expr = quote struct $(VecT){N, T} <: $(SuperT){N, T} @@ -204,9 +204,9 @@ const Vecf{N} = Vec{N, Float32} const PointT{T} = Point{N,T} where N const Pointf{N} = Point{N,Float32} -Base.isnan(p::Union{AbstractPoint,Vec}) = any(isnan, p) -Base.isinf(p::Union{AbstractPoint,Vec}) = any(isinf, p) -Base.isfinite(p::Union{AbstractPoint,Vec}) = all(isfinite, p) +Base.isnan(p::Union{Point,Vec}) = any(isnan, p) +Base.isinf(p::Union{Point,Vec}) = any(isinf, p) +Base.isfinite(p::Union{Point,Vec}) = all(isfinite, p) ## Generate aliases ## As a text file instead of eval/macro, to not confuse code linter @@ -232,6 +232,7 @@ open(joinpath(@__DIR__, "generated-aliases.jl"), "w") do io end =# +include("mat.jl") include("generated-aliases.jl") export Mat, Vec, Point, unit diff --git a/src/meshes.jl b/src/meshes.jl index 43d690a0..496a8880 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -58,7 +58,7 @@ function triangle_mesh(primitive::Mesh{N}) where {N} end end -function triangle_mesh(primitive::Meshable{N}; nvertices=nothing) where {N} +function triangle_mesh(primitive::GeometryPrimitive{N}; nvertices=nothing) where {N} if nvertices !== nothing @warn("nvertices argument deprecated. Wrap primitive in `Tesselation(primitive, nvertices)`") primitive = Tesselation(primitive, nvertices) From 26a861a58c7d145e4ba2789e970e26d39f12e40c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 30 Aug 2024 21:52:04 +0200 Subject: [PATCH 25/58] fix Simplex <: AbstractSimplex <: Polytope --- src/basic_types.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index ea5ff02e..0b3f39af 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -17,7 +17,7 @@ abstract type AbstractFace{N,T} <: StaticVector{N,T} end abstract type AbstractSimplexFace{N,T} <: AbstractFace{N,T} end abstract type AbstractNgonFace{N,T} <: AbstractFace{N,T} end -abstract type AbstractSimplex{Dim,N,T} <: StaticVector{Dim,T} end +abstract type AbstractSimplex{Dim,T} <: Polytope{Dim,T} end @propagate_inbounds function Base.getindex(points::AbstractVector{P}, face::F) where {P<: Point, F <: AbstractFace} return Polytope(P, F)(map(i-> points[i], face.data)) @@ -136,7 +136,7 @@ This is for a simpler implementation. It applies to infinite dimensions. The structure of this type is designed to allow embedding in higher-order spaces by parameterizing on `T`. """ -struct Simplex{Dim,T<:Real,N} <: Polytope{Dim,T} +struct Simplex{Dim,T<:Real,N} <: AbstractSimplex{Dim,T} points::NTuple{N,Point{Dim,T}} end From 5567b0410251ac66d7f53aa45e7ee8d81f0d0922 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 30 Aug 2024 23:58:26 +0200 Subject: [PATCH 26/58] fix decompose test --- src/meshes.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/meshes.jl b/src/meshes.jl index 496a8880..49a8de41 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -37,13 +37,13 @@ const TriangleMesh{N} = SimpleMesh{N, Float32, GLTriangleFace} Create a mesh from a polygon given as a vector of points, using triangulation. """ -function mesh(polygon::AbstractVector{P}; pointtype=P) where {P<:Point{2}} - return mesh(Polygon(polygon); pointtype=pointtype) +function mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace) where {P<:Point{2}} + return mesh(Polygon(polygon); pointtype=pointtype, facetype=facetype) end -function triangle_mesh(primitive::AbstractGeometry{N})::TriangleMesh{N} where {N} - return mesh(primitive; pointtype=Point{N, Float32}) -end +# function triangle_mesh(primitive::Union{AbstractGeometry{N}, AbstractVector{<: Point{2}}})::TriangleMesh{N} where {N} +# return mesh(primitive; pointtype=Point{N, Float32}) +# end pointtype(x::Mesh) = eltype(decompose(Point, x)) @@ -58,7 +58,7 @@ function triangle_mesh(primitive::Mesh{N}) where {N} end end -function triangle_mesh(primitive::GeometryPrimitive{N}; nvertices=nothing) where {N} +function triangle_mesh(primitive::Union{AbstractGeometry{N}, AbstractVector{<: Point{N}}}; nvertices = nothing)::TriangleMesh{N} where {N} if nvertices !== nothing @warn("nvertices argument deprecated. Wrap primitive in `Tesselation(primitive, nvertices)`") primitive = Tesselation(primitive, nvertices) From 12c6eb4f35f9bdb01e33e5fcb483e9a7cef74b75 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 31 Aug 2024 00:43:02 +0200 Subject: [PATCH 27/58] fix Rect2 Tesselation --- src/primitives/rectangles.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index 24fccd12..a23ce2c8 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -548,7 +548,8 @@ end function coordinates(rect::Rect2, nvertices=(2, 2)) mini, maxi = extrema(rect) - return [mini, Point(mini[1], maxi[2]), maxi, Point(maxi[1], mini[2])] + xrange, yrange = LinRange.(mini, maxi, nvertices) + return [Point(x, y) for y in yrange for x in xrange] end function texturecoordinates(rect::Rect2, nvertices=(2, 2)) From 2fa20202185a3f156314de6927e7d1920105e9bd Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 31 Aug 2024 01:18:57 +0200 Subject: [PATCH 28/58] fix ngeom for LineString --- src/geointerface.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/geointerface.jl b/src/geointerface.jl index 8cd8c0b7..ef66143b 100644 --- a/src/geointerface.jl +++ b/src/geointerface.jl @@ -43,7 +43,7 @@ GeoInterface.ngeom(::LineTrait, g::Line) = length(g) GeoInterface.getgeom(::LineTrait, g::Line, i::Int) = g[i] -GeoInterface.ngeom(::LineStringTrait, g::LineString) = length(g) + 1 # n line segments + 1 +GeoInterface.ngeom(::LineStringTrait, g::LineString) = length(g) # n connected points GeoInterface.ncoord(::LineStringTrait, g::LineString{Dim}) where {Dim} = Dim function GeoInterface.getgeom(::LineStringTrait, g::LineString, i::Int) return GeometryBasics.coordinates(g)[i] From 8a6c97533c83f84cfb373035815dfe485714c77c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 31 Aug 2024 01:24:42 +0200 Subject: [PATCH 29/58] fix compat bounds --- Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Project.toml b/Project.toml index 19ea1dfb..7f711f7b 100644 --- a/Project.toml +++ b/Project.toml @@ -21,6 +21,7 @@ IterTools = "1.3.0" LinearAlgebra = "<0.0.1,1" OffsetArrays = "1" Random = "<0.0.1,1" +Test = "<0.0.1,1" julia = "1.6" [extras] From 6c7b40743502c6833ff526187e4eca29b134f0ea Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 31 Aug 2024 17:58:53 +0200 Subject: [PATCH 30/58] change getindex index to Integer --- src/fixed_arrays.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index e811de97..269c0b80 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -115,7 +115,7 @@ function Random.randn(rng::Random.AbstractRNG, ::Type{V}) where V <: StaticVecto V(ntuple(i-> randn(rng, T), N)) end -Base.@propagate_inbounds function Base.getindex(v::StaticVector{N,T}, i::Int) where {N,T} +Base.@propagate_inbounds function Base.getindex(v::StaticVector{N,T}, i::Integer) where {N,T} return v.data[i] end From 15eb932c35b91081d880e6ed3c8a542642d18679 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 31 Aug 2024 20:33:33 +0200 Subject: [PATCH 31/58] remove triangle_mesh Tesselation deprecation warning --- src/meshes.jl | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/meshes.jl b/src/meshes.jl index 49a8de41..b29e1888 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -41,9 +41,9 @@ function mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace) return mesh(Polygon(polygon); pointtype=pointtype, facetype=facetype) end -# function triangle_mesh(primitive::Union{AbstractGeometry{N}, AbstractVector{<: Point{2}}})::TriangleMesh{N} where {N} -# return mesh(primitive; pointtype=Point{N, Float32}) -# end +function triangle_mesh(primitive::Union{AbstractGeometry{N}, AbstractVector{<: Point{N}}})::TriangleMesh{N} where {N} + return mesh(primitive; pointtype=Point{N, Float32}) +end pointtype(x::Mesh) = eltype(decompose(Point, x)) @@ -58,14 +58,6 @@ function triangle_mesh(primitive::Mesh{N}) where {N} end end -function triangle_mesh(primitive::Union{AbstractGeometry{N}, AbstractVector{<: Point{N}}}; nvertices = nothing)::TriangleMesh{N} where {N} - if nvertices !== nothing - @warn("nvertices argument deprecated. Wrap primitive in `Tesselation(primitive, nvertices)`") - primitive = Tesselation(primitive, nvertices) - end - return mesh(primitive; pointtype=Point{N,Float32}, facetype=GLTriangleFace) -end - function uv_mesh(primitive::AbstractGeometry{N,T}) where {N,T} m = triangle_mesh(primitive) return MetaMesh(m, (uv=decompose_uv(primitive),)) From f600cb62c42c453250a6e915d9b0191f108845c4 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 2 Sep 2024 15:51:22 +0200 Subject: [PATCH 32/58] fix Base.intersect extension --- src/primitives/rectangles.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index a23ce2c8..b1909b0b 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -361,7 +361,7 @@ diff(h1::Rect, h2::Rect) = h1 Perform a intersection between two Rects. """ -function intersect(h1::Rect{N}, h2::Rect{N}) where {N} +function Base.intersect(h1::Rect{N}, h2::Rect{N}) where {N} m = max.(minimum(h1), minimum(h2)) mm = min.(maximum(h1), maximum(h2)) return Rect{N}(m, mm - m) From 5bcefb1e783e27450cd57a0b5a38d201164c582e Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 2 Sep 2024 16:00:34 +0200 Subject: [PATCH 33/58] update union docs and comment out broken diff or rects --- src/primitives/rectangles.jl | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index b1909b0b..89c45cac 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -341,7 +341,9 @@ Return `true` if any of the widths of `h` are negative. Base.isempty(h::Rect{N,T}) where {N,T} = any(<(zero(T)), h.widths) """ -Perform a union between two Rects. + union(r1::Rect{N}, r2::Rect{N}) + +Returns a new `Rect{N}` which contains both r1 and r2. """ function Base.union(h1::Rect{N}, h2::Rect{N}) where {N} m = min.(minimum(h1), minimum(h2)) @@ -349,12 +351,14 @@ function Base.union(h1::Rect{N}, h2::Rect{N}) where {N} return Rect{N}(m, mm - m) end -""" - diff(h1::Rect, h2::Rect) +# TODO: What should this be? The difference is "h2 - h1", which could leave an +# L shaped cutout. Should we pad that back out into a full rect? +# """ +# diff(h1::Rect, h2::Rect) -Perform a difference between two Rects. -""" -diff(h1::Rect, h2::Rect) = h1 +# Perform a difference between two Rects. +# """ +# diff(h1::Rect, h2::Rect) = h1 """ intersect(h1::Rect, h2::Rect) From fe8380d7fcbc869f2abc42f5d7d5b985874193c5 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 2 Sep 2024 18:30:17 +0200 Subject: [PATCH 34/58] add decompose_x methdos with types --- src/interfaces.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/interfaces.jl b/src/interfaces.jl index b0b5a11e..bed84d4b 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -133,6 +133,10 @@ decompose_uv(primitive) = decompose(UV(), primitive) decompose_uvw(primitive) = decompose(UVW(), primitive) decompose_normals(primitive) = decompose(Normal(), primitive) +decompose_uv(T, primitive) = decompose(UV(T), primitive) +decompose_uvw(T, primitive) = decompose(UVW(T), primitive) +decompose_normals(T, primitive) = decompose(Normal(T), primitive) + function decompose(::Normal{T}, primitive) where {T} _n = normals(primitive) if isnothing(_n) From 73093c5ff5bce1dd7467dba92704ffa23faf030a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 2 Sep 2024 18:51:14 +0200 Subject: [PATCH 35/58] disable diff test --- test/geometrytypes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 7fed2ffe..ec68a151 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -224,7 +224,7 @@ end h1 = Rect(0.0, 0.0, 1.0, 1.0) h2 = Rect(1.0, 1.0, 2.0, 2.0) @test union(h1, h2) isa GeometryBasics.HyperRectangle{2,Float64} - @test GeometryBasics.diff(h1, h2) == h1 + # @test GeometryBasics.diff(h1, h2) == h1 @test GeometryBasics.intersect(h1, h2) isa GeometryBasics.HyperRectangle{2,Float64} b = Rect(0.0, 0.0, 1.0, 1.0) From 5d2a1341ae0cb037e5202dffdda393f05d22a648 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 4 Sep 2024 17:40:03 +0200 Subject: [PATCH 36/58] fix MatN constructors --- src/mat.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/mat.jl b/src/mat.jl index b2730a77..217bc23d 100644 --- a/src/mat.jl +++ b/src/mat.jl @@ -48,6 +48,10 @@ Mat{C, R, T}(x::Tuple) where {C, R, T} = Mat{C, R, T}(convert(NTuple{length(x), Mat{C, R}(x::NTuple{L, T}) where {C, R, L, T} = Mat{C, R, T}(x) Mat{C, R, T1}(x::NTuple{L, T2}) where {C, R, L, T1, T2} = Mat{C, R, T1}(convert(NTuple{L, T1}, x)) +# catch Mat2(...) etc which lowers to Mat{2, 2, T, 4} where T +(::Type{MT})(args...) where {C, R, MT <: Mat{C, R}} = Mat{C, R}(args) +(::Type{MT})(args::Tuple) where {C, R, MT <: Mat{C, R}} = Mat{C, R}(args) + Mat{C, R}(x::AbstractMatrix{T}) where {C, R, T} = Mat{C, R, T}(x) Mat{C, R, T}(x::AbstractMatrix) where {C, R, T} = Mat{C, R, T}(ntuple(i-> convert(T, x[i]), C*R)) From 406397ff718eec3ecaf76c9f1200cb0e850d570b Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 4 Sep 2024 18:10:02 +0200 Subject: [PATCH 37/58] fix type ambiguity --- src/fixed_arrays.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index 269c0b80..899eea23 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -50,7 +50,7 @@ macro fixed_vector(name_parent) return $(VecT){N,T}(ntuple(i -> convert(T, x), N)) end - $(VecT){1, T}(x) where {T} = $(VecT){1, T}((x,)) + $(VecT){1, T}(x::Number) where {T} = $(VecT){1, T}((x,)) $(VecT){1, T}(x::Tuple{Any}) where T = $(VecT){1, T}((T(x[1]),)) $(VecT)(x::Tuple) = $(VecT)(promote(x...)) $(VecT){N}(x::Tuple) where {N} = $(VecT){N}(promote(x...)) From ede77d7c411bbb1b683bfc6160e2733f1e8cdb9b Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 4 Sep 2024 18:57:39 +0200 Subject: [PATCH 38/58] fix vcat of StaticVector, Mat convert --- src/fixed_arrays.jl | 4 ++++ src/mat.jl | 1 + 2 files changed, 5 insertions(+) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index 899eea23..daadd84c 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -100,6 +100,8 @@ macro fixed_vector(name_parent) return esc(expr) end +# TODO: `ifelse.(vec, vec, vec)` broadcasts to Vector, not Vec +# e.g. ifelse.(Vec2(true), Vec2(0), Vec2(1)) Base.broadcasted(f, a::StaticVector) = similar_type(a)(f.(a.data)) Base.broadcasted(::typeof(+), a::StaticVector, b::Base.OneTo{Int64}) = similar_type(a)((a.data .+ b)) Base.broadcasted(f, a::StaticVector, b) = similar_type(a)(f.(a.data, b)) @@ -150,6 +152,8 @@ Base.length(::StaticVector{N}) where {N} = N Base.length(::Type{<: StaticVector{N}}) where {N} = N Base.ndims(::Type{<: StaticVector}) = 1 +Base.vcat(a::StaticVector, b::StaticVector) = (a..., b...) + function Base.iterate(A::StaticVector, i=1) i - 1 < length(A) ? (A[i], i + 1) : nothing end diff --git a/src/mat.jl b/src/mat.jl index 217bc23d..1374994e 100644 --- a/src/mat.jl +++ b/src/mat.jl @@ -41,6 +41,7 @@ end Mat{C, R, T}(args...) where {C, R, T} = Mat{C, R, T}(args) Mat{C, R, T, L}(args...) where {C, R, T, L} = Mat{C, R, T}(args) +Mat{C, R, T, L}(mat::Mat{C, R}) where {C, R, T, L} = Mat{C, R, T}(mat.values) Mat{C}(args...) where {C} = Mat{C, C}(args) Mat{C}(arg) where {C} = Mat{C, C}(arg) Mat{C, R}(x::Tuple) where {C, R} = Mat{C, R}(promote(x...)) From 54d9e52ba6b6edd42f0045384e9fe675e0443cd6 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 4 Sep 2024 20:48:36 +0200 Subject: [PATCH 39/58] add mat[VecOrInt, VecOrInt] and Vec(mat) --- src/mat.jl | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/mat.jl b/src/mat.jl index 1374994e..06631c54 100644 --- a/src/mat.jl +++ b/src/mat.jl @@ -18,6 +18,31 @@ Base.size(::Type{<: Mat{R, C}}) where {R, C} = (R, C) Base.ndims(::Type{<: Mat}) = 2 Base.getindex(mat::Mat{R, C}, i) where {R, C} = mat.values[i] +# TODO: maybe ranges as well? +function Base.getindex(mat::Mat{R, C}, i::StaticVector{N}, j::Integer) where {R, C, N} + @boundscheck begin + all(x -> 1 ≤ x ≤ R, i) && (1 ≤ j ≤ C) + end + @inbounds data = ntuple(n -> mat.values[i[n] + R * (j-1)], N) + return Mat{length(i), 1}(data) +end +function Base.getindex(mat::Mat{R, C}, i::Integer, j::StaticVector{N}) where {R, C, N} + @boundscheck begin + (1 ≤ i ≤ R) && all(x -> 1 ≤ x ≤ C, j) + end + @inbounds data = ntuple(n -> mat.values[i + R * (j[n] - 1)], N) + return Mat{1, length(j)}(data) +end +function Base.getindex(mat::Mat{R, C}, i::StaticVector{N}, j::StaticVector{M}) where {R, C, N, M} + @boundscheck begin + all(x -> 1 ≤ x ≤ R, i) && all(x -> 1 ≤ x ≤ C, j) + end + data = ntuple(N * M) do nm + m, n = fldmod1(nm, R) + @inbounds return mat.values[i[n] + R * (j[m] - 1)] + end + return Mat{1, length(j)}(data) +end Base.IndexStyle(::Mat)= Base.IndexLinear() @@ -182,6 +207,7 @@ end return :(similar_type(b)($elements)) end +# TODO: delete since we have a more specialized version up top? function Base.getindex(mat::Mat{R, C, T}, r::Vec{NR}, c::Vec{NC}) where {R, C, NR, NC, T} idx = CartesianIndices((NR, NC)) data = ntuple(NR * NC) do i @@ -190,3 +216,6 @@ function Base.getindex(mat::Mat{R, C, T}, r::Vec{NR}, c::Vec{NC}) where {R, C, N end return Mat{NR, NC, T}(data) end + +# TODO: Fix Vec(mat) becoming Vec((mat,)) (i.e. restrict eltype to Number?) +(VT::Type{<: StaticVector{N}})(mat::Mat{N, 1}) where {N} = VT(mat.values) \ No newline at end of file From e4201217cd225c08656fb0a6b45b6c9ad8c03da1 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 5 Sep 2024 00:24:25 +0200 Subject: [PATCH 40/58] match isapprox implementation with docstring --- src/fixed_arrays.jl | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index daadd84c..edd0e09e 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -175,11 +175,12 @@ function Base.:(==)(a::StaticVector{N}, b::StaticVector{N}) where N return true end -function Base.isapprox(a::StaticVector{N}, b::StaticVector{N}; kw...) where N - for i in 1:N - isapprox(a[i], b[i]; kw...) || return false - end - return true +function Base.isapprox( + a::StaticVector{N1, T1}, b::StaticVector{N2, T2}; + atol::Real = 0, + rtol::Real = atol > 0 ? 0 : sqrt(max(eps(T1), eps(T2))) + ) where {N1, N2, T1, T2} + return (N1 == N2) && norm(a - b) <= max(atol, rtol * max(norm(a), norm(b))) end @generated function Base.transpose(b::StaticVector{N,T}) where {N,T} From 58a4d831cf82aca2536058e76cca610494cf3ae5 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 5 Sep 2024 00:24:36 +0200 Subject: [PATCH 41/58] add isapprox to Mat --- src/mat.jl | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/mat.jl b/src/mat.jl index 06631c54..231aca99 100644 --- a/src/mat.jl +++ b/src/mat.jl @@ -87,6 +87,9 @@ Base.convert(::Type{Mat{C, R, T, L}}, from::Mat{C, R}) where {C, R, T, L} = Mat{ # General shape mismatched versions are errors (*)(a::Mat{M, N, T1}, b::Mat{O, K, T2}) where {T1, T2, M, N, O, K} = throw(DimensionMismatch("$N != $O in $(typeof(a)) and $(typeof(b))")) +Base.:(-)(a::Mat{R, C}, b::Mat{R, C}) where {R, C} = Mat{R, C}(a.values .- b.values) +Base.:(+)(a::Mat{R, C}, b::Mat{R, C}) where {R, C} = Mat{R, C}(a.values .+ b.values) + # matrix * matrix @generated function *(a::Mat{M, N, T1}, b::Mat{N, P, T2}) where {T1, T2, M, N, P} elements = Expr(:tuple) @@ -218,4 +221,13 @@ function Base.getindex(mat::Mat{R, C, T}, r::Vec{NR}, c::Vec{NC}) where {R, C, N end # TODO: Fix Vec(mat) becoming Vec((mat,)) (i.e. restrict eltype to Number?) -(VT::Type{<: StaticVector{N}})(mat::Mat{N, 1}) where {N} = VT(mat.values) \ No newline at end of file +(VT::Type{<: StaticVector{N}})(mat::Mat{N, 1}) where {N} = VT(mat.values) + +function Base.isapprox( + a::Mat{R1, C1, T1}, b::Mat{R2, C2, T2}; + atol::Real = 0, + rtol::Real = atol > 0 ? 0 : sqrt(max(eps(T1), eps(T2))) + ) where {R1, R2, C1, C2, T1, T2} + return (R1 == R2) && (C1 == C2) && + norm(a - b) <= max(atol, rtol * max(norm(a), norm(b))) +end \ No newline at end of file From 8562c1d767c43d603323f88b6dbaf6856135e3c4 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 5 Sep 2024 01:14:09 +0200 Subject: [PATCH 42/58] fix indexing instead of implementing isapprox --- src/mat.jl | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/src/mat.jl b/src/mat.jl index 231aca99..dba3f561 100644 --- a/src/mat.jl +++ b/src/mat.jl @@ -17,6 +17,7 @@ Base.size(::Mat{R, C}) where {R, C} = (R, C) Base.size(::Type{<: Mat{R, C}}) where {R, C} = (R, C) Base.ndims(::Type{<: Mat}) = 2 Base.getindex(mat::Mat{R, C}, i) where {R, C} = mat.values[i] +Base.getindex(mat::Mat{R, C}, i::CartesianIndex) where {R, C} = mat.values[i.I[1] + R * (i.I[2] - 1)] # TODO: maybe ranges as well? function Base.getindex(mat::Mat{R, C}, i::StaticVector{N}, j::Integer) where {R, C, N} @@ -44,7 +45,7 @@ function Base.getindex(mat::Mat{R, C}, i::StaticVector{N}, j::StaticVector{M}) w return Mat{1, length(j)}(data) end -Base.IndexStyle(::Mat)= Base.IndexLinear() +Base.IndexStyle(::Type{<: Mat}) = Base.IndexLinear() function Mat{C, R, T}(::LinearAlgebra.UniformScaling) where {C, R, T} idx = CartesianIndices((R, C)) @@ -210,24 +211,5 @@ end return :(similar_type(b)($elements)) end -# TODO: delete since we have a more specialized version up top? -function Base.getindex(mat::Mat{R, C, T}, r::Vec{NR}, c::Vec{NC}) where {R, C, NR, NC, T} - idx = CartesianIndices((NR, NC)) - data = ntuple(NR * NC) do i - ri, ci = Tuple(idx[i]) - return mat[r[ri], c[ci]] - end - return Mat{NR, NC, T}(data) -end - # TODO: Fix Vec(mat) becoming Vec((mat,)) (i.e. restrict eltype to Number?) -(VT::Type{<: StaticVector{N}})(mat::Mat{N, 1}) where {N} = VT(mat.values) - -function Base.isapprox( - a::Mat{R1, C1, T1}, b::Mat{R2, C2, T2}; - atol::Real = 0, - rtol::Real = atol > 0 ? 0 : sqrt(max(eps(T1), eps(T2))) - ) where {R1, R2, C1, C2, T1, T2} - return (R1 == R2) && (C1 == C2) && - norm(a - b) <= max(atol, rtol * max(norm(a), norm(b))) -end \ No newline at end of file +(VT::Type{<: StaticVector{N}})(mat::Mat{N, 1}) where {N} = VT(mat.values) \ No newline at end of file From 556df6fab4f0abe26e08dcb36e629de39f11d119 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 5 Sep 2024 15:14:41 +0200 Subject: [PATCH 43/58] rework broadcasting & add more tests --- src/fixed_arrays.jl | 69 +++++++++++++++++++++++++++++-------- src/mat.jl | 5 +++ test/fixed_arrays.jl | 81 +++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 135 insertions(+), 20 deletions(-) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index edd0e09e..dbad2fdc 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -6,6 +6,9 @@ import Base: setindex abstract type StaticVector{N, T} end function similar_type end +struct StaticArrayStyle{T, AsConst} <: Broadcast.BroadcastStyle end +StaticArrayStyle{T}() where T = StaticArrayStyle{T, false}() + macro fixed_vector(name_parent) @assert name_parent.head == :(=) VecT, SuperT = name_parent.args @@ -87,9 +90,9 @@ macro fixed_vector(name_parent) @inline similar_type(::$(VecT), n::Integer, ::Type{T}) where {T} = $(VecT){n, T} @inline similar_type(::$(VecT)) = $(VecT) - function Base.broadcasted(f, a::$(VecT), b::$(VecT)) - return $(VecT)(map(f, a.data, b.data)) - end + Base.BroadcastStyle(::Type{<: $(VecT)}) = StaticArrayStyle{$(VecT)}() + Base.values(v::$(VecT)) = v.data + function LinearAlgebra.cross(a::$(VecT){3}, b::$(VecT){3}) @inbounds elements = (a[2]*b[3]-a[3]*b[2], a[3]*b[1]-a[1]*b[3], @@ -100,13 +103,47 @@ macro fixed_vector(name_parent) return esc(expr) end -# TODO: `ifelse.(vec, vec, vec)` broadcasts to Vector, not Vec -# e.g. ifelse.(Vec2(true), Vec2(0), Vec2(1)) -Base.broadcasted(f, a::StaticVector) = similar_type(a)(f.(a.data)) -Base.broadcasted(::typeof(+), a::StaticVector, b::Base.OneTo{Int64}) = similar_type(a)((a.data .+ b)) -Base.broadcasted(f, a::StaticVector, b) = similar_type(a)(f.(a.data, b)) -Base.broadcasted(f, a, b::StaticVector{N}) where N = similar_type(b, N)(f.(a, b.data)) -Base.broadcasted(f, a::AbstractArray, b::StaticVector{N}) where N = f.(a, (b,)) +# Broadcasting +# style rules +Base.BroadcastStyle(::StaticArrayStyle{T, B1}, ::StaticArrayStyle{T, B2}) where {B1, B2, T} = StaticArrayStyle{T, B1 || B2}() +Base.BroadcastStyle(s::StaticArrayStyle, ::Broadcast.AbstractArrayStyle{0}) = s +Base.BroadcastStyle(s::StaticArrayStyle, ::Broadcast.Style{Tuple}) = s +Base.BroadcastStyle(::StaticArrayStyle{T, B}, ::Broadcast.AbstractArrayStyle) where {B, T} = StaticArrayStyle{T, true}() +# to allow mixing types, define: +# Base.BroadcastStyle(::StaticArrayStyle{<: Type1, B1}, ::StaticArrayStyle{<: Type2, B2}) where {B1, B2} = +# StaticArrayStyle{preffered_type, B1 || B2}() + +# If we don't inherit from AbstractVector we need this? +Base.broadcastable(x::StaticVector) = x + +# Resolve a .+ b .+ c +function Base.broadcasted(f, arg, bc::Broadcast.Broadcasted{<: StaticArrayStyle}) + return Base.broadcasted(f, arg, copy(bc)) +end +function Base.broadcasted(f, bc::Broadcast.Broadcasted{<: StaticArrayStyle}, arg) + return Base.broadcasted(f, copy(bc), arg) +end +function Base.broadcasted(f, bc1::Broadcast.Broadcasted{<: StaticArrayStyle}, bc2::Broadcast.Broadcasted{<: StaticArrayStyle}) + return Base.broadcasted(f, copy(bc1), copy(bc2)) +end + +# resolve element-wise operation +function Base.copy(bc::Broadcast.Broadcasted{StaticArrayStyle{T, false}}) where T + return T(broadcast(bc.f, values.(bc.args)...)) +end + +# resolve StaticVector-as-const (i.e. with base array) +function Base.copy(bc::Broadcast.Broadcasted{StaticArrayStyle{T, true}}) where T + args = map(bc.args) do arg + style = Base.BroadcastStyle(typeof(arg)) + if style isa Broadcast.AbstractArrayStyle # value or Array + return arg + else # tuple, StaticVector + return Ref(arg) + end + end + return broadcast(bc.f, args...) +end Base.map(f, b::StaticVector) = similar_type(b)(map(f, b.data)) @@ -133,9 +170,9 @@ import Base: *, +, -, / for op in [:*, :+, :-, :/] @eval begin - ($op)(a::StaticVector, b::StaticVector) = Base.broadcasted($(op), a, b) - ($op)(a::Number, b::StaticVector) = Base.broadcasted($(op), a, b) - ($op)(a::StaticVector, b::Number) = Base.broadcasted($(op), a, b) + ($op)(a::StaticVector, b::StaticVector) = Base.broadcast($(op), a, b) + ($op)(a::Number, b::StaticVector) = Base.broadcast($(op), a, b) + ($op)(a::StaticVector, b::Number) = Base.broadcast($(op), a, b) end end @@ -200,8 +237,10 @@ LinearAlgebra.promote_leaf_eltypes(x::StaticVector{N, T}) where {N,T} = T Base.lastindex(::StaticVector{N}) where N = N -Base.broadcasted(f, a::Point, b::Vec) = Vec(f.(a.data, b.data)) -Base.broadcasted(f, a::Vec, b::Point) = Vec(f.(a.data, b.data)) +# Allow mixing Point Vec in broadcast +Base.BroadcastStyle(::StaticArrayStyle{<: Point, B1}, ::StaticArrayStyle{<: Vec, B2}) where {B1, B2} = + StaticArrayStyle{Point, B1 || B2}() + Base.:(+)(a::Vec{N}, b::Point{N}) where {N} = Point{N}(a.data .+ b.data) const VecTypes{N,T} = Union{StaticVector{N,T}, NTuple{N,T}} diff --git a/src/mat.jl b/src/mat.jl index dba3f561..dd20aad2 100644 --- a/src/mat.jl +++ b/src/mat.jl @@ -47,6 +47,11 @@ end Base.IndexStyle(::Type{<: Mat}) = Base.IndexLinear() +# Broadcasting +Base.broadcastable(x::Mat) = x +Base.BroadcastStyle(::Type{<: Mat{C, R}}) where {C, R} = StaticArrayStyle{Mat{C, R}}() +Base.values(m::Mat) = m.values + function Mat{C, R, T}(::LinearAlgebra.UniformScaling) where {C, R, T} idx = CartesianIndices((R, C)) data = ntuple(C * R) do i diff --git a/test/fixed_arrays.jl b/test/fixed_arrays.jl index 061f556c..33521925 100644 --- a/test/fixed_arrays.jl +++ b/test/fixed_arrays.jl @@ -6,11 +6,82 @@ using Test end @testset "broadcast" begin - @testset for T in (Vec, Point) - x = [T(2, 3), T(7, 3)] - @test [T(4, 9), T(14, 9)] == x .* T(2, 3) - @test [T(4, 6), T(9, 6)] == x .+ T(2, 3) - @test [T(0, 0), T(5, 0)] == x .- T(2, 3) + foo(args...) = +(args...) * -(args...) + M1 = Mat{2, 2}(1,2,3,4) + M2 = Mat{2, 2}(2,2,1,1) + + @testset "with similar" begin + for T1 in (Vec, Point, tuple) + for T2 in (Vec, Point, tuple) + T1 == tuple && T2 == tuple && continue + T = ifelse(T1 == Point, Point, ifelse(T2 == Point, Point, Vec)) + @test T(2, 2, 4) == T1(1,2,3) .+ T2(1, 0, 1) + @test T(foo.((1,2,3), (1, 0, 1))) == foo.(T1(1,2,3), T2(1, 0, 1)) + end + end + + @test Mat{2, 2}(3,4,4,5) == M1 .+ M2 + @test Mat{2, 2}(foo.(values(M1), values(M2))) == foo.(M1, M2) + end + + @testset "with const" begin + for T in (Vec, Point) + @test T(-4, -3) == T(1,2) .- 5 + @test T(foo.((1,2), 5)) == foo.(T(1,2), 5) + end + + @test Mat{2, 2}(2,3,4,5) == M1 .+ 1 + @test Mat{2, 2}(foo.(values(M1), 1)) == foo.(M1, 1) + end + + # TODO: Should this work with tuple somehow? + @testset "with Vector (acting as const)" begin + for T1 in (Vec, Point) + x = [T1(2, 3), T1(7, 3)] + for T2 in (Vec, Point) + T = ifelse(T1 == Point, Point, ifelse(T2 == Point, Point, Vec)) + @test [T(4, 9), T(14, 9)] == x .* T2(2, 3) + @test [T(foo(v, T2(3, -1))) for v in x] == foo.(x, T2(3, -1)) + end + end + + x = [M1, M2] + @test [M1 + M2, M2 + M2] == x .+ M2 + @test [foo(M1, M2), foo(M2, M2)] == foo.(x, M2) + + # maybe bad...? + @test [Vec(2, 3), Vec(3, 4)] == [1, 2] .+ Vec(1, 2) + end + + @testset "chained" begin + for T1 in (Vec, Point, tuple) + for T2 in (Vec, Point, tuple) + T1 == tuple && T2 == tuple && continue + T = ifelse(T1 == Point, Point, ifelse(T2 == Point, Point, Vec)) + @test T(-6, -4, 4) == T1(1,2,3) .+ T2(1, 0, 1) .- T2(3, 2, 1) .* T1(2,2,0) .- T2(2,2,0) + end + end + + @test [Point(1, 3), Point(3, 1)] == [Vec(1,2), Vec(2, 1)] .+ [Vec(1,2), Vec(2, 1)] .+ Point(-1, -1) + @test [Vec(1, 3), Point(3, 1)] == [Vec(1,2), Vec(2, 1)] .+ Vec(-1, -1) .+ [Vec(1,2), Point(2, 1)] + + x = [M1, M2] + @test [M1 * M1 + M2, M1 * M2 + M2] == M1 .* x .+ M2 + @test [M1 + M2 * M1, M2 + M2 * M2] == x .+ M2 .* x + end + + @testset "Longer functions" begin + foo2(a, b, c, d) = (a + b) * (c + d) + for T1 in (Vec, Point, tuple) + for T2 in (Vec, Point, tuple) + T1 == tuple && T2 == tuple && continue + T = ifelse(T1 == Point, Point, ifelse(T2 == Point, Point, Vec)) + @test T(foo2.((1,2,3), (1, 0, 1), (3, 2, 1), (2,2,0))) == + foo2.(T1(1,2,3), T2(1, 0, 1), T2(3, 2, 1), T2(2,2,0)) + end + end + + @test Mat{2, 2}(foo2.(values.((M1, M1, M2, M2))...)) == foo2.(M1, M1, M2, M2) end end From 420789789991b9ddb043fec34a95333121a61aaf Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 5 Sep 2024 16:05:11 +0200 Subject: [PATCH 44/58] fix nested broadcast --- src/fixed_arrays.jl | 20 ++++++-------------- test/fixed_arrays.jl | 7 +++++-- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index dbad2fdc..ebbd25b0 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -116,25 +116,17 @@ Base.BroadcastStyle(::StaticArrayStyle{T, B}, ::Broadcast.AbstractArrayStyle) wh # If we don't inherit from AbstractVector we need this? Base.broadcastable(x::StaticVector) = x -# Resolve a .+ b .+ c -function Base.broadcasted(f, arg, bc::Broadcast.Broadcasted{<: StaticArrayStyle}) - return Base.broadcasted(f, arg, copy(bc)) -end -function Base.broadcasted(f, bc::Broadcast.Broadcasted{<: StaticArrayStyle}, arg) - return Base.broadcasted(f, copy(bc), arg) -end -function Base.broadcasted(f, bc1::Broadcast.Broadcasted{<: StaticArrayStyle}, bc2::Broadcast.Broadcasted{<: StaticArrayStyle}) - return Base.broadcasted(f, copy(bc1), copy(bc2)) -end - # resolve element-wise operation function Base.copy(bc::Broadcast.Broadcasted{StaticArrayStyle{T, false}}) where T - return T(broadcast(bc.f, values.(bc.args)...)) + # Broadcasted may end up in args from nested calls (e.g. foo(a, b .+ c); a .+ b .+ c) + args = map(arg -> values(arg isa Broadcast.Broadcasted ? copy(arg) : arg), bc.args) + return T(broadcast(bc.f, args...)) end # resolve StaticVector-as-const (i.e. with base array) function Base.copy(bc::Broadcast.Broadcasted{StaticArrayStyle{T, true}}) where T - args = map(bc.args) do arg + args_converted = map(arg -> arg isa Broadcast.Broadcasted ? copy(arg) : arg, bc.args) + maybe_const_args = map(args_converted) do arg style = Base.BroadcastStyle(typeof(arg)) if style isa Broadcast.AbstractArrayStyle # value or Array return arg @@ -142,7 +134,7 @@ function Base.copy(bc::Broadcast.Broadcasted{StaticArrayStyle{T, true}}) where T return Ref(arg) end end - return broadcast(bc.f, args...) + return broadcast(bc.f, maybe_const_args...) end Base.map(f, b::StaticVector) = similar_type(b)(map(f, b.data)) diff --git a/test/fixed_arrays.jl b/test/fixed_arrays.jl index 33521925..82f83de7 100644 --- a/test/fixed_arrays.jl +++ b/test/fixed_arrays.jl @@ -6,7 +6,7 @@ using Test end @testset "broadcast" begin - foo(args...) = +(args...) * -(args...) + foo(a, b) = (a + b) * (a - b) M1 = Mat{2, 2}(1,2,3,4) M2 = Mat{2, 2}(2,2,1,1) @@ -53,21 +53,24 @@ end @test [Vec(2, 3), Vec(3, 4)] == [1, 2] .+ Vec(1, 2) end - @testset "chained" begin + @testset "chained/nested" begin for T1 in (Vec, Point, tuple) for T2 in (Vec, Point, tuple) T1 == tuple && T2 == tuple && continue T = ifelse(T1 == Point, Point, ifelse(T2 == Point, Point, Vec)) @test T(-6, -4, 4) == T1(1,2,3) .+ T2(1, 0, 1) .- T2(3, 2, 1) .* T1(2,2,0) .- T2(2,2,0) + @test T(-15, -5) == foo.(T1(1,2), T1(-1, 0) .+ foo.(T1(1,1), T2(2,2))) end end @test [Point(1, 3), Point(3, 1)] == [Vec(1,2), Vec(2, 1)] .+ [Vec(1,2), Vec(2, 1)] .+ Point(-1, -1) @test [Vec(1, 3), Point(3, 1)] == [Vec(1,2), Vec(2, 1)] .+ Vec(-1, -1) .+ [Vec(1,2), Point(2, 1)] + @test [Vec(-1, -3), Point(-3, -1)] == foo.([Vec(1,2), Vec(2, 1)] .+ Vec(-1, -1), [Vec(1,2), Point(2, 1)]) x = [M1, M2] @test [M1 * M1 + M2, M1 * M2 + M2] == M1 .* x .+ M2 @test [M1 + M2 * M1, M2 + M2 * M2] == x .+ M2 .* x + @test [foo(M1+M2, M1), foo(M2+M2, M2)] == foo.(x .+ M2, x) end @testset "Longer functions" begin From 1e0842351c50e5afd4d368efb827483bd72f0fb8 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 5 Sep 2024 17:40:46 +0200 Subject: [PATCH 45/58] fix Mat getindex, Boundschecks and add tests --- src/mat.jl | 35 +++++++++++++++++++---------------- test/fixed_arrays.jl | 27 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/mat.jl b/src/mat.jl index dd20aad2..378e77cc 100644 --- a/src/mat.jl +++ b/src/mat.jl @@ -4,11 +4,11 @@ import Random struct Mat{Row, Column, T, L} <: AbstractMatrix{T} values::NTuple{L, T} function Mat{R, C, T}(values::NTuple{L, T}) where {R, C, T, L} - @assert L == R * C "$R * $C needs to be $L" + @assert L == R * C "Number of rows $R * number of columns $C needs to match the number of values $L." return new{R, C, T, L}(values) end function Mat{R, C, T, L}(values::NTuple{L, T}) where {R, C, T, L} - @assert L == R * C "$R * $C needs to be $L" + @assert L == R * C "Number of rows $R * number of columns $C needs to match the number of values $L." return new{R, C, T, L}(values) end end @@ -16,33 +16,36 @@ end Base.size(::Mat{R, C}) where {R, C} = (R, C) Base.size(::Type{<: Mat{R, C}}) where {R, C} = (R, C) Base.ndims(::Type{<: Mat}) = 2 -Base.getindex(mat::Mat{R, C}, i) where {R, C} = mat.values[i] -Base.getindex(mat::Mat{R, C}, i::CartesianIndex) where {R, C} = mat.values[i.I[1] + R * (i.I[2] - 1)] +Base.@propagate_inbounds Base.getindex(mat::Mat{R, C}, i) where {R, C} = mat.values[i] +Base.@propagate_inbounds Base.getindex(mat::Mat{R, C}, i::CartesianIndex) where {R, C} = mat.values[i.I[1] + R * (i.I[2] - 1)] # TODO: maybe ranges as well? -function Base.getindex(mat::Mat{R, C}, i::StaticVector{N}, j::Integer) where {R, C, N} +Base.@propagate_inbounds function Base.getindex(mat::Mat{R, C}, i::StaticVector{N}, j::Integer) where {R, C, N} @boundscheck begin - all(x -> 1 ≤ x ≤ R, i) && (1 ≤ j ≤ C) + inbounds = all(x -> 1 ≤ x ≤ R, i) && (1 ≤ j ≤ C) + inbounds || throw(BoundsError(mat, (i, j))) end @inbounds data = ntuple(n -> mat.values[i[n] + R * (j-1)], N) - return Mat{length(i), 1}(data) + return Mat{N, 1}(data) end -function Base.getindex(mat::Mat{R, C}, i::Integer, j::StaticVector{N}) where {R, C, N} +Base.@propagate_inbounds function Base.getindex(mat::Mat{R, C}, i::Integer, j::StaticVector{N}) where {R, C, N} @boundscheck begin - (1 ≤ i ≤ R) && all(x -> 1 ≤ x ≤ C, j) + inbounds = (1 ≤ i ≤ R) && all(x -> 1 ≤ x ≤ C, j) + inbounds || throw(BoundsError(mat, (i, j))) end @inbounds data = ntuple(n -> mat.values[i + R * (j[n] - 1)], N) - return Mat{1, length(j)}(data) + return Mat{1, N}(data) end -function Base.getindex(mat::Mat{R, C}, i::StaticVector{N}, j::StaticVector{M}) where {R, C, N, M} +Base.@propagate_inbounds function Base.getindex(mat::Mat{R, C}, i::StaticVector{N}, j::StaticVector{M}) where {R, C, N, M} @boundscheck begin - all(x -> 1 ≤ x ≤ R, i) && all(x -> 1 ≤ x ≤ C, j) + inbounds = all(x -> 1 ≤ x ≤ R, i) && all(x -> 1 ≤ x ≤ C, j) + inbounds || throw(BoundsError(mat, (i, j))) end - data = ntuple(N * M) do nm - m, n = fldmod1(nm, R) - @inbounds return mat.values[i[n] + R * (j[m] - 1)] + @inbounds data = ntuple(N * M) do nm + m, n = fldmod1(nm, N) + return mat.values[i[n] + R * (j[m] - 1)] end - return Mat{1, length(j)}(data) + return Mat{N, M}(data) end Base.IndexStyle(::Type{<: Mat}) = Base.IndexLinear() diff --git a/test/fixed_arrays.jl b/test/fixed_arrays.jl index 82f83de7..ced6bc22 100644 --- a/test/fixed_arrays.jl +++ b/test/fixed_arrays.jl @@ -104,3 +104,30 @@ end end end end + +@testset "Mat" begin + M3 = Mat3(1,2,3, 4,5,6, 7,8,9) + @test M3 isa Mat{3,3,Int,9} + + @testset "indexing" begin + for i in 1:9 + @test getindex(M3, i) == i + end + @test_throws BoundsError getindex(M3, 0) + @test_throws BoundsError getindex(M3, 10) + + # Sanity check for loop + @test M3[2, Vec(1,2)] == Mat{1, 2}(M3[2,1], M3[2,2]) + + for x in (2, Vec(1,2), Vec(1,1,2,2)) + for y in (2, Vec(1,2), Vec(1,1,2,2)) + x isa Real && y isa Real && continue + @test M3[x, y] == Mat{length(x), length(y)}((M3[i, j] for j in y for i in x)...) + @test_throws BoundsError M3[x .- 2, y] + @test_throws BoundsError M3[x, y .+ 2] + @test_throws BoundsError M3[x .+ 2, y .- 2] + end + end + + end +end \ No newline at end of file From 454cd4e0502b1aeaa4d2880028e3231948df45e2 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 5 Sep 2024 17:41:07 +0200 Subject: [PATCH 46/58] fix broadcast size missmatch --- src/fixed_arrays.jl | 32 ++++++++++++++++++++------------ test/fixed_arrays.jl | 12 ++++++------ 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index ebbd25b0..56735b28 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -107,25 +107,24 @@ end # style rules Base.BroadcastStyle(::StaticArrayStyle{T, B1}, ::StaticArrayStyle{T, B2}) where {B1, B2, T} = StaticArrayStyle{T, B1 || B2}() Base.BroadcastStyle(s::StaticArrayStyle, ::Broadcast.AbstractArrayStyle{0}) = s +Base.BroadcastStyle(::Broadcast.AbstractArrayStyle{0}, s::StaticArrayStyle) = s Base.BroadcastStyle(s::StaticArrayStyle, ::Broadcast.Style{Tuple}) = s -Base.BroadcastStyle(::StaticArrayStyle{T, B}, ::Broadcast.AbstractArrayStyle) where {B, T} = StaticArrayStyle{T, true}() +Base.BroadcastStyle(::Broadcast.Style{Tuple}, s::StaticArrayStyle) = s +Base.BroadcastStyle(::StaticArrayStyle{T, B}, ::Broadcast.BroadcastStyle) where {B, T} = StaticArrayStyle{T, true}() +Base.BroadcastStyle(::Broadcast.BroadcastStyle, ::StaticArrayStyle{T, B}) where {B, T} = StaticArrayStyle{T, true}() # to allow mixing types, define: # Base.BroadcastStyle(::StaticArrayStyle{<: Type1, B1}, ::StaticArrayStyle{<: Type2, B2}) where {B1, B2} = # StaticArrayStyle{preffered_type, B1 || B2}() +# Base.BroadcastStyle(::StaticArrayStyle{<: Type2, B1}, ::StaticArrayStyle{<: Type1, B2}) where {B1, B2} = +# StaticArrayStyle{preffered_type, B1 || B2}() # If we don't inherit from AbstractVector we need this? Base.broadcastable(x::StaticVector) = x -# resolve element-wise operation -function Base.copy(bc::Broadcast.Broadcasted{StaticArrayStyle{T, false}}) where T - # Broadcasted may end up in args from nested calls (e.g. foo(a, b .+ c); a .+ b .+ c) - args = map(arg -> values(arg isa Broadcast.Broadcasted ? copy(arg) : arg), bc.args) - return T(broadcast(bc.f, args...)) -end - -# resolve StaticVector-as-const (i.e. with base array) -function Base.copy(bc::Broadcast.Broadcasted{StaticArrayStyle{T, true}}) where T - args_converted = map(arg -> arg isa Broadcast.Broadcasted ? copy(arg) : arg, bc.args) +# Required to avoid size missmatches between Array and StaticVector +function Broadcast.instantiate(bc::Broadcast.Broadcasted{<: StaticArrayStyle{<: Any, true}}) + # transform this to an Array broadcast with Ref'd StaticVectors and tuples + args_converted = map(arg -> arg isa Broadcast.Broadcasted ? copy(Broadcast.instantiate(arg)) : arg, bc.args) maybe_const_args = map(args_converted) do arg style = Base.BroadcastStyle(typeof(arg)) if style isa Broadcast.AbstractArrayStyle # value or Array @@ -134,7 +133,14 @@ function Base.copy(bc::Broadcast.Broadcasted{StaticArrayStyle{T, true}}) where T return Ref(arg) end end - return broadcast(bc.f, maybe_const_args...) + return Broadcast.broadcasted(bc.f, maybe_const_args...) +end + +# resolve element-wise operation +function Base.copy(bc::Broadcast.Broadcasted{StaticArrayStyle{T, false}}) where T + # Broadcasted may end up in args from nested calls (e.g. foo(a, b .+ c); a .+ b .+ c) + args = map(arg -> values(arg isa Broadcast.Broadcasted ? copy(arg) : arg), bc.args) + return T(broadcast(bc.f, args...)) end Base.map(f, b::StaticVector) = similar_type(b)(map(f, b.data)) @@ -232,6 +238,8 @@ Base.lastindex(::StaticVector{N}) where N = N # Allow mixing Point Vec in broadcast Base.BroadcastStyle(::StaticArrayStyle{<: Point, B1}, ::StaticArrayStyle{<: Vec, B2}) where {B1, B2} = StaticArrayStyle{Point, B1 || B2}() +Base.BroadcastStyle(::StaticArrayStyle{<: Vec, B1}, ::StaticArrayStyle{<: Point, B2}) where {B1, B2} = + StaticArrayStyle{Point, B1 || B2}() Base.:(+)(a::Vec{N}, b::Point{N}) where {N} = Point{N}(a.data .+ b.data) diff --git a/test/fixed_arrays.jl b/test/fixed_arrays.jl index ced6bc22..cf82d0a3 100644 --- a/test/fixed_arrays.jl +++ b/test/fixed_arrays.jl @@ -37,17 +37,17 @@ end # TODO: Should this work with tuple somehow? @testset "with Vector (acting as const)" begin for T1 in (Vec, Point) - x = [T1(2, 3), T1(7, 3)] + x = [T1(2, 3, 1), T1(7, 3, 2)] for T2 in (Vec, Point) T = ifelse(T1 == Point, Point, ifelse(T2 == Point, Point, Vec)) - @test [T(4, 9), T(14, 9)] == x .* T2(2, 3) - @test [T(foo(v, T2(3, -1))) for v in x] == foo.(x, T2(3, -1)) + @test [T(4, 9, 4), T(14, 9, 8)] == x .* T2(2, 3, 4) + @test [T(foo(v, T2(3, -1, 1))) for v in x] == foo.(x, T2(3, -1, 1)) end end - x = [M1, M2] - @test [M1 + M2, M2 + M2] == x .+ M2 - @test [foo(M1, M2), foo(M2, M2)] == foo.(x, M2) + x = [M1, M2, M1] + @test [M1 + M2, M2 + M2, M1 + M2] == x .+ M2 + @test [foo(M1, M2), foo(M2, M2), foo(M1, M2)] == foo.(x, M2) # maybe bad...? @test [Vec(2, 3), Vec(3, 4)] == [1, 2] .+ Vec(1, 2) From a5ab6fd4d4590fcb552f688b0d4f78b47bb9d59f Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 5 Sep 2024 23:45:25 +0200 Subject: [PATCH 47/58] handle map like StaticArrays --- src/fixed_arrays.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index 56735b28..f74eed19 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -3,7 +3,7 @@ using LinearAlgebra import Random import Base: setindex -abstract type StaticVector{N, T} end +abstract type StaticVector{N, T} <: AbstractVector{T} end function similar_type end struct StaticArrayStyle{T, AsConst} <: Broadcast.BroadcastStyle end @@ -119,7 +119,7 @@ Base.BroadcastStyle(::Broadcast.BroadcastStyle, ::StaticArrayStyle{T, B}) where # StaticArrayStyle{preffered_type, B1 || B2}() # If we don't inherit from AbstractVector we need this? -Base.broadcastable(x::StaticVector) = x +# Base.broadcastable(x::StaticVector) = x # Required to avoid size missmatches between Array and StaticVector function Broadcast.instantiate(bc::Broadcast.Broadcasted{<: StaticArrayStyle{<: Any, true}}) @@ -143,7 +143,9 @@ function Base.copy(bc::Broadcast.Broadcasted{StaticArrayStyle{T, false}}) where return T(broadcast(bc.f, args...)) end -Base.map(f, b::StaticVector) = similar_type(b)(map(f, b.data)) +Base.map(f, a::StaticVector, args::AbstractArray...) = broadcast(f, a, args...) +Base.map(f, a::AbstractArray, b::StaticVector, args::AbstractArray...) = broadcast(f, a, b, args...) +Base.map(f, a::StaticVector, b::StaticVector, args::AbstractArray...) = broadcast(f, a, b, args...) function Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{V}) where V <: StaticVector{N,T} where {N, T} V(ntuple(i-> rand(rng, T), N)) From 4891a296609136ffcc018548e39d9f67672ed172 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 6 Sep 2024 13:44:42 +0200 Subject: [PATCH 48/58] add isapprox and converts from Matrix for Mat3f-like types --- src/mat.jl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/mat.jl b/src/mat.jl index 378e77cc..5880603d 100644 --- a/src/mat.jl +++ b/src/mat.jl @@ -89,6 +89,7 @@ Mat{C, R, T1}(x::NTuple{L, T2}) where {C, R, L, T1, T2} = Mat{C, R, T1}(convert( Mat{C, R}(x::AbstractMatrix{T}) where {C, R, T} = Mat{C, R, T}(x) Mat{C, R, T}(x::AbstractMatrix) where {C, R, T} = Mat{C, R, T}(ntuple(i-> convert(T, x[i]), C*R)) +Mat{C, R, T, N}(x::AbstractMatrix) where {C, R, T, N} = Mat{C, R, T}(ntuple(i-> convert(T, x[i]), C*R)) Base.convert(::Type{Mat{C, R, T, L}}, from::Mat{C, R}) where {C, R, T, L} = Mat{C, R, T}(from.values) @@ -220,4 +221,12 @@ end end # TODO: Fix Vec(mat) becoming Vec((mat,)) (i.e. restrict eltype to Number?) -(VT::Type{<: StaticVector{N}})(mat::Mat{N, 1}) where {N} = VT(mat.values) \ No newline at end of file +(VT::Type{<: StaticVector{N}})(mat::Mat{N, 1}) where {N} = VT(mat.values) + +function Base.isapprox( + a::Mat{R1, C1, T1}, b::Mat{R2, C2, T2}; + atol::Real = 0, + rtol::Real = atol > 0 ? 0 : sqrt(max(eps(T1), eps(T2))) + ) where {R1, R2, C1, C2, T1, T2} + return (R1 == R2) && (C1 == C2) && norm(a - b) <= max(atol, rtol * max(norm(a), norm(b))) +end \ No newline at end of file From 94aefd571df8d4ae5b151425908ebb6cc3449a3c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 6 Sep 2024 17:06:43 +0200 Subject: [PATCH 49/58] avoid creating Base arrays on copy --- src/fixed_arrays.jl | 2 ++ src/mat.jl | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index f74eed19..3753060a 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -189,6 +189,8 @@ Base.length(::StaticVector{N}) where {N} = N Base.length(::Type{<: StaticVector{N}}) where {N} = N Base.ndims(::Type{<: StaticVector}) = 1 +Base.copy(v::StaticVector) = deepcopy(v) + Base.vcat(a::StaticVector, b::StaticVector) = (a..., b...) function Base.iterate(A::StaticVector, i=1) diff --git a/src/mat.jl b/src/mat.jl index 5880603d..a638d4df 100644 --- a/src/mat.jl +++ b/src/mat.jl @@ -55,6 +55,8 @@ Base.broadcastable(x::Mat) = x Base.BroadcastStyle(::Type{<: Mat{C, R}}) where {C, R} = StaticArrayStyle{Mat{C, R}}() Base.values(m::Mat) = m.values +Base.copy(mat::Mat) = deepcopy(mat) + function Mat{C, R, T}(::LinearAlgebra.UniformScaling) where {C, R, T} idx = CartesianIndices((R, C)) data = ntuple(C * R) do i From d485293d34fccc27d922217b8082e885a9e706c9 Mon Sep 17 00:00:00 2001 From: Frederic Freyer Date: Wed, 16 Oct 2024 17:34:01 +0200 Subject: [PATCH 50/58] Geometry Basics refactor (#219) * update MetaMesh * add MultiFace type * update Mesh & MultiFace types * update Mesh & MetaMesh utils * add MultiFace remapping code * prototype MultiFace Rect -> Mesh pipeline * generalize MultiFace getindex to Integer * remove add_meta, pop_meta * update merge for MultiFace and views * add AbstractVertexFace and AbstractMultiFace * split up mesh() for better usability * minor fixes * add views to Mesh constructors * add `mesh()` method for converting facetype of mesh * switch back to "normals" * fix missing normals rename * add back point/normal/uv-type kwargs * consider face views in face decompose * add mesh(mesh; attribs...) & improve dispatch safety * fix normals(), cleanup tests * add mesh constructor tests + fixes * deprecate normals for normal as vertex attribute name * make NormalFace and NormalUVFace types again * cleanup tests & normals vs normal * let ci run * remove views aware face decomposition * fix rect index order * fix MultiFace remapping with OffsetIntegers * add moreMultiFace utils * restore decompose(FaceType, faces, views) * allow MultiFace -> LineFace conversion * define Rect3 faces counterclockwise * add more convenience types * filter nothing attributes * drop Base.Pairs for 1.6 compat * Add depwarn in hasproperty too * improve MultiFace show * update Pyramid * update Cylinder * add MultiFace decompose * fix Cylinder tests * make OffsetInteger printing copyable * update tests for Rect3 and normal gen * fix remaining tests * fix incorrect vertex index counter * simplify merge of mixed Face types * test merge(meshes) * prototype swapping from MultiFace to FaceViews * treat views in vertex index remapping + some fixes * fix face type change * clean up AbstractVertexFace * extend FaceView interface * update Cylinder, Pyramids * declutter NgonFace prints * update tests * cleanup some test_broken * switch to Dict * fix tests * remove shorthands * export vertex_attributes and FaceView * add center point to Circle to avoid shallow triangles * make untesselated rect vertices counter-clockwise * fix tests * fix Cylinder face windig direction * add `face_normals()` helepr function * cleanup face_normals and normals a bit more * update tests for Cylinder * rename connectivity -> faces * update docs (meshes, primitives, decompose, Point, Vec, Mat) * add/update docstrings * add quick test for face_normals() * fix TetrahedronFace conversions * restore volume functions * fix tests * add some Polygon tests * test Pyramids * test TetrahedronFace decomposition * test and improve Mesh validation * test Mesh inteface functions * test decompose with views * test and fix matrix det, inv, transpose, mat*vec * fix tests * cleanup normal gen and export face_normals * add util for splitting meshes by views * fix missing dot in range .+ idx * improve performance of merge * fix tests * improve GeoInterface conversion performance * switch back to NamedTuple for performance * cleanup merge(meshes) * test clear_faceviews with mesh.views * fix missing import in docs examples * add convert for arrays of meshes * add function for removing duplicate faces * update normal gen tests + fixes - fix normal gen for varying face types - fix normalization of face_normals * remove time piracy * bring back shorthand types * restrict type in meshes to error earlier * update precompiles * autoconvert point dim in merge(meshes) instead of restricitng type * add compat entry * ignore unused PrecompileTools in 1.6, 1.7 * bring back old precompiles * add convert target to orthogonal_vector * revert triangulation changes of Circle * revert to using StaticArrays * avoid some invalidations (and fix get) * fix stale instances due to AbstractVector Apparently T[] is an AbstractVector here? * use string interpolation in error to avoid invalidation from string * * fix test * export clear_faceviews & update FaceView docstring * update type docstring * add notes about views * update Mesh docs * add FaceView ref to Mesh docstring * add brief section about extending decompose * derive Point eltype when dimension is given * reuse docstring * fix docs? * fix docs?? --- .github/workflows/ci.yml | 1 + Project.toml | 4 + docs/make.jl | 3 +- docs/src/decomposition.md | 111 ++----- docs/src/implementation.md | 5 - docs/src/index.md | 85 +----- docs/src/meshes.md | 80 ++++- docs/src/metadata.md | 153 ---------- docs/src/primitives.md | 184 +++++++++++- docs/src/rectangles.md | 1 - docs/src/static_array_types.md | 38 +++ src/GeometryBasics.jl | 14 +- src/basic_types.jl | 508 ++++++++++++++++++++++++++++--- src/boundingboxes.jl | 11 +- src/fixed_arrays.jl | 376 ++++++++++------------- src/geointerface.jl | 7 +- src/geometry_primitives.jl | 126 ++++++-- src/interfaces.jl | 91 +++++- src/mat.jl | 234 --------------- src/meshes.jl | 532 ++++++++++++++++++++++++++------- src/offsetintegers.jl | 27 +- src/precompiles.jl | 46 ++- src/primitives/cylinders.jl | 138 ++++----- src/primitives/pyramids.jl | 36 ++- src/primitives/rectangles.jl | 71 +++-- src/primitives/spheres.jl | 14 +- test/fixed_arrays.jl | 53 ++-- test/geometrytypes.jl | 164 +++++----- test/meshes.jl | 372 ++++++++++++++++++++--- test/polygons.jl | 41 +-- test/runtests.jl | 90 +++--- 31 files changed, 2273 insertions(+), 1343 deletions(-) delete mode 100644 docs/src/implementation.md delete mode 100644 docs/src/metadata.md delete mode 100644 docs/src/rectangles.md create mode 100644 docs/src/static_array_types.md delete mode 100644 src/mat.jl diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bf35910..2cb3d95f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: pull_request: branches: - master + - sd/simple-mesh jobs: test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} diff --git a/Project.toml b/Project.toml index 7f711f7b..37374abc 100644 --- a/Project.toml +++ b/Project.toml @@ -9,7 +9,9 @@ Extents = "411431e0-e8b7-467b-b5e0-f676ba4f2910" GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" IterTools = "c8e1da08-722c-5040-9ed9-7db0dc04731e" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] Aqua = "0.8" @@ -20,7 +22,9 @@ GeoJSON = "0.7, 0.8" IterTools = "1.3.0" LinearAlgebra = "<0.0.1,1" OffsetArrays = "1" +PrecompileTools = "1.0" Random = "<0.0.1,1" +StaticArrays = "0.6, 1" Test = "<0.0.1,1" julia = "1.6" diff --git a/docs/make.jl b/docs/make.jl index c8cdefd0..9d41c5e7 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -10,11 +10,10 @@ makedocs(format=Documenter.HTML(prettyurls=get(ENV, "CI", "false") == "true"), pages=[ "index.md", "primitives.md", - "rectangles.md", "polygons.md", "meshes.md", "decomposition.md", - "metadata.md", + "static_array_types.md", "api.md" ], modules=[GeometryBasics]) diff --git a/docs/src/decomposition.md b/docs/src/decomposition.md index d0bf3dfe..80f09590 100644 --- a/docs/src/decomposition.md +++ b/docs/src/decomposition.md @@ -1,89 +1,26 @@ # Decomposition - -## GeometryBasics Mesh interface - -GeometryBasics defines an interface to decompose abstract geometries into -points and triangle meshes. -This can be done for any arbitrary primitive, by overloading the following interface: - -```julia - -function GeometryBasics.coordinates(rect::Rect2, nvertices=(2,2)) - mini, maxi = extrema(rect) - xrange, yrange = LinRange.(mini, maxi, nvertices) - return ivec(((x,y) for x in xrange, y in yrange)) -end - -function GeometryBasics.faces(rect::Rect2, nvertices=(2, 2)) - w, h = nvertices - idx = LinearIndices(nvertices) - quad(i, j) = QuadFace{Int}(idx[i, j], idx[i+1, j], idx[i+1, j+1], idx[i, j+1]) - return ivec((quad(i, j) for i=1:(w-1), j=1:(h-1))) -end -``` -Those methods, for performance reasons, expect you to return an iterator, to make -materializing them with different element types allocation free. But of course, -can also return any `AbstractArray`. - -With these methods defined, this constructor will magically work: - -```julia -rect = Rect2(0.0, 0.0, 1.0, 1.0) -m = GeometryBasics.mesh(rect) -``` -If you want to set the `nvertices` argument, you need to wrap your primitive in a `Tesselation` -object: -```julia -m = GeometryBasics.mesh(Tesselation(rect, (50, 50))) -length(coordinates(m)) == 50^2 -``` - -As you can see, `coordinates` and `faces` are also defined on a mesh -```julia -coordinates(m) -faces(m) -``` -But will actually not be an iterator anymore. Instead, the mesh constructor uses -the `decompose` function, that will collect the result of coordinates and will -convert it to a concrete element type: -```julia -decompose(Point2f, rect) == convert(Vector{Point2f}, collect(coordinates(rect))) -``` -The element conversion is handled by `simplex_convert`, which also handles convert -between different face types: -```julia -decompose(QuadFace{Int}, rect) == convert(Vector{QuadFace{Int}}, collect(faces(rect))) -length(decompose(QuadFace{Int}, rect)) == 1 -fs = decompose(GLTriangleFace, rect) -fs isa Vector{GLTriangleFace} -length(fs) == 2 # 2 triangles make up one quad ;) -``` -`mesh` uses the most natural element type by default, which you can get with the unqualified Point type: -```julia -decompose(Point, rect) isa Vector{Point{2, Float64}} -``` -You can also pass the element type to `mesh`: -```julia -m = GeometryBasics.mesh(rect, pointtype=Point2f, facetype=QuadFace{Int}) -``` -You can also set the uv and normal type for the mesh constructor, which will then -calculate them for you, with the requested element type: -```julia -m = GeometryBasics.mesh(rect, uv=Vec2f, normaltype=Vec3f) -``` - -As you can see, the normals are automatically calculated, -the same is true for texture coordinates. You can overload this behavior by overloading -`normals` or `texturecoordinates` the same way as coordinates. -`decompose` works a bit different for normals/texturecoordinates, since they dont have their own element type. -Instead, you can use `decompose` like this: -```julia -decompose(UV(Vec2f), rect) -decompose(Normal(Vec3f), rect) -# the short form for the above: -decompose_uv(rect) -decompose_normals(rect) -``` -You can also use `triangle_mesh`, `normal_mesh` and `uv_normal_mesh` to call the -`mesh` constructor with predefined element types (Point2/3f, Vec2/3f), and the requested attributes. +## decompose functions + +The `decompose` functions allow you to grab certain data from an `AbstractGeometry` like a mesh or primitive and convert it to a requested type, if possible. +They can also be used to convert an array of e.g. faces into a different face type directly. +The default decomposition implemented by GeoemtryBasics are: +- `decompose(::Type{<: Point}, source)` which collects data from `source` using `coordinates(source)` and converts it to the given point type. +- `decompose_normals([::Type{<: Vec},] source) = decompose([::Type{Normals{<: Vec}}},] source)` which collects data with `normals(source)` and converts it to the given Vec type. +- `decompose_uv([::Type{<: Vec},] source) = decompose([::Type{UV{<: Vec}}},] source)` which collects data with `texturecoordinates(source)` and converts it to the given Vec type. This function also exists with `UVW` texture coordinates. +- `decompose(::Type{<: AbstractFace}, source)` which collects data with `faces(source)` and converts it to the given face type. + +### Extending decompose + +For `decompose` to work there needs to be a conversion from some element type to some target type. +GeometryBasics relies on `GeometryBasics.convert_simplex(TargetType, value)` for this. +If you want to add new types to decompose, e.g. a new face type, you will need to add a method to that function. + +## Primitive decomposition + +GeometryBasics defines an interface to decompose geometry primitives into vertex attributes and faces. +The interface includes four functions: +- `coordinates(primitive[, nvertices])` which produces the positions associated with the primitive +- `faces(primitive[, nvertices])` which produces the faces which connect the vertex positions to a mesh +- `normals(primitive[, nvertices])` which optionally provide normal vectors of the primitive +- `texturecoordinates(primitive[, nvertices])` which optional provide texture coordinates (uv/uvw) of the primitive diff --git a/docs/src/implementation.md b/docs/src/implementation.md deleted file mode 100644 index 6334eed0..00000000 --- a/docs/src/implementation.md +++ /dev/null @@ -1,5 +0,0 @@ -# Implementation - -In the backend, GeometryTypes relies on fixed-size arrays, specifically static vectors. - -TODO add more here. diff --git a/docs/src/index.md b/docs/src/index.md index dc570e92..f9166eda 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -22,56 +22,20 @@ p2 = Point(1, 3); p3 = Point(4, 4); ``` -Geometries can carry metadata: - -```@repl quickstart -poi = meta(p1, city="Abuja", rainfall=1221.2) -``` - -Metadata is stored in a NamedTuple and can be retrieved as such: - -```@repl quickstart -meta(poi) -``` - -Specific metadata attributes can be directly retrieved: - -```@repl quickstart -poi.rainfall -``` - -To remove the metadata and keep only the geometry, use `metafree`: - -```@repl quickstart -metafree(poi) -``` - -Geometries have predefined metatypes: - -```@repl quickstart -multipoi = MultiPointMeta([p1], city="Abuja", rainfall=1221.2) -``` - -Connect the points with lines: +Connect pairs of points as line segments: ```@repl quickstart l1 = Line(p1, p2) l2 = Line(p2, p3); ``` -Connect the lines in a linestring: - -```@repl quickstart -LineString([l1, l2]) -``` - -Linestrings can also be constructed directly from points: +Or connect multiple points as a linestring: ```@repl quickstart LineString([p1, p2, p3]) ``` -The same goes for polygons: +You can also create polygons from points: ```@repl quickstart Polygon(Point{2, Int}[(3, 1), (4, 4), (2, 4), (1, 2), (3, 1)]) @@ -89,16 +53,16 @@ Decompose the rectangle into two triangular faces: rect_faces = decompose(TriangleFace{Int}, rect) ``` -Decompose the rectangle into four vertices: +Decompose the rectangle into four positions: ```@repl quickstart -rect_vertices = decompose(Point{2, Float64}, rect) +rect_positions = decompose(Point{2, Float64}, rect) ``` Combine the vertices and faces into a triangle mesh: ```@repl quickstart -mesh = Mesh(rect_vertices, rect_faces) +mesh = Mesh(rect_positions, rect_faces) ``` Use `GeometryBasics.mesh` to get a mesh directly from a geometry: @@ -106,40 +70,3 @@ Use `GeometryBasics.mesh` to get a mesh directly from a geometry: ```@repl quickstart mesh = GeometryBasics.mesh(rect) ``` - - -## Aliases - -GeometryBasics exports common aliases for Point, Vec, Mat and Rect: - -### Vec - -| |`T`(eltype) |`Float64` |`Float32` |`Int` |`UInt` | -|--------|------------|----------|----------|----------|----------| -|`N`(dim)|`Vec{N,T}` |`Vecd{N}` |`Vecf{N}` |`Veci{N}` |`Vecui{N}`| -|`2` |`Vec2{T}` |`Vec2d` |`Vec2f` |`Vec2i` |`Vec2ui` | -|`3` |`Vec3{T}` |`Vec3d` |`Vec3f` |`Vec3i` |`Vec3ui` | - -### Point - -| |`T`(eltype) |`Float64` |`Float32` |`Int` |`UInt` | -|--------|------------|----------|----------|----------|----------| -|`N`(dim)|`Point{N,T}`|`Pointd{N}`|`Pointf{N}`|`Pointi{N}`|`Pointui{N}`| -|`2` |`Point2{T}` |`Point2d` |`Point2f` |`Point2i` |`Point2ui`| -|`3` |`Point3{T}` |`Point3d` |`Point3f` |`Point3i` |`Point3ui`| - -### Mat - -| |`T`(eltype) |`Float64` |`Float32` |`Int` |`UInt` | -|--------|------------|----------|----------|----------|----------| -|`N`(dim)|`Mat{N,T}` |`Matd{N}` |`Matf{N}` |`Mati{N}` |`Matui{N}`| -|`2` |`Mat2{T}` |`Mat2d` |`Mat2f` |`Mat2i` |`Mat2ui` | -|`3` |`Mat3{T}` |`Mat3d` |`Mat3f` |`Mat3i` |`Mat3ui` | - -### Rect - -| |`T`(eltype) |`Float64` |`Float32` |`Int` |`UInt` | -|--------|------------|----------|----------|----------|----------| -|`N`(dim)|`Rect{N,T}` |`Rectd{N}`|`Rectf{N}`|`Recti{N}`|`Rectui{N}`| -|`2` |`Rect2{T}` |`Rect2d` |`Rect2f` |`Rect2i` |`Rect2ui` | -|`3` |`Rect3{T}` |`Rect3d` |`Rect3f` |`Rect3i` |`Rect3ui` | diff --git a/docs/src/meshes.md b/docs/src/meshes.md index 6fabaf24..b9b1564f 100644 --- a/docs/src/meshes.md +++ b/docs/src/meshes.md @@ -1,24 +1,80 @@ # Meshes -## Types +GeometryBasics defines two mesh types to work with - `Mesh` and `MetaMesh` -* [`AbstractMesh`](@ref) -* [`Mesh`](@ref) +## Mesh + +```@docs; canonical=false +Mesh +``` + +You can get data from a mesh using a few interface functions: +- `vertex_attributes(mesh) = mesh.vertex_attributes` +- `coordinates(mesh) = mesh.vertex_attributes[:position]` +- `normals(mesh) = mesh.vertex_attributes[:normal]` +- `texturecoordinates(mesh) = mesh.vertex_attributes[:uv]` +- `faces(mesh) = mesh.faces` + +You can also grab the contents of `mesh.vertex_attributes` as if they were fields of the `Mesh`, e.g. `mesh.position` works. + +### FaceView + +As mentioned above, a vertex attribute can be a `FaceView`. +A `FaceView` is simply defined as a vector of data and a vector of faces: + +```julia +struct FaceView{T, AVT <: AbstractVector{T}, FVT <: AbstractVector{<: AbstractFace}} + data::AVT + faces::FVT +end +``` + +Its purpose is to allow you to add data that needs to be defined per vertex but does not match the vertex structure used by `mesh.faces`. + +As a minimal example consider a mesh that is just one triangle, i.e. 3 position and one triangle face `TriangleFace(1,2,3)`. +Let's say we want to add a flat color to the triangle. +In this case we only have one color, but our face refers to 3 different vertices (3 different positions). +To avoid duplicating the color data, we can instead define a new triangle face `TriangleFace(1)` and add the color attribute as a `FaceView([color], [TriangleFace(1)])`. +If we ever need the mesh to be defined with just one common set of faces, i.e. no FaceView and appropriately duplicated vertex data, we can use `clear_faceviews(mesh)` to generate it. + +On a larger scale this can be useful for memory and performance reason, e.g. when you do calculations with vertex attributes. +It can also simplify some definitions, like for example `Rect3`. +In that case we have 8 positions and 6 normals with FaceViews, or 24 without (assuming per-face normals). + + +## MetaMesh + +A `MetaMesh` is given by + +```julia +struct MetaMesh{Dim, T, M <: AbstractMesh{Dim, T}} <: AbstractMesh{Dim, T} + mesh::M + meta::Dict{Symbol, Any} +end +``` + +where `meta` may contain any data you want to include with a mesh. +For example, you could include group names or material data corresponding to `mesh.views`. ## How to create a mesh -### Meshing.jl +### GeometryBasics -### MeshIO.jl +In GeometryBasics you mainly create meshes from primitives using a few constructors: +- `triangle_mesh(primitive)` generates the most basic mesh (i.e. positions and faces) +- `normal_mesh(primitive)` generates a mesh with normals (generated if the primitive doesn't implement `normal()`) +- `uv_mesh(primitive)` generates a mesh with texture coordinates (generated if the primitive doesn't implement `texturecoordinates()`) +- `uv_normal_mesh(primitive)` generates a mesh with normals and texture coordinates -The [`MeshIO.jl`](https://github.com/JuliaIO/MeshIO.jl) package provides load/save support for several file formats which store meshes. +Each of these constructors also includes keyword arguments for setting types, i.e. `pointtype`, `facetype`, `normaltype` and `uvtype` as appropriate. +Of course you can also construct a mesh directly from data, either with there various `Mesh()` or `GeometryBasics.mesh()` constructors. +The latter also include a `pointtype` and `facetype` conversion. -## How to access data +Finally there is also a `merge(::Vector{Mesh})` function which combines multiple meshes into a single one. +Note that this doesn't remove any data (e.g. hidden or duplicate vertices), and may remove `FaceView`s if they are incompatible between meshes. -The following functions can be called on an [`AbstractMesh`](@ref) to access its underlying data. +### Meshing.jl -* [`faces`](@ref) -* [`coordinates`](@ref) -* `texturecoordinates` -* [`normals`](@ref) +### MeshIO.jl +The [`MeshIO.jl`](https://github.com/JuliaIO/MeshIO.jl) package provides load/save support for several file formats which store meshes. diff --git a/docs/src/metadata.md b/docs/src/metadata.md deleted file mode 100644 index d7bebcaf..00000000 --- a/docs/src/metadata.md +++ /dev/null @@ -1,153 +0,0 @@ -# Metadata - -## Meta - -The `Meta` method provides metadata handling capabilities in GeometryBasics. -Similarly to remove the metadata and keep only the geometry, use `metafree`, and -for vice versa i.e., remove the geometry and keep the metadata use `meta`. - -### Syntax - -```julia -meta(geometry, meta::NamedTuple) -meta(geometry; meta...) - -metafree(meta-geometry) -meta(meta-geometry) -``` - -### Examples - -```@repl meta -using GeometryBasics -p1 = Point(2.2, 3.6) -poi = meta(p1, city="Abuja", rainfall=1221.2) -``` - -Metadata is stored in a NamedTuple and can be retrieved as such: - -```@repl meta -meta(poi) -``` - -Specific metadata attributes can be directly retrieved: - -```@repl meta -poi.rainfall -metafree(poi) -``` - -Metatypes are predefined for geometries: - -```@repl meta -multipoi = MultiPointMeta([p1], city="Abuja", rainfall=1221.2) -``` - -(In the above example we have also used a geometry-specific meta method.) - -```@repl meta -GeometryBasics.MetaType(Polygon) -GeometryBasics.MetaType(Mesh) -``` - -The metageometry objects are infact composed of the original geometry types. - -```@repl meta -GeometryBasics.MetaFree(PolygonMeta) -GeometryBasics.MetaFree(MeshMeta) -``` - -## MetaT - -In GeometryBasics we can have tabular layout for a collection of meta-geometries -by putting them into a StructArray that extends the [Tables.jl](https://github.com/JuliaData/Tables.jl) API. - -In practice it's not necessary for the geometry or metadata types to be consistent. -For example, a geojson format can have heterogeneous geometries. Hence, such cases require -automatic widening of the geometry data types to the most appropriate type. -The MetaT method works around the fact that, a collection of geometries and metadata -of different types can be represented tabularly whilst widening to the appropriate type. - -### Syntax - -```julia -MetaT(geometry, meta::NamedTuple) -MetaT(geometry; meta...) -``` -Returns a `MetaT` that holds a geometry and its metadata `MetaT` acts the same as `Meta` method. -The difference lies in the fact that it is designed to handle geometries and metadata of different/heterogeneous types. - -For example, while a Point MetaGeometry is a `PointMeta`, the MetaT representation is `MetaT{Point}`. - -### Examples - -```@repl meta -MetaT(Point(1, 2), city = "Mumbai") -``` - -For a tabular representation, an iterable of `MetaT` types can be passed on to a `meta_table` method. - -### Syntax - -```julia -meta_table(iter) -``` - -### Examples - - Create an array of 2 linestrings: - -```@repl meta -ls = [LineString([Point(i, i+1), Point(i-1,i+5)]) for i in 1:2]; -coordinates.(ls) -``` - -Create a multi-linestring: - -```@repl meta -mls = MultiLineString(ls); -coordinates.(mls) -``` - -Create a polygon: - -```@repl meta -poly = Polygon(Point{2, Int}[(40, 40), (20, 45), (45, 30), (40, 40)]); -coordinates(poly) -``` - -Put all geometries into an array: - -```@repl meta -geom = [ls..., mls, poly]; -``` - -Generate some random metadata: - -```@repl meta -prop = [(country_states = "India$(i)", rainfall = (i*9)/2) for i in 1:4] -feat = [MetaT(i, j) for (i,j) = zip(geom, prop)]; # create an array of MetaT -``` - -We can now generate a `StructArray` / `Table` with `meta_table`: - -```@repl meta -sa = meta_table(feat); -``` - -The data can be accessed through `sa.main` and the metadata through -`sa.country_states` and `sa.rainfall`. Here we print only the type names of the -data items for brevity: - -```@repl meta -[nameof.(typeof.(sa.main)) sa.country_states sa.rainfall] -``` - -### Disadvantages - - * The MetaT is pretty generic in terms of geometry types, it's not subtype to - geometries. eg : A `MetaT{Point, NamedTuple{Names, Types}}` is not subtyped to - `AbstractPoint` like a `PointMeta` is. - - * This might cause problems on using `MetaT` with other constructors/methods - inside or even outside GeometryBasics methods designed to work with the main `Meta` types. diff --git a/docs/src/primitives.md b/docs/src/primitives.md index 2f28a12c..a0712e97 100644 --- a/docs/src/primitives.md +++ b/docs/src/primitives.md @@ -1,17 +1,181 @@ # Primitives -## Points and Vectors +In GeometryBasics.jl, a `GeometryPrimitive` is an object from which a mesh can +be constructed. -## Simplices +## Existing GeometryPrimitives -## Shapes +GeometryBasics comes with a few predefined primitives: -* [`Circle`](@ref) -* [`Sphere`](@ref) -* [`Cylinder`](@ref) +#### HyperRectangle -## Abstract types +A `Rect{D, T} = HyperRectangle{D, T}` is a D-dimensional axis-aligned +hyperrectangle defined by an origin and a size. -* `GeometryPrimitive` -* `AbstractSimplex` -* [`AbstractMesh`](@ref) +```@repl rects +using GeometryBasics +r1 = HyperRectangle{4, Float64}(Point{4, Float64}(0), Vec{4, Float64}(1)) +r2 = Rect3f(Point3f(-1), Vec3f(2)) +r3 = Rect2i(0, 0, 1, 1) +``` + +Rect2 supports normal and texture coordinate generation as well as tesselation. +Without tesselation, the coordinates of 2D Rects are defined in anti-clockwise order. +Rect3 supports normals and texture coordinates, but not tesselation. + +Shorthands: + +| |`T`(eltype) |`Float64` |`Float32` |`Int` |`UInt` | +|--------|------------|----------|----------|----------|----------| +|`N`(dim)|`Rect{N,T}` |`Rectd{N}`|`Rectf{N}`|`Recti{N}`|`Rectui{N}`| +|`2` |`Rect2{T}` |`Rect2d` |`Rect2f` |`Rect2i` |`Rect2ui` | +|`3` |`Rect3{T}` |`Rect3d` |`Rect3f` |`Rect3i` |`Rect3ui` | + +#### Sphere and Circle + +`Circle` and `Sphere` are the 2 and 3 dimensional variants of `HyperSphere`. +They are defined by an origin and a radius. +While you can technically create a HyperSphere of any dimension, decomposition +is only defined in 2D and 3D. + +```@repl hypersphere +s1 = HyperSphere{4, Int}(Point{4, Int}(0), 5) +s2 = Sphere(Point3f(0, 0, 1), 1) +s3 = Circle(Point2d(0), 2.0) +``` + +Circle and Sphere support normal and texture coordinate generation as well as tesselation. +The coordinates of Circle are defined in anti-clockwise order. + +#### Cylinder + +A `Cylinder` is a 3D shape defined by two points and a radius. + +```@repl cylinder +c = Cylinder(Point3f(-1, 0, 0), Point3f(0, 0, 1), 0.3f0) # start point, end point, radius +``` + +Cylinder supports normals an Tesselation, but currently no texture coordinates. + +#### Pyramid + +`Pyramid` corresponds to a pyramid shape with a square base and four triangles +coming together into a sharp point. +It is defined by by the center point of the base, its height and its width. + +```@repl pyramid +p = Pyramid(Point3f(0), 1f0, 0.3f0) # center, height, width +``` + +Pyramid supports normals, but currently no texture coordinates or tesselation + +## Tesselation + +In GeometryBasics `Tesselation` is a wrapper type for primitives which communicates +how dense the mesh generated from one should be. + +```@repl tesselation +t = Tesselation(Cylinder(Point3f(0), Point3f(0,0,1), 0.2), 32) # 32 vertices for each circle +normal_mesh(t) + +t = Tesselation(Rect2(Point2f(0), Vec2f(1)), (8, 6)) # 8 vertices in x direction by 6 in y direction +triangle_mesh(t) +``` + +## Primitive Interface / Implementing a new GeometryPrimitive + +Every primitive should inherit from `GeometryPrimitive{Dim, eltype}` and implement at least `coordinates(primitive)` and `faces(primitive)` so that a mesh can be build from it. +This will also be enough to automatically generate normals for a 3D primitive and texture coordinates for a 2D primitive. +You can also implement functions to generate them directly with `normals(primitive)` and `texturecoordinates(primitive)`. +Depending on your primitive this might be necessary to get the normals and uvs you want. + +To be compatible with `Tesselation` all of the functions mentioned above should implement a second tesselation argument. +This will be the second argument passed to the Tesselation constructor. +It's up to you to decide what makes sense here, though typically it's just an integer that more or less corresponds to the number of generated vertices. + +#### Example + +As an example, let's implement a parallelepiped, i.e. a 3D version or a parallelogram. +In this case we need an origin and 3 vectors telling us how far and in which directions the object extends. + +```julia +struct Parallelepiped{T} <: GeometryPrimitive{3, T} + origin::Point{3, T} + v1::Vec{3, T} + v2::Vec{3, T} + v3::Vec{3, T} +end +``` + +Like the `Rect{3}`, this object comes with 8 unique positions which we want to return as its `coordinates`. + +```julia +function GeometryBasics.coordinates(primitive::Parallelepiped{T}) where {T} + o = primitive.origin + v1 = primitive.v1; v2 = primitive.v2; v3 = primitive.v3 + return Point{3, T}[o, o+v2, o+v1+v2, o+v1, o+v3, o+v2+v3, o+v1+v2+v3, o+v1+v3] +end +``` + +To connect these points into a mesh, we need to generate a set of faces. +The faces of a prallelepiped are parallelograms, which we can describe with `QuadFace`. +Here we should be concious of the winding direction of faces. +They are often used to determine the front vs the backside of a (2D) face. +For example GeometryBasics normal generation and OpenGL's backface culling assume a counter-clockwise windig direction to correspond to a front-facing face. +This means that if we look at a face from outside the shape, the positions referred to by that face should be ordered counter-clockwise. +With that in mind the faces of our primitive become: + +```julia +function GeometryBasics.faces(::Parallelepiped) + return QuadFace{Int}[ + (1, 2, 3, 4), (5, 8, 7, 6), # facing -n3, +n3 (n3 being the normal of v1 x v2) + (1, 5, 6, 2), (4, 3, 7, 8), # facing -n2, +n2 + (2, 6, 7, 3), (1, 4, 8, 5), # facing -n1, +n1 + ] +end +``` + +Note that you can check the correct winding direction fairly easily with Makie and the default generated normals. +After implementing faces and coordinates, you can create a mesh plot of your primitive with `Makie.mesh(primitive)`. +If the mesh reacts to light in a reasonable way, i.e. gets brighter when light shines on it, then your faces have the correct winding direction. +(It maybe useful to compare to other primitives like `Sphere(Point3f(0), 1f0)` here.) + +Next on our TODO list are normals. +The default normals produced by `GeometryBasics.normal(primitive)` are vertex normals, which assume that a primitive to be smooth. +Since this is not the case for our primitive, we need to implement custom normals. +Here we could rely on `GeometryBasics.face_normal()` which returns a normal per face, but for this example we will implement them ourselves. + +For our shape we want one normal per face, pointing in the normal direction of the corresponding 2D plane. +We can calculate the normal vector as `n = normalize(cross(v, w))` where v and w correspond to combinations of v1, v2 and v3. +To get them to act per face rather than per vertex, we need to overwrite the faces generated by `faces()`. +We can do that by creating a `FaceView` with a new set of faces which only act on normals. +Each of these new faces needs to refer to one normal by index to get what we want. + +```julia +using LinearAlgebra +function GeometryBasics.normals(primitive::Parallelepiped) + n1 = normalize(cross(primitive.v2, primitive.v3)) + n2 = normalize(cross(primitive.v3, primitive.v1)) + n3 = normalize(cross(primitive.v1, primitive.v2)) + ns = [-n3, n3, -n2, n2, -n1, n1] + fs = QuadFace{Int}[1, 2, 3, 4, 5, 6] # = [QuadFace{Int}(1), QuadFace{Int}(2), ...] + return FaceView(ns, fs) +end +``` + +As the last piece of the interface we can implement texture coordinates. +They generally refer to a 2D image with normalized 2D coordinates on a per-vertex basis. +There are many ways to define these coordinates. +Here we will partition the image in 2x3 even sized rectangular sections, split by the sign of the normal directions defined above. + +```julia +function GeometryBasics.texturecoordinates(::Parallelepiped{T}) where {T} + uvs = [Vec2f(x, y) for x in range(0, 1, length=4) for y in range(0, 1, 3)] + fs = QuadFace{Int}[ + (1, 2, 5, 4), (2, 3, 6, 5), + (4, 5, 8, 7), (5, 6, 9, 8), + (7, 8, 11, 10), (8, 9, 12, 11) + ] + return FaceView(uvs, fs) +end +``` \ No newline at end of file diff --git a/docs/src/rectangles.md b/docs/src/rectangles.md deleted file mode 100644 index 09c9795f..00000000 --- a/docs/src/rectangles.md +++ /dev/null @@ -1 +0,0 @@ -# Rectangles diff --git a/docs/src/static_array_types.md b/docs/src/static_array_types.md new file mode 100644 index 00000000..6d2c2907 --- /dev/null +++ b/docs/src/static_array_types.md @@ -0,0 +1,38 @@ +# Point, Vec and Mat + +GeometryBasics defines its own set of (small) Static Vectors and Matrices: +```julia +Point{N,T} <: StaticVector{N,T} <: AbstractVector{T} +Vec{N,T} <: StaticVector{N,T} <: AbstractVector{T} +Mat{Row, Column, T, L} <: AbstractMatrix{T} +``` + +These types are used throughout GeometryBasics to speed up calculations similar to how StaticArrays.jl does. + +## Aliases + +GeometryBasics exports common aliases for Point, Vec, Mat and Rect: + +### Vec + +| |`T`(eltype) |`Float64` |`Float32` |`Int` |`UInt` | +|--------|------------|----------|----------|----------|----------| +|`N`(dim)|`Vec{N,T}` |`Vecd{N}` |`Vecf{N}` |`Veci{N}` |`Vecui{N}`| +|`2` |`Vec2{T}` |`Vec2d` |`Vec2f` |`Vec2i` |`Vec2ui` | +|`3` |`Vec3{T}` |`Vec3d` |`Vec3f` |`Vec3i` |`Vec3ui` | + +### Point + +| |`T`(eltype) |`Float64` |`Float32` |`Int` |`UInt` | +|--------|------------|----------|----------|----------|----------| +|`N`(dim)|`Point{N,T}`|`Pointd{N}`|`Pointf{N}`|`Pointi{N}`|`Pointui{N}`| +|`2` |`Point2{T}` |`Point2d` |`Point2f` |`Point2i` |`Point2ui`| +|`3` |`Point3{T}` |`Point3d` |`Point3f` |`Point3i` |`Point3ui`| + +### Mat + +| |`T`(eltype) |`Float64` |`Float32` |`Int` |`UInt` | +|--------|------------|----------|----------|----------|----------| +|`N`(dim)|`Mat{N,T}` |`Matd{N}` |`Matf{N}` |`Mati{N}` |`Matui{N}`| +|`2` |`Mat2{T}` |`Mat2d` |`Mat2f` |`Mat2i` |`Mat2ui` | +|`3` |`Mat3{T}` |`Mat3d` |`Mat3f` |`Mat3i` |`Mat3ui` | diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index dc542474..21554b83 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -1,6 +1,6 @@ module GeometryBasics -using IterTools, LinearAlgebra +using IterTools, LinearAlgebra, StaticArrays using GeoInterface import Extents using EarCut_jll @@ -40,10 +40,11 @@ export Triangle export AbstractFace, TriangleFace, QuadFace, GLTriangleFace export OffsetInteger, ZeroIndex, OneIndex, GLIndex export decompose, coordinates, faces, normals, decompose_uv, decompose_normals, - texturecoordinates + texturecoordinates, vertex_attributes +export clear_faceviews +export face_normals export Tesselation, Normal, UV, UVW -export AbstractMesh, Mesh, MetaMesh -export add_meta, pop_meta +export AbstractMesh, Mesh, MetaMesh, FaceView # all the different predefined mesh types @@ -56,7 +57,7 @@ export uv_mesh, normal_mesh, uv_normal_mesh export height, origin, radius, width, widths export HyperSphere, Circle, Sphere -export Cylinder, Cylinder2, Cylinder3, Pyramid, extremity +export Cylinder, Pyramid, extremity export HyperRectangle, Rect, Rect2, Rect3, Recti, Rect2i, Rect3i, Rectf, Rect2f, Rect3f, Rectd, Rect2d, Rect3d export before, during, meets, overlaps, intersects, finishes export centered, direction, area, volume, update @@ -64,9 +65,8 @@ export max_dist_dim, max_euclidean, max_euclideansq, min_dist_dim, min_euclidean export min_euclideansq, minmax_dist_dim, minmax_euclidean, minmax_euclideansq export self_intersections, split_intersections -if Base.VERSION >= v"1.4.2" +if Base.VERSION >= v"1.8" include("precompiles.jl") - _precompile_() end end # module diff --git a/src/basic_types.jl b/src/basic_types.jl index 0b3f39af..855102c2 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -1,18 +1,30 @@ """ -Abstract Geometry in R{Dim} with Number type T + abstract type AbstractGeometry{Dimension, T<:Number} + +Base type for geometry types like GeometryPrimites and Polytopes. """ abstract type AbstractGeometry{Dim,T<:Number} end abstract type GeometryPrimitive{Dim,T} <: AbstractGeometry{Dim,T} end Base.ndims(::AbstractGeometry{Dim}) where {Dim} = Dim """ -Geometry made of N connected points. Connected as one flat geometry, it makes a Ngon / Polygon. -Connected as volume it will be a Simplex / Tri / Cube. -Note That `Polytope{N} where N == 3` denotes a Triangle both as a Simplex or Ngon. + Polytope{Dim, T} <: AbstractGeometry{Dim, T} + +A Polytope is the generalization of a Polygon to higher dimensions, i.e. a +geometric object consisting of flat faces. + +A `Polygon` and `Ngon` are both 2D `Polytope`s. A `Simplex` is the simplest +`Polytope` in arbitrary dimensions. """ abstract type Polytope{Dim,T} <: AbstractGeometry{Dim,T} end abstract type AbstractPolygon{Dim,T} <: Polytope{Dim,T} end +""" + AbstractFace{N_indices, T} <: StaticVector{N_indices, T} + +Parent type for all face types. The standard face type is typically a +`GLTriangleFace = NgonFace{3, GLIndex}`. +""" abstract type AbstractFace{N,T} <: StaticVector{N,T} end abstract type AbstractSimplexFace{N,T} <: AbstractFace{N,T} end abstract type AbstractNgonFace{N,T} <: AbstractFace{N,T} end @@ -32,20 +44,88 @@ end const TetrahedronFace{T} = SimplexFace{4,T} Face(::Type{<:SimplexFace{N}}, ::Type{T}) where {N,T} = SimplexFace{N,T} -""" -Face index, connecting points to form an Ngon -""" @fixed_vector NgonFace = AbstractNgonFace +""" + NgonFace{N, T} + +A planar face connecting N vertices. Shorthands include: +- `LineFace{T} = NgonFace{2,T}` +- `TriangleFace{T} = NgonFace{3,T}` +- `QuadFace{T} = NgonFace{4,T}` +- `GLTriangleFace = TriangleFace{GLIndex}` +""" +NgonFace const LineFace{T} = NgonFace{2,T} const TriangleFace{T} = NgonFace{3,T} const QuadFace{T} = NgonFace{4,T} const GLTriangleFace = TriangleFace{GLIndex} -function Base.show(io::IO, x::TriangleFace{T}) where {T} - return print(io, "TriangleFace(", join(x, ", "), ")") +function Base.show(io::IO, x::NgonFace{N, T}) where {N, T} + if N == 2 + name = "LineFace{$T}" + elseif N == 3 + if T == GLIndex + name = "GLTriangleFace" + else + name = "TriangleFace{$T}" + end + elseif N == 4 + name = "QuadFace{$T}" + else + name = "NgonFace{$N, $T}" + end + + return print(io, name, "(", join(value.(x), ", "), ")") +end + +# two faces are the same if they match or they just cycle indices +function Base.:(==)(f1::FT, f2::FT) where {N, FT <: AbstractFace{N}} + _, min_i1 = findmin(f1.data) + _, min_i2 = findmin(f2.data) + @inbounds for i in 1:N + if f1[mod1(min_i1 + i, end)] !== f2[mod1(min_i2 + i, end)] + return false + end + end + return true +end +function Base.hash(f::AbstractFace{N}, h::UInt) where {N} + _, min_i = findmin(f.data) + @inbounds for i in min_i:N + h = hash(f[i], h) + end + @inbounds for i in 1:min_i-1 + h = hash(f[i], h) + end + return h +end +Base.isequal(f1::AbstractFace, f2::AbstractFace) = ==(f1, f2) + +# Fastpaths +Base.:(==)(f1::FT, f2::FT) where {FT <: AbstractFace{2}} = minmax(f1.data...) == minmax(f2.data...) +Base.hash(f::AbstractFace{2}, h::UInt) = hash(minmax(f.data...), h) + +function Base.:(==)(f1::FT, f2::FT) where {FT <: AbstractFace{3}} + return (f1.data == f2.data) || (f1.data == (f2[2], f2[3], f2[1])) || + (f1.data == (f2[3], f2[1], f2[2])) +end +function Base.hash(f::AbstractFace{3}, h::UInt) + if f[1] < f[2] + if f[1] < f[3] + return hash(f.data, h) + else + return hash((f[3], f[1], f[2]), h) + end + else + if f[2] < f[3] + return hash((f[2], f[3], f[1]), h) + else + return hash((f[3], f[1], f[2]), h) + end + end end Face(::Type{<:NgonFace{N}}, ::Type{T}) where {N,T} = NgonFace{N,T} @@ -56,12 +136,16 @@ Face(F::Type{NgonFace{N,FT}}, ::Type{T}) where {FT,N,T} = F @propagate_inbounds Base.iterate(x::Polytope, i) = iterate(coordinates(x), i) """ -Fixed Size Polygon, e.g. + Ngon{D, T, N}(points::NTuple{N, Point{D, T}}) + +Defines a flat polygon (without holes) in D dimensional space using N points, e.g.: - N 1-2 : Illegal! - N = 3 : Triangle - N = 4 : Quadrilateral (or Quad, Or tetragon) - N = 5 : Pentagon - ... + +For polygons with holes, see `Polygon`. """ struct Ngon{Dim, T<:Real, N} <: AbstractPolygon{Dim,T} points::NTuple{N, Point{Dim, T}} @@ -81,6 +165,8 @@ Base.length(::Type{<:NNgon{N}}) where {N} = N Base.length(::NNgon{N}) where {N} = N """ + Polytope(::Type{<: Point}, ::Type{<: AbstractNgonFace}) + The Ngon Polytope element type when indexing an array of points with a SimplexFace """ function Polytope(::Type{Point{Dim,T}}, @@ -89,9 +175,14 @@ function Polytope(::Type{Point{Dim,T}}, end """ + Polytope(::Type{<: Ngon}, P::Type{<: Point}) + The fully concrete Ngon type, when constructed from a point type! """ -function Polytope(::Type{<:NNgon{N}}, P::Type{Point{NDim,T}}) where {N,NDim,T} +function Polytope(::Type{<:Ngon{_D, _T, N}}, P::Type{Point{NDim,T}}) where {N,NDim,T, _D,_T} + return Ngon{NDim,T,N} +end +function Polytope(::Type{<:Ngon{_D, _T, N} where {_D,_T}}, P::Type{Point{NDim,T}}) where {N,NDim,T} return Ngon{NDim,T,N} end @@ -121,6 +212,8 @@ function coordinates(lines::AbstractArray{Line{Dim,T}}) where {Dim,T} end """ + Simplex{D, T<:Real, N}(points::NTuple{N, Point{D, T}}) + A `Simplex` is a generalization of an N-dimensional tetrahedra and can be thought of as a minimal convex set containing the specified points. @@ -156,6 +249,8 @@ Base.length(::Type{<:NSimplex{N}}) where {N} = N Base.length(::NSimplex{N}) where {N} = N """ + Polytope(::Type{Point{Dim,T}}, ::Type{<:AbstractSimplexFace{N}}) + The Simplex Polytope element type when indexing an array of points with a SimplexFace """ function Polytope(::Type{Point{Dim,T}}, ::Type{<:AbstractSimplexFace{N}}) where {N,Dim,T} @@ -163,6 +258,8 @@ function Polytope(::Type{Point{Dim,T}}, ::Type{<:AbstractSimplexFace{N}}) where end """ + Polytope(::Type{<:NSimplex{N}}, P::Type{Point{NDim,T}}) + The fully concrete Simplex type, when constructed from a point type! """ function Polytope(::Type{<:NSimplex{N}}, P::Type{Point{NDim,T}}) where {N,NDim,T} @@ -175,6 +272,8 @@ Base.show(io::IO, x::Line) = print(io, "Line(", x[1], " => ", x[2], ")") Polygon(exterior::AbstractVector{<:Point}) Polygon(exterior::AbstractVector{<:Point}, interiors::Vector{<:AbstractVector{<:Point}}) +Constructs a polygon from a set of exterior points. If interiors are given, each +of them cuts away from the Polygon. """ struct Polygon{Dim,T<:Real} <: AbstractPolygon{Dim,T} exterior::Vector{Point{Dim, T}} @@ -224,6 +323,8 @@ end """ MultiPolygon(polygons::AbstractPolygon) + +A collection of polygons """ struct MultiPolygon{Dim, T<:Real} <: AbstractGeometry{Dim, T} polygons::Vector{<:AbstractPolygon{Dim,T}} @@ -239,7 +340,8 @@ Base.length(mp::MultiPolygon) = length(mp.polygons) """ LineString(points::AbstractVector{<:Point}) -A LineString is a geometry of connected line segments + +A LineString is a collection of points connected by line segments. """ struct LineString{Dim, T<:Real} <: AbstractGeometry{Dim, T} points::Vector{Point{Dim, T}} @@ -276,36 +378,303 @@ Base.getindex(mpt::MultiPoint, i) = mpt.points[i] Base.size(mpt::MultiPoint) = size(mpt.points) Base.length(mpt::MultiPoint) = length(mpt.points) + +""" + FaceView(data, faces) + +A FaceView is an alternative to passing a vertex attributes directly to a mesh. +It bundles `data` with a new set of `faces` which may index that data differently +from the faces defined in a mesh. This can be useful to avoid duplication of data. + +For example, `data` can be defined per face by giving each face just one (repeated) +index: +```julia +per_face_normals = FaceView( + normals, # one per face + FT.(eachindex(normals)) # with FT = facetype(mesh) +) +``` + +To remove `FaceView`s from a mesh, e.g. for rendering, use `clear_faceviews(mesh)`. + +You can get the data of a FaceView with `values(faceview)` and the faces with +`faces(faceview)`. +""" +struct FaceView{T, AVT <: AbstractVector{T}, FVT <: AbstractVector{<: AbstractFace}} + data::AVT + faces::FVT +end + +const VertexAttributeType{T} = Union{FaceView{T}, AbstractVector{T}} + +function Base.vcat(a::FaceView, b::FaceView) + N = length(a.data) + return FaceView( + vcat(a.data, b.data), + vcat(a.faces, map(f -> typeof(f)(f .+ N), b.faces)) + ) +end + +faces(x::FaceView) = x.faces +Base.values(x::FaceView) = x.data +facetype(x::FaceView) = eltype(x.faces) +Base.getindex(x::FaceView, f::AbstractFace) = getindex(values(x), f) +Base.isempty(x::FaceView) = isempty(values(x)) +Base.:(==)(a::FaceView, b::FaceView) = (values(a) == values(b)) && (faces(a) == faces(b)) + +# TODO: maybe underscore this as it requires care to make sure all FaceViews and +# mesh faces stay in sync +convert_facetype(::Type{FT}, x::AbstractVector) where {FT <: AbstractFace} = x +function convert_facetype(::Type{FT}, x::FaceView) where {FT <: AbstractFace} + if eltype(faces(x)) != FT + return FaceView(values(x), decompose(FT, faces(x))) + end + return x +end + +function verify(fs::AbstractVector{FT}, fv::FaceView, name = nothing) where {FT <: AbstractFace} + if length(faces(fv)) != length(fs) + error("Number of faces given in FaceView $(length(faces(fv))) does not match reference $(length(fs))") + end + + N = maximum(f -> value(maximum(f)), faces(fv), init = 0) + if length(values(fv)) < N + error("FaceView addresses $N vertices with faces, but only has $(length(values(fv))).") + end + + if isconcretetype(FT) && (FT == facetype(fv)) + return true + end + + for (i, (f1, f2)) in enumerate(zip(faces(fv), fs)) + if length(f1) != length(f2) + error("Length of face $i = $(length(f1)) does not match reference with $(length(f2))") + end + end + + return true +end + +# Dodgy definitions... (since attributes can be FaceView or Array it's often +# useful to treat a FaceView like the vertex data it contains) +Base.length(x::FaceView) = length(values(x)) +# Base.iterate(x::FaceView) = iterate(values(x)) +# Base.getindex(x::FaceView, i::Integer) = getindex(values(x), i) +# Taken from Base/arrayshow.jl +function Base.show(io::IO, ::MIME"text/plain", X::FaceView) + summary(io, X) + isempty(X) && return + print(io, ":") + + if get(io, :limit, false)::Bool && displaysize(io)[1]-4 <= 0 + return print(io, " …") + else + println(io) + end + + io = IOContext(io, :typeinfo => eltype(values(X))) + + recur_io = IOContext(io, :SHOWN_SET => values(X)) + Base.print_array(recur_io, values(X)) +end + + + """ AbstractMesh -An abstract mesh is a collection of Polytope elements (Simplices / Ngons). -The connections are defined via faces(mesh), the coordinates of the elements are returned by -coordinates(mesh). Arbitrary meta information can be attached per point or per face +An abstract mesh is a collection of Polytope elements (Simplices / Ngons). The +connections are defined via faces(mesh) and the coordinates of the elements are +returned by coordinates(mesh). """ abstract type AbstractMesh{Dim, T} <: AbstractGeometry{Dim, T} end """ - Mesh <: AbstractMesh{Element} -The concrete AbstractMesh type. + Mesh{PositionDim, PositionType, FaceType, VertexAttributeNames, VertexAttributeTypes, FaceVectorType} <: AbstractMesh{PositionDim, PositionType} <: AbstractGeometry{PositionDim, PositionType} + +The type of a concrete mesh. The associated struct contains 3 fields: + +```julia +struct Mesh{...} + vertex_attributes::NamedTuple{VertexAttributeNames, VertexAttributeTypes} + faces::FaceVectorType + views::Vector{UnitRange{Int}} +end +``` + +A vertex typically carries multiple distinct pieces of data, e.g. a position, +a normal, a texture coordinate, etc. We call those pieces of data vertex +attributes. The `vertex_attributes` field contains the name and a collection +`<: AbstractVector` or `<: FaceView` for each attribute. The n-th element of that +collection is the value of the corresponding attribute for the n-th vertex. + +```julia +# vertex 1 2 3 +vertex_attributes[:position] = [pos1, pos2, pos3, ...] +vertex_attributes[:normal] = [normal1, normal2, normal3, ...] +... +``` + +A `NamedTuple` is used here to allow different meshes to carry different vertex +attributes while also keeping things type stable. The constructor enforces a +few restrictions: +- The first attribute must be named `position` and must have a `Point{PositionDim, PositionType}` eltype. +- Each vertex attribute must refer to the same number of vertices. (All vertex attributes defined by +AbstractVector must match in length. For FaceViews, the number of faces needs to match.) + +See also: [`vertex_attributes`](@ref), [`coordinates`](@ref), [`normals`](@ref), +[`texturecoordinates`](@ref), [`decompose`](@ref), [`FaceView`](@ref), +[`clear_faceviews`](@ref) + +The `faces` field is a collection `<: AbstractVector{FaceType}` containing faces +that describe how vertices are connected. Typically these are `(GL)TriangleFace`s +or `QuadFace`s, but they can be any collection of vertex indices `<: AbstractFace`. + +See also: [`faces`](@ref), [`decompose`](@ref) + +The `views` field can be used to separate the mesh into mutliple submeshes. Each +submesh is described by a "view" into the `faces` vector, i.e. submesh n uses +`mesh.faces[mesh.views[n]]`. A `Mesh` can be constructed without `views`, which +results in an empty `views` vector. + +See also: [`merge`](@ref), [`split_mesh`](@ref) """ -struct Mesh{Dim, T<:Number, V<:AbstractVector{Point{Dim, T}}, C <: AbstractVector{<: AbstractFace}} <: AbstractMesh{Dim, T} - vertices::V - connectivity::C +struct Mesh{ + Dim, T <: Real, + FT <: AbstractFace, + Names, + VAT <: Tuple{<: AbstractVector{Point{Dim, T}}, Vararg{VertexAttributeType}}, + FVT <: AbstractVector{FT} + } <: AbstractMesh{Dim, T} + + vertex_attributes::NamedTuple{Names, VAT} + faces::FVT + views::Vector{UnitRange{Int}} + + function Mesh( + vertex_attributes::NamedTuple{Names, VAT}, + fs::FVT, + views::Vector{UnitRange{Int}} = UnitRange{Int}[] + ) where { + FT <: AbstractFace, FVT <: AbstractVector{FT}, Names, Dim, T, + VAT <: Tuple{<: AbstractVector{Point{Dim, T}}, Vararg{VertexAttributeType}} + } + + va = vertex_attributes + names = Names + + # verify type + if !haskey(va, :position ) + error("Vertex attributes must have a :position attribute.") + end + + if haskey(va, :normals) + @warn "`normals` as a vertex attribute name has been deprecated in favor of `normal` to bring it in line with mesh.position and mesh.uv" + names = ntuple(i -> ifelse(names[i] == :normal, :normal, names[i]), length(names)) + va = NamedTuple{names}(values(va)) + end + + # verify that all vertex attributes refer to the same number of vertices + # for Vectors this means same length + # for FaceViews this means same number of faces + N = maximum(f -> value(maximum(f)), fs, init = 0) + for (name, attrib) in pairs(va) + if attrib isa FaceView + try + verify(fs, attrib) + catch e + rethrow(ErrorException("Failed to verify $name attribute:\n$(e.msg)")) + end + else + length(attrib) < N && error("Failed to verify $name attribute:\nFaces address $N vertex attributes but only $(length(attrib)) are present.") + end + end + + return new{Dim, T, FT, names, VAT, FVT}(va, fs, views) + end +end + +@inline function Base.hasproperty(mesh::Mesh, field::Symbol) + if field === :normals + @warn "mesh.normals has been deprecated in favor of mesh.normal to bring it in line with mesh.position and mesh.uv" + return hasproperty(mesh, :normal) + end + return hasproperty(getfield(mesh, :vertex_attributes), field) || hasfield(Mesh, field) +end +@inline function Base.getproperty(mesh::Mesh, field::Symbol) + if hasfield(Mesh, field) + return getfield(mesh, field) + elseif field === :normals + @warn "mesh.normals has been deprecated in favor of mesh.normal to bring it in line with mesh.position and mesh.uv" + return getproperty(mesh, :normal) + else + return getproperty(getfield(mesh, :vertex_attributes), field) + end +end +@inline function Base.propertynames(mesh::Mesh) + return (fieldnames(Mesh)..., propertynames(getfield(mesh, :vertex_attributes))...) +end + +coordinates(mesh::Mesh) = mesh.position +faces(mesh::Mesh) = mesh.faces +normals(mesh::Mesh) = hasproperty(mesh, :normal) ? mesh.normal : nothing +texturecoordinates(mesh::Mesh) = hasproperty(mesh, :uv) ? mesh.uv : nothing + +""" + vertex_attributes(mesh::Mesh) + +Returns a dictionairy containing the vertex attributes of the given mesh. +Mutating these will change the mesh. +""" +vertex_attributes(mesh::Mesh) = getfield(mesh, :vertex_attributes) + +Base.getindex(mesh::Mesh, i::Integer) = mesh.position[mesh.faces[i]] +Base.length(mesh::Mesh) = length(mesh.faces) + +function Base.:(==)(a::Mesh, b::Mesh) + return (a.vertex_attributes == b.vertex_attributes) && + (faces(a) == faces(b)) && (a.views == b.views) end -coordinates(mesh::Mesh) = mesh.vertices -faces(mesh::Mesh) = mesh.connectivity -Base.getindex(mesh::Mesh, i::Integer) = mesh.vertices[mesh.connectivity[i]] -Base.length(mesh::Mesh) = length(mesh.connectivity) -Base.:(==)(a::Mesh, b::Mesh) = coordinates(a) == coordinates(b) && faces(a) == faces(b) function Base.iterate(mesh::Mesh, i=1) return i - 1 < length(mesh) ? (mesh[i], i + 1) : nothing end +function Base.convert(::Type{<: Mesh{D, T, FT}}, m::Mesh{D}) where {D, T <: Real, FT <: AbstractFace} + return mesh(m, pointtype = Point{D, T}, facetype = FT) +end + +""" + Mesh(faces[; views, attributes...]) + Mesh(positions, faces[; views]) + Mesh(positions, faces::AbstractVector{<: Integer}[; facetype = TriangleFace, skip = 1]) + Mesh(; attributes...) + +Constructs a mesh from the given arguments. + +If `positions` are given explicitly, they are merged with other vertex attributes +under the name `position`. Otherwise they must be part of `attributes`. If `faces` +are not given `attributes.position` must be a FaceView. + +Any other vertex attribute can be either an `AbstractVector` or a `FaceView` +thereof. Every vertex attribute that is an `AbstractVector` must be sufficiently +large to be indexable by `mesh.faces`. Every vertex attribute that is a `FaceView` +must contain similar faces to `mesh.faces`, i.e. contain the same number of faces +and have faces of matching length. + +`views` can be defined optionally to implicitly split the mesh into multi +sub-meshes. This is done by providing ranges for indexing faces which correspond +to the sub-meshes. By default this is left empty. +""" +function Mesh(faces::AbstractVector{<:AbstractFace}; views::Vector{UnitRange{Int}} = UnitRange{Int}[], attributes...) + return Mesh(NamedTuple(attributes), faces, views) +end + function Mesh(points::AbstractVector{Point{Dim, T}}, - faces::AbstractVector{<:AbstractFace}) where {Dim, T} - return Mesh{Dim, T, }(points, faces) + faces::AbstractVector{<:AbstractFace}; + views = UnitRange{Int}[], kwargs...) where {Dim, T} + va = (position = points, kwargs...) + return Mesh(va, faces, views) end function Mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Integer}, @@ -313,32 +682,87 @@ function Mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Integer}, return Mesh(points, connect(faces, facetype, skip)) end -struct MetaMesh{Dim, T, M <: AbstractMesh{Dim, T}, Names, Types} <: AbstractMesh{Dim, T} +function Mesh(; kwargs...) + fs = faces(kwargs[:position]::FaceView) + va = NamedTuple{keys(kwargs)}(map(keys(kwargs)) do k + return k == :position ? values(kwargs[k]) : kwargs[k] + end) + return Mesh(va, fs) +end + +# Shorthand types +const SimpleMesh{N, T, FT} = Mesh{N, T, FT, (:position,), Tuple{Vector{Point{N, T}}}, Vector{FT}} +const NormalMesh{N, T, FT} = Mesh{N, T, FT, (:position, :normal), Tuple{Vector{Point{N, T}}, Vector{Vec3f}}, Vector{FT}} +const NormalUVMesh{N, T, FT} = Mesh{N, T, FT, (:position, :normal, :uv), Tuple{Vector{Point{N, T}}, Vector{Vec3f}, Vector{Vec2f}}, Vector{FT}} + +const GLSimpleMesh{N} = SimpleMesh{N, Float32, GLTriangleFace} +const GLNormalMesh{N} = NormalMesh{N, Float32, GLTriangleFace} +const GLNormalUVMesh{N} = NormalUVMesh{N, Float32, GLTriangleFace} + + + +struct MetaMesh{Dim, T, M <: AbstractMesh{Dim, T}} <: AbstractMesh{Dim, T} mesh::M - meta::NamedTuple{Names, Types} - function MetaMesh(mesh::AbstractMesh{Dim, T}, meta::NamedTuple{Names, Types}) where {Dim, T, Names, Types} - new{Dim, T, typeof(mesh), Names, Types}(mesh, meta) - end + meta::Dict{Symbol, Any} end -function MetaMesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:AbstractFace}; meta...) - MetaMesh(Mesh(points, faces), values(meta)) +""" + MetaMesh(mesh; metadata...) + MetaMesh(positions, faces; metadata...) + +Constructs a MetaMesh either from another `mesh` or by constructing another mesh +with the given `positions` and `faces`. Any keyword arguments given will be +stored in the `meta` field in `MetaMesh`. + +This struct is meant to be used for storage of non-vertex data. Any vertex +related data should be stored as a vertex attribute in `Mesh`. + +The metadata added to the MetaMesh can be manipulated with Dict-like operations +(getindex, setindex!, get, delete, keys, etc). Vertex attributes can be accessed +via fields and the same getters as mesh. The mesh itself can be retrieved with +`Mesh(metamesh)`. +""" +function MetaMesh(mesh::AbstractMesh; kwargs...) + MetaMesh(mesh, Dict{Symbol, Any}(kwargs)) end -function MetaMesh(m::AbstractMesh; kw...) - MetaMesh(Mesh(m), merge(meta(m), values(kw))) +function MetaMesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:AbstractFace}; kwargs...) + MetaMesh(Mesh(points, faces), Dict{Symbol, Any}(kwargs)) +end + + +@inline function Base.hasproperty(mesh::MetaMesh, field::Symbol) + return hasfield(MetaMesh, field) || hasproperty(getfield(mesh, :mesh), field) +end +@inline function Base.getproperty(mesh::MetaMesh, field::Symbol) + if hasfield(MetaMesh, field) + return getfield(mesh, field) + else + return getproperty(getfield(mesh, :mesh), field) + end +end +@inline function Base.propertynames(mesh::MetaMesh) + return (fieldnames(MetaMesh)..., propertynames(getfield(mesh, :mesh))...) end -@inline Base.hasproperty(mesh::MetaMesh, field::Symbol) = hasproperty(getfield(mesh, :meta), field) -@inline Base.getproperty(mesh::MetaMesh, field::Symbol) = getproperty(getfield(mesh, :meta), field) -@inline Base.propertynames(mesh::MetaMesh) = propertynames(getfield(mesh, :meta)) +# TODO: or via getindex? +Base.haskey(mesh::MetaMesh, key::Symbol) = haskey(getfield(mesh, :meta), key) +Base.get(f::Base.Callable, mesh::MetaMesh, key::Symbol) = get(f, getfield(mesh, :meta), key) +Base.get!(f::Base.Callable, mesh::MetaMesh, key::Symbol) = get!(f, getfield(mesh, :meta), key) +Base.get(mesh::MetaMesh, key::Symbol, default) = get(getfield(mesh, :meta), key, default) +Base.get!(mesh::MetaMesh, key::Symbol, default) = get!(getfield(mesh, :meta), key, default) +Base.getindex(mesh::MetaMesh, key::Symbol) = getindex(getfield(mesh, :meta), key) +Base.setindex!(mesh::MetaMesh, value, key::Symbol) = setindex!(getfield(mesh, :meta), value, key) +Base.delete!(mesh::MetaMesh, key::Symbol) = delete!(getfield(mesh, :meta), key) +Base.keys(mesh::MetaMesh) = keys(getfield(mesh, :meta)) coordinates(mesh::MetaMesh) = coordinates(Mesh(mesh)) faces(mesh::MetaMesh) = faces(Mesh(mesh)) -normals(mesh::MetaMesh) = hasproperty(mesh, :normals) ? mesh.normals : nothing -texturecoordinates(mesh::MetaMesh) = hasproperty(mesh, :uv) ? mesh.uv : nothing +normals(mesh::MetaMesh) = normals(Mesh(mesh)) +texturecoordinates(mesh::MetaMesh) = texturecoordinates(Mesh(mesh)) +vertex_attributes(mesh::MetaMesh) = vertex_attributes(Mesh(mesh)) meta(@nospecialize(m)) = NamedTuple() meta(mesh::MetaMesh) = getfield(mesh, :meta) Mesh(mesh::MetaMesh) = getfield(mesh, :mesh) -Mesh(mesh::Mesh) = mesh +Mesh(mesh::Mesh) = mesh \ No newline at end of file diff --git a/src/boundingboxes.jl b/src/boundingboxes.jl index e2deeb32..447a7228 100644 --- a/src/boundingboxes.jl +++ b/src/boundingboxes.jl @@ -3,7 +3,9 @@ function Rect(geometry::AbstractArray{<:Point{N,T}}) where {N,T} end """ -Construct a HyperRectangle enclosing all points. + Rect(points::AbstractArray{<: Point}) + +Construct a bounding box countaining all the given points. """ function Rect{N1,T1}(geometry::AbstractArray{PT}) where {N1,T1,PT<:Point} N2, T2 = length(PT), eltype(PT) @@ -11,7 +13,7 @@ function Rect{N1,T1}(geometry::AbstractArray{PT}) where {N1,T1,PT<:Point} vmin = Point{N2,T2}(typemax(T2)) vmax = Point{N2,T2}(typemin(T2)) for p in geometry - vmin, vmax = minmax(p, vmin, vmax) + vmin, vmax = _minmax(p, vmin, vmax) end o = vmin w = vmax - vmin @@ -23,6 +25,11 @@ function Rect{N1,T1}(geometry::AbstractArray{PT}) where {N1,T1,PT<:Point} end end +""" + Rect(primitive::GeometryPrimitive) + +Construct a bounding box for the given primitive. +""" function Rect(primitive::GeometryPrimitive{N,T}) where {N,T} return Rect{N,T}(primitive) end diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index 3753060a..341b8ec2 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -1,260 +1,148 @@ -using LinearAlgebra -import Random -import Base: setindex - -abstract type StaticVector{N, T} <: AbstractVector{T} end -function similar_type end - -struct StaticArrayStyle{T, AsConst} <: Broadcast.BroadcastStyle end -StaticArrayStyle{T}() where T = StaticArrayStyle{T, false}() +function unit(::Type{T}, i::Integer) where {T <: StaticVector} + tup = ntuple(Val(length(T))) do j + return ifelse(i == j, 1, 0) + end + return T(tup) +end macro fixed_vector(name_parent) @assert name_parent.head == :(=) - VecT, SuperT = name_parent.args - + name, parent = name_parent.args expr = quote - struct $(VecT){N, T} <: $(SuperT){N, T} - data::NTuple{N,T} + struct $(name){S,T} <: $(parent){S,T} + data::NTuple{S,T} - function $(VecT){N,T}(x::NTuple{N,T}) where {N,T} - return new{N,T}(x) + function $(name){S,T}(x::NTuple{S,T}) where {S,T} + return new{S,T}(x) end - function $(VecT){N,T}(x::NTuple{N,Any}) where {N,T} - return new{N,T}(convert(NTuple{N, T}, x)) + function $(name){S,T}(x::NTuple{S,Any}) where {S,T} + return new{S,T}(StaticArrays.convert_ntuple(T, x)) end - $(VecT){1, T}(x::Tuple{T}) where {T} = new{1, T}(x) end + size_or(::Type{$(name)}, or) = or + eltype_or(::Type{$(name)}, or) = or + eltype_or(::Type{$(name){S,T} where S}, or) where {T} = T + eltype_or(::Type{$(name){S,T} where T}, or) where {S} = or + eltype_or(::Type{$(name){S,T}}, or) where {S,T} = T + + size_or(::Type{$(name){S,T} where S}, or) where {T} = or + size_or(::Type{$(name){S,T} where T}, or) where {S} = Size{(S,)}() + size_or(::Type{$(name){S,T}}, or) where {S,T} = (S,) + # Array constructor - $(VecT)(x::AbstractVector) = error("You need to supply size of vector") - $(VecT){N}(x::AbstractVector{T}) where {N,T} = $(VecT){N,T}(x) - function $(VecT){N,T1}(x::AbstractVector{T2}) where {N,T1,T2} - @assert N <= length(x) - return $(VecT){N,T1}(ntuple(i -> convert(T1, x[i]), N)) + function $(name){S}(x::AbstractVector{T}) where {S,T} + @assert S <= length(x) + return $(name){S,T}(ntuple(i -> x[i], Val(S))) end - # StaticVector conversion - $(VecT)(x::StaticVector{N, T}) where {N,T} = $(VecT){N, T}(x) - $(VecT){N}(x::StaticVector{N2, T}) where {N,N2,T} = $(VecT){N,T}(x) - function $(VecT){N1,T1}(x::StaticVector{N2, T2}) where {N1,T1,N2,T2} - @assert N1 <= N2 - return $(VecT){N1,T1}(ntuple(i -> convert(T1, x[i]), N1)) - end - function $(VecT){1,T1}(x::StaticVector{N2, T2}) where {T1,N2,T2} - @assert 1 <= N2 - return $(VecT){1,T1}(ntuple(i -> convert(T1, x[i]), 1)) + function $(name){S,T1}(x::AbstractVector{T2}) where {S,T1,T2} + @assert S <= length(x) + return $(name){S,T1}(ntuple(i -> convert(T1, x[i]), Val(S))) end - # repeat - $(VecT){N}(x::T) where {N,T<:Number} = $(VecT){N, T}(x) - function $(VecT){N,T}(x::Number) where {N,T} - return $(VecT){N,T}(ntuple(i -> convert(T, x), N)) + function $(name){S,T}(x) where {S,T} + return $(name){S,T}(ntuple(i -> convert(T, x), Val(S))) end - $(VecT){1, T}(x::Number) where {T} = $(VecT){1, T}((x,)) - $(VecT){1, T}(x::Tuple{Any}) where T = $(VecT){1, T}((T(x[1]),)) - $(VecT)(x::Tuple) = $(VecT)(promote(x...)) - $(VecT){N}(x::Tuple) where {N} = $(VecT){N}(promote(x...)) - $(VecT){N, T}(x::Tuple) where {N,T} = $(VecT){N,T}(convert(NTuple{N,T}, x)) + $(name){S}(x::T) where {S,T} = $(name){S,T}(ntuple(i -> x, Val(S))) + $(name){1,T}(x::T) where {T} = $(name){1,T}((x,)) + $(name)(x::NTuple{S}) where {S} = $(name){S}(x) + function $(name)(x::T) where {S,T <: Tuple{Vararg{Any,S}}} + return $(name){S,StaticArrays.promote_tuple_eltype(T)}(x) + end - $(VecT)(x::NTuple{N, T}) where {N, T} = $(VecT){N,T}(x) - $(VecT){N}(x::NTuple{N, T}) where {N, T} = $(VecT){N,T}(x) + function $(name){S}(x::T) where {S,T <: Tuple} + return $(name){S,StaticArrays.promote_tuple_eltype(T)}(x) + end + $(name){S,T}(x::StaticVector) where {S,T} = $(name){S,T}(Tuple(x)) - $(VecT)(x::Vararg{Any,N}) where {N} = $(VecT){N}(x) - $(VecT)(x::Vararg{T,N}) where {T,N} = $(VecT){N,T}(x) + @generated function (::Type{$(name){S,T}})(x::$(name)) where {S,T} + idx = [:(x[$i]) for i in 1:S] + return quote + $($(name)){S,T}($(idx...)) + end + end - $(VecT){N}(x::Vararg{Any,N}) where {N} = $(VecT){N}(x) - $(VecT){N}(x::Vararg{T,N}) where {T,N} = $(VecT){N,T}(x) + @generated function Base.convert(::Type{$(name){S,T}}, x::$(name)) where {S,T} + idx = [:(x[$i]) for i in 1:S] + return quote + $($(name)){S,T}($(idx...)) + end + end - $(VecT){N, T}(x::Vararg{Any,N}) where {T,N} = $(VecT){N,T}(x) - $(VecT){N, T1}(x::Vararg{T2,N}) where {T1,T2,N} = $(VecT){N, T1}(x) + @generated function (::Type{SV})(x::StaticVector) where {SV <: $(name)} + len = size_or(SV, size(x))[1] + return if length(x) == len + :(SV(Tuple(x))) + elseif length(x) > len + elems = [:(x[$i]) for i in 1:len] + :(SV($(Expr(:tuple, elems...)))) + else + error("Static Vector too short: $x, target type: $SV") + end + end - Base.convert(::Type{$(VecT){N,T}}, x) where {N,T} = $(VecT){N,T}(x) - Base.convert(::Type{$(VecT){N}}, x) where {N} = $(VecT){N}(x) - Base.convert(::Type{$(VecT){N}}, x::$(VecT){N}) where {N} = x - Base.convert(::Type{$(VecT){N,T}}, x::$(VecT){N,T}) where {N,T} = x + @generated function $(name){S}(x::StaticVector{N, T}) where {S, N, T} + SV = $(name){S, T} + len = size_or(SV, size(x))[1] + return if length(x) == len + :($(SV)(Tuple(x))) + elseif length(x) > len + elems = [:(x[$i]) for i in 1:len] + :($(SV)($(Expr(:tuple, elems...)))) + else + error("Static Vector too short: $x, target type: $SV") + end + end + Base.@propagate_inbounds function Base.getindex(v::$(name){S,T}, i::Int) where {S,T} + return v.data[i] + end - function Base.convert(::Type{$(VecT){N,T}}, x::NTuple{N,T}) where {N,T} - return $(VecT){N,T}(x) + Base.Tuple(v::$(name)) = v.data + function Base.convert(::Type{$(name){S,T}}, x::NTuple{S,T}) where {S,T} + return $(name){S,T}(x) end - function Base.convert(::Type{$(VecT){N,T}}, x::Tuple) where {N,T} - return $(VecT){N,T}(convert(NTuple{N,T}, x)) + function Base.convert(::Type{$(name){S,T}}, x::Tuple) where {S,T} + return $(name){S,T}(convert(NTuple{S,T}, x)) end - Base.convert(::Type{$(VecT)}, x::Tuple) = $(VecT)(x) - - @inline similar_type(::$(VecT){N, T}, n::Integer) where {N, T} = $(VecT){n} - @inline similar_type(::$(VecT){N}, ::Type{T}) where {N, T} = $(VecT){N, T} - @inline similar_type(::$(VecT), n::Integer, ::Type{T}) where {T} = $(VecT){n, T} - @inline similar_type(::$(VecT)) = $(VecT) - - Base.BroadcastStyle(::Type{<: $(VecT)}) = StaticArrayStyle{$(VecT)}() - Base.values(v::$(VecT)) = v.data - function LinearAlgebra.cross(a::$(VecT){3}, b::$(VecT){3}) - @inbounds elements = (a[2]*b[3]-a[3]*b[2], - a[3]*b[1]-a[1]*b[3], - a[1]*b[2]-a[2]*b[1]) - return $(VecT)(elements) + @generated function StaticArrays.similar_type(::Type{SV}, ::Type{T}, + s::Size{S}) where {SV <: $(name),T,S} + return if length(S) === 1 + $(name){S[1],T} + else + StaticArrays.default_similar_type(T, s(), Val{length(S)}) + end end - end - return esc(expr) -end -# Broadcasting -# style rules -Base.BroadcastStyle(::StaticArrayStyle{T, B1}, ::StaticArrayStyle{T, B2}) where {B1, B2, T} = StaticArrayStyle{T, B1 || B2}() -Base.BroadcastStyle(s::StaticArrayStyle, ::Broadcast.AbstractArrayStyle{0}) = s -Base.BroadcastStyle(::Broadcast.AbstractArrayStyle{0}, s::StaticArrayStyle) = s -Base.BroadcastStyle(s::StaticArrayStyle, ::Broadcast.Style{Tuple}) = s -Base.BroadcastStyle(::Broadcast.Style{Tuple}, s::StaticArrayStyle) = s -Base.BroadcastStyle(::StaticArrayStyle{T, B}, ::Broadcast.BroadcastStyle) where {B, T} = StaticArrayStyle{T, true}() -Base.BroadcastStyle(::Broadcast.BroadcastStyle, ::StaticArrayStyle{T, B}) where {B, T} = StaticArrayStyle{T, true}() -# to allow mixing types, define: -# Base.BroadcastStyle(::StaticArrayStyle{<: Type1, B1}, ::StaticArrayStyle{<: Type2, B2}) where {B1, B2} = -# StaticArrayStyle{preffered_type, B1 || B2}() -# Base.BroadcastStyle(::StaticArrayStyle{<: Type2, B1}, ::StaticArrayStyle{<: Type1, B2}) where {B1, B2} = -# StaticArrayStyle{preffered_type, B1 || B2}() - -# If we don't inherit from AbstractVector we need this? -# Base.broadcastable(x::StaticVector) = x - -# Required to avoid size missmatches between Array and StaticVector -function Broadcast.instantiate(bc::Broadcast.Broadcasted{<: StaticArrayStyle{<: Any, true}}) - # transform this to an Array broadcast with Ref'd StaticVectors and tuples - args_converted = map(arg -> arg isa Broadcast.Broadcasted ? copy(Broadcast.instantiate(arg)) : arg, bc.args) - maybe_const_args = map(args_converted) do arg - style = Base.BroadcastStyle(typeof(arg)) - if style isa Broadcast.AbstractArrayStyle # value or Array - return arg - else # tuple, StaticVector - return Ref(arg) + Base.:(*)(a::$name, b::$name) = a .* b + function Base.broadcasted(f, a::AbstractArray{T}, b::$name) where {T <: $name} + return Base.broadcasted(f, a, (b,)) end end - return Broadcast.broadcasted(bc.f, maybe_const_args...) -end - -# resolve element-wise operation -function Base.copy(bc::Broadcast.Broadcasted{StaticArrayStyle{T, false}}) where T - # Broadcasted may end up in args from nested calls (e.g. foo(a, b .+ c); a .+ b .+ c) - args = map(arg -> values(arg isa Broadcast.Broadcasted ? copy(arg) : arg), bc.args) - return T(broadcast(bc.f, args...)) -end - -Base.map(f, a::StaticVector, args::AbstractArray...) = broadcast(f, a, args...) -Base.map(f, a::AbstractArray, b::StaticVector, args::AbstractArray...) = broadcast(f, a, b, args...) -Base.map(f, a::StaticVector, b::StaticVector, args::AbstractArray...) = broadcast(f, a, b, args...) - -function Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{V}) where V <: StaticVector{N,T} where {N, T} - V(ntuple(i-> rand(rng, T), N)) -end -function Random.randn(rng::Random.AbstractRNG, ::Type{V}) where V <: StaticVector{N,T} where {N, T} - V(ntuple(i-> randn(rng, T), N)) -end - -Base.@propagate_inbounds function Base.getindex(v::StaticVector{N,T}, i::Integer) where {N,T} - return v.data[i] -end - -Base.setindex(c::V, v, i::Integer) where {V <: StaticVector} = V(Base.setindex(c.data, v, i)) - -Base.@propagate_inbounds function Base.getindex(a::StaticVector{N1, T}, idx::StaticVector{N, <:Integer}) where {N,N1,T} - return similar_type(idx, N, T)(map(i-> a[i], idx)) -end - -Base.:(-)(a::StaticVector) = (-).(a) - -import Base: *, +, -, / - -for op in [:*, :+, :-, :/] - @eval begin - ($op)(a::StaticVector, b::StaticVector) = Base.broadcast($(op), a, b) - ($op)(a::Number, b::StaticVector) = Base.broadcast($(op), a, b) - ($op)(a::StaticVector, b::Number) = Base.broadcast($(op), a, b) - end -end - -LinearAlgebra.cross(a::StaticVector{2}, b::StaticVector{2}) = a[1]*b[2]-a[2]*b[1] -LinearAlgebra.norm(a::StaticVector) = sqrt(dot(a,a)) -LinearAlgebra.normalize(a::StaticVector) = a ./ norm(a) - -Base.eltype(::StaticVector{N, T}) where {N, T} = T -Base.eltype(::Type{<: StaticVector{N, T}}) where {N, T} = T - -Base.size(::StaticVector{N}) where {N} = (N,) -Base.size(::Type{<: StaticVector{N}}) where {N} = (N,) -Base.length(::StaticVector{N}) where {N} = N -Base.length(::Type{<: StaticVector{N}}) where {N} = N -Base.ndims(::Type{<: StaticVector}) = 1 - -Base.copy(v::StaticVector) = deepcopy(v) - -Base.vcat(a::StaticVector, b::StaticVector) = (a..., b...) - -function Base.iterate(A::StaticVector, i=1) - i - 1 < length(A) ? (A[i], i + 1) : nothing -end - -function unit(::Type{T}, i::Integer) where {T <: StaticVector} - tup = ntuple(Val(length(T))) do j - return ifelse(i == j, 1, 0) - end - return T(tup) -end - -Base.zero(::Type{V}) where {V <:StaticVector} = V(0) -Base.zero(::V) where {V <:StaticVector} = zero(V) - -function Base.:(==)(a::StaticVector{N}, b::StaticVector{N}) where N - for i in 1:N - a[i] == b[i] || return false - end - return true -end - -function Base.isapprox( - a::StaticVector{N1, T1}, b::StaticVector{N2, T2}; - atol::Real = 0, - rtol::Real = atol > 0 ? 0 : sqrt(max(eps(T1), eps(T2))) - ) where {N1, N2, T1, T2} - return (N1 == N2) && norm(a - b) <= max(atol, rtol * max(norm(a), norm(b))) -end - -@generated function Base.transpose(b::StaticVector{N,T}) where {N,T} - expr = [:(transpose(b[$i])) for i=1:N] - return quote - Mat{1,N,T}($(expr...)) - end + return esc(expr) end -Base.reverse(x::P) where P <: StaticVector = P(reverse(x.data)) -# Since we don't inherit from AbstractArray, some extra functions need to be overloaded -LinearAlgebra.promote_leaf_eltypes(x::StaticVector{N, T}) where {N,T} = T +abstract type AbstractPoint{Dim,T} <: StaticVector{Dim,T} end -@fixed_vector Point = StaticVector +@fixed_vector Point = AbstractPoint @fixed_vector Vec = StaticVector -Base.lastindex(::StaticVector{N}) where N = N - -# Allow mixing Point Vec in broadcast -Base.BroadcastStyle(::StaticArrayStyle{<: Point, B1}, ::StaticArrayStyle{<: Vec, B2}) where {B1, B2} = - StaticArrayStyle{Point, B1 || B2}() -Base.BroadcastStyle(::StaticArrayStyle{<: Vec, B1}, ::StaticArrayStyle{<: Point, B2}) where {B1, B2} = - StaticArrayStyle{Point, B1 || B2}() - -Base.:(+)(a::Vec{N}, b::Point{N}) where {N} = Point{N}(a.data .+ b.data) -const VecTypes{N,T} = Union{StaticVector{N,T}, NTuple{N,T}} -const Vecf{N} = Vec{N, Float32} +const Mat = SMatrix +const VecTypes{N,T} = Union{StaticVector{N,T},NTuple{N,T}} +const Vecf{N} = Vec{N,Float32} const PointT{T} = Point{N,T} where N const Pointf{N} = Point{N,Float32} -Base.isnan(p::Union{Point,Vec}) = any(isnan, p) -Base.isinf(p::Union{Point,Vec}) = any(isinf, p) -Base.isfinite(p::Union{Point,Vec}) = all(isfinite, p) +Base.isnan(p::Union{AbstractPoint,Vec}) = any(isnan, p) +Base.isinf(p::Union{AbstractPoint,Vec}) = any(isinf, p) +Base.isfinite(p::Union{AbstractPoint,Vec}) = all(isfinite, p) ## Generate aliases ## As a text file instead of eval/macro, to not confuse code linter @@ -280,8 +168,64 @@ open(joinpath(@__DIR__, "generated-aliases.jl"), "w") do io end =# -include("mat.jl") include("generated-aliases.jl") export Mat, Vec, Point, unit export Vecf, Pointf + +""" + Vec{N, T}(args...) + Vec{N, T}(args::Union{AbstractVector, Tuple, NTuple, StaticVector}) + +Constructs a Vec of length `N` from the given arguments. + +Note that Point and Vec don't follow strict mathematical definitions. Instead +we allow them to be used interchangeably. + +## Aliases + +| |`T` |`Float64` |`Float32` |`Int` |`UInt` | +|--------|------------|----------|----------|----------|----------| +|`N` |`Vec{N,T}` |`Vecd{N}` |`Vecf{N}` |`Veci{N}` |`Vecui{N}`| +|`2` |`Vec2{T}` |`Vec2d` |`Vec2f` |`Vec2i` |`Vec2ui` | +|`3` |`Vec3{T}` |`Vec3d` |`Vec3f` |`Vec3i` |`Vec3ui` | +""" +Vec + + +""" + Point{N, T}(args...) + Point{N, T}(args::Union{AbstractVector, Tuple, NTuple, StaticVector}) + +Constructs a Point of length `N` from the given arguments. + +Note that Point and Vec don't follow strict mathematical definitions. Instead +we allow them to be used interchangeably. + +## Aliases + +| |`T` |`Float64` |`Float32` |`Int` |`UInt` | +|--------|------------|----------|----------|----------|----------| +|`N` |`Point{N,T}`|`Pointd{N}`|`Pointf{N}`|`Pointi{N}`|`Pointui{N}`| +|`2` |`Point2{T}` |`Point2d` |`Point2f` |`Point2i` |`Point2ui`| +|`3` |`Point3{T}` |`Point3d` |`Point3f` |`Point3i` |`Point3ui`| +""" +Point + +""" + Mat{R, C, T[, L]}(args::Union{UniformScaling, Tuple, AbstractMatrix}) + Mat{R, C}(args::Union{Tuple, AbstractMatrix}) + Mat{C}(args::Tuple) + +Constructs a static Matrix from the given inputs. Can also take multiple numeric +args. If only one size is given the matrix is assumed to be square. + +### Aliases + +| |`T` |`Float64` |`Float32` |`Int` |`UInt` | +|--------|------------|----------|----------|----------|----------| +|`N` |`Mat{N,T}` |`Matd{N}` |`Matf{N}` |`Mati{N}` |`Matui{N}`| +|`2` |`Mat2{T}` |`Mat2d` |`Mat2f` |`Mat2i` |`Mat2ui` | +|`3` |`Mat3{T}` |`Mat3d` |`Mat3f` |`Mat3i` |`Mat3ui` | +""" +Mat \ No newline at end of file diff --git a/src/geointerface.jl b/src/geointerface.jl index ef66143b..f806fc27 100644 --- a/src/geointerface.jl +++ b/src/geointerface.jl @@ -105,6 +105,11 @@ function GeoInterface.convert(::Type{Point}, type::PointTrait, geom) end end +# without a function barrier you get a lot of allocations from runtime types +function _collect_with_type(::Type{PT}, geom) where {PT <: Point{2}} + return [PT(GeoInterface.x(p), GeoInterface.y(p)) for p in getgeom(geom)] +end + function GeoInterface.convert(::Type{LineString}, type::LineStringTrait, geom) g1 = getgeom(geom, 1) x, y = GeoInterface.x(g1), GeoInterface.y(g1) @@ -114,7 +119,7 @@ function GeoInterface.convert(::Type{LineString}, type::LineStringTrait, geom) return LineString([Point{3,T}(GeoInterface.x(p), GeoInterface.y(p), GeoInterface.z(p)) for p in getgeom(geom)]) else T = promote_type(typeof(x), typeof(y)) - return LineString([Point{2,T}(GeoInterface.x(p), GeoInterface.y(p)) for p in getgeom(geom)]) + return LineString(_collect_with_type(Point{2, T}, geom)) end end diff --git a/src/geometry_primitives.jl b/src/geometry_primitives.jl index 36b25d69..7f60e481 100644 --- a/src/geometry_primitives.jl +++ b/src/geometry_primitives.jl @@ -8,6 +8,11 @@ end ## # conversion & decompose +""" + convert_simplex(::Type{TargetType}, x) + +Used to convert one object into another in `decompose(::Type{TargetType}, xs)`. +""" convert_simplex(::Type{T}, x::T) where {T} = (x,) convert_simplex(::Type{Vec{N, T}}, x::Vec{N, T}) where {N, T} = x @@ -38,14 +43,19 @@ Triangulate an N-Face into a tuple of triangular faces. return v end +# TODO: generic? +function convert_simplex(::Type{TriangleFace{T}}, f::SimplexFace{4}) where {T} + TF = TriangleFace{T} + return (TF(f[2],f[3],f[4]), TF(f[1],f[3],f[4]), TF(f[1],f[2],f[4]), TF(f[1],f[2],f[3])) +end + """ convert_simplex(::Type{Face{2}}, f::Face{N}) Extract all line segments in a Face. """ -@generated function convert_simplex(::Type{LineFace{T}}, - f::Union{SimplexFace{N},NgonFace{N}}) where {T,N} - 2 <= N || error("decompose not implented for N <= 2 yet. N: $N")# other wise degenerate +@generated function convert_simplex(::Type{LineFace{T}}, f::NgonFace{N}) where {T,N} + 2 <= N || error("decompose not implemented for N <= 2 yet. N: $N")# other wise degenerate v = Expr(:tuple) for i in 1:(N - 1) @@ -56,6 +66,18 @@ Extract all line segments in a Face. return v end +@generated function convert_simplex(::Type{LineFace{T}}, f::SimplexFace{N}) where {T,N} + 2 <= N || error("decompose not implemented for N <= 2 yet. N: $N")# other wise degenerate + + v = Expr(:tuple) + for i in 1:(N - 1) + for j in i+1:N + push!(v.args, :(LineFace{$T}(f[$i], f[$j]))) + end + end + return v +end + to_pointn(::Type{T}, x) where {T<:Point} = convert_simplex(T, x)[1] # disambiguation method overlords @@ -73,9 +95,22 @@ end collect_with_eltype(::Type{T}, vec::Vector{T}) where {T} = vec collect_with_eltype(::Type{T}, vec::AbstractVector{T}) where {T} = collect(vec) +collect_with_eltype(::Type{T}, vec::FaceView{T}) where {T} = vec function collect_with_eltype(::Type{T}, iter) where {T} - isempty(iter) && return T[] + return collect_with_eltype!(Vector{T}(undef, 0), iter) +end +function collect_with_eltype(::Type{T}, iter::FaceView) where {T} + return FaceView(collect_with_eltype!(Vector{T}(undef, 0), iter.data), iter.faces) +end + +function collect_with_eltype!(target::AbstractVector{T}, vec::AbstractVector{T}) where {T} + return append!(target, vec) +end + +function collect_with_eltype!(result::AbstractVector{T}, iter) where {T} + isempty(iter) && return result + # We need to get `eltype` information from `iter`, it seems to be `Any` # most of the time so the eltype checks here don't actually work l = if Base.IteratorSize(iter) isa Union{Base.HasShape,Base.HasLength} @@ -90,53 +125,53 @@ function collect_with_eltype(::Type{T}, iter) where {T} else 0 end - n = 0 - result = Vector{T}(undef, l) + + # Allow result to be pre-filled for handling faces with mesh.views + sizehint!(result, length(result) + l) + for element in iter # convert_simplex always returns a tuple, # so that e.g. convert(Triangle, quad) can return 2 elements for telement in convert_simplex(T, element) - n += 1 - if n > l - push!(result, telement) - else - result[n] = telement - end + push!(result, telement) end end return result end """ -The unnormalized normal of three vertices. + orthogonal_vector(p1, p2, p3) + +Calculates an orthogonal vector `cross(p2 - p1, p3 - p1)` to a plane described +by 3 points p1, p2, p3. """ -function orthogonal_vector(v1, v2, v3) - a = v2 .- v1 - b = v3 .- v1 - return cross(a, b) -end +orthogonal_vector(p1, p2, p3) = cross(p2 - p1, p3 - p1) +orthogonal_vector(::Type{VT}, p1, p2, p3) where {VT} = orthogonal_vector(VT(p1), VT(p2), VT(p3)) """ -``` -normals{VT,FD,FT,FO}(vertices::Vector{Point{3, VT}}, - faces::Vector{Face{FD,FT,FO}}, - NT = Normal{3, VT}) -``` -Compute all vertex normals. + normals(positions::Vector{Point3{T}}, faces::Vector{<: NgonFace}[; normaltype = Vec3{T}]) + +Compute vertex normals from the given `positions` and `faces`. + +This runs through all faces, computing a face normal each and adding it to every +involved vertex. The direction of the face normal is based on winding direction +and assumed counter-clockwise faces. At the end the summed face normals are +normalized again to produce a vertex normal. """ function normals(vertices::AbstractVector{Point{3,T}}, faces::AbstractVector{F}; normaltype=Vec{3,T}) where {T,F<:NgonFace} return normals(vertices, faces, normaltype) end -function normals(vertices::AbstractVector{<:Point{3}}, faces::AbstractVector{F}, - ::Type{NormalType}) where {F<:NgonFace,NormalType} +function normals(vertices::AbstractVector{<:Point{3}}, faces::AbstractVector{<: NgonFace}, + ::Type{NormalType}) where {NormalType} + normals_result = zeros(NormalType, length(vertices)) for face in faces v = vertices[face] # we can get away with two edges since faces are planar. - n = orthogonal_vector(v[1], v[2], v[3]) - for i in 1:length(F) + n = orthogonal_vector(NormalType, v[1], v[2], v[3]) + for i in 1:length(face) fi = face[i] normals_result[fi] = normals_result[fi] .+ n end @@ -144,3 +179,38 @@ function normals(vertices::AbstractVector{<:Point{3}}, faces::AbstractVector{F}, normals_result .= normalize.(normals_result) return normals_result end + + +""" + face_normals(positions::Vector{Point3{T}}, faces::Vector{<: NgonFace}[, target_type = Vec3{T}]) + +Compute face normals from the given `positions` and `faces` and returns an +appropriate `FaceView`. +""" +function face_normals( + positions::AbstractVector{<:Point3{T}}, fs::AbstractVector{<: AbstractFace}; + normaltype = Vec3{T}) where {T} + return face_normals(positions, fs, normaltype) +end + +@generated function face_normals(positions::AbstractVector{<:Point3}, fs::AbstractVector{F}, + ::Type{NormalType}) where {F<:NgonFace,NormalType} + + # If the facetype is not concrete it likely varies and we need to query it + # doing the iteration + FT = ifelse(isconcretetype(F), :($F), :(typeof(f))) + + quote + normals = resize!(NormalType[], length(fs)) + faces = resize!(F[], length(fs)) + + for (i, f) in enumerate(fs) + ps = positions[f] + n = orthogonal_vector(NormalType, ps[1], ps[2], ps[3]) + normals[i] = normalize(n) + faces[i] = $(FT)(i) + end + + return FaceView(normals, faces) + end +end diff --git a/src/interfaces.jl b/src/interfaces.jl index bed84d4b..dfbd30e3 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -1,8 +1,11 @@ """ coordinates(geometry) -Returns the edges/vertices/coordinates of a geometry. Is allowed to return lazy iterators! -Use `decompose(ConcretePointType, geometry)` to get `Vector{ConcretePointType}` with -`ConcretePointType` to be something like `Point{3, Float32}`. + +Returns the positions/coordinates of a geometry. + +This is allowed to return lazy iterators. Use `decompose(ConcretePointType, geometry)` +to get a `Vector{ConcretePointType}` with `ConcretePointType` being something like +`Point3f`. """ function coordinates(points::AbstractVector{<:Point}) return points @@ -10,14 +13,24 @@ end """ faces(geometry) -Returns the face connections of a geometry. Is allowed to return lazy iterators! -Use `decompose(ConcreteFaceType, geometry)` to get `Vector{ConcreteFaceType}` with -`ConcreteFaceType` to be something like `TriangleFace{Int}`. + +Returns the faces of a geometry. + +This is allowed to return lazy iterators. Use `decompose(ConcreteFaceType, geometry)` +to get a `Vector{ConcreteFaceType}` with `ConcreteFaceType` being something like `GLTriangleFace`. """ function faces(f::AbstractVector{<:AbstractFace}) return f end +""" + normals(primitive) + +Returns the normals of a geometry. + +This is allowed to return lazy iterators. Use `decompose_normals(ConcreteVecType, geometry)` +to get a `Vector{ConcreteVecType}` with `ConcreteVecType` being something like `Vec3f`. +""" function normals(primitive, nvertices=nothing; kw...) # doesn't have any specific algorithm to generate normals # so will be generated from faces + positions @@ -33,14 +46,25 @@ function faces(primitive, nvertices=nothing; kw...) return nothing end +""" + texturecoordinates(primitive) + +Returns the texturecoordinates of a geometry. + +This is allowed to return lazy iterators. Use `decompose_uv(ConcreteVecType, geometry)` +(or `decompose_uvw`) to get a `Vector{ConcreteVecType}` with `ConcreteVecType` being +something like `Vec2f`. +""" texturecoordinates(primitive, nvertices=nothing) = nothing """ Tesselation(primitive, nvertices) -For abstract geometries, when we generate -a mesh from them, we need to decide how fine grained we want to mesh them. -To transport this information to the various decompose methods, you can wrap it -in the Tesselation object e.g. like this: + +When generating a mesh from an abstract geometry, we can typically generate it +at different levels of detail, i.e. with different amounts of vertices. The +`Tesselation` wrapper allows you to specify this level of detail. When generating +a mesh from a tesselated geometry, the added information will be passed to +`coordinates`, `faces`, etc. ```julia sphere = Sphere(Point3f(0), 1) @@ -48,10 +72,13 @@ m1 = mesh(sphere) # uses a default value for tesselation m2 = mesh(Tesselation(sphere, 64)) # uses 64 for tesselation length(coordinates(m1)) != length(coordinates(m2)) ``` + For grid based tesselation, you can also use a tuple: + ```julia rect = Rect2(0, 0, 1, 1) Tesselation(rect, (5, 5)) +``` """ struct Tesselation{Dim,T,Primitive,NGrid} <: AbstractGeometry{Dim, T} primitive::Primitive @@ -94,6 +121,20 @@ struct Normal{T} end Normal(::Type{T}) where {T} = Normal{T}() Normal() = Normal(Vec3f) +""" + decompose(::Type{TargetType}, primitive) + decompose(::Type{TargetType}, data::AbstractVector) + +Dependent on the given type, extracts data from the primtive and converts its +eltype to `TargetType`. + +Possible `TargetType`s: +- `<: Point` extracts and converts positions (calling `coordinates()`) +- `<: AbstractFace` extracts and converts faces (calling `faces()`) +- `<: Normal{<: Vec}` extracts and converts normals, potentially generating them (calling `normals()`) +- `<: UV{<: Vec}` extracts and converts 2D texture coordinates, potentially generating them (calling `texturecoordinates()`) +- `<: UVW{<: Vec}` extracts and converts 3D texture coordinates, potentially generating them (calling `texturecoordinates()`) +""" function decompose(::Type{F}, primitive::AbstractGeometry) where {F<:AbstractFace} f = faces(primitive) if isnothing(f) @@ -104,15 +145,33 @@ function decompose(::Type{F}, primitive::AbstractGeometry) where {F<:AbstractFac return nothing end end - return collect_with_eltype(F, f) + return decompose(F, f) end - + function decompose(::Type{F}, f::AbstractVector) where {F<:AbstractFace} fs = faces(f) isnothing(fs) && error("No faces defined for $(typeof(f))") return collect_with_eltype(F, fs) end +# TODO: Should this be a completely different function? +function decompose(::Type{F}, f::AbstractVector, views::Vector{UnitRange{Int}}) where {F<:AbstractFace} + fs = faces(f) + isnothing(fs) && error("No faces defined for $(typeof(f))") + if isempty(views) + return collect_with_eltype(F, fs), views + else + output = F[] + new_views = sizehint!(UnitRange{Int}[], length(views)) + for range in views + start = length(output) + 1 + collect_with_eltype!(output, view(fs, range)) + push!(new_views, start:length(output)) + end + return output, new_views + end +end + function decompose(::Type{P}, primitive) where {P<:Point} return collect_with_eltype(P, coordinates(primitive)) end @@ -121,6 +180,10 @@ function decompose(::Type{Point}, primitive::AbstractGeometry{Dim,T}) where {Dim return collect_with_eltype(Point{Dim,T}, coordinates(primitive)) end +function decompose(::Type{Point{Dim}}, primitive::AbstractGeometry{Dim,T}) where {Dim,T} + return collect_with_eltype(Point{Dim,T}, coordinates(primitive)) +end + function decompose(::Type{PointT{T}}, primitive::AbstractGeometry{Dim}) where {Dim, T} return collect_with_eltype(Point{Dim,T}, coordinates(primitive)) end @@ -162,6 +225,10 @@ function decompose(UVT::Union{UV{T},UVW{T}}, primitive) where {T} # nothing, indicating that texturecoordinates aren't implemented positions = decompose(Point, primitive) isnothing(positions) && return nothing + # We should generate 3D uvw's if uv's are requested + # TODO: we should probably enforce that UV has a 2D type and UVW a 3D type + (length(T) != length(eltype(positions))) && return nothing + # Let this overlord do the work return decompose(UVT, positions) end diff --git a/src/mat.jl b/src/mat.jl deleted file mode 100644 index a638d4df..00000000 --- a/src/mat.jl +++ /dev/null @@ -1,234 +0,0 @@ -import LinearAlgebra: inv, det -import Random - -struct Mat{Row, Column, T, L} <: AbstractMatrix{T} - values::NTuple{L, T} - function Mat{R, C, T}(values::NTuple{L, T}) where {R, C, T, L} - @assert L == R * C "Number of rows $R * number of columns $C needs to match the number of values $L." - return new{R, C, T, L}(values) - end - function Mat{R, C, T, L}(values::NTuple{L, T}) where {R, C, T, L} - @assert L == R * C "Number of rows $R * number of columns $C needs to match the number of values $L." - return new{R, C, T, L}(values) - end -end - -Base.size(::Mat{R, C}) where {R, C} = (R, C) -Base.size(::Type{<: Mat{R, C}}) where {R, C} = (R, C) -Base.ndims(::Type{<: Mat}) = 2 -Base.@propagate_inbounds Base.getindex(mat::Mat{R, C}, i) where {R, C} = mat.values[i] -Base.@propagate_inbounds Base.getindex(mat::Mat{R, C}, i::CartesianIndex) where {R, C} = mat.values[i.I[1] + R * (i.I[2] - 1)] - -# TODO: maybe ranges as well? -Base.@propagate_inbounds function Base.getindex(mat::Mat{R, C}, i::StaticVector{N}, j::Integer) where {R, C, N} - @boundscheck begin - inbounds = all(x -> 1 ≤ x ≤ R, i) && (1 ≤ j ≤ C) - inbounds || throw(BoundsError(mat, (i, j))) - end - @inbounds data = ntuple(n -> mat.values[i[n] + R * (j-1)], N) - return Mat{N, 1}(data) -end -Base.@propagate_inbounds function Base.getindex(mat::Mat{R, C}, i::Integer, j::StaticVector{N}) where {R, C, N} - @boundscheck begin - inbounds = (1 ≤ i ≤ R) && all(x -> 1 ≤ x ≤ C, j) - inbounds || throw(BoundsError(mat, (i, j))) - end - @inbounds data = ntuple(n -> mat.values[i + R * (j[n] - 1)], N) - return Mat{1, N}(data) -end -Base.@propagate_inbounds function Base.getindex(mat::Mat{R, C}, i::StaticVector{N}, j::StaticVector{M}) where {R, C, N, M} - @boundscheck begin - inbounds = all(x -> 1 ≤ x ≤ R, i) && all(x -> 1 ≤ x ≤ C, j) - inbounds || throw(BoundsError(mat, (i, j))) - end - @inbounds data = ntuple(N * M) do nm - m, n = fldmod1(nm, N) - return mat.values[i[n] + R * (j[m] - 1)] - end - return Mat{N, M}(data) -end - -Base.IndexStyle(::Type{<: Mat}) = Base.IndexLinear() - -# Broadcasting -Base.broadcastable(x::Mat) = x -Base.BroadcastStyle(::Type{<: Mat{C, R}}) where {C, R} = StaticArrayStyle{Mat{C, R}}() -Base.values(m::Mat) = m.values - -Base.copy(mat::Mat) = deepcopy(mat) - -function Mat{C, R, T}(::LinearAlgebra.UniformScaling) where {C, R, T} - idx = CartesianIndices((R, C)) - data = ntuple(C * R) do i - ci, ri = Tuple(idx[i]) - return ci === ri ? T(1) : T(0) - end - return Mat{R, C, T}(data) -end - -function Mat{C, R, T, L}(::LinearAlgebra.UniformScaling) where {C, R, T, L} - idx = CartesianIndices((R, C)) - data = ntuple(C * R) do i - ci, ri = Tuple(idx[i]) - return ci === ri ? T(1) : T(0) - end - return Mat{R, C, T, L}(data) -end - -Mat{C, R, T}(args...) where {C, R, T} = Mat{C, R, T}(args) -Mat{C, R, T, L}(args...) where {C, R, T, L} = Mat{C, R, T}(args) -Mat{C, R, T, L}(mat::Mat{C, R}) where {C, R, T, L} = Mat{C, R, T}(mat.values) -Mat{C}(args...) where {C} = Mat{C, C}(args) -Mat{C}(arg) where {C} = Mat{C, C}(arg) -Mat{C, R}(x::Tuple) where {C, R} = Mat{C, R}(promote(x...)) -Mat{C, R, T}(x::Tuple) where {C, R, T} = Mat{C, R, T}(convert(NTuple{length(x), T}, x)) -Mat{C, R}(x::NTuple{L, T}) where {C, R, L, T} = Mat{C, R, T}(x) -Mat{C, R, T1}(x::NTuple{L, T2}) where {C, R, L, T1, T2} = Mat{C, R, T1}(convert(NTuple{L, T1}, x)) - -# catch Mat2(...) etc which lowers to Mat{2, 2, T, 4} where T -(::Type{MT})(args...) where {C, R, MT <: Mat{C, R}} = Mat{C, R}(args) -(::Type{MT})(args::Tuple) where {C, R, MT <: Mat{C, R}} = Mat{C, R}(args) - -Mat{C, R}(x::AbstractMatrix{T}) where {C, R, T} = Mat{C, R, T}(x) -Mat{C, R, T}(x::AbstractMatrix) where {C, R, T} = Mat{C, R, T}(ntuple(i-> convert(T, x[i]), C*R)) -Mat{C, R, T, N}(x::AbstractMatrix) where {C, R, T, N} = Mat{C, R, T}(ntuple(i-> convert(T, x[i]), C*R)) - -Base.convert(::Type{Mat{C, R, T, L}}, from::Mat{C, R}) where {C, R, T, L} = Mat{C, R, T}(from.values) - -# Matrix products -# General shape mismatched versions are errors -(*)(a::Mat{M, N, T1}, b::Mat{O, K, T2}) where {T1, T2, M, N, O, K} = throw(DimensionMismatch("$N != $O in $(typeof(a)) and $(typeof(b))")) - -Base.:(-)(a::Mat{R, C}, b::Mat{R, C}) where {R, C} = Mat{R, C}(a.values .- b.values) -Base.:(+)(a::Mat{R, C}, b::Mat{R, C}) where {R, C} = Mat{R, C}(a.values .+ b.values) - -# matrix * matrix -@generated function *(a::Mat{M, N, T1}, b::Mat{N, P, T2}) where {T1, T2, M, N, P} - elements = Expr(:tuple) - for j in 1:P - for i in 1:M - plus_expr = Expr(:call, :+, (:(a[$i,$k]*b[$k,$j]) for k = 1:N)...) - push!(elements.args, plus_expr) - end - end - :(Mat{$M, $P}($elements)) -end - -@inline det(A::Mat{1, 1, T}) where {T} = @inbounds return ( A[1] ) -@inline det(A::Mat{2, 2, T}) where {T} = @inbounds return ( A[1,1]*A[2,2] - A[1,2]*A[2,1]) -@inline det(A::Mat{3, 3, T}) where {T} = @inbounds return ( - A[1,1]*(A[2,2]*A[3,3]-A[2,3]*A[3,2]) - - A[1,2]*(A[2,1]*A[3,3]-A[2,3]*A[3,1]) + - A[1,3]*(A[2,1]*A[3,2]-A[2,2]*A[3,1]) -) - -det(A::Mat{4, 4, T}) where {T} = @inbounds return ( - A[13] * A[10] * A[7] * A[4] - A[9] * A[14] * A[7] * A[4] - - A[13] * A[6] * A[11] * A[4] + A[5] * A[14] * A[11] * A[4] + - A[9] * A[6] * A[15] * A[4] - A[5] * A[10] * A[15] * A[4] - - A[13] * A[10] * A[3] * A[8] + A[9] * A[14] * A[3] * A[8] + - A[13] * A[2] * A[11] * A[8] - A[1] * A[14] * A[11] * A[8] - - A[9] * A[2] * A[15] * A[8] + A[1] * A[10] * A[15] * A[8] + - A[13] * A[6] * A[3] * A[12] - A[5] * A[14] * A[3] * A[12] - - A[13] * A[2] * A[7] * A[12] + A[1] * A[14] * A[7] * A[12] + - A[5] * A[2] * A[15] * A[12] - A[1] * A[6] * A[15] * A[12] - - A[9] * A[6] * A[3] * A[16] + A[5] * A[10] * A[3] * A[16] + - A[9] * A[2] * A[7] * A[16] - A[1] * A[10] * A[7] * A[16] - - A[5] * A[2] * A[11] * A[16] + A[1] * A[6] * A[11] * A[16] -) - -det(A::Mat) = det(Matrix(A)) - -inv(A::Mat{1, 1, T, 1}) where T = @inbounds return Mat{1, 1, T, 1}(inv(A[1])) -function inv(A::Mat{2, 2, T, L}) where {T, L} - determinant = det(A) - @inbounds return Mat{2, 2, T}( - (A[2,2] /determinant, -A[2,1]/determinant), - (-A[1,2]/determinant, A[1,1] /determinant) - ) -end -function inv(A::Mat{3, 3, T, L}) where {T, L} - determinant = det(A) - @inbounds return Mat{3, 3, T}( - (A[2,2]*A[3,3]-A[2,3]*A[3,2]) /determinant, - -(A[2,1]*A[3,3]-A[2,3]*A[3,1])/determinant, - (A[2,1]*A[3,2]-A[2,2]*A[3,1]) /determinant, - - -(A[1,2]*A[3,3]-A[1,3]*A[3,2])/determinant, - (A[1,1]*A[3,3]-A[1,3]*A[3,1]) /determinant, - -(A[1,1]*A[3,2]-A[1,2]*A[3,1])/determinant, - - (A[1,2]*A[2,3]-A[1,3]*A[2,2]) /determinant, - -(A[1,1]*A[2,3]-A[1,3]*A[2,1])/determinant, - (A[1,1]*A[2,2]-A[1,2]*A[2,1]) /determinant - ) -end - -function inv(A::Mat{4, 4, T, L}) where {T, L} - determinant = det(A) - @inbounds return Mat{4, 4, T}( - (A[2,3]*A[3,4]*A[4,2] - A[2,4]*A[3,3]*A[4,2] + A[2,4]*A[3,2]*A[4,3] - A[2,2]*A[3,4]*A[4,3] - A[2,3]*A[3,2]*A[4,4] + A[2,2]*A[3,3]*A[4,4]) / determinant, - (A[2,4]*A[3,3]*A[4,1] - A[2,3]*A[3,4]*A[4,1] - A[2,4]*A[3,1]*A[4,3] + A[2,1]*A[3,4]*A[4,3] + A[2,3]*A[3,1]*A[4,4] - A[2,1]*A[3,3]*A[4,4]) / determinant, - (A[2,2]*A[3,4]*A[4,1] - A[2,4]*A[3,2]*A[4,1] + A[2,4]*A[3,1]*A[4,2] - A[2,1]*A[3,4]*A[4,2] - A[2,2]*A[3,1]*A[4,4] + A[2,1]*A[3,2]*A[4,4]) / determinant, - (A[2,3]*A[3,2]*A[4,1] - A[2,2]*A[3,3]*A[4,1] - A[2,3]*A[3,1]*A[4,2] + A[2,1]*A[3,3]*A[4,2] + A[2,2]*A[3,1]*A[4,3] - A[2,1]*A[3,2]*A[4,3]) / determinant, - - (A[1,4]*A[3,3]*A[4,2] - A[1,3]*A[3,4]*A[4,2] - A[1,4]*A[3,2]*A[4,3] + A[1,2]*A[3,4]*A[4,3] + A[1,3]*A[3,2]*A[4,4] - A[1,2]*A[3,3]*A[4,4]) / determinant, - (A[1,3]*A[3,4]*A[4,1] - A[1,4]*A[3,3]*A[4,1] + A[1,4]*A[3,1]*A[4,3] - A[1,1]*A[3,4]*A[4,3] - A[1,3]*A[3,1]*A[4,4] + A[1,1]*A[3,3]*A[4,4]) / determinant, - (A[1,4]*A[3,2]*A[4,1] - A[1,2]*A[3,4]*A[4,1] - A[1,4]*A[3,1]*A[4,2] + A[1,1]*A[3,4]*A[4,2] + A[1,2]*A[3,1]*A[4,4] - A[1,1]*A[3,2]*A[4,4]) / determinant, - (A[1,2]*A[3,3]*A[4,1] - A[1,3]*A[3,2]*A[4,1] + A[1,3]*A[3,1]*A[4,2] - A[1,1]*A[3,3]*A[4,2] - A[1,2]*A[3,1]*A[4,3] + A[1,1]*A[3,2]*A[4,3]) / determinant, - - (A[1,3]*A[2,4]*A[4,2] - A[1,4]*A[2,3]*A[4,2] + A[1,4]*A[2,2]*A[4,3] - A[1,2]*A[2,4]*A[4,3] - A[1,3]*A[2,2]*A[4,4] + A[1,2]*A[2,3]*A[4,4]) / determinant, - (A[1,4]*A[2,3]*A[4,1] - A[1,3]*A[2,4]*A[4,1] - A[1,4]*A[2,1]*A[4,3] + A[1,1]*A[2,4]*A[4,3] + A[1,3]*A[2,1]*A[4,4] - A[1,1]*A[2,3]*A[4,4]) / determinant, - (A[1,2]*A[2,4]*A[4,1] - A[1,4]*A[2,2]*A[4,1] + A[1,4]*A[2,1]*A[4,2] - A[1,1]*A[2,4]*A[4,2] - A[1,2]*A[2,1]*A[4,4] + A[1,1]*A[2,2]*A[4,4]) / determinant, - (A[1,3]*A[2,2]*A[4,1] - A[1,2]*A[2,3]*A[4,1] - A[1,3]*A[2,1]*A[4,2] + A[1,1]*A[2,3]*A[4,2] + A[1,2]*A[2,1]*A[4,3] - A[1,1]*A[2,2]*A[4,3]) / determinant, - - (A[1,4]*A[2,3]*A[3,2] - A[1,3]*A[2,4]*A[3,2] - A[1,4]*A[2,2]*A[3,3] + A[1,2]*A[2,4]*A[3,3] + A[1,3]*A[2,2]*A[3,4] - A[1,2]*A[2,3]*A[3,4]) / determinant, - (A[1,3]*A[2,4]*A[3,1] - A[1,4]*A[2,3]*A[3,1] + A[1,4]*A[2,1]*A[3,3] - A[1,1]*A[2,4]*A[3,3] - A[1,3]*A[2,1]*A[3,4] + A[1,1]*A[2,3]*A[3,4]) / determinant, - (A[1,4]*A[2,2]*A[3,1] - A[1,2]*A[2,4]*A[3,1] - A[1,4]*A[2,1]*A[3,2] + A[1,1]*A[2,4]*A[3,2] + A[1,2]*A[2,1]*A[3,4] - A[1,1]*A[2,2]*A[3,4]) / determinant, - (A[1,2]*A[2,3]*A[3,1] - A[1,3]*A[2,2]*A[3,1] + A[1,3]*A[2,1]*A[3,2] - A[1,1]*A[2,3]*A[3,2] - A[1,2]*A[2,1]*A[3,3] + A[1,1]*A[2,2]*A[3,3]) / determinant - ) -end - -inv(A::Mat) = typeof(A)(inv(Matrix(A))) - -function Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{<: Mat{R, C, T}}) where {R,C,T} - return Mat{R, C, T}(ntuple(i-> rand(rng, T), R*C)) -end - -function Random.randn(rng::Random.AbstractRNG, ::Type{<: Mat{R, C, T}}) where {R,C,T} - return Mat{R, C, T}(ntuple(i-> randn(rng, T), R*C)) -end - -function Base.transpose(a::Mat{R, C, T}) where {R, C, T} - idx = CartesianIndices((R, C)) - data = ntuple(R * C) do i - ci, ri = Tuple(idx[i]) - return a[ri, ci] - end - Mat{R, C, T}(data) -end - -(*)(a::Mat{M, N, T1}, b::StaticVector{O, T2}) where {T1, T2, M, N, O} = throw(DimensionMismatch("$N != $O in $(typeof(a)) and $(typeof(b))")) - -# vector * (row vector) -@generated function *(a::StaticVector{N, T1}, b::Mat{1, M, T2}) where {T1, T2, N, M} - elements = Expr(:tuple, [Expr(:tuple, [:(a[$i] * b[$j]) for i in 1:N]...) for j in 1:M]...) - return :(similar_type(a)($elements)) -end - -# matrix * vector -@generated function *(a::Mat{M, N, T1}, b::StaticVector{N, T2}) where {T1, T2, M, N} - elements = Expr(:tuple, [Expr(:call, :+, [:(a[$i,$k]*b[$k]) for k = 1:N]...) for i in 1:M]...) - return :(similar_type(b)($elements)) -end - -# TODO: Fix Vec(mat) becoming Vec((mat,)) (i.e. restrict eltype to Number?) -(VT::Type{<: StaticVector{N}})(mat::Mat{N, 1}) where {N} = VT(mat.values) - -function Base.isapprox( - a::Mat{R1, C1, T1}, b::Mat{R2, C2, T2}; - atol::Real = 0, - rtol::Real = atol > 0 ? 0 : sqrt(max(eps(T1), eps(T2))) - ) where {R1, R2, C1, C2, T1, T2} - return (R1 == R2) && (C1 == C2) && norm(a - b) <= max(atol, rtol * max(norm(a), norm(b))) -end \ No newline at end of file diff --git a/src/meshes.jl b/src/meshes.jl index b29e1888..0953ea0a 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -1,39 +1,110 @@ """ - mesh(primitive::GeometryPrimitive; - pointtype=Point, facetype=GLTriangle, - uv=nothing, normaltype=nothing) + mesh(primitive::GeometryPrimitive[; pointtype = Point, facetype = GLTriangleFace, vertex_attributes...]) -Creates a mesh from `primitive`. +Creates a mesh from a given `primitive` with the given `pointtype` and `facetype`. -Uses the element types from the keyword arguments to create the attributes. -The attributes that have their type set to nothing are not added to the mesh. -Note, that this can be an `Int` or `Tuple{Int, Int}``, when the primitive is grid based. -It also only losely correlates to the number of vertices, depending on the algorithm used. -#TODO: find a better number here! +This method only generates positions and faces from the primitive. Additional +vertex attributes like normals and texture coordinates can be given as extra +keyword arguments. + +Note that vertex attributes that are `nothing` get removed before creating a mesh. + +See also: [`triangle_mesh`](@ref), [`normal_mesh`](@ref), [`uv_mesh`](@ref), [`uv_normal_mesh`](@ref) """ -function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleFace) +function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleFace, vertex_attributes...) positions = decompose(pointtype, primitive) - _f = faces(primitive) + + # TODO: consider not allowing FaceView here? + if positions isa FaceView + positions = positions.data + _fs = positions.faces + isnothing(faces(primitive)) || @error("A primitive should not define `faces` and use a FaceView for `coordinates()`. Using faces from FaceView.") + else + # This tries `faces(prim)` first, then triangulation with the natural + # position type of the primitive. + _fs = decompose(facetype, primitive) + end + # If faces returns nothing for primitive, we try to triangulate! - if isnothing(_f) + if isnothing(_fs) if eltype(positions) <: Point2 - # triangulation.jl - f = decompose(facetype, positions) + # try triangulation with the converted positions as a last attempt + fs = decompose(facetype, positions) else error("No triangulation for $(typeof(primitive))") end else - f = decompose(facetype, _f) + fs = _fs end - return Mesh(positions, f) + + return mesh(positions, collect(fs); facetype = facetype, vertex_attributes...) end -const SimpleMesh{N, T, FT} = Mesh{N, T, Vector{Point{N, T}}, Vector{FT}} -const TriangleMesh{N} = SimpleMesh{N, Float32, GLTriangleFace} +""" + mesh(positions, faces[, facetype = GLTriangleFace, vertex_attributes...]) + +Creates a mesh from the given positions and faces. Other vertex attributes like +normals and texture coordinates can be added as keyword arguments. + +Note that vertex attributes that are `nothing` get removed before creating a mesh. + +See also:[`normal_mesh`](@ref) +""" +function mesh( + positions::AbstractVector{<:Point}, + faces::AbstractVector{FT}; + facetype=GLTriangleFace, vertex_attributes... + ) where {FT <: AbstractFace} + + fs = decompose(facetype, faces) + + names = keys(vertex_attributes) + valid_names = filter(name -> !isnothing(vertex_attributes[name]), names) + vals = convert_facetype.(Ref(facetype), getindex.(Ref(vertex_attributes), valid_names)) + va = NamedTuple{valid_names}(vals) + + return Mesh(positions, fs; va...) +end """ - mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace, - normaltype=nothing) + mesh(mesh::Mesh[; pointtype = Point, facetype = GLTriangleFace, vertex_attributes...] + +Recreates the given `mesh` with the given `pointtype`, `facetype` and vertex +attributes. If the new mesh would match the old mesh, the old mesh is returned instead. + +Note that vertex attributes that are `nothing` get removed before creating a mesh. +""" +function mesh( + mesh::Mesh{D, T, FT}; pointtype = Point{D, Float32}, + facetype::Type{<: AbstractFace} = GLTriangleFace, + attributes... + ) where {D, T, FT <: AbstractFace} + + names = keys(attributes) + valid_names = filter(name -> !isnothing(attributes[name]), names) + + if isempty(valid_names) && (GeometryBasics.pointtype(mesh) == pointtype) && (FT == facetype) + return mesh + else + vals = getindex.(Ref(attributes), valid_names) + va = NamedTuple{valid_names}(vals) + + # add vertex attributes + va = merge(vertex_attributes(mesh), va) + # convert position attribute and facetypes in FaceViews + va = NamedTuple{keys(va)}(map(keys(va)) do name + val = name == :position ? decompose(pointtype, va[:position]) : va[name] + return convert_facetype(facetype, val) + end) + + # update main face type + f, views = decompose(facetype, faces(mesh), mesh.views) + return Mesh(va, f, views) + end +end + +""" + mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace) Create a mesh from a polygon given as a vector of points, using triangulation. """ @@ -41,45 +112,101 @@ function mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace) return mesh(Polygon(polygon); pointtype=pointtype, facetype=facetype) end -function triangle_mesh(primitive::Union{AbstractGeometry{N}, AbstractVector{<: Point{N}}})::TriangleMesh{N} where {N} - return mesh(primitive; pointtype=Point{N, Float32}) +""" + triangle_mesh(primitive::GeometryPrimitive[; pointtype = Point, facetype = GLTriangleFace]) + +Creates a simple triangle mesh from a given `primitive` with the given `pointtype` +and `facetype`. + +See also: [`triangle_mesh`](@ref), [`normal_mesh`](@ref), [`uv_mesh`](@ref), [`uv_normal_mesh`](@ref) +""" +function triangle_mesh( + primitive::Union{AbstractGeometry{N}, AbstractVector{<: Point{N}}}; + pointtype = Point{N, Float32}, facetype = GLTriangleFace + ) where {N} + return mesh(primitive; pointtype = pointtype, facetype = facetype) end +pointtype(::Mesh{D, T}) where {D, T} = Point{D, T} +facetype(::Mesh{D, T, FT}) where {D, T, FT} = FT -pointtype(x::Mesh) = eltype(decompose(Point, x)) -facetype(x::Mesh) = eltype(faces(x)) +""" + uv_mesh(primitive::GeometryPrimitive{N}[; pointtype = Point{N, Float32}, facetype = GLTriangleFace, uvtype = Vec2f]) -function triangle_mesh(primitive::Mesh{N}) where {N} - # already target type: - if pointtype(primitive) === Point{N,Float32} && GLTriangleFace === facetype(primitive) - return primitive - else - return mesh(primitive; pointtype=Point{N,Float32}, facetype=GLTriangleFace) - end -end +Creates a triangle mesh with texture coordinates from a given `primitive`. The +`pointtype`, `facetype` and `uvtype` are set by the correspondering keyword arguments. -function uv_mesh(primitive::AbstractGeometry{N,T}) where {N,T} - m = triangle_mesh(primitive) - return MetaMesh(m, (uv=decompose_uv(primitive),)) +See also: [`triangle_mesh`](@ref), [`normal_mesh`](@ref), [`uv_mesh`](@ref), [`uv_normal_mesh`](@ref) +""" +function uv_mesh( + primitive::AbstractGeometry{N}; pointtype = Point{N, Float32}, + uvtype = Vec2f, facetype = GLTriangleFace + ) where {N} + + return mesh( + primitive, uv = decompose_uv(uvtype, primitive), + pointtype = pointtype, facetype = facetype + ) end -function uv_normal_mesh(primitive::AbstractGeometry{N}) where {N} - m = triangle_mesh(primitive) - return MetaMesh(m, (uv=decompose_uv(primitive), normals=decompose_normals(m))) +""" + uv_normal_mesh(primitive::GeometryPrimitive{N}[; pointtype = Point{N, Float32}, facetype = GLTriangleFace, uvtype = Vec2f, normaltype = Vec3f]) + +Creates a triangle mesh with texture coordinates and normals from a given +`primitive`. The `pointtype`, `facetype` and `uvtype` and `normaltype` are set +by the correspondering keyword arguments. + +See also: [`triangle_mesh`](@ref), [`normal_mesh`](@ref), [`uv_mesh`](@ref), [`uv_normal_mesh`](@ref) +""" +function uv_normal_mesh( + primitive::AbstractGeometry{N}; pointtype = Point{N, Float32}, + uvtype = Vec2f, normaltype = Vec3f, facetype = GLTriangleFace + ) where {N} + + return mesh( + primitive, uv = decompose_uv(uvtype, primitive), + normal = decompose_normals(normaltype, primitive), + pointtype = pointtype, facetype = facetype + ) end -function normal_mesh(points::AbstractVector{<:Point}, - faces::AbstractVector{<:AbstractFace}) - _points = decompose(Point3f, points) - _faces = decompose(GLTriangleFace, faces) - return MetaMesh(Mesh(_points, _faces), (normals=normals(_points, _faces),)) +""" + uv_normal_mesh(primitive::GeometryPrimitive{N}[; pointtype = Point{N, Float32}, facetype = GLTriangleFace, uvtype = Vec2f, normaltype = Vec3f]) + +Creates a triangle mesh with texture coordinates and normals from a given +`primitive`. The `pointtype`, `facetype` and `uvtype` and `normaltype` are set +by the correspondering keyword arguments. + +See also: [`triangle_mesh`](@ref), [`normal_mesh`](@ref), [`uv_mesh`](@ref), [`uv_normal_mesh`](@ref) +""" +function normal_mesh( + points::AbstractVector{<:Point}, faces::AbstractVector{<:AbstractFace}; + pointtype = Point3f, normaltype = Vec3f, facetype = GLTriangleFace + ) + _points = decompose(pointtype, points) + _faces = decompose(facetype, faces) + return Mesh(_faces, position = _points, normal = normals(_points, _faces, normaltype)) end -function normal_mesh(primitive::AbstractGeometry{N}) where {N} - m = triangle_mesh(primitive) - return MetaMesh(m, (normals=decompose_normals(m),)) +""" + normal_mesh(primitive::GeometryPrimitive{N}[; pointtype = Point{N, Float32}, facetype = GLTriangleFace, normaltype = Vec3f]) + +Creates a triangle mesh with normals from a given `primitive`. The `pointtype`, `facetype` and `uvtype` and `normaltype` are set +by the correspondering keyword arguments. + +See also: [`triangle_mesh`](@ref), [`normal_mesh`](@ref), [`uv_mesh`](@ref), [`uv_normal_mesh`](@ref) +""" +function normal_mesh( + primitive::AbstractGeometry{N}; pointtype = Point{N, Float32}, + normaltype = Vec3f, facetype = GLTriangleFace + ) where {N} + + return mesh( + primitive, normal = decompose_normals(normaltype, primitive), + pointtype = pointtype, facetype = facetype) end + """ volume(triangle) @@ -102,39 +229,259 @@ function volume(mesh::Mesh) return sum(volume, mesh) end + +""" + merge(meshes::AbstractVector{Mesh}) + +Generates a new mesh containing all the data of the individual meshes. + +If all meshes are consistent in their use of FaceViews they will be preserved. +Otherwise all of them will be converted with `clear_faceviews(mesh)`. + +This function will generate `views` in the new mesh which correspond to the +inputs of this function. +""" function Base.merge(meshes::AbstractVector{<:Mesh}) return if isempty(meshes) return Mesh(Point3f[], GLTriangleFace[]) + elseif length(meshes) == 1 return meshes[1] + + else + m1 = meshes[1] + + # Check that all meshes use the same VertexAttributes + # Could also do this via typing the function, but maybe error is nice? + names = keys(vertex_attributes(m1)) + if !all(m -> keys(vertex_attributes(m)) == names, meshes) + idx = findfirst(m -> keys(vertex_attributes(m)) != names, meshes) + error( + "Cannot merge meshes with different vertex attributes. " * + "First missmatch between meshes[1] with $names and " * + "meshes[$idx] with $(keys(vertex_attributes(meshes[idx])))." + ) + end + + consistent_face_views = true + for name in names + is_face_view = getproperty(m1, name) isa FaceView + for i in 2:length(meshes) + if (getproperty(meshes[i], name) isa FaceView) != is_face_view + consistent_face_views = false + @goto DOUBLE_BREAK + end + end + end + @label DOUBLE_BREAK + + if consistent_face_views + + # All the same kind of face, can just merge + new_attribs = NamedTuple{names}(map(names) do name + if name === :position + D = maximum(ndims, meshes) + T = mapreduce(m -> eltype(pointtype(m)), promote_type, meshes) + PT = Point{D, T} + return reduce(vcat, decompose.(PT, meshes)) + else + return reduce(vcat, getproperty.(meshes, name)) + end + end) + fs = reduce(vcat, faces.(meshes)) + + # TODO: is the type difference in offset bad? + idx = length(faces(m1)) + offset = length(coordinates(m1)) + views = isempty(m1.views) ? UnitRange{Int64}[1:idx] : copy(m1.views) + + for mesh in Iterators.drop(meshes, 1) + N = length(faces(mesh)) + + # update face indices + for i = idx .+ (1:N) + # TODO: face + Int changes type to Int + fs[i] = typeof(fs[i])(fs[i] .+ offset) + end + + # add views + if isempty(mesh.views) + push!(views, idx+1 : idx+N) + else + for view in mesh.views + push!(views, view .+ idx) + end + end + + idx += N + offset += length(coordinates(mesh)) + end + + return Mesh(new_attribs, fs, views) + + else # mixed FaceViews and Arrays + + # simplify to VertexFace types, then retry merge + return merge(clear_faceviews.(meshes)) + + end + + end +end + +""" + clear_faceviews(mesh::Mesh) + +Returns the given `mesh` if it contains no FaceViews. Otherwise, generates a new +mesh that contains no FaceViews, reordering and duplicating vertex atttributes +as necessary. If the mesh has `views` they will be adjusted as needed to produce +the same submeshes. +""" +function clear_faceviews(mesh::Mesh) + main_fs = faces(mesh) + va = vertex_attributes(mesh) + + names = filter(name -> va[name] isa FaceView, keys(va)) + isempty(names) && return mesh + + other_fs = faces.(getproperty.((mesh,), names)) + names = (:position, names...) + all_fs = tuple(main_fs, other_fs...) + + if isempty(mesh.views) + + new_fs, maps = merge_vertex_indices(all_fs) + + named_maps = NamedTuple{tuple(names...)}(maps) + + new_va = NamedTuple{keys(va)}(map(keys(va)) do name + values(va[name])[get(named_maps, name, maps[1])] + end) + + return Mesh(new_va, new_fs) + else - ps = reduce(vcat, coordinates.(meshes)) - fs = reduce(vcat, faces.(meshes)) - idx = length(faces(meshes[1])) - offset = length(coordinates(meshes[1])) - for mesh in Iterators.drop(meshes, 1) - N = length(faces(mesh)) - for i = idx .+ (1:N) - fs[i] = fs[i] .+ offset + + new_fs = sizehint!(eltype(main_fs)[], length(main_fs)) + new_views = sizehint!(UnitRange{Int}[], length(mesh.views)) + new_va = NamedTuple{keys(va)}(map(keys(va)) do name + sizehint!(similar(values(va[name]), 0), length(va[name])) + end) + + vertex_index_counter = eltype(first(main_fs))(1) + + for idxs in mesh.views + view_fs, maps = merge_vertex_indices(view.(all_fs, (idxs,)), vertex_index_counter) + + vertex_index_counter += length(maps[1]) + + for name in keys(new_va) + map = maps[something(findfirst(==(name), names), 1)] + append!(new_va[name], values(va[name])[map]) end - idx += N - offset += length(coordinates(mesh)) + + # add new faces and new view + start = length(new_fs) + 1 + append!(new_fs, view_fs) + push!(new_views, start:length(new_fs)) end - return Mesh(ps, fs) + + return Mesh(new_va, new_fs, new_views) + end +end + +function merge_vertex_indices( + faces::AbstractVector{FT}, args... + ) where {N, T, FT <: AbstractFace{N, T}} + if args[end] isa Integer + fs = tuple(faces, args[1:end-1]...) + return merge_vertex_indices(fs, args[end]) + else + return merge_vertex_indices(tuple(faces, args...)) end end -function Base.merge(meshes::AbstractVector{T}) where T <: MetaMesh - isempty(meshes) && return T(Point3f[], GLTriangleFace[]) - big_mesh = merge(map(Mesh, meshes)) - big_meta = deepcopy(meta(meshes[1])) - for mesh in Iterators.drop(meshes, 1) - mm = meta(mesh) - for (k, v) in pairs(mm) - append!(big_meta[k], v) +function merge_vertex_indices( + faces::NTuple{N_Attrib, <: AbstractVector{FT}}, + vertex_index_counter::Integer = T(1) + ) where {N, T, FT <: AbstractFace{N, T}, N_Attrib} + + N_faces = length(faces[1]) + + # maps a combination of old indices in MultiFace to a new vertex_index + vertex_index_map = Dict{NTuple{N_Attrib, T}, T}() + + # Faces after conversion + new_faces = sizehint!(FT[], N_faces) + + # indices that remap attributes + attribute_indices = ntuple(n -> sizehint!(UInt32[], N_faces), N_Attrib) + + # keep track of the remmaped indices for one vertex so we don't have to + # query the dict twice + temp = Vector{T}(undef, N) + + for multi_face in zip(faces...) + + for i in 1:N + # get the i-th set of vertex indices from multi_face, i.e. + # (multi_face.position_index[i], multi_face.normal_index[i], ...) + vertex = ntuple(n -> multi_face[n][i], N_Attrib) + + # if the vertex exists, get it's index + # otherwise register it with the next available vertex index + temp[i] = get!(vertex_index_map, vertex) do + vertex_index_counter += 1 + push!.(attribute_indices, vertex) + return vertex_index_counter - 1 + end end + + # generate new face + push!(new_faces, FT(temp)) + end + + # in case we are reserving more than needed + sizehint!(new_faces, length(new_faces)) + + return new_faces, attribute_indices +end + + +""" + split_mesh(mesh::Mesh, views::Vector{UnitRange{Int}} = mesh.views) + +Creates a new mesh containing `faces(mesh)[range]` for each range in `views`. +This also removes unused vertices. +""" +function split_mesh(mesh::Mesh, views::Vector{UnitRange{Int}} = mesh.views) + return map(views) do idxs + new_fs, maps = merge_vertex_indices((view(faces(mesh), idxs),)) + + names = keys(vertex_attributes(mesh)) + new_va = NamedTuple{names}(map(names) do name + v = getproperty(mesh, name) + if v isa FaceView + _fs, _maps = merge_vertex_indices((view(faces(v), idxs),)) + return FaceView(values(v)[_maps[1]], _fs) + else + return v[maps[1]] + end + end) + + return Mesh(new_va, new_fs) end - return MetaMesh(big_mesh, big_meta) +end + +""" + remove_duplicates(faces) + +Uses a Dict to remove duplicates from the given `faces`. +""" +function remove_duplicates(fs::AbstractVector{FT}) where {FT <: AbstractFace} + hashmap = Dict{FT, Nothing}() + foreach(k -> setindex!(hashmap, nothing, k), fs) + return collect(keys(hashmap)) end @@ -156,59 +503,26 @@ function map_coordinates!(f, mesh::AbstractMesh) return mesh end -function add_meta(mesh::MetaMesh; kw...) - return MetaMesh(Mesh(mesh), (; meta(mesh)..., kw...)) -end - -function add_meta(mesh::Mesh; kw...) - return MetaMesh(mesh, (; meta(mesh)..., kw...)) -end - -# I didn't find a simple way to remove a field from a namedtuple in a type stable way without -# a generated function.. -@generated function pop(nt::NamedTuple{Names, Values}, ::Val{name}) where {Names, Values, name} - if !(name in Names) - return :(throw(Base.KeyError($(QuoteNode(name))))) - else - names = filter(x-> x !== name, Names) - nt = map(names) do name - :($name = nt.$(name)) - end - return :((; $(nt...)), nt.$(name)) +function Base.show(io::IO, ::MIME"text/plain", mesh::Mesh{N, T, FT}) where {N, T, FT} + println(io, "Mesh{$N, $T, $FT}") + println(io, " faces: ", length(faces(mesh))) + for (name, attrib) in pairs(vertex_attributes(mesh)) + println(io, " vertex $(name): ", attrib isa FaceView ? length(attrib.data) : length(attrib)) end end -function pop_meta(mesh::MetaMesh, name::Symbol) - new_meta, value = pop(meta(mesh), Val(name)) - return MetaMesh(mesh, new_meta), value -end - - -function Base.get(f, mesh::MetaMesh, key::Symbol) - hasproperty(mesh, key) && return getproperty(mesh, key) - return f() -end - -function Base.show(io::IO, ::MIME"text/plain", mesh::Mesh{N, T}) where {N, T} - FT = eltype(faces(mesh)) - println(io, "Mesh{$N, $T, $(FT)}") - println(io, " vertices: ", length(coordinates(mesh))) - println(io, " faces: ", length(faces(mesh)), " $(FT)") -end - -function Base.show(io::IO, mesh::Mesh{N, T}) where {N, T} - FT = eltype(faces(mesh)) +function Base.show(io::IO, ::Mesh{N, T, FT}) where {N, T, FT} print(io, "Mesh{$N, $T, $(FT)}(...)") end function Base.show(io::IO, ::MIME"text/plain", mesh::MetaMesh{N, T}) where {N, T} FT = eltype(faces(mesh)) println(io, "MetaMesh{$N, $T, $(FT)}") - println(io, " vertices: ", length(coordinates(mesh))) - println(io, " faces: ", length(faces(mesh)), " $(FT)") - for (k, v) in pairs(meta(mesh)) - println(io, " ", k, ": ", length(v), " $(eltype(v))") + println(io, " faces: ", length(faces(mesh))) + for (name, attrib) in pairs(vertex_attributes(mesh)) + println(io, " vertex $(name): ", length(attrib)) end + println(io, " meta: ", keys(mesh.meta)) end function Base.show(io::IO, mesh::MetaMesh{N, T}) where {N, T} diff --git a/src/offsetintegers.jl b/src/offsetintegers.jl index c921f1dd..f0f0d85c 100644 --- a/src/offsetintegers.jl +++ b/src/offsetintegers.jl @@ -19,23 +19,18 @@ raw(x::Integer) = x value(x::OffsetInteger{O,T}) where {O,T} = raw(x) - O value(x::Integer) = x -function to_subscript(io::IO, x::Int) - if x in 0:9 - print(io, Char(0x2080+x)) - elseif x < 0 - print(io, Char(0x208B)); to_subscript(io, abs(x)) - else # positive + more than one digit - for d in reverse(digits(x)) - to_subscript(io, d) - end +function Base.show(io::IO, oi::OIT) where {O, T, OIT <: OffsetInteger{O, T}} + if OIT === GLIndex + typename = "GLIndex" + elseif O == 0 + typename = "OneIndex{$T}" + elseif O == 1 + typename = "ZeroIndex{$T}" + else + typename = "OffsetInteger{$O, $T}" end -end -function Base.show(io::IO, oi::OffsetInteger{O}) where {O} - o = O < 0 ? "ₒ₊" : "ₒ₋" - print(io, "<$(value(oi))$o") - to_subscript(io, abs(O)) - print(io, ">") + print(io, typename, "(", value(oi), ")") return end @@ -92,3 +87,5 @@ function Base.promote_rule(::Type{OffsetInteger{O1,T1}}, end Base.@pure pure_max(x1, x2) = x1 > x2 ? x1 : x2 + +Base.hash(o::OffsetInteger{O}, h::UInt) where {O} = hash(o.i, hash(O, h)) \ No newline at end of file diff --git a/src/precompiles.jl b/src/precompiles.jl index df41b1e6..100a69dc 100644 --- a/src/precompiles.jl +++ b/src/precompiles.jl @@ -1,6 +1,44 @@ +using PrecompileTools: @setup_workload, @compile_workload -function _precompile_() - ccall(:jl_generating_output, Cint, ()) == 1 || return nothing - Point2f(0.5, 0.1) in Triangle(Point2f(0), Point2f(0.5, 1), Point2f(1, 0)) - decompose(GLTriangleFace, [Point2f(0), Point2f(0.5, 1), Point2f(1, 0)]) +@setup_workload begin + @compile_workload begin + # Hits FaceView, QuadFace, all standard decompose's, some type conversions + r = Rect3d(Point3d(0), Vec3d(1)) + m1 = uv_normal_mesh(r) + + c = Circle(Point2f(0), 1) + m2 = uv_normal_mesh(c, pointtype = Point3f) # hits normal gen + + m = merge([m1, m2]) # hits mixed path, clear_faceviews, then normal path + GeometryBasics.split_mesh(m) + Rect3d(m) + + # Getters + vertex_attributes(m) + coordinates(m) + normals(m) + texturecoordinates(m) + faces(m) + + face_normals(coordinates(r), faces(r)) + + # Triangulation + triangle_mesh(Polygon(rand(Point2f, 4))) + + # Other primitives + uv_normal_mesh(Rect2(0,0,1,1)) + uv_normal_mesh(Tesselation(Sphere(Point3f(0), 1f0), 3)) + uv_normal_mesh(Cylinder(Point3f(0), Point3f(0,0,1), 1f0)) + uv_normal_mesh(Pyramid(Point3f(0), 1f0, 1f0)) + + # other disconnected compiles + M = Mat3f(I) + inv(M) + M[1, Vec(1, 3)] + M * Vec(1,2,3) + + Point2f(0.5, 0.1) in Triangle(Point2f(0), Point2f(0.5, 1), Point2f(1, 0)) + decompose(GLTriangleFace, [Point2f(0), Point2f(0.5, 1), Point2f(1, 0)]) + Point3f(0.5, 0, 1f0) in r + end end diff --git a/src/primitives/cylinders.jl b/src/primitives/cylinders.jl index adba8b8b..ce1c4aaa 100644 --- a/src/primitives/cylinders.jl +++ b/src/primitives/cylinders.jl @@ -1,40 +1,27 @@ """ - Cylinder{N, T} + Cylinder{T}(origin::Point3, extremity::Point3, radius) -A `Cylinder` is a 2D rectangle or a 3D cylinder defined by its origin point, -its extremity and a radius. `origin`, `extremity` and `r`, must be specified. +A `Cylinder` is a 3D primitive defined by an `origin`, an `extremity` (end point) +and a `radius`. """ -struct Cylinder{N,T} <: GeometryPrimitive{N,T} - origin::Point{N,T} - extremity::Point{N,T} +struct Cylinder{T} <: GeometryPrimitive{3, T} + origin::Point3{T} + extremity::Point3{T} r::T end -""" - Cylinder2{T} - Cylinder3{T} - -A `Cylinder2` or `Cylinder3` is a 2D/3D cylinder defined by its origin point, -its extremity and a radius. `origin`, `extremity` and `r`, must be specified. -""" -const Cylinder2{T} = Cylinder{2,T} -const Cylinder3{T} = Cylinder{3,T} - -origin(c::Cylinder{N,T}) where {N,T} = c.origin -extremity(c::Cylinder{N,T}) where {N,T} = c.extremity -radius(c::Cylinder{N,T}) where {N,T} = c.r -height(c::Cylinder{N,T}) where {N,T} = norm(c.extremity - c.origin) -direction(c::Cylinder{N,T}) where {N,T} = (c.extremity .- c.origin) ./ height(c) - -function rotation(c::Cylinder{2,T}) where {T} - d2 = direction(c) - u = Vec{3, T}(d2[1], d2[2], T(0)) - v = Vec{3, T}(u[2], -u[1], T(0)) - v = normalize(v) - return Mat{3, 3, T}(v..., u..., 0, 0, 1) +function Cylinder(origin::Point3{T1}, extremity::Point3{T2}, radius::T3) where {T1, T2, T3} + T = promote_type(T1, T2, T3) + Cylinder{T}(origin, extremity, radius) end -function rotation(c::Cylinder{3,T}) where {T} +origin(c::Cylinder) = c.origin +extremity(c::Cylinder) = c.extremity +radius(c::Cylinder) = c.r +height(c::Cylinder) = norm(c.extremity - c.origin) +direction(c::Cylinder) = (c.extremity .- c.origin) ./ height(c) + +function rotation(c::Cylinder{T}) where {T} d3 = direction(c) u = Vec{3, T}(d3[1], d3[2], d3[3]) if abs(u[1]) > 0 || abs(u[2]) > 0 @@ -48,60 +35,61 @@ function rotation(c::Cylinder{3,T}) where {T} return Mat{3, 3, T}(v..., w..., u...) end -function coordinates(c::Cylinder{2,T}, nvertices=(2, 2)) where {T} - r = Rect(c.origin[1] - c.r / 2, c.origin[2], c.r, height(c)) - M = rotation(c) - points = coordinates(r, nvertices) - vo = to_pointn(Point3{T}, origin(c)) - return (M * (to_pointn(Point3{T}, point) .- vo) .+ vo for point in points) -end +function coordinates(c::Cylinder{T}, nvertices=30) where {T} + nvertices += isodd(nvertices) + nhalf = div(nvertices, 2) -function faces(::Cylinder{2}, nvertices=(2, 2)) - return faces(Rect(0, 0, 1, 1), nvertices) + R = rotation(c) + step = 2pi / nhalf + + ps = Vector{Point3{T}}(undef, nvertices + 2) + for i in 1:nhalf + phi = (i-1) * step + ps[i] = R * Point3{T}(c.r * cos(phi), c.r * sin(phi), 0) + c.origin + end + for i in 1:nhalf + phi = (i-1) * step + ps[i + nhalf] = R * Point3{T}(c.r * cos(phi), c.r * sin(phi), 0) + c.extremity + end + ps[end-1] = c.origin + ps[end] = c.extremity + + return ps end -function coordinates(c::Cylinder{3,T}, nvertices=30) where {T} - if isodd(nvertices) - nvertices = 2 * (nvertices ÷ 2) - end - nvertices = max(8, nvertices) - nbv = nvertices ÷ 2 +function normals(c::Cylinder, nvertices = 30) + nvertices += isodd(nvertices) + nhalf = div(nvertices, 2) - M = rotation(c) - h = height(c) - range = 1:(2 * nbv + 2) - function inner(i) - return if i == length(range) - c.extremity - elseif i == length(range) - 1 - origin(c) - else - phi = T((2π * (((i + 1) ÷ 2) - 1)) / nbv) - up = ifelse(isodd(i), 0, h) - (M * Point(c.r * cos(phi), c.r * sin(phi), up)) .+ c.origin - end + R = rotation(c) + step = 2pi / nhalf + + ns = Vector{Vec3f}(undef, nhalf + 2) + for i in 1:nhalf + phi = (i-1) * step + ns[i] = R * Vec3f(cos(phi), sin(phi), 0) end + ns[end-1] = R * Vec3f(0, 0, -1) + ns[end] = R * Vec3f(0, 0, 1) - return (inner(i) for i in range) + disk1 = map(i -> GLTriangleFace(nhalf+1), 1:nhalf) + mantle = map(i -> QuadFace(i, mod1(i+1, nhalf), mod1(i+1, nhalf), i), 1:nhalf) + disk2 = map(i -> GLTriangleFace(nhalf+2), 1:nhalf) + fs = vcat(disk1, mantle, disk2) + + return FaceView(ns, fs) end -function faces(c::Cylinder{3}, facets=30) - isodd(facets) ? facets = 2 * div(facets, 2) : nothing - facets < 8 ? facets = 8 : nothing - nbv = Int(facets / 2) - indexes = Vector{TriangleFace{Int}}(undef, facets) - index = 1 - for j in 1:(nbv - 1) - indexes[index] = (index + 2, index + 1, index) - indexes[index + 1] = (index + 3, index + 1, index + 2) - index += 2 - end - indexes[index] = (1, index + 1, index) - indexes[index + 1] = (2, index + 1, 1) +function faces(::Cylinder, facets=30) + nvertices = facets + isodd(facets) + nhalf = div(nvertices, 2) - for i in 1:length(indexes) - i % 2 == 1 ? push!(indexes, (indexes[i][1], indexes[i][3], 2 * nbv + 1)) : - push!(indexes, (indexes[i][2], indexes[i][1], 2 * nbv + 2)) + disk1 = map(i -> GLTriangleFace(nvertices+1, mod1(i+1, nhalf), i), 1:nhalf) + mantle = map(1:nhalf) do i + i1 = mod1(i+1, nhalf) + QuadFace(i, i1, i1 + nhalf, i+nhalf) end - return indexes + disk2 = map(i -> GLTriangleFace(nvertices+2, i+nhalf, mod1(i+1, nhalf)+nhalf), 1:nhalf) + + return vcat(disk1, mantle, disk2) end diff --git a/src/primitives/pyramids.jl b/src/primitives/pyramids.jl index de2851fb..cfd89527 100644 --- a/src/primitives/pyramids.jl +++ b/src/primitives/pyramids.jl @@ -1,9 +1,21 @@ +""" + Pyramid(middle::Point3, length::Real, width::Real) + +A Pyramid is an axis-aligned primitive where the tip of the Pyramid extends by +`length` from `middle` in z direction and the square base extends by `width` +in ±x and ±y direction from `middle`. +""" struct Pyramid{T} <: GeometryPrimitive{3,T} middle::Point{3,T} length::T width::T end +function Pyramid(middle::Point{3, T1}, length::T2, width::T3) where {T1, T2, T3} + T = promote_type(T1, T2, T3) + return Pyramid(Point3{T}(middle), T(length), T(width)) +end + function coordinates(p::Pyramid{T}) where {T} leftup = Point{3,T}(-p.width, p.width, 0) / 2 leftdown = Point(-p.width, -p.width, 0) / 2 @@ -12,16 +24,22 @@ function coordinates(p::Pyramid{T}) where {T} ld = Point{3,T}(p.middle + leftdown) ru = Point{3,T}(p.middle - leftdown) rd = Point{3,T}(p.middle - leftup) - return Point{3,T}[ - tip, rd, ru, - tip, ru, lu, - tip, lu, ld, - tip, ld, rd, - ru, rd, lu, - ld, lu, rd - ] + return Point{3,T}[tip, rd, ru, lu, ld] +end + +function normals(p::Pyramid) + w = p.width; h = p.length + ns = normalize.(Vec3f[(h, 0, w), (0, h, w), (-h, 0, w), (0, -h, w), (0, 0, -1)]) + fs = [GLTriangleFace(1), GLTriangleFace(2), GLTriangleFace(3), GLTriangleFace(4), QuadFace(5)] + return FaceView(ns, fs) end function faces(::Pyramid) - return (TriangleFace(triangle) for triangle in TupleView{3}(1:18)) + return [ + GLTriangleFace(1, 2, 3), + GLTriangleFace(1, 3, 4), + GLTriangleFace(1, 4, 5), + GLTriangleFace(1, 5, 2), + QuadFace(2, 3, 4, 5) + ] end diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index 89c45cac..045d266d 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -62,7 +62,7 @@ function Rect{N,T1}(a::Rect{N,T2}) where {N,T1,T2} return Rect(Vec{N,T1}(minimum(a)), Vec{N,T1}(widths(a))) end -function Rect(v1::Vec{N,T1}, v2::Vec{N,T2}) where {N,T1,T2} +function Rect(v1::VecTypes{N,T1}, v2::VecTypes{N,T2}) where {N,T1,T2} T = promote_type(T1, T2) return Rect{N,T}(Vec{N,T}(v1), Vec{N,T}(v2)) end @@ -308,20 +308,21 @@ function Base.to_indices(A::AbstractMatrix{T}, I::Tuple{Rect2{IT}}) where {T,IT< return ((mini[1] + 1):(mini[1] + wh[1]), (mini[2] + 1):(mini[2] + wh[2])) end -function minmax(p::StaticVector, vmin, vmax) +function _minmax(p::StaticVector, vmin, vmax) any(isnan, p) && return (vmin, vmax) return min.(p, vmin), max.(p, vmax) end +# TODO: doesn't work regardless # Annoying special case for view(Vector{Point}, Vector{Face}) -function minmax(tup::Tuple, vmin, vmax) - for p in tup - any(isnan, p) && continue - vmin = min.(p, vmin) - vmax = max.(p, vmax) - end - return vmin, vmax -end +# function Base.minmax(tup::Tuple, vmin, vmax) +# for p in tup +# any(isnan, p) && continue +# vmin = min.(p, vmin) +# vmax = max.(p, vmax) +# end +# return vmin, vmax +# end function positive_widths(rect::Rect{N,T}) where {N,T} mini, maxi = minimum(rect), maximum(rect) @@ -544,21 +545,30 @@ centered(R::Type{Rect}) = R(Vec{2,Float32}(-0.5), Vec{2,Float32}(1)) # Rect2 decomposition function faces(rect::Rect2, nvertices=(2, 2)) - w, h = nvertices - idx = LinearIndices(nvertices) - quad(i, j) = QuadFace{Int}(idx[i, j], idx[i + 1, j], idx[i + 1, j + 1], idx[i, j + 1]) - return ivec((quad(i, j) for i in 1:(w - 1), j in 1:(h - 1))) + if nvertices == (2, 2) + return [QuadFace(1,2,3,4)] + else + w, h = nvertices + idx = LinearIndices(nvertices) + quad(i, j) = QuadFace{Int}(idx[i, j], idx[i + 1, j], idx[i + 1, j + 1], idx[i, j + 1]) + return [quad(i, j) for j in 1:(h - 1) for i in 1:(w - 1)] + end end -function coordinates(rect::Rect2, nvertices=(2, 2)) +function coordinates(rect::Rect2{T}, nvertices=(2, 2)) where {T} mini, maxi = extrema(rect) - xrange, yrange = LinRange.(mini, maxi, nvertices) - return [Point(x, y) for y in yrange for x in xrange] + if nvertices == (2, 2) + return Point2{T}[mini, (maxi[1], mini[2]), maxi, (mini[1], maxi[2])] + else + xrange, yrange = LinRange.(mini, maxi, nvertices) + return [Point(x, y) for y in yrange for x in xrange] + end end -function texturecoordinates(rect::Rect2, nvertices=(2, 2)) - xrange, yrange = LinRange.((0, 1), (1, 0), nvertices) - return ivec(((x, y) for x in xrange, y in yrange)) +function texturecoordinates(rect::Rect2{T}, nvertices=(2, 2)) where {T} + ps = coordinates(Rect2{T}(0, 0, 1, 1), nvertices) + ps = [Vec2{T}(0, 1) .+ Vec2{T}(1, -1) .* p for p in ps] + return ps end function normals(rect::Rect2, nvertices=(2, 2)) @@ -567,22 +577,25 @@ end ## # Rect3 decomposition -function coordinates(rect::Rect3) +function coordinates(rect::Rect3{T}) where T # TODO use n w = widths(rect) o = origin(rect) - points = Point{3,Int}[(0, 0, 0), (0, 0, 1), (0, 1, 1), (0, 1, 0), (0, 0, 0), (1, 0, 0), - (1, 0, 1), (0, 0, 1), (0, 0, 0), (0, 1, 0), (1, 1, 0), (1, 0, 0), - (1, 1, 1), (0, 1, 1), (0, 0, 1), (1, 0, 1), (1, 1, 1), (1, 0, 1), - (1, 0, 0), (1, 1, 0), (1, 1, 1), (1, 1, 0), (0, 1, 0), (0, 1, 1)] - return ((x .* w .+ o) for x in points) + return Point{3, T}[o + (x, y, z) .* w for x in (0, 1) for y in (0, 1) for z in (0, 1)] +end + +function normals(::Rect3) + ns = Vec3f[(-1,0,0), (1,0,0), (0,-1,0), (0,1,0), (0,0,-1), (0,0,1)] + return FaceView(ns, QuadFace{Int}.(1:6)) end function texturecoordinates(rect::Rect3) return coordinates(Rect3(0, 0, 0, 1, 1, 1)) end -function faces(rect::Rect3) - return QuadFace{Int}[(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12), (13, 14, 15, 16), - (17, 18, 19, 20), (21, 22, 23, 24),] +function faces(::Rect3) + return QuadFace{Int}[ + (1, 2, 4, 3), (7, 8, 6, 5), (5, 6, 2, 1), + (3, 4, 8, 7), (1, 3, 7, 5), (6, 8, 4, 2) + ] end diff --git a/src/primitives/spheres.jl b/src/primitives/spheres.jl index 0eb2fea0..76602326 100644 --- a/src/primitives/spheres.jl +++ b/src/primitives/spheres.jl @@ -42,15 +42,23 @@ function centered(::Type{T}) where {T<:HyperSphere} end function coordinates(s::Circle, nvertices=64) - rad = radius(s) - inner(fi) = Point(rad * sin(fi + pi), rad * cos(fi + pi)) .+ origin(s) - return [inner(fi) for fi in LinRange(0, 2pi, nvertices)] + r = radius(s); o = origin(s) + ps = [r * Point(cos(phi), sin(phi)) + o for phi in LinRange(0, 2pi, nvertices)] + # ps[end] = o + return ps end function texturecoordinates(::Circle, nvertices=64) return coordinates(Circle(Point2f(0.5), 0.5f0), nvertices) end +# TODO: Consider generating meshes for circles with a point in the center so +# that the triangles are more regular +# function faces(::Circle, nvertices=64) +# return [GLTriangleFace(nvertices+1, i, mod1(i+1, nvertices)) for i in 1:nvertices] +# end + + function coordinates(s::Sphere, nvertices=24) θ = LinRange(0, pi, nvertices) φ = LinRange(0, 2pi, nvertices) diff --git a/test/fixed_arrays.jl b/test/fixed_arrays.jl index cf82d0a3..256d5f31 100644 --- a/test/fixed_arrays.jl +++ b/test/fixed_arrays.jl @@ -34,25 +34,6 @@ end @test Mat{2, 2}(foo.(values(M1), 1)) == foo.(M1, 1) end - # TODO: Should this work with tuple somehow? - @testset "with Vector (acting as const)" begin - for T1 in (Vec, Point) - x = [T1(2, 3, 1), T1(7, 3, 2)] - for T2 in (Vec, Point) - T = ifelse(T1 == Point, Point, ifelse(T2 == Point, Point, Vec)) - @test [T(4, 9, 4), T(14, 9, 8)] == x .* T2(2, 3, 4) - @test [T(foo(v, T2(3, -1, 1))) for v in x] == foo.(x, T2(3, -1, 1)) - end - end - - x = [M1, M2, M1] - @test [M1 + M2, M2 + M2, M1 + M2] == x .+ M2 - @test [foo(M1, M2), foo(M2, M2), foo(M1, M2)] == foo.(x, M2) - - # maybe bad...? - @test [Vec(2, 3), Vec(3, 4)] == [1, 2] .+ Vec(1, 2) - end - @testset "chained/nested" begin for T1 in (Vec, Point, tuple) for T2 in (Vec, Point, tuple) @@ -62,15 +43,6 @@ end @test T(-15, -5) == foo.(T1(1,2), T1(-1, 0) .+ foo.(T1(1,1), T2(2,2))) end end - - @test [Point(1, 3), Point(3, 1)] == [Vec(1,2), Vec(2, 1)] .+ [Vec(1,2), Vec(2, 1)] .+ Point(-1, -1) - @test [Vec(1, 3), Point(3, 1)] == [Vec(1,2), Vec(2, 1)] .+ Vec(-1, -1) .+ [Vec(1,2), Point(2, 1)] - @test [Vec(-1, -3), Point(-3, -1)] == foo.([Vec(1,2), Vec(2, 1)] .+ Vec(-1, -1), [Vec(1,2), Point(2, 1)]) - - x = [M1, M2] - @test [M1 * M1 + M2, M1 * M2 + M2] == M1 .* x .+ M2 - @test [M1 + M2 * M1, M2 + M2 * M2] == x .+ M2 .* x - @test [foo(M1+M2, M1), foo(M2+M2, M2)] == foo.(x .+ M2, x) end @testset "Longer functions" begin @@ -117,12 +89,19 @@ end @test_throws BoundsError getindex(M3, 10) # Sanity check for loop - @test M3[2, Vec(1,2)] == Mat{1, 2}(M3[2,1], M3[2,2]) + # @test M3[2, Vec(1,2)] == Mat{1, 2}(M3[2,1], M3[2,2]) + @test M3[2, Vec(1,2)] == Vec2(M3[2,1], M3[2,2]) for x in (2, Vec(1,2), Vec(1,1,2,2)) for y in (2, Vec(1,2), Vec(1,1,2,2)) x isa Real && y isa Real && continue - @test M3[x, y] == Mat{length(x), length(y)}((M3[i, j] for j in y for i in x)...) + if length(x) == 1 + @test M3[x, y] == Vec{length(y)}((M3[i, j] for j in y for i in x)...) + elseif length(y) == 1 + @test M3[x, y] == Vec{length(x)}((M3[i, j] for j in y for i in x)...) + else + @test M3[x, y] == Mat{length(x), length(y)}((M3[i, j] for j in y for i in x)...) + end @test_throws BoundsError M3[x .- 2, y] @test_throws BoundsError M3[x, y .+ 2] @test_throws BoundsError M3[x .+ 2, y .- 2] @@ -130,4 +109,18 @@ end end end + + for N in 1:4 + @testset "math $N x $N" begin + bm = rand(N, N) + I + sm = Mat{N, N}(bm) + bv = rand(N) + sv = Vec{N, Float64}(bv) + @test bm == Matrix(sm) + @test det(bm) ≈ det(sm) + @test inv(bm) ≈ Matrix(inv(sm)) + @test collect(transpose(bm)) ≈ Matrix(transpose(sm)) + @test bm * bv ≈ collect(sm * sv) + end + end end \ No newline at end of file diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index ec68a151..420dbe37 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -2,24 +2,11 @@ using Test, GeometryBasics @testset "Cylinder" begin @testset "constructors" begin - o, extr, r = Point2f(1, 2), Point2f(3, 4), 5.0f0 - s = Cylinder(o, extr, r) - @test typeof(s) == Cylinder{2,Float32} - @test typeof(s) == Cylinder2{Float32} - @test origin(s) == o - @test extremity(s) == extr - @test radius(s) == r - #@test abs(height(s)- norm([1,2]-[3,4]))<1e-5 - h = norm(o - extr) - @test isapprox(height(s), h) - #@test norm(direction(s) - Point{2,Float32}([2,2]./norm([1,2]-[3,4])))<1e-5 - @test isapprox(direction(s), Point2f(2, 2) ./ h) v1 = rand(Point{3,Float64}) v2 = rand(Point{3,Float64}) R = rand() s = Cylinder(v1, v2, R) - @test typeof(s) == Cylinder{3,Float64} - @test typeof(s) == Cylinder3{Float64} + @test typeof(s) == Cylinder{Float64} @test origin(s) == v1 @test extremity(s) == v2 @test radius(s) == R @@ -29,79 +16,103 @@ using Test, GeometryBasics end @testset "decompose" begin - - m = GeometryBasics.normal_mesh(Sphere(Point3f(0), 1f0)) - @test decompose_uv(m) isa Vector{Vec2f} - - o, extr, r = Point2f(1, 2), Point2f(3, 4), 5.0f0 - s = Cylinder(o, extr, r) - positions = Point{3,Float32}[(-0.7677671, 3.767767, 0.0), - (2.767767, 0.23223293, 0.0), - (0.23223293, 4.767767, 0.0), - (3.767767, 1.2322329, 0.0), (1.2322329, 5.767767, 0.0), - (4.767767, 2.232233, 0.0)] - @test decompose(Point3f, Tesselation(s, (2, 3))) ≈ positions - - FT = TriangleFace{Int} - faces = FT[(1, 2, 4), (1, 4, 3), (3, 4, 6), (3, 6, 5)] - @test faces == decompose(FT, Tesselation(s, (2, 3))) - v1 = Point{3,Float64}(1, 2, 3) v2 = Point{3,Float64}(4, 5, 6) R = 5.0 s = Cylinder(v1, v2, R) - positions = Point{3,Float64}[(4.535533905932738, -1.5355339059327373, 3.0), - (7.535533905932738, 1.4644660940672627, 6.0), - (3.0412414523193148, 4.041241452319315, - -1.0824829046386295), - (6.041241452319315, 7.041241452319315, - 1.9175170953613705), - (-2.535533905932737, 5.535533905932738, - 2.9999999999999996), - (0.46446609406726314, 8.535533905932738, 6.0), - (-1.0412414523193152, -0.04124145231931431, - 7.0824829046386295), - (1.9587585476806848, 2.9587585476806857, - 10.08248290463863), (1, 2, 3), (4, 5, 6)] + positions = Point{3,Float64}[ + (4.535533905932738, -1.5355339059327373, 3.0), + (3.0412414523193148, 4.041241452319315, -1.0824829046386295), + (-2.535533905932737, 5.535533905932738, 2.9999999999999996), + (-1.0412414523193152, -0.04124145231931431, 7.0824829046386295), + (7.535533905932738, 1.4644660940672627, 6.0), + (6.041241452319315, 7.041241452319315, 1.9175170953613705), + (0.46446609406726314, 8.535533905932738, 6.0), + (1.9587585476806848, 2.9587585476806857, 10.08248290463863), + (1, 2, 3), + (4, 5, 6) + ] @test decompose(Point3{Float64}, Tesselation(s, 8)) ≈ positions - faces = TriangleFace{Int}[(3, 2, 1), (4, 2, 3), (5, 4, 3), (6, 4, 5), (7, 6, 5), - (8, 6, 7), (1, 8, 7), (2, 8, 1), (3, 1, 9), (2, 4, 10), - (5, 3, 9), (4, 6, 10), (7, 5, 9), (6, 8, 10), (1, 7, 9), - (8, 2, 10)] - @test faces == decompose(TriangleFace{Int}, Tesselation(s, 8)) + _faces = TriangleFace[ + (9, 2, 1), (9, 3, 2), (9, 4, 3), (9, 1, 4), (1, 2, 6), (1, 6, 5), + (2, 3, 7), (2, 7, 6), (3, 4, 8), (3, 8, 7), (4, 1, 5), (4, 5, 8), + (10, 5, 6), (10, 6, 7), (10, 7, 8), (10, 8, 5)] + + @test _faces == decompose(TriangleFace{Int}, Tesselation(s, 8)) m = triangle_mesh(Tesselation(s, 8)) @test m === triangle_mesh(m) - @test GeometryBasics.faces(m) == faces + @test GeometryBasics.faces(m) == decompose(GLTriangleFace, _faces) @test GeometryBasics.coordinates(m) ≈ positions - m = normal_mesh(s)# just test that it works without explicit resolution parameter + + m = normal_mesh(s) # just test that it works without explicit resolution parameter + @test hasproperty(m, :position) + @test hasproperty(m, :normal) + @test length(faces(m)) == length(faces(m.normal)) + @test faces(m) isa AbstractVector{GLTriangleFace} + @test faces(m.normal) isa AbstractVector{GLTriangleFace} + + ns = GeometryBasics.FaceView( + Vec{3, Float32}[ + [0.70710677, -0.70710677, 0.0], [0.4082483, 0.4082483, -0.8164966], + [-0.70710677, 0.70710677, -9.9991995f-17], [-0.4082483, -0.4082483, 0.8164966], + [-0.57735026, -0.57735026, -0.57735026], [0.57735026, 0.57735026, 0.57735026] + ], [ + GLTriangleFace(5, 5, 5), GLTriangleFace(5, 5, 5), + GLTriangleFace(5, 5, 5), GLTriangleFace(5, 5, 5), + QuadFace{Int64}(1, 2, 2, 1), QuadFace{Int64}(2, 3, 3, 2), + QuadFace{Int64}(3, 4, 4, 3), QuadFace{Int64}(4, 1, 1, 4), + GLTriangleFace(6, 6, 6), GLTriangleFace(6, 6, 6), + GLTriangleFace(6, 6, 6), GLTriangleFace(6, 6, 6) + ] + ) + + @test ns == decompose_normals(Tesselation(s, 8)) muv = uv_mesh(s) - @test Rect(Point.(texturecoordinates(muv))) == Rect2f(Vec2f(0), Vec2f(1.0)) + @test !hasproperty(muv, :uv) # not defined yet end end @testset "HyperRectangles" begin a = Rect(Vec(0, 0), Vec(1, 1)) - pt_expa = Point{2,Int}[(0, 0), (1, 0), (0, 1), (1, 1)] + pt_expa = Point{2,Int}[(0, 0), (1, 0), (1, 1), (0, 1)] @test decompose(Point{2,Int}, a) == pt_expa mesh = normal_mesh(a) @test decompose(Point2f, mesh) == pt_expa b = Rect(Vec(1, 1, 1), Vec(1, 1, 1)) - pt_expb = Point{3,Int64}[[1, 1, 1], [1, 1, 2], [1, 2, 2], [1, 2, 1], [1, 1, 1], - [2, 1, 1], [2, 1, 2], [1, 1, 2], [1, 1, 1], [1, 2, 1], - [2, 2, 1], [2, 1, 1], [2, 2, 2], [1, 2, 2], [1, 1, 2], - [2, 1, 2], [2, 2, 2], [2, 1, 2], [2, 1, 1], [2, 2, 1], - [2, 2, 2], [2, 2, 1], [1, 2, 1], [1, 2, 2]] + pt_expb = Point{3,Int64}[[1, 1, 1], [1, 1, 2], [1, 2, 1], [1, 2, 2], + [2, 1, 1], [2, 1, 2], [2, 2, 1], [2, 2, 2]] @test decompose(Point{3,Int}, b) == pt_expb + + mesh = normal_mesh(b) + @test faces(mesh) == GLTriangleFace[ + (1, 2, 4), (1, 4, 3), (7, 8, 6), (7, 6, 5), (5, 6, 2), (5, 2, 1), + (3, 4, 8), (3, 8, 7), (1, 3, 7), (1, 7, 5), (6, 8, 4), (6, 4, 2)] + @test normals(mesh) == GeometryBasics.FaceView( + Vec{3, Float32}[[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [0.0, 0.0, 1.0]], + GLTriangleFace[(1, 1, 1), (1, 1, 1), (2, 2, 2), (2, 2, 2), (3, 3, 3), (3, 3, 3), (4, 4, 4), (4, 4, 4), (5, 5, 5), (5, 5, 5), (6, 6, 6), (6, 6, 6)] + ) + @test coordinates(mesh) == Point{3, Float32}[ + [1.0, 1.0, 1.0], [1.0, 1.0, 2.0], [1.0, 2.0, 1.0], [1.0, 2.0, 2.0], + [2.0, 1.0, 1.0], [2.0, 1.0, 2.0], [2.0, 2.0, 1.0], [2.0, 2.0, 2.0]] @test isempty(Rect{3,Float32}()) end +@testset "Pyramids" begin + p = Pyramid(Point3f(0), 1f0, 0.2f0) + @test coordinates(p) == Point3f[[0.0, 0.0, 1.0], [0.1, -0.1, 0.0], [0.1, 0.1, 0.0], [-0.1, 0.1, 0.0], [-0.1, -0.1, 0.0]] + @test faces(p) == [GLTriangleFace(1, 2, 3), GLTriangleFace(1, 3, 4), GLTriangleFace(1, 4, 5), GLTriangleFace(1, 5, 2), QuadFace{Int64}(2, 3, 4, 5)] + ns = normals(p) + @test faces(ns) == [GLTriangleFace(1), GLTriangleFace(2), GLTriangleFace(3), GLTriangleFace(4), QuadFace{Int64}(5)] + @test values(ns) ≈ Vec3f[[0.9805807, 0.0, 0.19611615], [0.0, 0.9805807, 0.19611615], [-0.9805807, 0.0, 0.19611615], [0.0, -0.9805807, 0.19611615], [0.0, 0.0, -1.0]] +end + NFace = NgonFace @testset "Faces" begin @@ -127,18 +138,31 @@ NFace = NgonFace end @testset "Normals" begin - n64 = Vec{3,Float64}[(0.0, 0.0, -1.0), (0.0, 0.0, -1.0), (0.0, 0.0, -1.0), - (0.0, 0.0, -1.0), (0.0, 0.0, 1.0), (0.0, 0.0, 1.0), - (0.0, 0.0, 1.0), (0.0, 0.0, 1.0), (-1.0, 0.0, 0.0), - (-1.0, 0.0, 0.0), (-1.0, 0.0, 0.0), (-1.0, 0.0, 0.0), - (1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 0.0, 0.0), - (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), - (0.0, -1.0, 0.0), (0.0, -1.0, 0.0), (0.0, -1.0, 0.0), - (0.0, -1.0, 0.0),] - n32 = map(Vec{3,Float32}, n64) - r = triangle_mesh(centered(Rect3)) - # @test normals(coordinates(r), GeometryBasics.faces(r)) == n32 - # @test normals(coordinates(r), GeometryBasics.faces(r)) == n64 + # per face normals + r = Rect3f(Point3f(0), Vec3f(1)) + ns = face_normals(coordinates(r), faces(r)) + ux = unit(Vec3f, 1); uy = unit(Vec3f, 2); uz = unit(Vec3f, 3) + @test ns == normals(r) + @test values(ns) == [-ux, ux, -uy, uy, -uz, uz] + + # typing + ux = unit(Vec3d, 1); uy = unit(Vec3d, 2); uz = unit(Vec3d, 3) + ns = face_normals(decompose(Point3d, r), faces(r)) + @test ns isa FaceView{Vec3d} + @test values(ns) == [-ux, ux, -uy, uy, -uz, uz] + + # Mixed + c = Cylinder(Point3f(0), Point3f(0,0,1), 0.5f0) + ns = normals(c) + # caps without mantle + f_ns = face_normals(coordinates(c), filter!(f -> f isa TriangleFace, faces(c))) + @test all(n -> n == values(ns)[end-1], values(f_ns)[1:15]) + @test all(n -> n == values(ns)[end], values(f_ns)[16:end]) + # Mantle without caps + v_ns = normals(coordinates(c), filter!(f -> f isa QuadFace, faces(c)))[1:end-2] + @test values(ns)[1:15] ≈ v_ns[1:15] + @test values(ns)[1:15] ≈ v_ns[16:30] # repeated via FaceView in ns + end @testset "HyperSphere" begin diff --git a/test/meshes.jl b/test/meshes.jl index 192c7400..485fa54b 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -1,34 +1,338 @@ -@testset "Meshing a single triangle sometimes returns an empty mesh" begin - ϕ = (sqrt(5)+1)/2 - p,q,r = Point(ϕ,0,+1),Point(1,ϕ,0),Point(ϕ,0,-1) - m = triangle_mesh(Triangle(p,q,r)) - @test m isa Mesh - @test faces(m) == [TriangleFace(1, 2, 3)] -end - -@testset "Heterogenous faces" begin - # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/142 - f = [TriangleFace(1, 2, 3), QuadFace(1, 2, 3, 4)] - p = Point2f[(0, 1), (1, 2), (3, 4), (4, 5)] - m = Mesh(p, f) - @test collect(m) == [Triangle(p[1], p[2], p[3]), GeometryBasics.Quadrilateral(p[1], p[2], p[3], p[4])] -end - -@testset "Heterogenous faces" begin - # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/142 - f = [TriangleFace(1, 2, 3), QuadFace(1, 2, 3, 4)] - p = Point2f[(0, 1), (1, 2), (3, 4), (4, 5)] - m = Mesh(p, f) - @test collect(m) == [Triangle(p[1], p[2], p[3]), GeometryBasics.Quadrilateral(p[1], p[2], p[3], p[4])] -end - -@testset "Ambiguous NgonFace constructors" begin - # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/151 - # Currently no StaticVector support - # t = TriangleFace(SA[0.4, 0.2, 0.55]) -end - -@testset "Merge empty vector of meshes" begin - # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/136 - merge(Mesh[]) == Mesh(Point3f[], GLTriangleFace[]) -end +@testset "Meshing a single triangle sometimes returns an empty mesh" begin + ϕ = (sqrt(5)+1)/2 + p,q,r = Point(ϕ,0,+1),Point(1,ϕ,0),Point(ϕ,0,-1) + m = triangle_mesh(Triangle(p,q,r)) + @test m isa Mesh + @test faces(m) == [TriangleFace(1, 2, 3)] +end + +@testset "Heterogenous faces" begin + # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/142 + f = [TriangleFace(1, 2, 3), QuadFace(1, 2, 3, 4)] + p = Point2f[(0, 1), (1, 2), (3, 4), (4, 5)] + m = Mesh(p, f) + @test collect(m) == [Triangle(p[1], p[2], p[3]), GeometryBasics.Quadrilateral(p[1], p[2], p[3], p[4])] +end + +@testset "Heterogenous faces" begin + # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/142 + f = [TriangleFace(1, 2, 3), QuadFace(1, 2, 3, 4)] + p = Point2f[(0, 1), (1, 2), (3, 4), (4, 5)] + m = Mesh(p, f) + @test collect(m) == [Triangle(p[1], p[2], p[3]), GeometryBasics.Quadrilateral(p[1], p[2], p[3], p[4])] +end + +@testset "Ambiguous NgonFace constructors" begin + # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/151 + # Currently no StaticVector support + # t = TriangleFace(SA[0.4, 0.2, 0.55]) +end + +@testset "Merge empty vector of meshes" begin + # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/136 + merge(Mesh[]) == Mesh(Point3f[], GLTriangleFace[]) +end + +@testset "Vertex Index Remapping" begin + # Sanity Check + m = Mesh( + position = GeometryBasics.FaceView(Point2f[(0, 0), (1, 0), (1, 1), (0, 1)], [QuadFace(1,2,3,4)]), + normal = GeometryBasics.FaceView([Vec3f(0,0,1)], [QuadFace(1)]) + ) + + m2 = GeometryBasics.clear_faceviews(m) + + @test faces(m) == [QuadFace(1,2,3,4)] + @test coordinates(m) == Point2f[(0, 0), (1, 0), (1, 1), (0, 1)] + @test normals(m) == GeometryBasics.FaceView([Vec3f(0,0,1)], [QuadFace(1)]) + @test isempty(m.views) + + @test faces(m2) == [QuadFace(1,2,3,4)] + @test coordinates(m2) == coordinates(m) + @test normals(m2) != normals(m) + @test normals(m2) == [only(values(normals(m))) for _ in 1:4] + @test isempty(m2.views) +end + +@testset "complex merge" begin + rects = [Rect3f(Point3f(x, y, z), Vec3f(0.5)) for x in -1:1 for y in -1:1 for z in -1:1] + direct_meshes = map(rects) do r + GeometryBasics.Mesh(coordinates(r), faces(r), normal = normals(r)) + end + dm = merge(direct_meshes) + + @test GeometryBasics.facetype(dm) == QuadFace{Int64} + @test length(faces(dm)) == 27 * 6 # 27 rects, 6 quad faces + @test length(normals(dm)) == 27 * 6 + @test length(coordinates(dm)) == 27 * 8 + @test normals(dm) isa GeometryBasics.FaceView + @test coordinates(dm) isa Vector + @test !allunique([idx for f in faces(dm) for idx in f]) + @test !allunique([idx for f in faces(dm.normal) for idx in f]) + + indirect_meshes = map(rects) do r + m = GeometryBasics.mesh(coordinates(r), faces(r), normal = normals(r), facetype = QuadFace{Int64}) + # Also testing merge of meshes with views + push!(m.views, 1:length(faces(m))) + m + end + im = merge(indirect_meshes) + + @test im == dm + + converted_meshes = map(rects) do r + m = GeometryBasics.Mesh(coordinates(r), faces(r), normal = normals(r)) + GeometryBasics.clear_faceviews(m) + end + cm = merge(converted_meshes) + + @test GeometryBasics.facetype(cm) == QuadFace{Int64} + @test length(faces(cm)) == 27 * 6 # 27 rects, 6 quad faces + @test length(normals(cm)) == 27 * 6 * 4 # duplicate 4x across face + @test length(coordinates(cm)) == 27 * 8 * 3 # duplicate 3x across shared vertex + @test normals(cm) isa Vector + @test coordinates(cm) isa Vector + @test allunique([idx for f in faces(cm) for idx in f]) + + + mixed_meshes = map(direct_meshes, indirect_meshes, converted_meshes) do dm, im, cm + rand((dm, im, cm)) # (with FaceView, with mesh.views & FaceView, w/o FaceView) + end + mm = merge(mixed_meshes) + + @test mm == cm +end + +@testset "Mesh Constructor" begin + ps = rand(Point2f, 10) + ns = rand(Vec3f, 10) + fs = GLTriangleFace[(1,2,3), (3,4,5), (5,6,7), (8,9,10)] + + @testset "Extracting faces from position FaceView" begin + # can't extract from array + @test_throws TypeError Mesh(position = ps, normal = ns) + + m = Mesh(position = FaceView(ps, fs), normal = ns) + @test coordinates(m) == ps + @test normals(m) == ns + @test faces(m) == fs + end + + @testset "Verifaction" begin + # enough vertices present + @test_throws ErrorException Mesh(rand(Point2f, 7), fs) + m = Mesh(rand(Point2f, 12), fs) + @test length(m.position) == 12 + @test length(m.faces) == 4 + + @test_throws ErrorException Mesh(ps, fs, normal = rand(Vec3f, 8)) + m = Mesh(ps, fs, normal = rand(Vec3f, 12)) + @test length(m.position) == 10 + @test length(m.normal) == 12 + @test length(m.faces) == 4 + + # valid FaceView (enough faces, vertices, matching dims) + @test_throws ErrorException Mesh(ps, fs, normal = FaceView(ns, GLTriangleFace[])) + @test_throws ErrorException Mesh(ps, fs, normal = FaceView(Vec3f[], fs)) + @test_throws ErrorException Mesh(ps, fs, normal = FaceView(ns, QuadFace{Int}.(1:4))) + m = Mesh(ps, fs, normal = FaceView(rand(Vec3f, 9), TriangleFace{Int64}.(1:2:8))) + @test length(m.position) == 10 + @test length(values(m.normal)) == 9 + @test length(faces(m.normal)) == 4 + @test length(m.faces) == 4 + + msg = "`normals` as a vertex attribute name has been deprecated in favor of `normal` to bring it in line with mesh.position and mesh.uv" + @test_logs (:warn, msg) Mesh(ps, fs, normals = ns) + end +end + +@testset "Interface" begin + ps = rand(Point2f, 10) + ns = rand(Vec3f, 10) + uvs = FaceView(rand(Vec2f, 4), GLTriangleFace.(1:4)) + fs = GLTriangleFace[(1,2,3), (3,4,5), (5,6,7), (8,9,10)] + + m = Mesh(ps, fs, normal = ns, uv = uvs) + + @test vertex_attributes(m) == getfield(m, :vertex_attributes) + @test coordinates(m) == vertex_attributes(m)[:position] + @test normals(m) == vertex_attributes(m)[:normal] + @test texturecoordinates(m) == vertex_attributes(m)[:uv] + @test faces(m) == getfield(m, :faces) + + @test m.vertex_attributes == getfield(m, :vertex_attributes) + @test m.position == vertex_attributes(m)[:position] + @test m.normal == vertex_attributes(m)[:normal] + @test m.uv == vertex_attributes(m)[:uv] + @test m.faces == getfield(m, :faces) + + @test hasproperty(m, :vertex_attributes) + @test hasproperty(m, :position) + @test hasproperty(m, :normal) + @test hasproperty(m, :uv) + @test hasproperty(m, :faces) + + mm = MetaMesh(m, name = "test") + + @test Mesh(mm) == m + @test haskey(mm, :name) + @test get(mm, :name, nothing) == "test" + @test mm[:name] == "test" + @test !haskey(mm, :foo) + @test get!(mm, :foo, "bar") == "bar" + @test haskey(mm, :foo) + @test keys(mm) == keys(getfield(mm, :meta)) + + @test vertex_attributes(mm) == getfield(m, :vertex_attributes) + @test coordinates(mm) == vertex_attributes(m)[:position] + @test normals(mm) == vertex_attributes(m)[:normal] + @test texturecoordinates(mm) == vertex_attributes(m)[:uv] + @test faces(mm) == getfield(m, :faces) +end + +@testset "mesh() constructors" begin + r = Rect3d(Point3d(-1), Vec3d(2)) + + @testset "prerequisites" begin + ps = coordinates(r) + @test length(ps) == 8 + @test ps isa Vector{Point3d} + ns = normals(r) + @test length(ns) == 6 + @test ns isa GeometryBasics.FaceView{Vec3f, Vector{Vec3f}, Vector{QuadFace{Int64}}} + uvs = texturecoordinates(r) + @test length(uvs) == 8 + @test_broken uvs isa Vector{Vec2f} + fs = faces(r) + @test length(fs) == 6 + @test fs isa Vector{QuadFace{Int64}} + end + + @testset "normal_mesh()" begin + m = normal_mesh(r, pointtype = Point3f, normaltype = Vec3f) + m = GeometryBasics.clear_faceviews(m) + + @test hasproperty(m, :position) + @test coordinates(m) isa Vector{Point3f} + @test length(coordinates(m)) == 24 + @test GeometryBasics.pointtype(m) == Point3f + + @test hasproperty(m, :normal) + @test normals(m) isa Vector{Vec3f} + @test length(normals(m)) == 24 + + @test !hasproperty(m, :uv) + @test texturecoordinates(m) === nothing + + @test faces(m) isa Vector{GLTriangleFace} + @test length(faces(m)) == 12 + @test GeometryBasics.facetype(m) == GLTriangleFace + end + + @testset "normal_uv_mesh()" begin + m = uv_normal_mesh( + r, pointtype = Point3d, normaltype = Vec3d, + uvtype = Vec3d, facetype = QuadFace{Int32} + ) + + @test hasproperty(m, :position) + @test coordinates(m) isa Vector{Point3d} + @test length(coordinates(m)) == 8 + @test GeometryBasics.pointtype(m) == Point3d + + @test hasproperty(m, :normal) + @test normals(m) isa GeometryBasics.FaceView{Vec3d, Vector{Vec3d}, Vector{QuadFace{Int32}}} + @test length(normals(m)) == 6 + + @test hasproperty(m, :uv) + @test texturecoordinates(m) isa Vector{Vec3d} + @test length(texturecoordinates(m)) == 8 + + @test faces(m) isa Vector{QuadFace{Int32}} + @test length(faces(m)) == 6 + @test GeometryBasics.facetype(m) == QuadFace{Int32} + end + + @testset "uv_mesh()" begin + m = uv_mesh( + r, pointtype = Point3f, uvtype = Vec3f, facetype = GLTriangleFace + ) + + @test hasproperty(m, :position) + @test coordinates(m) isa Vector{Point3f} + @test length(coordinates(m)) == 8 + @test GeometryBasics.pointtype(m) == Point3f + + @test !hasproperty(m, :normal) + @test normals(m) === nothing + + @test hasproperty(m, :uv) + @test texturecoordinates(m) isa Vector{Vec3f} + @test length(texturecoordinates(m)) == 8 + + @test faces(m) isa Vector{GLTriangleFace} + @test length(faces(m)) == 12 + @test GeometryBasics.facetype(m) == GLTriangleFace + end + + @testset "triangle_mesh()" begin + m = triangle_mesh(r, pointtype = Point3f) + + @test hasproperty(m, :position) + @test coordinates(m) isa Vector{Point3f} + @test length(coordinates(m)) == 8 + @test GeometryBasics.pointtype(m) == Point3f + + @test !hasproperty(m, :normal) + @test normals(m) === nothing + + @test !hasproperty(m, :uv) + @test texturecoordinates(m) === nothing + + @test faces(m) isa Vector{GLTriangleFace} + @test length(faces(m)) == 12 + @test GeometryBasics.facetype(m) == GLTriangleFace + end + + @testset "mesh(mesh)" begin + m = GeometryBasics.mesh(r, pointtype = Point3f, normal = normals(r), facetype = QuadFace{Int64}) + + # Should be hit by normal_mesh as well... + @test coordinates(m) isa Vector{Point3f} + @test length(coordinates(m)) == 8 + @test normals(m) isa GeometryBasics.FaceView{Vec3f, Vector{Vec3f}, Vector{QuadFace{Int64}}} + @test length(normals(m)) == 6 + @test !hasproperty(m, :uv) + @test texturecoordinates(m) === nothing + @test faces(m) isa Vector{QuadFace{Int64}} + @test length(faces(m)) == 6 + + # Shoudl throw because uv's don't match length(position) or have faces + @test_throws ErrorException GeometryBasics.mesh(m, uv = Vec3f[]) + + # remap vertex attributes to merge faceviews into one face array + m2 = GeometryBasics.clear_faceviews(m) + + @test coordinates(m2) isa Vector{Point3f} + @test length(coordinates(m2)) == 24 + @test normals(m2) isa Vector{Vec3f} + @test length(normals(m2)) == 24 + @test !hasproperty(m2, :uv) + @test texturecoordinates(m2) === nothing + @test faces(m2) isa Vector{QuadFace{Int64}} + @test length(faces(m2)) == 6 + + # convert face type and add uvs + m2 = GeometryBasics.mesh(m2, facetype = GLTriangleFace, uv = decompose(Point3f, m2)) + + @test coordinates(m2) isa Vector{Point3f} + @test length(coordinates(m2)) == 24 + @test normals(m2) isa Vector{Vec3f} + @test length(normals(m2)) == 24 + @test texturecoordinates(m2) isa Vector{Point3f} + @test length(texturecoordinates(m2)) == 24 + @test faces(m2) isa Vector{GLTriangleFace} + @test length(faces(m2)) == 12 + + end +end \ No newline at end of file diff --git a/test/polygons.jl b/test/polygons.jl index 898a6f30..a9f1820e 100644 --- a/test/polygons.jl +++ b/test/polygons.jl @@ -1,28 +1,31 @@ @testset "Polygon" begin @testset "from points" begin - points = connect([1, 2, 3, 4, 5, 6], PointPoint2f(2)) + points = connect([1, 2, 3, 4, 5, 6], Point2f) polygon = Polygon(points) @test polygon == Polygon(points) end -end - -rect = Rect2f(0, 0, 1, 1) -hole = Circle(Point2f(0.5), 0.2) -poly2 = Polygon(decompose(Point2f, rect), [decompose(Point2f, hole)]) -poly1 = Polygon(rect, [hole]) -@test poly1 == poly2 -@test poly.exterior == decompose(Point2f, rect) -@test poly.interiors == [decompose(Point2f, hole)] + rect = Rect2f(0, 0, 1, 1) + hole = Tesselation(Circle(Point2f(0.5), 0.2), 8) + poly2 = Polygon(decompose(Point2f, rect), [decompose(Point2f, hole)]) + poly1 = Polygon(rect, [hole]) + @test poly1 == poly2 + @test poly1.exterior == decompose(Point2f, rect) + @test poly1.interiors == [decompose(Point2f, hole)] -faces(poly1) + # triangulation is inconsistent... + @test length(faces(poly1)) == 11 + ps = vcat(decompose(Point2f, rect), decompose(Point2f, hole)) + @test coordinates(poly1) == ps -GeometryBasics.earcut_triangulate([poly.exterior[[1, 2, 3, 4, 1]]]) + fs = GeometryBasics.earcut_triangulate([poly1.exterior[[1, 2, 3, 4, 1]]]) + @test fs == GLTriangleFace[(4,1,2), (2,3,4)] -poly = [ - [Point2f(100, 0), Point2f(100, 100), Point2f(0, 100), Point2f(0, 0)], - # Following polylines define holes. - [Point2f(75, 25), Point2f(75, 75), Point2f(25, 75), Point2f(25, 25)] -] - -GeometryBasics.earcut_triangulate(poly) + poly1 = [ + [Point2f(100, 0), Point2f(100, 100), Point2f(0, 100), Point2f(0, 0)], + # Following polylines define holes. + [Point2f(75, 25), Point2f(75, 75), Point2f(25, 75), Point2f(25, 25)] + ] + fs = GLTriangleFace[(4, 8, 7), (5, 8, 4), (3, 4, 7), (5, 4, 1), (2, 3, 7), (6, 5, 1), (2, 7, 6), (6, 1, 2)] + @test fs == GeometryBasics.earcut_triangulate(poly1) +end diff --git a/test/runtests.jl b/test/runtests.jl index 7cc3fea0..2c0170f9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,7 +1,7 @@ using Test, Random, OffsetArrays using GeometryBasics using LinearAlgebra -using GeometryBasics: MetaMesh, add_meta, pop_meta +using GeometryBasics: MetaMesh using GeoInterface using GeoJSON using Extents @@ -36,23 +36,23 @@ end tfaces = TetrahedronFace{Int}[(1, 2, 3, 4), (5, 6, 7, 8)] normals = rand(Vec{3, Float64}, 8) stress = LinRange(0, 1, 8) - mesh = MetaMesh(points, tfaces; normals = normals, stress = stress) + mesh = Mesh(points, tfaces; normal = normals, stress = stress) @test hasproperty(mesh, :stress) - @test hasproperty(mesh, :normals) + @test hasproperty(mesh, :normal) @test mesh.stress === stress - @test mesh.normals === normals - @test mesh.normals === normals + @test mesh.normal === normals + @test mesh.position === points @test GeometryBasics.faces(mesh) === tfaces - @test propertynames(mesh) == (:normals, :stress) + @test propertynames(mesh) == (:vertex_attributes, :faces, :views, :position, :normal, :stress) end end @testset "Mesh with metadata" begin m = triangle_mesh(Sphere(Point3f(0), 1)) m_meta = MetaMesh(m; boundingbox=Rect(1.0, 1.0, 2.0, 2.0)) - @test m_meta.boundingbox === Rect(1.0, 1.0, 2.0, 2.0) - @test propertynames(m_meta) == (:boundingbox,) + @test m_meta[:boundingbox] === Rect(1.0, 1.0, 2.0, 2.0) + @test collect(keys(m_meta)) == [:boundingbox,] end end @@ -130,14 +130,17 @@ end f = connect([1, 2, 3, 4], SimplexFace{4}) mesh = Mesh(points, f) @test collect(mesh) == [Tetrahedron(points...)] - + @test faces(mesh) == [TetrahedronFace{Int64}(1,2,3,4)] + @test decompose(LineFace{Int64}, mesh) == LineFace{Int64}[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] + @test decompose(GLTriangleFace, mesh) == GLTriangleFace[(2, 3, 4), (1, 3, 4), (1, 2, 4), (1, 2, 3)] + points = rand(Point3f, 8) tfaces = [GLTriangleFace(1, 2, 3), GLTriangleFace(5, 6, 7)] ns = rand(Vec3f, 8) uv = rand(Vec2f, 8) mesh = Mesh(points, tfaces) meshuv = MetaMesh(points, tfaces; uv=uv) - meshuvnormal = MetaMesh(points, tfaces; normals=ns, uv=uv) + meshuvnormal = MetaMesh(points, tfaces; normal=ns, uv=uv) t = Tesselation(Rect2f(0, 0, 2, 2), (30, 30)) m = GeometryBasics.mesh(t; pointtype=Point3f, facetype=QuadFace{Int}) @@ -173,7 +176,7 @@ end @test GeometryBasics.normals(m_normal) isa Vector{Vec3f} primitive = Rect3(0, 0, 0, 1, 1, 1) m_normal = normal_mesh(primitive) - @test GeometryBasics.normals(m_normal) isa Vector{Vec3f} + @test GeometryBasics.normals(m_normal) isa GeometryBasics.FaceView{Vec3f, Vector{Vec3f}, Vector{GLTriangleFace}} points = decompose(Point2f, Circle(Point2f(0), 1)) tmesh = triangle_mesh(points) @@ -186,61 +189,29 @@ end @test texturecoordinates(m) == nothing r2 = Rect2(0.0, 0.0, 1.0, 1.0) - @test collect(texturecoordinates(r2)) == [(0.0, 1.0), (1.0, 1.0), (0.0, 0.0), (1.0, 0.0)] + @test collect(texturecoordinates(r2)) == Point2f[(0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)] r3 = Rect3(0.0, 0.0, 1.0, 1.0, 2.0, 2.0) @test first(texturecoordinates(r3)) == Vec3(0, 0, 0) uv = decompose_uv(m) - @test Rect(Point.(uv)) == Rect(0, 0, 1, 1) + @test_broken false # Rect(Point.(uv)) == Rect(0, 0, 1, 1) # decompose_uv must now produces 2D uvs + uvw = GeometryBasics.decompose_uvw(m) + @test Rect(Point.(uvw)) == Rect(Point3f(0), Vec3f(1)) points = decompose(Point2f, Circle(Point2f(0), 1)) m = GeometryBasics.mesh(points) @test coordinates(m) === points + + fs = [QuadFace(1,2,3,4), QuadFace(3,4,5,6), QuadFace(7,8,9,10)] + views = [1:2, 3:3] + new_fs, new_views = decompose(GLTriangleFace, fs, views) + @test new_fs == GLTriangleFace[(1, 2, 3), (1, 3, 4), (3, 4, 5), (3, 5, 6), (7, 8, 9), (7, 9, 10)] + @test new_views == [1:4, 5:6] end @testset "convert mesh + meta" begin m = uv_normal_mesh(Circle(Point2f(0), 1f0)) # For 2d primitives normal is just the upvector - m.normals == [Vec3f(0, 0, 1) for p in coordinates(m)] -end - -@testset "convert mesh + meta" begin - m = uv_normal_mesh(Rect3f(Vec3f(-1), Vec3f(1, 2, 3))) - m_normal = add_meta(m; normals = decompose_normals(m)) - # make sure we don't loose the uv - @test hasproperty(m_normal, :uv) - # Make sure we don't create any copies - @test coordinates(m) === coordinates(m_normal) - @test m.normals == m_normal.normals - @test m.uv === m_normal.uv - - m = uv_mesh(Rect3f(Vec3f(-1), Vec3f(1, 2, 3))) - m_normal = add_meta(m, normals = decompose_normals(m)) - @test hasproperty(m_normal, :uv) - @test coordinates(m) === coordinates(m_normal) - @test decompose_normals(m) == GeometryBasics.normals(m_normal) - # uv stays untouched, since we don't specify the element type in normalmesh - @test m.uv === m_normal.uv -end - -@testset "modifying meta" begin - xx = rand(10) - points = rand(Point3f, 10) - m = MetaMesh(points, GLTriangleFace[(1,2,3), (3,4,5)]; xx=xx) - color = rand(10) - m = add_meta(m; color=color) - - @test hasproperty(m, :xx) - @test hasproperty(m, :color) - - @test m.xx === xx - @test m.color === color - - m, colpopt = GeometryBasics.pop_meta(m, :color) - m, xxpopt = GeometryBasics.pop_meta(m, :xx) - - @test propertynames(m) == () - @test colpopt === color - @test xxpopt === xx + @test m.normal == [Vec3f(0, 0, 1) for p in coordinates(m)] end @testset "mesh conversion" begin @@ -339,10 +310,19 @@ end include("geointerface.jl") end +include("polygons.jl") + using Aqua # Aqua tests # Intervals brings a bunch of ambiquities unfortunately # seems like we also run into https://github.com/JuliaTesting/Aqua.jl/issues/86 -Aqua.test_all(GeometryBasics; ambiguities=false, unbound_args=false) +# Aqua.test_ambiguities([GeometryBasics, Base, Core]) +# Aqua.test_unbound_args(GeometryBasics) +Aqua.test_undefined_exports(GeometryBasics) +Aqua.test_project_extras(GeometryBasics) +Aqua.test_stale_deps(GeometryBasics, ignore = [:PrecompileTools]) +Aqua.test_deps_compat(GeometryBasics) +Aqua.test_piracies(GeometryBasics) +Aqua.test_persistent_tasks(GeometryBasics) end # testset "GeometryBasics" From 73ae2c18df50435e0d69251c231c592308515c6a Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 16 Oct 2024 17:59:04 +0200 Subject: [PATCH 51/58] add GLMakie to docs --- .github/workflows/ci.yml | 8 +++++--- docs/src/meshes.md | 9 ++++++++- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cb3d95f..7f1a3637 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,18 +48,20 @@ jobs: file: lcov.info docs: name: Documentation - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 env: JULIA_PKG_SERVER: "" steps: - uses: actions/checkout@v2 + - run: sudo apt-get update && sudo apt-get install -y xorg-dev mesa-utils xvfb libgl1 freeglut3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev xsettingsd x11-xserver-utils - uses: julia-actions/setup-julia@v1 with: - version: "1.7" + version: "1.11" - run: | - julia --project=docs -e ' + DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=docs -e ' using Pkg Pkg.develop(PackageSpec(path=pwd())) + pkg"MeshIO#ff/GeometryBasics_refactor MakieCore#ff/GeometryBasics_refactor Makie#ff/GeometryBasics_refactor GLMakie#ff/GeometryBasics_refactor" Pkg.instantiate()' - run: julia --project=docs docs/make.jl env: diff --git a/docs/src/meshes.md b/docs/src/meshes.md index b9b1564f..3100805d 100644 --- a/docs/src/meshes.md +++ b/docs/src/meshes.md @@ -39,7 +39,7 @@ If we ever need the mesh to be defined with just one common set of faces, i.e. n On a larger scale this can be useful for memory and performance reason, e.g. when you do calculations with vertex attributes. It can also simplify some definitions, like for example `Rect3`. -In that case we have 8 positions and 6 normals with FaceViews, or 24 without (assuming per-face normals). +In that case we have 8 positions and 6 normals with FaceViews, or 24 without (assuming per-face normals). ## MetaMesh @@ -78,3 +78,10 @@ Note that this doesn't remove any data (e.g. hidden or duplicate vertices), and ### MeshIO.jl The [`MeshIO.jl`](https://github.com/JuliaIO/MeshIO.jl) package provides load/save support for several file formats which store meshes. + +```@example +using GLMakie, GLMakie.FileIO, GeometryBasics + +m = load(GLMakie.assetpath("cat.obj")) +GLMakie.mesh(m; color=load(GLMakie.assetpath("diffusemap.png")), axis=(; show_axis=false)) +``` From 6ba28064e0cfb85fb6df093f7acabfa293e6dafc Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 16 Oct 2024 18:08:06 +0200 Subject: [PATCH 52/58] fix pkg command --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f1a3637..80124fde 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,7 +61,7 @@ jobs: DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=docs -e ' using Pkg Pkg.develop(PackageSpec(path=pwd())) - pkg"MeshIO#ff/GeometryBasics_refactor MakieCore#ff/GeometryBasics_refactor Makie#ff/GeometryBasics_refactor GLMakie#ff/GeometryBasics_refactor" + pkg"add MeshIO#ff/GeometryBasics_refactor MakieCore#ff/GeometryBasics_refactor Makie#ff/GeometryBasics_refactor GLMakie#ff/GeometryBasics_refactor" Pkg.instantiate()' - run: julia --project=docs docs/make.jl env: From c8ba30b990bbda742995199956c2c44c8419f2bf Mon Sep 17 00:00:00 2001 From: = Date: Wed, 16 Oct 2024 18:17:03 +0200 Subject: [PATCH 53/58] rename clear_faceviews & update --- docs/src/meshes.md | 28 +++++++--------------------- src/GeometryBasics.jl | 2 +- src/basic_types.jl | 11 +++++++---- src/meshes.jl | 8 ++++---- src/precompiles.jl | 2 +- test/meshes.jl | 8 ++++---- 6 files changed, 24 insertions(+), 35 deletions(-) diff --git a/docs/src/meshes.md b/docs/src/meshes.md index b9b1564f..c1c12e33 100644 --- a/docs/src/meshes.md +++ b/docs/src/meshes.md @@ -19,23 +19,17 @@ You can also grab the contents of `mesh.vertex_attributes` as if they were field ### FaceView -As mentioned above, a vertex attribute can be a `FaceView`. -A `FaceView` is simply defined as a vector of data and a vector of faces: - -```julia -struct FaceView{T, AVT <: AbstractVector{T}, FVT <: AbstractVector{<: AbstractFace}} - data::AVT - faces::FVT -end -``` -Its purpose is to allow you to add data that needs to be defined per vertex but does not match the vertex structure used by `mesh.faces`. +```@docs; canoniical=false +FaceView +``` +The purpose of FaceView is to allow you to add data that doesn't use the same vertex indices as `mesh.faces` As a minimal example consider a mesh that is just one triangle, i.e. 3 position and one triangle face `TriangleFace(1,2,3)`. Let's say we want to add a flat color to the triangle. In this case we only have one color, but our face refers to 3 different vertices (3 different positions). To avoid duplicating the color data, we can instead define a new triangle face `TriangleFace(1)` and add the color attribute as a `FaceView([color], [TriangleFace(1)])`. -If we ever need the mesh to be defined with just one common set of faces, i.e. no FaceView and appropriately duplicated vertex data, we can use `clear_faceviews(mesh)` to generate it. +If we ever need the mesh to be defined with just one common set of faces, i.e. no FaceView and appropriately duplicated vertex data, we can use `expand_faceviews(mesh)` to generate it. On a larger scale this can be useful for memory and performance reason, e.g. when you do calculations with vertex attributes. It can also simplify some definitions, like for example `Rect3`. @@ -44,18 +38,10 @@ In that case we have 8 positions and 6 normals with FaceViews, or 24 without (as ## MetaMesh -A `MetaMesh` is given by - -```julia -struct MetaMesh{Dim, T, M <: AbstractMesh{Dim, T}} <: AbstractMesh{Dim, T} - mesh::M - meta::Dict{Symbol, Any} -end +```julia; canonical=false +MetaMesh ``` -where `meta` may contain any data you want to include with a mesh. -For example, you could include group names or material data corresponding to `mesh.views`. - ## How to create a mesh ### GeometryBasics diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index 21554b83..a430d861 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -41,7 +41,7 @@ export AbstractFace, TriangleFace, QuadFace, GLTriangleFace export OffsetInteger, ZeroIndex, OneIndex, GLIndex export decompose, coordinates, faces, normals, decompose_uv, decompose_normals, texturecoordinates, vertex_attributes -export clear_faceviews +export expand_faceviews export face_normals export Tesselation, Normal, UV, UVW export AbstractMesh, Mesh, MetaMesh, FaceView diff --git a/src/basic_types.jl b/src/basic_types.jl index 855102c2..dd07c657 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -382,9 +382,9 @@ Base.length(mpt::MultiPoint) = length(mpt.points) """ FaceView(data, faces) -A FaceView is an alternative to passing a vertex attributes directly to a mesh. +A FaceView is an alternative to passing a vertex attribute directly to a mesh. It bundles `data` with a new set of `faces` which may index that data differently -from the faces defined in a mesh. This can be useful to avoid duplication of data. +from the faces defined in a mesh. This can be useful to avoid duplication in `data`. For example, `data` can be defined per face by giving each face just one (repeated) index: @@ -395,7 +395,9 @@ per_face_normals = FaceView( ) ``` -To remove `FaceView`s from a mesh, e.g. for rendering, use `clear_faceviews(mesh)`. +If you need a mesh with strictly per-vertex data, e.g. for rendering, you can use +`expand_faceviews(mesh)` to convert every vertex attribute to be per-vertex. This +will duplicate data and reorder faces as needed. You can get the data of a FaceView with `values(faceview)` and the faces with `faces(faceview)`. @@ -715,7 +717,8 @@ with the given `positions` and `faces`. Any keyword arguments given will be stored in the `meta` field in `MetaMesh`. This struct is meant to be used for storage of non-vertex data. Any vertex -related data should be stored as a vertex attribute in `Mesh`. +related data should be stored as a vertex attribute in `Mesh`. One example of such +data is material data, which is defined per view in `mesh.views`, i.e. per submesh. The metadata added to the MetaMesh can be manipulated with Dict-like operations (getindex, setindex!, get, delete, keys, etc). Vertex attributes can be accessed diff --git a/src/meshes.jl b/src/meshes.jl index 0953ea0a..93b664c6 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -236,7 +236,7 @@ end Generates a new mesh containing all the data of the individual meshes. If all meshes are consistent in their use of FaceViews they will be preserved. -Otherwise all of them will be converted with `clear_faceviews(mesh)`. +Otherwise all of them will be converted with `expand_faceviews(mesh)`. This function will generate `views` in the new mesh which correspond to the inputs of this function. @@ -322,7 +322,7 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) else # mixed FaceViews and Arrays # simplify to VertexFace types, then retry merge - return merge(clear_faceviews.(meshes)) + return merge(expand_faceviews.(meshes)) end @@ -330,14 +330,14 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) end """ - clear_faceviews(mesh::Mesh) + expand_faceviews(mesh::Mesh) Returns the given `mesh` if it contains no FaceViews. Otherwise, generates a new mesh that contains no FaceViews, reordering and duplicating vertex atttributes as necessary. If the mesh has `views` they will be adjusted as needed to produce the same submeshes. """ -function clear_faceviews(mesh::Mesh) +function expand_faceviews(mesh::Mesh) main_fs = faces(mesh) va = vertex_attributes(mesh) diff --git a/src/precompiles.jl b/src/precompiles.jl index 100a69dc..e0a2b26b 100644 --- a/src/precompiles.jl +++ b/src/precompiles.jl @@ -9,7 +9,7 @@ using PrecompileTools: @setup_workload, @compile_workload c = Circle(Point2f(0), 1) m2 = uv_normal_mesh(c, pointtype = Point3f) # hits normal gen - m = merge([m1, m2]) # hits mixed path, clear_faceviews, then normal path + m = merge([m1, m2]) # hits mixed path, expand_faceviews, then normal path GeometryBasics.split_mesh(m) Rect3d(m) diff --git a/test/meshes.jl b/test/meshes.jl index 485fa54b..4c42cb3e 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -40,7 +40,7 @@ end normal = GeometryBasics.FaceView([Vec3f(0,0,1)], [QuadFace(1)]) ) - m2 = GeometryBasics.clear_faceviews(m) + m2 = GeometryBasics.expand_faceviews(m) @test faces(m) == [QuadFace(1,2,3,4)] @test coordinates(m) == Point2f[(0, 0), (1, 0), (1, 1), (0, 1)] @@ -82,7 +82,7 @@ end converted_meshes = map(rects) do r m = GeometryBasics.Mesh(coordinates(r), faces(r), normal = normals(r)) - GeometryBasics.clear_faceviews(m) + GeometryBasics.expand_faceviews(m) end cm = merge(converted_meshes) @@ -210,7 +210,7 @@ end @testset "normal_mesh()" begin m = normal_mesh(r, pointtype = Point3f, normaltype = Vec3f) - m = GeometryBasics.clear_faceviews(m) + m = GeometryBasics.expand_faceviews(m) @test hasproperty(m, :position) @test coordinates(m) isa Vector{Point3f} @@ -311,7 +311,7 @@ end @test_throws ErrorException GeometryBasics.mesh(m, uv = Vec3f[]) # remap vertex attributes to merge faceviews into one face array - m2 = GeometryBasics.clear_faceviews(m) + m2 = GeometryBasics.expand_faceviews(m) @test coordinates(m2) isa Vector{Point3f} @test length(coordinates(m2)) == 24 From 14ab2d356a8f06f46058f4950899fa1bc8642008 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 16 Oct 2024 18:19:09 +0200 Subject: [PATCH 54/58] fix pkg --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80124fde..71e3d203 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,7 +61,7 @@ jobs: DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=docs -e ' using Pkg Pkg.develop(PackageSpec(path=pwd())) - pkg"add MeshIO#ff/GeometryBasics_refactor MakieCore#ff/GeometryBasics_refactor Makie#ff/GeometryBasics_refactor GLMakie#ff/GeometryBasics_refactor" + pkg"add MeshIO#ff/GeometryBasics_refactor MakieCore#breaking-0.22 Makie#breaking-0.22 GLMakie#breaking-0.22" Pkg.instantiate()' - run: julia --project=docs docs/make.jl env: From 5fe7d9e069870c076a56dcfd85201b8c70fa9b8f Mon Sep 17 00:00:00 2001 From: = Date: Wed, 16 Oct 2024 18:22:31 +0200 Subject: [PATCH 55/58] fix another clear_faceviews --- src/basic_types.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index dd07c657..49c21ad1 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -526,7 +526,7 @@ AbstractVector must match in length. For FaceViews, the number of faces needs to See also: [`vertex_attributes`](@ref), [`coordinates`](@ref), [`normals`](@ref), [`texturecoordinates`](@ref), [`decompose`](@ref), [`FaceView`](@ref), -[`clear_faceviews`](@ref) +[`expand_faceviews`](@ref) The `faces` field is a collection `<: AbstractVector{FaceType}` containing faces that describe how vertices are connected. Typically these are `(GL)TriangleFace`s From a3b5ddd39a3c89708a72ac21dc98862dd4caaece Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 16 Oct 2024 18:36:23 +0200 Subject: [PATCH 56/58] fix docs --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 71e3d203..febfd9fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,7 +63,7 @@ jobs: Pkg.develop(PackageSpec(path=pwd())) pkg"add MeshIO#ff/GeometryBasics_refactor MakieCore#breaking-0.22 Makie#breaking-0.22 GLMakie#breaking-0.22" Pkg.instantiate()' - - run: julia --project=docs docs/make.jl + - run: DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=docs docs/make.jl env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} From 06fb3ea9b86e0e987e17336b367e9b5e184d38b0 Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 16 Oct 2024 18:43:12 +0200 Subject: [PATCH 57/58] use action cache --- .github/workflows/ci.yml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index febfd9fe..4603e5fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,16 +30,7 @@ jobs: with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v1 - env: - cache-name: cache-artifacts - with: - path: ~/.julia/artifacts - key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} - restore-keys: | - ${{ runner.os }}-test-${{ env.cache-name }}- - ${{ runner.os }}-test- - ${{ runner.os }}- + - uses: julia-actions/cache@v2 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 @@ -57,6 +48,7 @@ jobs: - uses: julia-actions/setup-julia@v1 with: version: "1.11" + - uses: julia-actions/cache@v2 - run: | DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=docs -e ' using Pkg From 4a27454597c99ac9ac968a83663eae0c7e310b1c Mon Sep 17 00:00:00 2001 From: SimonDanisch Date: Wed, 16 Oct 2024 18:59:31 +0200 Subject: [PATCH 58/58] fix typo --- docs/src/meshes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/meshes.md b/docs/src/meshes.md index 3bc7a2ed..51e11a0b 100644 --- a/docs/src/meshes.md +++ b/docs/src/meshes.md @@ -20,7 +20,7 @@ You can also grab the contents of `mesh.vertex_attributes` as if they were field ### FaceView -```@docs; canoniical=false +```@docs; canonical=false FaceView ```