├── .gitignore ├── defaultPrintOptions.js ├── getPrice.js ├── package.json ├── priceCalculator.js ├── pricing.js ├── readme.md └── test ├── testGetPrice.js └── testPriceCalculator.js /.gitignore: -------------------------------------------------------------------------------- 1 | ############# 2 | ## Custom 3 | ############# 4 | node_modules/* -------------------------------------------------------------------------------- /defaultPrintOptions.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printer: 'FFF', 3 | material: 'ABS', 4 | color: 'white', 5 | units: 'mm', 6 | layerResolution: 0.3, 7 | percentInfill: 25 8 | } 9 | -------------------------------------------------------------------------------- /getPrice.js: -------------------------------------------------------------------------------- 1 | var xtend = require('xtend') 2 | var priceCalculator = require('./priceCalculator.js') 3 | var defaultPrintOptions = require('./defaultPrintOptions.js') 4 | 5 | module.exports = function getPrice(printOptions,admeshOutput) { 6 | var printOptions = xtend(defaultPrintOptions,printOptions) 7 | 8 | //make sure percentages are numbers and between 0 and 100 9 | if (isNaN(printOptions.percentInfill) || printOptions.percentInfill < 0 || printOptions.percentInfill > 100){ 10 | throw new Error('invalid percent infill: ' + printOptions.percentInfill) 11 | } 12 | 13 | //make sure admeshOutput has volume 14 | if (typeof admeshOutput.volume === 'undefined') { 15 | throw new Error('undefined admesh volume: ' + admeshOutput.volume) 16 | } 17 | 18 | if (printOptions.units == "mm"){ 19 | return priceCalculator(printOptions,admeshOutput.volume) 20 | } else if (printOptions.units == "in"){ 21 | return priceCalculator(printOptions,admeshOutput.volume * 16387.064) 22 | } else { 23 | throw new Error('invalid print option units: ' + printOptions.units) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "3d-print-price-calculator", 3 | "version": "0.0.1", 4 | "main": "./getPrice.js", 5 | "author": { 6 | "name": "pwnate", 7 | "email": "me@davisnathan.com" 8 | }, 9 | "description": "this is a thing that calculates the price of a 3d object given its volume", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/coding-in-the-wild/3d-print-price-calculator" 13 | }, 14 | "devDependencies": { 15 | "tap": "~0.4.8" 16 | }, 17 | "directories": { 18 | "test": "test" 19 | }, 20 | "scripts": { 21 | "test": "tap ./test/*.js" 22 | }, 23 | "dependencies": { 24 | "xtend": "~2.1.2" 25 | } 26 | } -------------------------------------------------------------------------------- /priceCalculator.js: -------------------------------------------------------------------------------- 1 | var pricing = require('./pricing.js') 2 | 3 | module.exports = function calculator(printOptions,volume) { 4 | // make sure printer type is defined in pricing object 5 | if (typeof pricing[printOptions.printer] === 'undefined') { 6 | throw new Error('invalid printer type: ' + printOptions.printer) 7 | } 8 | 9 | // just to make things simpler 10 | var printerPricing = pricing[printOptions.printer] 11 | 12 | // make sure material and color is defined in pricing object 13 | if (typeof printerPricing[printOptions.material] === 'undefined') { 14 | throw new Error('invalid print material for printer type: ' + printOptions.material) 15 | } else if (typeof printerPricing[printOptions.material][printOptions.color] === 'undefined') { 16 | throw new Error('invalid print color for material and printer type: ' + printOptions.color) 17 | } 18 | 19 | // make sure layer resolution discount is defined in pricing object 20 | if (typeof printerPricing.layerResolutionDiscount === 'undefined') { 21 | throw new Error('undefined layer resolution discount: ' + printerPricing.layerResolutionDiscount) 22 | } else if (typeof printerPricing.layerResolutionDiscount[printOptions.layerResolution] === 'undefined') { 23 | throw new Error('invalid layer resolution: ' + printOptions.layerResolution) 24 | } 25 | 26 | // make sure infillDiscountThreshhold is a number and is between 0 and 100 27 | if (isNaN(printerPricing.infillDiscountThreshhold) || printerPricing.infillDiscountThreshhold < 0 || printerPricing.infillDiscountThreshhold > 100) { 28 | throw new Error('invalid infill discount threshhold: ' + printerPricing.infillDiscountThreshhold) 29 | } 30 | 31 | // make sure all necessary variables are numbers 32 | if (isNaN(printerPricing[printOptions.material][printOptions.color])) { 33 | throw new Error('price for printer, material, and color is NaN: ' + printerPricing[printOptions.material][printOptions.color]) 34 | } else if (isNaN(printerPricing.infillDiscount)) { 35 | throw new Error('infill discount is NaN: ' + printerPricing.infillDiscount) 36 | } else if (isNaN(printerPricing.layerResolutionDiscount[printOptions.layerResolution])) { 37 | throw new Error('layer resolution discount is NaN: ' + printerPricing.layerResolutionDiscount[printOptions.layerResolution]) 38 | } else if (isNaN(printerPricing.basePrice)) { 39 | throw new Error('minimum price is NaN: ' + printerPricing.basePrice) 40 | } 41 | 42 | // make sure volume is a number greater than zero 43 | if (isNaN(volume)) { 44 | throw new Error('volume is NaN: ' + volume) 45 | } else if (volume <= 0) { 46 | throw new Error('volume is not greater than zero: ' + volume) 47 | } 48 | 49 | 50 | var pricePerCC = printerPricing[printOptions.material][printOptions.color] 51 | var infillDiscount = (printOptions.percentInfill <= printerPricing.infillDiscountThreshhold ? 52 | printerPricing.infillDiscount : 0) 53 | var layerResolutionDiscount = printerPricing.layerResolutionDiscount[printOptions.layerResolution] 54 | var totalDiscount = infillDiscount + layerResolutionDiscount 55 | 56 | // make sure discount applied is not greater than 95% 57 | if (totalDiscount > 0.95) { 58 | throw new Error('total discount is greater than 95%: ' + totalDiscount) 59 | } 60 | 61 | return ((volume * pricePerCC * (1 - totalDiscount) / 1000) + printerPricing.basePrice) 62 | } 63 | -------------------------------------------------------------------------------- /pricing.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | FFF: { 3 | ABS: { 4 | white: 0.75, 5 | black: 0.75, 6 | grey: 0.75, 7 | navyBlue: 0.75, 8 | red: 0.75, 9 | natural: 0.75, 10 | silver: 0.80 11 | }, 12 | infillDiscount: 0.2, 13 | infillDiscountThreshhold: 50, 14 | layerResolutionDiscount: { 15 | '0.1': 0, 16 | '0.2': 0.05, 17 | '0.3': 0.1 18 | }, 19 | basePrice: 5 20 | }, 21 | 22 | 23 | SLA: { 24 | 'Acrylic Resin': { 25 | white: 0.95, 26 | black: 0.95 27 | }, 28 | infillDiscount: 0, 29 | infillDiscountThreshhold: 0, 30 | layerResolutionDiscount: { 31 | '0.1': 0, 32 | '0.2': 0.05, 33 | '0.3': 0.1 34 | }, 35 | basePrice: 10 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | #3d Print Price Calculator 2 | 3 | volume units for priceCalculator.js must be in cubic millimeters (mm^3) 4 | volume units for getPrice.js must be specified as "mm" or "in" in printOptions object 5 | pricing is based off of cubic centimeters (cm^3) -------------------------------------------------------------------------------- /test/testGetPrice.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test 2 | var getPrice = require('../getPrice.js') 3 | var pricing = require('../pricing.js') 4 | var defaultPrintOptions = require('../defaultPrintOptions') 5 | 6 | var admeshOutput = { 7 | volume: 10000 8 | } 9 | 10 | 11 | test("getPrice module returns a function", function(t){ 12 | t.equal(typeof getPrice, "function") 13 | t.end() 14 | }) 15 | 16 | test("printOptions returns price for defaultPrintOptions when empty or partially empty", function(t){ 17 | //should equal ((volume * pricePerCC * (1 - infillDiscount - layerResolutionDiscount) / 1000) 18 | // + printerPricing.basePrice) 19 | t.equal(Math.round(100*getPrice({},admeshOutput)),1025) 20 | t.equal(Math.round(100*getPrice({printer:'FFF',color:'silver'},admeshOutput)),1060) 21 | t.end() 22 | }) 23 | 24 | test("using percentInfill out of range throws error", function(t){ 25 | t.plan(2) 26 | try { 27 | getPrice({percentInfill:120},admeshOutput) 28 | } catch(e) { 29 | t.ok(true, 'percent infill of 120% throws error') 30 | } 31 | try { 32 | getPrice({percentInfill:-10},admeshOutput) 33 | } catch(e) { 34 | t.ok(true, 'percent infill of -10% throws error') 35 | } 36 | t.end() 37 | }) 38 | 39 | test("using method, material, units, or layerResolution not in pricing returns error", function(t){ 40 | t.plan(4) 41 | try { 42 | getPrice({printer:'supacoolPrinter'},admeshOutput) 43 | } catch(e) { 44 | t.ok(true,'printer type "supacoolPrinter" throws error') 45 | } 46 | 47 | try { 48 | getPrice({material:'blueSteel'},admeshOutput) 49 | } catch(e) { 50 | t.ok(true, 'material "blueSteel" throws error') 51 | } 52 | 53 | try { 54 | getPrice({color:'rainbow'},admeshOutput) 55 | } catch(e) { 56 | t.ok(true, 'color "rainbow" throws error') 57 | } 58 | 59 | try { 60 | getPrice({layerResolution:0.5},admeshOutput) 61 | } catch(e) { 62 | t.ok(true, 'layer resolution of 0.5mm throws error') 63 | } 64 | 65 | t.end() 66 | }) 67 | 68 | test("admeshOutput with wrong volume returns error", function(t){ 69 | t.plan(4) 70 | 71 | try { 72 | getPrice({},{}) 73 | } catch(e) { 74 | t.ok(true, 'admeshOutput with undefined volume throws error') 75 | } 76 | 77 | try { 78 | getPrice({},{volume: 'birds'}) 79 | } catch(e) { 80 | t.ok(true, 'admeshOutput volume that is not a number throws error') 81 | } 82 | 83 | try { 84 | getPrice({},{volume: -100}) 85 | } catch(e) { 86 | t.ok(true, 'admeshOutput volume less than zero throws error') 87 | } 88 | 89 | try { 90 | getPrice({},{volume: 0}) 91 | } catch(e) { 92 | t.ok(true, 'admeshOutput volume equal to zero throws error') 93 | } 94 | 95 | t.end() 96 | }) 97 | 98 | test("price is right", function(t){ 99 | //should equal ((volume * pricePerCC * (1 - infillDiscount - layerResolutionDiscount) / 1000) 100 | // + printerPricing.basePrice) 101 | t.equal(Math.round(100*getPrice({},admeshOutput)),1025) 102 | t.end() 103 | }) 104 | 105 | test("price is right with units in inches", function(t){ 106 | //should equal ((volume * 16387.064 * pricePerCC * (1 - infillDiscount - layerResolutionDiscount) / 1000) 107 | // + printerPricing.basePrice) 108 | t.equal(Math.round(100*getPrice({units:'in'},admeshOutput)),8603709) 109 | t.end() 110 | }) 111 | 112 | test("returns error for units in anything other than 'mm' or 'in'", function(t){ 113 | t.plan(1) 114 | try { 115 | getPrice(pricing,defaultPrintOptions,{units:'fake units'},admeshOutput) 116 | } catch(e) { 117 | t.ok(true, 'units other than in or mm throws error') 118 | } 119 | t.end() 120 | }) 121 | -------------------------------------------------------------------------------- /test/testPriceCalculator.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test 2 | var priceCalculator = require('../priceCalculator.js') 3 | var defaultPrintOptions = require('../defaultPrintOptions.js') 4 | var pricing = require('../pricing.js') 5 | 6 | 7 | var volume = 10000 8 | 9 | test("returns a function", function (t) { 10 | t.equal(typeof priceCalculator, 'function') 11 | t.end() 12 | }) 13 | 14 | test("returns number", function (t){ 15 | t.equal(typeof priceCalculator(defaultPrintOptions,volume), 'number') 16 | t.end() 17 | }) 18 | 19 | test("price is right", function (t) { 20 | //should equal ((volume * pricePerCC * (1 - infillDiscount - layerResolutionDiscount) / 1000) 21 | // + printerPricing.basePrice) 22 | t.equal(Math.round(100*priceCalculator(defaultPrintOptions,volume)),1025) 23 | t.end() 24 | }) 25 | 26 | test("price is right with different options", function(t){ 27 | //should equal ((volume * pricePerCC * (1 - infillDiscount - layerResolutionDiscount) / 1000) 28 | // + printerPricing.basePrice) 29 | t.equal(Math.round(100*priceCalculator({ 30 | printer:'FFF', 31 | material:'ABS', 32 | color:'silver', 33 | layerResolution:0.2, 34 | percentInfill:60},volume)),1260) 35 | t.end() 36 | }) 37 | 38 | test("tiny object has minimum price", function (t){ 39 | t.equal(Math.round(100*priceCalculator(defaultPrintOptions,100)),505) 40 | t.end() 41 | }) 42 | --------------------------------------------------------------------------------