2 # Copyright © 2003-2014 Jamie Zawinski <jwz@jwz.org>
4 # Permission to use, copy, modify, distribute, and sell this software and its
5 # documentation for any purpose is hereby granted without fee, provided that
6 # the above copyright notice appear in all copies and that both that
7 # copyright notice and this permission notice appear in supporting
8 # documentation. No representations are made about the suitability of this
9 # software for any purpose. It is provided "as is" without express or
12 # Reads a DXF file, and emits C data suitable for use with OpenGL's
13 # glInterleavedArrays() and glDrawArrays() routines.
17 # --normalize Compute the bounding box of the object, and scale all
18 # coordinates so that the object fits inside a unit cube.
20 # --smooth When computing normals for the vertexes, average the
21 # normals at any edge which is less than 90 degrees.
22 # If this option is not specified, planar normals will be
23 # used, resulting in a "faceted" object.
25 # --wireframe Emit lines instead of faces.
27 # --layers Emit a separate set of polygons for each layer in the
28 # input file, instead of emitting the whole file as a
31 # Created: 8-Mar-2003.
37 use POSIX qw(mktime strftime);
38 use Math::Trig qw(acos);
41 my $progname = $0; $progname =~ s@.*/@@g;
42 my ($version) = ('$Revision: 1.11 $' =~ m/\s(\d[.\d]+)\s/s);
47 # convert a vector to a unit vector
50 my $L = sqrt (($x * $x) + ($y * $y) + ($z * $z));
62 # Calculate the unit normal at p0 given two other points p1,p2 on the
63 # surface. The normal points in the direction of p1 crossproduct p2.
65 sub face_normal($$$$$$$$$) {
68 $p2x, $p2y, $p2z) = @_;
71 my ($pax, $pay, $paz);
72 my ($pbx, $pby, $pbz);
80 $nx = $pay * $pbz - $paz * $pby;
81 $ny = $paz * $pbx - $pax * $pbz;
82 $nz = $pax * $pby - $pay * $pbx;
84 return (normalize ($nx, $ny, $nz));
88 my $pi = 3.141592653589793;
89 my $radians_to_degrees = 180.0 / $pi;
91 # Calculate the angle (in degrees) between two vectors.
93 sub vector_angle($$$$$$) {
97 my $L1 = sqrt ($x1*$x1 + $y1*$y1 + $z1*$z1);
98 my $L2 = sqrt ($x2*$x2 + $y2*$y2 + $z2*$z2);
100 return 0 if ($L1 == 0 || $L2 == 0);
101 return 0 if ($x1 == $x2 && $y1 == $y2 && $z1 == $z2);
103 # dot product of two vectors is defined as:
104 # $L1 * $L1 * cos(angle between vectors)
105 # and is also defined as:
106 # $x1*$x2 + $y1*$y2 + $z1*$z2
108 # $L1 * $L1 * cos($angle) = $x1*$x2 + $y1*$y2 + $z1*$z2
109 # cos($angle) = ($x1*$x2 + $y1*$y2 + $z1*$z2) / ($L1 * $L2)
110 # $angle = acos (($x1*$x2 + $y1*$y2 + $z1*$z2) / ($L1 * $L2));
112 my $cos = ($x1*$x2 + $y1*$y2 + $z1*$z2) / ($L1 * $L2);
113 $cos = 1 if ($cos > 1); # avoid fp rounding error (1.000001 => sqrt error)
114 my $angle = acos ($cos);
116 return ($angle * $radians_to_degrees);
120 # given a list of triangles ( [ X1, Y1, Z1, X2, Y2, Z2, X3, Y3, Z3, ]+ )
121 # returns a list of the normals for each vertex. These are the smoothed
122 # normals: the average of the normals of the participating faces.
124 sub compute_vertex_normals(@) {
126 my $npoints = ($#points+1) / 3;
127 my $nfaces = $npoints / 3;
129 my @face_normals = ();
132 for (my $i = 0; $i < $nfaces; $i++) {
133 my ($ax, $ay, $az, $bx, $by, $bz, $cx, $cy, $cz) =
134 @points[($i*9) .. ($i*9)+8];
136 # store the normal for each face in the $face_normals array
137 # indexed by face number.
139 my @norm = face_normal ($ax, $ay, $az,
142 $face_normals[$i] = \@norm;
144 # store in the %point_faces hash table a list of every face number
145 # in which a point participates
147 foreach my $p ("$ax $ay $az", "$bx $by $bz", "$cx $cy $cz") {
148 my @flist = (defined($point_faces{$p}) ? @{$point_faces{$p}} : ());
150 $point_faces{$p} = \@flist;
155 # compute the normal for each vertex of each face.
156 # (these points are not unique -- because there might be multiple
157 # normals associated with the same vertex for different faces,
158 # in the case where it's a sharp angle.)
161 for (my $i = 0; $i < $nfaces; $i++) {
162 my @verts = @points[($i*9) .. ($i*9)+8];
163 error ("overshot in points?") unless defined($verts[8]);
165 my @norm = @{$face_normals[$i]};
166 error ("no normal $i?") unless defined($norm[2]);
168 # iterate over the (three) vertexes in this face.
170 for (my $j = 0; $j < 3; $j++) {
171 my ($x, $y, $z) = @verts[($j*3) .. ($j*3)+2];
172 error ("overshot in verts?") unless defined($z);
174 # Iterate over the faces in which this point participates.
175 # But ignore any other faces that are at more than an N degree
176 # angle from this point's face. Those are sharp edges.
178 my ($nx, $ny, $nz) = (0, 0, 0);
179 my @faces = @{$point_faces{"$x $y $z"}};
180 foreach my $fn (@faces) {
181 my ($ax, $ay, $az, $bx, $by, $bz, $cx, $cy, $cz) =
182 @points[($fn*9) .. ($fn*9)+8];
183 my @fnorm = @{$face_normals[$fn]};
185 # ignore any adjascent faces that are more than N degrees off.
186 my $angle = vector_angle ($norm[0], $norm[1], $norm[2],
187 $fnorm[0], $fnorm[1], $fnorm[2]);
188 next if ($angle >= 30);
195 push @normals, normalize ($nx, $ny, $nz);
203 sub parse_dxf($$$$$) {
204 my ($filename, $dxf, $normalize_p, $wireframe_p, $layers_p) = @_;
206 $dxf =~ s/\r\n/\n/gs; # CRLF
207 $dxf =~ s/^[ \t\n]+|[ \t\n]+$//s; # leading/trailing whitespace
209 # Convert whitespace within a line to _, e.g., "ObjectDBX Classes".
210 # What the hell is up with this file format!
211 1 while ($dxf =~ s/([^ \t\n])[ \t]+([^ \t\n])/$1_$2/gs);
215 # Turn blank lines into "", e.g., "$DIMBLK \n 1 \n \n 9 \n"
216 $dxf =~ s/\n\n/\n""\n/gs;
218 my @tokens = split (/[ \t\n]+/, $dxf); # tokenize
220 my @entities = (); # parse
223 my $key = shift @tokens; # sectionize at "0 WORD"
225 my $val = shift @tokens;
226 push @elts, [ $key, $val ]; # contents are [CODE VAL]
227 $key = shift @tokens;
228 } while ($key && $key ne 0);
229 unshift @tokens, $key if defined($key);
230 push @entities, \@elts;
232 my %triangles; # list of points, indexed by layer name
236 foreach my $entity (@entities) {
237 my $header = shift @$entity;
238 my ($code, $name) = @$header;
240 if ($name eq 'SECTION' ||
244 print STDERR "$progname: $filename: ignoring \"$code $name\"\n"
247 } elsif ($name eq '3DFACE') {
253 foreach my $entry (@$entity) {
254 my ($key, $val) = @$entry;
255 if ($key eq 8) { $layer = $val; # layer name
257 } elsif ($key eq 10) { $pc++; $points[0] = $val; # X1
258 } elsif ($key eq 20) { $pc++; $points[1] = $val; # Y1
259 } elsif ($key eq 30) { $pc++; $points[2] = $val; # Z1
261 } elsif ($key eq 11) { $pc++; $points[3] = $val; # X2
262 } elsif ($key eq 21) { $pc++; $points[4] = $val; # Y2
263 } elsif ($key eq 31) { $pc++; $points[5] = $val; # Z2
265 } elsif ($key eq 12) { $pc++; $points[6] = $val; # X3
266 } elsif ($key eq 22) { $pc++; $points[7] = $val; # Y3
267 } elsif ($key eq 32) { $pc++; $points[8] = $val; # Z3
269 } elsif ($key eq 13) { $pc++; $points[9] = $val; # X4
270 } elsif ($key eq 23) { $pc++; $points[10] = $val; # Y4
271 } elsif ($key eq 33) { $pc++; $points[11] = $val; # Z4
273 } elsif ($key eq 62) { # color number
274 } elsif ($key eq 70) { # invisible edge flag
276 print STDERR "$progname: $filename: WARNING:" .
277 " unknown $name: \"$key $val\"\n";
282 error ("got $pc points in $name") unless ($pc == 12);
284 if ($points[6] != $points[9] ||
285 $points[7] != $points[10] ||
286 $points[8] != $points[11]) {
287 error ("$filename: got a quad, not a triangle\n");
289 @points = @points[0 .. 8];
292 foreach (@points) { $_ += 0; } # convert strings to numbers
294 $layer = '' unless $layers_p;
296 $triangles{$layer} = [] unless defined ($triangles{$layer});
297 push @{$triangles{$layer}}, @points;
299 } elsif ($name eq 'LINE') {
305 foreach my $entry (@$entity) {
306 my ($key, $val) = @$entry;
307 if ($key eq 8) { $layer = $val; # layer name
309 } elsif ($key eq 10) { $pc++; $points[0] = $val; # X1
310 } elsif ($key eq 20) { $pc++; $points[1] = $val; # Y1
311 } elsif ($key eq 30) { $pc++; $points[2] = $val; # Z1
313 } elsif ($key eq 11) { $pc++; $points[3] = $val; # X2
314 } elsif ($key eq 21) { $pc++; $points[4] = $val; # Y2
315 } elsif ($key eq 31) { $pc++; $points[5] = $val; # Z2
317 } elsif ($key eq 39) { # thickness
318 } elsif ($key eq 62) { # color number
320 print STDERR "$progname: $filename: WARNING:" .
321 " unknown $name: \"$key $val\"\n";
326 error ("got $pc points in $name") unless ($pc == 6);
328 foreach (@points) { $_ += 0; } # convert strings to numbers
330 $layer = '' unless $layers_p;
332 $lines{$layer} = [] unless defined ($lines{$layer});
333 push @{$lines{$layer}}, @points;
335 } elsif ($name =~ m/^\d+$/s) {
336 error ("sequence lost: \"$code $name\"");
339 print STDERR "$progname: $filename: WARNING: unknown: \"$code $name\"\n";
343 error ("too many errors: bailing!") if ($error_count > 50);
348 # Convert faces to lines.
349 # Don't duplicate shared edges.
351 foreach my $layer (keys %triangles) {
353 my @triangles = @{$triangles{$layer}};
355 my $x1 = shift @triangles; # 0
356 my $y1 = shift @triangles; # 1
357 my $z1 = shift @triangles; # 2
358 my $x2 = shift @triangles; # 3
359 my $y2 = shift @triangles; # 4
360 my $z2 = shift @triangles; # 5
361 my $x3 = shift @triangles; # 6
362 my $y3 = shift @triangles; # 7
363 my $z3 = shift @triangles; # 8
366 my ($x1, $y1, $z1, $x2, $y2, $z2) = @_;
367 my $key1 = "$x1, $y1, $z1, $x2, $y2, $z2";
368 my $key2 = "$x2, $y2, $z2, $x1, $y1, $z1";
369 my $dup = $dups{$key1} || $dups{$key2};
372 push @{$lines{$layer}}, @_ unless $dup;
375 $p->($x1, $y1, $z1, $x2, $y2, $z2);
376 $p->($x2, $y2, $z2, $x3, $y3, $z3);
377 $p->($x3, $y3, $z3, $x1, $y1, $z1);
380 @{$triangles{$layer}} = ();
384 foreach my $layer (keys %lines) {
385 my $n = @{$lines{$layer}};
386 @{$lines{$layer}} = ();
387 print STDERR "$progname: $filename: $layer: WARNING:" .
388 " ignored $n stray LINE" . ($n == 1 ? "" : "s") . ".\n"
394 # find bounding box, and normalize
396 if ($normalize_p || $verbose) {
397 my $minx = 999999999;
398 my $miny = 999999999;
399 my $minz = 999999999;
400 my $maxx = -999999999;
401 my $maxy = -999999999;
402 my $maxz = -999999999;
405 foreach my $layer (keys %triangles) {
407 my @triangles = @{$triangles{$layer}};
409 foreach my $n (@{$lines{$layer}}, @{$triangles{$layer}}) {
410 if ($i == 0) { $minx = $n if ($n < $minx);
411 $maxx = $n if ($n > $maxx); }
412 elsif ($i == 1) { $miny = $n if ($n < $miny);
413 $maxy = $n if ($n > $maxy); }
414 else { $minz = $n if ($n < $minz);
415 $maxz = $n if ($n > $maxz); }
416 $i = 0 if (++$i == 3);
420 my $w = ($maxx - $minx);
421 my $h = ($maxy - $miny);
422 my $d = ($maxz - $minz);
423 my $sizea = ($w > $h ? $w : $h);
424 my $sizeb = ($w > $d ? $w : $d);
425 my $size = ($sizea > $sizeb ? $sizea : $sizeb);
427 print STDERR "$progname: $filename: bbox is " .
428 sprintf("%.2f x %.2f x %.2f\n", $w, $h, $d)
430 print STDERR "$progname: $filename: center is " .
431 sprintf("%.2f, %.2f, %.2f\n",
442 print STDERR "$progname: $filename: dividing by " .
443 sprintf("%.2f", $size) . " for bbox of " .
444 sprintf("%.2f x %.2f x %.2f\n", $w, $h, $d)
446 foreach my $layer (keys %triangles) {
447 foreach my $n (@{$triangles{$layer}}) { $n /= $size; }
448 foreach my $n (@{$lines{$layer}}) { $n /= $size; }
453 return ($wireframe_p ? \%lines : \%triangles);
457 sub generate_c_1($$$$$@) {
458 my ($name, $outfile, $smooth_p, $wireframe_p, $normalize_p, @points) = @_;
460 my $ccw_p = 1; # counter-clockwise winding rule for computing normals
462 my $npoints = ($#points + 1) / 3;
463 my $nfaces = ($wireframe_p ? $npoints/2 : $npoints/3);
466 if ($smooth_p && !$wireframe_p) {
467 @normals = compute_vertex_normals (@points);
469 if ($#normals != $#points) {
470 error ("computed " . (($#normals+1)/3) . " normals for " .
471 (($#points+1)/3) . " points?");
475 my $code .= "\nstatic const float ${name}_data[] = {\n";
479 for (my $i = 0; $i < $nfaces; $i++) {
480 my $ax = $points[$i*6];
481 my $ay = $points[$i*6+1];
482 my $az = $points[$i*6+2];
484 my $bx = $points[$i*6+3];
485 my $by = $points[$i*6+4];
486 my $bz = $points[$i*6+5];
488 my $lines = sprintf("\t" . "%.6f,%.6f,%.6f,\n" .
489 "\t" . "%.6f,%.6f,%.6f,\n",
492 $lines =~ s/([.\d])0+,/$1,/g; # lose trailing insignificant zeroes
494 $lines =~ s/-0,/0,/g;
500 for (my $i = 0; $i < $nfaces; $i++) {
501 my $ax = $points[$i*9];
502 my $ay = $points[$i*9+1];
503 my $az = $points[$i*9+2];
505 my $bx = $points[$i*9+3];
506 my $by = $points[$i*9+4];
507 my $bz = $points[$i*9+5];
509 my $cx = $points[$i*9+6];
510 my $cy = $points[$i*9+7];
511 my $cz = $points[$i*9+8];
513 my ($nax, $nay, $naz,
518 $nax = $normals[$i*9];
519 $nay = $normals[$i*9+1];
520 $naz = $normals[$i*9+2];
522 $nbx = $normals[$i*9+3];
523 $nby = $normals[$i*9+4];
524 $nbz = $normals[$i*9+5];
526 $ncx = $normals[$i*9+6];
527 $ncy = $normals[$i*9+7];
528 $ncz = $normals[$i*9+8];
532 ($nax, $nay, $naz) = face_normal ($ax, $ay, $az,
536 ($nax, $nay, $naz) = face_normal ($ax, $ay, $az,
540 ($nbx, $nby, $nbz) = ($nax, $nay, $naz);
541 ($ncx, $ncy, $ncz) = ($nax, $nay, $naz);
544 my $lines = sprintf("\t" . "%.6f,%.6f,%.6f," . "%.6f,%.6f,%.6f,\n" .
545 "\t" . "%.6f,%.6f,%.6f," . "%.6f,%.6f,%.6f,\n" .
546 "\t" . "%.6f,%.6f,%.6f," . "%.6f,%.6f,%.6f,\n",
547 $nax, $nay, $naz, $ax, $ay, $az,
548 $nbx, $nby, $nbz, $bx, $by, $bz,
549 $ncx, $ncy, $ncz, $cx, $cy, $cz);
550 $lines =~ s/([.\d])0+,/$1,/g; # lose trailing insignificant zeroes
552 $lines =~ s/-0,/0,/g;
558 my $format = ($wireframe_p ? 'GL_V3F' : 'GL_N3F_V3F');
559 my $primitive = ($wireframe_p ? 'GL_LINES' : 'GL_TRIANGLES');
563 $code .= "static const struct gllist ${name}_frame = {\n";
564 $code .= " $format, $primitive, $npoints, ${name}_data, 0\n};\n";
565 $code .= "const struct gllist *$name = &${name}_frame;\n";
567 print STDERR "$progname: $outfile: $name: $npoints points, $nfaces faces.\n"
570 return ($code, $npoints, $nfaces);
574 sub generate_c($$$$$$) {
575 my ($infile, $outfile, $smooth_p, $wireframe_p, $normalize_p, $layers) = @_;
579 my $token = $outfile; # guess at a C token from the filename
580 $token =~ s/\<[^<>]*\>//;
582 $token =~ s/\.[^.]*$//;
583 $token =~ s/[^a-z\d]/_/gi;
587 $token =~ tr [A-Z] [a-z];
588 $token = 'foo' if ($token eq '');
590 my @layers = sort (keys %$layers);
592 $infile =~ s@^.*/@@s;
593 $code .= ("/* Generated from \"$infile\" on " .
594 strftime ("%d-%b-%Y", localtime ()) . ".\n" .
598 "Smoothed vertex normals." :
599 "Faceted face normals.")) .
600 ($normalize_p ? " Normalized to unit bounding box." : "") .
603 ? wrap (" ", " ", "Components: " . join (", ", @layers)) . ".\n"
607 $code .= "#include \"gllist.h\"\n";
612 foreach my $layer (@layers) {
613 my $name = $layer ? "${token}_${layer}" : $token;
615 generate_c_1 ($name, $outfile,
616 $smooth_p, $wireframe_p, $normalize_p,
617 @{$layers->{$layer}});
623 print STDERR "$progname: $outfile: total: $npoints points, $nfaces faces.\n"
624 if ($verbose && @layers > 1);
630 # Returns true if the two files differ (by running "cmp")
633 my ($file1, $file2) = @_;
635 my @cmd = ("cmp", "-s", "$file1", "$file2");
636 print STDERR "$progname: executing \"" . join(" ", @cmd) . "\"\n"
640 my $exit_value = $? >> 8;
641 my $signal_num = $? & 127;
642 my $dumped_core = $? & 128;
644 error ("$cmd[0]: core dumped!") if ($dumped_core);
645 error ("$cmd[0]: signal $signal_num!") if ($signal_num);
650 sub dxf_to_gl($$$$$$) {
651 my ($infile, $outfile, $smooth_p, $normalize_p, $wireframe_p, $layers_p) = @_;
653 open (my $in, "<$infile") || error ("$infile: $!");
654 my $filename = ($infile eq '-' ? "<stdin>" : $infile);
655 print STDERR "$progname: reading $filename...\n"
658 local $/ = undef; # read entire file
662 my $data = parse_dxf ($filename, $dxf, $normalize_p, $wireframe_p, $layers_p);
664 $filename = ($outfile eq '-' ? "<stdout>" : $outfile);
665 my $code = generate_c ($infile, $filename, $smooth_p, $wireframe_p,
666 $normalize_p, $data);
668 if ($outfile eq '-') {
671 my $tmp = "$outfile.tmp";
672 open (my $out, '>', $tmp) || error ("$tmp: $!");
673 print $out $code || error ("$filename: $!");
674 close $out || error ("$filename: $!");
675 if (cmp_files ($filename, $tmp)) {
676 if (!rename ($tmp, $filename)) {
678 error ("mv $tmp $filename: $!");
680 print STDERR "$progname: wrote $filename\n";
682 unlink "$tmp" || error ("rm $tmp: $!\n");
683 print STDERR "$progname: $filename unchanged\n" if ($verbose);
691 print STDERR "$progname: $_\n";
696 print STDERR "usage: $progname " .
697 "[--verbose] [--normalize] [--smooth] [--wireframe] [--layers]\n" .
698 "[infile [outfile]]\n";
703 my ($infile, $outfile);
708 while ($_ = $ARGV[0]) {
710 if ($_ eq "--verbose") { $verbose++; }
711 elsif (m/^-v+$/) { $verbose += length($_)-1; }
712 elsif ($_ eq "--normalize") { $normalize_p = 1; }
713 elsif ($_ eq "--smooth") { $smooth_p = 1; }
714 elsif ($_ eq "--wireframe") { $wireframe_p = 1; }
715 elsif ($_ eq "--layers") { $layers_p = 1; }
716 elsif (m/^-./) { usage; }
717 elsif (!defined($infile)) { $infile = $_; }
718 elsif (!defined($outfile)) { $outfile = $_; }
722 $infile = "-" unless defined ($infile);
723 $outfile = "-" unless defined ($outfile);
725 dxf_to_gl ($infile, $outfile, $smooth_p, $normalize_p, $wireframe_p, $layers_p);