@needle-tools/engine 4.8.3-next.bc4f9a4 → 4.8.4-experimental.c93e134

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 (43) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +5 -0
  3. package/components.needle.json +1 -1
  4. package/dist/{gltf-progressive-DVx_cW0s.js → gltf-progressive-B3JW4cAu.js} +246 -243
  5. package/dist/{gltf-progressive-DLhfUtEV.min.js → gltf-progressive-DorC035H.min.js} +5 -5
  6. package/dist/{gltf-progressive-CHV7_60B.umd.cjs → gltf-progressive-PB_58h1b.umd.cjs} +6 -6
  7. package/dist/{needle-engine.bundle-CwXTBzUe.min.js → needle-engine.bundle-BLVnT5UY.min.js} +137 -127
  8. package/dist/{needle-engine.bundle-CLdEfO-e.js → needle-engine.bundle-Bx5wfOs0.js} +4761 -4529
  9. package/dist/{needle-engine.bundle-Bn8kOFud.umd.cjs → needle-engine.bundle-DhD3EKds.umd.cjs} +133 -123
  10. package/dist/needle-engine.d.ts +15 -15
  11. package/dist/needle-engine.js +411 -409
  12. package/dist/needle-engine.min.js +1 -1
  13. package/dist/needle-engine.umd.cjs +1 -1
  14. package/lib/engine/extensions/KHR_materials_variants.d.ts +25 -0
  15. package/lib/engine/extensions/KHR_materials_variants.js +113 -0
  16. package/lib/engine/extensions/KHR_materials_variants.js.map +1 -0
  17. package/lib/engine/extensions/extensions.js +2 -0
  18. package/lib/engine/extensions/extensions.js.map +1 -1
  19. package/lib/engine/extensions/index.d.ts +1 -0
  20. package/lib/engine/extensions/index.js +1 -0
  21. package/lib/engine/extensions/index.js.map +1 -1
  22. package/lib/engine/webcomponents/buttons.js +6 -2
  23. package/lib/engine/webcomponents/buttons.js.map +1 -1
  24. package/lib/engine/webcomponents/needle menu/needle-menu.js +10 -0
  25. package/lib/engine/webcomponents/needle menu/needle-menu.js.map +1 -1
  26. package/lib/engine/webcomponents/needle-engine.loading.js +1 -1
  27. package/lib/engine/webcomponents/needle-engine.loading.js.map +1 -1
  28. package/lib/engine-components/MaterialVariants.d.ts +52 -0
  29. package/lib/engine-components/MaterialVariants.js +210 -0
  30. package/lib/engine-components/MaterialVariants.js.map +1 -0
  31. package/lib/engine-components/codegen/components.d.ts +1 -0
  32. package/lib/engine-components/codegen/components.js +1 -0
  33. package/lib/engine-components/codegen/components.js.map +1 -1
  34. package/package.json +3 -3
  35. package/plugins/vite/poster-client.js +1 -1
  36. package/src/engine/extensions/KHR_materials_variants.ts +179 -0
  37. package/src/engine/extensions/extensions.ts +2 -0
  38. package/src/engine/extensions/index.ts +1 -0
  39. package/src/engine/webcomponents/buttons.ts +6 -2
  40. package/src/engine/webcomponents/needle menu/needle-menu.ts +10 -0
  41. package/src/engine/webcomponents/needle-engine.loading.ts +1 -1
  42. package/src/engine-components/MaterialVariants.ts +231 -0
  43. package/src/engine-components/codegen/components.ts +1 -0
@@ -0,0 +1,179 @@
1
+ import { type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
2
+ import { MaterialVariants } from "../../engine-components/MaterialVariants.js";
3
+ import { addComponent } from "../engine_components.js";
4
+ import { InstantiateIdProvider } from "../engine_networking_instantiate.js";
5
+ import { GLTF } from "../engine_types.js";
6
+ import { getParam } from "../engine_utils.js";
7
+
8
+ const debug = getParam("debugmaterialvariants");
9
+
10
+ export interface MaterialVariant {
11
+ name: string;
12
+ }
13
+
14
+ export interface RootMaterialVariantsExtension {
15
+ variants: MaterialVariant[];
16
+ }
17
+
18
+ export interface PrimitiveMaterialVariantsExtension {
19
+ mappings: Array<{
20
+ variants: number[];
21
+ material: number;
22
+ }>;
23
+ }
24
+
25
+ export class NEEDLE_material_variants implements GLTFLoaderPlugin {
26
+ get name() { return "KHR_materials_variants"; }
27
+
28
+ private parser: GLTFParser;
29
+ private variants: MaterialVariant[] = [];
30
+ private loadingMesh = false;
31
+
32
+
33
+ constructor(parser: GLTFParser) {
34
+ this.parser = parser;
35
+ }
36
+
37
+ beforeRoot(): Promise<void> | null {
38
+ const parser = this.parser;
39
+ const json = parser.json;
40
+
41
+ if (!json.extensions || !json.extensions[this.name]) {
42
+ return null;
43
+ }
44
+
45
+ const extension = json.extensions[this.name] as RootMaterialVariantsExtension;
46
+ this.variants = extension.variants || [];
47
+
48
+ if (debug) {
49
+ console.log("KHR_materials_variants found variants:", this.variants);
50
+ }
51
+
52
+ return null;
53
+ }
54
+
55
+ afterRoot(result: GLTF): Promise<void> | null {
56
+ if (this.variants.length > 0) {
57
+ // Store variants data on the GLTF result for components to access
58
+ if (!result.userData) result.userData = {};
59
+ result.userData.materialVariants = {
60
+ variants: this.variants,
61
+ parser: this.parser
62
+ };
63
+
64
+ if (debug) {
65
+ console.log("KHR_materials_variants stored variants data:", result.userData.materialVariants);
66
+ }
67
+ }
68
+
69
+
70
+ return null;
71
+ }
72
+
73
+ loadMesh(index: number): Promise<any> | null {
74
+ // Prevent infinite recursion
75
+ if (this.loadingMesh) {
76
+ return null;
77
+ }
78
+
79
+ const json = this.parser.json;
80
+ const meshDef = json.meshes[index];
81
+
82
+ if (!meshDef || !meshDef.primitives) {
83
+ return null;
84
+ }
85
+
86
+ // Check if any primitive has variant mappings
87
+ const allMappings: Array<{ variants: number[]; material: number; primitiveIndex: number; }> = [];
88
+
89
+ meshDef.primitives.forEach((primitive: any, primitiveIndex: number) => {
90
+ if (primitive.extensions && primitive.extensions[this.name]) {
91
+ const extension = primitive.extensions[this.name] as PrimitiveMaterialVariantsExtension;
92
+
93
+ extension.mappings.forEach(mapping => {
94
+ allMappings.push({
95
+ ...mapping,
96
+ primitiveIndex
97
+ });
98
+ });
99
+ }
100
+ });
101
+
102
+ // If no variants, let normal loading proceed
103
+ if (allMappings.length === 0) {
104
+ return null;
105
+ }
106
+
107
+ // Load the mesh with recursion protection
108
+ this.loadingMesh = true;
109
+
110
+ return this.parser.loadMesh(index).then(mesh => {
111
+ if (mesh) {
112
+ // Attach variant data directly to the loaded mesh
113
+ this.attachVariantDataToMesh(mesh, index, allMappings);
114
+
115
+ if (debug) {
116
+ console.log(`KHR_materials_variants processed mesh ${index} with ${allMappings.length} variant mappings`);
117
+ }
118
+ }
119
+ return mesh;
120
+ }).finally(() => {
121
+ this.loadingMesh = false;
122
+ });
123
+ }
124
+
125
+
126
+ private attachVariantDataToMesh(mesh: any, index: number, mappings: Array<{ variants: number[]; material: number; primitiveIndex: number; }>): void {
127
+ // Store variant mapping data on the mesh
128
+ if (!mesh.userData) mesh.userData = {};
129
+ mesh.userData.materialVariants = {
130
+ mappings: mappings,
131
+ variants: this.variants,
132
+ parser: this.parser
133
+ };
134
+
135
+ const idProvider = new InstantiateIdProvider(index);
136
+
137
+ // Automatically add the MaterialVariants component
138
+ try {
139
+ const component = addComponent(mesh, MaterialVariants, {}, { callAwake: false });
140
+ component.guid = idProvider.generateUUID();
141
+ if (debug) {
142
+ console.log("KHR_materials_variants added MaterialVariants component to mesh:", mesh.name);
143
+ }
144
+ } catch (error) {
145
+ console.warn("Failed to add MaterialVariants component to mesh:", mesh.name, error);
146
+ }
147
+
148
+ if (debug) {
149
+ console.log("KHR_materials_variants processed mesh with variants:", mesh.name, {
150
+ variants: this.variants,
151
+ mappings: mappings
152
+ });
153
+ }
154
+ }
155
+
156
+
157
+
158
+
159
+ // /**
160
+ // * Get all available variant names
161
+ // */
162
+ // getVariantNames(): string[] {
163
+ // return this.variants.map(variant => variant.name);
164
+ // }
165
+
166
+ // /**
167
+ // * Get variant by name
168
+ // */
169
+ // getVariantByName(name: string): MaterialVariant | undefined {
170
+ // return this.variants.find(variant => variant.name === name);
171
+ // }
172
+
173
+ // /**
174
+ // * Get variant by index
175
+ // */
176
+ // getVariantByIndex(index: number): MaterialVariant | undefined {
177
+ // return this.variants[index];
178
+ // }
179
+ }
@@ -9,6 +9,7 @@ import { type ConstructorConcrete, GLTF, type SourceIdentifier } from "../engine
9
9
  import { getParam } from "../engine_utils.js";
10
10
  import { NEEDLE_lightmaps } from "../extensions/NEEDLE_lightmaps.js";
11
11
  import { EXT_texture_exr } from "./EXT_texture_exr.js";
12
+ import { NEEDLE_material_variants } from "./KHR_materials_variants.js";
12
13
  import { NEEDLE_components } from "./NEEDLE_components.js";
13
14
  import { NEEDLE_gameobject_data } from "./NEEDLE_gameobject_data.js";
14
15
  import { NEEDLE_lighting_settings } from "./NEEDLE_lighting_settings.js";
@@ -105,6 +106,7 @@ export async function registerExtensions(loader: GLTFLoader, context: Context, u
105
106
  loader.register(p => new NEEDLE_techniques_webgl(p, url));
106
107
  loader.register(p => new NEEDLE_render_objects(p, url));
107
108
  loader.register(p => new NEEDLE_progressive(p));
109
+ loader.register(p => new NEEDLE_material_variants(p));
108
110
  loader.register(p => new EXT_texture_exr(p));
109
111
  if (isResourceTrackingEnabled()) loader.register(p => new InternalUsageTrackerPlugin(p))
110
112
 
@@ -1,5 +1,6 @@
1
1
  export { compareAssociation } from "./extension_utils.js"
2
2
  export * from "./extensions.js"
3
+ export * from "./KHR_materials_variants.js"
3
4
  export * from "./NEEDLE_animator_controller_model.js"
4
5
  export { SceneLightSettings } from "./NEEDLE_lighting_settings.js"
5
6
  export * from "./NEEDLE_progressive.js"
@@ -218,7 +218,11 @@ export class ButtonsFactory {
218
218
  await generateAndInsertQRCode();
219
219
  // TODO: make sure it doesnt overflow the screen
220
220
  // we need to add the qrCodeContainer to the body to get the correct size
221
- document.body.appendChild(qrCodeContainer);
221
+ // TODO: we would need to search for the right engine element to insert this into if there are more
222
+ // Insert the QR code overlay inside the needle-engine element
223
+ const engine_element = document.body.querySelector("needle-engine");
224
+ const parent = engine_element || document.body;
225
+ parent.appendChild(qrCodeContainer);
222
226
  const containerRect = qrCodeElement.getBoundingClientRect();
223
227
  const buttonRect = qrCodeButton.getBoundingClientRect();
224
228
  qrCodeContainer.style.left = (buttonRect.left + buttonRect.width * .5 - containerRect.width * .5) + "px";
@@ -245,7 +249,7 @@ export class ButtonsFactory {
245
249
  document.fullscreenElement.appendChild(qrCodeContainer);
246
250
  }
247
251
  else
248
- document.body.appendChild(qrCodeContainer);
252
+ parent.appendChild(qrCodeContainer);
249
253
  }
250
254
 
251
255
  /** hides to QRCode overlay and unsubscribes from events */
@@ -287,6 +287,16 @@ export class NeedleMenuElement extends HTMLElement {
287
287
  // TODO: make host full size again and move the buttons to a wrapper so that we can later easily open e.g. foldouts/dropdowns / use the whole canvas space
288
288
  template.innerHTML = `<style>
289
289
 
290
+ /** Styling attributes that ensure the nested menu z-index does not cause it to overlay elements outside of <needle-engine> */
291
+ :host {
292
+ position: absolute;
293
+ width: 100%;
294
+ height: 100%;
295
+ z-index: 0;
296
+ top: 0;
297
+ pointer-events: none;
298
+ }
299
+
290
300
  #root {
291
301
  position: absolute;
292
302
  width: auto;
@@ -206,7 +206,7 @@ export class EngineLoadingView implements ILoadingViewHandler {
206
206
  this._loadingElement.style.display = "flex";
207
207
  this._loadingElement.style.alignItems = "center";
208
208
  this._loadingElement.style.justifyContent = "center";
209
- this._loadingElement.style.zIndex = Number.MAX_SAFE_INTEGER.toString();
209
+ this._loadingElement.style.zIndex = "0";
210
210
  this._loadingElement.style.flexDirection = "column";
211
211
  this._loadingElement.style.pointerEvents = "none";
212
212
  this._loadingElement.style.color = "white";
@@ -0,0 +1,231 @@
1
+ import { Material, Mesh } from "three";
2
+ import { syncField } from "../engine/engine_networking_auto.js";
3
+
4
+ import { serializable } from "../engine/engine_serialization_decorator.js";
5
+ import { type MaterialVariant } from "../engine/extensions/KHR_materials_variants.js";
6
+ import { Behaviour } from "./Component.js";
7
+
8
+ export class MaterialVariants extends Behaviour {
9
+
10
+ @syncField()
11
+ @serializable()
12
+ selectedVariant: string = "";
13
+
14
+ @serializable()
15
+ createMenu: boolean = true;
16
+
17
+ private _menu: HTMLElement | null = null;
18
+
19
+ private _variants: MaterialVariant[] = [];
20
+ private _variantMappings: Array<{ variants: number[]; material: number; primitiveIndex: number; }> = [];
21
+ private _originalMaterials: Material[] = [];
22
+ private _mesh: Mesh | null = null;
23
+ private _materialCache = new Map<string, Material>();
24
+
25
+ awake() {
26
+ this._mesh = this.gameObject as any;
27
+ if (!this._mesh || !this._mesh.isMesh) {
28
+ console.warn("MaterialVariants component must be attached to a Mesh object", this.gameObject);
29
+ return;
30
+ }
31
+
32
+ this._initializeVariantData();
33
+ this._cacheOriginalMaterials();
34
+
35
+ if (this.createMenu) this._createVariantSelectionMenu();
36
+ }
37
+ onDestroy(): void {
38
+ this._menu?.remove();
39
+ }
40
+
41
+ private _createVariantSelectionMenu() {
42
+ const selectElement = document.createElement("select");
43
+ selectElement.addEventListener("change", (event) => {
44
+ const selectedVariant = (event.target as HTMLSelectElement).value;
45
+ this.selectVariant(selectedVariant);
46
+ });
47
+
48
+ // Add options for each variant
49
+ this._variants.forEach(variant => {
50
+ const option = document.createElement("option");
51
+ option.value = variant.name;
52
+ option.textContent = this.name + ": " + variantNameToDisplayName(variant.name);
53
+ selectElement.appendChild(option);
54
+ });
55
+
56
+ this._menu = selectElement;
57
+ this.context.menu.appendChild(selectElement);
58
+ }
59
+
60
+ async start() {
61
+ if (this.selectedVariant && this.selectedVariant.length > 0) {
62
+ await this.selectVariant(this.selectedVariant);
63
+ }
64
+ }
65
+
66
+ private _initializeVariantData() {
67
+ if (!this._mesh) return;
68
+
69
+ // Get variant data from the mesh userData (set by the extension)
70
+ const variantData = this._mesh.userData.materialVariants;
71
+ if (variantData) {
72
+ this._variants = variantData.variants || [];
73
+ this._variantMappings = variantData.mappings || [];
74
+ }
75
+
76
+ // Also check if the root GLTF has variant data
77
+ let current = this._mesh.parent;
78
+ while (current) {
79
+ if (current.userData.materialVariants) {
80
+ this._variants = current.userData.materialVariants.variants || [];
81
+ break;
82
+ }
83
+ current = current.parent;
84
+ }
85
+ }
86
+
87
+ private _cacheOriginalMaterials() {
88
+ if (!this._mesh) return;
89
+
90
+ if (Array.isArray(this._mesh.material)) {
91
+ this._originalMaterials = [...this._mesh.material];
92
+ } else {
93
+ this._originalMaterials = [this._mesh.material];
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Get all available variant names
99
+ */
100
+ getVariantNames(): string[] {
101
+ return this._variants.map(variant => variant.name);
102
+ }
103
+
104
+ /**
105
+ * Get the currently selected variant name
106
+ */
107
+ getCurrentVariant(): string {
108
+ return this.selectedVariant;
109
+ }
110
+
111
+ /**
112
+ * Select a variant by name
113
+ * @param variantName The name of the variant to select
114
+ * @returns Promise that resolves to true if the variant was successfully selected, false otherwise
115
+ */
116
+ async selectVariant(variantName: string): Promise<boolean> {
117
+ if (!this._mesh || !variantName) return false;
118
+
119
+ // Find the variant by name
120
+ const variantIndex = this._variants.findIndex(v => v.name === variantName);
121
+ if (variantIndex === -1) {
122
+ console.warn(`Material variant "${variantName}" not found. Available variants:`, this.getVariantNames());
123
+ return false;
124
+ }
125
+
126
+ return await this.selectVariantByIndex(variantIndex);
127
+ }
128
+
129
+ /**
130
+ * Select a variant by index
131
+ * @param variantIndex The index of the variant to select
132
+ * @returns Promise that resolves to true if the variant was successfully selected, false otherwise
133
+ */
134
+ async selectVariantByIndex(variantIndex: number): Promise<boolean> {
135
+ if (!this._mesh || variantIndex < 0 || variantIndex >= this._variants.length) return false;
136
+
137
+ const variant = this._variants[variantIndex];
138
+
139
+ // Find mappings that include this variant
140
+ const applicableMappings = this._variantMappings.filter(mapping =>
141
+ mapping.variants.includes(variantIndex)
142
+ );
143
+
144
+ if (applicableMappings.length === 0) {
145
+ console.warn(`No material mappings found for variant "${variant.name}"`);
146
+ return false;
147
+ }
148
+
149
+ // Apply the variant materials to specific primitives
150
+ for (const mapping of applicableMappings) {
151
+ const materialIndex = mapping.material;
152
+ const primitiveIndex = mapping.primitiveIndex;
153
+ const material = await this._getMaterial(materialIndex);
154
+
155
+ if (material) {
156
+ if (Array.isArray(this._mesh.material)) {
157
+ // For multi-material meshes, update the specific primitive's material
158
+ if (primitiveIndex < this._mesh.material.length) {
159
+ this._mesh.material[primitiveIndex] = material;
160
+ } else {
161
+ console.warn(`Primitive index ${primitiveIndex} out of bounds for material array of length ${this._mesh.material.length}`);
162
+ }
163
+ } else {
164
+ // Single material mesh - replace the entire material
165
+ this._mesh.material = material;
166
+ }
167
+ }
168
+ }
169
+
170
+ this.selectedVariant = variant.name;
171
+ return true;
172
+ }
173
+
174
+ /**
175
+ * Reset to the original materials
176
+ */
177
+ resetToOriginal(): void {
178
+ if (!this._mesh) return;
179
+
180
+ if (Array.isArray(this._mesh.material)) {
181
+ this._mesh.material = [...this._originalMaterials];
182
+ } else {
183
+ this._mesh.material = this._originalMaterials[0];
184
+ }
185
+
186
+ this.selectedVariant = "";
187
+ }
188
+
189
+ private async _getMaterial(materialIndex: number): Promise<Material | null> {
190
+ // First check if we have it cached
191
+ const cacheKey = `material_${materialIndex}`;
192
+ if (this._materialCache.has(cacheKey)) {
193
+ return this._materialCache.get(cacheKey)!;
194
+ }
195
+
196
+ // Try to get the material from the GLTF parser
197
+ const variantData = this._mesh?.userData.materialVariants;
198
+ if (variantData && variantData.parser) {
199
+ try {
200
+ const material = await variantData.parser.getDependency('material', materialIndex);
201
+ this._materialCache.set(cacheKey, material);
202
+ return material;
203
+ } catch (error) {
204
+ console.warn(`Failed to load material at index ${materialIndex}:`, error);
205
+ }
206
+ }
207
+
208
+ return null;
209
+ }
210
+
211
+ /**
212
+ * Check if a variant exists
213
+ * @param variantName The name of the variant to check
214
+ * @returns True if the variant exists
215
+ */
216
+ hasVariant(variantName: string): boolean {
217
+ return this._variants.some(v => v.name === variantName);
218
+ }
219
+
220
+ /**
221
+ * Get the number of available variants
222
+ */
223
+ getVariantCount(): number {
224
+ return this._variants.length;
225
+ }
226
+ }
227
+
228
+ function variantNameToDisplayName(name: string) {
229
+ // Make uppercase after space and beginning (unless number etc)
230
+ return name.replace(/(?:^|\s)\S/g, (match) => match.toUpperCase());
231
+ }
@@ -86,6 +86,7 @@ export { Light } from "../Light.js";
86
86
  export { LODModel } from "../LODGroup.js";
87
87
  export { LODGroup } from "../LODGroup.js";
88
88
  export { LookAtConstraint } from "../LookAtConstraint.js";
89
+ export { MaterialVariants } from "../MaterialVariants.js";
89
90
  export { NeedleMenu } from "../NeedleMenu.js";
90
91
  export { NestedGltf } from "../NestedGltf.js";
91
92
  export { Networking } from "../Networking.js";