立体solid.ts
1 import {chunk} from '@mathigon/core'; 2 import {$html, $N, Browser, CustomElementView, register, slide} from '@mathigon/boost'; 3 import {create3D, Graphics3D} from './webgl'; 4 5 const STROKE_COLOR = 0x666666; 6 const LINE_RADIUS = 0.012; 7 const LINE_SEGMENTS = 4; 8 const POINT_RADIUS = 0.08;
1 // Utilities 2 3 type Vector = [number, number, number]; 4 5 // Custom methods on the THREE.Object3D class 6 interface Object3D extends THREE.Object3D { 7 setClipPlanes?: (planes: THREE.Plane[]) => void; 8 updateGeometry?: (gep: THREE.Geometry) => void; 9 updateEnds?: (f: Vector, t: Vector) => void; 10 } 11 12 function rotate($solid: Solid, animate = true, speed = 1) { 13 // TODO Damping after mouse movement 14 // TODO Better mouse-to-point mapping 15 16 // Only Chrome is fast enough to support auto-rotation. 17 const autoRotate = animate && Browser.isChrome && !Browser.isMobile; 18 $solid.css('cursor', 'grab'); 19 20 let dragging = false; 21 let visible = false; 22 23 function frame() { 24 if (visible && autoRotate) requestAnimationFrame(frame); 25 $solid.scene.draw(); 26 if (!dragging) $solid.object.rotation.y += speed * 0.012; 27 } 28 29 if (autoRotate) { 30 $solid.scene.$canvas.on('enterViewport', () => { visible = true; frame(); }); 31 $solid.scene.$canvas.on('exitViewport', () => { visible = false; }); 32 } else { 33 setTimeout(frame); 34 } 35 36 // The 1.1 creates rotations that are slightly faster than the mouse/finger. 37 const s = Math.PI / 2 / $solid.scene.$canvas.width * 1.1; 38 39 slide($solid.scene.$canvas, { 40 start() { 41 dragging = true; 42 $html.addClass('grabbing'); 43 }, 44 move(posn, start, last) { 45 const d = posn.subtract(last).scale(s); 46 const q = new THREE.Quaternion().setFromEuler(new THREE.Euler(d.y, d.x)); 47 $solid.object.quaternion.multiplyQuaternions(q, $solid.object.quaternion); 48 $solid.trigger('rotate', {quaternion: $solid.object.quaternion}); 49 if (!autoRotate) frame(); 50 }, 51 end() { 52 dragging = false; 53 $html.removeClass('grabbing'); 54 } 55 }); 56 } 57 58 function createEdges(geometry: THREE.Geometry, material: THREE.Material, maxAngle?: number) { 59 const obj = new THREE.Object3D(); 60 if (!maxAngle) return obj; 61 62 const edges = new THREE.EdgesGeometry(geometry, maxAngle); 63 const edgeData = edges.attributes.position.array as number[]; 64 const points = chunk(chunk(edgeData, 3).map(p => new THREE.Vector3(...p)), 2); 65 66 for (const edge of points) { 67 const curve = new THREE.LineCurve3(edge[0], edge[1]); 68 const geometry = new THREE.TubeGeometry(curve, 1, LINE_RADIUS, LINE_SEGMENTS); 69 obj.add(new THREE.Mesh(geometry, material)); 70 } 71 72 return obj; 73 }
1 // Custom Element 2 3 @register('x-solid') 4 export class Solid extends CustomElementView { 5 private isReady = false; 6 object!: THREE.Object3D; 7 scene!: Graphics3D; 8 9 async ready() { 10 const size = this.attr('size').split(','); 11 const width = +size[0]; 12 const height = size.length > 1 ? +size[1] : width; 13 14 this.css({width: width + 'px', height: height + 'px'}); 15 16 this.scene = await create3D(this, 35, 2 * width, 2 * height); 17 this.scene.camera.position.set(0, 3, 6); 18 this.scene.camera.up = new THREE.Vector3(0, 1, 0); 19 this.scene.camera.lookAt(new THREE.Vector3(0, 0, 0)); 20 21 const light1 = new THREE.AmbientLight(0x555555); 22 this.scene.add(light1); 23 24 const light2 = new THREE.PointLight(0xffffff); 25 light2.position.set(3, 4.5, 6); 26 this.scene.add(light2); 27 28 this.object = new THREE.Object3D(); 29 this.scene.add(this.object); 30 31 this.trigger('loaded'); 32 this.isReady = true; 33 } 34 35 addMesh(fn: (scene: Graphics3D) => THREE.Object3D[]|void) { 36 if (this.isReady) { 37 this.addMeshCallback(fn); 38 } else { 39 this.one('loaded', () => this.addMeshCallback(fn)); 40 } 41 } 42 43 addMeshCallback(fn: (scene: Graphics3D) => THREE.Object3D[]|void) { 44 const items = fn(this.scene) || []; 45 for (const i of items) this.object.add(i); 46 47 if (!this.hasAttr('static')) { 48 const speed = +this.attr('rotate') || 1; 49 rotate(this, this.hasAttr('rotate'), speed); 50 } 51 52 this.scene.draw(); 53 } 54 55 rotate(q: THREE.Quaternion) { 56 this.object.quaternion.set(q.x, q.y, q.z, q.w); 57 this.scene.draw(); 58 }
1 // Element Creation Utilities 2 3 addLabel(text: string, posn: Vector, color = STROKE_COLOR, margin = '') { 4 const $label = $N('div', {text, class: 'label3d'}); 5 $label.css('color', '#' + color.toString(16).padStart(6, '0')); 6 if (margin) $label.css('margin', margin); 7 8 let posn1 = new THREE.Vector3(...posn); 9 this.scene.$canvas.insertAfter($label); 10 11 this.scene.onDraw(() => { 12 const p = posn1.clone().applyQuaternion(this.object.quaternion) 13 .add(this.object.position).project(this.scene.camera); 14 $label.css('left', (1 + p.x) * this.scene.$canvas.width / 2 + 'px'); 15 $label.css('top', (1 - p.y) * this.scene.$canvas.height / 2 + 'px'); 16 }); 17 18 return { 19 updatePosition(posn: Vector) { 20 posn1 = new THREE.Vector3(...posn); 21 } 22 }; 23 } 24 25 addArrow(from: Vector, to: Vector, color = STROKE_COLOR) { 26 const material = new THREE.MeshBasicMaterial({color}); 27 const obj = new THREE.Object3D() as Object3D; 28 29 const height = new THREE.Vector3(...from).distanceTo(new THREE.Vector3(...to)); 30 const line = new THREE.CylinderGeometry(0.02, 0.02, height - 0.3, 8, 1, true); 31 obj.add(new THREE.Mesh(line, material)); 32 33 const start = new THREE.ConeGeometry(0.1, 0.15, 16, 1); 34 start.translate(0, height/2 - 0.1, 0); 35 obj.add(new THREE.Mesh(start, material)); 36 37 const end = new THREE.ConeGeometry(0.1, 0.15, 16, 1); 38 end.rotateX(Math.PI); 39 end.translate(0, -height/2 + 0.1, 0); 40 obj.add(new THREE.Mesh(end, material)); 41 42 obj.updateEnds = function(f: Vector, t: Vector) { 43 // TODO Support changing the height of the arrow. 44 const q = new THREE.Quaternion(); 45 const v = new THREE.Vector3(t[0]-f[0], t[1]-f[1], t[2]-f[2]).normalize(); 46 q.setFromUnitVectors(new THREE.Vector3(0, 1, 0), v); 47 obj.setRotationFromQuaternion(q); 48 obj.position.set((f[0]+t[0])/2, (f[1]+t[1])/2, (f[2]+t[2])/2); 49 }; 50 51 obj.updateEnds(from, to); 52 this.object.add(obj); 53 return obj; 54 } 55 56 addCircle(radius: number, color = STROKE_COLOR, segments = 64) { 57 const path = new THREE.Curve<THREE.Vector3>(); 58 path.getPoint = function(t) { 59 const a = 2 * Math.PI * t; 60 return new THREE.Vector3(radius * Math.cos(a), radius * Math.sin(a), 0); 61 }; 62 63 const material = new THREE.MeshBasicMaterial({color}); 64 const geometry = new THREE.TubeGeometry(path, segments, LINE_RADIUS, LINE_SEGMENTS); 65 66 const mesh = new THREE.Mesh(geometry, material); 67 this.object.add(mesh); 68 return mesh; 69 } 70 71 addPoint(position: Vector, color = STROKE_COLOR) { 72 const material = new THREE.MeshBasicMaterial({color}); 73 const geometry = new THREE.SphereGeometry(POINT_RADIUS, 16, 16); 74 75 const mesh = new THREE.Mesh(geometry, material); 76 mesh.position.set(...position); 77 this.object.add(mesh); 78 } 79 80 addSolid(geo: THREE.Geometry, color: number, maxAngle = 5, flatShading = false) { 81 const edgeMaterial = new THREE.LineBasicMaterial({color: 0xffffff}); 82 const edges = new THREE.EdgesGeometry(geo, maxAngle); 83 84 const obj = new THREE.Object3D(); 85 obj.add(new THREE.LineSegments(edges, edgeMaterial)); 86 obj.add(new THREE.Mesh(geo, Solid.solidMaterial(color, flatShading))); 87 88 this.object.add(obj); 89 return obj; 90 } 91 92 // TODO merge addOutlined() and addWireframe(), by looking at 93 // geometry.isConeGeometry etc. 94 95 // A translucent material with a solid border. 96 addOutlined(geo: THREE.Geometry, color = 0xaaaaaa, maxAngle = 5, opacity = 0.1, strokeColor?: number) { 97 const solidMaterial = Solid.translucentMaterial(color, opacity); 98 const solid = new THREE.Mesh(geo, solidMaterial); 99 100 const edgeMaterial = new THREE.MeshBasicMaterial({color: strokeColor || STROKE_COLOR}); 101 let edges = createEdges(geo, edgeMaterial, maxAngle); 102 103 const obj = new THREE.Object3D() as Object3D; 104 obj.add(solid, edges); 105 106 obj.setClipPlanes = function(planes: THREE.Plane[]) { 107 solidMaterial.clippingPlanes = planes; 108 }; 109 110 obj.updateGeometry = function(geo: THREE.Geometry) { 111 solid.geometry.dispose(); 112 solid.geometry = geo; 113 obj.remove(edges); 114 edges = createEdges(geo, edgeMaterial, maxAngle); 115 obj.add(edges); 116 }; 117 118 this.object.add(obj); 119 return obj; 120 } 121 122 // Like .addOutlined, but we also add outlines for curved edges (e.g. of 123 // a sphere or cylinder). 124 addWireframe(geometry: THREE.Geometry, color = 0xaaaaaa, maxAngle = 5, opacity = 0.1) { 125 const solid = this.addOutlined(geometry, color, maxAngle, opacity); 126 127 const outlineMaterial = new THREE.MeshBasicMaterial({ 128 color: STROKE_COLOR, 129 side: THREE.BackSide 130 }); 131 outlineMaterial.onBeforeCompile = function(shader) { 132 const token = '#include <begin_vertex>'; 133 const customTransform = '\nvec3 transformed = position + vec3(normal) * 0.02;\n'; 134 shader.vertexShader = shader.vertexShader.replace(token,customTransform) 135 }; 136 const outline = new THREE.Mesh(geometry, outlineMaterial); 137 138 const knockoutMaterial = new THREE.MeshBasicMaterial({ 139 color: 0xffffff, 140 side: THREE.BackSide 141 }); 142 const knockout = new THREE.Mesh(geometry, knockoutMaterial); 143 144 const obj = new THREE.Object3D() as Object3D; 145 obj.add(solid, outline, knockout); 146 147 obj.setClipPlanes = function(planes: THREE.Plane[]) { 148 if (solid.setClipPlanes) solid.setClipPlanes(planes); 149 for (const m of [outlineMaterial, knockoutMaterial]) 150 m.clippingPlanes = planes; 151 }; 152 153 obj.updateGeometry = function(geo: THREE.Geometry) { 154 if (solid.updateGeometry) solid.updateGeometry(geo); 155 for (const mesh of [outline, knockout]) { 156 mesh.geometry.dispose(); 157 mesh.geometry = geo; 158 } 159 }; 160 161 this.object.add(obj); 162 return obj; 163 }
1 // Materials 2 3 static solidMaterial(color: number, flatShading = false) { 4 return new THREE.MeshPhongMaterial({ 5 side: THREE.DoubleSide, 6 transparent: true, 7 opacity: 0.9, 8 specular: 0x222222, 9 // depthWrite: false, 10 color, flatShading 11 }); 12 } 13 14 static translucentMaterial(color: number, opacity = 0.1) { 15 return new THREE.MeshLambertMaterial({ 16 side: THREE.DoubleSide, 17 transparent: true, 18 depthWrite: false, 19 opacity, color 20 }); 21 } 22 }