so in the past, i wrote code around "concepts", like "this is the drawing code" and "this is the file system code". i would make structs or data types that would string together fields i THOUGHT were related and then write loose functions all over the place to process data in those fields. this was bad: it would always lead to tons of spaghettii code because the fields that were related by some human concept were NOT necessarily related in code or the way the data is ACTUALLY used, so every struct just kinda had to go everywhere, save for the very lowest functions. i think i did this because i hated object oriented programming (since i hated working with c#) and tried to avoid doing it, but i think that hatred was unfounded. i think i was just using objects wrong, and c# is just a bad language imo lol.
as i've been writing more c projects of larger and larger scope for the past several years (including a game engine from scratch, just for fun and of no real use), i've slowly been adopting a more object-oriented approach, but with a data-driven design rather than a conceptual one. i used to program for "humans" so to speak: i would design my classes in such a way where stuff that "conceptually" went together did. i never understood why this rarely worked out, and why it always become so difficult to work with, but it's because the way data flows in a program is not often tied to our real-life concepts. sure, putting all the "drawing" related data together sounds good, but some of that drawing stuff is only related to the UI, and some of it is only related to data conversion INTO the drawing primitives, and you can see how everything starts to get tangled together perhaps. "drawing" is now dependent on the data format, because the raw data is converted into drawing primitives, and now the drawing primitives might be used for SOME ui tasks.
now, instead of programming for a nebulous "future self" and how i might want things organized, i program for the COMPUTER and how the ACTUAL DATA flows. no system cares about the format of the encoded save data, just that they can get what they want out of it. the data encoding system doesn't care about the format the drawing primitives expect, it only cares about the way it is structured within itself. so, i designed a data encoding and decoding "class" (in c it's just a struct that holds related data to encoding and decoding, plus functions which operate on this struct) whose "api" so to speak is just that you can dump data packages into it and take them out. the data packages are NOT the drawing primitives, they're just the structs which represent the underlying encoded data. then, i wrote a "layer" class which handles drawing into a buffer. maybe this seems obvious to most, but i guess i had lost the plot on programming lol. the layer class accepts its own data format: drawing primitives it knows how to render. these are DIFFERENT than the ones i store into the data encoder. then there's a connecting class "lineconverter" which can translate stuff from the data encoder format to the layer format. so i have core classes which are self contained and don't depend on other modules, and connecting tissue to help link them together. it forms a well-defined tree which is far easier to reason about and work on. and yes, i get that this is like... what most people learn in college in like programming 202 or whatever, but tbh it took me many years to really get a feel for "what" to put into a class. you can really break up your code any way you want, and some ways to break up code end up as a mess. imagine sorting pots and pans: you could sort by shape, or by type, or by how they are used. maybe some ways take up less space in the cabinet but are harder to take stuff out or put stuff in, maybe other ways allow quick access but take up a ton of space and you can't put as many in the cabinet. other ways may make sense to you because you're currently cooking a lot of the same stuff and keep the oft-used pots and pans easy to reach, but in the future this may break down because now you're cooking different things. i think it takes some people (like myself) many years before you find a way that works "in general" and when/why you might want to break the rules.
anyway, point is that now that i have a wrapped up "layer" class, i have pulled all the weird rendering code out of the rest of the codebase and it's all in one place, so now i was able to make a layer be either software rendered or hardware rendered. i had to learn a ton about how vram textures work (which was EXTREMELY difficult to find!!!) but now that a layer can either be software or hardware rendered, and you can convert between the two on the fly, it should be easy to implement stuff like flood fill, as i can just temporarily convert the layer you're filling to software, query all the pixels as needed, output the appropriate draw commands to fill it, then convert it back to hardware for fast rendering. but if i get my way, i might be able to make all drawing be software rendered, which lets me do basically anything, and i just convert the layers into hardware as needed.
also with this new codebase, it's so much easier to write tests, so i now have a test app. it's only light testing, but it should prevent major bugs from popping up unexpectedly, or at the VERY least prevent bugs from "reappearing" once they are fixed. once i have a test for some edge case, i will always know if some change i make causes it to appear. i usually like writing tests for code and do it often, but i felt i was unable to foro this codebase for the longest time. it also helps me with code design, as keeping code "testable" also keeps it modular and easier to reason about. if something is difficult to test, it's also difficult to reason about.
anyway, if you read all that, thank you! just been working on this so much for the past several days and wanted to get it off my chest.