Files
EVEngine/test/graphics_primitives.cpp

738 lines
30 KiB
C++

#include "graphics/Batcher.h"
#include "graphics/PrimitiveDrawList.h"
#include "graphics/PrimitivePath.h"
#include "graphics/PrimitiveScene.h"
#include "graphics/PrimitiveTessellator.h"
#include "graphics/PrimitiveTypes.h"
#include "zeroerr/assert.h"
#include "zeroerr/unittest.h"
#include <glm/gtc/constants.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <algorithm>
#include <array>
#include <cmath>
using namespace eve::graphics;
TEST_CASE("GraphicsPrimitives.dashPatternOwnsAndValidatesIntervals") {
DashPattern dash{{4.f, 2.f, 1.f, 3.f}, 0.5f, DashSpace::WorldUnits};
CHECK(std::fabs(dash.period() - 10.f) < 1e-5f);
dash.intervals = {1.f, 2.f, 3.f};
CHECK_THROWS((dash.validate(), false));
dash.intervals = {1.f, 0.f};
CHECK_THROWS((dash.validate(), false));
}
TEST_CASE("GraphicsPrimitives.canvas2DRecordsContinuousPolylineDistance") {
PrimitiveCanvas2D canvas;
PrimitivePaint paint;
paint.mode = PaintMode::Stroke;
paint.stroke.dash = DashPattern{{3.f, 2.f}, 0.f, DashSpace::ScreenPixels};
std::array points{glm::vec2{0.f, 0.f}, glm::vec2{3.f, 4.f}, glm::vec2{6.f, 4.f}};
canvas.drawPolyline(points, false, paint);
REQUIRE_EQ(canvas.commands().size(), 1u);
const auto& command = canvas.commands().front();
REQUIRE_EQ(command.cumulativeLengths.size(), 3u);
CHECK(std::fabs(command.cumulativeLengths[1] - 5.f) < 1e-5f);
CHECK(std::fabs(command.cumulativeLengths[2] - 8.f) < 1e-5f);
CHECK_EQ(canvas.statistics().segmentCount, 2u);
}
TEST_CASE("GraphicsPrimitives.closed3DPolylineIncludesClosingDistance") {
SceneDrawContext context;
context.viewportSize = {1280, 720};
PrimitiveSceneCanvas3D canvas(context);
ScenePrimitivePaint paint;
paint.mode = PaintMode::Stroke;
std::array points{glm::vec3{0.f, 0.f, 0.f}, glm::vec3{3.f, 0.f, 0.f}, glm::vec3{3.f, 4.f, 0.f}};
canvas.drawPolyline(points, true, paint);
const auto& command = canvas.commands().front();
REQUIRE_EQ(command.cumulativeWorldLengths.size(), 4u);
CHECK(std::fabs(command.cumulativeWorldLengths.back() - 12.f) < 1e-5f);
CHECK_EQ(canvas.statistics().segmentCount, 3u);
}
TEST_CASE("GraphicsPrimitives.canvasStateSnapshotsTransforms") {
SceneDrawContext context;
context.viewportSize = {640, 480};
PrimitiveSceneCanvas3D canvas(context);
ScenePrimitivePaint paint;
canvas.save();
canvas.concat(glm::translate(glm::mat4(1.f), glm::vec3(2.f, 3.f, 4.f)));
canvas.drawLine({0.f, 0.f, 0.f}, {1.f, 0.f, 0.f}, paint);
canvas.restore();
canvas.drawLine({0.f, 0.f, 0.f}, {1.f, 0.f, 0.f}, paint);
REQUIRE_EQ(canvas.commands().size(), 2u);
CHECK(std::fabs(canvas.commands()[0].transform[3].x - 2.f) < 1e-5f);
CHECK(std::fabs(canvas.commands()[1].transform[3].x) < 1e-5f);
}
TEST_CASE("GraphicsPrimitives.rejectsInvalidContextAndCapacity") {
SceneDrawContext context;
context.viewportSize = {0, 720};
CHECK_THROWS((PrimitiveSceneCanvas3D(context), false));
PrimitiveCanvas2D canvas(1);
PrimitivePaint paint;
canvas.drawLine({0.f, 0.f}, {1.f, 1.f}, paint);
CHECK_THROWS((canvas.drawLine({1.f, 1.f}, {2.f, 2.f}, paint), false));
}
TEST_CASE("GraphicsPrimitives.explicitRecordingReportsBudgetWithoutPartialMutation") {
PrimitivePaint paint;
paint.mode = PaintMode::Stroke;
PrimitiveCanvas2D canvas(1);
const std::array first{glm::vec2{0.f, 0.f}, glm::vec2{1.f, 0.f}};
auto recorded = canvas.tryDrawPolyline(first, false, paint);
REQUIRE(recorded.ok());
const std::array second{glm::vec2{0.f, 1.f}, glm::vec2{1.f, 1.f}};
auto rejected = canvas.tryDrawPolyline(second, false, paint);
CHECK(!rejected.ok());
REQUIRE(rejected.error() != nullptr);
CHECK_EQ(static_cast<int>(rejected.error()->code()), static_cast<int>(eve::DiagnosticCode::Failed));
CHECK_EQ(canvas.commands().size(), 1u);
CHECK_EQ(canvas.statistics().droppedCommands, 1u);
SceneDrawContext context;
context.viewportSize = {640, 480};
PrimitiveSceneCanvas3D sceneCanvas(context, 1);
ScenePrimitivePaint scenePaint;
scenePaint.mode = PaintMode::Stroke;
const std::array line{glm::vec3{0.f, 0.f, -2.f}, glm::vec3{1.f, 0.f, -2.f}};
auto sceneRecorded = sceneCanvas.tryDrawPolyline(line, false, scenePaint);
REQUIRE(sceneRecorded.ok());
auto sceneRejected = sceneCanvas.tryDrawPolyline(line, false, scenePaint);
CHECK(!sceneRejected.ok());
CHECK_EQ(sceneCanvas.commands().size(), 1u);
CHECK_EQ(sceneCanvas.statistics().droppedCommands, 1u);
}
TEST_CASE("GraphicsPrimitives.pathFlattensBezierAndPreservesClosedContour") {
Path2D path;
path.moveTo({0.f, 0.f}).quadTo({5.f, 10.f}, {10.f, 0.f}).cubicTo({12.f, -4.f}, {18.f, -4.f}, {20.f, 0.f}).close();
const auto coarse = path.flatten(2.f);
const auto fine = path.flatten(0.05f);
REQUIRE_EQ(coarse.size(), 1u);
CHECK(coarse.front().closed);
CHECK(fine.front().points.size() > coarse.front().points.size());
CHECK(std::fabs(fine.front().points.front().x) < 1e-5f);
CHECK(std::fabs(fine.front().points.back().x - 20.f) < 1e-5f);
}
TEST_CASE("GraphicsPrimitives.canvasPathUsesSharedContinuousDistanceModel") {
Path2D path;
path.moveTo({0.f, 0.f}).lineTo({4.f, 0.f}).lineTo({4.f, 3.f}).close();
PrimitiveCanvas2D canvas;
PrimitivePaint paint;
paint.mode = PaintMode::Stroke;
canvas.drawPath(path, paint);
REQUIRE_EQ(canvas.commands().size(), 1u);
CHECK(canvas.commands().front().closed);
CHECK(std::fabs(canvas.commands().front().cumulativeLengths.back() - 12.f) < 1e-5f);
}
TEST_CASE("GraphicsPrimitives.concaveClosedPathProducesEarClippedFill") {
Path2D path;
path.moveTo({0.f, 0.f}).lineTo({8.f, 0.f}).lineTo({8.f, 8.f}).lineTo({4.f, 3.f}).lineTo({0.f, 8.f}).close();
PrimitivePaint paint;
paint.mode = PaintMode::FillAndStroke;
paint.antialias = false;
PrimitiveCanvas2D canvas;
canvas.drawPath(path, paint);
CHECK_EQ(canvas.triangles().size(), 3u);
REQUIRE_EQ(canvas.commands().size(), 1u);
CHECK(canvas.commands().front().closed);
}
TEST_CASE("GraphicsPrimitives.pathFillRulesDistinguishNestedContourWinding") {
const auto appendSquare = [](Path2D& path, float minimum, float maximum, bool clockwise) {
if (clockwise)
path.moveTo({minimum, minimum})
.lineTo({minimum, maximum})
.lineTo({maximum, maximum})
.lineTo({maximum, minimum})
.close();
else
path.moveTo({minimum, minimum})
.lineTo({maximum, minimum})
.lineTo({maximum, maximum})
.lineTo({minimum, maximum})
.close();
};
const auto triangleArea = [](const PrimitiveCanvas2D& canvas) {
float area = 0.f;
for (const TriangleCommand2D& triangle : canvas.triangles()) {
const glm::vec2 ab = triangle.points[1] - triangle.points[0];
const glm::vec2 ac = triangle.points[2] - triangle.points[0];
area += std::fabs(ab.x * ac.y - ab.y * ac.x) * 0.5f;
}
return area;
};
PrimitivePaint fill;
fill.mode = PaintMode::Fill;
fill.antialias = false;
Path2D evenOdd;
appendSquare(evenOdd, 0.f, 10.f, false);
appendSquare(evenOdd, 3.f, 7.f, false);
evenOdd.setFillRule(PathFillRule::EvenOdd);
PrimitiveCanvas2D evenOddCanvas;
evenOddCanvas.drawPath(evenOdd, fill);
CHECK(std::fabs(triangleArea(evenOddCanvas) - 84.f) < 1e-4f);
Path2D nonZeroSolid;
appendSquare(nonZeroSolid, 0.f, 10.f, false);
appendSquare(nonZeroSolid, 3.f, 7.f, false);
nonZeroSolid.setFillRule(PathFillRule::NonZero);
PrimitiveCanvas2D solidCanvas;
solidCanvas.drawPath(nonZeroSolid, fill);
CHECK(std::fabs(triangleArea(solidCanvas) - 100.f) < 1e-4f);
Path2D nonZeroHole;
appendSquare(nonZeroHole, 0.f, 10.f, false);
appendSquare(nonZeroHole, 3.f, 7.f, true);
nonZeroHole.setFillRule(PathFillRule::NonZero);
PrimitiveCanvas2D holeCanvas;
holeCanvas.drawPath(nonZeroHole, fill);
CHECK(std::fabs(triangleArea(holeCanvas) - 84.f) < 1e-4f);
}
TEST_CASE("GraphicsPrimitives.pathRejectsVerbWithoutContour") {
Path2D path;
CHECK_THROWS((path.lineTo({1.f, 2.f}), false));
CHECK_THROWS((path.close(), false));
}
TEST_CASE("GraphicsPrimitives.spatialWireShapesSharePolylineCommands") {
SceneDrawContext context;
context.viewportSize = {800, 600};
PrimitiveSceneCanvas3D canvas(context);
ScenePrimitivePaint paint;
paint.mode = PaintMode::Stroke;
paint.stroke.dash = DashPattern{{0.5f, 0.25f}, 0.f, DashSpace::WorldUnits};
canvas.drawAabb({-1.f, -2.f, -3.f}, {1.f, 2.f, 3.f}, paint);
CHECK_EQ(canvas.commands().size(), 12u);
canvas.drawSphere({0.f, 0.f, 0.f}, 2.f, paint, 8);
CHECK_EQ(canvas.commands().size(), 15u);
CHECK_EQ(canvas.statistics().segmentCount, 36u);
for (const auto& command : canvas.commands()) {
CHECK(command.paint.stroke.dash.has_value());
}
}
TEST_CASE("GraphicsPrimitives.gridCylinderConeAndArrowProduceFiniteCommands") {
SceneDrawContext context;
context.viewportSize = {800, 600};
PrimitiveSceneCanvas3D canvas(context);
ScenePrimitivePaint paint;
paint.mode = PaintMode::Stroke;
canvas.drawGrid({0.f, 0.f, 0.f}, {4.f, 0.f, 0.f}, {0.f, 0.f, 3.f}, 4, 3, paint);
canvas.drawCylinder({0.f, 0.f, 0.f}, {0.f, 2.f, 0.f}, 0.5f, paint, 8);
canvas.drawCone({0.f, 2.f, 0.f}, {0.f, -1.f, 0.f}, 1.f, 0.5f, paint, 8);
canvas.drawArrow({0.f, 0.f, 0.f}, {2.f, 0.f, 0.f}, 0.5f, 0.2f, paint);
CHECK_EQ(canvas.statistics().droppedCommands, 0u);
CHECK(canvas.statistics().commandCount > 0u);
for (const auto& command : canvas.commands()) {
CHECK(std::isfinite(command.cumulativeWorldLengths.back()));
}
}
TEST_CASE("GraphicsPrimitives.resolve2DStrokeProducesPixelWidthQuad") {
PrimitiveCanvas2D canvas;
PrimitivePaint paint;
paint.mode = PaintMode::Stroke;
paint.stroke.width = 4.f;
paint.antialias = false;
canvas.drawLine({10.f, 20.f}, {30.f, 20.f}, paint);
const auto triangles = resolvePrimitiveStrokes2D(canvas, {100, 100});
REQUIRE_EQ(triangles.vertices.size(), 6u);
CHECK_EQ(triangles.statistics.triangleCount, 2u);
const float firstY = triangles.vertices[0].clipPosition.y;
const float oppositeY = triangles.vertices[2].clipPosition.y;
CHECK(std::fabs(std::fabs(oppositeY - firstY) - 0.08f) < 1e-5f);
CHECK_EQ(triangles.statistics.uploadBytes, triangles.vertices.size() * sizeof(PrimitiveTriangleVertex));
}
TEST_CASE("GraphicsPrimitives.coverageAaEmitsTransparentOnePixelFringe") {
PrimitivePaint paint;
paint.mode = PaintMode::Stroke;
paint.stroke.width = 4.f;
paint.antialias = true;
PrimitiveCanvas2D canvas;
canvas.drawLine({10.f, 20.f}, {30.f, 20.f}, paint);
const auto resolved = resolvePrimitiveStrokes2D(canvas, {100, 100});
CHECK_EQ(resolved.vertices.size(), 18u);
CHECK(std::any_of(resolved.vertices.begin(), resolved.vertices.end(),
[](const PrimitiveTriangleVertex& vertex) { return vertex.color.a == 0.f; }));
CHECK(std::any_of(resolved.vertices.begin(), resolved.vertices.end(),
[](const PrimitiveTriangleVertex& vertex) { return vertex.color.a == 1.f; }));
Batcher batch;
batch.addTriangle({0.f, 0.f}, {1.f, 0.f}, {0.f, 1.f}, Color(1.f), Color(1.f, 1.f, 1.f, 0.5f),
Color(1.f, 1.f, 1.f, 0.f));
REQUIRE_EQ(batch.vertices().size(), 3u);
CHECK_EQ(batch.vertices()[1].color.a, 0.5f);
CHECK_EQ(batch.vertices()[2].color.a, 0.f);
}
TEST_CASE("GraphicsPrimitives.filledContourCoverageAaUsesVertexAlphaFringe") {
PrimitiveCanvas2D canvas;
PrimitivePaint fill;
fill.mode = PaintMode::Fill;
fill.antialias = true;
canvas.drawRect({10.f, 10.f}, {30.f, 30.f}, fill);
CHECK_EQ(canvas.triangles().size(), 10u);
const auto resolved = resolvePrimitiveStrokes2D(canvas, {100, 100});
CHECK(std::any_of(resolved.vertices.begin(), resolved.vertices.end(),
[](const PrimitiveTriangleVertex& vertex) { return vertex.color.a == 0.f; }));
CHECK(std::any_of(resolved.vertices.begin(), resolved.vertices.end(),
[](const PrimitiveTriangleVertex& vertex) { return vertex.color.a == 1.f; }));
}
TEST_CASE("GraphicsPrimitives.resolve2DPreservesBlendAndSubmissionOrder") {
PrimitiveCanvas2D canvas;
PrimitivePaint opaque;
opaque.mode = PaintMode::Stroke;
opaque.blend = BlendMode::Opaque;
opaque.antialias = false;
canvas.drawLine({0.f, 0.f}, {10.f, 0.f}, opaque);
PrimitivePaint additive = opaque;
additive.blend = BlendMode::Additive;
canvas.drawLine({0.f, 2.f}, {10.f, 2.f}, additive);
const auto resolved = resolvePrimitiveStrokes2D(canvas, {100, 100});
REQUIRE_EQ(resolved.batches2D.size(), 2u);
CHECK_EQ(static_cast<int>(resolved.batches2D[0].blend), static_cast<int>(BlendMode::Opaque));
CHECK_EQ(static_cast<int>(resolved.batches2D[1].blend), static_cast<int>(BlendMode::Additive));
CHECK_EQ(resolved.batches2D[0].sequence, 0u);
CHECK_EQ(resolved.batches2D[1].sequence, 1u);
}
TEST_CASE("GraphicsPrimitives.resolve2DDashContinuesAcrossPolylineSegments") {
PrimitiveCanvas2D canvas;
PrimitivePaint paint;
paint.mode = PaintMode::Stroke;
paint.stroke.width = 2.f;
paint.antialias = false;
paint.stroke.dash = DashPattern{{6.f, 4.f}, 0.f, DashSpace::ScreenPixels};
const std::array points{glm::vec2{0.f, 10.f}, glm::vec2{5.f, 10.f}, glm::vec2{10.f, 10.f}};
canvas.drawPolyline(points, false, paint);
const auto triangles = resolvePrimitiveStrokes2D(canvas, {100, 100});
REQUIRE_EQ(triangles.vertices.size(), 12u);
float maximumDistance = 0.f;
for (const auto& vertex : triangles.vertices) {
maximumDistance = std::max(maximumDistance, vertex.pathDistance);
}
CHECK(std::fabs(maximumDistance - 6.f) < 1e-5f);
}
TEST_CASE("GraphicsPrimitives.resolve2DDashPhaseSelectsGapAtStart") {
PrimitiveCanvas2D canvas;
PrimitivePaint paint;
paint.mode = PaintMode::Stroke;
paint.stroke.dash = DashPattern{{4.f, 4.f}, 4.f, DashSpace::ScreenPixels};
paint.antialias = false;
canvas.drawLine({0.f, 0.f}, {8.f, 0.f}, paint);
const auto triangles = resolvePrimitiveStrokes2D(canvas, {100, 100});
REQUIRE_EQ(triangles.vertices.size(), 6u);
CHECK(std::fabs(triangles.vertices.front().pathDistance - 4.f) < 1e-5f);
CHECK(std::fabs(triangles.vertices[1].pathDistance - 8.f) < 1e-5f);
}
TEST_CASE("GraphicsPrimitives.rectangleHonorsFillStrokeAndCombinedModes") {
PrimitiveCanvas2D canvas;
PrimitivePaint paint;
paint.mode = PaintMode::Fill;
paint.antialias = false;
canvas.drawRect({10.f, 20.f}, {30.f, 40.f}, paint);
CHECK_EQ(canvas.triangles().size(), 2u);
CHECK(canvas.commands().empty());
canvas.reset();
paint.mode = PaintMode::Stroke;
canvas.drawRect({10.f, 20.f}, {30.f, 40.f}, paint);
CHECK(canvas.triangles().empty());
REQUIRE_EQ(canvas.commands().size(), 1u);
CHECK(canvas.commands().front().closed);
canvas.reset();
paint.mode = PaintMode::FillAndStroke;
canvas.drawRect({10.f, 20.f}, {30.f, 40.f}, paint);
CHECK_EQ(canvas.triangles().size(), 2u);
CHECK_EQ(canvas.commands().size(), 1u);
}
TEST_CASE("GraphicsPrimitives.circleEllipseAndArcGenerateRealFillTriangles") {
PrimitiveCanvas2D canvas;
PrimitivePaint fill;
fill.mode = PaintMode::Fill;
fill.antialias = false;
canvas.drawCircle({50.f, 50.f}, 20.f, fill, 8);
CHECK_EQ(canvas.triangles().size(), 8u);
CHECK(canvas.commands().empty());
PrimitivePaint combined;
combined.mode = PaintMode::FillAndStroke;
combined.antialias = false;
canvas.drawEllipse({20.f, 20.f}, {10.f, 5.f}, combined, 8);
CHECK_EQ(canvas.triangles().size(), 16u);
CHECK_EQ(canvas.commands().size(), 1u);
canvas.drawArc({20.f, 20.f}, {8.f, 8.f}, 0.f, glm::pi<float>(), combined, 4);
CHECK_EQ(canvas.triangles().size(), 20u);
CHECK_EQ(canvas.commands().size(), 2u);
}
TEST_CASE("GraphicsPrimitives.unitCircleGeometryCacheReportsReuse") {
PrimitivePaint fill;
fill.mode = PaintMode::Fill;
PrimitiveCanvas2D canvas;
canvas.drawCircle({0.f, 0.f}, 1.f, fill, 37);
const std::size_t firstHits = canvas.statistics().cacheHits;
canvas.drawEllipse({2.f, 0.f}, {2.f, 1.f}, fill, 37);
CHECK_EQ(canvas.statistics().cacheHits, firstHits + 1u);
SceneDrawContext context;
context.viewportSize = {640, 480};
PrimitiveSceneCanvas3D sceneCanvas(context);
ScenePrimitivePaint stroke;
stroke.mode = PaintMode::Stroke;
sceneCanvas.drawSphere({0.f, 0.f, -4.f}, 1.f, stroke, 41);
CHECK(sceneCanvas.statistics().cacheHits >= 2u);
}
TEST_CASE("GraphicsPrimitives.radialQualityAndAdaptiveLodResolveDeterministically") {
CHECK_EQ(resolveRadialSegments({PrimitiveQuality::Low, 0.75f, 0}, 100.f), 16u);
CHECK_EQ(resolveRadialSegments({PrimitiveQuality::Medium, 0.75f, 0}, 100.f), 32u);
CHECK_EQ(resolveRadialSegments({PrimitiveQuality::High, 0.75f, 0}, 100.f), 64u);
CHECK_EQ(resolveRadialSegments({PrimitiveQuality::Adaptive, 0.75f, 23}, 100.f), 23u);
SceneDrawContext context;
context.viewportSize = {800, 600};
context.projection = glm::perspective(glm::radians(60.f), 4.f / 3.f, 0.1f, 100.f);
ScenePrimitivePaint stroke;
stroke.mode = PaintMode::Stroke;
const RadialTessellation adaptive{PrimitiveQuality::Adaptive, 0.5f, 0};
PrimitiveSceneCanvas3D nearCanvas(context);
nearCanvas.drawSphere({0.f, 0.f, -2.f}, 1.f, stroke, adaptive);
PrimitiveSceneCanvas3D farCanvas(context);
farCanvas.drawSphere({0.f, 0.f, -20.f}, 1.f, stroke, adaptive);
REQUIRE(!nearCanvas.commands().empty());
REQUIRE(!farCanvas.commands().empty());
CHECK(nearCanvas.commands().front().points.size() > farCanvas.commands().front().points.size());
}
TEST_CASE("GraphicsPrimitives.resolve2DCombinesFillAndStrokeTriangles") {
PrimitiveCanvas2D canvas;
PrimitivePaint paint;
paint.mode = PaintMode::FillAndStroke;
paint.stroke.width = 2.f;
paint.antialias = false;
canvas.drawRect({10.f, 10.f}, {30.f, 30.f}, paint);
const auto resolved = resolvePrimitiveStrokes2D(canvas, {100, 100});
CHECK_EQ(resolved.statistics.triangleCount, 14u);
CHECK_EQ(resolved.vertices.size(), 42u);
}
TEST_CASE("GraphicsPrimitives.strokeCapsProduceDistinctEndpointGeometry") {
PrimitivePaint paint;
paint.mode = PaintMode::Stroke;
paint.stroke.width = 4.f;
paint.antialias = false;
PrimitiveCanvas2D butt;
paint.stroke.cap = LineCap::Butt;
butt.drawLine({10.f, 20.f}, {30.f, 20.f}, paint);
CHECK_EQ(resolvePrimitiveStrokes2D(butt, {100, 100}).vertices.size(), 6u);
PrimitiveCanvas2D square;
paint.stroke.cap = LineCap::Square;
square.drawLine({10.f, 20.f}, {30.f, 20.f}, paint);
CHECK_EQ(resolvePrimitiveStrokes2D(square, {100, 100}).vertices.size(), 18u);
PrimitiveCanvas2D round;
paint.stroke.cap = LineCap::Round;
round.drawLine({10.f, 20.f}, {30.f, 20.f}, paint);
CHECK_EQ(resolvePrimitiveStrokes2D(round, {100, 100}).vertices.size(), 54u);
}
TEST_CASE("GraphicsPrimitives.strokeJoinsFillPolylineOuterCorner") {
const std::array points{glm::vec2{10.f, 10.f}, glm::vec2{30.f, 10.f}, glm::vec2{30.f, 30.f}};
PrimitivePaint paint;
paint.mode = PaintMode::Stroke;
paint.stroke.width = 6.f;
paint.antialias = false;
paint.stroke.join = LineJoin::Bevel;
PrimitiveCanvas2D bevel;
bevel.drawPolyline(points, false, paint);
CHECK_EQ(resolvePrimitiveStrokes2D(bevel, {100, 100}).vertices.size(), 15u);
paint.stroke.join = LineJoin::Round;
PrimitiveCanvas2D round;
round.drawPolyline(points, false, paint);
CHECK(resolvePrimitiveStrokes2D(round, {100, 100}).vertices.size() > 15u);
}
TEST_CASE("GraphicsPrimitives.resolve3DScreenStrokeClipsNearPlane") {
SceneDrawContext context;
context.viewportSize = {800, 600};
context.nearPlane = 0.1f;
context.farPlane = 100.f;
context.projection = glm::perspective(glm::radians(60.f), 4.f / 3.f, 0.1f, 100.f);
PrimitiveSceneCanvas3D canvas(context);
ScenePrimitivePaint paint;
paint.mode = PaintMode::Stroke;
paint.stroke.width = 3.f;
paint.antialias = false;
paint.stroke.widthSpace = WidthSpace::ScreenPixels;
canvas.drawLine({-1.f, 0.f, -1.f}, {1.f, 0.f, 0.f}, paint);
canvas.drawLine({-1.f, 1.f, 1.f}, {1.f, 1.f, 2.f}, paint);
const auto triangles = resolvePrimitiveStrokes3D(canvas);
REQUIRE_EQ(triangles.vertices.size(), 6u);
for (const auto& vertex : triangles.vertices) {
CHECK(std::isfinite(vertex.clipPosition.x));
CHECK(std::isfinite(vertex.clipPosition.w));
CHECK(vertex.clipPosition.w > 0.f);
}
}
TEST_CASE("GraphicsPrimitives.resolve3DWorldStrokeExpandsInViewSpace") {
SceneDrawContext context;
context.viewportSize = {800, 600};
context.projection = glm::perspective(glm::radians(60.f), 4.f / 3.f, 0.1f, 100.f);
PrimitiveSceneCanvas3D canvas(context);
ScenePrimitivePaint paint;
paint.mode = PaintMode::Stroke;
paint.stroke.width = 0.2f;
paint.antialias = false;
paint.stroke.widthSpace = WidthSpace::WorldUnits;
canvas.drawLine({-1.f, 0.f, -4.f}, {1.f, 0.f, -4.f}, paint);
const auto triangles = resolvePrimitiveStrokes3D(canvas);
REQUIRE_EQ(triangles.vertices.size(), 6u);
CHECK(std::fabs(triangles.vertices[0].clipPosition.y - triangles.vertices[2].clipPosition.y) > 1e-5f);
}
TEST_CASE("GraphicsPrimitives.resolve3DStrokeHonorsCapsAndJoins") {
SceneDrawContext context;
context.viewportSize = {800, 600};
context.projection = glm::perspective(glm::radians(60.f), 4.f / 3.f, 0.1f, 100.f);
ScenePrimitivePaint paint;
paint.mode = PaintMode::Stroke;
paint.stroke.width = 4.f;
paint.stroke.widthSpace = WidthSpace::ScreenPixels;
paint.antialias = false;
PrimitiveSceneCanvas3D square(context);
paint.stroke.cap = LineCap::Square;
square.drawLine({-1.f, 0.f, -4.f}, {1.f, 0.f, -4.f}, paint);
CHECK_EQ(resolvePrimitiveStrokes3D(square).vertices.size(), 18u);
PrimitiveSceneCanvas3D round(context);
paint.stroke.cap = LineCap::Round;
round.drawLine({-1.f, 0.f, -4.f}, {1.f, 0.f, -4.f}, paint);
CHECK_EQ(resolvePrimitiveStrokes3D(round).vertices.size(), 54u);
PrimitiveSceneCanvas3D bevel(context);
paint.stroke.cap = LineCap::Butt;
paint.stroke.join = LineJoin::Bevel;
const std::array points{glm::vec3{-1.f, 0.f, -4.f}, glm::vec3{0.f, 0.f, -4.f}, glm::vec3{0.f, 1.f, -4.f}};
bevel.drawPolyline(points, false, paint);
CHECK_EQ(resolvePrimitiveStrokes3D(bevel).vertices.size(), 15u);
}
TEST_CASE("GraphicsPrimitives.resolve3DScreenDashUsesProjectedPixelDistance") {
SceneDrawContext context;
context.viewportSize = {800, 600};
context.projection = glm::perspective(glm::radians(60.f), 4.f / 3.f, 0.1f, 100.f);
PrimitiveSceneCanvas3D canvas(context);
ScenePrimitivePaint paint;
paint.mode = PaintMode::Stroke;
paint.stroke.width = 2.f;
paint.stroke.dash = DashPattern{{10.f, 10.f}, 0.f, DashSpace::ScreenPixels};
canvas.drawLine({-1.f, 0.f, -4.f}, {1.f, 0.f, -4.f}, paint);
const auto triangles = resolvePrimitiveStrokes3D(canvas);
CHECK(triangles.vertices.size() > 6u);
CHECK_EQ(triangles.vertices.size() % 6u, 0u);
}
TEST_CASE("GraphicsPrimitives.batcherAcceptsResolvedArbitraryTriangles") {
Batcher batch;
batch.addTriangle({1.f, 2.f}, {7.f, 3.f}, {2.f, 9.f}, Color(1.f, 0.f, 0.f, 1.f));
REQUIRE_EQ(batch.vertices().size(), 3u);
CHECK(std::fabs(batch.vertices()[1].pos.x - 7.f) < 1e-5f);
batch.toNDC(10, 10);
CHECK(std::fabs(batch.vertices()[0].pos.x - (-0.8f)) < 1e-5f);
}
TEST_CASE("GraphicsPrimitives.persistentSceneDetectsStaleAndReusedSlots") {
PrimitiveScene scene;
CHECK(scene.isStale({}));
PrimitiveDescriptor3D descriptor;
descriptor.geometry = PrimitiveSphere3D{{0.f, 0.f, 0.f}, 1.f, 8};
auto added = scene.add(descriptor);
REQUIRE(added.ok());
const PrimitiveHandle first = added.value();
CHECK(scene.tryGet(first) != nullptr);
auto removed = scene.remove(first);
REQUIRE(removed.ok());
CHECK(scene.isStale(first));
auto addedAgain = scene.add(descriptor);
REQUIRE(addedAgain.ok());
const PrimitiveHandle second = addedAgain.value();
CHECK_EQ(second.index(), first.index());
CHECK(second.generation() != first.generation());
auto staleUpdate = scene.update(first, descriptor);
CHECK(!staleUpdate.ok());
CHECK_EQ(static_cast<int>(staleUpdate.error()->code()), static_cast<int>(eve::DiagnosticCode::StaleHandle));
}
TEST_CASE("GraphicsPrimitives.persistentSceneRejectsInvalidSpatialDescriptorsBeforeRender") {
PrimitiveScene scene;
const std::array<PrimitiveGeometry3D, 8> invalidGeometries{{
PrimitiveAabb3D{{1.f, 0.f, 0.f}, {0.f, 1.f, 1.f}},
PrimitiveObb3D{{0.f, 0.f, 0.f}, {{{1.f, 0.f, 0.f}, {2.f, 0.f, 0.f}, {0.f, 0.f, 1.f}}}},
PrimitiveDisk3D{{0.f, 0.f, 0.f}, {0.f, 0.f, 0.f}, 1.f, 8},
PrimitiveArc3D{{0.f, 0.f, 0.f}, {0.f, 1.f, 0.f}, {0.f, 2.f, 0.f}, 1.f, 0.f, 1.f, 8},
PrimitiveCylinder3D{{0.f, 0.f, 0.f}, {0.f, 0.f, 0.f}, 1.f, 8},
PrimitiveCone3D{{0.f, 0.f, 0.f}, {0.f, 0.f, 0.f}, 1.f, 1.f, 8},
PrimitiveGrid3D{{0.f, 0.f, 0.f}, {1.f, 0.f, 0.f}, {2.f, 0.f, 0.f}, 2, 2},
PrimitiveArrow3D{{0.f, 0.f, 0.f}, {0.f, 0.f, 0.f}, 0.2f, 0.1f},
}};
for (const PrimitiveGeometry3D& geometry : invalidGeometries) {
PrimitiveDescriptor3D descriptor;
descriptor.geometry = geometry;
auto added = scene.add(std::move(descriptor));
CHECK(!added.ok());
}
CHECK_EQ(scene.size(), 0u);
}
TEST_CASE("GraphicsPrimitives.sceneRejectsInvalidPaintWithoutMutation") {
PrimitiveScene scene;
PrimitiveDescriptor3D descriptor;
descriptor.geometry = PrimitiveSphere3D{};
auto added = scene.add(descriptor);
REQUIRE(added.ok());
const auto handle = added.value();
descriptor.paint.stroke.width = -1.f;
auto badWidth = scene.update(handle, descriptor);
CHECK(!badWidth.ok());
REQUIRE(scene.tryGet(handle) != nullptr);
CHECK_EQ(scene.tryGet(handle)->paint.stroke.width, 1.f);
descriptor.paint.stroke.width = 1.f;
descriptor.paint.stroke.dash = DashPattern{{1.f, 0.f}, 0.f, DashSpace::WorldUnits};
auto badDash = scene.update(handle, descriptor);
CHECK(!badDash.ok());
CHECK(!scene.tryGet(handle)->paint.stroke.dash.has_value());
}
TEST_CASE("GraphicsPrimitives.persistentSceneProjectsVisibleDescriptorsInSlotOrder") {
PrimitiveScene scene;
PrimitiveDescriptor3D hidden;
hidden.geometry = PrimitiveAabb3D{glm::vec3(-1.f), glm::vec3(1.f)};
hidden.visible = false;
auto hiddenHandle = scene.add(hidden);
REQUIRE(hiddenHandle.ok());
PrimitiveDescriptor3D arrow;
arrow.geometry = PrimitiveArrow3D{{0.f, 0.f, 0.f}, {0.f, 2.f, 0.f}, 0.4f, 0.2f};
arrow.transform = glm::translate(glm::mat4(1.f), {4.f, 0.f, 0.f});
auto arrowHandle = scene.add(arrow);
REQUIRE(arrowHandle.ok());
SceneDrawContext context;
context.viewportSize = {800, 600};
PrimitiveSceneCanvas3D canvas(context);
scene.render(canvas);
CHECK(!canvas.commands().empty());
for (const auto& command : canvas.commands()) {
CHECK(std::fabs(command.transform[3].x - 4.f) < 1e-5f);
}
scene.clear();
CHECK_EQ(scene.size(), 0u);
CHECK(scene.isStale(arrowHandle.value()));
}
TEST_CASE("GraphicsPrimitives.persistentHandlesAreQualifiedByOwningScene") {
PrimitiveScene firstScene;
PrimitiveScene secondScene;
PrimitiveDescriptor3D descriptor;
descriptor.geometry = PrimitiveSphere3D{};
auto first = firstScene.add(descriptor);
auto second = secondScene.add(descriptor);
REQUIRE(first.ok());
REQUIRE(second.ok());
CHECK(first.value().owner() != second.value().owner());
CHECK(secondScene.tryGet(first.value()) == nullptr);
CHECK(!secondScene.update(first.value(), descriptor).ok());
}
TEST_CASE("GraphicsPrimitives.sceneBatchUpdateIsAtomicOnStaleHandle") {
PrimitiveScene scene;
PrimitiveDescriptor3D firstDescriptor;
firstDescriptor.geometry = PrimitiveSphere3D{{1.f, 0.f, 0.f}, 1.f, 8};
PrimitiveDescriptor3D secondDescriptor;
secondDescriptor.geometry = PrimitiveSphere3D{{2.f, 0.f, 0.f}, 1.f, 8};
auto first = scene.add(firstDescriptor);
auto second = scene.add(secondDescriptor);
REQUIRE(first.ok());
REQUIRE(second.ok());
REQUIRE(scene.remove(second.value()).ok());
PrimitiveDescriptor3D replacement = firstDescriptor;
replacement.visible = false;
const std::array updates{PrimitiveBatchUpdate{first.value(), replacement},
PrimitiveBatchUpdate{second.value(), replacement}};
auto result = scene.updateMany(updates);
CHECK(!result.ok());
REQUIRE(scene.tryGet(first.value()) != nullptr);
CHECK(scene.tryGet(first.value())->visible);
}
TEST_CASE("GraphicsPrimitives.persistentSceneRejectsInvalidGeometryAtomically") {
PrimitiveScene scene;
PrimitiveDescriptor3D valid;
valid.geometry = PrimitiveSphere3D{{0.f, 0.f, 0.f}, 1.f, 8};
auto handle = scene.add(valid);
REQUIRE(handle.ok());
PrimitiveDescriptor3D invalid = valid;
invalid.geometry = PrimitiveSphere3D{{0.f, 0.f, 0.f}, -1.f, 8};
auto update = scene.update(handle.value(), invalid);
CHECK(!update.ok());
REQUIRE(scene.tryGet(handle.value()) != nullptr);
const auto* sphere = std::get_if<PrimitiveSphere3D>(&scene.tryGet(handle.value())->geometry);
REQUIRE(sphere != nullptr);
CHECK_EQ(sphere->radius, 1.f);
auto add = scene.add(invalid);
CHECK(!add.ok());
CHECK_EQ(scene.size(), 1u);
}