|
| 1 | +import * as THREE from 'three' |
| 2 | +import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' |
| 3 | +import { Rhino3dmLoader } from 'three/examples/jsm/loaders/3DMLoader' |
| 4 | +import rhino3dm from 'https://cdn.jsdelivr.net/npm/[email protected]/rhino3dm.module.js' |
| 5 | +import { RhinoCompute } from 'https://cdn.jsdelivr.net/npm/[email protected]/compute.rhino3d.module.js' |
| 6 | + |
| 7 | +let data = {} |
| 8 | +data.definition = 'SampleGHConvertTo3dm.gh' |
| 9 | +data.inputs = { |
| 10 | + 'encodedFile':null, |
| 11 | + 'extension':null |
| 12 | +} |
| 13 | +data.fileName = null |
| 14 | + |
| 15 | +const downloadButton = document.getElementById("downloadButton") |
| 16 | +downloadButton.onclick = download |
| 17 | + |
| 18 | +// setup upload change events |
| 19 | +const upload = document.getElementById("upload") |
| 20 | + |
| 21 | +upload.addEventListener('change', function () { |
| 22 | + if (this.files && this.files[0]) { |
| 23 | + showSpinner(true) |
| 24 | + const file = this.files[0] |
| 25 | + data.inputs.extension = file.name.substring( file.name.lastIndexOf('.')) |
| 26 | + data.fileName = file.name.substring(0, file.name.lastIndexOf('.')) |
| 27 | + |
| 28 | + const reader = new FileReader() |
| 29 | + |
| 30 | + reader.readAsArrayBuffer(file) |
| 31 | + |
| 32 | + reader.addEventListener('load', function (e) { |
| 33 | + const buffer = new Uint8Array(e.target.result) |
| 34 | + const b64ba = base64ByteArray(buffer) |
| 35 | + data.inputs.encodedFile = b64ba |
| 36 | + |
| 37 | + compute() |
| 38 | + }) |
| 39 | + |
| 40 | + } |
| 41 | +}) |
| 42 | + |
| 43 | +// globals |
| 44 | +let rhino, definition, doc |
| 45 | +rhino3dm().then(async m => { |
| 46 | + console.log('Loaded rhino3dm.') |
| 47 | + rhino = m // global |
| 48 | + |
| 49 | + // enable download button |
| 50 | + upload.disabled = false |
| 51 | + |
| 52 | + init() |
| 53 | + |
| 54 | +}) |
| 55 | + |
| 56 | +async function compute() { |
| 57 | + |
| 58 | + console.log('compute') |
| 59 | + |
| 60 | + console.log(data) |
| 61 | + |
| 62 | + // use POST request |
| 63 | + const request = { |
| 64 | + 'method':'POST', |
| 65 | + 'body': JSON.stringify(data), |
| 66 | + 'headers': {'Content-Type': 'application/json'} |
| 67 | + } |
| 68 | + |
| 69 | + try { |
| 70 | + const response = await fetch('/solve', request) |
| 71 | + |
| 72 | + if(!response.ok) { |
| 73 | + // TODO: check for errors in response json |
| 74 | + throw new Error(response.statusText) |
| 75 | + } |
| 76 | + |
| 77 | + const responseJson = await response.json() |
| 78 | + |
| 79 | + collectResults(responseJson) |
| 80 | + |
| 81 | + } catch(error) { |
| 82 | + console.error(error) |
| 83 | + } |
| 84 | + |
| 85 | +} |
| 86 | + |
| 87 | +/** |
| 88 | + * Parse response |
| 89 | + */ |
| 90 | + function collectResults(responseJson) { |
| 91 | + |
| 92 | + const values = responseJson.values |
| 93 | + |
| 94 | + // clear doc |
| 95 | + if( doc !== undefined) |
| 96 | + doc.delete() |
| 97 | + |
| 98 | + //console.log(values) |
| 99 | + doc = new rhino.File3dm() |
| 100 | + |
| 101 | + // for each output (RH_OUT:*)... |
| 102 | + for ( let i = 0; i < values.length; i ++ ) { |
| 103 | + // ...iterate through data tree structure... |
| 104 | + for (const path in values[i].InnerTree) { |
| 105 | + const branch = values[i].InnerTree[path] |
| 106 | + // ...and for each branch... |
| 107 | + for( let j = 0; j < branch.length; j ++) { |
| 108 | + // ...load rhino geometry into doc |
| 109 | + const rhinoObject = decodeItem(branch[j]) |
| 110 | + if (rhinoObject !== null) { |
| 111 | + doc.objects().add(rhinoObject, null) |
| 112 | + } |
| 113 | + } |
| 114 | + } |
| 115 | + } |
| 116 | + |
| 117 | + if (doc.objects().count < 1) { |
| 118 | + console.error('No rhino objects to load!') |
| 119 | + showSpinner(false) |
| 120 | + return |
| 121 | + } |
| 122 | + |
| 123 | + // load rhino doc into three.js scene |
| 124 | + const buffer = new Uint8Array(doc.toByteArray()).buffer |
| 125 | + |
| 126 | + // set up loader for converting the results to threejs |
| 127 | + const loader = new Rhino3dmLoader() |
| 128 | + loader.setLibraryPath( 'https://cdn.jsdelivr.net/npm/[email protected]/' ) |
| 129 | + |
| 130 | + loader.parse( buffer, function ( object ) |
| 131 | + { |
| 132 | +/////////////////////////////////////////////////////////////////////////// |
| 133 | + // change mesh material |
| 134 | + object.traverse(child => { |
| 135 | + if (child.isMesh) { |
| 136 | + child.material = new THREE.MeshNormalMaterial({ wireframe: true}) |
| 137 | + } |
| 138 | + }, false) |
| 139 | +/////////////////////////////////////////////////////////////////////////// |
| 140 | + |
| 141 | + // clear objects from scene. do this here to avoid blink |
| 142 | + scene.traverse(child => { |
| 143 | + if (!child.isLight) { |
| 144 | + scene.remove(child) |
| 145 | + } |
| 146 | + }) |
| 147 | + |
| 148 | + // add object graph from rhino model to three.js scene |
| 149 | + scene.add( object ) |
| 150 | + |
| 151 | + // hide spinner and enable download button |
| 152 | + showSpinner(false) |
| 153 | + downloadButton.disabled = false |
| 154 | + |
| 155 | + // zoom to extents |
| 156 | + zoomCameraToSelection(camera, controls, scene.children) |
| 157 | + }) |
| 158 | +} |
| 159 | + |
| 160 | +/** |
| 161 | +* Attempt to decode data tree item to rhino geometry |
| 162 | +*/ |
| 163 | +function decodeItem(item) { |
| 164 | +const data = JSON.parse(item.data) |
| 165 | +if (item.type === 'System.String') { |
| 166 | + // hack for draco meshes |
| 167 | + try { |
| 168 | + return rhino.DracoCompression.decompressBase64String(data) |
| 169 | + } catch {} // ignore errors (maybe the string was just a string...) |
| 170 | +} else if (typeof data === 'object') { |
| 171 | + return rhino.CommonObject.decode(data) |
| 172 | +} |
| 173 | +return null |
| 174 | +} |
| 175 | + |
| 176 | +/** |
| 177 | + * Shows or hides the loading spinner |
| 178 | + */ |
| 179 | +function showSpinner(enable) { |
| 180 | + if (enable) |
| 181 | + document.getElementById('loader').style.display = 'block' |
| 182 | + else |
| 183 | + document.getElementById('loader').style.display = 'none' |
| 184 | +} |
| 185 | + |
| 186 | +// download button handler |
| 187 | +function download() { |
| 188 | + let buffer = doc.toByteArray() |
| 189 | + saveByteArray(data.fileName + '.3dm', buffer) |
| 190 | +} |
| 191 | + |
| 192 | +function saveByteArray(fileName, byte) { |
| 193 | + let blob = new Blob([byte], { type: 'application/octect-stream' }) |
| 194 | + let link = document.createElement('a') |
| 195 | + link.href = window.URL.createObjectURL(blob) |
| 196 | + link.download = fileName |
| 197 | + link.click() |
| 198 | +} |
| 199 | + |
| 200 | +// BOILERPLATE // |
| 201 | +// declare variables to store scene, camera, and renderer |
| 202 | +let scene, camera, renderer, controls |
| 203 | + |
| 204 | +function init() { |
| 205 | + |
| 206 | + // Rhino models are z-up, so set this as the default |
| 207 | + THREE.Object3D.DefaultUp = new THREE.Vector3(0, 0, 1) |
| 208 | + |
| 209 | + // create a scene and a camera |
| 210 | + scene = new THREE.Scene() |
| 211 | + scene.background = new THREE.Color(1, 1, 1) |
| 212 | + camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) |
| 213 | + camera.position.y = -30 |
| 214 | + camera.position.z = 30 |
| 215 | + |
| 216 | + // create the renderer and add it to the html |
| 217 | + renderer = new THREE.WebGLRenderer({ antialias: true }) |
| 218 | + renderer.setPixelRatio(window.devicePixelRatio) |
| 219 | + renderer.setSize(window.innerWidth, window.innerHeight) |
| 220 | + document.body.appendChild(renderer.domElement) |
| 221 | + |
| 222 | + // add some controls to orbit the camera |
| 223 | + controls = new OrbitControls(camera, renderer.domElement) |
| 224 | + |
| 225 | + // add a directional light |
| 226 | + const directionalLight = new THREE.DirectionalLight(0xffffff) |
| 227 | + directionalLight.intensity = 2 |
| 228 | + scene.add(directionalLight) |
| 229 | + |
| 230 | + const ambientLight = new THREE.AmbientLight() |
| 231 | + scene.add(ambientLight) |
| 232 | + |
| 233 | + // handle changes in the window size |
| 234 | + window.addEventListener('resize', onWindowResize, false) |
| 235 | + |
| 236 | + animate() |
| 237 | + |
| 238 | +} |
| 239 | + |
| 240 | +function onWindowResize() { |
| 241 | + camera.aspect = window.innerWidth / window.innerHeight |
| 242 | + camera.updateProjectionMatrix() |
| 243 | + renderer.setSize(window.innerWidth, window.innerHeight) |
| 244 | + animate() |
| 245 | +} |
| 246 | + |
| 247 | +// function to continuously render the scene |
| 248 | +function animate() { |
| 249 | + |
| 250 | + requestAnimationFrame(animate) |
| 251 | + renderer.render(scene, camera) |
| 252 | + |
| 253 | +} |
| 254 | + |
| 255 | +/** |
| 256 | + * Helper function that behaves like rhino's "zoom to selection", but for three.js! |
| 257 | + */ |
| 258 | +function zoomCameraToSelection(camera, controls, selection, fitOffset = 1.1) { |
| 259 | + |
| 260 | + const box = new THREE.Box3(); |
| 261 | + |
| 262 | + for (const object of selection) { |
| 263 | + if (object.isLight) continue |
| 264 | + box.expandByObject(object); |
| 265 | + } |
| 266 | + |
| 267 | + const size = box.getSize(new THREE.Vector3()); |
| 268 | + const center = box.getCenter(new THREE.Vector3()); |
| 269 | + |
| 270 | + const maxSize = Math.max(size.x, size.y, size.z); |
| 271 | + const fitHeightDistance = maxSize / (2 * Math.atan(Math.PI * camera.fov / 360)); |
| 272 | + const fitWidthDistance = fitHeightDistance / camera.aspect; |
| 273 | + const distance = fitOffset * Math.max(fitHeightDistance, fitWidthDistance); |
| 274 | + |
| 275 | + const direction = controls.target.clone() |
| 276 | + .sub(camera.position) |
| 277 | + .normalize() |
| 278 | + .multiplyScalar(distance); |
| 279 | + controls.maxDistance = distance * 10; |
| 280 | + controls.target.copy(center); |
| 281 | + |
| 282 | + camera.near = distance / 100; |
| 283 | + camera.far = distance * 100; |
| 284 | + camera.updateProjectionMatrix(); |
| 285 | + camera.position.copy(controls.target).sub(direction); |
| 286 | + |
| 287 | + controls.update(); |
| 288 | + |
| 289 | +} |
| 290 | + |
| 291 | +// https://gist.github.com/jonleighton/958841 |
| 292 | +/* |
| 293 | +MIT LICENSE |
| 294 | +Copyright 2011 Jon Leighton |
| 295 | +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
| 296 | +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. |
| 297 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 298 | +*/ |
| 299 | +function base64ByteArray(bytes) { |
| 300 | + var base64 = '' |
| 301 | + var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' |
| 302 | + |
| 303 | + // var bytes = new Uint8Array(arrayBuffer) |
| 304 | + |
| 305 | + // strip bom |
| 306 | + if (bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) |
| 307 | + bytes = bytes.slice(3) |
| 308 | + |
| 309 | + var byteLength = bytes.byteLength |
| 310 | + var byteRemainder = byteLength % 3 |
| 311 | + var mainLength = byteLength - byteRemainder |
| 312 | + |
| 313 | + var a, b, c, d |
| 314 | + var chunk |
| 315 | + |
| 316 | + // Main loop deals with bytes in chunks of 3 |
| 317 | + for (var i = 0; i < mainLength; i = i + 3) { |
| 318 | + // Combine the three bytes into a single integer |
| 319 | + chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2] |
| 320 | + |
| 321 | + // Use bitmasks to extract 6-bit segments from the triplet |
| 322 | + a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18 |
| 323 | + b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12 |
| 324 | + c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6 |
| 325 | + d = chunk & 63 // 63 = 2^6 - 1 |
| 326 | + |
| 327 | + // Convert the raw binary segments to the appropriate ASCII encoding |
| 328 | + base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d] |
| 329 | + } |
| 330 | + |
| 331 | + // Deal with the remaining bytes and padding |
| 332 | + if (byteRemainder == 1) { |
| 333 | + chunk = bytes[mainLength] |
| 334 | + |
| 335 | + a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2 |
| 336 | + |
| 337 | + // Set the 4 least significant bits to zero |
| 338 | + b = (chunk & 3) << 4 // 3 = 2^2 - 1 |
| 339 | + |
| 340 | + base64 += encodings[a] + encodings[b] + '==' |
| 341 | + } else if (byteRemainder == 2) { |
| 342 | + chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1] |
| 343 | + |
| 344 | + a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10 |
| 345 | + b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4 |
| 346 | + |
| 347 | + // Set the 2 least significant bits to zero |
| 348 | + c = (chunk & 15) << 2 // 15 = 2^4 - 1 |
| 349 | + |
| 350 | + base64 += encodings[a] + encodings[b] + encodings[c] + '=' |
| 351 | + } |
| 352 | + |
| 353 | + return base64 |
| 354 | +} |
0 commit comments