@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フォールバック。 - オーバーレイの各
Hotspotはshape(rect/circle/poly、配列で複合可)でヒット領域を作る。透明 fill でもpointer-events: allで当たり、cursor: pointer。 - select は controlled(
selectedIdprop)。URL ハッシュ等に載せれば会場図のディープリンク・クラッシュズームが成立する。focalはそのフォーカル点。 - hit-test 貫通:
hitTestAttribute(例"data-room-id")を渡すと、オーバーレイの隙間クリックをdocument.elementsFromPointで辿り、背景 SVG 要素の属性値と一致する id を select する(FloorMap 相当。オクルージョンを貫通)。 - テーマは全て
--spatial-*CSS 変数経由でイベント色を焼かない。既定描画を変えるならrenderHotspotレンダープロップ。debugで全ヒット形状を可視化。 data(unknown)に 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/schema の Session / Room の上に薄く構築し、型フォークを避ける。
TimetableTrack= グリッドの 1 列。通常はステージ(Room)だが、単一ステージのイベントでは「開催日」を列にする等、列の意味はイベントが決める。trackFromRoom(room)で Room → Track。toTimeslot(session, trackId?)…SessionをTimeslot(startMin/endMinは JST 壁時計分)に変換。trackId省略時はsession.roomId。packTimeslots(sessions, trackId?)… セッション群をtoTimeslot→assignLanesし、LanedTimeslot[](lane/lanes付き)を返す。アプリ層は日付でグルーピングしてから列 or 全体単位で呼ぶ。
spatial.config(config)
会場の空間データ(SVG + viewBox + ホットスポット / ブース座標)を宣言的に持つ型。SpatialConfig は overview(俯瞰 / 会場セレクタ)・floors(id → SpatialSvgSource)・boothGrids(id → BoothGrid)。SpatialSvgSource は svg / svgUrl / svgUrlEn / imageUrl + viewBox + hotspots。boothGridToHotspots(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-kitのexportPng(/capture。modern-screenshot 主 + html2canvas-pro fallback)へ渡し、実寸計測 +exportScale: 2で焼く。アプリ側に canvas コードは書かない。
エンジンは「ヒット判定つき SVG レイヤ」と「重なり → レーンの数式」「JST 壁時計」だけを提供し、レイアウト・ページ構成・書き出しはイベントごとに差し替え可能な形に保つ、という責務分担はこの実装でも変わらない。