1 /* Copyright (C) 1992-1998 The Geometry Center
2  * Copyright (C) 1998-2000 Stuart Levy, Tamara Munzner, Mark Phillips
3  *
4  * This file is part of Geomview.
5  *
6  * Geomview is free software; you can redistribute it and/or modify it
7  * under the terms of the GNU Lesser General Public License as published
8  * by the Free Software Foundation; either version 2, or (at your option)
9  * any later version.
10  *
11  * Geomview is distributed in the hope that it will be useful, but
12  * WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with Geomview; see the file COPYING.  If not, write
18  * to the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139,
19  * USA, or visit http://www.gnu.org.
20  */
21 
22 #if HAVE_CONFIG_H
23 # include "config.h"
24 #endif
25 
26 #if 0
27 static char copyright[] = "Copyright (C) 1992-1998 The Geometry Center\n\
28 Copyright (C) 1998-2000 Stuart Levy, Tamara Munzner, Mark Phillips";
29 #endif
30 
31 
32 /* Authors: Stuart Levy, Tamara Munzner, Mark Phillips */
33 
34 #include <stdio.h>
35 #include <math.h>
36 #include <string.h>
37 #include "drawer.h"
38 #include "ui.h"
39 #include "lang.h"
40 #include "lisp.h"
41 #include "mg.h"
42 #include "geom.h"
43 #include "vect.h"
44 #include "color.h"
45 #include "camera.h"
46 #include "space.h"
47 
48 #define	DGobj(obj)  ((DGeom *)obj)
49 #define	DVobj(obj)  ((DView *)obj)
50 
51 char *
spacename(int space)52 spacename(int space)
53 {
54   switch(space) {
55   case TM_SPHERICAL: return "spherical";
56   case TM_EUCLIDEAN: return "euclidean";
57   case TM_HYPERBOLIC: return "hyperbolic";
58   default: return NULL;
59   }
60 }
61 
62 void
camera_space_relabel(int id)63 camera_space_relabel(int id)
64 {
65   DView *dv = (DView *)drawer_get_object(id);
66   char *extra=NULL;
67   char *fmt;
68   char label[256];
69 
70   if(!ISCAM(id) || dv == NULL || dv->mgctx == NULL)
71     return;
72   mgctxselect(dv->mgctx);
73   extra = keywordname(hmodelkeyword("", dv->hmodel));
74   switch(spaceof(WORLDGEOM)) {
75   case TM_SPHERICAL: fmt = "%s (spherical %s view)"; break;
76   case TM_HYPERBOLIC: fmt = "%s (hyperbolic %s view)"; break;
77   case TM_EUCLIDEAN: fmt = "%s (Euclidean%s view)";
78     switch (dv->hmodel) {
79     case VIRTUAL:
80     case PROJECTIVE:
81       extra = "";
82       break;
83     case CONFORMALBALL:
84       extra = " inverted";
85       break;
86     }
87     break;
88   default: return;
89   }
90   sprintf(label, fmt, dv->name[1], extra);
91   mgctxset(MG_WnSet, WN_NAME, label, WN_END, MG_END);
92 }
93 
94 /*
95  * Is this a good directory to select in the file browser by default?
96  * Say it is if it contains the 3-char prefix of our space name.
97  */
98 int
our_space_dir(char * dirname)99 our_space_dir(char *dirname)
100 {
101   char *sname, sub[4];
102   if((sname = spacename(spaceof(WORLDGEOM))) == NULL)
103     return 0;
104   sprintf(sub, "%.3s", sname);
105   return (int)(long)strstr(dirname, sub);
106 }
107 
108 
109 Geom *unitsphere(int n, ColorA *color);
110 
111 #define N  50
112 
113 Geom *
unitsphere(int n,ColorA * color)114 unitsphere(int n, ColorA *color)
115 {
116   int i, j;
117   float si[N+1], ci[N+1], sj[N+1], cj[N+1];
118   Point3 verts[(N-2 + N)*N];
119   short vcounts[N-2 + N/2];
120   short ccounts[N-2 + N/2];
121   Point3 *p;
122   short *vc;
123   Geom *g;
124   float excess = 1. / cos(M_PI / n);
125 
126   for(i = 0; i <= n; i++) {
127     float t = i * 2*M_PI / n;
128     sj[i] = sin(t) * excess;
129     cj[i] = cos(t) * excess;
130     t = i * M_PI / n;
131     si[i] = sin(t);
132     ci[i] = cos(t);
133   }
134 
135   memset(ccounts, 0, sizeof(ccounts));
136   ccounts[0] = color ? 1 : 0;
137 
138   /* Construct n-2 parallels */
139   p = verts;
140   vc = vcounts;
141   for(i = 1; i < n-1; i++) {
142     float y = ci[i];
143     float r = si[i];
144 
145     *vc++ = -n;		/* Closed line with n vertices */
146     for(j = 0; j < n; j++) {
147       p->x = r*cj[j];
148       p->y = y;
149       p->z = r*sj[j];
150       p++;
151     }
152   }
153   /* Construct n/2 meridians, each a full circle */
154   for(j = 0; j < n/2; j++) {
155     float s = sj[j];
156     float c = cj[j];
157 
158     *vc++ = -2*n;
159     for(i = 0; i < n; i++) {
160       p->x = c*si[i];
161       p->y = ci[i];
162       p->z = s*si[i];
163       p++;
164     }
165     for(i = n; i > 0; i--) {
166       p->x = -c*si[i];
167       p->y = ci[i];
168       p->z = -s*si[i];
169       p++;
170     }
171   }
172   g = GeomCreate("vect",
173 		 CR_NVECT,	n-2 + n/2,
174 		 CR_VECTC,	vcounts,
175 		 CR_COLRC,	ccounts,
176 		 CR_NVERT,	(n-2 + n) * n,
177 		 CR_POINT,	verts,
178 		 CR_NCOLR,	color ? 1 : 0,
179 		 CR_COLOR,	color,
180 		 CR_END);
181   return g;
182 }
183 
184 void
set_hsphere_draw(int id,int draw)185 set_hsphere_draw(int id, int draw)
186 {
187   static ColorA color = { .3, .2, .15, .7 };
188   static Geom *hsphere = NULL;
189   DView *dv;
190 
191   if (!ISCAM(id)) return;
192   dv = (DView*)drawer_get_object(id);
193 
194   if (draw < 0) {
195     /* toggle */
196     draw = (dv->hsphere == NULL);
197   }
198 
199   if (draw) {
200     if (!hsphere) {
201       hsphere = unitsphere(16, &color);
202     }
203     dv->hsphere = hsphere;
204   } else {
205     dv->hsphere = NULL;
206   }
207   ui_maybe_refresh(id);
208 }
209 
210 NDcam *
NDcluster(char * name)211 NDcluster(char *name)
212 {
213   NDcam *c;
214 
215   for(c = drawerstate.NDcams; c != NULL; c = c->next)
216     if(strcmp(c->name, name) == 0) return c;
217   return NULL;
218 }
219 
220 NDcam *
NDnewcluster(char * name)221 NDnewcluster(char *name)
222 {
223   NDcam *c;
224 
225   c = NDcluster(name);
226   if(c == NULL) {
227     c = OOGLNewE(NDcam, "NDcam cluster");
228     c->name = strdup(name);
229     c->C2W = TmNIdentity(TmNCreate(drawerstate.NDim,drawerstate.NDim,NULL));
230     c->W2C = NULL;
231     c->next = drawerstate.NDcams;
232     drawerstate.NDcams = c;
233   }
234   return c;
235 }
236 
237 void
NDdeletecluster(NDcam * c)238 NDdeletecluster(NDcam *c)
239 {	/* Add this someday.  Note that there may be other references to this cluster. XXX */
240 }
241 
242 LDEFINE(ND_axes, LLIST,
243 	"(ND-axes CAMID [CLUSTERNAME [Xindex Yindex Zindex [Windex]]])\n\
244 	In our model for N-D viewing (enabled by (dimension)), objects in\n\
245 	N-space are viewed by N-dimensional \"camera clusters\".\n\
246 	Each real camera window belongs to some cluster, and shows &\n\
247 	manipulates a 3-D axis-aligned projected subspace of the N-space seen\n\
248 	by its cluster.  Moving one camera in a cluster affects its siblings.\n\
249 \n	The ND-axes command configures all this.  It specifies a camera's\n\
250 	cluster membership, and the set of N-space axes which become the\n\
251 	3-D camera's X, Y, and Z axes.  Axes are specified by their indices,\n\
252 	from 1 to N for an N-dimensional space.  Cluster CLUSTERNAME is\n\
253 	implicitly created if not previously known.\n\
254 	In principle it is possible to map the homogeneous component\n\
255 	of a conformal 4 point to some other index; this would be done\n\
256 	by specifying 0 for one of Xindex, Yindex or Zindex and giving\n\
257 	Windex some positive value. This is probably not useful because\n\
258 	Geomview does not support non-Euclidean geometries for in higher\n\
259 	dimensions.\n\
260 	\n\
261 	To read a camera's configuration, use \"(echo (ND-axes CAMID))\".\n\
262         The return value is an array of 4 integers, the last one should\n\
263 	be 0.")
264 {
265   int axes[4];
266   char *camname, *clustername = NULL;
267   int i, cam;
268   DView *dv;
269 
270   axes[0] = axes[1] = axes[2] = -1;
271   axes[3] = 0;
272   LDECLARE(("ND-axes", LBEGIN,
273 	    LSTRING, &camname,
274 	    LOPTIONAL,
275 	    LSTRING, &clustername,
276 	    LINT, &axes[0],
277 	    LINT, &axes[1],
278 	    LINT, &axes[2],
279 	    LINT, &axes[3],
280 	    LEND));
281   if((cam = drawer_idbyname(camname)) == NOID ||
282      (dv = (DView *)drawer_get_object(cam)) == NULL || !ISCAM(dv->id)) {
283     OOGLError(0, "ND-axes: unknown camera %s", camname);
284     return Lnil;
285   } else if(axes[0] < 0) {
286     /* return the current ND configuratino of cam */
287     LList *l = NULL;
288     if(dv->cluster) {
289       l = LListAppend(NULL, LTOOBJ(LSTRING)(&dv->cluster->name));
290       for(i = 0; i < 4; i++)
291 	l = LListAppend(l, LNew( LINT, &dv->NDPerm[i] ));
292     }
293     return LNew( LLIST, &l );
294   } else {
295     /* possibly generate a new cluster, or add to an existing
296      * cluster. NOTE: we first reset the camera to its default state.
297      */
298     NDcam *c = NDnewcluster(clustername);
299 
300     for(i = 0; i < 4; i++) {
301       if (axes[i] < 0) {
302 	const char names[4] = { 'X', 'Y', 'Z', 'W' };
303 	OOGLError(1,
304 		  "ERROR: bogus ND-axes specification (%d %d %d %d), %c-entry "
305 		  "must not be negative.\n",
306 		  axes[0], axes[1], axes[2], axes[3], names[i]);
307 	return Lnil;
308       }
309     }
310     NDdeletecluster(dv->cluster); /* a no-op yet */
311     dv->cluster = c;
312     for(i = 0; i < 4; i++) {
313       dv->NDPerm[i] = axes[i];
314     }
315 
316     CamReset(dv->cam);
317 
318     dv->changed |= CH_GEOMETRY;
319     return Lt;
320   }
321 }
322 
set_dimension(int d,TransformN ** Tp,TransformN ** Tinvp)323 static void set_dimension(int d, TransformN **Tp, TransformN **Tinvp)
324 {
325   if(Tinvp) {
326     TmNDelete(*Tinvp);
327     *Tinvp = NULL;
328   }
329   if(Tp == NULL) {
330     return;
331   }
332   if(d == 0) {
333     TmNDelete(*Tp);
334     *Tp = NULL;
335   } else {
336     if(TmNGetSize(*Tp, NULL,NULL) > d) {
337       TmNDelete(*Tp);
338       *Tp = NULL;
339     }
340     *Tp = TmNPad(*Tp, d, d, *Tp);
341   }
342 }
343 
344 LDEFINE(dimension, LLIST,
345 	"(dimension [N])\n\
346 	Sets or reads the space dimension for N-dimensional viewing.\n\
347 	(Since calculations are done using homogeneous coordinates,\n\
348 	this means matrices are (N+1)x(N+1).)\n\
349 	With no arguments, returns the current dimension, or 0 if\n\
350 	N-dimensional viewing has not been enabled. Note that N has to be"
351 	"at least 4 to enable ND-viewing, otherwise ND-viewing will be"
352 	"disabled.")
353 {
354   int i, d = -1;
355 
356   LDECLARE(("dimension", LBEGIN,
357 	    LOPTIONAL,
358 	    LINT, &d,
359 	    LEND));
360 
361   if (d < 0) {
362     d = drawerstate.NDim-1;
363     if(d < 0) {
364       d = 0;
365     }
366     return LNew(LINT, &d);
367   } else {
368     NDcam *c;
369     if (d < 4) { /* we do not allow low-dimension ND-view */
370       d = 0;
371       for(i = 0; i < dview_max; i++) {
372 	if(dview[i] && dview[i]->cluster) {
373 	  NDdeletecluster(dview[i]->cluster);
374 	  dview[i]->cluster = NULL;
375 	}
376       }
377     } else {
378       d++;	/* Include homogeneous coordinate drawerstate.NDim */
379     }
380     if(drawerstate.NDim != d) {
381       for(c = drawerstate.NDcams; c != NULL; c = c->next) {
382 	set_dimension(d, &c->C2W, &c->W2C);
383       }
384       for(i = 0; i < dview_max; i++) {
385 	if(dview[i]) {
386 	  dview[i]->changed = CH_GEOMETRY;
387 	}
388       }
389       for(i = 0; i < dgeom_max; i++) {
390 	if(dgeom[i]) {
391 	  set_dimension(d, &dgeom[i]->NDT, &dgeom[i]->NDTinv);
392 	  dgeom[i]->bboxvalid = false;
393 	  dgeom[i]->changed = CH_GEOMETRY;
394 	}
395       }
396       drawerstate.changed = true;
397       drawerstate.NDim = d;
398     }
399   }
400   return Lt;
401 }
402 
403 LDEFINE(ND_xform, LTRANSFORMN,
404         "(ND-xform OBJID [ntransform { idim odim ... }]\n"
405 	"Concatenate the given ND-transform with the current "
406 	"ND-transform of the object (apply the ND-transform to "
407 	"object ID, as opposed to simply setting its ND-transform)."
408 	"Note that ND-transforms have their homogeneous coordinate at "
409 	"index 0, while 3D transform have it at index 3.")
410 {
411   int id;
412   TransformN *T = NULL;
413   TmNStruct *ts = NULL;
414   DObject *obj;
415   NDcam *cl = NULL;
416 
417   LDECLARE(("ND-xform", LBEGIN,
418 	    LID, &id,
419 	    LTRANSFORMN, &ts,
420 	    LEND));
421 
422   if((obj = drawer_get_object(id)) == NULL || drawerstate.NDim == 0)
423     return Lnil;
424 
425   if(ISGEOM(obj->id)) {
426     T = ((DGeom *)obj)->NDT;
427   } else if(ISCAM(obj->id) && (cl = ((DView *)obj)->cluster)) {
428     T = cl->C2W;
429   }
430 
431   if (!T) {
432     T = REFGET(TransformN, ts->tm);
433   } else {
434     TmNConcat(ts->tm, T, T);
435   }
436 
437   if(ISGEOM(obj->id)) {
438     DGobj(obj)->NDT = T;
439     GeomSet(DGobj(obj)->Item, CR_NDAXIS, DGobj(obj)->NDT, CR_END);
440     obj->changed |= CH_GEOMETRY;
441   } else if(cl != NULL) {
442     cl->C2W = T;
443     drawerstate.changed = true;
444   }
445   TmIdentity(obj->Incr);
446   obj->redraw = true;
447   obj->moving = (obj->updateproc != NULL);
448   return Lt;
449 }
450 
451 LDEFINE(ND_xform_set, LTRANSFORMN,
452 	"(ND-xform-set OBJID [ntransform { idim odim  ... }])\n\
453 	Sets the N-D transform of the given object.\n\
454 	In dimension N, this is an (N+1)x(N+1) matrix, so in that case\n\
455 	idim and odim are expected to be both equal to (N+1). Note that\n\
456 	all cameras in a camera-cluster have the same N-D transform.\n"
457 	"Note that ND-transforms have their homogeneous coordinate at "
458 	"index 0, while 3D transform have it at index 3.")
459 {
460   int id;
461   DObject *obj;
462   TmNStruct *ts = NULL;
463   TransformN *T = NULL;
464   NDcam *cl = NULL;
465 
466   LDECLARE(("ND-xform-set", LBEGIN,
467 	    LID, &id,
468 	    LOPTIONAL,
469 	    LTRANSFORMN, &ts,
470 	    LEND));
471 
472   if((obj = drawer_get_object(id)) == NULL || drawerstate.NDim == 0)
473     return Lnil;
474 
475   if(ISGEOM(obj->id)) {
476     T = ((DGeom *)obj)->NDT;
477   } else if(ISCAM(obj->id) && (cl = ((DView *)obj)->cluster)) {
478     T = cl->C2W;
479   }
480 
481   /* (ND-xform-set id transformn { ... }) -> set transform */
482   TmNDelete(T);
483   if(ISGEOM(obj->id)) {
484     ((DGeom *)obj)->NDT = REFGET(TransformN, ts->tm);
485     GeomSet(DGobj(obj)->Item, CR_NDAXIS, DGobj(obj)->NDT, CR_END);
486     obj->changed |= CH_GEOMETRY;
487   } else if(cl != NULL) {
488     cl->C2W = REFGET(TransformN, ts->tm);
489     drawerstate.changed = true;
490   }
491   return Lt;
492 }
493 
494 LDEFINE(ND_xform_get, LTRANSFORMN,
495 	"(ND-xform-get ID [from-ID])\n\
496 	Returns the N-D transform of the given object in the coordinate\n\
497 	system of from-ID (default \"universe\"), in the sense\n\
498 	<point-in-ID-coords> * Transform = <point-in-from-ID-coords>"
499 	"Note that ND-transforms have their homogeneous coordinate at "
500 	"index 0, while 3D transform have it at index 3.")
501 {
502   int from_id;
503   int to_id = UNIVERSE;
504   TmNStruct *ts;
505   TransformN *tm;
506 
507   extern TransformN *drawer_get_ND_transform(int from_id, int to_id);
508 
509   LDECLARE(("ND-xform-get", LBEGIN,
510 	    LID, &from_id,
511 	    LOPTIONAL,
512 	    LID, &to_id,
513 	    LEND));
514   if((tm = drawer_get_ND_transform(from_id, to_id)) == NULL)
515     return Lnil;
516   ts = OOGLNewE(TmNStruct, "TmN");
517   ts->h = NULL;
518   ts->tm = tm;
519   return LNew(LTRANSFORMN, &ts);
520 }
521 
522 
523 LDEFINE(ND_color, LLIST,
524 	"(ND-color CAMID [ (( [ID] (x1 x2 x3 ... xN) v r g b a   v r g b a  ... )\n\
525 	((x1 ... xN)  v r g b a  v r g b a ...) ...)] )\n\
526 	Specifies a function, applied to each N-D vertex, which determines the\n\
527 	colors of N-dimensional objects as shown in camera CAMID.\n\
528 	Each coloring function is defined by a vector (in ID's coordinate system)\n\
529 	[x1 x1 ... xN] and by a sequence of value (v)/color(r g b a) tuples,\n\
530 	ordered by increasing v.  The inner product v = P.[x] is linearly\n\
531 	interpolated in this table to give a color.\n\
532 	If ID is omitted, the (xi) vector is assumed in universe coordinates.\n\
533 	The ND-color command specifies a list of such functions; each vertex\n\
534 	is colored by their sum (so e.g. green intensity could indicate\n\
535 	projection along one axis while red indicated another.\n\
536 	An empty list, as in (ND-color CAMID ()), suppresses coloring.\n\
537 	With no second argument, (ND-color CAMID) returns that camera's\n\
538 	color-function list.\n\
539 	Even when coloring is enabled, objects tagged with the \"keepcolor\"\n\
540 	appearance attribute are shown in their natural colors.\n")
541 {
542   int i, j, k, id;
543   DView *dv;
544   LList noarg;
545   LList *l = &noarg, *ents;
546   cmap *cm;
547   cent *ce;
548   char *err;
549   int nents, nfields, ncolors;
550 
551   LDECLARE(("ND-color", LBEGIN,
552 	    LID, &id,
553 	    LOPTIONAL,
554 	    LLITERAL,
555 	    LLIST, &l,
556 	    LEND));
557 
558   if((dv = (DView *)drawer_get_object(id)) == NULL || !ISCAM(dv->id)) {
559     OOGLError(0, "ND-color: expected camera name");
560     return Lnil;
561   }
562 
563   if(l == &noarg) {
564     ents = NULL;
565     cm = dv->NDcmap;
566     for(i = 0; i < dv->nNDcmap; i++, cm++) {
567       DObject *dobj = drawer_get_object(cm->coords);
568       char *dname = (dobj && dobj->name[1]) ? dobj->name[1] : "universe";
569 
570       dname = strdup(dname);
571       l = LListAppend(NULL, LNew(LSTRING, (char *)&dname));
572       l = LListAppend(l, LMakeArray(LFLOAT,
573 				    (char *)(cm->axis->v+1), cm->axis->dim-1));
574       for(k = VVCOUNT(cm->cents), ce = VVEC(cm->cents, cent); --k > 0; ce++) {
575 	l = LListAppend(l, LNew(LFLOAT, (char *)&ce->v));
576 	l = LListAppend(l, LNew(LFLOAT, (char *)&ce->c.r));
577 	l = LListAppend(l, LNew(LFLOAT, (char *)&ce->c.g));
578 	l = LListAppend(l, LNew(LFLOAT, (char *)&ce->c.b));
579 	l = LListAppend(l, LNew(LFLOAT, (char *)&ce->c.a));
580       }
581       ents = LListAppend(ents, LNew(LLIST, &l));
582     }
583     return LNew(LLIST, &ents);
584   }
585 
586   ents = l;
587 
588   nents = LListLength(ents);
589   if(nents > MAXCMAP) {
590     OOGLError(0, "Only %d colormaps allowed per camera; using first %d of %d",
591 	      MAXCMAP, MAXCMAP, nents);
592     nents = MAXCMAP;
593   }
594 
595   cm = dv->NDcmap;
596   for(i = 1; i <= nents; i++, cm++) {
597 
598     LObject *ent = LListEntry(ents, i);
599     LList *entlist, *axis;
600     int dim;
601     /*
602      * Each component of the 'ents' list looks like:
603      *  LLIST         ---  v0,r0,g0,b0,a0, v1,r1,g1,b1,a1, ...
604      *    x1,x2,x3,...
605      */
606     if(! LFROMOBJ(LLIST)(ent, &entlist)) {
607       err = "ND-color: expected list of lists";
608       goto no;
609     }
610 
611     cm->coords = UNIVERSE;
612     if( entlist->car->type == LSTRING ) {
613       cm->coords = drawer_idbyname(LSTRINGVAL(entlist->car));
614       if(cm->coords == NOID) {
615 	OOGLError(0, "ND-color: unknown coordinate system %s, using 'universe'",
616 		  LSTRINGVAL(entlist->car));
617 	cm->coords = UNIVERSE;
618       }
619       entlist = entlist->cdr;
620     }
621 
622     if(! LFROMOBJ(LLIST)(entlist->car, &axis)) {
623       err = "ND-color: expected N-D projection axis";
624       goto no;
625     }
626 
627     dim = LListLength(axis);
628     cm->axis = cm->axis ? HPtNPad(cm->axis, dim + 1, cm->axis)
629       : HPtNCreate(dim+1, NULL);
630     /* The projection axis is a vector, so its homogeneous component is zero */
631     cm->axis->v[0] = 0;
632     /* Extract the real components */
633     for(j = 1; j < dim+1; j++) {
634       if(!LFROMOBJ(LFLOAT)(LListEntry(axis, j), &cm->axis->v[j])) {
635 	err = "Non-numeric entry in projection axis?";
636 	goto no;
637       }
638     }
639 
640     entlist = entlist->cdr;	/* Look at the remainder of the list */
641     nfields = LListLength(entlist);
642     ncolors = nfields / 5;
643     if(nfields % 5 != 0 || nfields == 0) {
644       err = "Each colormap should contain a multiple of 5 numbers: v0 r0 g0 b0 a0  v1 r1 g1 b1 a1 ...";
645       goto no;
646     }
647 
648     vvneeds(&cm->cents, ncolors+1);
649     ce = VVEC(cm->cents, cent);
650 
651 
652     for(j = 1; j <= nfields; j += 5, ce++) {
653       ce->interp = 1;
654       if(!LFROMOBJ(LFLOAT)(LListEntry(entlist, j), &ce->v) ||
655 	 !LFROMOBJ(LFLOAT)(LListEntry(entlist, j+1), &ce->c.r) ||
656 	 !LFROMOBJ(LFLOAT)(LListEntry(entlist, j+2), &ce->c.g) ||
657 	 !LFROMOBJ(LFLOAT)(LListEntry(entlist, j+3), &ce->c.b) ||
658 	 !LFROMOBJ(LFLOAT)(LListEntry(entlist, j+4), &ce->c.a) ) {
659 	err = "Non-numeric entry in colormap?";
660 	goto no;
661       }
662     }
663     VVCOUNT(cm->cents) = ncolors+1;
664     ce = &VVEC(cm->cents, cent)[ncolors];
665     *ce = ce[-1];
666     ce->v = 1e20;		/* Huge value terminates list */
667   }
668   dv->nNDcmap = nents;
669   dv->changed |= CH_GEOMETRY;
670   return Lt;
671 
672  no:
673   OOGLError(0, err);
674   return Lnil;
675 }
676 
677 /*
678  * Local Variables: ***
679  * c-basic-offset: 2 ***
680  * End: ***
681  */
682