You've already forked flecs_tests
History, spatial grid and optimizations
This commit is contained in:
254
src/history.h
Normal file
254
src/history.h
Normal file
@@ -0,0 +1,254 @@
|
||||
#ifndef HISTORY_H
|
||||
#define HISTORY_H
|
||||
|
||||
#include "api.h"
|
||||
|
||||
// Each property kind we can undo/redo independently
|
||||
typedef enum {
|
||||
HIST_POSITION,
|
||||
HIST_SCALE,
|
||||
HIST_ROTATION,
|
||||
HIST_COLOR,
|
||||
} hist_prop_t;
|
||||
|
||||
// One property change on one shape (old → new)
|
||||
typedef struct hist_change_t {
|
||||
int shape_index;
|
||||
hist_prop_t prop;
|
||||
float old_val[4];
|
||||
float new_val[4];
|
||||
} hist_change_t;
|
||||
|
||||
// A history entry is one or more changes batched together.
|
||||
// Single-property edits = 1 change. Whole-selection edits = N changes.
|
||||
typedef struct hist_entry_t {
|
||||
hist_change_t *changes;
|
||||
int count;
|
||||
} hist_entry_t;
|
||||
|
||||
#define HIST_MAX 64
|
||||
|
||||
typedef struct history_t {
|
||||
hist_entry_t entries[HIST_MAX];
|
||||
int count;
|
||||
int current; // index of last applied entry, -1 = initial state
|
||||
|
||||
// Pending edit session (one ImGui widget interaction)
|
||||
bool capturing;
|
||||
int pending_shape_idx;
|
||||
hist_prop_t pending_prop;
|
||||
float pending_old[4];
|
||||
} history_t;
|
||||
|
||||
// -- internal helpers --
|
||||
|
||||
/**
|
||||
* Read the current value of a single property from a shape.
|
||||
*
|
||||
* @param s shape to read from
|
||||
* @param prop which property (HIST_POSITION, HIST_SCALE, etc.)
|
||||
* @param out receives the value, zero-padded to 4 floats
|
||||
*/
|
||||
static void hist_read_prop(shape_t *s, hist_prop_t prop, float out[4]) {
|
||||
memset(out, 0, sizeof(float[4]));
|
||||
switch (prop) {
|
||||
case HIST_POSITION: out[0] = s->cx; out[1] = s->cy; break;
|
||||
case HIST_SCALE: out[0] = s->sx; out[1] = s->sy; break;
|
||||
case HIST_ROTATION: out[0] = s->rotation; break;
|
||||
case HIST_COLOR: memcpy(out, s->uniform.base_color, sizeof(float[4])); break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a value to a single property of a shape. Does NOT regenerate buffers.
|
||||
*
|
||||
* @param s shape to modify in-place
|
||||
* @param prop which property to set
|
||||
* @param val new value (4 floats, zero-padded for smaller properties)
|
||||
*/
|
||||
static void hist_apply_prop(shape_t *s, hist_prop_t prop, const float val[4]) {
|
||||
switch (prop) {
|
||||
case HIST_POSITION: s->cx = val[0]; s->cy = val[1]; break;
|
||||
case HIST_SCALE: s->sx = val[0]; s->sy = val[1]; break;
|
||||
case HIST_ROTATION: s->rotation = val[0]; break;
|
||||
case HIST_COLOR: memcpy(s->uniform.base_color, val, sizeof(float[4])); break;
|
||||
}
|
||||
}
|
||||
|
||||
// -- history API --
|
||||
|
||||
/**
|
||||
* Zero-initialize the history stack. Call once during app init.
|
||||
*
|
||||
* @param h history to initialize
|
||||
*/
|
||||
static void history_init(history_t *h) {
|
||||
memset(h, 0, sizeof(*h));
|
||||
h->current = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Free all heap memory held by the history stack. Call during app shutdown.
|
||||
*
|
||||
* @param h history to destroy
|
||||
*/
|
||||
static void history_destroy(history_t *h) {
|
||||
for (int i = 0; i < h->count; i++) {
|
||||
if (h->entries[i].changes) FREE(h->entries[i].changes);
|
||||
}
|
||||
memset(h, 0, sizeof(*h));
|
||||
h->current = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a completed entry onto the stack, discarding any redo branch.
|
||||
* Takes ownership of entry.changes (must be heap-allocated with ALLOC).
|
||||
* Used internally by begin_edit/end_edit, or directly for batch edits.
|
||||
*
|
||||
* @param h history stack
|
||||
* @param entry entry to push (changes array is consumed, not copied)
|
||||
*/
|
||||
static void history_push_entry(history_t *h, hist_entry_t entry) {
|
||||
while (h->count > h->current + 1) {
|
||||
h->count--;
|
||||
if (h->entries[h->count].changes) {
|
||||
FREE(h->entries[h->count].changes);
|
||||
h->entries[h->count].changes = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
if (h->count >= HIST_MAX) {
|
||||
if (h->entries[0].changes) FREE(h->entries[0].changes);
|
||||
memmove(&h->entries[0], &h->entries[1],
|
||||
(h->count - 1) * sizeof(hist_entry_t));
|
||||
h->count--;
|
||||
h->current--;
|
||||
}
|
||||
|
||||
h->entries[h->count] = entry;
|
||||
h->count++;
|
||||
h->current = h->count - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin capturing an edit session. Snapshots the current value of one property.
|
||||
* If a prior session is still open (e.g. user switched widgets in the same frame),
|
||||
* it is finalized and pushed first.
|
||||
* Call when igIsItemActivated() is true after an ImGui widget.
|
||||
*
|
||||
* @param h history stack
|
||||
* @param shapes the shapes vector (used to read current values)
|
||||
* @param shape_idx index of the shape being edited
|
||||
* @param prop which property is about to change
|
||||
*/
|
||||
static void history_begin_edit(history_t *h, vector_t *shapes,
|
||||
int shape_idx, hist_prop_t prop) {
|
||||
if (h->capturing) {
|
||||
shape_t *s = (shape_t*) vec_get(shapes, h->pending_shape_idx);
|
||||
float new_val[4];
|
||||
hist_read_prop(s, h->pending_prop, new_val);
|
||||
if (memcmp(h->pending_old, new_val, sizeof(float[4])) != 0) {
|
||||
hist_change_t change = {
|
||||
.shape_index = h->pending_shape_idx,
|
||||
.prop = h->pending_prop,
|
||||
};
|
||||
memcpy(change.old_val, h->pending_old, sizeof(float[4]));
|
||||
memcpy(change.new_val, new_val, sizeof(float[4]));
|
||||
hist_entry_t entry = { .changes = NULL, .count = 1 };
|
||||
entry.changes = (hist_change_t*) ALLOC(sizeof(hist_change_t));
|
||||
*entry.changes = change;
|
||||
history_push_entry(h, entry);
|
||||
}
|
||||
h->capturing = false;
|
||||
}
|
||||
|
||||
h->capturing = true;
|
||||
h->pending_shape_idx = shape_idx;
|
||||
h->pending_prop = prop;
|
||||
shape_t *s = (shape_t*) vec_get(shapes, shape_idx);
|
||||
hist_read_prop(s, prop, h->pending_old);
|
||||
}
|
||||
|
||||
/**
|
||||
* End the current edit session and push an entry if the value changed.
|
||||
* Safe to call when no session is active (no-op).
|
||||
* Call when igIsAnyItemActive() transitions from true to false.
|
||||
*
|
||||
* @param h history stack
|
||||
* @param shapes the shapes vector (used to read final values)
|
||||
*/
|
||||
static void history_end_edit(history_t *h, vector_t *shapes) {
|
||||
if (!h->capturing) return;
|
||||
|
||||
shape_t *s = (shape_t*) vec_get(shapes, h->pending_shape_idx);
|
||||
float new_val[4];
|
||||
hist_read_prop(s, h->pending_prop, new_val);
|
||||
|
||||
if (memcmp(h->pending_old, new_val, sizeof(float[4])) != 0) {
|
||||
hist_change_t change = {
|
||||
.shape_index = h->pending_shape_idx,
|
||||
.prop = h->pending_prop,
|
||||
};
|
||||
memcpy(change.old_val, h->pending_old, sizeof(float[4]));
|
||||
memcpy(change.new_val, new_val, sizeof(float[4]));
|
||||
hist_entry_t entry = { .changes = NULL, .count = 1 };
|
||||
entry.changes = (hist_change_t*) ALLOC(sizeof(hist_change_t));
|
||||
*entry.changes = change;
|
||||
history_push_entry(h, entry);
|
||||
}
|
||||
h->capturing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply every change in an entry to the shapes vector and regenerate buffers.
|
||||
*
|
||||
* @param entry the history entry to apply
|
||||
* @param shapes the shapes vector to modify
|
||||
* @param forward true to use new_val (redo), false to use old_val (undo)
|
||||
*/
|
||||
static void history_apply_entry(hist_entry_t *entry, vector_t *shapes, bool forward) {
|
||||
for (int i = 0; i < entry->count; i++) {
|
||||
hist_change_t *c = &entry->changes[i];
|
||||
if (c->shape_index >= shapes->count) continue;
|
||||
shape_t *s = (shape_t*) vec_get(shapes, c->shape_index);
|
||||
hist_apply_prop(s, c->prop, forward ? c->new_val : c->old_val);
|
||||
shape_regenerate(s);
|
||||
shape_set_state(s, s->hovered, s->selected);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo the most recent history entry.
|
||||
*
|
||||
* @param h history stack
|
||||
* @param shapes the shapes vector to revert
|
||||
* @param selected_count out-parameter for updated selection count (currently passed through)
|
||||
* @return true if state was changed, false if nothing to undo
|
||||
*/
|
||||
static bool history_undo(history_t *h, vector_t *shapes, int *selected_count) {
|
||||
if (h->current < 0) return false;
|
||||
|
||||
history_apply_entry(&h->entries[h->current], shapes, false);
|
||||
h->current--;
|
||||
(void)selected_count;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redo the next history entry.
|
||||
*
|
||||
* @param h history stack
|
||||
* @param shapes the shapes vector to advance
|
||||
* @param selected_count out-parameter (currently passed through)
|
||||
* @return true if state was changed, false if nothing to redo
|
||||
*/
|
||||
static bool history_redo(history_t *h, vector_t *shapes, int *selected_count) {
|
||||
if (h->current + 1 >= h->count) return false;
|
||||
|
||||
h->current++;
|
||||
history_apply_entry(&h->entries[h->current], shapes, true);
|
||||
(void)selected_count;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user