├── .github
└── workflows
│ └── codeql.yml
├── .gitignore
├── Init.py
├── InitGui.py
├── LICENSE
├── OpticalObject.py
├── OpticsWorkbench.py
├── Plot.py
├── README.md
├── README_de.md
├── README_zh_TW.md
├── Ray.py
├── SunRay.py
├── apply_translations.sh
├── examples
├── Concave_mirror_grating_thorlabs_1.PNG
├── Concave_mirror_grating_thorlabs_2.PNG
├── Emitter.png
├── RayHits.png
├── beamsplitter_by_using_ignored_elements.PNG
├── ccd_xyplot.png
├── dispersion.png
├── echelle_example.PNG
├── example1.py
├── example2D.png
├── example3D.py
├── example_candle.png
├── example_candle.py
├── example_candle2D.png
├── example_dispersion.py
├── example_hierarchy2D.png
├── example_hierarchy2D.py
├── example_hierarchy3D.png
├── example_hierarchy3D.py
├── example_semi.png
├── example_semi.py
├── plot3Dexample1.png
├── plot3Dexample2.png
├── screenshot3D.png
├── simple_reflection_grating_set_of_planes.PNG
└── simple_transmission_grating.PNG
├── icons
├── Anonymous_Lightbulb_Lit.svg
├── Anonymous_Lightbulb_Off.svg
├── ExportCSV.svg
├── absorber.svg
├── emitter.svg
├── grating.svg
├── lens.svg
├── mirror.svg
├── ray.svg
├── rayarray.svg
├── raygridfocal.svg
├── raysun.svg
├── scatter3D.svg
├── sun.svg
└── sun3D.svg
├── optics_workbench_icon.svg
├── package.xml
├── translations
├── OpticsWorkbench.ts
├── OpticsWorkbench_de.qm
├── OpticsWorkbench_de.ts
├── OpticsWorkbench_es-AR.qm
├── OpticsWorkbench_es-AR.ts
├── OpticsWorkbench_es-ES.qm
├── OpticsWorkbench_es-ES.ts
├── OpticsWorkbench_zh-TW.qm
├── OpticsWorkbench_zh-TW.ts
└── update_translation.sh
├── update_translations.sh
└── wavelength_to_rgb
├── .gitattributes
├── README.rst
├── gentable.py
└── rgb.py
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 | schedule:
9 | - cron: "3 3 * * 0"
10 |
11 | jobs:
12 | analyze:
13 | name: Analyze
14 | runs-on: ubuntu-latest
15 | permissions:
16 | actions: read
17 | contents: read
18 | security-events: write
19 |
20 | strategy:
21 | fail-fast: false
22 | matrix:
23 | language: [ python ]
24 |
25 | steps:
26 | - name: Checkout
27 | uses: actions/checkout@v3
28 |
29 | - name: Initialize CodeQL
30 | uses: github/codeql-action/init@v2
31 | with:
32 | languages: ${{ matrix.language }}
33 | queries: +security-and-quality
34 |
35 | - name: Autobuild
36 | uses: github/codeql-action/autobuild@v2
37 |
38 | - name: Perform CodeQL Analysis
39 | uses: github/codeql-action/analyze@v2
40 | with:
41 | category: "/language:${{ matrix.language }}"
42 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 | # macos Finder
132 | .DS_Store
--------------------------------------------------------------------------------
/Init.py:
--------------------------------------------------------------------------------
1 | # FreeCAD init script of the PythonWorkbenchTemplate module
2 | # (c) 2020 Christian Bergmann LGPL
3 |
--------------------------------------------------------------------------------
/InitGui.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | __title__ = 'FreeCAD Optics Workbench - Init file'
4 | __author__ = 'Christian Bergmann'
5 | __url__ = ['http://www.freecadweb.org']
6 | __doc__ = 'Optics Workbench workbench'
7 | __version__ = '0.0.1'
8 |
9 |
10 | class OpticsWorkbench (Workbench):
11 | def __init__(self):
12 | import os
13 | import OpticsWorkbench
14 | import FreeCADGui
15 |
16 | translate = FreeCAD.Qt.translate
17 | translations_path = os.path.join(OpticsWorkbench.get_module_path(), "translations")
18 | FreeCADGui.addLanguagePath(translations_path)
19 | FreeCADGui.updateLocale()
20 |
21 | self.__class__.MenuText = 'Optics'
22 | self.__class__.ToolTip = translate("Workbench", 'Ray Tracing Simulation')
23 | self.__class__.Icon = os.path.join(OpticsWorkbench.get_module_path(), 'optics_workbench_icon.svg')
24 |
25 | def Initialize(self):
26 | '''This function is executed when FreeCAD starts'''
27 | # import here all the needed files that create your FreeCAD commands
28 | import Ray
29 | import OpticalObject
30 | import Plot
31 | from examples import example1, example3D, example_dispersion, example_candle, example_semi, example_hierarchy2D, example_hierarchy3D
32 | from PySide.QtCore import QT_TRANSLATE_NOOP
33 |
34 | rays = [QT_TRANSLATE_NOOP('Workbench', 'Ray (monochrome)'),
35 | QT_TRANSLATE_NOOP('Workbench', 'Ray (sun light)'),
36 | QT_TRANSLATE_NOOP('Workbench', 'Beam'),
37 | QT_TRANSLATE_NOOP('Workbench', '2D Radial Beam'),
38 | QT_TRANSLATE_NOOP('Workbench', 'Spherical Beam'),
39 | QT_TRANSLATE_NOOP('Workbench', 'Grid Focal Beam')]
40 | optics = [QT_TRANSLATE_NOOP('Workbench', 'Emitter'),
41 | QT_TRANSLATE_NOOP('Workbench', 'Mirror'),
42 | QT_TRANSLATE_NOOP('Workbench', 'Grating'),
43 | QT_TRANSLATE_NOOP('Workbench', 'Absorber'),
44 | QT_TRANSLATE_NOOP('Workbench', 'Lens')]
45 | actions = [QT_TRANSLATE_NOOP('Workbench', 'Off'), QT_TRANSLATE_NOOP('Workbench', 'Start')]
46 | analysis= [QT_TRANSLATE_NOOP('Workbench', 'RayHits'), QT_TRANSLATE_NOOP('Workbench', 'Hits2CSV')]
47 | separator = ['Separator']
48 | examples = [QT_TRANSLATE_NOOP('Workbench', 'Example2D'),
49 | QT_TRANSLATE_NOOP('Workbench', 'Example3D'),
50 | QT_TRANSLATE_NOOP('Workbench', 'ExampleDispersion'),
51 | QT_TRANSLATE_NOOP('Workbench', 'ExampleCandle'),
52 | QT_TRANSLATE_NOOP('Workbench', 'ExampleSemi'),
53 | QT_TRANSLATE_NOOP('Workbench', 'ExampleHierarchy2D'),
54 | QT_TRANSLATE_NOOP('Workbench', 'ExampleHierarchy3D')]
55 | self.list = rays + separator + optics + separator + actions + separator + analysis #A list of command names created in the line above
56 | self.menu = self.list + separator + examples
57 |
58 | self.appendToolbar(self.__class__.MenuText, self.list) # creates a new toolbar with your commands
59 | self.appendMenu(self.__class__.MenuText, self.menu) # creates a new menu
60 |
61 | def Activated(self):
62 | '''This function is executed when the workbench is activated'''
63 | return
64 |
65 | def Deactivated(self):
66 | '''This function is executed when the workbench is deactivated'''
67 | return
68 |
69 | def ContextMenu(self, recipient):
70 | '''This is executed whenever the user right-clicks on screen'''
71 | # 'recipient' will be either 'view' or 'tree'
72 | self.appendContextMenu(self.__class__.MenuText, self.list) # add commands to the context menu
73 |
74 | def GetClassName(self):
75 | # this function is mandatory if this is a full python workbench
76 | return 'Gui::PythonWorkbench'
77 |
78 |
79 | Gui.addWorkbench(OpticsWorkbench())
80 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/OpticsWorkbench.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import os
4 | from FreeCAD import Vector, Rotation, activeDocument
5 | import Ray
6 | import OpticalObject
7 | import SunRay
8 | from numpy import linspace
9 | from importlib import reload
10 | import Plot
11 |
12 |
13 | def recompute():
14 | activeDocument().recompute()
15 |
16 | def get_module_path():
17 | ''' Returns the current module path.
18 | Determines where this file is running from, so works regardless of whether
19 | the module is installed in the app's module directory or the user's app data folder.
20 | (The second overrides the first.)
21 | '''
22 | return os.path.dirname(__file__)
23 |
24 |
25 | def makeRay(position = Vector(0, 0, 0),
26 | direction = Vector(1, 0, 0),
27 | power = True,
28 | beamNrColumns = 1,
29 | beamNrRows = 1,
30 | beamDistance = 0.1,
31 | spherical = False,
32 | hideFirst = False,
33 | maxRayLength = 1000000,
34 | maxNrReflections = 200,
35 | wavelength = 580,
36 | order = 0,
37 | coneAngle = 360,
38 | ignoredElements=[],
39 | baseShape = None,
40 | focalPoint = Vector(0, 0, 100),
41 | rayBundleType = ''):
42 | reload(Ray)
43 | '''Python command to create a light ray.'''
44 | name = 'Ray'
45 | if beamNrColumns * beamNrRows > 1:
46 | name = 'Beam'
47 | if baseShape:
48 | name = 'Emitter'
49 |
50 | fp = activeDocument().addObject('Part::FeaturePython', name)
51 | fp.Placement.Base = position
52 | fp.Placement.Rotation = Rotation(Vector(1, 0, 0), direction)
53 | Ray.RayWorker(fp, power, spherical, beamNrColumns, beamNrRows, beamDistance, hideFirst, maxRayLength, maxNrReflections, wavelength, order, coneAngle, ignoredElements, baseShape, focalPoint, rayBundleType)
54 | Ray.RayViewProvider(fp.ViewObject)
55 | recompute()
56 | return fp
57 |
58 |
59 | def makeSunRay(position = Vector(0, 0, 0),
60 | direction = Vector(1, 0, 0),
61 | power = True,
62 | beamNrColumns = 1,
63 | beamNrRows = 1,
64 | beamDistance = 0.1,
65 | spherical = False,
66 | hideFirst = False,
67 | maxRayLength = 1000000,
68 | maxNrReflections = 900,
69 | wavelength_from = 450,
70 | wavelength_to = 750,
71 | num_rays = 70,
72 | order = 1,
73 | ignoredElements=[]):
74 | reload(SunRay)
75 | rays = []
76 | for l in linspace(wavelength_from, wavelength_to, num_rays):
77 | ray = makeRay(position = position,
78 | direction = direction,
79 | power = power,
80 | beamNrColumns=beamNrColumns,
81 | beamNrRows=beamNrRows,
82 | beamDistance=beamDistance,
83 | spherical=spherical,
84 | hideFirst = hideFirst,
85 | maxRayLength = maxRayLength,
86 | maxNrReflections = maxNrReflections,
87 | wavelength = l,
88 | order = order,
89 | ignoredElements = ignoredElements)
90 | ray.ViewObject.LineWidth = 1
91 | rays.append(ray)
92 |
93 | fp = activeDocument().addObject('Part::FeaturePython','SunRay')
94 | SunRay.SunRayWorker(fp, rays)
95 | SunRay.SunRayViewProvider(fp.ViewObject)
96 | recompute()
97 | return fp
98 |
99 | # reload(Ray)
100 | # doc = activeDocument()
101 | # rays = []
102 | # for l in linspace(wavelength_from, wavelength_to, num_rays):
103 | # ray = makeRay(position = position,
104 | # direction = direction,
105 | # power = power,
106 | # beamNrColumns=beamNrColumns,
107 | # beamNrRows=beamNrRows,
108 | # beamDistance=beamDistance,
109 | # spherical=spherical,
110 | # hideFirst = hideFirst,
111 | # maxRayLength = maxRayLength,
112 | # maxNrReflections = maxNrReflections,
113 | # wavelength = l,
114 | # order = order)
115 | # ray.ViewObject.LineWidth = 1
116 | # rays.append(ray)
117 |
118 | # group = doc.addObject('App::DocumentObjectGroup','SunRay')
119 | # group.Group = rays
120 | # recompute()
121 |
122 |
123 | def restartAll():
124 | for obj in activeDocument().Objects:
125 | if isRay(obj):
126 | obj.Power = True
127 | obj.touch()
128 |
129 | recompute()
130 |
131 | def allOff():
132 | for obj in activeDocument().Objects:
133 | if isRay(obj):
134 | obj.Power = False
135 |
136 | elif Ray.isOpticalObject(obj):
137 | for a in dir(obj):
138 | if a.startswith('HitsFrom') or a.startswith('HitCoordsFrom') or a.startswith('EnergyFrom'):
139 | obj.removeProperty(a)
140 |
141 | recompute()
142 |
143 | def makeMirror(base = [], collectStatistics = False, transparency=0):
144 | #reload(OpticalObject)
145 | '''All FreeCAD objects in base will be optical mirrors.'''
146 | fp = activeDocument().addObject('Part::FeaturePython', 'Mirror')
147 | OpticalObject.OpticalObjectWorker(fp, base, type = 'mirror', collectStatistics = collectStatistics, transparency = transparency)
148 | OpticalObject.OpticalObjectViewProvider(fp.ViewObject)
149 | recompute()
150 | return fp
151 |
152 | def makeAbsorber(base = [], collectStatistics = False, transparency=0):
153 | #reload(OpticalObject)
154 | '''All FreeCAD objects in base will be optical light absorbers.'''
155 | fp = activeDocument().addObject('Part::FeaturePython', 'Absorber')
156 | OpticalObject.OpticalObjectWorker(fp, base, type = 'absorber', collectStatistics = collectStatistics, transparency = transparency)
157 | OpticalObject.OpticalObjectViewProvider(fp.ViewObject)
158 | recompute()
159 | return fp
160 |
161 | def makeLens(base = [], RefractionIndex = 0, material = 'Quartz', collectStatistics = False, transparency=100):
162 | #reload(OpticalObject)
163 | '''All FreeCAD objects in base will be optical lenses.'''
164 | fp = activeDocument().addObject('Part::FeaturePython', 'Lens')
165 | OpticalObject.LensWorker(fp, base, RefractionIndex, material, collectStatistics, transparency = transparency)
166 | OpticalObject.OpticalObjectViewProvider(fp.ViewObject)
167 | recompute()
168 | return fp
169 |
170 | def makeGrating(base=[], RefractionIndex=1, material='', lpm = 500, GratingType = "reflection", GratingLinesPlane = Vector(0,1,0), order = 1, collectStatistics = False):
171 | #reload(OpticalObject)
172 | '''All FreeCAD objects in base will be diffraction gratings.'''
173 | fp = activeDocument().addObject('Part::FeaturePython', 'Grating')
174 | OpticalObject.GratingWorker(fp, base, RefractionIndex, material, lpm, GratingType, GratingLinesPlane, order, collectStatistics)
175 | OpticalObject.OpticalObjectViewProvider(fp.ViewObject)
176 | recompute()
177 | return fp
178 |
179 | def isRay(obj):
180 | return hasattr(obj, 'Power') and hasattr(obj, 'BeamNrColumns')
181 |
182 | def plot_xy(absorber):
183 | import numpy as np
184 | import matplotlib.pyplot as plt
185 |
186 | coords = []
187 | attr_names = [attr for attr in dir(absorber) if attr.startswith('HitCoordsFrom')]
188 | coords_per_beam = [getattr(absorber, attr) for attr in attr_names]
189 | all_coords = np.array([coord for coords in coords_per_beam for coord in coords])
190 | print("attr_names", attr_names)
191 | print("coords_per_beam", coords_per_beam)
192 | print("all_coords", all_coords)
193 |
194 | x = -all_coords[:,1]
195 | y = all_coords[:,2]
196 |
197 | if len(all_coords) > 0:
198 | plt.scatter(x, y)
199 | plt.show()
200 |
201 |
202 | def drawPlot(selectedObjList):
203 | ## Create the list of selected absorbers; if none then skip
204 | Plot.PlotRayHits.plot3D(selectedObjList)
205 |
206 |
207 | def Hits2CSV():
208 |
209 | sheet = activeDocument().addObject('Spreadsheet::Sheet', 'RayHits')
210 |
211 | sheet.set('A1','Absorber')
212 | sheet.set('B1','Ray')
213 | sheet.set('C1','X-axis')
214 | sheet.set('D1','Y-axis')
215 | sheet.set('E1','Z-axis')
216 | sheet.set('F1','Energy %')
217 | row = 1
218 |
219 | coords_per_beam = []
220 | for eachObject in activeDocument().Objects:
221 | if Ray.isOpticalObject(eachObject):
222 | #all_coords = np.array([coord for coords in coords_per_beam for coord in coords])
223 | for attr in dir(eachObject):
224 | if attr.startswith('HitCoordsFrom'):
225 | lastrow = row
226 | coords_per_beam = getattr(eachObject, attr)
227 | for co in coords_per_beam:
228 | row += 1
229 | sheet.set('A' + str(row), eachObject.Label)
230 | sheet.set('B' + str(row), attr[13:])
231 | sheet.set('C' + str(row), str(co[0]))
232 | sheet.set('D' + str(row), str(co[1]))
233 | sheet.set('E' + str(row), str(co[2]))
234 |
235 | eneryname = 'EnergyFrom' + attr[13:]
236 | if hasattr(eachObject, eneryname):
237 | energy = getattr(eachObject, eneryname)
238 | row = lastrow
239 | for e in energy:
240 | row += 1
241 | sheet.set('F' + str(row), str(e))
242 |
243 | sheet.recompute()
244 | sheet.ViewObject.doubleClicked()
245 |
246 |
--------------------------------------------------------------------------------
/Plot.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import matplotlib.pyplot as plt
3 | import FreeCADGui as Gui
4 | from FreeCAD import activeDocument
5 | import os
6 | import Ray
7 |
8 | from PySide.QtCore import QT_TRANSLATE_NOOP
9 |
10 | _icondir_ = os.path.join(os.path.dirname(__file__), 'icons')
11 |
12 | class PlotRayHits():
13 | '''This class will be loaded when the workbench is activated in FreeCAD. You must restart FreeCAD to apply changes in this class'''
14 | def plot3D(selectedObjList):
15 |
16 | # Figure out the selected absorber; if multiple absorbers selected, or multiple objects then loop through them all
17 | # and accumulate data from all of them
18 |
19 | #print("Selected Objects: ", len(selectedObjList))
20 | if len(selectedObjList) >0:
21 | coords = []
22 | attr_names=[]
23 | coords_per_beam = []
24 | for eachObject in selectedObjList:
25 | #print("Looping through: ", eachObject.Label)
26 | try:
27 | if Ray.isOpticalObject(eachObject):
28 | #coords = []
29 | attr_names[len(attr_names):] = [attr for attr in dir(eachObject) if attr.startswith('HitCoordsFrom')]
30 | coords_per_beam[len(coords_per_beam):] = [getattr(eachObject, attr) for attr in attr_names]
31 | #all_coords = np.array([coord for coords in coords_per_beam for coord in coords])
32 | else:
33 | print ("Ignoring: ",eachObject.Label)
34 | except:
35 | print ("Ignoring: ",eachObject.Label)
36 | all_coords = np.array([coord for coords in coords_per_beam for coord in coords])
37 | if len(all_coords) > 0:
38 | x = all_coords[:,0]
39 | y = all_coords[:,1]
40 | z = all_coords[:,2]
41 |
42 | startx = x[0]
43 | starty = y[0]
44 | startz = z[0]
45 | xpresent = False
46 | ypresent = False
47 | zpresent = False
48 | for co in all_coords:
49 | if abs(startx - co[0]) > Ray.EPSILON: xpresent = True
50 | if abs(starty - co[1]) > Ray.EPSILON: ypresent = True
51 | if abs(startz - co[2]) > Ray.EPSILON: zpresent = True
52 |
53 | fig = plt.figure()
54 |
55 | if xpresent and ypresent and zpresent:
56 | ax = fig.add_subplot(projection='3d')
57 | ax.scatter(x, y, z)
58 | ax.set_xlabel('X-axis')
59 | ax.set_ylabel('Y-axis')
60 | ax.set_zlabel('Z-axis')
61 | else:
62 | ax = fig.add_subplot()
63 | if not zpresent:
64 | ax.scatter(x, y)
65 | ax.set_xlabel('X-axis')
66 | ax.set_ylabel('Y-axis')
67 | elif not ypresent:
68 | ax.scatter(x, z)
69 | ax.set_xlabel('X-axis')
70 | ax.set_ylabel('Z-axis')
71 | else:
72 | ax.scatter(y, z)
73 | ax.set_xlabel('Y-axis')
74 | ax.set_ylabel('Z-axis')
75 |
76 | plt.show()
77 | else:
78 | print("No ray hits were found")
79 |
80 | def Activated(self):
81 | '''Will be called when the feature is executed.'''
82 | # Generate commands in the FreeCAD python console to plot ray hits for selected absorber
83 | Gui.doCommand('import OpticsWorkbench')
84 | Gui.doCommand('selectedObjList = FreeCADGui.Selection.getSelection()')
85 | Gui.doCommand('OpticsWorkbench.drawPlot(selectedObjList)')
86 |
87 |
88 | def IsActive(self):
89 | '''Here you can define if the command must be active or not (greyed) if certain conditions
90 | are met or not. This function is optional.'''
91 | if activeDocument():
92 | return(True)
93 | else:
94 | return(False)
95 |
96 | def GetResources(self):
97 | '''Return the icon which will appear in the tree view. This method is optional and if not defined a default icon is shown.'''
98 | return {'Pixmap' : os.path.join(_icondir_, 'scatter3D.svg'),
99 | 'Accel' : '', # a default shortcut (optional)
100 | 'MenuText': QT_TRANSLATE_NOOP('RayHits', '2D/3D Plot'),
101 | 'ToolTip' : QT_TRANSLATE_NOOP('RayHits', 'Show selected absorbers ray hits in scatter plot') }
102 |
103 |
104 | class RayHits2CSV():
105 | '''This class will be loaded when the workbench is activated in FreeCAD. You must restart FreeCAD to apply changes in this class'''
106 |
107 | def Activated(self):
108 | '''Will be called when the feature is executed.'''
109 | # Generate commands in the FreeCAD python console to plot ray hits for selected absorber
110 | Gui.doCommand('import OpticsWorkbench')
111 | Gui.doCommand('OpticsWorkbench.Hits2CSV()')
112 |
113 |
114 | def IsActive(self):
115 | '''Here you can define if the command must be active or not (greyed) if certain conditions
116 | are met or not. This function is optional.'''
117 | if activeDocument():
118 | return(True)
119 | else:
120 | return(False)
121 |
122 | def GetResources(self):
123 | '''Return the icon which will appear in the tree view. This method is optional and if not defined a default icon is shown.'''
124 | return {'Pixmap' : os.path.join(_icondir_, 'ExportCSV.svg'),
125 | 'Accel' : '', # a default shortcut (optional)
126 | 'MenuText': QT_TRANSLATE_NOOP('Hits2CSV', 'Ray Hits to Spreadsheet'),
127 | 'ToolTip' : QT_TRANSLATE_NOOP('Hits2CSV', 'Export Ray Hits to Spreadsheet') }
128 |
129 |
130 | Gui.addCommand('RayHits', PlotRayHits())
131 | Gui.addCommand('Hits2CSV', RayHits2CSV())
132 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | #  Optics Workbench
2 |
3 | [ >deutsch< ](README_de.md)
4 |
5 | [ >中文< ](README_zh_TW.md)
6 |
7 | Geometrical optics for FreeCAD.
8 | Performs simple raytracing through your FreeCAD objects.
9 |
10 | 
11 |
12 | ## Installation
13 |
14 | ### Auto Installation
15 |
16 | Optics workbench is available through the FreeCAD [Addon Manager](https://wiki.freecad.org/AddonManager)
17 |
18 | ### Manual Installation
19 |
20 |
21 | Expand to view manual installation instructions
22 |
23 | ```bash
24 | cd ~/.FreeCAD/Mod/
25 | git clone https://github.com/chbergmann/OpticsWorkbench.git
26 | ```
27 |
28 |
29 |
30 | #### Important Note
31 | Once Optics workbench is installed, FreeCAD will need to be restarted. When you restart FreeCAD, "Optics Workbench" workbench should now show up in the [workbench dropdown list](https://freecad.org/wiki/Std_Workbench).
32 |
33 | ## Getting started
34 | - Create some FreeCAD design objects. For 2D simulation Sketches will do the job.
35 | - Select one or more of your design objects and then create Optical Mirror to let your objects act as mirrors
36 | - Select one or more of your design objects and then create Optical Absorber to let your objects act as black walls for the light rays
37 | - Select one or more of your design objects and then create Optical Lenses to let your objects act as lenses. Lenses should be closed shapes.
38 | Select a material in the Lens properties or provide a refraction index.
39 | - Add some source of light (Ray, Beam).
40 |
41 | ## Tools
42 | ###  Ray (monochrome)
43 | A single ray for raytracing
44 | Parameters:
45 | - `Power`: On or Off
46 | - `RayBundleType`: `parallel` line or rectangle of beams into one direction, `spherical` all rays start at the same point, `focal` All rays are passing a focal point
47 | - `BeamNrColumns`: Number of rays in a beam per column
48 | - `BeamNrRows`: Number of rays in a beam per row
49 | - `BeamDistance`: Distance between two beams
50 | - `HideFirstPart`: Hide the first part of every ray that comes from the source and goes to the first point of reflection/refraction/nirvana
51 | - `MaxRayLength`: Maximum length of a ray
52 | - `MaxNrReflections`: Maximum number of reflections. This prevents endless loops if the ray is inside a mirror box.
53 | - `Ignored Optical Elements`: List of optical objects that will be ignored by the ray/beam.
54 | - `Base`: If a shape is selected here, an optical emitter will be created.
55 | - `FocalPoint`: set RayBundleType to `focal` to direct all rays to this point
56 |
57 | ###  Ray (sun light)
58 | A number of rays with different wavelengths of visible light.
59 | The rays overlap. If they hit a lens, they will be dispersed. See [Example - Dispersion](#-example---dispersion) below.
60 |
61 | ###  2D Beam
62 | A row of multiple rays for raytracing
63 | Parameters:
64 | * Ray. `BeamNrColumns` must be > 1 and `RayBundleType=parallel` to get a beam
65 |
66 | ###  2D Radial Beam
67 | Rays coming from one point going to all directions in a 2D plane
68 | Parameters:
69 | * Ray. `BeamNrColumns` must be > 1 and `BeamNrRows=1` and `RayBundleType=spherical` to get a radial beam
70 |
71 | ###  Spherical Beam
72 | Rays coming from one point going to all directions
73 | Parameters:
74 | * Ray. `BeamNrColumns` and `BeamNrRows` must be > 1 `RayBundleType=spherical` to get a spherical beam
75 |
76 | ###  Grid Focal Beam
77 | Rays coming from a grid going to a focal point
78 | Parameters:
79 | * Ray. `BeamNrColumns` and `BeamNrRows` must be > 1 `RayBundleType=focal` to get a spherical beam
80 |
81 | ###  Optical Emitter
82 | The FreeCAD objects in parameter Base will act as emitters
83 | Select some FreeCAD objects, faces or edges, then create Optical Emitter
84 | 
85 | Edges can also be selected as Base:
86 | 
87 |
88 | The parameters are the same as in the Ray tool.
89 | Parameters with different meaning for Emitter:
90 | - `Power`: On or Off
91 | - `Spherical`: (will be ignored for this feature)
92 | - `BeamNrColumns`: Number of rays per column distributed over every selected surface
93 | - `BeamNrRows`: Number of rays per row distributed over every selected surface
94 | - `BeamDistance`: (will be ignored for this feature)
95 | - `Base`: Base shape for optical emitter. If a solid is selected, rays will be created on every face. You can also select faces or edges separately.
96 |
97 | ###  Optical Mirror
98 | The FreeCAD objects in parameter Base will act as mirrors
99 | * Select some FreeCAD objects, then create Optical Mirror
100 | Parameters:
101 | - `Transparency`: Percentage of light that passes through the semi transparent mirror
102 | - `collectStatistics` see [Statistics](#Statistics)
103 |
104 | ###  Optical Absorber
105 | The FreeCAD objects in parameter `Base` will swallow the rays of light.
106 | * Select some FreeCAD objects
107 | * Create Optical Absorber
108 | Parameters:
109 | - `Transparency`: Percentage of light that passes through the semi transparent absorber
110 | - `collectStatistics` see [Statistics](#Statistics)
111 |
112 |
113 | ###  Diffraction grating
114 | The FreeCAD objects in parameter `Base` will do simple 1D grating simulation to the very superb OpticsWorkbench.
115 | Raytracing of simple 1D gratings is done following [Ludwig 1973](https://doi.org/10.1364/JOSA.63.001105)
116 |
117 | For this approach, rays now have the additional attribute `order`, which is taken into account when hitting an object specified as optical grating. Utilizing this it is possible to simulate multiple orders of diffraction at one grating by generating rays with the same wavelength but in a different order.
118 |
119 | Gratings are defined by their:
120 | * type:
121 | * reflection
122 | * transmission with diffraction at 1st surface
123 | * transmission with diffraction at 2nd surface
124 | * line spacing
125 | * line direction as specified by a hypothetical set of planes intersecting the body and generating the lines as intersecion cuts
126 | * attribute "order"
127 | Additionally, for transmission gratings a refractive index should be provided.
128 |
129 | Diffraction at a grating object can be specified to be calculated using the order defined by the ray, or by the hit grating, allowing for multiple diffractions of different orders at multiple gratings being hit in the path of a single ray.
130 | **Note** that due to the specific type of this approach to simulate diffraction gratings, one quickly ends up with a large quantity of rays or even sunrays, which consequently heavily increases calculation time.
131 | **Also note** that bugs in the code might of course be present, however in my testing diffraction (at least for reflection and transmission gratings without taking into account different indices of refactions) is simulated accurately.
132 |
133 | 
134 | *Above: illustrates a simple reflection grating with 500 lpm hit by sunray. Planes with normal 010 indicate the set of intersecting planes used to define the grating lines direction.*
135 |
136 | 
137 | *Above: shows the same body, defined as transmission grating. Note that the diffraction happens at the 2nd surface as specified by the grating type. Differences in refractive indices are taken into account.*
138 |
139 | 
140 | *Above: an example of a simple echelle spectrometer using a R2 52.91 lpm grating and a set of sunrays from order -47 to -82 (each order comprises ~5-10 nm, sampled by 15 rays around a center wavelength from blue to red) and a flint glass prism. Collimation and camera optics are thorlabs STEP files and a transparent absorber shows the resulting echelle spectrum. Entrance into the spectrometer design is by a 50 mu slit. This is an example with very long calculation time due to the high number of rays. **Note** that the sign of the order is not intuitive. If an error occurs stating that complex numbers are not supported, while diffraction with this order is considered valid by the user, try to change the sign of the order.*
141 |
142 | see also [Statistics](#Statistics)
143 |
144 | ###  Optical Lens
145 | The FreeCAD objects in parameter Base will act as lenses
146 | * Select some FreeCAD objects
147 | * create Optical Lens
148 | Parameters:
149 | - `Material`: contains a list with pre defined refraction indexes ans Sellmeier coefficients.
150 | - `Refration Index`: has to be provided if no material is selected
151 | - `Sellmeier`: wavelength dependent refraction index coefficents
152 | - `Transparency`: Percentage of light that passes through the lens. The rest will be mirrored at the outside
153 | - `collectStatistics` see [Statistics](#Statistics)
154 |
155 | ###  Switch off lights
156 | Switches off all Rays and Beams
157 |
158 | ###  (Re)start simulation
159 | Switches on and recalculates all Rays and Beams
160 |
161 | ## Statistics
162 | The optical objects have a parameter `collectStatistics`. If true, some statistics will be collected on every start of simulation:
163 | - `Hits From Ray/Beam...`: This is a counter of how many rays have hit this mirror (read only)
164 | - `Hit coordinates from ...`: records the position of each LIGHT RAY when it hits (read only). This way, it is possible to visualize the image on the absorber in a XY diagram.
165 | - `Energy from ...`: records the energy level of evey coordinate. Relevant if you are using semi transparent objects.
166 |
167 | ###  2D/3D Plot
168 | Select one or more optical objects with parameter `collectStatistics` = true and display the location rays hitting them on a scatter graph. It will ignore objects other than absorbers. To only display hits from select beam sources turn off the power for the beams to be ignored. Toggling beams or absorbers visibility in the document tree does not affect the 3D scatter plot output.
169 | If coordinates in all 3 dimensions are present, a 3D plot will be shown, otherwise you will see a 2D plot only.
170 |
171 |  
172 |
173 |
174 | ###  CSV Ray Hits Export
175 | Creates a spreadsheet with the coordinates of all hits of all beams in all optical objects with parameter `collectStatistics` = true.
176 | Go to the Spreadsheet workbench for doing further data processing or export the data to a file.
177 |
178 | 
179 |
180 | ###  Example - 2D
181 | 
182 |
183 | ###  Example - 3D
184 | 
185 |
186 | ###  Example - Dispersion
187 | 
188 |
189 | ###  Example - Candle
190 | 
191 |
192 | ###  Example - Semi transparency
193 | 
194 |
195 | ## Issues and Troubleshooting
196 | see [issues on Github](https://github.com/chbergmann/OpticsWorkbench/issues)
197 |
198 | ## Discussion
199 | Please offer feedback or connect with the developer via the [dedicated FreeCAD forum thread](https://forum.freecad.org/viewtopic.php?f=8&t=59860).
200 |
201 | ## License
202 | GNU Lesser General Public License v3.0 ([LICENSE](LICENSE))
203 |
--------------------------------------------------------------------------------
/README_de.md:
--------------------------------------------------------------------------------
1 | #  Optics Workbench
2 |
3 | [ >english< ](README.md)
4 |
5 | [ >中文< ](README_zh_TW.md)
6 |
7 | Geometrische Optik für FreeCAD.
8 | Simuliert Lichtstrahlen durch deine FreeCAD Objekte.
9 |
10 |
11 | 
12 |
13 | ## Installation
14 |
15 | ### Auto Installation
16 |
17 | Optics workbench kann mit Hilfe des [Addon Manager](https://wiki.freecad.org/AddonManager) installiert werden
18 |
19 | ### Manuelle Installation
20 |
21 |
22 |
23 | ```bash
24 | cd ~/.FreeCAD/Mod/
25 | git clone https://github.com/chbergmann/OpticsWorkbench.git
26 | ```
27 |
28 |
29 |
30 | #### Wichtiger Hinweis
31 | Nach der Installation der Optics Workbench muss FreeCAD neu gestartet werden.
32 | "Optics" ist danach auswählbar in der [workbench dropdown list](https://freecad.org/wiki/Std_Workbench).
33 |
34 | ## Einstieg in Optics workbench
35 | - Erstelle FreeCAD Objekte. Für 2D Simulationen können das Sketche sein.
36 | - Wähle ein oder mehrere FreeCAD objekte aus und klicke dann auf Spiegel, Linse, Absorber oder optisches Gitter, um das Objekt als solches zu verwenden.
37 | - Erstelle eine Lichtquelle (Lichtstrahl, Strahlenbündel, Sonnenstrahl)
38 |
39 | ## Tools
40 | ###  Lichtstrahl (monochrom)
41 | Ein einzelner Lichtstrahl für die optische Simulation.
42 | Parameter:
43 | - `Power`: An oder aus
44 | - `RayBundleType`: `parallel`: alle Lichtstrahlen gehen in die selbe Richtng, `spherical`: Punktförmige Lichtquelle, `focal`: alle Lichtstrahlen schneiden sich in FocalPoint
45 | - `BeamNrColumns`: Anzahl Lichtstrahlen pro Spalte
46 | - `BeamNrRows`: Anzahl Lichtstrahlen pro Reihe
47 | - `BeamDistance`: Abstand zwischen Strahlen
48 | - `Cone Angle`: Lichtkegel Öffnungswinkel
49 | - `HideFirstPart`: Den ersten Teil von jedem Lichtstrahl ausblenden
50 | - `MaxRayLength`: Maximale Länge
51 | - `MaxNrReflections`: Maximale Anzahl von Reflexionen, um Endlosschleifen in geschlossenen Spiegeln zu vermeiden
52 | - `Ignored Optical Elements`: Liste von Ojekten, die ignoriert werden sollen
53 | - `Base`: Falls vorhanden, wird ein Emitter auf Basis dieses Elements generiert
54 | - `FocalPoint`: mit `RayBundleType=focal`: schneiden sich alle Lichtstrahlen in diesem Punkt
55 |
56 | ###  Sonnenstrahl
57 | Ein Bündel sich überlappender Lichtstrahlen mit verschiedenen Wellenlängen im sichtbaren Bereich.
58 | Siehe [Beispiel - Lichtbrechung](#-Beispiel---Lichtbrechung).
59 |
60 | ###  2D Strahlenbündel
61 | Lichtstrahlen in einer Reihe angeordnet.
62 | Parameter:
63 | - `BeamNrColumns` muss größer als 1 sein, um ein Strahlenbündel zu bekommen
64 | - `Spherical`: Punktförmige Lichtquelle, muss False sein
65 |
66 | ###  2D punktförmige Lichtquelle
67 | Punktförmige, radiale Lichtquelle in einer Ebene.
68 | Parameter:
69 | - `BeamNrColumns` Anzahl Strahlen
70 | - `BeamNrRows` muss 1 sein
71 | - `Spherical`: Punktförmige Lichtquelle, muss True sein
72 | - `Cone Angle`: Lichtkegel Öffnungswinkel
73 |
74 | ###  Fokussierte Lichtquelle
75 | Alle Strahlen des Bündels schneiden ich in FocalPoint
76 | Parameters:
77 | * Ray. `BeamNrColumns` and `BeamNrRows` must be > 1 `RayBundleType=focal` to get a spherical beam
78 |
79 | ###  Optischer Emitter
80 | Punktförmige Lichtquelle im 3D Raum.
81 | Parameter:
82 | - `BeamNrColumns` Anzahl Strahlen = `BeamNrColumns` * `BeamNrRows`
83 | - `BeamNrRows` muss größer 1 sein
84 | - `Spherical`: Punktförmige Lichtquelle, muss True sein
85 | - `Cone Angle`: Lichtkegel Öffnungswinkel
86 |
87 | ###  Optischer Emitter
88 | Die FreeCAD Objekte im Parameter Base werden optische Emitter.
89 | Wähle ein FreeCAD Objekt, oder eine Oberfläche aus, dann Optischer Emitter.
90 | 
91 | Edges can also be selected as Base:
92 | 
93 |
94 | Parameter wie in Lichtstrahl (monochrom):
95 | - `Power`: An oder Aus
96 | - `BeamNrColumns`: Anzahl Lichtstrahlen in einer Spalte auf der selektierten Oberfläche
97 | - `BeamNrRows`: Anzahl Lichtstrahlen in einer Reihe auf der selektierten Oberfläche
98 | - `Base`: Basis Objekt für optischen Emitter. Bei Festkörpern werden Lichtstrahlen von allen Oberflächen aus generiert. Es ist auch möglich, einzelene Oberflächen auszuwählen.
99 |
100 | ###  Optischer Spiegel
101 | Die FreeCAD Objekte im Parameter Base werden optische Spiegel.
102 | * Wähle ein oder mehrere FreeCAD Objekte aus, dann Optischer Spiegel.
103 | Parameter:
104 | - `Transparency`: Prozentsatz des Lichts, das den Spiegel passiert. Der Rest wird an der Oberfläche reflektiert
105 | - `collectStatistics` siehe [Statistik](#Statistik)
106 |
107 | ###  Optischer Absorber
108 | Die FreeCAD Objekte im Parameter Base verschlucken alle auftreffenden Lichtstrahlen.
109 | * Wähle ein oder mehrere FreeCAD Objekte aus, dann Optischer Absorber.
110 | Parameter:
111 | - `Transparency`: Prozentsatz des Lichts, das den Absorber passiert
112 | - `collectStatistics` siehe [Statistik](#Statistik)
113 |
114 | ###  Optisches Gitter
115 | Die FreeCAD Objekte im Parameter Base werden optische Gitter.
116 | Simulation von einfachen 1D optischen Gittern nach [Ludwig 1973](https://doi.org/10.1364/JOSA.63.001105)
117 |
118 | For this approach, rays now have the additional attribute `order`, which is taken into account when hitting an object specified as optical grating. Utilizing this it is possible to simulate multiple orders of diffraction at one grating by generating rays with the same wavelength but in a different order.
119 |
120 | Gratings are defined by their:
121 | * type:
122 | * reflection
123 | * transmission with diffraction at 1st surface
124 | * transmission with diffraction at 2nd surface
125 | * line spacing
126 | * line direction as specified by a hypothetical set of planes intersecting the body and generating the lines as intersecion cuts
127 | * attribute "order"
128 | Additionally, for transmission gratings a refractive index should be provided.
129 |
130 | Diffraction at a grating object can be specified to be calculated using the order defined by the ray, or by the hit grating, allowing for multiple diffractions of different orders at multiple gratings being hit in the path of a single ray.
131 | **Note** that due to the specific type of this approach to simulate diffraction gratings, one quickly ends up with a large quantity of rays or even sunrays, which consequently heavily increases calculation time.
132 | **Also note** that bugs in the code might of course be present, however in my testing diffraction (at least for reflection and transmission gratings without taking into account different indices of refactions) is simulated accurately.
133 |
134 | 
135 | *Above: illustrates a simple reflection grating with 500 lpm hit by sunray. Planes with normal 010 indicate the set of intersecting planes used to define the grating lines direction.*
136 |
137 | 
138 | *Above: shows the same body, defined as transmission grating. Note that the diffraction happens at the 2nd surface as specified by the grating type. Differences in refractive indices are taken into account.*
139 |
140 | 
141 | *Above: an example of a simple echelle spectrometer using a R2 52.91 lpm grating and a set of sunrays from order -47 to -82 (each order comprises ~5-10 nm, sampled by 15 rays around a center wavelength from blue to red) and a flint glass prism. Collimation and camera optics are thorlabs STEP files and a transparent absorber shows the resulting echelle spectrum. Entrance into the spectrometer design is by a 50 mu slit. This is an example with very long calculation time due to the high number of rays. **Note** that the sign of the order is not intuitive. If an error occurs stating that complex numbers are not supported, while diffraction with this order is considered valid by the user, try to change the sign of the order.*
142 |
143 |
144 | ###  Optische Linse
145 | Die FreeCAD Objekte im Parameter Base werden optische Linsen.
146 | Wähle ein oder mehrere FreeCAD Objekte aus, dann Optische Linse.
147 | Parameter:
148 | - `RefractionIndex` Brechungsindex
149 | - `Sellmeier` Brechungsindex in Abhängigkeit der Wellenlänge
150 | - `Material` Die Auswahl eines Material stellt Brechungsindex und Sellmeier Parameter ein
151 | - `Transparency`: Prozentsatz des Lichts, das die Linse passiert. Der Rest wird an der Oberfläche reflektiert
152 | - `collectStatistics` siehe [Statistik](#Statistik)
153 |
154 | ###  Licht ausschalten
155 | Alle Lichtquellen deaktivieren.
156 |
157 | ###  Simulation neu starten
158 | Alle Lichtstrahlen neu berechnen.
159 |
160 | ## Statistik
161 | Die optischen Objekte haben einen Parameter `collectStatistics`. Wenn true, werden Statistiken von auftreffenden Lichtstrahlen gesammelt:
162 | - `Hits From Ray/Beam...` Anzahl Strahl-Treffer (schreibgeschützt)
163 | - `Hit coordinates from ... ` Koordination vom Strahl-Treffer (schreibgeschützt)
164 |
165 |
166 | ###  2D/3D Plot
167 | Zeigt eine Graphik mit den Strahl-Treffer Koordinaten von ausgewählten optischen Objekten mit dem Parameter `collectStatistics` = true
168 |
169 |  
170 |
171 |
172 | ###  CSV Ray Hits Export
173 | Exportiert die Statistik von ausgewählten optischen Objekten mit dem Parameter `collectStatistics` = true in eine Tabelle.
174 | Weitere Datenverarbeitung und Speichern in Datei ist dann über die Spreadsheet workbench möglich.
175 |
176 | 
177 |
178 | ###  Beispiel - 2D
179 | 
180 |
181 | ###  Beispiel - 3D
182 | 
183 |
184 | ###  Beispiel - Lichtbrechung
185 | 
186 |
187 | ###  Beispiel - Emitter
188 | 
189 | -
190 | ###  Beispiel - Halb-Transparenz
191 | 
192 |
193 | ## Fehlersuche und Report (englisch)
194 | siehe [issues on Github](https://github.com/chbergmann/OpticsWorkbench/issues)
195 |
196 | ## Diskussion (englisch)
197 | Please offer feedback or connect with the developer via the [dedicated FreeCAD forum thread](https://forum.freecad.org/viewtopic.php?f=8&t=59860).
198 |
199 | ## Lizenz
200 | GNU Lesser General Public License v3.0 ([LICENSE](LICENSE))
201 |
--------------------------------------------------------------------------------
/README_zh_TW.md:
--------------------------------------------------------------------------------
1 | #  Optics Workbench
2 |
3 | [ >deutsch< ](README_de.md)
4 |
5 | [ >English< ](README.md)
6 |
7 | FreeCAD 的幾何光學模組。
8 | 通過你的 FreeCAD 物件進行簡單的光線追蹤。
9 |
10 | 
11 |
12 | ## 安裝
13 |
14 | ### 自動安裝
15 |
16 | Optics 工作台可通過 FreeCAD 的 [Addon Manager](https://wiki.freecad.org/AddonManager) 安裝。
17 |
18 |
19 | ### 手動安裝
20 |
21 |
22 | 點擊展開顯示手動安裝說明
23 |
24 | ```bash
25 | cd ~/.FreeCAD/Mod/
26 | git clone https://github.com/chbergmann/OpticsWorkbench.git
27 | ```
28 |
29 | 如果是macOS下,你的FreeCAD Mod路徑可能如下
30 |
31 | ```
32 | cd ~/Library/Application\ Support/FreeCAD/Mod/
33 | git clone https://github.com/chbergmann/OpticsWorkbench.git
34 | ```
35 |
36 | 如果是Windows路徑可能如下
37 |
38 | ```
39 | C:\Program Files\FreeCAD X.YY
40 | ```
41 |
42 |
43 |
44 | #### 重要提醒
45 | 一旦安裝完成,必須重新啟動 FreeCAD。當重新啟動 FreeCAD 後,"Optics Workbench" 應該會顯示在 [工作台下拉選單](https://freecad.org/wiki/Std_Workbench) 中。
46 |
47 | ## 開始使用
48 | - 創建一些 FreeCAD 設計物件。對於 2D 模擬,草圖就能完成工作。
49 | - 選擇一個或多個設計物件,然後創建光學鏡面,使物件充當鏡子。
50 | - 選擇一個或多個設計物件,然後創建光學吸收體,使物件充當光線的黑牆。
51 | - 選擇一個或多個設計物件,然後創建光學透鏡,使物件充當透鏡。透鏡應該是封閉形狀。
52 | - 在透鏡屬性中選擇一個材料或提供折射率。
53 | - 添加一些光源(光線、光束)。
54 |
55 | ## 工具
56 | ###  光線 (單色)
57 | 單一光線進行光線追蹤
58 | 參數:
59 | - 功率:開啟或關閉
60 | - 球面:False = 光束單向,True = 從中心向所有方向發射的光線
61 | - BeamNrColumns:每列光線的數量
62 | - BeamNrRows:每行光線的數量
63 | - BeamDistance:兩束光線之間的距離
64 | - 隱藏第一部分:隱藏從光源出發到達第一個反射/折射/消失點的光線部分
65 | - 最大光線長度:光線的最大長度
66 | - 最大反射次數:光線的最大反射次數。防止光線進入鏡盒內無限循環。
67 | - 忽略光學元件:光線將忽略的光學物件列表。
68 | - 基礎:如果在此處選擇了一個形狀,將創建一個光學發射器。
69 |
70 | ###  光線 (陽光)
71 | 多條不同波長的可見光光線。
72 | 光線會重疊。如果它們撞擊到透鏡,則會發生色散。請參見 [範例 - 色散](#-example---dispersion) 下方。
73 |
74 | ###  2D 光束
75 | 多條光線組成的光束進行光線追蹤
76 | 參數:
77 | * 光線:BeamNrColumns 必須大於 1 才能生成光束
78 |
79 | ###  2D 放射性光束
80 | 從一點向所有方向發射的光線
81 | 參數:
82 | * 光線:BeamNrColumns 必須大於 1 且 BeamNrRows=1 且 Spherical=True 以生成放射性光束
83 |
84 | ###  球面光束
85 | 從一點向所有方向發射的光線
86 | 參數:
87 | * 光線:BeamNrColumns 和 BeamNrRows 必須大於 1 且 Spherical=True 才能生成球面光束
88 |
89 | ###  光學發射器
90 | FreeCAD 物件將作為光學發射器
91 | 選擇一些 FreeCAD 物件、面或邊,然後創建光學發射器
92 | 
93 | 邊也可以作為基礎選擇:
94 | 
95 |
96 | 參數與光線工具相同。
97 | 具有不同含義的參數:
98 | - 功率:開啟或關閉
99 | - 球面:(此功能將忽略此參數)
100 | - BeamNrColumns:每列光線在每個選定表面上的分佈數量
101 | - BeamNrRows:每行光線在每個選定表面上的分佈數量
102 | - BeamDistance:(此功能將忽略此參數)
103 | - 基礎:用於光學發射器的基礎形狀。如果選擇固體,則會在每個面上創建光線。你也可以分別選擇面或邊。
104 |
105 | ###  光學鏡面
106 | FreeCAD 物件將作為鏡子
107 | * 選擇一些 FreeCAD 物件,然後創建光學鏡面
108 | 請參見 [統計](#Statistics)
109 |
110 | ###  光學吸收體
111 | FreeCAD 物件將吸收光線
112 | * 選擇一些 FreeCAD 物件
113 | * 創建光學吸收體
114 | 請參見 [統計](#Statistics)
115 |
116 | ###  衍射光柵
117 | FreeCAD 物件將進行簡單的一維光柵模擬。
118 | 對簡單的一維光柵進行光線追蹤,依據 [Ludwig 1973](https://doi.org/10.1364/JOSA.63.001105) 進行。
119 |
120 | 此方法中,光線有額外的屬性順序,當撞擊指定為光學光柵的物件時會被考慮。利用此屬性,可以在單一光柵上模擬多次不同次序的衍射。
121 |
122 | 光柵根據以下屬性進行定義:
123 | * 類型:
124 | * 反射
125 | * 透射,且在第一面發生衍射
126 | * 透射,且在第二面發生衍射
127 | * 線間距
128 | * 線方向,由一組假設的平面定義,這些平面與物體交集並生成光柵線
129 | * 屬性 "順序"
130 | 此外,對於透射光柵,應提供折射率。
131 |
132 | 衍射光柵的計算方式可以基於光線定義的順序進行,也可以基於撞擊的光柵進行,這樣就能在多個光柵上同時計算不同的衍射順序。
133 | **注意** 由於此方法模擬光柵的方式,光線數量會迅速增多,尤其是使用陽光等高數量光源時,計算時間也會顯著增加。
134 | **同時注意**,由於代碼中的漏洞,衍射的模擬可能會有所不準,但在測試中反射和透射光柵的模擬是準確的。
135 |
136 | 
137 | *上圖:顯示一個簡單的反射光柵,500 lpm 被陽光照射。平面法線為 010,顯示定義光柵線方向的交集平面集合。*
138 |
139 | 
140 | *上圖:顯示相同的物體,定義為透射光柵。請注意,衍射發生在第二面,這是光柵類型的規定。*
141 |
142 | 
143 | *上圖:顯示一個簡單的 echelle 分光儀,使用 R2 52.91 lpm 光柵,並對來自順序 -47 到 -82 的陽光進行衍射。每個順序包含大約 5-10 nm,並從藍光到紅光分別採樣。*
144 |
145 | 請參見 [統計](#Statistics)
146 |
147 | ###  光學透鏡
148 | FreeCAD 物件將作為透鏡
149 | * 選擇一些 FreeCAD 物件
150 | * 創建光學透鏡
151 | 必須提供折射率。參數「材料」包含一個預定義折射率的列表。
152 | 請參見 [統計](#Statistics)
153 |
154 | ###  關閉光源
155 | 關閉所有光線和光束
156 |
157 | ###  (重新)啟動模擬
158 | 啟動並重新計算所有光線和光束
159 |
160 | ## Statistics
161 | 光學物件有一個參數 `collectStatistics`。如果設為 `true`,則每次開始模擬時會收集一些統計資料:
162 | - `Hits From Ray/Beam...` 這是計數器,顯示有多少光線擊中了此鏡面(只讀)
163 | - `Hit coordinates from ...` 記錄每條光線擊中物體的位置(只讀)。這樣可以在吸收體上以 XY 圖表方式視覺化光線的影像。
164 |
165 | ###  2D/3D 圖表
166 | 選擇一個或多個屬性為 `collectStatistics` = true 的光學物件,並顯示光線擊中它們的位置。將會顯示散佈圖,並忽略非吸收物體。若只顯示選定光束源的擊中位置,請將光束的功率設為關閉,以忽略這些光束。切換文件樹中的光束或吸收體的可見性不會影響 3D 散佈圖的顯示。
167 | 如果有三個維度的座標,將顯示 3D 圖表,否則只顯示 2D 圖表。
168 |
169 |  
170 |
171 |
172 | ###  CSV 光線擊中匯出
173 | 創建一個電子表格,列出所有光束擊中所有吸收體的座標。
174 | 前往「試算表工作台」進行進一步數據處理,或將數據匯出為檔案。
175 |
176 | 
177 |
178 | ###  範例 - 2D
179 | 
180 |
181 | ###  範例 - 3D
182 | 
183 |
184 | ###  範例 - 色散
185 | 
186 |
187 | ###  範例 - 蠟燭
188 | 
189 |
190 | ## 問題與疑難排解
191 | 請參見 [Github 問題](https://github.com/chbergmann/OpticsWorkbench/issues)
192 |
193 | ## 討論
194 | 請提供反饋或通過 [專用 FreeCAD 討論區](https://forum.freecad.org/viewtopic.php?f=8&t=59860) 與開發者聯繫。
195 |
196 | ## 授權
197 | GNU 輕量級公共許可證 v3.0 ([授權條款](LICENSE))
--------------------------------------------------------------------------------
/SunRay.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | __title__ = 'SunRay'
4 | __author__ = 'Christian Bergmann'
5 | __license__ = 'LGPL 3.0'
6 |
7 | import os
8 | import FreeCADGui as Gui
9 | import FreeCAD
10 | translate = FreeCAD.Qt.translate
11 |
12 | from PySide.QtCore import QT_TRANSLATE_NOOP
13 |
14 | _icondir_ = os.path.join(os.path.dirname(__file__), 'icons')
15 | __doc__ = translate('Sun Ray', 'Declare your FreeCAD objects to be optical mirrors, lenses or absorbers')
16 |
17 | class SunRayWorker:
18 | def __init__(self,
19 | fp, # an instance of Part::FeaturePython
20 | rays = []):
21 | fp.addProperty('App::PropertyLinkList', 'Rays', 'SunRay',
22 | translate('Sun Ray', 'Create rays of different wavelength')).Rays = rays
23 | fp.Proxy = self
24 | self.update = True
25 |
26 | def execute(self, fp):
27 | if not self.update: return
28 | self.update = False
29 | self.displace(fp)
30 | self.update = True
31 |
32 | def onChanged(self, fp, prop):
33 | proplist = ['Rays']
34 | if prop in proplist:
35 | self.execute(fp)
36 |
37 | def displace(self, fp):
38 | for ray in fp.Rays:
39 | if ray.Placement != fp.Placement:
40 | ray.Placement = fp.Placement
41 |
42 |
43 | class SunRayViewProvider:
44 | def __init__(self, vobj):
45 | '''Set this object to the proxy object of the actual view provider'''
46 | vobj.Proxy = self
47 | self.Object = vobj.Object
48 |
49 | def getIcon(self):
50 | '''Return the icon which will appear in the tree view. This method is optional and if not defined a default icon is shown.'''
51 | return os.path.join(_icondir_, 'raysun.svg')
52 |
53 | def attach(self, vobj):
54 | '''Setup the scene sub-graph of the view provider, this method is mandatory'''
55 | self.Object = vobj.Object
56 | self.onChanged(vobj, '')
57 |
58 | def updateData(self, fp, prop):
59 | '''If a property of the handled feature has changed we have the chance to handle this here'''
60 | pass
61 |
62 | def claimChildren(self):
63 | '''Return a list of objects that will be modified by this feature'''
64 | return self.Object.Rays
65 |
66 | def onDelete(self, feature, subelements):
67 | '''Here we can do something when the feature will be deleted'''
68 | return True
69 |
70 | def onChanged(self, fp, prop):
71 | '''Here we can do something when a single property got changed'''
72 | pass
73 |
74 | def __getstate__(self):
75 | '''When saving the document this object gets stored using Python's json module.\
76 | Since we have some un-serializable parts here we must define this method\
77 | to return a tuple of all serializable objects or None.'''
78 | return None
79 |
80 | def __setstate__(self, state):
81 | '''When restoring the serialized object from document we have the chance to set some internals here.\
82 | Since no data were serialized nothing needs to be done here.'''
83 | return None
84 |
85 |
86 |
87 | class RaySun():
88 | '''This class will be loaded when the workbench is activated in FreeCAD. You must restart FreeCAD to apply changes in this class'''
89 |
90 | def Activated(self):
91 | Gui.doCommand('import OpticsWorkbench')
92 | Gui.doCommand('OpticsWorkbench.makeSunRay()')
93 |
94 | def IsActive(self):
95 | '''Here you can define if the command must be active or not (greyed) if certain conditions
96 | are met or not. This function is optional.'''
97 | if FreeCAD.ActiveDocument:
98 | return(True)
99 | else:
100 | return(False)
101 |
102 | def GetResources(self):
103 | '''Return the icon which will appear in the tree view. This method is optional and if not defined a default icon is shown.'''
104 | return {'Pixmap' : os.path.join(_icondir_, 'raysun.svg'),
105 | 'Accel' : '', # a default shortcut (optional)
106 | 'MenuText': QT_TRANSLATE_NOOP('Sun Ray', 'Ray (sun light)'),
107 | 'ToolTip' : QT_TRANSLATE_NOOP('Sun Ray', 'Declare your FreeCAD objects to be optical mirrors') }
108 |
109 |
110 | Gui.addCommand('Sun Ray', RaySun())
111 |
--------------------------------------------------------------------------------
/apply_translations.sh:
--------------------------------------------------------------------------------
1 | for f in translations/*_*.ts
2 | do
3 | lrelease "$f"
4 | done
5 |
--------------------------------------------------------------------------------
/examples/Concave_mirror_grating_thorlabs_1.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/Concave_mirror_grating_thorlabs_1.PNG
--------------------------------------------------------------------------------
/examples/Concave_mirror_grating_thorlabs_2.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/Concave_mirror_grating_thorlabs_2.PNG
--------------------------------------------------------------------------------
/examples/Emitter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/Emitter.png
--------------------------------------------------------------------------------
/examples/RayHits.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/RayHits.png
--------------------------------------------------------------------------------
/examples/beamsplitter_by_using_ignored_elements.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/beamsplitter_by_using_ignored_elements.PNG
--------------------------------------------------------------------------------
/examples/ccd_xyplot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/ccd_xyplot.png
--------------------------------------------------------------------------------
/examples/dispersion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/dispersion.png
--------------------------------------------------------------------------------
/examples/echelle_example.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/echelle_example.PNG
--------------------------------------------------------------------------------
/examples/example1.py:
--------------------------------------------------------------------------------
1 | from FreeCAD import Vector
2 | import Sketcher
3 | import Part
4 | import FreeCAD as App
5 | import FreeCADGui as Gui
6 | import OpticsWorkbench
7 | import os
8 | from PySide.QtCore import QT_TRANSLATE_NOOP
9 |
10 | _icondir_ = os.path.join(os.path.dirname(__file__), '..')
11 | _exname_ = QT_TRANSLATE_NOOP('Example2D', 'Example - 2D')
12 |
13 | def createSketch_Sketch_Mirror1(doc):
14 | Sketch_Mirror1 = doc.addObject('Sketcher::SketchObject', 'Sketch_Mirror1')
15 | Sketch_Mirror1.addGeometry(Part.LineSegment(Vector (11.836481, 6.837696, 0.0), Vector (15.17516, -0.6152189999999997, 0.0)))
16 | Sketch_Mirror1.addGeometry(Part.Circle(Vector(0.00, -5.56, 0.00), Vector (0.0, 0.0, 1.0), 1.00))
17 | Sketch_Mirror1.toggleConstruction(1)
18 | Sketch_Mirror1.addGeometry(Part.Circle(Vector(0.00, -15.18, 0.00), Vector (0.0, 0.0, 1.0), 1.00))
19 | Sketch_Mirror1.toggleConstruction(2)
20 | Sketch_Mirror1.addGeometry(Part.Circle(Vector(7.19, -16.35, 0.00), Vector (0.0, 0.0, 1.0), 1.00))
21 | Sketch_Mirror1.toggleConstruction(3)
22 | Sketch_Mirror1.addGeometry(Part.BSplineCurve([Vector(0.00, -5.56, 0.00), Vector(0.00, -15.18, 0.00), Vector(7.19, -16.35, 0.00)]))
23 | Sketch_Mirror1.addGeometry(Part.Point(Vector(0.00, -5.56, 0.00)))
24 | Sketch_Mirror1.addConstraint(Sketcher.Constraint('PointOnObject', 4, 1, -2))
25 | Sketch_Mirror1.addConstraint(Sketcher.Constraint('Equal', 1, 2))
26 | Sketch_Mirror1.addConstraint(Sketcher.Constraint('PointOnObject', 2, 3, -2))
27 | Sketch_Mirror1.addConstraint(Sketcher.Constraint('Equal', 1, 3))
28 | Sketch_Mirror1.addConstraint(Sketcher.Constraint('InternalAlignment:Sketcher::BSplineControlPoint', 1, 3, 4, 0))
29 | Sketch_Mirror1.addConstraint(Sketcher.Constraint('InternalAlignment:Sketcher::BSplineControlPoint', 2, 3, 4, 1))
30 | Sketch_Mirror1.addConstraint(Sketcher.Constraint('InternalAlignment:Sketcher::BSplineControlPoint', 3, 3, 4, 2))
31 | return Sketch_Mirror1
32 |
33 | def createSketch_Sketch_Box(doc):
34 | Sketch_Box = doc.addObject('Sketcher::SketchObject', 'Sketch_Box')
35 | Sketch_Box.addGeometry(Part.LineSegment(Vector (-0.14878900000000073, 11.406045000000002, 0.0), Vector (40.0, 11.406045000000002, 0.0)))
36 | Sketch_Box.addGeometry(Part.LineSegment(Vector (40.0, 11.406045000000002, 0.0), Vector (40.0, -21.699192, 0.0)))
37 | Sketch_Box.addGeometry(Part.LineSegment(Vector (40.0, -21.699192, 0.0), Vector (-0.14878900000000073, -21.699192, 0.0)))
38 | Sketch_Box.addGeometry(Part.LineSegment(Vector (-0.14878900000000073, -21.699192, 0.0), Vector (-0.14878900000000073, 11.406045000000002, 0.0)))
39 | Sketch_Box.addConstraint(Sketcher.Constraint('Coincident', 0, 2, 1, 1))
40 | Sketch_Box.addConstraint(Sketcher.Constraint('Coincident', 1, 2, 2, 1))
41 | Sketch_Box.addConstraint(Sketcher.Constraint('Coincident', 2, 2, 3, 1))
42 | Sketch_Box.addConstraint(Sketcher.Constraint('Coincident', 3, 2, 0, 1))
43 | Sketch_Box.addConstraint(Sketcher.Constraint('Horizontal', 0))
44 | Sketch_Box.addConstraint(Sketcher.Constraint('Horizontal', 2))
45 | Sketch_Box.addConstraint(Sketcher.Constraint('Vertical', 1))
46 | Sketch_Box.addConstraint(Sketcher.Constraint('Vertical', 3))
47 | Sketch_Box.ViewObject.DiffuseColor = [(0.00, 0.00, 0.00, 0.00)]
48 | Sketch_Box.ViewObject.LineColor = (0.00, 0.00, 0.00, 0.00)
49 | Sketch_Box.ViewObject.LineColorArray = [(0.00, 0.00, 0.00, 0.00)]
50 | Sketch_Box.ViewObject.PointColor = (0.00, 0.00, 0.00, 0.00)
51 | Sketch_Box.ViewObject.PointColorArray = [(0.00, 0.00, 0.00, 0.00)]
52 | Sketch_Box.ViewObject.ShapeColor = (0.00, 0.00, 0.00, 0.00)
53 | return Sketch_Box
54 |
55 | def createSketch_Sketch_Mirror2(doc):
56 | Sketch_Mirror2 = doc.addObject('Sketcher::SketchObject', 'Sketch_Mirror2')
57 | Sketch_Mirror2.addGeometry(Part.ArcOfCircle(Part.Circle(Vector(56.12, -5.69, 0.00), Vector (0.0, 0.0, 1.0), 10.64), 0.6054907273817108, 6.799308361137584))
58 | return Sketch_Mirror2
59 |
60 | def createSketch_Sketch_Lens(doc):
61 | Sketch_Lens = doc.addObject('Sketcher::SketchObject', 'Sketch_Lens')
62 | Sketch_Lens.addGeometry(Part.LineSegment(Vector (11.566624790355402, -6.79, 0.0), Vector (11.566624790355402, -16.79, 0.0)))
63 | Sketch_Lens.addGeometry(Part.ArcOfCircle(Part.Circle(Vector(8.25, -11.79, 0.00), Vector (0.0, 0.0, 1.0), 6.00), 5.298074523841841, 7.268296090517332))
64 | Sketch_Lens.addConstraint(Sketcher.Constraint('Vertical', 0))
65 | Sketch_Lens.addConstraint(Sketcher.Constraint('Coincident', 1, 1, 0, 2))
66 | Sketch_Lens.addConstraint(Sketcher.Constraint('Coincident', 1, 2, 0, 1))
67 | Sketch_Lens.addConstraint(Sketcher.Constraint('Radius', 1, 6.00))
68 | Sketch_Lens.addConstraint(Sketcher.Constraint('DistanceY', 0, 2, 0, 1, 10.00))
69 | return Sketch_Lens
70 |
71 | def createSketch_Sketch_Prism(doc):
72 | Sketch_Prism = doc.addObject('Sketcher::SketchObject', 'Sketch_Prism')
73 | Sketch_Prism.addGeometry(Part.LineSegment(Vector (21.126467, -13.53020124766231, 0.0), Vector (21.126467, -21.018505, 0.0)))
74 | Sketch_Prism.addGeometry(Part.LineSegment(Vector (21.126467, -21.018505, 0.0), Vector (31.579980991773073, -17.27435312383116, 0.0)))
75 | Sketch_Prism.addGeometry(Part.LineSegment(Vector (31.579980991773073, -17.27435312383116, 0.0), Vector (21.126467, -13.53020124766231, 0.0)))
76 | Sketch_Prism.addConstraint(Sketcher.Constraint('Vertical', 0))
77 | Sketch_Prism.addConstraint(Sketcher.Constraint('Coincident', 0, 2, 1, 1))
78 | Sketch_Prism.addConstraint(Sketcher.Constraint('Coincident', 1, 2, 2, 1))
79 | Sketch_Prism.addConstraint(Sketcher.Constraint('Coincident', 2, 2, 0, 1))
80 | Sketch_Prism.addConstraint(Sketcher.Constraint('Equal', 1, 2))
81 | return Sketch_Prism
82 |
83 | def make_optics():
84 | App.newDocument()
85 | doc = App.activeDocument()
86 |
87 | Sketch_Mirror1 = createSketch_Sketch_Mirror1(doc)
88 | Sketch_Box1 = createSketch_Sketch_Box(doc)
89 | Sketch_Box2 = createSketch_Sketch_Box(doc)
90 | Sketch_Box2.Placement.Base.x += 41
91 | Sketch_Mirror2 = createSketch_Sketch_Mirror2(doc)
92 | Sketch_Lens = createSketch_Sketch_Lens(doc)
93 | Sketch_Prism = createSketch_Sketch_Prism(doc)
94 |
95 | OpticsWorkbench.makeMirror([Sketch_Mirror1, Sketch_Mirror2])
96 | OpticsWorkbench.makeAbsorber([Sketch_Box1, Sketch_Box2], True)
97 | OpticsWorkbench.makeLens([Sketch_Lens, Sketch_Prism], material='NBK7/Window glass')
98 |
99 | doc.recompute()
100 | OpticsWorkbench.makeRay(Vector(75.00, 0.00, 0.00), Vector(-1,0,0))
101 | OpticsWorkbench.makeRay(beamNrColumns=50)
102 |
103 | doc.recompute()
104 |
105 | class Example1():
106 | '''This class will be loaded when the workbench is activated in FreeCAD. You must restart FreeCAD to apply changes in this class'''
107 |
108 | def Activated(self):
109 | make_optics()
110 | Gui.runCommand('Std_OrthographicCamera',1)
111 | Gui.SendMsgToActiveView("ViewFit")
112 |
113 | def IsActive(self):
114 | return(True)
115 |
116 | def GetResources(self):
117 | '''Return the icon which will appear in the tree view. This method is optional and if not defined a default icon is shown.'''
118 | return {'Pixmap' : os.path.join(_icondir_, 'optics_workbench_icon.svg'),
119 | 'Accel' : "", # a default shortcut (optional)
120 | 'MenuText': _exname_,
121 | 'ToolTip' : '' }
122 |
123 | Gui.addCommand('Example2D', Example1())
124 |
--------------------------------------------------------------------------------
/examples/example2D.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/example2D.png
--------------------------------------------------------------------------------
/examples/example3D.py:
--------------------------------------------------------------------------------
1 | import FreeCAD as App
2 | import FreeCADGui as Gui
3 | import os
4 | from FreeCAD import Vector, Placement, Rotation
5 | import FreeCAD as app
6 | import OpticsWorkbench
7 | from PySide.QtCore import QT_TRANSLATE_NOOP
8 |
9 | _icondir_ = os.path.join(os.path.dirname(__file__), '..')
10 |
11 | def make_Test3D():
12 | App.newDocument("Example 3D")
13 | doc = app.activeDocument()
14 |
15 | Cube = doc.addObject('Part::Box', 'Cube')
16 | Cube.Placement = Placement(Vector(20.00, -3.17, -2.00), Rotation (-0.0, -0.0, 0.25881904510252074, 0.9659258262890684))
17 |
18 | Sphere = doc.addObject('Part::Sphere', 'Sphere')
19 | Sphere.Placement = Placement(Vector(3.00, -19.00, 0.00), Rotation (0.0, 0.0, 0.0, 1.0))
20 |
21 | Cone = doc.addObject('Part::Cone', 'Cone')
22 | Cone.Placement = Placement(Vector(68.90, -46.50, -2.30), Rotation (0.0, 0.0, 0.0, 1.0))
23 |
24 | Cylinder = doc.addObject('Part::Cylinder', 'Cylinder')
25 | Cylinder.Height = 50.0
26 | Cylinder.Placement = Placement(Vector(40.00, -26.00, -19.00), Rotation (0.0, 0.0, 0.0, 1.0))
27 | Cylinder.Radius = 50.0
28 | Cylinder.ViewObject.Transparency = 90
29 |
30 | Sphere001 = doc.addObject('Part::Sphere', 'Sphere001')
31 | Sphere001.Radius = 20.0
32 | Sphere001.Visibility = False
33 | Sphere001.ViewObject.Visibility = False
34 |
35 | Cube002 = doc.addObject('Part::Box', 'Cube002')
36 | Cube002.Height = 40.0
37 | Cube002.Length = 40.0
38 | Cube002.Placement = Placement(Vector(-20.00, -25.00, -20.00), Rotation (0.0, 0.0, 0.0, 1.0))
39 | Cube002.Visibility = False
40 | Cube002.Width = 40.0
41 | Cube002.ViewObject.Visibility = False
42 |
43 | HalfSphere = doc.addObject('Part::Cut', 'HalfSphere')
44 | HalfSphere.Base = Sphere001
45 | HalfSphere.Placement = Placement(Vector(25.86, -24.61, 0.00), Rotation (0.0, 0.0, -0.793353340291235, 0.6087614290087205))
46 | HalfSphere.Tool = Cube002
47 | HalfSphere.ViewObject.DiffuseColor = [(0.80, 0.80, 0.80, 0.00), (0.80, 0.80, 0.80, 0.00)]
48 | HalfSphere.ViewObject.Transparency = 75
49 |
50 | doc.recompute()
51 |
52 | OpticsWorkbench.makeMirror([Cube, Sphere, Cone], True)
53 | OpticsWorkbench.makeAbsorber([Cylinder])
54 | OpticsWorkbench.makeLens([HalfSphere])
55 |
56 | doc.recompute()
57 | OpticsWorkbench.makeRay(beamNrColumns=20, beamNrRows=10, beamDistance=0.05)
58 |
59 | doc.recompute()
60 |
61 |
62 | class Example3D():
63 | '''This class will be loaded when the workbench is activated in FreeCAD. You must restart FreeCAD to apply changes in this class'''
64 |
65 | def Activated(self):
66 | make_Test3D()
67 | Gui.activeDocument().activeView().viewDimetric()
68 | Gui.SendMsgToActiveView("ViewFit")
69 |
70 | def IsActive(self):
71 | return(True)
72 |
73 | def GetResources(self):
74 | '''Return the icon which will appear in the tree view. This method is optional and if not defined a default icon is shown.'''
75 | return {'Pixmap' : os.path.join(_icondir_, 'optics_workbench_icon.svg'),
76 | 'Accel' : "", # a default shortcut (optional)
77 | 'MenuText': QT_TRANSLATE_NOOP('Example3D', 'Example - 3D'),
78 | 'ToolTip' : '' }
79 |
80 | Gui.addCommand('Example3D', Example3D())
81 |
--------------------------------------------------------------------------------
/examples/example_candle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/example_candle.png
--------------------------------------------------------------------------------
/examples/example_candle.py:
--------------------------------------------------------------------------------
1 | from FreeCAD import Vector, Placement, Rotation
2 | import Sketcher
3 | import Part
4 | import FreeCAD as App
5 | import FreeCADGui as Gui
6 | import OpticsWorkbench
7 | import os
8 | from PySide.QtCore import QT_TRANSLATE_NOOP
9 |
10 | _icondir_ = os.path.join(os.path.dirname(__file__), '..')
11 |
12 | def createSketch_Sketch(doc):
13 | Sketch = doc.addObject('Sketcher::SketchObject', 'Sketch')
14 | geo0 = Sketch.addGeometry(Part.Circle(Vector(0.00, 12.01, 0.00), Vector (0.0, 0.0, 1.0), 1.00))
15 | Sketch.toggleConstruction(geo0)
16 | geo1 = Sketch.addGeometry(Part.Circle(Vector(-1.00, 12.42, 0.00), Vector (0.0, 0.0, 1.0), 1.00))
17 | Sketch.toggleConstruction(geo1)
18 | geo2 = Sketch.addGeometry(Part.Circle(Vector(-1.43, 15.03, 0.00), Vector (0.0, 0.0, 1.0), 1.00))
19 | Sketch.toggleConstruction(geo2)
20 | geo3 = Sketch.addGeometry(Part.Circle(Vector(-0.15, 15.56, 0.00), Vector (0.0, 0.0, 1.0), 1.00))
21 | Sketch.toggleConstruction(geo3)
22 | geo4 = Sketch.addGeometry(Part.Circle(Vector(0.00, 16.62, 0.00), Vector (0.0, 0.0, 1.0), 1.00))
23 | Sketch.toggleConstruction(geo4)
24 | geo5 = Sketch.addGeometry(Part.BSplineCurve([Vector(0.00, 12.01, 0.00), Vector(-1.00, 12.42, 0.00), Vector(-1.43, 15.03, 0.00), Vector(-0.15, 15.56, 0.00), Vector(0.00, 16.62, 0.00)]))
25 | Sketch.addConstraint(Sketcher.Constraint('Weight', geo0, 1.00))
26 | Sketch.addConstraint(Sketcher.Constraint('PointOnObject', geo5, 1, -2))
27 | Sketch.addConstraint(Sketcher.Constraint('Equal', geo0, geo1))
28 | Sketch.addConstraint(Sketcher.Constraint('Equal', geo0, geo2))
29 | Sketch.addConstraint(Sketcher.Constraint('Equal', geo0, geo3))
30 | Sketch.addConstraint(Sketcher.Constraint('Equal', geo0, geo4))
31 | Sketch.addConstraint(Sketcher.Constraint('PointOnObject', geo5, 2, -2))
32 | Sketch.addConstraint(Sketcher.Constraint('InternalAlignment:Sketcher::BSplineControlPoint', geo0, 3, geo5, 0))
33 | Sketch.addConstraint(Sketcher.Constraint('InternalAlignment:Sketcher::BSplineControlPoint', geo1, 3, geo5, 1))
34 | Sketch.addConstraint(Sketcher.Constraint('InternalAlignment:Sketcher::BSplineControlPoint', geo2, 3, geo5, 2))
35 | Sketch.addConstraint(Sketcher.Constraint('InternalAlignment:Sketcher::BSplineControlPoint', geo3, 3, geo5, 3))
36 | Sketch.addConstraint(Sketcher.Constraint('InternalAlignment:Sketcher::BSplineControlPoint', geo4, 3, geo5, 4))
37 | Sketch.Placement = Placement(Vector(0.00, 0.00, 0.00), Rotation (0.7071067811865475, 0.0, 0.0, 0.7071067811865476))
38 | Sketch.Visibility = False
39 | Sketch.ViewObject.Visibility = False
40 | return Sketch
41 |
42 |
43 | def make_candle():
44 | App.newDocument("Candle Example")
45 | doc = App.activeDocument()
46 |
47 | Candle1 = doc.addObject('Part::Cylinder', 'Candle1')
48 | Candle1.Radius = 3.0
49 |
50 | Candle2 = doc.addObject('Part::Cylinder', 'Candle2')
51 | Candle2.Height = 12.5
52 | Candle2.Radius = 0.2
53 |
54 | Sketch = createSketch_Sketch(doc)
55 |
56 | Revolve = doc.addObject('Part::Revolution', 'Flame')
57 | Revolve.Source = Sketch
58 | Revolve.ViewObject.LineColor = (1.00, 1.00, 1.00, 1.00)
59 | Revolve.ViewObject.LineColorArray = [(1.00, 1.00, 1.00, 1.00)]
60 | Revolve.ViewObject.PointColor = (1.00, 1.00, 1.00, 1.00)
61 | Revolve.ViewObject.PointColorArray = [(1.00, 1.00, 1.00, 1.00)]
62 |
63 | OpticsWorkbench.makeAbsorber([Candle1, Candle2])
64 | OpticsWorkbench.makeRay(beamNrColumns=10, beamNrRows=10, baseShape=Revolve, maxRayLength=100)
65 |
66 | doc.recompute()
67 |
68 |
69 | class ExampleCandle():
70 | '''This class will be loaded when the workbench is activated in FreeCAD. You must restart FreeCAD to apply changes in this class'''
71 |
72 | def Activated(self):
73 | make_candle()
74 | Gui.activeDocument().activeView().viewIsometric()
75 |
76 | def IsActive(self):
77 | return(True)
78 |
79 | def GetResources(self):
80 | '''Return the icon which will appear in the tree view. This method is optional and if not defined a default icon is shown.'''
81 | return {'Pixmap' : os.path.join(_icondir_, 'optics_workbench_icon.svg'),
82 | 'Accel' : "", # a default shortcut (optional)
83 | 'MenuText': QT_TRANSLATE_NOOP('ExampleCandle', 'Example - Candle'),
84 | 'ToolTip' : '' }
85 |
86 | Gui.addCommand('ExampleCandle', ExampleCandle())
--------------------------------------------------------------------------------
/examples/example_candle2D.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/example_candle2D.png
--------------------------------------------------------------------------------
/examples/example_dispersion.py:
--------------------------------------------------------------------------------
1 | from FreeCAD import Vector
2 | import Sketcher
3 | import Part
4 | import FreeCAD as App
5 | import FreeCADGui as Gui
6 | import OpticsWorkbench
7 | import os
8 | from PySide.QtCore import QT_TRANSLATE_NOOP
9 |
10 | _icondir_ = os.path.join(os.path.dirname(__file__), '..')
11 |
12 | def create_prism(doc):
13 | Sketch_Prism = doc.addObject('Sketcher::SketchObject', 'Sketch_Prism')
14 | add_line = lambda a,b: Sketch_Prism.addGeometry(Part.LineSegment(Vector (a), Vector(b)))
15 | a = (1.13, 0.26, 0.0)
16 | b = (1.50, -.38, 0.0)
17 | c = (0.76, -.38, 0.0)
18 | add_line(a, b)
19 | add_line(b, c)
20 | add_line(c, a)
21 |
22 | add_constraint = lambda *args: Sketch_Prism.addConstraint(Sketcher.Constraint(*args))
23 | add_constraint('Coincident', 0, 2, 1, 1)
24 | add_constraint('Coincident', 1, 2, 2, 1)
25 | add_constraint('Coincident', 2, 2, 0, 1)
26 | add_constraint('Equal', 1, 2)
27 | add_constraint('Equal', 0, 1)
28 | Sketch_Prism.Visibility = False
29 |
30 | prism = doc.addObject('PartDesign::Pad','prism')
31 | prism.Profile= Sketch_Prism
32 | prism.Midplane = 1 # symmetric
33 | prism.Length = 0.2
34 |
35 | prism.ViewObject.Transparency = 70
36 | prism.ViewObject.LineWidth = 1
37 | prism.ViewObject.LineMaterial.Transparency = 30
38 | prism.ViewObject.PointMaterial.Transparency = 30
39 | return prism
40 |
41 |
42 | def make_optics():
43 | App.newDocument("Dispersion Example")
44 | doc = App.activeDocument()
45 |
46 | prism = create_prism(doc)
47 | doc.recompute()
48 | OpticsWorkbench.makeLens(prism, material='PMMA (plexiglass)').Label = 'Refractor'
49 | OpticsWorkbench.makeSunRay(maxRayLength=2.0)
50 | doc.recompute()
51 |
52 | class ExampleDispersion():
53 | '''This class will be loaded when the workbench is activated in FreeCAD. You must restart FreeCAD to apply changes in this class'''
54 |
55 | def Activated(self):
56 | make_optics()
57 | Gui.activeDocument().activeView().viewTop()
58 | Gui.SendMsgToActiveView("ViewFit")
59 |
60 | def IsActive(self):
61 | return(True)
62 |
63 | def GetResources(self):
64 | '''Return the icon which will appear in the tree view. This method is optional and if not defined a default icon is shown.'''
65 | return {'Pixmap' : os.path.join(_icondir_, 'optics_workbench_icon.svg'),
66 | 'Accel' : "", # a default shortcut (optional)
67 | 'MenuText': QT_TRANSLATE_NOOP('ExampleDispersion', 'Example - Dispersion'),
68 | 'ToolTip' : '' }
69 |
70 | Gui.addCommand('ExampleDispersion', ExampleDispersion())
71 |
--------------------------------------------------------------------------------
/examples/example_hierarchy2D.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/example_hierarchy2D.png
--------------------------------------------------------------------------------
/examples/example_hierarchy2D.py:
--------------------------------------------------------------------------------
1 | from FreeCAD import Vector, Rotation
2 | import Sketcher
3 | import Part
4 | import FreeCAD as App
5 | import FreeCADGui as Gui
6 | import OpticsWorkbench
7 | import os
8 | from PySide.QtCore import QT_TRANSLATE_NOOP
9 |
10 | _icondir_ = os.path.join(os.path.dirname(__file__), '..')
11 | _exname_ = QT_TRANSLATE_NOOP('ExampleHierarchy2D', 'Example - Hierarchy 2D')
12 |
13 | def makeLens(doc, name):
14 | sketch = doc.addObject('Sketcher::SketchObject', name + '_sketch')
15 | circle = Part.Circle(Vector(2, 0, 0), Vector(0, 0, 1), 5)
16 | arc = Part.ArcOfCircle(circle, 2, 4)
17 | sketch.addGeometry(arc)
18 | line = Part.LineSegment(Vector(0, 4.5, 0), Vector(0, -4.5, 0))
19 | sketch.addGeometry(line)
20 | sketch.addConstraint(Sketcher.Constraint('Radius', 0, 5))
21 | sketch.addConstraint(Sketcher.Constraint('PointOnObject', 0, 1, -2))
22 | sketch.addConstraint(Sketcher.Constraint('PointOnObject', 0, 2, -2))
23 | sketch.addConstraint(Sketcher.Constraint('Coincident', 1, 1, 0, 1))
24 | sketch.addConstraint(Sketcher.Constraint('Coincident', 0, 2, 1, 2))
25 | sketch.addConstraint(Sketcher.Constraint('PointOnObject', 0, 3, -1))
26 | sketch.addConstraint(Sketcher.Constraint('DistanceX', 0, 3, 2))
27 | sketch.Placement.Base = Vector(0, 19, 0)
28 | sketch.Placement.Rotation = Rotation(-42, 0, 0)
29 | lens = OpticsWorkbench.makeLens([sketch])
30 | lens.Label = name
31 | return lens
32 |
33 | def makeMirror(doc, name):
34 | sketch = doc.addObject('Sketcher::SketchObject', name + '_sketch')
35 | sketch.addGeometry(Part.LineSegment(Vector(10.0, 0.0, 0.0), Vector(12.0, 10.0, 0.0)))
36 | mirror = OpticsWorkbench.makeMirror([sketch])
37 | mirror.Label = name
38 | return mirror
39 |
40 | def makeRay(name):
41 | ray = OpticsWorkbench.makeRay(Vector(0, 0, 0), Vector(2.0, 1.0, 0), beamNrColumns=10, beamDistance=0.4)
42 | ray.Placement.Base = Vector(0, -1.5, 0)
43 | ray.Label = name
44 | return ray
45 |
46 | def createRayInsideMirrorInside(doc):
47 | obj = doc.addObject('App::Part', 'ray_inside_mirror_inside')
48 | mirror = makeMirror(doc, 'mirror_a')
49 | obj.addObject(mirror)
50 | lens = makeLens(doc, 'lens_a')
51 | obj.addObject(lens)
52 | ray = makeRay('ray_a')
53 | obj.addObject(ray)
54 | obj.Placement.Base += Vector(-30.0, -30.0, 0.0)
55 | ray.MaxRayLength = 24.0
56 | return obj
57 |
58 | def createRayOutsideMirrorOutside(doc):
59 | obj = doc.addObject('App::DocumentObjectGroup', 'ray_outside_mirror_outside')
60 | mirror = makeMirror(doc, 'mirror_b')
61 | mirror.Base[0].Placement.Base += Vector(30.0, -30.0, 0.0)
62 | obj.addObject(mirror)
63 | lens = makeLens(doc, 'lens_b')
64 | lens.Base[0].Placement.Base += Vector(30.0, -30.0, 0.0)
65 | obj.addObject(lens)
66 | ray = makeRay('ray_b')
67 | ray.Placement.Base += Vector(30.0, -30.0, 0.0)
68 | obj.addObject(ray)
69 | ray.MaxRayLength = 24.0
70 | return obj
71 |
72 | def createRayInsideMirrorOutside(doc):
73 | obj = doc.addObject('App::DocumentObjectGroup', 'ray_inside_mirror_outside')
74 | obj_inside = doc.addObject('App::Part', 'ray_inside')
75 | obj_inside.Placement.Base = Vector(-30.0, 30.0, 0.0)
76 | obj.addObject(obj_inside)
77 | mirror = makeMirror(doc, 'mirror_c')
78 | mirror.Base[0].Placement.Base += Vector(-30.0, 30.0, 0.0)
79 | obj.addObject(mirror)
80 | lens = makeLens(doc, 'lens_c')
81 | lens.Base[0].Placement.Base += Vector(-30.0, 30.0, 0.0)
82 | obj.addObject(lens)
83 | ray = makeRay('ray_c')
84 | obj_inside.addObject(ray)
85 | ray.MaxRayLength = 24.0
86 | return obj
87 |
88 | def createRayOutsideMirrorInside(doc):
89 | obj = doc.addObject('App::DocumentObjectGroup', 'ray_outside_mirror_inside')
90 | obj_inside = doc.addObject('App::Part', 'mirror_inside')
91 | obj_inside.Placement.Base = Vector(30.0, 30.0, 0.0)
92 | obj.addObject(obj_inside)
93 | mirror = makeMirror(doc, 'mirror_d')
94 | obj_inside.addObject(mirror)
95 | lens = makeLens(doc, 'lens_d')
96 | obj_inside.addObject(lens)
97 | ray = makeRay('ray_d')
98 | ray.Placement.Base += Vector(30.0, 30.0, 0.0)
99 | obj.addObject(ray)
100 | ray.MaxRayLength = 24.0
101 | return obj
102 |
103 | def make_optics():
104 | App.newDocument()
105 | doc = App.activeDocument()
106 |
107 | createRayInsideMirrorInside(doc)
108 | createRayOutsideMirrorOutside(doc)
109 | createRayInsideMirrorOutside(doc)
110 | createRayOutsideMirrorInside(doc)
111 |
112 | doc.recompute()
113 |
114 | class ExampleHierarchy2D():
115 | '''This class will be loaded when the workbench is activated in FreeCAD. You must restart FreeCAD to apply changes in this class'''
116 |
117 | def Activated(self):
118 | make_optics()
119 | Gui.runCommand('Std_OrthographicCamera',1)
120 | Gui.SendMsgToActiveView("ViewFit")
121 |
122 | def IsActive(self):
123 | return(True)
124 |
125 | def GetResources(self):
126 | '''Return the icon which will appear in the tree view. This method is optional and if not defined a default icon is shown.'''
127 | return {'Pixmap' : os.path.join(_icondir_, 'optics_workbench_icon.svg'),
128 | 'Accel' : "", # a default shortcut (optional)
129 | 'MenuText': _exname_,
130 | 'ToolTip' : '' }
131 |
132 | Gui.addCommand('ExampleHierarchy2D', ExampleHierarchy2D())
133 |
--------------------------------------------------------------------------------
/examples/example_hierarchy3D.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/example_hierarchy3D.png
--------------------------------------------------------------------------------
/examples/example_hierarchy3D.py:
--------------------------------------------------------------------------------
1 | from FreeCAD import Vector, Rotation
2 | import Sketcher
3 | import Part
4 | import FreeCAD as App
5 | import FreeCADGui as Gui
6 | from BOPTools import BOPFeatures
7 | import OpticsWorkbench
8 | import os
9 | from PySide.QtCore import QT_TRANSLATE_NOOP
10 |
11 | _icondir_ = os.path.join(os.path.dirname(__file__), '..')
12 | _exname_ = QT_TRANSLATE_NOOP('ExampleHierarchy3D', 'Example - Hierarchy 3D')
13 |
14 | def makeLens(doc, name):
15 | sphere = doc.addObject('Part::Sphere', name + '_sphere')
16 | box = doc.addObject('Part::Box', name + '_box')
17 | box.Placement.Base = Vector(-2, -5, -5)
18 | bp = BOPFeatures.BOPFeatures(doc)
19 | cut = bp.make_cut([sphere.Label, box.Label])
20 | cut.Label = name + '_cut'
21 | cut.Placement.Base = Vector(0, 10, 0)
22 | cut.Placement.Rotation = Rotation(-42, 0, 0)
23 | lens = OpticsWorkbench.makeLens([cut])
24 | lens.Label = name
25 | return lens
26 |
27 | def makeMirror(doc, name):
28 | box = doc.addObject('Part::Box', name + '_box')
29 | box.Length = 2.0
30 | box.Placement.Base = Vector(5, -2.0, -5.0)
31 | box.Placement.Rotation = Rotation(-10, 0, 0)
32 | mirror = OpticsWorkbench.makeMirror([box])
33 | mirror.Label = name
34 | return mirror
35 |
36 | def makeRay(name):
37 | ray = OpticsWorkbench.makeRay(Vector(0, 0, 0), Vector(2.0, 1.0, 0), beamNrColumns=10, beamNrRows=10, beamDistance=0.4)
38 | ray.Placement.Base = Vector(0, -1.5, -2)
39 | ray.Label = name
40 | return ray
41 |
42 | def createRayInsideMirrorInside(doc):
43 | obj = doc.addObject('App::Part', 'ray_inside_mirror_inside')
44 | mirror = makeMirror(doc, 'mirror_a')
45 | obj.addObject(mirror)
46 | lens = makeLens(doc, 'lens_a')
47 | obj.addObject(lens)
48 | ray = makeRay('ray_a')
49 | obj.addObject(ray)
50 | obj.Placement.Base = Vector(-30.0, -30.0, 0.0)
51 | ray.MaxRayLength = 24.0
52 | return obj
53 |
54 | def createRayOutsideMirrorOutside(doc):
55 | obj = doc.addObject('App::DocumentObjectGroup', 'ray_outside_mirror_outside')
56 | mirror = makeMirror(doc, 'mirror_b')
57 | mirror.Base[0].Placement.Base += Vector(30.0, -30.0, 0.0)
58 | obj.addObject(mirror)
59 | lens = makeLens(doc, 'lens_b')
60 | lens.Base[0].Placement.Base += Vector(30.0, -30.0, 0.0)
61 | obj.addObject(lens)
62 | ray = makeRay('ray_b')
63 | ray.Placement.Base += Vector(30.0, -30.0, 0.0)
64 | obj.addObject(ray)
65 | ray.MaxRayLength = 24.0
66 | return obj
67 |
68 | def createRayInsideMirrorOutside(doc):
69 | obj = doc.addObject('App::DocumentObjectGroup', 'ray_inside_mirror_outside')
70 | obj_inside = doc.addObject('App::Part', 'ray_inside')
71 | obj_inside.Placement.Base = Vector(-30.0, 30.0, 0.0)
72 | obj.addObject(obj_inside)
73 | mirror = makeMirror(doc, 'mirror_c')
74 | mirror.Base[0].Placement.Base += Vector(-30.0, 30.0, 0.0)
75 | obj.addObject(mirror)
76 | lens = makeLens(doc, 'lens_c')
77 | lens.Base[0].Placement.Base += Vector(-30.0, 30.0, 0.0)
78 | obj.addObject(lens)
79 | ray = makeRay('ray_c')
80 | obj_inside.addObject(ray)
81 | ray.MaxRayLength = 24.0
82 | return obj
83 |
84 | def createRayOutsideMirrorInside(doc):
85 | obj = doc.addObject('App::DocumentObjectGroup', 'ray_outside_mirror_inside')
86 | obj_inside = doc.addObject('App::Part', 'mirror_inside')
87 | obj_inside.Placement.Base = Vector(30.0, 30.0, 0.0)
88 | obj.addObject(obj_inside)
89 | mirror = makeMirror(doc, 'mirror_d')
90 | obj_inside.addObject(mirror)
91 | lens = makeLens(doc, 'lens_d')
92 | obj_inside.addObject(lens)
93 | ray = makeRay('ray_d')
94 | ray.Placement.Base += Vector(30.0, 30.0, 0.0)
95 | obj.addObject(ray)
96 | ray.MaxRayLength = 24.0
97 | return obj
98 |
99 | def make_optics():
100 | App.newDocument()
101 | doc = App.activeDocument()
102 |
103 | createRayInsideMirrorInside(doc)
104 | createRayOutsideMirrorOutside(doc)
105 | createRayInsideMirrorOutside(doc)
106 | createRayOutsideMirrorInside(doc)
107 |
108 | doc.recompute()
109 |
110 | class ExampleHierarchy3D():
111 | '''This class will be loaded when the workbench is activated in FreeCAD. You must restart FreeCAD to apply changes in this class'''
112 |
113 | def Activated(self):
114 | make_optics()
115 | Gui.runCommand('Std_OrthographicCamera',1)
116 | Gui.SendMsgToActiveView("ViewFit")
117 |
118 | def IsActive(self):
119 | return(True)
120 |
121 | def GetResources(self):
122 | '''Return the icon which will appear in the tree view. This method is optional and if not defined a default icon is shown.'''
123 | return {'Pixmap' : os.path.join(_icondir_, 'optics_workbench_icon.svg'),
124 | 'Accel' : "", # a default shortcut (optional)
125 | 'MenuText': _exname_,
126 | 'ToolTip' : '' }
127 |
128 | Gui.addCommand('ExampleHierarchy3D', ExampleHierarchy3D())
129 |
--------------------------------------------------------------------------------
/examples/example_semi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/example_semi.png
--------------------------------------------------------------------------------
/examples/example_semi.py:
--------------------------------------------------------------------------------
1 | from FreeCAD import Vector, Placement, Rotation
2 | import Sketcher
3 | import Part
4 | import FreeCAD as App
5 | import FreeCADGui as Gui
6 | import OpticsWorkbench
7 | import os
8 | from PySide.QtCore import QT_TRANSLATE_NOOP
9 |
10 | _icondir_ = os.path.join(os.path.dirname(__file__), '..')
11 | _exname_ = QT_TRANSLATE_NOOP('ExampleSemi', 'Example - Semi transparent')
12 |
13 | def createSketch_Sketch(doc):
14 | Sketch = doc.addObject('Sketcher::SketchObject', 'Sketch')
15 | geo0 = Sketch.addGeometry(Part.LineSegment(Vector (17.57187, 4.200229, 0.0), Vector (3.394269999999999, -5.662041000000004, 0.0)))
16 | Sketch.AttacherEngine = 'Engine Plane'
17 | return Sketch
18 |
19 | def createSketch_Sketch002(doc):
20 | Sketch002 = doc.addObject('Sketcher::SketchObject', 'Sketch002')
21 | geo0 = Sketch002.addGeometry(Part.LineSegment(Vector (35.877303000000005, 5.425019999999973, 0.0), Vector (49.675442217789794, -5.392686170185302, 0.0)))
22 | geo1 = Sketch002.addGeometry(Part.ArcOfCircle(Part.Circle(Vector(36.78444246898194, -7.626625831826473, 0.0), Vector (0.0, 0.0, 1.0), 13.083132688145355), 0.1715903905166062, 1.640188568091698))
23 | Sketch002.addConstraint(Sketcher.Constraint('Coincident', geo1, 2, geo0, 1))
24 | Sketch002.addConstraint(Sketcher.Constraint('Coincident', geo1, 1, geo0, 2))
25 | Sketch002.AttacherEngine = 'Engine Plane'
26 | return Sketch002
27 |
28 | def createSketch_Sketch003(doc):
29 | Sketch003 = doc.addObject('Sketcher::SketchObject', 'Sketch003')
30 | geo0 = Sketch003.addGeometry(Part.LineSegment(Vector (-5.804982, 35.19952, 0.0), Vector (-5.804982, -28.56389, 0.0)))
31 | geo1 = Sketch003.addGeometry(Part.LineSegment(Vector (-5.804982, -28.56389, 0.0), Vector (72.434807, -28.56389, 0.0)))
32 | geo2 = Sketch003.addGeometry(Part.LineSegment(Vector (72.434807, -28.56389, 0.0), Vector (72.434807, 35.19952, 0.0)))
33 | geo3 = Sketch003.addGeometry(Part.LineSegment(Vector (72.434807, 35.19952, 0.0), Vector (-5.804981999999995, 35.19952, 0.0)))
34 | Sketch003.addConstraint(Sketcher.Constraint('Coincident', geo0, 2, geo1, 1))
35 | Sketch003.addConstraint(Sketcher.Constraint('Coincident', geo1, 2, geo2, 1))
36 | Sketch003.addConstraint(Sketcher.Constraint('Coincident', geo2, 2, geo3, 1))
37 | Sketch003.addConstraint(Sketcher.Constraint('Coincident', geo3, 2, geo0, 1))
38 | Sketch003.addConstraint(Sketcher.Constraint('Vertical', geo0))
39 | Sketch003.addConstraint(Sketcher.Constraint('Vertical', geo2))
40 | Sketch003.addConstraint(Sketcher.Constraint('Horizontal', geo1))
41 | Sketch003.addConstraint(Sketcher.Constraint('Horizontal', geo3))
42 | Sketch003.AttacherEngine = 'Engine Plane'
43 | Sketch003.ViewObject.LineColor = (0.0, 0.0, 0.0, 0.0)
44 | Sketch003.ViewObject.LineColorArray = [(0.0, 0.0, 0.0, 0.0)]
45 | return Sketch003
46 |
47 |
48 | def make_semi():
49 | App.newDocument(_exname_)
50 | doc = App.activeDocument()
51 |
52 | Sketch = createSketch_Sketch(doc)
53 | Mirror_semi40 = OpticsWorkbench.makeMirror([Sketch], True, 40)
54 | Mirror_semi40.Label = "Mirror_semi40"
55 |
56 | Cube = doc.addObject('Part::Box', 'Cube')
57 | Cube.Height = 2.0
58 | Cube.Length = 2.0
59 | Cube.Placement = Placement(Vector(27.0, -5.0, -1.0), Rotation (0.0, 0.0, 0.0, 1.0))
60 | Cube.ViewObject.Transparency = 50
61 | Absorber_semi50 = OpticsWorkbench.makeAbsorber([Cube], True, 50)
62 | Absorber_semi50.Label = "Absorber_semi50"
63 |
64 | Sketch002 = createSketch_Sketch002(doc)
65 | Lens_semi80 = OpticsWorkbench.makeLens([Sketch002], material='NBK7/Window glass', collectStatistics=True, transparency=80)
66 | Lens_semi80.Label = "Lens_semi80"
67 |
68 | Sketch003 = createSketch_Sketch003(doc)
69 | FullAbsorber = OpticsWorkbench.makeAbsorber([Sketch003], True, 0)
70 | FullAbsorber.Label = "FullAbsorber"
71 |
72 | doc.recompute()
73 | OpticsWorkbench.makeRay(beamNrColumns=3, beamDistance = 0.2)
74 | doc.recompute()
75 | OpticsWorkbench.Hits2CSV()
76 | doc.recompute()
77 | Gui.ActiveDocument=doc
78 |
79 |
80 | class ExampleSemi():
81 | '''This class will be loaded when the workbench is activated in FreeCAD. You must restart FreeCAD to apply changes in this class'''
82 |
83 | def Activated(self):
84 | make_semi()
85 | Gui.runCommand('Std_OrthographicCamera',1)
86 | Gui.SendMsgToActiveView("ViewFit")
87 |
88 | def IsActive(self):
89 | return(True)
90 |
91 | def GetResources(self):
92 | '''Return the icon which will appear in the tree view. This method is optional and if not defined a default icon is shown.'''
93 | return {'Pixmap' : os.path.join(_icondir_, 'optics_workbench_icon.svg'),
94 | 'Accel' : "", # a default shortcut (optional)
95 | 'MenuText': _exname_,
96 | 'ToolTip' : '' }
97 |
98 | Gui.addCommand('ExampleSemi', ExampleSemi())
--------------------------------------------------------------------------------
/examples/plot3Dexample1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/plot3Dexample1.png
--------------------------------------------------------------------------------
/examples/plot3Dexample2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/plot3Dexample2.png
--------------------------------------------------------------------------------
/examples/screenshot3D.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/screenshot3D.png
--------------------------------------------------------------------------------
/examples/simple_reflection_grating_set_of_planes.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/simple_reflection_grating_set_of_planes.PNG
--------------------------------------------------------------------------------
/examples/simple_transmission_grating.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/examples/simple_transmission_grating.PNG
--------------------------------------------------------------------------------
/icons/Anonymous_Lightbulb_Lit.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
152 |
--------------------------------------------------------------------------------
/icons/Anonymous_Lightbulb_Off.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
125 |
--------------------------------------------------------------------------------
/icons/ExportCSV.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
150 |
--------------------------------------------------------------------------------
/icons/absorber.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
119 |
--------------------------------------------------------------------------------
/icons/emitter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
187 |
--------------------------------------------------------------------------------
/icons/grating.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
171 |
--------------------------------------------------------------------------------
/icons/lens.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
182 |
--------------------------------------------------------------------------------
/icons/mirror.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
123 |
--------------------------------------------------------------------------------
/icons/ray.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
126 |
--------------------------------------------------------------------------------
/icons/rayarray.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
234 |
--------------------------------------------------------------------------------
/icons/raygridfocal.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
68 |
--------------------------------------------------------------------------------
/icons/raysun.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
139 |
--------------------------------------------------------------------------------
/icons/scatter3D.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
153 |
--------------------------------------------------------------------------------
/icons/sun.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
178 |
--------------------------------------------------------------------------------
/icons/sun3D.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
182 |
--------------------------------------------------------------------------------
/package.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Optics
4 | Geometrical optics for FreeCAD. Performs simple raytracing through your FreeCAD objects.
5 | 1.2.2
6 | 2025-06-01
7 | Christi
8 | LGPL-2.1
9 | https://github.com/chbergmann/OpticsWorkbench
10 | https://github.com/chbergmann/OpticsWorkbench/README.md
11 | optics_workbench_icon.svg
12 |
13 |
14 |
15 | Optics
16 | OpticsWorkbench
17 | ./
18 | 0.19.0
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/translations/OpticsWorkbench_de.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/translations/OpticsWorkbench_de.qm
--------------------------------------------------------------------------------
/translations/OpticsWorkbench_es-AR.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/translations/OpticsWorkbench_es-AR.qm
--------------------------------------------------------------------------------
/translations/OpticsWorkbench_es-ES.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/translations/OpticsWorkbench_es-ES.qm
--------------------------------------------------------------------------------
/translations/OpticsWorkbench_zh-TW.qm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chbergmann/OpticsWorkbench/e0da97cde9e661ef3df7195483a5a0f58d48df54/translations/OpticsWorkbench_zh-TW.qm
--------------------------------------------------------------------------------
/translations/update_translation.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # --------------------------------------------------------------------------------------------------
4 | #
5 | # Create, update and release translation files.
6 | #
7 | # Supported locales on FreeCAD <2024-11-28, FreeCADGui.supportedLocales(), total=44>:
8 | # {'English': 'en', 'Afrikaans': 'af', 'Arabic': 'ar', 'Basque': 'eu', 'Belarusian': 'be',
9 | # 'Bulgarian': 'bg', 'Catalan': 'ca', 'Chinese Simplified': 'zh-CN',
10 | # 'Chinese Traditional': 'zh-TW', 'Croatian': 'hr', 'Czech': 'cs', 'Danish': 'da',
11 | # 'Dutch': 'nl', 'Filipino': 'fil', 'Finnish': 'fi', 'French': 'fr', 'Galician': 'gl',
12 | # 'Georgian': 'ka', 'German': 'de', 'Greek': 'el', 'Hungarian': 'hu', 'Indonesian': 'id',
13 | # 'Italian': 'it', 'Japanese': 'ja', 'Kabyle': 'kab', 'Korean': 'ko', 'Lithuanian': 'lt',
14 | # 'Norwegian': 'no', 'Polish': 'pl', 'Portuguese': 'pt-PT', 'Portuguese, Brazilian': 'pt-BR',
15 | # 'Romanian': 'ro', 'Russian': 'ru', 'Serbian': 'sr', 'Serbian, Latin': 'sr-CS', 'Slovak': 'sk',
16 | # 'Slovenian': 'sl', 'Spanish': 'es-ES', 'Spanish, Argentina': 'es-AR', 'Swedish': 'sv-SE',
17 | # 'Turkish': 'tr', 'Ukrainian': 'uk', 'Valencian': 'val-ES', 'Vietnamese': 'vi'}
18 | #
19 | # NOTE: PREPARATION
20 | # - Install Qt tools
21 | # Debian-based (e.g., Ubuntu): $ sudo apt-get install qttools5-dev-tools pyqt6-dev-tools
22 | # Fedora-based: $ sudo dnf install qt6-linguist qt6-devel
23 | # Arch-based: $ sudo pacman -S qt6-tools python-pyqt6
24 | # - Make the script executable
25 | # $ chmod +x update_translation.sh
26 | # - The script has to be executed within the `translations` directory.
27 | # Executing the script with no flags invokes the help.
28 | # $ ./update_translation.sh
29 | #
30 | # NOTE: WORKFLOW TRANSLATOR (LOCAL)
31 | # - Execute the script passing the `-u` flag plus locale code as argument
32 | # Only update the file(s) you're translating!
33 | # $ ./update_translation.sh -u es-ES
34 | # - Do the translation via Qt Linguist and use `File>Release`
35 | # - If releasing with the script execute it passing the `-r` flag
36 | # plus locale code as argument
37 | # $ ./update_translation.sh -r es-ES
38 | #
39 | # NOTE: WORKFLOW MAINTAINER (CROWDIN)
40 | # - Execute the script passing the '-U' flag
41 | # $ ./update_translation.sh -U
42 | # - Once done, download the translated files, copy them to `translations`
43 | # - Upload the updated file to CrowdIn and wait for translators do their thing ;-)
44 | # and release all the files to update the changes
45 | # $ ./update_translation.sh -R
46 | #
47 | # --------------------------------------------------------------------------------------------------
48 |
49 | supported_locales=(
50 | "en" "af" "ar" "eu" "be" "bg" "ca" "zh-CN" "zh-TW" "hr"
51 | "cs" "da" "nl" "fil" "fi" "fr" "gl" "ka" "de" "el"
52 | "hu" "id" "it" "ja" "kab" "ko" "lt" "no" "pl" "pt-PT"
53 | "pt-BR" "ro" "ru" "sr" "sr-CS" "sk" "sl" "es-ES" "es-AR" "sv-SE"
54 | "tr" "uk" "val-ES" "vi"
55 | )
56 |
57 | is_locale_supported() {
58 | local locale="$1"
59 | for supported_locale in "${supported_locales[@]}"; do
60 | [ "$supported_locale" == "$locale" ] && return 0
61 | done
62 | return 1
63 | }
64 |
65 | update_locale() {
66 | local locale="$1"
67 | local u=${locale:+_} # Conditional underscore
68 | FILES="../*.py"
69 |
70 | # NOTE: Execute the right command depending on:
71 | # - if it's a locale file or the main, agnostic one
72 | [ ! -f "${WB}${u}${locale}.ts" ] && action="Creating" || action="Updating"
73 | echo -e "\033[1;34m\n\t<<< ${action} '${WB}${u}${locale}.ts' file >>>\n\033[m"
74 | if [ "$u" == "" ]; then
75 | eval $LUPDATE "$FILES" -ts "${WB}.ts" # locale-agnostic file
76 | else
77 | eval $LUPDATE "$FILES" -source-language en_US -target-language "${locale//-/_}" \
78 | -ts "${WB}_${locale}.ts"
79 | fi
80 | }
81 |
82 | help() {
83 | echo -e "\nDescription:"
84 | echo -e "\tCreate, update and release translation files."
85 | echo -e "\nUsage:"
86 | echo -e "\t./update_translation.sh [-R] [-U] [-r ] [-u ]"
87 | echo -e "\nFlags:"
88 | echo -e " -R\n\tRelease all translations (qm files)"
89 | echo -e " -U\n\tUpdate all translations (ts files)"
90 | echo -e " -r \n\tRelease the specified locale"
91 | echo -e " -u \n\tUpdate strings for the specified locale"
92 | }
93 |
94 | # Main function ------------------------------------------------------------------------------------
95 |
96 | LUPDATE=/usr/lib/qt6/bin/lupdate # from Qt6
97 | # LUPDATE=lupdate # from Qt5
98 | LRELEASE=/usr/lib/qt6/bin/lrelease # from Qt6
99 | # LRELEASE=lrelease # from Qt5
100 | WB="OpticsWorkbench"
101 |
102 | sed -i '3s/-/_/' ${WB}*.ts # Enforce underscore on locales
103 | sed -i '3s/\"en\"/\"en_US\"/g' ${WB}*.ts # Use en_US
104 |
105 | if [ $# -eq 1 ]; then
106 | if [ "$1" == "-R" ]; then
107 | find . -type f -name '*_*.ts' | while IFS= read -r file; do
108 | # Release all locales
109 | $LRELEASE -nounfinished "$file"
110 | echo
111 | done
112 | elif [ "$1" == "-U" ]; then
113 | for locale in "${supported_locales[@]}"; do
114 | update_locale "$locale"
115 | done
116 | elif [ "$1" == "-u" ]; then
117 | update_locale # Update main file (agnostic)
118 | else
119 | help
120 | fi
121 | elif [ $# -eq 2 ]; then
122 | LOCALE="$2"
123 | if is_locale_supported "$LOCALE"; then
124 | if [ "$1" == "-r" ]; then
125 | # Release locale (creation of *.qm file from *.ts file)
126 | $LRELEASE -nounfinished "${WB}_${LOCALE}.ts"
127 | elif [ "$1" == "-u" ]; then
128 | # Update main & locale files
129 | update_locale
130 | update_locale "$LOCALE"
131 | fi
132 | else
133 | echo "Verify your language code. Case sensitive."
134 | echo "If it's correct, ask a maintainer to add support for your language on FreeCAD."
135 | echo -e "\nSupported locales, '\033[1;34mFreeCADGui.supportedLocales()\033[m': \033[1;33m"
136 | for locale in $(printf "%s\n" "${supported_locales[@]}" | sort); do
137 | echo -n "$locale "
138 | done
139 | echo
140 | fi
141 | else
142 | help
143 | fi
144 |
--------------------------------------------------------------------------------
/update_translations.sh:
--------------------------------------------------------------------------------
1 | for f in $(ls translations/*.ts); do
2 | pylupdate5 *.py examples/*.py -ts $f
3 | done
--------------------------------------------------------------------------------
/wavelength_to_rgb/.gitattributes:
--------------------------------------------------------------------------------
1 | *.py filter=kw
2 | *.c filter=kw
3 | *.sh filter=kw
4 |
5 |
--------------------------------------------------------------------------------
/wavelength_to_rgb/README.rst:
--------------------------------------------------------------------------------
1 | Converting visible wavelengths to (r, g, b)
2 | ###########################################
3 |
4 | :date: 2017-09-17
5 | :tags: python, wavelength, RGB
6 | :author: Roland Smith
7 |
8 | .. Last modified: 2018-04-17T19:50:43+0200
9 |
10 | The ``rgb`` module contains a function that converts from a wavelength in nm
11 | to an 3-tuple of (R,G,B) values, each in the range 0--255.
12 |
13 | .. PELICAN_END_SUMMARY
14 |
15 | License
16 | -------
17 |
18 | The ``gentable.py`` script is licensed under the MIT licensce.
19 | Its output ``rgb.py`` is automatically generated and thus not copyrightable.
20 |
21 |
22 | Notes
23 | -----
24 |
25 | The algorithm is based on `Dan Bruton's work`_. The first time I came across
26 | it was in a Pascal translation of the original Fortran code.
27 |
28 | .. _Dan Bruton's work: http://www.physics.sfasu.edu/astro/color/spectra.html
29 |
30 | Since the conversion uses a fixed function over a relatively small range of
31 | values, I pre-compute the results for every integer value of the wavelength
32 | between 380 and 780 nm and record them in a ``bytes`` object. Three bytes are
33 | used for a single wavelength in the sequence red, green, blue.
34 | This ``bytes`` object is compressed with zlib and then encoded in base64.
35 |
36 | The code is generated as follows.
37 |
38 | .. code-block:: console
39 |
40 | python3 gentable.py > rgb.py
41 |
42 | The compression reduces the amount of data approximately by half. Encoding
43 | adds to the length, but the encoded string is significantly shorter than the
44 | compressed data in string form.
45 |
46 | .. code-block:: python
47 |
48 | In [1]: from gentable import wavelen2rgb
49 |
50 | In [2]: clrs = []
51 |
52 | In [3]: for wl in range(380, 781):
53 | ...: clrs += wavelen2rgb(wl)
54 | ...:
55 |
56 | In [4]: raw = bytes(clrs)
57 |
58 | In [5]: len(raw)
59 | Out[5]: 1203
60 |
61 | In [6]: import zlib
62 |
63 | In [7]: compressed = zlib.compress(raw, 9)
64 |
65 | In [8]: len(compressed)
66 | Out[8]: 653
67 |
68 | In [9]: import base64
69 |
70 | In [10]: enc = base64.b64encode(compressed)
71 |
72 | In [11]: len(enc)
73 | Out[11]: 872
74 |
75 | In [12]: len(str(compressed))
76 | Out[12]: 1811
77 |
78 | In [13]: len(str(enc))
79 | Out[13]: 875
80 |
81 | There is a one-time cost for decoding the table. As can be seen below, this
82 | cost is not very high.
83 |
84 | .. code-block:: python
85 |
86 | In [14]: %timeit -r 1 -n 1 zlib.decompress(base64.b64decode(enc))
87 | 1 loop, best of 1: 132 µs per loop
88 |
89 | The table look-up is significantly faster than doing the conversion directly.
90 |
91 | .. code-block:: python
92 |
93 | In [1]: from rgb import rgb
94 |
95 | In [2]: %timeit [rgb(n) for n in range(400, 501)]
96 | 1000 loops, best of 3: 205 µs per loop
97 |
98 | In [3]: from gentable import wavelen2rgb
99 |
100 | In [4]: %timeit [wavelen2rgb(n) for n in range(400, 501)]
101 | 1000 loops, best of 3: 592 µs per loop
102 |
103 |
104 |
--------------------------------------------------------------------------------
/wavelength_to_rgb/gentable.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # file: gentable.py
3 | # vim:fileencoding=utf-8:fdm=marker:ft=python
4 | #
5 | # Copyright © 2018 R.F. Smith .
6 | # SPDX-License-Identifier: MIT
7 | # Created: 2017-09-17T14:01:14+0200
8 | # Last modified: 2018-07-08T12:53:20+0200
9 | '''Generates a Python table to convert wavelenths in nm to (R,G,B) tuples.'''
10 |
11 | import base64
12 | import zlib
13 |
14 |
15 | def wavelen2rgb(nm):
16 | """
17 | Converts a wavelength between 380 and 780 nm to an RGB color tuple.
18 |
19 | Argument:
20 | nm: Wavelength in nanometers.
21 |
22 | Returns:
23 | a 3-tuple (red, green, blue) of integers in the range 0-255.
24 | """
25 |
26 | def adjust(color, factor):
27 | if color < 0.01:
28 | return 0
29 | max_intensity = 255
30 | gamma = 0.80
31 | rv = int(round(max_intensity * (color * factor)**gamma))
32 | if rv < 0:
33 | return 0
34 | if rv > max_intensity:
35 | return max_intensity
36 | return rv
37 |
38 | if nm < 380 or nm > 780:
39 | raise ValueError('wavelength out of range')
40 | red = 0.0
41 | green = 0.0
42 | blue = 0.0
43 | # Calculate intensities in the different wavelength bands.
44 | if nm < 440:
45 | red = -(nm - 440.0) / (440.0 - 380.0)
46 | blue = 1.0
47 | elif nm < 490:
48 | green = (nm - 440.0) / (490.0 - 440.0)
49 | blue = 1.0
50 | elif nm < 510:
51 | green = 1.0
52 | blue = -(nm - 510.0) / (510.0 - 490.0)
53 | elif nm < 580:
54 | red = (nm - 510.0) / (580.0 - 510.0)
55 | green = 1.0
56 | elif nm < 645:
57 | red = 1.0
58 | green = -(nm - 645.0) / (645.0 - 580.0)
59 | else:
60 | red = 1.0
61 | # Let the intensity fall off near the vision limits.
62 | if nm < 420:
63 | factor = 0.3 + 0.7 * (nm - 380.0) / (420.0 - 380.0)
64 | elif nm < 701:
65 | factor = 1.0
66 | else:
67 | factor = 0.3 + 0.7 * (780.0 - nm) / (780.0 - 700.0)
68 | # Return the calculated values in an (R,G,B) tuple.
69 | return (adjust(red, factor), adjust(green, factor), adjust(blue, factor))
70 |
71 |
72 | def binclrs():
73 | clrs = []
74 | for wl in range(380, 781):
75 | clrs += wavelen2rgb(wl)
76 | raw = bytes(clrs)
77 | return base64.b64encode(zlib.compress(raw, 9))
78 |
79 |
80 | def split_len(seq, length):
81 | return [seq[i:i + length] for i in range(0, len(seq), length)]
82 |
83 |
84 | def bintable():
85 | pieces = split_len(binclrs(), 65)
86 | start = pieces.pop(0)
87 | start = f'_ctbl = {start}'
88 | pieces = [f' {p}' for p in pieces]
89 | pieces.insert(0, start)
90 | hdr = """# This code has been automatically generated by the gentable.py script.
91 | # You can find this script at https://github.com/rsmith-nl/wavelength_to_rgb
92 | # The algorithm is based on Dan Bruton's work in
93 | # http://www.physics.sfasu.edu/astro/color/spectra.html
94 | """
95 | print(hdr)
96 | print('import base64')
97 | print('import zlib\n')
98 | print(' \\\n'.join(pieces))
99 | print('_ctbl = zlib.decompress(base64.b64decode(_ctbl))')
100 | fs = r'''
101 |
102 | def rgb(nm):
103 | """
104 | Converts a wavelength between 380 and 780 nm to an RGB color tuple.
105 |
106 | Argument:
107 | nm: Wavelength in nanometers. It is rounded to the nearest integer.
108 |
109 | Returns:
110 | a 3-tuple (red, green, blue) of integers in the range 0-255.
111 | """
112 | nm = int(round(nm))
113 | if nm < 380 or nm > 780:
114 | raise ValueError('wavelength out of range')
115 | nm = (nm - 380) * 3
116 | return _ctbl[nm:nm + 3]'''
117 | print(fs)
118 |
119 |
120 | if __name__ == '__main__':
121 | bintable()
122 |
--------------------------------------------------------------------------------
/wavelength_to_rgb/rgb.py:
--------------------------------------------------------------------------------
1 | # This code has been automatically generated by the gentable.py script.
2 | # You can find this script at https://github.com/rsmith-nl/wavelength_to_rgb
3 | # The algorithm is based on Dan Bruton's work in
4 | # http://www.physics.sfasu.edu/astro/color/spectra.html
5 |
6 | import base64
7 | import zlib
8 |
9 | _ctbl = b'eNrV0ltr0AUAxuHn1rqoiAqSiA6EEJ3ogBUJeeFFRUdJOjOyWau1bLZpGztnM1oyy' \
10 | b'2yYmcmMETPNlixNVpmssobKMJuYDVnNGksz0zTe5C9BXyF4v8Dz4y1RMlPpLGVlKs' \
11 | b'pVVqh+Vu0cDVVa5mqdp61Ge63FdTrqLWuwolFno64m3U3WNutp1ttsY7O+Jpub9Df' \
12 | b'a2migwY56O+sM1dpTY3iekblGq4zNcWC2QxWOlDtWJqVSIg/JfTJd7pRbZZpMlZtk' \
13 | b'slwtl8skuUjOk3PkDDlFnNipcWZMjAtjUlwR18aNcXNMi9virpgRD0ZJlMZTMTuqo' \
14 | b'ibqoyUWxCuxKJbEm/F2dEZXrI4PYn1siL7YHP3xTWyLwfg+9sRwjMT+GI/f4884Fj' \
15 | b'mxP2S/7JVB+Vr6pEe65C1ZJC9KjTxduKcX1smF74TMhDgtzopz4/y4uGBdFlfFdXF' \
16 | b'DTImpBe6WuD3ujnvj/ni4ID4WTxTKZ6IyqgtoXTTGC9EaL8fCgvt6dPwrXhmrCnR3' \
17 | b'rIl18VH0xicF/fPYEl/G1hiI7UWA72KoaPBj7Iufigxj8VtR4nAcjeMnYxyXo3JYD' \
18 | b'sq4/CqjMiLD8oPsll1Fp+0yUNTqly/kU9kkG2S9fChrpLtIuErekeWyVN6Q16Rd2m' \
19 | b'SBzJcmqSvqVkulVMiTMkselUfkAZkh98gd/znZFLlerpEr5VK5RC6QiXK2nC4TTv7' \
20 | b'sf7C/OcZfHOEwhzjIAcYZ4xdG+ZkR9jHMXvawmyF2sZNBdrCNAb5lK1/RzxY28xl9' \
21 | b'bGIjH9PLenpYx1rep5v36OJdOlnJCpazjKV0sITFvEo7C2njJVqZTwtNNFBHLc9Tz' \
22 | b'XNUMpsKyinjcUqZSQn/AJ7p9HY='
23 | _ctbl = zlib.decompress(base64.b64decode(_ctbl))
24 |
25 |
26 | def rgb(nm):
27 | """
28 | Converts a wavelength between 380 and 780 nm to an RGB color tuple.
29 |
30 | Argument:
31 | nm: Wavelength in nanometers. It is rounded to the nearest integer.
32 |
33 | Returns:
34 | a 3-tuple (red, green, blue) of integers in the range 0-255.
35 | """
36 | nm = int(round(nm))
37 | if nm < 380 or nm > 780:
38 | raise ValueError('wavelength out of range')
39 | nm = (nm - 380) * 3
40 | return _ctbl[nm:nm + 3]
41 |
--------------------------------------------------------------------------------