Lab21 — Curtain · How it works

This page shows which input fields drive which other fields or calculations. Use it to quickly understand why a value changes after a choice, and why an item is flagged as «not possible». For the exact formulas, see Calculation rules.

Table of contents

  1. Legend
  2. Main configurator flow
  3. Flow: can this item handle these dimensions?
  4. Flow: cut height (length per panel)
  5. Flow: required fabric width
  6. Flow: number of panels & total metreage
  7. Flow: sales price & final price
  8. Flow: weight-cord choice & bottom hem
  9. Flow: pleat suitability & Roman-blind flag
  10. Effect matrix: which field drives what?
  11. Flow: Style Engine (interior configurator)
  12. Flow: Free Choice (product-selector)

1. Legend

The flowcharts use four kinds of blocks:

Input Decision Calculation Result Error / blocker

Arrows run from top to bottom. At a decision the flow splits, and each branch carries a label (Yes, No, or the chosen option).

2. Main configurator flow

The configurator steps follow each other in a fixed order. Every step filters or drives the next one.

Configuration order
Group (session context)group number + description (e.g. Bedroom left) — carried by every order line
Step 1 — Curtain typeDrapery / Inbetween
Step 2 — Finished dimensionsFinishedWidth + FinishedHeight (cm)
Filter item listby type + height feasibility
Step 3 — Pick itemfrom filtered list
Step 4 — Colour
Step 5 — MountingCeiling / Wall / Inside recess
Step 6 — Pleat & splitpleat, number of parts, heading tape, header (0 / 2.5 mm)
Step 7 — Liningtype, colour, price
Step 8 — Finishing & operationweight cord (Existing / Add / No) · Operation (Hand operated / Electric → Lab21 wizard) · Rail family (DS / DS XL / KS, filtered by pleat) · Rail colour · Pull rod (hand operated only) · Electric → 6-question wizard (smart system · brand · power · operation · feedback)
compute() → order lines1 curtain · 2 rail · 3 bracket · 4 end piece · 5 glider · 6 lead glider · 7 wave runner (Wave) · 8 pull rod (hand operated) · total (excl. TBD)

3. Flow: can this item handle these dimensions?

While rendering the item list (step 3), a feasibility check runs for each item. The outcome decides whether the item is marked suitable, tilt fallback or not possible.

Per item — does it fit the finished dimensions?
Item with
kamerhoog, hoogte_stof,
kantelbaar, patroon
Compute cut height
= finished height + heading tape + bottom hem
kamerhoog = Yes?
Yes — Floor-to-ceiling
cutHeight
≤ hoogte_stof?
Yes
Suitable
scenario: upright
No
kantelbaar = Yes
& patroon = Plain?
Yes
Suitable
scenario: tilted
No
Not possible
fabric height too small
No — Roll fabric
Suitable
no height limit, length unlimited

4. Flow: cut height (length per panel)

The cut height is the length of fabric cut per panel. Finished height, heading tape and bottom hem together determine the cut height. Pattern repeat (patroonhoogte) does not enter the calculation — the workshop handles repeat alignment at processing time.

cutHeight = finishedHeight + heading tape + bottom hem
FinishedHeight
from step 2
Heading tape
8 / 10 / 12 / 15 cm
Weight cord
Existing / Add / No
Cord present?
Existing or Add
Yes — cord
bottom hem = 2 cm
cord embedded
No
bottom hem = 15 cm
steamed bottom hem
cutHeight = finishedHeight + heading tape + bottom hem (cm)

5. Flow: required fabric width

The effective width depends on the pleat (return pleats need extra slack) and the number of parts (side hems only apply to floor-to-ceiling).

requiredFabricWidth = effectiveWidth × pleat factor  (+ side hems)
FinishedWidth
from step 2
Pleat
Wave / Double R / Single R / Double / Single
Number of parts
Pair / Left / Right
Pleat = Single return
or Double return?
Yes
finishedWidth
≥ 300 cm?
Yes
returnAllowance = 10% × finishedWidth
No
returnAllowance = 30 cm
No
returnAllowance = 0
effectiveWidth = finishedWidth + returnAllowance
Item kamerhoog = Yes?
Yes — Floor-to-ceiling
+ partsCount × 24 cm side hems
Pair = 48, 1 part = 24
No — Roll fabric
no side-hem allowance
requiredFabricWidth (cm)
effectiveWidth > 600 cm?
Yes
Warning
split into 1 left part + 1 right part
No
OK

6. Flow: number of panels & total metreage

The way panels are counted depends on whether the item is floor-to-ceiling, and for floor-to-ceiling on whether the fabric needs to be tilted.

panelCount + length per panel → totalMetreage
requiredFabricWidth
cutHeight
item.hoogte_stof
item.breedte_stof
item.kantelbaar / patroon
kamerhoog = Yes?
Yes — Floor-to-ceiling
cutHeight
≤ hoogte_stof?
Yes — upright
panelCount = 1
totalMetreage = fabricWidth / 100
No — tilt
kantelbaar = Yes
& patroon = Plain?
Yes
cordLoss = existing cord? 2 : 0
only Existing costs fabric; Add places the cord after processing
panelWidth = fabricHeight − cordLoss
panelCount = ⌈fabricWidth / panelWidth⌉
totalMetreage = panels × cutHeight / 100
No
Calculation halts
item not suitable
No — Roll fabric
panelCount = ⌈fabricWidth / breedte_stof⌉
totalCm = panels × cutHeight
totalMetreage = totalCm / 100

7. Flow: sales price & final price

The fabric price uses the manual verkoopprijs_per_m1 field if it has been set; otherwise a 60% fallback based on the purchase price is used.

salesPrice/m¹ → fabricPrice → totalPrice
item.verkoopprijs_per_m1
manual field
item.prijs_per_m1
purchase
verkoopprijs_per_m1
filled & > 0?
Yes — manual
salesPrice/m¹ = manual value
source: manual
No — auto
salesPrice/m¹ = purchase × 3
rounded up to next xx.95
source: auto
fabricPrice = totalMetreage × salesPrice/m¹
Lining ≠ None?
Yes
liningPrice/m¹ = voering_prijs_per_m1
or purchase × 0.60 (fallback)
liningCost = totalMetreage × price
No
liningCost = 0
totalPrice = fabricPrice + liningCost

8. Flow: weight-cord choice & bottom hem

The weight-cord choice is a 3-state dropdown: Existing weight cord, Add weight cord or No. The article flag geschikt_voor_verzwaringskoord drives whether the option Add is available.

Weight-cord choice → bottom hem
Item.geschikt_voor_verzwaringskoord
Yes / No
suitable = Yes?
Yes
3 options available:
Existing / Add / No
No
2 options: Existing / No
Add hidden

Configurator choice: Existing or Add
bottom hem = 2 cm
cord embedded
in tilted scenario:
panelWidth − 2 cm
only with Existing
Add gives no loss

Configurator choice: No
bottom hem = 15 cm
steamed bottom hem

Pattern = Patterned (item)
Tiltable forced to No
pattern cannot rotate 90°
Pattern repeat does not enter cut height
workshop handles repeat at processing time

8b. Flow: header (finishing choice)

The header (Dutch: hoofdje) is the narrow strip of fabric above the pleat tape. It is a configurator choice in Step 6 — Pleat & split that documents the workshop finish — it does not affect the cut height. Allowed values: 0 or 2.5 mm. With Wave, Single return and Double return the header is technically always 0 — the configurator forces the value to 0 and disables the field.

Header choice (lock rule)
Pleat choice
from Step 6 dropdown
Pleat = Wave / Single return / Double return?
Yes
header = 0
field locked and disabled
No — Single or Double
header = user choice
0 or 2.5 mm

Header is stored as a workshop instruction. No effect on cut height or metreage.

9. Flow: pleat suitability & Roman-blind flag

The item field geschikte_plooi (Multi-Select Picklist) determines which pleat types are allowed for this fabric. A separate rule also applies: Wave pleat is only suitable for floor-to-ceiling fabrics — with panel fabric Wave is dropped automatically, even if it appears in geschikte_plooi. If the user picks an unsuitable pleat the configurator shows a warning — the calculation keeps running, but the user clearly sees that the recommendation is to deviate.

Pleat suitability — per item
Item.geschikte_plooi
Wave / Double R / Single R / Double / Single
Pleat choice
from Step 6 dropdown
geschikte_plooi empty?
Yes — no restriction
All pleats allowed
no warning
No — restriction in place
pleat choice in list?
Yes
Suitable
no notice
No
Warning
"not suitable for pleat X — suitable: …"

Item.geschikt_vouwgordijn
Yes / No / Yes (unlined)
Informational — not in calculation
Only shows which fabrics may also be made up as a (Roman) blind

Where is this visible?

10. Effect matrix: which field drives what?

Quick reference: find an input field in the first column and see which other fields or calculations change as a result.

Source field (input) Step Drives → target field or calculation
Curtain type 1 Filters the item list (only items with that gordijn_type).
FinishedWidth 2 returnAllowance (300 cm threshold)
effectiveWidth
requiredFabricWidth
panelCount (roll fabric or tilted)
→ warning > 600 cm
FinishedHeight 2 rawCutHeightcutHeight
→ floor-to-ceiling feasibility check
totalMetreage for roll fabric / tilted
Item.kamerhoog 3 Selects scenario A (floor-to-ceiling) or B (roll fabric). Floor-to-ceiling adds the side-hem allowance.
Item.hoogte_stof 3 Upper limit for upright floor-to-ceiling. In the tilted scenario this is the panel width.
Item.breedte_stof 3 For roll fabric: roll width used for panelCount = ⌈fabricWidth/breedte_stof⌉.
Item.kantelbaar 3 Toggles the tilted scenario on/off. Forced to No when patterned.
Item.patroon (+ pattern height) 3 For Patterned: forces kantelbaar = No. The pattern repeat (patroonhoogte) is informational and does not enter the cut height — the workshop handles repeat alignment at processing time.
Item.verkoopprijs_per_m1 3 Source manual → fabricPrice. Empty/0 → auto via purchase×3 / xx.95.
Pleat 6 pleat factor (1.80–2.50)
returnAllowance (only for Single/Double return)
requiredFabricWidth
→ compared against item.geschikte_plooi → warning if not in list
Item.geschikte_plooi 3 Multi-Select list of allowed pleats (Wave / Double return / Single return / Double / Single). Empty = no restriction. Drives the warning in step 6 plus badge rendering in the item list and aside.
Item.geschikt_vouwgordijn 3 Picklist Yes / No / Yes (unlined). Informational — not used in metreage or price.
Number of parts 6 → side-hem allowance (Pair +48 / 1 part +24, floor-to-ceiling only).
Heading tape (8/10/12/15) 6 rawCutHeight.
Lining 7 Lining ≠ None reveals colour + price field; multiplies with totalMetreage.
Weight cord (configurator) 8 3 options: Existing, Add, No.
→ Existing/Add → bottom hem = 2 cm
→ No → bottom hem = 15 cm
→ in tilted floor-to-ceiling: only Existing costs 2 cm panel width (cord cut away). Add gives no loss.
Item.geschikt_voor_verzwaringskoord 3 Field label "Suitable with weight cord" (Yes/No). Drives whether the option Add weight cord is offered in the configurator. No → option hidden.
Item.eenheid 3 Price unit. For curtain fabric ; for accessories may be unit. Shown next to cost & sales price in the aside and in the price line of the breakdown.
Pull rod (12 SKUs) 8 Only visible with Hand operated. Material (Fiber transparent / White aluminium / Silver aluminium / Black aluminium) × length (92 / 122 / 152 cm). The customer always picks the length themselves; per option the configurator shows the live bottom (bottom = FinishedHeight − length) with ✓ within 140–160 cm or ⚠ outside. Recommendation under the field: the length closest to 150 cm bottom. Prices incl. VAT: 92 cm €19.95 / 122 cm €22.95 / 152 cm €24.95. Quantity = number of curtain parts.
Rail (8 SKUs, hand operated) 8 Family × colour. DS (white/black) and DS XL (white/anthracite/silver) for Wave + Return; KS (white/black/silver) for Single/Double pleat. Family dropdown is filtered by the chosen pleat. Header is set automatically: KS → 2.5 mm, DS / DS XL → 0 mm. Rod of 600 cm; rod count = ⌈FinishedWidth÷600⌉, minimum 1. Prices range from €40 (KS white) to €150 (DS XL anthracite/silver). With Electric: TBD order line.
Rail accessories (TBD) 8 Per curtain order line auto-added, article numbers to be filled: bracket (platform for ceiling/recess, wall bracket for wall) — 1 pc per 50 cm; end piece — 2 pcs; glider — 10 per metre of FinishedWidth; lead glider — 1 pc; wave runner — 2 pcs only for Wave pleat.
Electric operation — Lab21 wizard 8 Choosing Electric reveals a 6-question wizard (derived from the Forest Group decision tree with Forest → Lab21). Questions: 1) smart system present, 2) operate via existing system, 3) system type (Domotics / Smart Home), 3a) brand (Somfy / HUE / IKEA / Apple HomeKit / Homey / Fibaro / KAKU / SmartThings / Apple Siri / Luxaflex / Google·Amazon / Coulisse), 3b) domotics type (RS232/485 / volt-free / switching wires), 4) power point at motor, 5) operation (multi: remote / wall switch / app / voice / domotics), 6) feedback. Live advice below the wizard with The solution (Lab21 Shuttle motor + WiFi dongle / Wireless Connector / Diamond Sense AB / Wall switch / External RF Multi receiver / RJ-45 / AC Control Set) and extras. Third-party plugs (HomeKit / KAKU / Z-Wave / Somfy / HUE / IKEA / Powerview) are explicitly marked external, not via Lab21. All components appear as separate order lines with price to be filled.
Wave pleat rule 6 Wave pleat is only suitable for floor-to-ceiling fabrics. With panel fabric Wave is dropped automatically (warning + exclusion in dropdown).

11. Flow: Style Engine (interior configurator)

The page style-configurator-en.html is a separate tool that translates a customer's living style and colour preference into a structured JSON profile. That profile is then consumed by Kevin's external AI visualizer API to place furniture into a customer photo.

6-step funnel — from inspiration to Style Profile JSON
Step 1 — Living styleIndustrial / Scandinavian / Modern / Rural
Filter colour palettes3 of 4 palettes that match the chosen style
Step 2 — Colour paletteWarm & Cosy / Cool & Fresh / Bold Contrast / Natural
Step 3 — Furniture inventoryphoto + L×W×H per kept piece (optional)
Step 4 — Project scope (multi-select)Floor / Window treatment / Wall / Furniture — at least one
Step 5 — Window treatment per windowdimensions + frame + customer goals → Top 4 from solutions matrix
'window_decor' not in scope?
→ skip step 5
Step 6 — Photo uploadJPG / PNG / WEBP — drag-drop or click
buildStyleProfile()style + palette + scope + furniture + windows + tags
Style Profile JSON+ sample-rack IDs per window → showroom link-up
Advisor CTA"Request physical samples for this design"

Filtering — style drives the palette

Step 2 still shows all four palettes, but always greys out the one that clashes stylistically. Non-matching palettes remain on screen and clickable so the user can see why the choice narrows — blocking instead of hiding prevents confusion.

StyleAllowed palettes (order)Greyed out
IndustrialBold Contrast · Warm & Cosy · NaturalCool & Fresh
ScandinavianCool & Fresh · Warm & Cosy · NaturalBold Contrast
Modern / MinimalistCool & Fresh · Bold Contrast · Warm & CosyNatural
Rural / ClassicWarm & Cosy · Natural · Bold ContrastCool & Fresh

Solutions matrix — customer goals filter the window treatment (step 5)

For each window the customer selects one or more goals (privacy, blackout, acoustics, heat/UV, …). The function filterRaamOplossingen(goals) filters the matrix from the source spreadsheet and shows the Top 4 combinations that cover all goals, sorted by versatility.

// TypeScript equivalent for the AI pipeline
function filterRaamOplossingen(goals: string[]): RaamOplossing[] {
  if (!goals.length) return [...RAAM_OPLOSSINGEN].sort(byCoverage);
  return RAAM_OPLOSSINGEN
    .filter(o => goals.every(g => o.goals.includes(g)))
    .sort((a, b) => b.goals.length - a.goals.length);
}

The 15 combinations in the matrix cover every product in the source file: Inbetween, Drapery, Venetian blind, Duette (incl. transparent variant) and Roller blind — either solo or in combinations of 2 or 3 products.

Auto warnings per window (step 5)

Sample-rack ID — showroom link-up

For every chosen window-treatment combination a kapstalen_id is generated automatically in the form KAP-{style}-{palette}-{products}-{nr}. With this code the showroom salesperson knows immediately which physical samples to pull from the rack.

generateKapstalenId('Industrial', 'Warm_Earth', 'inb_over_jal', 1)
// → "KAP-IND-WE-INB-OG-JAL-001"

Extended JSON output (input for Kevin's visualizer API)

The primary keys are still style_id and color_palette_id; alongside those, project_scope, existing_furniture and per-window window_specs now travel with the payload — including the technical advanced fields and the Sample-rack ID that links the salesperson to the showroom.

{
  "style_id": "Industrial",
  "color_palette_id": "Warm_Earth",
  "project_scope": ["flooring", "window_decor", "wall", "furniture"],
  "existing_furniture": [
    { "type": "Sofa", "dimensions": { "l": 220, "w": 90, "h": 80 }, "image_url": "data:…" }
  ],
  "window_specs": [
    {
      "name": "Window 1",
      "dimensions": { "width": 1800, "height": 2400 },
      "frame_material": "plastic",
      "operable_type": "movable",
      "mounting": "in_recess",
      "function_goals": ["privacy", "blackout", "acoustics"],
      "suggested_products": ["inbetween", "drapery", "duette"],
      "kapstalen_id": "KAP-IND-WE-INB-OG-DUE-001",
      "advanced": { "vol_muur": false, "draaikiep": true, "…": "…" }
    }
  ],
  "customer_photo_url": "data:image/jpeg;base64,…",
  "ai_prompt_tags": ["exposed brick walls", "warm earth tones", "…"],
  "meta": { "ai_suggested": false, "furniture_count": 1, "window_count": 1, "…": "…" }
}

Notable details

12. Flow: Free Choice (product-selector)

The page product-selector-en.html is a parallel path for customers who do not want style or colour advice but want to pick straight from a catalogue. The Style Engine and the Product Selector are cross-linked — the user can switch at any moment.

4-step shopping flow — from scope to render input
Step 1 — Scope (multi-select)Floor / Window treatment / Wall* / Furniture* (* = roadmap)
Step 2 — Existing furniturephoto + L×W×H per item that stays
Step 3 — Productstabs (Floor / Window treatment) + filters + grid + cart
Validation
frame conflict?
combination assistant?
Step 4 — Photo uploadJPG / PNG / WEBP — the customer's room
Free-Selection JSONmode: "free_selection" + Product_IDs + Texture_Maps

Validation rules in the cart

The cart (sticky side panel) runs validateCart() live after every product change and shows warnings + alternatives:

Every notice carries a clickable alternative; one click adds the suggested product to the cart directly.

Free-Selection JSON output

The schema follows the brief exactly, with arrays where combinations are possible (Inbetween + Duette). texture_map points to a static CDN URL that Kevin's diffusion pipeline loads in.

{
  "mode": "free_selection",
  "project_scope": ["flooring", "window_decor"],
  "selected_products": {
    "floor": {
      "id": "PVC_OAK_HERR_001", "pattern": "herringbone", "material": "PVC",
      "texture_map": "/textures/floor/PVC_OAK_HERR_001.jpg"
    },
    "window": [
      { "id": "DUE_BLK_003", "product_type": "duette",
        "mount": "clamp_on_frame",
        "texture_map": "/textures/window/DUE_BLK_003.jpg" },
      { "id": "INB_LIN_NAT_001", "product_type": "inbetween",
        "mount": "wall_mount",
        "texture_map": "/textures/window/INB_LIN_NAT_001.jpg" }
    ]
  },
  "window_context": { "frame_material": "plastic", "draaikiep": true, "mounting": "in_recess" },
  "custom_furniture": [
    { "id": "user_sofa_1", "type": "Sofa", "dimensions": [220, 90, 80], "image_url": "data:…" }
  ],
  "floor_plan": {
    "filename": "floor_plan_living_room.pdf",
    "mime_type": "application/pdf",
    "size_bytes": 234567,
    "data_url": "data:application/pdf;base64,…"
  },
  "room_photo": "data:image/jpeg;base64,…",
  "validation_notices": [
    { "icon": "ℹ", "message": "Tilt & turn window: chosen products block tilting …" }
  ],
  "meta": { "floor_count": 1, "window_count": 2, "furniture_count": 1, "…": "…" }
}

Differences: Style Engine vs. Free Choice

AspectStyle EngineFree Choice
Entry pointStyle + colour as anchorStraight into the catalogue
JSON mode(implicit)"mode": "free_selection"
Product choiceAI suggests (Top 4 from matrix)Customer picks from grid themselves
Output to AIstyle_id + color_palette_id + tagsProduct_ID + texture_map per item
Cross-link"🛍️ Free choice" in launch bar"Help me choose based on my style" in switch bar
Visual designIdentical — shared stylesheet style-engine.css

Shared stylesheet (DRY)

Both pages link style-engine.css for all visual basics: header, progress bar, buttons, .tile, .upload-zone, .toast, .final-note, .plan-upload, gradient backgrounds and mobile tweaks. The scope tiles in step 1 (Free Choice) and step 4 (Style Engine) now use the same .tile + bg-* + SVG-icon combination so the two paths share an identical look and feel.

Page-specific components (Style Engine: window cards, top4 grid, profile summary, advanced details; Free Choice: tabs, filter bar, product grid, cart panel, ctx card) stay inline on the page itself — that is intentional, they do not need to be shared and it keeps the CSS file small.

↑ Back to top