Three.js: dependencias cíclicas

Creado en 16 mar. 2015  ·  81Comentarios  ·  Fuente: mrdoob/three.js

Hola a todos.

@kumavis y yo hemos estado trabajando arduamente para encontrar una manera eficiente de mover THREE.js a una arquitectura browserify. Hicimos un buen progreso, incluso hasta el punto de mover todos los archivos a un sistema de compilación browserify y poder generar un three.min.js con gulp.

Desafortunadamente, los ejemplos no funcionan porque, a diferencia de commonjs, browserify no puede manejar dependencias cíclicas, de las cuales hay muchas en THREE.js.

He hecho un gráfico interactivo que representa las relaciones de dependencia aquí .

A menos y hasta que estos se desenreden, no podremos mover THREE.js a una compilación browserify.

No considero que esto sea una deficiencia de browserify, sino un problema con THREE.js. Las dependencias circulares son algo malo en el software en general y generan todo tipo de problemas.

Suggestion

Comentario más útil

@Mugen87 guau, 5 años! felicidades por finalmente darle :fuego: :clap:
Me divertí mucho haciendo esos gráficos en ese entonces :smile_cat:

Todos 81 comentarios

eso es todo un nudo para desenredar
http://jsbin.com/medezu/2/edit?html,js,salida
image

@coballast , ¿puedes publicar el código que usaste para generar la dependencia json?

Simplemente use el archivo three.min.js precompilado directamente. No hay necesidad de dividir Three.js en archivos individuales dentro de Browserfy, solo está haciendo su vida más difícil sin ningún beneficio real.

Hablo por experiencia, ya que usamos el módulo npm de three.js y funciona muy bien. Simplemente lo empaquetamos como un solo archivo y lo envolvemos en un módulo de estilo CommonJS. Este enfoque funcionaría para browserfy y mucha gente ya lo está haciendo, según tengo entendido.

No es necesario desenredar este nudo para este caso de uso.

@kumavis Acabo de descargar la estructura de dependencia. El siguiente código generó el gráfico:

var fs = require('fs-extra');
var unique = require('uniq');
var util = require('util');

function getRequiredObjects(dependencies){
  var result = [];
  for(var i = 0; i < dependencies.usedObjects.length; i++){
    var object = dependencies.usedObjects[i];
    if(dependencies.definedObjects.indexOf(object) == -1)
      result.push(object);
  }

  return result;
};

var dependencies = JSON.parse(fs.readFileSync('./dependencies.json'));

var objects = [];
for(var f in dependencies){
  objects = objects.concat(dependencies[f].usedObjects);
  objects = objects.concat(dependencies[f].definedObjects);
}

objects = unique(objects);


var nodes = objects.map(function(o){
  return {data: {id: o} };
});

var edges = [];
for(var f in dependencies){
  var dependency = dependencies[f];
  var requiredObjects = getRequiredObjects(dependency);
  for(var j = 0; j < dependency.definedObjects.length; j++){
    for(var k = 0; k < requiredObjects.length; k++){
      edges.push({ data: { source: dependency.definedObjects[j], target: requiredObjects[k] } });
    }
  }
}

var graph = {nodes: nodes, edges: edges};

var eliminateImpossibleCycleNodes = function(graph){
  graph.nodes = graph.nodes.filter(function(node){
    var source_edge = null;
    var dest_edge = null;
    for(var i = 0; i < graph.edges.length; i++){
      if(graph.edges[i].data.source == node.data.id)
        source_edge = graph.edges[i];
      if(graph.edges[i].data.target == node.data.id)
        dest_edge = graph.edges[i];
    }

    if(source_edge != null && dest_edge != null)
      return true;
    else
      return false;
  });

  graph.edges = graph.edges.filter(function(edge){
    var source_exists = false, target_exists = false;
    for(var i = 0; i < graph.nodes.length; i++){
      if(edge.data.source == graph.nodes[i].data.id)
        source_exists = true;
      if(edge.data.target == graph.nodes[i].data.id)
        target_exists = true;
    }

    return source_exists && target_exists;
  });
};

for(var i = 0; i < 500; i++)
  eliminateImpossibleCycleNodes(graph)


console.log(JSON.stringify(graph));

@bhouston se trata más de la salud del código base de three.js

Solo sé que en la biblioteca de matemáticas, en la que ayudé bastante, las dependencias cíclicas son la norma en todos los idiomas. Porque las funciones en Matrix4 pueden tomar Vector3 como parámetros, y Vector3 puede ser transformado por Matrix4. Hacer todas las dependencias de una manera en la biblioteca de matemáticas haría que esa parte de la biblioteca fuera molesta de usar.

Ahora, defiendo que la biblioteca de matemáticas no conozca ninguna otra parte de la biblioteca: los tipos más complejos no deberían filtrarse en ese módulo. Por lo tanto, en ese sentido, abogo por tratar de reducir las dependencias cíclicas entre módulos, pero sin eliminar todas las dependencias cíclicas entre archivos individuales dentro de un módulo.

He aquí un caso que ilustra las sutiles complicaciones. Para ser claros, aquí no critico la implementación en sí, sino los efectos secundarios.

Vector3 y Matrix4 forman una dependencia cíclica porque exponen un rango de funciones que se usan entre sí como tipos de entrada o salida. Ambos se implementan con un estilo común a Three.js, definiendo funciones a través de IIFE para incluir variables temporales para realizar cálculos.

Matrix4#lookAt puede instanciar el scratch inmediatamente, como parte de la definición de la función.

lookAt: function () {

  var x = new THREE.Vector3();
  var y = new THREE.Vector3();
  var z = new THREE.Vector3();

  return function ( eye, target, up ) {
    /* ... */

Sin embargo, Vector3#project debe crear una instancia del scratch en la primera ejecución.

project: function () {

  var matrix;

  return function ( camera ) {

    if ( matrix === undefined ) matrix = new THREE.Matrix4();

    /* ... */

¿Por qué? porque al definir la Clase, aún no se han definido todas las Clases. Al definir Vector3 , Matrix4 aún no existe. Ahora, el tiempo real de creación de instancias de las variables temporales no es realmente importante. La conclusión real aquí es que la implementación actual depende del orden en que el sistema de compilación concatena los archivos. Ese es un acoplamiento realmente distante, y los cambios en el sistema de compilación o el cambio de nombre de los archivos de tal manera que cambia el orden de concatenación pueden generar compilaciones no válidas, sin una conexión obvia.

Esta es solo una de las formas en que este nudo se manifiesta en errores. Sin embargo, aunque podemos abordar este problema específico, no tengo una solución general que no requiera muchos cambios importantes en la API.

Hmm... Eché un vistazo a la biblioteca de matemáticas C++ de ILM, que considero que es el estándar de oro en términos de bibliotecas de matemáticas. Sorprendentemente, no son cíclicos. Básicamente tienen un orden bien definido de tipos simples a complejos que definen y supongo que si lo haces con mucho cuidado funciona:

https://github.com/openexr/openexr/tree/master/IlmBase/Imath

Math y luego Vec parece ser el más simple.

Otras observaciones sobre el gráfico de dependencia:

Material tienen dependencias bidireccionales con su clase base
image
Un poco difícil de ver, pero Geometry parecen tener buenas relaciones unidireccionales con la clase base.
image
Light s y Camera s tienen una situación similar, buena con respecto a su clase base, pero la dependencia Object3D _sobre ellos_ parece innecesaria.
image
image
Curve s Path s Line s parece bueno, pero Shape está un poco enredado.
image

@coballast gracias! esta es una gran idea.

Me agrego para los comentarios :)

Por cierto, he mirado en la forma en que Material depende de MeshDepthMaterial, por ejemplo. Es simple

if ( this instanceof THREE.MeshDepthMaterial )

que es trivial cambiar a

if ( this.type == 'MeshDepthMaterial' )

y listo, sin dependencia. Supongo que la mitad de este gráfico aterrador es el mismo nivel de problema.

Lo curioso es que esta dependencia se lleva a cabo en un único método JSON. Quiero decir, ¿no podría simplemente reemplazarse en MeshDepthMaterial? algo como

THREE.MeshDepthMaterial.prototype.toJSON =  function () {
    var output = THREE.Material.prototype.toJSON.apply(this);
    if ( this.blending !== THREE.NormalBlending ) output.blending = this.blending;
    if ( this.side !== THREE.FrontSide ) output.side = this.side;

@makc En general, en cualquier lugar donde estemos haciendo instanceof , debemos mover ese código a la clase específica en sí. Eso ayudará a eliminar muchos de los nudos.

solo quería decir que si bien AMD no admite referencias circulares, los módulos ES6 sí lo hacen https://github.com/ModuleLoader/es6-module-loader/wiki/Circular-References-&-Bindings

Tengo curiosidad por saber, además del problema de resolución de dependencias (que se puede resolver en la implementación de un cargador de sistema de módulos, por ejemplo, system.js ), ¿qué problemas crea la referencia circular en three.js?

Afortunadamente, parece que podemos atacar esto por etapas. Creo que se han realizado muchos cambios importantes en la API en versiones anteriores, por lo que no estoy seguro de que eso esté fuera de discusión.

Para los casos instanceof (quizás la mayoría), deberían poder resolverse sin cambios importantes.

Yo también me suscribo aquí. Vamos paso a paso aquí.
Estoy de acuerdo en que debemos eliminar todas las dependencias cíclicas innecesarias como la material.
También estoy de acuerdo con @bhouston en que la biblioteca de matemáticas depende mucho entre sí porque la interacción es lo que hace que la biblioteca de matemáticas sea útil.

¿Alguien puede trazar los fáciles? Tener dependencias menos cíclicas siempre es una buena idea si no obstaculiza la biblioteca. Más tarde podemos ver qué hacer con los demás.

@ zz85 También me encontré con el problema de las dependencias circulares. Es principalmente un problema para cuando intentamos crear previamente ciertos objetos dentro de archivos de referencia circulares.

6252 debería eliminar muchas depresiones circulares en Material y Object3D .

Así es como se ve Mesh . Tal vez algunos fondos extraños pero no demasiado locos.
image

Circular con Object3D y Geometry . La referencia Object3D -> Mesh se aborda en el PR anterior. La referencia Mesh -> Geometry está bien, b/c Mesh controla una instancia de Geometry . Todavía podría romperse porque está haciendo una verificación de tipo para el comportamiento específico de la clase ( Geometry / BufferGeometry ).

En cuanto a la referencia Geometry -> Mesh , es para proporcionar geometry.mergeMesh( mesh ) . Geometry es un concepto de nivel más bajo que Mesh , por lo que lo invertiría como mesh.mergeIntoGeometry( geo ) y desaprobaría mergeMesh .

Si alguien obtiene una combinación de relaciones públicas que corrige algunos de estos, hágamelo saber y actualizaré el gráfico para reflejar el estado actual de las cosas.

@bhouston @gero3 No estoy convencido de que se requieran dependencias cíclicas para obtener el mismo nivel de facilidad de uso/utilidad para la biblioteca matemática. Podría estar equivocado, pero ¿no podríamos mantener Vector3 completamente aislado/ignorante del resto del sistema y modificar su prototipo para acomodar Matrix4 en el módulo Matrix4? Eso tiene sentido para mí conceptualmente, ya que las Matrices son más complejas que los Vectores. Creo que es mejor tener un orden bien definido en el que se construyen los prototipos y las clases para evitar percances.

@bhouston @gero3 Creo que podemos hacer eso sin cambiar la API en absoluto. Voy a hurgar a ver qué es qué.

con respecto a las matemáticas, podrías tener todos los "blocs de notas" en un solo lugar, supongo. pero apuesto a que no habrá un solo gráfico utilizable de 3js que no tenga Vector3 y Matrix4

Si hay una solución que no cambia el rendimiento o la API de la biblioteca matemática, estoy totalmente de acuerdo.

@coballast no puede eliminar la dependencia cíclica sin un cambio de API, porque ambos proporcionan métodos que usan el otro tipo. Vector3 Matrix4

En cuanto a browserify compat, nuestro único requisito es mover la creación de instancias en las variables temporales fuera del tiempo de definición de clase (hacer que se creen instancias en la primera ejecución). Haz esto perezoso así . Eso no tendría ningún impacto en la API o el rendimiento.

Creo que ese tipo de cambios están bien.

@kumavis Ah! Si. Esta bien, lo entiendo ahora. Eso es bastante fácil.

Apoyo totalmente que TRES se divida en módulos más pequeños con una estructura requerida, por las siguientes razones:

  1. TRES es muy grande. Si un usuario pudiera requerir solo lo que necesita, podría reducir los tamaños de compilación del cliente. Por ejemplo , react-bootstrap le permite hacer cosas como var Alert = require('react-bootstrap/lib/Alert'); que no incluye todos los módulos de arranque.
  2. Hay algunos "complementos", como OrbitControls.js que modifican los TRES objetos globales en sí mismos, colocándose en THREE.OrbitControls . Este es un antipatrón en los marcos de JavaScript modernos porque requiere que TRES estén presentes en el espacio de nombres global en un proceso de compilación, en lugar de que lo requieran los archivos que lo necesitan. THREE también hace esto internamente, siempre modificando el espacio de nombres THREE, que no es ideal para incluir TRES módulos específicos.

poniéndose en TRES.OrbitControls

pero todos y cada uno de los fragmentos de código en 3js hacen eso?

@DelvarWorld escribió:

Apoyo totalmente que TRES se divida en módulos más pequeños con una estructura requerida, por las siguientes razones:

Solía ​​pensar que era una buena idea dividirlo, pero hay una simplicidad en ThreeJS tal como es ahora. Es más útil para aquellos nuevos en 3D en la forma en que se encuentra ahora y esa ha sido una prioridad para el equipo de desarrollo. Puede usar ThreeJS sin necesidad de un sistema de módulos (de los cuales hay muchos, y no todos son totalmente compatibles).

Al igual que @makc , también estoy desconcertado por la sugerencia de @DelvarWorld de no poner cosas en el espacio de nombres TRES.

¿Dónde / cómo estarían en su lugar?

A mí me parece un buen patrón crear solo un objeto global, TRES, en el que estén todas las partes (y posiblemente algunas extensiones/complementos).

Estoy de acuerdo con @DelvarWorld en que la técnica de ponerlo en el global no es buena para la salud del código base; es una especie de argumento sutil porque ponerlo en el global en sí no es el problema, es el gráfico de dependencia oculto y otras prácticas que resultan de tener el global disponible.

Pero ese argumento se limita principalmente al desarrollo interno y la estructura del código. En cuanto a entregar la biblioteca como un paquete de código estático, para mí tiene sentido poner todas las clases en el TRES global.

Un contraargumento es que al deserializar un json de escena THREE.js, las entradas pueden enumerar su clase como una cadena que se puede extraer del global como: THREE[ obj.type ] . Esto funciona con clases que no están en la biblioteca estándar de three.js siempre que las defina en THREE antes de deserializar. No estoy seguro de cuál es la mejor manera de reemplazar este comportamiento sin el THREE global.

Esto funciona con clases que no están en la biblioteca estándar three.js siempre que las defina en TRES antes de deserializar. No estoy seguro de cuál es la mejor manera de reemplazar este comportamiento sin los TRES globales.

Puedes hacer este patrón (o alguna variante del mismo) si todo es un módulo:

var objectType = require( "THREE." + obj.type );

Vienen muchos cambios con ES6 con respecto a los módulos. Volvería a visitar la modularidad de ThreeJS en ese momento.

La versión construida de tres (el archivo javascript que las personas pueden descargar manualmente) todavía tendría todo en el espacio de nombres tres. podría hacer esto con el archivo de punto de entrada para la compilación:

var THREE = {
    Geometry: require("./geometry"),

etc., que aún sería útil para los recién llegados y fácil de comenzar.

Para aquellos que usan tres de npm y requirejs/browserify/webpack en una compilación moderna de javascript, podríamos hacer algo como

var Scene = require("three/scene"),
     Camera = require("three/camera"),

etc., lo que podría reducir parte del tamaño de tres integrados en un paquete de tamaño de cliente. Podría estar equivocado porque no sé cuánto de tres es "núcleo". Ahora mismo, sin embargo, esto es imposible porque three no usa instrucciones require.

de cualquier manera, el patrón moderno requiere módulos, y en lugar de hacer que todo su código modifique otra biblioteca (modificando el TRES global, mal), su código es independiente y modular, y especifica lo que requiere con declaraciones require , como el código fuente de React .

No creo que sea útil tratar de presentar un argumento completo para usar la sintaxis require/module, ya que hay muchos buenos recursos en línea al respecto. Pero es malo alentar a otros a modificar los TRES espacios de nombres para agregar complementos como OrbitControls.

Solo tenga en cuenta @DelvarWorld que ES6 introduce módulos oficialmente en JavaScript con una sintaxis muy específica y distinta:

http://www.2ality.com/2014/09/es6-modules-final.html

@bhouston oh sí, totalmente, soy agnóstico para requerir vs importar (importar es probablemente la mejor opción), solo admitiendo el patrón del módulo en general.

@bhouston @DelvarWorld @kumavis Uno de mis proyectos a largo plazo es escribir un convertidor automático es5 -> es6 que pueda acomodar y convertir módulos commonjs/amd a es6, y con suerte identificar y reescribir una gran cantidad de javascript usando construcciones es6 como clases/generadores y así. Se podría usar una transformación browserify como es6ify mientras los navegadores se están poniendo al día con el estándar para preparar el código para el consumo. Hacer que TRES pasen a browserify solo a nivel interno es un buen primer paso para prepararlo para la entrada a dicha herramienta.

Sin embargo, esto está fuera del punto que estaba (aparentemente muy mal) tratando de hacer. Quiero eliminar la mayor cantidad posible de estas dependencias cíclicas, independientemente de cualquier problema de modularidad, porque creo que hará que TRES sea más estable, flexible y probablemente eliminará muchos errores como un efecto secundario agradable.

@coballast https://github.com/mrdoob/three.js/pull/6252 se fusionó, debería reducir muchas de las depresiones cíclicas. ¿Crees que puedes generar un nuevo gráfico de dep? Tal vez convertirlo en una utilidad en el repositorio de la herramienta de conversión

el siguiente es: hacer que las variables temporales en Vector3 Matrix4 se definan de forma perezosa en el primer uso, no en el momento de la definición

alguien quiere ser voluntario? debería ser rápido

El gráfico ha sido actualizado. http://jsbin.com/medezu/3/

Aquí hay una captura de pantalla:

snapshot3

Me complace informar que se ha eliminado una gran cantidad de circ deps de Object3D. ¡Buen trabajo @kumavis!

wow es que la misma base de código? loco

Trabajará para hacer que la generación de gráficos sea parte de la utilidad.

Solo en la inspección del gráfico, Shape y Geometry parecen ser posibles árboles de clase que se pueden desentrañar.

@coballast ¿Crees que puedes encargarte de esto?

hacer que las variables temporales en Vector3 Matrix4 se definan de forma perezosa en el primer uso, no en el momento de la definición

eso sería una PR contra upstream dev en lugar de un cambio automatizado

También creo que podemos degradar el título del problema de "Problemas graves de dependencia cíclica" a "Dependencias cíclicas". ¡La situación ha mejorado mucho!

@kumavis Claro. Trabajará en ello cuando el tiempo lo permita.

Aquí está el estado actual de la limpieza de interdependencia tal como lo veo:
(las flechas muestran las conexiones que deben eliminarse si corresponde)

  • [x] Materiales
  • [x] geometrías
  • [x] Objetos 3D
  • [x] matemáticas
  • [x] Formas

    • [x] Forma -> FontUtils

    • [x] Forma -> Extruir Geometría

    • [x] Forma -> FormaGeometría

    • [x] Ruta -> Forma

  • [ ] Caja3

    • [ ] Box3 -> BufferGeometry

    • [ ] Box3 -> Geometría

Formas:

image

Caja 3:

image

Matemáticas:

Estos nodos están interconectados, pero brindan suficiente comodidad a través de esto.
image

Shape parece tener dependencias con ExtrudeGeometry y ShapeGeometry a través de un código como este:

// Convenience method to return ExtrudeGeometry

THREE.Shape.prototype.extrude = function ( options ) {

  var extruded = new THREE.ExtrudeGeometry( this, options );
  return extruded;

};

// Convenience method to return ShapeGeometry

THREE.Shape.prototype.makeGeometry = function ( options ) {

  var geometry = new THREE.ShapeGeometry( this, options );
  return geometry;

};

Ahora parece que Shape es una subclase de Path , y ExtrudeGeometry y ShapeGeometry son ambas subclases de Geometry . Entonces, ya sabes, puedes averiguar qué dependencias deberían desaparecer en el caso ideal.

Sí, esto entra en la misma categoría que Vector3 <-> Matrix4 . Están vinculados por conveniencia. Creo que es una mala idea, pero no vale la pena luchar contra eso. lo marcare como completo

Shape -> FontUtils podría eliminarse usando métodos de movimiento como triangulate a Utils más genéricos. Pero no es una gran victoria hacer eso. Voy a marcarlo como completo.

Box3 -> BufferGeometry y Box3 -> Geometry podrían limpiarse.

Es otro caso de no poner el comportamiento dependiente de la clase en la clase misma.

fuente :

setFromObject: function () {

  // Computes the world-axis-aligned bounding box of an object (including its children),
  // accounting for both the object's, and childrens', world transforms

  /* ... */

  if ( geometry instanceof THREE.Geometry ) {
    /* ... */
  } else if ( geometry instanceof THREE.BufferGeometry && geometry.attributes[ 'position' ] !== undefined ) {
    /* ... */
  }

  /* ... */

}

En ambos casos, solo intenta iterar a través de los vértices/posiciones de worldCoordinate de la geometría. Me pregunto si simplificaría enormemente el código para crear un objeto perezoso vertices en BufferGeometry que busca los valores a medida que se solicitan. No estoy seguro sobre el impacto del rendimiento.

Alternativamente, podríamos usar Geometry.computeBoundingBox :
Geometry
BufferGeometry

Box3 es un punto problemático cuando se ejecuta la compilación browserify. Consulte las notas en coballast/threejs-browserify-conversion-utility#21.

@kumavis ¿Le importaría describir su solución recomendada para tratar con Box3/Geometry/BufferGeometry? Si es rápido, puedo implementarlo.

No puedo verlo ahora, pero comenzaría con mi sugerencia anterior de usar geo.computeBoundingBox como se implementó en Geometry y BufferGeometry en lugar de if/else aquí . En su lugar box3.setFromObj debería llamar a geometry.computeBoundingBox y luego establecer parámetros basados ​​en el cuadro producido3.

Eso debería eliminar el extremo Box3 -> BufferGeometry y Box3 -> Geometry de las depresiones circulares. Avísame si me estoy perdiendo algo.

Hmm, tal vez el código resultante sea un poco complicado, ¿qué tiene sentido aquí? Box3.setFromObject no debería existir, pero esa no es una opción. Geo's debería poder producir box3s, no tengo ningún problema con eso. Sí, supongo que Box3.setFromObject debería pedirle a la geolocalización un cuadro delimitador/extensiones, pero tal vez deberían preguntarle a Object3D / Mesh el cuadro delimitador/extensiones.

Lo siento, me puse un poco divagante. déjame saber lo que piensas.

Posiblemente relevante: #6546

Sin algo como esto, es imposible analizar esas dependencias dinámicas de los scripts del cargador.

Según mis pruebas, las dependencias cíclicas no son un problema con commonJSification. Deben manejarse correctamente y, como se indicó anteriormente en este hilo, hacen que el gráfico de dependencia sea un desastre, pero no impiden que THREE.js funcione en entornos commonJS (cuando se transforman, por supuesto).

Acabo de publicar una versión completa de commonjs en npm como three.cjs usando mi transpiler de tres commonjs

Nota: para que esto funcione, tuve que elegir manualmente # 6546 en el maestro. Si bien las dependencias dinámicas funcionan bien en node.js, no pueden funcionar en Browserify (o cualquier otra herramienta de cjs para navegador) ya que necesitan realizar un análisis de dependencia estática.

Prueba de navegador: http://requirebin.com/?gist=b7fe528d8059a7403960

@kamicane FYI: aquí es donde se agregaron TRES como argumento de una función anónima a Raycaster (anteriormente Ray ).

Entiendo la necesidad de una función anónima (para evitar fugas globales en los navegadores), sin embargo, el argumento es superfluo y hace que todo el archivo caiga en la categoría de dependencias computadas. Si bien el argumento podría eliminarse dinámicamente con modificaciones AST, nunca puede ser una solución a prueba de balas (dependiendo de lo que se escriba en el argumento, se lea del argumento, etc.). El análisis estático se vuelve casi imposible. En este caso es necesaria la intervención manual.

Ahora que Raycaster es más liviano, podríamos intentar hacerlo como el resto de las clases.

@mrdoob , @Mugen87 usamos Rollup para usar solo las partes de Three.js que realmente necesitamos. Cuando ejecutamos el build todavía recibimos la siguiente advertencia:

(!) Circular dependency: node_modules/three/src/math/Vector3.js -> node_modules/three/src/math/Matrix4.js -> node_modules/three/src/math/Vector3.js
(!) Circular dependency: node_modules/three/src/math/Vector3.js -> node_modules/three/src/math/Quaternion.js -> node_modules/three/src/math/Vector3.js
(!) Circular dependency: node_modules/three/src/math/Sphere.js -> node_modules/three/src/math/Box3.js -> node_modules/three/src/math/Sphere.js
(!) Circular dependency: node_modules/three/src/objects/LineSegments.js -> node_modules/three/src/objects/Line.js -> node_modules/three/src/objects/LineSegments.js

¿Todavía hay dependencias circulares en Three.js o estamos haciendo algo mal?

Vector3 y Matrix4 están vinculados entre sí, si tira de uno, necesita tirar del otro. Técnicamente, las dependencias circulares deberían permitirse.

@bhouston sí, ya veo, gracias por la pista. Sí, las dependencias circulares están permitidas y el resumen no genera problemas, pero no estoy seguro de si es una buena práctica tener dependencias circulares. Vector3 solo depende de Matrix4 debido a multiplyMatrices y getInverse , para obtener más detalles, consulte (https://github.com/mrdoob/three.js/ blob/dev/src/math/Vector3.js#L315)

@roomle-build Idk, hombre, ¿solo porque hace referencia explícita al constructor Matrix4? qué pasa

    applyMatrix4: function ( m ) {

        var x = this.x, y = this.y, z = this.z;
        var e = m.elements;

        var w = 1 / ( e[ 3 ] * x + e[ 7 ] * y + e[ 11 ] * z + e[ 15 ] );

        this.x = ( e[ 0 ] * x + e[ 4 ] * y + e[ 8 ] * z + e[ 12 ] ) * w;
        this.y = ( e[ 1 ] * x + e[ 5 ] * y + e[ 9 ] * z + e[ 13 ] ) * w;
        this.z = ( e[ 2 ] * x + e[ 6 ] * y + e[ 10 ] * z + e[ 14 ] ) * w;

        return this;

},

?

puedes decir que podrías pasar {elements: [....] } y funcionará, pero todos sabemos que espera Matrix4 allí

Comencemos con Vector3 .

Vector3 depende de Matrix4 debido a project y unproject :

    project: function () {

        var matrix = new Matrix4();

        return function project( camera ) {

            matrix.multiplyMatrices( camera.projectionMatrix, matrix.getInverse( camera.matrixWorld ) );
            return this.applyMatrix4( matrix );

        };

    }(),

    unproject: function () {

        var matrix = new Matrix4();

        return function unproject( camera ) {

            matrix.multiplyMatrices( camera.matrixWorld, matrix.getInverse( camera.projectionMatrix ) );
            return this.applyMatrix4( matrix );

        };

    }(),

Vector3 depende de Quaternion debido a applyEuler y applyAxisAngle :

    applyEuler: function () {

        var quaternion = new Quaternion();

        return function applyEuler( euler ) {

            if ( ! ( euler && euler.isEuler ) ) {

                console.error( 'THREE.Vector3: .applyEuler() now expects an Euler rotation rather than a Vector3 and order.' );

            }

            return this.applyQuaternion( quaternion.setFromEuler( euler ) );

        };

    }(),

    applyAxisAngle: function () {

        var quaternion = new Quaternion();

        return function applyAxisAngle( axis, angle ) {

            return this.applyQuaternion( quaternion.setFromAxisAngle( axis, angle ) );

        };

    }(),

¿Sugerencias?

No estoy seguro de si necesitamos eliminar las dependencias circulares por todos los medios. Pero podría imaginar mover multiplyMatrices a un módulo Math . La firma cambiaría entonces, por supuesto, a multiplyMatrices( a: Matrix4, b: Matrix4, result: Matrix4 ): Matrix4 . Dentro Vector3 podrías entonces import { multiplyMatrices } from './Math'; lo mismo podría hacerse en Matrix4 (para mantener la superficie API de Matrix4 igual).

Acabo de echar un vistazo rápido (no miré el caso Quaternian , solo Vec3/Mat4 ) y tampoco estoy seguro de las implicaciones de rendimiento y las consecuencias para el resto del código base. Además, tampoco estoy convencido de que sea absolutamente necesario eliminar estas dependencias circulares. Solo quería compartir mi pensamiento porque @mrdoob pidió sugerencias

@roomle-build, así que básicamente crea más módulos por el bien de las dependencias circulares, pero ¿aún vas a usar todos esos módulos? esto tal vez podría tener más sentido si todos y cada uno de los métodos matemáticos fueran su propio módulo, entonces solo está extrayendo los que usa, pero serán muchos módulos.

@makc no realmente. Sería un módulo grande con muchas funciones pequeñas de "ayuda". Esto también ayudaría a sacudir árboles, etc. Un módulo matemático podría verse así:

export const multiplyMatrices( a, b, result ) { // ... DO STUFF ... // }
export const getInverse( /* ... */ ) { // ... DO STUFF ... // }
// ...
// ...

Y el módulo de consumo haría algo como:

import { Matrix4 } from './Matrix4.js';
import { multiplyMatrices } from './math';
const result = new Matrix4( );
multiplyMatrices( a, b, result );

Al agrupar todo junto, el resumen hace su magia y crea el paquete más eficiente.

Esto es lo que están haciendo muchas bibliotecas populares. En realidad, RxJS cambió su "lógica" import también al patrón que describí. Allí se ve algo como:

 import { flatMap, map, tap } from 'rxjs/operators';

myObject.run().pipe(
  tap(result => doSomething()), 
  flatMap(() => doSomethingElse()), 
  map(() => doAnotherThing())
);

Puede leer acerca de "por qué y cómo" cambiaron estas cosas en RxJS 6 en varias publicaciones de blog, por ejemplo: https://auth0.com/blog/whats-new-in-rxjs-6/

Pero como dije, es solo un pensamiento y no estoy seguro de todas las implicaciones que esto tendría para el resto de la base de código. Además, el módulo math actual no está "preparado" para ser utilizado de esta manera. Actualmente, todos los métodos en el módulo de matemáticas están adjuntos "algo estático". Esto también evita que el resumen detecte lo que realmente se necesita...

@roomle-build hmm, por lo que está diciendo que el resumen puede entender si el código en el mismo alcance en realidad no necesita todo el alcance, bien.

Está hablando de moverse hacia un enfoque funcional (funciones que toman objetos) en lugar de un enfoque orientado a objetos (objeto que tiene funciones miembro). Esto es algo real, pero dado que Three.JS está totalmente orientado a objetos, proponer este tipo de cambio es una bastante grande y rompería todo el código existente.

No estoy seguro de que los argumentos a favor de este cambio sean tan significativos en este punto como para justificar romper toda compatibilidad con versiones anteriores.

@makc no realmente. Sería un módulo grande con muchas funciones pequeñas de "ayuda". Esto también ayudaría a sacudir árboles, etc. Un módulo matemático podría verse así:

Si esto es lo que se propone, debe describirse correctamente. Es el cambio de Three.JS de un estilo de diseño orientado a objetos a un diseño funcional.

@roomle-build hmm, por lo que está diciendo que el resumen puede entender si el código en el mismo alcance en realidad no necesita todo el alcance, bien.

Sí, el paquete acumulativo comprende cómo se relacionan todas las importaciones entre sí y realiza sacudidas de árbol, eliminación de código muerto, etc. Las nuevas versiones del paquete acumulativo también pueden "fragmentar" y muchas otras cosas interesantes. Pero la estructura actual del proyecto no aprovecha al máximo estas características.

Está hablando de moverse hacia un enfoque funcional (funciones que toman objetos) en lugar de un enfoque orientado a objetos (objeto que tiene funciones miembro). Esto es algo real, pero dado que Three.JS está totalmente orientado a objetos, proponer este tipo de cambio es una bastante grande y rompería todo el código existente.

No creo que estos dos paradigmas sean mutuamente excluyentes. Creo que se pueden mezclar y combinar estos dos paradigmas. Tampoco propongo cambiar a programación funcional. Solo quería describir una forma de deshacerse de la dependencia cíclica. También puede adjuntar el método multiplyMatrices al objeto Math . Pero si alguien reescribe este tipo de cosas, tendría sentido considerar usar las características de los módulos ES6. Pero como dije, no soy un experto en el código base de Three.js y fue solo una idea de cómo eliminar la dependencia cíclica. Creo que Three.js es un proyecto increíble con una gran base de código y no quiero molestar. Así que espero que nadie se sienta ofendido por mis comentarios 😉

No estoy seguro de si deberíamos discutir las decisiones de diseño en un problema. ¿Tienes algún lugar donde este tipo de cosas encajen mejor?

Por cierto, gl-matrix es una biblioteca matemática funcional: https://github.com/toji/gl-matrix/tree/master/src/gl-matrix

@roomle-construir

Actualmente, todos los métodos en el módulo de matemáticas están adjuntos "algo estático".

¿Cómo es eso?

@mrdoob Creo que con el diseño funcional de gl-matrix, cada función de, por ejemplo, vec3 (en el archivo vec3 al que me vinculé en mi comentario anterior) se exporta individualmente. Esto le permite elegir qué funciones importar. No es necesario que traiga todo el vec3.

Mientras que con Three.JS, debido a que utiliza un diseño orientado a objetos, todas las funciones matemáticas para Vector3 se adjuntan al prototipo del objeto Vector3 y usted importa solo la clase Vector3 en sí.

Por lo tanto, las importaciones en Three.JS son de clases completas, mientras que con un enfoque funcional se importan funciones individuales.

(La otra cosa muy buena sobre la biblioteca gl-matrix es que todas las funciones individuales no usan otras funciones, @toji básicamente ha integrado una versión optimizada de todas las matemáticas en cada operación individual. Esto es probablemente bastante eficiente en términos de velocidad pero conduce a una biblioteca difícil de mantener).

No creo que necesitemos refactorizar esta parte de Three.JS, excepto tal vez para eliminar cualquier referencia en /math a otros directorios en three.js. La biblioteca de matemáticas es bastante pequeña y en realidad nunca aparece en mis pruebas de perfilado en estos días. Sí, no es máximamente eficiente, pero está lo suficientemente cerca como para mantener la legibilidad y la facilidad de uso.

@bhouston Lo tengo. ¡Muchas gracias por la explicación! 😊

Solo quería seguir con el tema. Pero quiero volver de importing function vs importing classes al tema de resolver cyclic dependencies . (Tampoco veo por qué import { someFunction } from 'SomeModule' es menos mantenible que import SomeClass from 'SomeModule' , pero definitivamente ese no es el tema de este problema/conversación.

Para resolver la dependencia cíclica, sería posible colocar la funcionalidad en una clase separada. Puede adjuntar un método multiplyMatrices a la clase matemática o crear una clase multiplicadora que tenga el método multiplyMatrices . Pero como dije antes, no estoy seguro si tenemos que eliminar las dependencias cíclicas. Si la decisión es no quitarlos, creo que este tema podría estar cerca 😃

Después de resolver #19137, esto se puede cerrar ahora 🎉.

@Mugen87 guau, 5 años! felicidades por finalmente darle :fuego: :clap:
Me divertí mucho haciendo esos gráficos en ese entonces :smile_cat:

¿Fue útil esta página
0 / 5 - 0 calificaciones

Temas relacionados

filharvey picture filharvey  ·  3Comentarios

ghost picture ghost  ·  3Comentarios

fuzihaofzh picture fuzihaofzh  ·  3Comentarios

scrubs picture scrubs  ·  3Comentarios

konijn picture konijn  ·  3Comentarios