ガイド 第35

マップ & タイムテーブル

@event/spatial

@event/spatial は「会場マップ(SVG ホットスポット)」と「タイムテーブル(レーン割当)」の2系統を、React 非依存の純アルゴリズムと 1 つの client プリミティブに畳み込む。ivs-cms の FloorMap / OverallMap / StartupZoneMap、xdeal-lp の FloorPlan、spikes-lp の StageTimetableGrid に散っていた同型コードの統合版である。実データはエンジンに焼かず、イベント側が SpatialConfig と 4S セッションを注入する。

SvgHotspotLayer(hotspot/)— THE core value

SvgHotspotLayer"use client" プリミティブ。「背景にインライン SVG を展開 + その上に viewBox 座標のオーバーレイ図形を重ねて hover / select / ヒット判定 + ディープリンク」を 1 コンポーネントにする。server component から直接描画してはいけない — 必ず自前の "use client" ラッパ(例では VenueMap.tsx)を経由する。

  • 背景 SVG は svg(インライン文字列)優先、無ければ svgUrl を client fetch してインライン展開(結果は module-level cache)。ラスタは imageUrl フォールバック。
  • オーバーレイの各 Hotspotshaperect / circle / poly、配列で複合可)でヒット領域を作る。透明 fill でも pointer-events: all で当たり、cursor: pointer
  • select は controlledselectedId prop)。URL ハッシュ等に載せれば会場図のディープリンク・クラッシュズームが成立する。focal はそのフォーカル点。
  • hit-test 貫通: hitTestAttribute(例 "data-room-id")を渡すと、オーバーレイの隙間クリックを document.elementsFromPoint で辿り、背景 SVG 要素の属性値と一致する id を select する(FloorMap 相当。オクルージョンを貫通)。
  • テーマは全て --spatial-* CSS 変数経由でイベント色を焼かない。既定描画を変えるなら renderHotspot レンダープロップ。debug で全ヒット形状を可視化。
  • dataunknown)に room / booth 等の任意ペイロードを載せ、onSelect(id, hotspot) で受け取れる。

lanePack(timetable/lanePack)— 重なり区間 → レーン

assignLanes<T extends LaneInterval>(items) は純関数。各区間(startMin / endMin)を greedy に最小の空きレーンへ詰め、返り値に lane(自分のレーン index)と lanes自分と時間が重なる全区間の concurrency = 同時並行最大数)を付ける。UI 側は lane / lanes で横位置(left = lane/lanes)と幅(1/lanes)を決めれば、同時刻セッションが自動で横並びに割れる。クラスタ単位で同じ幅を使うので歯抜けが減る。数式は spikes-lp の assignLanes から verbatim 移植。

parseHHMM(label)"HH:MM" 先頭を分(0–1439)に変換(パース不可で null)。

JST 固定 formatTime 群(timetable/lanePack)

本番(Vercel)はサーバが UTC で動く。Date#getHours() 等の「実行環境ローカル時刻」を SSR で使うと、JST 0:00–9:00 のセッションが前日にズレる@event/spatial の時刻系は全て、+9h シフトした Date の getUTC* で「JST の壁時計」を取り出す(実行 TZ 非依存)。

  • jst(isoOrDayKey) … +9h シフト済み Date。'YYYY-MM-DD' day-key は JST 0:00 と解釈。
  • fmtTime(iso) = "HH:MM"fmtTimeRange(session) = "HH:MM–HH:MM"(開始 + durationMin)、endTime / endIso
  • fmtDateLong / fmtDateShort / fmtDayLabel(day chip 用 { date, weekday })。
  • dayKey(iso) … 4S の UTC ISO を JST に換算してから 'YYYY-MM-DD' を切り出す(単純 slice だと UTC 日付が出て前日扱いになる)。jstMinutesOfDay(iso) … グリッド縦位置の原点計算に使う「その日の分」。

timetable model(timetable/model)

@event/schemaSession / Room の上に薄く構築し、型フォークを避ける。

  • TimetableTrack = グリッドの 1 列。通常はステージ(Room)だが、単一ステージのイベントでは「開催日」を列にする等、列の意味はイベントが決める。trackFromRoom(room) で Room → Track。
  • toTimeslot(session, trackId?)SessionTimeslotstartMin / endMin は JST 壁時計分)に変換。trackId 省略時は session.roomId
  • packTimeslots(sessions, trackId?) … セッション群を toTimeslotassignLanes し、LanedTimeslot[]lane / lanes 付き)を返す。アプリ層は日付でグルーピングしてから列 or 全体単位で呼ぶ。

spatial.config(config)

会場の空間データ(SVG + viewBox + ホットスポット / ブース座標)を宣言的に持つ型。SpatialConfigoverview(俯瞰 / 会場セレクタ)・floors(id → SpatialSvgSource)・boothGrids(id → BoothGrid)。SpatialSvgSourcesvg / svgUrl / svgUrlEn / imageUrl + viewBox + hotspotsboothGridToHotspots(grid, label?) はブース矩形テーブル(Record<boothId, BoothRect>)を Hotspot[] に写像するヘルパ(Startup Market の SA/SB/SC のような格子ブース向け)。座標は全て各 SVG の viewBox 単位。

アプリ層での組み立て — 実装済み(ScheduleMapView)

かつて「意図的に deferred」としていた 2 項目 — ScheduleMap ページ全体タイムテーブルの PNG エクスポート — は、方針どおり本パッケージに持ち込まずアプリ層で実装済みである(starter の /timetable = apps/starter/src/app/timetable/ScheduleMapView.tsx。完全手順はレシピ「add-spatial」)。

  • ScheduleMap ページ: 「タイムテーブル / 会場マップ」のタブ、day 切替(JST day-key ごと)、track / room フィルタ(フィルタ後は packTimeslots でレーンを詰め直す)、セッション click で下部の詳細パネル(モーダルではない)。
  • PNG 書き出し: 表示中グリッドと同一の React ツリーを第39章 @event/asset-kitexportPng/capture。modern-screenshot 主 + html2canvas-pro fallback)へ渡し、実寸計測 + exportScale: 2 で焼く。アプリ側に canvas コードは書かない。

エンジンは「ヒット判定つき SVG レイヤ」と「重なり → レーンの数式」「JST 壁時計」だけを提供し、レイアウト・ページ構成・書き出しはイベントごとに差し替え可能な形に保つ、という責務分担はこの実装でも変わらない。