Skip to content

Embedded e-commerce cut-list calculator

An embedded JavaScript widget that provides a complete cut-list calculator for your e-commerce, estimation or quotation website functionality.

Overview

SmartCut Checkout allows you to:

  • Embed a cut-list calculator directly on your website
  • Calculate prices / estimates in real-time on the client side
  • Filter & sort products in your catalogue
  • Customize the UI to match your brand
  • Support multiple materials, thicknesses, dimensions, extras, and configurations
  • No server-side integration required

Quick Start

Installation

Include the script and default css styling in your HTML:

html
<link rel="stylesheet" href="https://cutlistevo.com/checkout/checkout.css" />
<script type="module" src="https://cutlistevo.com/checkout/checkout.js"></script>

Basic Usage

html
<!-- Container for the calculator -->
<div id="smartcut-app"></div>

<script>
// Configuration
const initData = {
  stock: [
    {
      l: 2440,
      w: 1220,
      t: 18,
      cost: 65,
      material: 'Plywood'
    }
  ],
  saw: {
    stockType: 'sheet',
    bladeWidth: 3.2,
    cutType: 'efficiency',
    cutPreference: 'l'
  },
  options: {
    locale: 'en-US',
    currency: 'USD'
  }
}

// Wait for the ready event and then call init
window.addEventListener('smartcut/ready', () => {
  window.smartcutCheckout.init(initData)
})

// Listen for calculation events
window.addEventListener('smartcut/calculating', () => {
  console.log('Calculation started')
})

// Listen for results
window.addEventListener('smartcut/result', (e) => {
  const result = e.detail
  console.log('Optimization complete:', result)
  console.log('Total stock cost:', result.checkout.formattedTotalStockCost)
})
</script>

Configuration

Stock Configuration

Define the available stock materials:

javascript
const initData = {
  stock: [
    {
      l: 2440,
      w: 1220,
      t: 18,
      cost: 65,
      material: 'Plywood'
    }
  ],
  saw: {
    stockType: 'sheet',
    bladeWidth: 3.2,
    cutPreference: 'l'
  },
  options: {
    locale: 'en-US',
    currency: 'USD',
    apiVersion: 3 // 2 or 3 (default: 3)
  }
}

Parts Configuration

Pre-populate the calculator with parts:

javascript
const initData = {
  stock: [...],
  parts: [
    {
      l: 600,
      w: 400,
      t: 18,
      q: 4,
      name: 'Shelf',
      material: 'Plywood',
      orientationLock: 'l', // 'l', 'w', or '' (free)
      banding: {
        sides: { l1: 'oak|1mm', l2: 'oak|1mm', w1: '', w2: '' }
      },
      finish: {
        faces: { a: 'spray|matt', b: '' }
      },
      customData: { projectId: '12345' },
      order_id: 'ORD-001'
    }
  ],
  options: {...}
}

Part Properties:

PropertyTypeDescription
lnumberPart length (required)
wnumberPart width (required)
tnumber | stringThickness (optional, inherits from stock if not set)
qnumberQuantity (default: 1)
namestringPart name/label
materialstringMaterial identifier for stock matching
orientationLock'l' | 'w' | ''Orientation constraint: length-locked, width-locked, or free
bandingobjectEdge banding configuration
finishobjectSurface finish configuration
planingobjectPlaning configuration
customDataobjectCustom key-value data passed through to results
stockIdstringLock part to specific stock ID
order_idstringOrder ID for grouping parts

Saw Options

Saw configuration should be provided in the saw property (recommended) or in options for backward compatibility.

Complete Saw Configuration

javascript
saw: {
  stockType: 'sheet', // 'sheet', 'linear', or 'roll'
  bladeWidth: 3.2, // Blade width in mm
  cutType: 'guillotine', // 'efficiency', 'guillotine', 'beam'
  cutPreference: 'l', // 'l' (length), 'w' (width)
  stackHeight: 60, // Maximum stack height in mm (beam saws only)
  efficiencyOptions: { // Optional efficiency settings
    primaryCompression: 'l' // 'l' (length) or 'w' (width) - primary compression direction
  },
  guillotineOptions: { // Optional guillotine settings
    strategy: 'efficiency', // 'efficiency' or 'time'
    maxPhase: 0, // Maximum cutting phase depth (0 = unlimited, 1-10)
    headCuts: false // Enable head cuts for optimization
  },
  options: { // Additional saw options
    stockSelection: 'efficiency', // 'efficiency' or 'smallest' - stock selection method
    stackingMode: 'identical', // 'dimensions', 'identical', or 'none' - part stacking mode
    minSpacing: 0 // Minimum spacing between parts in mm
  }
}

Cut Type Values:

  • 'efficiency' - Standard efficiency optimization
  • 'guillotine' - Guillotine cutting (only straight cuts across entire stock)
  • 'beam' - Beam saw optimization

Cut Preference Values:

  • 'l' - Prefer length-first cuts
  • 'w' - Prefer width-first cuts

Stock Selection Values:

  • 'efficiency' - Select stock that maximizes material efficiency (default)
  • 'smallest' - Prefer smaller stock sizes when possible

Stacking Mode Values:

  • 'identical' - Stack only parts with identical dimensions and properties (default)
  • 'dimensions' - Stack parts with same dimensions regardless of other properties
  • 'none' - Disable part stacking

Configuration Options

API Version

javascript
options: {
  apiVersion: 3 // 2 or 3 (default: 3)
}

API Version Differences:

  • Version 2: Uses flat extras structure ({ banding: { x1: string, x2: string, y1: string, y2: string } })
  • Version 3: Uses nested extras structure ({ extras: { banding: { sides: { l1: string, l2: string } } } })

See Result Format for details on the differences between versions.

Emit API Result

javascript
options: {
  enable: {
    emitAPIResult: true // Include full API v3 response in result (default: false)
  }
}

When enabled, the result object will include an apiResultV3 property containing the complete API v3 response format. This is useful when you need the standardized API response structure regardless of which apiVersion is configured for the checkout result.

See API Result V3 in the Result Format section for the structure.

Debug Mode

javascript
options: {
  enable: {
    debug: true // Enable debug output in console (default: false)
  }
}

When enabled, additional debug information will be logged to the browser console during calculations.

Number Format

javascript
options: {
  numberFormat: 'decimal', // 'decimal' or 'fraction'
  decimalPlaces: 2,
  fractionRoundTo: 0 // For fraction formatting
}

Orientation Model

javascript
options: {
  orientationModel: 0, // 0, 1, or 2 (input parts)
  resultOrientationModel: 0 // 0, 1, or 2 (result parts)
}

Orientation Model Values:

  • 0 - No orientation control
  • 1 - Grain lock (parts maintain grain direction)
  • 2 - Dimension lock (length always maps to longer dimension)

Part Limits

javascript
options: {
  maxParts: 100, // Maximum parts per order (0 = unlimited)
  minDimension: 10, // Minimum allowed part dimension in mm
  partTrim: 0 // Amount to trim from parts (mm)
}

Pagination

javascript
options: {
  partsPerPage: 10, // Parts shown per page when pagination enabled
  enable: {
    pagination: true // Enable pagination for parts list
  }
}

Field Order

javascript
options: {
  fieldOrder: 'l, w, t, q, name, orientation' // Custom field display order
}

Control the order of input fields in the parts form. Provide a comma-separated list of field IDs.

Custom Fields

javascript
options: {
  customFields: [
    {
      type: 'string',
      label: 'Project Name',
      id: 'projectName',
      placeholder: 'Enter project name'
    },
    {
      type: 'integer',
      label: 'Quantity',
      id: 'quantity',
      default: 1
    },
    {
      type: 'select',
      label: 'Finish',
      id: 'finish',
      outputType: 'string',
      options: [
        { label: 'Matt', value: 'matt' },
        { label: 'Gloss', value: 'gloss' }
      ]
    },
    {
      type: 'checkbox',
      label: 'Rush Order',
      id: 'rush',
      trueValue: 'yes',
      falseValue: 'no',
      default: 'no'
    }
  ]
}

UI Customization

Control colors, enabled features, and field order:

javascript
options: {
  enable: {
    banding: true,       // Edge banding options
    finish: true,        // Finishing options
    planing: true,       // Planing options
    machining: true,     // Machining operations (holes, corners)
    diagram: true,       // Show cutting diagram
    orientation: true,   // Part orientation control
    csvImport: false,    // CSV import for parts
    partName: true,      // Allow naming parts
    pagination: false,   // Paginate parts list
    progressNumber: true // Show progress/step numbers in UI
  },
  colors: {
    partA: '#118ab2',
    partB: '#06d6a0',           // Alternating part color
    stock: '#ffd166',
    button: '#118ab2',
    buttonText: '#ffffff',
    headerBackground: '#ffffff', // Header background color
    headerText: '#212529'        // Header text color
  }
}

Extras Configuration

Extras are additional services like edge banding, finishing, planing, and machining that can be applied to parts with custom pricing.

Basic Extras Setup

javascript
const initData = {
  stock: [...],

  // Edge banding configuration
  banding: {
    labels: ['type', 'thickness'],
    pricing: {
      'oak|1mm': 1.0,
      'oak|2mm': 1.1,
      'pine|1mm': 2.0,
      'maple|1mm': 3.0
    }
  },

  // Finish configuration
  finish: {
    labels: ['type', 'style'],
    pricing: {
      'spray|matt': 1.0,
      'spray|satin': 1.1,
      'lacquer|matt': 2.0,
      'lacquer|satin': 2.2
    }
  },

  // Planing configuration
  planing: {
    labels: ['type'],
    pricing: {
      'standard': 0.5,
      'premium': 0.8
    }
  },

  options: {...}
}

Extras Location Filtering

Control which sides and faces are available for each extra type by adding a locations property directly to each extra configuration:

javascript
// Available sides: 'side.l1', 'side.l2', 'side.w1', 'side.w2' (main edges)
//                  'side.a', 'side.b', 'side.c', 'side.d' (corners)
// Available faces: 'face.a', 'face.b'

banding: {
  labels: ['material', 'thickness'],
  pricing: {...},
  locations: ['side.l1', 'side.l2', 'side.w1', 'side.w2'] // Only main edges
},

finish: {
  labels: ['type'],
  pricing: {...},
  locations: ['face.a', 'face.b'] // Both faces
},

planing: {
  labels: ['type'],
  pricing: {...},
  locations: ['side.w1', 'side.w2', 'face.a', 'face.b'] // Edges and faces
}

Extras Location Groups

Create custom groupings of locations for bulk operations with optional pricing by adding a groups array directly to each extra configuration:

javascript
planing: {
  labels: ['type'],
  pricing: {
    'standard': 0.5,
    'premium': 0.8
  },
  locations: ['side.w1', 'side.w2', 'face.a', 'face.b'],
  groups: [
    {
      id: 'two-sided',
      label: '2 sided',
      locations: ['face.a', 'face.b'],
      price: 100,
      hideIndividualLocations: true // Optional: hide individual location controls when group is available
    },
    {
      id: 'four-sided',
      label: '4 sided',
      locations: ['face.a', 'face.b', 'side.w1', 'side.w2'],
      price: 200,
      hideIndividualLocations: true,
      // Optional: Group-level validation rules
      rules: {
        longSide: { min: 200 },
        shortSide: { min: 150 },
        message: '4-sided planing requires minimum 200x150mm parts'
      }
    }
  ]
},

banding: {
  labels: ['material'],
  pricing: {...},
  groups: [
    {
      id: 'all-corners',
      label: 'All Corners',
      locations: ['side.a', 'side.b', 'side.c', 'side.d']
    },
    {
      id: 'long-sides',
      label: 'Long Sides',
      locations: ['side.l1', 'side.l2']
    }
  ]
}

Group Properties:

  • id (required) - Unique identifier for the group
  • label (required) - Display name shown in the UI
  • locations (required) - Array of location strings included in this group
  • price (optional) - Fixed price for the entire group (overrides individual location pricing)
  • hideIndividualLocations (optional) - When true, hides the individual location controls for locations in this group
  • rules (optional) - Validation rules that apply when using this group (see Group-Level Validation Rules)

Extras Validation Rules

Apply dimension constraints to control which parts can have extras applied by adding a rules property directly to each extra configuration:

javascript
planing: {
  labels: ['type'],
  pricing: {...},
  locations: [...],
  rules: {
    t: {
      min: 8,
      max: 230
    },
    shortSide: {
      min: 10,
      max: 620
    },
    longSide: {
      max: 1000
    },
    message: 'Planing requires thickness 8-230mm, short side 10-620mm, and long side max 1000mm'
  }
},

banding: {
  labels: ['material'],
  pricing: {...},
  rules: {
    t: {
      min: 12,
      max: 30
    },
    message: 'Banding is only available for 12-30mm thick materials'
  }
},

finish: {
  labels: ['type'],
  pricing: {...},
  rules: {
    longSide: {
      max: 2400
    },
    shortSide: {
      max: 1200
    },
    message: 'Finish is only available for parts smaller than 2400x1200mm'
  }
}

Validation Rule Fields:

  • longSide.min / longSide.max - Constraints on the longer dimension
  • shortSide.min / shortSide.max - Constraints on the shorter dimension
  • t.min / t.max - Thickness constraints
  • formula (optional) - JavaScript expression for complex validation (can reference longSide, shortSide, t)
  • message - Custom error message shown when validation fails
  • locations (optional) - Array of location-specific rules (see Location-Specific Validation Rules)

When a part doesn't meet the validation rules, the extras options will be disabled and an error message displayed.

Location-Specific Validation Rules

Apply different validation constraints to individual locations within an extra type. This allows fine-grained control over which parts can use specific edges or faces:

javascript
banding: {
  labels: ['material'],
  pricing: {...},
  rules: {
    // Type-level rules (apply to all locations by default)
    longSide: { min: 100 },
    shortSide: { min: 50 },
    message: 'Banding requires minimum 100x50mm parts',

    // Location-specific rules (override type-level for specific locations)
    locations: [
      {
        location: 'side.l1',
        longSide: { min: 500 },
        message: 'L1 banding only available for parts >= 500mm long side'
      },
      {
        location: 'side.l2',
        longSide: { min: 500 },
        message: 'L2 banding only available for parts >= 500mm long side'
      }
    ]
  }
}

Location Rule Properties:

  • location (required) - The location this rule applies to (e.g., 'side.l1', 'face.a')
  • longSide / shortSide / t - Dimension constraints for this location
  • formula (optional) - JavaScript expression for complex validation
  • message (optional) - Custom error message for this location

Group-Level Validation Rules

Groups can have their own validation rules that apply when any location in the group is selected:

javascript
planing: {
  labels: ['type'],
  pricing: {...},
  groups: [
    {
      id: 'four-sided',
      label: '4 Sided',
      locations: ['face.a', 'face.b', 'side.w1', 'side.w2'],
      price: 200,
      rules: {
        longSide: { min: 200 },
        shortSide: { min: 150 },
        t: { min: 18 },
        message: '4-sided planing requires minimum 200x150x18mm parts'
      }
    }
  ]
}

Rule Precedence

When multiple rules could apply to a location, the most specific rule wins:

  1. Location-specific rule - Rules defined in rules.locations[] for a specific location
  2. Group rule - Rules defined on a group that contains the location
  3. Type-level rule - The base rules for the extra type

Only one rule level applies per location - the most specific one completely overrides less specific rules.

Example:

javascript
banding: {
  rules: {
    longSide: { min: 100 },  // Type-level: default for all locations
    locations: [
      { location: 'side.l1', longSide: { min: 500 } }  // Location-specific: overrides for l1
    ]
  },
  groups: [
    {
      id: 'all-sides',
      locations: ['side.l1', 'side.l2', 'side.w1', 'side.w2'],
      rules: { longSide: { min: 200 } }  // Group rule: applies to group members without location rules
    }
  ]
}

In this example:

  • side.l1: Uses location rule (min 500mm) - most specific
  • side.l2, side.w1, side.w2: Use group rule (min 200mm) - group is more specific than type
  • Any other location: Uses type-level rule (min 100mm) - fallback

Part Validation Rules

Apply dimension constraints to parts themselves to control which parts can be processed by adding a partRules property to the init data:

javascript
const initData = {
  stock: [...],

  // Part validation rules
  partRules: {
    // Basic dimension constraints
    l: {
      min: 50,
      max: 2400
    },
    w: {
      min: 20,
      max: 1200
    },
    t: {
      min: 12,
      max: 30
    },
    longSide: {
      min: 100,
      max: 2400
    },
    shortSide: {
      min: 50,
      max: 1200
    },

    // Cross-dimensional rule: at least one side must meet primaryMin,
    // and the other side must meet secondaryMin
    crossDimensionalRule: {
      primaryMin: 50,
      secondaryMin: 20
    },

    // Optional custom message
    message: 'At least one side must be ≥ 50 mm and the other side must be ≥ 20 mm'
  },

  options: {...}
}

Part Validation Rule Fields:

  • l.min / l.max - Length constraints
  • w.min / w.max - Width constraints
  • t.min / t.max - Thickness constraints
  • longSide.min / longSide.max - Constraints on the longer dimension
  • shortSide.min / shortSide.max - Constraints on the shorter dimension
  • crossDimensionalRule (optional) - Cross-dimensional validation
    • primaryMin - At least one side must be ≥ this value
    • secondaryMin - The other side must be ≥ this value
  • formula (optional) - Formula expression for complex validation (can reference l, w, t, longSide, shortSide)
    • Supports arithmetic operators: +, -, *, /
    • Supports comparison operators: >, <, >=, <=, ==
    • Supports logical operators: && (AND), || (OR)
    • Supports ternary operator: condition ? trueValue : falseValue
  • message - Custom error message shown when validation fails

Example with formula:

javascript
partRules: {
  formula: '(l >= 50 && w >= 20) || (w >= 50 && l >= 20)',
  message: 'At least one side must be ≥ 50 mm and the other side must be ≥ 20 mm'
}

Complex formula example:

javascript
partRules: {
  formula: '(l * w) > 100000 && t >= 12',
  message: 'Part must have area > 100,000 mm² and thickness ≥ 12 mm'
}

When a part doesn't meet the validation rules, an error message will be displayed below the part and calculation will be blocked until the issue is resolved.

Custom Validation Rules

For more complex validation scenarios, you can use custom validation rules that support full access to part properties including extras and machining. This is configured via the EcommerceSettings admin page or by adding a customValidation property to the init data:

javascript
const initData = {
  stock: [...],

  // Custom validation rules
  customValidation: {
    enabled: true,
    rules: [
      {
        id: 'min-size',
        enabled: true,
        name: 'Minimum Part Size',
        formula: 'longSide >= 100 && shortSide >= 50',
        message: 'Parts must be at least 100mm x 50mm'
      },
      {
        id: 'banding-thickness',
        enabled: true,
        name: 'Banding Thickness Check',
        formula: '!hasBanding || t >= 12',
        message: 'Banding requires minimum 12mm thickness'
      },
      {
        id: 'machining-thickness',
        enabled: true,
        name: 'Machining Thickness Check',
        formula: '!hasMachining || t >= 18',
        message: 'Machining operations require minimum 18mm thickness'
      }
    ]
  },

  options: {...}
}

Custom Validation Rule Fields:

  • id - Unique identifier for the rule
  • enabled - Whether the rule is active (default: true)
  • name - User-friendly name for the rule (optional)
  • formula - Condition that must evaluate to true for the part to be valid
  • message - Error message shown when validation fails

Available Variables:

VariableTypeDescription
lnumberPart length
wnumberPart width
tnumberPart thickness
qnumberPart quantity
longSidenumbermax(l, w)
shortSidenumbermin(l, w)
materialstringMaterial name
namestringPart name
grainstringGrain direction
fullStockbooleanFull stock purchase flag
hasBandingbooleanTrue if any banding applied
hasFinishbooleanTrue if any finish applied
hasPlaningbooleanTrue if any planing applied
hasMachiningbooleanTrue if any machining operations
extras.banding.sides.l1stringBanding on long side 1
extras.banding.sides.l2stringBanding on long side 2
extras.banding.sides.w1stringBanding on short side 1
extras.banding.sides.w2stringBanding on short side 2
extras.finish.faces.astringFinish on face A
extras.finish.faces.bstringFinish on face B
machining.holesnumberNumber of holes
machining.cornersnumberNumber of corner operations

Supported Operators:

  • Arithmetic: +, -, *, /
  • Comparison: >, <, >=, <=, ==
  • Logical: && (AND), || (OR), ! (NOT)
  • Ternary: condition ? trueValue : falseValue
  • Parentheses: (expression)

Example Rules:

javascript
// Minimum area requirement
{
  formula: '(l * w) >= 10000',
  message: 'Parts must have minimum 10,000mm² area'
}

// Combined extras check
{
  formula: '!(hasBanding && hasFinish) || longSide >= 200',
  message: 'Parts with both banding and finish must be at least 200mm'
}

// Specific banding requirement
{
  formula: "extras.banding.sides.l1 == '' || t >= 15",
  message: 'Banding on long side 1 requires minimum 15mm thickness'
}

// Machining hole limit
{
  formula: 'machining.holes <= 10',
  message: 'Maximum 10 holes per part'
}

When a part fails custom validation, an error message will be displayed and calculation will be blocked until the issue is resolved.

Complete Extras Example

javascript
const initData = {
  stock: [
    {
      l: 2440,
      w: 1220,
      t: 18,
      cost: 65,
      material: 'Plywood'
    }
  ],

  // Edge banding
  banding: {
    labels: ['material', 'thickness'],
    pricing: {
      'oak|1mm': 1.0,
      'oak|2mm': 1.2,
      'pine|1mm': 0.8,
      'maple|1mm': 1.5
    },
    locations: ['side.l1', 'side.l2', 'side.w1', 'side.w2'],
    rules: {
      t: { min: 12, max: 30 },
      message: 'Banding only available for 12-30mm thick materials'
    }
  },

  // Finishing
  finish: {
    labels: ['type', 'style'],
    pricing: {
      'spray|matt': 5.0,
      'spray|satin': 5.5,
      'lacquer|matt': 8.0,
      'lacquer|satin': 8.5
    },
    locations: ['face.a', 'face.b'],
    rules: {
      longSide: { max: 2400 },
      shortSide: { max: 1200 },
      message: 'Finish is only available for parts smaller than 2400x1200mm'
    }
  },

  // Planing
  planing: {
    labels: ['type'],
    pricing: {
      'standard': 2.0,
      'premium': 3.5
    },
    locations: ['face.a', 'face.b', 'side.w1', 'side.w2'],
    groups: [
      {
        id: 'two-sided',
        label: 'Two Sides (Top & Bottom)',
        locations: ['face.a', 'face.b'],
        price: 100,
        hideIndividualLocations: true
      },
      {
        id: 'four-sided',
        label: 'Four Sides (All Faces & Edges)',
        locations: ['face.a', 'face.b', 'side.w1', 'side.w2'],
        price: 200,
        hideIndividualLocations: true
      }
    ],
    rules: {
      t: { min: 8, max: 230 },
      shortSide: { min: 10, max: 620 },
      longSide: { max: 1000 },
      message: 'Planing requires: thickness 8-230mm, width 10-620mm, length max 1000mm'
    }
  },

  saw: {
    stockType: 'sheet',
    bladeWidth: 3.2,
    cutType: 'guillotine',
    cutPreference: 'l'
  },

  options: {
    locale: 'en-US',
    currency: 'USD',
    apiVersion: 3, // 2 or 3 (default: 3)

    // Enable extras features
    enable: {
      banding: true,
      finish: true,
      planing: true,
      machining: false
    }
  }
}

Machining Configuration

Advanced machining features for holes, corners, and custom operations:

javascript
const initData = {
  machining: {
    faces: {
      enabled: true
    },
    holes: {
      enabled: true,
      defaultDiameter: 5,
      minDiameter: 5,
      maxDiameter: 10,
      enableDepth: true,
      minDepth: 5
    },
    hingeHoles: {
      enabled: true,
      minimumHoleDistance: 10,
      defaultDistanceFromEdge: 22,
      defaultOuterSpacing: 10,
      defaultHingeLength: 50
    },
    shelfHoles: {
      enabled: true,
      diameters: [5, 10]
    },
    corners: {
      enabled: true,
      types: ['radius', 'bevel'],
      enableBanding: true
    }
  },

  options: {
    enable: {
      machining: true
    }
  }
}

Events

The Checkout API dispatches custom window events for integration:

smartcut/ready

Fired when the calculator is loaded and ready to initialize.

javascript
window.addEventListener('smartcut/ready', () => {
  window.smartcutCheckout.init(initData)
})

smartcut/initComplete

Fired when initialization is complete.

javascript
window.addEventListener('smartcut/initComplete', () => {
  console.log('Calculator initialized')
})

smartcut/calculating

Fired when a calculation starts.

javascript
window.addEventListener('smartcut/calculating', () => {
  console.log('Calculation in progress...')
})

smartcut/beforeCalculate

Fired after internal validation passes but before the calculation is sent to the server. This allows you to add custom validation logic using JavaScript and optionally prevent the calculation from proceeding.

Event properties:

  • cancelable: true - Call event.preventDefault() to cancel the calculation
  • event.detail.data - The calculation data that would be sent (parts, stock, saw config, etc.)
  • event.detail.error - Set this to a string to display a custom error message
javascript
window.addEventListener('smartcut/beforeCalculate', (event) => {
  const calculationData = event.detail.data

  // Example: Custom validation - check total quantity
  const totalQuantity = calculationData.inputShapes.reduce(
    (sum, part) => sum + (part.q || 1),
    0
  )

  if (totalQuantity > 500) {
    // Prevent the calculation
    event.preventDefault()
    // Set a custom error message to display
    event.detail.error = 'Maximum 500 parts per order. Please split your order.'
    return
  }

  // Example: Check part dimensions
  for (const part of calculationData.inputShapes) {
    if (part.l > 3000 || part.w > 2000) {
      event.preventDefault()
      event.detail.error = `Part "${part.name || 'Unnamed'}" exceeds maximum dimensions (3000x2000mm)`
      return
    }
  }

  // Validation passed - calculation will proceed
  console.log('Custom validation passed, calculating...')
})

When the calculation is prevented:

  • The calculate button is re-enabled (thinking state is reset)
  • If event.detail.error is set, the error message is displayed to the user
  • The smartcut/result event will not fire

Calculation data structure (event.detail.data):

typescript
{
  inputSaw: {
    stockType: string,
    bladeWidth: number,
    cutType: string,
    cutPreference: string,
    // ... other saw properties
  },
  inputShapes: Array<{
    l: number,
    w: number,
    t: number | null,
    q: number,
    material: string | null,
    name: string | null,
    orientationLock: string | null,
    extras?: {...},
    customData?: Record<string, any>
  }>,
  inputStock: Array<{
    l: number,
    w: number,
    t: number | null,
    q: number,
    material: string | null,
    cost: number | null,
    // ... other stock properties
  }>,
  extrasOptions: {...} | null
}

smartcut/result

Fired when optimization results are available.

javascript
window.addEventListener('smartcut/result', (e) => {
  const result = e.detail

  // Optimization metadata
  console.log(result.metadata)

  // Parts used in optimization
  console.log(result.parts)

  // Stock sheets used
  console.log(result.stock)

  // Formatted pricing with currency
  console.log(result.checkout.formattedTotalStockCost)
  console.log(result.checkout.formattedBandingCost)
  console.log(result.checkout.formattedFinishCost)
})

smartcut/validationError

Fired when input validation fails.

javascript
window.addEventListener('smartcut/validationError', () => {
  console.log('Please check your inputs')
})

Result Format

The result object returned via the smartcut/result event contains different structures based on the apiVersion setting.

API Version 2 Result Structure

When apiVersion: 2 is set, results use a flat extras structure:

typescript
{
  jobId: number,

  metadata: {
    totalStockCost: number,
    bandingLengthByType: Record<string, number>,
    finishAreaByType: Record<string, number>,
    planingAreaByType: Record<string, number>,
    addedPartTally: Record<string, number>,
    usedStockTally: Record<string, number>,
    unplacedParts: Array<Part>
  },

  parts: Array<{
    l: number,
    w: number,
    t: number | null,
    q: number,
    material: string | null,
    name: string | null,
    orientationLock: '' | 'l' | 'w' | null,
    // Flat extras structure (API v2)
    banding?: Record<string, string | boolean>, // e.g., { x1: 'oak|1mm', x2: 'oak|1mm', y1: '', y2: '' }
    finish?: Record<string, string | boolean>, // e.g., { a: true, b: false }
    planing?: Record<string, string | boolean>,
    customData?: Record<string, any>
  }>,

  stock: Array<{
    id: string,
    name: string | null,
    l: number,
    w: number,
    t: number | null,
    material: string | null,
    q: number,
    trim?: { x1: number, x2: number, y1: number, y2: number },
    cost?: number,
    // Analysis is aggregated across all stock with same parentId when stacking
    // Most values are summed; areaEfficiency is averaged
    analysis?: {
      areaEfficiency: number,   // Average efficiency across all stacked stock
      finishArea: number,       // Total finish area
      bandingLength: number,    // Total banding length
      partArea: number,         // Total part area
      totalParts: number,       // Total number of parts
      stackedNumberOfCuts: number,
      numberOfCuts: number,
      stackedCutLength: number,
      cutLength: number,
      rollLength: number
    },
    customData?: Record<string, any>
  }>,

  offcuts: Array<{
    l: number,
    w: number,
    t: number | null,
    q: number,
    stockId: string
  }>,

  inputs: {
    parts: Array<Part> // Same structure as parts above
  }
}

API Version 3 Result Structure

When apiVersion: 3 is set (default), results use a nested extras structure:

typescript
{
  jobId: number,

  metadata: {
    totalStockCost: number,
    bandingLengthByType: Record<string, number>,
    finishAreaByType: Record<string, number>,
    planingAreaByType: Record<string, number>,
    addedPartTally: Record<string, number>,
    usedStockTally: Record<string, number>,
    unplacedParts: Array<Part>
  },

  parts: Array<{
    l: number,
    w: number,
    t: number | null,
    q: number,
    material: string | null,
    name: string | null,
    orientationLock: '' | 'l' | 'w' | null,
    // Nested extras structure (API v3)
    extras?: {
      banding?: {
        sides: Record<string, string | boolean> // e.g., { l1: 'oak|1mm', l2: 'oak|1mm', w1: '', w2: '' }
      },
      finish?: {
        faces: Record<string, string | boolean> // e.g., { a: true, b: false }
      },
      planing?: {
        sides?: Record<string, string | boolean>,
        faces?: Record<string, string | boolean>
      }
    },
    customData?: Record<string, any>
  }>,

  stock: Array<{
    id: string,
    name: string | null,
    l: number,
    w: number,
    t: number | null,
    material: string | null,
    q: number,
    trim?: { l1: number, l2: number, w1: number, w2: number }, // Note: uses l1/l2/w1/w2 in v3
    cost?: number,
    // Analysis is aggregated across all stock with same parentId when stacking
    // Most values are summed; areaEfficiency is averaged
    analysis?: {
      areaEfficiency: number,   // Average efficiency across all stacked stock
      finishArea: number,       // Total finish area
      bandingLength: number,    // Total banding length
      partArea: number,         // Total part area
      totalParts: number,       // Total number of parts
      stackedNumberOfCuts: number,
      numberOfCuts: number,
      stackedCutLength: number,
      cutLength: number,
      rollLength: number
    },
    // Additional v3 fields
    color?: { hex: string, name: string } | string,
    weight?: number,
    imageUrl?: string,
    tags?: string[],
    available?: boolean,
    customData?: Record<string, any>
  }>,

  offcuts: Array<{
    l: number,
    w: number,
    t: number | null,
    q: number,
    stockId: string
  }>,

  inputs: {
    parts: Array<Part> // Same structure as parts above
  }
}

Using Result Data

javascript
window.addEventListener('smartcut/result', (e) => {
  const result = e.detail

  // Get total cost from metadata
  const totalCost = result.metadata.totalStockCost

  // Get parts with optimization data
  const optimizedParts = result.parts

  // Get stock usage
  const stockUsed = result.stock

  // Get offcuts for reuse
  const offcuts = result.offcuts

  // Access extras based on API version
  const apiVersion = 3 // Your configured version

  result.parts.forEach(part => {
    if (apiVersion === 2) {
      // API v2: Flat structure
      const bandingX1 = part.banding?.x1
      const finishFaceA = part.finish?.a
    } else {
      // API v3: Nested structure
      const bandingL1 = part.extras?.banding?.sides?.l1
      const finishFaceA = part.extras?.finish?.faces?.a
    }
  })

  // Get extras costs breakdown from metadata
  const bandingCosts = result.metadata.bandingLengthByType
  const finishCosts = result.metadata.finishAreaByType
  const planingCosts = result.metadata.planingAreaByType
})

API Result V3

When the emitAPIResult option is enabled, the result object includes an apiResultV3 property containing the complete API v3 response. This provides a standardized format matching the SmartCut API v3 specification:

typescript
{
  // Standard checkout result properties...
  jobId: number,
  metadata: {...},
  parts: [...],
  stock: [...],
  offcuts: [...],
  inputs: {...},

  // Additional API v3 response (when emitAPIResult: true)
  apiResultV3?: {
    jobId: number,
    saw: {
      stockType: string,
      bladeWidth: number,
      cutType: string,
      cutPreference: string
    },
    stockList: Array<{...}>,
    shapeList: Array<{...}>,
    cutList: Array<{...}>,
    offcuts: Array<{...}>,
    unusableShapes: Array<{...}>,
    metadata: {...}
  }
}

This is useful when integrating with systems that expect the full API v3 response format, or when you need access to additional data like cutList that isn't included in the standard checkout result.

javascript
window.addEventListener('smartcut/result', (e) => {
  const result = e.detail

  // Access the API v3 response if available
  if (result.apiResultV3) {
    console.log('Cut list:', result.apiResultV3.cutList)
    console.log('Shape list:', result.apiResultV3.shapeList)
  }
})

Product Filtering

Enable dynamic stock selection with filtering and search:

javascript
const initData = {
  stock: [
    {
      l: 2800,
      w: 2070,
      t: 18,
      cost: 95,
      material: 'MDF',
      name: 'White MDF 2800×2070×18mm',
      category: 'Sheet Materials',
      tags: ['mdf', 'white', 'standard'],
      available: true
    }
  ],

  stockFilter: {
    enabled: true,
    config: {
      displayMode: 'grid', // 'grid' or 'list'
      enableSearch: true,
      itemsPerPage: 20,
      allowMultipleSelection: true,
      availableFilters: [
        {
          field: 'material',
          type: 'multiselect',
          label: 'Material'
        },
        {
          field: 't',
          type: 'range',
          label: 'Thickness (mm)',
          min: 0,
          max: 30
        },
        {
          field: 'category',
          type: 'multiselect',
          label: 'Category'
        }
      ]
    }
  },

  options: {...}
}

Support