test(3d): screenshot-baseline TDD suite for the 3D viewer OpenGL->WebGL port — 47 native goldens + red-state WebGL harness

tests/3d-regression mirrors the gal-regression pattern at renderer scale:
shared C++ scenarios call real KiCad 3D-viewer code (opengl_utils, display
lists + DrawCulled stencil subtraction, MODEL_3D VBOs, private generators via
a rob-template accessor, and full reload()+Redraw() composites over a
synthetic BOARD_ADAPTER). A native macOS harness renders them on real OpenGL
into 47 committed goldens (bit-deterministic, FBO capture); the wasm harness
compiles the same TUs against wasm/stubs/gl_ffp_stub.c no-ops so every
scenario renders blank — the TDD red state (parity meter: 47/47 changed).
Comparisons use the CI pixelmatch engine via the new generic compare-dirs.ts
(floors.json levels; manifest.json cmp-guards registry drift).

Documents an upstream bug: appendPostMachiningGeometry's countersink path
adds middle quads without normals, silently erasing the walls of any
display list it is batched into (3d-post-machining.png keeps the lists
separate to record it).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Istvan Matejcsok 2026-07-03 09:26:43 +02:00
commit ce1473c9ab
85 changed files with 6797 additions and 1 deletions

View file

@ -0,0 +1,238 @@
/**
* Tier-1 scenarios 21-28: MODEL_3D (VBO/IBO path), the navigator spheres
* gizmo, and camera projection/preset-view coverage.
*/
#include "scene3d_test_ctx.h"
#include "test_board_data.h"
#include "3d_rendering/opengl/3d_model.h"
#include "3d_rendering/opengl/3d_spheres_gizmo.h"
#include "3d_rendering/opengl/opengl_utils.h"
#include "common_ogl/ogl_utils.h"
#include <glm/ext.hpp>
#include <memory>
// 21: opaque + per-vertex-color meshes through the VBO/glDrawElements path.
void Scenario_Model3dOpaque( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
MODEL_3D model( TestS3DModel(), MATERIAL_MODE::NORMAL );
glPushMatrix();
glScalef( 2.0f, 2.0f, 2.0f );
MODEL_3D::BeginDrawMulti( true );
model.DrawOpaque( false );
MODEL_3D::EndDrawMulti();
glPopMatrix();
}
// 22: the transparent-model pass — blend + the glTexEnv COMBINE/INTERPOLATE
// block Redraw() sets up around renderTransparentModels
// (render_3d_opengl.cpp:800-831).
void Scenario_Model3dTransparent( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
MODEL_3D model( TestS3DModel(), MATERIAL_MODE::NORMAL );
glPushMatrix();
glScalef( 2.0f, 2.0f, 2.0f );
MODEL_3D::BeginDrawMulti( true );
model.DrawOpaque( false );
// State block replicated from Redraw() lines 800-827.
glDepthMask( GL_FALSE );
glEnable( GL_BLEND );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glEnable( GL_TEXTURE_2D );
glActiveTexture( GL_TEXTURE0 );
glBindTexture( GL_TEXTURE_2D, aCtx.GetCircleTexture() );
glTexEnvi( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE );
glTexEnvf( GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_INTERPOLATE );
glTexEnvf( GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_MODULATE );
glTexEnvi( GL_TEXTURE_ENV, GL_SRC0_RGB, GL_PRIMARY_COLOR );
glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND0_RGB, GL_SRC_COLOR );
glTexEnvi( GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PREVIOUS );
glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND1_RGB, GL_SRC_COLOR );
glTexEnvi( GL_TEXTURE_ENV, GL_SRC0_ALPHA, GL_PRIMARY_COLOR );
glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND0_ALPHA, GL_SRC_ALPHA );
glTexEnvi( GL_TEXTURE_ENV, GL_SRC1_ALPHA, GL_CONSTANT );
glTexEnvi( GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA );
model.DrawTransparent( 0.55f, false );
glDisable( GL_BLEND );
OglResetTextureState();
glDepthMask( GL_TRUE );
MODEL_3D::EndDrawMulti();
glPopMatrix();
}
// 23: MATERIAL_MODE branches — NORMAL / DIFFUSE_ONLY / CAD_MODE side by side.
void Scenario_Model3dMaterialModes( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
const MATERIAL_MODE modes[3] = { MATERIAL_MODE::NORMAL, MATERIAL_MODE::DIFFUSE_ONLY,
MATERIAL_MODE::CAD_MODE };
for( int i = 0; i < 3; i++ )
{
MODEL_3D model( TestS3DModel(), modes[i] );
glPushMatrix();
glTranslatef( ( i - 1 ) * 5.2f, 0.0f, 0.0f );
glScalef( 1.1f, 1.1f, 1.1f );
MODEL_3D::BeginDrawMulti( true );
model.DrawOpaque( false );
MODEL_3D::EndDrawMulti();
glPopMatrix();
}
}
// 24: model + mesh bounding boxes (glLineWidth>1 GL_LINES — a known WebGL
// port milestone: line width will need quad emulation there).
void Scenario_Model3dBbox( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
MODEL_3D model( TestS3DModel(), MATERIAL_MODE::NORMAL );
glPushMatrix();
glScalef( 2.0f, 2.0f, 2.0f );
MODEL_3D::BeginDrawMulti( true );
model.DrawOpaque( false );
// Same state the show_model_bbox path uses inside renderModel(): unlit
// blended colored lines, drawn between BeginDrawMulti/EndDrawMulti (the
// bbox VBO draw needs the client vertex-array state enabled there).
glDisable( GL_LIGHTING );
glEnable( GL_BLEND );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glColor4f( 0.4f, 1.0f, 0.4f, 0.75f );
model.DrawBboxes();
glColor4f( 1.0f, 0.3f, 0.3f, 0.9f );
model.DrawBbox();
glDisable( GL_BLEND );
glEnable( GL_LIGHTING );
MODEL_3D::EndDrawMulti();
glPopMatrix();
}
// 25: the navigator spheres gizmo — own corner viewport, gluPerspective,
// gluSphere billboards (RENDER_3D_OPENGL ctor uses SPHERES_GIZMO(4,4)).
void Scenario_SpheresGizmo( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
// Same construction/placement as RENDER_3D_OPENGL (render_3d_opengl.cpp:87,119):
// corner position (4,4), gizmo square is viewportHeight/8.
SPHERES_GIZMO gizmo( 4, 4 );
gizmo.setViewport( 0, 0, aCtx.m_width, aCtx.m_height );
gizmo.render3dSpheresGizmo( aCtx.m_camera.GetRotationMatrix() );
glViewport( 0, 0, aCtx.m_width, aCtx.m_height );
}
// Shared asymmetric marker so every camera pose is distinguishable: RGB axis
// triad + an off-axis segment.
static void drawCameraMarker( SCENE3D_CTX& aCtx )
{
OglSetDiffuseMaterial( SFVEC3F( 0.9f, 0.1f, 0.1f ), 1.0f );
DrawRoundArrow( SFVEC3F( 0.0f ), SFVEC3F( 3.5f, 0.0f, 0.0f ), 0.4f );
OglSetDiffuseMaterial( SFVEC3F( 0.1f, 0.9f, 0.1f ), 1.0f );
DrawRoundArrow( SFVEC3F( 0.0f ), SFVEC3F( 0.0f, 3.5f, 0.0f ), 0.4f );
OglSetDiffuseMaterial( SFVEC3F( 0.1f, 0.1f, 0.9f ), 1.0f );
DrawRoundArrow( SFVEC3F( 0.0f ), SFVEC3F( 0.0f, 0.0f, 3.5f ), 0.4f );
OglSetDiffuseMaterial( SFVEC3F( 0.75f, 0.61f, 0.23f ), 1.0f );
const ROUND_SEGMENT_2D segment( SFVEC2F( 1.5f, 1.5f ), SFVEC2F( 4.0f, 4.0f ), 1.0f,
DummyBoardItem() );
DrawSegment( segment, 24 );
}
// 26: perspective projection.
void Scenario_CameraPersp( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.SetOrtho( false );
aCtx.BeginFrame();
aCtx.SetupLights();
drawCameraMarker( aCtx );
}
// 27: orthographic projection of the identical scene.
void Scenario_CameraOrtho( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.SetOrtho( true );
aCtx.BeginFrame();
aCtx.SetupLights();
drawCameraMarker( aCtx );
}
// 28: the six preset views (ViewCommand_T1 T/B/L/R/F/Back), tiled 3x2.
void Scenario_CameraPresetViews( SCENE3D_CTX& aCtx )
{
aCtx.ResetCamera();
aCtx.BeginFrame();
aCtx.SetupLights();
const VIEW3D_TYPE views[6] = {
VIEW3D_TYPE::VIEW3D_TOP, VIEW3D_TYPE::VIEW3D_BOTTOM, VIEW3D_TYPE::VIEW3D_LEFT,
VIEW3D_TYPE::VIEW3D_RIGHT, VIEW3D_TYPE::VIEW3D_FRONT, VIEW3D_TYPE::VIEW3D_BACK,
};
const int tileW = aCtx.m_width / 3;
const int tileH = aCtx.m_height / 2;
for( int i = 0; i < 6; i++ )
{
aCtx.ResetCamera();
aCtx.SetView( views[i] );
glViewport( ( i % 3 ) * tileW, ( i / 3 ) * tileH, tileW, tileH );
glClear( GL_DEPTH_BUFFER_BIT );
// Re-upload the camera matrices for this tile (same calls Redraw makes).
glMatrixMode( GL_PROJECTION );
glLoadMatrixf( glm::value_ptr( aCtx.m_camera.GetProjectionMatrix() ) );
glMatrixMode( GL_MODELVIEW );
glLoadMatrixf( glm::value_ptr( aCtx.m_camera.GetViewMatrix() ) );
aCtx.PositionHeadlight();
drawCameraMarker( aCtx );
}
glViewport( 0, 0, aCtx.m_width, aCtx.m_height );
}

View file

@ -0,0 +1,207 @@
/**
* Tier-1 scenarios 13-20: TRIANGLE_DISPLAY_LIST / OPENGL_RENDER_LIST — display
* lists, extruded plates, the segment-ends texture + alpha-test path and the
* stencil hole-subtraction (DrawCulled).
*/
#include "scene3d_test_ctx.h"
#include "test_board_data.h"
#include "common_ogl/ogl_utils.h"
#include <cmath>
#include <memory>
// Fan-triangulate a closed convex contour into a TRIANGLE_LIST at height aZ.
// Data-filling only (mirrors what create_scene.cpp's generators feed the
// containers); the winding follows addTopAndBottomTriangles: top faces CCW in
// XY, bottom faces reversed.
static void addFanTriangles( TRIANGLE_LIST* aDst, const std::vector<SFVEC2F>& aClosedContour,
float aZ, bool aTopFace )
{
const SFVEC2F& v0 = aClosedContour.front();
for( size_t i = 1; i + 1 < aClosedContour.size(); i++ )
{
const SFVEC2F& v1 = aClosedContour[i];
const SFVEC2F& v2 = aClosedContour[i + 1];
if( aTopFace )
aDst->AddTriangle( SFVEC3F( v0.x, v0.y, aZ ), SFVEC3F( v1.x, v1.y, aZ ),
SFVEC3F( v2.x, v2.y, aZ ) );
else
aDst->AddTriangle( SFVEC3F( v2.x, v2.y, aZ ), SFVEC3F( v1.x, v1.y, aZ ),
SFVEC3F( v0.x, v0.y, aZ ) );
}
}
// An extruded plate: top + bottom fans and real AddToMiddleContours walls.
static std::unique_ptr<TRIANGLE_DISPLAY_LIST> makePlateTdl( const std::vector<SFVEC2F>& aContour,
float aZBot, float aZTop )
{
auto tdl = std::make_unique<TRIANGLE_DISPLAY_LIST>( 2 * aContour.size() );
addFanTriangles( tdl->m_layer_top_triangles, aContour, aZTop, true );
addFanTriangles( tdl->m_layer_bot_triangles, aContour, aZBot, false );
// Our contours are CCW in 3D space; board outlines arrive effectively CW
// (the BIU->3D conversion mirrors Y), so invert to get outward-facing walls.
tdl->AddToMiddleContours( aContour, aZBot, aZTop, true );
return tdl;
}
static std::unique_ptr<OPENGL_RENDER_LIST> makeHexPlateList( SCENE3D_CTX& aCtx, float aZBot,
float aZTop )
{
auto tdl = makePlateTdl( MakeCircleContour( 4.5f, 6 ), aZBot, aZTop );
return std::unique_ptr<OPENGL_RENDER_LIST>( aCtx.MakeRenderList( *tdl, aZBot, aZTop ) );
}
static void beginTdlScene( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
OglSetDiffuseMaterial( SFVEC3F( 0.75f, 0.61f, 0.23f ), 1.0f );
}
// 13: only the top-face display list.
void Scenario_TdlDrawTop( SCENE3D_CTX& aCtx )
{
beginTdlScene( aCtx );
makeHexPlateList( aCtx, -0.6f, 0.6f )->DrawTop();
}
// 14: only the bottom-face display list, seen from below.
void Scenario_TdlDrawBot( SCENE3D_CTX& aCtx )
{
aCtx.ResetCamera();
aCtx.SetView( VIEW3D_TYPE::VIEW3D_BOTTOM );
aCtx.BeginFrame();
aCtx.SetupLights();
OglSetDiffuseMaterial( SFVEC3F( 0.75f, 0.61f, 0.23f ), 1.0f );
makeHexPlateList( aCtx, -0.6f, 0.6f )->DrawBot();
}
// 15: only the extruded side walls (middle contour quads, per-vertex normals).
void Scenario_TdlDrawMiddle( SCENE3D_CTX& aCtx )
{
beginTdlScene( aCtx );
makeHexPlateList( aCtx, -1.2f, 1.2f )->DrawMiddle();
}
// 16: the closed extruded plate — all five sub-lists.
void Scenario_TdlDrawAll( SCENE3D_CTX& aCtx )
{
beginTdlScene( aCtx );
makeHexPlateList( aCtx, -0.9f, 0.9f )->DrawAll();
}
// 17: the segment-ends path — circle texture + glAlphaFunc(GL_GREATER,0.2)
// inside generate_top_or_bot_seg_ends (layer_triangles.cpp:600-624). The
// triangle pattern mirrors addObjectTriangles(FILLED_CIRCLE_2D)
// (create_scene.cpp:42-70): two triangles per circle whose UVs map the
// blurred-circle texture into a round disc.
void Scenario_TdlSegEndsTexture( SCENE3D_CTX& aCtx )
{
beginTdlScene( aCtx );
TRIANGLE_DISPLAY_LIST tdl( 8 );
const float texture_factor = ( 8.0f / 1024.0f ) + 1.0f; // SIZE_OF_CIRCLE_TEXTURE
const SFVEC2F centers[3] = { { -3.5f, 0.0f }, { 0.0f, 0.0f }, { 3.5f, 0.0f } };
const float radii[3] = { 1.2f, 1.7f, 2.2f };
for( int i = 0; i < 3; i++ )
{
const SFVEC2F& center = centers[i];
const float radius = radii[i] * 2.0f; // doubled like the generator
const float f = ( std::sqrt( 2.0f ) / 2.0f ) * radius * texture_factor;
const float z = 0.4f;
tdl.m_layer_top_segment_ends->AddTriangle(
SFVEC3F( center.x + f, center.y, z ), SFVEC3F( center.x - f, center.y, z ),
SFVEC3F( center.x, center.y - f, z ) );
tdl.m_layer_top_segment_ends->AddTriangle(
SFVEC3F( center.x - f, center.y, z ), SFVEC3F( center.x + f, center.y, z ),
SFVEC3F( center.x, center.y + f, z ) );
}
std::unique_ptr<OPENGL_RENDER_LIST> list( aCtx.MakeRenderList( tdl, 0.0f, 0.4f ) );
list->DrawTop();
}
// 18: DrawCulled — the stencil-based hole subtraction (layer_triangles.cpp:459-543).
// A plate with two hole volumes stenciled out of it.
void Scenario_TdlCulledStencil( SCENE3D_CTX& aCtx )
{
beginTdlScene( aCtx );
const float zBot = -0.5f, zTop = 0.5f;
auto plateTdl = makePlateTdl( MakeCircleContour( 4.5f, 6 ), zBot, zTop );
std::unique_ptr<OPENGL_RENDER_LIST> plate( aCtx.MakeRenderList( *plateTdl, zBot, zTop ) );
// Hole volumes: same z-range plates (a round one and a square one), like
// the outer-through-holes subtract lists Redraw passes to DrawCulled.
auto holesTdl = makePlateTdl( MakeCircleContour( 1.1f, 16, -1.8f, 0.0f ), zBot, zTop );
auto holes2Tdl = makePlateTdl( MakeSquareContour( 0.9f, 1.8f, 0.9f ), zBot, zTop );
std::unique_ptr<OPENGL_RENDER_LIST> holes( aCtx.MakeRenderList( *holesTdl, zBot, zTop ) );
std::unique_ptr<OPENGL_RENDER_LIST> holes2( aCtx.MakeRenderList( *holes2Tdl, zBot, zTop ) );
plate->DrawCulled( true, holes.get(), holes2.get() );
}
// 19: ApplyScalePosition — the z-translate/z-scale transform used to place
// layer plates at their board Z (layer_triangles.cpp beginTransformation).
void Scenario_TdlZScale( SCENE3D_CTX& aCtx )
{
beginTdlScene( aCtx );
auto tdl = makePlateTdl( MakeCircleContour( 3.5f, 6 ), 0.0f, 1.0f );
std::unique_ptr<OPENGL_RENDER_LIST> list( aCtx.MakeRenderList( *tdl, 0.0f, 1.0f ) );
// Thin plate below...
list->ApplyScalePosition( -1.6f, 0.25f );
list->DrawAll();
// ...thick plate above.
OglSetDiffuseMaterial( SFVEC3F( 0.2f, 0.5f, 0.8f ), 1.0f );
list->ApplyScalePosition( 0.6f, 1.8f );
list->DrawAll();
}
// 20: SetItIsTransparent + blended DrawAll over an opaque plate.
void Scenario_TdlTransparent( SCENE3D_CTX& aCtx )
{
beginTdlScene( aCtx );
auto baseTdl = makePlateTdl( MakeSquareContour( 3.0f ), -1.0f, -0.4f );
std::unique_ptr<OPENGL_RENDER_LIST> base( aCtx.MakeRenderList( *baseTdl, -1.0f, -0.4f ) );
base->DrawAll();
// Epoxy-like translucent plate above it (renderBoardBody pattern:
// material transparency + SetItIsTransparent, render_3d_opengl.cpp:468-501).
SMATERIAL epoxy;
epoxy.m_Ambient = SFVEC3F( 0.1f, 0.1f, 0.12f );
epoxy.m_Diffuse = SFVEC3F( 0.4f, 0.4f, 0.5f ); // BOARD_ADAPTER default board body
epoxy.m_Emissive = SFVEC3F( 0.0f, 0.0f, 0.0f );
epoxy.m_Specular = SFVEC3F( 0.2f, 0.2f, 0.2f );
epoxy.m_Shininess = 0.3f;
epoxy.m_Transparency = 0.1f; // == 1 - default body alpha 0.9
OglSetMaterial( epoxy, 0.6f );
auto topTdl = makePlateTdl( MakeCircleContour( 4.2f, 6 ), 0.0f, 0.8f );
std::unique_ptr<OPENGL_RENDER_LIST> top( aCtx.MakeRenderList( *topTdl, 0.0f, 0.8f ) );
top->SetItIsTransparent( true );
top->DrawAll();
}

View file

@ -0,0 +1,269 @@
/**
* Tier-1 scenarios 1-12: common_ogl/ogl_utils + opengl_utils free functions,
* FFP materials and lights. Standalone KiCad TUs only — no RENDER_3D_OPENGL
* members.
*/
#include "scene3d_test_ctx.h"
#include "test_board_data.h"
#include "3d_rendering/opengl/opengl_utils.h"
#include "3d_rendering/raytracing/shapes3D/bbox_3d.h"
#include "common_ogl/ogl_utils.h"
#include <glm/ext.hpp>
// 1: the default viewer background gradient — the simplest possible render
// (no geometry; identity matrices inside OglDrawBackground).
void Scenario_BgGradient( SCENE3D_CTX& aCtx )
{
aCtx.ResetCamera();
aCtx.BeginFrame();
}
// 2: translucent background colors exercising the premultiplied-alpha path
// Redraw() feeds through (render_3d_opengl.cpp:576-577).
void Scenario_BgGradientAlpha( SCENE3D_CTX& aCtx )
{
aCtx.ResetCamera();
aCtx.BeginFrame( SFVEC4F( 0.9f, 0.3f, 0.1f, 0.5f ), SFVEC4F( 0.1f, 0.3f, 0.9f, 0.8f ) );
}
// 3: DrawBoundingBox — GL_LINE_LOOP/GL_LINE_STRIP wireframe, unlit colored lines.
void Scenario_BoundingBox( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.BeginFrame();
glDisable( GL_LIGHTING );
glColor4f( 0.9f, 0.9f, 0.2f, 1.0f );
DrawBoundingBox( BBOX_3D( SFVEC3F( -3.0f, -2.0f, -1.0f ), SFVEC3F( 3.0f, 2.0f, 1.0f ) ) );
glColor4f( 0.2f, 0.9f, 0.9f, 1.0f );
DrawBoundingBox( BBOX_3D( SFVEC3F( -1.0f, -1.0f, -2.0f ), SFVEC3F( 1.0f, 1.0f, 2.0f ) ) );
}
// 4: DrawHalfOpenCylinder — TRIANGLE_FAN caps + QUAD_STRIP wall, smooth normals, lit.
void Scenario_HalfOpenCylinder( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
OglSetDiffuseMaterial( SFVEC3F( 0.75f, 0.35f, 0.15f ), 1.0f );
// Unit-sized primitive (d=1, h=1, base at origin) — scale up to fill the
// frame; GL_NORMALIZE (set in BeginFrame, as in Redraw) fixes the normals.
// Laid on its side so the curved wall catches the near-vertical
// directional lights (upright walls are almost unlit under init_lights()).
glPushMatrix();
glRotatef( 90.0f, 0.0f, 1.0f, 0.0f ); // axis along +X (screen horizontal)
glRotatef( -90.0f, 0.0f, 0.0f, 1.0f ); // convex half toward camera+top light
glScalef( 5.0f, 5.0f, 8.0f );
glTranslatef( 0.0f, 0.0f, -0.5f );
DrawHalfOpenCylinder( 32 );
glPopMatrix();
}
// 5: DrawSegment — one thick rounded-end track segment (quads + half-cylinders
// + matrix stack inside the helper).
void Scenario_SegmentSingle( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
OglSetDiffuseMaterial( SFVEC3F( 0.75f, 0.61f, 0.23f ), 1.0f );
const ROUND_SEGMENT_2D segment( SFVEC2F( -4.0f, -2.0f ), SFVEC2F( 4.0f, 2.0f ), 2.0f,
DummyBoardItem() );
DrawSegment( segment, 32 );
}
// 6: a star of DrawSegment calls with varying widths/angles.
void Scenario_SegmentsStar( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
OglSetDiffuseMaterial( SFVEC3F( 0.75f, 0.61f, 0.23f ), 1.0f );
for( int i = 0; i < 12; i++ )
{
const float a = 2.0f * glm::pi<float>() * i / 12.0f;
const float r = 5.5f;
const float width = 0.25f + 0.09f * i;
const ROUND_SEGMENT_2D segment( SFVEC2F( 1.2f * std::cos( a ), 1.2f * std::sin( a ) ),
SFVEC2F( r * std::cos( a ), r * std::sin( a ) ), width,
DummyBoardItem() );
DrawSegment( segment, 24 );
}
}
// 7: DrawRoundArrow — GLU cylinder + cone + disk + sphere quadrics.
void Scenario_RoundArrow( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
OglSetDiffuseMaterial( SFVEC3F( 0.2f, 0.7f, 0.3f ), 1.0f );
DrawRoundArrow( SFVEC3F( -2.0f, -2.0f, 0.0f ), SFVEC3F( 3.0f, 2.5f, 1.5f ), 0.5f );
}
// 8: the RGB axis triad the viewer draws — three arrows with per-axis materials.
void Scenario_RoundArrowsAxes( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
// Same layout as RENDER_3D_OPENGL::Redraw()'s show_axis block.
const float arrow_size = SCENE3D_RANGE_SCALE_3D * 0.30f;
OglSetDiffuseMaterial( SFVEC3F( 0.9f, 0.0f, 0.0f ), 1.0f );
DrawRoundArrow( SFVEC3F( 0.0f ), SFVEC3F( arrow_size, 0.0f, 0.0f ), 0.275f );
OglSetDiffuseMaterial( SFVEC3F( 0.0f, 0.9f, 0.0f ), 1.0f );
DrawRoundArrow( SFVEC3F( 0.0f ), SFVEC3F( 0.0f, arrow_size, 0.0f ), 0.275f );
OglSetDiffuseMaterial( SFVEC3F( 0.0f, 0.0f, 0.9f ), 1.0f );
DrawRoundArrow( SFVEC3F( 0.0f ), SFVEC3F( 0.0f, 0.0f, arrow_size ), 0.275f );
}
// Shared geometry for the material scenarios: cylinder + star spokes.
static void drawMaterialTestGeometry( SCENE3D_CTX& aCtx )
{
glPushMatrix();
glScalef( 3.0f, 3.0f, 2.5f );
DrawHalfOpenCylinder( 32 );
glPopMatrix();
for( int i = 0; i < 6; i++ )
{
const float a = 2.0f * glm::pi<float>() * i / 6.0f;
const ROUND_SEGMENT_2D segment( SFVEC2F( 2.2f * std::cos( a ), 2.2f * std::sin( a ) ),
SFVEC2F( 5.5f * std::cos( a ), 5.5f * std::sin( a ) ),
0.8f, DummyBoardItem() );
DrawSegment( segment, 24 );
}
}
// 9: full SMATERIAL via OglSetMaterial — ambient/diffuse/specular/shininess (copper-like).
void Scenario_MaterialCopper( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
SMATERIAL copper;
copper.m_Ambient = SFVEC3F( 0.26f, 0.23f, 0.11f );
copper.m_Diffuse = SFVEC3F( 0.75f, 0.61f, 0.23f ); // BOARD_ADAPTER default copper
copper.m_Emissive = SFVEC3F( 0.0f, 0.0f, 0.0f );
copper.m_Specular = SFVEC3F( 0.70f, 0.55f, 0.35f );
copper.m_Shininess = 0.4f;
copper.m_Transparency = 0.0f;
OglSetMaterial( copper, 1.0f );
drawMaterialTestGeometry( aCtx );
}
// 10: OglSetDiffuseMaterial — flat matte look, same geometry as #9.
void Scenario_MaterialDiffuseOnly( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
OglSetDiffuseMaterial( SFVEC3F( 0.75f, 0.61f, 0.23f ), 1.0f );
drawMaterialTestGeometry( aCtx );
}
// 11: transparent material + blending over opaque geometry.
void Scenario_MaterialTransparent( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
// Opaque base plate of segments.
OglSetDiffuseMaterial( SFVEC3F( 0.3f, 0.3f, 0.35f ), 1.0f );
for( int i = -2; i <= 2; i++ )
{
const ROUND_SEGMENT_2D segment( SFVEC2F( -5.0f, i * 1.6f ), SFVEC2F( 5.0f, i * 1.6f ),
1.2f, DummyBoardItem() );
DrawSegment( segment, 24 );
}
// Translucent solder-mask-like material on top (same blend state the
// renderer uses for transparent passes, layer_triangles.cpp setBlendfunction).
glEnable( GL_BLEND );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
glDepthMask( GL_FALSE );
SMATERIAL mask;
mask.m_Ambient = SFVEC3F( 0.1f, 0.2f, 0.1f );
mask.m_Diffuse = SFVEC3F( 0.1f, 0.6f, 0.2f );
mask.m_Emissive = SFVEC3F( 0.0f, 0.0f, 0.0f );
mask.m_Specular = SFVEC3F( 0.2f, 0.4f, 0.2f );
mask.m_Shininess = 0.5f;
mask.m_Transparency = 0.5f; // diffuse alpha = (1-transparency)*opacity
OglSetMaterial( mask, 1.0f );
glPushMatrix();
glTranslatef( 0.0f, 0.0f, 1.0f );
glScalef( 4.5f, 4.5f, 1.5f );
DrawHalfOpenCylinder( 32 );
glPopMatrix();
glDepthMask( GL_TRUE );
glDisable( GL_BLEND );
}
// 12-14: GL_LIGHT0/1/2 isolated — one scenario per light (front/headlight
// point light, top directional, bottom directional; the eye-space-anchored
// directions init_lights() bakes at context init). Same sideways cylinder so
// the three renders are directly comparable.
static void drawLightTestGeometry( SCENE3D_CTX& aCtx, bool aFront, bool aTop, bool aBottom )
{
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
aCtx.EnableLights( aFront, aTop, aBottom );
OglSetDiffuseMaterial( SFVEC3F( 0.7f, 0.7f, 0.75f ), 1.0f );
glPushMatrix();
glRotatef( 90.0f, 0.0f, 1.0f, 0.0f );
glRotatef( -90.0f, 0.0f, 0.0f, 1.0f );
glScalef( 4.5f, 4.5f, 8.0f );
glTranslatef( 0.0f, 0.0f, -0.5f );
DrawHalfOpenCylinder( 32 );
glPopMatrix();
aCtx.EnableLights( true, true, true );
}
void Scenario_LightFront( SCENE3D_CTX& aCtx )
{
drawLightTestGeometry( aCtx, true, false, false );
}
void Scenario_LightTop( SCENE3D_CTX& aCtx )
{
drawLightTestGeometry( aCtx, false, true, false );
}
void Scenario_LightBottom( SCENE3D_CTX& aCtx )
{
drawLightTestGeometry( aCtx, false, false, true );
}

View file

@ -0,0 +1,336 @@
/**
* Tier-2 scenarios: RENDER_3D_OPENGL's private geometry generators, grids and
* material setters, reached through the rob-template accessor
* (native/render3d_test_accessor.h) over a synthetic BOARD_ADAPTER
* (native/board_adapter_test_impl.cpp InitSettings).
*/
#include "scene3d_test_ctx.h"
#include "scene3d_test_rig.h"
#include "test_board_data.h"
#include "3d_rendering/opengl/opengl_utils.h"
#include "3d_rendering/raytracing/shapes2D/4pt_polygon_2d.h"
#include "3d_rendering/raytracing/shapes2D/filled_circle_2d.h"
#include "3d_rendering/raytracing/shapes2D/ring_2d.h"
#include "3d_rendering/raytracing/shapes2D/round_segment_2d.h"
#include "3d_rendering/raytracing/shapes2D/triangle_2d.h"
#include "common_ogl/ogl_utils.h"
#include <base_units.h> // pcbIUScale
#include <glm/ext.hpp>
#include <memory>
using TIER2_RIG = SCENE3D_TEST_RIG;
static void beginTier2Scene( SCENE3D_CTX& aCtx )
{
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
OglSetDiffuseMaterial( SFVEC3F( 0.75f, 0.61f, 0.23f ), 1.0f );
}
// 31: generateCylinder — the via/pad barrel wall generator.
void Scenario_GenCylinder( SCENE3D_CTX& aCtx )
{
TIER2_RIG rig( aCtx );
beginTier2Scene( aCtx );
TRIANGLE_DISPLAY_LIST tdl( 256 );
R3D_GenerateCylinder( *rig, SFVEC2F( 0.0f, 0.0f ), 2.2f, 3.0f, 1.8f, -1.8f, 32, &tdl );
std::unique_ptr<OPENGL_RENDER_LIST> list( aCtx.MakeRenderList( tdl, -1.8f, 1.8f ) );
list->DrawAll();
}
// 32: generateInvCone — the countersink cone generator.
void Scenario_GenInvCone( SCENE3D_CTX& aCtx )
{
TIER2_RIG rig( aCtx );
beginTier2Scene( aCtx );
TRIANGLE_DISPLAY_LIST tdl( 256 );
R3D_GenerateInvCone( *rig, SFVEC2F( 0.0f, 0.0f ), 1.2f, 3.2f, 1.5f, -1.5f, 32, &tdl,
EDA_ANGLE( 90.0, DEGREES_T ) );
std::unique_ptr<OPENGL_RENDER_LIST> list( aCtx.MakeRenderList( tdl, -1.5f, 1.5f ) );
list->DrawAll();
}
// 33: generateDisk — hole caps / annulus disks, top and bottom variants.
void Scenario_GenDisk( SCENE3D_CTX& aCtx )
{
TIER2_RIG rig( aCtx );
beginTier2Scene( aCtx );
TRIANGLE_DISPLAY_LIST tdl( 256 );
R3D_GenerateDisk( *rig, SFVEC2F( -2.6f, 0.0f ), 2.0f, 0.8f, 32, &tdl, true );
R3D_GenerateDisk( *rig, SFVEC2F( 2.6f, 0.0f ), 2.0f, -0.8f, 32, &tdl, false );
std::unique_ptr<OPENGL_RENDER_LIST> list( aCtx.MakeRenderList( tdl, -0.8f, 0.8f ) );
list->DrawAll();
}
// 34: generateDimple — the plated-hole cover bump.
void Scenario_GenDimple( SCENE3D_CTX& aCtx )
{
TIER2_RIG rig( aCtx );
beginTier2Scene( aCtx );
TRIANGLE_DISPLAY_LIST tdl( 1024 );
R3D_GenerateDimple( *rig, SFVEC2F( 0.0f, 0.0f ), 3.0f, 0.0f, 1.2f, 48, &tdl, true );
std::unique_ptr<OPENGL_RENDER_LIST> list( aCtx.MakeRenderList( tdl, 0.0f, 1.2f ) );
list->DrawAll();
}
// 35: all five addObjectTriangles overloads in a row.
void Scenario_AddObjAllShapes( SCENE3D_CTX& aCtx )
{
TIER2_RIG rig( aCtx );
beginTier2Scene( aCtx );
const float zTop = 0.5f, zBot = -0.5f;
TRIANGLE_DISPLAY_LIST tdl( 1024 );
const FILLED_CIRCLE_2D circle( SFVEC2F( -5.4f, 0.0f ), 1.1f, DummyBoardItem() );
R3D_AddObjTriangles( *rig, &circle, &tdl, zTop, zBot );
const RING_2D ring( SFVEC2F( -2.7f, 0.0f ), 0.6f, 1.2f, DummyBoardItem() );
R3D_AddObjTriangles( *rig, &ring, &tdl, zTop, zBot );
const POLYGON_4PT_2D poly( SFVEC2F( -1.0f, -1.0f ), SFVEC2F( 1.0f, -1.1f ),
SFVEC2F( 1.1f, 1.0f ), SFVEC2F( -0.9f, 1.1f ), DummyBoardItem() );
R3D_AddObjTriangles( *rig, &poly, &tdl, zTop, zBot );
const TRIANGLE_2D tri( SFVEC2F( 1.8f, -1.1f ), SFVEC2F( 3.6f, -1.1f ), SFVEC2F( 2.7f, 1.2f ),
DummyBoardItem() );
R3D_AddObjTriangles( *rig, &tri, &tdl, zTop, zBot );
const ROUND_SEGMENT_2D seg( SFVEC2F( 4.4f, -1.0f ), SFVEC2F( 6.0f, 1.0f ), 0.9f,
DummyBoardItem() );
R3D_AddObjTriangles( *rig, &seg, &tdl, zTop, zBot );
std::unique_ptr<OPENGL_RENDER_LIST> list( aCtx.MakeRenderList( tdl, zBot, zTop ) );
list->DrawAll();
}
// 36: appendPostMachiningGeometry — counterbore and countersink profiles.
void Scenario_PostMachining( SCENE3D_CTX& aCtx )
{
TIER2_RIG rig( aCtx );
beginTier2Scene( aCtx );
const float unitScale = static_cast<float>( rig.m_adapter.BiuTo3dUnits() );
float zEnd = 0.0f;
// Separate lists on purpose. UPSTREAM BUG (create_scene.cpp countersink
// path): the cone quads are added with AddQuad but no AddNormal, so the
// middle-quads normals array is half the vertex count and
// generate_middle_triangles rejects the WHOLE list — a countersink batched
// with other geometry kills that list's walls in the real viewer too.
// Keeping them separate makes the counterbore render correctly while the
// countersink half documents the buggy (empty) upstream output.
TRIANGLE_DISPLAY_LIST cbTdl( 4096 );
const bool cb = R3D_AppendPostMachining( *rig, &cbTdl, SFVEC2F( -3.2f, 0.0f ),
PAD_DRILL_POST_MACHINING_MODE::COUNTERBORE,
pcbIUScale.mmToIU( 14 ), pcbIUScale.mmToIU( 6 ),
1.0f, 1.0f, true, 0.4f, unitScale, &zEnd );
TRIANGLE_DISPLAY_LIST csTdl( 4096 );
const bool cs = R3D_AppendPostMachining( *rig, &csTdl, SFVEC2F( 3.2f, 0.0f ),
PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK,
pcbIUScale.mmToIU( 14 ), pcbIUScale.mmToIU( 6 ),
1.0f, 1.0f, true, 0.4f, unitScale, &zEnd );
wxASSERT( cb && cs );
(void) cb;
(void) cs;
std::unique_ptr<OPENGL_RENDER_LIST> cbList( aCtx.MakeRenderList( cbTdl, -1.0f, 1.0f ) );
cbList->DrawAll();
std::unique_ptr<OPENGL_RENDER_LIST> csList( aCtx.MakeRenderList( csTdl, -1.0f, 1.0f ) );
csList->DrawAll();
}
// 37: a complete via cross-section composed the way generateViaBarrels /
// generateViaCovers do: barrel + annular disks + cover dimple.
void Scenario_ViaComposite( SCENE3D_CTX& aCtx )
{
TIER2_RIG rig( aCtx );
beginTier2Scene( aCtx );
R3D_SetupMaterials( *rig );
R3D_SetLayerMaterial( *rig, F_Cu );
TRIANGLE_DISPLAY_LIST tdl( 2048 );
R3D_GenerateCylinder( *rig, SFVEC2F( 0.0f, 0.0f ), 1.6f, 2.0f, 1.4f, -1.4f, 32, &tdl );
R3D_GenerateDisk( *rig, SFVEC2F( 0.0f, 0.0f ), 2.6f, 1.4f, 32, &tdl, true );
R3D_GenerateDisk( *rig, SFVEC2F( 0.0f, 0.0f ), 2.6f, -1.4f, 32, &tdl, false );
R3D_GenerateDimple( *rig, SFVEC2F( 0.0f, 0.0f ), 1.6f, 1.4f, 0.5f, 32, &tdl, true );
std::unique_ptr<OPENGL_RENDER_LIST> list( aCtx.MakeRenderList( tdl, -1.4f, 1.4f ) );
list->DrawAll();
}
// 38-41: generate3dGrid at each density — display list of blended GL_LINES
// sized from the synthetic board adapter, drawn like Redraw()'s grid block.
static void renderGridScenario( SCENE3D_CTX& aCtx, GRID3D_TYPE aType )
{
TIER2_RIG rig( aCtx );
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
R3D_Generate3dGrid( *rig, aType );
glDisable( GL_LIGHTING );
const GLuint grid = R3D_GetGridList( *rig );
if( glIsList( grid ) )
glCallList( grid );
glEnable( GL_LIGHTING );
}
void Scenario_Grid1mm( SCENE3D_CTX& aCtx )
{
renderGridScenario( aCtx, GRID3D_TYPE::GRID_1MM );
}
void Scenario_Grid2p5mm( SCENE3D_CTX& aCtx )
{
renderGridScenario( aCtx, GRID3D_TYPE::GRID_2P5MM );
}
void Scenario_Grid5mm( SCENE3D_CTX& aCtx )
{
renderGridScenario( aCtx, GRID3D_TYPE::GRID_5MM );
}
void Scenario_Grid10mm( SCENE3D_CTX& aCtx )
{
renderGridScenario( aCtx, GRID3D_TYPE::GRID_10MM );
}
// Local extruded-plate builder (same construction as the tier-1 TDL scenarios).
static void addFan( TRIANGLE_LIST* aDst, const std::vector<SFVEC2F>& aContour, float aZ,
bool aTop )
{
const SFVEC2F& v0 = aContour.front();
for( size_t i = 1; i + 1 < aContour.size(); i++ )
{
const SFVEC2F& v1 = aContour[i];
const SFVEC2F& v2 = aContour[i + 1];
if( aTop )
aDst->AddTriangle( SFVEC3F( v0.x, v0.y, aZ ), SFVEC3F( v1.x, v1.y, aZ ),
SFVEC3F( v2.x, v2.y, aZ ) );
else
aDst->AddTriangle( SFVEC3F( v2.x, v2.y, aZ ), SFVEC3F( v1.x, v1.y, aZ ),
SFVEC3F( v0.x, v0.y, aZ ) );
}
}
static std::unique_ptr<OPENGL_RENDER_LIST> makePlate( SCENE3D_CTX& aCtx, float aHalf, float aZBot,
float aZTop )
{
const std::vector<SFVEC2F> contour = MakeSquareContour( aHalf );
TRIANGLE_DISPLAY_LIST tdl( 64 );
addFan( tdl.m_layer_top_triangles, contour, aZTop, true );
addFan( tdl.m_layer_bot_triangles, contour, aZBot, false );
tdl.AddToMiddleContours( contour, aZBot, aZTop, true );
return std::unique_ptr<OPENGL_RENDER_LIST>( aCtx.MakeRenderList( tdl, aZBot, aZTop ) );
}
// 42: setupMaterials + setLayerMaterial — material swatch plates for the
// technical layers (silk, mask, paste, copper).
void Scenario_LayerMaterials( SCENE3D_CTX& aCtx )
{
TIER2_RIG rig( aCtx );
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
R3D_SetupMaterials( *rig );
const PCB_LAYER_ID layers[4] = { F_SilkS, F_Mask, F_Paste, B_Cu };
// Mask is translucent — same blend state the transparent passes use.
glEnable( GL_BLEND );
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
for( int i = 0; i < 4; i++ )
{
R3D_SetLayerMaterial( *rig, layers[i] );
glPushMatrix();
glTranslatef( ( i - 1.5f ) * 3.4f, 0.0f, 0.0f );
makePlate( aCtx, 1.5f, -0.4f, 0.4f )->DrawAll();
glPopMatrix();
}
glDisable( GL_BLEND );
}
// 43: setArrowMaterial + glColor axes — the Redraw() show_axis block.
void Scenario_ArrowMaterial( SCENE3D_CTX& aCtx )
{
TIER2_RIG rig( aCtx );
aCtx.SetIsoView();
aCtx.BeginFrame();
aCtx.SetupLights();
R3D_SetArrowMaterial( *rig );
const float arrow_size = SCENE3D_RANGE_SCALE_3D * 0.30f;
glColor3f( 0.9f, 0.0f, 0.0f );
DrawRoundArrow( SFVEC3F( 0.0f ), SFVEC3F( arrow_size, 0.0f, 0.0f ), 0.275f );
glColor3f( 0.0f, 0.9f, 0.0f );
DrawRoundArrow( SFVEC3F( 0.0f ), SFVEC3F( 0.0f, arrow_size, 0.0f ), 0.275f );
glColor3f( 0.0f, 0.0f, 0.9f );
DrawRoundArrow( SFVEC3F( 0.0f ), SFVEC3F( 0.0f, 0.0f, arrow_size ), 0.275f );
}
// 44: createBoard — the real board-outline-to-display-list path (SHAPE_POLY_SET
// triangulation + middle contours), drawn like renderBoardBody().
void Scenario_CreateBoard( SCENE3D_CTX& aCtx )
{
TIER2_RIG rig( aCtx );
beginTier2Scene( aCtx );
std::unique_ptr<OPENGL_RENDER_LIST> board(
R3D_CreateBoard( *rig, rig.m_adapter.GetBoardPoly(), &rig.m_adapter.GetTH_ODs() ) );
// renderBoardBody() material + transform (render_3d_opengl.cpp:468-501).
SMATERIAL epoxy;
epoxy.m_Ambient = SFVEC3F( 0.1f, 0.1f, 0.12f );
epoxy.m_Diffuse = SFVEC3F( 0.4f, 0.4f, 0.5f );
epoxy.m_Emissive = SFVEC3F( 0.0f, 0.0f, 0.0f );
epoxy.m_Specular = SFVEC3F( 0.2f, 0.2f, 0.2f );
epoxy.m_Shininess = 0.3f;
epoxy.m_Transparency = 0.1f;
OglSetMaterial( epoxy, 1.0f );
board->ApplyScalePosition( -rig.m_adapter.GetBoardBodyThickness() / 2.0f,
rig.m_adapter.GetBoardBodyThickness() );
board->SetItIsTransparent( true );
board->DrawAll();
}

View file

@ -0,0 +1,65 @@
/**
* Tier-3 scenarios: full RENDER_3D_OPENGL::Redraw() composites over the
* synthetic mini-board (board_adapter_test_impl.cpp InitSettings). Redraw's
* reload() re-runs InitSettings itself, so these exercise the complete real
* pipeline: board body triangulation, layer display lists, stencil hole
* subtraction, solder mask transparency, grid list and the navigator gizmo.
*/
#include "scene3d_test_ctx.h"
#include "scene3d_test_rig.h"
// The composites drive Redraw() exactly like EDA_3D_CANVAS::DoRePaint does:
// window size, reload request, then Redraw(aIsMoving=false, no reporters).
static void runRedraw( SCENE3D_CTX& aCtx, SCENE3D_TEST_RIG& aRig )
{
aRig->SetCurWindowSize( wxSize( aCtx.m_width, aCtx.m_height ) );
aRig->ReloadRequest();
aRig->Redraw( false, nullptr, nullptr );
}
// 45: Redraw with every layer hidden — background, camera matrices, lights and
// the frame plumbing only (the "empty viewer" reference frame).
void Scenario_RedrawEmpty( SCENE3D_CTX& aCtx )
{
SCENE3D_TEST_RIG rig( aCtx );
rig.m_cfg.m_Render.show_board_body = false;
rig.m_cfg.m_Render.show_copper_top = false;
rig.m_cfg.m_Render.show_copper_bottom = false;
rig.m_cfg.m_Render.show_silkscreen_top = false;
rig.m_cfg.m_Render.show_silkscreen_bottom = false;
rig.m_cfg.m_Render.show_soldermask_top = false;
rig.m_cfg.m_Render.show_soldermask_bottom = false;
rig.m_cfg.m_Render.show_solderpaste = false;
rig.m_cfg.m_Render.show_adhesive = false;
rig.m_cfg.m_Render.show_plated_barrels = false;
aCtx.SetIsoView();
runRedraw( aCtx, rig );
}
// 46: the full synthetic two-layer mini-board — copper tracks/pads with
// stencil-subtracted through holes, silkscreen frame, translucent solder mask
// and board body.
void Scenario_RedrawMiniBoard( SCENE3D_CTX& aCtx )
{
SCENE3D_TEST_RIG rig( aCtx );
aCtx.SetIsoView();
runRedraw( aCtx, rig );
}
// 47: everything at once — mini-board plus the 5mm grid and the navigator
// spheres gizmo, straight top view. The port-complete gate.
void Scenario_RedrawMiniBoardNavigator( SCENE3D_CTX& aCtx )
{
SCENE3D_TEST_RIG rig( aCtx );
rig.m_cfg.m_Render.grid_type = GRID3D_TYPE::GRID_5MM;
rig.m_cfg.m_Render.show_navigator = true;
aCtx.ResetCamera();
aCtx.SetView( VIEW3D_TYPE::VIEW3D_TOP );
runRedraw( aCtx, rig );
}

View file

@ -0,0 +1,209 @@
#include "scene3d_test_ctx.h"
#include "3d_math.h" // SphericalToCartesian (inline, 3d-viewer/3d_math.h)
#include "3d_rendering/image.h" // IMAGE for the circle texture
#include "common_ogl/ogl_utils.h" // OglResetTextureState, OglDrawBackground, OglLoadTexture
#include <glm/ext.hpp> // value_ptr
// SIZE_OF_CIRCLE_TEXTURE lives in render_3d_opengl.h (too heavy for Stage 1);
// keep the value in sync (render_3d_opengl.h:53).
static constexpr int CIRCLE_TEXTURE_SIZE = 1024;
// Same premultiply the renderer applies before OglDrawBackground
// (render_3d_opengl.cpp:505-508).
static inline SFVEC4F premultiplyAlpha( const SFVEC4F& aInput )
{
return SFVEC4F( aInput.r * aInput.a, aInput.g * aInput.a, aInput.b * aInput.a, aInput.a );
}
SCENE3D_CTX::SCENE3D_CTX( int aWidth, int aHeight ) :
m_width( aWidth ),
m_height( aHeight ),
m_camera( 2.0f * SCENE3D_RANGE_SCALE_3D )
{
ResetCamera();
}
void SCENE3D_CTX::ResetCamera()
{
m_camera.SetProjection( PROJECTION_TYPE::PERSPECTIVE );
m_camera.SetCurWindowSize( wxSize( m_width, m_height ) );
m_camera.Reset();
}
void SCENE3D_CTX::SetView( VIEW3D_TYPE aView )
{
// The settled end state of EDA_3D_CANVAS::SetView3D's animation
// (eda_3d_canvas.cpp:1455-1481 + the Interpolate(1.0f) at :1354).
m_camera.SetT0_and_T1_current_T();
m_camera.ViewCommand_T1( aView );
m_camera.SetInterpolateMode( CAMERA_INTERPOLATION::LINEAR );
m_camera.Interpolate( 1.0f );
}
void SCENE3D_CTX::SetIsoView()
{
ResetCamera();
// Tilt board toward the viewer, then spin — a deterministic 3/4 view that
// shows top faces, side walls and lighting gradients at once.
m_camera.RotateX( -glm::pi<float>() / 3.0f ); // -60°
m_camera.RotateZ( glm::pi<float>() / 6.0f ); // +30°
}
void SCENE3D_CTX::SetOrtho( bool aOrtho )
{
m_camera.SetProjection( aOrtho ? PROJECTION_TYPE::ORTHO : PROJECTION_TYPE::PERSPECTIVE );
// Force a projection rebuild for the (possibly unchanged) window size:
// SetCurWindowSize only rebuilds on size change, so nudge through Reset-safe API.
m_camera.SetCurWindowSize( wxSize( m_width, m_height - 1 ) );
m_camera.SetCurWindowSize( wxSize( m_width, m_height ) );
}
void SCENE3D_CTX::BeginFrame()
{
// Default viewer background (BOARD_ADAPTER ctor, board_adapter.cpp:132-133).
BeginFrame( SFVEC4F( 0.8f, 0.8f, 0.9f, 1.0f ), SFVEC4F( 0.4f, 0.4f, 0.5f, 1.0f ) );
}
void SCENE3D_CTX::BeginFrame( const SFVEC4F& aBgTop, const SFVEC4F& aBgBot )
{
// Per-frame state block replicated verbatim from RENDER_3D_OPENGL::Redraw()
// (render_3d_opengl.cpp:553-586). Kept in the same order so the state the
// scenarios render under is auditable against the real renderer.
glDepthFunc( GL_LESS );
glEnable( GL_CULL_FACE );
glFrontFace( GL_CCW );
glEnable( GL_NORMALIZE );
glViewport( 0, 0, m_width, m_height );
glEnable( GL_MULTISAMPLE );
glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
glClearDepth( 1.0f );
glClearStencil( 0x00 );
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT );
OglResetTextureState();
OglDrawBackground( premultiplyAlpha( aBgTop ), premultiplyAlpha( aBgBot ) );
glEnable( GL_DEPTH_TEST );
glMatrixMode( GL_PROJECTION );
glLoadMatrixf( glm::value_ptr( m_camera.GetProjectionMatrix() ) );
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
glLoadMatrixf( glm::value_ptr( m_camera.GetViewMatrix() ) );
}
void SCENE3D_CTX::SetupLights()
{
// Per-frame part only: Redraw() enables the lights and repositions the
// headlight after loading the camera matrices (render_3d_opengl.cpp:589-611).
// The light PARAMETERS were set once by initLights() at init time — GL bakes
// directional-light positions in eye space using the modelview current at
// the glLightfv call, and the real renderer sets them under the identity
// matrix of a fresh context (initializeOpenGL -> init_lights).
EnableLights( true, true, true );
glEnable( GL_LIGHTING );
PositionHeadlight();
}
// The real light-rig initializer from render_3d_opengl.cpp:401 (free function
// with external linkage; linked since Stage 2).
void init_lights();
void SCENE3D_CTX::initLights()
{
::init_lights();
}
void SCENE3D_CTX::EnableLights( bool aFront, bool aTop, bool aBottom )
{
// Same GL_LIGHTx mapping as RENDER_3D_OPENGL::setLightFront/Top/Bottom
// (render_3d_opengl.cpp:128-148).
if( aFront )
glEnable( GL_LIGHT0 );
else
glDisable( GL_LIGHT0 );
if( aTop )
glEnable( GL_LIGHT1 );
else
glDisable( GL_LIGHT1 );
if( aBottom )
glEnable( GL_LIGHT2 );
else
glDisable( GL_LIGHT2 );
}
void SCENE3D_CTX::PositionHeadlight()
{
// Exact headlight placement from Redraw() (render_3d_opengl.cpp:595-611).
const SFVEC3F& cameraPos = m_camera.GetPos();
float zpos;
if( cameraPos.z > 0.0f )
zpos = glm::max( cameraPos.z, 0.5f ) + cameraPos.z * cameraPos.z;
else
zpos = glm::min( cameraPos.z, -0.5f ) - cameraPos.z * cameraPos.z;
const GLfloat headlight_pos[] = { cameraPos.x, cameraPos.y, zpos, 1.0f };
glLightfv( GL_LIGHT0, GL_POSITION, headlight_pos );
}
void SCENE3D_CTX::InitOnce()
{
if( m_circleTexture )
return;
// Replicates RENDER_3D_OPENGL::initializeOpenGL() (render_3d_opengl.cpp:858-896)
// minus init_lights() (SetupLights) and m_canvasInitialized bookkeeping.
glEnable( GL_LINE_SMOOTH );
glShadeModel( GL_SMOOTH );
glPixelStorei( GL_UNPACK_ALIGNMENT, 4 );
IMAGE circleImage( CIRCLE_TEXTURE_SIZE, CIRCLE_TEXTURE_SIZE );
const unsigned int circleRadius = ( CIRCLE_TEXTURE_SIZE / 2 ) - 4;
circleImage.CircleFilled( ( CIRCLE_TEXTURE_SIZE / 2 ) - 0, ( CIRCLE_TEXTURE_SIZE / 2 ) - 0,
circleRadius, 0xFF );
IMAGE circleImageBlured( circleImage.GetWidth(), circleImage.GetHeight() );
circleImageBlured.EfxFilter_SkipCenter( &circleImage, IMAGE_FILTER::GAUSSIAN_BLUR,
circleRadius - 8 );
m_circleTexture = OglLoadTexture( circleImageBlured );
// initializeOpenGL() ends with init_lights(): the directional lights are
// baked in EYE space under the fresh context's identity modelview — that is
// why the viewer's scene lighting follows the camera. Keep that semantic.
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
initLights();
}
OPENGL_RENDER_LIST* SCENE3D_CTX::MakeRenderList( const TRIANGLE_DISPLAY_LIST& aTdl, float aZBot,
float aZTop ) const
{
return new OPENGL_RENDER_LIST( aTdl, m_circleTexture, aZBot, aZTop );
}

View file

@ -0,0 +1,96 @@
/**
* SCENE3D_CTX — the scenario seam for the 3D-renderer regression suite.
*
* The ctx provides frame plumbing only: viewport, camera matrices, buffer
* clears and light setup, replicating RENDER_3D_OPENGL::Redraw()'s per-frame
* state block (render_3d_opengl.cpp:553-611) so that primitive scenarios render
* under the same GL state as the real viewer. Everything that produces pixels
* (geometry, materials, display lists, textures) must be real KiCad code
* called from the scenario bodies.
*
* This header compiles for the native macOS build AND (later) under
* emscripten — no native-only includes.
*/
#ifndef SCENE3D_TEST_CTX_H
#define SCENE3D_TEST_CTX_H
#include <kicad_gl/kiglu.h> // GL + GLU, platform-routed (glad native / GLES shim on wasm)
#include <gal/3d/camera.h>
#include "3d_rendering/track_ball.h"
#include "3d_rendering/opengl/layer_triangles.h"
// == RANGE_SCALE_3D from 3d-viewer/3d_canvas/board_adapter.h (that header is too
// heavy to pull into every scenario TU). EDA_3D_VIEWER_FRAME constructs its
// TRACK_BALL with 2 * RANGE_SCALE_3D (eda_3d_viewer_frame.cpp).
static constexpr float SCENE3D_RANGE_SCALE_3D = 8.0f;
struct SCENE3D_CTX
{
int m_width;
int m_height;
TRACK_BALL m_camera;
SCENE3D_CTX( int aWidth, int aHeight );
// ---- Camera helpers (all real CAMERA / TRACK_BALL API) ----
/// Perspective projection, window size set, Reset() — the straight top-down default.
void ResetCamera();
/// Apply a preset view the way EDA_3D_CANVAS does, but settled instantly:
/// SetT0_and_T1_current_T() -> ViewCommand_T1(aView) -> Interpolate(1.0f).
void SetView( VIEW3D_TYPE aView );
/// Deterministic 3/4 view for lit-geometry scenarios: Reset + RotateX/RotateZ.
void SetIsoView();
void SetOrtho( bool aOrtho );
// ---- Frame plumbing (mirrors RENDER_3D_OPENGL::Redraw() lines 553-611) ----
/// Clears + background gradient + camera matrix upload. Default viewer
/// background colors (BOARD_ADAPTER ctor: top 0.8,0.8,0.9 / bot 0.4,0.4,0.5).
void BeginFrame();
void BeginFrame( const SFVEC4F& aBgTop, const SFVEC4F& aBgBot );
// ---- Lights ----
/// Per-frame light enable + headlight placement, as Redraw() does after
/// loading the camera matrices (render_3d_opengl.cpp:589-611). The light
/// parameters themselves were baked at init time (InitOnce -> initLights).
void SetupLights();
/// glEnable/glDisable GL_LIGHT0 (headlight/front), GL_LIGHT1 (top), GL_LIGHT2 (bottom)
/// — same mapping as RENDER_3D_OPENGL::setLightFront/Top/Bottom.
void EnableLights( bool aFront, bool aTop, bool aBottom );
/// Exact headlight placement formula from Redraw() (render_3d_opengl.cpp:595-611).
void PositionHeadlight();
// ---- Renderer-init state + circle texture ----
/// One-time GL state + the segment-ends circle texture, replicating the
/// non-member-state part of RENDER_3D_OPENGL::initializeOpenGL()
/// (render_3d_opengl.cpp:858-896): GL_LINE_SMOOTH, glShadeModel(GL_SMOOTH),
/// GL_UNPACK_ALIGNMENT=4, then the real IMAGE::CircleFilled +
/// EfxFilter_SkipCenter(GAUSSIAN_BLUR) + OglLoadTexture recipe.
void InitOnce();
GLuint GetCircleTexture() const { return m_circleTexture; }
/// Wrap a filled TRIANGLE_DISPLAY_LIST into GL display lists — thin sugar
/// over the real OPENGL_RENDER_LIST ctor with the ctx circle texture.
OPENGL_RENDER_LIST* MakeRenderList( const TRIANGLE_DISPLAY_LIST& aTdl, float aZBot,
float aZTop ) const;
private:
/// Stage-1 verbatim copy of ::init_lights() (render_3d_opengl.cpp:401-445);
/// becomes a call to the real free function once Stage 2 links it.
void initLights();
GLuint m_circleTexture = 0;
};
#endif // SCENE3D_TEST_CTX_H

View file

@ -0,0 +1,44 @@
/**
* Shared Tier-2/Tier-3 rig: stub settings + synthetic BOARD_ADAPTER
* (native/board_adapter_test_impl.cpp InitSettings) + a real RENDER_3D_OPENGL
* bound to the scenario camera, initialized the way the first real Redraw()
* would (initializeOpenGL under the fresh identity modelview — init_lights
* re-runs identically, keeping the directional lights eye-anchored).
*/
#ifndef SCENE3D_TEST_RIG_H
#define SCENE3D_TEST_RIG_H
#include "scene3d_test_ctx.h"
#include "../native/render3d_test_accessor.h"
#include "3d_canvas/board_adapter.h"
#include "3d_rendering/opengl/render_3d_opengl.h"
#include "3d_viewer/eda_3d_viewer_settings.h"
#include <memory>
struct SCENE3D_TEST_RIG
{
EDA_3D_VIEWER_SETTINGS m_cfg;
BOARD_ADAPTER m_adapter;
std::unique_ptr<RENDER_3D_OPENGL> m_renderer;
explicit SCENE3D_TEST_RIG( SCENE3D_CTX& aCtx )
{
m_adapter.m_Cfg = &m_cfg;
m_adapter.InitSettings( nullptr, nullptr );
m_renderer = std::make_unique<RENDER_3D_OPENGL>( nullptr, m_adapter, aCtx.m_camera );
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
R3D_InitializeOpenGL( *m_renderer );
}
RENDER_3D_OPENGL& operator*() { return *m_renderer; }
RENDER_3D_OPENGL* operator->() { return m_renderer.get(); }
};
#endif // SCENE3D_TEST_RIG_H

View file

@ -0,0 +1,146 @@
#include "scene3d_test_scenarios.h"
#include "scene3d_test_ctx.h"
#include <wx/debug.h>
// Tier-1 scenario functions (scenario_tier1_*.cpp)
void Scenario_BgGradient( SCENE3D_CTX& aCtx );
void Scenario_BgGradientAlpha( SCENE3D_CTX& aCtx );
void Scenario_BoundingBox( SCENE3D_CTX& aCtx );
void Scenario_HalfOpenCylinder( SCENE3D_CTX& aCtx );
void Scenario_SegmentSingle( SCENE3D_CTX& aCtx );
void Scenario_SegmentsStar( SCENE3D_CTX& aCtx );
void Scenario_RoundArrow( SCENE3D_CTX& aCtx );
void Scenario_RoundArrowsAxes( SCENE3D_CTX& aCtx );
void Scenario_MaterialCopper( SCENE3D_CTX& aCtx );
void Scenario_MaterialDiffuseOnly( SCENE3D_CTX& aCtx );
void Scenario_MaterialTransparent( SCENE3D_CTX& aCtx );
void Scenario_LightFront( SCENE3D_CTX& aCtx );
void Scenario_LightTop( SCENE3D_CTX& aCtx );
void Scenario_LightBottom( SCENE3D_CTX& aCtx );
void Scenario_TdlDrawTop( SCENE3D_CTX& aCtx );
void Scenario_TdlDrawBot( SCENE3D_CTX& aCtx );
void Scenario_TdlDrawMiddle( SCENE3D_CTX& aCtx );
void Scenario_TdlDrawAll( SCENE3D_CTX& aCtx );
void Scenario_TdlSegEndsTexture( SCENE3D_CTX& aCtx );
void Scenario_TdlCulledStencil( SCENE3D_CTX& aCtx );
void Scenario_TdlZScale( SCENE3D_CTX& aCtx );
void Scenario_TdlTransparent( SCENE3D_CTX& aCtx );
void Scenario_Model3dOpaque( SCENE3D_CTX& aCtx );
void Scenario_Model3dTransparent( SCENE3D_CTX& aCtx );
void Scenario_Model3dMaterialModes( SCENE3D_CTX& aCtx );
void Scenario_Model3dBbox( SCENE3D_CTX& aCtx );
void Scenario_SpheresGizmo( SCENE3D_CTX& aCtx );
void Scenario_CameraPersp( SCENE3D_CTX& aCtx );
void Scenario_CameraOrtho( SCENE3D_CTX& aCtx );
void Scenario_CameraPresetViews( SCENE3D_CTX& aCtx );
// Tier-2 scenario functions (scenario_tier2_generators.cpp)
void Scenario_GenCylinder( SCENE3D_CTX& aCtx );
void Scenario_GenInvCone( SCENE3D_CTX& aCtx );
void Scenario_GenDisk( SCENE3D_CTX& aCtx );
void Scenario_GenDimple( SCENE3D_CTX& aCtx );
void Scenario_AddObjAllShapes( SCENE3D_CTX& aCtx );
void Scenario_PostMachining( SCENE3D_CTX& aCtx );
void Scenario_ViaComposite( SCENE3D_CTX& aCtx );
void Scenario_Grid1mm( SCENE3D_CTX& aCtx );
void Scenario_Grid2p5mm( SCENE3D_CTX& aCtx );
void Scenario_Grid5mm( SCENE3D_CTX& aCtx );
void Scenario_Grid10mm( SCENE3D_CTX& aCtx );
void Scenario_LayerMaterials( SCENE3D_CTX& aCtx );
void Scenario_ArrowMaterial( SCENE3D_CTX& aCtx );
void Scenario_CreateBoard( SCENE3D_CTX& aCtx );
// Tier-3 scenario functions (scenario_tier3_composite.cpp)
void Scenario_RedrawEmpty( SCENE3D_CTX& aCtx );
void Scenario_RedrawMiniBoard( SCENE3D_CTX& aCtx );
void Scenario_RedrawMiniBoardNavigator( SCENE3D_CTX& aCtx );
namespace Scene3DTest
{
struct SCENARIO_ENTRY
{
const char* name; // becomes 3d-<name>.png — append-only, never rename
void ( *render )( SCENE3D_CTX& );
};
static const SCENARIO_ENTRY SCENARIOS[] = {
{ "bg-gradient", Scenario_BgGradient },
{ "bg-gradient-alpha", Scenario_BgGradientAlpha },
{ "bounding-box", Scenario_BoundingBox },
{ "half-open-cylinder", Scenario_HalfOpenCylinder },
{ "segment-single", Scenario_SegmentSingle },
{ "segments-star", Scenario_SegmentsStar },
{ "round-arrow", Scenario_RoundArrow },
{ "round-arrows-axes", Scenario_RoundArrowsAxes },
{ "material-copper", Scenario_MaterialCopper },
{ "material-diffuse-only", Scenario_MaterialDiffuseOnly },
{ "material-transparent", Scenario_MaterialTransparent },
{ "light-front", Scenario_LightFront },
{ "light-top", Scenario_LightTop },
{ "light-bottom", Scenario_LightBottom },
{ "tdl-draw-top", Scenario_TdlDrawTop },
{ "tdl-draw-bot", Scenario_TdlDrawBot },
{ "tdl-draw-middle", Scenario_TdlDrawMiddle },
{ "tdl-draw-all", Scenario_TdlDrawAll },
{ "tdl-seg-ends-texture", Scenario_TdlSegEndsTexture },
{ "tdl-culled-stencil", Scenario_TdlCulledStencil },
{ "tdl-zscale", Scenario_TdlZScale },
{ "tdl-transparent", Scenario_TdlTransparent },
{ "model3d-opaque", Scenario_Model3dOpaque },
{ "model3d-transparent", Scenario_Model3dTransparent },
{ "model3d-material-modes", Scenario_Model3dMaterialModes },
{ "model3d-bbox", Scenario_Model3dBbox },
{ "spheres-gizmo", Scenario_SpheresGizmo },
{ "camera-persp", Scenario_CameraPersp },
{ "camera-ortho", Scenario_CameraOrtho },
{ "camera-preset-views", Scenario_CameraPresetViews },
{ "gen-cylinder", Scenario_GenCylinder },
{ "gen-invcone", Scenario_GenInvCone },
{ "gen-disk", Scenario_GenDisk },
{ "gen-dimple", Scenario_GenDimple },
{ "addobj-all-shapes", Scenario_AddObjAllShapes },
{ "post-machining", Scenario_PostMachining },
{ "via-composite", Scenario_ViaComposite },
{ "grid-1mm", Scenario_Grid1mm },
{ "grid-2p5mm", Scenario_Grid2p5mm },
{ "grid-5mm", Scenario_Grid5mm },
{ "grid-10mm", Scenario_Grid10mm },
{ "layer-materials", Scenario_LayerMaterials },
{ "arrow-material", Scenario_ArrowMaterial },
{ "create-board", Scenario_CreateBoard },
{ "redraw-empty", Scenario_RedrawEmpty },
{ "redraw-mini-board", Scenario_RedrawMiniBoard },
{ "redraw-mini-board-navigator", Scenario_RedrawMiniBoardNavigator },
};
static const int SCENARIO_COUNT = sizeof( SCENARIOS ) / sizeof( SCENARIOS[0] );
int GetScenarioCount()
{
return SCENARIO_COUNT;
}
const char* GetScenarioName( int aIndex )
{
if( aIndex < 0 || aIndex >= SCENARIO_COUNT )
return nullptr;
return SCENARIOS[aIndex].name;
}
void RenderScenario( SCENE3D_CTX& aCtx, int aIndex )
{
wxASSERT( aIndex >= 0 && aIndex < SCENARIO_COUNT );
if( aIndex < 0 || aIndex >= SCENARIO_COUNT )
return;
SCENARIOS[aIndex].render( aCtx );
}
} // namespace Scene3DTest

View file

@ -0,0 +1,26 @@
/**
* Scenario registry for the 3D-renderer regression suite.
*
* Scenario names are the single source of truth: they become the committed
* baseline filenames (3d-<name>.png), the entries of manifest.json and later
* the WebGL-port test IDs. Names are append-only — never renumber or rename.
*/
#ifndef SCENE3D_TEST_SCENARIOS_H
#define SCENE3D_TEST_SCENARIOS_H
struct SCENE3D_CTX;
namespace Scene3DTest
{
int GetScenarioCount();
const char* GetScenarioName( int aIndex );
/// Render scenario aIndex. The scenario body calls aCtx.BeginFrame(...) itself
/// (some scenarios use custom background colors) and then real KiCad 3D code.
void RenderScenario( SCENE3D_CTX& aCtx, int aIndex );
} // namespace Scene3DTest
#endif // SCENE3D_TEST_SCENARIOS_H

View file

@ -0,0 +1,202 @@
#include "test_board_data.h"
#include <cmath>
#include <cstddef>
const BOARD_ITEM& DummyBoardItem()
{
// The reference is stored by OBJECT_2D but never dereferenced (object_2d.h:114);
// aligned opaque storage stands in so no pcbnew types need to link.
alignas( 16 ) static unsigned char storage[256] = {};
return *reinterpret_cast<const BOARD_ITEM*>( storage );
}
// ---- S3DMODEL widget -------------------------------------------------------
// Static storage: S3DMODEL/SMESH point into these arrays (the struct carries
// raw pointers; MODEL_3D copies everything into VBOs on construction).
// Axis-aligned box: 24 vertices (4 per face, flat normals), 36 indices.
static void fillBox( SFVEC3F* aPos, SFVEC3F* aNorm, unsigned int* aIdx, const SFVEC3F& aMin,
const SFVEC3F& aMax )
{
const SFVEC3F n[6] = {
{ 0, 0, 1 }, { 0, 0, -1 }, { 1, 0, 0 }, { -1, 0, 0 }, { 0, 1, 0 }, { 0, -1, 0 },
};
// 4 corners per face, CCW seen from outside.
const SFVEC3F c[6][4] = {
// +Z
{ { aMin.x, aMin.y, aMax.z }, { aMax.x, aMin.y, aMax.z }, { aMax.x, aMax.y, aMax.z },
{ aMin.x, aMax.y, aMax.z } },
// -Z
{ { aMin.x, aMin.y, aMin.z }, { aMin.x, aMax.y, aMin.z }, { aMax.x, aMax.y, aMin.z },
{ aMax.x, aMin.y, aMin.z } },
// +X
{ { aMax.x, aMin.y, aMin.z }, { aMax.x, aMax.y, aMin.z }, { aMax.x, aMax.y, aMax.z },
{ aMax.x, aMin.y, aMax.z } },
// -X
{ { aMin.x, aMin.y, aMin.z }, { aMin.x, aMin.y, aMax.z }, { aMin.x, aMax.y, aMax.z },
{ aMin.x, aMax.y, aMin.z } },
// +Y
{ { aMin.x, aMax.y, aMin.z }, { aMin.x, aMax.y, aMax.z }, { aMax.x, aMax.y, aMax.z },
{ aMax.x, aMax.y, aMin.z } },
// -Y
{ { aMin.x, aMin.y, aMin.z }, { aMax.x, aMin.y, aMin.z }, { aMax.x, aMin.y, aMax.z },
{ aMin.x, aMin.y, aMax.z } },
};
for( int f = 0; f < 6; f++ )
{
for( int v = 0; v < 4; v++ )
{
aPos[f * 4 + v] = c[f][v];
aNorm[f * 4 + v] = n[f];
}
aIdx[f * 6 + 0] = f * 4 + 0;
aIdx[f * 6 + 1] = f * 4 + 1;
aIdx[f * 6 + 2] = f * 4 + 2;
aIdx[f * 6 + 3] = f * 4 + 0;
aIdx[f * 6 + 4] = f * 4 + 2;
aIdx[f * 6 + 5] = f * 4 + 3;
}
}
// Octahedron: 8 triangular faces, 24 vertices (flat normals), 24 indices.
static void fillOctahedron( SFVEC3F* aPos, SFVEC3F* aNorm, unsigned int* aIdx,
const SFVEC3F& aCenter, float aRadius )
{
const SFVEC3F apexTop = aCenter + SFVEC3F( 0, 0, aRadius );
const SFVEC3F apexBot = aCenter - SFVEC3F( 0, 0, aRadius );
const SFVEC3F equator[4] = {
aCenter + SFVEC3F( aRadius, 0, 0 ),
aCenter + SFVEC3F( 0, aRadius, 0 ),
aCenter + SFVEC3F( -aRadius, 0, 0 ),
aCenter + SFVEC3F( 0, -aRadius, 0 ),
};
unsigned int v = 0;
for( int i = 0; i < 4; i++ )
{
const SFVEC3F& e0 = equator[i];
const SFVEC3F& e1 = equator[( i + 1 ) % 4];
// top face (CCW from outside)
SFVEC3F nTop = glm::normalize( glm::cross( e1 - e0, apexTop - e0 ) );
aPos[v] = e0;
aPos[v + 1] = e1;
aPos[v + 2] = apexTop;
aNorm[v] = aNorm[v + 1] = aNorm[v + 2] = nTop;
aIdx[v] = v;
aIdx[v + 1] = v + 1;
aIdx[v + 2] = v + 2;
v += 3;
// bottom face
SFVEC3F nBot = glm::normalize( glm::cross( apexBot - e0, e1 - e0 ) );
aPos[v] = e1;
aPos[v + 1] = e0;
aPos[v + 2] = apexBot;
aNorm[v] = aNorm[v + 1] = aNorm[v + 2] = nBot;
aIdx[v] = v;
aIdx[v + 1] = v + 1;
aIdx[v + 2] = v + 2;
v += 3;
}
}
const S3DMODEL& TestS3DModel()
{
// mesh 0: opaque red plastic box
static SFVEC3F boxPos[24];
static SFVEC3F boxNorm[24];
static unsigned int boxIdx[36];
// mesh 1: transparent blue octahedron
static SFVEC3F octPos[24];
static SFVEC3F octNorm[24];
static unsigned int octIdx[24];
// mesh 2: per-vertex-colored box (m_Color array exercises GL_COLOR_ARRAY)
static SFVEC3F colBoxPos[24];
static SFVEC3F colBoxNorm[24];
static SFVEC3F colBoxColor[24];
static unsigned int colBoxIdx[36];
static SMESH meshes[3];
static SMATERIAL materials[3];
static S3DMODEL model;
static bool initialized = false;
if( !initialized )
{
initialized = true;
fillBox( boxPos, boxNorm, boxIdx, SFVEC3F( -2.2f, -1.2f, 0.0f ),
SFVEC3F( -0.2f, 1.2f, 1.2f ) );
fillOctahedron( octPos, octNorm, octIdx, SFVEC3F( 1.4f, 0.0f, 0.9f ), 1.1f );
fillBox( colBoxPos, colBoxNorm, colBoxIdx, SFVEC3F( -0.6f, -2.4f, 0.0f ),
SFVEC3F( 1.0f, -1.2f, 0.7f ) );
for( int i = 0; i < 24; i++ )
{
colBoxColor[i] = SFVEC3F( ( i % 3 ) == 0 ? 1.0f : 0.2f, ( i % 3 ) == 1 ? 1.0f : 0.2f,
( i % 3 ) == 2 ? 1.0f : 0.2f );
}
meshes[0] = { 24, boxPos, boxNorm, nullptr, nullptr, 36, boxIdx, 0 };
meshes[1] = { 24, octPos, octNorm, nullptr, nullptr, 24, octIdx, 1 };
meshes[2] = { 24, colBoxPos, colBoxNorm, nullptr, colBoxColor, 36, colBoxIdx, 2 };
// { Ambient, Diffuse, Emissive, Specular, Shininess, Transparency }
materials[0] = { { 0.30f, 0.05f, 0.05f }, { 0.80f, 0.10f, 0.10f }, { 0, 0, 0 },
{ 0.30f, 0.30f, 0.30f }, 0.30f, 0.00f };
materials[1] = { { 0.05f, 0.05f, 0.30f }, { 0.15f, 0.25f, 0.90f }, { 0, 0, 0 },
{ 0.60f, 0.60f, 0.70f }, 0.80f, 0.50f };
materials[2] = { { 0.15f, 0.15f, 0.15f }, { 0.70f, 0.70f, 0.70f }, { 0, 0, 0 },
{ 0.90f, 0.90f, 0.90f }, 0.90f, 0.00f };
model = { 3, meshes, 3, materials };
}
return model;
}
std::vector<SFVEC2F> MakeSquareContour( float aHalf, float aCenterX, float aCenterY )
{
// CCW, closed (first point repeated last) — AddToMiddleContours processes
// size-1 segments (layer_triangles.cpp:123-133).
return {
SFVEC2F( aCenterX - aHalf, aCenterY - aHalf ),
SFVEC2F( aCenterX + aHalf, aCenterY - aHalf ),
SFVEC2F( aCenterX + aHalf, aCenterY + aHalf ),
SFVEC2F( aCenterX - aHalf, aCenterY + aHalf ),
SFVEC2F( aCenterX - aHalf, aCenterY - aHalf ),
};
}
std::vector<SFVEC2F> MakeCircleContour( float aRadius, int aSides, float aCenterX,
float aCenterY )
{
std::vector<SFVEC2F> points;
points.reserve( aSides + 1 );
for( int i = 0; i < aSides; i++ )
{
const float a = 2.0f * static_cast<float>( M_PI ) * i / aSides;
points.emplace_back( aCenterX + aRadius * std::cos( a ),
aCenterY + aRadius * std::sin( a ) );
}
points.push_back( points.front() );
return points;
}

View file

@ -0,0 +1,35 @@
/**
* Hand-built input DATA for the scenarios (geometry containers, S3DMODEL,
* contours). Data only — everything that draws must be real KiCad code.
*/
#ifndef TEST_BOARD_DATA_H
#define TEST_BOARD_DATA_H
#include <plugins/3dapi/c3dmodel.h> // S3DMODEL / SMESH / SMATERIAL
#include <plugins/3dapi/xv3d_types.h>
#include <vector>
// The shapes2D constructors take a BOARD_ITEM& that is only stored for later
// identification, never dereferenced by any code the suite runs
// (object_2d.h:114). Only the forward declaration exists here.
class BOARD_ITEM;
/// An opaque never-dereferenced BOARD_ITEM reference for the shapes2D ctors.
const BOARD_ITEM& DummyBoardItem();
/// A small multi-mesh, multi-material widget: opaque box + transparent
/// octahedron + per-vertex-colored box. Arrays live in static storage; the
/// returned struct stays valid for the process lifetime.
const S3DMODEL& TestS3DModel();
/// Closed CCW square contour (first point repeated last) for AddToMiddleContours.
std::vector<SFVEC2F> MakeSquareContour( float aHalf, float aCenterX = 0.0f,
float aCenterY = 0.0f );
/// Closed regular-polygon contour approximating a circle.
std::vector<SFVEC2F> MakeCircleContour( float aRadius, int aSides, float aCenterX = 0.0f,
float aCenterY = 0.0f );
#endif // TEST_BOARD_DATA_H