Zum Hauptinhalt springen

3D / Viewer

Die Viewer-Erweiterungen ermöglichen es, den 3D-Canvas und dessen Komponenten anzupassen. Dies umfasst Custom Layout Components, dynamische Modelle, Label-Overlays und Canvas-Wrapper.

Grundstruktur​

Viewer-Erweiterungen werden im viewer Objekt registriert:

// In Plugin.tsx
export default {
// ...
viewer: {
canvas: { hoc: ..., description: "..." },
customLayoutComponents: { /* Ihre Custom Components */ },
models: [ /* Siehe "Dynamische Modelle" Seite */ ],
labels: {
display: { hoc: ..., description: "..." },
mobile: { hoc: ..., description: "..." }
}
}
}

Canvas​

Der Canvas ist das Three.js / React-Three-Fiber Root-Element, das die gesamte 3D-Szene rendert.

canvas​

Überschreibt den Root Canvas-Bereich:

canvas: {
hoc: (Original) => (props) => (
<div className="canvas-wrapper">
<div className="canvas-overlay">
<button className="reset-camera-btn">Kamera zurücksetzen</button>
</div>
<Original {...props} />
</div>
),
description: "Fügt Kamera-Reset-Button über Canvas hinzu"
}

Anwendungsbeispiele:

  • Overlays über dem Canvas
  • Custom Loading-Indikatoren
  • Performance-Monitoring
  • Screenshot-Buttons

Custom Layout Components​

Custom Layout Components sind React-Komponenten, die Sie im Layout-Editor platzieren können. Sie haben Zugriff auf BOM-Daten und Gesamtpreis.

Registrierung​

import type { CustomLayoutComponentProps } from "k3-plugin-api";

const PriceDisplay: React.FC<CustomLayoutComponentProps> = ({
name,
bom,
totalPrice
}) => {
return (
<div className="price-display-3d">
<h3>Gesamtpreis:</h3>
<div className="price">{totalPrice.toFixed(2)} €</div>
<div className="item-count">{bom.length} Artikel</div>
</div>
);
};

// Plugin Descriptor
customLayoutComponents: {
PriceDisplay: {
component: PriceDisplay,
label: "3D Preisanzeige",
description: "Zeigt Preis und Artikelanzahl in der Szene"
}
}

Verwendung im Layout​

Nach der Registrierung kann die Komponente im Layout-Editor verwendet werden:

<CustomLayoutComponent name="PriceDisplay" />

CustomLayoutComponentProps​

Alle Custom Layout Components erhalten diese Props:

interface CustomLayoutComponentProps {
/** Name der Komponente (aus dem Layout-Tag) */
name: string;

/** Live Bill-of-Materials der aktuellen Konfiguration */
bom: K3BomEntry[];

/** Aggregierter Gesamtpreis */
totalPrice: number;

/** Weitere Props aus dem Layout-Tag */
[key: string]: unknown;
}

K3BomEntry​

Jeder BOM-Eintrag hat diese Struktur:

interface K3BomEntry {
article: {
id: number;
no: string; // Artikelnummer
name: string;
description?: string | null;
thumbnail?: string | null;
category?: string | null;
};
qty: number; // Menge
amount?: number | null; // Optional: Override-Menge
price: {
price: number;
unit?: string; // z.B. "€/m"
};
main?: boolean; // true = Hauptartikel, false = Zubehör
}

Beispiel: Erweiterte Artikelliste​

const ArticleList: React.FC<CustomLayoutComponentProps> = ({ bom }) => {
const mainArticles = bom.filter(entry => entry.main);
const accessories = bom.filter(entry => !entry.main);

return (
<div className="article-list-3d">
<section>
<h4>Hauptartikel</h4>
{mainArticles.map((entry, i) => (
<div key={i} className="article-item">
{entry.article.thumbnail && (
<img src={entry.article.thumbnail} alt={entry.article.name} />
)}
<div>
<strong>{entry.article.name}</strong>
<div>Art.-Nr.: {entry.article.no}</div>
<div>Menge: {entry.qty}</div>
</div>
</div>
))}
</section>

{accessories.length > 0 && (
<section>
<h4>Zubehör</h4>
{accessories.map((entry, i) => (
<div key={i} className="article-item accessory">
<span>{entry.article.name}</span>
<span>{entry.qty}x</span>
</div>
))}
</section>
)}
</div>
);
};

Beispiel: Preis-Breakdown​

const PriceBreakdown: React.FC<CustomLayoutComponentProps> = ({ 
bom,
totalPrice
}) => {
const mainTotal = bom
.filter(e => e.main)
.reduce((sum, e) => sum + (e.price.price * e.qty), 0);

const accessoriesTotal = bom
.filter(e => !e.main)
.reduce((sum, e) => sum + (e.price.price * e.qty), 0);

return (
<div className="price-breakdown">
<h3>Preisaufstellung</h3>

<div className="breakdown-item">
<span>Hauptprodukte:</span>
<span>{mainTotal.toFixed(2)} €</span>
</div>

<div className="breakdown-item">
<span>Zubehör:</span>
<span>{accessoriesTotal.toFixed(2)} €</span>
</div>

<div className="breakdown-divider" />

<div className="breakdown-total">
<strong>Gesamt:</strong>
<strong>{totalPrice.toFixed(2)} €</strong>
</div>
</div>
);
};

Custom Props übergeben​

Sie können zusätzliche Props im Layout-Tag übergeben:

<CustomLayoutComponent 
name="PriceDisplay"
showVat={true}
currency="EUR"
/>

In der Komponente:

const PriceDisplay: React.FC<CustomLayoutComponentProps> = ({ 
name,
totalPrice,
showVat,
currency
}) => {
const vat = showVat ? totalPrice * 0.19 : 0;

return (
<div>
<div>Netto: {totalPrice.toFixed(2)} {currency}</div>
{showVat && <div>MwSt (19%): {vat.toFixed(2)} {currency}</div>}
{showVat && <div>Brutto: {(totalPrice + vat).toFixed(2)} {currency}</div>}
</div>
);
};

Dynamische Modelle​

Siehe die Dynamische Modelle Dokumentation für Details zu viewer.models.

Kurz:

viewer: {
models: [
{
type: "vendor.my-model",
label: "Mein Modell",
description: "Ein parametrisches 3D-Modell",
component: MyModelComponent,
materials: ["surface"],
disabledForAR: false,
propsDialog: { /* ... */ },
defaultProps: { /* ... */ }
}
]
}

Labels & Hotspots​

Labels sind Overlay-Elemente, die über der 3D-Szene gerendert werden (z.B. Maßketten, Hotspots).

labels.display​

Überschreibt die Desktop-Label-Anzeige (alias für ui.layout.labelActionDisplay):

labels: {
display: {
hoc: (Original) => (props) => (
<div className="custom-labels-wrapper">
<Original {...props} />
<div className="label-legend">
📏 = Maßkette | 📍 = Hotspot
</div>
</div>
),
description: "Fügt Legende für Labels hinzu"
}
}

labels.mobile​

Überschreibt die mobile Label-Anzeige (alias für ui.layout.mobileLabelActionDisplay):

labels: {
mobile: {
hoc: (Original) => (props) => (
<div className="mobile-labels-optimized">
<Original {...props} />
</div>
),
description: "Optimierte Label-Darstellung für mobile"
}
}

Scene Components (experimentell)​

Experimentell

sceneComponents ist ein experimentelles Feature und kann sich noch ändern.

Scene Components werden direkt in die Three.js-Szene eingefügt:

viewer: {
sceneComponents: {
ambientLight: {
hoc: (Original) => (props) => (
<>
<Original {...props} />
<ambientLight intensity={0.5} />
</>
),
description: "Fügt zusätzliches Umgebungslicht hinzu"
}
}
}

Gängige Namen sind "OrbitControls", "Bounds", "ContactShadows" und die Lichter der Szene – gemeint ist immer der Name des Eintrags im Szenen-JSON des Szenen-Editors.

Orbit-Rotation einschränken​

Soll sich das Produkt nur in einem bestimmten Winkelbereich drehen lassen – etwa eine wandmontierte Veranda, die man nicht von hinten ansehen soll – wickeln Sie die OrbitControls ein:

sceneComponents: {
OrbitControls: {
description: "Schränkt die Drehung ein, wenn die Veranda an der Wand hängt",
hoc: (Wrapped) => (props) => {
const freestanding = useConfigurationVariable("freistehend");
const limit = Math.PI * (85 / 180);
return (
<Wrapped
{...props}
minAzimuthAngle={freestanding ? -Infinity : -limit}
maxAzimuthAngle={freestanding ? Infinity : limit}
/>
);
},
},
}

Spreaden Sie props zuerst, damit die Einstellungen aus dem Szenen-Editor erhalten bleiben, und überschreiben Sie danach nur das, was Ihr Plugin steuert.

Prop niemals weglassen, sondern den Standardwert übergeben

Eine Prop, die zwischen zwei Renderings verschwindet, wird von R3F zurückgesetzt – bei Three.js-Objekten mit Konstruktor-Argumenten (OrbitControls, EffectComposer …) gibt es dafür aber keinen Prototyp-Standardwert, R3F fällt auf 0 zurück. minAzimuthAngle/maxAzimuthAngle einfach nicht mehr zu übergeben klemmt die Drehung also auf [0, 0] und friert sie ein, statt sie freizugeben. Die Ternaries oben liefern immer einen Wert – genau das macht sie sicher.

Bei mehreren Grenzen ist ein Defaults-Objekt übersichtlicher:

// Die echten Standardwerte der OrbitControls, explizit übergeben
const unrestrictedLimits = {
minAzimuthAngle: -Infinity,
maxAzimuthAngle: Infinity,
minPolarAngle: 0,
maxPolarAngle: Math.PI,
};

<Wrapped
{...unrestrictedLimits}
{...props}
{...(active ? engravingLimits : {})}
/>;
Feste Grenzen brauchen kein Plugin

Wenn die Grenzen nicht von der Konfiguration abhängen, tragen Sie minAzimuthAngle/maxAzimuthAngle direkt am OrbitControls-Eintrag im Szenen-JSON des Szenen-Editors ein.

Die Kameraposition selbst setzen Sie nicht hier, sondern mit useSetCameraPosition().

Vollständiges Beispiel​

import type { CustomLayoutComponentProps, HOC } from "k3-plugin-api";

// Custom Layout Component
const ConfigSummary: React.FC<CustomLayoutComponentProps> = ({
bom,
totalPrice
}) => {
return (
<div className="config-summary-3d">
<h3>Ihre Konfiguration</h3>

<div className="summary-stats">
<div className="stat">
<label>Artikel:</label>
<value>{bom.length}</value>
</div>
<div className="stat">
<label>Gesamtpreis:</label>
<value>{totalPrice.toFixed(2)} €</value>
</div>
</div>

<div className="article-preview">
{bom.slice(0, 3).map((entry, i) => (
<div key={i} className="preview-item">
<span>{entry.article.name}</span>
<span className="qty">×{entry.qty}</span>
</div>
))}
{bom.length > 3 && (
<div className="more">+ {bom.length - 3} weitere</div>
)}
</div>
</div>
);
};

// Canvas mit Toolbar
const canvasHOC: HOC = (Original) => (props) => (
<div className="enhanced-canvas">
<div className="canvas-toolbar">
<button>🏠 Ansicht zurücksetzen</button>
<button>📸 Screenshot</button>
</div>
<Original {...props} />
</div>
);

// Labels mit Custom Styling
const labelsHOC: HOC = (Original) => (props) => (
<div className="enhanced-labels">
<Original {...props} />
<style jsx>{`
.enhanced-labels {
/* Custom Label Styles */
}
`}</style>
</div>
);

// Im Plugin registrieren:
export default {
// ...
viewer: {
canvas: {
hoc: canvasHOC,
description: "Fügt Toolbar über Canvas hinzu"
},

customLayoutComponents: {
ConfigSummary: {
component: ConfigSummary,
label: "Konfigurations-Zusammenfassung",
description: "Zeigt kompakte Zusammenfassung in 3D-Szene"
}
},

labels: {
display: {
hoc: labelsHOC,
description: "Custom Label Styling"
}
}
}
};

Best Practices​

1. Performance bei BOM-Berechnungen​

BOM-Daten können groß werden. Memoizen Sie aufwändige Berechnungen:

const totalQty = useMemo(
() => bom.reduce((sum, entry) => sum + entry.qty, 0),
[bom]
);

2. Responsive Design für Custom Components​

Custom Layout Components sollten responsive sein:

const ConfigSummary: React.FC<CustomLayoutComponentProps> = ({ bom }) => {
const [isMobile, setIsMobile] = useState(false);

useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);

return (
<div className={isMobile ? "summary-mobile" : "summary-desktop"}>
{/* ... */}
</div>
);
};

3. Null-Checks für BOM-Daten​

BOM kann während des Ladens leer oder undefined sein:

if (!bom || bom.length === 0) {
return <div>Keine Artikel in der Konfiguration</div>;
}

4. Währungsformatierung​

Nutzen Sie einheitliche Währungsformatierung:

const formatPrice = (price: number) => {
return new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
}).format(price);
};

5. TypeScript verwenden​

Nutzen Sie die bereitgestellten Typen:

import type { 
CustomLayoutComponentProps,
K3BomEntry,
K3ViewerExtensions
} from "k3-plugin-api";

Layout-Editor Verwendung​

Nach der Registrierung können Custom Layout Components im Layout-Editor verwendet werden:

<CustomLayoutComponent name="ConfigSummary" />
<CustomLayoutComponent name="PriceDisplay" showVat={true} />

Sie erscheinen als normale React-Komponenten und können mit CSS positioniert werden.

Debugging​

Loggen Sie Props während der Entwicklung:

const MyComponent: React.FC<CustomLayoutComponentProps> = (props) => {
console.log("Custom Layout Component Props:", props);
console.log("BOM:", props.bom);
console.log("Total Price:", props.totalPrice);

return <div>...</div>;
};

WIP: Weitere Daten​

Work in Progress

Die CustomLayoutComponentProps werden in Zukunft erweitert um:

  • Zugriff auf Merkmalswerte
  • Konfigurationsstatus
  • Kamera-Informationen
  • Weitere Konfigurator-Metadaten