From 80b89e391e53951ee2f864c6047955ac98f9abb7 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 30 Aug 2024 22:06:21 +0200 Subject: [PATCH 001/120] update MetaMesh --- src/basic_types.jl | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 0b3f39af..af20f673 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -313,20 +313,21 @@ 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} +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) + meta::Dict{Symbol, Any} + + function MetaMesh(mesh::AbstractMesh{Dim, T}, meta::Dict{Symbol, Any}) where {Dim, T} + return new{Dim, T, typeof(mesh)}(mesh, meta) end end -function MetaMesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:AbstractFace}; meta...) - MetaMesh(Mesh(points, faces), values(meta)) +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 Base.hasproperty(mesh::MetaMesh, field::Symbol) = hasproperty(getfield(mesh, :meta), field) From 1aa721e9d3a2eebed307b77fc00687056a43bfe3 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 31 Aug 2024 02:37:17 +0200 Subject: [PATCH 002/120] add MultiFace type --- src/basic_types.jl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index af20f673..96426c02 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -38,7 +38,6 @@ 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} @@ -51,6 +50,13 @@ end Face(::Type{<:NgonFace{N}}, ::Type{T}) where {N,T} = NgonFace{N,T} Face(F::Type{NgonFace{N,FT}}, ::Type{T}) where {FT,N,T} = F +struct MultiFace{N, T, FaceType <: AbstractFace{N, T}, Names, M} + pos_face::FaceType + attrib_faces::NamedTuple{Names, NTuple{M, FaceType}} +end + +MultiFace(face::AbstractFace; kwargs...) = MultiFace(face, NamedTuple(kwargs)) + @propagate_inbounds Base.getindex(x::Polytope, i::Integer) = coordinates(x)[i] @propagate_inbounds Base.iterate(x::Polytope) = iterate(coordinates(x)) @propagate_inbounds Base.iterate(x::Polytope, i) = iterate(coordinates(x), i) From b08b6f9179c02e6406757cf3cf4984e1b2514d7b Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 31 Aug 2024 14:02:29 +0200 Subject: [PATCH 003/120] update Mesh & MultiFace types --- src/basic_types.jl | 67 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 9 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 96426c02..5cc3e7be 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -50,12 +50,18 @@ end Face(::Type{<:NgonFace{N}}, ::Type{T}) where {N,T} = NgonFace{N,T} Face(F::Type{NgonFace{N,FT}}, ::Type{T}) where {FT,N,T} = F -struct MultiFace{N, T, FaceType <: AbstractFace{N, T}, Names, M} - pos_face::FaceType - attrib_faces::NamedTuple{Names, NTuple{M, FaceType}} -end +struct MultiFace{N, T, FaceType <: AbstractFace{N, T}, Names, M} <: AbstractFace{N, T} + faces::NamedTuple{Names, NTuple{M, FaceType}} -MultiFace(face::AbstractFace; kwargs...) = MultiFace(face, NamedTuple(kwargs)) + function MultiFace(nt::NamedTuple{Names, NTuple{M, FT}}) where {N, T, FT <: AbstractFace{N, T}, Names, M} + if FT <: MultiFace + error("A MultiFace cannot contain MultiFaces.") + end + + return new{N, T, FT, Names, M}(nt) + end +end +Base.names(::Type{<: MultiFace{N, T, FT, Names}}) where {N, T, FT, Names} = Names @propagate_inbounds Base.getindex(x::Polytope, i::Integer) = coordinates(x)[i] @propagate_inbounds Base.iterate(x::Polytope) = iterate(coordinates(x)) @@ -295,11 +301,54 @@ abstract type AbstractMesh{Dim, T} <: AbstractGeometry{Dim, T} end Mesh <: AbstractMesh{Element} The concrete AbstractMesh type. """ -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, # TODO: Number? + FaceType <: AbstractFace, + Names, + VertexAttribTypes <: Tuple{AbstractVector{Point{Dim, T}}, Vararg{AbstractVector}}, + FaceVecType <: AbstractVector{FaceType} + } <: AbstractMesh{Dim, T} + + vertex_attributes::NamedTuple{Names, VertexAttribTypes} + connectivity::FaceVecType + views::Vector{UnitRange} + + function Mesh( + va::NamedTuple{Names, VAT}, + f::FVT, + views::Vector{UnitRange} = UnitRange[] + ) where { + D, T, FT, Names, + VAT <: Tuple{AbstractVector{Point{D, T}}, Vararg{AbstractVector}}, + FVT <: AbstractVector{FT} + } + + # verify type + if first(Names) !== :position + error("The first vertex attribute should be a 'position' but is a '$(first(Names))'.") + end + + if FT <: MultiFace + f_names = names(FT) + if Names != f_names + error( + "Cannot construct a mesh with vertex attribute names $Names and MultiFace " * + "attribute names $f_names. These must include the same names in the same order." + ) + end + elseif MultiFace <: FT + # TODO: This is supposed to catch mixed types like + # [MultiFace(position = f1, normal = f2), MultiFace(position = f3)] + # but really just catches AbstractFace{N, T}[]. Technically we can + # probably handle mixtures of MultiFace and other Face types, but do + # we want to bother? Also do we want to allow mixtures of e.g. + # TriangleFace and QuadFace? + error("Face vectors that may include `MultiFace`s with different names are not allowed. (Type $FT too abstract.)") + end + + return new{D, T, FT, Names, VAT, FVT}(va, f, views) + end 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) From d51280a21e3c2a6cbcada2c61124a98de42ccc75 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 31 Aug 2024 15:05:18 +0200 Subject: [PATCH 004/120] update Mesh & MetaMesh utils --- src/basic_types.jl | 82 ++++++++++++++++++++++++++------ src/meshes.jl | 114 ++++++++++++++++++++++++--------------------- test/runtests.jl | 6 +-- 3 files changed, 132 insertions(+), 70 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 5cc3e7be..8dfb46b5 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -349,18 +349,57 @@ struct Mesh{ return new{D, T, FT, Names, VAT, FVT}(va, f, views) end end + +@inline function Base.hasproperty(mesh::Mesh, field::Symbol) + 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) + 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.connectivity -Base.getindex(mesh::Mesh, i::Integer) = mesh.vertices[mesh.connectivity[i]] +normals(mesh::Mesh) = hasproperty(mesh, :normals) ? mesh.normals : nothing +texturecoordinates(mesh::Mesh) = hasproperty(mesh, :uv) ? mesh.uv : nothing +vertex_attributes(mesh::Mesh) = getfield(mesh, :vertex_attributes) + +Base.getindex(mesh::Mesh, i::Integer) = mesh[mesh.connectivity[i]] Base.length(mesh::Mesh) = length(mesh.connectivity) -Base.:(==)(a::Mesh, b::Mesh) = coordinates(a) == coordinates(b) && faces(a) == faces(b) + +# TODO: temp +function Base.getindex(mesh::Mesh{D, T, <: AbstractFace, (:position,)}, f::AbstractFace) where {D, T} + return getfield(mesh, :vertex_attributes).position[f] +end +function Base.getindex(::Mesh, f::MultiFace) + error("TODO") +end +function Base.getindex(::Mesh, f::AbstractFace) + error("TODO") +end + +function Base.:(==)(a::Mesh, b::Mesh) + return a.vertex_attributes == b.vertex_attributes && + faces(a) == faces(b) && a.views == b.views +end function Base.iterate(mesh::Mesh, i=1) return i - 1 < length(mesh) ? (mesh[i], i + 1) : nothing end +function Mesh(faces::AbstractVector{<:AbstractFace}; attributes...) + return Mesh(NamedTuple(attributes), faces) +end + function Mesh(points::AbstractVector{Point{Dim, T}}, - faces::AbstractVector{<:AbstractFace}) where {Dim, T} - return Mesh{Dim, T, }(points, faces) + faces::AbstractVector{<:AbstractFace}; kwargs...) where {Dim, T} + return Mesh((position = points, kwargs...), faces) end function Mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Integer}, @@ -368,13 +407,10 @@ function Mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Integer}, return Mesh(points, connect(faces, facetype, skip)) end + struct MetaMesh{Dim, T, M <: AbstractMesh{Dim, T}} <: AbstractMesh{Dim, T} mesh::M meta::Dict{Symbol, Any} - - function MetaMesh(mesh::AbstractMesh{Dim, T}, meta::Dict{Symbol, Any}) where {Dim, T} - return new{Dim, T, typeof(mesh)}(mesh, meta) - end end function MetaMesh(mesh::AbstractMesh; kwargs...) @@ -385,14 +421,34 @@ function MetaMesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Abstr MetaMesh(Mesh(points, faces), Dict{Symbol, Any}(kwargs)) 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: Do we want to access meta here? +@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 + +# TODO: or via getindex? +Base.haskey(mesh::MetaMesh, key::Symbol) = haskey(getfield(mesh, :meta), key) +Base.get(f, mesh::MetaMesh, key::Symbol) = get(f, getfield(mesh, :meta), key) +Base.get!(f, mesh::MetaMesh, key::Symbol) = get!(f, getfield(mesh, :meta), key) +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) 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) diff --git a/src/meshes.jl b/src/meshes.jl index b29e1888..aef5c7d2 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -11,7 +11,7 @@ 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::AbstractGeometry; pointtype=Point, facetype=GLTriangleFace) +function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleFace, vertex_attributes...) positions = decompose(pointtype, primitive) _f = faces(primitive) # If faces returns nothing for primitive, we try to triangulate! @@ -25,11 +25,11 @@ function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleF else f = decompose(facetype, _f) end - return Mesh(positions, f) + return Mesh(positions, f; vertex_attributes...) end -const SimpleMesh{N, T, FT} = Mesh{N, T, Vector{Point{N, T}}, Vector{FT}} -const TriangleMesh{N} = SimpleMesh{N, Float32, GLTriangleFace} +const SimpleMesh{N, T, FT} = Mesh{N, T, FT, (:position,), Tuple{Vector{Point{N, T}}}, Vector{FT}} +const SimpleTriangleMesh{N} = SimpleMesh{N, Float32, GLTriangleFace} """ mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace, @@ -58,26 +58,35 @@ function triangle_mesh(primitive::Mesh{N}) where {N} end end +function triangle_mesh(primitive::Union{AbstractGeometry{N}, AbstractVector{<: Point{N}}}; nvertices = nothing)::SimpleTriangleMesh{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),)) + return mesh(primitive, uv = decompose_uv(primitive), pointtype=Point{N,Float32}, facetype=GLTriangleFace) 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))) + return mesh( + primitive, uv = decompose_uv(primitive), normals = decompose_normals(primitive), + pointtype=Point{N,Float32}, facetype=GLTriangleFace) 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),)) + return Mesh(_faces, position = _points, normals = normals(_points, _faces)) end function normal_mesh(primitive::AbstractGeometry{N}) where {N} - m = triangle_mesh(primitive) - return MetaMesh(m, (normals=decompose_normals(m),)) + return mesh( + primitive, normals = decompose_normals(primitive), + pointtype=Point{N,Float32}, facetype=GLTriangleFace) end """ @@ -156,59 +165,56 @@ 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)) +# TODO: +add_meta(m, kw...) = error("TODO") +pop_meta(m, kw...) = error("TODO") +# 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 + +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): ", 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/test/runtests.jl b/test/runtests.jl index 7cc3fea0..7be77087 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -36,13 +36,13 @@ 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; normals = normals, stress = stress) @test hasproperty(mesh, :stress) @test hasproperty(mesh, :normals) @test mesh.stress === stress @test mesh.normals === normals - @test mesh.normals === normals + @test mesh.position === points @test GeometryBasics.faces(mesh) === tfaces @test propertynames(mesh) == (:normals, :stress) end @@ -200,7 +200,7 @@ 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)] + @test m.normals == [Vec3f(0, 0, 1) for p in coordinates(m)] end @testset "convert mesh + meta" begin From f08b12068cb3871e64aba2fb76cbd3d5c9cc0dc5 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 31 Aug 2024 16:50:44 +0200 Subject: [PATCH 005/120] add MultiFace remapping code --- src/basic_types.jl | 13 +++++- src/meshes.jl | 101 +++++++++++++++++++++++++++++++++++++++++++++ test/meshes.jl | 22 ++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 8dfb46b5..7998ac4d 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -61,7 +61,18 @@ struct MultiFace{N, T, FaceType <: AbstractFace{N, T}, Names, M} <: AbstractFace return new{N, T, FT, Names, M}(nt) end end + +MultiFace(; kwargs...) = MultiFace(NamedTuple(kwargs)) +MultiFace{Names}(args...) where {Names} = MultiFace(NamedTuple{Names}(args)) + Base.names(::Type{<: MultiFace{N, T, FT, Names}}) where {N, T, FT, Names} = Names +function Base.getindex(f::MultiFace{N, T, FT, Names, N_Attrib}, i::Integer) where {N, T, FT, Names, N_Attrib} + return ntuple(n -> f.faces[n][i], N_Attrib) +end + +# TODO: Some shorthands +const NormalFace = MultiFace{(:position, :normal)} +const NormalUVFace = MultiFace{(:position, :normal, :uv)} @propagate_inbounds Base.getindex(x::Polytope, i::Integer) = coordinates(x)[i] @propagate_inbounds Base.iterate(x::Polytope) = iterate(coordinates(x)) @@ -366,7 +377,7 @@ end coordinates(mesh::Mesh) = mesh.position faces(mesh::Mesh) = mesh.connectivity -normals(mesh::Mesh) = hasproperty(mesh, :normals) ? mesh.normals : nothing +normals(mesh::Mesh) = hasproperty(mesh, :normal) ? mesh.normal : nothing texturecoordinates(mesh::Mesh) = hasproperty(mesh, :uv) ? mesh.uv : nothing vertex_attributes(mesh::Mesh) = getfield(mesh, :vertex_attributes) diff --git a/src/meshes.jl b/src/meshes.jl index aef5c7d2..272d0b77 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -146,6 +146,107 @@ function Base.merge(meshes::AbstractVector{T}) where T <: MetaMesh return MetaMesh(big_mesh, big_meta) end +# TODO: naming +# synchronize_vertex_attributes +# merge_vertex_(attribute)_indices +# convert(Face, MultiFace) +# ... +function merge_vertex_indices(mesh) + attribs, fs, views = merge_vertex_indices( + vertex_attributes(mesh), faces(mesh), mesh.views) + + return Mesh(attribs, fs, views) +end + +function merge_vertex_indices( + attribs::NamedTuple{Names}, + faces::AbstractVector{<: MultiFace{N, T, FT, Names}}, + views::Vector{UnitRange} + ) where {Names, N, T, FT} + + # Note: typing checks for matching Names + + if isempty(views) + new_faces, vertex_map = merge_vertex_indices(faces) + new_attribs = ntuple(n -> attribs[n][vertex_map[n]], length(Names)) + return NamedTuple{Names}(new_attribs), new_faces, views + end + + new_attribs = NamedTuple((Pair(k, similar(v, 0)) for (k, v) in pairs(attribs))) + new_faces = similar(faces, FT, 0) + new_views = UnitRange[] + + for idxs in views + # TODO: this depends on T in Face (1 based -> 1, 0 based -> 0) + vertex_index_counter = T(length(new_attribs[1]) + 1) + + # Generate new face from current view, with the first vertex_index + # corresponding to the first vertex attribute added in this iteration + face_view = view(faces, idxs) + new_faces_in_view, vertex_map = merge_vertex_indices(face_view, vertex_index_counter) + + # update vertex attributes + for (name, indices) in pairs(vertex_map) + append!(new_attribs[name], view(attribs[name], indices)) + end + + # add new faces and new view + start = length(new_faces) + 1 + append!(new_faces, new_faces_in_view) + append!(new_views, start:length(new_faces)) + end + + return new_attribs, new_faces, new_views +end + +function merge_vertex_indices( + faces::AbstractVector{<: MultiFace{N, T, FT, Names, N_Attrib}}, + vertex_index_counter = T(1) + ) where {N, T, FT <: AbstractFace{N, T}, Names, N_Attrib} + + N_faces = length(faces) + + # 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 = zeros(N) + + for multi_face in 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.faces[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 + + + function map_coordinates(f, mesh::Mesh) result = copy(mesh) diff --git a/test/meshes.jl b/test/meshes.jl index 192c7400..408a59f8 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -32,3 +32,25 @@ end # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/136 merge(Mesh[]) == Mesh(Point3f[], GLTriangleFace[]) end + +@testset "Vertex Index Remapping" begin + # Sanity Check + # TODO: extend + m = Mesh( + [GeometryBasics.NormalFace(QuadFace(1, 2, 3, 4), QuadFace(1,1,1,1))], + position = Point2f[(0, 0), (1, 0), (1, 1), (0, 1)], + normal = [Vec3f(0,0,1)] + ) + + m2 = GeometryBasics.merge_vertex_indices(m) + + @test faces(m) isa AbstractVector{<: GeometryBasics.MultiFace} + @test names(eltype(faces(m))) == keys(GeometryBasics.vertex_attributes(m)) + @test isempty(m.views) + + @test faces(m2) isa AbstractVector{<: QuadFace} + @test coordinates(m2) == coordinates(m) + @test normals(m2) != normals(m) + @test normals(m2) == [only(normals(m)) for _ in 1:4] + @test isempty(m2.views) +end \ No newline at end of file From ced6283ea9c0cdff515cdf3f00ed03c6e4bac8fd Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 31 Aug 2024 17:50:12 +0200 Subject: [PATCH 006/120] prototype MultiFace Rect -> Mesh pipeline --- src/basic_types.jl | 15 ++++++++++----- src/meshes.jl | 28 ++++++++++++++++++++++++---- src/primitives/rectangles.jl | 29 ++++++++++++++++++++--------- 3 files changed, 54 insertions(+), 18 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 7998ac4d..7943a4c3 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -64,16 +64,21 @@ end MultiFace(; kwargs...) = MultiFace(NamedTuple(kwargs)) MultiFace{Names}(args...) where {Names} = MultiFace(NamedTuple{Names}(args)) +MultiFace{Names}(args::Tuple{Vararg{<: AbstractFace}}) where {Names} = MultiFace(NamedTuple{Names}(args)) +MultiFace{Names, FT}(args) where {Names, FT <: AbstractFace} = MultiFace(NamedTuple{Names}(FT.(args))) -Base.names(::Type{<: MultiFace{N, T, FT, Names}}) where {N, T, FT, Names} = Names -function Base.getindex(f::MultiFace{N, T, FT, Names, N_Attrib}, i::Integer) where {N, T, FT, Names, N_Attrib} - return ntuple(n -> f.faces[n][i], N_Attrib) -end +Base.getindex(f::MultiFace, i::Int64) = Base.getindex(getfield(f, :faces), i) # TODO: StaticVector should index with Integer +@inline Base.hasproperty(f::MultiFace, field::Symbol) = hasproperty(getfield(f, :faces), field) +@inline Base.getproperty(f::MultiFace, field::Symbol) = getproperty(getfield(f, :faces), field) +@inline Base.propertynames(f::MultiFace) = propertynames(getfield(f, :faces)) +@inline Base.propertynames(::Type{<: MultiFace{N, T, FT, Names}}) where {N, T, FT, Names} = Names # TODO: Some shorthands const NormalFace = MultiFace{(:position, :normal)} const NormalUVFace = MultiFace{(:position, :normal, :uv)} +# TODO: enable something like NormalUVFace{QuadFace}[...] + @propagate_inbounds Base.getindex(x::Polytope, i::Integer) = coordinates(x)[i] @propagate_inbounds Base.iterate(x::Polytope) = iterate(coordinates(x)) @propagate_inbounds Base.iterate(x::Polytope, i) = iterate(coordinates(x), i) @@ -340,7 +345,7 @@ struct Mesh{ end if FT <: MultiFace - f_names = names(FT) + f_names = propertynames(FT) if Names != f_names error( "Cannot construct a mesh with vertex attribute names $Names and MultiFace " * diff --git a/src/meshes.jl b/src/meshes.jl index 272d0b77..58b50182 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -14,6 +14,7 @@ It also only losely correlates to the number of vertices, depending on the algor function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleFace, vertex_attributes...) positions = decompose(pointtype, primitive) _f = faces(primitive) + # If faces returns nothing for primitive, we try to triangulate! if isnothing(_f) if eltype(positions) <: Point2 @@ -23,11 +24,30 @@ function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleF error("No triangulation for $(typeof(primitive))") end else + if _f isa AbstractVector{<: MultiFace} + if facetype isa MultiFace + # drop faces that facetype doesn't include + names = propertynames(facetype) + _f = map(f -> MultiFace{names}(getproperty.((f,), names)), _f) + else + # drop faces for vertex attributes that aren't given + names = (:position, keys(vertex_attributes)...) + _f2 = map(f -> MultiFace{names}(getproperty.((f,), names)), _f) + + # and remap to a simple face type so that decompose can handle the rest + _f, mappings = merge_vertex_indices(_f2) + positions = positions[mappings[1]] + vertex_attributes = NamedTuple( + (Pair(names[i], vertex_attributes[i-1][mappings[i]]) for i in 2:length(mappings)) + ) + end + end f = decompose(facetype, _f) end return Mesh(positions, f; vertex_attributes...) end + const SimpleMesh{N, T, FT} = Mesh{N, T, FT, (:position,), Tuple{Vector{Point{N, T}}}, Vector{FT}} const SimpleTriangleMesh{N} = SimpleMesh{N, Float32, GLTriangleFace} @@ -72,7 +92,7 @@ end function uv_normal_mesh(primitive::AbstractGeometry{N}) where {N} return mesh( - primitive, uv = decompose_uv(primitive), normals = decompose_normals(primitive), + primitive, uv = decompose_uv(primitive), normal = decompose_normals(primitive), pointtype=Point{N,Float32}, facetype=GLTriangleFace) end @@ -80,12 +100,12 @@ function normal_mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:AbstractFace}) _points = decompose(Point3f, points) _faces = decompose(GLTriangleFace, faces) - return Mesh(_faces, position = _points, normals = normals(_points, _faces)) + return Mesh(_faces, position = _points, normal = normals(_points, _faces)) end function normal_mesh(primitive::AbstractGeometry{N}) where {N} return mesh( - primitive, normals = decompose_normals(primitive), + primitive, normal = decompose_normals(primitive), pointtype=Point{N,Float32}, facetype=GLTriangleFace) end @@ -224,7 +244,7 @@ function merge_vertex_indices( 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.faces[n][i], N_Attrib) + 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 diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index 89c45cac..d898c8e3 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -567,22 +567,33 @@ 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) + # 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) + return Vec3f[(-1,0,0), (1,0,0), (0,-1,0), (0,1,0), (0,0,-1), (0,0,1)] 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 NormalUVFace{QuadFace}.([ + ((1, 2, 3, 4), 1, (1, 2, 3, 4)), # -x + ((5, 6, 7, 8), 2, (5, 6, 7, 8)), # +x + ((1, 2, 5, 6), 3, (1, 2, 5, 6)), # -y + ((3, 4, 7, 8), 4, (3, 4, 7, 8)), # +y + ((1, 3, 5, 7), 5, (1, 3, 5, 7)), # -z + ((2, 4, 6, 8), 6, (2, 4, 6, 8)), # +z + ]) end From c8c9a8f4fc9158e232bbc88e3548e51077d4ea41 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 31 Aug 2024 18:05:42 +0200 Subject: [PATCH 007/120] generalize MultiFace getindex to Integer --- 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 7943a4c3..5402a2a3 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -67,7 +67,7 @@ MultiFace{Names}(args...) where {Names} = MultiFace(NamedTuple{Names}(args)) MultiFace{Names}(args::Tuple{Vararg{<: AbstractFace}}) where {Names} = MultiFace(NamedTuple{Names}(args)) MultiFace{Names, FT}(args) where {Names, FT <: AbstractFace} = MultiFace(NamedTuple{Names}(FT.(args))) -Base.getindex(f::MultiFace, i::Int64) = Base.getindex(getfield(f, :faces), i) # TODO: StaticVector should index with Integer +Base.getindex(f::MultiFace, i::Integer) = Base.getindex(getfield(f, :faces), i) @inline Base.hasproperty(f::MultiFace, field::Symbol) = hasproperty(getfield(f, :faces), field) @inline Base.getproperty(f::MultiFace, field::Symbol) = getproperty(getfield(f, :faces), field) @inline Base.propertynames(f::MultiFace) = propertynames(getfield(f, :faces)) From a1e3137c4f14d2114f1640e1316fc44b2dae5302 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 31 Aug 2024 20:44:30 +0200 Subject: [PATCH 008/120] remove add_meta, pop_meta --- src/GeometryBasics.jl | 1 - src/meshes.jl | 32 +------------------------------- test/meshes.jl | 2 +- test/runtests.jl | 42 +----------------------------------------- 4 files changed, 3 insertions(+), 74 deletions(-) diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index dc542474..7eddfb52 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -43,7 +43,6 @@ export decompose, coordinates, faces, normals, decompose_uv, decompose_normals, texturecoordinates export Tesselation, Normal, UV, UVW export AbstractMesh, Mesh, MetaMesh -export add_meta, pop_meta # all the different predefined mesh types diff --git a/src/meshes.jl b/src/meshes.jl index 58b50182..89df421a 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -61,7 +61,7 @@ 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} +function triangle_mesh(primitive::Union{AbstractGeometry{N}, AbstractVector{<: Point{N}}})::SimpleTriangleMesh{N} where {N} return mesh(primitive; pointtype=Point{N, Float32}) end @@ -286,36 +286,6 @@ function map_coordinates!(f, mesh::AbstractMesh) return mesh end -# TODO: -add_meta(m, kw...) = error("TODO") -pop_meta(m, kw...) = error("TODO") -# 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 - 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))) diff --git a/test/meshes.jl b/test/meshes.jl index 408a59f8..4dc60b9e 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -45,7 +45,7 @@ end m2 = GeometryBasics.merge_vertex_indices(m) @test faces(m) isa AbstractVector{<: GeometryBasics.MultiFace} - @test names(eltype(faces(m))) == keys(GeometryBasics.vertex_attributes(m)) + @test propertynames(eltype(faces(m))) == keys(GeometryBasics.vertex_attributes(m)) @test isempty(m.views) @test faces(m2) isa AbstractVector{<: QuadFace} diff --git a/test/runtests.jl b/test/runtests.jl index 7be77087..4cc17007 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 @@ -203,46 +203,6 @@ end @test 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 -end - @testset "mesh conversion" begin s = Sphere(Point3(0.0), 1.0) m = GeometryBasics.mesh(s) From d89af0e588c545a0714e243355a282650269f6a4 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 31 Aug 2024 23:10:57 +0200 Subject: [PATCH 009/120] update merge for MultiFace and views --- src/basic_types.jl | 33 +++++++- src/meshes.jl | 184 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 180 insertions(+), 37 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 5402a2a3..8d17d5b1 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -67,11 +67,25 @@ MultiFace{Names}(args...) where {Names} = MultiFace(NamedTuple{Names}(args)) MultiFace{Names}(args::Tuple{Vararg{<: AbstractFace}}) where {Names} = MultiFace(NamedTuple{Names}(args)) MultiFace{Names, FT}(args) where {Names, FT <: AbstractFace} = MultiFace(NamedTuple{Names}(FT.(args))) +function MultiFace{Names}(f::MultiFace) where {Names} + return MultiFace{Names}(getproperty.((f,), Names)) +end + Base.getindex(f::MultiFace, i::Integer) = Base.getindex(getfield(f, :faces), i) @inline Base.hasproperty(f::MultiFace, field::Symbol) = hasproperty(getfield(f, :faces), field) @inline Base.getproperty(f::MultiFace, field::Symbol) = getproperty(getfield(f, :faces), field) @inline Base.propertynames(f::MultiFace) = propertynames(getfield(f, :faces)) @inline Base.propertynames(::Type{<: MultiFace{N, T, FT, Names}}) where {N, T, FT, Names} = Names +Base.eltype(::MultiFace{N, T, FT}) where {N, T, FT} = FT +Base.eltype(::Type{<: MultiFace{N, T, FT}}) where {N, T, FT} = FT + +function simplify_faces(::Type{MF1}, fs::AbstractVector{MF2}) where {MF1 <: MultiFace, MF2 <: MultiFace} + return simplify_faces(propertynames(MF1), fs) +end + +function simplify_faces(names::NTuple{N, Symbol}, fs::AbstractVector{MF2}) where {N, MF2 <: MultiFace} + return map(f -> MultiFace{names}(f), fs) +end # TODO: Some shorthands const NormalFace = MultiFace{(:position, :normal)} @@ -327,12 +341,12 @@ struct Mesh{ vertex_attributes::NamedTuple{Names, VertexAttribTypes} connectivity::FaceVecType - views::Vector{UnitRange} + views::Vector{UnitRange{Int}} function Mesh( va::NamedTuple{Names, VAT}, f::FVT, - views::Vector{UnitRange} = UnitRange[] + views::Vector{UnitRange{Int}} = UnitRange{Int}[] ) where { D, T, FT, Names, VAT <: Tuple{AbstractVector{Point{D, T}}, Vararg{AbstractVector}}, @@ -346,7 +360,18 @@ struct Mesh{ if FT <: MultiFace f_names = propertynames(FT) - if Names != f_names + # if Names != f_names + # error( + # "Cannot construct a mesh with vertex attribute names $Names and MultiFace " * + # "attribute names $f_names. These must include the same names in the same order." + # ) + # end + if Names == f_names + # all good + elseif issubset(Names, f_names) + # remove the extras/fix order + f = simplify_faces(Names, f) + else error( "Cannot construct a mesh with vertex attribute names $Names and MultiFace " * "attribute names $f_names. These must include the same names in the same order." @@ -362,7 +387,7 @@ struct Mesh{ error("Face vectors that may include `MultiFace`s with different names are not allowed. (Type $FT too abstract.)") end - return new{D, T, FT, Names, VAT, FVT}(va, f, views) + return new{D, T, eltype(typeof(f)), Names, VAT, typeof(f)}(va, f, views) end end diff --git a/src/meshes.jl b/src/meshes.jl index 89df421a..339e6a3c 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -27,12 +27,11 @@ function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleF if _f isa AbstractVector{<: MultiFace} if facetype isa MultiFace # drop faces that facetype doesn't include - names = propertynames(facetype) - _f = map(f -> MultiFace{names}(getproperty.((f,), names)), _f) + _f = simplify_faces(facetype, _f) else # drop faces for vertex attributes that aren't given names = (:position, keys(vertex_attributes)...) - _f2 = map(f -> MultiFace{names}(getproperty.((f,), names)), _f) + _f2 = simplify_faces(names, _f) # and remap to a simple face type so that decompose can handle the rest _f, mappings = merge_vertex_indices(_f2) @@ -47,7 +46,6 @@ function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleF return Mesh(positions, f; vertex_attributes...) end - const SimpleMesh{N, T, FT} = Mesh{N, T, FT, (:position,), Tuple{Vector{Point{N, T}}}, Vector{FT}} const SimpleTriangleMesh{N} = SimpleMesh{N, Float32, GLTriangleFace} @@ -131,41 +129,133 @@ function volume(mesh::Mesh) return sum(volume, mesh) end +# TODO: Is this ok as "public" function? +# MultiFace(f1, f2, f3) + (o1, o2, o3) = MultiFace(f1 + o1, f2 + o2, f3 + o3) +function Base.:+(f::MultiFace{N, T, FT, Names, M}, o::NTuple{M, T}) where {N, T, FT, Names, M} + return MultiFace{Names}(ntuple(m -> f[m] + o[m], M)) +end + function Base.merge(meshes::AbstractVector{<:Mesh}) return if isempty(meshes) return Mesh(Point3f[], GLTriangleFace[]) elseif length(meshes) == 1 return meshes[1] 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 - end - idx += N - offset += length(coordinates(mesh)) + + 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 = propertynames(m1.vertex_attributes) + idx = findfirst(m -> propertynames(m.vertex_attributes) != names, meshes) + if idx !== nothing + error( + "Cannot merge meshes with different vertex attributes. " * + "First missmatch between meshes[1] with $names and " * + "meshes[$idx] with $(propertynames(meshes[idx]))." + ) end - return Mesh(ps, fs) - 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) + # We can't merge MultiFace with standard faces because MutliFace allows + # desynchronizes vertex indices that normal faces assume synchronized. + is_multi = facetype(m1) <: MultiFace + + if all(m -> is_multi == (facetype(m) <: MultiFace), meshes) + + # All the same kind of face, can just merge + + new_attribs = NamedTuple{names}(map(names) do name + return mapreduce(m -> getproperty(m, name), vcat, meshes) + end) + fs = reduce(vcat, faces.(meshes)) + + # TODO: is the type difference in offset bad? + idx = length(faces(m1)) + offset = is_multi ? length.(values(vertex_attributes(m1))) : length(coordinates(m1)) + views = isempty(m1.views) ? [1:idx] : copy(m1.views) + + for mesh in Iterators.drop(meshes, 1) + # update face indices + N = length(faces(mesh)) + for i = idx .+ (1:N) + fs[i] = fs[i] + offset + end + + # add views + if isempty(mesh.views) + push!(views, idx+1 : idx+N) + else + append!(views, (view + idx for view in mesh.views)) + end + + idx += N + if is_multi + offset = offset .+ length.(values(vertex_attributes(mesh))) + else + offset += length(coordinates(mesh)) + end + end + + return Mesh(new_attribs, fs, views) + + else + + # TODO: We can probably simplify this to `merge(merge_vertex_indices.(meshes))` + # but need to check performance + + + # Varying ace types, need to convert MultiFace + new_attribs = NamedTuple{names}(similar.(values(vertex_attributes(m1)), 0)) + FT = facetype(m1) <: MultiFace ? eltype(facetype(m1)) : facetype(m1) + remapped_faces = [] + new_views = UnitRange{Int}[] + vertex_index_counter = eltype(FT)(1) + + for mesh in meshes + # convert MultiFace mesh to normal faces, synchronizing vertex indices + attribs, fs, views = merge_vertex_indices( + vertex_attributes(mesh), faces(mesh), mesh.views, vertex_index_counter) + + # increment first vertex index used by faces of the next iteration + vertex_index_counter += length(attribs[1]) + + # update merged data + for name in names + append!(new_attribs[name], attribs[name]) + end + + push!(remapped_faces, fs) + + if isempty(views) + push!(new_views, 1:length(fs)) + else + append!(new_views, views) + end + end + + # We did MultiFace -> normal face, now equalize normal face types + new_faces = reduce(vcat, remapped_faces) + + return Mesh(new_attribs, new_faces, new_views) end + end - return MetaMesh(big_mesh, big_meta) end +# TODO: Probably not our problem +# 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 + # TODO: naming # synchronize_vertex_attributes # merge_vertex_(attribute)_indices @@ -178,10 +268,28 @@ function merge_vertex_indices(mesh) return Mesh(attribs, fs, views) end +function merge_vertex_indices( + attribs::NamedTuple{Names}, + faces::AbstractVector{<: FT}, + views::Vector{UnitRange{Int}}, + vertex_index_counter = nothing + ) where {Names, FT <: AbstractFace} + + if FT <: MultiFace + error( + "Failed to call correct method. This likely happened because vertex " * + "attributes names $Names do not match face name $(propertynames(first(faces)))." + ) + end + + return attribs, faces, views +end + function merge_vertex_indices( attribs::NamedTuple{Names}, faces::AbstractVector{<: MultiFace{N, T, FT, Names}}, - views::Vector{UnitRange} + views::Vector{UnitRange{Int}}, + vertex_index_counter = T(1) # TODO: test 0 vs 1 base ) where {Names, N, T, FT} # Note: typing checks for matching Names @@ -194,17 +302,16 @@ function merge_vertex_indices( new_attribs = NamedTuple((Pair(k, similar(v, 0)) for (k, v) in pairs(attribs))) new_faces = similar(faces, FT, 0) - new_views = UnitRange[] + new_views = UnitRange{Int}[] + # TODO: this depends on T in Face (1 based -> 1, 0 based -> 0) for idxs in views - # TODO: this depends on T in Face (1 based -> 1, 0 based -> 0) - vertex_index_counter = T(length(new_attribs[1]) + 1) - # Generate new face from current view, with the first vertex_index # corresponding to the first vertex attribute added in this iteration face_view = view(faces, idxs) new_faces_in_view, vertex_map = merge_vertex_indices(face_view, vertex_index_counter) - + vertex_index_counter += length(vertex_map) + # update vertex attributes for (name, indices) in pairs(vertex_map) append!(new_attribs[name], view(attribs[name], indices)) @@ -219,6 +326,17 @@ function merge_vertex_indices( return new_attribs, new_faces, new_views end +function merge_vertex_indices( + faces::AbstractVector{FT}, + vertex_index_counter = T(1) + ) where {N, T, FT <: AbstractFace{N, T}} + + @assert !(FT <: MultiFace) "Dispatch failed?" + + N_vert = mapreduce(f -> max(f), max, faces) + return faces, (1:N_vert) .+ vertex_index_counter +end + function merge_vertex_indices( faces::AbstractVector{<: MultiFace{N, T, FT, Names, N_Attrib}}, vertex_index_counter = T(1) From 5577748cb22a66a75d3aa6ff45843df3101c61a5 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 2 Sep 2024 14:55:31 +0200 Subject: [PATCH 010/120] add AbstractVertexFace and AbstractMultiFace --- src/basic_types.jl | 122 +++++++++++++++++++++++++++------------------ src/meshes.jl | 28 +++-------- 2 files changed, 80 insertions(+), 70 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 8d17d5b1..c283f7e3 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -13,9 +13,27 @@ 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 +""" + AbstractFace{N, T} <: StaticVector{N, T} + +Parent type for all faces. You should inherit from one of the child types instead. +""" 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 +""" + AbstractMultiFace{N, T, M} <: AbstractFace{N, T} + +Parent type for faces addressing N vertices with M different vertex attribute +indices. +""" +abstract type AbstractMultiFace{N, T, M} <: AbstractFace{N, T} end +""" + AbstractVertexFace{N, T} <: AbstractFace{N, T} + +Parent type for faces addressing N vertices with common vertex indices. +""" +abstract type AbstractVertexFace{N, T} <: AbstractFace{N, T} end +abstract type AbstractSimplexFace{N,T} <: AbstractVertexFace{N,T} end +abstract type AbstractNgonFace{N,T} <: AbstractVertexFace{N,T} end abstract type AbstractSimplex{Dim,T} <: Polytope{Dim,T} end @@ -50,26 +68,26 @@ end Face(::Type{<:NgonFace{N}}, ::Type{T}) where {N,T} = NgonFace{N,T} Face(F::Type{NgonFace{N,FT}}, ::Type{T}) where {FT,N,T} = F -struct MultiFace{N, T, FaceType <: AbstractFace{N, T}, Names, M} <: AbstractFace{N, T} +struct MultiFace{N, T, FaceType <: AbstractVertexFace{N, T}, Names, M} <: AbstractMultiFace{N, T, M} faces::NamedTuple{Names, NTuple{M, FaceType}} - - function MultiFace(nt::NamedTuple{Names, NTuple{M, FT}}) where {N, T, FT <: AbstractFace{N, T}, Names, M} - if FT <: MultiFace - error("A MultiFace cannot contain MultiFaces.") - end - - return new{N, T, FT, Names, M}(nt) - end end +# TODO: Split these up? +""" + MultiFace(; kwargs...) + MultiFace{Names}(faces...) + MultiFace{Names}(faces::Tuple) + MultiFace{Names}(multiface::MultiFace) + MultiFace{Names, FaceType}(faces::Tuple) + +Constructs a `MultiFace` from a tuple of names `Names::NTuple{M, Symbol}` and +`faces::NTuple{M, FaceType}` similar to how a NamedTuple would. +""" MultiFace(; kwargs...) = MultiFace(NamedTuple(kwargs)) MultiFace{Names}(args...) where {Names} = MultiFace(NamedTuple{Names}(args)) MultiFace{Names}(args::Tuple{Vararg{<: AbstractFace}}) where {Names} = MultiFace(NamedTuple{Names}(args)) MultiFace{Names, FT}(args) where {Names, FT <: AbstractFace} = MultiFace(NamedTuple{Names}(FT.(args))) - -function MultiFace{Names}(f::MultiFace) where {Names} - return MultiFace{Names}(getproperty.((f,), Names)) -end +MultiFace{Names}(f::MultiFace) where {Names} = MultiFace{Names}(getproperty.((f,), Names)) Base.getindex(f::MultiFace, i::Integer) = Base.getindex(getfield(f, :faces), i) @inline Base.hasproperty(f::MultiFace, field::Symbol) = hasproperty(getfield(f, :faces), field) @@ -348,47 +366,56 @@ struct Mesh{ f::FVT, views::Vector{UnitRange{Int}} = UnitRange{Int}[] ) where { - D, T, FT, Names, + D, T, FT <: AbstractMultiFace, Names, VAT <: Tuple{AbstractVector{Point{D, T}}, Vararg{AbstractVector}}, FVT <: AbstractVector{FT} } - # verify type + # verify type / naming rules & consistency if first(Names) !== :position error("The first vertex attribute should be a 'position' but is a '$(first(Names))'.") end - if FT <: MultiFace - f_names = propertynames(FT) - # if Names != f_names - # error( - # "Cannot construct a mesh with vertex attribute names $Names and MultiFace " * - # "attribute names $f_names. These must include the same names in the same order." - # ) - # end - if Names == f_names - # all good - elseif issubset(Names, f_names) - # remove the extras/fix order - f = simplify_faces(Names, f) - else - error( - "Cannot construct a mesh with vertex attribute names $Names and MultiFace " * - "attribute names $f_names. These must include the same names in the same order." - ) - end - elseif MultiFace <: FT - # TODO: This is supposed to catch mixed types like - # [MultiFace(position = f1, normal = f2), MultiFace(position = f3)] - # but really just catches AbstractFace{N, T}[]. Technically we can - # probably handle mixtures of MultiFace and other Face types, but do - # we want to bother? Also do we want to allow mixtures of e.g. - # TriangleFace and QuadFace? - error("Face vectors that may include `MultiFace`s with different names are not allowed. (Type $FT too abstract.)") + f_names = propertynames(FT) + if Names == f_names + # all good + elseif issubset(Names, f_names) + # Remove the extra names in faces/fix order + # Note: This might be redundant with `mesh()`. It is supposed to allow + # using a general `MultiFace` in `faces(primitive)` which then + # gets reduced to the vertex attributes used in a general mesh. + f = simplify_faces(Names, f) + else + error( + "Cannot construct a mesh with vertex attribute names $Names and MultiFace " * + "attribute names $f_names. These must include the same names in the same order." + ) end return new{D, T, eltype(typeof(f)), Names, VAT, typeof(f)}(va, f, views) end + + function Mesh( + va::NamedTuple{Names, VAT}, + f::FVT, + views::Vector{UnitRange{Int}} = UnitRange{Int}[] + ) where { + D, T, FT <: AbstractVertexFace, Names, + VAT <: Tuple{AbstractVector{Point{D, T}}, Vararg{AbstractVector}}, + FVT <: AbstractVector{FT} + } + + # verify type + if first(Names) !== :position + error("The first vertex attribute should be a 'position' but is a '$(first(Names))'.") + end + + # Note: With VertexFaces all vertex attributes should have the same + # length as they use a common vertex index. We could check this + # here but maybe it's better not to to prevent over-eager checking? + + return new{D, T, FT, Names, VAT, FVT}(va, f, views) + end end @inline function Base.hasproperty(mesh::Mesh, field::Symbol) @@ -415,13 +442,10 @@ Base.getindex(mesh::Mesh, i::Integer) = mesh[mesh.connectivity[i]] Base.length(mesh::Mesh) = length(mesh.connectivity) # TODO: temp -function Base.getindex(mesh::Mesh{D, T, <: AbstractFace, (:position,)}, f::AbstractFace) where {D, T} +function Base.getindex(mesh::Mesh{D, T, <: AbstractVertexFace}, f::AbstractVertexFace) where {D, T} return getfield(mesh, :vertex_attributes).position[f] end -function Base.getindex(::Mesh, f::MultiFace) - error("TODO") -end -function Base.getindex(::Mesh, f::AbstractFace) +function Base.getindex(::Mesh, f::AbstractMultiFace) error("TODO") end diff --git a/src/meshes.jl b/src/meshes.jl index 339e6a3c..ef55374f 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -24,7 +24,7 @@ function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleF error("No triangulation for $(typeof(primitive))") end else - if _f isa AbstractVector{<: MultiFace} + if _f isa AbstractVector{<: AbstractMultiFace} if facetype isa MultiFace # drop faces that facetype doesn't include _f = simplify_faces(facetype, _f) @@ -95,7 +95,7 @@ function uv_normal_mesh(primitive::AbstractGeometry{N}) where {N} end function normal_mesh(points::AbstractVector{<:Point}, - faces::AbstractVector{<:AbstractFace}) + faces::AbstractVector{<:AbstractVertexFace}) _points = decompose(Point3f, points) _faces = decompose(GLTriangleFace, faces) return Mesh(_faces, position = _points, normal = normals(_points, _faces)) @@ -269,19 +269,11 @@ function merge_vertex_indices(mesh) end function merge_vertex_indices( - attribs::NamedTuple{Names}, - faces::AbstractVector{<: FT}, + attribs::NamedTuple, + faces::AbstractVector{<: AbstractVertexFace}, views::Vector{UnitRange{Int}}, vertex_index_counter = nothing - ) where {Names, FT <: AbstractFace} - - if FT <: MultiFace - error( - "Failed to call correct method. This likely happened because vertex " * - "attributes names $Names do not match face name $(propertynames(first(faces)))." - ) - end - + ) return attribs, faces, views end @@ -326,15 +318,9 @@ function merge_vertex_indices( return new_attribs, new_faces, new_views end -function merge_vertex_indices( - faces::AbstractVector{FT}, - vertex_index_counter = T(1) - ) where {N, T, FT <: AbstractFace{N, T}} - - @assert !(FT <: MultiFace) "Dispatch failed?" - +function merge_vertex_indices(faces::AbstractVector{<: AbstractVertexFace}, i = T(1)) N_vert = mapreduce(f -> max(f), max, faces) - return faces, (1:N_vert) .+ vertex_index_counter + return faces, i : N_vert - 1 + i end function merge_vertex_indices( From 8ccba36ea034cf0966296fb5ce55fd41ec341578 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 2 Sep 2024 16:45:09 +0200 Subject: [PATCH 011/120] split up mesh() for better usability --- src/meshes.jl | 75 ++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 56 insertions(+), 19 deletions(-) diff --git a/src/meshes.jl b/src/meshes.jl index ef55374f..4529c3a3 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -13,36 +13,73 @@ It also only losely correlates to the number of vertices, depending on the algor """ function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleFace, vertex_attributes...) positions = decompose(pointtype, primitive) - _f = faces(primitive) # If faces returns nothing for primitive, we try to triangulate! - if isnothing(_f) + _fs = faces(primitive) + if isnothing(ds) if eltype(positions) <: Point2 # triangulation.jl - f = decompose(facetype, positions) + fs = decompose(facetype, positions) else error("No triangulation for $(typeof(primitive))") end else - if _f isa AbstractVector{<: AbstractMultiFace} - if facetype isa MultiFace - # drop faces that facetype doesn't include - _f = simplify_faces(facetype, _f) - else - # drop faces for vertex attributes that aren't given - names = (:position, keys(vertex_attributes)...) - _f2 = simplify_faces(names, _f) + fs = _fs + end + + return mesh(positions, fs; facetype = facetype, vertex_attributes...) +end + +""" + 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. + +By default the generated mesh uses `GLTriangleFace`. If the input faces are of +type `MultiFace` they will get converted appropriately, which may cause +reordering and duplication of positions and other vertex attributes. +""" +function mesh( + positions::AbstractVector{<:Point}, + faces::AbstractVector{AbstractMultiFace}, + facetype=GLTriangleFace, vertex_attributes... + ) + + if facetype <: AbstractMultiFace + # drop vertex attribute references in faces that facetype does not include + f = simplify_faces(facetype, faces) # TODO: call this decompose? + + elseif facetype <: AbstractVertexFace + # drop vertex attributes references in faces that are not part of the + # given vertex attributes. (This allows multi_face to be more general + # than the mesh we construct) + names = (:position, keys(vertex_attributes)...) + _f2 = simplify_faces(names, faces) - # and remap to a simple face type so that decompose can handle the rest - _f, mappings = merge_vertex_indices(_f2) - positions = positions[mappings[1]] - vertex_attributes = NamedTuple( - (Pair(names[i], vertex_attributes[i-1][mappings[i]]) for i in 2:length(mappings)) - ) - end - end + # Convert MultiFace to its internally used VertexFace type and apply + # necessary vertex attribute remapping + _f, mappings = merge_vertex_indices(_f2) + positions = positions[mappings[1]] + vertex_attributes = NamedTuple( + (Pair(names[i], vertex_attributes[i-1][mappings[i]]) for i in 2:length(mappings)) + ) + + # Convert to actually requested facetype f = decompose(facetype, _f) + else + error("Cannot convert MultiFace to $facetype.") end + + return Mesh(positions, f; vertex_attributes...) +end + +function mesh( + positions::AbstractVector{<:Point}, + faces::AbstractVector{AbstractVertexFace}, + facetype=GLTriangleFace, vertex_attributes...) + + f = decompose(facetype, faces) return Mesh(positions, f; vertex_attributes...) end From 3461f82699461e2399bad46e496d5633a472ed54 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 2 Sep 2024 16:53:04 +0200 Subject: [PATCH 012/120] minor fixes --- src/meshes.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/meshes.jl b/src/meshes.jl index 4529c3a3..d675a16a 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -16,7 +16,7 @@ function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleF # If faces returns nothing for primitive, we try to triangulate! _fs = faces(primitive) - if isnothing(ds) + if isnothing(_fs) if eltype(positions) <: Point2 # triangulation.jl fs = decompose(facetype, positions) @@ -42,7 +42,7 @@ reordering and duplication of positions and other vertex attributes. """ function mesh( positions::AbstractVector{<:Point}, - faces::AbstractVector{AbstractMultiFace}, + faces::AbstractVector{<: AbstractMultiFace}; facetype=GLTriangleFace, vertex_attributes... ) @@ -76,7 +76,7 @@ end function mesh( positions::AbstractVector{<:Point}, - faces::AbstractVector{AbstractVertexFace}, + faces::AbstractVector{<: AbstractVertexFace}; facetype=GLTriangleFace, vertex_attributes...) f = decompose(facetype, faces) From d6fd0d09218e6f192ad84a2f8f3b146fa302637d Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 2 Sep 2024 18:19:27 +0200 Subject: [PATCH 013/120] add views to Mesh constructors --- src/basic_types.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index c283f7e3..e7f6e7f0 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -458,13 +458,14 @@ function Base.iterate(mesh::Mesh, i=1) return i - 1 < length(mesh) ? (mesh[i], i + 1) : nothing end -function Mesh(faces::AbstractVector{<:AbstractFace}; attributes...) - return Mesh(NamedTuple(attributes), faces) +function Mesh(faces::AbstractVector{<:AbstractFace}; views = UnitRange{Int}[], attributes...) + return Mesh(NamedTuple(attributes), faces, views) end function Mesh(points::AbstractVector{Point{Dim, T}}, - faces::AbstractVector{<:AbstractFace}; kwargs...) where {Dim, T} - return Mesh((position = points, kwargs...), faces) + faces::AbstractVector{<:AbstractFace}; + views = UnitRange{Int}[], kwargs...) where {Dim, T} + return Mesh((position = points, kwargs...), faces, views) end function Mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Integer}, From 8975fb154baac91dc3be0dffd246a81625bbedf0 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 2 Sep 2024 18:20:27 +0200 Subject: [PATCH 014/120] add `mesh()` method for converting facetype of mesh --- src/meshes.jl | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/meshes.jl b/src/meshes.jl index d675a16a..b1eb2101 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -83,6 +83,28 @@ function mesh( return Mesh(positions, f; vertex_attributes...) end +""" + mesh(mesh::Mesh[, facetype = GLTriangleFace]) + +Converts a mesh to the given facetype. +""" +function mesh(mesh::Mesh{D, T, FT}, facetype = GLTriangleFace) where {D, T, FT} + if FT == facetype + return mesh + + elseif (FT <: AbstractVertexFace) && (facetype <: AbstractVertexFace) + # TODO: handle views - changing faces changes views... + f = decompose(facetype, faces(mesh)) + return Mesh(vertex_attributes(mesh), f) + + elseif (FT <: AbstractMultiFace) && (facetype <: AbstractVertexFace) + return merge_vertex_indices(mesh) + + else + error("Conversion from $FT -> $facetype not implemented.") + end +end + const SimpleMesh{N, T, FT} = Mesh{N, T, FT, (:position,), Tuple{Vector{Point{N, T}}}, Vector{FT}} const SimpleTriangleMesh{N} = SimpleMesh{N, Float32, GLTriangleFace} @@ -194,7 +216,7 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) end # We can't merge MultiFace with standard faces because MutliFace allows - # desynchronizes vertex indices that normal faces assume synchronized. + # desynchronizes vertex indices that vertex faces assume synchronized. is_multi = facetype(m1) <: MultiFace if all(m -> is_multi == (facetype(m) <: MultiFace), meshes) @@ -249,7 +271,7 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) vertex_index_counter = eltype(FT)(1) for mesh in meshes - # convert MultiFace mesh to normal faces, synchronizing vertex indices + # convert MultiFace mesh to vertex faces, synchronizing vertex indices attribs, fs, views = merge_vertex_indices( vertex_attributes(mesh), faces(mesh), mesh.views, vertex_index_counter) @@ -270,7 +292,7 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) end end - # We did MultiFace -> normal face, now equalize normal face types + # We did MultiFace -> vertex face, now equalize vertex face types new_faces = reduce(vcat, remapped_faces) return Mesh(new_attribs, new_faces, new_views) From 4d8d7317532e5236466507281e0397c46601938c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 2 Sep 2024 18:20:36 +0200 Subject: [PATCH 015/120] switch back to "normals" --- src/basic_types.jl | 4 ++++ src/meshes.jl | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index e7f6e7f0..34f1d447 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -418,6 +418,10 @@ struct Mesh{ end end +# TODO: what shorthands could be useful? +# const GLMesh{D, T} = Mesh{D, T, GLTriangleFace} +# const SimpleGLMesh{D, T} = Mesh{D, T, GLTriangleFace, (:position,)} + @inline function Base.hasproperty(mesh::Mesh, field::Symbol) return hasproperty(getfield(mesh, :vertex_attributes), field) || hasfield(Mesh, field) end diff --git a/src/meshes.jl b/src/meshes.jl index b1eb2101..24160b5f 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -149,7 +149,7 @@ end function uv_normal_mesh(primitive::AbstractGeometry{N}) where {N} return mesh( - primitive, uv = decompose_uv(primitive), normal = decompose_normals(primitive), + primitive, uv = decompose_uv(primitive), normals = decompose_normals(primitive), pointtype=Point{N,Float32}, facetype=GLTriangleFace) end @@ -157,12 +157,12 @@ function normal_mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:AbstractVertexFace}) _points = decompose(Point3f, points) _faces = decompose(GLTriangleFace, faces) - return Mesh(_faces, position = _points, normal = normals(_points, _faces)) + return Mesh(_faces, position = _points, normals = normals(_points, _faces)) end function normal_mesh(primitive::AbstractGeometry{N}) where {N} return mesh( - primitive, normal = decompose_normals(primitive), + primitive, normals = decompose_normals(primitive), pointtype=Point{N,Float32}, facetype=GLTriangleFace) end From 59cd70bee87b9d9436e864ca51a22b0accdb77b5 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 2 Sep 2024 18:49:29 +0200 Subject: [PATCH 016/120] fix missing normals rename --- 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 34f1d447..3580c9c7 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -106,8 +106,8 @@ function simplify_faces(names::NTuple{N, Symbol}, fs::AbstractVector{MF2}) where end # TODO: Some shorthands -const NormalFace = MultiFace{(:position, :normal)} -const NormalUVFace = MultiFace{(:position, :normal, :uv)} +const NormalFace = MultiFace{(:position, :normals)} +const NormalUVFace = MultiFace{(:position, :normals, :uv)} # TODO: enable something like NormalUVFace{QuadFace}[...] From 8aa215cc40d5425346d98b77e8ea784cb7c0a00c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 2 Sep 2024 18:49:52 +0200 Subject: [PATCH 017/120] add back point/normal/uv-type kwargs --- src/meshes.jl | 71 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/src/meshes.jl b/src/meshes.jl index 24160b5f..19afe40f 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -118,52 +118,69 @@ 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}}})::SimpleTriangleMesh{N} where {N} - return mesh(primitive; pointtype=Point{N, Float32}) +function triangle_mesh( + primitive::Union{AbstractGeometry{N}, AbstractVector{<: Point{N}}}; + pointtype = Point{N, Float32}, facetype = GLTriangleFace + )::SimpleTriangleMesh{N} where {N} # TODO: is the output type doing anything here? + + return mesh(primitive; pointtype = pointtype, facetype = facetype) end -pointtype(x::Mesh) = eltype(decompose(Point, x)) -facetype(x::Mesh) = eltype(faces(x)) +pointtype(::Mesh{D, T}) where {D, T} = Point{D, T} +facetype(::Mesh{D, T, FT}) where {D, T, FT} = FT -function triangle_mesh(primitive::Mesh{N}) where {N} +function triangle_mesh(primitive::Mesh{N}; pointtype = Point{N, Float32}, facetype = GLTriangleFace) where {N} # already target type: - if pointtype(primitive) === Point{N,Float32} && GLTriangleFace === facetype(primitive) + if GeometryBasics.pointtype(primitive) === pointtype && + GeometryBasics.facetype(primitive) === facetype return primitive else - return mesh(primitive; pointtype=Point{N,Float32}, facetype=GLTriangleFace) + return mesh(primitive; pointtype = pointtype, facetype = facetype) end end -function triangle_mesh(primitive::Union{AbstractGeometry{N}, AbstractVector{<: Point{N}}}; nvertices = nothing)::SimpleTriangleMesh{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} - return mesh(primitive, uv = decompose_uv(primitive), pointtype=Point{N,Float32}, facetype=GLTriangleFace) +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} +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(primitive), normals = decompose_normals(primitive), - pointtype=Point{N,Float32}, facetype=GLTriangleFace) + primitive, uv = decompose_uv(uvtype, primitive), + normals = decompose_normals(normaltype, primitive), + pointtype = pointtype, facetype = facetype + ) end -function normal_mesh(points::AbstractVector{<:Point}, - faces::AbstractVector{<:AbstractVertexFace}) - _points = decompose(Point3f, points) - _faces = decompose(GLTriangleFace, faces) - return Mesh(_faces, position = _points, normals = normals(_points, _faces)) +function normal_mesh( + points::AbstractVector{<:Point}, faces::AbstractVector{<:AbstractVertexFace}; + pointtype = Point3f, normaltype = Vec3f, facetype = GLTriangleFace + ) + _points = decompose(pointtype, points) + _faces = decompose(facetype, faces) + return Mesh(_faces, position = _points, normals = normals(_points, _faces, normaltype)) end -function normal_mesh(primitive::AbstractGeometry{N}) where {N} +function normal_mesh( + primitive::AbstractGeometry{N}; pointtype = Point{N, Float32}, + normaltype = Vec3f, facetype = GLTriangleFace + ) where {N} + return mesh( - primitive, normals = decompose_normals(primitive), - pointtype=Point{N,Float32}, facetype=GLTriangleFace) + primitive, normals = decompose_normals(normaltype, primitive), + pointtype = pointtype, facetype = facetype) end """ From fc37dc15404458208b8386692f775404809054b3 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 2 Sep 2024 21:24:28 +0200 Subject: [PATCH 018/120] consider face views in face decompose --- src/geometry_primitives.jl | 23 +++++++++++++---------- src/interfaces.jl | 26 ++++++++++++++++++++++++-- src/meshes.jl | 10 +++++++--- src/triangulation.jl | 2 +- 4 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/geometry_primitives.jl b/src/geometry_primitives.jl index 36b25d69..105d1e84 100644 --- a/src/geometry_primitives.jl +++ b/src/geometry_primitives.jl @@ -73,9 +73,15 @@ 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}, iter) where {T} = collect_with_eltype!(T[], iter) + +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 -function collect_with_eltype(::Type{T}, iter) where {T} - isempty(iter) && return T[] # 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,18 +96,15 @@ 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 diff --git a/src/interfaces.jl b/src/interfaces.jl index bed84d4b..a7261b2c 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -94,7 +94,7 @@ struct Normal{T} end Normal(::Type{T}) where {T} = Normal{T}() Normal() = Normal(Vec3f) -function decompose(::Type{F}, primitive::AbstractGeometry) where {F<:AbstractFace} +function decompose(::Type{F}, primitive::AbstractGeometry) where {F<:AbstractVertexFace} f = faces(primitive) if isnothing(f) if ndims(primitive) == 2 @@ -106,13 +106,35 @@ function decompose(::Type{F}, primitive::AbstractGeometry) where {F<:AbstractFac end return collect_with_eltype(F, f) end + +function decompose(::Type{F}, mesh::AbstractMesh) where {F<:AbstractVertexFace} + m = Mesh(mesh) + return decompose(F, faces(m), m.views) +end -function decompose(::Type{F}, f::AbstractVector) where {F<:AbstractFace} +function decompose(::Type{F}, f::AbstractVector) where {F<:AbstractVertexFace} fs = faces(f) isnothing(fs) && error("No faces defined for $(typeof(f))") return collect_with_eltype(F, fs) end +function decompose(::Type{F}, f::AbstractVector, views::Vector{UnitRange{Int}}) where {F<:AbstractVertexFace} + 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 = UnitRange{Int}[] + 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 diff --git a/src/meshes.jl b/src/meshes.jl index 19afe40f..07e8a92a 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -94,12 +94,16 @@ function mesh(mesh::Mesh{D, T, FT}, facetype = GLTriangleFace) where {D, T, FT} elseif (FT <: AbstractVertexFace) && (facetype <: AbstractVertexFace) # TODO: handle views - changing faces changes views... - f = decompose(facetype, faces(mesh)) - return Mesh(vertex_attributes(mesh), f) + f, views = decompose(facetype, faces(mesh), mesh.views) + return Mesh(vertex_attributes(mesh), f, views) elseif (FT <: AbstractMultiFace) && (facetype <: AbstractVertexFace) - return merge_vertex_indices(mesh) + # MultiFace{VertexFace} -> VertexFace + attribs, fs, views = merge_vertex_indices(vertex_attributes(mesh), faces(mesh), mesh.views) + # VertexFace -> facetype + f, views = decompose(facetype, fs, views) + return Mesh(attribs, fs, views) else error("Conversion from $FT -> $facetype not implemented.") end diff --git a/src/triangulation.jl b/src/triangulation.jl index 1847f9b5..5604ef54 100644 --- a/src/triangulation.jl +++ b/src/triangulation.jl @@ -110,7 +110,7 @@ Triangulate a Polygon without hole. Returns a Vector{`facetype`} defining indexes into `contour`. """ -function decompose(::Type{F}, points::AbstractVector{<:Point}) where {F<:AbstractFace} +function decompose(::Type{F}, points::AbstractVector{<:Point}) where {F<:AbstractVertexFace} #= allocate and initialize list of Vertices in polygon =# result = F[] From d562ab252dc1860faa6cac12ba42f5c4782de50f Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 2 Sep 2024 21:46:42 +0200 Subject: [PATCH 019/120] add mesh(mesh; attribs...) & improve dispatch safety --- src/meshes.jl | 80 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 15 deletions(-) diff --git a/src/meshes.jl b/src/meshes.jl index 07e8a92a..6a6ee40b 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -27,7 +27,7 @@ function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleF fs = _fs end - return mesh(positions, fs; facetype = facetype, vertex_attributes...) + return mesh(positions, collect(fs); facetype = facetype, vertex_attributes...) end """ @@ -84,31 +84,81 @@ function mesh( end """ - mesh(mesh::Mesh[, facetype = GLTriangleFace]) + mesh(mesh::Mesh{D, T}[; pointtype = Point{D, Float32}, facetype = GLTriangleFace, vertex_attributes...]) -Converts a mesh to the given facetype. +Converts a mesh to the given point and face types and adds the given vertex attributes. + +Note that the target `facetype` must `<: AbstractVertexFace` and that vertex +attributes are only allowed if `facetype(mesh) <: AbstractVertexFace`. """ -function mesh(mesh::Mesh{D, T, FT}, facetype = GLTriangleFace) where {D, T, FT} - if FT == facetype - return mesh +function mesh( + mesh::Mesh{D, T, FT}; pointtype = Point{D, Float32}, + facetype::Type{<: AbstractVertexFace} = GLTriangleFace, + attributes... + ) where {D, T, FT <: AbstractMultiFace} + + if !isempty(attributes) + error( + "Cannot add vertex attributes to a mesh with face type $FT as the " * + "indexing of the new attributes is not clearly defined." + ) + end - elseif (FT <: AbstractVertexFace) && (facetype <: AbstractVertexFace) - # TODO: handle views - changing faces changes views... - f, views = decompose(facetype, faces(mesh), mesh.views) - return Mesh(vertex_attributes(mesh), f, views) + if FT == facetype + if GeometryBasics.pointtype(mesh) == pointtype + return mesh + else + va = merge(vertex_attributes(mesh), (position = decompose(pointtype, mesh),)) + return Mesh(va, faces(mesh), mesh.views) + end + else + # update position type (doing this first is likely cheaper since + # MultiFace likely requires duplication of vertex attributes) + va = merge(vertex_attributes(mesh), (position = decompose(pointtype, mesh),)) - elseif (FT <: AbstractMultiFace) && (facetype <: AbstractVertexFace) # MultiFace{VertexFace} -> VertexFace - attribs, fs, views = merge_vertex_indices(vertex_attributes(mesh), faces(mesh), mesh.views) + attribs, fs, views = merge_vertex_indices(va, faces(mesh), mesh.views) + # VertexFace -> facetype - f, views = decompose(facetype, fs, views) + faces, views = decompose(facetype, fs, views) + + return Mesh(attribs, faces, views) + end +end + +function mesh( + mesh::Mesh{D, T, FT}; pointtype = Point{D, Float32}, + facetype::Type{<: AbstractVertexFace} = GLTriangleFace, + attributes... + ) where {D, T, FT <: AbstractVertexFace} - return Mesh(attribs, fs, views) + if FT == facetype + if isempty(attributes) && GeometryBasics.pointtype(mesh) == pointtype + return mesh + else + # 1. add vertex attributes, 2. convert position attribute + va = merge(vertex_attributes(mesh), NamedTuple(attributes)) + va = merge(va, (position = decompose(pointtype, va.position),)) + return Mesh(va, faces(mesh), mesh.views) + end else - error("Conversion from $FT -> $facetype not implemented.") + # 1. add vertex attributes, 2. convert position attribute + va = merge(vertex_attributes(mesh), NamedTuple(attributes)) + va = merge(va, (position = decompose(pointtype, va.position),)) + + # update face types + f, views = decompose(facetype, faces(mesh), mesh.views) + return Mesh(va, f, views) end end +# catch-all else because these conversions need some oversight +function mesh(m::AbstractMesh, args...; kwargs...) + # TODO: no methoderror with kwargs? + error("No method mesh() found for the given inputs.") +end + + const SimpleMesh{N, T, FT} = Mesh{N, T, FT, (:position,), Tuple{Vector{Point{N, T}}}, Vector{FT}} const SimpleTriangleMesh{N} = SimpleMesh{N, Float32, GLTriangleFace} From 08aedf81fe1caf28df6ef4a67fc8ac8cf9ce7eda Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 2 Sep 2024 21:47:08 +0200 Subject: [PATCH 020/120] fix normals(), cleanup tests --- src/basic_types.jl | 4 +++- test/geometrytypes.jl | 2 +- test/meshes.jl | 2 +- test/runtests.jl | 9 ++++++--- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 3580c9c7..689ad10f 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -97,6 +97,7 @@ Base.getindex(f::MultiFace, i::Integer) = Base.getindex(getfield(f, :faces), i) Base.eltype(::MultiFace{N, T, FT}) where {N, T, FT} = FT Base.eltype(::Type{<: MultiFace{N, T, FT}}) where {N, T, FT} = FT +# TODO: can we do this with a conversion? E.g. MultiFace{Names}.(faces)? function simplify_faces(::Type{MF1}, fs::AbstractVector{MF2}) where {MF1 <: MultiFace, MF2 <: MultiFace} return simplify_faces(propertynames(MF1), fs) end @@ -438,7 +439,7 @@ end coordinates(mesh::Mesh) = mesh.position faces(mesh::Mesh) = mesh.connectivity -normals(mesh::Mesh) = hasproperty(mesh, :normal) ? mesh.normal : nothing +normals(mesh::Mesh) = hasproperty(mesh, :normals) ? mesh.normals : nothing texturecoordinates(mesh::Mesh) = hasproperty(mesh, :uv) ? mesh.uv : nothing vertex_attributes(mesh::Mesh) = getfield(mesh, :vertex_attributes) @@ -513,6 +514,7 @@ Base.get!(f, mesh::MetaMesh, key::Symbol) = get!(f, getfield(mesh, :meta), key) 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)) diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index ec68a151..f9eb5f18 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -96,7 +96,7 @@ end [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]] - @test decompose(Point{3,Int}, b) == pt_expb + @test_broken decompose(Point{3,Int}, b) == pt_expb mesh = normal_mesh(b) @test isempty(Rect{3,Float32}()) diff --git a/test/meshes.jl b/test/meshes.jl index 4dc60b9e..f867d841 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -39,7 +39,7 @@ end m = Mesh( [GeometryBasics.NormalFace(QuadFace(1, 2, 3, 4), QuadFace(1,1,1,1))], position = Point2f[(0, 0), (1, 0), (1, 1), (0, 1)], - normal = [Vec3f(0,0,1)] + normals = [Vec3f(0,0,1)] ) m2 = GeometryBasics.merge_vertex_indices(m) diff --git a/test/runtests.jl b/test/runtests.jl index 4cc17007..9df99b02 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -44,15 +44,18 @@ end @test mesh.normals === normals @test mesh.position === points @test GeometryBasics.faces(mesh) === tfaces - @test propertynames(mesh) == (:normals, :stress) + @test_broken propertynames(mesh) == (:normals, :stress) + @test propertynames(mesh) == (:vertex_attributes, :connectivity, :views, :position, :normals, :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_broken m_meta.boundingbox === Rect(1.0, 1.0, 2.0, 2.0) + @test_broken 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 From e594800fee15e21cb25fa6797af3d3976ffe23c0 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 3 Sep 2024 01:53:34 +0200 Subject: [PATCH 021/120] add mesh constructor tests + fixes --- src/basic_types.jl | 5 +- src/meshes.jl | 17 +++-- test/meshes.jl | 153 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 166 insertions(+), 9 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 689ad10f..28b59205 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -86,7 +86,6 @@ Constructs a `MultiFace` from a tuple of names `Names::NTuple{M, Symbol}` and MultiFace(; kwargs...) = MultiFace(NamedTuple(kwargs)) MultiFace{Names}(args...) where {Names} = MultiFace(NamedTuple{Names}(args)) MultiFace{Names}(args::Tuple{Vararg{<: AbstractFace}}) where {Names} = MultiFace(NamedTuple{Names}(args)) -MultiFace{Names, FT}(args) where {Names, FT <: AbstractFace} = MultiFace(NamedTuple{Names}(FT.(args))) MultiFace{Names}(f::MultiFace) where {Names} = MultiFace{Names}(getproperty.((f,), Names)) Base.getindex(f::MultiFace, i::Integer) = Base.getindex(getfield(f, :faces), i) @@ -107,8 +106,8 @@ function simplify_faces(names::NTuple{N, Symbol}, fs::AbstractVector{MF2}) where end # TODO: Some shorthands -const NormalFace = MultiFace{(:position, :normals)} -const NormalUVFace = MultiFace{(:position, :normals, :uv)} +NormalFace{FT}(faces::Tuple) where {FT} = MultiFace{(:position, :normals)}(FT.(faces)) +NormalUVFace{FT}(faces::Tuple) where {FT} = MultiFace{(:position, :normals, :uv)}(FT.(faces)) # TODO: enable something like NormalUVFace{QuadFace}[...] diff --git a/src/meshes.jl b/src/meshes.jl index 6a6ee40b..0341933b 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -117,12 +117,12 @@ function mesh( va = merge(vertex_attributes(mesh), (position = decompose(pointtype, mesh),)) # MultiFace{VertexFace} -> VertexFace - attribs, fs, views = merge_vertex_indices(va, faces(mesh), mesh.views) + attribs, _fs, views = merge_vertex_indices(va, faces(mesh), mesh.views) # VertexFace -> facetype - faces, views = decompose(facetype, fs, views) + fs, views = decompose(facetype, _fs, views) - return Mesh(attribs, faces, views) + return Mesh(attribs, fs, views) end end @@ -131,6 +131,11 @@ function mesh( facetype::Type{<: AbstractVertexFace} = GLTriangleFace, attributes... ) where {D, T, FT <: AbstractVertexFace} + + N = length(mesh.position) + if !all(attr -> length(attr) == N, values(attributes)) + error("At least one of the given vertex attributes does not match `length(mesh.positon) = $N`.") + end if FT == facetype if isempty(attributes) && GeometryBasics.pointtype(mesh) == pointtype @@ -184,13 +189,13 @@ end pointtype(::Mesh{D, T}) where {D, T} = Point{D, T} facetype(::Mesh{D, T, FT}) where {D, T, FT} = FT -function triangle_mesh(primitive::Mesh{N}; pointtype = Point{N, Float32}, facetype = GLTriangleFace) where {N} +function triangle_mesh(primitive::Mesh{N}; pointtype = Point{N, Float32}) where {N} # already target type: if GeometryBasics.pointtype(primitive) === pointtype && - GeometryBasics.facetype(primitive) === facetype + GeometryBasics.facetype(primitive) === GLTriangleFace return primitive else - return mesh(primitive; pointtype = pointtype, facetype = facetype) + return mesh(primitive; pointtype = pointtype, facetype = GLTriangleFace) end end diff --git a/test/meshes.jl b/test/meshes.jl index f867d841..0ad77f5f 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -53,4 +53,157 @@ end @test normals(m2) != normals(m) @test normals(m2) == [only(normals(m)) for _ in 1:4] @test isempty(m2.views) +end + +@testset "mesh() constructors" begin + r = Rect3d(Point3d(-1), Vec3d(2)) + + @testset "prerequisites" begin + ps = collect(coordinates(r)) + @test length(ps) == 8 + @test ps isa Vector{Point3d} + ns = collect(normals(r)) + @test length(ns) == 6 + @test ns isa Vector{Vec3f} + uvs = collect(texturecoordinates(r)) + @test length(uvs) == 8 + @test_broken uvs isa Vector{Vec2f} + fs = collect(faces(r)) + @test length(fs) == 6 + @test fs isa Vector{GeometryBasics.MultiFace{4, Int64, QuadFace{Int64}, (:position, :normals, :uv), 3}} + end + + @testset "normal_mesh()" begin + # TODO: simplify? + FT = GeometryBasics.MultiFace{4, Int64, QuadFace{Int64}, (:position, :normals), 2} + m = normal_mesh(r, pointtype = Point3f, normaltype = Vec3f, facetype = FT) + + @test hasproperty(m, :position) + @test coordinates(m) isa Vector{Point3f} + @test length(coordinates(m)) == 8 + @test GeometryBasics.pointtype(m) == Point3f + + @test hasproperty(m, :normals) + @test normals(m) isa Vector{Vec3f} + @test length(normals(m)) == 6 + + @test !hasproperty(m, :uv) + @test texturecoordinates(m) === nothing + + @test faces(m) isa Vector{FT} + @test length(faces(m)) == 6 + @test GeometryBasics.facetype(m) == FT + 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)) == 24 + @test GeometryBasics.pointtype(m) == Point3d + + @test hasproperty(m, :normals) + @test normals(m) isa Vector{Vec3d} + @test length(normals(m)) == 24 + + @test hasproperty(m, :uv) + @test texturecoordinates(m) isa Vector{Vec3d} + @test length(texturecoordinates(m)) == 24 + + @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, :normals) + @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, :normals) + @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 + FT = GeometryBasics.MultiFace{4, Int64, QuadFace{Int64}, (:position, :normals), 2} + m = GeometryBasics.mesh(r, pointtype = Point3f, normals = normals(r), facetype = FT) + + # Should be hit by normal_mesh as well... + @test coordinates(m) isa Vector{Point3f} + @test length(coordinates(m)) == 8 + @test normals(m) isa Vector{Vec3f} + @test length(normals(m)) == 6 + @test !hasproperty(m, :uv) + @test texturecoordinates(m) === nothing + @test faces(m) isa Vector{FT} + @test length(faces(m)) == 6 + + # shouldn't be able to add vertex attributes when they aren't synchronized + @test_throws ErrorException GeometryBasics.mesh(m, uv = Vec3f[]) + + # Can convert face type - should remap + m2 = GeometryBasics.mesh(m, facetype = QuadFace{Int32}) + + @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{Int32}} + @test length(faces(m2)) == 6 + + # remap + decompose face and then adding vertex attribute is fine + m2 = GeometryBasics.mesh(m, facetype = GLTriangleFace) + # should be same length as other vertices... + @test_throws ErrorException GeometryBasics.mesh(m2, uv = Vec3f[]) + m2 = GeometryBasics.mesh(m2, 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 From 74e430403b956b834b58722e3ec14a7c1ae09379 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 4 Sep 2024 16:32:26 +0200 Subject: [PATCH 022/120] deprecate normals for normal as vertex attribute name --- src/basic_types.jl | 21 ++++++++++++++++++--- src/meshes.jl | 6 +++--- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 28b59205..1d4bb93f 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -376,6 +376,12 @@ struct Mesh{ error("The first vertex attribute should be a 'position' but is a '$(first(Names))'.") end + if :normals in Names + @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 = map(name -> ifelse(name === :normals, :normal, name), Names) + va = NamedTuple{names}(values(va)) + end + f_names = propertynames(FT) if Names == f_names # all good @@ -392,7 +398,7 @@ struct Mesh{ ) end - return new{D, T, eltype(typeof(f)), Names, VAT, typeof(f)}(va, f, views) + return new{D, T, eltype(typeof(f)), keys(va), VAT, typeof(f)}(va, f, views) end function Mesh( @@ -410,11 +416,17 @@ struct Mesh{ error("The first vertex attribute should be a 'position' but is a '$(first(Names))'.") end + if :normals in Names + @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 = map(name -> ifelse(name === :normals, :normal, name), Names) + va = NamedTuple{names}(values(va)) + end + # Note: With VertexFaces all vertex attributes should have the same # length as they use a common vertex index. We could check this # here but maybe it's better not to to prevent over-eager checking? - return new{D, T, FT, Names, VAT, FVT}(va, f, views) + return new{D, T, FT, keys(va), VAT, FVT}(va, f, views) end end @@ -428,6 +440,9 @@ 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 @@ -438,7 +453,7 @@ end coordinates(mesh::Mesh) = mesh.position faces(mesh::Mesh) = mesh.connectivity -normals(mesh::Mesh) = hasproperty(mesh, :normals) ? mesh.normals : nothing +normals(mesh::Mesh) = hasproperty(mesh, :normal) ? mesh.normal : nothing texturecoordinates(mesh::Mesh) = hasproperty(mesh, :uv) ? mesh.uv : nothing vertex_attributes(mesh::Mesh) = getfield(mesh, :vertex_attributes) diff --git a/src/meshes.jl b/src/meshes.jl index 0341933b..541440f9 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -218,7 +218,7 @@ function uv_normal_mesh( return mesh( primitive, uv = decompose_uv(uvtype, primitive), - normals = decompose_normals(normaltype, primitive), + normal = decompose_normals(normaltype, primitive), pointtype = pointtype, facetype = facetype ) end @@ -229,7 +229,7 @@ function normal_mesh( ) _points = decompose(pointtype, points) _faces = decompose(facetype, faces) - return Mesh(_faces, position = _points, normals = normals(_points, _faces, normaltype)) + return Mesh(_faces, position = _points, normal = normals(_points, _faces, normaltype)) end function normal_mesh( @@ -238,7 +238,7 @@ function normal_mesh( ) where {N} return mesh( - primitive, normals = decompose_normals(normaltype, primitive), + primitive, normal = decompose_normals(normaltype, primitive), pointtype = pointtype, facetype = facetype) end From ad567dca78476b837df3cc6eb6c2c7dc852d7249 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 4 Sep 2024 16:59:26 +0200 Subject: [PATCH 023/120] make NormalFace and NormalUVFace types again --- src/basic_types.jl | 23 +- src/primitives/rectangles.jl | 5 +- test/meshes.jl | 416 +++++++++++++++++------------------ 3 files changed, 228 insertions(+), 216 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 1d4bb93f..1984e0ff 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -84,9 +84,17 @@ Constructs a `MultiFace` from a tuple of names `Names::NTuple{M, Symbol}` and `faces::NTuple{M, FaceType}` similar to how a NamedTuple would. """ MultiFace(; kwargs...) = MultiFace(NamedTuple(kwargs)) -MultiFace{Names}(args...) where {Names} = MultiFace(NamedTuple{Names}(args)) -MultiFace{Names}(args::Tuple{Vararg{<: AbstractFace}}) where {Names} = MultiFace(NamedTuple{Names}(args)) MultiFace{Names}(f::MultiFace) where {Names} = MultiFace{Names}(getproperty.((f,), Names)) +MultiFace{Names}(args...) where {Names} = MultiFace{Names}(args) +function MultiFace{Names}(args::Tuple{Vararg{<: AbstractVertexFace}}) where {Names} + return MultiFace(NamedTuple{Names}(args)) +end + +function MultiFace{N, T, FT, Names, M}(faces...) where {N, T, FT <: AbstractVertexFace{N, T}, Names, M} + @assert length(faces) === M + @assert length(Names) === M + return MultiFace{Names}(FT.(face)) +end Base.getindex(f::MultiFace, i::Integer) = Base.getindex(getfield(f, :faces), i) @inline Base.hasproperty(f::MultiFace, field::Symbol) = hasproperty(getfield(f, :faces), field) @@ -105,11 +113,14 @@ function simplify_faces(names::NTuple{N, Symbol}, fs::AbstractVector{MF2}) where return map(f -> MultiFace{names}(f), fs) end -# TODO: Some shorthands -NormalFace{FT}(faces::Tuple) where {FT} = MultiFace{(:position, :normals)}(FT.(faces)) -NormalUVFace{FT}(faces::Tuple) where {FT} = MultiFace{(:position, :normals, :uv)}(FT.(faces)) +# Shorthands for constructing faces +const NormalFace{N, T, FT <: AbstractVertexFace{N, T}} = MultiFace{N, T, FT, (:position, :normal), 2} +const NormalUVFace{N, T, FT <: AbstractVertexFace{N, T}} = MultiFace{N, T, FT, (:position, :normal, :uv), 3} -# TODO: enable something like NormalUVFace{QuadFace}[...] +# enable something like NormalUVFace{GLTriangleFace}[(pos_face, normal_face, uv_face), ...] +function Base.convert(::Type{<: MultiFace{N, T, FT, Names}}, t::Tuple) where {N, T, FT, Names} + return MultiFace{Names}(FT.(t)) +end @propagate_inbounds Base.getindex(x::Polytope, i::Integer) = coordinates(x)[i] @propagate_inbounds Base.iterate(x::Polytope) = iterate(coordinates(x)) diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index d898c8e3..a2f45f6e 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -588,12 +588,13 @@ function texturecoordinates(rect::Rect3) end function faces(::Rect3) - return NormalUVFace{QuadFace}.([ + return NormalUVFace{4, Int, QuadFace{Int}}[ + # position normal uv ((1, 2, 3, 4), 1, (1, 2, 3, 4)), # -x ((5, 6, 7, 8), 2, (5, 6, 7, 8)), # +x ((1, 2, 5, 6), 3, (1, 2, 5, 6)), # -y ((3, 4, 7, 8), 4, (3, 4, 7, 8)), # +y ((1, 3, 5, 7), 5, (1, 3, 5, 7)), # -z ((2, 4, 6, 8), 6, (2, 4, 6, 8)), # +z - ]) + ] end diff --git a/test/meshes.jl b/test/meshes.jl index 0ad77f5f..d75cd784 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -1,209 +1,209 @@ -@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 - # TODO: extend - m = Mesh( - [GeometryBasics.NormalFace(QuadFace(1, 2, 3, 4), QuadFace(1,1,1,1))], - position = Point2f[(0, 0), (1, 0), (1, 1), (0, 1)], - normals = [Vec3f(0,0,1)] - ) - - m2 = GeometryBasics.merge_vertex_indices(m) - - @test faces(m) isa AbstractVector{<: GeometryBasics.MultiFace} - @test propertynames(eltype(faces(m))) == keys(GeometryBasics.vertex_attributes(m)) - @test isempty(m.views) - - @test faces(m2) isa AbstractVector{<: QuadFace} - @test coordinates(m2) == coordinates(m) - @test normals(m2) != normals(m) - @test normals(m2) == [only(normals(m)) for _ in 1:4] - @test isempty(m2.views) -end - -@testset "mesh() constructors" begin - r = Rect3d(Point3d(-1), Vec3d(2)) - - @testset "prerequisites" begin - ps = collect(coordinates(r)) - @test length(ps) == 8 - @test ps isa Vector{Point3d} - ns = collect(normals(r)) - @test length(ns) == 6 - @test ns isa Vector{Vec3f} - uvs = collect(texturecoordinates(r)) - @test length(uvs) == 8 - @test_broken uvs isa Vector{Vec2f} - fs = collect(faces(r)) - @test length(fs) == 6 - @test fs isa Vector{GeometryBasics.MultiFace{4, Int64, QuadFace{Int64}, (:position, :normals, :uv), 3}} - end - - @testset "normal_mesh()" begin - # TODO: simplify? - FT = GeometryBasics.MultiFace{4, Int64, QuadFace{Int64}, (:position, :normals), 2} - m = normal_mesh(r, pointtype = Point3f, normaltype = Vec3f, facetype = FT) - - @test hasproperty(m, :position) - @test coordinates(m) isa Vector{Point3f} - @test length(coordinates(m)) == 8 - @test GeometryBasics.pointtype(m) == Point3f - - @test hasproperty(m, :normals) - @test normals(m) isa Vector{Vec3f} - @test length(normals(m)) == 6 - - @test !hasproperty(m, :uv) - @test texturecoordinates(m) === nothing - - @test faces(m) isa Vector{FT} - @test length(faces(m)) == 6 - @test GeometryBasics.facetype(m) == FT - 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)) == 24 - @test GeometryBasics.pointtype(m) == Point3d - - @test hasproperty(m, :normals) - @test normals(m) isa Vector{Vec3d} - @test length(normals(m)) == 24 - - @test hasproperty(m, :uv) - @test texturecoordinates(m) isa Vector{Vec3d} - @test length(texturecoordinates(m)) == 24 - - @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, :normals) - @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, :normals) - @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 - FT = GeometryBasics.MultiFace{4, Int64, QuadFace{Int64}, (:position, :normals), 2} - m = GeometryBasics.mesh(r, pointtype = Point3f, normals = normals(r), facetype = FT) - - # Should be hit by normal_mesh as well... - @test coordinates(m) isa Vector{Point3f} - @test length(coordinates(m)) == 8 - @test normals(m) isa Vector{Vec3f} - @test length(normals(m)) == 6 - @test !hasproperty(m, :uv) - @test texturecoordinates(m) === nothing - @test faces(m) isa Vector{FT} - @test length(faces(m)) == 6 - - # shouldn't be able to add vertex attributes when they aren't synchronized - @test_throws ErrorException GeometryBasics.mesh(m, uv = Vec3f[]) - - # Can convert face type - should remap - m2 = GeometryBasics.mesh(m, facetype = QuadFace{Int32}) - - @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{Int32}} - @test length(faces(m2)) == 6 - - # remap + decompose face and then adding vertex attribute is fine - m2 = GeometryBasics.mesh(m, facetype = GLTriangleFace) - # should be same length as other vertices... - @test_throws ErrorException GeometryBasics.mesh(m2, uv = Vec3f[]) - m2 = GeometryBasics.mesh(m2, 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 +@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 + # TODO: extend + m = Mesh( + [GeometryBasics.MultiFace(position = QuadFace(1, 2, 3, 4), normal = QuadFace(1,1,1,1))], + position = Point2f[(0, 0), (1, 0), (1, 1), (0, 1)], + normals = [Vec3f(0,0,1)] + ) + + m2 = GeometryBasics.merge_vertex_indices(m) + + @test faces(m) isa AbstractVector{<: GeometryBasics.MultiFace} + @test propertynames(eltype(faces(m))) == keys(GeometryBasics.vertex_attributes(m)) + @test isempty(m.views) + + @test faces(m2) isa AbstractVector{<: QuadFace} + @test coordinates(m2) == coordinates(m) + @test normals(m2) != normals(m) + @test normals(m2) == [only(normals(m)) for _ in 1:4] + @test isempty(m2.views) +end + +@testset "mesh() constructors" begin + r = Rect3d(Point3d(-1), Vec3d(2)) + + @testset "prerequisites" begin + ps = collect(coordinates(r)) + @test length(ps) == 8 + @test ps isa Vector{Point3d} + ns = collect(normals(r)) + @test length(ns) == 6 + @test ns isa Vector{Vec3f} + uvs = collect(texturecoordinates(r)) + @test length(uvs) == 8 + @test_broken uvs isa Vector{Vec2f} + fs = collect(faces(r)) + @test length(fs) == 6 + @test fs isa Vector{GeometryBasics.NormalUVFace{4, Int64, QuadFace{Int64}}} + end + + @testset "normal_mesh()" begin + # TODO: simplify? + FT = GeometryBasics.NormalFace{4, Int64, QuadFace{Int64}} + m = normal_mesh(r, pointtype = Point3f, normaltype = Vec3f, facetype = FT) + + @test hasproperty(m, :position) + @test coordinates(m) isa Vector{Point3f} + @test length(coordinates(m)) == 8 + @test GeometryBasics.pointtype(m) == Point3f + + @test hasproperty(m, :normals) + @test normals(m) isa Vector{Vec3f} + @test length(normals(m)) == 6 + + @test !hasproperty(m, :uv) + @test texturecoordinates(m) === nothing + + @test faces(m) isa Vector{FT} + @test length(faces(m)) == 6 + @test GeometryBasics.facetype(m) == FT + 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)) == 24 + @test GeometryBasics.pointtype(m) == Point3d + + @test hasproperty(m, :normals) + @test normals(m) isa Vector{Vec3d} + @test length(normals(m)) == 24 + + @test hasproperty(m, :uv) + @test texturecoordinates(m) isa Vector{Vec3d} + @test length(texturecoordinates(m)) == 24 + + @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, :normals) + @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, :normals) + @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 + FT = GeometryBasics.NormalFace{4, Int64, QuadFace{Int64}} + m = GeometryBasics.mesh(r, pointtype = Point3f, normals = normals(r), facetype = FT) + + # Should be hit by normal_mesh as well... + @test coordinates(m) isa Vector{Point3f} + @test length(coordinates(m)) == 8 + @test normals(m) isa Vector{Vec3f} + @test length(normals(m)) == 6 + @test !hasproperty(m, :uv) + @test texturecoordinates(m) === nothing + @test faces(m) isa Vector{FT} + @test length(faces(m)) == 6 + + # shouldn't be able to add vertex attributes when they aren't synchronized + @test_throws ErrorException GeometryBasics.mesh(m, uv = Vec3f[]) + + # Can convert face type - should remap + m2 = GeometryBasics.mesh(m, facetype = QuadFace{Int32}) + + @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{Int32}} + @test length(faces(m2)) == 6 + + # remap + decompose face and then adding vertex attribute is fine + m2 = GeometryBasics.mesh(m, facetype = GLTriangleFace) + # should be same length as other vertices... + @test_throws ErrorException GeometryBasics.mesh(m2, uv = Vec3f[]) + m2 = GeometryBasics.mesh(m2, 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 From 20eab7d5643942c8c8d7083f56f6c3d636a7bec6 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 4 Sep 2024 16:59:52 +0200 Subject: [PATCH 024/120] cleanup tests & normals vs normal --- src/basic_types.jl | 12 +++++++----- test/meshes.jl | 12 ++++++------ test/runtests.jl | 28 ++++++++++++++-------------- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 1984e0ff..6bfd9fb2 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -391,25 +391,27 @@ struct Mesh{ @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 = map(name -> ifelse(name === :normals, :normal, name), Names) va = NamedTuple{names}(values(va)) + else + names = Names end f_names = propertynames(FT) - if Names == f_names + if names == f_names # all good - elseif issubset(Names, f_names) + elseif issubset(names, f_names) # Remove the extra names in faces/fix order # Note: This might be redundant with `mesh()`. It is supposed to allow # using a general `MultiFace` in `faces(primitive)` which then # gets reduced to the vertex attributes used in a general mesh. - f = simplify_faces(Names, f) + f = simplify_faces(names, f) else error( - "Cannot construct a mesh with vertex attribute names $Names and MultiFace " * + "Cannot construct a mesh with vertex attribute names $names and MultiFace " * "attribute names $f_names. These must include the same names in the same order." ) end - return new{D, T, eltype(typeof(f)), keys(va), VAT, typeof(f)}(va, f, views) + return new{D, T, eltype(typeof(f)), names, VAT, typeof(f)}(va, f, views) end function Mesh( diff --git a/test/meshes.jl b/test/meshes.jl index d75cd784..8c6a4b16 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -39,7 +39,7 @@ end m = Mesh( [GeometryBasics.MultiFace(position = QuadFace(1, 2, 3, 4), normal = QuadFace(1,1,1,1))], position = Point2f[(0, 0), (1, 0), (1, 1), (0, 1)], - normals = [Vec3f(0,0,1)] + normal = [Vec3f(0,0,1)] ) m2 = GeometryBasics.merge_vertex_indices(m) @@ -83,7 +83,7 @@ end @test length(coordinates(m)) == 8 @test GeometryBasics.pointtype(m) == Point3f - @test hasproperty(m, :normals) + @test hasproperty(m, :normal) @test normals(m) isa Vector{Vec3f} @test length(normals(m)) == 6 @@ -106,7 +106,7 @@ end @test length(coordinates(m)) == 24 @test GeometryBasics.pointtype(m) == Point3d - @test hasproperty(m, :normals) + @test hasproperty(m, :normal) @test normals(m) isa Vector{Vec3d} @test length(normals(m)) == 24 @@ -129,7 +129,7 @@ end @test length(coordinates(m)) == 8 @test GeometryBasics.pointtype(m) == Point3f - @test !hasproperty(m, :normals) + @test !hasproperty(m, :normal) @test normals(m) === nothing @test hasproperty(m, :uv) @@ -149,7 +149,7 @@ end @test length(coordinates(m)) == 8 @test GeometryBasics.pointtype(m) == Point3f - @test !hasproperty(m, :normals) + @test !hasproperty(m, :normal) @test normals(m) === nothing @test !hasproperty(m, :uv) @@ -162,7 +162,7 @@ end @testset "mesh(mesh)" begin FT = GeometryBasics.NormalFace{4, Int64, QuadFace{Int64}} - m = GeometryBasics.mesh(r, pointtype = Point3f, normals = normals(r), facetype = FT) + m = GeometryBasics.mesh(r, pointtype = Point3f, normal = normals(r), facetype = FT) # Should be hit by normal_mesh as well... @test coordinates(m) isa Vector{Point3f} diff --git a/test/runtests.jl b/test/runtests.jl index 9df99b02..ce19d6e6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,13 +8,14 @@ using Extents @testset "GeometryBasics" begin @testset "algorithms" begin - cube = Rect(Vec3f(-0.5), Vec3f(1)) - cube_faces = decompose(TriangleFace{Int}, faces(cube)) - cube_vertices = decompose(Point{3,Float32}, cube) - @test area(cube_vertices, cube_faces) == 6 - mesh = Mesh(cube_vertices, cube_faces) - @test GeometryBasics.volume(mesh) ≈ 1 - @test GeometryBasics.volume(cube) ≈ 1 + @test_broken false + # cube = Rect(Vec3f(-0.5), Vec3f(1)) + # cube_faces = decompose(TriangleFace{Int}, faces(cube)) + # cube_vertices = decompose(Point{3,Float32}, cube) + # @test area(cube_vertices, cube_faces) == 6 + # mesh = Mesh(cube_vertices, cube_faces) + # @test GeometryBasics.volume(mesh) ≈ 1 + # @test GeometryBasics.volume(cube) ≈ 1 rect = Rect(1, 2, 7.5, 2.0) @test GeometryBasics.area(rect) ≈ 15 @@ -36,16 +37,15 @@ end tfaces = TetrahedronFace{Int}[(1, 2, 3, 4), (5, 6, 7, 8)] normals = rand(Vec{3, Float64}, 8) stress = LinRange(0, 1, 8) - mesh = Mesh(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.normal === normals @test mesh.position === points @test GeometryBasics.faces(mesh) === tfaces - @test_broken propertynames(mesh) == (:normals, :stress) - @test propertynames(mesh) == (:vertex_attributes, :connectivity, :views, :position, :normals, :stress) + @test propertynames(mesh) == (:vertex_attributes, :connectivity, :views, :position, :normal, :stress) end end @@ -140,7 +140,7 @@ end 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}) @@ -203,7 +203,7 @@ end @testset "convert mesh + meta" begin m = uv_normal_mesh(Circle(Point2f(0), 1f0)) # For 2d primitives normal is just the upvector - @test m.normals == [Vec3f(0, 0, 1) for p in coordinates(m)] + @test m.normal == [Vec3f(0, 0, 1) for p in coordinates(m)] end @testset "mesh conversion" begin From 35ddd364b4ce655951e35f7a70bb4e18dc7368a7 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 4 Sep 2024 17:01:36 +0200 Subject: [PATCH 025/120] let ci run --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) 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 }} From d20c615fd469557fc8afb979a4ca0552110bcd1d Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 4 Sep 2024 20:47:37 +0200 Subject: [PATCH 026/120] remove views aware face decomposition --- src/interfaces.jl | 44 +++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/interfaces.jl b/src/interfaces.jl index a7261b2c..51a5d0da 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -106,11 +106,12 @@ function decompose(::Type{F}, primitive::AbstractGeometry) where {F<:AbstractVer end return collect_with_eltype(F, f) end - -function decompose(::Type{F}, mesh::AbstractMesh) where {F<:AbstractVertexFace} - m = Mesh(mesh) - return decompose(F, faces(m), m.views) -end + +# TODO: move +# function decompose(::Type{F}, mesh::AbstractMesh) where {F<:AbstractVertexFace} +# m = Mesh(mesh) +# return decompose(F, faces(m), m.views) +# end function decompose(::Type{F}, f::AbstractVector) where {F<:AbstractVertexFace} fs = faces(f) @@ -118,22 +119,23 @@ function decompose(::Type{F}, f::AbstractVector) where {F<:AbstractVertexFace} return collect_with_eltype(F, fs) end -function decompose(::Type{F}, f::AbstractVector, views::Vector{UnitRange{Int}}) where {F<:AbstractVertexFace} - 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 = UnitRange{Int}[] - 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 +# TODO: Move +# function decompose(::Type{F}, f::AbstractVector, views::Vector{UnitRange{Int}}) where {F<:AbstractVertexFace} +# 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 = UnitRange{Int}[] +# 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)) From 4774ed422ff4bcad2066c45a7e3b337ec65f3c51 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 4 Sep 2024 21:36:07 +0200 Subject: [PATCH 027/120] fix rect index order --- src/primitives/rectangles.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index a2f45f6e..7e2f5798 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -590,11 +590,11 @@ end function faces(::Rect3) return NormalUVFace{4, Int, QuadFace{Int}}[ # position normal uv - ((1, 2, 3, 4), 1, (1, 2, 3, 4)), # -x - ((5, 6, 7, 8), 2, (5, 6, 7, 8)), # +x - ((1, 2, 5, 6), 3, (1, 2, 5, 6)), # -y - ((3, 4, 7, 8), 4, (3, 4, 7, 8)), # +y - ((1, 3, 5, 7), 5, (1, 3, 5, 7)), # -z - ((2, 4, 6, 8), 6, (2, 4, 6, 8)), # +z + ((1, 2, 4, 3), 1, (1, 2, 4, 3)), # -x + ((5, 6, 8, 7), 2, (5, 6, 8, 7)), # +x + ((1, 2, 6, 5), 3, (1, 2, 6, 5)), # -y + ((3, 4, 8, 7), 4, (3, 4, 8, 7)), # +y + ((1, 3, 7, 5), 5, (1, 3, 7, 5)), # -z + ((2, 4, 8, 6), 6, (2, 4, 8, 6)), # +z ] end From c1d9f8579d60c7760b0d5f3108bca0dfd440cf25 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 4 Sep 2024 23:00:32 +0200 Subject: [PATCH 028/120] fix MultiFace remapping with OffsetIntegers --- src/meshes.jl | 2 +- src/offsetintegers.jl | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/meshes.jl b/src/meshes.jl index 541440f9..a497742c 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -476,7 +476,7 @@ function merge_vertex_indices( # keep track of the remmaped indices for one vertex so we don't have to # query the dict twice - temp = zeros(N) + temp = Vector{T}(undef, N) for multi_face in faces diff --git a/src/offsetintegers.jl b/src/offsetintegers.jl index c921f1dd..40aef53e 100644 --- a/src/offsetintegers.jl +++ b/src/offsetintegers.jl @@ -92,3 +92,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 From 0bfe393d64a3281da2c79b3b1adcc0020a5d36b3 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 4 Sep 2024 23:01:14 +0200 Subject: [PATCH 029/120] add moreMultiFace utils --- src/basic_types.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/basic_types.jl b/src/basic_types.jl index 6bfd9fb2..3ed03af8 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -86,6 +86,9 @@ Constructs a `MultiFace` from a tuple of names `Names::NTuple{M, Symbol}` and MultiFace(; kwargs...) = MultiFace(NamedTuple(kwargs)) MultiFace{Names}(f::MultiFace) where {Names} = MultiFace{Names}(getproperty.((f,), Names)) MultiFace{Names}(args...) where {Names} = MultiFace{Names}(args) +function MultiFace{Names}(args::NTuple{M, NTuple{N, <: Integer}}) where {Names, N, M} + return MultiFace(NamedTuple{Names}(NgonFace.(args))) +end function MultiFace{Names}(args::Tuple{Vararg{<: AbstractVertexFace}}) where {Names} return MultiFace(NamedTuple{Names}(args)) end @@ -96,6 +99,14 @@ function MultiFace{N, T, FT, Names, M}(faces...) where {N, T, FT <: AbstractVert return MultiFace{Names}(FT.(face)) end +# As you get from shorthands without defining N, T, FT +const UnspecializedMultiFace{Names, M} = MultiFace{N, T, FT, Names, M} where {N, T, FT <: AbstractVertexFace{N, T}} +function UnspecializedMultiFace{Names, M}(faces...) where {Names, M} + @assert length(faces) === M + @assert length(Names) === M + return MultiFace{Names}(faces) +end + Base.getindex(f::MultiFace, i::Integer) = Base.getindex(getfield(f, :faces), i) @inline Base.hasproperty(f::MultiFace, field::Symbol) = hasproperty(getfield(f, :faces), field) @inline Base.getproperty(f::MultiFace, field::Symbol) = getproperty(getfield(f, :faces), field) @@ -114,6 +125,7 @@ function simplify_faces(names::NTuple{N, Symbol}, fs::AbstractVector{MF2}) where end # Shorthands for constructing faces +const UVFace{N, T, FT <: AbstractVertexFace{N, T}} = MultiFace{N, T, FT, (:position, :uv), 2} const NormalFace{N, T, FT <: AbstractVertexFace{N, T}} = MultiFace{N, T, FT, (:position, :normal), 2} const NormalUVFace{N, T, FT <: AbstractVertexFace{N, T}} = MultiFace{N, T, FT, (:position, :normal, :uv), 3} From ab6d8030b5ca330166be82dc6e5064ff2c530c6a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 5 Sep 2024 15:37:58 +0200 Subject: [PATCH 030/120] restore decompose(FaceType, faces, views) --- src/interfaces.jl | 40 +++++++++++++++++----------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/src/interfaces.jl b/src/interfaces.jl index 51a5d0da..0b8eb42f 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -107,35 +107,29 @@ function decompose(::Type{F}, primitive::AbstractGeometry) where {F<:AbstractVer return collect_with_eltype(F, f) end -# TODO: move -# function decompose(::Type{F}, mesh::AbstractMesh) where {F<:AbstractVertexFace} -# m = Mesh(mesh) -# return decompose(F, faces(m), m.views) -# end - function decompose(::Type{F}, f::AbstractVector) where {F<:AbstractVertexFace} fs = faces(f) isnothing(fs) && error("No faces defined for $(typeof(f))") return collect_with_eltype(F, fs) end -# TODO: Move -# function decompose(::Type{F}, f::AbstractVector, views::Vector{UnitRange{Int}}) where {F<:AbstractVertexFace} -# 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 = UnitRange{Int}[] -# 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 +# TODO: Should this be a completely different function? +function decompose(::Type{F}, f::AbstractVector, views::Vector{UnitRange{Int}}) where {F<:AbstractVertexFace} + 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 = UnitRange{Int}[] + 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)) From 00faf5593cafb8dd447906f37d124d5ef1948a1d Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 6 Sep 2024 13:24:22 +0200 Subject: [PATCH 031/120] allow MultiFace -> LineFace conversion --- src/geometry_primitives.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/geometry_primitives.jl b/src/geometry_primitives.jl index 105d1e84..318136ab 100644 --- a/src/geometry_primitives.jl +++ b/src/geometry_primitives.jl @@ -56,6 +56,11 @@ Extract all line segments in a Face. return v end +# This assumes that line faces don't care about normals, uvs, etc +function convert_simplex(::Type{LF}, f::MultiFace) where {LF <: LineFace} + return convert_simplex(LF, f.position) +end + to_pointn(::Type{T}, x) where {T<:Point} = convert_simplex(T, x)[1] # disambiguation method overlords From 2b49fb567a6293ead9833c19e7954593310165a1 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 6 Sep 2024 14:57:50 +0200 Subject: [PATCH 032/120] define Rect3 faces counterclockwise --- src/primitives/rectangles.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index 7e2f5798..b54c0039 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -591,10 +591,10 @@ function faces(::Rect3) return NormalUVFace{4, Int, QuadFace{Int}}[ # position normal uv ((1, 2, 4, 3), 1, (1, 2, 4, 3)), # -x - ((5, 6, 8, 7), 2, (5, 6, 8, 7)), # +x - ((1, 2, 6, 5), 3, (1, 2, 6, 5)), # -y + ((7, 8, 6, 5), 2, (7, 8, 6, 5)), # +x + ((5, 6, 2, 1), 3, (5, 6, 2, 1)), # -y ((3, 4, 8, 7), 4, (3, 4, 8, 7)), # +y ((1, 3, 7, 5), 5, (1, 3, 7, 5)), # -z - ((2, 4, 8, 6), 6, (2, 4, 8, 6)), # +z + ((6, 8, 4, 2), 6, (6, 8, 4, 2)), # +z ] end From d218d6dc663cd7e4c79ba34c731db83a37a6bb3d Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 6 Sep 2024 19:01:30 +0200 Subject: [PATCH 033/120] add more convenience types --- src/basic_types.jl | 16 ++++++++++++++-- src/meshes.jl | 4 ---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 3ed03af8..24ae3413 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -128,6 +128,7 @@ end const UVFace{N, T, FT <: AbstractVertexFace{N, T}} = MultiFace{N, T, FT, (:position, :uv), 2} const NormalFace{N, T, FT <: AbstractVertexFace{N, T}} = MultiFace{N, T, FT, (:position, :normal), 2} const NormalUVFace{N, T, FT <: AbstractVertexFace{N, T}} = MultiFace{N, T, FT, (:position, :normal, :uv), 3} +const UVNormalFace{N, T, FT <: AbstractVertexFace{N, T}} = MultiFace{N, T, FT, (:position, :uv, :normal), 3} # enable something like NormalUVFace{GLTriangleFace}[(pos_face, normal_face, uv_face), ...] function Base.convert(::Type{<: MultiFace{N, T, FT, Names}}, t::Tuple) where {N, T, FT, Names} @@ -494,8 +495,8 @@ function Base.getindex(::Mesh, f::AbstractMultiFace) end function Base.:(==)(a::Mesh, b::Mesh) - return a.vertex_attributes == b.vertex_attributes && - faces(a) == faces(b) && a.views == b.views + return (a.vertex_attributes == b.vertex_attributes) && + (faces(a) == faces(b)) && (a.views == b.views) end function Base.iterate(mesh::Mesh, i=1) @@ -565,3 +566,14 @@ meta(@nospecialize(m)) = NamedTuple() meta(mesh::MetaMesh) = getfield(mesh, :meta) Mesh(mesh::MetaMesh) = getfield(mesh, :mesh) Mesh(mesh::Mesh) = mesh + + +# Shorthand types +# used in meshes.jl +const SimpleMesh{N, T, FT} = Mesh{N, T, FT, (:position,), Tuple{Vector{Point{N, T}}}, Vector{FT}} +const SimpleTriangleMesh{N} = SimpleMesh{N, Float32, GLTriangleFace} +# +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 GLNormalMesh{N} = NormalMesh{N, Float32, GLTriangleFace} +const GLNormalUVMesh{N} = NormalUVMesh{N, Float32, GLTriangleFace} \ No newline at end of file diff --git a/src/meshes.jl b/src/meshes.jl index a497742c..46696709 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -163,10 +163,6 @@ function mesh(m::AbstractMesh, args...; kwargs...) error("No method mesh() found for the given inputs.") end - -const SimpleMesh{N, T, FT} = Mesh{N, T, FT, (:position,), Tuple{Vector{Point{N, T}}}, Vector{FT}} -const SimpleTriangleMesh{N} = SimpleMesh{N, Float32, GLTriangleFace} - """ mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace, normaltype=nothing) From e5cf8046155ebddd1670c2a8fc508520b37c6e4b Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 6 Sep 2024 19:01:43 +0200 Subject: [PATCH 034/120] filter nothing attributes --- src/meshes.jl | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/meshes.jl b/src/meshes.jl index 46696709..cc69cd3d 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -30,6 +30,12 @@ function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleF return mesh(positions, collect(fs); facetype = facetype, vertex_attributes...) end +function drop_nothing_kwargs(kwargs::Base.Pairs) + _keys = filter(k -> !isnothing(kwargs[k]), keys(kwargs)) + _vals = ntuple(n -> kwargs[_keys[n]], length(_keys)) + return NamedTuple{_keys}(_vals) +end + """ mesh(positions, faces[, facetype = GLTriangleFace, vertex_attributes...]) @@ -46,6 +52,8 @@ function mesh( facetype=GLTriangleFace, vertex_attributes... ) + va = drop_nothing_kwargs(vertex_attributes) + if facetype <: AbstractMultiFace # drop vertex attribute references in faces that facetype does not include f = simplify_faces(facetype, faces) # TODO: call this decompose? @@ -54,16 +62,14 @@ function mesh( # drop vertex attributes references in faces that are not part of the # given vertex attributes. (This allows multi_face to be more general # than the mesh we construct) - names = (:position, keys(vertex_attributes)...) + names = (:position, keys(va)...) _f2 = simplify_faces(names, faces) # Convert MultiFace to its internally used VertexFace type and apply # necessary vertex attribute remapping _f, mappings = merge_vertex_indices(_f2) positions = positions[mappings[1]] - vertex_attributes = NamedTuple( - (Pair(names[i], vertex_attributes[i-1][mappings[i]]) for i in 2:length(mappings)) - ) + va = NamedTuple((Pair(names[i], va[i-1][mappings[i]]) for i in 2:length(mappings))) # Convert to actually requested facetype f = decompose(facetype, _f) @@ -71,7 +77,7 @@ function mesh( error("Cannot convert MultiFace to $facetype.") end - return Mesh(positions, f; vertex_attributes...) + return Mesh(positions, f; va...) end function mesh( @@ -80,7 +86,8 @@ function mesh( facetype=GLTriangleFace, vertex_attributes...) f = decompose(facetype, faces) - return Mesh(positions, f; vertex_attributes...) + va = drop_nothing_kwargs(vertex_attributes) + return Mesh(positions, f; va...) end """ @@ -132,23 +139,25 @@ function mesh( attributes... ) where {D, T, FT <: AbstractVertexFace} + va = drop_nothing_kwargs(attributes) + N = length(mesh.position) - if !all(attr -> length(attr) == N, values(attributes)) + if !all(attr -> length(attr) == N, values(va)) error("At least one of the given vertex attributes does not match `length(mesh.positon) = $N`.") end if FT == facetype - if isempty(attributes) && GeometryBasics.pointtype(mesh) == pointtype + if isempty(va) && GeometryBasics.pointtype(mesh) == pointtype return mesh else # 1. add vertex attributes, 2. convert position attribute - va = merge(vertex_attributes(mesh), NamedTuple(attributes)) + va = merge(vertex_attributes(mesh), va) va = merge(va, (position = decompose(pointtype, va.position),)) return Mesh(va, faces(mesh), mesh.views) end else # 1. add vertex attributes, 2. convert position attribute - va = merge(vertex_attributes(mesh), NamedTuple(attributes)) + va = merge(vertex_attributes(mesh), va) va = merge(va, (position = decompose(pointtype, va.position),)) # update face types From 4e4ca8aeb236fc9d191cef190f158ebe540fef5d Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 7 Sep 2024 01:34:48 +0200 Subject: [PATCH 035/120] drop Base.Pairs for 1.6 compat --- src/meshes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/meshes.jl b/src/meshes.jl index cc69cd3d..a1b952b3 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -30,7 +30,7 @@ function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleF return mesh(positions, collect(fs); facetype = facetype, vertex_attributes...) end -function drop_nothing_kwargs(kwargs::Base.Pairs) +function drop_nothing_kwargs(kwargs) _keys = filter(k -> !isnothing(kwargs[k]), keys(kwargs)) _vals = ntuple(n -> kwargs[_keys[n]], length(_keys)) return NamedTuple{_keys}(_vals) From e7f12c1ed09557bb805041f4957b2cf8d3e59d04 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 9 Sep 2024 14:14:28 +0200 Subject: [PATCH 036/120] Add depwarn in hasproperty too --- src/basic_types.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/basic_types.jl b/src/basic_types.jl index 24ae3413..5f0a1c04 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -461,6 +461,10 @@ end # const SimpleGLMesh{D, T} = Mesh{D, T, GLTriangleFace, (:position,)} @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 hasfield(mesh, :normal) + end return hasproperty(getfield(mesh, :vertex_attributes), field) || hasfield(Mesh, field) end @inline function Base.getproperty(mesh::Mesh, field::Symbol) From 055d708d4258dcffb55c2ba942004eea21f95f21 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 10 Sep 2024 14:36:59 +0200 Subject: [PATCH 037/120] improve MultiFace show --- src/basic_types.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/basic_types.jl b/src/basic_types.jl index 5f0a1c04..11f4ff49 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -135,6 +135,12 @@ function Base.convert(::Type{<: MultiFace{N, T, FT, Names}}, t::Tuple) where {N, return MultiFace{Names}(FT.(t)) end +Base.show(io::IO, f::MultiFace) = show(io, getfield(f, :faces)) +function Base.show(io::IO, ::MIME"text/plain", f::MultiFace) + print(io, "MultiFace") + show(io, getfield(f, :faces)) +end + @propagate_inbounds Base.getindex(x::Polytope, i::Integer) = coordinates(x)[i] @propagate_inbounds Base.iterate(x::Polytope) = iterate(coordinates(x)) @propagate_inbounds Base.iterate(x::Polytope, i) = iterate(coordinates(x), i) From ad03f93ef900ff67412d270a9626c87e820cf464 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 10 Sep 2024 14:37:13 +0200 Subject: [PATCH 038/120] update Pyramid --- src/interfaces.jl | 4 ++++ src/meshes.jl | 2 +- src/primitives/pyramids.jl | 29 ++++++++++++++++++++--------- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/interfaces.jl b/src/interfaces.jl index 0b8eb42f..080491d5 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -180,6 +180,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/meshes.jl b/src/meshes.jl index a1b952b3..94e90539 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -401,7 +401,7 @@ end # merge_vertex_(attribute)_indices # convert(Face, MultiFace) # ... -function merge_vertex_indices(mesh) +function merge_vertex_indices(mesh::AbstractMesh) attribs, fs, views = merge_vertex_indices( vertex_attributes(mesh), faces(mesh), mesh.views) diff --git a/src/primitives/pyramids.jl b/src/primitives/pyramids.jl index de2851fb..67d6046b 100644 --- a/src/primitives/pyramids.jl +++ b/src/primitives/pyramids.jl @@ -4,6 +4,11 @@ struct Pyramid{T} <: GeometryPrimitive{3,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 +17,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 + return normalize.(Vec3f[(h, 0, w), (0, h, w), (-h, 0, w), (0, -h, w), (0, 0, -1)]) end function faces(::Pyramid) - return (TriangleFace(triangle) for triangle in TupleView{3}(1:18)) + # return (TriangleFace(triangle) for triangle in TupleView{3}(1:18)) + return NormalFace{3, Int, TriangleFace{Int}}[ + ((1, 2, 3), (1)), + ((1, 3, 4), (2)), + ((1, 4, 5), (3)), + ((1, 5, 2), (4)), + ((2, 3, 4), (5)), + ((4, 5, 2), (5)) + ] end From 36208423c02bc26ab668595c7b4ae79b193f9eb4 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 10 Sep 2024 15:37:49 +0200 Subject: [PATCH 039/120] update Cylinder --- src/primitives/cylinders.jl | 155 +++++++++++++++++++----------------- 1 file changed, 81 insertions(+), 74 deletions(-) diff --git a/src/primitives/cylinders.jl b/src/primitives/cylinders.jl index adba8b8b..33dd4ed9 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,80 @@ 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) + h = height(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) +function normals(c::Cylinder, nvertices = 30) + nvertices += isodd(nvertices) + nhalf = div(nvertices, 2) + + 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 - nvertices = max(8, nvertices) - nbv = nvertices ÷ 2 + ns[end-1] = R * Vec3f(0, 0, -1) + ns[end] = R * Vec3f(0, 0, 1) + + return ns +end - 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 +function faces(c::Cylinder, facets=30) + nvertices = facets + isodd(facets) + nhalf = div(nvertices, 2) + + disk1 = map(1:nhalf) do i + NormalFace( + GLTriangleFace(nvertices+1, i, mod1(i+1, nhalf)), + GLTriangleFace(nhalf+1) + ) end - return (inner(i) for i in range) -end + mantle1 = map(1:nhalf) do i + i1 = mod1(i+1, nhalf) + NormalFace( + GLTriangleFace(i, i+nhalf, i1 + nhalf), + GLTriangleFace(i, i, i1) + ) + 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 + mantle2 = map(1:nhalf) do i + i1 = mod1(i+1, nhalf) + NormalFace( + GLTriangleFace(i1 + nhalf, i1, i), + GLTriangleFace(i1, i1, i) + ) end - indexes[index] = (1, index + 1, index) - indexes[index + 1] = (2, index + 1, 1) - 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)) + disk2 = map(1:nhalf) do i + NormalFace( + GLTriangleFace(nvertices+2, i+nhalf, mod1(i+1, nhalf)+nhalf), + GLTriangleFace(nhalf+2) + ) end - return indexes + + return vcat(disk1, mantle1, mantle2, disk2) end From 86629b9e054f8dd8837ae695898092ac0a97a7f9 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 10 Sep 2024 16:33:24 +0200 Subject: [PATCH 040/120] add MultiFace decompose --- src/basic_types.jl | 2 +- src/interfaces.jl | 13 +++++++++++++ src/meshes.jl | 33 ++++++++++++++++++++++++++++++++- src/primitives/cylinders.jl | 19 +++++-------------- src/primitives/pyramids.jl | 14 ++++++-------- 5 files changed, 57 insertions(+), 24 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 11f4ff49..31d73bdb 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -89,7 +89,7 @@ MultiFace{Names}(args...) where {Names} = MultiFace{Names}(args) function MultiFace{Names}(args::NTuple{M, NTuple{N, <: Integer}}) where {Names, N, M} return MultiFace(NamedTuple{Names}(NgonFace.(args))) end -function MultiFace{Names}(args::Tuple{Vararg{<: AbstractVertexFace}}) where {Names} +function MultiFace{Names}(args::NTuple{M, FT}) where {Names, M, FT <: AbstractVertexFace} return MultiFace(NamedTuple{Names}(args)) end diff --git a/src/interfaces.jl b/src/interfaces.jl index 080491d5..3a4df863 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -131,6 +131,19 @@ function decompose(::Type{F}, f::AbstractVector, views::Vector{UnitRange{Int}}) end end +function decompose( + ::Type{FT}, + faces::AbstractVector{<: MultiFace{_N, _T, _FT, Names} where {_N, _T, _FT}} + ) where {Names, FT <: AbstractVertexFace} + + groups = map(Names) do name + single_attrib_faces = getproperty.(faces, name) + return decompose(FT, single_attrib_faces) + end + + return MultiFace{Names}.(groups...) +end + function decompose(::Type{P}, primitive) where {P<:Point} return collect_with_eltype(P, coordinates(primitive)) end diff --git a/src/meshes.jl b/src/meshes.jl index 94e90539..a102f958 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -417,11 +417,30 @@ function merge_vertex_indices( return attribs, faces, views end +function merge_vertex_indices( + attribs::NamedTuple, + faces::AbstractVector{<: MultiFace}, + views::Vector{UnitRange{Int}}, + vertex_index_counter = nothing, + looped = false + ) + + if !looped + return merge_vertex_indices( + attribs, decompose(GLTriangleFace, faces), views, + something(vertex_index_counter, GLIndex(1)), true + ) + else + throw(MethodError(merge_vertex_indices, (attribs, faces, views, vertex_index_counter))) + end +end + function merge_vertex_indices( attribs::NamedTuple{Names}, faces::AbstractVector{<: MultiFace{N, T, FT, Names}}, views::Vector{UnitRange{Int}}, - vertex_index_counter = T(1) # TODO: test 0 vs 1 base + vertex_index_counter = T(1), # TODO: test 0 vs 1 base + loop = false ) where {Names, N, T, FT} # Note: typing checks for matching Names @@ -463,6 +482,18 @@ function merge_vertex_indices(faces::AbstractVector{<: AbstractVertexFace}, i = return faces, i : N_vert - 1 + i end +function merge_vertex_indices( + faces::AbstractVector{<: MultiFace{N, T, FT} where {N, T, FT}}, + vertex_index_counter = nothing + ) + + return merge_vertex_indices( + decompose(GLTriangleFace, faces), + something(vertex_index_counter, GLIndex(1)) + ) +end + + function merge_vertex_indices( faces::AbstractVector{<: MultiFace{N, T, FT, Names, N_Attrib}}, vertex_index_counter = T(1) diff --git a/src/primitives/cylinders.jl b/src/primitives/cylinders.jl index 33dd4ed9..9865898f 100644 --- a/src/primitives/cylinders.jl +++ b/src/primitives/cylinders.jl @@ -40,7 +40,6 @@ function coordinates(c::Cylinder{T}, nvertices=30) where {T} nhalf = div(nvertices, 2) R = rotation(c) - h = height(c) step = 2pi / nhalf ps = Vector{Point3{T}}(undef, nvertices + 2) @@ -72,7 +71,7 @@ function normals(c::Cylinder, nvertices = 30) end ns[end-1] = R * Vec3f(0, 0, -1) ns[end] = R * Vec3f(0, 0, 1) - + return ns end @@ -87,19 +86,11 @@ function faces(c::Cylinder, facets=30) ) end - mantle1 = map(1:nhalf) do i - i1 = mod1(i+1, nhalf) - NormalFace( - GLTriangleFace(i, i+nhalf, i1 + nhalf), - GLTriangleFace(i, i, i1) - ) - end - - mantle2 = map(1:nhalf) do i + mantle = map(1:nhalf) do i i1 = mod1(i+1, nhalf) NormalFace( - GLTriangleFace(i1 + nhalf, i1, i), - GLTriangleFace(i1, i1, i) + QuadFace(i, i+nhalf, i1 + nhalf, i1), + QuadFace(i, i, i1, i1) ) end @@ -110,5 +101,5 @@ function faces(c::Cylinder, facets=30) ) end - return vcat(disk1, mantle1, mantle2, disk2) + return vcat(disk1, mantle, disk2) end diff --git a/src/primitives/pyramids.jl b/src/primitives/pyramids.jl index 67d6046b..889e5f04 100644 --- a/src/primitives/pyramids.jl +++ b/src/primitives/pyramids.jl @@ -26,13 +26,11 @@ function normals(p::Pyramid) end function faces(::Pyramid) - # return (TriangleFace(triangle) for triangle in TupleView{3}(1:18)) - return NormalFace{3, Int, TriangleFace{Int}}[ - ((1, 2, 3), (1)), - ((1, 3, 4), (2)), - ((1, 4, 5), (3)), - ((1, 5, 2), (4)), - ((2, 3, 4), (5)), - ((4, 5, 2), (5)) + return [ + NormalFace(GLTriangleFace(1, 2, 3), GLTriangleFace(1)), + NormalFace(GLTriangleFace(1, 3, 4), GLTriangleFace(2)), + NormalFace(GLTriangleFace(1, 4, 5), GLTriangleFace(3)), + NormalFace(GLTriangleFace(1, 5, 2), GLTriangleFace(4)), + NormalFace(QuadFace(2, 3, 4, 5), QuadFace(5)) ] end From 334d897ac405d9f6de678d35d9511224996476f3 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 10 Sep 2024 17:12:14 +0200 Subject: [PATCH 041/120] fix Cylinder tests --- src/GeometryBasics.jl | 2 +- src/basic_types.jl | 2 + src/interfaces.jl | 2 +- test/geometrytypes.jl | 96 +++++++++++++++++++------------------------ 4 files changed, 47 insertions(+), 55 deletions(-) diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index 7eddfb52..8952af30 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -55,7 +55,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 diff --git a/src/basic_types.jl b/src/basic_types.jl index 31d73bdb..604f9f7d 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -141,6 +141,8 @@ function Base.show(io::IO, ::MIME"text/plain", f::MultiFace) show(io, getfield(f, :faces)) end +Base.:(==)(f1::MultiFace, f2::MultiFace) = getfield(f1, :faces) == getfield(f2, :faces) + @propagate_inbounds Base.getindex(x::Polytope, i::Integer) = coordinates(x)[i] @propagate_inbounds Base.iterate(x::Polytope) = iterate(coordinates(x)) @propagate_inbounds Base.iterate(x::Polytope, i) = iterate(coordinates(x), i) diff --git a/src/interfaces.jl b/src/interfaces.jl index 3a4df863..f9f96cd4 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -104,7 +104,7 @@ function decompose(::Type{F}, primitive::AbstractGeometry) where {F<:AbstractVer return nothing end end - return collect_with_eltype(F, f) + return decompose(F, f) end function decompose(::Type{F}, f::AbstractVector) where {F<:AbstractVertexFace} diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index f9eb5f18..6ad15f97 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,57 +16,60 @@ 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 = [ + MultiFace(position = TriangleFace(9, 1, 2), normal = TriangleFace(5, 5, 5)), + MultiFace(position = TriangleFace(9, 2, 3), normal = TriangleFace(5, 5, 5)), + MultiFace(position = TriangleFace(9, 3, 4), normal = TriangleFace(5, 5, 5)), + MultiFace(position = TriangleFace(9, 4, 1), normal = TriangleFace(5, 5, 5)), + MultiFace(position = TriangleFace(1, 5, 6), normal = TriangleFace(1, 1, 2)), + MultiFace(position = TriangleFace(1, 6, 2), normal = TriangleFace(1, 2, 2)), + MultiFace(position = TriangleFace(2, 6, 7), normal = TriangleFace(2, 2, 3)), + MultiFace(position = TriangleFace(2, 7, 3), normal = TriangleFace(2, 3, 3)), + MultiFace(position = TriangleFace(3, 7, 8), normal = TriangleFace(3, 3, 4)), + MultiFace(position = TriangleFace(3, 8, 4), normal = TriangleFace(3, 4, 4)), + MultiFace(position = TriangleFace(4, 8, 5), normal = TriangleFace(4, 4, 1)), + MultiFace(position = TriangleFace(4, 5, 1), normal = TriangleFace(4, 1, 1)), + MultiFace(position = TriangleFace(10, 5, 6), normal = TriangleFace(6, 6, 6)), + MultiFace(position = TriangleFace(10, 6, 7), normal = TriangleFace(6, 6, 6)), + MultiFace(position = TriangleFace(10, 7, 8), normal = TriangleFace(6, 6, 6)), + MultiFace(position = TriangleFace(10, 8, 5), normal = TriangleFace(6, 6, 6)), + ] + + @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.coordinates(m) ≈ positions - m = normal_mesh(s)# just test that it works without explicit resolution parameter + fs, maps = GeometryBasics.merge_vertex_indices(GeometryBasics.simplify_faces((:position,), _faces)) + @test GeometryBasics.faces(m) == decompose(GLTriangleFace, fs) + @test GeometryBasics.coordinates(m) ≈ positions[maps[1]] + + m = normal_mesh(s) # just test that it works without explicit resolution parameter + @test hasproperty(m, :position) + @test hasproperty(m, :normal) + @test length(m.position) == length(m.normal) + @test faces(m) isa AbstractVector{GLTriangleFace} muv = uv_mesh(s) - @test Rect(Point.(texturecoordinates(muv))) == Rect2f(Vec2f(0), Vec2f(1.0)) + @test !hasproperty(muv, :uv) # not defined yet end end From 6944269f650f115310b044ee8d93d18f4f377c9f Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 10 Sep 2024 17:29:37 +0200 Subject: [PATCH 042/120] make OffsetInteger printing copyable --- src/offsetintegers.jl | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/offsetintegers.jl b/src/offsetintegers.jl index 40aef53e..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 From 883b3e1b4d7a708e090130e14f54a2f035e552f2 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 10 Sep 2024 17:30:18 +0200 Subject: [PATCH 043/120] update tests for Rect3 and normal gen --- test/geometrytypes.jl | 40 +++++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 6ad15f97..5a1d2e39 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -81,13 +81,24 @@ end @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]] - @test_broken decompose(Point{3,Int}, b) == pt_expb + 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, 3), (1, 3, 4), (5, 6, 7), (5, 7, 8), (9, 10, 11), (9, 11, 12), + (13, 14, 15), (13, 15, 16), (17, 18, 19), (17, 19, 20), (21, 22, 23), (21, 23, 24) + ] + @test normals(mesh) == [n for n in normals(b) for _ in 1:4] + @test coordinates(mesh) == Point{3, Float32}[ + [1.0, 1.0, 1.0], [1.0, 1.0, 2.0], [1.0, 2.0, 2.0], [1.0, 2.0, 1.0], + [2.0, 2.0, 1.0], [2.0, 2.0, 2.0], [2.0, 1.0, 2.0], [2.0, 1.0, 1.0], + [2.0, 1.0, 1.0], [2.0, 1.0, 2.0], [1.0, 1.0, 2.0], [1.0, 1.0, 1.0], + [1.0, 2.0, 1.0], [1.0, 2.0, 2.0], [2.0, 2.0, 2.0], [2.0, 2.0, 1.0], + [1.0, 1.0, 1.0], [1.0, 2.0, 1.0], [2.0, 2.0, 1.0], [2.0, 1.0, 1.0], + [2.0, 1.0, 2.0], [2.0, 2.0, 2.0], [1.0, 2.0, 2.0], [1.0, 1.0, 2.0]] @test isempty(Rect{3,Float32}()) end @@ -117,18 +128,13 @@ 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),] + r = centered(Rect3{Float64}) + n64 = [n for n in normals(r) for _ in 1:4] 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 + m32 = normal_mesh(r) + m64 = normal_mesh(r, pointtype = Point3d) + @test normals(coordinates(m32), GeometryBasics.faces(m32)) == n32 + @test normals(coordinates(m64), GeometryBasics.faces(m64)) == n64 end @testset "HyperSphere" begin From 7474624ee621ade44b3acc08ff7a7428b56b20de Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 10 Sep 2024 17:43:44 +0200 Subject: [PATCH 044/120] fix remaining tests --- src/meshes.jl | 5 +++-- src/primitives/rectangles.jl | 2 +- test/runtests.jl | 6 +++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/meshes.jl b/src/meshes.jl index a102f958..a234b2ff 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -412,14 +412,15 @@ function merge_vertex_indices( attribs::NamedTuple, faces::AbstractVector{<: AbstractVertexFace}, views::Vector{UnitRange{Int}}, - vertex_index_counter = nothing + vertex_index_counter = nothing, + loop = false ) return attribs, faces, views end function merge_vertex_indices( attribs::NamedTuple, - faces::AbstractVector{<: MultiFace}, + faces::AbstractVector{<: MultiFace{N, T, FT} where {N, T, FT}}, views::Vector{UnitRange{Int}}, vertex_index_counter = nothing, looped = false diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index b54c0039..5089d33e 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 diff --git a/test/runtests.jl b/test/runtests.jl index ce19d6e6..81b63bc1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,6 +6,8 @@ using GeoInterface using GeoJSON using Extents +using GeometryBasics: MultiFace + @testset "GeometryBasics" begin @testset "algorithms" begin @test_broken false @@ -193,7 +195,9 @@ end 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) From e5abc382cf79ac9a02f5690551b6e99f5a2a3d4a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 10 Sep 2024 18:32:17 +0200 Subject: [PATCH 045/120] fix incorrect vertex index counter --- src/meshes.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/meshes.jl b/src/meshes.jl index a234b2ff..588f1342 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -462,7 +462,7 @@ function merge_vertex_indices( # corresponding to the first vertex attribute added in this iteration face_view = view(faces, idxs) new_faces_in_view, vertex_map = merge_vertex_indices(face_view, vertex_index_counter) - vertex_index_counter += length(vertex_map) + vertex_index_counter += length(vertex_map[1]) # update vertex attributes for (name, indices) in pairs(vertex_map) @@ -472,7 +472,7 @@ function merge_vertex_indices( # add new faces and new view start = length(new_faces) + 1 append!(new_faces, new_faces_in_view) - append!(new_views, start:length(new_faces)) + push!(new_views, start:length(new_faces)) end return new_attribs, new_faces, new_views From 4f0bd4970c710c184103135e45f7aca44b777ce4 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 10 Sep 2024 18:38:08 +0200 Subject: [PATCH 046/120] simplify merge of mixed Face types --- src/meshes.jl | 66 +++++++-------------------------------------------- 1 file changed, 8 insertions(+), 58 deletions(-) diff --git a/src/meshes.jl b/src/meshes.jl index 588f1342..623abf3f 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -278,10 +278,11 @@ end function Base.merge(meshes::AbstractVector{<:Mesh}) return if isempty(meshes) return Mesh(Point3f[], GLTriangleFace[]) + elseif length(meshes) == 1 return meshes[1] - else + else m1 = meshes[1] # Check that all meshes use the same VertexAttributes @@ -338,69 +339,18 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) return Mesh(new_attribs, fs, views) - else - - # TODO: We can probably simplify this to `merge(merge_vertex_indices.(meshes))` - # but need to check performance - - - # Varying ace types, need to convert MultiFace - new_attribs = NamedTuple{names}(similar.(values(vertex_attributes(m1)), 0)) - FT = facetype(m1) <: MultiFace ? eltype(facetype(m1)) : facetype(m1) - remapped_faces = [] - new_views = UnitRange{Int}[] - vertex_index_counter = eltype(FT)(1) - - for mesh in meshes - # convert MultiFace mesh to vertex faces, synchronizing vertex indices - attribs, fs, views = merge_vertex_indices( - vertex_attributes(mesh), faces(mesh), mesh.views, vertex_index_counter) - - # increment first vertex index used by faces of the next iteration - vertex_index_counter += length(attribs[1]) - - # update merged data - for name in names - append!(new_attribs[name], attribs[name]) - end - - push!(remapped_faces, fs) - - if isempty(views) - push!(new_views, 1:length(fs)) - else - append!(new_views, views) - end - end - - # We did MultiFace -> vertex face, now equalize vertex face types - new_faces = reduce(vcat, remapped_faces) + else # mixed MultiFace and VertexFace + + # simplify to VertexFace types, then retry merge + return merge(merge_vertex_indices.(meshes)) - return Mesh(new_attribs, new_faces, new_views) end end end -# TODO: Probably not our problem -# 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 - -# TODO: naming -# synchronize_vertex_attributes -# merge_vertex_(attribute)_indices -# convert(Face, MultiFace) -# ... + + function merge_vertex_indices(mesh::AbstractMesh) attribs, fs, views = merge_vertex_indices( vertex_attributes(mesh), faces(mesh), mesh.views) From 5da6c027da7ced282f0c76a86ecfa24e9be03a26 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Tue, 10 Sep 2024 19:37:51 +0200 Subject: [PATCH 047/120] test merge(meshes) --- test/meshes.jl | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/meshes.jl b/test/meshes.jl index 8c6a4b16..5c19e889 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -55,6 +55,44 @@ end @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] + multiface_meshes = map(rects) do r + GeometryBasics.Mesh(coordinates(r), faces(r), normal = normals(r)) + end + mfm = merge(multiface_meshes) + + @test GeometryBasics.facetype(mfm) == GeometryBasics.NormalFace{4, Int64, QuadFace{Int64}} + @test length(faces(mfm)) == 27 * 6 # 27 rects, 6 quad faces + @test length(normals(mfm)) == 27 * 6 + @test length(coordinates(mfm)) == 27 * 8 + @test !allunique([idx for f in faces(mfm) for idx in f.position]) + @test !allunique([idx for f in faces(mfm) for idx in f.normal]) + + vertexface_meshes = map(rects) do r + GeometryBasics.mesh(coordinates(r), faces(r), normal = normals(r), facetype = QuadFace{Int64}) + end + vfm = merge(vertexface_meshes) + + @test GeometryBasics.facetype(vfm) == QuadFace{Int64} + @test length(faces(vfm)) == 27 * 6 # 27 rects, 6 quad faces each + @test length(normals(vfm)) == 27 * 6 * 4 # 27 rects, 6 faces, 4 normals each + @test length(coordinates(vfm)) == 27 * 8 * 3 # 27 rects, 8 points, 3 duplications (for 3 attached quad faces) + @test allunique([idx for f in faces(vfm) for idx in f]) + + mixed_meshes = map(multiface_meshes, vertexface_meshes) do mfm, vfm + rand() > 0.5 ? mfm : vfm + end + mm = merge(mixed_meshes) + + @test GeometryBasics.facetype(mm) == QuadFace{Int64} + @test length(faces(mm)) == 27 * 6 # 27 rects, 6 quad faces each + @test length(normals(mm)) == 27 * 6 * 4 # 27 rects, 6 faces, 4 normals each + @test length(coordinates(mm)) == 27 * 8 * 3 # 27 rects, 8 points, 3 duplications (for 3 attached quad faces) + @test allunique([idx for f in faces(mm) for idx in f]) + @test mm == vfm +end + @testset "mesh() constructors" begin r = Rect3d(Point3d(-1), Vec3d(2)) From 90d4794b5de534e64c5c75c6bf7ab8305d84dba5 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 11 Sep 2024 00:19:59 +0200 Subject: [PATCH 048/120] prototype swapping from MultiFace to FaceViews --- src/basic_types.jl | 202 ++++++++++---------------------- src/geometry_primitives.jl | 10 +- src/interfaces.jl | 13 --- src/meshes.jl | 220 ++++++++++++++++------------------- src/primitives/cylinders.jl | 3 + src/primitives/pyramids.jl | 15 +-- src/primitives/rectangles.jl | 19 +-- 7 files changed, 184 insertions(+), 298 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 604f9f7d..f0abc41e 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -19,13 +19,7 @@ abstract type AbstractPolygon{Dim,T} <: Polytope{Dim,T} end Parent type for all faces. You should inherit from one of the child types instead. """ abstract type AbstractFace{N,T} <: StaticVector{N,T} end -""" - AbstractMultiFace{N, T, M} <: AbstractFace{N, T} -Parent type for faces addressing N vertices with M different vertex attribute -indices. -""" -abstract type AbstractMultiFace{N, T, M} <: AbstractFace{N, T} end """ AbstractVertexFace{N, T} <: AbstractFace{N, T} @@ -68,81 +62,6 @@ end Face(::Type{<:NgonFace{N}}, ::Type{T}) where {N,T} = NgonFace{N,T} Face(F::Type{NgonFace{N,FT}}, ::Type{T}) where {FT,N,T} = F -struct MultiFace{N, T, FaceType <: AbstractVertexFace{N, T}, Names, M} <: AbstractMultiFace{N, T, M} - faces::NamedTuple{Names, NTuple{M, FaceType}} -end - -# TODO: Split these up? -""" - MultiFace(; kwargs...) - MultiFace{Names}(faces...) - MultiFace{Names}(faces::Tuple) - MultiFace{Names}(multiface::MultiFace) - MultiFace{Names, FaceType}(faces::Tuple) - -Constructs a `MultiFace` from a tuple of names `Names::NTuple{M, Symbol}` and -`faces::NTuple{M, FaceType}` similar to how a NamedTuple would. -""" -MultiFace(; kwargs...) = MultiFace(NamedTuple(kwargs)) -MultiFace{Names}(f::MultiFace) where {Names} = MultiFace{Names}(getproperty.((f,), Names)) -MultiFace{Names}(args...) where {Names} = MultiFace{Names}(args) -function MultiFace{Names}(args::NTuple{M, NTuple{N, <: Integer}}) where {Names, N, M} - return MultiFace(NamedTuple{Names}(NgonFace.(args))) -end -function MultiFace{Names}(args::NTuple{M, FT}) where {Names, M, FT <: AbstractVertexFace} - return MultiFace(NamedTuple{Names}(args)) -end - -function MultiFace{N, T, FT, Names, M}(faces...) where {N, T, FT <: AbstractVertexFace{N, T}, Names, M} - @assert length(faces) === M - @assert length(Names) === M - return MultiFace{Names}(FT.(face)) -end - -# As you get from shorthands without defining N, T, FT -const UnspecializedMultiFace{Names, M} = MultiFace{N, T, FT, Names, M} where {N, T, FT <: AbstractVertexFace{N, T}} -function UnspecializedMultiFace{Names, M}(faces...) where {Names, M} - @assert length(faces) === M - @assert length(Names) === M - return MultiFace{Names}(faces) -end - -Base.getindex(f::MultiFace, i::Integer) = Base.getindex(getfield(f, :faces), i) -@inline Base.hasproperty(f::MultiFace, field::Symbol) = hasproperty(getfield(f, :faces), field) -@inline Base.getproperty(f::MultiFace, field::Symbol) = getproperty(getfield(f, :faces), field) -@inline Base.propertynames(f::MultiFace) = propertynames(getfield(f, :faces)) -@inline Base.propertynames(::Type{<: MultiFace{N, T, FT, Names}}) where {N, T, FT, Names} = Names -Base.eltype(::MultiFace{N, T, FT}) where {N, T, FT} = FT -Base.eltype(::Type{<: MultiFace{N, T, FT}}) where {N, T, FT} = FT - -# TODO: can we do this with a conversion? E.g. MultiFace{Names}.(faces)? -function simplify_faces(::Type{MF1}, fs::AbstractVector{MF2}) where {MF1 <: MultiFace, MF2 <: MultiFace} - return simplify_faces(propertynames(MF1), fs) -end - -function simplify_faces(names::NTuple{N, Symbol}, fs::AbstractVector{MF2}) where {N, MF2 <: MultiFace} - return map(f -> MultiFace{names}(f), fs) -end - -# Shorthands for constructing faces -const UVFace{N, T, FT <: AbstractVertexFace{N, T}} = MultiFace{N, T, FT, (:position, :uv), 2} -const NormalFace{N, T, FT <: AbstractVertexFace{N, T}} = MultiFace{N, T, FT, (:position, :normal), 2} -const NormalUVFace{N, T, FT <: AbstractVertexFace{N, T}} = MultiFace{N, T, FT, (:position, :normal, :uv), 3} -const UVNormalFace{N, T, FT <: AbstractVertexFace{N, T}} = MultiFace{N, T, FT, (:position, :uv, :normal), 3} - -# enable something like NormalUVFace{GLTriangleFace}[(pos_face, normal_face, uv_face), ...] -function Base.convert(::Type{<: MultiFace{N, T, FT, Names}}, t::Tuple) where {N, T, FT, Names} - return MultiFace{Names}(FT.(t)) -end - -Base.show(io::IO, f::MultiFace) = show(io, getfield(f, :faces)) -function Base.show(io::IO, ::MIME"text/plain", f::MultiFace) - print(io, "MultiFace") - show(io, getfield(f, :faces)) -end - -Base.:(==)(f1::MultiFace, f2::MultiFace) = getfield(f1, :faces) == getfield(f2, :faces) - @propagate_inbounds Base.getindex(x::Polytope, i::Integer) = coordinates(x)[i] @propagate_inbounds Base.iterate(x::Polytope) = iterate(coordinates(x)) @propagate_inbounds Base.iterate(x::Polytope, i) = iterate(coordinates(x), i) @@ -377,33 +296,52 @@ coordinates(mesh). Arbitrary meta information can be attached per point or per f """ abstract type AbstractMesh{Dim, T} <: AbstractGeometry{Dim, T} end +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 -> f .+ N, b.faces)) + ) +end + """ Mesh <: AbstractMesh{Element} The concrete AbstractMesh type. """ struct Mesh{ Dim, T <: Real, # TODO: Number? - FaceType <: AbstractFace, + FT <: AbstractFace, Names, - VertexAttribTypes <: Tuple{AbstractVector{Point{Dim, T}}, Vararg{AbstractVector}}, - FaceVecType <: AbstractVector{FaceType} + VAT <: Tuple{AbstractVector{Point{Dim, T}}, Vararg{VertexAttributeType}}, + FVT <: AbstractVector{FT} } <: AbstractMesh{Dim, T} - vertex_attributes::NamedTuple{Names, VertexAttribTypes} - connectivity::FaceVecType + vertex_attributes::NamedTuple{Names, VAT} + connectivity::FVT views::Vector{UnitRange{Int}} function Mesh( va::NamedTuple{Names, VAT}, - f::FVT, + faces::FVT, views::Vector{UnitRange{Int}} = UnitRange{Int}[] ) where { - D, T, FT <: AbstractMultiFace, Names, - VAT <: Tuple{AbstractVector{Point{D, T}}, Vararg{AbstractVector}}, + D, T, FT <: AbstractFace, Names, + VAT <: Tuple{AbstractVector{Point{D, T}}, Vararg{VertexAttributeType}}, FVT <: AbstractVector{FT} } - # verify type / naming rules & consistency + # verify type if first(Names) !== :position error("The first vertex attribute should be a 'position' but is a '$(first(Names))'.") end @@ -412,55 +350,35 @@ struct Mesh{ @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 = map(name -> ifelse(name === :normals, :normal, name), Names) va = NamedTuple{names}(values(va)) - else - names = Names end - f_names = propertynames(FT) - if names == f_names - # all good - elseif issubset(names, f_names) - # Remove the extra names in faces/fix order - # Note: This might be redundant with `mesh()`. It is supposed to allow - # using a general `MultiFace` in `faces(primitive)` which then - # gets reduced to the vertex attributes used in a general mesh. - f = simplify_faces(names, f) - else - error( - "Cannot construct a mesh with vertex attribute names $names and MultiFace " * - "attribute names $f_names. These must include the same names in the same order." - ) + if va.position isa FaceView + if faces != va.position.faces + error("position faces do not match gives faces") + end + va = NamedTuple(map(pairs(va)) do (key, val) + return key => (key == :position ? val.data : val) + end) end - return new{D, T, eltype(typeof(f)), names, VAT, typeof(f)}(va, f, views) - end - - function Mesh( - va::NamedTuple{Names, VAT}, - f::FVT, - views::Vector{UnitRange{Int}} = UnitRange{Int}[] - ) where { - D, T, FT <: AbstractVertexFace, Names, - VAT <: Tuple{AbstractVector{Point{D, T}}, Vararg{AbstractVector}}, - FVT <: AbstractVector{FT} - } - - # verify type - if first(Names) !== :position - error("The first vertex attribute should be a 'position' but is a '$(first(Names))'.") + # verify matching faces between face views + for (name, attrib) in pairs(va) + name == :position && continue + + if attrib isa FaceView + if length(attrib.faces) != length(faces) + error("Number of faces defined for $name $(length(attrib.faces)) does not match position $(length(faces))") + end + + for (i, (f1, f2)) in enumerate(zip(attrib.faces, faces)) + if length(f1) != length(f2) + error("Length of face $name[$i] = $(length(f1)) does not match position[$i] = $(length(f2))") + end + end + end end - if :normals in Names - @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 = map(name -> ifelse(name === :normals, :normal, name), Names) - va = NamedTuple{names}(values(va)) - end - - # Note: With VertexFaces all vertex attributes should have the same - # length as they use a common vertex index. We could check this - # here but maybe it's better not to to prevent over-eager checking? - - return new{D, T, FT, keys(va), VAT, FVT}(va, f, views) + return new{D, T, FT, keys(va), VAT, FVT}(va, faces, views) end end @@ -499,12 +417,12 @@ Base.getindex(mesh::Mesh, i::Integer) = mesh[mesh.connectivity[i]] Base.length(mesh::Mesh) = length(mesh.connectivity) # TODO: temp -function Base.getindex(mesh::Mesh{D, T, <: AbstractVertexFace}, f::AbstractVertexFace) where {D, T} - return getfield(mesh, :vertex_attributes).position[f] -end -function Base.getindex(::Mesh, f::AbstractMultiFace) - error("TODO") -end +# function Base.getindex(mesh::Mesh{D, T, <: AbstractVertexFace}, f::AbstractVertexFace) where {D, T} +# return getfield(mesh, :vertex_attributes).position[f] +# end +# function Base.getindex(::Mesh, f::AbstractMultiFace) +# error("TODO") +# end function Base.:(==)(a::Mesh, b::Mesh) return (a.vertex_attributes == b.vertex_attributes) && @@ -515,9 +433,9 @@ function Base.iterate(mesh::Mesh, i=1) return i - 1 < length(mesh) ? (mesh[i], i + 1) : nothing end -function Mesh(faces::AbstractVector{<:AbstractFace}; views = UnitRange{Int}[], attributes...) - return Mesh(NamedTuple(attributes), faces, views) -end +# function Mesh(faces::AbstractVector{<:AbstractFace}; views = UnitRange{Int}[], attributes...) +# return Mesh(NamedTuple(attributes), faces, views) +# end function Mesh(points::AbstractVector{Point{Dim, T}}, faces::AbstractVector{<:AbstractFace}; diff --git a/src/geometry_primitives.jl b/src/geometry_primitives.jl index 318136ab..263db737 100644 --- a/src/geometry_primitives.jl +++ b/src/geometry_primitives.jl @@ -56,11 +56,6 @@ Extract all line segments in a Face. return v end -# This assumes that line faces don't care about normals, uvs, etc -function convert_simplex(::Type{LF}, f::MultiFace) where {LF <: LineFace} - return convert_simplex(LF, f.position) -end - to_pointn(::Type{T}, x) where {T<:Point} = convert_simplex(T, x)[1] # disambiguation method overlords @@ -78,8 +73,13 @@ 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 collect_with_eltype(::Type{T}, iter) where {T} = collect_with_eltype!(T[], iter) +function collect_with_eltype(::Type{T}, iter::FaceView) where {T} + return FaceView(collect_with_eltype!(T[], iter.data), iter.faces) +end + function collect_with_eltype!(target::AbstractVector{T}, vec::AbstractVector{T}) where {T} return append!(target, vec) end diff --git a/src/interfaces.jl b/src/interfaces.jl index f9f96cd4..ca42cdec 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -131,19 +131,6 @@ function decompose(::Type{F}, f::AbstractVector, views::Vector{UnitRange{Int}}) end end -function decompose( - ::Type{FT}, - faces::AbstractVector{<: MultiFace{_N, _T, _FT, Names} where {_N, _T, _FT}} - ) where {Names, FT <: AbstractVertexFace} - - groups = map(Names) do name - single_attrib_faces = getproperty.(faces, name) - return decompose(FT, single_attrib_faces) - end - - return MultiFace{Names}.(groups...) -end - function decompose(::Type{P}, primitive) where {P<:Point} return collect_with_eltype(P, coordinates(primitive)) end diff --git a/src/meshes.jl b/src/meshes.jl index 623abf3f..37061d1f 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -14,8 +14,14 @@ It also only losely correlates to the number of vertices, depending on the algor function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleFace, vertex_attributes...) positions = decompose(pointtype, primitive) + if positions isa FaceView + positions = positions.data + _fs = positions.faces + else + _fs = faces(primitive) + end + # If faces returns nothing for primitive, we try to triangulate! - _fs = faces(primitive) if isnothing(_fs) if eltype(positions) <: Point2 # triangulation.jl @@ -27,7 +33,7 @@ function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleF fs = _fs end - return mesh(positions, collect(fs); facetype = facetype, vertex_attributes...) + return mesh(positions, fs; facetype = facetype, vertex_attributes...) end function drop_nothing_kwargs(kwargs) @@ -48,103 +54,39 @@ reordering and duplication of positions and other vertex attributes. """ function mesh( positions::AbstractVector{<:Point}, - faces::AbstractVector{<: AbstractMultiFace}; + faces::AbstractVector{FT}; facetype=GLTriangleFace, vertex_attributes... - ) + ) where {FT <: AbstractVertexFace} + fs = decompose(facetype, faces) va = drop_nothing_kwargs(vertex_attributes) - if facetype <: AbstractMultiFace - # drop vertex attribute references in faces that facetype does not include - f = simplify_faces(facetype, faces) # TODO: call this decompose? - - elseif facetype <: AbstractVertexFace - # drop vertex attributes references in faces that are not part of the - # given vertex attributes. (This allows multi_face to be more general - # than the mesh we construct) - names = (:position, keys(va)...) - _f2 = simplify_faces(names, faces) - - # Convert MultiFace to its internally used VertexFace type and apply - # necessary vertex attribute remapping - _f, mappings = merge_vertex_indices(_f2) - positions = positions[mappings[1]] - va = NamedTuple((Pair(names[i], va[i-1][mappings[i]]) for i in 2:length(mappings))) - - # Convert to actually requested facetype - f = decompose(facetype, _f) - else - error("Cannot convert MultiFace to $facetype.") - end - - return Mesh(positions, f; va...) -end - -function mesh( - positions::AbstractVector{<:Point}, - faces::AbstractVector{<: AbstractVertexFace}; - facetype=GLTriangleFace, vertex_attributes...) - - f = decompose(facetype, faces) - va = drop_nothing_kwargs(vertex_attributes) - return Mesh(positions, f; va...) -end - -""" - mesh(mesh::Mesh{D, T}[; pointtype = Point{D, Float32}, facetype = GLTriangleFace, vertex_attributes...]) - -Converts a mesh to the given point and face types and adds the given vertex attributes. - -Note that the target `facetype` must `<: AbstractVertexFace` and that vertex -attributes are only allowed if `facetype(mesh) <: AbstractVertexFace`. -""" -function mesh( - mesh::Mesh{D, T, FT}; pointtype = Point{D, Float32}, - facetype::Type{<: AbstractVertexFace} = GLTriangleFace, - attributes... - ) where {D, T, FT <: AbstractMultiFace} - - if !isempty(attributes) - error( - "Cannot add vertex attributes to a mesh with face type $FT as the " * - "indexing of the new attributes is not clearly defined." - ) + if (facetype != FT) + va = NamedTuple(map(keys(va)) do name + attrib = va[name] + if attrib isa FaceView + return name => FaceView(attrib.data, decompose(facetype, attrib.faces)) + else + return name => attrib + end + end) end - if FT == facetype - if GeometryBasics.pointtype(mesh) == pointtype - return mesh - else - va = merge(vertex_attributes(mesh), (position = decompose(pointtype, mesh),)) - return Mesh(va, faces(mesh), mesh.views) - end - else - # update position type (doing this first is likely cheaper since - # MultiFace likely requires duplication of vertex attributes) - va = merge(vertex_attributes(mesh), (position = decompose(pointtype, mesh),)) - - # MultiFace{VertexFace} -> VertexFace - attribs, _fs, views = merge_vertex_indices(va, faces(mesh), mesh.views) - - # VertexFace -> facetype - fs, views = decompose(facetype, _fs, views) - - return Mesh(attribs, fs, views) - end + return Mesh(positions, fs; va...) end function mesh( mesh::Mesh{D, T, FT}; pointtype = Point{D, Float32}, - facetype::Type{<: AbstractVertexFace} = GLTriangleFace, + facetype::Type{<: AbstractFace} = GLTriangleFace, attributes... - ) where {D, T, FT <: AbstractVertexFace} + ) where {D, T, FT <: AbstractFace} va = drop_nothing_kwargs(attributes) - N = length(mesh.position) - if !all(attr -> length(attr) == N, values(va)) - error("At least one of the given vertex attributes does not match `length(mesh.positon) = $N`.") - end + # N = length(mesh.position) + # if !all(attr -> length(attr) == N, values(va)) + # error("At least one of the given vertex attributes does not match `length(mesh.positon) = $N`.") + # end if FT == facetype if isempty(va) && GeometryBasics.pointtype(mesh) == pointtype @@ -160,18 +102,22 @@ function mesh( va = merge(vertex_attributes(mesh), va) va = merge(va, (position = decompose(pointtype, va.position),)) - # update face types + # Resolve facetype conversions of FaceViews + va = NamedTuple(map(keys(va)) do name + attrib = va[name] + if attrib isa FaceView + return name => FaceView(attrib.data, decompose(facetype, attrib.faces)) + else + return name => attrib + end + end) + + # update main face type f, views = decompose(facetype, faces(mesh), mesh.views) return Mesh(va, f, views) end end -# catch-all else because these conversions need some oversight -function mesh(m::AbstractMesh, args...; kwargs...) - # TODO: no methoderror with kwargs? - error("No method mesh() found for the given inputs.") -end - """ mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace, normaltype=nothing) @@ -269,11 +215,6 @@ function volume(mesh::Mesh) return sum(volume, mesh) end -# TODO: Is this ok as "public" function? -# MultiFace(f1, f2, f3) + (o1, o2, o3) = MultiFace(f1 + o1, f2 + o2, f3 + o3) -function Base.:+(f::MultiFace{N, T, FT, Names, M}, o::NTuple{M, T}) where {N, T, FT, Names, M} - return MultiFace{Names}(ntuple(m -> f[m] + o[m], M)) -end function Base.merge(meshes::AbstractVector{<:Mesh}) return if isempty(meshes) @@ -297,11 +238,19 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) ) end - # We can't merge MultiFace with standard faces because MutliFace allows - # desynchronizes vertex indices that vertex faces assume synchronized. - is_multi = facetype(m1) <: MultiFace + 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 all(m -> is_multi == (facetype(m) <: MultiFace), meshes) + if consistent_face_views # All the same kind of face, can just merge @@ -312,7 +261,7 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) # TODO: is the type difference in offset bad? idx = length(faces(m1)) - offset = is_multi ? length.(values(vertex_attributes(m1))) : length(coordinates(m1)) + offset = length(coordinates(m1)) views = isempty(m1.views) ? [1:idx] : copy(m1.views) for mesh in Iterators.drop(meshes, 1) @@ -330,26 +279,27 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) end idx += N - if is_multi - offset = offset .+ length.(values(vertex_attributes(mesh))) - else - offset += length(coordinates(mesh)) - end + offset += length(coordinates(mesh)) end return Mesh(new_attribs, fs, views) else # mixed MultiFace and VertexFace - + # simplify to VertexFace types, then retry merge - return merge(merge_vertex_indices.(meshes)) + return merge(clear_face_views.(meshes)) end end end - +#= +# TODO: Is this ok as "public" function? +# MultiFace(f1, f2, f3) + (o1, o2, o3) = MultiFace(f1 + o1, f2 + o2, f3 + o3) +function Base.:+(f::MultiFace{N, T, FT, Names, M}, o::NTuple{M, T}) where {N, T, FT, Names, M} + return MultiFace{Names}(ntuple(m -> f[m] + o[m], M)) +end function merge_vertex_indices(mesh::AbstractMesh) attribs, fs, views = merge_vertex_indices( @@ -443,14 +393,51 @@ function merge_vertex_indices( something(vertex_index_counter, GLIndex(1)) ) end +=# + +function clear_face_views(mesh::Mesh) + main_fs = faces(mesh) + va = vertex_attributes(mesh) + # views = mesh.views # TODO: ignoring this for now + names = filter(name -> va[name] isa FaceView, collect(keys(va))) + other_fs = map(name -> va[name].faces, names) + + new_fs, maps = merge_vertex_indices(tuple(main_fs, other_fs...)) + + pushfirst!(names, :position) + named_maps = NamedTuple{tuple(names...)}(maps) + + new_va = NamedTuple(map(collect(keys(va))) do name + attrib = va[name] + if name === :position + return name => attrib[named_maps[name]] + elseif haskey(named_maps, name) + return name => attrib.data[named_maps[name]] + else + return name => attrib + end + end) + + return Mesh(new_va, new_fs) +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 merge_vertex_indices( - faces::AbstractVector{<: MultiFace{N, T, FT, Names, N_Attrib}}, - vertex_index_counter = T(1) - ) where {N, T, FT <: AbstractFace{N, T}, Names, N_Attrib} + 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) + 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}() @@ -465,7 +452,7 @@ function merge_vertex_indices( # query the dict twice temp = Vector{T}(undef, N) - for multi_face in faces + for multi_face in zip(faces...) for i in 1:N # get the i-th set of vertex indices from multi_face, i.e. @@ -493,7 +480,6 @@ end - function map_coordinates(f, mesh::Mesh) result = copy(mesh) map_coordinates!(f, result) @@ -516,7 +502,7 @@ function Base.show(io::IO, ::MIME"text/plain", mesh::Mesh{N, T, FT}) where {N, T println(io, "Mesh{$N, $T, $FT}") println(io, " faces: ", length(faces(mesh))) for (name, attrib) in pairs(vertex_attributes(mesh)) - println(io, " vertex $(name): ", length(attrib)) + println(io, " vertex $(name): ", attrib isa FaceView ? length(attrib.data) : length(attrib)) end end diff --git a/src/primitives/cylinders.jl b/src/primitives/cylinders.jl index 9865898f..a3701628 100644 --- a/src/primitives/cylinders.jl +++ b/src/primitives/cylinders.jl @@ -76,6 +76,8 @@ function normals(c::Cylinder, nvertices = 30) end function faces(c::Cylinder, facets=30) + return nothing + #= nvertices = facets + isodd(facets) nhalf = div(nvertices, 2) @@ -102,4 +104,5 @@ function faces(c::Cylinder, facets=30) end return vcat(disk1, mantle, disk2) + =# end diff --git a/src/primitives/pyramids.jl b/src/primitives/pyramids.jl index 889e5f04..6579349e 100644 --- a/src/primitives/pyramids.jl +++ b/src/primitives/pyramids.jl @@ -26,11 +26,12 @@ function normals(p::Pyramid) end function faces(::Pyramid) - return [ - NormalFace(GLTriangleFace(1, 2, 3), GLTriangleFace(1)), - NormalFace(GLTriangleFace(1, 3, 4), GLTriangleFace(2)), - NormalFace(GLTriangleFace(1, 4, 5), GLTriangleFace(3)), - NormalFace(GLTriangleFace(1, 5, 2), GLTriangleFace(4)), - NormalFace(QuadFace(2, 3, 4, 5), QuadFace(5)) - ] + return nothing + # return [ + # NormalFace(GLTriangleFace(1, 2, 3), GLTriangleFace(1)), + # NormalFace(GLTriangleFace(1, 3, 4), GLTriangleFace(2)), + # NormalFace(GLTriangleFace(1, 4, 5), GLTriangleFace(3)), + # NormalFace(GLTriangleFace(1, 5, 2), GLTriangleFace(4)), + # NormalFace(QuadFace(2, 3, 4, 5), QuadFace(5)) + # ] end diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index 5089d33e..edfae378 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -571,16 +571,12 @@ 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) - return Vec3f[(-1,0,0), (1,0,0), (0,-1,0), (0,1,0), (0,0,-1), (0,0,1)] + 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) @@ -588,13 +584,8 @@ function texturecoordinates(rect::Rect3) end function faces(::Rect3) - return NormalUVFace{4, Int, QuadFace{Int}}[ - # position normal uv - ((1, 2, 4, 3), 1, (1, 2, 4, 3)), # -x - ((7, 8, 6, 5), 2, (7, 8, 6, 5)), # +x - ((5, 6, 2, 1), 3, (5, 6, 2, 1)), # -y - ((3, 4, 8, 7), 4, (3, 4, 8, 7)), # +y - ((1, 3, 7, 5), 5, (1, 3, 7, 5)), # -z - ((6, 8, 4, 2), 6, (6, 8, 4, 2)), # +z + 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 From b9a0f7e5c0066ffb9e0f5a76d5ad2345c2103b18 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 11 Sep 2024 01:05:24 +0200 Subject: [PATCH 049/120] treat views in vertex index remapping + some fixes --- src/meshes.jl | 174 +++++++++++++++++--------------------------------- 1 file changed, 59 insertions(+), 115 deletions(-) diff --git a/src/meshes.jl b/src/meshes.jl index 37061d1f..3a4753d6 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -33,7 +33,7 @@ function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleF fs = _fs end - return mesh(positions, fs; facetype = facetype, vertex_attributes...) + return mesh(positions, collect(fs); facetype = facetype, vertex_attributes...) end function drop_nothing_kwargs(kwargs) @@ -242,7 +242,7 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) 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 + if (getproperty(meshes[i], name) isa FaceView) != is_face_view consistent_face_views = false @goto DOUBLE_BREAK end @@ -284,141 +284,85 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) return Mesh(new_attribs, fs, views) - else # mixed MultiFace and VertexFace + else # mixed FaceViews and Arrays # simplify to VertexFace types, then retry merge - return merge(clear_face_views.(meshes)) + return merge(clear_faceviews.(meshes)) end end end -#= -# TODO: Is this ok as "public" function? -# MultiFace(f1, f2, f3) + (o1, o2, o3) = MultiFace(f1 + o1, f2 + o2, f3 + o3) -function Base.:+(f::MultiFace{N, T, FT, Names, M}, o::NTuple{M, T}) where {N, T, FT, Names, M} - return MultiFace{Names}(ntuple(m -> f[m] + o[m], M)) -end - -function merge_vertex_indices(mesh::AbstractMesh) - attribs, fs, views = merge_vertex_indices( - vertex_attributes(mesh), faces(mesh), mesh.views) - - return Mesh(attribs, fs, views) -end +function clear_faceviews(mesh::Mesh) + main_fs = faces(mesh) + va = vertex_attributes(mesh) -function merge_vertex_indices( - attribs::NamedTuple, - faces::AbstractVector{<: AbstractVertexFace}, - views::Vector{UnitRange{Int}}, - vertex_index_counter = nothing, - loop = false - ) - return attribs, faces, views -end + names = filter(name -> va[name] isa FaceView, collect(keys(va))) + isempty(names) && return mesh -function merge_vertex_indices( - attribs::NamedTuple, - faces::AbstractVector{<: MultiFace{N, T, FT} where {N, T, FT}}, - views::Vector{UnitRange{Int}}, - vertex_index_counter = nothing, - looped = false - ) + other_fs = map(name -> va[name].faces, names) + pushfirst!(names, :position) + all_fs = tuple(main_fs, other_fs...) + + + if isempty(mesh.views) - if !looped - return merge_vertex_indices( - attribs, decompose(GLTriangleFace, faces), views, - something(vertex_index_counter, GLIndex(1)), true - ) - else - throw(MethodError(merge_vertex_indices, (attribs, faces, views, vertex_index_counter))) - end -end + new_fs, maps = merge_vertex_indices(all_fs) -function merge_vertex_indices( - attribs::NamedTuple{Names}, - faces::AbstractVector{<: MultiFace{N, T, FT, Names}}, - views::Vector{UnitRange{Int}}, - vertex_index_counter = T(1), # TODO: test 0 vs 1 base - loop = false - ) where {Names, N, T, FT} - - # Note: typing checks for matching Names - - if isempty(views) - new_faces, vertex_map = merge_vertex_indices(faces) - new_attribs = ntuple(n -> attribs[n][vertex_map[n]], length(Names)) - return NamedTuple{Names}(new_attribs), new_faces, views - end + named_maps = NamedTuple{tuple(names...)}(maps) - new_attribs = NamedTuple((Pair(k, similar(v, 0)) for (k, v) in pairs(attribs))) - new_faces = similar(faces, FT, 0) - new_views = UnitRange{Int}[] - - # TODO: this depends on T in Face (1 based -> 1, 0 based -> 0) - for idxs in views - # Generate new face from current view, with the first vertex_index - # corresponding to the first vertex attribute added in this iteration - face_view = view(faces, idxs) - new_faces_in_view, vertex_map = merge_vertex_indices(face_view, vertex_index_counter) - vertex_index_counter += length(vertex_map[1]) - - # update vertex attributes - for (name, indices) in pairs(vertex_map) - append!(new_attribs[name], view(attribs[name], indices)) - end + new_va = NamedTuple(map(collect(keys(va))) do name + attrib = va[name] + if name === :position + return name => attrib[named_maps[name]] + elseif haskey(named_maps, name) + return name => attrib.data[named_maps[name]] + else + return name => attrib + end + end) - # add new faces and new view - start = length(new_faces) + 1 - append!(new_faces, new_faces_in_view) - push!(new_views, start:length(new_faces)) - end + return Mesh(new_va, new_fs) - return new_attribs, new_faces, new_views -end + else -function merge_vertex_indices(faces::AbstractVector{<: AbstractVertexFace}, i = T(1)) - N_vert = mapreduce(f -> max(f), max, faces) - return faces, i : N_vert - 1 + i -end + new_fs = sizehint!(eltype(main_fs)[], length(main_fs)) + new_views = sizehint!(UnitRange{Int}[], length(mesh.views)) + new_va = NamedTuple(map(collect(keys(va))) do name + attrib = va[name] + if name == :position + return name => sizehint!(eltype(attrib)[], length(attrib)) + elseif name in names + return name => sizehint!(eltype(attrib.data)[], length(attrib.data)) + else # doesn't need remapping so we just add it here + return name => attrib + end + end) -function merge_vertex_indices( - faces::AbstractVector{<: MultiFace{N, T, FT} where {N, T, FT}}, - vertex_index_counter = nothing - ) + vertex_index_counter = eltype(first(main_fs))(1) - return merge_vertex_indices( - decompose(GLTriangleFace, faces), - something(vertex_index_counter, GLIndex(1)) - ) -end -=# + for idxs in mesh.views + view_fs, maps = merge_vertex_indices(view.(all_fs, (idxs,)), vertex_index_counter) -function clear_face_views(mesh::Mesh) - main_fs = faces(mesh) - va = vertex_attributes(mesh) - # views = mesh.views # TODO: ignoring this for now - names = filter(name -> va[name] isa FaceView, collect(keys(va))) - other_fs = map(name -> va[name].faces, names) - - new_fs, maps = merge_vertex_indices(tuple(main_fs, other_fs...)) + vertex_index_counter += length(maps[1]) + + for (name, map) in zip(names, maps) + if name == :position + append!(new_va[name], va[name][map]) + else + append!(new_va[name], va[name].data[map]) + end + end - pushfirst!(names, :position) - named_maps = NamedTuple{tuple(names...)}(maps) - - new_va = NamedTuple(map(collect(keys(va))) do name - attrib = va[name] - if name === :position - return name => attrib[named_maps[name]] - elseif haskey(named_maps, name) - return name => attrib.data[named_maps[name]] - else - return name => attrib + # add new faces and new view + start = length(new_fs) + 1 + append!(new_fs, view_fs) + push!(new_views, start:length(new_fs)) end - end) - return Mesh(new_va, new_fs) + return Mesh(new_va, new_fs, new_views) + end end function merge_vertex_indices( From 13b9881131cc646db7f8e7a24d641ccadcf6d8a6 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 11 Sep 2024 01:49:42 +0200 Subject: [PATCH 050/120] fix face type change --- src/basic_types.jl | 2 +- src/meshes.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index f0abc41e..e1c9a281 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -311,7 +311,7 @@ function Base.vcat(a::FaceView, b::FaceView) N = length(a.data) return FaceView( vcat(a.data, b.data), - vcat(a.faces, map(f -> f .+ N, b.faces)) + vcat(a.faces, map(f -> typeof(f)(f .+ N), b.faces)) ) end diff --git a/src/meshes.jl b/src/meshes.jl index 3a4753d6..64254051 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -268,7 +268,8 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) # update face indices N = length(faces(mesh)) for i = idx .+ (1:N) - fs[i] = fs[i] + offset + # TODO: face + Int changes type to Int + fs[i] = typeof(fs[i])(fs[i] + offset) end # add views @@ -305,7 +306,6 @@ function clear_faceviews(mesh::Mesh) pushfirst!(names, :position) all_fs = tuple(main_fs, other_fs...) - if isempty(mesh.views) new_fs, maps = merge_vertex_indices(all_fs) From aef6ea2f318b13a585f1b4b1ff3a0774aee88d57 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 12 Sep 2024 00:57:46 +0200 Subject: [PATCH 051/120] clean up AbstractVertexFace --- src/basic_types.jl | 11 ++--------- src/interfaces.jl | 6 +++--- src/meshes.jl | 4 ++-- src/triangulation.jl | 2 +- 4 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index e1c9a281..5f83dda1 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -19,15 +19,8 @@ abstract type AbstractPolygon{Dim,T} <: Polytope{Dim,T} end Parent type for all faces. You should inherit from one of the child types instead. """ abstract type AbstractFace{N,T} <: StaticVector{N,T} end - -""" - AbstractVertexFace{N, T} <: AbstractFace{N, T} - -Parent type for faces addressing N vertices with common vertex indices. -""" -abstract type AbstractVertexFace{N, T} <: AbstractFace{N, T} end -abstract type AbstractSimplexFace{N,T} <: AbstractVertexFace{N,T} end -abstract type AbstractNgonFace{N,T} <: AbstractVertexFace{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,T} <: Polytope{Dim,T} end diff --git a/src/interfaces.jl b/src/interfaces.jl index ca42cdec..92a7b0cf 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -94,7 +94,7 @@ struct Normal{T} end Normal(::Type{T}) where {T} = Normal{T}() Normal() = Normal(Vec3f) -function decompose(::Type{F}, primitive::AbstractGeometry) where {F<:AbstractVertexFace} +function decompose(::Type{F}, primitive::AbstractGeometry) where {F<:AbstractFace} f = faces(primitive) if isnothing(f) if ndims(primitive) == 2 @@ -107,14 +107,14 @@ function decompose(::Type{F}, primitive::AbstractGeometry) where {F<:AbstractVer return decompose(F, f) end -function decompose(::Type{F}, f::AbstractVector) where {F<:AbstractVertexFace} +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<:AbstractVertexFace} +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) diff --git a/src/meshes.jl b/src/meshes.jl index 64254051..1cc5d2e8 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -56,7 +56,7 @@ function mesh( positions::AbstractVector{<:Point}, faces::AbstractVector{FT}; facetype=GLTriangleFace, vertex_attributes... - ) where {FT <: AbstractVertexFace} + ) where {FT <: AbstractFace} fs = decompose(facetype, faces) va = drop_nothing_kwargs(vertex_attributes) @@ -175,7 +175,7 @@ function uv_normal_mesh( end function normal_mesh( - points::AbstractVector{<:Point}, faces::AbstractVector{<:AbstractVertexFace}; + points::AbstractVector{<:Point}, faces::AbstractVector{<:AbstractFace}; pointtype = Point3f, normaltype = Vec3f, facetype = GLTriangleFace ) _points = decompose(pointtype, points) diff --git a/src/triangulation.jl b/src/triangulation.jl index 5604ef54..1847f9b5 100644 --- a/src/triangulation.jl +++ b/src/triangulation.jl @@ -110,7 +110,7 @@ Triangulate a Polygon without hole. Returns a Vector{`facetype`} defining indexes into `contour`. """ -function decompose(::Type{F}, points::AbstractVector{<:Point}) where {F<:AbstractVertexFace} +function decompose(::Type{F}, points::AbstractVector{<:Point}) where {F<:AbstractFace} #= allocate and initialize list of Vertices in polygon =# result = F[] From 24c0c0064685a0c7a4bb9c8962b4ca8e4b6eed08 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 12 Sep 2024 00:58:45 +0200 Subject: [PATCH 052/120] extend FaceView interface --- src/basic_types.jl | 108 ++++++++++++++++++++++++++++++--------------- src/meshes.jl | 24 +++------- 2 files changed, 79 insertions(+), 53 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 5f83dda1..5e34b804 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -280,20 +280,9 @@ Base.getindex(mpt::MultiPoint, i) = mpt.points[i] Base.size(mpt::MultiPoint) = size(mpt.points) Base.length(mpt::MultiPoint) = length(mpt.points) -""" - 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 -""" -abstract type AbstractMesh{Dim, T} <: AbstractGeometry{Dim, T} end -struct FaceView{ - T, AVT <: AbstractVector{T}, - FVT <: AbstractVector{ <: AbstractFace} - } +struct FaceView{T, AVT <: AbstractVector{T}, FVT <: AbstractVector{<: AbstractFace}} data::AVT faces::FVT end @@ -308,6 +297,57 @@ function Base.vcat(a::FaceView, b::FaceView) ) end +faces(x::FaceView) = x.faces +# attribute(x::FaceView) = x.data +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)) + +# 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(x) != FT + return FaceView(values(x), decompose(FT, faces(x))) + end + return x +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 +""" +abstract type AbstractMesh{Dim, T} <: AbstractGeometry{Dim, T} end + """ Mesh <: AbstractMesh{Element} The concrete AbstractMesh type. @@ -326,7 +366,7 @@ struct Mesh{ function Mesh( va::NamedTuple{Names, VAT}, - faces::FVT, + fs::FVT, views::Vector{UnitRange{Int}} = UnitRange{Int}[] ) where { D, T, FT <: AbstractFace, Names, @@ -346,24 +386,28 @@ struct Mesh{ end if va.position isa FaceView - if faces != va.position.faces + if fs != faces(va.position) error("position faces do not match gives faces") end - va = NamedTuple(map(pairs(va)) do (key, val) - return key => (key == :position ? val.data : val) + va = NamedTuple(map(keys(va)) do key + return key => (key == :position ? values(va[key]) : va[key]) end) end - # verify matching faces between face views + # verify that faces of FaceViews match `fs` (in length per face) for (name, attrib) in pairs(va) name == :position && continue if attrib isa FaceView - if length(attrib.faces) != length(faces) - error("Number of faces defined for $name $(length(attrib.faces)) does not match position $(length(faces))") + if isconcretetype(FT) && (FT == facetype(attrib)) + continue + end + + if length(attrib.faces) != length(fs) + error("Number of faces defined for $name $(length(attrib.faces)) does not match position $(length(fs))") end - for (i, (f1, f2)) in enumerate(zip(attrib.faces, faces)) + for (i, (f1, f2)) in enumerate(zip(attrib.faces, fs)) if length(f1) != length(f2) error("Length of face $name[$i] = $(length(f1)) does not match position[$i] = $(length(f2))") end @@ -371,7 +415,7 @@ struct Mesh{ end end - return new{D, T, FT, keys(va), VAT, FVT}(va, faces, views) + return new{D, T, FT, keys(va), VAT, FVT}(va, fs, views) end end @@ -406,30 +450,22 @@ normals(mesh::Mesh) = hasproperty(mesh, :normal) ? mesh.normal : nothing texturecoordinates(mesh::Mesh) = hasproperty(mesh, :uv) ? mesh.uv : nothing vertex_attributes(mesh::Mesh) = getfield(mesh, :vertex_attributes) -Base.getindex(mesh::Mesh, i::Integer) = mesh[mesh.connectivity[i]] +# TODO: maybe instead of this: +# mesh.attrib -> FaceView (get or construct) +# faceview[i] -> values(faceview)[faces(faceview)[i]] +# this would maybe clash with other functions of faceview though... +# Base.getindex(mesh::Mesh, i::Integer) = mesh[mesh.connectivity[i]] Base.length(mesh::Mesh) = length(mesh.connectivity) -# TODO: temp -# function Base.getindex(mesh::Mesh{D, T, <: AbstractVertexFace}, f::AbstractVertexFace) where {D, T} -# return getfield(mesh, :vertex_attributes).position[f] -# end -# function Base.getindex(::Mesh, f::AbstractMultiFace) -# error("TODO") -# end - function Base.:(==)(a::Mesh, b::Mesh) return (a.vertex_attributes == b.vertex_attributes) && (faces(a) == faces(b)) && (a.views == b.views) end -function Base.iterate(mesh::Mesh, i=1) - return i - 1 < length(mesh) ? (mesh[i], i + 1) : nothing +function Mesh(faces::AbstractVector{<:AbstractFace}; views::Vector{UnitRange{Int}} = UnitRange{Int}[], attributes...) + return Mesh(NamedTuple(attributes), faces, views) end -# function Mesh(faces::AbstractVector{<:AbstractFace}; views = UnitRange{Int}[], attributes...) -# return Mesh(NamedTuple(attributes), faces, views) -# end - function Mesh(points::AbstractVector{Point{Dim, T}}, faces::AbstractVector{<:AbstractFace}; views = UnitRange{Int}[], kwargs...) where {Dim, T} diff --git a/src/meshes.jl b/src/meshes.jl index 1cc5d2e8..c8c9d2fe 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -14,9 +14,11 @@ It also only losely correlates to the number of vertices, depending on the algor function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleFace, vertex_attributes...) positions = decompose(pointtype, 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 _fs = faces(primitive) end @@ -61,15 +63,8 @@ function mesh( fs = decompose(facetype, faces) va = drop_nothing_kwargs(vertex_attributes) - if (facetype != FT) - va = NamedTuple(map(keys(va)) do name - attrib = va[name] - if attrib isa FaceView - return name => FaceView(attrib.data, decompose(facetype, attrib.faces)) - else - return name => attrib - end - end) + if facetype != FT + va = NamedTuple{keys(va)}(convert_facetype.(facetype, values(va))) end return Mesh(positions, fs; va...) @@ -103,14 +98,9 @@ function mesh( va = merge(va, (position = decompose(pointtype, va.position),)) # Resolve facetype conversions of FaceViews - va = NamedTuple(map(keys(va)) do name - attrib = va[name] - if attrib isa FaceView - return name => FaceView(attrib.data, decompose(facetype, attrib.faces)) - else - return name => attrib + if facetype != FT + va = NamedTuple{keys(va)}(convert_facetype.(facetype, values(va))) end - end) # update main face type f, views = decompose(facetype, faces(mesh), mesh.views) @@ -302,7 +292,7 @@ function clear_faceviews(mesh::Mesh) names = filter(name -> va[name] isa FaceView, collect(keys(va))) isempty(names) && return mesh - other_fs = map(name -> va[name].faces, names) + other_fs = faces.(getproperty.((mesh,), names)) pushfirst!(names, :position) all_fs = tuple(main_fs, other_fs...) From 50451e2e08db8a3c19d3a804068013681ac718f4 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 12 Sep 2024 00:59:00 +0200 Subject: [PATCH 053/120] update Cylinder, Pyramids --- src/primitives/cylinders.jl | 33 ++++++++++----------------------- src/primitives/pyramids.jl | 19 ++++++++++--------- 2 files changed, 20 insertions(+), 32 deletions(-) diff --git a/src/primitives/cylinders.jl b/src/primitives/cylinders.jl index a3701628..5196e882 100644 --- a/src/primitives/cylinders.jl +++ b/src/primitives/cylinders.jl @@ -72,37 +72,24 @@ function normals(c::Cylinder, nvertices = 30) ns[end-1] = R * Vec3f(0, 0, -1) ns[end] = R * Vec3f(0, 0, 1) - return ns + disk1 = map(i -> GLTriangleFace(nhalf+1), 1:nhalf) + mantle = map(i -> QuadFace(i, i, mod1(i+1, nhalf), mod1(i+1, nhalf)), 1:nhalf) + disk2 = map(i -> GLTriangleFace(nhalf+2), 1:nhalf) + fs = vcat(disk1, mantle, disk2) + + return FaceView(ns, fs) end -function faces(c::Cylinder, facets=30) - return nothing - #= +function faces(::Cylinder, facets=30) nvertices = facets + isodd(facets) nhalf = div(nvertices, 2) - disk1 = map(1:nhalf) do i - NormalFace( - GLTriangleFace(nvertices+1, i, mod1(i+1, nhalf)), - GLTriangleFace(nhalf+1) - ) - end - + disk1 = map(i -> GLTriangleFace(nvertices+1, i, mod1(i+1, nhalf)), 1:nhalf) mantle = map(1:nhalf) do i i1 = mod1(i+1, nhalf) - NormalFace( - QuadFace(i, i+nhalf, i1 + nhalf, i1), - QuadFace(i, i, i1, i1) - ) - end - - disk2 = map(1:nhalf) do i - NormalFace( - GLTriangleFace(nvertices+2, i+nhalf, mod1(i+1, nhalf)+nhalf), - GLTriangleFace(nhalf+2) - ) + QuadFace(i, i+nhalf, i1 + nhalf, i1) end + 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 6579349e..0a288729 100644 --- a/src/primitives/pyramids.jl +++ b/src/primitives/pyramids.jl @@ -22,16 +22,17 @@ end function normals(p::Pyramid) w = p.width; h = p.length - return normalize.(Vec3f[(h, 0, w), (0, h, w), (-h, 0, w), (0, -h, w), (0, 0, -1)]) + 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 nothing - # return [ - # NormalFace(GLTriangleFace(1, 2, 3), GLTriangleFace(1)), - # NormalFace(GLTriangleFace(1, 3, 4), GLTriangleFace(2)), - # NormalFace(GLTriangleFace(1, 4, 5), GLTriangleFace(3)), - # NormalFace(GLTriangleFace(1, 5, 2), GLTriangleFace(4)), - # NormalFace(QuadFace(2, 3, 4, 5), QuadFace(5)) - # ] + return [ + GLTriangleFace(1, 2, 3), + GLTriangleFace(1, 3, 4), + GLTriangleFace(1, 4, 5), + GLTriangleFace(1, 5, 2), + QuadFace(2, 3, 4, 5) + ] end From db6edb55bbb3ba6cb0699e7e868b7ac0bcc3ba99 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 12 Sep 2024 02:11:37 +0200 Subject: [PATCH 054/120] declutter NgonFace prints --- src/basic_types.jl | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 5e34b804..c6b8bdf3 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -48,8 +48,22 @@ 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 Face(::Type{<:NgonFace{N}}, ::Type{T}) where {N,T} = NgonFace{N,T} From e8d48d0e0957de69ba77ec7cb14dcb1b82ac3f24 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 12 Sep 2024 02:11:57 +0200 Subject: [PATCH 055/120] update tests --- src/basic_types.jl | 28 ++++----- src/meshes.jl | 11 ++-- test/geometrytypes.jl | 83 ++++++++++++++------------- test/meshes.jl | 128 +++++++++++++++++++++--------------------- test/runtests.jl | 4 +- 5 files changed, 132 insertions(+), 122 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index c6b8bdf3..0d5ccd53 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -317,6 +317,7 @@ 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 @@ -399,15 +400,6 @@ struct Mesh{ va = NamedTuple{names}(values(va)) end - if va.position isa FaceView - if fs != faces(va.position) - error("position faces do not match gives faces") - end - va = NamedTuple(map(keys(va)) do key - return key => (key == :position ? values(va[key]) : va[key]) - end) - end - # verify that faces of FaceViews match `fs` (in length per face) for (name, attrib) in pairs(va) name == :position && continue @@ -464,11 +456,7 @@ normals(mesh::Mesh) = hasproperty(mesh, :normal) ? mesh.normal : nothing texturecoordinates(mesh::Mesh) = hasproperty(mesh, :uv) ? mesh.uv : nothing vertex_attributes(mesh::Mesh) = getfield(mesh, :vertex_attributes) -# TODO: maybe instead of this: -# mesh.attrib -> FaceView (get or construct) -# faceview[i] -> values(faceview)[faces(faceview)[i]] -# this would maybe clash with other functions of faceview though... -# Base.getindex(mesh::Mesh, i::Integer) = mesh[mesh.connectivity[i]] +Base.getindex(mesh::Mesh, i::Integer) = mesh.position[mesh.connectivity[i]] Base.length(mesh::Mesh) = length(mesh.connectivity) function Base.:(==)(a::Mesh, b::Mesh) @@ -476,6 +464,10 @@ function Base.:(==)(a::Mesh, b::Mesh) (faces(a) == faces(b)) && (a.views == b.views) end +function Base.iterate(mesh::Mesh, i=1) + return i - 1 < length(mesh) ? (mesh[i], i + 1) : nothing +end + function Mesh(faces::AbstractVector{<:AbstractFace}; views::Vector{UnitRange{Int}} = UnitRange{Int}[], attributes...) return Mesh(NamedTuple(attributes), faces, views) end @@ -491,6 +483,14 @@ function Mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Integer}, return Mesh(points, connect(faces, facetype, skip)) end +function Mesh(; kwargs...) + fs = faces(kwargs[:position]) + va = NamedTuple{keys(kwargs)}( + map(k -> k == :position ? values(kwargs[k]) : kwargs[k], keys(kwargs)) + ) + return Mesh(va, fs) +end + struct MetaMesh{Dim, T, M <: AbstractMesh{Dim, T}} <: AbstractMesh{Dim, T} mesh::M diff --git a/src/meshes.jl b/src/meshes.jl index c8c9d2fe..a8c9106d 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -78,10 +78,13 @@ function mesh( va = drop_nothing_kwargs(attributes) - # N = length(mesh.position) - # if !all(attr -> length(attr) == N, values(va)) - # error("At least one of the given vertex attributes does not match `length(mesh.positon) = $N`.") - # end + N = length(mesh.position) + for name in keys(attributes) + attr = attributes[name] + if (attr isa AbstractVector) && (length(attr) < N) + error("Added Attribute $name has length $(length(attr)) but should have at least length $N.") + end # Mesh constructor checks faceviews already + end if FT == facetype if isempty(va) && GeometryBasics.pointtype(mesh) == pointtype diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 5a1d2e39..9205c64f 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -36,37 +36,43 @@ using Test, GeometryBasics @test decompose(Point3{Float64}, Tesselation(s, 8)) ≈ positions _faces = [ - MultiFace(position = TriangleFace(9, 1, 2), normal = TriangleFace(5, 5, 5)), - MultiFace(position = TriangleFace(9, 2, 3), normal = TriangleFace(5, 5, 5)), - MultiFace(position = TriangleFace(9, 3, 4), normal = TriangleFace(5, 5, 5)), - MultiFace(position = TriangleFace(9, 4, 1), normal = TriangleFace(5, 5, 5)), - MultiFace(position = TriangleFace(1, 5, 6), normal = TriangleFace(1, 1, 2)), - MultiFace(position = TriangleFace(1, 6, 2), normal = TriangleFace(1, 2, 2)), - MultiFace(position = TriangleFace(2, 6, 7), normal = TriangleFace(2, 2, 3)), - MultiFace(position = TriangleFace(2, 7, 3), normal = TriangleFace(2, 3, 3)), - MultiFace(position = TriangleFace(3, 7, 8), normal = TriangleFace(3, 3, 4)), - MultiFace(position = TriangleFace(3, 8, 4), normal = TriangleFace(3, 4, 4)), - MultiFace(position = TriangleFace(4, 8, 5), normal = TriangleFace(4, 4, 1)), - MultiFace(position = TriangleFace(4, 5, 1), normal = TriangleFace(4, 1, 1)), - MultiFace(position = TriangleFace(10, 5, 6), normal = TriangleFace(6, 6, 6)), - MultiFace(position = TriangleFace(10, 6, 7), normal = TriangleFace(6, 6, 6)), - MultiFace(position = TriangleFace(10, 7, 8), normal = TriangleFace(6, 6, 6)), - MultiFace(position = TriangleFace(10, 8, 5), normal = TriangleFace(6, 6, 6)), - ] + TriangleFace(9, 1, 2), TriangleFace(9, 2, 3), TriangleFace(9, 3, 4), + TriangleFace(9, 4, 1), TriangleFace(1, 5, 6), TriangleFace(1, 6, 2), + TriangleFace(2, 6, 7), TriangleFace(2, 7, 3), TriangleFace(3, 7, 8), + TriangleFace(3, 8, 4), TriangleFace(4, 8, 5), TriangleFace(4, 5, 1), + TriangleFace(10, 5, 6), TriangleFace(10, 6, 7), TriangleFace(10, 7, 8), + TriangleFace(10, 8, 5)] @test _faces == decompose(TriangleFace{Int}, Tesselation(s, 8)) m = triangle_mesh(Tesselation(s, 8)) @test m === triangle_mesh(m) - fs, maps = GeometryBasics.merge_vertex_indices(GeometryBasics.simplify_faces((:position,), _faces)) - @test GeometryBasics.faces(m) == decompose(GLTriangleFace, fs) - @test GeometryBasics.coordinates(m) ≈ positions[maps[1]] + @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 @test hasproperty(m, :position) @test hasproperty(m, :normal) - @test length(m.position) == length(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, 1, 2, 2), QuadFace{Int64}(2, 2, 3, 3), + QuadFace{Int64}(3, 3, 4, 4), QuadFace{Int64}(4, 4, 1, 1), + 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 !hasproperty(muv, :uv) # not defined yet @@ -88,17 +94,15 @@ end mesh = normal_mesh(b) @test faces(mesh) == GLTriangleFace[ - (1, 2, 3), (1, 3, 4), (5, 6, 7), (5, 7, 8), (9, 10, 11), (9, 11, 12), - (13, 14, 15), (13, 15, 16), (17, 18, 19), (17, 19, 20), (21, 22, 23), (21, 23, 24) - ] - @test normals(mesh) == [n for n in normals(b) for _ in 1:4] + (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, 2.0], [1.0, 2.0, 1.0], - [2.0, 2.0, 1.0], [2.0, 2.0, 2.0], [2.0, 1.0, 2.0], [2.0, 1.0, 1.0], - [2.0, 1.0, 1.0], [2.0, 1.0, 2.0], [1.0, 1.0, 2.0], [1.0, 1.0, 1.0], - [1.0, 2.0, 1.0], [1.0, 2.0, 2.0], [2.0, 2.0, 2.0], [2.0, 2.0, 1.0], - [1.0, 1.0, 1.0], [1.0, 2.0, 1.0], [2.0, 2.0, 1.0], [2.0, 1.0, 1.0], - [2.0, 1.0, 2.0], [2.0, 2.0, 2.0], [1.0, 2.0, 2.0], [1.0, 1.0, 2.0]] + [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 @@ -128,13 +132,16 @@ NFace = NgonFace end @testset "Normals" begin - r = centered(Rect3{Float64}) - n64 = [n for n in normals(r) for _ in 1:4] - n32 = map(Vec{3,Float32}, n64) - m32 = normal_mesh(r) - m64 = normal_mesh(r, pointtype = Point3d) - @test normals(coordinates(m32), GeometryBasics.faces(m32)) == n32 - @test normals(coordinates(m64), GeometryBasics.faces(m64)) == n64 + # Needs to be reworked + @test_broken false + + # r = centered(Rect3{Float64}) + # n64 = [n for n in normals(r) for _ in 1:4] + # n32 = map(Vec{3,Float32}, n64) + # m32 = normal_mesh(r) + # m64 = normal_mesh(r, pointtype = Point3d) + # @test normals(coordinates(m32), GeometryBasics.faces(m32)) == n32 + # @test normals(coordinates(m64), GeometryBasics.faces(m64)) == n64 end @testset "HyperSphere" begin diff --git a/test/meshes.jl b/test/meshes.jl index 5c19e889..20101ce5 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -37,100 +37,107 @@ end # Sanity Check # TODO: extend m = Mesh( - [GeometryBasics.MultiFace(position = QuadFace(1, 2, 3, 4), normal = QuadFace(1,1,1,1))], - position = Point2f[(0, 0), (1, 0), (1, 1), (0, 1)], - normal = [Vec3f(0,0,1)] + 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.merge_vertex_indices(m) + m2 = GeometryBasics.clear_faceviews(m) - @test faces(m) isa AbstractVector{<: GeometryBasics.MultiFace} - @test propertynames(eltype(faces(m))) == keys(GeometryBasics.vertex_attributes(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) isa AbstractVector{<: QuadFace} + @test faces(m2) == [QuadFace(1,2,3,4)] @test coordinates(m2) == coordinates(m) @test normals(m2) != normals(m) - @test normals(m2) == [only(normals(m)) for _ in 1:4] + @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] - multiface_meshes = map(rects) do r + direct_meshes = map(rects) do r GeometryBasics.Mesh(coordinates(r), faces(r), normal = normals(r)) end - mfm = merge(multiface_meshes) + dm = merge(direct_meshes) - @test GeometryBasics.facetype(mfm) == GeometryBasics.NormalFace{4, Int64, QuadFace{Int64}} - @test length(faces(mfm)) == 27 * 6 # 27 rects, 6 quad faces - @test length(normals(mfm)) == 27 * 6 - @test length(coordinates(mfm)) == 27 * 8 - @test !allunique([idx for f in faces(mfm) for idx in f.position]) - @test !allunique([idx for f in faces(mfm) for idx in f.normal]) + @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]) - vertexface_meshes = map(rects) do r + indirect_meshes = map(rects) do r GeometryBasics.mesh(coordinates(r), faces(r), normal = normals(r), facetype = QuadFace{Int64}) end - vfm = merge(vertexface_meshes) + im = merge(indirect_meshes) - @test GeometryBasics.facetype(vfm) == QuadFace{Int64} - @test length(faces(vfm)) == 27 * 6 # 27 rects, 6 quad faces each - @test length(normals(vfm)) == 27 * 6 * 4 # 27 rects, 6 faces, 4 normals each - @test length(coordinates(vfm)) == 27 * 8 * 3 # 27 rects, 8 points, 3 duplications (for 3 attached quad faces) - @test allunique([idx for f in faces(vfm) for idx in f]) + @test im == dm - mixed_meshes = map(multiface_meshes, vertexface_meshes) do mfm, vfm - rand() > 0.5 ? mfm : vfm + 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, converted_meshes) do dm, cm + rand() > 0.5 ? dm : cm end mm = merge(mixed_meshes) - @test GeometryBasics.facetype(mm) == QuadFace{Int64} - @test length(faces(mm)) == 27 * 6 # 27 rects, 6 quad faces each - @test length(normals(mm)) == 27 * 6 * 4 # 27 rects, 6 faces, 4 normals each - @test length(coordinates(mm)) == 27 * 8 * 3 # 27 rects, 8 points, 3 duplications (for 3 attached quad faces) - @test allunique([idx for f in faces(mm) for idx in f]) - @test mm == vfm + @test mm == cm end @testset "mesh() constructors" begin r = Rect3d(Point3d(-1), Vec3d(2)) @testset "prerequisites" begin - ps = collect(coordinates(r)) + ps = coordinates(r) @test length(ps) == 8 @test ps isa Vector{Point3d} - ns = collect(normals(r)) + ns = normals(r) @test length(ns) == 6 - @test ns isa Vector{Vec3f} - uvs = collect(texturecoordinates(r)) + @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 = collect(faces(r)) + fs = faces(r) @test length(fs) == 6 - @test fs isa Vector{GeometryBasics.NormalUVFace{4, Int64, QuadFace{Int64}}} + @test fs isa Vector{QuadFace{Int64}} end @testset "normal_mesh()" begin - # TODO: simplify? - FT = GeometryBasics.NormalFace{4, Int64, QuadFace{Int64}} - m = normal_mesh(r, pointtype = Point3f, normaltype = Vec3f, facetype = FT) + 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)) == 8 + @test length(coordinates(m)) == 24 @test GeometryBasics.pointtype(m) == Point3f @test hasproperty(m, :normal) @test normals(m) isa Vector{Vec3f} - @test length(normals(m)) == 6 + @test length(normals(m)) == 24 @test !hasproperty(m, :uv) @test texturecoordinates(m) === nothing - @test faces(m) isa Vector{FT} - @test length(faces(m)) == 6 - @test GeometryBasics.facetype(m) == FT + @test faces(m) isa Vector{GLTriangleFace} + @test length(faces(m)) == 12 + @test GeometryBasics.facetype(m) == GLTriangleFace end @testset "normal_uv_mesh()" begin @@ -141,16 +148,16 @@ end @test hasproperty(m, :position) @test coordinates(m) isa Vector{Point3d} - @test length(coordinates(m)) == 24 + @test length(coordinates(m)) == 8 @test GeometryBasics.pointtype(m) == Point3d @test hasproperty(m, :normal) - @test normals(m) isa Vector{Vec3d} - @test length(normals(m)) == 24 + @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)) == 24 + @test length(texturecoordinates(m)) == 8 @test faces(m) isa Vector{QuadFace{Int32}} @test length(faces(m)) == 6 @@ -199,24 +206,23 @@ end end @testset "mesh(mesh)" begin - FT = GeometryBasics.NormalFace{4, Int64, QuadFace{Int64}} - m = GeometryBasics.mesh(r, pointtype = Point3f, normal = normals(r), facetype = FT) + 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 Vector{Vec3f} + @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{FT} + @test faces(m) isa Vector{QuadFace{Int64}} @test length(faces(m)) == 6 - # shouldn't be able to add vertex attributes when they aren't synchronized + # Shoudl throw because uv's don't match length(position) or have faces @test_throws ErrorException GeometryBasics.mesh(m, uv = Vec3f[]) - # Can convert face type - should remap - m2 = GeometryBasics.mesh(m, facetype = QuadFace{Int32}) + # 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 @@ -224,14 +230,11 @@ end @test length(normals(m2)) == 24 @test !hasproperty(m2, :uv) @test texturecoordinates(m2) === nothing - @test faces(m2) isa Vector{QuadFace{Int32}} + @test faces(m2) isa Vector{QuadFace{Int64}} @test length(faces(m2)) == 6 - # remap + decompose face and then adding vertex attribute is fine - m2 = GeometryBasics.mesh(m, facetype = GLTriangleFace) - # should be same length as other vertices... - @test_throws ErrorException GeometryBasics.mesh(m2, uv = Vec3f[]) - m2 = GeometryBasics.mesh(m2, uv = decompose(Point3f, m2)) + # 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 @@ -242,6 +245,5 @@ end @test faces(m2) isa Vector{GLTriangleFace} @test length(faces(m2)) == 12 - end end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 81b63bc1..e110e658 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,8 +6,6 @@ using GeoInterface using GeoJSON using Extents -using GeometryBasics: MultiFace - @testset "GeometryBasics" begin @testset "algorithms" begin @test_broken false @@ -178,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) From 5ef74ff6dd2297622f7887b56ac7f671ea37f63f Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 12 Sep 2024 02:15:00 +0200 Subject: [PATCH 056/120] cleanup some test_broken --- test/runtests.jl | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index e110e658..c01f5154 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,14 +8,13 @@ using Extents @testset "GeometryBasics" begin @testset "algorithms" begin - @test_broken false - # cube = Rect(Vec3f(-0.5), Vec3f(1)) - # cube_faces = decompose(TriangleFace{Int}, faces(cube)) - # cube_vertices = decompose(Point{3,Float32}, cube) - # @test area(cube_vertices, cube_faces) == 6 - # mesh = Mesh(cube_vertices, cube_faces) - # @test GeometryBasics.volume(mesh) ≈ 1 - # @test GeometryBasics.volume(cube) ≈ 1 + cube = Rect(Vec3f(-0.5), Vec3f(1)) + cube_faces = decompose(TriangleFace{Int}, faces(cube)) + cube_vertices = decompose(Point{3,Float32}, cube) + @test area(cube_vertices, cube_faces) == 6 + mesh = Mesh(cube_vertices, cube_faces) + @test GeometryBasics.volume(mesh) ≈ 1 + @test GeometryBasics.volume(cube) ≈ 1 rect = Rect(1, 2, 7.5, 2.0) @test GeometryBasics.area(rect) ≈ 15 @@ -52,8 +51,6 @@ 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_broken m_meta.boundingbox === Rect(1.0, 1.0, 2.0, 2.0) - @test_broken propertynames(m_meta) == (:boundingbox,) @test m_meta[:boundingbox] === Rect(1.0, 1.0, 2.0, 2.0) @test collect(keys(m_meta)) == [:boundingbox,] end From 5aa7fa6b4a2eb76eb253d2490a932f94f9601877 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 12 Sep 2024 12:49:25 +0200 Subject: [PATCH 057/120] switch to Dict --- src/basic_types.jl | 114 ++++++++++++++++++++++++++++----------------- src/meshes.jl | 98 ++++++++++++++------------------------ 2 files changed, 106 insertions(+), 106 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 0d5ccd53..f5a1f014 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -323,12 +323,30 @@ Base.:(==)(a::FaceView, b::FaceView) = (values(a) == values(b)) && (faces(a) == # 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(x) != FT + 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 isconcretetype(FT) && (FT == facetype(fv)) + return true + end + + if length(faces(fv)) != length(fs) + error("Number of faces given in FaceView $(length(faces(fv))) does not match reference $(length(fs))") + 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)) @@ -370,58 +388,50 @@ The concrete AbstractMesh type. struct Mesh{ Dim, T <: Real, # TODO: Number? FT <: AbstractFace, - Names, - VAT <: Tuple{AbstractVector{Point{Dim, T}}, Vararg{VertexAttributeType}}, FVT <: AbstractVector{FT} } <: AbstractMesh{Dim, T} - vertex_attributes::NamedTuple{Names, VAT} + vertex_attributes::Dict{Symbol, VertexAttributeType} connectivity::FVT views::Vector{UnitRange{Int}} function Mesh( - va::NamedTuple{Names, VAT}, + va::Dict{Symbol, VertexAttributeType}, fs::FVT, views::Vector{UnitRange{Int}} = UnitRange{Int}[] - ) where { - D, T, FT <: AbstractFace, Names, - VAT <: Tuple{AbstractVector{Point{D, T}}, Vararg{VertexAttributeType}}, - FVT <: AbstractVector{FT} - } + ) where {FT <: AbstractFace, FVT <: AbstractVector{FT}} # verify type - if first(Names) !== :position - error("The first vertex attribute should be a 'position' but is a '$(first(Names))'.") + if !haskey(va, :position ) + error("Vertex attributes must have a :position attribute.") end - if :normals in Names + 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 = map(name -> ifelse(name === :normals, :normal, name), Names) - va = NamedTuple{names}(values(va)) + va[:normal] = pop!(va, :normal) end # verify that faces of FaceViews match `fs` (in length per face) - for (name, attrib) in pairs(va) + N = maximum(f -> value(maximum(f)), fs) + for (name, attrib) in va name == :position && continue if attrib isa FaceView - if isconcretetype(FT) && (FT == facetype(attrib)) - continue - end - - if length(attrib.faces) != length(fs) - error("Number of faces defined for $name $(length(attrib.faces)) does not match position $(length(fs))") - end - - for (i, (f1, f2)) in enumerate(zip(attrib.faces, fs)) - if length(f1) != length(f2) - error("Length of face $name[$i] = $(length(f1)) does not match position[$i] = $(length(f2))") - end + try + verify(fs, attrib) + catch e + @error "Failed to verify $name attribute:" + rethrow(e) 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{D, T, FT, keys(va), VAT, FVT}(va, fs, views) + D = length(eltype(va[:position])) + T = eltype(eltype(va[:position])) + + return new{D, T, FT, FVT}(va, fs, views) end end @@ -432,9 +442,9 @@ 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 hasfield(mesh, :normal) + return hasproperty(mesh, :normal) end - return hasproperty(getfield(mesh, :vertex_attributes), field) || hasfield(Mesh, field) + return haskey(getfield(mesh, :vertex_attributes), field) || hasfield(Mesh, field) end @inline function Base.getproperty(mesh::Mesh, field::Symbol) if hasfield(Mesh, field) @@ -443,11 +453,11 @@ end @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) + return getindex(getfield(mesh, :vertex_attributes), field) end end @inline function Base.propertynames(mesh::Mesh) - return (fieldnames(Mesh)..., propertynames(getfield(mesh, :vertex_attributes))...) + return (fieldnames(Mesh)..., keys(getfield(mesh, :vertex_attributes))...) end coordinates(mesh::Mesh) = mesh.position @@ -468,14 +478,31 @@ function Base.iterate(mesh::Mesh, i=1) return i - 1 < length(mesh) ? (mesh[i], i + 1) : nothing end +function add_vertex_attribute!(mesh::Mesh, val::AbstractVector, name::Symbol) + @boundscheck begin + N = maximum(f -> value(maximum(f)), faces(mesh)) + length(val) < N && error("Given vertex data not large enough to be adressed by all faces ($N required, $(length(val)) given)") + end + mesh.vertex_attributes[name] = val + return mesh +end + +function add_vertex_attribute!(mesh::Mesh, val::FaceView, name::Symbol) + @boundscheck verify(faces(mesh), val) + mesh.vertex_attributes[name] = val + return mesh +end + function Mesh(faces::AbstractVector{<:AbstractFace}; views::Vector{UnitRange{Int}} = UnitRange{Int}[], attributes...) - return Mesh(NamedTuple(attributes), faces, views) + return Mesh(Dict{Symbol, VertexAttributeType}(attributes), faces, views) end function Mesh(points::AbstractVector{Point{Dim, T}}, faces::AbstractVector{<:AbstractFace}; views = UnitRange{Int}[], kwargs...) where {Dim, T} - return Mesh((position = points, kwargs...), faces, views) + va = Dict{Symbol, VertexAttributeType}(kwargs) + va[:position] = points + return Mesh(va, faces, views) end function Mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Integer}, @@ -484,10 +511,9 @@ function Mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Integer}, end function Mesh(; kwargs...) - fs = faces(kwargs[:position]) - va = NamedTuple{keys(kwargs)}( - map(k -> k == :position ? values(kwargs[k]) : kwargs[k], keys(kwargs)) - ) + va = Dict{Symbol, VertexAttributeType}(kwargs) + fs = faces(va[:position]::FaceView) + va[:position] = values(va[:position]) return Mesh(va, fs) end @@ -543,10 +569,10 @@ Mesh(mesh::Mesh) = mesh # Shorthand types # used in meshes.jl -const SimpleMesh{N, T, FT} = Mesh{N, T, FT, (:position,), Tuple{Vector{Point{N, T}}}, Vector{FT}} +const SimpleMesh{N, T, FT} = Mesh{N, T, FT, Vector{FT}} const SimpleTriangleMesh{N} = SimpleMesh{N, Float32, GLTriangleFace} # -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 GLNormalMesh{N} = NormalMesh{N, Float32, GLTriangleFace} -const GLNormalUVMesh{N} = NormalUVMesh{N, Float32, GLTriangleFace} \ No newline at end of file +# 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 GLNormalMesh{N} = NormalMesh{N, Float32, GLTriangleFace} +# const GLNormalUVMesh{N} = NormalUVMesh{N, Float32, GLTriangleFace} \ No newline at end of file diff --git a/src/meshes.jl b/src/meshes.jl index a8c9106d..bf8fa5a1 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -38,12 +38,6 @@ function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleF return mesh(positions, collect(fs); facetype = facetype, vertex_attributes...) end -function drop_nothing_kwargs(kwargs) - _keys = filter(k -> !isnothing(kwargs[k]), keys(kwargs)) - _vals = ntuple(n -> kwargs[_keys[n]], length(_keys)) - return NamedTuple{_keys}(_vals) -end - """ mesh(positions, faces[, facetype = GLTriangleFace, vertex_attributes...]) @@ -61,10 +55,12 @@ function mesh( ) where {FT <: AbstractFace} fs = decompose(facetype, faces) - va = drop_nothing_kwargs(vertex_attributes) - if facetype != FT - va = NamedTuple{keys(va)}(convert_facetype.(facetype, values(va))) + va = Dict{Symbol, VertexAttributeType}() + for k in keys(vertex_attributes) + if !isnothing(vertex_attributes[k]) + va[k] = convert_facetype(facetype, vertex_attributes[k]) + end end return Mesh(positions, fs; va...) @@ -76,34 +72,22 @@ function mesh( attributes... ) where {D, T, FT <: AbstractFace} - va = drop_nothing_kwargs(attributes) - - N = length(mesh.position) - for name in keys(attributes) - attr = attributes[name] - if (attr isa AbstractVector) && (length(attr) < N) - error("Added Attribute $name has length $(length(attr)) but should have at least length $N.") - end # Mesh constructor checks faceviews already + va = Dict{Symbol, VertexAttributeType}() + for k in keys(attributes) + isnothing(attributes[k]) || setindex!(va, attributes[k], k) end - if FT == facetype - if isempty(va) && GeometryBasics.pointtype(mesh) == pointtype - return mesh - else - # 1. add vertex attributes, 2. convert position attribute - va = merge(vertex_attributes(mesh), va) - va = merge(va, (position = decompose(pointtype, va.position),)) - return Mesh(va, faces(mesh), mesh.views) - end + if isempty(va) && (GeometryBasics.pointtype(mesh) == pointtype) && (FT == facetype) + return mesh else # 1. add vertex attributes, 2. convert position attribute va = merge(vertex_attributes(mesh), va) - va = merge(va, (position = decompose(pointtype, va.position),)) + va[:position] = decompose(pointtype, va[:position]) # Resolve facetype conversions of FaceViews - if facetype != FT - va = NamedTuple{keys(va)}(convert_facetype.(facetype, values(va))) - end + for (k, v) in va + va[k] = convert_facetype(facetype, v) + end # update main face type f, views = decompose(facetype, faces(mesh), mesh.views) @@ -221,13 +205,13 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) # Check that all meshes use the same VertexAttributes # Could also do this via typing the function, but maybe error is nice? - names = propertynames(m1.vertex_attributes) - idx = findfirst(m -> propertynames(m.vertex_attributes) != names, meshes) + names = keys(m1.vertex_attributes) + idx = findfirst(m -> keys(m.vertex_attributes) != names, meshes) if idx !== nothing error( "Cannot merge meshes with different vertex attributes. " * "First missmatch between meshes[1] with $names and " * - "meshes[$idx] with $(propertynames(meshes[idx]))." + "meshes[$idx] with $(keys(vertex_attributes(meshes[idx])))." ) end @@ -246,9 +230,9 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) if consistent_face_views # All the same kind of face, can just merge - - new_attribs = NamedTuple{names}(map(names) do name - return mapreduce(m -> getproperty(m, name), vcat, meshes) + + new_attribs = Dict{Symbol, VertexAttributeType}(map(collect(names)) do name + return name => mapreduce(m -> getproperty(m, name), vcat, meshes) end) fs = reduce(vcat, faces.(meshes)) @@ -299,22 +283,21 @@ function clear_faceviews(mesh::Mesh) pushfirst!(names, :position) all_fs = tuple(main_fs, other_fs...) + # need to avoid self-overwrite + for key in keys(va) + va[key] = deepcopy(values(va[key])) + end + if isempty(mesh.views) new_fs, maps = merge_vertex_indices(all_fs) named_maps = NamedTuple{tuple(names...)}(maps) - new_va = NamedTuple(map(collect(keys(va))) do name - attrib = va[name] - if name === :position - return name => attrib[named_maps[name]] - elseif haskey(named_maps, name) - return name => attrib.data[named_maps[name]] - else - return name => attrib - end - end) + new_va = Dict{Symbol, VertexAttributeType}() + for (name, attrib) in va + new_va[name] = attrib[get(named_maps, name, maps[1])] + end return Mesh(new_va, new_fs) @@ -322,16 +305,10 @@ function clear_faceviews(mesh::Mesh) new_fs = sizehint!(eltype(main_fs)[], length(main_fs)) new_views = sizehint!(UnitRange{Int}[], length(mesh.views)) - new_va = NamedTuple(map(collect(keys(va))) do name - attrib = va[name] - if name == :position - return name => sizehint!(eltype(attrib)[], length(attrib)) - elseif name in names - return name => sizehint!(eltype(attrib.data)[], length(attrib.data)) - else # doesn't need remapping so we just add it here - return name => attrib - end - end) + new_va = Dict{Symbol, VertexAttributeType}() + for (name, attrib) in va + new_va[name] = sizehint!(eltype(attrib)[], length(attrib)) + end vertex_index_counter = eltype(first(main_fs))(1) @@ -340,12 +317,9 @@ function clear_faceviews(mesh::Mesh) vertex_index_counter += length(maps[1]) - for (name, map) in zip(names, maps) - if name == :position - append!(new_va[name], va[name][map]) - else - append!(new_va[name], va[name].data[map]) - end + for name in keys(new_va) + map = maps[something(findfirst(==(name), names), 1)] + append!(new_va[name], va[name][map]) end # add new faces and new view From 615af26a73b8f2097b30112763f41fc77aa0f86b Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 12 Sep 2024 15:35:40 +0200 Subject: [PATCH 058/120] fix tests --- src/basic_types.jl | 6 +----- src/meshes.jl | 11 +++-------- test/runtests.jl | 3 ++- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index f5a1f014..9e98aa98 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -412,7 +412,7 @@ struct Mesh{ end # verify that faces of FaceViews match `fs` (in length per face) - N = maximum(f -> value(maximum(f)), fs) + N = maximum(f -> value(maximum(f)), fs, init = 0) for (name, attrib) in va name == :position && continue @@ -435,10 +435,6 @@ struct Mesh{ end end -# TODO: what shorthands could be useful? -# const GLMesh{D, T} = Mesh{D, T, GLTriangleFace} -# const SimpleGLMesh{D, T} = Mesh{D, T, GLTriangleFace, (:position,)} - @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" diff --git a/src/meshes.jl b/src/meshes.jl index bf8fa5a1..fe4541dd 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -283,11 +283,6 @@ function clear_faceviews(mesh::Mesh) pushfirst!(names, :position) all_fs = tuple(main_fs, other_fs...) - # need to avoid self-overwrite - for key in keys(va) - va[key] = deepcopy(values(va[key])) - end - if isempty(mesh.views) new_fs, maps = merge_vertex_indices(all_fs) @@ -296,7 +291,7 @@ function clear_faceviews(mesh::Mesh) new_va = Dict{Symbol, VertexAttributeType}() for (name, attrib) in va - new_va[name] = attrib[get(named_maps, name, maps[1])] + new_va[name] = values(attrib)[get(named_maps, name, maps[1])] end return Mesh(new_va, new_fs) @@ -307,7 +302,7 @@ function clear_faceviews(mesh::Mesh) new_views = sizehint!(UnitRange{Int}[], length(mesh.views)) new_va = Dict{Symbol, VertexAttributeType}() for (name, attrib) in va - new_va[name] = sizehint!(eltype(attrib)[], length(attrib)) + new_va[name] = sizehint!(eltype(values(attrib))[], length(attrib)) end vertex_index_counter = eltype(first(main_fs))(1) @@ -319,7 +314,7 @@ function clear_faceviews(mesh::Mesh) for name in keys(new_va) map = maps[something(findfirst(==(name), names), 1)] - append!(new_va[name], va[name][map]) + append!(new_va[name], values(va[name])[map]) end # add new faces and new view diff --git a/test/runtests.jl b/test/runtests.jl index c01f5154..b57b33e0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -44,7 +44,8 @@ end @test mesh.normal === normals @test mesh.position === points @test GeometryBasics.faces(mesh) === tfaces - @test propertynames(mesh) == (:vertex_attributes, :connectivity, :views, :position, :normal, :stress) + # TODO: Is the order variable with Dict? + @test propertynames(mesh) == (:vertex_attributes, :connectivity, :views, :normal, :position, :stress) end end From 84bb1d79c73ec786302ea635d59c7e6f376eed09 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 12 Sep 2024 16:04:36 +0200 Subject: [PATCH 059/120] remove shorthands --- src/basic_types.jl | 13 +------------ src/meshes.jl | 3 +-- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 9e98aa98..97072c6c 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -560,15 +560,4 @@ 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 - - -# Shorthand types -# used in meshes.jl -const SimpleMesh{N, T, FT} = Mesh{N, T, FT, Vector{FT}} -const SimpleTriangleMesh{N} = SimpleMesh{N, Float32, GLTriangleFace} -# -# 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 GLNormalMesh{N} = NormalMesh{N, Float32, GLTriangleFace} -# const GLNormalUVMesh{N} = NormalUVMesh{N, Float32, GLTriangleFace} \ No newline at end of file +Mesh(mesh::Mesh) = mesh \ No newline at end of file diff --git a/src/meshes.jl b/src/meshes.jl index fe4541dd..1f262ae3 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -108,8 +108,7 @@ end function triangle_mesh( primitive::Union{AbstractGeometry{N}, AbstractVector{<: Point{N}}}; pointtype = Point{N, Float32}, facetype = GLTriangleFace - )::SimpleTriangleMesh{N} where {N} # TODO: is the output type doing anything here? - + ) where {N} return mesh(primitive; pointtype = pointtype, facetype = facetype) end From 18fff4aa96fc4902d1e2d8ca4d17ead8bb2bca5a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 12 Sep 2024 16:16:16 +0200 Subject: [PATCH 060/120] export vertex_attributes and FaceView --- src/GeometryBasics.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index 8952af30..9f46b007 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -40,9 +40,9 @@ 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 Tesselation, Normal, UV, UVW -export AbstractMesh, Mesh, MetaMesh +export AbstractMesh, Mesh, MetaMesh, FaceView # all the different predefined mesh types From 7736e84fbd781b1a8ed21fb408b1dd382984cde0 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 12 Sep 2024 18:54:30 +0200 Subject: [PATCH 061/120] add center point to Circle to avoid shallow triangles --- src/primitives/spheres.jl | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/primitives/spheres.jl b/src/primitives/spheres.jl index 0eb2fea0..62949f25 100644 --- a/src/primitives/spheres.jl +++ b/src/primitives/spheres.jl @@ -42,15 +42,21 @@ 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+1)] + ps[end] = o + return ps end function texturecoordinates(::Circle, nvertices=64) return coordinates(Circle(Point2f(0.5), 0.5f0), nvertices) end +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) From 2015cf2381d1f92ba3d63266b2922c091458dec9 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 12 Sep 2024 20:18:00 +0200 Subject: [PATCH 062/120] make untesselated rect vertices counter-clockwise --- src/primitives/rectangles.jl | 29 +++++++++++++++++++---------- test/polygons.jl | 2 +- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index edfae378..a72c2028 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -544,21 +544,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 i in 1:(w - 1), j in 1:(h - 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) .* ps + return ps end function normals(rect::Rect2, nvertices=(2, 2)) diff --git a/test/polygons.jl b/test/polygons.jl index 898a6f30..811001de 100644 --- a/test/polygons.jl +++ b/test/polygons.jl @@ -1,6 +1,6 @@ @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(2)) polygon = Polygon(points) @test polygon == Polygon(points) end From 24e6bfbac474e9e9b9b0e0d7d26d0c1a9e3062c6 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 12 Sep 2024 20:39:22 +0200 Subject: [PATCH 063/120] fix tests --- src/primitives/rectangles.jl | 2 +- test/geometrytypes.jl | 4 ++-- test/runtests.jl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index a72c2028..e5c0a444 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -550,7 +550,7 @@ 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 [quad(i, j) for i in 1:(w - 1), j in 1:(h - 1)] + return [quad(i, j) for j in 1:(h - 1) for i in 1:(w - 1)] end end diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 9205c64f..aa3a71cd 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -81,7 +81,7 @@ 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 @@ -162,7 +162,7 @@ end @test f == face_target circle = Circle(Point2f(0), 1.0f0) points = decompose(Point2f, Tesselation(circle, 20)) - @test length(points) == 20 + @test length(points) == 21 tess_circle = Tesselation(circle, 32) mesh = triangle_mesh(tess_circle) @test decompose(Point2f, mesh) ≈ decompose(Point2f, tess_circle) diff --git a/test/runtests.jl b/test/runtests.jl index b57b33e0..c0717749 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -187,7 +187,7 @@ 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) From 81e5ce59f313f2c3c8ae6827992af6b3c84790fd Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 13 Sep 2024 15:52:04 +0200 Subject: [PATCH 064/120] fix Cylinder face windig direction --- src/primitives/cylinders.jl | 6 +++--- test/geometrytypes.jl | 11 ++++------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/primitives/cylinders.jl b/src/primitives/cylinders.jl index 5196e882..ce1c4aaa 100644 --- a/src/primitives/cylinders.jl +++ b/src/primitives/cylinders.jl @@ -73,7 +73,7 @@ function normals(c::Cylinder, nvertices = 30) ns[end] = R * Vec3f(0, 0, 1) disk1 = map(i -> GLTriangleFace(nhalf+1), 1:nhalf) - mantle = map(i -> QuadFace(i, i, mod1(i+1, nhalf), mod1(i+1, nhalf)), 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) @@ -84,10 +84,10 @@ function faces(::Cylinder, facets=30) nvertices = facets + isodd(facets) nhalf = div(nvertices, 2) - disk1 = map(i -> GLTriangleFace(nvertices+1, i, mod1(i+1, nhalf)), 1:nhalf) + 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, i+nhalf, i1 + nhalf, i1) + QuadFace(i, i1, i1 + nhalf, i+nhalf) end disk2 = map(i -> GLTriangleFace(nvertices+2, i+nhalf, mod1(i+1, nhalf)+nhalf), 1:nhalf) diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index aa3a71cd..d276f88c 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -35,13 +35,10 @@ using Test, GeometryBasics @test decompose(Point3{Float64}, Tesselation(s, 8)) ≈ positions - _faces = [ - TriangleFace(9, 1, 2), TriangleFace(9, 2, 3), TriangleFace(9, 3, 4), - TriangleFace(9, 4, 1), TriangleFace(1, 5, 6), TriangleFace(1, 6, 2), - TriangleFace(2, 6, 7), TriangleFace(2, 7, 3), TriangleFace(3, 7, 8), - TriangleFace(3, 8, 4), TriangleFace(4, 8, 5), TriangleFace(4, 5, 1), - TriangleFace(10, 5, 6), TriangleFace(10, 6, 7), TriangleFace(10, 7, 8), - TriangleFace(10, 8, 5)] + _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)) From 355ee0113b19f8a642ca61923b9a3ba93187eb49 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 13 Sep 2024 15:53:24 +0200 Subject: [PATCH 065/120] add `face_normals()` helepr function --- src/geometry_primitives.jl | 50 +++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/src/geometry_primitives.jl b/src/geometry_primitives.jl index 263db737..666cdcad 100644 --- a/src/geometry_primitives.jl +++ b/src/geometry_primitives.jl @@ -125,12 +125,14 @@ function orthogonal_vector(v1, v2, v3) end """ -``` -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}[, target_type = 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} @@ -139,6 +141,7 @@ end function normals(vertices::AbstractVector{<:Point{3}}, faces::AbstractVector{F}, ::Type{NormalType}) where {F<:NgonFace,NormalType} + normals_result = zeros(NormalType, length(vertices)) for face in faces v = vertices[face] @@ -152,3 +155,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 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. +""" +face_normals(primitive) = face_normals(coordinates(primitive), faces(primitive)) + +@generated function face_normals(positions::AbstractVector{<:Point{3}}, fs::AbstractVector{F}, + ::Type{NormalType} = Vec3f) 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 = sizehint!(Vec3f[], length(fs)) + faces = sizehint!(F[], length(fs)) + + for f in fs + ps = positions[f] + n = GeometryBasics.orthogonal_vector(ps[1], ps[2], ps[3]) + push!(normals, n) + push!(faces, $(FT)(length(normals))) + end + + return FaceView(normals, faces) + end +end From 9c039c6d0020f173934a05d3c978f8d05dd8edb1 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 13 Sep 2024 17:04:39 +0200 Subject: [PATCH 066/120] cleanup face_normals and normals a bit more --- src/geometry_primitives.jl | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/geometry_primitives.jl b/src/geometry_primitives.jl index 666cdcad..5e07b3ea 100644 --- a/src/geometry_primitives.jl +++ b/src/geometry_primitives.jl @@ -125,7 +125,7 @@ function orthogonal_vector(v1, v2, v3) end """ - normals(positions::Vector{Point3{T}}, faces::Vector{<: NgonFace}[, target_type = Vec3{T}]) + normals(positions::Vector{Point3{T}}, faces::Vector{<: NgonFace}[; normaltype = Vec3{T}]) Compute vertex normals from the given `positions` and `faces`. @@ -139,6 +139,10 @@ function normals(vertices::AbstractVector{Point{3,T}}, faces::AbstractVector{F}; return normals(vertices, faces, normaltype) end +function normals(primitive::GeometryPrimitive{3, T}; normaltype=Vec{3,T}) where {T} + return normals(coordinates(primitive), faces(primitive), normaltype) +end + function normals(vertices::AbstractVector{<:Point{3}}, faces::AbstractVector{F}, ::Type{NormalType}) where {F<:NgonFace,NormalType} @@ -167,10 +171,18 @@ 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. """ -face_normals(primitive) = face_normals(coordinates(primitive), faces(primitive)) +function face_normals(primitive::GeometryPrimitive{3, T}; normaltype = Vec{3, T}) where {T} + return face_normals(coordinates(primitive), faces(primitive), normaltype) +end -@generated function face_normals(positions::AbstractVector{<:Point{3}}, fs::AbstractVector{F}, - ::Type{NormalType} = Vec3f) where {F<:NgonFace,NormalType} +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 From 00af22e7674f0392756b904c8831be9b8a5fd9aa Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 13 Sep 2024 17:04:53 +0200 Subject: [PATCH 067/120] update tests for Cylinder --- test/geometrytypes.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index d276f88c..9267b4b7 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -62,8 +62,8 @@ using Test, GeometryBasics ], [ GLTriangleFace(5, 5, 5), GLTriangleFace(5, 5, 5), GLTriangleFace(5, 5, 5), GLTriangleFace(5, 5, 5), - QuadFace{Int64}(1, 1, 2, 2), QuadFace{Int64}(2, 2, 3, 3), - QuadFace{Int64}(3, 3, 4, 4), QuadFace{Int64}(4, 4, 1, 1), + 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) ] From 075dc44f7027597a1f4dda1e78f21ab69371d429 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 13 Sep 2024 17:19:51 +0200 Subject: [PATCH 068/120] rename connectivity -> faces --- src/basic_types.jl | 8 ++++---- test/runtests.jl | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 97072c6c..9fab168d 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -392,7 +392,7 @@ struct Mesh{ } <: AbstractMesh{Dim, T} vertex_attributes::Dict{Symbol, VertexAttributeType} - connectivity::FVT + faces::FVT views::Vector{UnitRange{Int}} function Mesh( @@ -457,13 +457,13 @@ end end coordinates(mesh::Mesh) = mesh.position -faces(mesh::Mesh) = mesh.connectivity +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) = getfield(mesh, :vertex_attributes) -Base.getindex(mesh::Mesh, i::Integer) = mesh.position[mesh.connectivity[i]] -Base.length(mesh::Mesh) = length(mesh.connectivity) +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) && diff --git a/test/runtests.jl b/test/runtests.jl index c0717749..072e1668 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -45,7 +45,7 @@ end @test mesh.position === points @test GeometryBasics.faces(mesh) === tfaces # TODO: Is the order variable with Dict? - @test propertynames(mesh) == (:vertex_attributes, :connectivity, :views, :normal, :position, :stress) + @test propertynames(mesh) == (:vertex_attributes, :faces, :views, :normal, :position, :stress) end end From 6d3384f2c8f113bfa7fc848158add3a3a961cf71 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 13 Sep 2024 18:12:03 +0200 Subject: [PATCH 069/120] update docs (meshes, primitives, decompose, Point, Vec, Mat) --- docs/make.jl | 3 +- docs/src/decomposition.md | 105 ++++--------------- docs/src/implementation.md | 5 - docs/src/index.md | 85 ++------------- docs/src/meshes.md | 98 +++++++++++++++--- docs/src/metadata.md | 153 --------------------------- docs/src/primitives.md | 183 +++++++++++++++++++++++++++++++-- docs/src/rectangles.md | 1 - docs/src/static_array_types.md | 38 +++++++ 9 files changed, 322 insertions(+), 349 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 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..90d679a2 100644 --- a/docs/src/decomposition.md +++ b/docs/src/decomposition.md @@ -1,89 +1,20 @@ # 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. + +## 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..effbc3db 100644 --- a/docs/src/meshes.md +++ b/docs/src/meshes.md @@ -1,24 +1,98 @@ # Meshes -## Types +GeometryBasics defines two mesh types to work with - `Mesh` and `MetaMesh` -* [`AbstractMesh`](@ref) -* [`Mesh`](@ref) +## Mesh + +A `Mesh` is defined by the following struct: + +```julia +struct Mesh{ + Dim, PositionElType <: Real, + FT <: AbstractFace, + FVT <: AbstractVector{FT} + } <: AbstractMesh{Dim, T} + + vertex_attributes::Dict{Symbol, Union{FaceView, AbstractVector}} + faces::FVT + views::Vector{UnitRange{Int}} +end +``` + +`mesh.vertex_attributes` contains all the per-vertex data like positions, normals, texture coordinates, etc. +They must always contain at least positions. +`mesh.faces` defines a set of (default) faces to use for each vertex attribute. +If a vertex attribute is a `FaceView` it carries its own set of faces which are used instead of `mesh.faces`. +In this case the number of faces and length of each face must match between them. +`mesh.views` is a set of views into the `mesh.faces` which can be used to separate the mesh into smaller submeshes. + +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 `GeometryBasics.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..d8b49def 100644 --- a/docs/src/primitives.md +++ b/docs/src/primitives.md @@ -1,17 +1,180 @@ # 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 +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` | From 6c38a0d730b56dcfa2d5fb02d235237758fe9a33 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 13 Sep 2024 22:50:30 +0200 Subject: [PATCH 070/120] add/update docstrings --- src/basic_types.jl | 129 ++++++++++++++++++++++++++++------ src/boundingboxes.jl | 9 ++- src/fixed_arrays.jl | 46 +++++++++++++ src/geometry_primitives.jl | 16 +++-- src/interfaces.jl | 61 ++++++++++++++--- src/mat.jl | 22 ++++++ src/meshes.jl | 137 ++++++++++++++++++++++++++----------- src/primitives/pyramids.jl | 7 ++ 8 files changed, 349 insertions(+), 78 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 9fab168d..f3471802 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -1,22 +1,29 @@ """ -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, T} <: StaticVector{N, T} + AbstractFace{N_indices, T} <: StaticVector{N_indices, T} -Parent type for all faces. You should inherit from one of the child types instead. +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 @@ -37,12 +44,20 @@ 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} @@ -74,12 +89,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}} @@ -99,6 +118,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}}, @@ -107,9 +128,11 @@ 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{N}}, P::Type{Point{NDim,T}}) where {N,NDim,T} return Ngon{NDim,T,N} end @@ -139,6 +162,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. @@ -174,6 +199,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} @@ -181,6 +208,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} @@ -193,6 +222,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}} @@ -242,6 +273,8 @@ end """ MultiPolygon(polygons::AbstractPolygon) + +A collection of polygons """ struct MultiPolygon{Dim, T<:Real} <: AbstractGeometry{Dim, T} polygons::Vector{<:AbstractPolygon{Dim,T}} @@ -257,7 +290,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}} @@ -295,7 +329,17 @@ Base.size(mpt::MultiPoint) = size(mpt.points) Base.length(mpt::MultiPoint) = length(mpt.points) +""" + FaceView(data, faces) + +A FaceView is alternative to passing a vertex attribute directly to a mesh. It +bundles `data` with a different set of `faces` which replace the default faces +of a mesh. Doing this allows you to have the `data` in a different order from +vertices in the mesh, potentially avoiding duplication. +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 @@ -312,7 +356,6 @@ function Base.vcat(a::FaceView, b::FaceView) end faces(x::FaceView) = x.faces -# attribute(x::FaceView) = x.data Base.values(x::FaceView) = x.data facetype(x::FaceView) = eltype(x.faces) Base.getindex(x::FaceView, f::AbstractFace) = getindex(values(x), f) @@ -375,15 +418,16 @@ 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{D, T, FaceType, FaceArrayType} <: AbstractMesh{D, T} <: AbstractGeometry{D, T} + +The type of a concrete mesh. It can hold arbitrary vertex data (including FaceView's). """ struct Mesh{ Dim, T <: Real, # TODO: Number? @@ -460,6 +504,13 @@ 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]] @@ -489,6 +540,28 @@ function add_vertex_attribute!(mesh::Mesh, val::FaceView, name::Symbol) return mesh 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(Dict{Symbol, VertexAttributeType}(attributes), faces, views) end @@ -513,12 +586,28 @@ function Mesh(; kwargs...) return Mesh(va, fs) end - struct MetaMesh{Dim, T, M <: AbstractMesh{Dim, T}} <: AbstractMesh{Dim, T} mesh::M meta::Dict{Symbol, Any} end + +""" + 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 @@ -527,7 +616,7 @@ function MetaMesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Abstr MetaMesh(Mesh(points, faces), Dict{Symbol, Any}(kwargs)) end -# TODO: Do we want to access meta here? + @inline function Base.hasproperty(mesh::MetaMesh, field::Symbol) return hasfield(MetaMesh, field) || hasproperty(getfield(mesh, :mesh), field) end diff --git a/src/boundingboxes.jl b/src/boundingboxes.jl index e2deeb32..958a4609 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) @@ -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..34fb494f 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -3,6 +3,12 @@ using LinearAlgebra import Random import Base: setindex +""" + StaticVector{N, T} <: AbstractVector{T} + +An abstract type for GeometryBasics statically sized Vectors. These are used +for Point, Vec and Face types. +""" abstract type StaticVector{N, T} <: AbstractVector{T} end function similar_type end @@ -285,3 +291,43 @@ 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 \ No newline at end of file diff --git a/src/geometry_primitives.jl b/src/geometry_primitives.jl index 5e07b3ea..2618a63c 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 @@ -116,13 +121,12 @@ function collect_with_eltype!(result::AbstractVector{T}, iter) where {T} 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) """ normals(positions::Vector{Point3{T}}, faces::Vector{<: NgonFace}[; normaltype = Vec3{T}]) diff --git a/src/interfaces.jl b/src/interfaces.jl index 92a7b0cf..6d41df82 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) diff --git a/src/mat.jl b/src/mat.jl index a638d4df..e450b520 100644 --- a/src/mat.jl +++ b/src/mat.jl @@ -1,6 +1,12 @@ import LinearAlgebra: inv, det import Random +""" + Mat{R, C, T, L} <: AbstractMatrix{T} + +GeometryBasics type for statically sized matrices. `R` is the number of rows, +`C` columns, T the eltype and `L = R * C` the total number of elements. +""" 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} @@ -57,6 +63,22 @@ Base.values(m::Mat) = m.values Base.copy(mat::Mat) = deepcopy(mat) +""" + 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` | +""" function Mat{C, R, T}(::LinearAlgebra.UniformScaling) where {C, R, T} idx = CartesianIndices((R, C)) data = ntuple(C * R) do i diff --git a/src/meshes.jl b/src/meshes.jl index 1f262ae3..4d0ad54e 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -1,15 +1,15 @@ """ - 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, vertex_attributes...) positions = decompose(pointtype, primitive) @@ -44,9 +44,9 @@ end Creates a mesh from the given positions and faces. Other vertex attributes like normals and texture coordinates can be added as keyword arguments. -By default the generated mesh uses `GLTriangleFace`. If the input faces are of -type `MultiFace` they will get converted appropriately, which may cause -reordering and duplication of positions and other vertex attributes. +Note that vertex attributes that are `nothing` get removed before creating a mesh. + +See also:[`normal_mesh`](@ref) """ function mesh( positions::AbstractVector{<:Point}, @@ -66,6 +66,14 @@ function mesh( return Mesh(positions, fs; va...) end +""" + 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, @@ -96,8 +104,7 @@ function mesh( end """ - mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace, - normaltype=nothing) + mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace) Create a mesh from a polygon given as a vector of points, using triangulation. """ @@ -105,6 +112,14 @@ function mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace) return mesh(Polygon(polygon); pointtype=pointtype, facetype=facetype) end +""" + 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 @@ -112,21 +127,17 @@ function triangle_mesh( 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 -function triangle_mesh(primitive::Mesh{N}; pointtype = Point{N, Float32}) where {N} - # already target type: - if GeometryBasics.pointtype(primitive) === pointtype && - GeometryBasics.facetype(primitive) === GLTriangleFace - return primitive - else - return mesh(primitive; pointtype = pointtype, facetype = GLTriangleFace) - end -end +""" + uv_mesh(primitive::GeometryPrimitive{N}[; pointtype = Point{N, Float32}, facetype = GLTriangleFace, uvtype = Vec2f]) +Creates a triangle mesh with texture coordinates from a given `primitive`. The +`pointtype`, `facetype` and `uvtype` are set by the correspondering keyword arguments. +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 @@ -138,6 +149,15 @@ function uv_mesh( ) end +""" + 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 @@ -150,6 +170,15 @@ function uv_normal_mesh( ) end +""" + 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 @@ -159,6 +188,14 @@ function normal_mesh( return Mesh(_faces, position = _points, normal = normals(_points, _faces, normaltype)) end +""" + 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 @@ -169,29 +206,40 @@ function normal_mesh( pointtype = pointtype, facetype = facetype) end -""" - volume(triangle) -Calculate the signed volume of one tetrahedron. Be sure the orientation of your -surface is right. -""" -function volume(triangle::Triangle) - v1, v2, v3 = triangle - sig = sign(orthogonal_vector(v1, v2, v3) ⋅ v1) - return sig * abs(v1 ⋅ (v2 × v3)) / 6 -end +# TODO: What are these doing here? Shouldn't they be defined on a Tetrahedron if +# they are for tetrahedron? +# """ +# volume(triangle) + +# Calculate the signed volume of one tetrahedron. Be sure the orientation of your +# surface is right. +# """ +# function volume(triangle::Triangle) +# v1, v2, v3 = triangle +# sig = sign(orthogonal_vector(v1, v2, v3) ⋅ v1) +# return sig * abs(v1 ⋅ (v2 × v3)) / 6 +# end + +# """ +# volume(mesh) + +# Calculate the signed volume of all tetrahedra. Be sure the orientation of your +# surface is right. +# """ +# function volume(mesh::Mesh) +# return sum(volume, mesh) +# end -""" - volume(mesh) -Calculate the signed volume of all tetrahedra. Be sure the orientation of your -surface is right. """ -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)`. +""" function Base.merge(meshes::AbstractVector{<:Mesh}) return if isempty(meshes) return Mesh(Point3f[], GLTriangleFace[]) @@ -271,6 +319,13 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) 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. +""" function clear_faceviews(mesh::Mesh) main_fs = faces(mesh) va = vertex_attributes(mesh) diff --git a/src/primitives/pyramids.jl b/src/primitives/pyramids.jl index 0a288729..cfd89527 100644 --- a/src/primitives/pyramids.jl +++ b/src/primitives/pyramids.jl @@ -1,3 +1,10 @@ +""" + 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 From c3bb4b2107eaed9473d4d0e76b5b5e0f61a5df6e Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 14 Sep 2024 01:51:48 +0200 Subject: [PATCH 071/120] add quick test for face_normals() --- test/runtests.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 072e1668..3d497167 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -286,6 +286,12 @@ end @test <(x, x1) end +@testset "Face normals" begin + r = Rect3f(Point3f(0), Vec3f(1)) + ns = normals(r) + @test GeometryBasics.face_normals(r) == ns +end + @testset "Tests from GeometryTypes" begin include("geometrytypes.jl") end From bfaed13c9a7aae8bdcc4cd8bb670f3118962524c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 14 Sep 2024 14:13:38 +0200 Subject: [PATCH 072/120] fix TetrahedronFace conversions --- src/geometry_primitives.jl | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/geometry_primitives.jl b/src/geometry_primitives.jl index 2618a63c..73cc4793 100644 --- a/src/geometry_primitives.jl +++ b/src/geometry_primitives.jl @@ -43,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) @@ -61,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 From 51a008d9cffb0be05b40c0f1ff4bbeb83c19e32b Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 14 Sep 2024 14:21:38 +0200 Subject: [PATCH 073/120] restore volume functions --- src/meshes.jl | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/meshes.jl b/src/meshes.jl index 4d0ad54e..567b417f 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -207,29 +207,27 @@ function normal_mesh( end -# TODO: What are these doing here? Shouldn't they be defined on a Tetrahedron if -# they are for tetrahedron? -# """ -# volume(triangle) - -# Calculate the signed volume of one tetrahedron. Be sure the orientation of your -# surface is right. -# """ -# function volume(triangle::Triangle) -# v1, v2, v3 = triangle -# sig = sign(orthogonal_vector(v1, v2, v3) ⋅ v1) -# return sig * abs(v1 ⋅ (v2 × v3)) / 6 -# end - -# """ -# volume(mesh) - -# Calculate the signed volume of all tetrahedra. Be sure the orientation of your -# surface is right. -# """ -# function volume(mesh::Mesh) -# return sum(volume, mesh) -# end +""" + volume(triangle) + +Calculate the signed volume of one tetrahedron. Be sure the orientation of your +surface is right. +""" +function volume(triangle::Triangle) + v1, v2, v3 = triangle + sig = sign(orthogonal_vector(v1, v2, v3) ⋅ v1) + return sig * abs(v1 ⋅ (v2 × v3)) / 6 +end + +""" + volume(mesh) + +Calculate the signed volume of all tetrahedra. Be sure the orientation of your +surface is right. +""" +function volume(mesh::Mesh) + return sum(volume, mesh) +end """ From ad90d817b5c56dcbbbf78ae732e0331ccf1c339e Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 14 Sep 2024 15:11:15 +0200 Subject: [PATCH 074/120] fix tests --- src/basic_types.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index f3471802..ab4afc52 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -132,7 +132,10 @@ end The fully concrete Ngon type, when constructed from a point type! """ -function Polytope(::Type{<:Ngon{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 From 05c036b2705f4d363e41bdba0fb73c41005d86d5 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 14 Sep 2024 15:11:32 +0200 Subject: [PATCH 075/120] add some Polygon tests --- test/polygons.jl | 42 ++++++++++++++++++++++++------------------ test/runtests.jl | 2 ++ 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/test/polygons.jl b/test/polygons.jl index 811001de..ce8bb932 100644 --- a/test/polygons.jl +++ b/test/polygons.jl @@ -1,28 +1,34 @@ @testset "Polygon" begin @testset "from points" begin - points = connect([1, 2, 3, 4, 5, 6], Point2f(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) + fs = GLTriangleFace[ + (1, 9, 8), (5, 13, 12), (10, 9, 1), (4, 1, 8), (11, 10, 1), (3, 4, 8), + (12, 11, 1), (3, 8, 7), (12, 1, 2), (3, 7, 6), (5, 12, 2), (2, 3, 6), (6, 5, 2)] + @test fs == faces(poly1) + ps = vcat(decompose(Point2f, rect), decompose(Point2f, hole)) + @test coordinates(poly1) == ps -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)] -] + fs = GeometryBasics.earcut_triangulate([poly1.exterior[[1, 2, 3, 4, 1]]]) + @test fs == GLTriangleFace[(4,1,2), (2,3,4)] -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 3d497167..2f7a59f1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -308,6 +308,8 @@ end include("geointerface.jl") end +include("polygons.jl") + using Aqua # Aqua tests # Intervals brings a bunch of ambiquities unfortunately From e76dee1898c74695c69464fd15f935f6f1cf0db5 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 14 Sep 2024 15:11:42 +0200 Subject: [PATCH 076/120] test Pyramids --- test/geometrytypes.jl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 9267b4b7..99909b12 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -104,6 +104,15 @@ end @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 From 4ea085bf5f9651645aaa8037c9d6f6d0318c5d8b Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 14 Sep 2024 15:12:01 +0200 Subject: [PATCH 077/120] test TetrahedronFace decomposition --- test/runtests.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 2f7a59f1..6986565c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -131,7 +131,10 @@ 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) From 1c06df4cefbc0331067a3d5e20770abc01b433da Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 14 Sep 2024 15:36:05 +0200 Subject: [PATCH 078/120] test and improve Mesh validation --- src/basic_types.jl | 20 +++++++++++--------- test/meshes.jl | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index ab4afc52..86de2e57 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -376,14 +376,19 @@ function convert_facetype(::Type{FT}, x::FaceView) where {FT <: AbstractFace} 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 - if length(faces(fv)) != length(fs) - error("Number of faces given in FaceView $(length(faces(fv))) does not match reference $(length(fs))") - 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))") @@ -455,20 +460,17 @@ struct Mesh{ 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" - va[:normal] = pop!(va, :normal) + va[:normal] = pop!(va, :normals) end # verify that faces of FaceViews match `fs` (in length per face) N = maximum(f -> value(maximum(f)), fs, init = 0) for (name, attrib) in va - name == :position && continue - if attrib isa FaceView try verify(fs, attrib) catch e - @error "Failed to verify $name attribute:" - rethrow(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.") diff --git a/test/meshes.jl b/test/meshes.jl index 20101ce5..fd518b4c 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -101,6 +101,49 @@ end @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_warn msg Mesh(ps, fs, normals = ns) + end +end + @testset "mesh() constructors" begin r = Rect3d(Point3d(-1), Vec3d(2)) From 8d17e144dfa20f6881741a8329967492655a2bf6 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 14 Sep 2024 15:55:26 +0200 Subject: [PATCH 079/120] test Mesh inteface functions --- src/basic_types.jl | 2 ++ test/meshes.jl | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/src/basic_types.jl b/src/basic_types.jl index 86de2e57..e9c5303a 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -640,6 +640,8 @@ end Base.haskey(mesh::MetaMesh, key::Symbol) = haskey(getfield(mesh, :meta), key) Base.get(f, mesh::MetaMesh, key::Symbol) = get(f, getfield(mesh, :meta), key) Base.get!(f, mesh::MetaMesh, key::Symbol) = get!(f, getfield(mesh, :meta), key) +Base.get(mesh::MetaMesh, key::Symbol, default = nothing) = get(getfield(mesh, :meta), key, default) +Base.get!(mesh::MetaMesh, key::Symbol, default = nothing) = 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) diff --git a/test/meshes.jl b/test/meshes.jl index fd518b4c..ed6ffb92 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -144,6 +144,50 @@ end 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) == "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)) From 0757e58cd5009a327de5fe1bcd7f79e2185c434f Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 14 Sep 2024 15:58:48 +0200 Subject: [PATCH 080/120] test decompose with views --- src/interfaces.jl | 2 +- test/runtests.jl | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/interfaces.jl b/src/interfaces.jl index 6d41df82..52f8a2c1 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -162,7 +162,7 @@ function decompose(::Type{F}, f::AbstractVector, views::Vector{UnitRange{Int}}) return collect_with_eltype(F, fs), views else output = F[] - new_views = UnitRange{Int}[] + new_views = sizehint!(UnitRange{Int}[], length(views)) for range in views start = length(output) + 1 collect_with_eltype!(output, view(fs, range)) diff --git a/test/runtests.jl b/test/runtests.jl index 6986565c..549300d3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -201,6 +201,12 @@ end 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 From 728d349fb9ce4063f70a271c2db1428e321b70d7 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 14 Sep 2024 16:11:57 +0200 Subject: [PATCH 081/120] test and fix matrix det, inv, transpose, mat*vec --- src/fixed_arrays.jl | 7 +++++++ src/mat.jl | 4 ++-- test/fixed_arrays.jl | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index 34fb494f..b754ec08 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -41,6 +41,13 @@ macro fixed_vector(name_parent) return $(VecT){N,T1}(ntuple(i -> convert(T1, x[i]), N)) end + # fix ambiguity + $(VecT){1}(x::AbstractVector{T}) where {T} = $(VecT){1,T}(x) + function $(VecT){1,T1}(x::AbstractVector{T2}) where {T1,T2} + @assert 1 <= length(x) + return $(VecT){1,T1}((x[1],)) + 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) diff --git a/src/mat.jl b/src/mat.jl index e450b520..4e1ff25e 100644 --- a/src/mat.jl +++ b/src/mat.jl @@ -165,8 +165,8 @@ 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) + 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} diff --git a/test/fixed_arrays.jl b/test/fixed_arrays.jl index cf82d0a3..478c4d53 100644 --- a/test/fixed_arrays.jl +++ b/test/fixed_arrays.jl @@ -130,4 +130,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 From d6794490f09bd2e89296af0282dde6b7273f2cdf Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 14 Sep 2024 16:20:54 +0200 Subject: [PATCH 082/120] fix tests --- test/meshes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/meshes.jl b/test/meshes.jl index ed6ffb92..d364775f 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -140,7 +140,7 @@ end @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_warn msg Mesh(ps, fs, normals = ns) + @test_logs (:warn, msg) Mesh(ps, fs, normals = ns) end end From d39d1dc7a9166ed0a803f0a3dd26ae577ba511ec Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sat, 14 Sep 2024 16:37:25 +0200 Subject: [PATCH 083/120] cleanup normal gen and export face_normals --- src/GeometryBasics.jl | 1 + src/geometry_primitives.jl | 16 ++-------------- test/runtests.jl | 2 +- 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index 9f46b007..aaf71ca2 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -41,6 +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 face_normals export Tesselation, Normal, UV, UVW export AbstractMesh, Mesh, MetaMesh, FaceView diff --git a/src/geometry_primitives.jl b/src/geometry_primitives.jl index 73cc4793..bbc4917a 100644 --- a/src/geometry_primitives.jl +++ b/src/geometry_primitives.jl @@ -160,10 +160,6 @@ function normals(vertices::AbstractVector{Point{3,T}}, faces::AbstractVector{F}; return normals(vertices, faces, normaltype) end -function normals(primitive::GeometryPrimitive{3, T}; normaltype=Vec{3,T}) where {T} - return normals(coordinates(primitive), faces(primitive), normaltype) -end - function normals(vertices::AbstractVector{<:Point{3}}, faces::AbstractVector{F}, ::Type{NormalType}) where {F<:NgonFace,NormalType} @@ -185,17 +181,9 @@ end """ face_normals(positions::Vector{Point3{T}}, faces::Vector{<: NgonFace}[, target_type = 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. +Compute face normals from the given `positions` and `faces` and returns an +appropriate `FaceView`. """ -function face_normals(primitive::GeometryPrimitive{3, T}; normaltype = Vec{3, T}) where {T} - return face_normals(coordinates(primitive), faces(primitive), normaltype) -end - function face_normals( positions::AbstractVector{<:Point3{T}}, fs::AbstractVector{<: AbstractFace}; normaltype = Vec3{T}) where {T} diff --git a/test/runtests.jl b/test/runtests.jl index 549300d3..eb65c392 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -298,7 +298,7 @@ end @testset "Face normals" begin r = Rect3f(Point3f(0), Vec3f(1)) ns = normals(r) - @test GeometryBasics.face_normals(r) == ns + @test GeometryBasics.face_normals(coordinates(r), faces(r)) == ns end @testset "Tests from GeometryTypes" begin From 6402d7532266916667005e489f55c279cb463bfe Mon Sep 17 00:00:00 2001 From: ffreyer Date: Sun, 15 Sep 2024 17:13:04 +0200 Subject: [PATCH 084/120] add util for splitting meshes by views --- src/meshes.jl | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/meshes.jl b/src/meshes.jl index 567b417f..7fee239a 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -437,6 +437,30 @@ function merge_vertex_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),)) + new_va = Dict{Symbol, VertexAttributeType}() + + for (k, v) in vertex_attributes(mesh) + if v isa FaceView + _fs, _maps = merge_vertex_indices((view(faces(v), idxs),)) + new_va[k] = FaceView(values(v)[_maps[1]], _fs) + else + new_va[k] = v[maps[1]] + end + end + + return Mesh(new_va, new_fs) + end +end + function map_coordinates(f, mesh::Mesh) result = copy(mesh) From 033de56982625f3a7d90adefaa66a990b6306d25 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 16 Sep 2024 01:48:14 +0200 Subject: [PATCH 085/120] fix missing dot in range .+ idx --- src/meshes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/meshes.jl b/src/meshes.jl index 7fee239a..afee8f38 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -298,7 +298,7 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) if isempty(mesh.views) push!(views, idx+1 : idx+N) else - append!(views, (view + idx for view in mesh.views)) + append!(views, (view .+ idx for view in mesh.views)) end idx += N From 293a154d9121cb90d203d09a8c5db85c5a669c2d Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 16 Sep 2024 03:09:32 +0200 Subject: [PATCH 086/120] improve performance of merge --- src/meshes.jl | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/meshes.jl b/src/meshes.jl index afee8f38..ab58ca7e 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -240,7 +240,7 @@ Otherwise all of them will be converted with `clear_faceviews(mesh)`. """ function Base.merge(meshes::AbstractVector{<:Mesh}) return if isempty(meshes) - return Mesh(Point3f[], GLTriangleFace[]) + return Mesh{3, Float32, GLTriangleFace, Vector{GLTriangleFace}}(Point3f[], GLTriangleFace[]) elseif length(meshes) == 1 return meshes[1] @@ -275,34 +275,36 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) if consistent_face_views # All the same kind of face, can just merge - new_attribs = Dict{Symbol, VertexAttributeType}(map(collect(names)) do name - return name => mapreduce(m -> getproperty(m, name), vcat, meshes) + return name => reduce(vcat, getproperty.(meshes, name)) 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) ? [1:idx] : copy(m1.views) + offset = length(coordinates(m1))::Int + views = isempty(m1.views) ? UnitRange{Int64}[1:idx] : copy(m1.views) - for mesh in Iterators.drop(meshes, 1) + Ns = length.(faces.(meshes)) + Ms = length.(coordinates.(meshes))::Vector{Int} + for (mesh, N, M) in Iterators.drop(zip(meshes, Ns, Ms), 1) # update face indices - N = length(faces(mesh)) for i = idx .+ (1:N) # TODO: face + Int changes type to Int - fs[i] = typeof(fs[i])(fs[i] + offset) + fs[i] = typeof(fs[i])(fs[i] .+ offset) end # add views if isempty(mesh.views) push!(views, idx+1 : idx+N) else - append!(views, (view .+ idx for view in mesh.views)) + for view in mesh.views + push!(views, view .+ idx) + end end idx += N - offset += length(coordinates(mesh)) + offset += M end return Mesh(new_attribs, fs, views) From e9c83ec6ee4ce53f8697a76eff95427f4c8276e5 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 16 Sep 2024 04:04:44 +0200 Subject: [PATCH 087/120] fix tests --- src/meshes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/meshes.jl b/src/meshes.jl index ab58ca7e..ec3dadfc 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -240,7 +240,7 @@ Otherwise all of them will be converted with `clear_faceviews(mesh)`. """ function Base.merge(meshes::AbstractVector{<:Mesh}) return if isempty(meshes) - return Mesh{3, Float32, GLTriangleFace, Vector{GLTriangleFace}}(Point3f[], GLTriangleFace[]) + return Mesh(Point3f[], GLTriangleFace[]) elseif length(meshes) == 1 return meshes[1] From 5491a7290c9b3db295918f37f04b0408238f588e Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 16 Sep 2024 04:12:18 +0200 Subject: [PATCH 088/120] improve GeoInterface conversion performance --- src/geointerface.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 From fae59cdb63908b7d05056b16c5f445b221d2e5f9 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 16 Sep 2024 13:56:34 +0200 Subject: [PATCH 089/120] switch back to NamedTuple for performance --- src/basic_types.jl | 57 +++++++++++++++--------------------- src/meshes.jl | 73 ++++++++++++++++++++++------------------------ test/runtests.jl | 2 +- 3 files changed, 60 insertions(+), 72 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index e9c5303a..fae1afc2 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -440,18 +440,26 @@ The type of a concrete mesh. It can hold arbitrary vertex data (including FaceVi struct Mesh{ Dim, T <: Real, # TODO: Number? FT <: AbstractFace, + Names, + VAT <: Tuple{<: AbstractVector{Point{Dim, T}}, Vararg{VertexAttributeType}}, FVT <: AbstractVector{FT} } <: AbstractMesh{Dim, T} - vertex_attributes::Dict{Symbol, VertexAttributeType} + vertex_attributes::NamedTuple{Names, VAT} faces::FVT views::Vector{UnitRange{Int}} function Mesh( - va::Dict{Symbol, VertexAttributeType}, + vertex_attributes::NamedTuple{Names, VAT}, fs::FVT, views::Vector{UnitRange{Int}} = UnitRange{Int}[] - ) where {FT <: AbstractFace, FVT <: AbstractVector{FT}} + ) 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 ) @@ -460,12 +468,13 @@ struct Mesh{ 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" - va[:normal] = pop!(va, :normals) + names = ntuple(i -> ifelse(names[i] == :normal, :normal, names[i]), length(names)) + va = NamedTuple{names}(values(va)) end # verify that faces of FaceViews match `fs` (in length per face) N = maximum(f -> value(maximum(f)), fs, init = 0) - for (name, attrib) in va + for (name, attrib) in pairs(va) if attrib isa FaceView try verify(fs, attrib) @@ -477,10 +486,7 @@ struct Mesh{ end end - D = length(eltype(va[:position])) - T = eltype(eltype(va[:position])) - - return new{D, T, FT, FVT}(va, fs, views) + return new{Dim, T, FT, names, VAT, FVT}(va, fs, views) end end @@ -489,7 +495,7 @@ end @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 haskey(getfield(mesh, :vertex_attributes), field) || hasfield(Mesh, field) + return hasproperty(getfield(mesh, :vertex_attributes), field) || hasfield(Mesh, field) end @inline function Base.getproperty(mesh::Mesh, field::Symbol) if hasfield(Mesh, field) @@ -498,11 +504,11 @@ end @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 getindex(getfield(mesh, :vertex_attributes), field) + return getproperty(getfield(mesh, :vertex_attributes), field) end end @inline function Base.propertynames(mesh::Mesh) - return (fieldnames(Mesh)..., keys(getfield(mesh, :vertex_attributes))...) + return (fieldnames(Mesh)..., propertynames(getfield(mesh, :vertex_attributes))...) end coordinates(mesh::Mesh) = mesh.position @@ -530,21 +536,6 @@ function Base.iterate(mesh::Mesh, i=1) return i - 1 < length(mesh) ? (mesh[i], i + 1) : nothing end -function add_vertex_attribute!(mesh::Mesh, val::AbstractVector, name::Symbol) - @boundscheck begin - N = maximum(f -> value(maximum(f)), faces(mesh)) - length(val) < N && error("Given vertex data not large enough to be adressed by all faces ($N required, $(length(val)) given)") - end - mesh.vertex_attributes[name] = val - return mesh -end - -function add_vertex_attribute!(mesh::Mesh, val::FaceView, name::Symbol) - @boundscheck verify(faces(mesh), val) - mesh.vertex_attributes[name] = val - return mesh -end - """ Mesh(faces[; views, attributes...]) Mesh(positions, faces[; views]) @@ -568,14 +559,13 @@ 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(Dict{Symbol, VertexAttributeType}(attributes), faces, views) + return Mesh(NamedTuple(attributes), faces, views) end function Mesh(points::AbstractVector{Point{Dim, T}}, faces::AbstractVector{<:AbstractFace}; views = UnitRange{Int}[], kwargs...) where {Dim, T} - va = Dict{Symbol, VertexAttributeType}(kwargs) - va[:position] = points + va = (position = points, kwargs...) return Mesh(va, faces, views) end @@ -585,9 +575,10 @@ function Mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Integer}, end function Mesh(; kwargs...) - va = Dict{Symbol, VertexAttributeType}(kwargs) - fs = faces(va[:position]::FaceView) - va[:position] = values(va[:position]) + 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 diff --git a/src/meshes.jl b/src/meshes.jl index ec3dadfc..6ed9fbe3 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -56,12 +56,10 @@ function mesh( fs = decompose(facetype, faces) - va = Dict{Symbol, VertexAttributeType}() - for k in keys(vertex_attributes) - if !isnothing(vertex_attributes[k]) - va[k] = convert_facetype(facetype, vertex_attributes[k]) - end - end + 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 @@ -80,22 +78,22 @@ function mesh( attributes... ) where {D, T, FT <: AbstractFace} - va = Dict{Symbol, VertexAttributeType}() - for k in keys(attributes) - isnothing(attributes[k]) || setindex!(va, attributes[k], k) - end + names = keys(attributes) + valid_names = filter(name -> !isnothing(attributes[name]), names) - if isempty(va) && (GeometryBasics.pointtype(mesh) == pointtype) && (FT == facetype) + if isempty(valid_names) && (GeometryBasics.pointtype(mesh) == pointtype) && (FT == facetype) return mesh else - # 1. add vertex attributes, 2. convert position attribute + vals = getindex.(Ref(attributes), valid_names) + va = NamedTuple{valid_names}(vals) + + # add vertex attributes va = merge(vertex_attributes(mesh), va) - va[:position] = decompose(pointtype, va[:position]) - - # Resolve facetype conversions of FaceViews - for (k, v) in va - va[k] = convert_facetype(facetype, v) - end + # 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) @@ -275,18 +273,18 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) if consistent_face_views # All the same kind of face, can just merge - new_attribs = Dict{Symbol, VertexAttributeType}(map(collect(names)) do name - return name => reduce(vcat, getproperty.(meshes, name)) + new_attribs = NamedTuple{names}(map(names) do name + return reduce(vcat, getproperty.(meshes, name)) end) fs = reduce(vcat, faces.(meshes)) # TODO: is the type difference in offset bad? idx = length(faces(m1)) - offset = length(coordinates(m1))::Int + offset = length(coordinates(m1))::Int # TODO: unnecessary views = isempty(m1.views) ? UnitRange{Int64}[1:idx] : copy(m1.views) Ns = length.(faces.(meshes)) - Ms = length.(coordinates.(meshes))::Vector{Int} + Ms = length.(coordinates.(meshes))::Vector{Int} # TODO: unnecessary for (mesh, N, M) in Iterators.drop(zip(meshes, Ns, Ms), 1) # update face indices for i = idx .+ (1:N) @@ -330,11 +328,11 @@ function clear_faceviews(mesh::Mesh) main_fs = faces(mesh) va = vertex_attributes(mesh) - names = filter(name -> va[name] isa FaceView, collect(keys(va))) + names = filter(name -> va[name] isa FaceView, keys(va)) isempty(names) && return mesh other_fs = faces.(getproperty.((mesh,), names)) - pushfirst!(names, :position) + names = (:position, names...) all_fs = tuple(main_fs, other_fs...) if isempty(mesh.views) @@ -343,10 +341,9 @@ function clear_faceviews(mesh::Mesh) named_maps = NamedTuple{tuple(names...)}(maps) - new_va = Dict{Symbol, VertexAttributeType}() - for (name, attrib) in va - new_va[name] = values(attrib)[get(named_maps, name, maps[1])] - end + 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) @@ -354,10 +351,9 @@ function clear_faceviews(mesh::Mesh) new_fs = sizehint!(eltype(main_fs)[], length(main_fs)) new_views = sizehint!(UnitRange{Int}[], length(mesh.views)) - new_va = Dict{Symbol, VertexAttributeType}() - for (name, attrib) in va - new_va[name] = sizehint!(eltype(values(attrib))[], length(attrib)) - end + 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) @@ -448,16 +444,17 @@ 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),)) - new_va = Dict{Symbol, VertexAttributeType}() - - for (k, v) in vertex_attributes(mesh) + + 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),)) - new_va[k] = FaceView(values(v)[_maps[1]], _fs) + return FaceView(values(v)[_maps[1]], _fs) else - new_va[k] = v[maps[1]] + return v[maps[1]] end - end + end) return Mesh(new_va, new_fs) end diff --git a/test/runtests.jl b/test/runtests.jl index eb65c392..024898a5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -45,7 +45,7 @@ end @test mesh.position === points @test GeometryBasics.faces(mesh) === tfaces # TODO: Is the order variable with Dict? - @test propertynames(mesh) == (:vertex_attributes, :faces, :views, :normal, :position, :stress) + @test propertynames(mesh) == (:vertex_attributes, :faces, :views, :position, :normal, :stress) end end From 92f79b4c25ca592054ab24aa88075bdcf6fbeedd Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 16 Sep 2024 14:09:58 +0200 Subject: [PATCH 090/120] cleanup merge(meshes) --- src/meshes.jl | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/meshes.jl b/src/meshes.jl index 6ed9fbe3..1ced1115 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -248,9 +248,9 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) # Check that all meshes use the same VertexAttributes # Could also do this via typing the function, but maybe error is nice? - names = keys(m1.vertex_attributes) - idx = findfirst(m -> keys(m.vertex_attributes) != names, meshes) - if idx !== nothing + 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 " * @@ -276,16 +276,17 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) new_attribs = NamedTuple{names}(map(names) do name return reduce(vcat, getproperty.(meshes, name)) end) + # fs = reduce(vcat, faces.(meshes)) fs = reduce(vcat, faces.(meshes)) # TODO: is the type difference in offset bad? idx = length(faces(m1)) - offset = length(coordinates(m1))::Int # TODO: unnecessary + offset = length(coordinates(m1)) views = isempty(m1.views) ? UnitRange{Int64}[1:idx] : copy(m1.views) - Ns = length.(faces.(meshes)) - Ms = length.(coordinates.(meshes))::Vector{Int} # TODO: unnecessary - for (mesh, N, M) in Iterators.drop(zip(meshes, Ns, Ms), 1) + 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 @@ -302,7 +303,7 @@ function Base.merge(meshes::AbstractVector{<:Mesh}) end idx += N - offset += M + offset += length(coordinates(mesh)) end return Mesh(new_attribs, fs, views) From cb1ad711adff594b5b41616115f302768deae7e4 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 16 Sep 2024 14:27:27 +0200 Subject: [PATCH 091/120] test clear_faceviews with mesh.views --- test/meshes.jl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/test/meshes.jl b/test/meshes.jl index d364775f..0e2fcc7f 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -72,7 +72,10 @@ end @test !allunique([idx for f in faces(dm.normal) for idx in f]) indirect_meshes = map(rects) do r - GeometryBasics.mesh(coordinates(r), faces(r), normal = normals(r), facetype = QuadFace{Int64}) + 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) @@ -93,8 +96,8 @@ end @test allunique([idx for f in faces(cm) for idx in f]) - mixed_meshes = map(direct_meshes, converted_meshes) do dm, cm - rand() > 0.5 ? dm : cm + 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) From 5066c8a7b0ea6a756a2ab666b1a6ce8dd5a50220 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 18 Sep 2024 23:20:13 +0200 Subject: [PATCH 092/120] fix missing import in docs examples --- docs/src/primitives.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/primitives.md b/docs/src/primitives.md index d8b49def..a0712e97 100644 --- a/docs/src/primitives.md +++ b/docs/src/primitives.md @@ -13,6 +13,7 @@ A `Rect{D, T} = HyperRectangle{D, T}` is a D-dimensional axis-aligned hyperrectangle defined by an origin and a size. ```@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) From 61759f4c8805ebaa93cf899be221d8343ddec347 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 18 Sep 2024 23:27:28 +0200 Subject: [PATCH 093/120] add convert for arrays of meshes --- src/basic_types.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/basic_types.jl b/src/basic_types.jl index fae1afc2..8c910398 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -536,6 +536,10 @@ 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]) From 18ad6f0a42fc04b24afc834d25c698009aca7405 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 19 Sep 2024 00:58:58 +0200 Subject: [PATCH 094/120] add function for removing duplicate faces --- src/basic_types.jl | 47 ++++++++++++++++++++++++++++++++++++ src/meshes.jl | 11 +++++++++ src/primitives/rectangles.jl | 4 +-- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 8c910398..44f27780 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -81,6 +81,53 @@ function Base.show(io::IO, x::NgonFace{N, T}) where {N, T} 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} Face(F::Type{NgonFace{N,FT}}, ::Type{T}) where {FT,N,T} = F diff --git a/src/meshes.jl b/src/meshes.jl index 1ced1115..398ea647 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -461,6 +461,17 @@ function split_mesh(mesh::Mesh, views::Vector{UnitRange{Int}} = mesh.views) end 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 + function map_coordinates(f, mesh::Mesh) result = copy(mesh) diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index e5c0a444..b7c738b3 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -308,13 +308,13 @@ 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 Base.minmax(p::StaticVector, vmin, vmax) any(isnan, p) && return (vmin, vmax) return min.(p, vmin), max.(p, vmax) end # Annoying special case for view(Vector{Point}, Vector{Face}) -function minmax(tup::Tuple, vmin, vmax) +function Base.minmax(tup::Tuple, vmin, vmax) for p in tup any(isnan, p) && continue vmin = min.(p, vmin) From 81488f9e7d9ffc07656dbcdef2869aa72091b8ab Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 19 Sep 2024 01:23:14 +0200 Subject: [PATCH 095/120] update normal gen tests + fixes - fix normal gen for varying face types - fix normalization of face_normals --- src/geometry_primitives.jl | 16 ++++++++-------- test/geometrytypes.jl | 35 +++++++++++++++++++++++++---------- test/meshes.jl | 1 - test/runtests.jl | 7 ------- 4 files changed, 33 insertions(+), 26 deletions(-) diff --git a/src/geometry_primitives.jl b/src/geometry_primitives.jl index bbc4917a..e0e11ea4 100644 --- a/src/geometry_primitives.jl +++ b/src/geometry_primitives.jl @@ -160,15 +160,15 @@ function normals(vertices::AbstractVector{Point{3,T}}, faces::AbstractVector{F}; 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) + for i in 1:length(face) fi = face[i] normals_result[fi] = normals_result[fi] .+ n end @@ -198,14 +198,14 @@ end FT = ifelse(isconcretetype(F), :($F), :(typeof(f))) quote - normals = sizehint!(Vec3f[], length(fs)) - faces = sizehint!(F[], length(fs)) + normals = resize!(NormalType[], length(fs)) + faces = resize!(F[], length(fs)) - for f in fs + for (i, f) in enumerate(fs) ps = positions[f] n = GeometryBasics.orthogonal_vector(ps[1], ps[2], ps[3]) - push!(normals, n) - push!(faces, $(FT)(length(normals))) + normals[i] = normalize(n) + faces[i] = $(FT)(i) end return FaceView(normals, faces) diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 99909b12..c178781b 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -138,16 +138,31 @@ NFace = NgonFace end @testset "Normals" begin - # Needs to be reworked - @test_broken false - - # r = centered(Rect3{Float64}) - # n64 = [n for n in normals(r) for _ in 1:4] - # n32 = map(Vec{3,Float32}, n64) - # m32 = normal_mesh(r) - # m64 = normal_mesh(r, pointtype = Point3d) - # @test normals(coordinates(m32), GeometryBasics.faces(m32)) == n32 - # @test normals(coordinates(m64), GeometryBasics.faces(m64)) == 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 0e2fcc7f..ec82b59f 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -35,7 +35,6 @@ end @testset "Vertex Index Remapping" begin # Sanity Check - # TODO: extend 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)]) diff --git a/test/runtests.jl b/test/runtests.jl index 024898a5..27c1b145 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -44,7 +44,6 @@ end @test mesh.normal === normals @test mesh.position === points @test GeometryBasics.faces(mesh) === tfaces - # TODO: Is the order variable with Dict? @test propertynames(mesh) == (:vertex_attributes, :faces, :views, :position, :normal, :stress) end end @@ -295,12 +294,6 @@ end @test <(x, x1) end -@testset "Face normals" begin - r = Rect3f(Point3f(0), Vec3f(1)) - ns = normals(r) - @test GeometryBasics.face_normals(coordinates(r), faces(r)) == ns -end - @testset "Tests from GeometryTypes" begin include("geometrytypes.jl") end From 1f0f27076a4e72dfd05c056480fc8dac96dd2bbd Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 19 Sep 2024 01:39:16 +0200 Subject: [PATCH 096/120] remove time piracy --- src/primitives/rectangles.jl | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index b7c738b3..7c3489b3 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -313,15 +313,16 @@ function Base.minmax(p::StaticVector, 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 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 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) From 7fc8760cf7a54edafa9a67bbb33506a63ce4317f Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 19 Sep 2024 10:52:19 +0200 Subject: [PATCH 097/120] bring back shorthand types --- src/basic_types.jl | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 44f27780..e5115e86 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -633,12 +633,22 @@ function Mesh(; kwargs...) 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::Dict{Symbol, Any} end - """ MetaMesh(mesh; metadata...) MetaMesh(positions, faces; metadata...) From da912167d76f3fd4166b72c0df28f08f23d0a4b7 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 19 Sep 2024 11:54:13 +0200 Subject: [PATCH 098/120] restrict type in meshes to error earlier --- src/meshes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/meshes.jl b/src/meshes.jl index 398ea647..5b4782d7 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -236,7 +236,7 @@ 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)`. """ -function Base.merge(meshes::AbstractVector{<:Mesh}) +function Base.merge(meshes::AbstractVector{<:Mesh{D}}) where {D} return if isempty(meshes) return Mesh(Point3f[], GLTriangleFace[]) From e084c6ab03549dbf65223e846668e6962479d8ef Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 19 Sep 2024 12:08:31 +0200 Subject: [PATCH 099/120] update precompiles --- Project.toml | 1 + src/GeometryBasics.jl | 3 +-- src/precompiles.jl | 42 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/Project.toml b/Project.toml index 7f711f7b..9a34f631 100644 --- a/Project.toml +++ b/Project.toml @@ -9,6 +9,7 @@ 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" [compat] diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index aaf71ca2..0c7204f5 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -64,9 +64,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/precompiles.jl b/src/precompiles.jl index df41b1e6..2ba966fe 100644 --- a/src/precompiles.jl +++ b/src/precompiles.jl @@ -1,6 +1,40 @@ +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) + end end From 652b8a7c796d30b8a7f36970ec56dcf2d7f5612a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 19 Sep 2024 12:28:58 +0200 Subject: [PATCH 100/120] autoconvert point dim in merge(meshes) instead of restricitng type --- src/meshes.jl | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/meshes.jl b/src/meshes.jl index 5b4782d7..df614853 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -236,7 +236,7 @@ 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)`. """ -function Base.merge(meshes::AbstractVector{<:Mesh{D}}) where {D} +function Base.merge(meshes::AbstractVector{<:Mesh}) return if isempty(meshes) return Mesh(Point3f[], GLTriangleFace[]) @@ -271,12 +271,18 @@ function Base.merge(meshes::AbstractVector{<:Mesh{D}}) where {D} @label DOUBLE_BREAK if consistent_face_views - + # All the same kind of face, can just merge new_attribs = NamedTuple{names}(map(names) do name - return reduce(vcat, getproperty.(meshes, 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)) fs = reduce(vcat, faces.(meshes)) # TODO: is the type difference in offset bad? From d47f511083bf857fc0e5fa7e1c928a2c2cc2a8f5 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 19 Sep 2024 12:30:54 +0200 Subject: [PATCH 101/120] add compat entry --- Project.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Project.toml b/Project.toml index 9a34f631..c8b1f52f 100644 --- a/Project.toml +++ b/Project.toml @@ -24,6 +24,7 @@ OffsetArrays = "1" Random = "<0.0.1,1" Test = "<0.0.1,1" julia = "1.6" +PrecompileTools = "1.0" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" From 211209c29a9f4ccaccca1b1e205f78ab8de80439 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 19 Sep 2024 12:48:36 +0200 Subject: [PATCH 102/120] ignore unused PrecompileTools in 1.6, 1.7 --- test/runtests.jl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 27c1b145..2c0170f9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -316,6 +316,13 @@ 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 11d7593aae4eaa05bd449707e79ded110fc74ebe Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 19 Sep 2024 13:06:10 +0200 Subject: [PATCH 103/120] bring back old precompiles --- src/precompiles.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/precompiles.jl b/src/precompiles.jl index 2ba966fe..100a69dc 100644 --- a/src/precompiles.jl +++ b/src/precompiles.jl @@ -36,5 +36,9 @@ using PrecompileTools: @setup_workload, @compile_workload 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 From 81d5bfefc4432d2f5ba93b4220c63a26e8208a62 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 19 Sep 2024 14:07:32 +0200 Subject: [PATCH 104/120] add convert target to orthogonal_vector --- src/geometry_primitives.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/geometry_primitives.jl b/src/geometry_primitives.jl index e0e11ea4..b6549306 100644 --- a/src/geometry_primitives.jl +++ b/src/geometry_primitives.jl @@ -143,7 +143,8 @@ end Calculates an orthogonal vector `cross(p2 - p1, p3 - p1)` to a plane described by 3 points p1, p2, p3. """ -orthogonal_vector(p1, p2, p3) = cross(p2 .- p1, p3 .- p1) +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(positions::Vector{Point3{T}}, faces::Vector{<: NgonFace}[; normaltype = Vec3{T}]) @@ -167,7 +168,7 @@ function normals(vertices::AbstractVector{<:Point{3}}, faces::AbstractVector{<: 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]) + 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 @@ -203,7 +204,7 @@ end for (i, f) in enumerate(fs) ps = positions[f] - n = GeometryBasics.orthogonal_vector(ps[1], ps[2], ps[3]) + n = orthogonal_vector(NormalType, ps[1], ps[2], ps[3]) normals[i] = normalize(n) faces[i] = $(FT)(i) end From e12e85b00c53dd7a3be056fb1032f76120548869 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 19 Sep 2024 15:47:46 +0200 Subject: [PATCH 105/120] revert triangulation changes of Circle --- src/meshes.jl | 6 ++++-- src/primitives/spheres.jl | 12 +++++++----- test/geometrytypes.jl | 2 +- test/polygons.jl | 7 ++----- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/meshes.jl b/src/meshes.jl index df614853..47e617f9 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -20,13 +20,15 @@ function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleF _fs = positions.faces isnothing(faces(primitive)) || @error("A primitive should not define `faces` and use a FaceView for `coordinates()`. Using faces from FaceView.") else - _fs = faces(primitive) + # 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(_fs) if eltype(positions) <: Point2 - # triangulation.jl + # try triangulation with the converted positions as a last attempt fs = decompose(facetype, positions) else error("No triangulation for $(typeof(primitive))") diff --git a/src/primitives/spheres.jl b/src/primitives/spheres.jl index 62949f25..76602326 100644 --- a/src/primitives/spheres.jl +++ b/src/primitives/spheres.jl @@ -43,8 +43,8 @@ end function coordinates(s::Circle, nvertices=64) r = radius(s); o = origin(s) - ps = [r * Point(cos(phi), sin(phi)) + o for phi in LinRange(0, 2pi, nvertices+1)] - ps[end] = o + ps = [r * Point(cos(phi), sin(phi)) + o for phi in LinRange(0, 2pi, nvertices)] + # ps[end] = o return ps end @@ -52,9 +52,11 @@ function texturecoordinates(::Circle, nvertices=64) return coordinates(Circle(Point2f(0.5), 0.5f0), nvertices) end -function faces(::Circle, nvertices=64) - return [GLTriangleFace(nvertices+1, i, mod1(i+1, nvertices)) for i in 1: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) diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index c178781b..24bea6c0 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -183,7 +183,7 @@ end @test f == face_target circle = Circle(Point2f(0), 1.0f0) points = decompose(Point2f, Tesselation(circle, 20)) - @test length(points) == 21 + @test length(points) == 20 tess_circle = Tesselation(circle, 32) mesh = triangle_mesh(tess_circle) @test decompose(Point2f, mesh) ≈ decompose(Point2f, tess_circle) diff --git a/test/polygons.jl b/test/polygons.jl index ce8bb932..a9f1820e 100644 --- a/test/polygons.jl +++ b/test/polygons.jl @@ -13,14 +13,11 @@ @test poly1.exterior == decompose(Point2f, rect) @test poly1.interiors == [decompose(Point2f, hole)] - fs = GLTriangleFace[ - (1, 9, 8), (5, 13, 12), (10, 9, 1), (4, 1, 8), (11, 10, 1), (3, 4, 8), - (12, 11, 1), (3, 8, 7), (12, 1, 2), (3, 7, 6), (5, 12, 2), (2, 3, 6), (6, 5, 2)] - @test fs == faces(poly1) + # triangulation is inconsistent... + @test length(faces(poly1)) == 11 ps = vcat(decompose(Point2f, rect), decompose(Point2f, hole)) @test coordinates(poly1) == ps - fs = GeometryBasics.earcut_triangulate([poly1.exterior[[1, 2, 3, 4, 1]]]) @test fs == GLTriangleFace[(4,1,2), (2,3,4)] From 12eb1c546a6a95859e73d7345925e2210a1760bc Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 19 Sep 2024 16:15:52 +0200 Subject: [PATCH 106/120] revert to using StaticArrays --- Project.toml | 4 +- src/GeometryBasics.jl | 2 +- src/boundingboxes.jl | 2 +- src/fixed_arrays.jl | 352 ++++++++++++----------------------- src/mat.jl | 256 ------------------------- src/primitives/rectangles.jl | 4 +- test/fixed_arrays.jl | 39 +--- test/geometrytypes.jl | 2 +- 8 files changed, 140 insertions(+), 521 deletions(-) delete mode 100644 src/mat.jl diff --git a/Project.toml b/Project.toml index c8b1f52f..37374abc 100644 --- a/Project.toml +++ b/Project.toml @@ -11,6 +11,7 @@ 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" @@ -21,10 +22,11 @@ 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" -PrecompileTools = "1.0" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index 0c7204f5..e98571c7 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 diff --git a/src/boundingboxes.jl b/src/boundingboxes.jl index 958a4609..447a7228 100644 --- a/src/boundingboxes.jl +++ b/src/boundingboxes.jl @@ -13,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 diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index b754ec08..abcd2f23 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -1,273 +1,151 @@ -using LinearAlgebra -import Random -import Base: setindex - -""" - StaticVector{N, T} <: AbstractVector{T} - -An abstract type for GeometryBasics statically sized Vectors. These are used -for Point, Vec and Face types. -""" -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 - # fix ambiguity - $(VecT){1}(x::AbstractVector{T}) where {T} = $(VecT){1,T}(x) - function $(VecT){1,T1}(x::AbstractVector{T2}) where {T1,T2} - @assert 1 <= length(x) - return $(VecT){1,T1}((x[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 - # 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,T}(x) where {S,T} + return $(name){S,T}(ntuple(i -> convert(T, x), 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)) + $(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){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)) + 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::NTuple{N, T}) where {N, T} = $(VecT){N,T}(x) - $(VecT){N}(x::NTuple{N, T}) where {N, T} = $(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)(x::Vararg{Any,N}) where {N} = $(VecT){N}(x) - $(VecT)(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}(x::Vararg{Any,N}) where {N} = $(VecT){N}(x) - $(VecT){N}(x::Vararg{T,N}) where {T,N} = $(VecT){N,T}(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 - $(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 $(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.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 + 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.@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 @@ -293,13 +171,11 @@ 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}) @@ -337,4 +213,22 @@ we allow them to be used interchangeably. |`2` |`Point2{T}` |`Point2d` |`Point2f` |`Point2i` |`Point2ui`| |`3` |`Point3{T}` |`Point3d` |`Point3f` |`Point3i` |`Point3ui`| """ -Point \ No newline at end of file +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/mat.jl b/src/mat.jl deleted file mode 100644 index 4e1ff25e..00000000 --- a/src/mat.jl +++ /dev/null @@ -1,256 +0,0 @@ -import LinearAlgebra: inv, det -import Random - -""" - Mat{R, C, T, L} <: AbstractMatrix{T} - -GeometryBasics type for statically sized matrices. `R` is the number of rows, -`C` columns, T the eltype and `L = R * C` the total number of elements. -""" -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) - -""" - 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` | -""" -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/primitives/rectangles.jl b/src/primitives/rectangles.jl index 7c3489b3..045d266d 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -308,7 +308,7 @@ 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 Base.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 @@ -567,7 +567,7 @@ end 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) .* ps + ps = [Vec2{T}(0, 1) .+ Vec2{T}(1, -1) .* p for p in ps] return ps end diff --git a/test/fixed_arrays.jl b/test/fixed_arrays.jl index 478c4d53..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] diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 24bea6c0..420dbe37 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -110,7 +110,7 @@ end @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]] + @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 From af276479d1d598fda516d4480e3caf8c16e866cc Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 26 Sep 2024 20:59:26 +0200 Subject: [PATCH 107/120] avoid some invalidations (and fix get) --- src/basic_types.jl | 8 ++++---- src/fixed_arrays.jl | 3 --- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index e5115e86..f90e6ef4 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -690,10 +690,10 @@ end # TODO: or via getindex? Base.haskey(mesh::MetaMesh, key::Symbol) = haskey(getfield(mesh, :meta), key) -Base.get(f, mesh::MetaMesh, key::Symbol) = get(f, getfield(mesh, :meta), key) -Base.get!(f, mesh::MetaMesh, key::Symbol) = get!(f, getfield(mesh, :meta), key) -Base.get(mesh::MetaMesh, key::Symbol, default = nothing) = get(getfield(mesh, :meta), key, default) -Base.get!(mesh::MetaMesh, key::Symbol, default = nothing) = get!(getfield(mesh, :meta), key, default) +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) diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index abcd2f23..341b8ec2 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -98,9 +98,6 @@ macro fixed_vector(name_parent) end 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.@propagate_inbounds function Base.getindex(v::$(name){S,T}, i::Int) where {S,T} return v.data[i] end From d90ca226c2ec67f52a4ff731bca6cdd11a616823 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 26 Sep 2024 21:21:47 +0200 Subject: [PATCH 108/120] fix stale instances due to AbstractVector Apparently T[] is an AbstractVector here? --- src/geometry_primitives.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/geometry_primitives.jl b/src/geometry_primitives.jl index b6549306..7f60e481 100644 --- a/src/geometry_primitives.jl +++ b/src/geometry_primitives.jl @@ -96,10 +96,12 @@ 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 -collect_with_eltype(::Type{T}, iter) where {T} = collect_with_eltype!(T[], iter) +function collect_with_eltype(::Type{T}, iter) where {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!(T[], iter.data), iter.faces) + 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} From b9cb8c27b5e35b02579d79c039ec78b78861d977 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 26 Sep 2024 22:15:19 +0200 Subject: [PATCH 109/120] use string interpolation in error to avoid invalidation from string * --- 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 f90e6ef4..cf952b2a 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -526,7 +526,7 @@ struct Mesh{ try verify(fs, attrib) catch e - rethrow(ErrorException("Failed to verify $name attribute:\n" * e.msg)) + 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.") From b33f82d8eb47e0433587d248e8b61e1d60ceb7d7 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 26 Sep 2024 22:52:47 +0200 Subject: [PATCH 110/120] fix test --- test/meshes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/meshes.jl b/test/meshes.jl index ec82b59f..485fa54b 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -176,7 +176,7 @@ end @test Mesh(mm) == m @test haskey(mm, :name) - @test get(mm, :name) == "test" + @test get(mm, :name, nothing) == "test" @test mm[:name] == "test" @test !haskey(mm, :foo) @test get!(mm, :foo, "bar") == "bar" From 82dd5cec9944fc1ffea2e7bf0c6002404e837a4c Mon Sep 17 00:00:00 2001 From: ffreyer Date: Wed, 9 Oct 2024 23:41:37 +0200 Subject: [PATCH 111/120] export clear_faceviews & update FaceView docstring --- docs/src/meshes.md | 2 +- src/GeometryBasics.jl | 1 + src/basic_types.jl | 18 ++++++++++++++---- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/src/meshes.md b/docs/src/meshes.md index effbc3db..640f8b88 100644 --- a/docs/src/meshes.md +++ b/docs/src/meshes.md @@ -53,7 +53,7 @@ As a minimal example consider a mesh that is just one triangle, i.e. 3 position 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 `GeometryBasics.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 `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`. diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index e98571c7..21554b83 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -41,6 +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 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 cf952b2a..932f1654 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -382,10 +382,20 @@ Base.length(mpt::MultiPoint) = length(mpt.points) """ FaceView(data, faces) -A FaceView is alternative to passing a vertex attribute directly to a mesh. It -bundles `data` with a different set of `faces` which replace the default faces -of a mesh. Doing this allows you to have the `data` in a different order from -vertices in the mesh, potentially avoiding duplication. +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)`. From 5da7aa88cb51e3c9126180c97a57621a68489d1a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 10 Oct 2024 12:27:30 +0200 Subject: [PATCH 112/120] update type docstring --- src/basic_types.jl | 52 +++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 932f1654..15cc5a6b 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -490,9 +490,53 @@ returned by coordinates(mesh). abstract type AbstractMesh{Dim, T} <: AbstractGeometry{Dim, T} end """ - Mesh{D, T, FaceType, FaceArrayType} <: AbstractMesh{D, T} <: AbstractGeometry{D, T} + Mesh{PositionDim, PositionType, FaceType, VertexAttributeNames, VertexAttributeTypes, FaceVectorType} <: AbstractMesh{PositionDim, PositionType} <: AbstractGeometry{PositionDim, PositionType} -The type of a concrete mesh. It can hold arbitrary vertex data (including FaceView's). +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), [`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 <: Real, # TODO: Number? @@ -529,7 +573,9 @@ struct Mesh{ va = NamedTuple{names}(values(va)) end - # verify that faces of FaceViews match `fs` (in length per face) + # 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 From 44de3f4ef764a707a698c2368ded5d4d3a112c81 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 10 Oct 2024 12:27:43 +0200 Subject: [PATCH 113/120] add notes about views --- src/meshes.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/meshes.jl b/src/meshes.jl index 47e617f9..0953ea0a 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -237,6 +237,9 @@ 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) @@ -331,7 +334,8 @@ end 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. +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) From a0a6a84c4e75ffab5ed2070568465bd1aa9c6bc0 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 10 Oct 2024 12:29:25 +0200 Subject: [PATCH 114/120] update Mesh docs --- docs/src/meshes.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/src/meshes.md b/docs/src/meshes.md index 640f8b88..f6dc5abc 100644 --- a/docs/src/meshes.md +++ b/docs/src/meshes.md @@ -8,12 +8,14 @@ A `Mesh` is defined by the following struct: ```julia struct Mesh{ - Dim, PositionElType <: Real, + Dim, T <: Real, # TODO: Number? FT <: AbstractFace, + Names, + VAT <: Tuple{<: AbstractVector{Point{Dim, T}}, Vararg{VertexAttributeType}}, FVT <: AbstractVector{FT} } <: AbstractMesh{Dim, T} - vertex_attributes::Dict{Symbol, Union{FaceView, AbstractVector}} + vertex_attributes::NamedTuple{Names, VAT} faces::FVT views::Vector{UnitRange{Int}} end From 69dc7462fce646175fa142711ee6285d0814021a Mon Sep 17 00:00:00 2001 From: ffreyer Date: Thu, 10 Oct 2024 12:30:09 +0200 Subject: [PATCH 115/120] add FaceView ref to Mesh docstring --- src/basic_types.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/basic_types.jl b/src/basic_types.jl index 15cc5a6b..615cc163 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -523,7 +523,8 @@ few restrictions: 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), [`clear_faceviews`](@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 From 275a41ca323987b1b9825ca036d364bcf81e5dd1 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Fri, 11 Oct 2024 13:32:47 +0200 Subject: [PATCH 116/120] add brief section about extending decompose --- docs/src/decomposition.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/src/decomposition.md b/docs/src/decomposition.md index 90d679a2..80f09590 100644 --- a/docs/src/decomposition.md +++ b/docs/src/decomposition.md @@ -10,6 +10,12 @@ The default decomposition implemented by GeoemtryBasics are: - `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. From 410964577326dfa7dad4274fa2b5f79aefbf70c5 Mon Sep 17 00:00:00 2001 From: ffreyer Date: Mon, 14 Oct 2024 21:07:28 +0200 Subject: [PATCH 117/120] derive Point eltype when dimension is given --- src/interfaces.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/interfaces.jl b/src/interfaces.jl index 52f8a2c1..dfbd30e3 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -180,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 From 7edf0fa1e0f3107daf9d1dbe67f366a3df39e422 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 16 Oct 2024 14:07:13 +0200 Subject: [PATCH 118/120] reuse docstring --- docs/src/meshes.md | 24 ++---------------------- src/basic_types.jl | 2 +- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/docs/src/meshes.md b/docs/src/meshes.md index f6dc5abc..df6a9011 100644 --- a/docs/src/meshes.md +++ b/docs/src/meshes.md @@ -4,30 +4,10 @@ GeometryBasics defines two mesh types to work with - `Mesh` and `MetaMesh` ## Mesh -A `Mesh` is defined by the following struct: - -```julia -struct Mesh{ - Dim, T <: Real, # TODO: Number? - 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}} -end +```@docs +Mesh ``` -`mesh.vertex_attributes` contains all the per-vertex data like positions, normals, texture coordinates, etc. -They must always contain at least positions. -`mesh.faces` defines a set of (default) faces to use for each vertex attribute. -If a vertex attribute is a `FaceView` it carries its own set of faces which are used instead of `mesh.faces`. -In this case the number of faces and length of each face must match between them. -`mesh.views` is a set of views into the `mesh.faces` which can be used to separate the mesh into smaller submeshes. - You can get data from a mesh using a few interface functions: - `vertex_attributes(mesh) = mesh.vertex_attributes` - `coordinates(mesh) = mesh.vertex_attributes[:position]` diff --git a/src/basic_types.jl b/src/basic_types.jl index 615cc163..855102c2 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -540,7 +540,7 @@ results in an empty `views` vector. See also: [`merge`](@ref), [`split_mesh`](@ref) """ struct Mesh{ - Dim, T <: Real, # TODO: Number? + Dim, T <: Real, FT <: AbstractFace, Names, VAT <: Tuple{<: AbstractVector{Point{Dim, T}}, Vararg{VertexAttributeType}}, From a04d5ed5d0e3bc4dfe131607b8d9d2d903bf7703 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 16 Oct 2024 14:15:36 +0200 Subject: [PATCH 119/120] fix docs? --- docs/src/api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index ed120236..8e723000 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -2,7 +2,7 @@ ## Exports -```@autodocs +```@autodocs; canonical=false Modules = [GeometryBasics] Order = [:module, :constant, :type, :function, :macro] Public = true @@ -11,7 +11,7 @@ Private = false ## Private -```@autodocs +```@autodocs; canonical=false Modules = [GeometryBasics] Order = [:module, :constant, :type, :function, :macro] Public = false From a1bac27bdd418bb43c7a22855caadc1a373db9d4 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 16 Oct 2024 14:17:54 +0200 Subject: [PATCH 120/120] fix docs?? --- docs/src/api.md | 4 ++-- docs/src/meshes.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/api.md b/docs/src/api.md index 8e723000..ed120236 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -2,7 +2,7 @@ ## Exports -```@autodocs; canonical=false +```@autodocs Modules = [GeometryBasics] Order = [:module, :constant, :type, :function, :macro] Public = true @@ -11,7 +11,7 @@ Private = false ## Private -```@autodocs; canonical=false +```@autodocs Modules = [GeometryBasics] Order = [:module, :constant, :type, :function, :macro] Public = false diff --git a/docs/src/meshes.md b/docs/src/meshes.md index df6a9011..b9b1564f 100644 --- a/docs/src/meshes.md +++ b/docs/src/meshes.md @@ -4,7 +4,7 @@ GeometryBasics defines two mesh types to work with - `Mesh` and `MetaMesh` ## Mesh -```@docs +```@docs; canonical=false Mesh ```