YSMlearnsCode commited on
Commit
dddf9a7
·
1 Parent(s): 7600206

changed gradio to make py scripts

Browse files
Files changed (5) hide show
  1. .github/workflows/ci.yml +1 -1
  2. app.py +124 -62
  3. app/app_prompt.txt +0 -10
  4. app/process.py +34 -129
  5. generated/result_script.py +21 -27
.github/workflows/ci.yml CHANGED
@@ -15,7 +15,7 @@ jobs:
15
  fetch-depth: 0
16
  lfs: true
17
 
18
- - name: Push to Hugging Face Space
19
  env:
20
  HF_TOKEN: ${{ secrets.HF_TOKEN }}
21
  run: |
 
15
  fetch-depth: 0
16
  lfs: true
17
 
18
+ - name: Push to Hugggit remote listing Face Space
19
  env:
20
  HF_TOKEN: ${{ secrets.HF_TOKEN }}
21
  run: |
app.py CHANGED
@@ -1,83 +1,145 @@
1
  import gradio as gr
2
  from pathlib import Path
3
- from app.process import generate_script_and_run # Correct import from nested app directory
4
-
5
- # === File Paths ===
6
- generated_dir = Path("app/generated")
7
- fcstd_file = generated_dir / "model.FCStd"
8
- obj_file = generated_dir / "model.obj"
9
-
10
- # Remove stale files (optional cleanup)
11
- for file in ["generated_model.FCStd", "generated_model.obj"]:
12
- fpath = generated_dir / file
13
- if fpath.exists():
14
- fpath.unlink()
15
-
16
- # === CAD Generation Callback ===
17
- def prepare_outputs(description):
18
- generate_script_and_run(description)
19
- return str(fcstd_file), str(obj_file), str(obj_file)
20
-
21
- # === UI with Custom CSS ===
22
- with gr.Blocks(css="""
23
- #generate-btn .gr-button {
24
- background-color: #28a745 !important;
25
- color: white !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  }
27
-
28
- #fcstd-download .gr-button,
29
- #obj-download .gr-button {
30
- background-color: #fd7e14 !important;
31
- color: white !important;
 
32
  }
33
-
34
- .footer-text {
35
- text-align: center;
36
- font-size: 0.85rem;
37
- margin-top: 2em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  color: #888;
 
 
39
  }
 
 
 
40
 
41
- .footer-text a {
42
- color: #fd7e14;
43
- text-decoration: none;
44
- }
45
 
46
- .footer-text a:hover {
47
- text-decoration: underline;
48
- }
49
- """) as demo:
50
 
51
- gr.Markdown("<h1 style='text-align: center;'>CADomatic - FreeCAD Script Generator</h1>")
52
- gr.Markdown("Generate 3D models by describing them in plain English. Powered by FreeCAD and LLMs.")
 
 
53
 
54
- input_text = gr.Textbox(
55
- label="📝 Describe your FreeCAD part",
 
 
 
 
56
  lines=3,
57
- placeholder="e.g., Create a 10mm thick cylinder with radius 5mm..."
58
  )
59
 
60
- generate_btn = gr.Button("Generate", elem_id="generate-btn")
61
-
62
- model_preview = gr.Model3D(label="🔍 3D Preview", height=400)
63
 
64
  with gr.Row():
65
- fcstd_download = gr.DownloadButton("Download .FCStd file", elem_id="fcstd-download")
66
- obj_download = gr.DownloadButton("Download .obj file", elem_id="obj-download")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
  generate_btn.click(
69
- fn=prepare_outputs,
70
- inputs=input_text,
71
- outputs=[fcstd_download, obj_download, model_preview]
72
  )
73
 
74
- gr.HTML("""
75
- <div class='footer-text'>
76
- <strong>Note:</strong> CADomatic is still under development and needs refinement. For best results, run locally. Toggle view in the .FCStd file for full geometry.<br>
77
- Please refresh and run if there is no preview.<br>
78
- View the source on <a href="https://github.com/yas1nsyed/CADomatic" target="_blank">GitHub</a>.
79
- </div>
80
- """)
81
-
82
  if __name__ == "__main__":
83
  demo.launch()
 
1
  import gradio as gr
2
  from pathlib import Path
3
+ import sys
4
+
5
+ # Paths
6
+ PROJECT_ROOT = Path(__file__).resolve().parent
7
+ GENERATED_SCRIPT_PATH = PROJECT_ROOT / "generated" / "result_script.py"
8
+
9
+ # Add root to sys.path so we can import process.py from app/
10
+ sys.path.insert(0, str(PROJECT_ROOT / "app"))
11
+
12
+ from app.process import main as generate_from_llm # This runs generation
13
+
14
+ def generate_script_and_preview(description):
15
+ """
16
+ Generates the FreeCAD script using process.py logic and returns:
17
+ - The script text for preview
18
+ - The file path for download
19
+ """
20
+ import builtins
21
+ original_input = builtins.input
22
+ builtins.input = lambda _: description
23
+ try:
24
+ generate_from_llm()
25
+ finally:
26
+ builtins.input = original_input
27
+
28
+ if GENERATED_SCRIPT_PATH.exists():
29
+ script_text = GENERATED_SCRIPT_PATH.read_text(encoding="utf-8")
30
+ return script_text, str(GENERATED_SCRIPT_PATH)
31
+ else:
32
+ return "Error: Script was not generated.", None
33
+
34
+
35
+ css = """
36
+ body { background-color: #202020; color: white; margin: 0; padding: 0; }
37
+ .gradio-container {
38
+ max-width: 1400px;
39
+ width: 95vw;
40
+ margin: auto;
41
  }
42
+ .title { text-align: center; font-size: 2.5em; margin-bottom: 0.1em; }
43
+ .description { text-align: center; font-size: 1.1em; margin-bottom: 1em; color: #ccc; }
44
+ .preview-box {
45
+ height: 400px; overflow-y: auto;
46
+ background-color: #111; border: 1px solid #444; padding: 10px;
47
+ font-family: monospace; white-space: pre-wrap; color: #0f0;
48
  }
49
+ .download-container {
50
+ display: flex;
51
+ flex-direction: column;
52
+ align-items: flex-start;
53
+ gap: 0.5em;
54
+ padding-left: 15px;
55
+ height: 400px;
56
+ justify-content: flex-start;
57
+ width: 300px;
58
+ }
59
+ .download-button { width: 100%; }
60
+ .instructions {
61
+ font-size: 0.9em; color: #aaa;
62
+ max-width: 300px;
63
+ white-space: pre-line;
64
+ }
65
+ .footer {
66
+ margin-top: 2em;
67
+ text-align: center;
68
+ font-size: 0.9em;
69
  color: #888;
70
+ border-top: 1px solid #444;
71
+ padding-top: 1em;
72
  }
73
+ .footer a { color: #6af; text-decoration: none; }
74
+ .footer a:hover { text-decoration: underline; }
75
+ """
76
 
77
+ # Description
78
+ cadomatic_description_md = """
79
+ <div style="text-align: center;">
 
80
 
81
+ Seamlessly creating python scripts for FreeCAD — from prompt to model.
 
 
 
82
 
83
+ CADomatic is a Python-powered tool that transforms prompts into **editable** parametric CAD scripts for FreeCAD. Rather than static models, it generates fully customizable Python code that programmatically builds CAD geometry — enabling engineers to define parts, reuse templates, and iterate rapidly.<br>
84
+ CADomatic primarily aims at **reducing product development time** by making a base design which can be modified to suit the designer's main goal.<br>
85
+ CADomatic creates native FreeCAD Python scripts for simple parts **with a complete design tree**.<br>
86
+ """
87
 
88
+ with gr.Blocks(css=css) as demo:
89
+ gr.Markdown("<div class='title'>CADomatic - AI powered CAD design generator</div>") # Title
90
+ gr.Markdown(cadomatic_description_md)
91
+
92
+ description_input = gr.Textbox(
93
+ label="Describe your desired CAD model below-",
94
  lines=3,
95
+ placeholder="e.g., Create a flange with OD 100mm, bore size 50mm and 6 m8 holes at PCD 75mm..."
96
  )
97
 
98
+ generate_btn = gr.Button("Generate Script", variant="primary")
 
 
99
 
100
  with gr.Row():
101
+ with gr.Column(scale=1):
102
+ preview_output = gr.Code(
103
+ label="Generated Script Preview",
104
+ language="python",
105
+ elem_classes="preview-box"
106
+ )
107
+ download_btn = gr.DownloadButton(
108
+ label="Download Python Script",
109
+ elem_classes="download-button"
110
+ )
111
+ with gr.Column(scale=1):
112
+ gr.Markdown(
113
+ """
114
+ <div class='instructions'>
115
+ <b>Instructions:</b><br>
116
+ - Enter the description for your desired CAD part.<br>
117
+ - Click on "Generate Script".<br>
118
+ - Preview the generated Python code.<br>
119
+ - Paste the generated code into the python console of your FreeCAD app.<br>
120
+ - (or)<br>
121
+ - Download the script.<br>
122
+ - In your FreeCAD python console, paste - exec(open(r"path to your python script").read())
123
+ </div>
124
+ """
125
+ )
126
+
127
+ # Footer
128
+ gr.Markdown(
129
+ """
130
+ <div class='footer'>
131
+ CADomatic is still under development and may sometimes produce inaccurate results.<br>
132
+ <br>
133
+ Made with ❤️ by Yasin
134
+ </div>
135
+ """
136
+ )
137
 
138
  generate_btn.click(
139
+ fn=generate_script_and_preview,
140
+ inputs=description_input,
141
+ outputs=[preview_output, download_btn]
142
  )
143
 
 
 
 
 
 
 
 
 
144
  if __name__ == "__main__":
145
  demo.launch()
app/app_prompt.txt DELETED
@@ -1,10 +0,0 @@
1
- - Generate FreeCAD 1.0 Python code to create:
2
- - Requirements:
3
- - Must work in FreeCAD command-line mode
4
- - No GUI functions (FreeCADGui)
5
- - Ensure valid geometry creation
6
- - DO NOT wrap the logic inside any `def` functions or `if __name__ == "__main__":` blocks.
7
- - Always include `App.newDocument(...)` to create a document.
8
- - Always use top-level, immediately executable statements.
9
- - Use the `Part` module to construct valid geometry.
10
- - Recompute the document after adding objects using `doc.recompute()`.
 
 
 
 
 
 
 
 
 
 
 
app/process.py CHANGED
@@ -1,142 +1,47 @@
1
- import sys
2
- import os
3
- import subprocess
4
- import platform
5
  from pathlib import Path
 
 
6
 
7
- # Define paths
8
- APP_DIR = Path(__file__).parent.resolve()
9
- PROJECT_ROOT = APP_DIR.parent
10
- GEN_DIR = APP_DIR / "generated"
11
- GEN_SCRIPT = GEN_DIR / "result_script.py"
12
- OBJ_PATH = GEN_DIR / "model.obj"
13
- FCSTD_PATH = GEN_DIR / "model.FCStd"
14
- PREVIEW_PATH = GEN_DIR / "preview.txt" # Dummy preview
15
-
16
- sys.path.append(str(PROJECT_ROOT))
17
 
18
  from src.llm_client import prompt_llm
19
 
20
- def get_freecad_cmd():
21
- """Get FreeCAD command path for Windows/Linux"""
22
- if platform.system() == "Windows":
23
- for path in [
24
- r"C:\Program Files\FreeCAD 1.0\bin\freecadcmd.exe",
25
- r"C:\Program Files\FreeCAD\bin\freecadcmd.exe"
26
- ]:
27
- if os.path.exists(path):
28
- return path
29
- return "freecadcmd" # Assume it's in PATH
30
-
31
- # Escape Windows paths manually outside the f-string
32
- fcstd_path_str = str(FCSTD_PATH).replace("\\", "\\\\")
33
- obj_path_str = str(OBJ_PATH).replace("\\", "\\\\")
34
- preview_path_str = str(PREVIEW_PATH).replace("\\", "\\\\")
35
-
36
- EXPORT_SNIPPET = f"""
37
- import Mesh
38
- import os
39
-
40
- print(">>> Running export snippet...")
41
-
42
- try:
43
- if App.ActiveDocument:
44
- print(">>> Active document found")
45
- doc = App.ActiveDocument
46
- doc.recompute()
47
- doc.saveAs(r"{fcstd_path_str}")
48
- print(">>> Document saved")
49
-
50
- objs = []
51
- for obj in doc.Objects:
52
- if hasattr(obj, "Shape"):
53
- objs.append(obj)
54
-
55
- print(f">>> Found {{len(objs)}} shape object(s)")
56
-
57
- if objs:
58
- Mesh.export(objs, r"{obj_path_str}")
59
- print(">>> Exported OBJ file")
60
-
61
- with open(r"{preview_path_str}", "w") as f:
62
- f.write("Preview placeholder")
63
-
64
- else:
65
- print(">>> No active document!")
66
-
67
- except Exception as e:
68
- App.Console.PrintError("Export failed: " + str(e) + "\\n")
69
  """
70
 
71
- def generate_script_and_run(user_input: str):
72
- # Load modular prompt parts
73
- base_prompt_path = PROJECT_ROOT / "prompts/base_instruction.txt"
74
- example_prompt_path = PROJECT_ROOT / "prompts/example_code.txt"
75
- app_prompt_path = APP_DIR / "app_prompt.txt"
76
-
77
- base_instruction = base_prompt_path.read_text(encoding="utf-8").strip()
78
- example_code = example_prompt_path.read_text(encoding="utf-8").strip()
79
- app_prompt = app_prompt_path.read_text(encoding="utf-8").strip()
80
 
81
- prompt = (
82
- f"{base_instruction}\n\n"
83
- f"{example_code}\n\n"
84
- f"{app_prompt}\n\n"
85
- f"User request: {user_input.strip()}"
86
- )
87
 
88
- generated_code = prompt_llm(prompt)
89
-
90
- if "App.newDocument" not in generated_code:
91
- generated_code = "App.newDocument('Unnamed')\n" + generated_code
92
 
 
93
  if generated_code.startswith("```"):
94
- generated_code = generated_code[generated_code.find("\n") + 1:].rsplit("```", 1)[0]
95
-
96
- if "__name__" in generated_code and "def " in generated_code:
97
- lines = generated_code.splitlines()
98
- in_main = False
99
- unwrapped = []
100
- for line in lines:
101
- if line.strip().startswith("if __name__"):
102
- in_main = True
103
- continue
104
- if in_main:
105
- unwrapped.append(line[4:] if line.startswith(" ") else line)
106
- else:
107
- unwrapped.append(line)
108
- generated_code = "\n".join(unwrapped)
109
-
110
- GEN_DIR.mkdir(exist_ok=True)
111
-
112
- full_script = f"{generated_code.strip()}\n\n{EXPORT_SNIPPET}"
113
- GEN_SCRIPT.write_text(full_script, encoding="utf-8")
114
-
115
- for path in [FCSTD_PATH, OBJ_PATH, PREVIEW_PATH]:
116
- if path.exists():
117
- path.unlink()
118
-
119
- freecad_cmd = get_freecad_cmd()
120
- try:
121
- result = subprocess.run(
122
- [freecad_cmd, str(GEN_SCRIPT)],
123
- cwd=APP_DIR,
124
- capture_output=True,
125
- text=True,
126
- timeout=60
127
- )
128
-
129
- (GEN_DIR / "run_stdout.txt").write_text(result.stdout or "", encoding="utf-8")
130
- (GEN_DIR / "run_stderr.txt").write_text(result.stderr or "", encoding="utf-8")
131
-
132
- if result.returncode != 0:
133
- raise RuntimeError(result.stderr or result.stdout)
134
-
135
- if not FCSTD_PATH.exists() or not OBJ_PATH.exists():
136
- raise FileNotFoundError("One or more output files not created.")
137
 
138
- except Exception as e:
139
- FCSTD_PATH.write_text(f"Error: {e}")
140
- PREVIEW_PATH.write_text(f"Error: {e}")
141
 
142
- return str(FCSTD_PATH), str(PREVIEW_PATH)
 
 
 
 
 
 
 
1
  from pathlib import Path
2
+ import subprocess
3
+ import sys
4
 
5
+ # Make sure we can import from root/src
6
+ ROOT_DIR = Path(__file__).resolve().parent.parent
7
+ sys.path.insert(0, str(ROOT_DIR))
 
 
 
 
 
 
 
8
 
9
  from src.llm_client import prompt_llm
10
 
11
+ # File paths (relative to project root)
12
+ prompt_base = ROOT_DIR / "prompts" / "base_instruction.txt"
13
+ prompt_examples = ROOT_DIR / "prompts" / "example_code.txt"
14
+ GEN_SCRIPT = ROOT_DIR / "generated" / "result_script.py"
15
+ RUN_SCRIPT = ROOT_DIR / "src" / "run_freecad.py"
16
+
17
+ # Snippet to adjust FreeCAD GUI view
18
+ GUI_SNIPPET = """
19
+ import FreeCADGui
20
+ FreeCADGui.activeDocument().activeView().viewAxometric()
21
+ FreeCADGui.SendMsgToActiveView("ViewFit")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  """
23
 
24
+ def main():
25
+ # Step 1: Get user input
26
+ user_input = input("Describe your FreeCAD part: ")
 
 
 
 
 
 
27
 
28
+ # Step 2: Build prompt
29
+ base_prompt = prompt_base.read_text(encoding="utf-8").strip()
30
+ example_prompt = prompt_examples.read_text(encoding="utf-8").strip()
31
+ full_prompt = f"{base_prompt}\n\nExamples:\n{example_prompt}\n\nUser instruction: {user_input.strip()}"
 
 
32
 
33
+ # Step 3: Get response from LLM
34
+ generated_code = prompt_llm(full_prompt)
 
 
35
 
36
+ # Step 4: Clean up ```python code blocks if any
37
  if generated_code.startswith("```"):
38
+ generated_code = generated_code.strip("`\n ")
39
+ if generated_code.lower().startswith("python"):
40
+ generated_code = generated_code[len("python"):].lstrip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
+ # Step 5: Append GUI snippet for viewing
43
+ generated_code += "\n\n" + GUI_SNIPPET
 
44
 
45
+ # Step 6: Save to script file
46
+ GEN_SCRIPT.write_text(generated_code, encoding="utf-8")
47
+ print(f"\n Code generated and written to {GEN_SCRIPT}")
generated/result_script.py CHANGED
@@ -7,53 +7,47 @@ import math
7
  def createFlangeAssembly():
8
  doc = App.newDocument("Flange")
9
 
10
- # === Parameters ===
11
  FLANGE_OUTER_DIAMETER = 100.0
12
  FLANGE_THICKNESS = 7.5
13
  BORE_INNER_DIAMETER = 50.0
14
  NECK_HEIGHT = 15.0
15
- NECK_OUTER_DIAMETER = 60.0 # Keeping this from the template as not specified in prompt
16
  NUM_BOLT_HOLES = 6
17
  BOLT_HOLE_DIAMETER = 12.0
18
  PCD = 75.0
19
 
20
  total_height = FLANGE_THICKNESS + NECK_HEIGHT
21
 
22
- # === 1. Create flange base ===
23
- flange = doc.addObject("Part::Cylinder", "Flange_Base")
24
  flange.Radius = FLANGE_OUTER_DIAMETER / 2
25
  flange.Height = FLANGE_THICKNESS
26
 
27
- # === 2. Cut central bore from flange ===
28
- bore = doc.addObject("Part::Cylinder", "Central_Bore_Cutter")
29
  bore.Radius = BORE_INNER_DIAMETER / 2
30
  bore.Height = FLANGE_THICKNESS
31
- bore_cut = doc.addObject("Part::Cut", "Flange_with_Bore")
32
  bore_cut.Base = flange
33
  bore_cut.Tool = bore
34
 
35
- # === 3. Create neck ===
36
- neck_outer = doc.addObject("Part::Cylinder", "Neck_Outer")
37
  neck_outer.Radius = NECK_OUTER_DIAMETER / 2
38
  neck_outer.Height = NECK_HEIGHT
39
  neck_outer.Placement.Base = Vector(0, 0, FLANGE_THICKNESS)
40
 
41
- neck_inner = doc.addObject("Part::Cylinder", "Neck_Inner_Cutter")
42
  neck_inner.Radius = BORE_INNER_DIAMETER / 2
43
  neck_inner.Height = NECK_HEIGHT
44
  neck_inner.Placement.Base = Vector(0, 0, FLANGE_THICKNESS)
45
 
46
- neck_hollow = doc.addObject("Part::Cut", "Hollow_Neck_Part")
47
  neck_hollow.Base = neck_outer
48
  neck_hollow.Tool = neck_inner
49
 
50
- # === 4. Fuse flange (with central hole) and neck ===
51
- fused = doc.addObject("Part::Fuse", "Flange_and_Neck_Fused")
52
  fused.Base = bore_cut
53
  fused.Tool = neck_hollow
54
 
55
- # === 5. Cut bolt holes sequentially ===
56
- current_shape_obj = fused # Reference to the last cut object in the tree
57
  bolt_radius = BOLT_HOLE_DIAMETER / 2
58
  bolt_circle_radius = PCD / 2
59
 
@@ -63,20 +57,16 @@ def createFlangeAssembly():
63
  x = bolt_circle_radius * math.cos(angle_rad)
64
  y = bolt_circle_radius * math.sin(angle_rad)
65
 
66
- hole_cutter = doc.addObject("Part::Cylinder", f"Bolt_Hole_Cutter_{i+1:02d}")
67
- hole_cutter.Radius = bolt_radius
68
- hole_cutter.Height = total_height
69
- hole_cutter.Placement.Base = Vector(x, y, 0)
70
 
71
- cut_obj = doc.addObject("Part::Cut", f"Flange_with_Hole_{i+1:02d}")
72
- cut_obj.Base = current_shape_obj
73
- cut_obj.Tool = hole_cutter
74
- current_shape_obj = cut_obj # Update for the next iteration
75
 
76
- # === 6. Final result ===
77
- # The final object is current_shape_obj after all cuts
78
-
79
- # Recompute and fit view
80
  doc.recompute()
81
  Gui.activeDocument().activeView().viewAxometric()
82
  Gui.SendMsgToActiveView("ViewFit")
@@ -86,3 +76,7 @@ def createFlangeAssembly():
86
  if __name__ == "__main__":
87
  createFlangeAssembly()
88
 
 
 
 
 
 
7
  def createFlangeAssembly():
8
  doc = App.newDocument("Flange")
9
 
 
10
  FLANGE_OUTER_DIAMETER = 100.0
11
  FLANGE_THICKNESS = 7.5
12
  BORE_INNER_DIAMETER = 50.0
13
  NECK_HEIGHT = 15.0
14
+ NECK_OUTER_DIAMETER = 60.0
15
  NUM_BOLT_HOLES = 6
16
  BOLT_HOLE_DIAMETER = 12.0
17
  PCD = 75.0
18
 
19
  total_height = FLANGE_THICKNESS + NECK_HEIGHT
20
 
21
+ flange = doc.addObject("Part::Cylinder", "Flange")
 
22
  flange.Radius = FLANGE_OUTER_DIAMETER / 2
23
  flange.Height = FLANGE_THICKNESS
24
 
25
+ bore = doc.addObject("Part::Cylinder", "CentralBore")
 
26
  bore.Radius = BORE_INNER_DIAMETER / 2
27
  bore.Height = FLANGE_THICKNESS
28
+ bore_cut = doc.addObject("Part::Cut", "FlangeWithBore")
29
  bore_cut.Base = flange
30
  bore_cut.Tool = bore
31
 
32
+ neck_outer = doc.addObject("Part::Cylinder", "NeckOuter")
 
33
  neck_outer.Radius = NECK_OUTER_DIAMETER / 2
34
  neck_outer.Height = NECK_HEIGHT
35
  neck_outer.Placement.Base = Vector(0, 0, FLANGE_THICKNESS)
36
 
37
+ neck_inner = doc.addObject("Part::Cylinder", "NeckInner")
38
  neck_inner.Radius = BORE_INNER_DIAMETER / 2
39
  neck_inner.Height = NECK_HEIGHT
40
  neck_inner.Placement.Base = Vector(0, 0, FLANGE_THICKNESS)
41
 
42
+ neck_hollow = doc.addObject("Part::Cut", "HollowNeck")
43
  neck_hollow.Base = neck_outer
44
  neck_hollow.Tool = neck_inner
45
 
46
+ fused = doc.addObject("Part::Fuse", "FlangeAndNeck")
 
47
  fused.Base = bore_cut
48
  fused.Tool = neck_hollow
49
 
50
+ current_shape = fused
 
51
  bolt_radius = BOLT_HOLE_DIAMETER / 2
52
  bolt_circle_radius = PCD / 2
53
 
 
57
  x = bolt_circle_radius * math.cos(angle_rad)
58
  y = bolt_circle_radius * math.sin(angle_rad)
59
 
60
+ hole = doc.addObject("Part::Cylinder", f"BoltHole_{i+1:02d}")
61
+ hole.Radius = bolt_radius
62
+ hole.Height = total_height
63
+ hole.Placement.Base = Vector(x, y, 0)
64
 
65
+ cut = doc.addObject("Part::Cut", f"Cut_Bolt_{i+1:02d}")
66
+ cut.Base = current_shape
67
+ cut.Tool = hole
68
+ current_shape = cut
69
 
 
 
 
 
70
  doc.recompute()
71
  Gui.activeDocument().activeView().viewAxometric()
72
  Gui.SendMsgToActiveView("ViewFit")
 
76
  if __name__ == "__main__":
77
  createFlangeAssembly()
78
 
79
+
80
+ import FreeCADGui
81
+ FreeCADGui.activeDocument().activeView().viewAxometric()
82
+ FreeCADGui.SendMsgToActiveView("ViewFit")