Draw: implement the BASIC 7.0 graphics verbs

GRAPHIC, COLOR, DRAW, BOX, CIRCLE, PAINT, SCALE, SSHAPE, GSHAPE and LOCATE, all
against the akbasic_GraphicsBackend record rather than akgl_draw_* directly, so
src/runtime_graphics.c includes no SDL and the whole group is testable in a build
with no SDL on the machine.

The reference lists every one of these as unimplemented, so the semantics come
from Commodore BASIC 7.0 rather than from a port, and four places where a modern
renderer cannot do what a C128 did are recorded in TODO.md section 5 rather than
silently substituted:

- CIRCLE is drawn as a polygon of inc-degree segments and akgl_draw_circle is
  deliberately unused. 7.0's CIRCLE takes two radii, an arc range and a rotation,
  so the primitive could serve only the fully-defaulted call, and a shape that
  changed character depending on whether the radii happened to be equal would be
  worse than one uniformly a polygon.
- SSHAPE puts a SHAPE:<n> handle in the string variable rather than the pixels,
  because a value's string is a fixed 256 bytes and a region is a device surface.
  GSHAPE refuses a string without that prefix instead of parsing whatever digits
  it finds and pasting an unrelated slot.
- BOX fills on a negative angle; 7.0 puts the fill flag after the rotation, which
  would make a filled box a seventh argument.
- GRAPHIC stores its mode and honours only the one consequence that means
  anything here -- mode 0 is text -- while still refusing an out-of-range mode,
  since that is a typo worth catching.

PAINT surfaces the flood fill's AKERR_OUTOFBOUNDS as an error rather than
success. The device gives up when its span stack runs out having filled *part* of
the region, and a program that cannot tell that happened cannot recover from it.
Note the shape of that handler: HANDLE sets handled = true on the context, so a
FAIL_RETURN from inside the HANDLE block hands the caller something already
marked handled, whose FINISH_LOGIC then declines to pass it up and releases it --
the error disappears and PAINT reports success. Flag inside the block, raise
after FINISH.

COLOR, LOCATE and SCALE need no device on purpose, so a program can set itself up
before a host has lent it a renderer.

Adds a second golden corpus under tests/language/. The corpus in
deps/basicinterpret is a submodule and nothing here may add files to it, but
goal 2's new verbs still need the .bas/.txt half of their coverage. Registered
under local_ so a failure names which corpus it came from. What it can cover is
limited -- these verbs draw rather than print -- so the behaviour that reaches a
device is asserted against tests/mockdevice.h instead.

65/65 ctest, clean under -Wall -Wextra, doxygen clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
2026-07-31 08:17:48 -04:00
parent f269a97c06
commit a6ac2ee9e8
15 changed files with 1264 additions and 0 deletions

View File

@@ -40,6 +40,25 @@ static const akbasic_Color PALETTE[17] = {
{ 0x9f, 0x9f, 0x9f, 0xff } /* 16 -- light grey */
};
/**
* @brief What each COLOR source is bound to before a program says otherwise.
*
* A C128 powers on with a blue screen and light blue text. That is not
* reproduced: a graphics screen defaulting to blue-on-blue makes an unset COLOR
* look like a bug in the interpreter, so the defaults here are the readable ones.
* The choice is worth naming rather than hiding, since it is the one place these
* tables are not a transcription.
*/
static const int SOURCE_DEFAULTS[AKBASIC_COLOR_SOURCES] = {
1, /* 0 -- 40-column background: black */
2, /* 1 -- 40-column foreground: white */
3, /* 2 -- multicolor 1: red */
4, /* 3 -- multicolor 2: cyan */
1, /* 4 -- 40-column border: black */
2, /* 5 -- character color: white */
1 /* 6 -- 80-column background: black */
};
akerr_ErrorContext *akbasic_graphics_palette(int index, akbasic_Color *dest)
{
PREPARE_ERROR(errctx);
@@ -51,3 +70,36 @@ akerr_ErrorContext *akbasic_graphics_palette(int index, akbasic_Color *dest)
*dest = PALETTE[index];
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_graphics_state_init(akbasic_GraphicsState *obj)
{
PREPARE_ERROR(errctx);
int source = 0;
FAIL_ZERO_RETURN(errctx, (obj != NULL), AKERR_NULLPOINTER,
"NULL graphics state in init");
obj->mode = 0;
obj->x = 0.0;
obj->y = 0.0;
obj->scaling = false;
obj->xmax = (double)AKBASIC_GRAPHICS_WIDTH;
obj->ymax = (double)AKBASIC_GRAPHICS_HEIGHT;
for ( source = 0; source < AKBASIC_COLOR_SOURCES; source++ ) {
obj->source[source] = SOURCE_DEFAULTS[source];
}
SUCCEED_RETURN(errctx);
}
akerr_ErrorContext *akbasic_graphics_source_color(akbasic_GraphicsState *obj, int source, akbasic_Color *dest)
{
PREPARE_ERROR(errctx);
FAIL_ZERO_RETURN(errctx, (obj != NULL && dest != NULL), AKERR_NULLPOINTER,
"NULL argument in source_color");
FAIL_ZERO_RETURN(errctx, (source >= 0 && source < AKBASIC_COLOR_SOURCES),
AKBASIC_ERR_BOUNDS,
"Color source %d out of range (0 to %d)",
source, AKBASIC_COLOR_SOURCES - 1);
PASS(errctx, akbasic_graphics_palette(obj->source[source], dest));
SUCCEED_RETURN(errctx);
}