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,124 @@
# 3D Renderer Regression Suite (OpenGL → WebGL port)
Screenshot-baseline TDD harness for porting KiCad's 3D viewer OpenGL renderer
(`RENDER_3D_OPENGL`, pure GL 1.x fixed-function) to WebGL2 — the same approach
used for the 2D GAL port (`tests/gal-regression/`), but compared with the CI
pixelmatch engine instead of ImageMagick.
Shared C++ **scenarios** call real KiCad 3D-viewer code (draw helpers, display
lists, materials, camera, stencil hole-subtraction, VBO models…) and are
compiled two ways:
- **native/** — macOS app on real desktop OpenGL (2.1 compatibility context via
the vendored glad loader). Renders each scenario into a fixed 800×600
offscreen FBO (`GL_RGBA8` + `GL_DEPTH24_STENCIL8`, the
`EDA_3D_CANVAS::RenderToFrameBuffer` recipe) → **golden baselines**.
- **wasm/** — the same scenario TUs compiled with em++ (planned; red state
first: linked against `wasm/stubs/gl_ffp_stub.c` no-ops, every render blank
until the port makes them green).
## Directory layout
```
scenarios/ shared scenario sources (native + wasm) — the registry in
scene3d_test_scenarios.cpp is the single source of truth
native/ golden generator (CMake; homebrew wxWidgets + OpenGL.framework)
wasm/ WebGL harness (planned)
baseline/ committed native goldens: 3d-<name>.png
baseline-webgl/ committed browser renders (port era, CI-promoted)
output/ run output + diffs (gitignored)
manifest.json {width,height,scenarios[]} written by the native harness;
committed — the anti-drift anchor for specs and orchestrator
floors.json pixelmatch verdict floors per comparison level
```
## Running
```
./scripts/test-3d-regression.sh # build + render + gate (native; webgl when present)
./scripts/test-3d-regression.sh native # native phase only
./scripts/test-3d-regression.sh compare # comparisons only
./scripts/test-3d-regression.sh promote # promote output/native -> baseline/ (byte-diff guarded)
```
Logs land in `logs/test-3d-regression/` (build logs in `tests/logs/`).
## Comparison levels
Engine: `tests/tools/screenshots/compare-dirs.ts` (pixelmatch
`{threshold: 0.1, includeAA: false}` — AA edge pixels ignored), floors from
`floors.json`. Diff triptychs/heatmaps + `report.json` land in
`output/diff/<level>/`.
| Level | Pair | Floor (changedRatio) | Role |
|---|---|---|---|
| `native-self` | `baseline/` vs `output/native/` | 0.001 (measured noise: exactly 0 on Apple M5) | gating regression check on the dev Mac |
| `webgl-vs-native` | `baseline/` vs `output/webgl/` | 0.02, **report-only** | the TDD port-progress meter (`npm run 3d:check:parity`) |
| `webgl-self` | `baseline-webgl/` vs `output/webgl/` | 0.005 | browser regression anchor (once the port renders) |
npm scripts (from `tests/`): `3d:check`, `3d:check:webgl`, `3d:check:parity`,
`3d:compare` (generic dir pair), `3d:test:webgl`.
## Updating baselines
Baselines are generated on the dev Mac (CI has no native GL — same model as the
GAL suite). After an intentional render change:
1. `./scripts/test-3d-regression.sh native` (fails with triptychs in
`output/diff/native-self/` — eyeball them),
2. `./scripts/test-3d-regression.sh promote` (byte-diff-guarded copy, zero
churn) and commit `baseline/` + `manifest.json`.
The orchestrator `cmp`s the freshly-written manifest against the committed one
every run, so a scenario registry change can't silently drift past the
baselines. Scenario names are append-only — never rename or renumber (they are
the PNG names and the WebGL test IDs).
## TDD red state
Once `wasm/` exists, the Playwright spec (`tests/e2e/3d-webgl.spec.ts`) is
capture-only and stays green; the red signal is `npm run 3d:check:parity`
reporting ~100% changed for every scenario. Port progress = scenarios dropping
out of that report. `glLineWidth > 1` (model bbox scenario) has no WebGL
equivalent — expect that one to need quad emulation to go green.
## Scenario tiers (47 scenarios)
- **Tier 1 (30)** — standalone TUs: `opengl_utils`
(arrows/segments/bbox/half-cylinder), `ogl_utils` (background gradient,
materials, textures), `TRIANGLE_DISPLAY_LIST`/`OPENGL_RENDER_LIST` (display
lists, seg-ends alpha-test texture, `DrawCulled` stencil subtraction,
z-transform, transparency), `MODEL_3D` (VBO/IBO, material modes, bboxes),
`SPHERES_GIZMO`, camera (perspective/ortho/preset views, isolated lights).
- **Tier 2 (14)**`RENDER_3D_OPENGL` private generators
(`generateCylinder/Disk/Dimple/InvCone`, all five `addObjectTriangles`
overloads, `appendPostMachiningGeometry`, via composite, the four grid
densities, `setupMaterials`/`setLayerMaterial`/`setArrowMaterial`,
`createBoard`) via the rob-template accessor
(`native/render3d_test_accessor.*`) over the synthetic `BOARD_ADAPTER`
(`native/board_adapter_test_impl.cpp` — its `InitSettings` is the test-data
seam).
- **Tier 3 (3)** — full `reload()` + `Redraw()` composites over the synthetic
mini-board: `redraw-empty`, `redraw-mini-board` (copper/silk/mask/stencil
holes), `redraw-mini-board-navigator` (grid + gizmo — the port-complete
gate).
## Known upstream bug (documented by `3d-post-machining.png`)
`appendPostMachiningGeometry`'s COUNTERSINK path adds middle-contour quads
with `AddQuad` but never calls `AddNormal`, so the normals array ends up half
the vertex count and `OPENGL_RENDER_LIST::generate_middle_triangles` rejects
the whole middle list — a countersunk hole silently erases the walls of any
geometry batched into the same `TRIANGLE_DISPLAY_LIST` (the real viewer has
the same defect). The scenario keeps counterbore and countersink in separate
lists so the counterbore renders correctly while the countersink half records
the buggy (empty) upstream output.
Lighting semantics worth knowing: `init_lights()` runs once at context init
under the identity modelview, so the two directional lights are anchored in
**eye space** (they follow the camera) — the harness replicates that
(`SCENE3D_CTX::InitOnce`), and only the headlight is repositioned per frame
like `Redraw()` does. Also avoid toggling `GL_LIGHTx` between draws inside one
frame: the Apple GL driver drops the first draw after a mid-frame toggle (the
real renderer never does this; the isolated-light scenarios use one light per
frame instead).

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View file

@ -0,0 +1,14 @@
{
"native-self": {
"default": { "changedRatio": 0.001, "meanChannelGuard": 2.0 },
"overrides": {}
},
"webgl-vs-native": {
"default": { "changedRatio": 0.02, "meanChannelGuard": 4.0 },
"overrides": {}
},
"webgl-self": {
"default": { "changedRatio": 0.005, "meanChannelGuard": 2.0 },
"overrides": {}
}
}

View file

@ -0,0 +1,53 @@
{
"width": 800,
"height": 600,
"scenarios": [
"bg-gradient",
"bg-gradient-alpha",
"bounding-box",
"half-open-cylinder",
"segment-single",
"segments-star",
"round-arrow",
"round-arrows-axes",
"material-copper",
"material-diffuse-only",
"material-transparent",
"light-front",
"light-top",
"light-bottom",
"tdl-draw-top",
"tdl-draw-bot",
"tdl-draw-middle",
"tdl-draw-all",
"tdl-seg-ends-texture",
"tdl-culled-stencil",
"tdl-zscale",
"tdl-transparent",
"model3d-opaque",
"model3d-transparent",
"model3d-material-modes",
"model3d-bbox",
"spheres-gizmo",
"camera-persp",
"camera-ortho",
"camera-preset-views",
"gen-cylinder",
"gen-invcone",
"gen-disk",
"gen-dimple",
"addobj-all-shapes",
"post-machining",
"via-composite",
"grid-1mm",
"grid-2p5mm",
"grid-5mm",
"grid-10mm",
"layer-materials",
"arrow-material",
"create-board",
"redraw-empty",
"redraw-mini-board",
"redraw-mini-board-navigator"
]
}

View file

@ -0,0 +1,168 @@
# Native golden-baseline generator for the 3D-renderer regression suite.
# Compiles REAL KiCad 3D-viewer sources against desktop OpenGL (macOS 2.1
# compatibility context via the vendored glad loader — no GLEW).
# Modeled on tests/gal-regression/native/CMakeLists.txt.
cmake_minimum_required(VERSION 3.16)
project(scene3d_native_test)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(APPLE)
add_compile_definitions(GL_SILENCE_DEPRECATION)
endif()
# KiCad builds clipper2 with USINGZ (PUBLIC), so every TU touching clipper
# types must agree.
add_compile_definitions(USINGZ)
# System wxWidgets (homebrew)
set(wxWidgets_CONFIG_EXECUTABLE "/opt/homebrew/bin/wx-config")
find_package(wxWidgets REQUIRED COMPONENTS core base gl)
include(${wxWidgets_USE_FILE})
# OpenGL.framework provides GL and GLU on macOS
find_package(OpenGL REQUIRED)
# KiCad source root
set(KICAD_ROOT ${CMAKE_SOURCE_DIR}/../../../kicad)
# Vendored glad loader (the fork's kicad_gl/kiglad.h routes to it natively)
set(GLAD_SOURCES ${KICAD_ROOT}/thirdparty/glad/src/gl.c)
# Real KiCad 3D-viewer TUs — Stage 1 (standalone, no RENDER_3D_OPENGL members).
# Stage 2 adds render_3d_opengl.cpp / create_scene.cpp + the board_adapter /
# settings test impls (see the suite README).
set(KICAD_3D_SOURCES
${KICAD_ROOT}/3d-viewer/common_ogl/ogl_utils.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/image.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/buffers_debug.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/color_rgba.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/track_ball.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/trackball.cpp
${KICAD_ROOT}/common/gal/3d/camera.cpp
# display lists / plates / stencil subtraction
${KICAD_ROOT}/3d-viewer/3d_rendering/opengl/layer_triangles.cpp
# immediate-mode helpers (arrows, segments, bbox, half-cylinder)
${KICAD_ROOT}/3d-viewer/3d_rendering/opengl/opengl_utils.cpp
# 3D model VBO path + navigator gizmo
${KICAD_ROOT}/3d-viewer/3d_rendering/opengl/3d_model.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/opengl/3d_spheres_gizmo.cpp
# shapes2D objects the scenarios construct (ROUND_SEGMENT_2D et al.)
${KICAD_ROOT}/3d-viewer/3d_rendering/raytracing/ray.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/raytracing/accelerators/container_2d.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/raytracing/shapes2D/object_2d.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/raytracing/shapes2D/bbox_2d.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/raytracing/shapes2D/round_segment_2d.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/raytracing/shapes2D/filled_circle_2d.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/raytracing/shapes3D/bbox_3d.cpp
# kimath bits the above reference (RotatePoint in DrawHalfOpenCylinder)
${KICAD_ROOT}/libs/kimath/src/trigo.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/eda_angle.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/seg.cpp
${KICAD_ROOT}/libs/kimath/src/math/util.cpp
# ---- Stage 2: RENDER_3D_OPENGL itself ----
${KICAD_ROOT}/3d-viewer/3d_rendering/opengl/render_3d_opengl.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/opengl/create_scene.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/render_3d_base.cpp
# remaining shapes2D types the generators take as inputs
${KICAD_ROOT}/3d-viewer/3d_rendering/raytracing/shapes2D/ring_2d.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/raytracing/shapes2D/triangle_2d.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/raytracing/shapes2D/4pt_polygon_2d.cpp
${KICAD_ROOT}/3d-viewer/3d_rendering/raytracing/shapes2D/polygon_2d.cpp
# kimath polygon set + triangulation (createBoard/generateLayerList)
${KICAD_ROOT}/libs/kimath/src/geometry/shape_poly_set.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/shape_line_chain.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/shape.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/shape_arc.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/vertex_set.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/geometry_utils.cpp
${KICAD_ROOT}/libs/kimath/src/convert_basic_shapes_to_polygon.cpp
${KICAD_ROOT}/libs/kimath/src/md5_hash.cpp
${KICAD_ROOT}/libs/kimath/src/bezier_curves.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/circle.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/arc_chord_params.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/corner_operations.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/shape_collisions.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/shape_compound.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/shape_rect.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/shape_nearest_points.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/half_line.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/shape_utils.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/roundrect.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/line.cpp
${KICAD_ROOT}/libs/kimath/src/geometry/shape_segment.cpp
${KICAD_ROOT}/libs/kimath/src/math/vector2.cpp
# small common/core TUs referenced by the render TUs
${KICAD_ROOT}/common/layer_id.cpp
${KICAD_ROOT}/common/gal/color4d.cpp
${KICAD_ROOT}/common/kicad_gl/gl_context_mgr.cpp
${KICAD_ROOT}/common/lset.cpp
${KICAD_ROOT}/libs/core/utf8.cpp
# vendored clipper2 (SHAPE_POLY_SET booleans)
${KICAD_ROOT}/thirdparty/clipper2/Clipper2Lib/src/clipper.engine.cpp
${KICAD_ROOT}/thirdparty/clipper2/Clipper2Lib/src/clipper.offset.cpp
${KICAD_ROOT}/thirdparty/clipper2/Clipper2Lib/src/clipper.rectclip.cpp
)
set(SCENARIO_SOURCES
${CMAKE_SOURCE_DIR}/../scenarios/scene3d_test_scenarios.cpp
${CMAKE_SOURCE_DIR}/../scenarios/scene3d_test_ctx.cpp
${CMAKE_SOURCE_DIR}/../scenarios/test_board_data.cpp
${CMAKE_SOURCE_DIR}/../scenarios/scenario_tier1_utils.cpp
${CMAKE_SOURCE_DIR}/../scenarios/scenario_tier1_tdl.cpp
${CMAKE_SOURCE_DIR}/../scenarios/scenario_tier1_model.cpp
${CMAKE_SOURCE_DIR}/../scenarios/scenario_tier2_generators.cpp
${CMAKE_SOURCE_DIR}/../scenarios/scenario_tier3_composite.cpp
)
add_executable(scene3d_native_test
scene3d_native_test.cpp
fbo_capture.cpp
kicad_stubs_3d.cpp
board_adapter_test_impl.cpp
settings_3d_stub.cpp
render3d_test_accessor.cpp
${SCENARIO_SOURCES}
${KICAD_3D_SOURCES}
${GLAD_SOURCES}
)
target_include_directories(scene3d_native_test PRIVATE
${CMAKE_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/../scenarios
# KiCad includes
${KICAD_ROOT}/include # kicad_gl/, gal/3d/camera.h, plugins/3dapi/
${KICAD_ROOT}/3d-viewer # "3d_rendering/...", common_ogl/, 3d_math.h
${KICAD_ROOT}/3d-viewer/3d_rendering # trackball.cpp does #include <trackball.h>
${KICAD_ROOT}/3d-viewer/3d_viewer # create_scene.cpp: <eda_3d_viewer_frame.h>
${KICAD_ROOT}/pcbnew # pad.h et al. (Stage 2 headers)
${KICAD_ROOT}/common
# KiCad libs
${KICAD_ROOT}/libs/kimath/include
${KICAD_ROOT}/libs/core/include
# KiCad thirdparty
${KICAD_ROOT}/thirdparty/glad/include
${KICAD_ROOT}/thirdparty/clipper2/Clipper2Lib/include
${KICAD_ROOT}/thirdparty/dynamic_bitset
${KICAD_ROOT}/thirdparty/rtree # geometry/rtree.h (shape_poly_set.cpp)
${KICAD_ROOT}/thirdparty/magic_enum/magic_enum # magic_enum.hpp (layer_id.cpp)
${KICAD_ROOT}/thirdparty/expected/include # tl/expected.hpp (library_table.h)
${KICAD_ROOT}/thirdparty
# Homebrew (glm)
/opt/homebrew/include
)
target_link_libraries(scene3d_native_test
${wxWidgets_LIBRARIES}
OpenGL::GL
)
message(STATUS "KiCad root: ${KICAD_ROOT}")
message(STATUS "Building scene3d_native_test with actual KiCad 3D-viewer sources")

View file

@ -0,0 +1,521 @@
/**
* Test-harness definitions of BOARD_ADAPTER's out-of-line members.
*
* The real board_adapter.cpp / create_layer_items.cpp drag the whole pcbnew
* board model + settings machinery in none of which exists in this harness.
* Instead WE define the declared members: an out-of-line definition of a
* declared member function has full private access, so this TU is both the
* stub layer and the synthetic-board-data injection seam (InitSettings).
*
* Bodies marked "verbatim" are copied from board_adapter.cpp and must behave
* identically; bodies marked "test" are simplified board-less variants (any
* behavioral drift shows up as a baseline change and is reviewed there).
*/
#include "kicad_stubs_3d.h"
#include "3d_canvas/board_adapter.h"
#include "3d_rendering/raytracing/shapes2D/filled_circle_2d.h"
#include "3d_rendering/raytracing/shapes2D/round_segment_2d.h"
#include "3d_viewer/eda_3d_viewer_settings.h"
#include <board_design_settings.h>
#include <convert_basic_shapes_to_polygon.h>
#include <geometry/geometry_utils.h> // GetArcToSegmentCount
// Same values as board_adapter.cpp:51-57 (defined there as TU-local macros).
#define DEFAULT_BOARD_THICKNESS pcbIUScale.mmToIU( 1.6 )
#define DEFAULT_COPPER_THICKNESS pcbIUScale.mmToIU( 0.035 )
#define DEFAULT_TECH_LAYER_THICKNESS pcbIUScale.mmToIU( 0.025 )
#define SOLDERPASTE_LAYER_THICKNESS pcbIUScale.mmToIU( 0.04 )
#include "../scenarios/test_board_data.h"
// Statics (board_adapter.cpp:60-89, verbatim).
CUSTOM_COLORS_LIST BOARD_ADAPTER::g_SilkColors;
CUSTOM_COLORS_LIST BOARD_ADAPTER::g_MaskColors;
CUSTOM_COLORS_LIST BOARD_ADAPTER::g_PasteColors;
CUSTOM_COLORS_LIST BOARD_ADAPTER::g_FinishColors;
CUSTOM_COLORS_LIST BOARD_ADAPTER::g_BoardColors;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultBackgroundTop;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultBackgroundBot;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultSilkscreen;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultSolderMask;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultSolderPaste;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultSurfaceFinish;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultBoardBody;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultComments;
KIGFX::COLOR4D BOARD_ADAPTER::g_DefaultECOs;
const wxChar* BOARD_ADAPTER::m_logTrace = wxT( "KI_TRACE_EDA_CINFO3D_VISU" );
// The raytracer bevel global lives in board_adapter.cpp too.
float g_BevelThickness3DU = 0.0f;
// test: same default block as the real ctor (board_adapter.cpp:92-157) minus
// ReloadColorSettings() (our version below is settings-free) and the custom
// stackup color tables (not needed — GetLayerColors below is default-based).
BOARD_ADAPTER::BOARD_ADAPTER() :
m_Cfg( nullptr ),
m_IsBoardView( true ),
m_MousewheelPanning( true ),
m_IsPreviewer( false ),
m_board( nullptr ),
m_3dModelManager( nullptr ),
m_layerZcoordTop(),
m_layerZcoordBottom()
{
m_boardPos = VECTOR2I();
m_boardSize = VECTOR2I();
m_boardCenter = SFVEC3F( 0.0f );
m_boardBoundingBox.Reset();
m_TH_IDs.Clear();
m_TH_ODs.Clear();
m_viaAnnuli.Clear();
m_copperLayersCount = 2;
m_biuTo3Dunits = 1.0;
m_boardBodyThickness3DU = DEFAULT_BOARD_THICKNESS * m_biuTo3Dunits;
m_frontCopperThickness3DU = DEFAULT_COPPER_THICKNESS * m_biuTo3Dunits;
m_backCopperThickness3DU = DEFAULT_COPPER_THICKNESS * m_biuTo3Dunits;
m_nonCopperLayerThickness3DU = DEFAULT_TECH_LAYER_THICKNESS * m_biuTo3Dunits;
m_frontMaskThickness3DU = DEFAULT_TECH_LAYER_THICKNESS * m_biuTo3Dunits;
m_backMaskThickness3DU = DEFAULT_TECH_LAYER_THICKNESS * m_biuTo3Dunits;
m_solderPasteLayerThickness3DU = SOLDERPASTE_LAYER_THICKNESS * m_biuTo3Dunits;
m_trackCount = 0;
m_viaCount = 0;
m_averageViaHoleDiameter = 0.0f;
m_holeCount = 0;
m_averageHoleDiameter = 0.0f;
m_averageTrackWidth = 0.0f;
m_BgColorBot = SFVEC4F( 0.4, 0.4, 0.5, 1.0 );
m_BgColorTop = SFVEC4F( 0.8, 0.8, 0.9, 1.0 );
m_BoardBodyColor = SFVEC4F( 0.4, 0.4, 0.5, 0.9 );
m_SolderMaskColorTop = SFVEC4F( 0.1, 0.2, 0.1, 0.83 );
m_SolderMaskColorBot = SFVEC4F( 0.1, 0.2, 0.1, 0.83 );
m_SolderPasteColor = SFVEC4F( 0.4, 0.4, 0.4, 1.0 );
m_SilkScreenColorTop = SFVEC4F( 0.9, 0.9, 0.9, 1.0 );
m_SilkScreenColorBot = SFVEC4F( 0.9, 0.9, 0.9, 1.0 );
m_CopperColor = SFVEC4F( 0.75, 0.61, 0.23, 1.0 );
m_UserDrawingsColor = SFVEC4F( 0.85, 0.85, 0.85, 1.0 );
m_UserCommentsColor = SFVEC4F( 0.85, 0.85, 0.85, 1.0 );
m_ECO1Color = SFVEC4F( 0.70, 0.10, 0.10, 1.0 );
m_ECO2Color = SFVEC4F( 0.70, 0.10, 0.10, 1.0 );
for( int ii = 0; ii < 45; ++ii )
m_UserDefinedLayerColor[ii] = SFVEC4F( 0.70, 0.10, 0.10, 1.0 );
m_platedPadsFront = nullptr;
m_platedPadsBack = nullptr;
m_offboardPadsFront = nullptr;
m_offboardPadsBack = nullptr;
m_frontPlatedCopperPolys = nullptr;
m_backPlatedCopperPolys = nullptr;
ReloadColorSettings();
g_DefaultBackgroundTop = COLOR4D( 0.80, 0.80, 0.90, 1.0 );
g_DefaultBackgroundBot = COLOR4D( 0.40, 0.40, 0.50, 1.0 );
g_DefaultSilkscreen = COLOR4D( 0.94, 0.94, 0.94, 1.0 );
g_DefaultSolderMask = COLOR4D( 0.08, 0.20, 0.14, 0.83 );
g_DefaultSolderPaste = COLOR4D( 0.50, 0.50, 0.50, 1.0 );
g_DefaultSurfaceFinish = COLOR4D( 0.75, 0.61, 0.23, 1.0 );
g_DefaultBoardBody = COLOR4D( 0.4, 0.4, 0.5, 0.9 );
g_DefaultComments = COLOR4D( 0.85, 0.85, 0.85, 1.0 );
g_DefaultECOs = COLOR4D( 0.70, 0.10, 0.10, 1.0 );
}
BOARD_ADAPTER::~BOARD_ADAPTER()
{
destroyLayers();
}
// test: frees exactly what our InitSettings allocates.
void BOARD_ADAPTER::destroyLayers()
{
for( auto& [layer, container] : m_layerMap )
delete container;
m_layerMap.clear();
for( auto& [layer, container] : m_layerHoleMap )
delete container;
m_layerHoleMap.clear();
for( auto& [layer, poly] : m_layers_poly )
delete poly;
m_layers_poly.clear();
for( auto& [layer, poly] : m_layerHoleOdPolys )
delete poly;
m_layerHoleOdPolys.clear();
for( auto& [layer, poly] : m_layerHoleIdPolys )
delete poly;
m_layerHoleIdPolys.clear();
delete m_platedPadsFront;
delete m_platedPadsBack;
delete m_offboardPadsFront;
delete m_offboardPadsBack;
m_platedPadsFront = m_platedPadsBack = nullptr;
m_offboardPadsFront = m_offboardPadsBack = nullptr;
delete m_frontPlatedCopperPolys;
delete m_backPlatedCopperPolys;
m_frontPlatedCopperPolys = nullptr;
m_backPlatedCopperPolys = nullptr;
m_TH_ODs.Clear();
m_TH_IDs.Clear();
m_viaAnnuli.Clear();
m_viaTH_ODs.Clear();
m_board_poly.RemoveAllContours();
m_TH_ODPolys.RemoveAllContours();
m_NPTH_ODPolys.RemoveAllContours();
m_viaTH_ODPolys.RemoveAllContours();
m_viaAnnuliPolys.RemoveAllContours();
}
// test: settings-free — the board-editor color table gets a neutral default
// (the real one loads COLOR_SETTINGS; scenarios don't use per-PCB-layer colors).
void BOARD_ADAPTER::ReloadColorSettings() noexcept
{
for( int layer = F_Cu; layer < PCB_LAYER_ID_COUNT; ++layer )
m_BoardEditorColors[layer] = COLOR4D( 0.75, 0.61, 0.23, 1.0 );
}
// verbatim (board_adapter.cpp:243-288) minus the m_board branches (no board).
bool BOARD_ADAPTER::Is3dLayerEnabled( PCB_LAYER_ID aLayer,
const std::bitset<LAYER_3D_END>& aVisibilityFlags ) const
{
wxASSERT( aLayer < PCB_LAYER_ID_COUNT );
switch( aLayer )
{
case B_Cu: return aVisibilityFlags.test( LAYER_3D_COPPER_BOTTOM );
case F_Cu: return aVisibilityFlags.test( LAYER_3D_COPPER_TOP );
case B_Adhes: return aVisibilityFlags.test( LAYER_3D_ADHESIVE );
case F_Adhes: return aVisibilityFlags.test( LAYER_3D_ADHESIVE );
case B_Paste: return aVisibilityFlags.test( LAYER_3D_SOLDERPASTE );
case F_Paste: return aVisibilityFlags.test( LAYER_3D_SOLDERPASTE );
case B_SilkS: return aVisibilityFlags.test( LAYER_3D_SILKSCREEN_BOTTOM );
case F_SilkS: return aVisibilityFlags.test( LAYER_3D_SILKSCREEN_TOP );
case B_Mask: return aVisibilityFlags.test( LAYER_3D_SOLDERMASK_BOTTOM );
case F_Mask: return aVisibilityFlags.test( LAYER_3D_SOLDERMASK_TOP );
case Dwgs_User: return aVisibilityFlags.test( LAYER_3D_USER_DRAWINGS );
case Cmts_User: return aVisibilityFlags.test( LAYER_3D_USER_COMMENTS );
case Eco1_User: return aVisibilityFlags.test( LAYER_3D_USER_ECO1 );
case Eco2_User: return aVisibilityFlags.test( LAYER_3D_USER_ECO2 );
default:
return false; // test: no board -> unmapped layers hidden
}
}
// test: previews/boards not modeled — footprints always "shown".
bool BOARD_ADAPTER::IsFootprintShown( const FOOTPRINT* aFootprint ) const
{
return aFootprint != nullptr;
}
// verbatim no-board branch (board_adapter.cpp:315-320).
int BOARD_ADAPTER::GetHolePlatingThickness() const noexcept
{
return DEFAULT_COPPER_THICKNESS;
}
// verbatim (board_adapter.cpp:322-328).
unsigned int BOARD_ADAPTER::GetCircleSegmentCount( float aDiameter3DU ) const
{
wxASSERT( aDiameter3DU > 0.0f );
return GetCircleSegmentCount( (int) ( aDiameter3DU / m_biuTo3Dunits ) );
}
// test: like board_adapter.cpp:330-336 with the BOARD_DESIGN_SETTINGS default
// max error (ARC_HIGH_DEF) instead of a live board's setting.
unsigned int BOARD_ADAPTER::GetCircleSegmentCount( int aDiameterBIU ) const
{
wxASSERT( aDiameterBIU > 0 );
return GetArcToSegmentCount( aDiameterBIU / 2, ARC_HIGH_DEF, FULL_CIRCLE );
}
// verbatim non-previewer path (board_adapter.cpp:808-905) minus FOLLOW_PCB
// (needs a board).
std::bitset<LAYER_3D_END> BOARD_ADAPTER::GetVisibleLayers() const
{
std::bitset<LAYER_3D_END> ret;
ret.set( LAYER_3D_BOARD, m_Cfg->m_Render.show_board_body );
ret.set( LAYER_3D_PLATED_BARRELS, m_Cfg->m_Render.show_plated_barrels );
ret.set( LAYER_3D_COPPER_TOP, m_Cfg->m_Render.show_copper_top );
ret.set( LAYER_3D_COPPER_BOTTOM, m_Cfg->m_Render.show_copper_bottom );
ret.set( LAYER_3D_SILKSCREEN_TOP, m_Cfg->m_Render.show_silkscreen_top );
ret.set( LAYER_3D_SILKSCREEN_BOTTOM, m_Cfg->m_Render.show_silkscreen_bottom );
ret.set( LAYER_3D_SOLDERMASK_TOP, m_Cfg->m_Render.show_soldermask_top );
ret.set( LAYER_3D_SOLDERMASK_BOTTOM, m_Cfg->m_Render.show_soldermask_bottom );
ret.set( LAYER_3D_SOLDERPASTE, m_Cfg->m_Render.show_solderpaste );
ret.set( LAYER_3D_ADHESIVE, m_Cfg->m_Render.show_adhesive );
ret.set( LAYER_3D_USER_COMMENTS, m_Cfg->m_Render.show_comments );
ret.set( LAYER_3D_USER_DRAWINGS, m_Cfg->m_Render.show_drawings );
ret.set( LAYER_3D_USER_ECO1, m_Cfg->m_Render.show_eco1 );
ret.set( LAYER_3D_USER_ECO2, m_Cfg->m_Render.show_eco2 );
for( int layer = LAYER_3D_USER_1; layer <= LAYER_3D_USER_45; ++layer )
ret.set( layer, m_Cfg->m_Render.show_user[layer - LAYER_3D_USER_1] );
ret.set( LAYER_FP_REFERENCES, m_Cfg->m_Render.show_fp_references );
ret.set( LAYER_FP_VALUES, m_Cfg->m_Render.show_fp_values );
ret.set( LAYER_FP_TEXT, m_Cfg->m_Render.show_fp_text );
ret.set( LAYER_3D_TH_MODELS, m_Cfg->m_Render.show_footprints_normal );
ret.set( LAYER_3D_SMD_MODELS, m_Cfg->m_Render.show_footprints_insert );
ret.set( LAYER_3D_VIRTUAL_MODELS, m_Cfg->m_Render.show_footprints_virtual );
ret.set( LAYER_3D_MODELS_NOT_IN_POS, m_Cfg->m_Render.show_footprints_not_in_posfile );
ret.set( LAYER_3D_MODELS_MARKED_DNP, m_Cfg->m_Render.show_footprints_dnp );
ret.set( LAYER_3D_BOUNDING_BOXES, m_Cfg->m_Render.show_model_bbox );
ret.set( LAYER_3D_OFF_BOARD_SILK, m_Cfg->m_Render.show_off_board_silk );
ret.set( LAYER_3D_NAVIGATOR, m_Cfg->m_Render.show_navigator );
return ret;
}
// test: no board -> board-editor copper colors never apply.
bool BOARD_ADAPTER::GetUseBoardEditorCopperLayerColors() const
{
return false;
}
// verbatim (board_adapter.cpp:1039-1053).
float BOARD_ADAPTER::GetFootprintZPos( bool aIsFlipped ) const
{
if( aIsFlipped )
{
if( auto it = m_layerZcoordBottom.find( B_Paste ); it != m_layerZcoordBottom.end() )
return it->second;
}
else
{
if( auto it = m_layerZcoordTop.find( F_Paste ); it != m_layerZcoordTop.end() )
return it->second;
}
return 0.0;
}
// verbatim (board_adapter.cpp:1056-1070); user-layer remap dropped (unused here).
SFVEC4F BOARD_ADAPTER::GetLayerColor( int aLayerId ) const
{
wxASSERT( aLayerId < PCB_LAYER_ID_COUNT );
return GetColor( m_BoardEditorColors.at( aLayerId ) );
}
SFVEC4F BOARD_ADAPTER::GetColor( const COLOR4D& aColor ) const
{
return SFVEC4F( aColor.r, aColor.g, aColor.b, aColor.a );
}
// test: default-color scheme only (no COLOR_SETTINGS machinery).
std::map<int, COLOR4D> BOARD_ADAPTER::GetDefaultColors() const
{
std::map<int, COLOR4D> colors;
colors[LAYER_3D_BACKGROUND_TOP] = g_DefaultBackgroundTop;
colors[LAYER_3D_BACKGROUND_BOTTOM] = g_DefaultBackgroundBot;
colors[LAYER_3D_BOARD] = g_DefaultBoardBody;
colors[LAYER_3D_COPPER_TOP] = g_DefaultSurfaceFinish;
colors[LAYER_3D_COPPER_BOTTOM] = g_DefaultSurfaceFinish;
colors[LAYER_3D_SILKSCREEN_TOP] = g_DefaultSilkscreen;
colors[LAYER_3D_SILKSCREEN_BOTTOM] = g_DefaultSilkscreen;
colors[LAYER_3D_SOLDERMASK_TOP] = g_DefaultSolderMask;
colors[LAYER_3D_SOLDERMASK_BOTTOM] = g_DefaultSolderMask;
colors[LAYER_3D_SOLDERPASTE] = g_DefaultSolderPaste;
colors[LAYER_3D_USER_DRAWINGS] = g_DefaultComments;
colors[LAYER_3D_USER_COMMENTS] = g_DefaultComments;
colors[LAYER_3D_USER_ECO1] = g_DefaultECOs;
colors[LAYER_3D_USER_ECO2] = g_DefaultECOs;
return colors;
}
std::map<int, COLOR4D> BOARD_ADAPTER::GetLayerColors() const
{
return GetDefaultColors();
}
// ---------------------------------------------------------------------------
// THE SEAM: synthetic test-board data instead of a real BOARD.
//
// A 40 x 30 mm two-layer board. Layer Z stacking follows the real
// InitSettings maths (board body centered on Z=0, copper plated on top/bottom,
// tech layers above copper). All values in BIU (nm) scaled by m_biuTo3Dunits.
// ---------------------------------------------------------------------------
void BOARD_ADAPTER::InitSettings( REPORTER* aStatusReporter, REPORTER* aWarningReporter )
{
(void) aStatusReporter;
(void) aWarningReporter;
destroyLayers();
const int boardW = pcbIUScale.mmToIU( 40 );
const int boardH = pcbIUScale.mmToIU( 30 );
m_boardSize = VECTOR2I( boardW, boardH );
m_boardPos = VECTOR2I( 0, 0 );
m_copperLayersCount = 2;
// Same scale maths as the real InitSettings (board_adapter.cpp:382-387):
// no BOARD -> the "footprint holder" zoom hack applies.
m_biuTo3Dunits = RANGE_SCALE_3D / std::max( m_boardSize.x, m_boardSize.y );
m_biuTo3Dunits *= 1.6f;
m_boardBodyThickness3DU = DEFAULT_BOARD_THICKNESS * m_biuTo3Dunits;
m_frontCopperThickness3DU = DEFAULT_COPPER_THICKNESS * m_biuTo3Dunits;
m_backCopperThickness3DU = DEFAULT_COPPER_THICKNESS * m_biuTo3Dunits;
m_nonCopperLayerThickness3DU = DEFAULT_TECH_LAYER_THICKNESS * m_biuTo3Dunits;
m_frontMaskThickness3DU = DEFAULT_TECH_LAYER_THICKNESS * m_biuTo3Dunits;
m_backMaskThickness3DU = DEFAULT_TECH_LAYER_THICKNESS * m_biuTo3Dunits;
m_solderPasteLayerThickness3DU = SOLDERPASTE_LAYER_THICKNESS * m_biuTo3Dunits;
// Layer Z coordinates (board body spans -body/2 .. +body/2).
const float bodyTop = m_boardBodyThickness3DU / 2.0f;
const float bodyBot = -m_boardBodyThickness3DU / 2.0f;
m_layerZcoordBottom[F_Cu] = bodyTop;
m_layerZcoordTop[F_Cu] = bodyTop + m_frontCopperThickness3DU;
m_layerZcoordBottom[B_Cu] = bodyBot;
m_layerZcoordTop[B_Cu] = bodyBot - m_backCopperThickness3DU;
m_layerZcoordBottom[F_Mask] = m_layerZcoordTop[F_Cu];
m_layerZcoordTop[F_Mask] = m_layerZcoordBottom[F_Mask] + m_frontMaskThickness3DU;
m_layerZcoordBottom[B_Mask] = m_layerZcoordTop[B_Cu];
m_layerZcoordTop[B_Mask] = m_layerZcoordBottom[B_Mask] - m_backMaskThickness3DU;
m_layerZcoordBottom[F_SilkS] = m_layerZcoordTop[F_Mask];
m_layerZcoordTop[F_SilkS] = m_layerZcoordBottom[F_SilkS] + m_nonCopperLayerThickness3DU;
m_layerZcoordBottom[B_SilkS] = m_layerZcoordTop[B_Mask];
m_layerZcoordTop[B_SilkS] = m_layerZcoordBottom[B_SilkS] - m_nonCopperLayerThickness3DU;
m_layerZcoordBottom[F_Paste] = m_layerZcoordTop[F_Cu];
m_layerZcoordTop[F_Paste] = m_layerZcoordBottom[F_Paste] + m_solderPasteLayerThickness3DU;
m_layerZcoordBottom[B_Paste] = m_layerZcoordTop[B_Cu];
m_layerZcoordTop[B_Paste] = m_layerZcoordBottom[B_Paste] - m_solderPasteLayerThickness3DU;
m_boardCenter = SFVEC3F( 0.0f, 0.0f, 0.0f );
m_boardBoundingBox.Set( SFVEC3F( -boardW / 2 * m_biuTo3Dunits, -boardH / 2 * m_biuTo3Dunits,
bodyBot ),
SFVEC3F( boardW / 2 * m_biuTo3Dunits, boardH / 2 * m_biuTo3Dunits,
bodyTop ) );
// Board outline: plain rectangle.
m_board_poly.RemoveAllContours();
m_board_poly.NewOutline();
m_board_poly.Append( -boardW / 2, -boardH / 2 );
m_board_poly.Append( boardW / 2, -boardH / 2 );
m_board_poly.Append( boardW / 2, boardH / 2 );
m_board_poly.Append( -boardW / 2, boardH / 2 );
m_board_poly.Outline( 0 ).SetClosed( true );
// Copper: a few tracks + round pads per side; silkscreen: a frame; the
// BVH containers own the objects (they delete them).
auto addTrack = [&]( BVH_CONTAINER_2D* aDst, double aX1mm, double aY1mm, double aX2mm,
double aY2mm, double aWidthMm )
{
aDst->Add( new ROUND_SEGMENT_2D(
SFVEC2F( pcbIUScale.mmToIU( aX1mm ) * m_biuTo3Dunits,
pcbIUScale.mmToIU( aY1mm ) * m_biuTo3Dunits ),
SFVEC2F( pcbIUScale.mmToIU( aX2mm ) * m_biuTo3Dunits,
pcbIUScale.mmToIU( aY2mm ) * m_biuTo3Dunits ),
pcbIUScale.mmToIU( aWidthMm ) * m_biuTo3Dunits, DummyBoardItem() ) );
};
auto addCircle = [&]( BVH_CONTAINER_2D* aDst, double aXmm, double aYmm, double aRmm )
{
aDst->Add( new FILLED_CIRCLE_2D( SFVEC2F( pcbIUScale.mmToIU( aXmm ) * m_biuTo3Dunits,
pcbIUScale.mmToIU( aYmm ) * m_biuTo3Dunits ),
pcbIUScale.mmToIU( aRmm ) * m_biuTo3Dunits,
DummyBoardItem() ) );
};
BVH_CONTAINER_2D* frontCu = new BVH_CONTAINER_2D;
addTrack( frontCu, -15, -10, 15, -10, 1.0 );
addTrack( frontCu, -15, -10, -15, 10, 1.0 );
addTrack( frontCu, -15, 10, 0, 10, 0.6 );
addTrack( frontCu, 0, 10, 8, 2, 0.6 );
addCircle( frontCu, -15, -10, 1.5 );
addCircle( frontCu, 15, -10, 1.5 );
addCircle( frontCu, 8, 2, 1.2 );
frontCu->BuildBVH();
m_layerMap[F_Cu] = frontCu;
BVH_CONTAINER_2D* backCu = new BVH_CONTAINER_2D;
addTrack( backCu, 15, -10, 15, 10, 1.2 );
addTrack( backCu, 15, 10, -8, 10, 1.2 );
addCircle( backCu, -15, -10, 1.5 );
addCircle( backCu, 15, -10, 1.5 );
backCu->BuildBVH();
m_layerMap[B_Cu] = backCu;
BVH_CONTAINER_2D* frontSilk = new BVH_CONTAINER_2D;
addTrack( frontSilk, -17, -13, 17, -13, 0.3 );
addTrack( frontSilk, 17, -13, 17, 13, 0.3 );
addTrack( frontSilk, 17, 13, -17, 13, 0.3 );
addTrack( frontSilk, -17, 13, -17, -13, 0.3 );
addCircle( frontSilk, -12, 6, 0.8 );
frontSilk->BuildBVH();
m_layerMap[F_SilkS] = frontSilk;
// Through holes: the two big pads are plated through.
auto addHolePoly = [&]( SHAPE_POLY_SET& aPolys, double aXmm, double aYmm, double aRmm )
{
TransformCircleToPolygon( aPolys,
VECTOR2I( pcbIUScale.mmToIU( aXmm ), pcbIUScale.mmToIU( aYmm ) ),
pcbIUScale.mmToIU( aRmm ), ARC_HIGH_DEF, ERROR_INSIDE );
};
addCircle( &m_TH_ODs, -15, -10, 0.8 );
addCircle( &m_TH_ODs, 15, -10, 0.8 );
m_TH_ODs.BuildBVH();
addCircle( &m_TH_IDs, -15, -10, 0.65 );
addCircle( &m_TH_IDs, 15, -10, 0.65 );
m_TH_IDs.BuildBVH();
addHolePoly( m_TH_ODPolys, -15, -10, 0.8 );
addHolePoly( m_TH_ODPolys, 15, -10, 0.8 );
m_TH_ODPolys.Simplify();
}

View file

@ -0,0 +1,26 @@
// Minimal config.h for native GAL test
// Based on KiCad's generated config
#ifndef KICAD_CONFIG_H
#define KICAD_CONFIG_H
// Version info
#define KICAD_MAJOR_VERSION 8
#define KICAD_MINOR_VERSION 0
#define KICAD_PATCH_VERSION 0
// Enable OpenGL
#define KICAD_USE_OCC 0
#define KICAD_USE_EGL 0
// Platform detection
#ifdef __APPLE__
#define KICAD_MACOS 1
#endif
// Math
#define KICAD_USE_STDROUND 1
// Ensure types are properly sized
#include <cstdint>
#endif // KICAD_CONFIG_H

View file

@ -0,0 +1,146 @@
#include "fbo_capture.h"
#include <cstdio>
// Core-vs-EXT selection: Apple's 2.1 context exports the ARB entry points on
// all modern renderers, but keep the EXT fallback the compositor path proves
// works (see plan §7 risk 2). Enum values are shared between core and EXT.
static PFNGLGENFRAMEBUFFERSPROC s_glGenFramebuffers = nullptr;
static PFNGLBINDFRAMEBUFFERPROC s_glBindFramebuffer = nullptr;
static PFNGLFRAMEBUFFERTEXTURE2DPROC s_glFramebufferTexture2D = nullptr;
static PFNGLGENRENDERBUFFERSPROC s_glGenRenderbuffers = nullptr;
static PFNGLBINDRENDERBUFFERPROC s_glBindRenderbuffer = nullptr;
static PFNGLRENDERBUFFERSTORAGEPROC s_glRenderbufferStorage = nullptr;
static PFNGLFRAMEBUFFERRENDERBUFFERPROC s_glFramebufferRenderbuffer = nullptr;
static PFNGLCHECKFRAMEBUFFERSTATUSPROC s_glCheckFramebufferStatus = nullptr;
static PFNGLDELETEFRAMEBUFFERSPROC s_glDeleteFramebuffers = nullptr;
static PFNGLDELETERENDERBUFFERSPROC s_glDeleteRenderbuffers = nullptr;
static bool resolveFboEntryPoints()
{
if( s_glGenFramebuffers )
return true;
if( glad_glGenFramebuffers )
{
s_glGenFramebuffers = glad_glGenFramebuffers;
s_glBindFramebuffer = glad_glBindFramebuffer;
s_glFramebufferTexture2D = glad_glFramebufferTexture2D;
s_glGenRenderbuffers = glad_glGenRenderbuffers;
s_glBindRenderbuffer = glad_glBindRenderbuffer;
s_glRenderbufferStorage = glad_glRenderbufferStorage;
s_glFramebufferRenderbuffer = glad_glFramebufferRenderbuffer;
s_glCheckFramebufferStatus = glad_glCheckFramebufferStatus;
s_glDeleteFramebuffers = glad_glDeleteFramebuffers;
s_glDeleteRenderbuffers = glad_glDeleteRenderbuffers;
return true;
}
if( glad_glGenFramebuffersEXT )
{
std::fprintf( stderr, "[fbo] core FBO entry points missing; using EXT fallback\n" );
s_glGenFramebuffers = glad_glGenFramebuffersEXT;
s_glBindFramebuffer = glad_glBindFramebufferEXT;
s_glFramebufferTexture2D = glad_glFramebufferTexture2DEXT;
s_glGenRenderbuffers = glad_glGenRenderbuffersEXT;
s_glBindRenderbuffer = glad_glBindRenderbufferEXT;
s_glRenderbufferStorage = glad_glRenderbufferStorageEXT;
s_glFramebufferRenderbuffer = glad_glFramebufferRenderbufferEXT;
s_glCheckFramebufferStatus = glad_glCheckFramebufferStatusEXT;
s_glDeleteFramebuffers = glad_glDeleteFramebuffersEXT;
s_glDeleteRenderbuffers = glad_glDeleteRenderbuffersEXT;
return true;
}
std::fprintf( stderr, "[fbo] no framebuffer object support in this context\n" );
return false;
}
bool FBO_CAPTURE::Create( int aWidth, int aHeight )
{
if( !resolveFboEntryPoints() )
return false;
m_width = aWidth;
m_height = aHeight;
// Same sequence as EDA_3D_CANVAS::RenderToFrameBuffer (eda_3d_canvas.cpp:736-772).
s_glGenFramebuffers( 1, &m_fbo );
s_glBindFramebuffer( GL_FRAMEBUFFER, m_fbo );
glGenTextures( 1, &m_colorTexture );
glBindTexture( GL_TEXTURE_2D, m_colorTexture );
glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA8, aWidth, aHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE,
nullptr );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
s_glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
m_colorTexture, 0 );
glBindTexture( GL_TEXTURE_2D, 0 );
s_glGenRenderbuffers( 1, &m_depthStencil );
s_glBindRenderbuffer( GL_RENDERBUFFER, m_depthStencil );
s_glRenderbufferStorage( GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, aWidth, aHeight );
s_glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER,
m_depthStencil );
const GLenum status = s_glCheckFramebufferStatus( GL_FRAMEBUFFER );
if( status != GL_FRAMEBUFFER_COMPLETE )
{
std::fprintf( stderr, "[fbo] framebuffer incomplete: 0x%04X\n", status );
Destroy();
return false;
}
return true;
}
void FBO_CAPTURE::Bind()
{
s_glBindFramebuffer( GL_FRAMEBUFFER, m_fbo );
glViewport( 0, 0, m_width, m_height );
}
bool FBO_CAPTURE::ReadPixels( std::vector<uint8_t>& aOut )
{
if( !m_fbo )
return false;
s_glBindFramebuffer( GL_FRAMEBUFFER, m_fbo );
glFinish();
aOut.resize( static_cast<size_t>( m_width ) * m_height * 4 );
glPixelStorei( GL_PACK_ALIGNMENT, 1 );
glReadPixels( 0, 0, m_width, m_height, GL_RGBA, GL_UNSIGNED_BYTE, aOut.data() );
const GLenum err = glGetError();
if( err != GL_NO_ERROR )
{
std::fprintf( stderr, "[fbo] glReadPixels error: 0x%04X\n", err );
return false;
}
return true;
}
void FBO_CAPTURE::Destroy()
{
if( m_fbo )
s_glDeleteFramebuffers( 1, &m_fbo );
if( m_colorTexture )
glDeleteTextures( 1, &m_colorTexture );
if( m_depthStencil )
s_glDeleteRenderbuffers( 1, &m_depthStencil );
m_fbo = m_colorTexture = m_depthStencil = 0;
}

View file

@ -0,0 +1,46 @@
/**
* Offscreen FBO for deterministic fixed-size capture, mirroring
* EDA_3D_CANVAS::RenderToFrameBuffer (eda_3d_canvas.cpp:736-772):
* GL_RGBA8 color texture + GL_DEPTH24_STENCIL8 renderbuffer on
* GL_DEPTH_STENCIL_ATTACHMENT (the stencil is required by
* OPENGL_RENDER_LIST::DrawCulled hole cutting).
*
* Rendering into an FBO makes the output independent of window size and
* Retina scaling pixels are exactly aWidth x aHeight.
*/
#ifndef FBO_CAPTURE_H
#define FBO_CAPTURE_H
#include <kicad_gl/kiglad.h>
#include <cstdint>
#include <vector>
class FBO_CAPTURE
{
public:
/// Create the FBO; returns false if the framebuffer is incomplete.
/// Falls back to the EXT entry points if the core ARB ones didn't load
/// (Apple GL 2.1 exposes both; glad loads by symbol name).
bool Create( int aWidth, int aHeight );
void Bind();
/// glFinish + glReadPixels(GL_RGBA); aOut is resized to w*h*4.
bool ReadPixels( std::vector<uint8_t>& aOut );
void Destroy();
int Width() const { return m_width; }
int Height() const { return m_height; }
private:
int m_width = 0;
int m_height = 0;
GLuint m_fbo = 0;
GLuint m_colorTexture = 0;
GLuint m_depthStencil = 0;
};
#endif // FBO_CAPTURE_H

View file

@ -0,0 +1,303 @@
/**
* Link stubs for the native 3D-renderer test build.
*
* The harness compiles real KiCad 3D-viewer TUs; the symbols those TUs
* reference from elsewhere in KiCad but only reach through dead branches
* (m_board / model-cache / app machinery is never populated here) are defined
* as safe no-ops. One stub per linker error, each annotated with the
* referencing TU. Seeded from tests/gal-regression/native/kicad_stubs.cpp.
*/
#include "kicad_stubs_3d.h"
#include <advanced_config.h>
#include <pgm_base.h>
#include <singleton.h>
#include <kicad_gl/gl_context_mgr.h>
// PGM_BASE holds unique_ptrs to these — complete types needed to define
// its ctor/dtor here.
#include <background_jobs_monitor.h>
#include <notifications_manager.h>
#include <settings/settings_manager.h>
#include <wx/snglinst.h>
#include <board.h>
#include <pad.h>
#include <pcb_track.h>
#include <libraries/library_manager.h>
#include <project_pcb.h>
#include "3d_cache/3d_cache.h"
//=============================================================================
// PGM_BASE / Pgm() — render_3d_opengl.cpp uses
// Pgm().GetGLContextManager()->RunWithoutCtxLock() during reload().
//=============================================================================
class PGM_BASE_TEST : public PGM_BASE
{
public:
PGM_BASE_TEST() { m_singleton.m_GLContextManager = new GL_CONTEXT_MANAGER(); }
void MacOpenFile( const wxString& ) override {}
};
PGM_BASE& Pgm()
{
static PGM_BASE_TEST* s_pgm = nullptr;
if( !s_pgm )
s_pgm = new PGM_BASE_TEST();
return *s_pgm;
}
// PGM_BASE out-of-line methods (pgm_base.cpp is not compiled) — same stub set
// as the GAL harness.
PGM_BASE::PGM_BASE()
{
}
PGM_BASE::~PGM_BASE()
{
}
wxApp& PGM_BASE::App()
{
static wxApp* app = nullptr;
return *app;
}
COMMON_SETTINGS* PGM_BASE::GetCommonSettings() const
{
return nullptr;
}
const wxString& PGM_BASE::GetExecutablePath() const
{
static wxString s;
return s;
}
ENV_VAR_MAP& PGM_BASE::GetLocalEnvVariables() const
{
static ENV_VAR_MAP map;
return map;
}
bool PGM_BASE::SetLanguage( wxString&, bool )
{
return false;
}
const wxString& PGM_BASE::GetTextEditor( bool )
{
static wxString s;
return s;
}
void PGM_BASE::SetTextEditor( const wxString& )
{
}
wxString PGM_BASE::GetLanguageTag()
{
return wxString();
}
void PGM_BASE::SetLanguagePath()
{
}
void PGM_BASE::ReadPdfBrowserInfos()
{
}
bool PGM_BASE::SetLocalEnvVariable( const wxString&, const wxString& )
{
return false;
}
void PGM_BASE::SetLocalEnvVariables()
{
}
void PGM_BASE::WritePdfBrowserInfos()
{
}
void PGM_BASE::SetLanguageIdentifier( int )
{
}
const wxString PGM_BASE::AskUserForPreferredEditor( const wxString& )
{
return wxString();
}
//=============================================================================
// ADVANCED_CFG singleton (advanced_config.cpp is not compiled)
//=============================================================================
const ADVANCED_CFG& ADVANCED_CFG::GetCfg()
{
static ADVANCED_CFG instance;
return instance;
}
ADVANCED_CFG::ADVANCED_CFG()
{
// Only fields the compiled TUs actually read; KiCad default DPI.
m_ScreenDPI = 91;
m_3DRT_BevelHeight_um = 30;
m_3DRT_BevelExtentFactor = 1.0 / 16.0;
}
//=============================================================================
// Profiling clock — deterministic zero for the test harness.
//=============================================================================
int64_t GetRunningMicroSecs()
{
return 0;
}
//=============================================================================
// Destructors of app-machinery members PGM_BASE owns (their real TUs would
// drag the whole settings/library world in; nothing here ever populates them).
//=============================================================================
KICAD_SINGLETON::~KICAD_SINGLETON()
{
delete m_GLContextManager;
m_GLContextManager = nullptr;
}
LIBRARY_MANAGER::~LIBRARY_MANAGER() = default;
SETTINGS_MANAGER::~SETTINGS_MANAGER() = default;
//=============================================================================
// STATUSBAR_REPORTER (reporter.cpp drags fontconfig; Redraw() takes REPORTER*
// but the harness always passes nullptr).
//=============================================================================
#include <reporter.h>
REPORTER& STATUSBAR_REPORTER::Report( const wxString&, SEVERITY )
{
return *this;
}
//=============================================================================
// BOARD / BOARD_ITEM / PAD / PCB_VIA — referenced from create_scene.cpp and
// render_3d_opengl.cpp branches that only run with a real BOARD loaded
// (m_board stays nullptr in this harness).
//=============================================================================
int BOARD::GetCopperLayerCount() const
{
return 2;
}
const EMBEDDED_FILES* BOARD::GetEmbeddedFiles() const
{
return nullptr;
}
const wxString BOARD::GetLayerName( PCB_LAYER_ID aLayer ) const
{
return LayerName( aLayer );
}
int BOARD_ITEM::GetMaxError() const
{
return ARC_HIGH_DEF;
}
bool PAD::TransformHoleToPolygon( SHAPE_POLY_SET&, int, int, ERROR_LOC ) const
{
return false;
}
int PCB_VIA::GetDrillValue() const
{
return 0;
}
void PCB_VIA::LayerPair( PCB_LAYER_ID* aTopLayer, PCB_LAYER_ID* aBottomLayer ) const
{
if( aTopLayer )
*aTopLayer = F_Cu;
if( aBottomLayer )
*aBottomLayer = B_Cu;
}
std::optional<int> PCB_VIA::GetSecondaryDrillSize() const
{
return std::nullopt;
}
std::optional<int> PCB_VIA::GetTertiaryDrillSize() const
{
return std::nullopt;
}
FILLING_MODE PCB_VIA::GetFillingMode() const
{
return static_cast<FILLING_MODE>( 0 );
}
CAPPING_MODE PCB_VIA::GetCappingMode() const
{
return static_cast<CAPPING_MODE>( 0 );
}
PLUGGING_MODE PCB_VIA::GetFrontPluggingMode() const
{
return static_cast<PLUGGING_MODE>( 0 );
}
PLUGGING_MODE PCB_VIA::GetBackPluggingMode() const
{
return static_cast<PLUGGING_MODE>( 0 );
}
COVERING_MODE PCB_VIA::GetFrontCoveringMode() const
{
return static_cast<COVERING_MODE>( 0 );
}
COVERING_MODE PCB_VIA::GetBackCoveringMode() const
{
return static_cast<COVERING_MODE>( 0 );
}
//=============================================================================
// Footprint-model loading chain (Load3dModelsIfNeeded is never called).
//=============================================================================
FOOTPRINT_LIBRARY_ADAPTER* PROJECT_PCB::FootprintLibAdapter( PROJECT* )
{
return nullptr;
}
wxString LIBRARY_MANAGER::GetFullURI( const LIBRARY_TABLE_ROW*, bool )
{
return wxString();
}
std::optional<LIBRARY_TABLE_ROW*> LIBRARY_MANAGER_ADAPTER::GetRow( const wxString&,
LIBRARY_TABLE_SCOPE ) const
{
return std::nullopt;
}
S3DMODEL* S3D_CACHE::GetModel( const wxString&, const wxString&,
std::vector<const EMBEDDED_FILES*> )
{
return nullptr;
}

View file

@ -0,0 +1,15 @@
/**
* Include shim for the native 3D-renderer test build.
* kiglad.h must precede wx/glcanvas.h so GL symbols come from the vendored
* glad loader instead of the (deprecated) Apple GL headers.
*/
#ifndef KICAD_STUBS_3D_H
#define KICAD_STUBS_3D_H
#include <kicad_gl/kiglad.h>
#include <wx/wx.h>
#include <wx/glcanvas.h>
#endif // KICAD_STUBS_3D_H

View file

@ -0,0 +1,273 @@
#include "kicad_stubs_3d.h"
#include "render3d_test_accessor.h"
#include "3d_rendering/opengl/render_3d_opengl.h"
// Member-pointer private-access technique, same as gal_test_accessor.cpp
// (https://bloglitb.blogspot.com/2010/07/access-to-private-members-thats-easy.html).
template <typename Tag>
struct result
{
typedef typename Tag::type type;
static type ptr;
};
template <typename Tag>
typename result<Tag>::type result<Tag>::ptr;
template <typename Tag, typename Tag::type p>
struct rob : result<Tag>
{
struct filler
{
filler() { result<Tag>::ptr = p; }
};
static filler filler_obj;
};
template <typename Tag, typename Tag::type p>
typename rob<Tag, p>::filler rob<Tag, p>::filler_obj;
// Tags — the typedef's signature picks the right overload of the member.
struct R3D_initializeOpenGL
{
typedef bool ( RENDER_3D_OPENGL::*type )();
};
template struct rob<R3D_initializeOpenGL, &RENDER_3D_OPENGL::initializeOpenGL>;
struct R3D_generateCylinder
{
typedef void ( RENDER_3D_OPENGL::*type )( const SFVEC2F&, float, float, float, float,
unsigned int, TRIANGLE_DISPLAY_LIST* );
};
template struct rob<R3D_generateCylinder, &RENDER_3D_OPENGL::generateCylinder>;
struct R3D_generateInvCone
{
typedef void ( RENDER_3D_OPENGL::*type )( const SFVEC2F&, float, float, float, float,
unsigned int, TRIANGLE_DISPLAY_LIST*, EDA_ANGLE );
};
template struct rob<R3D_generateInvCone, &RENDER_3D_OPENGL::generateInvCone>;
struct R3D_generateDisk
{
typedef void ( RENDER_3D_OPENGL::*type )( const SFVEC2F&, float, float, unsigned int,
TRIANGLE_DISPLAY_LIST*, bool );
};
template struct rob<R3D_generateDisk, &RENDER_3D_OPENGL::generateDisk>;
struct R3D_generateDimple
{
typedef void ( RENDER_3D_OPENGL::*type )( const SFVEC2F&, float, float, float, unsigned int,
TRIANGLE_DISPLAY_LIST*, bool );
};
template struct rob<R3D_generateDimple, &RENDER_3D_OPENGL::generateDimple>;
struct R3D_generateRing
{
typedef void ( RENDER_3D_OPENGL::*type )( const SFVEC2F&, float, float, unsigned int,
std::vector<SFVEC2F>&, std::vector<SFVEC2F>&,
bool );
};
template struct rob<R3D_generateRing, &RENDER_3D_OPENGL::generateRing>;
struct R3D_addObj_Circle
{
typedef void ( RENDER_3D_OPENGL::*type )( const FILLED_CIRCLE_2D*, TRIANGLE_DISPLAY_LIST*,
float, float );
};
template struct rob<R3D_addObj_Circle, &RENDER_3D_OPENGL::addObjectTriangles>;
struct R3D_addObj_Ring
{
typedef void ( RENDER_3D_OPENGL::*type )( const RING_2D*, TRIANGLE_DISPLAY_LIST*, float,
float );
};
template struct rob<R3D_addObj_Ring, &RENDER_3D_OPENGL::addObjectTriangles>;
struct R3D_addObj_Poly4
{
typedef void ( RENDER_3D_OPENGL::*type )( const POLYGON_4PT_2D*, TRIANGLE_DISPLAY_LIST*,
float, float );
};
template struct rob<R3D_addObj_Poly4, &RENDER_3D_OPENGL::addObjectTriangles>;
struct R3D_addObj_Tri
{
typedef void ( RENDER_3D_OPENGL::*type )( const TRIANGLE_2D*, TRIANGLE_DISPLAY_LIST*, float,
float );
};
template struct rob<R3D_addObj_Tri, &RENDER_3D_OPENGL::addObjectTriangles>;
struct R3D_addObj_Seg
{
typedef void ( RENDER_3D_OPENGL::*type )( const ROUND_SEGMENT_2D*, TRIANGLE_DISPLAY_LIST*,
float, float );
};
template struct rob<R3D_addObj_Seg, &RENDER_3D_OPENGL::addObjectTriangles>;
struct R3D_appendPostMachining
{
typedef bool ( RENDER_3D_OPENGL::*type )( TRIANGLE_DISPLAY_LIST*, const SFVEC2F&,
PAD_DRILL_POST_MACHINING_MODE, int, int, float,
float, bool, float, float, float* );
};
template struct rob<R3D_appendPostMachining, &RENDER_3D_OPENGL::appendPostMachiningGeometry>;
struct R3D_createBoard
{
typedef OPENGL_RENDER_LIST* ( RENDER_3D_OPENGL::*type )( const SHAPE_POLY_SET&,
const BVH_CONTAINER_2D* );
};
template struct rob<R3D_createBoard, &RENDER_3D_OPENGL::createBoard>;
struct R3D_generate3dGrid
{
typedef void ( RENDER_3D_OPENGL::*type )( GRID3D_TYPE );
};
template struct rob<R3D_generate3dGrid, &RENDER_3D_OPENGL::generate3dGrid>;
struct R3D_setupMaterials
{
typedef void ( RENDER_3D_OPENGL::*type )();
};
template struct rob<R3D_setupMaterials, &RENDER_3D_OPENGL::setupMaterials>;
struct R3D_setLayerMaterial
{
typedef void ( RENDER_3D_OPENGL::*type )( PCB_LAYER_ID );
};
template struct rob<R3D_setLayerMaterial, &RENDER_3D_OPENGL::setLayerMaterial>;
struct R3D_setArrowMaterial
{
typedef void ( RENDER_3D_OPENGL::*type )();
};
template struct rob<R3D_setArrowMaterial, &RENDER_3D_OPENGL::setArrowMaterial>;
struct R3D_m_grid
{
typedef GLuint RENDER_3D_OPENGL::*type;
};
template struct rob<R3D_m_grid, &RENDER_3D_OPENGL::m_grid>;
// ---- public wrappers ----
bool R3D_InitializeOpenGL( RENDER_3D_OPENGL& aRenderer )
{
return ( aRenderer.*result<R3D_initializeOpenGL>::ptr )();
}
void R3D_GenerateCylinder( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter,
float aInnerRadius, float aOuterRadius, float aZtop, float aZbot,
unsigned int aNrSides, TRIANGLE_DISPLAY_LIST* aDst )
{
( aRenderer.*result<R3D_generateCylinder>::ptr )( aCenter, aInnerRadius, aOuterRadius, aZtop,
aZbot, aNrSides, aDst );
}
void R3D_GenerateInvCone( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter,
float aInnerRadius, float aOuterRadius, float aZtop, float aZbot,
unsigned int aNrSides, TRIANGLE_DISPLAY_LIST* aDst, EDA_ANGLE aAngle )
{
( aRenderer.*result<R3D_generateInvCone>::ptr )( aCenter, aInnerRadius, aOuterRadius, aZtop,
aZbot, aNrSides, aDst, aAngle );
}
void R3D_GenerateDisk( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter, float aRadius,
float aZ, unsigned int aNrSides, TRIANGLE_DISPLAY_LIST* aDst, bool aTop )
{
( aRenderer.*result<R3D_generateDisk>::ptr )( aCenter, aRadius, aZ, aNrSides, aDst, aTop );
}
void R3D_GenerateDimple( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter, float aRadius,
float aZ, float aDepth, unsigned int aNrSides,
TRIANGLE_DISPLAY_LIST* aDst, bool aTop )
{
( aRenderer.*result<R3D_generateDimple>::ptr )( aCenter, aRadius, aZ, aDepth, aNrSides, aDst,
aTop );
}
void R3D_GenerateRing( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter, float aInnerRadius,
float aOuterRadius, unsigned int aNrSides,
std::vector<SFVEC2F>& aInnerContour, std::vector<SFVEC2F>& aOuterContour,
bool aInvertOrder )
{
( aRenderer.*result<R3D_generateRing>::ptr )( aCenter, aInnerRadius, aOuterRadius, aNrSides,
aInnerContour, aOuterContour, aInvertOrder );
}
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const FILLED_CIRCLE_2D* aCircle,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot )
{
( aRenderer.*result<R3D_addObj_Circle>::ptr )( aCircle, aDst, aZtop, aZbot );
}
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const RING_2D* aRing,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot )
{
( aRenderer.*result<R3D_addObj_Ring>::ptr )( aRing, aDst, aZtop, aZbot );
}
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const POLYGON_4PT_2D* aPoly,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot )
{
( aRenderer.*result<R3D_addObj_Poly4>::ptr )( aPoly, aDst, aZtop, aZbot );
}
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const TRIANGLE_2D* aTri,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot )
{
( aRenderer.*result<R3D_addObj_Tri>::ptr )( aTri, aDst, aZtop, aZbot );
}
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const ROUND_SEGMENT_2D* aSeg,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot )
{
( aRenderer.*result<R3D_addObj_Seg>::ptr )( aSeg, aDst, aZtop, aZbot );
}
bool R3D_AppendPostMachining( RENDER_3D_OPENGL& aRenderer, TRIANGLE_DISPLAY_LIST* aDst,
const SFVEC2F& aHoleCenter, PAD_DRILL_POST_MACHINING_MODE aMode,
int aSizeIU, int aDepthIU, float aHoleInnerRadius, float aZSurface,
bool aIsFront, float aPlatingThickness3d, float aUnitScale,
float* aZEnd )
{
return ( aRenderer.*result<R3D_appendPostMachining>::ptr )(
aDst, aHoleCenter, aMode, aSizeIU, aDepthIU, aHoleInnerRadius, aZSurface, aIsFront,
aPlatingThickness3d, aUnitScale, aZEnd );
}
OPENGL_RENDER_LIST* R3D_CreateBoard( RENDER_3D_OPENGL& aRenderer,
const SHAPE_POLY_SET& aBoardPoly,
const BVH_CONTAINER_2D* aThroughHoles )
{
return ( aRenderer.*result<R3D_createBoard>::ptr )( aBoardPoly, aThroughHoles );
}
void R3D_Generate3dGrid( RENDER_3D_OPENGL& aRenderer, GRID3D_TYPE aGridType )
{
( aRenderer.*result<R3D_generate3dGrid>::ptr )( aGridType );
}
unsigned int R3D_GetGridList( RENDER_3D_OPENGL& aRenderer )
{
return aRenderer.*result<R3D_m_grid>::ptr;
}
void R3D_SetupMaterials( RENDER_3D_OPENGL& aRenderer )
{
( aRenderer.*result<R3D_setupMaterials>::ptr )();
}
void R3D_SetLayerMaterial( RENDER_3D_OPENGL& aRenderer, PCB_LAYER_ID aLayerID )
{
( aRenderer.*result<R3D_setLayerMaterial>::ptr )( aLayerID );
}
void R3D_SetArrowMaterial( RENDER_3D_OPENGL& aRenderer )
{
( aRenderer.*result<R3D_setArrowMaterial>::ptr )();
}

View file

@ -0,0 +1,83 @@
/**
* Access to RENDER_3D_OPENGL's private geometry generators / material setters
* for the Tier-2 scenarios, using the same member-pointer technique as
* tests/gal-regression/native/gal_test_accessor.cpp (no KiCad header edits).
* Overloads are disambiguated by the tag's member-function-pointer typedef.
*/
#ifndef RENDER3D_TEST_ACCESSOR_H
#define RENDER3D_TEST_ACCESSOR_H
#include <plugins/3dapi/xv3d_types.h>
#include <3d_enums.h>
#include <geometry/eda_angle.h>
#include <layer_ids.h>
#include <padstack.h> // PAD_DRILL_POST_MACHINING_MODE
#include <vector>
class RENDER_3D_OPENGL;
class TRIANGLE_DISPLAY_LIST;
class OPENGL_RENDER_LIST;
class FILLED_CIRCLE_2D;
class RING_2D;
class POLYGON_4PT_2D;
class TRIANGLE_2D;
class ROUND_SEGMENT_2D;
class SHAPE_POLY_SET;
class BVH_CONTAINER_2D;
bool R3D_InitializeOpenGL( RENDER_3D_OPENGL& aRenderer );
void R3D_GenerateCylinder( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter,
float aInnerRadius, float aOuterRadius, float aZtop, float aZbot,
unsigned int aNrSides, TRIANGLE_DISPLAY_LIST* aDst );
void R3D_GenerateInvCone( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter,
float aInnerRadius, float aOuterRadius, float aZtop, float aZbot,
unsigned int aNrSides, TRIANGLE_DISPLAY_LIST* aDst, EDA_ANGLE aAngle );
void R3D_GenerateDisk( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter, float aRadius,
float aZ, unsigned int aNrSides, TRIANGLE_DISPLAY_LIST* aDst, bool aTop );
void R3D_GenerateDimple( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter, float aRadius,
float aZ, float aDepth, unsigned int aNrSides,
TRIANGLE_DISPLAY_LIST* aDst, bool aTop );
void R3D_GenerateRing( RENDER_3D_OPENGL& aRenderer, const SFVEC2F& aCenter, float aInnerRadius,
float aOuterRadius, unsigned int aNrSides,
std::vector<SFVEC2F>& aInnerContour, std::vector<SFVEC2F>& aOuterContour,
bool aInvertOrder );
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const FILLED_CIRCLE_2D* aCircle,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot );
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const RING_2D* aRing,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot );
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const POLYGON_4PT_2D* aPoly,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot );
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const TRIANGLE_2D* aTri,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot );
void R3D_AddObjTriangles( RENDER_3D_OPENGL& aRenderer, const ROUND_SEGMENT_2D* aSeg,
TRIANGLE_DISPLAY_LIST* aDst, float aZtop, float aZbot );
bool R3D_AppendPostMachining( RENDER_3D_OPENGL& aRenderer, TRIANGLE_DISPLAY_LIST* aDst,
const SFVEC2F& aHoleCenter, PAD_DRILL_POST_MACHINING_MODE aMode,
int aSizeIU, int aDepthIU, float aHoleInnerRadius, float aZSurface,
bool aIsFront, float aPlatingThickness3d, float aUnitScale,
float* aZEnd );
OPENGL_RENDER_LIST* R3D_CreateBoard( RENDER_3D_OPENGL& aRenderer,
const SHAPE_POLY_SET& aBoardPoly,
const BVH_CONTAINER_2D* aThroughHoles = nullptr );
void R3D_Generate3dGrid( RENDER_3D_OPENGL& aRenderer, GRID3D_TYPE aGridType );
/// The compiled grid display-list id (private m_grid).
unsigned int R3D_GetGridList( RENDER_3D_OPENGL& aRenderer );
void R3D_SetupMaterials( RENDER_3D_OPENGL& aRenderer );
void R3D_SetLayerMaterial( RENDER_3D_OPENGL& aRenderer, PCB_LAYER_ID aLayerID );
void R3D_SetArrowMaterial( RENDER_3D_OPENGL& aRenderer );
#endif // RENDER3D_TEST_ACCESSOR_H

View file

@ -0,0 +1,299 @@
/**
* Native 3D-renderer test application golden-baseline generator.
*
* Renders the shared scenarios (tests/3d-regression/scenarios/) through the
* REAL KiCad 3D-viewer OpenGL code on a desktop GL 2.1 compatibility context,
* captures each into a fixed-size offscreen FBO and writes PNGs. These PNGs
* are the committed goldens the WebGL port will be compared against with the
* pixelmatch engine (tests/tools/screenshots/compare-dirs.ts).
*
* Modeled on tests/gal-regression/native/gal_native_test.cpp.
*/
#include "kicad_stubs_3d.h" // kiglad before wx
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
#include "fbo_capture.h"
#include "scene3d_test_ctx.h"
#include "scene3d_test_scenarios.h"
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
namespace fs = std::filesystem;
// Fixed capture size — must match the WebGL harness canvas (wasm/3d_webgl_test.html)
// and manifest.json. FBO capture makes this independent of window size / Retina.
static const int CAPTURE_WIDTH = 800;
static const int CAPTURE_HEIGHT = 600;
static std::string g_outputDir;
static std::string g_manifestPath;
static std::string g_filter;
static bool g_showWindow = false;
static bool SavePng( const std::string& aPath, std::vector<uint8_t>& aPixels, int aWidth,
int aHeight )
{
// Force alpha opaque: the browser canvas the WebGL side screenshots is
// composited opaque, while the FBO keeps partial alpha (same rationale as
// gal_native_test.cpp SaveScreenshot).
for( size_t i = 3; i < aPixels.size(); i += 4 )
aPixels[i] = 255;
// OpenGL rows are bottom-up.
stbi_flip_vertically_on_write( 1 );
return stbi_write_png( aPath.c_str(), aWidth, aHeight, 4, aPixels.data(), aWidth * 4 ) != 0;
}
static bool WriteManifest( const std::string& aPath )
{
std::ofstream out( aPath );
if( !out )
{
std::cerr << "Failed to write manifest: " << aPath << "\n";
return false;
}
out << "{\n \"width\": " << CAPTURE_WIDTH << ",\n \"height\": " << CAPTURE_HEIGHT
<< ",\n \"scenarios\": [\n";
for( int i = 0; i < Scene3DTest::GetScenarioCount(); i++ )
{
out << " \"" << Scene3DTest::GetScenarioName( i ) << "\""
<< ( i + 1 < Scene3DTest::GetScenarioCount() ? "," : "" ) << "\n";
}
out << " ]\n}\n";
return true;
}
class TEST_GL_CANVAS : public wxGLCanvas
{
public:
explicit TEST_GL_CANVAS( wxWindow* aParent, const wxGLAttributes& aAttrs ) :
wxGLCanvas( aParent, aAttrs, wxID_ANY, wxDefaultPosition,
wxSize( CAPTURE_WIDTH, CAPTURE_HEIGHT ) )
{
wxGLContextAttrs ctxAttrs; // default: legacy compatibility profile —
ctxAttrs.PlatformDefaults().EndList(); // required for immediate mode + display lists
m_context = new wxGLContext( this, nullptr, &ctxAttrs );
}
~TEST_GL_CANVAS() override { delete m_context; }
bool MakeCurrent() { return SetCurrent( *m_context ); }
private:
wxGLContext* m_context;
};
class SCENE3D_TEST_FRAME : public wxFrame
{
public:
SCENE3D_TEST_FRAME() :
wxFrame( nullptr, wxID_ANY, "3D Renderer Native Test", wxDefaultPosition,
wxSize( CAPTURE_WIDTH, CAPTURE_HEIGHT ) )
{
wxGLAttributes attrs;
attrs.PlatformDefaults().RGBA().DoubleBuffer().Depth( 24 ).Stencil( 8 ).EndList();
m_canvas = new TEST_GL_CANVAS( this, attrs );
CallAfter( &SCENE3D_TEST_FRAME::RunScenarios );
}
void RunScenarios()
{
std::vector<int> toRun;
for( int i = 0; i < Scene3DTest::GetScenarioCount(); i++ )
{
const std::string name = Scene3DTest::GetScenarioName( i );
if( g_filter.empty() || name.find( g_filter ) != std::string::npos )
toRun.push_back( i );
}
m_total = static_cast<int>( toRun.size() );
if( !m_canvas->MakeCurrent() )
{
std::cerr << "Failed to make GL context current\n";
Close();
return;
}
const int gladVersion = gladLoaderLoadGL();
if( !gladVersion )
{
std::cerr << "gladLoaderLoadGL failed\n";
Close();
return;
}
std::cout << "GL_VERSION: " << (const char*) glGetString( GL_VERSION ) << "\n";
std::cout << "GL_RENDERER: " << (const char*) glGetString( GL_RENDERER ) << "\n";
// The FFP renderer needs a compatibility context: display lists must exist.
if( !glad_glGenLists )
{
std::cerr << "glGenLists did not load — not a compatibility context?\n";
Close();
return;
}
FBO_CAPTURE fbo;
if( !fbo.Create( CAPTURE_WIDTH, CAPTURE_HEIGHT ) )
{
std::cerr << "FBO creation failed\n";
Close();
return;
}
fs::create_directories( g_outputDir );
SCENE3D_CTX ctx( CAPTURE_WIDTH, CAPTURE_HEIGHT );
ctx.InitOnce(); // initializeOpenGL()-equivalent state + circle texture
std::vector<uint8_t> pixels;
for( int i : toRun )
{
const std::string name = Scene3DTest::GetScenarioName( i );
std::cout << "Scenario " << i << ": " << name << "... " << std::flush;
fbo.Bind();
Scene3DTest::RenderScenario( ctx, i );
const std::string path = g_outputDir + "/3d-" + name + ".png";
if( fbo.ReadPixels( pixels ) && SavePng( path, pixels, fbo.Width(), fbo.Height() ) )
{
std::cout << "OK\n";
m_passed++;
}
else
{
std::cout << "FAILED\n";
}
}
fbo.Destroy();
if( !g_manifestPath.empty() )
{
if( WriteManifest( g_manifestPath ) )
std::cout << "Manifest: " << g_manifestPath << "\n";
else
m_passed = -1;
}
std::cout << "\nResults: " << m_passed << "/" << m_total << " scenarios saved\n";
if( !g_showWindow )
Close();
}
int GetPassed() const { return m_passed; }
int GetTotal() const { return m_total; }
private:
TEST_GL_CANVAS* m_canvas;
int m_passed = 0;
int m_total = 0;
};
class SCENE3D_TEST_APP : public wxApp
{
public:
bool OnInit() override
{
m_frame = new SCENE3D_TEST_FRAME();
m_frame->Show( true );
return true;
}
int OnExit() override
{
if( m_frame )
return ( m_frame->GetPassed() == m_frame->GetTotal() ) ? 0 : 1;
return 1;
}
private:
SCENE3D_TEST_FRAME* m_frame = nullptr;
};
wxIMPLEMENT_APP_NO_MAIN( SCENE3D_TEST_APP );
int main( int argc, char** argv )
{
for( int i = 1; i < argc; i++ )
{
const std::string arg = argv[i];
if( arg == "--output" && i + 1 < argc )
{
g_outputDir = argv[++i];
}
else if( arg == "--manifest" && i + 1 < argc )
{
g_manifestPath = argv[++i];
}
else if( arg == "--filter" && i + 1 < argc )
{
g_filter = argv[++i];
}
else if( arg == "--show" )
{
g_showWindow = true;
}
else if( arg == "--list" )
{
for( int s = 0; s < Scene3DTest::GetScenarioCount(); s++ )
std::cout << s << ": " << Scene3DTest::GetScenarioName( s ) << "\n";
return 0;
}
else
{
std::cout << "Usage: scene3d_native_test --output <dir> [options]\n"
" --output <dir> Output directory for 3d-<name>.png\n"
" --manifest <file> Write the scenario manifest JSON\n"
" --filter <substr> Only run scenarios whose name contains <substr>\n"
" --list Print scenario names and exit\n"
" --show Keep the window open after rendering\n";
return arg == "--help" ? 0 : 2;
}
}
if( g_outputDir.empty() )
{
std::cerr << "--output is required (or use --list)\n";
return 2;
}
std::cout << "3D Renderer Native Test - RENDER_3D_OPENGL Baseline Generator\n";
std::cout << "==============================================================\n\n";
return wxEntry( argc, argv );
}

View file

@ -0,0 +1,159 @@
/**
* Test-harness stub of the EDA_3D_VIEWER_SETTINGS / APP_SETTINGS_BASE /
* JSON_SETTINGS chain. The renderer only reads m_Render/m_Camera plain
* fields; none of the JSON load/store machinery ever runs, so every virtual
* is a no-op and the ctor fills the render settings with the upstream
* defaults (deterministic test values, documented deviations flagged).
*/
#include "kicad_stubs_3d.h"
#include "3d_viewer/eda_3d_viewer_settings.h"
#include "common_ogl/ogl_attr_list.h" // ANTIALIASING_MODE (fwd-declared in the settings header)
#include <settings/json_settings_internals.h>
// ---- JSON_SETTINGS ----
JSON_SETTINGS::JSON_SETTINGS( const wxString& aFilename, SETTINGS_LOC aLocation,
int aSchemaVersion, bool aCreateIfMissing, bool aCreateIfDefault,
bool aWriteFile ) :
m_filename( aFilename ),
m_legacy_filename( "" ),
m_location( aLocation ),
m_createIfMissing( aCreateIfMissing ),
m_createIfDefault( aCreateIfDefault ),
m_writeFile( aWriteFile ),
m_modified( false ),
m_deleteLegacyAfterMigration( false ),
m_resetParamsIfMissing( true ),
m_schemaVersion( aSchemaVersion ),
m_manager( nullptr )
{
m_internals = std::make_unique<JSON_SETTINGS_INTERNALS>();
}
JSON_SETTINGS::~JSON_SETTINGS() = default;
void JSON_SETTINGS::Load() {}
bool JSON_SETTINGS::Store() { return false; }
bool JSON_SETTINGS::LoadFromFile( const wxString& ) { return false; }
bool JSON_SETTINGS::SaveToFile( const wxString&, bool ) { return false; }
std::map<std::string, nlohmann::json> JSON_SETTINGS::GetFileHistories() { return {}; }
bool JSON_SETTINGS::MigrateFromLegacy( wxConfigBase* ) { return false; }
// ---- APP_SETTINGS_BASE ----
APP_SETTINGS_BASE::APP_SETTINGS_BASE( const std::string& aFilename, int aSchemaVersion ) :
JSON_SETTINGS( aFilename, SETTINGS_LOC::USER, aSchemaVersion, true, true, true ),
m_CrossProbing(),
m_FindReplace(),
m_Graphics(),
m_ColorPicker(),
m_LibTree(),
m_Printing(),
m_SearchPane(),
m_System(),
m_Window(),
m_appSettingsSchemaVersion( aSchemaVersion )
{
}
bool APP_SETTINGS_BASE::MigrateFromLegacy( wxConfigBase* ) { return false; }
// ---- EDA_3D_VIEWER_SETTINGS ----
EDA_3D_VIEWER_SETTINGS::EDA_3D_VIEWER_SETTINGS() :
APP_SETTINGS_BASE( "3d_viewer", 0 ),
m_Render(),
m_Camera()
{
RENDER_SETTINGS& r = m_Render;
// Upstream defaults (eda_3d_viewer_settings.cpp PARAM defaults), with two
// determinism-driven deviations: engine is OPENGL (the renderer under
// test) and AA is NONE (context is single-sample anyway).
r.engine = RENDER_ENGINE::OPENGL;
r.grid_type = GRID3D_TYPE::NONE;
r.opengl_AA_mode = ANTIALIASING_MODE::AA_NONE;
r.material_mode = MATERIAL_MODE::NORMAL;
r.opengl_AA_disableOnMove = false;
r.opengl_thickness_disableOnMove = false;
r.opengl_microvias_disableOnMove = false;
r.opengl_holes_disableOnMove = false;
r.opengl_render_bbox_only_OnMove = false;
r.opengl_copper_thickness = true;
r.show_model_bbox = false;
r.show_off_board_silk = false;
r.highlight_on_rollover = false;
r.opengl_selection_color = KIGFX::COLOR4D( 0.0, 1.0, 0.0, 1.0 );
r.raytrace_anti_aliasing = false;
r.raytrace_backfloor = false;
r.raytrace_post_processing = false;
r.raytrace_procedural_textures = false;
r.raytrace_reflections = false;
r.raytrace_refractions = false;
r.raytrace_shadows = false;
r.raytrace_nrsamples_shadows = 0;
r.raytrace_nrsamples_reflections = 0;
r.raytrace_nrsamples_refractions = 0;
r.raytrace_spread_shadows = 0.0f;
r.raytrace_spread_reflections = 0.0f;
r.raytrace_spread_refractions = 0.0f;
r.raytrace_recursivelevel_reflections = 0;
r.raytrace_recursivelevel_refractions = 0;
r.raytrace_lightColorCamera = KIGFX::COLOR4D( 0.2, 0.2, 0.2, 1.0 );
r.raytrace_lightColorTop = KIGFX::COLOR4D( 0.247, 0.247, 0.247, 1.0 );
r.raytrace_lightColorBottom = KIGFX::COLOR4D( 0.247, 0.247, 0.247, 1.0 );
r.show_adhesive = true;
r.show_navigator = false;
r.show_board_body = true;
r.show_plated_barrels = true;
r.show_comments = true;
r.show_drawings = true;
r.show_eco1 = true;
r.show_eco2 = true;
for( bool& user : r.show_user )
user = false;
r.show_footprints_insert = true;
r.show_footprints_normal = true;
r.show_footprints_virtual = true;
r.show_footprints_not_in_posfile = true;
r.show_footprints_dnp = true;
r.show_silkscreen_top = true;
r.show_silkscreen_bottom = true;
r.show_soldermask_top = true;
r.show_soldermask_bottom = true;
r.show_solderpaste = true;
r.show_copper_top = true;
r.show_copper_bottom = true;
r.show_zones = true;
r.show_fp_references = true;
r.show_fp_values = true;
r.show_fp_text = true;
r.subtract_mask_from_silk = false;
r.clip_silk_on_via_annuli = true;
r.differentiate_plated_copper = true;
r.use_board_editor_copper_colors = false;
r.preview_show_board_body = true;
m_Camera.animation_enabled = false;
m_Camera.moving_speed_multiplier = 3;
m_Camera.rotation_increment = 10.0;
m_Camera.projection_mode = 0;
}
LAYER_PRESET_3D* EDA_3D_VIEWER_SETTINGS::FindPreset( const wxString& )
{
return nullptr;
}
bool EDA_3D_VIEWER_SETTINGS::MigrateFromLegacy( wxConfigBase* )
{
return false;
}

File diff suppressed because it is too large Load diff

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

View file

@ -0,0 +1,120 @@
/**
* 3D-renderer WebGL test harness (WASM) runs the shared scenarios
* (tests/3d-regression/scenarios/) in the browser. Today the legacy GL
* surface is wasm/stubs/gl_ffp_stub.c no-ops, so every scenario renders a
* black canvas: the TDD red state the WebGL port turns green.
*
* Unlike the GAL harness this needs no wx window: the renderer draws into
* whatever context is current, so a direct emscripten WebGL2 context on
* #canvas is enough created with the attributes the suite requires
* (stencil for DrawCulled, no MSAA to match the native single-sample FBO,
* preserveDrawingBuffer for Playwright canvas screenshots).
*/
#include <emscripten.h>
#include <emscripten/html5.h>
#include "scene3d_test_ctx.h"
#include "scene3d_test_scenarios.h"
#include <cstdio>
static const int CAPTURE_WIDTH = 800; // must match manifest.json + native FBO
static const int CAPTURE_HEIGHT = 600;
static EMSCRIPTEN_WEBGL_CONTEXT_HANDLE g_context = 0;
static SCENE3D_CTX* g_ctx = nullptr;
extern "C"
{
EMSCRIPTEN_KEEPALIVE
int getTotalScenarios()
{
return Scene3DTest::GetScenarioCount();
}
EMSCRIPTEN_KEEPALIVE
const char* getScenarioName( int aIndex )
{
return Scene3DTest::GetScenarioName( aIndex );
}
EMSCRIPTEN_KEEPALIVE
int getCanvasWidth()
{
return CAPTURE_WIDTH;
}
EMSCRIPTEN_KEEPALIVE
int getCanvasHeight()
{
return CAPTURE_HEIGHT;
}
EMSCRIPTEN_KEEPALIVE
int runScenario( int aIndex )
{
if( aIndex < 0 || aIndex >= Scene3DTest::GetScenarioCount() )
return -1;
if( emscripten_webgl_make_context_current( g_context ) != EMSCRIPTEN_RESULT_SUCCESS )
return -2;
if( !g_ctx )
{
g_ctx = new SCENE3D_CTX( CAPTURE_WIDTH, CAPTURE_HEIGHT );
g_ctx->InitOnce();
}
std::printf( "[3d-webgl] scenario %d: %s\n", aIndex, Scene3DTest::GetScenarioName( aIndex ) );
// Start from a cleared frame; scenarios call BeginFrame themselves.
glViewport( 0, 0, CAPTURE_WIDTH, CAPTURE_HEIGHT );
glClearColor( 0.0f, 0.0f, 0.0f, 1.0f );
glClearDepth( 1.0f );
glClearStencil( 0 );
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT );
Scene3DTest::RenderScenario( *g_ctx, aIndex );
glFinish();
return 0;
}
} // extern "C"
int main()
{
EmscriptenWebGLContextAttributes attrs;
emscripten_webgl_init_context_attributes( &attrs );
attrs.majorVersion = 2;
attrs.minorVersion = 0;
attrs.alpha = false;
attrs.depth = true;
attrs.stencil = true; // DrawCulled hole subtraction
attrs.antialias = false; // native goldens are single-sample
attrs.preserveDrawingBuffer = true; // Playwright canvas.screenshot()
emscripten_set_canvas_element_size( "#canvas", CAPTURE_WIDTH, CAPTURE_HEIGHT );
g_context = emscripten_webgl_create_context( "#canvas", &attrs );
if( g_context <= 0 )
{
std::fprintf( stderr, "[3d-webgl] failed to create WebGL2 context (%ld)\n",
(long) g_context );
return 1;
}
emscripten_webgl_make_context_current( g_context );
std::printf( "[3d-webgl] ready: %d scenarios, %dx%d\n", Scene3DTest::GetScenarioCount(),
CAPTURE_WIDTH, CAPTURE_HEIGHT );
EM_ASM( { if( window._threeDTestReady ) window._threeDTestReady(); } );
return 0;
}

View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>3D Renderer WebGL Test</title>
<style>
body { margin: 0; background: #202020; }
#canvas { display: block; width: 800px; height: 600px; }
</style>
</head>
<body>
<canvas id="canvas" width="800" height="600"></canvas>
<script>
let ready = false;
let wasmModule = null;
window._threeDTestReady = () => { ready = true; };
window.threeDTest = {
isReady: () => ready,
runScenario: (i) => wasmModule.ccall('runScenario', 'number', ['number'], [i]),
getTotalScenarios: () => wasmModule.ccall('getTotalScenarios', 'number', [], []),
getScenarioName: (i) => wasmModule.ccall('getScenarioName', 'string', ['number'], [i]),
getCanvasWidth: () => wasmModule.ccall('getCanvasWidth', 'number', [], []),
getCanvasHeight: () => wasmModule.ccall('getCanvasHeight', 'number', [], []),
};
</script>
<script src="3d_webgl_test.js"></script>
<script>
create3DTest({ canvas: document.getElementById('canvas') })
.then((mod) => { wasmModule = mod; })
.catch((err) => { console.error('[3d-webgl] module init failed:', err); });
</script>
</body>
</html>

View file

@ -0,0 +1,194 @@
# Makefile for the 3D-renderer WebGL test harness (WASM) — TDD RED STATE.
#
# Compiles the SAME shared scenarios + real KiCad 3D-viewer TUs as the native
# golden generator (tests/3d-regression/native/CMakeLists.txt), but links the
# fixed-function GL surface against wasm/stubs/gl_ffp_stub.c no-ops — exactly
# how the production kicad build satisfies RENDER_3D_OPENGL's link today.
# Every scenario therefore renders BLANK until the WebGL port replaces those
# stubs; `npm run 3d:check:parity` is the port-progress meter.
#
# Usage:
# make # Build
# make clean # Clean build artifacts
# make DEBUG=1 # Debug build with source maps
CXX = em++
CC = emcc
PROJECT_ROOT = ../../..
KICAD_ROOT = $(PROJECT_ROOT)/kicad
WX_BUILD = $(PROJECT_ROOT)/build-wasm/wxwidgets
OUTPUT_DIR = ../../apps/3d-webgl
# wxWidgets WASM (the KiCad TUs reference wxString/wxLogTrace/wxASSERT).
WXCONFIG = $(WX_BUILD)/wx-config
WX_CXXFLAGS := $(shell $(WXCONFIG) --cxxflags)
WX_LDFLAGS := $(shell $(WXCONFIG) --libs base,core,gl)
# Sysroot (boost, glm, ...)
SYSROOT = $(PROJECT_ROOT)/build-wasm/sysroot
# Mirrors the include set of tests/3d-regression/native/CMakeLists.txt.
KICAD_INCLUDES = -I../native \
-I../scenarios \
-I$(KICAD_ROOT)/include \
-I$(KICAD_ROOT)/3d-viewer \
-I$(KICAD_ROOT)/3d-viewer/3d_rendering \
-I$(KICAD_ROOT)/3d-viewer/3d_viewer \
-I$(KICAD_ROOT)/pcbnew \
-I$(KICAD_ROOT)/common \
-I$(KICAD_ROOT)/libs/kimath/include \
-I$(KICAD_ROOT)/libs/core/include \
-I$(KICAD_ROOT)/thirdparty/clipper2/Clipper2Lib/include \
-I$(KICAD_ROOT)/thirdparty/dynamic_bitset \
-I$(KICAD_ROOT)/thirdparty/rtree \
-I$(KICAD_ROOT)/thirdparty/magic_enum/magic_enum \
-I$(KICAD_ROOT)/thirdparty/expected/include \
-I$(KICAD_ROOT)/thirdparty/nlohmann_json \
-I$(KICAD_ROOT)/thirdparty \
-I$(PROJECT_ROOT)/wasm/stubs \
-I$(SYSROOT)/include
ifdef DEBUG
OPT_FLAGS = -g -O0
DEBUG_LDFLAGS = -g -gsource-map
else
OPT_FLAGS = -O1
DEBUG_LDFLAGS =
endif
# Match the wx/KiCad WASM exception model (scripts/common/env.sh DEPS_EH_FLAGS).
DEPS_EH_FLAGS ?= -fwasm-exceptions -sSUPPORT_LONGJMP=wasm -sWASM_LEGACY_EXCEPTIONS=1
# USINGZ: KiCad builds clipper2 with it (PUBLIC compile definition).
CXXFLAGS = $(OPT_FLAGS) $(DEPS_EH_FLAGS) -DUSINGZ $(WX_CXXFLAGS) $(KICAD_INCLUDES) -std=c++20 -MMD -MP
CFLAGS = $(OPT_FLAGS) $(DEPS_EH_FLAGS) -I$(PROJECT_ROOT)/wasm/stubs
BASE_LDFLAGS = $(DEPS_EH_FLAGS) \
-sALLOW_MEMORY_GROWTH=1 \
-sERROR_ON_UNDEFINED_SYMBOLS=0 \
-sEXPORTED_FUNCTIONS=['_main','_runScenario','_getTotalScenarios','_getScenarioName','_getCanvasWidth','_getCanvasHeight'] \
-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap'] \
-sMODULARIZE=1 \
-sEXPORT_NAME='create3DTest' \
-sENVIRONMENT=web,worker
# Pure WebGL 2.0 context; the FFP calls are stubbed C no-ops (no
# LEGACY_GL_EMULATION), same as the production kicad_editor build.
EM_GL_FLAGS = -sMAX_WEBGL_VERSION=2
LDFLAGS = $(DEBUG_LDFLAGS) $(BASE_LDFLAGS) $(EM_GL_FLAGS) $(WX_LDFLAGS)
MAIN_SRCS = 3d_webgl_test.cpp
SCENARIO_SRCS = ../scenarios/scene3d_test_scenarios.cpp \
../scenarios/scene3d_test_ctx.cpp \
../scenarios/test_board_data.cpp \
$(wildcard ../scenarios/scenario_*.cpp)
# Test-impl TUs shared with the native harness (BOARD_ADAPTER seam, settings
# stub, link stubs, private-member accessor).
TESTIMPL_SRCS = ../native/board_adapter_test_impl.cpp \
../native/settings_3d_stub.cpp \
../native/kicad_stubs_3d.cpp \
../native/render3d_test_accessor.cpp
# Same real-KiCad TU list as the native CMakeLists (minus glad — Emscripten
# provides the modern GL surface, gl_ffp_stub.c the legacy one).
KICAD_3D_SRCS = \
$(KICAD_ROOT)/3d-viewer/common_ogl/ogl_utils.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/image.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/buffers_debug.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/color_rgba.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/track_ball.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/trackball.cpp \
$(KICAD_ROOT)/common/gal/3d/camera.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/opengl/layer_triangles.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/opengl/opengl_utils.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/opengl/3d_model.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/opengl/3d_spheres_gizmo.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/ray.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/accelerators/container_2d.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes2D/object_2d.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes2D/bbox_2d.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes2D/round_segment_2d.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes2D/filled_circle_2d.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes3D/bbox_3d.cpp \
$(KICAD_ROOT)/libs/kimath/src/trigo.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/eda_angle.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/seg.cpp \
$(KICAD_ROOT)/libs/kimath/src/math/util.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/opengl/render_3d_opengl.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/opengl/create_scene.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/render_3d_base.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes2D/ring_2d.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes2D/triangle_2d.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes2D/4pt_polygon_2d.cpp \
$(KICAD_ROOT)/3d-viewer/3d_rendering/raytracing/shapes2D/polygon_2d.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_poly_set.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_line_chain.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/shape.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_arc.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/vertex_set.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/geometry_utils.cpp \
$(KICAD_ROOT)/libs/kimath/src/convert_basic_shapes_to_polygon.cpp \
$(KICAD_ROOT)/libs/kimath/src/md5_hash.cpp \
$(KICAD_ROOT)/libs/kimath/src/bezier_curves.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/circle.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/arc_chord_params.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/corner_operations.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_collisions.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_compound.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_rect.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_nearest_points.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/half_line.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_utils.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/roundrect.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/line.cpp \
$(KICAD_ROOT)/libs/kimath/src/geometry/shape_segment.cpp \
$(KICAD_ROOT)/libs/kimath/src/math/vector2.cpp \
$(KICAD_ROOT)/common/layer_id.cpp \
$(KICAD_ROOT)/common/gal/color4d.cpp \
$(KICAD_ROOT)/common/kicad_gl/gl_context_mgr.cpp \
$(KICAD_ROOT)/common/lset.cpp \
$(KICAD_ROOT)/libs/core/utf8.cpp \
$(KICAD_ROOT)/thirdparty/clipper2/Clipper2Lib/src/clipper.engine.cpp \
$(KICAD_ROOT)/thirdparty/clipper2/Clipper2Lib/src/clipper.offset.cpp \
$(KICAD_ROOT)/thirdparty/clipper2/Clipper2Lib/src/clipper.rectclip.cpp
# THE RED STATE: legacy fixed-function GL as no-ops.
FFP_STUB_SRCS = $(PROJECT_ROOT)/wasm/stubs/gl_ffp_stub.c
SRCS = $(MAIN_SRCS) $(SCENARIO_SRCS) $(TESTIMPL_SRCS) $(KICAD_3D_SRCS)
OBJS = $(SRCS:.cpp=.o) $(FFP_STUB_SRCS:.c=.o)
DEPS = $(SRCS:.cpp=.d)
TARGET = $(OUTPUT_DIR)/3d_webgl_test.js
all: $(OUTPUT_DIR) $(TARGET)
$(OUTPUT_DIR):
mkdir -p $(OUTPUT_DIR)
%.o: %.cpp
$(CXX) -c $(CXXFLAGS) $< -o $@
%.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
$(TARGET): $(OBJS)
@echo "Linking $(words $(OBJS)) objects..."
$(CXX) $(OBJS) $(LDFLAGS) -o $@
cp 3d_webgl_test.html $(OUTPUT_DIR)/
clean:
rm -f $(OBJS) $(DEPS)
rm -f $(OUTPUT_DIR)/3d_webgl_test.js
rm -f $(OUTPUT_DIR)/3d_webgl_test.wasm
rm -f $(OUTPUT_DIR)/3d_webgl_test.html
rm -f ../scenarios/*.o ../scenarios/*.d ../native/*.o ../native/*.d
.PHONY: all clean
-include $(DEPS)