ThreeMfGeometryExtractor -> ProductImageRenderer * -> decoded PNG pixels) using a correctly-wound 20x20x20 cube with a distinct colour * on each of its 6 faces. This is the same cube used to generate the sample image for * visual inspection. Only the three faces whose outward normal points toward the * (1,1,1) camera (+Z "top", +X "right", +Y "back") should ever be visible; the other * three (-Z, -X, -Y) must be fully culled. This catches both the "upside down" * projection defect (an earlier version flipped Z) and the "no real hidden-surface * removal" defect (an earlier version only sorted by triangle centroid, which does not * guarantee a culled-looking, non-overlapping solid) with pixel-level assertions, not * just "the canvas isn't blank". */ class ProductImageRenderingPipelineTest extends TestCase { private const NS = 'http://schemas.microsoft.com/3dmanufacturing/core/2015/02'; // Base material indices, in declaration order. private const TOP = 0; // +Z, visible, red private const RIGHT = 1; // +X, visible, green private const BACK = 2; // +Y, visible, blue private const BOTTOM = 3; // -Z, culled, yellow private const LEFT = 4; // -X, culled, magenta private const FRONT = 5; // -Y, culled, cyan /** * A 20x20x20 cube with correct outward CCW winding (verified by hand per * the 3MF Core Specification's right-hand-rule convention) and one * base-material colour triangle-pinned per face. */ private function coloredCubeBytes(): string { $vertices = '' .''; $tri = fn (int $v1, int $v2, int $v3, int $colorIndex): string => sprintf( '', $v1, $v2, $v3, $colorIndex, ); $triangles = $tri(0, 3, 2, self::BOTTOM).$tri(0, 2, 1, self::BOTTOM) .$tri(4, 5, 6, self::TOP).$tri(4, 6, 7, self::TOP) .$tri(1, 2, 6, self::RIGHT).$tri(1, 6, 5, self::RIGHT) .$tri(0, 4, 7, self::LEFT).$tri(0, 7, 3, self::LEFT) .$tri(0, 1, 5, self::FRONT).$tri(0, 5, 4, self::FRONT) .$tri(2, 7, 6, self::BACK).$tri(2, 3, 7, self::BACK); $modelXml = '' .'' .'' .'' .'' .'' .'' .'' .'' ."{$vertices}{$triangles}" .''; $path = tempnam(sys_get_temp_dir(), 'cube').'.3mf'; $zip = new ZipArchive; $zip->open($path, ZipArchive::CREATE); $zip->addFromString('[Content_Types].xml', ''); $zip->addFromString('3D/3dmodel.model', $modelXml); $zip->addFromString('_rels/.rels', ''); $zip->close(); $bytes = file_get_contents($path); unlink($path); return $bytes; } /** * @return \GdImage */ private function renderCube() { $path = tempnam(sys_get_temp_dir(), 'cube').'.3mf'; file_put_contents($path, $this->coloredCubeBytes()); $scene = (new ThreeMfGeometryExtractor)->extract($path); unlink($path); $rendered = (new ProductImageRenderer(gdAvailable: true))->render($scene['triangles']); $this->assertSame(12, $scene['triangle_count']); $image = imagecreatefromstring($rendered['bytes']); $this->assertNotFalse($image); return $image; } /** * @return array{red: int, green: int, blue: int} */ private function rgbAt(\GdImage $image, int $x, int $y): array { return imagecolorsforindex($image, imagecolorat($image, $x, $y)); } public function test_top_face_is_visible_at_the_top_of_the_image_with_its_own_colour(): void { $image = $this->renderCube(); // Face centroid in screen space, derived from the cube's known world // coordinates and the corrected (non-flipped) isometric projection. $top = $this->rgbAt($image, 226, 144); $this->assertGreaterThan(100, $top['red'], 'Top face should be red-shaded.'); $this->assertLessThan(50, $top['green']); $this->assertLessThan(50, $top['blue']); imagedestroy($image); } public function test_right_face_is_visible_with_its_own_colour(): void { $image = $this->renderCube(); $right = $this->rgbAt($image, 323, 312); $this->assertLessThan(50, $right['red']); $this->assertGreaterThan(100, $right['green'], 'Right face should be green-shaded.'); $this->assertLessThan(50, $right['blue']); imagedestroy($image); } public function test_back_face_is_visible_with_its_own_colour(): void { $image = $this->renderCube(); $back = $this->rgbAt($image, 129, 312); $this->assertLessThan(50, $back['red']); $this->assertLessThan(50, $back['green']); $this->assertGreaterThan(30, $back['blue'], 'Back face should be blue-shaded.'); imagedestroy($image); } public function test_the_three_hidden_interior_faces_are_never_visible_anywhere_in_the_image(): void { $image = $this->renderCube(); // Yellow (bottom), magenta (left) and cyan (front) each have two // channels simultaneously high and one low; the dark-navy background // (15,23,42) and the three shaded visible faces (each with exactly // one dominant channel) never match that signature, so any match // here proves a culled face leaked through. for ($x = 0; $x < ProductImageRenderer::CANVAS_SIZE; $x += 4) { for ($y = 0; $y < ProductImageRenderer::CANVAS_SIZE; $y += 4) { $rgb = $this->rgbAt($image, $x, $y); $isYellowLike = $rgb['red'] > 100 && $rgb['green'] > 100 && $rgb['blue'] < 50; $isMagentaLike = $rgb['red'] > 100 && $rgb['blue'] > 100 && $rgb['green'] < 50; $isCyanLike = $rgb['green'] > 100 && $rgb['blue'] > 100 && $rgb['red'] < 50; $this->assertFalse($isYellowLike, "Bottom (culled) face leaked at ({$x},{$y})."); $this->assertFalse($isMagentaLike, "Left (culled) face leaked at ({$x},{$y})."); $this->assertFalse($isCyanLike, "Front (culled) face leaked at ({$x},{$y})."); } } imagedestroy($image); } public function test_background_corners_remain_background(): void { $image = $this->renderCube(); $corner = $this->rgbAt($image, 5, 5); $this->assertSame(15, $corner['red']); $this->assertSame(23, $corner['green']); $this->assertSame(42, $corner['blue']); imagedestroy($image); } }