// Hyperbolic Rogue -- Arbitrary Tilings // Copyright (C) 2011-2019 Zeno Rogue, see 'hyper.cpp' for details /** \file arbitrile.cpp * \brief Arbitrary tilings * * Arbitrary tilings, defined in .tes files. */ #include "hyper.h" namespace hr { EX namespace arb { EX int affine_limit = 200; #if HDR /** a type used to specify the connections between shapes */ struct connection_t { /** the index of the connected shape in the 'shapes' table */ int sid; /** the index of the edge in the 'shapes' table */ int eid; /** 1 if this connection mirrored, 0 otherwise. do_unmirror() removes all mirrors by doubling shapes */ int mirror; bool operator == (const arb::connection_t& b) const { return tie(sid, eid, mirror) == tie(b.sid, b.eid, b.mirror); } bool operator < (const arb::connection_t& b) const { return tie(sid, eid, mirror) < tie(b.sid, b.eid, b.mirror); } }; inline void print(hstream& hs, const connection_t& conn) { print(hs, tie(conn.sid, conn.eid, conn.mirror)); } /** \brief each shape of the arb tessellation * note: the usual HyperRogue convention is: vertex 0, edge 0, vertex 1, edge 1, ... * note: the tesfile convention is: edge 0, vertex 0, edge 1, vertex 1, ... */ /** edge with infinite end on the left */ constexpr ld INFINITE_LEFT = -1; /** edge with infinite end on the right */ constexpr ld INFINITE_RIGHT = -2; /** edge with two infinite ends */ constexpr ld INFINITE_BOTH = -3; struct shape { /** index in the arbi_tiling::shapes */ int id; /** index in the original file */ int orig_id; /** flags such as sfLINE and sfPH */ int flags; /** list of vertices in the usual convention */ vector<hyperpoint> vertices; /** list of angles in the tesfile convention */ vector<ld> angles; /** list of edge lengths */ vector<ld> edges; /** list of input edges */ vector<ld> in_edges; /** list of input angles */ vector<ld> in_angles; /** (ultra)ideal markers */ vector<bool> ideal_markers; /** list of edge connections */ vector<connection_t> connections; int size() const { return isize(vertices); } void build_from_angles_edges(bool is_comb); vector<pair<int, int> > sublines; vector<pair<ld, ld>> stretch_shear; /** '*inf' was applied to represent an apeirogon/pseudogon */ bool apeirogonal; /** connections repeat `repeat_value` times */ int repeat_value; /** 0 if the no mirror symmetries are declared; otherwise, edge i is the mirror of edge gmod(symmetric_value-i, size()). Make sure symmetric_value != 0, e.g., by adding size() */ int symmetric_value; /** if a tile/edge combination may be connected to edges j1 and j2 of this, j1-j2 must be divisible by cycle_length */ int cycle_length; /** list of valences of vertices in the tesfile convention */ vector<int> vertex_valence; /** list of periods of vertices in the tesfile convention */ vector<int> vertex_period; /** list of angles at vertices in the tesfile convention */ vector<vector<ld>> vertex_angles; /** football types */ int football_type; /** is it a mirrored version of an original tile */ bool is_mirrored; /** auxiliary function for symmetric_value: is the edge index reflectable? */ bool reflectable(int id) { if(!symmetric_value) return false; if(apeirogonal && gmod(id, size()) >= size() - 2) return false; return true; } /** reflect a reflectable reflect index */ int reflect(int id) { return gmod(symmetric_value - id, size() - (apeirogonal ? 2 : 0)); } }; struct slider { string name; ld zero; ld current; ld min; ld max; }; struct intslider { string name; int zero; int current; int min; int max; }; struct arbi_tiling { int order; /* line flags have been marked for tiles */ bool have_line; /* pseudohept flags have been marked for tiles (1), or the tiling is football-colorable (2), or neither (0) */ int have_ph; /* is the tree structure given in the tes file */ bool have_tree; /* is the valence data reliable */ bool have_valence; /* use "star." if the tessellation includs star polygons */ bool is_star; /* use "combinatorial." for combinatorial tessellations; vertex valences computed based on their angles. Currently only rulegen works for combinatorial tessellations */ bool is_combinatorial; /* reserved for future flags */ bool res0, res1, res2, res3; int yendor_backsteps; vector<shape> shapes; string name; string comment; vector<slider> sliders; vector<intslider> intsliders; ld cscale; int range; ld floor_scale; ld boundary_ratio; string filename; int mirror_rules; vector<string> options; int min_valence, max_valence; bool is_football_colorable; bool was_unmirrored; bool was_split_for_football; geometryinfo1& get_geometry(); eGeometryClass get_class() { return get_geometry().kind; } ld scale(); }; #endif /** currently loaded tiling */ EX arbi_tiling current; /** is the currently displayed map current or slided */ EX bool using_slided; /** for real-valued sliders, current is the tiling used by the map, while slided is the tiling used for the display */ EX arbi_tiling slided; EX bool in_slided() { return in() && using_slided; } EX arbi_tiling& current_or_slided() { return using_slided ? slided : current; } /** id of vertex in the arbitrary tiling */ EX short& id_of(heptagon *h) { return h->zebraval; } #if HDR struct hr_polygon_error : hr_exception { vector<transmatrix> v; eGeometryClass c; int id; transmatrix end; map<string, cld> params; hr_polygon_error(const vector<transmatrix>& _v, int _id, transmatrix _e) : v(_v), c(cgclass), id(_id), end(_e) {} ~hr_polygon_error() noexcept(true) {} string generate_error(); }; #endif string hr_polygon_error::generate_error() { cld dist = (hdist0(tC0(end)) / params["distunit"]); bool angle = abs(dist) < 1e-9; if(angle) dist = (atan2(end * lxpush0(1)) / params["angleunit"]); return XLAT("Polygon number %1 did not close correctly (%2 %3). Here is the picture to help you understand the issue.\n\n", its(id), angle ? "angle" : "distance", lalign(0, dist) ); } struct connection_debug_request : hr_exception { int id; eGeometryClass c; connection_debug_request(int i): id(i), c(cgclass) {} }; void ensure_geometry(eGeometryClass c) { stop_game(); if(c != cgclass) { if(c == gcEuclid) set_geometry(gEuclid); if(c == gcHyperbolic) set_geometry(gNormal); if(c == gcSphere) set_geometry(gSphere); } if(specialland != laCanvas) { canvas_default_wall = waInvisibleFloor; ccolor::set_plain_nowall(0xFFFFFF); enable_canvas(); } start_game(); } void start_poly_debugger(hr_polygon_error& err) { #if CAP_EDIT ensure_geometry(err.c); drawthemap(); mapeditor::drawing_tool = true; pushScreen(mapeditor::showDrawEditor); mapeditor::initdraw(cwt.at); int n = isize(err.v); mapeditor::dtcolor = 0xFF0000FF; mapeditor::dtwidth = 0.02; for(int i=0; i<n-1; i++) mapeditor::dt_add_line(shiftless(tC0(err.v[i])), shiftless(tC0(err.v[i+1])), 0); mapeditor::dtcolor = 0xFFFFFFFF; for(int i=0; i<n; i++) mapeditor::dt_add_text(shiftless(tC0(err.v[i])), 0.5, its(i)); #endif } void shape::build_from_angles_edges(bool is_comb) { transmatrix at = Id; int n = isize(in_angles); hyperpoint ctr = Hypc; vector<transmatrix> matrices; for(int i=0; i<n; i++) { matrices.push_back(at); if(debugflags & DF_GEOM) println(hlog, "at = ", at); ctr += tC0(at); at = at * lxpush(in_edges[i]) * spin(in_angles[i]+M_PI); } matrices.push_back(at); if(is_comb) return; if(!eqmatrix(at, Id) && !apeirogonal) { throw hr_polygon_error(matrices, id, at); } if(sqhypot_d(3, ctr) < 1e-2) { // this may happen for some spherical tilings // try to move towards the center if(debugflags & DF_GEOM) println(hlog, "special case encountered"); for(int i=0; i<n; i++) { ctr += at * lxpush(in_edges[i]) * spin((in_angles[i]+M_PI)/2) * lxpush0(.01); at = at * lxpush(in_edges[i]) * spin(in_angles[i]); } if(debugflags & DF_GEOM) println(hlog, "ctr = ", ctr); } hyperpoint inf_point; if(apeirogonal) { transmatrix U = at; for(int i=0; i<3; i++) for(int j=0; j<3; j++) U[i][j] -= Id[i][j]; hyperpoint v; ld det = U[0][1] * U[1][0] - U[1][1] * U[0][0]; v[1] = (U[1][2] * U[0][0] - U[0][2] * U[1][0]) / det; v[0] = (U[0][2] * U[1][1] - U[1][2] * U[0][1]) / det; v[2] = 1; inf_point = v; ctr = mid(C0, tC0(at)); ctr = towards_inf(ctr, inf_point); } ctr = normalize(ctr); vertices.clear(); angles.clear(); for(int i=0; i<n; i++) { edges.push_back(in_edges[i]); if(!ideal_markers[i]) { vertices.push_back(tC0(gpushxto0(ctr) * matrices[i])); angles.push_back(in_angles[i]); } else { angles.push_back(0); hyperpoint a1 = tC0(matrices[i]); hyperpoint t1 = get_column(matrices[i], 0); hyperpoint a2 = tC0(matrices[i+2]); hyperpoint t2 = get_column(matrices[i+2], 0); a1 /= a1[2]; a2 /= a2[2]; t1 -= a1 * t1[2]; t2 -= a2 * t2[2]; ld c1 = a2[0] - a1[0], c2 = a2[1] - a1[1]; ld v1 = t1[0], v2 = t1[1]; ld u1 = t2[0], u2 = t2[1]; ld r = (u2 * c1 - c2 * u1) / (v1 * u2 - v2 * u1); // ld s = (v2 * c1 - c2 * v1) / (v1 * u2 - v2 * u1); hyperpoint v = a1 + r * t1; // also v == a2 + s * t2; v[2] = 1; v = gpushxto0(ctr) * v; v /= v[2]; vertices.push_back(v); i++; } } if(apeirogonal) { vertices.push_back(gpushxto0(ctr) * tC0(at)); hyperpoint v = gpushxto0(ctr) * inf_point; v /= v[2]; vertices.push_back(v); auto b = angles.back() / 2; angles.back() = b; angles.push_back(0); angles.push_back(b); edges.push_back(0); edges.push_back(0); } n = isize(angles); for(int i=0; i<n; i++) { bool left = angles[i] == 0; bool right = angles[gmod(i-1, isize(vertices))] == 0; if(left && right) edges[i] = INFINITE_BOTH; else if(left) edges[i] = INFINITE_LEFT; else if(right) edges[i] = INFINITE_RIGHT; } } EX bool correct_index(int index, int size) { return index >= 0 && index < size; } template<class T> bool correct_index(int index, const T& v) { return correct_index(index, isize(v)); } template<class T> void verify_index(int index, const T& v, exp_parser& ep) { if(!correct_index(index, v)) throw hr_parse_exception("bad index: " + its(index) + " at " + ep.where()); } string unnamed = "unnamed"; EX void load_tile(exp_parser& ep, arbi_tiling& c, bool unit) { c.shapes.emplace_back(); auto& cc = c.shapes.back(); cc.id = isize(c.shapes) - 1; cc.orig_id = cc.id; cc.is_mirrored = false; cc.symmetric_value = 0; cc.flags = 0; cc.repeat_value = 1; cc.apeirogonal = false; bool is_symmetric = false; while(ep.next() != ')') { cld dist = 1; ep.skip_white(); if(ep.eat("|")) { cc.symmetric_value = ep.iparse(); is_symmetric = true; ep.force_eat(")"); break; } if(ep.eat("*")) { ld frep = ep.rparse(0); if(isinf(frep)) { cc.apeirogonal = true; set_flag(ginf[gArbitrary].flags, qIDEAL, true); if(ep.eat(",") && ep.eat("|")) { is_symmetric = true; if(isize(cc.in_edges) == 1 && ep.eat(")")) break; cc.symmetric_value = ep.iparse(); } ep.force_eat(")"); break; } int rep = int(frep+.5); int repeat_from = 0; int repeat_to = cc.in_edges.size(); if(rep == 0) { cc.in_edges.resize(repeat_from); cc.in_angles.resize(repeat_from); cc.ideal_markers.resize(repeat_from); } else if(rep < 0) throw hr_parse_exception("don't know how to use a negative repeat in tile definition"); for(int i=1; i<rep; i++) for(int j=repeat_from; j<repeat_to; j++) { cc.in_edges.push_back(cc.in_edges[j]); cc.in_angles.push_back(cc.in_angles[j]); cc.ideal_markers.push_back(cc.ideal_markers[j]); } ep.skip_white(); if(ep.eat(",")) { ep.force_eat("|"); is_symmetric = true; if(repeat_to == 1 && ep.eat(")")) goto skip; cc.symmetric_value = ep.iparse(); } if(ep.eat(")")) { skip: if(repeat_from == 0) cc.repeat_value = rep; break; } else throw hr_parse_exception("expecting ) after repeat"); } if(!unit) { dist = ep.parse(0); ep.force_eat(","); } cld angle; ep.skip_white(); if(ep.eat("[")) { cc.in_edges.push_back(ep.validate_real(dist * ep.extra_params["distunit"])); angle = ep.parse(0); ep.force_eat(","); cc.in_angles.push_back(ep.validate_real(angle * ep.extra_params["angleunit"])); cc.ideal_markers.push_back(true); dist = ep.parse(0); ep.force_eat(","); angle = ep.parse(0); ep.force_eat("]"); set_flag(ginf[gArbitrary].flags, qIDEAL, true); } else angle = ep.parse(0); cc.in_edges.push_back(ep.validate_real(dist * ep.extra_params["distunit"])); cc.in_angles.push_back(ep.validate_real(angle * ep.extra_params["angleunit"])); cc.ideal_markers.push_back(false); if(ep.eat(",")) continue; else if(ep.eat(")")) break; else throw hr_parse_exception("expecting , or )"); } try { cc.build_from_angles_edges(c.is_combinatorial); } catch(hr_parse_exception& ex) { throw hr_parse_exception(ex.s + ep.where()); } catch(hr_polygon_error& poly) { poly.params = ep.extra_params; throw; } int n = cc.size(); if(is_symmetric && !cc.symmetric_value) cc.symmetric_value += n - (cc.apeirogonal ? 2 : 0); cc.connections.resize(n); for(int i=0; i<isize(cc.connections); i++) cc.connections[i] = connection_t{cc.id, i, false}; if(cc.apeirogonal) { cc.connections[n-2].eid = n-1; cc.connections[n-1].eid = n-2; } cc.stretch_shear.resize(n, make_pair(1, 0)); } EX bool do_unmirror = true; template<class T> void cycle(vector<T>& t) { std::rotate(t.begin(), t.begin() + 2, t.end()); } /** \brief for tessellations which contain mirror rules, remove them by taking the orientable double cover */ EX void unmirror(arbi_tiling& c) { if(cgflags & qAFFINE) return; auto& mirror_rules = c.mirror_rules; mirror_rules = 0; for(auto& s: c.shapes) for(auto& t: s.connections) if(t.mirror) mirror_rules++; if(!mirror_rules) return; auto& sh = c.shapes; int s = isize(sh); vector<int> mirrored_id(s, -1); for(int i=0; i<s; i++) if(!sh[i].symmetric_value) { mirrored_id[i] = isize(sh); sh.push_back(sh[i]); } int ss = isize(sh); for(int i=0; i<ss; i++) { sh[i].id = i; if(i >= s) sh[i].is_mirrored = true; } for(int i=s; i<ss; i++) { for(auto& v: sh[i].vertices) v[1] = -v[1]; reverse(sh[i].edges.begin(), sh[i].edges.end()); for(auto& e: sh[i].edges) { if(e == INFINITE_LEFT) e = INFINITE_RIGHT; else if(e == INFINITE_RIGHT) e = INFINITE_LEFT; } reverse(sh[i].vertices.begin()+1, sh[i].vertices.end()); reverse(sh[i].angles.begin(), sh[i].angles.end()-1); reverse(sh[i].connections.begin(), sh[i].connections.end()); if(sh[i].apeirogonal) { cycle(sh[i].edges); cycle(sh[i].vertices); if(debugflags & DF_GEOM) println(hlog, "angles before = ", sh[i].angles); cycle(sh[i].angles); if(debugflags & DF_GEOM) println(hlog, "angles now = ", sh[i].angles); cycle(sh[i].connections); } } if(true) for(int i=0; i<ss; i++) { for(auto& co: sh[i].connections) { bool mirr = co.mirror ^ (i >= s); co.mirror = false; if(mirr && mirrored_id[co.sid] == -1) { if(sh[co.sid].reflectable(co.eid)) { co.eid = sh[co.sid].reflect(co.eid); } } else if(mirr) { co.sid = mirrored_id[co.sid]; co.eid = isize(sh[co.sid].angles) - 1 - co.eid; if(sh[co.sid].apeirogonal) co.eid = gmod(co.eid - 2, isize(sh[co.sid].angles)); } } } c.was_unmirrored = true; } static void reduce_gcd(int& a, int b) { a = abs(gcd(a, b)); } EX void mirror_connection(arb::arbi_tiling& ac, connection_t& co) { if(co.mirror && ac.shapes[co.sid].reflectable(co.eid)) { co.eid = ac.shapes[co.sid].reflect(co.eid); co.mirror = !co.mirror; } } EX void compute_vertex_valence_prepare(arb::arbi_tiling& ac) { int tcl = -1; while(true) { for(auto& sh: ac.shapes) { int i = sh.id; int n = isize(sh.vertices); for(int k=sh.cycle_length; k<n; k++) { auto co = sh.connections[k]; auto co1 = sh.connections[k-sh.cycle_length]; if(co.sid != co1.sid) { println(hlog, "ik = ", tie(i,k), " co=", co, " co1=", co1, " cl=", sh.cycle_length); throw hr_parse_exception("connection error #2 in compute_vertex_valence"); } mirror_connection(ac, co); mirror_connection(ac, co1); reduce_gcd(ac.shapes[co.sid].cycle_length, co.eid - co1.eid); } for(int k=0; k<n; k++) { auto co = sh.connections[k]; auto co0 = co; co = ac.shapes[co.sid].connections[co.eid]; if(co.sid != i) throw hr_parse_exception("connection error in compute_vertex_valence"); co.mirror ^= co0.mirror; mirror_connection(ac, co); reduce_gcd(sh.cycle_length, k-co.eid); } if(debugflags & DF_GEOM) println(hlog, "tile ", i, " cycle_length = ", sh.cycle_length, " / ", n); } int new_tcl = 0; for(auto& sh: ac.shapes) { auto& len = sh.cycle_length; if(len < 0) len = -len; new_tcl += len; } if(new_tcl == tcl) break; tcl = new_tcl; } } /** returns true if we need to recompute */ EX bool compute_vertex_valence_flat(arb::arbi_tiling& ac) { for(auto& sh: ac.shapes) { int n = sh.size(); int i = sh.id; sh.vertex_valence.resize(n); sh.vertex_period.resize(n); sh.vertex_angles.resize(n); for(int k=0; k<n; k++) { ld total = 0; int qty = 0, pqty = 0; connection_t at = {i, k, false}; connection_t at1 = at; vector<ld> anglelist; do { if(at.sid == at1.sid && (at.eid-at1.eid) % ac.shapes[at.sid].cycle_length == 0) pqty = 0; if(qty && pqty == 0 && !total) break; ld a = ac.shapes[at.sid].angles[at.eid]; while(a < 0) a += TAU; while(a > TAU) a -= TAU; total += a; anglelist.push_back(a); qty++; pqty++; at.eid++; if(at.eid == isize(ac.shapes[at.sid].angles)) at.eid = 0; at = ac.shapes[at.sid].connections[at.eid]; } while(total < TAU - 1e-6); if(total == 0) qty = OINF; if(total > TAU + 1e-6) throw hr_parse_exception("improper total in compute_stats"); if(at.sid != i) throw hr_parse_exception("ended at wrong type determining vertex_valence"); if((at.eid - k) % ac.shapes[i].cycle_length) { reduce_gcd(ac.shapes[i].cycle_length, at.eid - k); return true; } sh.vertex_valence[k] = qty; sh.vertex_period[k] = pqty; sh.vertex_angles[k] = std::move(anglelist); } if(debugflags & DF_GEOM) println(hlog, "computed vertex_valence of ", i, " as ", ac.shapes[i].vertex_valence); } return false; } /** returns true if we need to recompute */ EX bool compute_vertex_valence_generic(arb::arbi_tiling& ac) { for(auto& sh: ac.shapes) { int n = sh.size(); int i = sh.id; sh.vertex_valence.resize(n); for(int k=0; k<n; k++) { connection_t at = {i, k, false}; transmatrix T = Id; int qty = 0; do { if(qty && at.sid == i) { auto co1 = at; bool found = find_connection(T, Id, co1); if(found) { mirror_connection(ac, co1); if((co1.eid - k) % ac.shapes[i].cycle_length) { reduce_gcd(ac.shapes[i].cycle_length, co1.eid - k); return true; } break; } } if(at.mirror) { if(at.eid == 0) at.eid = isize(ac.shapes[at.sid].angles); at.eid--; } else { at.eid++; if(at.eid == isize(ac.shapes[at.sid].angles)) at.eid = 0; } auto at0 = at; at = ac.shapes[at.sid].connections[at.eid]; T = T * get_adj(ac, at0.sid, at0.eid, at.sid, at.eid, at.mirror); at.mirror ^= at0.mirror; qty++; } while(qty < OINF); sh.vertex_valence[k] = qty; } if(debugflags & DF_GEOM) println(hlog, "computed vertex_valence of ", i, " as ", ac.shapes[i].vertex_valence); } return false; } EX void compute_vertex_valence(arb::arbi_tiling& ac) { for(auto& sh: ac.shapes) sh.cycle_length = isize(sh.vertices) / sh.repeat_value; bool generic = false; if(!ac.was_unmirrored) for(auto& sh: ac.shapes) if(sh.symmetric_value) generic = true; for(auto& sh: ac.shapes) for(auto& co: sh.connections) if(co.mirror) generic = true; if(cgflags & qAFFINE) generic = true; if(ac.is_star) generic = true; recompute: compute_vertex_valence_prepare(ac); if(generic ? compute_vertex_valence_generic(ac) : compute_vertex_valence_flat(ac)) goto recompute; ac.have_valence = true; ac.min_valence = UNKNOWN; ac.max_valence = 0; for(auto& sh: ac.shapes) for(auto& val: sh.vertex_valence) { if(val < ac.min_valence) ac.min_valence = val; if(val > ac.max_valence) ac.max_valence = val; } } EX bool extended_football = true; EX void check_football_colorability(arbi_tiling& c) { if(!c.have_valence) return; for(auto&sh: c.shapes) for(auto v: sh.vertex_valence) if(v % 3) return; for(int i=0; i<3; i++) { for(auto&sh: c.shapes) sh.football_type = 3; vector<int> aqueue; c.shapes[0].football_type = i; aqueue = {0}; bool bad = false; for(int qi=0; qi<isize(aqueue); qi++) { int sid = aqueue[qi]; auto& sh = c.shapes[sid]; for(int j=0; j<sh.size(); j++) { auto &co = sh.connections[j]; auto t = sh.football_type; if(c.have_ph && ((sh.flags & arcm::sfPH) != (t==2))) bad = true; if(sh.apeirogonal && t < 2 && (isize(sh) & 1)) bad = true; auto assign = [&] (int tt) { auto& t1 = c.shapes[co.sid].football_type; if(t1 == 3) { t1 = tt; aqueue.push_back(co.sid); } else { if(t1 != tt) bad = true; } }; if(t < 2) { if((j & 1) == t) assign(2); else assign((co.eid & 1) ? 0 : 1); } else { assign((co.eid & 1) ? 1 : 0); } } } if(!bad) { c.have_ph = 2; for(auto& sh: c.shapes) if(sh.football_type == 2) sh.flags |= arcm::sfPH; return; } } if(extended_football && !c.have_tree) { for(auto&sh: c.shapes) sh.football_type = 0; for(int i=0; i<3*isize(c.shapes); i++) { for(auto&sh: c.shapes) { int &res = sh.football_type; int siz = sh.size(); if(sh.apeirogonal) siz -= 2; else if(siz & 1) res |= 3; if((sh.cycle_length & 1) && !sh.apeirogonal) { if(res & 3) res |= 3; } if(sh.apeirogonal && (siz & 1)) { if(res & 3) res |= 3; } if(sh.flags & arcm::sfPH) res |= 3; for(int i=0; i<sh.size(); i++) { auto co = sh.connections[i]; co.eid %= c.shapes[co.sid].cycle_length; if(res & 1) { if(i&1) { if(co.eid & 1) c.shapes[co.sid].football_type |= 1; else c.shapes[co.sid].football_type |= 2; } else c.shapes[co.sid].football_type |= 4; } if(res & 2) { if(!(i&1)) { if(co.eid & 1) c.shapes[co.sid].football_type |= 1; else c.shapes[co.sid].football_type |= 2; } else c.shapes[co.sid].football_type |= 4; } if(res & 4) { if(co.eid & 1) c.shapes[co.sid].football_type |= 2; else c.shapes[co.sid].football_type |= 1; } } } } c.is_football_colorable = true; c.was_split_for_football = true; for(auto&sh: c.shapes) if(sh.football_type == 7) c.is_football_colorable = false; if(c.is_football_colorable) { vector<array<int, 3> > new_indices(isize(c.shapes), make_array(-1, -1, -1)); auto oldshapes = c.shapes; c.shapes.clear(); for(int i=0; i<isize(oldshapes); i++) for(int t=0; t<3; t++) if(!(oldshapes[i].football_type & (1<<t))) { if(t == 1 && (oldshapes[i].cycle_length & 1) && !oldshapes[i].apeirogonal) continue; new_indices[i][t] = isize(c.shapes); c.shapes.push_back(oldshapes[i]); c.shapes.back().football_type = t; if(t == 2) c.shapes.back().flags |= arcm::sfPH; } for(int i=0; i<isize(oldshapes); i++) for(int t=0; t<3; t++) { int ni = new_indices[i][t]; if(ni == -1) continue; auto& sh = c.shapes[ni]; sh.id = ni; for(int j=0; j<isize(sh); j++) { auto &co = sh.connections[j]; auto assign = [&] (int tt) { auto ni1 = new_indices[co.sid][tt]; if(ni1 == -1 && tt == 1) { ni1 = new_indices[co.sid][0]; co.eid += oldshapes[co.sid].cycle_length; co.eid %= isize(oldshapes[co.sid]); } co.sid = ni1; }; if(sh.apeirogonal && j >= isize(sh)-2) { co.sid = ni; if(t < 2 && (isize(sh) & 1)) co.sid = new_indices[i][t^1]; continue; } co.eid %= oldshapes[co.sid].cycle_length; if(t < 2) { if((j & 1) == t) assign(2); else assign((co.eid & 1) ? 0 : 1); } else { assign((co.eid & 1) ? 1 : 0); } } if((sh.cycle_length&1) && (t < 2) && !sh.apeirogonal) sh.cycle_length *= 2; if(debugflags & DF_GEOM) println(hlog, tie(i,t), " becomes ", ni, " with connections ", sh.connections, " and cycle length = ", sh.cycle_length); } c.have_ph = 2; return; } } for(auto&sh: c.shapes) sh.football_type = 3; } EX void add_connection_sub(arbi_tiling& c, int ai, int as, int bi, int bs, int m) { int as0 = as, bs0 = bs; auto& ash = c.shapes[ai]; auto& bsh = c.shapes[bi]; do { ash.connections[as] = connection_t{bi, bs, m}; as = gmod(as + ash.size() / ash.repeat_value, ash.size()); } while(as != as0); do { c.shapes[bi].connections[bs] = connection_t{ai, as, m}; bs = gmod(bs + bsh.size() / bsh.repeat_value, bsh.size()); } while(bs != bs0); } EX void add_connection(arbi_tiling& c, int ai, int as, int bi, int bs, int m) { auto& ash = c.shapes[ai]; auto& bsh = c.shapes[bi]; add_connection_sub(c, ai, as, bi, bs, m); int as1, bs1; if(ash.symmetric_value) { as1 = ash.reflect(as); add_connection_sub(c, ai, as1, bi, bs, !m); } if(bsh.symmetric_value) { bs1 = bsh.reflect(bs); add_connection_sub(c, ai, as, bi, bs1, !m); } if(ash.symmetric_value && bsh.symmetric_value) add_connection_sub(c, ai, as1, bi, bs1, m); } EX void set_defaults(arb::arbi_tiling& c, bool keep_sliders, string fname) { c.order++; c.name = unnamed; c.comment = ""; c.filename = fname; c.cscale = 1; c.range = 0; c.boundary_ratio = 1; c.floor_scale = .5; c.have_ph = 0; c.have_line = false; c.is_football_colorable = false; c.have_tree = false; c.have_valence = false; c.yendor_backsteps = 0; c.is_star = false; c.is_combinatorial = false; c.was_unmirrored = false; c.was_split_for_football = false; c.shapes.clear(); if(!keep_sliders) { c.sliders.clear(); c.intsliders.clear(); } } EX void load(const string& fname, bool load_as_slided IS(false), bool keep_sliders IS(false)) { fhstream f(fname, "rt"); if(!f.f) throw hr_parse_exception("file " + fname + " does not exist"); string s; while(true) { int c = fgetc(f.f); if(c < 0) break; s += c; } auto& c = load_as_slided ? slided : current; set_defaults(c, keep_sliders, fname); int qsliders = 0, qintsliders = 0; exp_parser ep; ep.s = s; ld angleunit = 1, distunit = 1; auto addflag = [&] (int f) { int ai; if(ep.next() == ')') ai = isize(c.shapes)-1; else ai = ep.iparse(); verify_index(ai, c.shapes, ep); c.shapes[ai].flags |= f; ep.force_eat(")"); }; while(true) { ep.extra_params["distunit"] = distunit; ep.extra_params["angleunit"] = angleunit; ep.skip_white(); if(ep.next() == 0) break; if(ep.eat("#")) { bool doubled = ep.eat("#"); while(ep.eat(" ")) ; string s = ""; while(ep.next() >= 32) s += ep.next(), ep.at++; if(doubled) { if(c.name == unnamed) c.name = s; else { c.comment += s; c.comment += "\n"; } } } else if(ep.eat("c2(")) { ld curv = ep.rparse(0); ep.force_eat(")"); ginf[gArbitrary].g = curv > 0 ? giSphere2 : curv < 0 ? giHyperb2 : giEuclid2; ginf[gArbitrary].sides = 7; set_flag(ginf[gArbitrary].flags, qCLOSED, curv > 0); set_flag(ginf[gArbitrary].flags, qAFFINE, false); geom3::apply_always3(); } else if(ep.eat("e2.")) { ginf[gArbitrary].g = giEuclid2; ginf[gArbitrary].sides = 7; set_flag(ginf[gArbitrary].flags, qCLOSED, false); set_flag(ginf[gArbitrary].flags, qAFFINE, false); geom3::apply_always3(); } else if(ep.eat("a2.")) { ginf[gArbitrary].g = giEuclid2; ginf[gArbitrary].sides = 7; set_flag(ginf[gArbitrary].flags, qCLOSED, false); set_flag(ginf[gArbitrary].flags, qAFFINE, true); affine_limit = 200; geom3::apply_always3(); } else if(ep.eat("h2.")) { ginf[gArbitrary].g = giHyperb2; ginf[gArbitrary].sides = 7; set_flag(ginf[gArbitrary].flags, qCLOSED, false); set_flag(ginf[gArbitrary].flags, qAFFINE, false); geom3::apply_always3(); } else if(ep.eat("s2.")) { ginf[gArbitrary].g = giSphere2; ginf[gArbitrary].sides = 5; set_flag(ginf[gArbitrary].flags, qCLOSED, true); set_flag(ginf[gArbitrary].flags, qAFFINE, false); geom3::apply_always3(); } else if(ep.eat("star.")) { c.is_star = true; } else if(ep.eat("combinatorial.")) { c.is_combinatorial = true; } else if(ep.eat("option(\"")) { next: string s = ""; while(ep.next() != '"') s += ep.eatchar(); ep.force_eat("\""); c.options.push_back(s); ep.skip_white(); if(ep.eat(",")) { ep.skip_white(); ep.force_eat("\""); goto next; } ep.force_eat(")"); } else if(ep.eat("angleunit(")) angleunit = real(ep.parsepar()); else if(ep.eat("distunit(")) distunit = real(ep.parsepar()); else if(ep.eat("line(")) { addflag(arcm::sfLINE); c.have_line = true; } else if(ep.eat("grave(")) { addflag(arcm::sfPH); c.have_ph = true; } else if(ep.eat("slider(")) { slider sl; sl.name = ep.next_token(); ep.force_eat(","); sl.current = sl.zero = ep.rparse(); ep.force_eat(","); sl.min = ep.rparse(); ep.force_eat(","); sl.max = ep.rparse(); ep.force_eat(")"); if(load_as_slided || !keep_sliders) c.sliders.push_back(sl); if(load_as_slided || keep_sliders) ep.extra_params[sl.name] = current.sliders[qsliders++].current; else ep.extra_params[sl.name] = sl.zero; } else if(ep.eat("intslider(")) { intslider sl; sl.name = ep.next_token(); ep.force_eat(","); sl.current = sl.zero = ep.iparse(); ep.force_eat(","); sl.min = ep.iparse(); ep.force_eat(","); sl.max = ep.iparse(); ep.force_eat(")"); if(load_as_slided || !keep_sliders) c.intsliders.push_back(sl); if(load_as_slided || keep_sliders) ep.extra_params[sl.name] = current.intsliders[qintsliders++].current; else ep.extra_params[sl.name] = sl.zero; } else if(ep.eat("let(")) { string tok = ep.next_token(); ep.force_eat("="); ep.extra_params[tok] =ep.parsepar(); if(debugflags & DF_GEOM) println(hlog, "let ", tok, " = ", ep.extra_params[tok]); } else if(ep.eat("unittile(")) load_tile(ep, c, true); else if(ep.eat("tile(")) load_tile(ep, c, false); else if(ep.eat("affine_limit(")) { affine_limit = ep.iparse(); ep.force_eat(")"); } else if(ep.eat("cscale(")) { c.cscale = ep.rparse(); ep.force_eat(")"); } else if(ep.eat("treestate(")) { rulegen::parse_treestate(c, ep); } else if(ep.eat("first_treestate(")) { rulegen::rule_root = ep.iparse(); ep.force_eat(")"); } else if(ep.eat("yendor_backsteps(")) { c.yendor_backsteps = ep.iparse(); ep.force_eat(")"); } else if(ep.eat("range(")) { c.range = ep.iparse(); ep.force_eat(")"); } else if(ep.eat("floor_scale(")) { c.floor_scale = ep.rparse(); ep.force_eat(")"); } else if(ep.eat("boundary_ratio(")) { c.boundary_ratio = ep.rparse(); ep.force_eat(")"); } else if(ep.eat("conway(\"")) { string s = ""; while(true) { int m = 0; if(ep.eat("(")) m = 0; else if(ep.eat("[")) m = 1; else if(ep.eat("\"")) break; else throw hr_parse_exception("cannot parse Conway notation, " + ep.where()); int ai = 0; int as = ep.iparse(); while(ep.eat("'")) ai++; if(ep.eat("@")) ai = ep.iparse(); int bi = 0, bs = 0; if(ep.eat(")") || ep.eat("]")) bs = as, bi = ai; else { bs = ep.iparse(); while(ep.eat("'")) bi++; if(ep.eat("@")) bi = ep.iparse(); } if(ep.eat(")") || ep.eat("]")) {} verify_index(ai, c.shapes, ep); verify_index(as, c.shapes[ai], ep); verify_index(bi, c.shapes, ep); verify_index(bs, c.shapes[bi], ep); add_connection(c, ai, as, bi, bs, m); } ep.force_eat(")"); } else if(ep.eat("c(")) { int ai = ep.iparse(); verify_index(ai, c.shapes, ep); ep.force_eat(","); int as = ep.iparse(); verify_index(as, c.shapes[ai], ep); ep.force_eat(","); int bi = ep.iparse(); verify_index(bi, c.shapes, ep); ep.force_eat(","); int bs = ep.iparse(); verify_index(bs, c.shapes[bi], ep); ep.force_eat(","); int m = ep.iparse(); ep.force_eat(")"); add_connection(c, ai, as, bi, bs, m); } else if(ep.eat("subline(")) { int ai = ep.iparse(); verify_index(ai, c.shapes, ep); ep.force_eat(","); int as = ep.iparse(); verify_index(as, c.shapes[ai], ep); ep.force_eat(","); int bs = ep.iparse(); verify_index(bs, c.shapes[ai], ep); ep.force_eat(")"); c.shapes[ai].sublines.emplace_back(as, bs); } else if(ep.eat("sublines(")) { ld d = ep.rparse() * distunit; ld eps = 1e-4; if(ep.eat(",")) eps = ep.rparse() * distunit; ep.force_eat(")"); for(auto& sh: c.shapes) { for(int i=0; i<isize(sh.vertices); i++) for(int j=0; j<i; j++) if(j != i+1 && i != j+1 && !(i==0 && j == isize(sh.vertices)-1) && !(j==0 && i == isize(sh.vertices)-1) && i != j) { ld dist = hdist(sh.vertices[i], sh.vertices[j]); if(abs(dist - d) < eps) { sh.sublines.emplace_back(i, j); if(debugflags & DF_GEOM) println(hlog, "add subline ", i, "-", j); } } } } else if(ep.eat("repeat(")) { int i = ep.iparse(0); verify_index(i, c.shapes, ep); ep.force_eat(","); int rep = ep.iparse(0); ep.force_eat(")"); auto& sh = c.shapes[i]; int N = isize(sh.angles); if(N % rep) throw hr_parse_exception("repeat value should be a factor of the number of vertices, " + ep.where()); sh.repeat_value = rep; int d = N / rep; for(int i=d; i<N; i++) sh.connections[i] = sh.connections[i-d]; } else if(ep.eat("debug(")) { int i = ep.iparse(0); verify_index(i, c.shapes, ep); ep.force_eat(")"); throw connection_debug_request(i); } else if(ep.eat("stretch_shear(")) { ld stretch = ep.rparse(0); ep.force_eat(","); ld shear = ep.rparse(0); ep.force_eat(","); int i = ep.iparse(0); verify_index(i, c.shapes, ep); ep.force_eat(","); int j = ep.iparse(0); verify_index(j, c.shapes[i], ep); ep.force_eat(")"); auto& sh = c.shapes[i]; sh.stretch_shear[j] = {stretch, shear}; auto& co = sh.connections[j]; auto& xsh = c.shapes[co.sid]; ld scale = sh.edges[j] / xsh.edges[co.eid]; println(hlog, "scale = ", scale); xsh.stretch_shear[co.eid] = {1/stretch, shear * (co.mirror ? 1 : -1) * stretch }; } else throw hr_parse_exception("expecting command, " + ep.where()); } if(!(cgflags & qAFFINE)) { for(int i=0; i<isize(c.shapes); i++) { auto& sh = c.shapes[i]; for(int j=0; j<isize(sh.edges); j++) { ld d1 = sh.edges[j]; auto con = sh.connections[j]; auto& xsh = c.shapes[con.sid]; ld d2 = xsh.edges[con.eid]; if(d1 == INFINITE_LEFT) d1 = INFINITE_RIGHT; else if(d1 == INFINITE_RIGHT) d1 = INFINITE_LEFT; if(abs(d1 - d2) > 1e-6) throw hr_parse_exception(lalign(0, "connecting ", make_pair(i,j), " to ", con, " of different lengths only possible in a2")); } } } if(do_unmirror) { unmirror(c); } if(!c.have_tree) compute_vertex_valence(c); check_football_colorability(c); if(c.have_tree) rulegen::verify_parsed_treestates(c); if(!load_as_slided) slided = current; } arbi_tiling debugged; vector<pair<transmatrix, int> > debug_polys; string primes(int i) { string res; while(i--) res += "'"; return res; } void connection_debugger() { cmode = sm::SIDE | sm::DIALOG_STRICT_X; gamescreen(); auto& last = debug_polys.back(); initquickqueue(); for(auto& p: debug_polys) { int id = p.second; shiftmatrix V = gmatrix[cwt.at] * p.first; auto& sh = debugged.shapes[id].vertices; for(auto& v: sh) curvepoint(v); curvepoint(sh[0]); color_t col = ccolor::shape.ctab[id]; col = darkena(col, 0, 0xFF); if(&p == &last) { vid.linewidth *= 2; queuecurve(V, 0xFFFF00FF, col, PPR::LINE); vid.linewidth /= 2; for(int i=0; i<isize(sh); i++) queuestr(V * sh[i], vid.fsize, its(i), 0xFFFFFFFF); } else queuecurve(V, 0xFFFFFFFF, col, PPR::LINE); } quickqueue(); dialog::init(XLAT("connection debugger")); dialog::addInfo(debugged.name); dialog::addHelp(debugged.comment); dialog::addBreak(50); dialog::addInfo("face index " + its(last.second)); dialog::addBreak(50); auto& sh = debugged.shapes[last.second]; int N = isize(sh.edges); for(int k=0; k<N; k++) { auto con = sh.connections[k]; string cap = its(k) + primes(last.second) + " -> " + its(con.eid) + primes(con.sid) + (con.mirror ? " (m) " : ""); dialog::addSelItem(cap, "go", '0' + k); dialog::add_action([k, last, con] { if(euclid) cgflags |= qAFFINE; debug_polys.emplace_back(last.first * get_adj(debugged, last.second, k), con.sid); if(euclid) cgflags &= ~qAFFINE; }); } dialog::addItem("undo", 'u'); dialog::add_action([] { if(isize(debug_polys) > 1) debug_polys.pop_back(); }); dialog::addBack(); dialog::display(); keyhandler = [] (int sym, int uni) { handlePanning(sym, uni); dialog::handleNavigation(sym, uni); if(doexiton(sym, uni)) popScreen(); }; } geometryinfo1& arbi_tiling::get_geometry() { return ginf[gEuclid].g; } map<heptagon*, vector<pair<heptagon*, transmatrix> > > altmap; EX map<heptagon*, pair<heptagon*, transmatrix>> arbi_matrix; EX hrmap *current_altmap; heptagon *build_child(heptspin p, pair<int, int> adj); /** get the midedge of lr; it takes infinite vertices into account */ EX hyperpoint get_midedge(ld len, const hyperpoint &l, const hyperpoint &r) { if(len == INFINITE_BOTH) { return normalize(closest_to_zero(l, r)); } else if(len == INFINITE_RIGHT) { return towards_inf(r, l); } else if(len == INFINITE_LEFT) { return towards_inf(l, r); } else return mid(l, r); } EX bool is_apeirogonal(cell *c) { if(!in()) return false; return current_or_slided().shapes[id_of(c->master)].apeirogonal; } EX bool is_apeirogonal() { if(!in()) return false; for(auto& sh: current_or_slided().shapes) if(sh.apeirogonal) return true; return false; } EX bool apeirogon_consistent_coloring = true; EX bool apeirogon_hide_grid_edges = true; EX bool apeirogon_simplified_display = false; /** get the adj matrix corresponding to the connection of (t,dl) to connection_t{t1, xdl, xmirror} */ EX transmatrix get_adj(arbi_tiling& c, int t, int dl, int t1, int xdl, bool xmirror) { auto& sh = c.shapes[t]; int dr = gmod(dl+1, sh.size()); auto& xsh = c.shapes[t1]; int xdr = gmod(xdl+1, xsh.size()); hyperpoint vl = sh.vertices[dl]; hyperpoint vr = sh.vertices[dr]; hyperpoint xvl = xsh.vertices[xdl]; hyperpoint xvr = xsh.vertices[xdr]; bool emb = embedded_plane; if(emb) { vl = cgi.emb->actual_to_base(vl); vr = cgi.emb->actual_to_base(vr); xvl = cgi.emb->actual_to_base(xvl); xvr = cgi.emb->actual_to_base(xvr); geom3::light_flip(true); } hyperpoint vm = get_midedge(sh.edges[dl], vl, vr); transmatrix rm = gpushxto0(vm); hyperpoint xvm = get_midedge(xsh.edges[xdl], xvl, xvr); transmatrix xrm = gpushxto0(xvm); transmatrix Res = rgpushxto0(vm) * rspintox(rm*vr); if(cgflags & qAFFINE) { ld sca = hdist(vl, vr) / hdist(xvl, xvr); transmatrix Tsca = Id; Tsca[0][0] = Tsca[1][1] = sca; auto& ss = sh.stretch_shear[dl]; Tsca[0][1] = ss.first * ss.second * sca; Tsca[1][1] *= ss.first; Res = Res * Tsca; } if(xmirror) Res = Res * MirrorX; Res = Res * spintox(xrm*xvl) * xrm; if(xmirror) swap(vl, vr); if(hdist(vl, Res*xvr) + hdist(vr, Res*xvl) > .1 && !c.is_combinatorial) { println(hlog, "s1 = ", kz(spintox(rm*vr)), " s2 = ", kz(rspintox(xrm*xvr))); println(hlog, tie(t, dl), " = ", kz(Res)); println(hlog, hdist(vl, Res * xvr), " # ", hdist(vr, Res * xvl)); throw hr_exception("error in arb::get_adj"); } if(emb) { Res = cgi.emb->base_to_actual(Res); geom3::light_flip(false); } return Res; } /** get the adj matrix corresponding to the connection of (t,dl) -- note: it may be incorrect for rotated/symmetric connections */ EX transmatrix get_adj(arbi_tiling& c, int t, int dl) { auto& sh = c.shapes[t]; auto& co = sh.connections[dl]; return get_adj(c, t, dl, co.sid, co.eid, co.mirror); } /** Returns if F describes the same tile as T, taking possible symmetries into account. Paramater co is the expected edge (co.sid tells us the tile type); if yes, co may be adjusted */ EX bool find_connection(const transmatrix& T, const transmatrix& F, connection_t& co) { if(!same_point_may_warn(tC0(F), tC0(T))) return false; auto& xsh = current.shapes[co.sid]; int n = isize(xsh.connections); for(int oth = 0; oth < n; oth++) { int oth1 = gmod(oth+1, n); int eid1 = gmod(co.eid+1, n); if(same_point_may_warn(F * xsh.vertices[oth], T * xsh.vertices[co.eid]) && same_point_may_warn(F * xsh.vertices[oth1], T * xsh.vertices[eid1])) { co.eid = oth; return true; } if(same_point_may_warn(F * xsh.vertices[oth], T * xsh.vertices[eid1]) && same_point_may_warn(F * xsh.vertices[oth1], T * xsh.vertices[co.eid])) { co.eid = oth; co.mirror = !co.mirror; return true; } } return false; } struct hrmap_arbi : hrmap { heptagon *origin; heptagon *getOrigin() override { return origin; } hrmap_arbi() { dynamicval<hrmap*> curmap(currentmap, this); origin = init_heptagon(current.shapes[0].size()); origin->s = hsOrigin; origin->c7 = newCell(origin->type, origin); heptagon *alt = NULL; if(mhyperbolic) { dynamicval<eGeometry> g(geometry, gNormal); alt = init_heptagon(S7); alt->s = hsOrigin; alt->alt = alt; current_altmap = newAltMap(alt); } transmatrix T = lxpush(.01241) * spin(1.4117) * lxpush(0.1241) * Id; arbi_matrix[origin] = make_pair(alt, T); altmap[alt].emplace_back(origin, T); if(!current.range) current.range = auto_compute_range(origin->c7); } ~hrmap_arbi() { clearfrom(origin); altmap.clear(); arbi_matrix.clear(); if(current_altmap) { dynamicval<eGeometry> g(geometry, gNormal); delete current_altmap; current_altmap = NULL; } } void verify() override { } transmatrix adj(heptagon *h, int dl) override { if(h->c.move(dl)) return get_adj(current_or_slided(), id_of(h), dl, id_of(h->c.move(dl)), h->c.spin(dl), h->c.mirror(dl)); else return get_adj(current_or_slided(), id_of(h), dl); } heptagon *create_step(heptagon *h, int d) override { if(geom3::flipped) return geom3::in_not_flipped([&] { return create_step(h, d); }); dynamicval<bool> sl(using_slided, false); int t = id_of(h); auto& sh = current.shapes[t]; auto& co = sh.connections[d]; if(cgflags & qAFFINE) { set<heptagon*> visited; vector<pair<heptagon*, transmatrix> > v; visited.insert(h); v.emplace_back(h, Id); transmatrix goal = adj(h, d); for(int i=0; i<affine_limit && i < isize(v); i++) { transmatrix T = v[i].second; heptagon *h2 = v[i].first; if(eqmatrix(T, goal)) { h->c.connect(d, h2, co.eid, co.mirror); return h2; } for(int i=0; i<h2->type; i++) { heptagon *h3 = h2->move(i); if(!h3) continue; if(visited.count(h3)) continue; visited.insert(h3); v.emplace_back(h3, T * adj(h2, i)); } } auto h1 = init_heptagon(current.shapes[co.sid].size()); h1->distance = h->distance + 1; h1->zebraval = co.sid; h1->c7 = newCell(h1->type, h1); h1->emeraldval = h->emeraldval ^ co.mirror; h->c.connect(d, h1, co.eid, co.mirror); return h1; } const auto& p = arbi_matrix[h]; heptagon *alt = p.first; transmatrix T = p.second * adj(h, d); if(mhyperbolic) { dynamicval<eGeometry> g(geometry, gNormal); dynamicval<hrmap*> cm(currentmap, current_altmap); // transmatrix U = T; current_altmap->virtualRebase(alt, T); // U = U * inverse(T); } fixmatrix(T); if(meuclid) { /* hash the rough coordinates as heptagon* alt */ size_t s = size_t(T[0][LDIM]+.261) * 124101 + size_t(T[1][LDIM]+.261) * 82143; alt = (heptagon*) s; } for(auto& p2: altmap[alt]) if(id_of(p2.first) == co.sid) { connection_t co1 = co; if(find_connection(T, p2.second, co1)) { if(p2.first->move(co1.eid)) { throw hr_exception("already connected!"); } h->c.connect(d, p2.first, co1.eid, co1.mirror); return p2.first; } } auto h1 = init_heptagon(current.shapes[co.sid].size()); h1->distance = h->distance + 1; h1->zebraval = co.sid; h1->c7 = newCell(h1->type, h1); h1->emeraldval = h->emeraldval ^ co.mirror; h->c.connect(d, h1, co.eid, co.mirror); arbi_matrix[h1] = make_pair(alt, T); altmap[alt].emplace_back(h1, T); return h1; } transmatrix relative_matrixh(heptagon *h2, heptagon *h1, const hyperpoint& hint) override { return relative_matrix_recursive(h2, h1); } transmatrix adj(cell *c, int dir) override { return adj(c->master, dir); } ld spin_angle(cell *c, int d) override { return SPIN_NOT_AVAILABLE; } int shvid(cell *c) override { return id_of(c->master); } hyperpoint get_corner(cell *c, int cid, ld cf) override { auto& sh = arb::current_or_slided().shapes[arb::id_of(c->master)]; int id = gmod(cid, c->type); if(sh.angles[gmod(id-1, c->type)] <= 0) return sh.vertices[id]; return normalize(C0 + (sh.vertices[id] - C0) * 3 / cf); } }; EX hrmap *new_map() { return new hrmap_arbi; } EX void run_raw(string fname) { stop_game(); set_geometry(gArbitrary); load(fname); ginf[gArbitrary].tiling_name = current.name; tes = fname; convert::base_geometry = gArbitrary; } EX void run(string fname) { eGeometry g = geometry; arbi_tiling t = current; auto v = variation; try { run_raw(fname); } catch(hr_polygon_error& poly) { set_geometry(g); set_variation(v); current = t; start_poly_debugger(poly); string help = poly.generate_error(); showstartmenu = false; for(auto& p: poly.params) help += lalign(-1, p.first, " = ", p.second, "\n"); gotoHelp(help); } catch(hr_parse_exception& ex) { println(hlog, "failed: ", ex.s); set_geometry(g); current = t; start_game(); addMessage("failed: " + ex.s); } catch(connection_debug_request& cr) { set_geometry(g); debugged = current; current = t; ensure_geometry(cr.c); debug_polys.clear(); debug_polys.emplace_back(Id, cr.id); pushScreen(connection_debugger); } start_game(); } string slider_error; EX void sliders_changed(bool need_restart, bool need_start) { if(need_restart) stop_game(); auto& c = current_or_slided(); arbi_tiling backup = c; try { load(current.filename, !need_restart, need_restart); using_slided = !need_restart; slider_error = "OK"; #if CAP_TEXTURE texture::config.remap(); #endif } catch(hr_parse_exception& ex) { c = backup; slider_error = ex.s; } catch(hr_polygon_error& poly) { c = backup; slider_error = poly.generate_error(); } if(need_restart && need_start) start_game(); } EX void set_sliders() { cmode = sm::SIDE | sm::MAYDARK; gamescreen(); dialog::init(XLAT("tessellation sliders")); dialog::addHelp(current.comment); char ch = 'A'; for(auto& sl: current.sliders) { dialog::addSelItem(sl.name, fts(sl.current), ch++); dialog::add_action([&] { dialog::editNumber(sl.current, sl.min, sl.max, 1, sl.zero, sl.name, sl.name); dialog::get_di().reaction = [] { sliders_changed(false, false); }; }); } if(isize(current.intsliders)) dialog::addInfo(XLAT("the following sliders will restart the game")); for(auto& sl: current.intsliders) { dialog::addSelItem(sl.name, its(sl.current), ch++); dialog::add_action([&] { dialog::editNumber(sl.current, sl.min, sl.max, 1, sl.zero, sl.name, sl.name); dialog::get_di().reaction = [] { sliders_changed(true, true); }; }); } dialog::addInfo(slider_error); dialog::addBack(); dialog::display(); } /** convert a tessellation (e.g. Archimedean, regular, etc.) to the arb::current internal representation */ EX namespace convert { EX eGeometry base_geometry = gArbitrary; EX eVariation base_variation; struct id_record { int target; /* master of this id type */ int shift; /* sample direction 0 == our direction shift */ int modval; /* this master type is the same as itself rotated by modval */ cell *sample; /* sample of the master type */ }; inline void print(hstream& hs, const id_record& i) { print(hs, "[", i.target, " shift=", i.shift, " mod=", i.modval, "]"); } map<int, id_record> identification; id_record& get_identification(int s, cell *c) { if(!identification.count(s)) { auto &id = identification[s]; id.target = s; id.shift = 0; id.modval = c->type; id.sample = c; } return identification[s]; } id_record& get_identification(cell *c) { auto id = currentmap->full_shvid(c); return get_identification(id, c); } int changes; void be_identified(cellwalker cw1, cellwalker cw2) { auto& id1 = get_identification(cw1.at); auto& id2 = get_identification(cw2.at); indenter ind(2); int t = cw2.at->type; if(cw1.at->type != t) { println(hlog, cw1.at->type, " vs ", t); throw hr_exception("numbers disagree"); } int d2 = gmod(-cw2.to_spin(id2.shift), id2.modval); int d1 = gmod(-cw1.to_spin(id1.shift), id1.modval); indenter ind1(2); if(id2.target != id1.target) { auto oid2 = id2; id1.modval = gcd(id1.modval, id2.modval); for(auto& p: identification) { auto& idr = p.second; if(idr.target == oid2.target) { idr.target = id1.target; idr.modval = id1.modval; idr.shift = gmod(idr.shift + (d2-d1), idr.modval); idr.sample = id1.sample; } } changes++; println(hlog, identification); return; } if(d2 != d1) { auto oid2 = id2; id2.modval = gcd(id2.modval, abs(d2-d1)); for(auto& p: identification) if(p.second.target == oid2.target) p.second.modval = id2.modval; changes++; println(hlog, identification); return; } } EX bool reverse_order; EX bool minimize_on_convert; EX void convert_max() { identification.clear(); changes = 0; manual_celllister cl; cl.add(currentmap->gamestart()); int more_tests = 1000; pointer_indices.clear(); int chg = -1; while(changes > chg) { changes = chg; set<int> masters_analyzed; for(int i=0; i<isize(cl.lst); i++) { auto c = cl.lst[i]; auto& id = get_identification(c); if(masters_analyzed.count(id.target)) { more_tests--; if(more_tests < 0) continue; } masters_analyzed.insert(id.target); cellwalker cw0(c, id.shift); cellwalker cws(id.sample, 0); for(int i=0; i<c->type; i++) { if(1) { indenter ind(2); be_identified(cw0 + i + wstep, cws + i + wstep); be_identified(cw0 + i + wstep, cw0 + i + id.modval + wstep); } if(1) { indenter ind(2); auto cwx = cw0 + i + wstep; auto idx = get_identification(cwx.at); cellwalker xsample(idx.sample, cwx.spin); xsample -= idx.shift; be_identified(cwx + wstep, xsample + wstep); cl.add((cw0 + i).cpeek()); } } } } } EX void convert_minimize(int N, vector<int>& old_shvids, map<int, int>& old_to_new) { vector<pair<int, int>> address; vector<int> address_start; for(int i=0; i<N; i++) { int q = identification[old_shvids[i]].modval; int c = isize(address); address_start.push_back(c); for(int j=0; j<q; j++) { address.emplace_back(i, j); } } int K = isize(address); vector<int> next(K), step(K); for(int k=0; k<K; k++) { auto i = address[k].first; auto j = address[k].second; auto& id = identification[old_shvids[i]]; next[k] = address_start[i] + (j+1) % id.modval; cell *s = id.sample; cellwalker cw(s, j); cw += wstep; auto idx = get_identification(cw.at); step[k] = address_start[old_to_new.at(idx.target)] + gmod(cw.spin - idx.shift, idx.modval); } vector<array<ld, 3> > dists(K); for(int i=0; i<K; i++) { auto pi = address[i]; auto si = identification[old_shvids[pi.first]]; pi.second += si.shift; array<hyperpoint, 3> pcorner; array<ld, 3> pdists; for(int j=0; j<3; j++) pcorner[j] = currentmap->get_corner(si.sample, gmod(pi.second+j, si.sample->type)); for(int j=0; j<3; j++) pdists[j] = hdist(pcorner[j], pcorner[(j+1)%3]); dists[i] = pdists; } // this is O(K^3) and also possibly could get confused on convex/concave, // but should be good enough, hopefully vector<vector<int>> equal(K); for(int i=0; i<K; i++) equal[i].resize(K, 0); for(int i=0; i<K; i++) for(int j=0; j<K; j++) { equal[i][j] = true; for(int s=0; s<3; s++) equal[i][j] = equal[i][j] && abs(dists[i][s] - dists[j][s]) < 1e-6; } int chg = 1; while(chg) { if(debugflags & DF_GEOM) { println(hlog, "current table of equals:"); int eqid = 0; for(auto& eq: equal) { println(hlog, eq, " for ", eqid, ": ", address[eqid], " next= ", next[eqid], " step= ", step[eqid]); eqid++; } } chg = 0; for(int i=0; i<K; i++) for(int j=0; j<K; j++) if(equal[i][j] && (!equal[next[i]][next[j]] || !equal[step[i]][step[j]])) { equal[i][j] = false; chg++; } } for(int i=0; i<K; i++) for(int j=0; j<K; j++) if(i!=j && equal[i][j]) { auto pi = address[i]; auto si = identification[old_shvids[pi.first]]; cellwalker cwi(si.sample, si.shift + pi.second); auto pj = address[j]; auto sj = identification[old_shvids[pj.first]]; cellwalker cwj(sj.sample, sj.shift + pj.second); be_identified(cwi, cwj); } } EX void convert() { start_game(); convert_max(); bool minimize = minimize_on_convert; reidentify: vector<int> old_shvids; map<int, int> old_to_new; for(auto id: identification) if(id.first == id.second.target) { old_to_new[id.first] = isize(old_shvids); old_shvids.push_back(id.first); } int N = isize(old_shvids); println(hlog, "N = ", N); if(minimize) { convert_minimize(N, old_shvids, old_to_new); minimize = false; goto reidentify; } if(reverse_order) { reverse(old_shvids.begin(), old_shvids.end()); for(int i=0; i<isize(old_shvids); i++) old_to_new[old_shvids[i]] = i; } auto& ac = arb::current; ac.order++; ac.filename = full_geometry_name(); ac.comment = "converted from: " + ac.filename; ac.cscale = cgi.scalefactor; ac.boundary_ratio = 1; ac.floor_scale = cgi.hexvdist / cgi.scalefactor; ac.range = cgi.base_distlimit; ac.shapes.clear(); ac.shapes.resize(N); ginf[gArbitrary].g = cginf.g; ginf[gArbitrary].flags = cgflags & qCLOSED; ginf[gArbitrary].tiling_name = full_geometry_name(); for(int i=0; i<N; i++) { auto id = identification[old_shvids[i]]; cell *s = id.sample; auto& sh = ac.shapes[i]; sh.id = i; int t = s->type; sh.vertices.clear(); sh.connections.clear(); sh.cycle_length = id.modval; if(arcm::in()) sh.orig_id = arcm::get_graphical_id(s); else sh.orig_id = shvid(s); sh.repeat_value = t / id.modval; sh.flags = hr::pseudohept(s) ? arcm::sfPH : 0; #if CAP_ARCM if(arcm::in() && arcm::linespattern(s)) { sh.flags |= arcm::sfLINE; ac.have_line = true; } #endif for(int j=0; j<t; j++) { auto co = currentmap->get_corner(s, j); sh.vertices.push_back(co); cellwalker cw(s, j); cw += wstep; auto idx = get_identification(cw.at); cellwalker xsample(idx.sample, cw.spin); xsample -= idx.shift; sh.connections.emplace_back(arb::connection_t{old_to_new.at(idx.target), xsample.spin, false}); } sh.stretch_shear.resize(t, make_pair(1, 0)); sh.edges.clear(); for(int j=0; j<t-1; j++) sh.edges.push_back(hdist(sh.vertices[j], sh.vertices[j+1])); sh.edges.push_back(hdist(sh.vertices[t-1], sh.vertices[0])); sh.angles.clear(); for(int j=0; j<t; j++) { hyperpoint v0 = sh.vertices[j]; hyperpoint v1 = sh.vertices[(j+1) % t]; hyperpoint v2 = sh.vertices[(j+2) % t]; transmatrix T = gpushxto0(v1); v0 = T * v0; v2 = T * v2; ld alpha = atan2(v0) - atan2(v2); cyclefix(alpha, 0); sh.angles.push_back(alpha); } if(debugflags & DF_GEOM) { println(hlog, "shape ", i, ":"); indenter indp(2); println(hlog, "vertices=", sh.vertices); println(hlog, "connections=", sh.connections); println(hlog, "edges=", sh.edges); println(hlog, "angles=", sh.angles); } } arb::compute_vertex_valence(ac); ac.have_ph = geosupport_football() ? 1 : 0; arb::check_football_colorability(ac); } EX bool in() { return arb::in() && base_geometry != gArbitrary; } /** activate the converted tessellation */ EX void activate() { if(geometry != gArbitrary) { base_geometry = geometry; base_variation = variation; stop_game(); geometry = gArbitrary; variation = eVariation::pure; } } EX } #if CAP_COMMANDLINE int readArgs() { using namespace arg; if(0) ; else if(argis("-tes") || argis("-arbi")) { PHASEFROM(2); shift(); run_raw(args()); } else if(argis("-tes-opt")) { arg::run_arguments(current.options); } else if(argis("-arb-convert")) { try { convert::convert(); set_geometry(gArbitrary); } catch(hr_exception& e) { println(hlog, "failed to convert: ", e.what()); } } else if(argis("-arb-unmirror")) { shift(); do_unmirror = argi(); } else if(argis("-arb-football")) { shift(); extended_football = argi(); } else if(argis("-arb-slider")) { PHASEFROM(2); shift(); string slider = args(); bool found = true; for(auto& sl: current.sliders) if(sl.name == slider) { shift_arg_formula(sl.current, [] { sliders_changed(false, false); }); found = true; } for(auto& sl: current.intsliders) if(sl.name == slider) { shift(); sl.current = argi(); stop_game(); sliders_changed(true, false); found = true; } if(!found) { println(hlog, "warning: no slider named ", slider, " found"); shift(); } } else return 1; return 0; } auto hook = addHook(hooks_args, 100, readArgs); #endif EX bool in() { return geometry == gArbitrary; } EX string tes = find_file("tessellations/sample/marjorie-rice.tes"); EX bool linespattern(cell *c) { return current.shapes[id_of(c->master)].flags & arcm::sfLINE; } EX bool pseudohept(cell *c) { if(!current.have_ph) return true; return current.shapes[id_of(c->master)].flags & arcm::sfPH; } EX void choose() { dialog::openFileDialog(tes, XLAT("open a tiling"), ".tes", [] () { run(tes); #if CAP_COMMANDLINE if(!current.options.empty()) dialog::push_confirm_dialog([] { arg::run_arguments(current.options); start_game(); }, "load the settings defined in this file?"); #endif return true; }); } EX pair<ld, ld> rep_ideal(ld e, ld u IS(1)) { ld alpha = TAU / e; hyperpoint h1 = point3(cos(alpha)*u, -sin(alpha)*u, 1); hyperpoint h2 = point3(u, 0, 1); hyperpoint h3 = point3(cos(alpha)*u, sin(alpha)*u, 1); hyperpoint h12 = mid(h1, h2); hyperpoint h23 = mid(h2, h3); ld len = hdist(h12, h23); transmatrix T = gpushxto0(h12); auto T0 = T * C0; auto Th23 = T * h23; ld beta = atan2(T0); ld gamma = atan2(Th23); return {len, 90._deg - (gamma - beta)}; } EX void swap_vertices() { for(auto& p: {¤t, &slided}) for(auto& s: p->shapes) for(auto& v: s.vertices) swappoint(v); } #if MAXMDIM >= 4 auto hooksw = addHook(hooks_swapdim, 100, [] { swap_vertices(); for(auto& p: altmap) for(auto& pp: p.second) swapmatrix(pp.second); for(auto& p: arbi_matrix) swapmatrix(p.second.second); }); #endif EX } }