105 lines
4.4 KiB
PHP
105 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit;
|
|
|
|
use App\Services\ProductImageRenderer;
|
|
use RuntimeException;
|
|
use Tests\TestCase;
|
|
|
|
class ProductImageRendererTest extends TestCase
|
|
{
|
|
/**
|
|
* @return list<array{v: array{0:array{0:float,1:float,2:float},1:array{0:float,1:float,2:float},2:array{0:float,1:float,2:float}}, color: array{0:int,1:int,2:int,3:int}}>
|
|
*/
|
|
private function oneRedTriangle(): array
|
|
{
|
|
return [[
|
|
'v' => [[0.0, 0.0, 0.0], [10.0, 0.0, 0.0], [0.0, 10.0, 0.0]],
|
|
'color' => [204, 0, 0, 255],
|
|
]];
|
|
}
|
|
|
|
public function test_renders_a_real_png_when_gd_is_available(): void
|
|
{
|
|
$renderer = new ProductImageRenderer(gdAvailable: true);
|
|
|
|
$result = $renderer->render($this->oneRedTriangle());
|
|
|
|
$this->assertSame('image/png', $result['mime']);
|
|
$this->assertSame('gd', $result['renderer']);
|
|
$this->assertSame(512, $result['width']);
|
|
$this->assertSame(512, $result['height']);
|
|
$this->assertStringStartsWith("\x89PNG\x0d\x0a\x1a\x0a", $result['bytes']);
|
|
|
|
// Decode it back and confirm an actual non-background pixel exists where the
|
|
// triangle should project — this is a real rasterisation, not a blank canvas.
|
|
$image = imagecreatefromstring($result['bytes']);
|
|
$this->assertNotFalse($image);
|
|
$backgroundRgb = imagecolorat($image, 5, 5);
|
|
// The projected triangle's centroid, computed from its known world
|
|
// coordinates and the corrected (non-flipped) isometric projection:
|
|
// vertices land at pixels (256,32), (480,161), (32,161).
|
|
$insideTriangleRgb = imagecolorat($image, 256, 120);
|
|
$this->assertNotSame($backgroundRgb, $insideTriangleRgb);
|
|
imagedestroy($image);
|
|
}
|
|
|
|
public function test_a_triangle_facing_away_from_the_camera_is_culled_and_never_drawn(): void
|
|
{
|
|
// Same triangle as oneRedTriangle() but with v1/v2 swapped, flipping
|
|
// its winding so the normal points -Z (away from the (1,1,1) camera).
|
|
$backFacing = [[
|
|
'v' => [[0.0, 0.0, 0.0], [0.0, 10.0, 0.0], [10.0, 0.0, 0.0]],
|
|
'color' => [204, 0, 0, 255],
|
|
]];
|
|
$renderer = new ProductImageRenderer(gdAvailable: true);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
$this->expectExceptionMessageMatches('/faces away from the camera/i');
|
|
$renderer->render($backFacing);
|
|
}
|
|
|
|
public function test_falls_back_to_fixed_content_numeric_only_svg_when_gd_is_unavailable(): void
|
|
{
|
|
$renderer = new ProductImageRenderer(gdAvailable: false);
|
|
|
|
$result = $renderer->render($this->oneRedTriangle());
|
|
|
|
$this->assertSame('image/svg+xml', $result['mime']);
|
|
$this->assertSame('svg', $result['renderer']);
|
|
$this->assertStringContainsString('<svg', $result['bytes']);
|
|
// Shaded from the pure red base colour (204,0,0) by this face's normal
|
|
// angle to the fixed light direction, not the unshaded base colour.
|
|
$this->assertStringContainsString('fill="#bb0000"', $result['bytes']);
|
|
$this->assertStringNotContainsString('<script', $result['bytes']);
|
|
$this->assertStringNotContainsString('<image', $result['bytes']);
|
|
$this->assertStringNotContainsString('xlink:href', $result['bytes']);
|
|
$this->assertStringNotContainsString('url(', $result['bytes']);
|
|
// Only numeric coordinates and hex colours appear inside each polygon tag.
|
|
$this->assertMatchesRegularExpression('/<polygon points="[\d.,\s-]+" fill="#[0-9a-f]{6}"\/>/', $result['bytes']);
|
|
}
|
|
|
|
public function test_rejects_an_empty_scene_rather_than_rendering_a_blank_image(): void
|
|
{
|
|
$renderer = new ProductImageRenderer(gdAvailable: true);
|
|
|
|
$this->expectException(RuntimeException::class);
|
|
$renderer->render([]);
|
|
}
|
|
|
|
public function test_rejects_translucent_material_instead_of_silently_rendering_it_opaque(): void
|
|
{
|
|
$triangles = $this->oneRedTriangle();
|
|
$triangles[0]['color'][3] = 128;
|
|
$this->expectException(RuntimeException::class);
|
|
$this->expectExceptionMessageMatches('/transparen/i');
|
|
(new ProductImageRenderer(gdAvailable: true))->render($triangles);
|
|
}
|
|
|
|
public function test_renderer_name_reflects_gd_availability(): void
|
|
{
|
|
$this->assertSame('gd', (new ProductImageRenderer(gdAvailable: true))->rendererName());
|
|
$this->assertSame('svg', (new ProductImageRenderer(gdAvailable: false))->rendererName());
|
|
}
|
|
}
|