/*
* Copyright (c) 2011-2015 Hypertriton, Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
* USE OF THIS SOFTWARE EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*
* FreeSG script interpreter and editor.
*/
#include
#include
#include
#include
#include
#include
#include
#include
const char *sgScriptInsnNames[] = {
N_("Noop"),
N_("Create"),
N_("Delete"),
N_("Action"),
N_("CamAction")
};
/* Edition context for a SG_Script. */
typedef struct sg_script_edit_ctx {
AG_Window *win; /* Parent window */
SG_Script *scr; /* Script object */
SG *sg; /* Constructed scene */
SG_View *sv; /* Visualization widget */
AG_Menu *menu; /* Main menu */
AG_Pane *paHoriz; /* Main horizontal pane */
AG_Pane *paLeft; /* Left side vertical pane */
AG_Box *boxBtns; /* Save/Cancel buttons */
AG_Widget *wEdit; /* Edit area */
SG_ScriptInsn *siNew; /* New instruction */
AG_Label *stat; /* Status bar label */
AG_Timer toCamMove; /* Camera motion timer */
M_Vector3 vCamMove; /* Camera motion vector */
M_Vector3 vCamMoveSum; /* Camera motion vector sum */
int camMoving;
AG_Slider *slTime; /* Time slider */
AG_Surface *suUnder; /* Underlay (for animation) */
} SG_ScriptEditCtx;
/* Rendering context for a SG_Script. */
typedef struct sg_script_render_ctx {
int nFirst, nLast; /* Frames to render */
double timeScale; /* Time scale */
double fps; /* Frames per second */
enum sg_script_interp_mode interp; /* Interpolation method */
int iInt; /* Current interpolated frame# */
int nInt; /* Interpolated video frames per frame */
int clearDir; /* Clear target dir on render */
} SG_ScriptRenderCtx;
/* Resolve an instruction's target node. */
static __inline__ void *_Nullable
GetInsnTarget(SG *_Nonnull sg, SG_ScriptInsn *_Nonnull si)
{
return AG_ObjectFindS(sg, si->tgtName);
}
/* Create a new script instance. */
SG_Script *
SG_ScriptNew(void *parent, const char *name)
{
SG_Script *scr;
scr = Malloc(sizeof(SG_Script));
AG_ObjectInitNamed(scr, &sgScriptClass, name);
AG_ObjectAttach(parent, scr);
return (scr);
}
static void
Init(void *_Nonnull obj)
{
SG_Script *scr = obj;
scr->flags = 0;
scr->fps = 0;
scr->n = 0;
scr->t = 0;
scr->tPrev = -1;
scr->tFirst = 0;
scr->tLast = 0;
scr->frames = NULL;
if ((SG_ScriptAlloc(scr, 201)) == -1)
AG_FatalError(NULL);
}
static void
Reset(void *_Nonnull obj)
{
SG_Script *scr = obj;
Uint i;
for (i = 0; i < scr->n; i++) {
SG_ScriptFrame *sf = &scr->frames[i];
SG_ScriptInsn *si, *siNext;
for (si = TAILQ_FIRST(&sf->insns);
si != TAILQ_END(&sf->insns);
si = siNext) {
siNext = TAILQ_NEXT(si, insns);
SG_ScriptInsnFree(si);
}
}
Free(scr->frames);
scr->frames = NULL;
scr->n = 0;
scr->tFirst = 0;
scr->tLast = 0;
scr->tPrev = 0;
}
static int
LoadInsn(SG_Script *_Nonnull scr, SG_ScriptInsn *_Nonnull si,
AG_DataSource *_Nonnull ds)
{
enum sg_script_insn_type siType;
siType = (enum sg_script_insn_type)AG_ReadUint8(ds);
if (siType >= SG_INSN_LAST) {
AG_SetError("Bad insn type: %d", siType);
return (-1);
}
si->type = siType;
si->flags &= ~(SG_SCRIPT_INSN_SAVED);
si->flags |= (AG_ReadUint8(ds) & SG_SCRIPT_INSN_SAVED);
si->tgtName = AG_ReadStringLen(ds, AG_OBJECT_PATH_MAX);
if (si->tgtName[0] == '\0') { Free(si->tgtName); si->tgtName = NULL; }
switch (si->type) {
case SG_INSN_CREATE:
{
char clsName[AG_OBJECT_HIER_MAX];
AG_ObjectClass *cls;
size_t dataSize;
si->si_create.name = AG_ReadStringLen(ds, AG_OBJECT_NAME_MAX);
if (si->si_create.name == NULL) {
return (-1);
}
AG_CopyString(clsName, ds, sizeof(clsName));
if ((cls = AG_LookupClass(clsName)) == NULL) {
return (-1);
}
dataSize = (size_t)AG_ReadUint32(ds);
if ((si->si_create.data = TryMalloc(dataSize)) == NULL) {
return (-1);
}
si->si_create.cls = cls;
si->si_create.size = dataSize;
if (AG_Read(ds, si->si_create.data, si->si_create.size) != 0)
return (-1);
}
break;
case SG_INSN_ACTION:
case SG_INSN_CAMACTION:
return SG_ActionLoad(&si->si_action, ds);
default:
break;
}
return (0);
}
static int
Load(void *_Nonnull obj, AG_DataSource *_Nonnull ds,
const AG_Version *_Nonnull ver)
{
SG_Script *scr = obj;
Uint nFrames, fr, i;
nFrames = (Uint)AG_ReadUint32(ds);
if (SG_ScriptAlloc(scr, nFrames) == -1)
return (-1);
scr->flags &= ~(SG_SCRIPT_SAVED);
scr->flags |= ((Uint)AG_ReadUint32(ds) & SG_SCRIPT_SAVED);
scr->fps = AG_ReadDouble(ds);
(void)AG_ReadSint32(ds); /* Ignore last t */
scr->t = 0;
scr->tFirst = (int)AG_ReadSint32(ds);
scr->tLast = (int)AG_ReadSint32(ds);
scr->tPrev = -1;
for (fr = 0; fr < scr->n; fr++) {
SG_ScriptInsn *si;
Uint nInsns;
nInsns = AG_ReadUint32(ds);
for (i = 0; i < nInsns; i++) {
if ((si = SG_ScriptInsnNew(scr)) == NULL) {
return (-1);
}
if (LoadInsn(scr, si, ds) == -1) {
SG_ScriptInsnFree(si);
return (-1);
}
SG_ScriptAddInsn(scr, fr, si);
}
}
return (0);
}
static void
SaveInsn(SG_Script *_Nonnull scr, SG_ScriptInsn *_Nonnull si,
AG_DataSource *_Nonnull ds)
{
AG_WriteUint8(ds, (Uint8)si->type);
AG_WriteUint8(ds, (Uint8)si->flags);
AG_WriteString(ds, (si->tgtName != NULL) ? si->tgtName : "");
switch (si->type) {
case SG_INSN_CREATE:
AG_WriteString(ds, si->si_create.name);
AG_WriteString(ds, si->si_create.cls->hier);
AG_WriteUint32(ds, (Uint32)si->si_create.size);
AG_Write(ds, si->si_create.data, si->si_create.size);
break;
case SG_INSN_ACTION:
case SG_INSN_CAMACTION:
SG_ActionSave(&si->si_action, ds);
default:
break;
}
}
static int
Save(void *_Nonnull obj, AG_DataSource *_Nonnull ds)
{
SG_Script *scr = obj;
Uint fr;
AG_WriteUint32(ds, (Uint32)scr->n);
AG_WriteUint32(ds, (Uint32)(scr->flags & SG_SCRIPT_SAVED));
AG_WriteDouble(ds, scr->fps);
AG_WriteSint32(ds, scr->t);
AG_WriteSint32(ds, scr->tFirst);
AG_WriteSint32(ds, scr->tLast);
for (fr = 0; fr < scr->n; fr++) {
SG_ScriptInsn *si;
Uint32 count = 0;
TAILQ_FOREACH(si, &scr->frames[fr].insns, insns) {
count++;
}
AG_WriteUint32(ds, count);
TAILQ_FOREACH(si, &scr->frames[fr].insns, insns) {
SaveInsn(scr, si, ds);
}
}
return (0);
}
/* Allocate time frames. */
int
SG_ScriptAlloc(SG_Script *scr, Uint n)
{
SG_ScriptFrame *framesNew;
Uint i;
if (n <= scr->n) {
scr->n = n;
scr->tLast = (n-1);
return (0);
}
if ((framesNew = TryRealloc(scr->frames, n*sizeof(SG_ScriptFrame)))
== NULL) {
return (-1);
}
scr->frames = framesNew;
for (i = scr->n; i < n; i++) {
TAILQ_INIT(&scr->frames[i].insns);
}
scr->n = n;
scr->tLast = (n-1);
return (0);
}
/* Allocate a new script instruction. */
SG_ScriptInsn *
SG_ScriptInsnNew(SG_Script *scr)
{
SG_ScriptInsn *si;
if ((si = TryMalloc(sizeof(SG_ScriptInsn))) == NULL) {
return (NULL);
}
si->type = SG_INSN_NOOP;
si->flags = 0;
si->tgtName = NULL;
return (si);
}
/* Insert instruction in specified time frame, at tail. */
int
SG_ScriptAddInsn(SG_Script *scr, Uint t, SG_ScriptInsn *si)
{
SG_ScriptFrame *sf;
if (t >= scr->n) {
AG_SetError("Bad frame#: %u", t);
return (-1);
}
sf = &scr->frames[t];
TAILQ_INSERT_TAIL(&sf->insns, si, insns);
return (0);
}
/* Insert instruction in specified time frame, before another instruction. */
int
SG_ScriptAddInsnBefore(SG_Script *scr, Uint t, SG_ScriptInsn *siOther,
SG_ScriptInsn *si)
{
if (t >= scr->n) {
AG_SetError("Bad frame#: %u", t);
return (-1);
}
TAILQ_INSERT_BEFORE(siOther, si, insns);
return (0);
}
/* Destroy a script instruction structure. */
void
SG_ScriptInsnFree(SG_ScriptInsn *si)
{
switch (si->type) {
case SG_INSN_CREATE:
Free(si->si_create.name);
Free(si->si_create.data);
break;
default:
break;
}
Free(si->tgtName);
Free(si);
}
/* Remove specified instruction from script (without freeing it). */
int
SG_ScriptDelInsn(SG_Script *scr, Uint t, SG_ScriptInsn *si)
{
if (t >= scr->n) {
AG_SetError("Bad frame#: %u", t);
return (-1);
}
TAILQ_REMOVE(&scr->frames[t].insns, si, insns);
return (0);
}
/* Print the given instruction to a fixed-size buffer. */
void
SG_ScriptPrintInsn(const SG_ScriptInsn *si, char *buf, size_t len)
{
if (si->type >= SG_INSN_LAST) {
if (len > 0) { buf[0] = '\0'; }
return;
}
switch (si->type) {
case SG_INSN_CREATE:
snprintf(buf, len, "%s(<%s> %s%s)",
sgScriptInsnNames[si->type],
si->si_create.cls->name,
si->tgtName, si->si_create.name);
break;
case SG_INSN_ACTION:
case SG_INSN_CAMACTION:
{
char abuf[128];
SG_ActionPrint(&si->si_action, abuf, sizeof(abuf));
snprintf(buf, len, "%s(%s -> %s): %s",
sgScriptInsnNames[si->type],
sgActionNames[si->si_action.type],
si->tgtName, abuf);
}
break;
default:
snprintf(buf, len, "%s(%s)",
sgScriptInsnNames[si->type],
si->tgtName);
break;
}
}
/*
* Execute a Create instruction. A new node is created, initialized from
* the instruction's argument and attached to the parent node (tgtName).
*/
static int
ExecCreateInsn(SG_ScriptEditCtx *_Nonnull e, SG_ScriptRenderCtx *_Nullable re,
SG_ScriptInsn *_Nonnull si)
{
SG_View *sv = e->sv;
AG_DataSource *ds;
SG_Node *node, *parent;
if (re != NULL && re->iInt > 0) {
/* TODO: fade in */
return (0);
}
if ((parent = GetInsnTarget(e->sg, si)) == NULL)
return (-1);
if ((ds = AG_OpenCore(si->si_create.data, si->si_create.size)) == NULL)
return (-1);
if ((node = TryMalloc(si->si_create.cls->size)) == NULL) {
goto fail;
}
AG_ObjectInitNamed(node, si->si_create.cls, NULL);
if (AG_ObjectUnserialize(node, ds) == -1) {
AG_ObjectDestroy(node);
goto fail;
}
AG_ObjectAttach(parent, node);
/* Ignore any saved transformation matrix. */
SG_Identity(node);
if (parent != e->sg->root) {
M_Vector3 vOffsZ = SG_NodeDir(sv->cam);
M_VecScale3v(&vOffsZ, 0.01);
SG_Translatev(node, vOffsZ);
}
AG_CloseCore(ds);
return (0);
fail:
AG_CloseCore(ds);
return (-1);
}
/* Execute a Delete instruction. The specified node is destroyed. */
static int
ExecDeleteInsn(SG_ScriptEditCtx *_Nonnull e, SG_ScriptRenderCtx *_Nonnull re,
SG_ScriptInsn *_Nonnull si)
{
SG_Node *tgt;
if (re != NULL &&
re->iInt == (re->nInt-1)) {
/* TODO: fade out */
return (0);
}
if ((tgt = GetInsnTarget(e->sg, si)) == NULL)
return (-1);
if (!TAILQ_EMPTY(&OBJECT(tgt)->children)) {
AG_SetError("Cannot delete, node has child objects");
return (-1);
}
AG_ObjectDetach(tgt);
AG_ObjectDestroy(tgt);
return (0);
}
/* Execute an Action instruction. */
static int
ExecActionInsn(SG_ScriptEditCtx *_Nonnull e, SG_ScriptRenderCtx *_Nonnull re,
SG_ScriptInsn *_Nonnull si)
{
SG_Node *tgt;
if ((tgt = GetInsnTarget(e->sg, si)) == NULL) {
return (-1);
}
if (re != NULL && re->nInt > 1) { /* Interpolate */
SG_Action *ao = &si->si_action;
SG_Action a;
SG_ActionInit(&a, ao->type);
switch (a.type) {
case SG_ACTION_MOVE:
case SG_ACTION_ZMOVE:
a.act_move = M_VecScale3(ao->act_move,
1.0/(M_Real)re->nInt);
break;
case SG_ACTION_ROTATE:
a.act_rotate.theta = ao->act_rotate.theta /
(M_Real)re->nInt;
a.act_rotate.axis = ao->act_rotate.axis;
break;
case SG_ACTION_SCALE:
a.act_scale = M_VecScale3(ao->act_scale,
1.0/(M_Real)re->nInt);
break;
default:
break;
}
return SGNODE_OPS(tgt)->script_action(tgt, &a, 0);
} else {
return SGNODE_OPS(tgt)->script_action(tgt, &si->si_action, 0);
}
}
/*
* Invert a Create instruction. Look up the created node under the
* target parent and destroy it.
*/
static int
UndoCreateInsn(SG_ScriptEditCtx *_Nonnull e, SG_ScriptInsn *_Nonnull si)
{
SG_Node *tgtParent, *tgtCreated;
if ((tgtParent = GetInsnTarget(e->sg, si)) == NULL)
return (-1);
if ((tgtCreated = AG_ObjectFindChild(tgtParent, si->si_create.name))
== NULL) {
return (-1);
}
AG_ObjectDetach(tgtCreated);
AG_ObjectDestroy(tgtCreated);
return (0);
}
/*
* Invert a Delete instruction. We do this by executing the previous Create
* instruction corresponding to the deleted node.
*/
static int
UndoDeleteInsn(SG_ScriptEditCtx *_Nonnull e, SG_ScriptInsn *_Nonnull si)
{
SG_Script *scr = e->scr;
char path[AG_OBJECT_PATH_MAX];
SG_ScriptInsn *siCreate = NULL;
int tCreate;
for (tCreate = scr->t; tCreate >= 0; tCreate--) {
SG_ScriptFrame *sf = &scr->frames[tCreate];
TAILQ_FOREACH(siCreate, &sf->insns, insns) {
if (siCreate->type != SG_INSN_CREATE) {
continue;
}
Strlcpy(path, siCreate->tgtName, sizeof(path));
Strlcat(path, "/", sizeof(path));
Strlcat(path, siCreate->si_create.name, sizeof(path));
if (strcmp(path, si->tgtName) == 0)
break;
}
if (siCreate != NULL)
break;
}
if (tCreate < 0) {
AG_SetError("No matching Create insn for %s", si->tgtName);
return (-1);
}
return ExecCreateInsn(e, NULL, siCreate);
}
/* Invert an Action instruction. We call script_action() with invert=1. */
static int
UndoActionInsn(SG_ScriptEditCtx *_Nonnull e, SG_ScriptInsn *_Nonnull si)
{
SG_Node *tgt;
if ((tgt = GetInsnTarget(e->sg, si)) == NULL) {
return (-1);
}
return SGNODE_OPS(tgt)->script_action(tgt, &si->si_action, 1);
}
/* Execute a script instruction. */
static int
ExecInsn(SG_ScriptEditCtx *_Nonnull e, SG_ScriptRenderCtx *_Nullable re,
SG_ScriptInsn *_Nonnull si)
{
#if 1
char buf[128];
SG_ScriptPrintInsn(si, buf, sizeof(buf));
if (re != NULL && re->nInt > 1) {
printf("Exec: %s (Interp: %d/%d)\n", buf, re->iInt, re->nInt);
} else {
printf("Exec: %s\n", buf);
}
#endif
switch (si->type) {
case SG_INSN_CREATE:
return ExecCreateInsn(e, re, si);
case SG_INSN_DELETE:
return ExecDeleteInsn(e, re, si);
case SG_INSN_ACTION:
case SG_INSN_CAMACTION:
return ExecActionInsn(e, re, si);
case SG_INSN_NOOP:
break;
default:
AG_SetError("Illegal instruction: 0x%x", si->type);
return (-1);
}
return (0);
}
/* Undo the action of a script instruction. */
static int
UndoInsn(SG_ScriptEditCtx *_Nonnull e, SG_ScriptInsn *_Nonnull si)
{
#ifdef SG_DEBUG
char buf[128];
SG_ScriptPrintInsn(si, buf, sizeof(buf));
printf("Undo: %s\n", buf);
#endif
switch (si->type) {
case SG_INSN_CREATE:
return UndoCreateInsn(e, si);
case SG_INSN_DELETE:
return UndoDeleteInsn(e, si);
case SG_INSN_ACTION:
case SG_INSN_CAMACTION:
return UndoActionInsn(e, si);
case SG_INSN_NOOP:
break;
default:
AG_SetError("Illegal instruction: 0x%x", si->type);
return (-1);
}
return (0);
}
static void
PollInsns(AG_Event *_Nonnull event)
{
char text[1024];
AG_Tlist *tl = AG_SELF();
SG_Script *scr = AG_PTR(1);
SG_ScriptFrame *sf = &scr->frames[scr->t];
SG_ScriptInsn *si;
AG_TlistClear(tl);
AG_ObjectLock(scr);
TAILQ_FOREACH(si, &sf->insns, insns) {
AG_TlistItem *it;
SG_ScriptPrintInsn(si, text, sizeof(text));
it = AG_TlistAddS(tl, sgIconNode.s, text);
it->cat = "insn";
it->p1 = si;
}
AG_ObjectUnlock(scr);
AG_TlistRestore(tl);
}
static void
SelectInsn(AG_Event *_Nonnull event)
{
#ifdef AG_THREADS
SG_Script *scr = AG_PTR(1);
#endif
AG_TlistItem *it = AG_PTR(2);
int state = AG_INT(3);
if (strcmp(it->cat, "insn") == 0) {
SG_ScriptInsn *si = it->p1;
AG_ObjectLock(scr);
AG_SETFLAGS(si->flags, SG_SCRIPT_INSN_SELECTED, state);
AG_ObjectUnlock(scr);
}
}
static void
AutosizeEditPane(SG_ScriptEditCtx *_Nonnull e)
{
AG_SizeReq rDiv;
AG_WidgetSizeReq(e->paLeft->div[1], &rDiv);
AG_PaneMoveDivider(e->paLeft, WIDGET(e->paLeft)->h - rDiv.h);
}
static void
ClearEditPane(SG_ScriptEditCtx *_Nonnull e)
{
if (e->wEdit != NULL) {
AG_ObjectDetach(e->wEdit);
e->wEdit = NULL;
}
if (e->boxBtns != NULL) {
AG_ObjectDetach(e->boxBtns);
e->boxBtns = NULL;
}
}
/*
* Insert a new "Create" instruction from an active node object. The
* Create instruction will contains the node's serialized dataset.
*/
static void
InsertCreateInsn(AG_Event *_Nonnull event)
{
char path[AG_OBJECT_PATH_MAX];
SG_ScriptEditCtx *e = AG_PTR(1);
SG_Node *parent = AG_PTR(2);
SG_Node *node = AG_PTR(3);
SG_Script *scr = e->scr;
SG_ScriptInsn *si = NULL, *siRef;
AG_DataSource *ds;
AG_CoreSource *cs;
int selOrig;
if ((ds = AG_OpenAutoCore()) == NULL) {
return;
}
cs = AG_CORE_SOURCE(ds);
selOrig = (node->flags & SG_NODE_SELECTED);
node->flags &= ~(SG_NODE_SELECTED);
if (AG_ObjectSerialize(node, ds) == -1) {
goto fail;
}
node->flags |= selOrig;
if ((si = SG_ScriptInsnNew(scr)) == NULL)
goto fail;
si->type = SG_INSN_CREATE;
if ((si->tgtName = AG_ObjectGetName(parent)) == NULL)
goto fail;
if ((si->si_create.name = TryStrdup(OBJECT(node)->name)) == NULL)
goto fail;
if ((si->si_create.data = TryMalloc(cs->size)) == NULL) {
goto fail;
}
memcpy(si->si_create.data, cs->data, cs->size);
si->si_create.size = cs->size;
si->si_create.cls = OBJECT_CLASS(node);
/*
* Insert Create insn before any other newly created instruction
* referencing created object.
*/
AG_ObjectCopyName(node, path, sizeof(path));
TAILQ_FOREACH(siRef, &scr->frames[scr->t].insns, insns) {
if (siRef->tgtName != NULL &&
strcmp(siRef->tgtName, path) == 0)
break;
}
if (siRef != NULL) {
SG_ScriptAddInsnBefore(scr, scr->t, siRef, si);
} else {
SG_ScriptAddInsn(scr, scr->t, si);
}
AG_LabelText(e->stat, _("Created %s object (%u bytes)"),
OBJECT(node)->name, (Uint)cs->size);
ClearEditPane(e);
AG_CloseAutoCore(ds);
return;
fail:
if (si != NULL) { SG_ScriptInsnFree(si); }
AG_CloseAutoCore(ds);
AG_TextMsgFromError();
return;
}
static void
CancelCreateInsn(AG_Event *_Nonnull event)
{
SG_ScriptEditCtx *e = AG_PTR(1);
SG_Node *node = AG_PTR(2);
SG_Script *scr = e->scr;
SG_ScriptInsn *siRef, *siRefNext;
SG_ScriptFrame *sf;
char *nodeName;
ClearEditPane(e);
AG_LabelText(e->stat, _("Aborted creation of %s"), OBJECT(node)->name);
/*
* Remove other newly created insns referencing the new object.
*/
nodeName = AG_ObjectGetName(node);
sf = &scr->frames[scr->t];
for (siRef = TAILQ_FIRST(&sf->insns);
siRef != TAILQ_END(&sf->insns);
siRef = siRefNext) {
siRefNext = TAILQ_NEXT(siRef, insns);
if (siRef->tgtName != NULL &&
strcmp(siRef->tgtName, nodeName) == 0) {
SG_ScriptDelInsn(scr, scr->t, siRef);
SG_ScriptInsnFree(siRef);
}
}
e->siNew = NULL;
AG_ObjectDetach(node);
AG_ObjectDestroy(node);
}
/* Insert a new "Delete" instruction to delete the selected node(s). */
static void
InsertDeleteInsn(AG_Event *_Nonnull event)
{
SG_ScriptEditCtx *e = AG_PTR(1);
SG_Script *scr = e->scr;
SG *sg = e->sg;
SG_Node *node;
int nDeleted = 0;
SG_FOREACH_NODE(node, sg) {
SG_ScriptInsn *si;
if ((node->flags & SG_NODE_SELECTED) == 0) {
continue;
}
if (!TAILQ_EMPTY(&OBJECT(node)->children)) {
AG_LabelText(e->stat,
_("Cannot Delete: %s has child objects"),
OBJECT(node)->name);
return;
}
if ((si = SG_ScriptInsnNew(scr)) == NULL) {
continue;
}
si->type = SG_INSN_DELETE;
if ((si->tgtName = AG_ObjectGetName(node)) == NULL) {
SG_ScriptInsnFree(si);
continue;
}
SG_ScriptAddInsn(scr, scr->t, si);
AG_ObjectDetach(node);
AG_ObjectDestroy(node);
nDeleted++;
}
AG_LabelText(e->stat, _("Deleted %d node(s)"), nDeleted);
}
static void
InsertCreateInsnDlg(AG_Event *_Nonnull event)
{
SG_ScriptEditCtx *e = AG_PTR(1);
AG_ObjectClass *cls = AG_PTR(2);
SG *sg = e->sg;
AG_Widget *w;
SG_Node *node, *parent = NULL;
/* Use any currently selected node as parent. */
SG_FOREACH_NODE(node, sg) {
if (node->flags & SG_NODE_SELECTED) {
parent = node;
break;
}
}
if (parent == NULL)
parent = sg->root;
/* Create a new node instance. */
if ((node = Malloc(cls->size)) == NULL) {
AG_TextMsgFromError();
return;
}
AG_ObjectInitNamed(node, cls, NULL);
AG_ObjectAttach(parent, node);
if (parent != sg->root) {
M_Vector3 vOffsZ = SG_NodeDir(e->sv->cam);
M_VecScale3v(&vOffsZ, 0.01);
SG_Translatev(node, vOffsZ);
}
/* Edit node parameters. */
ClearEditPane(e);
e->wEdit = (AG_Widget *)AG_BoxNewVert(e->paLeft->div[1], AG_BOX_EXPAND);
if (NODE_OPS(node)->edit != NULL) {
w = NODE_OPS(node)->edit(node, e->sv);
if (w != NULL && AG_OfClass(w, "AG_Widget:*"))
AG_ObjectAttach(e->wEdit, w);
}
e->boxBtns = AG_BoxNewHoriz(e->paLeft->div[1], AG_BOX_HOMOGENOUS|AG_BOX_HFILL);
{
AG_ButtonNewFn(e->boxBtns, 0, _("Save"),
InsertCreateInsn, "%p,%p,%p", e, parent, node);
AG_ButtonNewFn(e->boxBtns, 0, _("Cancel"),
CancelCreateInsn, "%p,%p", e, node);
}
AutosizeEditPane(e);
}
static void
EditInsn(AG_Event *_Nonnull event)
{
AG_Tlist *tl = AG_SELF();
SG_ScriptEditCtx *e = AG_PTR(1);
AG_TlistItem *it = AG_TlistSelectedItem(tl);
if (strcmp(it->cat, "insn") != 0)
return;
#if 0
SG_GUI_EditNode(node, paEdit->div[1], sv);
#endif
AutosizeEditPane(e);
}
static void
AllocFrames(AG_Event *_Nonnull event)
{
SG_Script *scr = AG_PTR(1);
AG_Numerical *numAlloc = AG_PTR(2);
if (SG_ScriptAlloc(scr, scr->n+AG_GetInt(numAlloc,"value")) == -1)
AG_TextMsgFromError();
}
/* Pick nearest point on nearest node. */
static SG_Node *_Nullable
PickNearestNode(SG_ScriptEditCtx *_Nonnull e, int xWid, int yWid,
M_Vector3 *_Nonnull Xnear)
{
M_Real dNearest = M_INFINITY;
SG_Node *node, *nodeNearest = NULL;
M_Vector4 pNear, pFar;
M_Line3 ray;
M_Matrix44 T;
/* Find corresponding points on Near and Far plane. */
SG_ViewUnProject(e->sv, xWid, yWid, &pNear, &pFar);
SG_GetNodeTransform(e->sv->cam, &T);
M_MatMultVector44v(&pNear, &T);
M_MatMultVector44v(&pFar, &T);
/* Intersect ray with nodes in the scene. */
SG_FOREACH_NODE(node, e->sg) {
M_Vector3 pRayNear, pRayFar;
M_GeomSet3 S = M_GEOM_SET_EMPTY;
M_Matrix44 T;
M_Geom3 gt;
M_Real d;
M_Vector3 lp1, lp2;
SG_GetNodeTransformInverse(node, &T);
pRayNear = M_VecFromProj3(M_MatMultVector44(T,pNear));
pRayFar = M_VecFromProj3(M_MatMultVector44(T,pFar));
ray = M_LineFromPts3(pRayNear, pRayFar);
gt.type = M_LINE;
gt.g.line = ray;
if (SG_Intersect(node, gt, &S) != 1) {
continue;
}
switch (S.g[0].type) {
case M_POINT:
d = M_VecDistance3(S.g[0].g.point, pRayNear);
if (d < dNearest) {
dNearest = d;
nodeNearest = node;
*Xnear = S.g[0].g.point;
}
break;
case M_LINE:
M_LineToPts3(S.g[0].g.line, &lp1, &lp2);
d = M_VecDistance3(lp1, pRayNear);
if (d < dNearest) {
dNearest = d;
nodeNearest = node;
*Xnear = lp1;
}
d = M_VecDistance3(lp2, pRayNear);
if (d < dNearest) {
dNearest = d;
nodeNearest = node;
*Xnear = lp2;
}
break;
default:
break;
}
M_GeomSetFree3(&S);
}
return (nodeNearest);
}
/* Pick a node, initiate an Action and create a corresponding Insn. */
static int
BeginAction(SG_ScriptEditCtx *_Nonnull e, int xWid, int yWid)
{
int *kbd = AG_GetKeyState(e->sv);
SG_Script *scr = e->scr;
SG_Node *node;
M_Vector3 Xnear = M_VecZero3();
SG_ScriptInsn *si = NULL;
SG_Action *actReg, *act;
M_Vector4 pNear, pFar;
M_Matrix44 T;
/* Select the nearest intersecting node. */
SG_FOREACH_NODE(node, e->sg) {
node->flags &= ~(SG_NODE_SELECTED);
}
if ((node = PickNearestNode(e, xWid, yWid, &Xnear)) == NULL) {
return (0);
}
node->flags |= SG_NODE_SELECTED;
/* Look for a matching action key shortcut (default to Move). */
TAILQ_FOREACH(actReg, &node->actions, actions) {
if (kbd[actReg->key])
break;
}
if (actReg == NULL) {
TAILQ_FOREACH(actReg, &node->actions, actions) {
if (actReg->type == SG_ACTION_MOVE)
break;
}
if (actReg == NULL)
return (0);
}
/* Compute the mouse control ray. */
SG_ViewUnProject(e->sv, xWid, yWid, &pNear, &pFar);
SG_GetNodeTransform(e->sv->cam, &T);
M_MatMultVector44v(&pNear, &T);
M_MatMultVector44v(&pFar, &T);
/* Bring the ray into the node's coordinate system. */
SG_GetNodeTransformInverse(node, &T);
/* Create a new Action instruction. */
if ((si = SG_ScriptInsnNew(scr)) == NULL) {
goto fail;
}
si->type = SG_INSN_ACTION;
if ((si->tgtName = AG_ObjectGetName(node)) == NULL) {
goto fail;
}
act = &si->si_action;
SG_ActionInit(act, actReg->type);
act->Rorig = M_LineFromPts3(M_VecFromProj3(M_MatMultVector44(T,pNear)),
M_VecFromProj3(M_MatMultVector44(T,pFar)));
act->Rcur = act->Rorig;
act->vOrig = Xnear;
act->Torig = node->T;
SG_ScriptAddInsn(scr, scr->t, si);
/* Invoke the SG_ACTION_*_BEGIN routine. */
SGNODE_OPS(node)->editor_action(node, act->type+1, act);
AG_LabelText(e->stat, _("Selected %s for %s"),
OBJECT(node)->name, sgActionNames[act->type]);
e->siNew = si;
return (1);
fail:
if (si != NULL) { SG_ScriptInsnFree(si); }
AG_LabelText(e->stat, _("Failed: %s"), AG_GetError());
return (0);
}
/* Update the current action by mouse gesture. */
static void
UpdateAction(SG_ScriptEditCtx *_Nonnull e, int xWid, int yWid)
{
M_Vector4 pNear, pFar;
M_Matrix44 T, Tsave;
SG_Node *node;
SG_Action *act;
if (e->siNew == NULL) {
return;
}
act = &e->siNew->si_action;
if ((node = GetInsnTarget(e->sg, e->siNew)) == NULL) {
AG_LabelText(e->stat, _("Failed: %s"), AG_GetError());
return;
}
/* Compute the mouse control ray. */
SG_ViewUnProject(e->sv, xWid, yWid, &pNear, &pFar);
SG_GetNodeTransform(e->sv->cam, &T);
M_MatMultVector44v(&pNear, &T);
M_MatMultVector44v(&pFar, &T);
/*
* Bring the control ray into the node's coordinate system,
* as it was when the action was first initiated (using Torig).
* This enables transformations such as rotations to be done
* in a numerically stable manner.
*/
Tsave = node->T;
node->T = act->Torig;
SG_GetNodeTransformInverse(node, &T);
act->Rcur = M_LineFromPts3(M_VecFromProj3(M_MatMultVector44(T,pNear)),
M_VecFromProj3(M_MatMultVector44(T,pFar)));
node->T = Tsave;
/* Invoke the node's editor_action routine. */
SGNODE_OPS(node)->editor_action(node, act->type, act);
}
/* Terminate any current action. */
static void
EndAction(SG_ScriptEditCtx *_Nonnull e)
{
SG_ScriptInsn *si = e->siNew;
SG_Action *act = &si->si_action;
SG_Node *tgt;
if ((tgt = GetInsnTarget(e->sg, si)) == NULL)
return;
/* Invoke SG_ACTION_*_END routine. */
SGNODE_OPS(tgt)->editor_action(tgt, act->type+2, act);
/* Cancel any no-op instructions. */
switch (act->type) {
case SG_ACTION_MOVE:
case SG_ACTION_ZMOVE:
if (act->act_move.x == 0 &&
act->act_move.y == 0 &&
act->act_move.z == 0) {
SG_ScriptDelInsn(e->scr, e->scr->t, si);
SG_ScriptInsnFree(si);
}
break;
case SG_ACTION_ROTATE:
if (act->act_rotate.theta == 0) {
SG_ScriptDelInsn(e->scr, e->scr->t, si);
SG_ScriptInsnFree(si);
}
break;
default:
break;
}
e->siNew = NULL;
}
static Uint32
CamMoveTimeout(AG_Timer *_Nonnull to, AG_Event *_Nonnull event)
{
SG_View *sv = AG_SELF();
SG_ScriptEditCtx *e = AG_PTR(1);
SG_Translatev(sv->cam, e->vCamMove);
e->vCamMoveSum = M_VecAdd3(e->vCamMoveSum, e->vCamMove);
AG_LabelText(e->stat, _("%s: +[%f,%f,%f]"), OBJECT(sv->cam)->name,
e->vCamMoveSum.x, e->vCamMoveSum.y, e->vCamMoveSum.z);
return (to->ival);
}
static void
BeginCameraMove(SG_ScriptEditCtx *_Nonnull e)
{
e->camMoving = 1;
e->vCamMove = M_VecZero3();
e->vCamMoveSum = M_VecZero3();
AG_AddTimer(e->sv, &e->toCamMove, 1, CamMoveTimeout, "%p", e);
}
static void
EndCameraMove(SG_ScriptEditCtx *_Nonnull e)
{
SG_View *sv = e->sv;
AG_DelTimer(sv, &e->toCamMove);
e->camMoving = 0;
/*
* If requested, generate a new Camera Action instruction.
*/
if (AG_GetModState(sv) & AG_KEYMOD_CTRL) {
SG_Script *scr = e->scr;
SG_ScriptInsn *si;
if ((si = SG_ScriptInsnNew(scr)) == NULL) {
goto fail;
}
si->type = SG_INSN_CAMACTION;
if ((si->tgtName = AG_ObjectGetName(sv->cam)) == NULL) {
goto fail;
}
SG_ActionInit(&si->si_action, SG_ACTION_MOVE);
SG_ScriptAddInsn(scr, scr->t, si);
si->si_action.act_move = e->vCamMoveSum;
}
return;
fail:
AG_LabelText(e->stat, _("Failed: %s"), AG_GetError());
}
static void
SeekToFrame(SG_ScriptEditCtx *_Nonnull e, int t)
{
SG_Script *scr = e->scr;
int i;
scr->t = t;
if (scr->t > scr->tPrev) {
for (i = scr->tPrev+1; i <= scr->t; i++) {
SG_ScriptFrame *sf = &scr->frames[i];
SG_ScriptInsn *si;
printf("Forward seek: #%d\n", i);
TAILQ_FOREACH(si, &sf->insns, insns) {
if (ExecInsn(e, NULL, si) == -1)
goto fail;
}
}
} else if (scr->t < scr->tPrev) {
for (i = scr->tPrev; i > scr->t; i--) {
SG_ScriptFrame *sf = &scr->frames[i];
SG_ScriptInsn *si;
printf("Backward seek: #%d\n", scr->t);
TAILQ_FOREACH_REVERSE(si, &sf->insns, sg_script_insnsq,
insns) {
if (UndoInsn(e, si) == -1)
goto fail;
}
}
}
scr->tPrev = scr->t;
return;
fail:
AG_LabelText(e->stat, _("Seek failed: %s"), AG_GetError());
scr->t = scr->tPrev;
}
static AG_Surface *_Nullable
GenerateUnderlay(SG_ScriptEditCtx *_Nonnull e)
{
AG_Rect2 r = WIDGET(e->sv)->rView;
Uint8 *buf = NULL;
if ((buf = TryMalloc(r.w*r.h*4)) == NULL)
return (NULL);
glReadPixels(r.x1, WIDGET(e->win)->h - r.y2,
r.w, r.h,
GL_RGBA, GL_UNSIGNED_BYTE, buf);
AG_PackedPixelFlip(buf, r.h, r.w*4);
return AG_SurfaceFromPixelsRGBA(buf, r.w, r.h, 32,
0x000000ff, 0x0000ff00, 0x00ff0000, 0);
}
static void
KeyDown(AG_Event *_Nonnull event)
{
SG_ScriptEditCtx *e = AG_PTR(1);
int key = AG_INT(2);
AG_Event ev;
switch (key) {
case AG_KEY_DELETE:
AG_EventInit(&ev);
AG_EventPushPointer(&ev, "", e);
InsertDeleteInsn(&ev);
break;
case AG_KEY_SPACE:
if ((e->scr->t+1) < e->scr->n) {
if (e->suUnder != NULL) {
AG_SurfaceFree(e->suUnder);
}
e->suUnder = GenerateUnderlay(e);
SeekToFrame(e, e->scr->t+1);
}
break;
}
}
static void
MouseButtonDown(AG_Event *_Nonnull event)
{
SG_ScriptEditCtx *e = AG_PTR(1);
SG_View *sv = e->sv;
int button = AG_INT(2);
int x = AG_INT(3);
int y = AG_INT(4);
if (sv->cam == NULL)
return;
AG_WidgetFocus(sv);
switch (button) {
case AG_MOUSE_LEFT:
BeginAction(e, x, y);
break;
case AG_MOUSE_MIDDLE:
BeginCameraMove(e);
break;
case AG_MOUSE_WHEELUP:
if (e->camMoving) {
e->vCamMove.z -= 0.01;
} else {
SG_Translate(sv->cam, 0.0, 0.0, -0.01);
}
break;
case AG_MOUSE_WHEELDOWN:
if (e->camMoving) {
e->vCamMove.z += 0.01;
} else {
SG_Translate(sv->cam, 0.0, 0.0, +0.01);
}
break;
case AG_MOUSE_RIGHT:
if (AG_GetModState(sv) & AG_KEYMOD_CTRL) {
sv->flags |= SG_VIEW_ROTATING;
} else {
sv->flags |= SG_VIEW_PANNING;
}
break;
}
AG_Redraw(sv);
}
static void
MouseButtonUp(AG_Event *_Nonnull event)
{
SG_ScriptEditCtx *e = AG_PTR(1);
int button = AG_INT(2);
switch (button) {
case AG_MOUSE_LEFT:
if (e->siNew != NULL && e->siNew->type == SG_INSN_ACTION) {
EndAction(e);
AG_Redraw(e->sv);
}
break;
case AG_MOUSE_RIGHT:
e->sv->flags &= ~(SG_VIEW_PANNING|SG_VIEW_ROTATING);
break;
case AG_MOUSE_MIDDLE:
EndCameraMove(e);
break;
}
}
static void
MouseMotion(AG_Event *_Nonnull event)
{
SG_ScriptEditCtx *e = AG_PTR(1);
int xWid = AG_INT(2);
int yWid = AG_INT(3);
int xRel = AG_INT(4);
int yRel = AG_INT(5);
SG_View *sv = e->sv;
if (e->siNew != NULL && e->siNew->type == SG_INSN_ACTION) {
UpdateAction(e, xWid, yWid);
AG_Redraw(e->sv);
} else {
if (sv->flags & SG_VIEW_PANNING) {
SG_CameraMoveMouse(sv->cam, sv, xRel, yRel, 0);
} else if (sv->flags & SG_VIEW_ROTATING) {
SG_CameraRotMouse(sv->cam, sv, xRel, yRel);
}
}
}
static void
OnUnderlay(AG_Event *_Nonnull event)
{
SG_ScriptEditCtx *e = AG_PTR(1);
SG_View *sv = e->sv;
AG_Rect r;
AG_Color c;
if (e->suUnder == NULL)
return;
AG_WidgetBlit(sv, e->suUnder, 0, 0);
r.x = 0;
r.y = 0;
r.w = WIDTH(sv);
r.h = HEIGHT(sv);
AG_ColorRGBA_8(&c, 0,0,0, 128);
AG_DrawRectBlended(sv, &r, &c, AG_ALPHA_SRC);
}
/* Reset generated scene and seek to first frame. */
static void
ResetScene(SG_ScriptEditCtx *_Nonnull e, SG_ScriptRenderCtx *_Nullable re)
{
SG_Script *scr = e->scr;
SG_ScriptInsn *si;
SG_Clear(e->sg);
scr->t = 0;
scr->tPrev = 0;
if (scr->n > 0) {
TAILQ_FOREACH(si, &scr->frames[0].insns, insns) {
if (ExecInsn(e, re, si) == -1)
break;
}
}
}
/* Render the scripted scene to disk. */
static void
Render(AG_Event *_Nonnull event)
{
char path[AG_PATHNAME_MAX];
SG_ScriptEditCtx *e = AG_PTR(1);
SG_ScriptRenderCtx *re = AG_PTR(2);
AG_Window *winDlg = AG_PTR(3);
AG_DirDlg *dirDlg = AG_PTR(4);
char *outDir = Strdup(dirDlg->cwd);
SG_Script *scr = e->scr;
AG_Driver *drv = WIDGET(e->sv)->drv;
AG_Rect2 r = WIDGET(e->sv)->rView;
Uint8 *buf = NULL;
Uint32 ticks;
Uint nOutFrames = 0;
AG_Dir *dir;
int i;
if ((dir = AG_OpenDir(outDir)) == NULL) {
if (AG_MkPath(outDir) == -1) {
AG_TextMsgFromError();
return;
}
} else {
if (re->clearDir) { /* Delete existing frames */
for (i = 0; i < dir->nents; i++) {
char *dent = dir->ents[i], *c;
if (dent[0] == '.') {
continue;
}
for (c = &dent[0]; *c != '.' && *c != '\0'; c++) {
if (!isdigit(*c))
break;
}
if (*c == '.' || *c == '\0') {
if (*c == '.') {
if (Strcasecmp(c, ".jpg") != 0 &&
Strcasecmp(c, ".jpeg") != 0 &&
Strcasecmp(c, ".png") != 0) {
continue;
}
}
Strlcpy(path, outDir, sizeof(path));
Strlcat(path, AG_PATHSEP, sizeof(path));
Strlcat(path, dent, sizeof(path));
if (AG_FileDelete(path) == -1) {
fprintf(stderr, "%s: %s\n", path,
AG_GetError());
}
}
}
}
AG_CloseDir(dir);
}
if (re->nFirst >= re->nLast) {
AG_SetError("Invalid range");
goto fail;
}
switch (re->interp) {
case SG_SCRIPT_INTERP_LINEAR:
re->nInt = re->timeScale * re->fps / (re->nLast - re->nFirst);
if (re->nInt < 1) { re->nInt = 1; }
break;
default:
re->nInt = 1;
break;
}
ClearEditPane(e);
AG_ObjectDetach(winDlg);
SG_Clear(e->sg);
if ((buf = TryMalloc(r.w*r.h*4)) == NULL)
goto fail;
printf("Rendering %s to %s (@ %f fps)\n",
OBJECT(scr)->name, outDir, re->fps);
scr->tPrev = re->nFirst - 1;
ticks = AG_GetTicks();
for (scr->t = re->nFirst;
scr->t < re->nLast;
scr->t++) {
SG_ScriptFrame *sf = &scr->frames[scr->t];
SG_ScriptInsn *si;
AG_Surface *s;
fprintf(stderr, "Rendering #%d (", scr->t);
for (re->iInt = 0;
re->iInt < re->nInt;
re->iInt++) {
fprintf(stderr, "%u ", nOutFrames);
TAILQ_FOREACH(si, &sf->insns, insns) {
if (ExecInsn(e, re, si) == -1) {
fprintf(stderr, "Insn[%d/%d]: %s; ignoring\n",
re->iInt, re->nInt, AG_GetError());
/* goto fail; */
}
}
Snprintf(path, sizeof(path), "%s%s%08u.jpg",
outDir, AG_PATHSEP, nOutFrames);
/* TODO: render offscreen */
AG_BeginRendering(drv);
AG_ObjectLock(e->win);
AG_WindowDraw(e->win);
AG_ObjectUnlock(e->win);
AG_EndRendering(drv);
glReadPixels(r.x1, WIDGET(e->win)->h - r.y2,
r.w, r.h,
GL_RGBA, GL_UNSIGNED_BYTE, buf);
AG_PackedPixelFlip(buf, r.h, r.w*4);
s = AG_SurfaceFromPixelsRGBA(buf, r.w, r.h, 32,
0x000000ff, 0x0000ff00, 0x00ff0000, 0);
if (s == NULL)
goto fail;
if (AG_SurfaceExportJPEG(s, path, 100,
AG_EXPORT_JPEG_JDCT_ISLOW) == -1) {
AG_SurfaceFree(s);
goto fail;
}
AG_SurfaceFree(s);
nOutFrames++;
}
fprintf(stderr, ")\n");
}
AG_LabelText(e->stat, _("Rendering Complete: %u frames in %s (%.01fs elapsed)"),
nOutFrames, outDir,
(float)(AG_GetTicks() - ticks)/1000);
ResetScene(e, NULL);
Free(buf);
Free(outDir);
return;
fail:
AG_LabelText(e->stat, _("Render Failed: %s"), AG_GetError());
AG_TextMsgFromError();
Free(buf);
Free(outDir);
}
static void
RenderDlg(AG_Event *_Nonnull event)
{
SG_ScriptEditCtx *e = AG_PTR(1);
SG_ScriptRenderCtx *re;
SG_Script *scr = e->scr;
AG_Window *win;
AG_DirDlg *dd;
AG_Box *hBox;
AG_Numerical *num;
if ((win = AG_WindowNew(0)) == NULL) {
return;
}
AG_WindowSetCaption(win, _("Script Rendering: %s"), OBJECT(e->scr)->name);
re = Malloc(sizeof(SG_ScriptRenderCtx));
re->interp = SG_SCRIPT_INTERP_NONE;
re->nInt = 0;
re->iInt = 0;
re->timeScale = (scr->tLast - scr->tFirst)/5;
re->fps = 30.0;
re->clearDir = 1;
re->nFirst = scr->tFirst;
for (re->nLast = scr->tLast; re->nLast >= 0; re->nLast--) {
if (!TAILQ_EMPTY(&scr->frames[re->nLast].insns))
break;
}
if (re->nLast < (scr->tLast-1)) { re->nLast++; }
/* Range */
hBox = AG_BoxNewHoriz(win, AG_BOX_HFILL);
{
AG_LabelNewS(hBox, 0, _("Render #"));
num = AG_NumericalNewInt(hBox, 0, NULL, NULL, &re->nFirst);
AG_NumericalSizeHint(num, "<0>");
AG_LabelNewS(hBox, 0, _("to #"));
num = AG_NumericalNewInt(hBox, 0, NULL, NULL, &re->nLast);
AG_NumericalSizeHint(num, "<0>");
}
/* Time scale */
hBox = AG_BoxNewHoriz(win, AG_BOX_HFILL);
{
AG_LabelNewS(hBox, 0, _("Scaled to: "));
num = AG_NumericalNewS(hBox, 0, "sec", NULL);
AG_BindDouble(num, "value", &re->timeScale);
AG_SetDouble(num, "min", 1.0);
AG_NumericalSetPrecision(num, "f", 1);
AG_NumericalSizeHint(num, "<10.00>");
AG_LabelNewS(hBox, 0, _("at "));
num = AG_NumericalNewS(hBox, 0, NULL, NULL);
AG_BindDouble(num, "value", &re->fps);
AG_SetDouble(num, "min", 1.0);
AG_NumericalSetPrecision(num, "f", 1);
AG_NumericalSizeHint(num, "<00>");
AG_LabelNewS(hBox, 0, _("fps"));
}
/* Interpolation mode */
{
const char *items[] = {
N_("No Interpolation"),
N_("Linear Interpolation"),
NULL
};
AG_RadioNewUint(win, 0, items, &re->interp);
}
AG_SeparatorNewHoriz(win);
AG_LabelNewS(win, 0, _("Output Directory:"));
dd = AG_DirDlgNewMRU(win, "sg-script-output-dir",
AG_DIRDLG_EXPAND|AG_DIRDLG_SAVE|AG_DIRDLG_NOBUTTONS);
/* AG_DirDlgOkAction(dd, Render, "%p,%p", e, win); */
AG_CheckboxNewInt(win, 0, _("Delete Existing Frames"), &re->clearDir);
AG_ButtonNewFn(win, AG_BUTTON_HFILL, _("Render"),
Render, "%p,%p,%p,%p", e, re, win, dd);
AG_WindowSetGeometryAlignedPct(win, AG_WINDOW_MC, 22, 30);
AG_WindowShow(win);
}
static void
SliderChanged(AG_Event *_Nonnull event)
{
SG_ScriptEditCtx *e = AG_PTR(1);
if (e->suUnder != NULL) {
AG_SurfaceFree(e->suUnder);
e->suUnder = NULL;
}
SeekToFrame(e, e->scr->t);
}
static void *_Nullable
Edit(void *_Nonnull obj)
{
SG_ScriptEditCtx *e;
SG_Script *scr = obj;
AG_Mutex *lock = &OBJECT(scr)->pvt.lock;
AG_Window *win;
AG_MenuItem *m;
AG_Label *lbl;
AG_Box *hBox, *statBox;
SG_View *sv;
if ((e = AG_TryMalloc(sizeof(SG_ScriptEditCtx))) == NULL) {
return (NULL);
}
e->boxBtns = NULL;
e->wEdit = NULL;
e->siNew = NULL;
e->suUnder = NULL;
AG_InitTimer(&e->toCamMove, "cam-move", 0);
if ((win = e->win = AG_WindowNew(AG_WINDOW_MAIN)) == NULL) {
return (NULL);
}
AG_WindowSetCaptionS(win, OBJECT(scr)->name);
AG_WindowSetPaddingTop(win, 0);
AG_WindowSetSpacing(win, 0);
e->scr = scr;
e->sg = SG_New(&sgVfsRoot, NULL, 0);
e->sv = sv = SG_ViewNew(NULL, e->sg, SG_VIEW_EXPAND);
AG_SetEvent(sv, "key-down", KeyDown, "%p", e);
/* AG_SetEvent(sv, "key-up", KeyUp, "%p", e); */
AG_SetEvent(sv, "mouse-button-down", MouseButtonDown, "%p", e);
AG_SetEvent(sv, "mouse-button-up", MouseButtonUp, "%p", e);
AG_SetEvent(sv, "mouse-motion", MouseMotion, "%p", e);
AG_SetEvent(sv, "widget-underlay", OnUnderlay, "%p", e);
e->menu = AG_MenuNew(win, AG_MENU_HFILL);
m = AG_MenuNode(e->menu->root, _("File"), NULL);
{
AG_MenuAction(m, _("Render to disk..."), agIconSave.s,
RenderDlg, "%p", e);
AG_MenuSeparator(m);
SG_FileMenu(m, scr, win);
}
m = AG_MenuNode(e->menu->root, _("Edit"), NULL);
{
SG_EditMenu(m, scr, win);
}
m = AG_MenuNode(e->menu->root, _("View"), NULL);
{
SG_ViewMenu(m, e->sg, win, sv);
}
e->paHoriz = AG_PaneNew(win, AG_PANE_HORIZ, AG_PANE_EXPAND);
{
AG_Tlist *tl;
AG_Toolbar *tbInsns;
e->paLeft = AG_PaneNew(e->paHoriz->div[0], AG_PANE_VERT,
AG_PANE_EXPAND|AG_PANE_DIV1FILL);
lbl = AG_LabelNewPolledMT(e->paLeft->div[0], 0, lock,
_("Instructions at f%u: "), &scr->t);
AG_LabelSizeHint(lbl, 1,
_("Instructions at f0000: "));
tl = AG_TlistNew(e->paLeft->div[0], AG_TLIST_POLL|AG_TLIST_TREE|
AG_TLIST_EXPAND|AG_TLIST_MULTI);
WIDGET(tl)->flags &= ~(AG_WIDGET_FOCUSABLE);
AG_TlistSizeHint(tl, "", 2);
AG_SetEvent(tl, "tlist-poll", PollInsns, "%p", scr);
AG_SetEvent(tl, "tlist-changed", SelectInsn, "%p", scr);
AG_SetEvent(tl, "tlist-dblclick", EditInsn, "%p", e);
/* Action buttons */
AG_LabelNew(e->paLeft->div[0], 0, _("Insert Instruction: "));
tbInsns = AG_ToolbarNew(e->paLeft->div[0],
AG_TOOLBAR_VERT, 2, AG_TOOLBAR_HFILL);
{
AG_ToolbarButton(tbInsns, _("Create(Image)"), 0,
InsertCreateInsnDlg, "%p,%p", e, &sgImageClass);
AG_ToolbarButton(tbInsns, _("Create(Object)"), 0,
InsertCreateInsnDlg, "%p,%p", e, &sgObjectClass);
AG_ToolbarButton(tbInsns, _("Create(Polyball)"), 0,
InsertCreateInsnDlg, "%p,%p", e, &sgPolyballClass);
AG_ToolbarButton(tbInsns, _("Delete"), 0,
InsertDeleteInsn, "%p", e);
}
AG_PaneMoveDividerPct(e->paLeft, 50);
}
AG_ObjectAttach(e->paHoriz->div[1], sv);
lbl = AG_LabelNewPolledMT(win, 0, lock, _("Position: %u/%u"),
&scr->t, &scr->tLast);
AG_LabelSizeHint(lbl, 1, _("Position: 0000/0000"));
hBox = AG_BoxNewHoriz(win, AG_BOX_HFILL);
{
AG_Numerical *numAlloc;
e->slTime = AG_SliderNew(hBox, AG_SLIDER_HORIZ, AG_SLIDER_HFILL);
AG_SliderSetControlSize(e->slTime, 32);
AG_BindIntMp(e->slTime, "value", &scr->t, lock);
AG_BindIntMp(e->slTime, "min", &scr->tFirst, lock);
AG_BindIntMp(e->slTime, "max", &scr->tLast, lock);
AG_SetEvent(e->slTime, "slider-changed", SliderChanged, "%p", e);
AG_SeparatorNewVert(hBox);
numAlloc = AG_NumericalNewS(hBox, 0, NULL, NULL);
AG_SetInt(numAlloc, "value", 100);
AG_SetInt(numAlloc, "min", 0);
AG_ButtonNewFn(hBox, 0, "+",
AllocFrames, "%p,%p", scr, numAlloc);
}
statBox = AG_BoxNewHoriz(win, AG_BOX_HFILL|AG_BOX_FRAME);
e->stat = AG_LabelNew(statBox, AG_LABEL_STATIC|AG_LABEL_HFILL, _("Idle"));
AG_PaneMoveDividerPct(e->paHoriz, 20);
AG_WindowSetGeometryAlignedPct(win, AG_WINDOW_MC, 60, 60);
AG_WidgetFocus(sv);
if (scr->n > 0) {
SG_ScriptFrame *sf = &scr->frames[0];
SG_ScriptInsn *si;
TAILQ_FOREACH(si, &sf->insns, insns) {
if (ExecInsn(e, NULL, si) == -1) {
AG_LabelText(e->stat, _("Seek failed: %s"), AG_GetError());
break;
}
}
scr->tPrev = 0;
}
return (win);
}
AG_ObjectClass sgScriptClass = {
"SG_Script",
sizeof(SG_Script),
{ 0,0 },
Init,
Reset,
NULL, /* destroy */
Load,
Save,
Edit
};