1.00, // 0% extra waste for single-color 2 => 1.15, // +15% purge tower & poop waste 3 => 1.25, // +25% purge tower & poop waste 4 => 1.35, // +35% purge tower & poop waste ]; /** * Material base multipliers relative to standard PLA. */ public const MATERIAL_MULTIPLIERS = [ 'pla' => 1.00, 'petg' => 1.20, 'silk' => 1.30, 'tpu' => 1.40, ]; /** * Calculate comprehensive 3D printing costs and suggested retail price. * * @param float|int|null $weightGrams Model weight in grams * @param float|int|null $printTimeHours Printing duration in hours * @param int $colorCount Number of filament colors (1 to 4) * @param string $material Material code ('pla', 'petg', 'silk', 'tpu') * @param float|null $customMarkupPercent Optional override for markup percentage (e.g. 60%) * @return array Detailed calculation breakdown */ public function calculate( float|int|null $weightGrams = 80, float|int|null $printTimeHours = 4.0, int $colorCount = 1, string $material = 'pla', ?float $customMarkupPercent = null ): array { $weight = max(1.0, (float) ($weightGrams ?: 80)); $hours = max(0.2, (float) ($printTimeHours ?: 4.0)); $colors = max(1, min(4, $colorCount)); $matCode = strtolower(trim($material)) ?: 'pla'; // 1. Settings / Rates configuration $filamentPriceKg = (float) Setting::get('calc_filament_price_kg', 20.00); // € 20 / kg $electricityPriceKwh = (float) Setting::get('calc_electricity_kwh', 0.35); // € 0.35 / kWh $printerWattage = (int) Setting::get('calc_printer_wattage', 150); // 150 W average Bambu Lab $machineHourlyRate = (float) Setting::get('calc_machine_hourly_rate', 0.50); // € 0.50 / uur afschrijving & nozzle/bed slijtage $handlingFlat = (float) Setting::get('calc_handling_flat', 1.50); // € 1.50 voorbereiding, supports & verpakking $failureRate = (float) Setting::get('calc_failure_rate', 5.0); // 5% faalkans opslag $markupPercent = $customMarkupPercent !== null ? $customMarkupPercent : (float) Setting::get('calc_default_markup', 65.0); // 65% winstmarge $vatPercent = 21.0; // 21% btw // 2. Multipliers $colorWasteMult = self::COLOR_WASTE_MULTIPLIERS[$colors] ?? 1.00; $matMult = self::MATERIAL_MULTIPLIERS[$matCode] ?? 1.00; // 3. Material cost (includes multi-color purge tower waste) $effectiveWeight = round($weight * $colorWasteMult, 1); $materialCost = ($effectiveWeight / 1000.0) * ($filamentPriceKg * $matMult); // 4. Electricity cost $kw = $printerWattage / 1000.0; $kwhUsed = $hours * $kw; $electricityCost = $kwhUsed * $electricityPriceKwh; // 5. Machine wear & depreciation $machineCost = $hours * $machineHourlyRate; // 6. Multi-color handling fee (extra setup for loading AMS spools) $colorHandlingCost = max(0, $colors - 1) * 0.50; // 7. Handling / labor $handlingCost = $handlingFlat + $colorHandlingCost; // 8. Direct production cost $subtotalCost = $materialCost + $electricityCost + $machineCost + $handlingCost; // 9. Failure rate risk buffer $failureCost = $subtotalCost * ($failureRate / 100.0); // 10. Net cost price (direct production cost + risk) $totalCostPrice = $subtotalCost + $failureCost; // 11. Margin & Retail price excl. VAT $profitAmount = $totalCostPrice * ($markupPercent / 100.0); $priceExclVat = $totalCostPrice + $profitAmount; // 12. VAT (21%) & Consumer price $vatAmount = $priceExclVat * ($vatPercent / 100.0); $consumerPriceExact = $priceExclVat + $vatAmount; // 13. Smart commercial rounding (.95 / .50) $suggestedRetailPrice = $this->smartRound($consumerPriceExact); return [ 'inputs' => [ 'weight_grams' => $weight, 'print_time_hours' => $hours, 'color_count' => $colors, 'material' => $matCode, 'markup_percent' => $markupPercent, ], 'rates' => [ 'filament_price_kg' => $filamentPriceKg, 'electricity_price_kwh' => $electricityPriceKwh, 'machine_hourly_rate' => $machineHourlyRate, 'handling_flat' => $handlingFlat, 'failure_rate_percent' => $failureRate, ], 'breakdown' => [ 'effective_weight_grams' => $effectiveWeight, 'purge_waste_grams' => round($effectiveWeight - $weight, 1), 'material_cost' => round($materialCost, 2), 'electricity_cost' => round($electricityCost, 2), 'machine_cost' => round($machineCost, 2), 'handling_cost' => round($handlingCost, 2), 'failure_cost' => round($failureCost, 2), 'total_cost_price' => round($totalCostPrice, 2), 'profit_amount' => round($profitAmount, 2), 'price_excl_vat' => round($priceExclVat, 2), 'vat_amount' => round($vatAmount, 2), 'price_incl_vat_exact' => round($consumerPriceExact, 2), ], 'suggested_price' => $suggestedRetailPrice, 'suggested_price_formatted' => '€ '.number_format($suggestedRetailPrice, 2, ',', '.'), ]; } /** * Smart commercial rounding to appealing .95 or .50 price points. */ protected function smartRound(float $rawPrice): float { // Enforce minimum price of 6.95 if ($rawPrice <= 7.00) { return 6.95; } $floor = floor($rawPrice); $fraction = $rawPrice - $floor; if ($fraction <= 0.25) { $rounded = $floor - 0.05; // e.g. 14.10 -> 13.95 or 14.00 -> 13.95, but keep at least floor return max(6.95, $floor < $rawPrice ? $floor + 0.50 : $floor - 0.05); } elseif ($fraction <= 0.70) { return $floor + 0.50; // e.g. 14.40 -> 14.50 } else { return $floor + 0.95; // e.g. 14.80 -> 14.95 } } }