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

903 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```js
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:
```powershell
node scripts/check-sgs-hall-poi-adapter.mjs
```
Expected output:
```text
SGS hall POI adapter smoke check passed
```
- [ ] **Step 3: Commit the smoke coverage**
```powershell
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:
```ts
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:
```ts
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:
```ts
export const isExhibitionHallSpace = (space: SgsSpacePayload) => (
normalizedText(space.type).toLowerCase() === 'exhibition_hall'
)
```
with:
```ts
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:
```ts
return exhibitionHallKeywords.some((keyword) => searchableText.includes(keyword))
```
with:
```ts
return hasExhibitionKeyword(searchableText)
```
- [ ] **Step 5: Add space fallback hall POI helper**
After `createHallPoiId(...)`, add:
```ts
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:
```ts
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:
```ts
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:
```powershell
node scripts/check-sgs-hall-poi-adapter.mjs
```
Expected output:
```text
SGS hall POI adapter smoke check passed
```
- [ ] **Step 9: Run type-check for adapter changes**
Run:
```powershell
pnpm type-check
```
Expected: exits 0. If it fails, fix TypeScript errors in `sgsSdkGuideAdapter.ts` only before continuing.
- [ ] **Step 10: Commit adapter behavior**
```powershell
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:
```ts
import {
formatSgsFloorLabel,
toMuseumHallPoisFromSgs,
toMuseumPoiFromSgs
} from '@/data/adapters/sgsSdkGuideAdapter'
```
with:
```ts
import {
createSgsHallPoiDiagnostics,
formatSgsFloorLabel,
toMuseumHallPoisFromSgs,
toMuseumPoiFromSgs
} from '@/data/adapters/sgsSdkGuideAdapter'
```
- [ ] **Step 2: Add development warning helper**
After `isSharedSgsModelAsset(...)`, add:
```ts
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:
```ts
const floorId = String(matchedFloor.floorId)
```
add this local helper:
```ts
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:
```ts
const [pois, spaces, navigablePlaces] = await Promise.all([
this.provider.getFloorPois(floorId).catch(() => []),
this.provider.getFloorSpaces(floorId).catch(() => []),
this.provider.getNavigablePlaces(floorId).catch(() => [])
])
```
with:
```ts
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:
```ts
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:
```ts
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:
```powershell
node scripts/check-sgs-hall-poi-adapter.mjs
pnpm type-check
```
Expected:
```text
SGS hall POI adapter smoke check passed
```
and `pnpm type-check` exits 0.
- [ ] **Step 7: Commit repository diagnostics**
```powershell
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:
```ts
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:
```ts
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:
```powershell
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:
```powershell
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**
```powershell
node scripts/check-sgs-hall-poi-adapter.mjs
```
Expected:
```text
SGS hall POI adapter smoke check passed
```
- [ ] **Step 2: Run type-check**
```powershell
pnpm type-check
```
Expected: exits 0.
- [ ] **Step 3: Run lint**
```powershell
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.
```powershell
pnpm build:h5
```
Expected: exits 0.
- [ ] **Step 5: Inspect changed files**
```powershell
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:
```powershell
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.