noahlee1234 commited on
Commit
e5a7f86
·
1 Parent(s): 73bbb49

feat: add 1d_path mode and DXF export/download support

Browse files
apps/cad-worker/main.py CHANGED
@@ -61,7 +61,7 @@ image = (
61
  # ---------------------------------------------------------------------------
62
 
63
  _VALID_MODES = {"part", "assembly", "sketch"}
64
- _VALID_OUTPUTS = {"3d_solid", "surface", "2d_vector"}
65
  _MAX_PROMPT_CHARS = int(os.environ.get("NATURALCAD_MAX_PROMPT_CHARS", "1200"))
66
 
67
  _RATE_WINDOW_SECONDS = int(os.environ.get("NATURALCAD_RATE_WINDOW_SECONDS", "60"))
@@ -401,6 +401,11 @@ _OUTPUT_RULES = {
401
  "Extrude with a minimal thickness of 1 mm so the geometry exports as STL/STEP. "
402
  "result must be a Part (bp.part)."
403
  ),
 
 
 
 
 
404
  }
405
 
406
  _MODE_HINTS = {
@@ -646,7 +651,7 @@ def generate_cad(prompt: str, mode: str = "part", output_type: str = "3d_solid")
646
  if log_generated_code:
647
  _log_info(f"Generated code:\n{generated_code}")
648
 
649
- from build123d import export_stl, export_step
650
 
651
  with tempfile.TemporaryDirectory() as tmpdir:
652
  script_path = Path(tmpdir) / "script.py"
@@ -727,13 +732,14 @@ def generate_cad(prompt: str, mode: str = "part", output_type: str = "3d_solid")
727
  }
728
 
729
  # ----------------------------------------------------------------
730
- # Export: STL, STEP, GLB
731
  # ----------------------------------------------------------------
732
  shape = result_shape
733
  urls = {}
734
  stl_path = Path(tmpdir) / "output.stl"
735
  step_path = Path(tmpdir) / "output.step"
736
  glb_path = Path(tmpdir) / "output.glb"
 
737
 
738
  try:
739
  export_stl(shape, str(stl_path))
@@ -765,6 +771,24 @@ def generate_cad(prompt: str, mode: str = "part", output_type: str = "3d_solid")
765
  except Exception as e:
766
  _log_error(f"GLB export failed: {e}")
767
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
768
  # ----------------------------------------------------------------
769
  # Upload to Supabase storage
770
  # ----------------------------------------------------------------
@@ -772,6 +796,8 @@ def generate_cad(prompt: str, mode: str = "part", output_type: str = "3d_solid")
772
  ("stl", stl_path, "model/stl"),
773
  ("step", step_path, "application/octet-stream"),
774
  ]
 
 
775
  if store_glb:
776
  file_pairs.append(("glb", glb_path, "model/gltf-binary"))
777
  for fmt, file_path, content_type in file_pairs:
 
61
  # ---------------------------------------------------------------------------
62
 
63
  _VALID_MODES = {"part", "assembly", "sketch"}
64
+ _VALID_OUTPUTS = {"3d_solid", "surface", "2d_vector", "1d_path"}
65
  _MAX_PROMPT_CHARS = int(os.environ.get("NATURALCAD_MAX_PROMPT_CHARS", "1200"))
66
 
67
  _RATE_WINDOW_SECONDS = int(os.environ.get("NATURALCAD_RATE_WINDOW_SECONDS", "60"))
 
401
  "Extrude with a minimal thickness of 1 mm so the geometry exports as STL/STEP. "
402
  "result must be a Part (bp.part)."
403
  ),
404
+ "1d_path": (
405
+ "Output goal: a 1D path-style layout (linework/centerlines). Build the geometry from lines/arcs on Plane.XY. "
406
+ "For compatibility with STL/STEP preview, give the path a minimal thickness (about 1 mm) by using a thin profile. "
407
+ "result must be a Part (bp.part)."
408
+ ),
409
  }
410
 
411
  _MODE_HINTS = {
 
651
  if log_generated_code:
652
  _log_info(f"Generated code:\n{generated_code}")
653
 
654
+ from build123d import Axis, ExportDXF, Unit, export_step, export_stl
655
 
656
  with tempfile.TemporaryDirectory() as tmpdir:
657
  script_path = Path(tmpdir) / "script.py"
 
732
  }
733
 
734
  # ----------------------------------------------------------------
735
+ # Export: STL, STEP, GLB, DXF
736
  # ----------------------------------------------------------------
737
  shape = result_shape
738
  urls = {}
739
  stl_path = Path(tmpdir) / "output.stl"
740
  step_path = Path(tmpdir) / "output.step"
741
  glb_path = Path(tmpdir) / "output.glb"
742
+ dxf_path = Path(tmpdir) / "output.dxf"
743
 
744
  try:
745
  export_stl(shape, str(stl_path))
 
771
  except Exception as e:
772
  _log_error(f"GLB export failed: {e}")
773
 
774
+ try:
775
+ if output_type in {"2d_vector", "1d_path"}:
776
+ exporter = ExportDXF(unit=Unit.MM)
777
+ if output_type == "1d_path":
778
+ exporter.add_shape(shape.edges())
779
+ else:
780
+ faces = shape.faces()
781
+ if faces:
782
+ top_face = faces.sort_by(Axis.Z)[-1]
783
+ wires = [top_face.outer_wire(), *list(top_face.inner_wires())]
784
+ exporter.add_shape(wires)
785
+ else:
786
+ exporter.add_shape(shape.edges())
787
+ exporter.write(str(dxf_path))
788
+ _log_info(f"DXF exported: {dxf_path.exists()}")
789
+ except Exception as e:
790
+ _log_error(f"DXF export failed: {e}")
791
+
792
  # ----------------------------------------------------------------
793
  # Upload to Supabase storage
794
  # ----------------------------------------------------------------
 
796
  ("stl", stl_path, "model/stl"),
797
  ("step", step_path, "application/octet-stream"),
798
  ]
799
+ if dxf_path.exists():
800
+ file_pairs.append(("dxf", dxf_path, "application/dxf"))
801
  if store_glb:
802
  file_pairs.append(("glb", glb_path, "model/gltf-binary"))
803
  for fmt, file_path, content_type in file_pairs:
apps/gradio-demo/app/main.py CHANGED
@@ -40,6 +40,7 @@ EXAMPLE_PROMPTS = [
40
  ["Industrial notched tower block, 140 mm tall", "part", "3d_solid"],
41
  ["Smooth roof canopy surface, 200 mm span, shallow rise", "part", "surface"],
42
  ["Bracket plate profile with 6 holes for a laser-cut sketch", "sketch", "2d_vector"],
 
43
  ]
44
 
45
  DEFAULT_CODE = '''from build123d import *
@@ -531,7 +532,7 @@ def generate_from_prompt(prompt: str, mode: str, output_type: str):
531
  result = json.loads(response.read().decode())
532
 
533
  if "error" in result:
534
- return None, None, None, f"Error from backend:\n{result['error']}", "Backend generation failed."
535
 
536
  urls = result.get("urls", {})
537
  code = result.get("generated_code", "")
@@ -540,6 +541,7 @@ def generate_from_prompt(prompt: str, mode: str, output_type: str):
540
  glb_url = urls.get("glb")
541
  stl_url = urls.get("stl")
542
  step_url = urls.get("step")
 
543
 
544
  # Download files to artifacts directory (same as local mode)
545
  # so Gradio can serve them properly
@@ -550,6 +552,7 @@ def generate_from_prompt(prompt: str, mode: str, output_type: str):
550
  glb_file = None
551
  stl_file = None
552
  step_file = None
 
553
 
554
  if glb_url:
555
  glb_path = run_dir / f"{run_id}.glb"
@@ -602,6 +605,23 @@ def generate_from_prompt(prompt: str, mode: str, output_type: str):
602
  _log_error(f"STEP download failed: {e}")
603
  step_file = None
604
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
605
  if not glb_file and stl_file:
606
  try:
607
  glb_path = run_dir / f"{run_id}.glb"
@@ -623,32 +643,34 @@ def generate_from_prompt(prompt: str, mode: str, output_type: str):
623
  final_summary = f"Model ready!{' · ' + job_id[:8] if job_id else ''}"
624
  preview_file = glb_file
625
 
626
- return preview_file, stl_file, step_file, combined_logs, final_summary
627
  except error.HTTPError as exc:
628
  body = exc.read().decode() if exc.fp else ""
629
  try:
630
  detail = json.loads(body).get("error", body) if body else str(exc)
631
  except Exception:
632
  detail = body or str(exc)
633
- return None, None, None, f"Backend HTTP {exc.code}: {detail}", "Generation failed."
634
  except error.URLError as exc:
635
  if isinstance(exc.reason, TimeoutError) or "timed out" in str(exc.reason).lower():
636
  return (
637
  None,
638
  None,
639
  None,
 
640
  (
641
  f"Backend timeout after {BACKEND_TIMEOUT_SECONDS:.0f}s. "
642
  "Try a shorter prompt (fewer clauses), or retry."
643
  ),
644
  "Generation timed out.",
645
  )
646
- return None, None, None, f"Backend error: {exc}", "Generation failed."
647
  except TimeoutError:
648
  return (
649
  None,
650
  None,
651
  None,
 
652
  (
653
  f"Backend timeout after {BACKEND_TIMEOUT_SECONDS:.0f}s. "
654
  "Try a shorter prompt (fewer clauses), or retry."
@@ -656,7 +678,7 @@ def generate_from_prompt(prompt: str, mode: str, output_type: str):
656
  "Generation timed out.",
657
  )
658
  except Exception as exc:
659
- return None, None, None, f"Backend error: {exc}", "Generation failed."
660
 
661
  # Fallback to local code stub if backend is missing
662
  spec = {
@@ -667,7 +689,7 @@ def generate_from_prompt(prompt: str, mode: str, output_type: str):
667
  code = render_code_from_spec(spec)
668
  combined_logs = f"Local fallback:\n{code}"
669
  final_summary = "Code generated."
670
- return None, None, None, combined_logs, final_summary
671
 
672
 
673
  def use_example(prompt: str, mode: str, output_type: str):
@@ -693,7 +715,7 @@ def build_ui() -> gr.Blocks:
693
  )
694
  with gr.Row():
695
  mode_picker = gr.Dropdown(choices=["part", "assembly", "sketch"], value="part", label="Mode")
696
- output_picker = gr.Dropdown(choices=["3d_solid", "surface", "2d_vector"], value="3d_solid", label="Output")
697
  generate_btn = gr.Button("Generate Model", variant="primary")
698
  gr.Markdown("### Try one of these")
699
  gr.Examples(
@@ -709,6 +731,7 @@ def build_ui() -> gr.Blocks:
709
  with gr.Row():
710
  stl_download = gr.File(label="Download STL")
711
  step_download = gr.File(label="Download STEP")
 
712
  status_output = gr.Markdown("Ready. Use the mouse to orbit, pan, and zoom the model.")
713
 
714
  log_output = gr.Textbox(
@@ -722,7 +745,7 @@ def build_ui() -> gr.Blocks:
722
  generate_btn.click(
723
  fn=generate_from_prompt,
724
  inputs=[prompt_input, mode_picker, output_picker],
725
- outputs=[model_viewer, stl_download, step_download, log_output, status_output],
726
  )
727
 
728
  return demo
 
40
  ["Industrial notched tower block, 140 mm tall", "part", "3d_solid"],
41
  ["Smooth roof canopy surface, 200 mm span, shallow rise", "part", "surface"],
42
  ["Bracket plate profile with 6 holes for a laser-cut sketch", "sketch", "2d_vector"],
43
+ ["Single-line floor route centerline with 3 jogs, 120 mm long", "sketch", "1d_path"],
44
  ]
45
 
46
  DEFAULT_CODE = '''from build123d import *
 
532
  result = json.loads(response.read().decode())
533
 
534
  if "error" in result:
535
+ return None, None, None, None, f"Error from backend:\n{result['error']}", "Backend generation failed."
536
 
537
  urls = result.get("urls", {})
538
  code = result.get("generated_code", "")
 
541
  glb_url = urls.get("glb")
542
  stl_url = urls.get("stl")
543
  step_url = urls.get("step")
544
+ dxf_url = urls.get("dxf")
545
 
546
  # Download files to artifacts directory (same as local mode)
547
  # so Gradio can serve them properly
 
552
  glb_file = None
553
  stl_file = None
554
  step_file = None
555
+ dxf_file = None
556
 
557
  if glb_url:
558
  glb_path = run_dir / f"{run_id}.glb"
 
605
  _log_error(f"STEP download failed: {e}")
606
  step_file = None
607
 
608
+ if dxf_url:
609
+ dxf_path = run_dir / f"{run_id}.dxf"
610
+ _log_info(f"Downloading DXF from {dxf_url}")
611
+ try:
612
+ with request.urlopen(dxf_url) as r:
613
+ data = r.read()
614
+ _log_info(f"Downloaded DXF bytes: {len(data)}")
615
+ if len(data) < 100:
616
+ _log_error("DXF file too small")
617
+ with open(dxf_path, "wb") as f:
618
+ f.write(data)
619
+ dxf_file = str(dxf_path)
620
+ _log_info(f"DXF saved to {dxf_file}, size {os.path.getsize(dxf_file)}")
621
+ except Exception as e:
622
+ _log_error(f"DXF download failed: {e}")
623
+ dxf_file = None
624
+
625
  if not glb_file and stl_file:
626
  try:
627
  glb_path = run_dir / f"{run_id}.glb"
 
643
  final_summary = f"Model ready!{' · ' + job_id[:8] if job_id else ''}"
644
  preview_file = glb_file
645
 
646
+ return preview_file, stl_file, step_file, dxf_file, combined_logs, final_summary
647
  except error.HTTPError as exc:
648
  body = exc.read().decode() if exc.fp else ""
649
  try:
650
  detail = json.loads(body).get("error", body) if body else str(exc)
651
  except Exception:
652
  detail = body or str(exc)
653
+ return None, None, None, None, f"Backend HTTP {exc.code}: {detail}", "Generation failed."
654
  except error.URLError as exc:
655
  if isinstance(exc.reason, TimeoutError) or "timed out" in str(exc.reason).lower():
656
  return (
657
  None,
658
  None,
659
  None,
660
+ None,
661
  (
662
  f"Backend timeout after {BACKEND_TIMEOUT_SECONDS:.0f}s. "
663
  "Try a shorter prompt (fewer clauses), or retry."
664
  ),
665
  "Generation timed out.",
666
  )
667
+ return None, None, None, None, f"Backend error: {exc}", "Generation failed."
668
  except TimeoutError:
669
  return (
670
  None,
671
  None,
672
  None,
673
+ None,
674
  (
675
  f"Backend timeout after {BACKEND_TIMEOUT_SECONDS:.0f}s. "
676
  "Try a shorter prompt (fewer clauses), or retry."
 
678
  "Generation timed out.",
679
  )
680
  except Exception as exc:
681
+ return None, None, None, None, f"Backend error: {exc}", "Generation failed."
682
 
683
  # Fallback to local code stub if backend is missing
684
  spec = {
 
689
  code = render_code_from_spec(spec)
690
  combined_logs = f"Local fallback:\n{code}"
691
  final_summary = "Code generated."
692
+ return None, None, None, None, combined_logs, final_summary
693
 
694
 
695
  def use_example(prompt: str, mode: str, output_type: str):
 
715
  )
716
  with gr.Row():
717
  mode_picker = gr.Dropdown(choices=["part", "assembly", "sketch"], value="part", label="Mode")
718
+ output_picker = gr.Dropdown(choices=["3d_solid", "surface", "2d_vector", "1d_path"], value="3d_solid", label="Output")
719
  generate_btn = gr.Button("Generate Model", variant="primary")
720
  gr.Markdown("### Try one of these")
721
  gr.Examples(
 
731
  with gr.Row():
732
  stl_download = gr.File(label="Download STL")
733
  step_download = gr.File(label="Download STEP")
734
+ dxf_download = gr.File(label="Download DXF")
735
  status_output = gr.Markdown("Ready. Use the mouse to orbit, pan, and zoom the model.")
736
 
737
  log_output = gr.Textbox(
 
745
  generate_btn.click(
746
  fn=generate_from_prompt,
747
  inputs=[prompt_input, mode_picker, output_picker],
748
+ outputs=[model_viewer, stl_download, step_download, dxf_download, log_output, status_output],
749
  )
750
 
751
  return demo