Pārlūkot izejas kodu

introduce scene.text3d for rendering 3D labels in 3D scene

Falko Schindler 2 gadi atpakaļ
vecāks
revīzija
1358230370

+ 2 - 1
main.py

@@ -292,7 +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)
+        scene.text('2D', 'background: rgba(0, 0, 0, 0.2); border-radius: 5px; padding: 5px').move(z=2)
+        scene.text3d('3D', 'background: rgba(0, 0, 0, 0.2); border-radius: 5px; padding: 5px').move(y=-2).scale(.05)
 
 with example(ui.chart):
     from numpy.random import random

+ 290 - 0
nicegui/elements/lib/CSS3DRenderer.js

@@ -0,0 +1,290 @@
+(function () {
+  const _position = new THREE.Vector3();
+  const _quaternion = new THREE.Quaternion();
+  const _scale = new THREE.Vector3();
+
+  class CSS3DObject extends THREE.Object3D {
+    constructor(element = document.createElement("div")) {
+      super();
+
+      this.isCSS3DObject = true;
+
+      this.element = element;
+      this.element.style.position = "absolute";
+      this.element.style.pointerEvents = "none";
+      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;
+    }
+  }
+
+  class CSS3DSprite extends CSS3DObject {
+    constructor(element) {
+      super(element);
+
+      this.isCSS3DSprite = true;
+
+      this.rotation2D = 0;
+    }
+
+    copy(source, recursive) {
+      super.copy(source, recursive);
+
+      this.rotation2D = source.rotation2D;
+
+      return this;
+    }
+  }
+
+  //
+
+  const _matrix = new THREE.Matrix4();
+  const _matrix2 = new THREE.Matrix4();
+
+  class CSS3DRenderer {
+    constructor(parameters = {}) {
+      const _this = this;
+
+      let _width, _height;
+      let _widthHalf, _heightHalf;
+
+      const cache = {
+        camera: { fov: 0, style: "" },
+        objects: new WeakMap(),
+      };
+
+      const domElement = parameters.element !== undefined ? parameters.element : document.createElement("div");
+
+      domElement.style.overflow = "hidden";
+
+      this.domElement = domElement;
+
+      const cameraElement = document.createElement("div");
+
+      cameraElement.style.transformStyle = "preserve-3d";
+      cameraElement.style.pointerEvents = "none";
+
+      domElement.appendChild(cameraElement);
+
+      this.getSize = function () {
+        return {
+          width: _width,
+          height: _height,
+        };
+      };
+
+      this.render = function (scene, camera) {
+        const fov = camera.projectionMatrix.elements[5] * _heightHalf;
+
+        if (cache.camera.fov !== fov) {
+          domElement.style.perspective = camera.isPerspectiveCamera ? fov + "px" : "";
+          cache.camera.fov = fov;
+        }
+
+        if (scene.autoUpdate === true) scene.updateMatrixWorld();
+        if (camera.parent === null) camera.updateMatrixWorld();
+
+        let tx, ty;
+
+        if (camera.isOrthographicCamera) {
+          tx = -(camera.right + camera.left) / 2;
+          ty = (camera.top + camera.bottom) / 2;
+        }
+
+        const cameraCSSMatrix = camera.isOrthographicCamera
+          ? "scale(" +
+            fov +
+            ")" +
+            "translate(" +
+            epsilon(tx) +
+            "px," +
+            epsilon(ty) +
+            "px)" +
+            getCameraCSSMatrix(camera.matrixWorldInverse)
+          : "translateZ(" + fov + "px)" + getCameraCSSMatrix(camera.matrixWorldInverse);
+
+        const style = cameraCSSMatrix + "translate(" + _widthHalf + "px," + _heightHalf + "px)";
+
+        if (cache.camera.style !== style) {
+          cameraElement.style.transform = style;
+
+          cache.camera.style = style;
+        }
+
+        renderObject(scene, scene, camera, cameraCSSMatrix);
+      };
+
+      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";
+
+        cameraElement.style.width = width + "px";
+        cameraElement.style.height = height + "px";
+      };
+
+      function epsilon(value) {
+        return Math.abs(value) < 1e-10 ? 0 : value;
+      }
+
+      function getCameraCSSMatrix(matrix) {
+        const elements = matrix.elements;
+
+        return (
+          "matrix3d(" +
+          epsilon(elements[0]) +
+          "," +
+          epsilon(-elements[1]) +
+          "," +
+          epsilon(elements[2]) +
+          "," +
+          epsilon(elements[3]) +
+          "," +
+          epsilon(elements[4]) +
+          "," +
+          epsilon(-elements[5]) +
+          "," +
+          epsilon(elements[6]) +
+          "," +
+          epsilon(elements[7]) +
+          "," +
+          epsilon(elements[8]) +
+          "," +
+          epsilon(-elements[9]) +
+          "," +
+          epsilon(elements[10]) +
+          "," +
+          epsilon(elements[11]) +
+          "," +
+          epsilon(elements[12]) +
+          "," +
+          epsilon(-elements[13]) +
+          "," +
+          epsilon(elements[14]) +
+          "," +
+          epsilon(elements[15]) +
+          ")"
+        );
+      }
+
+      function getObjectCSSMatrix(matrix) {
+        const elements = matrix.elements;
+        const matrix3d =
+          "matrix3d(" +
+          epsilon(elements[0]) +
+          "," +
+          epsilon(elements[1]) +
+          "," +
+          epsilon(elements[2]) +
+          "," +
+          epsilon(elements[3]) +
+          "," +
+          epsilon(-elements[4]) +
+          "," +
+          epsilon(-elements[5]) +
+          "," +
+          epsilon(-elements[6]) +
+          "," +
+          epsilon(-elements[7]) +
+          "," +
+          epsilon(elements[8]) +
+          "," +
+          epsilon(elements[9]) +
+          "," +
+          epsilon(elements[10]) +
+          "," +
+          epsilon(elements[11]) +
+          "," +
+          epsilon(elements[12]) +
+          "," +
+          epsilon(elements[13]) +
+          "," +
+          epsilon(elements[14]) +
+          "," +
+          epsilon(elements[15]) +
+          ")";
+
+        return "translate(-50%,-50%)" + matrix3d;
+      }
+
+      function renderObject(object, scene, camera, cameraCSSMatrix) {
+        if (object.isCSS3DObject) {
+          const visible = object.visible === true && object.layers.test(camera.layers) === true;
+          object.element.style.display = visible === true ? "" : "none";
+
+          if (visible === true) {
+            object.onBeforeRender(_this, scene, camera);
+
+            let style;
+
+            if (object.isCSS3DSprite) {
+              // http://swiftcoder.wordpress.com/2008/11/25/constructing-a-billboard-matrix/
+
+              _matrix.copy(camera.matrixWorldInverse);
+              _matrix.transpose();
+
+              if (object.rotation2D !== 0) _matrix.multiply(_matrix2.makeRotationZ(object.rotation2D));
+
+              object.matrixWorld.decompose(_position, _quaternion, _scale);
+              _matrix.setPosition(_position);
+              _matrix.scale(_scale);
+
+              _matrix.elements[3] = 0;
+              _matrix.elements[7] = 0;
+              _matrix.elements[11] = 0;
+              _matrix.elements[15] = 1;
+
+              style = getObjectCSSMatrix(_matrix);
+            } else {
+              style = getObjectCSSMatrix(object.matrixWorld);
+            }
+
+            const element = object.element;
+            const cachedObject = cache.objects.get(object);
+
+            if (cachedObject === undefined || cachedObject.style !== style) {
+              element.style.transform = style;
+
+              const objectData = { style: style };
+              cache.objects.set(object, objectData);
+            }
+
+            if (element.parentNode !== cameraElement) {
+              cameraElement.appendChild(element);
+            }
+
+            object.onAfterRender(_this, scene, camera);
+          }
+        }
+
+        for (let i = 0, l = object.children.length; i < l; i++) {
+          renderObject(object.children[i], scene, camera, cameraCSSMatrix);
+        }
+      }
+    }
+  }
+
+  THREE.CSS3DObject = CSS3DObject;
+  THREE.CSS3DSprite = CSS3DSprite;
+  THREE.CSS3DRenderer = CSS3DRenderer;
+})();

+ 12 - 0
nicegui/elements/scene.js

@@ -59,6 +59,7 @@ Vue.component("scene", {
     <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 style="position:absolute;pointer-events:none;top:0"></div>
     </div>`,
 
   mounted() {
@@ -92,6 +93,11 @@ Vue.component("scene", {
     });
     text_renderer.setSize(width, height);
 
+    const text3d_renderer = new THREE.CSS3DRenderer({
+      element: document.getElementById(this.$props.jp_props.id).children[2],
+    });
+    text3d_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";
@@ -110,6 +116,7 @@ Vue.component("scene", {
       TWEEN.update();
       renderer.render(scene, camera);
       text_renderer.render(scene, camera);
+      text3d_renderer.render(scene, camera);
     };
     render();
 
@@ -185,6 +192,11 @@ Vue.component("scene", {
         div.textContent = args[0];
         div.style.cssText = args[1];
         mesh = new THREE.CSS2DObject(div);
+      } else if (type == "text3d") {
+        const div = document.createElement("div");
+        div.textContent = args[0];
+        div.style.cssText = "userSelect:none;" + args[1];
+        mesh = new THREE.CSS3DObject(div);
       } else if (type == "texture") {
         const url = args[0];
         const coords = args[1];

+ 9 - 1
nicegui/elements/scene.py

@@ -13,7 +13,14 @@ from .element import Element
 from .page import Page
 from .scene_object3d import Object3D
 
-CustomView.use(__file__, ['three.min.js', 'CSS2DRenderer.js', 'OrbitControls.js', 'STLLoader.js', 'tween.umd.min.js'])
+CustomView.use(__file__, [
+    'three.min.js',
+    'CSS2DRenderer.js',
+    'CSS3DRenderer.js',
+    'OrbitControls.js',
+    'STLLoader.js',
+    'tween.umd.min.js',
+])
 
 
 @dataclass
@@ -82,6 +89,7 @@ class Scene(Element):
     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 Text3d as text3d
     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

@@ -135,6 +135,15 @@ class Text(Object3D):
         super().__init__('text', text, style)
 
 
+class Text3d(Object3D):
+
+    def __init__(self,
+                 text: str,
+                 style: str = '',
+                 ):
+        super().__init__('text3d', text, style)
+
+
 class Texture(Object3D):
 
     def __init__(self,