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>
2
.gitignore
vendored
|
|
@ -65,6 +65,8 @@ wxwidgets-clean/
|
|||
!tests/apps/kicad/pcbnew.html
|
||||
/tests/apps/gal-webgl/*.js
|
||||
/tests/apps/gal-webgl/*.wasm
|
||||
/tests/apps/3d-webgl/*.js
|
||||
/tests/apps/3d-webgl/*.wasm
|
||||
/temp/
|
||||
*.log
|
||||
*.tmp
|
||||
|
|
|
|||
55
scripts/build-3d-native-test.sh
Executable file
|
|
@ -0,0 +1,55 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Build script for the native 3D-renderer test harness (tests/3d-regression/native).
|
||||
#
|
||||
# Builds a standalone macOS application that renders the shared 3D scenarios
|
||||
# through KiCad's actual RENDER_3D_OPENGL code paths (real desktop OpenGL) and
|
||||
# writes golden baseline PNGs for the OpenGL->WebGL port regression suite.
|
||||
#
|
||||
# Modeled on scripts/build-gal-native-test.sh.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
TEST_DIR="$PROJECT_ROOT/tests/3d-regression/native"
|
||||
BUILD_DIR="$TEST_DIR/build"
|
||||
LOG_FILE="$PROJECT_ROOT/tests/logs/3d-native-build.log"
|
||||
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
echo "Building 3D Renderer Native Test..."
|
||||
echo " Test dir: $TEST_DIR"
|
||||
echo " Build dir: $BUILD_DIR"
|
||||
echo " Log file: $LOG_FILE"
|
||||
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
echo "Running CMake..."
|
||||
if ! cmake -S "$TEST_DIR" -B "$BUILD_DIR" >> "$LOG_FILE" 2>&1; then
|
||||
echo "CMake configuration failed! Check $LOG_FILE for details."
|
||||
echo ""
|
||||
echo "Last 50 lines of log:"
|
||||
tail -50 "$LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Building..."
|
||||
if ! make -C "$BUILD_DIR" -j"$(sysctl -n hw.ncpu 2>/dev/null || echo 4)" >> "$LOG_FILE" 2>&1; then
|
||||
echo "Build failed! Check $LOG_FILE for details."
|
||||
echo ""
|
||||
echo "Last 100 lines of log:"
|
||||
tail -100 "$LOG_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Build successful!"
|
||||
echo "Executable: $BUILD_DIR/scene3d_native_test"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --output <dir> Output directory for 3d-<name>.png"
|
||||
echo " --manifest <file> Write the scenario manifest JSON"
|
||||
echo " --filter <substr> Only run scenarios whose name contains <substr>"
|
||||
echo " --list Print scenario names and exit"
|
||||
echo " --show Keep the window open after rendering"
|
||||
75
scripts/build-3d-webgl-test.sh
Executable file
|
|
@ -0,0 +1,75 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Build script for the 3D-renderer WebGL test harness (WASM).
|
||||
#
|
||||
# Builds the shared 3D scenarios (tests/3d-regression/scenarios) + real KiCad
|
||||
# 3D-viewer TUs against the FFP no-op stubs (wasm/stubs/gl_ffp_stub.c) — the
|
||||
# TDD red state for the OpenGL->WebGL port. Output: tests/apps/3d-webgl/.
|
||||
#
|
||||
# Requires the wxWidgets WASM build (scripts/build-wx-wasm.sh) and the sysroot
|
||||
# headers (boost/glm) in build-wasm/sysroot.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/build-3d-webgl-test.sh # Clean build (default)
|
||||
# ./scripts/build-3d-webgl-test.sh --no-clean # Incremental build
|
||||
# ./scripts/build-3d-webgl-test.sh --debug # Debug build with source maps
|
||||
#
|
||||
# Modeled on scripts/build-gal-webgl-test.sh (no shader-generation step — the
|
||||
# FFP renderer has no GLSL yet; the port will add one here).
|
||||
#
|
||||
|
||||
source "$(dirname "$0")/common/logging.sh"
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
JOBS="${JOBS:-$(nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)}"
|
||||
|
||||
QUIET=1 source "$SCRIPT_DIR/common/env.sh"
|
||||
|
||||
TEST_DIR="$PROJECT_ROOT/tests/3d-regression/wasm"
|
||||
OUTPUT_DIR="$PROJECT_ROOT/tests/apps/3d-webgl"
|
||||
|
||||
echo "Building 3D WebGL Test (red-state harness)..."
|
||||
echo " Test dir: $TEST_DIR"
|
||||
echo " Output dir: $OUTPUT_DIR"
|
||||
|
||||
if ! command -v em++ &> /dev/null; then
|
||||
echo "ERROR: em++ not found. Run: ./scripts/setup-emsdk.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -x "$PROJECT_ROOT/build-wasm/wxwidgets/wx-config" ]; then
|
||||
echo "ERROR: wxWidgets WASM build missing. Run: ./scripts/build-wx-wasm.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo " Emscripten: $(em++ --version 2>&1 | head -1)"
|
||||
|
||||
DEBUG_BUILD=0
|
||||
CLEAN_BUILD=1
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--debug) DEBUG_BUILD=1 ;;
|
||||
--no-clean) CLEAN_BUILD=0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
cd "$TEST_DIR"
|
||||
|
||||
if [ "$CLEAN_BUILD" = "1" ]; then
|
||||
make clean || true
|
||||
fi
|
||||
|
||||
rm -f "$OUTPUT_DIR"/*.js "$OUTPUT_DIR"/*.wasm
|
||||
|
||||
if [ "$DEBUG_BUILD" = "1" ]; then
|
||||
make -j"$JOBS" DEBUG=1
|
||||
else
|
||||
make -j"$JOBS"
|
||||
fi
|
||||
|
||||
echo "Build successful: $OUTPUT_DIR/3d_webgl_test.{js,wasm,html}"
|
||||
echo "Serve with: cd tests && npm run serve -> /3d-webgl/3d_webgl_test.html"
|
||||
179
scripts/test-3d-regression.sh
Executable file
|
|
@ -0,0 +1,179 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# 3D-renderer regression suite orchestrator (tests/3d-regression).
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/test-3d-regression.sh # native: build -> run -> compare vs baseline;
|
||||
# # webgl phase too once the wasm harness exists
|
||||
# ./scripts/test-3d-regression.sh native # native phase only
|
||||
# ./scripts/test-3d-regression.sh webgl # wasm build -> playwright -> webgl-self + parity
|
||||
# ./scripts/test-3d-regression.sh compare # comparisons only (skip builds/runs)
|
||||
# ./scripts/test-3d-regression.sh promote # promote output/native -> baseline/ (byte-diff
|
||||
# # guarded) + manifest.json
|
||||
#
|
||||
# Comparison engine: the pixelmatch CI tooling (tests/tools/screenshots/compare-dirs.ts),
|
||||
# NOT ImageMagick. Levels/floors: tests/3d-regression/floors.json.
|
||||
#
|
||||
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common/logging.sh"
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
THREED_DIR="$PROJECT_ROOT/tests/3d-regression"
|
||||
BASELINE_DIR="$THREED_DIR/baseline"
|
||||
BASELINE_WEBGL_DIR="$THREED_DIR/baseline-webgl"
|
||||
NATIVE_OUT="$THREED_DIR/output/native"
|
||||
WEBGL_OUT="$THREED_DIR/output/webgl"
|
||||
NATIVE_BIN="$THREED_DIR/native/build/scene3d_native_test"
|
||||
|
||||
MODE="${1:-all}"
|
||||
|
||||
NATIVE_COMPARE_STATUS=0
|
||||
WEBGL_COMPARE_STATUS=0
|
||||
|
||||
run_native() {
|
||||
echo "=== Native phase: build + render ==="
|
||||
"$SCRIPT_DIR/build-3d-native-test.sh"
|
||||
|
||||
mkdir -p "$NATIVE_OUT"
|
||||
"$NATIVE_BIN" --output "$NATIVE_OUT" --manifest "$NATIVE_OUT/manifest.json"
|
||||
|
||||
# Anti-drift guard: the scenario registry (names/size) must match the
|
||||
# committed manifest; a mismatch means scenarios changed and the baselines
|
||||
# need review + re-promote.
|
||||
if [ -f "$THREED_DIR/manifest.json" ]; then
|
||||
if ! cmp -s "$THREED_DIR/manifest.json" "$NATIVE_OUT/manifest.json"; then
|
||||
echo "ERROR: scenario registry changed (manifest.json differs from committed copy)."
|
||||
echo "Review the change, then: ./scripts/test-3d-regression.sh promote"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "NOTE: no committed manifest yet (first run) — promote will create it."
|
||||
fi
|
||||
}
|
||||
|
||||
compare_native() {
|
||||
if [ ! -d "$BASELINE_DIR" ] || [ -z "$(ls -A "$BASELINE_DIR" 2>/dev/null)" ]; then
|
||||
echo "SKIP native-self compare: no committed baseline yet (run promote first)."
|
||||
return
|
||||
fi
|
||||
|
||||
echo "=== Compare: native-self (baseline/ vs output/native/) ==="
|
||||
( cd "$PROJECT_ROOT/tests" && npm run --silent 3d:check ) || NATIVE_COMPARE_STATUS=$?
|
||||
}
|
||||
|
||||
run_webgl() {
|
||||
if [ ! -f "$THREED_DIR/wasm/Makefile" ]; then
|
||||
echo "SKIP webgl phase: tests/3d-regression/wasm not present yet."
|
||||
return
|
||||
fi
|
||||
|
||||
if [ ! -x "$PROJECT_ROOT/build-wasm/wxwidgets/wx-config" ]; then
|
||||
echo "SKIP webgl phase: wxWidgets WASM build missing (scripts/build-wx-wasm.sh)."
|
||||
return
|
||||
fi
|
||||
|
||||
echo "=== WebGL phase: build + playwright capture ==="
|
||||
"$SCRIPT_DIR/build-3d-webgl-test.sh"
|
||||
( cd "$PROJECT_ROOT/tests" && npx playwright test e2e/3d-webgl.spec.ts )
|
||||
}
|
||||
|
||||
compare_webgl() {
|
||||
if [ ! -d "$WEBGL_OUT" ] || [ -z "$(ls -A "$WEBGL_OUT" 2>/dev/null)" ]; then
|
||||
echo "SKIP webgl compares: no webgl renders in output/webgl."
|
||||
return
|
||||
fi
|
||||
|
||||
if [ -d "$BASELINE_WEBGL_DIR" ] && [ -n "$(ls -A "$BASELINE_WEBGL_DIR" 2>/dev/null)" ]; then
|
||||
echo "=== Compare: webgl-self (baseline-webgl/ vs output/webgl/) ==="
|
||||
( cd "$PROJECT_ROOT/tests" && npm run --silent 3d:check:webgl ) || WEBGL_COMPARE_STATUS=$?
|
||||
else
|
||||
echo "SKIP webgl-self compare: no baseline-webgl yet."
|
||||
fi
|
||||
|
||||
# Port-parity is the TDD progress meter: informational, never gates.
|
||||
echo "=== Compare: parity (baseline/ vs output/webgl/, informational) ==="
|
||||
( cd "$PROJECT_ROOT/tests" && npm run --silent 3d:check:parity ) || true
|
||||
}
|
||||
|
||||
promote() {
|
||||
if [ ! -d "$NATIVE_OUT" ] || [ -z "$(ls -A "$NATIVE_OUT" 2>/dev/null)" ]; then
|
||||
echo "ERROR: nothing to promote — run the native phase first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$BASELINE_DIR"
|
||||
local changed=0
|
||||
|
||||
for png in "$NATIVE_OUT"/3d-*.png; do
|
||||
local name
|
||||
name="$(basename "$png")"
|
||||
|
||||
if ! cmp -s "$png" "$BASELINE_DIR/$name"; then
|
||||
cp "$png" "$BASELINE_DIR/$name"
|
||||
echo " promoted: $name"
|
||||
changed=$((changed + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if ! cmp -s "$NATIVE_OUT/manifest.json" "$THREED_DIR/manifest.json"; then
|
||||
cp "$NATIVE_OUT/manifest.json" "$THREED_DIR/manifest.json"
|
||||
echo " promoted: manifest.json"
|
||||
changed=$((changed + 1))
|
||||
fi
|
||||
|
||||
# Baselines removed from the registry linger in baseline/ — report them.
|
||||
for png in "$BASELINE_DIR"/3d-*.png; do
|
||||
[ -e "$png" ] || continue
|
||||
if [ ! -f "$NATIVE_OUT/$(basename "$png")" ]; then
|
||||
echo " STALE baseline (not in registry anymore): $(basename "$png")"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Promote done: $changed file(s) updated (byte-diff guarded, zero churn)."
|
||||
echo "Review + git add tests/3d-regression/{baseline,manifest.json}."
|
||||
}
|
||||
|
||||
case "$MODE" in
|
||||
all)
|
||||
run_native
|
||||
compare_native
|
||||
run_webgl
|
||||
compare_webgl
|
||||
;;
|
||||
native)
|
||||
run_native
|
||||
compare_native
|
||||
;;
|
||||
webgl)
|
||||
run_webgl
|
||||
compare_webgl
|
||||
;;
|
||||
compare)
|
||||
compare_native
|
||||
compare_webgl
|
||||
;;
|
||||
promote)
|
||||
promote
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [all|native|webgl|compare|promote]"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
echo ""
|
||||
if [ $NATIVE_COMPARE_STATUS -ne 0 ]; then
|
||||
echo "RESULT: FAIL (native-self comparison found changes — see tests/3d-regression/output/diff/native-self/)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ $WEBGL_COMPARE_STATUS -ne 0 ]; then
|
||||
echo "RESULT: FAIL (webgl-self comparison found changes — see tests/3d-regression/output/diff/webgl-self/)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "RESULT: PASS"
|
||||
124
tests/3d-regression/README.md
Normal 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).
|
||||
BIN
tests/3d-regression/baseline/3d-addobj-all-shapes.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
tests/3d-regression/baseline/3d-arrow-material.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
tests/3d-regression/baseline/3d-bg-gradient-alpha.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
tests/3d-regression/baseline/3d-bg-gradient.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
tests/3d-regression/baseline/3d-bounding-box.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
tests/3d-regression/baseline/3d-camera-ortho.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
tests/3d-regression/baseline/3d-camera-persp.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
tests/3d-regression/baseline/3d-camera-preset-views.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
tests/3d-regression/baseline/3d-create-board.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
tests/3d-regression/baseline/3d-gen-cylinder.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
tests/3d-regression/baseline/3d-gen-dimple.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
tests/3d-regression/baseline/3d-gen-disk.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
tests/3d-regression/baseline/3d-gen-invcone.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
tests/3d-regression/baseline/3d-grid-10mm.png
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
tests/3d-regression/baseline/3d-grid-1mm.png
Normal file
|
After Width: | Height: | Size: 653 KiB |
BIN
tests/3d-regression/baseline/3d-grid-2p5mm.png
Normal file
|
After Width: | Height: | Size: 327 KiB |
BIN
tests/3d-regression/baseline/3d-grid-5mm.png
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
tests/3d-regression/baseline/3d-half-open-cylinder.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
tests/3d-regression/baseline/3d-layer-materials.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
tests/3d-regression/baseline/3d-light-bottom.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
tests/3d-regression/baseline/3d-light-front.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
tests/3d-regression/baseline/3d-light-top.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
tests/3d-regression/baseline/3d-material-copper.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
tests/3d-regression/baseline/3d-material-diffuse-only.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
tests/3d-regression/baseline/3d-material-transparent.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
tests/3d-regression/baseline/3d-model3d-bbox.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
tests/3d-regression/baseline/3d-model3d-material-modes.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
tests/3d-regression/baseline/3d-model3d-opaque.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
tests/3d-regression/baseline/3d-model3d-transparent.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
tests/3d-regression/baseline/3d-post-machining.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
tests/3d-regression/baseline/3d-redraw-empty.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
tests/3d-regression/baseline/3d-redraw-mini-board-navigator.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
tests/3d-regression/baseline/3d-redraw-mini-board.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
tests/3d-regression/baseline/3d-round-arrow.png
Normal file
|
After Width: | Height: | Size: 29 KiB |
BIN
tests/3d-regression/baseline/3d-round-arrows-axes.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
tests/3d-regression/baseline/3d-segment-single.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
tests/3d-regression/baseline/3d-segments-star.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
tests/3d-regression/baseline/3d-spheres-gizmo.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
tests/3d-regression/baseline/3d-tdl-culled-stencil.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
tests/3d-regression/baseline/3d-tdl-draw-all.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
tests/3d-regression/baseline/3d-tdl-draw-bot.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
tests/3d-regression/baseline/3d-tdl-draw-middle.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
tests/3d-regression/baseline/3d-tdl-draw-top.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
tests/3d-regression/baseline/3d-tdl-seg-ends-texture.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
tests/3d-regression/baseline/3d-tdl-transparent.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
tests/3d-regression/baseline/3d-tdl-zscale.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
tests/3d-regression/baseline/3d-via-composite.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
14
tests/3d-regression/floors.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
53
tests/3d-regression/manifest.json
Normal 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"
|
||||
]
|
||||
}
|
||||
168
tests/3d-regression/native/CMakeLists.txt
Normal 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")
|
||||
521
tests/3d-regression/native/board_adapter_test_impl.cpp
Normal 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();
|
||||
}
|
||||
26
tests/3d-regression/native/config.h
Normal 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
|
||||
146
tests/3d-regression/native/fbo_capture.cpp
Normal 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;
|
||||
}
|
||||
46
tests/3d-regression/native/fbo_capture.h
Normal 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
|
||||
303
tests/3d-regression/native/kicad_stubs_3d.cpp
Normal 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;
|
||||
}
|
||||
15
tests/3d-regression/native/kicad_stubs_3d.h
Normal 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
|
||||
273
tests/3d-regression/native/render3d_test_accessor.cpp
Normal 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 )();
|
||||
}
|
||||
83
tests/3d-regression/native/render3d_test_accessor.h
Normal 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
|
||||
299
tests/3d-regression/native/scene3d_native_test.cpp
Normal 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 );
|
||||
}
|
||||
159
tests/3d-regression/native/settings_3d_stub.cpp
Normal 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;
|
||||
}
|
||||
1724
tests/3d-regression/native/stb_image_write.h
Normal file
238
tests/3d-regression/scenarios/scenario_tier1_model.cpp
Normal 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 );
|
||||
}
|
||||
207
tests/3d-regression/scenarios/scenario_tier1_tdl.cpp
Normal 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();
|
||||
}
|
||||
269
tests/3d-regression/scenarios/scenario_tier1_utils.cpp
Normal 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 );
|
||||
}
|
||||
336
tests/3d-regression/scenarios/scenario_tier2_generators.cpp
Normal 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();
|
||||
}
|
||||
65
tests/3d-regression/scenarios/scenario_tier3_composite.cpp
Normal 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 );
|
||||
}
|
||||
209
tests/3d-regression/scenarios/scene3d_test_ctx.cpp
Normal 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 );
|
||||
}
|
||||
96
tests/3d-regression/scenarios/scene3d_test_ctx.h
Normal 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
|
||||
44
tests/3d-regression/scenarios/scene3d_test_rig.h
Normal 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
|
||||
146
tests/3d-regression/scenarios/scene3d_test_scenarios.cpp
Normal 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
|
||||
26
tests/3d-regression/scenarios/scene3d_test_scenarios.h
Normal 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
|
||||
202
tests/3d-regression/scenarios/test_board_data.cpp
Normal 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;
|
||||
}
|
||||
35
tests/3d-regression/scenarios/test_board_data.h
Normal 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
|
||||
120
tests/3d-regression/wasm/3d_webgl_test.cpp
Normal 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;
|
||||
}
|
||||
35
tests/3d-regression/wasm/3d_webgl_test.html
Normal 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>
|
||||
194
tests/3d-regression/wasm/Makefile
Normal 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)
|
||||
35
tests/apps/3d-webgl/3d_webgl_test.html
Normal 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>
|
||||
81
tests/e2e/3d-webgl.spec.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* 3D Renderer WebGL Regression — capture-only.
|
||||
*
|
||||
* Renders every scenario of the 3D suite (tests/3d-regression) in the browser
|
||||
* and writes 3d-<name>.png into tests/3d-regression/output/webgl/. This spec
|
||||
* never compares pixels: the gates live in `npm run 3d:check:webgl`
|
||||
* (browser-regression, once baseline-webgl exists) and the informational
|
||||
* `npm run 3d:check:parity` port-progress meter (expected ~100% changed while
|
||||
* the FFP stubs render blank — the TDD red state).
|
||||
*
|
||||
* Anti-drift: no hand-typed scenario list. The committed
|
||||
* tests/3d-regression/manifest.json (written by the native golden generator,
|
||||
* cmp-guarded by scripts/test-3d-regression.sh) is the single source of truth,
|
||||
* and the wasm registry is asserted against it name-by-name.
|
||||
*/
|
||||
|
||||
import { test, expect } from './utils/fixtures';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const MANIFEST_PATH = path.join(__dirname, '../3d-regression/manifest.json');
|
||||
const OUTPUT_DIR = path.join(__dirname, '../3d-regression/output/webgl');
|
||||
const APP_JS = path.join(__dirname, '../apps/3d-webgl/3d_webgl_test.js');
|
||||
|
||||
const MANIFEST: { width: number; height: number; scenarios: string[] } = JSON.parse(
|
||||
fs.readFileSync(MANIFEST_PATH, 'utf8')
|
||||
);
|
||||
|
||||
test.describe('3D WebGL Regression', () => {
|
||||
test.skip(
|
||||
!fs.existsSync(APP_JS),
|
||||
'3d-webgl harness not built (run scripts/build-3d-webgl-test.sh)'
|
||||
);
|
||||
|
||||
test.beforeAll(async () => {
|
||||
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
||||
});
|
||||
|
||||
test('module loads and registry matches the committed manifest', async ({ page }) => {
|
||||
await page.goto('/3d-webgl/3d_webgl_test.html');
|
||||
|
||||
await page.waitForFunction(() => (window as any).threeDTest?.isReady(), undefined, {
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
const total = await page.evaluate(() => (window as any).threeDTest.getTotalScenarios());
|
||||
expect(total).toBe(MANIFEST.scenarios.length);
|
||||
|
||||
const names = await page.evaluate((count) => {
|
||||
const t = (window as any).threeDTest;
|
||||
return Array.from({ length: count }, (_, i) => t.getScenarioName(i));
|
||||
}, total);
|
||||
expect(names).toEqual(MANIFEST.scenarios);
|
||||
|
||||
const width = await page.evaluate(() => (window as any).threeDTest.getCanvasWidth());
|
||||
const height = await page.evaluate(() => (window as any).threeDTest.getCanvasHeight());
|
||||
expect(width).toBe(MANIFEST.width);
|
||||
expect(height).toBe(MANIFEST.height);
|
||||
});
|
||||
|
||||
test('render all scenarios', async ({ page }) => {
|
||||
test.setTimeout(300000);
|
||||
|
||||
await page.goto('/3d-webgl/3d_webgl_test.html');
|
||||
await page.waitForFunction(() => (window as any).threeDTest?.isReady(), undefined, {
|
||||
timeout: 60000,
|
||||
});
|
||||
|
||||
for (const [i, name] of MANIFEST.scenarios.entries()) {
|
||||
const rc = await page.evaluate((idx) => (window as any).threeDTest.runScenario(idx), i);
|
||||
expect(rc, `runScenario(${i}) [${name}]`).toBe(0);
|
||||
|
||||
// One composite tick so the preserved drawing buffer is presentable.
|
||||
await page.evaluate(() => new Promise(requestAnimationFrame));
|
||||
|
||||
await page
|
||||
.locator('#canvas')
|
||||
.screenshot({ path: path.join(OUTPUT_DIR, `3d-${name}.png`) });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -48,7 +48,12 @@
|
|||
"screenshots:noise": "tsx tools/screenshots/noise.ts",
|
||||
"screenshots:report": "tsx tools/screenshots/post-discord.ts",
|
||||
"screenshots:changelog": "tsx tools/screenshots/changelog.ts",
|
||||
"screenshots:manifest": "tsx tools/screenshots/gen-manifest.ts"
|
||||
"screenshots:manifest": "tsx tools/screenshots/gen-manifest.ts",
|
||||
"3d:compare": "tsx tools/screenshots/compare-dirs.ts",
|
||||
"3d:check": "tsx tools/screenshots/compare-dirs.ts --old 3d-regression/baseline --new 3d-regression/output/native --out 3d-regression/output/diff/native-self --floors 3d-regression/floors.json --level native-self --label 3d-native --fail-on-change",
|
||||
"3d:check:webgl": "tsx tools/screenshots/compare-dirs.ts --old 3d-regression/baseline-webgl --new 3d-regression/output/webgl --out 3d-regression/output/diff/webgl-self --floors 3d-regression/floors.json --level webgl-self --label 3d-webgl --fail-on-change",
|
||||
"3d:check:parity": "tsx tools/screenshots/compare-dirs.ts --old 3d-regression/baseline --new 3d-regression/output/webgl --out 3d-regression/output/diff/parity --floors 3d-regression/floors.json --level webgl-vs-native --label 3d-parity",
|
||||
"3d:test:webgl": "playwright test e2e/3d-webgl.spec.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.0",
|
||||
|
|
|
|||
188
tests/tools/screenshots/compare-dirs.ts
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
/**
|
||||
* Directory-pair screenshot comparison for the standalone regression suites
|
||||
* (first consumer: tests/3d-regression). Reuses the one comparison engine
|
||||
* (comparePair + image-ops) without touching the product gate in compare.ts —
|
||||
* that gate is welded to BASELINE_DIRS/RESULTS_DIR/manifest and must stay
|
||||
* byte-identical in behavior.
|
||||
*
|
||||
* Run modes (from the `tests/` directory):
|
||||
* tsx tools/screenshots/compare-dirs.ts --old <dirA> --new <dirB> --out <diffDir> \
|
||||
* [--floors <floors.json> --level <level>] # per-suite floor config
|
||||
* [--floor <changedRatio>] # ad-hoc floor (wins over --floors)
|
||||
* [--fail-on-change] # exit 1 on changed/missing/extra
|
||||
* [--label <text>] # caption suffix on triptychs
|
||||
*
|
||||
* The old dir is the reference side. A PNG present only in --old is MISSING,
|
||||
* only in --new is EXTRA; with a name-stable scenario registry both are
|
||||
* failures under --fail-on-change (an extra PNG means registry and baselines
|
||||
* diverged). Exit codes: 0 ok (or report-only), 1 fail-on-change tripped,
|
||||
* 2 usage/IO error.
|
||||
*/
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { LABEL, labelText, type EngineFloor } from './config';
|
||||
import { comparePair, type ChangedEntry } from './compare';
|
||||
import { loadPng, savePng, withBottomLabel } from './image-ops';
|
||||
|
||||
const DEFAULT_FLOOR: EngineFloor = { changedRatio: 0.005, meanChannelGuard: 2.0 };
|
||||
|
||||
type LevelFloors = { default?: EngineFloor; overrides?: Record<string, EngineFloor> };
|
||||
type FloorsFile = Record<string, LevelFloors>;
|
||||
|
||||
type DirReport = {
|
||||
old: string;
|
||||
new: string;
|
||||
level: string | null;
|
||||
changed: ChangedEntry[];
|
||||
/** present in --new only (kept as `added` to match compare.ts's Report shape) */
|
||||
added: Array<{ name: string; image: string }>;
|
||||
/** present in --old only */
|
||||
removed: Array<{ name: string; image: string }>;
|
||||
unchangedCount: number;
|
||||
};
|
||||
|
||||
function listPngs(dir: string): string[] {
|
||||
return fs
|
||||
.readdirSync(dir)
|
||||
.filter((f) => f.toLowerCase().endsWith('.png'))
|
||||
.sort();
|
||||
}
|
||||
|
||||
function parseArgs(argv: string[]): Record<string, string | boolean> {
|
||||
const out: Record<string, string | boolean> = {};
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i];
|
||||
if (a === '--old') out.old = argv[++i];
|
||||
else if (a === '--new') out.new = argv[++i];
|
||||
else if (a === '--out') out.out = argv[++i];
|
||||
else if (a === '--floors') out.floors = argv[++i];
|
||||
else if (a === '--level') out.level = argv[++i];
|
||||
else if (a === '--floor') out.floor = argv[++i];
|
||||
else if (a === '--label') out.label = argv[++i];
|
||||
else if (a === '--fail-on-change') out.failOnChange = true;
|
||||
else {
|
||||
console.error(`[compare-dirs] unknown argument: ${a}`);
|
||||
process.exit(2);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function usageError(msg: string): never {
|
||||
console.error(`[compare-dirs] ${msg}`);
|
||||
console.error(
|
||||
'[compare-dirs] usage: --old <dir> --new <dir> --out <dir> ' +
|
||||
'[--floors <json> --level <level>] [--floor <ratio>] [--fail-on-change] [--label <text>]'
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
/** Resolution: --floor (ad hoc) → floors.json per-name override → level default → built-in. */
|
||||
function makeFloorResolver(args: Record<string, string | boolean>): (name: string) => EngineFloor {
|
||||
if (args.floor !== undefined) {
|
||||
const ratio = Number(args.floor);
|
||||
if (!Number.isFinite(ratio) || ratio < 0) usageError(`--floor must be a non-negative number, got "${args.floor}"`);
|
||||
const adHoc: EngineFloor = { changedRatio: ratio, meanChannelGuard: DEFAULT_FLOOR.meanChannelGuard };
|
||||
return () => adHoc;
|
||||
}
|
||||
if (args.floors === undefined) return () => DEFAULT_FLOOR;
|
||||
if (args.level === undefined) usageError('--floors requires --level');
|
||||
if (!fs.existsSync(args.floors as string)) usageError(`floors file not found: ${args.floors}`);
|
||||
let floorsFile: FloorsFile;
|
||||
try {
|
||||
floorsFile = JSON.parse(fs.readFileSync(args.floors as string, 'utf8'));
|
||||
} catch (e) {
|
||||
usageError(`could not parse ${args.floors}: ${(e as Error).message}`);
|
||||
}
|
||||
const level = floorsFile[args.level as string];
|
||||
if (!level) usageError(`level "${args.level}" not found in ${args.floors}`);
|
||||
return (name) => level.overrides?.[name] ?? level.default ?? DEFAULT_FLOOR;
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
for (const req of ['old', 'new', 'out'] as const) {
|
||||
if (!args[req]) usageError(`--${req} is required`);
|
||||
}
|
||||
const oldDir = args.old as string;
|
||||
const newDir = args.new as string;
|
||||
const outDir = args.out as string;
|
||||
if (!fs.existsSync(oldDir)) usageError(`--old dir not found: ${oldDir}`);
|
||||
if (!fs.existsSync(newDir)) usageError(`--new dir not found: ${newDir}`);
|
||||
const label = (args.label as string) || null;
|
||||
const levelName = (args.level as string) || null;
|
||||
const floorFor = makeFloorResolver(args);
|
||||
|
||||
fs.mkdirSync(outDir, { recursive: true });
|
||||
const oldNames = listPngs(oldDir);
|
||||
const newNames = new Set(listPngs(newDir));
|
||||
|
||||
const report: DirReport = {
|
||||
old: oldDir,
|
||||
new: newDir,
|
||||
level: levelName,
|
||||
changed: [],
|
||||
added: [],
|
||||
removed: [],
|
||||
unchangedCount: 0,
|
||||
};
|
||||
|
||||
const caption = (status: 'added' | 'removed' | 'changed', name: string) => labelText(status, name, label);
|
||||
|
||||
for (const name of oldNames) {
|
||||
if (!newNames.has(name)) {
|
||||
const imagePath = path.join(outDir, `${name}.removed.png`);
|
||||
savePng(imagePath, withBottomLabel(loadPng(path.join(oldDir, name)), caption('removed', name), LABEL.colors.removed));
|
||||
report.removed.push({ name, image: imagePath });
|
||||
continue;
|
||||
}
|
||||
let pair: ReturnType<typeof comparePair>;
|
||||
try {
|
||||
pair = comparePair(
|
||||
loadPng(path.join(oldDir, name)),
|
||||
loadPng(path.join(newDir, name)),
|
||||
name,
|
||||
floorFor(name)
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(`[compare-dirs] failed to compare ${name}: ${(e as Error).message}`);
|
||||
process.exit(2);
|
||||
}
|
||||
const { result, heatmap, triptych } = pair;
|
||||
if (result.verdict === 'unchanged') {
|
||||
report.unchangedCount++;
|
||||
continue;
|
||||
}
|
||||
const triptychPath = path.join(outDir, `${name}.triptych.png`);
|
||||
const heatmapPath = path.join(outDir, `${name}.heatmap.png`);
|
||||
savePng(triptychPath, withBottomLabel(triptych, caption('changed', name), LABEL.colors.changed));
|
||||
savePng(heatmapPath, heatmap);
|
||||
report.changed.push({ ...result, triptych: triptychPath, heatmap: heatmapPath });
|
||||
}
|
||||
|
||||
for (const name of newNames) {
|
||||
if (oldNames.includes(name)) continue;
|
||||
const imagePath = path.join(outDir, `${name}.added.png`);
|
||||
savePng(imagePath, withBottomLabel(loadPng(path.join(newDir, name)), caption('added', name), LABEL.colors.added));
|
||||
report.added.push({ name, image: imagePath });
|
||||
}
|
||||
|
||||
report.changed.sort((a, b) => b.changedRatio - a.changedRatio);
|
||||
fs.writeFileSync(path.join(outDir, 'report.json'), JSON.stringify(report, null, 2));
|
||||
|
||||
console.log(
|
||||
`[compare-dirs]${levelName ? ` level=${levelName}` : ''} changed=${report.changed.length} ` +
|
||||
`missing=${report.removed.length} extra=${report.added.length} unchanged=${report.unchangedCount}`
|
||||
);
|
||||
for (const c of report.changed.slice(0, 10)) {
|
||||
console.log(` CHANGED ${c.name} ratio=${(c.changedRatio * 100).toFixed(3)}% ${c.driftHint}`);
|
||||
}
|
||||
for (const r of report.removed) console.log(` MISSING ${r.name} (in --old only)`);
|
||||
for (const a of report.added) console.log(` EXTRA ${a.name} (in --new only)`);
|
||||
|
||||
if (args.failOnChange && (report.changed.length || report.added.length || report.removed.length)) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||