create-playcanvas 0.2.6 → 0.3.0

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 (33) hide show
  1. package/README.md +39 -47
  2. package/dist/index.mjs +48 -47
  3. package/package.json +53 -49
  4. package/templates/index.ts +15 -57
  5. package/templates/react-ts/README.md +32 -59
  6. package/templates/react-ts/eslint.config.mjs +14 -0
  7. package/templates/react-ts/index.html +10 -10
  8. package/templates/react-ts/package.json +34 -27
  9. package/templates/react-ts/prettier.config.mjs +1 -0
  10. package/templates/react-ts/src/App.css +37 -49
  11. package/templates/react-ts/src/App.tsx +38 -41
  12. package/templates/react-ts/src/Scene.tsx +50 -50
  13. package/templates/react-ts/src/index.css +30 -52
  14. package/templates/react-ts/src/main.tsx +9 -8
  15. package/templates/react-ts/src/vite-env.d.ts +12 -0
  16. package/templates/react-ts/tsconfig.json +24 -5
  17. package/templates/react-ts/vite.config.ts +4 -4
  18. package/templates/vanilla-ts/README.md +31 -91
  19. package/templates/vanilla-ts/eslint.config.mjs +3 -0
  20. package/templates/vanilla-ts/index.html +10 -10
  21. package/templates/vanilla-ts/package.json +28 -21
  22. package/templates/vanilla-ts/prettier.config.mjs +1 -0
  23. package/templates/vanilla-ts/src/App.ts +149 -151
  24. package/templates/vanilla-ts/src/main.ts +7 -7
  25. package/templates/vanilla-ts/src/style.css +60 -93
  26. package/templates/vanilla-ts/src/utils.ts +9 -8
  27. package/templates/vanilla-ts/src/vite-env.d.ts +12 -0
  28. package/templates/vanilla-ts/tsconfig.json +22 -22
  29. package/index.js +0 -3
  30. package/templates/react-ts/scripts/ensure-node-version.cjs +0 -12
  31. package/templates/react-ts/tsconfig.app.json +0 -27
  32. package/templates/react-ts/tsconfig.node.json +0 -25
  33. package/templates/vanilla-ts/scripts/ensure-node-version.cjs +0 -12
@@ -1,10 +1,11 @@
1
- import { StrictMode } from 'react'
2
- import { createRoot } from 'react-dom/client'
3
- import './index.css'
4
- import App from './App.tsx'
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+
4
+ import './index.css';
5
+ import App from './App.tsx';
5
6
 
6
7
  createRoot(document.getElementById('root')!).render(
7
- <StrictMode>
8
- <App />
9
- </StrictMode>,
10
- )
8
+ <StrictMode>
9
+ <App />
10
+ </StrictMode>
11
+ );
@@ -1 +1,13 @@
1
1
  /// <reference types="vite/client" />
2
+
3
+ declare module 'playcanvas/scripts/esm/camera-controls.mjs' {
4
+ import type { Script } from 'playcanvas';
5
+
6
+ export const CameraControls: typeof Script & (new () => Script);
7
+ }
8
+
9
+ declare module 'playcanvas/scripts/esm/grid.mjs' {
10
+ import type { Script } from 'playcanvas';
11
+
12
+ export const Grid: typeof Script & (new () => Script);
13
+ }
@@ -1,7 +1,26 @@
1
1
  {
2
- "files": [],
3
- "references": [
4
- { "path": "./tsconfig.app.json" },
5
- { "path": "./tsconfig.node.json" }
6
- ]
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "verbatimModuleSyntax": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+ "jsx": "react-jsx",
16
+
17
+ /* Linting */
18
+ "strict": true,
19
+ "noUnusedLocals": true,
20
+ "noUnusedParameters": true,
21
+ "erasableSyntaxOnly": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["src", "vite.config.ts"]
7
26
  }
@@ -1,7 +1,7 @@
1
- import { defineConfig } from 'vite'
2
- import react from '@vitejs/plugin-react'
1
+ import react from '@vitejs/plugin-react';
2
+ import { defineConfig } from 'vite';
3
3
 
4
4
  // https://vite.dev/config/
5
5
  export default defineConfig({
6
- plugins: [react()],
7
- })
6
+ plugins: [react()]
7
+ });
@@ -1,114 +1,54 @@
1
- # PlayCanvas + Vite Starter
1
+ # PlayCanvas + TypeScript Starter
2
2
 
3
- A modern, batteries-included starter for building WebGL/PlayCanvas experiences with TypeScript.
4
-
5
- * **PlayCanvas** `^2.8.2` – high-performance WebGL engine
6
- * **Vite** `^7` – ultra-fast dev server & bundler with HMR
7
- * **TypeScript** `~5.8` – static typing out-of-the-box
8
- * **ESLint** `^9` – linting with recommended TypeScript rules
9
-
10
- ---
3
+ A Vite-powered PlayCanvas Engine project with TypeScript, hot module replacement, ESLint and Prettier.
11
4
 
12
5
  ## Prerequisites
13
6
 
14
- * **Node.js ≥ 20.19.0** (or ≥ 22.12.0). Use [nvm](https://github.com/nvm-sh/nvm) to install/switch:
15
-
16
- ```bash
17
- nvm install 22 # or 20.19+
18
- nvm use 22
19
- ```
20
-
21
- ---
7
+ Node.js 22.23.2 or later.
22
8
 
23
- ## Getting Started
24
-
25
- Clone the repo and install dependencies:
9
+ ## Getting started
26
10
 
27
11
  ```bash
28
- git clone <your-repo-url> playcanvas-project
12
+ npm create playcanvas@latest playcanvas-project -- --template vanilla-ts
29
13
  cd playcanvas-project
30
14
  npm install
31
- ```
32
-
33
- Start the dev server with hot-module reload:
34
-
35
- ```bash
36
15
  npm run dev
37
16
  ```
38
17
 
39
- Open <http://localhost:5173> to view the app. Any file changes trigger instant reloads.
40
-
41
- ---
18
+ Open <http://localhost:5173>. Edit `src/App.ts` to test hot module replacement.
42
19
 
43
20
  ## Scripts
44
21
 
45
- | Command | Description |
46
- |--------------------|--------------------------------------|
47
- | `npm run dev` | Start Vite dev server with HMR |
48
- | `npm run build` | Type-check & bundle for production |
49
- | `npm run preview` | Preview the production build locally |
50
- | `npm run lint` | Run ESLint over the codebase |
22
+ | Command | Description |
23
+ | ------------------- | --------------------------------- |
24
+ | `npm run dev` | Start the Vite development server |
25
+ | `npm run build` | Build for production |
26
+ | `npm run start` | Preview the production build |
27
+ | `npm run lint` | Run ESLint |
28
+ | `npm run fmt` | Check formatting |
29
+ | `npm run typecheck` | Run TypeScript checks |
51
30
 
52
- ---
31
+ ## Project structure
53
32
 
54
- ## Project Structure
55
-
56
- ```
33
+ ```text
57
34
  .
58
- ├── public/ # Static assets copied as-is
35
+ ├── public/
36
+ │ └── environment-map.png
59
37
  ├── src/
60
- │ ├── components/
61
- │ │ └── PlayCanvasCanvas.tsx # React wrapper around a PlayCanvas application
62
- │ ├── App.tsx # Entry component
63
- │ └── main.tsx # Vite/React bootstrap
64
- ├── vite.config.ts # Vite + React plugin config
65
- └── ...
66
- ```
67
-
68
- ### `PlayCanvasCanvas.tsx` (concept)
69
-
70
- ```tsx
71
- import { useEffect, useRef } from 'react'
72
- import * as pc from 'playcanvas'
73
-
74
- export default function PlayCanvasCanvas() {
75
- const canvasRef = useRef<HTMLCanvasElement>(null)
76
-
77
- useEffect(() => {
78
- const canvas = canvasRef.current!
79
- const app = new pc.Application(canvas, {})
80
- app.start()
81
- // demo scene setup here…
82
-
83
- return () => app.destroy() // cleanup on unmount
84
- }, [])
85
-
86
- return <canvas ref={canvasRef} style={{ width: '100%', height: '100%' }} />
87
- }
38
+ │ ├── assets/
39
+ │ ├── App.ts
40
+ │ ├── main.ts
41
+ │ └── style.css
42
+ ├── eslint.config.mjs
43
+ ├── index.html
44
+ └── tsconfig.json
88
45
  ```
89
46
 
90
- Use the component anywhere in your React tree to embed a PlayCanvas scene.
91
-
92
- ---
93
-
94
- ## Linting & Formatting
95
-
96
- ESLint is pre-configured with TypeScript, React hooks, React Refresh, and global browser/node globals. Adjust `eslint.config.js` as your codebase grows.
97
-
98
- ---
99
-
100
- ## Deployment
101
-
102
- Run `npm run build` to generate a static production bundle in `dist/`. Deploy the contents of that folder to any static hosting provider (Netlify, Vercel, GitHub Pages, Cloudflare Pages, etc.).
103
-
104
- ---
105
-
106
- ## Further Reading
107
-
108
- * [PlayCanvas Engine Docs](https://developer.playcanvas.com/en/api/)
109
- * [Vite Docs](https://vitejs.dev/)
110
- * [TypeScript Handbook](https://www.typescriptlang.org/docs/)
47
+ Run `npm run build` to generate a deployable static site in `dist/`.
111
48
 
112
- ---
49
+ ## Further reading
113
50
 
114
- Made with ❤️ and ☕ by **Your Name**. PRs and issues welcome!
51
+ - [PlayCanvas Engine manual](https://developer.playcanvas.com/user-manual/engine/)
52
+ - [PlayCanvas examples](https://playcanvas.github.io/)
53
+ - [Vite documentation](https://vite.dev/)
54
+ - [TypeScript documentation](https://www.typescriptlang.org/docs/)
@@ -0,0 +1,3 @@
1
+ import typescriptConfig from '@playcanvas/eslint-config/typescript';
2
+
3
+ export default typescriptConfig;
@@ -1,13 +1,13 @@
1
1
  <!doctype html>
2
2
  <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>PlayCanvas + TS</title>
8
- </head>
9
- <body>
10
- <div id="root"></div>
11
- <script type="module" src="/src/main.ts"></script>
12
- </body>
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/png" href="/src/assets/playcanvas.png" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>PlayCanvas + TS</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.ts"></script>
12
+ </body>
13
13
  </html>
@@ -1,23 +1,30 @@
1
1
  {
2
- "name": "playcanvas-project",
3
- "private": true,
4
- "version": "0.0.0",
5
- "type": "module",
6
- "scripts": {
7
- "predev": "node ./scripts/ensure-node-version.cjs",
8
- "dev": "vite",
9
- "build": "tsc && vite build",
10
- "preview": "vite preview"
11
- },
12
- "dependencies": {
13
- "playcanvas": "^2.17.2"
14
- },
15
- "devDependencies": {
16
- "semver": "^7.7.4",
17
- "typescript": "~6.0.2",
18
- "vite": "^8.0.8"
19
- },
20
- "engines": {
21
- "node": "^20.19.0 || >=22.12.0"
22
- }
2
+ "name": "playcanvas-project",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "description": "A PlayCanvas TypeScript project",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "vite build",
9
+ "dev": "vite",
10
+ "fmt": "prettier --check .",
11
+ "fmt:fix": "prettier --write .",
12
+ "lint": "eslint .",
13
+ "lint:fix": "eslint . --fix",
14
+ "start": "vite preview",
15
+ "typecheck": "tsc --noEmit"
16
+ },
17
+ "dependencies": {
18
+ "playcanvas": "^2.21.3"
19
+ },
20
+ "devDependencies": {
21
+ "@playcanvas/eslint-config": "3.0.0-beta.8",
22
+ "eslint": "^9.39.5",
23
+ "prettier": "^3.9.6",
24
+ "typescript": "~6.0.3",
25
+ "vite": "^8.2.0"
26
+ },
27
+ "engines": {
28
+ "node": ">=22.23.2"
29
+ }
23
30
  }
@@ -0,0 +1 @@
1
+ export { default } from '@playcanvas/eslint-config/prettier';
@@ -1,35 +1,30 @@
1
+ import type { Layer, Texture } from 'playcanvas';
1
2
  import {
2
- createGraphicsDevice,
3
- AppBase,
4
- AppOptions,
5
- RenderComponentSystem,
6
- CameraComponentSystem,
7
- ScriptComponentSystem,
8
- TextureHandler,
9
- ContainerHandler,
10
- FILLMODE_FILL_WINDOW,
11
- RESOLUTION_AUTO,
12
- Entity,
13
- Color,
14
- Vec3,
15
- Picker,
16
- EVENT_MOUSEMOVE,
17
- EVENT_MOUSEUP,
18
- TouchDevice,
19
- Mouse,
20
- MeshInstance,
21
- StandardMaterial,
22
- Layer,
23
- Asset,
24
- AssetListLoader,
25
- TEXTURETYPE_RGBP
3
+ createGraphicsDevice,
4
+ AppBase,
5
+ AppOptions,
6
+ RenderComponentSystem,
7
+ CameraComponentSystem,
8
+ ScriptComponentSystem,
9
+ TextureHandler,
10
+ ContainerHandler,
11
+ FILLMODE_FILL_WINDOW,
12
+ RESOLUTION_AUTO,
13
+ Entity,
14
+ Color,
15
+ Vec3,
16
+ Picker,
17
+ EVENT_MOUSEMOVE,
18
+ EVENT_MOUSEUP,
19
+ TouchDevice,
20
+ Mouse,
21
+ StandardMaterial,
22
+ Asset,
23
+ AssetListLoader,
24
+ TEXTURETYPE_RGBP
26
25
  } from 'playcanvas';
27
-
28
- // @ts-expect-error - PlayCanvas ESM scripts don't have type declarations
29
- import { Grid } from 'playcanvas/scripts/esm/grid.mjs';
30
-
31
- // @ts-expect-error - PlayCanvas ESM scripts don't have type declarations
32
26
  import { CameraControls } from 'playcanvas/scripts/esm/camera-controls.mjs';
27
+ import { Grid } from 'playcanvas/scripts/esm/grid.mjs';
33
28
 
34
29
  import { throttle } from './utils';
35
30
 
@@ -38,10 +33,15 @@ const DEFAULT_COLOR = new Color(0.827, 0.827, 0.827);
38
33
 
39
34
  // Assets to load
40
35
  const assets = {
41
- envAtlas: new Asset('env-atlas', 'texture', { url: '/environment-map.png' }, {
42
- type: TEXTURETYPE_RGBP,
43
- mipmaps: false
44
- })
36
+ envAtlas: new Asset(
37
+ 'env-atlas',
38
+ 'texture',
39
+ { url: '/environment-map.png' },
40
+ {
41
+ type: TEXTURETYPE_RGBP,
42
+ mipmaps: false
43
+ }
44
+ )
45
45
  };
46
46
 
47
47
  /**
@@ -49,134 +49,132 @@ const assets = {
49
49
  * @param canvas - The canvas element
50
50
  * @param onClick - The function to call when the user clicks on the sphere
51
51
  */
52
- async function setupApp(canvas: HTMLCanvasElement, onClick: () => void) {
52
+ const setupApp = async (canvas: HTMLCanvasElement, onClick: () => void) => {
53
+ if (!canvas) {
54
+ throw new Error('Canvas not found');
55
+ }
53
56
 
54
- if (!canvas) {
55
- throw new Error('Canvas not found');
56
- }
57
+ // Create graphics device
58
+ const device = await createGraphicsDevice(canvas);
57
59
 
58
- // Create graphics device
59
- const device = await createGraphicsDevice(canvas);
60
+ // Create app options
61
+ const createOptions = new AppOptions();
62
+ createOptions.graphicsDevice = device;
63
+ createOptions.mouse = new Mouse(document.body);
64
+ createOptions.touch = new TouchDevice(document.body);
65
+ createOptions.componentSystems = [RenderComponentSystem, CameraComponentSystem, ScriptComponentSystem];
66
+ createOptions.resourceHandlers = [TextureHandler, ContainerHandler];
60
67
 
61
- // Create app options
62
- const createOptions = new AppOptions();
63
- createOptions.graphicsDevice = device;
64
- createOptions.mouse = new Mouse(document.body);
65
- createOptions.touch = new TouchDevice(document.body);
66
- createOptions.componentSystems = [
67
- RenderComponentSystem,
68
- CameraComponentSystem,
69
- ScriptComponentSystem
70
- ];
71
- createOptions.resourceHandlers = [TextureHandler, ContainerHandler];
72
-
73
- // Create app
74
- const app = new AppBase(canvas);
75
- app.init(createOptions);
76
-
77
- // Set the canvas to fill the window
78
- app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
79
- app.setCanvasResolution(RESOLUTION_AUTO);
80
-
81
- // Ensure canvas is resized when window changes size
82
- const resize = () => app.resizeCanvas();
83
- window.addEventListener('resize', resize);
84
-
85
- app.once('destroy', () => {
86
- window.removeEventListener('resize', resize);
87
- });
88
-
89
- // Load assets
90
- await new Promise<void>((resolve) => {
91
- new AssetListLoader(Object.values(assets), app.assets).load(() => resolve());
92
- });
93
-
94
- app.start();
95
-
96
- // Set up environment lighting (no skybox, just IBL)
97
- app.scene.envAtlas = assets.envAtlas.resource;
98
- const skyboxLayer = app.scene.layers.getLayerByName('Skybox');
99
- if (skyboxLayer) {
100
- skyboxLayer.enabled = false;
101
- }
102
-
103
- // Create sphere entity
104
- const sphere = new Entity('sphere');
105
- sphere.setPosition(new Vec3(0, 0.5, 0));
106
-
107
- // Create a new material
108
- const material = new StandardMaterial();
109
- material.diffuse.copy(DEFAULT_COLOR);
110
- material.update();
111
-
112
- sphere.addComponent('render', {
113
- type: 'sphere',
114
- material: material
115
- });
116
- app.root.addChild(sphere);
117
-
118
- // Create camera entity
119
- const camera = new Entity('camera');
120
- camera.addComponent('camera', {
121
- clearColor: new Color(0.09, 0.09, 0.09)
122
- });
123
- camera.setPosition(new Vec3(4, 1, 4));
124
- app.root.addChild(camera);
125
-
126
- // Create camera controls
127
- camera.addComponent('script');
128
- camera.script?.create(CameraControls);
129
-
130
- // Create grid entity
131
- const grid = new Entity('grid');
132
- grid.addComponent('script');
133
- grid.script?.create(Grid);
134
- grid.setLocalScale(1000, 1000, 1000);
135
- app.root.addChild(grid);
136
-
137
- // Create a picker for mouse interaction
138
- const picker = new Picker(app, 1, 1);
139
- const worldLayer = app.scene.layers.getLayerByName('World');
140
-
141
- const intersectsSphere = (x: number, y: number, layer: Layer): Promise<boolean> => {
142
- if (!camera.camera) {
143
- return Promise.resolve(false);
144
- }
68
+ // Create app
69
+ const app = new AppBase(canvas);
70
+ app.init(createOptions);
145
71
 
146
- const pickerScale = 0.5;
147
- picker.resize(canvas.clientWidth * pickerScale, canvas.clientHeight * pickerScale);
72
+ // Set the canvas to fill the window
73
+ app.setCanvasFillMode(FILLMODE_FILL_WINDOW);
74
+ app.setCanvasResolution(RESOLUTION_AUTO);
148
75
 
149
- if (!layer) {
150
- return Promise.resolve(false);
76
+ // Ensure canvas is resized when window changes size
77
+ const resize = () => app.resizeCanvas();
78
+ window.addEventListener('resize', resize);
79
+
80
+ app.once('destroy', () => {
81
+ window.removeEventListener('resize', resize);
82
+ });
83
+
84
+ // Load assets
85
+ await new Promise<void>((resolve) => {
86
+ new AssetListLoader(Object.values(assets), app.assets).load(() => resolve());
87
+ });
88
+
89
+ app.start();
90
+
91
+ // Set up environment lighting (no skybox, just IBL)
92
+ app.scene.envAtlas = assets.envAtlas.resource as Texture;
93
+ const skyboxLayer = app.scene.layers.getLayerByName('Skybox');
94
+ if (skyboxLayer) {
95
+ skyboxLayer.enabled = false;
151
96
  }
152
97
 
153
- picker.prepare(camera.camera, app.scene, [layer]);
98
+ // Create sphere entity
99
+ const sphere = new Entity('sphere');
100
+ sphere.setPosition(new Vec3(0, 0.5, 0));
101
+
102
+ // Create a new material
103
+ const material = new StandardMaterial();
104
+ material.diffuse.copy(DEFAULT_COLOR);
105
+ material.update();
154
106
 
155
- return picker.getSelectionAsync(x * pickerScale, y * pickerScale, 1, 1).then((meshInstances: MeshInstance[]): boolean => {
156
- if (meshInstances.length === 0) return false;
157
- return meshInstances[0] === sphere.render?.meshInstances[0];
107
+ sphere.addComponent('render', {
108
+ type: 'sphere',
109
+ material: material
158
110
  });
159
- };
160
-
161
- // On mouse move, check if hovering over sphere and update cursor/color
162
- app.mouse?.on(EVENT_MOUSEMOVE, throttle((event) => {
163
- if (!worldLayer) return;
164
- intersectsSphere(event.x, event.y, worldLayer).then((intersects) => {
165
- material.diffuse.copy(intersects ? HOVER_COLOR : DEFAULT_COLOR);
166
- document.body.style.cursor = intersects ? 'pointer' : 'default';
167
- material.update();
111
+ app.root.addChild(sphere);
112
+
113
+ // Create camera entity
114
+ const camera = new Entity('camera');
115
+ camera.addComponent('camera', {
116
+ clearColor: new Color(0.09, 0.09, 0.09)
168
117
  });
169
- }, 100));
170
-
171
- // On mouse up, check if clicked on sphere and call onClick
172
- app.mouse?.on(EVENT_MOUSEUP, (event) => {
173
- if (!worldLayer || !onClick) return;
174
- intersectsSphere(event.x, event.y, worldLayer).then((intersects) => {
175
- if (intersects) {
176
- onClick();
177
- }
118
+ camera.setPosition(new Vec3(4, 1, 4));
119
+ app.root.addChild(camera);
120
+
121
+ // Create camera controls
122
+ camera.addComponent('script');
123
+ camera.script?.create(CameraControls);
124
+
125
+ // Create grid entity
126
+ const grid = new Entity('grid');
127
+ grid.addComponent('script');
128
+ grid.script?.create(Grid);
129
+ grid.setLocalScale(1000, 1000, 1000);
130
+ app.root.addChild(grid);
131
+
132
+ // Create a picker for mouse interaction
133
+ const picker = new Picker(app, 1, 1);
134
+ const worldLayer = app.scene.layers.getLayerByName('World');
135
+
136
+ const intersectsSphere = (x: number, y: number, layer: Layer) => {
137
+ if (!camera.camera) {
138
+ return Promise.resolve(false);
139
+ }
140
+
141
+ const pickerScale = 0.5;
142
+ picker.resize(canvas.clientWidth * pickerScale, canvas.clientHeight * pickerScale);
143
+
144
+ if (!layer) {
145
+ return Promise.resolve(false);
146
+ }
147
+
148
+ picker.prepare(camera.camera, app.scene, [layer]);
149
+
150
+ return picker.getSelectionAsync(x * pickerScale, y * pickerScale, 1, 1).then((items) => {
151
+ if (items.length === 0) return false;
152
+ return items[0] === sphere.render?.meshInstances[0];
153
+ });
154
+ };
155
+
156
+ // On mouse move, check if hovering over sphere and update cursor/color
157
+ app.mouse?.on(
158
+ EVENT_MOUSEMOVE,
159
+ throttle((event) => {
160
+ if (!worldLayer) return;
161
+ intersectsSphere(event.x, event.y, worldLayer).then((intersects) => {
162
+ material.diffuse.copy(intersects ? HOVER_COLOR : DEFAULT_COLOR);
163
+ document.body.style.cursor = intersects ? 'pointer' : 'default';
164
+ material.update();
165
+ });
166
+ }, 100)
167
+ );
168
+
169
+ // On mouse up, check if clicked on sphere and call onClick
170
+ app.mouse?.on(EVENT_MOUSEUP, (event) => {
171
+ if (!worldLayer || !onClick) return;
172
+ intersectsSphere(event.x, event.y, worldLayer).then((intersects) => {
173
+ if (intersects) {
174
+ onClick();
175
+ }
176
+ });
178
177
  });
179
- });
180
- }
178
+ };
181
179
 
182
180
  export { setupApp };
@@ -1,7 +1,7 @@
1
- import './style.css'
2
- import typescriptLogo from './assets/typescript.svg'
3
- import playcanvasLogo from './assets/playcanvas.png'
4
- import { setupApp } from './App'
1
+ import './style.css';
2
+ import { setupApp } from './App';
3
+ import playcanvasLogo from './assets/playcanvas.png';
4
+ import typescriptLogo from './assets/typescript.svg';
5
5
 
6
6
  let count = 0;
7
7
 
@@ -33,13 +33,13 @@ document.querySelector<HTMLDivElement>('#root')!.innerHTML = `
33
33
  </p>
34
34
  </div>
35
35
  </div>
36
- `
36
+ `;
37
37
 
38
38
  const counterElement = document.getElementById('counter')!;
39
39
 
40
40
  const increment = () => {
41
- count++;
42
- counterElement.textContent = `Click Count: ${count}`;
41
+ count++;
42
+ counterElement.textContent = `Click Count: ${count}`;
43
43
  };
44
44
 
45
45
  setupApp(document.getElementById('application-canvas') as HTMLCanvasElement, increment);