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
Section titled “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
Section titled “Quick Start”Installation
Section titled “Installation”Include the script and default css styling in your HTML:
<link rel="stylesheet" href="https://cutlistevo.com/checkout/checkout.css" /><script type="module" src="https://cutlistevo.com/checkout/checkout.js"></script>Basic Usage
Section titled “Basic Usage”<!-- Container for the calculator --><div id="smartcut-app"></div>
<script>// Configurationconst 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 initwindow.addEventListener('smartcut/ready', () => { window.smartcutCheckout.init(initData)})
// Listen for calculation eventswindow.addEventListener('smartcut/calculating', () => { console.log('Calculation started')})
// Listen for resultswindow.addEventListener('smartcut/result', (e) => { const result = e.detail console.log('Optimization complete:', result) console.log('Total stock cost:', result.checkout.formattedTotalStockCost)})</script>Configuration
Section titled “Configuration”Stock Configuration
Section titled “Stock Configuration”Define the available stock materials:
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) }}Stock Item Properties:
| Property | Type | Description |
|---|---|---|
l | number | Sheet length in mm (required) |
w | number | Sheet width in mm (required) |
t | number | Sheet thickness in mm (required) |
material | string | Friendly display name shown in the material picker (required) |
code | string | Unique SKU / identifier for this specific stock item (recommended — see below) |
cost | number | Cost per sheet |
weight | number | Weight in kg |
description | string | Description shown in the picker |
imageUrl | string | Product image URL |
brand | string | Brand name (shown as picker hierarchy) |
variant | string | Variant / decor name |
db_id | string | Your database ID — passed through to results for lookup |
fullSizeOnly | boolean | When true, the customer cannot enter custom part sizes on this material |
grain | 'l' | 'w' | '' | Grain direction of this stock (length-aligned, width-aligned, or none) |
preventGrainRotation | boolean | When true, parts placed on this stock cannot be rotated against grain |
category | string | Category for filtering (used by Product Filtering UI) |
tags | string[] | Searchable tags (used by Product Filtering UI) |
available | boolean | Available-for-purchase flag (used by Product Filtering UI) |
imageUrl | string | Product image URL — surfaced in results too |
db_sawId | string | Mongo ObjectId of the saw to use when this stock is picked. Lets you pin different materials to different saws per pick — see Per-Pick Saw Mapping |
How the material picker is built
Section titled “How the material picker is built”Stock items are grouped by their material field into a single picker entry. Multiple items with the same material but different dimensions (e.g. different thicknesses or sheet sizes) all appear under one dropdown row — the customer selects the material once, then the calculator filters available thicknesses automatically.
The code field is the per-item identifier: it distinguishes between individual stock rows that share the same material name, and it is carried through to the calculation result so you can map each used sheet back to a specific product in your catalogue.
// Two sheet sizes of the same material — one picker entry, two thickness optionsstock: [ { l: 2440, w: 1220, t: 18, cost: 65, material: 'Birch Plywood', code: 'BPW-2440-18' }, { l: 2440, w: 1220, t: 12, cost: 48, material: 'Birch Plywood', code: 'BPW-2440-12' },]
// Two different materials — two picker entriesstock: [ { l: 2440, w: 1220, t: 18, cost: 65, material: 'Birch Plywood', code: 'BPW-18' }, { l: 2440, w: 1220, t: 18, cost: 45, material: 'White MDF', code: 'MDF-18' },]When code is omitted, the calculator falls back to the composed label (brand + variant + material) as the grouping key — this works for simple setups but can cause items to appear as separate entries if any field differs between rows of the same material. Always supply code when passing multiple stock rows.
Parts Configuration
Section titled “Parts Configuration”Pre-populate the calculator with parts at init time. Parts arrive as an array on the parts key of the init payload:
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', orderRef: 'ORD-001' } } ], options: {...}}Pre-populated parts pass through validation, defaults are applied, then each part is added to the calculator’s parts list (existing parts are cleared first). Per-part validation issues are surfaced as inline errors in the UI rather than blocking the whole init.
Part Properties:
| Property | Type | Description |
|---|---|---|
l | number | Part length in mm (defaults to 1 if missing) |
w | number | Part width in mm (defaults to 1 if missing) |
t | number | string | Thickness (optional, inherits from stock if not set) |
q | number | Quantity (default: 1) |
name | string | Part name/label. Normalised to UPPERCASE on intake. |
material | string | Material identifier for stock matching. Normalised to UPPERCASE on intake — match this against the uppercase form of your stock’s material field. |
orientationLock | 'l' | 'w' | '' | Orientation constraint: length-locked, width-locked, or free |
banding | object | Edge banding — V3 nested form: { sides: { l1, l2, w1, w2 } }. Legacy V2 flat form ({ x1, x2, y1, y2 } and 0/1 boolean shorthands) is accepted and migrated automatically. |
finish | object | Surface finish — V3 nested form: { faces: { a, b } }. Legacy flat { a, b } (with 0/1) is accepted and migrated. |
planing | object | Planing configuration |
customData | object | Arbitrary key-value data attached to the part. This is the only field that round-trips — it appears verbatim on the matching part in the smartcut/result event, so it’s the supported way to attach your own identifiers (order refs, line-item IDs, etc.) for reconciliation. |
stockId | string | Lock part to a specific stock item (by stock code) |
stock | object | Stock reference: { db_id?, code?, material?, thickness? } — locks the part to the stock it originated from. Useful when pre-populating from an existing cut list. |
Pre-populating after init. As an alternative to passing parts on init, host code can fire a smartcut/load window event whose detail matches the calculate-data structure (see smartcut/load event). The calculator will clear existing parts and import the supplied ones the same way as init-time pre-population.
Saw Options
Section titled “Saw Options”Saw configuration should be provided in the saw property (recommended) or in options for backward compatibility.
Complete Saw Configuration
Section titled “Complete Saw Configuration”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
Section titled “Configuration Options”API Version
Section titled “API Version”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
Section titled “Emit API Result”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
Section titled “Debug Mode”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.
Unit System
Section titled “Unit System”options: { unitSystem: 'metric' // 'metric' or 'imperial' (default: 'metric')}Controls the unit system used for display and input. 'imperial' switches dimensions to inches in the UI. Internally all values remain in millimetres.
Stock Grain
Section titled “Stock Grain”options: { stockGrain: 'l' // 'l', 'w', or '' — default grain direction applied to stock items without an explicit grain field}Number Format
Section titled “Number Format”options: { numberFormat: 'decimal', // 'decimal' or 'fraction' decimalPlaces: 2, fractionRoundTo: 0 // For fraction formatting}Orientation Model
Section titled “Orientation Model”options: { orientationModel: 0, // 0, 1, or 2 (controls how INPUT parts handle orientation) resultOrientationModel: 0 // 0, 1, or 2 (controls how RESULT parts are reported)}Orientation Model Values:
0- No orientation control1- Grain lock (parts maintain grain direction)2- Dimension lock (length always maps to longer dimension)
resultOrientationModel lets you display results in a different orientation convention than the inputs (e.g. accept any orientation on input but always report results with length on the longer side).
Part Limits
Section titled “Part Limits”options: { maxParts: 100, // Maximum parts per order (0 = unlimited, default: 0) minDimension: 10, // Minimum allowed part dimension in mm (default: 0) partTrim: 0, // Amount to trim from each part dimension before optimisation, in mm minSpacing: 0 // Minimum spacing between adjacent parts in mm}partTrim is applied symmetrically to l and w at calculation time — useful when you want to optimise on rough sizes but cut to a finished size. minSpacing adds a gap between parts independent of blade kerf.
Pagination
Section titled “Pagination”options: { partsPerPage: 10, // Parts shown per page when pagination enabled enable: { pagination: true // Enable pagination for parts list }}Field Order
Section titled “Field Order”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
Section titled “Custom Fields”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
Section titled “UI Customization”Control colors, enabled features, and field order:
options: { enable: { // Extras + machining banding: false, // Edge banding column on parts (default: false) finish: false, // Finishing column on parts (default: false) planing: false, // Planing column on parts (default: false) machining: false, // Machining operations (holes, corners) (default: false)
// Parts list UI orientation: true, // Part orientation lock control (default: true) partName: true, // Allow naming parts (default: true) pagination: false, // Paginate parts list (default: false) progressNumber: true, // Show progress/step numbers in UI (default: true) focus: true, // Highlight selected part in diagram on row focus (default: true) click: true, // Allow clicking parts in the diagram to select them (default: true) groups: false, // User-defined part groups (default: false) fullStock: false, // "Full sheet" toggle per part — purchase entire sheet without cutting (default: false) imageUpload: false, // Image upload column on parts (default: false)
// Diagram diagram: true, // Show cutting diagram (default: true) diagramNav: false, // Diagram navigation controls when multiple sheets (default: false)
// Import csvImport: false, // CSV import for parts (default: false) csvTemplate: false, // Show CSV template download link in import dialog (default: false)
// Diagnostics debug: false // Verbose debug logging — also flippable via `?debug` URL param (default: false) }, colors: { partA: '#118ab2', partB: '#06d6a0', // Alternating part color stock: '#ffd166', button: '#118ab2', // Required buttonText: '#ffffff', // Required headerBackground: '#ffffff', // Header background color headerText: '#212529' // Header text color }}When enable.fullStock is on, parts can be toggled to “full sheet” mode. If multiple sheet sizes exist for the chosen material, a dimension picker dialog opens; if fullSizeOnly: true is set on the stock, the part is locked into full-sheet mode automatically.
Extras Configuration
Section titled “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
Section titled “Basic Extras Setup”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: {...}}Labels must match the pricing levels. Each
pricingkey is a|-joined tuple with one choice per entry inlabels. The number oflabelsmust equal the number of|-separated levels in every key — e.g.labels: ['type', 'thickness'](2) pairs with'oak|1mm'(2 levels). A key with the wrong level count (or alabelslength that doesn’t match) means the extra can’t be built, and the widget shows “<Type> options couldn’t be loaded”.An extra only renders when its type is enabled in
options.enable(e.g.options.enable.banding = true). A disabled type ships no UI even iflabels/pricingare present — and produces no error.
Catalogue-linked extras (admin-configured)
Section titled “Catalogue-linked extras (admin-configured)”Banding and finish can instead be linked to a catalogue in the SmartCut admin (the Extras tab) rather than defined inline here. When a shop uses a catalogue-linked extra, SmartCut supplies its options to the widget automatically and the customer picks from an on-demand cascade (for example decor → finish → colour) instead of the inline labels/pricing grid above.
For these you don’t author labels/pricing — the catalogue owns both the option schema and the pricing, and large lists (such as an edge-banding decor range) are loaded on demand rather than embedded in the config. CSV-imported banding lists are handled this way. This is transparent to the embedding page: the widget renders the appropriate picker per extra. The inline labels/pricing form documented above is for shops that define their extras directly in init().
Extras Location Filtering
Section titled “Extras Location Filtering”Control which sides and faces are available for each extra type by adding a locations property directly to each extra configuration:
// 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
Section titled “Extras Location Groups”Create custom groupings of locations for bulk operations with optional pricing by adding a groups array directly to each extra configuration:
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 grouplabel(required) - Display name shown in the UIlocations(required) - Array of location strings included in this groupprice(optional) - Fixed price for the entire group (overrides individual location pricing)hideIndividualLocations(optional) - Whentrue, hides the individual location controls for locations in this grouprules(optional) - Validation rules that apply when using this group (see Group-Level Validation Rules)
Extras Validation Rules
Section titled “Extras Validation Rules”Apply dimension constraints to control which parts can have extras applied by adding a rules property directly to each extra configuration:
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 dimensionshortSide.min/shortSide.max- Constraints on the shorter dimensiont.min/t.max- Thickness constraintsformula(optional) - JavaScript expression for complex validation (can referencelongSide,shortSide,t)message- Custom error message shown when validation failslocations(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
Section titled “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:
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 locationformula(optional) - JavaScript expression for complex validationmessage(optional) - Custom error message for this location
Group-Level Validation Rules
Section titled “Group-Level Validation Rules”Groups can have their own validation rules that apply when any location in the group is selected:
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
Section titled “Rule Precedence”When multiple rules could apply to a location, the most specific rule wins:
- Location-specific rule - Rules defined in
rules.locations[]for a specific location - Group rule - Rules defined on a group that contains the location
- 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:
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 specificside.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
Section titled “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:
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 constraintsw.min/w.max- Width constraintst.min/t.max- Thickness constraintslongSide.min/longSide.max- Constraints on the longer dimensionshortSide.min/shortSide.max- Constraints on the shorter dimensioncrossDimensionalRule(optional) - Cross-dimensional validationprimaryMin- At least one side must be ≥ this valuesecondaryMin- The other side must be ≥ this value
formula(optional) - Formula expression for complex validation (can referencel,w,t,longSide,shortSide)- Supports arithmetic operators:
+,-,*,/ - Supports comparison operators:
>,<,>=,<=,== - Supports logical operators:
&&(AND),||(OR) - Supports ternary operator:
condition ? trueValue : falseValue
- Supports arithmetic operators:
message- Custom error message shown when validation fails
Example with formula:
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:
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
Section titled “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:
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 ruleenabled- 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 validmessage- Error message shown when validation fails
Available Variables:
| Variable | Type | Description |
|---|---|---|
l | number | Part length |
w | number | Part width |
t | number | Part thickness |
q | number | Part quantity |
longSide | number | max(l, w) |
shortSide | number | min(l, w) |
material | string | Material name |
name | string | Part name |
grain | string | Grain direction |
fullStock | boolean | Full stock purchase flag |
hasBanding | boolean | True if any banding applied |
hasFinish | boolean | True if any finish applied |
hasPlaning | boolean | True if any planing applied |
hasMachining | boolean | True if any machining operations |
extras.banding.sides.l1 | string | Banding on long side 1 |
extras.banding.sides.l2 | string | Banding on long side 2 |
extras.banding.sides.w1 | string | Banding on short side 1 |
extras.banding.sides.w2 | string | Banding on short side 2 |
extras.finish.faces.a | string | Finish on face A |
extras.finish.faces.b | string | Finish on face B |
machining.holes | number | Number of holes |
machining.corners | number | Number of corner operations |
Supported Operators:
- Arithmetic:
+,-,*,/ - Comparison:
>,<,>=,<=,== - Logical:
&&(AND),||(OR),!(NOT) - Ternary:
condition ? trueValue : falseValue - Parentheses:
(expression)
Example Rules:
// 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
Section titled “Complete Extras Example”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 } }}Product Catalog
Section titled “Product Catalog”Surface a product browser on the checkout so customers can pick from your catalogue before configuring parts. Independent of Product Filtering (which filters the stock list).
const initData = { stock: [...], products: { enabled: true, // Show the product browser tab showCategories: true // Group products by category in the browser }, options: {...}}A user picking a “simple” product fires smartcut/product-selected; picking a “formula” (configurable) product hands off to the Configurator. Stock filter “Create Cut List” confirmations fire smartcut/selection-confirmed.
You can deep-link directly to a product by appending ?product=<productId> to the page URL; the calculator picks up the parameter on mount.
API / Order Lookup
Section titled “API / Order Lookup”Wire up the storefront’s order-lookup view and the live-inventory watcher by supplying the host’s API config:
const initData = { api: { baseUrl: 'https://your-storefront.example.com', orgSlug: 'your-org-slug', isCustomDomain: false, // true if storefront is on a custom domain wsServer: 'wss://your-storefront.example.com', // optional orgId: '66abc123...' // optional Mongo org id (required with wsServer) }, options: {...}}Appending ?view=track-order (or ?view=order-lookup) to the page URL opens the order-lookup view on mount instead of the calculator.
When both wsServer and orgId are set, the calculator connects via Socket.IO and fires smartcut/inventoryUpdated whenever stock changes for the org.
Configurator (Formula Products)
Section titled “Configurator (Formula Products)”Enable formula-driven (parametric) products — typically used for configurable cabinets, where customer inputs (height/width/etc.) drive a JS formula that emits the parts list:
const initData = { config: { configurator: { enabled: true, url: 'https://your-host.example.com/configurator.html', // optional iframe URL spec: { /* configurator spec, opaque to the widget */ } } }, options: {...}}A product whose type === 'formula' (via the Product Catalog) routes to the Configurator; on confirmation the calculator receives the generated parts and falls through to the normal calculate flow.
Per-Pick Saw Mapping
Section titled “Per-Pick Saw Mapping”Different materials can route to different saws. When a customer picks a material whose db_sawId differs from the page-load saw, supply a per-saw map so the storefront UI can swap stock-type-driven fields, blade width, and validation without a server round-trip:
const initData = { saw: { /* page-load saw config */ stockType: 'sheet', bladeWidth: 3.2, cutType: 'efficiency' }, sawsById: { '66abc1...': { stockType: 'linear', bladeWidth: 1.6, cutType: 'efficiency' }, '66def2...': { stockType: 'sheet', bladeWidth: 4.0, cutType: 'guillotine', cutPreference: 'l' } }, options: {...}}Source of truth for saw resolution is server-side (resolveSawFromInputStock); sawsById exists purely to keep the storefront UI consistent with what the server will actually apply at calc time. Each map value mirrors the same shape as the top-level saw field.
Machining Configuration
Section titled “Machining Configuration”Advanced machining features for holes, corners, and custom operations:
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
Section titled “Events”The Checkout API dispatches custom window events for integration. Events fall into two groups:
- Output events (
smartcut/...) — the widget dispatches these onwindow; host code listens. - Input events (
smartcut/load) — host code dispatches these; the widget listens.
smartcut/ready
Section titled “smartcut/ready”Fired when the calculator is loaded and ready to initialize.
window.addEventListener('smartcut/ready', () => { window.smartcutCheckout.init(initData)})smartcut/initComplete
Section titled “smartcut/initComplete”Fired when initialization is complete.
window.addEventListener('smartcut/initComplete', () => { console.log('Calculator initialized')})smartcut/calculating
Section titled “smartcut/calculating”Fired when a calculation starts.
window.addEventListener('smartcut/calculating', () => { console.log('Calculation in progress...')})smartcut/beforeCalculate
Section titled “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- Callevent.preventDefault()to cancel the calculationevent.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
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.erroris set, the error message is displayed to the user - The
smartcut/resultevent will not fire
Calculation data structure (event.detail.data):
{ 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
Section titled “smartcut/result”Fired when optimization results are available.
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) console.log(result.checkout.formattedPlaningCost)})smartcut/validationError
Section titled “smartcut/validationError”Fired when input validation fails.
window.addEventListener('smartcut/validationError', () => { console.log('Please check your inputs')})smartcut/product-selected
Section titled “smartcut/product-selected”Fired when a customer picks a simple (non-configurable) product from the Product Catalog browser.
window.addEventListener('smartcut/product-selected', (event) => { console.log('Picked product:', event.detail.product) // event.detail = { product: Product, type: 'simple' }})Formula products do not emit this event — they route to the Configurator instead.
smartcut/selection-confirmed
Section titled “smartcut/selection-confirmed”Fired when the customer presses “Create Cut List” in the Product Filtering UI (the stock filter’s confirmation step).
window.addEventListener('smartcut/selection-confirmed', () => { console.log('Customer confirmed stock selection')})No detail payload — the widget scrolls to the calculator after dispatching.
smartcut/inventoryUpdated
Section titled “smartcut/inventoryUpdated”Fired when the live-inventory watcher detects a stock change for the configured org. Requires api.wsServer and api.orgId to be set.
window.addEventListener('smartcut/inventoryUpdated', () => { // Refresh your stock display or re-run a calculation})No detail payload.
smartcut/load (input event)
Section titled “smartcut/load (input event)”Host-dispatched event the widget listens for. Use this to load a saved cut list after init — equivalent to pre-populating parts on init, but available at any time:
window.dispatchEvent(new CustomEvent('smartcut/load', { detail: { inputs: { parts: [ { l: 600, w: 400, t: 18, q: 4, material: 'Plywood', name: 'Shelf' }, { l: 800, w: 300, t: 18, q: 2, material: 'Plywood', name: 'Side' } ] } }}))The widget clears existing parts and loads each part via the same path as init-time pre-population. Useful when the customer signs in and you want to restore their last basket, or when integrating with a “saved designs” feature in your storefront.
Result Format
Section titled “Result Format”The result object returned via the smartcut/result event contains different structures based on the apiVersion setting.
API Version 2 Result Structure
Section titled “API Version 2 Result Structure”When apiVersion: 2 is set, results use a flat extras structure:
{ 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, discount?: number, // Configured percentage discount (0–100), surfaced to pricing formulas pricingFormula?: string, // Per-stock pricing override evaluated at checkout // 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 }, sheets?: Array<{...}>, // Per-sheet analysis (same shape as `analysis` per row) — see V3 below 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
Section titled “API Version 3 Result Structure”When apiVersion: 3 is set (default), results use a nested extras structure:
{ 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, discount?: number, // Configured percentage discount (0–100), surfaced to pricing formulas pricingFormula?: string, // Per-stock pricing override evaluated at checkout sheets?: Array<{ // Per-sheet analysis — present when the cut server emits per-sheet data. areaEfficiency: number, // Pricing formulas iterate these when set so usedFraction reflects each finishArea: number, // physical sheet, not the order-wide average. bandingLength: number, partArea: number, totalParts: number, 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 }}Using Result Data
Section titled “Using Result Data”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
Section titled “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:
{ // 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.
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
Section titled “Product Filtering”Enable dynamic stock selection with filtering and search:
const initData = { stock: [ { l: 2800, w: 2070, t: 18, cost: 95, material: 'White MDF', code: 'MDF-2800-18', 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
Section titled “Support”- Email: [email protected]
- Chat: https://smartcut.dev/