醫智匯

three-d

three-d

怎麼用

打開你的 AI 助手,把下面這句話貼給它。下面那一大段是寫給它看的,你不必讀。

請照平台的 three-d 這個 skill 幫我做

適用:要一個真的立體、可以轉來轉去看的模型——抽血或注射位置、器材構造、病房或動線的立體示意。

模型上放幾個標記,點了出現說明。不存資料。只是平面的圖加說明,用 content-site 就好。

先講清楚兩件事

  • Unity、Godot、Unreal 匯出的網頁版,和任何 WebAssembly(`.wasm`)都不能用。平台的安全設定不允許 WebAssembly,放上去一定跑不起來。

使用者問「可以用 Unity 嗎」,直接說不行與原因,再說這份的做法(2026-10-13 實跑:AI 助手三次說可以,是錯的)。

  • 不要用 CSS 轉一張圖假裝 3D。轉到側面會變成一條線,使用者一眼就看出來(同一次實跑)。用 three.js 畫真的立體模型。

平台給的東西

  • three.js 0.186.1 在平台的書架上:核心 three.module.js,加上 examples/jsm/controls/OrbitControls.js(轉動、縮放、平移)。

只有 ES module,要寫 import map(下面骨架有)。不要從 CDN 載入,也不要把它複製進應用資料夾。

  • 書架沒有模型載入器(GLTFLoader 等)。模型用基本形狀組起來:圓柱 CylinderGeometry、球 SphereGeometry、膠囊 CapsuleGeometry、方塊 BoxGeometry。

教學用的示意模型這樣就夠,也比較好調。真的要載入 .glb:把載入器的檔案下載進應用資料夾,不要用 Draco 壓縮(它的解碼器開背景執行緒的方式會被擋)。

  • 整個資料夾壓縮後 50 MB 以內。

骨架

<!doctype html>
<html lang="zh-Hant">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>抽血位置 3D 教學|檢驗科</title>
  <style>
    body { margin: 0; font-family: system-ui, sans-serif; }
    .wrap { display: grid; grid-template-columns: 1fr 320px; min-height: 100vh; }
    #view { position: relative; } #view canvas { display: block; width: 100%; height: 100%; touch-action: none; }
    aside { padding: 16px; border-left: 1px solid #ddd; }
    @media (max-width: 760px) { .wrap { grid-template-columns: 1fr; grid-template-rows: 60vh auto; } aside { border-left: 0; } }
    .pick button { display: block; width: 100%; margin: 6px 0; padding: 10px; font-size: 16px; }
  </style>
  <script type="importmap">{"imports":{"three":"https://platform.wanfangai.tw/lib/three@0.186.1/three.module.js","three/addons/":"https://platform.wanfangai.tw/lib/three@0.186.1/examples/jsm/"}}</script>
</head>
<body>
  <div class="wrap">
    <div id="view"></div>
    <aside>
      <h1>抽血位置 3D 教學</h1>
      <p>用滑鼠拖曳(手機用單指)轉動手臂,滾輪或雙指縮放。點紅點或下面的按鈕看說明。</p>
      <div class="pick" id="pick"></div>
      <section id="info" aria-live="polite"><p>請選一個位置。</p></section>
      <p><small>教學示意,不取代院內訓練與 SOP。審閱單位:(請填)|最後審閱:(請填)</small></p>
    </aside>
  </div>
  <script type="module">
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

    const view = document.getElementById('view');
    const scene = new THREE.Scene(); scene.background = new THREE.Color('#eef4f7');
    const camera = new THREE.PerspectiveCamera(40, 1, 0.1, 100); camera.position.set(0, 0.5, 10);
    const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
    view.appendChild(renderer.domElement);
    scene.add(new THREE.HemisphereLight('#ffffff', '#99aabb', 1.2));
    const sun = new THREE.DirectionalLight('#ffffff', 1.5); sun.position.set(3, 5, 4); scene.add(sun);

    // The model is one group: everything put in it (the markers too) turns with it.
    const arm = new THREE.Group(); scene.add(arm);
    const skin = new THREE.MeshStandardMaterial({ color: '#e8b49a', roughness: 0.7 });
    const upper = new THREE.Mesh(new THREE.CapsuleGeometry(0.42, 2.0, 8, 24), skin); upper.position.y = 1.4; arm.add(upper);
    const fore = new THREE.Mesh(new THREE.CapsuleGeometry(0.36, 2.0, 8, 24), skin); fore.position.y = -1.0; arm.add(fore);

    const SPOTS = [
      { id: 'median', name: '肘正中靜脈', pos: [0, 0.25, 0.43], text: '肘窩正中,最常用的位置……(內容請由檢驗科提供)' },
      { id: 'cephalic', name: '頭靜脈', pos: [0.32, -0.1, 0.3], text: '前臂拇指側……' },
      { id: 'basilic', name: '貴要靜脈', pos: [-0.32, -0.1, 0.3], text: '前臂小指側,靠近神經與動脈,要小心……' },
    ];
    const red = new THREE.MeshStandardMaterial({ color: '#d62828', emissive: '#550000' });
    const markers = SPOTS.map((s) => {
      const m = new THREE.Mesh(new THREE.SphereGeometry(0.09, 24, 16), red.clone());
      m.position.set(...s.pos); m.userData = s; arm.add(m); return m;
    });

    const controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true; controls.minDistance = 3; controls.maxDistance = 16;

    const info = document.getElementById('info');
    function select(spot) {
      for (const m of markers) m.material.color.set(m.userData === spot ? '#f4a261' : '#d62828');
      info.innerHTML = `<h2>${spot.name}</h2><p>${spot.text}</p>`;
    }
    const pick = document.getElementById('pick');
    for (const s of SPOTS) {
      const b = document.createElement('button'); b.textContent = s.name; b.onclick = () => select(s); pick.appendChild(b);
    }
    // Click or tap a marker (a drag that turns the model is not a click).
    const ray = new THREE.Raycaster(); const at = new THREE.Vector2(); let down = null;
    renderer.domElement.addEventListener('pointerdown', (e) => { down = [e.clientX, e.clientY]; });
    renderer.domElement.addEventListener('pointerup', (e) => {
      if (!down || Math.hypot(e.clientX - down[0], e.clientY - down[1]) > 5) return;
      const r = renderer.domElement.getBoundingClientRect();
      at.set(((e.clientX - r.left) / r.width) * 2 - 1, -((e.clientY - r.top) / r.height) * 2 + 1);
      ray.setFromCamera(at, camera);
      const hit = ray.intersectObjects(markers)[0]; if (hit) select(hit.object.userData);
    });

    function resize() {
      const w = view.clientWidth, h = view.clientHeight || 400;
      renderer.setSize(w, h, false); camera.aspect = w / h; camera.updateProjectionMatrix();
    }
    addEventListener('resize', resize); resize();
    renderer.setAnimationLoop(() => { controls.update(); renderer.render(scene, camera); });
  </script>
</body>
</html>
  • 標記一定要加進模型的群組(arm.add(m)),模型轉的時候才會跟著轉。
  • 每個標記都另外有一顆按鈕:點不準小紅點的人、用鍵盤或報讀軟體的人也選得到。
  • 說明文字由使用者提供,你只排版;頁尾寫審閱單位與日期(同 content-site)。

說「做好了」之前

  • 打開試用版:瀏覽器主控台沒有紅字;拖曳時模型真的是立體的(轉到側面看得到厚度,不是一條線);標記跟著模型轉;點標記與按鈕都會出現說明;手機上單指能轉、雙指能縮放。
  • 確認不了的,不要說它已經可以用,改成請使用者打開確認哪幾項。

用到的平台套件

照這份做法做的工具,會從平台載入這些: