From bdd2c6e56b43c1979bf7e40b55ce88704f663764 Mon Sep 17 00:00:00 2001 From: Kaktus Klaus Date: Mon, 2 Mar 2026 17:37:32 +0000 Subject: [PATCH 1/6] feat(skill): expand needle-engine skill with API reference, troubleshooting, and template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SKILL.md: add Quick Start, When to Use, Unity→Needle cheat sheet table, missing lifecycle methods (lateUpdate, fixedUpdate, collision/trigger callbacks), coroutines, instantiate/destroy, animation API, runtime asset loading, custom events, TypeScript config gotcha, expanded troubleshooting (6→11 items), needle_search usage examples, allowed-tools frontmatter - Add references/api.md: full lifecycle, decorator, context, coroutine, animation, asset loading, networking, and XR API reference - Add references/troubleshooting.md: 11 common issues with causes + fixes (missing @registerType, useDefineForClassFields, sync, physics, XR, perf) - Add templates/my-component.ts: annotated starter component template Co-Authored-By: Claude Sonnet 4.6 --- .../plugin/skills/needle-engine/SKILL.md | 303 +++++++++++++++--- .../skills/needle-engine/references/api.md | 241 ++++++++++++++ .../references/troubleshooting.md | 163 ++++++++++ .../needle-engine/templates/my-component.ts | 87 +++++ 4 files changed, 745 insertions(+), 49 deletions(-) create mode 100644 providers/claude/plugin/skills/needle-engine/references/api.md create mode 100644 providers/claude/plugin/skills/needle-engine/references/troubleshooting.md create mode 100644 providers/claude/plugin/skills/needle-engine/templates/my-component.ts diff --git a/providers/claude/plugin/skills/needle-engine/SKILL.md b/providers/claude/plugin/skills/needle-engine/SKILL.md index 7f201f9..0d37d0e 100644 --- a/providers/claude/plugin/skills/needle-engine/SKILL.md +++ b/providers/claude/plugin/skills/needle-engine/SKILL.md @@ -1,13 +1,61 @@ --- name: needle-engine description: Automatically provides Needle Engine context when working in a Needle Engine web project. Use this skill when editing TypeScript components, Vite config, GLB assets, or anything related to @needle-tools/engine. +allowed-tools: + - needle_search + - Read + - Write + - Edit + - Bash --- # Needle Engine You are an expert in Needle Engine — a web-first 3D engine built on Three.js with a Unity/Blender-based workflow. -## Key concepts +## When to Use This Skill + +**Use when the user is:** +- Editing TypeScript files that import from `@needle-tools/engine` +- Working on a project with `vite.config.ts` that uses `needlePlugins` +- Loading or debugging `.glb` files in the browser +- Asking about component lifecycle, serialization, XR, networking, or deployment + +**Do NOT use for:** +- Pure Three.js projects with no Needle Engine +- Non-web Unity/Blender work with no GLB export + +--- + +## Quick Start + +```html + + + +``` + +Minimal TypeScript component: +```ts +import { Behaviour, serializable, registerType } from "@needle-tools/engine"; + +@registerType +export class HelloWorld extends Behaviour { + @serializable() message: string = "Hello!"; + + start() { + console.log(this.message); + } +} +``` + +> ⚠️ **TypeScript config required:** `tsconfig.json` must have `"experimentalDecorators": true` and `"useDefineForClassFields": false` for decorators to work. + +--- + +## Key Concepts **Needle Engine** ships 3D scenes from Unity (or Blender) as GLB files and renders them in the browser using Three.js. TypeScript components attached to GameObjects in Unity are serialized into the GLB and re-hydrated at runtime in the browser. @@ -18,7 +66,12 @@ You are an expert in Needle Engine — a web-first 3D engine built on Three.js w ``` Access the context programmatically: `document.querySelector("needle-engine").context` -### Component lifecycle (mirrors Unity MonoBehaviour) +--- + +## Component Lifecycle + +Mirrors Unity MonoBehaviour exactly: + ```ts import { Behaviour, serializable, registerType } from "@needle-tools/engine"; @@ -26,46 +79,139 @@ import { Behaviour, serializable, registerType } from "@needle-tools/engine"; export class MyComponent extends Behaviour { @serializable() myValue: number = 1; - awake() {} // called once when instantiated - start() {} // called once on first frame - update() {} // called every frame - onEnable() {} - onDisable() {} - onDestroy() {} + awake() {} // once on instantiate (before Start) + start() {} // once on first frame + update() {} // every frame + lateUpdate() {} // every frame, after all update() calls + fixedUpdate() {} // fixed timestep (physics) + onEnable() {} // when component/GO becomes active + onDisable() {} // when component/GO becomes inactive + onDestroy() {} // on removal onBeforeRender(_frame: XRFrame | null) {} } ``` -### Serialization +### Physics Callbacks (requires Rapier collider on GameObject) +```ts +onCollisionEnter(col: Collision) {} +onCollisionStay(col: Collision) {} +onCollisionExit(col: Collision) {} +onTriggerEnter(col: Collision) {} +onTriggerStay(col: Collision) {} +onTriggerExit(col: Collision) {} +``` + +### Coroutines +```ts +start() { + this.startCoroutine(this.myRoutine()); +} + +*myRoutine() { + console.log("Start"); + yield; // wait one frame + // yield WaitForSeconds(2); // wait N seconds + console.log("One frame later"); +} +``` + +--- + +## Serialization + - `@registerType` — makes the class discoverable by the GLB deserializer - `@serializable()` — marks a field for GLB deserialization (primitives) - `@serializable(Object3D)` — for Three.js object references - `@serializable(Texture)` — for textures (import Texture from "three") - `@serializable(RGBAColor)` — for colors +- `@serializable(AssetReference)` — for lazily-loaded GLB assets + +--- + +## Accessing the Scene -### Accessing the scene ```ts this.context.scene // THREE.Scene this.context.mainCamera // active camera (THREE.Camera) this.context.renderer // THREE.WebGLRenderer -this.context.time.frame // current frame number +this.context.time.frame // current frame number this.context.time.deltaTime // seconds since last frame -this.gameObject // the THREE.Object3D this component is on +this.gameObject // the THREE.Object3D this component is on ``` -### Finding components +--- + +## Finding Components + ```ts this.gameObject.getComponent(MyComponent) this.gameObject.getComponentInChildren(MyComponent) this.context.scene.getComponentInChildren(MyComponent) -// Global search (import as standalone functions from "@needle-tools/engine") +// Global search import { findObjectOfType, findObjectsOfType } from "@needle-tools/engine"; findObjectOfType(MyComponent, this.context) findObjectsOfType(MyComponent, this.context) ``` -### Input handling +--- + +## Instantiate & Destroy + +```ts +import { GameObject, Instantiate } from "@needle-tools/engine"; + +// Clone an object (like Unity Instantiate) +const clone = GameObject.instantiate(this.gameObject); +// With position/rotation: +const clone2 = GameObject.instantiate(prefab, { position: new Vector3(1,0,0) }); + +// Destroy +GameObject.destroy(clone); // removes object + calls onDestroy on all components +this.gameObject.removeComponent(comp); // removes component (does NOT call onDestroy) +``` + +--- + +## Animation + +```ts +import { Animator } from "@needle-tools/engine"; + +const animator = this.gameObject.getComponent(Animator); +animator?.play("Run"); // play by clip name +animator?.crossFade("Walk", 0.25); // cross-fade transition +animator?.setFloat("Speed", 1.5); // Animator parameter +animator?.setBool("IsJumping", true); +``` + +--- + +## Loading Assets at Runtime + +```ts +import { AssetReference } from "@needle-tools/engine"; + +@registerType +export class LazyLoader extends Behaviour { + @serializable(AssetReference) sceneRef!: AssetReference; + + async loadIt() { + const instance = await this.sceneRef.instantiate({ parent: this.gameObject }); + } +} +``` + +Or load a raw URL: +```ts +import { NeedleEngine } from "@needle-tools/engine"; +NeedleEngine.addContextCreatedCallback((ctx) => { /* scene ready */ }); +``` + +--- + +## Input Handling + ```ts // Polling if (this.context.input.getPointerDown(0)) { /* pointer pressed */ } @@ -75,17 +221,33 @@ if (this.context.input.getKeyDown("Space")) { /* space pressed */ } this.gameObject.addEventListener("pointerdown", (e: NEPointerEvent) => { }); ``` -### Physics & raycasting +### Custom Events Between Components +```ts +// Dispatch +this.gameObject.dispatchEvent(new CustomEvent("myEvent", { detail: { score: 42 } })); + +// Listen from another component +other.gameObject.addEventListener("myEvent", (e: CustomEvent) => { + console.log(e.detail.score); +}); +``` + +--- + +## Physics & Raycasting + ```ts // Default raycasts hit visible geometry — no colliders needed -// Uses mesh BVH (bounding volume hierarchy) for accelerated raycasting, BVH is generated on a worker const hits = this.context.physics.raycast(); // Physics-based raycasts (require colliders, uses Rapier physics engine) const physicsHits = this.context.physics.raycastPhysics(); ``` -### Networking & multiplayer +--- + +## Networking & Multiplayer + Needle Engine has built-in multiplayer. Add a `SyncedRoom` component to enable networking. - `@syncField()` — automatically syncs a field across all connected clients @@ -94,16 +256,43 @@ Needle Engine has built-in multiplayer. Add a `SyncedRoom` component to enable n - Key components: `SyncedRoom`, `SyncedTransform`, `PlayerSync`, `Voip` - Uses WebSockets + optional WebRTC peer-to-peer connections -### WebXR (VR & AR) +--- + +## WebXR (VR & AR) + Needle Engine has built-in WebXR support for VR and AR across Meta Quest, Apple Vision Pro, and mobile AR. - Add the `WebXR` component to enable VR/AR sessions - Use `XRRig` to define the user's starting position — the user is parented to the rig during XR sessions - Available components: `WebXRImageTracking`, `WebXRPlaneTracking`, `XRControllerModel`, `NeedleXRSession` -## Creating a new project +--- + +## Unity → Needle Cheat Sheet + +| Unity (C#) | Needle Engine (TypeScript) | +|---|---| +| `MonoBehaviour` | `Behaviour` | +| `[SerializeField]` | `@serializable()` | +| `[RequireComponent]` | `@requireComponent(Type)` | +| `Instantiate(prefab)` | `GameObject.instantiate(obj)` | +| `Destroy(obj)` | `GameObject.destroy(obj)` | +| `GetComponent()` | `this.gameObject.getComponent(T)` | +| `FindObjectOfType()` | `findObjectOfType(T, ctx)` | +| `transform.position` | `this.gameObject.position` | +| `transform.rotation` | `this.gameObject.quaternion` | +| `Resources.Load()` | `@serializable(AssetReference)` | +| `StartCoroutine()` | `this.startCoroutine()` | +| `Time.deltaTime` | `this.context.time.deltaTime` | +| `Camera.main` | `this.context.mainCamera` | +| `Debug.Log()` | `console.log()` | +| `OnCollisionEnter()` | `onCollisionEnter(col: Collision)` | +| `OnTriggerEnter()` | `onTriggerEnter(col: Collision)` | + +--- + +## Creating a New Project -Use `create-needle` to scaffold a new Needle Engine project: ```bash npm create needle my-app # default Vite template npm create needle my-app -t react # React template @@ -112,11 +301,9 @@ npm create needle my-app -t vue # Vue.js template Available templates: `vite` (default), `react`, `vue`, `sveltekit`, `svelte`, `nextjs`, `react-three-fiber`. -Use `npm create needle --list` to see all available templates. - -## Vite plugin system +--- -Needle Engine ships a set of Vite plugins via `needlePlugins(command, config, userSettings)`. Custom project plugins go in `vite.config.ts`. +## Vite Plugin System ```ts import { defineConfig } from "vite"; @@ -129,47 +316,65 @@ export default defineConfig(async ({ command }) => ({ })); ``` +--- + ## Deployment -Projects can be deployed to: -- **Needle Cloud** — official hosting with automatic optimization (`npx needle-cloud deploy`) -- **Vercel** / **Netlify** — standard web hosting -- **itch.io** — for games and interactive experiences -- **Any static host** — Needle Engine projects are standard Vite web apps +- **Needle Cloud** — `npx needle-cloud deploy` +- **Vercel / Netlify** — standard Vite web app +- **itch.io** — for games +- **Any static host** — `npm run build` produces a standard dist folder From Unity, use built-in deployment components (e.g. `DeployToNeedleCloud`, `DeployToNetlify`). -## Progressive loading (`@needle-tools/gltf-progressive`) +--- -Needle Engine includes `@needle-tools/gltf-progressive` for progressive streaming of 3D models and textures. It creates a tiny initial file with embedded low-quality proxy geometry, then streams higher-quality LODs on demand. Results in ~90% smaller initial downloads with instant display. +## Progressive Loading (`@needle-tools/gltf-progressive`) -Works standalone with any three.js project: ```ts -import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; -import { WebGLRenderer } from "three"; import { useNeedleProgressive } from "@needle-tools/gltf-progressive"; +useNeedleProgressive(gltfLoader, renderer); +gltfLoader.load(url, (gltf) => scene.add(gltf.scene)); +``` -const gltfLoader = new GLTFLoader(); -const renderer = new WebGLRenderer(); +In Needle Engine projects this is built in — configure via **Compression & LOD Settings** in Unity. -// Register once — progressive loading happens automatically for all subsequent loads -useNeedleProgressive(gltfLoader, renderer); +--- -gltfLoader.load(url, (gltf) => scene.add(gltf.scene)); +## Searching the Documentation + +Use the `needle_search` MCP tool to find relevant docs, forum posts, and community answers: + +``` +needle_search("how to play animation clip from code") +needle_search("SyncedTransform multiplayer") +needle_search("deploy to Needle Cloud CI") ``` -In Needle Engine projects, progressive loading is built in and can be configured via the **Compression & LOD Settings** component in Unity. +Use this *before* guessing at API details — the docs are the source of truth. + +--- + +## Common Gotchas + +- `@registerType` is required or the component won't be instantiated from GLB (Unity/Blender export adds this automatically, but hand-written components need it) +- GLB assets go in `assets/`, static files (fonts, images) in `public/` +- `useDefineForClassFields: false` must be set in `tsconfig.json` — otherwise decorators silently break field initialization +- `@syncField()` only triggers on reassignment — mutating an array/object in place won't sync; do `this.arr = this.arr` +- Physics callbacks (`onCollisionEnter` etc.) require a Rapier `Collider` component on the GameObject +- `removeComponent()` does NOT call `onDestroy` — use `GameObject.destroy()` for full cleanup + +--- + +## References + +- 📖 [Full API Reference](references/api.md) — complete lifecycle, decorators, and context API +- 🐛 [Troubleshooting](references/troubleshooting.md) — common errors and fixes +- 🧩 [Component Template](templates/my-component.ts) — annotated starter component ## Important URLs + - Docs: https://engine.needle.tools/docs/ - Samples: https://engine.needle.tools/samples/ - GitHub: https://github.com/needle-tools/needle-engine-support - npm: https://www.npmjs.com/package/@needle-tools/engine - -## Searching the documentation - -Use the `needle_search` MCP tool to find relevant docs, forum posts, and community answers. - -## Common gotchas -- Components must use `@registerType` or they won't be instantiated from GLB (this is handled automatically when exporting from Unity or Blender, but must be added manually for hand-written components) -- GLB assets are in `assets/`, static files in `include/` or `public/` diff --git a/providers/claude/plugin/skills/needle-engine/references/api.md b/providers/claude/plugin/skills/needle-engine/references/api.md new file mode 100644 index 0000000..249457d --- /dev/null +++ b/providers/claude/plugin/skills/needle-engine/references/api.md @@ -0,0 +1,241 @@ +# Needle Engine — Full API Reference + +## Lifecycle Methods (complete) + +All methods are optional — only implement what you need. + +```ts +class MyComponent extends Behaviour { + // Initialization + awake() // first, before Start, even if disabled + onEnable() // whenever component/GO becomes active + start() // once, on first enabled frame + + // Per-frame + update() // every frame + lateUpdate() // every frame, after all update() runs + fixedUpdate() // fixed timestep (default 50Hz, used for physics) + onBeforeRender(frame: XRFrame | null) // just before Three.js renders + + // Deactivation / cleanup + onDisable() // when component/GO becomes inactive + onDestroy() // called by GameObject.destroy() — NOT by removeComponent() + + // Physics (requires Rapier Collider component on same GameObject) + onCollisionEnter(col: Collision) + onCollisionStay(col: Collision) + onCollisionExit(col: Collision) + onTriggerEnter(col: Collision) + onTriggerStay(col: Collision) + onTriggerExit(col: Collision) +} +``` + +--- + +## Decorators + +| Decorator | Purpose | +|---|---| +| `@registerType` | Required on every component — registers the class for GLB deserialization | +| `@serializable()` | Serialize/deserialize a primitive (number, string, boolean) | +| `@serializable(Type)` | Serialize/deserialize a typed field (Object3D, Texture, Color, etc.) | +| `@syncField()` | Auto-sync field over the network in a SyncedRoom | +| `@syncField(onChange)` | Sync + call a callback when value changes remotely | +| `@requireComponent(Type)` | Ensure a component of Type exists on the same GO | + +### Serializable Types (import from correct source) + +```ts +import { RGBAColor, AssetReference } from "@needle-tools/engine"; +import { Object3D, Texture, Vector2, Vector3, Color } from "three"; + +@serializable(Object3D) myRef!: Object3D; +@serializable(Texture) tex!: Texture; +@serializable(RGBAColor) col!: RGBAColor; +@serializable(AssetReference) asset!: AssetReference; +@serializable(Vector3) pos!: Vector3; +``` + +--- + +## Context API (`this.context`) + +```ts +this.context.scene // THREE.Scene +this.context.mainCamera // THREE.Camera (currently active) +this.context.renderer // THREE.WebGLRenderer +this.context.domElement // HTML element + +// Time +this.context.time.frame // frame counter (number) +this.context.time.deltaTime // seconds since last frame +this.context.time.time // total elapsed seconds +this.context.time.realtimeSinceStartup + +// Input +this.context.input.getPointerDown(index) // pointer just pressed +this.context.input.getPointerUp(index) // pointer just released +this.context.input.getPointerPressed(index) // pointer held +this.context.input.getPointerPosition(index) // {x, y} in screen pixels +this.context.input.getKeyDown(key) // "Space", "ArrowLeft", "a", etc. +this.context.input.getKeyUp(key) +this.context.input.getKeyPressed(key) + +// Physics +this.context.physics.raycast() // hits visible geometry (no collider needed) +this.context.physics.raycastPhysics() // hits Rapier colliders only +this.context.physics.engine // access Rapier world directly + +// Network +this.context.connection // active network connection (if SyncedRoom present) +``` + +--- + +## GameObject Utilities + +```ts +import { GameObject } from "@needle-tools/engine"; + +// Component access +go.getComponent(Type) +go.getComponentInChildren(Type) +go.getComponentInParent(Type) +go.getComponents(Type) // all matching on same GO +go.getComponentsInChildren(Type) + +// Lifecycle +GameObject.instantiate(source, opts?) // clone; opts: { position, rotation, parent } +GameObject.destroy(obj) // destroys GO + calls onDestroy on components +obj.removeComponent(comp) // removes without calling onDestroy + +// Active state +go.visible = false // hides in scene (still ticks) +GameObject.setActive(go, false) // disables lifecycle callbacks + +// Tag / name +go.name // string +go.userData.tags // string[] (set from Unity via Tag component) +``` + +--- + +## Finding Objects + +```ts +import { findObjectOfType, findObjectsOfType, findObjectByName } from "@needle-tools/engine"; + +findObjectOfType(MyComponent, ctx) // first match in scene +findObjectsOfType(MyComponent, ctx) // all matches +findObjectByName("Player", ctx.scene) // by object name +``` + +--- + +## Coroutines + +Generator functions that can yield across frames: + +```ts +import { WaitForSeconds } from "@needle-tools/engine"; + +start() { + this.startCoroutine(this.flashLight()); +} + +*flashLight() { + while (true) { + this.light.visible = !this.light.visible; + yield WaitForSeconds(0.5); // wait 0.5 seconds + // yield; // wait exactly one frame + } +} + +// Stop all coroutines on this component: +this.stopAllCoroutines(); +``` + +--- + +## Animation + +```ts +import { Animator, AnimationClip } from "@needle-tools/engine"; + +const anim = this.gameObject.getComponent(Animator); + +anim.play("Run"); // play by state name +anim.crossFade("Walk", 0.3); // cross-fade over 0.3s +anim.setFloat("Speed", 1.5); +anim.setInteger("State", 2); +anim.setBool("IsGrounded", true); +anim.setTrigger("Jump"); +anim.speed = 0.5; // global playback speed multiplier +``` + +--- + +## Asset Loading at Runtime + +```ts +import { AssetReference } from "@needle-tools/engine"; + +// Declare in component (set in Unity Inspector) +@serializable(AssetReference) prefab!: AssetReference; + +async start() { + // Load and instantiate + const instance = await this.prefab.instantiate({ parent: this.gameObject }); + + // Or just load the GLTF (no instantiate) + const gltf = await this.prefab.loadAssetAsync(); +} +``` + +Load a URL directly: +```ts +import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; +const loader = new GLTFLoader(); +const gltf = await loader.loadAsync("assets/extra.glb"); +this.context.scene.add(gltf.scene); +``` + +--- + +## Networking + +```ts +import { SyncedRoom, SyncedTransform } from "@needle-tools/engine"; + +// In a component with a SyncedRoom in the scene: +@syncField() score: number = 0; // auto-syncs on change + +// Complex type — must reassign to trigger sync: +@syncField() items: string[] = []; +this.items.push("sword"); +this.items = this.items; // ← triggers sync + +// Low-level RPC: +this.context.connection.send("my-event", { data: 42 }); +this.context.connection.beginListen("my-event", (msg) => { console.log(msg.data); }); +``` + +--- + +## WebXR + +```ts +import { WebXR, XRRig, NeedleXRSession } from "@needle-tools/engine"; + +// In code: check XR state +if (this.context.xr?.isInXR) { ... } +this.context.xr?.session // XRSession + +// XR controller input +onBeforeRender(frame: XRFrame | null) { + if (!frame) return; + const controllers = this.context.xr?.controllers; + // controllers[0].gamepad, controllers[0].raycastHit, etc. +} +``` diff --git a/providers/claude/plugin/skills/needle-engine/references/troubleshooting.md b/providers/claude/plugin/skills/needle-engine/references/troubleshooting.md new file mode 100644 index 0000000..341ef09 --- /dev/null +++ b/providers/claude/plugin/skills/needle-engine/references/troubleshooting.md @@ -0,0 +1,163 @@ +# Needle Engine — Troubleshooting + +## Component Not Instantiated from GLB + +**Symptom:** Component exists in Unity/Blender scene but `getComponent(MyComponent)` returns null at runtime. + +**Causes & fixes:** +1. **Missing `@registerType`** — Every component class must have `@registerType` above the class declaration. Without it the GLB deserializer can't match the class name to the serialized data. +2. **Class not imported** — The file containing the class must be imported somewhere in your entry point (`main.ts`). Tree-shaking can eliminate unreferenced classes. +3. **Name mismatch** — The C# class name in Unity must exactly match the TypeScript class name. Check for typos. +4. **Wrong namespace** — If the Unity C# class is in a namespace, the TypeScript class must match (or the codegen mapping must be set up). + +```ts +// ✅ Correct +@registerType +export class MyComponent extends Behaviour { ... } + +// ❌ Wrong — missing @registerType +export class MyComponent extends Behaviour { ... } +``` + +--- + +## Decorators Not Working / Fields Always Undefined + +**Symptom:** `@serializable` fields are always their default TypeScript values; deserialized values never appear. + +**Fix:** Check `tsconfig.json`: +```json +{ + "compilerOptions": { + "experimentalDecorators": true, + "useDefineForClassFields": false // ← CRITICAL — must be false + } +} +``` + +`useDefineForClassFields: true` (the TS5+ default) causes class field initializers to run *after* decorators, overwriting deserialized values. + +--- + +## GLB Not Loading / Scene Is Empty + +**Checklist:** +1. Is the `src` path on `` correct? Paths are relative to the HTML file. +2. Is the file in `assets/` (not `src/` or `public/`)? Static assets belong in `assets/` for Vite to copy them. +3. Check browser console for 404 errors on the GLB request. +4. If the file exists but scene is empty: check if the root object is active in Unity before export. +5. CORS issues when loading from a different origin — serve from the same host or configure CORS headers. + +--- + +## `@syncField` Not Syncing + +**Symptom:** Field changes locally but other clients don't see updates. + +**Causes:** +1. **No `SyncedRoom`** in the scene — networking requires a `SyncedRoom` component. +2. **Mutating array/object in place** — `this.arr.push(x)` does NOT trigger sync. You must reassign: `this.arr = [...this.arr, x]` or `this.arr = this.arr`. +3. **Missing `@registerType`** on the component — sync relies on class registration. +4. **Not connected** — check `this.context.connection.isConnected`. + +--- + +## Physics Callbacks Never Fire + +**Symptom:** `onCollisionEnter`, `onTriggerEnter`, etc. never called. + +**Requirements:** +- Rapier physics must be active — add a `Rigidbody` or `Collider` component in Unity on both objects +- The GameObject must have a `Collider` component (Box, Sphere, Mesh, etc.) +- For trigger events, the collider must be set to **Is Trigger** in Unity +- Both objects need Rapier colliders — mesh-only objects don't participate in physics events + +--- + +## `onDestroy` Not Called When Removing Component + +**By design:** `this.gameObject.removeComponent(comp)` removes the component from update loops but does **not** call `onDestroy`. This is consistent with Unity's `DestroyImmediate` on a component (vs the object). + +**Fix:** Use `GameObject.destroy(go)` to fully clean up an object and all its components. If you need cleanup on component removal specifically, call `onDestroy()` manually before `removeComponent()`. + +--- + +## Animation Not Playing + +**Checklist:** +1. `Animator` component must be on the same or parent GameObject +2. State name must match exactly what's in the Unity AnimatorController +3. Check that `animator.runtimeAnimatorController` is set (not null) +4. If calling `play()` in `awake()`, try `start()` instead — the animator may not be initialized yet + +--- + +## Vite Build Fails with Decorator Errors + +Typical error: `Experimental support for decorators is a feature that is subject to change` + +**Fix:** Ensure `tsconfig.json` has: +```json +"experimentalDecorators": true +``` + +And verify that `vite.config.ts` uses the Needle plugins (they configure esbuild/swc for decorator support automatically): +```ts +import { needlePlugins } from "@needle-tools/engine/vite"; +``` + +--- + +## TypeScript Errors on `this.context` or `this.gameObject` + +**Symptom:** TS error: Property 'context' does not exist on type 'MyComponent' + +**Fix:** Make sure you extend `Behaviour` (not `Component` or a plain class): +```ts +import { Behaviour } from "@needle-tools/engine"; +export class MyComponent extends Behaviour { ... } +``` + +--- + +## XR Session Doesn't Start + +**Checklist:** +1. Must be served over **HTTPS** (or localhost) — WebXR is blocked on plain HTTP +2. `WebXR` component must be in the scene (added in Unity or created in TS) +3. Device must support WebXR — test with [WebXR Emulator](https://chrome.google.com/webstore/detail/webxr-api-emulator) in Chrome +4. Check browser console for XR-related permission errors + +--- + +## Performance: Frame Rate Drop + +**Common causes:** +- Per-frame `new Vector3()` / `new THREE.Color()` allocations — reuse objects +- `getComponent()` called every frame — cache the result in `start()` +- `findObjectOfType()` called every frame — very slow, use `start()` or events +- Too many draw calls — use instancing or merge geometries in Unity before export +- Large uncompressed textures — enable **Texture Compression** in Unity Needle settings + +```ts +// ❌ Bad — allocates every frame +update() { + const pos = new Vector3(1, 0, 0); + this.gameObject.position.copy(pos); +} + +// ✅ Good — reuse +private _pos = new Vector3(1, 0, 0); +update() { + this.gameObject.position.copy(this._pos); +} +``` + +--- + +## Getting More Help + +- Search docs: `needle_search("your question here")` +- [Needle Engine Docs](https://engine.needle.tools/docs/) +- [Community Forum / GitHub Discussions](https://github.com/needle-tools/needle-engine-support/discussions) +- [Discord](https://discord.needle.tools) diff --git a/providers/claude/plugin/skills/needle-engine/templates/my-component.ts b/providers/claude/plugin/skills/needle-engine/templates/my-component.ts new file mode 100644 index 0000000..b0aea0f --- /dev/null +++ b/providers/claude/plugin/skills/needle-engine/templates/my-component.ts @@ -0,0 +1,87 @@ +/** + * Needle Engine — Annotated Component Template + * + * Copy this file and rename the class to get started. + * Remove any lifecycle methods you don't need. + */ + +import { Behaviour, serializable, registerType, GameObject } from "@needle-tools/engine"; +import { Object3D } from "three"; + +// @registerType — required for GLB deserialization. +// Without this, the component won't be instantiated from GLB. +@registerType +export class MyComponent extends Behaviour { + + // --------------------------------------------------------------------------- + // Serialized Fields + // These values are set in the Unity Inspector and baked into the GLB. + // --------------------------------------------------------------------------- + + /** A simple number field — set in Unity Inspector */ + @serializable() + speed: number = 1; + + /** A reference to another object in the scene */ + @serializable(Object3D) + target?: Object3D; + + // --------------------------------------------------------------------------- + // Private State + // --------------------------------------------------------------------------- + + private _elapsed: number = 0; + + // --------------------------------------------------------------------------- + // Lifecycle + // --------------------------------------------------------------------------- + + awake() { + // Called once when the component is instantiated (even if disabled). + // Use for initialization that doesn't depend on other components being ready. + } + + start() { + // Called once on the first frame the component is active. + // Other components are initialized by now — safe to call getComponent() etc. + console.log(`${this.name} started on ${this.gameObject.name}`); + } + + update() { + // Called every frame. Use this.context.time.deltaTime for frame-rate independence. + this._elapsed += this.context.time.deltaTime; + + // Example: rotate this object + this.gameObject.rotation.y += this.speed * this.context.time.deltaTime; + } + + onEnable() { + // Called each time this component becomes active. + } + + onDisable() { + // Called each time this component becomes inactive. + } + + onDestroy() { + // Called when this component/object is destroyed (via GameObject.destroy()). + // Note: NOT called by removeComponent() — only by destroy(). + // Clean up event listeners, timers, or external references here. + } + + // --------------------------------------------------------------------------- + // Physics callbacks (require a Rapier Collider on this GameObject) + // --------------------------------------------------------------------------- + + // onCollisionEnter(col: Collision) { } + // onTriggerEnter(col: Collision) { } + + // --------------------------------------------------------------------------- + // Example: coroutine + // --------------------------------------------------------------------------- + + private *exampleCoroutine() { + // yield; // wait one frame + // yield WaitForSeconds(1); // wait 1 second + } +} From 68e687e0cf1016c7cd4ba9468baee1906be44b48 Mon Sep 17 00:00:00 2001 From: Kaktus Klaus Date: Mon, 2 Mar 2026 17:55:33 +0000 Subject: [PATCH 2/6] fix(skill): correct API inaccuracies found in self-review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Physics callbacks and coroutines: add "add inside your class" label (were shown outside class body, which was confusing) - Instantiate section: remove dead `Instantiate` import (only used `GameObject.instantiate()` in the example) - Animation: remove `crossFade()` and `setInteger()` which may not exist in Needle Engine's Animator; remove dead `AnimationClip` import - Loading assets: replace incorrect `NeedleEngine.addContextCreatedCallback` (wrong use — it fires on context creation, not asset load) with correct `GLTFLoader.loadAsync()` pattern - api.md: replace nonexistent `findObjectByName` with Three.js built-in `scene.getObjectByName()` Co-Authored-By: Claude Sonnet 4.6 --- .../plugin/skills/needle-engine/SKILL.md | 21 ++++++++++++------- .../skills/needle-engine/references/api.md | 18 +++++++--------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/providers/claude/plugin/skills/needle-engine/SKILL.md b/providers/claude/plugin/skills/needle-engine/SKILL.md index 0d37d0e..7b40426 100644 --- a/providers/claude/plugin/skills/needle-engine/SKILL.md +++ b/providers/claude/plugin/skills/needle-engine/SKILL.md @@ -92,6 +92,8 @@ export class MyComponent extends Behaviour { ``` ### Physics Callbacks (requires Rapier collider on GameObject) + +Add inside your component class: ```ts onCollisionEnter(col: Collision) {} onCollisionStay(col: Collision) {} @@ -102,6 +104,8 @@ onTriggerExit(col: Collision) {} ``` ### Coroutines + +Add inside your component class: ```ts start() { this.startCoroutine(this.myRoutine()); @@ -159,7 +163,7 @@ findObjectsOfType(MyComponent, this.context) ## Instantiate & Destroy ```ts -import { GameObject, Instantiate } from "@needle-tools/engine"; +import { GameObject } from "@needle-tools/engine"; // Clone an object (like Unity Instantiate) const clone = GameObject.instantiate(this.gameObject); @@ -179,10 +183,10 @@ this.gameObject.removeComponent(comp); // removes component (does NOT call onDes import { Animator } from "@needle-tools/engine"; const animator = this.gameObject.getComponent(Animator); -animator?.play("Run"); // play by clip name -animator?.crossFade("Walk", 0.25); // cross-fade transition -animator?.setFloat("Speed", 1.5); // Animator parameter +animator?.play("Run"); // play by state name +animator?.setFloat("Speed", 1.5); // Animator parameters animator?.setBool("IsJumping", true); +animator?.setTrigger("Jump"); ``` --- @@ -202,10 +206,13 @@ export class LazyLoader extends Behaviour { } ``` -Or load a raw URL: +Or load a GLB URL directly using Three.js: ```ts -import { NeedleEngine } from "@needle-tools/engine"; -NeedleEngine.addContextCreatedCallback((ctx) => { /* scene ready */ }); +import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; + +const loader = new GLTFLoader(); +const gltf = await loader.loadAsync("assets/extra.glb"); +this.context.scene.add(gltf.scene); ``` --- diff --git a/providers/claude/plugin/skills/needle-engine/references/api.md b/providers/claude/plugin/skills/needle-engine/references/api.md index 249457d..134d2d0 100644 --- a/providers/claude/plugin/skills/needle-engine/references/api.md +++ b/providers/claude/plugin/skills/needle-engine/references/api.md @@ -124,11 +124,11 @@ go.userData.tags // string[] (set from Unity via Tag component) ## Finding Objects ```ts -import { findObjectOfType, findObjectsOfType, findObjectByName } from "@needle-tools/engine"; +import { findObjectOfType, findObjectsOfType } from "@needle-tools/engine"; -findObjectOfType(MyComponent, ctx) // first match in scene -findObjectsOfType(MyComponent, ctx) // all matches -findObjectByName("Player", ctx.scene) // by object name +findObjectOfType(MyComponent, ctx) // first match in scene +findObjectsOfType(MyComponent, ctx) // all matches +ctx.scene.getObjectByName("Player") // by name (Three.js built-in) ``` --- @@ -161,17 +161,15 @@ this.stopAllCoroutines(); ## Animation ```ts -import { Animator, AnimationClip } from "@needle-tools/engine"; +import { Animator } from "@needle-tools/engine"; const anim = this.gameObject.getComponent(Animator); -anim.play("Run"); // play by state name -anim.crossFade("Walk", 0.3); // cross-fade over 0.3s -anim.setFloat("Speed", 1.5); -anim.setInteger("State", 2); +anim.play("Run"); // play by state name +anim.setFloat("Speed", 1.5); // Animator parameters (match Unity parameter names) anim.setBool("IsGrounded", true); anim.setTrigger("Jump"); -anim.speed = 0.5; // global playback speed multiplier +anim.speed = 0.5; // global playback speed multiplier ``` --- From ca6e316ceb7d707219dcf65983c654d98af937d8 Mon Sep 17 00:00:00 2001 From: Kaktus Klaus Date: Mon, 2 Mar 2026 18:07:20 +0000 Subject: [PATCH 3/6] fix(skill): add Blender workflow, replace GLTFLoader with AssetReference - Add Unity/Blender workflow explanation (Blender addon, same TS workflow) - Update "When to Use" to mention Blender addon explicitly - Replace raw GLTFLoader.loadAsync() with AssetReference.getOrCreate() in both SKILL.md and references/api.md (per Needle Engine best practices) Co-Authored-By: Claude Sonnet 4.6 --- .../plugin/skills/needle-engine/SKILL.md | 20 ++++++++++++------- .../skills/needle-engine/references/api.md | 10 +++++----- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/providers/claude/plugin/skills/needle-engine/SKILL.md b/providers/claude/plugin/skills/needle-engine/SKILL.md index 7b40426..690925c 100644 --- a/providers/claude/plugin/skills/needle-engine/SKILL.md +++ b/providers/claude/plugin/skills/needle-engine/SKILL.md @@ -18,7 +18,8 @@ You are an expert in Needle Engine — a web-first 3D engine built on Three.js w **Use when the user is:** - Editing TypeScript files that import from `@needle-tools/engine` - Working on a project with `vite.config.ts` that uses `needlePlugins` -- Loading or debugging `.glb` files in the browser +- Loading or debugging `.glb` files exported from Unity or Blender +- Using the Needle Engine Blender addon - Asking about component lifecycle, serialization, XR, networking, or deployment **Do NOT use for:** @@ -57,7 +58,13 @@ export class HelloWorld extends Behaviour { ## Key Concepts -**Needle Engine** ships 3D scenes from Unity (or Blender) as GLB files and renders them in the browser using Three.js. TypeScript components attached to GameObjects in Unity are serialized into the GLB and re-hydrated at runtime in the browser. +**Needle Engine** ships 3D scenes from Unity or Blender as GLB files and renders them in the browser using Three.js. TypeScript components attached to objects are serialized into the GLB and re-hydrated at runtime in the browser. + +### Unity workflow +Components are C# MonoBehaviours in Unity. The Needle Unity package auto-generates TypeScript stubs and exports the scene as GLB on play/build. + +### Blender workflow +Components are added via the **Needle Engine Blender addon** — custom properties panel in Blender. The addon exports the scene as GLB with Needle component data embedded. TypeScript component files live in your web project alongside the GLB, exactly like with Unity. ### Embedding in HTML ```html @@ -206,13 +213,12 @@ export class LazyLoader extends Behaviour { } ``` -Or load a GLB URL directly using Three.js: +Or load a GLB by URL at runtime: ```ts -import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; +import { AssetReference } from "@needle-tools/engine"; -const loader = new GLTFLoader(); -const gltf = await loader.loadAsync("assets/extra.glb"); -this.context.scene.add(gltf.scene); +const ref = AssetReference.getOrCreate(this.context.domElement.baseURI, "assets/extra.glb"); +const instance = await ref.instantiate({ parent: this.gameObject }); ``` --- diff --git a/providers/claude/plugin/skills/needle-engine/references/api.md b/providers/claude/plugin/skills/needle-engine/references/api.md index 134d2d0..7f4bf73 100644 --- a/providers/claude/plugin/skills/needle-engine/references/api.md +++ b/providers/claude/plugin/skills/needle-engine/references/api.md @@ -191,12 +191,12 @@ async start() { } ``` -Load a URL directly: +Load a GLB by URL at runtime: ```ts -import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js"; -const loader = new GLTFLoader(); -const gltf = await loader.loadAsync("assets/extra.glb"); -this.context.scene.add(gltf.scene); +import { AssetReference } from "@needle-tools/engine"; + +const ref = AssetReference.getOrCreate(this.context.domElement.baseURI, "assets/extra.glb"); +const instance = await ref.instantiate({ parent: this.gameObject }); ``` --- From 710554c9fdbbcf6969e9de8cb8aef8bc4728c84a Mon Sep 17 00:00:00 2001 From: Kaktus Klaus Date: Mon, 2 Mar 2026 18:07:58 +0000 Subject: [PATCH 4/6] =?UTF-8?q?feat(skill):=20add=20engine=20hooks,=20UI?= =?UTF-8?q?=E2=86=943D=20communication,=20and=20template=20links?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Engine Hooks section: NeedleEngine.addContextCreatedCallback(), DOM events (ready, loadingfinished, loadstart) on - Frontend UI ↔ 3D section: bidirectional communication patterns - HTML/JS → 3D: getComponentInChildren() from page JS - 3D → HTML/JS: dispatchEvent() bubbling from component to DOM - React example: useEffect + CustomEvent listener → useState - Svelte example: onMount + CustomEvent listener + reactive var - Creating a Project: expanded template list with GitHub links for Vite, React, Vue, SvelteKit, Next.js Co-Authored-By: Claude Sonnet 4.6 --- .../plugin/skills/needle-engine/SKILL.md | 117 +++++++++++++++++- 1 file changed, 113 insertions(+), 4 deletions(-) diff --git a/providers/claude/plugin/skills/needle-engine/SKILL.md b/providers/claude/plugin/skills/needle-engine/SKILL.md index 690925c..0c4a0e4 100644 --- a/providers/claude/plugin/skills/needle-engine/SKILL.md +++ b/providers/claude/plugin/skills/needle-engine/SKILL.md @@ -304,15 +304,124 @@ Needle Engine has built-in WebXR support for VR and AR across Meta Quest, Apple --- +## Engine Hooks + +Use these hooks to integrate Needle Engine with any JavaScript framework or plain HTML — without writing a component. + +```ts +import { NeedleEngine, Context } from "@needle-tools/engine"; + +// Fires when any element finishes loading its scene +NeedleEngine.addContextCreatedCallback((ctx: Context) => { + console.log("Scene loaded", ctx.scene); +}); +``` + +The `` web component also fires DOM events you can listen to from outside: + +```ts +const ne = document.querySelector("needle-engine")!; + +ne.addEventListener("ready", () => { /* context initialized */ }); +ne.addEventListener("loadingfinished", () => { /* all assets loaded */ }); +ne.addEventListener("loadstart", () => { /* loading started */ }); +``` + +--- + +## Frontend UI ↔ 3D Scene Communication + +Needle Engine is a standard web component — any JavaScript on the page can talk to it. + +### HTML/JS → 3D (call a method on a component) +```ts +// In your component +@registerType +export class GameManager extends Behaviour { + addScore(points: number) { this.score += points; } +} +``` +```js +// From any JS on the page (button click, fetch result, etc.) +const ctx = document.querySelector("needle-engine").context; +const gm = ctx.scene.getComponentInChildren(GameManager); +gm?.addScore(10); +``` + +### 3D → HTML/JS (dispatch a custom DOM event from a component) +```ts +// In your component +update() { + if (playerDied) { + // Bubble up to the page — any framework can listen + this.context.domElement.dispatchEvent( + new CustomEvent("game-over", { bubbles: true, detail: { score: this.score } }) + ); + } +} +``` +```js +// In React / Svelte / Vue / vanilla JS +document.querySelector("needle-engine").addEventListener("game-over", (e) => { + showModal(`Game over! Score: ${e.detail.score}`); +}); +``` + +### React example +```tsx +import { useEffect, useState } from "react"; + +function ScoreDisplay() { + const [score, setScore] = useState(0); + + useEffect(() => { + const ne = document.querySelector("needle-engine"); + const onScore = (e: CustomEvent) => setScore(e.detail.score); + ne?.addEventListener("score-changed", onScore as EventListener); + return () => ne?.removeEventListener("score-changed", onScore as EventListener); + }, []); + + return
Score: {score}
; +} +``` + +### Svelte example +```svelte + + +

Score: {score}

+ +``` + +--- + ## Creating a New Project ```bash -npm create needle my-app # default Vite template -npm create needle my-app -t react # React template -npm create needle my-app -t vue # Vue.js template +npm create needle my-app # Vite (default) +npm create needle my-app -t react # React + Vite +npm create needle my-app -t vue # Vue + Vite +npm create needle my-app -t sveltekit # SvelteKit +npm create needle my-app -t nextjs # Next.js +npm create needle my-app -t react-three-fiber # R3F ``` -Available templates: `vite` (default), `react`, `vue`, `sveltekit`, `svelte`, `nextjs`, `react-three-fiber`. +**Starter templates on GitHub:** +- [Vite](https://github.com/needle-tools/needle-engine-support/tree/main/packages/create/templates/vite) +- [React](https://github.com/needle-tools/needle-engine-support/tree/main/packages/create/templates/react) +- [Vue](https://github.com/needle-tools/needle-engine-support/tree/main/packages/create/templates/vue) +- [SvelteKit](https://github.com/needle-tools/needle-engine-support/tree/main/packages/create/templates/sveltekit) +- [Next.js](https://github.com/needle-tools/needle-engine-support/tree/main/packages/create/templates/nextjs) --- From 4a976ecb952335c87dc1902de4cb6acbd2ed3acf Mon Sep 17 00:00:00 2001 From: Kaktus Klaus Date: Mon, 2 Mar 2026 18:10:06 +0000 Subject: [PATCH 5/6] feat(skill): fix engine hooks (onStart/onUpdate), slim SKILL.md, add integration.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Engine Hooks: replace NeedleEngine.addContextCreatedCallback with correct standalone hooks: onStart, onUpdate, onBeforeRender, onDestroy - Frontend UI ↔ 3D: condense to core pattern + link to new integration.md - Add references/integration.md: full React, Svelte, Vue, vanilla JS examples with bidirectional communication + hook reference table - SKILL.md: 502 → ~430 lines (verbose framework examples moved to reference) Co-Authored-By: Claude Sonnet 4.6 --- .../plugin/skills/needle-engine/SKILL.md | 98 ++++-------- .../needle-engine/references/integration.md | 144 ++++++++++++++++++ 2 files changed, 171 insertions(+), 71 deletions(-) create mode 100644 providers/claude/plugin/skills/needle-engine/references/integration.md diff --git a/providers/claude/plugin/skills/needle-engine/SKILL.md b/providers/claude/plugin/skills/needle-engine/SKILL.md index 0c4a0e4..ea8b7af 100644 --- a/providers/claude/plugin/skills/needle-engine/SKILL.md +++ b/providers/claude/plugin/skills/needle-engine/SKILL.md @@ -306,25 +306,28 @@ Needle Engine has built-in WebXR support for VR and AR across Meta Quest, Apple ## Engine Hooks -Use these hooks to integrate Needle Engine with any JavaScript framework or plain HTML — without writing a component. +Standalone lifecycle hooks — use these outside of a component class (e.g. in framework setup code): ```ts -import { NeedleEngine, Context } from "@needle-tools/engine"; +import { onStart, onUpdate, onBeforeRender, onDestroy } from "@needle-tools/engine"; -// Fires when any element finishes loading its scene -NeedleEngine.addContextCreatedCallback((ctx: Context) => { - console.log("Scene loaded", ctx.scene); +onStart((ctx) => { + // runs once when the scene is ready + console.log("Scene ready", ctx.scene); }); -``` -The `` web component also fires DOM events you can listen to from outside: +onUpdate((ctx) => { + // runs every frame + const dt = ctx.time.deltaTime; +}); -```ts -const ne = document.querySelector("needle-engine")!; +onBeforeRender((ctx) => { + // runs just before Three.js renders each frame +}); -ne.addEventListener("ready", () => { /* context initialized */ }); -ne.addEventListener("loadingfinished", () => { /* all assets loaded */ }); -ne.addEventListener("loadstart", () => { /* loading started */ }); +onDestroy((ctx) => { + // runs when the context is torn down +}); ``` --- @@ -333,75 +336,27 @@ ne.addEventListener("loadstart", () => { /* loading started */ }); Needle Engine is a standard web component — any JavaScript on the page can talk to it. -### HTML/JS → 3D (call a method on a component) -```ts -// In your component -@registerType -export class GameManager extends Behaviour { - addScore(points: number) { this.score += points; } -} -``` +**JS → 3D:** get a component and call methods on it directly ```js -// From any JS on the page (button click, fetch result, etc.) const ctx = document.querySelector("needle-engine").context; -const gm = ctx.scene.getComponentInChildren(GameManager); +const gm = ctx.scene.getComponentInChildren(GameManager); gm?.addScore(10); ``` -### 3D → HTML/JS (dispatch a custom DOM event from a component) +**3D → JS:** dispatch a custom DOM event from inside a component ```ts -// In your component -update() { - if (playerDied) { - // Bubble up to the page — any framework can listen - this.context.domElement.dispatchEvent( - new CustomEvent("game-over", { bubbles: true, detail: { score: this.score } }) - ); - } -} +// In your component: +this.context.domElement.dispatchEvent( + new CustomEvent("score-changed", { bubbles: true, detail: { score: 42 } }) +); ``` ```js -// In React / Svelte / Vue / vanilla JS -document.querySelector("needle-engine").addEventListener("game-over", (e) => { - showModal(`Game over! Score: ${e.detail.score}`); -}); -``` - -### React example -```tsx -import { useEffect, useState } from "react"; - -function ScoreDisplay() { - const [score, setScore] = useState(0); - - useEffect(() => { - const ne = document.querySelector("needle-engine"); - const onScore = (e: CustomEvent) => setScore(e.detail.score); - ne?.addEventListener("score-changed", onScore as EventListener); - return () => ne?.removeEventListener("score-changed", onScore as EventListener); - }, []); - - return
Score: {score}
; -} +// Anywhere on the page (React, Svelte, Vue, vanilla): +document.querySelector("needle-engine") + .addEventListener("score-changed", (e) => console.log(e.detail.score)); ``` -### Svelte example -```svelte - - -

Score: {score}

- -``` +See [references/integration.md](references/integration.md) for full React, Svelte, and Vue examples. --- @@ -491,6 +446,7 @@ Use this *before* guessing at API details — the docs are the source of truth. ## References - 📖 [Full API Reference](references/api.md) — complete lifecycle, decorators, and context API +- 🔗 [Framework Integration](references/integration.md) — React, Svelte, Vue, vanilla JS examples + hook reference - 🐛 [Troubleshooting](references/troubleshooting.md) — common errors and fixes - 🧩 [Component Template](templates/my-component.ts) — annotated starter component diff --git a/providers/claude/plugin/skills/needle-engine/references/integration.md b/providers/claude/plugin/skills/needle-engine/references/integration.md new file mode 100644 index 0000000..54aeff6 --- /dev/null +++ b/providers/claude/plugin/skills/needle-engine/references/integration.md @@ -0,0 +1,144 @@ +# Needle Engine — Framework Integration + +## React + +### Listen for 3D events in a component +```tsx +import { useEffect, useState } from "react"; + +function ScoreDisplay() { + const [score, setScore] = useState(0); + + useEffect(() => { + const ne = document.querySelector("needle-engine"); + const handler = (e: CustomEvent) => setScore(e.detail.score); + ne?.addEventListener("score-changed", handler as EventListener); + return () => ne?.removeEventListener("score-changed", handler as EventListener); + }, []); + + return
Score: {score}
; +} +``` + +### Call into the 3D scene from React +```tsx +function GameControls() { + const addScore = () => { + const ctx = (document.querySelector("needle-engine") as any)?.context; + ctx?.scene.getComponentInChildren(GameManager)?.addScore(10); + }; + return ; +} +``` + +### Wait for scene ready before accessing components +```tsx +useEffect(() => { + const ne = document.querySelector("needle-engine"); + const onReady = () => { + const ctx = (ne as any).context; + // safe to access components here + }; + ne?.addEventListener("loadingfinished", onReady); + return () => ne?.removeEventListener("loadingfinished", onReady); +}, []); +``` + +--- + +## Svelte / SvelteKit + +```svelte + + +

Score: {score}

+ + +``` + +--- + +## Vue + +```vue + + + +``` + +--- + +## Vanilla JS / No Framework + +```html + + + +``` + +--- + +## Engine Hooks Reference + +These standalone functions from `@needle-tools/engine` mirror the component lifecycle but work outside of a class: + +| Hook | When it fires | +|---|---| +| `onStart(cb)` | Once when the context/scene is ready | +| `onUpdate(cb)` | Every frame (before rendering) | +| `onBeforeRender(cb)` | Just before Three.js renders | +| `onDestroy(cb)` | When the context is torn down | + +All callbacks receive `(ctx: Context)` as their argument. + +```ts +import { onStart, onUpdate, onBeforeRender, onDestroy } from "@needle-tools/engine"; + +onStart((ctx) => { /* setup */ }); +onUpdate((ctx) => { /* per-frame logic */ }); +onDestroy((ctx) => { /* cleanup */ }); +``` From e2d8f867bcbc0d0509a9b1443ef4648bfd4bd28d Mon Sep 17 00:00:00 2001 From: Kaktus Klaus Date: Mon, 2 Mar 2026 18:17:39 +0000 Subject: [PATCH 6/6] chore(skill): add version stamp (reviewed against @needle-tools/engine@4.15.0) Co-Authored-By: Claude Sonnet 4.6 --- providers/claude/plugin/skills/needle-engine/SKILL.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/providers/claude/plugin/skills/needle-engine/SKILL.md b/providers/claude/plugin/skills/needle-engine/SKILL.md index ea8b7af..b7f1beb 100644 --- a/providers/claude/plugin/skills/needle-engine/SKILL.md +++ b/providers/claude/plugin/skills/needle-engine/SKILL.md @@ -1,6 +1,8 @@ --- name: needle-engine description: Automatically provides Needle Engine context when working in a Needle Engine web project. Use this skill when editing TypeScript components, Vite config, GLB assets, or anything related to @needle-tools/engine. +reviewed-against: "@needle-tools/engine@4.15.0" +last-reviewed: "2026-03" allowed-tools: - needle_search - Read