diff --git a/Project.toml b/Project.toml index 7072f1ef..77d925ba 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "GeometryBasics" uuid = "5c1252a2-5f33-56bf-86c9-59e7332b4326" authors = ["SimonDanisch "] -version = "0.2.10" +version = "0.2.13" [deps] IterTools = "c8e1da08-722c-5040-9ed9-7db0dc04731e" diff --git a/docs/src/decomposition.md b/docs/src/decomposition.md index 9e562b91..698c07bc 100644 --- a/docs/src/decomposition.md +++ b/docs/src/decomposition.md @@ -1,51 +1,89 @@ # Decomposition -## Displaying primitives +## GeometryBasic Mesh interface -To display geometry primitives, they need to be decomposable. +GeometryBasic 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 -# Let's take SimpleRectangle as an example: -# Below is a minimal set of decomposable attributes to build up a triangle mesh: -isdecomposable(::Type{T}, ::Type{HR}) where {T<:Point, HR<:SimpleRectangle} = true -isdecomposable(::Type{T}, ::Type{HR}) where {T<:Face, HR<:SimpleRectangle} = true - -# This is an example implementation of `decompose` for points. -function GeometryBasics.decompose(P::Type{Point{3, PT}}, r::SimpleRectangle, resolution=(2,2)) where PT - w,h = resolution - vec( - PT[ - (x,y,0) - for x in range(r.x, stop = r.x+r.w, length = w), - y in range(r.y, stop = r.y+ r .h, length = h) - ] - ) + +function GeometryBasics.coordinates(rect::Rect2D, 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.decompose(::Type{T}, r::SimpleRectangle, resolution=(2,2)) where T <: Face - w,h = resolution - Idx = LinearIndices(resolution) - faces = vec([Face{4, Int}( - Idx[i, j], Idx[i+1, j], - Idx[i+1, j+1], Idx[i, j+1] - ) for i=1:(w-1), j=1:(h-1)] - ) - decompose(T, faces) +function GeometryBasics.faces(rect::Rect2D, 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 = SimpleRectangle(0, 0, 1, 1) -m = GLNormalMesh(rect) -vertices(m) == decompose(Point3f0, rect) +rect = Rect2D(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 +``` -faces(m) == decompose(GLTriangle, rect) # GLFace{3} == GLTriangle -normals(m) # automatically calculated from mesh +As you can see, `coordinates` and `faces` is 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(Point2f0, rect) == convert(Vector{Point2f0}, 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=Point2f0, 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=Vec2f0, normaltype=Vec3f0) ``` -As you can see, the normals are automatically calculated only with the faces and points. -You can overwrite that behavior by also defining decompose for the `Normal` type! +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(Vec2f0), rect) +decompose(Normal(Vec3f0), 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/3f0, Vec2/3f0), and the requested attributes. diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index d9fcef4c..a2f5def4 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -29,7 +29,7 @@ module GeometryBasics export OffsetInteger, ZeroIndex, OneIndex, GLIndex export FaceView, SimpleFaceView export AbstractPoint, PointMeta, PointWithUV - export PolygonMeta, MultiPointMeta + export PolygonMeta, MultiPointMeta, MultiLineStringMeta, MeshMeta export decompose, coordinates, faces, normals, decompose_uv, decompose_normals, texturecoordinates export Tesselation, pointmeta, Normal, UV, UVW export GLTriangleFace, GLNormalMesh3D, GLPlainTriangleMesh, GLUVMesh3D, GLUVNormalMesh3D diff --git a/src/basic_types.jl b/src/basic_types.jl index 8a70be3c..c83291e2 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -8,7 +8,7 @@ Base.ndims(x::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 `Polytype{N} where N == 3` denotes a Triangle both as a Simplex or Ngon. +Note That `Polytope{N} where N == 3` denotes a Triangle both as a Simplex or Ngon. """ abstract type Polytope{Dim, T} <: AbstractGeometry{Dim, T} end abstract type AbstractPolygon{Dim, T} <: Polytope{Dim, T} end @@ -108,6 +108,10 @@ Base.summary(io::IO, x::Type{<: TriangleP}) = print(io, "Triangle") const Quadrilateral{Dim, T} = Ngon{Dim, T, 4, P} where P <: AbstractPoint{Dim, T} +Base.show(io::IO, x::Quadrilateral) = print(io, "Quad(", join(x, ", "), ")") +Base.summary(io::IO, x::Type{<: Quadrilateral}) = print(io, "Quad") + + """ A `Simplex` is a generalization of an N-dimensional tetrahedra and can be thought of as a minimal convex set containing the specified points. @@ -336,7 +340,7 @@ 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 """ -const AbstractMesh{Element} = AbstractVector{Element} +abstract type AbstractMesh{Element<:Polytope} <: AbstractVector{Element} end """ Mesh <: AbstractVector{Element} diff --git a/src/geometry_primitives.jl b/src/geometry_primitives.jl index 469472c1..b4ab70d4 100644 --- a/src/geometry_primitives.jl +++ b/src/geometry_primitives.jl @@ -8,6 +8,13 @@ end ## # conversion & decompose +convert_simplex(::Type{T}, x::T) where T = (x,) + +function convert_simplex(NFT::Type{NgonFace{N, T1}}, f::Union{NgonFace{N, T2}}) where {T1, T2, N} + return (convert(NFT, f),) +end + +convert_simplex(NFT::Type{NgonFace{3,T}}, f::NgonFace{3,T2}) where {T, T2} = (convert(NFT, f),) """ convert_simplex(::Type{Face{3}}, f::Face{N}) @@ -42,7 +49,9 @@ end to_pointn(::Type{T}, x) where T<:Point = convert_simplex(T, x)[1] +# disambiguation method overlords convert_simplex(::Type{Point}, x::Point) = (x,) +convert_simplex(::Type{Point{N,T}}, p::Point{N,T}) where {N, T} = (p,) function convert_simplex(::Type{Point{N, T}}, x) where {N, T} N2 = length(x) return (Point{N, T}(ntuple(i-> i <= N2 ? T(x[i]) : T(0), N)),) diff --git a/src/interfaces.jl b/src/interfaces.jl index b6670b7b..427c61f1 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -33,6 +33,8 @@ function faces(primitive, nvertices=nothing) return nothing end +texturecoordinates(primitive, nvertices=nothing) = nothing + """ Tesselation(primitive, nvertices) For abstract geometries, when we generate @@ -123,16 +125,25 @@ function decompose(NT::Normal{T}, primitive) where T end function decompose(UVT::Union{UV{T}, UVW{T}}, primitive) where T + # This is the fallback for texture coordinates if a primitive doesn't overload them + # We just take the positions and normalize them uv = texturecoordinates(primitive) if uv === nothing - return decompose(UVT, texturecoordinates(coordinates(primitive))) + # If the primitive doesn't even have coordinates, we're out of options and return + # nothing, indicating that texturecoordinates aren't implemented + positions = decompose(Point, primitive) + positions === nothing && return nothing + # Let this overlord do the work + return decompose(UVT, positions) end return collect_with_eltype(T, uv) end -function texturecoordinates(positions::AbstractVector{<:VecTypes}) - bb = Rect(positions) - return map(positions) do p +function decompose(UVT::Union{UV{T}, UVW{T}}, positions::AbstractVector{<:VecTypes}) where T + N = length(T) + positions_nd = decompose(Point{N, eltype(T)}, positions) + bb = Rect(positions_nd) # Make sure we get this as points + return map(positions_nd) do p return (p .- minimum(bb)) ./ widths(bb) end end diff --git a/src/meshes.jl b/src/meshes.jl index 32a93991..5c1821fd 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -85,11 +85,9 @@ const GLNormalUVWMesh{Dim} = NormalUVWMesh{Dim, Float32} const GLNormalUVWMesh2D = GLNormalUVWMesh{2} const GLNormalUVWMesh3D = GLNormalUVWMesh{3} -best_pointtype(::Meshable{Dim, T}) where {Dim, T} = Point{Dim, T} - """ mesh(primitive::GeometryPrimitive; - pointtype=best_pointtype(primitive), facetype=GLTriangle, + pointtype=Point, facetype=GLTriangle, uvtype=nothing, normaltype=nothing) Creates a mesh from `primitive`. @@ -100,7 +98,7 @@ It also only losely correlates to the number of vertices, depending on the algor #TODO: find a better number here! """ function mesh(primitive::Meshable; - pointtype=best_pointtype(primitive), facetype=GLTriangleFace, + pointtype=Point, facetype=GLTriangleFace, uv=nothing, normaltype=nothing) positions = decompose(pointtype, primitive) diff --git a/src/metadata.jl b/src/metadata.jl index cf261c24..dbb51196 100644 --- a/src/metadata.jl +++ b/src/metadata.jl @@ -63,46 +63,52 @@ macro meta_type(name, mainfield, supertype, params...) MetaName = Symbol("$(name)Meta") field = QuoteNode(mainfield) NoParams = Symbol("$(MetaName)NoParams") + + params_sym = map(params) do param + param isa Symbol && return param + param isa Expr && param.head == :(<:) && return param.args[1] + error("Unsupported type parameter: $(param)") + end + expr = quote - struct $MetaName{$(params...), Typ <: $supertype{$(params...)}, Names, Types} <: $supertype{$(params...)} + struct $MetaName{$(params...), Typ <: $supertype{$(params_sym...)}, Names, Types} <: $supertype{$(params_sym...)} main::Typ meta::NamedTuple{Names, Types} end - const $NoParams{Typ, Names, Types} = $MetaName{$(params...), Typ, Names, Types} where {$(params...)} + const $NoParams{Typ, Names, Types} = $MetaName{$(params_sym...), Typ, Names, Types} where {$(params_sym...)} - function Base.getproperty(x::$MetaName{$(params...), Typ, Names, Types}, field::Symbol) where {$(params...), Typ, Names, Types} + function Base.getproperty(x::$MetaName{$(params_sym...), Typ, Names, Types}, + field::Symbol) where {$(params...), Typ, Names, Types} field === $field && return getfield(x, :main) field === :main && return getfield(x, :main) Base.sym_in(field, Names) && return getfield(getfield(x, :meta), field) error("Field $field not part of Element") end - GeometryBasics.MetaType(T::Type{<: $supertype}) = $MetaName{T} + function GeometryBasics.MetaType(XX::Type{<: $supertype{$(params_sym...)} where {$(params...)}}) + return $MetaName + end + function GeometryBasics.MetaType( - ST::Type{<: $supertype{$(params...)}}, + ST::Type{<: $supertype{$(params_sym...)}}, ::Type{NamedTuple{Names, Types}}) where {$(params...), Names, Types} - return $MetaName{$(params...), ST, Names, Types} + return $MetaName{$(params_sym...), ST, Names, Types} end - GeometryBasics.MetaFree(::Type{<: $MetaName{Typ}}) where Typ = Typ GeometryBasics.MetaFree(::Type{<: $MetaName}) = $name GeometryBasics.metafree(x::$MetaName) = getfield(x, :main) - GeometryBasics.metafree(x::AbstractVector{<: $MetaName}) = getcolumns(x, $field)[1] + GeometryBasics.metafree(x::AbstractVector{<: $MetaName}) = getproperty(x, $field) GeometryBasics.meta(x::$MetaName) = getfield(x, :meta) - GeometryBasics.meta(x::AbstractVector{<: $MetaName}) = getcolumns(x, :meta)[1] + GeometryBasics.meta(x::AbstractVector{<: $MetaName}) = getproperty(x, :meta) - function GeometryBasics.meta(main::$supertype; meta...) + function GeometryBasics.meta(main::$supertype{$(params_sym...)}; meta...) where {$(params...)} isempty(meta) && return elements # no meta to add! return $MetaName(main; meta...) end - function GeometryBasics.attributes(hasmeta::$MetaName) - return Dict{Symbol, Any}((name => getproperty(hasmeta, name) for name in propertynames(hasmeta))) - end - - function GeometryBasics.meta(elements::AbstractVector{T}; meta...) where T <: $supertype + function GeometryBasics.meta(elements::AbstractVector{XX}; meta...) where XX <: $supertype{$(params_sym...)} where {$(params...)} isempty(meta) && return elements # no meta to add! n = length(elements) for (k, v) in meta @@ -118,7 +124,11 @@ macro meta_type(name, mainfield, supertype, params...) # get the first element to get the per element named tuple type ElementNT = typeof(map(first, nt)) - return StructArray{MetaType(T, ElementNT)}(($(mainfield) = elements, nt...)) + return StructArray{MetaType(XX, ElementNT)}(($(mainfield) = elements, nt...)) + end + + function GeometryBasics.attributes(hasmeta::$MetaName) + return Dict{Symbol, Any}((name => getproperty(hasmeta, name) for name in propertynames(hasmeta))) end function (MT::Type{<: $MetaName})(args...; meta...) @@ -132,22 +142,20 @@ macro meta_type(name, mainfield, supertype, params...) return MT(main, nt) end - function Base.propertynames(::$MetaName{$(params...), Typ, Names, Types}) where {$(params...), Typ, Names, Types} + function Base.propertynames(::$MetaName{$(params_sym...), Typ, Names, Types}) where {$(params...), Typ, Names, Types} return ($field, Names...) end - function StructArrays.staticschema(::Type{$MetaName{$(params...), Typ, Names, Types}}) where {$(params...), Typ, Names, Types} + function StructArrays.staticschema(::Type{$MetaName{$(params_sym...), Typ, Names, Types}}) where {$(params...), Typ, Names, Types} NamedTuple{($field, Names...), Base.tuple_type_cons(Typ, Types)} end function StructArrays.createinstance( - ::Type{$MetaName{$(params...), Typ, Names, Types}}, + ::Type{$MetaName{$(params_sym...), Typ, Names, Types}}, metafree, args... ) where {$(params...), Typ, Names, Types} $MetaName(metafree, NamedTuple{Names, Types}(args)) end - - end return esc(expr) end @@ -163,6 +171,14 @@ Base.getindex(x::SimplexFaceMeta, idx::Int) = getindex(metafree(x), idx) @meta_type(Polygon, polygon, AbstractPolygon, N, T) -@meta_type(MultiPoint, points, AbstractVector, P) +@meta_type(MultiPoint, points, AbstractVector, P <: AbstractPoint) Base.getindex(x::MultiPointMeta, idx::Int) = getindex(metafree(x), idx) Base.size(x::MultiPointMeta) = size(metafree(x)) + +@meta_type(MultiLineString, linestrings, AbstractVector, P <: LineString) +Base.getindex(x::MultiLineStringMeta, idx::Int) = getindex(metafree(x), idx) +Base.size(x::MultiLineStringMeta) = size(metafree(x)) + +@meta_type(Mesh, mesh, AbstractMesh, Element <: Polytope) +Base.getindex(x::MeshMeta, idx::Int) = getindex(metafree(x), idx) +Base.size(x::MeshMeta) = size(metafree(x)) diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 03669359..c79dd984 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -102,6 +102,9 @@ end @test GeometryBasics.coordinates(m) ≈ positions m = normal_mesh(s)# just test that it works without explicit resolution parameter @test m isa GLNormalMesh + + muv = uv_mesh(s) + @test Rect(Point.(texturecoordinates(muv))) == FRect2D(Vec2f0(0), Vec2f0(1.0)) end end @@ -240,7 +243,7 @@ end @test !in(rect1, split1) prim = Rect(0.0, 0.0, 1.0, 1.0) - @test length(prim) == 2 + @test length(prim) == 2 @test width(prim) == 1.0 @test height(prim) == 1.0 @@ -267,7 +270,7 @@ end @test update(b, v) isa GeometryBasics.HyperRectangle{2,Float64} v = Vec(1.0, 2.0) @test update(b, v) isa GeometryBasics.HyperRectangle{2,Float64} - + p = Vec(5.0, 4.0) rect = Rect(0.0, 0.0, 1.0, 1.0) @test min_dist_dim(rect, p, 1) == 4.0 diff --git a/test/runtests.jl b/test/runtests.jl index 09735802..6600d169 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -77,7 +77,7 @@ using GeometryBasics: attributes @test length(filtered) == 7 end @test GeometryBasics.getcolumn(plain, :name) == pnames - @test GeometryBasics.MetaType(Polygon) == PolygonMeta{Polygon,T,Typ,Names,Types} where Types where Names where Typ<:GeometryBasics.AbstractPolygon{Polygon,T} where T + @test GeometryBasics.MetaType(Polygon) == PolygonMeta @test_throws ErrorException GeometryBasics.meta(plain) @test GeometryBasics.MetaFree(PolygonMeta) == Polygon @@ -99,6 +99,25 @@ using GeometryBasics: attributes @test metafree(pm) === p @test propertynames(pm) == (:position, :a, :b) end + + @testset "MultiLineString with metadata" begin + linestring1 = LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]) + linestring2 = LineString(Point{2, Int}[(40, 40), (30, 30), (40, 20), (30, 10)]) + multilinestring = MultiLineString([linestring1, linestring2]) + multilinestringmeta = MultiLineStringMeta([linestring1, linestring2]; boundingbox = Rect(1.0, 1.0, 2.0, 2.0)) + @test multilinestringmeta isa AbstractVector + @test meta(multilinestringmeta) === (boundingbox = Rect(1.0, 1.0, 2.0, 2.0),) + @test metafree(multilinestringmeta) == multilinestring + @test propertynames(multilinestringmeta) == (:linestrings, :boundingbox) + end + + @testset "Mesh with metadata" begin + m = triangle_mesh(Sphere(Point3f0(0), 1)) + m_meta = MeshMeta(m; boundingbox=Rect(1.0, 1.0, 2.0, 2.0)) + @test meta(m_meta) === (boundingbox = Rect(1.0, 1.0, 2.0, 2.0),) + @test metafree(m_meta) === m + @test propertynames(m_meta) == (:mesh, :boundingbox) + end end @testset "view" begin @@ -255,6 +274,12 @@ end @test meshuv isa GLUVMesh3D @test meshuvnormal isa GLNormalUVMesh3D + t = Tesselation(FRect2D(0, 0, 2, 2), (30, 30)) + m = GeometryBasics.mesh(t, pointtype=Point3f0, facetype=QuadFace{Int}) + m2 = GeometryBasics.mesh(m, facetype=QuadFace{GLIndex}) + @test GeometryBasics.faces(m2) isa Vector{QuadFace{GLIndex}} + @test GeometryBasics.coordinates(m2) isa Vector{Point3f0} + end @testset "Multi geometries" begin