feat(web + backend): advanced and simple quote.

This commit is contained in:
2026-01-27 23:38:47 +01:00
parent 7dc6741808
commit 443ff04430
26 changed files with 1773 additions and 52 deletions

View File

@@ -60,11 +60,14 @@ class GCodeParser:
# Parse Time
if "estimated printing time =" in line: # Header
time_str = line.split("=")[1].strip()
logger.info(f"Parsing time string (Header): '{time_str}'")
stats["print_time_seconds"] = GCodeParser._parse_time_string(time_str)
elif "total estimated time:" in line: # Footer
parts = line.split("total estimated time:")
if len(parts) > 1:
stats["print_time_seconds"] = GCodeParser._parse_time_string(parts[1].strip())
time_str = parts[1].strip()
logger.info(f"Parsing time string (Footer): '{time_str}'")
stats["print_time_seconds"] = GCodeParser._parse_time_string(time_str)
# Parse Filament info
if "filament used [g] =" in line:
@@ -94,9 +97,21 @@ class GCodeParser:
@staticmethod
def _parse_time_string(time_str: str) -> int:
"""
Converts '1d 2h 3m 4s' to seconds.
Converts '1d 2h 3m 4s' or 'HH:MM:SS' to seconds.
"""
total_seconds = 0
# Try HH:MM:SS or MM:SS format
if ':' in time_str:
parts = time_str.split(':')
parts = [int(p) for p in parts]
if len(parts) == 3: # HH:MM:SS
total_seconds = parts[0] * 3600 + parts[1] * 60 + parts[2]
elif len(parts) == 2: # MM:SS
total_seconds = parts[0] * 60 + parts[1]
return total_seconds
# Original regex parsing for "1h 2m 3s"
days = re.search(r'(\d+)d', time_str)
hours = re.search(r'(\d+)h', time_str)
mins = re.search(r'(\d+)m', time_str)
@@ -137,6 +152,13 @@ class QuoteCalculator:
markup_factor = 1.0 + (settings.MARKUP_PERCENT / 100.0)
total_price = subtotal * markup_factor
logger.info("Cost Calculation:")
logger.info(f" - Use: {stats['filament_weight_g']:.2f}g @ {settings.FILAMENT_COST_PER_KG}€/kg = {material_cost:.2f}")
logger.info(f" - Time: {print_time_hours:.4f}h @ {settings.MACHINE_COST_PER_HOUR}€/h = {machine_cost:.2f}")
logger.info(f" - Power: {kwh_used:.4f}kWh @ {settings.ENERGY_COST_PER_KWH}€/kWh = {energy_cost:.2f}")
logger.info(f" - Subtotal: {subtotal:.2f}")
logger.info(f" - Total (Markup {settings.MARKUP_PERCENT}%): {total_price:.2f}")
return {
"breakdown": {
"material_cost": round(material_cost, 2),

View File

@@ -1,4 +1,5 @@
import os
import sys
class Settings:
# Directories
@@ -7,13 +8,18 @@ class Settings:
PROFILES_DIR = os.environ.get("PROFILES_DIR", os.path.join(BASE_DIR, "profiles"))
# Slicer Paths
SLICER_PATH = os.environ.get("SLICER_PATH", "/opt/orcaslicer/AppRun")
if sys.platform == "darwin":
_DEFAULT_SLICER_PATH = "/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer"
else:
_DEFAULT_SLICER_PATH = "/opt/orcaslicer/AppRun"
SLICER_PATH = os.environ.get("SLICER_PATH", _DEFAULT_SLICER_PATH)
ORCA_HOME = os.environ.get("ORCA_HOME", "/opt/orcaslicer")
# Defaults Profiles (Bambu A1)
MACHINE_PROFILE = os.path.join(ORCA_HOME, "resources/profiles/BBL/machine/Bambu Lab A1 0.4 nozzle.json")
PROCESS_PROFILE = os.path.join(ORCA_HOME, "resources/profiles/BBL/process/0.20mm Standard @BBL A1.json")
FILAMENT_PROFILE = os.path.join(ORCA_HOME, "resources/profiles/BBL/filament/Generic PLA @BBL A1.json")
MACHINE_PROFILE = os.path.join(PROFILES_DIR, "Bambu_Lab_A1_machine.json")
PROCESS_PROFILE = os.path.join(PROFILES_DIR, "Bambu_Process_0.20_Standard.json")
FILAMENT_PROFILE = os.path.join(PROFILES_DIR, "Bambu_PLA_Basic.json")
# Pricing
FILAMENT_COST_PER_KG = float(os.environ.get("FILAMENT_COST_PER_KG", 25.0))

View File

@@ -67,6 +67,7 @@ async def calculate_from_stl(file: UploadFile = File(...)):
try:
# 1. Save Uploaded File
logger.info(f"Received request {req_id} for file: {file.filename}")
with open(input_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)

BIN
backend/obj_1_Base.stl Normal file

Binary file not shown.

View File

@@ -6,13 +6,13 @@ bed_shape = 0x0,256x0,256x256,0x256
nozzle_diameter = 0.4
filament_diameter = 1.75
max_print_speed = 500
travel_speed = 500
travel_speed = 700
gcode_flavor = klipper
# Bambu uses specific gcode but klipper/marlin is close enough for time est if accel matches
machine_max_acceleration_x = 10000
machine_max_acceleration_y = 10000
machine_max_acceleration_e = 5000
machine_max_acceleration_extruding = 5000
machine_max_acceleration_e = 6000
machine_max_acceleration_extruding = 6000
# PRINT SETTINGS
layer_height = 0.2
@@ -26,17 +26,17 @@ bottom_solid_layers = 3
# SPEED SETTINGS (Conservative defaults for A1)
perimeter_speed = 200
external_perimeter_speed = 150
external_perimeter_speed = 200
infill_speed = 250
solid_infill_speed = 200
top_solid_infill_speed = 150
support_material_speed = 150
bridge_speed = 50
bridge_speed = 150
gap_fill_speed = 50
# FILAMENT SETTINGS
filament_density = 1.24
filament_cost = 25.0
filament_cost = 18.0
filament_max_volumetric_speed = 15
temperature = 220
bed_temperature = 60
bed_temperature = 65

View File

@@ -1,21 +0,0 @@
import os
import argparse
def write_structure(root_dir, output_file):
with open(output_file, 'w', encoding='utf-8') as f:
for dirpath, dirnames, filenames in os.walk(root_dir):
relative = os.path.relpath(dirpath, root_dir)
indent_level = 0 if relative == '.' else relative.count(os.sep) + 1
indent = ' ' * (indent_level - 1) if indent_level > 0 else ''
dir_name = os.path.basename(dirpath)
f.write(f"{indent}{dir_name}/\n")
for file in sorted(filenames):
if file.endswith('.java'):
f.write(f"{indent} {file}\n")
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Generate a text file listing .java files with folder structure')
parser.add_argument('root_dir', nargs='?', default='.', help='Directory to scan')
parser.add_argument('-o', '--output', default='java_structure.txt', help='Output text file')
args = parser.parse_args()
write_structure(args.root_dir, args.output)

View File

@@ -23,6 +23,9 @@ class SlicerService:
"""
Runs OrcaSlicer in headless mode to slice the STL file.
"""
if not os.path.exists(settings.SLICER_PATH):
raise RuntimeError(f"Slicer executable not found at: {settings.SLICER_PATH}. Please install OrcaSlicer.")
if not os.path.exists(input_stl_path):
raise FileNotFoundError(f"STL file not found: {input_stl_path}")
@@ -32,6 +35,9 @@ class SlicerService:
# Prepare command
command = self._build_slicer_command(input_stl_path, output_dir, override_path)
logger.info(f"Slicing Command: {' '.join(command)}")
logger.info(f"Using Profiles provided in command settings argument.")
logger.info(f"Starting slicing for {input_stl_path}...")
try:
self._run_command(command)
@@ -39,8 +45,9 @@ class SlicerService:
logger.info("Slicing completed successfully.")
return True
except subprocess.CalledProcessError as e:
logger.error(f"Slicing failed: {e.stderr}")
raise RuntimeError(f"Slicing failed: {e.stderr}")
msg = f"Slicing failed. Return code: {e.returncode}\nSTDOUT:\n{e.stdout}\nSTDERR:\n{e.stderr}"
logger.error(msg)
raise RuntimeError(f"Slicing failed: {e.stderr if e.stderr else e.stdout}")
def _create_override_machine_config(self, output_dir: str) -> str:
"""
@@ -99,13 +106,17 @@ class SlicerService:
]
def _run_command(self, command: list):
subprocess.run(
result = subprocess.run(
command,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True
)
if result.stdout:
logger.info(f"Slicer STDOUT:\n{result.stdout[:2000]}...") # Log first 2000 chars to avoid explosion
if result.stderr:
logger.warning(f"Slicer STDERR:\n{result.stderr}")
def _finalize_output(self, output_dir: str, input_path: str, target_path: str):
"""

View File

@@ -1,7 +0,0 @@
{
"error_string": "There are some incorrect slicing parameters in the 3mf. Please verify the slicing of all plates in Orca Slicer before uploading.",
"export_time": 93825087757208,
"plate_index": 1,
"prepare_time": 0,
"return_code": -51
}