YSMlearnsCode commited on
Commit
41d6e2b
·
0 Parent(s):

clean hf integration

Browse files
.github/workflows/ci.yml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face hub
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ sync-to-hub:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - uses: actions/checkout@v3
14
+ with:
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: |
22
+ git push https://Yas1n:[email protected]/spaces/Yas1n/CADomatic main
.gitignore ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Environments
138
+ .env
139
+ .envrc
140
+ .venv
141
+ env/
142
+ venv/
143
+ ENV/
144
+ env.bak/
145
+ venv.bak/
146
+
147
+ # Spyder project settings
148
+ .spyderproject
149
+ .spyproject
150
+
151
+ # Rope project settings
152
+ .ropeproject
153
+
154
+ # mkdocs documentation
155
+ /site
156
+
157
+ # mypy
158
+ .mypy_cache/
159
+ .dmypy.json
160
+ dmypy.json
161
+
162
+ # Pyre type checker
163
+ .pyre/
164
+
165
+ # pytype static type analyzer
166
+ .pytype/
167
+
168
+ # Cython debug symbols
169
+ cython_debug/
170
+
171
+ # PyCharm
172
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
173
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
174
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
175
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
176
+ #.idea/
177
+
178
+ # Abstra
179
+ # Abstra is an AI-powered process automation framework.
180
+ # Ignore directories containing user credentials, local state, and settings.
181
+ # Learn more at https://abstra.io/docs
182
+ .abstra/
183
+
184
+ # Visual Studio Code
185
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
186
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
187
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
188
+ # you could uncomment the following to ignore the entire vscode folder
189
+ # .vscode/
190
+
191
+ # Ruff stuff:
192
+ .ruff_cache/
193
+
194
+ # PyPI configuration file
195
+ .pypirc
196
+
197
+ # Cursor
198
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
199
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
200
+ # refer to https://docs.cursor.com/context/ignore-files
201
+ .cursorignore
202
+ .cursorindexingignore
203
+
204
+ # Marimo
205
+ marimo/_static/
206
+ marimo/_lsp/
207
+ __marimo__/
208
+
209
+ /vectorstore/
210
+
211
+ # Ignore FAISS and pickle index files
212
+ /vectorstore/index.faiss
213
+ /vectorstore/index.pkl
214
+
215
+ # Ignore the generated model
216
+ app/generated/
217
+ app/generated/model.FCStd
218
+ app/generated/model.obj
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.11
LICENSE ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CADomatic 🛠️
2
+ **From prompt to CAD**
3
+
4
+ CADomatic is a Python-based tool that generates editable parametric CAD scripts for FreeCAD. Instead of creating static 3D models, CADomatic produces **fully customizable Python scripts** that build CAD geometry — allowing engineers to programmatically define parts, reuse templates, and iterate fast.
5
+
6
+ ---
7
+
8
+ ## 🔍 What It Does
9
+
10
+ ![CADomatic Demo](demo/0.1.0_flange_generation.gif)
11
+
12
+ - ✅ **Generates editable FreeCAD Python scripts** for parts like screws, nuts, fasteners, and more
13
+ - ✅ Each script can be modified for custom parameters (length, diameter, features, etc.)
14
+ - ✅ Outputs native `.py` scripts which use FreeCAD’s API to build geometry
15
+ - ✅ Enables **version-controlled**, reusable, parametric CAD pipelines
16
+ - ✅ Eliminates the need for manual modeling in the FreeCAD GUI
17
+
18
+ ---
19
+ ## 💡 Why Use CADomatic?
20
+
21
+ - 🔁 Automate repetitive CAD tasks
22
+ - 🧱 Build part libraries as **code**
23
+ - 🧪 Integrate CAD into testing or CI workflows
24
+ - 🔧 Customize geometry by changing script parameters
25
+ - 📐 Keep models lightweight and editable at the code level
26
+
27
+ ---
28
+ ## 💬 Example Prompts
29
+
30
+ Here are some example natural language prompts you can use to generate CAD scripts with CADomatic:
31
+
32
+ - "Build a flange with a 100mm outer diameter, 10mm thickness, and 6 bolt holes evenly spaced."
33
+ - "Make a cylindrical spacer, 20mm diameter and 30mm height, with a 5mm through hole."
34
+ - "Produce a washer with an outer diameter of 25mm and an inner diameter of 10mm."
35
+ - "Design a toy car with a rectangular box as the body and 4 circular wheels attached to the sides of the box."
36
+
37
+ These prompts will be converted into editable Python scripts that you can modify and reuse.
38
+
39
+
40
+ ---
41
+ ⚠️ **This is the first version** of CADomatic — a flash of what's possible.
42
+ Future versions under development will include:
43
+ - Improved **LLM-driven script generation**
44
+ - A **dedicated user interface** for part selection and parameter tuning
45
+ - More robust **template and geometry libraries**
46
+ ---
47
+ ## 🚀 How to Use CADomatic
48
+
49
+ ### ✅ Prerequisites
50
+ - Python 3.11+
51
+ - [FreeCAD](https://www.freecad.org/downloads.php) (must be installed and added to PATH)
52
+ - [uv](https://github.com/astral-sh/uv) (install via `pip install uv`)
53
+
54
+ ### ⚙️ Setup
55
+ ```bash
56
+ git clone https://github.com/yas1nsyed/CADomatic.git
57
+ cd CADomatic
58
+
59
+ # Create and activate virtual environment
60
+ python -m venv .venv
61
+ .venv\Scripts\activate
62
+
63
+ # Install dependencies
64
+ uv pip install -r requirements.txt
65
+ ```
66
+
67
+ - 🔐 [Set Up Gemini API Key](https://aistudio.google.com/app/apikey)
68
+ - Create a .env file in the project root:
69
+
70
+ - ▶️ Run CADomatic
71
+ ```bash
72
+ # Run the program
73
+ uv run main.py
74
+ ```
75
+ - Enter your prompt (e.g., "Create a 10mm cube with 2mm hole").
76
+ - FreeCAD will auto-launch with your generated model.
77
+
78
+ ---
app/app.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from pathlib import Path
3
+ from process import generate_script_and_run
4
+
5
+ # === File Paths ===
6
+ generated_dir = Path(__file__).parent / "generated"
7
+ fcstd_file = generated_dir / "model.FCStd"
8
+ obj_file = generated_dir / "model.obj"
9
+
10
+ # Remove stale files
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 still needs to be refined. For best results, run it locally. Toggle to view all in downloaded .FCStd file to see the generated part<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()
app/app_prompt.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ # Updated export logic with debug logging
37
+ EXPORT_SNIPPET = f"""
38
+ import Mesh
39
+ import os
40
+
41
+ print(">>> Running export snippet...")
42
+
43
+ try:
44
+ if App.ActiveDocument:
45
+ print(">>> Active document found")
46
+ doc = App.ActiveDocument
47
+ doc.recompute()
48
+ doc.saveAs(r"{fcstd_path_str}")
49
+ print(">>> Document saved")
50
+
51
+ objs = []
52
+ for obj in doc.Objects:
53
+ if hasattr(obj, "Shape"):
54
+ objs.append(obj)
55
+
56
+ print(f">>> Found {{len(objs)}} shape object(s)")
57
+
58
+ if objs:
59
+ Mesh.export(objs, r"{obj_path_str}")
60
+ print(">>> Exported OBJ file")
61
+
62
+ with open(r"{preview_path_str}", "w") as f:
63
+ f.write("Preview placeholder")
64
+
65
+ else:
66
+ print(">>> No active document!")
67
+
68
+ except Exception as e:
69
+ App.Console.PrintError("Export failed: " + str(e) + "\\n")
70
+ """
71
+
72
+ def generate_script_and_run(user_input: str):
73
+ # Load modular prompt parts
74
+ base_prompt_path = PROJECT_ROOT / "prompts/base_instruction.txt"
75
+ example_prompt_path = PROJECT_ROOT / "prompts/example_code.txt"
76
+ app_prompt_path = APP_DIR / "app_prompt.txt"
77
+
78
+ base_instruction = base_prompt_path.read_text(encoding="utf-8").strip()
79
+ example_code = example_prompt_path.read_text(encoding="utf-8").strip()
80
+ app_prompt = app_prompt_path.read_text(encoding="utf-8").strip()
81
+
82
+ # Build prompt
83
+ prompt = (
84
+ f"{base_instruction}\n\n"
85
+ f"{example_code}\n\n"
86
+ f"{app_prompt}\n\n"
87
+ f"User request: {user_input.strip()}"
88
+ )
89
+
90
+ # Generate LLM code
91
+ generated_code = prompt_llm(prompt)
92
+
93
+ # Auto-inject App.newDocument if missing
94
+ if "App.newDocument" not in generated_code:
95
+ generated_code = "App.newDocument('Unnamed')\n" + generated_code
96
+
97
+ # Clean up markdown formatting
98
+ # Clean up markdown formatting
99
+ if generated_code.startswith("```"):
100
+ generated_code = generated_code[generated_code.find("\n") + 1:].rsplit("```", 1)[0]
101
+
102
+ # Unwrap if __name__ == "__main__" blocks (FreeCAD won't execute them)
103
+ if "__name__" in generated_code and "def " in generated_code:
104
+ lines = generated_code.splitlines()
105
+ in_main = False
106
+ unwrapped = []
107
+ for line in lines:
108
+ if line.strip().startswith("if __name__"):
109
+ in_main = True
110
+ continue
111
+ if in_main:
112
+ # Remove leading indentation (usually 4 spaces)
113
+ unwrapped.append(line[4:] if line.startswith(" ") else line)
114
+ else:
115
+ unwrapped.append(line)
116
+ generated_code = "\n".join(unwrapped)
117
+
118
+ # Create output folder if needed
119
+ GEN_DIR.mkdir(exist_ok=True)
120
+
121
+ # Build final script with export logic
122
+ full_script = f"{generated_code.strip()}\n\n{EXPORT_SNIPPET}"
123
+
124
+ # Write script
125
+ GEN_SCRIPT.write_text(full_script, encoding="utf-8")
126
+
127
+ # Delete old outputs
128
+ for path in [FCSTD_PATH, OBJ_PATH, PREVIEW_PATH]:
129
+ if path.exists():
130
+ path.unlink()
131
+
132
+ # Run FreeCAD
133
+ freecad_cmd = get_freecad_cmd()
134
+ try:
135
+ result = subprocess.run(
136
+ [freecad_cmd, str(GEN_SCRIPT)],
137
+ cwd=APP_DIR,
138
+ capture_output=True,
139
+ text=True,
140
+ timeout=60
141
+ )
142
+
143
+ # Log output
144
+ (GEN_DIR / "run_stdout.txt").write_text(result.stdout or "", encoding="utf-8")
145
+ (GEN_DIR / "run_stderr.txt").write_text(result.stderr or "", encoding="utf-8")
146
+
147
+ if result.returncode != 0:
148
+ raise RuntimeError(result.stderr or result.stdout)
149
+
150
+ if not FCSTD_PATH.exists() or not OBJ_PATH.exists():
151
+ raise FileNotFoundError("One or more output files not created.")
152
+
153
+ except Exception as e:
154
+ FCSTD_PATH.write_text(f"Error: {e}")
155
+ PREVIEW_PATH.write_text(f"Error: {e}")
156
+
157
+ return str(FCSTD_PATH), str(PREVIEW_PATH)
generated/result_script.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import FreeCAD as App
2
+ import FreeCADGui as Gui
3
+ from FreeCAD import Vector
4
+ import math
5
+
6
+
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
+
60
+ for i in range(NUM_BOLT_HOLES):
61
+ angle_deg = 360 * i / NUM_BOLT_HOLES
62
+ angle_rad = math.radians(angle_deg)
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")
83
+
84
+ return doc
85
+
86
+ if __name__ == "__main__":
87
+ createFlangeAssembly()
88
+
main.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from src.llm_client import prompt_llm
2
+ from pathlib import Path
3
+ 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
+
11
+ # Snippet to adjust FreeCAD GUI view
12
+ GUI_SNIPPET = """
13
+ import FreeCADGui
14
+ FreeCADGui.activeDocument().activeView().viewAxometric()
15
+ FreeCADGui.SendMsgToActiveView("ViewFit")
16
+ """
17
+
18
+ def main():
19
+ # Step 1: Get user input
20
+ user_input = input("Describe your FreeCAD part: ")
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\nExamples:\n{example_prompt}\n\nUser instruction: {user_input.strip()}"
26
+
27
+
28
+ # Step 3: Get response from LLM
29
+ generated_code = prompt_llm(full_prompt)
30
+
31
+ # Step 4: Clean up ```python code blocks if any
32
+ if generated_code.startswith("```"):
33
+ generated_code = generated_code.strip("`\n ")
34
+ if generated_code.lower().startswith("python"):
35
+ generated_code = generated_code[len("python"):].lstrip()
36
+
37
+ # Step 5: Append GUI snippet for viewing
38
+ generated_code += "\n\n" + GUI_SNIPPET
39
+
40
+ # Step 6: Save to script file
41
+ GEN_SCRIPT.write_text(generated_code)
42
+ print(f"\n Code generated and written to {GEN_SCRIPT}")
43
+
44
+ # Step 7: Execute the script via FreeCAD
45
+ print("Running FreeCAD with the generated script...")
46
+ try:
47
+ subprocess.run(["python", str(RUN_SCRIPT)], check=True)
48
+ except subprocess.CalledProcessError as e:
49
+ print(f"❌ FreeCAD script execution failed with error code: {e.returncode}")
50
+ except Exception as e:
51
+ print(f"❌ Error running run_freecad.py: {e}")
52
+
53
+ if __name__ == "__main__":
54
+ main()
prompts/base_instruction.txt ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ You are an assistant that generates valid Python code for FreeCAD.
2
+
3
+ - Use Part and Sketcher modules.
4
+ - Do not use GUI commands unless required.
5
+ - Make sure the code can be executed using FreeCAD.
6
+ - make code according to FreeCAD 1.0
7
+ - make the parts using the freecad part workbench, not partdesign workbench
8
+ - ensure all dimensions are correct and parts dont intersect each other
9
+ - Always use from FreeCAD import Vector instead of import FreeCAD.Vector when importing the Vector class in FreeCAD. This is the correct and preferred method.
10
+ - Always use from FreeCAD import Placement instead of import FreeCAD.Placement when importing the Placement class in FreeCAD. This is the correct and preferred method.
11
+ - Always use from FreeCAD import Rotation instead of import FreeCAD.Rotation when importing the Rotation class in FreeCAD. This is the correct and preferred method.
12
+ - use the fuse() function instead of Part.Union(). Use it only where necessary. Only when it is necessary to combine parts
13
+ - make the design tree as well for all parts. In the design tree, I must be able to change the dimension and placement of parts.
14
+ - dont make several features and then copy and make one feature at the end.
15
+ - Make the generated object visible
16
+ - try to make the design as modular as possible. for example, if i need to generate a cup, make the cup body as one feature in design tree and handle as other feature. in this way, i can edit the dimensions of both separately.
17
+ - There is no built-in Part.makeTube function in FreeCAD. You're trying to create a hollow cylinder (tube) directly with parameters like outer_radius, inner_radius, height, but FreeCAD expects shapes, not numbers.
18
+ - dont write comments
prompts/example_code.txt ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"
pyproject.toml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "cadomatic"
3
+ version = "0.1.0"
4
+ description = "This is a code to create editable CAD files from natural language prompts"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "python-dotenv>=1.1.1",
9
+ "langchain>=0.3.27",
10
+ "beautifulsoup4>=4.13.4",
11
+ "langchain-google-genai>=2.1.8",
12
+ "langchain-community>=0.3.27",
13
+ "faiss-cpu>=1.11.0",
14
+ "huggingface-hub>=0.34.3",
15
+ "gradio>=5.39.0",
16
+ ]
requirements.txt ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # This file was autogenerated by uv via the following command:
2
+ # uv pip compile pyproject.toml --output-file requirements.txt
3
+ aiofiles==24.1.0
4
+ # via gradio
5
+ aiohappyeyeballs==2.6.1
6
+ # via aiohttp
7
+ aiohttp==3.12.15
8
+ # via langchain-community
9
+ aiosignal==1.4.0
10
+ # via aiohttp
11
+ annotated-types==0.7.0
12
+ # via pydantic
13
+ anyio==4.10.0
14
+ # via
15
+ # gradio
16
+ # httpx
17
+ # starlette
18
+ attrs==25.3.0
19
+ # via aiohttp
20
+ beautifulsoup4==4.13.4
21
+ # via cadomatic (pyproject.toml)
22
+ brotli==1.1.0
23
+ # via gradio
24
+ cachetools==5.5.2
25
+ # via google-auth
26
+ certifi==2025.8.3
27
+ # via
28
+ # httpcore
29
+ # httpx
30
+ # requests
31
+ charset-normalizer==3.4.2
32
+ # via requests
33
+ click==8.2.1
34
+ # via
35
+ # typer
36
+ # uvicorn
37
+ colorama==0.4.6
38
+ # via
39
+ # click
40
+ # tqdm
41
+ dataclasses-json==0.6.7
42
+ # via langchain-community
43
+ faiss-cpu==1.11.0.post1
44
+ # via cadomatic (pyproject.toml)
45
+ fastapi==0.116.1
46
+ # via gradio
47
+ ffmpy==0.6.1
48
+ # via gradio
49
+ filelock==3.18.0
50
+ # via huggingface-hub
51
+ filetype==1.2.0
52
+ # via langchain-google-genai
53
+ frozenlist==1.7.0
54
+ # via
55
+ # aiohttp
56
+ # aiosignal
57
+ fsspec==2025.7.0
58
+ # via
59
+ # gradio-client
60
+ # huggingface-hub
61
+ google-ai-generativelanguage==0.6.18
62
+ # via langchain-google-genai
63
+ google-api-core==2.25.1
64
+ # via google-ai-generativelanguage
65
+ google-auth==2.40.3
66
+ # via
67
+ # google-ai-generativelanguage
68
+ # google-api-core
69
+ googleapis-common-protos==1.70.0
70
+ # via
71
+ # google-api-core
72
+ # grpcio-status
73
+ gradio==5.41.1
74
+ # via cadomatic (pyproject.toml)
75
+ gradio-client==1.11.0
76
+ # via gradio
77
+ greenlet==3.2.4
78
+ # via sqlalchemy
79
+ groovy==0.1.2
80
+ # via gradio
81
+ grpcio==1.74.0
82
+ # via
83
+ # google-api-core
84
+ # grpcio-status
85
+ grpcio-status==1.74.0
86
+ # via google-api-core
87
+ h11==0.16.0
88
+ # via
89
+ # httpcore
90
+ # uvicorn
91
+ httpcore==1.0.9
92
+ # via httpx
93
+ httpx==0.28.1
94
+ # via
95
+ # gradio
96
+ # gradio-client
97
+ # langsmith
98
+ # safehttpx
99
+ httpx-sse==0.4.1
100
+ # via langchain-community
101
+ huggingface-hub==0.34.3
102
+ # via
103
+ # cadomatic (pyproject.toml)
104
+ # gradio
105
+ # gradio-client
106
+ idna==3.10
107
+ # via
108
+ # anyio
109
+ # httpx
110
+ # requests
111
+ # yarl
112
+ jinja2==3.1.6
113
+ # via gradio
114
+ jsonpatch==1.33
115
+ # via langchain-core
116
+ jsonpointer==3.0.0
117
+ # via jsonpatch
118
+ langchain==0.3.27
119
+ # via
120
+ # cadomatic (pyproject.toml)
121
+ # langchain-community
122
+ langchain-community==0.3.27
123
+ # via cadomatic (pyproject.toml)
124
+ langchain-core==0.3.73
125
+ # via
126
+ # langchain
127
+ # langchain-community
128
+ # langchain-google-genai
129
+ # langchain-text-splitters
130
+ langchain-google-genai==2.1.9
131
+ # via cadomatic (pyproject.toml)
132
+ langchain-text-splitters==0.3.9
133
+ # via langchain
134
+ langsmith==0.4.13
135
+ # via
136
+ # langchain
137
+ # langchain-community
138
+ # langchain-core
139
+ markdown-it-py==3.0.0
140
+ # via rich
141
+ markupsafe==3.0.2
142
+ # via
143
+ # gradio
144
+ # jinja2
145
+ marshmallow==3.26.1
146
+ # via dataclasses-json
147
+ mdurl==0.1.2
148
+ # via markdown-it-py
149
+ multidict==6.6.3
150
+ # via
151
+ # aiohttp
152
+ # yarl
153
+ mypy-extensions==1.1.0
154
+ # via typing-inspect
155
+ numpy==2.3.2
156
+ # via
157
+ # faiss-cpu
158
+ # gradio
159
+ # langchain-community
160
+ # pandas
161
+ orjson==3.11.1
162
+ # via
163
+ # gradio
164
+ # langsmith
165
+ packaging==25.0
166
+ # via
167
+ # faiss-cpu
168
+ # gradio
169
+ # gradio-client
170
+ # huggingface-hub
171
+ # langchain-core
172
+ # langsmith
173
+ # marshmallow
174
+ pandas==2.3.1
175
+ # via gradio
176
+ pillow==11.3.0
177
+ # via gradio
178
+ propcache==0.3.2
179
+ # via
180
+ # aiohttp
181
+ # yarl
182
+ proto-plus==1.26.1
183
+ # via
184
+ # google-ai-generativelanguage
185
+ # google-api-core
186
+ protobuf==6.31.1
187
+ # via
188
+ # google-ai-generativelanguage
189
+ # google-api-core
190
+ # googleapis-common-protos
191
+ # grpcio-status
192
+ # proto-plus
193
+ pyasn1==0.6.1
194
+ # via
195
+ # pyasn1-modules
196
+ # rsa
197
+ pyasn1-modules==0.4.2
198
+ # via google-auth
199
+ pydantic==2.11.7
200
+ # via
201
+ # fastapi
202
+ # gradio
203
+ # langchain
204
+ # langchain-core
205
+ # langchain-google-genai
206
+ # langsmith
207
+ # pydantic-settings
208
+ pydantic-core==2.33.2
209
+ # via pydantic
210
+ pydantic-settings==2.10.1
211
+ # via langchain-community
212
+ pydub==0.25.1
213
+ # via gradio
214
+ pygments==2.19.2
215
+ # via rich
216
+ python-dateutil==2.9.0.post0
217
+ # via pandas
218
+ python-dotenv==1.1.1
219
+ # via
220
+ # cadomatic (pyproject.toml)
221
+ # pydantic-settings
222
+ python-multipart==0.0.20
223
+ # via gradio
224
+ pytz==2025.2
225
+ # via pandas
226
+ pyyaml==6.0.2
227
+ # via
228
+ # gradio
229
+ # huggingface-hub
230
+ # langchain
231
+ # langchain-community
232
+ # langchain-core
233
+ requests==2.32.4
234
+ # via
235
+ # google-api-core
236
+ # huggingface-hub
237
+ # langchain
238
+ # langchain-community
239
+ # langsmith
240
+ # requests-toolbelt
241
+ requests-toolbelt==1.0.0
242
+ # via langsmith
243
+ rich==14.1.0
244
+ # via typer
245
+ rsa==4.9.1
246
+ # via google-auth
247
+ ruff==0.12.7
248
+ # via gradio
249
+ safehttpx==0.1.6
250
+ # via gradio
251
+ semantic-version==2.10.0
252
+ # via gradio
253
+ shellingham==1.5.4
254
+ # via typer
255
+ six==1.17.0
256
+ # via python-dateutil
257
+ sniffio==1.3.1
258
+ # via anyio
259
+ soupsieve==2.7
260
+ # via beautifulsoup4
261
+ sqlalchemy==2.0.42
262
+ # via
263
+ # langchain
264
+ # langchain-community
265
+ starlette==0.47.2
266
+ # via
267
+ # fastapi
268
+ # gradio
269
+ tenacity==9.1.2
270
+ # via
271
+ # langchain-community
272
+ # langchain-core
273
+ tomlkit==0.13.3
274
+ # via gradio
275
+ tqdm==4.67.1
276
+ # via huggingface-hub
277
+ typer==0.16.0
278
+ # via gradio
279
+ typing-extensions==4.14.1
280
+ # via
281
+ # aiosignal
282
+ # anyio
283
+ # beautifulsoup4
284
+ # fastapi
285
+ # gradio
286
+ # gradio-client
287
+ # huggingface-hub
288
+ # langchain-core
289
+ # pydantic
290
+ # pydantic-core
291
+ # sqlalchemy
292
+ # starlette
293
+ # typer
294
+ # typing-inspect
295
+ # typing-inspection
296
+ typing-inspect==0.9.0
297
+ # via dataclasses-json
298
+ typing-inspection==0.4.1
299
+ # via
300
+ # pydantic
301
+ # pydantic-settings
302
+ tzdata==2025.2
303
+ # via pandas
304
+ urllib3==2.5.0
305
+ # via requests
306
+ uvicorn==0.35.0
307
+ # via gradio
308
+ websockets==15.0.1
309
+ # via gradio-client
310
+ yarl==1.20.1
311
+ # via aiohttp
312
+ zstandard==0.23.0
313
+ # via langsmith
src/__init__.py ADDED
File without changes
src/llm_client.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 dotenv import load_dotenv
7
+ import google.generativeai as genai
8
+
9
+ # Load API key
10
+ load_dotenv()
11
+ genai.configure(api_key=os.environ.get("GEMINI_API_KEY"))
12
+
13
+ # Set up Hugging Face repo and files
14
+ REPO_ID = "Yas1n/CADomatic_vectorstore"
15
+ FILENAME_FAISS = "index.faiss"
16
+ FILENAME_PKL = "index.pkl"
17
+
18
+ # Download once (uses HF cache internally)
19
+ faiss_path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME_FAISS)
20
+ pkl_path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME_PKL)
21
+
22
+ # Use same folder for both files
23
+ download_dir = Path(faiss_path).parent
24
+
25
+ # Load vectorstore
26
+ embedding = GoogleGenerativeAIEmbeddings(model="models/embedding-001")
27
+ vectorstore = FAISS.load_local(str(download_dir), embeddings=embedding, allow_dangerous_deserialization=True)
28
+ retriever = vectorstore.as_retriever(search_kwargs={"k": 40})
29
+
30
+ # Gemini 2.5 Flash
31
+ llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=1.2)
32
+
33
+ def prompt_llm(user_prompt: str) -> str:
34
+ docs = retriever.invoke(user_prompt)
35
+ context = "\n\n".join(doc.page_content for doc in docs)
36
+
37
+ final_prompt = f"""
38
+ You are a helpful assistant that writes FreeCAD Python scripts from CAD instructions.
39
+ Use the following FreeCAD wiki documentation as context:
40
+
41
+ {context}
42
+
43
+ Instruction:
44
+ {user_prompt}
45
+
46
+ Respond with valid FreeCAD Python code only, no extra commentary.
47
+ """
48
+
49
+ try:
50
+ response = llm.invoke(final_prompt)
51
+ return response.content
52
+ except Exception as e:
53
+ print("❌ Error generating FreeCAD code:", e)
54
+ return ""
src/rag_builder.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ from bs4 import BeautifulSoup
4
+ from urllib.parse import urljoin
5
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
6
+ from langchain_community.vectorstores import FAISS
7
+ from langchain_google_genai import GoogleGenerativeAIEmbeddings
8
+
9
+ # --- Step 1: ENV setup ---
10
+ from dotenv import load_dotenv
11
+ import google.generativeai as genai
12
+
13
+ load_dotenv()
14
+ genai.configure(api_key=os.environ.get("GEMINI_API_KEY"))
15
+
16
+ # --- Step 2: Crawler ---
17
+ BASE_URL_WIKI = "https://wiki.freecad.org/Power_users_hub"
18
+ BASE_URL_GITHUB = "https://github.com/shaise/FreeCAD_FastenersWB"
19
+
20
+ DOMAIN_WHITELIST = [
21
+ "https://wiki.freecad.org",
22
+ "https://github.com/shaise"
23
+ ]
24
+
25
+ # List of language identifiers to exclude (only for wiki)
26
+ LANG_IDENTIFIERS = [
27
+ "/id", "/de", "/tr", "/es", "/fr", "/hr", "/it", "/pl",
28
+ "/pt", "/pt-br", "/ro", "/fi", "/sv", "/cs", "/ru", "/zh-cn",
29
+ "/zh-tw", "/ja", "/ko"
30
+ ]
31
+
32
+ def is_excluded_url(url):
33
+ url_lower = url.lower()
34
+
35
+ # Apply language filters only to FreeCAD wiki URLs
36
+ if "wiki.freecad.org" in url_lower:
37
+ if any(lang in url_lower for lang in LANG_IDENTIFIERS):
38
+ return True
39
+
40
+ return (
41
+ ".jpg" in url_lower or
42
+ ".png" in url_lower or
43
+ "edit&section" in url_lower
44
+ )
45
+
46
+ def crawl_wiki(start_url, max_pages):
47
+ visited = set()
48
+ to_visit = [start_url]
49
+ pages = []
50
+
51
+ while to_visit and len(visited) < max_pages:
52
+ url = to_visit.pop(0)
53
+ if url in visited or is_excluded_url(url):
54
+ continue
55
+ try:
56
+ print(f"Fetching: {url}")
57
+ res = requests.get(url)
58
+ res.raise_for_status()
59
+ soup = BeautifulSoup(res.text, "html.parser")
60
+ visited.add(url)
61
+
62
+ for tag in soup(["script", "style", "header", "footer", "nav", "aside"]):
63
+ tag.extract()
64
+ text = soup.get_text(separator="\n")
65
+ clean = "\n".join([line.strip() for line in text.splitlines() if line.strip()])
66
+ pages.append({"url": url, "text": clean})
67
+
68
+ # Queue internal links
69
+ for a in soup.find_all("a", href=True):
70
+ full = urljoin(url, a["href"])
71
+ if any(full.startswith(domain) for domain in DOMAIN_WHITELIST):
72
+ if full not in visited and not is_excluded_url(full):
73
+ to_visit.append(full)
74
+ except Exception as e:
75
+ print(f"Error fetching {url}: {e}")
76
+
77
+ print(f"Crawled {len(pages)} pages from {start_url}")
78
+ return pages
79
+
80
+ # --- Step 3: RAG Build ---
81
+ def build_vectorstore():
82
+ wiki_pages = crawl_wiki(BASE_URL_WIKI, max_pages=2000) # Uncomment if you want both
83
+ github_pages = crawl_wiki(BASE_URL_GITHUB, max_pages=450)
84
+ pages = wiki_pages + github_pages
85
+
86
+ if not pages:
87
+ print("No pages crawled. Exiting.")
88
+ return
89
+
90
+ texts = [p["text"] for p in pages]
91
+ metadatas = [{"source": p["url"]} for p in pages]
92
+
93
+ splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=150)
94
+ docs = splitter.create_documents(texts, metadatas=metadatas)
95
+
96
+ print(f"Split into {len(docs)} chunks")
97
+
98
+ embeddings = GoogleGenerativeAIEmbeddings(model="models/embedding-001")
99
+ vectorstore = FAISS.from_documents(docs, embeddings)
100
+
101
+ src_path = os.path.dirname(os.path.abspath(__file__))
102
+ root_dir_path = os.path.dirname(src_path)
103
+ vectorstore_path = os.path.join(root_dir_path, "vectorstore")
104
+
105
+ os.makedirs(vectorstore_path, exist_ok=True)
106
+ vectorstore.save_local(vectorstore_path)
107
+ print("Vectorstore saved to ./vectorstore")
108
+
109
+ if __name__ == "__main__":
110
+ build_vectorstore()
src/run_freecad.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # run_freecad.py
2
+ import subprocess
3
+ from pathlib import Path
4
+
5
+ freecad_exe = r"C:\Program Files\FreeCAD 1.0\bin\freecad.exe"
6
+ script_path = Path("generated/result_script.py")
7
+
8
+ if not script_path.exists():
9
+ raise FileNotFoundError("Generated script not found. Run main.py first.")
10
+
11
+ subprocess.run([freecad_exe, str(script_path)], check=True)
uv.lock ADDED
The diff for this file is too large to render. See raw diff