[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71A51QNGFPCXAE6Z9PC2JH9":3,"skill-same:01M71A51QNGFPCXAE6Z9PC2JH9":64},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":39,"linkOnly":40,"markdownContent":41,"markdownBody":42,"frontMatter":43,"refName":44,"commitSha":45,"skillDirectory":46,"forksCount":14,"repositoryHtmlUrl":47,"repositoryHomepageUrl":6,"ownerHtmlUrl":48,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":49,"install":50,"resources":63},"01M71A51QNGFPCXAE6Z9PC2JH9","threejs-interaction",null,"Three.js interaction - raycasting, controls, mouse\u002Ftouch input, object selection. Use when handling user input, implementing click detection, adding camera controls, or creating interactive 3D experiences.","sapsapshen\u002Fai-memory-router","sapsapshen","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F20347959?v=4","01M6XDQEF1DP50DKF37H81J3J5","shenmian","01M6XDQEJPE41Y3DT2763X8ZJN",0,"MIT","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fglobal\u002Fthreejs-interaction\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"threejs","interaction","threejs interaction","when","detection","unknown","implementing","object","touch","controls","input","click","three","selection","community","camera","use","interactive","js","3D","2026-11-10 10:12:30",false,"---\nname: threejs-interaction\ndescription: Three.js interaction - raycasting, controls, mouse\u002Ftouch input, object selection. Use when handling user input, implementing click detection, adding camera controls, or creating interactive 3D experiences.\nrisk: unknown\nsource: community\n---\n\n# Three.js Interaction\n\n## Quick Start\n\n```javascript\nimport * as THREE from \"three\";\nimport { OrbitControls } from \"three\u002Faddons\u002Fcontrols\u002FOrbitControls.js\";\n\n\u002F\u002F Camera controls\nconst controls = new OrbitControls(camera, renderer.domElement);\ncontrols.enableDamping = true;\n\n\u002F\u002F Raycasting for click detection\nconst raycaster = new THREE.Raycaster();\nconst mouse = new THREE.Vector2();\n\nfunction onClick(event) {\n  mouse.x = (event.clientX \u002F window.innerWidth) * 2 - 1;\n  mouse.y = -(event.clientY \u002F window.innerHeight) * 2 + 1;\n\n  raycaster.setFromCamera(mouse, camera);\n  const intersects = raycaster.intersectObjects(scene.children);\n\n  if (intersects.length > 0) {\n    console.log(\"Clicked:\", intersects[0].object);\n  }\n}\n\nwindow.addEventListener(\"click\", onClick);\n```\n\n## Raycaster\n\n### Basic Raycasting\n\n```javascript\nconst raycaster = new THREE.Raycaster();\n\n\u002F\u002F From camera (mouse picking)\nraycaster.setFromCamera(mousePosition, camera);\n\n\u002F\u002F From any origin and direction\nraycaster.set(origin, direction); \u002F\u002F origin: Vector3, direction: normalized Vector3\n\n\u002F\u002F Get intersections\nconst intersects = raycaster.intersectObjects(objects, recursive);\n\n\u002F\u002F intersects array contains:\n\u002F\u002F {\n\u002F\u002F   distance: number,          \u002F\u002F Distance from ray origin\n\u002F\u002F   point: Vector3,            \u002F\u002F Intersection point in world coords\n\u002F\u002F   face: Face3,               \u002F\u002F Intersected face\n\u002F\u002F   faceIndex: number,         \u002F\u002F Face index\n\u002F\u002F   object: Object3D,          \u002F\u002F Intersected object\n\u002F\u002F   uv: Vector2,               \u002F\u002F UV coordinates at intersection\n\u002F\u002F   uv1: Vector2,              \u002F\u002F Second UV channel\n\u002F\u002F   normal: Vector3,           \u002F\u002F Interpolated face normal\n\u002F\u002F   instanceId: number         \u002F\u002F For InstancedMesh\n\u002F\u002F }\n```\n\n### Mouse Position Conversion\n\n```javascript\nconst mouse = new THREE.Vector2();\n\nfunction updateMouse(event) {\n  \u002F\u002F For full window\n  mouse.x = (event.clientX \u002F window.innerWidth) * 2 - 1;\n  mouse.y = -(event.clientY \u002F window.innerHeight) * 2 + 1;\n}\n\n\u002F\u002F For specific canvas element\nfunction updateMouseCanvas(event, canvas) {\n  const rect = canvas.getBoundingClientRect();\n  mouse.x = ((event.clientX - rect.left) \u002F rect.width) * 2 - 1;\n  mouse.y = -((event.clientY - rect.top) \u002F rect.height) * 2 + 1;\n}\n```\n\n### Touch Support\n\n```javascript\nfunction onTouchStart(event) {\n  event.preventDefault();\n\n  if (event.touches.length === 1) {\n    const touch = event.touches[0];\n    mouse.x = (touch.clientX \u002F window.innerWidth) * 2 - 1;\n    mouse.y = -(touch.clientY \u002F window.innerHeight) * 2 + 1;\n\n    raycaster.setFromCamera(mouse, camera);\n    const intersects = raycaster.intersectObjects(clickableObjects);\n\n    if (intersects.length > 0) {\n      handleSelection(intersects[0]);\n    }\n  }\n}\n\nrenderer.domElement.addEventListener(\"touchstart\", onTouchStart);\n```\n\n### Raycaster Options\n\n```javascript\nconst raycaster = new THREE.Raycaster();\n\n\u002F\u002F Near\u002Ffar clipping (default: 0, Infinity)\nraycaster.near = 0;\nraycaster.far = 100;\n\n\u002F\u002F Line\u002FPoints precision\nraycaster.params.Line.threshold = 0.1;\nraycaster.params.Points.threshold = 0.1;\n\n\u002F\u002F Layers (only intersect objects on specific layers)\nraycaster.layers.set(1);\n```\n\n### Efficient Raycasting\n\n```javascript\n\u002F\u002F Only check specific objects\nconst clickables = [mesh1, mesh2, mesh3];\nconst intersects = raycaster.intersectObjects(clickables, false);\n\n\u002F\u002F Use layers for filtering\nmesh1.layers.set(1); \u002F\u002F Clickable layer\nraycaster.layers.set(1);\n\n\u002F\u002F Throttle raycast for hover effects\nlet lastRaycast = 0;\nfunction onMouseMove(event) {\n  const now = Date.now();\n  if (now - lastRaycast \u003C 50) return; \u002F\u002F 20fps max\n  lastRaycast = now;\n\n  \u002F\u002F Raycast here\n}\n```\n\n## Camera Controls\n\n### OrbitControls\n\n```javascript\nimport { OrbitControls } from \"three\u002Faddons\u002Fcontrols\u002FOrbitControls.js\";\n\nconst controls = new OrbitControls(camera, renderer.domElement);\n\n\u002F\u002F Damping (smooth movement)\ncontrols.enableDamping = true;\ncontrols.dampingFactor = 0.05;\n\n\u002F\u002F Rotation limits\ncontrols.minPolarAngle = 0; \u002F\u002F Top\ncontrols.maxPolarAngle = Math.PI \u002F 2; \u002F\u002F Horizon\ncontrols.minAzimuthAngle = -Math.PI \u002F 4; \u002F\u002F Left\ncontrols.maxAzimuthAngle = Math.PI \u002F 4; \u002F\u002F Right\n\n\u002F\u002F Zoom limits\ncontrols.minDistance = 2;\ncontrols.maxDistance = 50;\n\n\u002F\u002F Enable\u002Fdisable features\ncontrols.enableRotate = true;\ncontrols.enableZoom = true;\ncontrols.enablePan = true;\n\n\u002F\u002F Auto-rotate\ncontrols.autoRotate = true;\ncontrols.autoRotateSpeed = 2.0;\n\n\u002F\u002F Target (orbit point)\ncontrols.target.set(0, 1, 0);\n\n\u002F\u002F Update in animation loop\nfunction animate() {\n  controls.update(); \u002F\u002F Required for damping and auto-rotate\n  renderer.render(scene, camera);\n}\n```\n\n### FlyControls\n\n```javascript\nimport { FlyControls } from \"three\u002Faddons\u002Fcontrols\u002FFlyControls.js\";\n\nconst controls = new FlyControls(camera, renderer.domElement);\ncontrols.movementSpeed = 10;\ncontrols.rollSpeed = Math.PI \u002F 24;\ncontrols.dragToLook = true;\n\n\u002F\u002F Update with delta\nfunction animate() {\n  controls.update(clock.getDelta());\n  renderer.render(scene, camera);\n}\n```\n\n### FirstPersonControls\n\n```javascript\nimport { FirstPersonControls } from \"three\u002Faddons\u002Fcontrols\u002FFirstPersonControls.js\";\n\nconst controls = new FirstPersonControls(camera, renderer.domElement);\ncontrols.movementSpeed = 10;\ncontrols.lookSpeed = 0.1;\ncontrols.lookVertical = true;\ncontrols.constrainVertical = true;\ncontrols.verticalMin = Math.PI \u002F 4;\ncontrols.verticalMax = (Math.PI * 3) \u002F 4;\n\nfunction animate() {\n  controls.update(clock.getDelta());\n}\n```\n\n### PointerLockControls\n\n```javascript\nimport { PointerLockControls } from \"three\u002Faddons\u002Fcontrols\u002FPointerLockControls.js\";\n\nconst controls = new PointerLockControls(camera, document.body);\n\n\u002F\u002F Lock pointer on click\ndocument.addEventListener(\"click\", () => {\n  controls.lock();\n});\n\ncontrols.addEventListener(\"lock\", () => {\n  console.log(\"Pointer locked\");\n});\n\ncontrols.addEventListener(\"unlock\", () => {\n  console.log(\"Pointer unlocked\");\n});\n\n\u002F\u002F Movement\nconst velocity = new THREE.Vector3();\nconst direction = new THREE.Vector3();\nconst moveForward = false;\nconst moveBackward = false;\n\ndocument.addEventListener(\"keydown\", (event) => {\n  switch (event.code) {\n    case \"KeyW\":\n      moveForward = true;\n      break;\n    case \"KeyS\":\n      moveBackward = true;\n      break;\n  }\n});\n\nfunction animate() {\n  if (controls.isLocked) {\n    direction.z = Number(moveForward) - Number(moveBackward);\n    direction.normalize();\n\n    velocity.z -= direction.z * 0.1;\n    velocity.z *= 0.9; \u002F\u002F Friction\n\n    controls.moveForward(-velocity.z);\n  }\n}\n```\n\n### TrackballControls\n\n```javascript\nimport { TrackballControls } from \"three\u002Faddons\u002Fcontrols\u002FTrackballControls.js\";\n\nconst controls = new TrackballControls(camera, renderer.domElement);\ncontrols.rotateSpeed = 2.0;\ncontrols.zoomSpeed = 1.2;\ncontrols.panSpeed = 0.8;\ncontrols.staticMoving = true;\n\nfunction animate() {\n  controls.update();\n}\n```\n\n### MapControls\n\n```javascript\nimport { MapControls } from \"three\u002Faddons\u002Fcontrols\u002FMapControls.js\";\n\nconst controls = new MapControls(camera, renderer.domElement);\ncontrols.enableDamping = true;\ncontrols.dampingFactor = 0.05;\ncontrols.screenSpacePanning = false;\ncontrols.maxPolarAngle = Math.PI \u002F 2;\n```\n\n## TransformControls\n\nGizmo for moving\u002Frotating\u002Fscaling objects.\n\n```javascript\nimport { TransformControls } from \"three\u002Faddons\u002Fcontrols\u002FTransformControls.js\";\n\nconst transformControls = new TransformControls(camera, renderer.domElement);\nscene.add(transformControls);\n\n\u002F\u002F Attach to object\ntransformControls.attach(selectedMesh);\n\n\u002F\u002F Switch modes\ntransformControls.setMode(\"translate\"); \u002F\u002F 'translate', 'rotate', 'scale'\n\n\u002F\u002F Change space\ntransformControls.setSpace(\"local\"); \u002F\u002F 'local', 'world'\n\n\u002F\u002F Size\ntransformControls.setSize(1);\n\n\u002F\u002F Events\ntransformControls.addEventListener(\"dragging-changed\", (event) => {\n  \u002F\u002F Disable orbit controls while dragging\n  orbitControls.enabled = !event.value;\n});\n\ntransformControls.addEventListener(\"change\", () => {\n  renderer.render(scene, camera);\n});\n\n\u002F\u002F Keyboard shortcuts\nwindow.addEventListener(\"keydown\", (event) => {\n  switch (event.key) {\n    case \"g\":\n      transformControls.setMode(\"translate\");\n      break;\n    case \"r\":\n      transformControls.setMode(\"rotate\");\n      break;\n    case \"s\":\n      transformControls.setMode(\"scale\");\n      break;\n    case \"Escape\":\n      transformControls.detach();\n      break;\n  }\n});\n```\n\n## DragControls\n\nDrag objects directly.\n\n```javascript\nimport { DragControls } from \"three\u002Faddons\u002Fcontrols\u002FDragControls.js\";\n\nconst draggableObjects = [mesh1, mesh2, mesh3];\nconst dragControls = new DragControls(\n  draggableObjects,\n  camera,\n  renderer.domElement,\n);\n\ndragControls.addEventListener(\"dragstart\", (event) => {\n  orbitControls.enabled = false;\n  event.object.material.emissive.set(0xaaaaaa);\n});\n\ndragControls.addEventListener(\"drag\", (event) => {\n  \u002F\u002F Constrain to ground plane\n  event.object.position.y = 0;\n});\n\ndragControls.addEventListener(\"dragend\", (event) => {\n  orbitControls.enabled = true;\n  event.object.material.emissive.set(0x000000);\n});\n```\n\n## Selection System\n\n### Click to Select\n\n```javascript\nconst raycaster = new THREE.Raycaster();\nconst mouse = new THREE.Vector2();\nlet selectedObject = null;\n\nfunction onMouseDown(event) {\n  mouse.x = (event.clientX \u002F window.innerWidth) * 2 - 1;\n  mouse.y = -(event.clientY \u002F window.innerHeight) * 2 + 1;\n\n  raycaster.setFromCamera(mouse, camera);\n  const intersects = raycaster.intersectObjects(selectableObjects);\n\n  \u002F\u002F Deselect previous\n  if (selectedObject) {\n    selectedObject.material.emissive.set(0x000000);\n  }\n\n  \u002F\u002F Select new\n  if (intersects.length > 0) {\n    selectedObject = intersects[0].object;\n    selectedObject.material.emissive.set(0x444444);\n  } else {\n    selectedObject = null;\n  }\n}\n```\n\n### Box Selection\n\n```javascript\nimport { SelectionBox } from \"three\u002Faddons\u002Finteractive\u002FSelectionBox.js\";\nimport { SelectionHelper } from \"three\u002Faddons\u002Finteractive\u002FSelectionHelper.js\";\n\nconst selectionBox = new SelectionBox(camera, scene);\nconst selectionHelper = new SelectionHelper(renderer, \"selectBox\"); \u002F\u002F CSS class\n\ndocument.addEventListener(\"pointerdown\", (event) => {\n  selectionBox.startPoint.set(\n    (event.clientX \u002F window.innerWidth) * 2 - 1,\n    -(event.clientY \u002F window.innerHeight) * 2 + 1,\n    0.5,\n  );\n});\n\ndocument.addEventListener(\"pointermove\", (event) => {\n  if (selectionHelper.isDown) {\n    selectionBox.endPoint.set(\n      (event.clientX \u002F window.innerWidth) * 2 - 1,\n      -(event.clientY \u002F window.innerHeight) * 2 + 1,\n      0.5,\n    );\n  }\n});\n\ndocument.addEventListener(\"pointerup\", (event) => {\n  selectionBox.endPoint.set(\n    (event.clientX \u002F window.innerWidth) * 2 - 1,\n    -(event.clientY \u002F window.innerHeight) * 2 + 1,\n    0.5,\n  );\n\n  const selected = selectionBox.select();\n  console.log(\"Selected objects:\", selected);\n});\n```\n\n### Hover Effects\n\n```javascript\nconst raycaster = new THREE.Raycaster();\nconst mouse = new THREE.Vector2();\nlet hoveredObject = null;\n\nfunction onMouseMove(event) {\n  mouse.x = (event.clientX \u002F window.innerWidth) * 2 - 1;\n  mouse.y = -(event.clientY \u002F window.innerHeight) * 2 + 1;\n\n  raycaster.setFromCamera(mouse, camera);\n  const intersects = raycaster.intersectObjects(hoverableObjects);\n\n  \u002F\u002F Reset previous hover\n  if (hoveredObject) {\n    hoveredObject.material.color.set(hoveredObject.userData.originalColor);\n    document.body.style.cursor = \"default\";\n  }\n\n  \u002F\u002F Apply new hover\n  if (intersects.length > 0) {\n    hoveredObject = intersects[0].object;\n    if (!hoveredObject.userData.originalColor) {\n      hoveredObject.userData.originalColor =\n        hoveredObject.material.color.getHex();\n    }\n    hoveredObject.material.color.set(0xff6600);\n    document.body.style.cursor = \"pointer\";\n  } else {\n    hoveredObject = null;\n  }\n}\n\nwindow.addEventListener(\"mousemove\", onMouseMove);\n```\n\n## Keyboard Input\n\n```javascript\nconst keys = {};\n\ndocument.addEventListener(\"keydown\", (event) => {\n  keys[event.code] = true;\n});\n\ndocument.addEventListener(\"keyup\", (event) => {\n  keys[event.code] = false;\n});\n\nfunction update() {\n  const speed = 0.1;\n\n  if (keys[\"KeyW\"]) player.position.z -= speed;\n  if (keys[\"KeyS\"]) player.position.z += speed;\n  if (keys[\"KeyA\"]) player.position.x -= speed;\n  if (keys[\"KeyD\"]) player.position.x += speed;\n  if (keys[\"Space\"]) player.position.y += speed;\n  if (keys[\"ShiftLeft\"]) player.position.y -= speed;\n}\n```\n\n## World-Screen Coordinate Conversion\n\n### World to Screen\n\n```javascript\nfunction worldToScreen(position, camera) {\n  const vector = position.clone();\n  vector.project(camera);\n\n  return {\n    x: ((vector.x + 1) \u002F 2) * window.innerWidth,\n    y: (-(vector.y - 1) \u002F 2) * window.innerHeight,\n  };\n}\n\n\u002F\u002F Position HTML element over 3D object\nconst screenPos = worldToScreen(mesh.position, camera);\nelement.style.left = screenPos.x + \"px\";\nelement.style.top = screenPos.y + \"px\";\n```\n\n### Screen to World\n\n```javascript\nfunction screenToWorld(screenX, screenY, camera, targetZ = 0) {\n  const vector = new THREE.Vector3(\n    (screenX \u002F window.innerWidth) * 2 - 1,\n    -(screenY \u002F window.innerHeight) * 2 + 1,\n    0.5,\n  );\n\n  vector.unproject(camera);\n\n  const dir = vector.sub(camera.position).normalize();\n  const distance = (targetZ - camera.position.z) \u002F dir.z;\n\n  return camera.position.clone().add(dir.multiplyScalar(distance));\n}\n```\n\n### Ray-Plane Intersection\n\n```javascript\nfunction getRayPlaneIntersection(mouse, camera, plane) {\n  const raycaster = new THREE.Raycaster();\n  raycaster.setFromCamera(mouse, camera);\n\n  const intersection = new THREE.Vector3();\n  raycaster.ray.intersectPlane(plane, intersection);\n\n  return intersection;\n}\n\n\u002F\u002F Ground plane\nconst groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);\nconst worldPos = getRayPlaneIntersection(mouse, camera, groundPlane);\n```\n\n## Event Handling Best Practices\n\n```javascript\nclass InteractionManager {\n  constructor(camera, renderer, scene) {\n    this.camera = camera;\n    this.renderer = renderer;\n    this.scene = scene;\n    this.raycaster = new THREE.Raycaster();\n    this.mouse = new THREE.Vector2();\n    this.clickables = [];\n\n    this.bindEvents();\n  }\n\n  bindEvents() {\n    const canvas = this.renderer.domElement;\n\n    canvas.addEventListener(\"click\", (e) => this.onClick(e));\n    canvas.addEventListener(\"mousemove\", (e) => this.onMouseMove(e));\n    canvas.addEventListener(\"touchstart\", (e) => this.onTouchStart(e));\n  }\n\n  updateMouse(event) {\n    const rect = this.renderer.domElement.getBoundingClientRect();\n    this.mouse.x = ((event.clientX - rect.left) \u002F rect.width) * 2 - 1;\n    this.mouse.y = -((event.clientY - rect.top) \u002F rect.height) * 2 + 1;\n  }\n\n  getIntersects() {\n    this.raycaster.setFromCamera(this.mouse, this.camera);\n    return this.raycaster.intersectObjects(this.clickables, true);\n  }\n\n  onClick(event) {\n    this.updateMouse(event);\n    const intersects = this.getIntersects();\n\n    if (intersects.length > 0) {\n      const object = intersects[0].object;\n      if (object.userData.onClick) {\n        object.userData.onClick(intersects[0]);\n      }\n    }\n  }\n\n  addClickable(object, callback) {\n    this.clickables.push(object);\n    object.userData.onClick = callback;\n  }\n\n  dispose() {\n    \u002F\u002F Remove event listeners\n  }\n}\n\n\u002F\u002F Usage\nconst interaction = new InteractionManager(camera, renderer, scene);\ninteraction.addClickable(mesh, (intersect) => {\n  console.log(\"Clicked at:\", intersect.point);\n});\n```\n\n## Performance Tips\n\n1. **Limit raycasts**: Throttle mousemove handlers\n2. **Use layers**: Filter raycast targets\n3. **Simple collision meshes**: Use invisible simpler geometry for raycasting\n4. **Disable controls when not needed**: `controls.enabled = false`\n5. **Batch updates**: Group interaction checks\n\n```javascript\n\u002F\u002F Use simpler geometry for raycasting\nconst complexMesh = loadedModel;\nconst collisionMesh = new THREE.Mesh(\n  new THREE.BoxGeometry(1, 1, 1),\n  new THREE.MeshBasicMaterial({ visible: false }),\n);\ncollisionMesh.userData.target = complexMesh;\nclickables.push(collisionMesh);\n```\n\n## See Also\n\n- `threejs-fundamentals` - Camera and scene setup\n- `threejs-animation` - Animating interactions\n- `threejs-shaders` - Visual feedback effects\n","# Three.js Interaction\n\n## Quick Start\n\n```javascript\nimport * as THREE from \"three\";\nimport { OrbitControls } from \"three\u002Faddons\u002Fcontrols\u002FOrbitControls.js\";\n\n\u002F\u002F Camera controls\nconst controls = new OrbitControls(camera, renderer.domElement);\ncontrols.enableDamping = true;\n\n\u002F\u002F Raycasting for click detection\nconst raycaster = new THREE.Raycaster();\nconst mouse = new THREE.Vector2();\n\nfunction onClick(event) {\n  mouse.x = (event.clientX \u002F window.innerWidth) * 2 - 1;\n  mouse.y = -(event.clientY \u002F window.innerHeight) * 2 + 1;\n\n  raycaster.setFromCamera(mouse, camera);\n  const intersects = raycaster.intersectObjects(scene.children);\n\n  if (intersects.length > 0) {\n    console.log(\"Clicked:\", intersects[0].object);\n  }\n}\n\nwindow.addEventListener(\"click\", onClick);\n```\n\n## Raycaster\n\n### Basic Raycasting\n\n```javascript\nconst raycaster = new THREE.Raycaster();\n\n\u002F\u002F From camera (mouse picking)\nraycaster.setFromCamera(mousePosition, camera);\n\n\u002F\u002F From any origin and direction\nraycaster.set(origin, direction); \u002F\u002F origin: Vector3, direction: normalized Vector3\n\n\u002F\u002F Get intersections\nconst intersects = raycaster.intersectObjects(objects, recursive);\n\n\u002F\u002F intersects array contains:\n\u002F\u002F {\n\u002F\u002F   distance: number,          \u002F\u002F Distance from ray origin\n\u002F\u002F   point: Vector3,            \u002F\u002F Intersection point in world coords\n\u002F\u002F   face: Face3,               \u002F\u002F Intersected face\n\u002F\u002F   faceIndex: number,         \u002F\u002F Face index\n\u002F\u002F   object: Object3D,          \u002F\u002F Intersected object\n\u002F\u002F   uv: Vector2,               \u002F\u002F UV coordinates at intersection\n\u002F\u002F   uv1: Vector2,              \u002F\u002F Second UV channel\n\u002F\u002F   normal: Vector3,           \u002F\u002F Interpolated face normal\n\u002F\u002F   instanceId: number         \u002F\u002F For InstancedMesh\n\u002F\u002F }\n```\n\n### Mouse Position Conversion\n\n```javascript\nconst mouse = new THREE.Vector2();\n\nfunction updateMouse(event) {\n  \u002F\u002F For full window\n  mouse.x = (event.clientX \u002F window.innerWidth) * 2 - 1;\n  mouse.y = -(event.clientY \u002F window.innerHeight) * 2 + 1;\n}\n\n\u002F\u002F For specific canvas element\nfunction updateMouseCanvas(event, canvas) {\n  const rect = canvas.getBoundingClientRect();\n  mouse.x = ((event.clientX - rect.left) \u002F rect.width) * 2 - 1;\n  mouse.y = -((event.clientY - rect.top) \u002F rect.height) * 2 + 1;\n}\n```\n\n### Touch Support\n\n```javascript\nfunction onTouchStart(event) {\n  event.preventDefault();\n\n  if (event.touches.length === 1) {\n    const touch = event.touches[0];\n    mouse.x = (touch.clientX \u002F window.innerWidth) * 2 - 1;\n    mouse.y = -(touch.clientY \u002F window.innerHeight) * 2 + 1;\n\n    raycaster.setFromCamera(mouse, camera);\n    const intersects = raycaster.intersectObjects(clickableObjects);\n\n    if (intersects.length > 0) {\n      handleSelection(intersects[0]);\n    }\n  }\n}\n\nrenderer.domElement.addEventListener(\"touchstart\", onTouchStart);\n```\n\n### Raycaster Options\n\n```javascript\nconst raycaster = new THREE.Raycaster();\n\n\u002F\u002F Near\u002Ffar clipping (default: 0, Infinity)\nraycaster.near = 0;\nraycaster.far = 100;\n\n\u002F\u002F Line\u002FPoints precision\nraycaster.params.Line.threshold = 0.1;\nraycaster.params.Points.threshold = 0.1;\n\n\u002F\u002F Layers (only intersect objects on specific layers)\nraycaster.layers.set(1);\n```\n\n### Efficient Raycasting\n\n```javascript\n\u002F\u002F Only check specific objects\nconst clickables = [mesh1, mesh2, mesh3];\nconst intersects = raycaster.intersectObjects(clickables, false);\n\n\u002F\u002F Use layers for filtering\nmesh1.layers.set(1); \u002F\u002F Clickable layer\nraycaster.layers.set(1);\n\n\u002F\u002F Throttle raycast for hover effects\nlet lastRaycast = 0;\nfunction onMouseMove(event) {\n  const now = Date.now();\n  if (now - lastRaycast \u003C 50) return; \u002F\u002F 20fps max\n  lastRaycast = now;\n\n  \u002F\u002F Raycast here\n}\n```\n\n## Camera Controls\n\n### OrbitControls\n\n```javascript\nimport { OrbitControls } from \"three\u002Faddons\u002Fcontrols\u002FOrbitControls.js\";\n\nconst controls = new OrbitControls(camera, renderer.domElement);\n\n\u002F\u002F Damping (smooth movement)\ncontrols.enableDamping = true;\ncontrols.dampingFactor = 0.05;\n\n\u002F\u002F Rotation limits\ncontrols.minPolarAngle = 0; \u002F\u002F Top\ncontrols.maxPolarAngle = Math.PI \u002F 2; \u002F\u002F Horizon\ncontrols.minAzimuthAngle = -Math.PI \u002F 4; \u002F\u002F Left\ncontrols.maxAzimuthAngle = Math.PI \u002F 4; \u002F\u002F Right\n\n\u002F\u002F Zoom limits\ncontrols.minDistance = 2;\ncontrols.maxDistance = 50;\n\n\u002F\u002F Enable\u002Fdisable features\ncontrols.enableRotate = true;\ncontrols.enableZoom = true;\ncontrols.enablePan = true;\n\n\u002F\u002F Auto-rotate\ncontrols.autoRotate = true;\ncontrols.autoRotateSpeed = 2.0;\n\n\u002F\u002F Target (orbit point)\ncontrols.target.set(0, 1, 0);\n\n\u002F\u002F Update in animation loop\nfunction animate() {\n  controls.update(); \u002F\u002F Required for damping and auto-rotate\n  renderer.render(scene, camera);\n}\n```\n\n### FlyControls\n\n```javascript\nimport { FlyControls } from \"three\u002Faddons\u002Fcontrols\u002FFlyControls.js\";\n\nconst controls = new FlyControls(camera, renderer.domElement);\ncontrols.movementSpeed = 10;\ncontrols.rollSpeed = Math.PI \u002F 24;\ncontrols.dragToLook = true;\n\n\u002F\u002F Update with delta\nfunction animate() {\n  controls.update(clock.getDelta());\n  renderer.render(scene, camera);\n}\n```\n\n### FirstPersonControls\n\n```javascript\nimport { FirstPersonControls } from \"three\u002Faddons\u002Fcontrols\u002FFirstPersonControls.js\";\n\nconst controls = new FirstPersonControls(camera, renderer.domElement);\ncontrols.movementSpeed = 10;\ncontrols.lookSpeed = 0.1;\ncontrols.lookVertical = true;\ncontrols.constrainVertical = true;\ncontrols.verticalMin = Math.PI \u002F 4;\ncontrols.verticalMax = (Math.PI * 3) \u002F 4;\n\nfunction animate() {\n  controls.update(clock.getDelta());\n}\n```\n\n### PointerLockControls\n\n```javascript\nimport { PointerLockControls } from \"three\u002Faddons\u002Fcontrols\u002FPointerLockControls.js\";\n\nconst controls = new PointerLockControls(camera, document.body);\n\n\u002F\u002F Lock pointer on click\ndocument.addEventListener(\"click\", () => {\n  controls.lock();\n});\n\ncontrols.addEventListener(\"lock\", () => {\n  console.log(\"Pointer locked\");\n});\n\ncontrols.addEventListener(\"unlock\", () => {\n  console.log(\"Pointer unlocked\");\n});\n\n\u002F\u002F Movement\nconst velocity = new THREE.Vector3();\nconst direction = new THREE.Vector3();\nconst moveForward = false;\nconst moveBackward = false;\n\ndocument.addEventListener(\"keydown\", (event) => {\n  switch (event.code) {\n    case \"KeyW\":\n      moveForward = true;\n      break;\n    case \"KeyS\":\n      moveBackward = true;\n      break;\n  }\n});\n\nfunction animate() {\n  if (controls.isLocked) {\n    direction.z = Number(moveForward) - Number(moveBackward);\n    direction.normalize();\n\n    velocity.z -= direction.z * 0.1;\n    velocity.z *= 0.9; \u002F\u002F Friction\n\n    controls.moveForward(-velocity.z);\n  }\n}\n```\n\n### TrackballControls\n\n```javascript\nimport { TrackballControls } from \"three\u002Faddons\u002Fcontrols\u002FTrackballControls.js\";\n\nconst controls = new TrackballControls(camera, renderer.domElement);\ncontrols.rotateSpeed = 2.0;\ncontrols.zoomSpeed = 1.2;\ncontrols.panSpeed = 0.8;\ncontrols.staticMoving = true;\n\nfunction animate() {\n  controls.update();\n}\n```\n\n### MapControls\n\n```javascript\nimport { MapControls } from \"three\u002Faddons\u002Fcontrols\u002FMapControls.js\";\n\nconst controls = new MapControls(camera, renderer.domElement);\ncontrols.enableDamping = true;\ncontrols.dampingFactor = 0.05;\ncontrols.screenSpacePanning = false;\ncontrols.maxPolarAngle = Math.PI \u002F 2;\n```\n\n## TransformControls\n\nGizmo for moving\u002Frotating\u002Fscaling objects.\n\n```javascript\nimport { TransformControls } from \"three\u002Faddons\u002Fcontrols\u002FTransformControls.js\";\n\nconst transformControls = new TransformControls(camera, renderer.domElement);\nscene.add(transformControls);\n\n\u002F\u002F Attach to object\ntransformControls.attach(selectedMesh);\n\n\u002F\u002F Switch modes\ntransformControls.setMode(\"translate\"); \u002F\u002F 'translate', 'rotate', 'scale'\n\n\u002F\u002F Change space\ntransformControls.setSpace(\"local\"); \u002F\u002F 'local', 'world'\n\n\u002F\u002F Size\ntransformControls.setSize(1);\n\n\u002F\u002F Events\ntransformControls.addEventListener(\"dragging-changed\", (event) => {\n  \u002F\u002F Disable orbit controls while dragging\n  orbitControls.enabled = !event.value;\n});\n\ntransformControls.addEventListener(\"change\", () => {\n  renderer.render(scene, camera);\n});\n\n\u002F\u002F Keyboard shortcuts\nwindow.addEventListener(\"keydown\", (event) => {\n  switch (event.key) {\n    case \"g\":\n      transformControls.setMode(\"translate\");\n      break;\n    case \"r\":\n      transformControls.setMode(\"rotate\");\n      break;\n    case \"s\":\n      transformControls.setMode(\"scale\");\n      break;\n    case \"Escape\":\n      transformControls.detach();\n      break;\n  }\n});\n```\n\n## DragControls\n\nDrag objects directly.\n\n```javascript\nimport { DragControls } from \"three\u002Faddons\u002Fcontrols\u002FDragControls.js\";\n\nconst draggableObjects = [mesh1, mesh2, mesh3];\nconst dragControls = new DragControls(\n  draggableObjects,\n  camera,\n  renderer.domElement,\n);\n\ndragControls.addEventListener(\"dragstart\", (event) => {\n  orbitControls.enabled = false;\n  event.object.material.emissive.set(0xaaaaaa);\n});\n\ndragControls.addEventListener(\"drag\", (event) => {\n  \u002F\u002F Constrain to ground plane\n  event.object.position.y = 0;\n});\n\ndragControls.addEventListener(\"dragend\", (event) => {\n  orbitControls.enabled = true;\n  event.object.material.emissive.set(0x000000);\n});\n```\n\n## Selection System\n\n### Click to Select\n\n```javascript\nconst raycaster = new THREE.Raycaster();\nconst mouse = new THREE.Vector2();\nlet selectedObject = null;\n\nfunction onMouseDown(event) {\n  mouse.x = (event.clientX \u002F window.innerWidth) * 2 - 1;\n  mouse.y = -(event.clientY \u002F window.innerHeight) * 2 + 1;\n\n  raycaster.setFromCamera(mouse, camera);\n  const intersects = raycaster.intersectObjects(selectableObjects);\n\n  \u002F\u002F Deselect previous\n  if (selectedObject) {\n    selectedObject.material.emissive.set(0x000000);\n  }\n\n  \u002F\u002F Select new\n  if (intersects.length > 0) {\n    selectedObject = intersects[0].object;\n    selectedObject.material.emissive.set(0x444444);\n  } else {\n    selectedObject = null;\n  }\n}\n```\n\n### Box Selection\n\n```javascript\nimport { SelectionBox } from \"three\u002Faddons\u002Finteractive\u002FSelectionBox.js\";\nimport { SelectionHelper } from \"three\u002Faddons\u002Finteractive\u002FSelectionHelper.js\";\n\nconst selectionBox = new SelectionBox(camera, scene);\nconst selectionHelper = new SelectionHelper(renderer, \"selectBox\"); \u002F\u002F CSS class\n\ndocument.addEventListener(\"pointerdown\", (event) => {\n  selectionBox.startPoint.set(\n    (event.clientX \u002F window.innerWidth) * 2 - 1,\n    -(event.clientY \u002F window.innerHeight) * 2 + 1,\n    0.5,\n  );\n});\n\ndocument.addEventListener(\"pointermove\", (event) => {\n  if (selectionHelper.isDown) {\n    selectionBox.endPoint.set(\n      (event.clientX \u002F window.innerWidth) * 2 - 1,\n      -(event.clientY \u002F window.innerHeight) * 2 + 1,\n      0.5,\n    );\n  }\n});\n\ndocument.addEventListener(\"pointerup\", (event) => {\n  selectionBox.endPoint.set(\n    (event.clientX \u002F window.innerWidth) * 2 - 1,\n    -(event.clientY \u002F window.innerHeight) * 2 + 1,\n    0.5,\n  );\n\n  const selected = selectionBox.select();\n  console.log(\"Selected objects:\", selected);\n});\n```\n\n### Hover Effects\n\n```javascript\nconst raycaster = new THREE.Raycaster();\nconst mouse = new THREE.Vector2();\nlet hoveredObject = null;\n\nfunction onMouseMove(event) {\n  mouse.x = (event.clientX \u002F window.innerWidth) * 2 - 1;\n  mouse.y = -(event.clientY \u002F window.innerHeight) * 2 + 1;\n\n  raycaster.setFromCamera(mouse, camera);\n  const intersects = raycaster.intersectObjects(hoverableObjects);\n\n  \u002F\u002F Reset previous hover\n  if (hoveredObject) {\n    hoveredObject.material.color.set(hoveredObject.userData.originalColor);\n    document.body.style.cursor = \"default\";\n  }\n\n  \u002F\u002F Apply new hover\n  if (intersects.length > 0) {\n    hoveredObject = intersects[0].object;\n    if (!hoveredObject.userData.originalColor) {\n      hoveredObject.userData.originalColor =\n        hoveredObject.material.color.getHex();\n    }\n    hoveredObject.material.color.set(0xff6600);\n    document.body.style.cursor = \"pointer\";\n  } else {\n    hoveredObject = null;\n  }\n}\n\nwindow.addEventListener(\"mousemove\", onMouseMove);\n```\n\n## Keyboard Input\n\n```javascript\nconst keys = {};\n\ndocument.addEventListener(\"keydown\", (event) => {\n  keys[event.code] = true;\n});\n\ndocument.addEventListener(\"keyup\", (event) => {\n  keys[event.code] = false;\n});\n\nfunction update() {\n  const speed = 0.1;\n\n  if (keys[\"KeyW\"]) player.position.z -= speed;\n  if (keys[\"KeyS\"]) player.position.z += speed;\n  if (keys[\"KeyA\"]) player.position.x -= speed;\n  if (keys[\"KeyD\"]) player.position.x += speed;\n  if (keys[\"Space\"]) player.position.y += speed;\n  if (keys[\"ShiftLeft\"]) player.position.y -= speed;\n}\n```\n\n## World-Screen Coordinate Conversion\n\n### World to Screen\n\n```javascript\nfunction worldToScreen(position, camera) {\n  const vector = position.clone();\n  vector.project(camera);\n\n  return {\n    x: ((vector.x + 1) \u002F 2) * window.innerWidth,\n    y: (-(vector.y - 1) \u002F 2) * window.innerHeight,\n  };\n}\n\n\u002F\u002F Position HTML element over 3D object\nconst screenPos = worldToScreen(mesh.position, camera);\nelement.style.left = screenPos.x + \"px\";\nelement.style.top = screenPos.y + \"px\";\n```\n\n### Screen to World\n\n```javascript\nfunction screenToWorld(screenX, screenY, camera, targetZ = 0) {\n  const vector = new THREE.Vector3(\n    (screenX \u002F window.innerWidth) * 2 - 1,\n    -(screenY \u002F window.innerHeight) * 2 + 1,\n    0.5,\n  );\n\n  vector.unproject(camera);\n\n  const dir = vector.sub(camera.position).normalize();\n  const distance = (targetZ - camera.position.z) \u002F dir.z;\n\n  return camera.position.clone().add(dir.multiplyScalar(distance));\n}\n```\n\n### Ray-Plane Intersection\n\n```javascript\nfunction getRayPlaneIntersection(mouse, camera, plane) {\n  const raycaster = new THREE.Raycaster();\n  raycaster.setFromCamera(mouse, camera);\n\n  const intersection = new THREE.Vector3();\n  raycaster.ray.intersectPlane(plane, intersection);\n\n  return intersection;\n}\n\n\u002F\u002F Ground plane\nconst groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);\nconst worldPos = getRayPlaneIntersection(mouse, camera, groundPlane);\n```\n\n## Event Handling Best Practices\n\n```javascript\nclass InteractionManager {\n  constructor(camera, renderer, scene) {\n    this.camera = camera;\n    this.renderer = renderer;\n    this.scene = scene;\n    this.raycaster = new THREE.Raycaster();\n    this.mouse = new THREE.Vector2();\n    this.clickables = [];\n\n    this.bindEvents();\n  }\n\n  bindEvents() {\n    const canvas = this.renderer.domElement;\n\n    canvas.addEventListener(\"click\", (e) => this.onClick(e));\n    canvas.addEventListener(\"mousemove\", (e) => this.onMouseMove(e));\n    canvas.addEventListener(\"touchstart\", (e) => this.onTouchStart(e));\n  }\n\n  updateMouse(event) {\n    const rect = this.renderer.domElement.getBoundingClientRect();\n    this.mouse.x = ((event.clientX - rect.left) \u002F rect.width) * 2 - 1;\n    this.mouse.y = -((event.clientY - rect.top) \u002F rect.height) * 2 + 1;\n  }\n\n  getIntersects() {\n    this.raycaster.setFromCamera(this.mouse, this.camera);\n    return this.raycaster.intersectObjects(this.clickables, true);\n  }\n\n  onClick(event) {\n    this.updateMouse(event);\n    const intersects = this.getIntersects();\n\n    if (intersects.length > 0) {\n      const object = intersects[0].object;\n      if (object.userData.onClick) {\n        object.userData.onClick(intersects[0]);\n      }\n    }\n  }\n\n  addClickable(object, callback) {\n    this.clickables.push(object);\n    object.userData.onClick = callback;\n  }\n\n  dispose() {\n    \u002F\u002F Remove event listeners\n  }\n}\n\n\u002F\u002F Usage\nconst interaction = new InteractionManager(camera, renderer, scene);\ninteraction.addClickable(mesh, (intersect) => {\n  console.log(\"Clicked at:\", intersect.point);\n});\n```\n\n## Performance Tips\n\n1. **Limit raycasts**: Throttle mousemove handlers\n2. **Use layers**: Filter raycast targets\n3. **Simple collision meshes**: Use invisible simpler geometry for raycasting\n4. **Disable controls when not needed**: `controls.enabled = false`\n5. **Batch updates**: Group interaction checks\n\n```javascript\n\u002F\u002F Use simpler geometry for raycasting\nconst complexMesh = loadedModel;\nconst collisionMesh = new THREE.Mesh(\n  new THREE.BoxGeometry(1, 1, 1),\n  new THREE.MeshBasicMaterial({ visible: false }),\n);\ncollisionMesh.userData.target = complexMesh;\nclickables.push(collisionMesh);\n```\n\n## See Also\n\n- `threejs-fundamentals` - Camera and scene setup\n- `threejs-animation` - Animating interactions\n- `threejs-shaders` - Visual feedback effects\n",{"name":5,"risk":24,"source":33,"description":7},"main","7a38d8e7073e61c16eda85681cb32fb68a8657a5","skills-store\u002Fglobal\u002Fthreejs-interaction","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router","https:\u002F\u002Fgithub.com\u002Fsapsapshen","2026-04-14 21:12:12",{"source":8,"skillName":5,"commands":51,"prompt":61,"zipAvailable":62},[52,55,58],{"tool":53,"command":54},"npx","npx skills add sapsapshen\u002Fai-memory-router --skill threejs-interaction",{"tool":56,"command":57},"bunx","bunx skills add sapsapshen\u002Fai-memory-router --skill threejs-interaction",{"tool":59,"command":60},"pnpm","pnpm dlx skills add sapsapshen\u002Fai-memory-router --skill threejs-interaction","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fglobal\u002Fthreejs-interaction\u002FSKILL.md",true,[],[65,89,114,124,137,156],{"skillNo":66,"name":67,"nameZh":6,"description":68,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":69,"categories":70,"keywords":76,"publishedAt":39},"01M71ATQRKG48SH06FQ697T5G9","wechat-search","Search WeChat Official Account articles using OpenClaw's web search and fetch capabilities with compliance-focused design.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fwechat-search-release\u002FSKILL.md",[71],{"categoryNo":72,"categoryName":73,"taxonomyType":74,"taxonomyTypeDesc":75},"01JBSEEDCAT000000000000037","律师与法务","PROFESSION","适用职业",[77,78,79,80,81,82,83,84,85,86,87,88],"wechat search","wechat","search","capabilities","openclaw","design","web search","compliance","web","official","fetch","with",{"skillNo":90,"name":91,"nameZh":6,"description":92,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":93,"categories":94,"keywords":95,"publishedAt":39},"01M71ATNAVGZB35PFYM2GC5N00","trend-tap","Real-time trending topics aggregator across 7 platforms (X\u002FTwitter, Reddit, Google Trends, Hacker News, Zhihu, Bilibili, Weibo). Trigger: when user says 'trend', 'trends', 'trend tap', '热点', '热搜', or asks what is trending\u002Fhot. Do NOT use web_search — this skill already fetches live data from all platforms with zero API keys.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftrend-tap\u002FSKILL.md",[],[96,97,98,99,100,101,79,102,103,104,105,22,106,107,108,109,110,111,112,113],"trend","trend tap","tap","api keys","Bash","time","trending","hacker","aggregator","do","brew","live","data","google","install","read","across","api",{"skillNo":115,"name":116,"nameZh":6,"description":117,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":118,"categories":119,"keywords":120,"publishedAt":39},"01M71ATJ9AM5KM15AA8PS302EB","trend-radar","Real-time trending topics aggregator across 7 platforms (X\u002FTwitter, Reddit, Google Trends, Hacker News, Zhihu, Bilibili, Weibo). Trigger: when user says 'trend', 'trends', 'trend pulse', '热点', '热搜', or asks what is trending\u002Fhot. Do NOT use web_search — this skill already fetches live data from all platforms with zero API keys.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftrend-radar\u002FSKILL.md",[],[121,122,96,106,107,108,109,100,79,111,123,112,113,22,104,103,102,110,101,99],"trend radar","radar","pulse",{"skillNo":125,"name":126,"nameZh":6,"description":127,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":128,"categories":129,"keywords":130,"publishedAt":39},"01M71ATGF095N3EJM9G8SD0BB5","tech-news-daily","每日科技新闻汇——从全球多个 RSS 源抓取最新科技新闻，翻译（英→中）、去重、排版成每日简报。当用户询问科技新闻、科技新鲜事、每日科技、技术动态、Daily Tech News 或类似表达时触发此技能。","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftech-news-daily\u002FSKILL.md",[],[131,132,133,134,135,136],"news","tech","tech news daily","daily","rss","翻译",{"skillNo":138,"name":139,"nameZh":6,"description":140,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":141,"categories":142,"keywords":148,"publishedAt":39},"01M71ATFAEAMQ17M70NQNY7SR8","summarize","Summarize URLs or files with the summarize CLI (web, PDFs, images, audio, YouTube).","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fsummarize\u002FSKILL.md",[143],{"categoryNo":144,"categoryName":145,"taxonomyType":146,"taxonomyTypeDesc":147},"01JBSEEDCAT000000000000021","资料检索与研究","SCENARIO","使用场景",[139,85,149,150,151,88,106,152,153,154,98,155,110],"Audio","cli","youtube","images","https","files","pdfs",{"skillNo":157,"name":158,"nameZh":6,"description":159,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":160,"categories":161,"keywords":162,"publishedAt":39},"01M71ATDNS3R0M4H936PACQAWZ","sonoscli","Control Sonos speakers (discover\u002Fstatus\u002Fplay\u002Fvolume\u002Fgroup).","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fsonoscli\u002FSKILL.md",[],[158,163,164,165,110,166,153,167,168,169,170,171],"status","play","go","latest","github","discover","control","cmd","volume"]