diff --git a/lineclip/Makefile b/lineclip/Makefile new file mode 100644 index 000000000..0888b3c82 --- /dev/null +++ b/lineclip/Makefile @@ -0,0 +1,26 @@ +# Line clipping library and test programs +CC = gcc +CFLAGS = -Wall -Wextra -O2 +LDFLAGS = +LIBS = +SDL_CFLAGS = $(shell pkg-config --cflags sdl3 2>/dev/null) +SDL_LIBS = $(shell pkg-config --libs sdl3 2>/dev/null || echo "-lSDL3") + +# Sources +LIB_SRCS = lineclip.c +CS_SRCS = test_clip.c +VIS_SRCS = clip_visual.c + +# Targets +all: test_clip clip_visual + +test_clip: $(CS_SRCS) $(LIB_SRCS) + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(CS_SRCS) $(LIB_SRCS) $(LIBS) + +clip_visual: $(VIS_SRCS) $(LIB_SRCS) + $(CC) $(CFLAGS) $(SDL_CFLAGS) $(LDFLAGS) -o $@ $(VIS_SRCS) $(LIB_SRCS) $(SDL_LIBS) + +clean: + rm -f test_clip clip_visual + +.PHONY: all clean diff --git a/lineclip/clip_visual.c b/lineclip/clip_visual.c new file mode 100644 index 000000000..5b65054b8 --- /dev/null +++ b/lineclip/clip_visual.c @@ -0,0 +1,264 @@ +/* clip_visual.c - SDL3 visual test for line clipping algorithms + * + * Draws the clip window and all test cases. + * Gray = original line. Green = clipped result (visible). Red = rejected. + * Press SPACE to cycle algorithms. Press Q to quit. + * + * Build: gcc -Wall -Wextra -O2 -o clip_visual clip_visual.c lineclip.c -lSDL3 + */ +#include +#include +#include +#include "lineclip.h" + +/* ----------------------------------------------------------------------- */ + +#define WIDTH 1200 +#define HEIGHT 900 + +/* World coordinate range: 0..2000 mapped to screen */ +#define WORLD_MIN 0 +#define WORLD_MAX 2000 + +/* Clip window: (400,300) to (1600,1400) */ +#define CLIP_X1 400 +#define CLIP_Y1 300 +#define CLIP_X2 1600 +#define CLIP_Y2 1400 + +/* Map world coords to screen coords */ +static float sx(int16_t x) { + return 80.0f + ((float)(x - WORLD_MIN) * (float)(WIDTH - 160) / (float)(WORLD_MAX - WORLD_MIN)); +} +static float sy(int16_t y) { + return (float)(HEIGHT - 80) - ((float)(y - WORLD_MIN) * (float)(HEIGHT - 160) / (float)(WORLD_MAX - WORLD_MIN)); +} + +typedef struct { + int16_t x1, y1, x2, y2; + const char *name; +} TestCase; + +static const TestCase tests[] = { + /* Inside the clip window */ + { 800, 600, 1200, 1000, "T1 inside" }, + { 420, 320, 500, 400, "T2 near TL" }, + { 1580, 1380, 1600, 1400, "T3 near BR" }, + { 1000, 850, 1000, 850, "T4 point inside" }, + + /* Completely outside -- far away */ + { -500, -200, -300, 100, "T5 far left" }, + { 2200, 1500, 2500, 1800, "T6 far right" }, + { 800, -500, 1000, -200, "T7 far above" }, + { 1000, 1700, 1200, 2000, "T8 far below" }, + { -300, -300, 200, 100, "T9 far TL" }, + { 1800, 1600, 2200, 1700, "T10 far BR" }, + { -400, 1500, 100, 1800, "T11 far BL" }, + { 1800, -300, 2000, 100, "T12 far TR" }, + + /* Crossing from outside into window */ + { 200, 800, 600, 800, "T13 enter left" }, + { 1400, 800, 1800, 800, "T14 enter right" }, + { 1000, 100, 1000, 500, "T15 enter top" }, + { 1000, 1200, 1000, 1600, "T16 enter bottom" }, + + /* Diagonals crossing the window */ + { 100, 200, 1800, 1700, "T17 diag TL-BR" }, + { 100, 1700, 1800, 200, "T18 diag BL-TR" }, + { 1800, 200, 100, 1700, "T19 diag TR-BL" }, + { 1800, 1700, 100, 200, "T20 diag BR-TL" }, + + /* Very long lines crossing the world */ + { 0, 1000, 2000, 1000, "T21 horiz world" }, + { 1000, 0, 1000, 2000, "T22 vert world" }, + { 0, 0, 2000, 2000, "T23 full world diag" }, + { 2000, 0, 0, 2000, "T24 full world diag2" }, + + /* Shallow diagonals */ + { 200, 800, 1800, 810, "T25 shallow up" }, + { 200, 900, 1800, 890, "T26 shallow down" }, + + /* Steep diagonals */ + { 1000, 100, 1010, 1900, "T27 steep right" }, + { 990, 100, 1000, 1900, "T28 steep left" }, + + /* Long rejects: parallel but far outside */ + { 200, 100, 350, 100, "T29 above parallel" }, + { 200, 1550, 350, 1550, "T30 below parallel" }, + { 100, 800, 100, 1100, "T31 left parallel" }, + { 1800, 800, 1800, 1100, "T32 right parallel" }, + + /* Extreme coordinates (within int16 range) */ + { 0, 1000, 0, 1001, "T33 x=0 tiny" }, + { 2000, 1000, 2000, 1001, "T34 x=2000 tiny" }, + { 1000, 0, 1001, 0, "T35 y=0 tiny" }, + { 1000, 2000, 1001, 2000, "T36 y=2000 tiny" }, + + /* Lines grazing the clip window edges */ + { 200, 300, 1800, 300, "T37 graze top edge" }, + { 200, 1400, 1800, 1400, "T38 graze bottom" }, + { 400, 100, 400, 1600, "T39 graze left edge" }, + { 1600, 100, 1600, 1600, "T40 graze right edge" }, + + /* Points on clip window corners */ + { 400, 300, 400, 300, "T41 pt TL corner" }, + { 1600, 300, 1600, 300, "T42 pt TR corner" }, + { 400, 1400, 400, 1400, "T43 pt BL corner" }, + { 1600, 1400, 1600, 1400, "T44 pt BR corner" }, + + /* Lines connecting two distant corners through the window */ + { 0, 0, 2000, 2000, "T45 origin to far BR" }, + { 2000, 0, 0, 2000, "T46 far TL to far BR" }, + + /* Short lines just outside each edge */ + { 390, 800, 395, 800, "T47 near left edge" }, + { 1605, 800, 1610, 800, "T48 near right edge" }, + { 1000, 295, 1005, 295, "T49 near top edge" }, + { 1000, 1405, 1005, 1405, "T50 near bottom edge" }, +}; +#define NUM_TESTS (sizeof(tests) / sizeof(tests[0])) + +typedef struct { + const char *name; + bool (*clip)(int16_t*, int16_t*, int16_t*, int16_t*, + int16_t, int16_t, int16_t, int16_t); +} Algo; + +static const Algo algos[] = { + { "CS-float", clip_cohen_sutherland }, + { "CS-int16", clip_cohen_sutherland_int16 }, + { "Elite-style", clip_elite }, +}; +#define NUM_ALGOS (sizeof(algos) / sizeof(algos[0])) + +/* Draw a line */ +static void draw_line(SDL_Renderer *r, float x1, float y1, float x2, float y2, + SDL_FColor c) +{ + SDL_FPoint pts[2] = { { x1, y1 }, { x2, y2 } }; + SDL_SetRenderDrawColorFloat(r, c.r, c.g, c.b, c.a); + SDL_RenderLines(r, pts, 2); +} + +/* ----------------------------------------------------------------------- */ + +int main(void) +{ + SDL_Window *win; + SDL_Renderer *ren; + SDL_Event ev; + int algo_idx = 0; + int quit = 0; + + if (!SDL_Init(SDL_INIT_VIDEO)) { + SDL_Log("SDL_Init failed: %s", SDL_GetError()); + return 1; + } + + win = SDL_CreateWindow("Line Clipping Visual Test", WIDTH, HEIGHT, 0); + ren = SDL_CreateRenderer(win, NULL); + if (!win || !ren) { + SDL_Log("Window/Renderer failed: %s", SDL_GetError()); + return 1; + } + + /* Clip window rect */ + SDL_FRect clip_rect = { + sx(CLIP_X1), sy(CLIP_Y2), + sx(CLIP_X2) - sx(CLIP_X1), + sy(CLIP_Y1) - sy(CLIP_Y2) + }; + + /* Grid lines */ + SDL_SetRenderDrawColor(ren, 50, 50, 60, 255); + for (int i = 0; i <= 2000; i += 200) { + SDL_RenderLine(ren, sx(i), sy(WORLD_MIN), sx(i), sy(WORLD_MAX)); + SDL_RenderLine(ren, sx(WORLD_MIN), sy(i), sx(WORLD_MAX), sy(i)); + } + + while (!quit) { + while (SDL_PollEvent(&ev)) { + if (ev.type == SDL_EVENT_QUIT) quit = 1; + if (ev.type == SDL_EVENT_KEY_DOWN) { + switch (ev.key.key) { + case SDLK_ESCAPE: case SDLK_Q: quit = 1; break; + case SDLK_SPACE: + algo_idx = (algo_idx + 1) % NUM_ALGOS; + break; + } + } + } + + /* Clear */ + SDL_SetRenderDrawColor(ren, 15, 15, 25, 255); + SDL_RenderClear(ren); + + /* Grid */ + SDL_SetRenderDrawColor(ren, 40, 40, 55, 255); + for (int i = 0; i <= WORLD_MAX; i += 200) { + SDL_RenderLine(ren, sx(i), sy(WORLD_MIN), sx(i), sy(WORLD_MAX)); + SDL_RenderLine(ren, sx(WORLD_MIN), sy(i), sx(WORLD_MAX), sy(i)); + } + + /* Fill clip window */ + SDL_FColor fill = { 0.1f, 0.12f, 0.18f, 0.7f }; + SDL_SetRenderDrawColorFloat(ren, fill.r, fill.g, fill.b, fill.a); + SDL_RenderFillRect(ren, &clip_rect); + + /* Draw clip window border */ + SDL_SetRenderDrawColor(ren, 100, 100, 130, 255); + SDL_RenderRect(ren, &clip_rect); + + const Algo *algo = &algos[algo_idx]; + + /* Draw every test */ + for (int i = 0; i < (int)NUM_TESTS; i++) { + int16_t ox1 = tests[i].x1, oy1 = tests[i].y1; + int16_t ox2 = tests[i].x2, oy2 = tests[i].y2; + int16_t rx1 = ox1, ry1 = oy1, rx2 = ox2, ry2 = oy2; + + int vis = algo->clip(&rx1, &ry1, &rx2, &ry2, + CLIP_X1, CLIP_Y1, CLIP_X2, CLIP_Y2); + + /* Original line in dim gray */ + SDL_FColor gray = { 0.3f, 0.3f, 0.35f, 0.6f }; + draw_line(ren, sx(ox1), sy(oy1), sx(ox2), sy(oy2), gray); + + if (vis) { + /* Clipped result in green */ + SDL_FColor green = { 0.15f, 0.9f, 0.3f, 1.0f }; + draw_line(ren, sx(rx1), sy(ry1), sx(rx2), sy(ry2), green); + + /* Endpoint markers */ + SDL_FColor marker = { 0.3f, 1.0f, 0.4f, 1.0f }; + SDL_FRect m = { sx(rx1)-3, sy(ry1)-3, 6, 6 }; + SDL_SetRenderDrawColorFloat(ren, marker.r, marker.g, marker.b, marker.a); + SDL_RenderFillRect(ren, &m); + m.x = sx(rx2)-3; m.y = sy(ry2)-3; + SDL_RenderFillRect(ren, &m); + } else { + /* Rejected: red X at midpoint */ + SDL_FColor red = { 0.8f, 0.15f, 0.15f, 1.0f }; + float mx = (sx(ox1) + sx(ox2)) / 2; + float my = (sy(oy1) + sy(oy2)) / 2; + SDL_SetRenderDrawColorFloat(ren, red.r, red.g, red.b, red.a); + SDL_FPoint xa[2] = { { mx-5, my-5 }, { mx+5, my+5 } }; + SDL_FPoint xb[2] = { { mx-5, my+5 }, { mx+5, my-5 } }; + SDL_RenderLines(ren, xa, 2); + SDL_RenderLines(ren, xb, 2); + } + } + + /* Algorithm name in title bar */ + char title[128]; + SDL_snprintf(title, sizeof(title), "%s (%d tests)", algo->name, (int)NUM_TESTS); + SDL_SetWindowTitle(win, title); + + SDL_RenderPresent(ren); + } + + SDL_DestroyRenderer(ren); + SDL_DestroyWindow(win); + SDL_Quit(); + return 0; +} diff --git a/lineclip/lineclip.c b/lineclip/lineclip.c new file mode 100644 index 000000000..4fc284764 --- /dev/null +++ b/lineclip/lineclip.c @@ -0,0 +1,266 @@ +/* lineclip.c - Line clipping algorithms + * + * CURRENT implementation: optimized for SMALLNESS using C's native float. + * + * FUTURE constraints for a planned integer-only 6502 target implementation: + * (These are NOT enforced in the current code!) + * ----------------------------------------------------- + * 1. NO floating point arithmetic. + * 2. NO signed modulo operator (%). + * 3. Results MUST be strictly inside the clip window. + * 4. Return true if visible, false if fully clipped. + * 5. Parametric t is scaled by 256. + * 6. Keep intermediates in int16_t; use int32_t only when necessary. + * Input coords are int16_t (-32767..32767). Clip window is [0,0]-[639,479]. + * 7. NO recursion — iterative only. + * 8. 16-bit signed division IS available. No 32-bit/64-bit division. + * Division by 256 is a right shift. + */ +#include "lineclip.h" +#include +#include + +/* (dist * num) / den — 32-bit intermediate, 16-bit result. + * Used by both CS-int16 and Elite-style clipping. + * + * 6502 Implementation Notes: + * --------------------------- + * On a plain 6502 this expands to a 16x16→32 multiply followed by a + * 32/16→16 division. Neither operation is natively supported, so both + * require software routines: + * + * MUL16x16: shift-add multiply, ~160 cycles + * DIV32x16: restoring division, ~200 cycles + * TOTAL: ~360 cycles per call + * + * Optimisation opportunities for 6502: + * ------------------------------------- + * 1. When den is a power of two (e.g. 256), replace the division + * with a simple byte swap or right-shift. This saves the entire + * DIV32x16 routine (~200 cycles). + * + * 2. When num is 256 (common in frac_mul(dx, t0, 256)), the 32-bit + * multiply reduces to a 16-bit left-shift by 8 (i.e. store dx in + * the high word). The result is simply the high byte of the + * 32-bit product — no actual multiply loop needed. + * + * 3. When |dist| < 256 (common for short clip distances), the 16x16 + * multiply can be reduced to an 8x16 multiply, saving ~40 cycles. + * + * 4. Sign handling: track the sign separately, do unsigned math on + * absolute values, then negate if needed. This avoids signed + * multiplication and division entirely, using simpler unsigned + * routines. + * + * 5. Zero-page placement: keep all 6 working variables in zero-page + * ($00-$09) to save 2 cycles per access. + * + * Combined, these optimizations can reduce a single frac_mul call + * from ~360 cycles down to ~60-120 cycles depending on the inputs. + */ +static inline int16_t frac_mul(int16_t dist, int16_t num, int16_t den) +{ + return (int16_t)((int32_t)dist * num / den); +} + +/* ========================================================================== */ +/* Cohen-Sutherland: iterative region-code clipping (float) */ +/* ========================================================================== */ + +static inline int _oc(int16_t x, int16_t y, int16_t x1, int16_t y1, int16_t x2, int16_t y2) +{ + int c = 0; + if (x < x1) c |= 1; + if (x > x2) c |= 2; + if (y < y1) c |= 4; + if (y > y2) c |= 8; + return c; +} + +/* After clipping against LEFT/RIGHT edge: x is exactly cx1 or cx2, so + * LEFT=RIGHT=0. Only need to test the new y against cy1/cy2. */ +static inline int _oc_after_x(int16_t y, int16_t cy1, int16_t cy2) +{ + int c = 0; + if (y < cy1) c |= 4; + if (y > cy2) c |= 8; + return c; +} + +/* After clipping against TOP/BOTTOM edge: y is exactly cy1 or cy2, so + * TOP=BOTTOM=0. Only need to test the new x against cx1/cx2. */ +static inline int _oc_after_y(int16_t x, int16_t cx1, int16_t cx2) +{ + int c = 0; + if (x < cx1) c |= 1; + if (x > cx2) c |= 2; + return c; +} + +bool clip_cohen_sutherland(int16_t *x1, int16_t *y1, int16_t *x2, int16_t *y2, + int16_t cx1, int16_t cy1, int16_t cx2, int16_t cy2) +{ + int16_t ax = *x1, ay = *y1, bx = *x2, by = *y2; + int ca = _oc(ax, ay, cx1, cy1, cx2, cy2); + int cb = _oc(bx, by, cx1, cy1, cx2, cy2); + float dx = bx - ax, dy = by - ay; + + for (;;) { + if ((ca | cb) == 0) { *x1 = ax; *y1 = ay; *x2 = bx; *y2 = by; return true; } + if ((ca & cb) != 0) return false; + + int co = ca ? ca : cb; + int16_t x = 0, y = 0; + + if (co & 8) { x = ax + (int16_t)(dx * (cy2 - ay) / dy); y = cy2; } + else if (co & 4) { x = ax + (int16_t)(dx * (cy1 - ay) / dy); y = cy1; } + else if (co & 2) { y = ay + (int16_t)(dy * (cx2 - ax) / dx); x = cx2; } + else { y = ay + (int16_t)(dy * (cx1 - ax) / dx); x = cx1; } + + if (co == ca) { + dx -= x - ax; + dy -= y - ay; + ax = x; ay = y; + ca = (co & 0xC) ? _oc_after_y(ax, cx1, cx2) : _oc_after_x(ay, cy1, cy2); + } else { + dx -= bx - x; + dy -= by - y; + bx = x; by = y; + cb = (co & 0xC) ? _oc_after_y(bx, cx1, cx2) : _oc_after_x(by, cy1, cy2); + } + } +} + +/* ========================================================================== */ +/* Cohen-Sutherland: iterative region-code clipping, pure int16_t */ +/* Uses frac_mul for the intersection math -- no float operations at all. */ +/* ========================================================================== */ + +bool clip_cohen_sutherland_int16(int16_t *x1, int16_t *y1, int16_t *x2, int16_t *y2, + int16_t cx1, int16_t cy1, int16_t cx2, int16_t cy2) +{ + int16_t ax = *x1, ay = *y1, bx = *x2, by = *y2; + int ca = _oc(ax, ay, cx1, cy1, cx2, cy2); + int cb = _oc(bx, by, cx1, cy1, cx2, cy2); + int16_t dx = bx - ax, dy = by - ay; + + for (;;) { + if ((ca | cb) == 0) { *x1 = ax; *y1 = ay; *x2 = bx; *y2 = by; return true; } + if ((ca & cb) != 0) return false; + + int co = ca ? ca : cb; + int16_t x = 0, y = 0; + + if (co & 8) { x = ax + frac_mul(cy2 - ay, dx, dy); y = cy2; } + else if (co & 4) { x = ax + frac_mul(cy1 - ay, dx, dy); y = cy1; } + else if (co & 2) { y = ay + frac_mul(cx2 - ax, dy, dx); x = cx2; } + else { y = ay + frac_mul(cx1 - ax, dy, dx); x = cx1; } + + if (co == ca) { + dx -= x - ax; + dy -= y - ay; + ax = x; ay = y; + ca = (co & 0xC) ? _oc_after_y(ax, cx1, cx2) : _oc_after_x(ay, cy1, cy2); + } else { + dx -= bx - x; + dy -= by - y; + bx = x; by = y; + cb = (co & 0xC) ? _oc_after_y(bx, cx1, cx2) : _oc_after_x(by, cy1, cy2); + } + } +} + +/* ========================================================================== */ +/* Elite-inspired line clipping (BBC Micro, 6502) */ +/* Based on the two-stage algorithm from Mark Moxon's Elite deep dive: */ +/* https://elite.bbcelite.com/deep_dives/line-clipping.html */ +/* */ +/* Uses the line equation directly: x = ax + dx*t, y = ay + dy*t. */ +/* At an X edge: t = (edge - ax) / dx → Δy = dy * t */ +/* At a Y edge: t = (edge - ay) / dy → Δx = dx * t */ +/* ========================================================================== */ + +/* Test whether a point is inside the clip window */ +static inline bool elite_inside(int16_t x, int16_t y, int16_t cx1, int16_t cy1, + int16_t cx2, int16_t cy2) +{ + return x >= cx1 && x <= cx2 && y >= cy1 && y <= cy2; +} + +/* Clip one off-screen endpoint toward the other endpoint. + * A moves toward B, B moves toward A (signs handled by caller). + * Loops to handle secondary clipping after the first axis is fixed. */ +static void elite_clip_one(int16_t *ax, int16_t *ay, + int16_t cx1, int16_t cy1, + int16_t cx2, int16_t cy2, + int16_t odx, int16_t ody) +{ + for (int iter = 0; iter < 2; iter++) { + int16_t x = *ax, y = *ay; + int did_clip = 0; + + if (x < cx1 && odx > 0) { + y += frac_mul(cx1 - x, ody, odx); + x = cx1; + did_clip = 1; + } else if (x > cx2 && odx < 0) { + y += frac_mul(cx2 - x, ody, odx); + x = cx2; + did_clip = 1; + } + + if (y < cy1 && ody > 0) { + x += frac_mul(cy1 - y, odx, ody); + y = cy1; + did_clip = 1; + } else if (y > cy2 && ody < 0) { + x += frac_mul(cy2 - y, odx, ody); + y = cy2; + did_clip = 1; + } + + *ax = x; + *ay = y; + if (!did_clip) break; + } +} + +/* Elite-style two-stage clipping. + * Returns true if any part of the line is visible, false otherwise. + * Source: https://elite.bbcelite.com/deep_dives/line-clipping.html */ +bool clip_elite(int16_t *x1, int16_t *y1, int16_t *x2, int16_t *y2, + int16_t cx1, int16_t cy1, int16_t cx2, int16_t cy2) +{ + int16_t ax = *x1, ay = *y1, bx = *x2, by = *y2; + bool a_in = elite_inside(ax, ay, cx1, cy1, cx2, cy2); + bool b_in = elite_inside(bx, by, cx1, cy1, cx2, cy2); + + /* Fast accept */ + if (a_in && b_in) return true; + + /* Quick reject: both off-screen on the same side */ + int a_code = 0, b_code = 0; + if (ax < cx1) a_code |= 1; + if (ax > cx2) a_code |= 2; + if (ay < cy1) a_code |= 4; + if (ay > cy2) a_code |= 8; + if (bx < cx1) b_code |= 1; + if (bx > cx2) b_code |= 2; + if (by < cy1) b_code |= 4; + if (by > cy2) b_code |= 8; + if ((a_code & b_code) != 0) return false; + + int16_t ddx = bx - ax; + int16_t ddy = by - ay; + + /* Clip off-screen endpoints */ + if (!a_in) elite_clip_one(&ax, &ay, cx1, cy1, cx2, cy2, ddx, ddy); + if (!b_in) elite_clip_one(&bx, &by, cx1, cy1, cx2, cy2, -ddx, -ddy); + + /* Final validation */ + if (!elite_inside(ax, ay, cx1, cy1, cx2, cy2)) return false; + if (!elite_inside(bx, by, cx1, cy1, cx2, cy2)) return false; + + *x1 = ax; *y1 = ay; *x2 = bx; *y2 = by; + return true; +} diff --git a/lineclip/lineclip.h b/lineclip/lineclip.h new file mode 100644 index 000000000..ae60feb03 --- /dev/null +++ b/lineclip/lineclip.h @@ -0,0 +1,23 @@ +/* lineclip.h - Line clipping algorithms (Cohen-Sutherland, Liang-Barsky, Elite-inspired) */ +#ifndef LINECLIP_H +#define LINECLIP_H + +#include +#include + +/* All coordinates are signed to support off-screen positions */ + +/* Cohen-Sutherland line clipping (float intersection math) */ +bool clip_cohen_sutherland(int16_t *x1, int16_t *y1, int16_t *x2, int16_t *y2, + int16_t clipx1, int16_t clipy1, int16_t clipx2, int16_t clipy2); + +/* Cohen-Sutherland line clipping (pure int16_t, uses frac_mul internally) */ +bool clip_cohen_sutherland_int16(int16_t *x1, int16_t *y1, int16_t *x2, int16_t *y2, + int16_t clipx1, int16_t clipy1, int16_t clipx2, int16_t clipy2); + +/* Elite-inspired two-stage clipping (BBC Micro, 6502) */ +/* Based on: https://elite.bbcelite.com/deep_dives/line-clipping.html */ +bool clip_elite(int16_t *x1, int16_t *y1, int16_t *x2, int16_t *y2, + int16_t clipx1, int16_t clipy1, int16_t clipx2, int16_t clipy2); + +#endif /* LINECLIP_H */ diff --git a/lineclip/test_clip.c b/lineclip/test_clip.c new file mode 100644 index 000000000..109ef6828 --- /dev/null +++ b/lineclip/test_clip.c @@ -0,0 +1,176 @@ +/* test_clip.c - Console test program for line clipping algorithms */ +#include +#include +#include +#include "lineclip.h" + +/* Clip window: (400,300) to (1600,1400) */ +#define CLIP_X1 400 +#define CLIP_Y1 300 +#define CLIP_X2 1600 +#define CLIP_Y2 1400 + +typedef struct { + int16_t x1, y1, x2, y2; + int16_t ex1, ey1, ex2, ey2; + const char *name; +} TestCase; + +static const TestCase tests[] = { + /* Inside the clip window */ + { 800, 600, 1200, 1000, 800, 600, 1200, 1000, "T1 inside" }, + { 420, 320, 500, 400, 420, 320, 500, 400, "T2 near TL" }, + { 1580, 1380, 1600, 1400, 1580, 1380, 1600, 1400, "T3 near BR" }, + { 1000, 850, 1000, 850, 1000, 850, 1000, 850, "T4 point inside" }, + + /* Completely outside -- far away */ + { -500, -200, -300, 100, 0, 0, 0, 0, "T5 far left" }, + { 2200, 1500, 2500, 1800, 0, 0, 0, 0, "T6 far right" }, + { 800, -500, 1000, -200, 0, 0, 0, 0, "T7 far above" }, + { 1000, 1700, 1200, 2000, 0, 0, 0, 0, "T8 far below" }, + { -300, -300, 200, 100, 0, 0, 0, 0, "T9 far TL" }, + { 1800, 1600, 2200, 1700, 0, 0, 0, 0, "T10 far BR" }, + { -400, 1500, 100, 1800, 0, 0, 0, 0, "T11 far BL" }, + { 1800, -300, 2000, 100, 0, 0, 0, 0, "T12 far TR" }, + + /* Crossing from outside into window */ + { 200, 800, 600, 800, 400, 800, 600, 800, "T13 enter left" }, + { 1400, 800, 1800, 800, 1400, 800, 1600, 800, "T14 enter right" }, + { 1000, 100, 1000, 500, 1000, 300, 1000, 500, "T15 enter top" }, + { 1000, 1200, 1000, 1600, 1000, 1200, 1000, 1400, "T16 enter bottom" }, + + /* Diagonals crossing the window */ + { 100, 200, 1800, 1700, 381, 300, 1600, 1425, "T17 diag TL-BR" }, + { 100, 1700, 1800, 200, 381, 1400, 1600, 283, "T18 diag BL-TR" }, + { 1800, 200, 100, 1700, 1600, 283, 381, 1400, "T19 diag TR-BL" }, + { 1800, 1700, 100, 200, 1600, 1425, 381, 300, "T20 diag BR-TL" }, + + /* Very long lines crossing the world */ + { 0, 1000, 2000, 1000, 400, 1000, 1600, 1000, "T21 horiz world" }, + { 1000, 0, 1000, 2000, 1000, 300, 1000, 1400, "T22 vert world" }, + { 0, 0, 2000, 2000, 400, 400, 1600, 1600, "T23 full world diag" }, + { 2000, 0, 0, 2000, 1600, 400, 400, 1600, "T24 full world diag2" }, + + /* Shallow diagonals */ + { 200, 800, 1800, 810, 400, 801, 1600, 809, "T25 shallow up" }, + { 200, 900, 1800, 890, 400, 899, 1600, 891, "T26 shallow down" }, + + /* Steep diagonals */ + { 1000, 100, 1010, 1900, 1000, 300, 1001, 1400, "T27 steep right" }, + { 990, 100, 1000, 1900, 999, 300, 1000, 1400, "T28 steep left" }, + + /* Long rejects: parallel but far outside */ + { 200, 100, 350, 100, 0, 0, 0, 0, "T29 above parallel" }, + { 200, 1550, 350, 1550, 0, 0, 0, 0, "T30 below parallel" }, + { 100, 800, 100, 1100, 0, 0, 0, 0, "T31 left parallel" }, + { 1800, 800, 1800, 1100, 0, 0, 0, 0, "T32 right parallel" }, + + /* Extreme coordinates (within int16 range) */ + { 0, 1000, 0, 1001, 0, 0, 0, 0, "T33 x=0 tiny" }, + { 2000, 1000, 2000, 1001, 0, 0, 0, 0, "T34 x=2000 tiny" }, + { 1000, 0, 1001, 0, 0, 0, 0, 0, "T35 y=0 tiny" }, + { 1000, 2000, 1001, 2000, 0, 0, 0, 0, "T36 y=2000 tiny" }, + + /* Lines grazing the clip window edges */ + { 200, 300, 1800, 300, 400, 300, 1600, 300, "T37 graze top edge" }, + { 200, 1400, 1800, 1400, 400, 1400, 1600, 1400, "T38 graze bottom" }, + { 400, 100, 400, 1600, 400, 300, 400, 1400, "T39 graze left edge" }, + { 1600, 100, 1600, 1600, 1600, 300, 1600, 1400, "T40 graze right edge" }, + + /* Points on clip window corners */ + { 400, 300, 400, 300, 400, 300, 400, 300, "T41 pt TL corner" }, + { 1600, 300, 1600, 300, 1600, 300, 1600, 300, "T42 pt TR corner" }, + { 400, 1400, 400, 1400, 400, 1400, 400, 1400, "T43 pt BL corner" }, + { 1600, 1400, 1600, 1400, 1600, 1400, 1600, 1400, "T44 pt BR corner" }, + + /* Lines connecting two distant corners through the window */ + { 0, 0, 2000, 2000, 400, 400, 1600, 1600, "T45 origin to far BR" }, + { 2000, 0, 0, 2000, 1600, 400, 400, 1600, "T46 far TL to far BR" }, + + /* Short lines just outside each edge */ + { 390, 800, 395, 800, 0, 0, 0, 0, "T47 near left edge" }, + { 1605, 800, 1610, 800, 0, 0, 0, 0, "T48 near right edge" }, + { 1000, 295, 1005, 295, 0, 0, 0, 0, "T49 near top edge" }, + { 1000, 1405, 1005, 1405, 0, 0, 0, 0, "T50 near bottom edge" }, +}; +#define NUM_TESTS (sizeof(tests) / sizeof(tests[0])) + +static bool run_test(const TestCase *tc) +{ + int16_t cs_x1 = tc->x1, cs_y1 = tc->y1, cs_x2 = tc->x2, cs_y2 = tc->y2; + int16_t ci_x1 = tc->x1, ci_y1 = tc->y1, ci_x2 = tc->x2, ci_y2 = tc->y2; + int16_t el_x1 = tc->x1, el_y1 = tc->y1, el_x2 = tc->x2, el_y2 = tc->y2; + + bool cs_vis = clip_cohen_sutherland(&cs_x1, &cs_y1, &cs_x2, &cs_y2, + CLIP_X1, CLIP_Y1, CLIP_X2, CLIP_Y2); + bool ci_vis = clip_cohen_sutherland_int16(&ci_x1, &ci_y1, &ci_x2, &ci_y2, + CLIP_X1, CLIP_Y1, CLIP_X2, CLIP_Y2); + bool el_vis = clip_elite(&el_x1, &el_y1, &el_x2, &el_y2, + CLIP_X1, CLIP_Y1, CLIP_X2, CLIP_Y2); + + bool pass = true; + + /* CS-float is the reference. Check visibility agrees. */ + if (ci_vis != cs_vis || el_vis != cs_vis) { + printf(" VISIBILITY MISMATCH: CS=%d CSi=%d Elite=%d\n", + cs_vis, ci_vis, el_vis); + pass = false; + } + + /* CS-int16 must match CS-float within ±1 pixel. */ + if (cs_vis) { + if (ci_x1 < cs_x1 - 1 || ci_x1 > cs_x1 + 1 || + ci_y1 < cs_y1 - 1 || ci_y1 > cs_y1 + 1 || + ci_x2 < cs_x2 - 1 || ci_x2 > cs_x2 + 1 || + ci_y2 < cs_y2 - 1 || ci_y2 > cs_y2 + 1) { + printf(" CSi!=CS: (%d,%d)-(%d,%d) vs (%d,%d)-(%d,%d)\n", + ci_x1, ci_y1, ci_x2, ci_y2, cs_x1, cs_y1, cs_x2, cs_y2); + pass = false; + } + } + + /* Elite must match CS within ±2 pixels (integer truncation vs float rounding). */ + if (cs_vis) { + if (el_x1 < cs_x1 - 2 || el_x1 > cs_x1 + 2 || + el_y1 < cs_y1 - 2 || el_y1 > cs_y1 + 2 || + el_x2 < cs_x2 - 2 || el_x2 > cs_x2 + 2 || + el_y2 < cs_y2 - 2 || el_y2 > cs_y2 + 2) { + printf(" Elite!=CS: (%d,%d)-(%d,%d) vs (%d,%d)-(%d,%d)\n", + el_x1, el_y1, el_x2, el_y2, cs_x1, cs_y1, cs_x2, cs_y2); + pass = false; + } + /* Elite results MUST be strictly inside the clip window. */ + if (el_x1 < CLIP_X1 || el_x1 > CLIP_X2 || + el_y1 < CLIP_Y1 || el_y1 > CLIP_Y2 || + el_x2 < CLIP_X1 || el_x2 > CLIP_X2 || + el_y2 < CLIP_Y1 || el_y2 > CLIP_Y2) { + printf(" Elite OUTSIDE window: (%d,%d)-(%d,%d) window=(%d,%d)-(%d,%d)\n", + el_x1, el_y1, el_x2, el_y2, CLIP_X1, CLIP_Y1, CLIP_X2, CLIP_Y2); + pass = false; + } + } + + printf("%s: %s\n", tc->name, pass ? "PASS" : "FAIL"); + return pass; +} + +int main(void) +{ + int passed = 0, failed = 0; + + printf("Line clipping algorithm tests\n"); + printf("Clip window: (%d,%d)-(%d,%d)\n\n", CLIP_X1, CLIP_Y1, CLIP_X2, CLIP_Y2); + + for (int i = 0; i < (int)NUM_TESTS; i++) { + if (run_test(&tests[i])) { + passed++; + } else { + failed++; + } + } + + printf("\n%d passed, %d failed out of %d tests\n", + passed, failed, (int)NUM_TESTS); + + return failed > 0 ? 1 : 0; +}