pcbjam/tests/gal-regression/scenarios/scenario_negative_mode.cpp
Viktor Vaczi 051fb87cab feat(gal-test): Add 100% GAL API test coverage (28 scenarios)
Expand GAL native test harness from 24 to 28 scenarios covering all 70
GAL methods. New scenarios:

- scenario_text_attrs.cpp (24): Text attribute APIs (SetGlyphSize,
  SetFontBold/Italic/Underlined, SetTextMirrored, justification)
- scenario_glyphs.cpp (25): DrawGlyph/DrawGlyphs with stroke glyphs
- scenario_bitmap.cpp (26): DrawBitmap with test patterns
- scenario_transform.cpp (27): Transform() API documentation

Additional API coverage in existing scenarios:
- Flush() in test harness
- SetFlip(), SetRotation() in screen-transform
- SetDepthRange() in depth-testing
- GetGridPoint() in grid-native

New stub files:
- kifont_stub.h: STROKE_GLYPH factory functions for letter glyphs
- bitmap_base_stub.h: Test pattern generators (checkerboard, gradient)

Note: Bitmap scenario shows empty panels - DrawBitmap uses legacy OpenGL
immediate mode (glBegin/glEnd) which doesn't work while shader is active.
This is a known limitation when testing outside KiCad's VIEW rendering flow.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 12:27:06 +01:00

275 lines
9.8 KiB
C++

/**
* Negative Mode / Diff Layer Scenario
*
* Tests Gerber-style negative rendering concepts.
*
* IMPORTANT: In OPENGL_GAL these are mostly no-ops:
* - SetNegativeDrawMode() - NO-OP (Cairo uses CAIRO_OPERATOR_CLEAR)
* - StartNegativesLayer() / EndNegativesLayer() - NO-OP
* - StartDiffLayer() / EndDiffLayer() - Requires m_tempBuffer (compositor setup)
*
* This test:
* 1. Calls the APIs to verify they don't crash
* 2. Demonstrates what negative mode LOOKS like (simulated with layered drawing)
* 3. Shows PCB thermal relief patterns (common use case for negative mode)
*
* In Gerbview, negative objects "cut out" from the copper layer.
* The polarity is determined by: item->GetLayerPolarity() XOR image->m_ImageNegative
*/
#include <gal/graphics_abstraction_layer.h>
#include <cmath>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
namespace GALTest {
using KIGFX::COLOR4D;
using KIGFX::GAL;
void RenderNegativeMode(GAL* gal, int width, int height) {
// Enable depth testing for proper layering
gal->EnableDepthTest(true);
//=========================================================================
// Section 1: Simulated thermal relief (what negative mode produces)
//=========================================================================
// This shows the RESULT of negative mode - holes cut in copper pour
gal->SetLayerDepth(100);
gal->SetIsFill(true);
gal->SetIsStroke(false);
// Dark background panel
gal->SetFillColor(COLOR4D(0.12, 0.12, 0.15, 1.0));
gal->DrawRectangle(VECTOR2D(20, 20), VECTOR2D(380, 280));
// Copper pour (ground plane)
gal->SetLayerDepth(80);
gal->SetFillColor(COLOR4D(0.7, 0.5, 0.2, 1.0));
gal->DrawRectangle(VECTOR2D(40, 40), VECTOR2D(360, 260));
// Thermal relief pattern - simulated by drawing background color
// (In real Gerbview, SetNegativeDrawMode(true) + draw = erase)
gal->SetLayerDepth(60);
COLOR4D clearColor(0.12, 0.12, 0.15, 1.0); // "Cut through" to background
gal->SetFillColor(clearColor);
// First pad thermal - cross pattern
double pad1X = 120, pad1Y = 150;
double spokeLen = 35, spokeW = 5, clearance = 18;
// Clearance ring
gal->SetIsFill(false);
gal->SetIsStroke(true);
gal->SetLineWidth(8.0);
gal->SetStrokeColor(clearColor);
gal->DrawCircle(VECTOR2D(pad1X, pad1Y), clearance);
// Thermal spokes (horizontal and vertical)
gal->SetIsFill(true);
gal->SetIsStroke(false);
gal->SetFillColor(clearColor);
gal->DrawRectangle(VECTOR2D(pad1X - clearance - spokeLen, pad1Y - spokeW/2),
VECTOR2D(pad1X - clearance, pad1Y + spokeW/2));
gal->DrawRectangle(VECTOR2D(pad1X + clearance, pad1Y - spokeW/2),
VECTOR2D(pad1X + clearance + spokeLen, pad1Y + spokeW/2));
gal->DrawRectangle(VECTOR2D(pad1X - spokeW/2, pad1Y - clearance - spokeLen),
VECTOR2D(pad1X + spokeW/2, pad1Y - clearance));
gal->DrawRectangle(VECTOR2D(pad1X - spokeW/2, pad1Y + clearance),
VECTOR2D(pad1X + spokeW/2, pad1Y + clearance + spokeLen));
// Pad on top
gal->SetLayerDepth(40);
gal->SetFillColor(COLOR4D(0.85, 0.65, 0.25, 1.0));
gal->DrawCircle(VECTOR2D(pad1X, pad1Y), 14);
// Second pad thermal - diagonal spokes
double pad2X = 280, pad2Y = 150;
gal->SetLayerDepth(60);
gal->SetFillColor(clearColor);
// Diagonal thermal spokes
for (int i = 0; i < 4; i++) {
double angle = i * M_PI / 2 + M_PI / 4;
double x1 = pad2X + cos(angle) * clearance;
double y1 = pad2Y + sin(angle) * clearance;
double x2 = pad2X + cos(angle) * (clearance + spokeLen);
double y2 = pad2Y + sin(angle) * (clearance + spokeLen);
gal->DrawSegment(VECTOR2D(x1, y1), VECTOR2D(x2, y2), spokeW);
}
// Clearance ring
gal->SetIsFill(false);
gal->SetIsStroke(true);
gal->SetLineWidth(8.0);
gal->SetStrokeColor(clearColor);
gal->DrawCircle(VECTOR2D(pad2X, pad2Y), clearance);
// Pad
gal->SetLayerDepth(40);
gal->SetIsFill(true);
gal->SetFillColor(COLOR4D(0.85, 0.65, 0.25, 1.0));
gal->DrawCircle(VECTOR2D(pad2X, pad2Y), 14);
//=========================================================================
// Section 2: API calls test (verify they don't crash)
//=========================================================================
// These are NO-OPs in OpenGL but we call them to test the API
gal->SetLayerDepth(100);
gal->SetIsFill(true);
gal->SetIsStroke(false);
gal->SetFillColor(COLOR4D(0.15, 0.12, 0.15, 1.0));
gal->DrawRectangle(VECTOR2D(400, 20), VECTOR2D(780, 280));
// Draw base content
gal->SetLayerDepth(80);
gal->SetFillColor(COLOR4D(0.6, 0.3, 0.6, 1.0));
gal->DrawRectangle(VECTOR2D(420, 40), VECTOR2D(760, 260));
// Test SetNegativeDrawMode API (NO-OP in OpenGL)
gal->SetNegativeDrawMode(true);
// In Cairo, this would ERASE. In OpenGL, it draws normally.
gal->SetLayerDepth(60);
gal->SetFillColor(COLOR4D(0.15, 0.12, 0.15, 1.0));
gal->DrawCircle(VECTOR2D(500, 150), 35);
gal->DrawCircle(VECTOR2D(590, 150), 35);
gal->DrawCircle(VECTOR2D(680, 150), 35);
// Disable negative mode
gal->SetNegativeDrawMode(false);
// Draw something after to show mode was reset
gal->SetLayerDepth(50);
gal->SetFillColor(COLOR4D(0.9, 0.9, 0.3, 0.9));
gal->DrawCircle(VECTOR2D(500, 150), 15);
gal->DrawCircle(VECTOR2D(590, 150), 15);
gal->DrawCircle(VECTOR2D(680, 150), 15);
//=========================================================================
// Section 3: Negatives layer API test
//=========================================================================
gal->SetLayerDepth(100);
gal->SetFillColor(COLOR4D(0.12, 0.15, 0.12, 1.0));
gal->DrawRectangle(VECTOR2D(20, 300), VECTOR2D(380, 580));
// Base content before negatives layer
gal->SetLayerDepth(80);
gal->SetFillColor(COLOR4D(0.3, 0.6, 0.3, 1.0));
gal->DrawRectangle(VECTOR2D(40, 320), VECTOR2D(360, 560));
// Test StartNegativesLayer / EndNegativesLayer (NO-OP in OpenGL)
gal->StartNegativesLayer();
// Content that would be on negatives layer
gal->SetLayerDepth(60);
gal->SetFillColor(COLOR4D(0.12, 0.15, 0.12, 1.0));
// Via clearances (simulated)
for (int row = 0; row < 2; row++) {
for (int col = 0; col < 4; col++) {
double x = 80 + col * 80;
double y = 380 + row * 100;
gal->DrawCircle(VECTOR2D(x, y), 20);
}
}
gal->EndNegativesLayer();
// Vias on top
gal->SetLayerDepth(40);
gal->SetFillColor(COLOR4D(0.85, 0.65, 0.25, 1.0));
for (int row = 0; row < 2; row++) {
for (int col = 0; col < 4; col++) {
double x = 80 + col * 80;
double y = 380 + row * 100;
gal->DrawCircle(VECTOR2D(x, y), 12);
}
}
// Drill holes
gal->SetLayerDepth(20);
gal->SetFillColor(COLOR4D(0.1, 0.1, 0.1, 1.0));
for (int row = 0; row < 2; row++) {
for (int col = 0; col < 4; col++) {
double x = 80 + col * 80;
double y = 380 + row * 100;
gal->DrawCircle(VECTOR2D(x, y), 5);
}
}
//=========================================================================
// Section 4: What "show negative objects" mode looks like
//=========================================================================
// In Gerbview, there's an option to show negative objects in a highlight color
// instead of actually cutting them out
gal->SetLayerDepth(100);
gal->SetFillColor(COLOR4D(0.15, 0.15, 0.12, 1.0));
gal->DrawRectangle(VECTOR2D(400, 300), VECTOR2D(780, 580));
// Copper layer
gal->SetLayerDepth(80);
gal->SetFillColor(COLOR4D(0.6, 0.5, 0.2, 1.0));
gal->DrawRectangle(VECTOR2D(420, 320), VECTOR2D(760, 560));
// "Negative objects" shown as semi-transparent overlay (like Gerbview's show_negative_objects mode)
gal->SetLayerDepth(60);
gal->SetFillColor(COLOR4D(0.2, 0.8, 0.8, 0.5)); // Cyan highlight for negatives
// Trace clearance
gal->DrawSegment(VECTOR2D(440, 380), VECTOR2D(740, 380), 25);
// Pad clearances
gal->DrawCircle(VECTOR2D(480, 380), 20);
gal->DrawCircle(VECTOR2D(590, 380), 20);
gal->DrawCircle(VECTOR2D(700, 380), 20);
// Via clearances
for (int i = 0; i < 3; i++) {
gal->DrawCircle(VECTOR2D(480 + i * 110, 480), 18);
}
// Actual traces and pads (drawn on top)
gal->SetLayerDepth(40);
gal->SetFillColor(COLOR4D(0.8, 0.6, 0.2, 1.0));
gal->DrawSegment(VECTOR2D(440, 380), VECTOR2D(740, 380), 10);
gal->SetFillColor(COLOR4D(0.9, 0.7, 0.3, 1.0));
gal->DrawCircle(VECTOR2D(480, 380), 14);
gal->DrawCircle(VECTOR2D(590, 380), 14);
gal->DrawCircle(VECTOR2D(700, 380), 14);
gal->DrawCircle(VECTOR2D(480, 480), 12);
gal->DrawCircle(VECTOR2D(590, 480), 12);
gal->DrawCircle(VECTOR2D(700, 480), 12);
// Holes
gal->SetLayerDepth(20);
gal->SetFillColor(COLOR4D(0.1, 0.1, 0.1, 1.0));
gal->DrawCircle(VECTOR2D(480, 380), 5);
gal->DrawCircle(VECTOR2D(590, 380), 5);
gal->DrawCircle(VECTOR2D(700, 380), 5);
gal->DrawCircle(VECTOR2D(480, 480), 4);
gal->DrawCircle(VECTOR2D(590, 480), 4);
gal->DrawCircle(VECTOR2D(700, 480), 4);
//=========================================================================
// Section frames
//=========================================================================
gal->SetLayerDepth(5);
gal->SetIsFill(false);
gal->SetIsStroke(true);
gal->SetLineWidth(2.0);
gal->SetStrokeColor(COLOR4D(0.5, 0.5, 0.6, 0.8));
gal->DrawRectangle(VECTOR2D(20, 20), VECTOR2D(380, 280));
gal->DrawRectangle(VECTOR2D(400, 20), VECTOR2D(780, 280));
gal->DrawRectangle(VECTOR2D(20, 300), VECTOR2D(380, 580));
gal->DrawRectangle(VECTOR2D(400, 300), VECTOR2D(780, 580));
}
} // namespace GALTest