/* 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; }