Window construction DSL guide
This guide documents the current TypeScript authoring model used by the rewrite engine. It is not a future generic DSL. It explains the real `ElementDefinition` contract, the helper layers available today, and the output primitives a custom window type is expected to produce. For the current samples, the preferred path is usually to reuse a family helper rather than wire every leaf manually.
What a custom window type is
A custom window type is a typed `ElementDefinition` object. The UI, order composition, pricing, and preview layers all depend on the result of this one definition.
type ElementDefinition = {
readonly key: string;
readonly displayName: string;
readonly paramSchema: ParamSchema;
readonly compose: (
context: ComposeContext,
input: ElementInput,
) => ElementResult;
};Composition lifecycle
The current engine uses a small, explicit pipeline. Definitions stay simple because the composer normalizes inputs first and the definition returns one canonical result.
- The app calls `composeElement`, which loads the definition and normalizes `input.params` against `paramSchema`.
- The definition resolves catalog data such as wood, glass, and fitting price references from `context.catalogs`.
- The definition computes derived dimensions such as clear openings, family-specific leaf dimension bundles, falz sizes, glass sizes, and influence deltas.
- The definition assembles window-level output and merges family helper output into `leaves`, `parts`, `glasses`, and `fittings`.
- The definition builds explicit `priceLines`, derives `totals`, and emits `geometry2d` for previews.
Primitives available today
These are the main building blocks business implementors can rely on when adding a new custom window type.
- `paramSchema` declares the supported number and choice inputs.
- `input.params` contains normalized numeric values by key.
- Ranges and defaults are enforced before the definition composes.
- `context.catalogs.woods` drives profile thickness and wood pricing.
- `context.catalogs.glassTypes` drives glass weight and material pricing.
- `context.catalogs.fittings` is used when converting fitting items into price lines.
- `input.optionKeys` enables optional hardware or construction packages.
- `input.influenceKeys` resolves window-level geometry deltas and surcharges.
- Definitions usually turn option keys into booleans once, then pass those flags into shared helpers.
- `derivedParams` is where the definition exposes computed dimensions and other reusable numeric outputs.
- These values feed previews, debugging, exports, and leaf-local fitting rules.
- `leaves` describe the active and passive wings or sashes with outer, falz, and glass dimensions.
- `parts` describe timber pieces with role, profile, dimensions, and quantity. In current samples this is usually window-level frame parts plus merged helper output.
- `glasses` describe panes with size, area, and weight.
- `fittings` and `joints` describe hardware demand and joinery operations, often derived inside family helpers and then flattened.
- `priceLines` keeps pricing explicit and inspectable.
- `totals` aggregates wood length, glass area, glass weight, and price.
- `geometry2d` is the canonical 2D scene graph used by the preview UI.
Helper functions available today
The current definitions are intentionally helper-based rather than meta-programmed. Reuse shared helpers when the logic is genuinely common.
How to choose the right helper level
The DSL has improved since the first draft. New samples should show the simplest authoring layer that matches the construction family.
- If the element is a turn-tilt or lift-slide variant, derive dimensions and call the existing family helper functions first.
- This keeps member definitions, glass variant resolution, and leaf-local fitting rules out of the page-sized definition.
- The element definition should still own the outer frame, window influences, top-level options, pricing, notes, and preview geometry.
- Helpers should not absorb business decisions that are only true for one concrete window type.
- Use `createRectangularLeafAssembly`, direct fitting predicates, or raw part arrays when you are creating a new reusable family helper or modeling a genuinely new construction pattern.
- If the sample can be expressed with an existing helper, the docs should show that simpler path.
Sample code for the key concepts
These snippets mirror the current engine style. Derive dimensions once, call the highest-level helper that fits, then keep window-specific frame, pricing, and preview logic in the definition.
import type { ElementDefinition } from "../domain/element-definition";
export const customWindowDefinition: ElementDefinition = {
key: "custom-window",
displayName: "Custom window",
paramSchema,
compose: (context, input) => {
return {
elementKey: "custom-window",
displayName: "Custom window",
quantity: input.count ?? 1,
derivedParams: {},
notes: [],
options: [],
leaves: [],
parts: [],
glasses: [],
fittings: [],
joints: [],
priceLines: [],
priceBreakdown: {
woodMaterialEuro: 0,
processingEuro: 0,
operationEuro: 0,
glassEuro: 0,
jointEuro: 0,
fittingEuro: 0,
surchargeEuro: 0,
assemblyEuro: 0,
},
geometry2d: [],
totals: {
woodLinearMeters: 0,
glassAreaM2: 0,
glassWeightKg: 0,
priceEuro: 0,
},
};
},
};const paramSchema = defineParamSchema({
widthMm: {
label: "Width",
min: 900,
max: 2800,
defaultValue: 1800,
},
heightMm: {
label: "Height",
min: 900,
max: 2200,
defaultValue: 1400,
},
});const count = input.count ?? 1;
const widthMm = input.params.widthMm;
const heightMm = input.params.heightMm;
const wood = resolveWood(context.catalogs.woods, input.woodKey);
const glassType = resolveGlassType(
context.catalogs.glassTypes,
input.glassTypeKey,
);
const influence = resolveWindowInfluences(input.influenceKeys);const frameSectionMm = 68;
const sashSectionMm = 56;
const centerPostWidthMm = 78;
const glazingClearanceMm = 26;
const leftFrameWidthMm = frameSectionMm + influence.leftFrameDeltaMm;
const rightFrameWidthMm = frameSectionMm + influence.rightFrameDeltaMm;
const innerWidthMm = widthMm - leftFrameWidthMm - rightFrameWidthMm;
const innerHeightMm = heightMm - frameSectionMm * 2;
const sashOuterWidthMm = (innerWidthMm - centerPostWidthMm) / 2;
const sashOuterHeightMm = innerHeightMm;
const sashFalzWidthMm = sashOuterWidthMm - sashSectionMm * 2;
const sashFalzHeightMm = sashOuterHeightMm - sashSectionMm * 2;
const glassWidthMm = sashFalzWidthMm - glazingClearanceMm;
const glassHeightMm = sashFalzHeightMm - glazingClearanceMm;
const sashDimensions = {
outerWidthMm: sashOuterWidthMm,
outerHeightMm: sashOuterHeightMm,
falzWidthMm: sashFalzWidthMm,
falzHeightMm: sashFalzHeightMm,
glassWidthMm,
glassHeightMm,
sectionWidthMm: sashSectionMm,
} as const;const concealedHinges = hasOption(input.optionKeys, "concealed-hinges");
const leafFittingRules =
createTurnTiltLeafFittingRules(concealedHinges);
const leafOutput = mergeLeafAssemblies([
createTurnTiltLeafAssembly({
side: "left",
count,
wood,
glassType,
concealedHinges,
fittingRules: leafFittingRules,
dimensions: sashDimensions,
}),
createTurnTiltLeafAssembly({
side: "right",
count,
wood,
glassType,
concealedHinges,
fittingRules: leafFittingRules,
dimensions: sashDimensions,
}),
] as const);const fittingRules = createSlidingLeafFittingRules(softClose);
const leafOutput = mergeLeafAssemblies([
createLiftSlidingActiveLeafAssembly({
count,
wood,
glassType,
softClose,
openingSide,
fittingRules,
dimensions: activeLeafDimensions,
}),
createLiftSlidingFixedLightAssembly({
count,
wood,
glassType,
dimensions: fixedLightDimensions,
}),
] as const);const fixedLightAssembly = createRectangularLeafAssembly({
key: "fixed-light",
description: "Fixed light",
role: "fixed-light",
kind: "fixed",
handing: "none",
profileKey: "fixed-light-56x68",
outerWidthMm: fixedLightOuterWidthMm,
outerHeightMm: fixedLightOuterHeightMm,
falzWidthMm,
falzHeightMm,
glassWidthMm,
glassHeightMm,
quantity: count,
parts: {
wood,
profileKey: "fixed-light-56x68",
sectionWidthMm: fixedLightSectionMm,
verticalKey: "fixed-light-verticals",
verticalDescription: "Fixed light verticals",
verticalRole: "fixed-light-jamb",
horizontalKey: "fixed-light-horizontals",
horizontalDescription: "Fixed light horizontals",
horizontalRole: "fixed-light-rail",
},
glass: {
key: "glass-fixed",
glassType: resolveGlassVariant(glassType, glassWidthMm, glassHeightMm),
},
fittingRules: fixedLeafFittingRules,
});const frameParts = withRoughCutLengths(
[
{
key: "frame-left",
description: "Frame jamb left",
role: "frame-jamb",
lengthMm: heightMm,
widthMm: leftFrameWidthMm,
manufacturingFeatures: createTurnTiltFrameFeatures("vertical"),
},
{
key: "frame-right",
description: "Frame jamb right",
role: "frame-jamb",
lengthMm: heightMm,
widthMm: rightFrameWidthMm,
manufacturingFeatures: createTurnTiltFrameFeatures("vertical"),
},
] as const,
{
defaults: {
woodKey: wood.key,
profileKey: "frame-68x68",
thicknessMm: wood.defaultThicknessMm,
quantity: count,
processedSides: 2,
cornerCount: 2,
cutCount: 1,
},
},
);const concealedHinges = hasOption(input.optionKeys, "concealed-hinges");
const softClose = hasOption(input.optionKeys, "soft-close");
const turnTiltFittingRules =
createTurnTiltLeafFittingRules(concealedHinges);
const slidingFittingRules = createSlidingLeafFittingRules(softClose);
const options = [
...whenOption(input.optionKeys, {
key: "concealed-hinges",
label: "Concealed hinges",
description: "Upgrade both sashes to concealed hinges.",
}),
...whenOption(input.optionKeys, {
key: "soft-close",
label: "Soft-close",
description: "Add a soft-close module to the active sliding leaf.",
}),
];const influence = resolveWindowInfluences(input.influenceKeys);
const { priceLines, breakdown } = createLegacyProcessingPriceLines({
displayName: "Custom window",
count,
wood,
glassType,
fittingsCatalog: context.catalogs.fittings,
parts,
glasses,
fittings,
assemblyUnitPriceEuro: laborRate.laborUnitPriceEuro,
surchargeEuro: influence.priceSurchargeEuro,
});
const notes = [...influence.notes];const frameParts = withRoughCutLengths(
[
{
key: "frame-left",
description: "Frame jamb left",
role: "frame-jamb",
profileKey: "frame-68x68",
lengthMm: heightMm,
widthMm: leftFrameWidthMm,
quantity: count,
},
],
{
defaults: {
woodKey: wood.key,
thicknessMm: wood.defaultThicknessMm,
processedSides: 2,
cornerCount: 2,
cutCount: 1,
},
},
);
const parts = [...frameParts, ...leafOutput.parts];
const leaves = leafOutput.leaves;
const glasses = leafOutput.glasses;
const fittings = leafOutput.fittings;return {
elementKey: "custom-window",
displayName: "Custom window",
quantity: count,
derivedParams: {
widthMm,
heightMm,
innerWidthMm,
innerHeightMm,
glassWidthMm,
glassHeightMm,
},
notes,
options,
leaves: leafOutput.leaves,
parts,
glasses,
fittings,
joints,
priceLines,
priceBreakdown: breakdown,
geometry2d: [
{
key: "outer-frame",
role: "frame",
xMm: 0,
yMm: 0,
widthMm,
heightMm,
},
],
totals: createTotals({ parts, glasses, priceLines }),
};import { customWindowDefinition } from "./custom-window";
import { fixedWindowDefinition } from "./fixed-window";
export const elementDefinitions = {
"fixed-window": fixedWindowDefinition,
"custom-window": customWindowDefinition,
} as const;Live engine reference
These sections are derived from the current engine exports so the guide stays aligned with the real rewrite implementation.
- Width (widthMm): 250-7.000 mm, default 1.000
- Height (heightMm): 250-7.000 mm, default 1.000
- Construction variant (constructionVariant): 68 mm standard, 68 mm center panel, 92 mm standard, default 68 mm standard
- Frame mode (frameMode): Standard, Outer rebate 120, Vent-light 120, default Standard
- Vertical bars (verticalBarCount): 0-1 mm, default 0
- Vertical bar width (verticalBarWidthMm): 40-300 mm, default 80
- Transom (hasTransom): No transom, One transom, default No transom
- Transom width (transomBarWidthMm): 40-300 mm, default 80
- Lower section glass height (lowerSectionHeightMm): 80-4.000 mm, default 600
- Center panel width (centerPanelWidthMm): 70-1.060 mm, default 120
- Width (widthMm): 250-7.000 mm, default 1.000
- Height (heightMm): 250-7.000 mm, default 1.000
- Construction variant (constructionVariant): 68 mm standard, 68 mm center panel, 68 mm zwh112, 68 mm center panel zwh112, 92 mm standard, default 68 mm standard
- Turns to (openingSide): Left, Right, default Left
- Frame mode (frameMode): Standard, Outer rebate 120, Vent-light 120, default Standard
- Profile variant (profileVariant): Standard, Slim 41x67, default Standard
- Horizontal split (hasHorizontalSplit): No, Yes, default No
- Split bar width (transomBarWidthMm): 40-200 mm, default 80
- Lower glass height (lowerSectionHeightMm): 80-2.000 mm, default 600
- Center vertical bars (centerVerticalBarCount): 0-1 mm, default 0
- Center bar width (centerVerticalBarWidthMm): 40-300 mm, default 80
- Center panel width (centerPanelWidthMm): 70-1.060 mm, default 120
- Width (widthMm): 300-7.000 mm, default 2.000
- Height (heightMm): 300-7.000 mm, default 2.000
- Construction variant (constructionVariant): 68 mm standard, 68 mm asymmetric, 68 mm zwh112, 68 mm asymmetric zwh112, 92 mm standard, 92 mm asymmetric, default 68 mm standard
- Active side (openingSide): Left, Right, default Left
- Frame mode (frameMode): Standard, Outer rebate 120, Vent-light 120, default Standard
- Profile variant (profileVariant): Standard, Outer profile, Slim 41x67, default Standard
- Left leaf width (leftLeafWidthMm): 300-3.200 mm, default 900
- Cover strip width (coverStripWidthMm): 15-100 mm, default 15
- Horizontal split (hasHorizontalSplit): No, Yes, default No
- Split bar width (transomBarWidthMm): 40-200 mm, default 80
- Lower glass height (lowerSectionHeightMm): 80-2.000 mm, default 600
- Width (widthMm): 1.800-8.000 mm, default 3.200
- Height (heightMm): 1.800-3.500 mm, default 2.300
- Section (sectionType): 58 mm, 68 mm, 92 mm, default 68 mm
- Construction variant (constructionVariant): Renovation, Two-part, Two-part asymmetric, Multi-part, Three-part center, C-schema, E-schema, default Two-part
- Opening side (openingSide): Left, Right, default Right
- Fixed section width (fixedSectionWidthMm): 700-3.200 mm, default 1.400
- Active leaf width (activeLeafWidthMm): 700-3.200 mm, default 1.400
- Segment count (segmentCount): 2-4 mm, default 3
- Fixed pane glass offset (fixedPaneGlassOffsetMm): 0-100 mm, default 0
- Outer rebate 82 (outerRebate82Mode): Standard, Outer rebate 82, default Standard
- Width (widthMm): 1.800-6.000 mm, default 3.000
- Height (heightMm): 1.200-3.500 mm, default 2.200
- Construction variant (constructionVariant): 68 mm two-part, 68 mm two fixed lights, default 68 mm two-part
- Frame mode (frameMode): Standard, Outer rebate, default Standard
- Operation package (operationVariant): Left, Right, Upgrade left, default Left
| Influence | Surcharge | What it changes | Meaning |
|---|---|---|---|
| Coupling left | €45.00 | left frame -20 mm | Prepare the left side for coupling to the neighboring element. |
| Coupling right | €45.00 | right frame -20 mm | Prepare the right side for coupling to the neighboring element. |
| Renovation fin | €85.00 | perimeter fin 30 mm | Adds an outer renovation fin around the perimeter. |
- Wood is priced per linear meter by wood family.
- Glass is priced per square meter by glass package.
- Sealing joints are priced per linear meter by joint profile.
- Fittings and hardware are priced per fitting article.
- Part processing follows the base processing model: corner cost, per-side-per-meter processing, and optional planing, transport, and saw constants.
- Manufacturing features are now price-bearing through explicit operation rates for the supported families.
- Window influences add direct surcharges and can also change derived geometry.
- Assembly is still modeled as a fixed base charge per element family in this prototype.
- This prototype does not yet model margin layers, discounts, VAT, or the full workpost-based operation matrix from the legacy system.