@materials3d/element 0.4.1 → 0.6.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.
- package/README.md +28 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +19 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -35,13 +35,15 @@ registers the class under another tag name.
|
|
|
35
35
|
| `transparent` | drop the backdrop, so the scene composites over the page |
|
|
36
36
|
| `paused` | live; toggles playback |
|
|
37
37
|
| `poster`, `poster-fit` | `poster-fit` is `fill` (default, matches the canvas), `cover` or `contain` |
|
|
38
|
+
| `fade-ms` | poster→canvas crossfade in ms; default `300`, `0` swaps instantly |
|
|
38
39
|
| `lazy`, `webgl`, `min-size` | shell options |
|
|
39
40
|
| `renderer` | `webgpu` selects the experimental WebGPU/TSL engine; anything else is WebGL |
|
|
40
41
|
|
|
41
42
|
Boolean attributes: present means true, `"false"` or `"0"` means false, absent means the shell
|
|
42
43
|
default.
|
|
43
44
|
|
|
44
|
-
`poster`, `poster-fit`, `lazy`, `webgl`, `min-size` and `renderer` are read once at
|
|
45
|
+
`poster`, `poster-fit`, `fade-ms`, `lazy`, `webgl`, `min-size` and `renderer` are read once at
|
|
46
|
+
mount.
|
|
45
47
|
Changing one on a connected element does nothing until it is disconnected and reconnected.
|
|
46
48
|
`config`, `src`, `preset`, `transparent` and `paused` are live.
|
|
47
49
|
|
|
@@ -65,6 +67,31 @@ if (el) {
|
|
|
65
67
|
}
|
|
66
68
|
```
|
|
67
69
|
|
|
70
|
+
### Capture a poster at the pixel ratio it will be shown at
|
|
71
|
+
|
|
72
|
+
`snapshot()` returns the canvas at its **backing-store** size: the element's CSS size times the
|
|
73
|
+
device pixel ratio, capped by `dprMax`. If posters are generated in a headless browser, that
|
|
74
|
+
browser's `deviceScaleFactor` is what decides their resolution — and it defaults to `1`.
|
|
75
|
+
|
|
76
|
+
A poster captured that way is half the resolution the same canvas renders at on a 2x display. The
|
|
77
|
+
browser stretches it to fill the element while the live canvas behind it does not, so the crossfade
|
|
78
|
+
lands on a visible sharpening: edges tighten, and because soft edges bloom outward the poster reads
|
|
79
|
+
as slightly _heavier_ than the live scene rather than merely blurrier.
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
const page = await browser.newPage({
|
|
83
|
+
viewport: { width: 1440, height: 820 },
|
|
84
|
+
deviceScaleFactor: 2,
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Two things go with it. Keep the encoder quality high enough not to ring the high-contrast edges — a
|
|
89
|
+
lossy poster shows up as thin outlines tracing every rim. And remember a fixed-aspect poster is
|
|
90
|
+
stretched by `poster-fit` while the canvas re-frames itself from `camera.fit`, so capture at the
|
|
91
|
+
aspect the element is widest at. Where the two policies disagree — `minVisibleWidth` widens the
|
|
92
|
+
camera but never the poster — expect a mismatch at narrow sizes; `fade-ms="0"` at least stops it
|
|
93
|
+
being a dissolve.
|
|
94
|
+
|
|
68
95
|
Peer dependency: `three >= 0.180 < 1`. `@types/three` is an optional peer.
|
|
69
96
|
|
|
70
97
|
MIT.
|
package/dist/index.d.ts
CHANGED
|
@@ -10,11 +10,11 @@ declare const ElementBase: typeof HTMLElement;
|
|
|
10
10
|
* playback. These are LIVE: a change after mount is pushed to the running scene. A `src` is
|
|
11
11
|
* fetched once per URL and a `preset` resolved once per name, so an update re-reads neither.
|
|
12
12
|
*
|
|
13
|
-
* Shell options: `poster`, `poster-fit` (`fill` | `cover` | `contain`), `
|
|
14
|
-
*
|
|
15
|
-
* `
|
|
16
|
-
*
|
|
17
|
-
* wrapper's mount-time props).
|
|
13
|
+
* Shell options: `poster`, `poster-fit` (`fill` | `cover` | `contain`), `fade-ms` (poster→canvas
|
|
14
|
+
* crossfade, default 300; `0` swaps instantly), `lazy`, `webgl` (`auto` | `force` | `off`),
|
|
15
|
+
* `min-size` (CSS px) and `renderer` (`webgl`, the default, or `webgpu` for the experimental node
|
|
16
|
+
* engine, a separate bundle). Read once at mount; changing one on a live element does nothing
|
|
17
|
+
* until it is re-connected (the same contract as the React wrapper's mount-time props).
|
|
18
18
|
*
|
|
19
19
|
* Also a `config` property and a read-only `handle` getter. Emits `materials3d-ready` (detail =
|
|
20
20
|
* renderer) and `materials3d-fallback` (detail = reason).
|
package/dist/index.js
CHANGED
|
@@ -18,11 +18,11 @@ const ElementBase = typeof HTMLElement !== "undefined" ? HTMLElement : function
|
|
|
18
18
|
* playback. These are LIVE: a change after mount is pushed to the running scene. A `src` is
|
|
19
19
|
* fetched once per URL and a `preset` resolved once per name, so an update re-reads neither.
|
|
20
20
|
*
|
|
21
|
-
* Shell options: `poster`, `poster-fit` (`fill` | `cover` | `contain`), `
|
|
22
|
-
*
|
|
23
|
-
* `
|
|
24
|
-
*
|
|
25
|
-
* wrapper's mount-time props).
|
|
21
|
+
* Shell options: `poster`, `poster-fit` (`fill` | `cover` | `contain`), `fade-ms` (poster→canvas
|
|
22
|
+
* crossfade, default 300; `0` swaps instantly), `lazy`, `webgl` (`auto` | `force` | `off`),
|
|
23
|
+
* `min-size` (CSS px) and `renderer` (`webgl`, the default, or `webgpu` for the experimental node
|
|
24
|
+
* engine, a separate bundle). Read once at mount; changing one on a live element does nothing
|
|
25
|
+
* until it is re-connected (the same contract as the React wrapper's mount-time props).
|
|
26
26
|
*
|
|
27
27
|
* Also a `config` property and a read-only `handle` getter. Emits `materials3d-ready` (detail =
|
|
28
28
|
* renderer) and `materials3d-fallback` (detail = reason).
|
|
@@ -98,6 +98,7 @@ var Materials3DElement = class extends ElementBase {
|
|
|
98
98
|
const options = {
|
|
99
99
|
poster: this.getAttribute("poster") ?? void 0,
|
|
100
100
|
posterFit: this.getAttribute("poster-fit") ?? void 0,
|
|
101
|
+
fadeMs: this.#numAttr("fade-ms"),
|
|
101
102
|
lazy: this.#boolAttr("lazy"),
|
|
102
103
|
webgl: this.getAttribute("webgl") ?? void 0,
|
|
103
104
|
minSizeForWebGL: Number.isFinite(minSize) && minSize > 0 ? minSize : void 0,
|
|
@@ -154,6 +155,19 @@ var Materials3DElement = class extends ElementBase {
|
|
|
154
155
|
if (this.#handle !== handle) return;
|
|
155
156
|
handle.set(config);
|
|
156
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* A non-negative number attribute, or undefined for the shell default.
|
|
160
|
+
*
|
|
161
|
+
* Not the inline `Number(getAttribute(...))` that `min-size` uses: that leans on `Number(null)`
|
|
162
|
+
* being 0 and rejects 0 as "absent", which is fine for a size and wrong for a duration.
|
|
163
|
+
* `fade-ms="0"` means swap with no crossfade at all, so it must not share an encoding with
|
|
164
|
+
* "not set". Unparseable and negative still fall through to the default.
|
|
165
|
+
*/
|
|
166
|
+
#numAttr(name) {
|
|
167
|
+
if (!this.hasAttribute(name)) return void 0;
|
|
168
|
+
const value = Number(this.getAttribute(name));
|
|
169
|
+
return Number.isFinite(value) && value >= 0 ? value : void 0;
|
|
170
|
+
}
|
|
157
171
|
/** Presence = true; `"false"`/`"0"` = false; absent = undefined (shell default). */
|
|
158
172
|
#boolAttr(name) {
|
|
159
173
|
if (!this.hasAttribute(name)) return void 0;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["#handle","#config","#scheduleUpdate","#mount","#mountId","#debounce","#boolAttr","#buildConfig","#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`), `lazy`, `webgl`\n * (`auto` | `force` | `off`), `min-size` (CSS px) and `renderer` (`webgl`, the default, or\n * `webgpu` for the experimental node engine, a separate bundle). Read once at mount; changing one\n * on a live element does nothing until it is re-connected (the same contract as the React\n * 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 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 /** 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":["#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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@materials3d/element",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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.
|
|
42
|
+
"@materials3d/core": "^0.6.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"@types/three": ">=0.180",
|