diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bf35910..4603e5fc 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 }} @@ -29,16 +30,7 @@ jobs: with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v1 - env: - cache-name: cache-artifacts - with: - path: ~/.julia/artifacts - key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} - restore-keys: | - ${{ runner.os }}-test-${{ env.cache-name }}- - ${{ runner.os }}-test- - ${{ runner.os }}- + - uses: julia-actions/cache@v2 - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 @@ -47,20 +39,23 @@ jobs: file: lcov.info docs: name: Documentation - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 env: JULIA_PKG_SERVER: "" steps: - uses: actions/checkout@v2 + - run: sudo apt-get update && sudo apt-get install -y xorg-dev mesa-utils xvfb libgl1 freeglut3-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev xsettingsd x11-xserver-utils - uses: julia-actions/setup-julia@v1 with: - version: "1.7" + version: "1.11" + - uses: julia-actions/cache@v2 - run: | - julia --project=docs -e ' + DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=docs -e ' using Pkg Pkg.develop(PackageSpec(path=pwd())) + pkg"add MeshIO#ff/GeometryBasics_refactor MakieCore#breaking-0.22 Makie#breaking-0.22 GLMakie#breaking-0.22" Pkg.instantiate()' - - run: julia --project=docs docs/make.jl + - run: DISPLAY=:0 xvfb-run -s '-screen 0 1024x768x24' julia --project=docs docs/make.jl env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} diff --git a/Project.toml b/Project.toml index 25c03ef3..37374abc 100644 --- a/Project.toml +++ b/Project.toml @@ -9,9 +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" -StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" -Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c" [compat] Aqua = "0.8" @@ -22,10 +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.12, 1.0" -StructArrays = "0.6" -Tables = "0.2, 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..51e11a0b 100644 --- a/docs/src/meshes.md +++ b/docs/src/meshes.md @@ -1,24 +1,73 @@ # 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 + + +```@docs; canonical=false +FaceView +``` + +The purpose of FaceView is to allow you to add data that doesn't use the same vertex indices as `mesh.faces` +As a minimal example consider a mesh that is just one triangle, i.e. 3 position and one triangle face `TriangleFace(1,2,3)`. +Let's say we want to add a flat color to the triangle. +In this case we only have one color, but our face refers to 3 different vertices (3 different positions). +To avoid duplicating the color data, we can instead define a new triangle face `TriangleFace(1)` and add the color attribute as a `FaceView([color], [TriangleFace(1)])`. +If we ever need the mesh to be defined with just one common set of faces, i.e. no FaceView and appropriately duplicated vertex data, we can use `expand_faceviews(mesh)` to generate it. + +On a larger scale this can be useful for memory and performance reason, e.g. when you do calculations with vertex attributes. +It can also simplify some definitions, like for example `Rect3`. +In that case we have 8 positions and 6 normals with FaceViews, or 24 without (assuming per-face normals). + + +## MetaMesh + +```julia; canonical=false +MetaMesh +``` ## How to create a mesh +### GeometryBasics + +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 + +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. + +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. + ### Meshing.jl ### MeshIO.jl The [`MeshIO.jl`](https://github.com/JuliaIO/MeshIO.jl) package provides load/save support for several file formats which store meshes. -## How to access data - -The following functions can be called on an [`AbstractMesh`](@ref) to access its underlying data. - -* [`faces`](@ref) -* [`coordinates`](@ref) -* `texturecoordinates` -* [`normals`](@ref) +```@example +using GLMakie, GLMakie.FileIO, GeometryBasics +m = load(GLMakie.assetpath("cat.obj")) +GLMakie.mesh(m; color=load(GLMakie.assetpath("diffusemap.png")), axis=(; show_axis=false)) +``` 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 0e84e5c6..a430d861 100644 --- a/src/GeometryBasics.jl +++ b/src/GeometryBasics.jl @@ -1,9 +1,10 @@ module GeometryBasics -using StaticArrays, Tables, StructArrays, IterTools, LinearAlgebra +using IterTools, LinearAlgebra, StaticArrays using GeoInterface import Extents using EarCut_jll +import Base: * using Base: @propagate_inbounds @@ -18,7 +19,6 @@ include("primitives/pyramids.jl") include("primitives/particles.jl") include("interfaces.jl") -include("metadata.jl") include("viewtypes.jl") include("geometry_primitives.jl") include("meshes.jl") @@ -26,38 +26,30 @@ include("triangulation.jl") include("lines.jl") include("boundingboxes.jl") -include("deprecated.jl") include("geointerface.jl") export AbstractGeometry, GeometryPrimitive export Mat, Point, Vec export LineFace, Polytope, Line, NgonFace, convert_simplex -export LineString, AbstractPolygon, Polygon, MultiPoint, MultiLineString, MultiPolygon +export LineString, MultiLineString, MultiPoint +export AbstractPolygon, Polygon, MultiPolygon export Simplex, connect, Triangle, NSimplex, Tetrahedron -export QuadFace, metafree, coordinates, TetrahedronFace -export TupleView, SimplexFace, Mesh, meta -export Triangle, TriangleP +export QuadFace, coordinates, TetrahedronFace +export TupleView, SimplexFace +export Triangle export AbstractFace, TriangleFace, QuadFace, GLTriangleFace export OffsetInteger, ZeroIndex, OneIndex, GLIndex -export FaceView, SimpleFaceView -export AbstractPoint, PointMeta, PointWithUV -export PolygonMeta, MultiPointMeta, MultiLineStringMeta, MeshMeta, LineStringMeta, - MultiPolygonMeta export decompose, coordinates, faces, normals, decompose_uv, decompose_normals, - texturecoordinates -export Tesselation, pointmeta, Normal, UV, UVW -export GLTriangleFace, GLUVMesh3D -export AbstractMesh, Mesh, TriangleMesh -export GLNormalMesh2D -export MetaT, meta_table + texturecoordinates, vertex_attributes +export expand_faceviews +export face_normals +export Tesselation, Normal, UV, UVW +export AbstractMesh, Mesh, MetaMesh, FaceView + # all the different predefined mesh types # Note: meshes can contain arbitrary meta information, -export AbstractMesh, TriangleMesh, PlainMesh, GLPlainMesh, GLPlainMesh2D, GLPlainMesh3D -export UVMesh, GLUVMesh, GLUVMesh2D, GLUVMesh3D -export NormalMesh, GLNormalMesh, GLNormalMesh2D, GLNormalMesh3D -export NormalUVMesh, GLNormalUVMesh, GLNormalUVMesh2D, GLNormalUVMesh3D -export NormalUVWMesh, GLNormalUVWMesh, GLNormalUVWMesh2D, GLNormalUVWMesh3D +export AbstractMesh # mesh creation functions export triangle_mesh, triangle_mesh, uv_mesh @@ -65,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 @@ -73,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" - include("precompile.jl") - _precompile_() +if Base.VERSION >= v"1.8" + include("precompiles.jl") end end # module diff --git a/src/basic_types.jl b/src/basic_types.jl index dc73dd29..49c21ad1 100644 --- a/src/basic_types.jl +++ b/src/basic_types.jl @@ -1,44 +1,131 @@ """ -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 -abstract type AbstractPoint{Dim,T} <: StaticVector{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 -abstract type AbstractSimplex{Dim,N,T} <: StaticVector{Dim,T} end +abstract type AbstractSimplex{Dim,T} <: Polytope{Dim,T} end -""" -Face index, connecting points to form a simplex -""" +@propagate_inbounds function Base.getindex(points::AbstractVector{P}, face::F) where {P<: Point, F <: AbstractFace} + return Polytope(P, F)(map(i-> points[i], face.data)) +end + +@propagate_inbounds function Base.getindex(elements::AbstractVector, face::F) where {F <: AbstractFace} + return map(i-> elements[i], face.data) +end @fixed_vector SimplexFace = AbstractSimplexFace + const TetrahedronFace{T} = SimplexFace{4,T} Face(::Type{<:SimplexFace{N}}, ::Type{T}) where {N,T} = SimplexFace{N,T} + +@fixed_vector NgonFace = AbstractNgonFace + """ -Face index, connecting points to form an Ngon + 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 -@fixed_vector NgonFace = AbstractNgonFace 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::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) -function Base.show(io::IO, x::TriangleFace{T}) where {T} - return print(io, "TriangleFace(", join(x, ", "), ")") +# 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} @@ -49,23 +136,25 @@ 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,Point<:AbstractPoint{Dim,T}} <: AbstractPolygon{Dim,T} - points::SVector{N,Point} +struct Ngon{Dim, T<:Real, N} <: AbstractPolygon{Dim,T} + points::NTuple{N, Point{Dim, T}} end -const NNgon{N} = Ngon{Dim,T,N,P} where {Dim,T,P} +const NNgon{N} = Ngon{Dim,T,N} where {Dim,T} -function (::Type{<:NNgon{N1}})(p0::P, points::Vararg{P,N2}) where {P<:AbstractPoint{Dim,T}, - N1, N2} where {Dim,T} - @assert N1 == N2+1 - return Ngon{Dim,T,N1,P}(SVector(p0, points...)) +function (::Type{<: NNgon{N}})(points::Vararg{Point{Dim,T}, N}) where {N,Dim,T} + return Ngon{Dim,T,N}(points) end Base.show(io::IO, x::NNgon{N}) where {N} = print(io, "Ngon{$N}(", join(x, ", "), ")") @@ -76,51 +165,55 @@ 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(P::Type{<:AbstractPoint{Dim,T}}, +function Polytope(::Type{Point{Dim,T}}, ::Type{<:AbstractNgonFace{N,IT}}) where {N,Dim,T,IT} - return Ngon{Dim,T,N,P} + return Ngon{Dim,T,N} end """ + Polytope(::Type{<: Ngon}, P::Type{<: Point}) + The fully concrete Ngon type, when constructed from a point type! """ -function Polytope(::Type{<:NNgon{N}}, P::Type{<:AbstractPoint{NDim,T}}) where {N,NDim,T} - return Ngon{NDim,T,N,P} +function Polytope(::Type{<: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 -const LineP{Dim,T,P<:AbstractPoint{Dim,T}} = Ngon{Dim,T,2,P} -const Line{Dim,T} = LineP{Dim,T,Point{Dim,T}} +const Line{Dim,T} = Ngon{Dim,T,2} # Simplex{D, T, 3} & Ngon{D, T, 3} are both representing a triangle. # Since Ngon is supposed to be flat and a triangle is flat, lets prefer Ngon # for triangle: -const TriangleP{Dim,T,P<:AbstractPoint{Dim,T}} = Ngon{Dim,T,3,P} -const Triangle{Dim,T} = TriangleP{Dim,T,Point{Dim,T}} +const Triangle{Dim,T} = Ngon{Dim,T,3} const Triangle3d{T} = Triangle{3,T} +const GLTriangleElement = Triangle{3,Float32} -Base.show(io::IO, x::TriangleP) = print(io, "Triangle(", join(x, ", "), ")") -Base.summary(io::IO, ::Type{<:TriangleP}) = print(io, "Triangle") +faces(x::Ngon{Dim, T, N}) where {Dim, T, N} = [NgonFace{N, Int}(ntuple(identity, N))] -const Quadrilateral{Dim,T} = Ngon{Dim,T,4,P} where {P<:AbstractPoint{Dim,T}} +Base.show(io::IO, x::Triangle) = print(io, "Triangle(", join(x, ", "), ")") -Base.show(io::IO, x::Quadrilateral) = print(io, "Quad(", join(x, ", "), ")") -Base.summary(io::IO, ::Type{<:Quadrilateral}) = print(io, "Quad") +const Quadrilateral{Dim,T} = Ngon{Dim,T,4} -function coordinates(lines::AbstractArray{LineP{Dim,T,PointType}}) where {Dim,T,PointType} - return if lines isa Base.ReinterpretArray - return coordinates(lines.parent) - else - result = PointType[] - for line in lines - append!(result, coordinates(line)) - end - return result +Base.show(io::IO, x::Quadrilateral) = print(io, "Quadrilateral(", join(x, ", "), ")") + +function coordinates(lines::AbstractArray{Line{Dim,T}}) where {Dim,T} + result = Point{Dim, T}[] + for line in lines + append!(result, coordinates(line)) end + return result 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. @@ -136,22 +229,19 @@ This is for a simpler implementation. It applies to infinite dimensions. The structure of this type is designed to allow embedding in higher-order spaces by parameterizing on `T`. """ -struct Simplex{Dim,T<:Real,N,Point<:AbstractPoint{Dim,T}} <: Polytope{Dim,T} - points::SVector{N,Point} +struct Simplex{Dim,T<:Real,N} <: AbstractSimplex{Dim,T} + points::NTuple{N,Point{Dim,T}} end -const NSimplex{N} = Simplex{Dim,T,N,P} where {Dim,T,P} -const TetrahedronP{T,P<:AbstractPoint{3,T}} = Simplex{3,T,4,P} -const Tetrahedron{T} = TetrahedronP{T,Point{3,T}} +const NSimplex{N} = Simplex{Dim,T,N} where {Dim,T} +const Tetrahedron{T} = Simplex{3,T,4} -Base.show(io::IO, x::TetrahedronP) = print(io, "Tetrahedron(", join(x, ", "), ")") +Base.show(io::IO, x::Tetrahedron) = print(io, "Tetrahedron(", join(x, ", "), ")") coordinates(x::Simplex) = x.points -function (::Type{<:NSimplex{N1}})(p0::P, points::Vararg{P,N2}) where {P<:AbstractPoint{Dim,T}, - N1, N2} where {Dim,T} - @assert N1 == N2+1 - return Simplex{Dim,T,N1,P}(SVector(p0, points...)) +function (::Type{<:NSimplex{N}})(points::Vararg{Point{Dim,T},N}) where {Dim,T,N} + return Simplex{Dim,T,N}(points) end # Base Array interface @@ -159,151 +249,72 @@ Base.length(::Type{<:NSimplex{N}}) where {N} = N Base.length(::NSimplex{N}) where {N} = N """ -The Simplex Polytope element type when indexing an array of points with a SimplexFace -""" -function Polytope(P::Type{<:AbstractPoint{Dim,T}}, - ::Type{<:AbstractSimplexFace{N}}) where {N,Dim,T} - return Simplex{Dim,T,N,P} -end + Polytope(::Type{Point{Dim,T}}, ::Type{<:AbstractSimplexFace{N}}) +The Simplex Polytope element type when indexing an array of points with a SimplexFace """ -The fully concrete Simplex type, when constructed from a point type! -""" -function Polytope(::Type{<:NSimplex{N}}, P::Type{<:AbstractPoint{NDim,T}}) where {N,NDim,T} - return Simplex{NDim,T,N,P} +function Polytope(::Type{Point{Dim,T}}, ::Type{<:AbstractSimplexFace{N}}) where {N,Dim,T} + return Simplex{Dim,T,N} end -Base.show(io::IO, x::LineP) = print(io, "Line(", x[1], " => ", x[2], ")") - -""" - LineString(points::AbstractVector{<:AbstractPoint}) -A LineString is a geometry of connected line segments """ -struct LineString{Dim,T<:Real,P<:AbstractPoint,V<:AbstractVector{<:LineP{Dim,T,P}}} <: - AbstractVector{LineP{Dim,T,P}} - points::V -end - -coordinates(x::LineString) = coordinates(x.points) - -Base.copy(x::LineString) = LineString(copy(x.points)) -Base.size(x::LineString) = size(getfield(x, :points)) -Base.getindex(x::LineString, i) = getindex(getfield(x, :points), i) - -function LineString(points::AbstractVector{LineP{Dim,T,P}}) where {Dim,T,P} - return LineString{Dim,T,P,typeof(points)}(points) -end + Polytope(::Type{<:NSimplex{N}}, P::Type{Point{NDim,T}}) +The fully concrete Simplex type, when constructed from a point type! """ - LineString(points::AbstractVector{<: AbstractPoint}, skip = 1) - -Creates a LineString from a vector of points. -With `skip == 1`, the default, it will connect the line like this: -```julia -points = Point[a, b, c, d] -linestring = LineString(points) -@assert linestring == LineString([a => b, b => c, c => d]) -``` -""" -function LineString(points::AbstractVector{<:AbstractPoint}, skip=1) - return LineString(connect(points, LineP, skip)) +function Polytope(::Type{<:NSimplex{N}}, P::Type{Point{NDim,T}}) where {N,NDim,T} + return Simplex{NDim,T,N} end +Base.show(io::IO, x::Line) = print(io, "Line(", x[1], " => ", x[2], ")") -function LineString(points::AbstractVector{<:Pair{P,P}}) where {P<:AbstractPoint{N,T}} where {N, - T} - return LineString(reinterpret(LineP{N,T,P}, points)) -end - -function LineString(points::AbstractVector{<:AbstractPoint}, - faces::AbstractVector{<:LineFace}) - return LineString(connect(points, faces)) -end - -""" - LineString(points::AbstractVector{<: AbstractPoint}, indices::AbstractVector{<: Integer}, skip = 1) - -Creates a LineString from a vector of points and an index list. -With `skip == 1`, the default, it will connect the line like this: - - -```julia -points = Point[a, b, c, d]; faces = [1, 2, 3, 4] -linestring = LineString(points, faces) -@assert linestring == LineString([a => b, b => c, c => d]) -``` -To make a segmented line, set skip to 2 -```julia -points = Point[a, b, c, d]; faces = [1, 2, 3, 4] -linestring = LineString(points, faces, 2) -@assert linestring == LineString([a => b, c => d]) -``` -""" -function LineString(points::AbstractVector{<:AbstractPoint}, - indices::AbstractVector{<:Integer}, skip=1) - faces = connect(indices, LineFace, skip) - return LineString(points, faces) -end """ Polygon(exterior::AbstractVector{<:Point}) - Polygon(exterior::AbstractVector{<:Point}, interiors::Vector{<:AbstractVector{<:AbstractPoint}}) + 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,P<:AbstractPoint{Dim,T},L<:AbstractVector{<:LineP{Dim,T,P}}, - V<:AbstractVector{L}} <: AbstractPolygon{Dim,T} - exterior::L - interiors::V +struct Polygon{Dim,T<:Real} <: AbstractPolygon{Dim,T} + exterior::Vector{Point{Dim, T}} + interiors::Vector{Vector{Point{Dim, T}}} end -Base.copy(x::Polygon) = Polygon(copy(x.exterior), copy(x.interiors)) +Base.copy(x::Polygon) = Polygon(copy(x.exterior), deepcopy(x.interiors)) function Base.:(==)(a::Polygon, b::Polygon) return (a.exterior == b.exterior) && (a.interiors == b.interiors) end -function Polygon(exterior::E, - interiors::AbstractVector{E}) where {E<:AbstractVector{LineP{Dim,T,P}}} where {Dim, - T, - P} - return Polygon{Dim,T,P,typeof(exterior),typeof(interiors)}(exterior, interiors) -end - -Polygon(exterior::L) where {L<:AbstractVector{<:LineP}} = Polygon(exterior, L[]) - -function Polygon(exterior::AbstractVector{P}, - skip::Int=1) where {P<:AbstractPoint{Dim,T}} where {Dim,T} - return Polygon(LineString(exterior, skip)) +function Polygon( + exterior::AbstractVector{Point{Dim,T}}, + interiors::AbstractVector{AbstractVector{Point{Dim,T}}}) where {Dim, T} + tov(v) = convert(Vector{Point{Dim, T}}, v) + return Polygon{Dim,T}(tov(exterior), map(tov, interiors)) end -function Polygon(exterior::AbstractVector{P}, faces::AbstractVector{<:Integer}, - skip::Int=1) where {P<:AbstractPoint{Dim,T}} where {Dim,T} - return Polygon(LineString(exterior, faces, skip)) -end +Polygon(exterior::AbstractVector{Point{N, T}}) where {N, T} = Polygon(exterior, Vector{Point{N, T}}[]) -function Polygon(exterior::AbstractVector{P}, - faces::AbstractVector{<:LineFace}) where {P<:AbstractPoint{Dim,T}} where {Dim, - T} +function Polygon(exterior::AbstractVector{Point{Dim,T}}, + faces::AbstractVector{<:LineFace}) where {Dim, T} return Polygon(LineString(exterior, faces)) end -function Polygon(exterior::AbstractVector{P}, - interior::AbstractVector{<:AbstractVector{P}}) where {P<:AbstractPoint{Dim, - T}} where {Dim, - T} - ext = LineString(exterior) - # We need to take extra care for empty interiors, since - # if we just map over it, it won't infer the element type correctly! - int = typeof(ext)[] - foreach(x -> push!(int, LineString(x)), interior) - return Polygon(ext, int) +function Polygon(exterior::AbstractGeometry{Dim, T}, interior::AbstractVector=[]) where {Dim, T} + to_p(v) = decompose(Point{Dim, T}, v) + int = Vector{Point{Dim, T}}[] + for i in interior + push!(int, to_p(i)) + end + return Polygon(to_p(exterior), int) end -function coordinates(polygon::Polygon{N,T,PointType}) where {N,T,PointType} +function coordinates(polygon::Polygon{N,T}) where {N,T} exterior = coordinates(polygon.exterior) if isempty(polygon.interiors) return exterior else - result = PointType[] + result = Point{N, T}[] append!(result, exterior) foreach(x -> append!(result, coordinates(x)), polygon.interiors) return result @@ -312,109 +323,449 @@ end """ MultiPolygon(polygons::AbstractPolygon) + +A collection of polygons """ -struct MultiPolygon{Dim,T<:Real,Element<:AbstractPolygon{Dim,T}, - A<:AbstractVector{Element}} <: AbstractVector{Element} - polygons::A +struct MultiPolygon{Dim, T<:Real} <: AbstractGeometry{Dim, T} + polygons::Vector{<:AbstractPolygon{Dim,T}} end -function MultiPolygon(polygons::AbstractVector{P}; - kw...) where {P<:AbstractPolygon{Dim,T}} where {Dim,T} - return MultiPolygon(meta(polygons; kw...)) +function MultiPolygon(polygons::AbstractVector{<:AbstractPolygon{Dim,T}}) where {Dim,T} + return MultiPolygon(convert(Vector{eltype(polygons)}, polygons)) end Base.getindex(mp::MultiPolygon, i) = mp.polygons[i] Base.size(mp::MultiPolygon) = size(mp.polygons) +Base.length(mp::MultiPolygon) = length(mp.polygons) -struct MultiLineString{Dim,T<:Real,Element<:LineString{Dim,T},A<:AbstractVector{Element}} <: - AbstractVector{Element} - linestrings::A +""" + LineString(points::AbstractVector{<:Point}) + +A LineString is a collection of points connected by line segments. +""" +struct LineString{Dim, T<:Real} <: AbstractGeometry{Dim, T} + points::Vector{Point{Dim, T}} end +Base.length(ls::LineString) = length(coordinates(ls)) +coordinates(ls::LineString) = ls.points -function MultiLineString(linestrings::AbstractVector{L}; - kw...) where {L<:AbstractVector{LineP{Dim,T,P}}} where {Dim,T,P} - return MultiLineString(meta(linestrings; kw...)) +struct MultiLineString{Dim, T<:Real} <: AbstractGeometry{Dim, T} + linestrings::Vector{LineString{Dim, T}} +end + +function MultiLineString(linestrings::AbstractVector{L}) where {L<:LineString} + return MultiLineString(convert(Vector{L}, linestrings)) end Base.getindex(ms::MultiLineString, i) = ms.linestrings[i] Base.size(ms::MultiLineString) = size(ms.linestrings) +Base.length(mpt::MultiLineString) = length(mpt.linestrings) """ MultiPoint(points::AbstractVector{AbstractPoint}) A collection of points """ -struct MultiPoint{Dim,T<:Real,P<:AbstractPoint{Dim,T},A<:AbstractVector{P}} <: - AbstractVector{P} - points::A +struct MultiPoint{Dim,T<:Real} <: AbstractGeometry{Dim, T} + points::Vector{Point{Dim, T}} end -function MultiPoint(points::AbstractVector{P}; - kw...) where {P<:AbstractPoint{Dim,T}} where {Dim,T} - return MultiPoint(meta(points; kw...)) +function MultiPoint(points::AbstractVector{Point{Dim, T}}) where {Dim,T} + return MultiPoint(convert(Vector{Point{Dim, T}}, points)) end Base.getindex(mpt::MultiPoint, i) = mpt.points[i] Base.size(mpt::MultiPoint) = size(mpt.points) +Base.length(mpt::MultiPoint) = length(mpt.points) + """ - AbstractMesh + FaceView(data, faces) + +A FaceView is an alternative to passing a vertex attribute directly to a mesh. +It bundles `data` with a new set of `faces` which may index that data differently +from the faces defined in a mesh. This can be useful to avoid duplication in `data`. -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 +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) +) +``` + +If you need a mesh with strictly per-vertex data, e.g. for rendering, you can use +`expand_faceviews(mesh)` to convert every vertex attribute to be per-vertex. This +will duplicate data and reorder faces as needed. + +You can get the data of a FaceView with `values(faceview)` and the faces with +`faces(faceview)`. """ -abstract type AbstractMesh{Element<:Polytope} <: AbstractVector{Element} end +struct FaceView{T, AVT <: AbstractVector{T}, FVT <: AbstractVector{<: AbstractFace}} + data::AVT + faces::FVT +end + +const VertexAttributeType{T} = Union{FaceView{T}, AbstractVector{T}} + +function Base.vcat(a::FaceView, b::FaceView) + N = length(a.data) + return FaceView( + vcat(a.data, b.data), + vcat(a.faces, map(f -> 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) and the coordinates of the elements are +returned by coordinates(mesh). """ - Mesh <: AbstractVector{Element} -The concrete AbstractMesh implementation. +abstract type AbstractMesh{Dim, T} <: AbstractGeometry{Dim, T} end + """ -struct Mesh{Dim,T<:Number,Element<:Polytope{Dim,T},V<:AbstractVector{Element}} <: - AbstractMesh{Element} - simplices::V # usually a FaceView, to connect a set of points via a set of faces. + 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 +``` -Tables.schema(mesh::Mesh) = Tables.schema(getfield(mesh, :simplices)) +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. -function Base.getproperty(mesh::Mesh, name::Symbol) - if name === :position - # a mesh always has position defined by coordinates... - return metafree(coordinates(mesh)) - else - return getproperty(getfield(mesh, :simplices), name) +```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), +[`expand_faceviews`](@ref) + +The `faces` field is a collection `<: AbstractVector{FaceType}` containing faces +that describe how vertices are connected. Typically these are `(GL)TriangleFace`s +or `QuadFace`s, but they can be any collection of vertex indices `<: AbstractFace`. + +See also: [`faces`](@ref), [`decompose`](@ref) + +The `views` field can be used to separate the mesh into mutliple submeshes. Each +submesh is described by a "view" into the `faces` vector, i.e. submesh n uses +`mesh.faces[mesh.views[n]]`. A `Mesh` can be constructed without `views`, which +results in an empty `views` vector. + +See also: [`merge`](@ref), [`split_mesh`](@ref) +""" +struct Mesh{ + Dim, T <: Real, + 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 -function Base.propertynames(mesh::Mesh) - names = propertynames(getfield(mesh, :simplices)) - if :position in names - return names +@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 - # a mesh always has positions! - return (names..., :position) + 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) -function Base.summary(io::IO, ::Mesh{Dim,T,Element}) where {Dim,T,Element} - print(io, "Mesh{$Dim, $T, ") - summary(io, Element) - return print(io, "}") +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 -Base.size(mesh::Mesh) = size(getfield(mesh, :simplices)) -Base.getindex(mesh::Mesh, i::Integer) = getfield(mesh, :simplices)[i] +function Base.iterate(mesh::Mesh, i=1) + return i - 1 < length(mesh) ? (mesh[i], i + 1) : nothing +end -function Mesh(elements::AbstractVector{<:Polytope{Dim,T}}) where {Dim,T} - return Mesh{Dim,T,eltype(elements),typeof(elements)}(elements) +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 -function Mesh(points::AbstractVector{<:AbstractPoint}, - faces::AbstractVector{<:AbstractFace}) - return Mesh(connect(points, faces)) +""" + 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{<:AbstractPoint}, faces::AbstractVector{<:Integer}, +function Mesh(points::AbstractVector{Point{Dim, T}}, + 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}, facetype=TriangleFace, skip=1) - return Mesh(connect(points, connect(faces, facetype, skip))) + return Mesh(points, connect(faces, facetype, skip)) +end + +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::Dict{Symbol, Any} +end + +""" + MetaMesh(mesh; metadata...) + MetaMesh(positions, faces; metadata...) + +Constructs a MetaMesh either from another `mesh` or by constructing another mesh +with the given `positions` and `faces`. Any keyword arguments given will be +stored in the `meta` field in `MetaMesh`. + +This struct is meant to be used for storage of non-vertex data. Any vertex +related data should be stored as a vertex attribute in `Mesh`. One example of such +data is material data, which is defined per view in `mesh.views`, i.e. per submesh. + +The metadata added to the MetaMesh can be manipulated with Dict-like operations +(getindex, setindex!, get, delete, keys, etc). Vertex attributes can be accessed +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(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 + +# 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) = 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 \ No newline at end of file diff --git a/src/boundingboxes.jl b/src/boundingboxes.jl index fa19d296..447a7228 100644 --- a/src/boundingboxes.jl +++ b/src/boundingboxes.jl @@ -3,15 +3,17 @@ 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<:AbstractPoint} +function Rect{N1,T1}(geometry::AbstractArray{PT}) where {N1,T1,PT<:Point} N2, T2 = length(PT), eltype(PT) @assert N1 >= N2 vmin = Point{N2,T2}(typemax(T2)) 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<:AbstractPoint 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/deprecated.jl b/src/deprecated.jl deleted file mode 100644 index 54336aa0..00000000 --- a/src/deprecated.jl +++ /dev/null @@ -1,30 +0,0 @@ -using Base: @deprecate_binding - -# Types ...f0 renamed to ...f -@deprecate_binding Vecf0 Vecf -@deprecate_binding Pointf0 Pointf -for i in 1:4 - for T in [:Point, :Vec] - oldname = Symbol("$T$(i)f0") - newname = Symbol("$T$(i)f") - @eval begin - @deprecate_binding $oldname $newname - end - end - oldname = Symbol("Mat$(i)f0") - newname = Symbol("Mat$(i)f") - @eval begin - @deprecate_binding $oldname $newname - end -end - -# Rect types -@deprecate_binding Rect2D Rect2 -@deprecate_binding Rect3D Rect3 -@deprecate_binding FRect Rectf -@deprecate_binding FRect2D Rect2f -@deprecate_binding FRect3D Rect3f -@deprecate_binding IRect Recti -@deprecate_binding IRect2D Rect2i -@deprecate_binding IRect3D Rect3i -@deprecate_binding TRect RectT diff --git a/src/fixed_arrays.jl b/src/fixed_arrays.jl index d51481a9..341b8ec2 100644 --- a/src/fixed_arrays.jl +++ b/src/fixed_arrays.jl @@ -85,8 +85,18 @@ macro fixed_vector(name_parent) end end - Base.@pure StaticArrays.Size(::Type{$(name){S,Any}}) where {S} = Size(S) - Base.@pure StaticArrays.Size(::Type{$(name){S,T}}) where {S,T} = Size(S) + @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] @@ -127,6 +137,7 @@ abstract type AbstractPoint{Dim,T} <: StaticVector{Dim,T} end 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{AbstractPoint,Vec}) = any(isnan, p) @@ -161,3 +172,60 @@ 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 63b5b71b..f806fc27 100644 --- a/src/geointerface.jl +++ b/src/geointerface.jl @@ -2,7 +2,8 @@ GeoInterface.isgeometry(::Type{<:AbstractGeometry}) = true GeoInterface.isgeometry(::Type{<:AbstractFace}) = true -GeoInterface.isgeometry(::Type{<:AbstractPoint}) = true + +GeoInterface.isgeometry(::Type{<:Point}) = true GeoInterface.isgeometry(::Type{<:AbstractMesh}) = true GeoInterface.isgeometry(::Type{<:AbstractPolygon}) = true GeoInterface.isgeometry(::Type{<:LineString}) = true @@ -41,7 +42,8 @@ GeoInterface.getcoord(::PointTrait, g::Point, i::Int) = g[i] GeoInterface.ngeom(::LineTrait, g::Line) = length(g) GeoInterface.getgeom(::LineTrait, g::Line, i::Int) = g[i] -GeoInterface.ngeom(::LineStringTrait, g::LineString) = length(g) + 1 # n line segments + 1 + +GeoInterface.ngeom(::LineStringTrait, g::LineString) = length(g) # n connected points GeoInterface.ncoord(::LineStringTrait, g::LineString{Dim}) where {Dim} = Dim function GeoInterface.getgeom(::LineStringTrait, g::LineString, i::Int) return GeometryBasics.coordinates(g)[i] @@ -50,8 +52,8 @@ end GeoInterface.ngeom(::PolygonTrait, g::Polygon) = length(g.interiors) + 1 # +1 for exterior function GeoInterface.getgeom(::PolygonTrait, g::Polygon, - i::Int)::typeof(g.exterior) - return i > 1 ? g.interiors[i - 1] : g.exterior + i::Int) + return i > 1 ? LineString(g.interiors[i - 1]) : LineString(g.exterior) end GeoInterface.ngeom(::MultiPointTrait, g::MultiPoint) = length(g) @@ -69,7 +71,7 @@ GeoInterface.ngeom(::MultiPolygonTrait, g::MultiPolygon) = length(g) GeoInterface.getgeom(::MultiPolygonTrait, g::MultiPolygon, i::Int) = g[i] function GeoInterface.ncoord(::AbstractGeometryTrait, - ::Simplex{Dim,T,N,P}) where {Dim,T,N,P} + ::Simplex{Dim,T,N}) where {Dim,T,N} return Dim end function GeoInterface.ncoord(::AbstractGeometryTrait, @@ -77,11 +79,11 @@ function GeoInterface.ncoord(::AbstractGeometryTrait, return Dim end function GeoInterface.ngeom(::AbstractGeometryTrait, - ::Simplex{Dim,T,N,P}) where {Dim,T,N,P} + ::Simplex{Dim,T,N}) where {Dim,T,N} return N end GeoInterface.ngeom(::PolygonTrait, ::Ngon) = 1 # can't have any holes -GeoInterface.getgeom(::PolygonTrait, g::Ngon, _) = LineString(g.points) +GeoInterface.getgeom(::PolygonTrait, g::Ngon, _) = LineString([g.points...]) function GeoInterface.ncoord(::PolyhedralSurfaceTrait, ::Mesh{Dim,T,E,V} where {Dim,T,E,V}) @@ -103,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) @@ -112,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 bb077324..7f60e481 100644 --- a/src/geometry_primitives.jl +++ b/src/geometry_primitives.jl @@ -8,7 +8,13 @@ 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 function convert_simplex(NFT::Type{NgonFace{N,T1}}, f::Union{NgonFace{N,T2}}) where {T1,T2,N} @@ -37,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) @@ -55,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 @@ -72,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} @@ -89,57 +125,92 @@ 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{<:AbstractPoint{3,T}}, faces::AbstractVector{F}; +function normals(vertices::AbstractVector{Point{3,T}}, faces::AbstractVector{F}; normaltype=Vec{3,T}) where {T,F<:NgonFace} return normals(vertices, faces, normaltype) end -function normals(vertices::AbstractVector{<:AbstractPoint{3,T}}, faces::AbstractVector{F}, - ::Type{N}) where {T,F<:NgonFace,N} - normals_result = zeros(N, length(vertices)) +function normals(vertices::AbstractVector{<:Point{3}}, faces::AbstractVector{<: NgonFace}, + ::Type{NormalType}) where {NormalType} + + normals_result = zeros(NormalType, length(vertices)) for face in faces - v = metafree.(vertices[face]) + v = vertices[face] # we can get away with two edges since faces are planar. - n = orthogonal_vector(v[1], v[2], v[3]) - for i in 1:length(F) + 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 + normals_result[fi] = normals_result[fi] .+ n end end 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 c9905fcb..dfbd30e3 100644 --- a/src/interfaces.jl +++ b/src/interfaces.jl @@ -1,23 +1,36 @@ """ 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{<:AbstractPoint}) +function coordinates(points::AbstractVector{<:Point}) return points 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,12 +72,15 @@ 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} +struct Tesselation{Dim,T,Primitive,NGrid} <: AbstractGeometry{Dim, T} primitive::Primitive nvertices::NTuple{NGrid,Int} end @@ -84,11 +111,6 @@ end # Dispatch type to make `decompose(UV{Vec2f}, primitive)` work # and to pass through tesselation information -# Types that can be converted to a mesh via the functions below -const Meshable{Dim,T} = Union{Tesselation{Dim,T},Mesh{Dim,T},AbstractPolygon{Dim,T}, - GeometryPrimitive{Dim,T}, - AbstractVector{<:AbstractPoint{Dim,T}}} - struct UV{T} end UV(::Type{T}) where {T} = UV{T}() UV() = UV(Vec2f) @@ -99,22 +121,71 @@ struct Normal{T} end Normal(::Type{T}) where {T} = Normal{T}() Normal() = Normal(Vec3f) -function decompose(::Type{F}, primitive) where {F<:AbstractFace} +""" + 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) - f === nothing && return nothing - return collect_with_eltype(F, f) + if isnothing(f) + if ndims(primitive) == 2 + # if 2d, we can fallback to Polygon triangulation + return decompose(F, Polygon(decompose(Point, primitive))) + else + return nothing + end + end + return 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<:AbstractPoint} - return collect_with_eltype(P, metafree(coordinates(primitive))) +function decompose(::Type{P}, primitive) where {P<:Point} + return collect_with_eltype(P, coordinates(primitive)) end -function decompose(::Type{Point}, primitive::Meshable{Dim,T}) where {Dim,T} - return collect_with_eltype(Point{Dim,T}, metafree(coordinates(primitive))) +function decompose(::Type{Point}, primitive::AbstractGeometry{Dim,T}) where {Dim,T} + return collect_with_eltype(Point{Dim,T}, coordinates(primitive)) end -function decompose(::Type{Point}, primitive::LineString{Dim,T}) where {Dim,T} - return collect_with_eltype(Point{Dim,T}, metafree(coordinates(primitive))) +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 function decompose(::Type{T}, primitive) where {T} @@ -125,10 +196,22 @@ decompose_uv(primitive) = decompose(UV(), primitive) decompose_uvw(primitive) = decompose(UVW(), primitive) decompose_normals(primitive) = decompose(Normal(), primitive) -function decompose(NT::Normal{T}, primitive) where {T} - n = normals(primitive) - if n === nothing - return collect_with_eltype(T, normals(coordinates(primitive), faces(primitive))) +decompose_uv(T, primitive) = decompose(UV(T), primitive) +decompose_uvw(T, primitive) = decompose(UVW(T), primitive) +decompose_normals(T, primitive) = decompose(Normal(T), primitive) + +function decompose(::Normal{T}, primitive) where {T} + _n = normals(primitive) + if isnothing(_n) + # For 3D primitives, we can calculate the normals from the vertices + faces + if ndims(primitive) == 3 + n = normals(decompose(Point, primitive), faces(primitive), T) + else + points = decompose(Point, primitive) + n = [T(0, 0, 1) for p in points] + end + else + n = _n end return collect_with_eltype(T, n) end @@ -141,25 +224,23 @@ function decompose(UVT::Union{UV{T},UVW{T}}, primitive) where {T} # If the primitive doesn't even have coordinates, we're out of options and return # nothing, indicating that texturecoordinates aren't implemented positions = decompose(Point, primitive) - positions === nothing && return nothing + isnothing(positions) && return nothing + # 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 return collect_with_eltype(T, uv) end -function decompose(UVT::Union{UV{T},UVW{T}}, - positions::AbstractVector{<:VecTypes}) where {T} +function decompose(::Union{UV{T},UVW{T}}, positions::AbstractVector{<:VecTypes}) where {T} N = length(T) positions_nd = decompose(Point{N,eltype(T)}, positions) bb = Rect(positions_nd) # Make sure we get this as points + mini, w = minimum(bb), widths(bb) return map(positions_nd) do p - return T((p .- minimum(bb)) ./ widths(bb)) + return T((p .- mini) ./ w) end end - -# Stay backward compatible: - -function decompose(::Type{T}, primitive::Meshable, nvertices) where {T} - return decompose(T, Tesselation(primitive, nvertices)) -end diff --git a/src/lines.jl b/src/lines.jl index 60a04b56..9ead98e4 100644 --- a/src/lines.jl +++ b/src/lines.jl @@ -60,11 +60,11 @@ function consecutive_pairs(arr) end """ - self_intersections(points::AbstractVector{AbstractPoint}) + self_intersections(points::AbstractVector{<:Point}) Finds all self intersections of polygon `points` """ -function self_intersections(points::AbstractVector{<:AbstractPoint}) +function self_intersections(points::AbstractVector{<:Point}) sections = similar(points, 0) intersections = Int[] @@ -90,12 +90,12 @@ function self_intersections(points::AbstractVector{<:AbstractPoint}) end """ - split_intersections(points::AbstractVector{AbstractPoint}) + split_intersections(points::AbstractVector{<: Point}) Splits polygon `points` into it's self intersecting parts. Only 1 intersection is handled right now. """ -function split_intersections(points::AbstractVector{<:AbstractPoint}) +function split_intersections(points::AbstractVector{<:Point}) intersections, sections = self_intersections(points) return if isempty(intersections) return [points] diff --git a/src/meshes.jl b/src/meshes.jl index 3ab03daa..93b664c6 100644 --- a/src/meshes.jl +++ b/src/meshes.jl @@ -1,216 +1,211 @@ -const FaceMesh{Dim,T,Element} = Mesh{Dim,T,Element,<:FaceView{Element}} +""" + mesh(primitive::GeometryPrimitive[; pointtype = Point, facetype = GLTriangleFace, vertex_attributes...]) -coordinates(mesh::FaceMesh) = coordinates(getfield(mesh, :simplices)) -faces(mesh::FaceMesh) = faces(getfield(mesh, :simplices)) +Creates a mesh from a given `primitive` with the given `pointtype` and `facetype`. -function texturecoordinates(mesh::AbstractMesh) - hasproperty(mesh, :uv) && return mesh.uv - hasproperty(mesh, :uvw) && return mesh.uvw - return nothing -end +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. -function normals(mesh::AbstractMesh) - hasproperty(mesh, :normals) && return mesh.normals - return nothing -end - -const GLTriangleElement = Triangle{3,Float32} -const GLTriangleFace = TriangleFace{GLIndex} -const PointWithUV{Dim,T} = PointMeta{Dim,T,Point{Dim,T},(:uv,),Tuple{Vec{2,T}}} -const PointWithNormal{Dim,T} = PointMeta{Dim,T,Point{Dim,T},(:normals,),Tuple{Vec{3,T}}} -const PointWithUVNormal{Dim,T} = PointMeta{Dim,T,Point{Dim,T},(:normals, :uv), - Tuple{Vec{3,T},Vec{2,T}}} -const PointWithUVWNormal{Dim,T} = PointMeta{Dim,T,Point{Dim,T},(:normals, :uvw), - Tuple{Vec{3,T},Vec{3,T}}} +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) """ - TriangleMesh{Dim, T, PointType} +function mesh(primitive::AbstractGeometry; pointtype=Point, facetype=GLTriangleFace, vertex_attributes...) + positions = decompose(pointtype, primitive) -Abstract Mesh with triangle elements of eltype `T`. -""" -const TriangleMesh{Dim,T,PointType} = AbstractMesh{TriangleP{Dim,T,PointType}} + # 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 -""" - PlainMesh{Dim, T} + # If faces returns nothing for primitive, we try to triangulate! + if isnothing(_fs) + if eltype(positions) <: Point2 + # try triangulation with the converted positions as a last attempt + fs = decompose(facetype, positions) + else + error("No triangulation for $(typeof(primitive))") + end + else + fs = _fs + end -Triangle mesh with no meta information (just points + triangle faces) -""" -const PlainMesh{Dim,T} = TriangleMesh{Dim,T,Point{Dim,T}} -const GLPlainMesh{Dim} = PlainMesh{Dim,Float32} -const GLPlainMesh2D = GLPlainMesh{2} -const GLPlainMesh3D = GLPlainMesh{3} + return mesh(positions, collect(fs); facetype = facetype, vertex_attributes...) +end """ - UVMesh{Dim, T} + mesh(positions, faces[, facetype = GLTriangleFace, vertex_attributes...]) -PlainMesh with texture coordinates meta at each point. -`uvmesh.uv isa AbstractVector{Vec2f}` -""" -const UVMesh{Dim,T} = TriangleMesh{Dim,T,PointWithUV{Dim,T}} -const GLUVMesh{Dim} = UVMesh{Dim,Float32} -const GLUVMesh2D = UVMesh{2} -const GLUVMesh3D = UVMesh{3} +Creates a mesh from the given positions and faces. Other vertex attributes like +normals and texture coordinates can be added as keyword arguments. -""" - NormalMesh{Dim, T} +Note that vertex attributes that are `nothing` get removed before creating a mesh. -PlainMesh with normals meta at each point. -`normalmesh.normals isa AbstractVector{Vec3f}` +See also:[`normal_mesh`](@ref) """ -const NormalMesh{Dim,T} = TriangleMesh{Dim,T,PointWithNormal{Dim,T}} -const GLNormalMesh{Dim} = NormalMesh{Dim,Float32} -const GLNormalMesh2D = GLNormalMesh{2} -const GLNormalMesh3D = GLNormalMesh{3} +function mesh( + positions::AbstractVector{<:Point}, + faces::AbstractVector{FT}; + facetype=GLTriangleFace, vertex_attributes... + ) where {FT <: AbstractFace} -""" - NormalUVMesh{Dim, T} + fs = decompose(facetype, faces) -PlainMesh with normals and uv meta at each point. -`normalmesh.normals isa AbstractVector{Vec3f}` -`normalmesh.uv isa AbstractVector{Vec2f}` -""" -const NormalUVMesh{Dim,T} = TriangleMesh{Dim,T,PointWithUVNormal{Dim,T}} -const GLNormalUVMesh{Dim} = NormalUVMesh{Dim,Float32} -const GLNormalUVMesh2D = GLNormalUVMesh{2} -const GLNormalUVMesh3D = GLNormalUVMesh{3} + 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) -""" - NormalUVWMesh{Dim, T} + return Mesh(positions, fs; va...) +end -PlainMesh with normals and uvw (texture coordinates in 3D) meta at each point. -`normalmesh.normals isa AbstractVector{Vec3f}` -`normalmesh.uvw isa AbstractVector{Vec3f}` """ -const NormalUVWMesh{Dim,T} = TriangleMesh{Dim,T,PointWithUVWNormal{Dim,T}} -const GLNormalUVWMesh{Dim} = NormalUVWMesh{Dim,Float32} -const GLNormalUVWMesh2D = GLNormalUVWMesh{2} -const GLNormalUVWMesh3D = GLNormalUVWMesh{3} + mesh(mesh::Mesh[; pointtype = Point, facetype = GLTriangleFace, vertex_attributes...] -function decompose_triangulate_fallback(primitive::Meshable; pointtype, facetype) - positions = decompose(pointtype, primitive) - faces = decompose(facetype, primitive) - # If faces returns nothing for primitive, we try to triangulate! - if faces === nothing - # triangulation.jl - faces = decompose(facetype, positions) +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 - return positions, faces end """ - mesh(primitive::GeometryPrimitive; - pointtype=Point, facetype=GLTriangle, - uv=nothing, normaltype=nothing) + mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace) -Creates a mesh from `primitive`. - -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! +Create a mesh from a polygon given as a vector of points, using triangulation. """ -function mesh(primitive::Meshable; pointtype=Point, facetype=GLTriangleFace, uv=nothing, - normaltype=nothing) - - positions, faces = decompose_triangulate_fallback(primitive; pointtype=pointtype, facetype=facetype) +function mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace) where {P<:Point{2}} + return mesh(Polygon(polygon); pointtype=pointtype, facetype=facetype) +end - # We want to preserve any existing attributes! - attrs = attributes(primitive) - # Make sure this doesn't contain position, we'll add position explicitely via meta! - delete!(attrs, :position) +""" + triangle_mesh(primitive::GeometryPrimitive[; pointtype = Point, facetype = GLTriangleFace]) - if uv !== nothing - # this may overwrite an existing :uv, but will only create a copy - # if it has a different eltype, otherwise it should replace it - # with exactly the same instance - which is what we want here - attrs[:uv] = decompose(UV(uv), primitive) - end +Creates a simple triangle mesh from a given `primitive` with the given `pointtype` +and `facetype`. - if normaltype !== nothing - primitive_normals = normals(primitive) - if primitive_normals !== nothing - attrs[:normals] = decompose(normaltype, primitive_normals) - else - # Normals not implemented for primitive, so we calculate them! - n = normals(positions, faces; normaltype=normaltype) - if n !== nothing # ok jeez, this is a 2d mesh which cant have normals - attrs[:normals] = n - end - end - end - return Mesh(meta(positions; attrs...), faces) +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 -""" - mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace, - normaltype=nothing) +pointtype(::Mesh{D, T}) where {D, T} = Point{D, T} +facetype(::Mesh{D, T, FT}) where {D, T, FT} = FT -Create a mesh from a polygon given as a vector of points, using triangulation. """ -function mesh(polygon::AbstractVector{P}; pointtype=P, facetype=GLTriangleFace, - normaltype=nothing) where {P<:AbstractPoint{2}} + uv_mesh(primitive::GeometryPrimitive{N}[; pointtype = Point{N, Float32}, facetype = GLTriangleFace, uvtype = Vec2f]) - return mesh(Polygon(polygon); pointtype=pointtype, facetype=facetype, - normaltype=normaltype) +Creates a triangle mesh with texture coordinates from a given `primitive`. The +`pointtype`, `facetype` and `uvtype` are set by the correspondering keyword arguments. + +See also: [`triangle_mesh`](@ref), [`normal_mesh`](@ref), [`uv_mesh`](@ref), [`uv_normal_mesh`](@ref) +""" +function uv_mesh( + primitive::AbstractGeometry{N}; pointtype = Point{N, Float32}, + uvtype = Vec2f, facetype = GLTriangleFace + ) where {N} + + return mesh( + primitive, uv = decompose_uv(uvtype, primitive), + pointtype = pointtype, facetype = facetype + ) end -function mesh(polygon::AbstractPolygon{Dim,T}; pointtype=Point{Dim,T}, - facetype=GLTriangleFace, normaltype=nothing) where {Dim,T} +""" + uv_normal_mesh(primitive::GeometryPrimitive{N}[; pointtype = Point{N, Float32}, facetype = GLTriangleFace, uvtype = Vec2f, normaltype = Vec3f]) - positions, faces = decompose_triangulate_fallback(polygon; pointtype=pointtype, facetype=facetype) +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. - if normaltype !== nothing - n = normals(positions, faces; normaltype=normaltype) - positions = meta(positions; normals=n) - end - return Mesh(positions, faces) +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 -pointtype(x::Mesh) = eltype(decompose(Point, x)) -facetype(x::Mesh) = eltype(faces(x)) +""" + uv_normal_mesh(primitive::GeometryPrimitive{N}[; pointtype = Point{N, Float32}, facetype = GLTriangleFace, uvtype = Vec2f, normaltype = Vec3f]) -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 and normals from a given +`primitive`. The `pointtype`, `facetype` and `uvtype` and `normaltype` are set +by the correspondering keyword arguments. -function triangle_mesh(primitive::Meshable{N}; nvertices=nothing) where {N} - if nvertices !== nothing - @warn("nvertices argument deprecated. Wrap primitive in `Tesselation(primitive, nvertices)`") - primitive = Tesselation(primitive, nvertices) - end - return mesh(primitive; pointtype=Point{N,Float32}, facetype=GLTriangleFace) +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 uv_mesh(primitive::Meshable{N,T}) where {N,T} - return mesh(primitive; pointtype=Point{N,Float32}, uv=Vec2f, facetype=GLTriangleFace) -end +""" + normal_mesh(primitive::GeometryPrimitive{N}[; pointtype = Point{N, Float32}, facetype = GLTriangleFace, normaltype = Vec3f]) -function uv_normal_mesh(primitive::Meshable{N}) where {N} - return mesh(primitive; pointtype=Point{N,Float32}, uv=Vec2f, normaltype=Vec3f, - facetype=GLTriangleFace) -end +Creates a triangle mesh with normals from a given `primitive`. The `pointtype`, `facetype` and `uvtype` and `normaltype` are set +by the correspondering keyword arguments. -function normal_mesh(points::AbstractVector{<:AbstractPoint}, - faces::AbstractVector{<:AbstractFace}) - _points = decompose(Point3f, points) - _faces = decompose(GLTriangleFace, faces) - return Mesh(meta(_points; normals=normals(_points, _faces)), _faces) +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 -function normal_mesh(primitive::Meshable{N}; nvertices=nothing) where {N} - if nvertices !== nothing - @warn("nvertices argument deprecated. Wrap primitive in `Tesselation(primitive, nvertices)`") - primitive = Tesselation(primitive, nvertices) - end - return mesh(primitive; pointtype=Point{N,Float32}, normaltype=Vec3f, - facetype=GLTriangleFace) -end """ volume(triangle) @@ -234,75 +229,303 @@ 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 `expand_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) - error("No meshes to merge") + return Mesh(Point3f[], GLTriangleFace[]) + elseif length(meshes) == 1 return meshes[1] + else - ps = reduce(vcat, coordinates.(meshes)) - fs = reduce(vcat, faces.(meshes)) - idx = length(faces(meshes[1])) - offset = length(coordinates(meshes[1])) - for mesh in Iterators.drop(meshes, 1) - N = length(faces(mesh)) - for i = idx .+ (1:N) - fs[i] = fs[i] .+ offset + 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 - idx += N - offset += length(coordinates(mesh)) end - return Mesh(ps, fs) + @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(expand_faceviews.(meshes)) + + end + end end - """ - pointmeta(mesh::Mesh; meta_data...) + expand_faceviews(mesh::Mesh) -Attaches metadata to the coordinates of a 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 pointmeta(mesh::Mesh; meta_data...) - points = coordinates(mesh) - attr = attributes(points) - delete!(attr, :position) # position == metafree(points) - # delete overlapping attributes so we can replace with `meta_data` - foreach(k -> delete!(attr, k), keys(meta_data)) - return Mesh(meta(metafree(points); attr..., meta_data...), faces(mesh)) +function expand_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 + + 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 + + # 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(new_va, new_fs, new_views) + end end -function pointmeta(mesh::Mesh, uv::UV) - return pointmeta(mesh; uv=decompose(uv, mesh)) +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 pointmeta(mesh::Mesh, normal::Normal) - return pointmeta(mesh; normals=decompose(normal, mesh)) +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 + """ - pop_pointmeta(mesh::Mesh, property::Symbol) -Remove `property` from point metadata. -Returns the new mesh, and the property! + 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 pop_pointmeta(mesh::Mesh, property::Symbol) - points = coordinates(mesh) - attr = attributes(points) - delete!(attr, :position) # position == metafree(points) - # delete overlapping attributes so we can replace with `meta_data` - m = pop!(attr, property) - return Mesh(meta(metafree(points); attr...), faces(mesh)), m +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 end """ - facemeta(mesh::Mesh; meta_data...) + remove_duplicates(faces) -Attaches metadata to the faces of a mesh +Uses a Dict to remove duplicates from the given `faces`. """ -function facemeta(mesh::Mesh; meta_data...) - return Mesh(coordinates(mesh), meta(faces(mesh); meta_data...)) +function remove_duplicates(fs::AbstractVector{FT}) where {FT <: AbstractFace} + hashmap = Dict{FT, Nothing}() + foreach(k -> setindex!(hashmap, nothing, k), fs) + return collect(keys(hashmap)) +end + + +function map_coordinates(f, mesh::Mesh) + result = copy(mesh) + map_coordinates!(f, result) + return result +end + +function map_coordinates(f, mesh::MetaMesh) + result = copy(Mesh(mesh)) + map_coordinates!(f, result) + return MetaMesh(result, meta(mesh)) +end + +function map_coordinates!(f, mesh::AbstractMesh) + points = coordinates(mesh) + map!(f, points, points) + return mesh +end + +function 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 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, " 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 attributes(hasmeta::Mesh) - return Dict{Symbol,Any}((name => getproperty(hasmeta, name) - for name in propertynames(hasmeta))) +function Base.show(io::IO, mesh::MetaMesh{N, T}) where {N, T} + FT = eltype(faces(mesh)) + println(io, "MetaMesh{$N, $T, $(FT)}($(join(keys(meta(mesh)), ", ")))") end diff --git a/src/metadata.jl b/src/metadata.jl deleted file mode 100644 index c8e3378b..00000000 --- a/src/metadata.jl +++ /dev/null @@ -1,305 +0,0 @@ -#= -Helper functions that works around the fact, that there is no generic -Table interface for this functionality. Once this is in e.g. Tables.jl, -it should be removed from GeometryBasics! -=# - -""" - attributes(hasmeta) -Returns all attributes of meta as a Dict{Symbol, Any}. -Needs to be overloaded, and returns empty dict for non overloaded types! -Gets overloaded by default for all Meta types. -""" -function attributes(hasmeta) - return Dict{Symbol,Any}() -end - -function attributes(hasmeta::StructArray) - return Dict{Symbol,Any}((name => getproperty(hasmeta, name) - for name in propertynames(hasmeta))) -end - -""" - getcolumns(t, colnames::Symbol...) - -Gets a column from any Array like (Table/AbstractArray). -For AbstractVectors, a column will be the field names of the element type. -""" -function getcolumns(tablelike, colnames::Symbol...) - return getproperty.((tablelike,), colnames) -end - -getcolumn(t, colname::Symbol) = getcolumns(t, colname)[1] - -""" - MetaType(::Type{T}) - -Returns the Meta Type corresponding to `T` -E.g: -```julia -MetaType(Point) == PointMeta -``` -""" -MetaType(::Type{T}) where {T} = error("No Meta Type for $T") - -""" - MetaFree(::Type{T}) - -Returns the original type containing no metadata for `T` -E.g: -```julia -MetaFree(PointMeta) == Point -``` -""" -MetaFree(::Type{T}) where {T} = error("No meta free Type for $T") - -""" - meta(x::MetaObject) - -Returns the metadata of `x` -""" -meta(x::T) where {T} = error("$T has no meta!") - -metafree(x::T) where {T} = x - -macro meta_type(name, mainfield, supertype, params...) - MetaName = Symbol("$(name)Meta") - field = QuoteNode(mainfield) - NoParams = Symbol("$(MetaName)NoParams") - - params_sym = map(params) do param - param isa Symbol && return param - param isa Expr && param.head == :(<:) && return param.args[1] - return error("Unsupported type parameter: $(param)") - end - - expr = quote - struct $MetaName{$(params...),Typ<:$supertype{$(params_sym...)},Names,Types} <: - $supertype{$(params_sym...)} - main::Typ - meta::NamedTuple{Names,Types} - end - - const $NoParams{Typ,Names,Types} = $MetaName{$(params_sym...),Typ,Names, - Types} where {$(params_sym...)} - - function Base.getproperty(x::$MetaName{$(params_sym...),Typ,Names,Types}, - field::Symbol) where {$(params...),Typ,Names,Types} - field === $field && return getfield(x, :main) - field === :main && return getfield(x, :main) - Base.sym_in(field, Names) && return getfield(getfield(x, :meta), field) - return error("Field $field not part of Element") - end - - function GeometryBasics.MetaType(XX::Type{<:$supertype{$(params_sym...)} where {$(params...)}}) - return $MetaName - end - - function GeometryBasics.MetaType(ST::Type{<:$supertype{$(params_sym...)}}, - ::Type{NamedTuple{Names,Types}}) where {$(params...), - Names, - Types} - return $MetaName{$(params_sym...),ST,Names,Types} - end - - GeometryBasics.MetaFree(::Type{<:$MetaName{$(params_sym...),Typ}}) where {$(params_sym...), Typ<:$supertype{$(params_sym...)} } = Typ - GeometryBasics.MetaFree(::Type{<:$MetaName}) = $name - GeometryBasics.metafree(x::$MetaName) = getfield(x, :main) - GeometryBasics.metafree(x::AbstractVector{<:$MetaName}) = getproperty(x, $field) - GeometryBasics.meta(x::$MetaName) = getfield(x, :meta) - GeometryBasics.meta(x::AbstractVector{<:$MetaName}) = getproperty(x, :meta) - - function GeometryBasics.meta(main::$supertype{$(params_sym...)}; - meta...) where {$(params...)} - isempty(meta) && return elements # no meta to add! - return $MetaName(main; meta...) - end - - function GeometryBasics.meta(elements::AbstractVector{XX}; - meta...) where {XX<:$supertype{$(params_sym...)}} where {$(params...)} - isempty(meta) && return elements # no meta to add! - n = length(elements) - for (k, v) in meta - if v isa AbstractVector - mn = length(v) - mn != n && error("Metadata array needs to have same length as data. - Found $(n) data items, and $mn metadata items") - else - error("Metadata needs to be an array with the same length as data items. Found: $(typeof(v))") - end - end - nt = values(meta) - # get the first element to get the per element named tuple type - ElementNT = typeof(map(first, nt)) - - return StructArray{MetaType(XX, ElementNT)}(($(mainfield)=elements, nt...)) - end - - function GeometryBasics.attributes(hasmeta::$MetaName) - return Dict{Symbol,Any}((name => getproperty(hasmeta, name) - for name in propertynames(hasmeta))) - end - - function (MT::Type{<:$MetaName})(args...; meta...) - nt = values(meta) - obj = MetaFree(MT)(args...) - return MT(obj, nt) - end - - function (MT::Type{<:$MetaName})(main::$(name); meta...) - nt = values(meta) - return MT(main, nt) - end - - function Base.propertynames(::$MetaName{$(params_sym...),Typ,Names,Types}) where {$(params...), - Typ, - Names, - Types} - return ($field, Names...) - end - - function StructArrays.component(x::$MetaName{$(params_sym...),Typ,Names,Types}, - field::Symbol) where {$(params...),Typ,Names,Types} - return getproperty(x, field) - end - - function StructArrays.staticschema(::Type{$MetaName{$(params_sym...),Typ,Names, - Types}}) where {$(params...), - Typ,Names,Types} - return NamedTuple{($field, Names...),Base.tuple_type_cons(Typ, Types)} - end - - function StructArrays.createinstance(::Type{$MetaName{$(params_sym...),Typ,Names, - Types}}, metafree, - args...) where {$(params...),Typ,Names,Types} - return $MetaName(metafree, NamedTuple{Names,Types}(args)) - end - end - return esc(expr) -end - -@meta_type(Point, position, AbstractPoint, Dim, T) -Base.getindex(x::PointMeta, idx::Int) = getindex(metafree(x), idx) - -@meta_type(NgonFace, ngon, AbstractNgonFace, N, T) -Base.getindex(x::NgonFaceMeta, idx::Int) = getindex(metafree(x), idx) - -@meta_type(SimplexFace, simplex, AbstractSimplexFace, N, T) -Base.getindex(x::SimplexFaceMeta, idx::Int) = getindex(metafree(x), idx) - -@meta_type(Polygon, polygon, AbstractPolygon, N, T) - -@meta_type(LineString, lines, AbstractVector, P <: Line) -Base.getindex(x::LineStringMeta, idx::Int) = getindex(metafree(x), idx) -Base.size(x::LineStringMeta) = size(metafree(x)) - -@meta_type(MultiPoint, points, AbstractVector, P <: AbstractPoint) -Base.getindex(x::MultiPointMeta, idx::Int) = getindex(metafree(x), idx) -Base.size(x::MultiPointMeta) = size(metafree(x)) - -@meta_type(MultiLineString, linestrings, AbstractVector, P <: LineString) -Base.getindex(x::MultiLineStringMeta, idx::Int) = getindex(metafree(x), idx) -Base.size(x::MultiLineStringMeta) = size(metafree(x)) - -@meta_type(MultiPolygon, polygons, AbstractVector, P <: Polygon) -Base.getindex(x::MultiPolygonMeta, idx::Int) = getindex(metafree(x), idx) -Base.size(x::MultiPolygonMeta) = size(metafree(x)) - -@meta_type(Mesh, mesh, AbstractMesh, Element <: Polytope) -Base.getindex(x::MeshMeta, idx::Int) = getindex(metafree(x), idx) -Base.size(x::MeshMeta) = size(metafree(x)) - -""" - - MetaT(geometry, meta::NamedTuple) - MetaT(geometry; meta...) - -Returns a `MetaT` that holds a geometry and its metadata - -`MetaT` acts the same as `Meta` method. -The difference lies in the fact that it is designed to handle -geometries and metadata of different/heterogeneous types. - -eg: While a Point MetaGeometry is a `PointMeta`, the MetaT representation is `MetaT{Point}` -The downside being it's not subtyped to `AbstractPoint` like a `PointMeta` is. - -Example: -```julia -julia> MetaT(Point(1, 2), city = "Mumbai") -MetaT{Point{2,Int64},(:city,),Tuple{String}}([1, 2], (city = "Mumbai",)) -``` -""" -struct MetaT{T,Names,Types} - main::T - meta::NamedTuple{Names,Types} -end - -MetaT(x; kwargs...) = MetaT(x, values(kwargs)) - -""" - - metafree(x::MetaT) - metafree(x::Array{MetaT}) - -Free the MetaT from metadata -i.e. returns the geometry/array of geometries -""" -function metafree(x::MetaT) - return getfield(x, :main) -end -metafree(x::AbstractVector{<:MetaT}) = map(metafree, x) - -""" - - meta(x::MetaT) - meta(x::Array{MetaT}) - -Returns the metadata of a `MetaT` -""" -function meta(x::MetaT) - return getfield(x, :meta) -end -meta(x::AbstractVector{<:MetaT}) = map(meta, x) - -# helper methods -function Base.getproperty(x::MetaT, field::Symbol) - return if field == :main - metafree(x) - elseif field == :meta - meta(x) - else - getproperty(meta(x), field) - end -end - -Base.propertynames(x::MetaT) = (:main, propertynames(meta(x))...) -getnamestypes(::Type{MetaT{T,Names,Types}}) where {T,Names,Types} = (T, Names, Types) - -# explicitly give the "schema" of the object to StructArrays -function StructArrays.staticschema(::Type{F}) where {F<:MetaT} - T, names, types = getnamestypes(F) - return NamedTuple{(:main, names...),Base.tuple_type_cons(T, types)} -end - -# generate an instance of MetaT type -function StructArrays.createinstance(::Type{F}, x, args...) where {F<:MetaT} - T, names, types = getnamestypes(F) - return MetaT(x, NamedTuple{names,types}(args)) -end - -""" -Puts an iterable of MetaT's into a StructArray -""" -function meta_table(iter) - cols = Tables.columntable(iter) - return meta_table(first(cols), Base.tail(cols)) -end - -function meta_table(main, meta::NamedTuple{names}) where {names} - eltypes = Tuple{map(eltype, values(meta))...} - F = MetaT{eltype(main),names,eltypes} - return StructArray{F}(; main=main, meta...) -end - -Base.getindex(x::MetaT, idx::Int) = getindex(metafree(x), idx) -Base.size(x::MetaT) = size(metafree(x)) diff --git a/src/offsetintegers.jl b/src/offsetintegers.jl index 12c567be..f0f0d85c 100644 --- a/src/offsetintegers.jl +++ b/src/offsetintegers.jl @@ -1,4 +1,3 @@ - """ OffsetInteger{O, T} @@ -20,8 +19,19 @@ raw(x::Integer) = x value(x::OffsetInteger{O,T}) where {O,T} = raw(x) - O value(x::Integer) = x -function show(io::IO, oi::OffsetInteger) - return print(io, "|$(raw(oi)) (indexes as $(value(oi))|") +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 + + print(io, typename, "(", value(oi), ")") + return end Base.eltype(::Type{OffsetInteger{O,T}}) where {O,T} = T @@ -37,21 +47,12 @@ OffsetInteger{O}(x::OffsetInteger) where {O} = OffsetInteger{O,eltype(x)}(x) # This constructor has a massive method invalidation as a consequence, # and doesn't seem to be needed, so let's remove it! -Base.convert(::Type{IT}, x::OffsetInteger) where {IT<:Integer} = IT(value(x)) - -Base.promote_rule(::Type{IT}, ::Type{<:OffsetInteger}) where {IT<:Integer} = IT - -function Base.promote_rule(::Type{OffsetInteger{O1,T1}}, - ::Type{OffsetInteger{O2,T2}}) where {O1,O2,T1<:Integer, - T2<:Integer} - return OffsetInteger{pure_max(O1, O2),promote_type(T1, T2)} -end - -Base.@pure pure_max(x1, x2) = x1 > x2 ? x1 : x2 - # Need to convert to Int here because of: https://github.com/JuliaLang/julia/issues/35038 -Base.to_index(I::OffsetInteger) = convert(Int, raw(OneIndex(I))) -Base.to_index(I::OffsetInteger{0}) = convert(Int, raw(I)) +Base.to_index(idx::OffsetInteger) = convert(Int, raw(OneIndex(idx))) + +Base.convert(::Type{IT}, x::OffsetInteger) where {IT<:Integer} = IT(value(x)) +Base.convert(::Type{IT}, x::OffsetInteger) where {IT<:OffsetInteger} = IT(value(x)) +Base.convert(::Type{O}, x::Integer) where {O<:OffsetInteger} = O(x) # basic operators for op in (:(-), :abs) @@ -68,8 +69,23 @@ end for op in (:(==), :(>=), :(<=), :(<), :(>), :sub_with_overflow) @eval begin - @inline function Base.$op(x::OffsetInteger{O}, y::OffsetInteger{O}) where {O} + function Base.$op(x::OffsetInteger{O}, y::OffsetInteger{O}) where {O} return $op(x.i, y.i) end + Base.$op(x::OffsetInteger, y::OffsetInteger) = $op(value(x), value(y)) + Base.$op(x::OffsetInteger, y::Integer) = $op(value(x), y) + Base.$op(x::Integer, y::OffsetInteger) = $op(x, value(y)) end end + +Base.promote_rule(::Type{IT}, ::Type{<:OffsetInteger}) where {IT<:Integer} = IT + +function Base.promote_rule(::Type{OffsetInteger{O1,T1}}, + ::Type{OffsetInteger{O2,T2}}) where {O1,O2,T1<:Integer, + T2<:Integer} + return OffsetInteger{pure_max(O1, O2),promote_type(T1, T2)} +end + +Base.@pure pure_max(x1, x2) = x1 > x2 ? x1 : x2 + +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/precompile.jl b/src/precompile.jl deleted file mode 100644 index f7b1c52b..00000000 --- a/src/precompile.jl +++ /dev/null @@ -1,39 +0,0 @@ -macro warnpcfail(ex::Expr) - modl = __module__ - file = __source__.file === nothing ? "?" : String(__source__.file) - line = __source__.line - quote - $(esc(ex)) || @warn """precompile directive - $($(Expr(:quote, ex))) - failed. Please report an issue in $($modl) (after checking for duplicates) or remove this directive.""" _file=$file _line=$line - end -end - -function _precompile_() - ccall(:jl_generating_output, Cint, ()) == 1 || return nothing - @warnpcfail precompile(HyperRectangle{2,Float32}, (Int, Int, Int, Int)) - @warnpcfail precompile(==, (HyperRectangle{2,Float32}, HyperRectangle{2,Float32})) - @warnpcfail precompile(normal_mesh, (Tesselation{3,Float32,Cylinder{3,Float32},1},)) - @warnpcfail precompile(normal_mesh, (Tesselation{3,Float32,HyperSphere{3,Float32},1},)) - @warnpcfail precompile(normal_mesh, (HyperSphere{3,Float32},)) - @warnpcfail precompile(Tuple{typeof(*),SMatrix{4, 4, Float32, 16},HyperRectangle{3, Float32}}) # time: 0.11091917 - @warnpcfail precompile(Tuple{typeof(coordinates),HyperRectangle{2, Float32},Tuple{Int64, Int64}}) # time: 0.08693867 - @warnpcfail precompile(union, (HyperRectangle{3, Float32}, HyperRectangle{3, Float32})) - @warnpcfail precompile(Tuple{typeof(decompose),Type{Point{2, Float32}},HyperRectangle{2, Float32}}) # time: 0.026609203 - @warnpcfail precompile(Tuple{Type{HyperRectangle{3, Float32}},HyperRectangle{2, Float32}}) # time: 0.023717888 - @warnpcfail precompile(Tuple{typeof(+),HyperRectangle{3, Float32},Point{3, Float32}}) # time: 0.006633118 - @warnpcfail precompile(Tuple{Type{Rect2{T} where T},Float32,Float32,Float32,Float32}) # time: 0.001636267 - @warnpcfail precompile(Tuple{typeof(*),HyperRectangle{2, Float32},Float32}) # time: 0.001057589 - - if Base.VERSION >= v"1.6.0-DEV.1083" - @warnpcfail precompile(triangle_mesh, (Polygon{2, Float32, Point2f, LineString{2, Float32, Point2f, - Base.ReinterpretArray{Line{2, Float32}, 1, Tuple{Point2f, Point2f}, TupleView{Tuple{Point2f, Point2f}, 2, 1, Vector{Point2f}}, false}}, - Vector{LineString{2, Float32, Point2f, Base.ReinterpretArray{Line{2, Float32}, 1, Tuple{Point2f, Point2f}, TupleView{Tuple{Point2f, Point2f}, 2, 1, Vector{Point2f}}, false}}}},)) - else - @warnpcfail precompile(triangle_mesh, (Polygon{2, Float32, Point2f, LineString{2, Float32, Point2f, - Base.ReinterpretArray{Line{2, Float32}, 1, Tuple{Point2f, Point2f}, TupleView{Tuple{Point2f, Point2f}, 2, 1, Vector{Point2f}}}}, - Vector{LineString{2, Float32, Point2f, Base.ReinterpretArray{Line{2, Float32}, 1, Tuple{Point2f, Point2f}, TupleView{Tuple{Point2f, Point2f}, 2, 1, Vector{Point2f}}}}}},)) - end - - @warnpcfail precompile(split_intersections, (Vector{Point2f},)) -end diff --git a/src/precompiles.jl b/src/precompiles.jl new file mode 100644 index 00000000..e0a2b26b --- /dev/null +++ b/src/precompiles.jl @@ -0,0 +1,44 @@ +using PrecompileTools: @setup_workload, @compile_workload + +@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, expand_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 9804cf53..ce1c4aaa 100644 --- a/src/primitives/cylinders.jl +++ b/src/primitives/cylinders.jl @@ -1,107 +1,95 @@ """ - 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 = @SVector [d2[1], d2[2], T(0)] - v = @MVector [u[2], -u[1], T(0)] - normalize!(v) - return hcat(v, u, @SVector T[0, 0, 1]) +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 = @SVector [d3[1], d3[2], d3[3]] + u = Vec{3, T}(d3[1], d3[2], d3[3]) if abs(u[1]) > 0 || abs(u[2]) > 0 - v = @MVector [u[2], -u[1], T(0)] + v = Vec{3, T}(u[2], -u[1], T(0)) else - v = @MVector [T(0), -u[3], u[2]] + v = Vec{3, T}(T(0), -u[3], u[2]) end - normalize!(v) - w = @SVector [u[2] * v[3] - u[3] * v[2], -u[1] * v[3] + u[3] * v[1], - u[1] * v[2] - u[2] * v[1]] - return hcat(v, w, u) + v = normalize(v) + w = Vec{3, T}(u[2] * v[3] - u[3] * v[2], -u[1] * v[3] + u[3] * v[1], + u[1] * v[2] - u[2] * v[1]) + return Mat{3, 3, T}(v..., w..., u...) end -function coordinates(c::Cylinder{2,T}, nvertices=(2, 2)) where {T} - 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(sphere::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 92dda1cc..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 @@ -199,8 +199,8 @@ split(b::Rect, axis, value::Number) = _split(b, axis, value) function _split(b::H, axis, value) where {H<:Rect} bmin = minimum(b) bmax = maximum(b) - b1max = setindex(bmax, value, axis) - b2min = setindex(bmin, value, axis) + b1max = Base.setindex(bmax, value, axis) + b2min = Base.setindex(bmin, value, axis) return H(bmin, b1max - bmin), H(b2min, bmax - b2min) end @@ -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) @@ -341,7 +342,9 @@ Return `true` if any of the widths of `h` are negative. Base.isempty(h::Rect{N,T}) where {N,T} = any(<(zero(T)), h.widths) """ -Perform a union between two Rects. + union(r1::Rect{N}, r2::Rect{N}) + +Returns a new `Rect{N}` which contains both r1 and r2. """ function Base.union(h1::Rect{N}, h2::Rect{N}) where {N} m = min.(minimum(h1), minimum(h2)) @@ -349,19 +352,21 @@ function Base.union(h1::Rect{N}, h2::Rect{N}) where {N} return Rect{N}(m, mm - m) end -""" - diff(h1::Rect, h2::Rect) +# TODO: What should this be? The difference is "h2 - h1", which could leave an +# L shaped cutout. Should we pad that back out into a full rect? +# """ +# diff(h1::Rect, h2::Rect) -Perform a difference between two Rects. -""" -diff(h1::Rect, h2::Rect) = h1 +# Perform a difference between two Rects. +# """ +# diff(h1::Rect, h2::Rect) = h1 """ intersect(h1::Rect, h2::Rect) Perform a intersection between two Rects. """ -function intersect(h1::Rect{N}, h2::Rect{N}) where {N} +function Base.intersect(h1::Rect{N}, h2::Rect{N}) where {N} m = max.(minimum(h1), minimum(h2)) mm = min.(maximum(h1), maximum(h2)) return Rect{N}(m, mm - m) @@ -540,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 ivec(((x, y) for x in xrange, y in yrange)) + 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)) @@ -563,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 de7c59b6..76602326 100644 --- a/src/primitives/spheres.jl +++ b/src/primitives/spheres.jl @@ -32,7 +32,7 @@ origin(c::HyperSphere) = c.center Base.minimum(c::HyperSphere{N,T}) where {N,T} = Vec{N,T}(origin(c)) - Vec{N,T}(radius(c)) Base.maximum(c::HyperSphere{N,T}) where {N,T} = Vec{N,T}(origin(c)) + Vec{N,T}(radius(c)) -function Base.in(x::AbstractPoint, c::HyperSphere) +function Base.in(x::Point, c::HyperSphere) return norm(origin(c) - x) ≤ radius(c) end @@ -42,31 +42,39 @@ 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(s::Circle, nvertices=64) +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) inner(θ, φ) = Point(cos(φ) * sin(θ), sin(φ) * sin(θ), cos(θ)) .* s.r .+ s.center - return ivec((inner(θ, φ) for θ in θ, φ in φ)) + return [inner(θ, φ) for φ in φ for θ in θ] end -function texturecoordinates(s::Sphere, nvertices=24) +function texturecoordinates(::Sphere, nvertices=24) ux = LinRange(0, 1, nvertices) return ivec(((φ, θ) for θ in reverse(ux), φ in ux)) end -function faces(sphere::Sphere, nvertices=24) +function faces(::Sphere, nvertices=24) return faces(Rect(0, 0, 1, 1), (nvertices, nvertices)) end -function normals(s::Sphere{T}, nvertices=24) where {T} +function normals(::Sphere{T}, nvertices=24) where {T} return coordinates(Sphere(Point{3,T}(0), 1), nvertices) end diff --git a/src/triangulation.jl b/src/triangulation.jl index 06e2e894..1847f9b5 100644 --- a/src/triangulation.jl +++ b/src/triangulation.jl @@ -13,35 +13,35 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. =# """ - area(vertices::AbstractVector{AbstractPoint{3}}, face::TriangleFace) + area(vertices::AbstractVector{Point{3}}, face::TriangleFace) Calculate the area of one triangle. """ -function area(vertices::AbstractVector{<:AbstractPoint{3,VT}}, +function area(vertices::AbstractVector{<:Point{3,VT}}, face::TriangleFace{FT}) where {VT,FT} v1, v2, v3 = vertices[face] return 0.5 * norm(orthogonal_vector(v1, v2, v3)) end """ - area(vertices::AbstractVector{AbstractPoint{3}}, faces::AbstractVector{TriangleFace}) + area(vertices::AbstractVector{Point{3}}, faces::AbstractVector{TriangleFace}) Calculate the area of all triangles. """ -function area(vertices::AbstractVector{<:AbstractPoint{3,VT}}, +function area(vertices::AbstractVector{Point{3,VT}}, faces::AbstractVector{TriangleFace{FT}}) where {VT,FT} return sum(x -> area(vertices, x), faces) end """ - area(contour::AbstractVector{AbstractPoint}}) + area(contour::AbstractVector{Point}}) Calculate the area of a polygon. For 2D points, the oriented area is returned (negative when the points are oriented clockwise). """ -function area(contour::AbstractVector{<:AbstractPoint{2,T}}) where {T} +function area(contour::AbstractVector{Point{2,T}}) where {T} length(contour) < 3 && return zero(T) A = zero(T) p = lastindex(contour) @@ -52,7 +52,7 @@ function area(contour::AbstractVector{<:AbstractPoint{2,T}}) where {T} return A * T(0.5) end -function area(contour::AbstractVector{<:AbstractPoint{3,T}}) where {T} +function area(contour::AbstractVector{Point{3,T}}) where {T} A = zero(eltype(contour)) o = first(contour) for i in (firstindex(contour) + 1):(lastindex(contour) - 1) @@ -66,26 +66,25 @@ end Determine if a point is inside of a triangle. """ -function Base.in(P::T, triangle::Triangle) where {T<:AbstractPoint} +function Base.in(P::T, triangle::Triangle) where {T<:Point} A, B, C = coordinates(triangle) - a = C .- B - b = A .- C - c = B .- A - ap = P .- A - bp = P .- B - cp = P .- C + ap = A .- P + bp = B .- P + cp = C .- P - a_bp = a[1] * bp[2] - a[2] * bp[1] - c_ap = c[1] * ap[2] - c[2] * ap[1] - b_cp = b[1] * cp[2] - b[2] * cp[1] + u = cross(bp, cp) + v = cross(cp, ap) + w = cross(ap, bp) t0 = zero(eltype(T)) - - return ((a_bp >= t0) && (b_cp >= t0) && (c_ap >= t0)) + dot(u, v) < t0 && return false + dot(u, w) < t0 && return false + return true end -function snip(contour::AbstractVector{<:AbstractPoint}, u, v, w, n, V) + +function snip(contour::AbstractVector{<:Point}, u, v, w, n, V) A = contour[V[u]] B = contour[V[v]] C = contour[V[w]] @@ -105,16 +104,15 @@ function snip(contour::AbstractVector{<:AbstractPoint}, u, v, w, n, V) end """ - decompose(facetype, contour::AbstractArray{<:AbstractPoint}) + decompose(facetype, contour::AbstractArray{<:Point}) Triangulate a Polygon without hole. Returns a Vector{`facetype`} defining indexes into `contour`. """ -function decompose(::Type{FaceType}, - points::AbstractArray{P}) where {P<:AbstractPoint,FaceType<:AbstractFace} +function decompose(::Type{F}, points::AbstractVector{<:Point}) where {F<:AbstractFace} #= allocate and initialize list of Vertices in polygon =# - result = FaceType[] + result = F[] # the algorithm doesn't like closed contours contour = if isapprox(last(points), first(points)) @@ -160,7 +158,7 @@ function decompose(::Type{FaceType}, b = V[v] c = V[w] #= output Triangle =# - push!(result, convert_simplex(FaceType, TriangleFace(a, b, c))...) + push!(result, convert_simplex(F, TriangleFace(a, b, c))...) #= remove v from remaining polygon =# s = v t = v + 1 diff --git a/src/viewtypes.jl b/src/viewtypes.jl index b700b6ea..41937a39 100644 --- a/src/viewtypes.jl +++ b/src/viewtypes.jl @@ -56,13 +56,8 @@ function TupleView{N,M}(x::AbstractVector{T}; connect=false) where {T,N,M} return TupleView{NTuple{N,T},N,M,typeof(x)}(x, connect) end -@inline function connected_line(points::AbstractVector{<:AbstractPoint{N}}, - skip=N) where {N} - return connect(points, Line, skip) -end - """ - connect(points::AbstractVector{<: AbstractPoint}, P::Type{<: Polytope{N}}, skip::Int = N) + connect(points::AbstractVector{<: Point}, P::Type{<: Polytope{N}}, skip::Int = N) Creates a view that connects a number of points to a Polytope `P`. Between each polytope, `skip` elements are skipped untill the next starts. @@ -71,24 +66,22 @@ Example: x = connect(Point[(1, 2), (3, 4), (5, 6), (7, 8)], Line, 2) x == [Line(Point(1, 2), Point(3, 4)), Line(Point(5, 6), Point(7, 8))] """ -@inline function connect(points::AbstractVector{Point}, +function connect(points::AbstractVector{Point}, P::Type{<:Polytope{N,T} where {N,T}}, - skip::Int=length(P)) where {Point <: AbstractPoint} - return reinterpret(Polytope(P, Point), TupleView{length(P),skip}(points)) + skip::Int=length(P)) where {Point <: Point} + return map(Polytope(P, Point), TupleView{length(P),skip}(points)) end -@inline function connect(points::AbstractVector{T}, P::Type{<:Point{N}}, - skip::Int=N) where {T <: Real,N} - return reinterpret(Point{N,T}, TupleView{N,skip}(points)) +function connect(points::AbstractVector{T}, ::Type{<:Point{N}}, skip::Int=N) where {T <: Real,N} + return map(Point{N,T}, TupleView{N,skip}(points)) end -@inline function connect(points::AbstractVector{T}, P::Type{<:AbstractFace{N}}, - skip::Int=N) where {T <: Real,N} - return reinterpret(Face(P, T), TupleView{N,skip}(points)) +function connect(indices::AbstractVector{T}, P::Type{<:AbstractFace{N}}, + skip::Int=N) where {T <: Integer, N} + return collect(reinterpret(Face(P, T), TupleView{N, skip}(indices))) end -@inline function connect(points::AbstractMatrix{T}, - P::Type{<:AbstractPoint{N}}) where {T <: Real,N} +function connect(points::AbstractMatrix{T}, P::Type{<:Point{N}}) where {T <: Real, N} return if size(points, 1) === N return reinterpret(Point{N,T}, points) elseif size(points, 2) === N @@ -96,66 +89,12 @@ end columns = ntuple(N) do i return view(points, ((i - 1) * seglen + 1):(i * seglen)) end - return StructArray{Point{N,T}}(columns) + return P.(columns...) else error("Dim 1 or 2 must be equal to the point dimension!") end end -""" - FaceView{Element, Point, Face, P, F} - -FaceView enables to link one array of points via a face array, to generate one -abstract array of elements. -E.g., this becomes possible: -``` -x = FaceView(rand(Point3f, 10), TriangleFace[(1, 2, 3), (2, 4, 5), ...]) -x[1] isa Triangle == true -x isa AbstractVector{<: Triangle} == true -# This means we can use it as a mesh: -Mesh(x) # should just work! -# Can also be used for Points: - -linestring = FaceView(points, LineFace[...]) -Polygon(linestring) -``` -""" -struct FaceView{Element,Point <: AbstractPoint,Face <: AbstractFace,P <: AbstractVector{Point},F <: AbstractVector{Face}} <: AbstractVector{Element} - elements::P - faces::F -end - -const SimpleFaceView{Dim,T,NFace,IndexType,PointType <: AbstractPoint{Dim,T},FaceType <: AbstractFace{NFace,IndexType}} = FaceView{Ngon{Dim,T,NFace,PointType},PointType,FaceType,Vector{PointType},Vector{FaceType}} - -function Base.getproperty(faceview::FaceView, name::Symbol) - return getproperty(getfield(faceview, :elements), name) -end - -function Base.propertynames(faceview::FaceView) - return propertynames(getfield(faceview, :elements)) -end - -Tables.schema(faceview::FaceView) = Tables.schema(getfield(faceview, :elements)) - -Base.size(faceview::FaceView) = size(getfield(faceview, :faces)) - -@propagate_inbounds function Base.getindex(x::FaceView{Element}, i) where {Element} - return Element(map(idx -> coordinates(x)[idx], faces(x)[i])) -end - -@propagate_inbounds function Base.setindex!(x::FaceView{Element}, element::Element, - i) where {Element} - face = faces(x)[i] - for (i, f) in enumerate(face) # TODO unroll!? - coordinates(x)[face[i]] = element[i] - end - return element -end - -function connect(points::AbstractVector{P}, - faces::AbstractVector{F}) where {P <: AbstractPoint,F <: AbstractFace} - return FaceView{Polytope(P, F),P,F,typeof(points),typeof(faces)}(points, faces) +function connect(elements::AbstractVector, faces::AbstractVector{<: AbstractFace}) + return [elements[i] for f in faces for i in f] end - -coordinates(mesh::FaceView) = getfield(mesh, :elements) -faces(mesh::FaceView) = getfield(mesh, :faces) diff --git a/test/fixed_arrays.jl b/test/fixed_arrays.jl index f9c16b9e..256d5f31 100644 --- a/test/fixed_arrays.jl +++ b/test/fixed_arrays.jl @@ -6,12 +6,57 @@ using Test end @testset "broadcast" begin - @testset for T in (Vec, Point) - x = [T(2, 3), T(7, 3)] + foo(a, b) = (a + b) * (a - b) + M1 = Mat{2, 2}(1,2,3,4) + M2 = Mat{2, 2}(2,2,1,1) - @test [T(4, 9), T(14, 9)] == x .* T(2, 3) - @test [T(4, 6), T(9, 6)] == x .+ T(2, 3) - @test [T(0, 0), T(5, 0)] == x .- T(2, 3) + @testset "with similar" begin + for T1 in (Vec, Point, tuple) + for T2 in (Vec, Point, tuple) + T1 == tuple && T2 == tuple && continue + T = ifelse(T1 == Point, Point, ifelse(T2 == Point, Point, Vec)) + @test T(2, 2, 4) == T1(1,2,3) .+ T2(1, 0, 1) + @test T(foo.((1,2,3), (1, 0, 1))) == foo.(T1(1,2,3), T2(1, 0, 1)) + end + end + + @test Mat{2, 2}(3,4,4,5) == M1 .+ M2 + @test Mat{2, 2}(foo.(values(M1), values(M2))) == foo.(M1, M2) + end + + @testset "with const" begin + for T in (Vec, Point) + @test T(-4, -3) == T(1,2) .- 5 + @test T(foo.((1,2), 5)) == foo.(T(1,2), 5) + end + + @test Mat{2, 2}(2,3,4,5) == M1 .+ 1 + @test Mat{2, 2}(foo.(values(M1), 1)) == foo.(M1, 1) + end + + @testset "chained/nested" begin + for T1 in (Vec, Point, tuple) + for T2 in (Vec, Point, tuple) + T1 == tuple && T2 == tuple && continue + T = ifelse(T1 == Point, Point, ifelse(T2 == Point, Point, Vec)) + @test T(-6, -4, 4) == T1(1,2,3) .+ T2(1, 0, 1) .- T2(3, 2, 1) .* T1(2,2,0) .- T2(2,2,0) + @test T(-15, -5) == foo.(T1(1,2), T1(-1, 0) .+ foo.(T1(1,1), T2(2,2))) + end + end + end + + @testset "Longer functions" begin + foo2(a, b, c, d) = (a + b) * (c + d) + for T1 in (Vec, Point, tuple) + for T2 in (Vec, Point, tuple) + T1 == tuple && T2 == tuple && continue + T = ifelse(T1 == Point, Point, ifelse(T2 == Point, Point, Vec)) + @test T(foo2.((1,2,3), (1, 0, 1), (3, 2, 1), (2,2,0))) == + foo2.(T1(1,2,3), T2(1, 0, 1), T2(3, 2, 1), T2(2,2,0)) + end + end + + @test Mat{2, 2}(foo2.(values.((M1, M1, M2, M2))...)) == foo2.(M1, M1, M2, M2) end end @@ -31,3 +76,51 @@ end end end end + +@testset "Mat" begin + M3 = Mat3(1,2,3, 4,5,6, 7,8,9) + @test M3 isa Mat{3,3,Int,9} + + @testset "indexing" begin + for i in 1:9 + @test getindex(M3, i) == i + end + @test_throws BoundsError getindex(M3, 0) + @test_throws BoundsError getindex(M3, 10) + + # Sanity check for loop + # @test M3[2, Vec(1,2)] == Mat{1, 2}(M3[2,1], M3[2,2]) + @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 + 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] + 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/geointerface.jl b/test/geointerface.jl index a304a5bb..31c7de38 100644 --- a/test/geointerface.jl +++ b/test/geointerface.jl @@ -97,8 +97,10 @@ end @test point_gb === Point{2,Float32}(30.1, 10.1) @test point_3d_gb === Point{3,Float32}(30.1, 10.1, 5.1) @test linestring_gb isa LineString - @test length(linestring_gb) == 2 - @test eltype(linestring_gb) == Line{2,Float32} + # TODO, what should we do exactly with linestrings? + # @test length(linestring_gb) == 2 + # @test eltype(linestring_gb) == Line{2, Float64} + @test polygon_gb isa Polygon @test isempty(polygon_gb.interiors) @test polygon_hole_gb isa Polygon diff --git a/test/geometrytypes.jl b/test/geometrytypes.jl index 93d7a511..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,80 +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 - @test m isa GLNormalMesh + + 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 @@ -128,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 @@ -225,7 +248,7 @@ end h1 = Rect(0.0, 0.0, 1.0, 1.0) h2 = Rect(1.0, 1.0, 2.0, 2.0) @test union(h1, h2) isa GeometryBasics.HyperRectangle{2,Float64} - @test GeometryBasics.diff(h1, h2) == h1 + # @test GeometryBasics.diff(h1, h2) == h1 @test GeometryBasics.intersect(h1, h2) isa GeometryBasics.HyperRectangle{2,Float64} b = Rect(0.0, 0.0, 1.0, 1.0) diff --git a/test/meshes.jl b/test/meshes.jl new file mode 100644 index 00000000..4c42cb3e --- /dev/null +++ b/test/meshes.jl @@ -0,0 +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 "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.expand_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.expand_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.expand_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.expand_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 new file mode 100644 index 00000000..a9f1820e --- /dev/null +++ b/test/polygons.jl @@ -0,0 +1,31 @@ +@testset "Polygon" begin + @testset "from points" begin + points = connect([1, 2, 3, 4, 5, 6], Point2f) + polygon = Polygon(points) + @test polygon == Polygon(points) + end + + 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)] + + # triangulation is inconsistent... + @test length(faces(poly1)) == 11 + ps = vcat(decompose(Point2f, rect), decompose(Point2f, hole)) + @test coordinates(poly1) == ps + + fs = GeometryBasics.earcut_triangulate([poly1.exterior[[1, 2, 3, 4, 1]]]) + @test fs == GLTriangleFace[(4,1,2), (2,3,4)] + + 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 4ef2f034..2c0170f9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,13 +1,12 @@ -using Test, Random, StructArrays, Tables, StaticArrays, OffsetArrays +using Test, Random, OffsetArrays using GeometryBasics using LinearAlgebra -using GeometryBasics: attributes +using GeometryBasics: MetaMesh using GeoInterface using GeoJSON using Extents @testset "GeometryBasics" begin - @testset "algorithms" begin cube = Rect(Vec3f(-0.5), Vec3f(1)) cube_faces = decompose(TriangleFace{Int}, faces(cube)) @@ -28,238 +27,32 @@ using Extents points3d = Point3f[(0,0,0), (0,0,1), (0,1,1)] @test area(OffsetArray(points3d, -2)) ≈ 0.5 - - pm2d = [PointMeta(0.0, 0.0, a=:d), PointMeta(0.0, 1.0, a=:e), PointMeta(1.0, 0.0, a=:f)] - @test area(pm2d) ≈ -0.5 - - pm3d = [PointMeta(0.0, 0.0, 0.0, a=:d), PointMeta(0.0, 1.0, 0.0, a=:e), PointMeta(1.0, 0.0, 0.0, a=:f)] - @test_broken area(pm3d) ≈ 0.5 # Currently broken as zero(PointMeta(0.0, 0.0, 0.0, a=:d)) fails end @testset "embedding metadata" begin @testset "Meshes" begin - @testset "per vertex attributes" begin points = rand(Point{3, Float64}, 8) tfaces = TetrahedronFace{Int}[(1, 2, 3, 4), (5, 6, 7, 8)] - normals = rand(SVector{3, Float64}, 8) + normals = rand(Vec{3, Float64}, 8) stress = LinRange(0, 1, 8) - mesh = Mesh(meta(points, normals = normals, stress = stress), tfaces) + mesh = Mesh(points, tfaces; normal = normals, stress = stress) - @test hasproperty(coordinates(mesh), :stress) - @test hasproperty(coordinates(mesh), :normals) - @test coordinates(mesh).stress === stress - @test coordinates(mesh).normals === normals - @test coordinates(mesh).normals === normals + @test hasproperty(mesh, :stress) + @test hasproperty(mesh, :normal) + @test mesh.stress === stress + @test mesh.normal === normals + @test mesh.position === points @test GeometryBasics.faces(mesh) === tfaces - @test propertynames(coordinates(mesh)) == (:position, :normals, :stress) - + @test propertynames(mesh) == (:vertex_attributes, :faces, :views, :position, :normal, :stress) end - - @testset "per face attributes" begin - - # Construct a cube out of Quads - points = Point{3, Float64}[ - (0.0, 0.0, 0.0), (2.0, 0.0, 0.0), - (2.0, 2.0, 0.0), (0.0, 2.0, 0.0), - (0.0, 0.0, 12.0), (2.0, 0.0, 12.0), - (2.0, 2.0, 12.0), (0.0, 2.0, 12.0) - ] - - facets = QuadFace{Cint}[ - 1:4, - 5:8, - [1,5,6,2], - [2,6,7,3], - [3, 7, 8, 4], - [4, 8, 5, 1] - ] - - markers = Cint[-1, -2, 0, 0, 0, 0] - # attach some additional information to our faces! - mesh = Mesh(points, meta(facets, markers = markers)) - @test hasproperty(GeometryBasics.faces(mesh), :markers) - # test with === to assert we're not doing any copies - @test GeometryBasics.faces(mesh).markers === markers - @test coordinates(mesh) === points - @test metafree(GeometryBasics.faces(mesh)) === facets - - end - - end - - @testset "polygon with metadata" begin - polys = [Polygon(rand(Point{2, Float32}, 20)) for i in 1:10] - pnames = [randstring(4) for i in 1:10] - numbers = LinRange(0.0, 1.0, 10) - bin = rand(Bool, 10) - # create a polygon - poly = PolygonMeta(polys[1], name = pnames[1], value = numbers[1], category = bin[1]) - # create a MultiPolygon with the right type & meta information! - multipoly = MultiPolygonMeta(polys, name = pnames, value = numbers, category = bin) - @test multipoly isa AbstractVector - @test poly isa GeometryBasics.AbstractPolygon - - @test GeometryBasics.getcolumn(poly, :name) == pnames[1] - @test GeometryBasics.MetaFree(PolygonMeta) == Polygon - - @test GeometryBasics.getcolumn(multipoly, :name) == pnames - @test GeometryBasics.MetaFree(MultiPolygonMeta) == MultiPolygon - - meta_p = meta(polys[1], boundingbox=Rect(0, 0, 2, 2)) - @test meta_p.boundingbox === Rect(0, 0, 2, 2) - @test metafree(meta_p) === polys[1] - attributes(meta_p) == Dict{Symbol, Any}(:boundingbox => meta_p.boundingbox, - :polygon => polys[1]) - end - @testset "point with metadata" begin - p = Point(1.1, 2.2) - @test p isa AbstractVector{Float64} - pm = PointMeta(1.1, 2.2; a=1, b=2) - p1 = Point(2.2, 3.6) - p2 = [p, p1] - @test coordinates(p2) == p2 - @test meta(pm) === (a=1, b=2) - @test metafree(pm) === p - @test propertynames(pm) == (:position, :a, :b) - @test GeometryBasics.MetaFree(typeof(pm)) == Point{2,Float64} - @test_broken zero(pm) == [0, 0] - end - - @testset "MultiPoint with metadata" begin - p = collect(Point{2, Float64}(x, x+1) for x in 1:5) - @test p isa AbstractVector - mpm = MultiPointMeta(p, a=1, b=2) - @test coordinates(mpm) == mpm - @test meta(mpm) === (a=1, b=2) - @test metafree(mpm) == p - @test propertynames(mpm) == (:points, :a, :b) - end - - @testset "LineString with metadata" begin - linestring = LineStringMeta(Point{2, Int}[(10, 10), (20, 20), (10, 40)], a = 1, b = 2) - @test linestring isa AbstractVector - @test meta(linestring) === (a = 1, b = 2) - @test metafree(linestring) == linestring - @test propertynames(linestring) == (:lines, :a, :b) - end - - @testset "MultiLineString with metadata" begin - linestring1 = LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]) - linestring2 = LineString(Point{2, Int}[(40, 40), (30, 30), (40, 20), (30, 10)]) - multilinestring = MultiLineString([linestring1, linestring2]) - multilinestringmeta = MultiLineStringMeta([linestring1, linestring2]; boundingbox = Rect(1.0, 1.0, 2.0, 2.0)) - @test multilinestringmeta isa AbstractVector - @test meta(multilinestringmeta) === (boundingbox = Rect(1.0, 1.0, 2.0, 2.0),) - @test metafree(multilinestringmeta) == multilinestring - @test propertynames(multilinestringmeta) == (:linestrings, :boundingbox) end @testset "Mesh with metadata" begin m = triangle_mesh(Sphere(Point3f(0), 1)) - m_meta = MeshMeta(m; boundingbox=Rect(1.0, 1.0, 2.0, 2.0)) - @test meta(m_meta) === (boundingbox = Rect(1.0, 1.0, 2.0, 2.0),) - @test metafree(m_meta) === m - @test propertynames(m_meta) == (:mesh, :boundingbox) - end -end - -@testset "embedding MetaT" begin - @testset "MetaT{Polygon}" begin - polys = [Polygon(rand(Point{2, Float32}, 20)) for i in 1:10] - multipol = MultiPolygon(polys) - pnames = [randstring(4) for i in 1:10] - numbers = LinRange(0.0, 1.0, 10) - bin = rand(Bool, 10) - # create a polygon - poly = MetaT(polys[1], name = pnames[1], value = numbers[1], category = bin[1]) - # create a MultiPolygon with the right type & meta information! - multipoly = MetaT(multipol, name = pnames, value = numbers, category = bin) - @test multipoly isa MetaT - @test poly isa MetaT - - @test GeometryBasics.getcolumn(poly, :name) == pnames[1] - @test GeometryBasics.getcolumn(multipoly, :name) == pnames - - meta_p = MetaT(polys[1], boundingbox=Rect(0, 0, 2, 2)) - @test meta_p.boundingbox === Rect(0, 0, 2, 2) - @test GeometryBasics.metafree(meta_p) == polys[1] - @test GeometryBasics.metafree(poly) == polys[1] - @test GeometryBasics.metafree(multipoly) == multipol - @test GeometryBasics.meta(meta_p) == (boundingbox = GeometryBasics.HyperRectangle{2,Int64}([0, 0], [2, 2]),) - @test GeometryBasics.meta(poly) == (name = pnames[1], value = 0.0, category = bin[1]) - @test GeometryBasics.meta(multipoly) == (name = pnames, value = numbers, category = bin) - end - - @testset "MetaT{Point}" begin - p = Point(1.1, 2.2) - @test p isa AbstractVector{Float64} - pm = MetaT(Point(1.1, 2.2); a=1, b=2) - p1 = Point(2.2, 3.6) - p2 = [p, p1] - @test coordinates(p2) == p2 - @test pm.meta === (a=1, b=2) - @test pm.main === p - @test propertynames(pm) == (:main, :a, :b) - @test GeometryBasics.metafree(pm) == p - @test GeometryBasics.meta(pm) == (a = 1, b = 2) - end - - @testset "MetaT{MultiPoint}" begin - p = collect(Point{2, Float64}(x, x+1) for x in 1:5) - @test p isa AbstractVector - mpm = MetaT(MultiPoint(p); a=1, b=2) - @test coordinates(mpm.main) == Point{2, Float64}[(x, x+1) for x in 1:5] - @test mpm.meta === (a=1, b=2) - @test mpm.main == p - @test propertynames(mpm) == (:main, :a, :b) - @test GeometryBasics.metafree(mpm) == p - @test GeometryBasics.meta(mpm) == (a = 1, b = 2) - end - - @testset "MetaT{LineString}" begin - linestring = MetaT(LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]), a = 1, b = 2) - @test linestring isa MetaT - @test linestring.meta === (a = 1, b = 2) - @test propertynames(linestring) == (:main, :a, :b) - @test GeometryBasics.metafree(linestring) == LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]) - @test GeometryBasics.meta(linestring) == (a = 1, b = 2) - end - - @testset "MetaT{MultiLineString}" begin - linestring1 = LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]) - linestring2 = LineString(Point{2, Int}[(40, 40), (30, 30), (40, 20), (30, 10)]) - multilinestring = MultiLineString([linestring1, linestring2]) - multilinestringmeta = MetaT(MultiLineString([linestring1, linestring2]); boundingbox = Rect(1.0, 1.0, 2.0, 2.0)) - @test multilinestringmeta isa MetaT - @test multilinestringmeta.meta === (boundingbox = Rect(1.0, 1.0, 2.0, 2.0),) - @test multilinestringmeta.main == multilinestring - @test propertynames(multilinestringmeta) == (:main, :boundingbox) - @test GeometryBasics.metafree(multilinestringmeta) == multilinestring - @test GeometryBasics.meta(multilinestringmeta) == (boundingbox = GeometryBasics.HyperRectangle{2,Float64}([1.0, 1.0], [2.0, 2.0]),) - end - - #= - So mesh works differently for MetaT - since `MetaT{Point}` not subtyped to `AbstractPoint` - =# - - @testset "MetaT{Mesh}" begin - @testset "per vertex attributes" begin - points = rand(Point{3, Float64}, 8) - tfaces = TetrahedronFace{Int}[(1, 2, 3, 4), (5, 6, 7, 8)] - normals = rand(SVector{3, Float64}, 8) - stress = LinRange(0, 1, 8) - mesh_nometa = Mesh(points, tfaces) - mesh = MetaT(mesh_nometa, normals = normals, stress = stress) - - @test hasproperty(mesh, :stress) - @test hasproperty(mesh, :normals) - @test mesh.stress == stress - @test mesh.normals == normals - @test GeometryBasics.faces(mesh.main) == tfaces - @test propertynames(mesh) == (:main, :normals, :stress) - end + 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 collect(keys(m_meta)) == [:boundingbox,] end end @@ -310,21 +103,6 @@ end end end - @testset "face views" begin - numbers = [1, 2, 3, 4, 5, 6] - points = connect(numbers, Point{2}) - faces = connect([1, 2, 3], TriangleFace) - triangles = connect(points, faces) - @test triangles == [Triangle(Point(1, 2), Point(3, 4), Point(5, 6))] - x = Point{3}(1.0) - triangles = connect([x], [TriangleFace(1, 1, 1)]) - @test triangles == [Triangle(x, x, x)] - points = connect([1, 2, 3, 4, 5, 6, 7, 8], Point{2}) - faces = connect([1, 2, 3, 4], SimplexFace{4}) - triangles = connect(points, faces) - @test triangles == [Tetrahedron(points...)] - end - @testset "reinterpret" begin numbers = collect(reshape(1:6, 2, 3)) points = reinterpret(Point{2, Int}, numbers) @@ -336,125 +114,40 @@ end end @testset "constructors" begin - @testset "LineFace" begin - - points = connect([1, 2, 3, 4, 5, 6], Point{2}) - linestring = LineString(points) - @test linestring == [Line(points[1], points[2]), Line(points[2], points[3])] - - points = rand(Point{2, Float64}, 4) - linestring = LineString(points, 2) - @test linestring == [Line(points[1], points[2]), Line(points[3], points[4])] - - linestring = LineString([points[1] => points[2], points[2] => points[3]]) - @test linestring == [Line(points[1], points[2]), Line(points[2], points[3])] - - faces = [1, 2, 3] - linestring = LineString(points, faces) - @test linestring == LineString([points[1] => points[2], points[2] => points[3]]) - a, b, c, d = Point(1, 2), Point(3, 4), Point(5, 6), Point(7, 8) - points = [a, b, c, d]; faces = [1, 2, 3, 4] - linestring = LineString(points, faces, 2) - @test linestring == LineString([a => b, c => d]) - - faces = [LineFace(1, 2) - , LineFace(3, 4)] - linestring = LineString(points, faces) - @test linestring == LineString([a => b, c => d]) - end - - @testset "Polygon" begin - - points = connect([1, 2, 3, 4, 5, 6], Point{2}) - polygon = Polygon(points) - @test polygon == Polygon(LineString(points)) - - points = rand(Point{2, Float64}, 4) - linestring = LineString(points, 2) - @test Polygon(points, 2) == Polygon(linestring) - - faces = [1, 2, 3] - polygon = Polygon(points, faces) - @test polygon == Polygon(LineString(points, faces)) - - a, b, c, d = Point(1, 2), Point(3, 4), Point(5, 6), Point(7, 8) - points = [a, b, c, d]; faces = [1, 2, 3, 4] - polygon = Polygon(points, faces, 2) - @test polygon == Polygon(LineString(points, faces, 2)) - - faces = [LineFace(1, 2), LineFace(3, 4)] - polygon = Polygon(points, faces) - @test polygon == Polygon(LineString(points, faces)) - @test ndims(polygon) === 2 - end - @testset "Mesh" begin - numbers = [1, 2, 3, 4, 5, 6] points = connect(numbers, Point{2}) mesh = Mesh(points, [1,2,3]) - @test mesh == [Triangle(points...)] + @test faces(mesh) == [TriangleFace(1, 2, 3)] x = Point{3}(1.0) mesh = Mesh([x], [TriangleFace(1, 1, 1)]) - @test mesh == [Triangle(x, x, x)] + @test coordinates(mesh) == [x] + @test faces(mesh) == [TriangleFace(1, 1, 1)] points = connect([1, 2, 3, 4, 5, 6, 7, 8], Point{2}) - faces = connect([1, 2, 3, 4], SimplexFace{4}) - mesh = Mesh(points, faces) - @test mesh == [Tetrahedron(points...)] - + f = connect([1, 2, 3, 4], SimplexFace{4}) + mesh = Mesh(points, f) + @test collect(mesh) == [Tetrahedron(points...)] + @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)] - normals = rand(Vec3f, 8) + ns = rand(Vec3f, 8) uv = rand(Vec2f, 8) mesh = Mesh(points, tfaces) - meshuv = Mesh(meta(points; uv=uv), tfaces) - meshuvnormal = Mesh(meta(points; normals=normals, uv=uv), tfaces) - - @test mesh isa GLPlainMesh - @test meshuv isa GLUVMesh3D - @test meshuvnormal isa GLNormalUVMesh3D - + meshuv = MetaMesh(points, tfaces; 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}) + + m = GeometryBasics.mesh(t; pointtype=Point3f, facetype=QuadFace{Int}) m2 = GeometryBasics.mesh(m, facetype=QuadFace{GLIndex}) @test GeometryBasics.faces(m2) isa Vector{QuadFace{GLIndex}} @test GeometryBasics.coordinates(m2) isa Vector{Point3f} - end - - @testset "Multi geometries" begin - # coordinates from https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry#Geometric_objects - points = Point{2, Int}[(10, 40), (40, 30), (20, 20), (30, 10)] - multipoint = MultiPoint(points) - @test size(multipoint) === size(points) - @test multipoint[3] === points[3] - - linestring1 = LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]) - linestring2 = LineString(Point{2, Int}[(40, 40), (30, 30), (40, 20), (30, 10)]) - multilinestring = MultiLineString([linestring1, linestring2]) - @test size(multilinestring) === (2,) - @test multilinestring[1] === linestring1 - @test multilinestring[2] === linestring2 - - polygon11 = Polygon(Point{2, Int}[(30, 20), (45, 40), (10, 40), (30, 20)]) - polygon12 = Polygon(Point{2, Int}[(15, 5), (40, 10), (10, 20), (5, 10), (15, 5)]) - multipolygon1 = MultiPolygon([polygon11, polygon12]) - @test size(multipolygon1) === (2,) - @test multipolygon1[1] === polygon11 - @test multipolygon1[2] === polygon12 - - polygon21 = Polygon(Point{2, Int}[(40, 40), (20, 45), (45, 30), (40, 40)]) - polygon22 = Polygon(LineString(Point{2, Int}[(20, 35), (10, 30), (10, 10), (30, 5), (45, 20), (20, 35)]), - [LineString(Point{2, Int}[(30, 20), (20, 15), (20, 25), (30, 20)])]) - multipolygon2 = MultiPolygon([polygon21, polygon22]) - @test size(multipolygon2) === (2,) - @test multipolygon2[1] === polygon21 - @test multipolygon2[2] === polygon22 - end - end @testset "decompose/triangulation" begin @@ -477,118 +170,48 @@ end primitive = Sphere(Point3f(0), 1) m_normal = normal_mesh(primitive) - @test normals(m_normal) isa Vector{Vec3f} + @test GeometryBasics.normals(m_normal) isa Vector{Vec3f} primitive = Rect2(0, 0, 1, 1) m_normal = normal_mesh(primitive) - @test normals(m_normal) isa Vector{Vec3f} + @test GeometryBasics.normals(m_normal) isa Vector{Vec3f} primitive = Rect3(0, 0, 0, 1, 1, 1) m_normal = normal_mesh(primitive) - @test normals(m_normal) isa Vector{Vec3f} + @test GeometryBasics.normals(m_normal) isa GeometryBasics.FaceView{Vec3f, Vector{Vec3f}, Vector{GLTriangleFace}} points = decompose(Point2f, Circle(Point2f(0), 1)) tmesh = triangle_mesh(points) - @test normals(tmesh) == nothing + @test GeometryBasics.normals(tmesh) == nothing m = GeometryBasics.mesh(Sphere(Point3f(0), 1)) - @test normals(m) == nothing - m_normals = pointmeta(m, Normal()) - @test normals(m_normals) isa Vector{Vec3f} + @test GeometryBasics.normals(m) == nothing + m_normals = decompose_normals(m) + @test m_normals isa Vector{Vec3f} @test texturecoordinates(m) == nothing r2 = Rect2(0.0, 0.0, 1.0, 1.0) - @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 - linestring = LineString(Point{2, Int}[(10, 10), (20, 20), (10, 40)]) - pts = Point{2, Int}[(10, 10), (20, 20), (10, 40)] - linestring = LineString(pts) - pts_decomp = decompose(Point{2, Int}, linestring) - @test pts === pts_decomp - - pts_ext = Point{2, Int}[(5, 1), (3, 3), (4, 8), (1, 2), (5, 1)] - ls_ext = LineString(pts_ext) - pts_int1 = Point{2, Int}[(2, 2), (3, 8),(5, 6), (3, 4), (2, 2)] - ls_int1 = LineString(pts_int1) - pts_int2 = Point{2, Int}[(3, 2), (4, 5),(6, 1), (1, 4), (3, 2)] - ls_int2 = LineString(pts_int2) - poly_ext = Polygon(ls_ext) - poly_ext_int = Polygon(ls_ext, [ls_int1, ls_int2]) - @test decompose(Point{2, Int}, poly_ext) == pts_ext - @test decompose(Point{2, Int}, poly_ext_int) == [pts_ext..., pts_int1..., pts_int2...] -end - -@testset "mesh" begin - primitive = Triangle(Point2f(0), Point2f(1), Point2f(1,0)) - m = GeometryBasics.mesh(primitive) - @test length(faces(m)) == 1 + 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 we dont actually calculate normals - @test !hasproperty(m, :normals) -end - -@testset "convert mesh + meta" begin - m = uv_normal_mesh(Rect3f(Vec3f(-1), Vec3f(1, 2, 3))) - m_normal = normal_mesh(m) - # make sure we don't loose the uv - @test hasproperty(m_normal, :uv) - @test m == m_normal - # Make sure we don't create any copies - @test m.position === m_normal.position - @test m.normals === m_normal.normals - @test m.uv === m_normal.uv - - m = GeometryBasics.mesh(Rect3f(Vec3f(-1), Vec3f(1, 2, 3)); - uv=Vec2{Float64}, normaltype=Vec3{Float64}, pointtype=Point3{Float64}) - m_normal = normal_mesh(m) - @test hasproperty(m_normal, :uv) - @test m.position !== m_normal.position - @test m.normals !== m_normal.normals - # 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 = GeometryBasics.Mesh(meta(points, xx=xx), GLTriangleFace[(1,2,3), (3,4,5)]) - color = rand(10) - m = pointmeta(m; color=color) - - @test hasproperty(m, :xx) - @test hasproperty(m, :color) - @test_throws ErrorException GeometryBasics.MetaType(Simplex) - @test_throws ErrorException GeometryBasics.MetaFree(Simplex) - - - @test m.xx === xx - @test m.color === color - - m, colpopt = GeometryBasics.pop_pointmeta(m, :color) - m, xxpopt = GeometryBasics.pop_pointmeta(m, :xx) - - @test propertynames(m) == (:position,) - @test colpopt === color - @test xxpopt === xx - - @testset "creating meta" begin - x = Point3f[(1,3,4)] - # no meta gets added, so should stay the same - @test meta(x) === x - @test meta(x, value=[1]).position === x - end - pos = Point2f[(10, 2)] - m = Mesh(meta(pos, uv=[Vec2f(1, 1)]), [GLTriangleFace(1, 1, 1)]) - @test m.position === pos + # For 2d primitives normal is just the upvector + @test m.normal == [Vec3f(0, 0, 1) for p in coordinates(m)] end @testset "mesh conversion" begin @@ -601,13 +224,11 @@ end @test coordinates(m) === decompose(Point{3, Float64}, m) tmesh = triangle_mesh(m) - @test tmesh isa GLPlainMesh @test coordinates(tmesh) === decompose(Point3f, tmesh) nmesh = normal_mesh(m) - @test nmesh isa GLNormalMesh - @test metafree(coordinates(nmesh)) === decompose(Point3f, nmesh) - @test normals(nmesh) === decompose_normals(nmesh) + @test coordinates(nmesh) === decompose(Point3f, nmesh) + @test GeometryBasics.normals(nmesh) === decompose_normals(nmesh) m = GeometryBasics.mesh(s, pointtype=Point3f) @test m isa Mesh{3, Float32} @@ -657,10 +278,7 @@ end @test typeof(x) == OffsetInteger{0,Int64} x1 = OffsetInteger{0}(2) - @test GeometryBasics.pure_max(x, x1) == x1 - @test promote_rule(typeof(x), typeof(x1)) == OffsetInteger{0,Int64} x2 = 1 - @test promote_rule(typeof(x2), typeof(x1)) == Int64 @test Base.to_index(x1) == 2 @test -(x1) == OffsetInteger{0,Int64}(-2) @test abs(x1) == OffsetInteger{0,Int64}(2) @@ -676,56 +294,6 @@ end @test <(x, x1) end -@testset "MetaT and heterogeneous data" begin - ls = [LineString([Point(i, (i+1)^2/6), Point(i*0.86,i+5), Point(i/3, i/7)]) for i in 1:10] - mls = MultiLineString([LineString([Point(i+1, (i)^2/6), Point(i*0.75,i+8), Point(i/2.5, i/6.79)]) for i in 5:10]) - poly = Polygon(Point{2, Int}[(40, 40), (20, 45), (45, 30), (40, 40)]) - geom = [ls..., mls, poly] - prop = Any[(country_states = "India$(i)", rainfall = (i*9)/2) for i in 1:11] - push!(prop, (country_states = 12, rainfall = 1000)) # a pinch of heterogeneity - - feat = [MetaT(i, j) for (i,j) = zip(geom, prop)] - sa = meta_table(feat) - - @test nameof(eltype(feat)) == :MetaT - @test eltype(sa) === MetaT{Any,(:country_states, :rainfall),Tuple{Any,Float64}} - @test propertynames(sa) === (:main, :country_states, :rainfall) - @test getproperty(sa, :country_states) isa Array{Any} - @test getproperty(sa, :main) == geom - - maintype, metanames, metatype = GeometryBasics.getnamestypes(typeof(feat[1])) - @test (metanames, metatype) == ((:country_states, :rainfall), Tuple{String,Float64}) - - - @test StructArrays.createinstance(typeof(feat[1]), LineString([Point(1, (2)^2/6), Point(1*0.86,6), Point(1/3, 1/7)]), "Mumbai", 100) isa typeof(feat[1]) - - @test Base.getindex(feat[1], 1) isa Line - @test Base.size(feat[1]) == (2,) -end - -@testset "StructArrays integration" begin - pt = meta(Point(0.0, 0.0), color="red", alpha=0.1) - @test StructArrays.component(pt, :position) == Point(0.0, 0.0) - @test StructArrays.component(pt, :color) == "red" - @test StructArrays.component(pt, :alpha) == 0.1 - @test StructArrays.staticschema(typeof(pt)) == - NamedTuple{(:position, :color, :alpha), Tuple{Point2{Float64}, String, Float64}} - @test StructArrays.createinstance(typeof(pt), Point(0.0, 0.0), "red", 0.1) == pt - - s = StructArray([pt, pt]) - @test StructArrays.components(s) == ( - position = [Point(0.0, 0.0), Point(0.0, 0.0)], - color = ["red", "red"], - alpha = [0.1, 0.1] - ) - s[2] = meta(Point(0.1, 0.1), color="blue", alpha=0.3) - @test StructArrays.components(s) == ( - position = [Point(0.0, 0.0), Point(0.1, 0.1)], - color = ["red", "blue"], - alpha = [0.1, 0.3] - ) -end - @testset "Tests from GeometryTypes" begin include("geometrytypes.jl") end @@ -734,13 +302,27 @@ end include("fixed_arrays.jl") end +@testset "Some mesh issues" begin + include("meshes.jl") +end + @testset "GeoInterface" begin include("geointerface.jl") end +include("polygons.jl") + using Aqua # Aqua tests # Intervals brings a bunch of ambiquities unfortunately -Aqua.test_all(GeometryBasics; ambiguities=false) +# seems like we also run into https://github.com/JuliaTesting/Aqua.jl/issues/86 +# Aqua.test_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"