SEED MAP shared wheel rim: the table's OWN sky (planets-only, canonical signs) rings the tessellation — one frame for all six gamers — TDD
All checks were successful
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline was successful

The Voronoi felt gains a stripped sky-wheel rim drawn from the ROOM's own sky
(Room.sky_chart) — identical for every gamer, updating toward the shared map —
with the tessellation sized into the wheel's freed hub. Roadmap step 21, Step
2's coordinate frame.

- Room.convened_at + Room.sky_chart (migration 0019); pick_roles stamps
  convened_at at gate-close (stamp only — no HTTP on the transition)
- epic.table_sky: lazy planets-only chart via PySwiss at the null location
  (geocentric longitudes need only the convened TIME; houses/ASC/MC need a
  birth LOCATION a virtual table lacks → omitted), cached on Room.sky_chart;
  legacy rooms key off created_at; seated-gamer gated, 502 on PySwiss down
- SkyWheel.drawRim(svg, data): pure static renderer — canonical asc=0 frame,
  signs ring + planet glyphs only, NO element ring / centre disc / houses /
  axes / aspects / tooltips; never writes the interactive wheel's singleton
  state; returns {size, cx, cy, r, hubR} so the felt sizes the map into the hub
- _seed_map_overlay.html: rim draws on open; map svg shrinks to 2×hubR +
  .voronoi-map--rimmed clip; lazy table-sky fetch on open; preload-then-repaint
  so a cold-cache open doesn't strand the zodiac glyphs; ResizeObserver on the
  col (not the self-sized map svg)
- _sky.scss: stacked centred svgs in .seed-map-col; .seed-wheel pointer-events
  none; circle clip on the rimmed map
- room_sky_json ctx in _role_select_context; rootvars: --sixUser/--octUser
  nudged within the Trs ramp (parallel palette tune)
- drawRim Jasmine suite (R1–R6: signs+planets, strip, hub geometry, static
  placement, singleton untouched, signs-only fallback) in both spec copies;
  epic TableSkyViewTest + convened_at stamp + seed-overlay rim ITs; FT rim
  assertions (12 signs, 3 planets, no stripped/located layers, hub sizing)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Disco DeDisco
2026-06-10 00:33:23 -04:00
parent cde556b178
commit 9ed877168e
12 changed files with 764 additions and 21 deletions

View File

@@ -696,6 +696,11 @@ def _role_select_context(room, user, seat_param=None):
if (sky_confirmed and confirmed_char.chart_data)
else "null"
)
# The table's OWN sky — the shared SEED MAP rim frame (one canonical
# frame for all six gamers, NOT this seat's natal chart). None until
# the lazy table_sky endpoint computes + caches it; the felt then
# fetches it on open.
ctx["room_sky_json"] = json.dumps(room.sky_chart) if room.sky_chart else None
if sky_confirmed:
# Fall back to seat.significator for Characters created before the sync was added
ctx["my_tray_sig"] = confirmed_char.significator or _sky_seat.significator
@@ -1282,6 +1287,10 @@ def pick_roles(request, room_id):
room = Room.objects.get(id=room_id)
if room.gate_status == Room.OPEN and room.table_status is None:
room.table_status = Room.ROLE_SELECT
# The table's convene moment — the timestamp its OWN sky is cast
# from (the shared SEED MAP rim). Stamp only; the chart computes
# LAZILY in table_sky so this transition never fires HTTP.
room.convened_at = timezone.now()
room.save()
for slot in room.gate_slots.filter(status=GateSlot.FILLED).order_by("slot_number"):
TableSeat.objects.create(
@@ -1823,6 +1832,47 @@ def sky_preview(request, room_id):
return JsonResponse(data)
@login_required
def table_sky(request, room_id):
"""The table's OWN sky — the shared SEED MAP rim frame ([[project-voronoi-spec]]).
Planets-only: geocentric longitudes need only the convened TIME (gate
close, Room.convened_at; legacy rooms key off created_at), while houses/
ASC/MC need a birth LOCATION a virtual table doesn't have — so PySwiss is
queried at the null location and only the location-independent keys
(planets, aspects) are kept. Computed LAZILY here on first request and
cached on Room.sky_chart — never at the pick_roles transition, which must
stay HTTP-free.
Returns {planets, aspects} 200 · 403 not seated · 502 PySwiss unreachable.
"""
room = Room.objects.get(id=room_id)
if _canonical_user_seat(room, request.user) is None:
return HttpResponse(status=403)
if room.sky_chart:
return JsonResponse(room.sky_chart)
convened = room.convened_at or room.created_at
dt_iso = convened.astimezone(zoneinfo.ZoneInfo('UTC')).strftime('%Y-%m-%dT%H:%M:%SZ')
try:
resp = http_requests.get(
settings.PYSWISS_URL + '/api/chart/',
params={'dt': dt_iso, 'lat': '0', 'lon': '0'},
timeout=5,
)
resp.raise_for_status()
except Exception:
return HttpResponse(status=502)
data = resp.json()
room.sky_chart = {
'planets': data.get('planets') or {},
'aspects': data.get('aspects') or [],
}
room.save(update_fields=['sky_chart'])
return JsonResponse(room.sky_chart)
@login_required
def sky_save(request, room_id):
"""Create or update the draft Character for the requesting gamer's seat.