Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9abeb6266a | |||
| 6b6f7de5ab | |||
| b184ade967 | |||
| 67b73c13b8 | |||
| 813ddc3596 | |||
| 37e5335446 | |||
| 108ad2d5b5 | |||
| 1eee203ba8 | |||
| a00129603c | |||
| 3d63f033f2 | |||
| b29bc11b42 | |||
| 79ad85594e | |||
| 655df71cd8 | |||
| 7072dcee08 | |||
| 1e9b0cab1c | |||
| a7e7a9f2c3 | |||
| 48042659fc | |||
| baa2fd5d47 | |||
| 0daa6152ee | |||
| 9f1c29d319 | |||
| 2037106c0d | |||
| d6e829c23d | |||
| 742d06d242 | |||
| c78d0af78c | |||
| dda9db822b | |||
| 9f1387fe68 | |||
| 210e3cfb5d | |||
| b8516fff91 | |||
| d7e5929a13 | |||
| 2b2afbc479 | |||
| b0aebdc04f | |||
| be22c44a3f | |||
| 80ba3cc70a | |||
| 5269c0897c | |||
| 3a169007f7 | |||
| b595b88e04 | |||
| 9f10a5c5e5 | |||
| 6ba742ddf5 | |||
| 01833e4af2 | |||
| f860ff3e77 | |||
| 9938f4ddd4 | |||
| f6422e0847 | |||
| f8f16ea800 | |||
| 54ac2c098a | |||
| ea34e7e29d | |||
| 7091f530ee | |||
| 75d4820292 | |||
| d52106a48a | |||
| d7ebbf45d5 | |||
| daed79dac6 | |||
| e13769840b | |||
| 5371bf7c38 | |||
| bf00310889 | |||
| 8c6a413137 | |||
| fe23ca610c | |||
| d6044e551a | |||
| 11d053fda4 | |||
| 54261bb8fd | |||
| d373b50644 | |||
| 0a9d557ce0 | |||
| 15cc30edac |
+74
-1
@@ -1,3 +1,76 @@
|
||||
*.xml
|
||||
*.iml
|
||||
.idea
|
||||
.idea
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# Lock files
|
||||
uv.lock
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
/src/fluency/Tesfiles/bordo_adapter3.fluency
|
||||
/CONSTRAINT_STATUS_FINAL.md
|
||||
/CONSTRAINT_STATUS_IMPLEMENTATION.md
|
||||
/src/fluency/rendering/first.png
|
||||
/littlebrother.md
|
||||
/src/fluency/Tesfiles/multiboidy.fluency
|
||||
/src/fluency/rendering/nromal_test.png
|
||||
/package.json
|
||||
/package-lock.json
|
||||
/src/fluency/Screenshot 2026-06-28 at 17.57.52.png
|
||||
/src/fluency/rendering/Screenshot 2026-07-12 at 16.54.14.png
|
||||
/src/fluency/Screenshot 2026-07-26 at 20.23.42.png
|
||||
/Screenshot 2026-08-05 at 10.14.15.png
|
||||
/Screenshot 2026-08-05 at 11.15.33.png
|
||||
/Screenshot 2026-08-05 at 11.22.42.png
|
||||
/SURFACE_MODIFIER_PLAN.md
|
||||
/test.step
|
||||
/src/fluency/test333.step
|
||||
/src/fluency/tests/test_arc_attached_to_rectangle.py
|
||||
/src/fluency/tests/test_array_pattern.py
|
||||
/src/fluency/tests/test_chamfer.py
|
||||
/src/fluency/tests/test_circle_diameter_constraint.py
|
||||
/tests/test_distance_constraint_picking.py
|
||||
/tests/test_extrude_geometry.py
|
||||
/src/fluency/tests/test_feature_replay.py
|
||||
/src/fluency/tests/test_fillet.py
|
||||
/tests/test_mirror.py
|
||||
/test_modifier.py
|
||||
/src/fluency/tests/test_projection_constraints.py
|
||||
/tests/test_re_extrude.py
|
||||
/scripts/test_render_zoom.py
|
||||
/test_thread.py
|
||||
/test_thread_hole.step
|
||||
/test_thread_m3.step
|
||||
/test_thread_m5_hole.step
|
||||
/test_thread_shaft.step
|
||||
/test_thread_tilted.step
|
||||
/src/fluency/testpart.step
|
||||
|
||||
Generated
-11
@@ -1,11 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/sdfcad" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Generated
+1
-1
@@ -3,7 +3,7 @@
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.11 (fluency)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (fluency)" project-jdk-type="Python SDK" />
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (fluency)" project-jdk-type="Python SDK" />
|
||||
<component name="PyCharmProfessionalAdvertiser">
|
||||
<option name="shown" value="true" />
|
||||
</component>
|
||||
|
||||
Generated
+1
-1
@@ -2,7 +2,7 @@
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/fluency.iml" filepath="$PROJECT_DIR$/.idea/fluency.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/fluency-cad.iml" filepath="$PROJECT_DIR$/.idea/fluency-cad.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
-1
@@ -2,6 +2,5 @@
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
<mapping directory="$PROJECT_DIR$/sdfcad" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+482
-45
@@ -4,23 +4,17 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="8f0bafd6-58a0-4b20-aa2b-ddc3ba278873" name="Changes" comment="init">
|
||||
<change afterPath="$PROJECT_DIR$/.idea/fluency.iml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/Project_Default.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/profiles_settings.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/vcs.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/2dtest.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/fluencyb.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/main.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/meshtest.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/modules/gl_widget.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/modules/out.stl" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/side_fluency.py" afterDir="false" />
|
||||
<change afterPath="$PROJECT_DIR$/vulkan.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/fluency.py" beforeDir="false" afterPath="$PROJECT_DIR$/fluency.py" afterDir="false" />
|
||||
<list default="true" id="8f0bafd6-58a0-4b20-aa2b-ddc3ba278873" name="Changes" comment="Fiexed highlighting of operations">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/fluency/geometry_occ/sketch.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/geometry_occ/sketch.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/fluency/io/project_io.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/io/project_io.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/fluency/models/data_model.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/models/data_model.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/fluency/rendering/occ_renderer.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/rendering/occ_renderer.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/fluency/ui/main_window.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/main_window.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/fluency/ui/render_window.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/render_window.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/fluency/ui/sketch_widget.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/sketch_widget.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/fluency/ui/technical_drawing_widget.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/technical_drawing_widget.py" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/src/fluency/ui/viewer_widget.py" beforeDir="false" afterPath="$PROJECT_DIR$/src/fluency/ui/viewer_widget.py" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -35,6 +29,11 @@
|
||||
</option>
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_BRANCH_BY_REPOSITORY">
|
||||
<map>
|
||||
<entry key="$PROJECT_DIR$" value="feature/surface-modifier-pyramid-patterns" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
<option name="ROOT_SYNC" value="DONT_SYNC" />
|
||||
</component>
|
||||
@@ -48,29 +47,52 @@
|
||||
"associatedIndex": 6
|
||||
}</component>
|
||||
<component name="ProjectId" id="2aDywQvESFCKbJK4JUVHIhkN4S6" />
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"Python.2dtest.executor": "Run",
|
||||
"Python.3d_windows.executor": "Run",
|
||||
"Python.fluency.executor": "Run",
|
||||
"Python.fluencyb.executor": "Run",
|
||||
"Python.gl_widget.executor": "Run",
|
||||
"Python.main.executor": "Run",
|
||||
"Python.meshtest.executor": "Run",
|
||||
"Python.side_fluency.executor": "Run",
|
||||
"Python.vulkan.executor": "Run",
|
||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"last_opened_file_path": "/Volumes/Data_drive/Programming/fluency/modules",
|
||||
"settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings"
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
"Python.2dtest.executor": "Run",
|
||||
"Python.3d_windows.executor": "Run",
|
||||
"Python.Unnamed.executor": "Run",
|
||||
"Python.base.executor": "Run",
|
||||
"Python.data_model.executor": "Run",
|
||||
"Python.debug_dragging.executor": "Run",
|
||||
"Python.draw_widget2d.executor": "Run",
|
||||
"Python.draw_widget_solve.executor": "Run",
|
||||
"Python.fluency.executor": "Run",
|
||||
"Python.fluencyb.executor": "Run",
|
||||
"Python.gl_widget.executor": "Run",
|
||||
"Python.gui_ui.executor": "Run",
|
||||
"Python.kernel.executor": "Run",
|
||||
"Python.main.executor": "Run",
|
||||
"Python.main_window.executor": "Run",
|
||||
"Python.meshtest.executor": "Run",
|
||||
"Python.occ_renderer.executor": "Run",
|
||||
"Python.occ_to_mesh.executor": "Run",
|
||||
"Python.render_backend.executor": "Run",
|
||||
"Python.side_fluency.executor": "Run",
|
||||
"Python.simple_mesh.executor": "Run",
|
||||
"Python.sketch.executor": "Run",
|
||||
"Python.technical_drawing_widget.executor": "Run",
|
||||
"Python.vtk_widget.executor": "Run",
|
||||
"Python.vulkan.executor": "Run",
|
||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"codeWithMe.voiceChat.enabledByDefault": "false",
|
||||
"git-widget-placeholder": "feature/occ-migration",
|
||||
"last_opened_file_path": "/Volumes/Data_drive/Programming/fluency/src/fluency",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"settings.editor.selected.configurable": "project.propVCSSupport.DirectoryMappings"
|
||||
}
|
||||
}]]></component>
|
||||
}</component>
|
||||
<component name="QodanaReportsService">
|
||||
<option name="descriptions">
|
||||
<ReportDescription localRun="true" path="/private/var/folders/kg/zm48w_r96yb68mlbzvb9gtq40000gn/T/qodana_output/qodana.sarif.json" reportGuid="5f5b823c-c594-48c5-ae1f-062e30303918" reportId="fluency/qodana/2024-02-04" />
|
||||
@@ -78,7 +100,11 @@
|
||||
</component>
|
||||
<component name="RecentsManager">
|
||||
<key name="CopyFile.RECENT_KEYS">
|
||||
<recent name="$PROJECT_DIR$/modules" />
|
||||
<recent name="$PROJECT_DIR$/src/fluency" />
|
||||
<recent name="$PROJECT_DIR$" />
|
||||
<recent name="$PROJECT_DIR$/src/fluency/Tesfiles" />
|
||||
<recent name="$PROJECT_DIR$/src/fluency/rendering" />
|
||||
<recent name="$PROJECT_DIR$/drawing_modules" />
|
||||
</key>
|
||||
<key name="MoveFile.RECENT_KEYS">
|
||||
<recent name="$PROJECT_DIR$" />
|
||||
@@ -87,7 +113,7 @@
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<set>
|
||||
<option value="bundled-python-sdk-09665e90c3a7-d3b881c8e49f-com.jetbrains.pycharm.community.sharedIndexes.bundled-PC-233.15026.15" />
|
||||
<option value="bundled-python-sdk-c59985aa861c-c2ffad84badb-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-261.24374.152" />
|
||||
</set>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
@@ -100,17 +126,404 @@
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1703867682707</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="init">
|
||||
<task id="LOCAL-00007" summary="- changing compos for sketches works">
|
||||
<option name="closed" value="true" />
|
||||
<created>1703951701948</created>
|
||||
<option name="number" value="00001" />
|
||||
<option name="presentableId" value="LOCAL-00001" />
|
||||
<created>1735652081552</created>
|
||||
<option name="number" value="00007" />
|
||||
<option name="presentableId" value="LOCAL-00007" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1703951701948</updated>
|
||||
<updated>1735652081552</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="2" />
|
||||
<task id="LOCAL-00008" summary="- changing compos including sketches and bodies">
|
||||
<option name="closed" value="true" />
|
||||
<created>1735662119176</created>
|
||||
<option name="number" value="00008" />
|
||||
<option name="presentableId" value="LOCAL-00008" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1735662119176</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00009" summary="- Drawing bodys depending on the selected compo - Cut working - Edit sketch working">
|
||||
<option name="closed" value="true" />
|
||||
<created>1735685300102</created>
|
||||
<option name="number" value="00009" />
|
||||
<option name="presentableId" value="LOCAL-00009" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1735685300102</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00010" summary="- delete sketch working - added mid point snap - added hovering line with distance">
|
||||
<option name="closed" value="true" />
|
||||
<created>1735763743346</created>
|
||||
<option name="number" value="00010" />
|
||||
<option name="presentableId" value="LOCAL-00010" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1735763743346</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00011" summary="- Added new buttons and settings">
|
||||
<option name="closed" value="true" />
|
||||
<created>1735825176611</created>
|
||||
<option name="number" value="00011" />
|
||||
<option name="presentableId" value="LOCAL-00011" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1735825176611</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00012" summary="- Added new buttons and settings">
|
||||
<option name="closed" value="true" />
|
||||
<created>1735842523870</created>
|
||||
<option name="number" value="00012" />
|
||||
<option name="presentableId" value="LOCAL-00012" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1735842523870</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00013" summary="- Added construction lines switching - Moved callbacks into sketchwidget from main. - Changed reset on right click">
|
||||
<option name="closed" value="true" />
|
||||
<created>1739739664763</created>
|
||||
<option name="number" value="00013" />
|
||||
<option name="presentableId" value="LOCAL-00013" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1739739664763</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00014" summary="- Added contrain displayed next to line - Slight change to point check from solver.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1743193041868</created>
|
||||
<option name="number" value="00014" />
|
||||
<option name="presentableId" value="LOCAL-00014" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1743193041868</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00015" summary="- Added enabling of midpsnap and prepared others - Show dimesnion on hover">
|
||||
<option name="closed" value="true" />
|
||||
<created>1743284173326</created>
|
||||
<option name="number" value="00015" />
|
||||
<option name="presentableId" value="LOCAL-00015" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1743284173326</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00016" summary="- Fixed redraw when component changed">
|
||||
<option name="closed" value="true" />
|
||||
<created>1744555255868</created>
|
||||
<option name="number" value="00016" />
|
||||
<option name="presentableId" value="LOCAL-00016" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1744555255868</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00017" summary="- added MIT license">
|
||||
<option name="closed" value="true" />
|
||||
<created>1748764814845</created>
|
||||
<option name="number" value="00017" />
|
||||
<option name="presentableId" value="LOCAL-00017" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1748764814845</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00018" summary="- added screenshot">
|
||||
<option name="closed" value="true" />
|
||||
<created>1748765318267</created>
|
||||
<option name="number" value="00018" />
|
||||
<option name="presentableId" value="LOCAL-00018" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1748765318267</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00019" summary="- added sdf folder ( doesnt work via pip or git=)">
|
||||
<option name="closed" value="true" />
|
||||
<created>1755369224187</created>
|
||||
<option name="number" value="00019" />
|
||||
<option name="presentableId" value="LOCAL-00019" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1755369224187</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00020" summary="- Tons of addtions">
|
||||
<option name="closed" value="true" />
|
||||
<created>1782673954850</created>
|
||||
<option name="number" value="00020" />
|
||||
<option name="presentableId" value="LOCAL-00020" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1782673954850</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00021" summary="- Tons of addtions">
|
||||
<option name="closed" value="true" />
|
||||
<created>1782679912834</created>
|
||||
<option name="number" value="00021" />
|
||||
<option name="presentableId" value="LOCAL-00021" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1782679912834</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00022" summary="- Basic operations">
|
||||
<option name="closed" value="true" />
|
||||
<created>1782768610475</created>
|
||||
<option name="number" value="00022" />
|
||||
<option name="presentableId" value="LOCAL-00022" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1782768610475</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00023" summary="- removed cadquery deoendency">
|
||||
<option name="closed" value="true" />
|
||||
<created>1782928990792</created>
|
||||
<option name="number" value="00023" />
|
||||
<option name="presentableId" value="LOCAL-00023" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1782928990792</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00024" summary="- sketch enhacements">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783108151675</created>
|
||||
<option name="number" value="00024" />
|
||||
<option name="presentableId" value="LOCAL-00024" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783108151676</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00025" summary="- sketch enhacements">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783159860774</created>
|
||||
<option name="number" value="00025" />
|
||||
<option name="presentableId" value="LOCAL-00025" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783159860774</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00026" summary="- UI refinement, button position ui file as source no dirty drafting anymore">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783174566362</created>
|
||||
<option name="number" value="00026" />
|
||||
<option name="presentableId" value="LOCAL-00026" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783174566362</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00027" summary="- assembly draft">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783239410744</created>
|
||||
<option name="number" value="00027" />
|
||||
<option name="presentableId" value="LOCAL-00027" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783239410744</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00028" summary="- assembly draft">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783272988957</created>
|
||||
<option name="number" value="00028" />
|
||||
<option name="presentableId" value="LOCAL-00028" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783272988957</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00029" summary="- Added save file foramt - Split main.py refactor">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783282570014</created>
|
||||
<option name="number" value="00029" />
|
||||
<option name="presentableId" value="LOCAL-00029" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783282570014</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00030" summary="- Added save file foramt - Split main.py refactor">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783453889199</created>
|
||||
<option name="number" value="00030" />
|
||||
<option name="presentableId" value="LOCAL-00030" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783453889199</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00031" summary="- Added save file foramt - Split main.py refactor">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783456842297</created>
|
||||
<option name="number" value="00031" />
|
||||
<option name="presentableId" value="LOCAL-00031" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783456842297</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00032" summary="- Added save file foramt - Split main.py refactor">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783755278516</created>
|
||||
<option name="number" value="00032" />
|
||||
<option name="presentableId" value="LOCAL-00032" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783755278516</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00033" summary="- Added save file foramt - Split main.py refactor">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783777171864</created>
|
||||
<option name="number" value="00033" />
|
||||
<option name="presentableId" value="LOCAL-00033" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783777171864</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00034" summary="- Working assembly multi :)">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783798201133</created>
|
||||
<option name="number" value="00034" />
|
||||
<option name="presentableId" value="LOCAL-00034" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783798201133</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00035" summary="- Working assembly multi :)">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783798929531</created>
|
||||
<option name="number" value="00035" />
|
||||
<option name="presentableId" value="LOCAL-00035" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783798929531</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00036" summary="- added renderer">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783887682052</created>
|
||||
<option name="number" value="00036" />
|
||||
<option name="presentableId" value="LOCAL-00036" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783887682052</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00037" summary="- added renderer - Added undo">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783887704766</created>
|
||||
<option name="number" value="00037" />
|
||||
<option name="presentableId" value="LOCAL-00037" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783887704766</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00038" summary="- added renderer - Added undo">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783891561116</created>
|
||||
<option name="number" value="00038" />
|
||||
<option name="presentableId" value="LOCAL-00038" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783891561116</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00039" summary="- added renderer - Added undo">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783918463034</created>
|
||||
<option name="number" value="00039" />
|
||||
<option name="presentableId" value="LOCAL-00039" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783918463034</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00040" summary="- Render improvements, camera plane, update">
|
||||
<option name="closed" value="true" />
|
||||
<created>1783976498520</created>
|
||||
<option name="number" value="00040" />
|
||||
<option name="presentableId" value="LOCAL-00040" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1783976498520</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00041" summary="- added "measurement lines"">
|
||||
<option name="closed" value="true" />
|
||||
<created>1785094789247</created>
|
||||
<option name="number" value="00041" />
|
||||
<option name="presentableId" value="LOCAL-00041" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1785094789248</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00042" summary="- added "measurement lines"">
|
||||
<option name="closed" value="true" />
|
||||
<created>1785697123545</created>
|
||||
<option name="number" value="00042" />
|
||||
<option name="presentableId" value="LOCAL-00042" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1785697123545</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00043" summary="- arc improvements, fillets, operations, bodys">
|
||||
<option name="closed" value="true" />
|
||||
<created>1785701099363</created>
|
||||
<option name="number" value="00043" />
|
||||
<option name="presentableId" value="LOCAL-00043" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1785701099363</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00044" summary="- arc improvements, fillets, operations, bodys">
|
||||
<option name="closed" value="true" />
|
||||
<created>1785915859137</created>
|
||||
<option name="number" value="00044" />
|
||||
<option name="presentableId" value="LOCAL-00044" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1785915859137</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00045" summary="- arc improvements, fillets, operations, bodys">
|
||||
<option name="closed" value="true" />
|
||||
<created>1785941246695</created>
|
||||
<option name="number" value="00045" />
|
||||
<option name="presentableId" value="LOCAL-00045" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1785941246695</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00046" summary="- arc improvements, fillets, operations, bodys">
|
||||
<option name="closed" value="true" />
|
||||
<created>1785948828454</created>
|
||||
<option name="number" value="00046" />
|
||||
<option name="presentableId" value="LOCAL-00046" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1785948828454</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00047" summary="- arc improvements, fillets, operations, bodys">
|
||||
<option name="closed" value="true" />
|
||||
<created>1786110295218</created>
|
||||
<option name="number" value="00047" />
|
||||
<option name="presentableId" value="LOCAL-00047" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1786110295218</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00048" summary="- arc improvements, fillets, operations, bodys">
|
||||
<option name="closed" value="true" />
|
||||
<created>1786125317688</created>
|
||||
<option name="number" value="00048" />
|
||||
<option name="presentableId" value="LOCAL-00048" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1786125317688</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00049" summary="- Operation highlighting, body highlighting">
|
||||
<option name="closed" value="true" />
|
||||
<created>1786180549405</created>
|
||||
<option name="number" value="00049" />
|
||||
<option name="presentableId" value="LOCAL-00049" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1786180549405</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00050" summary="- tech draw draft">
|
||||
<option name="closed" value="true" />
|
||||
<created>1786888483687</created>
|
||||
<option name="number" value="00050" />
|
||||
<option name="presentableId" value="LOCAL-00050" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1786888483688</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00051" summary="- tech draw draft v2">
|
||||
<option name="closed" value="true" />
|
||||
<created>1786910497589</created>
|
||||
<option name="number" value="00051" />
|
||||
<option name="presentableId" value="LOCAL-00051" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1786910497589</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00052" summary="- tech draw draft v2">
|
||||
<option name="closed" value="true" />
|
||||
<created>1786984519780</created>
|
||||
<option name="number" value="00052" />
|
||||
<option name="presentableId" value="LOCAL-00052" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1786984519781</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00053" summary="- tech draw draft v2">
|
||||
<option name="closed" value="true" />
|
||||
<created>1786995561375</created>
|
||||
<option name="number" value="00053" />
|
||||
<option name="presentableId" value="LOCAL-00053" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1786995561376</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00054" summary="- tech drawing and render improv">
|
||||
<option name="closed" value="true" />
|
||||
<created>1787058413911</created>
|
||||
<option name="number" value="00054" />
|
||||
<option name="presentableId" value="LOCAL-00054" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1787058413911</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00055" summary="Fiexed highlighting of operations">
|
||||
<option name="closed" value="true" />
|
||||
<created>1787091089061</created>
|
||||
<option name="number" value="00055" />
|
||||
<option name="presentableId" value="LOCAL-00055" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1787091089061</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="56" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="Vcs.Log.Tabs.Properties">
|
||||
<option name="TAB_STATES">
|
||||
<map>
|
||||
@@ -126,7 +539,31 @@
|
||||
<ignored-roots>
|
||||
<path value="$PROJECT_DIR$/pythonProject" />
|
||||
</ignored-roots>
|
||||
<MESSAGE value="init" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="init" />
|
||||
<MESSAGE value="- Added construction lines switching - Moved callbacks into sketchwidget from main. - Changed reset on right click" />
|
||||
<MESSAGE value="- Added contrain displayed next to line - Slight change to point check from solver." />
|
||||
<MESSAGE value="- Added enabling of midpsnap and prepared others - Show dimesnion on hover" />
|
||||
<MESSAGE value="- Fixed redraw when component changed" />
|
||||
<MESSAGE value="- added MIT license" />
|
||||
<MESSAGE value="- added screenshot" />
|
||||
<MESSAGE value="- added sdf folder ( doesnt work via pip or git=)" />
|
||||
<MESSAGE value="- Tons of addtions" />
|
||||
<MESSAGE value="- Basic operations" />
|
||||
<MESSAGE value="- removed cadquery deoendency" />
|
||||
<MESSAGE value="- sketch enhacements" />
|
||||
<MESSAGE value="- UI refinement, button position ui file as source no dirty drafting anymore" />
|
||||
<MESSAGE value="- assembly draft" />
|
||||
<MESSAGE value="- Added save file foramt - Split main.py refactor" />
|
||||
<MESSAGE value="- Working assembly multi :)" />
|
||||
<MESSAGE value="- added renderer" />
|
||||
<MESSAGE value="- added renderer - Added undo" />
|
||||
<MESSAGE value="- Render improvements, camera plane, update" />
|
||||
<MESSAGE value="- added "measurement lines"" />
|
||||
<MESSAGE value="- arc improvements, fillets, operations, bodys" />
|
||||
<MESSAGE value="- Operation highlighting, body highlighting" />
|
||||
<MESSAGE value="- tech draw draft" />
|
||||
<MESSAGE value="- tech draw draft v2" />
|
||||
<MESSAGE value="- tech drawing and render improv" />
|
||||
<MESSAGE value="Fiexed highlighting of operations" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Fiexed highlighting of operations" />
|
||||
</component>
|
||||
</project>
|
||||
+376
@@ -0,0 +1,376 @@
|
||||
# ── Sheet layout regions ────────────────────────────────────────────────────
|
||||
|
||||
_LAYOUT_MARGIN_MM = 10.0
|
||||
_VIEW_GAP_MM = 12.0
|
||||
|
||||
# Title block box (see _title_block_primitives): 180 × 52 at the bottom-right
|
||||
# corner with a 5 mm sheet margin. Views must clear it (plus clearance).
|
||||
_TB_LEFT_MM = _A3_WIDTH_MM - 180.0 - 5.0
|
||||
_TB_TOP_MM = 5.0 + 52.0
|
||||
_TB_CLEARANCE_MM = 5.0
|
||||
|
||||
# Sheet interior (border margin) as (x0, y0, x1, y1) in sheet mm.
|
||||
_SHEET_INNER = (
|
||||
_LAYOUT_MARGIN_MM,
|
||||
_LAYOUT_MARGIN_MM,
|
||||
_A3_WIDTH_MM - _LAYOUT_MARGIN_MM,
|
||||
_A3_HEIGHT_MM - _LAYOUT_MARGIN_MM,
|
||||
)
|
||||
|
||||
# Regions the orthographic layout may occupy, as (x0, y0, w, h):
|
||||
# - UPPER: full sheet width above the title block
|
||||
# - LEFT: full sheet height in the left strip beside the title block
|
||||
_REGION_UPPER = (
|
||||
_LAYOUT_MARGIN_MM,
|
||||
_TB_TOP_MM + _TB_CLEARANCE_MM,
|
||||
_A3_WIDTH_MM - 2 * _LAYOUT_MARGIN_MM,
|
||||
_A3_HEIGHT_MM - _LAYOUT_MARGIN_MM - (_TB_TOP_MM + _TB_CLEARANCE_MM),
|
||||
)
|
||||
_REGION_LEFT = (
|
||||
_LAYOUT_MARGIN_MM,
|
||||
_LAYOUT_MARGIN_MM,
|
||||
_TB_LEFT_MM - _TB_CLEARANCE_MM - _LAYOUT_MARGIN_MM,
|
||||
_A3_HEIGHT_MM - 2 * _LAYOUT_MARGIN_MM,
|
||||
)
|
||||
|
||||
_MID_ORDER = ("left", "front", "right", "back")
|
||||
_COL_ORDER = ("top", "front", "bottom") # sheet top → bottom
|
||||
_GRID_ORDER = ("front", "right", "back", "top", "left", "bottom")
|
||||
|
||||
_RectList = List[Tuple[str, float, float, float, float]]
|
||||
|
||||
|
||||
def _place_cross(
|
||||
dims: Dict[str, Tuple[float, float]], gap: float
|
||||
) -> _RectList:
|
||||
"""Classic third-angle cross: mid views run left→right (left, front,
|
||||
right, back), col views stack top→bottom (top, front, bottom), with the
|
||||
anchor view (front, or the first present one) at the intersection.
|
||||
|
||||
*dims* maps view id → ``(w, h)`` in sheet units. Returns local
|
||||
``(vid, x, y, w, h)`` rects (origin arbitrary — the caller centres the
|
||||
union on the sheet).
|
||||
"""
|
||||
mid = [k for k in _MID_ORDER if k in dims]
|
||||
col = [k for k in _COL_ORDER if k in dims]
|
||||
if not mid and not col:
|
||||
return []
|
||||
rects: Dict[str, Tuple[float, float, float, float]] = {}
|
||||
|
||||
if col:
|
||||
# Stack bottom → top.
|
||||
y = 0.0
|
||||
for k in reversed(col):
|
||||
w, h = dims[k]
|
||||
rects[k] = (0.0, y, w, h)
|
||||
y += h + gap
|
||||
col_h = y - gap
|
||||
cx = max(dims[k][0] for k in col) / 2.0
|
||||
for k in col:
|
||||
_x, yy, w, h = rects[k]
|
||||
rects[k] = (cx - w / 2.0, yy, w, h)
|
||||
else:
|
||||
col_h = 0.0
|
||||
cx = 0.0
|
||||
|
||||
anchor = "front" if "front" in dims else (mid[0] if mid else col[0])
|
||||
if anchor in rects:
|
||||
ax, ay, aw, _ah = rects[anchor]
|
||||
anchor_cy = ay + _ah / 2.0
|
||||
else:
|
||||
aw, ah = dims[anchor]
|
||||
ax = cx - aw / 2.0
|
||||
ay = col_h / 2.0 - ah / 2.0
|
||||
rects[anchor] = (ax, ay, aw, ah)
|
||||
anchor_cy = col_h / 2.0
|
||||
|
||||
ia = mid.index(anchor) if anchor in mid else -1
|
||||
x = ax
|
||||
for k in reversed(mid[:ia]):
|
||||
w, h = dims[k]
|
||||
x -= w + gap
|
||||
rects[k] = (x, anchor_cy - h / 2.0, w, h)
|
||||
x = ax + aw
|
||||
for k in mid[ia + 1 :]:
|
||||
w, h = dims[k]
|
||||
x += gap
|
||||
rects[k] = (x, anchor_cy - h / 2.0, w, h)
|
||||
x += w
|
||||
return [(k, *r) for k, r in rects.items()]
|
||||
|
||||
|
||||
def _place_swapped(dims: Dict[str, Tuple[float, float]], gap: float) -> _RectList:
|
||||
"""Cross with the view families swapped: the mid views stack vertically
|
||||
(left, front, right, back from the top) and the col views run
|
||||
horizontally (bottom, front, top from the left) — the classic cross
|
||||
turned a quarter turn, for sheets where that orientation fits more.
|
||||
"""
|
||||
mid = [k for k in _MID_ORDER if k in dims]
|
||||
col = [k for k in _COL_ORDER if k in dims]
|
||||
if not mid or not col:
|
||||
return []
|
||||
rects: Dict[str, Tuple[float, float, float, float]] = {}
|
||||
cx = max(dims[k][0] for k in mid) / 2.0
|
||||
y = 0.0
|
||||
for k in mid: # top → bottom
|
||||
w, h = dims[k]
|
||||
rects[k] = (cx - w / 2.0, y, w, h)
|
||||
y += h + gap
|
||||
anchor = "front" if "front" in dims else mid[0]
|
||||
anchor_cy = rects[anchor][1] + dims[anchor][1] / 2.0
|
||||
|
||||
row: Dict[str, Tuple[float, float, float, float]] = {}
|
||||
x = 0.0
|
||||
x_anchor = 0.0
|
||||
for k in reversed(col): # bottom, front, top → left to right
|
||||
w, h = dims[k]
|
||||
if k == anchor:
|
||||
x_anchor = x
|
||||
row[k] = (x, anchor_cy - h / 2.0, w, h)
|
||||
x += w + gap
|
||||
shift = rects[anchor][0] - x_anchor
|
||||
for k, r in row.items():
|
||||
if k == anchor:
|
||||
continue
|
||||
x0, y0, w, h = r
|
||||
rects[k] = (x0 + shift, y0, w, h)
|
||||
return [(k, *r) for k, r in rects.items()]
|
||||
|
||||
|
||||
def _place_grid(
|
||||
dims: Dict[str, Tuple[float, float]], gap: float, rows: int
|
||||
) -> _RectList:
|
||||
"""Wrap the present standard views into a grid of *rows* rows, filled
|
||||
bottom→top and left→right (so the primary views sit near the bottom,
|
||||
like in the cross)."""
|
||||
order = [k for k in _GRID_ORDER if k in dims]
|
||||
if not order:
|
||||
return []
|
||||
cols = max(1, -(-len(order) // rows))
|
||||
rects: _RectList = []
|
||||
y = 0.0
|
||||
for r in range(rows):
|
||||
chunk = order[r * cols : (r + 1) * cols]
|
||||
if not chunk:
|
||||
break
|
||||
x = 0.0
|
||||
row_h = 0.0
|
||||
for k in chunk:
|
||||
w, h = dims[k]
|
||||
rects.append((k, x, y, w, h))
|
||||
x += w + gap
|
||||
row_h = max(row_h, h)
|
||||
y += row_h + gap
|
||||
return rects
|
||||
|
||||
|
||||
def _fit_scale(
|
||||
place: Callable[
|
||||
[Dict[str, Tuple[float, float]], float], _RectList
|
||||
],
|
||||
dims_m: Dict[str, Tuple[float, float]],
|
||||
region: Tuple[float, float, float, float],
|
||||
) -> float:
|
||||
"""Largest shared scale at which *dims_m* (model units) laid out by
|
||||
*place* fits the ``(x0, y0, w, h)`` *region* of the sheet.
|
||||
|
||||
The union size grows monotonically with the scale, so a bisection
|
||||
converges to the tight fit.
|
||||
"""
|
||||
_rx0, _ry0, rw, rh = region
|
||||
|
||||
def fits(s: float) -> bool:
|
||||
rects = place(
|
||||
{k: (w * s, h * s) for k, (w, h) in dims_m.items()}, _VIEW_GAP_MM
|
||||
)
|
||||
if not rects:
|
||||
return True
|
||||
minx = min(r[1] for r in rects)
|
||||
miny = min(r[2] for r in rects)
|
||||
maxx = max(r[1] + r[3] for r in rects)
|
||||
maxy = max(r[2] + r[4] for r in rects)
|
||||
return (maxx - minx) <= rw + 1e-9 and (maxy - miny) <= rh + 1e-9
|
||||
|
||||
s_lo, s_hi = 0.0, 1.0
|
||||
if fits(s_hi):
|
||||
s_lo = s_hi
|
||||
while s_hi < 1.0e6 and fits(s_hi * 2.0):
|
||||
s_hi *= 2.0
|
||||
for _ in range(60):
|
||||
mid = 0.5 * (s_lo + s_hi)
|
||||
if fits(mid):
|
||||
s_lo = mid
|
||||
else:
|
||||
s_hi = mid
|
||||
return s_lo
|
||||
|
||||
|
||||
def _free_rects(
|
||||
used_rects: Sequence[Tuple[float, float, float, float]]
|
||||
) -> List[Tuple[float, float, float, float]]:
|
||||
"""Axis-aligned free rects ``(x0, y0, w, h)`` around *used_rects*,
|
||||
clearing the sheet border and the title block zone."""
|
||||
ix0, iy0, ix1, iy1 = _SHEET_INNER
|
||||
if used_rects:
|
||||
ux0 = min(r[0] for r in used_rects)
|
||||
uy0 = min(r[1] for r in used_rects)
|
||||
ux1 = max(r[0] + r[2] for r in used_rects)
|
||||
uy1 = max(r[1] + r[3] for r in used_rects)
|
||||
cands = [
|
||||
(ux1 + _VIEW_GAP_MM, iy0, ix1, iy1), # right of the used block
|
||||
(ix0, iy0, ux0 - _VIEW_GAP_MM, iy1), # left
|
||||
(ix0, uy1 + _VIEW_GAP_MM, ix1, iy1), # above
|
||||
(ix0, iy0, ix1, uy0 - _VIEW_GAP_MM), # below
|
||||
]
|
||||
else:
|
||||
cands = [(ix0, iy0, ix1, iy1)]
|
||||
tb = (
|
||||
_TB_LEFT_MM - _TB_CLEARANCE_MM,
|
||||
0.0,
|
||||
_A3_WIDTH_MM - (_TB_LEFT_MM - _TB_CLEARANCE_MM),
|
||||
_TB_TOP_MM + _TB_CLEARANCE_MM,
|
||||
)
|
||||
out: List[Tuple[float, float, float, float]] = []
|
||||
for x0, y0, x1, y1 in cands:
|
||||
x0, y0 = max(x0, ix0), max(y0, iy0)
|
||||
x1, y1 = min(x1, ix1), min(y1, iy1)
|
||||
if x1 - x0 < 1.0 or y1 - y0 < 1.0:
|
||||
continue
|
||||
if not (x1 <= tb[0] or tb[2] <= x0 or y1 <= tb[1] or tb[3] <= y0):
|
||||
# Overlaps the title block zone — keep the parts above/left of it.
|
||||
subs = [
|
||||
(x0, max(y0, tb[3]), x1, y1),
|
||||
(x0, y0, min(x1, tb[0]), y1),
|
||||
]
|
||||
else:
|
||||
subs = [(x0, y0, x1, y1)]
|
||||
for sx0, sy0, sx1, sy1 in subs:
|
||||
if sx1 - sx0 > 1.0 and sy1 - sy0 > 1.0:
|
||||
out.append((sx0, sy0, sx1 - sx0, sy1 - sy0))
|
||||
return out
|
||||
|
||||
|
||||
def _layout_views_on_sheet(
|
||||
views: Sequence[DrawingView],
|
||||
bboxes: Dict[str, Tuple[float, float, float, float]],
|
||||
) -> Tuple[
|
||||
Dict[str, Tuple[float, float, float, float]],
|
||||
Optional[float],
|
||||
Dict[str, float],
|
||||
]:
|
||||
"""Compute a slot rectangle and sheet rotation for each view.
|
||||
|
||||
*bboxes* maps view_id → ``(min_x, min_y, max_x, max_y)`` in model
|
||||
units (from :func:`_edges_bounds`). Returns ``(slots, common_scale,
|
||||
rotations)``: slots are ``(left, bottom, width, height)`` in sheet mm
|
||||
(origin at the sheet's bottom-left corner, +y up), common_scale is the
|
||||
shared model→sheet scale of the standard orthographic views, and
|
||||
rotations maps view_id → sheet rotation in degrees (0 or 90).
|
||||
|
||||
The sheet is filled, not just used: every candidate arrangement
|
||||
(classic third-angle cross, the cross with the view families swapped,
|
||||
and 1/2/3-row grids) is combined with every per-view 90° rotation
|
||||
assignment, and the candidate giving the largest shared scale is used.
|
||||
Candidates within 0.5% of the best scale prefer the one with fewer
|
||||
rotated views, then the more conventional arrangement, so layouts stay
|
||||
stable and standard whenever they are already the best fit. All
|
||||
orthographic views share one scale so the projections stay mutually
|
||||
consistent. Isometric and custom views take the largest remaining
|
||||
free rect (clearing the title block).
|
||||
"""
|
||||
slots: Dict[str, Tuple[float, float, float, float]] = {}
|
||||
rotations: Dict[str, float] = {}
|
||||
common_scale: Optional[float] = None
|
||||
|
||||
ortho = [
|
||||
v for v in views if v.kind in _STANDARD_VIEWS and v.kind != "isometric"
|
||||
]
|
||||
|
||||
used_rects: List[Tuple[float, float, float, float]] = []
|
||||
if ortho:
|
||||
dims0: Dict[str, Tuple[float, float]] = {}
|
||||
for v in ortho:
|
||||
b = bboxes.get(v.kind)
|
||||
vs = max(v.scale, 1e-9)
|
||||
if b is None:
|
||||
dims0[v.kind] = (1.0 * vs, 1.0 * vs)
|
||||
else:
|
||||
dims0[v.kind] = (
|
||||
max(b[2] - b[0], 1e-6) * vs,
|
||||
max(b[3] - b[1], 1e-6) * vs,
|
||||
)
|
||||
keys = list(dims0)
|
||||
arrangements: Tuple[
|
||||
Tuple[str, int, Callable[[Dict[str, Tuple[float, float]], float], _RectList]]
|
||||
] = (
|
||||
("cross", 0, _place_cross),
|
||||
("swapped", 1, _place_swapped),
|
||||
("grid1", 2, lambda d, g: _place_grid(d, g, 1)),
|
||||
("grid2", 3, lambda d, g: _place_grid(d, g, 2)),
|
||||
("grid3", 4, lambda d, g: _place_grid(d, g, 3)),
|
||||
)
|
||||
cands: List[
|
||||
Tuple[float, int, int, Tuple[float, float, float, float],
|
||||
Dict[str, Tuple[float, float]],
|
||||
Callable[[Dict[str, Tuple[float, float]], float], _RectList],
|
||||
List[bool]]
|
||||
] = []
|
||||
for mask in range(1 << len(keys)):
|
||||
rotated = [bool(mask & (1 << i)) for i in range(len(keys))]
|
||||
dims_m = {
|
||||
k: (
|
||||
dims0[k][1] if rotated[i] else dims0[k][0],
|
||||
dims0[k][0] if rotated[i] else dims0[k][1],
|
||||
)
|
||||
for i, k in enumerate(keys)
|
||||
}
|
||||
for _name, rank, place in arrangements:
|
||||
s_up = _fit_scale(place, dims_m, _REGION_UPPER)
|
||||
s_left = _fit_scale(place, dims_m, _REGION_LEFT)
|
||||
if s_up >= s_left:
|
||||
s, region = s_up, _REGION_UPPER
|
||||
else:
|
||||
s, region = s_left, _REGION_LEFT
|
||||
if s <= 0.0:
|
||||
continue
|
||||
cands.append((s, sum(rotated), rank, region, dims_m, place, rotated))
|
||||
if cands:
|
||||
best_s = max(c[0] for c in cands)
|
||||
s, _nrot, _rank, region, dims_m, place, rotated = min(
|
||||
(c for c in cands if c[0] >= best_s * 0.995),
|
||||
key=lambda c: (c[1], c[2], -c[0]),
|
||||
)
|
||||
rx0, _ry0, rw, rh = region
|
||||
ds = {k: (w * s, h * s) for k, (w, h) in dims_m.items()}
|
||||
rects = place(ds, _VIEW_GAP_MM)
|
||||
minx = min(r[1] for r in rects)
|
||||
miny = min(r[2] for r in rects)
|
||||
maxx = max(r[1] + r[3] for r in rects)
|
||||
maxy = max(r[2] + r[4] for r in rects)
|
||||
ox = rx0 + (rw - (maxx - minx)) / 2.0
|
||||
oy = _ry0 + (rh - (maxy - miny)) / 2.0
|
||||
for vid, x, y, w, h in rects:
|
||||
slots[vid] = (x - minx + ox, y - miny + oy, w, h)
|
||||
rotations[vid] = 90.0 if rotated[keys.index(vid)] else 0.0
|
||||
common_scale = s
|
||||
used_rects = [(ox, oy, maxx - minx, maxy - miny)]
|
||||
|
||||
# Isometric and custom views: the largest remaining free rect each,
|
||||
# clearing the title block.
|
||||
extra = [
|
||||
v for v in views
|
||||
if v.kind == "isometric" or v.kind not in _STANDARD_VIEWS
|
||||
]
|
||||
assigned = list(used_rects)
|
||||
for i, v in enumerate(extra):
|
||||
vid = v.kind if v.kind in _STANDARD_VIEWS else (v.name or v.id)
|
||||
free = _free_rects(assigned)
|
||||
if free:
|
||||
slot = max(free, key=lambda r: r[2] * r[3])
|
||||
else:
|
||||
# No free rect left — park in the bottom-left corner stack.
|
||||
slot = (_LAYOUT_MARGIN_MM, _LAYOUT_MARGIN_MM + i * 60.0, 120.0, 50.0)
|
||||
slots[vid] = slot
|
||||
assigned.append(slot)
|
||||
|
||||
return slots, common_scale, rotations
|
||||
@@ -0,0 +1,50 @@
|
||||
import os, sys
|
||||
os.environ["QT_QPA_PLATFORM"] = "offscreen"
|
||||
sys.path.insert(0, "/Volumes/Data_drive/Programming/fluency/src")
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtGui import QPixmap, QPainter, QColor
|
||||
from PySide6.QtCore import QRectF
|
||||
import math
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
|
||||
from fluency.geometry.base import Point2D
|
||||
from fluency.geometry_occ.kernel import OCGeometryKernel
|
||||
from fluency.models.data_model import Body, Component, Project, DrawingView, TechnicalDrawing
|
||||
from fluency.technical_drawing import generate_drawing, render_drawing, _A3_WIDTH_MM, _A3_HEIGHT_MM
|
||||
|
||||
kernel = OCGeometryKernel()
|
||||
# Long thin bar: 120 x 25 x 30 (matches the "wide bar" screenshot case).
|
||||
points = [Point2D(0, 0), Point2D(120, 0), Point2D(120, 25), Point2D(0, 25)]
|
||||
box = kernel.extrude(kernel.create_polygon(points), 30.0)
|
||||
body = Body(name="Bar", geometry=box)
|
||||
comp = Component(name="BarComp")
|
||||
comp.bodies[body.id] = body
|
||||
project = Project()
|
||||
project.components[comp.id] = comp
|
||||
project.active_component = comp.id
|
||||
|
||||
W = 2400
|
||||
H = int(W * _A3_HEIGHT_MM / _A3_WIDTH_MM)
|
||||
pm = QPixmap(W, H)
|
||||
pm.fill(QColor(255, 255, 255))
|
||||
|
||||
for name, kinds in [
|
||||
("four", ["front", "top", "right", "back"]),
|
||||
("six", ["front", "top", "right", "left", "back", "bottom"]),
|
||||
("sixiso", ["front", "top", "right", "left", "back", "bottom", "isometric"]),
|
||||
]:
|
||||
drawing = TechnicalDrawing(
|
||||
source_kind="component", source_id=comp.id,
|
||||
views=[DrawingView(kind=k) for k in kinds],
|
||||
auto_dimensions=True, title=name,
|
||||
)
|
||||
result = generate_drawing(drawing, project, kernel)
|
||||
p = QPainter(pm)
|
||||
render_drawing(p, result, QRectF(0, 0, W, H))
|
||||
p.end()
|
||||
out = f"/tmp/drawing_{name}.png"
|
||||
pm.save(out)
|
||||
print(name, "saved", out, "prims", len(result.primitives), "scale",
|
||||
round(result.view_transforms.get("front", (None,))[0] or 0, 3))
|
||||
@@ -0,0 +1,159 @@
|
||||
"""Smoke test: layout optimizer fills the page, no overlaps, title block clear."""
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
sys.path.insert(0, "/Volumes/Data_drive/Programming/fluency/src")
|
||||
|
||||
from fluency.models.data_model import DrawingView
|
||||
from fluency.technical_drawing import (
|
||||
_layout_views_on_sheet,
|
||||
build_manual_candidates,
|
||||
)
|
||||
|
||||
A3W, A3H = 420.0, 297.0
|
||||
TB = (235.0, 0.0, 420.0, 62.0) # title block zone incl. clearance
|
||||
|
||||
|
||||
def b(x, y, w, h):
|
||||
return (x, y, x + w, y + h)
|
||||
|
||||
|
||||
def overlaps(r1, r2, clear=0.0):
|
||||
x0, y0, w, h = r1
|
||||
x1, y1, w2, h2 = r2
|
||||
return not (x0 + w <= x1 + clear or x1 + w2 <= x0 + clear
|
||||
or y0 + h <= y1 + clear or y1 + h2 <= y0 + clear)
|
||||
|
||||
|
||||
def tb_overlap(r, clear=5.0):
|
||||
x0, y0, w, h = r
|
||||
x1, y1, w2, h2 = TB
|
||||
return not (x0 + w <= x1 + clear or x1 + w2 <= x0 + clear
|
||||
or y0 + h <= y1 + clear or y1 + h2 <= y0 + clear)
|
||||
|
||||
|
||||
def union_rect(rects):
|
||||
x0 = min(r[0] for r in rects)
|
||||
y0 = min(r[1] for r in rects)
|
||||
x1 = max(r[0] + r[2] for r in rects)
|
||||
y1 = max(r[1] + r[3] for r in rects)
|
||||
return (x0, y0, x1 - x0, y1 - y0)
|
||||
|
||||
|
||||
def check(name, kinds, boxes, expect_rot=None):
|
||||
views = [DrawingView(kind=k) for k in kinds]
|
||||
bboxes = {k: boxes[k] for k in kinds}
|
||||
slots, scale, rots = _layout_views_on_sheet(views, bboxes)
|
||||
print(f"--- {name}: scale={scale:.4f} rots={rots}")
|
||||
# all slots within sheet
|
||||
for k, s in slots.items():
|
||||
assert 10 - 1e-6 <= s[0] and 10 - 1e-6 <= s[1], f"{k} outside sheet {s}"
|
||||
assert s[0] + s[2] <= A3W - 10 + 1e-6, f"{k} beyond right {s}"
|
||||
assert s[1] + s[3] <= A3H - 10 + 1e-6, f"{k} beyond top {s}"
|
||||
# no overlaps between slots
|
||||
ks = list(slots)
|
||||
for i in range(len(ks)):
|
||||
for j in range(i + 1, len(ks)):
|
||||
assert not overlaps(slots[ks[i]], slots[ks[j]], 11.9), \
|
||||
f"{ks[i]} overlaps {ks[j]}: {slots[ks[i]]} / {slots[ks[j]]}"
|
||||
# title block clear
|
||||
for k, s in slots.items():
|
||||
assert not tb_overlap(s), f"{k} intrudes title block {s}"
|
||||
# fill report
|
||||
u = union_rect(list(slots.values()))
|
||||
area = u[2] * u[3]
|
||||
print(f" union: x0={u[0]:.1f} y0={u[1]:.1f} w={u[2]:.1f} h={u[3]:.1f} "
|
||||
f"area={area:.0f}mm^2 ({100*area/(A3W*A3H):.0f}% of sheet)")
|
||||
for k in ks:
|
||||
print(f" {k}: {tuple(round(v,1) for v in slots[k])}")
|
||||
if expect_rot is not None:
|
||||
assert rots == expect_rot, f"expected {expect_rot}, got {rots}"
|
||||
return slots, scale, rots
|
||||
|
||||
|
||||
# 1. Two square views (front+top): should fill the page, no rotation.
|
||||
check("two square", ["front", "top"],
|
||||
{"front": b(0, 0, 40, 40), "top": b(0, 0, 40, 20)})
|
||||
|
||||
# 2. Wide bar, 4 views (old screenshot case): front+back wide, top+right.
|
||||
check("wide bar 4", ["front", "top", "right", "back"],
|
||||
{"front": b(0, 0, 120, 25), "top": b(0, 0, 25, 40),
|
||||
"right": b(0, 0, 25, 40), "back": b(0, 0, 120, 25)})
|
||||
|
||||
# 3. Six views of a long thin part: rotation should kick in.
|
||||
check("thin part 6", ["front", "top", "right", "left", "back", "bottom"],
|
||||
{"front": b(0, 0, 200, 30), "top": b(0, 0, 30, 50),
|
||||
"right": b(0, 0, 50, 30), "left": b(0, 0, 50, 30),
|
||||
"back": b(0, 0, 200, 30), "bottom": b(0, 0, 30, 50)})
|
||||
|
||||
# 4. Single front view: fills the whole page.
|
||||
check("single", ["front"], {"front": b(0, 0, 10, 20)})
|
||||
|
||||
# 5. All 6 + isometric + custom.
|
||||
check("everything",
|
||||
["front", "top", "right", "left", "back", "bottom", "isometric"],
|
||||
{"front": b(0, 0, 80, 40), "top": b(0, 0, 80, 30),
|
||||
"right": b(0, 0, 30, 40), "left": b(0, 0, 30, 40),
|
||||
"back": b(0, 0, 80, 40), "bottom": b(0, 0, 80, 30),
|
||||
"isometric": b(0, 0, 60, 60)})
|
||||
|
||||
# ── Inverse-transform roundtrip ─────────────────────────────────────────
|
||||
# A 90°-rotated view: forward via _assemble_view's recorded 6-tuple,
|
||||
# inverse via the widget's formula.
|
||||
t = (2.0, 150.0, 80.0, 90.0, 10.0, 5.0) # s, o_x, o_y, deg, cx, cy
|
||||
scale, ox, oy, deg, cx, cy = t
|
||||
th = math.radians(deg)
|
||||
cos_t, sin_t = math.cos(th), math.sin(th)
|
||||
|
||||
|
||||
def fwd(p):
|
||||
dx, dy = p[0] - cx, p[1] - cy
|
||||
return ((dx * cos_t - dy * sin_t) * scale + ox,
|
||||
(dx * sin_t + dy * cos_t) * scale + oy)
|
||||
|
||||
|
||||
def inv(p):
|
||||
sx, sy = (p[0] - ox) / scale, (p[1] - oy) / scale
|
||||
return (sx * cos_t + sy * sin_t + cx, -sx * sin_t + sy * cos_t + cy)
|
||||
|
||||
|
||||
for p in [(0, 0), (10, 5), (3, -7), (42.5, 11.25)]:
|
||||
rt = inv(fwd(p))
|
||||
assert abs(rt[0] - p[0]) < 1e-9 and abs(rt[1] - p[1]) < 1e-9, (p, rt)
|
||||
print("inverse roundtrip OK")
|
||||
|
||||
# Legacy 3-tuple still works through build_manual_candidates.
|
||||
from fluency.models.data_model import DrawingAnnotation, TechnicalDrawing
|
||||
d = TechnicalDrawing(source_kind="component", source_id="c")
|
||||
ann = DrawingAnnotation(kind="dimension", dimension_kind="length",
|
||||
view_id="front", anchors=[(0.0, 0.0), (0.0, 12.5)],
|
||||
direction=(0.0, 1.0))
|
||||
d.annotations.append(ann)
|
||||
cands, res, unres = build_manual_candidates(d, {"front": (2.0, 10.0, 20.0)})
|
||||
assert unres == [] and cands[0].anchor_points[1] == (10.0, 45.0)
|
||||
print("legacy 3-tuple OK")
|
||||
|
||||
# 6-tuple manual: rotated length direction must rotate too.
|
||||
ann2 = DrawingAnnotation(kind="dimension", dimension_kind="length",
|
||||
view_id="front",
|
||||
anchors=[(0.0, 0.0), (0.0, 10.0)],
|
||||
direction=(0.0, 1.0))
|
||||
d2 = TechnicalDrawing(source_kind="component", source_id="c")
|
||||
d2.annotations.append(ann2)
|
||||
# 90° rotation about centre c=(5,5), scale 2, o=(100,80)
|
||||
cands, res, unres = build_manual_candidates(
|
||||
d2, {"front": (2.0, 100.0, 80.0, 90.0, 5.0, 5.0)}
|
||||
)
|
||||
c = cands[0]
|
||||
# anchors: (0,0)->rot90 about (5,5) = (5-(0-5)*0 - ... compute: dx=-5,dy=-5
|
||||
# fwd: (dx*cos - dy*sin)*2+100 = (0 - (-5))*2+100 = 110 ; (dx*sin+dy*cos)*2+80 = (-5)*2+80=70
|
||||
# (0,10): dx=-5, dy=5 -> (0-5)*2+100=90 ; (−5*1+0)*2+80=70
|
||||
assert c.anchor_points[0] == (110.0, 70.0), c.anchor_points
|
||||
assert c.anchor_points[1] == (90.0, 70.0), c.anchor_points
|
||||
# direction (0,1) rotated 90° CCW -> (-1, 0)
|
||||
assert c.direction[0] == -1.0 and abs(c.direction[1]) < 1e-9, c.direction
|
||||
print("6-tuple manual (rotated) OK")
|
||||
|
||||
print("ALL SMOKE CHECKS PASSED")
|
||||
@@ -0,0 +1,238 @@
|
||||
"""Headless test: connectors follow moved features across ALL assemblies.
|
||||
|
||||
Simulates the test-file scenario: a plate with a hole, mated hole-to-face
|
||||
in two different assemblies. The hole is moved (rebuilt geometry) and the
|
||||
body-update connector recalculation must:
|
||||
1. re-locate the hole connector on every instance of the component
|
||||
(both the active and a non-active assembly),
|
||||
2. re-solve each mated pair so the partner parts follow,
|
||||
3. never snap a planar connector onto the cylindrical hole (type match),
|
||||
4. mark a connector invalid when its feature disappears.
|
||||
5. auto-follow a FAR move when the candidate is unambiguous (single
|
||||
feature of its class) — the demo case,
|
||||
6. NOT auto-apply an ambiguous far candidate (another same-class feature
|
||||
is nearer) — that needs a manual pick, simulated here.
|
||||
7. backfill legacy connectors' entity_type from their auto-generated name.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
||||
|
||||
import numpy as np
|
||||
|
||||
from OCP.gp import gp_Pnt, gp_Dir, gp_Ax2
|
||||
from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from fluency.ui.main_window import MainWindow
|
||||
from fluency.models.data_model import Assembly, Body
|
||||
from fluency.geometry_occ.kernel import OCCGeometryObject
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
w = MainWindow()
|
||||
|
||||
comp = w._current_component
|
||||
|
||||
|
||||
def make_plate(hole_xy):
|
||||
box = BRepPrimAPI_MakeBox(60.0, 40.0, 5.0).Shape()
|
||||
ax = gp_Ax2(gp_Pnt(float(hole_xy[0]), float(hole_xy[1]), 0.0), gp_Dir(0, 0, 1))
|
||||
cyl = BRepPrimAPI_MakeCylinder(ax, 3.0, 6.0).Shape()
|
||||
return BRepAlgoAPI_Cut(box, cyl).Shape()
|
||||
|
||||
|
||||
body = Body(name="plate")
|
||||
comp.bodies[body.id] = body
|
||||
body.geometry = OCCGeometryObject(make_plate((10.0, 5.0)))
|
||||
|
||||
partner = w._project.add_component()
|
||||
pbody = Body(name="partner")
|
||||
partner.bodies[pbody.id] = pbody
|
||||
pbody.geometry = OCCGeometryObject(BRepPrimAPI_MakeBox(30.0, 30.0, 10.0).Shape())
|
||||
|
||||
|
||||
def make_pair(asm):
|
||||
ac1 = asm.add_component_instance(comp.id, name="A")
|
||||
ac2 = asm.add_component_instance(partner.id, name="B")
|
||||
ac1.position = np.zeros(3)
|
||||
ac1.rotation = np.eye(3)
|
||||
ac2.position = np.array([0.0, 0.0, 15.0])
|
||||
ac2.rotation = np.eye(3)
|
||||
ac1.geom_cache[body.id] = body.geometry
|
||||
|
||||
c1 = ac1.add_connector(
|
||||
position=(10.0, 5.0, 2.5),
|
||||
normal=(0.0, 0.0, 1.0),
|
||||
x_dir=(1.0, 0.0, 0.0),
|
||||
source_obj_id=f"asm_{ac1.id}_{body.id}",
|
||||
name="Conn hole A",
|
||||
entity_type="cylindrical_face",
|
||||
)
|
||||
c2 = ac2.add_connector(
|
||||
position=(15.0, 15.0, 0.0),
|
||||
normal=(0.0, 0.0, -1.0),
|
||||
x_dir=(1.0, 0.0, 0.0),
|
||||
source_obj_id=f"asm_{ac2.id}_{pbody.id}",
|
||||
name="Conn face B",
|
||||
entity_type="planar_face",
|
||||
)
|
||||
c1.is_grounded = True
|
||||
c1.partner_ac_id = ac2.id
|
||||
c1.partner_connector_id = c2.id
|
||||
c2.partner_ac_id = ac1.id
|
||||
c2.partner_connector_id = c1.id
|
||||
aconn = asm.add_connection(ac1.id, ac2.id)
|
||||
aconn.first_connector_id = c1.id
|
||||
aconn.second_connector_id = c2.id
|
||||
return ac1, ac2, c1, c2, aconn
|
||||
|
||||
|
||||
asm1 = w._project.get_active_assembly()
|
||||
asm2 = w._project.add_assembly(Assembly(name="second"))
|
||||
pair1 = make_pair(asm1)
|
||||
pair2 = make_pair(asm2)
|
||||
assert w._project.active_assembly == asm1.id # asm2 is the NON-active one
|
||||
|
||||
# ── Move the hole: rebuild the plate with the hole at (25, 12) ──────────
|
||||
new_geom = OCCGeometryObject(make_plate((25.0, 12.0)))
|
||||
body.geometry = new_geom
|
||||
for asm in (asm1, asm2):
|
||||
for ac in asm.components.values():
|
||||
if ac.component_id == comp.id:
|
||||
ac.geom_cache[body.id] = new_geom
|
||||
|
||||
# ── The body-update auto path ───────────────────────────────────────────
|
||||
w._recalculate_connectors()
|
||||
|
||||
for (ac1, ac2, c1, c2, aconn), label in ((pair1, "asm1"), (pair2, "asm2")):
|
||||
# The hole connector followed the hole on every instance.
|
||||
assert np.allclose(c1.position, (25.0, 12.0, 2.5), atol=1e-6), (label, c1.position)
|
||||
# The mated pair re-aligned: both world connectors coincide.
|
||||
w1 = ac1.position + ac1.rotation @ np.asarray(c1.position)
|
||||
w2 = ac2.position + ac2.rotation @ np.asarray(c2.position)
|
||||
assert np.allclose(w1, w2, atol=1e-6), (label, w1, w2)
|
||||
print(f"{label}: connector at {np.round(c1.position, 3)}, "
|
||||
f"partner moved to {np.round(ac2.position, 3)}")
|
||||
|
||||
# ── The 'Upd' button path: move the hole again, re-run the handler ─────
|
||||
w._refresh_connection_list()
|
||||
w._connection_list.setCurrentRow(0) # active assembly = asm1
|
||||
geom3 = OCCGeometryObject(make_plate((35.0, 20.0)))
|
||||
body.geometry = geom3
|
||||
for ac in asm1.components.values():
|
||||
if ac.component_id == comp.id:
|
||||
ac.geom_cache[body.id] = geom3
|
||||
w._on_update_connection_from_list()
|
||||
ac1, ac2, c1, c2, aconn = pair1
|
||||
assert np.allclose(c1.position, (35.0, 20.0, 2.5), atol=1e-6), c1.position
|
||||
w1 = ac1.position + ac1.rotation @ np.asarray(c1.position)
|
||||
w2 = ac2.position + ac2.rotation @ np.asarray(c2.position)
|
||||
assert np.allclose(w1, w2, atol=1e-6), (w1, w2)
|
||||
print("Upd button: connector at", np.round(c1.position, 3),
|
||||
"partner at", np.round(ac2.position, 3))
|
||||
|
||||
# ── Type matching: a planar connector must not snap onto the hole ───────
|
||||
ac1 = pair1[0]
|
||||
c3 = ac1.add_connector(
|
||||
position=(30.0, 20.0, 5.0),
|
||||
normal=(0.0, 0.0, 1.0),
|
||||
x_dir=(1.0, 0.0, 0.0),
|
||||
source_obj_id=f"asm_{ac1.id}_{body.id}",
|
||||
name="Conn face",
|
||||
entity_type="planar_face",
|
||||
)
|
||||
res = w._redetect_connector_on_geometry(c3, ac1, comp)
|
||||
assert res is not None, "planar connector candidate missing"
|
||||
assert not c3.is_invalid, "pure relocator must not mutate the connector"
|
||||
assert np.allclose(c3.position, (30.0, 20.0, 5.0), atol=1e-6), c3.position
|
||||
assert np.allclose(res[1], (30.0, 20.0, 5.0), atol=1e-6), res[1]
|
||||
print("planar connector stayed on the face:", np.round(res[1], 3))
|
||||
|
||||
# ── Feature removed: relocator finds nothing; auto path marks invalid ──
|
||||
plain = OCCGeometryObject(BRepPrimAPI_MakeBox(60.0, 40.0, 5.0).Shape())
|
||||
body.geometry = plain
|
||||
ac1.geom_cache[body.id] = plain
|
||||
res = w._redetect_connector_on_geometry(pair1[2], ac1, comp)
|
||||
assert res is None, "hole connector should find no candidate on a plain box"
|
||||
w._recalculate_connectors()
|
||||
assert pair1[2].is_invalid, "auto path must mark the connector invalid"
|
||||
print("removed feature -> connector marked invalid")
|
||||
|
||||
# ── Far move with a decoy: ambiguous candidate is NOT auto-applied ─────
|
||||
# The real hole moved to (25, 12) — ~12.8mm from the stored (35, 20) — but
|
||||
# a SECOND hole now sits at (28, 16), only ~8mm away. The nearest
|
||||
# candidate is ambiguous (different feature), so the auto path must leave
|
||||
# the connector alone and queue it for a manual pick.
|
||||
def make_plate2(holes):
|
||||
box = BRepPrimAPI_MakeBox(60.0, 40.0, 5.0).Shape()
|
||||
for hx, hy in holes:
|
||||
ax = gp_Ax2(gp_Pnt(hx, hy, 0.0), gp_Dir(0, 0, 1))
|
||||
box = BRepAlgoAPI_Cut(box, BRepPrimAPI_MakeCylinder(ax, 3.0, 6.0).Shape()).Shape()
|
||||
return box
|
||||
# ── Stage 1: the user's demo — a SINGLE hole moved far away ─────────────
|
||||
# 12.8mm from the stored position, but the only cylindrical face on the
|
||||
# body → unambiguous → must be auto-applied (and the mate re-solved).
|
||||
geom4 = OCCGeometryObject(make_plate2([(25.0, 12.0)]))
|
||||
body.geometry = geom4
|
||||
for asm in (asm1, asm2):
|
||||
for ac in asm.components.values():
|
||||
if ac.component_id == comp.id:
|
||||
ac.geom_cache[body.id] = geom4
|
||||
w._recalculate_connectors()
|
||||
assert np.allclose(pair1[2].position, (25.0, 12.0, 2.5), atol=1e-6), \
|
||||
"unique far candidate must be auto-applied (the demo case)"
|
||||
assert not pair1[2].is_invalid
|
||||
w1 = ac1.position + ac1.rotation @ np.asarray(pair1[2].position)
|
||||
w2 = pair1[1].position + pair1[1].rotation @ np.asarray(pair1[3].position)
|
||||
assert np.allclose(w1, w2, atol=1e-6), (w1, w2)
|
||||
print("single far hole: auto-followed to", np.round(pair1[2].position, 3))
|
||||
|
||||
# ── Stage 2: far move with a decoy — ambiguous, NOT auto-applied ───────
|
||||
# The real hole now sits at (32, 20) — 10.6mm from the stored (25, 12) —
|
||||
# while a decoy hole at (21, 7) is only 6.4mm away. The nearest
|
||||
# candidate is likely a DIFFERENT feature, so the auto path must leave
|
||||
# the connector alone and queue it for a manual pick.
|
||||
geom5 = OCCGeometryObject(make_plate2([(32.0, 20.0), (21.0, 7.0)]))
|
||||
body.geometry = geom5
|
||||
for asm in (asm1, asm2):
|
||||
for ac in asm.components.values():
|
||||
if ac.component_id == comp.id:
|
||||
ac.geom_cache[body.id] = geom5
|
||||
w._recalculate_connectors()
|
||||
assert np.allclose(pair1[2].position, (25.0, 12.0, 2.5), atol=1e-6), \
|
||||
"ambiguous far candidate must not be auto-applied"
|
||||
assert not pair1[2].is_invalid, "ambiguous candidate is not a missing feature"
|
||||
|
||||
# Simulate the user clicking the REAL hole in the relocate pick flow:
|
||||
w._relocate_pending = [(asm1, ac1, pair1[2])]
|
||||
w._on_relocate_picked(
|
||||
(32.0, 20.0, 2.5), (0.0, 0.0, 1.0), (1.0, 0.0, 0.0),
|
||||
"cylindrical_face", f"asm_{ac1.id}_{body.id}",
|
||||
)
|
||||
assert np.allclose(pair1[2].position, (32.0, 20.0, 2.5), atol=1e-6), pair1[2].position
|
||||
assert not pair1[2].is_invalid, "manual pick must re-validate the connector"
|
||||
assert w._relocate_pending is None, "pending queue must drain after the pick"
|
||||
w1 = ac1.position + ac1.rotation @ np.asarray(pair1[2].position)
|
||||
w2 = pair1[1].position + pair1[1].rotation @ np.asarray(pair1[3].position)
|
||||
assert np.allclose(w1, w2, atol=1e-6), (w1, w2)
|
||||
print("far move: manual pick re-homed connector at", np.round(pair1[2].position, 3),
|
||||
"partner at", np.round(pair1[1].position, 3))
|
||||
|
||||
# ── Legacy backfill: empty entity_type recovered from the auto name ────
|
||||
from fluency.models.data_model import Connector
|
||||
legacy = Connector(
|
||||
name="Conn cylindrical_face anchor",
|
||||
position=(32.0, 20.0, 2.5),
|
||||
source_obj_id=f"asm_{ac1.id}_{body.id}",
|
||||
)
|
||||
assert legacy.entity_type == "cylindrical_face", legacy.entity_type
|
||||
res = w._redetect_connector_on_geometry(legacy, ac1, comp)
|
||||
assert res is not None and res[0] < 1e-3, res
|
||||
print("legacy name backfill: entity_type =", legacy.entity_type)
|
||||
|
||||
print("CONNECTOR_RELOCATE_OK")
|
||||
@@ -0,0 +1,158 @@
|
||||
"""Headless repro: load assemblytest.fluency, compare sketch circle centers
|
||||
vs. saved body hole axes vs. connector positions, then run the real
|
||||
body-update path and check where connectors land.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
||||
|
||||
import numpy as np
|
||||
|
||||
from OCP.BRepAdaptor import BRepAdaptor_Surface
|
||||
from OCP.GeomAbs import GeomAbs_Cylinder
|
||||
from OCP.TopAbs import TopAbs_FACE
|
||||
from OCP.TopExp import TopExp_Explorer
|
||||
from OCP.TopoDS import TopoDS
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
from fluency.ui.main_window import MainWindow
|
||||
|
||||
def etype(e):
|
||||
if isinstance(e, dict):
|
||||
return e.get("type")
|
||||
return getattr(e, "entity_type", None) or getattr(e, "type", None)
|
||||
|
||||
def egeom(e):
|
||||
if isinstance(e, dict):
|
||||
return e.get("geometry")
|
||||
return getattr(e, "geometry", None)
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
w = MainWindow()
|
||||
|
||||
path = os.path.join(os.path.dirname(__file__), "assemblytest.fluency")
|
||||
ok = w._open_project_file(path)
|
||||
assert ok, "failed to open demo file"
|
||||
|
||||
proj = w._project
|
||||
|
||||
for comp in proj.components.values():
|
||||
print(f"=== {comp.name} ({comp.id})")
|
||||
for sk in comp.sketches.values():
|
||||
occ = sk.occ_sketch
|
||||
if occ is None:
|
||||
print(" sketch with no occ_sketch:", sk.id)
|
||||
continue
|
||||
for e in occ._entities.values():
|
||||
t = etype(e)
|
||||
if t == "circle":
|
||||
g = egeom(e)
|
||||
cid = e.get("id") if isinstance(e, dict) else e.id
|
||||
print(f" circle id={cid} center=({g[0][0]!r}, {g[0][1]!r}) r={g[1]!r}")
|
||||
for body in comp.bodies.values():
|
||||
if not body.geometry:
|
||||
print(f" body {body.name}: no geometry")
|
||||
continue
|
||||
shape = w._kernel._get_shape(body.geometry)
|
||||
print(f" body {body.name}: extrude len={body.extrude_length}")
|
||||
expl = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
while expl.More():
|
||||
face = TopoDS.Face_s(expl.Current())
|
||||
try:
|
||||
adaptor = BRepAdaptor_Surface(face)
|
||||
if adaptor.GetType() == GeomAbs_Cylinder:
|
||||
cyl = adaptor.Cylinder()
|
||||
loc = cyl.Location()
|
||||
d = cyl.Axis().Direction()
|
||||
print(
|
||||
f" cyl axis at ({loc.X()!r}, {loc.Y()!r}) "
|
||||
f"dir=({d.X():.4f},{d.Y():.4f},{d.Z():.4f})"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
expl.Next()
|
||||
|
||||
asm = proj.get_active_assembly()
|
||||
print("=== active assembly:", asm.name)
|
||||
for ac in asm.components.values():
|
||||
comp = proj.get_component_by_id(ac.component_id)
|
||||
print(f" instance '{ac.name}' -> {comp.name}, pos={np.round(ac.position, 4)}")
|
||||
for conn in ac.connectors.values():
|
||||
print(
|
||||
f" conn '{conn.name}' pos={np.round(conn.position, 6)} "
|
||||
f"normal={np.round(conn.normal, 3)} et={conn.entity_type!r} "
|
||||
f"invalid={conn.is_invalid}"
|
||||
)
|
||||
|
||||
# ── Run the real update path: rebuild bodies from sketch, recalc connectors ──
|
||||
# Activate the component with the holes (Component 1).
|
||||
comp1 = None
|
||||
for comp in proj.components.values():
|
||||
if any(
|
||||
etype(e) == "circle"
|
||||
for sk in comp.sketches.values()
|
||||
for e in (sk.occ_sketch._entities.values() if sk.occ_sketch else [])
|
||||
):
|
||||
comp1 = comp
|
||||
break
|
||||
assert comp1 is not None
|
||||
w._current_component = comp1
|
||||
print("=== running _update_bodies_from_sketch()")
|
||||
w._update_bodies_from_sketch()
|
||||
print("=== running _recalculate_connectors()")
|
||||
w._recalculate_connectors()
|
||||
|
||||
print("=== after update")
|
||||
for comp in proj.components.values():
|
||||
for body in comp.bodies.values():
|
||||
if not body.geometry:
|
||||
continue
|
||||
shape = w._kernel._get_shape(body.geometry)
|
||||
expl = TopExp_Explorer(shape, TopAbs_FACE)
|
||||
axes = []
|
||||
while expl.More():
|
||||
face = TopoDS.Face_s(expl.Current())
|
||||
try:
|
||||
adaptor = BRepAdaptor_Surface(face)
|
||||
if adaptor.GetType() == GeomAbs_Cylinder:
|
||||
loc = adaptor.Cylinder().Location()
|
||||
axes.append((round(loc.X(), 9), round(loc.Y(), 9)))
|
||||
except Exception:
|
||||
pass
|
||||
expl.Next()
|
||||
print(f" {comp.name} body axes: {axes}")
|
||||
for sk in comp.sketches.values():
|
||||
for e in sk.occ_sketch._entities.values():
|
||||
t = etype(e)
|
||||
if t == "circle":
|
||||
g = egeom(e)
|
||||
print(f" {comp.name} circle: ({g[0][0]!r}, {g[0][1]!r})")
|
||||
|
||||
asm = proj.get_active_assembly()
|
||||
for ac in asm.components.values():
|
||||
for conn in ac.connectors.values():
|
||||
print(
|
||||
f" conn '{conn.name}' pos={np.round(conn.position, 6)} "
|
||||
f"invalid={conn.is_invalid}"
|
||||
)
|
||||
|
||||
# Partner alignment check: for each connection, world positions of the pair.
|
||||
for aconn in asm.connections:
|
||||
a1 = asm.components.get(aconn.first_ac_id)
|
||||
a2 = asm.components.get(aconn.second_ac_id)
|
||||
c1 = a1.connectors.get(aconn.first_connector_id)
|
||||
c2 = a2.connectors.get(aconn.second_connector_id)
|
||||
if c1 is None or c2 is None:
|
||||
continue
|
||||
w1 = a1.position + a1.rotation @ np.asarray(c1.position, dtype=float)
|
||||
w2 = a2.position + a2.rotation @ np.asarray(c2.position, dtype=float)
|
||||
print(
|
||||
f" conn {aconn.id[:8]}: w1={np.round(w1, 6)} w2={np.round(w2, 6)} "
|
||||
f"gap={float(np.linalg.norm(w1 - w2))!r}"
|
||||
)
|
||||
|
||||
print("DEMO_REPRO_DONE")
|
||||
@@ -0,0 +1,89 @@
|
||||
"""Round-trip smoke test: instance-local sketches + modifiers survive save/load.
|
||||
|
||||
Builds a minimal project (component with a body, assembly with two instances,
|
||||
one carrying an instance sketch + cut modifier + fillet modifier), saves to a
|
||||
temp .fluency, reloads, and asserts:
|
||||
1. the shared component is untouched by instance work,
|
||||
2. the instance sketch + modifiers round-trip with sketch refs intact,
|
||||
3. the plain instance has none.
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import numpy as np
|
||||
|
||||
from fluency.models.data_model import (
|
||||
Project, Component, Body, Sketch, Feature, Assembly, AssemblyComponent,
|
||||
)
|
||||
from fluency.io.project_io import save_project, load_project
|
||||
|
||||
|
||||
def main():
|
||||
project = Project(name="inst test")
|
||||
comp = project.add_component()
|
||||
comp.name = "BasePart"
|
||||
body = comp.add_body(Body(name="MainBody"))
|
||||
base_sketch = comp.add_sketch(Sketch(name="BaseSketch"))
|
||||
body.features.append(
|
||||
Feature(operation="extrude", sketch=base_sketch, length=10.0)
|
||||
)
|
||||
|
||||
asm = project.add_assembly(Assembly(name="TestAsm"))
|
||||
ac1 = asm.add_component_instance(comp.id, name="Instance A")
|
||||
ac1.position = np.array([0.0, 0.0, 0.0])
|
||||
ac2 = asm.add_component_instance(comp.id, name="Instance B")
|
||||
ac2.position = np.array([50.0, 0.0, 0.0])
|
||||
|
||||
# Instance A: local sketch + cut modifier referencing it + fillet.
|
||||
inst_sketch = ac1.add_instance_sketch()
|
||||
inst_sketch.name = "InstCutSketch"
|
||||
ac1.add_modifier(body.id, Feature(operation="cut", sketch=inst_sketch,
|
||||
length=5.0, through_all=True))
|
||||
ac1.add_modifier(body.id, Feature(operation="fillet", radius=1.0))
|
||||
|
||||
# ---- save / load ----
|
||||
fd, path = tempfile.mkstemp(suffix=".fluency")
|
||||
os.close(fd)
|
||||
try:
|
||||
save_project(project, path)
|
||||
loaded, _view = load_project(path)
|
||||
|
||||
lcomp = loaded.components[comp.id]
|
||||
lac1 = None
|
||||
lac2 = None
|
||||
for lasm in loaded.assemblies.values():
|
||||
for ac in lasm.components.values():
|
||||
if ac.name == "Instance A":
|
||||
lac1 = ac
|
||||
elif ac.name == "Instance B":
|
||||
lac2 = ac
|
||||
assert lac1 is not None and lac2 is not None, "instances missing"
|
||||
|
||||
# 1. component untouched
|
||||
assert len(lcomp.sketches) == 1, "component sketch count changed"
|
||||
assert len(lcomp.bodies[body.id].features) == 1, "feature chain changed"
|
||||
assert not getattr(lcomp.bodies[body.id], "modifiers", None)
|
||||
|
||||
# 2. instance A round-trip
|
||||
assert len(lac1.sketches) == 1, "instance sketch missing"
|
||||
lsk_id, lsk = next(iter(lac1.sketches.items()))
|
||||
assert lsk.name == "InstCutSketch"
|
||||
mods = lac1.modifiers
|
||||
lbody_id = next(iter(lcomp.bodies))
|
||||
assert len(mods.get(lbody_id, [])) == 2, f"modifiers missing: {mods}"
|
||||
cut, fil = mods[lbody_id][0], mods[lbody_id][1]
|
||||
assert cut.operation == "cut" and fil.operation == "fillet"
|
||||
assert cut.sketch is not None and cut.sketch.id == lsk_id, \
|
||||
"cut sketch ref did not resolve to instance sketch"
|
||||
assert cut.length == 5.0 and cut.through_all
|
||||
|
||||
# 3. plain instance clean
|
||||
assert not lac2.sketches and not lac2.modifiers
|
||||
|
||||
print("ROUND_TRIP_OK")
|
||||
finally:
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Headless smoke: the manual re-pick fallback path (prompt -> frame -> pick mode)."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
|
||||
|
||||
import numpy as np
|
||||
|
||||
from OCP.gp import gp_Pnt, gp_Dir, gp_Ax2
|
||||
from OCP.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder
|
||||
from OCP.BRepAlgoAPI import BRepAlgoAPI_Cut
|
||||
|
||||
from PySide6.QtWidgets import QApplication, QMessageBox
|
||||
|
||||
from fluency.ui.main_window import MainWindow
|
||||
from fluency.models.data_model import Body
|
||||
from fluency.geometry_occ.kernel import OCCGeometryObject
|
||||
|
||||
app = QApplication.instance() or QApplication([])
|
||||
w = MainWindow()
|
||||
|
||||
comp = w._current_component
|
||||
body = Body(name="plate")
|
||||
comp.bodies[body.id] = body
|
||||
box = BRepPrimAPI_MakeBox(60.0, 40.0, 5.0).Shape()
|
||||
ax = gp_Ax2(gp_Pnt(10.0, 5.0, 0.0), gp_Dir(0, 0, 1))
|
||||
box = BRepAlgoAPI_Cut(box, BRepPrimAPI_MakeCylinder(ax, 3.0, 6.0).Shape()).Shape()
|
||||
body.geometry = OCCGeometryObject(box)
|
||||
|
||||
partner = w._project.add_component()
|
||||
pbody = Body(name="partner")
|
||||
partner.bodies[pbody.id] = pbody
|
||||
pbody.geometry = OCCGeometryObject(BRepPrimAPI_MakeBox(30.0, 30.0, 10.0).Shape())
|
||||
|
||||
asm = w._project.get_active_assembly()
|
||||
ac1 = asm.add_component_instance(comp.id, name="A")
|
||||
ac2 = asm.add_component_instance(partner.id, name="B")
|
||||
ac1.geom_cache[body.id] = body.geometry
|
||||
c1 = ac1.add_connector(
|
||||
position=(10.0, 5.0, 2.5), normal=(0, 0, 1), x_dir=(1, 0, 0),
|
||||
source_obj_id=f"asm_{ac1.id}_{body.id}",
|
||||
name="Conn hole", entity_type="cylindrical_face",
|
||||
)
|
||||
c1.partner_ac_id = ac2.id
|
||||
|
||||
# Activate the assembly view so the prompt path is taken.
|
||||
w._assembly_view_active = True
|
||||
w._selected_assembly_component_id = ac1.id
|
||||
|
||||
# Force a Yes from the question dialog, and record that it actually fired.
|
||||
asked = {}
|
||||
def fake_question(parent, title, text, buttons, default):
|
||||
asked["title"] = title
|
||||
return QMessageBox.StandardButton.Yes
|
||||
QMessageBox.question = staticmethod(fake_question)
|
||||
def fake_warning(*a, **k):
|
||||
return QMessageBox.StandardButton.Ok
|
||||
QMessageBox.warning = staticmethod(fake_warning)
|
||||
|
||||
w._prompt_relocate_unresolved([(asm, ac1, c1)])
|
||||
assert asked.get("title") == "Connector Position Needed", asked
|
||||
assert w._relocate_pending == [(asm, ac1, c1)], w._relocate_pending
|
||||
assert w._viewer_3d._connector_pick_mode, "pick mode must be active"
|
||||
|
||||
# A wrong-part click must not consume the pending entry.
|
||||
w._on_relocate_picked((0, 0, 0), (0, 0, 1), (1, 0, 0), "planar_face", f"asm_{ac2.id}_{pbody.id}")
|
||||
assert w._relocate_pending == [(asm, ac1, c1)], "wrong part must not consume the pick"
|
||||
|
||||
# The right click re-homes and drains.
|
||||
w._on_relocate_picked((40.0, 30.0, 2.5), (0, 0, 1), (1, 0, 0), "cylindrical_face", f"asm_{ac1.id}_{body.id}")
|
||||
assert w._relocate_pending is None
|
||||
assert np.allclose(c1.position, (40.0, 30.0, 2.5)), c1.position
|
||||
assert not w._viewer_3d._connector_pick_mode, "pick mode must be off after completion"
|
||||
|
||||
# Esc cancel mid-flight clears the state.
|
||||
w._relocate_pending = [(asm, ac1, c1)]
|
||||
w._start_relocate_pick_next()
|
||||
w._on_connector_pick_cancelled()
|
||||
assert w._relocate_pending is None
|
||||
assert not w._viewer_3d._connector_pick_mode
|
||||
print("RELOCATE_PICK_FALLBACK_OK")
|
||||
@@ -0,0 +1,149 @@
|
||||
"""Headless test: update_external_entities handles circle/arc dict entries.
|
||||
|
||||
Reproduces the crash where a re-projected face contains a circular edge
|
||||
(e.g. an instance cut hole): _project_face_to_uv returns a mixed list of
|
||||
polylines + curve dicts, and update_external_entities used to unpack the
|
||||
dict entries as (u, v) tuples.
|
||||
|
||||
Covers:
|
||||
1. mixed projection (polylines + circle dict) -> rebuild + rebind path,
|
||||
no crash, no duplicate/orphan curve entities, user geometry re-anchored.
|
||||
2. repeated update with the same mixed projection -> stable (idempotent).
|
||||
3. polylines-only same-topology projection -> in-place path still works
|
||||
(external ids preserved).
|
||||
"""
|
||||
import math
|
||||
|
||||
from fluency.geometry_occ.sketch import OCCSketch
|
||||
|
||||
RECT = [
|
||||
[(0.0, 0.0), (10.0, 0.0)],
|
||||
[(10.0, 0.0), (10.0, 10.0)],
|
||||
[(10.0, 10.0), (0.0, 10.0)],
|
||||
[(0.0, 10.0), (0.0, 0.0)],
|
||||
]
|
||||
|
||||
|
||||
def _counts(sk):
|
||||
ents = list(sk._entities.values())
|
||||
return {
|
||||
"ext_points": sum(
|
||||
1
|
||||
for e in ents
|
||||
if e.entity_type == "point" and getattr(e, "is_external", False)
|
||||
),
|
||||
"ext_lines": sum(
|
||||
1
|
||||
for e in ents
|
||||
if e.entity_type == "line" and getattr(e, "is_external", False)
|
||||
),
|
||||
"circles": sum(1 for e in ents if e.entity_type == "circle"),
|
||||
"arcs": sum(1 for e in ents if e.entity_type == "arc"),
|
||||
"user_points": sum(
|
||||
1
|
||||
for e in ents
|
||||
if e.entity_type == "point" and not getattr(e, "is_external", False)
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def test_mixed_projection_rebuild():
|
||||
sk = OCCSketch()
|
||||
sk.add_external_polylines([list(p) for p in RECT])
|
||||
center = sk.add_external_point(5.0, 5.0)
|
||||
sk.add_circle(center, 2.0)
|
||||
user = sk.add_point(5.0, 5.0)
|
||||
assert sk.constrain_coincident(user, center)
|
||||
assert sk.solve()
|
||||
|
||||
# Re-projection: same rectangle, circle moved + resized -> mixed list.
|
||||
new_proj = [
|
||||
list(p) for p in RECT
|
||||
] + [
|
||||
{"type": "circle", "center": [6.0, 6.0], "radius": 1.5},
|
||||
]
|
||||
old_ext_ids = set(sk._external_entity_ids)
|
||||
assert sk.update_external_entities(new_proj), "rebuild + rebind solve failed"
|
||||
|
||||
c = _counts(sk)
|
||||
assert c["circles"] == 1, f"duplicate circle entities: {c}"
|
||||
assert c["ext_points"] == 5, f"ext point count wrong (expect 4 corners + 1 centre): {c}"
|
||||
assert c["ext_lines"] == 4, f"ext line count wrong (expect 4): {c}"
|
||||
assert c["user_points"] == 1
|
||||
# The coincident rebind must anchor the user point to the NEW centre.
|
||||
ux, uy = user.geometry
|
||||
assert math.hypot(ux - 6.0, uy - 6.0) < 1e-6, f"user point at {(ux, uy)}"
|
||||
# Rebuild path: fresh external ids.
|
||||
assert not (old_ext_ids & sk._external_entity_ids)
|
||||
|
||||
# Idempotent second pass with the same projection.
|
||||
assert sk.update_external_entities(list(new_proj)), "second pass failed"
|
||||
c2 = _counts(sk)
|
||||
assert c2 == c, f"counts changed on second pass: {c} -> {c2}"
|
||||
ux, uy = user.geometry
|
||||
assert math.hypot(ux - 6.0, uy - 6.0) < 1e-6
|
||||
print("test_mixed_projection_rebuild OK")
|
||||
|
||||
|
||||
def test_polylines_only_inplace():
|
||||
sk = OCCSketch()
|
||||
sk.add_external_polylines([list(p) for p in RECT])
|
||||
corner = None
|
||||
for eid in sk._external_entity_ids:
|
||||
ent = sk._entities[eid]
|
||||
if ent.entity_type == "point" and ent.geometry == (0.0, 0.0):
|
||||
corner = ent
|
||||
break
|
||||
assert corner is not None
|
||||
user = sk.add_point(0.0, 0.0)
|
||||
assert sk.constrain_coincident(user, corner)
|
||||
assert sk.solve()
|
||||
|
||||
# Same topology, slightly shifted rectangle -> in-place move.
|
||||
moved = [[(u + 1.0, v + 2.0) for (u, v) in poly] for poly in RECT]
|
||||
old_ext_ids = set(sk._external_entity_ids)
|
||||
assert sk.update_external_entities(moved), "in-place solve failed"
|
||||
assert sk._external_entity_ids == old_ext_ids, "in-place path must keep ids"
|
||||
ux, uy = user.geometry
|
||||
assert math.hypot(ux - 1.0, uy - 2.0) < 1e-6, f"user point at {(ux, uy)}"
|
||||
print("test_polylines_only_inplace OK")
|
||||
|
||||
|
||||
def test_arc_import_shares_corners():
|
||||
"""_import_external_curves must merge arc endpoints with existing
|
||||
polyline corner points (no floating duplicate endpoints)."""
|
||||
sk = OCCSketch()
|
||||
# Rectangle with the top-right corner filleted: the arc endpoints must
|
||||
# land on the truncated-edge corner points, not create new ones.
|
||||
r = 2.0
|
||||
sk.add_external_polylines([
|
||||
[(0.0, 0.0), (10.0, 0.0)],
|
||||
[(10.0, 0.0), (10.0, 10.0 - r)],
|
||||
[(10.0 - r, 10.0), (0.0, 10.0)],
|
||||
[(0.0, 10.0), (0.0, 0.0)],
|
||||
])
|
||||
sk._import_external_curves(
|
||||
[],
|
||||
[
|
||||
{
|
||||
"type": "arc",
|
||||
"center": [10.0 - r, 10.0 - r],
|
||||
"start": [10.0, 10.0 - r],
|
||||
"end": [10.0 - r, 10.0],
|
||||
"radius": r,
|
||||
},
|
||||
],
|
||||
)
|
||||
c = _counts(sk)
|
||||
# 5 corners + 1 arc centre, NO extra endpoint entities.
|
||||
assert c["ext_points"] == 6, f"expected 6 ext points, got {c}"
|
||||
assert c["arcs"] == 1, f"expected 1 arc, got {c}"
|
||||
assert sk.solve()
|
||||
print("test_arc_import_shares_corners OK")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_mixed_projection_rebuild()
|
||||
test_polylines_only_inplace()
|
||||
test_arc_import_shares_corners()
|
||||
print("UNDERLAY_CURVES_OK")
|
||||
@@ -0,0 +1,111 @@
|
||||
# Fluency CAD 2.0
|
||||
|
||||
A parametric CAD application built on OpenCASCADE Technology (OCCT) with a modern pygfx-based 3D renderer.
|
||||
|
||||
## Features
|
||||
|
||||
- **OpenCASCADE Geometry Kernel**: Industry-standard BRep geometry with exact precision
|
||||
- **STEP/IGES Import/Export**: Full support for industry-standard CAD file formats
|
||||
- **Parametric Sketching**: 2D sketching with constraint solving using SolveSpace
|
||||
- **Boolean Operations**: Union, difference, and intersection
|
||||
- **Fillet & Chamfer**: Apply edge treatments to solid bodies
|
||||
- **Modern Renderer**: WebGPU-based rendering with pygfx (smaller footprint than VTK)
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
fluency/
|
||||
├── src/fluency/
|
||||
│ ├── geometry/ # Geometry abstraction layer
|
||||
│ │ └── base.py # Abstract interfaces
|
||||
│ ├── geometry_occ/ # OpenCASCADE implementation
|
||||
│ │ ├── kernel.py # OCGeometryKernel
|
||||
│ │ └── sketch.py # OCCSketch with constraints
|
||||
│ ├── rendering/ # Rendering abstraction
|
||||
│ │ ├── base.py # Abstract renderer
|
||||
│ │ └── pygfx_renderer.py
|
||||
│ ├── models/ # Data models
|
||||
│ │ └── data_model.py # Project, Component, Sketch, Body
|
||||
│ └── main.py # Application entry point
|
||||
├── tests/
|
||||
│ └── test_geometry.py
|
||||
└── pyproject.toml
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Create virtual environment
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
||||
|
||||
# Install dependencies
|
||||
pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| cadquery-ocp | OpenCASCADE Python bindings (OCP) |
|
||||
| pygfx | WebGPU-based 3D renderer |
|
||||
| wgpu | WebGPU Python bindings |
|
||||
| PySide6 | Qt GUI framework |
|
||||
| numpy | Numerical computing |
|
||||
| scipy | Scientific computing |
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Run the application
|
||||
fluency-cad
|
||||
|
||||
# Or directly
|
||||
python -m fluency.main
|
||||
```
|
||||
|
||||
## API Example
|
||||
|
||||
```python
|
||||
from fluency.geometry_occ.kernel import OCGeometryKernel
|
||||
from fluency.geometry.base import Point2D
|
||||
|
||||
# Create kernel
|
||||
kernel = OCGeometryKernel()
|
||||
|
||||
# Create a sketch
|
||||
points = [
|
||||
Point2D(0, 0),
|
||||
Point2D(10, 0),
|
||||
Point2D(10, 10),
|
||||
Point2D(0, 10),
|
||||
]
|
||||
polygon = kernel.create_polygon(points)
|
||||
|
||||
# Extrude to 3D
|
||||
body = kernel.extrude(polygon, height=20.0)
|
||||
|
||||
# Apply fillet
|
||||
body = kernel.fillet(body, radius=2.0)
|
||||
|
||||
# Export to STEP
|
||||
kernel.export_step(body, "part.step")
|
||||
|
||||
# Export to STL
|
||||
kernel.export_stl(body, "part.stl")
|
||||
```
|
||||
|
||||
## Comparison: Before vs After
|
||||
|
||||
| Aspect | Before (SDF + VTK) | After (OCC + pygfx) |
|
||||
|--------|-------------------|---------------------|
|
||||
| Geometry Precision | Approximate (mesh) | Exact (BRep) |
|
||||
| Export Formats | STL only | STEP, IGES, STL, BREP |
|
||||
| File Size | Large (mesh) | Small (BRep) |
|
||||
| Fillet/Chamfer | Approximate | Exact |
|
||||
| Dependency Size | ~200MB (VTK) | ~30MB (pygfx) |
|
||||
| Constraint Solver | SolveSpace (separate) | SolveSpace (integrated) |
|
||||
|
||||
## License
|
||||
|
||||
MIT License
|
||||
@@ -0,0 +1,219 @@
|
||||
# Fluency CAD — Agent Guide
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Fluency CAD 2.0** is a parametric CAD application built on **OpenCASCADE Technology (OCCT)** with a modern **pygfx-based 3D renderer**. It provides 2D sketching with SolveSpace constraint solving, boolean operations, STEP/IGES/STL import/export, and exact BRep geometry.
|
||||
|
||||
- Language: **Python 3.10+**
|
||||
- GUI: **PySide6** (Qt6)
|
||||
- Geometry Kernel: **OCP** (cadquery-ocp — OpenCASCADE Python bindings)
|
||||
- Constraint Solver: **python_solvespace**
|
||||
- Renderer: **pygfx** (WebGPU) + **OCCRenderer** (native OCC AIS display)
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/fluency/
|
||||
├── __init__.py # Package entry (version, imports)
|
||||
├── main.py # Application entry point, MainWindow, Sketch2DWidget (~5000 lines)
|
||||
├── sketch_solver.py # SolveSpace constraint solver wrapper (legacy)
|
||||
├── geometry/
|
||||
│ └── base.py # Abstract interfaces: GeometryKernel, SketchInterface, data classes
|
||||
├── geometry_occ/
|
||||
│ ├── kernel.py # OCGeometryKernel (extrude, boolean, fillet, import/export, mesh)
|
||||
│ └── sketch.py # OCCSketch with SolveSpace integration (face detection, constraints)
|
||||
├── models/
|
||||
│ └── data_model.py # Project, Component, Sketch, Body dataclasses
|
||||
├── rendering/
|
||||
│ ├── base.py # Abstract Renderer interface
|
||||
│ ├── occ_renderer.py # OCC AIS renderer (preferred — smooth BRep display)
|
||||
│ └── pygfx_renderer.py # Legacy pygfx renderer
|
||||
├── utils/ # Utility modules
|
||||
└── widgets/ # Custom widgets
|
||||
tests/
|
||||
└── test_geometry.py # Comprehensive test suite (52+ tests)
|
||||
```
|
||||
|
||||
### Key Classes & Responsibilities
|
||||
|
||||
| Class | File | Purpose |
|
||||
|-------|------|---------|
|
||||
| `OCGeometryKernel` | `kernel.py` | OCC shape ops: extrude, boolean, fillet, mesh, import/export |
|
||||
| `OCCSketch` | `sketch.py` | 2D sketch with SolveSpace solver, face detection, workplane |
|
||||
| `OCCSketchEntity` | `sketch.py` | Entity (point/line/circle/arc) with solver handle, is_construction, is_external |
|
||||
| `Sketch2DWidget` | `main.py` | Qt widget for interactive 2D sketching (draw, snap, constrain) |
|
||||
| `MainWindow` | `main.py` | Main application window, toolbars, 3D viewer, operations |
|
||||
| `OCCRenderer` | `occ_renderer.py` | Native OCC AIS display (shaded + edges, face pick) |
|
||||
| `Sketch` | `data_model.py` | Data model: workplane, occ_sketch ref, source_body_id |
|
||||
| `Body` | `data_model.py` | 3D solid with geometry, visibility, render object |
|
||||
| `Component` | `data_model.py` | Container for sketches and bodies |
|
||||
| `Project` | `data_model.py` | Top-level container with kernel |
|
||||
|
||||
### Data Flow
|
||||
|
||||
```
|
||||
User draws in Sketch2DWidget
|
||||
→ OCCSketch entities created in solver
|
||||
→ Constraint solving (python_solvespace)
|
||||
→ OCCSketch.get_geometry() → detect_faces() → build_face_geometry()
|
||||
→ OCGeometryKernel.extrude() → BRepPrimAPI_MakePrism
|
||||
→ Boolean operations → Body added to Component
|
||||
→ OCCRenderer.add_shape() → AIS display
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Install editable
|
||||
pip install -e ".[dev]"
|
||||
|
||||
# Run app
|
||||
python -m fluency.main
|
||||
|
||||
# Run tests (52 tests)
|
||||
python -m pytest tests/test_geometry.py -v
|
||||
|
||||
# Run single test
|
||||
python -m pytest tests/test_geometry.py::TestOCCSketch::test_workplane_extrude_with_hole -xvs
|
||||
|
||||
# Quck geometry test (raw OCC, no Qt)
|
||||
python -c "from fluency.geometry_occ.sketch import OCCSketch; ..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Code Conventions
|
||||
|
||||
### General
|
||||
- Line length: **100 chars** (black/ruff config)
|
||||
- Target Python: **3.10+** (uses `from __future__ import annotations`, walrus, pattern matching)
|
||||
- Docstrings: Google/NumPy style preferred
|
||||
- Logging: `logger = logging.getLogger(__name__)` with `logging.DEBUG` level
|
||||
|
||||
### OCC / OCP
|
||||
- Always use `is not None` for OCP objects — `TopoDS_Shape.__bool__` can be falsy even for valid shapes
|
||||
- `BRepBuilderAPI_MakeFace.Add(wire)` expects a `TopoDS_Wire`. `wire.Reversed()` returns `TopoDS_Shape` → cast via `_TopoDS.Wire_s(wire.Reversed())`
|
||||
- Face normal direction: check `face.Orientation()` vs `TopAbs_REVERSED` — a REVERSED face's outward normal is the NEGATION of the surface axis
|
||||
- `TopoDS_Wire_s(shape)`, `TopoDS_Face_s(shape)` — use `_s` suffix from OCP for downcasts
|
||||
- Mesh: `BRepMesh_IncrementalMesh(shape, tol, False, 0.15, True)` — default deflection 0.15 rad for smooth curves
|
||||
|
||||
### Extrude / Cut Workflow
|
||||
- Snapshot `list(self._current_component.bodies.items())` **BEFORE** `add_body()` — the new body must not be in the target set
|
||||
- Cut targets the **source body** (`sketch._source_body_id` from face pick), not `bodies[0]`
|
||||
- The fix: apply boolean to **target** geometry, then remove tool body
|
||||
- Plain extrude with holes: inner wires must have **OPPOSITE** geometric winding to the outer wire (see `build_face_geometry` and `_loop_signed_area`)
|
||||
|
||||
### Sketch / Solvers
|
||||
- `python_solvespace` has NO remove API for entities/constraints. Deleting requires: drop from `_points`/`_lines`, prune `_constraint_log`, `_rebuild_solver()` (recreates entire system), `_rebuild_labels()`, re-solve
|
||||
- `_constraint_log`: each entry is `{"type": str, "ids": tuple[int,...], "params": tuple, "labels": set[str]}`
|
||||
- Constraint labels: stored on **point** entities for paintEvent rendering; rebuilt via `_rebuild_labels()`
|
||||
- Line constraints (`horizontal`/`vertical`/`parallel`/`perpendicular`) need the **line's** solver handle, not a point's. Use `_find_line_sketch_entity()` to get the correct handle
|
||||
- External entities (underlay): `is_external=True`, `is_construction=True`, fixed in solver (always `dragged`). Stored in `_external_entity_ids`, excluded from `_line_segments()`, `get_polygon_points()`, `get_closed_loops()`, `detect_faces()`, `get_geometry()`
|
||||
|
||||
### Face Detection
|
||||
- `get_closed_loops()`: uses snapped-coordinate graph (`_SNAP_TOL = 1e-4`) from line endpoint adjacency. Only accepts simple cycles (all nodes degree 2)
|
||||
- `detect_faces()`: even-odd nesting rule via `_loop_contains`. Even depth = outer boundary, odd = hole
|
||||
- `_loop_rep_point`: midpoint between centroid and first vertex. **Fragile** — can land inside a nested shape for certain geometries (e.g., a small hole near the centroid's direction from the first vertex)
|
||||
- `_loop_signed_area`: shoelace formula for polygons, `πr²` (positive = CCW) for circles
|
||||
|
||||
### Rendering
|
||||
- **OCCRenderer** is the main renderer (not pygfx). Uses `AIS_Shape`, `V3d_Viewer`, `AIS_InteractiveContext`
|
||||
- Face pick: `pick_planar_face(x, y)` → `MoveTo` → `DetectedShape` → `TopoDS_Face_s` → `BRepAdaptor_Surface` plane check
|
||||
- Highlight: `highlight_face(face)` creates a transparent AIS overlay; `clear_face_highlight()` removes it
|
||||
- Preview: `preview_shape(shape)` for live transparent extrude preview
|
||||
- Navigation: Left=orbit, Middle=pan, Wheel=zoom. **Right is RESERVED** — check user before reassigning
|
||||
|
||||
### Paint-Event Safety
|
||||
- Every constraint-tag rendering loop wraps each entry in `try/except` so a bad entry (dangling id, corrupted geometry) doesn't crash the entire paint event
|
||||
- `_point_world()` and `_entity_anchor()` return `None` (not raise) for malformed input
|
||||
|
||||
---
|
||||
|
||||
## Known Bugs & Fix Patterns
|
||||
|
||||
### 1. Hole Orientation in Extrusion (FIXED 2026-07-03)
|
||||
**Symptom**: Inner shapes (circle/triangle/slot) inside a rectangle become solid islands instead of holes when extruding, depending on drag direction.
|
||||
|
||||
**Root Cause**: `wire_loop` in `build_face_geometry` unconditionally reversed hole wires (`w.Reversed()`). When the outer polygon was CW-winding (e.g., dragging from top-left to bottom-right), the reversed inner had the SAME effective direction as the outer, making OCC treat it as solid.
|
||||
|
||||
**Fix**: Added `_loop_signed_area()` to compute geometric winding. Hole wires are only reversed when their natural winding matches the outer's (ensuring opposite winding for holes).
|
||||
|
||||
**Relevant code**: `sketch.py`, `build_face_geometry()` and `_loop_signed_area()`
|
||||
|
||||
### 2. _loop_rep_point Fragility (KNOWN)
|
||||
**Symptom**: Face detection fails when a nested shape contains the outer loop's representative point (midpoint between centroid and first vertex).
|
||||
|
||||
**Would-be fix**: Use a guaranteed-interior point (maximum inscribed circle center or perturbed centroid) instead of the centroid-first-vertex midpoint.
|
||||
|
||||
### 3. Extrude Cut / Target Selection (FIXED 2026-06-29)
|
||||
**Symptom**: Cut created a separate "cavity-shaped" body next to the original instead of modifying the target.
|
||||
|
||||
**Fix**: Boolean result stored on TARGET body geometry; tool body removed from component. Auto-target via `sketch._source_body_id`.
|
||||
|
||||
### 4. Workplane Preservation (FIXED 2026-06-29)
|
||||
**Symptom**: Sketch placed on a face lost its workplane after being added to component.
|
||||
|
||||
**Fix**: Copy `occ_sketch` workplane fields into `Sketch` dataclass BEFORE `apply_workplane()`.
|
||||
|
||||
---
|
||||
|
||||
## API Quirks
|
||||
|
||||
- **`QPoint(0,0)`**: falsy via `isNull()` in PySide6 → always use `is not None` for `Optional[QPoint]`
|
||||
- **`QMouseEvent`/`QPainterPath`**: live in `PySide6.QtGui` (NOT `QtCore`)
|
||||
- **`BRepBuilderAPI_MakeFace.Add()`**: needs `TopoDS_Wire`. `wire.Reversed()` returns `TopoDS_Shape` — cast via `TopoDS_Wire_s()`
|
||||
- **python_solvespace**: NO entity/constraint remove API — workaround via `_rebuild_solver()`. Parameters read via `solver.params(handle.params)` → returns `(x, y)` tuple
|
||||
- **OCGeometryKernel.extrude**: unwraps `OCCGeometryObject`, raw `TopoDS_Shape`, or cadquery `Workplane`. Always use `is not None` for the shape (not truthiness)
|
||||
- **Sketch._source_body_id**: dynamic attribute set on `Sketch` dataclass, set during face-pick flow
|
||||
- **`_get_shape(obj)`**: returns `obj.shape.wrapped` for `OCCGeometryObject`, `obj.shape` for raw shapes, `None` for empty. Use `is not None` guards everywhere
|
||||
|
||||
---
|
||||
|
||||
## Memory / Agent Context
|
||||
|
||||
This project has extensive Pi memory (hermes-memory) for:
|
||||
- `project="fluency"` with `target="failure"`: bugs, fixes, corrections, insights
|
||||
- `project="fluency"` with `target="memory"`: conventions, decisions, workflow patterns
|
||||
- Available skills: `fix-cad-app-pipeline`, `refactor-from-cadquery-to-ocp`
|
||||
|
||||
Key memory queries for debugging:
|
||||
- "hole orientation" → `_loop_signed_area` / `build_face_geometry` fix
|
||||
- "extrude cut auto-target" → cut/target body fix
|
||||
- "workplane preservation" → _add_sketch_to_component fix
|
||||
- "_loop_rep_point" → face detection fragility
|
||||
- "paint-event safety" → try/except per entry pattern
|
||||
- "solver rebuild" → delete workflow via _rebuild_solver
|
||||
- "face pick origin" → pick_planar_face face bbox centre
|
||||
|
||||
---
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
```python
|
||||
# Direct OCC test (no Qt)
|
||||
from OCP.BRepBuilderAPI import BRepBuilderAPI_MakePolygon, BRepBuilderAPI_MakeFace
|
||||
from OCP.gp import gp_Pnt
|
||||
from OCP.BRepPrimAPI import BRepPrimAPI_MakePrism
|
||||
from OCP.GProp import GProp_GProps; from OCP.BRepGProp import BRepGProp
|
||||
|
||||
# Build test shape, extrude, verify volume
|
||||
mp = BRepBuilderAPI_MakePolygon(); ...
|
||||
g = GProp_GProps(); BRepGProp.VolumeProperties_s(shape, g)
|
||||
assert abs(g.Mass() - expected) < 0.1
|
||||
```
|
||||
|
||||
```python
|
||||
# Sketch-based test
|
||||
from fluency.geometry_occ.sketch import OCCSketch
|
||||
from fluency.geometry_occ.kernel import OCGeometryKernel
|
||||
|
||||
sk = OCCSketch()
|
||||
# ... add points, lines, circles ...
|
||||
sk.solve()
|
||||
geom = sk.get_geometry()
|
||||
solid = OCGeometryKernel().extrude(geom, 10.0)
|
||||
```
|
||||
Binary file not shown.
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Debug script to test point dragging functionality in ImprovedSketchWidget
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.append('/Volumes/Data_drive/Programming/fluency')
|
||||
|
||||
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QHBoxLayout
|
||||
from PySide6.QtCore import Qt
|
||||
from drawing_modules.improved_sketcher import ImprovedSketchWidget, SketchMode, Point2D
|
||||
import logging
|
||||
|
||||
# Set up logging to see debug messages
|
||||
logging.basicConfig(level=logging.DEBUG, format='%(levelname)s: %(message)s')
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DebugMainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Debug Point Dragging")
|
||||
self.resize(1000, 700)
|
||||
|
||||
# Create central widget
|
||||
central_widget = QWidget()
|
||||
self.setCentralWidget(central_widget)
|
||||
layout = QVBoxLayout(central_widget)
|
||||
|
||||
# Create button layout
|
||||
button_layout = QHBoxLayout()
|
||||
|
||||
# Add test points button
|
||||
add_points_btn = QPushButton("Add Test Points")
|
||||
add_points_btn.clicked.connect(self.add_test_points)
|
||||
button_layout.addWidget(add_points_btn)
|
||||
|
||||
# Check mode button
|
||||
check_mode_btn = QPushButton("Check Mode")
|
||||
check_mode_btn.clicked.connect(self.check_mode)
|
||||
button_layout.addWidget(check_mode_btn)
|
||||
|
||||
# Reset mode button
|
||||
reset_mode_btn = QPushButton("Reset to NONE Mode")
|
||||
reset_mode_btn.clicked.connect(self.reset_mode)
|
||||
button_layout.addWidget(reset_mode_btn)
|
||||
|
||||
layout.addLayout(button_layout)
|
||||
|
||||
# Create the sketcher widget
|
||||
self.sketcher = ImprovedSketchWidget()
|
||||
layout.addWidget(self.sketcher)
|
||||
|
||||
print("Debug window created. Current mode:", self.sketcher.current_mode)
|
||||
|
||||
def add_test_points(self):
|
||||
"""Add some test points to the sketch"""
|
||||
print("Adding test points...")
|
||||
|
||||
# Add a few points at different locations
|
||||
points = [
|
||||
Point2D(100, 100),
|
||||
Point2D(200, 150),
|
||||
Point2D(150, 200),
|
||||
Point2D(50, 250)
|
||||
]
|
||||
|
||||
for point in points:
|
||||
self.sketcher.sketch.add_point(point)
|
||||
print(f"Added point at ({point.x}, {point.y})")
|
||||
|
||||
self.sketcher.update()
|
||||
print(f"Total points in sketch: {len(self.sketcher.sketch.points)}")
|
||||
|
||||
def check_mode(self):
|
||||
"""Check current mode and dragging state"""
|
||||
print(f"Current mode: {self.sketcher.current_mode}")
|
||||
print(f"Dragging point: {self.sketcher.dragging_point}")
|
||||
print(f"Drag start pos: {self.sketcher.drag_start_pos}")
|
||||
print(f"Hovered point: {self.sketcher.hovered_point}")
|
||||
print(f"Number of points: {len(self.sketcher.sketch.points)}")
|
||||
|
||||
# Check if points have solver handles
|
||||
for i, point in enumerate(self.sketcher.sketch.points):
|
||||
print(f"Point {i}: ({point.x}, {point.y}), handle: {point.handle}")
|
||||
|
||||
def reset_mode(self):
|
||||
"""Reset to NONE mode to enable dragging"""
|
||||
print("Resetting mode to NONE")
|
||||
self.sketcher.set_mode(SketchMode.NONE)
|
||||
print(f"Mode after reset: {self.sketcher.current_mode}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
window = DebugMainWindow()
|
||||
window.show()
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("DEBUG INSTRUCTIONS:")
|
||||
print("1. Click 'Add Test Points' to create some points")
|
||||
print("2. Click 'Check Mode' to verify the current state")
|
||||
print("3. Click 'Reset to NONE Mode' to ensure dragging is enabled")
|
||||
print("4. Try to drag points by clicking and dragging them")
|
||||
print("5. Watch the console for debug messages")
|
||||
print("="*50 + "\n")
|
||||
|
||||
sys.exit(app.exec())
|
||||
@@ -1 +0,0 @@
|
||||
pyside6-uic gui.ui > Gui.py -g python
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
# Signal Flow
|
||||
## 2D SketchWidget
|
||||
|
||||
- 2D QPoint form custom Qpainter widget in linear space
|
||||
- 2D QPoint ot cartesian space
|
||||
- 2D tuple into slvspace dict system and solvespace
|
||||
- get calced position from Solvespace solver
|
||||
- add to internal reference dict
|
||||
- Transform to linear QPainter space for display to show
|
||||
|
||||
## 3D custom Widget
|
||||
|
||||
- Take Tuple points form solvespace main dict
|
||||
- Draw Interactor and sdfCAD model
|
||||
|
||||
### Select and Project
|
||||
|
||||
- Project cartesian flattened mesh into 2D
|
||||
- Transform to 2D xy
|
||||
- Transform to linear space for 2D widget to draw.
|
||||
- Result into 2D cartesian for body interaction extrude etc
|
||||
|
||||
### Elements
|
||||
|
||||
So far these are the elements:
|
||||
|
||||
- Project: Main File
|
||||
- Timeline : Used to track the steps
|
||||
- Assembly: Uses Components and Connectors to from Assemblies
|
||||
- Component: Container for multiple smaller elements "part"
|
||||
- Connector: Preserves connections between parts even if the part in between is deleted
|
||||
- Code: A special type that directly builds bodys from sdfCAD code.
|
||||
- Body: The 3D meshed result from sdfCAD
|
||||
- Sketch: The base to draw new entities.
|
||||
- Interactor (edges): A special component mesh that is used to manipulate the bodys in 3d view.
|
||||
@@ -1,3 +0,0 @@
|
||||
## Compile ui file
|
||||
pyside6-uic gui.ui > Gui.py -g python
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
# Realistic Render View — Implementation Plan
|
||||
|
||||
## Context
|
||||
|
||||
Add a **"Render"** feature to Fluency CAD that opens a separate window for photorealistic rendering of the selected component or assembly (like KeyShot/Cacles).
|
||||
|
||||
**Constraints:**
|
||||
- Open in a **new window** — don't clutter the workspace
|
||||
- **Keep existing OCCRenderer** for the interactive 3D viewport — untouched
|
||||
- Render backend must be a **separate, swappable module** so we can change the renderer later
|
||||
- Use **Mitsuba 3** as the initial backend (`pip install mitsuba`, ~50MB)
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Main Fluency Window (existing OCCRenderer — untouched) │
|
||||
│ │
|
||||
│ [Select body/assembly] → [Click "Render"] │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────┐ │
|
||||
│ │ RenderWindow (separate QMainWindow)│ │
|
||||
│ │ │ │
|
||||
│ │ ┌───────────────────────────────┐ │ │
|
||||
│ │ │ RenderBackend (ABC) │ │ │
|
||||
│ │ │ ├─ MitsubaBackend ← current │ │ │
|
||||
│ │ │ ├─ (future: BlenderBackend) │ │ │
|
||||
│ │ │ └─ (future: CyclesBackend) │ │ │
|
||||
│ │ └───────────────────────────────┘ │ │
|
||||
│ │ │ │\n│ │ [Image preview] [Progress bar] │ │
|
||||
│ │ [Material ▾] [Quality ▾] [Render] │ │
|
||||
│ │ [Export PNG] │ │
|
||||
│ └─────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Swappable Backend Interface
|
||||
|
||||
```python
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
import numpy as np
|
||||
|
||||
@dataclass
|
||||
class RenderMaterial:
|
||||
name: str
|
||||
color: tuple[float, float, float] = (0.7, 0.7, 0.7)
|
||||
metallic: float = 0.0 # 0.0–1.0
|
||||
roughness: float = 0.5 # 0.0–1.0
|
||||
bsdf_type: str = "diffuse" # diffuse | roughconductor | roughdielectric | plastic
|
||||
|
||||
@dataclass
|
||||
class RenderCamera:
|
||||
origin: tuple[float, float, float] = (100, 100, 100)
|
||||
target: tuple[float, float, float] = (0, 0, 0)
|
||||
up: tuple[float, float, float] = (0, 0, 1)
|
||||
fov: float = 45.0
|
||||
|
||||
@dataclass
|
||||
class RenderSettings:
|
||||
width: int = 1920
|
||||
height: int = 1080
|
||||
spp: int = 256 # samples per pixel
|
||||
max_depth: int = 8 # path tracer bounces
|
||||
|
||||
class RenderBackend(ABC):
|
||||
"""Swap this to change the rendering engine."""
|
||||
@abstractmethod
|
||||
def render(self, obj_path: str, material: RenderMaterial,
|
||||
camera: RenderCamera, settings: RenderSettings) -> np.ndarray: ...
|
||||
@abstractmethod
|
||||
def render_preview(self, obj_path: str, material: RenderMaterial,
|
||||
camera: RenderCamera, settings: RenderSettings) -> np.ndarray: ...
|
||||
@abstractmethod
|
||||
def name(self) -> str: ...
|
||||
```
|
||||
|
||||
Switching backends later = write a new class implementing `RenderBackend`. One import change.
|
||||
|
||||
---
|
||||
|
||||
## Mitsuba 3 Backend
|
||||
|
||||
### Why Mitsuba
|
||||
|
||||
| Feature | Status |
|
||||
|---------|--------|
|
||||
| `pip install mitsuba` | Single install, no system deps |
|
||||
| True path tracing | GI, caustics, spectral rendering |
|
||||
| PBR materials | `roughconductor`, `roughdielectric`, `diffuse`, `plastic` |
|
||||
| Python dict API | Build scenes programmatically, no XML |
|
||||
| CPU + GPU backends | `scalar_rgb` (CPU), `cuda_rgb` (NVIDIA) |
|
||||
| Output formats | PNG, EXR (HDR) with tonemapping |
|
||||
|
||||
### OCC → OBJ Conversion Path
|
||||
|
||||
```python
|
||||
from OCP.BRepMesh import BRepMesh_IncrementalMesh
|
||||
from OCP.StlAPI import StlAPI_Writer
|
||||
from OCP.BRep import BRep_Builder
|
||||
import tempfile, os
|
||||
|
||||
def occ_shape_to_obj(shape, obj_path: str, linear_deflection: float = 0.1):
|
||||
"""Tessellate OCC shape and write as OBJ for Mitsuba."""
|
||||
tess = BRepMesh_IncrementalMesh(shape, linear_deflection, False, 0.5, True)
|
||||
tess.Perform()
|
||||
# Write STL (reliable), then convert to OBJ via trimesh or direct
|
||||
writer = StlAPI_Writer()
|
||||
writer.SetASCIIMode(False)
|
||||
stl_path = obj_path.replace(".obj", ".stl")
|
||||
writer.Write(shape, stl_path)
|
||||
# Mitsuba can read STL directly, or we convert to OBJ
|
||||
return stl_path
|
||||
```
|
||||
|
||||
### Mitsuba Scene Construction
|
||||
|
||||
```python
|
||||
import mitsuba as mi
|
||||
mi.set_variant("scalar_rgb")
|
||||
|
||||
def build_scene(mesh_path: str, material: RenderMaterial,
|
||||
camera: RenderCamera, settings: RenderSettings) -> mi.Scene:
|
||||
# Map our material to Mitsuba BSDF
|
||||
bsdf_map = {
|
||||
"diffuse": {"type": "diffuse", "reflectance": {"type": "rgb", "value": material.color}},
|
||||
"roughconductor": {
|
||||
"type": "roughconductor",
|
||||
"material": "copper", # or铝, 钢, etc.
|
||||
"alpha": material.roughness,
|
||||
},
|
||||
"roughdielectric": {
|
||||
"type": "roughdielectric",
|
||||
"int_ior": 1.5,
|
||||
"alpha": material.roughness,
|
||||
},
|
||||
"plastic": {
|
||||
"type": "plastic",
|
||||
"diffuse_reflectance": {"type": "rgb", "value": material.color},
|
||||
"int_ior": 1.5,
|
||||
},
|
||||
}
|
||||
|
||||
return mi.load_dict({
|
||||
"type": "scene",
|
||||
"integrator": {"type": "path", "max_depth": settings.max_depth},
|
||||
"sensor": {
|
||||
"type": "perspective",
|
||||
"fov": camera.fov,
|
||||
"to_world": mi.ScalarTransform4f.look_at(
|
||||
origin=camera.origin, target=camera.target, up=camera.up
|
||||
),
|
||||
"film": {"type": "hdrfilm", "width": settings.width, "height": settings.height},
|
||||
"sampler": {"type": "independent", "sample_count": settings.spp},
|
||||
},
|
||||
"emitter": {"type": "constant"},
|
||||
"shape": {
|
||||
"type": "stl", # or "obj"
|
||||
"filename": mesh_path,
|
||||
"bsdf": bsdf_map.get(material.bsdf_type, bsdf_map["diffuse"]),
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files to Create/Modify
|
||||
|
||||
| File | Action | Description |
|
||||
|------|--------|-------------|
|
||||
| `src/fluency/rendering/render_backend.py` | **NEW** | Abstract `RenderBackend`, `RenderMaterial`, `RenderCamera`, `RenderSettings` |
|
||||
| `src/fluency/rendering/mitsuba_backend.py` | **NEW** | `MitsubaBackend(RenderBackend)` implementation |
|
||||
| `src/fluency/rendering/occ_to_mesh.py` | **NEW** | OCC `TopoDS_Shape` → STL/OBJ tessellation |
|
||||
| `src/fluency/rendering/material_presets.py` | **NEW** | Preset library: Steel, Aluminum, Brass, Chrome, Plastic, Rubber, Wood |
|
||||
| `src/fluency/ui/render_window.py` | **NEW** | `RenderWindow(QMainWindow)` — image preview, material/quality controls, render/export |
|
||||
| `src/fluency/ui/main_window.py` | MODIFY | Add "Render" button → get selected shapes → open `RenderWindow` |
|
||||
|
||||
---
|
||||
|
||||
## UI: RenderWindow
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ Render — [Part Name] [─][□][×] │
|
||||
├──────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ │ Rendered Image Preview │ │
|
||||
│ │ (QLabel with QPixmap) │ │
|
||||
│ │ │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Material: [Steel ▾] │
|
||||
│ Quality: [256 SPP ▾] │
|
||||
│ Resolution: [1920×1080 ▾] │
|
||||
│ │
|
||||
│ [▶ Render] [⏹ Cancel] [💾 Export PNG] │
|
||||
│ │
|
||||
│ ████████████████░░░░░░ 65% (23s left) │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
- **Preview**: progressive refinement (low SPP first, then ramp)
|
||||
- **Cancel**: kill Mitsuba render thread
|
||||
- **Export**: save to PNG/EXR
|
||||
|
||||
---
|
||||
|
||||
## Material Presets
|
||||
|
||||
| Preset | Color | Metallic | Roughness | BSDF |
|
||||
|--------|-------|----------|-----------|------|
|
||||
| Brushed Steel | (0.65, 0.67, 0.72) | 0.9 | 0.35 | roughconductor |
|
||||
| Polished Chrome | (0.8, 0.8, 0.8) | 1.0 | 0.05 | roughconductor |
|
||||
| Brushed Aluminum | (0.75, 0.75, 0.75) | 0.85 | 0.25 | roughconductor |
|
||||
| Copper | (0.95, 0.64, 0.54) | 0.95 | 0.15 | roughconductor |
|
||||
| Gold | (1.0, 0.76, 0.33) | 1.0 | 0.1 | roughconductor |
|
||||
| Blackened Steel | (0.15, 0.15, 0.17) | 0.8 | 0.4 | roughconductor |
|
||||
| Matte Plastic | (0.2, 0.5, 0.8) | 0.0 | 0.6 | plastic |
|
||||
| Glossy Plastic | (0.2, 0.5, 0.8) | 0.0 | 0.1 | plastic |
|
||||
| White Nylon | (0.85, 0.85, 0.83) | 0.0 | 0.45 | plastic |
|
||||
| Black ABS | (0.05, 0.05, 0.05) | 0.0 | 0.35 | plastic |
|
||||
| Red PA12 | (0.75, 0.08, 0.08) | 0.0 | 0.4 | plastic |
|
||||
| Rubber | (0.1, 0.1, 0.1) | 0.0 | 0.9 | diffuse |
|
||||
| Ceramic White | (0.92, 0.91, 0.88) | 0.0 | 0.15 | dielectric |
|
||||
| Glass | (0.95, 0.95, 0.95) | 0.0 | 0.0 | dielectric |
|
||||
| Wood | (0.6, 0.4, 0.2) | 0.0 | 0.7 | diffuse |
|
||||
|
||||
**Note:** Mitsuba pip installs don't include spectral metal data files (iron.spd, copper.spd, etc.), so metal presets use `material="none"` with `specular_reflectance` set to the metal color instead.
|
||||
|
||||
---
|
||||
|
||||
## Risks & Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|-----------|
|
||||
| Mitsuba not installed | Graceful error: "pip install mitsuba" shown in UI |
|
||||
| Slow CPU rendering | Default to low SPP (64) for preview; offer GPU variant if CUDA available |
|
||||
| Large meshes slow to tessellate | Progress indicator; optional mesh decimation |
|
||||
| Mitsuba STL/OCC compatibility | Test tessellation quality; tune `linear_deflection` |
|
||||
|
||||
---
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
- **Phase 1** (abstract backend + OCC→mesh + Mitsuba impl): ~4-6 hours
|
||||
- **Phase 2** (render window UI + material presets): ~3-4 hours
|
||||
- **Phase 3** (polish, export, swap test): ~2-3 hours
|
||||
- **Total**: ~9-13 hours
|
||||
@@ -1,916 +0,0 @@
|
||||
import math
|
||||
import re
|
||||
from copy import copy
|
||||
from typing import Optional
|
||||
|
||||
import numpy as np
|
||||
from PySide6.QtWidgets import QApplication, QWidget, QMessageBox, QInputDialog
|
||||
from PySide6.QtGui import QPainter, QPen, QColor, QTransform
|
||||
from PySide6.QtCore import Qt, QPoint, QPointF, Signal, QLine
|
||||
from python_solvespace import SolverSystem, ResultFlag
|
||||
|
||||
|
||||
class SketchWidget(QWidget):
|
||||
constrain_done = Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.line_draw_buffer = [None, None]
|
||||
self.drag_buffer = [None, None]
|
||||
self.main_buffer = [None, None]
|
||||
|
||||
self.hovered_point = None
|
||||
self.selected_line = None
|
||||
|
||||
self.snapping_range = 20 # Range in pixels for snapping
|
||||
self.zoom = 1
|
||||
|
||||
self.setMouseTracking(True)
|
||||
self.mouse_mode = False
|
||||
self.solv = SolverSystem()
|
||||
|
||||
self.sketch = None
|
||||
|
||||
def set_sketch(self, sketch) -> None:
|
||||
print(sketch)
|
||||
self.sketch = sketch
|
||||
self.create_workplane()
|
||||
|
||||
def get_sketch(self):
|
||||
return self.sketch
|
||||
|
||||
def reset_buffers(self):
|
||||
self.line_draw_buffer = [None, None]
|
||||
self.drag_buffer = [None, None]
|
||||
self.main_buffer = [None, None]
|
||||
|
||||
def set_points(self, points: list):
|
||||
self.points = points
|
||||
#self.update()
|
||||
|
||||
def create_workplane(self):
|
||||
self.sketch.working_plane = self.solv.create_2d_base()
|
||||
|
||||
def create_workplane_projected(self):
|
||||
self.sketch.working_plane = self.solv.create_2d_base()
|
||||
|
||||
def convert_proj_points(self):
|
||||
out_points = []
|
||||
for point in self.sketch.proj_points:
|
||||
x, y = point
|
||||
coord = QPoint(x, y)
|
||||
out_points.append(coord)
|
||||
|
||||
self.sketch.proj_points = out_points
|
||||
|
||||
def convert_proj_lines(self):
|
||||
out_lines = []
|
||||
for line in self.sketch.proj_lines:
|
||||
start = QPoint(line[0][0], line[0][1])
|
||||
end = QPoint(line[1][0], line[1][1])
|
||||
coord = QLine(start, end)
|
||||
out_lines.append(coord)
|
||||
self.sketch.proj_lines = out_lines
|
||||
|
||||
def find_duplicate_points_2d(self, edges):
|
||||
points = []
|
||||
seen = set()
|
||||
duplicates = []
|
||||
|
||||
for edge in edges:
|
||||
for point in edge:
|
||||
# Extract only x and y coordinates
|
||||
point_2d = (point[0], point[1])
|
||||
if point_2d in seen:
|
||||
if point_2d not in duplicates:
|
||||
duplicates.append(point_2d)
|
||||
else:
|
||||
seen.add(point_2d)
|
||||
points.append(point_2d)
|
||||
|
||||
return duplicates
|
||||
|
||||
def normal_to_quaternion(self, normal):
|
||||
normal = np.array(normal)
|
||||
#normal = normal / np.linalg.norm(normal)
|
||||
|
||||
axis = np.cross([0, 0, 1], normal)
|
||||
if np.allclose(axis, 0):
|
||||
axis = np.array([1, 0, 0])
|
||||
else:
|
||||
axis = axis / np.linalg.norm(axis) # Normalize the axis
|
||||
|
||||
angle = np.arccos(np.dot([0, 0, 1], normal))
|
||||
|
||||
qw = np.cos(angle / 2)
|
||||
sin_half_angle = np.sin(angle / 2)
|
||||
qx, qy, qz = axis * sin_half_angle # This will now work correctly
|
||||
|
||||
return qw, qx, qy, qz
|
||||
|
||||
def create_workplane_space(self, points, normal):
|
||||
print("edges", points)
|
||||
origin = self.find_duplicate_points_2d(points)
|
||||
print(origin)
|
||||
x, y = origin[0]
|
||||
origin = QPoint(x, y)
|
||||
|
||||
origin_handle = self.get_handle_from_ui_point(origin)
|
||||
qw, qx, qy, qz = self.normal_to_quaternion(normal)
|
||||
|
||||
slv_normal = self.solv.add_normal_3d(qw, qx, qy, qz)
|
||||
self.sketch.working_plane = self.solv.add_work_plane(origin_handle, slv_normal)
|
||||
print(self.sketch.working_plane)
|
||||
|
||||
def get_handle_nr(self, input_str: str) -> int:
|
||||
# Define the regex pattern to extract the handle number
|
||||
pattern = r"handle=(\d+)"
|
||||
|
||||
# Use re.search to find the handle number in the string
|
||||
match = re.search(pattern, input_str)
|
||||
|
||||
if match:
|
||||
handle_number = int(match.group(1))
|
||||
print(f"Handle number: {handle_number}")
|
||||
return int(handle_number)
|
||||
|
||||
else:
|
||||
print("Handle number not found.")
|
||||
return 0
|
||||
|
||||
def get_keys(self, d: dict, target: QPoint) -> list:
|
||||
result = []
|
||||
path = []
|
||||
print(d)
|
||||
print(target)
|
||||
for k, v in d.items():
|
||||
path.append(k)
|
||||
if isinstance(v, dict):
|
||||
self.get_keys(v, target)
|
||||
if v == target:
|
||||
result.append(copy(path))
|
||||
path.pop()
|
||||
|
||||
return result
|
||||
|
||||
def get_handle_from_ui_point(self, ui_point: QPoint):
|
||||
"""Input QPoint and you shall reveive a slvs entity handle!"""
|
||||
for point in self.sketch.slv_points:
|
||||
if ui_point == point['ui_point']:
|
||||
slv_handle = point['solv_handle']
|
||||
|
||||
return slv_handle
|
||||
|
||||
def get_line_handle_from_ui_point(self, ui_point: QPoint):
|
||||
"""Input Qpoint that is on a line and you shall receive the handle of the line!"""
|
||||
for target_line_con in self.sketch.slv_lines:
|
||||
if self.is_point_on_line(ui_point, target_line_con['ui_points'][0], target_line_con['ui_points'][1]):
|
||||
slv_handle = target_line_con['solv_handle']
|
||||
|
||||
return slv_handle
|
||||
|
||||
def get_point_line_handles_from_ui_point(self, ui_point: QPoint) -> tuple:
|
||||
"""Input Qpoint that is on a line and you shall receive the handles of the points of the line!"""
|
||||
for target_line_con in self.sketch.slv_lines:
|
||||
if self.is_point_on_line(ui_point, target_line_con['ui_points'][0], target_line_con['ui_points'][1]):
|
||||
lines_to_cons = target_line_con['solv_entity_points']
|
||||
|
||||
return lines_to_cons
|
||||
|
||||
def distance(self, p1, p2):
|
||||
return math.sqrt((p1.x() - p2.x())**2 + (p1.y() - p2.y())**2)
|
||||
|
||||
def calculate_midpoint(self, point1, point2):
|
||||
mx = (point1.x() + point2.x()) // 2
|
||||
my = (point1.y() + point2.y()) // 2
|
||||
return QPoint(mx, my)
|
||||
|
||||
def is_point_on_line(self, p, p1, p2, tolerance=5):
|
||||
# Calculate the lengths of the sides of the triangle
|
||||
a = self.distance(p, p1)
|
||||
b = self.distance(p, p2)
|
||||
c = self.distance(p1, p2)
|
||||
|
||||
# Calculate the semi-perimeter
|
||||
s = (a + b + c) / 2
|
||||
|
||||
# Calculate the area using Heron's formula
|
||||
area = math.sqrt(s * (s - a) * (s - b) * (s - c))
|
||||
|
||||
# Calculate the height (perpendicular distance from the point to the line)
|
||||
if c > 0:
|
||||
height = (2 * area) / c
|
||||
# Check if the height is within the tolerance distance to the line
|
||||
if height > tolerance:
|
||||
return False
|
||||
|
||||
# Check if the projection of the point onto the line is within the line segment
|
||||
dot_product = ((p.x() - p1.x()) * (p2.x() - p1.x()) + (p.y() - p1.y()) * (p2.y() - p1.y())) / (c ** 2)
|
||||
|
||||
return 0 <= dot_product <= 1
|
||||
else:
|
||||
return None
|
||||
|
||||
def viewport_to_local_coord(self, qt_pos : QPoint) -> QPoint:
|
||||
return QPoint(self.to_quadrant_coords(qt_pos))
|
||||
|
||||
def check_all_points(self,) -> list:
|
||||
old_points_ui = []
|
||||
new_points_ui = []
|
||||
|
||||
for old_point_ui in self.sketch.slv_points:
|
||||
old_points_ui.append(old_point_ui['ui_point'])
|
||||
|
||||
for i in range(self.solv.entity_len()):
|
||||
# Iterate though full length because mixed list from SS
|
||||
entity = self.solv.entity(i)
|
||||
if entity.is_point_2d() and self.solv.params(entity.params):
|
||||
x_tbu, y_tbu = self.solv.params(entity.params)
|
||||
point_solved = QPoint(x_tbu, y_tbu)
|
||||
new_points_ui.append(point_solved)
|
||||
|
||||
# Now we have old_points_ui and new_points_ui, let's compare them
|
||||
differences = []
|
||||
|
||||
if len(old_points_ui) != len(new_points_ui):
|
||||
print(f"Length mismatch {len(old_points_ui)} - {len(new_points_ui)}")
|
||||
|
||||
for index, (old_point, new_point) in enumerate(zip(old_points_ui, new_points_ui)):
|
||||
if old_point != new_point:
|
||||
differences.append((index, old_point, new_point))
|
||||
|
||||
return differences
|
||||
|
||||
def update_ui_points(self, point_list: list):
|
||||
# Print initial state of slv_points_main
|
||||
# print("Initial slv_points_main:", self.slv_points_main)
|
||||
print("Change list:", point_list)
|
||||
|
||||
if len(point_list) > 0:
|
||||
for tbu_points_idx in point_list:
|
||||
# Each tbu_points_idx is a tuple: (index, old_point, new_point)
|
||||
index, old_point, new_point = tbu_points_idx
|
||||
|
||||
# Update the point in slv_points_main
|
||||
self.sketch.slv_points[index]['ui_point'] = new_point
|
||||
# Print updated state
|
||||
# print("Updated slv_points_main:", self.slv_points_main)
|
||||
|
||||
def check_all_lines_and_update(self,changed_points: list):
|
||||
for tbu_points_idx in changed_points:
|
||||
index, old_point, new_point = tbu_points_idx
|
||||
for line_needs_update in self.sketch.slv_lines:
|
||||
if old_point == line_needs_update['ui_points'][0]:
|
||||
line_needs_update['ui_points'][0] = new_point
|
||||
elif old_point == line_needs_update['ui_points'][1]:
|
||||
line_needs_update['ui_points'][1] = new_point
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
local_event_pos = self.viewport_to_local_coord(event.pos())
|
||||
|
||||
if event.button() == Qt.LeftButton and not self.mouse_mode:
|
||||
self.drag_buffer[1] = local_event_pos
|
||||
|
||||
print("Le main buffer", self.drag_buffer)
|
||||
|
||||
if len(self.main_buffer) == 2:
|
||||
entry = self.drag_buffer[0]
|
||||
new_params = self.drag_buffer[1].x(), self.drag_buffer[1].y()
|
||||
self.solv.set_params(entry.params, new_params)
|
||||
|
||||
self.solv.solve()
|
||||
|
||||
points_need_update = self.check_all_points()
|
||||
self.update_ui_points(points_need_update)
|
||||
self.check_all_lines_and_update(points_need_update)
|
||||
|
||||
self.update()
|
||||
self.drag_buffer = [None, None]
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
local_event_pos = self.viewport_to_local_coord(event.pos())
|
||||
|
||||
relation_point = {
|
||||
'handle_nr': None,
|
||||
'solv_handle': None,
|
||||
'ui_point': None,
|
||||
'part_of_entity': None
|
||||
}
|
||||
|
||||
relation_line = {
|
||||
'handle_nr': None,
|
||||
'solv_handle': None,
|
||||
'solv_entity_points': None,
|
||||
'ui_points': None
|
||||
}
|
||||
|
||||
if event.button() == Qt.LeftButton and not self.mouse_mode:
|
||||
self.drag_buffer[0] = self.get_handle_from_ui_point(self.hovered_point)
|
||||
|
||||
if event.button() == Qt.RightButton and self.mouse_mode:
|
||||
self.reset_buffers()
|
||||
|
||||
if event.button() == Qt.LeftButton and self.mouse_mode == "line":
|
||||
if self.hovered_point:
|
||||
clicked_pos = self.hovered_point
|
||||
else:
|
||||
clicked_pos = local_event_pos
|
||||
|
||||
if not self.line_draw_buffer[0]:
|
||||
self.line_draw_buffer[0] = clicked_pos
|
||||
u = clicked_pos.x()
|
||||
v = clicked_pos.y()
|
||||
|
||||
point = self.solv.add_point_2d(u, v, self.sketch.working_plane)
|
||||
|
||||
relation_point = {} # Reinitialize the dictionary
|
||||
handle_nr = self.get_handle_nr(str(point))
|
||||
relation_point['handle_nr'] = handle_nr
|
||||
relation_point['solv_handle'] = point
|
||||
relation_point['ui_point'] = clicked_pos
|
||||
|
||||
self.sketch.slv_points.append(relation_point)
|
||||
|
||||
print("points", self.sketch.slv_points)
|
||||
print("lines", self.sketch.slv_lines)
|
||||
|
||||
elif self.line_draw_buffer[0]:
|
||||
self.line_draw_buffer[1] = clicked_pos
|
||||
u = clicked_pos.x()
|
||||
v = clicked_pos.y()
|
||||
|
||||
point2 = self.solv.add_point_2d(u, v, self.sketch.working_plane)
|
||||
|
||||
relation_point = {} # Reinitialize the dictionary
|
||||
handle_nr = self.get_handle_nr(str(point2))
|
||||
relation_point['handle_nr'] = handle_nr
|
||||
relation_point['solv_handle'] = point2
|
||||
relation_point['ui_point'] = clicked_pos
|
||||
|
||||
self.sketch.slv_points.append(relation_point)
|
||||
|
||||
print("points", self.sketch.slv_points)
|
||||
print("lines", self.sketch.slv_lines)
|
||||
|
||||
print("Buffer state", self.line_draw_buffer)
|
||||
|
||||
if self.line_draw_buffer[0] and self.line_draw_buffer[1]:
|
||||
|
||||
point_slv1 = self.get_handle_from_ui_point(self.line_draw_buffer[0])
|
||||
point_slv2 = self.get_handle_from_ui_point(self.line_draw_buffer[1])
|
||||
print(point_slv1)
|
||||
print(point_slv2)
|
||||
|
||||
line = self.solv.add_line_2d(point_slv1, point_slv2, self.sketch.working_plane)
|
||||
|
||||
relation_line = {} # Reinitialize the dictionary
|
||||
handle_nr_line = self.get_handle_nr(str(line))
|
||||
relation_line['handle_nr'] = handle_nr_line
|
||||
relation_line['solv_handle'] = line
|
||||
relation_line['solv_entity_points'] = (point_slv1, point_slv2)
|
||||
relation_line['ui_points'] = [self.line_draw_buffer[0], self.line_draw_buffer[1]]
|
||||
|
||||
# Track relationship of point in line
|
||||
relation_point['part_of_entity'] = handle_nr_line
|
||||
|
||||
self.sketch.slv_lines.append(relation_line)
|
||||
|
||||
# Reset the buffer for the next line segment
|
||||
self.line_draw_buffer[0] = self.line_draw_buffer[1]
|
||||
self.line_draw_buffer[1] = None
|
||||
|
||||
# Track Relationship
|
||||
# Points
|
||||
|
||||
if event.button() == Qt.LeftButton and self.mouse_mode == "pt_pt":
|
||||
if self.hovered_point and not self.main_buffer[0]:
|
||||
self.main_buffer[0] = self.get_handle_from_ui_point(self.hovered_point)
|
||||
|
||||
elif self.main_buffer[0]:
|
||||
self.main_buffer[1] = self.get_handle_from_ui_point(self.hovered_point)
|
||||
|
||||
if self.main_buffer[0] and self.main_buffer[1]:
|
||||
print("buf", self.main_buffer)
|
||||
|
||||
self.solv.coincident(self.main_buffer[0], self.main_buffer[1], self.sketch.working_plane)
|
||||
|
||||
if self.solv.solve() == ResultFlag.OKAY:
|
||||
print("Fuck yeah")
|
||||
|
||||
elif self.solv.solve() == ResultFlag.DIDNT_CONVERGE:
|
||||
print("Solve_failed - Converge")
|
||||
|
||||
elif self.solv.solve() == ResultFlag.TOO_MANY_UNKNOWNS:
|
||||
print("Solve_failed - Unknowns")
|
||||
|
||||
elif self.solv.solve() == ResultFlag.INCONSISTENT:
|
||||
print("Solve_failed - Incons")
|
||||
self.constrain_done.emit()
|
||||
self.main_buffer = [None, None]
|
||||
|
||||
if event.button() == Qt.LeftButton and self.mouse_mode == "pt_line":
|
||||
print("ptline")
|
||||
line_selected = None
|
||||
|
||||
if self.hovered_point and not self.main_buffer[1]:
|
||||
self.main_buffer[0] = self.get_handle_from_ui_point(self.hovered_point)
|
||||
|
||||
elif self.main_buffer[0]:
|
||||
self.main_buffer[1] = self.get_line_handle_from_ui_point(local_event_pos)
|
||||
|
||||
# Contrain point to line
|
||||
if self.main_buffer[1]:
|
||||
self.solv.coincident(self.main_buffer[0], self.main_buffer[1], self.sketch.working_plane)
|
||||
|
||||
if self.solv.solve() == ResultFlag.OKAY:
|
||||
print("Fuck yeah")
|
||||
self.constrain_done.emit()
|
||||
|
||||
elif self.solv.solve() == ResultFlag.DIDNT_CONVERGE:
|
||||
print("Solve_failed - Converge")
|
||||
|
||||
elif self.solv.solve() == ResultFlag.TOO_MANY_UNKNOWNS:
|
||||
print("Solve_failed - Unknowns")
|
||||
|
||||
elif self.solv.solve() == ResultFlag.INCONSISTENT:
|
||||
print("Solve_failed - Incons")
|
||||
|
||||
self.constrain_done.emit()
|
||||
# Clear saved_points after solve attempt
|
||||
self.main_buffer = [None, None]
|
||||
|
||||
if event.button() == Qt.LeftButton and self.mouse_mode == "pb_con_mid":
|
||||
print("ptline")
|
||||
line_selected = None
|
||||
|
||||
if self.hovered_point and not self.main_buffer[1]:
|
||||
self.main_buffer[0] = self.get_handle_from_ui_point(self.hovered_point)
|
||||
|
||||
elif self.main_buffer[0]:
|
||||
self.main_buffer[1] = self.get_line_handle_from_ui_point(local_event_pos)
|
||||
|
||||
# Contrain point to line
|
||||
if self.main_buffer[1]:
|
||||
self.solv.midpoint(self.main_buffer[0], self.main_buffer[1], self.sketch.working_plane)
|
||||
|
||||
if self.solv.solve() == ResultFlag.OKAY:
|
||||
print("Fuck yeah")
|
||||
|
||||
elif self.solv.solve() == ResultFlag.DIDNT_CONVERGE:
|
||||
print("Solve_failed - Converge")
|
||||
|
||||
elif self.solv.solve() == ResultFlag.TOO_MANY_UNKNOWNS:
|
||||
print("Solve_failed - Unknowns")
|
||||
|
||||
elif self.solv.solve() == ResultFlag.INCONSISTENT:
|
||||
print("Solve_failed - Incons")
|
||||
self.constrain_done.emit()
|
||||
|
||||
self.main_buffer = [None, None]
|
||||
|
||||
if event.button() == Qt.LeftButton and self.mouse_mode == "horiz":
|
||||
|
||||
line_selected = self.get_line_handle_from_ui_point(local_event_pos)
|
||||
|
||||
if line_selected:
|
||||
self.solv.horizontal(line_selected, self.sketch.working_plane)
|
||||
|
||||
if self.solv.solve() == ResultFlag.OKAY:
|
||||
print("Fuck yeah")
|
||||
|
||||
elif self.solv.solve() == ResultFlag.DIDNT_CONVERGE:
|
||||
print("Solve_failed - Converge")
|
||||
|
||||
elif self.solv.solve() == ResultFlag.TOO_MANY_UNKNOWNS:
|
||||
print("Solve_failed - Unknowns")
|
||||
|
||||
elif self.solv.solve() == ResultFlag.INCONSISTENT:
|
||||
print("Solve_failed - Incons")
|
||||
|
||||
if event.button() == Qt.LeftButton and self.mouse_mode == "vert":
|
||||
line_selected = self.get_line_handle_from_ui_point(local_event_pos)
|
||||
|
||||
if line_selected:
|
||||
self.solv.vertical(line_selected, self.sketch.working_plane)
|
||||
|
||||
if self.solv.solve() == ResultFlag.OKAY:
|
||||
print("Fuck yeah")
|
||||
|
||||
elif self.solv.solve() == ResultFlag.DIDNT_CONVERGE:
|
||||
print("Solve_failed - Converge")
|
||||
|
||||
elif self.solv.solve() == ResultFlag.TOO_MANY_UNKNOWNS:
|
||||
print("Solve_failed - Unknowns")
|
||||
|
||||
elif self.solv.solve() == ResultFlag.INCONSISTENT:
|
||||
print("Solve_failed - Incons")
|
||||
|
||||
if event.button() == Qt.LeftButton and self.mouse_mode == "distance":
|
||||
# Depending on selected elemnts either point line or line distance
|
||||
#print("distance")
|
||||
e1 = None
|
||||
e2 = None
|
||||
|
||||
if self.hovered_point:
|
||||
print("buf point")
|
||||
# Get the point as UI point as buffer
|
||||
self.main_buffer[0] = self.hovered_point
|
||||
|
||||
elif self.selected_line:
|
||||
# Get the point as UI point as buffer
|
||||
self.main_buffer[1] = local_event_pos
|
||||
|
||||
if self.main_buffer[0] and self.main_buffer[1]:
|
||||
# Define point line combination
|
||||
e1 = self.get_handle_from_ui_point(self.main_buffer[0])
|
||||
e2 = self.get_line_handle_from_ui_point(self.main_buffer[1])
|
||||
|
||||
elif not self.main_buffer[0]:
|
||||
# Define only line selection
|
||||
e1, e2 = self.get_point_line_handles_from_ui_point(local_event_pos)
|
||||
|
||||
if e1 and e2:
|
||||
# Ask fo the dimension and solve if both elements are present
|
||||
length, ok = QInputDialog.getDouble(self, 'Distance', 'Enter a mm value:', value=100, decimals=2)
|
||||
self.solv.distance(e1, e2, length, self.sketch.working_plane)
|
||||
|
||||
if self.solv.solve() == ResultFlag.OKAY:
|
||||
print("Fuck yeah")
|
||||
|
||||
elif self.solv.solve() == ResultFlag.DIDNT_CONVERGE:
|
||||
print("Solve_failed - Converge")
|
||||
|
||||
elif self.solv.solve() == ResultFlag.TOO_MANY_UNKNOWNS:
|
||||
print("Solve_failed - Unknowns")
|
||||
|
||||
elif self.solv.solve() == ResultFlag.INCONSISTENT:
|
||||
print("Solve_failed - Incons")
|
||||
|
||||
self.constrain_done.emit()
|
||||
self.main_buffer = [None, None]
|
||||
|
||||
# Update the main point list with the new elements and draw them
|
||||
points_need_update = self.check_all_points()
|
||||
self.update_ui_points(points_need_update)
|
||||
self.check_all_lines_and_update(points_need_update)
|
||||
|
||||
self.update()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
local_event_pos = self.viewport_to_local_coord(event.pos())
|
||||
|
||||
closest_point = None
|
||||
min_distance = float('inf')
|
||||
threshold = 10 # Distance threshold for highlighting
|
||||
|
||||
if self.sketch:
|
||||
|
||||
for point in self.sketch.slv_points:
|
||||
distance = (local_event_pos - point['ui_point']).manhattanLength()
|
||||
if distance < threshold and distance < min_distance:
|
||||
closest_point = point['ui_point']
|
||||
min_distance = distance
|
||||
|
||||
for point in self.sketch.proj_points:
|
||||
distance = (local_event_pos - point).manhattanLength()
|
||||
if distance < threshold and distance < min_distance:
|
||||
closest_point = point
|
||||
min_distance = distance
|
||||
|
||||
if closest_point != self.hovered_point:
|
||||
self.hovered_point = closest_point
|
||||
print(self.hovered_point)
|
||||
|
||||
for dic in self.sketch.slv_lines:
|
||||
p1 = dic['ui_points'][0]
|
||||
p2 = dic['ui_points'][1]
|
||||
|
||||
if self.is_point_on_line(local_event_pos, p1, p2):
|
||||
self.selected_line = p1, p2
|
||||
break
|
||||
else:
|
||||
self.selected_line = None
|
||||
|
||||
self.update()
|
||||
|
||||
def mouseDoubleClickEvent(self, event):
|
||||
pass
|
||||
|
||||
def drawBackgroundGrid(self, painter):
|
||||
"""Draw a background grid."""
|
||||
grid_spacing = 50
|
||||
pen = QPen(QColor(200, 200, 200), 1, Qt.SolidLine)
|
||||
painter.setPen(pen)
|
||||
|
||||
# Draw vertical grid lines
|
||||
for x in range(-self.width() // 2, self.width() // 2, grid_spacing):
|
||||
painter.drawLine(x, -self.height() // 2, x, self.height() // 2)
|
||||
|
||||
# Draw horizontal grid lines
|
||||
for y in range(-self.height() // 2, self.height() // 2, grid_spacing):
|
||||
painter.drawLine(-self.width() // 2, y, self.width() // 2, y)
|
||||
|
||||
def drawAxes(self, painter):
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
# Set up pen for dashed lines
|
||||
pen = QPen(Qt.gray, 1, Qt.DashLine)
|
||||
painter.setPen(pen)
|
||||
|
||||
middle_x = self.width() // 2
|
||||
middle_y = self.height() // 2
|
||||
|
||||
# Draw X axis as dashed line
|
||||
painter.drawLine(0, middle_y, self.width(), middle_y)
|
||||
|
||||
# Draw Y axis as dashed line
|
||||
painter.drawLine(middle_x, 0, middle_x, self.height())
|
||||
|
||||
# Draw tick marks
|
||||
tick_length = int(10 * self.zoom)
|
||||
tick_spacing = int(50 * self.zoom)
|
||||
|
||||
pen = QPen(Qt.gray, 1, Qt.SolidLine)
|
||||
painter.setPen(pen)
|
||||
|
||||
# Draw tick marks on the X axis to the right and left from the middle point
|
||||
for x in range(0, self.width() // 2, tick_spacing):
|
||||
painter.drawLine(middle_x + x, middle_y - tick_length // 2, middle_x + x, middle_y + tick_length // 2)
|
||||
painter.drawLine(middle_x - x, middle_y - tick_length // 2, middle_x - x, middle_y + tick_length // 2)
|
||||
|
||||
# Draw tick marks on the Y axis upwards and downwards from the middle point
|
||||
for y in range(0, self.height() // 2, tick_spacing):
|
||||
painter.drawLine(middle_x - tick_length // 2, middle_y + y, middle_x + tick_length // 2, middle_y + y)
|
||||
painter.drawLine(middle_x - tick_length // 2, middle_y - y, middle_x + tick_length // 2, middle_y - y)
|
||||
|
||||
# Draw the origin point in red
|
||||
painter.setPen(QPen(Qt.red, 4))
|
||||
painter.drawPoint(middle_x, middle_y)
|
||||
|
||||
def draw_cross(self, painter, pos: QPoint, size=10):
|
||||
# Set up the pen
|
||||
pen = QPen(QColor('green')) # You can change the color as needed
|
||||
pen.setWidth(int(2 / self.zoom)) # Set the line widt)h
|
||||
painter.setPen(pen)
|
||||
x = pos.x()
|
||||
y = pos.y()
|
||||
|
||||
# Calculate the endpoints of the cross
|
||||
half_size = size // 2
|
||||
|
||||
# Draw the horizontal line
|
||||
painter.drawLine(x - half_size, y, x + half_size, y)
|
||||
|
||||
# Draw the vertical line
|
||||
painter.drawLine(x, y - half_size, x, y + half_size)
|
||||
|
||||
def to_quadrant_coords(self, point):
|
||||
"""Translate linear coordinates to quadrant coordinates."""
|
||||
center_x = self.width() // 2
|
||||
center_y = self.height() // 2
|
||||
quadrant_x = point.x() - center_x
|
||||
quadrant_y = center_y - point.y() # Note the change here
|
||||
return QPoint(quadrant_x, quadrant_y) / self.zoom
|
||||
|
||||
def from_quadrant_coords(self, point: QPoint):
|
||||
"""Translate quadrant coordinates to linear coordinates."""
|
||||
center_x = self.width() // 2
|
||||
center_y = self.height() // 2
|
||||
widget_x = center_x + point.x() * self.zoom
|
||||
widget_y = center_y - point.y() * self.zoom # Note the subtraction here
|
||||
|
||||
return QPoint(int(widget_x), int(widget_y))
|
||||
|
||||
def from_quadrant_coords_no_center(self, point):
|
||||
"""Invert Y Coordinate for mesh"""
|
||||
center_x = 0
|
||||
center_y = 0
|
||||
widget_x = point.x()
|
||||
widget_y = -point.y()
|
||||
return QPoint(int(widget_x), int(widget_y))
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
|
||||
self.drawAxes(painter)
|
||||
|
||||
# Create a QTransform object
|
||||
transform = QTransform()
|
||||
|
||||
# Translate the origin to the center of the widget
|
||||
center = QPointF(self.width() / 2, self.height() / 2)
|
||||
transform.translate(center.x(), center.y())
|
||||
|
||||
# Apply the zoom factor
|
||||
transform.scale(self.zoom, -self.zoom) # Negative y-scale to invert y-axis
|
||||
|
||||
# Set the transform to the painter
|
||||
painter.setTransform(transform)
|
||||
|
||||
pen = QPen(Qt.gray)
|
||||
pen.setWidthF(2 / self.zoom)
|
||||
painter.setPen(pen)
|
||||
|
||||
# Draw points
|
||||
if self.sketch:
|
||||
for point in self.sketch.slv_points:
|
||||
painter.drawEllipse(point['ui_point'], 3 / self.zoom, 3 / self.zoom)
|
||||
|
||||
for dic in self.sketch.slv_lines:
|
||||
p1 = dic['ui_points'][0]
|
||||
p2 = dic['ui_points'][1]
|
||||
painter.drawLine(p1, p2)
|
||||
|
||||
dis = self.distance(p1, p2)
|
||||
mid = self.calculate_midpoint(p1, p2)
|
||||
painter.drawText(mid, str(round(dis, 2)))
|
||||
|
||||
pen = QPen(Qt.green)
|
||||
pen.setWidthF(2 / self.zoom)
|
||||
painter.setPen(pen)
|
||||
|
||||
if self.solv.entity_len():
|
||||
for i in range(self.solv.entity_len()):
|
||||
entity = self.solv.entity(i)
|
||||
if entity.is_point_2d() and self.solv.params(entity.params):
|
||||
x, y = self.solv.params(entity.params)
|
||||
point = QPointF(x, y)
|
||||
painter.drawEllipse(point, 6 / self.zoom, 6 / self.zoom)
|
||||
|
||||
# Highlight point hovered
|
||||
if self.hovered_point:
|
||||
highlight_pen = QPen(QColor(255, 0, 0))
|
||||
highlight_pen.setWidthF(2 / self.zoom)
|
||||
painter.setPen(highlight_pen)
|
||||
painter.drawEllipse(self.hovered_point, 5 / self.zoom, 5 / self.zoom)
|
||||
|
||||
# Highlight line hovered
|
||||
if self.selected_line and not self.hovered_point:
|
||||
p1, p2 = self.selected_line
|
||||
painter.setPen(QPen(Qt.red, 2 / self.zoom))
|
||||
painter.drawLine(p1, p2)
|
||||
|
||||
for cross in self.sketch.proj_points:
|
||||
self.draw_cross(painter, cross, 10 / self.zoom)
|
||||
|
||||
for selected in self.sketch.proj_lines:
|
||||
pen = QPen(Qt.white, 1, Qt.DashLine)
|
||||
painter.setPen(pen)
|
||||
painter.drawLine(selected)
|
||||
|
||||
painter.end()
|
||||
|
||||
def wheelEvent(self, event):
|
||||
delta = event.angleDelta().y()
|
||||
self.zoom += (delta / 200) * 0.1
|
||||
self.update()
|
||||
|
||||
def aspect_ratio(self):
|
||||
return self.width() / self.height() * (1.0 / abs(self.zoom))
|
||||
|
||||
|
||||
class Point2D:
|
||||
"""Improved oop aaproach?"""
|
||||
def __init__(self):
|
||||
self.ui_point = None
|
||||
self.solve_handle_nr = None
|
||||
self.solve_handle = None
|
||||
self.part_of_entity = None
|
||||
|
||||
def to_quadrant_coords(self, point):
|
||||
"""Translate linear coordinates to quadrant coordinates."""
|
||||
center_x = self.width() // 2
|
||||
center_y = self.height() // 2
|
||||
quadrant_x = point.x() - center_x
|
||||
quadrant_y = center_y - point.y() # Note the change here
|
||||
|
||||
return QPoint(quadrant_x, quadrant_y) / self.zoom
|
||||
|
||||
def from_quadrant_coords(self, point: QPoint):
|
||||
"""Translate quadrant coordinates to linear coordinates."""
|
||||
center_x = self.width() // 2
|
||||
center_y = self.height() // 2
|
||||
widget_x = center_x + point.x() * self.zoom
|
||||
widget_y = center_y - point.y() * self.zoom # Note the subtraction here
|
||||
|
||||
return QPoint(int(widget_x), int(widget_y))
|
||||
|
||||
def from_quadrant_coords_no_center(self, point):
|
||||
"""Invert Y Coordinate for mesh"""
|
||||
center_x = 0
|
||||
center_y = 0
|
||||
widget_x = point.x()
|
||||
widget_y = -point.y()
|
||||
|
||||
return QPoint(int(widget_x), int(widget_y))
|
||||
|
||||
def get_handle_nr(self, input_str: str) -> int:
|
||||
# Define the regex pattern to extract the handle number
|
||||
pattern = r"handle=(\d+)"
|
||||
|
||||
# Use re.search to find the handle number in the string
|
||||
match = re.search(pattern, input_str)
|
||||
|
||||
if match:
|
||||
handle_number = int(match.group(1))
|
||||
print(f"Handle number: {handle_number}")
|
||||
return int(handle_number)
|
||||
|
||||
else:
|
||||
print("Handle number not found.")
|
||||
return 0
|
||||
|
||||
def get_keys(self, d: dict, target: QPoint) -> list:
|
||||
result = []
|
||||
path = []
|
||||
print(d)
|
||||
print(target)
|
||||
for k, v in d.items():
|
||||
path.append(k)
|
||||
if isinstance(v, dict):
|
||||
self.get_keys(v, target)
|
||||
if v == target:
|
||||
result.append(copy(path))
|
||||
path.pop()
|
||||
|
||||
return result
|
||||
|
||||
def get_handle_from_ui_point(self, ui_point: QPoint):
|
||||
"""Input QPoint and you shall reveive a slvs entity handle!"""
|
||||
for point in self.sketch.slv_points:
|
||||
if ui_point == point['ui_point']:
|
||||
slv_handle = point['solv_handle']
|
||||
|
||||
return slv_handle
|
||||
|
||||
def get_line_handle_from_ui_point(self, ui_point: QPoint):
|
||||
"""Input Qpoint that is on a line and you shall receive the handle of the line!"""
|
||||
for target_line_con in self.sketch.slv_lines:
|
||||
if self.is_point_on_line(ui_point, target_line_con['ui_points'][0], target_line_con['ui_points'][1]):
|
||||
slv_handle = target_line_con['solv_handle']
|
||||
|
||||
return slv_handle
|
||||
|
||||
def get_point_line_handles_from_ui_point(self, ui_point: QPoint) -> tuple:
|
||||
"""Input Qpoint that is on a line and you shall receive the handles of the points of the line!"""
|
||||
for target_line_con in self.sketch.slv_lines:
|
||||
if self.is_point_on_line(ui_point, target_line_con['ui_points'][0], target_line_con['ui_points'][1]):
|
||||
lines_to_cons = target_line_con['solv_entity_points']
|
||||
|
||||
return lines_to_cons
|
||||
|
||||
def distance(self, p1, p2):
|
||||
return math.sqrt((p1.x() - p2.x())**2 + (p1.y() - p2.y())**2)
|
||||
|
||||
def calculate_midpoint(self, point1, point2):
|
||||
mx = (point1.x() + point2.x()) // 2
|
||||
my = (point1.y() + point2.y()) // 2
|
||||
return QPoint(mx, my)
|
||||
|
||||
def is_point_on_line(self, p, p1, p2, tolerance=5):
|
||||
# Calculate the lengths of the sides of the triangle
|
||||
a = self.distance(p, p1)
|
||||
b = self.distance(p, p2)
|
||||
c = self.distance(p1, p2)
|
||||
|
||||
# Calculate the semi-perimeter
|
||||
s = (a + b + c) / 2
|
||||
|
||||
# Calculate the area using Heron's formula
|
||||
area = math.sqrt(s * (s - a) * (s - b) * (s - c))
|
||||
|
||||
# Calculate the height (perpendicular distance from the point to the line)
|
||||
if c > 0:
|
||||
height = (2 * area) / c
|
||||
# Check if the height is within the tolerance distance to the line
|
||||
if height > tolerance:
|
||||
return False
|
||||
|
||||
# Check if the projection of the point onto the line is within the line segment
|
||||
dot_product = ((p.x() - p1.x()) * (p2.x() - p1.x()) + (p.y() - p1.y()) * (p2.y() - p1.y())) / (c ** 2)
|
||||
|
||||
return 0 <= dot_product <= 1
|
||||
else:
|
||||
return None
|
||||
|
||||
def viewport_to_local_coord(self, qt_pos : QPoint) -> QPoint:
|
||||
return QPoint(self.to_quadrant_coords(qt_pos))
|
||||
|
||||
|
||||
class Line2D:
|
||||
pass
|
||||
|
||||
class Sketch2d(SolverSystem):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
window = SketchWidget()
|
||||
window.setWindowTitle("Snap Line Widget")
|
||||
window.resize(800, 600)
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,504 +0,0 @@
|
||||
import sys
|
||||
import numpy as np
|
||||
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
|
||||
from PySide6.QtOpenGLWidgets import QOpenGLWidget
|
||||
from PySide6.QtCore import Qt, QPoint
|
||||
from OpenGL.GL import *
|
||||
from OpenGL.GLU import *
|
||||
|
||||
##testing
|
||||
|
||||
def create_cube(scale=1):
|
||||
vertices = np.array([
|
||||
[0, 0, 0],
|
||||
[2, 0, 0],
|
||||
[2, 2, 0],
|
||||
[0, 2, 0],
|
||||
[0, 0, 2],
|
||||
[2, 0, 2],
|
||||
[2, 2, 2],
|
||||
[0, 2, 2]
|
||||
]) * scale
|
||||
|
||||
faces = np.array([
|
||||
[0, 1, 2],
|
||||
[2, 3, 0],
|
||||
[4, 5, 6],
|
||||
[6, 7, 4],
|
||||
[0, 1, 5],
|
||||
[5, 4, 0],
|
||||
[2, 3, 7],
|
||||
[7, 6, 2],
|
||||
[0, 3, 7],
|
||||
[7, 4, 0],
|
||||
[1, 2, 6],
|
||||
[6, 5, 1]
|
||||
])
|
||||
|
||||
return vertices, faces
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("OpenGL Cube Viewer")
|
||||
self.setGeometry(100, 100, 800, 600)
|
||||
|
||||
self.opengl_widget = OpenGLWidget()
|
||||
|
||||
central_widget = QWidget()
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self.opengl_widget)
|
||||
central_widget.setLayout(layout)
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
# Load cube data
|
||||
vertices, faces = create_cube()
|
||||
self.opengl_widget.load_interactor_mesh((vertices, faces))
|
||||
|
||||
|
||||
class OpenGLWidget(QOpenGLWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.vertices = None
|
||||
self.faces = None
|
||||
self.selected_face = -1
|
||||
self.scale_factor = 1
|
||||
self.mesh_loaded = None
|
||||
self.interactor_loaded = None
|
||||
self.centroid = None
|
||||
self.stl_file = "out.stl" # Replace with your STL file path
|
||||
self.lastPos = QPoint()
|
||||
self.startPos = None
|
||||
self.endPos = None
|
||||
self.xRot = 180
|
||||
self.yRot = 0
|
||||
self.zoom = -2
|
||||
self.sketch = []
|
||||
self.gl_width = self.width()
|
||||
self.gl_height = self.height()
|
||||
|
||||
def map_value_to_range(self, value, value_min=0, value_max=1920, range_min=-1, range_max=1):
|
||||
value = max(value_min, min(value_max, value))
|
||||
mapped_value = ((value - value_min) / (value_max - value_min)) * (range_max - range_min) + range_min
|
||||
|
||||
return mapped_value
|
||||
|
||||
def load_stl(self, filename: str) -> object:
|
||||
try:
|
||||
stl_mesh = mesh.Mesh.from_file(filename)
|
||||
|
||||
# Extract vertices
|
||||
vertices = np.concatenate([stl_mesh.v0, stl_mesh.v1, stl_mesh.v2])
|
||||
|
||||
# Calculate bounding box
|
||||
min_x, min_y, min_z = vertices.min(axis=0)
|
||||
max_x, max_y, max_z = vertices.max(axis=0)
|
||||
|
||||
# Calculate centroid
|
||||
centroid_x = (min_x + max_x) / 2.0
|
||||
centroid_y = (min_y + max_y) / 2.0
|
||||
centroid_z = (min_z + max_z) / 2.0
|
||||
|
||||
self.mesh_loaded = stl_mesh.vectors
|
||||
self.centroid = (centroid_x, centroid_y, centroid_z)
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"Error: File {filename} not found.")
|
||||
except Exception as e:
|
||||
print(f"Error loading {filename}: {e}")
|
||||
|
||||
return None, (0, 0, 0)
|
||||
|
||||
def load_interactor_mesh(self, simp_mesh):
|
||||
self.interactor_loaded = simp_mesh
|
||||
# Calculate centroid based on the average position of vertices
|
||||
centroid = np.mean(simp_mesh[0], axis=0)
|
||||
|
||||
self.centroid = tuple(centroid)
|
||||
print(f"Centroid: {self.centroid}")
|
||||
|
||||
self.update()
|
||||
|
||||
def load_mesh_direct(self, mesh):
|
||||
try:
|
||||
stl_mesh = mesh
|
||||
|
||||
# Extract vertices
|
||||
vertices = np.array(stl_mesh)
|
||||
|
||||
# Calculate centroid based on the average position of vertices
|
||||
centroid = np.mean(vertices, axis=0)
|
||||
|
||||
self.mesh_loaded = vertices
|
||||
self.centroid = tuple(centroid)
|
||||
print(f"Centroid: {self.centroid}")
|
||||
self.update()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def clear_mesh(self):
|
||||
self.mesh_loaded = None
|
||||
|
||||
def initializeGL(self):
|
||||
glClearColor(0, 0, 0, 1)
|
||||
glEnable(GL_DEPTH_TEST)
|
||||
|
||||
def resizeGL(self, width, height):
|
||||
glViewport(0, 0, width, height)
|
||||
glMatrixMode(GL_PROJECTION)
|
||||
glLoadIdentity()
|
||||
|
||||
aspect = width / float(height)
|
||||
|
||||
self.gl_width = self.width()
|
||||
self.gl_height = self.height()
|
||||
|
||||
gluPerspective(45.0, aspect, 0.01, 1000.0)
|
||||
glMatrixMode(GL_MODELVIEW)
|
||||
|
||||
def unproject(self, x, y, z, modelview, projection, viewport):
|
||||
mvp = np.dot(projection, modelview)
|
||||
mvp_inv = np.linalg.inv(mvp)
|
||||
|
||||
ndc = np.array([(x - viewport[0]) / viewport[2] * 2 - 1,
|
||||
(y - viewport[1]) / viewport[3] * 2 - 1,
|
||||
2 * z - 1,
|
||||
1])
|
||||
|
||||
world = np.dot(mvp_inv, ndc)
|
||||
print("world undproj", world)
|
||||
return world[:3] / world[3]
|
||||
|
||||
def draw_ray(self, ray_start, ray_end):
|
||||
glColor3f(1.0, 0.0, 0.0) # Set the color of the ray (red)
|
||||
glBegin(GL_LINES)
|
||||
glVertex3f(*ray_start)
|
||||
glVertex3f(*ray_end)
|
||||
glEnd()
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.buttons() & Qt.RightButton:
|
||||
self.select_face(event)
|
||||
|
||||
def select_face(self, event):
|
||||
x = event.position().x()
|
||||
y = event.position().y()
|
||||
|
||||
modelview = glGetDoublev(GL_MODELVIEW_MATRIX)
|
||||
projection = glGetDoublev(GL_PROJECTION_MATRIX)
|
||||
viewport = glGetIntegerv(GL_VIEWPORT)
|
||||
|
||||
# Unproject near and far points in world space
|
||||
ray_start = gluUnProject(x, y, 0.0, modelview, projection, viewport)
|
||||
ray_end = gluUnProject(x, y, 1.0, modelview, projection, viewport)
|
||||
|
||||
ray_start = np.array(ray_start)
|
||||
ray_end = np.array(ray_end)
|
||||
ray_direction = ray_end - ray_start
|
||||
ray_direction /= np.linalg.norm(ray_direction)
|
||||
|
||||
print(f"Ray start: {ray_start}")
|
||||
print(f"Ray end: {ray_end}")
|
||||
print(f"Ray direction: {ray_direction}")
|
||||
|
||||
self.selected_face = self.check_intersection(ray_start, ray_end)
|
||||
print(f"Selected face: {self.selected_face}")
|
||||
|
||||
self.update()
|
||||
|
||||
def ray_box_intersection(self, ray_origin, ray_direction, box_min, box_max):
|
||||
inv_direction = 1 / (ray_direction + 1e-7) # Add small value to avoid division by zero
|
||||
t1 = (box_min - ray_origin) * inv_direction
|
||||
t2 = (box_max - ray_origin) * inv_direction
|
||||
|
||||
t_min = np.max(np.minimum(t1, t2))
|
||||
t_max = np.min(np.maximum(t1, t2))
|
||||
|
||||
print(f"min: {t_min}, max: {t_max}" )
|
||||
|
||||
return t_max >= t_min and t_max > 0
|
||||
|
||||
def check_intersection(self, ray_start, ray_end):
|
||||
# Get the current modelview matrix
|
||||
modelview = glGetDoublev(GL_MODELVIEW_MATRIX)
|
||||
|
||||
# Transform vertices to camera space
|
||||
vertices_cam = [np.dot(modelview, np.append(v, 1))[:3] for v in self.interactor_loaded[0]]
|
||||
|
||||
ray_direction = ray_end - ray_start
|
||||
ray_direction /= np.linalg.norm(ray_direction)
|
||||
|
||||
print(f"Checking intersection with {len(self.interactor_loaded[1])} faces")
|
||||
for face_idx, face in enumerate(self.interactor_loaded[1]):
|
||||
v0, v1, v2 = [vertices_cam[i] for i in face]
|
||||
intersection = self.moller_trumbore(ray_start, ray_direction, v0, v1, v2)
|
||||
if intersection is not None:
|
||||
print(f"Intersection found with face {face_idx}")
|
||||
return face_idx
|
||||
|
||||
print("No intersection found")
|
||||
return None
|
||||
|
||||
def moller_trumbore(self, ray_origin, ray_direction, v0, v1, v2):
|
||||
epsilon = 1e-6
|
||||
# Find vectors for two edges sharing v0
|
||||
edge1 = v1 - v0
|
||||
edge2 = v2 - v0
|
||||
pvec = np.cross(ray_direction, edge2)
|
||||
|
||||
det = np.dot(edge1, pvec)
|
||||
print(det)
|
||||
|
||||
"""if det < epsilon:
|
||||
return None"""
|
||||
|
||||
inv_det = 1.0 / det
|
||||
tvec = ray_origin - v0
|
||||
u = np.dot(tvec, pvec) * inv_det
|
||||
|
||||
print("u", u )
|
||||
|
||||
if u < 0.0 or u > 1.0:
|
||||
return None
|
||||
|
||||
qvec = np.cross(tvec, edge1)
|
||||
|
||||
# Calculate v parameter and test bounds
|
||||
v = np.dot(ray_direction, qvec) * inv_det
|
||||
print("v", v)
|
||||
|
||||
if v < 0.0 or u + v > 1.0:
|
||||
return None
|
||||
|
||||
# Calculate t, ray intersects triangle
|
||||
t = np.dot(edge2, qvec) * inv_det
|
||||
print("t",t)
|
||||
|
||||
if t > epsilon:
|
||||
return ray_origin + t * ray_direction
|
||||
|
||||
return None
|
||||
|
||||
def ray_triangle_intersection(self, ray_origin, ray_direction, v0, v1, v2):
|
||||
epsilon = 1e-5
|
||||
edge1 = v1 - v0
|
||||
edge2 = v2 - v0
|
||||
h = np.cross(ray_direction, edge2)
|
||||
a = np.dot(edge1, h)
|
||||
|
||||
print(f"Triangle vertices: {v0}, {v1}, {v2}")
|
||||
print(f"a: {a}")
|
||||
|
||||
if abs(a) < epsilon:
|
||||
print("Ray is parallel to the triangle")
|
||||
return None # Ray is parallel to the triangle
|
||||
|
||||
f = 1.0 / a
|
||||
s = ray_origin - v0
|
||||
u = f * np.dot(s, h)
|
||||
|
||||
print(f"u: {u}")
|
||||
|
||||
if u < 0.0 or u > 1.0:
|
||||
print("u is out of range")
|
||||
return None
|
||||
|
||||
q = np.cross(s, edge1)
|
||||
v = f * np.dot(ray_direction, q)
|
||||
|
||||
print(f"v: {v}")
|
||||
|
||||
if v < 0.0 or u + v > 1.0:
|
||||
print("v is out of range")
|
||||
return None
|
||||
|
||||
t = f * np.dot(edge2, q)
|
||||
|
||||
print(f"t: {t}")
|
||||
|
||||
if t > epsilon:
|
||||
intersection_point = ray_origin + t * ray_direction
|
||||
print(f"Intersection point: {intersection_point}")
|
||||
return intersection_point
|
||||
|
||||
print("t is too small")
|
||||
return None
|
||||
def paintGL(self):
|
||||
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
|
||||
glMatrixMode(GL_MODELVIEW)
|
||||
glLoadIdentity()
|
||||
|
||||
# Apply camera transformation
|
||||
glTranslatef(0, 0, self.zoom)
|
||||
glRotatef(self.xRot, 1.0, 0.0, 0.0)
|
||||
glRotatef(self.yRot, 0.0, 1.0, 0.0)
|
||||
|
||||
"""# Apply model transformation
|
||||
glTranslatef(self.tx, self.ty, self.tz)
|
||||
glScalef(self.scale, self.scale, self.scale)
|
||||
glRotatef(self.model_xRot, 1.0, 0.0, 0.0)
|
||||
glRotatef(self.model_yRot, 0.0, 1.0, 0.0)
|
||||
glRotatef(self.model_zRot, 0.0, 0.0, 1.0)"""
|
||||
|
||||
glColor3f(0.9, 0.8, 0.8)
|
||||
self.draw_area()
|
||||
|
||||
if self.mesh_loaded is not None:
|
||||
# Adjust the camera for the STL mesh
|
||||
if self.centroid:
|
||||
glPushMatrix() # Save current transformation matrix
|
||||
glScalef(self.scale_factor, self.scale_factor, self.scale_factor) # Apply scaling
|
||||
|
||||
cx, cy, cz = self.centroid
|
||||
gluLookAt(cx, cy, cz + 100, cx, cy, cz, 0, 1, 0)
|
||||
|
||||
self.draw_mesh_direct(self.mesh_loaded)
|
||||
glPopMatrix() # Restore transformation matrix
|
||||
|
||||
if self.interactor_loaded is not None:
|
||||
# Draw interactor mesh
|
||||
glPushMatrix() # Save current transformation matrix
|
||||
glScalef(self.scale_factor, self.scale_factor, self.scale_factor) # Apply scaling
|
||||
|
||||
self.draw_interactor(self.interactor_loaded)
|
||||
glPopMatrix() # Restore transformation matrix
|
||||
|
||||
if self.selected_face is not None:
|
||||
glColor3f(0.0, 1.0, 0.0) # Red color for selected face
|
||||
glBegin(GL_TRIANGLES)
|
||||
for vertex_idx in self.interactor_loaded[1][self.selected_face]:
|
||||
glVertex3fv(self.interactor_loaded[0][vertex_idx])
|
||||
glEnd()
|
||||
|
||||
# Flush the OpenGL pipeline and swap buffers
|
||||
|
||||
|
||||
if hasattr(self, 'ray_start') and hasattr(self, 'ray_end'):
|
||||
self.draw_ray(self.ray_start, self.ray_end)
|
||||
|
||||
glFlush()
|
||||
|
||||
def draw_stl(self, vertices):
|
||||
glEnable(GL_LIGHTING)
|
||||
glEnable(GL_LIGHT0)
|
||||
glEnable(GL_DEPTH_TEST)
|
||||
glEnable(GL_COLOR_MATERIAL)
|
||||
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
|
||||
|
||||
glLightfv(GL_LIGHT0, GL_POSITION, (0, 1, 1, 0))
|
||||
glLightfv(GL_LIGHT0, GL_DIFFUSE, (0.6, 0.6, 0.6, 1.0))
|
||||
|
||||
glBegin(GL_TRIANGLES)
|
||||
for triangle in vertices:
|
||||
for vertex in triangle:
|
||||
glVertex3fv(vertex)
|
||||
glEnd()
|
||||
self.update()
|
||||
|
||||
def draw_interactor(self, simp_mesh: tuple):
|
||||
vertices, faces = simp_mesh
|
||||
|
||||
glEnable(GL_LIGHTING)
|
||||
glEnable(GL_LIGHT0)
|
||||
glEnable(GL_DEPTH_TEST)
|
||||
glEnable(GL_COLOR_MATERIAL)
|
||||
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
|
||||
|
||||
glLightfv(GL_LIGHT0, GL_POSITION, (0, 0.6, 0.6, 0))
|
||||
glLightfv(GL_LIGHT0, GL_DIFFUSE, (0.4, 0.4, 0.4, 0.6))
|
||||
|
||||
# Draw the faces
|
||||
glDisable(GL_LIGHTING)
|
||||
glColor3f(0.2, 0.0, 0.0) # Set face color to red (or any color you prefer)
|
||||
|
||||
glBegin(GL_TRIANGLES)
|
||||
for face in faces:
|
||||
for vertex_index in face:
|
||||
glVertex3fv(vertices[vertex_index])
|
||||
glEnd()
|
||||
|
||||
# Draw the lines (edges of the triangles)
|
||||
glColor3f(0.0, 1.0, 0.0) # Set line color to green (or any color you prefer)
|
||||
|
||||
glBegin(GL_LINES)
|
||||
for face in faces:
|
||||
for i in range(len(face)):
|
||||
glVertex3fv(vertices[face[i]])
|
||||
glVertex3fv(vertices[face[(i + 1) % len(face)]])
|
||||
glEnd()
|
||||
|
||||
glEnable(GL_LIGHTING) # Re-enable lighting if further drawing requires it
|
||||
|
||||
def draw_mesh_direct(self, points):
|
||||
glEnable(GL_LIGHTING)
|
||||
glEnable(GL_LIGHT0)
|
||||
glEnable(GL_DEPTH_TEST)
|
||||
glEnable(GL_COLOR_MATERIAL)
|
||||
glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE)
|
||||
|
||||
glLightfv(GL_LIGHT0, GL_POSITION, (0, 0.6, 0.6, 0))
|
||||
glLightfv(GL_LIGHT0, GL_DIFFUSE, (0.4, 0.4, 0.4, 0.6))
|
||||
|
||||
glDisable(GL_LIGHTING)
|
||||
glBegin(GL_TRIANGLES)
|
||||
for vertex in points:
|
||||
glVertex3fv(vertex)
|
||||
glEnd()
|
||||
|
||||
# Draw the lines (edges of the triangles)
|
||||
#glDisable(GL_LIGHTING) # Disable lighting to avoid affecting the line color
|
||||
glColor3f(0.0, 0.0, 0.0) # Set line color to black (or any color you prefer)
|
||||
|
||||
glBegin(GL_LINES)
|
||||
for i in range(0, len(points), 3):
|
||||
glVertex3fv(points[i])
|
||||
glVertex3fv(points[i + 1])
|
||||
|
||||
glVertex3fv(points[i + 1])
|
||||
glVertex3fv(points[i + 2])
|
||||
|
||||
glVertex3fv(points[i + 2])
|
||||
glVertex3fv(points[i])
|
||||
glEnd()
|
||||
|
||||
glEnable(GL_LIGHTING) # Re-enable lighting if further drawing requires it
|
||||
|
||||
def draw_area(self):
|
||||
glColor3f(0.5, 0.5, 0.5) # Gray color
|
||||
|
||||
glBegin(GL_LINES)
|
||||
for x in range(0, self.width(), 1):
|
||||
x_ndc = self.map_value_to_range(x, 0, value_max=self.width(), range_min=-self.gl_width, range_max=self.gl_width)
|
||||
glVertex2f(x_ndc, -self.gl_height) # Start from y = -1
|
||||
glVertex2f(x_ndc, self.gl_height) # End at y = 1
|
||||
|
||||
for y in range(0, self.height(), 1):
|
||||
y_ndc = self.map_value_to_range(y, 0, value_max=self.height(), range_min=-self.gl_height, range_max=self.gl_height)
|
||||
glVertex2f(-self.gl_width, y_ndc) # Start from x = -1
|
||||
glVertex2f(self.gl_width, y_ndc) # End at x = 1
|
||||
glEnd()
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
dx = event.x() - self.lastPos.x()
|
||||
dy = event.y() - self.lastPos.y()
|
||||
|
||||
if event.buttons() & Qt.MouseButton.LeftButton :
|
||||
self.xRot += 0.5 * dy
|
||||
self.yRot += 0.5 * dx
|
||||
self.lastPos = event.pos()
|
||||
self.update()
|
||||
|
||||
def wheelEvent(self, event):
|
||||
delta = event.angleDelta().y()
|
||||
self.zoom += delta / 200
|
||||
self.update()
|
||||
|
||||
def aspect_ratio(self):
|
||||
return self.width() / self.height() * (1.0 / abs(self.zoom))
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
@@ -1,35 +0,0 @@
|
||||
from python_solvespace import SolverSystem, ResultFlag
|
||||
|
||||
def solve_constraint():
|
||||
solv = SolverSystem()
|
||||
wp = solv.create_2d_base() # Workplane (Entity)
|
||||
p0 = solv.add_point_2d(0, 0, wp) # Entity
|
||||
p1 = solv.add_point_2d(10, 10, wp) # Entity
|
||||
p2 = solv.add_point_2d(0, 10, wp) # Entity
|
||||
solv.dragged(p0, wp) # Make a constraint with the entity
|
||||
|
||||
line0 = solv.add_line_2d(p0, p1, wp) # Create entity with others
|
||||
line1 = solv.add_line_2d(p0, p2, wp)
|
||||
#solv.angle(line0, line1, 45, wp) # Constrain two entities
|
||||
solv.coincident(p0, p1, wp)
|
||||
solv.add_constraint(100006, wp, 0, p1,p2, line0, line1)
|
||||
|
||||
line1 = solv.entity(-1) # Entity handle can be re-generated and negatively indexed
|
||||
solv.
|
||||
if solv.solve() == ResultFlag.OKAY:
|
||||
# Get the result (unpack from the entity or parameters)
|
||||
# x and y are actually float type
|
||||
dof = solv.dof()
|
||||
x, y = solv.params(p1.params)
|
||||
print(dof)
|
||||
print(x)
|
||||
print(y)
|
||||
|
||||
else:
|
||||
# Error!
|
||||
# Get the list of all constraints
|
||||
failures = solv.failures()
|
||||
print(failures)
|
||||
...
|
||||
|
||||
solve_constraint()
|
||||
@@ -1,815 +0,0 @@
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import vtk
|
||||
from PySide6 import QtCore, QtWidgets
|
||||
from PySide6.QtCore import Signal
|
||||
from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
|
||||
from vtkmodules.util.numpy_support import vtk_to_numpy, numpy_to_vtk
|
||||
|
||||
|
||||
class VTKWidget(QtWidgets.QWidget):
|
||||
face_data = Signal(dict)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.selected_vtk_line = []
|
||||
self.access_selected_points = []
|
||||
self.selected_normal = None
|
||||
self.centroid = None
|
||||
self.selected_edges = []
|
||||
self.cell_normals = None
|
||||
|
||||
self.local_matrix = None
|
||||
|
||||
self.project_tosketch_points = []
|
||||
self.project_tosketch_lines = []
|
||||
|
||||
self.vtk_widget = QVTKRenderWindowInteractor(self)
|
||||
|
||||
self.picked_edge_actors = []
|
||||
self.displayed_normal_actors = []
|
||||
self.body_actors_orig = []
|
||||
self.projected_mesh_actors = []
|
||||
self.interactor_actors = []
|
||||
|
||||
self.flip_toggle = False
|
||||
|
||||
# Create layout and add VTK widget
|
||||
layout = QtWidgets.QVBoxLayout()
|
||||
layout.addWidget(self.vtk_widget)
|
||||
self.setLayout(layout)
|
||||
|
||||
# Create VTK pipeline
|
||||
self.renderer = vtk.vtkRenderer()
|
||||
self.renderer_projections = vtk.vtkRenderer()
|
||||
self.renderer_indicators = vtk.vtkRenderer()
|
||||
|
||||
self.renderer.SetViewport(0, 0, 1, 1) # Full viewport
|
||||
self.renderer_projections.SetViewport(0, 0, 1, 1) # Full viewport, overlays the first
|
||||
self.renderer_indicators.SetViewport(0, 0, 1, 1) # Full viewport, overlays the first
|
||||
|
||||
self.renderer.SetLayer(0)
|
||||
self.renderer_projections.SetLayer(1)
|
||||
self.renderer_indicators.SetLayer(2) # This will be on top
|
||||
|
||||
# Preserve color and depth buffers for non-zero layers
|
||||
self.renderer_projections.SetPreserveColorBuffer(True)
|
||||
self.renderer_projections.SetPreserveDepthBuffer(True)
|
||||
self.renderer_indicators.SetPreserveColorBuffer(True)
|
||||
self.renderer_indicators.SetPreserveDepthBuffer(True)
|
||||
|
||||
# Add renderers to the render window
|
||||
render_window = self.vtk_widget.GetRenderWindow()
|
||||
render_window.SetNumberOfLayers(3)
|
||||
render_window.AddRenderer(self.renderer)
|
||||
render_window.AddRenderer(self.renderer_projections)
|
||||
render_window.AddRenderer(self.renderer_indicators)
|
||||
|
||||
self.camera = vtk.vtkCamera()
|
||||
self.camera.SetPosition(5, 5, 1000)
|
||||
self.camera.SetFocalPoint(0, 0, 0)
|
||||
self.camera.SetClippingRange(1, 10000) # Adjusted clipping range
|
||||
|
||||
self.renderer.SetActiveCamera(self.camera)
|
||||
self.renderer_projections.SetActiveCamera(self.camera)
|
||||
self.renderer_indicators.SetActiveCamera(self.camera)
|
||||
|
||||
self.interactor = self.vtk_widget.GetRenderWindow().GetInteractor()
|
||||
|
||||
# Light Setup
|
||||
def add_light(renderer, position, color=(1, 1, 1), intensity=1.0):
|
||||
light = vtk.vtkLight()
|
||||
light.SetPosition(position)
|
||||
light.SetColor(color)
|
||||
light.SetIntensity(intensity)
|
||||
renderer.AddLight(light)
|
||||
|
||||
# Add lights from multiple directions
|
||||
add_light(self.renderer, (1000, 0, 0), intensity=1.5)
|
||||
add_light(self.renderer, (-1000, 0, 0), intensity=1.5)
|
||||
add_light(self.renderer, (0, 1000, 0), intensity=1.5)
|
||||
add_light(self.renderer, (0, -1000, 0), intensity=1.5)
|
||||
add_light(self.renderer, (0, 0, 1000), intensity=1.5)
|
||||
add_light(self.renderer, (0, 0, -1000), intensity=1.5)
|
||||
|
||||
# Set up picking
|
||||
self.picker = vtk.vtkCellPicker()
|
||||
self.picker.SetTolerance(0.005)
|
||||
|
||||
# Create a mapper and actor for picked cells
|
||||
self.picked_mapper = vtk.vtkDataSetMapper()
|
||||
self.picked_actor = vtk.vtkActor()
|
||||
self.picked_actor.SetMapper(self.picked_mapper)
|
||||
self.picked_actor.GetProperty().SetColor(1.0, 0.0, 0.0) # Red color for picked faces
|
||||
self.picked_actor.VisibilityOff() # Initially hide the actor
|
||||
self.renderer.AddActor(self.picked_actor)
|
||||
|
||||
# Create an extract selection filter
|
||||
self.extract_selection = vtk.vtkExtractSelection()
|
||||
|
||||
# Set up interactor style
|
||||
self.style = vtk.vtkInteractorStyleTrackballCamera()
|
||||
self.interactor.SetInteractorStyle(self.style)
|
||||
|
||||
# Add observer for mouse clicks
|
||||
self.interactor.AddObserver("RightButtonPressEvent", self.on_click)
|
||||
|
||||
# Add axis gizmo (smaller size)
|
||||
self.axes = vtk.vtkAxesActor()
|
||||
self.axes.SetTotalLength(0.5, 0.5, 0.5) # Reduced size
|
||||
self.axes.SetShaftType(0)
|
||||
self.axes.SetAxisLabels(1)
|
||||
|
||||
# Create an orientation marker
|
||||
self.axes_widget = vtk.vtkOrientationMarkerWidget()
|
||||
self.axes_widget.SetOrientationMarker(self.axes)
|
||||
self.axes_widget.SetInteractor(self.interactor)
|
||||
self.axes_widget.SetViewport(0.0, 0.0, 0.2, 0.2) # Set position and size
|
||||
self.axes_widget.EnabledOn()
|
||||
self.axes_widget.InteractiveOff()
|
||||
|
||||
# Start the interactor
|
||||
self.interactor.Initialize()
|
||||
self.interactor.Start()
|
||||
|
||||
# Create the grid
|
||||
grid = self.create_grid(size=100, spacing=10)
|
||||
|
||||
# Setup actor and mapper
|
||||
mapper = vtk.vtkPolyDataMapper()
|
||||
mapper.SetInputData(grid)
|
||||
|
||||
actor = vtk.vtkActor()
|
||||
actor.SetPickable(False)
|
||||
actor.SetMapper(mapper)
|
||||
actor.GetProperty().SetColor(0.5, 0.5, 0.5) # Set grid color to gray
|
||||
|
||||
self.renderer.AddActor(actor)
|
||||
|
||||
def reset_camera(self):
|
||||
self.renderer.ResetCamera()
|
||||
self.camera.SetClippingRange(1, 100000) # Set your desired range
|
||||
self.vtk_widget.GetRenderWindow().Render()
|
||||
|
||||
def update_render(self):
|
||||
self.renderer.ResetCameraClippingRange()
|
||||
self.renderer_projections.ResetCameraClippingRange()
|
||||
self.renderer_indicators.ResetCameraClippingRange()
|
||||
self.camera.SetClippingRange(1, 100000)
|
||||
self.vtk_widget.GetRenderWindow().Render()
|
||||
|
||||
def create_grid(self, size=100, spacing=10):
|
||||
# Create a vtkPoints object and store the points in it
|
||||
points = vtk.vtkPoints()
|
||||
|
||||
# Create lines
|
||||
lines = vtk.vtkCellArray()
|
||||
|
||||
# Create the grid
|
||||
for i in range(-size, size + 1, spacing):
|
||||
# X-direction line
|
||||
points.InsertNextPoint(i, -size, 0)
|
||||
points.InsertNextPoint(i, size, 0)
|
||||
line = vtk.vtkLine()
|
||||
line.GetPointIds().SetId(0, points.GetNumberOfPoints() - 2)
|
||||
line.GetPointIds().SetId(1, points.GetNumberOfPoints() - 1)
|
||||
lines.InsertNextCell(line)
|
||||
|
||||
# Y-direction line
|
||||
points.InsertNextPoint(-size, i, 0)
|
||||
points.InsertNextPoint(size, i, 0)
|
||||
line = vtk.vtkLine()
|
||||
line.GetPointIds().SetId(0, points.GetNumberOfPoints() - 2)
|
||||
line.GetPointIds().SetId(1, points.GetNumberOfPoints() - 1)
|
||||
lines.InsertNextCell(line)
|
||||
|
||||
# Create a polydata to store everything in
|
||||
grid = vtk.vtkPolyData()
|
||||
|
||||
# Add the points to the dataset
|
||||
grid.SetPoints(points)
|
||||
|
||||
# Add the lines to the dataset
|
||||
grid.SetLines(lines)
|
||||
|
||||
return grid
|
||||
|
||||
def on_receive_command(self, command):
|
||||
"""Calls the individual commands pressed in main"""
|
||||
print("Receive command: ", command)
|
||||
if command == "flip":
|
||||
self.clear_actors_projection()
|
||||
self.flip_toggle = not self.flip_toggle # Toggle the flag
|
||||
self.on_invert_normal()
|
||||
|
||||
@staticmethod
|
||||
def compute_normal_from_lines(line1, line2):
|
||||
vec1 = line1[1] - line1[0]
|
||||
vec2 = line2[1] - line2[0]
|
||||
normal = np.cross(vec1, vec2)
|
||||
print(normal)
|
||||
normal = normal / np.linalg.norm(normal)
|
||||
return normal
|
||||
|
||||
def load_interactor_mesh(self, edges, off_vector):
|
||||
# Create vtkPoints to store all points
|
||||
points = vtk.vtkPoints()
|
||||
|
||||
# Create vtkCellArray to store the lines
|
||||
lines = vtk.vtkCellArray()
|
||||
|
||||
for edge in edges:
|
||||
# Add points for this edge
|
||||
point_id1 = points.InsertNextPoint(edge[0])
|
||||
point_id2 = points.InsertNextPoint(edge[1])
|
||||
|
||||
# Create a line using the point IDs
|
||||
line = vtk.vtkLine()
|
||||
line.GetPointIds().SetId(0, point_id1)
|
||||
line.GetPointIds().SetId(1, point_id2)
|
||||
|
||||
# Add the line to the cell array
|
||||
lines.InsertNextCell(line)
|
||||
|
||||
# Create vtkPolyData to store the geometry
|
||||
polydata = vtk.vtkPolyData()
|
||||
polydata.SetPoints(points)
|
||||
polydata.SetLines(lines)
|
||||
|
||||
# Create a transform for mirroring across the y-axis
|
||||
matrix_transform = vtk.vtkTransform()
|
||||
|
||||
if self.local_matrix:
|
||||
print(self.local_matrix)
|
||||
matrix = vtk.vtkMatrix4x4()
|
||||
matrix.DeepCopy(self.local_matrix)
|
||||
matrix.Invert()
|
||||
matrix_transform.SetMatrix(matrix)
|
||||
#matrix_transform.Scale(1, 1, 1) # This mirrors across the y-axis
|
||||
|
||||
# Apply the matrix transform
|
||||
transformFilter = vtk.vtkTransformPolyDataFilter()
|
||||
transformFilter.SetInputData(polydata)
|
||||
transformFilter.SetTransform(matrix_transform)
|
||||
transformFilter.Update()
|
||||
|
||||
# Create and apply the offset transform
|
||||
offset_transform = vtk.vtkTransform()
|
||||
offset_transform.Translate(off_vector[0], off_vector[1], off_vector[2])
|
||||
|
||||
offsetFilter = vtk.vtkTransformPolyDataFilter()
|
||||
offsetFilter.SetInputConnection(transformFilter.GetOutputPort())
|
||||
offsetFilter.SetTransform(offset_transform)
|
||||
offsetFilter.Update()
|
||||
|
||||
# Create a mapper and actor
|
||||
mapper = vtk.vtkPolyDataMapper()
|
||||
mapper.SetInputConnection(offsetFilter.GetOutputPort())
|
||||
|
||||
actor = vtk.vtkActor()
|
||||
actor.SetMapper(mapper)
|
||||
actor.GetProperty().SetColor(1.0, 1.0, 1.0)
|
||||
actor.GetProperty().SetLineWidth(4) # Set line width
|
||||
|
||||
# Add the actor to the scene
|
||||
self.renderer.AddActor(actor)
|
||||
self.interactor_actors.append(actor)
|
||||
|
||||
mapper.Update()
|
||||
self.vtk_widget.GetRenderWindow().Render()
|
||||
|
||||
def render_from_points_direct_with_faces(self, vertices, faces, color=(0.1, 0.2, 0.8), line_width=2, point_size=5):
|
||||
"""Sketch Widget has inverted Y axiis therefore we invert y via scale here until fix"""
|
||||
|
||||
points = vtk.vtkPoints()
|
||||
|
||||
# Use SetData with numpy array
|
||||
vtk_array = numpy_to_vtk(vertices, deep=True)
|
||||
points.SetData(vtk_array)
|
||||
|
||||
# Create a vtkCellArray to store the triangles
|
||||
triangles = vtk.vtkCellArray()
|
||||
for face in faces:
|
||||
triangle = vtk.vtkTriangle()
|
||||
triangle.GetPointIds().SetId(0, face[0])
|
||||
triangle.GetPointIds().SetId(1, face[1])
|
||||
triangle.GetPointIds().SetId(2, face[2])
|
||||
triangles.InsertNextCell(triangle)
|
||||
|
||||
# Create a polydata object
|
||||
polydata = vtk.vtkPolyData()
|
||||
polydata.SetPoints(points)
|
||||
polydata.SetPolys(triangles)
|
||||
|
||||
# Calculate normals
|
||||
normalGenerator = vtk.vtkPolyDataNormals()
|
||||
normalGenerator.SetInputData(polydata)
|
||||
normalGenerator.ComputePointNormalsOn()
|
||||
normalGenerator.ComputeCellNormalsOn()
|
||||
normalGenerator.Update()
|
||||
|
||||
self.cell_normals = vtk_to_numpy(normalGenerator.GetOutput().GetCellData().GetNormals())
|
||||
|
||||
# Create a mapper and actor
|
||||
mapper = vtk.vtkPolyDataMapper()
|
||||
mapper.SetInputData(polydata)
|
||||
|
||||
actor = vtk.vtkActor()
|
||||
actor.SetMapper(mapper)
|
||||
actor.GetProperty().SetColor(color)
|
||||
actor.GetProperty().EdgeVisibilityOff()
|
||||
actor.GetProperty().SetLineWidth(line_width)
|
||||
actor.GetProperty().SetMetallic(1)
|
||||
actor.GetProperty().SetOpacity(0.8)
|
||||
actor.SetPickable(False)
|
||||
|
||||
self.renderer.AddActor(actor)
|
||||
self.body_actors_orig.append(actor)
|
||||
self.vtk_widget.GetRenderWindow().Render()
|
||||
|
||||
def clear_body_actors(self):
|
||||
for actor in self.body_actors_orig:
|
||||
self.renderer.RemoveActor(actor)
|
||||
|
||||
def visualize_matrix(self, matrix):
|
||||
points = vtk.vtkPoints()
|
||||
for i in range(4):
|
||||
for j in range(4):
|
||||
points.InsertNextPoint(matrix.GetElement(0, j),
|
||||
matrix.GetElement(1, j),
|
||||
matrix.GetElement(2, j))
|
||||
|
||||
polydata = vtk.vtkPolyData()
|
||||
polydata.SetPoints(points)
|
||||
|
||||
mapper = vtk.vtkPolyDataMapper()
|
||||
mapper.SetInputData(polydata)
|
||||
|
||||
actor = vtk.vtkActor()
|
||||
actor.SetMapper(mapper)
|
||||
actor.GetProperty().SetPointSize(5)
|
||||
|
||||
self.renderer.AddActor(actor)
|
||||
|
||||
def numpy_to_vtk(self, array, deep=True):
|
||||
"""Convert a numpy array to a vtk array."""
|
||||
vtk_array = vtk.vtkDoubleArray()
|
||||
vtk_array.SetNumberOfComponents(array.shape[1])
|
||||
vtk_array.SetNumberOfTuples(array.shape[0])
|
||||
|
||||
for i in range(array.shape[0]):
|
||||
for j in range(array.shape[1]):
|
||||
vtk_array.SetComponent(i, j, array[i, j])
|
||||
|
||||
return vtk_array
|
||||
|
||||
def get_points_and_edges_from_polydata(self, polydata) -> list:
|
||||
# Extract points
|
||||
points = {}
|
||||
vtk_points = polydata.GetPoints()
|
||||
for i in range(vtk_points.GetNumberOfPoints()):
|
||||
point = vtk_points.GetPoint(i)
|
||||
points[i] = np.array(point)
|
||||
|
||||
# Extract edges
|
||||
edges = []
|
||||
for i in range(polydata.GetNumberOfCells()):
|
||||
cell = polydata.GetCell(i)
|
||||
if cell.GetCellType() == vtk.VTK_LINE:
|
||||
point_ids = cell.GetPointIds()
|
||||
edge = (point_ids.GetId(0), point_ids.GetId(1))
|
||||
edges.append(edge)
|
||||
|
||||
return points, edges
|
||||
|
||||
def project_mesh_to_plane(self, input_mesh, normal, origin):
|
||||
# Create the projector
|
||||
projector = vtk.vtkProjectPointsToPlane()
|
||||
projector.SetInputData(input_mesh)
|
||||
projector.SetProjectionTypeToSpecifiedPlane()
|
||||
|
||||
# Set the normal and origin of the plane
|
||||
projector.SetNormal(normal)
|
||||
projector.SetOrigin(origin)
|
||||
|
||||
# Execute the projection
|
||||
projector.Update()
|
||||
|
||||
# Get the projected mesh
|
||||
projected_mesh = projector.GetOutput()
|
||||
return projected_mesh
|
||||
|
||||
def compute_2d_coordinates(self, projected_mesh, normal):
|
||||
# Normalize the normal vector
|
||||
normal = np.array(normal)
|
||||
normal = normal / np.linalg.norm(normal)
|
||||
|
||||
# Create a vtkTransform
|
||||
transform = vtk.vtkTransform()
|
||||
transform.PostMultiply() # This ensures transforms are applied in the order we specify
|
||||
|
||||
# Rotate so that the normal aligns with the Z-axis
|
||||
rotation_axis = np.cross(normal, [0, 0, 1])
|
||||
angle = np.arccos(np.dot(normal, [0, 0, 1])) * 180 / np.pi # Convert to degrees
|
||||
|
||||
if np.linalg.norm(rotation_axis) > 1e-6: # Check if rotation is needed
|
||||
transform.RotateWXYZ(angle, rotation_axis[0], rotation_axis[1], rotation_axis[2])
|
||||
|
||||
# Get the transformation matrix
|
||||
matrix = transform.GetMatrix()
|
||||
self.local_matrix = [matrix.GetElement(i, j) for i in range(4) for j in range(4)]
|
||||
|
||||
# Apply the transform to the polydata
|
||||
transformFilter = vtk.vtkTransformPolyDataFilter()
|
||||
transformFilter.SetInputData(projected_mesh)
|
||||
transformFilter.SetTransform(transform)
|
||||
transformFilter.Update()
|
||||
|
||||
# Get the transformed points
|
||||
transformed_polydata = transformFilter.GetOutput()
|
||||
points = transformed_polydata.GetPoints()
|
||||
|
||||
# Extract 2D coordinates
|
||||
xy_coordinates = []
|
||||
for i in range(points.GetNumberOfPoints()):
|
||||
point = points.GetPoint(i)
|
||||
xy_coordinates.append((point[0], point[1]))
|
||||
|
||||
return xy_coordinates
|
||||
|
||||
def compute_2d_coordinates_line(self, projected_mesh, normal):
|
||||
# Normalize the normal vector
|
||||
normal = np.array(normal)
|
||||
normal = normal / np.linalg.norm(normal)
|
||||
|
||||
# Create a vtkTransform
|
||||
transform = vtk.vtkTransform()
|
||||
transform.PostMultiply() # This ensures transforms are applied in the order we specify
|
||||
|
||||
# Rotate so that the normal aligns with the Z-axis
|
||||
rotation_axis = np.cross(normal, [0, 0, 1])
|
||||
angle = np.arccos(np.dot(normal, [0, 0, 1])) * 180 / np.pi # Convert to degrees
|
||||
|
||||
if np.linalg.norm(rotation_axis) > 1e-6: # Check if rotation is needed
|
||||
transform.RotateWXYZ(angle, rotation_axis[0], rotation_axis[1], rotation_axis[2])
|
||||
|
||||
# Get the transformation matrix
|
||||
matrix = transform.GetMatrix()
|
||||
self.local_matrix = [matrix.GetElement(i, j) for i in range(4) for j in range(4)]
|
||||
|
||||
# Apply the transform to the polydata
|
||||
transformFilter = vtk.vtkTransformPolyDataFilter()
|
||||
transformFilter.SetInputData(projected_mesh)
|
||||
transformFilter.SetTransform(transform)
|
||||
transformFilter.Update()
|
||||
|
||||
# Get the transformed points
|
||||
transformed_polydata = transformFilter.GetOutput()
|
||||
points = transformed_polydata.GetPoints()
|
||||
lines = transformed_polydata.GetLines()
|
||||
|
||||
# Extract 2D coordinates
|
||||
xy_coordinates = []
|
||||
|
||||
if points and lines:
|
||||
points_data = points.GetData()
|
||||
line_ids = vtk.vtkIdList()
|
||||
|
||||
# Loop through all the lines in the vtkCellArray
|
||||
lines.InitTraversal()
|
||||
while lines.GetNextCell(line_ids):
|
||||
line_coordinates = []
|
||||
for j in range(line_ids.GetNumberOfIds()):
|
||||
point_id = line_ids.GetId(j)
|
||||
point = points.GetPoint(point_id)
|
||||
line_coordinates.append((point[0], point[1])) # Only take x, y
|
||||
xy_coordinates.append(line_coordinates)
|
||||
|
||||
return xy_coordinates
|
||||
|
||||
|
||||
def compute_2d_coordinates_line_bak(self, line_source, normal):
|
||||
# Ensure the input is a vtkLineSource
|
||||
print("line", line_source)
|
||||
if not isinstance(line_source, vtk.vtkLineSource):
|
||||
raise ValueError("Input must be a vtkLineSource")
|
||||
|
||||
# Normalize the normal vector
|
||||
normal = np.array(normal)
|
||||
normal = normal / np.linalg.norm(normal)
|
||||
|
||||
# Create a vtkTransform
|
||||
transform = vtk.vtkTransform()
|
||||
transform.PostMultiply() # This ensures transforms are applied in the order we specify
|
||||
|
||||
# Rotate so that the normal aligns with the Z-axis
|
||||
rotation_axis = np.cross(normal, [0, 0, 1])
|
||||
angle = np.arccos(np.dot(normal, [0, 0, 1])) * 180 / np.pi # Convert to degrees
|
||||
|
||||
if np.linalg.norm(rotation_axis) > 1e-6: # Check if rotation is needed
|
||||
transform.RotateWXYZ(angle, rotation_axis[0], rotation_axis[1], rotation_axis[2])
|
||||
|
||||
# Get the transformation matrix
|
||||
matrix = transform.GetMatrix()
|
||||
local_matrix = [matrix.GetElement(i, j) for i in range(4) for j in range(4)]
|
||||
|
||||
# Get the polydata from the line source
|
||||
line_source.Update()
|
||||
polydata = line_source.GetOutput()
|
||||
|
||||
# Apply the transform to the polydata
|
||||
transform_filter = vtk.vtkTransformPolyDataFilter()
|
||||
transform_filter.SetInputData(polydata)
|
||||
transform_filter.SetTransform(transform)
|
||||
transform_filter.Update()
|
||||
|
||||
# Get the transformed points
|
||||
transformed_polydata = transform_filter.GetOutput()
|
||||
transformed_points = transformed_polydata.GetPoints()
|
||||
|
||||
# Extract 2D coordinates
|
||||
xy_coordinates = []
|
||||
for i in range(transformed_points.GetNumberOfPoints()):
|
||||
point = transformed_points.GetPoint(i)
|
||||
xy_coordinates.append((point[0], point[1]))
|
||||
|
||||
return xy_coordinates
|
||||
|
||||
def project_2d_to_3d(self, xy_coordinates, normal):
|
||||
# Normalize the normal vector
|
||||
normal = np.array(normal)
|
||||
normal = normal / np.linalg.norm(normal)
|
||||
|
||||
# Create a vtkTransform for the reverse transformation
|
||||
reverse_transform = vtk.vtkTransform()
|
||||
reverse_transform.PostMultiply() # This ensures transforms are applied in the order we specify
|
||||
|
||||
# Compute the rotation axis and angle (same as in compute_2d_coordinates)
|
||||
rotation_axis = np.cross(normal, [0, 0, 1])
|
||||
angle = np.arccos(np.dot(normal, [0, 0, 1])) * 180 / np.pi # Convert to degrees
|
||||
|
||||
if np.linalg.norm(rotation_axis) > 1e-6: # Check if rotation is needed
|
||||
# Apply the inverse rotation
|
||||
reverse_transform.RotateWXYZ(-angle, rotation_axis[0], rotation_axis[1], rotation_axis[2])
|
||||
|
||||
# Create vtkPoints to store the 2D points
|
||||
points_2d = vtk.vtkPoints()
|
||||
for x, y in xy_coordinates:
|
||||
points_2d.InsertNextPoint(x, y, 0) # Z-coordinate is 0 for 2D points
|
||||
|
||||
# Create a polydata with the 2D points
|
||||
polydata_2d = vtk.vtkPolyData()
|
||||
polydata_2d.SetPoints(points_2d)
|
||||
|
||||
# Apply the reverse transform to the polydata
|
||||
transform_filter = vtk.vtkTransformPolyDataFilter()
|
||||
transform_filter.SetInputData(polydata_2d)
|
||||
transform_filter.SetTransform(reverse_transform)
|
||||
transform_filter.Update()
|
||||
|
||||
# Get the transformed points (now in 3D)
|
||||
transformed_polydata = transform_filter.GetOutput()
|
||||
transformed_points = transformed_polydata.GetPoints()
|
||||
|
||||
# Extract 3D coordinates
|
||||
xyz_coordinates = []
|
||||
for i in range(transformed_points.GetNumberOfPoints()):
|
||||
point = transformed_points.GetPoint(i)
|
||||
xyz_coordinates.append((point[0], point[1], point[2]))
|
||||
|
||||
return xyz_coordinates
|
||||
|
||||
def add_normal_line(self, origin, normal, length=10.0, color=(1, 0, 0)):
|
||||
# Normalize the normal vector
|
||||
normal = np.array(normal)
|
||||
normal = normal / np.linalg.norm(normal)
|
||||
|
||||
# Calculate the end point
|
||||
end_point = origin + normal * length
|
||||
|
||||
# Create vtkPoints
|
||||
points = vtk.vtkPoints()
|
||||
points.InsertNextPoint(origin)
|
||||
points.InsertNextPoint(end_point)
|
||||
|
||||
# Create a line
|
||||
line = vtk.vtkLine()
|
||||
line.GetPointIds().SetId(0, 0)
|
||||
line.GetPointIds().SetId(1, 1)
|
||||
|
||||
# Create a cell array to store the line
|
||||
lines = vtk.vtkCellArray()
|
||||
lines.InsertNextCell(line)
|
||||
|
||||
# Create a polydata to store everything in
|
||||
polyData = vtk.vtkPolyData()
|
||||
polyData.SetPoints(points)
|
||||
polyData.SetLines(lines)
|
||||
|
||||
# Create mapper and actor
|
||||
mapper = vtk.vtkPolyDataMapper()
|
||||
mapper.SetInputData(polyData)
|
||||
|
||||
actor = vtk.vtkActor()
|
||||
actor.SetMapper(mapper)
|
||||
actor.GetProperty().SetColor(color)
|
||||
actor.GetProperty().SetLineWidth(2) # Adjust line width as needed
|
||||
|
||||
# Add to renderer
|
||||
self.renderer.AddActor(actor)
|
||||
self.vtk_widget.GetRenderWindow().Render()
|
||||
|
||||
return actor # Return the actor in case you need to remove or modify it later
|
||||
|
||||
def on_invert_normal(self):
|
||||
# Kippstufe für Normal flip
|
||||
if self.selected_normal is not None:
|
||||
self.clear_actors_normals()
|
||||
self.compute_projection(self.flip_toggle)
|
||||
|
||||
def on_click(self, obj, event):
|
||||
click_pos = self.interactor.GetEventPosition()
|
||||
|
||||
# Perform pick
|
||||
self.picker.Pick(click_pos[0], click_pos[1], 0, self.renderer)
|
||||
|
||||
# Get picked cell ID
|
||||
cell_id = self.picker.GetCellId()
|
||||
|
||||
if cell_id != -1:
|
||||
print(f"Picked cell ID: {cell_id}")
|
||||
|
||||
# Get the polydata and the picked cell
|
||||
polydata = self.picker.GetActor().GetMapper().GetInput()
|
||||
cell = polydata.GetCell(cell_id)
|
||||
|
||||
# Ensure it's a line
|
||||
if cell.GetCellType() == vtk.VTK_LINE:
|
||||
|
||||
# Get the two points of the line
|
||||
point_id1 = cell.GetPointId(0)
|
||||
point_id2 = cell.GetPointId(1)
|
||||
|
||||
proj_point1 = polydata.GetPoint(point_id1)
|
||||
proj_point2 = polydata.GetPoint(point_id2)
|
||||
|
||||
self.access_selected_points.append((proj_point1, proj_point2))
|
||||
|
||||
point1 = np.array(proj_point1)
|
||||
point2 = np.array(proj_point2)
|
||||
|
||||
#print(f"Line starts at: {point1}")
|
||||
#print(f"Line ends at: {point2}")
|
||||
|
||||
# Store this line for later use if needed
|
||||
self.selected_edges.append((point1, point2))
|
||||
|
||||
# Create a new vtkLineSource for the picked edge
|
||||
line_source = vtk.vtkLineSource()
|
||||
line_source.SetPoint1(point1)
|
||||
line_source.SetPoint2(point2)
|
||||
|
||||
self.selected_vtk_line.append(line_source)
|
||||
|
||||
# Create a mapper and actor for the picked edge
|
||||
edge_mapper = vtk.vtkPolyDataMapper()
|
||||
edge_mapper.SetInputConnection(line_source.GetOutputPort())
|
||||
|
||||
edge_actor = vtk.vtkActor()
|
||||
edge_actor.SetMapper(edge_mapper)
|
||||
edge_actor.GetProperty().SetColor(1.0, 0.0, 0.0) # Red color for picked edges
|
||||
edge_actor.GetProperty().SetLineWidth(5) # Make the line thicker
|
||||
|
||||
# Add the actor to the renderer and store it
|
||||
self.renderer_indicators.AddActor(edge_actor)
|
||||
self.picked_edge_actors.append(edge_actor)
|
||||
|
||||
if len(self.selected_edges) == 2:
|
||||
self.compute_projection(False)
|
||||
|
||||
if len(self.selected_edges) > 2:
|
||||
# Clear lists for selection
|
||||
self.selected_vtk_line.clear()
|
||||
self.selected_edges.clear()
|
||||
self.clear_edge_select()
|
||||
|
||||
# Clear Actors from view
|
||||
self.clear_actors_projection()
|
||||
self.clear_actors_sel_edges()
|
||||
self.clear_actors_normals()
|
||||
|
||||
|
||||
def find_origin_vertex(self, edge1, edge2):
|
||||
if edge1[0] == edge2[0]or edge1[0] == edge2[1]:
|
||||
return edge1[0]
|
||||
elif edge1[1] == edge2[0] or edge1[1] == edge2[1]:
|
||||
return edge1[1]
|
||||
else:
|
||||
return None # The edges don't share a vertex
|
||||
|
||||
def clear_edge_select(self ):
|
||||
# Clear selection after projection was succesful
|
||||
self.selected_edges = []
|
||||
self.selected_normal = []
|
||||
|
||||
def clear_actors_projection(self):
|
||||
"""Removes all actors that were used for projection"""
|
||||
for flat_mesh in self.projected_mesh_actors:
|
||||
self.renderer_projections.RemoveActor(flat_mesh)
|
||||
|
||||
def clear_actors_normals(self):
|
||||
for normals in self.displayed_normal_actors:
|
||||
self.renderer_indicators.RemoveActor(normals)
|
||||
|
||||
def clear_actors_sel_edges(self):
|
||||
for edge_line in self.picked_edge_actors:
|
||||
self.renderer_indicators.RemoveActor(edge_line)
|
||||
|
||||
def clear_actors_interactor(self):
|
||||
### Clear the outline of the mesh
|
||||
for interactor in self.interactor_actors:
|
||||
self.renderer.RemoveActor(interactor)
|
||||
|
||||
def compute_projection(self, direction_invert: bool = False):
|
||||
|
||||
# Compute the normal from the two selected edges )
|
||||
edge1 = self.selected_edges[0][1] - self.selected_edges[0][0]
|
||||
edge2 = self.selected_edges[1][1] - self.selected_edges[1][0]
|
||||
selected_normal = np.cross(edge1, edge2)
|
||||
selected_normal = selected_normal / np.linalg.norm(selected_normal)
|
||||
#print("Computed normal:", self.selected_normal)
|
||||
|
||||
# Invert the normal in local z if direction_invert is True
|
||||
if direction_invert:
|
||||
self.selected_normal = -selected_normal
|
||||
else:
|
||||
self.selected_normal = selected_normal
|
||||
|
||||
self.centroid = np.mean([point for edge in self.selected_edges for point in edge], axis=0)
|
||||
#self.centroid = self.find_origin_vertex(edge1, edge2)
|
||||
|
||||
# Draw the normal line
|
||||
normal_length = 50 # Adjust this value to change the length of the normal line
|
||||
normal_actor = self.add_normal_line(self.centroid, self.selected_normal, length=normal_length,
|
||||
color=(1, 0, 0))
|
||||
|
||||
polydata = self.picker.GetActor().GetMapper().GetInput()
|
||||
|
||||
projected_polydata = self.project_mesh_to_plane(polydata, self.selected_normal, self.centroid)
|
||||
|
||||
# Extract 2D coordinates
|
||||
self.project_tosketch_points = self.compute_2d_coordinates(projected_polydata, self.selected_normal)
|
||||
|
||||
# Green indicator mesh needs to be translated to xy point paris start end.
|
||||
self.project_tosketch_lines = self.compute_2d_coordinates_line(projected_polydata, self.selected_normal)
|
||||
|
||||
print("result", self.project_tosketch_lines)
|
||||
"""# Seperately rotate selected edges for drawing
|
||||
self.project_tosketch_lines.clear()
|
||||
for vtk_line in self.selected_vtk_line:
|
||||
proj_vtk_line = self.compute_2d_coordinates_line(vtk_line, self.selected_normal)
|
||||
self.project_tosketch_lines.append(proj_vtk_line)
|
||||
print("outgoing lines", self.project_tosketch_lines)"""
|
||||
|
||||
# Create a mapper and actor for the projected data
|
||||
mapper = vtk.vtkPolyDataMapper()
|
||||
mapper.SetInputData(projected_polydata)
|
||||
|
||||
# Projected mesh in green
|
||||
actor = vtk.vtkActor()
|
||||
actor.SetMapper(mapper)
|
||||
#actor.GetProperty().SetRenderLinesAsTubes(True)
|
||||
actor.GetProperty().SetColor(0.0, 1.0, 0.0) # Set color to green
|
||||
actor.GetProperty().SetLineWidth(4) # Set line width
|
||||
|
||||
self.renderer_indicators.AddActor(normal_actor)
|
||||
self.displayed_normal_actors.append(normal_actor)
|
||||
|
||||
self.renderer_projections.AddActor(actor)
|
||||
self.projected_mesh_actors.append(actor)
|
||||
|
||||
# Render the scene
|
||||
self.update_render()
|
||||
self.vtk_widget.GetRenderWindow().Render()
|
||||
|
||||
def start(self):
|
||||
self.interactor.Initialize()
|
||||
self.interactor.Start()
|
||||
|
||||
|
||||
class MainWindow(QtWidgets.QMainWindow):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.vtk_widget = VTKWidget()
|
||||
self.setCentralWidget(self.vtk_widget)
|
||||
self.setWindowTitle("VTK Mesh Viewer")
|
||||
self.vtk_widget.create_cube_mesh()
|
||||
self.show()
|
||||
self.vtk_widget.start()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
window = MainWindow()
|
||||
sys.exit(app.exec())
|
||||
@@ -1,337 +0,0 @@
|
||||
def are_coplanar(self, normal1, normal2, point1, point2, tolerance=1e-6):
|
||||
# Check if normals are parallel
|
||||
if np.abs(np.dot(normal1, normal2)) < 1 - tolerance:
|
||||
return False
|
||||
|
||||
# Check if points lie on the same plane
|
||||
diff = point2 - point1
|
||||
return np.abs(np.dot(diff, normal1)) < tolerance
|
||||
|
||||
|
||||
def merge_coplanar_triangles(self, polydata):
|
||||
# Compute normals
|
||||
normalGenerator = vtk.vtkPolyDataNormals()
|
||||
normalGenerator.SetInputData(polydata)
|
||||
normalGenerator.ComputePointNormalsOff()
|
||||
normalGenerator.ComputeCellNormalsOn()
|
||||
normalGenerator.Update()
|
||||
|
||||
mesh = normalGenerator.GetOutput()
|
||||
n_cells = mesh.GetNumberOfCells()
|
||||
|
||||
# Create a map to store merged triangles
|
||||
merged = {}
|
||||
|
||||
for i in range(n_cells):
|
||||
if i in merged:
|
||||
continue
|
||||
|
||||
cell = mesh.GetCell(i)
|
||||
normal = np.array(mesh.GetCellData().GetNormals().GetTuple(i))
|
||||
point = np.array(cell.GetPoints().GetPoint(0))
|
||||
|
||||
merged[i] = [i]
|
||||
|
||||
for j in range(i + 1, n_cells):
|
||||
if j in merged:
|
||||
continue
|
||||
|
||||
cell_j = mesh.GetCell(j)
|
||||
normal_j = np.array(mesh.GetCellData().GetNormals().GetTuple(j))
|
||||
point_j = np.array(cell_j.GetPoints().GetPoint(0))
|
||||
|
||||
if self.are_coplanar(normal, normal_j, point, point_j):
|
||||
merged[i].append(j)
|
||||
|
||||
# Create new polygons
|
||||
new_polygons = vtk.vtkCellArray()
|
||||
for group in merged.values():
|
||||
if len(group) > 1:
|
||||
polygon = vtk.vtkPolygon()
|
||||
points = set()
|
||||
for idx in group:
|
||||
cell = mesh.GetCell(idx)
|
||||
for j in range(3):
|
||||
point_id = cell.GetPointId(j)
|
||||
points.add(point_id)
|
||||
polygon.GetPointIds().SetNumberOfIds(len(points))
|
||||
for j, point_id in enumerate(points):
|
||||
polygon.GetPointIds().SetId(j, point_id)
|
||||
new_polygons.InsertNextCell(polygon)
|
||||
else:
|
||||
new_polygons.InsertNextCell(mesh.GetCell(group[0]))
|
||||
|
||||
# Create new polydata
|
||||
new_polydata = vtk.vtkPolyData()
|
||||
new_polydata.SetPoints(mesh.GetPoints())
|
||||
new_polydata.SetPolys(new_polygons)
|
||||
|
||||
return new_polydata
|
||||
|
||||
|
||||
def create_cube_mesh(self):
|
||||
# cube_source = vtk.vtkSuperquadricSource()
|
||||
|
||||
reader = vtk.vtkSTLReader()
|
||||
reader.SetFileName("case.stl") # Replace with your mesh file path
|
||||
reader.Update()
|
||||
|
||||
featureEdges = vtk.vtkFeatureEdges()
|
||||
featureEdges.SetInputConnection(reader.GetOutputPort())
|
||||
featureEdges.BoundaryEdgesOn()
|
||||
featureEdges.FeatureEdgesOn()
|
||||
featureEdges.ManifoldEdgesOff()
|
||||
featureEdges.NonManifoldEdgesOff()
|
||||
featureEdges.Update()
|
||||
|
||||
# print(cube_source)
|
||||
mapper = vtk.vtkPolyDataMapper()
|
||||
mapper.SetInputConnection(reader.GetOutputPort())
|
||||
actor = vtk.vtkActor()
|
||||
actor.SetMapper(mapper)
|
||||
self.renderer.AddActor(actor)
|
||||
|
||||
mapper_edge = vtk.vtkPolyDataMapper()
|
||||
mapper_edge.SetInputConnection(featureEdges.GetOutputPort())
|
||||
actor = vtk.vtkActor()
|
||||
actor.SetMapper(mapper_edge)
|
||||
self.renderer.AddActor(actor)
|
||||
|
||||
|
||||
def simplify_mesh(self, input_mesh, target_reduction):
|
||||
# Create the quadric decimation filter
|
||||
decimate = vtk.vtkDecimatePro()
|
||||
decimate.SetInputData(input_mesh)
|
||||
|
||||
# Set the reduction factor (0 to 1, where 1 means maximum reduction)
|
||||
decimate.SetTargetReduction(target_reduction)
|
||||
|
||||
# Optional: Preserve topology (if needed)
|
||||
decimate.PreserveTopologyOn()
|
||||
|
||||
# Perform the decimation
|
||||
decimate.Update()
|
||||
|
||||
return decimate.GetOutput()
|
||||
|
||||
|
||||
def combine_coplanar_faces(self, input_polydata, tolerance=0.001):
|
||||
# Clean the polydata to merge duplicate points
|
||||
clean = vtk.vtkCleanPolyData()
|
||||
clean.SetInputData(input_polydata)
|
||||
clean.SetTolerance(tolerance)
|
||||
clean.Update()
|
||||
|
||||
# Generate normals and merge coplanar polygons
|
||||
normals = vtk.vtkPolyDataNormals()
|
||||
normals.SetInputConnection(clean.GetOutputPort())
|
||||
normals.SplittingOff() # Disable splitting of sharp edges
|
||||
normals.ConsistencyOn() # Ensure consistent polygon ordering
|
||||
normals.AutoOrientNormalsOn() # Automatically orient normals
|
||||
normals.ComputePointNormalsOff() # We only need face normals
|
||||
normals.ComputeCellNormalsOn() # Compute cell normals
|
||||
normals.Update()
|
||||
|
||||
return normals.GetOutput()
|
||||
|
||||
|
||||
def poisson_reconstruction(self, points):
|
||||
# Create a polydata object from points
|
||||
point_polydata = vtk.vtkPolyData()
|
||||
point_polydata.SetPoints(points)
|
||||
|
||||
# Create a surface reconstruction filter
|
||||
surf = vtk.vtkSurfaceReconstructionFilter()
|
||||
surf.SetInputData(point_polydata)
|
||||
surf.Update()
|
||||
|
||||
# Create a contour filter to extract the surface
|
||||
cf = vtk.vtkContourFilter()
|
||||
cf.SetInputConnection(surf.GetOutputPort())
|
||||
cf.SetValue(0, 0.0)
|
||||
cf.Update()
|
||||
|
||||
# Reverse normals
|
||||
reverse = vtk.vtkReverseSense()
|
||||
reverse.SetInputConnection(cf.GetOutputPort())
|
||||
reverse.ReverseCellsOn()
|
||||
reverse.ReverseNormalsOn()
|
||||
reverse.Update()
|
||||
|
||||
return reverse.GetOutput()
|
||||
|
||||
|
||||
def create_simplified_outline(self, polydata):
|
||||
featureEdges = vtk.vtkFeatureEdges()
|
||||
featureEdges.SetInputData(polydata)
|
||||
featureEdges.BoundaryEdgesOn()
|
||||
featureEdges.FeatureEdgesOn()
|
||||
featureEdges.ManifoldEdgesOff()
|
||||
featureEdges.NonManifoldEdgesOff()
|
||||
featureEdges.Update()
|
||||
|
||||
"""# 3. Clean the edges to merge duplicate points
|
||||
cleaner = vtk.vtkCleanPolyData()
|
||||
cleaner.SetInputConnection(feature_edges.GetOutputPort())
|
||||
cleaner.Update()
|
||||
|
||||
# 4. Optional: Smooth the outline
|
||||
smooth = vtk.vtkSmoothPolyDataFilter()
|
||||
smooth.SetInputConnection(cleaner.GetOutputPort())
|
||||
smooth.SetNumberOfIterations(15)
|
||||
smooth.SetRelaxationFactor(0.1)
|
||||
smooth.FeatureEdgeSmoothingOff()
|
||||
smooth.BoundarySmoothingOn()
|
||||
smooth.Update()"""
|
||||
|
||||
return featureEdges
|
||||
|
||||
|
||||
def render_from_points_direct_with_faces(self, vertices, faces):
|
||||
points = vtk.vtkPoints()
|
||||
for i in range(vertices.shape[0]):
|
||||
points.InsertNextPoint(vertices[i])
|
||||
|
||||
# Create a vtkCellArray to store the triangles
|
||||
triangles = vtk.vtkCellArray()
|
||||
for i in range(faces.shape[0]):
|
||||
triangle = vtk.vtkTriangle()
|
||||
triangle.GetPointIds().SetId(0, faces[i, 0])
|
||||
triangle.GetPointIds().SetId(1, faces[i, 1])
|
||||
triangle.GetPointIds().SetId(2, faces[i, 2])
|
||||
triangles.InsertNextCell(triangle)
|
||||
|
||||
"""vtk_points = vtk.vtkPoints()
|
||||
for point in points:
|
||||
vtk_points.InsertNextPoint(point)
|
||||
|
||||
# Create a vtkCellArray to store the triangles
|
||||
triangles = vtk.vtkCellArray()
|
||||
|
||||
# Assuming points are organized as triplets forming triangles
|
||||
for i in range(0, len(points), 3):
|
||||
triangle = vtk.vtkTriangle()
|
||||
triangle.GetPointIds().SetId(0, i)
|
||||
triangle.GetPointIds().SetId(1, i + 1)
|
||||
triangle.GetPointIds().SetId(2, i + 2)
|
||||
triangles.InsertNextCell(triangle)"""
|
||||
|
||||
# Create a polydata object
|
||||
polydata = vtk.vtkPolyData()
|
||||
polydata.SetPoints(points)
|
||||
polydata.SetPolys(triangles)
|
||||
|
||||
# Calculate normals
|
||||
normalGenerator = vtk.vtkPolyDataNormals()
|
||||
normalGenerator.SetInputData(polydata)
|
||||
normalGenerator.ComputePointNormalsOn()
|
||||
normalGenerator.ComputeCellNormalsOn()
|
||||
normalGenerator.Update()
|
||||
|
||||
self.cell_normals = vtk_to_numpy(normalGenerator.GetOutput().GetCellData().GetNormals())
|
||||
|
||||
# merged_polydata = self.merge_coplanar_triangles(polydata)
|
||||
|
||||
# Create a mapper and actor
|
||||
mapper = vtk.vtkPolyDataMapper()
|
||||
mapper.SetInputData(polydata)
|
||||
|
||||
actor = vtk.vtkActor()
|
||||
actor.SetMapper(mapper)
|
||||
actor.GetProperty().SetColor(1, 1, 1) # Set color (white in this case)
|
||||
actor.GetProperty().EdgeVisibilityOn() # Show edges
|
||||
actor.GetProperty().SetLineWidth(2) # Set line width
|
||||
|
||||
feature_edges = self.create_simplified_outline(polydata)
|
||||
|
||||
# Create a mapper for the feature edges
|
||||
edge_mapper = vtk.vtkPolyDataMapper()
|
||||
# Already wiht output
|
||||
edge_mapper.SetInputConnection(feature_edges.GetOutputPort())
|
||||
|
||||
# Create an actor for the feature edges
|
||||
edge_actor = vtk.vtkActor()
|
||||
edge_actor.SetMapper(edge_mapper)
|
||||
|
||||
# Set the properties of the edge actor
|
||||
edge_actor.GetProperty().SetColor(1, 0, 0) # Set color (red in this case)
|
||||
edge_actor.GetProperty().SetLineWidth(2) # Set line width
|
||||
|
||||
# Optionally, if you want to keep the original mesh visible:
|
||||
# (assuming you have the original mesh mapper and actor set up)
|
||||
self.renderer.AddActor(actor) # Add the original mesh actor
|
||||
# Add the edge actor to the renderer
|
||||
self.renderer.AddActor(edge_actor)
|
||||
|
||||
# Force an update of the pipeline
|
||||
mapper.Update()
|
||||
self.vtk_widget.GetRenderWindow().Render()
|
||||
|
||||
"""# Print statistics
|
||||
print(f"Original points: {len(points)}")
|
||||
print(f"Number of triangles: {triangles.GetNumberOfCells()}")
|
||||
print(f"Final number of points: {normals.GetOutput().GetNumberOfPoints()}")
|
||||
print(f"Final number of cells: {normals.GetOutput().GetNumberOfCells()}")"""
|
||||
|
||||
|
||||
def render_from_points_direct(self, points):
|
||||
### Rendermethod for SDF mesh (output)
|
||||
# Create a vtkPoints object and store the points in it
|
||||
vtk_points = vtk.vtkPoints()
|
||||
for point in points:
|
||||
vtk_points.InsertNextPoint(point)
|
||||
|
||||
# Create a polydata object
|
||||
point_polydata = vtk.vtkPolyData()
|
||||
point_polydata.SetPoints(vtk_points)
|
||||
|
||||
# Surface reconstruction
|
||||
surf = vtk.vtkSurfaceReconstructionFilter()
|
||||
surf.SetInputData(point_polydata)
|
||||
surf.Update()
|
||||
|
||||
# Create a contour filter to extract the surface
|
||||
cf = vtk.vtkContourFilter()
|
||||
cf.SetInputConnection(surf.GetOutputPort())
|
||||
cf.SetValue(0, 0.0)
|
||||
cf.Update()
|
||||
|
||||
# Reverse the normals
|
||||
reverse = vtk.vtkReverseSense()
|
||||
reverse.SetInputConnection(cf.GetOutputPort())
|
||||
reverse.ReverseCellsOn()
|
||||
reverse.ReverseNormalsOn()
|
||||
reverse.Update()
|
||||
|
||||
# Get the reconstructed mesh
|
||||
reconstructed_mesh = reverse.GetOutput()
|
||||
|
||||
"""# Simplify the mesh
|
||||
target_reduction = 1 # Adjust this value as needed
|
||||
simplified_mesh = self.simplify_mesh(reconstructed_mesh, target_reduction)
|
||||
|
||||
combinded_faces = self.combine_coplanar_faces(simplified_mesh, 0.001)"""
|
||||
|
||||
# Create a mapper and actor for the simplified mesh
|
||||
mapper = vtk.vtkPolyDataMapper()
|
||||
mapper.SetInputData(reconstructed_mesh)
|
||||
|
||||
actor = vtk.vtkActor()
|
||||
actor.SetMapper(mapper)
|
||||
actor.GetProperty().SetColor(1, 1, 1) # Set color (white in this case)
|
||||
actor.GetProperty().EdgeVisibilityOn() # Show edges
|
||||
actor.GetProperty().SetLineWidth(2) # Set line width
|
||||
|
||||
# Add the actor to the renderer
|
||||
self.renderer.AddActor(actor)
|
||||
|
||||
# Force an update of the pipeline
|
||||
# mapper.Update()
|
||||
self.vtk_widget.GetRenderWindow().Render()
|
||||
|
||||
# Print statistics
|
||||
print(f"Original points: {len(points)}")
|
||||
print(
|
||||
f"Reconstructed mesh: {reconstructed_mesh.GetNumberOfPoints()} points, {reconstructed_mesh.GetNumberOfCells()} cells")
|
||||
"""print(
|
||||
f"Simplified mesh: {simplified_mesh.GetNumberOfPoints()} points, {simplified_mesh.GetNumberOfCells()} cells")"""
|
||||
@@ -1,111 +0,0 @@
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import pyvista as pv
|
||||
from pyvista.plotting.opts import ElementType
|
||||
from pyvistaqt import QtInteractor
|
||||
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget
|
||||
|
||||
|
||||
class PyVistaWidget(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
# Create the PyVista plotter
|
||||
self.plotter = QtInteractor(self)
|
||||
self.plotter.background_color = "darkgray"
|
||||
|
||||
# Create a layout and add the PyVista widget
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self.plotter.interactor)
|
||||
self.setLayout(layout)
|
||||
|
||||
# Set up the picker
|
||||
#self.plotter.enable_cell_picking(callback=self.on_cell_pick, show=True)
|
||||
self.plotter.enable_element_picking(callback=self.on_cell_pick, show=True, mode="face", left_clicking=True)
|
||||
|
||||
def on_cell_pick(self, element):
|
||||
if element is not None:
|
||||
mesh = self.plotter.mesh # Get the current mesh
|
||||
print(mesh)
|
||||
print(element)
|
||||
|
||||
"""# Get the face data
|
||||
face = mesh.extract_cells(element)
|
||||
|
||||
# Compute face normal
|
||||
face.compute_normals(cell_normals=True, inplace=True)
|
||||
normal = face.cell_data['Normals'][0]
|
||||
|
||||
# Get the points of the face
|
||||
points = face.points
|
||||
|
||||
print(f"Picked face ID: {face_id}")
|
||||
print(f"Face normal: {normal}")
|
||||
print("Face points:")
|
||||
for point in points:
|
||||
print(point)"""
|
||||
else:
|
||||
print("No face was picked or the picked element is not a face.")
|
||||
def create_simplified_outline(self, mesh, camera):
|
||||
# Project 3D to 2D
|
||||
points_2d = self.plotter.map_to_2d(mesh.points)
|
||||
|
||||
# Detect silhouette edges (simplified approach)
|
||||
edges = mesh.extract_feature_edges(feature_angle=90, boundary_edges=False, non_manifold_edges=False)
|
||||
|
||||
# Project edges to 2D
|
||||
edge_points_2d = self.plotter.map_to_2d(edges.points)
|
||||
|
||||
# Create 2D outline
|
||||
self.plotter.add_lines(edge_points_2d, color='black', width=2)
|
||||
self.plotter.render()
|
||||
|
||||
def mesh_from_points(self, points):
|
||||
# Convert points to numpy array if not already
|
||||
points = np.array(points)
|
||||
|
||||
# Create faces array
|
||||
num_triangles = len(points) // 3
|
||||
faces = np.arange(len(points)).reshape(num_triangles, 3)
|
||||
faces = np.column_stack((np.full(num_triangles, 3), faces)) # Add 3 as first column
|
||||
|
||||
# Create PyVista PolyData
|
||||
mesh = pv.PolyData(points, faces)
|
||||
|
||||
# Optional: Merge duplicate points
|
||||
mesh = mesh.clean()
|
||||
|
||||
# Optional: Compute normals
|
||||
mesh = mesh.compute_normals(point_normals=False, cell_normals=True, consistent_normals=True)
|
||||
edges = mesh.extract_feature_edges(30, non_manifold_edges=False)
|
||||
|
||||
# Clear any existing meshes
|
||||
self.plotter.clear()
|
||||
|
||||
# Add the mesh to the plotter
|
||||
self.plotter.add_mesh(mesh, pickable=True, color='white', show_edges=True, line_width=2, pbr=True, metallic=0.8, roughness=0.1, diffuse=1)
|
||||
self.plotter.add_mesh(edges, color="red", line_width=10)
|
||||
|
||||
# Reset the camera to fit the new mesh
|
||||
self.plotter.reset_camera()
|
||||
|
||||
# Update the render window
|
||||
self.plotter.update()
|
||||
|
||||
# Print statistics
|
||||
print(f"Original points: {len(points)}")
|
||||
print(f"Number of triangles: {num_triangles}")
|
||||
print(f"Final number of points: {mesh.n_points}")
|
||||
print(f"Final number of cells: {mesh.n_cells}")
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("PyVista in PySide6")
|
||||
self.setGeometry(100, 100, 800, 600)
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 319 KiB |
@@ -0,0 +1,948 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
################################################################################
|
||||
## Form generated from reading UI file 'gui.ui'
|
||||
##
|
||||
## Created by: Qt User Interface Compiler version 6.10.2
|
||||
##
|
||||
## WARNING! All changes made in this file will be lost when recompiling UI file!
|
||||
################################################################################
|
||||
|
||||
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
|
||||
QMetaObject, QObject, QPoint, QRect,
|
||||
QSize, QTime, QUrl, Qt)
|
||||
from PySide6.QtGui import (QAction, QBrush, QColor, QConicalGradient,
|
||||
QCursor, QFont, QFontDatabase, QGradient,
|
||||
QIcon, QImage, QKeySequence, QLinearGradient,
|
||||
QPainter, QPalette, QPixmap, QRadialGradient,
|
||||
QTransform)
|
||||
from PySide6.QtWidgets import (QApplication, QFrame, QGridLayout, QGroupBox,
|
||||
QHBoxLayout, QLabel, QListWidget, QListWidgetItem,
|
||||
QMainWindow, QMenu, QMenuBar, QPushButton,
|
||||
QSizePolicy, QSpinBox, QStatusBar, QTabWidget,
|
||||
QTextEdit, QVBoxLayout, QWidget)
|
||||
|
||||
class Ui_fluencyCAD(object):
|
||||
def setupUi(self, fluencyCAD):
|
||||
if not fluencyCAD.objectName():
|
||||
fluencyCAD.setObjectName(u"fluencyCAD")
|
||||
fluencyCAD.resize(2551, 1265)
|
||||
sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(fluencyCAD.sizePolicy().hasHeightForWidth())
|
||||
fluencyCAD.setSizePolicy(sizePolicy)
|
||||
self.actionNew_Project = QAction(fluencyCAD)
|
||||
self.actionNew_Project.setObjectName(u"actionNew_Project")
|
||||
self.actionOpen_Project = QAction(fluencyCAD)
|
||||
self.actionOpen_Project.setObjectName(u"actionOpen_Project")
|
||||
self.actionSave_Project = QAction(fluencyCAD)
|
||||
self.actionSave_Project.setObjectName(u"actionSave_Project")
|
||||
self.actionSave_Project_As = QAction(fluencyCAD)
|
||||
self.actionSave_Project_As.setObjectName(u"actionSave_Project_As")
|
||||
self.actionImport_File = QAction(fluencyCAD)
|
||||
self.actionImport_File.setObjectName(u"actionImport_File")
|
||||
self.actionExport_Step = QAction(fluencyCAD)
|
||||
self.actionExport_Step.setObjectName(u"actionExport_Step")
|
||||
self.actionExport_Iges = QAction(fluencyCAD)
|
||||
self.actionExport_Iges.setObjectName(u"actionExport_Iges")
|
||||
self.actionExport_Stl = QAction(fluencyCAD)
|
||||
self.actionExport_Stl.setObjectName(u"actionExport_Stl")
|
||||
self.actionExit = QAction(fluencyCAD)
|
||||
self.actionExit.setObjectName(u"actionExit")
|
||||
self.centralwidget = QWidget(fluencyCAD)
|
||||
self.centralwidget.setObjectName(u"centralwidget")
|
||||
self.gridLayout = QGridLayout(self.centralwidget)
|
||||
self.gridLayout.setObjectName(u"gridLayout")
|
||||
self.groupBox_5 = QGroupBox(self.centralwidget)
|
||||
self.groupBox_5.setObjectName(u"groupBox_5")
|
||||
sizePolicy.setHeightForWidth(self.groupBox_5.sizePolicy().hasHeightForWidth())
|
||||
self.groupBox_5.setSizePolicy(sizePolicy)
|
||||
self.gridLayout_11 = QGridLayout(self.groupBox_5)
|
||||
self.gridLayout_11.setObjectName(u"gridLayout_11")
|
||||
self.gridLayout_11.setContentsMargins(12, 12, 12, 12)
|
||||
self.label = QLabel(self.groupBox_5)
|
||||
self.label.setObjectName(u"label")
|
||||
|
||||
self.gridLayout_11.addWidget(self.label, 5, 0, 1, 1)
|
||||
|
||||
self.pb_snap_vert = QPushButton(self.groupBox_5)
|
||||
self.pb_snap_vert.setObjectName(u"pb_snap_vert")
|
||||
self.pb_snap_vert.setCheckable(True)
|
||||
self.pb_snap_vert.setAutoExclusive(False)
|
||||
|
||||
self.gridLayout_11.addWidget(self.pb_snap_vert, 2, 1, 1, 1)
|
||||
|
||||
self.line_2 = QFrame(self.groupBox_5)
|
||||
self.line_2.setObjectName(u"line_2")
|
||||
self.line_2.setFrameShape(QFrame.Shape.HLine)
|
||||
self.line_2.setFrameShadow(QFrame.Shadow.Sunken)
|
||||
|
||||
self.gridLayout_11.addWidget(self.line_2, 4, 0, 1, 2)
|
||||
|
||||
self.label_2 = QLabel(self.groupBox_5)
|
||||
self.label_2.setObjectName(u"label_2")
|
||||
|
||||
self.gridLayout_11.addWidget(self.label_2, 5, 1, 1, 1)
|
||||
|
||||
self.spinbox_snap_distance = QSpinBox(self.groupBox_5)
|
||||
self.spinbox_snap_distance.setObjectName(u"spinbox_snap_distance")
|
||||
self.spinbox_snap_distance.setMaximum(30)
|
||||
self.spinbox_snap_distance.setValue(10)
|
||||
|
||||
self.gridLayout_11.addWidget(self.spinbox_snap_distance, 6, 0, 1, 1)
|
||||
|
||||
self.pushButton_7 = QPushButton(self.groupBox_5)
|
||||
self.pushButton_7.setObjectName(u"pushButton_7")
|
||||
self.pushButton_7.setCheckable(True)
|
||||
self.pushButton_7.setAutoExclusive(False)
|
||||
|
||||
self.gridLayout_11.addWidget(self.pushButton_7, 3, 0, 1, 1)
|
||||
|
||||
self.pb_snap_horiz = QPushButton(self.groupBox_5)
|
||||
self.pb_snap_horiz.setObjectName(u"pb_snap_horiz")
|
||||
self.pb_snap_horiz.setCheckable(True)
|
||||
self.pb_snap_horiz.setAutoExclusive(False)
|
||||
|
||||
self.gridLayout_11.addWidget(self.pb_snap_horiz, 2, 0, 1, 1)
|
||||
|
||||
self.spinbox_angle_steps = QSpinBox(self.groupBox_5)
|
||||
self.spinbox_angle_steps.setObjectName(u"spinbox_angle_steps")
|
||||
self.spinbox_angle_steps.setMaximum(180)
|
||||
self.spinbox_angle_steps.setValue(15)
|
||||
|
||||
self.gridLayout_11.addWidget(self.spinbox_angle_steps, 6, 1, 1, 1)
|
||||
|
||||
self.pushButton_8 = QPushButton(self.groupBox_5)
|
||||
self.pushButton_8.setObjectName(u"pushButton_8")
|
||||
self.pushButton_8.setCheckable(True)
|
||||
self.pushButton_8.setAutoExclusive(False)
|
||||
|
||||
self.gridLayout_11.addWidget(self.pushButton_8, 0, 0, 1, 1)
|
||||
|
||||
self.pb_snap_midp = QPushButton(self.groupBox_5)
|
||||
self.pb_snap_midp.setObjectName(u"pb_snap_midp")
|
||||
self.pb_snap_midp.setCheckable(True)
|
||||
self.pb_snap_midp.setAutoExclusive(False)
|
||||
|
||||
self.gridLayout_11.addWidget(self.pb_snap_midp, 0, 1, 1, 1)
|
||||
|
||||
self.pb_snap_angle = QPushButton(self.groupBox_5)
|
||||
self.pb_snap_angle.setObjectName(u"pb_snap_angle")
|
||||
self.pb_snap_angle.setCheckable(True)
|
||||
self.pb_snap_angle.setAutoExclusive(False)
|
||||
|
||||
self.gridLayout_11.addWidget(self.pb_snap_angle, 3, 1, 1, 1)
|
||||
|
||||
|
||||
self.gridLayout.addWidget(self.groupBox_5, 3, 0, 1, 1)
|
||||
|
||||
self.groupBox_4 = QGroupBox(self.centralwidget)
|
||||
self.groupBox_4.setObjectName(u"groupBox_4")
|
||||
self.groupBox_4.setMaximumSize(QSize(200, 16777215))
|
||||
self.gridLayout_9 = QGridLayout(self.groupBox_4)
|
||||
self.gridLayout_9.setObjectName(u"gridLayout_9")
|
||||
self.pushButton_2 = QPushButton(self.groupBox_4)
|
||||
self.pushButton_2.setObjectName(u"pushButton_2")
|
||||
|
||||
self.gridLayout_9.addWidget(self.pushButton_2, 0, 0, 1, 1)
|
||||
|
||||
self.pb_export_iges = QPushButton(self.groupBox_4)
|
||||
self.pb_export_iges.setObjectName(u"pb_export_iges")
|
||||
|
||||
self.gridLayout_9.addWidget(self.pb_export_iges, 2, 0, 1, 1)
|
||||
|
||||
self.pb_export_step = QPushButton(self.groupBox_4)
|
||||
self.pb_export_step.setObjectName(u"pb_export_step")
|
||||
|
||||
self.gridLayout_9.addWidget(self.pb_export_step, 0, 1, 1, 1)
|
||||
|
||||
|
||||
self.gridLayout.addWidget(self.groupBox_4, 1, 3, 1, 1)
|
||||
|
||||
self.InputTab = QTabWidget(self.centralwidget)
|
||||
self.InputTab.setObjectName(u"InputTab")
|
||||
sizePolicy.setHeightForWidth(self.InputTab.sizePolicy().hasHeightForWidth())
|
||||
self.InputTab.setSizePolicy(sizePolicy)
|
||||
self.sketch_tab = QWidget()
|
||||
self.sketch_tab.setObjectName(u"sketch_tab")
|
||||
self.verticalLayout_4 = QVBoxLayout(self.sketch_tab)
|
||||
self.verticalLayout_4.setObjectName(u"verticalLayout_4")
|
||||
self.InputTab.addTab(self.sketch_tab, "")
|
||||
self.code_tab = QWidget()
|
||||
self.code_tab.setObjectName(u"code_tab")
|
||||
self.verticalLayout = QVBoxLayout(self.code_tab)
|
||||
self.verticalLayout.setObjectName(u"verticalLayout")
|
||||
self.textEdit = QTextEdit(self.code_tab)
|
||||
self.textEdit.setObjectName(u"textEdit")
|
||||
|
||||
self.verticalLayout.addWidget(self.textEdit)
|
||||
|
||||
self.groupBox_7 = QGroupBox(self.code_tab)
|
||||
self.groupBox_7.setObjectName(u"groupBox_7")
|
||||
self.gridLayout_5 = QGridLayout(self.groupBox_7)
|
||||
self.gridLayout_5.setObjectName(u"gridLayout_5")
|
||||
self.pushButton_5 = QPushButton(self.groupBox_7)
|
||||
self.pushButton_5.setObjectName(u"pushButton_5")
|
||||
|
||||
self.gridLayout_5.addWidget(self.pushButton_5, 2, 0, 1, 1)
|
||||
|
||||
self.pushButton_4 = QPushButton(self.groupBox_7)
|
||||
self.pushButton_4.setObjectName(u"pushButton_4")
|
||||
|
||||
self.gridLayout_5.addWidget(self.pushButton_4, 2, 1, 1, 1)
|
||||
|
||||
self.pb_apply_code = QPushButton(self.groupBox_7)
|
||||
self.pb_apply_code.setObjectName(u"pb_apply_code")
|
||||
|
||||
self.gridLayout_5.addWidget(self.pb_apply_code, 1, 0, 1, 1)
|
||||
|
||||
self.pushButton = QPushButton(self.groupBox_7)
|
||||
self.pushButton.setObjectName(u"pushButton")
|
||||
|
||||
self.gridLayout_5.addWidget(self.pushButton, 1, 1, 1, 1)
|
||||
|
||||
|
||||
self.verticalLayout.addWidget(self.groupBox_7)
|
||||
|
||||
self.InputTab.addTab(self.code_tab, "")
|
||||
|
||||
self.gridLayout.addWidget(self.InputTab, 0, 1, 5, 1)
|
||||
|
||||
self.compo_tool_box = QGroupBox(self.centralwidget)
|
||||
self.compo_tool_box.setObjectName(u"compo_tool_box")
|
||||
sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Preferred)
|
||||
sizePolicy1.setHorizontalStretch(0)
|
||||
sizePolicy1.setVerticalStretch(0)
|
||||
sizePolicy1.setHeightForWidth(self.compo_tool_box.sizePolicy().hasHeightForWidth())
|
||||
self.compo_tool_box.setSizePolicy(sizePolicy1)
|
||||
self.compo_tool_box.setMinimumSize(QSize(0, 50))
|
||||
self.horizontalLayout = QHBoxLayout(self.compo_tool_box)
|
||||
self.horizontalLayout.setObjectName(u"horizontalLayout")
|
||||
self.pb_new_compo = QPushButton(self.compo_tool_box)
|
||||
self.pb_new_compo.setObjectName(u"pb_new_compo")
|
||||
self.pb_new_compo.setMinimumSize(QSize(50, 50))
|
||||
self.pb_new_compo.setMaximumSize(QSize(50, 50))
|
||||
|
||||
self.horizontalLayout.addWidget(self.pb_new_compo)
|
||||
|
||||
self.pb_del_compo = QPushButton(self.compo_tool_box)
|
||||
self.pb_del_compo.setObjectName(u"pb_del_compo")
|
||||
self.pb_del_compo.setEnabled(True)
|
||||
sizePolicy.setHeightForWidth(self.pb_del_compo.sizePolicy().hasHeightForWidth())
|
||||
self.pb_del_compo.setSizePolicy(sizePolicy)
|
||||
self.pb_del_compo.setMinimumSize(QSize(50, 50))
|
||||
self.pb_del_compo.setMaximumSize(QSize(50, 50))
|
||||
self.pb_del_compo.setLayoutDirection(Qt.LeftToRight)
|
||||
|
||||
self.horizontalLayout.addWidget(self.pb_del_compo)
|
||||
|
||||
|
||||
self.gridLayout.addWidget(self.compo_tool_box, 7, 0, 1, 1)
|
||||
|
||||
self.groupBox_3 = QGroupBox(self.centralwidget)
|
||||
self.groupBox_3.setObjectName(u"groupBox_3")
|
||||
sizePolicy.setHeightForWidth(self.groupBox_3.sizePolicy().hasHeightForWidth())
|
||||
self.groupBox_3.setSizePolicy(sizePolicy)
|
||||
self.groupBox_3.setMaximumSize(QSize(200, 16777213))
|
||||
self.gridLayout_4 = QGridLayout(self.groupBox_3)
|
||||
self.gridLayout_4.setObjectName(u"gridLayout_4")
|
||||
self.pb_con_ptpt = QPushButton(self.groupBox_3)
|
||||
self.pb_con_ptpt.setObjectName(u"pb_con_ptpt")
|
||||
icon = QIcon()
|
||||
icon.addFile(u"icons/pt_pt.png", QSize(), QIcon.Mode.Normal, QIcon.State.Off)
|
||||
self.pb_con_ptpt.setIcon(icon)
|
||||
self.pb_con_ptpt.setCheckable(True)
|
||||
self.pb_con_ptpt.setAutoExclusive(False)
|
||||
|
||||
self.gridLayout_4.addWidget(self.pb_con_ptpt, 1, 0, 1, 1)
|
||||
|
||||
self.pb_con_vert = QPushButton(self.groupBox_3)
|
||||
self.pb_con_vert.setObjectName(u"pb_con_vert")
|
||||
self.pb_con_vert.setCheckable(True)
|
||||
self.pb_con_vert.setAutoExclusive(False)
|
||||
|
||||
self.gridLayout_4.addWidget(self.pb_con_vert, 3, 1, 1, 1)
|
||||
|
||||
self.pb_con_sym = QPushButton(self.groupBox_3)
|
||||
self.pb_con_sym.setObjectName(u"pb_con_sym")
|
||||
self.pb_con_sym.setCheckable(True)
|
||||
self.pb_con_sym.setAutoExclusive(False)
|
||||
|
||||
self.gridLayout_4.addWidget(self.pb_con_sym, 4, 1, 1, 1)
|
||||
|
||||
self.pb_con_mid = QPushButton(self.groupBox_3)
|
||||
self.pb_con_mid.setObjectName(u"pb_con_mid")
|
||||
self.pb_con_mid.setCheckable(True)
|
||||
self.pb_con_mid.setAutoExclusive(False)
|
||||
|
||||
self.gridLayout_4.addWidget(self.pb_con_mid, 2, 0, 1, 1)
|
||||
|
||||
self.pb_con_line = QPushButton(self.groupBox_3)
|
||||
self.pb_con_line.setObjectName(u"pb_con_line")
|
||||
self.pb_con_line.setCheckable(True)
|
||||
self.pb_con_line.setAutoExclusive(False)
|
||||
|
||||
self.gridLayout_4.addWidget(self.pb_con_line, 1, 1, 1, 1)
|
||||
|
||||
self.pb_con_horiz = QPushButton(self.groupBox_3)
|
||||
self.pb_con_horiz.setObjectName(u"pb_con_horiz")
|
||||
self.pb_con_horiz.setCheckable(True)
|
||||
self.pb_con_horiz.setAutoExclusive(False)
|
||||
|
||||
self.gridLayout_4.addWidget(self.pb_con_horiz, 3, 0, 1, 1)
|
||||
|
||||
self.pb_con_dist = QPushButton(self.groupBox_3)
|
||||
self.pb_con_dist.setObjectName(u"pb_con_dist")
|
||||
self.pb_con_dist.setCheckable(True)
|
||||
self.pb_con_dist.setAutoExclusive(False)
|
||||
self.pb_con_dist.setAutoRepeatDelay(297)
|
||||
|
||||
self.gridLayout_4.addWidget(self.pb_con_dist, 4, 0, 1, 1)
|
||||
|
||||
self.pb_con_perp = QPushButton(self.groupBox_3)
|
||||
self.pb_con_perp.setObjectName(u"pb_con_perp")
|
||||
self.pb_con_perp.setCheckable(True)
|
||||
self.pb_con_perp.setAutoExclusive(False)
|
||||
|
||||
self.gridLayout_4.addWidget(self.pb_con_perp, 2, 1, 1, 1)
|
||||
|
||||
self.pb_con_diameter = QPushButton(self.groupBox_3)
|
||||
self.pb_con_diameter.setObjectName(u"pb_con_diameter")
|
||||
|
||||
self.gridLayout_4.addWidget(self.pb_con_diameter, 5, 0, 1, 1)
|
||||
|
||||
|
||||
self.gridLayout.addWidget(self.groupBox_3, 2, 0, 1, 1)
|
||||
|
||||
self.assembly_tools = QGroupBox(self.centralwidget)
|
||||
self.assembly_tools.setObjectName(u"assembly_tools")
|
||||
sizePolicy1.setHeightForWidth(self.assembly_tools.sizePolicy().hasHeightForWidth())
|
||||
self.assembly_tools.setSizePolicy(sizePolicy1)
|
||||
self.assembly_tools.setMinimumSize(QSize(113, 50))
|
||||
self.horizontalLayout_2 = QHBoxLayout(self.assembly_tools)
|
||||
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
|
||||
self.pb_compo_to_assembly = QPushButton(self.assembly_tools)
|
||||
self.pb_compo_to_assembly.setObjectName(u"pb_compo_to_assembly")
|
||||
self.pb_compo_to_assembly.setMinimumSize(QSize(50, 50))
|
||||
self.pb_compo_to_assembly.setMaximumSize(QSize(50, 50))
|
||||
|
||||
self.horizontalLayout_2.addWidget(self.pb_compo_to_assembly)
|
||||
|
||||
self.pb_remove_compo_from_assembly = QPushButton(self.assembly_tools)
|
||||
self.pb_remove_compo_from_assembly.setObjectName(u"pb_remove_compo_from_assembly")
|
||||
self.pb_remove_compo_from_assembly.setEnabled(True)
|
||||
sizePolicy.setHeightForWidth(self.pb_remove_compo_from_assembly.sizePolicy().hasHeightForWidth())
|
||||
self.pb_remove_compo_from_assembly.setSizePolicy(sizePolicy)
|
||||
self.pb_remove_compo_from_assembly.setMinimumSize(QSize(50, 50))
|
||||
self.pb_remove_compo_from_assembly.setMaximumSize(QSize(50, 50))
|
||||
self.pb_remove_compo_from_assembly.setLayoutDirection(Qt.LeftToRight)
|
||||
|
||||
self.horizontalLayout_2.addWidget(self.pb_remove_compo_from_assembly)
|
||||
|
||||
|
||||
self.gridLayout.addWidget(self.assembly_tools, 8, 0, 1, 1)
|
||||
|
||||
self.groupBox_9 = QGroupBox(self.centralwidget)
|
||||
self.groupBox_9.setObjectName(u"groupBox_9")
|
||||
self.groupBox_9.setMaximumSize(QSize(200, 16777215))
|
||||
self.gridLayout_7 = QGridLayout(self.groupBox_9)
|
||||
self.gridLayout_7.setObjectName(u"gridLayout_7")
|
||||
self.pb_origin_wp = QPushButton(self.groupBox_9)
|
||||
self.pb_origin_wp.setObjectName(u"pb_origin_wp")
|
||||
|
||||
self.gridLayout_7.addWidget(self.pb_origin_wp, 0, 0, 1, 1)
|
||||
|
||||
self.pb_origin_face = QPushButton(self.groupBox_9)
|
||||
self.pb_origin_face.setObjectName(u"pb_origin_face")
|
||||
self.pb_origin_face.setCheckable(True)
|
||||
|
||||
self.gridLayout_7.addWidget(self.pb_origin_face, 0, 1, 1, 1)
|
||||
|
||||
self.pb_flip_face = QPushButton(self.groupBox_9)
|
||||
self.pb_flip_face.setObjectName(u"pb_flip_face")
|
||||
|
||||
self.gridLayout_7.addWidget(self.pb_flip_face, 1, 0, 1, 1)
|
||||
|
||||
self.pb_underlay = QPushButton(self.groupBox_9)
|
||||
self.pb_underlay.setObjectName(u"pb_underlay")
|
||||
self.pb_underlay.setEnabled(False)
|
||||
self.pb_underlay.setCheckable(True)
|
||||
self.pb_underlay.setChecked(True)
|
||||
|
||||
self.gridLayout_7.addWidget(self.pb_underlay, 3, 0, 1, 1)
|
||||
|
||||
self.pb_clr_face = QPushButton(self.groupBox_9)
|
||||
self.pb_clr_face.setObjectName(u"pb_clr_face")
|
||||
self.pb_clr_face.setEnabled(False)
|
||||
|
||||
self.gridLayout_7.addWidget(self.pb_clr_face, 3, 1, 1, 1)
|
||||
|
||||
self.pb_to_sketch = QPushButton(self.groupBox_9)
|
||||
self.pb_to_sketch.setObjectName(u"pb_to_sketch")
|
||||
self.pb_to_sketch.setEnabled(False)
|
||||
|
||||
self.gridLayout_7.addWidget(self.pb_to_sketch, 4, 0, 1, 2)
|
||||
|
||||
self.pb_wp_new = QPushButton(self.groupBox_9)
|
||||
self.pb_wp_new.setObjectName(u"pb_wp_new")
|
||||
|
||||
self.gridLayout_7.addWidget(self.pb_wp_new, 1, 1, 1, 1)
|
||||
|
||||
|
||||
self.gridLayout.addWidget(self.groupBox_9, 0, 0, 1, 1)
|
||||
|
||||
self.groupBox_12 = QGroupBox(self.centralwidget)
|
||||
self.groupBox_12.setObjectName(u"groupBox_12")
|
||||
sizePolicy2 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding)
|
||||
sizePolicy2.setHorizontalStretch(0)
|
||||
sizePolicy2.setVerticalStretch(0)
|
||||
sizePolicy2.setHeightForWidth(self.groupBox_12.sizePolicy().hasHeightForWidth())
|
||||
self.groupBox_12.setSizePolicy(sizePolicy2)
|
||||
self.groupBox_12.setMaximumSize(QSize(200, 16777215))
|
||||
self.verticalLayout_8 = QVBoxLayout(self.groupBox_12)
|
||||
self.verticalLayout_8.setObjectName(u"verticalLayout_8")
|
||||
self.verticalLayout_8.setContentsMargins(5, 5, 5, 5)
|
||||
self.connection_list = QListWidget(self.groupBox_12)
|
||||
self.connection_list.setObjectName(u"connection_list")
|
||||
self.connection_list.setSelectionRectVisible(True)
|
||||
|
||||
self.verticalLayout_8.addWidget(self.connection_list)
|
||||
|
||||
self.groupBox_13 = QGroupBox(self.groupBox_12)
|
||||
self.groupBox_13.setObjectName(u"groupBox_13")
|
||||
sizePolicy.setHeightForWidth(self.groupBox_13.sizePolicy().hasHeightForWidth())
|
||||
self.groupBox_13.setSizePolicy(sizePolicy)
|
||||
self.groupBox_13.setMaximumSize(QSize(200, 16777215))
|
||||
self.gridLayout_13 = QGridLayout(self.groupBox_13)
|
||||
self.gridLayout_13.setObjectName(u"gridLayout_13")
|
||||
self.gridLayout_13.setContentsMargins(2, 2, 2, 2)
|
||||
self.pb_del_connection = QPushButton(self.groupBox_13)
|
||||
self.pb_del_connection.setObjectName(u"pb_del_connection")
|
||||
|
||||
self.gridLayout_13.addWidget(self.pb_del_connection, 0, 2, 1, 1)
|
||||
|
||||
self.pb_update_connection = QPushButton(self.groupBox_13)
|
||||
self.pb_update_connection.setObjectName(u"pb_update_connection")
|
||||
|
||||
self.gridLayout_13.addWidget(self.pb_update_connection, 0, 0, 1, 1)
|
||||
|
||||
self.pb_edt_sktch_4 = QPushButton(self.groupBox_13)
|
||||
self.pb_edt_sktch_4.setObjectName(u"pb_edt_sktch_4")
|
||||
|
||||
self.gridLayout_13.addWidget(self.pb_edt_sktch_4, 0, 1, 1, 1)
|
||||
|
||||
|
||||
self.verticalLayout_8.addWidget(self.groupBox_13)
|
||||
|
||||
|
||||
self.gridLayout.addWidget(self.groupBox_12, 4, 3, 1, 1)
|
||||
|
||||
self.joint_tools = QGroupBox(self.centralwidget)
|
||||
self.joint_tools.setObjectName(u"joint_tools")
|
||||
self.joint_tools.setMinimumSize(QSize(0, 50))
|
||||
self.gridLayout_10 = QGridLayout(self.joint_tools)
|
||||
self.gridLayout_10.setObjectName(u"gridLayout_10")
|
||||
self.pb_add_connector = QPushButton(self.joint_tools)
|
||||
self.pb_add_connector.setObjectName(u"pb_add_connector")
|
||||
self.pb_add_connector.setMinimumSize(QSize(50, 50))
|
||||
self.pb_add_connector.setMaximumSize(QSize(50, 50))
|
||||
|
||||
self.gridLayout_10.addWidget(self.pb_add_connector, 0, 0, 1, 1)
|
||||
|
||||
self.pb_remove_connector = QPushButton(self.joint_tools)
|
||||
self.pb_remove_connector.setObjectName(u"pb_remove_connector")
|
||||
self.pb_remove_connector.setMinimumSize(QSize(50, 50))
|
||||
self.pb_remove_connector.setMaximumSize(QSize(50, 50))
|
||||
|
||||
self.gridLayout_10.addWidget(self.pb_remove_connector, 0, 1, 1, 1)
|
||||
|
||||
self.pb_add_connector_3 = QPushButton(self.joint_tools)
|
||||
self.pb_add_connector_3.setObjectName(u"pb_add_connector_3")
|
||||
self.pb_add_connector_3.setMinimumSize(QSize(50, 50))
|
||||
self.pb_add_connector_3.setMaximumSize(QSize(50, 50))
|
||||
|
||||
self.gridLayout_10.addWidget(self.pb_add_connector_3, 1, 1, 1, 1)
|
||||
|
||||
self.pb_add_connector_2 = QPushButton(self.joint_tools)
|
||||
self.pb_add_connector_2.setObjectName(u"pb_add_connector_2")
|
||||
self.pb_add_connector_2.setMinimumSize(QSize(50, 50))
|
||||
self.pb_add_connector_2.setMaximumSize(QSize(50, 50))
|
||||
|
||||
self.gridLayout_10.addWidget(self.pb_add_connector_2, 1, 0, 1, 1)
|
||||
|
||||
|
||||
self.gridLayout.addWidget(self.joint_tools, 7, 3, 2, 1)
|
||||
|
||||
self.gl_box = QGroupBox(self.centralwidget)
|
||||
self.gl_box.setObjectName(u"gl_box")
|
||||
sizePolicy3 = QSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)
|
||||
sizePolicy3.setHorizontalStretch(0)
|
||||
sizePolicy3.setVerticalStretch(4)
|
||||
sizePolicy3.setHeightForWidth(self.gl_box.sizePolicy().hasHeightForWidth())
|
||||
self.gl_box.setSizePolicy(sizePolicy3)
|
||||
font = QFont()
|
||||
font.setPointSize(12)
|
||||
self.gl_box.setFont(font)
|
||||
self.horizontalLayout_4 = QHBoxLayout(self.gl_box)
|
||||
#ifndef Q_OS_MAC
|
||||
self.horizontalLayout_4.setSpacing(-1)
|
||||
#endif
|
||||
self.horizontalLayout_4.setObjectName(u"horizontalLayout_4")
|
||||
self.horizontalLayout_4.setContentsMargins(12, -1, -1, -1)
|
||||
|
||||
self.gridLayout.addWidget(self.gl_box, 0, 2, 5, 1)
|
||||
|
||||
self.compo_box = QGroupBox(self.centralwidget)
|
||||
self.compo_box.setObjectName(u"compo_box")
|
||||
self.compo_box.setMinimumSize(QSize(0, 120))
|
||||
|
||||
self.gridLayout.addWidget(self.compo_box, 7, 1, 1, 2)
|
||||
|
||||
self.groupBox_11 = QGroupBox(self.centralwidget)
|
||||
self.groupBox_11.setObjectName(u"groupBox_11")
|
||||
sizePolicy2.setHeightForWidth(self.groupBox_11.sizePolicy().hasHeightForWidth())
|
||||
self.groupBox_11.setSizePolicy(sizePolicy2)
|
||||
self.groupBox_11.setMaximumSize(QSize(200, 16777215))
|
||||
self.verticalLayout_7 = QVBoxLayout(self.groupBox_11)
|
||||
self.verticalLayout_7.setObjectName(u"verticalLayout_7")
|
||||
self.verticalLayout_7.setContentsMargins(5, 5, 5, 5)
|
||||
self.sketch_list = QListWidget(self.groupBox_11)
|
||||
self.sketch_list.setObjectName(u"sketch_list")
|
||||
sizePolicy4 = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
||||
sizePolicy4.setHorizontalStretch(0)
|
||||
sizePolicy4.setVerticalStretch(0)
|
||||
sizePolicy4.setHeightForWidth(self.sketch_list.sizePolicy().hasHeightForWidth())
|
||||
self.sketch_list.setSizePolicy(sizePolicy4)
|
||||
self.sketch_list.setSelectionRectVisible(True)
|
||||
|
||||
self.verticalLayout_7.addWidget(self.sketch_list)
|
||||
|
||||
self.groupBox_6 = QGroupBox(self.groupBox_11)
|
||||
self.groupBox_6.setObjectName(u"groupBox_6")
|
||||
sizePolicy.setHeightForWidth(self.groupBox_6.sizePolicy().hasHeightForWidth())
|
||||
self.groupBox_6.setSizePolicy(sizePolicy)
|
||||
self.gridLayout_6 = QGridLayout(self.groupBox_6)
|
||||
self.gridLayout_6.setObjectName(u"gridLayout_6")
|
||||
self.gridLayout_6.setContentsMargins(2, 2, 2, 2)
|
||||
self.pb_edt_sktch = QPushButton(self.groupBox_6)
|
||||
self.pb_edt_sktch.setObjectName(u"pb_edt_sktch")
|
||||
|
||||
self.gridLayout_6.addWidget(self.pb_edt_sktch, 1, 1, 1, 1)
|
||||
|
||||
self.pb_nw_sktch = QPushButton(self.groupBox_6)
|
||||
self.pb_nw_sktch.setObjectName(u"pb_nw_sktch")
|
||||
|
||||
self.gridLayout_6.addWidget(self.pb_nw_sktch, 1, 0, 1, 1)
|
||||
|
||||
self.pb_del_sketch = QPushButton(self.groupBox_6)
|
||||
self.pb_del_sketch.setObjectName(u"pb_del_sketch")
|
||||
|
||||
self.gridLayout_6.addWidget(self.pb_del_sketch, 1, 2, 1, 1)
|
||||
|
||||
|
||||
self.verticalLayout_7.addWidget(self.groupBox_6)
|
||||
|
||||
|
||||
self.gridLayout.addWidget(self.groupBox_11, 4, 0, 1, 1)
|
||||
|
||||
self.groupBox_2 = QGroupBox(self.centralwidget)
|
||||
self.groupBox_2.setObjectName(u"groupBox_2")
|
||||
sizePolicy.setHeightForWidth(self.groupBox_2.sizePolicy().hasHeightForWidth())
|
||||
self.groupBox_2.setSizePolicy(sizePolicy)
|
||||
self.groupBox_2.setMaximumSize(QSize(200, 16777215))
|
||||
self.gridLayout_2 = QGridLayout(self.groupBox_2)
|
||||
self.gridLayout_2.setObjectName(u"gridLayout_2")
|
||||
self.gridLayout_2.setContentsMargins(10, -1, -1, -1)
|
||||
self.pb_arc_tool = QPushButton(self.groupBox_2)
|
||||
self.pb_arc_tool.setObjectName(u"pb_arc_tool")
|
||||
self.pb_arc_tool.setCheckable(True)
|
||||
|
||||
self.gridLayout_2.addWidget(self.pb_arc_tool, 2, 0, 1, 1)
|
||||
|
||||
self.pb_rectool = QPushButton(self.groupBox_2)
|
||||
self.pb_rectool.setObjectName(u"pb_rectool")
|
||||
self.pb_rectool.setCheckable(True)
|
||||
self.pb_rectool.setAutoExclusive(False)
|
||||
|
||||
self.gridLayout_2.addWidget(self.pb_rectool, 0, 1, 1, 1)
|
||||
|
||||
self.pb_circtool = QPushButton(self.groupBox_2)
|
||||
self.pb_circtool.setObjectName(u"pb_circtool")
|
||||
self.pb_circtool.setCheckable(True)
|
||||
self.pb_circtool.setAutoExclusive(False)
|
||||
|
||||
self.gridLayout_2.addWidget(self.pb_circtool, 1, 0, 1, 1)
|
||||
|
||||
self.pb_enable_construct = QPushButton(self.groupBox_2)
|
||||
self.pb_enable_construct.setObjectName(u"pb_enable_construct")
|
||||
self.pb_enable_construct.setCheckable(True)
|
||||
|
||||
self.gridLayout_2.addWidget(self.pb_enable_construct, 4, 0, 1, 1)
|
||||
|
||||
self.pb_enable_snap = QPushButton(self.groupBox_2)
|
||||
self.pb_enable_snap.setObjectName(u"pb_enable_snap")
|
||||
self.pb_enable_snap.setIconSize(QSize(13, 16))
|
||||
self.pb_enable_snap.setCheckable(True)
|
||||
self.pb_enable_snap.setChecked(True)
|
||||
|
||||
self.gridLayout_2.addWidget(self.pb_enable_snap, 4, 1, 1, 1)
|
||||
|
||||
self.pb_linetool = QPushButton(self.groupBox_2)
|
||||
self.pb_linetool.setObjectName(u"pb_linetool")
|
||||
self.pb_linetool.setCheckable(True)
|
||||
self.pb_linetool.setAutoExclusive(False)
|
||||
|
||||
self.gridLayout_2.addWidget(self.pb_linetool, 0, 0, 1, 1)
|
||||
|
||||
self.pb_slotool = QPushButton(self.groupBox_2)
|
||||
self.pb_slotool.setObjectName(u"pb_slotool")
|
||||
self.pb_slotool.setCheckable(True)
|
||||
self.pb_slotool.setAutoExclusive(False)
|
||||
|
||||
self.gridLayout_2.addWidget(self.pb_slotool, 1, 1, 1, 1)
|
||||
|
||||
self.line = QFrame(self.groupBox_2)
|
||||
self.line.setObjectName(u"line")
|
||||
self.line.setFrameShape(QFrame.Shape.HLine)
|
||||
self.line.setFrameShadow(QFrame.Shadow.Sunken)
|
||||
|
||||
self.gridLayout_2.addWidget(self.line, 3, 0, 1, 2)
|
||||
|
||||
self.pb_offset_tool = QPushButton(self.groupBox_2)
|
||||
self.pb_offset_tool.setObjectName(u"pb_offset_tool")
|
||||
|
||||
self.gridLayout_2.addWidget(self.pb_offset_tool, 2, 1, 1, 1)
|
||||
|
||||
|
||||
self.gridLayout.addWidget(self.groupBox_2, 1, 0, 1, 1)
|
||||
|
||||
self.assembly_box = QGroupBox(self.centralwidget)
|
||||
self.assembly_box.setObjectName(u"assembly_box")
|
||||
self.assembly_box.setMinimumSize(QSize(0, 120))
|
||||
|
||||
self.gridLayout.addWidget(self.assembly_box, 8, 1, 1, 2)
|
||||
|
||||
self.groupBox = QGroupBox(self.centralwidget)
|
||||
self.groupBox.setObjectName(u"groupBox")
|
||||
self.groupBox.setMaximumSize(QSize(200, 16777215))
|
||||
self.gridLayout_3 = QGridLayout(self.groupBox)
|
||||
self.gridLayout_3.setObjectName(u"gridLayout_3")
|
||||
self.pb_chamfer_op = QPushButton(self.groupBox)
|
||||
self.pb_chamfer_op.setObjectName(u"pb_chamfer_op")
|
||||
|
||||
self.gridLayout_3.addWidget(self.pb_chamfer_op, 2, 1, 1, 1)
|
||||
|
||||
self.pb_fillet_op = QPushButton(self.groupBox)
|
||||
self.pb_fillet_op.setObjectName(u"pb_fillet_op")
|
||||
|
||||
self.gridLayout_3.addWidget(self.pb_fillet_op, 2, 0, 1, 1)
|
||||
|
||||
self.pb_thread = QPushButton(self.groupBox)
|
||||
self.pb_thread.setObjectName(u"pb_thread")
|
||||
|
||||
self.gridLayout_3.addWidget(self.pb_thread, 6, 0, 1, 1)
|
||||
|
||||
self.pb_extrdop = QPushButton(self.groupBox)
|
||||
self.pb_extrdop.setObjectName(u"pb_extrdop")
|
||||
|
||||
self.gridLayout_3.addWidget(self.pb_extrdop, 0, 0, 1, 1)
|
||||
|
||||
self.pb_revop = QPushButton(self.groupBox)
|
||||
self.pb_revop.setObjectName(u"pb_revop")
|
||||
|
||||
self.gridLayout_3.addWidget(self.pb_revop, 4, 1, 1, 1)
|
||||
|
||||
self.pb_cutop = QPushButton(self.groupBox)
|
||||
self.pb_cutop.setObjectName(u"pb_cutop")
|
||||
|
||||
self.gridLayout_3.addWidget(self.pb_cutop, 0, 1, 1, 1)
|
||||
|
||||
self.pb_arrayop = QPushButton(self.groupBox)
|
||||
self.pb_arrayop.setObjectName(u"pb_arrayop")
|
||||
|
||||
self.gridLayout_3.addWidget(self.pb_arrayop, 4, 0, 1, 1)
|
||||
|
||||
self.pb_combop = QPushButton(self.groupBox)
|
||||
self.pb_combop.setObjectName(u"pb_combop")
|
||||
|
||||
self.gridLayout_3.addWidget(self.pb_combop, 1, 0, 1, 1)
|
||||
|
||||
self.pb_moveop = QPushButton(self.groupBox)
|
||||
self.pb_moveop.setObjectName(u"pb_moveop")
|
||||
|
||||
self.gridLayout_3.addWidget(self.pb_moveop, 1, 1, 1, 1)
|
||||
|
||||
self.pb_mirror_op = QPushButton(self.groupBox)
|
||||
self.pb_mirror_op.setObjectName(u"pb_mirror_op")
|
||||
|
||||
self.gridLayout_3.addWidget(self.pb_mirror_op, 6, 1, 1, 1)
|
||||
|
||||
|
||||
self.gridLayout.addWidget(self.groupBox, 0, 3, 1, 1)
|
||||
|
||||
self.line_3 = QFrame(self.centralwidget)
|
||||
self.line_3.setObjectName(u"line_3")
|
||||
self.line_3.setFrameShape(QFrame.Shape.HLine)
|
||||
self.line_3.setFrameShadow(QFrame.Shadow.Sunken)
|
||||
|
||||
self.gridLayout.addWidget(self.line_3, 5, 0, 1, 4)
|
||||
|
||||
self.groupBox_10 = QGroupBox(self.centralwidget)
|
||||
self.groupBox_10.setObjectName(u"groupBox_10")
|
||||
sizePolicy2.setHeightForWidth(self.groupBox_10.sizePolicy().hasHeightForWidth())
|
||||
self.groupBox_10.setSizePolicy(sizePolicy2)
|
||||
self.groupBox_10.setMaximumSize(QSize(200, 16777215))
|
||||
self.verticalLayout_6 = QVBoxLayout(self.groupBox_10)
|
||||
self.verticalLayout_6.setObjectName(u"verticalLayout_6")
|
||||
self.verticalLayout_6.setContentsMargins(5, 5, 5, 5)
|
||||
self.body_list = QListWidget(self.groupBox_10)
|
||||
self.body_list.setObjectName(u"body_list")
|
||||
self.body_list.setSelectionRectVisible(True)
|
||||
|
||||
self.verticalLayout_6.addWidget(self.body_list)
|
||||
|
||||
self.groupBox_8 = QGroupBox(self.groupBox_10)
|
||||
self.groupBox_8.setObjectName(u"groupBox_8")
|
||||
sizePolicy.setHeightForWidth(self.groupBox_8.sizePolicy().hasHeightForWidth())
|
||||
self.groupBox_8.setSizePolicy(sizePolicy)
|
||||
self.groupBox_8.setMaximumSize(QSize(200, 16777215))
|
||||
self.gridLayout_8 = QGridLayout(self.groupBox_8)
|
||||
self.gridLayout_8.setObjectName(u"gridLayout_8")
|
||||
self.gridLayout_8.setContentsMargins(2, 2, 2, 2)
|
||||
self.pb_body_hide = QPushButton(self.groupBox_8)
|
||||
self.pb_body_hide.setObjectName(u"pb_body_hide")
|
||||
|
||||
self.gridLayout_8.addWidget(self.pb_body_hide, 0, 1, 1, 1)
|
||||
|
||||
self.pb_update_body = QPushButton(self.groupBox_8)
|
||||
self.pb_update_body.setObjectName(u"pb_update_body")
|
||||
|
||||
self.gridLayout_8.addWidget(self.pb_update_body, 0, 0, 1, 1)
|
||||
|
||||
self.pb_del_body = QPushButton(self.groupBox_8)
|
||||
self.pb_del_body.setObjectName(u"pb_del_body")
|
||||
|
||||
self.gridLayout_8.addWidget(self.pb_del_body, 0, 2, 1, 1)
|
||||
|
||||
|
||||
self.verticalLayout_6.addWidget(self.groupBox_8)
|
||||
|
||||
|
||||
self.gridLayout.addWidget(self.groupBox_10, 2, 3, 2, 1)
|
||||
|
||||
fluencyCAD.setCentralWidget(self.centralwidget)
|
||||
self.menubar = QMenuBar(fluencyCAD)
|
||||
self.menubar.setObjectName(u"menubar")
|
||||
self.menubar.setGeometry(QRect(0, 0, 2551, 24))
|
||||
self.menuFile = QMenu(self.menubar)
|
||||
self.menuFile.setObjectName(u"menuFile")
|
||||
self.menuSettings = QMenu(self.menubar)
|
||||
self.menuSettings.setObjectName(u"menuSettings")
|
||||
fluencyCAD.setMenuBar(self.menubar)
|
||||
self.statusbar = QStatusBar(fluencyCAD)
|
||||
self.statusbar.setObjectName(u"statusbar")
|
||||
fluencyCAD.setStatusBar(self.statusbar)
|
||||
|
||||
self.menubar.addAction(self.menuFile.menuAction())
|
||||
self.menubar.addAction(self.menuSettings.menuAction())
|
||||
self.menuFile.addAction(self.actionNew_Project)
|
||||
self.menuFile.addAction(self.actionOpen_Project)
|
||||
self.menuFile.addAction(self.actionSave_Project)
|
||||
self.menuFile.addAction(self.actionSave_Project_As)
|
||||
self.menuFile.addSeparator()
|
||||
self.menuFile.addAction(self.actionImport_File)
|
||||
self.menuFile.addSeparator()
|
||||
self.menuFile.addAction(self.actionExport_Step)
|
||||
self.menuFile.addAction(self.actionExport_Iges)
|
||||
self.menuFile.addAction(self.actionExport_Stl)
|
||||
self.menuFile.addSeparator()
|
||||
self.menuFile.addAction(self.actionExit)
|
||||
|
||||
self.retranslateUi(fluencyCAD)
|
||||
|
||||
self.InputTab.setCurrentIndex(0)
|
||||
|
||||
|
||||
QMetaObject.connectSlotsByName(fluencyCAD)
|
||||
# setupUi
|
||||
|
||||
def retranslateUi(self, fluencyCAD):
|
||||
fluencyCAD.setWindowTitle(QCoreApplication.translate("fluencyCAD", u"fluencyCAD", None))
|
||||
self.actionNew_Project.setText(QCoreApplication.translate("fluencyCAD", u"New Project", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionNew_Project.setShortcut(QCoreApplication.translate("fluencyCAD", u"Ctrl+N", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionOpen_Project.setText(QCoreApplication.translate("fluencyCAD", u"Open Project...", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionOpen_Project.setShortcut(QCoreApplication.translate("fluencyCAD", u"Ctrl+O", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionSave_Project.setText(QCoreApplication.translate("fluencyCAD", u"Save Project", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionSave_Project.setShortcut(QCoreApplication.translate("fluencyCAD", u"Ctrl+S", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionSave_Project_As.setText(QCoreApplication.translate("fluencyCAD", u"Save Project As...", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionSave_Project_As.setShortcut(QCoreApplication.translate("fluencyCAD", u"Ctrl+Shift+S", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.actionImport_File.setText(QCoreApplication.translate("fluencyCAD", u"Import STEP/IGES...", None))
|
||||
self.actionExport_Step.setText(QCoreApplication.translate("fluencyCAD", u"Export STEP...", None))
|
||||
self.actionExport_Iges.setText(QCoreApplication.translate("fluencyCAD", u"Export IGES...", None))
|
||||
self.actionExport_Stl.setText(QCoreApplication.translate("fluencyCAD", u"Export STL...", None))
|
||||
self.actionExit.setText(QCoreApplication.translate("fluencyCAD", u"Exit", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.actionExit.setShortcut(QCoreApplication.translate("fluencyCAD", u"Ctrl+Q", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.groupBox_5.setTitle(QCoreApplication.translate("fluencyCAD", u"Snapping Points", None))
|
||||
self.label.setText(QCoreApplication.translate("fluencyCAD", u"Snp Dst", None))
|
||||
self.pb_snap_vert.setText(QCoreApplication.translate("fluencyCAD", u"Vert", None))
|
||||
self.label_2.setText(QCoreApplication.translate("fluencyCAD", u"Angl Stps", None))
|
||||
self.spinbox_snap_distance.setSuffix(QCoreApplication.translate("fluencyCAD", u"mm", None))
|
||||
self.pushButton_7.setText(QCoreApplication.translate("fluencyCAD", u"Grid", None))
|
||||
self.pb_snap_horiz.setText(QCoreApplication.translate("fluencyCAD", u"Horiz", None))
|
||||
self.spinbox_angle_steps.setSuffix(QCoreApplication.translate("fluencyCAD", u"\u00b0", None))
|
||||
self.pushButton_8.setText(QCoreApplication.translate("fluencyCAD", u"Pnt", None))
|
||||
self.pb_snap_midp.setText(QCoreApplication.translate("fluencyCAD", u"MidP", None))
|
||||
self.pb_snap_angle.setText(QCoreApplication.translate("fluencyCAD", u"Angles", None))
|
||||
self.groupBox_4.setTitle(QCoreApplication.translate("fluencyCAD", u"Export", None))
|
||||
self.pushButton_2.setText(QCoreApplication.translate("fluencyCAD", u"STL", None))
|
||||
self.pb_export_iges.setText(QCoreApplication.translate("fluencyCAD", u"IGES", None))
|
||||
self.pb_export_step.setText(QCoreApplication.translate("fluencyCAD", u"STEP", None))
|
||||
self.InputTab.setTabText(self.InputTab.indexOf(self.sketch_tab), QCoreApplication.translate("fluencyCAD", u"Sketch", None))
|
||||
self.groupBox_7.setTitle(QCoreApplication.translate("fluencyCAD", u"Executive", None))
|
||||
self.pushButton_5.setText(QCoreApplication.translate("fluencyCAD", u"Load Code", None))
|
||||
self.pushButton_4.setText(QCoreApplication.translate("fluencyCAD", u"Save code", None))
|
||||
self.pb_apply_code.setText(QCoreApplication.translate("fluencyCAD", u"Apply Code", None))
|
||||
self.pushButton.setText(QCoreApplication.translate("fluencyCAD", u"Delete Code", None))
|
||||
self.InputTab.setTabText(self.InputTab.indexOf(self.code_tab), QCoreApplication.translate("fluencyCAD", u"Code", None))
|
||||
self.compo_tool_box.setTitle(QCoreApplication.translate("fluencyCAD", u"Component Tools", None))
|
||||
self.pb_new_compo.setText(QCoreApplication.translate("fluencyCAD", u"New", None))
|
||||
self.pb_del_compo.setText(QCoreApplication.translate("fluencyCAD", u"Del", None))
|
||||
self.groupBox_3.setTitle(QCoreApplication.translate("fluencyCAD", u"Constrain", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.pb_con_ptpt.setToolTip(QCoreApplication.translate("fluencyCAD", u"Poin to Point Constrain", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pb_con_ptpt.setText(QCoreApplication.translate("fluencyCAD", u"Pt_Pt", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.pb_con_vert.setToolTip(QCoreApplication.translate("fluencyCAD", u"Vertical Constrain", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pb_con_vert.setText(QCoreApplication.translate("fluencyCAD", u"Vert", None))
|
||||
self.pb_con_sym.setText(QCoreApplication.translate("fluencyCAD", u"Symetrc", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.pb_con_mid.setToolTip(QCoreApplication.translate("fluencyCAD", u"Point to Middle Point Constrain", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pb_con_mid.setText(QCoreApplication.translate("fluencyCAD", u"Pt_Mid_L", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.pb_con_line.setToolTip(QCoreApplication.translate("fluencyCAD", u"Point to Line Constrain", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pb_con_line.setText(QCoreApplication.translate("fluencyCAD", u"Pt_Lne", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.pb_con_horiz.setToolTip(QCoreApplication.translate("fluencyCAD", u"Horizontal Constrain ", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pb_con_horiz.setText(QCoreApplication.translate("fluencyCAD", u"Horiz", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.pb_con_dist.setToolTip(QCoreApplication.translate("fluencyCAD", u"Dimension of Line of Distance from Point to Line", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pb_con_dist.setText(QCoreApplication.translate("fluencyCAD", u"Distnce", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.pb_con_perp.setToolTip(QCoreApplication.translate("fluencyCAD", u"Constrain Line perpendicular to another line.", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pb_con_perp.setText(QCoreApplication.translate("fluencyCAD", u"Perp_Lne", None))
|
||||
self.pb_con_diameter.setText(QCoreApplication.translate("fluencyCAD", u"Diameter", None))
|
||||
self.assembly_tools.setTitle(QCoreApplication.translate("fluencyCAD", u"Assembly Tools", None))
|
||||
self.pb_compo_to_assembly.setText(QCoreApplication.translate("fluencyCAD", u"Add", None))
|
||||
self.pb_remove_compo_from_assembly.setText(QCoreApplication.translate("fluencyCAD", u"Rem", None))
|
||||
self.groupBox_9.setTitle(QCoreApplication.translate("fluencyCAD", u"Workplanes", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.pb_origin_wp.setToolTip(QCoreApplication.translate("fluencyCAD", u"<W>orking Plane at 0, 0, 0", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pb_origin_wp.setText(QCoreApplication.translate("fluencyCAD", u"WP Origin", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.pb_origin_wp.setShortcut(QCoreApplication.translate("fluencyCAD", u"W", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.pb_origin_face.setToolTip(QCoreApplication.translate("fluencyCAD", u"Working Plane >P<rojection at selected edges face", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pb_origin_face.setText(QCoreApplication.translate("fluencyCAD", u" WP Face", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.pb_origin_face.setShortcut(QCoreApplication.translate("fluencyCAD", u"P", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.pb_flip_face.setToolTip(QCoreApplication.translate("fluencyCAD", u"Flip >N<ormal of projected mesh.", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pb_flip_face.setText(QCoreApplication.translate("fluencyCAD", u"WP Flip", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.pb_flip_face.setShortcut(QCoreApplication.translate("fluencyCAD", u"N", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.pb_underlay.setToolTip(QCoreApplication.translate("fluencyCAD", u"Show / hide the construction lines projected from the source face", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pb_underlay.setText(QCoreApplication.translate("fluencyCAD", u"Underlay", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.pb_clr_face.setToolTip(QCoreApplication.translate("fluencyCAD", u"Forget the picked source face (keep the workplane)", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pb_clr_face.setText(QCoreApplication.translate("fluencyCAD", u"ClrFace", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.pb_to_sketch.setToolTip(QCoreApplication.translate("fluencyCAD", u"Convert projected construction lines into real sketch geometry", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pb_to_sketch.setText(QCoreApplication.translate("fluencyCAD", u"ToSketch", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.pb_wp_new.setToolTip(QCoreApplication.translate("fluencyCAD", u"Create a new independent workplane (datum plane)", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pb_wp_new.setText(QCoreApplication.translate("fluencyCAD", u"WP New", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.pb_wp_new.setShortcut(QCoreApplication.translate("fluencyCAD", u"Shift+W", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.groupBox_12.setTitle(QCoreApplication.translate("fluencyCAD", u"Component Connections", None))
|
||||
self.groupBox_13.setTitle(QCoreApplication.translate("fluencyCAD", u"Tools", None))
|
||||
self.pb_del_connection.setText(QCoreApplication.translate("fluencyCAD", u"Del", None))
|
||||
self.pb_update_connection.setText(QCoreApplication.translate("fluencyCAD", u"Upd", None))
|
||||
self.pb_edt_sktch_4.setText(QCoreApplication.translate("fluencyCAD", u"Nothing", None))
|
||||
self.joint_tools.setTitle(QCoreApplication.translate("fluencyCAD", u"Joint Tools", None))
|
||||
self.pb_add_connector.setText(QCoreApplication.translate("fluencyCAD", u"+ Cnct", None))
|
||||
self.pb_remove_connector.setText(QCoreApplication.translate("fluencyCAD", u"- Cnct", None))
|
||||
self.pb_add_connector_3.setText(QCoreApplication.translate("fluencyCAD", u"-Jnt", None))
|
||||
self.pb_add_connector_2.setText(QCoreApplication.translate("fluencyCAD", u"+Jnt", None))
|
||||
self.gl_box.setTitle(QCoreApplication.translate("fluencyCAD", u"Model Viewer", None))
|
||||
self.compo_box.setTitle(QCoreApplication.translate("fluencyCAD", u"Components", None))
|
||||
self.groupBox_11.setTitle(QCoreApplication.translate("fluencyCAD", u"Sketch", None))
|
||||
self.groupBox_6.setTitle(QCoreApplication.translate("fluencyCAD", u"Tools", None))
|
||||
self.pb_edt_sktch.setText(QCoreApplication.translate("fluencyCAD", u"Edt", None))
|
||||
self.pb_nw_sktch.setText(QCoreApplication.translate("fluencyCAD", u"Add", None))
|
||||
self.pb_del_sketch.setText(QCoreApplication.translate("fluencyCAD", u"Del", None))
|
||||
self.groupBox_2.setTitle(QCoreApplication.translate("fluencyCAD", u"Drawing", None))
|
||||
self.pb_arc_tool.setText(QCoreApplication.translate("fluencyCAD", u"Arc", None))
|
||||
self.pb_rectool.setText(QCoreApplication.translate("fluencyCAD", u"Rctgl", None))
|
||||
self.pb_circtool.setText(QCoreApplication.translate("fluencyCAD", u"Circle", None))
|
||||
self.pb_enable_construct.setText(QCoreApplication.translate("fluencyCAD", u"Cstrct", None))
|
||||
self.pb_enable_snap.setText(QCoreApplication.translate("fluencyCAD", u"Snap", None))
|
||||
self.pb_linetool.setText(QCoreApplication.translate("fluencyCAD", u"Line", None))
|
||||
#if QT_CONFIG(shortcut)
|
||||
self.pb_linetool.setShortcut(QCoreApplication.translate("fluencyCAD", u"S", None))
|
||||
#endif // QT_CONFIG(shortcut)
|
||||
self.pb_slotool.setText(QCoreApplication.translate("fluencyCAD", u"Slot", None))
|
||||
#if QT_CONFIG(tooltip)
|
||||
self.pb_offset_tool.setToolTip(QCoreApplication.translate("fluencyCAD", u"Offset selected sketch face (duplicate + offset boundary)", None))
|
||||
#endif // QT_CONFIG(tooltip)
|
||||
self.pb_offset_tool.setText(QCoreApplication.translate("fluencyCAD", u"Offst", None))
|
||||
self.assembly_box.setTitle(QCoreApplication.translate("fluencyCAD", u"Assembly", None))
|
||||
self.groupBox.setTitle(QCoreApplication.translate("fluencyCAD", u"Modify", None))
|
||||
self.pb_chamfer_op.setText(QCoreApplication.translate("fluencyCAD", u"Chamfer", None))
|
||||
self.pb_fillet_op.setText(QCoreApplication.translate("fluencyCAD", u"Fillet", None))
|
||||
self.pb_thread.setText(QCoreApplication.translate("fluencyCAD", u"Thread", None))
|
||||
self.pb_extrdop.setText(QCoreApplication.translate("fluencyCAD", u"Extrd", None))
|
||||
self.pb_revop.setText(QCoreApplication.translate("fluencyCAD", u"Rev", None))
|
||||
self.pb_cutop.setText(QCoreApplication.translate("fluencyCAD", u"Cut", None))
|
||||
self.pb_arrayop.setText(QCoreApplication.translate("fluencyCAD", u"Arry", None))
|
||||
self.pb_combop.setText(QCoreApplication.translate("fluencyCAD", u"Comb", None))
|
||||
self.pb_moveop.setText(QCoreApplication.translate("fluencyCAD", u"Mve", None))
|
||||
self.pb_mirror_op.setText(QCoreApplication.translate("fluencyCAD", u"Mirror", None))
|
||||
self.groupBox_10.setTitle(QCoreApplication.translate("fluencyCAD", u"Bodys / Operations", None))
|
||||
self.groupBox_8.setTitle(QCoreApplication.translate("fluencyCAD", u"Tools", None))
|
||||
self.pb_body_hide.setText(QCoreApplication.translate("fluencyCAD", u"Hide", None))
|
||||
self.pb_update_body.setText(QCoreApplication.translate("fluencyCAD", u"Upd", None))
|
||||
self.pb_del_body.setText(QCoreApplication.translate("fluencyCAD", u"Del", None))
|
||||
self.menuFile.setTitle(QCoreApplication.translate("fluencyCAD", u"File", None))
|
||||
self.menuSettings.setTitle(QCoreApplication.translate("fluencyCAD", u"Settings", None))
|
||||
# retranslateUi
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 267 B |
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>main</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>main</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>main</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>main</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user