344 lines
15 KiB
C
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; }
|