Hello, I hope you can help me. My click is registered in the browser, but not on iOS (AR). What could be causing this?
import { Material, Mesh, Object3D } from "three";
import { serializable } from "@needle-tools/engine";
import { Behaviour, GameObject } from "@needle-tools/engine";
import { IPointerClickHandler, PointerEventData } from "@needle-tools/engine";
import { isDevEnvironment } from "@needle-tools/engine";
import { ObjectRaycaster } from "@needle-tools/engine";
import type { UsdzBehaviour, BehaviorExtension } from "@needle-tools/engine";
import { USDObject, USDZExporterContext } from "@needle-tools/engine";
import { ActionBuilder, BehaviorModel, TriggerBuilder } from "@needle-tools/engine";
// Hilfsfunktion für Raycaster, da wir möglicherweise keinen direkten Zugriff auf die originale haben
function ensureRaycaster(obj: GameObject) {
if (!obj) return;
if (!obj.getComponentInParent(ObjectRaycaster)) {
if (isDevEnvironment())
console.debug("Raycaster on \"" + obj.name + "\" was automatically added, because no raycaster was found in the parent hierarchy.");
obj.addComponent(ObjectRaycaster);
}
}
/**
* Wechselt zyklisch durch eine Liste von Materialien bei jedem Klick.
* Funktioniert zur Laufzeit und in AR (USDZ).
* @category Everywhere Actions
* @group Components
*/
export class CycleMaterialOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour {
/**
* Das Mesh-Objekt, dessen Material geändert werden soll.
* Wenn nicht zugewiesen, wird versucht, das Material auf allen Meshes in diesem GameObject und seinen Kindern zu finden.
*/
@serializable(Object3D)
public targetObject?: Object3D;
/**
* Das ursprĂĽngliche Material, das ersetzt werden soll.
*/
@serializable(Material)
public originalMaterial?: Material;
/**
* Eine Liste von Materialien, durch die zyklisch gewechselt wird.
*/
@serializable(Material)
public materialCycle: Material[] = [];
/**
* Die Dauer des Ăśbergangs-Effekts in Sekunden (nur fĂĽr USDZ/AR).
* @default 0.3
*/
@serializable()
public fadeDuration: number = 0.3;
private currentIndex: number = -1;
private meshesWithMaterial: Mesh[] = [];
// --- AR / USDZ spezifische Variablen ---
private selfModel?: USDObject;
private variantModels: USDObject[][] = [];
private static _startHiddenBehaviours: BehaviorModel[] = [];
private static _isAfterCreateDocumentRun: boolean = false;
start(): void {
ensureRaycaster(this.gameObject);
this.collectMeshes();
if (isDevEnvironment() && this.meshesWithMaterial.length === 0) {
console.warn(`CycleMaterialOnClick: Auf dem Zielobjekt "${this.targetObject?.name ?? this.gameObject.name}" wurde kein Mesh mit dem "Original Material" (${this.originalMaterial?.name}) gefunden.`);
}
if (isDevEnvironment() && this.materialCycle.length === 0) {
console.warn(`CycleMaterialOnClick: Die "Material Cycle"-Liste ist leer. Bitte weisen Sie Materialien zu.`);
}
}
onPointerClick(args: PointerEventData): void {
if (this.materialCycle.length === 0) return;
args.use();
// Index hochzählen und am Ende der Liste wieder von vorne anfangen
this.currentIndex = (this.currentIndex + 1) % this.materialCycle.length;
const newMaterial = this.materialCycle[this.currentIndex];
for (const mesh of this.meshesWithMaterial) {
// Falls das Mesh mehrere Materialien hat
if (Array.isArray(mesh.material)) {
const matIndex = mesh.material.indexOf(this.originalMaterial!);
if (matIndex !== -1) {
// Erstelle eine Kopie des Material-Arrays, um die Szene nicht direkt zu verändern
const newMaterials = [...mesh.material];
newMaterials[matIndex] = newMaterial;
mesh.material = newMaterials;
}
} else if (mesh.material === this.originalMaterial) {
mesh.material = newMaterial;
}
}
// Nach dem ersten Klick ist das "Originalmaterial" das vorherige aus der Cycle-Liste
this.originalMaterial = newMaterial;
}
private collectMeshes() {
this.meshesWithMaterial = [];
const target = this.targetObject ?? this.gameObject;
if (!this.originalMaterial) {
if (isDevEnvironment()) console.error("CycleMaterialOnClick: Bitte weisen Sie ein 'Original Material' zu.");
return;
}
target.traverse((obj: Object3D) => { // Hier expliziten Typ hinzugefĂĽgt
if (obj instanceof Mesh) {
if (Array.isArray(obj.material)) {
if (obj.material.includes(this.originalMaterial!)) {
this.meshesWithMaterial.push(obj);
}
} else if (obj.material === this.originalMaterial) {
this.meshesWithMaterial.push(obj);
}
}
});
}
// --- USDZ Export Implementierung ---
beforeCreateDocument(_ext: BehaviorExtension, _context: USDZExporterContext): void {
// Temporär Fehlerbehandlung für AR-Export hinzufügen
try {
// Reset fĂĽr jeden Export
this.variantModels = [];
CycleMaterialOnClick._isAfterCreateDocumentRun = false;
CycleMaterialOnClick._startHiddenBehaviours = [];
// PrĂĽfung, ob die notwendigen Daten vorhanden sind
if (!this.originalMaterial) {
console.warn("CycleMaterialOnClick AR Export: Kein Original-Material zugewiesen");
}
if (this.materialCycle.length === 0) {
console.warn("CycleMaterialOnClick AR Export: Material-Zyklus ist leer");
}
// Mesh-Sammlung sicherstellen
this.collectMeshes();
} catch (e) {
console.error("CycleMaterialOnClick beforeCreateDocument Error:", e);
}
}
createBehaviours(_ext: BehaviorExtension, model: USDObject, _context: USDZExporterContext): void {
try {
if (this.gameObject.uuid === model.uuid) {
this.selfModel = model;
}
// Prüfe, ob dieses Modell eines der Mesh-Objekte ist, die wir ändern wollen
const matchingMesh = this.meshesWithMaterial.find(m => m.uuid === model.uuid);
if (matchingMesh && this.originalMaterial) {
// Sicher gehen, dass wir ein Parent haben, damit wir Varianten hinzufügen können
if (!model.parent) {
console.log("Erstelle Parent fĂĽr Modell:", model.name);
// Korrekter Konstruktoraufruf mit ID (uuid) als erstem Parameter
const parentUUID = "parent_" + model.uuid;
const parent = USDObject.createEmptyParent(model); // Diese Helper-Methode nutzen anstatt direkt zu konstruieren
if (parent) {
parent.name = model.name + "_parent";
}
}
// Klon fĂĽr das Originalmaterial erstellen (wird das erste sichtbare sein)
const originalClone = model.clone();
originalClone.name += "_variant_original";
originalClone.material = this.originalMaterial;
model.parent?.add(originalClone);
// Alle anderen Varianten erstellen und initial verstecken
const variantsForThisModel: USDObject[] = [originalClone];
// Erstelle Varianten fĂĽr jedes Material im Zyklus
for (let i = 0; i < this.materialCycle.length; i++) {
try {
const material = this.materialCycle[i];
if (!material) continue;
const variantClone = model.clone();
// Einfacheren Namen für besser AR-Kompatibilität
variantClone.name += `_variant_${i}`;
variantClone.material = material;
model.parent?.add(variantClone);
variantsForThisModel.push(variantClone);
} catch (materialError) {
console.error(`Fehler beim Erstellen der Variante fĂĽr Material ${i}:`, materialError);
}
}
this.variantModels.push(variantsForThisModel);
// Das ursprĂĽngliche Mesh "leeren", da es nur noch als Container fĂĽr die Varianten dient
model.geometry = null;
model.material = null;
}
} catch (e) {
console.error("CycleMaterialOnClick createBehaviours Error:", e);
}
}
afterCreateDocument(ext: BehaviorExtension, _context: USDZExporterContext): void {
try {
// Vermeiden von Doppel-AusfĂĽhrungen
if (CycleMaterialOnClick._isAfterCreateDocumentRun || !this.selfModel) return;
CycleMaterialOnClick._isAfterCreateDocumentRun = true;
// Alle Varianten flach machen
const allVariants = this.variantModels.flat();
if (allVariants.length === 0) {
console.warn("CycleMaterialOnClick: Keine Varianten erstellt fĂĽr AR Export");
return;
}
// Vereinfachte AR-Verhaltensweise
// Statt komplexer Verkettungen von Verhalten erstellen wir nur einen direkten Tap-Trigger
// auf das Originalobjekt und wechseln dann direkt die Sichtbarkeit
const tapVariant = allVariants[0]; // Erste Variante als Tap-Trigger verwenden
// Einfachere Variante fĂĽr AR: Wir zeigen alle Materialien beim Tippen nacheinander
for (let i = 0; i < allVariants.length; i++) {
const currentVariant = allVariants[i];
const nextIndex = (i + 1) % allVariants.length;
const nextVariant = allVariants[nextIndex];
try {
// Sehr einfaches Verhalten: Aktuell sichtbar -> Beim Tippen -> Nächstes sichtbar
const showNextAction = ActionBuilder.fadeAction(nextVariant, 0.1, true);
const hideCurrentAction = ActionBuilder.fadeAction(currentVariant, 0.1, false);
const cycleBehavior = new BehaviorModel(
`Tap_${currentVariant.name}_ShowNext`,
TriggerBuilder.tapTrigger(currentVariant),
ActionBuilder.parallel(showNextAction, hideCurrentAction)
);
ext.addBehavior(cycleBehavior);
} catch (behaviorError) {
console.error(`Fehler beim Erstellen des Tap-Verhaltens fĂĽr Variante ${i}:`, behaviorError);
}
}
// Verstecke alle Varianten auĂźer der ersten beim Start
try {
const objectsToHideOnStart = allVariants.slice(1); // Alle auĂźer dem ersten
if (objectsToHideOnStart.length > 0) {
const startHiddenBehaviour = new BehaviorModel(
"HideVariantsOnStart",
TriggerBuilder.sceneStartTrigger(),
ActionBuilder.fadeAction(objectsToHideOnStart, 0, false)
);
ext.addBehavior(startHiddenBehaviour);
}
} catch (startHideError) {
console.error("Fehler beim Erstellen des Start-Versteck-Verhaltens:", startHideError);
}
} catch (e) {
console.error("CycleMaterialOnClick afterCreateDocument Error:", e);
}
}
}