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 }