feat(web + backend): advanced and simple quote.
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
BIN
backend/obj_1_Base.stl
Normal file
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user