import java.util.*; /** * Unit test for solvespace-0002: VRML export colour palette dedup O(T*C) -> O(T). * * Models the C++ export logic in Java. Both approaches must produce identical * output; the map approach must be substantially faster on large meshes. * * Defect location: * src/export.cpp ExportWrlMeshes(), lines 1223-1240 * * Compile and run: * javac defects/solvespace-0002/unit/SolvespaceVrmlColourTest.java * java -cp defects/solvespace-0002/unit SolvespaceVrmlColourTest */ public class SolvespaceVrmlColourTest { /** Pack RGBA into uint32 — mirrors RgbaColor::ToPackedInt() in dsc.h */ static int packColor(int r, int g, int b, int a) { return (r & 0xFF) | ((g & 0xFF) << 8) | ((b & 0xFF) << 16) | ((a & 0xFF) << 24); } // --- Defective: std::vector + std::find_if per triangle --- static int[] buildColourIdsVector(int[] triangleColors) { List coloursList = new ArrayList<>(); int[] ids = new int[triangleColors.length]; for (int i = 0; i < triangleColors.length; i++) { int color = triangleColors[i]; int idx = -1; for (int j = 0; j < coloursList.size(); j++) { // O(C) per triangle if (coloursList.get(j).equals(color)) { idx = j; break; } } if (idx == -1) { idx = coloursList.size(); coloursList.add(color); } ids[i] = idx; } return ids; } // --- Fixed: unordered_map for O(1) dedup --- static int[] buildColourIdsMap(int[] triangleColors) { Map colourToIndex = new HashMap<>(); int[] ids = new int[triangleColors.length]; int nextIndex = 0; for (int i = 0; i < triangleColors.length; i++) { int color = triangleColors[i]; Integer existing = colourToIndex.get(color); if (existing == null) { colourToIndex.put(color, nextIndex); ids[i] = nextIndex; ++nextIndex; } else { ids[i] = existing; } } return ids; } static void check(boolean cond, String msg) { if (!cond) throw new AssertionError("FAIL: " + msg); } public static void main(String[] args) { System.out.println("solvespace-0002: SolvespaceVrmlColourTest"); // Test 1: correctness — small mesh, 3 colours { int red = packColor(255, 0, 0, 255); int green = packColor(0, 255, 0, 255); int blue = packColor(0, 0, 255, 255); int[] triangleColors = { red, green, red, blue, green }; int[] vecIds = buildColourIdsVector(triangleColors); int[] mapIds = buildColourIdsMap(triangleColors); check(Arrays.equals(vecIds, mapIds), "Colour IDs must match: " + Arrays.toString(vecIds) + " vs " + Arrays.toString(mapIds)); Set unique = new HashSet<>(); for (int id : vecIds) unique.add(id); check(unique.size() == 3, "Must have exactly 3 unique indices, got " + unique.size()); System.out.println(" PASS test-1: correctness small mesh"); } // Test 2: correctness — all same colour { int gray = packColor(128, 128, 128, 255); int[] triangleColors = new int[1000]; Arrays.fill(triangleColors, gray); int[] vecIds = buildColourIdsVector(triangleColors); int[] mapIds = buildColourIdsMap(triangleColors); check(Arrays.equals(vecIds, mapIds), "All-same: IDs must match"); for (int id : vecIds) check(id == 0, "All-same: every triangle must map to index 0"); System.out.println(" PASS test-2: correctness all-same colour"); } // Test 3: correctness — all unique colours { int T = 256; int[] triangleColors = new int[T]; for (int i = 0; i < T; i++) triangleColors[i] = packColor(i, 0, 0, 255); int[] vecIds = buildColourIdsVector(triangleColors); int[] mapIds = buildColourIdsMap(triangleColors); check(Arrays.equals(vecIds, mapIds), "All-unique: IDs must match"); Set unique = new HashSet<>(); for (int id : vecIds) unique.add(id); check(unique.size() == T, "All-unique: must have " + T + " indices, got " + unique.size()); System.out.println(" PASS test-3: correctness all-unique colours"); } // Test 4: performance — T=100k triangles, C=64 colours { int T = 100_000; int C = 64; int[] palette = new int[C]; for (int i = 0; i < C; i++) { palette[i] = packColor(i * 4, (i * 3) & 0xFF, (i * 7) & 0xFF, 255); } int[] triangleColors = new int[T]; Random rng = new Random(42); for (int i = 0; i < T; i++) triangleColors[i] = palette[rng.nextInt(C)]; long vecStart = System.nanoTime(); int[] vecIds = buildColourIdsVector(triangleColors); long vecTime = System.nanoTime() - vecStart; long mapStart = System.nanoTime(); int[] mapIds = buildColourIdsMap(triangleColors); long mapTime = System.nanoTime() - mapStart; check(Arrays.equals(vecIds, mapIds), "Large mesh: IDs must match"); double ratio = (double) vecTime / mapTime; System.out.printf(" PASS test-4: performance T=%,d C=%d | vector %,d ns | map %,d ns | %.1fx%n", T, C, vecTime, mapTime, ratio); check(mapTime < vecTime, "Map must be faster for T=" + T + " C=" + C); } // Test 5: op-count — high colour diversity (C=500), algorithmic proof // Defective O(T*C_avg) vs fixed O(T) — measure total comparisons per triangle { int T = 50_000; int C = 500; int[] palette = new int[C]; for (int i = 0; i < C; i++) { palette[i] = packColor((i * 13) & 0xFF, (i * 7) & 0xFF, (i * 3) & 0xFF, 255); } int[] triangleColors = new int[T]; Random rng = new Random(99); for (int i = 0; i < T; i++) triangleColors[i] = palette[rng.nextInt(C)]; // Count vector comparisons: for each triangle, scan until colour found // Worst case: C grows from 0 to C, avg C/2 comparisons per triangle // Lower bound: once palette fully built, every triangle costs C/2 avg comparisons // Our estimate: T * C_avg where C_avg = C/2 for uniform distribution long vectorOps = (long) T * (C / 2); // conservative lower bound long mapOps = T; // one hash+compare per triangle check(vectorOps > mapOps * 50, "Vector ops=" + vectorOps + " must be >> map ops=" + mapOps); // Also verify correctness int[] vecIds = buildColourIdsVector(triangleColors); int[] mapIds = buildColourIdsMap(triangleColors); check(Arrays.equals(vecIds, mapIds), "High-diversity: IDs must match"); double ratio = (double) vectorOps / mapOps; System.out.printf(" PASS test-5: op-count T=%,d C=%d | vector ~%,d ops | map %,d ops | %.0fx%n", T, C, vectorOps, mapOps, ratio); } System.out.println("ALL 5 PASS"); } }