Skip to content

Commit 3256216

Browse files
authored
Merge pull request #8 from Doxel-AI/fix-headless
Fix headless
2 parents ba063e9 + d208eb2 commit 3256216

File tree

9 files changed

+484
-60
lines changed

9 files changed

+484
-60
lines changed

package-lock.json

Lines changed: 416 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/docs/components/component-guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Available attributes are:
2121
| `dpr` | `Infinity` | [Device pixel ratio](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio). Will automatically adjust to the screen's DPR if set to `Infinity`. |
2222
| `dispatch-after-render` | `false` | Set to `true` to dispatch an `afterrender` `CustomEvent` after every render. See [`<three-lunchbox>` events](/components/events#three-lunchbox-events). |
2323
| `dispatch-before-render` | `false` | Set to `true` to dispatch a `beforerender` `CustomEvent` before every render. See [`<three-lunchbox>` events](/components/events#three-lunchbox-events). |
24+
| `headless` | `false` | Set to `true` to prevent automatic WebGLRenderer initialization. Useful in unit tests, for example. |
2425
| `manual-render` | `false` | Set to `true` to prevent automatic rendering. Note you'll need to call `renderThree()` yourself if this is the case. |
2526
| `renderer` | `null` | Options to pass to the default renderer. Accepts an object that is parsed and whose values are sent to the renderer. See `camera` for formatting. |
2627
| `scene` | `null` | Options to pass to the default scene. Accepts an object that is parsed and whose values are sent to the scene. See `camera` for formatting. |

packages/lunchboxjs/cypress/e2e/disposal.cy.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ describe('vanilla HTML spec', () => {
1313
// ensure scene has correct number of children...
1414
expect(lb.three.scene.children).to.have.length(2);
1515
// ...geometries in memory...
16-
expect(lb.three.renderer.info.memory.geometries).to.eq(2);
16+
expect(lb.three.renderer?.info.memory.geometries).to.eq(2);
1717
// ...and draw calls
18-
expect(lb.three.renderer.info.render.calls).to.eq(2);
18+
expect(lb.three.renderer?.info.render.calls).to.eq(2);
1919
// set child to invisible - note no change to child/geo counts
2020
cy.get('three-mesh').then(async mesh => {
2121
const el = mesh.get(0) as unknown as Lunchbox<THREE.Mesh>;
@@ -25,15 +25,15 @@ describe('vanilla HTML spec', () => {
2525
const el = mesh.get(0) as unknown as Lunchbox<THREE.Mesh>;
2626
expect(el.instance.visible).to.be.false;
2727
expect(lb.three.scene.children).to.have.length(2);
28-
expect(lb.three.renderer.info.memory.geometries).to.eq(2);
28+
expect(lb.three.renderer?.info.memory.geometries).to.eq(2);
2929

3030
// remove child - should see child, geo, and draw call count changes after this
3131
el.remove();
3232
expect(lb.three.scene.children).to.have.length(1);
33-
expect(lb.three.renderer.info.memory.geometries).to.eq(1);
33+
expect(lb.three.renderer?.info.memory.geometries).to.eq(1);
3434
// (note we're waiting a frame so the draw calls count has the opportunity to update)
3535
await new Promise(requestAnimationFrame);
36-
expect(lb.three.renderer.info.render.calls).to.eq(1);
36+
expect(lb.three.renderer?.info.render.calls).to.eq(1);
3737
});
3838
});
3939
});

packages/lunchboxjs/package.json

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,17 @@
1212
"types": "./dist/src/index.d.ts"
1313
}
1414
},
15-
"version": "2.1.0",
15+
"version": "2.1.2",
1616
"type": "module",
1717
"types": "./dist/src/index.d.ts",
1818
"scripts": {
1919
"dev": "vite",
2020
"build": "tsc && vite build",
2121
"preview": "vite preview",
2222
"cy:open": "cypress open",
23-
"test": "cypress run"
23+
"test:e2e": "cypress run",
24+
"test:unit": "vitest",
25+
"test": "npm run test:unit && npm run test:e2e"
2426
},
2527
"dependencies": {
2628
"json5": "^2.2.3",
@@ -31,9 +33,11 @@
3133
"@types/node": "^20.12.7",
3234
"@types/three": "^0.164.0",
3335
"cypress": "^13.9.0",
36+
"happy-dom": "^16.8.1",
3437
"three": "^0.164.1",
3538
"typescript": "^5.2.2",
3639
"vite": "^5.2.0",
37-
"vite-plugin-dts": "^3.9.1"
40+
"vite-plugin-dts": "^3.9.1",
41+
"vitest": "^3.0.5"
3842
}
3943
}

packages/lunchboxjs/src/parseAttributeValue.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const valueShortcuts = {
2020
'$domElement': (element: HTMLElement) => {
2121
// TODO: allow non-wrapper dom element
2222
const el = element.closest('three-lunchbox') as unknown as ThreeLunchbox | null;
23-
return el?.three.renderer.domElement;
23+
return el?.three.renderer?.domElement;
2424
},
2525
};
2626

packages/lunchboxjs/src/three-base.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,13 @@ export const buildClass = <T extends IsClass>(targetClass: keyof typeof THREE |
7878

7979
// Do some attaching based on common use cases
8080
// ==================
81-
const parent = this.parentElement as ThreeBase;
82-
if (parent.instance) {
81+
const parent = this.parentNode as unknown as ThreeBase & { three?: { scene?: THREE.Scene } };
82+
if (parent.instance || parent.three?.scene) {
8383
const thisAsGeometry = this.instance as unknown as THREE.BufferGeometry;
8484
const thisAsMaterial = this.instance as unknown as THREE.Material;
8585
const parentAsMesh = parent.instance as unknown as THREE.Mesh;
8686
// const thisAsLoader = this.instance as unknown as THREE.Loader<U>;
87-
const parentAsAddTarget = parent.instance as unknown as { add?: (item: THREE.Object3D) => void };
87+
const parentAsAddTarget = (parent.instance ?? parent.three?.scene) as unknown as { add?: (item: THREE.Object3D) => void };
8888
// const thisIsALoader = this.tagName.toString().toLowerCase().endsWith('-loader');
8989
const instanceAsObject3d = this.instance as unknown as THREE.Object3D;
9090

packages/lunchboxjs/src/three-lunchbox.ts

Lines changed: 25 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@ export class ThreeLunchbox extends LitElement {
2323
three = {
2424
scene: new THREE.Scene(),
2525
camera: null as null | THREE.Camera,
26-
renderer: new THREE.WebGLRenderer({
27-
antialias: true,
28-
alpha: true,
29-
}),
26+
renderer: null as null | THREE.WebGLRenderer
3027
};
3128

3229
@property()
@@ -35,6 +32,9 @@ export class ThreeLunchbox extends LitElement {
3532
@property()
3633
dpr: number = DEFAULT_DPR;
3734

35+
@property()
36+
headless: boolean = false;
37+
3838
@property({
3939
attribute: 'manual-render',
4040
type: Boolean,
@@ -62,7 +62,7 @@ export class ThreeLunchbox extends LitElement {
6262
this.resizeObserver = new ResizeObserver(entries => {
6363
entries.forEach(({ target, contentRect }) => {
6464
if (target === this as unknown as Element) {
65-
this.three.renderer.setSize(contentRect.width * this.dpr, contentRect.height * this.dpr);
65+
this.three.renderer?.setSize(contentRect.width * this.dpr, contentRect.height * this.dpr);
6666
if (this.three.camera) {
6767
const aspect = contentRect.width / contentRect.height;
6868
if (this.three.camera.type.toLowerCase() === 'perspectivecamera') {
@@ -124,10 +124,14 @@ export class ThreeLunchbox extends LitElement {
124124
}
125125

126126
// Prep mouse info
127-
this.three.renderer.domElement.addEventListener('pointermove', this.onPointerMove.bind(this));
128-
this.three.renderer.domElement.addEventListener('mousemove', this.onPointerMove.bind(this));
129-
this.three.renderer.domElement.addEventListener('click', this.onClick.bind(this));
130-
// this.renderer.domElement.addEventListener('touchstart', this.onClick.bind(this));
127+
if (!this.headless) {
128+
const renderer = new THREE.WebGLRenderer();
129+
renderer.domElement.addEventListener('pointermove', this.onPointerMove.bind(this));
130+
renderer.domElement.addEventListener('mousemove', this.onPointerMove.bind(this));
131+
renderer.domElement.addEventListener('click', this.onClick.bind(this));
132+
// this.renderer.domElement.addEventListener('touchstart', this.onClick.bind(this));
133+
this.three.renderer = renderer;
134+
}
131135

132136

133137
// Kick update loop
@@ -137,11 +141,11 @@ export class ThreeLunchbox extends LitElement {
137141
}
138142

139143
disconnectedCallback(): void {
140-
this.three.renderer.domElement.removeEventListener('pointermove', this.onPointerMove.bind(this));
141-
this.three.renderer.domElement.removeEventListener('mousemove', this.onPointerMove.bind(this));
142-
this.three.renderer.domElement.removeEventListener('click', this.onClick.bind(this));
144+
this.three.renderer?.domElement.removeEventListener('pointermove', this.onPointerMove.bind(this));
145+
this.three.renderer?.domElement.removeEventListener('mousemove', this.onPointerMove.bind(this));
146+
this.three.renderer?.domElement.removeEventListener('click', this.onClick.bind(this));
143147
// this.renderer.domElement.removeEventListener('touchstart', this.onClick.bind(this));
144-
this.three.renderer.dispose();
148+
this.three.renderer?.dispose();
145149
this.resizeObserver.unobserve(this as unknown as Element);
146150

147151
cancelAnimationFrame(this.frame);
@@ -150,21 +154,7 @@ export class ThreeLunchbox extends LitElement {
150154
handleDefaultSlotChange(evt: { target: HTMLSlotElement }) {
151155
evt.target.assignedElements().forEach(el => {
152156
const elAsThree = el as unknown as Lunchbox<unknown>;
153-
if (elAsThree.instance instanceof THREE.Object3D) {
154-
// TODO: optimize so we're not searching through whole scene graph
155-
let alreadyExists = false;
156-
this.three.scene.traverse(child => {
157-
if (alreadyExists) return;
158-
159-
if (child.uuid === (elAsThree.instance as THREE.Object3D).uuid) {
160-
alreadyExists = true;
161-
}
162-
});
163-
if (alreadyExists) return;
164-
165-
// add to scene
166-
this.three.scene.add(elAsThree.instance);
167-
157+
if (elAsThree.instance instanceof THREE.Object3D && el.getAttributeNames().includes(RAYCASTABLE_ATTRIBUTE_NAME)) {
168158
// Add to raycast pool
169159
if (el.getAttributeNames().includes(RAYCASTABLE_ATTRIBUTE_NAME)) {
170160
this.raycastPool.push(elAsThree.instance);
@@ -185,9 +175,12 @@ export class ThreeLunchbox extends LitElement {
185175
}) {
186176
if (!this.raycastPool.length || !this.three.camera) return [];
187177

178+
const domElementWidth = this.three.renderer?.domElement.width ?? 0;
179+
const domElementHeight = this.three.renderer?.domElement.height ?? 0;
180+
188181
const ndc = this.scratchV2.clone().set(
189-
(evt.clientX / (this.three.renderer.domElement.width / this.dpr)) * 2 - 1,
190-
-(evt.clientY / (this.three.renderer.domElement.height / this.dpr)) * 2 + 1
182+
(evt.clientX / (domElementWidth / this.dpr)) * 2 - 1,
183+
-(evt.clientY / (domElementHeight / this.dpr)) * 2 + 1
191184
);
192185

193186
this.raycaster.setFromCamera(ndc, this.three.camera);
@@ -272,7 +265,7 @@ export class ThreeLunchbox extends LitElement {
272265
this.dispatchEvent(new CustomEvent<object>(BEFORE_RENDER_EVENT_NAME, {}));
273266
}
274267
if (!this.three.camera) return;
275-
this.three.renderer.render(
268+
this.three.renderer?.render(
276269
overrideScene ?? this.three.scene,
277270
overrideCamera ?? this.three.camera
278271
);
@@ -285,7 +278,7 @@ export class ThreeLunchbox extends LitElement {
285278
// TODO: more robust slot changes
286279
return html`
287280
<slot @slotchange=${this.handleDefaultSlotChange}></slot>
288-
${this.three.renderer.domElement}
281+
${this.three.renderer?.domElement}
289282
`;
290283
}
291284
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Scene } from "three";
2+
import { initLunchbox, ThreeLunchbox } from "../src";
3+
import { beforeAll, describe, expect, it } from 'vitest';
4+
5+
describe('three-lunchbox wrapper', () => {
6+
beforeAll(() => {
7+
initLunchbox();
8+
});
9+
10+
it('mounts the wrapper', () => {
11+
const lunchbox = document.createElement('three-lunchbox') as ThreeLunchbox;
12+
expect(lunchbox.three).toHaveProperty('scene');
13+
expect(lunchbox.three.scene).toBeInstanceOf(Scene);
14+
});
15+
16+
it('mounts example scene objects', async () => {
17+
const lunchbox = document.createElement('three-lunchbox') as ThreeLunchbox;
18+
lunchbox.setAttribute('headless', 'true');
19+
lunchbox.innerHTML = '<three-mesh></three-mesh>';
20+
document.body.append(lunchbox as unknown as Node);
21+
expect(lunchbox.three.scene.children).toHaveLength(1);
22+
});
23+
});

packages/lunchboxjs/vite.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,7 @@ export default defineConfig({
2525
}
2626
},
2727
},
28+
test: {
29+
environment: 'happy-dom'
30+
}
2831
});

0 commit comments

Comments
 (0)