ldraw/lsynth/src/hose.c

890 lines
25 KiB
C

/*
* This is the LDRAW parts synthesis library.
* By Kevin Clague and Don Heyse
*/
#include <math.h>
#include "lsynthcp.h"
#include "hose.h"
#include "curve.h"
#include "mathlib.h"
#define PI 2*atan2(1,0)
/*
* 0 SYNTH BEGIN DEFINE HOSE <fill> PNEUMATIC_HOSE "descr" <diameter> <stiffness> <twist>
* 1 <length> a b c d e f g h i j k l <part>
* 1 <length> a b c d e f g h i j k l <part>
* 0 SYNTH END
*/
int n_hose_types = 0;
hose_attrib_t hose_types[64];
#define N_HOSE_TYPES n_hose_types
/* In hoses, the attrib field in constraints, indicates that
* LSynth should turn the final constraint around to get everything
* to work correctly (e.g. flex-axle ends).
*/
/*
* 0 SYNTH BEGIN DEFINE HOSE CONSTRAINTS
* 1 <length> a b c d e f g h i j k l <part>
* 1 <length> a b c d e f g h i j k l <part>
* 0 SYNTH END
*/
int n_hose_constraints = 0;
part_t hose_constraints[128];
#define N_HOSE_CONSTRAINTS n_hose_constraints
void
list_hose_types(void)
{
int i;
printf("\n\nHose like synthesizable parts\n");
for (i = 0; i < N_HOSE_TYPES; i++) {
printf(" %-20s %s\n",hose_types[i].type, hose_types[i].descr);
}
}
void
list_hose_constraints(void)
{
int i;
printf("\n\nHose constraints\n");
for (i = 0; i < N_HOSE_CONSTRAINTS; i++) {
printf(" %11s\n",hose_constraints[i].type);
}
}
void
hose_ini(void)
{
int i;
for (i = 0; i < N_HOSE_TYPES; i++) {
printf("%-20s = SYNTH BEGIN %s 16\n",hose_types[i].type, hose_types[i].type);
}
}
int
ishosetype(char *type)
{
int i;
for (i = 0; i < N_HOSE_TYPES; i++) {
if (strncasecmp(hose_types[i].type,type,strlen(hose_types[i].type)) == 0) {
return 1;
}
}
return 0;
}
// casecmp
int
ishoseconstraint(char *type)
{
int i;
for (i = 0; i < N_HOSE_CONSTRAINTS;i++) {
if (strcasecmp(hose_constraints[i].type,type) == 0) {
return 1;
}
}
return 0;
}
PRECISION
line_angle(
PRECISION va[3],
PRECISION vb[3])
{
PRECISION denom;
PRECISION theta;
denom = vectorlen(va)*vectorlen(vb);
if (fabs(denom) < 1e-9) {
//printf("line angle calculation gives us length of zero\n");
return 0;
}
theta = (va[0]*vb[0]+va[1]*vb[1]+va[2]*vb[2])/denom;
if (theta >= 1 || theta <= -1) {
theta = 0;
} else {
theta = acos(theta);
}
return theta;
}
PRECISION
line_angle3(
int a,
int b,
part_t *segments)
{
PRECISION va[3]; /* line A */
PRECISION vb[3]; /* line B */
PRECISION denom;
PRECISION theta;
/* we get the line A as point[a] to point[a+1] */
/* and line B as point[b] to point[b+1] */
vectorsub3(va,segments[a+1].offset,segments[a].offset);
vectorsub3(vb,segments[b+1].offset,segments[b].offset);
return line_angle(va,vb);
}
/*
* merge adjacent points until we either experience too much
* bend, or too much twist
*/
int
merge_segments_angular(
part_t *start,
part_t *end,
part_t *segments,
int *n_segments,
PRECISION max_bend,
PRECISION max_twist,
FILE *output)
{
int a,b;
int n;
PRECISION theta1,theta2;
PRECISION total_length, cur_length;
PRECISION start_up[3] = { 1, 0, 0 };
PRECISION end_up[3] = { 1, 0, 0 };
PRECISION cur_up[3] = { 1, 0, 0 };
PRECISION next_up[3];
PRECISION len[3], sub_len;
vectorrot(start_up,start->orient);
vectorrot(cur_up, start->orient);
vectorrot(end_up, end->orient);
total_length = hose_length(*n_segments, segments);
cur_length = 0;
a = 0; b = 1;
n = 0;
do {
vectorsub3(len,segments[a].offset,segments[b].offset);
sub_len = vectorlen(len);
if (sub_len < 1e-9) {
b++;
}
} while (sub_len < 1e-9);
while (b < *n_segments) {
PRECISION len[3],normalized;
if (b < *n_segments - 1) {
vectorsub3(len,segments[b+1].offset,segments[b].offset);
cur_length += vectorlen(len);
} else {
cur_length += (total_length - cur_length)/2;
}
if (cur_length != 0) {
PRECISION left;
normalized = cur_length/total_length;
left = 1 - normalized;
next_up[0] = start_up[0]*left + end_up[0]*normalized;
next_up[1] = start_up[1]*left + end_up[1]*normalized;
next_up[2] = start_up[2]*left + end_up[2]*normalized;
normalized = next_up[0]*next_up[0] +
next_up[1]*next_up[1] +
next_up[2]*next_up[2];
normalized = sqrt(normalized);
next_up[0] /= normalized;
next_up[1] /= normalized;
next_up[2] /= normalized;
theta2 = line_angle(cur_up,next_up);
theta1 = line_angle3(a,b,segments);
if (theta1 < max_bend && theta2 < max_twist) {
b++;
} else {
segments[n++] = segments[a++];
a = b++;
cur_up[0] = next_up[0];
cur_up[1] = next_up[1];
cur_up[2] = next_up[2];
}
} else {
b++;
}
}
if (n <= 2) {
segments[1] = segments[*n_segments-1];
n = 2;
} else if (b - a > 1) {
//n--; // -= 2;
segments[n-1] = segments[a];
} else {
n--;
}
*n_segments = n;
return 0;
}
int
merge_segments_length(
part_t *segments,
int *n_segments,
PRECISION max,
FILE *output)
{
int a,b;
int n;
PRECISION d[3],l;
a = 0; b = 1;
n = 1;
while (b < *n_segments) {
vectorsub3(d,segments[a].offset,segments[b].offset);
l = vectorlen(d);
if (l + 0.5 < max) {
b++;
} else {
a = b;
segments[n++] = segments[b++];
}
}
if (n < 2) {
segments[1] = segments[*n_segments-1];
n = 2;
} else if (b - a > 1) {
//n--; // -= 2;
segments[n-1] = segments[a];
} else {
n--;
}
*n_segments = n;
return 0;
}
int
merge_segments_count(
hose_attrib_t *hose,
part_t *start,
part_t *end,
part_t *segments,
int *n_segments,
int count,
FILE *output)
{
int n, i;
PRECISION d[3],l;
PRECISION len, lenS, lenM, lenE;
// Get the total length of the curve and divide by the expected segment count.
len = 0;
for (i = 0; i < *n_segments-1; i++) {
vectorsub3(d,segments[i].offset,segments[i+1].offset);
len += vectorlen(d);
}
printf("Total segment len = %.3f\n", len);
// If S or E do not match the N parts subtract their lengths from total.
if ((strcasecmp(hose->start.type, hose->mid.type) != 0) ||
(strcasecmp(hose->end.type, hose->mid.type) != 0) ||
(hose->start.attrib != hose->mid.attrib) ||
(hose->end.attrib != hose->mid.attrib))
{
lenS = hose->start.attrib;
lenE = hose->end.attrib;
lenM = hose->mid.attrib;
len = len - (lenS + lenE);
printf("Net segment len = %.3f (S=%d, M=%d, E= %d)\n", len, lenS, lenM, lenE);
len = len / (PRECISION)(count-1); //len /= (count);
}
else
{
len = len / (PRECISION)(count-1); //len /= (count);
lenS = lenE = len;
}
printf("Merging %d segments to %d segments of len %.3f\n", *n_segments, count, len);
// Break up the curve into count intervals of length len.
l = 0;
n = 1; // Keep the first point.
// Find intermediate points.
for (i = 0; i < *n_segments-1; i++) {
vectorsub3(d,segments[i].offset,segments[i+1].offset);
l += vectorlen(d);
if ((l + 0.05) > (((n-1) * len) + lenS))
segments[n++] = segments[i+1];
//if (n >= count) break;
}
if (lenE != len) // If E did not match above, place it at the constraint.
{
segments[n-1] = segments[*n_segments-1]; // Use the last point twice?
}
segments[n++] = segments[*n_segments-1]; // Keep the last point.
*n_segments = n;
// NOTE: What I really need to do here is place the last point
// segments[n-1].offset at the location of the end constraint + a lenE
// offset in the direction of the orientation of the end constraint.
// otherwise the final vector is tiny and rounding can point it backwards.
// We can see a backwards vector by checking for a negative dot product.
// So, move the last point lenE from the start of the End constraint.
// This should place it at the far point of the end constraint.
d[0] = 0; d[1] = lenE; d[2] = 0; // Create an offset vector of lenE
d[1] *= -1; // along the -Y axis. Reorient it
vectorrot(d,end->orient); // along the end constraint axis, and
vectoradd(segments[n-1].offset,d); // add it to the end constraint origin.
if (0) // Debug printouts
{
extern PRECISION dotprod(PRECISION a[3], PRECISION b[3]); // from curve.c
PRECISION last, next;
PRECISION dn[3],dp;
PRECISION m2[3][3];
matrixcp(m2,end->orient);
printf(" E =(%g, %g, %g, %g, %g, %g, %g, %g, %g)\n", m2[0][0], m2[0][1], m2[0][2],
m2[1][0], m2[1][1], m2[1][2], m2[2][0], m2[2][1], m2[2][2]);
printf(" d=(%g, %g, %g)", d[0], d[1], d[2]);
vectorsub3(d,segments[n-1].offset,segments[n-2].offset);
last = vectorlen(d);
vectorsub3(dn,segments[n-2].offset,segments[n-3].offset);
next = vectorlen(dn);
dp = dotprod(dn,d);
printf(" last=%g, next=%g, dp=%g\n", last, next, dp);
}
printf("Produced %d points (%d segments)\n", *n_segments, *n_segments-1);
// Reorient the segments.
// Warning! Can interact badly with twist if hose makes a dx/dz (dy=0) turn.
// Also, I think this ignores the orientation of the start and end constraints.
// The fin on the constraints should guide the orientation (or twist?) somehow.
// orient(n,segments);
return 0;
}
#define MAX_SEGMENTS 1024*8
part_t segments[MAX_SEGMENTS];
// Create a second list to combine all patches between constraints.
part_t seglist[MAX_SEGMENTS];
void
output_line(
FILE *output,
int ghost,
char *group,
int color,
PRECISION a,
PRECISION b,
PRECISION c,
PRECISION d,
PRECISION e,
PRECISION f,
PRECISION g,
PRECISION h,
PRECISION i,
PRECISION j,
PRECISION k,
PRECISION l,
char *type)
{
if (group) {
fprintf(output,"0 MLCAD BTG %s\n",group);
}
fprintf(output,"%s1 %d %1.4f %1.4f %1.4f %1.4f %1.4f %1.4f %1.4f %1.4f %1.4f %1.4f %1.4f %1.4f %s\n",
ghost ? "0 GHOST " : "",
color,
a,b,c,d,e,f,g,h,i,j,k,l,
type);
group_size++;
fflush(stdout);
}
/*
* Twist
* cos(t) 0 sin(t)
* 0 1 0
* -sin(t) 0 cos(t)
*
* We need to add orientation of the segments based on the orientation
* of the constraint. We must bring the constraint into the normalized
* orientation of the constraint (pointing up at Y), and determine the
* angle of the tab relative to the Y axis.
*/
void
render_hose_segment(
hose_attrib_t *hose,
int ghost,
char *group,
int *group_size,
int color,
part_t *segments,
int n_segments,
PRECISION *total_twist,
int first,
int last,
FILE *output,
part_t *constraint)
{
int i,j,k;
PRECISION pi = 2*atan2(1,0);
PRECISION m1[3][3];
PRECISION m2[3][3];
PRECISION offset[3];
char *type;
int gs = *group_size;
for (i = 0; i < n_segments-1; i++) {
PRECISION tx,ty,tz,l,theta;
if (hose->fill != STRETCH) {
l = 1;
} else {
PRECISION d[3];
vectorsub3(d,segments[i+1].offset,segments[i].offset);
// the length of the segment is the distance between the
// two points
l = vectorlen(d);
// plus the length needed to make sure that the outer edges
// of the cross sections touch
if (n_segments > 1) {
PRECISION phi;
phi = line_angle3(i+1,i,segments);
phi = sin(phi)*(hose->diameter);
l += phi + phi;
}
}
m1[0][0] = 1;
m1[0][1] = 0;
m1[0][2] = 0;
m1[1][0] = 0;
m1[1][1] = l; // Stretch it by length of segment.
m1[1][2] = 0;
m1[2][0] = 0;
m1[2][1] = 0;
m1[2][2] = 1;
// Default offset is nothing.
offset[0] = 0; offset[1] = 0; offset[2] = 0;
if (i == 0 && first) {
type = hose->start.type;
vectorcp(offset,hose->start.offset);
if (hose->start.attrib != 0) // FIXED size start, do not stretch it.
matrixcp(m2,hose->start.orient);
else // Stretch it.
matrixmult3(m2,hose->start.orient,m1);
} else if (i == n_segments-2 && last) {
type = hose->end.type;
vectorcp(offset,hose->end.offset);
if (hose->end.attrib != 0) // FIXED size end, do not stretch it.
matrixcp(m2,hose->end.orient);
else // Stretch it.
matrixmult3(m2,hose->end.orient,m1);
} else if ((i & 0x01) && (strlen(hose->alt.type) != 0)) {
if (i == 1) printf("ALT = %s\n", hose->alt.type);
type = hose->alt.type;
vectorcp(offset,hose->alt.offset);
matrixmult3(m2,hose->alt.orient,m1);
} else {
type = hose->mid.type;
vectorcp(offset,hose->mid.offset);
matrixmult3(m2,hose->mid.orient,m1);
}
/*
* twist helps with string or chains
*/
m1[0][0] = 1;
m1[0][1] = 0;
m1[0][2] = 0;
m1[1][0] = 0;
m1[1][1] = 1;
m1[1][2] = 0;
m1[2][0] = 0;
m1[2][1] = 0;
m1[2][2] = 1;
if (hose->fill != STRETCH) {
PRECISION angle;
*total_twist += hose->twist; // One twist before calculating angle.
#define ORIENT_FN_FIXED_FOR_XZ_AND_YZ_CURVES
#ifdef ORIENT_FN_FIXED_FOR_XZ_AND_YZ_CURVES
// For N FIXED segments start with a full twist (not a half twist).
if (hose->fill > FIXED) {
angle = *total_twist * pi / 180; // Calculate angle after the twist.
}else
#endif
{
angle = *total_twist * pi / 360; // Not (pi/180) because we double the twist.
*total_twist += hose->twist; // Another twist after calculating angle.
// NOTE:
// Breaking the twist up this way draws the Minifig chain with
// the links twisted 45 degrees from the start post (instead of 90).
// This looks OK because the real chains sometimes rest that way.
// It also avoids the problem with the orient() fn.
// Chains which turn in the XZ or YZ planes may rotate 45 degrees
// the other way because orient() only works in the XY plane.
// But that's better than the 90 degrees wrong without this hack.
}
m1[0][0] = cos(angle);
m1[0][2] = sin(angle);
m1[2][0] = -sin(angle);
m1[2][2] = cos(angle);
}
matrixmult(m1,m2);
matrixmult3(m2,segments[i].orient,m1);
// NOTE: We handle hose->(start,mid,end).orient here, but we do nothing
// with the hose->(start,mid,end).offset.
// I really think we need to use this to fix the minifig chain link, the
// origin of which is not centered. Instead its ~4Y over toward one end.
#if 0
offset[0] = 0; offset[1] = 0; offset[2] = 0;
vectoradd(offset,hose->mid.offset); // Should also consider first and last.
#else
// Get offset (first, mid, or last) from lsynth.mpd file above.
// That way it matches the first, mid, or last orient from lsynth.mpd.
#endif
vectorrot(offset,m2);
vectoradd(segments[i].offset,offset);
// FIXME: I would expect to have to flip the array along the diagonal
// like we have to do in input.
output_line(output,ghost,group,color,
segments[i].offset[0], segments[i].offset[1], segments[i].offset[2],
m2[0][0], m2[0][1], m2[0][2],
m2[1][0], m2[1][1], m2[1][2],
m2[2][0], m2[2][1], m2[2][2],
type);
if (group) {
gs++;
}
}
*group_size = gs;
}
void
adjust_constraint(
part_t *part,
part_t *orig,
int last)
{
int i;
PRECISION m[3][3];
*part = *orig;
for (i = 0; i < N_HOSE_CONSTRAINTS; i++) {
if (strcasecmp(part->type,hose_constraints[i].type) == 0) {
// adjust the constraints offset via hose_constraint
vectorcp(part->offset,hose_constraints[i].offset);
vectorrot(part->offset,hose_constraints[i].orient);
vectoradd(part->offset,orig->offset);
// compensate part orient via hose_constraint
matrixcp(part->orient,hose_constraints[i].orient);
matrixmult(part->orient,orig->orient);
if (hose_constraints[i].attrib && last) {
matrixcp(m,part->orient);
matrixneg(part->orient,m);
}
break;
}
}
return;
}
/*
* a 1x1 brick is 20 LDU wide and 24 LDU high
*
* hose length = 14 brick widths long = 280 LDU
* number of ribs = 45
* 6.2 LDU per rib
*
*/
void
render_hose(
hose_attrib_t *hose,
int n_constraints,
part_t *constraints,
PRECISION bend_res,
PRECISION twist_res,
int ghost,
char *group,
int group_size,
int color,
FILE *output)
{
int c, n_segments;
part_t mid_constraint;
PRECISION total_twist = 0;
if ( ! ldraw_part) {
fprintf(output,"0 SYNTH SYNTHESIZED BEGIN\n");
}
// First and Last parts for STRETCH hose could be FIXED length.
// Add up to two new constraints to handle this. They're not used afterwards.
// Just don't go over 128 constraints.
if (hose->fill == STRETCH) {
PRECISION offset[3];
PRECISION l;
#ifdef DEBUGGING_HOSES
printf("STRETCH = (%d, %d, %d)\n",
hose->start.attrib, hose->mid.attrib, hose->end.attrib);
#endif
l = hose->start.attrib;
if (l != 0.0) {
n_constraints++;
for (c = n_constraints-1; c > 0; c--) {
memcpy(&constraints[c], &constraints[c-1], sizeof(part_t));
}
offset[0] = 0; offset[1] = -l; offset[2] = 0;
vectorrot(offset,constraints[0].orient);
vectoradd(constraints[1].offset, offset);
}
l = hose->end.attrib;
if (l != 0.0){
n_constraints++;
c = n_constraints-1;
memcpy(&constraints[c], &constraints[c-1], sizeof(part_t));
offset[0] = 0; offset[1] = l; offset[2] = 0;
vectorrot(offset,constraints[n_constraints-1].orient);
vectoradd(constraints[n_constraints-2].offset, offset);
}
}
mid_constraint = constraints[0];
for (c = 0; c < n_constraints - 1; c++) {
part_t first,second;
// reorient imperfectly oriented or displaced constraint types
adjust_constraint(&first,&mid_constraint,0);
// reorient imperflectly oriented or displaced constraint types
adjust_constraint(&second, &constraints[c+1],c == n_constraints-2);
n_segments = MAX_SEGMENTS;
// For N FIXED segments break up segments[MAX_SEGMENTS] into chunks.
if (hose->fill > FIXED)
{
// Break up seglist into fixed size chunks based on number of constraints.
// We could do better by considering the actual length of each chunk...
n_segments = MAX_SEGMENTS / (n_constraints - 1);
//if (c == 0) printf("FIXED%d, N_constraints = %d, chunksize = %d\n", hose->fill, n_constraints, n_segments);
}
// create an oversampled curve
if (hose->fill == FIXED) // Save room for end constraint point.
synth_curve(&first,&second,segments,n_segments-1,hose->stiffness,output);
else if (hose->fill == STRETCH)
synth_curve(&first,&second,segments,n_segments-1,hose->stiffness,output);
else if (hose->fill > FIXED)
synth_curve(&first,&second,segments,n_segments-1,hose->stiffness,output);
else // Old way. Overwrite last point with end constraint point. Not good.
synth_curve(&first,&second,segments,n_segments,hose->stiffness,output);
// reduce oversampled curve to fixed length chunks, or segments limit
// by angular resolution
if (hose->fill == STRETCH) {
// Make sure final segment matches second constraint
vectorcp(segments[n_segments-1].offset,second.offset);
merge_segments_angular(
&first,
&second,
segments,
&n_segments,
bend_res,
twist_res,
output);
// Make sure final segment matches second constraint
vectorcp(segments[n_segments-1].offset,second.offset);
// move normalized result back into its original orientation and position
mid_constraint = constraints[c+1];
#ifdef DEBUGGING_HOSES
printf("orient(N_SEGMENTS = %d)\n", n_segments);
#endif
orientq(&first,&second,n_segments,segments); // With quaternions!
}
else if (hose->fill == FIXED) {
// Make sure final segment point matches second constraint
vectorcp(segments[n_segments-1].offset,second.offset);
#ifdef ADJUST_FINAL_FIXED_HOSE_END
// Hmmm, how do we make a hose comprised of fixed length parts
// reach exactly to the end constraint?
// This is OK for ribbed hoses
// but not for string or chain, so we probably shouldn't do it.
if (c == n_constraints-2)
{
int i = n_segments;
memcpy(seglist, segments, n_segments*sizeof(part_t));
// Set i to how many segments we need to get near to the end.
merge_segments_length(seglist,&i,hose->mid.attrib,output);
// Squish an extra part into the last segment to make it reach the end.
merge_segments_count(hose,&first,&second,segments,&n_segments,i,output); // Yuck!
// Or, stretch the last part of the hose a bit to make it reach the end.
// merge_segments_count(hose, segments,&n_segments,i-1,output); // Yuckier!
}
else
#endif
merge_segments_length(segments,&n_segments,hose->mid.attrib,output);
// move normalized result back into its original orientation and position
mid_constraint = constraints[c+1];
vectorcp(mid_constraint.offset,segments[n_segments-1].offset);
orient(&first,&second,n_segments,segments);
//orientq(&first,&second,n_segments,segments);
}
else { // For N fixed size chunks just copy into one big list, merge later.
// Make sure final segment matches second constraint
vectorcp(segments[n_segments-1].offset,second.offset);
// move normalized result back into its original orientation and position
mid_constraint = constraints[c+1];
vectorcp(mid_constraint.offset,segments[n_segments-2].offset);
memcpy(seglist+(n_segments*c), segments, n_segments*sizeof(part_t));
}
// output the result (if not FIXED number of segments)
if (hose->fill <= FIXED)
render_hose_segment(
hose,
ghost,
group,
&group_size,
color,
segments,n_segments,
&total_twist,
c ==0,
c == n_constraints-2,
output,
&constraints[0]);
}
// output the result (if FIXED number of segments)
if (hose->fill > FIXED) {
part_t first,second;
// Reorient imperfectly oriented or displaced constraint types.
// NOTE: I really need to study the new orient code and see why it
// does not seem to work until after merge_segment_count() below.
// It needs to orient based on ALL constraints, not just first and last.
adjust_constraint(&first,&constraints[0],0);
adjust_constraint(&second, &constraints[n_constraints-1],1);
n_segments *= c;
// Make sure final segment matches second constraint
vectorcp(segments[n_segments-1].offset,second.offset);
merge_segments_count(hose,&first,&second,seglist,&n_segments,hose->fill,output);
//printf("Merged segments to %d segments of len %d\n", n_segments, hose->mid.attrib);
//orient(&first,&second,n_segments,seglist);
orientq(&first,&second,n_segments,seglist); // With quaternions!
//printf("oriented %d\n",n_segments);
render_hose_segment(
hose,
ghost,
group,
&group_size,
color,
seglist,n_segments,
&total_twist,
1, // First AND
1, // Last part of the hose. (seglist = one piece hose)
output,
&constraints[0]);
//printf("Total twist = %.1f (%.1f * %.1f) n = %d\n", total_twist, hose->twist, total_twist/hose->twist, n_segments);
}
if (group) {
fprintf(output,"0 GROUP %d %s\n",group_size,group);
}
if ( ! ldraw_part) {
fprintf(output,"0 SYNTH SYNTHESIZED END\n");
}
}
int
synth_hose(
char *type,
int n_constraints,
part_t *constraints,
int ghost,
char *group,
int group_size,
int color,
FILE *output)
{
int i;
for (i = 0; i < N_HOSE_TYPES; i++) {
if (strcasecmp(hose_types[i].type,type) == 0) {
render_hose(
&hose_types[i],
n_constraints,constraints,
max_bend,
max_twist,
ghost,
group,
group_size,
color,
output);
return 0;
}
}
return 1;
}