Skip to main content

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:

MechanismWhatHow to use
parentGeometry propComplete structure geometryis injected into every slot model
slotAnchor propPre-calculated installation position of the slotposition, rotation, installation space
autoAnchorAutomatic placement in the slot’s free spaceautoAnchor: "1" in defaultProps
useSlotSpace()Seamless, collision-free height assignment within the slotvia window.__VERANDA_SDK__.useSlotSpace
window.__VERANDA_SDK__Context, helpers, slot IDsfor advanced cases

Slot IDs​

Veranda (single-slope roof) and Qubus (flat roof) share the same slot IDs with the prefix konstruktion-slot-.

Slot KeySlot IDPurposeDefault Model
postkonstruktion-slot-pfostenSupport structureveranda-pfosten-eckig
roofingkonstruktion-slot-eindeckungRoofveranda-glas-eindeckung
gutterkonstruktion-slot-rinneRain Gutterveranda-regenrinne-kasten
shadingkonstruktion-slot-beschattungShadingveranda-markise
wallLeft / Right / Front / Backkonstruktion-slot-wand-*Wallsveranda-wand-keil or -rahmenwand
LEDkonstruktion-slot-ledLED 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, innerDepth
  • dachneigung in degrees — for the Qubus, see 0; the actual tilt is listed in eindeckungDachneigung
  • pfostenBreite, pfostenTiefe, pfostenAnzahlVorne, pfostenAnzahlHinten
  • sparrenAnzahl, sparrenBreite, sparrenHoehe, sparrenAuflage
  • schwelle, schwelleBreite, schwelleHoehe, pfette, pfettenBreite, pfettenHoehe
  • rinnenHoehe, rinnenBreite
  • isQubus, 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:

Slotpositionrotationmeta
gutterLower edge of the gutter in front of the sill; on the Qubus, it replaces the front beam180° around YschwelleOK, innenliegend, isQubus
roofingfront edge of the supportroof pitch around XhoeheVorne, hoeheHinten, neigung, dachneigung
shading, LEDFront edge of subroof—hoeheVorne, hoeheHinten, sparrenHoehe
wall, structureCenter of side at the base; local +x along the wall, +z outward0, ±90, or 180° around YhoeheVorne, hoeheHinten
postConstruction 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:

prioritySignificance for wallsMode
0Privacy screen (floor)fixed — exact height
1Structure (frame wall, sliding door, shutters)fill — fills the remainder
2Wedge (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:

  • useSlotSpace must 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; desiredHeight is ignored.
  • Multiple fill models share the remaining space evenly. The last entry gets the exact remainder, so no gap remains.
  • If SlotLayoutProvider is missing from the tree, the hook returns null. 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 import useWandSeite directly, but it can via the SDK.
  • slotAnchor — calculated down to the segment using calcWandSlotAnchor. Takes into account the clearance from the roof overhang, posts, purlins, and Qubus frames, and—if segmentIndex or segmentAnzahl is set—the position between posts i and i+1. size.breite is the usable wall width; meta.hoeheVorne and meta.hoeheHinten are 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:

SlotIDPurpose
Assemblyb2f4e8a1-1234-4abc-9def-000000000001Wall element at the top of the privacy screen
Filler35abe01f-0247-4eb2-ae2a-d62104227bd4Filler 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), and wandSeite
  • aufbauBasis — the Y-start position, i.e., the top edge of the privacy screen
  • aufbauMaxHoehe — the available space up to the bottom edge of the rafters
  • wandBreite — the width of the support wall
  • autoAnchor: "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;
}
ExportTypeDescription
VerandaGeometryContextContextConstruction Dimensions
useVerandaGeometryHookShort form for the context
calcVerandaGeometryFunctionCalculate roof values: slope, front and rear height
calcSlotAnchorsFunctionCalculate all slot anchors from the geometry
calcWandSlotAnchorFunctionSegment-precise wall anchor
WandSeiteContext, useWandSeiteContext, HookCurrent wall side
WandInfoContext, useWandInfoContext, HookPrivacy screen heights
SlotLayoutContextContextSlot Layout Context
useSlotSpaceHookGuaranteed Slot Space Allocation
KONSTRUKTION_SLOTSConstantSlot ID Definitions
SICHTSCHUTZWAND_AUFBAU_SLOTSConstantStructure Slot IDs of the privacy screen
SICHTSCHUTZWAND_FUELLUNG_SLOT_IDstringFiller 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, and autoAnchor are exceptions: Geometry changes do not trigger a remount.
  • An occupied channel slot hides the front bar. In Qubus, hideFront takes effect automatically as soon as there is at least one instance in the channel slot.
  • Gutter prop contract. Provide rinnenBreite and rinnenHoehe as 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 using Number(...) and a fallback.