@needle-tools/engine 4.16.0-next.73c93c0 → 4.16.0-next.bc4d35d
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -46
- package/SKILL.md +229 -0
- package/dist/{needle-engine.bundle-75BC4qb_.min.js → needle-engine.bundle-Cqmarm0m.min.js} +115 -115
- package/dist/{needle-engine.bundle-CqSR6UY7.umd.cjs → needle-engine.bundle-DT0VwGmo.umd.cjs} +114 -114
- package/dist/{needle-engine.bundle-CwvwWDWq.js → needle-engine.bundle-KyGODqE9.js} +3305 -3283
- package/dist/needle-engine.d.ts +7 -284
- package/dist/needle-engine.js +2 -2
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/lib/engine/webcomponents/logo-element.d.ts +6 -3
- package/lib/engine/webcomponents/logo-element.js +18 -0
- package/lib/engine/webcomponents/logo-element.js.map +1 -1
- package/lib/engine/webcomponents/needle menu/needle-menu.d.ts +10 -7
- package/lib/engine/webcomponents/needle menu/needle-menu.js +13 -2
- package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
- package/lib/engine/webcomponents/needle-engine.d.ts +3 -0
- package/lib/engine/webcomponents/needle-engine.js +14 -5
- package/lib/engine/webcomponents/needle-engine.js.map +1 -1
- package/lib/engine/xr/NeedleXRSession.js +15 -3
- package/lib/engine/xr/NeedleXRSession.js.map +1 -1
- package/package.json +3 -3
- package/plugins/common/needle-engine-skill.md +106 -168
- package/plugins/vite/ai.js +1 -1
- package/src/engine/webcomponents/logo-element.ts +20 -3
- package/src/engine/webcomponents/needle menu/needle-menu.ts +22 -9
- package/src/engine/webcomponents/needle-engine.ts +19 -6
- package/src/engine/xr/NeedleXRSession.ts +16 -3
|
@@ -1,160 +1,122 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: needle-engine
|
|
3
3
|
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.
|
|
4
|
-
metadata:
|
|
5
|
-
reviewed-against: "@needle-tools/engine@4.15.0"
|
|
6
|
-
last-reviewed: "2026-03"
|
|
7
4
|
---
|
|
8
5
|
|
|
9
6
|
# Needle Engine
|
|
10
7
|
|
|
11
|
-
You are an expert in Needle Engine — a web-first 3D engine built on Three.js with a
|
|
8
|
+
You are an expert in Needle Engine — a web-first 3D engine built on Three.js with a Unity/Blender-based workflow.
|
|
12
9
|
|
|
13
|
-
##
|
|
10
|
+
## Key concepts
|
|
14
11
|
|
|
15
|
-
**
|
|
16
|
-
- Editing TypeScript files that import from `@needle-tools/engine`
|
|
17
|
-
- Working on a project with `vite.config.ts` that uses `needlePlugins`
|
|
18
|
-
- Loading or debugging `.glb` files exported from Unity or Blender
|
|
19
|
-
- Using the Needle Engine Blender addon or Unity package
|
|
20
|
-
- Asking about component lifecycle, serialization, XR, networking, or deployment
|
|
21
|
-
|
|
22
|
-
**Do NOT use for:**
|
|
23
|
-
- Pure Three.js projects with no Needle Engine
|
|
24
|
-
- Non-web Unity/Blender work with no GLB export
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Quick Start
|
|
12
|
+
**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.
|
|
29
13
|
|
|
14
|
+
### Embedding in HTML
|
|
30
15
|
```html
|
|
16
|
+
<!-- The <needle-engine> web component creates and manages a 3D context -->
|
|
31
17
|
<needle-engine src="assets/scene.glb"></needle-engine>
|
|
32
|
-
<script type="module">
|
|
33
|
-
import "@needle-tools/engine";
|
|
34
|
-
</script>
|
|
35
18
|
```
|
|
19
|
+
Access the context programmatically: `document.querySelector("needle-engine").context`
|
|
36
20
|
|
|
37
|
-
|
|
21
|
+
### Component lifecycle (mirrors Unity MonoBehaviour)
|
|
38
22
|
```ts
|
|
39
23
|
import { Behaviour, serializable, registerType } from "@needle-tools/engine";
|
|
40
24
|
|
|
41
25
|
@registerType
|
|
42
|
-
export class
|
|
43
|
-
@serializable()
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
26
|
+
export class MyComponent extends Behaviour {
|
|
27
|
+
@serializable() myValue: number = 1;
|
|
28
|
+
|
|
29
|
+
awake() {} // called once when instantiated
|
|
30
|
+
start() {} // called once on first frame
|
|
31
|
+
update() {} // called every frame
|
|
32
|
+
onEnable() {}
|
|
33
|
+
onDisable() {}
|
|
34
|
+
onDestroy() {}
|
|
35
|
+
onBeforeRender(_frame: XRFrame | null) {}
|
|
48
36
|
}
|
|
49
37
|
```
|
|
50
38
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
**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.
|
|
39
|
+
### Serialization
|
|
40
|
+
- `@registerType` — makes the class discoverable by the GLB deserializer
|
|
41
|
+
- `@serializable()` — marks a field for GLB deserialization (primitives)
|
|
42
|
+
- `@serializable(Object3D)` — for Three.js object references
|
|
43
|
+
- `@serializable(Texture)` — for textures (import Texture from "three")
|
|
44
|
+
- `@serializable(RGBAColor)` — for colors
|
|
58
45
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
46
|
+
### Accessing the scene
|
|
47
|
+
```ts
|
|
48
|
+
this.context.scene // THREE.Scene
|
|
49
|
+
this.context.mainCamera // active camera (THREE.Camera)
|
|
50
|
+
this.context.renderer // THREE.WebGLRenderer
|
|
51
|
+
this.context.time.frame // current frame number
|
|
52
|
+
this.context.time.deltaTime // seconds since last frame
|
|
53
|
+
this.gameObject // the THREE.Object3D this component is on
|
|
54
|
+
```
|
|
63
55
|
|
|
64
|
-
###
|
|
56
|
+
### Finding components
|
|
57
|
+
```ts
|
|
58
|
+
this.gameObject.getComponent(MyComponent)
|
|
59
|
+
this.gameObject.getComponentInChildren(MyComponent)
|
|
60
|
+
this.context.scene.getComponentInChildren(MyComponent)
|
|
61
|
+
|
|
62
|
+
// Global search (import as standalone functions from "@needle-tools/engine")
|
|
63
|
+
import { findObjectOfType, findObjectsOfType } from "@needle-tools/engine";
|
|
64
|
+
findObjectOfType(MyComponent, this.context)
|
|
65
|
+
findObjectsOfType(MyComponent, this.context)
|
|
66
|
+
```
|
|
65
67
|
|
|
66
|
-
|
|
68
|
+
### Input handling
|
|
69
|
+
```ts
|
|
70
|
+
// Polling
|
|
71
|
+
if (this.context.input.getPointerDown(0)) { /* pointer pressed */ }
|
|
72
|
+
if (this.context.input.getKeyDown("Space")) { /* space pressed */ }
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
src="assets/scene.glb"
|
|
71
|
-
camera-controls
|
|
72
|
-
auto-rotate
|
|
73
|
-
autoplay
|
|
74
|
-
background-color="#222"
|
|
75
|
-
environment-image="studio"
|
|
76
|
-
contactshadows
|
|
77
|
-
></needle-engine>
|
|
74
|
+
// Event-based (NEPointerEvent works across mouse, touch, and XR controllers)
|
|
75
|
+
this.gameObject.addEventListener("pointerdown", (e: NEPointerEvent) => { });
|
|
78
76
|
```
|
|
79
77
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
| `autoplay` | Auto-play animations in the loaded scene |
|
|
86
|
-
| `background-color` | Hex or RGB background color (e.g. `#ff0000`) |
|
|
87
|
-
| `background-image` | Skybox URL or preset: `studio`, `blurred-skybox`, `quicklook`, `quicklook-ar` |
|
|
88
|
-
| `background-blurriness` | Blur intensity for background (0–1) |
|
|
89
|
-
| `environment-image` | Environment lighting image URL or preset (same presets as `background-image`) |
|
|
90
|
-
| `contactshadows` | Enable contact shadows |
|
|
91
|
-
| `tone-mapping` | `none`, `linear`, `neutral`, `agx` |
|
|
92
|
-
| `poster` | Placeholder image URL shown while loading |
|
|
93
|
-
| `loadstart` / `progress` / `loadfinished` | Callback functions for loading lifecycle |
|
|
78
|
+
### Physics & raycasting
|
|
79
|
+
```ts
|
|
80
|
+
// Default raycasts hit visible geometry — no colliders needed
|
|
81
|
+
// Uses mesh BVH (bounding volume hierarchy) for accelerated raycasting, BVH is generated on a worker
|
|
82
|
+
const hits = this.context.physics.raycast();
|
|
94
83
|
|
|
95
|
-
|
|
84
|
+
// Physics-based raycasts (require colliders, uses Rapier physics engine)
|
|
85
|
+
const physicsHits = this.context.physics.raycastPhysics();
|
|
86
|
+
```
|
|
96
87
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
| Unity (C#) | Needle Engine (TypeScript) |
|
|
100
|
-
|---|---|
|
|
101
|
-
| `MonoBehaviour` | `Behaviour` |
|
|
102
|
-
| `[SerializeField]` / public field | `@serializable()` (required for all serialized fields) |
|
|
103
|
-
| `Instantiate(prefab)` | `instantiate(obj)` |
|
|
104
|
-
| `Destroy(obj)` | `destroy(obj)` |
|
|
105
|
-
| `GetComponent<T>()` | `this.gameObject.getComponent(T)` |
|
|
106
|
-
| `AddComponent<T>()` | `this.gameObject.addComponent(T)` |
|
|
107
|
-
| `FindObjectOfType<T>()` | `findObjectOfType(T, ctx)` |
|
|
108
|
-
| `transform.position` | `this.gameObject.worldPosition` (world) / `this.gameObject.position` (local) |
|
|
109
|
-
| `transform.rotation` | `this.gameObject.worldQuaternion` (world) / `this.gameObject.quaternion` (local) |
|
|
110
|
-
| `transform.localScale` | `this.gameObject.worldScale` (world) / `this.gameObject.scale` (local) |
|
|
111
|
-
| `Resources.Load<T>()` | No direct equivalent — use `@serializable(AssetReference)` to assign refs in editor, then `.instantiate()` or `.asset` at runtime |
|
|
112
|
-
| `StartCoroutine()` | `this.startCoroutine()` (in a component; unlike Unity, coroutines stop when the component is disabled) |
|
|
113
|
-
| `Time.deltaTime` | `this.context.time.deltaTime` |
|
|
114
|
-
| `Camera.main` | `this.context.mainCamera` (THREE.Camera) / `this.context.mainCameraComponent` (Needle Camera component) |
|
|
115
|
-
| `Debug.Log()` | `console.log()` |
|
|
116
|
-
| `OnCollisionEnter()` | `onCollisionEnter(col: Collision)` |
|
|
117
|
-
| `OnTriggerEnter()` | `onTriggerEnter(col: Collision)` |
|
|
88
|
+
### Networking & multiplayer
|
|
89
|
+
Needle Engine has built-in multiplayer. Add a `SyncedRoom` component to enable networking.
|
|
118
90
|
|
|
119
|
-
|
|
91
|
+
- `@syncField()` — automatically syncs a field across all connected clients
|
|
92
|
+
- Primitives (string, number, boolean) sync automatically on change
|
|
93
|
+
- Complex types (arrays/objects) require reassignment to trigger sync: `this.myArray = this.myArray`
|
|
94
|
+
- Key components: `SyncedRoom`, `SyncedTransform`, `PlayerSync`, `Voip`
|
|
95
|
+
- Uses WebSockets + optional WebRTC peer-to-peer connections
|
|
120
96
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
| Three.js | Needle Engine |
|
|
124
|
-
|---|---|
|
|
125
|
-
| `new Mesh(geo, mat)` | Created in Unity/Blender, exported as GLB; access via `Renderer.sharedMesh` / `Renderer.sharedMaterials` |
|
|
126
|
-
| `scene.add(obj)` | `this.gameObject.add(obj)` or `instantiate(prefab)` |
|
|
127
|
-
| `scene.remove(obj)` | `obj.removeFromParent()` (re-parent) or `destroy(obj)` (permanent) |
|
|
128
|
-
| `obj.position` | `obj.position` (local) / `obj.worldPosition` (world — Needle extension) |
|
|
129
|
-
| `obj.quaternion` | `obj.quaternion` (local) / `obj.worldQuaternion` (world — Needle extension) |
|
|
130
|
-
| `obj.scale` | `obj.scale` (local) / `obj.worldScale` (world — Needle extension) |
|
|
131
|
-
| `obj.getWorldPosition(v)` | `obj.worldPosition` (getter, no temp vec needed) |
|
|
132
|
-
| `obj.traverse(cb)` | `obj.traverse(cb)` (same — it's Three.js underneath) |
|
|
133
|
-
| `obj.children` | `obj.children` (same) |
|
|
134
|
-
| `obj.parent` | `obj.parent` (same) |
|
|
135
|
-
| `raycaster.intersectObjects()` | `this.context.physics.raycast()` (auto BVH, faster) |
|
|
136
|
-
| `renderer.setAnimationLoop(cb)` | `update() {}` in a component, or `onUpdate(cb)` hook |
|
|
137
|
-
| `clock.getDelta()` | `this.context.time.deltaTime` |
|
|
138
|
-
| `new GLTFLoader().load(url)` | `AssetReference.getOrCreate(base, url)` then `.instantiate()`, or `loadAsset(url)` |
|
|
139
|
-
|
|
140
|
-
Needle Engine extends `Object3D` with component methods (`getComponent`, `addComponent`, `worldPosition`, `worldQuaternion`, `worldScale`, `worldForward`, `worldRight`, `worldUp`, `contains`, etc.). `this.gameObject` is the `Object3D` a component is attached to. The underlying Three.js API still works directly.
|
|
97
|
+
### WebXR (VR & AR)
|
|
98
|
+
Needle Engine has built-in WebXR support for VR and AR across Meta Quest, Apple Vision Pro, and mobile AR.
|
|
141
99
|
|
|
142
|
-
|
|
100
|
+
- Add the `WebXR` component to enable VR/AR sessions
|
|
101
|
+
- Use `XRRig` to define the user's starting position — the user is parented to the rig during XR sessions
|
|
102
|
+
- Available components: `WebXRImageTracking`, `WebXRPlaneTracking`, `XRControllerModel`, `NeedleXRSession`
|
|
143
103
|
|
|
144
|
-
## Creating a
|
|
104
|
+
## Creating a new project
|
|
145
105
|
|
|
106
|
+
Use `create-needle` to scaffold a new Needle Engine project:
|
|
146
107
|
```bash
|
|
147
|
-
npm create needle my-app
|
|
148
|
-
npm create needle my-app -t react
|
|
149
|
-
npm create needle my-app -t vue
|
|
150
|
-
npm create needle my-app -t sveltekit # SvelteKit
|
|
151
|
-
npm create needle my-app -t nextjs # Next.js
|
|
152
|
-
npm create needle my-app -t react-three-fiber # R3F
|
|
108
|
+
npm create needle my-app # default Vite template
|
|
109
|
+
npm create needle my-app -t react # React template
|
|
110
|
+
npm create needle my-app -t vue # Vue.js template
|
|
153
111
|
```
|
|
154
112
|
|
|
155
|
-
|
|
113
|
+
Available templates: `vite` (default), `react`, `vue`, `sveltekit`, `svelte`, `nextjs`, `react-three-fiber`.
|
|
156
114
|
|
|
157
|
-
|
|
115
|
+
Use `npm create needle --list` to see all available templates.
|
|
116
|
+
|
|
117
|
+
## Vite plugin system
|
|
118
|
+
|
|
119
|
+
Needle Engine ships a set of Vite plugins via `needlePlugins(command, config, userSettings)`. Custom project plugins go in `vite.config.ts`.
|
|
158
120
|
|
|
159
121
|
```ts
|
|
160
122
|
import { defineConfig } from "vite";
|
|
@@ -167,71 +129,47 @@ export default defineConfig(async ({ command }) => ({
|
|
|
167
129
|
}));
|
|
168
130
|
```
|
|
169
131
|
|
|
170
|
-
---
|
|
171
|
-
|
|
172
132
|
## Deployment
|
|
173
133
|
|
|
174
|
-
|
|
175
|
-
- **
|
|
176
|
-
- **
|
|
177
|
-
- **
|
|
134
|
+
Projects can be deployed to:
|
|
135
|
+
- **Needle Cloud** — official hosting with automatic optimization (`npx needle-cloud deploy`)
|
|
136
|
+
- **Vercel** / **Netlify** — standard web hosting
|
|
137
|
+
- **itch.io** — for games and interactive experiences
|
|
138
|
+
- **Any static host** — Needle Engine projects are standard Vite web apps
|
|
178
139
|
|
|
179
|
-
From Unity, built-in deployment components (e.g. `DeployToNetlify`)
|
|
140
|
+
From Unity, use built-in deployment components (e.g. `DeployToNeedleCloud`, `DeployToNetlify`).
|
|
180
141
|
|
|
181
|
-
|
|
142
|
+
## Progressive loading (`@needle-tools/gltf-progressive`)
|
|
182
143
|
|
|
183
|
-
|
|
144
|
+
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.
|
|
184
145
|
|
|
146
|
+
Works standalone with any three.js project:
|
|
185
147
|
```ts
|
|
148
|
+
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
|
149
|
+
import { WebGLRenderer } from "three";
|
|
186
150
|
import { useNeedleProgressive } from "@needle-tools/gltf-progressive";
|
|
187
|
-
useNeedleProgressive(gltfLoader, renderer);
|
|
188
|
-
gltfLoader.load(url, (gltf) => scene.add(gltf.scene));
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
In Needle Engine projects this is built in — configure via **Compression & LOD Settings** in Unity.
|
|
192
151
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
## Searching the Documentation
|
|
152
|
+
const gltfLoader = new GLTFLoader();
|
|
153
|
+
const renderer = new WebGLRenderer();
|
|
196
154
|
|
|
197
|
-
|
|
155
|
+
// Register once — progressive loading happens automatically for all subsequent loads
|
|
156
|
+
useNeedleProgressive(gltfLoader, renderer);
|
|
198
157
|
|
|
158
|
+
gltfLoader.load(url, (gltf) => scene.add(gltf.scene));
|
|
199
159
|
```
|
|
200
|
-
needle_search("how to play animation clip from code")
|
|
201
|
-
needle_search("SyncedTransform multiplayer")
|
|
202
|
-
needle_search("deploy to Needle Cloud CI")
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
Use this *before* guessing at API details — the docs are the source of truth.
|
|
206
160
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
## Common Gotchas
|
|
210
|
-
|
|
211
|
-
- `@registerType` is required or the component won't be instantiated from GLB (Unity/Blender export adds this automatically, but hand-written components need it)
|
|
212
|
-
- GLB assets go in `assets/`, static files (fonts, images) in `public/` (configurable via `needle.config.json`)
|
|
213
|
-
- `useDefineForClassFields: false` must be set in `tsconfig.json` — otherwise decorators silently break field initialization
|
|
214
|
-
- `@syncField()` only triggers on reassignment — mutating an array/object in place won't sync; do `this.arr = this.arr`
|
|
215
|
-
- Physics callbacks (`onCollisionEnter` etc.) require a Needle `Collider` component on the GameObject
|
|
216
|
-
- `removeComponent()` does NOT call `onDestroy` — use `destroy(obj)` for full cleanup
|
|
217
|
-
- Prefer `instantiate()` and `destroy()` functions over `GameObject.instantiate()` / `GameObject.destroy()`
|
|
218
|
-
- `loadAsset()` returns a model wrapper (not an Object3D) — use `.scene` to get the root Object3D
|
|
219
|
-
- `AssetReference.getOrCreateFromUrl()` caches by URL — loading the same URL twice returns the same Object3D. Use `.instantiate()` or `loadAsset()` with `{ context }` for multiple copies
|
|
220
|
-
|
|
221
|
-
---
|
|
222
|
-
|
|
223
|
-
## References
|
|
224
|
-
|
|
225
|
-
For detailed API usage, read these reference files:
|
|
226
|
-
|
|
227
|
-
- [Full API Reference](https://raw.githubusercontent.com/needle-tools/ai/refs/heads/main/providers/claude/plugin/skills/needle-engine/references/api.md) — lifecycle, decorators, context API, animation, networking, XR, physics
|
|
228
|
-
- [Framework Integration](https://raw.githubusercontent.com/needle-tools/ai/refs/heads/main/providers/claude/plugin/skills/needle-engine/references/integration.md) — React, Svelte, Vue, vanilla JS examples + SSR patterns
|
|
229
|
-
- [Troubleshooting](https://raw.githubusercontent.com/needle-tools/ai/refs/heads/main/providers/claude/plugin/skills/needle-engine/references/troubleshooting.md) — common errors and fixes
|
|
230
|
-
- [Component Template](https://raw.githubusercontent.com/needle-tools/ai/refs/heads/main/providers/claude/plugin/skills/needle-engine/templates/my-component.ts) — annotated starter component
|
|
161
|
+
In Needle Engine projects, progressive loading is built in and can be configured via the **Compression & LOD Settings** component in Unity.
|
|
231
162
|
|
|
232
163
|
## Important URLs
|
|
233
|
-
|
|
234
164
|
- Docs: https://engine.needle.tools/docs/
|
|
235
165
|
- Samples: https://engine.needle.tools/samples/
|
|
236
166
|
- GitHub: https://github.com/needle-tools/needle-engine-support
|
|
237
167
|
- npm: https://www.npmjs.com/package/@needle-tools/engine
|
|
168
|
+
|
|
169
|
+
## Searching the documentation
|
|
170
|
+
|
|
171
|
+
Use the `needle_search` MCP tool to find relevant docs, forum posts, and community answers.
|
|
172
|
+
|
|
173
|
+
## Common gotchas
|
|
174
|
+
- 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)
|
|
175
|
+
- GLB assets are in `assets/`, static files in `include/` or `public/`
|
package/plugins/vite/ai.js
CHANGED
|
@@ -42,7 +42,7 @@ function writeSkill(claudeDir) {
|
|
|
42
42
|
mkdirSync(skillDir, { recursive: true });
|
|
43
43
|
}
|
|
44
44
|
const skillPath = join(skillDir, "SKILL.md");
|
|
45
|
-
const templatePath = join(__dirname, "
|
|
45
|
+
const templatePath = join(__dirname, "../../SKILL.md");
|
|
46
46
|
const content = readFileSync(templatePath, "utf8");
|
|
47
47
|
writeFileSync(skillPath, content, "utf8");
|
|
48
48
|
return skillPath;
|
|
@@ -20,8 +20,13 @@ export class NeedleLogoElement extends HTMLElement {
|
|
|
20
20
|
return document.createElement(elementName) as NeedleLogoElement;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
private _didInitialize = false;
|
|
24
|
+
|
|
23
25
|
constructor() {
|
|
24
26
|
super();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private initializeDom() {
|
|
25
30
|
this._root = this.attachShadow({ mode: 'closed' });
|
|
26
31
|
const template = document.createElement('template');
|
|
27
32
|
template.innerHTML = `<style>
|
|
@@ -83,25 +88,37 @@ export class NeedleLogoElement extends HTMLElement {
|
|
|
83
88
|
this.addEventListener("click", () => {
|
|
84
89
|
globalThis.open("https://needle.tools", "_blank");
|
|
85
90
|
});
|
|
91
|
+
}
|
|
86
92
|
|
|
93
|
+
ensureInitialized() {
|
|
94
|
+
if (!this._didInitialize) {
|
|
95
|
+
this._didInitialize = true;
|
|
96
|
+
this.initializeDom();
|
|
97
|
+
}
|
|
87
98
|
}
|
|
88
99
|
|
|
89
100
|
connectedCallback() {
|
|
101
|
+
this.ensureInitialized();
|
|
102
|
+
if (!this.wrapper) return;
|
|
90
103
|
this.wrapper.setAttribute("title", "Made with Needle Engine");
|
|
91
104
|
this.setAttribute("aria-label", "Needle Engine logo. Click to open the Needle Engine website.");
|
|
92
105
|
}
|
|
93
106
|
|
|
94
|
-
private
|
|
95
|
-
private
|
|
96
|
-
private
|
|
107
|
+
private _root!: ShadowRoot;
|
|
108
|
+
private wrapper!: HTMLDivElement;
|
|
109
|
+
private logoElement!: HTMLImageElement;
|
|
97
110
|
|
|
98
111
|
/** Show or hide the logo element (used by the menu) */
|
|
99
112
|
setLogoVisible(val: boolean) {
|
|
113
|
+
this.ensureInitialized();
|
|
114
|
+
if (!this.logoElement) return;
|
|
100
115
|
this.logoElement.style.display = val ? "block" : "none";
|
|
101
116
|
}
|
|
102
117
|
|
|
103
118
|
/** Switch the logo between full and compact versions */
|
|
104
119
|
setType(type: "full" | "compact") {
|
|
120
|
+
this.ensureInitialized();
|
|
121
|
+
if (!this.logoElement) return;
|
|
105
122
|
if (type === "full") {
|
|
106
123
|
this.logoElement.src = needleLogoSVG;
|
|
107
124
|
this.logoElement.classList.remove("with-text");
|
|
@@ -129,6 +129,7 @@ export class NeedleMenu {
|
|
|
129
129
|
|
|
130
130
|
constructor(context: Context) {
|
|
131
131
|
this._menu = NeedleMenuElement.getOrCreate(context.domElement, context);
|
|
132
|
+
this._menu.ensureInitialized();
|
|
132
133
|
this._context = context;
|
|
133
134
|
this._spatialMenu = new NeedleSpatialMenu(context, this._menu);
|
|
134
135
|
window.addEventListener("message", this.onPostMessage);
|
|
@@ -337,9 +338,13 @@ export class NeedleMenuElement extends HTMLElement {
|
|
|
337
338
|
|
|
338
339
|
private _domElement: HTMLElement | null = null;
|
|
339
340
|
private _context: Context | null = null;
|
|
341
|
+
private _didInitialize = false;
|
|
340
342
|
|
|
341
343
|
constructor() {
|
|
342
344
|
super();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private initializeDom() {
|
|
343
348
|
|
|
344
349
|
const template = document.createElement('template');
|
|
345
350
|
// TODO: make host full size again and move the buttons to a wrapper so that we can later easily open e.g. foldouts/dropdowns / use the whole canvas space
|
|
@@ -793,7 +798,7 @@ export class NeedleMenuElement extends HTMLElement {
|
|
|
793
798
|
|
|
794
799
|
|
|
795
800
|
let context = this._context;
|
|
796
|
-
// we need to assign it in the timeout because the reference is set *after* the
|
|
801
|
+
// we need to assign it in the timeout because the reference is set *after* the element is initialized
|
|
797
802
|
setTimeout(() => context = this._context);
|
|
798
803
|
|
|
799
804
|
// watch changes
|
|
@@ -862,13 +867,21 @@ export class NeedleMenuElement extends HTMLElement {
|
|
|
862
867
|
}
|
|
863
868
|
}
|
|
864
869
|
|
|
870
|
+
ensureInitialized() {
|
|
871
|
+
if (!this._didInitialize) {
|
|
872
|
+
this._didInitialize = true;
|
|
873
|
+
this.initializeDom();
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
865
877
|
private _sizeChangeInterval;
|
|
866
878
|
|
|
867
879
|
connectedCallback() {
|
|
880
|
+
this.ensureInitialized();
|
|
868
881
|
window.addEventListener("resize", this.handleSizeChange);
|
|
869
882
|
this.handleMenuVisible();
|
|
870
883
|
this._sizeChangeInterval = setInterval(() => this.handleSizeChange(undefined, false), 5000);
|
|
871
|
-
// the dom element is set after
|
|
884
|
+
// the dom element is set after initialization runs
|
|
872
885
|
setTimeout(() => {
|
|
873
886
|
this._domElement?.addEventListener("resize", this.handleSizeChange);
|
|
874
887
|
this._domElement?.addEventListener("click", this.#onClick);
|
|
@@ -956,19 +969,19 @@ export class NeedleMenuElement extends HTMLElement {
|
|
|
956
969
|
|
|
957
970
|
// private _root: ShadowRoot | null = null;
|
|
958
971
|
/** @private root container element inside shadow DOM */
|
|
959
|
-
private
|
|
972
|
+
private root!: HTMLDivElement;
|
|
960
973
|
/** @private wraps the whole content (internal layout) */
|
|
961
|
-
private
|
|
974
|
+
private wrapper!: HTMLDivElement;
|
|
962
975
|
/** @private contains the buttons and dynamic elements */
|
|
963
|
-
private
|
|
976
|
+
private options!: HTMLDivElement;
|
|
964
977
|
/** @private contains options visible when in compact mode */
|
|
965
|
-
private
|
|
978
|
+
private optionsCompactMode!: HTMLDivElement;
|
|
966
979
|
/** @private contains the needle-logo html element */
|
|
967
|
-
private
|
|
980
|
+
private logoContainer!: HTMLDivElement;
|
|
968
981
|
/** @private compact menu button element */
|
|
969
|
-
private
|
|
982
|
+
private compactMenuButton!: HTMLButtonElement;
|
|
970
983
|
/** @private foldout container used in compact mode */
|
|
971
|
-
private
|
|
984
|
+
private foldout!: HTMLDivElement;
|
|
972
985
|
|
|
973
986
|
|
|
974
987
|
private readonly trackedElements: WeakSet<Node> = new WeakSet();
|
|
@@ -168,23 +168,35 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
|
|
|
168
168
|
*/
|
|
169
169
|
public get context() { return this._context; }
|
|
170
170
|
|
|
171
|
-
private _context
|
|
171
|
+
private _context!: Context;
|
|
172
172
|
private _overlay_ar: AROverlayHandler;
|
|
173
173
|
private _loadingProgress01: number = 0;
|
|
174
174
|
private _loadingView?: ILoadingViewHandler;
|
|
175
175
|
private _previousSrc: string | null | string[] = null;
|
|
176
176
|
/** @private set to true after <needle-engine> did load completely at least once. Set to false when < to false when <needle-engine> is removed from the document removed from the document */
|
|
177
177
|
private _didFullyLoad: boolean = false;
|
|
178
|
+
private _didInitialize = false;
|
|
178
179
|
|
|
179
180
|
constructor() {
|
|
180
181
|
super();
|
|
181
|
-
this._overlay_ar = new AROverlayHandler();
|
|
182
182
|
// TODO: do we want to rename this event?
|
|
183
183
|
this.addEventListener("ready", this.onReady);
|
|
184
|
+
this._overlay_ar = new AROverlayHandler();
|
|
185
|
+
this._context = new Context({ domElement: this });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private ensureInitialized() {
|
|
189
|
+
if (!this._didInitialize) {
|
|
190
|
+
this._didInitialize = true;
|
|
191
|
+
this.initializeDom();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private initializeDom() {
|
|
184
196
|
|
|
185
197
|
ensureFonts();
|
|
186
198
|
|
|
187
|
-
this.attachShadow({ mode: 'open', delegatesFocus: true });
|
|
199
|
+
const shadow = this.attachShadow({ mode: 'open', delegatesFocus: true });
|
|
188
200
|
this.setAttribute("role", "application");
|
|
189
201
|
this.setAttribute("aria-label", "Needle Engine 3D scene");
|
|
190
202
|
const template = document.createElement('template');
|
|
@@ -269,10 +281,8 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
|
|
|
269
281
|
`;
|
|
270
282
|
// #endregion
|
|
271
283
|
|
|
272
|
-
|
|
273
|
-
this.shadowRoot.appendChild(template.content.cloneNode(true));
|
|
284
|
+
shadow.appendChild(template.content.cloneNode(true));
|
|
274
285
|
|
|
275
|
-
this._context = new Context({ domElement: this });
|
|
276
286
|
this.addEventListener("error", this.onError);
|
|
277
287
|
}
|
|
278
288
|
|
|
@@ -281,9 +291,12 @@ export class NeedleEngineWebComponent extends HTMLElement implements INeedleEngi
|
|
|
281
291
|
* @internal
|
|
282
292
|
*/
|
|
283
293
|
async connectedCallback() {
|
|
294
|
+
|
|
284
295
|
if (debug) {
|
|
285
296
|
console.log("<needle-engine> connected");
|
|
286
297
|
}
|
|
298
|
+
|
|
299
|
+
this.ensureInitialized();
|
|
287
300
|
|
|
288
301
|
this.setPublicKey();
|
|
289
302
|
this.setVersion();
|
|
@@ -255,6 +255,7 @@ function handleAutoStart(value: string | null) {
|
|
|
255
255
|
// }
|
|
256
256
|
|
|
257
257
|
const $initialFov = Symbol("initial-fov");
|
|
258
|
+
const $initialNear = Symbol("initial-near");
|
|
258
259
|
|
|
259
260
|
/**
|
|
260
261
|
* This class manages an XRSession to provide helper methods and events. It provides easy access to the XRInputSources (controllers and hands)
|
|
@@ -1070,6 +1071,12 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
1070
1071
|
this._originalCameraParent = this.context.mainCamera.parent;
|
|
1071
1072
|
if (this.context.mainCamera instanceof PerspectiveCamera) {
|
|
1072
1073
|
this.context.mainCamera[$initialFov] = this.context.mainCamera.fov;
|
|
1074
|
+
|
|
1075
|
+
// if (this.mode === "immersive-ar" && this.context.mainCamera.near > .1) {
|
|
1076
|
+
// console.log("[WebXR] Setting near plane to 0.1 for better AR experience (was " + this.context.mainCamera.near + "). The initial near plane will be restored when the session ends.");
|
|
1077
|
+
// this.context.mainCamera.near = 0.1;
|
|
1078
|
+
// this.context.mainCamera[$initialNear] = this.context.mainCamera.near;
|
|
1079
|
+
// }
|
|
1073
1080
|
}
|
|
1074
1081
|
}
|
|
1075
1082
|
|
|
@@ -1308,9 +1315,15 @@ export class NeedleXRSession implements INeedleXRSession {
|
|
|
1308
1315
|
setWorldScale(this.context.mainCamera, this._originalCameraWorldScale);
|
|
1309
1316
|
}
|
|
1310
1317
|
|
|
1311
|
-
if (this.context.mainCamera instanceof PerspectiveCamera
|
|
1312
|
-
|
|
1313
|
-
|
|
1318
|
+
if (this.context.mainCamera instanceof PerspectiveCamera) {
|
|
1319
|
+
if (this.context.mainCamera[$initialFov]) {
|
|
1320
|
+
this.context.mainCamera.fov = this.context.mainCamera[$initialFov]!;
|
|
1321
|
+
this.context.mainCamera[$initialFov] = 0;
|
|
1322
|
+
}
|
|
1323
|
+
if (this.context.mainCamera[$initialNear]) {
|
|
1324
|
+
this.context.mainCamera.near = this.context.mainCamera[$initialNear]!;
|
|
1325
|
+
this.context.mainCamera[$initialNear] = 0;
|
|
1326
|
+
}
|
|
1314
1327
|
}
|
|
1315
1328
|
}
|
|
1316
1329
|
|