# 編者與前端工程選讀

本篇保存實作原理與工程檢查，供編者及想深入的讀者選讀。初學者請先閱讀「短指令跟做教材」，不需要先理解本篇參數或照寫工程提示詞。

先開啟原始參考網站：[HEART 政策島](https://puma.taipei/heart-island)。本篇接續「從企劃、資料到 3D 互動小島」的入門影片，練習把資料變成一座可旋轉、可點選、可切換日夜的模型城市。

使用者提供的第三方「創島提示詞」是本篇的需求參考；原貼文作者與網址尚未提供。六角座標、路網、小人與日夜等描述，在本篇都是獨立範例的實作選擇，不能據此推定原始網站的內部架構。

## 先看這次可操作的結果

開啟上方「進階可操作範例」。範例有 12 個主題、48 筆虛構提案、12 種程序模型，以及 8 條示意關係。它以原生 ES Modules 載入 Three.js 0.160.0，幾何與地面貼圖由程式產生，沒有外部圖片、模型或字型檔。

- 點「文化」島或搜尋「典藏」，選擇「開放典藏工坊」；清單、卡片與建物應指向同一筆資料。
- 切換「06 清晨」與「21 夜晚」，觀察天空、水面、霧與窗戶發光。
- 開啟「協作線」，看路徑如何經過步道與橋。pending 關係保留靜態虛線，不派送搬運角色。
- 點「送入一個示意任務」，觀察選取項目的狀態、高亮與任務日誌；再按一次完成該任務。
- 點「暫停動態」，角色、水面、雲與自動時間應停止。手動選取與時間按鈕仍可使用。

**重要界線：** 搬運角色與任務事件都標為教學示意，此頁沒有接上 AgentArk 或其他真實後端。內容數字不是政策統計；official／thematic／pending 在此只用來示範線型，並不表示虛構資料有真實官方認證。

## 實作檔案與版本

入門範例使用 Three.js 0.186.1；本篇獨立範例鎖定 0.160.0，配合第三方提示詞的版本。不直接替換入門專案依賴。

上方提供單一 HTML、進階教材與含測試的分檔原始碼 ZIP。解壓 ZIP 後，依 README 執行，不需要安裝 npm 依賴。來源採分檔管理，建置後產生可下載的單一 HTML。修改時依以下責任分工閱讀：

- `advanced/src/data.mjs`：DATA，保存主題、細項、模型類型、關係、日間活動與固定種子。
- `advanced/src/core.mjs`：六角座標、步道與橋路網、Dijkstra、instance 映射、事件歸併。
- `advanced/src/kit.mjs`：幾何零件庫、12 種模型、批次實例化與原始顏色。
- `advanced/src/world.mjs`：場景、相機、天空、水、光、地形、角色與拾取。
- `advanced/src/app.mjs`：清單、卡片、搜尋、時間、暫停、錯誤備援與事件入口。
- `advanced/src/style.css`：桌面版與手機面板。

在教學庫根目錄執行：

```sh
node --test courses/public-learning-island/advanced/tests/core.test.mjs
node scripts/build-island-advanced.mjs
npm run build
npm run preview
```

預覽網址是 `http://127.0.0.1:4173/courses/public-learning-island/advanced/`。單一 HTML 仍需要網路下載鎖版 Three.js；建議用 HTTP 預覽。CDN 載入失敗時，文字探索應繼續可用。

## 第 1 階段：六角島、相機、光與橋

**前置成果：** 一份 DATA，至少三座相鄰島，並保留每座島的穩定 ID 與 axial 座標 q、r。

pointy-top 六角形的座標換算如下。SPACING 是公式中的尺寸參數，不是兩島中心距離。

```js
const R = 2.02;
const SPACING = 2.28;
function axialToWorld(q, r) {
  return {
    x: Math.sqrt(3) * SPACING * (q + r / 2),
    z: 1.5 * SPACING * r
  };
}
// 相鄰中心距離：sqrt(3) × 2.28 ≈ 3.9491
// 平行邊之間水道：sqrt(3) × (2.28 - 2.02) ≈ 0.4503
```

島面用六角 CircleGeometry；側壁的朝向必須與島面一致，不能只看正上方就認定沒有重疊。步道沿六角形周圍配置頂點與邊中點，共 12 個節點。相鄰島以 axial 距離 1 判斷，橋連到彼此朝向的邊中點，每座橋拆成 9 片甲板，用 sin 曲線形成小拱。

相機採透視相機與窄視角，搭配 OrbitControls；選取時同時插值 camera.position 與 controls.target。使用者開始拖曳就取消飛行，避免程式與滑鼠搶控制。鳥瞰時要一併調整 polar angle 下限，否則控制器會把相機拉回原限制。

### 可複製提示詞 1

```text
請建立原生 ES Modules 的單一 HTML 教學範例，Three.js 固定 0.160.0。
先用 DATA 放三座相鄰的虛構六角島，R=2.02、SPACING=2.28、TOP=0.65。
依 axial 公式計算位置，建立島面、側壁、步道與相鄰島之間的 9 片拱橋。
加入相機、OrbitControls、光線和可點選的島名標籤。
載入畫面與文字清單先出現，再在 try/catch 內 await import Three.js 與 addons。
選取島時同步移動相機與 target；使用者拖曳立即取消相機飛行。
本階段不要加入人流與複雜模型。交付可直接預覽的 index.html。
```

**輸出與驗收：** `index.html` 能以 HTTP 開啟，三島不相交，水道約 0.45；橋沒有懸空。拖曳、縮放、重設與島名按鈕可用，窄螢幕仍看得到島嶼。刻意阻斷 CDN，應看到載入錯誤與可讀清單。

## 第 2 階段：程序建物零件庫

**前置成果：** 第 1 階段可正常執行的 HTML，以及每筆細項的 `model` 欄位。

黏土城市的辨識度來自小型場景的組合：牆、屋頂、窗、桌椅、輪子與植栽。先建立固定幾何，再把位置、旋轉、縮放與顏色記成零件資料。模型函式只負責排零件。

```js
// 概念節錄；完整版本見 kit.mjs
const parts = [];
function addPart(kind, matrix, color, itemId) {
  parts.push({ kind, matrix, color, itemId });
}
const factories = { house, hall, school, clinic, bus, bike,
  court, garden, stage, book, clock, globe };
factories[item.model]();
// 最後依 geometry／material 相容性分批建立 InstancedMesh。
```

本範例的 12 種模型由 DATA 指定，包含小屋、塔樓、教室、診所、公車站、腳踏車、球場、花圃、舞台、書、時鐘與地球儀。相同 geometry 重複使用；實例顏色保存原始值，選取與時段高亮都從原色重新計算，避免越點越紅。

固定種子要用在幾何抖動、植栽位置與程序地面紋理。本版 SimplexNoise 接收的是有 random 方法的物件：

```js
const random = seeded(20261023);
const noise = new SimplexNoise({ random });
```

### 可複製提示詞 2

```text
延續已可執行的第一階段，保留資料 ID、相機與橋。
加入零件庫，將 kind、matrix、color、itemId 收集後依幾何與材質分批實例化。
做 12 種小型模型，由 DATA.items[].model 決定，不以主題名稱寫死模型。
加入原始顏色表與 instance→itemId 反查表。
所有程序亂數用同一固定種子；SimplexNoise 使用 { random: seeded(seed) }。
加入少量植栽與街道家具，鏡頭拉遠時隱藏細節。
輸出更新後的 index.html，並列出本階段實測 draw calls 與 triangles。
```

**輸出與驗收：** 重新載入兩次，幾何位置相同。把一筆 model 改成 book，只改該筆視覺表示。48 筆細項皆有對應模型；選取一棟不會讓同批所有建物一起變色。20–30 calls 只能是特定批次的設計目標，完整場景數字必須實測。

## 第 3 階段：點選、卡片與手機介面

**前置成果：** 零件批次，以及由資料 ID 驅動的清單。

InstancedMesh 的一次命中會提供 `object` 與 `instanceId`；同一個 item 可能由不同批次的多個零件組成。先反查內容 ID，再讓清單、卡片、相機與高亮走同一個選取流程。

```js
const hit = raycaster.intersectObjects(pickMeshes)[0];
const itemId = instanceToItem[hit.object.name][hit.instanceId];
select('item', itemId);
// 更新批次後：
mesh.instanceColor.needsUpdate = true;
mesh.instanceMatrix.needsUpdate = true;
```

指標座標必須依 canvas 的 bounding rect 換算；pointerdown 與 pointerup 位移超過 6px 視為拖曳。裝飾零件不映射內容 ID，讓拾取繼續檢查下一個命中。搜尋後被隱藏的模型，也不能繼續被選取。

手機清單放在底部面板，本版提供高度調整滑桿；單一按鈕最小可操作區域要夠大。文字清單保留鍵盤入口。CSS2DRenderer 官方只保證 100% 瀏覽器及顯示縮放，因此要另外驗收 200% 縮放，並維持清單可用。

### 可複製提示詞 3

```text
延續第二階段，加入共用 selectedId 狀態、搜尋、主題清單與詳情卡片。
Raycaster 命中後依 mesh＋instanceId 找到 itemId；整棟所有零件一起高亮。
清單按鈕與 3D 點擊共用 select，搜尋結果和場景同步。
拖曳超過 6px 不當成點擊。保留 Tab／Enter 選取與 Esc 關閉。
手機將清單改成可調整高度的底部面板，避免遮住所有 3D 內容。
交付 index.html，列出滑鼠、鍵盤、375px 的實際檢查步驟。
```

**輸出與驗收：** 搜尋典藏，選取「開放典藏工坊」；清單、卡片與 3D 指向同一筆。實際用滑鼠點模型，再用鍵盤選同一筆，結果應一致。拖曳不開卡片，手機沒有水平捲動。

## 第 4 階段：日夜、水面、路網與角色

**前置成果：** 可用的選取介面、幾何零件庫及六角步道。

日夜是一組可插值的場景參數。天空、horizon、霧、水色與光源一起更新；霧色跟 horizon 一致，避免遠景出現突兀色帶。夜晚用較強的冷色環境光保留可讀性，窗戶 emissive 隨 night 變化。水面 Shader 用兩組正弦波做色彩漂移，並支援霧與輸出色彩轉換。

角色行走必須沿可通行路網。每島 12 個節點連成環，橋加入跨島邊，Dijkstra 求最短路。依累積路程計算位置，不把角色沿兩島中心直線移動。本範例的小人以頭、髮、身體和兩腿五個動態批次繪製；到站交接紀錄與角色路程一致。

**關係與路徑分開：** 協作資料說明兩個主題的關係，路網只回答如何移動。pending 線可作為靜態待確認示意，不派出搬運角色。畫面所有搬運仍是教學動畫，不能拿來當真實流量或任務成效。

### 可複製提示詞 4

```text
保留第三階段功能，加入可手動切換的清晨、白天、黃昏、夜晚。
以 keyframes 插值天空、水色、霧與光線，night 控制窗戶 emissive。
步道建立每島 12 節點環；相鄰島的橋接到邊中點，Dijkstra 尋路。
角色依弧長沿路線往返，到站停留後交接，日誌標示「示意搬運」。
pending 關係用靜態虛線說明，不產生搬運角色。
暫停與 prefers-reduced-motion 必須讓時間、角色、水面、雲與自動日誌停止；
手動操作仍可使用。交付 index.html 與暫停前後的觀察結果。
```

**輸出與驗收：** 夜晚文字和建物仍可辨識。角色過橋，不穿海；到站交接與日誌相符。暫停數秒後，時間、角色及日誌不增加；在系統減少動態偏好變更後，立即停止動態，選取相機直接定位。

## 第 5 階段：接真實事件，並測完整場景

**前置成果：** 前四階段穩定可操作，且已定義真實系統能提供什麼事件。

若要顯示 Agent 或工作系統的實際運作，另接事件契約。不要依角色走到目的地就推定任務完成。本範例提供 `receiveIslandEvent(event)` 入口與本機示意按鈕，未設定任何外部連線。

```json
{
  "eventId": "evt-1024",
  "agentId": "worker-a",
  "taskId": "job-42",
  "itemId": "item-26",
  "seq": 2,
  "status": "completed",
  "occurredAt": "2026-10-23T08:00:00Z",
  "source": "external"
}
```

資料入口先檢查已知 item、合法狀態、有效時間與遞增序號；相同 eventId、過期序號與非法內容不更新畫面。本版序號以每個 item 為範圍，正式後端應由伺服器提供該 item 的單調版本，避免不同任務各自從 1 開始互相覆寫。agentId、taskId 保留為追溯資訊。

```js
// 後端適配器收到已驗證事件後才呼叫；此段不代表已連線。
receiveIslandEvent(event);
// 可選用 SSE／WebSocket／輪詢；先定義斷線、重連與快照恢復。
// 不把 token 寫進公開 HTML，瀏覽器僅接收有權限閱讀的資料。
```

本版 seen 僅保留最近 1,000 個 eventId，日誌保留 40 筆，屬於有界的前端顯示邏輯。正式系統的長期去重、儲存、權限與事件真實性由後端負責。選到過濾結果以外的項目時，需先定義是否顯示提示或清除選取。

### 可複製提示詞 5

```text
保留既有場景與原始資料，加入獨立 receiveIslandEvent 入口。
事件有 eventId、agentId、taskId、itemId、seq、status、occurredAt、source。
同一 item 用伺服器單調 seq；重複、過期、未知 item 與非法狀態不更新。
先用明示「本機示意」的按鈕驗證狀態、高亮、卡片與日誌同步；
沒有後端資料時，不顯示已連線或真實 LIVE。
量測完整場景 calls、triangles、首次可操作與互動幀時間；
檢查 375px、200% 縮放、鍵盤、真實觸控、減少動態與載入失敗。
輸出 index.html、事件契約，以及已測／未測的驗收紀錄。
```

**輸出與驗收：** 同一事件送兩次，日誌不增加；較小 seq 不能把 completed 改回 running；合法新事件能更新卡片與建物。WebGL 或 CDN 不可用時，搜尋、文字與示意任務仍能操作。實測呼叫數不與幾何批次數混用，另外記錄陰影與背景物件的成本。

## 範例的範圍與後續擴充

本次提供五個階段的可閱讀教材與可執行整合範例。範例選擇基本的五零件角色、少量背景植栽、線條協作路徑及手機高度滑桿，方便讀者看清楚資料與工程結構。

第三方提示詞中的完整職業配件、頭頂 badge、每幀標籤避讓、三圈遠山、流動 Tube Shader、中英雙語與大數量背景樹，可在上述閉環穩定後逐項擴充。它們不應被記錄成這份教學已交付的功能。完整驗收結果以實際執行紀錄為準。

2026-10-22 舊版影片是資料與互動入門；本次簡易跟做 v2 改用短指令主線，並示範日夜與角色效果。此處保留較細的工程原理供選讀，不是初學者必須照寫的規格。

## 原始來源與 API 依據

- [HEART 政策島](https://puma.taipei/heart-island)：原始視覺與互動參考。
- 使用者提供的第三方「創島提示詞」：2026-10-23 收到；原貼文作者與網址未提供。
- [Three.js r160 原始碼](https://github.com/mrdoob/three.js/tree/r160)：本例鎖版依據。
- [SimplexNoise r160](https://github.com/mrdoob/three.js/blob/r160/examples/jsm/math/SimplexNoise.js)：帶 random 方法的種子物件。
- [InstancedMesh r160](https://github.com/mrdoob/three.js/blob/r160/src/objects/InstancedMesh.js)：實例拾取及 instanceId。
- [動態 import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import)：載入失敗的 Promise 錯誤處理。
- [CSS2DRenderer](https://threejs.org/docs/pages/CSS2DRenderer.html)：瀏覽器及顯示縮放限制。
