Docs

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.

Supported types
5
Current element definitions exported by the engine registry.
Author helpers
16
Core helper functions used by the current definitions.
Window influences
3
Live influence records that can change geometry and pricing.
Contract

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.

`key`
Stable identifier used in order lines and in the element definition registry.
`displayName`
Human-readable name shown in outputs, previews, and documentation.
`paramSchema`
Typed parameter definitions with labels, units, choices, ranges, and defaults.
`compose(context, input)`
Pure composition function that resolves catalogs, derives dimensions, and returns a full `ElementResult`.
Current contract shape
type ElementDefinition = {
  readonly key: string;
  readonly displayName: string;
  readonly paramSchema: ParamSchema;
  readonly compose: (
    context: ComposeContext,
    input: ElementInput,
  ) => ElementResult;
};
Flow

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.

  1. The app calls `composeElement`, which loads the definition and normalizes `input.params` against `paramSchema`.
  2. The definition resolves catalog data such as wood, glass, and fitting price references from `context.catalogs`.
  3. The definition computes derived dimensions such as clear openings, family-specific leaf dimension bundles, falz sizes, glass sizes, and influence deltas.
  4. The definition assembles window-level output and merges family helper output into `leaves`, `parts`, `glasses`, and `fittings`.
  5. The definition builds explicit `priceLines`, derives `totals`, and emits `geometry2d` for previews.
Primitives

Primitives available today

These are the main building blocks business implementors can rely on when adding a new custom window type.

Input params and schema
  • `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.
Catalog context
  • `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.
Options and influences
  • `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.
Derived values
  • `derivedParams` is where the definition exposes computed dimensions and other reusable numeric outputs.
  • These values feed previews, debugging, exports, and leaf-local fitting rules.
Manufacturing output
  • `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.
Commercial and preview output
  • `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.
Important note
The current engine contract includes `priceBreakdown`. The authoring surface is layered: prefer existing family helpers first, and only drop to low-level leaf primitives when you are introducing a new reusable construction pattern.
Helpers

Helper functions available today

The current definitions are intentionally helper-based rather than meta-programmed. Reuse shared helpers when the logic is genuinely common.

`resolveWood`
Look up the selected wood catalog item by `input.woodKey`.
`resolveGlassType`
Look up the selected glass package so pane weight and price can be derived.
`createTurnTiltLeafFittingRules`
Build the standard turn-tilt fitting rules from a small option flag instead of hand-writing the whole rule list in each definition.
`createTurnTiltLeafAssembly`
Build one fully-configured turn-tilt leaf with its own parts, glass resolution, machining features, and fittings.
`createSlidingLeafFittingRules`
Build the active sliding leaf fitting package, including width-driven bogie selection and optional soft-close hardware.
`createLiftSlidingActiveLeafAssembly / createLiftSlidingFixedLightAssembly`
Build the two inner lift-slide assemblies without re-specifying member-level timber and glass wiring in the element definition.
`createRectangularLeafAssembly`
Low-level fallback for defining a new family helper or a one-off fixed-light style assembly when no higher-level helper exists yet.
`mergeLeafAssemblies`
Flatten multiple leaf assemblies into the final `leaves`, `parts`, `glasses`, and `fittings` arrays.
`createGlassPane`
Low-level glass helper used by the leaf builders so pane area and weight stay centralized and consistent.
`resolveLeafFittingRules`
Convert declarative leaf fitting rules into `FittingItem[]`. Most element definitions now use family helper wrappers around this rather than calling it directly.
`leafMetricBetween / leafMetricAtLeast / leafMetricLessThan`
Reusable predicates for size-driven fitting selection when creating or extending a family helper.
`createLegacyProcessingPriceLines`
Build explicit material, fitting, labor, and surcharge price lines from the assembled output.
`createTotals`
Aggregate wood length, glass area, glass weight, and total price for the final result.
`hasOption`
Check whether an option key is active before adding construction variants or hardware upgrades.
`whenOption`
Conditionally emit an `AppliedOption` entry for the final `options` list.
`resolveWindowInfluences`
Resolve window-level influences into geometry deltas, surcharge values, and operator-facing notes.
Approach

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.

Start with family helpers
  • 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.
Keep window ownership local
  • 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.
Drop lower only when needed
  • 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.
Examples

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.

Minimal `ElementDefinition` skeleton
Every custom window type is a plain object with metadata and a pure composition function.
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,
      },
    };
  },
};
`paramSchema` example
Parameter schemas define the input contract the UI and composer both rely on. Definitions can be authored tersely and normalized once.
const paramSchema = defineParamSchema({
  widthMm: {
    label: "Width",
    min: 900,
    max: 2800,
    defaultValue: 1800,
  },
  heightMm: {
    label: "Height",
    min: 900,
    max: 2200,
    defaultValue: 1400,
  },
});
Reading normalized params and catalogs
The composer normalizes params before `compose` runs, so the definition can read trusted values.
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);
Deriving geometry and leaf sizes
Definitions should derive shared geometry once, then package the repeated leaf values into a dimension object that can be passed to helpers.
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;
Building turn-tilt leaves with family helpers
Most current samples should not build turn-tilt leaves member by member. Use the shared turn-tilt helper and pass the derived dimension bundle.
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);
Building lift-slide assemblies with specialized helpers
Lift-slide now follows the same pattern: derive separate dimension objects once, then let the shared helpers build the active leaf and fixed light.
const fittingRules = createSlidingLeafFittingRules(softClose);

const leafOutput = mergeLeafAssemblies([
  createLiftSlidingActiveLeafAssembly({
    count,
    wood,
    glassType,
    softClose,
    openingSide,
    fittingRules,
    dimensions: activeLeafDimensions,
  }),
  createLiftSlidingFixedLightAssembly({
    count,
    wood,
    glassType,
    dimensions: fixedLightDimensions,
  }),
] as const);
Dropping to the low-level rectangular helper
Use `createRectangularLeafAssembly` directly when there is no family helper yet, or when you are extracting one. That helper owns the part, glass, and fitting wiring.
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,
});
Building window-level `parts`
Leaf helpers already create leaf-local timber. In the element definition, `parts` usually means the outer frame or other shared window-level members.
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,
    },
  },
);
Conditional options and leaf fittings
Definitions usually select option flags once and then delegate the detailed fitting rules to family helpers.
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.",
  }),
];
Influence-driven geometry and surcharge handling
Window influences can alter geometry, notes, and price lines in one place.
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];
Merging frame parts and leaf output
Window definitions still own the outer frame. Leaf helpers generate the leaf-local output, which is then merged into the final arrays.
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;
Final `ElementResult` assembly
The final result is the single canonical output for pricing, previews, and downstream projections.
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 }),
};
Registry wiring
A new window type becomes available once it is added to the element definition registry.
import { customWindowDefinition } from "./custom-window";
import { fixedWindowDefinition } from "./fixed-window";

export const elementDefinitions = {
  "fixed-window": fixedWindowDefinition,
  "custom-window": customWindowDefinition,
} as const;
Reference

Live engine reference

These sections are derived from the current engine exports so the guide stays aligned with the real rewrite implementation.

Fixed window
fixed-window
Current parameters exposed by this element definition.
  • 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
Single turn-tilt window
single-turn-tilt-window
Current parameters exposed by this element definition.
  • 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
Double turn-tilt window
double-turn-tilt-window
Current parameters exposed by this element definition.
  • 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
Lift sliding window
lift-sliding-window
Current parameters exposed by this element definition.
  • 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
Tilt sliding window
tilt-sliding-window
Current parameters exposed by this element definition.
  • 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
Current window influences
Getypeerde catalogusdata rechtstreeks geprojecteerd uit de standaardwaarden van de engine.
Current window influences
InfluenceSurchargeWhat it changesMeaning
Coupling left€45.00left frame -20 mmPrepare the left side for coupling to the neighboring element.
Coupling right€45.00right frame -20 mmPrepare the right side for coupling to the neighboring element.
Renovation fin€85.00perimeter fin 30 mmAdds an outer renovation fin around the perimeter.
Current pricing formula notes
The guide assumes these exported rules remain the commercial frame around a new definition.
  • 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.