From d77a75b1bede2ac7d3bd0a9d83ac7e5dc70b3619 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Fri, 28 Sep 2018 13:11:21 +0200 Subject: [PATCH] Add thick lines example Vendor the thick lines code from three.js (r97), and expose to kernel. --- examples/Geometries.ipynb | 2 +- examples/LineGeometry.ipynb | 368 ++++++++++++++++ js/scripts/generate-wrappers.js | 5 + js/scripts/prop-types.js | 51 ++- js/scripts/three-class-config.js | 52 +++ js/src/_base/Three.js | 10 +- js/src/examples/lines/Line2.js | 52 +++ js/src/examples/lines/LineGeometry.js | 105 +++++ js/src/examples/lines/LineMaterial.js | 402 ++++++++++++++++++ js/src/examples/lines/LineSegments2.js | 52 +++ js/src/examples/lines/LineSegmentsGeometry.js | 254 +++++++++++ js/src/geometries/LineGeometry.js | 24 ++ js/src/geometries/LineSegmentsGeometry.js | 56 +++ js/src/materials/LineMaterial.js | 30 ++ js/src/objects/Line2.js | 21 + js/src/objects/LineSegments2.js | 21 + pythreejs/traits.py | 2 +- 17 files changed, 1485 insertions(+), 22 deletions(-) create mode 100644 examples/LineGeometry.ipynb create mode 100644 js/src/examples/lines/Line2.js create mode 100644 js/src/examples/lines/LineGeometry.js create mode 100644 js/src/examples/lines/LineMaterial.js create mode 100644 js/src/examples/lines/LineSegments2.js create mode 100644 js/src/examples/lines/LineSegmentsGeometry.js create mode 100644 js/src/geometries/LineGeometry.js create mode 100644 js/src/geometries/LineSegmentsGeometry.js create mode 100644 js/src/materials/LineMaterial.js create mode 100644 js/src/objects/Line2.js create mode 100644 js/src/objects/LineSegments2.js diff --git a/examples/Geometries.ipynb b/examples/Geometries.ipynb index 3d375b62..91133b9c 100644 --- a/examples/Geometries.ipynb +++ b/examples/Geometries.ipynb @@ -1116,7 +1116,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.3" + "version": "3.6.6" }, "widgets": { "application/vnd.jupyter.widget-state+json": { diff --git a/examples/LineGeometry.ipynb b/examples/LineGeometry.ipynb new file mode 100644 index 00000000..492e75a7 --- /dev/null +++ b/examples/LineGeometry.ipynb @@ -0,0 +1,368 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Line Geometry" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Three.js has some example code for thick lines via an instance-based geometry. Since WebGL does not guarantee support for line thickness greater than 1 for GL lines, pytheejs includes these objects." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from pythreejs import *\n", + "from IPython.display import display\n", + "from ipywidgets import VBox, HBox, Checkbox, jslink\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "g1 = BufferGeometry(\n", + " attributes={\n", + " 'position': BufferAttribute(np.array([[0, 0, 0], [1, 1, 1], [2, 2, 2], [4, 4, 4]], dtype=np.float32), normalized=False),\n", + " 'color': BufferAttribute(np.array([[1, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=np.float32), normalized=False),\n", + " },\n", + ")\n", + "m1 = LineBasicMaterial(vertexColors='VertexColors', linewidth=10)\n", + "line1 = LineSegments(g1, m1);" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "g2 = LineSegmentsGeometry(\n", + " positions=[\n", + " [[0, 0, 0], [1, 1, 1]],\n", + " [[2, 2, 2], [4, 4, 4]]\n", + " ],\n", + " colors=[\n", + " [[1, 0, 0], [1, 0, 0]],\n", + " [[0, 1, 0], [0, 0, 1]]\n", + " ],\n", + ")\n", + "m2 = LineMaterial(linewidth=10, vertexColors='VertexColors')\n", + "line2 = LineSegments2(g2, m2)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "view_width = 600\n", + "view_height = 400\n", + "camera = PerspectiveCamera(position=[10, 0, 0], aspect=view_width/view_height)\n", + "key_light = DirectionalLight(position=[0, 10, 10])\n", + "ambient_light = AmbientLight()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "scene = Scene(children=[line1, line2, camera, key_light, ambient_light])\n", + "controller = OrbitControls(controlling=camera, screenSpacePanning=False)\n", + "renderer = Renderer(camera=camera, scene=scene, controls=[controller],\n", + " width=view_width, height=view_height)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "chks = [Checkbox(True, description='GL line'), Checkbox(True, description='Fat line')]\n", + "jslink((chks[0], 'value'), (line1, 'visible'))\n", + "jslink((chks[1], 'value'), (line2, 'visible'))\n", + "VBox([renderer, HBox(chks)])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "posInstBuffer = InstancedInterleavedBuffer( np.array([[0, 0, 0, 1, 1, 1], [2, 2, 2, 4, 4, 4]], dtype=np.float32))\n", + "colInstBuffer = InstancedInterleavedBuffer( np.array([[1, 0, 0, 1, 0, 0], [0, 1, 0, 0, 0, 1]], dtype=np.float32))\n", + "dbgG = InstancedBufferGeometry(attributes={\n", + " 'position': BufferAttribute(np.array([ [- 1, 2, 0], [1, 2, 0], [- 1, 1, 0], [1, 1, 0], [- 1, 0, 0], [1, 0, 0], [- 1, - 1, 0], [1, - 1, 0] ], dtype=np.float32)),\n", + " 'uv': BufferAttribute(np.array([ [- 1, 2], [1, 2], [- 1, 1], [1, 1], [- 1, - 1], [1, - 1], [- 1, - 2], [1, - 2] ], dtype=np.float32)),\n", + " 'index': BufferAttribute(np.array([ 0, 2, 1, 2, 3, 1, 2, 4, 3, 4, 5, 3, 4, 6, 5, 6, 7, 5 ], dtype=np.uint8)),\n", + " 'instanceStart': InterleavedBufferAttribute(posInstBuffer, 3, 0),\n", + " 'instanceEnd': InterleavedBufferAttribute(posInstBuffer, 3, 3),\n", + " 'instanceColorStart': InterleavedBufferAttribute(colInstBuffer, 3, 0),\n", + " 'instanceColorEnd': InterleavedBufferAttribute(colInstBuffer, 3, 3),\n", + "})" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "m = ShaderMaterial(\n", + " vertexShader='''\n", + "#include \n", + "#include \n", + "#include \n", + "#include \n", + "#include \n", + "\n", + "uniform float linewidth;\n", + "uniform vec2 resolution;\n", + "\n", + "attribute vec3 instanceStart;\n", + "attribute vec3 instanceEnd;\n", + "\n", + "attribute vec3 instanceColorStart;\n", + "attribute vec3 instanceColorEnd;\n", + "\n", + "varying vec2 vUv;\n", + "\n", + "void trimSegment( const in vec4 start, inout vec4 end ) {\n", + "\n", + " // trim end segment so it terminates between the camera plane and the near plane\n", + "\n", + " // conservative estimate of the near plane\n", + " float a = projectionMatrix[ 2 ][ 2 ]; // 3nd entry in 3th column\n", + " float b = projectionMatrix[ 3 ][ 2 ]; // 3nd entry in 4th column\n", + " float nearEstimate = - 0.5 * b / a;\n", + "\n", + " float alpha = ( nearEstimate - start.z ) / ( end.z - start.z );\n", + "\n", + " end.xyz = mix( start.xyz, end.xyz, alpha );\n", + "\n", + "}\n", + "\n", + "void main() {\n", + "\n", + " #ifdef USE_COLOR\n", + "\n", + " vColor.xyz = ( position.y < 0.5 ) ? instanceColorStart : instanceColorEnd;\n", + "\n", + " #endif\n", + " \n", + " float aspect = resolution.x / resolution.y;\n", + "\n", + " vUv = uv;\n", + " \n", + " // camera space\n", + " vec4 start = modelViewMatrix * vec4( instanceStart, 1.0 );\n", + " vec4 end = modelViewMatrix * vec4( instanceEnd, 1.0 );\n", + "\n", + " // special case for perspective projection, and segments that terminate either in, or behind, the camera plane\n", + " // clearly the gpu firmware has a way of addressing this issue when projecting into ndc space\n", + " // but we need to perform ndc-space calculations in the shader, so we must address this issue directly\n", + " // perhaps there is a more elegant solution -- WestLangley\n", + "\n", + " bool perspective = ( projectionMatrix[ 2 ][ 3 ] == - 1.0 ); // 4th entry in the 3rd column\n", + "\n", + " if ( perspective ) {\n", + "\n", + " if ( start.z < 0.0 && end.z >= 0.0 ) {\n", + "\n", + " trimSegment( start, end );\n", + "\n", + " } else if ( end.z < 0.0 && start.z >= 0.0 ) {\n", + "\n", + " trimSegment( end, start );\n", + "\n", + " }\n", + "\n", + " }\n", + "\n", + " // clip space\n", + " vec4 clipStart = projectionMatrix * start;\n", + " vec4 clipEnd = projectionMatrix * end;\n", + "\n", + " // ndc space\n", + " vec2 ndcStart = clipStart.xy / clipStart.w;\n", + " vec2 ndcEnd = clipEnd.xy / clipEnd.w;\n", + "\n", + " // direction\n", + " vec2 dir = ndcEnd - ndcStart;\n", + "\n", + " // account for clip-space aspect ratio\n", + " dir.x *= aspect;\n", + " dir = normalize( dir );\n", + "\n", + " // perpendicular to dir\n", + " vec2 offset = vec2( dir.y, - dir.x );\n", + "\n", + " // undo aspect ratio adjustment\n", + " dir.x /= aspect;\n", + " offset.x /= aspect;\n", + "\n", + " // sign flip\n", + " if ( position.x < 0.0 ) offset *= - 1.0;\n", + "\n", + " // endcaps\n", + " if ( position.y < 0.0 ) {\n", + "\n", + " offset += - dir;\n", + "\n", + " } else if ( position.y > 1.0 ) {\n", + "\n", + " offset += dir;\n", + "\n", + " }\n", + "\n", + " // adjust for linewidth\n", + " offset *= linewidth;\n", + " \n", + " // adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ...\n", + " offset /= resolution.y;\n", + "\n", + " // select end\n", + " vec4 clip = ( position.y < 0.5 ) ? clipStart : clipEnd;\n", + "\n", + " // back to clip space\n", + " offset *= clip.w;\n", + "\n", + " clip.xy += offset;\n", + "\n", + " gl_Position = clip;\n", + " \n", + " //gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );\n", + " \n", + " //if ( instanceStart.x > 1.5) {\n", + " // gl_Position.x += 2.0;\n", + " //}\n", + " \n", + " vec4 mvPosition = ( position.y < 0.5 ) ? start : end; // this is an approximation\n", + "\n", + " #include \n", + " #include \n", + " #include \n", + "}\n", + "''',\n", + " fragmentShader='''\n", + "uniform vec3 diffuse;\n", + "uniform float opacity;\n", + "\n", + "varying float vLineDistance;\n", + "\n", + "#include \n", + "#include \n", + "#include \n", + "#include \n", + "#include \n", + "\n", + "varying vec2 vUv;\n", + "\n", + "void main() {\n", + "\n", + " #include \n", + "\n", + "\n", + " if ( abs( vUv.y ) > 1.0 ) {\n", + "\n", + " float a = vUv.x;\n", + " float b = ( vUv.y > 0.0 ) ? vUv.y - 1.0 : vUv.y + 1.0;\n", + " float len2 = a * a + b * b;\n", + "\n", + " if ( len2 > 1.0 ) discard;\n", + "\n", + " }\n", + "\n", + " vec4 diffuseColor = vec4( diffuse, opacity );\n", + "\n", + " #include \n", + " #include \n", + "\n", + " gl_FragColor = vec4( diffuseColor.rgb, diffuseColor.a );\n", + "\n", + " #include \n", + " #include \n", + " #include \n", + " #include \n", + "\n", + "}\n", + "''',\n", + " vertexColors='VertexColors',\n", + " uniforms=dict(\n", + " **UniformsLib['common'],\n", + " linewidth={'value': 10.0},\n", + " resolution={'value': (100., 100.)},\n", + " )\n", + ")\n", + "Mesh(g2, m)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(repr(line2))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.6" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/js/scripts/generate-wrappers.js b/js/scripts/generate-wrappers.js index 0abeecd5..031909a3 100644 --- a/js/scripts/generate-wrappers.js +++ b/js/scripts/generate-wrappers.js @@ -40,6 +40,11 @@ const CUSTOM_CLASSES = [ 'core/BaseBufferGeometry.js', 'objects/CloneArray.js', 'objects/Blackbox.js', + 'objects/Line2.js', + 'objects/LineSegments2.js', + 'geometries/LineGeometry.js', + 'geometries/LineSegmentsGeometry.js', + 'materials/LineMaterial.js', ]; const IGNORE_FILES = [ diff --git a/js/scripts/prop-types.js b/js/scripts/prop-types.js index 7bb99fb7..4095be2d 100644 --- a/js/scripts/prop-types.js +++ b/js/scripts/prop-types.js @@ -2,6 +2,21 @@ const WIDGET_SERIALIZER = '{ deserialize: widgets.unpack_models }'; + +function pythonify(value) { + if (value === false) { return 'False'; } + if (value === true) { return 'True'; } + if (value === Infinity) { return "float('inf')"; } + if (value === -Infinity) { return "-float('inf')"; } + if (value === undefined || value === null) { return 'None'; } + if (Array.isArray(value)) { + return `[${ + value.map(function(v) { return pythonify(v); }).join(', ') + }]`; + } + return JSON.stringify(value); +} + class BaseType { constructor(options) { options = options || {}; @@ -18,15 +33,7 @@ class BaseType { return null; } getPythonDefaultValue() { - if (this.defaultValue === false) { return 'False'; } - if (this.defaultValue === true) { return 'True'; } - if (this.defaultValue === 0) { return '0'; } - if (this.defaultValue === '') { return "''"; } - if (this.defaultValue === Infinity) { return "float('inf')"; } - if (this.defaultValue === -Infinity) { return "-float('inf')"; } - if (!this.defaultValue) { return 'None'; } - - return JSON.stringify(this.defaultValue); + return pythonify(this.defaultValue); } getPropertyConverterFn() { return null; @@ -382,16 +389,24 @@ class ArrayBufferType extends BaseType { super(options); this.arrayType = arrayType; this.shapeConstraint = shapeConstraint; - this.defaultValue = null; + this.defaultValue = options && options.nullable ? null : undefined; this.serializer = 'dataserializers.data_union_serialization'; } getTraitlet() { const args = []; + if (this.defaultValue !== undefined) { + args.push(pythonify(this.defaultValue)); + } if (this.arrayType) { - args.push(`dtype=${this.arrayType}`); + args.push(`dtype=${pythonify(this.arrayType)}`); } if (this.shapeConstraint) { - args.push(`shape_constraint=${this.shapeConstraint}`); + args.push(`shape_constraint=shape_constraints(${ + this.shapeConstraint.map(function(v) { return pythonify(v); }).join(', ') + })`); + } + if (this.nullable) { + args.push(this.getNullableStr()); } return `WebGLDataUnion(${args.join(', ')})${this.getTagString()}`; @@ -450,7 +465,7 @@ class Vector2 extends BaseType { } getTraitlet() { return `Vector2(default_value=${ - JSON.stringify(this.defaultValue)})${this.getTagString()}`; + pythonify(this.defaultValue)})${this.getTagString()}`; } getPropertyConverterFn() { return 'convertVector'; @@ -467,7 +482,7 @@ class Vector3 extends BaseType { } getTraitlet() { return `Vector3(default_value=${ - JSON.stringify(this.defaultValue)})${this.getTagString()}`; + pythonify(this.defaultValue)})${this.getTagString()}`; } getPropertyConverterFn() { return 'convertVector'; @@ -484,7 +499,7 @@ class Vector4 extends BaseType { } getTraitlet() { return `Vector4(default_value=${ - JSON.stringify(this.defaultValue)})${this.getTagString()}`; + pythonify(this.defaultValue)})${this.getTagString()}`; } getPropertyConverterFn() { return 'convertVector'; @@ -537,7 +552,7 @@ class Matrix3 extends BaseType { } getTraitlet() { return `Matrix3(default_value=${ - JSON.stringify(this.defaultValue)})${this.getTagString()}`; + pythonify(this.defaultValue)})${this.getTagString()}`; } getPropertyConverterFn() { return 'convertMatrix'; @@ -559,7 +574,7 @@ class Matrix4 extends BaseType { } getTraitlet() { return `Matrix4(default_value=${ - JSON.stringify(this.defaultValue)})${this.getTagString()}`; + pythonify(this.defaultValue)})${this.getTagString()}`; } getPropertyConverterFn() { return 'convertMatrix'; @@ -578,7 +593,7 @@ class Euler extends BaseType { getTraitlet() { return `Euler(default_value=${ - JSON.stringify(this.defaultValue)})${this.getTagString()}`; + pythonify(this.defaultValue)})${this.getTagString()}`; } getPropertyConverterFn() { return 'convertEuler'; diff --git a/js/scripts/three-class-config.js b/js/scripts/three-class-config.js index ad5e8062..d1946a76 100644 --- a/js/scripts/three-class-config.js +++ b/js/scripts/three-class-config.js @@ -653,6 +653,20 @@ module.exports = { }, constructorArgs: [ 'parameters' ], }, + LineMaterial: { + relativePath: './materials/LineMaterial', + superClass: 'Material', + properties: { + color: new Types.Color('#ffffff'), + fog: new Types.Bool(false), + lights: new Types.Bool(false), + linewidth: new Types.Float(1.0), + dashScale: new Types.Float(1.0), + dashSize: new Types.Float(1.0), + gapSize: new Types.Float(1.0), + }, + constructorArgs: [ 'parameters' ], + }, Material: { relativePath: './materials/Material', properties: { @@ -1154,6 +1168,26 @@ module.exports = { constructorArgs: [ 'original', 'positions', 'merge' ], // TODO: Add restriction: Source cannot use strip/fan draw modes }, + Line2: { + relativePath: './objects/Line2', + superClass: 'Mesh', + properties: { + material: new Types.InitializedThreeType('LineMaterial', {nullable: true}), + geometry: new Types.InitializedThreeType('LineGeometry', {nullable: true}), + }, + constructorArgs: [ 'geometry', 'material' ], + propsDefinedByThree: [ 'geometry', 'material' ], + }, + LineSegments2: { + relativePath: './objects/LineSegments2', + superClass: 'Mesh', + properties: { + material: new Types.InitializedThreeType('LineMaterial', {nullable: true}), + geometry: new Types.InitializedThreeType('LineSegmentsGeometry', {nullable: true}), + }, + constructorArgs: [ 'geometry', 'material' ], + propsDefinedByThree: [ 'geometry', 'material' ], + }, WebGLRenderTarget: { relativePath: './renderers/WebGLRenderTarget', }, @@ -1559,6 +1593,24 @@ module.exports = { phiLength: new Types.Float(Math.PI * 2.0), }, }, + LineGeometry: { + relativePath: './geometries/LineGeometry', + superClass: 'LineSegmentsGeometry', + constructorArgs: [], + properties: { + positions: new Types.ArrayBuffer('float32', [null, 3]), + colors: new Types.ArrayBuffer('float32', [null, 3], {nullable: true}), + }, + }, + LineSegmentsGeometry: { + relativePath: './geometries/LineSegmentsGeometry', + superClass: 'BaseBufferGeometry', + constructorArgs: [], + properties: { + positions: new Types.ArrayBuffer('float32', [null, 2, 3]), + colors: new Types.ArrayBuffer('float32', [null, 2, 3], {nullable: true}), + }, + }, OctahedronGeometry: { relativePath: './geometries/OctahedronGeometry', superClass: 'BaseGeometry', diff --git a/js/src/_base/Three.js b/js/src/_base/Three.js index e19a1689..dea317aa 100644 --- a/js/src/_base/Three.js +++ b/js/src/_base/Three.js @@ -798,11 +798,17 @@ var ThreeModel = widgets.WidgetModel.extend({ return arr && arr.data; }, - convertArrayBufferThreeToModel: function(arrBuffer) { + convertArrayBufferThreeToModel: function(arrBuffer, propName) { if (arrBuffer === null) { return null; } - // Never back-convert to a new widget + var current = this.get(propName); + var currentArray = dataserializers.getArray(current); + if (currentArray && (currentArray.data === arrBuffer)) { + // Unchanged, do nothing + return current; + } + // Never create a new widget, even if current is one return ndarray(arrBuffer); }, diff --git a/js/src/examples/lines/Line2.js b/js/src/examples/lines/Line2.js new file mode 100644 index 00000000..d88b13f3 --- /dev/null +++ b/js/src/examples/lines/Line2.js @@ -0,0 +1,52 @@ +/** + * @author WestLangley / http://github.com/WestLangley + * + */ + +var THREE = require('three'); +var LineGeometry = require('./LineGeometry').LineGeometry; +var LineMaterial = require('./LineMaterial').LineMaterial; + + +var Line2 = function ( geometry, material ) { + + THREE.Mesh.call( this ); + + this.type = 'Line2'; + + this.geometry = geometry !== undefined ? geometry : new LineGeometry(); + this.material = material !== undefined ? material : new LineMaterial( { color: Math.random() * 0xffffff } ); + +}; + +Line2.prototype = Object.assign( Object.create( THREE.Mesh.prototype ), { + + constructor: Line2, + + isLine2: true, + + onBeforeRender: function( renderer, scene, camera, geometry, material, group ) { + + if ( material.isLineMaterial ) { + + var size = renderer.getSize(); + + material.resolution = new THREE.Vector2(size.width, size.height); + + } + + }, + + copy: function ( source ) { + + // todo + + return this; + + } + +} ); + +module.exports = { + Line2: Line2 +}; diff --git a/js/src/examples/lines/LineGeometry.js b/js/src/examples/lines/LineGeometry.js new file mode 100644 index 00000000..105d7735 --- /dev/null +++ b/js/src/examples/lines/LineGeometry.js @@ -0,0 +1,105 @@ +/** + * @author WestLangley / http://github.com/WestLangley + * + */ + +var LineSegmentsGeometry = require('./LineSegmentsGeometry').LineSegmentsGeometry; + + +var LineGeometry = function () { + + LineSegmentsGeometry.call( this ); + + this.type = 'LineGeometry'; + +}; + +LineGeometry.prototype = Object.assign( Object.create( LineSegmentsGeometry.prototype ), { + + constructor: LineGeometry, + + isLineGeometry: true, + + setPositions: function ( array ) { + + // converts [ x1, y1, z1, x2, y2, z2, ... ] to pairs format + + var length = array.length - 3; + var points = new Float32Array( 2 * length ); + + for ( var i = 0; i < length; i += 3 ) { + + points[ 2 * i ] = array[ i ]; + points[ 2 * i + 1 ] = array[ i + 1 ]; + points[ 2 * i + 2 ] = array[ i + 2 ]; + + points[ 2 * i + 3 ] = array[ i + 3 ]; + points[ 2 * i + 4 ] = array[ i + 4 ]; + points[ 2 * i + 5 ] = array[ i + 5 ]; + + } + + LineSegmentsGeometry.prototype.setPositions.call( this, points ); + + return this; + + }, + + setColors: function ( array ) { + + // converts [ r1, g1, b1, r2, g2, b2, ... ] to pairs format + + var length = array.length - 3; + var colors = new Float32Array( 2 * length ); + + for ( var i = 0; i < length; i += 3 ) { + + colors[ 2 * i ] = array[ i ]; + colors[ 2 * i + 1 ] = array[ i + 1 ]; + colors[ 2 * i + 2 ] = array[ i + 2 ]; + + colors[ 2 * i + 3 ] = array[ i + 3 ]; + colors[ 2 * i + 4 ] = array[ i + 4 ]; + colors[ 2 * i + 5 ] = array[ i + 5 ]; + + } + + LineSegmentsGeometry.prototype.setColors.call( this, colors ); + + return this; + + }, + + fromLine: function ( line ) { + + var geometry = line.geometry; + + if ( geometry.isGeometry ) { + + this.setPositions( geometry.vertices ); + + } else if ( geometry.isBufferGeometry ) { + + this.setPositions( geometry.position.array ); // assumes non-indexed + + } + + // set colors, maybe + + return this; + + }, + + copy: function ( source ) { + + // todo + + return this; + + } + +} ); + +module.exports = { + LineGeometry: LineGeometry +}; diff --git a/js/src/examples/lines/LineMaterial.js b/js/src/examples/lines/LineMaterial.js new file mode 100644 index 00000000..b797a022 --- /dev/null +++ b/js/src/examples/lines/LineMaterial.js @@ -0,0 +1,402 @@ +/** + * @author WestLangley / http://github.com/WestLangley + * + * parameters = { + * color: , + * linewidth: , + * dashed: , + * dashScale: , + * dashSize: , + * gapSize: , + * resolution: , // to be set by renderer + * } + */ + +var THREE = require('three'); + +lineUniforms = { + + linewidth: { value: 1 }, + resolution: { value: new THREE.Vector2( 1, 1 ) }, + dashScale: { value: 1 }, + dashSize: { value: 1 }, + gapSize: { value: 1 } // todo FIX - maybe change to totalSize + +}; + +lineShaders = { + + uniforms: THREE.UniformsUtils.merge( [ + THREE.UniformsLib.common, + THREE.UniformsLib.fog, + lineUniforms + ] ), + + vertexShader: + ` + #include + #include + #include + #include + #include + + uniform float linewidth; + uniform vec2 resolution; + + attribute vec3 instanceStart; + attribute vec3 instanceEnd; + + attribute vec3 instanceColorStart; + attribute vec3 instanceColorEnd; + + varying vec2 vUv; + + #ifdef USE_DASH + + uniform float dashScale; + attribute float instanceDistanceStart; + attribute float instanceDistanceEnd; + varying float vLineDistance; + + #endif + + void trimSegment( const in vec4 start, inout vec4 end ) { + + // trim end segment so it terminates between the camera plane and the near plane + + // conservative estimate of the near plane + float a = projectionMatrix[ 2 ][ 2 ]; // 3nd entry in 3th column + float b = projectionMatrix[ 3 ][ 2 ]; // 3nd entry in 4th column + float nearEstimate = - 0.5 * b / a; + + float alpha = ( nearEstimate - start.z ) / ( end.z - start.z ); + + end.xyz = mix( start.xyz, end.xyz, alpha ); + + } + + void main() { + + #ifdef USE_COLOR + + vColor.xyz = ( position.y < 0.5 ) ? instanceColorStart : instanceColorEnd; + + #endif + + #ifdef USE_DASH + + vLineDistance = ( position.y < 0.5 ) ? dashScale * instanceDistanceStart : dashScale * instanceDistanceEnd; + + #endif + + float aspect = resolution.x / resolution.y; + + vUv = uv; + + // camera space + vec4 start = modelViewMatrix * vec4( instanceStart, 1.0 ); + vec4 end = modelViewMatrix * vec4( instanceEnd, 1.0 ); + + // special case for perspective projection, and segments that terminate either in, or behind, the camera plane + // clearly the gpu firmware has a way of addressing this issue when projecting into ndc space + // but we need to perform ndc-space calculations in the shader, so we must address this issue directly + // perhaps there is a more elegant solution -- WestLangley + + bool perspective = ( projectionMatrix[ 2 ][ 3 ] == - 1.0 ); // 4th entry in the 3rd column + + if ( perspective ) { + + if ( start.z < 0.0 && end.z >= 0.0 ) { + + trimSegment( start, end ); + + } else if ( end.z < 0.0 && start.z >= 0.0 ) { + + trimSegment( end, start ); + + } + + } + + // clip space + vec4 clipStart = projectionMatrix * start; + vec4 clipEnd = projectionMatrix * end; + + // ndc space + vec2 ndcStart = clipStart.xy / clipStart.w; + vec2 ndcEnd = clipEnd.xy / clipEnd.w; + + // direction + vec2 dir = ndcEnd - ndcStart; + + // account for clip-space aspect ratio + dir.x *= aspect; + dir = normalize( dir ); + + // perpendicular to dir + vec2 offset = vec2( dir.y, - dir.x ); + + // undo aspect ratio adjustment + dir.x /= aspect; + offset.x /= aspect; + + // sign flip + if ( position.x < 0.0 ) offset *= - 1.0; + + // endcaps + if ( position.y < 0.0 ) { + + offset += - dir; + + } else if ( position.y > 1.0 ) { + + offset += dir; + + } + + // adjust for linewidth + offset *= linewidth; + + // adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ... + offset /= resolution.y; + + // select end + vec4 clip = ( position.y < 0.5 ) ? clipStart : clipEnd; + + // back to clip space + offset *= clip.w; + + clip.xy += offset; + + gl_Position = clip; + + vec4 mvPosition = ( position.y < 0.5 ) ? start : end; // this is an approximation + + #include + #include + #include + + } + `, + + fragmentShader: + ` + uniform vec3 diffuse; + uniform float opacity; + + #ifdef USE_DASH + + uniform float dashSize; + uniform float gapSize; + + #endif + + varying float vLineDistance; + + #include + #include + #include + #include + #include + + varying vec2 vUv; + + void main() { + + #include + + #ifdef USE_DASH + + if ( vUv.y < - 1.0 || vUv.y > 1.0 ) discard; // discard endcaps + + if ( mod( vLineDistance, dashSize + gapSize ) > dashSize ) discard; // todo - FIX + + #endif + + if ( abs( vUv.y ) > 1.0 ) { + + float a = vUv.x; + float b = ( vUv.y > 0.0 ) ? vUv.y - 1.0 : vUv.y + 1.0; + float len2 = a * a + b * b; + + if ( len2 > 1.0 ) discard; + + } + + vec4 diffuseColor = vec4( diffuse, opacity ); + + #include + #include + + gl_FragColor = vec4( diffuseColor.rgb, diffuseColor.a ); + + #include + #include + #include + #include + + } + ` +}; + + +var LineMaterial = function ( parameters ) { + + THREE.ShaderMaterial.call( this, { + + type: 'LineMaterial', + + uniforms: THREE.UniformsUtils.clone( lineShaders.uniforms ), + + vertexShader: lineShaders.vertexShader, + fragmentShader: lineShaders.fragmentShader + + } ); + + this.dashed = false; + + Object.defineProperties( this, { + + color: { + + enumerable: true, + + get: function () { + + return this.uniforms.diffuse.value; + + }, + + set: function ( value ) { + + this.uniforms.diffuse.value = value; + + } + + }, + + linewidth: { + + enumerable: true, + + get: function () { + + return this.uniforms.linewidth.value; + + }, + + set: function ( value ) { + + this.uniforms.linewidth.value = value; + + } + + }, + + dashScale: { + + enumerable: true, + + get: function () { + + return this.uniforms.dashScale.value; + + }, + + set: function ( value ) { + + this.uniforms.dashScale.value = value; + + } + + }, + + dashSize: { + + enumerable: true, + + get: function () { + + return this.uniforms.dashSize.value; + + }, + + set: function ( value ) { + + this.uniforms.dashSize.value = value; + + } + + }, + + gapSize: { + + enumerable: true, + + get: function () { + + return this.uniforms.gapSize.value; + + }, + + set: function ( value ) { + + this.uniforms.gapSize.value = value; + + } + + }, + + resolution: { + + enumerable: true, + + get: function () { + + return this.uniforms.resolution.value; + + }, + + set: function ( value ) { + + this.uniforms.resolution.value.copy( value ); + + } + + } + + } ); + + this.setValues( parameters ); + +}; + +LineMaterial.prototype = Object.create( THREE.ShaderMaterial.prototype ); +LineMaterial.prototype.constructor = LineMaterial; + +LineMaterial.prototype.isLineMaterial = true; + +LineMaterial.prototype.copy = function ( source ) { + + THREE.ShaderMaterial.prototype.copy.call( this, source ); + + this.color.copy( source.color ); + + this.linewidth = source.linewidth; + + this.resolution = source.resolution; + + this.dashScale = source.dashScale; + + this.dashSize = source.dashSize; + + this.gapSize = source.gapSize; + + return this; + +}; + + +module.exports = { + LineMaterial: LineMaterial +}; diff --git a/js/src/examples/lines/LineSegments2.js b/js/src/examples/lines/LineSegments2.js new file mode 100644 index 00000000..7deee27c --- /dev/null +++ b/js/src/examples/lines/LineSegments2.js @@ -0,0 +1,52 @@ +/** + * @author WestLangley / http://github.com/WestLangley + * + */ + +var THREE = require('three'); +var LineSegmentsGeometry = require('./LineSegmentsGeometry').LineSegmentsGeometry; +var LineMaterial = require('./LineMaterial').LineMaterial; + + +var LineSegments2 = function ( geometry, material ) { + + THREE.Mesh.call( this ); + + this.type = 'LineSegments2'; + + this.geometry = geometry !== undefined ? geometry : new LineSegmentsGeometry(); + this.material = material !== undefined ? material : new LineMaterial( { color: Math.random() * 0xffffff } ); + +}; + +LineSegments2.prototype = Object.assign( Object.create( THREE.Mesh.prototype ), { + + constructor: LineSegments2, + + isLineSegments2: true, + + onBeforeRender: function( renderer, scene, camera, geometry, material, group ) { + + if ( material.isLineMaterial ) { + + var size = renderer.getSize(); + + material.resolution = new THREE.Vector2(size.width, size.height); + + } + + }, + + copy: function ( source ) { + + // todo + + return this; + + }, + +} ); + +module.exports = { + LineSegments2: LineSegments2 +}; diff --git a/js/src/examples/lines/LineSegmentsGeometry.js b/js/src/examples/lines/LineSegmentsGeometry.js new file mode 100644 index 00000000..d57685ef --- /dev/null +++ b/js/src/examples/lines/LineSegmentsGeometry.js @@ -0,0 +1,254 @@ +/** + * @author WestLangley / http://github.com/WestLangley + * + */ + +var THREE = require('three'); + +var LineSegmentsGeometry = function () { + + THREE.InstancedBufferGeometry.call( this ); + + this.type = 'LineSegmentsGeometry'; + + var positions = [ - 1, 2, 0, 1, 2, 0, - 1, 1, 0, 1, 1, 0, - 1, 0, 0, 1, 0, 0, - 1, - 1, 0, 1, - 1, 0 ]; + var uvs = [ - 1, 2, 1, 2, - 1, 1, 1, 1, - 1, - 1, 1, - 1, - 1, - 2, 1, - 2 ]; + var index = [ 0, 2, 1, 2, 3, 1, 2, 4, 3, 4, 5, 3, 4, 6, 5, 6, 7, 5 ]; + + this.setIndex( index ); + this.addAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) ); + this.addAttribute( 'uv', new THREE.Float32BufferAttribute( uvs, 2 ) ); + +}; + +LineSegmentsGeometry.prototype = Object.assign( Object.create( THREE.InstancedBufferGeometry.prototype ), { + + constructor: LineSegmentsGeometry, + + isLineSegmentsGeometry: true, + + applyMatrix: function ( matrix ) { + + var start = this.attributes.instanceStart; + var end = this.attributes.instanceEnd; + + if ( start !== undefined ) { + + matrix.applyToBufferAttribute( start ); + + matrix.applyToBufferAttribute( end ); + + start.data.needsUpdate = true; + + } + + if ( this.boundingBox !== null ) { + + this.computeBoundingBox(); + + } + + if ( this.boundingSphere !== null ) { + + this.computeBoundingSphere(); + + } + + return this; + + }, + + setPositions: function ( array ) { + + var lineSegments; + + if ( array instanceof Float32Array ) { + + lineSegments = array; + + } else if ( Array.isArray( array ) ) { + + lineSegments = new Float32Array( array ); + + } + + var instanceBuffer = new THREE.InstancedInterleavedBuffer( lineSegments, 6, 1 ); // xyz, xyz + + this.addAttribute( 'instanceStart', new THREE.InterleavedBufferAttribute( instanceBuffer, 3, 0 ) ); // xyz + this.addAttribute( 'instanceEnd', new THREE.InterleavedBufferAttribute( instanceBuffer, 3, 3 ) ); // xyz + + // + + this.computeBoundingBox(); + this.computeBoundingSphere(); + + return this; + + }, + + setColors: function ( array ) { + + var colors; + + if ( array instanceof Float32Array ) { + + colors = array; + + } else if ( Array.isArray( array ) ) { + + colors = new Float32Array( array ); + + } + + var instanceColorBuffer = new THREE.InstancedInterleavedBuffer( colors, 6, 1 ); // rgb, rgb + + this.addAttribute( 'instanceColorStart', new THREE.InterleavedBufferAttribute( instanceColorBuffer, 3, 0 ) ); // rgb + this.addAttribute( 'instanceColorEnd', new THREE.InterleavedBufferAttribute( instanceColorBuffer, 3, 3 ) ); // rgb + + return this; + + }, + + fromWireframeGeometry: function ( geometry ) { + + this.setPositions( geometry.attributes.position.array ); + + return this; + + }, + + fromEdgesGeometry: function ( geometry ) { + + this.setPositions( geometry.attributes.position.array ); + + return this; + + }, + + fromLineSegements: function ( lineSegments ) { + + var geometry = lineSegments.geometry; + + if ( geometry.isGeometry ) { + + this.setPositions( geometry.vertices ); + + } else if ( geometry.isBufferGeometry ) { + + this.setPositions( geometry.position.array ); // assumes non-indexed + + } + + // set colors, maybe + + return this; + + }, + + computeBoundingBox: function () { + + var box = new THREE.Box3(); + + return function computeBoundingBox() { + + if ( this.boundingBox === null ) { + + this.boundingBox = new THREE.Box3(); + + } + + var start = this.attributes.instanceStart; + var end = this.attributes.instanceEnd; + + if ( start !== undefined && end !== undefined ) { + + this.boundingBox.setFromBufferAttribute( start ); + + box.setFromBufferAttribute( end ); + + this.boundingBox.union( box ); + + } + + }; + + }(), + + computeBoundingSphere: function () { + + var vector = new THREE.Vector3(); + + return function computeBoundingSphere() { + + if ( this.boundingSphere === null ) { + + this.boundingSphere = new THREE.Sphere(); + + } + + if ( this.boundingBox === null ) { + + this.computeBoundingBox(); + + } + + var start = this.attributes.instanceStart; + var end = this.attributes.instanceEnd; + + if ( start !== undefined && end !== undefined ) { + + var center = this.boundingSphere.center; + + this.boundingBox.getCenter( center ); + + var maxRadiusSq = 0; + + for ( var i = 0, il = start.count; i < il; i ++ ) { + + vector.fromBufferAttribute( start, i ); + maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( vector ) ); + + vector.fromBufferAttribute( end, i ); + maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( vector ) ); + + } + + this.boundingSphere.radius = Math.sqrt( maxRadiusSq ); + + if ( isNaN( this.boundingSphere.radius ) ) { + + console.error( 'LineSegmentsGeometry.computeBoundingSphere(): Computed radius is NaN. The instanced position data is likely to have NaN values.', this ); + + } + + } + + }; + + }(), + + toJSON: function () { + + // todo + + }, + + clone: function () { + + // todo + + }, + + copy: function ( source ) { + + // todo + + return this; + + } + +} ); + +module.exports = { + LineSegmentsGeometry: LineSegmentsGeometry +}; diff --git a/js/src/geometries/LineGeometry.js b/js/src/geometries/LineGeometry.js new file mode 100644 index 00000000..607888cc --- /dev/null +++ b/js/src/geometries/LineGeometry.js @@ -0,0 +1,24 @@ +var Promise = require('bluebird'); +var LineGeometry = require('../examples/lines/LineGeometry.js').LineGeometry; +var LineGeometryAutogen = require('./LineGeometry.autogen'); + +var utils = require('../_base/utils'); + + +var LineGeometryModel = LineGeometryAutogen.LineGeometryModel.extend({ + + + constructThreeObject: function() { + + var result = new LineGeometry(); + return Promise.resolve(result); + + }, + +}); + +utils.customModelsLut[LineGeometry.prototype.constructor.name] = 'LineGeometry'; + +module.exports = { + LineGeometryModel: LineGeometryModel, +}; diff --git a/js/src/geometries/LineSegmentsGeometry.js b/js/src/geometries/LineSegmentsGeometry.js new file mode 100644 index 00000000..ef60a347 --- /dev/null +++ b/js/src/geometries/LineSegmentsGeometry.js @@ -0,0 +1,56 @@ +var Promise = require('bluebird'); +var LineSegmentsGeometry = require('../examples/lines/LineSegmentsGeometry.js').LineSegmentsGeometry; +var LineSegmentsGeometryAutogen = require('./LineSegmentsGeometry.autogen').LineSegmentsGeometryModel; + +var utils = require('../_base/utils'); + + +var LineSegmentsGeometryModel = LineSegmentsGeometryAutogen.extend({ + + constructThreeObject: function() { + + var result = new LineSegmentsGeometry(); + return Promise.resolve(result); + + }, + + createPropertiesArrays: function() { + + LineSegmentsGeometryAutogen.prototype.createPropertiesArrays.call(this); + + this.property_assigners['positions'] = 'assignLineAttribute'; + this.property_assigners['colors'] = 'assignLineAttribute'; + + }, + + assignLineAttribute: function(obj, key, value) { + if (key === 'positions') { + obj.setPositions(value); + } else if (key === 'colors') { + obj.setColors(value); + } else { + throw new Error(`Unknown line attribute key: ${key}`); + } + }, + + convertArrayBufferThreeToModel: function(arrBuffer, propName) { + if (arrBuffer === null) { + return null; + } + var current = this.get(propName); + var currentArray = dataserializers.getArray(current); + if (currentArray && (currentArray.data === arrBuffer)) { + // Unchanged, do nothing + return current; + } + // Never create a new widget, even if current is one + return ndarray(arrBuffer, currentArray && currentArray.shape); + }, + +}); + +utils.customModelsLut[LineSegmentsGeometry.prototype.constructor.name] = 'LineSegmentsGeometry'; + +module.exports = { + LineSegmentsGeometryModel: LineSegmentsGeometryModel, +}; diff --git a/js/src/materials/LineMaterial.js b/js/src/materials/LineMaterial.js new file mode 100644 index 00000000..accd0557 --- /dev/null +++ b/js/src/materials/LineMaterial.js @@ -0,0 +1,30 @@ +var Promise = require('bluebird'); +var LineMaterial = require('../examples/lines/LineMaterial.js').LineMaterial; +var LineMaterialAutogen = require('./LineMaterial.autogen').LineMaterialModel; + +var utils = require('../_base/utils'); + + +var LineMaterialModel = LineMaterialAutogen.extend({ + + constructThreeObject: function() { + + var result = new LineMaterial({ + color: this.convertColorModelToThree(this.get('color'), 'color'), + dashScale: this.convertFloatModelToThree(this.get('dashScale'), 'dashScale'), + dashSize: this.convertFloatModelToThree(this.get('dashSize'), 'dashSize'), + gapSize: this.convertFloatModelToThree(this.get('gapSize'), 'gapSize'), + linewidth: this.convertFloatModelToThree(this.get('linewidth'), 'linewidth'), + type: this.get('type'), + }); + return Promise.resolve(result); + + }, + +}); + +utils.customModelsLut[LineMaterial.prototype.constructor.name] = 'LineMaterial'; + +module.exports = { + LineMaterialModel: LineMaterialModel, +}; diff --git a/js/src/objects/Line2.js b/js/src/objects/Line2.js new file mode 100644 index 00000000..c4fd5cfd --- /dev/null +++ b/js/src/objects/Line2.js @@ -0,0 +1,21 @@ +var Promise = require('bluebird'); +var Line2 = require('../examples/lines/Line2.js').Line2; +var Line2Autogen = require('./Line2.autogen').Line2Model; + +var Line2Model = Line2Autogen.extend({ + + constructThreeObject: function() { + + var result = new Line2( + this.convertThreeTypeModelToThree(this.get('geometry'), 'geometry'), + this.convertThreeTypeArrayModelToThree(this.get('material'), 'material') + ); + return Promise.resolve(result); + + }, + +}); + +module.exports = { + Line2Model: Line2Model, +}; diff --git a/js/src/objects/LineSegments2.js b/js/src/objects/LineSegments2.js new file mode 100644 index 00000000..a601334b --- /dev/null +++ b/js/src/objects/LineSegments2.js @@ -0,0 +1,21 @@ +var Promise = require('bluebird'); +var LineSegments2 = require('../examples/lines/LineSegments2.js').LineSegments2; +var LineSegments2Autogen = require('./LineSegments2.autogen').LineSegments2Model; + +var LineSegments2Model = LineSegments2Autogen.extend({ + + constructThreeObject: function() { + + var result = new LineSegments2( + this.convertThreeTypeModelToThree(this.get('geometry'), 'geometry'), + this.convertThreeTypeArrayModelToThree(this.get('material'), 'material') + ); + return Promise.resolve(result); + + }, + +}); + +module.exports = { + LineSegments2Model: LineSegments2Model, +}; diff --git a/pythreejs/traits.py b/pythreejs/traits.py index 687d1700..ff91ff04 100644 --- a/pythreejs/traits.py +++ b/pythreejs/traits.py @@ -11,7 +11,7 @@ from ipywidgets import widget_serialization -from ipydatawidgets import DataUnion, NDArrayWidget +from ipydatawidgets import DataUnion, NDArrayWidget, shape_constraints import numpy as np