瀏覽代碼

introduce scene.text for rendering 2D labels in 3D scene

Falko Schindler 2 年之前
父節點
當前提交
818d5d7a90
共有 6 個文件被更改,包括 202 次插入29 次删除
  1. 1 0
      .vscode/settings.json
  2. 2 0
      main.py
  3. 167 0
      nicegui/elements/lib/CSS2DRenderer.js
  4. 21 28
      nicegui/elements/scene.js
  5. 2 1
      nicegui/elements/scene.py
  6. 9 0
      nicegui/elements/scene_objects.py

+ 1 - 0
.vscode/settings.json

@@ -2,6 +2,7 @@
   "editor.defaultFormatter": "esbenp.prettier-vscode",
   "editor.formatOnSave": true,
   "editor.minimap.enabled": false,
+  "prettier.printWidth": 120,
   "python.formatting.provider": "autopep8",
   "python.formatting.autopep8Args": ["--max-line-length=120", "--experimental"],
   "python.sortImports.args": ["--line-length=120"],

+ 2 - 0
main.py

@@ -292,6 +292,8 @@ with example(ui.scene):
         teapot = 'https://upload.wikimedia.org/wikipedia/commons/9/93/Utah_teapot_(solid).stl'
         scene.stl(teapot).scale(0.2).move(-3, 4)
 
+        scene.text('3D', 'background-color: rgba(0, 0, 0, 0.2); border-radius: 5px; padding: 5px').move(z=2)
+
 with example(ui.chart):
     from numpy.random import random
 

+ 167 - 0
nicegui/elements/lib/CSS2DRenderer.js

@@ -0,0 +1,167 @@
+(function () {
+  class CSS2DObject extends THREE.Object3D {
+    constructor(element = document.createElement("div")) {
+      super();
+
+      this.isCSS2DObject = true;
+
+      this.element = element;
+
+      this.element.style.position = "absolute";
+      this.element.style.userSelect = "none";
+
+      this.element.setAttribute("draggable", false);
+
+      this.addEventListener("removed", function () {
+        this.traverse(function (object) {
+          if (object.element instanceof Element && object.element.parentNode !== null) {
+            object.element.parentNode.removeChild(object.element);
+          }
+        });
+      });
+    }
+
+    copy(source, recursive) {
+      super.copy(source, recursive);
+
+      this.element = source.element.cloneNode(true);
+
+      return this;
+    }
+  }
+
+  //
+
+  const _vector = new THREE.Vector3();
+  const _viewMatrix = new THREE.Matrix4();
+  const _viewProjectionMatrix = new THREE.Matrix4();
+  const _a = new THREE.Vector3();
+  const _b = new THREE.Vector3();
+
+  class CSS2DRenderer {
+    constructor(parameters = {}) {
+      const _this = this;
+
+      let _width, _height;
+      let _widthHalf, _heightHalf;
+
+      const cache = {
+        objects: new WeakMap(),
+      };
+
+      const domElement = parameters.element !== undefined ? parameters.element : document.createElement("div");
+
+      domElement.style.overflow = "hidden";
+
+      this.domElement = domElement;
+
+      this.getSize = function () {
+        return {
+          width: _width,
+          height: _height,
+        };
+      };
+
+      this.render = function (scene, camera) {
+        if (scene.autoUpdate === true) scene.updateMatrixWorld();
+        if (camera.parent === null) camera.updateMatrixWorld();
+
+        _viewMatrix.copy(camera.matrixWorldInverse);
+        _viewProjectionMatrix.multiplyMatrices(camera.projectionMatrix, _viewMatrix);
+
+        renderObject(scene, scene, camera);
+        zOrder(scene);
+      };
+
+      this.setSize = function (width, height) {
+        _width = width;
+        _height = height;
+
+        _widthHalf = _width / 2;
+        _heightHalf = _height / 2;
+
+        domElement.style.width = width + "px";
+        domElement.style.height = height + "px";
+      };
+
+      function renderObject(object, scene, camera) {
+        if (object.isCSS2DObject) {
+          _vector.setFromMatrixPosition(object.matrixWorld);
+          _vector.applyMatrix4(_viewProjectionMatrix);
+
+          const visible =
+            object.visible === true && _vector.z >= -1 && _vector.z <= 1 && object.layers.test(camera.layers) === true;
+          object.element.style.display = visible === true ? "" : "none";
+
+          if (visible === true) {
+            object.onBeforeRender(_this, scene, camera);
+
+            const element = object.element;
+
+            element.style.transform =
+              "translate(-50%,-50%) translate(" +
+              (_vector.x * _widthHalf + _widthHalf) +
+              "px," +
+              (-_vector.y * _heightHalf + _heightHalf) +
+              "px)";
+
+            if (element.parentNode !== domElement) {
+              domElement.appendChild(element);
+            }
+
+            object.onAfterRender(_this, scene, camera);
+          }
+
+          const objectData = {
+            distanceToCameraSquared: getDistanceToSquared(camera, object),
+          };
+
+          cache.objects.set(object, objectData);
+        }
+
+        for (let i = 0, l = object.children.length; i < l; i++) {
+          renderObject(object.children[i], scene, camera);
+        }
+      }
+
+      function getDistanceToSquared(object1, object2) {
+        _a.setFromMatrixPosition(object1.matrixWorld);
+        _b.setFromMatrixPosition(object2.matrixWorld);
+
+        return _a.distanceToSquared(_b);
+      }
+
+      function filterAndFlatten(scene) {
+        const result = [];
+
+        scene.traverse(function (object) {
+          if (object.isCSS2DObject) result.push(object);
+        });
+
+        return result;
+      }
+
+      function zOrder(scene) {
+        const sorted = filterAndFlatten(scene).sort(function (a, b) {
+          if (a.renderOrder !== b.renderOrder) {
+            return b.renderOrder - a.renderOrder;
+          }
+
+          const distanceA = cache.objects.get(a).distanceToCameraSquared;
+          const distanceB = cache.objects.get(b).distanceToCameraSquared;
+
+          return distanceA - distanceB;
+        });
+
+        const zMax = sorted.length;
+
+        for (let i = 0, l = sorted.length; i < l; i++) {
+          sorted[i].element.style.zIndex = zMax - i;
+        }
+      }
+    }
+  }
+
+  THREE.CSS2DRenderer = CSS2DRenderer;
+  THREE.CSS2DObject = CSS2DObject;
+})();

+ 21 - 28
nicegui/elements/scene.js

@@ -27,12 +27,7 @@ function texture_geometry(coords) {
   }
   for (let j = 0; j < nJ - 1; ++j) {
     for (let i = 0; i < nI - 1; ++i) {
-      if (
-        coords[j][i] &&
-        coords[j][i + 1] &&
-        coords[j + 1][i] &&
-        coords[j + 1][i + 1]
-      ) {
+      if (coords[j][i] && coords[j][i + 1] && coords[j + 1][i] && coords[j + 1][i + 1]) {
         const idx00 = i + j * nI;
         const idx10 = i + j * nI + 1;
         const idx01 = i + j * nI + nI;
@@ -43,10 +38,7 @@ function texture_geometry(coords) {
     }
   }
   geometry.setIndex(new THREE.Uint32BufferAttribute(indices, 1));
-  geometry.setAttribute(
-    "position",
-    new THREE.Float32BufferAttribute(vertices, 3)
-  );
+  geometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
   geometry.setAttribute("uv", new THREE.Float32BufferAttribute(uvs, 2));
   geometry.computeVertexNormals();
   geometry.computeFaceNormals();
@@ -63,7 +55,11 @@ function texture_material(texture) {
 }
 
 Vue.component("scene", {
-  template: `<canvas v-bind:id="jp_props.id"></div>`,
+  template: `
+    <div v-bind:id="jp_props.id" style="position:relative">
+      <canvas style="position:relative"></canvas>
+      <div style="position:absolute;pointer-events:none;top:0"></div>
+    </div>`,
 
   mounted() {
     scene = new THREE.Scene();
@@ -86,15 +82,17 @@ Vue.component("scene", {
     const renderer = new THREE.WebGLRenderer({
       antialias: true,
       alpha: true,
-      canvas: document.getElementById(this.$props.jp_props.id),
+      canvas: document.getElementById(this.$props.jp_props.id).children[0],
     });
     renderer.setClearColor("#eee");
     renderer.setSize(width, height);
 
-    const ground = new THREE.Mesh(
-      new THREE.PlaneGeometry(100, 100),
-      new THREE.MeshPhongMaterial({ color: "#eee" })
-    );
+    const text_renderer = new THREE.CSS2DRenderer({
+      element: document.getElementById(this.$props.jp_props.id).children[1],
+    });
+    text_renderer.setSize(width, height);
+
+    const ground = new THREE.Mesh(new THREE.PlaneGeometry(100, 100), new THREE.MeshPhongMaterial({ color: "#eee" }));
     ground.translateZ(-0.01);
     ground.object_id = "ground";
     scene.add(ground);
@@ -111,6 +109,7 @@ Vue.component("scene", {
       requestAnimationFrame(() => setTimeout(() => render(), 1000 / 20));
       TWEEN.update();
       renderer.render(scene, camera);
+      text_renderer.render(scene, camera);
     };
     render();
 
@@ -181,6 +180,11 @@ Vue.component("scene", {
         const geometry = new THREE.BufferGeometry().setFromPoints(points);
         const material = new THREE.LineBasicMaterial({ transparent: true });
         mesh = new THREE.Line(geometry, material);
+      } else if (type == "text") {
+        const div = document.createElement("div");
+        div.textContent = args[0];
+        div.style.cssText = args[1];
+        mesh = new THREE.CSS2DObject(div);
       } else if (type == "texture") {
         const url = args[0];
         const coords = args[1];
@@ -284,18 +288,7 @@ Vue.component("scene", {
     set_texture_coordinates(object_id, coords) {
       objects.get(object_id).geometry = texture_geometry(coords);
     },
-    move_camera(
-      x,
-      y,
-      z,
-      look_at_x,
-      look_at_y,
-      look_at_z,
-      up_x,
-      up_y,
-      up_z,
-      duration
-    ) {
+    move_camera(x, y, z, look_at_x, look_at_y, look_at_z, up_x, up_y, up_z, duration) {
       if (camera_tween) camera_tween.stop();
       camera_tween = new TWEEN.Tween([
         camera.position.x,

+ 2 - 1
nicegui/elements/scene.py

@@ -13,7 +13,7 @@ from .element import Element
 from .page import Page
 from .scene_object3d import Object3D
 
-CustomView.use(__file__, ['three.min.js', 'OrbitControls.js', 'STLLoader.js', 'tween.umd.min.js'])
+CustomView.use(__file__, ['three.min.js', 'CSS2DRenderer.js', 'OrbitControls.js', 'STLLoader.js', 'tween.umd.min.js'])
 
 
 @dataclass
@@ -81,6 +81,7 @@ class Scene(Element):
     from .scene_objects import Sphere as sphere
     from .scene_objects import SpotLight as spot_light
     from .scene_objects import Stl as stl
+    from .scene_objects import Text as text
     from .scene_objects import Texture as texture
 
     def __init__(self, width: int = 400, height: int = 300, on_click: Optional[Callable] = None):

+ 9 - 0
nicegui/elements/scene_objects.py

@@ -126,6 +126,15 @@ class Curve(Object3D):
         super().__init__('curve', start, control1, control2, end, num_points)
 
 
+class Text(Object3D):
+
+    def __init__(self,
+                 text: str,
+                 style: str = '',
+                 ):
+        super().__init__('text', text, style)
+
+
 class Texture(Object3D):
 
     def __init__(self,