lunchboxjs 2.1.10 → 2.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 (37) hide show
  1. package/package.json +5 -5
  2. package/src/auto-components.ts +94 -0
  3. package/src/html-anchor.ts +88 -0
  4. package/src/index.ts +138 -0
  5. package/src/parseAttributeValue.ts +43 -0
  6. package/src/setThreeProperty.ts +50 -0
  7. package/src/three-base.ts +283 -0
  8. package/src/three-lunchbox.ts +295 -0
  9. package/src/types.d.ts +1 -0
  10. package/src/utils.ts +195 -0
  11. package/dist/cypress/e2e/camera.cy.d.ts +0 -1
  12. package/dist/cypress/e2e/core-events.cy.d.ts +0 -1
  13. package/dist/cypress/e2e/core.cy.d.ts +0 -1
  14. package/dist/cypress/e2e/disposal.cy.d.ts +0 -1
  15. package/dist/cypress/e2e/docs-examples.cy.d.ts +0 -0
  16. package/dist/cypress/e2e/extend.cy.d.ts +0 -1
  17. package/dist/cypress/e2e/html-anchor-2.cy.d.ts +0 -0
  18. package/dist/cypress/e2e/html-anchor.cy.d.ts +0 -0
  19. package/dist/cypress/e2e/loader.cy.d.ts +0 -1
  20. package/dist/cypress/e2e/shadow-parents.cy.d.ts +0 -1
  21. package/dist/cypress/e2e/vue.cy.d.ts +0 -1
  22. package/dist/cypress/e2e/wrapped-lunchbox-2.cy.d.ts +0 -1
  23. package/dist/cypress/e2e/wrapped-lunchbox.cy.d.ts +0 -1
  24. package/dist/cypress/support/commands.d.ts +0 -0
  25. package/dist/cypress/support/e2e.d.ts +0 -1
  26. package/dist/cypress.config.d.ts +0 -3
  27. package/dist/demo.d.ts +0 -1
  28. package/dist/src/auto-components.d.ts +0 -3
  29. package/dist/src/html-anchor.d.ts +0 -13
  30. package/dist/src/index.d.ts +0 -77
  31. package/dist/src/parseAttributeValue.d.ts +0 -1
  32. package/dist/src/setThreeProperty.d.ts +0 -1
  33. package/dist/src/three-lunchbox.d.ts +0 -48
  34. package/dist/src/utils.d.ts +0 -18
  35. package/dist/tests/three-lunchbox.test.d.ts +0 -1
  36. package/dist/types.d.ts +0 -17
  37. package/dist/vite.config.d.ts +0 -2
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "lunchboxjs",
3
3
  "files": [
4
- "dist"
4
+ "dist",
5
+ "src"
5
6
  ],
6
7
  "main": "./dist/lunchboxjs.umd.cjs",
7
8
  "module": "./dist/lunchboxjs.js",
@@ -9,12 +10,12 @@
9
10
  ".": {
10
11
  "import": "./dist/lunchboxjs.js",
11
12
  "require": "./dist/lunchboxjs.umd.cjs",
12
- "types": "./dist/src/index.d.ts"
13
+ "types": "./src/types.d.ts"
13
14
  }
14
15
  },
15
- "version": "2.1.10",
16
+ "version": "2.1.11",
16
17
  "type": "module",
17
- "types": "./dist/src/index.d.ts",
18
+ "types": "./src/types.d.ts",
18
19
  "scripts": {
19
20
  "dev": "vite",
20
21
  "build": "tsc && vite build",
@@ -37,7 +38,6 @@
37
38
  "three": "^0.164.1",
38
39
  "typescript": "^5.2.2",
39
40
  "vite": "^5.2.0",
40
- "vite-plugin-dts": "^3.9.1",
41
41
  "vitest": "^3.0.5"
42
42
  }
43
43
  }
@@ -0,0 +1,94 @@
1
+ import * as THREE from 'three';
2
+
3
+ /** All components that will automatically be registered when Lunchbox is initialized. */
4
+ export const autoComponents: Partial<keyof typeof THREE>[] = [
5
+ // ORDER MATTERS HERE!
6
+ // Place the objects most likely to wrap other objects at the beginning of the list.
7
+
8
+ // Main wrappers
9
+ 'WebGLRenderer',
10
+ 'Scene',
11
+ 'Group',
12
+
13
+ // Secondary wrappers (objects, meshes, etc)
14
+ 'Object3D',
15
+ 'Mesh',
16
+ 'Sprite',
17
+
18
+ // Tertiary items (individual geometries, materials, etc)
19
+ // Geometries
20
+ 'BoxGeometry',
21
+ 'BufferGeometry',
22
+ 'CircleGeometry',
23
+ 'ConeGeometry',
24
+ 'CylinderGeometry',
25
+ 'DodecahedronGeometry',
26
+ 'ExtrudeGeometry',
27
+ 'IcosahedronGeometry',
28
+ 'InstancedBufferGeometry',
29
+ 'LatheGeometry',
30
+ 'OctahedronGeometry',
31
+ 'PlaneGeometry',
32
+ 'PolyhedronGeometry',
33
+ 'RingGeometry',
34
+ 'ShapeGeometry',
35
+ 'SphereGeometry',
36
+ 'TetrahedronGeometry',
37
+ 'TorusGeometry',
38
+ 'TorusKnotGeometry',
39
+ 'TubeGeometry',
40
+ 'WireframeGeometry',
41
+ // Materials
42
+ 'PointsMaterial',
43
+ 'ShaderMaterial',
44
+ 'ShadowMaterial',
45
+ 'SpriteMaterial',
46
+ 'MeshToonMaterial',
47
+ 'MeshBasicMaterial',
48
+ 'MeshDepthMaterial',
49
+ 'MeshPhongMaterial',
50
+ 'LineBasicMaterial',
51
+ 'RawShaderMaterial',
52
+ 'MeshMatcapMaterial',
53
+ 'MeshNormalMaterial',
54
+ 'LineDashedMaterial',
55
+ 'MeshLambertMaterial',
56
+ 'MeshStandardMaterial',
57
+ 'MeshDistanceMaterial',
58
+ 'MeshPhysicalMaterial',
59
+ // Lights
60
+ 'Light',
61
+ 'SpotLight',
62
+ 'SpotLightHelper',
63
+ 'PointLight',
64
+ 'PointLightHelper',
65
+ 'AmbientLight',
66
+ 'RectAreaLight',
67
+ 'HemisphereLight',
68
+ 'HemisphereLightHelper',
69
+ 'DirectionalLight',
70
+ 'DirectionalLightHelper',
71
+ // Cameras
72
+ 'CubeCamera',
73
+ 'ArrayCamera',
74
+ 'StereoCamera',
75
+ 'PerspectiveCamera',
76
+ 'OrthographicCamera',
77
+ // Textures
78
+ 'Texture',
79
+ 'CubeTexture',
80
+ 'DataTexture',
81
+ 'DepthTexture',
82
+ 'VideoTexture',
83
+ 'CanvasTexture',
84
+ 'CompressedTexture',
85
+ // Misc
86
+ 'CatmullRomCurve3',
87
+ 'Points',
88
+ 'Raycaster',
89
+ 'CameraHelper',
90
+ 'Color',
91
+
92
+ // Loaders
93
+ 'TextureLoader',
94
+ ];
@@ -0,0 +1,88 @@
1
+ import { html, LitElement } from "lit";
2
+ import { Lunchbox, ThreeLunchbox } from ".";
3
+ import * as THREE from "three";
4
+ import { closestPassShadow } from "./utils";
5
+
6
+ export class HtmlAnchor extends LitElement {
7
+ private parentLunchbox: Lunchbox<THREE.Object3D> | null = null;
8
+ private frame = -1;
9
+ private scratchV3 = new THREE.Vector3();
10
+
11
+ /** Try attaching the update function to pass the parent's position to children. */
12
+ tryAttachUpdate() {
13
+ const instance = this.parentLunchbox?.instance;
14
+ if (!instance) return false;
15
+ if (!instance.isObject3D) throw new Error('html-anchor must be the child of an Object3D');
16
+
17
+ // const lunchboxParent = closestPassShadow(this, 'three-lunchbox') as ThreeLunchbox | null;
18
+ const lunchboxParent = closestPassShadow(this, (el) => {
19
+ return !!(el as ThreeLunchbox)?.three?.renderer;
20
+ }) as Pick<ThreeLunchbox, 'three'> | null;
21
+
22
+ if (!lunchboxParent) {
23
+ console.error('three-lunchbox parent required for html-anchor');
24
+ return false;
25
+ }
26
+ const camera = lunchboxParent.three.camera;
27
+ if (!camera) {
28
+ console.error('camera required for html-anchor');
29
+ return false;
30
+ }
31
+ const renderer = lunchboxParent.three.renderer;
32
+ if (!renderer?.domElement) {
33
+ console.error('renderer and DOM element required for html-anchor');
34
+ return false;
35
+ }
36
+
37
+ const update = () => {
38
+ this.frame = requestAnimationFrame(update);
39
+ instance.getWorldPosition(this.scratchV3);
40
+ this.scratchV3.project(camera);
41
+ this.scratchV3.multiplyScalar(0.5).addScalar(0.5);
42
+ this.scratchV3.y = 1 - this.scratchV3.y;
43
+ const rendererSize = this.scratchV3.clone().set(renderer.domElement.width, renderer.domElement.height, 1);
44
+ rendererSize.divideScalar(devicePixelRatio);
45
+ this.scratchV3.multiply(rendererSize);
46
+ Array.from(this.children).forEach(child => {
47
+ (child as unknown as HTMLElement).style.setProperty('--left', `${this.scratchV3.x}px`);
48
+ (child as unknown as HTMLElement).style.setProperty('--top', `${this.scratchV3.y}px`);
49
+ })
50
+ }
51
+ update();
52
+
53
+ return true;
54
+ }
55
+
56
+ // Setup - save local parent and try adding update
57
+ connectedCallback() {
58
+ super.connectedCallback();
59
+
60
+ const parent = this.parentNode as Lunchbox<THREE.Object3D>;
61
+ if (!parent) {
62
+ throw new Error('html-anchor requires a 3D parent');
63
+ }
64
+ this.parentLunchbox = parent;
65
+ const attached = this.tryAttachUpdate();
66
+ if (!attached) {
67
+ this.parentLunchbox.addEventListener('instanceadded', () => {
68
+ const attachedAfterInstanceCreated = this.tryAttachUpdate();
69
+ if (!attachedAfterInstanceCreated) throw new Error('error attaching html-anchor to Object3D')
70
+ }, { once: true });
71
+ }
72
+
73
+ }
74
+
75
+ // Teardown
76
+ disconnectedCallback(): void {
77
+ if (this.frame !== -1) cancelAnimationFrame(this.frame);
78
+ }
79
+
80
+
81
+ protected render(): unknown {
82
+ return html`<slot></slot>`
83
+ }
84
+
85
+ protected createRenderRoot() {
86
+ return this;
87
+ }
88
+ }
package/src/index.ts ADDED
@@ -0,0 +1,138 @@
1
+ import { autoComponents } from './auto-components';
2
+ import { buildClass } from './three-base';
3
+ import { ThreeLunchbox } from './three-lunchbox';
4
+ import { IsClass } from './utils';
5
+ import * as THREE from '../node_modules/@types/three';
6
+ import { HtmlAnchor } from './html-anchor';
7
+
8
+ export * from './three-lunchbox';
9
+ export * from './html-anchor';
10
+ export { ThreeBase } from './three-base';
11
+
12
+ /** Every component in a Lunchbox scene is of the Lunchbox type - it contains its ThreeJS instance
13
+ * as a property called `instance`.
14
+ *
15
+ *
16
+ * ## Basic example
17
+ *
18
+ * HTML:
19
+ *
20
+ * ```html
21
+ * <three-mesh></three-mesh>
22
+ * ```
23
+ *
24
+ * TS:
25
+ *
26
+ * ```ts
27
+ * const mesh = document.querySelector<Lunchbox<THREE.Mesh>>('three-mesh');
28
+ * mesh?.instance; // this is typed as a THREE.Mesh, so you can do things like move it up:
29
+ * mesh?.instance.position.set(0, 1, 0); // - or anything else you can do with a mesh
30
+ * ```
31
+ */
32
+ export type Lunchbox<T = THREE.Object3D> = Element & LunchboxProperties<T>
33
+
34
+ export type LunchboxProperties<T = THREE.Object3D> = {
35
+ instance: T;
36
+ }
37
+
38
+ /** Options for initializing Lunchbox. */
39
+ interface LunchboxOptions {
40
+ /** Add THREE class names that should be registered first here. */
41
+ prependList?: string[];
42
+ }
43
+
44
+ /** Initialize Lunchbox. Required to register Lunchbox components. */
45
+ export const initLunchbox = ({
46
+ prependList = [],
47
+ }: LunchboxOptions = {}) => {
48
+ const toDefine = {
49
+ 'three-lunchbox': ThreeLunchbox,
50
+ 'html-anchor': HtmlAnchor,
51
+ }
52
+
53
+ Object.entries(toDefine).forEach(([k, v]) => {
54
+ if (!customElements.get(k)){
55
+ customElements.define(k, v);
56
+ }
57
+ });
58
+
59
+ // define components
60
+ [...prependList, ...autoComponents].forEach(className => {
61
+ const kebabCase = convertThreeClassToWebComponent(className);
62
+
63
+ // ignore if already defined
64
+ if (customElements.get(kebabCase)) {
65
+ return;
66
+ }
67
+
68
+ const result = buildClass(className as keyof typeof THREE);
69
+ if (result) {
70
+ customElements.define(kebabCase, result);
71
+ }
72
+ });
73
+ };
74
+
75
+ /** Create and register a custom Lunchbox component. For example:
76
+ *
77
+ * ```ts
78
+ * import { CustomGeometry } from 'your-custom-geometry-source';
79
+ * import { extend } from 'lunchboxjs';
80
+ *
81
+ * extend('custom-geometry', CustomGeometry);
82
+ * ```
83
+ *
84
+ * Now in your HTML, you can do:
85
+ *
86
+ * ```html
87
+ * <three-lunchbox>
88
+ * <custom-geometry args="[0, 1, 2]"></custom-geometry>
89
+ * </three-lunchbox>
90
+ * ```
91
+ */
92
+ export const extend = (name: string, classDefinition: IsClass, targetWindow = window) => {
93
+ if (targetWindow.customElements.get(name)) {
94
+ console.log(`${name} already registered as a custom element. Try a different name if registering is still required.`);
95
+ return;
96
+ }
97
+
98
+ const result = buildClass(classDefinition);
99
+ if (result) {
100
+ targetWindow.customElements.define(name, result);
101
+ } else {
102
+ throw new Error(`Could not extend ${name}. The second paramater must be a class.`);
103
+ }
104
+ };
105
+
106
+ // Utilities
107
+ // ==================
108
+ export const THREE_POINTER_MOVE_EVENT_NAME = 'threepointermove';
109
+ export const THREE_MOUSE_MOVE_EVENT_NAME = 'threemousemove';
110
+ export const THREE_CLICK_EVENT_NAME = 'threeclick';
111
+ export const BEFORE_RENDER_EVENT_NAME = 'beforerender';
112
+ export const AFTER_RENDER_EVENT_NAME = 'afterrender';
113
+ export type ThreeIntersectEvent = {
114
+ intersect: THREE.Intersection<THREE.Object3D<THREE.Object3DEventMap>>;
115
+ element: Element | null;
116
+ }
117
+ export interface InstanceEvent<T = unknown> {
118
+ instance: T;
119
+ }
120
+ export type InstanceAddedEvent<T = unknown> = InstanceEvent<T> & {
121
+ parent: THREE.Scene | THREE.Object3D
122
+ }
123
+ export interface LoadedEvent<T = unknown> {
124
+ loaded: T;
125
+ }
126
+
127
+ // Components
128
+ export { autoComponents };
129
+ const convertThreeClassToWebComponent = (className: string) => {
130
+ // convert name to kebab-case; prepend `three-` if needed; `-g-l-` becomes `-gl-`
131
+ let kebabCase = className.split(/\.?(?=[A-Z])/).join('-').toLowerCase().replace(/-g-l-/, '-gl-');
132
+ if (!kebabCase.includes('-')) {
133
+ kebabCase = `three-${kebabCase}`;
134
+ }
135
+ return kebabCase;
136
+ };
137
+ /** The kebab-cased name of the ThreeJS web components automatically registered in Lunchbox. */
138
+ export const webComponentNames = autoComponents.map(convertThreeClassToWebComponent);
@@ -0,0 +1,43 @@
1
+ import { ThreeLunchbox } from "./three-lunchbox";
2
+ import * as THREE from 'three';
3
+
4
+ const valueShortcuts = {
5
+ '$scene': (element: HTMLElement) => {
6
+ // TODO: allow non-wrapper scene
7
+ const el = element.closest('three-lunchbox') as unknown as ThreeLunchbox | null;
8
+ return el?.three.scene;
9
+ },
10
+ '$camera': (element: HTMLElement) => {
11
+ // TODO: allow non-wrapper camera
12
+ const el = element.closest('three-lunchbox') as unknown as ThreeLunchbox | null;
13
+ return el?.three.camera;
14
+ },
15
+ '$renderer': (element: HTMLElement) => {
16
+ // TODO: allow non-wrapper renderer
17
+ const el = element.closest('three-lunchbox') as unknown as ThreeLunchbox | null;
18
+ return el?.three.renderer;
19
+ },
20
+ '$domElement': (element: HTMLElement) => {
21
+ // TODO: allow non-wrapper dom element
22
+ const el = element.closest('three-lunchbox') as unknown as ThreeLunchbox | null;
23
+ return el?.three.renderer?.domElement;
24
+ },
25
+ };
26
+
27
+ export const parseAttributeOrPropertyValue = (targetValue: unknown, element: HTMLElement) => {
28
+ // leave as-is if this isn't a string
29
+ if (typeof targetValue !== 'string') return targetValue;
30
+
31
+ // return `true` for blank values
32
+ if (targetValue === '') return true;
33
+
34
+ // look for Lunchbox-specific shortcuts
35
+ // TODO: allow extending these
36
+ const result = valueShortcuts[targetValue as keyof typeof valueShortcuts]?.(element);
37
+
38
+ // Color support
39
+ if (CSS.supports('color', targetValue)) return new THREE.Color(targetValue);
40
+
41
+ // default - return target value
42
+ return result ?? targetValue;
43
+ };
@@ -0,0 +1,50 @@
1
+ import { get, isNumber, set } from "./utils";
2
+ import * as THREE from 'three';
3
+
4
+ export const setThreeProperty = <T extends object>(target: T, split: string[], parsedValue: unknown) => {
5
+ const property: {
6
+ setScalar?: (n: number) => unknown,
7
+ set?: (...args: unknown[]) => unknown,
8
+ } | undefined = get(target, split);
9
+
10
+ if (isNumber(parsedValue) && property?.setScalar) {
11
+ // Set scalar
12
+ property.setScalar(+parsedValue);
13
+ } else if (property?.set) {
14
+ if (typeof parsedValue === 'string') {
15
+ const asNumbers = parsedValue.split(',');
16
+ const isAllNumbers = asNumbers.every(n => !n.match(/^[^\d,]+$/));
17
+
18
+ // handle hash hex numbers
19
+ if (parsedValue.toLowerCase().trim().match(/^#[\dabcdef]{3,6}$/)?.length) {
20
+ if (parsedValue.length === 4) {
21
+ const str = [parsedValue[1], parsedValue[1], parsedValue[2], parsedValue[2], parsedValue[3], parsedValue[3]].join('');
22
+ property.set(+`0x${str}`);
23
+ } else {
24
+ property.set(+`0x${parsedValue.slice(1)}`);
25
+ }
26
+ } else if (asNumbers?.length && isAllNumbers) {
27
+ // assume this is a string like `1,2,3`
28
+ // and try converting to an array of numbers
29
+ // (we get arrays as strings like this from Vue, for example)
30
+ property.set(...asNumbers.map(n => +n));
31
+ } else {
32
+ property.set(parsedValue);
33
+ }
34
+ }
35
+ else {
36
+ // Set as values in an array
37
+ const parsedValueAsArray = Array.isArray(parsedValue) ? parsedValue : [parsedValue];
38
+ property.set(...parsedValueAsArray);
39
+ }
40
+ } else {
41
+ // Manually set
42
+ set(target, split, parsedValue);
43
+ }
44
+
45
+ // handle common update functions
46
+ const targetAsMaterial = target as THREE.Material;
47
+ if (typeof targetAsMaterial.type === 'string' && targetAsMaterial.type?.toLowerCase().endsWith('material')) {
48
+ targetAsMaterial.needsUpdate = true;
49
+ }
50
+ };