diff --git a/.gitignore b/.gitignore index 8a527dc..5185bf7 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/scripts/build-3d-native-test.sh b/scripts/build-3d-native-test.sh new file mode 100755 index 0000000..b46d87c --- /dev/null +++ b/scripts/build-3d-native-test.sh @@ -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 Output directory for 3d-.png" +echo " --manifest Write the scenario manifest JSON" +echo " --filter Only run scenarios whose name contains " +echo " --list Print scenario names and exit" +echo " --show Keep the window open after rendering" diff --git a/scripts/build-3d-webgl-test.sh b/scripts/build-3d-webgl-test.sh new file mode 100755 index 0000000..7ae7e03 --- /dev/null +++ b/scripts/build-3d-webgl-test.sh @@ -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" diff --git a/scripts/test-3d-regression.sh b/scripts/test-3d-regression.sh new file mode 100755 index 0000000..e8db905 --- /dev/null +++ b/scripts/test-3d-regression.sh @@ -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" diff --git a/tests/3d-regression/README.md b/tests/3d-regression/README.md new file mode 100644 index 0000000..3755eff --- /dev/null +++ b/tests/3d-regression/README.md @@ -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-.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 | 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). diff --git a/tests/3d-regression/baseline/3d-addobj-all-shapes.png b/tests/3d-regression/baseline/3d-addobj-all-shapes.png new file mode 100644 index 0000000..5b6d8c8 Binary files /dev/null and b/tests/3d-regression/baseline/3d-addobj-all-shapes.png differ diff --git a/tests/3d-regression/baseline/3d-arrow-material.png b/tests/3d-regression/baseline/3d-arrow-material.png new file mode 100644 index 0000000..401cf61 Binary files /dev/null and b/tests/3d-regression/baseline/3d-arrow-material.png differ diff --git a/tests/3d-regression/baseline/3d-bg-gradient-alpha.png b/tests/3d-regression/baseline/3d-bg-gradient-alpha.png new file mode 100644 index 0000000..122c4e1 Binary files /dev/null and b/tests/3d-regression/baseline/3d-bg-gradient-alpha.png differ diff --git a/tests/3d-regression/baseline/3d-bg-gradient.png b/tests/3d-regression/baseline/3d-bg-gradient.png new file mode 100644 index 0000000..989923f Binary files /dev/null and b/tests/3d-regression/baseline/3d-bg-gradient.png differ diff --git a/tests/3d-regression/baseline/3d-bounding-box.png b/tests/3d-regression/baseline/3d-bounding-box.png new file mode 100644 index 0000000..79de865 Binary files /dev/null and b/tests/3d-regression/baseline/3d-bounding-box.png differ diff --git a/tests/3d-regression/baseline/3d-camera-ortho.png b/tests/3d-regression/baseline/3d-camera-ortho.png new file mode 100644 index 0000000..3dd97b5 Binary files /dev/null and b/tests/3d-regression/baseline/3d-camera-ortho.png differ diff --git a/tests/3d-regression/baseline/3d-camera-persp.png b/tests/3d-regression/baseline/3d-camera-persp.png new file mode 100644 index 0000000..585fc9a Binary files /dev/null and b/tests/3d-regression/baseline/3d-camera-persp.png differ diff --git a/tests/3d-regression/baseline/3d-camera-preset-views.png b/tests/3d-regression/baseline/3d-camera-preset-views.png new file mode 100644 index 0000000..9010d72 Binary files /dev/null and b/tests/3d-regression/baseline/3d-camera-preset-views.png differ diff --git a/tests/3d-regression/baseline/3d-create-board.png b/tests/3d-regression/baseline/3d-create-board.png new file mode 100644 index 0000000..588fe3a Binary files /dev/null and b/tests/3d-regression/baseline/3d-create-board.png differ diff --git a/tests/3d-regression/baseline/3d-gen-cylinder.png b/tests/3d-regression/baseline/3d-gen-cylinder.png new file mode 100644 index 0000000..7803d04 Binary files /dev/null and b/tests/3d-regression/baseline/3d-gen-cylinder.png differ diff --git a/tests/3d-regression/baseline/3d-gen-dimple.png b/tests/3d-regression/baseline/3d-gen-dimple.png new file mode 100644 index 0000000..182e49c Binary files /dev/null and b/tests/3d-regression/baseline/3d-gen-dimple.png differ diff --git a/tests/3d-regression/baseline/3d-gen-disk.png b/tests/3d-regression/baseline/3d-gen-disk.png new file mode 100644 index 0000000..e98c0ca Binary files /dev/null and b/tests/3d-regression/baseline/3d-gen-disk.png differ diff --git a/tests/3d-regression/baseline/3d-gen-invcone.png b/tests/3d-regression/baseline/3d-gen-invcone.png new file mode 100644 index 0000000..5a0936c Binary files /dev/null and b/tests/3d-regression/baseline/3d-gen-invcone.png differ diff --git a/tests/3d-regression/baseline/3d-grid-10mm.png b/tests/3d-regression/baseline/3d-grid-10mm.png new file mode 100644 index 0000000..87f7944 Binary files /dev/null and b/tests/3d-regression/baseline/3d-grid-10mm.png differ diff --git a/tests/3d-regression/baseline/3d-grid-1mm.png b/tests/3d-regression/baseline/3d-grid-1mm.png new file mode 100644 index 0000000..3c3e07b Binary files /dev/null and b/tests/3d-regression/baseline/3d-grid-1mm.png differ diff --git a/tests/3d-regression/baseline/3d-grid-2p5mm.png b/tests/3d-regression/baseline/3d-grid-2p5mm.png new file mode 100644 index 0000000..ba9b56b Binary files /dev/null and b/tests/3d-regression/baseline/3d-grid-2p5mm.png differ diff --git a/tests/3d-regression/baseline/3d-grid-5mm.png b/tests/3d-regression/baseline/3d-grid-5mm.png new file mode 100644 index 0000000..6f9f578 Binary files /dev/null and b/tests/3d-regression/baseline/3d-grid-5mm.png differ diff --git a/tests/3d-regression/baseline/3d-half-open-cylinder.png b/tests/3d-regression/baseline/3d-half-open-cylinder.png new file mode 100644 index 0000000..b151074 Binary files /dev/null and b/tests/3d-regression/baseline/3d-half-open-cylinder.png differ diff --git a/tests/3d-regression/baseline/3d-layer-materials.png b/tests/3d-regression/baseline/3d-layer-materials.png new file mode 100644 index 0000000..5710044 Binary files /dev/null and b/tests/3d-regression/baseline/3d-layer-materials.png differ diff --git a/tests/3d-regression/baseline/3d-light-bottom.png b/tests/3d-regression/baseline/3d-light-bottom.png new file mode 100644 index 0000000..9ee1fa0 Binary files /dev/null and b/tests/3d-regression/baseline/3d-light-bottom.png differ diff --git a/tests/3d-regression/baseline/3d-light-front.png b/tests/3d-regression/baseline/3d-light-front.png new file mode 100644 index 0000000..c7bb068 Binary files /dev/null and b/tests/3d-regression/baseline/3d-light-front.png differ diff --git a/tests/3d-regression/baseline/3d-light-top.png b/tests/3d-regression/baseline/3d-light-top.png new file mode 100644 index 0000000..b6ae66c Binary files /dev/null and b/tests/3d-regression/baseline/3d-light-top.png differ diff --git a/tests/3d-regression/baseline/3d-material-copper.png b/tests/3d-regression/baseline/3d-material-copper.png new file mode 100644 index 0000000..bc478dc Binary files /dev/null and b/tests/3d-regression/baseline/3d-material-copper.png differ diff --git a/tests/3d-regression/baseline/3d-material-diffuse-only.png b/tests/3d-regression/baseline/3d-material-diffuse-only.png new file mode 100644 index 0000000..c798eab Binary files /dev/null and b/tests/3d-regression/baseline/3d-material-diffuse-only.png differ diff --git a/tests/3d-regression/baseline/3d-material-transparent.png b/tests/3d-regression/baseline/3d-material-transparent.png new file mode 100644 index 0000000..3d3f0bb Binary files /dev/null and b/tests/3d-regression/baseline/3d-material-transparent.png differ diff --git a/tests/3d-regression/baseline/3d-model3d-bbox.png b/tests/3d-regression/baseline/3d-model3d-bbox.png new file mode 100644 index 0000000..110489b Binary files /dev/null and b/tests/3d-regression/baseline/3d-model3d-bbox.png differ diff --git a/tests/3d-regression/baseline/3d-model3d-material-modes.png b/tests/3d-regression/baseline/3d-model3d-material-modes.png new file mode 100644 index 0000000..ad2ec73 Binary files /dev/null and b/tests/3d-regression/baseline/3d-model3d-material-modes.png differ diff --git a/tests/3d-regression/baseline/3d-model3d-opaque.png b/tests/3d-regression/baseline/3d-model3d-opaque.png new file mode 100644 index 0000000..9a36b20 Binary files /dev/null and b/tests/3d-regression/baseline/3d-model3d-opaque.png differ diff --git a/tests/3d-regression/baseline/3d-model3d-transparent.png b/tests/3d-regression/baseline/3d-model3d-transparent.png new file mode 100644 index 0000000..3d2f11a Binary files /dev/null and b/tests/3d-regression/baseline/3d-model3d-transparent.png differ diff --git a/tests/3d-regression/baseline/3d-post-machining.png b/tests/3d-regression/baseline/3d-post-machining.png new file mode 100644 index 0000000..18ae549 Binary files /dev/null and b/tests/3d-regression/baseline/3d-post-machining.png differ diff --git a/tests/3d-regression/baseline/3d-redraw-empty.png b/tests/3d-regression/baseline/3d-redraw-empty.png new file mode 100644 index 0000000..989923f Binary files /dev/null and b/tests/3d-regression/baseline/3d-redraw-empty.png differ diff --git a/tests/3d-regression/baseline/3d-redraw-mini-board-navigator.png b/tests/3d-regression/baseline/3d-redraw-mini-board-navigator.png new file mode 100644 index 0000000..6108acc Binary files /dev/null and b/tests/3d-regression/baseline/3d-redraw-mini-board-navigator.png differ diff --git a/tests/3d-regression/baseline/3d-redraw-mini-board.png b/tests/3d-regression/baseline/3d-redraw-mini-board.png new file mode 100644 index 0000000..e58be9a Binary files /dev/null and b/tests/3d-regression/baseline/3d-redraw-mini-board.png differ diff --git a/tests/3d-regression/baseline/3d-round-arrow.png b/tests/3d-regression/baseline/3d-round-arrow.png new file mode 100644 index 0000000..21c9ae9 Binary files /dev/null and b/tests/3d-regression/baseline/3d-round-arrow.png differ diff --git a/tests/3d-regression/baseline/3d-round-arrows-axes.png b/tests/3d-regression/baseline/3d-round-arrows-axes.png new file mode 100644 index 0000000..9595981 Binary files /dev/null and b/tests/3d-regression/baseline/3d-round-arrows-axes.png differ diff --git a/tests/3d-regression/baseline/3d-segment-single.png b/tests/3d-regression/baseline/3d-segment-single.png new file mode 100644 index 0000000..7686e32 Binary files /dev/null and b/tests/3d-regression/baseline/3d-segment-single.png differ diff --git a/tests/3d-regression/baseline/3d-segments-star.png b/tests/3d-regression/baseline/3d-segments-star.png new file mode 100644 index 0000000..a4d464e Binary files /dev/null and b/tests/3d-regression/baseline/3d-segments-star.png differ diff --git a/tests/3d-regression/baseline/3d-spheres-gizmo.png b/tests/3d-regression/baseline/3d-spheres-gizmo.png new file mode 100644 index 0000000..d6c36be Binary files /dev/null and b/tests/3d-regression/baseline/3d-spheres-gizmo.png differ diff --git a/tests/3d-regression/baseline/3d-tdl-culled-stencil.png b/tests/3d-regression/baseline/3d-tdl-culled-stencil.png new file mode 100644 index 0000000..ae9a060 Binary files /dev/null and b/tests/3d-regression/baseline/3d-tdl-culled-stencil.png differ diff --git a/tests/3d-regression/baseline/3d-tdl-draw-all.png b/tests/3d-regression/baseline/3d-tdl-draw-all.png new file mode 100644 index 0000000..bbedb00 Binary files /dev/null and b/tests/3d-regression/baseline/3d-tdl-draw-all.png differ diff --git a/tests/3d-regression/baseline/3d-tdl-draw-bot.png b/tests/3d-regression/baseline/3d-tdl-draw-bot.png new file mode 100644 index 0000000..cd924d3 Binary files /dev/null and b/tests/3d-regression/baseline/3d-tdl-draw-bot.png differ diff --git a/tests/3d-regression/baseline/3d-tdl-draw-middle.png b/tests/3d-regression/baseline/3d-tdl-draw-middle.png new file mode 100644 index 0000000..077b927 Binary files /dev/null and b/tests/3d-regression/baseline/3d-tdl-draw-middle.png differ diff --git a/tests/3d-regression/baseline/3d-tdl-draw-top.png b/tests/3d-regression/baseline/3d-tdl-draw-top.png new file mode 100644 index 0000000..f1480f3 Binary files /dev/null and b/tests/3d-regression/baseline/3d-tdl-draw-top.png differ diff --git a/tests/3d-regression/baseline/3d-tdl-seg-ends-texture.png b/tests/3d-regression/baseline/3d-tdl-seg-ends-texture.png new file mode 100644 index 0000000..558d6b4 Binary files /dev/null and b/tests/3d-regression/baseline/3d-tdl-seg-ends-texture.png differ diff --git a/tests/3d-regression/baseline/3d-tdl-transparent.png b/tests/3d-regression/baseline/3d-tdl-transparent.png new file mode 100644 index 0000000..f274ef8 Binary files /dev/null and b/tests/3d-regression/baseline/3d-tdl-transparent.png differ diff --git a/tests/3d-regression/baseline/3d-tdl-zscale.png b/tests/3d-regression/baseline/3d-tdl-zscale.png new file mode 100644 index 0000000..2f9128d Binary files /dev/null and b/tests/3d-regression/baseline/3d-tdl-zscale.png differ diff --git a/tests/3d-regression/baseline/3d-via-composite.png b/tests/3d-regression/baseline/3d-via-composite.png new file mode 100644 index 0000000..b65d8e7 Binary files /dev/null and b/tests/3d-regression/baseline/3d-via-composite.png differ diff --git a/tests/3d-regression/floors.json b/tests/3d-regression/floors.json new file mode 100644 index 0000000..8814728 --- /dev/null +++ b/tests/3d-regression/floors.json @@ -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": {} + } +} diff --git a/tests/3d-regression/manifest.json b/tests/3d-regression/manifest.json new file mode 100644 index 0000000..09447e4 --- /dev/null +++ b/tests/3d-regression/manifest.json @@ -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" + ] +} diff --git a/tests/3d-regression/native/CMakeLists.txt b/tests/3d-regression/native/CMakeLists.txt new file mode 100644 index 0000000..e4a79e2 --- /dev/null +++ b/tests/3d-regression/native/CMakeLists.txt @@ -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 + ${KICAD_ROOT}/3d-viewer/3d_viewer # create_scene.cpp: + ${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") diff --git a/tests/3d-regression/native/board_adapter_test_impl.cpp b/tests/3d-regression/native/board_adapter_test_impl.cpp new file mode 100644 index 0000000..76e13a5 --- /dev/null +++ b/tests/3d-regression/native/board_adapter_test_impl.cpp @@ -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 +#include +#include // 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& 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 BOARD_ADAPTER::GetVisibleLayers() const +{ + std::bitset 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 BOARD_ADAPTER::GetDefaultColors() const +{ + std::map 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 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(); +} diff --git a/tests/3d-regression/native/config.h b/tests/3d-regression/native/config.h new file mode 100644 index 0000000..88a3f7a --- /dev/null +++ b/tests/3d-regression/native/config.h @@ -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 + +#endif // KICAD_CONFIG_H diff --git a/tests/3d-regression/native/fbo_capture.cpp b/tests/3d-regression/native/fbo_capture.cpp new file mode 100644 index 0000000..06e0043 --- /dev/null +++ b/tests/3d-regression/native/fbo_capture.cpp @@ -0,0 +1,146 @@ +#include "fbo_capture.h" + +#include + +// 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& aOut ) +{ + if( !m_fbo ) + return false; + + s_glBindFramebuffer( GL_FRAMEBUFFER, m_fbo ); + glFinish(); + + aOut.resize( static_cast( 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; +} diff --git a/tests/3d-regression/native/fbo_capture.h b/tests/3d-regression/native/fbo_capture.h new file mode 100644 index 0000000..ec41656 --- /dev/null +++ b/tests/3d-regression/native/fbo_capture.h @@ -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 + +#include +#include + +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& 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 diff --git a/tests/3d-regression/native/kicad_stubs_3d.cpp b/tests/3d-regression/native/kicad_stubs_3d.cpp new file mode 100644 index 0000000..f729524 --- /dev/null +++ b/tests/3d-regression/native/kicad_stubs_3d.cpp @@ -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 +#include +#include +#include + +// PGM_BASE holds unique_ptrs to these — complete types needed to define +// its ctor/dtor here. +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#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& 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 PCB_VIA::GetSecondaryDrillSize() const +{ + return std::nullopt; +} + +std::optional PCB_VIA::GetTertiaryDrillSize() const +{ + return std::nullopt; +} + +FILLING_MODE PCB_VIA::GetFillingMode() const +{ + return static_cast( 0 ); +} + +CAPPING_MODE PCB_VIA::GetCappingMode() const +{ + return static_cast( 0 ); +} + +PLUGGING_MODE PCB_VIA::GetFrontPluggingMode() const +{ + return static_cast( 0 ); +} + +PLUGGING_MODE PCB_VIA::GetBackPluggingMode() const +{ + return static_cast( 0 ); +} + +COVERING_MODE PCB_VIA::GetFrontCoveringMode() const +{ + return static_cast( 0 ); +} + +COVERING_MODE PCB_VIA::GetBackCoveringMode() const +{ + return static_cast( 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_MANAGER_ADAPTER::GetRow( const wxString&, + LIBRARY_TABLE_SCOPE ) const +{ + return std::nullopt; +} + +S3DMODEL* S3D_CACHE::GetModel( const wxString&, const wxString&, + std::vector ) +{ + return nullptr; +} diff --git a/tests/3d-regression/native/kicad_stubs_3d.h b/tests/3d-regression/native/kicad_stubs_3d.h new file mode 100644 index 0000000..4c09837 --- /dev/null +++ b/tests/3d-regression/native/kicad_stubs_3d.h @@ -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 + +#include +#include + +#endif // KICAD_STUBS_3D_H diff --git a/tests/3d-regression/native/render3d_test_accessor.cpp b/tests/3d-regression/native/render3d_test_accessor.cpp new file mode 100644 index 0000000..6274c6b --- /dev/null +++ b/tests/3d-regression/native/render3d_test_accessor.cpp @@ -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 +struct result +{ + typedef typename Tag::type type; + static type ptr; +}; + +template +typename result::type result::ptr; + +template +struct rob : result +{ + struct filler + { + filler() { result::ptr = p; } + }; + static filler filler_obj; +}; + +template +typename rob::filler rob::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; + +struct R3D_generateCylinder +{ + typedef void ( RENDER_3D_OPENGL::*type )( const SFVEC2F&, float, float, float, float, + unsigned int, TRIANGLE_DISPLAY_LIST* ); +}; +template struct rob; + +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; + +struct R3D_generateDisk +{ + typedef void ( RENDER_3D_OPENGL::*type )( const SFVEC2F&, float, float, unsigned int, + TRIANGLE_DISPLAY_LIST*, bool ); +}; +template struct rob; + +struct R3D_generateDimple +{ + typedef void ( RENDER_3D_OPENGL::*type )( const SFVEC2F&, float, float, float, unsigned int, + TRIANGLE_DISPLAY_LIST*, bool ); +}; +template struct rob; + +struct R3D_generateRing +{ + typedef void ( RENDER_3D_OPENGL::*type )( const SFVEC2F&, float, float, unsigned int, + std::vector&, std::vector&, + bool ); +}; +template struct rob; + +struct R3D_addObj_Circle +{ + typedef void ( RENDER_3D_OPENGL::*type )( const FILLED_CIRCLE_2D*, TRIANGLE_DISPLAY_LIST*, + float, float ); +}; +template struct rob; + +struct R3D_addObj_Ring +{ + typedef void ( RENDER_3D_OPENGL::*type )( const RING_2D*, TRIANGLE_DISPLAY_LIST*, float, + float ); +}; +template struct rob; + +struct R3D_addObj_Poly4 +{ + typedef void ( RENDER_3D_OPENGL::*type )( const POLYGON_4PT_2D*, TRIANGLE_DISPLAY_LIST*, + float, float ); +}; +template struct rob; + +struct R3D_addObj_Tri +{ + typedef void ( RENDER_3D_OPENGL::*type )( const TRIANGLE_2D*, TRIANGLE_DISPLAY_LIST*, float, + float ); +}; +template struct rob; + +struct R3D_addObj_Seg +{ + typedef void ( RENDER_3D_OPENGL::*type )( const ROUND_SEGMENT_2D*, TRIANGLE_DISPLAY_LIST*, + float, float ); +}; +template struct rob; + +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; + +struct R3D_createBoard +{ + typedef OPENGL_RENDER_LIST* ( RENDER_3D_OPENGL::*type )( const SHAPE_POLY_SET&, + const BVH_CONTAINER_2D* ); +}; +template struct rob; + +struct R3D_generate3dGrid +{ + typedef void ( RENDER_3D_OPENGL::*type )( GRID3D_TYPE ); +}; +template struct rob; + +struct R3D_setupMaterials +{ + typedef void ( RENDER_3D_OPENGL::*type )(); +}; +template struct rob; + +struct R3D_setLayerMaterial +{ + typedef void ( RENDER_3D_OPENGL::*type )( PCB_LAYER_ID ); +}; +template struct rob; + +struct R3D_setArrowMaterial +{ + typedef void ( RENDER_3D_OPENGL::*type )(); +}; +template struct rob; + +struct R3D_m_grid +{ + typedef GLuint RENDER_3D_OPENGL::*type; +}; +template struct rob; + +// ---- public wrappers ---- + +bool R3D_InitializeOpenGL( RENDER_3D_OPENGL& aRenderer ) +{ + return ( aRenderer.*result::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::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::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::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::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& aInnerContour, std::vector& aOuterContour, + bool aInvertOrder ) +{ + ( aRenderer.*result::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::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::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::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::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::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::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::ptr )( aBoardPoly, aThroughHoles ); +} + +void R3D_Generate3dGrid( RENDER_3D_OPENGL& aRenderer, GRID3D_TYPE aGridType ) +{ + ( aRenderer.*result::ptr )( aGridType ); +} + +unsigned int R3D_GetGridList( RENDER_3D_OPENGL& aRenderer ) +{ + return aRenderer.*result::ptr; +} + +void R3D_SetupMaterials( RENDER_3D_OPENGL& aRenderer ) +{ + ( aRenderer.*result::ptr )(); +} + +void R3D_SetLayerMaterial( RENDER_3D_OPENGL& aRenderer, PCB_LAYER_ID aLayerID ) +{ + ( aRenderer.*result::ptr )( aLayerID ); +} + +void R3D_SetArrowMaterial( RENDER_3D_OPENGL& aRenderer ) +{ + ( aRenderer.*result::ptr )(); +} diff --git a/tests/3d-regression/native/render3d_test_accessor.h b/tests/3d-regression/native/render3d_test_accessor.h new file mode 100644 index 0000000..b6ead08 --- /dev/null +++ b/tests/3d-regression/native/render3d_test_accessor.h @@ -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 + +#include <3d_enums.h> +#include +#include +#include // PAD_DRILL_POST_MACHINING_MODE + +#include + +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& aInnerContour, std::vector& 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 diff --git a/tests/3d-regression/native/scene3d_native_test.cpp b/tests/3d-regression/native/scene3d_native_test.cpp new file mode 100644 index 0000000..3d30ae9 --- /dev/null +++ b/tests/3d-regression/native/scene3d_native_test.cpp @@ -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 +#include +#include +#include +#include +#include + +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& 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 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( 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 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 [options]\n" + " --output Output directory for 3d-.png\n" + " --manifest Write the scenario manifest JSON\n" + " --filter Only run scenarios whose name contains \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 ); +} diff --git a/tests/3d-regression/native/settings_3d_stub.cpp b/tests/3d-regression/native/settings_3d_stub.cpp new file mode 100644 index 0000000..4735782 --- /dev/null +++ b/tests/3d-regression/native/settings_3d_stub.cpp @@ -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 + +// ---- 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::~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 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; +} diff --git a/tests/3d-regression/native/stb_image_write.h b/tests/3d-regression/native/stb_image_write.h new file mode 100644 index 0000000..e4b32ed --- /dev/null +++ b/tests/3d-regression/native/stb_image_write.h @@ -0,0 +1,1724 @@ +/* stb_image_write - v1.16 - public domain - http://nothings.org/stb + writes out PNG/BMP/TGA/JPEG/HDR images to C stdio - Sean Barrett 2010-2015 + no warranty implied; use at your own risk + + Before #including, + + #define STB_IMAGE_WRITE_IMPLEMENTATION + + in the file that you want to have the implementation. + + Will probably not work correctly with strict-aliasing optimizations. + +ABOUT: + + This header file is a library for writing images to C stdio or a callback. + + The PNG output is not optimal; it is 20-50% larger than the file + written by a decent optimizing implementation; though providing a custom + zlib compress function (see STBIW_ZLIB_COMPRESS) can mitigate that. + This library is designed for source code compactness and simplicity, + not optimal image file size or run-time performance. + +BUILDING: + + You can #define STBIW_ASSERT(x) before the #include to avoid using assert.h. + You can #define STBIW_MALLOC(), STBIW_REALLOC(), and STBIW_FREE() to replace + malloc,realloc,free. + You can #define STBIW_MEMMOVE() to replace memmove() + You can #define STBIW_ZLIB_COMPRESS to use a custom zlib-style compress function + for PNG compression (instead of the builtin one), it must have the following signature: + unsigned char * my_compress(unsigned char *data, int data_len, int *out_len, int quality); + The returned data will be freed with STBIW_FREE() (free() by default), + so it must be heap allocated with STBIW_MALLOC() (malloc() by default), + +UNICODE: + + If compiling for Windows and you wish to use Unicode filenames, compile + with + #define STBIW_WINDOWS_UTF8 + and pass utf8-encoded filenames. Call stbiw_convert_wchar_to_utf8 to convert + Windows wchar_t filenames to utf8. + +USAGE: + + There are five functions, one for each image file format: + + int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); + int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data); + int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data); + int stbi_write_jpg(char const *filename, int w, int h, int comp, const void *data, int quality); + int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data); + + void stbi_flip_vertically_on_write(int flag); // flag is non-zero to flip data vertically + + There are also five equivalent functions that use an arbitrary write function. You are + expected to open/close your file-equivalent before and after calling these: + + int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes); + int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); + int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); + int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data); + int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality); + + where the callback is: + void stbi_write_func(void *context, void *data, int size); + + You can configure it with these global variables: + int stbi_write_tga_with_rle; // defaults to true; set to 0 to disable RLE + int stbi_write_png_compression_level; // defaults to 8; set to higher for more compression + int stbi_write_force_png_filter; // defaults to -1; set to 0..5 to force a filter mode + + + You can define STBI_WRITE_NO_STDIO to disable the file variant of these + functions, so the library will not use stdio.h at all. However, this will + also disable HDR writing, because it requires stdio for formatted output. + + Each function returns 0 on failure and non-0 on success. + + The functions create an image file defined by the parameters. The image + is a rectangle of pixels stored from left-to-right, top-to-bottom. + Each pixel contains 'comp' channels of data stored interleaved with 8-bits + per channel, in the following order: 1=Y, 2=YA, 3=RGB, 4=RGBA. (Y is + monochrome color.) The rectangle is 'w' pixels wide and 'h' pixels tall. + The *data pointer points to the first byte of the top-left-most pixel. + For PNG, "stride_in_bytes" is the distance in bytes from the first byte of + a row of pixels to the first byte of the next row of pixels. + + PNG creates output files with the same number of components as the input. + The BMP format expands Y to RGB in the file format and does not + output alpha. + + PNG supports writing rectangles of data even when the bytes storing rows of + data are not consecutive in memory (e.g. sub-rectangles of a larger image), + by supplying the stride between the beginning of adjacent rows. The other + formats do not. (Thus you cannot write a native-format BMP through the BMP + writer, both because it is in BGR order and because it may have padding + at the end of the line.) + + PNG allows you to set the deflate compression level by setting the global + variable 'stbi_write_png_compression_level' (it defaults to 8). + + HDR expects linear float data. Since the format is always 32-bit rgb(e) + data, alpha (if provided) is discarded, and for monochrome data it is + replicated across all three channels. + + TGA supports RLE or non-RLE compressed data. To use non-RLE-compressed + data, set the global variable 'stbi_write_tga_with_rle' to 0. + + JPEG does ignore alpha channels in input data; quality is between 1 and 100. + Higher quality looks better but results in a bigger image. + JPEG baseline (no JPEG progressive). + +CREDITS: + + + Sean Barrett - PNG/BMP/TGA + Baldur Karlsson - HDR + Jean-Sebastien Guay - TGA monochrome + Tim Kelsey - misc enhancements + Alan Hickman - TGA RLE + Emmanuel Julien - initial file IO callback implementation + Jon Olick - original jo_jpeg.cpp code + Daniel Gibson - integrate JPEG, allow external zlib + Aarni Koskela - allow choosing PNG filter + + bugfixes: + github:Chribba + Guillaume Chereau + github:jry2 + github:romigrou + Sergio Gonzalez + Jonas Karlsson + Filip Wasil + Thatcher Ulrich + github:poppolopoppo + Patrick Boettcher + github:xeekworx + Cap Petschulat + Simon Rodriguez + Ivan Tikhonov + github:ignotion + Adam Schackart + Andrew Kensler + +LICENSE + + See end of file for license information. + +*/ + +#ifndef INCLUDE_STB_IMAGE_WRITE_H +#define INCLUDE_STB_IMAGE_WRITE_H + +#include + +// if STB_IMAGE_WRITE_STATIC causes problems, try defining STBIWDEF to 'inline' or 'static inline' +#ifndef STBIWDEF +#ifdef STB_IMAGE_WRITE_STATIC +#define STBIWDEF static +#else +#ifdef __cplusplus +#define STBIWDEF extern "C" +#else +#define STBIWDEF extern +#endif +#endif +#endif + +#ifndef STB_IMAGE_WRITE_STATIC // C++ forbids static forward declarations +STBIWDEF int stbi_write_tga_with_rle; +STBIWDEF int stbi_write_png_compression_level; +STBIWDEF int stbi_write_force_png_filter; +#endif + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_png(char const *filename, int w, int h, int comp, const void *data, int stride_in_bytes); +STBIWDEF int stbi_write_bmp(char const *filename, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_tga(char const *filename, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_hdr(char const *filename, int w, int h, int comp, const float *data); +STBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void *data, int quality); + +#ifdef STBIW_WINDOWS_UTF8 +STBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input); +#endif +#endif + +typedef void stbi_write_func(void *context, void *data, int size); + +STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data, int stride_in_bytes); +STBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const void *data); +STBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int w, int h, int comp, const float *data); +STBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality); + +STBIWDEF void stbi_flip_vertically_on_write(int flip_boolean); + +#endif//INCLUDE_STB_IMAGE_WRITE_H + +#ifdef STB_IMAGE_WRITE_IMPLEMENTATION + +#ifdef _WIN32 + #ifndef _CRT_SECURE_NO_WARNINGS + #define _CRT_SECURE_NO_WARNINGS + #endif + #ifndef _CRT_NONSTDC_NO_DEPRECATE + #define _CRT_NONSTDC_NO_DEPRECATE + #endif +#endif + +#ifndef STBI_WRITE_NO_STDIO +#include +#endif // STBI_WRITE_NO_STDIO + +#include +#include +#include +#include + +#if defined(STBIW_MALLOC) && defined(STBIW_FREE) && (defined(STBIW_REALLOC) || defined(STBIW_REALLOC_SIZED)) +// ok +#elif !defined(STBIW_MALLOC) && !defined(STBIW_FREE) && !defined(STBIW_REALLOC) && !defined(STBIW_REALLOC_SIZED) +// ok +#else +#error "Must define all or none of STBIW_MALLOC, STBIW_FREE, and STBIW_REALLOC (or STBIW_REALLOC_SIZED)." +#endif + +#ifndef STBIW_MALLOC +#define STBIW_MALLOC(sz) malloc(sz) +#define STBIW_REALLOC(p,newsz) realloc(p,newsz) +#define STBIW_FREE(p) free(p) +#endif + +#ifndef STBIW_REALLOC_SIZED +#define STBIW_REALLOC_SIZED(p,oldsz,newsz) STBIW_REALLOC(p,newsz) +#endif + + +#ifndef STBIW_MEMMOVE +#define STBIW_MEMMOVE(a,b,sz) memmove(a,b,sz) +#endif + + +#ifndef STBIW_ASSERT +#include +#define STBIW_ASSERT(x) assert(x) +#endif + +#define STBIW_UCHAR(x) (unsigned char) ((x) & 0xff) + +#ifdef STB_IMAGE_WRITE_STATIC +static int stbi_write_png_compression_level = 8; +static int stbi_write_tga_with_rle = 1; +static int stbi_write_force_png_filter = -1; +#else +int stbi_write_png_compression_level = 8; +int stbi_write_tga_with_rle = 1; +int stbi_write_force_png_filter = -1; +#endif + +static int stbi__flip_vertically_on_write = 0; + +STBIWDEF void stbi_flip_vertically_on_write(int flag) +{ + stbi__flip_vertically_on_write = flag; +} + +typedef struct +{ + stbi_write_func *func; + void *context; + unsigned char buffer[64]; + int buf_used; +} stbi__write_context; + +// initialize a callback-based context +static void stbi__start_write_callbacks(stbi__write_context *s, stbi_write_func *c, void *context) +{ + s->func = c; + s->context = context; +} + +#ifndef STBI_WRITE_NO_STDIO + +static void stbi__stdio_write(void *context, void *data, int size) +{ + fwrite(data,1,size,(FILE*) context); +} + +#if defined(_WIN32) && defined(STBIW_WINDOWS_UTF8) +#ifdef __cplusplus +#define STBIW_EXTERN extern "C" +#else +#define STBIW_EXTERN extern +#endif +STBIW_EXTERN __declspec(dllimport) int __stdcall MultiByteToWideChar(unsigned int cp, unsigned long flags, const char *str, int cbmb, wchar_t *widestr, int cchwide); +STBIW_EXTERN __declspec(dllimport) int __stdcall WideCharToMultiByte(unsigned int cp, unsigned long flags, const wchar_t *widestr, int cchwide, char *str, int cbmb, const char *defchar, int *used_default); + +STBIWDEF int stbiw_convert_wchar_to_utf8(char *buffer, size_t bufferlen, const wchar_t* input) +{ + return WideCharToMultiByte(65001 /* UTF8 */, 0, input, -1, buffer, (int) bufferlen, NULL, NULL); +} +#endif + +static FILE *stbiw__fopen(char const *filename, char const *mode) +{ + FILE *f; +#if defined(_WIN32) && defined(STBIW_WINDOWS_UTF8) + wchar_t wMode[64]; + wchar_t wFilename[1024]; + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, filename, -1, wFilename, sizeof(wFilename)/sizeof(*wFilename))) + return 0; + + if (0 == MultiByteToWideChar(65001 /* UTF8 */, 0, mode, -1, wMode, sizeof(wMode)/sizeof(*wMode))) + return 0; + +#if defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != _wfopen_s(&f, wFilename, wMode)) + f = 0; +#else + f = _wfopen(wFilename, wMode); +#endif + +#elif defined(_MSC_VER) && _MSC_VER >= 1400 + if (0 != fopen_s(&f, filename, mode)) + f=0; +#else + f = fopen(filename, mode); +#endif + return f; +} + +static int stbi__start_write_file(stbi__write_context *s, const char *filename) +{ + FILE *f = stbiw__fopen(filename, "wb"); + stbi__start_write_callbacks(s, stbi__stdio_write, (void *) f); + return f != NULL; +} + +static void stbi__end_write_file(stbi__write_context *s) +{ + fclose((FILE *)s->context); +} + +#endif // !STBI_WRITE_NO_STDIO + +typedef unsigned int stbiw_uint32; +typedef int stb_image_write_test[sizeof(stbiw_uint32)==4 ? 1 : -1]; + +static void stbiw__writefv(stbi__write_context *s, const char *fmt, va_list v) +{ + while (*fmt) { + switch (*fmt++) { + case ' ': break; + case '1': { unsigned char x = STBIW_UCHAR(va_arg(v, int)); + s->func(s->context,&x,1); + break; } + case '2': { int x = va_arg(v,int); + unsigned char b[2]; + b[0] = STBIW_UCHAR(x); + b[1] = STBIW_UCHAR(x>>8); + s->func(s->context,b,2); + break; } + case '4': { stbiw_uint32 x = va_arg(v,int); + unsigned char b[4]; + b[0]=STBIW_UCHAR(x); + b[1]=STBIW_UCHAR(x>>8); + b[2]=STBIW_UCHAR(x>>16); + b[3]=STBIW_UCHAR(x>>24); + s->func(s->context,b,4); + break; } + default: + STBIW_ASSERT(0); + return; + } + } +} + +static void stbiw__writef(stbi__write_context *s, const char *fmt, ...) +{ + va_list v; + va_start(v, fmt); + stbiw__writefv(s, fmt, v); + va_end(v); +} + +static void stbiw__write_flush(stbi__write_context *s) +{ + if (s->buf_used) { + s->func(s->context, &s->buffer, s->buf_used); + s->buf_used = 0; + } +} + +static void stbiw__putc(stbi__write_context *s, unsigned char c) +{ + s->func(s->context, &c, 1); +} + +static void stbiw__write1(stbi__write_context *s, unsigned char a) +{ + if ((size_t)s->buf_used + 1 > sizeof(s->buffer)) + stbiw__write_flush(s); + s->buffer[s->buf_used++] = a; +} + +static void stbiw__write3(stbi__write_context *s, unsigned char a, unsigned char b, unsigned char c) +{ + int n; + if ((size_t)s->buf_used + 3 > sizeof(s->buffer)) + stbiw__write_flush(s); + n = s->buf_used; + s->buf_used = n+3; + s->buffer[n+0] = a; + s->buffer[n+1] = b; + s->buffer[n+2] = c; +} + +static void stbiw__write_pixel(stbi__write_context *s, int rgb_dir, int comp, int write_alpha, int expand_mono, unsigned char *d) +{ + unsigned char bg[3] = { 255, 0, 255}, px[3]; + int k; + + if (write_alpha < 0) + stbiw__write1(s, d[comp - 1]); + + switch (comp) { + case 2: // 2 pixels = mono + alpha, alpha is written separately, so same as 1-channel case + case 1: + if (expand_mono) + stbiw__write3(s, d[0], d[0], d[0]); // monochrome bmp + else + stbiw__write1(s, d[0]); // monochrome TGA + break; + case 4: + if (!write_alpha) { + // composite against pink background + for (k = 0; k < 3; ++k) + px[k] = bg[k] + ((d[k] - bg[k]) * d[3]) / 255; + stbiw__write3(s, px[1 - rgb_dir], px[1], px[1 + rgb_dir]); + break; + } + /* FALLTHROUGH */ + case 3: + stbiw__write3(s, d[1 - rgb_dir], d[1], d[1 + rgb_dir]); + break; + } + if (write_alpha > 0) + stbiw__write1(s, d[comp - 1]); +} + +static void stbiw__write_pixels(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, void *data, int write_alpha, int scanline_pad, int expand_mono) +{ + stbiw_uint32 zero = 0; + int i,j, j_end; + + if (y <= 0) + return; + + if (stbi__flip_vertically_on_write) + vdir *= -1; + + if (vdir < 0) { + j_end = -1; j = y-1; + } else { + j_end = y; j = 0; + } + + for (; j != j_end; j += vdir) { + for (i=0; i < x; ++i) { + unsigned char *d = (unsigned char *) data + (j*x+i)*comp; + stbiw__write_pixel(s, rgb_dir, comp, write_alpha, expand_mono, d); + } + stbiw__write_flush(s); + s->func(s->context, &zero, scanline_pad); + } +} + +static int stbiw__outfile(stbi__write_context *s, int rgb_dir, int vdir, int x, int y, int comp, int expand_mono, void *data, int alpha, int pad, const char *fmt, ...) +{ + if (y < 0 || x < 0) { + return 0; + } else { + va_list v; + va_start(v, fmt); + stbiw__writefv(s, fmt, v); + va_end(v); + stbiw__write_pixels(s,rgb_dir,vdir,x,y,comp,data,alpha,pad, expand_mono); + return 1; + } +} + +static int stbi_write_bmp_core(stbi__write_context *s, int x, int y, int comp, const void *data) +{ + if (comp != 4) { + // write RGB bitmap + int pad = (-x*3) & 3; + return stbiw__outfile(s,-1,-1,x,y,comp,1,(void *) data,0,pad, + "11 4 22 4" "4 44 22 444444", + 'B', 'M', 14+40+(x*3+pad)*y, 0,0, 14+40, // file header + 40, x,y, 1,24, 0,0,0,0,0,0); // bitmap header + } else { + // RGBA bitmaps need a v4 header + // use BI_BITFIELDS mode with 32bpp and alpha mask + // (straight BI_RGB with alpha mask doesn't work in most readers) + return stbiw__outfile(s,-1,-1,x,y,comp,1,(void *)data,1,0, + "11 4 22 4" "4 44 22 444444 4444 4 444 444 444 444", + 'B', 'M', 14+108+x*y*4, 0, 0, 14+108, // file header + 108, x,y, 1,32, 3,0,0,0,0,0, 0xff0000,0xff00,0xff,0xff000000u, 0, 0,0,0, 0,0,0, 0,0,0, 0,0,0); // bitmap V4 header + } +} + +STBIWDEF int stbi_write_bmp_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_bmp_core(&s, x, y, comp, data); +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_bmp(char const *filename, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_bmp_core(&s, x, y, comp, data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif //!STBI_WRITE_NO_STDIO + +static int stbi_write_tga_core(stbi__write_context *s, int x, int y, int comp, void *data) +{ + int has_alpha = (comp == 2 || comp == 4); + int colorbytes = has_alpha ? comp-1 : comp; + int format = colorbytes < 2 ? 3 : 2; // 3 color channels (RGB/RGBA) = 2, 1 color channel (Y/YA) = 3 + + if (y < 0 || x < 0) + return 0; + + if (!stbi_write_tga_with_rle) { + return stbiw__outfile(s, -1, -1, x, y, comp, 0, (void *) data, has_alpha, 0, + "111 221 2222 11", 0, 0, format, 0, 0, 0, 0, 0, x, y, (colorbytes + has_alpha) * 8, has_alpha * 8); + } else { + int i,j,k; + int jend, jdir; + + stbiw__writef(s, "111 221 2222 11", 0,0,format+8, 0,0,0, 0,0,x,y, (colorbytes + has_alpha) * 8, has_alpha * 8); + + if (stbi__flip_vertically_on_write) { + j = 0; + jend = y; + jdir = 1; + } else { + j = y-1; + jend = -1; + jdir = -1; + } + for (; j != jend; j += jdir) { + unsigned char *row = (unsigned char *) data + j * x * comp; + int len; + + for (i = 0; i < x; i += len) { + unsigned char *begin = row + i * comp; + int diff = 1; + len = 1; + + if (i < x - 1) { + ++len; + diff = memcmp(begin, row + (i + 1) * comp, comp); + if (diff) { + const unsigned char *prev = begin; + for (k = i + 2; k < x && len < 128; ++k) { + if (memcmp(prev, row + k * comp, comp)) { + prev += comp; + ++len; + } else { + --len; + break; + } + } + } else { + for (k = i + 2; k < x && len < 128; ++k) { + if (!memcmp(begin, row + k * comp, comp)) { + ++len; + } else { + break; + } + } + } + } + + if (diff) { + unsigned char header = STBIW_UCHAR(len - 1); + stbiw__write1(s, header); + for (k = 0; k < len; ++k) { + stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin + k * comp); + } + } else { + unsigned char header = STBIW_UCHAR(len - 129); + stbiw__write1(s, header); + stbiw__write_pixel(s, -1, comp, has_alpha, 0, begin); + } + } + } + stbiw__write_flush(s); + } + return 1; +} + +STBIWDEF int stbi_write_tga_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_tga_core(&s, x, y, comp, (void *) data); +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_tga(char const *filename, int x, int y, int comp, const void *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_tga_core(&s, x, y, comp, (void *) data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif + +// ************************************************************************************************* +// Radiance RGBE HDR writer +// by Baldur Karlsson + +#define stbiw__max(a, b) ((a) > (b) ? (a) : (b)) + +#ifndef STBI_WRITE_NO_STDIO + +static void stbiw__linear_to_rgbe(unsigned char *rgbe, float *linear) +{ + int exponent; + float maxcomp = stbiw__max(linear[0], stbiw__max(linear[1], linear[2])); + + if (maxcomp < 1e-32f) { + rgbe[0] = rgbe[1] = rgbe[2] = rgbe[3] = 0; + } else { + float normalize = (float) frexp(maxcomp, &exponent) * 256.0f/maxcomp; + + rgbe[0] = (unsigned char)(linear[0] * normalize); + rgbe[1] = (unsigned char)(linear[1] * normalize); + rgbe[2] = (unsigned char)(linear[2] * normalize); + rgbe[3] = (unsigned char)(exponent + 128); + } +} + +static void stbiw__write_run_data(stbi__write_context *s, int length, unsigned char databyte) +{ + unsigned char lengthbyte = STBIW_UCHAR(length+128); + STBIW_ASSERT(length+128 <= 255); + s->func(s->context, &lengthbyte, 1); + s->func(s->context, &databyte, 1); +} + +static void stbiw__write_dump_data(stbi__write_context *s, int length, unsigned char *data) +{ + unsigned char lengthbyte = STBIW_UCHAR(length); + STBIW_ASSERT(length <= 128); // inconsistent with spec but consistent with official code + s->func(s->context, &lengthbyte, 1); + s->func(s->context, data, length); +} + +static void stbiw__write_hdr_scanline(stbi__write_context *s, int width, int ncomp, unsigned char *scratch, float *scanline) +{ + unsigned char scanlineheader[4] = { 2, 2, 0, 0 }; + unsigned char rgbe[4]; + float linear[3]; + int x; + + scanlineheader[2] = (width&0xff00)>>8; + scanlineheader[3] = (width&0x00ff); + + /* skip RLE for images too small or large */ + if (width < 8 || width >= 32768) { + for (x=0; x < width; x++) { + switch (ncomp) { + case 4: /* fallthrough */ + case 3: linear[2] = scanline[x*ncomp + 2]; + linear[1] = scanline[x*ncomp + 1]; + linear[0] = scanline[x*ncomp + 0]; + break; + default: + linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0]; + break; + } + stbiw__linear_to_rgbe(rgbe, linear); + s->func(s->context, rgbe, 4); + } + } else { + int c,r; + /* encode into scratch buffer */ + for (x=0; x < width; x++) { + switch(ncomp) { + case 4: /* fallthrough */ + case 3: linear[2] = scanline[x*ncomp + 2]; + linear[1] = scanline[x*ncomp + 1]; + linear[0] = scanline[x*ncomp + 0]; + break; + default: + linear[0] = linear[1] = linear[2] = scanline[x*ncomp + 0]; + break; + } + stbiw__linear_to_rgbe(rgbe, linear); + scratch[x + width*0] = rgbe[0]; + scratch[x + width*1] = rgbe[1]; + scratch[x + width*2] = rgbe[2]; + scratch[x + width*3] = rgbe[3]; + } + + s->func(s->context, scanlineheader, 4); + + /* RLE each component separately */ + for (c=0; c < 4; c++) { + unsigned char *comp = &scratch[width*c]; + + x = 0; + while (x < width) { + // find first run + r = x; + while (r+2 < width) { + if (comp[r] == comp[r+1] && comp[r] == comp[r+2]) + break; + ++r; + } + if (r+2 >= width) + r = width; + // dump up to first run + while (x < r) { + int len = r-x; + if (len > 128) len = 128; + stbiw__write_dump_data(s, len, &comp[x]); + x += len; + } + // if there's a run, output it + if (r+2 < width) { // same test as what we break out of in search loop, so only true if we break'd + // find next byte after run + while (r < width && comp[r] == comp[x]) + ++r; + // output run up to r + while (x < r) { + int len = r-x; + if (len > 127) len = 127; + stbiw__write_run_data(s, len, comp[x]); + x += len; + } + } + } + } + } +} + +static int stbi_write_hdr_core(stbi__write_context *s, int x, int y, int comp, float *data) +{ + if (y <= 0 || x <= 0 || data == NULL) + return 0; + else { + // Each component is stored separately. Allocate scratch space for full output scanline. + unsigned char *scratch = (unsigned char *) STBIW_MALLOC(x*4); + int i, len; + char buffer[128]; + char header[] = "#?RADIANCE\n# Written by stb_image_write.h\nFORMAT=32-bit_rle_rgbe\n"; + s->func(s->context, header, sizeof(header)-1); + +#ifdef __STDC_LIB_EXT1__ + len = sprintf_s(buffer, sizeof(buffer), "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); +#else + len = sprintf(buffer, "EXPOSURE= 1.0000000000000\n\n-Y %d +X %d\n", y, x); +#endif + s->func(s->context, buffer, len); + + for(i=0; i < y; i++) + stbiw__write_hdr_scanline(s, x, comp, scratch, data + comp*x*(stbi__flip_vertically_on_write ? y-1-i : i)); + STBIW_FREE(scratch); + return 1; + } +} + +STBIWDEF int stbi_write_hdr_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const float *data) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_hdr_core(&s, x, y, comp, (float *) data); +} + +STBIWDEF int stbi_write_hdr(char const *filename, int x, int y, int comp, const float *data) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_hdr_core(&s, x, y, comp, (float *) data); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif // STBI_WRITE_NO_STDIO + + +////////////////////////////////////////////////////////////////////////////// +// +// PNG writer +// + +#ifndef STBIW_ZLIB_COMPRESS +// stretchy buffer; stbiw__sbpush() == vector<>::push_back() -- stbiw__sbcount() == vector<>::size() +#define stbiw__sbraw(a) ((int *) (void *) (a) - 2) +#define stbiw__sbm(a) stbiw__sbraw(a)[0] +#define stbiw__sbn(a) stbiw__sbraw(a)[1] + +#define stbiw__sbneedgrow(a,n) ((a)==0 || stbiw__sbn(a)+n >= stbiw__sbm(a)) +#define stbiw__sbmaybegrow(a,n) (stbiw__sbneedgrow(a,(n)) ? stbiw__sbgrow(a,n) : 0) +#define stbiw__sbgrow(a,n) stbiw__sbgrowf((void **) &(a), (n), sizeof(*(a))) + +#define stbiw__sbpush(a, v) (stbiw__sbmaybegrow(a,1), (a)[stbiw__sbn(a)++] = (v)) +#define stbiw__sbcount(a) ((a) ? stbiw__sbn(a) : 0) +#define stbiw__sbfree(a) ((a) ? STBIW_FREE(stbiw__sbraw(a)),0 : 0) + +static void *stbiw__sbgrowf(void **arr, int increment, int itemsize) +{ + int m = *arr ? 2*stbiw__sbm(*arr)+increment : increment+1; + void *p = STBIW_REALLOC_SIZED(*arr ? stbiw__sbraw(*arr) : 0, *arr ? (stbiw__sbm(*arr)*itemsize + sizeof(int)*2) : 0, itemsize * m + sizeof(int)*2); + STBIW_ASSERT(p); + if (p) { + if (!*arr) ((int *) p)[1] = 0; + *arr = (void *) ((int *) p + 2); + stbiw__sbm(*arr) = m; + } + return *arr; +} + +static unsigned char *stbiw__zlib_flushf(unsigned char *data, unsigned int *bitbuffer, int *bitcount) +{ + while (*bitcount >= 8) { + stbiw__sbpush(data, STBIW_UCHAR(*bitbuffer)); + *bitbuffer >>= 8; + *bitcount -= 8; + } + return data; +} + +static int stbiw__zlib_bitrev(int code, int codebits) +{ + int res=0; + while (codebits--) { + res = (res << 1) | (code & 1); + code >>= 1; + } + return res; +} + +static unsigned int stbiw__zlib_countm(unsigned char *a, unsigned char *b, int limit) +{ + int i; + for (i=0; i < limit && i < 258; ++i) + if (a[i] != b[i]) break; + return i; +} + +static unsigned int stbiw__zhash(unsigned char *data) +{ + stbiw_uint32 hash = data[0] + (data[1] << 8) + (data[2] << 16); + hash ^= hash << 3; + hash += hash >> 5; + hash ^= hash << 4; + hash += hash >> 17; + hash ^= hash << 25; + hash += hash >> 6; + return hash; +} + +#define stbiw__zlib_flush() (out = stbiw__zlib_flushf(out, &bitbuf, &bitcount)) +#define stbiw__zlib_add(code,codebits) \ + (bitbuf |= (code) << bitcount, bitcount += (codebits), stbiw__zlib_flush()) +#define stbiw__zlib_huffa(b,c) stbiw__zlib_add(stbiw__zlib_bitrev(b,c),c) +// default huffman tables +#define stbiw__zlib_huff1(n) stbiw__zlib_huffa(0x30 + (n), 8) +#define stbiw__zlib_huff2(n) stbiw__zlib_huffa(0x190 + (n)-144, 9) +#define stbiw__zlib_huff3(n) stbiw__zlib_huffa(0 + (n)-256,7) +#define stbiw__zlib_huff4(n) stbiw__zlib_huffa(0xc0 + (n)-280,8) +#define stbiw__zlib_huff(n) ((n) <= 143 ? stbiw__zlib_huff1(n) : (n) <= 255 ? stbiw__zlib_huff2(n) : (n) <= 279 ? stbiw__zlib_huff3(n) : stbiw__zlib_huff4(n)) +#define stbiw__zlib_huffb(n) ((n) <= 143 ? stbiw__zlib_huff1(n) : stbiw__zlib_huff2(n)) + +#define stbiw__ZHASH 16384 + +#endif // STBIW_ZLIB_COMPRESS + +STBIWDEF unsigned char * stbi_zlib_compress(unsigned char *data, int data_len, int *out_len, int quality) +{ +#ifdef STBIW_ZLIB_COMPRESS + // user provided a zlib compress implementation, use that + return STBIW_ZLIB_COMPRESS(data, data_len, out_len, quality); +#else // use builtin + static unsigned short lengthc[] = { 3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258, 259 }; + static unsigned char lengtheb[]= { 0,0,0,0,0,0,0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0 }; + static unsigned short distc[] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577, 32768 }; + static unsigned char disteb[] = { 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13 }; + unsigned int bitbuf=0; + int i,j, bitcount=0; + unsigned char *out = NULL; + unsigned char ***hash_table = (unsigned char***) STBIW_MALLOC(stbiw__ZHASH * sizeof(unsigned char**)); + if (hash_table == NULL) + return NULL; + if (quality < 5) quality = 5; + + stbiw__sbpush(out, 0x78); // DEFLATE 32K window + stbiw__sbpush(out, 0x5e); // FLEVEL = 1 + stbiw__zlib_add(1,1); // BFINAL = 1 + stbiw__zlib_add(1,2); // BTYPE = 1 -- fixed huffman + + for (i=0; i < stbiw__ZHASH; ++i) + hash_table[i] = NULL; + + i=0; + while (i < data_len-3) { + // hash next 3 bytes of data to be compressed + int h = stbiw__zhash(data+i)&(stbiw__ZHASH-1), best=3; + unsigned char *bestloc = 0; + unsigned char **hlist = hash_table[h]; + int n = stbiw__sbcount(hlist); + for (j=0; j < n; ++j) { + if (hlist[j]-data > i-32768) { // if entry lies within window + int d = stbiw__zlib_countm(hlist[j], data+i, data_len-i); + if (d >= best) { best=d; bestloc=hlist[j]; } + } + } + // when hash table entry is too long, delete half the entries + if (hash_table[h] && stbiw__sbn(hash_table[h]) == 2*quality) { + STBIW_MEMMOVE(hash_table[h], hash_table[h]+quality, sizeof(hash_table[h][0])*quality); + stbiw__sbn(hash_table[h]) = quality; + } + stbiw__sbpush(hash_table[h],data+i); + + if (bestloc) { + // "lazy matching" - check match at *next* byte, and if it's better, do cur byte as literal + h = stbiw__zhash(data+i+1)&(stbiw__ZHASH-1); + hlist = hash_table[h]; + n = stbiw__sbcount(hlist); + for (j=0; j < n; ++j) { + if (hlist[j]-data > i-32767) { + int e = stbiw__zlib_countm(hlist[j], data+i+1, data_len-i-1); + if (e > best) { // if next match is better, bail on current match + bestloc = NULL; + break; + } + } + } + } + + if (bestloc) { + int d = (int) (data+i - bestloc); // distance back + STBIW_ASSERT(d <= 32767 && best <= 258); + for (j=0; best > lengthc[j+1]-1; ++j); + stbiw__zlib_huff(j+257); + if (lengtheb[j]) stbiw__zlib_add(best - lengthc[j], lengtheb[j]); + for (j=0; d > distc[j+1]-1; ++j); + stbiw__zlib_add(stbiw__zlib_bitrev(j,5),5); + if (disteb[j]) stbiw__zlib_add(d - distc[j], disteb[j]); + i += best; + } else { + stbiw__zlib_huffb(data[i]); + ++i; + } + } + // write out final bytes + for (;i < data_len; ++i) + stbiw__zlib_huffb(data[i]); + stbiw__zlib_huff(256); // end of block + // pad with 0 bits to byte boundary + while (bitcount) + stbiw__zlib_add(0,1); + + for (i=0; i < stbiw__ZHASH; ++i) + (void) stbiw__sbfree(hash_table[i]); + STBIW_FREE(hash_table); + + // store uncompressed instead if compression was worse + if (stbiw__sbn(out) > data_len + 2 + ((data_len+32766)/32767)*5) { + stbiw__sbn(out) = 2; // truncate to DEFLATE 32K window and FLEVEL = 1 + for (j = 0; j < data_len;) { + int blocklen = data_len - j; + if (blocklen > 32767) blocklen = 32767; + stbiw__sbpush(out, data_len - j == blocklen); // BFINAL = ?, BTYPE = 0 -- no compression + stbiw__sbpush(out, STBIW_UCHAR(blocklen)); // LEN + stbiw__sbpush(out, STBIW_UCHAR(blocklen >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(~blocklen)); // NLEN + stbiw__sbpush(out, STBIW_UCHAR(~blocklen >> 8)); + memcpy(out+stbiw__sbn(out), data+j, blocklen); + stbiw__sbn(out) += blocklen; + j += blocklen; + } + } + + { + // compute adler32 on input + unsigned int s1=1, s2=0; + int blocklen = (int) (data_len % 5552); + j=0; + while (j < data_len) { + for (i=0; i < blocklen; ++i) { s1 += data[j+i]; s2 += s1; } + s1 %= 65521; s2 %= 65521; + j += blocklen; + blocklen = 5552; + } + stbiw__sbpush(out, STBIW_UCHAR(s2 >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(s2)); + stbiw__sbpush(out, STBIW_UCHAR(s1 >> 8)); + stbiw__sbpush(out, STBIW_UCHAR(s1)); + } + *out_len = stbiw__sbn(out); + // make returned pointer freeable + STBIW_MEMMOVE(stbiw__sbraw(out), out, *out_len); + return (unsigned char *) stbiw__sbraw(out); +#endif // STBIW_ZLIB_COMPRESS +} + +static unsigned int stbiw__crc32(unsigned char *buffer, int len) +{ +#ifdef STBIW_CRC32 + return STBIW_CRC32(buffer, len); +#else + static unsigned int crc_table[256] = + { + 0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3, + 0x0eDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91, + 0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7, + 0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5, + 0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B, + 0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59, + 0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F, + 0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D, + 0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433, + 0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01, + 0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457, + 0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65, + 0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB, + 0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9, + 0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F, + 0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD, + 0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683, + 0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1, + 0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7, + 0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5, + 0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B, + 0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79, + 0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F, + 0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D, + 0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713, + 0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21, + 0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777, + 0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45, + 0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB, + 0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9, + 0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF, + 0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D + }; + + unsigned int crc = ~0u; + int i; + for (i=0; i < len; ++i) + crc = (crc >> 8) ^ crc_table[buffer[i] ^ (crc & 0xff)]; + return ~crc; +#endif +} + +#define stbiw__wpng4(o,a,b,c,d) ((o)[0]=STBIW_UCHAR(a),(o)[1]=STBIW_UCHAR(b),(o)[2]=STBIW_UCHAR(c),(o)[3]=STBIW_UCHAR(d),(o)+=4) +#define stbiw__wp32(data,v) stbiw__wpng4(data, (v)>>24,(v)>>16,(v)>>8,(v)); +#define stbiw__wptag(data,s) stbiw__wpng4(data, s[0],s[1],s[2],s[3]) + +static void stbiw__wpcrc(unsigned char **data, int len) +{ + unsigned int crc = stbiw__crc32(*data - len - 4, len+4); + stbiw__wp32(*data, crc); +} + +static unsigned char stbiw__paeth(int a, int b, int c) +{ + int p = a + b - c, pa = abs(p-a), pb = abs(p-b), pc = abs(p-c); + if (pa <= pb && pa <= pc) return STBIW_UCHAR(a); + if (pb <= pc) return STBIW_UCHAR(b); + return STBIW_UCHAR(c); +} + +// @OPTIMIZE: provide an option that always forces left-predict or paeth predict +static void stbiw__encode_png_line(unsigned char *pixels, int stride_bytes, int width, int height, int y, int n, int filter_type, signed char *line_buffer) +{ + static int mapping[] = { 0,1,2,3,4 }; + static int firstmap[] = { 0,1,0,5,6 }; + int *mymap = (y != 0) ? mapping : firstmap; + int i; + int type = mymap[filter_type]; + unsigned char *z = pixels + stride_bytes * (stbi__flip_vertically_on_write ? height-1-y : y); + int signed_stride = stbi__flip_vertically_on_write ? -stride_bytes : stride_bytes; + + if (type==0) { + memcpy(line_buffer, z, width*n); + return; + } + + // first loop isn't optimized since it's just one pixel + for (i = 0; i < n; ++i) { + switch (type) { + case 1: line_buffer[i] = z[i]; break; + case 2: line_buffer[i] = z[i] - z[i-signed_stride]; break; + case 3: line_buffer[i] = z[i] - (z[i-signed_stride]>>1); break; + case 4: line_buffer[i] = (signed char) (z[i] - stbiw__paeth(0,z[i-signed_stride],0)); break; + case 5: line_buffer[i] = z[i]; break; + case 6: line_buffer[i] = z[i]; break; + } + } + switch (type) { + case 1: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-n]; break; + case 2: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - z[i-signed_stride]; break; + case 3: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - ((z[i-n] + z[i-signed_stride])>>1); break; + case 4: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], z[i-signed_stride], z[i-signed_stride-n]); break; + case 5: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - (z[i-n]>>1); break; + case 6: for (i=n; i < width*n; ++i) line_buffer[i] = z[i] - stbiw__paeth(z[i-n], 0,0); break; + } +} + +STBIWDEF unsigned char *stbi_write_png_to_mem(const unsigned char *pixels, int stride_bytes, int x, int y, int n, int *out_len) +{ + int force_filter = stbi_write_force_png_filter; + int ctype[5] = { -1, 0, 4, 2, 6 }; + unsigned char sig[8] = { 137,80,78,71,13,10,26,10 }; + unsigned char *out,*o, *filt, *zlib; + signed char *line_buffer; + int j,zlen; + + if (stride_bytes == 0) + stride_bytes = x * n; + + if (force_filter >= 5) { + force_filter = -1; + } + + filt = (unsigned char *) STBIW_MALLOC((x*n+1) * y); if (!filt) return 0; + line_buffer = (signed char *) STBIW_MALLOC(x * n); if (!line_buffer) { STBIW_FREE(filt); return 0; } + for (j=0; j < y; ++j) { + int filter_type; + if (force_filter > -1) { + filter_type = force_filter; + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, force_filter, line_buffer); + } else { // Estimate the best filter by running through all of them: + int best_filter = 0, best_filter_val = 0x7fffffff, est, i; + for (filter_type = 0; filter_type < 5; filter_type++) { + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, filter_type, line_buffer); + + // Estimate the entropy of the line using this filter; the less, the better. + est = 0; + for (i = 0; i < x*n; ++i) { + est += abs((signed char) line_buffer[i]); + } + if (est < best_filter_val) { + best_filter_val = est; + best_filter = filter_type; + } + } + if (filter_type != best_filter) { // If the last iteration already got us the best filter, don't redo it + stbiw__encode_png_line((unsigned char*)(pixels), stride_bytes, x, y, j, n, best_filter, line_buffer); + filter_type = best_filter; + } + } + // when we get here, filter_type contains the filter type, and line_buffer contains the data + filt[j*(x*n+1)] = (unsigned char) filter_type; + STBIW_MEMMOVE(filt+j*(x*n+1)+1, line_buffer, x*n); + } + STBIW_FREE(line_buffer); + zlib = stbi_zlib_compress(filt, y*( x*n+1), &zlen, stbi_write_png_compression_level); + STBIW_FREE(filt); + if (!zlib) return 0; + + // each tag requires 12 bytes of overhead + out = (unsigned char *) STBIW_MALLOC(8 + 12+13 + 12+zlen + 12); + if (!out) return 0; + *out_len = 8 + 12+13 + 12+zlen + 12; + + o=out; + STBIW_MEMMOVE(o,sig,8); o+= 8; + stbiw__wp32(o, 13); // header length + stbiw__wptag(o, "IHDR"); + stbiw__wp32(o, x); + stbiw__wp32(o, y); + *o++ = 8; + *o++ = STBIW_UCHAR(ctype[n]); + *o++ = 0; + *o++ = 0; + *o++ = 0; + stbiw__wpcrc(&o,13); + + stbiw__wp32(o, zlen); + stbiw__wptag(o, "IDAT"); + STBIW_MEMMOVE(o, zlib, zlen); + o += zlen; + STBIW_FREE(zlib); + stbiw__wpcrc(&o, zlen); + + stbiw__wp32(o,0); + stbiw__wptag(o, "IEND"); + stbiw__wpcrc(&o,0); + + STBIW_ASSERT(o == out + *out_len); + + return out; +} + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_png(char const *filename, int x, int y, int comp, const void *data, int stride_bytes) +{ + FILE *f; + int len; + unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len); + if (png == NULL) return 0; + + f = stbiw__fopen(filename, "wb"); + if (!f) { STBIW_FREE(png); return 0; } + fwrite(png, 1, len, f); + fclose(f); + STBIW_FREE(png); + return 1; +} +#endif + +STBIWDEF int stbi_write_png_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int stride_bytes) +{ + int len; + unsigned char *png = stbi_write_png_to_mem((const unsigned char *) data, stride_bytes, x, y, comp, &len); + if (png == NULL) return 0; + func(context, png, len); + STBIW_FREE(png); + return 1; +} + + +/* *************************************************************************** + * + * JPEG writer + * + * This is based on Jon Olick's jo_jpeg.cpp: + * public domain Simple, Minimalistic JPEG writer - http://www.jonolick.com/code.html + */ + +static const unsigned char stbiw__jpg_ZigZag[] = { 0,1,5,6,14,15,27,28,2,4,7,13,16,26,29,42,3,8,12,17,25,30,41,43,9,11,18, + 24,31,40,44,53,10,19,23,32,39,45,52,54,20,22,33,38,46,51,55,60,21,34,37,47,50,56,59,61,35,36,48,49,57,58,62,63 }; + +static void stbiw__jpg_writeBits(stbi__write_context *s, int *bitBufP, int *bitCntP, const unsigned short *bs) { + int bitBuf = *bitBufP, bitCnt = *bitCntP; + bitCnt += bs[1]; + bitBuf |= bs[0] << (24 - bitCnt); + while(bitCnt >= 8) { + unsigned char c = (bitBuf >> 16) & 255; + stbiw__putc(s, c); + if(c == 255) { + stbiw__putc(s, 0); + } + bitBuf <<= 8; + bitCnt -= 8; + } + *bitBufP = bitBuf; + *bitCntP = bitCnt; +} + +static void stbiw__jpg_DCT(float *d0p, float *d1p, float *d2p, float *d3p, float *d4p, float *d5p, float *d6p, float *d7p) { + float d0 = *d0p, d1 = *d1p, d2 = *d2p, d3 = *d3p, d4 = *d4p, d5 = *d5p, d6 = *d6p, d7 = *d7p; + float z1, z2, z3, z4, z5, z11, z13; + + float tmp0 = d0 + d7; + float tmp7 = d0 - d7; + float tmp1 = d1 + d6; + float tmp6 = d1 - d6; + float tmp2 = d2 + d5; + float tmp5 = d2 - d5; + float tmp3 = d3 + d4; + float tmp4 = d3 - d4; + + // Even part + float tmp10 = tmp0 + tmp3; // phase 2 + float tmp13 = tmp0 - tmp3; + float tmp11 = tmp1 + tmp2; + float tmp12 = tmp1 - tmp2; + + d0 = tmp10 + tmp11; // phase 3 + d4 = tmp10 - tmp11; + + z1 = (tmp12 + tmp13) * 0.707106781f; // c4 + d2 = tmp13 + z1; // phase 5 + d6 = tmp13 - z1; + + // Odd part + tmp10 = tmp4 + tmp5; // phase 2 + tmp11 = tmp5 + tmp6; + tmp12 = tmp6 + tmp7; + + // The rotator is modified from fig 4-8 to avoid extra negations. + z5 = (tmp10 - tmp12) * 0.382683433f; // c6 + z2 = tmp10 * 0.541196100f + z5; // c2-c6 + z4 = tmp12 * 1.306562965f + z5; // c2+c6 + z3 = tmp11 * 0.707106781f; // c4 + + z11 = tmp7 + z3; // phase 5 + z13 = tmp7 - z3; + + *d5p = z13 + z2; // phase 6 + *d3p = z13 - z2; + *d1p = z11 + z4; + *d7p = z11 - z4; + + *d0p = d0; *d2p = d2; *d4p = d4; *d6p = d6; +} + +static void stbiw__jpg_calcBits(int val, unsigned short bits[2]) { + int tmp1 = val < 0 ? -val : val; + val = val < 0 ? val-1 : val; + bits[1] = 1; + while(tmp1 >>= 1) { + ++bits[1]; + } + bits[0] = val & ((1<0)&&(DU[end0pos]==0); --end0pos) { + } + // end0pos = first element in reverse order !=0 + if(end0pos == 0) { + stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB); + return DU[0]; + } + for(i = 1; i <= end0pos; ++i) { + int startpos = i; + int nrzeroes; + unsigned short bits[2]; + for (; DU[i]==0 && i<=end0pos; ++i) { + } + nrzeroes = i-startpos; + if ( nrzeroes >= 16 ) { + int lng = nrzeroes>>4; + int nrmarker; + for (nrmarker=1; nrmarker <= lng; ++nrmarker) + stbiw__jpg_writeBits(s, bitBuf, bitCnt, M16zeroes); + nrzeroes &= 15; + } + stbiw__jpg_calcBits(DU[i], bits); + stbiw__jpg_writeBits(s, bitBuf, bitCnt, HTAC[(nrzeroes<<4)+bits[1]]); + stbiw__jpg_writeBits(s, bitBuf, bitCnt, bits); + } + if(end0pos != 63) { + stbiw__jpg_writeBits(s, bitBuf, bitCnt, EOB); + } + return DU[0]; +} + +static int stbi_write_jpg_core(stbi__write_context *s, int width, int height, int comp, const void* data, int quality) { + // Constants that don't pollute global namespace + static const unsigned char std_dc_luminance_nrcodes[] = {0,0,1,5,1,1,1,1,1,1,0,0,0,0,0,0,0}; + static const unsigned char std_dc_luminance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11}; + static const unsigned char std_ac_luminance_nrcodes[] = {0,0,2,1,3,3,2,4,3,5,5,4,4,0,0,1,0x7d}; + static const unsigned char std_ac_luminance_values[] = { + 0x01,0x02,0x03,0x00,0x04,0x11,0x05,0x12,0x21,0x31,0x41,0x06,0x13,0x51,0x61,0x07,0x22,0x71,0x14,0x32,0x81,0x91,0xa1,0x08, + 0x23,0x42,0xb1,0xc1,0x15,0x52,0xd1,0xf0,0x24,0x33,0x62,0x72,0x82,0x09,0x0a,0x16,0x17,0x18,0x19,0x1a,0x25,0x26,0x27,0x28, + 0x29,0x2a,0x34,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58,0x59, + 0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x83,0x84,0x85,0x86,0x87,0x88,0x89, + 0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4,0xb5,0xb6, + 0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda,0xe1,0xe2, + 0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf1,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa + }; + static const unsigned char std_dc_chrominance_nrcodes[] = {0,0,3,1,1,1,1,1,1,1,1,1,0,0,0,0,0}; + static const unsigned char std_dc_chrominance_values[] = {0,1,2,3,4,5,6,7,8,9,10,11}; + static const unsigned char std_ac_chrominance_nrcodes[] = {0,0,2,1,2,4,4,3,4,7,5,4,4,0,1,2,0x77}; + static const unsigned char std_ac_chrominance_values[] = { + 0x00,0x01,0x02,0x03,0x11,0x04,0x05,0x21,0x31,0x06,0x12,0x41,0x51,0x07,0x61,0x71,0x13,0x22,0x32,0x81,0x08,0x14,0x42,0x91, + 0xa1,0xb1,0xc1,0x09,0x23,0x33,0x52,0xf0,0x15,0x62,0x72,0xd1,0x0a,0x16,0x24,0x34,0xe1,0x25,0xf1,0x17,0x18,0x19,0x1a,0x26, + 0x27,0x28,0x29,0x2a,0x35,0x36,0x37,0x38,0x39,0x3a,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x53,0x54,0x55,0x56,0x57,0x58, + 0x59,0x5a,0x63,0x64,0x65,0x66,0x67,0x68,0x69,0x6a,0x73,0x74,0x75,0x76,0x77,0x78,0x79,0x7a,0x82,0x83,0x84,0x85,0x86,0x87, + 0x88,0x89,0x8a,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0xa2,0xa3,0xa4,0xa5,0xa6,0xa7,0xa8,0xa9,0xaa,0xb2,0xb3,0xb4, + 0xb5,0xb6,0xb7,0xb8,0xb9,0xba,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7,0xc8,0xc9,0xca,0xd2,0xd3,0xd4,0xd5,0xd6,0xd7,0xd8,0xd9,0xda, + 0xe2,0xe3,0xe4,0xe5,0xe6,0xe7,0xe8,0xe9,0xea,0xf2,0xf3,0xf4,0xf5,0xf6,0xf7,0xf8,0xf9,0xfa + }; + // Huffman tables + static const unsigned short YDC_HT[256][2] = { {0,2},{2,3},{3,3},{4,3},{5,3},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9}}; + static const unsigned short UVDC_HT[256][2] = { {0,2},{1,2},{2,2},{6,3},{14,4},{30,5},{62,6},{126,7},{254,8},{510,9},{1022,10},{2046,11}}; + static const unsigned short YAC_HT[256][2] = { + {10,4},{0,2},{1,2},{4,3},{11,4},{26,5},{120,7},{248,8},{1014,10},{65410,16},{65411,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {12,4},{27,5},{121,7},{502,9},{2038,11},{65412,16},{65413,16},{65414,16},{65415,16},{65416,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {28,5},{249,8},{1015,10},{4084,12},{65417,16},{65418,16},{65419,16},{65420,16},{65421,16},{65422,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {58,6},{503,9},{4085,12},{65423,16},{65424,16},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {59,6},{1016,10},{65430,16},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {122,7},{2039,11},{65438,16},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {123,7},{4086,12},{65446,16},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {250,8},{4087,12},{65454,16},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {504,9},{32704,15},{65462,16},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {505,9},{65470,16},{65471,16},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {506,9},{65479,16},{65480,16},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1017,10},{65488,16},{65489,16},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1018,10},{65497,16},{65498,16},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2040,11},{65506,16},{65507,16},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {65515,16},{65516,16},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2041,11},{65525,16},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0} + }; + static const unsigned short UVAC_HT[256][2] = { + {0,2},{1,2},{4,3},{10,4},{24,5},{25,5},{56,6},{120,7},{500,9},{1014,10},{4084,12},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {11,4},{57,6},{246,8},{501,9},{2038,11},{4085,12},{65416,16},{65417,16},{65418,16},{65419,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {26,5},{247,8},{1015,10},{4086,12},{32706,15},{65420,16},{65421,16},{65422,16},{65423,16},{65424,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {27,5},{248,8},{1016,10},{4087,12},{65425,16},{65426,16},{65427,16},{65428,16},{65429,16},{65430,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {58,6},{502,9},{65431,16},{65432,16},{65433,16},{65434,16},{65435,16},{65436,16},{65437,16},{65438,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {59,6},{1017,10},{65439,16},{65440,16},{65441,16},{65442,16},{65443,16},{65444,16},{65445,16},{65446,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {121,7},{2039,11},{65447,16},{65448,16},{65449,16},{65450,16},{65451,16},{65452,16},{65453,16},{65454,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {122,7},{2040,11},{65455,16},{65456,16},{65457,16},{65458,16},{65459,16},{65460,16},{65461,16},{65462,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {249,8},{65463,16},{65464,16},{65465,16},{65466,16},{65467,16},{65468,16},{65469,16},{65470,16},{65471,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {503,9},{65472,16},{65473,16},{65474,16},{65475,16},{65476,16},{65477,16},{65478,16},{65479,16},{65480,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {504,9},{65481,16},{65482,16},{65483,16},{65484,16},{65485,16},{65486,16},{65487,16},{65488,16},{65489,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {505,9},{65490,16},{65491,16},{65492,16},{65493,16},{65494,16},{65495,16},{65496,16},{65497,16},{65498,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {506,9},{65499,16},{65500,16},{65501,16},{65502,16},{65503,16},{65504,16},{65505,16},{65506,16},{65507,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {2041,11},{65508,16},{65509,16},{65510,16},{65511,16},{65512,16},{65513,16},{65514,16},{65515,16},{65516,16},{0,0},{0,0},{0,0},{0,0},{0,0},{0,0}, + {16352,14},{65517,16},{65518,16},{65519,16},{65520,16},{65521,16},{65522,16},{65523,16},{65524,16},{65525,16},{0,0},{0,0},{0,0},{0,0},{0,0}, + {1018,10},{32707,15},{65526,16},{65527,16},{65528,16},{65529,16},{65530,16},{65531,16},{65532,16},{65533,16},{65534,16},{0,0},{0,0},{0,0},{0,0},{0,0} + }; + static const int YQT[] = {16,11,10,16,24,40,51,61,12,12,14,19,26,58,60,55,14,13,16,24,40,57,69,56,14,17,22,29,51,87,80,62,18,22, + 37,56,68,109,103,77,24,35,55,64,81,104,113,92,49,64,78,87,103,121,120,101,72,92,95,98,112,100,103,99}; + static const int UVQT[] = {17,18,24,47,99,99,99,99,18,21,26,66,99,99,99,99,24,26,56,99,99,99,99,99,47,66,99,99,99,99,99,99, + 99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99}; + static const float aasf[] = { 1.0f * 2.828427125f, 1.387039845f * 2.828427125f, 1.306562965f * 2.828427125f, 1.175875602f * 2.828427125f, + 1.0f * 2.828427125f, 0.785694958f * 2.828427125f, 0.541196100f * 2.828427125f, 0.275899379f * 2.828427125f }; + + int row, col, i, k, subsample; + float fdtbl_Y[64], fdtbl_UV[64]; + unsigned char YTable[64], UVTable[64]; + + if(!data || !width || !height || comp > 4 || comp < 1) { + return 0; + } + + quality = quality ? quality : 90; + subsample = quality <= 90 ? 1 : 0; + quality = quality < 1 ? 1 : quality > 100 ? 100 : quality; + quality = quality < 50 ? 5000 / quality : 200 - quality * 2; + + for(i = 0; i < 64; ++i) { + int uvti, yti = (YQT[i]*quality+50)/100; + YTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (yti < 1 ? 1 : yti > 255 ? 255 : yti); + uvti = (UVQT[i]*quality+50)/100; + UVTable[stbiw__jpg_ZigZag[i]] = (unsigned char) (uvti < 1 ? 1 : uvti > 255 ? 255 : uvti); + } + + for(row = 0, k = 0; row < 8; ++row) { + for(col = 0; col < 8; ++col, ++k) { + fdtbl_Y[k] = 1 / (YTable [stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]); + fdtbl_UV[k] = 1 / (UVTable[stbiw__jpg_ZigZag[k]] * aasf[row] * aasf[col]); + } + } + + // Write Headers + { + static const unsigned char head0[] = { 0xFF,0xD8,0xFF,0xE0,0,0x10,'J','F','I','F',0,1,1,0,0,1,0,1,0,0,0xFF,0xDB,0,0x84,0 }; + static const unsigned char head2[] = { 0xFF,0xDA,0,0xC,3,1,0,2,0x11,3,0x11,0,0x3F,0 }; + const unsigned char head1[] = { 0xFF,0xC0,0,0x11,8,(unsigned char)(height>>8),STBIW_UCHAR(height),(unsigned char)(width>>8),STBIW_UCHAR(width), + 3,1,(unsigned char)(subsample?0x22:0x11),0,2,0x11,1,3,0x11,1,0xFF,0xC4,0x01,0xA2,0 }; + s->func(s->context, (void*)head0, sizeof(head0)); + s->func(s->context, (void*)YTable, sizeof(YTable)); + stbiw__putc(s, 1); + s->func(s->context, UVTable, sizeof(UVTable)); + s->func(s->context, (void*)head1, sizeof(head1)); + s->func(s->context, (void*)(std_dc_luminance_nrcodes+1), sizeof(std_dc_luminance_nrcodes)-1); + s->func(s->context, (void*)std_dc_luminance_values, sizeof(std_dc_luminance_values)); + stbiw__putc(s, 0x10); // HTYACinfo + s->func(s->context, (void*)(std_ac_luminance_nrcodes+1), sizeof(std_ac_luminance_nrcodes)-1); + s->func(s->context, (void*)std_ac_luminance_values, sizeof(std_ac_luminance_values)); + stbiw__putc(s, 1); // HTUDCinfo + s->func(s->context, (void*)(std_dc_chrominance_nrcodes+1), sizeof(std_dc_chrominance_nrcodes)-1); + s->func(s->context, (void*)std_dc_chrominance_values, sizeof(std_dc_chrominance_values)); + stbiw__putc(s, 0x11); // HTUACinfo + s->func(s->context, (void*)(std_ac_chrominance_nrcodes+1), sizeof(std_ac_chrominance_nrcodes)-1); + s->func(s->context, (void*)std_ac_chrominance_values, sizeof(std_ac_chrominance_values)); + s->func(s->context, (void*)head2, sizeof(head2)); + } + + // Encode 8x8 macroblocks + { + static const unsigned short fillBits[] = {0x7F, 7}; + int DCY=0, DCU=0, DCV=0; + int bitBuf=0, bitCnt=0; + // comp == 2 is grey+alpha (alpha is ignored) + int ofsG = comp > 2 ? 1 : 0, ofsB = comp > 2 ? 2 : 0; + const unsigned char *dataR = (const unsigned char *)data; + const unsigned char *dataG = dataR + ofsG; + const unsigned char *dataB = dataR + ofsB; + int x, y, pos; + if(subsample) { + for(y = 0; y < height; y += 16) { + for(x = 0; x < width; x += 16) { + float Y[256], U[256], V[256]; + for(row = y, pos = 0; row < y+16; ++row) { + // row >= height => use last input row + int clamped_row = (row < height) ? row : height - 1; + int base_p = (stbi__flip_vertically_on_write ? (height-1-clamped_row) : clamped_row)*width*comp; + for(col = x; col < x+16; ++col, ++pos) { + // if col >= width => use pixel from last input column + int p = base_p + ((col < width) ? col : (width-1))*comp; + float r = dataR[p], g = dataG[p], b = dataB[p]; + Y[pos]= +0.29900f*r + 0.58700f*g + 0.11400f*b - 128; + U[pos]= -0.16874f*r - 0.33126f*g + 0.50000f*b; + V[pos]= +0.50000f*r - 0.41869f*g - 0.08131f*b; + } + } + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+0, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+8, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+128, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y+136, 16, fdtbl_Y, DCY, YDC_HT, YAC_HT); + + // subsample U,V + { + float subU[64], subV[64]; + int yy, xx; + for(yy = 0, pos = 0; yy < 8; ++yy) { + for(xx = 0; xx < 8; ++xx, ++pos) { + int j = yy*32+xx*2; + subU[pos] = (U[j+0] + U[j+1] + U[j+16] + U[j+17]) * 0.25f; + subV[pos] = (V[j+0] + V[j+1] + V[j+16] + V[j+17]) * 0.25f; + } + } + DCU = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, subU, 8, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, subV, 8, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + } + } + } + } else { + for(y = 0; y < height; y += 8) { + for(x = 0; x < width; x += 8) { + float Y[64], U[64], V[64]; + for(row = y, pos = 0; row < y+8; ++row) { + // row >= height => use last input row + int clamped_row = (row < height) ? row : height - 1; + int base_p = (stbi__flip_vertically_on_write ? (height-1-clamped_row) : clamped_row)*width*comp; + for(col = x; col < x+8; ++col, ++pos) { + // if col >= width => use pixel from last input column + int p = base_p + ((col < width) ? col : (width-1))*comp; + float r = dataR[p], g = dataG[p], b = dataB[p]; + Y[pos]= +0.29900f*r + 0.58700f*g + 0.11400f*b - 128; + U[pos]= -0.16874f*r - 0.33126f*g + 0.50000f*b; + V[pos]= +0.50000f*r - 0.41869f*g - 0.08131f*b; + } + } + + DCY = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, Y, 8, fdtbl_Y, DCY, YDC_HT, YAC_HT); + DCU = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, U, 8, fdtbl_UV, DCU, UVDC_HT, UVAC_HT); + DCV = stbiw__jpg_processDU(s, &bitBuf, &bitCnt, V, 8, fdtbl_UV, DCV, UVDC_HT, UVAC_HT); + } + } + } + + // Do the bit alignment of the EOI marker + stbiw__jpg_writeBits(s, &bitBuf, &bitCnt, fillBits); + } + + // EOI + stbiw__putc(s, 0xFF); + stbiw__putc(s, 0xD9); + + return 1; +} + +STBIWDEF int stbi_write_jpg_to_func(stbi_write_func *func, void *context, int x, int y, int comp, const void *data, int quality) +{ + stbi__write_context s = { 0 }; + stbi__start_write_callbacks(&s, func, context); + return stbi_write_jpg_core(&s, x, y, comp, (void *) data, quality); +} + + +#ifndef STBI_WRITE_NO_STDIO +STBIWDEF int stbi_write_jpg(char const *filename, int x, int y, int comp, const void *data, int quality) +{ + stbi__write_context s = { 0 }; + if (stbi__start_write_file(&s,filename)) { + int r = stbi_write_jpg_core(&s, x, y, comp, data, quality); + stbi__end_write_file(&s); + return r; + } else + return 0; +} +#endif + +#endif // STB_IMAGE_WRITE_IMPLEMENTATION + +/* Revision history + 1.16 (2021-07-11) + make Deflate code emit uncompressed blocks when it would otherwise expand + support writing BMPs with alpha channel + 1.15 (2020-07-13) unknown + 1.14 (2020-02-02) updated JPEG writer to downsample chroma channels + 1.13 + 1.12 + 1.11 (2019-08-11) + + 1.10 (2019-02-07) + support utf8 filenames in Windows; fix warnings and platform ifdefs + 1.09 (2018-02-11) + fix typo in zlib quality API, improve STB_I_W_STATIC in C++ + 1.08 (2018-01-29) + add stbi__flip_vertically_on_write, external zlib, zlib quality, choose PNG filter + 1.07 (2017-07-24) + doc fix + 1.06 (2017-07-23) + writing JPEG (using Jon Olick's code) + 1.05 ??? + 1.04 (2017-03-03) + monochrome BMP expansion + 1.03 ??? + 1.02 (2016-04-02) + avoid allocating large structures on the stack + 1.01 (2016-01-16) + STBIW_REALLOC_SIZED: support allocators with no realloc support + avoid race-condition in crc initialization + minor compile issues + 1.00 (2015-09-14) + installable file IO function + 0.99 (2015-09-13) + warning fixes; TGA rle support + 0.98 (2015-04-08) + added STBIW_MALLOC, STBIW_ASSERT etc + 0.97 (2015-01-18) + fixed HDR asserts, rewrote HDR rle logic + 0.96 (2015-01-17) + add HDR output + fix monochrome BMP + 0.95 (2014-08-17) + add monochrome TGA output + 0.94 (2014-05-31) + rename private functions to avoid conflicts with stb_image.h + 0.93 (2014-05-27) + warning fixes + 0.92 (2010-08-01) + casts to unsigned char to fix warnings + 0.91 (2010-07-17) + first public release + 0.90 first internal release +*/ + +/* +------------------------------------------------------------------------------ +This software is available under 2 licenses -- choose whichever you prefer. +------------------------------------------------------------------------------ +ALTERNATIVE A - MIT License +Copyright (c) 2017 Sean Barrett +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +------------------------------------------------------------------------------ +ALTERNATIVE B - Public Domain (www.unlicense.org) +This is free and unencumbered software released into the public domain. +Anyone is free to copy, modify, publish, use, compile, sell, or distribute this +software, either in source code form or as a compiled binary, for any purpose, +commercial or non-commercial, and by any means. +In jurisdictions that recognize copyright laws, the author or authors of this +software dedicate any and all copyright interest in the software to the public +domain. We make this dedication for the benefit of the public at large and to +the detriment of our heirs and successors. We intend this dedication to be an +overt act of relinquishment in perpetuity of all present and future rights to +this software under copyright law. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +------------------------------------------------------------------------------ +*/ diff --git a/tests/3d-regression/scenarios/scenario_tier1_model.cpp b/tests/3d-regression/scenarios/scenario_tier1_model.cpp new file mode 100644 index 0000000..5b1258a --- /dev/null +++ b/tests/3d-regression/scenarios/scenario_tier1_model.cpp @@ -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 +#include + +// 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 ); +} diff --git a/tests/3d-regression/scenarios/scenario_tier1_tdl.cpp b/tests/3d-regression/scenarios/scenario_tier1_tdl.cpp new file mode 100644 index 0000000..f13df49 --- /dev/null +++ b/tests/3d-regression/scenarios/scenario_tier1_tdl.cpp @@ -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 +#include + +// 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& 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 makePlateTdl( const std::vector& aContour, + float aZBot, float aZTop ) +{ + auto tdl = std::make_unique( 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 makeHexPlateList( SCENE3D_CTX& aCtx, float aZBot, + float aZTop ) +{ + auto tdl = makePlateTdl( MakeCircleContour( 4.5f, 6 ), aZBot, aZTop ); + return std::unique_ptr( 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 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 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 holes( aCtx.MakeRenderList( *holesTdl, zBot, zTop ) ); + std::unique_ptr 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 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 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 top( aCtx.MakeRenderList( *topTdl, 0.0f, 0.8f ) ); + + top->SetItIsTransparent( true ); + top->DrawAll(); +} diff --git a/tests/3d-regression/scenarios/scenario_tier1_utils.cpp b/tests/3d-regression/scenarios/scenario_tier1_utils.cpp new file mode 100644 index 0000000..5215b7a --- /dev/null +++ b/tests/3d-regression/scenarios/scenario_tier1_utils.cpp @@ -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 + +// 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() * 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() * 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 ); +} diff --git a/tests/3d-regression/scenarios/scenario_tier2_generators.cpp b/tests/3d-regression/scenarios/scenario_tier2_generators.cpp new file mode 100644 index 0000000..c10e7ff --- /dev/null +++ b/tests/3d-regression/scenarios/scenario_tier2_generators.cpp @@ -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 // pcbIUScale +#include +#include + +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 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 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 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 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 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( 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 cbList( aCtx.MakeRenderList( cbTdl, -1.0f, 1.0f ) ); + cbList->DrawAll(); + + std::unique_ptr 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 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& 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 makePlate( SCENE3D_CTX& aCtx, float aHalf, float aZBot, + float aZTop ) +{ + const std::vector 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( 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 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(); +} diff --git a/tests/3d-regression/scenarios/scenario_tier3_composite.cpp b/tests/3d-regression/scenarios/scenario_tier3_composite.cpp new file mode 100644 index 0000000..8f7c87e --- /dev/null +++ b/tests/3d-regression/scenarios/scenario_tier3_composite.cpp @@ -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 ); +} diff --git a/tests/3d-regression/scenarios/scene3d_test_ctx.cpp b/tests/3d-regression/scenarios/scene3d_test_ctx.cpp new file mode 100644 index 0000000..5d1b371 --- /dev/null +++ b/tests/3d-regression/scenarios/scene3d_test_ctx.cpp @@ -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 // 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() / 3.0f ); // -60° + m_camera.RotateZ( glm::pi() / 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 ); +} diff --git a/tests/3d-regression/scenarios/scene3d_test_ctx.h b/tests/3d-regression/scenarios/scene3d_test_ctx.h new file mode 100644 index 0000000..ce97339 --- /dev/null +++ b/tests/3d-regression/scenarios/scene3d_test_ctx.h @@ -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 // GL + GLU, platform-routed (glad native / GLES shim on wasm) + +#include +#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 diff --git a/tests/3d-regression/scenarios/scene3d_test_rig.h b/tests/3d-regression/scenarios/scene3d_test_rig.h new file mode 100644 index 0000000..f289ad6 --- /dev/null +++ b/tests/3d-regression/scenarios/scene3d_test_rig.h @@ -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 + +struct SCENE3D_TEST_RIG +{ + EDA_3D_VIEWER_SETTINGS m_cfg; + BOARD_ADAPTER m_adapter; + std::unique_ptr 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( 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 diff --git a/tests/3d-regression/scenarios/scene3d_test_scenarios.cpp b/tests/3d-regression/scenarios/scene3d_test_scenarios.cpp new file mode 100644 index 0000000..5bf3cd3 --- /dev/null +++ b/tests/3d-regression/scenarios/scene3d_test_scenarios.cpp @@ -0,0 +1,146 @@ +#include "scene3d_test_scenarios.h" +#include "scene3d_test_ctx.h" + +#include + +// 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-.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 diff --git a/tests/3d-regression/scenarios/scene3d_test_scenarios.h b/tests/3d-regression/scenarios/scene3d_test_scenarios.h new file mode 100644 index 0000000..c26f66e --- /dev/null +++ b/tests/3d-regression/scenarios/scene3d_test_scenarios.h @@ -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-.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 diff --git a/tests/3d-regression/scenarios/test_board_data.cpp b/tests/3d-regression/scenarios/test_board_data.cpp new file mode 100644 index 0000000..ad7ab8c --- /dev/null +++ b/tests/3d-regression/scenarios/test_board_data.cpp @@ -0,0 +1,202 @@ +#include "test_board_data.h" + +#include +#include + +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( 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 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 MakeCircleContour( float aRadius, int aSides, float aCenterX, + float aCenterY ) +{ + std::vector points; + points.reserve( aSides + 1 ); + + for( int i = 0; i < aSides; i++ ) + { + const float a = 2.0f * static_cast( M_PI ) * i / aSides; + points.emplace_back( aCenterX + aRadius * std::cos( a ), + aCenterY + aRadius * std::sin( a ) ); + } + + points.push_back( points.front() ); + return points; +} diff --git a/tests/3d-regression/scenarios/test_board_data.h b/tests/3d-regression/scenarios/test_board_data.h new file mode 100644 index 0000000..c39ed73 --- /dev/null +++ b/tests/3d-regression/scenarios/test_board_data.h @@ -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 // S3DMODEL / SMESH / SMATERIAL +#include + +#include + +// 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 MakeSquareContour( float aHalf, float aCenterX = 0.0f, + float aCenterY = 0.0f ); + +/// Closed regular-polygon contour approximating a circle. +std::vector MakeCircleContour( float aRadius, int aSides, float aCenterX = 0.0f, + float aCenterY = 0.0f ); + +#endif // TEST_BOARD_DATA_H diff --git a/tests/3d-regression/wasm/3d_webgl_test.cpp b/tests/3d-regression/wasm/3d_webgl_test.cpp new file mode 100644 index 0000000..8134a12 --- /dev/null +++ b/tests/3d-regression/wasm/3d_webgl_test.cpp @@ -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 +#include + +#include "scene3d_test_ctx.h" +#include "scene3d_test_scenarios.h" + +#include + +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; +} diff --git a/tests/3d-regression/wasm/3d_webgl_test.html b/tests/3d-regression/wasm/3d_webgl_test.html new file mode 100644 index 0000000..519c42c --- /dev/null +++ b/tests/3d-regression/wasm/3d_webgl_test.html @@ -0,0 +1,35 @@ + + + + + 3D Renderer WebGL Test + + + + + + + + + diff --git a/tests/3d-regression/wasm/Makefile b/tests/3d-regression/wasm/Makefile new file mode 100644 index 0000000..3199816 --- /dev/null +++ b/tests/3d-regression/wasm/Makefile @@ -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) diff --git a/tests/apps/3d-webgl/3d_webgl_test.html b/tests/apps/3d-webgl/3d_webgl_test.html new file mode 100644 index 0000000..519c42c --- /dev/null +++ b/tests/apps/3d-webgl/3d_webgl_test.html @@ -0,0 +1,35 @@ + + + + + 3D Renderer WebGL Test + + + + + + + + + diff --git a/tests/e2e/3d-webgl.spec.ts b/tests/e2e/3d-webgl.spec.ts new file mode 100644 index 0000000..ff45033 --- /dev/null +++ b/tests/e2e/3d-webgl.spec.ts @@ -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-.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`) }); + } + }); +}); diff --git a/tests/package.json b/tests/package.json index 83aa729..4545190 100644 --- a/tests/package.json +++ b/tests/package.json @@ -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", diff --git a/tests/tools/screenshots/compare-dirs.ts b/tests/tools/screenshots/compare-dirs.ts new file mode 100644 index 0000000..870cdaa --- /dev/null +++ b/tests/tools/screenshots/compare-dirs.ts @@ -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 --new --out \ + * [--floors --level ] # per-suite floor config + * [--floor ] # ad-hoc floor (wins over --floors) + * [--fail-on-change] # exit 1 on changed/missing/extra + * [--label ] # 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 }; +type FloorsFile = Record; + +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 { + const out: Record = {}; + 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 --new --out ' + + '[--floors --level ] [--floor ] [--fail-on-change] [--label ]' + ); + process.exit(2); +} + +/** Resolution: --floor (ad hoc) → floors.json per-name override → level default → built-in. */ +function makeFloorResolver(args: Record): (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; + 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();