@materials3d/element 0.6.0 → 0.6.1

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/dist/index.d.ts CHANGED
@@ -19,7 +19,7 @@ declare const ElementBase: typeof HTMLElement;
19
19
  * Also a `config` property and a read-only `handle` getter. Emits `materials3d-ready` (detail =
20
20
  * renderer) and `materials3d-fallback` (detail = reason).
21
21
  */
22
- declare class Materials3DElement extends ElementBase {
22
+ export declare class Materials3DElement extends ElementBase {
23
23
  #private;
24
24
  static get observedAttributes(): string[];
25
25
  /** The live shell handle (null before connect / after disconnect). */
@@ -44,7 +44,7 @@ declare class Materials3DElement extends ElementBase {
44
44
  attributeChangedCallback(name: string): void;
45
45
  }
46
46
  /** Define the element (idempotent, SSR/Node-import-safe). */
47
- declare function register(tag?: string): void;
47
+ export declare function register(tag?: string): void;
48
48
  declare global {
49
49
  interface HTMLElementTagNameMap {
50
50
  /** So `document.querySelector("materials-3d")` types as the element. */
@@ -52,5 +52,5 @@ declare global {
52
52
  }
53
53
  }
54
54
  //#endregion
55
- export { type FallbackReason, type MaterialHandle, Materials3DElement, type SceneConfig, type SnapshotOptions, register };
55
+ export type { FallbackReason, MaterialHandle, SceneConfig, SnapshotOptions };
56
56
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#handle","#config","#scheduleUpdate","#mount","#mountId","#debounce","#boolAttr","#buildConfig","#numAttr","#preset","#src","#update"],"sources":["../src/index.ts"],"sourcesContent":["import { createMaterials, mergeSceneConfig } from \"@materials3d/core\";\nimport type {\n EngineFor,\n RendererKind,\n FallbackReason,\n SceneConfig,\n MaterialHandle,\n MaterialOptions,\n TiltStatus,\n} from \"@materials3d/core\";\n\n/** The attributes a live element answers to. The shell options are read once at mount instead. */\nconst LIVE_ATTRIBUTES = [\"config\", \"src\", \"preset\", \"transparent\", \"paused\"] as const;\n\n// SSR-safe base: `class extends HTMLElement` evaluates HTMLElement at import time, which throws\n// under Node. Fall back to a dummy base there, the element is never instantiated server-side\n// (register() is guarded), so the missing DOM methods are never called.\nconst ElementBase: typeof HTMLElement =\n typeof HTMLElement !== \"undefined\"\n ? HTMLElement\n : (function Materials3DElementBase() {} as unknown as typeof HTMLElement);\n\n/**\n * `<materials-3d>`: the framework-agnostic drop-in (Vue/Svelte/plain HTML). Light DOM, `display:block`.\n *\n * Config attributes: `config` (JSON), `src` (URL to a config JSON), `preset` (name) and\n * `transparent` (drop the backdrop; `\"false\"` or `\"0\"` keeps it), merged in that order, each one\n * level deep (see `mergeSceneConfig`), then the `config` property last. `paused` starts and stops\n * playback. These are LIVE: a change after mount is pushed to the running scene. A `src` is\n * fetched once per URL and a `preset` resolved once per name, so an update re-reads neither.\n *\n * Shell options: `poster`, `poster-fit` (`fill` | `cover` | `contain`), `fade-ms` (poster→canvas\n * crossfade, default 300; `0` swaps instantly), `lazy`, `webgl` (`auto` | `force` | `off`),\n * `min-size` (CSS px) and `renderer` (`webgl`, the default, or `webgpu` for the experimental node\n * engine, a separate bundle). Read once at mount; changing one on a live element does nothing\n * until it is re-connected (the same contract as the React wrapper's mount-time props).\n *\n * Also a `config` property and a read-only `handle` getter. Emits `materials3d-ready` (detail =\n * renderer) and `materials3d-fallback` (detail = reason).\n */\nexport class Materials3DElement extends ElementBase {\n static get observedAttributes(): string[] {\n return [...LIVE_ATTRIBUTES];\n }\n\n #handle: MaterialHandle | null = null;\n #config: Partial<SceneConfig> = {};\n #debounce?: ReturnType<typeof setTimeout>;\n /**\n * Which connect the in-flight `#mount` belongs to. `#mount` awaits the config, and a\n * disconnect + reconnect during that await queues a SECOND mount, without this check both\n * would finish and the first handle would be overwritten undestroyed (a leaked renderer).\n */\n #mountId = 0;\n /** The preset the last build resolved, so an update to another attribute skips the import. */\n #preset: { name: string; make: () => Partial<SceneConfig> } | null = null;\n /** The last `src` fetched successfully; a failed fetch is not cached, so the next update retries. */\n #src: { url: string; config: Partial<SceneConfig> } | null = null;\n\n /** The live shell handle (null before connect / after disconnect). */\n get handle(): MaterialHandle | null {\n return this.#handle;\n }\n\n /**\n * Explicitly ask for the device-orientation sensor. OPTIONAL, and on iOS it opens a modal\n * permission dialog — nothing calls it for you, and a decorative scene should simply go without\n * tilt there. CALL IT FROM A USER GESTURE (iOS grants the sensor only from inside a tap handler)\n * and only once `materials3d-ready` has fired — before that there is no renderer to ask, and the\n * replayed request has left the gesture.\n */\n enableTilt(): Promise<boolean> {\n return this.#handle?.enableTilt() ?? Promise.resolve(false);\n }\n\n /** Where the tilt sensor stands. `\"prompt\"` is exactly when a tap-to-enable affordance helps. */\n tiltStatus(): TiltStatus {\n return this.#handle?.tiltStatus() ?? \"prompt\";\n }\n\n /** Take the next orientation reading as the neutral pose (the reader has changed grip). */\n recenterTilt(): void {\n this.#handle?.recenterTilt();\n }\n\n /** Programmatic config, merged last (over the `preset`/`src`/`config` attributes). */\n get config(): Partial<SceneConfig> {\n return this.#config;\n }\n set config(value: Partial<SceneConfig>) {\n this.#config = value ?? {};\n this.#scheduleUpdate();\n }\n\n connectedCallback(): void {\n if (!this.style.display) this.style.display = \"block\";\n void this.#mount(++this.#mountId);\n }\n\n disconnectedCallback(): void {\n this.#mountId++; // invalidate a mount still awaiting its config\n clearTimeout(this.#debounce);\n this.#handle?.destroy();\n this.#handle = null;\n }\n\n attributeChangedCallback(name: string): void {\n if (!this.#handle) return;\n if (name === \"paused\") {\n if (this.#boolAttr(\"paused\")) this.#handle.pause();\n else this.#handle.play();\n } else {\n this.#scheduleUpdate();\n }\n }\n\n async #mount(id: number): Promise<void> {\n const config = await this.#buildConfig();\n if (id !== this.#mountId || !this.isConnected) return; // superseded while the config resolved\n const minSize = Number(this.getAttribute(\"min-size\"));\n const options: MaterialOptions<RendererKind> = {\n poster: this.getAttribute(\"poster\") ?? undefined,\n posterFit: (this.getAttribute(\"poster-fit\") as MaterialOptions[\"posterFit\"]) ?? undefined,\n fadeMs: this.#numAttr(\"fade-ms\"),\n lazy: this.#boolAttr(\"lazy\"),\n webgl: (this.getAttribute(\"webgl\") as MaterialOptions[\"webgl\"]) ?? undefined,\n minSizeForWebGL: Number.isFinite(minSize) && minSize > 0 ? minSize : undefined,\n paused: this.#boolAttr(\"paused\"),\n // `renderer=\"webgpu\"` fetches the node-renderer build. Anything else, including absent, is\n // the default WebGL engine, the option is opt-in precisely because it is a second bundle.\n renderer: this.getAttribute(\"renderer\") === \"webgpu\" ? \"webgpu\" : undefined,\n onReady: (renderer: EngineFor<RendererKind>) =>\n this.dispatchEvent(new CustomEvent(\"materials3d-ready\", { detail: renderer })),\n onFallback: (reason: FallbackReason) =>\n this.dispatchEvent(new CustomEvent(\"materials3d-fallback\", { detail: reason })),\n };\n this.#handle = createMaterials<RendererKind>(this, config, options);\n }\n\n /** default ← preset ← src JSON ← config attribute ← config property, one level deep each. */\n async #buildConfig(): Promise<Partial<SceneConfig>> {\n let base: Partial<SceneConfig> = {};\n const presetName = this.getAttribute(\"preset\");\n if (presetName) {\n if (this.#preset?.name !== presetName) {\n const { PRESETS, isPresetName } = await import(\"@materials3d/core/presets\");\n const make = isPresetName(presetName)\n ? PRESETS[presetName]\n : (): Partial<SceneConfig> => ({});\n this.#preset = { name: presetName, make };\n }\n base = this.#preset.make();\n }\n const src = this.getAttribute(\"src\");\n if (src) {\n if (this.#src?.url !== src) {\n try {\n const json: unknown = await fetch(src).then((r) => r.json());\n if (json && typeof json === \"object\" && !Array.isArray(json)) {\n this.#src = { url: src, config: json as Partial<SceneConfig> };\n }\n } catch {\n // ignore a failed/invalid config fetch; fall through to whatever we have\n }\n }\n // Cloned so the cache never shares an object with a config the shell may go on to hold.\n if (this.#src?.url === src) base = mergeSceneConfig(base, structuredClone(this.#src.config));\n }\n const transparent = this.#boolAttr(\"transparent\");\n if (transparent !== undefined) base = { ...base, transparentBackground: transparent };\n return mergeSceneConfig(\n mergeSceneConfig(base, parseJson(this.getAttribute(\"config\"))),\n this.#config,\n );\n }\n\n #scheduleUpdate(): void {\n clearTimeout(this.#debounce);\n this.#debounce = setTimeout(() => {\n void this.#update();\n }, 50);\n }\n\n async #update(): Promise<void> {\n const handle = this.#handle;\n if (!handle) return;\n const config = await this.#buildConfig();\n if (this.#handle !== handle) return; // destroyed or remounted while the config resolved\n handle.set(config);\n }\n\n /**\n * A non-negative number attribute, or undefined for the shell default.\n *\n * Not the inline `Number(getAttribute(...))` that `min-size` uses: that leans on `Number(null)`\n * being 0 and rejects 0 as \"absent\", which is fine for a size and wrong for a duration.\n * `fade-ms=\"0\"` means swap with no crossfade at all, so it must not share an encoding with\n * \"not set\". Unparseable and negative still fall through to the default.\n */\n #numAttr(name: string): number | undefined {\n if (!this.hasAttribute(name)) return undefined;\n const value = Number(this.getAttribute(name));\n return Number.isFinite(value) && value >= 0 ? value : undefined;\n }\n\n /** Presence = true; `\"false\"`/`\"0\"` = false; absent = undefined (shell default). */\n #boolAttr(name: string): boolean | undefined {\n if (!this.hasAttribute(name)) return undefined;\n const value = this.getAttribute(name);\n return value !== \"false\" && value !== \"0\";\n }\n}\n\nfunction parseJson(json: string | null): Partial<SceneConfig> {\n if (!json) return {};\n try {\n return JSON.parse(json) as Partial<SceneConfig>;\n } catch {\n return {};\n }\n}\n\n/** Define the element (idempotent, SSR/Node-import-safe). */\nexport function register(tag = \"materials-3d\"): void {\n if (typeof window === \"undefined\" || typeof customElements === \"undefined\") return;\n if (!customElements.get(tag)) customElements.define(tag, Materials3DElement);\n}\n\n// Self-register on import so a bare `import \"@materials3d/element\"` makes <materials-3d> work. Guarded so\n// importing under Node (SSR) is a no-op rather than a ReferenceError.\nregister();\n\ndeclare global {\n interface HTMLElementTagNameMap {\n /** So `document.querySelector(\"materials-3d\")` types as the element. */\n \"materials-3d\": Materials3DElement;\n }\n}\n\nexport type {\n SceneConfig,\n MaterialHandle,\n FallbackReason,\n SnapshotOptions,\n} from \"@materials3d/core\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { createMaterials, mergeSceneConfig } from \"@materials3d/core\";\nimport type {\n EngineFor,\n RendererKind,\n FallbackReason,\n SceneConfig,\n MaterialHandle,\n MaterialOptions,\n TiltStatus,\n} from \"@materials3d/core\";\n\n/** The attributes a live element answers to. The shell options are read once at mount instead. */\nconst LIVE_ATTRIBUTES = [\"config\", \"src\", \"preset\", \"transparent\", \"paused\"] as const;\n\n// SSR-safe base: `class extends HTMLElement` evaluates HTMLElement at import time, which throws\n// under Node. Fall back to a dummy base there, the element is never instantiated server-side\n// (register() is guarded), so the missing DOM methods are never called.\nconst ElementBase: typeof HTMLElement =\n typeof HTMLElement !== \"undefined\"\n ? HTMLElement\n : (function Materials3DElementBase() {} as unknown as typeof HTMLElement);\n\n/**\n * `<materials-3d>`: the framework-agnostic drop-in (Vue/Svelte/plain HTML). Light DOM, `display:block`.\n *\n * Config attributes: `config` (JSON), `src` (URL to a config JSON), `preset` (name) and\n * `transparent` (drop the backdrop; `\"false\"` or `\"0\"` keeps it), merged in that order, each one\n * level deep (see `mergeSceneConfig`), then the `config` property last. `paused` starts and stops\n * playback. These are LIVE: a change after mount is pushed to the running scene. A `src` is\n * fetched once per URL and a `preset` resolved once per name, so an update re-reads neither.\n *\n * Shell options: `poster`, `poster-fit` (`fill` | `cover` | `contain`), `fade-ms` (poster→canvas\n * crossfade, default 300; `0` swaps instantly), `lazy`, `webgl` (`auto` | `force` | `off`),\n * `min-size` (CSS px) and `renderer` (`webgl`, the default, or `webgpu` for the experimental node\n * engine, a separate bundle). Read once at mount; changing one on a live element does nothing\n * until it is re-connected (the same contract as the React wrapper's mount-time props).\n *\n * Also a `config` property and a read-only `handle` getter. Emits `materials3d-ready` (detail =\n * renderer) and `materials3d-fallback` (detail = reason).\n */\nexport class Materials3DElement extends ElementBase {\n static get observedAttributes(): string[] {\n return [...LIVE_ATTRIBUTES];\n }\n\n #handle: MaterialHandle | null = null;\n #config: Partial<SceneConfig> = {};\n #debounce?: ReturnType<typeof setTimeout>;\n /**\n * Which connect the in-flight `#mount` belongs to. `#mount` awaits the config, and a\n * disconnect + reconnect during that await queues a SECOND mount, without this check both\n * would finish and the first handle would be overwritten undestroyed (a leaked renderer).\n */\n #mountId = 0;\n /** The preset the last build resolved, so an update to another attribute skips the import. */\n #preset: { name: string; make: () => Partial<SceneConfig> } | null = null;\n /** The last `src` fetched successfully; a failed fetch is not cached, so the next update retries. */\n #src: { url: string; config: Partial<SceneConfig> } | null = null;\n\n /** The live shell handle (null before connect / after disconnect). */\n get handle(): MaterialHandle | null {\n return this.#handle;\n }\n\n /**\n * Explicitly ask for the device-orientation sensor. OPTIONAL, and on iOS it opens a modal\n * permission dialog — nothing calls it for you, and a decorative scene should simply go without\n * tilt there. CALL IT FROM A USER GESTURE (iOS grants the sensor only from inside a tap handler)\n * and only once `materials3d-ready` has fired — before that there is no renderer to ask, and the\n * replayed request has left the gesture.\n */\n enableTilt(): Promise<boolean> {\n return this.#handle?.enableTilt() ?? Promise.resolve(false);\n }\n\n /** Where the tilt sensor stands. `\"prompt\"` is exactly when a tap-to-enable affordance helps. */\n tiltStatus(): TiltStatus {\n return this.#handle?.tiltStatus() ?? \"prompt\";\n }\n\n /** Take the next orientation reading as the neutral pose (the reader has changed grip). */\n recenterTilt(): void {\n this.#handle?.recenterTilt();\n }\n\n /** Programmatic config, merged last (over the `preset`/`src`/`config` attributes). */\n get config(): Partial<SceneConfig> {\n return this.#config;\n }\n set config(value: Partial<SceneConfig>) {\n this.#config = value ?? {};\n this.#scheduleUpdate();\n }\n\n connectedCallback(): void {\n if (!this.style.display) this.style.display = \"block\";\n void this.#mount(++this.#mountId);\n }\n\n disconnectedCallback(): void {\n this.#mountId++; // invalidate a mount still awaiting its config\n clearTimeout(this.#debounce);\n this.#handle?.destroy();\n this.#handle = null;\n }\n\n attributeChangedCallback(name: string): void {\n if (!this.#handle) return;\n if (name === \"paused\") {\n if (this.#boolAttr(\"paused\")) this.#handle.pause();\n else this.#handle.play();\n } else {\n this.#scheduleUpdate();\n }\n }\n\n async #mount(id: number): Promise<void> {\n const config = await this.#buildConfig();\n if (id !== this.#mountId || !this.isConnected) return; // superseded while the config resolved\n const minSize = Number(this.getAttribute(\"min-size\"));\n const options: MaterialOptions<RendererKind> = {\n poster: this.getAttribute(\"poster\") ?? undefined,\n posterFit: (this.getAttribute(\"poster-fit\") as MaterialOptions[\"posterFit\"]) ?? undefined,\n fadeMs: this.#numAttr(\"fade-ms\"),\n lazy: this.#boolAttr(\"lazy\"),\n webgl: (this.getAttribute(\"webgl\") as MaterialOptions[\"webgl\"]) ?? undefined,\n minSizeForWebGL: Number.isFinite(minSize) && minSize > 0 ? minSize : undefined,\n paused: this.#boolAttr(\"paused\"),\n // `renderer=\"webgpu\"` fetches the node-renderer build. Anything else, including absent, is\n // the default WebGL engine, the option is opt-in precisely because it is a second bundle.\n renderer: this.getAttribute(\"renderer\") === \"webgpu\" ? \"webgpu\" : undefined,\n onReady: (renderer: EngineFor<RendererKind>) =>\n this.dispatchEvent(new CustomEvent(\"materials3d-ready\", { detail: renderer })),\n onFallback: (reason: FallbackReason) =>\n this.dispatchEvent(new CustomEvent(\"materials3d-fallback\", { detail: reason })),\n };\n this.#handle = createMaterials<RendererKind>(this, config, options);\n }\n\n /** default ← preset ← src JSON ← config attribute ← config property, one level deep each. */\n async #buildConfig(): Promise<Partial<SceneConfig>> {\n let base: Partial<SceneConfig> = {};\n const presetName = this.getAttribute(\"preset\");\n if (presetName) {\n if (this.#preset?.name !== presetName) {\n const { PRESETS, isPresetName } = await import(\"@materials3d/core/presets\");\n const make = isPresetName(presetName)\n ? PRESETS[presetName]\n : (): Partial<SceneConfig> => ({});\n this.#preset = { name: presetName, make };\n }\n base = this.#preset.make();\n }\n const src = this.getAttribute(\"src\");\n if (src) {\n if (this.#src?.url !== src) {\n try {\n const json: unknown = await fetch(src).then((r) => r.json());\n if (json && typeof json === \"object\" && !Array.isArray(json)) {\n this.#src = { url: src, config: json as Partial<SceneConfig> };\n }\n } catch {\n // ignore a failed/invalid config fetch; fall through to whatever we have\n }\n }\n // Cloned so the cache never shares an object with a config the shell may go on to hold.\n if (this.#src?.url === src) base = mergeSceneConfig(base, structuredClone(this.#src.config));\n }\n const transparent = this.#boolAttr(\"transparent\");\n if (transparent !== undefined) base = { ...base, transparentBackground: transparent };\n return mergeSceneConfig(\n mergeSceneConfig(base, parseJson(this.getAttribute(\"config\"))),\n this.#config,\n );\n }\n\n #scheduleUpdate(): void {\n clearTimeout(this.#debounce);\n this.#debounce = setTimeout(() => {\n void this.#update();\n }, 50);\n }\n\n async #update(): Promise<void> {\n const handle = this.#handle;\n if (!handle) return;\n const config = await this.#buildConfig();\n if (this.#handle !== handle) return; // destroyed or remounted while the config resolved\n handle.set(config);\n }\n\n /**\n * A non-negative number attribute, or undefined for the shell default.\n *\n * Not the inline `Number(getAttribute(...))` that `min-size` uses: that leans on `Number(null)`\n * being 0 and rejects 0 as \"absent\", which is fine for a size and wrong for a duration.\n * `fade-ms=\"0\"` means swap with no crossfade at all, so it must not share an encoding with\n * \"not set\". Unparseable and negative still fall through to the default.\n */\n #numAttr(name: string): number | undefined {\n if (!this.hasAttribute(name)) return undefined;\n const value = Number(this.getAttribute(name));\n return Number.isFinite(value) && value >= 0 ? value : undefined;\n }\n\n /** Presence = true; `\"false\"`/`\"0\"` = false; absent = undefined (shell default). */\n #boolAttr(name: string): boolean | undefined {\n if (!this.hasAttribute(name)) return undefined;\n const value = this.getAttribute(name);\n return value !== \"false\" && value !== \"0\";\n }\n}\n\nfunction parseJson(json: string | null): Partial<SceneConfig> {\n if (!json) return {};\n try {\n return JSON.parse(json) as Partial<SceneConfig>;\n } catch {\n return {};\n }\n}\n\n/** Define the element (idempotent, SSR/Node-import-safe). */\nexport function register(tag = \"materials-3d\"): void {\n if (typeof window === \"undefined\" || typeof customElements === \"undefined\") return;\n if (!customElements.get(tag)) customElements.define(tag, Materials3DElement);\n}\n\n// Self-register on import so a bare `import \"@materials3d/element\"` makes <materials-3d> work. Guarded so\n// importing under Node (SSR) is a no-op rather than a ReferenceError.\nregister();\n\ndeclare global {\n interface HTMLElementTagNameMap {\n /** So `document.querySelector(\"materials-3d\")` types as the element. */\n \"materials-3d\": Materials3DElement;\n }\n}\n\nexport type {\n SceneConfig,\n MaterialHandle,\n FallbackReason,\n SnapshotOptions,\n} from \"@materials3d/core\";\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@materials3d/element",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Drop-in Materials3D scene for Vue, Svelte, or plain HTML: the <materials-3d> custom element",
5
5
  "keywords": [
6
6
  "custom-elements",
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@materials3d/core": "^0.6.0"
42
+ "@materials3d/core": "^0.6.1"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "@types/three": ">=0.180",