Inserting Custom Models
This page describes how a standalone K3 plugin inserts its own 3D models into the slots of the Veranda or Qubus structure—including automatic placement.
The basics of K3 plugins can be found at Plugins.
Overview
The structure renders all assigned model instances per slot. External models are automatically assigned:
| Mechanism | What | How to use |
|---|---|---|
parentGeometry prop | Complete structure geometry | is injected into every slot model |
slotAnchor prop | Pre-calculated installation position of the slot | position, rotation, installation space |
autoAnchor | Automatic placement in the slot’s free space | autoAnchor: "1" in defaultProps |
useSlotSpace() | Seamless, collision-free height assignment within the slot | via window.__VERANDA_SDK__.useSlotSpace |
window.__VERANDA_SDK__ | Context, helpers, slot IDs | for advanced cases |
Slot IDs
Veranda (single-slope roof) and Qubus (flat roof) share the same slot IDs with the prefix
konstruktion-slot-.
| Slot Key | Slot ID | Purpose | Default Model |
|---|---|---|---|
| post | konstruktion-slot-pfosten | Support structure | veranda-pfosten-eckig |
| roofing | konstruktion-slot-eindeckung | Roof | veranda-glas-eindeckung |
| gutter | konstruktion-slot-rinne | Rain Gutter | veranda-regenrinne-kasten |
| shading | konstruktion-slot-beschattung | Shading | veranda-markise |
| wallLeft / Right / Front / Back | konstruktion-slot-wand-* | Walls | veranda-wand-keil or -rahmenwand |
| LED | konstruktion-slot-led | LED lighting | — |
| structureLeft / … | veranda-slot-aufbau-* | Structure Walls (sub-slot of the privacy wall) | — |
The structure slots belong to the privacy wall and use the prefix veranda-slot-aufbau-,
not konstruktion-slot-.
Minimal Example: External Gutter with Auto-Placement
__PROTECTED_0__
function MeineRinne(props: any) {
// Durch autoAnchor ist der Ursprung bereits die Rinnen-Einbauposition
// (X-Mitte, Y = Rinnen-Unterkante-Anker, Z = Vorderkante, 180° gedreht).
const anchor = props.slotAnchor; // SlotAnchor (siehe unten)
const geo = props.parentGeometry; // VerandaParentGeometry
const laenge = anchor?.size.breite ?? geo.width;
return (
<group>
<mesh position={[0, anchor.size.hoehe / 2, 0]}>
<boxGeometry args={[laenge, anchor.size.hoehe, anchor.size.tiefe]} />
<meshStandardMaterial color="#808080" />
</mesh>
</group>
);
}
export const meineRinneDynamicModel: DynamicModel = {
type: "mein-plugin-rinne",
label: "Meine Rinne",
defaultProps: {
autoAnchor: "1",
rinnenBreite: { expression: "0.12" },
rinnenHoehe: { expression: "0.08" },
},
component: MeineRinne,
};
Assign the model in K3 to slot konstruktion-slot-rinne — done. The gutter is automatically
built in the correct position, updates in real-time with every change to width, depth, height, and
roof pitch, and the front frame beam of the Qubus is hidden as soon as the gutter slot
is occupied.
The Injected Props
parentGeometry
All construction dimensions are in meters. Here is an excerpt; the complete list can be found in
src/shared/VerandaGeometryContext.tsx:
width,depth,height,innerWidth,innerDepthdachneigungin degrees — for the Qubus, see0; the actual tilt is listed ineindeckungDachneigungpfostenBreite,pfostenTiefe,pfostenAnzahlVorne,pfostenAnzahlHintensparrenAnzahl,sparrenBreite,sparrenHoehe,sparrenAuflageschwelle,schwelleBreite,schwelleHoehe,pfette,pfettenBreite,pfettenHoeherinnenHoehe,rinnenBreiteisQubus,qubusRahmenBreite,qubusRahmenHoehe
slotAnchor
interface SlotAnchor {
position: [number, number, number]; // Einbauposition, Ursprung: Boden-Mitte der Konstruktion
rotation: [number, number, number]; // Euler XYZ, im Bogenmaß
size: { breite: number; hoehe: number; tiefe: number }; // verfügbarer Bauraum
meta: Record<string, number | boolean>; // Slot-Extras
}
What the anchor means for each slot:
| Slot | position | rotation | meta |
|---|---|---|---|
| gutter | Lower edge of the gutter in front of the sill; on the Qubus, it replaces the front beam | 180° around Y | schwelleOK, innenliegend, isQubus |
| roofing | front edge of the support | roof pitch around X | hoeheVorne, hoeheHinten, neigung, dachneigung |
| shading, LED | Front edge of subroof | — | hoeheVorne, hoeheHinten, sparrenHoehe |
| wall, structure | Center of side at the base; local +x along the wall, +z outward | 0, ±90, or 180° around Y | hoeheVorne, hoeheHinten |
| post | Construction origin | — | pfostenBreite, pfostenTiefe |
autoAnchor
If autoAnchor is set to defaultProps on "1" (or { expression: "1" }), the
construction wraps the model into a group at the anchor position. The model then renders only at the
local origin.
Without autoAnchor, the model positions itself—via the slotAnchor prop or its own
logic. This is how the internal models work.
The Slot Layout System
Each slot is embedded in a SlotLayoutProvider. Models within the same slot
register their space requirements and receive a guaranteed gap-free, collision-free allocation.
Space allocation proceeds vertically, from bottom to top, according to priority:
| priority | Significance for walls | Mode |
|---|---|---|
| 0 | Privacy screen (floor) | fixed — exact height |
| 1 | Structure (frame wall, sliding door, shutters) | fill — fills the remainder |
| 2 | Wedge (top edge) | fill — fills the top remaining space |
Each model calls useSlotSpace() exactly once. The algorithm uses this to calculate yOffset
(bottom edge of the zone) and height (allocated height). The sum of all zones always equals exactly
the total slot height.
Example
__PROTECTED_1__
// oder über das SDK:
const { useSlotSpace } = window.__VERANDA_SDK__;
function MeinWandElement({ slotAnchor, parentGeometry }) {
const alloc = useSlotSpace({
priority: 1, // wo im Stack, 0 = unten
mode: 'fill', // 'fixed' = genau desiredHeight, 'fill' = Rest
desiredHeight: 0, // nur bei 'fixed' relevant
minHeight: 0.3, // Mindesthöhe; alloc.fits = false wenn unterschritten
});
// kein Provider im Baum -> null (kein Fehler, Fallback zur eigenen Logik)
if (!alloc) return null;
if (!alloc.fits) return null; // zu wenig Platz
const { yOffset, height } = alloc;
const { size } = slotAnchor;
return (
<group position={[0, yOffset, 0]}>
<mesh scale={[size.breite, height, 0.02]}>
<boxGeometry />
<meshStandardMaterial color="#a0c0e0" />
</mesh>
</group>
);
}
export const meinWandModell = {
type: 'mein-plugin-wand',
label: 'Meine Wand',
defaultProps: { autoAnchor: '1' },
component: MeinWandElement,
};
API
// Registrierungsparameter; die id wird intern erzeugt
interface SlotRegistration {
priority: number; // Stacking-Reihenfolge, 0 = unterste Zone
mode: 'fixed' | 'fill'; // fixed = exakt desiredHeight, fill = Restmenge
desiredHeight: number; // Wunschhöhe, nur bei mode 'fixed' wirksam
minHeight: number; // Mindesthöhe; fits = false wenn unterschritten
// Optional: abweichende Werte für die Schrägwand hinten
desiredHeightH?: number;
minHeightH?: number;
}
interface SlotAllocation {
yOffset: number; // Unterkante der Zone, vom Slot-Boden aus
height: number; // zugeteilte Höhe
fits: boolean; // false, wenn height kleiner als minHeight ist
// nur wenn totalHeightH gesetzt ist (Schrägwand):
yOffsetH?: number;
heightH?: number;
fitsH?: boolean;
}
function useSlotSpace(reg: Omit<SlotRegistration, 'id'>): SlotAllocation | null
What you need to keep in mind:
useSlotSpacemust be placed at the beginning of the component body—it is a React hook and must not be placed inside conditions or loops.- With
mode: 'fill', the height is calculated;desiredHeightis ignored. - Multiple
fillmodels share the remaining space evenly. The last entry gets the exact remainder, so no gap remains. - If
SlotLayoutProvideris missing from the tree, the hook returnsnull. This is not an error—in that case, your own positioning logic takes over.
Walls and Segments
The side convention is the same everywhere: 0 = right, 1 = left, 2 = front, 3 = back.
Every model in a wall or structure slot is injected with:
wandSeite— the side as a number. External code cannot importuseWandSeitedirectly, but it can via the SDK.slotAnchor— calculated down to the segment usingcalcWandSlotAnchor. Takes into account the clearance from the roof overhang, posts, purlins, and Qubus frames, and—ifsegmentIndexorsegmentAnzahlis set—the position between posts i and i+1.size.breiteis the usable wall width;meta.hoeheVorneandmeta.hoeheHintenare the lower edges of the rafters.segmentIndex— is automatically set to the instance index if the model does not provide its own index.
function MeineWand(props: any) {
const a = props.slotAnchor;
// Mit autoAnchor "1" ist der Ursprung bereits die Wand-Einbauposition:
// lokale +x entlang der Wand, +z nach außen.
return (
<mesh position={[0, a.size.hoehe / 2, 0]}>
<boxGeometry args={[a.size.breite, a.size.hoehe, 0.02]} />
<meshStandardMaterial color="#a0a0a0" />
</mesh>
);
}
Use defaultProps: { autoAnchor: "1", segmentIndex: { expression: "-1" } } to assign the model
to, for example, konstruktion-slot-wand-links, and assemble segment 2 via segmentIndex: 2 together with
segmentAnzahl: 3.
Assembly on the privacy screen
The privacy screen has two sub-slots of its own:
| Slot | ID | Purpose |
|---|---|---|
| Assembly | b2f4e8a1-1234-4abc-9def-000000000001 | Wall element at the top of the privacy screen |
| Filler | 35abe01f-0247-4eb2-ae2a-d62104227bd4 | Filler within the frame |
Both IDs are also available as sdk.SICHTSCHUTZWAND_AUFBAU_SLOT_ID and
sdk.SICHTSCHUTZWAND_FUELLUNG_SLOT_ID.
Structure instances also receive:
parentGeometry,slotAnchor(anchors of the supporting wall), andwandSeiteaufbauBasis— the Y-start position, i.e., the top edge of the privacy screenaufbauMaxHoehe— the available space up to the bottom edge of the rafterswandBreite— the width of the support wallautoAnchor: "1"automatically groups based on the support wall position and the top edge of the privacy screen; the model renders upward starting at local Y = 0.
Filling instances are assigned wandBreite, wandHoehe, wandHoeheHinten, material,
farbeHex, topBeamHeight, and hasTopBeam. They render in the local wall coordinate system, i.e.,
centered in X and starting at Y = 0.
An external model in the wall slot, which serves as a privacy screen, is registered with useSlotSpace
and priority: 0, mode: 'fixed'. Structure models with priority: 1 are then automatically assigned
the space above them:
const alloc = sdk.useSlotSpace({
priority: 0,
mode: 'fixed',
desiredHeight: meineSichtschutzHoehe,
minHeight: 0.1,
});
Manual height assignment is not necessary; the layout system coordinates this automatically.
The SDK
For models that require more than the injected props:
const sdk = window.__VERANDA_SDK__;
if (sdk?.version === "1") {
// Geometrie
const geo = React.useContext(sdk.VerandaGeometryContext);
const anchors = sdk.calcSlotAnchors(geo);
// Slot-Layout — die primäre API für eigene Raumzuweisung
const alloc = sdk.useSlotSpace({ priority: 1, mode: 'fill', desiredHeight: 0, minHeight: 0.3 });
// Slot-ID-Konstanten
const slotIds = sdk.KONSTRUKTION_SLOTS;
}
| Export | Type | Description |
|---|---|---|
VerandaGeometryContext | Context | Construction Dimensions |
useVerandaGeometry | Hook | Short form for the context |
calcVerandaGeometry | Function | Calculate roof values: slope, front and rear height |
calcSlotAnchors | Function | Calculate all slot anchors from the geometry |
calcWandSlotAnchor | Function | Segment-precise wall anchor |
WandSeiteContext, useWandSeite | Context, Hook | Current wall side |
WandInfoContext, useWandInfo | Context, Hook | Privacy screen heights |
SlotLayoutContext | Context | Slot Layout Context |
useSlotSpace | Hook | Guaranteed Slot Space Allocation |
KONSTRUKTION_SLOTS | Constant | Slot ID Definitions |
SICHTSCHUTZWAND_AUFBAU_SLOTS | Constant | Structure Slot IDs of the privacy screen |
SICHTSCHUTZWAND_FUELLUNG_SLOT_ID | string | Filler slot IDs of the privacy screen |
The types are defined as VerandaSdk in src/sdk/types.ts.
Using the context requires that both plugins use the same React instance. The
prop variant with parentGeometry and slotAnchor, on the other hand, always works, even without
shared React.
Module Federation
Your plugin must declare the same singletons as the Veranda plugin:
shared: {
"k3-plugin-api": { singleton: true, requiredVersion: "*" },
react: { singleton: true, requiredVersion: "*" },
"react-dom": { singleton: true, requiredVersion: "*" },
"react/jsx-runtime": { singleton: true, requiredVersion: "*" },
three: { singleton: true, requiredVersion: "*" },
"@react-three/fiber": { singleton: true, requiredVersion: "*" },
"@react-three/drei": { singleton: true, requiredVersion: "*" },
}
If the K3 host does not provide React via the shared scope, you must also apply the
inject-host-react patch from vite.config.js.
Behavior You Should Be Aware Of
- Remount on Prop Change. The constructor builds the React key from all model props.
Therefore, any prop change triggers a remount of the slot model—do not maintain any state in the
model that needs to survive this.
parentGeometry,slotAnchor, andautoAnchorare exceptions: Geometry changes do not trigger a remount. - An occupied channel slot hides the front bar. In Qubus,
hideFronttakes effect automatically as soon as there is at least one instance in the channel slot. - Gutter prop contract. Provide
rinnenBreiteandrinnenHoeheas expression props. The construction reads them and adjusts the post height and sill accordingly. - Read expression props defensively. K3 sometimes returns props as
{ expression: "0.12" }. Always parse values usingNumber(...)and a fallback.