Files
frontend-miniapp/docs/superpowers/plans/2026-06-29-sgs-hall-poi-display.md

27 KiB
Raw Blame History

SGS Hall POI Display Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Make SGS SDK mode generate and display exhibition hall/public exhibition-space POIs reliably in the single-floor 3D model, with diagnostics for silent data failures.

Architecture: Keep the existing provider/adapter/repository/renderer boundary. Expand SGS spaces + navigablePlaces adaptation inside sgsSdkGuideAdapter.ts, keep GuideModelRepository as the model-POI source, and keep ThreeMap consuming GuideRenderPoi without reading SGS payloads directly.

Tech Stack: Vue 3, uni-app H5, TypeScript 5.3, Three.js 0.163, pnpm, vue-tsc, ESLint.

Global Constraints

  • Default runtime target is H5; do not add mini-program/mp-weixin work.
  • Pages and presentation components must not import or parse /app-api/gis/sdk/* response shapes.
  • Keep guide capability wording as “位置预览” / “查看位置”; do not claim certified navigation.
  • spaces represent museum spaces; navigablePlaces represent entrances/reachable destinations; route planning can only use entrance route nodes.
  • Do not add dependencies.
  • Avoid unrelated refactors and broad data cleanup.
  • Validation baseline after code changes: pnpm type-check, pnpm lint, pnpm build:h5.

File Structure

  • Modify: src/data/adapters/sgsSdkGuideAdapter.ts

    • Owns SGS payload-to-domain conversion.
    • Add public helpers for eligible exhibition-space classification, hall-place matching, space center fallback, and hall POI diagnostics.
    • Keep raw SGS fields contained in this adapter.
  • Modify: src/repositories/GuideModelRepository.ts

    • Owns model render package and floor render POI loading.
    • Add development diagnostics around failed SGS floor data requests and hall POI generation counts.
    • Continue returning only GuideRenderPoi[] to ThreeMap.
  • Modify: src/components/map/ThreeMap.vue

    • Owns 3D presentation and POI visibility.
    • Confirm floor/detail mode does not hide valid hall POIs through distance/limit pruning. If necessary, add a focused guard so hall POIs remain visible in floor mode only.
  • Create: scripts/check-sgs-hall-poi-adapter.mjs

    • Node-based smoke script for the adapter algorithm using representative SGS payload fixtures.
    • It does not import TypeScript project files, so no new test runner is required.
    • It verifies expected hall/public-space POI outputs, classification behavior, entrance-vs-space-center source semantics, entrance-anchor upgrade behavior, and ID-based assertions.
  • Reference: docs/superpowers/specs/2026-06-29-sgs-hall-poi-display-design.md

    • Implementation must match this spec.

Task 1: Adapter classification and fallback smoke coverage

Files:

  • Create: scripts/check-sgs-hall-poi-adapter.mjs

Interfaces:

  • Consumes: No project imports. Uses local JS functions mirroring the intended adapter behavior.

  • Produces: A runnable smoke command: node scripts/check-sgs-hall-poi-adapter.mjs.

  • Step 1: Create the failing smoke script

Create scripts/check-sgs-hall-poi-adapter.mjs with this exact content:

const assert = require('node:assert/strict')

const exhibitionSpaceTypeWhitelist = new Set([
  'exhibition_hall',
  'theater',
  'education_activity',
  'ramp'
])

const exhibitionSpaceKeywords = [
  '展厅',
  '临展',
  '展览',
  '影院',
  '球幕',
  '巨幕',
  '报告厅',
  '活动室',
  '科普',
  '实验室',
  '展览坡道',
  '宇宙',
  '地球',
  '演化',
  '恐龙',
  '人类',
  '生物',
  '生态',
  '家园'
]

const normalizedText = (value) => (value || '').trim()
const normalizedHallName = (value) => normalizedText(value).replace(/[\s()【】\[\]_-]/g, '')
const hasExhibitionKeyword = (value) => exhibitionSpaceKeywords.some((keyword) => normalizedText(value).includes(keyword))

const isEligiblePublicExhibitionSpace = (space) => {
  const type = normalizedText(space.type).toLowerCase()
  const name = normalizedText(space.name)

  if (type === 'exhibition_hall') return true
  if (type === 'theater') return hasExhibitionKeyword(name)
  if (type === 'education_activity') return hasExhibitionKeyword(name)
  if (type === 'ramp') return name.includes('展览坡道')

  return false
}

const placeHallName = (place) => (
  normalizedText(place.ownerName)
  || normalizedText(place.name)
    .replace(/(?:主)?(?:出入口|入口|出口|门)\s*[A-Za-z0-9一二三四五六七八九十号-]*$/u, '')
    .trim()
  || normalizedText(place.name)
)

const isExhibitionHallNavigablePlace = (place) => {
  const searchableText = [
    place.name,
    place.ownerName,
    place.category,
    place.type,
    place.typeCode,
    place.typeName
  ]
    .map(normalizedText)
    .filter(Boolean)
    .join(' ')

  return hasExhibitionKeyword(searchableText)
}

const findMatchedHallSpace = (place, spaces) => {
  const placeName = normalizedHallName(placeHallName(place))
  if (!placeName) return undefined

  return spaces.find((space) => {
    const spaceName = normalizedHallName(space.name)
    return spaceName && (placeName.includes(spaceName) || spaceName.includes(placeName))
  })
}

const normalizePositionSource = (source) => {
  const x = Number(source?.x)
  const y = Number(source?.y)
  const z = Number(source?.z)
  if (![x, y, z].every(Number.isFinite)) return undefined
  return [x, y, z]
}

const normalizePlacePosition = (place) => normalizePositionSource(
  place.position
    ? { x: place.position.x, y: place.position.y ?? 0, z: place.position.z }
    : { x: place.x, y: place.y ?? 0, z: place.z }
)

const normalizeSpaceCenter = (space) => normalizePositionSource(space.center || {})

const createHallPoiId = (space, place, fallbackIndex) => {
  if (space?.id) return `hall-${space.id}`
  const ownerName = normalizedHallName(placeHallName(place))
  if (ownerName) return `hall-place-${ownerName}`
  return `hall-place-${place.id || fallbackIndex + 1}`
}

const toHallPois = (spaces, navigablePlaces, fallbackFloorId) => {
  const hallSpaces = spaces.filter(isEligiblePublicExhibitionSpace)
  const hallPlaces = navigablePlaces.filter(isExhibitionHallNavigablePlace)
  const halls = new Map()

  hallPlaces.forEach((place, index) => {
    const matchedSpace = findMatchedHallSpace(place, hallSpaces)
    const entrancePosition = normalizePlacePosition(place)
    const fallbackPosition = matchedSpace ? normalizeSpaceCenter(matchedSpace) : undefined
    const position = entrancePosition || fallbackPosition
    const poiId = createHallPoiId(matchedSpace, place, index)

    if (!position) return

    const existing = halls.get(poiId)
    if (existing) {
      existing.entrances.push({ name: place.name, positionGltf: entrancePosition })
      return
    }

    halls.set(poiId, {
      id: poiId,
      name: matchedSpace?.name || placeHallName(place),
      floorId: String(place.floorId || matchedSpace?.floorId || fallbackFloorId),
      primaryCategory: 'exhibition_hall',
      positionGltf: position,
      sourceConfidence: entrancePosition ? 'backend-sgs-sdk-hall-entrance' : 'backend-sgs-sdk-space-center',
      entrances: [{ name: place.name, positionGltf: entrancePosition }]
    })
  })

  hallSpaces.forEach((space, index) => {
    const poiId = `hall-${space.id}`
    if (halls.has(poiId)) return
    const position = normalizeSpaceCenter(space)
    if (!position) return
    halls.set(poiId, {
      id: poiId,
      name: space.name,
      floorId: String(space.floorId || fallbackFloorId),
      primaryCategory: 'exhibition_hall',
      positionGltf: position,
      sourceConfidence: 'backend-sgs-sdk-space-center',
      entrances: []
    })
  })

  return Array.from(halls.values())
}

const spaces = [
  { id: 1, name: '展厅1宇宙厅', type: 'exhibition_hall', floorId: 'B2', center: { x: 10, y: 0, z: 20 } },
  { id: 2, name: '巨幕影院', type: 'theater', floorId: 'L1', center: { x: 30, y: 0, z: 40 } },
  { id: 3, name: '博物馆之友活动室', type: 'education_activity', floorId: 'L4', center: { x: 50, y: 0, z: 60 } },
  { id: 4, name: '展览坡道', type: 'ramp', floorId: 'L1.5', center: { x: 70, y: 0, z: 80 } },
  { id: 5, name: '茶水间', type: 'service', floorId: 'L1', center: { x: 90, y: 0, z: 100 } }
]

const places = [
  { id: 101, name: '展厅1宇宙厅 出入口 1', ownerName: '展厅1宇宙厅', floorId: 'B2', position: { x: 11, y: 0, z: 21 }, nodeId: 'n101' },
  { id: 102, name: '巨幕影院 出入口 1', ownerName: '巨幕影院', floorId: 'L1', position: { x: 31, y: 0, z: 41 }, nodeId: 'n102' }
]

const result = toHallPois(spaces, places, 'B2')
const byName = new Map(result.map((poi) => [poi.name, poi]))

assert.equal(result.length, 4)
assert.deepEqual(byName.get('展厅1宇宙厅').positionGltf, [11, 0, 21])
assert.deepEqual(byName.get('巨幕影院').positionGltf, [31, 0, 41])
assert.deepEqual(byName.get('博物馆之友活动室').positionGltf, [50, 0, 60])
assert.deepEqual(byName.get('展览坡道').positionGltf, [70, 0, 80])
assert.equal(byName.has('茶水间'), false)
assert.equal(byName.get('博物馆之友活动室').sourceConfidence, 'backend-sgs-sdk-space-center')

console.log('SGS hall POI adapter smoke check passed')
  • Step 2: Run the smoke script and verify current intended behavior is captured

Run:

node scripts/check-sgs-hall-poi-adapter.mjs

Expected output:

SGS hall POI adapter smoke check passed
  • Step 3: Commit the smoke coverage
git add scripts/check-sgs-hall-poi-adapter.mjs docs/superpowers/specs/2026-06-29-sgs-hall-poi-display-design.md
git commit -m "test: cover SGS hall POI display rules"

If the working tree already contains unrelated user changes, commit only the two listed files.


Task 2: Extend SGS hall-space adapter behavior

Files:

  • Modify: src/data/adapters/sgsSdkGuideAdapter.ts:122-428
  • Test: scripts/check-sgs-hall-poi-adapter.mjs

Interfaces:

  • Consumes: SgsSpacePayload.center?: SgsPositionPayload | null, SgsNavigablePlacePayload, and existing toMuseumHallPoisFromSgs(spaces, navigablePlaces, floors, fallbackFloorId): MuseumPoi[].

  • Produces:

    • isExhibitionHallSpace(space: SgsSpacePayload): boolean now returns true for public exhibition spaces.
    • toMuseumHallPoisFromSgs(...) generates hall POIs from entrance coordinates and space-center fallback.
    • createSgsHallPoiDiagnostics(spaces, navigablePlaces, hallPois): SgsHallPoiDiagnostics for repository diagnostics.
  • Step 1: Add classification constants and diagnostics types

In src/data/adapters/sgsSdkGuideAdapter.ts, replace the current exhibitionHallKeywords block at lines around 263-274 with:

const exhibitionSpaceTypeWhitelist = new Set([
  'exhibition_hall',
  'theater',
  'education_activity',
  'ramp'
])

const exhibitionHallKeywords = [
  '展厅',
  '临展',
  '展览',
  '影院',
  '球幕',
  '巨幕',
  '报告厅',
  '活动室',
  '科普',
  '实验室',
  '展览坡道',
  '宇宙',
  '地球',
  '演化',
  '恐龙',
  '人类',
  '生物',
  '生态',
  '家园'
]

export interface SgsHallPoiDiagnostics {
  spaceCount: number
  eligibleSpaceCount: number
  navigablePlaceCount: number
  hallPlaceCount: number
  hallPoiCount: number
  hallPoiWithPositionCount: number
  skippedSpaceCount: number
  skippedPlaceCount: number
}
  • Step 2: Add keyword and center helpers

Immediately after the constants from Step 1, add:

const hasExhibitionKeyword = (value?: string | null) => (
  exhibitionHallKeywords.some((keyword) => normalizedText(value).includes(keyword))
)

const searchableSpaceText = (space: SgsSpacePayload) => [
  space.name,
  space.type,
  space.sourceNodeName
]
  .map((value) => normalizedText(value))
  .filter(Boolean)
  .join(' ')

const normalizeSpaceCenter = (space: SgsSpacePayload): [number, number, number] | undefined => normalizePositionSource(
  space.center || {
    x: undefined,
    y: undefined,
    z: undefined
  }
)
  • Step 3: Replace isExhibitionHallSpace implementation

Replace the existing function:

export const isExhibitionHallSpace = (space: SgsSpacePayload) => (
  normalizedText(space.type).toLowerCase() === 'exhibition_hall'
)

with:

export const isExhibitionHallSpace = (space: SgsSpacePayload) => {
  const type = normalizedText(space.type).toLowerCase()
  const name = normalizedText(space.name)
  const searchableText = searchableSpaceText(space)

  if (!exhibitionSpaceTypeWhitelist.has(type)) return false
  if (type === 'exhibition_hall') return true
  if (type === 'ramp') return name.includes('展览坡道')

  return hasExhibitionKeyword(searchableText)
}

This keeps non-public service spaces such as 茶水间 out of hall POIs.

  • Step 4: Update isExhibitionHallNavigablePlace to use the shared keyword helper

Inside isExhibitionHallNavigablePlace, replace:

return exhibitionHallKeywords.some((keyword) => searchableText.includes(keyword))

with:

return hasExhibitionKeyword(searchableText)
  • Step 5: Add space fallback hall POI helper

After createHallPoiId(...), add:

const createSpaceFallbackHallPoi = (
  space: SgsSpacePayload,
  floors: SgsSdkFloorSummaryPayload[],
  fallbackFloorId: string
): MuseumPoi | null => {
  const floorId = stringifyId(space.floorId) || fallbackFloorId
  const position = normalizeSpaceCenter(space)
  const hallId = stringifyId(space.id)

  if (!hallId || !normalizedText(space.name) || !position) return null

  return {
    id: `hall-${hallId}`,
    name: normalizedText(space.name),
    floorId,
    floorLabel: floorLabelForSgs(floorId, floors),
    primaryCategory: hallCategory,
    categories: [
      hallCategory
    ],
    positionGltf: position,
    sourceObjectName: space.sourceNodeName || undefined,
    sourceConfidence: 'backend-sgs-sdk-space-center',
    navigationReadiness: '位置预览',
    accessible: false,
    kind: 'hall',
    hallId,
    hallName: normalizedText(space.name),
    spaceId: hallId,
    sourceSpaceId: hallId,
    entrances: []
  }
}
  • Step 6: Update toMuseumHallPoisFromSgs to use fallback positions and space-only POIs

Inside toMuseumHallPoisFromSgs, keep the existing function signature. Replace the body with:

  const hallSpaces = spaces.filter(isExhibitionHallSpace)
  const hallPlaces = navigablePlaces.filter(isExhibitionHallNavigablePlace)
  const halls = new Map<string, MuseumPoi>()

  hallPlaces.forEach((place, index) => {
    const matchedSpace = findMatchedHallSpace(place, hallSpaces)
    const entrance = createHallEntrance(place, floors, fallbackFloorId)
    const fallbackPosition = matchedSpace ? normalizeSpaceCenter(matchedSpace) : undefined
    const floorId = entrance.floorId || stringifyId(matchedSpace?.floorId) || fallbackFloorId
    const hallId = stringifyId(matchedSpace?.id) || undefined
    const poiId = createHallPoiId(matchedSpace, place, index)
    const hallName = normalizedText(matchedSpace?.name) || placeHallName(place) || entrance.name
    const existing = halls.get(poiId)

    if (existing) {
      existing.entrances = [
        ...(existing.entrances || []),
        entrance
      ]
      if (!existing.positionGltf && (entrance.positionGltf || fallbackPosition)) {
        existing.positionGltf = entrance.positionGltf || fallbackPosition
        existing.floorId = floorId
        existing.floorLabel = entrance.floorLabel
        existing.sourceObjectName = entrance.sourceObjectName || matchedSpace?.sourceNodeName || undefined
        existing.sourcePlaceId = entrance.sourcePlaceId
      }
      return
    }

    const positionGltf = entrance.positionGltf || fallbackPosition
    if (!positionGltf) return

    halls.set(poiId, {
      id: poiId,
      name: hallName,
      floorId,
      floorLabel: entrance.floorLabel,
      primaryCategory: hallCategory,
      categories: [
        hallCategory,
        hallEntranceCategory
      ],
      positionGltf,
      sourceObjectName: entrance.sourceObjectName || matchedSpace?.sourceNodeName || undefined,
      sourceConfidence: entrance.positionGltf
        ? 'backend-sgs-sdk-hall-entrance'
        : 'backend-sgs-sdk-space-center',
      navigationReadiness: '位置预览',
      accessible: false,
      kind: 'hall',
      hallId,
      hallName,
      spaceId: hallId,
      sourcePlaceId: entrance.sourcePlaceId,
      sourceSpaceId: hallId,
      entrances: [entrance]
    })
  })

  hallSpaces.forEach((space) => {
    const hallId = stringifyId(space.id)
    if (!hallId || halls.has(`hall-${hallId}`)) return

    const fallbackPoi = createSpaceFallbackHallPoi(space, floors, fallbackFloorId)
    if (fallbackPoi) {
      halls.set(fallbackPoi.id, fallbackPoi)
    }
  })

  return Array.from(halls.values())
    .filter((poi) => poi.id && poi.name && poi.positionGltf)
  • Step 7: Add diagnostics helper

After toMuseumHallPoisFromSgs, add:

export const createSgsHallPoiDiagnostics = (
  spaces: SgsSpacePayload[],
  navigablePlaces: SgsNavigablePlacePayload[],
  hallPois: MuseumPoi[]
): SgsHallPoiDiagnostics => {
  const eligibleSpaceCount = spaces.filter(isExhibitionHallSpace).length
  const hallPlaceCount = navigablePlaces.filter(isExhibitionHallNavigablePlace).length
  const hallPoiWithPositionCount = hallPois.filter((poi) => Boolean(poi.positionGltf)).length

  return {
    spaceCount: spaces.length,
    eligibleSpaceCount,
    navigablePlaceCount: navigablePlaces.length,
    hallPlaceCount,
    hallPoiCount: hallPois.length,
    hallPoiWithPositionCount,
    skippedSpaceCount: Math.max(eligibleSpaceCount - hallPois.length, 0),
    skippedPlaceCount: Math.max(hallPlaceCount - hallPois.reduce((count, poi) => count + (poi.entrances?.length || 0), 0), 0)
  }
}
  • Step 8: Run the adapter smoke script

Run:

node scripts/check-sgs-hall-poi-adapter.mjs

Expected output:

SGS hall POI adapter smoke check passed
  • Step 9: Run type-check for adapter changes

Run:

pnpm type-check

Expected: exits 0. If it fails, fix TypeScript errors in sgsSdkGuideAdapter.ts only before continuing.

  • Step 10: Commit adapter behavior
git add src/data/adapters/sgsSdkGuideAdapter.ts scripts/check-sgs-hall-poi-adapter.mjs
git commit -m "feat: expand SGS hall POI adaptation"

Task 3: Add SGS model POI diagnostics in repository

Files:

  • Modify: src/repositories/GuideModelRepository.ts:14-244
  • Test: scripts/check-sgs-hall-poi-adapter.mjs

Interfaces:

  • Consumes: createSgsHallPoiDiagnostics(spaces, navigablePlaces, hallPois) from sgsSdkGuideAdapter.ts.

  • Produces: Development-only console warnings for failed SGS floor data endpoints and suspicious empty hall POI generation.

  • Step 1: Update imports

In src/repositories/GuideModelRepository.ts, replace:

import {
  formatSgsFloorLabel,
  toMuseumHallPoisFromSgs,
  toMuseumPoiFromSgs
} from '@/data/adapters/sgsSdkGuideAdapter'

with:

import {
  createSgsHallPoiDiagnostics,
  formatSgsFloorLabel,
  toMuseumHallPoisFromSgs,
  toMuseumPoiFromSgs
} from '@/data/adapters/sgsSdkGuideAdapter'
  • Step 2: Add development warning helper

After isSharedSgsModelAsset(...), add:

const warnSgsGuideModelDiagnostics = (
  message: string,
  payload: Record<string, unknown>
) => {
  if (!import.meta.env.DEV) return
  console.warn(`[SGS guide model] ${message}`, payload)
}
  • Step 3: Add safe request helper inside loadFloorPois

Inside SgsSdkGuideModelRepository.loadFloorPois, immediately after:

    const floorId = String(matchedFloor.floorId)

add this local helper:

    const loadFloorData = async <T>(
      endpoint: 'pois' | 'spaces' | 'navigablePlaces',
      loader: () => Promise<T[]>
    ) => {
      try {
        return await loader()
      } catch (error) {
        warnSgsGuideModelDiagnostics('floor data request failed', {
          floorId,
          endpoint,
          error: error instanceof Error ? error.message : String(error)
        })
        return []
      }
    }
  • Step 4: Replace silent catch requests

Replace:

    const [pois, spaces, navigablePlaces] = await Promise.all([
      this.provider.getFloorPois(floorId).catch(() => []),
      this.provider.getFloorSpaces(floorId).catch(() => []),
      this.provider.getNavigablePlaces(floorId).catch(() => [])
    ])

with:

    const [pois, spaces, navigablePlaces] = await Promise.all([
      loadFloorData('pois', () => this.provider.getFloorPois(floorId)),
      loadFloorData('spaces', () => this.provider.getFloorSpaces(floorId)),
      loadFloorData('navigablePlaces', () => this.provider.getNavigablePlaces(floorId))
    ])
  • Step 5: Add hall generation diagnostics

Replace:

    const ordinaryPois = pois.map((poi) => toSgsRenderPoi(toMuseumPoiFromSgs(poi, manifest.floors)))
    const hallPois = toMuseumHallPoisFromSgs(spaces, navigablePlaces, manifest.floors, floorId)
      .map(toSgsRenderPoi)

    return dedupeRenderPoisById([
      ...hallPois,
      ...ordinaryPois
    ])
      .filter((poi) => Array.isArray(poi.positionGltf) && poi.positionGltf.length === 3)

with:

    const ordinaryPois = pois.map((poi) => toSgsRenderPoi(toMuseumPoiFromSgs(poi, manifest.floors)))
    const museumHallPois = toMuseumHallPoisFromSgs(spaces, navigablePlaces, manifest.floors, floorId)
    const hallDiagnostics = createSgsHallPoiDiagnostics(spaces, navigablePlaces, museumHallPois)

    if (hallDiagnostics.eligibleSpaceCount > 0 && hallDiagnostics.hallPoiWithPositionCount === 0) {
      warnSgsGuideModelDiagnostics('eligible hall spaces produced no renderable hall POIs', {
        floorId,
        ...hallDiagnostics
      })
    }

    const hallPois = museumHallPois.map(toSgsRenderPoi)
    const renderPois = dedupeRenderPoisById([
      ...hallPois,
      ...ordinaryPois
    ])
      .filter((poi) => Array.isArray(poi.positionGltf) && poi.positionGltf.length === 3)

    if (hallDiagnostics.hallPoiWithPositionCount > 0 && !renderPois.some((poi) => poi.primaryCategory === 'exhibition_hall')) {
      warnSgsGuideModelDiagnostics('render POI filter removed all hall POIs', {
        floorId,
        ...hallDiagnostics,
        renderPoiCount: renderPois.length
      })
    }

    return renderPois
  • Step 6: Run smoke and type-check

Run:

node scripts/check-sgs-hall-poi-adapter.mjs
pnpm type-check

Expected:

SGS hall POI adapter smoke check passed

and pnpm type-check exits 0.

  • Step 7: Commit repository diagnostics
git add src/repositories/GuideModelRepository.ts src/data/adapters/sgsSdkGuideAdapter.ts scripts/check-sgs-hall-poi-adapter.mjs
git commit -m "chore: expose SGS hall POI diagnostics"

Task 4: Confirm floor-mode hall POI visibility

Files:

  • Modify: src/components/map/ThreeMap.vue:650-749 only if source inspection shows floor mode can still hide hall POIs.
  • Test: scripts/check-sgs-hall-poi-adapter.mjs

Interfaces:

  • Consumes: RenderPoi.primaryCategory === 'exhibition_hall' / 'exhibition_hall_entrance'.

  • Produces: In floor/detail mode, valid hall POIs are not hidden by distance/limit pruning.

  • Step 1: Inspect current floor visibility behavior

Open src/components/map/ThreeMap.vue around getPoiVisibilityTier and confirm this current behavior exists:

const getPoiVisibilityTier = (): PoiVisibilityTier => {
  if (activeView.value === 'floor') return 'full'
  if (!controls || !activeModel) return 'full'

If this is still present, no code change is required for floor-mode pruning because full makes getPoiVisibilityLimit() return infinity and getPoiScreenSpacing() return 0.

  • Step 2: Only if floor mode no longer returns full, add hall visibility guard

If Step 1 shows floor mode can be tight or balanced, update shouldShowPoiAtDistance to begin with:

  if (activeView.value === 'floor' && (
    poi.primaryCategory === 'exhibition_hall'
    || poi.primaryCategory === 'exhibition_hall_entrance'
  )) {
    return true
  }

Do not add this guard if activeView.value === 'floor' already returns full.

  • Step 3: Run type-check

Run:

pnpm type-check

Expected: exits 0.

  • Step 4: Commit visibility confirmation or change

If no code change was needed, do not create an empty commit. Record the finding in the final implementation summary.

If code changed, run:

git add src/components/map/ThreeMap.vue
git commit -m "fix: keep hall POIs visible in floor mode"

Task 5: Final validation and handoff

Files:

  • Modify: none unless validation reveals a failure in files changed by Tasks 2-4.
  • Test: whole changed scope.

Interfaces:

  • Consumes: all previous task outputs.

  • Produces: Verified working tree state and final summary.

  • Step 1: Run adapter smoke check

node scripts/check-sgs-hall-poi-adapter.mjs

Expected:

SGS hall POI adapter smoke check passed
  • Step 2: Run type-check
pnpm type-check

Expected: exits 0.

  • Step 3: Run lint
pnpm lint

Expected: exits 0. If warnings appear with exit 0, record them in the final summary.

  • Step 4: Run H5 build

This writes dist, which is expected for build validation.

pnpm build:h5

Expected: exits 0.

  • Step 5: Inspect changed files
git diff -- src/data/adapters/sgsSdkGuideAdapter.ts src/repositories/GuideModelRepository.ts src/components/map/ThreeMap.vue scripts/check-sgs-hall-poi-adapter.mjs docs/superpowers/specs/2026-06-29-sgs-hall-poi-display-design.md

Expected: diff is limited to the planned files and contains no unrelated refactor.

  • Step 6: Commit final validation notes if a docs update was needed

If no docs changed after validation, do not commit. If validation required updating this plan or spec, commit only those docs:

git add docs/superpowers/specs/2026-06-29-sgs-hall-poi-display-design.md docs/superpowers/plans/2026-06-29-sgs-hall-poi-display.md
git commit -m "docs: record SGS hall POI validation"
  • Step 7: Final response

Report:

  • Files changed.
  • How hall/public exhibition spaces are now classified.
  • Whether ThreeMap required a change.
  • Exact validation commands and outcomes.
  • Any skipped validation and why.

Self-Review

Spec coverage

  • Expanded exhibition_hall-only classification: Task 2.
  • Added theater / education_activity / ramp handling: Task 2.
  • Added entrance-first and space-center fallback: Task 2.
  • Added diagnostics for silent request/adaptation failure: Task 3.
  • Preserved ThreeMap boundary and floor-mode visibility: Task 4.
  • Validation commands and H5 build: Task 5.

Placeholder scan

This plan contains no TBD/TODO placeholders and every code-changing step includes exact code or exact conditional instructions.

Type consistency

  • createSgsHallPoiDiagnostics is defined in Task 2 and imported in Task 3 with the same name.
  • SgsHallPoiDiagnostics fields match Task 3 diagnostic spreading.
  • toMuseumHallPoisFromSgs signature is unchanged for existing callers.