├── tools ├── release_screenshots │ ├── screenshots │ │ └── README.md │ ├── install_tools.sh │ ├── README.md │ └── getscreenshots.sh ├── ideal_luneburg_maker │ ├── clean_models.sh │ ├── Dockerfile │ ├── README.md │ ├── docker-compose.yml │ └── src │ │ ├── ideal_lens.py │ │ └── modules │ │ ├── permitivity_solver.py │ │ └── stl_generator_pymesh.py └── permitivity_calculator │ ├── example.png │ └── effective.py ├── clean_models.sh ├── .gitattributes ├── src └── app │ ├── static │ ├── docs │ │ └── Docs.md │ ├── favicon.ico │ ├── LuneForge.png │ ├── build │ │ ├── stlwebviewer2.css │ │ └── stlwebviewer2.js │ └── index.html │ ├── Dockerfile │ ├── templates │ ├── materials.html │ ├── no_builds.html │ ├── containers.html │ ├── documentation.html │ ├── builds.html │ └── render.html │ ├── modules │ ├── release_info.py │ ├── geometry.py │ ├── plotter.py │ ├── permitivity_solver.py │ ├── lens_builder.py │ ├── unit_cells.py │ └── stl_generator_pymesh.py │ └── luneforge.py ├── .gitignore ├── docker-compose.yml ├── .github └── workflows │ └── docker-image.yml ├── LICENSE.md └── README.md /tools/release_screenshots/screenshots/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /clean_models.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | rm -f models/* -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored 2 | *.py linguist-vendored=false 3 | -------------------------------------------------------------------------------- /src/app/static/docs/Docs.md: -------------------------------------------------------------------------------- 1 | **Documentation:** 2 | 3 | Coming soon... -------------------------------------------------------------------------------- /tools/ideal_luneburg_maker/clean_models.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | rm -f models/*.obj 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | models 2 | tools/release_screenshots/webscreenshot 3 | tools/release_screenshots/screenshots/*.png -------------------------------------------------------------------------------- /src/app/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jboirazian/LuneForge/HEAD/src/app/static/favicon.ico -------------------------------------------------------------------------------- /src/app/static/LuneForge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jboirazian/LuneForge/HEAD/src/app/static/LuneForge.png -------------------------------------------------------------------------------- /tools/release_screenshots/install_tools.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | git clone https://github.com/maaaaz/webscreenshot -------------------------------------------------------------------------------- /tools/permitivity_calculator/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jboirazian/LuneForge/HEAD/tools/permitivity_calculator/example.png -------------------------------------------------------------------------------- /tools/ideal_luneburg_maker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM pymesh/pymesh:py3.7-slim 2 | WORKDIR /app 3 | COPY src /app 4 | CMD ["python3","-u","ideal_lens.py","100","20"] 5 | -------------------------------------------------------------------------------- /src/app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM pymesh/pymesh:py3.7-slim 2 | RUN pip install Flask==2.2.5 3 | RUN pip install Markdown==3.4.4 4 | WORKDIR /app 5 | COPY src/app /app 6 | CMD ["python3","-u","luneforge.py"] -------------------------------------------------------------------------------- /tools/ideal_luneburg_maker/README.md: -------------------------------------------------------------------------------- 1 | # Ideal Luneburg maker 2 | 3 | A simple CLI tool to build ideal luneburg lenses for debugging and generating lenses to be imported in CST Studio 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tools/ideal_luneburg_maker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | ideal_luneburg_maker: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | volumes: 8 | - ./models:/app/models -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | services: 3 | luneforge: 4 | build: 5 | context: . 6 | dockerfile: src/app/Dockerfile 7 | volumes: 8 | - ./models:/app/static/models 9 | network_mode: host -------------------------------------------------------------------------------- /tools/release_screenshots/README.md: -------------------------------------------------------------------------------- 1 | # Dev tools for LuneForge 2 | 3 | 4 | + Webscreenshot by maaaaz For generating Screenshots: https://github.com/maaaaz/webscreenshot 5 | 6 | 7 | 8 | to install all the tools run ./install_tools.sh -------------------------------------------------------------------------------- /src/app/templates/materials.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/app/templates/no_builds.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tools/release_screenshots/getscreenshots.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | CMD="webscreenshot/webscreenshot.py -r chrome -t 60 -q 100 --window-size '1920,1080'" 4 | BASE_URL="http://127.0.0.1:5000" 5 | 6 | # List of paths to append to the base URL 7 | URLS=( 8 | "" 9 | "/builds" 10 | "/docs" 11 | "/render?model_uuid=0c7843ad-d284-4151-a77f-782127ef152e" 12 | ) 13 | 14 | # Loop through the URLs and run the command 15 | for path in "${URLS[@]}" 16 | do 17 | python3 $CMD "$BASE_URL$path" 18 | done -------------------------------------------------------------------------------- /src/app/modules/release_info.py: -------------------------------------------------------------------------------- 1 | 2 | RELEASE_VERSION="0.5" 3 | 4 | 5 | def show_release_info(): 6 | print(''' 7 | # # # # # ###### ###### #### ##### #### ###### 8 | # # # ## # # # # # # # # # # 9 | # # # # # # ##### ##### # # # # # ##### 10 | # # # # # # # # # # ##### # ### # 11 | # # # # ## # # # # # # # # # 12 | ####### #### # # ###### # #### # # #### ###### 13 | ''') 14 | 15 | print(f"Version : {RELEASE_VERSION}") 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/app/modules/geometry.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def generate_sphere_points(radius, num_points): 4 | points = [] 5 | 6 | for _ in range(num_points): 7 | theta = np.arccos(2 * np.random.rand() - 1) # theta: [0, pi] 8 | phi = 2 * np.pi * np.random.rand() # phi: [0, 2pi] 9 | 10 | x = radius * np.sin(theta) * np.cos(phi) 11 | y = radius * np.sin(theta) * np.sin(phi) 12 | z = radius * np.cos(theta) 13 | 14 | points.append((x, y, z)) 15 | 16 | return points 17 | 18 | 19 | 20 | def check_point_in_sphere(point, radius): 21 | x, y, z = point 22 | distance_squared = x**2 + y**2 + z**2 23 | radius_squared = radius**2 24 | 25 | if distance_squared <= radius_squared: 26 | return True 27 | else: 28 | return False 29 | -------------------------------------------------------------------------------- /.github/workflows/docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up environment variable for timestamp 19 | id: vars 20 | run: echo "TIMESTAMP=$(date +%s)" >> $GITHUB_ENV 21 | 22 | - name: Log in to Docker Hub 23 | uses: docker/login-action@v2 24 | with: 25 | username: ${{ secrets.DOCKER_USERNAME }} 26 | password: ${{ secrets.DOCKER_PASSWORD }} 27 | 28 | - name: Build the Docker image 29 | run: docker build . --file src/app/Dockerfile --tag luneforge:${{ env.TIMESTAMP }} 30 | 31 | - name: Push the Docker image to Docker Hub 32 | run: | 33 | docker tag luneforge:${{ env.TIMESTAMP }} ${{ secrets.DOCKER_USER }}/luneforge:latest 34 | docker push ${{ secrets.DOCKER_USER }}/luneforge:latest 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /src/app/modules/plotter.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | 3 | 4 | def generate_plots(name: str, data: dict): 5 | fig, axs = plt.subplots(1, 3, figsize=(15, 5)) 6 | 7 | # Plot permittivity vs Ro 8 | axs[0].plot(data["Ro_values"], data["permittivity_values"], marker='o', color='b', label="Permittivity") 9 | axs[0].set_xlabel("Ro (meters)") 10 | axs[0].set_ylabel("Permittivity (ε)") 11 | axs[0].set_title("Variation of Permittivity vs. Ro") 12 | axs[0].grid(True) 13 | axs[0].legend() 14 | 15 | # Plot material ratio vs Ro 16 | axs[1].plot(data["Ro_values"], data["f2_ratio"], marker='o', color='r', label="Material ratio") 17 | axs[1].set_xlabel("Ro (meters)") 18 | axs[1].set_ylabel("Material ratio") 19 | axs[1].set_title("Material Ratio vs. Ro") 20 | axs[1].grid(True) 21 | axs[1].legend() 22 | 23 | # Plot air ratio vs Ro 24 | axs[2].plot(data["Ro_values"], data["f1_ratio"], marker='o', color='g', label="Air ratio") 25 | axs[2].set_xlabel("Ro (meters)") 26 | axs[2].set_ylabel("Air ratio") 27 | axs[2].set_title("Air Permittivity Ratio vs. Ro") 28 | axs[2].grid(True) 29 | axs[2].legend() 30 | 31 | # Save the figure 32 | fig.savefig(f"{name}.png", format="png", dpi=300) 33 | plt.close(fig) # Close the figure to free memory if not displaying it -------------------------------------------------------------------------------- /tools/ideal_luneburg_maker/src/ideal_lens.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import modules.stl_generator_pymesh as stl_gen 3 | import numpy as np 4 | import pymesh 5 | import os 6 | import glob 7 | 8 | # Luneburg equation for required permittivity at radius R 9 | def luneburg_eq(Ro, R): 10 | return 2 - ((Ro ** 2) / (R ** 2)) 11 | 12 | 13 | if __name__ == "__main__": 14 | if(len(sys.argv)!=3): 15 | print(f"INVALID PARAMETERS RUN : {sys.argv[0]} [Lens_radious] [Layer_resoltion]") 16 | sys.exit(-1) 17 | R=float(sys.argv[1]) 18 | RESOLUTION=int(sys.argv[2]) 19 | DELTA=R/RESOLUTION 20 | LAYER=0 21 | print(f"Lens_radious: {R} | Layer_resoltion: {RESOLUTION}") 22 | for Ro in np.linspace(0,R, RESOLUTION): 23 | epsilon=luneburg_eq(Ro=Ro,R=R) 24 | if(Ro>0): 25 | solid_sphere=stl_gen.generate_sphere(radius=Ro,resolution=5) 26 | if(LAYER!=0): 27 | hollow_sphere=stl_gen.generate_sphere(radius=Ro-DELTA,resolution=5) 28 | shell_sphere=pymesh.boolean(solid_sphere, hollow_sphere, operation="difference") 29 | else: 30 | shell_sphere=solid_sphere 31 | print(f"models/{LAYER}_Ep_{epsilon}-Ro_{Ro}.obj") 32 | stl_gen.export_to_stl(mesh=shell_sphere,filename=f"models/{LAYER}_Ep_{epsilon}-Ro_{Ro}.obj") 33 | LAYER+=1 34 | 35 | -------------------------------------------------------------------------------- /src/app/modules/permitivity_solver.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.optimize import fsolve 3 | 4 | 5 | # Calculate f2 given an effective permittivity, epsilon1, and epsilon2 6 | def calculate_f2(epsilon_eff, epsilon1, epsilon2): 7 | def bruggeman_eq(f2): 8 | f1 = 1 - f2 9 | term1 = f1 * (epsilon1 - epsilon_eff) / (epsilon1 + 2 * epsilon_eff) 10 | term2 = f2 * (epsilon2 - epsilon_eff) / (epsilon2 + 2 * epsilon_eff) 11 | return term1 + term2 12 | 13 | # Initial guess for f2 14 | f2_initial_guess = 0.5 15 | f2_solution = fsolve(bruggeman_eq, f2_initial_guess)[0] 16 | 17 | # Ensure f2 is within valid bounds 18 | if 0 <= f2_solution <= 1: 19 | f1_solution = 1 - f2_solution 20 | return f2_solution, f1_solution 21 | else: 22 | raise ValueError("No valid solution for f2 in the range [0, 1]") 23 | 24 | # Luneburg equation for required permittivity at radius R 25 | def luneburg_eq(Ro, R): 26 | return 2 - ((Ro ** 2) / (R ** 2)) 27 | 28 | def get_ratio(epsilon1:float,epsilon2:float,min_epsilon:float,sphere_radius:float,center_distance:float): 29 | epsilon_Ro = luneburg_eq(Ro=center_distance, R=sphere_radius) 30 | print(f"epsilon_Ro = {epsilon_Ro}") 31 | if min_epsilon > epsilon_Ro: 32 | epsilon_Ro = min_epsilon 33 | print(f"epsilon_Ro = {epsilon_Ro} | {epsilon1} {epsilon2}") 34 | f2, f1 = calculate_f2(epsilon_eff=epsilon_Ro, epsilon1=epsilon1, epsilon2=epsilon2) 35 | return f2,f1 -------------------------------------------------------------------------------- /tools/ideal_luneburg_maker/src/modules/permitivity_solver.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.optimize import fsolve 3 | 4 | 5 | # Calculate f2 given an effective permittivity, epsilon1, and epsilon2 6 | def calculate_f2(epsilon_eff, epsilon1, epsilon2): 7 | def bruggeman_eq(f2): 8 | f1 = 1 - f2 9 | term1 = f1 * (epsilon1 - epsilon_eff) / (epsilon1 + 2 * epsilon_eff) 10 | term2 = f2 * (epsilon2 - epsilon_eff) / (epsilon2 + 2 * epsilon_eff) 11 | return term1 + term2 12 | 13 | # Initial guess for f2 14 | f2_initial_guess = 0.5 15 | f2_solution = fsolve(bruggeman_eq, f2_initial_guess)[0] 16 | 17 | # Ensure f2 is within valid bounds 18 | if 0 <= f2_solution <= 1: 19 | f1_solution = 1 - f2_solution 20 | return f2_solution, f1_solution 21 | else: 22 | raise ValueError("No valid solution for f2 in the range [0, 1]") 23 | 24 | # Luneburg equation for required permittivity at radius R 25 | def luneburg_eq(Ro, R): 26 | return 2 - ((Ro ** 2) / (R ** 2)) 27 | 28 | def get_ratio(epsilon1:float,epsilon2:float,min_epsilon:float,sphere_radius:float,center_distance:float): 29 | epsilon_Ro = luneburg_eq(Ro=center_distance, R=sphere_radius) 30 | print(f"epsilon_Ro = {epsilon_Ro}") 31 | if min_epsilon > epsilon_Ro: 32 | epsilon_Ro = min_epsilon 33 | print(f"epsilon_Ro = {epsilon_Ro} | {epsilon1} {epsilon2}") 34 | f2, f1 = calculate_f2(epsilon_eff=epsilon_Ro, epsilon1=epsilon1, epsilon2=epsilon2) 35 | return f2,f1 -------------------------------------------------------------------------------- /src/app/templates/containers.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/static/build/stlwebviewer2.css: -------------------------------------------------------------------------------- 1 | .stlwv2-model { 2 | position: relative; 3 | min-height: 20em; 4 | border: 1px solid #ccc 5 | } 6 | 7 | .stlwv2-inner { 8 | position: absolute; 9 | overflow: hidden; 10 | top: 0; 11 | bottom: 0; 12 | left: 0; 13 | right: 0 14 | } 15 | 16 | .stlwv2-inner>.stlwv2-percent { 17 | position: absolute; 18 | top: 50%; 19 | transform: translateY(-50%); 20 | font-size: 5em; 21 | text-align: center; 22 | width: 100%; 23 | color: #ccc; 24 | animation-name: stlwv2-pulse; 25 | animation-duration: 2s; 26 | animation-iteration-count: infinite 27 | } 28 | 29 | @keyframes stlwv2-pulse { 30 | 0% { 31 | opacity: 1 32 | } 33 | 34 | 50% { 35 | opacity: .625 36 | } 37 | } 38 | 39 | .stlwv2-inner>canvas { 40 | position: absolute; 41 | top: 0; 42 | left: 0; 43 | opacity: 0; 44 | transition: opacity .5s 45 | } 46 | 47 | .stlwv2-inner.stlwv2-loaded>canvas { 48 | opacity: 1 49 | } 50 | 51 | .stlwv2-fullscreen-checkbox { 52 | display: none !important 53 | } 54 | 55 | .stlwv2-hud { 56 | display: none 57 | } 58 | 59 | .stlwv2-loaded>.stlwv2-hud { 60 | position: absolute; 61 | padding: .25em; 62 | z-index: 1000; 63 | cursor: pointer; 64 | font-weight: 400 65 | } 66 | 67 | .stlwv2-loaded>.stlwv2-github-link { 68 | font-size: 1.2em; 69 | top: .57em; 70 | right: 3em; 71 | text-decoration: none; 72 | color: #999; 73 | display: none 74 | } 75 | 76 | .stlwv2-loaded>.stlwv2-fullscreen-on { 77 | font-size: 1.5em; 78 | top: 0; 79 | right: .2em; 80 | transform: rotate(90deg); 81 | color: #ccc; 82 | display: block 83 | } 84 | 85 | .stlwv2-loaded>.stlwv2-fullscreen-off { 86 | font-size: 2em; 87 | top: 0; 88 | right: .5em; 89 | color: #c33; 90 | display: none 91 | } 92 | 93 | .stlwv2-fullscreen-checkbox:checked~.stlwv2-loaded>.stlwv2-github-link { 94 | display: block 95 | } 96 | 97 | .stlwv2-fullscreen-checkbox:checked~.stlwv2-loaded>.stlwv2-fullscreen-on { 98 | display: none 99 | } 100 | 101 | .stlwv2-fullscreen-checkbox:checked~.stlwv2-loaded>.stlwv2-fullscreen-off { 102 | display: block 103 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LuneForge 2 | [![Featured on Hacker News](https://hackerbadge.now.sh/api?id=41425675)](https://news.ycombinator.com/item?id=41425675) 3 |

4 | LuneForge 5 |

6 | 7 | 8 | 9 | ## What is LuneForge? 10 | 11 | **TLDR**: an Open-Source Software for Designing 3D-Printable Luneburg Lenses for RF Applications 12 | 13 | LuneForge is a cutting-edge **work in progress** open-source tool designed for creating precise Luneburg lens models tailored for radio frequency (RF) applications, optimized for SLA 3D printing. Ideal for RF engineers, hobbyists, and researchers, LuneForge simplifies the design process with user-friendly features and customizable parameters. Enhance your RF projects with advanced lens designs, improve signal focusing and directionality, and join a community of innovators in the field of radio frequency technology. 14 | 15 | ## Screenshots 16 | 17 | ![image](https://github.com/user-attachments/assets/4bcfc710-36c8-4828-9a6e-e17417089945) 18 | 19 | 20 | 21 | 22 | https://github.com/user-attachments/assets/7037960c-ab61-4e32-aaee-4b98a8713ff3 23 | 24 | 25 | 26 | 27 | 28 | ## Features + Roadmap 29 | 30 | - [x] Lightweight and fast generation of Luneburg lenses 31 | - [x] Elegant Web base UI 32 | - [x] Export generated image to .stl file for 3D printing 33 | - [x] Export generated image to .obj file for CST studio 34 | - [ ] Release online demo 35 | - [ ] Material DRC Check 36 | - [ ] Ability to introduce multiple lattice unti cells for the lens generation 37 | - [x] DockerHub image for fast instalation 38 | - [ ] Multi Lens generation 39 | - [ ] Integration with CST Studio 40 | - [ ] And many more ! 41 | 42 | 43 | ## Get started: 44 | 45 | For the following , it is required to have [Docker](https://www.docker.com/) installed 46 | 47 | Install from Dockerhub (recommended): 48 | 49 | ```bash 50 | docker run --name luneforge -v $(pwd)/models:/app/static/models juanboirazian/luneforge 51 | 52 | ``` 53 | 54 | Install from source: 55 | 56 | ```bash 57 | git clone https://github.com/jboirazian/LuneForge 58 | ``` 59 | 60 | ```bash 61 | cd LuneForge 62 | ``` 63 | 64 | ```bash 65 | docker compose up --build 66 | ``` 67 | 68 | 69 | ## Tech Stack 70 | 71 | **Client:** HTMX, DaisyUI 72 | 73 | **Server:** Flask , Pymesh 74 | 75 | ## Contact information: 76 | 77 | - Email: jboirazian@frba.utn.edu.ar 78 | - Linkdin : https://www.linkedin.com/in/juan-boirazian/ 79 | -------------------------------------------------------------------------------- /src/app/modules/lens_builder.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import modules.geometry as geometry 3 | import modules.stl_generator_pymesh as stl_gen 4 | import modules.unit_cells as unit_cells 5 | import modules.permitivity_solver as permitivity_solver 6 | 7 | 8 | def build_cells(sphere_radius: float, epsilon: float, support_length: float): 9 | 10 | MIN_EPSILON = 1.2 11 | models = [] 12 | half_models = [] 13 | size = sphere_radius*2 14 | 15 | # We calulcate first the parameters of the cell structural integrety 16 | f2, f1 = permitivity_solver.get_ratio( 17 | epsilon1=1, epsilon2=epsilon, min_epsilon=MIN_EPSILON, sphere_radius=sphere_radius, center_distance=sphere_radius) 18 | 19 | support_side_length=unit_cells.calculate_support_side_length(f2_ratio=f2,support_length=support_length) 20 | 21 | print(f"{f2} {support_length} {support_side_length}") 22 | # for z in np.arange(-size/2, size/2, support_length): 23 | # for y in np.arange(-size/2, size/2, support_length): 24 | # for x in np.arange(-size/2, size/2, support_length): 25 | # if geometry.check_point_in_sphere(point=[x, y, z], radius=sphere_radius): 26 | # center_distance = (x**2 + y**2 + z**2)**0.5 27 | # f2, f1 = permitivity_solver.get_ratio( 28 | # epsilon1=1, epsilon2=epsilon, min_epsilon=MIN_EPSILON, sphere_radius=sphere_radius, center_distance=center_distance) 29 | 30 | # cube_side_length_adjusted = cube_side_length * \ 31 | # (1 - (((x**2 + y**2+z**2)) * 0.02)) 32 | # if (cube_side_length_adjusted >= dielectric_ratio_length): 33 | # cube_side_length_adjusted = dielectric_ratio_length 34 | # model = unit_cells.generate_cubic_unit_cell( 35 | # cubic_center=[x, y, z], 36 | # support_length=support_length, 37 | # cube_side_lenght=cube_side_length_adjusted, 38 | # support_side_length=cube_side_length 39 | # ) 40 | # models.append(model) 41 | # if x >= 0: 42 | # half_models.append(model) 43 | 44 | return models, half_models 45 | 46 | 47 | def build_models(models: list, half_models: list, models_dir: str, filename: str): 48 | 49 | k = 10 50 | 51 | model = stl_gen.merge_models(models=models) 52 | model_scaled = stl_gen.scale_model(mesh=model, scale_factor=k) 53 | 54 | half_model = stl_gen.merge_models(models=half_models) 55 | half_model_scaled = stl_gen.scale_model(mesh=half_model, scale_factor=k) 56 | stl_gen.export_to_stl( 57 | mesh=model_scaled, filename=f"{models_dir}/{filename}.stl") 58 | stl_gen.export_to_stl( 59 | mesh=model_scaled, filename=f"{models_dir}/{filename}.obj") 60 | stl_gen.export_to_stl(mesh=half_model_scaled, 61 | filename=f"{models_dir}/{filename}_cross.stl") 62 | -------------------------------------------------------------------------------- /src/app/templates/documentation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LuneForge 6 | 7 | 8 | 9 | 10 | 11 | 58 | 59 | 60 | 61 | 82 |
83 | 84 |
85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /tools/permitivity_calculator/effective.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from scipy.optimize import fsolve 4 | 5 | # Parameters 6 | D = 140e-3 # Sphere radius in meters 7 | f1 = 0.1 # Volume fraction of material 1 8 | f2 = 0.9 # Volume fraction of material 2 9 | epsilon1 = 1.0 # Permittivity of material 1 10 | epsilon2 = 2.0 # Permittivity of material 2 11 | min_epsion_Ro = 1.1 12 | axis_cell_resolution= 10 # The amout of cells that are on a single axis , must be a even number 13 | 14 | def generate_plots(name: str, data: dict): 15 | fig, axs = plt.subplots(1, 3, figsize=(15, 5)) 16 | 17 | # Plot permittivity vs Ro 18 | axs[0].plot(data["Ro_values"], data["permittivity_values"], marker='o', color='b', label="Permittivity") 19 | axs[0].set_xlabel("Ro (meters)") 20 | axs[0].set_ylabel("Permittivity (ε)") 21 | axs[0].set_title("Variation of Permittivity vs. Ro") 22 | axs[0].grid(True) 23 | axs[0].legend() 24 | 25 | # Plot material ratio vs Ro 26 | axs[1].plot(data["Ro_values"], data["f2_ratio"], marker='o', color='r', label="Material ratio") 27 | axs[1].set_xlabel("Ro (meters)") 28 | axs[1].set_ylabel("Material ratio") 29 | axs[1].set_title("Material Ratio vs. Ro") 30 | axs[1].grid(True) 31 | axs[1].legend() 32 | 33 | # Plot air ratio vs Ro 34 | axs[2].plot(data["Ro_values"], data["f1_ratio"], marker='o', color='g', label="Air ratio") 35 | axs[2].set_xlabel("Ro (meters)") 36 | axs[2].set_ylabel("Air ratio") 37 | axs[2].set_title("Air Permittivity Ratio vs. Ro") 38 | axs[2].grid(True) 39 | axs[2].legend() 40 | 41 | # Save the figure 42 | fig.savefig(f"{name}.png", format="png", dpi=300) 43 | plt.close(fig) # Close the figure to free memory if not displaying it 44 | 45 | 46 | 47 | # Calculate f2 given an effective permittivity, epsilon1, and epsilon2 48 | def calculate_f2(epsilon_eff, epsilon1, epsilon2): 49 | def bruggeman_eq(f2): 50 | f1 = 1 - f2 51 | term1 = f1 * (epsilon1 - epsilon_eff) / (epsilon1 + 2 * epsilon_eff) 52 | term2 = f2 * (epsilon2 - epsilon_eff) / (epsilon2 + 2 * epsilon_eff) 53 | return term1 + term2 54 | 55 | # Initial guess for f2 56 | f2_initial_guess = 0.5 57 | f2_solution = fsolve(bruggeman_eq, f2_initial_guess)[0] 58 | 59 | # Ensure f2 is within valid bounds 60 | if 0 <= f2_solution <= 1: 61 | f1_solution = 1 - f2_solution 62 | return f2_solution, f1_solution 63 | else: 64 | raise ValueError("No valid solution for f2 in the range [0, 1]") 65 | 66 | # Luneburg equation for required permittivity at radius R 67 | def luneburg_eq(Ro, R): 68 | return 2 - ((Ro ** 2) / (R ** 2)) 69 | 70 | # Lists to store Ro and permittivity values 71 | Ro_values = [] 72 | permittivity_values = [] 73 | f2_ratio = [] 74 | f1_ratio = [] 75 | 76 | 77 | # Calculate permittivity and f2 for each Ro in range 78 | for Ro in np.linspace(-D / 2, D / 2, axis_cell_resolution): 79 | epsilon_Ro = luneburg_eq(Ro=Ro, R=D / 2) 80 | if min_epsion_Ro > epsilon_Ro: 81 | epsilon_Ro = min_epsion_Ro 82 | f2, f1 = calculate_f2(epsilon_eff=epsilon_Ro, epsilon1=epsilon1, epsilon2=epsilon2) 83 | # Store Ro and permittivity for plotting 84 | Ro_values.append(Ro) 85 | permittivity_values.append(epsilon_Ro) 86 | f2_ratio.append(f2) 87 | f1_ratio.append(f1) 88 | 89 | 90 | data ={} 91 | data["Ro_values"]=Ro_values 92 | data["permittivity_values"]=permittivity_values 93 | data["f2_ratio"]=f2_ratio 94 | data["f1_ratio"]=f1_ratio 95 | 96 | print(f"Estimated cells {axis_cell_resolution**3}") 97 | 98 | generate_plots(name="example",data=data) 99 | 100 | -------------------------------------------------------------------------------- /src/app/templates/builds.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LuneForge 6 | 7 | 8 | 9 | 10 | 11 | 65 | 66 | 67 | 68 | 87 |
88 | 89 |
90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /src/app/luneforge.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, jsonify, render_template, send_from_directory 2 | import os 3 | import modules.stl_generator_pymesh as stl_gen 4 | import modules.lens_builder as lens_builder 5 | from modules.release_info import show_release_info 6 | import numpy as np 7 | import modules.unit_cells as unit_cells 8 | import modules.geometry as geometry 9 | import uuid 10 | from os import listdir, stat 11 | from os.path import isfile, join, getctime 12 | import datetime 13 | import json 14 | import markdown 15 | 16 | app = Flask(__name__, static_url_path='', 17 | static_folder='static', template_folder='templates') 18 | 19 | 20 | models_dir = f"{app.static_folder}/models" 21 | 22 | 23 | 24 | @app.route('/favicon.ico') 25 | def favicon(): 26 | return send_from_directory(os.path.join(app.root_path, 'static'), 27 | 'favicon.ico', mimetype='image/vnd.microsoft.icon') 28 | 29 | 30 | @app.route('/render') 31 | def render_model(): 32 | try: 33 | model_uuid = request.args.get('model_uuid', default="", type=str) 34 | return render_template("render.html", cst_studio_model_id=f"models/{model_uuid}.obj", model1_id=f"models/{model_uuid}.stl", model2_id=f"models/{model_uuid}_cross.stl"), 200 35 | except Exception as e: 36 | app.logger.error(f"Error serving render.html: {e}") 37 | return jsonify({"error": "File not found"}), 404 38 | 39 | 40 | @app.route('/docs') 41 | def show_docs(): 42 | try: 43 | return render_template("documentation.html"), 200 44 | except Exception as e: 45 | app.logger.error(f"Error serving documentation.html: {e}") 46 | return jsonify({"error": "File not found"}), 404 47 | 48 | 49 | @app.route('/builds') 50 | def show_builds(): 51 | try: 52 | return render_template("builds.html"), 200 53 | except Exception as e: 54 | app.logger.error(f"Error serving builds.html: {e}") 55 | return jsonify({"error": "File not found"}), 404 56 | 57 | 58 | @app.route('/get_builds') 59 | def get_builds(): 60 | try: 61 | 62 | files = [f for f in listdir(models_dir) if isfile( 63 | join(models_dir, f))] 64 | html_data = "" 65 | if (len(files) == 0): 66 | # No files in directory 67 | html_data = render_template("no_builds.html") 68 | return html_data, 200 69 | 70 | for file in files: 71 | if ((".stl" in file) and ('_' not in file)): 72 | id = file.replace(".stl", "") 73 | creation_time = getctime(f"{models_dir}/{file}") 74 | creation_date = datetime.datetime.fromtimestamp(creation_time) 75 | info = json.load(open(f"{models_dir}/{id}.json")) 76 | html_data += render_template("containers.html", 77 | filename=id, creation_date=creation_date, info=info) 78 | return html_data, 200 79 | except Exception as e: 80 | app.logger.error(f"Error: {e}") 81 | return jsonify({"error": "File not found"}), 404 82 | 83 | 84 | @app.route('/') 85 | def main_page(): 86 | try: 87 | return app.send_static_file("index.html") 88 | except Exception as e: 89 | app.logger.error(f"Error serving index.html: {e}") 90 | return jsonify({"error": "File not found"}), 404 91 | 92 | 93 | @app.route('/get_docs') 94 | def documentation_page(): 95 | try: 96 | with open(f"{app.static_folder}/docs/Docs.md", 'r', encoding='utf-8') as file: 97 | text = file.read() 98 | html_docs = markdown.markdown(text) 99 | return html_docs, 200 100 | except Exception as e: 101 | app.logger.error(f"Error serving docs/Docs.md: {e}") 102 | return jsonify({"error": "File not found"}), 404 103 | 104 | 105 | @app.route('/generate_sphere_mesh', methods=['POST']) 106 | def generate_sphere_mesh(): 107 | try: 108 | data = request.form 109 | filename = str(uuid.uuid4()) 110 | material=data.get('material', type=str) 111 | epsilon=float(material.split(";")[0]) 112 | tan_loss=float(material.split(";")[-1]) 113 | cube_side_length = data.get('cube_side_length', type=float) 114 | support_length = data.get('support_length', type=float) 115 | sphere_radius = data.get('sphere_radius', type=float) 116 | 117 | models, half_models = lens_builder.build_cells(sphere_radius=sphere_radius,support_length=support_length,epsilon=epsilon) 118 | 119 | lens_builder.build_models( 120 | models=models, half_models=half_models, models_dir=models_dir, filename=filename) 121 | # Save metadata of file 122 | data = {"id": filename, "cube_side_length": cube_side_length, 123 | "support_length": support_length, "sphere_radius": sphere_radius, "n_cells": len(models)} 124 | 125 | stl_gen.generate_metadata(path=models_dir, data=data) 126 | 127 | html_button = f'''''' 128 | return html_button 129 | except Exception as e: 130 | error_html = f'''
Error : Click to view

{e}

''' 131 | return error_html 132 | 133 | 134 | if __name__ == "__main__": 135 | show_release_info() 136 | app.run(host='0.0.0.0', threaded=True, port=5000, debug=False) 137 | -------------------------------------------------------------------------------- /src/app/templates/render.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | LuneForge 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 67 | 68 | 69 | 70 | 90 |
91 |
92 |
93 |
94 | 95 | 96 | 97 |
98 |
99 | 100 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /src/app/modules/unit_cells.py: -------------------------------------------------------------------------------- 1 | import modules.stl_generator_pymesh as stl_gen 2 | import pymesh 3 | import numpy as np 4 | import math 5 | 6 | def get_cubic_unit_cell_ratios(support_length, cube_side_lenght, support_side_length): 7 | support_structure_volume = (3*(support_side_length*(support_length**2)))-(2*(support_length**3)) 8 | cube_volume = (cube_side_lenght**3) 9 | unit_cell_max_volume = support_side_length**3 10 | 11 | print(f"Max Space volume: {unit_cell_max_volume} | Cube volume: {cube_volume} | Support only volume: {support_structure_volume} (ratio : {support_structure_volume/unit_cell_max_volume})") 12 | 13 | 14 | 15 | 16 | 17 | import math 18 | 19 | def calculate_support_side_length(f2_ratio, support_length): 20 | # Ensure f2_ratio and support_length are positive 21 | if f2_ratio <= 0 or support_length <= 0: 22 | raise ValueError("f2_ratio and support_length must be positive and greater than 0.") 23 | 24 | # Check if f2_ratio < 1 25 | if f2_ratio < 1: 26 | # Calculate the inner term to ensure it's non-negative 27 | inner_sqrt_term = (support_length**6 * (f2_ratio - 1)) / f2_ratio**3 28 | if inner_sqrt_term < 0: 29 | raise ValueError("The term under the square root is negative, leading to a math domain error.") 30 | 31 | # Calculating intermediate components 32 | term1 = f2_ratio * math.sqrt(max(0, (support_length**6 * (f2_ratio - 1)) / f2_ratio**3)) + support_length**3 33 | inner_term = term1 / f2_ratio 34 | 35 | # Calculate support_side_length based on the derived equation 36 | support_side_length = -((f2_ratio * (inner_term ** (2 / 3)) + support_length**2) / 37 | (f2_ratio * (inner_term ** (1 / 3)))) 38 | 39 | return support_side_length 40 | 41 | 42 | 43 | 44 | 45 | 46 | def calculate_mesh_volume(mesh): 47 | unique_faces = set() 48 | volume = 0.0 49 | 50 | for face in mesh.faces: 51 | # Sort the vertices within the face to check for duplicates 52 | sorted_face = tuple(sorted(face)) 53 | 54 | # Skip this face if it's already in the set (i.e., it's a duplicate) 55 | if sorted_face in unique_faces: 56 | continue 57 | 58 | # Add the face to the set as we've now processed it 59 | unique_faces.add(sorted_face) 60 | 61 | # Calculate the signed volume of the tetrahedron formed by this face and the origin 62 | v0, v1, v2 = mesh.vertices[face] 63 | tetra_volume = np.dot(v0, np.cross(v1, v2)) / 6.0 64 | volume += tetra_volume 65 | 66 | return abs(volume) # Return the absolute value to get the total volume 67 | 68 | 69 | def generate_cubic_unit_cell(cubic_center: list = [0, 0, 0], support_length: int = 0, cube_side_lenght: float = 0.0, support_side_length: float = 0.0): 70 | # Unit cell based of the "A Highly Efficient Energy Harvesting Circuit Using Luneburg Lens" 71 | models = [] 72 | 73 | # if(cube_side_lenght>support_length): 74 | # # # Make the initial center cube 75 | # cube = stl_gen.generate_prism(x_lenght=cube_side_lenght, y_lenght=cube_side_lenght, 76 | # z_lenght=cube_side_lenght, xyz_position=cubic_center) 77 | # models.append(cube) 78 | 79 | # Then make all the 6 joints 80 | models.append(stl_gen.generate_prism(x_lenght=support_side_length, y_lenght=support_length, 81 | z_lenght=support_length, xyz_position=[cubic_center[0], cubic_center[1], cubic_center[2]])) 82 | models.append(stl_gen.generate_prism(x_lenght=support_length, y_lenght=support_length, 83 | z_lenght=support_side_length, xyz_position=[cubic_center[0], cubic_center[1], cubic_center[2]])) 84 | models.append(stl_gen.generate_prism(x_lenght=support_length, y_lenght=support_side_length, 85 | z_lenght=support_length, xyz_position=[cubic_center[0], cubic_center[1], cubic_center[2]])) 86 | 87 | unit_cell = stl_gen.merge_models(models=models) 88 | 89 | volume = calculate_mesh_volume(unit_cell) 90 | print("Volume of the mesh:", volume) 91 | 92 | # We manually calculate the volume of each cell 93 | get_cubic_unit_cell_ratios(support_length=support_length, 94 | support_side_length=support_side_length, cube_side_lenght=cube_side_lenght) 95 | 96 | return unit_cell 97 | 98 | 99 | def generate_sphere_unit_cell(sphere_center: list = [0, 0, 0], support_length: int = 0, sphere_radius: int = 0): 100 | # Unit cell based of the "A Highly Efficient Energy Harvesting Circuit Using Luneburg Lens" 101 | 102 | # Make the initial center sphere 103 | sphere = stl_gen.generate_sphere( 104 | radius=sphere_radius, resolution=2, center=sphere_center) 105 | models = [sphere] 106 | # Then make all the 6 joints 107 | models.append(stl_gen.generate_prism(L=support_length, A=support_length, xyz_position=[ 108 | sphere_center[0]+sphere_radius, sphere_center[1], sphere_center[2]])) 109 | models.append(stl_gen.generate_prism(L=support_length, A=support_length, xyz_position=[ 110 | sphere_center[0]-sphere_radius, sphere_center[1], sphere_center[2]])) 111 | models.append(stl_gen.generate_prism(L=support_length, A=support_length, xyz_position=[ 112 | sphere_center[0], sphere_center[1]+sphere_radius, sphere_center[2]])) 113 | models.append(stl_gen.generate_prism(L=support_length, A=support_length, xyz_position=[ 114 | sphere_center[0], sphere_center[1]-sphere_radius, sphere_center[2]])) 115 | models.append(stl_gen.generate_prism(L=support_length, A=support_length, xyz_position=[ 116 | sphere_center[0], sphere_center[1], sphere_center[2]+sphere_radius])) 117 | models.append(stl_gen.generate_prism(L=support_length, A=support_length, xyz_position=[ 118 | sphere_center[0], sphere_center[1], sphere_center[2]-sphere_radius])) 119 | 120 | unit_cell = stl_gen.merge_models(models=models) 121 | return unit_cell 122 | -------------------------------------------------------------------------------- /src/app/modules/stl_generator_pymesh.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pymesh 3 | import json 4 | 5 | def generate_metadata(path:str,data:dict): 6 | if('id' in data): 7 | json.dump( data, open( f"{path}/{data['id']}.json", 'w' ) ) 8 | return True 9 | else: 10 | return False 11 | 12 | 13 | def add_square_holes(sphere:pymesh.Mesh,holes:list): 14 | """ 15 | Add square holes to the Lunenburg lens, going through the entire object. 16 | 17 | Parameters: 18 | - vertices: 3D coordinates of lens vertices. 19 | - holes: List of dictionaries, each specifying a hole with keys 'size' and 'position'. 20 | - through_value: Value to set for the z-coordinate inside the hole. 21 | 22 | Returns: 23 | - Updated vertices after adding the square holes. 24 | """ 25 | for hole in holes: 26 | hole_size = hole['size'] 27 | hole_position = hole['position'] 28 | 29 | x=hole_position[0] 30 | y=hole_position[1] 31 | z=hole_position[2] 32 | 33 | square_hole_mesh=generate_prism_xy(L=z,A=hole_size,xy_positon=[x,y]) 34 | 35 | 36 | sphere=pymesh.boolean(sphere, square_hole_mesh, operation="difference") 37 | 38 | return sphere 39 | 40 | 41 | 42 | def generate_sphere(radius, resolution=16,center=[0,0,0]): 43 | """ 44 | Generate a 3D solid sphere using PyMesh. 45 | 46 | Args: 47 | radius (float): Radius of the sphere. 48 | center (list): Center of the sphere [x, y, z]. 49 | refinement_order (int): Number of refinement steps. 50 | 51 | Returns: 52 | PyMesh.Mesh: The generated sphere mesh. 53 | """ 54 | sphere = pymesh.generate_icosphere(radius, np.array(center), resolution) 55 | return pymesh.form_mesh(sphere.vertices, sphere.faces) 56 | 57 | 58 | def generate_cylinder(L, R, resolution=100): 59 | """ 60 | Generate a 3D cylinder using PyMesh. 61 | 62 | Args: 63 | L (float): Length of the cylinder. 64 | R (float): Radius of the cylinder. 65 | resolution (int): Number of triangles to approximate the cylinder surface. 66 | 67 | Returns: 68 | PyMesh.Mesh: The generated cylinder mesh. 69 | """ 70 | # Create the cylinder 71 | cylinder = pymesh.generate_cylinder(R, L, resolution) 72 | 73 | return pymesh.form_mesh(cylinder.vertices, cylinder.faces) 74 | 75 | 76 | 77 | def merge_models(models:list): 78 | return pymesh.merge_meshes(models) 79 | 80 | 81 | def scale_model(mesh,scale_factor): 82 | # Get the vertices of the mesh 83 | vertices = mesh.vertices 84 | 85 | # Scale the vertices by multiplying them by the scaling factors 86 | scaled_vertices = vertices * [scale_factor, scale_factor, scale_factor] 87 | 88 | # Create a new mesh with the scaled vertices 89 | scaled_mesh = pymesh.form_mesh(scaled_vertices, mesh.faces) 90 | 91 | return scaled_mesh 92 | 93 | 94 | def generate_prism_xy(L, A, num_samples=1, subdiv_order=0, xy_positon=[0,0]): 95 | """ 96 | Generate a 3D prism with a square face using PyMesh. 97 | 98 | Args: 99 | L (float): Length of the prism. 100 | A (float): Area of the square face. 101 | num_samples (int): Number of segments on each edge of the box. 102 | subdiv_order (int): The subdivision order. 103 | x (float): X-coordinate of the position. 104 | y (float): Y-coordinate of the position. 105 | 106 | Returns: 107 | PyMesh.Mesh: The generated prism mesh. 108 | """ 109 | 110 | x,y=xy_positon 111 | 112 | # Calculate the side length of the square face 113 | side_length = A 114 | 115 | # Define the min and max corners of the box 116 | box_min = np.array([x - side_length / 2, y - side_length / 2,-L/2]) 117 | box_max = np.array([x + side_length / 2, y + side_length / 2,L/2]) 118 | 119 | # Create the prism 120 | prism = pymesh.generate_box_mesh(box_min, box_max, num_samples, subdiv_order) 121 | 122 | return prism 123 | 124 | 125 | def generate_prism(x_lenght:float,y_lenght:float,z_lenght:float, num_samples=1, subdiv_order=0, xyz_position=[0, 0, 0]): 126 | """ 127 | Generate a 3D prism with a square face using PyMesh. 128 | 129 | Args: 130 | L (float): Length of the prism. 131 | A (float): Area of the square face. 132 | num_samples (int): Number of segments on each edge of the box. 133 | subdiv_order (int): The subdivision order. 134 | xyz_position (list of float): Coordinates of the position [x, y, z]. 135 | 136 | Returns: 137 | PyMesh.Mesh: The generated prism mesh. 138 | """ 139 | 140 | x, y, z = xyz_position 141 | 142 | # Define the min and max corners of the box 143 | box_min = np.array([x - x_lenght / 2, y - y_lenght / 2, z - z_lenght / 2]) 144 | box_max = np.array([x + x_lenght / 2, y + y_lenght / 2, z + z_lenght / 2]) 145 | 146 | # Create the prism 147 | prism = pymesh.generate_box_mesh(box_min, box_max, num_samples, subdiv_order) 148 | 149 | return prism 150 | 151 | 152 | 153 | 154 | def export_to_stl(mesh, filename): 155 | """ 156 | Export a PyMesh mesh to an STL file. 157 | 158 | Args: 159 | mesh (PyMesh.Mesh): The mesh to export. 160 | filename (str): The name of the STL file. 161 | 162 | """ 163 | pymesh.save_mesh(filename, mesh) 164 | 165 | 166 | 167 | def cut_mesh_in_half(mesh, axis='x'): 168 | """ 169 | Cuts a mesh in half along the specified axis (default is x-axis). 170 | 171 | Returns: 172 | - cut_mesh: The cut mesh object. 173 | """ 174 | 175 | # Define the cutting plane direction based on the specified axis 176 | if axis == 'x': 177 | direction = np.array([1.0, 0.0, 0.0]) 178 | elif axis == 'y': 179 | direction = np.array([0.0, 1.0, 0.0]) 180 | elif axis == 'z': 181 | direction = np.array([0.0, 0.0, 1.0]) 182 | else: 183 | raise ValueError("Invalid axis specified. Choose from 'x', 'y', or 'z'.") 184 | 185 | # Slice the mesh into 2 parts along the specified direction 186 | slices = pymesh.slice_mesh(mesh, direction, 2) 187 | 188 | return slices[0] -------------------------------------------------------------------------------- /tools/ideal_luneburg_maker/src/modules/stl_generator_pymesh.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pymesh 3 | import json 4 | 5 | def generate_metadata(path:str,data:dict): 6 | if('id' in data): 7 | json.dump( data, open( f"{path}/{data['id']}.json", 'w' ) ) 8 | return True 9 | else: 10 | return False 11 | 12 | 13 | def add_square_holes(sphere:pymesh.Mesh,holes:list): 14 | """ 15 | Add square holes to the Lunenburg lens, going through the entire object. 16 | 17 | Parameters: 18 | - vertices: 3D coordinates of lens vertices. 19 | - holes: List of dictionaries, each specifying a hole with keys 'size' and 'position'. 20 | - through_value: Value to set for the z-coordinate inside the hole. 21 | 22 | Returns: 23 | - Updated vertices after adding the square holes. 24 | """ 25 | for hole in holes: 26 | hole_size = hole['size'] 27 | hole_position = hole['position'] 28 | 29 | x=hole_position[0] 30 | y=hole_position[1] 31 | z=hole_position[2] 32 | 33 | square_hole_mesh=generate_prism_xy(L=z,A=hole_size,xy_positon=[x,y]) 34 | 35 | 36 | sphere=pymesh.boolean(sphere, square_hole_mesh, operation="difference") 37 | 38 | return sphere 39 | 40 | 41 | 42 | def generate_sphere(radius, resolution=16,center=[0,0,0]): 43 | """ 44 | Generate a 3D solid sphere using PyMesh. 45 | 46 | Args: 47 | radius (float): Radius of the sphere. 48 | center (list): Center of the sphere [x, y, z]. 49 | refinement_order (int): Number of refinement steps. 50 | 51 | Returns: 52 | PyMesh.Mesh: The generated sphere mesh. 53 | """ 54 | sphere = pymesh.generate_icosphere(radius, np.array(center), resolution) 55 | return pymesh.form_mesh(sphere.vertices, sphere.faces) 56 | 57 | 58 | def generate_cylinder(L, R, resolution=100): 59 | """ 60 | Generate a 3D cylinder using PyMesh. 61 | 62 | Args: 63 | L (float): Length of the cylinder. 64 | R (float): Radius of the cylinder. 65 | resolution (int): Number of triangles to approximate the cylinder surface. 66 | 67 | Returns: 68 | PyMesh.Mesh: The generated cylinder mesh. 69 | """ 70 | # Create the cylinder 71 | cylinder = pymesh.generate_cylinder(R, L, resolution) 72 | 73 | return pymesh.form_mesh(cylinder.vertices, cylinder.faces) 74 | 75 | 76 | 77 | def merge_models(models:list): 78 | return pymesh.merge_meshes(models) 79 | 80 | 81 | def scale_model(mesh,scale_factor): 82 | # Get the vertices of the mesh 83 | vertices = mesh.vertices 84 | 85 | # Scale the vertices by multiplying them by the scaling factors 86 | scaled_vertices = vertices * [scale_factor, scale_factor, scale_factor] 87 | 88 | # Create a new mesh with the scaled vertices 89 | scaled_mesh = pymesh.form_mesh(scaled_vertices, mesh.faces) 90 | 91 | return scaled_mesh 92 | 93 | 94 | def generate_prism_xy(L, A, num_samples=1, subdiv_order=0, xy_positon=[0,0]): 95 | """ 96 | Generate a 3D prism with a square face using PyMesh. 97 | 98 | Args: 99 | L (float): Length of the prism. 100 | A (float): Area of the square face. 101 | num_samples (int): Number of segments on each edge of the box. 102 | subdiv_order (int): The subdivision order. 103 | x (float): X-coordinate of the position. 104 | y (float): Y-coordinate of the position. 105 | 106 | Returns: 107 | PyMesh.Mesh: The generated prism mesh. 108 | """ 109 | 110 | x,y=xy_positon 111 | 112 | # Calculate the side length of the square face 113 | side_length = A 114 | 115 | # Define the min and max corners of the box 116 | box_min = np.array([x - side_length / 2, y - side_length / 2,-L/2]) 117 | box_max = np.array([x + side_length / 2, y + side_length / 2,L/2]) 118 | 119 | # Create the prism 120 | prism = pymesh.generate_box_mesh(box_min, box_max, num_samples, subdiv_order) 121 | 122 | return prism 123 | 124 | 125 | def generate_prism(x_lenght:float,y_lenght:float,z_lenght:float, num_samples=1, subdiv_order=0, xyz_position=[0, 0, 0]): 126 | """ 127 | Generate a 3D prism with a square face using PyMesh. 128 | 129 | Args: 130 | L (float): Length of the prism. 131 | A (float): Area of the square face. 132 | num_samples (int): Number of segments on each edge of the box. 133 | subdiv_order (int): The subdivision order. 134 | xyz_position (list of float): Coordinates of the position [x, y, z]. 135 | 136 | Returns: 137 | PyMesh.Mesh: The generated prism mesh. 138 | """ 139 | 140 | x, y, z = xyz_position 141 | 142 | # Define the min and max corners of the box 143 | box_min = np.array([x - x_lenght / 2, y - y_lenght / 2, z - z_lenght / 2]) 144 | box_max = np.array([x + x_lenght / 2, y + y_lenght / 2, z + z_lenght / 2]) 145 | 146 | # Create the prism 147 | prism = pymesh.generate_box_mesh(box_min, box_max, num_samples, subdiv_order) 148 | 149 | return prism 150 | 151 | 152 | 153 | 154 | def export_to_stl(mesh, filename): 155 | """ 156 | Export a PyMesh mesh to an STL file. 157 | 158 | Args: 159 | mesh (PyMesh.Mesh): The mesh to export. 160 | filename (str): The name of the STL file. 161 | 162 | """ 163 | pymesh.save_mesh(filename, mesh) 164 | 165 | 166 | 167 | def cut_mesh_in_half(mesh, axis='x'): 168 | """ 169 | Cuts a mesh in half along the specified axis (default is x-axis). 170 | 171 | Returns: 172 | - cut_mesh: The cut mesh object. 173 | """ 174 | 175 | # Define the cutting plane direction based on the specified axis 176 | if axis == 'x': 177 | direction = np.array([1.0, 0.0, 0.0]) 178 | elif axis == 'y': 179 | direction = np.array([0.0, 1.0, 0.0]) 180 | elif axis == 'z': 181 | direction = np.array([0.0, 0.0, 1.0]) 182 | else: 183 | raise ValueError("Invalid axis specified. Choose from 'x', 'y', or 'z'.") 184 | 185 | # Slice the mesh into 2 parts along the specified direction 186 | slices = pymesh.slice_mesh(mesh, direction, 2) 187 | 188 | return slices[0] -------------------------------------------------------------------------------- /src/app/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | LuneForge 8 | 9 | 10 | 11 | 12 | 77 | 78 | 79 | 80 | 99 | 100 |
101 |
102 |
104 | 105 | 109 | 113 | 114 | 118 | 122 | 123 | 124 | 126 | 127 | 128 | 130 | 131 | 132 | 134 | 135 | 136 | 138 | 139 | 140 | 141 |
142 | 143 |
144 | 145 |
146 |
147 | 148 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /src/app/static/build/stlwebviewer2.js: -------------------------------------------------------------------------------- 1 | (()=>{var e=function(){return void 0!==window.pageYOffset?window.pageYOffset:document.documentElement.scrollTop?document.documentElement.scrollTop:document.body.scrollTop?document.body.scrollTop:0},t=function(){return void 0!==window.pageXOffset?window.pageXOffset:document.documentElement.scrollLeft?document.documentElement.scrollLeft:document.body.scrollLeft?document.body.scrollLeft:0},n=function(e){this.manager=void 0!==e?e:THREE.DefaultLoadingManager};function o(e,t){if(this.modelUrl=e,this.$container=t,!a.webgl)return void a.addGetWebGLMessage({parent:this.$container[0]});o.instanceCount=(o.instanceCount||0)+1;let r="stlwv2-fullscreen-checkbox-"+(o.instanceCount-1);this.$container.append(['','
','
',' ",' ",' '," STL Web Viewer","
"].join("\n")),this.$innerContainer=this.$container.children(".stlwv2-inner"),this.$fullscreenCheckbox=$("#"+r),this.$fullscreenCheckbox.on("change",this.fullscreenToggleHandler.bind(this)),this.scene=new THREE.Scene,this.camera=new THREE.PerspectiveCamera(40,this.$innerContainer.width()/this.$innerContainer.height(),1,15e3),this.cameraTarget=new THREE.Vector3,this.renderer=this.makeRenderer(antialias=!0),this.$innerContainer.append(this.renderer.domElement),this.controls=new i(this.camera,this.$innerContainer.get(0)),this.controls.target=this.cameraTarget,this.controls.enableDamping=!0,this.controls.enableKeys=!1,this.controls.rotateSpeed=.15,this.controls.dampingFactor=.125,this.controls.enableZoom=!0,this.controls.autoRotate=!0,this.controls.autoRotateSpeed=.25,this.controls.autoRotateDelay=5e3,this.hemisphereLight=new THREE.HemisphereLight(10066329,5592405),this.scene.add(this.hemisphereLight),this.pointLight=new THREE.PointLight(14540253,.75,0),this.camera.add(this.pointLight),this.scene.add(this.camera),(new n).load(this.modelUrl,this.stlLoadedCallback.bind(this),this.updateProgress.bind(this))}n.prototype={constructor:n,load:function(e,t,n,o){var a=this,i=new THREE.FileLoader(a.manager);i.setResponseType("arraybuffer"),i.load(e,(function(e){t(a.parse(e))}),n,o)},parse:function(e){var t=this.ensureBinary(e);return function(){var e;if(50,84+50*(e=new DataView(t)).getUint32(80,!0)===e.byteLength)return!0;for(var n=e.byteLength,o=0;o127)return!0;return!1}()?this.parseBinary(t):this.parseASCII(this.ensureString(e))},parseBinary:function(e){for(var t,n,o,a,i,r,s,c,l=new DataView(e),d=l.getUint32(80,!0),h=!1,u=0;u<70;u++)1129270351==l.getUint32(u,!1)&&82==l.getUint8(u+4)&&61==l.getUint8(u+5)&&(h=!0,a=[],i=l.getUint8(u+6)/255,r=l.getUint8(u+7)/255,s=l.getUint8(u+8)/255,c=l.getUint8(u+9)/255);for(var m=new THREE.BufferGeometry,p=[],b=[],f=0;f>5&31)/31,o=(y>>10&31)/31):(t=i,n=r,o=s)}for(var R=1;R<=3;R++){var T=g+12*R;p.push(l.getFloat32(T,!0)),p.push(l.getFloat32(T+4,!0)),p.push(l.getFloat32(T+8,!0)),b.push(E,w,v),h&&a.push(t,n,o)}}return m.addAttribute("position",new THREE.BufferAttribute(new Float32Array(p),3)),m.addAttribute("normal",new THREE.BufferAttribute(new Float32Array(b),3)),h&&(m.addAttribute("color",new THREE.BufferAttribute(new Float32Array(a),3)),m.hasColors=!0,m.alpha=c),m},parseASCII:function(e){var t,n,o,a,i,r;t=new THREE.BufferGeometry,n=/facet([\s\S]*?)endfacet/g;for(var s=[],c=[],l=new THREE.Vector3;null!==(i=n.exec(e));){for(r=i[0],o=/normal[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g;null!==(i=o.exec(r));)l.x=parseFloat(i[1]),l.y=parseFloat(i[3]),l.z=parseFloat(i[5]);for(a=/vertex[\s]+([\-+]?[0-9]+\.?[0-9]*([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+[\s]+([\-+]?[0-9]*\.?[0-9]+([eE][\-+]?[0-9]+)?)+/g;null!==(i=a.exec(r));)s.push(parseFloat(i[1]),parseFloat(i[3]),parseFloat(i[5])),c.push(l.x,l.y,l.z)}return t.addAttribute("position",new THREE.BufferAttribute(new Float32Array(s),3)),t.addAttribute("normal",new THREE.BufferAttribute(new Float32Array(c),3)),t},ensureString:function(e){if("string"!=typeof e){for(var t=new Uint8Array(e),n=[],o=0;o{$(".stlwv2-model").each((function(){let e=$(this);new o(e.data("model-url"),e)})),$(document).keyup((function(e){"Escape"===e.key&&$(".stlwv2-model .stlwv2-fullscreen-checkbox").each((function(){$(this).prop("checked")&&$(this).prop("checked",!1).trigger("change")}))}))}),o.prototype.updateProgress=function(e){console.log("Loading "+this.modelUrl+": "+e.loaded+"/"+e.total),this.$innerContainer.children(".stlwv2-percent").text(Math.floor(e.loaded/e.total*100)+"%")},o.prototype.stlLoadedCallback=function(e){let t=new THREE.MeshPhongMaterial({color:16251135,specular:1118481,shininess:0,wireframe:!1,polygonOffset:!0,polygonOffsetFactor:1,polygonOffsetUnits:1,transparent:!0,opacity:.85}),n=new THREE.Mesh(e,t);n.position.set(0,0,0),n.castShadow=!1,n.receiveShadow=!1,this.scene.add(n);let o=new THREE.EdgesGeometry(e,29),a=new THREE.LineSegments(o,new THREE.LineBasicMaterial({color:6710886}));this.scene.add(a),e.computeBoundingSphere(),e.computeBoundingBox(),this.cameraTarget.copy(e.boundingSphere.center);let i=e.boundingSphere.radius;this.controls.maxDistance=10*i,this.pointLight.position.set(0,i,0),this.camera.position.set(1.5*i+this.cameraTarget.x,1.5*i+this.cameraTarget.y,1.5*i+this.cameraTarget.z),this.animate(),this.$innerContainer.addClass("stlwv2-loaded")},o.prototype.animate=function(){if(this.animateLoops=(this.animateLoops||0)+1,!this.performanceChecked)if(5==this.animateLoops)this.performanceCheckStartTime=performance.now();else if(this.animateLoops>5){let e=performance.now()-this.performanceCheckStartTime;if(e>2e3){let t=1e3*(this.animateLoops-5)/e;console.log("Cumulative framerate: "+t),t<30&&(console.log("Disabling anti-aliasing"),this.renderer.domElement.remove(),delete this.renderer,this.renderer=this.makeRenderer(antialias=!1),this.$innerContainer.append(this.renderer.domElement)),this.performanceChecked=!0}}this.camera.aspect=this.$innerContainer.width()/this.$innerContainer.height(),this.camera.updateProjectionMatrix(),this.renderer.setSize(this.$innerContainer.width(),this.$innerContainer.height()),requestAnimationFrame(this.animate.bind(this)),this.controls.update(),this.renderer.render(this.scene,this.camera)},o.prototype.makeRenderer=function(e){let t=new THREE.WebGLRenderer({antialias:e});return t.setClearColor(16777215),t.setPixelRatio(Math.min(window.devicePixelRatio,2)),t.gammaInput=!0,t.gammaOutput=!0,t.shadowMap.enabled=!0,t},o.prototype.fullscreenToggleHandler=function(){let n=this.$container.position().top-e()+1,o=this.$container.position().left-t()+1,a=$(window).height()-(n+this.$container.innerHeight())+1,i=this.$container.width()-2;this.$fullscreenCheckbox.prop("checked")?(this.$innerContainer.css({top:n+"px",bottom:a+"px",left:o+"px",width:i+"px",position:"fixed",opacity:"0.5","z-index":2e3}),this.$innerContainer.animate({top:"0",bottom:"0",left:"0",width:"100%",opacity:"1"},300,()=>{this.$innerContainer.animate({opacity:"1"},500)})):(this.$innerContainer.css({opacity:"0.5"}),this.$innerContainer.animate({top:n+"px",bottom:a+"px",left:o+"px",width:i+"px"},300,()=>{this.$innerContainer.css({position:"",top:"",bottom:"",left:"",width:"","z-index":""}),this.$innerContainer.animate({opacity:"1"},500)}))};var a={canvas:!!window.CanvasRenderingContext2D,webgl:function(){try{var e=document.createElement("canvas");return!(!window.WebGLRenderingContext||!e.getContext("webgl")&&!e.getContext("experimental-webgl"))}catch(e){return!1}}(),workers:!!window.Worker,fileapi:window.File&&window.FileReader&&window.FileList&&window.Blob,getWebGLErrorMessage:function(){var e=document.createElement("div");return e.id="webgl-error-message",e.style.fontFamily="monospace",e.style.fontSize="13px",e.style.fontWeight="normal",e.style.textAlign="center",e.style.background="#fff",e.style.color="#000",e.style.padding="1.5em",e.style.width="400px",e.style.margin="5em auto 0",this.webgl||(e.innerHTML=window.WebGLRenderingContext?['Your graphics card does not seem to support WebGL.
','Find out how to get it here.'].join("\n"):['Your browser does not seem to support WebGL.
','Find out how to get it here.'].join("\n")),e},addGetWebGLMessage:function(e){var t,n,o;t=void 0!==(e=e||{}).parent?e.parent:document.body,n=void 0!==e.id?e.id:"oldie",(o=a.getWebGLErrorMessage()).id=n,t.appendChild(o)}};"object"==typeof module&&(module.exports=a);var i=function(e,t){var n,o,a,i,r;this.object=e,this.domElement=void 0!==t?t:document,this.enabled=!0,this.target=new THREE.Vector3,this.minDistance=0,this.maxDistance=1/0,this.minZoom=0,this.maxZoom=1/0,this.minPolarAngle=0,this.maxPolarAngle=Math.PI,this.minAzimuthAngle=-1/0,this.maxAzimuthAngle=1/0,this.enableDamping=!1,this.dampingFactor=.25,this.enableZoom=!0,this.zoomSpeed=1,this.enableRotate=!0,this.rotateSpeed=1,this.enablePan=!0,this.keyPanSpeed=7,this.autoRotate=!1,this.autoRotateSpeed=2,this.autoRotateDelay=0,this.autoRotateTimeout,this.enableKeys=!0,this.keys={LEFT:37,UP:38,RIGHT:39,BOTTOM:40},this.mouseButtons={ORBIT:THREE.MOUSE.LEFT,ZOOM:THREE.MOUSE.MIDDLE,PAN:THREE.MOUSE.RIGHT},this.target0=this.target.clone(),this.position0=this.object.position.clone(),this.zoom0=this.object.zoom,this.getPolarAngle=function(){return p.phi},this.getAzimuthalAngle=function(){return p.theta},this.reset=function(){s.target.copy(s.target0),s.object.position.copy(s.position0),s.object.zoom=s.zoom0,s.object.updateProjectionMatrix(),s.dispatchEvent(c),s.update(),u=h.NONE},this.update=(n=new THREE.Vector3,o=(new THREE.Quaternion).setFromUnitVectors(e.up,new THREE.Vector3(0,1,0)),a=o.clone().inverse(),i=new THREE.Vector3,r=new THREE.Quaternion,function(){var e=s.object.position;return n.copy(e).sub(s.target),n.applyQuaternion(o),p.setFromVector3(n),s.autoRotate&&u===h.NONE&&!w?P(2*Math.PI/60/60*s.autoRotateSpeed):s.autoRotate&&s.autoRotateDelay>0&&(s.autoRotateSpeed>0&&(s.autoRotateSpeedActual=s.autoRotateSpeed,s.autoRotateSpeed=0),clearTimeout(s.autoRotateTimeout),s.autoRotateTimeout=setTimeout((function(){s.autoRotateSpeed=s.autoRotateSpeedActual}),s.autoRotateDelay),w=!1),p.theta+=b.theta,p.phi+=b.phi,p.theta=Math.max(s.minAzimuthAngle,Math.min(s.maxAzimuthAngle,p.theta)),p.phi=Math.max(s.minPolarAngle,Math.min(s.maxPolarAngle,p.phi)),p.makeSafe(),p.radius*=f,p.radius=Math.max(s.minDistance,Math.min(s.maxDistance,p.radius)),s.target.add(g),n.setFromSpherical(p),n.applyQuaternion(a),e.copy(s.target).add(n),s.object.lookAt(s.target),!0===s.enableDamping?(b.theta*=1-s.dampingFactor,b.phi*=1-s.dampingFactor):b.set(0,0,0),f=1,g.set(0,0,0),!!(E||i.distanceToSquared(s.object.position)>m||8*(1-r.dot(s.object.quaternion))>m)&&(s.dispatchEvent(c),i.copy(s.object.position),r.copy(s.object.quaternion),E=!1,!0)}),this.dispose=function(){s.domElement.removeEventListener("contextmenu",I,!1),s.domElement.removeEventListener("mousedown",D,!1),s.domElement.removeEventListener("wheel",z,!1),s.domElement.removeEventListener("touchstart",B,!1),s.domElement.removeEventListener("touchend",Z,!1),s.domElement.removeEventListener("touchmove",G,!1),document.removeEventListener("mousemove",N,!1),document.removeEventListener("mouseup",V,!1),document.removeEventListener("mouseleave",V,!1),window.removeEventListener("keydown",Y,!1)};var s=this,c={type:"change"},l={type:"start"},d={type:"end"},h={NONE:-1,ROTATE:0,DOLLY:1,PAN:2,TOUCH_ROTATE:3,TOUCH_DOLLY:4,TOUCH_PAN:5},u=h.NONE,m=1e-6,p=new THREE.Spherical,b=new THREE.Spherical,f=1,g=new THREE.Vector3,E=!1,w=!1,v=new THREE.Vector2,y=new THREE.Vector2,R=new THREE.Vector2,T=new THREE.Vector2,C=new THREE.Vector2,L=new THREE.Vector2,O=new THREE.Vector2,H=new THREE.Vector2,x=new THREE.Vector2;function A(){return Math.pow(.95,s.zoomSpeed)}function P(e){b.theta-=e}function k(e){b.phi-=e}var M,j=(M=new THREE.Vector3,function(e,t){M.setFromMatrixColumn(t,0),M.multiplyScalar(-e),g.add(M)}),S=function(){var e=new THREE.Vector3;return function(t,n){e.setFromMatrixColumn(n,1),e.multiplyScalar(t),g.add(e)}}(),F=function(){var e=new THREE.Vector3;return function(t,n){var o=s.domElement===document?s.domElement.body:s.domElement;if(s.object instanceof THREE.PerspectiveCamera){var a=s.object.position;e.copy(a).sub(s.target);var i=e.length();i*=Math.tan(s.object.fov/2*Math.PI/180),j(2*t*i/o.clientHeight,s.object.matrix),S(2*n*i/o.clientHeight,s.object.matrix)}else s.object instanceof THREE.OrthographicCamera?(j(t*(s.object.right-s.object.left)/s.object.zoom/o.clientWidth,s.object.matrix),S(n*(s.object.top-s.object.bottom)/s.object.zoom/o.clientHeight,s.object.matrix)):(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - pan disabled."),s.enablePan=!1)}}();function U(e){s.object instanceof THREE.PerspectiveCamera?f/=e:s.object instanceof THREE.OrthographicCamera?(s.object.zoom=Math.max(s.minZoom,Math.min(s.maxZoom,s.object.zoom*e)),s.object.updateProjectionMatrix(),E=!0):(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."),s.enableZoom=!1)}function $(e){s.object instanceof THREE.PerspectiveCamera?f*=e:s.object instanceof THREE.OrthographicCamera?(s.object.zoom=Math.max(s.minZoom,Math.min(s.maxZoom,s.object.zoom/e)),s.object.updateProjectionMatrix(),E=!0):(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."),s.enableZoom=!1)}function D(e){if(!1!==s.enabled){if(e.preventDefault(),e.button===s.mouseButtons.ORBIT){if(!1===s.enableRotate)return;!function(e){v.set(e.clientX,e.clientY)}(e),u=h.ROTATE}else if(e.button===s.mouseButtons.ZOOM){if(!1===s.enableZoom)return;!function(e){O.set(e.clientX,e.clientY)}(e),u=h.DOLLY}else if(e.button===s.mouseButtons.PAN){if(!1===s.enablePan)return;!function(e){T.set(e.clientX,e.clientY)}(e),u=h.PAN}u!==h.NONE&&(document.addEventListener("mousemove",N,!1),document.addEventListener("mouseup",V,!1),document.addEventListener("mouseleave",V,!1),s.dispatchEvent(l))}}function N(e){if(!1!==s.enabled)if(e.preventDefault(),u===h.ROTATE){if(!1===s.enableRotate)return;!function(e){y.set(e.clientX,e.clientY),R.subVectors(y,v);var t=s.domElement===document?s.domElement.body:s.domElement;P(2*Math.PI*R.x/t.clientWidth*s.rotateSpeed),k(2*Math.PI*R.y/t.clientHeight*s.rotateSpeed),v.copy(y),s.update()}(e)}else if(u===h.DOLLY){if(!1===s.enableZoom)return;!function(e){H.set(e.clientX,e.clientY),x.subVectors(H,O),x.y>0?U(A()):x.y<0&&$(A()),O.copy(H),s.update()}(e)}else if(u===h.PAN){if(!1===s.enablePan)return;!function(e){C.set(e.clientX,e.clientY),L.subVectors(C,T),F(L.x,L.y),T.copy(C),s.update()}(e)}}function V(e){!1!==s.enabled&&(document.removeEventListener("mousemove",N,!1),document.removeEventListener("mouseup",V,!1),document.removeEventListener("mouseleave",V,!1),s.dispatchEvent(d),u=h.NONE)}function z(e){if(!1!==s.enabled&&!1!==s.enableZoom&&(u===h.NONE||u===h.ROTATE))return function(e){e.deltaY<0?$(A()):e.deltaY>0&&U(A()),s.update()}(e),s.dispatchEvent(l),s.dispatchEvent(d),w=!0,e.preventDefault(),e.stopPropagation(),!1}function Y(e){!1!==s.enabled&&!1!==s.enableKeys&&!1!==s.enablePan&&function(e){switch(e.keyCode){case s.keys.UP:F(0,s.keyPanSpeed),s.update();break;case s.keys.BOTTOM:F(0,-s.keyPanSpeed),s.update();break;case s.keys.LEFT:F(s.keyPanSpeed,0),s.update();break;case s.keys.RIGHT:F(-s.keyPanSpeed,0),s.update()}}(e)}function B(e){if(!1!==s.enabled){switch(e.touches.length){case 1:if(!1===s.enableRotate)return;!function(e){v.set(e.touches[0].pageX,e.touches[0].pageY)}(e),u=h.TOUCH_ROTATE;break;case 2:if(!1===s.enableZoom)return;!function(e){var t=e.touches[0].pageX-e.touches[1].pageX,n=e.touches[0].pageY-e.touches[1].pageY,o=Math.sqrt(t*t+n*n);O.set(0,o)}(e),u=h.TOUCH_DOLLY;break;case 3:if(!1===s.enablePan)return;!function(e){T.set(e.touches[0].pageX,e.touches[0].pageY)}(e),u=h.TOUCH_PAN;break;default:u=h.NONE}u!==h.NONE&&s.dispatchEvent(l)}}function G(e){if(!1!==s.enabled)switch(e.preventDefault(),e.stopPropagation(),e.touches.length){case 1:if(!1===s.enableRotate)return;if(u!==h.TOUCH_ROTATE)return;!function(e){y.set(e.touches[0].pageX,e.touches[0].pageY),R.subVectors(y,v);var t=s.domElement===document?s.domElement.body:s.domElement;P(2*Math.PI*R.x/t.clientWidth*s.rotateSpeed),k(2*Math.PI*R.y/t.clientHeight*s.rotateSpeed),v.copy(y),s.update()}(e);break;case 2:if(!1===s.enableZoom)return;if(u!==h.TOUCH_DOLLY)return;!function(e){var t=e.touches[0].pageX-e.touches[1].pageX,n=e.touches[0].pageY-e.touches[1].pageY,o=Math.sqrt(t*t+n*n);H.set(0,o),x.subVectors(H,O),x.y>0?$(A()):x.y<0&&U(A()),O.copy(H),s.update()}(e);break;case 3:if(!1===s.enablePan)return;if(u!==h.TOUCH_PAN)return;!function(e){C.set(e.touches[0].pageX,e.touches[0].pageY),L.subVectors(C,T),F(L.x,L.y),T.copy(C),s.update()}(e);break;default:u=h.NONE}}function Z(e){!1!==s.enabled&&(s.dispatchEvent(d),u=h.NONE)}function I(e){e.preventDefault()}s.domElement.addEventListener("contextmenu",I,!1),s.domElement.addEventListener("mousedown",D,!1),s.domElement.addEventListener("wheel",z,!1),s.domElement.addEventListener("touchstart",B,!1),s.domElement.addEventListener("touchend",Z,!1),s.domElement.addEventListener("touchmove",G,!1),window.addEventListener("keydown",Y,!1),this.update()};i.prototype=Object.create(THREE.EventDispatcher.prototype),i.prototype.constructor=i,Object.defineProperties(i.prototype,{center:{get:function(){return console.warn("OrbitControls: .center has been renamed to .target"),this.target}},noZoom:{get:function(){return console.warn("OrbitControls: .noZoom has been deprecated. Use .enableZoom instead."),!this.enableZoom},set:function(e){console.warn("OrbitControls: .noZoom has been deprecated. Use .enableZoom instead."),this.enableZoom=!e}},noRotate:{get:function(){return console.warn("OrbitControls: .noRotate has been deprecated. Use .enableRotate instead."),!this.enableRotate},set:function(e){console.warn("OrbitControls: .noRotate has been deprecated. Use .enableRotate instead."),this.enableRotate=!e}},noPan:{get:function(){return console.warn("OrbitControls: .noPan has been deprecated. Use .enablePan instead."),!this.enablePan},set:function(e){console.warn("OrbitControls: .noPan has been deprecated. Use .enablePan instead."),this.enablePan=!e}},noKeys:{get:function(){return console.warn("OrbitControls: .noKeys has been deprecated. Use .enableKeys instead."),!this.enableKeys},set:function(e){console.warn("OrbitControls: .noKeys has been deprecated. Use .enableKeys instead."),this.enableKeys=!e}},staticMoving:{get:function(){return console.warn("OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead."),!this.enableDamping},set:function(e){console.warn("OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead."),this.enableDamping=!e}},dynamicDampingFactor:{get:function(){return console.warn("OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead."),this.dampingFactor},set:function(e){console.warn("OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead."),this.dampingFactor=e}}})})(); 2 | --------------------------------------------------------------------------------