chiark / gitweb /
angs mandatory
[trains.git] / layout / layout
1 #!/usr/bin/perl -w
2
3 use POSIX;
4
5 # Data structures:
6 #  $ctx->{CmdLog}= undef                  } not in defobj
7 #  $ctx->{CmdLog}[]= [ command args ]     } in defobj
8 #  $ctx->{LocsMade}[]= $id
9 #  $ctx->{Loc}{$id}{X}
10 #  $ctx->{Loc}{$id}{Y}
11 #  $ctx->{Loc}{$id}{A}
12 #  $ctx->{Trans}{X0}                  } transformation
13 #  $ctx->{Trans}{Y0}                  }  matrix
14 #  $ctx->{Trans}{XY}                  }
15 #  $ctx->{Trans}{YX}                  }
16 #  $ctx->{Trans}{XX}                  }
17 #  $ctx->{Trans}{YY}                  }
18 #  $ctx->{Trans}{AA}                  }
19 #  $ctx->{Trans}{AS}                  }
20 #
21 #  $objs{$id}{CmdLog}
22 #  $objs{$id}{Loc}
23
24 $debug=1;
25 open DEBUG, ($debug ? ">&2" : ">/dev/null") or die $!;
26
27 if ($debug) {
28     select(DEBUG); $|=1;
29     select(STDOUT); $|=1;
30 }
31
32 sub canf ($$) {
33     my ($converter,$defaulter)=@_;
34     my ($spec,$v);
35     return &$defaulter unless @al;
36     $spec= shift @al;
37     $v= &$converter($spec);
38     dv('canf ','$spec',$spec, '$v',$v);
39     return $v;
40 }
41 sub can ($) { my ($c)=@_; canf($c, sub { die "too few args"; }); }
42 sub cano ($$) { my ($c,$def)=@_; canf($c, sub { return $def }); }
43
44 $pi= atan2(0,-1);
45
46 %units_len= qw(- mm  mm 1  cm 10  m 1000);
47 %units_ang= qw(- d   r 1); $units_ang{'d'}= 2*$pi / 360;
48
49 sub cva_len ($) { my ($sp)=@_; cva_units($sp,\%units_len); }
50 sub cva_ang ($) { my ($sp)=@_; cva_units($sp,\%units_ang); }
51 sub cva_absang ($) { input_absang(cva_ang($_[0])) }
52 sub cva_units ($$) {
53     my ($sp,$ua)=@_;
54     my ($n,$u,$r);
55     $sp =~ m/^([-0-9eE.]*[0-9.])([A-Za-z]*)$/
56         or die "lexically invalid quantity";
57     ($n,$u)= ($1,$2);
58     $u=$ua->{'-'} unless length $u;
59     defined $ua->{$u} or die "unknown unit $u";
60     $r= $n * $ua->{$u};
61     print DEBUG "cva_units($sp,)=$r ($n $u $ua->{$u})\n";
62     return $r;
63 }
64 sub cva_idstr ($) {
65     my ($sp)=@_;
66     die "invalid id" unless $sp =~ m/^[a-z][_0-9A-Za-z]*$/;
67     return $&;
68 }
69 sub cva_idex ($) {
70     my ($sp,$id)=@_;
71     my ($r,$d,$k);
72     $id=cva_idstr($sp);
73     die "unknown $id" unless defined $ctx->{Loc}{$id};
74     $r= $ctx->{Loc}{$id};
75     $d= "idex $id";
76     foreach $k (sort keys %$r) { $d .= " $k=$r->{$k}"; }
77     printf DEBUG "%s\n", $d;
78     return $r;
79 }
80 sub cva_idnew ($) {
81     my ($sp)=@_;
82     my ($id);
83     $id=cva_idstr($sp);
84     die "duplicate $id" if exists $ctx->{Loc}{$id};
85     exists $ctx->{Loc}{$id}{X};
86     push @{ $ctx->{LocsMade} }, $id;
87     return $ctx->{Loc}{$id};
88 }
89 sub cva_cmd ($) { return cva_idstr($_[0]); }
90 sub cva__enum ($$) {
91     my ($sp,$el)=@_;
92     return $sp if grep { $_ eq $sp } @$el;
93     die "invalid option (permitted: @$el)";
94 }
95 sub cvam_enum { my (@e) = @_; return sub { cva__enum($_[0],\@e); }; }
96
97 sub cmd_abs {
98     my ($x,$y);
99     $nl= can(\&cva_idnew);
100     $x= can(\&cva_len);
101     $y= can(\&cva_len);
102     ($nl->{X}, $nl->{Y})= input_abscoords($x,$y);
103     $nl->{A}= can(\&cva_absang);
104 }
105
106 sub cmd_rel {
107     $from= can(\&cva_idex);
108     $to= can(\&cva_idnew);
109     $len= can(\&cva_len);
110     $right= can(\&cva_len);
111     $turn= cano(\&cva_ang, 0);
112     $to->{X}= $from->{X} + $len * cos($from->{A}) + $right * sin($from->{A});
113     $to->{Y}= $from->{Y} + $len * sin($from->{A}) - $right * cos($from->{A});
114     $to->{A}= $from->{A} + $turn;
115 }
116
117 sub evreff ($) {
118     my ($pfx) = @_;
119     $pfx . ($pfx =~ m/\}$|\]$/ ? '' : '->');
120 }
121 sub evr ($) {
122     my ($v) = @_;
123     return $v if $v !~ m/\W/ && $v =~ m/[A-Z]/ && $v =~ m/^[a-z_]/i;
124     return $v if $v =~ m/^[0-9.]+/;
125     $v =~ s/[\\\']/\\$&/g;
126     return "'$v'";
127 }
128 sub dv1 ($$$);
129 sub dv1_kind ($$$$$$$) {
130     my ($pfx,$expr,$ref,$ref_exp,$ixfmt,$ixesfn,$ixmapfn) = @_;
131     my ($ix,$any);
132     return 0 if $ref ne $ref_exp;
133     $any=0;
134     foreach $ix (&$ixesfn) {
135         $any=1;
136         my ($v)= &$ixmapfn($ix);
137 #print STDERR "dv1_kind($pfx,$expr,$ref,$ref_exp,$ixmapfn) ix=$ix v=$v\n";
138         dv1($pfx,$expr.sprintf($ixfmt,evr($ix)),$v);
139     }
140     if (!$any) {
141         printf DEBUG "%s%s= $ixfmt\n", $pfx, $expr, ' ';
142     }
143     1;
144 }    
145 sub dv1 ($$$) {
146     return 0 unless $debug;
147     my ($pfx,$expr,$v) = @_;
148     $ref= ref $v;
149 #print STDERR "dv1 >$pfx|$ref<\n";
150     if (!$ref) {
151         printf DEBUG "%s%s= %s\n", $pfx,$expr, evr($v);
152         return;
153     } elsif ($ref eq 'SCALAR') {
154         dv1($pfx, ($expr =~ m/^\$/ ? "\$$expr" : '${'.$expr.'}'), $$v);
155         return;
156     }
157     $expr.='->' unless $expr =~ m/\]$|\}$/;
158     return if dv1_kind($pfx,$expr,$ref,'ARRAY','[%s]',
159                        sub { ($[ .. $#$v) },
160                        sub { $v->[$_[0]] });
161     return if dv1_kind($pfx,$expr,$ref,'HASH','{%s}',
162                        sub { sort keys %$v },
163                        sub { $v->{$_[0]} });
164     printf DEBUG "%s%s is %s\n", $pfx, $expr, $ref;
165 }
166     
167 sub dv {
168     my ($pfx,@l) = @_;
169     my ($expr,$v,$ref);
170     while (@l) {
171         ($expr,$v,@l)=@l;
172         dv1($pfx,$expr,$v);
173     }
174 }                   
175
176 sub loc_lin_comb ($$$) {
177     my ($a,$b,$p) = @_;
178     my ($q,$r) = 1.0-$p;
179     map { $r->{$_} = $q * $a->{$_} + $p * $b->{$_} } qw(X Y A);
180 #    dv("loc_lin_comb ",'$a',$a,'$b',$b,'$p',$p,'$r',$r);
181     return $r;
182 }
183
184 $psu_ulen= 4.5;
185 $psu_edgelw= 0.5;
186 $psu_ticklw= 0.1;
187 $psu_ticksperu= 1;
188 $psu_ticklen= 5.0;
189 $psu_allwidth= 37.0/2;
190 $psu_gauge= 9;
191 $psu_sleeperlen= 17;
192 $psu_sleeperlw= 15;
193 $psu_raillw= 1.0;
194
195 sub o ($) {
196     # fixme optional marking
197     print "$_[0]" or die $!;
198 }
199
200 sub o_path_begin () {
201     o("      newpath\n");
202     $o_path_verb= 'moveto';
203 }
204 sub o_path_point ($) {
205     my ($pt)=@_;
206     o("        $pt $o_path_verb\n");
207     $o_path_verb= 'lineto';
208 }
209 sub o_path_stroke ($) {
210     my ($width)=@_;
211     o("        $width setlinewidth stroke\n");
212 }    
213
214 sub o_line ($$$) {
215     my ($a,$b,$width)=@_;
216     o_path_begin();
217     o_path_point($a);
218     o_path_point($b);
219     o_path_stroke($width);
220 }
221
222 sub psu_coords ($$$) {
223     my ($ends,$inunit,$across)=@_;
224     # $ends->[0]{X} etc.; $inunit 0 to 1 (but go to 1.5);
225     # $across in mm, +ve to right.
226     my (%ea_zo);
227     $ea_zo{X}=$ea_zo{Y}=0;
228     foreach $zo (qw(0 1)) {
229         $prop= $zo ? $inunit : (1.0 - $inunit);
230         $ea_zo{X} += $prop * ($ends->[$zo]{X} - $across * sin($ends->[0]{A}));
231         $ea_zo{Y} += $prop * ($ends->[$zo]{Y} + $across * cos($ends->[0]{A}));
232     }
233 #    dv("psu_coords ", '$ends',$ends, '$inunit',$inunit, '$across',$across,
234 #       '\\%ea_zo', \%ea_zo);
235     return $ea_zo{X}." ".$ea_zo{Y};
236 }
237
238 sub parametric_segment ($$$$$) {
239     my ($endstatuses,$p0,$p1,$lenperp,$calcfn) = @_;
240     # makes $p (global) go from $p0 to $p1  ($p1>$p0)
241     # $ends is II, SI, IS, SS (I=actual lineobj end, S=in mid of lineobj)
242     # $lenperp is the length of one unit p, ie the curve
243     # must have a uniform `density' in parameter space
244     # $calcfn is invoked with $p set and should return a loc
245     # (ie, ref to X =>, Y =>, A =>).
246     my ($pa,$pb,@ends,$side,$ppu,$e,$v,$tick);
247     return if defined $ctx->{InDefObj};
248     $ppu= $psu_ulen/$lenperp;
249     my ($railctr)=($psu_gauge + $psu_raillw)*0.5;
250     my ($tickend)=($psu_allwidth - $psu_ticklen);
251     my ($tickpitch)=($psu_ulen / $psu_ticksperu);
252     my ($sleeperctr)=($psu_ulen*0.5);
253     my ($sleeperend)=($psu_sleeperlen*0.5);
254 print DEBUG "ps $p0 $p1 $lenperp ($ppu)\n";
255     for ($pa= $p0; $pa<$p1; $pa=$pb) {
256         $pb= $pa + $ppu;
257         $p= $pa; $ends[0]= @ends ? $ends[1] : &$calcfn;
258         $p= $pb; $ends[1]= &$calcfn;
259 #print DEBUG "pa $pa $ends[0]{X} $ends[0]{Y} $ends[0]{A}\n";
260 #print DEBUG "pb $pb $ends[1]{X} $ends[1]{Y} $ends[1]{A}\n";
261         $e= $pb<=$p1 ? 1.0 : ($p1-$pa)/$ppu;
262         o("    gsave\n");
263         o_path_begin();
264         o_path_point(psu_coords(\@ends,0,-$psu_allwidth));
265         o_path_point(psu_coords(\@ends,0,$psu_allwidth));
266         o_path_point(psu_coords(\@ends,$e,$psu_allwidth));
267         o_path_point(psu_coords(\@ends,$e,-$psu_allwidth));
268         o("        closepath clip\n");
269         foreach $side qw(-1 1) {
270             o_line(psu_coords(\@ends,0,$side*$psu_allwidth),
271                    psu_coords(\@ends,1.5,$side*$psu_allwidth),
272                    $psu_edgelw);
273             o_line(psu_coords(\@ends,0,$side*$railctr),
274                    psu_coords(\@ends,1.5,$side*$railctr),
275                    $psu_raillw);
276             for ($tick=0; $tick<1.5; $tick+=$tickpitch/$psu_ulen) {
277                 o_line(psu_coords(\@ends,$tick,$side*$psu_allwidth),
278                        psu_coords(\@ends,$tick,$side*$tickend),
279                        $psu_ticklw);
280             }
281         }
282         o_line(psu_coords(\@ends,$sleeperctr,-$sleeperend),
283                psu_coords(\@ends,$sleeperctr,+$sleeperend),
284                $psu_sleeperlw);
285         o("      grestore\n");
286     }
287 }
288
289 sub cmd_extend {
290     my ($from,$to,$radius,$ctr,$beta,$ang,$how,$signum);
291     $from= can(\&cva_idex);
292     $to= can(\&cva_idnew);
293     printf DEBUG "from $from->{X} $from->{Y} $from->{A}\n";
294     $how= can(cvam_enum(qw(len upto ang uptoang parallel)));
295     if ($how eq 'len') { $len= can(\&cva_len); }
296     elsif ($how =~ m/ang$/) { $ang= can(\&cva_ang); }
297     elsif ($how eq 'parallel' || $how eq 'upto') { $upto= can(\&cva_idex); }
298     $radius= cano(\&cva_len, 'Inf'); # +ve is right hand bend
299     if ($radius eq 'Inf') {
300         print DEBUG "extend inf $len\n";
301         if ($how eq 'ang') { die "len of straight spec by angle"; }
302         if ($how eq 'upto') {
303             $len= ($upto->{X} - $from->{X}) * cos($from->{A})
304                 + ($upto->{Y} - $from->{Y}) * sin($from->{A});
305         }
306         printf DEBUG "len $len\n";
307         $to->{X}= $from->{X} + $len * cos($from->{A});
308         $to->{Y}= $from->{Y} + $len * sin($from->{A});
309         $to->{A}= $from->{A};
310         parametric_segment(II, 0.0, 1.0, $len, sub {
311             loc_lin_comb($from, $to, $p);
312         });
313     } else {
314         print DEBUG "radius >$radius<\n";
315         $radius *= $ctx->{Trans}{AA};
316         $signum= $radius / abs($radius);
317         $ctr->{X}= $from->{X} + $radius * sin($from->{A});
318         $ctr->{Y}= $from->{Y} - $radius * cos($from->{A});
319         if ($how eq 'upto') {
320             $beta= atan2(-$signum * ($upto->{X} - $ctr->{X}),
321                          $signum * ($upto->{Y} - $ctr->{Y}));
322             $beta_interval= 1.0;
323         } elsif ($how eq 'parallel') {
324             $beta= $upto->{A};
325             $beta_interval= 1.0;
326         } elsif ($how eq 'uptoang') {
327             $beta= input_absang($ang);
328             $beta_interval= 2.0;
329         } elsif ($how eq 'len') {
330             $beta= $from->{A} - $signum * $len / abs($radius);
331             $beta_interval= 2.0;
332         } else {
333             $beta= $from->{A} - $signum * $ang;
334             $beta_interval= 2.0;
335         }
336     printf DEBUG "ctr->{Y}=$ctr->{Y} radius=$radius beta=$beta\n";
337         $beta += $signum * 4.0 * $pi;
338         for (;;) {
339             $delta= $beta - $from->{A};
340             last if $signum * $delta <= 0;
341             $beta -= $signum * $beta_interval * $pi;
342         }       
343     printf DEBUG "ctr->{Y}=$ctr->{Y} radius=$radius beta=$beta\n";
344         $to->{A}= $beta;
345         $to->{X}= $ctr->{X} - $radius * sin($beta);
346         $to->{Y}= $ctr->{Y} + $radius * cos($beta);
347         parametric_segment(II, 0.0, 1.0, abs($radius*$delta), sub {
348             my ($beta) = $from->{A} + $delta * $p;
349             return { X => $ctr->{X} - $radius * sin($beta),
350                      Y => $ctr->{Y} + $radius * cos($beta),
351                      A => $beta }
352         });
353     }
354     printf DEBUG "to $to->{X} $to->{Y} $to->{A}\n";
355 }
356
357 sub cmd__do {
358     my ($id, $cmd, $loc);
359     $ctx->{LocsMade}= [ ];
360     $cmd= can(\&cva_cmd);
361     &{ "cmd_$cmd" };
362     die "too many args" if @al;
363     foreach $id (@{ $ctx->{LocsMade} }) {
364         $loc= $ctx->{Loc}{$id};
365         o("%  point $id $loc->{X} $loc->{Y} ".ang2deg($loc->{A})."\n");
366     }
367 }
368
369 sub cmd__one {
370     cmd__do();
371 }
372
373 sub ang2deg ($) {
374     return $_[0] * 180 / $pi;
375 }
376 sub input_absang ($) {
377     return $_[0] * $ctx->{Trans}{AA} + $ctx->{Trans}{A0};
378 }
379 sub input_abscoords ($$) {
380     my ($in,$out, $i);
381     ($in->{X}, $in->{Y})= @_;
382     foreach $o (qw(X Y)) {
383         $out->{$o}= $ctx->{Trans}{$o.0};
384         foreach $i (qw(X Y)) {
385             $out->{$o} += $ctx->{Trans}{"$i$o"} * $in->{$i};
386         }
387     }
388     return ($out->{X}, $out->{Y});
389 }
390
391 sub newctx () {
392     $ctx= { Trans => { X0 => 0.0, Y0 => 0.0,
393                        XY => 0.0, YX => 0.0,
394                        A0 => 0.0, AA => 1.0,
395                        XX => 1.0, YY => 1.0 } }
396 }
397
398 sub cmd_defobj {
399     my ($id);
400     $id= can(\&cva_idstr);
401     die "nested defobj" if $defobj_save;
402     die "repeated defobj" if exists $objs{$id};
403     $defobj_save= $ctx;
404     newctx();
405     $ctx->{CmdLog}= [ ];
406     $ctx->{InDefObj}= $id;
407 }
408
409 sub cmd_enddefobj {
410     my ($bit,$id);
411     $id= $ctx->{InDefObj};
412     die "unmatched enddefobj" unless defined $id;
413     foreach $bit (qw(CmdLog Loc)) {
414         $objs{$id}{$bit}= $ctx->{$bit};
415     }
416     $ctx= $defobj_save;
417     $defobj_save= undef;
418 }
419
420 sub cmd_obj { cmd__obj(1); }
421 sub cmd_objflip { cmd__obj(-1); }
422 sub cmd__obj ($) {
423     my ($flipsignum)=@_;
424     my ($obj_id, $ctx_save, $pfx);
425     $obj_id= can(\&cva_idstr);
426     $actual= can(\&cva_idex);
427     $formal_id= can(\&cva_idstr);
428     $obj= $objs{$obj_id};
429     dv("cmd__obj ",'$obj',$obj);
430     die "unknown obj $obj_id" unless $obj;
431     $formal= $obj->{Loc}{$formal_id};
432     die "unknown formal $formal_id" unless $formal;
433     $ctx_save= $ctx;
434     newctx();
435     o("%  obj $obj_id\n");
436     $ctx->{Trans}{AA}= $flipsignum;
437     $ctx->{Trans}{A0}= $actual->{A} - $formal->{A}/$flipsignum;
438     $ctx->{Trans}{XX}= cos($ctx->{Trans}{A0});
439     $ctx->{Trans}{YY}= $flipsignum * cos($ctx->{Trans}{A0});
440     $ctx->{Trans}{XY}= $flipsignum * sin($ctx->{Trans}{A0});
441     $ctx->{Trans}{YX}= -$flipsignum * sin($ctx->{Trans}{A0});
442     ($xformcv,$yformcv)= input_abscoords($formal->{X}, $formal->{Y});
443 print STDERR ">$xformcv|$yformcv<\n";
444     $ctx->{Trans}{X0}= $actual->{X} - $xformcv;
445     $ctx->{Trans}{Y0}= $actual->{Y} - $yformcv;
446     {
447         local (@al);
448         foreach $c (@{ $obj->{CmdLog} }) {
449             @al= @$c;
450             next if $al[0] eq 'enddefobj';
451             cmd__one();
452         }
453     }
454     $pfx= cano(\&cva_idstr,'');
455     if (length $pfx) {
456         foreach $id (keys %{ $ctx->{Loc} }) {
457             $newid= $pfx.$id;
458             next if exists $ctx_save->{Loc}{$newid};
459             $pt= $ctx->{Loc}{$id};
460             $newpt= { A => input_absang($pt->{A}) };
461             ($newpt->{X}, $newpt->{Y})= input_abscoords($pt->{X}, $pt->{Y});
462             $ctx_save->{Loc}{$newid}= $newpt;
463         }
464     }
465     $ctx= $ctx_save;
466 }
467
468 $ptscale= 72/25.4 / 5.0;
469
470 o("%!\n".
471   "  $ptscale $ptscale scale\n");
472
473 newctx();
474     
475 while (<>) {
476     next if m/^\s*\#/;
477     chomp; s/^\s+//; s/\s+$//;
478     @al= split /\s+/, $_;
479     next unless @al;
480     print DEBUG "=== @al\n";
481     push @{ $ctx->{CmdLog} }, [ @al ] if exists $ctx->{CmdLog};
482     cmd__one();
483 }
484 o("  showpage\n");