浏览代码

support stl meshes in 3d scene

Falko Schindler 3 年之前
父节点
当前提交
2ae488f119
共有 4 个文件被更改,包括 379 次插入3 次删除
  1. 361 0
      nicegui/elements/lib/STLLoader.js
  2. 8 2
      nicegui/elements/scene.js
  3. 2 1
      nicegui/elements/scene.py
  4. 8 0
      nicegui/elements/scene_objects.py

+ 361 - 0
nicegui/elements/lib/STLLoader.js

@@ -0,0 +1,361 @@
+(function () {
+  /**
+   * Description: A THREE loader for STL ASCII files, as created by Solidworks and other CAD programs.
+   *
+   * Supports both binary and ASCII encoded files, with automatic detection of type.
+   *
+   * The loader returns a non-indexed buffer geometry.
+   *
+   * Limitations:
+   *  Binary decoding supports "Magics" color format (http://en.wikipedia.org/wiki/STL_(file_format)#Color_in_binary_STL).
+   *  There is perhaps some question as to how valid it is to always assume little-endian-ness.
+   *  ASCII decoding assumes file is UTF-8.
+   *
+   * Usage:
+   *  const loader = new STLLoader();
+   *  loader.load( './models/stl/slotted_disk.stl', function ( geometry ) {
+   *    scene.add( new THREE.Mesh( geometry ) );
+   *  });
+   *
+   * For binary STLs geometry might contain colors for vertices. To use it:
+   *  // use the same code to load STL as above
+   *  if (geometry.hasColors) {
+   *    material = new THREE.MeshPhongMaterial({ opacity: geometry.alpha, vertexColors: true });
+   *  } else { .... }
+   *  const mesh = new THREE.Mesh( geometry, material );
+   *
+   * For ASCII STLs containing multiple solids, each solid is assigned to a different group.
+   * Groups can be used to assign a different color by defining an array of materials with the same length of
+   * geometry.groups and passing it to the Mesh constructor:
+   *
+   * const mesh = new THREE.Mesh( geometry, material );
+   *
+   * For example:
+   *
+   *  const materials = [];
+   *  const nGeometryGroups = geometry.groups.length;
+   *
+   *  const colorMap = ...; // Some logic to index colors.
+   *
+   *  for (let i = 0; i < nGeometryGroups; i++) {
+   *
+   *		const material = new THREE.MeshPhongMaterial({
+   *			color: colorMap[i],
+   *			wireframe: false
+   *		});
+   *
+   *  }
+   *
+   *  materials.push(material);
+   *  const mesh = new THREE.Mesh(geometry, materials);
+   */
+
+  class STLLoader extends THREE.Loader {
+    constructor(manager) {
+      super(manager);
+    }
+
+    load(url, onLoad, onProgress, onError) {
+      const scope = this;
+
+      const loader = new THREE.FileLoader(this.manager);
+      loader.setPath(this.path);
+      loader.setResponseType("arraybuffer");
+      loader.setRequestHeader(this.requestHeader);
+      loader.setWithCredentials(this.withCredentials);
+
+      loader.load(
+        url,
+        function (text) {
+          try {
+            onLoad(scope.parse(text));
+          } catch (e) {
+            if (onError) {
+              onError(e);
+            } else {
+              console.error(e);
+            }
+
+            scope.manager.itemError(url);
+          }
+        },
+        onProgress,
+        onError
+      );
+    }
+
+    parse(data) {
+      function isBinary(data) {
+        const reader = new DataView(data);
+        const face_size = (32 / 8) * 3 + (32 / 8) * 3 * 3 + 16 / 8;
+        const n_faces = reader.getUint32(80, true);
+        const expect = 80 + 32 / 8 + n_faces * face_size;
+
+        if (expect === reader.byteLength) {
+          return true;
+        }
+
+        // An ASCII STL data must begin with 'solid ' as the first six bytes.
+        // However, ASCII STLs lacking the SPACE after the 'd' are known to be
+        // plentiful.  So, check the first 5 bytes for 'solid'.
+
+        // Several encodings, such as UTF-8, precede the text with up to 5 bytes:
+        // https://en.wikipedia.org/wiki/Byte_order_mark#Byte_order_marks_by_encoding
+        // Search for "solid" to start anywhere after those prefixes.
+
+        // US-ASCII ordinal values for 's', 'o', 'l', 'i', 'd'
+
+        const solid = [115, 111, 108, 105, 100];
+
+        for (let off = 0; off < 5; off++) {
+          // If "solid" text is matched to the current offset, declare it to be an ASCII STL.
+
+          if (matchDataViewAt(solid, reader, off)) return false;
+        }
+
+        // Couldn't find "solid" text at the beginning; it is binary STL.
+
+        return true;
+      }
+
+      function matchDataViewAt(query, reader, offset) {
+        // Check if each byte in query matches the corresponding byte from the current offset
+
+        for (let i = 0, il = query.length; i < il; i++) {
+          if (query[i] !== reader.getUint8(offset + i, false)) return false;
+        }
+
+        return true;
+      }
+
+      function parseBinary(data) {
+        const reader = new DataView(data);
+        const faces = reader.getUint32(80, true);
+
+        let r,
+          g,
+          b,
+          hasColors = false,
+          colors;
+        let defaultR, defaultG, defaultB, alpha;
+
+        // process STL header
+        // check for default color in header ("COLOR=rgba" sequence).
+
+        for (let index = 0; index < 80 - 10; index++) {
+          if (
+            reader.getUint32(index, false) == 0x434f4c4f /*COLO*/ &&
+            reader.getUint8(index + 4) == 0x52 /*'R'*/ &&
+            reader.getUint8(index + 5) == 0x3d /*'='*/
+          ) {
+            hasColors = true;
+            colors = new Float32Array(faces * 3 * 3);
+
+            defaultR = reader.getUint8(index + 6) / 255;
+            defaultG = reader.getUint8(index + 7) / 255;
+            defaultB = reader.getUint8(index + 8) / 255;
+            alpha = reader.getUint8(index + 9) / 255;
+          }
+        }
+
+        const dataOffset = 84;
+        const faceLength = 12 * 4 + 2;
+
+        const geometry = new THREE.BufferGeometry();
+
+        const vertices = new Float32Array(faces * 3 * 3);
+        const normals = new Float32Array(faces * 3 * 3);
+
+        for (let face = 0; face < faces; face++) {
+          const start = dataOffset + face * faceLength;
+          const normalX = reader.getFloat32(start, true);
+          const normalY = reader.getFloat32(start + 4, true);
+          const normalZ = reader.getFloat32(start + 8, true);
+
+          if (hasColors) {
+            const packedColor = reader.getUint16(start + 48, true);
+
+            if ((packedColor & 0x8000) === 0) {
+              // facet has its own unique color
+
+              r = (packedColor & 0x1f) / 31;
+              g = ((packedColor >> 5) & 0x1f) / 31;
+              b = ((packedColor >> 10) & 0x1f) / 31;
+            } else {
+              r = defaultR;
+              g = defaultG;
+              b = defaultB;
+            }
+          }
+
+          for (let i = 1; i <= 3; i++) {
+            const vertexstart = start + i * 12;
+            const componentIdx = face * 3 * 3 + (i - 1) * 3;
+
+            vertices[componentIdx] = reader.getFloat32(vertexstart, true);
+            vertices[componentIdx + 1] = reader.getFloat32(
+              vertexstart + 4,
+              true
+            );
+            vertices[componentIdx + 2] = reader.getFloat32(
+              vertexstart + 8,
+              true
+            );
+
+            normals[componentIdx] = normalX;
+            normals[componentIdx + 1] = normalY;
+            normals[componentIdx + 2] = normalZ;
+
+            if (hasColors) {
+              colors[componentIdx] = r;
+              colors[componentIdx + 1] = g;
+              colors[componentIdx + 2] = b;
+            }
+          }
+        }
+
+        geometry.setAttribute(
+          "position",
+          new THREE.BufferAttribute(vertices, 3)
+        );
+        geometry.setAttribute("normal", new THREE.BufferAttribute(normals, 3));
+
+        if (hasColors) {
+          geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
+          geometry.hasColors = true;
+          geometry.alpha = alpha;
+        }
+
+        return geometry;
+      }
+
+      function parseASCII(data) {
+        const geometry = new THREE.BufferGeometry();
+        const patternSolid = /solid([\s\S]*?)endsolid/g;
+        const patternFace = /facet([\s\S]*?)endfacet/g;
+        let faceCounter = 0;
+
+        const patternFloat = /[\s]+([+-]?(?:\d*)(?:\.\d*)?(?:[eE][+-]?\d+)?)/
+          .source;
+        const patternVertex = new RegExp(
+          "vertex" + patternFloat + patternFloat + patternFloat,
+          "g"
+        );
+        const patternNormal = new RegExp(
+          "normal" + patternFloat + patternFloat + patternFloat,
+          "g"
+        );
+
+        const vertices = [];
+        const normals = [];
+
+        const normal = new Vector3();
+
+        let result;
+
+        let groupCount = 0;
+        let startVertex = 0;
+        let endVertex = 0;
+
+        while ((result = patternSolid.exec(data)) !== null) {
+          startVertex = endVertex;
+
+          const solid = result[0];
+
+          while ((result = patternFace.exec(solid)) !== null) {
+            let vertexCountPerFace = 0;
+            let normalCountPerFace = 0;
+
+            const text = result[0];
+
+            while ((result = patternNormal.exec(text)) !== null) {
+              normal.x = parseFloat(result[1]);
+              normal.y = parseFloat(result[2]);
+              normal.z = parseFloat(result[3]);
+              normalCountPerFace++;
+            }
+
+            while ((result = patternVertex.exec(text)) !== null) {
+              vertices.push(
+                parseFloat(result[1]),
+                parseFloat(result[2]),
+                parseFloat(result[3])
+              );
+              normals.push(normal.x, normal.y, normal.z);
+              vertexCountPerFace++;
+              endVertex++;
+            }
+
+            // every face have to own ONE valid normal
+
+            if (normalCountPerFace !== 1) {
+              console.error(
+                "THREE.STLLoader: Something isn't right with the normal of face number " +
+                  faceCounter
+              );
+            }
+
+            // each face have to own THREE valid vertices
+
+            if (vertexCountPerFace !== 3) {
+              console.error(
+                "THREE.STLLoader: Something isn't right with the vertices of face number " +
+                  faceCounter
+              );
+            }
+
+            faceCounter++;
+          }
+
+          const start = startVertex;
+          const count = endVertex - startVertex;
+
+          geometry.addGroup(start, count, groupCount);
+          groupCount++;
+        }
+
+        geometry.setAttribute(
+          "position",
+          new THREE.Float32BufferAttribute(vertices, 3)
+        );
+        geometry.setAttribute(
+          "normal",
+          new THREE.Float32BufferAttribute(normals, 3)
+        );
+
+        return geometry;
+      }
+
+      function ensureString(buffer) {
+        if (typeof buffer !== "string") {
+          return LoaderUtils.decodeText(new Uint8Array(buffer));
+        }
+
+        return buffer;
+      }
+
+      function ensureBinary(buffer) {
+        if (typeof buffer === "string") {
+          const array_buffer = new Uint8Array(buffer.length);
+          for (let i = 0; i < buffer.length; i++) {
+            array_buffer[i] = buffer.charCodeAt(i) & 0xff; // implicitly assumes little-endian
+          }
+
+          return array_buffer.buffer || array_buffer;
+        } else {
+          return buffer;
+        }
+      }
+
+      // start
+
+      const binData = ensureBinary(data);
+
+      return isBinary(binData)
+        ? parseBinary(binData)
+        : parseASCII(ensureString(data));
+    }
+  }
+
+  THREE.STLLoader = STLLoader;
+})();

+ 8 - 2
nicegui/elements/scene.js

@@ -1,7 +1,8 @@
 var scene;
 var camera;
 var orbitControls;
-var loader = new THREE.TextureLoader();
+var texture_loader = new THREE.TextureLoader();
+var stl_loader = new THREE.STLLoader();
 var objects = new Map();
 
 const False = false;
@@ -164,7 +165,7 @@ Vue.component("scene", {
         geometry.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
         geometry.computeVertexNormals();
         geometry.computeFaceNormals();
-        const texture = loader.load(url);
+        const texture = texture_loader.load(url);
         texture.flipY = false;
         texture.minFilter = THREE.LinearFilter;
         const material = new THREE.MeshLambertMaterial({
@@ -188,6 +189,11 @@ Vue.component("scene", {
           const settings = { depth: height, bevelEnabled: false };
           geometry = new THREE.ExtrudeGeometry(shape, settings);
         }
+        if (type == "stl") {
+          const url = args[0];
+          geometry = new THREE.BoxGeometry();
+          stl_loader.load(url, (geometry) => (mesh.geometry = geometry));
+        }
         let material;
         if (wireframe) {
           mesh = new THREE.LineSegments(

+ 2 - 1
nicegui/elements/scene.py

@@ -7,7 +7,7 @@ from .scene_object3d import Object3D
 class SceneView(CustomView):
 
     def __init__(self, *, width: int, height: int, on_click: Callable):
-        dependencies = ['three.min.js', 'OrbitControls.js']
+        dependencies = ['three.min.js', 'OrbitControls.js', 'STLLoader.js']
         super().__init__('scene', __file__, dependencies, width=width, height=height)
         self.on_click = on_click
         self.allowed_events = ['onConnect', 'onClick']
@@ -38,6 +38,7 @@ class Scene(Element):
     from .scene_objects import Sphere as sphere
     from .scene_objects import Cylinder as cylinder
     from .scene_objects import Extrusion as extrusion
+    from .scene_objects import Stl as stl
     from .scene_objects import Line as line
     from .scene_objects import Curve as curve
     from .scene_objects import Texture as texture

+ 8 - 0
nicegui/elements/scene_objects.py

@@ -51,6 +51,14 @@ class Extrusion(Object3D):
                  ):
         super().__init__('extrusion', outline, height, wireframe)
 
+class Stl(Object3D):
+
+    def __init__(self,
+                 url: str,
+                 wireframe: bool = False,
+                 ):
+        super().__init__('stl', url, wireframe)
+
 class Line(Object3D):
 
     def __init__(self,