diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bf35910..2cb3d95f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: pull_request: branches: - master + - sd/simple-mesh jobs: test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} diff --git a/Project.toml b/Project.toml index 7f711f7b..37374abc 100644 --- a/Project.toml +++ b/Project.toml @@ -9,7 +9,9 @@ Extents = "411431e0-e8b7-467b-b5e0-f676ba4f2910" GeoInterface = "cf35fbd7-0cd7-5166-be24-54bfbe79505f" IterTools = "c8e1da08-722c-5040-9ed9-7db0dc04731e" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] Aqua = "0.8" @@ -20,7 +22,9 @@ GeoJSON = "0.7, 0.8" IterTools = "1.3.0" LinearAlgebra = "<0.0.1,1" OffsetArrays = "1" +PrecompileTools = "1.0" Random = "<0.0.1,1" +StaticArrays = "0.6, 1" Test = "<0.0.1,1" julia = "1.6" diff --git a/docs/make.jl b/docs/make.jl index c8cdefd0..9d41c5e7 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -10,11 +10,10 @@ makedocs(format=Documenter.HTML(prettyurls=get(ENV, "CI", "false") == "true"), pages=[ "index.md", "primitives.md", - "rectangles.md", "polygons.md", "meshes.md", "decomposition.md", - "metadata.md", + "static_array_types.md", "api.md" ], modules=[GeometryBasics]) diff --git a/docs/src/decomposition.md b/docs/src/decomposition.md index d0bf3dfe..80f09590 100644 --- a/docs/src/decomposition.md +++ b/docs/src/decomposition.md @@ -1,89 +1,26 @@ # Decomposition - -## GeometryBasics Mesh interface - -GeometryBasics defines an interface to decompose abstract geometries into -points and triangle meshes. -This can be done for any arbitrary primitive, by overloading the following interface: - -```julia - -function GeometryBasics.coordinates(rect::Rect2, nvertices=(2,2)) - mini, maxi = extrema(rect) - xrange, yrange = LinRange.(mini, maxi, nvertices) - return ivec(((x,y) for x in xrange, y in yrange)) -end - -function GeometryBasics.faces(rect::Rect2, nvertices=(2, 2)) - w, h = nvertices - idx = LinearIndices(nvertices) - quad(i, j) = QuadFace{Int}(idx[i, j], idx[i+1, j], idx[i+1, j+1], idx[i, j+1]) - return ivec((quad(i, j) for i=1:(w-1), j=1:(h-1))) -end -``` -Those methods, for performance reasons, expect you to return an iterator, to make -materializing them with different element types allocation free. But of course, -can also return any `AbstractArray`. - -With these methods defined, this constructor will magically work: - -```julia -rect = Rect2(0.0, 0.0, 1.0, 1.0) -m = GeometryBasics.mesh(rect) -``` -If you want to set the `nvertices` argument, you need to wrap your primitive in a `Tesselation` -object: -```julia -m = GeometryBasics.mesh(Tesselation(rect, (50, 50))) -length(coordinates(m)) == 50^2 -``` - -As you can see, `coordinates` and `faces` are also defined on a mesh -```julia -coordinates(m) -faces(m) -``` -But will actually not be an iterator anymore. Instead, the mesh constructor uses -the `decompose` function, that will collect the result of coordinates and will -convert it to a concrete element type: -```julia -decompose(Point2f, rect) == convert(Vector{Point2f}, collect(coordinates(rect))) -``` -The element conversion is handled by `simplex_convert`, which also handles convert -between different face types: -```julia -decompose(QuadFace{Int}, rect) == convert(Vector{QuadFace{Int}}, collect(faces(rect))) -length(decompose(QuadFace{Int}, rect)) == 1 -fs = decompose(GLTriangleFace, rect) -fs isa Vector{GLTriangleFace} -length(fs) == 2 # 2 triangles make up one quad ;) -``` -`mesh` uses the most natural element type by default, which you can get with the unqualified Point type: -```julia -decompose(Point, rect) isa Vector{Point{2, Float64}} -``` -You can also pass the element type to `mesh`: -```julia -m = GeometryBasics.mesh(rect, pointtype=Point2f, facetype=QuadFace{Int}) -``` -You can also set the uv and normal type for the mesh constructor, which will then -calculate them for you, with the requested element type: -```julia -m = GeometryBasics.mesh(rect, uv=Vec2f, normaltype=Vec3f) -``` - -As you can see, the normals are automatically calculated, -the same is true for texture coordinates. You can overload this behavior by overloading -`normals` or `texturecoordinates` the same way as coordinates. -`decompose` works a bit different for normals/texturecoordinates, since they dont have their own element type. -Instead, you can use `decompose` like this: -```julia -decompose(UV(Vec2f), rect) -decompose(Normal(Vec3f), rect) -# the short form for the above: -decompose_uv(rect) -decompose_normals(rect) -``` -You can also use `triangle_mesh`, `normal_mesh` and `uv_normal_mesh` to call the -`mesh` constructor with predefined element types (Point2/3f, Vec2/3f), and the requested attributes. +## decompose functions + +The `decompose` functions allow you to grab certain data from an `AbstractGeometry` like a mesh or primitive and convert it to a requested type, if possible. +They can also be used to convert an array of e.g. faces into a different face type directly. +The default decomposition implemented by GeoemtryBasics are: +- `decompose(::Type{<: Point}, source)` which collects data from `source` using `coordinates(source)` and converts it to the given point type. +- `decompose_normals([::Type{<: Vec},] source) = decompose([::Type{Normals{<: Vec}}},] source)` which collects data with `normals(source)` and converts it to the given Vec type. +- `decompose_uv([::Type{<: Vec},] source) = decompose([::Type{UV{<: Vec}}},] source)` which collects data with `texturecoordinates(source)` and converts it to the given Vec type. This function also exists with `UVW` texture coordinates. +- `decompose(::Type{<: AbstractFace}, source)` which collects data with `faces(source)` and converts it to the given face type. + +### Extending decompose + +For `decompose` to work there needs to be a conversion from some element type to some target type. +GeometryBasics relies on `GeometryBasics.convert_simplex(TargetType, value)` for this. +If you want to add new types to decompose, e.g. a new face type, you will need to add a method to that function. + +## Primitive decomposition + +GeometryBasics defines an interface to decompose geometry primitives into vertex attributes and faces. +The interface includes four functions: +- `coordinates(primitive[, nvertices])` which produces the positions associated with the primitive +- `faces(primitive[, nvertices])` which produces the faces which connect the vertex positions to a mesh +- `normals(primitive[, nvertices])` which optionally provide normal vectors of the primitive +- `texturecoordinates(primitive[, nvertices])` which optional provide texture coordinates (uv/uvw) of the primitive diff --git a/docs/src/implementation.md b/docs/src/implementation.md deleted file mode 100644 index 6334eed0..00000000 --- a/docs/src/implementation.md +++ /dev/null @@ -1,5 +0,0 @@ -# Implementation - -In the backend, GeometryTypes relies on fixed-size arrays, specifically static vectors. - -TODO add more here. diff --git a/docs/src/index.md b/docs/src/index.md index dc570e92..f9166eda 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -22,56 +22,20 @@ p2 = Point(1, 3); p3 = Point(4, 4); ``` -Geometries can carry metadata: - -```@repl quickstart -poi = meta(p1, city="Abuja", rainfall=1221.2) -``` - -Metadata is stored in a NamedTuple and can be retrieved as such: - -```@repl quickstart -meta(poi) -``` - -Specific metadata attributes can be directly retrieved: - -```@repl quickstart -poi.rainfall -``` - -To remove the metadata and keep only the geometry, use `metafree`: - -```@repl quickstart -metafree(poi) -``` - -Geometries have predefined metatypes: - -```@repl quickstart -multipoi = MultiPointMeta([p1], city="Abuja", rainfall=1221.2) -``` - -Connect the points with lines: +Connect pairs of points as line segments: ```@repl quickstart l1 = Line(p1, p2) l2 = Line(p2, p3); ``` -Connect the lines in a linestring: - -```@repl quickstart -LineString([l1, l2]) -``` - -Linestrings can also be constructed directly from points: +Or connect multiple points as a linestring: ```@repl quickstart LineString([p1, p2, p3]) ``` -The same goes for polygons: +You can also create polygons from points: ```@repl quickstart Polygon(Point{2, Int}[(3, 1), (4, 4), (2, 4), (1, 2), (3, 1)]) @@ -89,16 +53,16 @@ Decompose the rectangle into two triangular faces: rect_faces = decompose(TriangleFace{Int}, rect) ``` -Decompose the rectangle into four vertices: +Decompose the rectangle into four positions: ```@repl quickstart -rect_vertices = decompose(Point{2, Float64}, rect) +rect_positions = decompose(Point{2, Float64}, rect) ``` Combine the vertices and faces into a triangle mesh: ```@repl quickstart -mesh = Mesh(rect_vertices, rect_faces) +mesh = Mesh(rect_positions, rect_faces) ``` Use `GeometryBasics.mesh` to get a mesh directly from a geometry: @@ -106,40 +70,3 @@ Use `GeometryBasics.mesh` to get a mesh directly from a geometry: ```@repl quickstart mesh = GeometryBasics.mesh(rect) ``` - - -## Aliases - -GeometryBasics exports common aliases for Point, Vec, Mat and Rect: - -### Vec - -| |`T`(eltype) |`Float64` |`Float32` |`Int` |`UInt` | -|--------|------------|----------|----------|----------|----------| -|`N`(dim)|`Vec{N,T}` |`Vecd{N}` |`Vecf{N}` |`Veci{N}` |`Vecui{N}`| -|`2` |`Vec2{T}` |`Vec2d` |`Vec2f` |`Vec2i` |`Vec2ui` | -|`3` |`Vec3{T}` |`Vec3d` |`Vec3f` |`Vec3i` |`Vec3ui` | - -### Point - -| |`T`(eltype) |`Float64` |`Float32` |`Int` |`UInt` | -|--------|------------|----------|----------|----------|----------| -|`N`(dim)|`Point{N,T}`|`Pointd{N}`|`Pointf{N}`|`Pointi{N}`|`Pointui{N}`| -|`2` |`Point2{T}` |`Point2d` |`Point2f` |`Point2i` |`Point2ui`| -|`3` |`Point3{T}` |`Point3d` |`Point3f` |`Point3i` |`Point3ui`| - -### Mat - -| |`T`(eltype) |`Float64` |`Float32` |`Int` |`UInt` | -|--------|------------|----------|----------|----------|----------| -|`N`(dim)|`Mat{N,T}` |`Matd{N}` |`Matf{N}` |`Mati{N}` |`Matui{N}`| -|`2` |`Mat2{T}` |`Mat2d` |`Mat2f` |`Mat2i` |`Mat2ui` | -|`3` |`Mat3{T}` |`Mat3d` |`Mat3f` |`Mat3i` |`Mat3ui` | - -### Rect - -| |`T`(eltype) |`Float64` |`Float32` |`Int` |`UInt` | -|--------|------------|----------|----------|----------|----------| -|`N`(dim)|`Rect{N,T}` |`Rectd{N}`|`Rectf{N}`|`Recti{N}`|`Rectui{N}`| -|`2` |`Rect2{T}` |`Rect2d` |`Rect2f` |`Rect2i` |`Rect2ui` | -|`3` |`Rect3{T}` |`Rect3d` |`Rect3f` |`Rect3i` |`Rect3ui` | diff --git a/docs/src/meshes.md b/docs/src/meshes.md index 6fabaf24..b9b1564f 100644 --- a/docs/src/meshes.md +++ b/docs/src/meshes.md @@ -1,24 +1,80 @@ # Meshes -## Types +GeometryBasics defines two mesh types to work with - `Mesh` and `MetaMesh` -* [`AbstractMesh`](@ref) -* [`Mesh`](@ref) +## Mesh + +```@docs; canonical=false +Mesh +``` + +You can get data from a mesh using a few interface functions: +- `vertex_attributes(mesh) = mesh.vertex_attributes` +- `coordinates(mesh) = mesh.vertex_attributes[:position]` +- `normals(mesh) = mesh.vertex_attributes[:normal]` +- `texturecoordinates(mesh) = mesh.vertex_attributes[:uv]` +- `faces(mesh) = mesh.faces` + +You can also grab the contents of `mesh.vertex_attributes` as if they were fields of the `Mesh`, e.g. `mesh.position` works. + +### FaceView + +As mentioned above, a vertex attribute can be a `FaceView`. +A `FaceView` is simply defined as a vector of data and a vector of faces: + +```julia +struct FaceView{T, AVT <: AbstractVector{T}, FVT <: AbstractVector{<: AbstractFace}} + data::AVT + faces::FVT +end +``` + +Its purpose is to allow you to add data that needs to be defined per vertex but does not match the vertex structure used by `mesh.faces`. + +As a minimal example consider a mesh that is just one triangle, i.e. 3 position and one triangle face `TriangleFace(1,2,3)`. +Let's say we want to add a flat color to the triangle. +In this case we only have one color, but our face refers to 3 different vertices (3 different positions). +To avoid duplicating the color data, we can instead define a new triangle face `TriangleFace(1)` and add the color attribute as a `FaceView([color], [TriangleFace(1)])`. +If we ever need the mesh to be defined with just one common set of faces, i.e. no FaceView and appropriately duplicated vertex data, we can use `clear_faceviews(mesh)` to generate it. + +On a larger scale this can be useful for memory and performance reason, e.g. when you do calculations with vertex attributes. +It can also simplify some definitions, like for example `Rect3`. +In that case we have 8 positions and 6 normals with FaceViews, or 24 without (assuming per-face normals). + + +## MetaMesh + +A `MetaMesh` is given by + +```julia +struct MetaMesh{Dim, T, M <: AbstractMesh{Dim, T}} <: AbstractMesh{Dim, T} + mesh::M + meta::Dict{Symbol, Any} +end +``` + +where `meta` may contain any data you want to include with a mesh. +For example, you could include group names or material data corresponding to `mesh.views`. ## How to create a mesh -### Meshing.jl +### GeometryBasics -### MeshIO.jl +In GeometryBasics you mainly create meshes from primitives using a few constructors: +- `triangle_mesh(primitive)` generates the most basic mesh (i.e. positions and faces) +- `normal_mesh(primitive)` generates a mesh with normals (generated if the primitive doesn't implement `normal()`) +- `uv_mesh(primitive)` generates a mesh with texture coordinates (generated if the primitive doesn't implement `texturecoordinates()`) +- `uv_normal_mesh(primitive)` generates a mesh with normals and texture coordinates -The [`MeshIO.jl`](https://github.com/JuliaIO/MeshIO.jl) package provides load/save support for several file formats which store meshes. +Each of these constructors also includes keyword arguments for setting types, i.e. `pointtype`, `facetype`, `normaltype` and `uvtype` as appropriate. +Of course you can also construct a mesh directly from data, either with there various `Mesh()` or `GeometryBasics.mesh()` constructors. +The latter also include a `pointtype` and `facetype` conversion. -## How to access data +Finally there is also a `merge(::Vector{Mesh})` function which combines multiple meshes into a single one. +Note that this doesn't remove any data (e.g. hidden or duplicate vertices), and may remove `FaceView`s if they are incompatible between meshes. -The following functions can be called on an [`AbstractMesh`](@ref) to access its underlying data. +### Meshing.jl -* [`faces`](@ref) -* [`coordinates`](@ref) -* `texturecoordinates` -* [`normals`](@ref) +### MeshIO.jl +The [`MeshIO.jl`](https://github.com/JuliaIO/MeshIO.jl) package provides load/save support for several file formats which store meshes. diff --git a/docs/src/metadata.md b/docs/src/metadata.md deleted file mode 100644 index d7bebcaf..00000000 --- a/docs/src/metadata.md +++ /dev/null @@ -1,153 +0,0 @@ -# Metadata - -## Meta - -The `Meta` method provides metadata handling capabilities in GeometryBasics. -Similarly to remove the metadata and keep only the geometry, use `metafree`, and -for vice versa i.e., remove the geometry and keep the metadata use `meta`. - -### Syntax - -```julia -meta(geometry, meta::NamedTuple) -meta(geometry; meta...) - -metafree(meta-geometry) -meta(meta-geometry) -``` - -### Examples - -```@repl meta -using GeometryBasics -p1 = Point(2.2, 3.6) -poi = meta(p1, city="Abuja", rainfall=1221.2) -``` - -Metadata is stored in a NamedTuple and can be retrieved as such: - -```@repl meta -meta(poi) -``` - -Specific metadata attributes can be directly retrieved: - -```@repl meta -poi.rainfall -metafree(poi) -``` - -Metatypes are predefined for geometries: - -```@repl meta -multipoi = MultiPointMeta([p1], city="Abuja", rainfall=1221.2) -``` - -(In the above example we have also used a geometry-specific meta method.) - -```@repl meta -GeometryBasics.MetaType(Polygon) -GeometryBasics.MetaType(Mesh) -``` - -The metageometry objects are infact composed of the original geometry types. - -```@repl meta -GeometryBasics.MetaFree(PolygonMeta) -GeometryBasics.MetaFree(MeshMeta) -``` - -## MetaT - -In GeometryBasics we can have tabular layout for a collection of meta-geometries -by putting them into a StructArray that extends the [Tables.jl](https://github.com/JuliaData/Tables.jl) API. - -In practice it's not necessary for the geometry or metadata types to be consistent. -For example, a geojson format can have heterogeneous geometries. Hence, such cases require -automatic widening of the geometry data types to the most appropriate type. -The MetaT method works around the fact that, a collection of geometries and metadata -of different types can be represented tabularly whilst widening to the appropriate type. - -### Syntax - -```julia -MetaT(geometry, meta::NamedTuple) -MetaT(geometry; meta...) -``` -Returns a `MetaT` that holds a geometry and its metadata `MetaT` acts the same as `Meta` method. -The difference lies in the fact that it is designed to handle geometries and metadata of different/heterogeneous types. - -For example, while a Point MetaGeometry is a `PointMeta`, the MetaT representation is `MetaT{Point}`. - -### Examples - -```@repl meta -MetaT(Point(1, 2), city = "Mumbai") -``` - -For a tabular representation, an iterable of `MetaT` types can be passed on to a `meta_table` method. - -### Syntax - -```julia -meta_table(iter) -``` - -### Examples - - Create an array of 2 linestrings: - -```@repl meta -ls = [LineString([Point(i, i+1), Point(i-1,i+5)]) for i in 1:2]; -coordinates.(ls) -``` - -Create a multi-linestring: - -```@repl meta -mls = MultiLineString(ls); -coordinates.(mls) -``` - -Create a polygon: - -```@repl meta -poly = Polygon(Point{2, Int}[(40, 40), (20, 45), (45, 30), (40, 40)]); -coordinates(poly) -``` - -Put all geometries into an array: - -```@repl meta -geom = [ls..., mls, poly]; -``` - -Generate some random metadata: - -```@repl meta -prop = [(country_states = "India$(i)", rainfall = (i*9)/2) for i in 1:4] -feat = [MetaT(i, j) for (i,j) = zip(geom, prop)]; # create an array of MetaT -``` - -We can now generate a `StructArray` / `Table` with `meta_table`: - -```@repl meta -sa = meta_table(feat); -``` - -The data can be accessed through `sa.main` and the metadata through -`sa.country_states` and `sa.rainfall`. Here we print only the type names of the -data items for brevity: - -```@repl meta -[nameof.(typeof.(sa.main)) sa.country_states sa.rainfall] -``` - -### Disadvantages - - * The MetaT is pretty generic in terms of geometry types, it's not subtype to - geometries. eg : A `MetaT{Point, NamedTuple{Names, Types}}` is not subtyped to - `AbstractPoint` like a `PointMeta` is. - - * This might cause problems on using `MetaT` with other constructors/methods - inside or even outside GeometryBasics methods designed to work with the main `Meta` types. diff --git a/docs/src/primitives.md b/docs/src/primitives.md index 2f28a12c..a0712e97 100644 --- a/docs/src/primitives.md +++ b/docs/src/primitives.md @@ -1,17 +1,181 @@ # Primitives -## Points and Vectors +In GeometryBasics.jl, a `GeometryPrimitive` is an object from which a mesh can +be constructed. -## Simplices +## Existing GeometryPrimitives -## Shapes +GeometryBasics comes with a few predefined primitives: -* [`Circle`](@ref) -* [`Sphere`](@ref) -* [`Cylinder`](@ref) +#### HyperRectangle -## Abstract types +A `Rect{D, T} = HyperRectangle{D, T}` is a D-dimensional axis-aligned +hyperrectangle defined by an origin and a size. -* `GeometryPrimitive` -* `AbstractSimplex` -* [`AbstractMesh`](@ref) +```@repl rects +using GeometryBasics +r1 = HyperRectangle{4, Float64}(Point{4, Float64}(0), Vec{4, Float64}(1)) +r2 = Rect3f(Point3f(-1), Vec3f(2)) +r3 = Rect2i(0, 0, 1, 1) +``` + +Rect2 supports normal and texture coordinate generation as well as tesselation. +Without tesselation, the coordinates of 2D Rects are defined in anti-clockwise order. +Rect3 supports normals and texture coordinates, but not tesselation. + +Shorthands: + +| |`T`(eltype) |`Float64` |`Float32` |`Int` |`UInt` | +|--------|------------|----------|----------|----------|----------| +|`N`(dim)|`Rect{N,T}` |`Rectd{N}`|`Rectf{N}`|`Recti{N}`|`Rectui{N}`| +|`2` |`Rect2{T}` |`Rect2d` |`Rect2f` |`Rect2i` |`Rect2ui` | +|`3` |`Rect3{T}` |`Rect3d` |`Rect3f` |`Rect3i` |`Rect3ui` | + +#### Sphere and Circle + +`Circle` and `Sphere` are the 2 and 3 dimensional variants of `HyperSphere`. +They are defined by an origin and a radius. +While you can technically create a HyperSphere of any dimension, decomposition +is only defined in 2D and 3D. + +```@repl hypersphere +s1 = HyperSphere{4, Int}(Point{4, Int}(0), 5) +s2 = Sphere(Point3f(0, 0, 1), 1) +s3 = Circle(Point2d(0), 2.0) +``` + +Circle and Sphere support normal and texture coordinate generation as well as tesselation. +The coordinates of Circle are defined in anti-clockwise order. + +#### Cylinder + +A `Cylinder` is a 3D shape defined by two points and a radius. + +```@repl cylinder +c = Cylinder(Point3f(-1, 0, 0), Point3f(0, 0, 1), 0.3f0) # start point, end point, radius +``` + +Cylinder supports normals an Tesselation, but currently no texture coordinates. + +#### Pyramid + +`Pyramid` corresponds to a pyramid shape with a square base and four triangles +coming together into a sharp point. +It is defined by by the center point of the base, its height and its width. + +```@repl pyramid +p = Pyramid(Point3f(0), 1f0, 0.3f0) # center, height, width +``` + +Pyramid supports normals, but currently no texture coordinates or tesselation + +## Tesselation + +In GeometryBasics `Tesselation` is a wrapper type for primitives which communicates +how dense the mesh generated from one should be. + +```@repl tesselation +t = Tesselation(Cylinder(Point3f(0), Point3f(0,0,1), 0.2), 32) # 32 vertices for each circle +normal_mesh(t) + +t = Tesselation(Rect2(Point2f(0), Vec2f(1)), (8, 6)) # 8 vertices in x direction by 6 in y direction +triangle_mesh(t) +``` + +## Primitive Interface / Implementing a new GeometryPrimitive + +Every primitive should inherit from `GeometryPrimitive{Dim, eltype}` and implement at least `coordinates(primitive)` and `faces(primitive)` so that a mesh can be build from it. +This will also be enough to automatically generate normals for a 3D primitive and texture coordinates for a 2D primitive. +You can also implement functions to generate them directly with `normals(primitive)` and `texturecoordinates(primitive)`. +Depending on your primitive this might be necessary to get the normals and uvs you want. + +To be compatible with `Tesselation` all of the functions mentioned above should implement a second tesselation argument. +This will be the second argument passed to the Tesselation constructor. +It's up to you to decide what makes sense here, though typically it's just an integer that more or less corresponds to the number of generated vertices. + +#### Example + +As an example, let's implement a parallelepiped, i.e. a 3D version or a parallelogram. +In this case we need an origin and 3 vectors telling us how far and in which directions the object extends. + +```julia +struct Parallelepiped{T} <: GeometryPrimitive{3, T} + origin::Point{3, T} + v1::Vec{3, T} + v2::Vec{3, T} + v3::Vec{3, T} +end +``` + +Like the `Rect{3}`, this object comes with 8 unique positions which we want to return as its `coordinates`. + +```julia +function GeometryBasics.coordinates(primitive::Parallelepiped{T}) where {T} + o = primitive.origin + v1 = primitive.v1; v2 = primitive.v2; v3 = primitive.v3 + return Point{3, T}[o, o+v2, o+v1+v2, o+v1, o+v3, o+v2+v3, o+v1+v2+v3, o+v1+v3] +end +``` + +To connect these points into a mesh, we need to generate a set of faces. +The faces of a prallelepiped are parallelograms, which we can describe with `QuadFace`. +Here we should be concious of the winding direction of faces. +They are often used to determine the front vs the backside of a (2D) face. +For example GeometryBasics normal generation and OpenGL's backface culling assume a counter-clockwise windig direction to correspond to a front-facing face. +This means that if we look at a face from outside the shape, the positions referred to by that face should be ordered counter-clockwise. +With that in mind the faces of our primitive become: + +```julia +function GeometryBasics.faces(::Parallelepiped) + return QuadFace{Int}[ + (1, 2, 3, 4), (5, 8, 7, 6), # facing -n3, +n3 (n3 being the normal of v1 x v2) + (1, 5, 6, 2), (4, 3, 7, 8), # facing -n2, +n2 + (2, 6, 7, 3), (1, 4, 8, 5), # facing -n1, +n1 + ] +end +``` + +Note that you can check the correct winding direction fairly easily with Makie and the default generated normals. +After implementing faces and coordinates, you can create a mesh plot of your primitive with `Makie.mesh(primitive)`. +If the mesh reacts to light in a reasonable way, i.e. gets brighter when light shines on it, then your faces have the correct winding direction. +(It maybe useful to compare to other primitives like `Sphere(Point3f(0), 1f0)` here.) + +Next on our TODO list are normals. +The default normals produced by `GeometryBasics.normal(primitive)` are vertex normals, which assume that a primitive to be smooth. +Since this is not the case for our primitive, we need to implement custom normals. +Here we could rely on `GeometryBasics.face_normal()` which returns a normal per face, but for this example we will implement them ourselves. + +For our shape we want one normal per face, pointing in the normal direction of the corresponding 2D plane. +We can calculate the normal vector as `n = normalize(cross(v, w))` where v and w correspond to combinations of v1, v2 and v3. +To get them to act per face rather than per vertex, we need to overwrite the faces generated by `faces()`. +We can do that by creating a `FaceView` with a new set of faces which only act on normals. +Each of these new faces needs to refer to one normal by index to get what we want. + +```julia +using LinearAlgebra +function GeometryBasics.normals(primitive::Parallelepiped) + n1 = normalize(cross(primitive.v2, primitive.v3)) + n2 = normalize(cross(primitive.v3, primitive.v1)) + n3 = normalize(cross(primitive.v1, primitive.v2)) + ns = [-n3, n3, -n2, n2, -n1, n1] + fs = QuadFace{Int}[1, 2, 3, 4, 5, 6] # = [QuadFace{Int}(1), QuadFace{Int}(2), ...] + return FaceView(ns, fs) +end +``` + +As the last piece of the interface we can implement texture coordinates. +They generally refer to a 2D image with normalized 2D coordinates on a per-vertex basis. +There are many ways to define these coordinates. +Here we will partition the image in 2x3 even sized rectangular sections, split by the sign of the normal directions defined above. + +```julia +function GeometryBasics.texturecoordinates(::Parallelepiped{T}) where {T} + uvs = [Vec2f(x, y) for x in range(0, 1, length=4) for y in range(0, 1, 3)] + fs = QuadFace{Int}[ + (1, 2, 5, 4), (2, 3, 6, 5), + (4, 5, 8, 7), (5, 6, 9, 8), + (7, 8, 11, 10), (8, 9, 12, 11) + ] + return FaceView(uvs, fs) +end +``` \ No newline at end of file diff --git a/docs/src/rectangles.md b/docs/src/rectangles.md deleted file mode 100644 index 09c9795f..00000000 --- a/docs/src/rectangles.md +++ /dev/null @@ -1 +0,0 @@ -# Rectangles diff --git a/docs/src/static_array_types.md b/docs/src/static_array_types.md new file mode 100644 index 00000000..6d2c2907 --- /dev/null +++ b/docs/src/static_array_types.md @@ -0,0 +1,38 @@ +# Point, Vec and Mat + +GeometryBasics defines its own set of (small) Static Vectors and Matrices: +```julia +Point{N,T} <: StaticVector{N,T} <: AbstractVector{T} +Vec{N,T} <: StaticVector{N,T} <: AbstractVector{T} +Mat{Row, Column, T, L} <: AbstractMatrix{T} +``` + +These types are used throughout GeometryBasics to speed up calculations similar to how StaticArrays.jl does. + +## Aliases + +GeometryBasics exports common aliases for Point, Vec, Mat and Rect: + +### Vec + +| |`T`(eltype) |`Float64` |`Float32` |`Int` |`UInt` | +|--------|------------|----------|----------|----------|----------| +|`N`(dim)|`Vec{N,T}` |`Vecd{N}` |`Vecf{N}` |`Veci{N}` |`Vecui{N}`| +|`2` |`Vec2{T}` |`Vec2d` |`Vec2f` |`Vec2i` |`Vec2ui` | +|`3` |`Vec3{T}` |`Vec3d` |`Vec3f` |`Vec3i` |`Vec3ui` | + +### Point + +| |`T`(eltype) |`Float64` |`Float32` |`Int` |`UInt` | +|--------|------------|----------|----------|----------|----------| +|`N`(dim)|`Point{N,T}`|`Pointd{N}`|`Pointf{N}`|`Pointi{N}`|`Pointui{N}`| +|`2` |`Point2{T}` |`Point2d` |`Point2f` |`Point2i` |`Point2ui`| +|`3` |`Point3{T}` |`Point3d` |`Point3f` |`Point3i` |`Point3ui`| + +### Mat + +| |`T`(eltype) |`Float64` |`Float32` |`Int` |`UInt` | +|--------|------------|----------|----------|----------|----------| +|`N`(dim)|`Mat{N,T}` |`Matd{N}` |`Matf{N}` |`Mati{N}` |`Matui{N}`| +|`2` |`Mat2{T}` |`Mat2d` |`Mat2f` |`Mat2i` |`Mat2ui` | +|`3` |`Mat3{T}` |`Mat3d` |`Mat3f` |`Mat3i` |`Mat3ui` | diff --git a/src/GeometryBasics.jl b/src/GeometryBasics.jl index dc542474..21554b83 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -1,6 +1,6 @@ module GeometryBasics -using IterTools, LinearAlgebra +using IterTools, LinearAlgebra, StaticArrays using GeoInterface import Extents using EarCut_jll @@ -40,10 +40,11 @@ export Triangle export AbstractFace, TriangleFace, QuadFace, GLTriangleFace export OffsetInteger, ZeroIndex, OneIndex, GLIndex export decompose, coordinates, faces, normals, decompose_uv, decompose_normals, - texturecoordinates + texturecoordinates, vertex_attributes +export clear_faceviews +export face_normals export Tesselation, Normal, UV, UVW -export AbstractMesh, Mesh, MetaMesh -export add_meta, pop_meta +export AbstractMesh, Mesh, MetaMesh, FaceView # all the different predefined mesh types @@ -56,7 +57,7 @@ export uv_mesh, normal_mesh, uv_normal_mesh export height, origin, radius, width, widths export HyperSphere, Circle, Sphere -export Cylinder, Cylinder2, Cylinder3, Pyramid, extremity +export Cylinder, Pyramid, extremity export HyperRectangle, Rect, Rect2, Rect3, Recti, Rect2i, Rect3i, Rectf, Rect2f, Rect3f, Rectd, Rect2d, Rect3d export before, during, meets, overlaps, intersects, finishes export centered, direction, area, volume, update @@ -64,9 +65,8 @@ export max_dist_dim, max_euclidean, max_euclideansq, min_dist_dim, min_euclidean export min_euclideansq, minmax_dist_dim, minmax_euclidean, minmax_euclideansq export self_intersections, split_intersections -if Base.VERSION >= v"1.4.2" +if Base.VERSION >= v"1.8" include("precompiles.jl") - _precompile_() end end # module diff --git a/src/basic_types.jl b/src/basic_types.jl index 0b3f39af..855102c2 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -1,18 +1,30 @@ """ -Abstract Geometry in R{Dim} with Number type T + abstract type AbstractGeometry{Dimension, T<:Number} + +Base type for geometry types like GeometryPrimites and Polytopes. """ abstract type AbstractGeometry{Dim,T<:Number} end abstract type GeometryPrimitive{Dim,T} <: AbstractGeometry{Dim,T} end Base.ndims(::AbstractGeometry{Dim}) where {Dim} = Dim """ -Geometry made of N connected points. Connected as one flat geometry, it makes a Ngon / Polygon. -Connected as volume it will be a Simplex / Tri / Cube. -Note That `Polytope{N} where N == 3` denotes a Triangle both as a Simplex or Ngon. + Polytope{Dim, T} <: AbstractGeometry{Dim, T} + +A Polytope is the generalization of a Polygon to higher dimensions, i.e. a +geometric object consisting of flat faces. + +A `Polygon` and `Ngon` are both 2D `Polytope`s. A `Simplex` is the simplest +`Polytope` in arbitrary dimensions. """ abstract type Polytope{Dim,T} <: AbstractGeometry{Dim,T} end abstract type AbstractPolygon{Dim,T} <: Polytope{Dim,T} end +""" + AbstractFace{N_indices, T} <: StaticVector{N_indices, T} + +Parent type for all face types. The standard face type is typically a +`GLTriangleFace = NgonFace{3, GLIndex}`. +""" abstract type AbstractFace{N,T} <: StaticVector{N,T} end abstract type AbstractSimplexFace{N,T} <: AbstractFace{N,T} end abstract type AbstractNgonFace{N,T} <: AbstractFace{N,T} end @@ -32,20 +44,88 @@ end const TetrahedronFace{T} = SimplexFace{4,T} Face(::Type{<:SimplexFace{N}}, ::Type{T}) where {N,T} = SimplexFace{N,T} -""" -Face index, connecting points to form an Ngon -""" @fixed_vector NgonFace = AbstractNgonFace +""" + NgonFace{N, T} + +A planar face connecting N vertices. Shorthands include: +- `LineFace{T} = NgonFace{2,T}` +- `TriangleFace{T} = NgonFace{3,T}` +- `QuadFace{T} = NgonFace{4,T}` +- `GLTriangleFace = TriangleFace{GLIndex}` +""" +NgonFace const LineFace{T} = NgonFace{2,T} const TriangleFace{T} = NgonFace{3,T} const QuadFace{T} = NgonFace{4,T} const GLTriangleFace = TriangleFace{GLIndex} -function Base.show(io::IO, x::TriangleFace{T}) where {T} - return print(io, "TriangleFace(", join(x, ", "), ")") +function Base.show(io::IO, x::NgonFace{N, T}) where {N, T} + if N == 2 + name = "LineFace{$T}" + elseif N == 3 + if T == GLIndex + name = "GLTriangleFace" + else + name = "TriangleFace{$T}" + end + elseif N == 4 + name = "QuadFace{$T}" + else + name = "NgonFace{$N, $T}" + end + + return print(io, name, "(", join(value.(x), ", "), ")") +end + +# two faces are the same if they match or they just cycle indices +function Base.:(==)(f1::FT, f2::FT) where {N, FT <: AbstractFace{N}} + _, min_i1 = findmin(f1.data) + _, min_i2 = findmin(f2.data) + @inbounds for i in 1:N + if f1[mod1(min_i1 + i, end)] !== f2[mod1(min_i2 + i, end)] + return false + end + end + return true +end +function Base.hash(f::AbstractFace{N}, h::UInt) where {N} + _, min_i = findmin(f.data) + @inbounds for i in min_i:N + h = hash(f[i], h) + end + @inbounds for i in 1:min_i-1 + h = hash(f[i], h) + end + return h +end +Base.isequal(f1::AbstractFace, f2::AbstractFace) = ==(f1, f2) + +# Fastpaths +Base.:(==)(f1::FT, f2::FT) where {FT <: AbstractFace{2}} = minmax(f1.data...) == minmax(f2.data...) +Base.hash(f::AbstractFace{2}, h::UInt) = hash(minmax(f.data...), h) + +function Base.:(==)(f1::FT, f2::FT) where {FT <: AbstractFace{3}} + return (f1.data == f2.data) || (f1.data == (f2[2], f2[3], f2[1])) || + (f1.data == (f2[3], f2[1], f2[2])) +end +function Base.hash(f::AbstractFace{3}, h::UInt) + if f[1] < f[2] + if f[1] < f[3] + return hash(f.data, h) + else + return hash((f[3], f[1], f[2]), h) + end + else + if f[2] < f[3] + return hash((f[2], f[3], f[1]), h) + else + return hash((f[3], f[1], f[2]), h) + end + end end Face(::Type{<:NgonFace{N}}, ::Type{T}) where {N,T} = NgonFace{N,T} @@ -56,12 +136,16 @@ Face(F::Type{NgonFace{N,FT}}, ::Type{T}) where {FT,N,T} = F @propagate_inbounds Base.iterate(x::Polytope, i) = iterate(coordinates(x), i) """ -Fixed Size Polygon, e.g. + Ngon{D, T, N}(points::NTuple{N, Point{D, T}}) + +Defines a flat polygon (without holes) in D dimensional space using N points, e.g.: - N 1-2 : Illegal! - N = 3 : Triangle - N = 4 : Quadrilateral (or Quad, Or tetragon) - N = 5 : Pentagon - ... + +For polygons with holes, see `Polygon`. """ struct Ngon{Dim, T<:Real, N} <: AbstractPolygon{Dim,T} points::NTuple{N, Point{Dim, T}} @@ -81,6 +165,8 @@ Base.length(::Type{<:NNgon{N}}) where {N} = N Base.length(::NNgon{N}) where {N} = N """ + Polytope(::Type{<: Point}, ::Type{<: AbstractNgonFace}) + The Ngon Polytope element type when indexing an array of points with a SimplexFace """ function Polytope(::Type{Point{Dim,T}}, @@ -89,9 +175,14 @@ function Polytope(::Type{Point{Dim,T}}, end """ + Polytope(::Type{<: Ngon}, P::Type{<: Point}) + The fully concrete Ngon type, when constructed from a point type! """ -function Polytope(::Type{<:NNgon{N}}, P::Type{Point{NDim,T}}) where {N,NDim,T} +function Polytope(::Type{<:Ngon{_D, _T, N}}, P::Type{Point{NDim,T}}) where {N,NDim,T, _D,_T} + return Ngon{NDim,T,N} +end +function Polytope(::Type{<:Ngon{_D, _T, N} where {_D,_T}}, P::Type{Point{NDim,T}}) where {N,NDim,T} return Ngon{NDim,T,N} end @@ -121,6 +212,8 @@ function coordinates(lines::AbstractArray{Line{Dim,T}}) where {Dim,T} end """ + Simplex{D, T<:Real, N}(points::NTuple{N, Point{D, T}}) + A `Simplex` is a generalization of an N-dimensional tetrahedra and can be thought of as a minimal convex set containing the specified points. @@ -156,6 +249,8 @@ Base.length(::Type{<:NSimplex{N}}) where {N} = N Base.length(::NSimplex{N}) where {N} = N """ + Polytope(::Type{Point{Dim,T}}, ::Type{<:AbstractSimplexFace{N}}) + The Simplex Polytope element type when indexing an array of points with a SimplexFace """ function Polytope(::Type{Point{Dim,T}}, ::Type{<:AbstractSimplexFace{N}}) where {N,Dim,T} @@ -163,6 +258,8 @@ function Polytope(::Type{Point{Dim,T}}, ::Type{<:AbstractSimplexFace{N}}) where end """ + Polytope(::Type{<:NSimplex{N}}, P::Type{Point{NDim,T}}) + The fully concrete Simplex type, when constructed from a point type! """ function Polytope(::Type{<:NSimplex{N}}, P::Type{Point{NDim,T}}) where {N,NDim,T} @@ -175,6 +272,8 @@ Base.show(io::IO, x::Line) = print(io, "Line(", x[1], " => ", x[2], ")") Polygon(exterior::AbstractVector{<:Point}) Polygon(exterior::AbstractVector{<:Point}, interiors::Vector{<:AbstractVector{<:Point}}) +Constructs a polygon from a set of exterior points. If interiors are given, each +of them cuts away from the Polygon. """ struct Polygon{Dim,T<:Real} <: AbstractPolygon{Dim,T} exterior::Vector{Point{Dim, T}} @@ -224,6 +323,8 @@ end """ MultiPolygon(polygons::AbstractPolygon) + +A collection of polygons """ struct MultiPolygon{Dim, T<:Real} <: AbstractGeometry{Dim, T} polygons::Vector{<:AbstractPolygon{Dim,T}} @@ -239,7 +340,8 @@ Base.length(mp::MultiPolygon) = length(mp.polygons) """ LineString(points::AbstractVector{<:Point}) -A LineString is a geometry of connected line segments + +A LineString is a collection of points connected by line segments. """ struct LineString{Dim, T<:Real} <: AbstractGeometry{Dim, T} points::Vector{Point{Dim, T}} @@ -276,36 +378,303 @@ Base.getindex(mpt::MultiPoint, i) = mpt.points[i] Base.size(mpt::MultiPoint) = size(mpt.points) Base.length(mpt::MultiPoint) = length(mpt.points) + +""" + FaceView(data, faces) + +A FaceView is an alternative to passing a vertex attributes directly to a mesh. +It bundles `data` with a new set of `faces` which may index that data differently +from the faces defined in a mesh. This can be useful to avoid duplication of data. + +For example, `data` can be defined per face by giving each face just one (repeated) +index: +```julia +per_face_normals = FaceView( + normals, # one per face + FT.(eachindex(normals)) # with FT = facetype(mesh) +) +``` + +To remove `FaceView`s from a mesh, e.g. for rendering, use `clear_faceviews(mesh)`. + +You can get the data of a FaceView with `values(faceview)` and the faces with +`faces(faceview)`. +""" +struct FaceView{T, AVT <: AbstractVector{T}, FVT <: AbstractVector{<: AbstractFace}} + data::AVT + faces::FVT +end + +const VertexAttributeType{T} = Union{FaceView{T}, AbstractVector{T}} + +function Base.vcat(a::FaceView, b::FaceView) + N = length(a.data) + return FaceView( + vcat(a.data, b.data), + vcat(a.faces, map(f -> typeof(f)(f .+ N), b.faces)) + ) +end + +faces(x::FaceView) = x.faces +Base.values(x::FaceView) = x.data +facetype(x::FaceView) = eltype(x.faces) +Base.getindex(x::FaceView, f::AbstractFace) = getindex(values(x), f) +Base.isempty(x::FaceView) = isempty(values(x)) +Base.:(==)(a::FaceView, b::FaceView) = (values(a) == values(b)) && (faces(a) == faces(b)) + +# TODO: maybe underscore this as it requires care to make sure all FaceViews and +# mesh faces stay in sync +convert_facetype(::Type{FT}, x::AbstractVector) where {FT <: AbstractFace} = x +function convert_facetype(::Type{FT}, x::FaceView) where {FT <: AbstractFace} + if eltype(faces(x)) != FT + return FaceView(values(x), decompose(FT, faces(x))) + end + return x +end + +function verify(fs::AbstractVector{FT}, fv::FaceView, name = nothing) where {FT <: AbstractFace} + if length(faces(fv)) != length(fs) + error("Number of faces given in FaceView $(length(faces(fv))) does not match reference $(length(fs))") + end + + N = maximum(f -> value(maximum(f)), faces(fv), init = 0) + if length(values(fv)) < N + error("FaceView addresses $N vertices with faces, but only has $(length(values(fv))).") + end + + if isconcretetype(FT) && (FT == facetype(fv)) + return true + end + + for (i, (f1, f2)) in enumerate(zip(faces(fv), fs)) + if length(f1) != length(f2) + error("Length of face $i = $(length(f1)) does not match reference with $(length(f2))") + end + end + + return true +end + +# Dodgy definitions... (since attributes can be FaceView or Array it's often +# useful to treat a FaceView like the vertex data it contains) +Base.length(x::FaceView) = length(values(x)) +# Base.iterate(x::FaceView) = iterate(values(x)) +# Base.getindex(x::FaceView, i::Integer) = getindex(values(x), i) +# Taken from Base/arrayshow.jl +function Base.show(io::IO, ::MIME"text/plain", X::FaceView) + summary(io, X) + isempty(X) && return + print(io, ":") + + if get(io, :limit, false)::Bool && displaysize(io)[1]-4 <= 0 + return print(io, " …") + else + println(io) + end + + io = IOContext(io, :typeinfo => eltype(values(X))) + + recur_io = IOContext(io, :SHOWN_SET => values(X)) + Base.print_array(recur_io, values(X)) +end + + + """ AbstractMesh -An abstract mesh is a collection of Polytope elements (Simplices / Ngons). -The connections are defined via faces(mesh), the coordinates of the elements are returned by -coordinates(mesh). Arbitrary meta information can be attached per point or per face +An abstract mesh is a collection of Polytope elements (Simplices / Ngons). The +connections are defined via faces(mesh) and the coordinates of the elements are +returned by coordinates(mesh). """ abstract type AbstractMesh{Dim, T} <: AbstractGeometry{Dim, T} end """ - Mesh <: AbstractMesh{Element} -The concrete AbstractMesh type. + Mesh{PositionDim, PositionType, FaceType, VertexAttributeNames, VertexAttributeTypes, FaceVectorType} <: AbstractMesh{PositionDim, PositionType} <: AbstractGeometry{PositionDim, PositionType} + +The type of a concrete mesh. The associated struct contains 3 fields: + +```julia +struct Mesh{...} + vertex_attributes::NamedTuple{VertexAttributeNames, VertexAttributeTypes} + faces::FaceVectorType + views::Vector{UnitRange{Int}} +end +``` + +A vertex typically carries multiple distinct pieces of data, e.g. a position, +a normal, a texture coordinate, etc. We call those pieces of data vertex +attributes. The `vertex_attributes` field contains the name and a collection +`<: AbstractVector` or `<: FaceView` for each attribute. The n-th element of that +collection is the value of the corresponding attribute for the n-th vertex. + +```julia +# vertex 1 2 3 +vertex_attributes[:position] = [pos1, pos2, pos3, ...] +vertex_attributes[:normal] = [normal1, normal2, normal3, ...] +... +``` + +A `NamedTuple` is used here to allow different meshes to carry different vertex +attributes while also keeping things type stable. The constructor enforces a +few restrictions: +- The first attribute must be named `position` and must have a `Point{PositionDim, PositionType}` eltype. +- Each vertex attribute must refer to the same number of vertices. (All vertex attributes defined by +AbstractVector must match in length. For FaceViews, the number of faces needs to match.) + +See also: [`vertex_attributes`](@ref), [`coordinates`](@ref), [`normals`](@ref), +[`texturecoordinates`](@ref), [`decompose`](@ref), [`FaceView`](@ref), +[`clear_faceviews`](@ref) + +The `faces` field is a collection `<: AbstractVector{FaceType}` containing faces +that describe how vertices are connected. Typically these are `(GL)TriangleFace`s +or `QuadFace`s, but they can be any collection of vertex indices `<: AbstractFace`. + +See also: [`faces`](@ref), [`decompose`](@ref) + +The `views` field can be used to separate the mesh into mutliple submeshes. Each +submesh is described by a "view" into the `faces` vector, i.e. submesh n uses +`mesh.faces[mesh.views[n]]`. A `Mesh` can be constructed without `views`, which +results in an empty `views` vector. + +See also: [`merge`](@ref), [`split_mesh`](@ref) """ -struct Mesh{Dim, T<:Number, V<:AbstractVector{Point{Dim, T}}, C <: AbstractVector{<: AbstractFace}} <: AbstractMesh{Dim, T} - vertices::V - connectivity::C +struct Mesh{ + Dim, T <: Real, + FT <: AbstractFace, + Names, + VAT <: Tuple{<: AbstractVector{Point{Dim, T}}, Vararg{VertexAttributeType}}, + FVT <: AbstractVector{FT} + } <: AbstractMesh{Dim, T} + + vertex_attributes::NamedTuple{Names, VAT} + faces::FVT + views::Vector{UnitRange{Int}} + + function Mesh( + vertex_attributes::NamedTuple{Names, VAT}, + fs::FVT, + views::Vector{UnitRange{Int}} = UnitRange{Int}[] + ) where { + FT <: AbstractFace, FVT <: AbstractVector{FT}, Names, Dim, T, + VAT <: Tuple{<: AbstractVector{Point{Dim, T}}, Vararg{VertexAttributeType}} + } + + va = vertex_attributes + names = Names + + # verify type + if !haskey(va, :position ) + error("Vertex attributes must have a :position attribute.") + end + + if haskey(va, :normals) + @warn "`normals` as a vertex attribute name has been deprecated in favor of `normal` to bring it in line with mesh.position and mesh.uv" + names = ntuple(i -> ifelse(names[i] == :normal, :normal, names[i]), length(names)) + va = NamedTuple{names}(values(va)) + end + + # verify that all vertex attributes refer to the same number of vertices + # for Vectors this means same length + # for FaceViews this means same number of faces + N = maximum(f -> value(maximum(f)), fs, init = 0) + for (name, attrib) in pairs(va) + if attrib isa FaceView + try + verify(fs, attrib) + catch e + rethrow(ErrorException("Failed to verify $name attribute:\n$(e.msg)")) + end + else + length(attrib) < N && error("Failed to verify $name attribute:\nFaces address $N vertex attributes but only $(length(attrib)) are present.") + end + end + + return new{Dim, T, FT, names, VAT, FVT}(va, fs, views) + end +end + +@inline function Base.hasproperty(mesh::Mesh, field::Symbol) + if field === :normals + @warn "mesh.normals has been deprecated in favor of mesh.normal to bring it in line with mesh.position and mesh.uv" + return hasproperty(mesh, :normal) + end + return hasproperty(getfield(mesh, :vertex_attributes), field) || hasfield(Mesh, field) +end +@inline function Base.getproperty(mesh::Mesh, field::Symbol) + if hasfield(Mesh, field) + return getfield(mesh, field) + elseif field === :normals + @warn "mesh.normals has been deprecated in favor of mesh.normal to bring it in line with mesh.position and mesh.uv" + return getproperty(mesh, :normal) + else + return getproperty(getfield(mesh, :vertex_attributes), field) + end +end +@inline function Base.propertynames(mesh::Mesh) + return (fieldnames(Mesh)..., propertynames(getfield(mesh, :vertex_attributes))...) +end + +coordinates(mesh::Mesh) = mesh.position +faces(mesh::Mesh) = mesh.faces +normals(mesh::Mesh) = hasproperty(mesh, :normal) ? mesh.normal : nothing +texturecoordinates(mesh::Mesh) = hasproperty(mesh, :uv) ? mesh.uv : nothing + +""" + vertex_attributes(mesh::Mesh) + +Returns a dictionairy containing the vertex attributes of the given mesh. +Mutating these will change the mesh. +""" +vertex_attributes(mesh::Mesh) = getfield(mesh, :vertex_attributes) + +Base.getindex(mesh::Mesh, i::Integer) = mesh.position[mesh.faces[i]] +Base.length(mesh::Mesh) = length(mesh.faces) + +function Base.:(==)(a::Mesh, b::Mesh) + return (a.vertex_attributes == b.vertex_attributes) && + (faces(a) == faces(b)) && (a.views == b.views) end -coordinates(mesh::Mesh) = mesh.vertices -faces(mesh::Mesh) = mesh.connectivity -Base.getindex(mesh::Mesh, i::Integer) = mesh.vertices[mesh.connectivity[i]] -Base.length(mesh::Mesh) = length(mesh.connectivity) -Base.:(==)(a::Mesh, b::Mesh) = coordinates(a) == coordinates(b) && faces(a) == faces(b) function Base.iterate(mesh::Mesh, i=1) return i - 1 < length(mesh) ? (mesh[i], i + 1) : nothing end +function Base.convert(::Type{<: Mesh{D, T, FT}}, m::Mesh{D}) where {D, T <: Real, FT <: AbstractFace} + return mesh(m, pointtype = Point{D, T}, facetype = FT) +end + +""" + Mesh(faces[; views, attributes...]) + Mesh(positions, faces[; views]) + Mesh(positions, faces::AbstractVector{<: Integer}[; facetype = TriangleFace, skip = 1]) + Mesh(; attributes...) + +Constructs a mesh from the given arguments. + +If `positions` are given explicitly, they are merged with other vertex attributes +under the name `position`. Otherwise they must be part of `attributes`. If `faces` +are not given `attributes.position` must be a FaceView. + +Any other vertex attribute can be either an `AbstractVector` or a `FaceView` +thereof. Every vertex attribute that is an `AbstractVector` must be sufficiently +large to be indexable by `mesh.faces`. Every vertex attribute that is a `FaceView` +must contain similar faces to `mesh.faces`, i.e. contain the same number of faces +and have faces of matching length. + +`views` can be defined optionally to implicitly split the mesh into multi +sub-meshes. This is done by providing ranges for indexing faces which correspond +to the sub-meshes. By default this is left empty. +""" +function Mesh(faces::AbstractVector{<:AbstractFace}; views::Vector{UnitRange{Int}} = UnitRange{Int}[], attributes...) + return Mesh(NamedTuple(attributes), faces, views) +end + function Mesh(points::AbstractVector{Point{Dim, T}}, - faces::AbstractVector{<:AbstractFace}) where {Dim, T} - return Mesh{Dim, T, }(points, faces) + faces::AbstractVector{<:AbstractFace}; + views = UnitRange{Int}[], kwargs...) where {Dim, T} + va = (position = points, kwargs...) + return Mesh(va, faces, views) end function Mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Integer}, @@ -313,32 +682,87 @@ function Mesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:Integer}, return Mesh(points, connect(faces, facetype, skip)) end -struct MetaMesh{Dim, T, M <: AbstractMesh{Dim, T}, Names, Types} <: AbstractMesh{Dim, T} +function Mesh(; kwargs...) + fs = faces(kwargs[:position]::FaceView) + va = NamedTuple{keys(kwargs)}(map(keys(kwargs)) do k + return k == :position ? values(kwargs[k]) : kwargs[k] + end) + return Mesh(va, fs) +end + +# Shorthand types +const SimpleMesh{N, T, FT} = Mesh{N, T, FT, (:position,), Tuple{Vector{Point{N, T}}}, Vector{FT}} +const NormalMesh{N, T, FT} = Mesh{N, T, FT, (:position, :normal), Tuple{Vector{Point{N, T}}, Vector{Vec3f}}, Vector{FT}} +const NormalUVMesh{N, T, FT} = Mesh{N, T, FT, (:position, :normal, :uv), Tuple{Vector{Point{N, T}}, Vector{Vec3f}, Vector{Vec2f}}, Vector{FT}} + +const GLSimpleMesh{N} = SimpleMesh{N, Float32, GLTriangleFace} +const GLNormalMesh{N} = NormalMesh{N, Float32, GLTriangleFace} +const GLNormalUVMesh{N} = NormalUVMesh{N, Float32, GLTriangleFace} + + + +struct MetaMesh{Dim, T, M <: AbstractMesh{Dim, T}} <: AbstractMesh{Dim, T} mesh::M - meta::NamedTuple{Names, Types} - function MetaMesh(mesh::AbstractMesh{Dim, T}, meta::NamedTuple{Names, Types}) where {Dim, T, Names, Types} - new{Dim, T, typeof(mesh), Names, Types}(mesh, meta) - end + meta::Dict{Symbol, Any} end -function MetaMesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:AbstractFace}; meta...) - MetaMesh(Mesh(points, faces), values(meta)) +""" + MetaMesh(mesh; metadata...) + MetaMesh(positions, faces; metadata...) + +Constructs a MetaMesh either from another `mesh` or by constructing another mesh +with the given `positions` and `faces`. Any keyword arguments given will be +stored in the `meta` field in `MetaMesh`. + +This struct is meant to be used for storage of non-vertex data. Any vertex +related data should be stored as a vertex attribute in `Mesh`. + +The metadata added to the MetaMesh can be manipulated with Dict-like operations +(getindex, setindex!, get, delete, keys, etc). Vertex attributes can be accessed +via fields and the same getters as mesh. The mesh itself can be retrieved with +`Mesh(metamesh)`. +""" +function MetaMesh(mesh::AbstractMesh; kwargs...) + MetaMesh(mesh, Dict{Symbol, Any}(kwargs)) end -function MetaMesh(m::AbstractMesh; kw...) - MetaMesh(Mesh(m), merge(meta(m), values(kw))) +function MetaMesh(points::AbstractVector{<:Point}, faces::AbstractVector{<:AbstractFace}; kwargs...) + MetaMesh(Mesh(points, faces), Dict{Symbol, Any}(kwargs)) +end + + +@inline function Base.hasproperty(mesh::MetaMesh, field::Symbol) + return hasfield(MetaMesh, field) || hasproperty(getfield(mesh, :mesh), field) +end +@inline function Base.getproperty(mesh::MetaMesh, field::Symbol) + if hasfield(MetaMesh, field) + return getfield(mesh, field) + else + return getproperty(getfield(mesh, :mesh), field) + end +end +@inline function Base.propertynames(mesh::MetaMesh) + return (fieldnames(MetaMesh)..., propertynames(getfield(mesh, :mesh))...) end -@inline Base.hasproperty(mesh::MetaMesh, field::Symbol) = hasproperty(getfield(mesh, :meta), field) -@inline Base.getproperty(mesh::MetaMesh, field::Symbol) = getproperty(getfield(mesh, :meta), field) -@inline Base.propertynames(mesh::MetaMesh) = propertynames(getfield(mesh, :meta)) +# TODO: or via getindex? +Base.haskey(mesh::MetaMesh, key::Symbol) = haskey(getfield(mesh, :meta), key) +Base.get(f::Base.Callable, mesh::MetaMesh, key::Symbol) = get(f, getfield(mesh, :meta), key) +Base.get!(f::Base.Callable, mesh::MetaMesh, key::Symbol) = get!(f, getfield(mesh, :meta), key) +Base.get(mesh::MetaMesh, key::Symbol, default) = get(getfield(mesh, :meta), key, default) +Base.get!(mesh::MetaMesh, key::Symbol, default) = get!(getfield(mesh, :meta), key, default) +Base.getindex(mesh::MetaMesh, key::Symbol) = getindex(getfield(mesh, :meta), key) +Base.setindex!(mesh::MetaMesh, value, key::Symbol) = setindex!(getfield(mesh, :meta), value, key) +Base.delete!(mesh::MetaMesh, key::Symbol) = delete!(getfield(mesh, :meta), key) +Base.keys(mesh::MetaMesh) = keys(getfield(mesh, :meta)) coordinates(mesh::MetaMesh) = coordinates(Mesh(mesh)) faces(mesh::MetaMesh) = faces(Mesh(mesh)) -normals(mesh::MetaMesh) = hasproperty(mesh, :normals) ? mesh.normals : nothing -texturecoordinates(mesh::MetaMesh) = hasproperty(mesh, :uv) ? mesh.uv : nothing +normals(mesh::MetaMesh) = normals(Mesh(mesh)) +texturecoordinates(mesh::MetaMesh) = texturecoordinates(Mesh(mesh)) +vertex_attributes(mesh::MetaMesh) = vertex_attributes(Mesh(mesh)) meta(@nospecialize(m)) = NamedTuple() meta(mesh::MetaMesh) = getfield(mesh, :meta) Mesh(mesh::MetaMesh) = getfield(mesh, :mesh) -Mesh(mesh::Mesh) = mesh +Mesh(mesh::Mesh) = mesh \ No newline at end of file diff --git a/src/boundingboxes.jl b/src/boundingboxes.jl index e2deeb32..447a7228 100644 --- a/src/boundingboxes.jl +++ b/src/boundingboxes.jl @@ -3,7 +3,9 @@ function Rect(geometry::AbstractArray{<:Point{N,T}}) where {N,T} end """ -Construct a HyperRectangle enclosing all points. + Rect(points::AbstractArray{<: Point}) + +Construct a bounding box countaining all the given points. """ function Rect{N1,T1}(geometry::AbstractArray{PT}) where {N1,T1,PT<:Point} N2, T2 = length(PT), eltype(PT) @@ -11,7 +13,7 @@ function Rect{N1,T1}(geometry::AbstractArray{PT}) where {N1,T1,PT<:Point} vmin = Point{N2,T2}(typemax(T2)) vmax = Point{N2,T2}(typemin(T2)) for p in geometry - vmin, vmax = minmax(p, vmin, vmax) + vmin, vmax = _minmax(p, vmin, vmax) end o = vmin w = vmax - vmin @@ -23,6 +25,11 @@ function Rect{N1,T1}(geometry::AbstractArray{PT}) where {N1,T1,PT<:Point} end end +""" + Rect(primitive::GeometryPrimitive) + +Construct a bounding box for the given primitive. +""" function Rect(primitive::GeometryPrimitive{N,T}) where {N,T} return Rect{N,T}(primitive) end diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index 3753060a..341b8ec2 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -1,260 +1,148 @@ -using LinearAlgebra -import Random -import Base: setindex - -abstract type StaticVector{N, T} <: AbstractVector{T} end -function similar_type end - -struct StaticArrayStyle{T, AsConst} <: Broadcast.BroadcastStyle end -StaticArrayStyle{T}() where T = StaticArrayStyle{T, false}() +function unit(::Type{T}, i::Integer) where {T <: StaticVector} + tup = ntuple(Val(length(T))) do j + return ifelse(i == j, 1, 0) + end + return T(tup) +end macro fixed_vector(name_parent) @assert name_parent.head == :(=) - VecT, SuperT = name_parent.args - + name, parent = name_parent.args expr = quote - struct $(VecT){N, T} <: $(SuperT){N, T} - data::NTuple{N,T} + struct $(name){S,T} <: $(parent){S,T} + data::NTuple{S,T} - function $(VecT){N,T}(x::NTuple{N,T}) where {N,T} - return new{N,T}(x) + function $(name){S,T}(x::NTuple{S,T}) where {S,T} + return new{S,T}(x) end - function $(VecT){N,T}(x::NTuple{N,Any}) where {N,T} - return new{N,T}(convert(NTuple{N, T}, x)) + function $(name){S,T}(x::NTuple{S,Any}) where {S,T} + return new{S,T}(StaticArrays.convert_ntuple(T, x)) end - $(VecT){1, T}(x::Tuple{T}) where {T} = new{1, T}(x) end + size_or(::Type{$(name)}, or) = or + eltype_or(::Type{$(name)}, or) = or + eltype_or(::Type{$(name){S,T} where S}, or) where {T} = T + eltype_or(::Type{$(name){S,T} where T}, or) where {S} = or + eltype_or(::Type{$(name){S,T}}, or) where {S,T} = T + + size_or(::Type{$(name){S,T} where S}, or) where {T} = or + size_or(::Type{$(name){S,T} where T}, or) where {S} = Size{(S,)}() + size_or(::Type{$(name){S,T}}, or) where {S,T} = (S,) + # Array constructor - $(VecT)(x::AbstractVector) = error("You need to supply size of vector") - $(VecT){N}(x::AbstractVector{T}) where {N,T} = $(VecT){N,T}(x) - function $(VecT){N,T1}(x::AbstractVector{T2}) where {N,T1,T2} - @assert N <= length(x) - return $(VecT){N,T1}(ntuple(i -> convert(T1, x[i]), N)) + function $(name){S}(x::AbstractVector{T}) where {S,T} + @assert S <= length(x) + return $(name){S,T}(ntuple(i -> x[i], Val(S))) end - # StaticVector conversion - $(VecT)(x::StaticVector{N, T}) where {N,T} = $(VecT){N, T}(x) - $(VecT){N}(x::StaticVector{N2, T}) where {N,N2,T} = $(VecT){N,T}(x) - function $(VecT){N1,T1}(x::StaticVector{N2, T2}) where {N1,T1,N2,T2} - @assert N1 <= N2 - return $(VecT){N1,T1}(ntuple(i -> convert(T1, x[i]), N1)) - end - function $(VecT){1,T1}(x::StaticVector{N2, T2}) where {T1,N2,T2} - @assert 1 <= N2 - return $(VecT){1,T1}(ntuple(i -> convert(T1, x[i]), 1)) + function $(name){S,T1}(x::AbstractVector{T2}) where {S,T1,T2} + @assert S <= length(x) + return $(name){S,T1}(ntuple(i -> convert(T1, x[i]), Val(S))) end - # repeat - $(VecT){N}(x::T) where {N,T<:Number} = $(VecT){N, T}(x) - function $(VecT){N,T}(x::Number) where {N,T} - return $(VecT){N,T}(ntuple(i -> convert(T, x), N)) + function $(name){S,T}(x) where {S,T} + return $(name){S,T}(ntuple(i -> convert(T, x), Val(S))) end - $(VecT){1, T}(x::Number) where {T} = $(VecT){1, T}((x,)) - $(VecT){1, T}(x::Tuple{Any}) where T = $(VecT){1, T}((T(x[1]),)) - $(VecT)(x::Tuple) = $(VecT)(promote(x...)) - $(VecT){N}(x::Tuple) where {N} = $(VecT){N}(promote(x...)) - $(VecT){N, T}(x::Tuple) where {N,T} = $(VecT){N,T}(convert(NTuple{N,T}, x)) + $(name){S}(x::T) where {S,T} = $(name){S,T}(ntuple(i -> x, Val(S))) + $(name){1,T}(x::T) where {T} = $(name){1,T}((x,)) + $(name)(x::NTuple{S}) where {S} = $(name){S}(x) + function $(name)(x::T) where {S,T <: Tuple{Vararg{Any,S}}} + return $(name){S,StaticArrays.promote_tuple_eltype(T)}(x) + end - $(VecT)(x::NTuple{N, T}) where {N, T} = $(VecT){N,T}(x) - $(VecT){N}(x::NTuple{N, T}) where {N, T} = $(VecT){N,T}(x) + function $(name){S}(x::T) where {S,T <: Tuple} + return $(name){S,StaticArrays.promote_tuple_eltype(T)}(x) + end + $(name){S,T}(x::StaticVector) where {S,T} = $(name){S,T}(Tuple(x)) - $(VecT)(x::Vararg{Any,N}) where {N} = $(VecT){N}(x) - $(VecT)(x::Vararg{T,N}) where {T,N} = $(VecT){N,T}(x) + @generated function (::Type{$(name){S,T}})(x::$(name)) where {S,T} + idx = [:(x[$i]) for i in 1:S] + return quote + $($(name)){S,T}($(idx...)) + end + end - $(VecT){N}(x::Vararg{Any,N}) where {N} = $(VecT){N}(x) - $(VecT){N}(x::Vararg{T,N}) where {T,N} = $(VecT){N,T}(x) + @generated function Base.convert(::Type{$(name){S,T}}, x::$(name)) where {S,T} + idx = [:(x[$i]) for i in 1:S] + return quote + $($(name)){S,T}($(idx...)) + end + end - $(VecT){N, T}(x::Vararg{Any,N}) where {T,N} = $(VecT){N,T}(x) - $(VecT){N, T1}(x::Vararg{T2,N}) where {T1,T2,N} = $(VecT){N, T1}(x) + @generated function (::Type{SV})(x::StaticVector) where {SV <: $(name)} + len = size_or(SV, size(x))[1] + return if length(x) == len + :(SV(Tuple(x))) + elseif length(x) > len + elems = [:(x[$i]) for i in 1:len] + :(SV($(Expr(:tuple, elems...)))) + else + error("Static Vector too short: $x, target type: $SV") + end + end - Base.convert(::Type{$(VecT){N,T}}, x) where {N,T} = $(VecT){N,T}(x) - Base.convert(::Type{$(VecT){N}}, x) where {N} = $(VecT){N}(x) - Base.convert(::Type{$(VecT){N}}, x::$(VecT){N}) where {N} = x - Base.convert(::Type{$(VecT){N,T}}, x::$(VecT){N,T}) where {N,T} = x + @generated function $(name){S}(x::StaticVector{N, T}) where {S, N, T} + SV = $(name){S, T} + len = size_or(SV, size(x))[1] + return if length(x) == len + :($(SV)(Tuple(x))) + elseif length(x) > len + elems = [:(x[$i]) for i in 1:len] + :($(SV)($(Expr(:tuple, elems...)))) + else + error("Static Vector too short: $x, target type: $SV") + end + end + Base.@propagate_inbounds function Base.getindex(v::$(name){S,T}, i::Int) where {S,T} + return v.data[i] + end - function Base.convert(::Type{$(VecT){N,T}}, x::NTuple{N,T}) where {N,T} - return $(VecT){N,T}(x) + Base.Tuple(v::$(name)) = v.data + function Base.convert(::Type{$(name){S,T}}, x::NTuple{S,T}) where {S,T} + return $(name){S,T}(x) end - function Base.convert(::Type{$(VecT){N,T}}, x::Tuple) where {N,T} - return $(VecT){N,T}(convert(NTuple{N,T}, x)) + function Base.convert(::Type{$(name){S,T}}, x::Tuple) where {S,T} + return $(name){S,T}(convert(NTuple{S,T}, x)) end - Base.convert(::Type{$(VecT)}, x::Tuple) = $(VecT)(x) - - @inline similar_type(::$(VecT){N, T}, n::Integer) where {N, T} = $(VecT){n} - @inline similar_type(::$(VecT){N}, ::Type{T}) where {N, T} = $(VecT){N, T} - @inline similar_type(::$(VecT), n::Integer, ::Type{T}) where {T} = $(VecT){n, T} - @inline similar_type(::$(VecT)) = $(VecT) - - Base.BroadcastStyle(::Type{<: $(VecT)}) = StaticArrayStyle{$(VecT)}() - Base.values(v::$(VecT)) = v.data - function LinearAlgebra.cross(a::$(VecT){3}, b::$(VecT){3}) - @inbounds elements = (a[2]*b[3]-a[3]*b[2], - a[3]*b[1]-a[1]*b[3], - a[1]*b[2]-a[2]*b[1]) - return $(VecT)(elements) + @generated function StaticArrays.similar_type(::Type{SV}, ::Type{T}, + s::Size{S}) where {SV <: $(name),T,S} + return if length(S) === 1 + $(name){S[1],T} + else + StaticArrays.default_similar_type(T, s(), Val{length(S)}) + end end - end - return esc(expr) -end -# Broadcasting -# style rules -Base.BroadcastStyle(::StaticArrayStyle{T, B1}, ::StaticArrayStyle{T, B2}) where {B1, B2, T} = StaticArrayStyle{T, B1 || B2}() -Base.BroadcastStyle(s::StaticArrayStyle, ::Broadcast.AbstractArrayStyle{0}) = s -Base.BroadcastStyle(::Broadcast.AbstractArrayStyle{0}, s::StaticArrayStyle) = s -Base.BroadcastStyle(s::StaticArrayStyle, ::Broadcast.Style{Tuple}) = s -Base.BroadcastStyle(::Broadcast.Style{Tuple}, s::StaticArrayStyle) = s -Base.BroadcastStyle(::StaticArrayStyle{T, B}, ::Broadcast.BroadcastStyle) where {B, T} = StaticArrayStyle{T, true}() -Base.BroadcastStyle(::Broadcast.BroadcastStyle, ::StaticArrayStyle{T, B}) where {B, T} = StaticArrayStyle{T, true}() -# to allow mixing types, define: -# Base.BroadcastStyle(::StaticArrayStyle{<: Type1, B1}, ::StaticArrayStyle{<: Type2, B2}) where {B1, B2} = -# StaticArrayStyle{preffered_type, B1 || B2}() -# Base.BroadcastStyle(::StaticArrayStyle{<: Type2, B1}, ::StaticArrayStyle{<: Type1, B2}) where {B1, B2} = -# StaticArrayStyle{preffered_type, B1 || B2}() - -# If we don't inherit from AbstractVector we need this? -# Base.broadcastable(x::StaticVector) = x - -# Required to avoid size missmatches between Array and StaticVector -function Broadcast.instantiate(bc::Broadcast.Broadcasted{<: StaticArrayStyle{<: Any, true}}) - # transform this to an Array broadcast with Ref'd StaticVectors and tuples - args_converted = map(arg -> arg isa Broadcast.Broadcasted ? copy(Broadcast.instantiate(arg)) : arg, bc.args) - maybe_const_args = map(args_converted) do arg - style = Base.BroadcastStyle(typeof(arg)) - if style isa Broadcast.AbstractArrayStyle # value or Array - return arg - else # tuple, StaticVector - return Ref(arg) + Base.:(*)(a::$name, b::$name) = a .* b + function Base.broadcasted(f, a::AbstractArray{T}, b::$name) where {T <: $name} + return Base.broadcasted(f, a, (b,)) end end - return Broadcast.broadcasted(bc.f, maybe_const_args...) -end - -# resolve element-wise operation -function Base.copy(bc::Broadcast.Broadcasted{StaticArrayStyle{T, false}}) where T - # Broadcasted may end up in args from nested calls (e.g. foo(a, b .+ c); a .+ b .+ c) - args = map(arg -> values(arg isa Broadcast.Broadcasted ? copy(arg) : arg), bc.args) - return T(broadcast(bc.f, args...)) -end - -Base.map(f, a::StaticVector, args::AbstractArray...) = broadcast(f, a, args...) -Base.map(f, a::AbstractArray, b::StaticVector, args::AbstractArray...) = broadcast(f, a, b, args...) -Base.map(f, a::StaticVector, b::StaticVector, args::AbstractArray...) = broadcast(f, a, b, args...) - -function Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{V}) where V <: StaticVector{N,T} where {N, T} - V(ntuple(i-> rand(rng, T), N)) -end -function Random.randn(rng::Random.AbstractRNG, ::Type{V}) where V <: StaticVector{N,T} where {N, T} - V(ntuple(i-> randn(rng, T), N)) -end - -Base.@propagate_inbounds function Base.getindex(v::StaticVector{N,T}, i::Integer) where {N,T} - return v.data[i] -end - -Base.setindex(c::V, v, i::Integer) where {V <: StaticVector} = V(Base.setindex(c.data, v, i)) - -Base.@propagate_inbounds function Base.getindex(a::StaticVector{N1, T}, idx::StaticVector{N, <:Integer}) where {N,N1,T} - return similar_type(idx, N, T)(map(i-> a[i], idx)) -end - -Base.:(-)(a::StaticVector) = (-).(a) - -import Base: *, +, -, / - -for op in [:*, :+, :-, :/] - @eval begin - ($op)(a::StaticVector, b::StaticVector) = Base.broadcast($(op), a, b) - ($op)(a::Number, b::StaticVector) = Base.broadcast($(op), a, b) - ($op)(a::StaticVector, b::Number) = Base.broadcast($(op), a, b) - end -end - -LinearAlgebra.cross(a::StaticVector{2}, b::StaticVector{2}) = a[1]*b[2]-a[2]*b[1] -LinearAlgebra.norm(a::StaticVector) = sqrt(dot(a,a)) -LinearAlgebra.normalize(a::StaticVector) = a ./ norm(a) - -Base.eltype(::StaticVector{N, T}) where {N, T} = T -Base.eltype(::Type{<: StaticVector{N, T}}) where {N, T} = T - -Base.size(::StaticVector{N}) where {N} = (N,) -Base.size(::Type{<: StaticVector{N}}) where {N} = (N,) -Base.length(::StaticVector{N}) where {N} = N -Base.length(::Type{<: StaticVector{N}}) where {N} = N -Base.ndims(::Type{<: StaticVector}) = 1 - -Base.copy(v::StaticVector) = deepcopy(v) - -Base.vcat(a::StaticVector, b::StaticVector) = (a..., b...) - -function Base.iterate(A::StaticVector, i=1) - i - 1 < length(A) ? (A[i], i + 1) : nothing -end - -function unit(::Type{T}, i::Integer) where {T <: StaticVector} - tup = ntuple(Val(length(T))) do j - return ifelse(i == j, 1, 0) - end - return T(tup) -end - -Base.zero(::Type{V}) where {V <:StaticVector} = V(0) -Base.zero(::V) where {V <:StaticVector} = zero(V) - -function Base.:(==)(a::StaticVector{N}, b::StaticVector{N}) where N - for i in 1:N - a[i] == b[i] || return false - end - return true -end - -function Base.isapprox( - a::StaticVector{N1, T1}, b::StaticVector{N2, T2}; - atol::Real = 0, - rtol::Real = atol > 0 ? 0 : sqrt(max(eps(T1), eps(T2))) - ) where {N1, N2, T1, T2} - return (N1 == N2) && norm(a - b) <= max(atol, rtol * max(norm(a), norm(b))) -end - -@generated function Base.transpose(b::StaticVector{N,T}) where {N,T} - expr = [:(transpose(b[$i])) for i=1:N] - return quote - Mat{1,N,T}($(expr...)) - end + return esc(expr) end -Base.reverse(x::P) where P <: StaticVector = P(reverse(x.data)) -# Since we don't inherit from AbstractArray, some extra functions need to be overloaded -LinearAlgebra.promote_leaf_eltypes(x::StaticVector{N, T}) where {N,T} = T +abstract type AbstractPoint{Dim,T} <: StaticVector{Dim,T} end -@fixed_vector Point = StaticVector +@fixed_vector Point = AbstractPoint @fixed_vector Vec = StaticVector -Base.lastindex(::StaticVector{N}) where N = N - -# Allow mixing Point Vec in broadcast -Base.BroadcastStyle(::StaticArrayStyle{<: Point, B1}, ::StaticArrayStyle{<: Vec, B2}) where {B1, B2} = - StaticArrayStyle{Point, B1 || B2}() -Base.BroadcastStyle(::StaticArrayStyle{<: Vec, B1}, ::StaticArrayStyle{<: Point, B2}) where {B1, B2} = - StaticArrayStyle{Point, B1 || B2}() - -Base.:(+)(a::Vec{N}, b::Point{N}) where {N} = Point{N}(a.data .+ b.data) -const VecTypes{N,T} = Union{StaticVector{N,T}, NTuple{N,T}} -const Vecf{N} = Vec{N, Float32} +const Mat = SMatrix +const VecTypes{N,T} = Union{StaticVector{N,T},NTuple{N,T}} +const Vecf{N} = Vec{N,Float32} const PointT{T} = Point{N,T} where N const Pointf{N} = Point{N,Float32} -Base.isnan(p::Union{Point,Vec}) = any(isnan, p) -Base.isinf(p::Union{Point,Vec}) = any(isinf, p) -Base.isfinite(p::Union{Point,Vec}) = all(isfinite, p) +Base.isnan(p::Union{AbstractPoint,Vec}) = any(isnan, p) +Base.isinf(p::Union{AbstractPoint,Vec}) = any(isinf, p) +Base.isfinite(p::Union{AbstractPoint,Vec}) = all(isfinite, p) ## Generate aliases ## As a text file instead of eval/macro, to not confuse code linter @@ -280,8 +168,64 @@ open(joinpath(@__DIR__, "generated-aliases.jl"), "w") do io end =# -include("mat.jl") include("generated-aliases.jl") export Mat, Vec, Point, unit export Vecf, Pointf + +""" + Vec{N, T}(args...) + Vec{N, T}(args::Union{AbstractVector, Tuple, NTuple, StaticVector}) + +Constructs a Vec of length `N` from the given arguments. + +Note that Point and Vec don't follow strict mathematical definitions. Instead +we allow them to be used interchangeably. + +## Aliases + +| |`T` |`Float64` |`Float32` |`Int` |`UInt` | +|--------|------------|----------|----------|----------|----------| +|`N` |`Vec{N,T}` |`Vecd{N}` |`Vecf{N}` |`Veci{N}` |`Vecui{N}`| +|`2` |`Vec2{T}` |`Vec2d` |`Vec2f` |`Vec2i` |`Vec2ui` | +|`3` |`Vec3{T}` |`Vec3d` |`Vec3f` |`Vec3i` |`Vec3ui` | +""" +Vec + + +""" + Point{N, T}(args...) + Point{N, T}(args::Union{AbstractVector, Tuple, NTuple, StaticVector}) + +Constructs a Point of length `N` from the given arguments. + +Note that Point and Vec don't follow strict mathematical definitions. Instead +we allow them to be used interchangeably. + +## Aliases + +| |`T` |`Float64` |`Float32` |`Int` |`UInt` | +|--------|------------|----------|----------|----------|----------| +|`N` |`Point{N,T}`|`Pointd{N}`|`Pointf{N}`|`Pointi{N}`|`Pointui{N}`| +|`2` |`Point2{T}` |`Point2d` |`Point2f` |`Point2i` |`Point2ui`| +|`3` |`Point3{T}` |`Point3d` |`Point3f` |`Point3i` |`Point3ui`| +""" +Point + +""" + Mat{R, C, T[, L]}(args::Union{UniformScaling, Tuple, AbstractMatrix}) + Mat{R, C}(args::Union{Tuple, AbstractMatrix}) + Mat{C}(args::Tuple) + +Constructs a static Matrix from the given inputs. Can also take multiple numeric +args. If only one size is given the matrix is assumed to be square. + +### Aliases + +| |`T` |`Float64` |`Float32` |`Int` |`UInt` | +|--------|------------|----------|----------|----------|----------| +|`N` |`Mat{N,T}` |`Matd{N}` |`Matf{N}` |`Mati{N}` |`Matui{N}`| +|`2` |`Mat2{T}` |`Mat2d` |`Mat2f` |`Mat2i` |`Mat2ui` | +|`3` |`Mat3{T}` |`Mat3d` |`Mat3f` |`Mat3i` |`Mat3ui` | +""" +Mat \ No newline at end of file diff --git a/src/geointerface.jl b/src/geointerface.jl index ef66143b..f806fc27 100644 --- a/src/geointerface.jl +++ b/src/geointerface.jl @@ -105,6 +105,11 @@ function GeoInterface.convert(::Type{Point}, type::PointTrait, geom) end end +# without a function barrier you get a lot of allocations from runtime types +function _collect_with_type(::Type{PT}, geom) where {PT <: Point{2}} + return [PT(GeoInterface.x(p), GeoInterface.y(p)) for p in getgeom(geom)] +end + function GeoInterface.convert(::Type{LineString}, type::LineStringTrait, geom) g1 = getgeom(geom, 1) x, y = GeoInterface.x(g1), GeoInterface.y(g1) @@ -114,7 +119,7 @@ function GeoInterface.convert(::Type{LineString}, type::LineStringTrait, geom) return LineString([Point{3,T}(GeoInterface.x(p), GeoInterface.y(p), GeoInterface.z(p)) for p in getgeom(geom)]) else T = promote_type(typeof(x), typeof(y)) - return LineString([Point{2,T}(GeoInterface.x(p), GeoInterface.y(p)) for p in getgeom(geom)]) + return LineString(_collect_with_type(Point{2, T}, geom)) end end diff --git a/src/geometry_primitives.jl b/src/geometry_primitives.jl index 36b25d69..7f60e481 100644 --- a/src/geometry_primitives.jl +++ b/src/geometry_primitives.jl @@ -8,6 +8,11 @@ end ## # conversion & decompose +""" + convert_simplex(::Type{TargetType}, x) + +Used to convert one object into another in `decompose(::Type{TargetType}, xs)`. +""" convert_simplex(::Type{T}, x::T) where {T} = (x,) convert_simplex(::Type{Vec{N, T}}, x::Vec{N, T}) where {N, T} = x @@ -38,14 +43,19 @@ Triangulate an N-Face into a tuple of triangular faces. return v end +# TODO: generic? +function convert_simplex(::Type{TriangleFace{T}}, f::SimplexFace{4}) where {T} + TF = TriangleFace{T} + return (TF(f[2],f[3],f[4]), TF(f[1],f[3],f[4]), TF(f[1],f[2],f[4]), TF(f[1],f[2],f[3])) +end + """ convert_simplex(::Type{Face{2}}, f::Face{N}) Extract all line segments in a Face. """ -@generated function convert_simplex(::Type{LineFace{T}}, - f::Union{SimplexFace{N},NgonFace{N}}) where {T,N} - 2 <= N || error("decompose not implented for N <= 2 yet. N: $N")# other wise degenerate +@generated function convert_simplex(::Type{LineFace{T}}, f::NgonFace{N}) where {T,N} + 2 <= N || error("decompose not implemented for N <= 2 yet. N: $N")# other wise degenerate v = Expr(:tuple) for i in 1:(N - 1) @@ -56,6 +66,18 @@ Extract all line segments in a Face. return v end +@generated function convert_simplex(::Type{LineFace{T}}, f::SimplexFace{N}) where {T,N} + 2 <= N || error("decompose not implemented for N <= 2 yet. N: $N")# other wise degenerate + + v = Expr(:tuple) + for i in 1:(N - 1) + for j in i+1:N + push!(v.args, :(LineFace{$T}(f[$i], f[$j]))) + end + end + return v +end + to_pointn(::Type{T}, x) where {T<:Point} = convert_simplex(T, x)[1] # disambiguation method overlords @@ -73,9 +95,22 @@ end collect_with_eltype(::Type{T}, vec::Vector{T}) where {T} = vec collect_with_eltype(::Type{T}, vec::AbstractVector{T}) where {T} = collect(vec) +collect_with_eltype(::Type{T}, vec::FaceView{T}) where {T} = vec function collect_with_eltype(::Type{T}, iter) where {T} - isempty(iter) && return T[] + return collect_with_eltype!(Vector{T}(undef, 0), iter) +end +function collect_with_eltype(::Type{T}, iter::FaceView) where {T} + return FaceView(collect_with_eltype!(Vector{T}(undef, 0), iter.data), iter.faces) +end + +function collect_with_eltype!(target::AbstractVector{T}, vec::AbstractVector{T}) where {T} + return append!(target, vec) +end + +function collect_with_eltype!(result::AbstractVector{T}, iter) where {T} + isempty(iter) && return result + # We need to get `eltype` information from `iter`, it seems to be `Any` # most of the time so the eltype checks here don't actually work l = if Base.IteratorSize(iter) isa Union{Base.HasShape,Base.HasLength} @@ -90,53 +125,53 @@ function collect_with_eltype(::Type{T}, iter) where {T} else 0 end - n = 0 - result = Vector{T}(undef, l) + + # Allow result to be pre-filled for handling faces with mesh.views + sizehint!(result, length(result) + l) + for element in iter # convert_simplex always returns a tuple, # so that e.g. convert(Triangle, quad) can return 2 elements for telement in convert_simplex(T, element) - n += 1 - if n > l - push!(result, telement) - else - result[n] = telement - end + push!(result, telement) end end return result end """ -The unnormalized normal of three vertices. + orthogonal_vector(p1, p2, p3) + +Calculates an orthogonal vector `cross(p2 - p1, p3 - p1)` to a plane described +by 3 points p1, p2, p3. """ -function orthogonal_vector(v1, v2, v3) - a = v2 .- v1 - b = v3 .- v1 - return cross(a, b) -end +orthogonal_vector(p1, p2, p3) = cross(p2 - p1, p3 - p1) +orthogonal_vector(::Type{VT}, p1, p2, p3) where {VT} = orthogonal_vector(VT(p1), VT(p2), VT(p3)) """ -``` -normals{VT,FD,FT,FO}(vertices::Vector{Point{3, VT}}, - faces::Vector{Face{FD,FT,FO}}, - NT = Normal{3, VT}) -``` -Compute all vertex normals. + normals(positions::Vector{Point3{T}}, faces::Vector{<: NgonFace}[; normaltype = Vec3{T}]) + +Compute vertex normals from the given `positions` and `faces`. + +This runs through all faces, computing a face normal each and adding it to every +involved vertex. The direction of the face normal is based on winding direction +and assumed counter-clockwise faces. At the end the summed face normals are +normalized again to produce a vertex normal. """ function normals(vertices::AbstractVector{Point{3,T}}, faces::AbstractVector{F}; normaltype=Vec{3,T}) where {T,F<:NgonFace} return normals(vertices, faces, normaltype) end -function normals(vertices::AbstractVector{<:Point{3}}, faces::AbstractVector{F}, - ::Type{NormalType}) where {F<:NgonFace,NormalType} +function normals(vertices::AbstractVector{<:Point{3}}, faces::AbstractVector{<: NgonFace}, + ::Type{NormalType}) where {NormalType} + normals_result = zeros(NormalType, length(vertices)) for face in faces v = vertices[face] # we can get away with two edges since faces are planar. - n = orthogonal_vector(v[1], v[2], v[3]) - for i in 1:length(F) + n = orthogonal_vector(NormalType, v[1], v[2], v[3]) + for i in 1:length(face) fi = face[i] normals_result[fi] = normals_result[fi] .+ n end @@ -144,3 +179,38 @@ function normals(vertices::AbstractVector{<:Point{3}}, faces::AbstractVector{F}, normals_result .= normalize.(normals_result) return normals_result end + + +""" + face_normals(positions::Vector{Point3{T}}, faces::Vector{<: NgonFace}[, target_type = Vec3{T}]) + +Compute face normals from the given `positions` and `faces` and returns an +appropriate `FaceView`. +""" +function face_normals( + positions::AbstractVector{<:Point3{T}}, fs::AbstractVector{<: AbstractFace}; + normaltype = Vec3{T}) where {T} + return face_normals(positions, fs, normaltype) +end + +@generated function face_normals(positions::AbstractVector{<:Point3}, fs::AbstractVector{F}, + ::Type{NormalType}) where {F<:NgonFace,NormalType} + + # If the facetype is not concrete it likely varies and we need to query it + # doing the iteration + FT = ifelse(isconcretetype(F), :($F), :(typeof(f))) + + quote + normals = resize!(NormalType[], length(fs)) + faces = resize!(F[], length(fs)) + + for (i, f) in enumerate(fs) + ps = positions[f] + n = orthogonal_vector(NormalType, ps[1], ps[2], ps[3]) + normals[i] = normalize(n) + faces[i] = $(FT)(i) + end + + return FaceView(normals, faces) + end +end diff --git a/src/interfaces.jl b/src/interfaces.jl index bed84d4b..dfbd30e3 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -1,8 +1,11 @@ """ coordinates(geometry) -Returns the edges/vertices/coordinates of a geometry. Is allowed to return lazy iterators! -Use `decompose(ConcretePointType, geometry)` to get `Vector{ConcretePointType}` with -`ConcretePointType` to be something like `Point{3, Float32}`. + +Returns the positions/coordinates of a geometry. + +This is allowed to return lazy iterators. Use `decompose(ConcretePointType, geometry)` +to get a `Vector{ConcretePointType}` with `ConcretePointType` being something like +`Point3f`. """ function coordinates(points::AbstractVector{<:Point}) return points @@ -10,14 +13,24 @@ end """ faces(geometry) -Returns the face connections of a geometry. Is allowed to return lazy iterators! -Use `decompose(ConcreteFaceType, geometry)` to get `Vector{ConcreteFaceType}` with -`ConcreteFaceType` to be something like `TriangleFace{Int}`. + +Returns the faces of a geometry. + +This is allowed to return lazy iterators. Use `decompose(ConcreteFaceType, geometry)` +to get a `Vector{ConcreteFaceType}` with `ConcreteFaceType` being something like `GLTriangleFace`. """ function faces(f::AbstractVector{<:AbstractFace}) return f end +""" + normals(primitive) + +Returns the normals of a geometry. + +This is allowed to return lazy iterators. Use `decompose_normals(ConcreteVecType, geometry)` +to get a `Vector{ConcreteVecType}` with `ConcreteVecType` being something like `Vec3f`. +""" function normals(primitive, nvertices=nothing; kw...) # doesn't have any specific algorithm to generate normals # so will be generated from faces + positions @@ -33,14 +46,25 @@ function faces(primitive, nvertices=nothing; kw...) return nothing end +""" + texturecoordinates(primitive) + +Returns the texturecoordinates of a geometry. + +This is allowed to return lazy iterators. Use `decompose_uv(ConcreteVecType, geometry)` +(or `decompose_uvw`) to get a `Vector{ConcreteVecType}` with `ConcreteVecType` being +something like `Vec2f`. +""" texturecoordinates(primitive, nvertices=nothing) = nothing """ Tesselation(primitive, nvertices) -For abstract geometries, when we generate -a mesh from them, we need to decide how fine grained we want to mesh them. -To transport this information to the various decompose methods, you can wrap it -in the Tesselation object e.g. like this: + +When generating a mesh from an abstract geometry, we can typically generate it +at different levels of detail, i.e. with different amounts of vertices. The +`Tesselation` wrapper allows you to specify this level of detail. When generating +a mesh from a tesselated geometry, the added information will be passed to +`coordinates`, `faces`, etc. ```julia sphere = Sphere(Point3f(0), 1) @@ -48,10 +72,13 @@ m1 = mesh(sphere) # uses a default value for tesselation m2 = mesh(Tesselation(sphere, 64)) # uses 64 for tesselation length(coordinates(m1)) != length(coordinates(m2)) ``` + For grid based tesselation, you can also use a tuple: + ```julia rect = Rect2(0, 0, 1, 1) Tesselation(rect, (5, 5)) +``` """ struct Tesselation{Dim,T,Primitive,NGrid} <: AbstractGeometry{Dim, T} primitive::Primitive @@ -94,6 +121,20 @@ struct Normal{T} end Normal(::Type{T}) where {T} = Normal{T}() Normal() = Normal(Vec3f) +""" + decompose(::Type{TargetType}, primitive) + decompose(::Type{TargetType}, data::AbstractVector) + +Dependent on the given type, extracts data from the primtive and converts its +eltype to `TargetType`. + +Possible `TargetType`s: +- `<: Point` extracts and converts positions (calling `coordinates()`) +- `<: AbstractFace` extracts and converts faces (calling `faces()`) +- `<: Normal{<: Vec}` extracts and converts normals, potentially generating them (calling `normals()`) +- `<: UV{<: Vec}` extracts and converts 2D texture coordinates, potentially generating them (calling `texturecoordinates()`) +- `<: UVW{<: Vec}` extracts and converts 3D texture coordinates, potentially generating them (calling `texturecoordinates()`) +""" function decompose(::Type{F}, primitive::AbstractGeometry) where {F<:AbstractFace} f = faces(primitive) if isnothing(f) @@ -104,15 +145,33 @@ function decompose(::Type{F}, primitive::AbstractGeometry) where {F<:AbstractFac return nothing end end - return collect_with_eltype(F, f) + return decompose(F, f) end - + function decompose(::Type{F}, f::AbstractVector) where {F<:AbstractFace} fs = faces(f) isnothing(fs) && error("No faces defined for $(typeof(f))") return collect_with_eltype(F, fs) end +# TODO: Should this be a completely different function? +function decompose(::Type{F}, f::AbstractVector, views::Vector{UnitRange{Int}}) where {F<:AbstractFace} + fs = faces(f) + isnothing(fs) && error("No faces defined for $(typeof(f))") + if isempty(views) + return collect_with_eltype(F, fs), views + else + output = F[] + new_views = sizehint!(UnitRange{Int}[], length(views)) + for range in views + start = length(output) + 1 + collect_with_eltype!(output, view(fs, range)) + push!(new_views, start:length(output)) + end + return output, new_views + end +end + function decompose(::Type{P}, primitive) where {P<:Point} return collect_with_eltype(P, coordinates(primitive)) end @@ -121,6 +180,10 @@ function decompose(::Type{Point}, primitive::AbstractGeometry{Dim,T}) where {Dim return collect_with_eltype(Point{Dim,T}, coordinates(primitive)) end +function decompose(::Type{Point{Dim}}, primitive::AbstractGeometry{Dim,T}) where {Dim,T} + return collect_with_eltype(Point{Dim,T}, coordinates(primitive)) +end + function decompose(::Type{PointT{T}}, primitive::AbstractGeometry{Dim}) where {Dim, T} return collect_with_eltype(Point{Dim,T}, coordinates(primitive)) end @@ -162,6 +225,10 @@ function decompose(UVT::Union{UV{T},UVW{T}}, primitive) where {T} # nothing, indicating that texturecoordinates aren't implemented positions = decompose(Point, primitive) isnothing(positions) && return nothing + # We should generate 3D uvw's if uv's are requested + # TODO: we should probably enforce that UV has a 2D type and UVW a 3D type + (length(T) != length(eltype(positions))) && return nothing + # Let this overlord do the work return decompose(UVT, positions) end diff --git a/src/mat.jl b/src/mat.jl deleted file mode 100644 index a638d4df..00000000 --- a/src/mat.jl +++ /dev/null @@ -1,234 +0,0 @@ -import LinearAlgebra: inv, det -import Random - -struct Mat{Row, Column, T, L} <: AbstractMatrix{T} - values::NTuple{L, T} - function Mat{R, C, T}(values::NTuple{L, T}) where {R, C, T, L} - @assert L == R * C "Number of rows $R * number of columns $C needs to match the number of values $L." - return new{R, C, T, L}(values) - end - function Mat{R, C, T, L}(values::NTuple{L, T}) where {R, C, T, L} - @assert L == R * C "Number of rows $R * number of columns $C needs to match the number of values $L." - return new{R, C, T, L}(values) - end -end - -Base.size(::Mat{R, C}) where {R, C} = (R, C) -Base.size(::Type{<: Mat{R, C}}) where {R, C} = (R, C) -Base.ndims(::Type{<: Mat}) = 2 -Base.@propagate_inbounds Base.getindex(mat::Mat{R, C}, i) where {R, C} = mat.values[i] -Base.@propagate_inbounds Base.getindex(mat::Mat{R, C}, i::CartesianIndex) where {R, C} = mat.values[i.I[1] + R * (i.I[2] - 1)] - -# TODO: maybe ranges as well? -Base.@propagate_inbounds function Base.getindex(mat::Mat{R, C}, i::StaticVector{N}, j::Integer) where {R, C, N} - @boundscheck begin - inbounds = all(x -> 1 ≤ x ≤ R, i) && (1 ≤ j ≤ C) - inbounds || throw(BoundsError(mat, (i, j))) - end - @inbounds data = ntuple(n -> mat.values[i[n] + R * (j-1)], N) - return Mat{N, 1}(data) -end -Base.@propagate_inbounds function Base.getindex(mat::Mat{R, C}, i::Integer, j::StaticVector{N}) where {R, C, N} - @boundscheck begin - inbounds = (1 ≤ i ≤ R) && all(x -> 1 ≤ x ≤ C, j) - inbounds || throw(BoundsError(mat, (i, j))) - end - @inbounds data = ntuple(n -> mat.values[i + R * (j[n] - 1)], N) - return Mat{1, N}(data) -end -Base.@propagate_inbounds function Base.getindex(mat::Mat{R, C}, i::StaticVector{N}, j::StaticVector{M}) where {R, C, N, M} - @boundscheck begin - inbounds = all(x -> 1 ≤ x ≤ R, i) && all(x -> 1 ≤ x ≤ C, j) - inbounds || throw(BoundsError(mat, (i, j))) - end - @inbounds data = ntuple(N * M) do nm - m, n = fldmod1(nm, N) - return mat.values[i[n] + R * (j[m] - 1)] - end - return Mat{N, M}(data) -end - -Base.IndexStyle(::Type{<: Mat}) = Base.IndexLinear() - -# Broadcasting -Base.broadcastable(x::Mat) = x -Base.BroadcastStyle(::Type{<: Mat{C, R}}) where {C, R} = StaticArrayStyle{Mat{C, R}}() -Base.values(m::Mat) = m.values - -Base.copy(mat::Mat) = deepcopy(mat) - -function Mat{C, R, T}(::LinearAlgebra.UniformScaling) where {C, R, T} - idx = CartesianIndices((R, C)) - data = ntuple(C * R) do i - ci, ri = Tuple(idx[i]) - return ci === ri ? T(1) : T(0) - end - return Mat{R, C, T}(data) -end - -function Mat{C, R, T, L}(::LinearAlgebra.UniformScaling) where {C, R, T, L} - idx = CartesianIndices((R, C)) - data = ntuple(C * R) do i - ci, ri = Tuple(idx[i]) - return ci === ri ? T(1) : T(0) - end - return Mat{R, C, T, L}(data) -end - -Mat{C, R, T}(args...) where {C, R, T} = Mat{C, R, T}(args) -Mat{C, R, T, L}(args...) where {C, R, T, L} = Mat{C, R, T}(args) -Mat{C, R, T, L}(mat::Mat{C, R}) where {C, R, T, L} = Mat{C, R, T}(mat.values) -Mat{C}(args...) where {C} = Mat{C, C}(args) -Mat{C}(arg) where {C} = Mat{C, C}(arg) -Mat{C, R}(x::Tuple) where {C, R} = Mat{C, R}(promote(x...)) -Mat{C, R, T}(x::Tuple) where {C, R, T} = Mat{C, R, T}(convert(NTuple{length(x), T}, x)) -Mat{C, R}(x::NTuple{L, T}) where {C, R, L, T} = Mat{C, R, T}(x) -Mat{C, R, T1}(x::NTuple{L, T2}) where {C, R, L, T1, T2} = Mat{C, R, T1}(convert(NTuple{L, T1}, x)) - -# catch Mat2(...) etc which lowers to Mat{2, 2, T, 4} where T -(::Type{MT})(args...) where {C, R, MT <: Mat{C, R}} = Mat{C, R}(args) -(::Type{MT})(args::Tuple) where {C, R, MT <: Mat{C, R}} = Mat{C, R}(args) - -Mat{C, R}(x::AbstractMatrix{T}) where {C, R, T} = Mat{C, R, T}(x) -Mat{C, R, T}(x::AbstractMatrix) where {C, R, T} = Mat{C, R, T}(ntuple(i-> convert(T, x[i]), C*R)) -Mat{C, R, T, N}(x::AbstractMatrix) where {C, R, T, N} = Mat{C, R, T}(ntuple(i-> convert(T, x[i]), C*R)) - -Base.convert(::Type{Mat{C, R, T, L}}, from::Mat{C, R}) where {C, R, T, L} = Mat{C, R, T}(from.values) - -# Matrix products -# General shape mismatched versions are errors -(*)(a::Mat{M, N, T1}, b::Mat{O, K, T2}) where {T1, T2, M, N, O, K} = throw(DimensionMismatch("$N != $O in $(typeof(a)) and $(typeof(b))")) - -Base.:(-)(a::Mat{R, C}, b::Mat{R, C}) where {R, C} = Mat{R, C}(a.values .- b.values) -Base.:(+)(a::Mat{R, C}, b::Mat{R, C}) where {R, C} = Mat{R, C}(a.values .+ b.values) - -# matrix * matrix -@generated function *(a::Mat{M, N, T1}, b::Mat{N, P, T2}) where {T1, T2, M, N, P} - elements = Expr(:tuple) - for j in 1:P - for i in 1:M - plus_expr = Expr(:call, :+, (:(a[$i,$k]*b[$k,$j]) for k = 1:N)...) - push!(elements.args, plus_expr) - end - end - :(Mat{$M, $P}($elements)) -end - -@inline det(A::Mat{1, 1, T}) where {T} = @inbounds return ( A[1] ) -@inline det(A::Mat{2, 2, T}) where {T} = @inbounds return ( A[1,1]*A[2,2] - A[1,2]*A[2,1]) -@inline det(A::Mat{3, 3, T}) where {T} = @inbounds return ( - A[1,1]*(A[2,2]*A[3,3]-A[2,3]*A[3,2]) - - A[1,2]*(A[2,1]*A[3,3]-A[2,3]*A[3,1]) + - A[1,3]*(A[2,1]*A[3,2]-A[2,2]*A[3,1]) -) - -det(A::Mat{4, 4, T}) where {T} = @inbounds return ( - A[13] * A[10] * A[7] * A[4] - A[9] * A[14] * A[7] * A[4] - - A[13] * A[6] * A[11] * A[4] + A[5] * A[14] * A[11] * A[4] + - A[9] * A[6] * A[15] * A[4] - A[5] * A[10] * A[15] * A[4] - - A[13] * A[10] * A[3] * A[8] + A[9] * A[14] * A[3] * A[8] + - A[13] * A[2] * A[11] * A[8] - A[1] * A[14] * A[11] * A[8] - - A[9] * A[2] * A[15] * A[8] + A[1] * A[10] * A[15] * A[8] + - A[13] * A[6] * A[3] * A[12] - A[5] * A[14] * A[3] * A[12] - - A[13] * A[2] * A[7] * A[12] + A[1] * A[14] * A[7] * A[12] + - A[5] * A[2] * A[15] * A[12] - A[1] * A[6] * A[15] * A[12] - - A[9] * A[6] * A[3] * A[16] + A[5] * A[10] * A[3] * A[16] + - A[9] * A[2] * A[7] * A[16] - A[1] * A[10] * A[7] * A[16] - - A[5] * A[2] * A[11] * A[16] + A[1] * A[6] * A[11] * A[16] -) - -det(A::Mat) = det(Matrix(A)) - -inv(A::Mat{1, 1, T, 1}) where T = @inbounds return Mat{1, 1, T, 1}(inv(A[1])) -function inv(A::Mat{2, 2, T, L}) where {T, L} - determinant = det(A) - @inbounds return Mat{2, 2, T}( - (A[2,2] /determinant, -A[2,1]/determinant), - (-A[1,2]/determinant, A[1,1] /determinant) - ) -end -function inv(A::Mat{3, 3, T, L}) where {T, L} - determinant = det(A) - @inbounds return Mat{3, 3, T}( - (A[2,2]*A[3,3]-A[2,3]*A[3,2]) /determinant, - -(A[2,1]*A[3,3]-A[2,3]*A[3,1])/determinant, - (A[2,1]*A[3,2]-A[2,2]*A[3,1]) /determinant, - - -(A[1,2]*A[3,3]-A[1,3]*A[3,2])/determinant, - (A[1,1]*A[3,3]-A[1,3]*A[3,1]) /determinant, - -(A[1,1]*A[3,2]-A[1,2]*A[3,1])/determinant, - - (A[1,2]*A[2,3]-A[1,3]*A[2,2]) /determinant, - -(A[1,1]*A[2,3]-A[1,3]*A[2,1])/determinant, - (A[1,1]*A[2,2]-A[1,2]*A[2,1]) /determinant - ) -end - -function inv(A::Mat{4, 4, T, L}) where {T, L} - determinant = det(A) - @inbounds return Mat{4, 4, T}( - (A[2,3]*A[3,4]*A[4,2] - A[2,4]*A[3,3]*A[4,2] + A[2,4]*A[3,2]*A[4,3] - A[2,2]*A[3,4]*A[4,3] - A[2,3]*A[3,2]*A[4,4] + A[2,2]*A[3,3]*A[4,4]) / determinant, - (A[2,4]*A[3,3]*A[4,1] - A[2,3]*A[3,4]*A[4,1] - A[2,4]*A[3,1]*A[4,3] + A[2,1]*A[3,4]*A[4,3] + A[2,3]*A[3,1]*A[4,4] - A[2,1]*A[3,3]*A[4,4]) / determinant, - (A[2,2]*A[3,4]*A[4,1] - A[2,4]*A[3,2]*A[4,1] + A[2,4]*A[3,1]*A[4,2] - A[2,1]*A[3,4]*A[4,2] - A[2,2]*A[3,1]*A[4,4] + A[2,1]*A[3,2]*A[4,4]) / determinant, - (A[2,3]*A[3,2]*A[4,1] - A[2,2]*A[3,3]*A[4,1] - A[2,3]*A[3,1]*A[4,2] + A[2,1]*A[3,3]*A[4,2] + A[2,2]*A[3,1]*A[4,3] - A[2,1]*A[3,2]*A[4,3]) / determinant, - - (A[1,4]*A[3,3]*A[4,2] - A[1,3]*A[3,4]*A[4,2] - A[1,4]*A[3,2]*A[4,3] + A[1,2]*A[3,4]*A[4,3] + A[1,3]*A[3,2]*A[4,4] - A[1,2]*A[3,3]*A[4,4]) / determinant, - (A[1,3]*A[3,4]*A[4,1] - A[1,4]*A[3,3]*A[4,1] + A[1,4]*A[3,1]*A[4,3] - A[1,1]*A[3,4]*A[4,3] - A[1,3]*A[3,1]*A[4,4] + A[1,1]*A[3,3]*A[4,4]) / determinant, - (A[1,4]*A[3,2]*A[4,1] - A[1,2]*A[3,4]*A[4,1] - A[1,4]*A[3,1]*A[4,2] + A[1,1]*A[3,4]*A[4,2] + A[1,2]*A[3,1]*A[4,4] - A[1,1]*A[3,2]*A[4,4]) / determinant, - (A[1,2]*A[3,3]*A[4,1] - A[1,3]*A[3,2]*A[4,1] + A[1,3]*A[3,1]*A[4,2] - A[1,1]*A[3,3]*A[4,2] - A[1,2]*A[3,1]*A[4,3] + A[1,1]*A[3,2]*A[4,3]) / determinant, - - (A[1,3]*A[2,4]*A[4,2] - A[1,4]*A[2,3]*A[4,2] + A[1,4]*A[2,2]*A[4,3] - A[1,2]*A[2,4]*A[4,3] - A[1,3]*A[2,2]*A[4,4] + A[1,2]*A[2,3]*A[4,4]) / determinant, - (A[1,4]*A[2,3]*A[4,1] - A[1,3]*A[2,4]*A[4,1] - A[1,4]*A[2,1]*A[4,3] + A[1,1]*A[2,4]*A[4,3] + A[1,3]*A[2,1]*A[4,4] - A[1,1]*A[2,3]*A[4,4]) / determinant, - (A[1,2]*A[2,4]*A[4,1] - A[1,4]*A[2,2]*A[4,1] + A[1,4]*A[2,1]*A[4,2] - A[1,1]*A[2,4]*A[4,2] - A[1,2]*A[2,1]*A[4,4] + A[1,1]*A[2,2]*A[4,4]) / determinant, - (A[1,3]*A[2,2]*A[4,1] - A[1,2]*A[2,3]*A[4,1] - A[1,3]*A[2,1]*A[4,2] + A[1,1]*A[2,3]*A[4,2] + A[1,2]*A[2,1]*A[4,3] - A[1,1]*A[2,2]*A[4,3]) / determinant, - - (A[1,4]*A[2,3]*A[3,2] - A[1,3]*A[2,4]*A[3,2] - A[1,4]*A[2,2]*A[3,3] + A[1,2]*A[2,4]*A[3,3] + A[1,3]*A[2,2]*A[3,4] - A[1,2]*A[2,3]*A[3,4]) / determinant, - (A[1,3]*A[2,4]*A[3,1] - A[1,4]*A[2,3]*A[3,1] + A[1,4]*A[2,1]*A[3,3] - A[1,1]*A[2,4]*A[3,3] - A[1,3]*A[2,1]*A[3,4] + A[1,1]*A[2,3]*A[3,4]) / determinant, - (A[1,4]*A[2,2]*A[3,1] - A[1,2]*A[2,4]*A[3,1] - A[1,4]*A[2,1]*A[3,2] + A[1,1]*A[2,4]*A[3,2] + A[1,2]*A[2,1]*A[3,4] - A[1,1]*A[2,2]*A[3,4]) / determinant, - (A[1,2]*A[2,3]*A[3,1] - A[1,3]*A[2,2]*A[3,1] + A[1,3]*A[2,1]*A[3,2] - A[1,1]*A[2,3]*A[3,2] - A[1,2]*A[2,1]*A[3,3] + A[1,1]*A[2,2]*A[3,3]) / determinant - ) -end - -inv(A::Mat) = typeof(A)(inv(Matrix(A))) - -function Random.rand(rng::Random.AbstractRNG, ::Random.SamplerType{<: Mat{R, C, T}}) where {R,C,T} - return Mat{R, C, T}(ntuple(i-> rand(rng, T), R*C)) -end - -function Random.randn(rng::Random.AbstractRNG, ::Type{<: Mat{R, C, T}}) where {R,C,T} - return Mat{R, C, T}(ntuple(i-> randn(rng, T), R*C)) -end - -function Base.transpose(a::Mat{R, C, T}) where {R, C, T} - idx = CartesianIndices((R, C)) - data = ntuple(R * C) do i - ci, ri = Tuple(idx[i]) - return a[ri, ci] - end - Mat{R, C, T}(data) -end - -(*)(a::Mat{M, N, T1}, b::StaticVector{O, T2}) where {T1, T2, M, N, O} = throw(DimensionMismatch("$N != $O in $(typeof(a)) and $(typeof(b))")) - -# vector * (row vector) -@generated function *(a::StaticVector{N, T1}, b::Mat{1, M, T2}) where {T1, T2, N, M} - elements = Expr(:tuple, [Expr(:tuple, [:(a[$i] * b[$j]) for i in 1:N]...) for j in 1:M]...) - return :(similar_type(a)($elements)) -end - -# matrix * vector -@generated function *(a::Mat{M, N, T1}, b::StaticVector{N, T2}) where {T1, T2, M, N} - elements = Expr(:tuple, [Expr(:call, :+, [:(a[$i,$k]*b[$k]) for k = 1:N]...) for i in 1:M]...) - return :(similar_type(b)($elements)) -end - -# TODO: Fix Vec(mat) becoming Vec((mat,)) (i.e. restrict eltype to Number?) -(VT::Type{<: StaticVector{N}})(mat::Mat{N, 1}) where {N} = VT(mat.values) - -function Base.isapprox( - a::Mat{R1, C1, T1}, b::Mat{R2, C2, T2}; - atol::Real = 0, - rtol::Real = atol > 0 ? 0 : sqrt(max(eps(T1), eps(T2))) - ) where {R1, R2, C1, C2, T1, T2} - return (R1 == R2) && (C1 == C2) && norm(a - b) <= max(atol, rtol * max(norm(a), norm(b))) -end \ No newline at end of file diff --git a/src/meshes.jl b/src/meshes.jl index b29e1888..0953ea0a 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -1,39 +1,110 @@ """ - mesh(primitive::GeometryPrimitive; - pointtype=Point, facetype=GLTriangle, - uv=nothing, normaltype=nothing) + mesh(primitive::GeometryPrimitive[; pointtype = Point, facetype = GLTriangleFace, vertex_attributes...]) -Creates a mesh from `primitive`. +Creates a mesh from a given `primitive` with the given `pointtype` and `facetype`. -Uses the element types from the keyword arguments to create the attributes. -The attributes that have their type set to nothing are not added to the mesh. -Note, that this can be an `Int` or `Tuple{Int, Int}``, when the primitive is grid based. -It also only losely correlates to the number of vertices, depending on the algorithm used. -#TODO: find a better number here! +This method only generates positions and faces from the primitive. Additional +vertex attributes like normals and texture coordinates can be given as extra +keyword arguments. + +Note that vertex attributes that are `nothing` get removed before creating a mesh. + +See also: [`triangle_mesh`](@ref), [`normal_mesh`](@ref), [`uv_mesh`](@ref), [`uv_normal_mesh`](@ref) """ -function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleFace) +function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleFace, vertex_attributes...) positions = decompose(pointtype, primitive) - _f = faces(primitive) + + # TODO: consider not allowing FaceView here? + if positions isa FaceView + positions = positions.data + _fs = positions.faces + isnothing(faces(primitive)) || @error("A primitive should not define `faces` and use a FaceView for `coordinates()`. Using faces from FaceView.") + else + # This tries `faces(prim)` first, then triangulation with the natural + # position type of the primitive. + _fs = decompose(facetype, primitive) + end + # If faces returns nothing for primitive, we try to triangulate! - if isnothing(_f) + if isnothing(_fs) if eltype(positions) <: Point2 - # triangulation.jl - f = decompose(facetype, positions) + # try triangulation with the converted positions as a last attempt + fs = decompose(facetype, positions) else error("No triangulation for $(typeof(primitive))") end else - f = decompose(facetype, _f) + fs = _fs end - return Mesh(positions, f) + + return mesh(positions, collect(fs); facetype = facetype, vertex_attributes...) end -const SimpleMesh{N, T, FT} = Mesh{N, T, Vector{Point{N, T}}, Vector{FT}} -const TriangleMesh{N} = SimpleMesh{N, Float32, GLTriangleFace} +""" + mesh(positions, faces[, facetype = GLTriangleFace, vertex_attributes...]) + +Creates a mesh from the given positions and faces. Other vertex attributes like +normals and texture coordinates can be added as keyword arguments. + +Note that vertex attributes that are `nothing` get removed before creating a mesh. + +See also:[`normal_mesh`](@ref) +""" +function mesh( + positions::AbstractVector{<:Point}, + faces::AbstractVector{FT}; + facetype=GLTriangleFace, vertex_attributes... + ) where {FT <: AbstractFace} + + fs = decompose(facetype, faces) + + names = keys(vertex_attributes) + valid_names = filter(name -> !isnothing(vertex_attributes[name]), names) + vals = convert_facetype.(Ref(facetype), getindex.(Ref(vertex_attributes), valid_names)) + va = NamedTuple{valid_names}(vals) + + return Mesh(positions, fs; va...) +end """ - mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace, - normaltype=nothing) + mesh(mesh::Mesh[; pointtype = Point, facetype = GLTriangleFace, vertex_attributes...] + +Recreates the given `mesh` with the given `pointtype`, `facetype` and vertex +attributes. If the new mesh would match the old mesh, the old mesh is returned instead. + +Note that vertex attributes that are `nothing` get removed before creating a mesh. +""" +function mesh( + mesh::Mesh{D, T, FT}; pointtype = Point{D, Float32}, + facetype::Type{<: AbstractFace} = GLTriangleFace, + attributes... + ) where {D, T, FT <: AbstractFace} + + names = keys(attributes) + valid_names = filter(name -> !isnothing(attributes[name]), names) + + if isempty(valid_names) && (GeometryBasics.pointtype(mesh) == pointtype) && (FT == facetype) + return mesh + else + vals = getindex.(Ref(attributes), valid_names) + va = NamedTuple{valid_names}(vals) + + # add vertex attributes + va = merge(vertex_attributes(mesh), va) + # convert position attribute and facetypes in FaceViews + va = NamedTuple{keys(va)}(map(keys(va)) do name + val = name == :position ? decompose(pointtype, va[:position]) : va[name] + return convert_facetype(facetype, val) + end) + + # update main face type + f, views = decompose(facetype, faces(mesh), mesh.views) + return Mesh(va, f, views) + end +end + +""" + mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace) Create a mesh from a polygon given as a vector of points, using triangulation. """ @@ -41,45 +112,101 @@ function mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace) return mesh(Polygon(polygon); pointtype=pointtype, facetype=facetype) end -function triangle_mesh(primitive::Union{AbstractGeometry{N}, AbstractVector{<: Point{N}}})::TriangleMesh{N} where {N} - return mesh(primitive; pointtype=Point{N, Float32}) +""" + triangle_mesh(primitive::GeometryPrimitive[; pointtype = Point, facetype = GLTriangleFace]) + +Creates a simple triangle mesh from a given `primitive` with the given `pointtype` +and `facetype`. + +See also: [`triangle_mesh`](@ref), [`normal_mesh`](@ref), [`uv_mesh`](@ref), [`uv_normal_mesh`](@ref) +""" +function triangle_mesh( + primitive::Union{AbstractGeometry{N}, AbstractVector{<: Point{N}}}; + pointtype = Point{N, Float32}, facetype = GLTriangleFace + ) where {N} + return mesh(primitive; pointtype = pointtype, facetype = facetype) end +pointtype(::Mesh{D, T}) where {D, T} = Point{D, T} +facetype(::Mesh{D, T, FT}) where {D, T, FT} = FT -pointtype(x::Mesh) = eltype(decompose(Point, x)) -facetype(x::Mesh) = eltype(faces(x)) +""" + uv_mesh(primitive::GeometryPrimitive{N}[; pointtype = Point{N, Float32}, facetype = GLTriangleFace, uvtype = Vec2f]) -function triangle_mesh(primitive::Mesh{N}) where {N} - # already target type: - if pointtype(primitive) === Point{N,Float32} && GLTriangleFace === facetype(primitive) - return primitive - else - return mesh(primitive; pointtype=Point{N,Float32}, facetype=GLTriangleFace) - end -end +Creates a triangle mesh with texture coordinates from a given `primitive`. The +`pointtype`, `facetype` and `uvtype` are set by the correspondering keyword arguments. -function uv_mesh(primitive::AbstractGeometry{N,T}) where {N,T} - m = triangle_mesh(primitive) - return MetaMesh(m, (uv=decompose_uv(primitive),)) +See also: [`triangle_mesh`](@ref), [`normal_mesh`](@ref), [`uv_mesh`](@ref), [`uv_normal_mesh`](@ref) +""" +function uv_mesh( + primitive::AbstractGeometry{N}; pointtype = Point{N, Float32}, + uvtype = Vec2f, facetype = GLTriangleFace + ) where {N} + + return mesh( + primitive, uv = decompose_uv(uvtype, primitive), + pointtype = pointtype, facetype = facetype + ) end -function uv_normal_mesh(primitive::AbstractGeometry{N}) where {N} - m = triangle_mesh(primitive) - return MetaMesh(m, (uv=decompose_uv(primitive), normals=decompose_normals(m))) +""" + uv_normal_mesh(primitive::GeometryPrimitive{N}[; pointtype = Point{N, Float32}, facetype = GLTriangleFace, uvtype = Vec2f, normaltype = Vec3f]) + +Creates a triangle mesh with texture coordinates and normals from a given +`primitive`. The `pointtype`, `facetype` and `uvtype` and `normaltype` are set +by the correspondering keyword arguments. + +See also: [`triangle_mesh`](@ref), [`normal_mesh`](@ref), [`uv_mesh`](@ref), [`uv_normal_mesh`](@ref) +""" +function uv_normal_mesh( + primitive::AbstractGeometry{N}; pointtype = Point{N, Float32}, + uvtype = Vec2f, normaltype = Vec3f, facetype = GLTriangleFace + ) where {N} + + return mesh( + primitive, uv = decompose_uv(uvtype, primitive), + normal = decompose_normals(normaltype, primitive), + pointtype = pointtype, facetype = facetype + ) end -function normal_mesh(points::AbstractVector{<:Point}, - faces::AbstractVector{<:AbstractFace}) - _points = decompose(Point3f, points) - _faces = decompose(GLTriangleFace, faces) - return MetaMesh(Mesh(_points, _faces), (normals=normals(_points, _faces),)) +""" + uv_normal_mesh(primitive::GeometryPrimitive{N}[; pointtype = Point{N, Float32}, facetype = GLTriangleFace, uvtype = Vec2f, normaltype = Vec3f]) + +Creates a triangle mesh with texture coordinates and normals from a given +`primitive`. The `pointtype`, `facetype` and `uvtype` and `normaltype` are set +by the correspondering keyword arguments. + +See also: [`triangle_mesh`](@ref), [`normal_mesh`](@ref), [`uv_mesh`](@ref), [`uv_normal_mesh`](@ref) +""" +function normal_mesh( + points::AbstractVector{<:Point}, faces::AbstractVector{<:AbstractFace}; + pointtype = Point3f, normaltype = Vec3f, facetype = GLTriangleFace + ) + _points = decompose(pointtype, points) + _faces = decompose(facetype, faces) + return Mesh(_faces, position = _points, normal = normals(_points, _faces, normaltype)) end -function normal_mesh(primitive::AbstractGeometry{N}) where {N} - m = triangle_mesh(primitive) - return MetaMesh(m, (normals=decompose_normals(m),)) +""" + normal_mesh(primitive::GeometryPrimitive{N}[; pointtype = Point{N, Float32}, facetype = GLTriangleFace, normaltype = Vec3f]) + +Creates a triangle mesh with normals from a given `primitive`. The `pointtype`, `facetype` and `uvtype` and `normaltype` are set +by the correspondering keyword arguments. + +See also: [`triangle_mesh`](@ref), [`normal_mesh`](@ref), [`uv_mesh`](@ref), [`uv_normal_mesh`](@ref) +""" +function normal_mesh( + primitive::AbstractGeometry{N}; pointtype = Point{N, Float32}, + normaltype = Vec3f, facetype = GLTriangleFace + ) where {N} + + return mesh( + primitive, normal = decompose_normals(normaltype, primitive), + pointtype = pointtype, facetype = facetype) end + """ volume(triangle) @@ -102,39 +229,259 @@ function volume(mesh::Mesh) return sum(volume, mesh) end + +""" + merge(meshes::AbstractVector{Mesh}) + +Generates a new mesh containing all the data of the individual meshes. + +If all meshes are consistent in their use of FaceViews they will be preserved. +Otherwise all of them will be converted with `clear_faceviews(mesh)`. + +This function will generate `views` in the new mesh which correspond to the +inputs of this function. +""" function Base.merge(meshes::AbstractVector{<:Mesh}) return if isempty(meshes) return Mesh(Point3f[], GLTriangleFace[]) + elseif length(meshes) == 1 return meshes[1] + + else + m1 = meshes[1] + + # Check that all meshes use the same VertexAttributes + # Could also do this via typing the function, but maybe error is nice? + names = keys(vertex_attributes(m1)) + if !all(m -> keys(vertex_attributes(m)) == names, meshes) + idx = findfirst(m -> keys(vertex_attributes(m)) != names, meshes) + error( + "Cannot merge meshes with different vertex attributes. " * + "First missmatch between meshes[1] with $names and " * + "meshes[$idx] with $(keys(vertex_attributes(meshes[idx])))." + ) + end + + consistent_face_views = true + for name in names + is_face_view = getproperty(m1, name) isa FaceView + for i in 2:length(meshes) + if (getproperty(meshes[i], name) isa FaceView) != is_face_view + consistent_face_views = false + @goto DOUBLE_BREAK + end + end + end + @label DOUBLE_BREAK + + if consistent_face_views + + # All the same kind of face, can just merge + new_attribs = NamedTuple{names}(map(names) do name + if name === :position + D = maximum(ndims, meshes) + T = mapreduce(m -> eltype(pointtype(m)), promote_type, meshes) + PT = Point{D, T} + return reduce(vcat, decompose.(PT, meshes)) + else + return reduce(vcat, getproperty.(meshes, name)) + end + end) + fs = reduce(vcat, faces.(meshes)) + + # TODO: is the type difference in offset bad? + idx = length(faces(m1)) + offset = length(coordinates(m1)) + views = isempty(m1.views) ? UnitRange{Int64}[1:idx] : copy(m1.views) + + for mesh in Iterators.drop(meshes, 1) + N = length(faces(mesh)) + + # update face indices + for i = idx .+ (1:N) + # TODO: face + Int changes type to Int + fs[i] = typeof(fs[i])(fs[i] .+ offset) + end + + # add views + if isempty(mesh.views) + push!(views, idx+1 : idx+N) + else + for view in mesh.views + push!(views, view .+ idx) + end + end + + idx += N + offset += length(coordinates(mesh)) + end + + return Mesh(new_attribs, fs, views) + + else # mixed FaceViews and Arrays + + # simplify to VertexFace types, then retry merge + return merge(clear_faceviews.(meshes)) + + end + + end +end + +""" + clear_faceviews(mesh::Mesh) + +Returns the given `mesh` if it contains no FaceViews. Otherwise, generates a new +mesh that contains no FaceViews, reordering and duplicating vertex atttributes +as necessary. If the mesh has `views` they will be adjusted as needed to produce +the same submeshes. +""" +function clear_faceviews(mesh::Mesh) + main_fs = faces(mesh) + va = vertex_attributes(mesh) + + names = filter(name -> va[name] isa FaceView, keys(va)) + isempty(names) && return mesh + + other_fs = faces.(getproperty.((mesh,), names)) + names = (:position, names...) + all_fs = tuple(main_fs, other_fs...) + + if isempty(mesh.views) + + new_fs, maps = merge_vertex_indices(all_fs) + + named_maps = NamedTuple{tuple(names...)}(maps) + + new_va = NamedTuple{keys(va)}(map(keys(va)) do name + values(va[name])[get(named_maps, name, maps[1])] + end) + + return Mesh(new_va, new_fs) + else - ps = reduce(vcat, coordinates.(meshes)) - fs = reduce(vcat, faces.(meshes)) - idx = length(faces(meshes[1])) - offset = length(coordinates(meshes[1])) - for mesh in Iterators.drop(meshes, 1) - N = length(faces(mesh)) - for i = idx .+ (1:N) - fs[i] = fs[i] .+ offset + + new_fs = sizehint!(eltype(main_fs)[], length(main_fs)) + new_views = sizehint!(UnitRange{Int}[], length(mesh.views)) + new_va = NamedTuple{keys(va)}(map(keys(va)) do name + sizehint!(similar(values(va[name]), 0), length(va[name])) + end) + + vertex_index_counter = eltype(first(main_fs))(1) + + for idxs in mesh.views + view_fs, maps = merge_vertex_indices(view.(all_fs, (idxs,)), vertex_index_counter) + + vertex_index_counter += length(maps[1]) + + for name in keys(new_va) + map = maps[something(findfirst(==(name), names), 1)] + append!(new_va[name], values(va[name])[map]) end - idx += N - offset += length(coordinates(mesh)) + + # add new faces and new view + start = length(new_fs) + 1 + append!(new_fs, view_fs) + push!(new_views, start:length(new_fs)) end - return Mesh(ps, fs) + + return Mesh(new_va, new_fs, new_views) + end +end + +function merge_vertex_indices( + faces::AbstractVector{FT}, args... + ) where {N, T, FT <: AbstractFace{N, T}} + if args[end] isa Integer + fs = tuple(faces, args[1:end-1]...) + return merge_vertex_indices(fs, args[end]) + else + return merge_vertex_indices(tuple(faces, args...)) end end -function Base.merge(meshes::AbstractVector{T}) where T <: MetaMesh - isempty(meshes) && return T(Point3f[], GLTriangleFace[]) - big_mesh = merge(map(Mesh, meshes)) - big_meta = deepcopy(meta(meshes[1])) - for mesh in Iterators.drop(meshes, 1) - mm = meta(mesh) - for (k, v) in pairs(mm) - append!(big_meta[k], v) +function merge_vertex_indices( + faces::NTuple{N_Attrib, <: AbstractVector{FT}}, + vertex_index_counter::Integer = T(1) + ) where {N, T, FT <: AbstractFace{N, T}, N_Attrib} + + N_faces = length(faces[1]) + + # maps a combination of old indices in MultiFace to a new vertex_index + vertex_index_map = Dict{NTuple{N_Attrib, T}, T}() + + # Faces after conversion + new_faces = sizehint!(FT[], N_faces) + + # indices that remap attributes + attribute_indices = ntuple(n -> sizehint!(UInt32[], N_faces), N_Attrib) + + # keep track of the remmaped indices for one vertex so we don't have to + # query the dict twice + temp = Vector{T}(undef, N) + + for multi_face in zip(faces...) + + for i in 1:N + # get the i-th set of vertex indices from multi_face, i.e. + # (multi_face.position_index[i], multi_face.normal_index[i], ...) + vertex = ntuple(n -> multi_face[n][i], N_Attrib) + + # if the vertex exists, get it's index + # otherwise register it with the next available vertex index + temp[i] = get!(vertex_index_map, vertex) do + vertex_index_counter += 1 + push!.(attribute_indices, vertex) + return vertex_index_counter - 1 + end end + + # generate new face + push!(new_faces, FT(temp)) + end + + # in case we are reserving more than needed + sizehint!(new_faces, length(new_faces)) + + return new_faces, attribute_indices +end + + +""" + split_mesh(mesh::Mesh, views::Vector{UnitRange{Int}} = mesh.views) + +Creates a new mesh containing `faces(mesh)[range]` for each range in `views`. +This also removes unused vertices. +""" +function split_mesh(mesh::Mesh, views::Vector{UnitRange{Int}} = mesh.views) + return map(views) do idxs + new_fs, maps = merge_vertex_indices((view(faces(mesh), idxs),)) + + names = keys(vertex_attributes(mesh)) + new_va = NamedTuple{names}(map(names) do name + v = getproperty(mesh, name) + if v isa FaceView + _fs, _maps = merge_vertex_indices((view(faces(v), idxs),)) + return FaceView(values(v)[_maps[1]], _fs) + else + return v[maps[1]] + end + end) + + return Mesh(new_va, new_fs) end - return MetaMesh(big_mesh, big_meta) +end + +""" + remove_duplicates(faces) + +Uses a Dict to remove duplicates from the given `faces`. +""" +function remove_duplicates(fs::AbstractVector{FT}) where {FT <: AbstractFace} + hashmap = Dict{FT, Nothing}() + foreach(k -> setindex!(hashmap, nothing, k), fs) + return collect(keys(hashmap)) end @@ -156,59 +503,26 @@ function map_coordinates!(f, mesh::AbstractMesh) return mesh end -function add_meta(mesh::MetaMesh; kw...) - return MetaMesh(Mesh(mesh), (; meta(mesh)..., kw...)) -end - -function add_meta(mesh::Mesh; kw...) - return MetaMesh(mesh, (; meta(mesh)..., kw...)) -end - -# I didn't find a simple way to remove a field from a namedtuple in a type stable way without -# a generated function.. -@generated function pop(nt::NamedTuple{Names, Values}, ::Val{name}) where {Names, Values, name} - if !(name in Names) - return :(throw(Base.KeyError($(QuoteNode(name))))) - else - names = filter(x-> x !== name, Names) - nt = map(names) do name - :($name = nt.$(name)) - end - return :((; $(nt...)), nt.$(name)) +function Base.show(io::IO, ::MIME"text/plain", mesh::Mesh{N, T, FT}) where {N, T, FT} + println(io, "Mesh{$N, $T, $FT}") + println(io, " faces: ", length(faces(mesh))) + for (name, attrib) in pairs(vertex_attributes(mesh)) + println(io, " vertex $(name): ", attrib isa FaceView ? length(attrib.data) : length(attrib)) end end -function pop_meta(mesh::MetaMesh, name::Symbol) - new_meta, value = pop(meta(mesh), Val(name)) - return MetaMesh(mesh, new_meta), value -end - - -function Base.get(f, mesh::MetaMesh, key::Symbol) - hasproperty(mesh, key) && return getproperty(mesh, key) - return f() -end - -function Base.show(io::IO, ::MIME"text/plain", mesh::Mesh{N, T}) where {N, T} - FT = eltype(faces(mesh)) - println(io, "Mesh{$N, $T, $(FT)}") - println(io, " vertices: ", length(coordinates(mesh))) - println(io, " faces: ", length(faces(mesh)), " $(FT)") -end - -function Base.show(io::IO, mesh::Mesh{N, T}) where {N, T} - FT = eltype(faces(mesh)) +function Base.show(io::IO, ::Mesh{N, T, FT}) where {N, T, FT} print(io, "Mesh{$N, $T, $(FT)}(...)") end function Base.show(io::IO, ::MIME"text/plain", mesh::MetaMesh{N, T}) where {N, T} FT = eltype(faces(mesh)) println(io, "MetaMesh{$N, $T, $(FT)}") - println(io, " vertices: ", length(coordinates(mesh))) - println(io, " faces: ", length(faces(mesh)), " $(FT)") - for (k, v) in pairs(meta(mesh)) - println(io, " ", k, ": ", length(v), " $(eltype(v))") + println(io, " faces: ", length(faces(mesh))) + for (name, attrib) in pairs(vertex_attributes(mesh)) + println(io, " vertex $(name): ", length(attrib)) end + println(io, " meta: ", keys(mesh.meta)) end function Base.show(io::IO, mesh::MetaMesh{N, T}) where {N, T} diff --git a/src/offsetintegers.jl b/src/offsetintegers.jl index c921f1dd..f0f0d85c 100644 --- a/src/offsetintegers.jl +++ b/src/offsetintegers.jl @@ -19,23 +19,18 @@ raw(x::Integer) = x value(x::OffsetInteger{O,T}) where {O,T} = raw(x) - O value(x::Integer) = x -function to_subscript(io::IO, x::Int) - if x in 0:9 - print(io, Char(0x2080+x)) - elseif x < 0 - print(io, Char(0x208B)); to_subscript(io, abs(x)) - else # positive + more than one digit - for d in reverse(digits(x)) - to_subscript(io, d) - end +function Base.show(io::IO, oi::OIT) where {O, T, OIT <: OffsetInteger{O, T}} + if OIT === GLIndex + typename = "GLIndex" + elseif O == 0 + typename = "OneIndex{$T}" + elseif O == 1 + typename = "ZeroIndex{$T}" + else + typename = "OffsetInteger{$O, $T}" end -end -function Base.show(io::IO, oi::OffsetInteger{O}) where {O} - o = O < 0 ? "ₒ₊" : "ₒ₋" - print(io, "<$(value(oi))$o") - to_subscript(io, abs(O)) - print(io, ">") + print(io, typename, "(", value(oi), ")") return end @@ -92,3 +87,5 @@ function Base.promote_rule(::Type{OffsetInteger{O1,T1}}, end Base.@pure pure_max(x1, x2) = x1 > x2 ? x1 : x2 + +Base.hash(o::OffsetInteger{O}, h::UInt) where {O} = hash(o.i, hash(O, h)) \ No newline at end of file diff --git a/src/precompiles.jl b/src/precompiles.jl index df41b1e6..100a69dc 100644 --- a/src/precompiles.jl +++ b/src/precompiles.jl @@ -1,6 +1,44 @@ +using PrecompileTools: @setup_workload, @compile_workload -function _precompile_() - ccall(:jl_generating_output, Cint, ()) == 1 || return nothing - Point2f(0.5, 0.1) in Triangle(Point2f(0), Point2f(0.5, 1), Point2f(1, 0)) - decompose(GLTriangleFace, [Point2f(0), Point2f(0.5, 1), Point2f(1, 0)]) +@setup_workload begin + @compile_workload begin + # Hits FaceView, QuadFace, all standard decompose's, some type conversions + r = Rect3d(Point3d(0), Vec3d(1)) + m1 = uv_normal_mesh(r) + + c = Circle(Point2f(0), 1) + m2 = uv_normal_mesh(c, pointtype = Point3f) # hits normal gen + + m = merge([m1, m2]) # hits mixed path, clear_faceviews, then normal path + GeometryBasics.split_mesh(m) + Rect3d(m) + + # Getters + vertex_attributes(m) + coordinates(m) + normals(m) + texturecoordinates(m) + faces(m) + + face_normals(coordinates(r), faces(r)) + + # Triangulation + triangle_mesh(Polygon(rand(Point2f, 4))) + + # Other primitives + uv_normal_mesh(Rect2(0,0,1,1)) + uv_normal_mesh(Tesselation(Sphere(Point3f(0), 1f0), 3)) + uv_normal_mesh(Cylinder(Point3f(0), Point3f(0,0,1), 1f0)) + uv_normal_mesh(Pyramid(Point3f(0), 1f0, 1f0)) + + # other disconnected compiles + M = Mat3f(I) + inv(M) + M[1, Vec(1, 3)] + M * Vec(1,2,3) + + Point2f(0.5, 0.1) in Triangle(Point2f(0), Point2f(0.5, 1), Point2f(1, 0)) + decompose(GLTriangleFace, [Point2f(0), Point2f(0.5, 1), Point2f(1, 0)]) + Point3f(0.5, 0, 1f0) in r + end end diff --git a/src/primitives/cylinders.jl b/src/primitives/cylinders.jl index adba8b8b..ce1c4aaa 100644 --- a/src/primitives/cylinders.jl +++ b/src/primitives/cylinders.jl @@ -1,40 +1,27 @@ """ - Cylinder{N, T} + Cylinder{T}(origin::Point3, extremity::Point3, radius) -A `Cylinder` is a 2D rectangle or a 3D cylinder defined by its origin point, -its extremity and a radius. `origin`, `extremity` and `r`, must be specified. +A `Cylinder` is a 3D primitive defined by an `origin`, an `extremity` (end point) +and a `radius`. """ -struct Cylinder{N,T} <: GeometryPrimitive{N,T} - origin::Point{N,T} - extremity::Point{N,T} +struct Cylinder{T} <: GeometryPrimitive{3, T} + origin::Point3{T} + extremity::Point3{T} r::T end -""" - Cylinder2{T} - Cylinder3{T} - -A `Cylinder2` or `Cylinder3` is a 2D/3D cylinder defined by its origin point, -its extremity and a radius. `origin`, `extremity` and `r`, must be specified. -""" -const Cylinder2{T} = Cylinder{2,T} -const Cylinder3{T} = Cylinder{3,T} - -origin(c::Cylinder{N,T}) where {N,T} = c.origin -extremity(c::Cylinder{N,T}) where {N,T} = c.extremity -radius(c::Cylinder{N,T}) where {N,T} = c.r -height(c::Cylinder{N,T}) where {N,T} = norm(c.extremity - c.origin) -direction(c::Cylinder{N,T}) where {N,T} = (c.extremity .- c.origin) ./ height(c) - -function rotation(c::Cylinder{2,T}) where {T} - d2 = direction(c) - u = Vec{3, T}(d2[1], d2[2], T(0)) - v = Vec{3, T}(u[2], -u[1], T(0)) - v = normalize(v) - return Mat{3, 3, T}(v..., u..., 0, 0, 1) +function Cylinder(origin::Point3{T1}, extremity::Point3{T2}, radius::T3) where {T1, T2, T3} + T = promote_type(T1, T2, T3) + Cylinder{T}(origin, extremity, radius) end -function rotation(c::Cylinder{3,T}) where {T} +origin(c::Cylinder) = c.origin +extremity(c::Cylinder) = c.extremity +radius(c::Cylinder) = c.r +height(c::Cylinder) = norm(c.extremity - c.origin) +direction(c::Cylinder) = (c.extremity .- c.origin) ./ height(c) + +function rotation(c::Cylinder{T}) where {T} d3 = direction(c) u = Vec{3, T}(d3[1], d3[2], d3[3]) if abs(u[1]) > 0 || abs(u[2]) > 0 @@ -48,60 +35,61 @@ function rotation(c::Cylinder{3,T}) where {T} return Mat{3, 3, T}(v..., w..., u...) end -function coordinates(c::Cylinder{2,T}, nvertices=(2, 2)) where {T} - r = Rect(c.origin[1] - c.r / 2, c.origin[2], c.r, height(c)) - M = rotation(c) - points = coordinates(r, nvertices) - vo = to_pointn(Point3{T}, origin(c)) - return (M * (to_pointn(Point3{T}, point) .- vo) .+ vo for point in points) -end +function coordinates(c::Cylinder{T}, nvertices=30) where {T} + nvertices += isodd(nvertices) + nhalf = div(nvertices, 2) -function faces(::Cylinder{2}, nvertices=(2, 2)) - return faces(Rect(0, 0, 1, 1), nvertices) + R = rotation(c) + step = 2pi / nhalf + + ps = Vector{Point3{T}}(undef, nvertices + 2) + for i in 1:nhalf + phi = (i-1) * step + ps[i] = R * Point3{T}(c.r * cos(phi), c.r * sin(phi), 0) + c.origin + end + for i in 1:nhalf + phi = (i-1) * step + ps[i + nhalf] = R * Point3{T}(c.r * cos(phi), c.r * sin(phi), 0) + c.extremity + end + ps[end-1] = c.origin + ps[end] = c.extremity + + return ps end -function coordinates(c::Cylinder{3,T}, nvertices=30) where {T} - if isodd(nvertices) - nvertices = 2 * (nvertices ÷ 2) - end - nvertices = max(8, nvertices) - nbv = nvertices ÷ 2 +function normals(c::Cylinder, nvertices = 30) + nvertices += isodd(nvertices) + nhalf = div(nvertices, 2) - M = rotation(c) - h = height(c) - range = 1:(2 * nbv + 2) - function inner(i) - return if i == length(range) - c.extremity - elseif i == length(range) - 1 - origin(c) - else - phi = T((2π * (((i + 1) ÷ 2) - 1)) / nbv) - up = ifelse(isodd(i), 0, h) - (M * Point(c.r * cos(phi), c.r * sin(phi), up)) .+ c.origin - end + R = rotation(c) + step = 2pi / nhalf + + ns = Vector{Vec3f}(undef, nhalf + 2) + for i in 1:nhalf + phi = (i-1) * step + ns[i] = R * Vec3f(cos(phi), sin(phi), 0) end + ns[end-1] = R * Vec3f(0, 0, -1) + ns[end] = R * Vec3f(0, 0, 1) - return (inner(i) for i in range) + disk1 = map(i -> GLTriangleFace(nhalf+1), 1:nhalf) + mantle = map(i -> QuadFace(i, mod1(i+1, nhalf), mod1(i+1, nhalf), i), 1:nhalf) + disk2 = map(i -> GLTriangleFace(nhalf+2), 1:nhalf) + fs = vcat(disk1, mantle, disk2) + + return FaceView(ns, fs) end -function faces(c::Cylinder{3}, facets=30) - isodd(facets) ? facets = 2 * div(facets, 2) : nothing - facets < 8 ? facets = 8 : nothing - nbv = Int(facets / 2) - indexes = Vector{TriangleFace{Int}}(undef, facets) - index = 1 - for j in 1:(nbv - 1) - indexes[index] = (index + 2, index + 1, index) - indexes[index + 1] = (index + 3, index + 1, index + 2) - index += 2 - end - indexes[index] = (1, index + 1, index) - indexes[index + 1] = (2, index + 1, 1) +function faces(::Cylinder, facets=30) + nvertices = facets + isodd(facets) + nhalf = div(nvertices, 2) - for i in 1:length(indexes) - i % 2 == 1 ? push!(indexes, (indexes[i][1], indexes[i][3], 2 * nbv + 1)) : - push!(indexes, (indexes[i][2], indexes[i][1], 2 * nbv + 2)) + disk1 = map(i -> GLTriangleFace(nvertices+1, mod1(i+1, nhalf), i), 1:nhalf) + mantle = map(1:nhalf) do i + i1 = mod1(i+1, nhalf) + QuadFace(i, i1, i1 + nhalf, i+nhalf) end - return indexes + disk2 = map(i -> GLTriangleFace(nvertices+2, i+nhalf, mod1(i+1, nhalf)+nhalf), 1:nhalf) + + return vcat(disk1, mantle, disk2) end diff --git a/src/primitives/pyramids.jl b/src/primitives/pyramids.jl index de2851fb..cfd89527 100644 --- a/src/primitives/pyramids.jl +++ b/src/primitives/pyramids.jl @@ -1,9 +1,21 @@ +""" + Pyramid(middle::Point3, length::Real, width::Real) + +A Pyramid is an axis-aligned primitive where the tip of the Pyramid extends by +`length` from `middle` in z direction and the square base extends by `width` +in ±x and ±y direction from `middle`. +""" struct Pyramid{T} <: GeometryPrimitive{3,T} middle::Point{3,T} length::T width::T end +function Pyramid(middle::Point{3, T1}, length::T2, width::T3) where {T1, T2, T3} + T = promote_type(T1, T2, T3) + return Pyramid(Point3{T}(middle), T(length), T(width)) +end + function coordinates(p::Pyramid{T}) where {T} leftup = Point{3,T}(-p.width, p.width, 0) / 2 leftdown = Point(-p.width, -p.width, 0) / 2 @@ -12,16 +24,22 @@ function coordinates(p::Pyramid{T}) where {T} ld = Point{3,T}(p.middle + leftdown) ru = Point{3,T}(p.middle - leftdown) rd = Point{3,T}(p.middle - leftup) - return Point{3,T}[ - tip, rd, ru, - tip, ru, lu, - tip, lu, ld, - tip, ld, rd, - ru, rd, lu, - ld, lu, rd - ] + return Point{3,T}[tip, rd, ru, lu, ld] +end + +function normals(p::Pyramid) + w = p.width; h = p.length + ns = normalize.(Vec3f[(h, 0, w), (0, h, w), (-h, 0, w), (0, -h, w), (0, 0, -1)]) + fs = [GLTriangleFace(1), GLTriangleFace(2), GLTriangleFace(3), GLTriangleFace(4), QuadFace(5)] + return FaceView(ns, fs) end function faces(::Pyramid) - return (TriangleFace(triangle) for triangle in TupleView{3}(1:18)) + return [ + GLTriangleFace(1, 2, 3), + GLTriangleFace(1, 3, 4), + GLTriangleFace(1, 4, 5), + GLTriangleFace(1, 5, 2), + QuadFace(2, 3, 4, 5) + ] end diff --git a/src/primitives/rectangles.jl b/src/primitives/rectangles.jl index 89c45cac..045d266d 100644 --- a/src/primitives/rectangles.jl +++ b/src/primitives/rectangles.jl @@ -62,7 +62,7 @@ function Rect{N,T1}(a::Rect{N,T2}) where {N,T1,T2} return Rect(Vec{N,T1}(minimum(a)), Vec{N,T1}(widths(a))) end -function Rect(v1::Vec{N,T1}, v2::Vec{N,T2}) where {N,T1,T2} +function Rect(v1::VecTypes{N,T1}, v2::VecTypes{N,T2}) where {N,T1,T2} T = promote_type(T1, T2) return Rect{N,T}(Vec{N,T}(v1), Vec{N,T}(v2)) end @@ -308,20 +308,21 @@ function Base.to_indices(A::AbstractMatrix{T}, I::Tuple{Rect2{IT}}) where {T,IT< return ((mini[1] + 1):(mini[1] + wh[1]), (mini[2] + 1):(mini[2] + wh[2])) end -function minmax(p::StaticVector, vmin, vmax) +function _minmax(p::StaticVector, vmin, vmax) any(isnan, p) && return (vmin, vmax) return min.(p, vmin), max.(p, vmax) end +# TODO: doesn't work regardless # Annoying special case for view(Vector{Point}, Vector{Face}) -function minmax(tup::Tuple, vmin, vmax) - for p in tup - any(isnan, p) && continue - vmin = min.(p, vmin) - vmax = max.(p, vmax) - end - return vmin, vmax -end +# function Base.minmax(tup::Tuple, vmin, vmax) +# for p in tup +# any(isnan, p) && continue +# vmin = min.(p, vmin) +# vmax = max.(p, vmax) +# end +# return vmin, vmax +# end function positive_widths(rect::Rect{N,T}) where {N,T} mini, maxi = minimum(rect), maximum(rect) @@ -544,21 +545,30 @@ centered(R::Type{Rect}) = R(Vec{2,Float32}(-0.5), Vec{2,Float32}(1)) # Rect2 decomposition function faces(rect::Rect2, nvertices=(2, 2)) - w, h = nvertices - idx = LinearIndices(nvertices) - quad(i, j) = QuadFace{Int}(idx[i, j], idx[i + 1, j], idx[i + 1, j + 1], idx[i, j + 1]) - return ivec((quad(i, j) for i in 1:(w - 1), j in 1:(h - 1))) + if nvertices == (2, 2) + return [QuadFace(1,2,3,4)] + else + w, h = nvertices + idx = LinearIndices(nvertices) + quad(i, j) = QuadFace{Int}(idx[i, j], idx[i + 1, j], idx[i + 1, j + 1], idx[i, j + 1]) + return [quad(i, j) for j in 1:(h - 1) for i in 1:(w - 1)] + end end -function coordinates(rect::Rect2, nvertices=(2, 2)) +function coordinates(rect::Rect2{T}, nvertices=(2, 2)) where {T} mini, maxi = extrema(rect) - xrange, yrange = LinRange.(mini, maxi, nvertices) - return [Point(x, y) for y in yrange for x in xrange] + if nvertices == (2, 2) + return Point2{T}[mini, (maxi[1], mini[2]), maxi, (mini[1], maxi[2])] + else + xrange, yrange = LinRange.(mini, maxi, nvertices) + return [Point(x, y) for y in yrange for x in xrange] + end end -function texturecoordinates(rect::Rect2, nvertices=(2, 2)) - xrange, yrange = LinRange.((0, 1), (1, 0), nvertices) - return ivec(((x, y) for x in xrange, y in yrange)) +function texturecoordinates(rect::Rect2{T}, nvertices=(2, 2)) where {T} + ps = coordinates(Rect2{T}(0, 0, 1, 1), nvertices) + ps = [Vec2{T}(0, 1) .+ Vec2{T}(1, -1) .* p for p in ps] + return ps end function normals(rect::Rect2, nvertices=(2, 2)) @@ -567,22 +577,25 @@ end ## # Rect3 decomposition -function coordinates(rect::Rect3) +function coordinates(rect::Rect3{T}) where T # TODO use n w = widths(rect) o = origin(rect) - points = Point{3,Int}[(0, 0, 0), (0, 0, 1), (0, 1, 1), (0, 1, 0), (0, 0, 0), (1, 0, 0), - (1, 0, 1), (0, 0, 1), (0, 0, 0), (0, 1, 0), (1, 1, 0), (1, 0, 0), - (1, 1, 1), (0, 1, 1), (0, 0, 1), (1, 0, 1), (1, 1, 1), (1, 0, 1), - (1, 0, 0), (1, 1, 0), (1, 1, 1), (1, 1, 0), (0, 1, 0), (0, 1, 1)] - return ((x .* w .+ o) for x in points) + return Point{3, T}[o + (x, y, z) .* w for x in (0, 1) for y in (0, 1) for z in (0, 1)] +end + +function normals(::Rect3) + ns = Vec3f[(-1,0,0), (1,0,0), (0,-1,0), (0,1,0), (0,0,-1), (0,0,1)] + return FaceView(ns, QuadFace{Int}.(1:6)) end function texturecoordinates(rect::Rect3) return coordinates(Rect3(0, 0, 0, 1, 1, 1)) end -function faces(rect::Rect3) - return QuadFace{Int}[(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, 11, 12), (13, 14, 15, 16), - (17, 18, 19, 20), (21, 22, 23, 24),] +function faces(::Rect3) + return QuadFace{Int}[ + (1, 2, 4, 3), (7, 8, 6, 5), (5, 6, 2, 1), + (3, 4, 8, 7), (1, 3, 7, 5), (6, 8, 4, 2) + ] end diff --git a/src/primitives/spheres.jl b/src/primitives/spheres.jl index 0eb2fea0..76602326 100644 --- a/src/primitives/spheres.jl +++ b/src/primitives/spheres.jl @@ -42,15 +42,23 @@ function centered(::Type{T}) where {T<:HyperSphere} end function coordinates(s::Circle, nvertices=64) - rad = radius(s) - inner(fi) = Point(rad * sin(fi + pi), rad * cos(fi + pi)) .+ origin(s) - return [inner(fi) for fi in LinRange(0, 2pi, nvertices)] + r = radius(s); o = origin(s) + ps = [r * Point(cos(phi), sin(phi)) + o for phi in LinRange(0, 2pi, nvertices)] + # ps[end] = o + return ps end function texturecoordinates(::Circle, nvertices=64) return coordinates(Circle(Point2f(0.5), 0.5f0), nvertices) end +# TODO: Consider generating meshes for circles with a point in the center so +# that the triangles are more regular +# function faces(::Circle, nvertices=64) +# return [GLTriangleFace(nvertices+1, i, mod1(i+1, nvertices)) for i in 1:nvertices] +# end + + function coordinates(s::Sphere, nvertices=24) θ = LinRange(0, pi, nvertices) φ = LinRange(0, 2pi, nvertices) diff --git a/test/fixed_arrays.jl b/test/fixed_arrays.jl index cf82d0a3..256d5f31 100644 --- a/test/fixed_arrays.jl +++ b/test/fixed_arrays.jl @@ -34,25 +34,6 @@ end @test Mat{2, 2}(foo.(values(M1), 1)) == foo.(M1, 1) end - # TODO: Should this work with tuple somehow? - @testset "with Vector (acting as const)" begin - for T1 in (Vec, Point) - x = [T1(2, 3, 1), T1(7, 3, 2)] - for T2 in (Vec, Point) - T = ifelse(T1 == Point, Point, ifelse(T2 == Point, Point, Vec)) - @test [T(4, 9, 4), T(14, 9, 8)] == x .* T2(2, 3, 4) - @test [T(foo(v, T2(3, -1, 1))) for v in x] == foo.(x, T2(3, -1, 1)) - end - end - - x = [M1, M2, M1] - @test [M1 + M2, M2 + M2, M1 + M2] == x .+ M2 - @test [foo(M1, M2), foo(M2, M2), foo(M1, M2)] == foo.(x, M2) - - # maybe bad...? - @test [Vec(2, 3), Vec(3, 4)] == [1, 2] .+ Vec(1, 2) - end - @testset "chained/nested" begin for T1 in (Vec, Point, tuple) for T2 in (Vec, Point, tuple) @@ -62,15 +43,6 @@ end @test T(-15, -5) == foo.(T1(1,2), T1(-1, 0) .+ foo.(T1(1,1), T2(2,2))) end end - - @test [Point(1, 3), Point(3, 1)] == [Vec(1,2), Vec(2, 1)] .+ [Vec(1,2), Vec(2, 1)] .+ Point(-1, -1) - @test [Vec(1, 3), Point(3, 1)] == [Vec(1,2), Vec(2, 1)] .+ Vec(-1, -1) .+ [Vec(1,2), Point(2, 1)] - @test [Vec(-1, -3), Point(-3, -1)] == foo.([Vec(1,2), Vec(2, 1)] .+ Vec(-1, -1), [Vec(1,2), Point(2, 1)]) - - x = [M1, M2] - @test [M1 * M1 + M2, M1 * M2 + M2] == M1 .* x .+ M2 - @test [M1 + M2 * M1, M2 + M2 * M2] == x .+ M2 .* x - @test [foo(M1+M2, M1), foo(M2+M2, M2)] == foo.(x .+ M2, x) end @testset "Longer functions" begin @@ -117,12 +89,19 @@ end @test_throws BoundsError getindex(M3, 10) # Sanity check for loop - @test M3[2, Vec(1,2)] == Mat{1, 2}(M3[2,1], M3[2,2]) + # @test M3[2, Vec(1,2)] == Mat{1, 2}(M3[2,1], M3[2,2]) + @test M3[2, Vec(1,2)] == Vec2(M3[2,1], M3[2,2]) for x in (2, Vec(1,2), Vec(1,1,2,2)) for y in (2, Vec(1,2), Vec(1,1,2,2)) x isa Real && y isa Real && continue - @test M3[x, y] == Mat{length(x), length(y)}((M3[i, j] for j in y for i in x)...) + if length(x) == 1 + @test M3[x, y] == Vec{length(y)}((M3[i, j] for j in y for i in x)...) + elseif length(y) == 1 + @test M3[x, y] == Vec{length(x)}((M3[i, j] for j in y for i in x)...) + else + @test M3[x, y] == Mat{length(x), length(y)}((M3[i, j] for j in y for i in x)...) + end @test_throws BoundsError M3[x .- 2, y] @test_throws BoundsError M3[x, y .+ 2] @test_throws BoundsError M3[x .+ 2, y .- 2] @@ -130,4 +109,18 @@ end end end + + for N in 1:4 + @testset "math $N x $N" begin + bm = rand(N, N) + I + sm = Mat{N, N}(bm) + bv = rand(N) + sv = Vec{N, Float64}(bv) + @test bm == Matrix(sm) + @test det(bm) ≈ det(sm) + @test inv(bm) ≈ Matrix(inv(sm)) + @test collect(transpose(bm)) ≈ Matrix(transpose(sm)) + @test bm * bv ≈ collect(sm * sv) + end + end end \ No newline at end of file diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index ec68a151..420dbe37 100644 --- a/test/geometrytypes.jl +++ b/test/geometrytypes.jl @@ -2,24 +2,11 @@ using Test, GeometryBasics @testset "Cylinder" begin @testset "constructors" begin - o, extr, r = Point2f(1, 2), Point2f(3, 4), 5.0f0 - s = Cylinder(o, extr, r) - @test typeof(s) == Cylinder{2,Float32} - @test typeof(s) == Cylinder2{Float32} - @test origin(s) == o - @test extremity(s) == extr - @test radius(s) == r - #@test abs(height(s)- norm([1,2]-[3,4]))<1e-5 - h = norm(o - extr) - @test isapprox(height(s), h) - #@test norm(direction(s) - Point{2,Float32}([2,2]./norm([1,2]-[3,4])))<1e-5 - @test isapprox(direction(s), Point2f(2, 2) ./ h) v1 = rand(Point{3,Float64}) v2 = rand(Point{3,Float64}) R = rand() s = Cylinder(v1, v2, R) - @test typeof(s) == Cylinder{3,Float64} - @test typeof(s) == Cylinder3{Float64} + @test typeof(s) == Cylinder{Float64} @test origin(s) == v1 @test extremity(s) == v2 @test radius(s) == R @@ -29,79 +16,103 @@ using Test, GeometryBasics end @testset "decompose" begin - - m = GeometryBasics.normal_mesh(Sphere(Point3f(0), 1f0)) - @test decompose_uv(m) isa Vector{Vec2f} - - o, extr, r = Point2f(1, 2), Point2f(3, 4), 5.0f0 - s = Cylinder(o, extr, r) - positions = Point{3,Float32}[(-0.7677671, 3.767767, 0.0), - (2.767767, 0.23223293, 0.0), - (0.23223293, 4.767767, 0.0), - (3.767767, 1.2322329, 0.0), (1.2322329, 5.767767, 0.0), - (4.767767, 2.232233, 0.0)] - @test decompose(Point3f, Tesselation(s, (2, 3))) ≈ positions - - FT = TriangleFace{Int} - faces = FT[(1, 2, 4), (1, 4, 3), (3, 4, 6), (3, 6, 5)] - @test faces == decompose(FT, Tesselation(s, (2, 3))) - v1 = Point{3,Float64}(1, 2, 3) v2 = Point{3,Float64}(4, 5, 6) R = 5.0 s = Cylinder(v1, v2, R) - positions = Point{3,Float64}[(4.535533905932738, -1.5355339059327373, 3.0), - (7.535533905932738, 1.4644660940672627, 6.0), - (3.0412414523193148, 4.041241452319315, - -1.0824829046386295), - (6.041241452319315, 7.041241452319315, - 1.9175170953613705), - (-2.535533905932737, 5.535533905932738, - 2.9999999999999996), - (0.46446609406726314, 8.535533905932738, 6.0), - (-1.0412414523193152, -0.04124145231931431, - 7.0824829046386295), - (1.9587585476806848, 2.9587585476806857, - 10.08248290463863), (1, 2, 3), (4, 5, 6)] + positions = Point{3,Float64}[ + (4.535533905932738, -1.5355339059327373, 3.0), + (3.0412414523193148, 4.041241452319315, -1.0824829046386295), + (-2.535533905932737, 5.535533905932738, 2.9999999999999996), + (-1.0412414523193152, -0.04124145231931431, 7.0824829046386295), + (7.535533905932738, 1.4644660940672627, 6.0), + (6.041241452319315, 7.041241452319315, 1.9175170953613705), + (0.46446609406726314, 8.535533905932738, 6.0), + (1.9587585476806848, 2.9587585476806857, 10.08248290463863), + (1, 2, 3), + (4, 5, 6) + ] @test decompose(Point3{Float64}, Tesselation(s, 8)) ≈ positions - faces = TriangleFace{Int}[(3, 2, 1), (4, 2, 3), (5, 4, 3), (6, 4, 5), (7, 6, 5), - (8, 6, 7), (1, 8, 7), (2, 8, 1), (3, 1, 9), (2, 4, 10), - (5, 3, 9), (4, 6, 10), (7, 5, 9), (6, 8, 10), (1, 7, 9), - (8, 2, 10)] - @test faces == decompose(TriangleFace{Int}, Tesselation(s, 8)) + _faces = TriangleFace[ + (9, 2, 1), (9, 3, 2), (9, 4, 3), (9, 1, 4), (1, 2, 6), (1, 6, 5), + (2, 3, 7), (2, 7, 6), (3, 4, 8), (3, 8, 7), (4, 1, 5), (4, 5, 8), + (10, 5, 6), (10, 6, 7), (10, 7, 8), (10, 8, 5)] + + @test _faces == decompose(TriangleFace{Int}, Tesselation(s, 8)) m = triangle_mesh(Tesselation(s, 8)) @test m === triangle_mesh(m) - @test GeometryBasics.faces(m) == faces + @test GeometryBasics.faces(m) == decompose(GLTriangleFace, _faces) @test GeometryBasics.coordinates(m) ≈ positions - m = normal_mesh(s)# just test that it works without explicit resolution parameter + + m = normal_mesh(s) # just test that it works without explicit resolution parameter + @test hasproperty(m, :position) + @test hasproperty(m, :normal) + @test length(faces(m)) == length(faces(m.normal)) + @test faces(m) isa AbstractVector{GLTriangleFace} + @test faces(m.normal) isa AbstractVector{GLTriangleFace} + + ns = GeometryBasics.FaceView( + Vec{3, Float32}[ + [0.70710677, -0.70710677, 0.0], [0.4082483, 0.4082483, -0.8164966], + [-0.70710677, 0.70710677, -9.9991995f-17], [-0.4082483, -0.4082483, 0.8164966], + [-0.57735026, -0.57735026, -0.57735026], [0.57735026, 0.57735026, 0.57735026] + ], [ + GLTriangleFace(5, 5, 5), GLTriangleFace(5, 5, 5), + GLTriangleFace(5, 5, 5), GLTriangleFace(5, 5, 5), + QuadFace{Int64}(1, 2, 2, 1), QuadFace{Int64}(2, 3, 3, 2), + QuadFace{Int64}(3, 4, 4, 3), QuadFace{Int64}(4, 1, 1, 4), + GLTriangleFace(6, 6, 6), GLTriangleFace(6, 6, 6), + GLTriangleFace(6, 6, 6), GLTriangleFace(6, 6, 6) + ] + ) + + @test ns == decompose_normals(Tesselation(s, 8)) muv = uv_mesh(s) - @test Rect(Point.(texturecoordinates(muv))) == Rect2f(Vec2f(0), Vec2f(1.0)) + @test !hasproperty(muv, :uv) # not defined yet end end @testset "HyperRectangles" begin a = Rect(Vec(0, 0), Vec(1, 1)) - pt_expa = Point{2,Int}[(0, 0), (1, 0), (0, 1), (1, 1)] + pt_expa = Point{2,Int}[(0, 0), (1, 0), (1, 1), (0, 1)] @test decompose(Point{2,Int}, a) == pt_expa mesh = normal_mesh(a) @test decompose(Point2f, mesh) == pt_expa b = Rect(Vec(1, 1, 1), Vec(1, 1, 1)) - pt_expb = Point{3,Int64}[[1, 1, 1], [1, 1, 2], [1, 2, 2], [1, 2, 1], [1, 1, 1], - [2, 1, 1], [2, 1, 2], [1, 1, 2], [1, 1, 1], [1, 2, 1], - [2, 2, 1], [2, 1, 1], [2, 2, 2], [1, 2, 2], [1, 1, 2], - [2, 1, 2], [2, 2, 2], [2, 1, 2], [2, 1, 1], [2, 2, 1], - [2, 2, 2], [2, 2, 1], [1, 2, 1], [1, 2, 2]] + pt_expb = Point{3,Int64}[[1, 1, 1], [1, 1, 2], [1, 2, 1], [1, 2, 2], + [2, 1, 1], [2, 1, 2], [2, 2, 1], [2, 2, 2]] @test decompose(Point{3,Int}, b) == pt_expb + + mesh = normal_mesh(b) + @test faces(mesh) == GLTriangleFace[ + (1, 2, 4), (1, 4, 3), (7, 8, 6), (7, 6, 5), (5, 6, 2), (5, 2, 1), + (3, 4, 8), (3, 8, 7), (1, 3, 7), (1, 7, 5), (6, 8, 4), (6, 4, 2)] + @test normals(mesh) == GeometryBasics.FaceView( + Vec{3, Float32}[[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [0.0, 0.0, 1.0]], + GLTriangleFace[(1, 1, 1), (1, 1, 1), (2, 2, 2), (2, 2, 2), (3, 3, 3), (3, 3, 3), (4, 4, 4), (4, 4, 4), (5, 5, 5), (5, 5, 5), (6, 6, 6), (6, 6, 6)] + ) + @test coordinates(mesh) == Point{3, Float32}[ + [1.0, 1.0, 1.0], [1.0, 1.0, 2.0], [1.0, 2.0, 1.0], [1.0, 2.0, 2.0], + [2.0, 1.0, 1.0], [2.0, 1.0, 2.0], [2.0, 2.0, 1.0], [2.0, 2.0, 2.0]] @test isempty(Rect{3,Float32}()) end +@testset "Pyramids" begin + p = Pyramid(Point3f(0), 1f0, 0.2f0) + @test coordinates(p) == Point3f[[0.0, 0.0, 1.0], [0.1, -0.1, 0.0], [0.1, 0.1, 0.0], [-0.1, 0.1, 0.0], [-0.1, -0.1, 0.0]] + @test faces(p) == [GLTriangleFace(1, 2, 3), GLTriangleFace(1, 3, 4), GLTriangleFace(1, 4, 5), GLTriangleFace(1, 5, 2), QuadFace{Int64}(2, 3, 4, 5)] + ns = normals(p) + @test faces(ns) == [GLTriangleFace(1), GLTriangleFace(2), GLTriangleFace(3), GLTriangleFace(4), QuadFace{Int64}(5)] + @test values(ns) ≈ Vec3f[[0.9805807, 0.0, 0.19611615], [0.0, 0.9805807, 0.19611615], [-0.9805807, 0.0, 0.19611615], [0.0, -0.9805807, 0.19611615], [0.0, 0.0, -1.0]] +end + NFace = NgonFace @testset "Faces" begin @@ -127,18 +138,31 @@ NFace = NgonFace end @testset "Normals" begin - n64 = Vec{3,Float64}[(0.0, 0.0, -1.0), (0.0, 0.0, -1.0), (0.0, 0.0, -1.0), - (0.0, 0.0, -1.0), (0.0, 0.0, 1.0), (0.0, 0.0, 1.0), - (0.0, 0.0, 1.0), (0.0, 0.0, 1.0), (-1.0, 0.0, 0.0), - (-1.0, 0.0, 0.0), (-1.0, 0.0, 0.0), (-1.0, 0.0, 0.0), - (1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 0.0, 0.0), (1.0, 0.0, 0.0), - (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), - (0.0, -1.0, 0.0), (0.0, -1.0, 0.0), (0.0, -1.0, 0.0), - (0.0, -1.0, 0.0),] - n32 = map(Vec{3,Float32}, n64) - r = triangle_mesh(centered(Rect3)) - # @test normals(coordinates(r), GeometryBasics.faces(r)) == n32 - # @test normals(coordinates(r), GeometryBasics.faces(r)) == n64 + # per face normals + r = Rect3f(Point3f(0), Vec3f(1)) + ns = face_normals(coordinates(r), faces(r)) + ux = unit(Vec3f, 1); uy = unit(Vec3f, 2); uz = unit(Vec3f, 3) + @test ns == normals(r) + @test values(ns) == [-ux, ux, -uy, uy, -uz, uz] + + # typing + ux = unit(Vec3d, 1); uy = unit(Vec3d, 2); uz = unit(Vec3d, 3) + ns = face_normals(decompose(Point3d, r), faces(r)) + @test ns isa FaceView{Vec3d} + @test values(ns) == [-ux, ux, -uy, uy, -uz, uz] + + # Mixed + c = Cylinder(Point3f(0), Point3f(0,0,1), 0.5f0) + ns = normals(c) + # caps without mantle + f_ns = face_normals(coordinates(c), filter!(f -> f isa TriangleFace, faces(c))) + @test all(n -> n == values(ns)[end-1], values(f_ns)[1:15]) + @test all(n -> n == values(ns)[end], values(f_ns)[16:end]) + # Mantle without caps + v_ns = normals(coordinates(c), filter!(f -> f isa QuadFace, faces(c)))[1:end-2] + @test values(ns)[1:15] ≈ v_ns[1:15] + @test values(ns)[1:15] ≈ v_ns[16:30] # repeated via FaceView in ns + end @testset "HyperSphere" begin diff --git a/test/meshes.jl b/test/meshes.jl index 192c7400..485fa54b 100644 --- a/test/meshes.jl +++ b/test/meshes.jl @@ -1,34 +1,338 @@ -@testset "Meshing a single triangle sometimes returns an empty mesh" begin - ϕ = (sqrt(5)+1)/2 - p,q,r = Point(ϕ,0,+1),Point(1,ϕ,0),Point(ϕ,0,-1) - m = triangle_mesh(Triangle(p,q,r)) - @test m isa Mesh - @test faces(m) == [TriangleFace(1, 2, 3)] -end - -@testset "Heterogenous faces" begin - # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/142 - f = [TriangleFace(1, 2, 3), QuadFace(1, 2, 3, 4)] - p = Point2f[(0, 1), (1, 2), (3, 4), (4, 5)] - m = Mesh(p, f) - @test collect(m) == [Triangle(p[1], p[2], p[3]), GeometryBasics.Quadrilateral(p[1], p[2], p[3], p[4])] -end - -@testset "Heterogenous faces" begin - # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/142 - f = [TriangleFace(1, 2, 3), QuadFace(1, 2, 3, 4)] - p = Point2f[(0, 1), (1, 2), (3, 4), (4, 5)] - m = Mesh(p, f) - @test collect(m) == [Triangle(p[1], p[2], p[3]), GeometryBasics.Quadrilateral(p[1], p[2], p[3], p[4])] -end - -@testset "Ambiguous NgonFace constructors" begin - # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/151 - # Currently no StaticVector support - # t = TriangleFace(SA[0.4, 0.2, 0.55]) -end - -@testset "Merge empty vector of meshes" begin - # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/136 - merge(Mesh[]) == Mesh(Point3f[], GLTriangleFace[]) -end +@testset "Meshing a single triangle sometimes returns an empty mesh" begin + ϕ = (sqrt(5)+1)/2 + p,q,r = Point(ϕ,0,+1),Point(1,ϕ,0),Point(ϕ,0,-1) + m = triangle_mesh(Triangle(p,q,r)) + @test m isa Mesh + @test faces(m) == [TriangleFace(1, 2, 3)] +end + +@testset "Heterogenous faces" begin + # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/142 + f = [TriangleFace(1, 2, 3), QuadFace(1, 2, 3, 4)] + p = Point2f[(0, 1), (1, 2), (3, 4), (4, 5)] + m = Mesh(p, f) + @test collect(m) == [Triangle(p[1], p[2], p[3]), GeometryBasics.Quadrilateral(p[1], p[2], p[3], p[4])] +end + +@testset "Heterogenous faces" begin + # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/142 + f = [TriangleFace(1, 2, 3), QuadFace(1, 2, 3, 4)] + p = Point2f[(0, 1), (1, 2), (3, 4), (4, 5)] + m = Mesh(p, f) + @test collect(m) == [Triangle(p[1], p[2], p[3]), GeometryBasics.Quadrilateral(p[1], p[2], p[3], p[4])] +end + +@testset "Ambiguous NgonFace constructors" begin + # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/151 + # Currently no StaticVector support + # t = TriangleFace(SA[0.4, 0.2, 0.55]) +end + +@testset "Merge empty vector of meshes" begin + # https://github.com/JuliaGeometry/GeometryBasics.jl/issues/136 + merge(Mesh[]) == Mesh(Point3f[], GLTriangleFace[]) +end + +@testset "Vertex Index Remapping" begin + # Sanity Check + m = Mesh( + position = GeometryBasics.FaceView(Point2f[(0, 0), (1, 0), (1, 1), (0, 1)], [QuadFace(1,2,3,4)]), + normal = GeometryBasics.FaceView([Vec3f(0,0,1)], [QuadFace(1)]) + ) + + m2 = GeometryBasics.clear_faceviews(m) + + @test faces(m) == [QuadFace(1,2,3,4)] + @test coordinates(m) == Point2f[(0, 0), (1, 0), (1, 1), (0, 1)] + @test normals(m) == GeometryBasics.FaceView([Vec3f(0,0,1)], [QuadFace(1)]) + @test isempty(m.views) + + @test faces(m2) == [QuadFace(1,2,3,4)] + @test coordinates(m2) == coordinates(m) + @test normals(m2) != normals(m) + @test normals(m2) == [only(values(normals(m))) for _ in 1:4] + @test isempty(m2.views) +end + +@testset "complex merge" begin + rects = [Rect3f(Point3f(x, y, z), Vec3f(0.5)) for x in -1:1 for y in -1:1 for z in -1:1] + direct_meshes = map(rects) do r + GeometryBasics.Mesh(coordinates(r), faces(r), normal = normals(r)) + end + dm = merge(direct_meshes) + + @test GeometryBasics.facetype(dm) == QuadFace{Int64} + @test length(faces(dm)) == 27 * 6 # 27 rects, 6 quad faces + @test length(normals(dm)) == 27 * 6 + @test length(coordinates(dm)) == 27 * 8 + @test normals(dm) isa GeometryBasics.FaceView + @test coordinates(dm) isa Vector + @test !allunique([idx for f in faces(dm) for idx in f]) + @test !allunique([idx for f in faces(dm.normal) for idx in f]) + + indirect_meshes = map(rects) do r + m = GeometryBasics.mesh(coordinates(r), faces(r), normal = normals(r), facetype = QuadFace{Int64}) + # Also testing merge of meshes with views + push!(m.views, 1:length(faces(m))) + m + end + im = merge(indirect_meshes) + + @test im == dm + + converted_meshes = map(rects) do r + m = GeometryBasics.Mesh(coordinates(r), faces(r), normal = normals(r)) + GeometryBasics.clear_faceviews(m) + end + cm = merge(converted_meshes) + + @test GeometryBasics.facetype(cm) == QuadFace{Int64} + @test length(faces(cm)) == 27 * 6 # 27 rects, 6 quad faces + @test length(normals(cm)) == 27 * 6 * 4 # duplicate 4x across face + @test length(coordinates(cm)) == 27 * 8 * 3 # duplicate 3x across shared vertex + @test normals(cm) isa Vector + @test coordinates(cm) isa Vector + @test allunique([idx for f in faces(cm) for idx in f]) + + + mixed_meshes = map(direct_meshes, indirect_meshes, converted_meshes) do dm, im, cm + rand((dm, im, cm)) # (with FaceView, with mesh.views & FaceView, w/o FaceView) + end + mm = merge(mixed_meshes) + + @test mm == cm +end + +@testset "Mesh Constructor" begin + ps = rand(Point2f, 10) + ns = rand(Vec3f, 10) + fs = GLTriangleFace[(1,2,3), (3,4,5), (5,6,7), (8,9,10)] + + @testset "Extracting faces from position FaceView" begin + # can't extract from array + @test_throws TypeError Mesh(position = ps, normal = ns) + + m = Mesh(position = FaceView(ps, fs), normal = ns) + @test coordinates(m) == ps + @test normals(m) == ns + @test faces(m) == fs + end + + @testset "Verifaction" begin + # enough vertices present + @test_throws ErrorException Mesh(rand(Point2f, 7), fs) + m = Mesh(rand(Point2f, 12), fs) + @test length(m.position) == 12 + @test length(m.faces) == 4 + + @test_throws ErrorException Mesh(ps, fs, normal = rand(Vec3f, 8)) + m = Mesh(ps, fs, normal = rand(Vec3f, 12)) + @test length(m.position) == 10 + @test length(m.normal) == 12 + @test length(m.faces) == 4 + + # valid FaceView (enough faces, vertices, matching dims) + @test_throws ErrorException Mesh(ps, fs, normal = FaceView(ns, GLTriangleFace[])) + @test_throws ErrorException Mesh(ps, fs, normal = FaceView(Vec3f[], fs)) + @test_throws ErrorException Mesh(ps, fs, normal = FaceView(ns, QuadFace{Int}.(1:4))) + m = Mesh(ps, fs, normal = FaceView(rand(Vec3f, 9), TriangleFace{Int64}.(1:2:8))) + @test length(m.position) == 10 + @test length(values(m.normal)) == 9 + @test length(faces(m.normal)) == 4 + @test length(m.faces) == 4 + + msg = "`normals` as a vertex attribute name has been deprecated in favor of `normal` to bring it in line with mesh.position and mesh.uv" + @test_logs (:warn, msg) Mesh(ps, fs, normals = ns) + end +end + +@testset "Interface" begin + ps = rand(Point2f, 10) + ns = rand(Vec3f, 10) + uvs = FaceView(rand(Vec2f, 4), GLTriangleFace.(1:4)) + fs = GLTriangleFace[(1,2,3), (3,4,5), (5,6,7), (8,9,10)] + + m = Mesh(ps, fs, normal = ns, uv = uvs) + + @test vertex_attributes(m) == getfield(m, :vertex_attributes) + @test coordinates(m) == vertex_attributes(m)[:position] + @test normals(m) == vertex_attributes(m)[:normal] + @test texturecoordinates(m) == vertex_attributes(m)[:uv] + @test faces(m) == getfield(m, :faces) + + @test m.vertex_attributes == getfield(m, :vertex_attributes) + @test m.position == vertex_attributes(m)[:position] + @test m.normal == vertex_attributes(m)[:normal] + @test m.uv == vertex_attributes(m)[:uv] + @test m.faces == getfield(m, :faces) + + @test hasproperty(m, :vertex_attributes) + @test hasproperty(m, :position) + @test hasproperty(m, :normal) + @test hasproperty(m, :uv) + @test hasproperty(m, :faces) + + mm = MetaMesh(m, name = "test") + + @test Mesh(mm) == m + @test haskey(mm, :name) + @test get(mm, :name, nothing) == "test" + @test mm[:name] == "test" + @test !haskey(mm, :foo) + @test get!(mm, :foo, "bar") == "bar" + @test haskey(mm, :foo) + @test keys(mm) == keys(getfield(mm, :meta)) + + @test vertex_attributes(mm) == getfield(m, :vertex_attributes) + @test coordinates(mm) == vertex_attributes(m)[:position] + @test normals(mm) == vertex_attributes(m)[:normal] + @test texturecoordinates(mm) == vertex_attributes(m)[:uv] + @test faces(mm) == getfield(m, :faces) +end + +@testset "mesh() constructors" begin + r = Rect3d(Point3d(-1), Vec3d(2)) + + @testset "prerequisites" begin + ps = coordinates(r) + @test length(ps) == 8 + @test ps isa Vector{Point3d} + ns = normals(r) + @test length(ns) == 6 + @test ns isa GeometryBasics.FaceView{Vec3f, Vector{Vec3f}, Vector{QuadFace{Int64}}} + uvs = texturecoordinates(r) + @test length(uvs) == 8 + @test_broken uvs isa Vector{Vec2f} + fs = faces(r) + @test length(fs) == 6 + @test fs isa Vector{QuadFace{Int64}} + end + + @testset "normal_mesh()" begin + m = normal_mesh(r, pointtype = Point3f, normaltype = Vec3f) + m = GeometryBasics.clear_faceviews(m) + + @test hasproperty(m, :position) + @test coordinates(m) isa Vector{Point3f} + @test length(coordinates(m)) == 24 + @test GeometryBasics.pointtype(m) == Point3f + + @test hasproperty(m, :normal) + @test normals(m) isa Vector{Vec3f} + @test length(normals(m)) == 24 + + @test !hasproperty(m, :uv) + @test texturecoordinates(m) === nothing + + @test faces(m) isa Vector{GLTriangleFace} + @test length(faces(m)) == 12 + @test GeometryBasics.facetype(m) == GLTriangleFace + end + + @testset "normal_uv_mesh()" begin + m = uv_normal_mesh( + r, pointtype = Point3d, normaltype = Vec3d, + uvtype = Vec3d, facetype = QuadFace{Int32} + ) + + @test hasproperty(m, :position) + @test coordinates(m) isa Vector{Point3d} + @test length(coordinates(m)) == 8 + @test GeometryBasics.pointtype(m) == Point3d + + @test hasproperty(m, :normal) + @test normals(m) isa GeometryBasics.FaceView{Vec3d, Vector{Vec3d}, Vector{QuadFace{Int32}}} + @test length(normals(m)) == 6 + + @test hasproperty(m, :uv) + @test texturecoordinates(m) isa Vector{Vec3d} + @test length(texturecoordinates(m)) == 8 + + @test faces(m) isa Vector{QuadFace{Int32}} + @test length(faces(m)) == 6 + @test GeometryBasics.facetype(m) == QuadFace{Int32} + end + + @testset "uv_mesh()" begin + m = uv_mesh( + r, pointtype = Point3f, uvtype = Vec3f, facetype = GLTriangleFace + ) + + @test hasproperty(m, :position) + @test coordinates(m) isa Vector{Point3f} + @test length(coordinates(m)) == 8 + @test GeometryBasics.pointtype(m) == Point3f + + @test !hasproperty(m, :normal) + @test normals(m) === nothing + + @test hasproperty(m, :uv) + @test texturecoordinates(m) isa Vector{Vec3f} + @test length(texturecoordinates(m)) == 8 + + @test faces(m) isa Vector{GLTriangleFace} + @test length(faces(m)) == 12 + @test GeometryBasics.facetype(m) == GLTriangleFace + end + + @testset "triangle_mesh()" begin + m = triangle_mesh(r, pointtype = Point3f) + + @test hasproperty(m, :position) + @test coordinates(m) isa Vector{Point3f} + @test length(coordinates(m)) == 8 + @test GeometryBasics.pointtype(m) == Point3f + + @test !hasproperty(m, :normal) + @test normals(m) === nothing + + @test !hasproperty(m, :uv) + @test texturecoordinates(m) === nothing + + @test faces(m) isa Vector{GLTriangleFace} + @test length(faces(m)) == 12 + @test GeometryBasics.facetype(m) == GLTriangleFace + end + + @testset "mesh(mesh)" begin + m = GeometryBasics.mesh(r, pointtype = Point3f, normal = normals(r), facetype = QuadFace{Int64}) + + # Should be hit by normal_mesh as well... + @test coordinates(m) isa Vector{Point3f} + @test length(coordinates(m)) == 8 + @test normals(m) isa GeometryBasics.FaceView{Vec3f, Vector{Vec3f}, Vector{QuadFace{Int64}}} + @test length(normals(m)) == 6 + @test !hasproperty(m, :uv) + @test texturecoordinates(m) === nothing + @test faces(m) isa Vector{QuadFace{Int64}} + @test length(faces(m)) == 6 + + # Shoudl throw because uv's don't match length(position) or have faces + @test_throws ErrorException GeometryBasics.mesh(m, uv = Vec3f[]) + + # remap vertex attributes to merge faceviews into one face array + m2 = GeometryBasics.clear_faceviews(m) + + @test coordinates(m2) isa Vector{Point3f} + @test length(coordinates(m2)) == 24 + @test normals(m2) isa Vector{Vec3f} + @test length(normals(m2)) == 24 + @test !hasproperty(m2, :uv) + @test texturecoordinates(m2) === nothing + @test faces(m2) isa Vector{QuadFace{Int64}} + @test length(faces(m2)) == 6 + + # convert face type and add uvs + m2 = GeometryBasics.mesh(m2, facetype = GLTriangleFace, uv = decompose(Point3f, m2)) + + @test coordinates(m2) isa Vector{Point3f} + @test length(coordinates(m2)) == 24 + @test normals(m2) isa Vector{Vec3f} + @test length(normals(m2)) == 24 + @test texturecoordinates(m2) isa Vector{Point3f} + @test length(texturecoordinates(m2)) == 24 + @test faces(m2) isa Vector{GLTriangleFace} + @test length(faces(m2)) == 12 + + end +end \ No newline at end of file diff --git a/test/polygons.jl b/test/polygons.jl index 898a6f30..a9f1820e 100644 --- a/test/polygons.jl +++ b/test/polygons.jl @@ -1,28 +1,31 @@ @testset "Polygon" begin @testset "from points" begin - points = connect([1, 2, 3, 4, 5, 6], PointPoint2f(2)) + points = connect([1, 2, 3, 4, 5, 6], Point2f) polygon = Polygon(points) @test polygon == Polygon(points) end -end - -rect = Rect2f(0, 0, 1, 1) -hole = Circle(Point2f(0.5), 0.2) -poly2 = Polygon(decompose(Point2f, rect), [decompose(Point2f, hole)]) -poly1 = Polygon(rect, [hole]) -@test poly1 == poly2 -@test poly.exterior == decompose(Point2f, rect) -@test poly.interiors == [decompose(Point2f, hole)] + rect = Rect2f(0, 0, 1, 1) + hole = Tesselation(Circle(Point2f(0.5), 0.2), 8) + poly2 = Polygon(decompose(Point2f, rect), [decompose(Point2f, hole)]) + poly1 = Polygon(rect, [hole]) + @test poly1 == poly2 + @test poly1.exterior == decompose(Point2f, rect) + @test poly1.interiors == [decompose(Point2f, hole)] -faces(poly1) + # triangulation is inconsistent... + @test length(faces(poly1)) == 11 + ps = vcat(decompose(Point2f, rect), decompose(Point2f, hole)) + @test coordinates(poly1) == ps -GeometryBasics.earcut_triangulate([poly.exterior[[1, 2, 3, 4, 1]]]) + fs = GeometryBasics.earcut_triangulate([poly1.exterior[[1, 2, 3, 4, 1]]]) + @test fs == GLTriangleFace[(4,1,2), (2,3,4)] -poly = [ - [Point2f(100, 0), Point2f(100, 100), Point2f(0, 100), Point2f(0, 0)], - # Following polylines define holes. - [Point2f(75, 25), Point2f(75, 75), Point2f(25, 75), Point2f(25, 25)] -] - -GeometryBasics.earcut_triangulate(poly) + poly1 = [ + [Point2f(100, 0), Point2f(100, 100), Point2f(0, 100), Point2f(0, 0)], + # Following polylines define holes. + [Point2f(75, 25), Point2f(75, 75), Point2f(25, 75), Point2f(25, 25)] + ] + fs = GLTriangleFace[(4, 8, 7), (5, 8, 4), (3, 4, 7), (5, 4, 1), (2, 3, 7), (6, 5, 1), (2, 7, 6), (6, 1, 2)] + @test fs == GeometryBasics.earcut_triangulate(poly1) +end diff --git a/test/runtests.jl b/test/runtests.jl index 7cc3fea0..2c0170f9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,7 +1,7 @@ using Test, Random, OffsetArrays using GeometryBasics using LinearAlgebra -using GeometryBasics: MetaMesh, add_meta, pop_meta +using GeometryBasics: MetaMesh using GeoInterface using GeoJSON using Extents @@ -36,23 +36,23 @@ end tfaces = TetrahedronFace{Int}[(1, 2, 3, 4), (5, 6, 7, 8)] normals = rand(Vec{3, Float64}, 8) stress = LinRange(0, 1, 8) - mesh = MetaMesh(points, tfaces; normals = normals, stress = stress) + mesh = Mesh(points, tfaces; normal = normals, stress = stress) @test hasproperty(mesh, :stress) - @test hasproperty(mesh, :normals) + @test hasproperty(mesh, :normal) @test mesh.stress === stress - @test mesh.normals === normals - @test mesh.normals === normals + @test mesh.normal === normals + @test mesh.position === points @test GeometryBasics.faces(mesh) === tfaces - @test propertynames(mesh) == (:normals, :stress) + @test propertynames(mesh) == (:vertex_attributes, :faces, :views, :position, :normal, :stress) end end @testset "Mesh with metadata" begin m = triangle_mesh(Sphere(Point3f(0), 1)) m_meta = MetaMesh(m; boundingbox=Rect(1.0, 1.0, 2.0, 2.0)) - @test m_meta.boundingbox === Rect(1.0, 1.0, 2.0, 2.0) - @test propertynames(m_meta) == (:boundingbox,) + @test m_meta[:boundingbox] === Rect(1.0, 1.0, 2.0, 2.0) + @test collect(keys(m_meta)) == [:boundingbox,] end end @@ -130,14 +130,17 @@ end f = connect([1, 2, 3, 4], SimplexFace{4}) mesh = Mesh(points, f) @test collect(mesh) == [Tetrahedron(points...)] - + @test faces(mesh) == [TetrahedronFace{Int64}(1,2,3,4)] + @test decompose(LineFace{Int64}, mesh) == LineFace{Int64}[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)] + @test decompose(GLTriangleFace, mesh) == GLTriangleFace[(2, 3, 4), (1, 3, 4), (1, 2, 4), (1, 2, 3)] + points = rand(Point3f, 8) tfaces = [GLTriangleFace(1, 2, 3), GLTriangleFace(5, 6, 7)] ns = rand(Vec3f, 8) uv = rand(Vec2f, 8) mesh = Mesh(points, tfaces) meshuv = MetaMesh(points, tfaces; uv=uv) - meshuvnormal = MetaMesh(points, tfaces; normals=ns, uv=uv) + meshuvnormal = MetaMesh(points, tfaces; normal=ns, uv=uv) t = Tesselation(Rect2f(0, 0, 2, 2), (30, 30)) m = GeometryBasics.mesh(t; pointtype=Point3f, facetype=QuadFace{Int}) @@ -173,7 +176,7 @@ end @test GeometryBasics.normals(m_normal) isa Vector{Vec3f} primitive = Rect3(0, 0, 0, 1, 1, 1) m_normal = normal_mesh(primitive) - @test GeometryBasics.normals(m_normal) isa Vector{Vec3f} + @test GeometryBasics.normals(m_normal) isa GeometryBasics.FaceView{Vec3f, Vector{Vec3f}, Vector{GLTriangleFace}} points = decompose(Point2f, Circle(Point2f(0), 1)) tmesh = triangle_mesh(points) @@ -186,61 +189,29 @@ end @test texturecoordinates(m) == nothing r2 = Rect2(0.0, 0.0, 1.0, 1.0) - @test collect(texturecoordinates(r2)) == [(0.0, 1.0), (1.0, 1.0), (0.0, 0.0), (1.0, 0.0)] + @test collect(texturecoordinates(r2)) == Point2f[(0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)] r3 = Rect3(0.0, 0.0, 1.0, 1.0, 2.0, 2.0) @test first(texturecoordinates(r3)) == Vec3(0, 0, 0) uv = decompose_uv(m) - @test Rect(Point.(uv)) == Rect(0, 0, 1, 1) + @test_broken false # Rect(Point.(uv)) == Rect(0, 0, 1, 1) # decompose_uv must now produces 2D uvs + uvw = GeometryBasics.decompose_uvw(m) + @test Rect(Point.(uvw)) == Rect(Point3f(0), Vec3f(1)) points = decompose(Point2f, Circle(Point2f(0), 1)) m = GeometryBasics.mesh(points) @test coordinates(m) === points + + fs = [QuadFace(1,2,3,4), QuadFace(3,4,5,6), QuadFace(7,8,9,10)] + views = [1:2, 3:3] + new_fs, new_views = decompose(GLTriangleFace, fs, views) + @test new_fs == GLTriangleFace[(1, 2, 3), (1, 3, 4), (3, 4, 5), (3, 5, 6), (7, 8, 9), (7, 9, 10)] + @test new_views == [1:4, 5:6] end @testset "convert mesh + meta" begin m = uv_normal_mesh(Circle(Point2f(0), 1f0)) # For 2d primitives normal is just the upvector - m.normals == [Vec3f(0, 0, 1) for p in coordinates(m)] -end - -@testset "convert mesh + meta" begin - m = uv_normal_mesh(Rect3f(Vec3f(-1), Vec3f(1, 2, 3))) - m_normal = add_meta(m; normals = decompose_normals(m)) - # make sure we don't loose the uv - @test hasproperty(m_normal, :uv) - # Make sure we don't create any copies - @test coordinates(m) === coordinates(m_normal) - @test m.normals == m_normal.normals - @test m.uv === m_normal.uv - - m = uv_mesh(Rect3f(Vec3f(-1), Vec3f(1, 2, 3))) - m_normal = add_meta(m, normals = decompose_normals(m)) - @test hasproperty(m_normal, :uv) - @test coordinates(m) === coordinates(m_normal) - @test decompose_normals(m) == GeometryBasics.normals(m_normal) - # uv stays untouched, since we don't specify the element type in normalmesh - @test m.uv === m_normal.uv -end - -@testset "modifying meta" begin - xx = rand(10) - points = rand(Point3f, 10) - m = MetaMesh(points, GLTriangleFace[(1,2,3), (3,4,5)]; xx=xx) - color = rand(10) - m = add_meta(m; color=color) - - @test hasproperty(m, :xx) - @test hasproperty(m, :color) - - @test m.xx === xx - @test m.color === color - - m, colpopt = GeometryBasics.pop_meta(m, :color) - m, xxpopt = GeometryBasics.pop_meta(m, :xx) - - @test propertynames(m) == () - @test colpopt === color - @test xxpopt === xx + @test m.normal == [Vec3f(0, 0, 1) for p in coordinates(m)] end @testset "mesh conversion" begin @@ -339,10 +310,19 @@ end include("geointerface.jl") end +include("polygons.jl") + using Aqua # Aqua tests # Intervals brings a bunch of ambiquities unfortunately # seems like we also run into https://github.com/JuliaTesting/Aqua.jl/issues/86 -Aqua.test_all(GeometryBasics; ambiguities=false, unbound_args=false) +# Aqua.test_ambiguities([GeometryBasics, Base, Core]) +# Aqua.test_unbound_args(GeometryBasics) +Aqua.test_undefined_exports(GeometryBasics) +Aqua.test_project_extras(GeometryBasics) +Aqua.test_stale_deps(GeometryBasics, ignore = [:PrecompileTools]) +Aqua.test_deps_compat(GeometryBasics) +Aqua.test_piracies(GeometryBasics) +Aqua.test_persistent_tasks(GeometryBasics) end # testset "GeometryBasics"