From 19bd89c405e811320d156aa5e162412795420c1a Mon Sep 17 00:00:00 2001 From: jlpoole Date: Thu, 19 Mar 2026 09:10:05 -0700 Subject: [PATCH] All ChatGPT created, I just was scrivener and Feerless Leader. --- README.md | 46 +++++++- data/assumptions.json | 33 ++++++ data/bom.json | 85 ++++++++++++++ tools/generate_cvs.pl | 21 ++++ web/app.js | 249 ++++++++++++++++++++++++++++++++++++++++++ web/index.html | 89 +++++++++++++++ web/style.css | 75 +++++++++++++ 7 files changed, 596 insertions(+), 2 deletions(-) create mode 100644 data/assumptions.json create mode 100644 data/bom.json create mode 100755 tools/generate_cvs.pl create mode 100644 web/app.js create mode 100644 web/index.html create mode 100644 web/style.css diff --git a/README.md b/README.md index 2023e68..b744273 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,45 @@ -# filament-drybox-cost +# Filament Drybox Cost Model -Ascertain the out-of-pocket costs to create one or more dryboxes. \ No newline at end of file +20260319 ChatGPT +$Header$ + +## Purpose + +This project provides a transparent and reproducible cost model for the +"Filament Storage 2026" drybox project by GunplaMark. + +The model is intended to answer questions such as: + +- What is the out-of-pocket cost to build 1 drybox? +- How does the per-unit cost change when building 4 dryboxes? +- How does the per-unit cost change when building 12 dryboxes? +- What inventory remains after each build scenario? +- How much do filament and electricity contribute to the total? + +The model accounts for: + +- lot-based purchasing +- per-box usage +- printed part filament cost +- electrical cost of printing +- optional waste factor + +See video at https://youtu.be/lJDoVH7qTKs?si=cQMhUda7RQcuIDJU by user GunplaMark entitled "Filament Storage 2026 - The Final Boss of My Drybox Journey". + + +## Repository Layout + +```text +. +├── LICENSE +├── README.md +├── data +│ ├── assumptions.json +│ └── bom.json +├── export +├── tools +│ └── generate_csv.pl +└── web + ├── app.js + ├── index.html + └── style.css diff --git a/data/assumptions.json b/data/assumptions.json new file mode 100644 index 0000000..0f75eab --- /dev/null +++ b/data/assumptions.json @@ -0,0 +1,33 @@ +{ + "currency": "USD", + "waste_factor": 1.15, + "electricity_cost_per_kwh": 0.12, + "printer_average_watts": 120, + "print_hours_per_box": 8.0, + "target_box_counts": [1, 4, 12], + "printed_materials": [ + { + "id": "petg", + "name": "PETG", + "cost_per_kg": 22.00, + "grams_per_box": 220 + }, + { + "id": "asa", + "name": "ASA", + "cost_per_kg": 28.00, + "grams_per_box": 90 + }, + { + "id": "tpu", + "name": "TPU", + "cost_per_kg": 26.00, + "grams_per_box": 20 + } + ], + "notes": [ + "Printed weights are initial estimates and should later be replaced with slicer-derived actuals.", + "Waste factor is applied to printed material cost only.", + "Electricity uses printer_average_watts multiplied by print_hours_per_box." + ] +} diff --git a/data/bom.json b/data/bom.json new file mode 100644 index 0000000..41691db --- /dev/null +++ b/data/bom.json @@ -0,0 +1,85 @@ +{ + "project_name": "Filament Drybox Cost Model", + "source_model": "Filament Storage 2026 by GunplaMark", + "source_url": "https://www.printables.com/", + "currency": "USD", + "components": [ + { + "id": "container", + "name": "4L cereal container", + "category": "hardware", + "unit_type": "each", + "pack_qty": 4, + "pack_cost": 39.99, + "units_used_per_box": 1, + "optional": false, + "notes": "Praki, Wildone, or Skroam style 4L container" + }, + { + "id": "bearing", + "name": "608 bearing", + "category": "hardware", + "unit_type": "each", + "pack_qty": 10, + "pack_cost": 11.99, + "units_used_per_box": 4, + "optional": false, + "notes": "Open, ungreased fidget-spinner style bearings" + }, + { + "id": "badge_reel", + "name": "badge reel", + "category": "hardware", + "unit_type": "each", + "pack_qty": 10, + "pack_cost": 14.99, + "units_used_per_box": 1, + "optional": false, + "notes": "Used to harvest spring" + }, + { + "id": "foam_cord", + "name": "3mm silicone foam cord", + "category": "consumable", + "unit_type": "meter", + "pack_qty": 10, + "pack_cost": 9.99, + "units_used_per_box": 0.70, + "optional": false, + "notes": "Length per box is an estimate; measure actual lid circumference" + }, + { + "id": "desiccant", + "name": "activated alumina desiccant", + "category": "consumable", + "unit_type": "gram", + "pack_qty": 2000, + "pack_cost": 24.99, + "units_used_per_box": 150, + "optional": false, + "notes": "Silica gel could be modeled as an alternative later" + }, + { + "id": "hygrometer", + "name": "rectangular hygrometer", + "category": "hardware", + "unit_type": "each", + "pack_qty": 4, + "pack_cost": 17.99, + "units_used_per_box": 1, + "optional": false, + "notes": "Common small rectangular hygrometer" + }, + { + "id": "nfc_tag", + "name": "25mm NFC tag", + "category": "optional", + "unit_type": "each", + "pack_qty": 20, + "pack_cost": 9.99, + "units_used_per_box": 1, + "optional": true, + "notes": "Optional spool identification tag" + } + ] +} diff --git a/tools/generate_cvs.pl b/tools/generate_cvs.pl new file mode 100755 index 0000000..b27de50 --- /dev/null +++ b/tools/generate_cvs.pl @@ -0,0 +1,21 @@ +#!/usr/bin/perl +# +# Example command lines: +# +# perl generate_csv.pl +# +# 20260319 ChatGPT +# $Header$ +# +# Purpose: +# Placeholder script for later CSV export from the BOM and assumptions data. +# + +use strict; +use warnings; + +print "generate_csv.pl: placeholder script\n"; +print "Later revision will read ../data/bom.json and ../data/assumptions.json\n"; +print "and produce a CSV summary under ../export/.\n"; + +exit 0; diff --git a/web/app.js b/web/app.js new file mode 100644 index 0000000..c592037 --- /dev/null +++ b/web/app.js @@ -0,0 +1,249 @@ +/* +20260319 ChatGPT +$Header$ +*/ + +"use strict"; + +function currency(value) { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD" + }).format(value); +} + +function ceilDivUsage(requiredUnits, packQty) { + return Math.ceil(requiredUnits / packQty); +} + +function computePrintingCost(assumptions, boxCount) { + let materialCost = 0; + + for (const material of assumptions.printed_materials) { + const totalGrams = material.grams_per_box * boxCount; + const totalKg = totalGrams / 1000.0; + materialCost += totalKg * material.cost_per_kg; + } + + materialCost *= assumptions.waste_factor; + + const totalKwh = + (assumptions.printer_average_watts / 1000.0) * + assumptions.print_hours_per_box * + boxCount; + + const electricityCost = totalKwh * assumptions.electricity_cost_per_kwh; + + return { + material_cost: materialCost, + electricity_cost: electricityCost, + total_printing_cost: materialCost + electricityCost + }; +} + +function computeHardwareScenario(bom, boxCount, includeOptional = true) { + const componentRows = []; + let totalHardwareCost = 0; + + for (const component of bom.components) { + if (!includeOptional && component.optional) { + continue; + } + + const requiredUnits = component.units_used_per_box * boxCount; + const packsRequired = ceilDivUsage(requiredUnits, component.pack_qty); + const purchasedUnits = packsRequired * component.pack_qty; + const leftoverUnits = purchasedUnits - requiredUnits; + const totalCost = packsRequired * component.pack_cost; + + totalHardwareCost += totalCost; + + componentRows.push({ + id: component.id, + name: component.name, + required_units: requiredUnits, + pack_qty: component.pack_qty, + pack_cost: component.pack_cost, + packs_required: packsRequired, + purchased_units: purchasedUnits, + leftover_units: leftoverUnits, + total_cost: totalCost, + optional: component.optional, + unit_type: component.unit_type, + notes: component.notes + }); + } + + return { + box_count: boxCount, + total_hardware_cost: totalHardwareCost, + component_rows: componentRows + }; +} + +function renderAssumptions(assumptions) { + const div = document.getElementById("assumptions"); + + const materialLines = assumptions.printed_materials.map((m) => { + return `
  • ${m.name}: ${m.grams_per_box} g/box at ${currency(m.cost_per_kg)}/kg
  • `; + }).join(""); + + div.innerHTML = ` + +

    Printed materials

    + + `; +} + +function renderSummaryTable(bom, assumptions) { + const tbody = document.querySelector("#summary-table tbody"); + tbody.innerHTML = ""; + + for (const boxCount of assumptions.target_box_counts) { + const hardware = computeHardwareScenario(bom, boxCount, true); + const printing = computePrintingCost(assumptions, boxCount); + const totalCost = hardware.total_hardware_cost + printing.total_printing_cost; + const costPerBox = totalCost / boxCount; + + const tr = document.createElement("tr"); + tr.innerHTML = ` + ${boxCount} + ${currency(hardware.total_hardware_cost)} + ${currency(printing.total_printing_cost)} + ${currency(totalCost)} + ${currency(costPerBox)} + `; + tbody.appendChild(tr); + } +} + +function renderComponentBreakdowns(bom, assumptions) { + const container = document.getElementById("component-breakdowns"); + container.innerHTML = ""; + + for (const boxCount of assumptions.target_box_counts) { + const hardware = computeHardwareScenario(bom, boxCount, true); + + const section = document.createElement("div"); + section.className = "scenario-block"; + + let rowsHtml = ""; + for (const row of hardware.component_rows) { + rowsHtml += ` + + ${row.name}${row.optional ? " (optional)" : ""} + ${row.required_units} + ${row.pack_qty} + ${currency(row.pack_cost)} + ${row.packs_required} + ${currency(row.total_cost)} + ${row.notes || ""} + + `; + } + + section.innerHTML = ` +

    ${boxCount} box${boxCount === 1 ? "" : "es"}

    + + + + + + + + + + + + + + ${rowsHtml} + +
    ComponentRequired UnitsPack QtyPack CostPacks RequiredTotal CostNotes
    + `; + + container.appendChild(section); + } +} + +function renderLeftovers(bom, assumptions) { + const container = document.getElementById("leftovers"); + container.innerHTML = ""; + + for (const boxCount of assumptions.target_box_counts) { + const hardware = computeHardwareScenario(bom, boxCount, true); + + const section = document.createElement("div"); + section.className = "scenario-block"; + + let rowsHtml = ""; + for (const row of hardware.component_rows) { + rowsHtml += ` + + ${row.name} + ${row.purchased_units} + ${row.required_units} + ${row.leftover_units} + ${row.unit_type} + + `; + } + + section.innerHTML = ` +

    ${boxCount} box${boxCount === 1 ? "" : "es"}

    + + + + + + + + + + + + ${rowsHtml} + +
    ComponentPurchased UnitsUsed UnitsLeftover UnitsUnit Type
    + `; + + container.appendChild(section); + } +} + +async function loadJson(url) { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to load ${url}: ${response.status}`); + } + return await response.json(); +} + +async function main() { + try { + const bom = await loadJson("../data/bom.json"); + const assumptions = await loadJson("../data/assumptions.json"); + + renderAssumptions(assumptions); + renderSummaryTable(bom, assumptions); + renderComponentBreakdowns(bom, assumptions); + renderLeftovers(bom, assumptions); + } catch (err) { + console.error(err); + document.body.innerHTML = ` +
    +

    Error

    +

    Unable to load project data.

    +
    ${String(err)}
    +
    + `; + } +} + +main(); diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..8cac54b --- /dev/null +++ b/web/index.html @@ -0,0 +1,89 @@ + + + + + + + Filament Drybox Cost Model + + + +
    +

    Filament Drybox Cost Model

    +

    + Lot-based purchasing, filament cost, and electricity for 1, 4, and 12 dryboxes +

    +
    + +
    +
    +

    Overview

    +

    + This page estimates the out-of-pocket cost of building filament dryboxes, + taking into account both purchased hardware sold in lots and the printing + cost of the required parts. +

    +
    + +
    +

    Assumptions

    +
    +
    + +
    +

    Summary

    + + + + + + + + + + + +
    BoxesTotal HardwareTotal PrintingTotal CostCost / Box
    +
    + +
    +

    Component Breakdown

    +
    +
    + +
    +

    Inventory Leftovers

    +
    +
    + +
    +

    Notes

    +
      +
    • Hardware pricing is modeled using whole-pack purchases.
    • +
    • Printed material pricing is based on estimated grams per box.
    • +
    • Electricity is based on average printer wattage over total print time.
    • +
    • All initial prices and print weights should be revised with measured values.
    • +
    +
    +
    + + + + + + diff --git a/web/style.css b/web/style.css new file mode 100644 index 0000000..135a027 --- /dev/null +++ b/web/style.css @@ -0,0 +1,75 @@ +/* +20260319 ChatGPT +$Header$ +*/ + +body { + font-family: Arial, Helvetica, sans-serif; + margin: 0; + padding: 0; + background: #f4f6f8; + color: #222; +} + +header, +footer { + background: #1f2933; + color: #fff; + padding: 1rem 1.5rem; +} + +header h1, +footer p { + margin: 0; +} + +.subtitle { + margin-top: 0.5rem; + color: #d9e2ec; +} + +main { + max-width: 1100px; + margin: 1.5rem auto; + padding: 0 1rem; +} + +.card { + background: #fff; + border: 1px solid #d9e2ec; + border-radius: 8px; + padding: 1rem 1.25rem; + margin-bottom: 1rem; + box-shadow: 0 1px 3px rgba(0,0,0,0.06); +} + +table { + width: 100%; + border-collapse: collapse; + margin-top: 0.5rem; +} + +th, +td { + border: 1px solid #cbd2d9; + padding: 0.55rem 0.7rem; + text-align: left; + vertical-align: top; +} + +th { + background: #e9eff5; +} + +.scenario-block { + margin-bottom: 1.5rem; +} + +.small-note { + color: #52606d; + font-size: 0.92rem; +} + +.mono { + font-family: "Courier New", Courier, monospace; +}