YSMlearnsCode
commited on
Commit
·
3c38be2
1
Parent(s):
6e7d971
added chat feature
Browse files- app.py +32 -10
- app/process.py +3 -3
- generated/result_script.py +82 -17
- main.py +3 -3
- prompts/example_code.txt +0 -565
- src/llm_client.py +50 -33
app.py
CHANGED
@@ -10,13 +10,27 @@ GENERATED_SCRIPT_PATH = PROJECT_ROOT / "generated" / "result_script.py"
|
|
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
|
15 |
"""
|
16 |
-
Generates
|
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
|
@@ -79,7 +93,6 @@ css = """
|
|
79 |
"""
|
80 |
|
81 |
# Description
|
82 |
-
# ToDo write as features
|
83 |
cadomatic_description_md = """
|
84 |
<div style="text-align: center;">
|
85 |
|
@@ -97,12 +110,14 @@ with gr.Blocks(css=css) as demo:
|
|
97 |
gr.Markdown(cadomatic_description_md)
|
98 |
|
99 |
description_input = gr.Textbox(
|
100 |
-
label="Describe your desired CAD model below
|
101 |
lines=2,
|
102 |
placeholder="e.g., Create a flange with OD 100mm, bore size 50mm and 6 m8 holes at PCD 75mm..."
|
103 |
)
|
104 |
|
105 |
-
|
|
|
|
|
106 |
|
107 |
with gr.Row():
|
108 |
with gr.Column(scale=1):
|
@@ -116,12 +131,13 @@ with gr.Blocks(css=css) as demo:
|
|
116 |
elem_classes="download-button"
|
117 |
)
|
118 |
with gr.Column(scale=1):
|
119 |
-
gr.Markdown(
|
120 |
"""
|
121 |
<div class='instructions'>
|
122 |
<b>Instructions:</b><br>
|
123 |
-
- Enter the description for your desired CAD part.<br>
|
124 |
-
- Click on "Generate
|
|
|
125 |
- Preview the generated Python code.<br>
|
126 |
- Paste the generated code into the python console of your FreeCAD app.<br>
|
127 |
- (or)<br>
|
@@ -142,8 +158,14 @@ with gr.Blocks(css=css) as demo:
|
|
142 |
"""
|
143 |
)
|
144 |
|
145 |
-
|
146 |
-
fn=
|
|
|
|
|
|
|
|
|
|
|
|
|
147 |
inputs=description_input,
|
148 |
outputs=[preview_output, download_btn]
|
149 |
)
|
|
|
10 |
sys.path.insert(0, str(PROJECT_ROOT / "app"))
|
11 |
|
12 |
from app.process import main as generate_from_llm # This runs generation
|
13 |
+
from src.llm_client import reset_memory # ✅ Import reset function for new part
|
14 |
|
15 |
+
def generate_new_part(description):
|
16 |
"""
|
17 |
+
Generates a completely new FreeCAD script (resets memory) and returns:
|
18 |
- The script text for preview
|
19 |
- The file path for download
|
20 |
"""
|
21 |
+
reset_memory() # ✅ Start fresh for a new part
|
22 |
+
return _generate_script(description)
|
23 |
+
|
24 |
+
def edit_existing_part(description):
|
25 |
+
"""
|
26 |
+
Edits the existing FreeCAD script using conversation context and returns:
|
27 |
+
- The updated script text for preview
|
28 |
+
- The file path for download
|
29 |
+
"""
|
30 |
+
return _generate_script(description)
|
31 |
+
|
32 |
+
def _generate_script(description):
|
33 |
+
"""Helper to call process.py with given description."""
|
34 |
import builtins
|
35 |
original_input = builtins.input
|
36 |
builtins.input = lambda _: description
|
|
|
93 |
"""
|
94 |
|
95 |
# Description
|
|
|
96 |
cadomatic_description_md = """
|
97 |
<div style="text-align: center;">
|
98 |
|
|
|
110 |
gr.Markdown(cadomatic_description_md)
|
111 |
|
112 |
description_input = gr.Textbox(
|
113 |
+
label="Describe your desired CAD model or modification below:",
|
114 |
lines=2,
|
115 |
placeholder="e.g., Create a flange with OD 100mm, bore size 50mm and 6 m8 holes at PCD 75mm..."
|
116 |
)
|
117 |
|
118 |
+
with gr.Row():
|
119 |
+
generate_new_btn = gr.Button("Generate New Part", variant="primary")
|
120 |
+
edit_existing_btn = gr.Button("Edit Existing Part", variant="secondary")
|
121 |
|
122 |
with gr.Row():
|
123 |
with gr.Column(scale=1):
|
|
|
131 |
elem_classes="download-button"
|
132 |
)
|
133 |
with gr.Column(scale=1):
|
134 |
+
gr.Markdown(
|
135 |
"""
|
136 |
<div class='instructions'>
|
137 |
<b>Instructions:</b><br>
|
138 |
+
- Enter the description for your desired CAD part or an edit instruction.<br>
|
139 |
+
- Click on "Generate New Part" to start fresh.<br>
|
140 |
+
- Click on "Edit Existing Part" to modify the last script.<br>
|
141 |
- Preview the generated Python code.<br>
|
142 |
- Paste the generated code into the python console of your FreeCAD app.<br>
|
143 |
- (or)<br>
|
|
|
158 |
"""
|
159 |
)
|
160 |
|
161 |
+
generate_new_btn.click(
|
162 |
+
fn=generate_new_part,
|
163 |
+
inputs=description_input,
|
164 |
+
outputs=[preview_output, download_btn]
|
165 |
+
)
|
166 |
+
|
167 |
+
edit_existing_btn.click(
|
168 |
+
fn=edit_existing_part,
|
169 |
inputs=description_input,
|
170 |
outputs=[preview_output, download_btn]
|
171 |
)
|
app/process.py
CHANGED
@@ -10,7 +10,7 @@ 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 |
|
@@ -27,8 +27,8 @@ def main():
|
|
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
|
32 |
|
33 |
# Step 3: Get response from LLM
|
34 |
generated_code = prompt_llm(full_prompt)
|
|
|
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 |
|
|
|
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\nUser instruction: {user_input.strip()}"
|
32 |
|
33 |
# Step 3: Get response from LLM
|
34 |
generated_code = prompt_llm(full_prompt)
|
generated/result_script.py
CHANGED
@@ -1,23 +1,88 @@
|
|
1 |
import FreeCAD as App
|
2 |
-
import FreeCADGui as Gui
|
3 |
-
from FreeCAD import Vector, Placement, Rotation
|
4 |
import Part
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
5 |
|
6 |
-
|
7 |
-
|
8 |
-
box = doc.addObject("Part::Box", "Box")
|
9 |
-
box.Length = length
|
10 |
-
box.Width = width
|
11 |
-
box.Height = height
|
12 |
-
box.Placement = Placement(Vector(0, 0, 0), Rotation())
|
13 |
-
box.ViewObject.ShapeColor = (0.8, 0.8, 0.8)
|
14 |
-
doc.recompute()
|
15 |
-
Gui.ActiveDocument.ActiveView.viewAxometric()
|
16 |
-
Gui.SendMsgToActiveView("ViewFit")
|
17 |
-
return doc
|
18 |
-
|
19 |
-
if __name__ == "__main__":
|
20 |
-
createBox()
|
21 |
|
22 |
|
23 |
import FreeCADGui
|
|
|
1 |
import FreeCAD as App
|
|
|
|
|
2 |
import Part
|
3 |
+
import math
|
4 |
+
from FreeCAD import Vector, Placement, Rotation
|
5 |
+
|
6 |
+
doc = App.newDocument()
|
7 |
+
|
8 |
+
outer_diameter = 100.0
|
9 |
+
inner_diameter = 50.0
|
10 |
+
thickness = 10.0
|
11 |
+
bolt_circle_diameter = 75.0
|
12 |
+
num_bolt_holes = 6
|
13 |
+
bolt_hole_diameter = 8.0
|
14 |
+
|
15 |
+
overall_placement_vector = Vector(0, 0, 0)
|
16 |
+
overall_rotation_axis = Vector(0, 0, 1)
|
17 |
+
overall_rotation_angle = 0.0
|
18 |
+
|
19 |
+
outer_radius = outer_diameter / 2
|
20 |
+
inner_radius = inner_diameter / 2
|
21 |
+
bolt_hole_radius = bolt_hole_diameter / 2
|
22 |
+
|
23 |
+
obj_base = doc.addObject("Part::Cylinder", "FlangeBase")
|
24 |
+
obj_base.Radius = outer_radius
|
25 |
+
obj_base.Height = thickness
|
26 |
+
obj_base.Placement = Placement(Vector(0, 0, 0))
|
27 |
+
|
28 |
+
obj_center_hole = doc.addObject("Part::Cylinder", "CenterHole")
|
29 |
+
obj_center_hole.Radius = inner_radius
|
30 |
+
obj_center_hole.Height = thickness
|
31 |
+
obj_center_hole.Placement = Placement(Vector(0, 0, 0))
|
32 |
+
|
33 |
+
bolt_hole_objects = []
|
34 |
+
for i in range(num_bolt_holes):
|
35 |
+
angle = 2 * math.pi * i / num_bolt_holes
|
36 |
+
x = bolt_circle_diameter / 2 * math.cos(angle)
|
37 |
+
y = bolt_circle_diameter / 2 * math.sin(angle)
|
38 |
+
|
39 |
+
obj_bolt_hole = doc.addObject("Part::Cylinder", f"BoltHole_{i+1}")
|
40 |
+
obj_bolt_hole.Radius = bolt_hole_radius
|
41 |
+
obj_bolt_hole.Height = thickness
|
42 |
+
obj_bolt_hole.Placement = Placement(Vector(x, y, 0))
|
43 |
+
bolt_hole_objects.append(obj_bolt_hole)
|
44 |
+
|
45 |
+
doc.recompute()
|
46 |
+
|
47 |
+
obj_cut_center = doc.addObject("Part::Cut", "FlangeWithCenterHole")
|
48 |
+
obj_cut_center.Base = obj_base
|
49 |
+
obj_cut_center.Tool = obj_center_hole
|
50 |
+
doc.recompute()
|
51 |
+
|
52 |
+
obj_base.Visibility = False
|
53 |
+
obj_center_hole.Visibility = False
|
54 |
+
|
55 |
+
obj_fused_holes = doc.addObject("Part::MultiFuse", "FusedBoltHoles")
|
56 |
+
obj_fused_holes.Objects = bolt_hole_objects
|
57 |
+
doc.recompute()
|
58 |
+
|
59 |
+
for hole_obj in bolt_hole_objects:
|
60 |
+
hole_obj.Visibility = False
|
61 |
+
|
62 |
+
obj_final_flange = doc.addObject("Part::Cut", "FinalFlange")
|
63 |
+
obj_final_flange.Base = obj_cut_center
|
64 |
+
obj_final_flange.Tool = obj_fused_holes
|
65 |
+
doc.recompute()
|
66 |
+
|
67 |
+
obj_cut_center.Visibility = False
|
68 |
+
obj_fused_holes.Visibility = False
|
69 |
+
|
70 |
+
final_placement = Placement(overall_placement_vector, Rotation(overall_rotation_axis, overall_rotation_angle))
|
71 |
+
obj_final_flange.Placement = final_placement
|
72 |
+
|
73 |
+
rod_length = 200.0
|
74 |
+
rod_diameter = inner_diameter
|
75 |
+
rod_radius = rod_diameter / 2
|
76 |
+
|
77 |
+
rod_placement_z = thickness / 2 - rod_length / 2
|
78 |
+
|
79 |
+
obj_rod = doc.addObject("Part::Cylinder", "Rod")
|
80 |
+
obj_rod.Radius = rod_radius
|
81 |
+
obj_rod.Height = rod_length
|
82 |
+
obj_rod.Placement = Placement(Vector(0, 0, rod_placement_z))
|
83 |
|
84 |
+
doc.recompute()
|
85 |
+
App.ActiveDocument.ActiveView.fitAll()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
86 |
|
87 |
|
88 |
import FreeCADGui
|
main.py
CHANGED
@@ -4,7 +4,7 @@ import subprocess
|
|
4 |
|
5 |
# File paths
|
6 |
prompt_base = Path("prompts/base_instruction.txt")
|
7 |
-
prompt_examples = Path("prompts/example_code.txt")
|
8 |
GEN_SCRIPT = Path("generated/result_script.py")
|
9 |
RUN_SCRIPT = Path("src/run_freecad.py")
|
10 |
|
@@ -21,8 +21,8 @@ def main():
|
|
21 |
|
22 |
# Step 2: Build prompt
|
23 |
base_prompt = prompt_base.read_text().strip()
|
24 |
-
example_prompt = prompt_examples.read_text().strip()
|
25 |
-
full_prompt = f"{base_prompt}\n\
|
26 |
|
27 |
|
28 |
# Step 3: Get response from LLM
|
|
|
4 |
|
5 |
# File paths
|
6 |
prompt_base = Path("prompts/base_instruction.txt")
|
7 |
+
# prompt_examples = Path("prompts/example_code.txt")
|
8 |
GEN_SCRIPT = Path("generated/result_script.py")
|
9 |
RUN_SCRIPT = Path("src/run_freecad.py")
|
10 |
|
|
|
21 |
|
22 |
# Step 2: Build prompt
|
23 |
base_prompt = prompt_base.read_text().strip()
|
24 |
+
# example_prompt = prompt_examples.read_text().strip()
|
25 |
+
full_prompt = f"{base_prompt}\n\n\nUser instruction: {user_input.strip()}"
|
26 |
|
27 |
|
28 |
# Step 3: Get response from LLM
|
prompts/example_code.txt
DELETED
@@ -1,565 +0,0 @@
|
|
1 |
-
- Correct Usage of fuse() in FreeCAD-
|
2 |
-
When performing a union (boolean fuse) of multiple shapes in FreeCAD, always use the iterative .fuse() method on Part objects instead of Part.Union().
|
3 |
-
|
4 |
-
Correct Approach:
|
5 |
-
|
6 |
-
fan_final_shape = all_parts_to_fuse[0] # Start with the first shape
|
7 |
-
for shape in all_parts_to_fuse[1:]: # Iterate over remaining shapes
|
8 |
-
fan_final_shape = fan_final_shape.fuse(shape) # Fuse one by one
|
9 |
-
Avoid:
|
10 |
-
|
11 |
-
fan_final_shape = Part.Union(all_parts_to_fuse) # Incorrect method
|
12 |
-
|
13 |
-
|
14 |
-
- When applying a Placement to a FreeCAD shape (like a Part.Solid or Part.Shape), do not use .Placed(placement) — this method does not exist.
|
15 |
-
Instead, use .copy() and assign the Placement directly, like this:
|
16 |
-
|
17 |
-
shape = Part.makeBox(10, 10, 10)
|
18 |
-
placed_shape = shape.copy()
|
19 |
-
placed_shape.Placement = Placement(Vector(x, y, z), Rotation(Vector(0,0,1), angle))
|
20 |
-
Always use .copy() to avoid modifying the original shape directly, and set Placement as an attribute on the copied shape.
|
21 |
-
|
22 |
-
|
23 |
-
- Whenever you are asked to make a fastner including nut bolt and screw, you need to make a similar code as the one given below. you have the rag in your context window from where you must write the necessary function of calculating dimensions from screw_maker.py. You need to then make a dummy function for the variables of the screw as asked-
|
24 |
-
|
25 |
-
from screw_maker import *
|
26 |
-
|
27 |
-
try:
|
28 |
-
import FreeCADGui
|
29 |
-
GUI_AVAILABLE = True
|
30 |
-
except ImportError:
|
31 |
-
GUI_AVAILABLE = False
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
def makeAllMetalFlangedLockNut(self, fa):
|
36 |
-
"""Creates a distorted thread lock nut with a flange
|
37 |
-
Supported types:
|
38 |
-
- ISO 7044 all metal lock nuts with flange
|
39 |
-
- ISO 12126 all metal flanged lock nuts with fine pitch thread
|
40 |
-
"""
|
41 |
-
dia = self.getDia(fa.calc_diam, True)
|
42 |
-
if fa.baseType in ["ISO7044", "ISO12126"]:
|
43 |
-
P, c, _, _, dc, _, _, h, _, m_min, _, s, _, _ = fa.dimTable
|
44 |
-
m_w = m_min
|
45 |
-
else:
|
46 |
-
raise NotImplementedError(f"Unknown fastener type: {fa.Type}")
|
47 |
-
# main hexagonal body of the nut
|
48 |
-
shape = self.makeHexPrism(s, h)
|
49 |
-
# flange of the hex
|
50 |
-
fm = FSFaceMaker()
|
51 |
-
fm.AddPoint((1.05 * dia + s) / 4, 0.0)
|
52 |
-
fm.AddPoint((dc + sqrt3 * c) / 2, 0.0)
|
53 |
-
fm.AddPoint((dc - c) / 2, 0.0)
|
54 |
-
fm.AddArc2(0, c / 2, 150)
|
55 |
-
fm.AddPoint(
|
56 |
-
(1.05 * dia + s) / 4,
|
57 |
-
sqrt3
|
58 |
-
/ 3
|
59 |
-
* ((dc - c) / 2 + c / (4 - 2 * sqrt3) - (1.05 * dia + s) / 4),
|
60 |
-
)
|
61 |
-
flange = self.RevolveZ(fm.GetFace())
|
62 |
-
shape = shape.fuse(flange).removeSplitter()
|
63 |
-
# internal bore
|
64 |
-
fm.Reset()
|
65 |
-
id = self.GetInnerThreadMinDiameter(dia, P, 0.0)
|
66 |
-
bore_cham_ht = (dia * 1.05 - id) / 2 * tan15
|
67 |
-
fm.AddPoint(0.0, 0.0)
|
68 |
-
fm.AddPoint(dia * 1.05 / 2, 0.0)
|
69 |
-
fm.AddPoint(id / 2, bore_cham_ht)
|
70 |
-
fm.AddPoint(id / 2, h - bore_cham_ht)
|
71 |
-
fm.AddPoint(dia * 1.05 / 2, h)
|
72 |
-
fm.AddPoint(0.0, h)
|
73 |
-
bore_cutter = self.RevolveZ(fm.GetFace())
|
74 |
-
shape = shape.cut(bore_cutter)
|
75 |
-
# outer chamfer on the hex
|
76 |
-
fm.Reset()
|
77 |
-
fm.AddPoint((s / sqrt3 + 1.05 * dia / 2) / 2, h)
|
78 |
-
fm.AddPoint(s / sqrt3, h)
|
79 |
-
fm.AddPoint(s / sqrt3, m_w)
|
80 |
-
top_cham_cutter = self.RevolveZ(fm.GetFace())
|
81 |
-
shape = shape.cut(top_cham_cutter)
|
82 |
-
# add modelled threads if needed
|
83 |
-
if fa.Thread:
|
84 |
-
thread_cutter = self.CreateInnerThreadCutter(dia, P, h + P)
|
85 |
-
shape = shape.cut(thread_cutter)
|
86 |
-
return shape
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
if __name__ == "__main__":
|
91 |
-
import sys
|
92 |
-
sys.path.append(r"C:\Users\yasin\Desktop\Code\text2cad_trial")
|
93 |
-
import screw_maker as sm
|
94 |
-
import FreeCAD as App, Part
|
95 |
-
|
96 |
-
s = sm.Screw()
|
97 |
-
|
98 |
-
class DummyFA:
|
99 |
-
baseType = "ISO7044"
|
100 |
-
Type = "ISO7044"
|
101 |
-
calc_diam = "M10"
|
102 |
-
Diameter = "M10"
|
103 |
-
Thread = True
|
104 |
-
LeftHanded = False
|
105 |
-
dimTable = [1.5, 0.5, 0, 0, 17.5, 0, 0, 8.0, 0, 7.5, 0, 15, 0, 0]
|
106 |
-
|
107 |
-
fa = DummyFA()
|
108 |
-
shape = makeAllMetalFlangedLockNut(s, fa)
|
109 |
-
|
110 |
-
doc = App.newDocument("AutoNut")
|
111 |
-
obj = doc.addObject("Part::Feature", "AutoFlangedNut")
|
112 |
-
obj.Shape = shape
|
113 |
-
doc.recompute()
|
114 |
-
|
115 |
-
print("Flanged lock nut generated successfully.")
|
116 |
-
|
117 |
-
if GUI_AVAILABLE:
|
118 |
-
# FreeCADGui.showMainWindow()
|
119 |
-
FreeCADGui.activeDocument().activeView().viewAxometric()
|
120 |
-
FreeCADGui.SendMsgToActiveView("ViewFit")
|
121 |
-
|
122 |
-
|
123 |
-
- when asked to "make a flange of OD 100mm, bore size as 50mm, thickness 7.5mm. the height of the middle hollow neck must be 15mm. make 6 m12 holes at PCD 75mm", make the following code:
|
124 |
-
import FreeCAD as App
|
125 |
-
import FreeCADGui as Gui
|
126 |
-
from FreeCAD import Vector
|
127 |
-
import math
|
128 |
-
|
129 |
-
|
130 |
-
def createFlangeAssembly():
|
131 |
-
doc = App.newDocument("Flange")
|
132 |
-
|
133 |
-
# === Parameters ===
|
134 |
-
FLANGE_OUTER_DIAMETER = 100.0
|
135 |
-
FLANGE_THICKNESS = 7.5
|
136 |
-
BORE_INNER_DIAMETER = 50.0
|
137 |
-
NECK_HEIGHT = 15.0
|
138 |
-
NECK_OUTER_DIAMETER = 60.0
|
139 |
-
NUM_BOLT_HOLES = 6
|
140 |
-
BOLT_HOLE_DIAMETER = 12.0
|
141 |
-
PCD = 75.0
|
142 |
-
|
143 |
-
total_height = FLANGE_THICKNESS + NECK_HEIGHT
|
144 |
-
|
145 |
-
# === 1. Create flange base ===
|
146 |
-
flange = doc.addObject("Part::Cylinder", "Flange")
|
147 |
-
flange.Radius = FLANGE_OUTER_DIAMETER / 2
|
148 |
-
flange.Height = FLANGE_THICKNESS
|
149 |
-
|
150 |
-
# === 2. Cut central bore from flange ===
|
151 |
-
bore = doc.addObject("Part::Cylinder", "CentralBore")
|
152 |
-
bore.Radius = BORE_INNER_DIAMETER / 2
|
153 |
-
bore.Height = FLANGE_THICKNESS
|
154 |
-
bore_cut = doc.addObject("Part::Cut", "FlangeWithBore")
|
155 |
-
bore_cut.Base = flange
|
156 |
-
bore_cut.Tool = bore
|
157 |
-
|
158 |
-
# === 3. Create neck ===
|
159 |
-
neck_outer = doc.addObject("Part::Cylinder", "NeckOuter")
|
160 |
-
neck_outer.Radius = NECK_OUTER_DIAMETER / 2
|
161 |
-
neck_outer.Height = NECK_HEIGHT
|
162 |
-
neck_outer.Placement.Base = Vector(0, 0, FLANGE_THICKNESS)
|
163 |
-
|
164 |
-
neck_inner = doc.addObject("Part::Cylinder", "NeckInner")
|
165 |
-
neck_inner.Radius = BORE_INNER_DIAMETER / 2
|
166 |
-
neck_inner.Height = NECK_HEIGHT
|
167 |
-
neck_inner.Placement.Base = Vector(0, 0, FLANGE_THICKNESS)
|
168 |
-
|
169 |
-
neck_hollow = doc.addObject("Part::Cut", "HollowNeck")
|
170 |
-
neck_hollow.Base = neck_outer
|
171 |
-
neck_hollow.Tool = neck_inner
|
172 |
-
|
173 |
-
# === 4. Fuse flange (with central hole) and neck ===
|
174 |
-
fused = doc.addObject("Part::Fuse", "FlangeAndNeck")
|
175 |
-
fused.Base = bore_cut
|
176 |
-
fused.Tool = neck_hollow
|
177 |
-
|
178 |
-
# === 5. Cut bolt holes sequentially ===
|
179 |
-
current_shape = fused
|
180 |
-
bolt_radius = BOLT_HOLE_DIAMETER / 2
|
181 |
-
bolt_circle_radius = PCD / 2
|
182 |
-
|
183 |
-
for i in range(NUM_BOLT_HOLES):
|
184 |
-
angle_deg = 360 * i / NUM_BOLT_HOLES
|
185 |
-
angle_rad = math.radians(angle_deg)
|
186 |
-
x = bolt_circle_radius * math.cos(angle_rad)
|
187 |
-
y = bolt_circle_radius * math.sin(angle_rad)
|
188 |
-
|
189 |
-
hole = doc.addObject("Part::Cylinder", f"BoltHole_{i+1:02d}")
|
190 |
-
hole.Radius = bolt_radius
|
191 |
-
hole.Height = total_height
|
192 |
-
hole.Placement.Base = Vector(x, y, 0)
|
193 |
-
|
194 |
-
cut = doc.addObject("Part::Cut", f"Cut_Bolt_{i+1:02d}")
|
195 |
-
cut.Base = current_shape
|
196 |
-
cut.Tool = hole
|
197 |
-
current_shape = cut # update for next iteration
|
198 |
-
|
199 |
-
# === 6. Final result ===
|
200 |
-
|
201 |
-
|
202 |
-
# Recompute and fit view
|
203 |
-
doc.recompute()
|
204 |
-
Gui.activeDocument().activeView().viewAxometric()
|
205 |
-
Gui.SendMsgToActiveView("ViewFit")
|
206 |
-
|
207 |
-
return doc
|
208 |
-
|
209 |
-
if __name__ == "__main__":
|
210 |
-
createFlangeAssembly()
|
211 |
-
|
212 |
-
use this template whenever asked to make a flange
|
213 |
-
|
214 |
-
- Use material only when specified by user. An example of using material is-
|
215 |
-
|
216 |
-
view_obj = final_obj.ViewObject
|
217 |
-
view_obj.ShapeColor = (0.8, 0.8, 0.85) # Light grey-blue tone
|
218 |
-
view_obj.DiffuseColor = [(0.8, 0.8, 0.85)] # Consistent color across faces
|
219 |
-
view_obj.Transparency = 0
|
220 |
-
|
221 |
-
material_obj = doc.addObject("App::MaterialObject", "Material")
|
222 |
-
material_obj.Material = {
|
223 |
-
'Name': 'Stainless steel',
|
224 |
-
'Density': '8000 kg/m^3',
|
225 |
-
'YoungsModulus': '200000 MPa',
|
226 |
-
'PoissonRatio': '0.3'
|
227 |
-
}
|
228 |
-
material_obj.Label = "StainlessSteelMaterial"
|
229 |
-
|
230 |
-
- This is a good example for a teapot. Whenever asked to generate a teapot, make something similar:
|
231 |
-
import FreeCAD as App
|
232 |
-
import FreeCADGui as Gui
|
233 |
-
from FreeCAD import Vector, Placement, Rotation
|
234 |
-
import Part
|
235 |
-
|
236 |
-
# Teapot dimensions
|
237 |
-
BODY_BOTTOM_RADIUS = 50.0
|
238 |
-
BODY_MAX_RADIUS = 80.0
|
239 |
-
BODY_HEIGHT = 100.0
|
240 |
-
LID_OPENING_RADIUS = 35.0
|
241 |
-
|
242 |
-
# Spout parameters
|
243 |
-
SPOUT_ATTACH_HEIGHT = BODY_HEIGHT * 0.5 # 50.0
|
244 |
-
SPOUT_OFFSET_Y = BODY_MAX_RADIUS * 0.7 # 56.0
|
245 |
-
SPOUT_LENGTH_HORIZONTAL = 60.0
|
246 |
-
SPOUT_LENGTH_VERTICAL = 30.0
|
247 |
-
SPOUT_RADIUS = 7.0
|
248 |
-
|
249 |
-
# Handle parameters
|
250 |
-
HANDLE_ATTACH_TOP_HEIGHT = BODY_HEIGHT * 0.7 # 70.0
|
251 |
-
HANDLE_ATTACH_BOTTOM_HEIGHT = BODY_HEIGHT * 0.3 # 30.0
|
252 |
-
HANDLE_OFFSET_Y = -BODY_MAX_RADIUS * 0.7 # -56.0
|
253 |
-
HANDLE_RADIUS = 6.0
|
254 |
-
|
255 |
-
def createTeapot():
|
256 |
-
doc = App.newDocument("Teapot")
|
257 |
-
|
258 |
-
# --- 1. Body ---
|
259 |
-
body_profile_pts = [
|
260 |
-
Vector(BODY_BOTTOM_RADIUS, 0, 0),
|
261 |
-
Vector(BODY_MAX_RADIUS, 0, BODY_HEIGHT * 0.4),
|
262 |
-
Vector(BODY_MAX_RADIUS * 0.8, 0, BODY_HEIGHT * 0.7),
|
263 |
-
Vector(LID_OPENING_RADIUS, 0, BODY_HEIGHT)
|
264 |
-
]
|
265 |
-
body_spline = Part.BSplineCurve(body_profile_pts)
|
266 |
-
body_edge = body_spline.toShape()
|
267 |
-
line1 = Part.LineSegment(Vector(LID_OPENING_RADIUS, 0, BODY_HEIGHT), Vector(0, 0, BODY_HEIGHT)).toShape()
|
268 |
-
line2 = Part.LineSegment(Vector(0, 0, BODY_HEIGHT), Vector(0, 0, 0)).toShape()
|
269 |
-
line3 = Part.LineSegment(Vector(0, 0, 0), Vector(BODY_BOTTOM_RADIUS, 0, 0)).toShape()
|
270 |
-
wire = Part.Wire([body_edge, line1, line2, line3])
|
271 |
-
face = Part.Face(wire)
|
272 |
-
body_solid = face.revolve(Vector(0, 0, 0), Vector(0, 0, 1), 360)
|
273 |
-
|
274 |
-
obj_body = doc.addObject("Part::Feature", "Body")
|
275 |
-
obj_body.Shape = body_solid
|
276 |
-
obj_body.ViewObject.ShapeColor = (0.9, 0.7, 0.7)
|
277 |
-
|
278 |
-
# --- 2. Lid ---
|
279 |
-
lid_profile_pts = [
|
280 |
-
Vector(36.0, 0, 0),
|
281 |
-
Vector(36.0, 0, 3.0),
|
282 |
-
Vector(35.0, 0, 3.0 + 20.0 * 0.2),
|
283 |
-
Vector(17.5, 0, 3.0 + 20.0 * 0.7),
|
284 |
-
Vector(10.0, 0, 3.0 + 20.0),
|
285 |
-
Vector(5.0, 0, 3.0 + 20.0 + 15.0 * 0.8),
|
286 |
-
Vector(0, 0, 3.0 + 20.0 + 15.0)
|
287 |
-
]
|
288 |
-
lid_spline = Part.BSplineCurve(lid_profile_pts)
|
289 |
-
lid_edge = lid_spline.toShape()
|
290 |
-
line1 = Part.LineSegment(Vector(0, 0, 3.0 + 20.0 + 15.0), Vector(0, 0, 0)).toShape()
|
291 |
-
line2 = Part.LineSegment(Vector(0, 0, 0), Vector(36.0, 0, 0)).toShape()
|
292 |
-
wire_lid = Part.Wire([lid_edge, line1, line2])
|
293 |
-
face_lid = Part.Face(wire_lid)
|
294 |
-
lid_solid = face_lid.revolve(Vector(0, 0, 0), Vector(0, 0, 1), 360)
|
295 |
-
|
296 |
-
obj_lid = doc.addObject("Part::Feature", "Lid")
|
297 |
-
obj_lid.Shape = lid_solid
|
298 |
-
obj_lid.Placement = Placement(Vector(0, 0, BODY_HEIGHT), Rotation())
|
299 |
-
obj_lid.ViewObject.ShapeColor = (0.9, 0.7, 0.7)
|
300 |
-
|
301 |
-
# --- 3. Spout (Precomputed final positions) ---
|
302 |
-
spout_path_pts = [
|
303 |
-
Vector(0, -121, 66), # Original: (0, -56, 50) -> transformed
|
304 |
-
Vector(0, -91, 51), # Original: (0, -26, 65) -> transformed
|
305 |
-
Vector(0, -61, 36) # Original: (0, 4, 80) -> transformed
|
306 |
-
]
|
307 |
-
|
308 |
-
spout_curve = Part.BSplineCurve(spout_path_pts)
|
309 |
-
spout_wire = Part.Wire(spout_curve.toShape())
|
310 |
-
|
311 |
-
tangent_spout = spout_curve.tangent(spout_curve.FirstParameter)[0]
|
312 |
-
tangent_spout.normalize()
|
313 |
-
|
314 |
-
spout_circle = Part.Circle()
|
315 |
-
spout_circle.Center = spout_path_pts[0]
|
316 |
-
spout_circle.Axis = tangent_spout
|
317 |
-
spout_circle.Radius = SPOUT_RADIUS
|
318 |
-
spout_profile = Part.Wire(spout_circle.toShape())
|
319 |
-
|
320 |
-
spout_solid = spout_wire.makePipe(spout_profile)
|
321 |
-
obj_spout = doc.addObject("Part::Feature", "Spout")
|
322 |
-
obj_spout.Shape = spout_solid
|
323 |
-
obj_spout.ViewObject.ShapeColor = (0.9, 0.7, 0.7)
|
324 |
-
|
325 |
-
# --- 4. Handle (Precomputed final positions) ---
|
326 |
-
handle_path_pts = [
|
327 |
-
Vector(0, 56, 31), # Original: (0, 56, 70) -> transformed
|
328 |
-
Vector(0, 78, 43), # Original: (0, 78, 58) -> transformed
|
329 |
-
Vector(0, 78, 79), # Original: (0, 78, 22) -> transformed
|
330 |
-
Vector(0, 56, 71) # Original: (0, 56, 30) -> transformed
|
331 |
-
]
|
332 |
-
|
333 |
-
handle_curve = Part.BSplineCurve(handle_path_pts)
|
334 |
-
handle_wire = Part.Wire(handle_curve.toShape())
|
335 |
-
|
336 |
-
tangent_handle = handle_curve.tangent(handle_curve.FirstParameter)[0]
|
337 |
-
tangent_handle.normalize()
|
338 |
-
|
339 |
-
handle_circle = Part.Circle()
|
340 |
-
handle_circle.Center = handle_path_pts[0]
|
341 |
-
handle_circle.Axis = tangent_handle
|
342 |
-
handle_circle.Radius = HANDLE_RADIUS
|
343 |
-
handle_profile = Part.Wire(handle_circle.toShape())
|
344 |
-
|
345 |
-
handle_solid = handle_wire.makePipe(handle_profile)
|
346 |
-
obj_handle = doc.addObject("Part::Feature", "Handle")
|
347 |
-
obj_handle.Shape = handle_solid
|
348 |
-
obj_handle.ViewObject.ShapeColor = (0.9, 0.7, 0.7)
|
349 |
-
|
350 |
-
# --- 5. Fuse all parts ---
|
351 |
-
fused = obj_body.Shape.fuse(obj_lid.Shape)
|
352 |
-
fused = fused.fuse(obj_spout.Shape)
|
353 |
-
fused = fused.fuse(obj_handle.Shape)
|
354 |
-
|
355 |
-
obj_final = doc.addObject("Part::Feature", "Teapot_Complete")
|
356 |
-
obj_final.Shape = fused
|
357 |
-
obj_final.ViewObject.ShapeColor = (0.9, 0.6, 0.6)
|
358 |
-
|
359 |
-
# Hide individual parts for clarity
|
360 |
-
obj_body.ViewObject.Visibility = False
|
361 |
-
obj_lid.ViewObject.Visibility = False
|
362 |
-
obj_spout.ViewObject.Visibility = False
|
363 |
-
obj_handle.ViewObject.Visibility = False
|
364 |
-
|
365 |
-
doc.recompute()
|
366 |
-
|
367 |
-
Gui.activeDocument().activeView().viewAxometric()
|
368 |
-
Gui.SendMsgToActiveView("ViewFit")
|
369 |
-
|
370 |
-
return doc
|
371 |
-
|
372 |
-
if __name__ == "__main__":
|
373 |
-
createTeapot()
|
374 |
-
|
375 |
-
- This is a good example for a herringbone gear. If asked to make a herringbone gear, generate similar:
|
376 |
-
#Herringbone gear
|
377 |
-
|
378 |
-
import FreeCAD as App
|
379 |
-
import FreeCADGui as Gui
|
380 |
-
import Part
|
381 |
-
import math
|
382 |
-
from FreeCAD import Vector, Placement, Rotation
|
383 |
-
|
384 |
-
def createHerringboneGear(
|
385 |
-
num_teeth=20,
|
386 |
-
module=5.0,
|
387 |
-
pressure_angle_deg=20.0,
|
388 |
-
helix_angle_deg=25.0,
|
389 |
-
face_width=50.0,
|
390 |
-
central_bore_diameter=20.0,
|
391 |
-
num_loft_sections=50,
|
392 |
-
addendum_factor=1.0,
|
393 |
-
dedendum_factor=1.25,
|
394 |
-
tooth_radial_offset=1.5 # Teeth pushed radially outward
|
395 |
-
):
|
396 |
-
doc = App.newDocument("HerringboneGear")
|
397 |
-
|
398 |
-
pressure_angle_rad = math.radians(pressure_angle_deg)
|
399 |
-
helix_angle_rad = math.radians(helix_angle_deg)
|
400 |
-
|
401 |
-
pitch_diameter = module * num_teeth
|
402 |
-
pitch_radius = pitch_diameter / 2
|
403 |
-
addendum = addendum_factor * module
|
404 |
-
dedendum = dedendum_factor * module
|
405 |
-
root_radius = pitch_radius - dedendum + tooth_radial_offset
|
406 |
-
outer_radius = pitch_radius + addendum + tooth_radial_offset
|
407 |
-
|
408 |
-
gear_total_height = face_width
|
409 |
-
half_gear_height = gear_total_height / 2
|
410 |
-
|
411 |
-
total_angular_twist_rad = (face_width * math.tan(helix_angle_rad)) / pitch_radius
|
412 |
-
|
413 |
-
gear_hub = doc.addObject("Part::Cylinder", "GearHub")
|
414 |
-
gear_hub.Radius = outer_radius - tooth_radial_offset
|
415 |
-
gear_hub.Height = gear_total_height
|
416 |
-
gear_hub.Placement.Base = Vector(0, 0, 0)
|
417 |
-
|
418 |
-
if central_bore_diameter > 0:
|
419 |
-
bore_radius = central_bore_diameter / 2
|
420 |
-
central_bore = doc.addObject("Part::Cylinder", "CentralBore")
|
421 |
-
central_bore.Radius = bore_radius
|
422 |
-
central_bore.Height = gear_total_height
|
423 |
-
central_bore.Placement.Base = Vector(0, 0, 0)
|
424 |
-
|
425 |
-
hub_base = doc.addObject("Part::Cut", "Hub_With_Bore")
|
426 |
-
hub_base.Base = gear_hub
|
427 |
-
hub_base.Tool = central_bore
|
428 |
-
else:
|
429 |
-
hub_base = gear_hub
|
430 |
-
|
431 |
-
angle_per_tooth = 360.0 / num_teeth
|
432 |
-
|
433 |
-
effective_half_angle_for_flank_base = (math.pi / num_teeth) / 2
|
434 |
-
effective_half_angle_for_flank_tip = effective_half_angle_for_flank_base * 0.7
|
435 |
-
|
436 |
-
P_A = Vector(root_radius * math.sin(effective_half_angle_for_flank_base), root_radius * math.cos(effective_half_angle_for_flank_base), 0)
|
437 |
-
P_B = Vector(root_radius * math.sin(-effective_half_angle_for_flank_base), root_radius * math.cos(-effective_half_angle_for_flank_base), 0)
|
438 |
-
|
439 |
-
P_C = Vector(outer_radius * math.sin(effective_half_angle_for_flank_tip), outer_radius * math.cos(effective_half_angle_for_flank_tip), 0)
|
440 |
-
P_D = Vector(outer_radius * math.sin(-effective_half_angle_for_flank_tip), outer_radius * math.cos(-effective_half_angle_for_flank_tip), 0)
|
441 |
-
|
442 |
-
e_flank1 = Part.LineSegment(P_B, P_D).toShape()
|
443 |
-
e_flank2 = Part.LineSegment(P_A, P_C).toShape()
|
444 |
-
|
445 |
-
def offset_midpoint(p1, p2, offset=0.1):
|
446 |
-
mid = (p1 + p2).multiply(0.5)
|
447 |
-
vec = p2.sub(p1)
|
448 |
-
perp = Vector(-vec.y, vec.x, 0)
|
449 |
-
perp.normalize()
|
450 |
-
return mid.add(perp.multiply(offset))
|
451 |
-
|
452 |
-
tip_midpoint = offset_midpoint(P_D, P_C, 0.1)
|
453 |
-
e_tip_arc = Part.ArcOfCircle(P_D, tip_midpoint, P_C).toShape()
|
454 |
-
|
455 |
-
root_midpoint = offset_midpoint(P_A, P_B, 0.1)
|
456 |
-
e_root_arc = Part.ArcOfCircle(P_A, root_midpoint, P_B).toShape()
|
457 |
-
|
458 |
-
try:
|
459 |
-
tooth_profile_wire = Part.Wire([e_root_arc, e_flank1, e_tip_arc, e_flank2])
|
460 |
-
except Exception as e:
|
461 |
-
App.Console.PrintError(f"Error creating tooth profile wire: {e}. Using fallback wire.\n")
|
462 |
-
fallback_edges = [
|
463 |
-
Part.LineSegment(P_A, P_B).toShape(),
|
464 |
-
Part.LineSegment(P_B, P_D).toShape(),
|
465 |
-
Part.LineSegment(P_D, P_C).toShape(),
|
466 |
-
Part.LineSegment(P_C, P_A).toShape()
|
467 |
-
]
|
468 |
-
tooth_profile_wire = Part.Wire(fallback_edges)
|
469 |
-
|
470 |
-
tooth_profile_face = Part.Face(tooth_profile_wire)
|
471 |
-
|
472 |
-
helical_teeth_LH_fused = None
|
473 |
-
lh_z_start = 0
|
474 |
-
lh_z_end = half_gear_height
|
475 |
-
lh_twist_start = 0
|
476 |
-
lh_twist_end = total_angular_twist_rad / 2
|
477 |
-
|
478 |
-
for tooth_idx in range(num_teeth):
|
479 |
-
current_tooth_LH_profiles = []
|
480 |
-
initial_tooth_rotation_deg = tooth_idx * angle_per_tooth
|
481 |
-
|
482 |
-
for i in range(num_loft_sections + 1):
|
483 |
-
z_pos_current = lh_z_start + (lh_z_end - lh_z_start) * (i / num_loft_sections)
|
484 |
-
current_slice_twist_angle_rad = lh_twist_start + (lh_twist_end - lh_twist_start) * (i / num_loft_sections)
|
485 |
-
|
486 |
-
combined_rotation_deg = initial_tooth_rotation_deg + math.degrees(current_slice_twist_angle_rad)
|
487 |
-
|
488 |
-
profile_copy = tooth_profile_face.copy()
|
489 |
-
profile_copy.Placement = Placement(
|
490 |
-
Vector(0, 0, z_pos_current),
|
491 |
-
Rotation(Vector(0, 0, 1), combined_rotation_deg)
|
492 |
-
)
|
493 |
-
current_tooth_LH_profiles.append(profile_copy)
|
494 |
-
|
495 |
-
helical_tooth_LH_solid = Part.makeLoft(current_tooth_LH_profiles, True)
|
496 |
-
|
497 |
-
if helical_teeth_LH_fused is None:
|
498 |
-
helical_teeth_LH_fused = helical_tooth_LH_solid
|
499 |
-
else:
|
500 |
-
helical_teeth_LH_fused = helical_teeth_LH_fused.fuse(helical_tooth_LH_solid)
|
501 |
-
|
502 |
-
obj_helical_LH = doc.addObject("Part::Feature", "HelicalTeeth_Left_Section")
|
503 |
-
obj_helical_LH.Shape = helical_teeth_LH_fused
|
504 |
-
obj_helical_LH.ViewObject.ShapeColor = (0.7, 0.7, 0.9)
|
505 |
-
|
506 |
-
helical_teeth_RH_fused = None
|
507 |
-
rh_z_start = half_gear_height
|
508 |
-
rh_z_end = gear_total_height
|
509 |
-
rh_twist_start = total_angular_twist_rad / 2
|
510 |
-
rh_twist_end = 0
|
511 |
-
|
512 |
-
for tooth_idx in range(num_teeth):
|
513 |
-
current_tooth_RH_profiles = []
|
514 |
-
initial_tooth_rotation_deg = tooth_idx * angle_per_tooth
|
515 |
-
|
516 |
-
for i in range(num_loft_sections + 1):
|
517 |
-
z_pos_current = rh_z_start + (rh_z_end - rh_z_start) * (i / num_loft_sections)
|
518 |
-
current_slice_twist_angle_rad = rh_twist_start + (rh_twist_end - rh_twist_start) * (i / num_loft_sections)
|
519 |
-
|
520 |
-
combined_rotation_deg = initial_tooth_rotation_deg + math.degrees(current_slice_twist_angle_rad)
|
521 |
-
|
522 |
-
profile_copy = tooth_profile_face.copy()
|
523 |
-
profile_copy.Placement = Placement(
|
524 |
-
Vector(0, 0, z_pos_current),
|
525 |
-
Rotation(Vector(0, 0, 1), combined_rotation_deg)
|
526 |
-
)
|
527 |
-
current_tooth_RH_profiles.append(profile_copy)
|
528 |
-
|
529 |
-
helical_tooth_RH_solid = Part.makeLoft(current_tooth_RH_profiles, True)
|
530 |
-
|
531 |
-
if helical_teeth_RH_fused is None:
|
532 |
-
helical_teeth_RH_fused = helical_tooth_RH_solid
|
533 |
-
else:
|
534 |
-
helical_teeth_RH_fused = helical_teeth_RH_fused.fuse(helical_tooth_RH_solid)
|
535 |
-
|
536 |
-
obj_helical_RH = doc.addObject("Part::Feature", "HelicalTeeth_Right_Section")
|
537 |
-
obj_helical_RH.Shape = helical_teeth_RH_fused
|
538 |
-
obj_helical_RH.ViewObject.ShapeColor = (0.7, 0.9, 0.7)
|
539 |
-
|
540 |
-
combined_teeth_sections = doc.addObject("Part::Fuse", "Combined_Teeth_Sections")
|
541 |
-
combined_teeth_sections.Base = obj_helical_LH
|
542 |
-
combined_teeth_sections.Tool = obj_helical_RH
|
543 |
-
|
544 |
-
final_gear = doc.addObject("Part::Fuse", "HerringboneGear_Complete")
|
545 |
-
final_gear.Base = hub_base
|
546 |
-
final_gear.Tool = combined_teeth_sections
|
547 |
-
|
548 |
-
final_gear.ViewObject.ShapeColor = (0.8, 0.6, 0.9)
|
549 |
-
|
550 |
-
if central_bore_diameter > 0:
|
551 |
-
central_bore.ViewObject.Visibility = False
|
552 |
-
gear_hub.ViewObject.Visibility = False
|
553 |
-
hub_base.ViewObject.Visibility = False
|
554 |
-
obj_helical_LH.ViewObject.Visibility = False
|
555 |
-
obj_helical_RH.ViewObject.Visibility = False
|
556 |
-
combined_teeth_sections.ViewObject.Visibility = False
|
557 |
-
|
558 |
-
doc.recompute()
|
559 |
-
Gui.activeDocument().activeView().viewAxometric()
|
560 |
-
Gui.SendMsgToActiveView("ViewFit")
|
561 |
-
|
562 |
-
return doc
|
563 |
-
|
564 |
-
if __name__ == "__main__":
|
565 |
-
createHerringboneGear()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/llm_client.py
CHANGED
@@ -1,15 +1,19 @@
|
|
1 |
import os
|
2 |
from pathlib import Path
|
3 |
-
from huggingface_hub import hf_hub_download
|
4 |
from langchain_community.vectorstores import FAISS
|
5 |
-
from
|
|
|
|
|
6 |
from dotenv import load_dotenv
|
|
|
7 |
|
8 |
-
|
|
|
9 |
load_dotenv()
|
10 |
-
|
11 |
-
if not
|
12 |
-
raise ValueError("
|
13 |
|
14 |
# FAISS vectorstore setup
|
15 |
REPO_ID = "Yas1n/CADomatic_vectorstore"
|
@@ -27,49 +31,62 @@ vectorstore = FAISS.load_local(
|
|
27 |
allow_dangerous_deserialization=True,
|
28 |
index_name="index_oss120b"
|
29 |
)
|
30 |
-
retriever = vectorstore.as_retriever(search_kwargs={"k":
|
31 |
|
32 |
-
#
|
33 |
-
|
34 |
-
MODEL_NAME = "openai/gpt-oss-120b"
|
35 |
|
36 |
-
#
|
37 |
-
|
38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
docs = retriever.invoke(user_prompt)
|
40 |
context = "\n\n".join(doc.page_content for doc in docs)
|
41 |
|
42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
You are a helpful assistant that writes FreeCAD Python scripts from CAD instructions.
|
44 |
Use the following FreeCAD wiki documentation as context:
|
45 |
|
46 |
{context}
|
47 |
|
48 |
-
|
|
|
|
|
|
|
49 |
{user_prompt}
|
50 |
|
51 |
Respond with valid FreeCAD 1.0.1 Python code only, no extra comments.
|
52 |
"""
|
53 |
|
|
|
|
|
|
|
54 |
try:
|
55 |
-
|
56 |
-
|
57 |
-
messages=[{"role": "user", "content": final_prompt}],
|
58 |
-
)
|
59 |
-
return completion.choices[0].message.content
|
60 |
-
except Exception as e:
|
61 |
-
print("❌ Error in HF response:", e)
|
62 |
-
return ""
|
63 |
|
64 |
-
#
|
65 |
-
|
66 |
-
|
67 |
-
code = prompt_llm(prompt)
|
68 |
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
f.write(code)
|
74 |
|
75 |
-
|
|
|
|
|
|
|
|
1 |
import os
|
2 |
from pathlib import Path
|
3 |
+
from huggingface_hub import hf_hub_download
|
4 |
from langchain_community.vectorstores import FAISS
|
5 |
+
from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings
|
6 |
+
from langchain.memory import ConversationBufferMemory
|
7 |
+
from langchain.schema import HumanMessage, AIMessage
|
8 |
from dotenv import load_dotenv
|
9 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
10 |
|
11 |
+
|
12 |
+
# Load GEMINI API key
|
13 |
load_dotenv()
|
14 |
+
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
15 |
+
if not GEMINI_API_KEY:
|
16 |
+
raise ValueError("GEMINI_API_KEY missing in .env")
|
17 |
|
18 |
# FAISS vectorstore setup
|
19 |
REPO_ID = "Yas1n/CADomatic_vectorstore"
|
|
|
31 |
allow_dangerous_deserialization=True,
|
32 |
index_name="index_oss120b"
|
33 |
)
|
34 |
+
retriever = vectorstore.as_retriever(search_kwargs={"k": 8})
|
35 |
|
36 |
+
# Conversation memory
|
37 |
+
memory = ConversationBufferMemory(return_messages=True)
|
|
|
38 |
|
39 |
+
# Gemini 2.5 Flash (LangChain wrapper, no genai import needed)
|
40 |
+
llm = ChatGoogleGenerativeAI(
|
41 |
+
model="gemini-2.5-flash",
|
42 |
+
temperature=1.2,
|
43 |
+
api_key=GEMINI_API_KEY # Use the key directly
|
44 |
+
)
|
45 |
+
|
46 |
+
def build_prompt(user_prompt: str) -> str:
|
47 |
+
"""Builds full prompt with RAG context and conversation history."""
|
48 |
docs = retriever.invoke(user_prompt)
|
49 |
context = "\n\n".join(doc.page_content for doc in docs)
|
50 |
|
51 |
+
history_text = ""
|
52 |
+
for msg in memory.chat_memory.messages:
|
53 |
+
if isinstance(msg, HumanMessage):
|
54 |
+
history_text += f"User: {msg.content}\n"
|
55 |
+
elif isinstance(msg, AIMessage):
|
56 |
+
history_text += f"Assistant: {msg.content}\n"
|
57 |
+
|
58 |
+
return f"""
|
59 |
You are a helpful assistant that writes FreeCAD Python scripts from CAD instructions.
|
60 |
Use the following FreeCAD wiki documentation as context:
|
61 |
|
62 |
{context}
|
63 |
|
64 |
+
Here is the conversation so far:
|
65 |
+
{history_text}
|
66 |
+
|
67 |
+
Current instruction:
|
68 |
{user_prompt}
|
69 |
|
70 |
Respond with valid FreeCAD 1.0.1 Python code only, no extra comments.
|
71 |
"""
|
72 |
|
73 |
+
def prompt_llm(user_prompt: str) -> str:
|
74 |
+
"""Generate FreeCAD code using Gemini 2.5 Flash."""
|
75 |
+
final_prompt = build_prompt(user_prompt)
|
76 |
try:
|
77 |
+
response = llm.invoke(final_prompt)
|
78 |
+
result = response.content.strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
79 |
|
80 |
+
# Save user and AI messages in memory
|
81 |
+
memory.chat_memory.add_user_message(user_prompt)
|
82 |
+
memory.chat_memory.add_ai_message(result)
|
|
|
83 |
|
84 |
+
return result
|
85 |
+
except Exception as e:
|
86 |
+
print("❌ Error generating FreeCAD code:", e)
|
87 |
+
return ""
|
|
|
88 |
|
89 |
+
def reset_memory():
|
90 |
+
"""Clear conversation history."""
|
91 |
+
global memory
|
92 |
+
memory = ConversationBufferMemory(return_messages=True)
|