/* * Copyright (C) 2018 Vito Caputo - * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License version 3 as published * by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include #include "list.h" #include "stage.h" typedef struct v2f_t { float x, y; } v2f_t; typedef struct aabb_t { v2f_t min, max; } aabb_t; typedef struct stage_cached_t { int width, height; SDL_Texture *texture; } stage_cached_t; struct stage_node_t { list_head_t nodes; char name[STAGE_NODE_NAME_MAX]; stage_cached_t cached; aabb_t aabb; /* node coordinates expressed in the space of -1.0...1.0 */ v2f_t position; /* position of node, optionally used to move aabb as a whole */ float alpha; /* alpha for the texture when composited */ double angle; /* angle for the texture when composited */ unsigned active:1; /* node is active */ unsigned locked:1; /* node is locked */ stage_render_func_t render; /* render object into a texture */ stage_free_func_t free; /* free object */ void *object; /* object */ }; struct stage_t { list_head_t layers[STAGE_LAYERS_MAX]; SDL_Renderer *renderer; /* target renderer */ float aspect_ratio; /* width/height ratio of stage, -1.0...1.0 range of node coordinates map to this ratio. */ }; /* minimally initialize a node to carry an object */ static void node_init(stage_node_t *node, const char *name, void *object, stage_render_func_t render_func, stage_free_func_t free_func) { strncpy(node->name, name, sizeof(node->name)); node->render = render_func; node->free = free_func; node->object = object; } /* allocate a node, no list manipulation occurs, this purely creates a node in isolation and assigns its associated name, object and functions */ static stage_node_t * node_new(const char *name, void *object, stage_render_func_t render_func, stage_free_func_t free_func) { stage_node_t *node; node = calloc(1, sizeof(stage_node_t)); if (!node) return NULL; node_init(node, name, object, render_func, free_func); return node; } /* free a node, no list manipulation occurs, this is purely cleanup of the node and its associated texture and object */ static void node_free(stage_node_t *node) { if (node->free) node->free(node->object); if (node->cached.texture) SDL_DestroyTexture(node->cached.texture); free(node); } /* returns a new node installed at the specified layer */ stage_node_t * stage_node_new(stage_t *stage, int layer, const char *name, void *object, stage_render_func_t render_func, stage_free_func_t free_func) { stage_node_t *node; node = node_new(name, object, render_func, free_func); if (!node) return NULL; list_add_tail(&node->nodes, &stage->layers[layer]); return node; } /* replace a given node's object only - retain render/free hooks and everything else, invalidates cache - DOES NOT FREE OLD OBJECT */ /* FIXME: this is being used as a kludge to swap frames in an svg - * eliminate this bullshit and add support for animations or at least frames, * the stage really shouldn't have anything to do with it, we can dick with * the svg behind its back. I just don't currently have the helpers for creating * svg nodes actually giving out the bare svg handle currently, so the stage_node_t * is all I have to dick with at the moment. Next time! This really dicks with * the resource lifecycle - if the stage frees the node, which object was active * at the time and which object still needs to be freed because it wasn't in * the stage when the node was killed? It's totally shitty, and I'm aware. */ void stage_node_set_object(const stage_t *stage, stage_node_t *node, void *object) { if (node->cached.texture) { SDL_DestroyTexture(node->cached.texture); node->cached.texture = NULL; } node->object = object; } /* FIXME: GROSS fuck me */ void stage_node_get_object(const stage_t *stage, stage_node_t *node, void **res_object) { *res_object = node->object; } /* replaces a given node's object-related properties but otherwise keeping the existing state */ void stage_node_replace(const stage_t *stage, stage_node_t *node, const char *name, void *object, stage_render_func_t render_func, stage_free_func_t free_func) { if (node->free) node->free(node->object); if (node->cached.texture) { SDL_DestroyTexture(node->cached.texture); node->cached.texture = NULL; } node_init(node, name, object, render_func, free_func); } /* frees a given node removing it from the stage */ stage_node_t * stage_node_free(stage_t *stage, stage_node_t *node) { if (node) { list_del(&node->nodes); node_free(node); } return NULL; } /* set the alpha on a node */ void stage_node_set_alpha(const stage_t *stage, stage_node_t *node, float alpha) { /* TODO: this should probably mark something dirty for when we stop always recomposing everything */ node->alpha = alpha; } /* set the aabb for a node */ void stage_node_set_aabb(const stage_t *stage, stage_node_t *node, const aabb_t *aabb) { node->aabb = *aabb; } /* get the aabb for a node */ void stage_node_get_aabb(const stage_t *stage, stage_node_t *node, aabb_t *res_aabb) { *res_aabb = node->aabb; } /* set the position for a node */ void stage_node_set_position(const stage_t *stage, stage_node_t *node, float x, float y) { node->position.x = x; node->position.y = y; } /* get the position for a node */ void stage_node_get_position(const stage_t *stage, stage_node_t *node, float *res_x, float *res_y) { *res_x = node->position.x; *res_y = node->position.y; } /* set a node to active (participates in rendering) */ void stage_node_set_active(const stage_t *stage, stage_node_t *node) { node->active = 1; } /* set a node to inactive (doesn't participate in rendering) */ void stage_node_set_inactive(const stage_t *stage, stage_node_t *node) { /* TODO: should this discard the potentially cached texture? */ node->active = 0; } /* set a node to locked (doesn't get freed by clears) */ void stage_node_set_locked(const stage_t *stage, stage_node_t *node) { node->locked = 1; } /* set a node to unlocked (default, gets freed by clears) */ void stage_node_set_unlocked(const stage_t *stage, stage_node_t *node) { node->locked = 0; } /* set a node's layer */ void stage_node_set_layer(stage_t *stage, stage_node_t *node, int layer) { /* TODO: assert layer sanity */ list_del(&node->nodes); list_add_tail(&node->nodes, &stage->layers[layer]); } void stage_node_set_angle(const stage_t *stage, stage_node_t *node, double angle) { node->angle = angle; } void stage_node_get_angle(const stage_t *stage, stage_node_t *node, double *res_angle) { *res_angle = node->angle; } /* create a new stage with the given aspect ratio */ stage_t * stage_new(SDL_Renderer *renderer, float aspect_ratio) { stage_t *stage; int i; stage = calloc(1, sizeof(stage_t)); if (!stage) return NULL; for (i = 0; i < STAGE_LAYERS_MAX; i++) INIT_LIST_HEAD(&stage->layers[i]); stage->renderer = renderer; stage->aspect_ratio = aspect_ratio; return stage; } static void _stage_clear(stage_t *stage, int force) { stage_node_t *node, *_node; int i; for (i = 0; i < STAGE_LAYERS_MAX; i++) { list_for_each_entry_safe(node, _node, &stage->layers[i], nodes) { if (force || !node->locked) stage_node_free(stage, node); } } } /* free everything in the stage, but keep the stage around */ /* probably good hygiene to clear everything when there's no intention of * reusing nodes at the start of a new context, in case something is left * around unintentionally. */ void stage_clear(stage_t *stage) { _stage_clear(stage, 0); } /* free the supplied stage and all associated textures and objects */ stage_t * stage_free(stage_t *stage) { if (stage) { _stage_clear(stage, 1); free(stage); } return NULL; } /* fit a stage to the supplied dimensions, returns the fitted dimensions in the result pointers */ /* the idea is this can be used in a window resize hook to enforce the stage's aspect ratio */ void stage_fit(const stage_t *stage, int width, int height, int *res_width, int *res_height) { float full_width = stage->aspect_ratio * ((float)height); if (full_width == width) { /* perfect fit */ *res_width = width; *res_height = height; } else if (full_width > width) { /* height is too large */ *res_height = (1.0f / stage->aspect_ratio) * ((float)width); *res_width = width; } else { /* width is too large */ *res_width = full_width; *res_height = height; } } static void aabb_to_rect(const aabb_t *aabb, const v2f_t *pos, const SDL_Rect *stage_rect, SDL_Rect *res_rect) { float half_w = ((float)stage_rect->w) * .5f, half_h = ((float)stage_rect->h) * .5f; /* FIXME silly to recompute this repeatedly... */ res_rect->x = stage_rect->x; res_rect->y = stage_rect->y; res_rect->x += ((float)aabb->min.x + pos->x) * half_w + half_w; res_rect->y += stage_rect->h - (((float)aabb->max.y + pos->y) * half_h + half_h); res_rect->w = (aabb->max.x - aabb->min.x) * half_w; res_rect->h = (aabb->max.y - aabb->min.y) * half_h; } static void render_nodes(list_head_t *head, SDL_Renderer *renderer, SDL_Rect *dest_rect) { stage_node_t *node; list_for_each_entry(node, head, nodes) { SDL_Rect node_rect; if (!node->active) continue; /* scale the node's aabb stage coordinates to destination renderer coordinates */ aabb_to_rect(&node->aabb, &node->position, dest_rect, &node_rect); /* if we have a cached texture, see if the dimensions changed */ if (node->cached.texture && (node_rect.w != node->cached.width || node_rect.h != node->cached.height)) { SDL_DestroyTexture(node->cached.texture); node->cached.texture = NULL; } /* Tell the node to draw its cached texture */ /* The draw function may either use the existing texture when non-NULL or destroy and create a new one, * whatever is most convenient for it. * If the texture is NULL, a create & render must be performed, and 1 returned. * If the texture is non-NULL, and there's no animation or anything to be done, 0 may simply be returned. * Otherwise, if supplied a non-NULL texture and it's modified, 1 must be returned. */ /* XXX: at this time, the return value is ignored (it always composites the stage even when unchanged) */ node->render(renderer, node->object, node_rect.w, node_rect.h, &node->cached.texture); node->cached.width = node_rect.w; node->cached.height = node_rect.h; SDL_SetTextureAlphaMod(node->cached.texture, ((float)node->alpha * 255.0f)); /* copy the texture to renderer */ if (node->angle == 0) SDL_RenderCopy(renderer, node->cached.texture, NULL, &node_rect); else SDL_RenderCopyEx(renderer, node->cached.texture, NULL, &node_rect, node->angle, NULL, SDL_FLIP_NONE); } } /* XXX: ANOTHER KLUDGE FIXME */ /* we shouldn't need to let this out of the stage, but for the mouse aiming * it was easier this way for now. */ void stage_get_output_size(stage_t *stage, int *res_width, int *res_height) { SDL_GetRendererOutputSize(stage->renderer, res_width, res_height); } /* render the supplied stage into the supplied renderer */ /* the aspect_ratio of the stage will be enforced in filling the logical size * of the renderer. Unless the aspect ratio matches precisely, stage_render() * will center the rendering and leave blank borders on the edges left empty. * The renderer may be sized precisely by first calling stage_fit(). */ void stage_render(stage_t *stage) { SDL_Rect rect; int i, width, height; /* XXX TODO: investigate renderer viewports and scale factors */ SDL_GetRendererOutputSize(stage->renderer, &width, &height); stage_fit(stage, width, height, &rect.w, &rect.h); rect.x = (width - rect.w) / 2; rect.y = (height - rect.h) / 2; SDL_RenderClear(stage->renderer); /* TODO: handle -1 on error */ for (i = 0; i < STAGE_LAYERS_MAX; i++) render_nodes(&stage->layers[i], stage->renderer, &rect); } /* lookup a node from a name */ stage_node_t * stage_node_lookup_name(const stage_t *stage, const char *name) { stage_node_t *node; int i; for (i = 0; i < STAGE_LAYERS_MAX; i++) { list_for_each_entry(node, &stage->layers[i], nodes) { if (!strncmp(node->name, name, sizeof(node->name))) return node; } } return NULL; } /* lookup a node from a cartesian renderer coordinate */ stage_node_t * stage_node_lookup_cartesian(const stage_t *stage, int x, int y) { stage_node_t *node; SDL_Rect rect; int i, width, height; /* FIXME: copy-pasta with render, factor out */ SDL_GetRendererOutputSize(stage->renderer, &width, &height); stage_fit(stage, width, height, &rect.w, &rect.h); rect.x = (width - rect.w) / 2; rect.y = (height - rect.h) / 2; /* this is currently very simple: find the first top-most node * including the specified coordinate. */ for (i = STAGE_LAYERS_MAX - 1; i >= 0; i--) { list_for_each_entry(node, &stage->layers[i], nodes) { SDL_Rect node_rect; if (!node->active) continue; aabb_to_rect(&node->aabb, &node->position, &rect, &node_rect); if (x >= node_rect.x && x < node_rect.x + node_rect.w && y >= node_rect.y && y < node_rect.y + node_rect.h) return node; } } return NULL; }