spoint 0.1.0 → 0.1.11

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.
Files changed (204) hide show
  1. package/README.md +134 -209
  2. package/SKILL.md +95 -0
  3. package/apps/environment/index.js +200 -1
  4. package/apps/environment/models/decorative/.gitkeep +0 -0
  5. package/apps/environment/models/hazards/.gitkeep +0 -0
  6. package/apps/environment/models/interactive/.gitkeep +0 -0
  7. package/apps/environment/models/structures/.gitkeep +0 -0
  8. package/apps/environment/smartObjects.js +114 -0
  9. package/apps/interactable/index.js +155 -0
  10. package/apps/physics-crate/index.js +15 -9
  11. package/apps/power-crate/index.js +18 -12
  12. package/apps/tps-game/$GDUPI.vrm +0 -0
  13. package/apps/tps-game/Cleetus.vrm +0 -0
  14. package/apps/tps-game/index.js +185 -27
  15. package/apps/world/index.js +68 -22
  16. package/bin/create-app.js +337 -0
  17. package/client/ARControls.js +301 -0
  18. package/client/LoadingManager.js +117 -0
  19. package/client/MobileControls.js +1122 -0
  20. package/client/anim-lib.glb +0 -0
  21. package/client/animation.js +306 -0
  22. package/client/app.js +1341 -65
  23. package/client/camera.js +191 -33
  24. package/client/createLoadingScreen.js +69 -0
  25. package/client/editor/bridge.js +113 -0
  26. package/client/editor/css/main.css +794 -0
  27. package/client/editor/images/rotate.svg +4 -0
  28. package/client/editor/images/scale.svg +4 -0
  29. package/client/editor/images/translate.svg +4 -0
  30. package/client/editor/index.html +103 -0
  31. package/client/editor/js/Command.js +41 -0
  32. package/client/editor/js/Config.js +81 -0
  33. package/client/editor/js/Editor.js +785 -0
  34. package/client/editor/js/EditorControls.js +438 -0
  35. package/client/editor/js/History.js +321 -0
  36. package/client/editor/js/Loader.js +987 -0
  37. package/client/editor/js/LoaderUtils.js +90 -0
  38. package/client/editor/js/Menubar.Add.js +510 -0
  39. package/client/editor/js/Menubar.Edit.js +145 -0
  40. package/client/editor/js/Menubar.File.js +466 -0
  41. package/client/editor/js/Menubar.Help.js +73 -0
  42. package/client/editor/js/Menubar.Status.js +51 -0
  43. package/client/editor/js/Menubar.View.js +183 -0
  44. package/client/editor/js/Menubar.js +27 -0
  45. package/client/editor/js/Player.js +53 -0
  46. package/client/editor/js/Resizer.js +58 -0
  47. package/client/editor/js/Script.js +503 -0
  48. package/client/editor/js/Selector.js +102 -0
  49. package/client/editor/js/Sidebar.Geometry.BoxGeometry.js +121 -0
  50. package/client/editor/js/Sidebar.Geometry.BufferGeometry.js +115 -0
  51. package/client/editor/js/Sidebar.Geometry.CapsuleGeometry.js +97 -0
  52. package/client/editor/js/Sidebar.Geometry.CircleGeometry.js +97 -0
  53. package/client/editor/js/Sidebar.Geometry.CylinderGeometry.js +121 -0
  54. package/client/editor/js/Sidebar.Geometry.DodecahedronGeometry.js +73 -0
  55. package/client/editor/js/Sidebar.Geometry.ExtrudeGeometry.js +196 -0
  56. package/client/editor/js/Sidebar.Geometry.IcosahedronGeometry.js +73 -0
  57. package/client/editor/js/Sidebar.Geometry.LatheGeometry.js +98 -0
  58. package/client/editor/js/Sidebar.Geometry.Modifiers.js +73 -0
  59. package/client/editor/js/Sidebar.Geometry.OctahedronGeometry.js +74 -0
  60. package/client/editor/js/Sidebar.Geometry.PlaneGeometry.js +97 -0
  61. package/client/editor/js/Sidebar.Geometry.RingGeometry.js +121 -0
  62. package/client/editor/js/Sidebar.Geometry.ShapeGeometry.js +76 -0
  63. package/client/editor/js/Sidebar.Geometry.SphereGeometry.js +133 -0
  64. package/client/editor/js/Sidebar.Geometry.TetrahedronGeometry.js +74 -0
  65. package/client/editor/js/Sidebar.Geometry.TorusGeometry.js +109 -0
  66. package/client/editor/js/Sidebar.Geometry.TorusKnotGeometry.js +121 -0
  67. package/client/editor/js/Sidebar.Geometry.TubeGeometry.js +135 -0
  68. package/client/editor/js/Sidebar.Geometry.js +332 -0
  69. package/client/editor/js/Sidebar.Material.BooleanProperty.js +60 -0
  70. package/client/editor/js/Sidebar.Material.ColorProperty.js +87 -0
  71. package/client/editor/js/Sidebar.Material.ConstantProperty.js +62 -0
  72. package/client/editor/js/Sidebar.Material.MapProperty.js +249 -0
  73. package/client/editor/js/Sidebar.Material.NumberProperty.js +60 -0
  74. package/client/editor/js/Sidebar.Material.Program.js +73 -0
  75. package/client/editor/js/Sidebar.Material.RangeValueProperty.js +63 -0
  76. package/client/editor/js/Sidebar.Material.js +751 -0
  77. package/client/editor/js/Sidebar.Object.Animation.js +102 -0
  78. package/client/editor/js/Sidebar.Object.js +898 -0
  79. package/client/editor/js/Sidebar.Project.App.js +165 -0
  80. package/client/editor/js/Sidebar.Project.Image.js +225 -0
  81. package/client/editor/js/Sidebar.Project.Materials.js +82 -0
  82. package/client/editor/js/Sidebar.Project.Renderer.js +144 -0
  83. package/client/editor/js/Sidebar.Project.Video.js +242 -0
  84. package/client/editor/js/Sidebar.Project.js +31 -0
  85. package/client/editor/js/Sidebar.Properties.js +73 -0
  86. package/client/editor/js/Sidebar.Scene.js +585 -0
  87. package/client/editor/js/Sidebar.Script.js +129 -0
  88. package/client/editor/js/Sidebar.Settings.History.js +146 -0
  89. package/client/editor/js/Sidebar.Settings.Shortcuts.js +175 -0
  90. package/client/editor/js/Sidebar.Settings.js +60 -0
  91. package/client/editor/js/Sidebar.js +41 -0
  92. package/client/editor/js/Storage.js +98 -0
  93. package/client/editor/js/Strings.js +2028 -0
  94. package/client/editor/js/Toolbar.js +84 -0
  95. package/client/editor/js/Viewport.Controls.js +92 -0
  96. package/client/editor/js/Viewport.Info.js +136 -0
  97. package/client/editor/js/Viewport.Pathtracer.js +91 -0
  98. package/client/editor/js/Viewport.ViewHelper.js +39 -0
  99. package/client/editor/js/Viewport.XR.js +222 -0
  100. package/client/editor/js/Viewport.js +900 -0
  101. package/client/editor/js/commands/AddObjectCommand.js +68 -0
  102. package/client/editor/js/commands/AddScriptCommand.js +75 -0
  103. package/client/editor/js/commands/Commands.js +23 -0
  104. package/client/editor/js/commands/MoveObjectCommand.js +111 -0
  105. package/client/editor/js/commands/MultiCmdsCommand.js +85 -0
  106. package/client/editor/js/commands/RemoveObjectCommand.js +88 -0
  107. package/client/editor/js/commands/RemoveScriptCommand.js +81 -0
  108. package/client/editor/js/commands/SetColorCommand.js +73 -0
  109. package/client/editor/js/commands/SetGeometryCommand.js +87 -0
  110. package/client/editor/js/commands/SetGeometryValueCommand.js +70 -0
  111. package/client/editor/js/commands/SetMaterialColorCommand.js +86 -0
  112. package/client/editor/js/commands/SetMaterialCommand.js +79 -0
  113. package/client/editor/js/commands/SetMaterialMapCommand.js +143 -0
  114. package/client/editor/js/commands/SetMaterialRangeCommand.js +91 -0
  115. package/client/editor/js/commands/SetMaterialValueCommand.js +90 -0
  116. package/client/editor/js/commands/SetMaterialVectorCommand.js +79 -0
  117. package/client/editor/js/commands/SetPositionCommand.js +84 -0
  118. package/client/editor/js/commands/SetRotationCommand.js +84 -0
  119. package/client/editor/js/commands/SetScaleCommand.js +84 -0
  120. package/client/editor/js/commands/SetSceneCommand.js +103 -0
  121. package/client/editor/js/commands/SetScriptValueCommand.js +80 -0
  122. package/client/editor/js/commands/SetShadowValueCommand.js +73 -0
  123. package/client/editor/js/commands/SetUuidCommand.js +70 -0
  124. package/client/editor/js/commands/SetValueCommand.js +75 -0
  125. package/client/editor/js/libs/acorn/acorn.js +3236 -0
  126. package/client/editor/js/libs/acorn/acorn_loose.js +1299 -0
  127. package/client/editor/js/libs/acorn/walk.js +344 -0
  128. package/client/editor/js/libs/app/index.html +57 -0
  129. package/client/editor/js/libs/app.js +251 -0
  130. package/client/editor/js/libs/codemirror/addon/dialog.css +32 -0
  131. package/client/editor/js/libs/codemirror/addon/dialog.js +163 -0
  132. package/client/editor/js/libs/codemirror/addon/show-hint.css +36 -0
  133. package/client/editor/js/libs/codemirror/addon/show-hint.js +529 -0
  134. package/client/editor/js/libs/codemirror/addon/tern.css +87 -0
  135. package/client/editor/js/libs/codemirror/addon/tern.js +750 -0
  136. package/client/editor/js/libs/codemirror/codemirror.css +344 -0
  137. package/client/editor/js/libs/codemirror/codemirror.js +9849 -0
  138. package/client/editor/js/libs/codemirror/mode/glsl.js +233 -0
  139. package/client/editor/js/libs/codemirror/mode/javascript.js +959 -0
  140. package/client/editor/js/libs/codemirror/theme/monokai.css +41 -0
  141. package/client/editor/js/libs/esprima.js +6401 -0
  142. package/client/editor/js/libs/jsonlint.js +453 -0
  143. package/client/editor/js/libs/signals.min.js +14 -0
  144. package/client/editor/js/libs/tern-threejs/threejs.js +5031 -0
  145. package/client/editor/js/libs/ternjs/comment.js +87 -0
  146. package/client/editor/js/libs/ternjs/def.js +588 -0
  147. package/client/editor/js/libs/ternjs/doc_comment.js +401 -0
  148. package/client/editor/js/libs/ternjs/infer.js +1635 -0
  149. package/client/editor/js/libs/ternjs/polyfill.js +80 -0
  150. package/client/editor/js/libs/ternjs/signal.js +26 -0
  151. package/client/editor/js/libs/ternjs/tern.js +993 -0
  152. package/client/editor/js/libs/ui.js +1346 -0
  153. package/client/editor/js/libs/ui.three.js +855 -0
  154. package/client/facial-animation.js +455 -0
  155. package/client/index.html +7 -4
  156. package/client/loading.css +147 -0
  157. package/client/loading.html +25 -0
  158. package/client/style.css +251 -0
  159. package/package.json +7 -3
  160. package/server.js +9 -1
  161. package/src/apps/AppContext.js +1 -1
  162. package/src/apps/AppLoader.js +50 -37
  163. package/src/apps/AppRuntime.js +32 -8
  164. package/src/client/InputHandler.js +233 -0
  165. package/src/client/JitterBuffer.js +207 -0
  166. package/src/client/KalmanFilter.js +125 -0
  167. package/src/client/MessageHandler.js +101 -0
  168. package/src/client/PhysicsNetworkClient.js +141 -68
  169. package/src/client/ReconnectManager.js +62 -0
  170. package/src/client/SmoothInterpolation.js +127 -0
  171. package/src/client/SnapshotProcessor.js +144 -0
  172. package/src/connection/ConnectionManager.js +21 -3
  173. package/src/connection/SessionStore.js +13 -3
  174. package/src/index.client.js +4 -6
  175. package/src/netcode/EventLog.js +29 -15
  176. package/src/netcode/LagCompensator.js +25 -26
  177. package/src/netcode/NetworkState.js +4 -1
  178. package/src/netcode/PhysicsIntegration.js +20 -6
  179. package/src/netcode/PlayerManager.js +10 -2
  180. package/src/netcode/SnapshotEncoder.js +66 -19
  181. package/src/netcode/TickSystem.js +13 -4
  182. package/src/physics/World.js +66 -13
  183. package/src/protocol/msgpack.js +90 -63
  184. package/src/sdk/ReloadHandlers.js +12 -2
  185. package/src/sdk/ReloadManager.js +5 -0
  186. package/src/sdk/ServerHandlers.js +50 -11
  187. package/src/sdk/StaticHandler.js +22 -6
  188. package/src/sdk/TickHandler.js +101 -34
  189. package/src/sdk/scaffold.js +31 -0
  190. package/src/sdk/server.js +59 -33
  191. package/src/shared/movement.js +2 -1
  192. package/src/spatial/Octree.js +5 -0
  193. package/apps/interactive-door/index.js +0 -33
  194. package/apps/patrol-npc/index.js +0 -37
  195. package/src/connection/QualityMonitor.js +0 -46
  196. package/src/debug/StateInspector.js +0 -42
  197. package/src/index.js +0 -1
  198. package/src/index.server.js +0 -27
  199. package/src/protocol/Codec.js +0 -60
  200. package/src/protocol/SequenceTracker.js +0 -71
  201. package/src/sdk/ClientMessageHandler.js +0 -80
  202. package/src/sdk/client.js +0 -122
  203. package/world/kaira.glb +0 -0
  204. package/world/schwust.glb +0 -0
@@ -0,0 +1,337 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
4
+ import { resolve, join } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const __dirname = import.meta.dirname || resolve(path.dirname(fileURLToPath(import.meta.url)))
8
+
9
+ const TEMPLATES = {
10
+ simple: 'simple',
11
+ physics: 'physics',
12
+ interactive: 'interactive',
13
+ spawner: 'spawner'
14
+ }
15
+
16
+ function showHelp() {
17
+ console.log(`
18
+ Usage: spoint create-app [options] <app-name>
19
+
20
+ Options:
21
+ --template <type> Template to use: simple, physics, interactive, spawner (default: simple)
22
+ --help Show this help message
23
+
24
+ Examples:
25
+ spoint create-app my-app
26
+ spoint create-app --template physics my-physics-object
27
+ spoint create-app --template spawner my-spawner
28
+ `)
29
+ }
30
+
31
+ function parseArgs(argv) {
32
+ const args = { name: null, template: 'simple' }
33
+
34
+ for (let i = 0; i < argv.length; i++) {
35
+ if (argv[i] === '--help') {
36
+ showHelp()
37
+ process.exit(0)
38
+ }
39
+ if (argv[i] === '--template' && argv[i + 1]) {
40
+ args.template = argv[++i]
41
+ } else if (!argv[i].startsWith('--')) {
42
+ args.name = argv[i]
43
+ }
44
+ }
45
+
46
+ return args
47
+ }
48
+
49
+ function getTemplateContent(templateType) {
50
+ const templates = {
51
+ simple: `export default {
52
+ server: {
53
+ setup(ctx) {
54
+ ctx.entity.custom = {
55
+ mesh: 'box',
56
+ color: 0x00ff00
57
+ }
58
+ ctx.physics.setStatic(true)
59
+ ctx.physics.addBoxCollider([0.5, 0.5, 0.5])
60
+ },
61
+
62
+ update(ctx, dt) {
63
+ // Your update logic here
64
+ },
65
+
66
+ teardown(ctx) {
67
+ // Cleanup resources
68
+ }
69
+ },
70
+
71
+ client: {
72
+ render(ctx) {
73
+ return {
74
+ position: ctx.entity.position,
75
+ rotation: ctx.entity.rotation,
76
+ custom: ctx.entity.custom
77
+ }
78
+ }
79
+ }
80
+ }`,
81
+
82
+ physics: `export default {
83
+ server: {
84
+ setup(ctx) {
85
+ ctx.entity.custom = {
86
+ mesh: 'box',
87
+ color: 0xff8800,
88
+ sx: 1,
89
+ sy: 1,
90
+ sz: 1
91
+ }
92
+ ctx.physics.setDynamic(true)
93
+ ctx.physics.setMass(5)
94
+ ctx.physics.addBoxCollider([0.5, 0.5, 0.5])
95
+ },
96
+
97
+ update(ctx, dt) {
98
+ const ent = ctx._entity
99
+ if (!ent?._physicsBodyId || !ctx._runtime?._physics) return
100
+ const pw = ctx._runtime._physics
101
+ ent.position = pw.getBodyPosition(ent._physicsBodyId)
102
+ ent.rotation = pw.getBodyRotation(ent._physicsBodyId)
103
+ },
104
+
105
+ teardown(ctx) {
106
+ const ent = ctx._entity
107
+ if (ent?._physicsBodyId && ctx._runtime?._physics) {
108
+ ctx._runtime._physics.removeBody(ent._physicsBodyId)
109
+ ent._physicsBodyId = null
110
+ }
111
+ }
112
+ },
113
+
114
+ client: {
115
+ render(ctx) {
116
+ return {
117
+ position: ctx.entity.position,
118
+ rotation: ctx.entity.rotation,
119
+ custom: ctx.entity.custom
120
+ }
121
+ }
122
+ }
123
+ }`,
124
+
125
+ interactive: `export default {
126
+ server: {
127
+ setup(ctx) {
128
+ ctx.entity.custom = {
129
+ mesh: 'box',
130
+ color: 0x00ff88,
131
+ sx: 1.5,
132
+ sy: 0.5,
133
+ sz: 1.5,
134
+ label: 'INTERACT'
135
+ }
136
+ ctx.physics.setStatic(true)
137
+ ctx.physics.addBoxCollider([0.75, 0.25, 0.75])
138
+ ctx.state.interactionCount = 0
139
+ ctx.state.interactionRadius = 3.5
140
+ ctx.state.interactionCooldown = new Map()
141
+ },
142
+
143
+ update(ctx, dt) {
144
+ const nearby = ctx.players.getNearest(ctx.entity.position, ctx.state.interactionRadius)
145
+ if (!nearby?.state?.interact) return
146
+
147
+ const now = Date.now()
148
+ const playerId = nearby.id
149
+ const lastInteract = ctx.state.interactionCooldown.get(playerId) || 0
150
+
151
+ if (now - lastInteract > 500) {
152
+ ctx.state.interactionCooldown.set(playerId, now)
153
+ ctx.state.interactionCount++
154
+
155
+ ctx.players.send(playerId, {
156
+ type: 'interact_response',
157
+ message: 'You interacted!',
158
+ count: ctx.state.interactionCount
159
+ })
160
+
161
+ ctx.network.broadcast({
162
+ type: 'interact_effect',
163
+ position: ctx.entity.position
164
+ })
165
+ }
166
+ },
167
+
168
+ teardown(ctx) {
169
+ ctx.state.interactionCooldown?.clear()
170
+ }
171
+ },
172
+
173
+ client: {
174
+ setup(engine) {
175
+ this._lastMessage = null
176
+ this._messageExpire = 0
177
+ this._canInteract = false
178
+ },
179
+
180
+ onFrame(dt, engine) {
181
+ const ent = engine.client?.state?.entities?.find(e => e.app === 'my-app')
182
+ const local = engine.client?.state?.players?.find(p => p.id === engine.playerId)
183
+ if (!ent?.position || !local?.position) {
184
+ this._canInteract = false
185
+ return
186
+ }
187
+
188
+ const dx = ent.position[0] - local.position[0]
189
+ const dy = ent.position[1] - local.position[1]
190
+ const dz = ent.position[2] - local.position[2]
191
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz)
192
+ this._canInteract = dist < 3.5
193
+ },
194
+
195
+ onEvent(payload, engine) {
196
+ if (payload.type === 'interact_response') {
197
+ this._lastMessage = payload.message
198
+ this._messageExpire = Date.now() + 2000
199
+ }
200
+ },
201
+
202
+ render(ctx) {
203
+ const custom = { ...ctx.entity.custom }
204
+ if (this._canInteract) {
205
+ custom.glow = true
206
+ custom.glowColor = 0x00ff88
207
+ }
208
+
209
+ const ui = []
210
+ if (this._lastMessage && Date.now() < this._messageExpire) {
211
+ const opacity = Math.max(0, (this._messageExpire - Date.now()) / 2000)
212
+ if (ctx.h) {
213
+ ui.push(
214
+ ctx.h('div', {
215
+ style: `position:fixed;top:30%;left:50%;transform:translate(-50%,-50%);padding:16px 32px;background:rgba(0,0,0,0.8);border-radius:12px;color:#0f0;font-weight:bold;font-size:20px;opacity:${opacity}`
216
+ }, this._lastMessage)
217
+ )
218
+ }
219
+ }
220
+
221
+ return {
222
+ position: ctx.entity.position,
223
+ rotation: ctx.entity.rotation,
224
+ custom,
225
+ ui: ui.length > 0 ? ctx.h('div', null, ...ui) : null
226
+ }
227
+ }
228
+ }
229
+ }`,
230
+
231
+ spawner: `const CONFIG = {
232
+ spawnInterval: 5,
233
+ maxEntities: 10,
234
+ entityApp: 'physics-crate'
235
+ }
236
+
237
+ export default {
238
+ server: {
239
+ setup(ctx) {
240
+ ctx.state.entities = new Set()
241
+ ctx.state.nextId = 0
242
+
243
+ ctx.entity.custom = {
244
+ mesh: 'box',
245
+ color: 0x4488ff,
246
+ sx: 1.5,
247
+ sy: 1.5,
248
+ sz: 1.5,
249
+ label: 'SPAWNER'
250
+ }
251
+
252
+ ctx.time.every(CONFIG.spawnInterval, () => {
253
+ if (ctx.state.entities.size >= CONFIG.maxEntities) return
254
+
255
+ const id = \`spawned_\${ctx.state.nextId++}\`
256
+ const pos = [
257
+ ctx.entity.position[0] + (Math.random() - 0.5) * 4,
258
+ ctx.entity.position[1] + 2,
259
+ ctx.entity.position[2] + (Math.random() - 0.5) * 4
260
+ ]
261
+
262
+ ctx.world.spawn(id, {
263
+ position: pos,
264
+ app: CONFIG.entityApp
265
+ })
266
+ ctx.state.entities.add(id)
267
+ })
268
+ },
269
+
270
+ onMessage(ctx, msg) {
271
+ if (msg.type === 'entity_destroyed') {
272
+ ctx.state.entities.delete(msg.entityId)
273
+ }
274
+ },
275
+
276
+ teardown(ctx) {
277
+ ctx.state.entities.forEach(id => ctx.world.destroy(id))
278
+ ctx.state.entities.clear()
279
+ }
280
+ },
281
+
282
+ client: {
283
+ render(ctx) {
284
+ return {
285
+ position: ctx.entity.position,
286
+ rotation: ctx.entity.rotation,
287
+ custom: ctx.entity.custom
288
+ }
289
+ }
290
+ }
291
+ }`
292
+ }
293
+
294
+ return templates[templateType] || templates.simple
295
+ }
296
+
297
+ function createApp(name, template) {
298
+ const appsDir = resolve('apps')
299
+ const appDir = join(appsDir, name)
300
+
301
+ if (existsSync(appDir)) {
302
+ console.error(`Error: App '${name}' already exists at ${appDir}`)
303
+ process.exit(1)
304
+ }
305
+
306
+ mkdirSync(appDir, { recursive: true })
307
+
308
+ const indexJsPath = join(appDir, 'index.js')
309
+ const indexJsContent = getTemplateContent(template)
310
+ writeFileSync(indexJsPath, indexJsContent)
311
+
312
+ console.log(`✓ Created app: ${name}`)
313
+ console.log(` Location: ${appDir}`)
314
+ console.log(` Template: ${template}`)
315
+ console.log(`\nTo test your app:`)
316
+ console.log(` 1. Start server: npm start`)
317
+ console.log(` 2. Connect to http://localhost:3001`)
318
+ console.log(` 3. Spawn entity with app: \`${name}\``)
319
+ console.log(` 4. Edit ${indexJsPath} to make changes`)
320
+ console.log(` 5. Server hot-reloads automatically`)
321
+ }
322
+
323
+ const args = parseArgs(process.argv.slice(2))
324
+
325
+ if (!args.name) {
326
+ console.error('Error: App name required')
327
+ showHelp()
328
+ process.exit(1)
329
+ }
330
+
331
+ if (args.template && !TEMPLATES[args.template]) {
332
+ console.error(`Error: Unknown template '${args.template}'`)
333
+ console.log(`Available: ${Object.keys(TEMPLATES).join(', ')}`)
334
+ process.exit(1)
335
+ }
336
+
337
+ createApp(args.name, args.template)
@@ -0,0 +1,301 @@
1
+ import * as THREE from 'three'
2
+
3
+ export class ARControls {
4
+ constructor(options = {}) {
5
+ this.enabled = false
6
+ this.options = {
7
+ placementMode: true,
8
+ planeDetection: true,
9
+ scale: 1,
10
+ ...options
11
+ }
12
+
13
+ this.session = null
14
+ this.referenceSpace = null
15
+ this.hitTestSource = null
16
+ this.planeDetected = false
17
+ this.anchorPlaced = false
18
+ this.anchorPosition = new THREE.Vector3()
19
+ this.anchorRotation = new THREE.Quaternion()
20
+ this.cameraPosition = new THREE.Vector3()
21
+ this.cameraQuaternion = new THREE.Quaternion()
22
+ this.localOrigin = new THREE.Vector3()
23
+ this.offsetTransform = null
24
+
25
+ this.planes = new Map()
26
+ this.planeMeshes = []
27
+
28
+ this.reticle = null
29
+ this.reticleVisible = false
30
+ }
31
+
32
+ createReticle() {
33
+ const geometry = new THREE.RingGeometry(0.15, 0.2, 32).rotateX(-Math.PI / 2)
34
+ const material = new THREE.MeshBasicMaterial({
35
+ color: 0x00ff00,
36
+ opacity: 0.7,
37
+ transparent: true,
38
+ side: THREE.DoubleSide
39
+ })
40
+ this.reticle = new THREE.Mesh(geometry, material)
41
+ this.reticle.visible = false
42
+ return this.reticle
43
+ }
44
+
45
+ createPlaneMesh(plane) {
46
+ const geometry = new THREE.PlaneGeometry(1, 1)
47
+ const material = new THREE.MeshBasicMaterial({
48
+ color: 0x00ff88,
49
+ opacity: 0.2,
50
+ transparent: true,
51
+ side: THREE.DoubleSide
52
+ })
53
+ const mesh = new THREE.Mesh(geometry, material)
54
+ mesh.userData.plane = plane
55
+ return mesh
56
+ }
57
+
58
+ async init(renderer) {
59
+ if (!navigator.xr) {
60
+ console.warn('[AR] WebXR not supported')
61
+ return false
62
+ }
63
+
64
+ const isSupported = await navigator.xr.isSessionSupported('immersive-ar')
65
+ if (!isSupported) {
66
+ console.warn('[AR] immersive-ar not supported')
67
+ return false
68
+ }
69
+
70
+ this.renderer = renderer
71
+ return true
72
+ }
73
+
74
+ async start() {
75
+ if (!this.renderer) return false
76
+
77
+ try {
78
+ const sessionInit = {
79
+ requiredFeatures: ['local-floor'],
80
+ optionalFeatures: ['hit-test', 'plane-detection', 'anchors', 'dom-overlay'],
81
+ domOverlay: { root: document.body }
82
+ }
83
+
84
+ this.session = await navigator.xr.requestSession('immersive-ar', sessionInit)
85
+ this.renderer.xr.setSession(this.session)
86
+ this.renderer.xr.setReferenceSpaceType('local-floor')
87
+
88
+ this.referenceSpace = await this.session.requestReferenceSpace('local-floor')
89
+ const viewerSpace = await this.session.requestReferenceSpace('viewer')
90
+
91
+ if (this.session.enabledFeatures?.includes('hit-test')) {
92
+ this.hitTestSource = await this.session.requestHitTestSource({ space: viewerSpace })
93
+ }
94
+
95
+ this.session.addEventListener('end', () => this.onSessionEnd())
96
+ this.session.addEventListener('planesdetected', (e) => this.onPlanesDetected(e))
97
+
98
+ this.enabled = true
99
+ console.log('[AR] Session started')
100
+ return true
101
+ } catch (err) {
102
+ console.error('[AR] Failed to start session:', err)
103
+ return false
104
+ }
105
+ }
106
+
107
+ async end() {
108
+ if (this.session) {
109
+ await this.session.end()
110
+ }
111
+ }
112
+
113
+ onSessionEnd() {
114
+ this.enabled = false
115
+ this.session = null
116
+ this.hitTestSource = null
117
+ this.planeDetected = false
118
+ this.anchorPlaced = false
119
+ console.log('[AR] Session ended')
120
+ }
121
+
122
+ onPlanesDetected(event) {
123
+ if (!this.options.planeDetection) return
124
+
125
+ for (const plane of event.detectedPlanes) {
126
+ if (!this.planes.has(plane)) {
127
+ const mesh = this.createPlaneMesh(plane)
128
+ this.planes.set(plane, mesh)
129
+ this.planeMeshes.push(mesh)
130
+ }
131
+ }
132
+ this.planeDetected = true
133
+ }
134
+
135
+ update(frame, camera, sceneRoot) {
136
+ if (!this.enabled || !frame) return
137
+
138
+ const pose = frame.getViewerPose(this.referenceSpace)
139
+ if (!pose) return
140
+
141
+ const view = pose.views[0]
142
+ const viewMatrix = new THREE.Matrix4().fromArray(view.transform.matrix)
143
+ viewMatrix.decompose(this.cameraPosition, this.cameraQuaternion, new THREE.Vector3())
144
+
145
+ if (!this.anchorPlaced && this.hitTestSource && this.options.placementMode) {
146
+ const results = frame.getHitTestResults(this.hitTestSource)
147
+ if (results.length > 0) {
148
+ const hit = results[0]
149
+ const pose = hit.getPose(this.referenceSpace)
150
+ if (pose) {
151
+ this.showReticle(pose.transform)
152
+ }
153
+ }
154
+ }
155
+
156
+ if (this.anchorPlaced && sceneRoot) {
157
+ this.updateSceneTransform(sceneRoot)
158
+ }
159
+ }
160
+
161
+ showReticle(transform) {
162
+ if (!this.reticle) return
163
+ this.reticle.position.setFromMatrixPosition(new THREE.Matrix4().fromArray(transform.matrix))
164
+ this.reticle.quaternion.setFromRotationMatrix(new THREE.Matrix4().fromArray(transform.matrix))
165
+ this.reticle.visible = true
166
+ this.reticleVisible = true
167
+ }
168
+
169
+ hideReticle() {
170
+ if (this.reticle) {
171
+ this.reticle.visible = false
172
+ this.reticleVisible = false
173
+ }
174
+ }
175
+
176
+ placeAnchor() {
177
+ if (!this.reticleVisible) return false
178
+
179
+ this.anchorPosition.copy(this.reticle.position)
180
+ this.anchorRotation.copy(this.reticle.quaternion)
181
+ this.anchorPlaced = true
182
+ this.hideReticle()
183
+ console.log('[AR] Anchor placed at:', this.anchorPosition)
184
+ return true
185
+ }
186
+
187
+ updateSceneTransform(sceneRoot) {
188
+ if (!sceneRoot || !this.anchorPlaced) return
189
+
190
+ sceneRoot.position.set(
191
+ -this.anchorPosition.x,
192
+ -this.anchorPosition.y,
193
+ -this.anchorPosition.z
194
+ )
195
+ sceneRoot.updateMatrixWorld(true)
196
+ }
197
+
198
+ placeAtCamera() {
199
+ this.anchorPosition.copy(this.cameraPosition)
200
+ this.anchorRotation.copy(this.cameraQuaternion)
201
+ this.anchorPlaced = true
202
+ this.hideReticle()
203
+ console.log('[AR] Anchor placed at camera:', this.anchorPosition)
204
+ return true
205
+ }
206
+
207
+ localizeAroundFPS(fpsPosition, fpsYaw, fpsPitch) {
208
+ if (!this.anchorPlaced) return
209
+
210
+ const offset = new THREE.Vector3()
211
+ offset.x = -fpsPosition[0]
212
+ offset.y = -fpsPosition[1] - 1.6
213
+ offset.z = -fpsPosition[2]
214
+
215
+ const yawQuat = new THREE.Quaternion()
216
+ yawQuat.setFromAxisAngle(new THREE.Vector3(0, 1, 0), fpsYaw)
217
+
218
+ this.anchorPosition.copy(offset)
219
+ this.anchorRotation.copy(yawQuat)
220
+ }
221
+
222
+ setInitialFPSPosition(fpsPosition, fpsYaw) {
223
+ const offset = new THREE.Vector3()
224
+ offset.x = this.cameraPosition.x
225
+ offset.y = this.cameraPosition.y - fpsPosition[1] - 1.6
226
+ offset.z = this.cameraPosition.z
227
+
228
+ const invYaw = fpsYaw !== undefined ? -fpsYaw : 0
229
+ const yawQuat = new THREE.Quaternion()
230
+ yawQuat.setFromAxisAngle(new THREE.Vector3(0, 1, 0), invYaw)
231
+
232
+ this.anchorPosition.copy(offset)
233
+ this.anchorRotation.copy(yawQuat)
234
+ this.anchorPlaced = true
235
+ this.hideReticle()
236
+ console.log('[AR] Set initial FPS position:', this.anchorPosition)
237
+ return true
238
+ }
239
+
240
+ getPlacementInfo() {
241
+ return {
242
+ placed: this.anchorPlaced,
243
+ position: this.anchorPosition.toArray(),
244
+ rotation: this.anchorRotation.toArray(),
245
+ planeDetected: this.planeDetected
246
+ }
247
+ }
248
+
249
+ dispose() {
250
+ this.end()
251
+ this.planes.clear()
252
+ this.planeMeshes = []
253
+ if (this.reticle) {
254
+ this.reticle.geometry.dispose()
255
+ this.reticle.material.dispose()
256
+ this.reticle = null
257
+ }
258
+ }
259
+ }
260
+
261
+ export async function createARButton(renderer, onStart, onEnd) {
262
+ if (!navigator.xr) return null
263
+
264
+ const isSupported = await navigator.xr.isSessionSupported('immersive-ar')
265
+ if (!isSupported) return null
266
+
267
+ const button = document.createElement('button')
268
+ button.id = 'ar-button'
269
+ button.textContent = 'Enter XR'
270
+ button.style.cssText = `
271
+ position: fixed;
272
+ bottom: 20px;
273
+ left: 50%;
274
+ transform: translateX(-50%);
275
+ padding: 12px 24px;
276
+ background: rgba(0, 150, 0, 0.8);
277
+ color: white;
278
+ border: none;
279
+ border-radius: 8px;
280
+ font-size: 16px;
281
+ font-weight: bold;
282
+ cursor: pointer;
283
+ z-index: 1001;
284
+ touch-action: none;
285
+ `
286
+
287
+ button.addEventListener('click', async () => {
288
+ if (onStart) {
289
+ const started = await onStart()
290
+ if (started) {
291
+ button.textContent = 'Exit AR'
292
+ button.style.background = 'rgba(150, 0, 0, 0.8)'
293
+ if (onEnd) {
294
+ button.onclick = onEnd
295
+ }
296
+ }
297
+ }
298
+ })
299
+
300
+ return button
301
+ }