1 // Hyperbolic Rogue -- models of hyperbolic geometry
2 // Copyright (C) 2011-2019 Zeno Rogue, see 'hyper.cpp' for details
3 
4 /** \file models.cpp
5  *  \brief models of hyperbolic geometry: their properties, projection menu
6  *
7  *  The actual models are implemented in hypgraph.cpp. Also shaders.cpp,
8  *  drawing.cpp, and basegraph.cpp are important.
9  */
10 
11 #include "hyper.h"
12 namespace hr {
13 
14 EX namespace polygonal {
15 
16   #if ISMOBWEB
17   typedef double precise;
18   #else
19   typedef long double precise;
20   #endif
21 
22   #if HDR
23   static const int MSI = 120;
24   #endif
25 
26   typedef long double xld;
27 
28   typedef complex<xld> cxld;
29 
30   EX int SI = 4;
31   EX ld  STAR = 0;
32 
33   EX int deg = ISMOBWEB ? 2 : 20;
34 
35   precise matrix[MSI][MSI];
36   precise ans[MSI];
37 
38   cxld coef[MSI];
39   EX ld coefr[MSI], coefi[MSI];
40   EX int maxcoef, coefid;
41 
solve()42   EX void solve() {
43     if(pmodel == mdPolynomial) {
44       for(int i=0; i<MSI; i++) coef[i] = cxld(coefr[i], coefi[i]);
45       return;
46       }
47     if(pmodel != mdPolygonal) return;
48     if(SI < 3) SI = 3;
49     for(int i=0; i<MSI; i++) ans[i] = cos(M_PI / SI);
50     for(int i=0; i<MSI; i++)
51       for(int j=0; j<MSI; j++) {
52         precise i0 = (i+0.) / (MSI-1);
53         // i0 *= i0;
54         // i0 = 1 - i0;
55         i0 *= M_PI;
56         matrix[i][j] =
57           cos(i0 * (j + 1./SI)) * (STAR > 0 ? (1+STAR) : 1)
58         - sin(i0 * (j + 1./SI)) * (STAR > 0 ? STAR : STAR/(1+STAR));
59         }
60 
61     for(int i=0; i<MSI; i++) {
62       precise dby = matrix[i][i];
63       for(int k=0; k<MSI; k++) matrix[i][k] /= dby;
64       ans[i] /= dby;
65       for(int j=i+1; j<MSI; j++) {
66         precise sub = matrix[j][i];
67         ans[j] -= ans[i] * sub;
68         for(int k=0; k<MSI; k++)
69            matrix[j][k] -= sub * matrix[i][k];
70         }
71       }
72     for(int i=MSI-1; i>=0; i--) {
73       for(int j=0; j<i; j++) {
74         precise sub = matrix[j][i];
75         ans[j] -= ans[i] * sub;
76         for(int k=0; k<MSI; k++)
77            matrix[j][k] -= sub * matrix[i][k];
78         }
79       }
80     }
81 
compute(ld x,ld y,int prec)82   EX pair<ld, ld> compute(ld x, ld y, int prec) {
83     if(x*x+y*y > 1) {
84       xld r  = hypot(x,y);
85       x /= r;
86       y /= r;
87       }
88     if(pmodel == mdPolynomial) {
89       cxld z(x,y);
90       cxld res (0,0);
91       for(int i=maxcoef; i>=0; i--) { res += coef[i]; if(i) res *= z; }
92       return make_pair(real(res), imag(res));
93       }
94 
95     cxld z(x, y);
96     cxld res (0,0);
97     cxld zp = 1; for(int i=0; i<SI; i++) zp *= z;
98 
99     for(int i=prec; i>0; i--) {
100       res += ans[i];
101       res *= zp;
102       }
103     res += ans[0]; res *= z;
104     return make_pair(real(res), imag(res));
105     }
106 
compute(ld x,ld y)107   EX pair<ld, ld> compute(ld x, ld y) { return compute(x,y,deg); }
108   EX }
109 
110 #if HDR
mdAzimuthalEqui()111 inline bool mdAzimuthalEqui() { return among(pmodel, mdEquidistant, mdEquiarea, mdEquivolume); }
mdBandAny()112 inline bool mdBandAny() { return mdinf[pmodel].flags & mf::pseudoband; }
mdPseudocylindrical()113 inline bool mdPseudocylindrical() { return mdBandAny() && !(mdinf[pmodel].flags & mf::cylindrical); }
114 #endif
115 
116 EX namespace models {
117 
118   EX ld rotation = 0;
119   EX ld rotation_xz = 90;
120   EX ld rotation_xy2 = 90;
121   EX int do_rotate = 1;
122   EX ld ocos, osin, ocos_yz, osin_yz;
123   EX ld cos_ball, sin_ball;
124   EX bool model_straight, model_straight_yz;
125 
126   #if HDR
127     // screen coordinates to logical coordinates: apply_orientation(x,y)
128   // logical coordinates back to screen coordinates: apply_orientation(y,x)
129   template<class A>
apply_orientation(A & x,A & y)130   void apply_orientation(A& x, A& y) { if(!model_straight) tie(x,y) = make_pair(x*ocos + y*osin, y*ocos - x*osin); }
131   template<class A>
apply_orientation_yz(A & x,A & y)132   void apply_orientation_yz(A& x, A& y) { if(!model_straight_yz) tie(x,y) = make_pair(x*ocos_yz + y*osin_yz, y*ocos_yz - x*osin_yz); }
133   template<class A>
apply_ball(A & x,A & y)134   void apply_ball(A& x, A& y) { tie(x,y) = make_pair(x*cos_ball + y*sin_ball, y*cos_ball - x*sin_ball); }
135   #endif
136 
rotmatrix()137   EX transmatrix rotmatrix() {
138     if(GDIM == 2 || prod) return spin(rotation * degree);
139     return spin(rotation_xy2 * degree) * cspin(0, 2, -rotation_xz * degree) * spin(rotation * degree);
140     }
141 
142   int spiral_id = 7;
143 
144   EX cld spiral_multiplier;
145   EX ld spiral_cone_rad;
146   EX bool ring_not_spiral;
147 
148   /** the matrix to rotate the Euclidean view from the standard coordinates to the screen coordinates */
149   EX transmatrix euclidean_spin;
150 
configure()151   EX void configure() {
152     ld ball = -pconf.ballangle * degree;
153     cos_ball = cos(ball), sin_ball = sin(ball);
154     ocos = cos(pconf.model_orientation * degree);
155     osin = sin(pconf.model_orientation * degree);
156     ocos_yz = cos(pconf.model_orientation_yz * degree);
157     osin_yz = sin(pconf.model_orientation_yz * degree);
158     model_straight = (ocos > 1 - 1e-9);
159     model_straight_yz = GDIM == 2 || (ocos_yz > 1-1e-9);
160     if(history::on) history::apply();
161 
162     if(!euclid) {
163       ld b = pconf.spiral_angle * degree;
164       ld cos_spiral = cos(b);
165       ld sin_spiral = sin(b);
166       spiral_cone_rad = pconf.spiral_cone * degree;
167       ring_not_spiral = abs(cos_spiral) < 1e-3;
168       ld mul = 1;
169       if(sphere) mul = .5 * pconf.sphere_spiral_multiplier;
170       else if(ring_not_spiral) mul = pconf.right_spiral_multiplier;
171       else mul = pconf.any_spiral_multiplier * cos_spiral;
172 
173       spiral_multiplier = cld(cos_spiral, sin_spiral) * cld(spiral_cone_rad * mul / 2., 0);
174       }
175     if(euclid) {
176       euclidean_spin = pispin * iso_inverse(cview().T * currentmap->master_relative(centerover, true));
177       euclidean_spin = gpushxto0(euclidean_spin * C0) * euclidean_spin;
178       hyperpoint h = inverse(euclidean_spin) * (C0 + (euc::eumove(gp::loc{1,0})*C0 - C0) * vpconf.spiral_x + (euc::eumove(gp::loc{0,1})*C0 - C0) * vpconf.spiral_y);
179       spiral_multiplier = cld(0, 2 * M_PI) / cld(h[0], h[1]);
180       }
181 
182     if(centerover && !history::on)
183     if(isize(history::path_for_lineanimation) == 0 || ((quotient || arb::in()) && history::path_for_lineanimation.back() != centerover)) {
184       history::path_for_lineanimation.push_back(centerover);
185       }
186     }
187 
model_available(eModel pm)188   EX bool model_available(eModel pm) {
189     if(prod) {
190       if(pm == mdPerspective) return true;
191       if(among(pm, mdBall, mdHemisphere)) return false;
192       return PIU(model_available(pm));
193       }
194     if(sl2) return pm == mdGeodesic;
195     if(nonisotropic) return among(pm, mdDisk, mdPerspective, mdHorocyclic, mdGeodesic, mdEquidistant, mdFisheye);
196     if(pm == mdGeodesic && !sol) return false;
197     if(sphere && (pm == mdHalfplane || pm == mdBall))
198       return false;
199     if(GDIM == 2 && pm == mdPerspective) return false;
200     if(GDIM == 2 && pm == mdEquivolume) return false;
201     if(pm == mdThreePoint && !(GDIM == 3 && !nonisotropic && !prod)) return false;
202     if(GDIM == 3 && among(pm, mdBall, mdHyperboloid, mdFormula, mdPolygonal, mdRotatedHyperboles, mdSpiral, mdHemisphere)) return false;
203     if(pm == mdCentralInversion && !euclid) return false;
204     if(pm == mdPoorMan) return hyperbolic;
205     if(pm == mdRetroHammer) return hyperbolic;
206     return true;
207     }
208 
has_orientation(eModel m)209   EX bool has_orientation(eModel m) {
210     if(m == mdHorocyclic)
211       return hyperbolic;
212     if((m == mdPerspective || m == mdGeodesic) && panini_alpha) return true;
213     return
214       among(m, mdHalfplane, mdPolynomial, mdPolygonal, mdTwoPoint, mdJoukowsky, mdJoukowskyInverted, mdSpiral, mdSimulatedPerspective, mdTwoHybrid, mdHorocyclic, mdAxial, mdAntiAxial, mdQuadrant,
215         mdWerner, mdAitoff, mdHammer, mdLoximuthal, mdWinkelTripel, mdThreePoint) || mdBandAny();
216     }
217 
218   /** @brief returns the broken coordinate, or zero */
get_broken_coord(eModel m)219   EX int get_broken_coord(eModel m) {
220     if(m == mdWerner) return 1;
221     if(sphere) return (mdinf[m].flags & mf::broken) ? 2 : 0;
222     return 0;
223     }
224 
is_perspective(eModel m)225   EX bool is_perspective(eModel m) {
226     return among(m, mdPerspective, mdGeodesic);
227     }
228 
is_3d(const projection_configuration & p)229   EX bool is_3d(const projection_configuration& p) {
230     if(GDIM == 3) return true;
231     return among(p.model, mdBall, mdHyperboloid, mdHemisphere) || (p.model == mdSpiral && p.spiral_cone != 360);
232     }
233 
has_transition(eModel m)234   EX bool has_transition(eModel m) {
235     return among(m, mdJoukowsky, mdJoukowskyInverted, mdBand, mdAxial) && GDIM == 2;
236     }
237 
product_model(eModel m)238   EX bool product_model(eModel m) {
239     if(!prod) return false;
240     if(among(m, mdPerspective, mdHyperboloid, mdEquidistant, mdThreePoint)) return false;
241     return true;
242     }
243 
244   int editpos = 0;
245 
get_model_name(eModel m)246   EX string get_model_name(eModel m) {
247     if(m == mdDisk && GDIM == 3 && (hyperbolic || nonisotropic)) return XLAT("ball model/Gans");
248     if(m == mdPerspective && prod) return XLAT("native perspective");
249     if(prod) return PIU(get_model_name(m));
250     if(nonisotropic) {
251       if(m == mdHorocyclic && !sol) return XLAT("simple model: projection");
252       if(m == mdPerspective) return XLAT("simple model: perspective");
253       if(m == mdGeodesic) return XLAT("native perspective");
254       if(among(m, mdEquidistant, mdFisheye, mdHorocyclic)) return XLAT(mdinf[m].name_hyperbolic);
255       }
256     if(m == mdDisk && GDIM == 3) return XLAT("perspective in 4D");
257     if(m == mdHalfplane && GDIM == 3 && hyperbolic) return XLAT("half-space");
258     if(sphere)
259       return XLAT(mdinf[m].name_spherical);
260     if(euclid)
261       return XLAT(mdinf[m].name_euclidean);
262     if(hyperbolic)
263       return XLAT(mdinf[m].name_hyperbolic);
264     return "?";
265     }
266 
267   vector<gp::loc> torus_zeros;
268 
match_torus_period()269   void match_torus_period() {
270     torus_zeros.clear();
271     for(int y=0; y<=200; y++)
272     for(int x=-200; x<=200; x++) {
273       if(y == 0 && x <= 0) continue;
274       transmatrix dummy = Id;
275       euc::coord v(x, y, 0);
276       bool mirr = false;
277       auto t = euc::eutester;
278       euc::eu.canonicalize(v, t, dummy, mirr);
279       if(v == euc::euzero && t == euc::eutester)
280         torus_zeros.emplace_back(x, y);
281       }
282     sort(torus_zeros.begin(), torus_zeros.end(), [] (const gp::loc p1, const gp::loc p2) {
283       ld d1 = hdist0(tC0(euc::eumove(p1)));
284       ld d2 = hdist0(tC0(euc::eumove(p2)));
285       if(d1 < d2 - 1e-6) return true;
286       if(d1 > d2 + 1e-6) return false;
287       return p1 < p2;
288       });
289     if(spiral_id > isize(torus_zeros)) spiral_id = 0;
290     dialog::editNumber(spiral_id, 0, isize(torus_zeros)-1, 1, 10, XLAT("match the period of the torus"), "");
291     dialog::reaction = [] () {
292       auto& co = torus_zeros[spiral_id];
293       vpconf.spiral_x = co.first;
294       vpconf.spiral_y = co.second;
295       };
296     dialog::bound_low(0);
297     dialog::bound_up(isize(torus_zeros)-1);
298     }
299 
edit_formula()300   EX void edit_formula() {
301     if(vpconf.model != mdFormula) vpconf.basic_model = vpconf.model;
302     dialog::edit_string(vpconf.formula, "formula",
303       XLAT(
304       "This lets you specify the projection as a formula f. "
305       "The formula has access to the value 'z', which is a complex number corresponding to the (x,y) coordinates in the currently selected model; "
306       "the point z is mapped to f(z). You can also use the underlying coordinates ux, uy, uz."
307       )
308       );
309     #if CAP_QUEUE && CAP_CURVE
310     dialog::extra_options = [] () {
311       dialog::parser_help();
312       initquickqueue();
313       queuereset(mdPixel, PPR::LINE);
314       for(int a=-1; a<=1; a++) {
315         curvepoint(point2(-M_PI/2 * current_display->radius, a*current_display->radius));
316         curvepoint(point2(+M_PI/2 * current_display->radius, a*current_display->radius));
317         queuecurve(shiftless(Id), forecolor, 0, PPR::LINE);
318         curvepoint(point2(a*current_display->radius, -M_PI/2*current_display->radius));
319         curvepoint(point2(a*current_display->radius, +M_PI/2*current_display->radius));
320         queuecurve(shiftless(Id), forecolor, 0, PPR::LINE);
321         }
322       queuereset(vpconf.model, PPR::LINE);
323       quickqueue();
324       };
325     #endif
326     dialog::reaction_final = [] () {
327       vpconf.model = mdFormula;
328       };
329     }
330 
edit_rotation(ld & which)331   EX void edit_rotation(ld& which) {
332     dialog::editNumber(which, 0, 360, 90, 0, XLAT("rotation"),
333       "This controls the automatic rotation of the world. "
334       "It affects the line animation in the history mode, and "
335       "lands which have a special direction. Note that if finding this special direction is a part of the puzzle, "
336       "it works only in the cheat mode.");
337     dialog::dialogflags |= sm::CENTER;
338     dialog::extra_options = [] () {
339       dialog::addBreak(100);
340       dialog::addBoolItem_choice("line animation only", models::do_rotate, 0, 'N');
341       dialog::addBoolItem_choice("gravity lands", models::do_rotate, 1, 'G');
342       dialog::addBoolItem_choice("all directional lands", models::do_rotate, 2, 'D');
343       if(GDIM == 3) {
344         dialog::addBreak(100);
345         dialog::addSelItem(XLAT("XY plane"), fts(models::rotation) + "°", 'A');
346         dialog::add_action([] { popScreen(); edit_rotation(models::rotation); });
347         dialog::addSelItem(XLAT("XZ plane"), fts(models::rotation_xz) + "°", 'B');
348         dialog::add_action([] { popScreen(); edit_rotation(models::rotation_xz); });
349         dialog::addSelItem(XLAT("XY plane #2"), fts(models::rotation_xy2) + "°", 'C');
350         dialog::add_action([] { popScreen(); edit_rotation(models::rotation_xy2); });
351         }
352       };
353     }
354 
model_list()355   EX void model_list() {
356     cmode = sm::SIDE | sm::MAYDARK | sm::CENTER;
357     gamescreen(0);
358     dialog::init(XLAT("models & projections"));
359     #if CAP_RUG
360     USING_NATIVE_GEOMETRY_IN_RUG;
361     #endif
362 
363     for(int i=0; i<mdGUARD; i++) {
364       eModel m = eModel(i);
365       if(m == mdFormula && ISMOBILE) continue;
366       if(model_available(m)) {
367         dialog::addBoolItem(get_model_name(m), vpconf.model == m, (i < 26 ? 'a'+i : 'A'+i-26));
368         dialog::add_action([m] () {
369           if(m == mdFormula) {
370             edit_formula();
371             return;
372             }
373           vpconf.model = m;
374           polygonal::solve();
375           vpconf.alpha = 1; vpconf.scale = 1;
376           if(pmodel == mdBand && sphere)
377             vpconf.scale = .3;
378           if(pmodel == mdDisk && sphere)
379             vpconf.scale = .4;
380           popScreen();
381           });
382         }
383       }
384 
385     dialog::display();
386     }
387 
stretch_extra()388   void stretch_extra() {
389     dialog::addBreak(100);
390     if(sphere && pmodel == mdBandEquiarea) {
391       dialog::addBoolItem("Gall-Peters", vpconf.stretch == 2, 'O');
392       dialog::add_action([] { vpconf.stretch = 2; dialog::ne.s = "2"; });
393       }
394     if(pmodel == mdBandEquiarea) {
395       // y = K * sin(phi)
396       // cos(phi) * cos(phi) = 1/K
397       if(sphere && vpconf.stretch >= 1) {
398         ld phi = acos(sqrt(1/vpconf.stretch));
399         dialog::addInfo(XLAT("The current value makes the map conformal at the latitude of %1 (%2°).", fts(phi), fts(phi / degree)));
400         }
401       else if(hyperbolic && abs(vpconf.stretch) <= 1 && abs(vpconf.stretch) >= 1e-9) {
402         ld phi = acosh(abs(sqrt(1/vpconf.stretch)));
403         dialog::addInfo(XLAT("The current value makes the map conformal %1 units from the main line.", fts(phi)));
404         }
405       else dialog::addInfo("");
406       }
407     }
408 
409   bool set_vr_settings = true;
410 
model_menu()411   EX void model_menu() {
412     cmode = sm::SIDE | sm::MAYDARK | sm::CENTER;
413     gamescreen(0);
414     #if CAP_RUG
415     USING_NATIVE_GEOMETRY_IN_RUG;
416     #endif
417     dialog::init(XLAT("models & projections"));
418 
419     auto vpmodel = vpconf.model;
420 
421     dialog::addSelItem(XLAT("projection type"), get_model_name(vpmodel), 'm');
422     dialog::add_action_push(model_list);
423 
424     if(nonisotropic && !sl2)
425       dialog::addBoolItem_action(XLAT("geodesic movement in Sol/Nil"), nisot::geodesic_movement, 'G');
426 
427     dialog::addBoolItem(XLAT("rotation"), do_rotate == 2, 'r');
428     if(do_rotate == 0) dialog::lastItem().value = XLAT("NEVER");
429     if(GDIM == 2)
430       dialog::lastItem().value += " " + its(rotation) + "°";
431     else
432       dialog::lastItem().value += " " + its(rotation) + "°" + its(rotation_xz) + "°" + its(rotation_xy2) + "°";
433     dialog::add_action([] { edit_rotation(rotation); });
434 
435     bool vr_settings = vrhr::active() && set_vr_settings;
436 
437     if(vrhr::active()) {
438       dialog::addBoolItem_action(XLAT("edit VR or non-VR settings"), set_vr_settings, 'V');
439       if(set_vr_settings) dialog::items.back().value = "VR";
440       else dialog::items.back().value = "non-VR";
441       }
442 
443     // if(vpmodel == mdBand && sphere)
444     if(!in_perspective_v() && !vr_settings) {
445       dialog::addSelItem(XLAT("scale factor"), fts(vpconf.scale), 'z');
446       dialog::add_action(editScale);
447       }
448 
449     if(abs(vpconf.alpha-1) > 1e-3 && vpmodel != mdBall && vpmodel != mdHyperboloid && vpmodel != mdHemisphere && vpmodel != mdDisk) {
450       dialog::addBreak(50);
451       dialog::addInfo("NOTE: this works 'correctly' only if the Poincaré model/stereographic projection is used.");
452       dialog::addBreak(50);
453       }
454 
455     if(among(vpmodel, mdDisk, mdBall, mdHyperboloid, mdRotatedHyperboles, mdPanini)) {
456       dynamicval<eModel> v(vpconf.model, vpconf.model);
457       if(vpmodel == mdHyperboloid) vpconf.model = mdDisk;
458       add_edit(vpconf.alpha);
459       }
460 
461     if(has_orientation(vpmodel)) {
462       dialog::addSelItem(XLAT("model orientation"), fts(vpconf.model_orientation) + "°", 'l');
463       dialog::add_action([] () {
464         dialog::editNumber(vpconf.model_orientation, 0, 360, 90, 0, XLAT("model orientation"), "");
465         });
466       if(GDIM == 3) {
467         dialog::addSelItem(XLAT("model orientation (y/z plane)"), fts(vpconf.model_orientation_yz) + "°", 'L');
468         dialog::add_action([] () {
469           dialog::editNumber(vpconf.model_orientation_yz, 0, 360, 90, 0, XLAT("model orientation (y/z plane)"), "");
470           });
471         }
472       }
473 
474     if(among(vpmodel, mdPerspective, mdHorocyclic) && nil) {
475       dialog::addSelItem(XLAT("model orientation"), fts(vpconf.model_orientation) + "°", 'l');
476       dialog::add_action([] () {
477         dialog::editNumber(vpconf.model_orientation, 0, 360, 90, 0, XLAT("model orientation"), "");
478         });
479       dialog::addSelItem(XLAT("rotational or Heisenberg"), fts(vpconf.rotational_nil), 'L');
480       dialog::add_action([] () {
481         dialog::editNumber(vpconf.rotational_nil, 0, 1, 1, 1, XLAT("1 = Heisenberg, 0 = rotational"), "");
482         });
483       }
484 
485   if(GDIM == 3 && vpmodel != mdPerspective && !vr_settings) {
486     const string cliphelp = XLAT(
487       "Your view of the 3D model is naturally bounded from four directions by your window. "
488       "Here, you can also set up similar bounds in the Z direction. Radius of the ball/band "
489       "models, and the distance from the center to the plane in the halfspace model, are 1.\n\n");
490     dialog::addSelItem(XLAT("near clipping plane"), fts(vpconf.clip_max), 'c');
491     dialog::add_action([cliphelp] () {
492       dialog::editNumber(vpconf.clip_max, -10, 10, 0.2, 1, XLAT("near clipping plane"),
493         cliphelp + XLAT("Objects with Z coordinate "
494           "bigger than this parameter are not shown. This is useful with the models which "
495           "extend infinitely in the Z direction, or if you want things close to your character "
496           "to be not obscured by things closer to the camera."));
497       });
498     dialog::addSelItem(XLAT("far clipping plane"), fts(vpconf.clip_min), 'C');
499     dialog::add_action([cliphelp] () {
500       dialog::editNumber(vpconf.clip_min, -10, 10, 0.2, -1, XLAT("far clipping plane"),
501         cliphelp + XLAT("Objects with Z coordinate "
502           "smaller than this parameter are not shown; it also affects the fog effect"
503           " (near clipping plane = 0% fog, far clipping plane = 100% fog)."));
504       });
505     }
506 
507     if(vpmodel == mdPolynomial) {
508       dialog::addSelItem(XLAT("coefficient"),
509         fts(polygonal::coefr[polygonal::coefid]), 'x');
510       dialog::add_action([] () {
511         polygonal::maxcoef = max(polygonal::maxcoef, polygonal::coefid);
512         int ci = polygonal::coefid + 1;
513         dialog::editNumber(polygonal::coefr[polygonal::coefid], -10, 10, .01/ci/ci, 0, XLAT("coefficient"), "");
514         });
515       dialog::addSelItem(XLAT("coefficient (imaginary)"),
516         fts(polygonal::coefi[polygonal::coefid]), 'y');
517       dialog::add_action([] () {
518         polygonal::maxcoef = max(polygonal::maxcoef, polygonal::coefid);
519         int ci = polygonal::coefid + 1;
520         dialog::editNumber(polygonal::coefi[polygonal::coefid], -10, 10, .01/ci/ci, 0, XLAT("coefficient (imaginary)"), "");
521         });
522       dialog::addSelItem(XLAT("which coefficient"), its(polygonal::coefid), 'n');
523       dialog::add_action([] () {
524         dialog::editNumber(polygonal::coefid, 0, polygonal::MSI-1, 1, 0, XLAT("which coefficient"), "");
525         dialog::bound_low(0); dialog::bound_up(polygonal::MSI-1);
526         });
527       }
528 
529     if(vpmodel == mdHalfplane) {
530       dialog::addSelItem(XLAT("half-plane scale"), fts(vpconf.halfplane_scale), 'b');
531       dialog::add_action([] () {
532         dialog::editNumber(vpconf.halfplane_scale, 0, 2, 0.25, 1, XLAT("half-plane scale"), "");
533         });
534       }
535 
536     if(vpmodel == mdRotatedHyperboles) {
537       dialog::addBoolItem_action(XLAT("use atan to make it finite"), vpconf.use_atan, 'x');
538       }
539 
540     if(vpmodel == mdBall && !vr_settings) {
541       dialog::addSelItem(XLAT("projection in ball model"), fts(vpconf.ballproj), 'x');
542       dialog::add_action([] () {
543         dialog::editNumber(vpconf.ballproj, 0, 100, .1, 0, XLAT("projection in ball model"),
544           "This parameter affects the ball model the same way as the projection parameter affects the disk model.");
545         });
546       }
547 
548     if(vpmodel == mdPolygonal) {
549       dialog::addSelItem(XLAT("polygon sides"), its(polygonal::SI), 'x');
550       dialog::add_action([] () {
551         dialog::editNumber(polygonal::SI, 3, 10, 1, 4, XLAT("polygon sides"), "");
552         dialog::reaction = polygonal::solve;
553         });
554       dialog::addSelItem(XLAT("star factor"), fts(polygonal::STAR), 'y');
555       dialog::add_action([]() {
556         dialog::editNumber(polygonal::STAR, -1, 1, .1, 0, XLAT("star factor"), "");
557         dialog::reaction = polygonal::solve;
558         });
559       dialog::addSelItem(XLAT("degree of the approximation"), its(polygonal::deg), 'n');
560       dialog::add_action([](){
561         dialog::editNumber(polygonal::deg, 2, polygonal::MSI-1, 1, 2, XLAT("degree of the approximation"), "");
562         dialog::reaction = polygonal::solve;
563         dialog::bound_low(0); dialog::bound_up(polygonal::MSI-1);
564         });
565       }
566 
567     if(is_3d(vpconf) && GDIM == 2 && !vr_settings)
568       add_edit(vpconf.ballangle);
569 
570     if(vr_settings) {
571       dialog::addSelItem(XLAT("VR: rotate the 3D model"), fts(vpconf.vr_angle) + "°", 'B');
572       dialog::add_action([] {
573         dialog::editNumber(vpconf.vr_angle, 0, 90, 5, 0, XLAT("VR: rotate the 3D model"),
574           "How the VR model should be rotated."
575           );
576         });
577       dialog::addSelItem(XLAT("VR: shift the 3D model"), fts(vpconf.vr_zshift), 'Z');
578       dialog::add_action([] {
579         dialog::editNumber(vpconf.vr_zshift, 0, 5, 0.1, 1, XLAT("VR: shift the 3D model"),
580           "How the VR model should be shifted forward, in units. "
581           "The Poincaré disk has the size of 1 unit. You probably do not want this in perspective projections, but "
582           "it is useful to see e.g. the Poincaré ball not from the center."
583           );
584         });
585       dialog::addSelItem(XLAT("VR: scale the 3D model"), fts(vpconf.vr_scale_factor) + "m", 'S');
586       dialog::add_action([] {
587         dialog::editNumber(vpconf.vr_scale_factor, 0, 5, 0.1, 1, XLAT("VR: scale the 3D model"),
588           "How the VR model should be scaled. At scale 1, 1 unit = 1 meter. Does not affect perspective projections, "
589           "where the 'absolute unit' setting is used instead."
590           );
591         });
592       }
593 
594     if(vpmodel == mdHyperboloid)
595       add_edit(vpconf.top_z);
596 
597     if(has_transition(vpmodel))
598       add_edit(vpconf.model_transition);
599 
600     if(among(vpmodel, mdJoukowsky, mdJoukowskyInverted, mdSpiral) && GDIM == 2)
601       add_edit(vpconf.skiprope);
602 
603     if(vpmodel == mdHemisphere && euclid)
604       add_edit(vpconf.euclid_to_sphere);
605 
606     if(among(vpmodel, mdTwoPoint, mdSimulatedPerspective, mdTwoHybrid, mdThreePoint))
607       add_edit(vpconf.twopoint_param);
608 
609     if(vpmodel == mdFisheye)
610       add_edit(vpconf.fisheye_param);
611 
612     if(vpmodel == mdHyperboloid)
613       add_edit(pconf.show_hyperboloid_flat);
614 
615     if(vpmodel == mdCollignon)
616       add_edit(vpconf.collignon_parameter);
617 
618     if(vpmodel == mdMiller) {
619       dialog::addSelItem(XLAT("parameter"), fts(vpconf.miller_parameter), 'b');
620       dialog::add_action([](){
621         dialog::editNumber(vpconf.miller_parameter, -1, 1, .1, 4/5., XLAT("parameter"),
622           "The Miller projection is obtained by multiplying the latitude by 4/5, using Mercator projection, and then multiplying Y by 5/4. "
623           "Here you can change this parameter."
624           );
625         });
626       }
627 
628     if(among(vpmodel, mdLoximuthal, mdRetroHammer, mdRetroCraig))
629       add_edit(vpconf.loximuthal_parameter);
630 
631     if(among(vpmodel, mdAitoff, mdHammer, mdWinkelTripel))
632       add_edit(vpconf.aitoff_parameter);
633 
634     if(vpmodel == mdWinkelTripel)
635       add_edit(vpconf.winkel_parameter);
636 
637     if(vpmodel == mdSpiral && !euclid) {
638       add_edit(vpconf.spiral_angle);
639 
640       add_edit(
641         sphere ? vpconf.sphere_spiral_multiplier :
642         ring_not_spiral ? vpconf.right_spiral_multiplier :
643         vpconf.any_spiral_multiplier
644         );
645 
646       add_edit(vpconf.spiral_cone);
647       }
648 
649     if(vpmodel == mdSpiral && euclid) {
650       add_edit(vpconf.spiral_x);
651       add_edit(vpconf.spiral_y);
652       if(euclid && quotient) {
653         dialog::addSelItem(XLAT("match the period"), its(spiral_id), 'n');
654         dialog::add_action(match_torus_period);
655         }
656       }
657 
658     add_edit(vpconf.stretch);
659 
660     if(product_model(vpmodel))
661       add_edit(vpconf.product_z_scale);
662 
663     #if CAP_GL
664     dialog::addBoolItem(XLAT("use GPU to compute projections"), vid.consider_shader_projection, 'G');
665     bool shaderside_projection = get_shader_flags() & SF_DIRECT;
666     if(vid.consider_shader_projection && !shaderside_projection)
667       dialog::lastItem().value = XLAT("N/A");
668     if(vid.consider_shader_projection && shaderside_projection && vpmodel)
669       dialog::lastItem().value += XLAT(" (2D only)");
670     dialog::add_action([] { vid.consider_shader_projection = !vid.consider_shader_projection; });
671     #endif
672 
673     menuitem_sightrange('R');
674 
675     dialog::addBreak(100);
676     dialog::addItem(XLAT("history mode"), 'a');
677     dialog::add_action_push(history::history_menu);
678 #if CAP_RUG
679     if(GDIM == 2 || rug::rugged) {
680       dialog::addItem(XLAT("hypersian rug mode"), 'u');
681       dialog::add_action_push(rug::show);
682       }
683 #endif
684     dialog::addBack();
685 
686     dialog::display();
687     mouseovers = XLAT("see http://www.roguetemple.com/z/hyper/models.php");
688     }
689 
quick_model()690   EX void quick_model() {
691     cmode = sm::CENTER;
692     gamescreen(1);
693     dialog::init("models & projections");
694 
695     if(GDIM == 2 && !euclid) {
696       dialog::addItem(hyperbolic ? XLAT("Gans model") : XLAT("orthographic projection"), '1');
697       dialog::add_action([] { if(rug::rugged) rug::close(); pconf.alpha = 999; pconf.scale = 998; pconf.xposition = pconf.yposition = 0; popScreen(); });
698       dialog::addItem(hyperbolic ? XLAT("Poincaré model") : XLAT("stereographic projection"), '2');
699       dialog::add_action([] { if(rug::rugged) rug::close(); pconf.alpha = 1; pconf.scale = 1; pconf.xposition = pconf.yposition = 0; popScreen(); });
700       dialog::addItem(hyperbolic ? XLAT("Beltrami-Klein model") : XLAT("gnomonic projection"), '3');
701       dialog::add_action([] { if(rug::rugged) rug::close(); pconf.alpha = 0; pconf.scale = 1; pconf.xposition = pconf.yposition = 0; popScreen(); });
702       if(sphere) {
703         dialog::addItem(XLAT("stereographic projection") + " " + XLAT("(zoomed out)"), '4');
704         dialog::add_action([] { if(rug::rugged) rug::close(); pconf.alpha = 1; pconf.scale = 0.4; pconf.xposition = pconf.yposition = 0; popScreen(); });
705         }
706       if(hyperbolic) {
707         dialog::addItem(XLAT("Gans model") + " " + XLAT("(zoomed out)"), '4');
708         dialog::add_action([] { if(rug::rugged) rug::close(); pconf.alpha = 999; pconf.scale = 499; pconf.xposition = pconf.yposition = 0; popScreen(); });
709         #if CAP_RUG
710         dialog::addItem(XLAT("Hypersian Rug"), 'u');
711         dialog::add_action([] {
712           if(rug::rugged) pushScreen(rug::show);
713           else {
714             pconf.alpha = 1, pconf.scale = 1; if(!rug::rugged) rug::init(); popScreen();
715             }
716           });
717         #endif
718         }
719       }
720     else if(GDIM == 2 && euclid) {
721       auto zoom_to = [] (ld s) {
722         pconf.xposition = pconf.yposition = 0;
723         ld maxs = 0;
724         auto& cd = current_display;
725         for(auto& p: gmatrix) for(int i=0; i<p.first->type; i++) {
726           shiftpoint h = tC0(p.second * currentmap->adj(p.first, i));
727           hyperpoint onscreen;
728           applymodel(h, onscreen);
729           maxs = max(maxs, onscreen[0] / cd->xsize);
730           maxs = max(maxs, onscreen[1] / cd->ysize);
731           }
732         pconf.alpha = 1;
733         pconf.scale = s * pconf.scale / 2 / maxs / cd->radius;
734         popScreen();
735         };
736       dialog::addItem(XLAT("zoom 2x"), '1');
737       dialog::add_action([zoom_to] { zoom_to(2); });
738       dialog::addItem(XLAT("zoom 1x"), '2');
739       dialog::add_action([zoom_to] { zoom_to(1); });
740       dialog::addItem(XLAT("zoom 0.5x"), '3');
741       dialog::add_action([zoom_to] { zoom_to(.5); });
742       #if CAP_RUG
743       if(quotient) {
744         dialog::addItem(XLAT("cylinder/donut view"), 'u');
745         dialog::add_action([] {
746           if(rug::rugged) pushScreen(rug::show);
747           else {
748             pconf.alpha = 1, pconf.scale = 1; if(!rug::rugged) rug::init(); popScreen();
749             }
750           });
751         }
752       #endif
753       }
754     else if(GDIM == 3) {
755       auto& ysh = (WDIM == 2 ? vid.camera : vid.yshift);
756       dialog::addItem(XLAT("first-person perspective"), '1');
757       dialog::add_action([&ysh] { ysh = 0; vid.sspeed = 0; popScreen(); } );
758       dialog::addItem(XLAT("fixed point of view"), '2');
759       dialog::add_action([&ysh] { ysh = 0; vid.sspeed = -10; popScreen(); } );
760       dialog::addItem(XLAT("third-person perspective"), '3');
761       dialog::add_action([&ysh] { ysh = 1; vid.sspeed = 0; popScreen(); } );
762       }
763     if(WDIM == 2) {
764       dialog::addItem(XLAT("toggle full 3D graphics"), 'f');
765       dialog::add_action([] { geom3::switch_fpp(); popScreen(); });
766       }
767     dialog::addItem(XLAT("advanced projections"), 'a');
768     dialog::add_action_push(model_menu);
769     menuitem_sightrange('r');
770     dialog::addBack();
771     dialog::display();
772     }
773 
774   #if CAP_COMMANDLINE
775 
read_model(const string & ss)776   eModel read_model(const string& ss) {
777     for(int i=0; i<isize(mdinf); i++) {
778       if(appears(mdinf[i].name_hyperbolic, ss)) return eModel(i);
779       if(appears(mdinf[i].name_euclidean, ss)) return eModel(i);
780       if(appears(mdinf[i].name_spherical, ss)) return eModel(i);
781       }
782     return eModel(atoi(ss.c_str()));
783     }
784 
readArgs()785   int readArgs() {
786     using namespace arg;
787 
788     if(0) ;
789     else if(argis("-els")) {
790       shift_arg_formula(history::extra_line_steps);
791       }
792     else if(argis("-stretch")) {
793       PHASEFROM(2); shift_arg_formula(vpconf.stretch);
794       }
795     else if(argis("-PM")) {
796       PHASEFROM(2); shift(); vpconf.model = read_model(args());
797       if(vpconf.model == mdFormula) {
798         shift(); vpconf.basic_model = eModel(argi());
799         shift(); vpconf.formula = args();
800         }
801       }
802     else if(argis("-ballangle")) {
803       PHASEFROM(2);
804       shift_arg_formula(vpconf.ballangle);
805       }
806     else if(argis("-topz")) {
807       PHASEFROM(2);
808       shift_arg_formula(vpconf.top_z);
809       }
810     else if(argis("-twopoint")) {
811       PHASEFROM(2);
812       shift_arg_formula(vpconf.twopoint_param);
813       }
814     else if(argis("-hp")) {
815       PHASEFROM(2);
816       shift_arg_formula(vpconf.halfplane_scale);
817       }
818     else if(argis("-mori")) {
819       PHASEFROM(2);
820       shift_arg_formula(vpconf.model_orientation);
821       }
822     else if(argis("-mets")) {
823       PHASEFROM(2);
824       shift_arg_formula(vpconf.euclid_to_sphere);
825       }
826     else if(argis("-mhyp")) {
827       PHASEFROM(2);
828       shift_arg_formula(vpconf.hyperboloid_scaling);
829       }
830     else if(argis("-mdepth")) {
831       PHASEFROM(2);
832       shift_arg_formula(vpconf.depth_scaling);
833       }
834     else if(argis("-mnil")) {
835       PHASEFROM(2);
836       shift_arg_formula(vpconf.rotational_nil);
837       }
838     else if(argis("-mori2")) {
839       PHASEFROM(2);
840       shift_arg_formula(vpconf.model_orientation);
841       shift_arg_formula(vpconf.model_orientation_yz);
842       }
843     else if(argis("-crot")) {
844       PHASEFROM(2);
845       shift_arg_formula(models::rotation);
846       if(GDIM == 3) shift_arg_formula(models::rotation_xz);
847       if(GDIM == 3) shift_arg_formula(models::rotation_xy2);
848       }
849     else if(argis("-clip")) {
850       PHASEFROM(2);
851       shift_arg_formula(vpconf.clip_min);
852       shift_arg_formula(vpconf.clip_max);
853       }
854     else if(argis("-mtrans")) {
855       PHASEFROM(2);
856       shift_arg_formula(vpconf.model_transition);
857       }
858     else if(argis("-mparam")) {
859       PHASEFROM(2);
860       if(pmodel == mdCollignon) shift_arg_formula(vpconf.collignon_parameter);
861       else if(pmodel == mdMiller) shift_arg_formula(vpconf.miller_parameter);
862       else if(among(pmodel, mdLoximuthal, mdRetroCraig, mdRetroHammer)) shift_arg_formula(vpconf.loximuthal_parameter);
863       else if(among(pmodel, mdAitoff, mdHammer, mdWinkelTripel)) shift_arg_formula(vpconf.aitoff_parameter);
864       if(pmodel == mdWinkelTripel) shift_arg_formula(vpconf.winkel_parameter);
865       }
866     else if(argis("-sang")) {
867       PHASEFROM(2);
868       shift_arg_formula(vpconf.spiral_angle);
869       if(sphere)
870         shift_arg_formula(vpconf.sphere_spiral_multiplier);
871       else if(vpconf.spiral_angle == 90)
872         shift_arg_formula(vpconf.right_spiral_multiplier);
873       }
874     else if(argis("-ssm")) {
875       PHASEFROM(2);
876       shift_arg_formula(vpconf.any_spiral_multiplier);
877       }
878     else if(argis("-scone")) {
879       PHASEFROM(2);
880       shift_arg_formula(vpconf.spiral_cone);
881       }
882     else if(argis("-sxy")) {
883       PHASEFROM(2);
884       shift_arg_formula(vpconf.spiral_x);
885       shift_arg_formula(vpconf.spiral_y);
886       }
887     else if(argis("-mob")) {
888       PHASEFROM(2);
889       shift_arg_formula(vpconf.skiprope);
890       }
891     else if(argis("-palpha")) {
892       PHASEFROM(2);
893       #if CAP_GL
894       shift_arg_formula(panini_alpha, reset_all_shaders);
895       #else
896       shift_arg_formula(panini_alpha);
897       #endif
898       }
899     else if(argis("-salpha")) {
900       PHASEFROM(2);
901       #if CAP_GL
902       shift_arg_formula(stereo_alpha, reset_all_shaders);
903       #else
904       shift_arg_formula(stereo_alpha);
905       #endif
906       }
907     else if(argis("-zoom")) {
908       PHASEFROM(2); shift_arg_formula(vpconf.scale);
909       }
910     else if(argis("-alpha")) {
911       PHASEFROM(2); shift_arg_formula(vpconf.alpha);
912       }
913     else if(argis("-d:model"))
914       launch_dialog(model_menu);
915     else if(argis("-d:formula")) {
916       launch_dialog();
917       edit_formula();
918       }
919     else if(argis("-d:match")) {
920       launch_dialog(match_torus_period);
921       edit_formula();
922       }
923     else return 1;
924     return 0;
925     }
926 
927   auto hookArg = addHook(hooks_args, 100, readArgs);
928   #endif
929 
add_model_config()930   void add_model_config() {
931     addsaver(polygonal::SI, "polygon sides");
932     param_f(polygonal::STAR, "star", "polygon star factor");
933     addsaver(polygonal::deg, "polygonal degree");
934 
935     addsaver(polygonal::maxcoef, "polynomial degree");
936     for(int i=0; i<polygonal::MSI; i++) {
937       addsaver(polygonal::coefr[i], "polynomial "+its(i)+".real");
938       addsaver(polygonal::coefi[i], "polynomial "+its(i)+".imag");
939       }
940 
941     param_f(models::rotation, "rotation", "conformal rotation");
942     addsaver(models::rotation_xz, "conformal rotation_xz");
943     addsaver(models::rotation_xy2, "conformal rotation_2");
944     addsaver(models::do_rotate, "conformal rotation mode", 1);
945 
946     param_f(pconf.halfplane_scale, "hp", "halfplane scale", 1);
947 
948     auto add_all = [&] (projection_configuration& p, string pp, string sp) {
949 
950       bool rug = pp != "";
951       dynamicval<function<bool()>> ds(auto_restrict);
952       auto_restrict = [&p] { return &vpconf == &p; };
953 
954       addsaverenum(p.model, pp+"used model", mdDisk);
955       param_custom(pmodel, "projection|Poincare|Klein|half-plane|perspective", menuitem_projection, '1');
956 
957       param_f(p.model_orientation, pp+"mori", sp+"model orientation", 0);
958       param_f(p.model_orientation_yz, pp+"mori_yz", sp+"model orientation-yz", 0);
959 
960       param_f(p.top_z, sp+"topz", 5)
961       -> editable(1, 20, .25, "maximum z coordinate to show", "maximum z coordinate to show", 'l');
962 
963       param_f(p.model_transition, pp+"mtrans", sp+"model transition", 1)
964       -> editable(0, 1, .1, "model transition",
965           "You can change this parameter for a transition from another model to this one.", 't');
966 
967       param_f(p.rotational_nil, sp+"rotnil", 1);
968 
969       param_f(p.clip_min, pp+"clipmin", sp+"clip-min", rug ? -100 : -1);
970       param_f(p.clip_max, pp+"clipmax", sp+"clip-max", rug ? +10 : +1);
971 
972       param_f(p.euclid_to_sphere, pp+"ets", sp+"euclid to sphere projection", 1.5)
973       -> editable(1e-1, 10, .1, "ETS parameter", "Stereographic projection to a sphere. Choose the radius of the sphere.", 'l')
974       -> set_sets(dialog::scaleLog);
975 
976       param_f(p.twopoint_param, pp+"twopoint", sp+"twopoint parameter", 1)
977       -> editable(1e-3, 10, .1, "two-point parameter", "In two-point-based models, this parameter gives the distance from each of the two points to the center.", 'b')
978       -> set_sets(dialog::scaleLog)
979 ;
980       param_f(p.fisheye_param, pp+"fisheye", sp+"fisheye parameter", 1)
981       -> editable(1e-3, 10, .1, "fisheye parameter", "Size of the fish eye.", 'b')
982       -> set_sets(dialog::scaleLog);
983 
984       param_f(p.stretch, pp+"stretch", 1)
985       -> editable(0, 10, .1, "vertical stretch", "Vertical stretch factor.", 's')
986       -> set_extra(stretch_extra);
987 
988       param_f(p.product_z_scale, pp+"zstretch")
989       -> editable(0.1, 10, 0.1, "product Z stretch", "", 'Z');
990 
991       param_f(p.collignon_parameter, pp+"collignon", sp+"collignon-parameter", 1)
992       -> editable(-1, 1, .1, "Collignon parameter", "", 'b')
993       -> modif([] (float_setting* f) {
994         f->unit = vpconf.collignon_reflected ? " (r)" : "";
995         })
996       -> set_extra([&p] {
997         add_edit(p.collignon_reflected);
998         });
999       param_b(p.collignon_reflected, sp+"collignon-reflect", false)
1000       -> editable("Collignon reflect", 'R');
1001 
1002       param_f(p.aitoff_parameter, sp+"aitoff")
1003       -> editable(-1, 1, .1, "Aitoff parameter",
1004           "The Aitoff projection is obtained by multiplying the longitude by 1/2, using azimuthal equidistant projection, and then dividing X by 1/2. "
1005           "Hammer projection is similar but equi-area projection is used instead. "
1006           "Here you can change this parameter.", 'b');
1007       param_f(p.miller_parameter, sp+"miller");
1008       param_f(p.loximuthal_parameter, sp+"loximuthal")
1009       -> editable(-M_PI/2, M_PI/2, .1, "loximuthal parameter",
1010           "Loximuthal is similar to azimuthal equidistant, but based on loxodromes (lines of constant geographic direction) rather than geodesics. "
1011           "The loximuthal projection maps (the shortest) loxodromes to straight lines of the same length, going through the starting point. "
1012           "This setting changes the latitude of the starting point.\n\n"
1013           "In retroazimuthal projections, a point is drawn at such a point that the azimuth *from* that point to the chosen central point is correct. "
1014           "For example, if you should move east, the point is drawn to the right. This parameter is the latitude of the central point."
1015           "\n\n(In hyperbolic geometry directions are assigned according to the Lobachevsky coordinates.)", 'b'
1016           );
1017       param_f(p.winkel_parameter, sp+"winkel")
1018       -> editable(-1, 1, .1, "Winkel Tripel mixing",
1019         "The Winkel Tripel projection is the average of Aitoff projection and equirectangular projection. Here you can change the proportion.", 'B');
1020 
1021       param_b(p.show_hyperboloid_flat, sp+"hyperboloid-flat", true)
1022       -> editable("show flat", 'b');
1023 
1024       param_f(p.skiprope, sp+"mobius", 0)
1025       -> editable(0, 360, 15, "Möbius transformations", "", 'S')->unit = "°";
1026 
1027       addsaver(p.formula, sp+"formula");
1028       addsaverenum(p.basic_model, sp+"basic model");
1029       addsaver(p.use_atan, sp+"use_atan");
1030 
1031       param_f(p.spiral_angle, sp+"sang")
1032       -> editable(0, 360, 15, "spiral angle", "set to 90° for the ring projection", 'x')
1033       -> unit = "°";
1034       param_f(p.spiral_x, sp+"spiralx")
1035       -> editable(-20, 20, 1, "spiral period: x", "", 'x');
1036       param_f(p.spiral_y, sp+"spiraly")
1037       -> editable(-20, 20, 1, "spiral period: y", "", 'y');
1038 
1039       param_f(p.scale, sp+"scale", 1);
1040       param_f(p.xposition, sp+"xposition", 0);
1041       param_f(p.yposition, sp+"yposition", 0);
1042 
1043       addsaver(p.alpha, sp+"projection", 1);
1044       param_custom(p.alpha, sp+"projection", menuitem_projection_distance, 'p')
1045       ->help_text = "projection distance|Gans Klein Poincare orthographic stereographic";
1046 
1047       param_f(p.camera_angle, pp+"cameraangle", sp+"camera angle", 0);
1048       addsaver(p.ballproj, sp+"ballproj", 1);
1049 
1050       param_f(p.ballangle, pp+"ballangle", sp+"ball angle", 20)
1051       -> editable(0, 90, 5, "camera rotation in 3D models",
1052         "Rotate the camera in 3D models (ball model, hyperboloid, and hemisphere). "
1053         "Note that hyperboloid and hemisphere models are also available in the "
1054         "Hypersian Rug surfaces menu, but they are rendered differently there -- "
1055         "by making a flat picture first, then mapping it to a surface. "
1056         "This makes the output better in some ways, but 3D effects are lost. "
1057         "Hypersian Rug model also allows more camera freedom.",
1058         'b')
1059       -> unit = "°";
1060 
1061       string help =
1062         "This parameter has a bit different scale depending on the settings:\n"
1063         "(1) in spherical geometry (with spiral angle=90°, 1 produces a stereographic projection)\n"
1064         "(2) in hyperbolic geometry, with spiral angle being +90° or -90°\n"
1065         "(3) in hyperbolic geometry, with other spiral angles (1 makes the bands fit exactly)";
1066 
1067       param_f(p.sphere_spiral_multiplier, "sphere_spiral_multiplier")
1068       -> editable(0, 10, .1, "sphere spiral multiplier", help, 'M')->unit = "°";
1069 
1070       param_f(p.right_spiral_multiplier, "right_spiral_multiplier")
1071       -> editable(0, 10, .1, "right spiral multiplier", help, 'M')->unit = "°";
1072 
1073       param_f(p.any_spiral_multiplier, "any_spiral_multiplier")
1074       -> editable(0, 10, .1, "any spiral multiplier", help, 'M')->unit = "°";
1075 
1076       param_f(p.spiral_cone, "spiral_cone")
1077       -> editable(0, 360, -45, "spiral cone", "", 'C')->unit = "°";
1078       };
1079 
1080     add_all(pconf, "", "");
1081     add_all(vid.rug_config, "rug_", "rug-");
1082     }
1083 
1084   auto hookSet = addHook(hooks_configfile, 100, add_model_config);
1085   }
1086 
1087 }