Skip to content
heapbyte - A name of excellence

Architecture · 13 October 2026

Made-to-measure pricing is yield and seaming, not variant arithmetic

A homeowner enters the dimensions of their living room window into an online curtain store: 260 centimetres wide, 225 centimetres finished drop, in an unlined natural linen voile. A developer unfamiliar with textile manufacturing calculates the price using elementary school arithmetic: 2.6 metres multiplied by 2.25 metres equals 5.85 square metres, multiplied by the fabric's square-metre rate. The customer checks out, the atelier receives the cutting ticket, and the workshop manager stops production immediately. Because the customer chose a wave pleat, the curtain requires 2.2 times the track width in flat fabric—5.72 metres of flat width. The linen bolt is milled at a standard width of 140 centimetres. To create a 5.72-metre wide curtain, the workshop must cut five separate vertical panel drops, sew four vertical seams, allow 25 centimetres for the weighted bottom hem and header tape, and match the pattern repeat. The order required 12.5 linear metres of fabric and four sewing seams. The website charged for less than half of the material consumed.

7 min read
Written by the HeapByte engineering team

shopify made to measure pricing

Why square-metre pricing fails curtains

When merchants sell timber shelves, sheet metal, or tiles, pricing is largely proportional to surface area. A customer ordering two square metres of oak pays twice as much as a customer ordering one square metre. The offcut waste can be amortised or reused across future jobs.

Textiles behave completely differently because fabric is manufactured and sold in continuous rolls of fixed width. A curtain does not hang flat; it is pleated to create depth and light filtering. The flat fabric required to cover a track is fundamentally disconnected from the visual track width.

More critically, you cannot piece together random fabric offcuts to make a luxury curtain. Every vertical join must run the entire drop of the curtain, and all seams must be sewn straight down the grain. If a customer's flat width requires 285 centimetres of fabric and the roll width is 140 centimetres, two full drops provide 280 centimetres—five centimetres short. The atelier must cut a third full drop simply to harvest that remaining five centimetres. The remaining 135 centimetres of that third drop along the entire drop length is cutting waste that cannot be resold.

If your pricing engine calculates price based on nominal surface area, the merchant pays for that unharvested bolt width out of their own margin on every single custom order.

The four physical constraints of confection

To build an accurate pricing engine for made-to-measure products, application logic must model four physical manufacturing constraints:

  • Pleat Fullness Ratio: A wave pleat requires 2.2 times the track width in flat fabric to achieve uniform S-folds. A double pinch pleat requires 2.0 times the width. A gathered pencil pleat requires 1.8 times the width.
  • Bolt Roll Width and Panel Drops: Fabric bolts arrive in standard widths (typically 140cm for drapery and 280–300cm for wide sheers). Panel drops are an integer ceiling: Math.ceil((trackWidth * fullness + sideHems) / boltWidth).
  • Railroading vs Vertical Drops: When dealing with wide-width sheers (290–300cm bolts), fabric can be turned 90 degrees to eliminate vertical seams, provided finished cut length is less than bolt width.
  • Vertical Pattern Repeats (Rapport): Patterned fabrics require vertical pattern alignment across adjacent drops. Cut drops must round up to integer multiples of the repeat rapport.

Turning continuous inputs into discrete cut sheets

On Vorhang Schweiz, a Swiss made-to-measure window treatments specialist, the storefront needed to capture continuous customer measurements in centimetres while outputting both exact commercial pricing and workshop-ready manufacturing specifications.

A typical product page cannot rely on Shopify's native option dropdowns. Instead, an interactive configurator collects the continuous measurements (width and height in centimetres), heading style, and installation options outside the variant model. But rather than emitting an unverified price directly to the cart, the inputs pass through a deterministic calculation engine.

The calculation engine performs two tasks simultaneously: First, it calculates the true commercial price, factoring in the linear metres of fabric consumed, the atelier confection sewing fee per vertical drop, and the hardware accessories (such as wave gliders and suspension hooks). Second, it constructs a manufacturing cut sheet that is stamped directly into Shopify line item properties (_cutDrops, _fabricLinearMeters, _orientation, _gliderCount). When the order synchronises to the workshop's production queue, the cutting technicians receive the exact cut lengths and drop counts without manual recalculation.

ts
export type PleatStyle = "wave" | "double_pinch" | "pencil";

export interface FabricSpecification {
  sku: string;
  name: string;
  rollWidthCm: number;
  pricePerMeter: number;
  patternRepeatCm?: number;
  canRailroad?: boolean;
}

export interface CurtainOrderInput {
  railWidthCm: number;
  dropHeightCm: number;
  pleatStyle: PleatStyle;
  fabric: FabricSpecification;
  hemAllowanceCm?: number;
  confectionLaborPerDrop?: number;
  railroadConfectionLaborPerMeter?: number;
  gliderPrice?: number;
  pricingStep?: number;
}

export interface CurtainCutSheetAndPrice {
  railWidthCm: number;
  dropHeightCm: number;
  pleatStyle: PleatStyle;
  fullnessRatio: number;
  isRailroaded: boolean;
  dropCount: number;
  cutLengthPerDropCm: number;
  totalLinearMeters: number;
  gliderCount: number;
  fabricCost: number;
  confectionCost: number;
  hardwareCost: number;
  totalPrice: number;
  lineItemProperties: Record<string, string>;
}

const PLEAT_FULLNESS_RATIOS: Record<PleatStyle, number> = {
  wave: 2.2,
  double_pinch: 2.0,
  pencil: 1.8,
};

/**
 * Calculates deterministic made-to-measure curtain cut-sheet specifications and pricing,
 * transforming continuous dimensional inputs into discrete manufacturing panel drops.
 */
export function calculateCurtainCutSheetAndPrice(
  input: CurtainOrderInput
): CurtainCutSheetAndPrice {
  const {
    railWidthCm,
    dropHeightCm,
    pleatStyle,
    fabric,
    hemAllowanceCm = 25,
    confectionLaborPerDrop = 28.0,
    railroadConfectionLaborPerMeter = 18.0,
    gliderPrice = 0.45,
    pricingStep = 0.05,
  } = input;

  if (railWidthCm < 40 || dropHeightCm < 50) {
    throw new Error(`Dimensions below producible limits: width=${railWidthCm}, drop=${dropHeightCm}`);
  }

  const fullnessRatio = PLEAT_FULLNESS_RATIOS[pleatStyle];
  const requiredFlatWidthCm = railWidthCm * fullnessRatio + 16; // 8cm double-folded side hems

  const baseCutLengthCm = dropHeightCm + hemAllowanceCm;
  let isRailroaded = false;
  let dropCount = 1;
  let cutLengthPerDropCm = baseCutLengthCm;
  let totalLinearMeters = 0;

  if (fabric.canRailroad && baseCutLengthCm <= fabric.rollWidthCm) {
    isRailroaded = true;
    dropCount = 1;
    cutLengthPerDropCm = baseCutLengthCm;
    totalLinearMeters = Number((requiredFlatWidthCm / 100).toFixed(3));
  } else {
    isRailroaded = false;
    dropCount = Math.ceil(requiredFlatWidthCm / fabric.rollWidthCm);

    if (fabric.patternRepeatCm && fabric.patternRepeatCm > 0) {
      const repeatCount = Math.ceil(baseCutLengthCm / fabric.patternRepeatCm);
      cutLengthPerDropCm = repeatCount * fabric.patternRepeatCm;
    } else {
      cutLengthPerDropCm = baseCutLengthCm;
    }

    totalLinearMeters = Number(((dropCount * cutLengthPerDropCm) / 100).toFixed(3));
  }

  let gliderCount = Math.round(railWidthCm / 7);
  if (gliderCount < 6) gliderCount = 6;
  if (pleatStyle === "wave" && gliderCount % 2 !== 0) {
    gliderCount += 1; // Wave pleats require an even number of gliders so end folds return to the wall
  }

  const fabricCost = Number((totalLinearMeters * fabric.pricePerMeter).toFixed(2));
  const confectionCost = isRailroaded
    ? Number((totalLinearMeters * railroadConfectionLaborPerMeter).toFixed(2))
    : Number((dropCount * confectionLaborPerDrop).toFixed(2));
  const hardwareCost = Number((gliderCount * gliderPrice).toFixed(2));

  const unroundedTotal = fabricCost + confectionCost + hardwareCost;
  const totalPrice = Number((Math.ceil(unroundedTotal / pricingStep) * pricingStep).toFixed(2));

  const lineItemProperties: Record<string, string> = {
    _makerModel: "made_to_measure_curtain",
    _pleatStyle: pleatStyle,
    _railWidthCm: railWidthCm.toString(),
    _dropHeightCm: dropHeightCm.toString(),
    _fullnessRatio: fullnessRatio.toFixed(1),
    _orientation: isRailroaded ? "railroaded" : "vertical_seamed",
    _dropCount: dropCount.toString(),
    _cutLengthPerDropCm: cutLengthPerDropCm.toString(),
    _fabricLinearMeters: totalLinearMeters.toFixed(2),
    _gliderCount: gliderCount.toString(),
    _fabricSku: fabric.sku,
  };

  return {
    railWidthCm,
    dropHeightCm,
    pleatStyle,
    fullnessRatio,
    isRailroaded,
    dropCount,
    cutLengthPerDropCm,
    totalLinearMeters,
    gliderCount,
    fabricCost,
    confectionCost,
    hardwareCost,
    totalPrice,
    lineItemProperties,
  };
}
The calculation engine evaluates bolt width against finished cut length to determine whether railroading is possible before calculating panel drops. If the customer orders a wave pleat, the function enforces an even glider count so the outer pleat folds terminate against the wall rather than twisting toward the room. These physical atelier rules cannot be represented by simple linear regression.

What this architecture does not handle

While this calculation engine accurately models rectilinear made-to-measure curtains, it maintains clear boundaries:

Curved bay window geometries: Calculations assume linear track spans. Curved architectural bay windows or French return rods require trigonometric bend radius calculations that cannot be inferred from a single width input.

Multi-layer blackout interlining: When combining thermal acoustic interlinings with delicate silk face fabrics, shrinkage coefficients and differential drop stretching require physical testing and separate lining drop counts.

Roll flaw splicing: Industrial textile bolts occasionally contain fabric weave defects tagged by mills. Cutting tables must manually navigate roll splices; the digital configurator models mathematical yield, not physical bolt defects.

When this needs an engineer

If your made-to-measure products are sold in coarse standard sizes (for example, small, medium, and large window blinds with five-centimetre trimming kits), you do not need custom calculation software. Standard Shopify variants combined with theme dropdowns can handle stepped products effectively.

It requires dedicated engineering when products are truly bespoke and continuously dimensioned: when pricing depends on discrete manufacturing yield ceilings (such as fabric bolt widths, discrete panel drops, and pattern repeats) rather than continuous surface area; when confection labor scales by panel drops or orientation changes (such as automatic railroading detection); and when fulfillment teams require deterministic cutting tickets and hardware counts passed cleanly into order metadata.

In our work on bespoke Shopify configurators and complex client builds, we treat product configuration not as theme cosmetic styling, but as an engineering bridge between customer desires and workshop production reality.

Send us the store and the symptom.

Insights

Apply this to your store.

An audit turns the general principle into a specific list of changes, ordered by what actually pays back.