Files
Tahgibu/src/document/document.c
T
2026-10-24 12:01:52 -03:00

344 lines
15 KiB
C

/* Modelo de documento: creación, destrucción, consultas y mutadores de bajo nivel. */
#include "doc_internal.h"
#include "../history/history_internal.h"
uint32_t doc_default_duration_ms(int fps) {
uint32_t ms = (uint32_t)((1000 + fps / 2) / fps);
if (ms < TGB_MIN_FRAME_MS) ms = TGB_MIN_FRAME_MS;
return ms;
}
uint32_t doc_new_id(tgb_document *doc) { return doc->next_id++; }
void tgb_emit_simple(tgb_document *doc, tgb_event_type type) {
if (!doc || !doc->ctx) return;
tgb_event ev = {type, doc, 0, 0, {0, 0, 0, 0}, NULL};
tgb_emit(doc->ctx, &ev);
}
void doc_touch(tgb_document *doc) {
doc->revision++;
tgb_emit_simple(doc, TGB_EVENT_DOCUMENT_CHANGED);
}
tgb_status tgb_document_create(tgb_context *ctx, int width, int height, int fps, tgb_document **out) {
if (!ctx || !out) return tgb_fail(TGB_ERR_INVALID_ARG, "tgb_document_create: argumento nulo");
*out = NULL;
if (width < 1 || height < 1 || width > TGB_MAX_DIMENSION || height > TGB_MAX_DIMENSION)
return tgb_fail(TGB_ERR_RANGE, "Dimensiones no válidas %dx%d (1..%d)", width, height, TGB_MAX_DIMENSION);
if (fps < TGB_MIN_FPS || fps > TGB_MAX_FPS)
return tgb_fail(TGB_ERR_RANGE, "fps %d fuera de rango (%d..%d)", fps, TGB_MIN_FPS, TGB_MAX_FPS);
tgb_document *doc = calloc(1, sizeof *doc);
if (!doc) return tgb_fail(TGB_ERR_NOMEM, "Sin memoria para el documento");
doc->ctx = ctx;
doc->width = width;
doc->height = height;
doc->fps = fps;
doc->next_id = 1;
doc->next_rpoint_id = 1;
doc->history = hist_create();
doc->layers = calloc(4, sizeof *doc->layers);
doc->frames = calloc(4, sizeof *doc->frames);
tgb_canvas **cels = calloc(1, sizeof *cels);
if (!doc->history || !doc->layers || !doc->frames || !cels) {
free(cels);
tgb_document_destroy(doc);
return tgb_fail(TGB_ERR_NOMEM, "Sin memoria para el documento");
}
doc->caplayers = 4;
doc->capframes = 4;
doc->layers[0].id = doc_new_id(doc);
tgb_strlcpy(doc->layers[0].name, "Capa 1", TGB_NAME_MAX);
doc->layers[0].visible = true;
doc->layers[0].opacity = 255;
doc->nlayers = 1;
doc->frames[0].id = doc_new_id(doc);
doc->frames[0].duration_ms = doc_default_duration_ms(fps);
doc->frames[0].cels = cels;
doc->nframes = 1;
tgb_history_adapt(doc); /* límites iniciales según la máquina (no es fatal si falla) */
*out = doc;
return TGB_OK;
}
void doc_clear_extra(tgb_document *doc) {
for (size_t i = 0; i < doc->nextra; i++) { free(doc->extra[i].key); free(doc->extra[i].raw); }
free(doc->extra);
doc->extra = NULL;
doc->nextra = 0;
}
void doc_frame_free_cels(tgb_canvas **cels, size_t n) {
if (!cels) return;
for (size_t i = 0; i < n; i++) tgb_canvas_release(cels[i]);
free(cels);
}
void tgb_document_destroy(tgb_document *doc) {
if (!doc) return;
if (doc->edit.active) { tgb_canvas_release(doc->edit.base); }
hist_destroy(doc->history); /* antes que las celdas: las operaciones pueden retener celdas */
for (size_t i = 0; i < doc->nframes; i++) doc_frame_free_cels(doc->frames[i].cels, doc->nlayers);
free(doc->frames);
free(doc->layers);
for (size_t i = 0; i < doc->nrpoints; i++) {
if (doc->rpoints[i].compatible) state_free(&doc->rpoints[i].state);
free(doc->rpoints[i].raw_json);
}
free(doc->rpoints);
doc_clear_extra(doc);
free(doc->path);
free(doc);
}
/* ---- consultas ---- */
tgb_context *tgb_document_context(const tgb_document *d) { return d ? d->ctx : NULL; }
int tgb_document_width(const tgb_document *d) { return d ? d->width : 0; }
int tgb_document_height(const tgb_document *d) { return d ? d->height : 0; }
int tgb_document_fps(const tgb_document *d) { return d ? d->fps : 0; }
uint64_t tgb_document_revision(const tgb_document *d) { return d ? d->revision : 0; }
bool tgb_document_dirty(const tgb_document *d) { return d && d->revision != d->saved_revision; }
void tgb_document_mark_saved(tgb_document *d) { if (d) d->saved_revision = d->revision; }
const char *tgb_document_path(const tgb_document *d) { return d ? d->path : NULL; }
bool tgb_document_is_consistent(const tgb_document *d) { return d && !d->inconsistent; }
tgb_status tgb_document_set_path(tgb_document *doc, const char *path) {
if (!doc) return tgb_fail(TGB_ERR_INVALID_ARG, "Documento nulo");
char *copy = NULL;
if (path) { copy = tgb_strdup(path); if (!copy) return tgb_fail(TGB_ERR_NOMEM, "Sin memoria"); }
free(doc->path);
doc->path = copy;
return TGB_OK;
}
const char *tgb_document_extra_field(const tgb_document *doc, const char *key) {
if (!doc || !key) return NULL;
for (size_t i = 0; i < doc->nextra; i++) if (!strcmp(doc->extra[i].key, key)) return doc->extra[i].raw;
return NULL;
}
size_t tgb_frame_count(const tgb_document *d) { return d ? d->nframes : 0; }
uint32_t tgb_frame_id(const tgb_document *d, size_t i) { return (d && i < d->nframes) ? d->frames[i].id : 0; }
uint32_t tgb_frame_duration_ms(const tgb_document *d, size_t i) { return (d && i < d->nframes) ? d->frames[i].duration_ms : 0; }
size_t tgb_layer_count(const tgb_document *d) { return d ? d->nlayers : 0; }
uint32_t tgb_layer_id(const tgb_document *d, size_t i) { return (d && i < d->nlayers) ? d->layers[i].id : 0; }
const char *tgb_layer_name(const tgb_document *d, size_t i) { return (d && i < d->nlayers) ? d->layers[i].name : ""; }
bool tgb_layer_visible(const tgb_document *d, size_t i) { return d && i < d->nlayers && d->layers[i].visible; }
bool tgb_layer_locked(const tgb_document *d, size_t i) { return d && i < d->nlayers && d->layers[i].locked; }
uint8_t tgb_layer_opacity(const tgb_document *d, size_t i) { return (d && i < d->nlayers) ? d->layers[i].opacity : 0; }
size_t doc_find_frame(const tgb_document *d, uint32_t id) {
for (size_t i = 0; i < d->nframes; i++) if (d->frames[i].id == id) return i;
return NPOS;
}
size_t doc_find_layer(const tgb_document *d, uint32_t id) {
for (size_t i = 0; i < d->nlayers; i++) if (d->layers[i].id == id) return i;
return NPOS;
}
tgb_status tgb_frame_index_of(const tgb_document *d, uint32_t id, size_t *out) {
if (!d || !out) return tgb_fail(TGB_ERR_INVALID_ARG, "Argumento nulo");
size_t i = doc_find_frame(d, id);
if (i == NPOS) return tgb_fail(TGB_ERR_NOT_FOUND, "No existe el fotograma con id %u", (unsigned)id);
*out = i;
return TGB_OK;
}
tgb_status tgb_layer_index_of(const tgb_document *d, uint32_t id, size_t *out) {
if (!d || !out) return tgb_fail(TGB_ERR_INVALID_ARG, "Argumento nulo");
size_t i = doc_find_layer(d, id);
if (i == NPOS) return tgb_fail(TGB_ERR_NOT_FOUND, "No existe la capa con id %u", (unsigned)id);
*out = i;
return TGB_OK;
}
const tgb_canvas *tgb_frame_cel(const tgb_document *d, size_t f, size_t l) {
if (!d || f >= d->nframes || l >= d->nlayers) return NULL;
return d->frames[f].cels[l];
}
tgb_status doc_check_mutable(const tgb_document *doc) {
if (!doc) return tgb_fail(TGB_ERR_INVALID_ARG, "Documento nulo");
if (doc->edit.active) return tgb_fail(TGB_ERR_STATE, "Hay una sesión de edición abierta: ciérrala antes de cambiar la estructura");
return TGB_OK;
}
/* ---- mutadores de bajo nivel ---- */
tgb_status doc_frame_insert(tgb_document *doc, size_t index, tgb_frame *f) {
if (index > doc->nframes) return tgb_fail(TGB_ERR_RANGE, "Índice de fotograma %zu fuera de rango", index);
if (doc->nframes == doc->capframes) {
size_t nc = doc->capframes ? doc->capframes * 2 : 4;
tgb_frame *nf = realloc(doc->frames, nc * sizeof *nf);
if (!nf) return tgb_fail(TGB_ERR_NOMEM, "Sin memoria para más fotogramas");
doc->frames = nf;
doc->capframes = nc;
}
memmove(&doc->frames[index + 1], &doc->frames[index], (doc->nframes - index) * sizeof doc->frames[0]);
doc->frames[index] = *f;
doc->nframes++;
f->cels = NULL;
return TGB_OK;
}
void doc_frame_remove(tgb_document *doc, size_t index, tgb_frame *out) {
*out = doc->frames[index];
memmove(&doc->frames[index], &doc->frames[index + 1], (doc->nframes - index - 1) * sizeof doc->frames[0]);
doc->nframes--;
}
void doc_frame_move(tgb_document *doc, size_t from, size_t to) {
if (from == to) return;
tgb_frame tmp = doc->frames[from];
if (from < to) memmove(&doc->frames[from], &doc->frames[from + 1], (to - from) * sizeof tmp);
else memmove(&doc->frames[to + 1], &doc->frames[to], (from - to) * sizeof tmp);
doc->frames[to] = tmp;
}
tgb_status doc_layer_insert(tgb_document *doc, size_t index, const tgb_layer *layer, tgb_canvas **cels) {
if (index > doc->nlayers) return tgb_fail(TGB_ERR_RANGE, "Índice de capa %zu fuera de rango", index);
/* 1) reservar todo antes de tocar nada */
if (doc->nlayers == doc->caplayers) {
size_t nc = doc->caplayers ? doc->caplayers * 2 : 4;
tgb_layer *nl = realloc(doc->layers, nc * sizeof *nl);
if (!nl) return tgb_fail(TGB_ERR_NOMEM, "Sin memoria para más capas");
doc->layers = nl;
doc->caplayers = nc;
}
for (size_t i = 0; i < doc->nframes; i++) {
tgb_canvas **na = realloc(doc->frames[i].cels, (doc->nlayers + 1) * sizeof *na);
if (!na) return tgb_fail(TGB_ERR_NOMEM, "Sin memoria para añadir la capa");
doc->frames[i].cels = na; /* hueco extra inofensivo si luego falla otro fotograma */
}
/* 2) confirmar */
for (size_t i = 0; i < doc->nframes; i++) {
tgb_canvas **a = doc->frames[i].cels;
memmove(&a[index + 1], &a[index], (doc->nlayers - index) * sizeof *a);
a[index] = cels ? cels[i] : NULL;
}
memmove(&doc->layers[index + 1], &doc->layers[index], (doc->nlayers - index) * sizeof doc->layers[0]);
doc->layers[index] = *layer;
doc->nlayers++;
return TGB_OK;
}
void doc_layer_remove(tgb_document *doc, size_t index, tgb_layer *out, tgb_canvas **cels_out) {
for (size_t i = 0; i < doc->nframes; i++) {
tgb_canvas **a = doc->frames[i].cels;
if (cels_out) cels_out[i] = a[index]; else tgb_canvas_release(a[index]);
memmove(&a[index], &a[index + 1], (doc->nlayers - index - 1) * sizeof *a);
}
*out = doc->layers[index];
memmove(&doc->layers[index], &doc->layers[index + 1], (doc->nlayers - index - 1) * sizeof doc->layers[0]);
doc->nlayers--;
}
void doc_layer_move(tgb_document *doc, size_t from, size_t to) {
if (from == to) return;
for (size_t i = 0; i < doc->nframes; i++) {
tgb_canvas **a = doc->frames[i].cels;
tgb_canvas *tmp = a[from];
if (from < to) memmove(&a[from], &a[from + 1], (to - from) * sizeof tmp);
else memmove(&a[to + 1], &a[to], (from - to) * sizeof tmp);
a[to] = tmp;
}
tgb_layer tl = doc->layers[from];
if (from < to) memmove(&doc->layers[from], &doc->layers[from + 1], (to - from) * sizeof tl);
else memmove(&doc->layers[to + 1], &doc->layers[to], (from - to) * sizeof tl);
doc->layers[to] = tl;
}
tgb_canvas *doc_cel_for_write(tgb_document *doc, uint32_t frame_id, uint32_t layer_id, bool create, bool *created) {
size_t fi = doc_find_frame(doc, frame_id), li = doc_find_layer(doc, layer_id);
if (created) *created = false;
if (fi == NPOS || li == NPOS) { tgb_fail(TGB_ERR_NOT_FOUND, "Celda inexistente (fotograma %u, capa %u)", (unsigned)frame_id, (unsigned)layer_id); return NULL; }
tgb_canvas **slot = &doc->frames[fi].cels[li];
if (!*slot) {
if (!create) return NULL;
*slot = tgb_canvas_create(doc->width, doc->height);
if (!*slot) return NULL;
if (created) *created = true;
} else if (tgb_canvas_refs(*slot) > 1) { /* copy-on-write */
tgb_canvas *copy = tgb_canvas_clone(*slot);
if (!copy) return NULL;
tgb_canvas_release(*slot);
*slot = copy;
}
return *slot;
}
/* ---- instantáneas ---- */
void state_free(tgb_state *st) {
if (!st) return;
for (size_t i = 0; i < st->nframes; i++) doc_frame_free_cels(st->frames[i].cels, st->nlayers);
free(st->frames);
free(st->layers);
memset(st, 0, sizeof *st);
}
static tgb_status copy_structure(const tgb_layer *layers, size_t nl, const tgb_frame *frames, size_t nf,
tgb_layer **out_l, tgb_frame **out_f) {
tgb_layer *l = malloc((nl ? nl : 1) * sizeof *l);
tgb_frame *f = calloc(nf ? nf : 1, sizeof *f);
if (!l || !f) { free(l); free(f); return tgb_fail(TGB_ERR_NOMEM, "Sin memoria para la instantánea"); }
memcpy(l, layers, nl * sizeof *l);
for (size_t i = 0; i < nf; i++) {
f[i].id = frames[i].id;
f[i].duration_ms = frames[i].duration_ms;
f[i].cels = calloc(nl ? nl : 1, sizeof *f[i].cels);
if (!f[i].cels) {
for (size_t k = 0; k < i; k++) doc_frame_free_cels(f[k].cels, nl);
free(l); free(f);
return tgb_fail(TGB_ERR_NOMEM, "Sin memoria para la instantánea");
}
for (size_t j = 0; j < nl; j++) f[i].cels[j] = tgb_canvas_retain(frames[i].cels[j]);
}
*out_l = l;
*out_f = f;
return TGB_OK;
}
tgb_status state_capture(const tgb_document *doc, tgb_state *out) {
memset(out, 0, sizeof *out);
tgb_status s = copy_structure(doc->layers, doc->nlayers, doc->frames, doc->nframes, &out->layers, &out->frames);
if (TGB_FAILED(s)) return s;
out->nlayers = doc->nlayers;
out->nframes = doc->nframes;
out->fps = doc->fps;
return TGB_OK;
}
tgb_status state_apply(tgb_document *doc, const tgb_state *st) {
tgb_layer *nl; tgb_frame *nf;
tgb_status s = copy_structure(st->layers, st->nlayers, st->frames, st->nframes, &nl, &nf);
if (TGB_FAILED(s)) return s;
for (size_t i = 0; i < doc->nframes; i++) doc_frame_free_cels(doc->frames[i].cels, doc->nlayers);
free(doc->frames);
free(doc->layers);
doc->layers = nl; doc->nlayers = st->nlayers; doc->caplayers = st->nlayers ? st->nlayers : 1;
doc->frames = nf; doc->nframes = st->nframes; doc->capframes = st->nframes ? st->nframes : 1;
doc->fps = st->fps;
return TGB_OK;
}
static int cmp_ptr(const void *a, const void *b) {
const void *x = *(const void *const *)a, *y = *(const void *const *)b;
return (x > y) - (x < y);
}
size_t state_extra_bytes(const tgb_state *st, const tgb_document *doc) {
size_t n = 0;
for (size_t i = 0; i < doc->nframes; i++) for (size_t j = 0; j < doc->nlayers; j++) if (doc->frames[i].cels[j]) n++;
const void **cur = malloc((n ? n : 1) * sizeof *cur);
if (!cur) return 0;
size_t k = 0;
for (size_t i = 0; i < doc->nframes; i++) for (size_t j = 0; j < doc->nlayers; j++) if (doc->frames[i].cels[j]) cur[k++] = doc->frames[i].cels[j];
qsort(cur, n, sizeof *cur, cmp_ptr);
size_t total = 0;
for (size_t i = 0; i < st->nframes; i++) for (size_t j = 0; j < st->nlayers; j++) {
tgb_canvas *c = st->frames[i].cels[j];
if (c && !bsearch(&c, cur, n, sizeof *cur, cmp_ptr)) total += tgb_canvas_bytes(c);
}
free(cur);
return total;
}
size_t cel_exclusive_bytes(tgb_canvas *c) { return (c && tgb_canvas_refs(c) == 1) ? tgb_canvas_bytes(c) : 0; }