No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3d.js 7.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. /*
  2. * 3d.js
  3. *
  4. * Copyright (c) 2024 Thomas Buck (thomas@xythobuz.de)
  5. *
  6. * For the fitCameraToObject() function also:
  7. * Copyright (c) 2024 Michal Jirků
  8. * https://wejn.org/2020/12/cracking-the-threejs-object-fitting-nut/
  9. *
  10. * For everything else:
  11. *
  12. * This program is free software: you can redistribute it and/or modify
  13. * it under the terms of the GNU General Public License as published by
  14. * the Free Software Foundation, either version 3 of the License, or
  15. * (at your option) any later version.
  16. *
  17. * This program is distributed in the hope that it will be useful,
  18. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  19. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  20. * GNU General Public License for more details.
  21. *
  22. * See <http://www.gnu.org/licenses/>.
  23. */
  24. import * as THREE from 'three';
  25. import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
  26. import { STLLoader } from 'three/addons/loaders/STLLoader.js'
  27. import { VRMLLoader } from 'three/addons/loaders/VRMLLoader.js';
  28. function fitCameraToObject(camera, object, offset, orbitControls, yOffset) {
  29. const boundingBox = new THREE.Box3();
  30. boundingBox.setFromObject( object );
  31. var middle = new THREE.Vector3();
  32. boundingBox.getCenter(middle);
  33. var size = new THREE.Vector3();
  34. boundingBox.getSize(size);
  35. // figure out how to fit the box in the view:
  36. // 1. figure out horizontal FOV (on non-1.0 aspects)
  37. // 2. figure out distance from the object in X and Y planes
  38. // 3. select the max distance (to fit both sides in)
  39. //
  40. // The reason is as follows:
  41. //
  42. // Imagine a bounding box (BB) is centered at (0,0,0).
  43. // Camera has vertical FOV (camera.fov) and horizontal FOV
  44. // (camera.fov scaled by aspect, see fovh below)
  45. //
  46. // Therefore if you want to put the entire object into the field of view,
  47. // you have to compute the distance as: z/2 (half of Z size of the BB
  48. // protruding towards us) plus for both X and Y size of BB you have to
  49. // figure out the distance created by the appropriate FOV.
  50. //
  51. // The FOV is always a triangle:
  52. //
  53. // (size/2)
  54. // +--------+
  55. // | /
  56. // | /
  57. // | /
  58. // | F° /
  59. // | /
  60. // | /
  61. // | /
  62. // |/
  63. //
  64. // F° is half of respective FOV, so to compute the distance (the length
  65. // of the straight line) one has to: `size/2 / Math.tan(F)`.
  66. //
  67. // FTR, from https://threejs.org/docs/#api/en/cameras/PerspectiveCamera
  68. // the camera.fov is the vertical FOV.
  69. const fov = camera.fov * ( Math.PI / 180 );
  70. const fovh = 2*Math.atan(Math.tan(fov/2) * camera.aspect);
  71. let dx = size.z / 2 + Math.abs( size.x / 2 / Math.tan( fovh / 2 ) );
  72. let dy = size.z / 2 + Math.abs( size.y / 2 / Math.tan( fov / 2 ) );
  73. let cameraZ = Math.max(dx, dy);
  74. // offset the camera, if desired (to avoid filling the whole canvas)
  75. if( offset !== undefined && offset !== 0 ) cameraZ *= offset;
  76. camera.target = middle;
  77. camera.position.set( middle.x, middle.y + yOffset * cameraZ, middle.z + cameraZ );
  78. // set the far plane of the camera so that it easily encompasses the whole object
  79. const minZ = boundingBox.min.z;
  80. const cameraToFarEdge = ( minZ < 0 ) ? -minZ + cameraZ : cameraZ - minZ;
  81. camera.far = cameraToFarEdge * 3;
  82. camera.updateProjectionMatrix();
  83. if ( orbitControls !== undefined ) {
  84. // set camera to rotate around the center
  85. orbitControls.target = middle;
  86. // prevent camera from zooming out far enough to create far plane cutoff
  87. orbitControls.maxDistance = cameraToFarEdge * 2;
  88. }
  89. }
  90. export function init_3d(path, container, status, div_width, div_height) {
  91. const width = div_width;
  92. const height = div_height;
  93. const scene = new THREE.Scene();
  94. //scene.add(new THREE.AxesHelper(1));
  95. const camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000);
  96. const renderer = new THREE.WebGLRenderer({ antialias: true });
  97. renderer.setSize(width, height);
  98. const controls = new OrbitControls(camera, renderer.domElement);
  99. controls.enableDamping = true;
  100. controls.autoRotate = true;
  101. for (const i of [1, -1]) {
  102. for (const j of [0, 1, 2]) {
  103. const light = new THREE.DirectionalLight(0xffffff, 0.5);
  104. light.position.set(i * (j == 0 ? 1 : 0),
  105. i * (j == 1 ? 1 : 0),
  106. i * (j == 2 ? 1 : 0));
  107. scene.add(light);
  108. }
  109. }
  110. if (path.endsWith(".stl")) {
  111. const light_amb = new THREE.AmbientLight(0x424242);
  112. scene.add(light_amb);
  113. const material = new THREE.MeshStandardMaterial();
  114. //material.roughness = 0.75;
  115. const loader = new STLLoader();
  116. loader.load(
  117. path,
  118. function (geometry) {
  119. const mesh = new THREE.Mesh(geometry, material);
  120. mesh.rotation.setFromVector3(new THREE.Vector3(-Math.PI / 2, 0, 0));
  121. scene.add(mesh);
  122. fitCameraToObject(camera, scene, 0, controls, 0);
  123. controls.update();
  124. status.textContent = "Loaded STL 100%";
  125. },
  126. (xhr) => {
  127. const s = Math.floor((xhr.loaded / xhr.total) * 100) + '% loaded';
  128. console.log(s);
  129. status.textContent = s;
  130. },
  131. (error) => {
  132. console.log(error);
  133. status.textContent = error;
  134. }
  135. );
  136. } else if (path.endsWith(".wrl")) {
  137. const light_amb = new THREE.AmbientLight(0xffffff);
  138. scene.add(light_amb);
  139. const loader = new VRMLLoader();
  140. loader.load(
  141. path,
  142. function (object) {
  143. scene.add(object);
  144. fitCameraToObject(camera, scene, 0, controls, 0);
  145. controls.update();
  146. status.textContent = "Loaded VRML 100%";
  147. },
  148. (xhr) => {
  149. const s = Math.floor((xhr.loaded / xhr.total) * 100) + '% loaded';
  150. console.log(s);
  151. status.textContent = s;
  152. },
  153. (error) => {
  154. console.log(error);
  155. status.textContent = error;
  156. }
  157. );
  158. } else {
  159. const s = "error: unknown filetype for " + path;
  160. console.log(s);
  161. status.textContent = s;
  162. }
  163. camera.position.z = 50;
  164. controls.update();
  165. function animate() {
  166. requestAnimationFrame(animate);
  167. controls.update();
  168. renderer.render(scene, camera);
  169. }
  170. animate();
  171. status.textContent = "3D model ready!";
  172. container.appendChild(renderer.domElement);
  173. const div = document.createElement("div");
  174. div.style.position = "absolute";
  175. div.style.left = "5px";
  176. div.style.top = "5px";
  177. div.style.background = "white";
  178. div.style.color = "black";
  179. container.appendChild(div);
  180. const chk_ar = document.createElement("input");
  181. chk_ar.type = "checkbox";
  182. chk_ar.checked = true;
  183. chk_ar.addEventListener('change', function() {
  184. controls.autoRotate = this.checked;
  185. });
  186. const div_ar = document.createElement("div");
  187. div_ar.appendChild(chk_ar);
  188. div_ar.appendChild(document.createTextNode("Auto-Rotate"));
  189. div.appendChild(div_ar);
  190. const btn_rst = document.createElement("input");
  191. btn_rst.type = "button";
  192. btn_rst.value = "Reset Camera";
  193. btn_rst.addEventListener('click', function() {
  194. fitCameraToObject(camera, scene, 0, controls, 0);
  195. controls.update();
  196. });
  197. const div_rst = document.createElement("div");
  198. div_rst.appendChild(btn_rst);
  199. div.appendChild(div_rst);
  200. }