Ver Fonte

Lint manual (#25312)

* Lint manual

* Restore changes to French manual
Levi Pesin há 2 anos atrás
pai
commit
cdf48ceecf
35 ficheiros alterados com 7865 adições e 5970 exclusões
  1. 2 2
      examples/jsm/csm/CSM.js
  2. 117 93
      manual/examples/offscreencanvas-cubes.js
  3. 20 13
      manual/examples/offscreencanvas-worker-cubes.js
  4. 109 75
      manual/examples/offscreencanvas-worker-orbitcontrols.js
  5. 26 17
      manual/examples/offscreencanvas-worker-picking.js
  6. 70 49
      manual/examples/resources/drag-and-drop.js
  7. 213 155
      manual/examples/resources/editor-settings.js
  8. 1303 964
      manual/examples/resources/editor.js
  9. 726 507
      manual/examples/resources/lessons-helper.js
  10. 201 148
      manual/examples/resources/lessons-worker-helper.js
  11. 328 221
      manual/examples/resources/lut-reader.js
  12. 42 29
      manual/examples/resources/threejs-utils.js
  13. 558 437
      manual/examples/resources/webgl-debug-helper.js
  14. 99 82
      manual/examples/shared-cubes.js
  15. 191 154
      manual/examples/shared-orbitcontrols.js
  16. 143 116
      manual/examples/shared-picking.js
  17. 92 75
      manual/examples/threejs-responsive.js
  18. 165 114
      manual/resources/canvas-wrapper.js
  19. 68 46
      manual/resources/lesson.js
  20. 364 279
      manual/resources/threejs-align-html-elements-to-3d.js
  21. 81 67
      manual/resources/threejs-cameras.js
  22. 77 66
      manual/resources/threejs-custom-buffergeometry.js
  23. 201 157
      manual/resources/threejs-fog.js
  24. 342 268
      manual/resources/threejs-lesson-utils.js
  25. 138 104
      manual/resources/threejs-lights.js
  26. 136 107
      manual/resources/threejs-lots-of-objects.js
  27. 420 333
      manual/resources/threejs-materials.js
  28. 138 98
      manual/resources/threejs-post-processing-3dlut.js
  29. 580 481
      manual/resources/threejs-primitives.js
  30. 290 244
      manual/resources/threejs-textures.js
  31. 71 58
      manual/resources/threejs-voxel-geometry.js
  32. 30 20
      manual/resources/tools/geo-picking/make-geo-picking-texture-ogc.js
  33. 298 228
      manual/resources/tools/geo-picking/make-geo-picking-texture.js
  34. 225 162
      manual/resources/tools/geo-picking/ogc-parser.js
  35. 1 1
      package.json

+ 2 - 2
examples/jsm/csm/CSM.js

@@ -16,8 +16,8 @@ const _center = new Vector3();
 const _bbox = new Box3();
 const _uniformArray = [];
 const _logArray = [];
-const _lightOrientationMatrix = new Matrix4()
-const _lightOrientationMatrixInverse = new Matrix4()
+const _lightOrientationMatrix = new Matrix4();
+const _lightOrientationMatrixInverse = new Matrix4();
 const _up = new Vector3( 0, 1, 0 );
 
 export class CSM {

+ 117 - 93
manual/examples/offscreencanvas-cubes.js

@@ -1,106 +1,130 @@
 import * as THREE from 'https://cdn.skypack.dev/[email protected]/build/three.module.js';
 
 const state = {
-  width: 300,   // canvas default
-  height: 150,  // canvas default
+	width: 300, // canvas default
+	height: 150, // canvas default
 };
 
-function main(data) {
-  const {canvas} = data;
-  const renderer = new THREE.WebGLRenderer({canvas});
-
-  state.width = canvas.width;
-  state.height = canvas.height;
-
-  const fov = 75;
-  const aspect = 2; // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 4;
-
-  const scene = new THREE.Scene();
-
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(-1, 2, 4);
-    scene.add(light);
-  }
-
-  const boxWidth = 1;
-  const boxHeight = 1;
-  const boxDepth = 1;
-  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
-
-  function makeInstance(geometry, color, x) {
-    const material = new THREE.MeshPhongMaterial({
-      color,
-    });
-
-    const cube = new THREE.Mesh(geometry, material);
-    scene.add(cube);
-
-    cube.position.x = x;
-
-    return cube;
-  }
-
-  const cubes = [
-    makeInstance(geometry, 0x44aa88, 0),
-    makeInstance(geometry, 0x8844aa, -2),
-    makeInstance(geometry, 0xaa8844, 2),
-  ];
-
-  function resizeRendererToDisplaySize(renderer) {
-    const canvas = renderer.domElement;
-    const width = state.width;
-    const height = state.height;
-    const needResize = canvas.width !== width || canvas.height !== height;
-    if (needResize) {
-      renderer.setSize(width, height, false);
-    }
-    return needResize;
-  }
-
-  function render(time) {
-    time *= 0.001;
-
-    if (resizeRendererToDisplaySize(renderer)) {
-      camera.aspect = state.width / state.height;
-      camera.updateProjectionMatrix();
-    }
-
-    cubes.forEach((cube, ndx) => {
-      const speed = 1 + ndx * .1;
-      const rot = time * speed;
-      cube.rotation.x = rot;
-      cube.rotation.y = rot;
-    });
-
-    renderer.render(scene, camera);
-
-    requestAnimationFrame(render);
-  }
-
-  requestAnimationFrame(render);
+function main( data ) {
+
+	const { canvas } = data;
+	const renderer = new THREE.WebGLRenderer( { canvas } );
+
+	state.width = canvas.width;
+	state.height = canvas.height;
+
+	const fov = 75;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 4;
+
+	const scene = new THREE.Scene();
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 1;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( - 1, 2, 4 );
+		scene.add( light );
+
+	}
+
+	const boxWidth = 1;
+	const boxHeight = 1;
+	const boxDepth = 1;
+	const geometry = new THREE.BoxGeometry( boxWidth, boxHeight, boxDepth );
+
+	function makeInstance( geometry, color, x ) {
+
+		const material = new THREE.MeshPhongMaterial( {
+			color,
+		} );
+
+		const cube = new THREE.Mesh( geometry, material );
+		scene.add( cube );
+
+		cube.position.x = x;
+
+		return cube;
+
+	}
+
+	const cubes = [
+		makeInstance( geometry, 0x44aa88, 0 ),
+		makeInstance( geometry, 0x8844aa, - 2 ),
+		makeInstance( geometry, 0xaa8844, 2 ),
+	];
+
+	function resizeRendererToDisplaySize( renderer ) {
+
+		const canvas = renderer.domElement;
+		const width = state.width;
+		const height = state.height;
+		const needResize = canvas.width !== width || canvas.height !== height;
+		if ( needResize ) {
+
+			renderer.setSize( width, height, false );
+
+		}
+
+		return needResize;
+
+	}
+
+	function render( time ) {
+
+		time *= 0.001;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			camera.aspect = state.width / state.height;
+			camera.updateProjectionMatrix();
+
+		}
+
+		cubes.forEach( ( cube, ndx ) => {
+
+			const speed = 1 + ndx * .1;
+			const rot = time * speed;
+			cube.rotation.x = rot;
+			cube.rotation.y = rot;
+
+		} );
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
+
 }
 
-function size(data) {
-  state.width = data.width;
-  state.height = data.height;
+function size( data ) {
+
+	state.width = data.width;
+	state.height = data.height;
+
 }
 
 const handlers = {
-  main,
-  size,
+	main,
+	size,
 };
 
-self.onmessage = function(e) {
-  const fn = handlers[e.data.type];
-  if (typeof fn !== 'function') {
-    throw new Error('no handler for type: ' + e.data.type);
-  }
-  fn(e.data);
+self.onmessage = function ( e ) {
+
+	const fn = handlers[ e.data.type ];
+	if ( typeof fn !== 'function' ) {
+
+		throw new Error( 'no handler for type: ' + e.data.type );
+
+	}
+
+	fn( e.data );
+
 };

+ 20 - 13
manual/examples/offscreencanvas-worker-cubes.js

@@ -1,19 +1,26 @@
-import {init, state} from './shared-cubes.js';
+import { init, state } from './shared-cubes.js';
+
+function size( data ) {
+
+	state.width = data.width;
+	state.height = data.height;
 
-function size(data) {
-  state.width = data.width;
-  state.height = data.height;
 }
 
 const handlers = {
-  init,
-  size,
+	init,
+	size,
 };
 
-self.onmessage = function(e) {
-  const fn = handlers[e.data.type];
-  if (typeof fn !== 'function') {
-    throw new Error('no handler for type: ' + e.data.type);
-  }
-  fn(e.data);
-};
+self.onmessage = function ( e ) {
+
+	const fn = handlers[ e.data.type ];
+	if ( typeof fn !== 'function' ) {
+
+		throw new Error( 'no handler for type: ' + e.data.type );
+
+	}
+
+	fn( e.data );
+
+};

+ 109 - 75
manual/examples/offscreencanvas-worker-orbitcontrols.js

@@ -1,95 +1,129 @@
-import {init} from './shared-orbitcontrols.js';
-import {EventDispatcher} from 'https://cdn.skypack.dev/[email protected]/build/three.module.js';
+import { init } from './shared-orbitcontrols.js';
+import { EventDispatcher } from 'https://cdn.skypack.dev/[email protected]/build/three.module.js';
 
 function noop() {
 }
 
 class ElementProxyReceiver extends EventDispatcher {
-  constructor() {
-    super();
-    // because OrbitControls try to set style.touchAction;
-    this.style = {};
-  }
-  get clientWidth() {
-    return this.width;
-  }
-  get clientHeight() {
-    return this.height;
-  }
-  // OrbitControls call these as of r132. Maybe we should implement them
-  setPointerCapture() { }
-  releasePointerCapture() { }
-  getBoundingClientRect() {
-    return {
-      left: this.left,
-      top: this.top,
-      width: this.width,
-      height: this.height,
-      right: this.left + this.width,
-      bottom: this.top + this.height,
-    };
-  }
-  handleEvent(data) {
-    if (data.type === 'size') {
-      this.left = data.left;
-      this.top = data.top;
-      this.width = data.width;
-      this.height = data.height;
-      return;
-    }
-    data.preventDefault = noop;
-    data.stopPropagation = noop;
-    this.dispatchEvent(data);
-  }
-  focus() {
-    // no-op
-  }
+
+	constructor() {
+
+		super();
+		// because OrbitControls try to set style.touchAction;
+		this.style = {};
+
+	}
+	get clientWidth() {
+
+		return this.width;
+
+	}
+	get clientHeight() {
+
+		return this.height;
+
+	}
+	// OrbitControls call these as of r132. Maybe we should implement them
+	setPointerCapture() { }
+	releasePointerCapture() { }
+	getBoundingClientRect() {
+
+		return {
+			left: this.left,
+			top: this.top,
+			width: this.width,
+			height: this.height,
+			right: this.left + this.width,
+			bottom: this.top + this.height,
+		};
+
+	}
+	handleEvent( data ) {
+
+		if ( data.type === 'size' ) {
+
+			this.left = data.left;
+			this.top = data.top;
+			this.width = data.width;
+			this.height = data.height;
+			return;
+
+		}
+
+		data.preventDefault = noop;
+		data.stopPropagation = noop;
+		this.dispatchEvent( data );
+
+	}
+	focus() {
+		// no-op
+	}
+
 }
 
 class ProxyManager {
-  constructor() {
-    this.targets = {};
-    this.handleEvent = this.handleEvent.bind(this);
-  }
-  makeProxy(data) {
-    const {id} = data;
-    const proxy = new ElementProxyReceiver();
-    this.targets[id] = proxy;
-  }
-  getProxy(id) {
-    return this.targets[id];
-  }
-  handleEvent(data) {
-    this.targets[data.id].handleEvent(data.data);
-  }
+
+	constructor() {
+
+		this.targets = {};
+		this.handleEvent = this.handleEvent.bind( this );
+
+	}
+	makeProxy( data ) {
+
+		const { id } = data;
+		const proxy = new ElementProxyReceiver();
+		this.targets[ id ] = proxy;
+
+	}
+	getProxy( id ) {
+
+		return this.targets[ id ];
+
+	}
+	handleEvent( data ) {
+
+		this.targets[ data.id ].handleEvent( data.data );
+
+	}
+
 }
 
 const proxyManager = new ProxyManager();
 
-function start(data) {
-  const proxy = proxyManager.getProxy(data.canvasId);
-  proxy.ownerDocument = proxy; // HACK!
-  self.document = {};  // HACK!
-  init({
-    canvas: data.canvas,
-    inputElement: proxy,
-  });
+function start( data ) {
+
+	const proxy = proxyManager.getProxy( data.canvasId );
+	proxy.ownerDocument = proxy; // HACK!
+	self.document = {}; // HACK!
+	init( {
+		canvas: data.canvas,
+		inputElement: proxy,
+	} );
+
 }
 
-function makeProxy(data) {
-  proxyManager.makeProxy(data);
+function makeProxy( data ) {
+
+	proxyManager.makeProxy( data );
+
 }
 
 const handlers = {
-  start,
-  makeProxy,
-  event: proxyManager.handleEvent,
+	start,
+	makeProxy,
+	event: proxyManager.handleEvent,
 };
 
-self.onmessage = function(e) {
-  const fn = handlers[e.data.type];
-  if (typeof fn !== 'function') {
-    throw new Error('no handler for type: ' + e.data.type);
-  }
-  fn(e.data);
+self.onmessage = function ( e ) {
+
+	const fn = handlers[ e.data.type ];
+	if ( typeof fn !== 'function' ) {
+
+		throw new Error( 'no handler for type: ' + e.data.type );
+
+	}
+
+	fn( e.data );
+
 };

+ 26 - 17
manual/examples/offscreencanvas-worker-picking.js

@@ -1,25 +1,34 @@
-import {state, init, pickPosition} from './shared-picking.js';
+import { state, init, pickPosition } from './shared-picking.js';
+
+function size( data ) {
+
+	state.width = data.width;
+	state.height = data.height;
 
-function size(data) {
-  state.width = data.width;
-  state.height = data.height;
 }
 
-function mouse(data) {
-  pickPosition.x = data.x;
-  pickPosition.y = data.y;
+function mouse( data ) {
+
+	pickPosition.x = data.x;
+	pickPosition.y = data.y;
+
 }
 
 const handlers = {
-  init,
-  mouse,
-  size,
+	init,
+	mouse,
+	size,
 };
 
-self.onmessage = function(e) {
-  const fn = handlers[e.data.type];
-  if (typeof fn !== 'function') {
-    throw new Error('no handler for type: ' + e.data.type);
-  }
-  fn(e.data);
-};
+self.onmessage = function ( e ) {
+
+	const fn = handlers[ e.data.type ];
+	if ( typeof fn !== 'function' ) {
+
+		throw new Error( 'no handler for type: ' + e.data.type );
+
+	}
+
+	fn( e.data );
+
+};

+ 70 - 49
manual/examples/resources/drag-and-drop.js

@@ -1,9 +1,10 @@
 const handlers = {
-  onDropFile: () => {},
+	onDropFile: () => {},
 };
 
-export function setup(options) {
-  const html = `
+export function setup( options ) {
+
+	const html = `
   <style>
     .dragInfo {
         position: fixed;
@@ -35,55 +36,75 @@ export function setup(options) {
   </div>
   `;
 
-  const elem = document.createElement('div');
-  elem.innerHTML = html;
-  document.body.appendChild(elem);
-
-  const dragInfo = document.querySelector('.dragInfo');
-  function showDragInfo(show) {
-    dragInfo.style.display = show ? '' : 'none';
-  }
-
-  document.body.addEventListener('dragenter', () => {
-    showDragInfo(true);
-  });
-
-  const dragElem = dragInfo;
-
-  dragElem.addEventListener('dragover', (e) => {
-    e.preventDefault();
-    return false;
-  });
-
-  dragElem.addEventListener('dragleave', () => {
-    showDragInfo(false);
-    return false;
-  });
-
-  dragElem.addEventListener('dragend', () => {
-    showDragInfo(false);
-    return false;
-  });
-
-  dragElem.addEventListener('drop', (e) => {
-    e.preventDefault();
-    showDragInfo(false);
-    if (e.dataTransfer.items) {
-      let fileNdx = 0;
-      for (let i = 0; i < e.dataTransfer.items.length; ++i) {
-        const item = e.dataTransfer.items[i];
-        if (item.kind === 'file') {
-          handlers.onDropFile(item.getAsFile(), fileNdx++);
-        }
-      }
-    }
+	const elem = document.createElement( 'div' );
+	elem.innerHTML = html;
+	document.body.appendChild( elem );
+
+	const dragInfo = document.querySelector( '.dragInfo' );
+	function showDragInfo( show ) {
+
+		dragInfo.style.display = show ? '' : 'none';
+
+	}
+
+	document.body.addEventListener( 'dragenter', () => {
+
+		showDragInfo( true );
+
+	} );
+
+	const dragElem = dragInfo;
+
+	dragElem.addEventListener( 'dragover', ( e ) => {
+
+		e.preventDefault();
+		return false;
+
+	} );
+
+	dragElem.addEventListener( 'dragleave', () => {
+
+		showDragInfo( false );
+		return false;
+
+	} );
 
-    return false;
-  });
+	dragElem.addEventListener( 'dragend', () => {
+
+		showDragInfo( false );
+		return false;
+
+	} );
+
+	dragElem.addEventListener( 'drop', ( e ) => {
+
+		e.preventDefault();
+		showDragInfo( false );
+		if ( e.dataTransfer.items ) {
+
+			let fileNdx = 0;
+			for ( let i = 0; i < e.dataTransfer.items.length; ++ i ) {
+
+				const item = e.dataTransfer.items[ i ];
+				if ( item.kind === 'file' ) {
+
+					handlers.onDropFile( item.getAsFile(), fileNdx ++ );
+
+				}
+
+			}
+
+		}
+
+		return false;
+
+	} );
 
 }
 
-export function onDropFile(fn) {
-  handlers.onDropFile = fn;
+export function onDropFile( fn ) {
+
+	handlers.onDropFile = fn;
+
 }
 

+ 213 - 155
manual/examples/resources/editor-settings.js

@@ -1,38 +1,53 @@
 
-(function() {  // eslint-disable-line strict
-'use strict';  // eslint-disable-line strict
-
-function dirname(path) {
-  const ndx = path.lastIndexOf('/');
-  return path.substring(0, ndx + 1);
-}
-
-function getPrefix(url) {
-  const u = new URL(url, window.location.href);
-  const prefix = u.origin + dirname(u.pathname);
-  return prefix;
-}
-
-function getRootPrefix(url) {
-  const u = new URL(url, window.location.href);
-  return u.origin;
-}
-
-function removeDotDotSlash(url) {
-  // assumes a well formed URL. In other words: 'https://..//foo.html" is a bad URL and this code would fail.
-  const parts = url.split('/');
-  for (;;) {
-    const dotDotNdx = parts.indexOf('..');
-    if (dotDotNdx < 0) {
-      break;
-    }
-    parts.splice(dotDotNdx - 1, 2);
-  }
-  const newUrl = parts.join('/');
-  return newUrl;
-}
-
-/**
+( function () { // eslint-disable-line strict
+
+	'use strict'; // eslint-disable-line strict
+
+	function dirname( path ) {
+
+		const ndx = path.lastIndexOf( '/' );
+		return path.substring( 0, ndx + 1 );
+
+	}
+
+	function getPrefix( url ) {
+
+		const u = new URL( url, window.location.href );
+		const prefix = u.origin + dirname( u.pathname );
+		return prefix;
+
+	}
+
+	function getRootPrefix( url ) {
+
+		const u = new URL( url, window.location.href );
+		return u.origin;
+
+	}
+
+	function removeDotDotSlash( url ) {
+
+		// assumes a well formed URL. In other words: 'https://..//foo.html" is a bad URL and this code would fail.
+		const parts = url.split( '/' );
+		for ( ;; ) {
+
+			const dotDotNdx = parts.indexOf( '..' );
+			if ( dotDotNdx < 0 ) {
+
+				break;
+
+			}
+
+			parts.splice( dotDotNdx - 1, 2 );
+
+		}
+
+		const newUrl = parts.join( '/' );
+		return newUrl;
+
+	}
+
+	/**
  * Fix any local URLs into fully qualified urls.
  *
  * Examples:
@@ -49,135 +64,178 @@ function removeDotDotSlash(url) {
  * @param {string} source An HTML file or JavaScript file
  * @returns {string} the source after having urls fixed.
  */
-function fixSourceLinks(url, source) {
-  const srcRE = /(src=)(")(.*?)(")()/g;
-  const linkRE = /(href=)(")(.*?)(")()/g;
-  const imageSrcRE = /((?:image|img)\.src = )(")(.*?)(")()/g;
-  const loaderLoadRE = /(loader\.load[a-z]*\s*\(\s*)('|")(.*?)('|")/ig;
-  const loaderArrayLoadRE = /(loader\.load[a-z]*\(\[)([\s\S]*?)(\])/ig;
-  const loadFileRE = /(loadFile\s*\(\s*)('|")(.*?)('|")/ig;
-  const threejsUrlRE = /(.*?)('|")([^"']*?)('|")([^'"]*?)(\/\*\s+threejs.org:\s+url\s+\*\/)/ig;
-  const arrayLineRE = /^(\s*["|'])([\s\S]*?)(["|']*$)/;
-  const urlPropRE = /(url:\s*)('|")(.*?)('|")/g;
-  const workerRE = /(new\s+Worker\s*\(\s*)('|")(.*?)('|")/g;
-  const importScriptsRE = /(importScripts\s*\(\s*)('|")(.*?)('|")/g;
-  const moduleRE = /(import.*?)('|")(.*?)('|")/g;
-  const prefix = getPrefix(url);
-  const rootPrefix = getRootPrefix(url);
-
-  function addCorrectPrefix(url) {
-    return (url.startsWith('/'))
-       ? `${rootPrefix}${url}`
-       : removeDotDotSlash((prefix + url).replace(/\/.\//g, '/'));
-  }
-
-  function addPrefix(url) {
-    return url.indexOf('://') < 0 && !url.startsWith('data:') && url[0] !== '?'
-        ? removeDotDotSlash(addCorrectPrefix(url))
-        : url;
-  }
-  function makeLinkFDedQuotes(match, fn, q1, url, q2) {
-    return fn + q1 + addPrefix(url) + q2;
-  }
-  function makeTaggedFDedQuotes(match, start, q1, url, q2, suffix) {
-    return start + q1 + addPrefix(url) + q2 + suffix;
-  }
-  function makeFDedQuotesModule(match, start, q1, url, q2) {
-    // modules require relative paths or fully qualified, otherwise they are module names
-    return `${start}${q1}${url.startsWith('.') ? addPrefix(url) : url}${q2}`;
-  }
-  function makeArrayLinksFDed(match, prefix, arrayStr, suffix) {
-    const lines = arrayStr.split(',').map((line) => {
-      const m = arrayLineRE.exec(line);
-      return m
-          ? `${m[1]}${addPrefix(m[2])}${m[3]}`
-          : line;
-    });
-    return `${prefix}${lines.join(',')}${suffix}`;
-  }
-
-  source = source.replace(srcRE, makeTaggedFDedQuotes);
-  source = source.replace(linkRE, makeTaggedFDedQuotes);
-  source = source.replace(imageSrcRE, makeTaggedFDedQuotes);
-  source = source.replace(urlPropRE, makeLinkFDedQuotes);
-  source = source.replace(loadFileRE, makeLinkFDedQuotes);
-  source = source.replace(loaderLoadRE, makeLinkFDedQuotes);
-  source = source.replace(workerRE, makeLinkFDedQuotes);
-  source = source.replace(importScriptsRE, makeLinkFDedQuotes);
-  source = source.replace(loaderArrayLoadRE, makeArrayLinksFDed);
-  source = source.replace(threejsUrlRE, makeTaggedFDedQuotes);
-  source = source.replace(moduleRE, makeFDedQuotesModule);
-
-  return source;
-}
-
-/**
+	function fixSourceLinks( url, source ) {
+
+		const srcRE = /(src=)(")(.*?)(")()/g;
+		const linkRE = /(href=)(")(.*?)(")()/g;
+		const imageSrcRE = /((?:image|img)\.src = )(")(.*?)(")()/g;
+		const loaderLoadRE = /(loader\.load[a-z]*\s*\(\s*)('|")(.*?)('|")/ig;
+		const loaderArrayLoadRE = /(loader\.load[a-z]*\(\[)([\s\S]*?)(\])/ig;
+		const loadFileRE = /(loadFile\s*\(\s*)('|")(.*?)('|")/ig;
+		const threejsUrlRE = /(.*?)('|")([^"']*?)('|")([^'"]*?)(\/\*\s+threejs.org:\s+url\s+\*\/)/ig;
+		const arrayLineRE = /^(\s*["|'])([\s\S]*?)(["|']*$)/;
+		const urlPropRE = /(url:\s*)('|")(.*?)('|")/g;
+		const workerRE = /(new\s+Worker\s*\(\s*)('|")(.*?)('|")/g;
+		const importScriptsRE = /(importScripts\s*\(\s*)('|")(.*?)('|")/g;
+		const moduleRE = /(import.*?)('|")(.*?)('|")/g;
+		const prefix = getPrefix( url );
+		const rootPrefix = getRootPrefix( url );
+
+		function addCorrectPrefix( url ) {
+
+			return ( url.startsWith( '/' ) )
+				? `${rootPrefix}${url}`
+				: removeDotDotSlash( ( prefix + url ).replace( /\/.\//g, '/' ) );
+
+		}
+
+		function addPrefix( url ) {
+
+			return url.indexOf( '://' ) < 0 && ! url.startsWith( 'data:' ) && url[ 0 ] !== '?'
+				? removeDotDotSlash( addCorrectPrefix( url ) )
+				: url;
+
+		}
+
+		function makeLinkFDedQuotes( match, fn, q1, url, q2 ) {
+
+			return fn + q1 + addPrefix( url ) + q2;
+
+		}
+
+		function makeTaggedFDedQuotes( match, start, q1, url, q2, suffix ) {
+
+			return start + q1 + addPrefix( url ) + q2 + suffix;
+
+		}
+
+		function makeFDedQuotesModule( match, start, q1, url, q2 ) {
+
+			// modules require relative paths or fully qualified, otherwise they are module names
+			return `${start}${q1}${url.startsWith( '.' ) ? addPrefix( url ) : url}${q2}`;
+
+		}
+
+		function makeArrayLinksFDed( match, prefix, arrayStr, suffix ) {
+
+			const lines = arrayStr.split( ',' ).map( ( line ) => {
+
+				const m = arrayLineRE.exec( line );
+				return m
+					? `${m[ 1 ]}${addPrefix( m[ 2 ] )}${m[ 3 ]}`
+					: line;
+
+			} );
+			return `${prefix}${lines.join( ',' )}${suffix}`;
+
+		}
+
+		source = source.replace( srcRE, makeTaggedFDedQuotes );
+		source = source.replace( linkRE, makeTaggedFDedQuotes );
+		source = source.replace( imageSrcRE, makeTaggedFDedQuotes );
+		source = source.replace( urlPropRE, makeLinkFDedQuotes );
+		source = source.replace( loadFileRE, makeLinkFDedQuotes );
+		source = source.replace( loaderLoadRE, makeLinkFDedQuotes );
+		source = source.replace( workerRE, makeLinkFDedQuotes );
+		source = source.replace( importScriptsRE, makeLinkFDedQuotes );
+		source = source.replace( loaderArrayLoadRE, makeArrayLinksFDed );
+		source = source.replace( threejsUrlRE, makeTaggedFDedQuotes );
+		source = source.replace( moduleRE, makeFDedQuotesModule );
+
+		return source;
+
+	}
+
+	/**
  * Called after parsing to give a change to update htmlParts
  * @param {string} html The main page html turned into a template with the <style>, <script> and <body> parts extracted
  * @param {Object<string, HTMLPart>} htmlParts All the extracted parts
  * @return {string} The modified html template
  */
-function extraHTMLParsing(html /* , htmlParts */) {
-  return html;
-}
+	function extraHTMLParsing( html /* , htmlParts */ ) {
+
+		return html;
 
-/**
+	}
+
+	/**
  * Change JavaScript before uploading code to JSFiddle/Codepen
  *
  * @param {string} js JavaScript source
  * @returns {string} The JavaScript source with any fixes applied.
  */
-let version;
-async function fixJSForCodeSite(js) {
-  const moduleRE = /(import.*?)('|")(.*?)('|")/g;
-
-  // convert https://threejs.org/build/three.module.js -> https://unpkg.com/three@<version>
-  // convert https://threejs.org/examples/jsm/.?? -> https://unpkg.com/three@<version>/examples/jsm/.??
-
-  if (!version) {
-    try {
-      const res = await fetch('https://raw.githubusercontent.com/mrdoob/three.js/master/package.json');
-      const json = await res.json();
-      version = json.version;
-    } catch (e) {
-      console.error(e);
-    }
-  }
-
-  function addVersion(href) {
-    if (href.startsWith(window.location.origin)) {
-      if (href.includes('/build/three.module.js')) {
-        return `https://unpkg.com/three@${version}`;
-      } else if (href.includes('/examples/jsm/')) {
-        const url = new URL(href);
-        return `https://unpkg.com/three@${version}${url.pathname}${url.search}${url.hash}`;
-      }
-    }
-    return href;
-  }
-
-  function addVersionToURL(match, start, q1, url, q2) {
-    return start + q1 + addVersion(url) + q2;
-  }
-
-  if (version !== undefined) {
-    js = js.replace(moduleRE, addVersionToURL);
-  }
-
-  return js;
-}
-
-window.lessonEditorSettings = {
-  extraHTMLParsing,
-  fixSourceLinks,
-  fixJSForCodeSite,
-  runOnResize: false,
-  lessonSettings: {
-    glDebug: false,
-  },
-  tags: ['three.js'],
-  name: 'three.js',
-  icon: '/files/icon.svg',
-};
-
-}());
+	let version;
+	async function fixJSForCodeSite( js ) {
+
+		const moduleRE = /(import.*?)('|")(.*?)('|")/g;
+
+		// convert https://threejs.org/build/three.module.js -> https://unpkg.com/three@<version>
+		// convert https://threejs.org/examples/jsm/.?? -> https://unpkg.com/three@<version>/examples/jsm/.??
+
+		if ( ! version ) {
+
+			try {
+
+				const res = await fetch( 'https://raw.githubusercontent.com/mrdoob/three.js/master/package.json' );
+				const json = await res.json();
+				version = json.version;
+
+			} catch ( e ) {
+
+				console.error( e );
+
+			}
+
+		}
+
+		function addVersion( href ) {
+
+			if ( href.startsWith( window.location.origin ) ) {
+
+				if ( href.includes( '/build/three.module.js' ) ) {
+
+					return `https://unpkg.com/three@${version}`;
+
+				} else if ( href.includes( '/examples/jsm/' ) ) {
+
+					const url = new URL( href );
+					return `https://unpkg.com/three@${version}${url.pathname}${url.search}${url.hash}`;
+
+				}
+
+			}
+
+			return href;
+
+		}
+
+		function addVersionToURL( match, start, q1, url, q2 ) {
+
+			return start + q1 + addVersion( url ) + q2;
+
+		}
+
+		if ( version !== undefined ) {
+
+			js = js.replace( moduleRE, addVersionToURL );
+
+		}
+
+		return js;
+
+	}
+
+	window.lessonEditorSettings = {
+		extraHTMLParsing,
+		fixSourceLinks,
+		fixJSForCodeSite,
+		runOnResize: false,
+		lessonSettings: {
+			glDebug: false,
+		},
+		tags: [ 'three.js' ],
+		name: 'three.js',
+		icon: '/files/icon.svg',
+	};
+
+}() );

Diff do ficheiro suprimidas por serem muito extensas
+ 1303 - 964
manual/examples/resources/editor.js


+ 726 - 507
manual/examples/resources/lessons-helper.js

@@ -32,51 +32,72 @@
 /* global define */
 
 (function(root, factory) {  // eslint-disable-line
-  if (typeof define === 'function' && define.amd) {
-    // AMD. Register as an anonymous module.
-    define([], function() {
-      return factory.call(root);
-    });
-  } else {
-    // Browser globals
-    root.lessonsHelper = factory.call(root);
-  }
-}(this, function() {
+	if ( typeof define === 'function' && define.amd ) {
+
+		// AMD. Register as an anonymous module.
+		define( [], function () {
+
+			return factory.call( root );
+
+		} );
+
+	} else {
+
+		// Browser globals
+		root.lessonsHelper = factory.call( root );
+
+	}
+
+}( this, function () {
+
   'use strict';  // eslint-disable-line
 
-  const lessonSettings = window.lessonSettings || {};
-  const topWindow = this;
+	const lessonSettings = window.lessonSettings || {};
+	const topWindow = this;
 
-  /**
+	/**
    * Check if the page is embedded.
    * @param {Window?) w window to check
    * @return {boolean} True of we are in an iframe
    */
-  function isInIFrame(w) {
-    w = w || topWindow;
-    return w !== w.top;
-  }
-
-  function updateCSSIfInIFrame() {
-    if (isInIFrame()) {
-      try {
-        document.getElementsByTagName('html')[0].className = 'iframe';
-      } catch (e) {
+	function isInIFrame( w ) {
+
+		w = w || topWindow;
+		return w !== w.top;
+
+	}
+
+	function updateCSSIfInIFrame() {
+
+		if ( isInIFrame() ) {
+
+			try {
+
+				document.getElementsByTagName( 'html' )[ 0 ].className = 'iframe';
+
+			} catch ( e ) {
         // eslint-disable-line
-      }
-      try {
-        document.body.className = 'iframe';
-      } catch (e) {
+			}
+
+			try {
+
+				document.body.className = 'iframe';
+
+			} catch ( e ) {
         // eslint-disable-line
-      }
-    }
-  }
+			}
+
+		}
+
+	}
+
+	function isInEditor() {
 
-  function isInEditor() {
-    return window.location.href.substring(0, 4) === 'blob';
-  }
+		return window.location.href.substring( 0, 4 ) === 'blob';
 
-  /**
+	}
+
+	/**
    * Creates a webgl context. If creation fails it will
    * change the contents of the container of the <canvas>
    * tag to an error message with the correct links for WebGL.
@@ -87,11 +108,13 @@
    * @return {WebGLRenderingContext} The created context.
    * @memberOf module:webgl-utils
    */
-  function showNeedWebGL(canvas) {
-    const doc = canvas.ownerDocument;
-    if (doc) {
-      const temp = doc.createElement('div');
-      temp.innerHTML = `
+	function showNeedWebGL( canvas ) {
+
+		const doc = canvas.ownerDocument;
+		if ( doc ) {
+
+			const temp = doc.createElement( 'div' );
+			temp.innerHTML = `
         <div style="
           position: absolute;
           left: 0;
@@ -111,16 +134,19 @@
           </div>
         </div>
       `;
-      const div = temp.querySelector('div');
-      doc.body.appendChild(div);
-    }
-  }
+			const div = temp.querySelector( 'div' );
+			doc.body.appendChild( div );
 
-  const origConsole = {};
+		}
 
-  function setupConsole() {
-    const style = document.createElement('style');
-    style.innerText = `
+	}
+
+	const origConsole = {};
+
+	function setupConsole() {
+
+		const style = document.createElement( 'style' );
+		style.innerText = `
     .console {
       font-family: monospace;
       font-size: medium;
@@ -142,98 +168,123 @@
       color: red;
     }
     `;
-    const parent = document.createElement('div');
-    parent.className = 'console';
-    const toggle = document.createElement('div');
-    let show = false;
-    Object.assign(toggle.style, {
-      position: 'absolute',
-      right: 0,
-      bottom: 0,
-      background: '#EEE',
-      'font-size': 'smaller',
-      cursor: 'pointer',
-    });
-    toggle.addEventListener('click', showHideConsole);
-
-    function showHideConsole() {
-      show = !show;
-      toggle.textContent = show ? '☒' : '☐';
-      parent.style.display = show ? '' : 'none';
-    }
-    showHideConsole();
-
-    const maxLines = 100;
-    const lines = [];
-    let added = false;
-
-    function addLine(type, str, prefix) {
-      const div = document.createElement('div');
-      div.textContent = (prefix + str) || ' ';
-      div.className = `console-line ${type}`;
-      parent.appendChild(div);
-      lines.push(div);
-      if (!added) {
-        added = true;
-        document.body.appendChild(style);
-        document.body.appendChild(parent);
-        document.body.appendChild(toggle);
-      }
-      // scrollIntoView only works in Chrome
-      // In Firefox and Safari scrollIntoView inside an iframe moves
-      // that element into the view. It should arguably only move that
-      // element inside the iframe itself, otherwise that's giving
-      // any random iframe control to bring itself into view against
-      // the parent's wishes.
-      //
-      // note that even if we used a solution (which is to manually set
-      // scrollTop) there's a UI issue that if the user manually scrolls
-      // we want to stop scrolling automatically and if they move back
-      // to the bottom we want to pick up scrolling automatically.
-      // Kind of a PITA so TBD
-      //
-      // div.scrollIntoView();
-    }
+		const parent = document.createElement( 'div' );
+		parent.className = 'console';
+		const toggle = document.createElement( 'div' );
+		let show = false;
+		Object.assign( toggle.style, {
+			position: 'absolute',
+			right: 0,
+			bottom: 0,
+			background: '#EEE',
+			'font-size': 'smaller',
+			cursor: 'pointer',
+		} );
+		toggle.addEventListener( 'click', showHideConsole );
 
-    function addLines(type, str, prefix) {
-      while (lines.length > maxLines) {
-        const div = lines.shift();
-        div.parentNode.removeChild(div);
-      }
-      addLine(type, str, prefix);
-    }
+		function showHideConsole() {
 
-    const threePukeRE = /WebGLRenderer.*?extension not supported/;
-    function wrapFunc(obj, funcName, prefix) {
-      const oldFn = obj[funcName];
-      origConsole[funcName] = oldFn.bind(obj);
-      return function(...args) {
-        // three.js pukes all over so filter here
-        const src = [...args].join(' ');
-        if (!threePukeRE.test(src)) {
-          addLines(funcName, src, prefix);
-        }
-        oldFn.apply(obj, arguments);
-      };
-    }
+			show = ! show;
+			toggle.textContent = show ? '☒' : '☐';
+			parent.style.display = show ? '' : 'none';
 
-    window.console.log = wrapFunc(window.console, 'log', '');
-    window.console.warn = wrapFunc(window.console, 'warn', '⚠');
-    window.console.error = wrapFunc(window.console, 'error', '❌');
-  }
-
-  function reportJSError(url, lineNo, colNo, msg) {
-    try {
-      const {origUrl, actualLineNo} = window.parent.getActualLineNumberAndMoveTo(url, lineNo, colNo);
-      url = origUrl;
-      lineNo = actualLineNo;
-    } catch (ex) {
-      origConsole.error(ex);
-    }
+		}
+
+		showHideConsole();
+
+		const maxLines = 100;
+		const lines = [];
+		let added = false;
+
+		function addLine( type, str, prefix ) {
+
+			const div = document.createElement( 'div' );
+			div.textContent = ( prefix + str ) || ' ';
+			div.className = `console-line ${type}`;
+			parent.appendChild( div );
+			lines.push( div );
+			if ( ! added ) {
+
+				added = true;
+				document.body.appendChild( style );
+				document.body.appendChild( parent );
+				document.body.appendChild( toggle );
+
+			}
+			// scrollIntoView only works in Chrome
+			// In Firefox and Safari scrollIntoView inside an iframe moves
+			// that element into the view. It should arguably only move that
+			// element inside the iframe itself, otherwise that's giving
+			// any random iframe control to bring itself into view against
+			// the parent's wishes.
+			//
+			// note that even if we used a solution (which is to manually set
+			// scrollTop) there's a UI issue that if the user manually scrolls
+			// we want to stop scrolling automatically and if they move back
+			// to the bottom we want to pick up scrolling automatically.
+			// Kind of a PITA so TBD
+			//
+			// div.scrollIntoView();
+
+		}
+
+		function addLines( type, str, prefix ) {
+
+			while ( lines.length > maxLines ) {
+
+				const div = lines.shift();
+				div.parentNode.removeChild( div );
+
+			}
+
+			addLine( type, str, prefix );
+
+		}
+
+		const threePukeRE = /WebGLRenderer.*?extension not supported/;
+		function wrapFunc( obj, funcName, prefix ) {
+
+			const oldFn = obj[ funcName ];
+			origConsole[ funcName ] = oldFn.bind( obj );
+			return function ( ...args ) {
+
+				// three.js pukes all over so filter here
+				const src = [ ...args ].join( ' ' );
+				if ( ! threePukeRE.test( src ) ) {
+
+					addLines( funcName, src, prefix );
+
+				}
+
+				oldFn.apply( obj, arguments );
+
+			};
+
+		}
+
+		window.console.log = wrapFunc( window.console, 'log', '' );
+		window.console.warn = wrapFunc( window.console, 'warn', '⚠' );
+		window.console.error = wrapFunc( window.console, 'error', '❌' );
+
+	}
+
+	function reportJSError( url, lineNo, colNo, msg ) {
+
+		try {
+
+			const { origUrl, actualLineNo } = window.parent.getActualLineNumberAndMoveTo( url, lineNo, colNo );
+			url = origUrl;
+			lineNo = actualLineNo;
+
+		} catch ( ex ) {
+
+			origConsole.error( ex );
+
+		}
     console.error(url, "line:", lineNo, ":", msg);  // eslint-disable-line
-  }
+	}
 
-  /**
+	/**
    * @typedef {Object} StackInfo
    * @property {string} url Url of line
    * @property {number} lineNo line number of error
@@ -241,401 +292,569 @@
    * @property {string} [funcName] name of function
    */
 
-  /**
+	/**
    * @parameter {string} stack A stack string as in `(new Error()).stack`
    * @returns {StackInfo}
    */
-  const parseStack = function() {
-    const browser = getBrowser();
-    let lineNdx;
-    let matcher;
-    if ((/chrome|opera/i).test(browser.name)) {
-      lineNdx = 3;
-      matcher = function(line) {
-        const m = /at ([^(]*?)\(*(.*?):(\d+):(\d+)/.exec(line);
-        if (m) {
-          let userFnName = m[1];
-          let url = m[2];
-          const lineNo = parseInt(m[3]);
-          const colNo = parseInt(m[4]);
-          if (url === '') {
-            url = userFnName;
-            userFnName = '';
-          }
-          return {
-            url: url,
-            lineNo: lineNo,
-            colNo: colNo,
-            funcName: userFnName,
-          };
-        }
-        return undefined;
-      };
-    } else if ((/firefox|safari/i).test(browser.name)) {
-      lineNdx = 2;
-      matcher = function(line) {
-        const m = /@(.*?):(\d+):(\d+)/.exec(line);
-        if (m) {
-          const url = m[1];
-          const lineNo = parseInt(m[2]);
-          const colNo = parseInt(m[3]);
-          return {
-            url: url,
-            lineNo: lineNo,
-            colNo: colNo,
-          };
-        }
-        return undefined;
-      };
-    }
+	const parseStack = function () {
 
-    return function stackParser(stack) {
-      if (matcher) {
-        try {
-          const lines = stack.split('\n');
-          // window.fooLines = lines;
-          // lines.forEach(function(line, ndx) {
-          //   origConsole.log("#", ndx, line);
-          // });
-          return matcher(lines[lineNdx]);
-        } catch (e) {
-          // do nothing
-        }
-      }
-      return undefined;
-    };
-  }();
-
-  function setupWorkerSupport() {
-    function log(data) {
-      const {logType, msg} = data;
-      console[logType]('[Worker]', msg);  /* eslint-disable-line no-console */
-    }
+		const browser = getBrowser();
+		let lineNdx;
+		let matcher;
+		if ( ( /chrome|opera/i ).test( browser.name ) ) {
 
-    function lostContext(/* data */) {
-      addContextLostHTML();
-    }
+			lineNdx = 3;
+			matcher = function ( line ) {
 
-    function jsError(data) {
-      const {url, lineNo, colNo, msg} = data;
-      reportJSError(url, lineNo, colNo, msg);
-    }
+				const m = /at ([^(]*?)\(*(.*?):(\d+):(\d+)/.exec( line );
+				if ( m ) {
+
+					let userFnName = m[ 1 ];
+					let url = m[ 2 ];
+					const lineNo = parseInt( m[ 3 ] );
+					const colNo = parseInt( m[ 4 ] );
+					if ( url === '' ) {
+
+						url = userFnName;
+						userFnName = '';
+
+					}
+
+					return {
+						url: url,
+						lineNo: lineNo,
+						colNo: colNo,
+						funcName: userFnName,
+					};
+
+				}
+
+				return undefined;
+
+			};
+
+		} else if ( ( /firefox|safari/i ).test( browser.name ) ) {
+
+			lineNdx = 2;
+			matcher = function ( line ) {
+
+				const m = /@(.*?):(\d+):(\d+)/.exec( line );
+				if ( m ) {
+
+					const url = m[ 1 ];
+					const lineNo = parseInt( m[ 2 ] );
+					const colNo = parseInt( m[ 3 ] );
+					return {
+						url: url,
+						lineNo: lineNo,
+						colNo: colNo,
+					};
+
+				}
+
+				return undefined;
+
+			};
+
+		}
+
+		return function stackParser( stack ) {
+
+			if ( matcher ) {
+
+				try {
+
+					const lines = stack.split( '\n' );
+					// window.fooLines = lines;
+					// lines.forEach(function(line, ndx) {
+					//   origConsole.log("#", ndx, line);
+					// });
+					return matcher( lines[ lineNdx ] );
+
+				} catch ( e ) {
+					// do nothing
+				}
+
+			}
+
+			return undefined;
+
+		};
+
+	}();
+
+	function setupWorkerSupport() {
+
+		function log( data ) {
+
+			const { logType, msg } = data;
+			console[ logType ]( '[Worker]', msg ); /* eslint-disable-line no-console */
+
+		}
+
+		function lostContext( /* data */ ) {
+
+			addContextLostHTML();
+
+		}
+
+		function jsError( data ) {
+
+			const { url, lineNo, colNo, msg } = data;
+			reportJSError( url, lineNo, colNo, msg );
+
+		}
+
+		function jsErrorWithStack( data ) {
+
+			const { url, stack, msg } = data;
+			const errorInfo = parseStack( stack );
+			if ( errorInfo ) {
+
+				reportJSError( errorInfo.url || url, errorInfo.lineNo, errorInfo.colNo, msg );
+
+			} else {
 
-    function jsErrorWithStack(data) {
-      const {url, stack, msg} = data;
-      const errorInfo = parseStack(stack);
-      if (errorInfo) {
-        reportJSError(errorInfo.url || url, errorInfo.lineNo, errorInfo.colNo, msg);
-      } else {
         console.error(errorMsg)  // eslint-disable-line
-      }
-    }
+			}
 
-    const handlers = {
-      log,
-      lostContext,
-      jsError,
-      jsErrorWithStack,
-    };
-    const OrigWorker = self.Worker;
-    class WrappedWorker extends OrigWorker {
-      constructor(url, ...args) {
-        super(url, ...args);
-        let listener;
-        this.onmessage = function(e) {
-          if (!e || !e.data || e.data.type !== '___editor___') {
-            if (listener) {
-              listener(e);
-            }
-            return;
-          }
-
-          e.stopImmediatePropagation();
-          const data = e.data.data;
-          const fn = handlers[data.type];
-          if (typeof fn !== 'function') {
-            origConsole.error('unknown editor msg:', data.type);
-          } else {
-            fn(data);
-          }
-          return;
-        };
-        Object.defineProperty(this, 'onmessage', {
-          get() {
-            return listener;
-          },
-          set(fn) {
-            listener = fn;
-          },
-        });
-      }
-    }
-    self.Worker = WrappedWorker;
-  }
-
-  function addContextLostHTML() {
-    const div = document.createElement('div');
-    div.className = 'contextlost';
-    div.innerHTML = '<div>Context Lost: Click To Reload</div>';
-    div.addEventListener('click', function() {
-        window.location.reload();
-    });
-    document.body.appendChild(div);
-  }
-
-  /**
+		}
+
+		const handlers = {
+			log,
+			lostContext,
+			jsError,
+			jsErrorWithStack,
+		};
+		const OrigWorker = self.Worker;
+		class WrappedWorker extends OrigWorker {
+
+			constructor( url, ...args ) {
+
+				super( url, ...args );
+				let listener;
+				this.onmessage = function ( e ) {
+
+					if ( ! e || ! e.data || e.data.type !== '___editor___' ) {
+
+						if ( listener ) {
+
+							listener( e );
+
+						}
+
+						return;
+
+					}
+
+					e.stopImmediatePropagation();
+					const data = e.data.data;
+					const fn = handlers[ data.type ];
+					if ( typeof fn !== 'function' ) {
+
+						origConsole.error( 'unknown editor msg:', data.type );
+
+					} else {
+
+						fn( data );
+
+					}
+
+					return;
+
+				};
+
+				Object.defineProperty( this, 'onmessage', {
+					get() {
+
+						return listener;
+
+					},
+					set( fn ) {
+
+						listener = fn;
+
+					},
+				} );
+
+			}
+
+		}
+		self.Worker = WrappedWorker;
+
+	}
+
+	function addContextLostHTML() {
+
+		const div = document.createElement( 'div' );
+		div.className = 'contextlost';
+		div.innerHTML = '<div>Context Lost: Click To Reload</div>';
+		div.addEventListener( 'click', function () {
+
+			window.location.reload();
+
+		} );
+		document.body.appendChild( div );
+
+	}
+
+	/**
    * Gets a WebGL context.
    * makes its backing store the size it is displayed.
    * @param {HTMLCanvasElement} canvas a canvas element.
    * @memberOf module:webgl-utils
    */
-  let setupLesson = function(canvas) {
-    // only once
-    setupLesson = function() {};
-
-    if (canvas) {
-      canvas.addEventListener('webglcontextlost', function() {
-          // the default is to do nothing. Preventing the default
-          // means allowing context to be restored
-          // e.preventDefault();  // can't do this because firefox bug - https://bugzilla.mozilla.org/show_bug.cgi?id=1633280
-          addContextLostHTML();
-      });
-      /* can't do this because firefox bug - https://bugzilla.mozilla.org/show_bug.cgi?id=1633280
+	let setupLesson = function ( canvas ) {
+
+		// only once
+		setupLesson = function () {};
+
+		if ( canvas ) {
+
+			canvas.addEventListener( 'webglcontextlost', function () {
+
+				// the default is to do nothing. Preventing the default
+				// means allowing context to be restored
+				// e.preventDefault();  // can't do this because firefox bug - https://bugzilla.mozilla.org/show_bug.cgi?id=1633280
+				addContextLostHTML();
+
+			} );
+			/* can't do this because firefox bug - https://bugzilla.mozilla.org/show_bug.cgi?id=1633280
       canvas.addEventListener('webglcontextrestored', function() {
           // just reload the page. Easiest.
           window.location.reload();
       });
       */
-    }
 
-    if (isInIFrame()) {
-      updateCSSIfInIFrame();
-    }
-  };
-
-  // Replace requestAnimationFrame and cancelAnimationFrame with one
-  // that only executes when the body is visible (we're in an iframe).
-  // It's frustrating that th browsers don't do this automatically.
-  // It's half of the point of rAF that it shouldn't execute when
-  // content is not visible but browsers execute rAF in iframes even
-  // if they are not visible.
-  if (topWindow.requestAnimationFrame) {
-    topWindow.requestAnimationFrame = (function(oldRAF, oldCancelRAF) {
-      let nextFakeRAFId = 1;
-      const fakeRAFIdToCallbackMap = new Map();
-      let rafRequestId;
-      let isBodyOnScreen;
-
-      function rAFHandler(time) {
-        rafRequestId = undefined;
-        const ids = [...fakeRAFIdToCallbackMap.keys()];  // WTF! Map.keys() iterates over live keys!
-        for (const id of ids) {
-          const callback = fakeRAFIdToCallbackMap.get(id);
-          fakeRAFIdToCallbackMap.delete(id);
-          if (callback) {
-            callback(time);
-          }
-        }
-      }
-
-      function startRAFIfIntersectingAndNeeded() {
-        if (!rafRequestId && isBodyOnScreen && fakeRAFIdToCallbackMap.size > 0) {
-          rafRequestId = oldRAF(rAFHandler);
-        }
-      }
-
-      function stopRAF() {
-        if (rafRequestId) {
-          oldCancelRAF(rafRequestId);
-          rafRequestId = undefined;
-        }
-      }
-
-      function initIntersectionObserver() {
-        const intersectionObserver = new IntersectionObserver((entries) => {
-          entries.forEach(entry => {
-            isBodyOnScreen = entry.isIntersecting;
-          });
-          if (isBodyOnScreen) {
-            startRAFIfIntersectingAndNeeded();
-          } else {
-            stopRAF();
-          }
-        });
-        intersectionObserver.observe(document.body);
-      }
-
-      function betterRAF(callback) {
-        const fakeRAFId = nextFakeRAFId++;
-        fakeRAFIdToCallbackMap.set(fakeRAFId, callback);
-        startRAFIfIntersectingAndNeeded();
-        return fakeRAFId;
-      }
-
-      function betterCancelRAF(id) {
-        fakeRAFIdToCallbackMap.delete(id);
-      }
-
-      topWindow.cancelAnimationFrame = betterCancelRAF;
-
-      return function(callback) {
-        // we need to lazy init this because this code gets parsed
-        // before body exists. We could fix it by moving lesson-helper.js
-        // after <body> but that would require changing 100s of examples
-        initIntersectionObserver();
-        topWindow.requestAnimationFrame = betterRAF;
-        return betterRAF(callback);
-      };
-
-    }(topWindow.requestAnimationFrame, topWindow.cancelAnimationFrame));
-  }
-
-  updateCSSIfInIFrame();
-
-  function captureJSErrors() {
-    // capture JavaScript Errors
-    window.addEventListener('error', function(e) {
-      const msg = e.message || e.error;
-      const url = e.filename;
-      const lineNo = e.lineno || 1;
-      const colNo = e.colno || 1;
-      reportJSError(url, lineNo, colNo, msg);
-      origConsole.error(e.error);
-    });
-  }
-
-  // adapted from http://stackoverflow.com/a/2401861/128511
-  function getBrowser() {
-    const userAgent = navigator.userAgent;
-    let m = userAgent.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [];
-    if (/trident/i.test(m[1])) {
-      m = /\brv[ :]+(\d+)/g.exec(userAgent) || [];
-      return {
-        name: 'IE',
-        version: m[1],
-      };
-    }
-    if (m[1] === 'Chrome') {
-      const temp = userAgent.match(/\b(OPR|Edge)\/(\d+)/);
-      if (temp) {
-        return {
-          name: temp[1].replace('OPR', 'Opera'),
-          version: temp[2],
-        };
-      }
-    }
-    m = m[2] ? [m[1], m[2]] : [navigator.appName, navigator.appVersion, '-?'];
-    const version = userAgent.match(/version\/(\d+)/i);
-    if (version) {
-      m.splice(1, 1, version[1]);
-    }
-    return {
-      name: m[0],
-      version: m[1],
-    };
-  }
-
-  const canvasesToTimeoutMap = new Map();
-  const isWebGLRE = /^(webgl|webgl2|experimental-webgl)$/i;
-  const isWebGL2RE = /^webgl2$/i;
-  function installWebGLLessonSetup() {
-    HTMLCanvasElement.prototype.getContext = (function(oldFn) {
-      return function() {
-        const timeoutId = canvasesToTimeoutMap.get(this);
-        if (timeoutId) {
-          clearTimeout(timeoutId);
-        }
-        const type = arguments[0];
-        const isWebGL1or2 = isWebGLRE.test(type);
-        const isWebGL2 = isWebGL2RE.test(type);
-        if (isWebGL1or2) {
-          setupLesson(this);
-        }
-        const args = [].slice.apply(arguments);
-        args[1] = {
-          powerPreference: 'low-power',
-          ...args[1],
-        };
-        const ctx = oldFn.apply(this, args);
-        if (!ctx) {
-          if (isWebGL2) {
-            // three tries webgl2 then webgl1
-            // so wait 1/2 a second before showing the failure
-            // message. If we get success on the same canvas
-            // we'll cancel this.
-            canvasesToTimeoutMap.set(this, setTimeout(() => {
-              canvasesToTimeoutMap.delete(this);
-              showNeedWebGL(this);
-            }, 500));
-          } else {
-            showNeedWebGL(this);
-          }
-        }
-        return ctx;
-      };
-    }(HTMLCanvasElement.prototype.getContext));
-  }
-
-  function installWebGLDebugContextCreator() {
-    if (!self.webglDebugHelper) {
-      return;
-    }
+		}
+
+		if ( isInIFrame() ) {
+
+			updateCSSIfInIFrame();
+
+		}
+
+	};
+
+	// Replace requestAnimationFrame and cancelAnimationFrame with one
+	// that only executes when the body is visible (we're in an iframe).
+	// It's frustrating that th browsers don't do this automatically.
+	// It's half of the point of rAF that it shouldn't execute when
+	// content is not visible but browsers execute rAF in iframes even
+	// if they are not visible.
+	if ( topWindow.requestAnimationFrame ) {
+
+		topWindow.requestAnimationFrame = ( function ( oldRAF, oldCancelRAF ) {
+
+			let nextFakeRAFId = 1;
+			const fakeRAFIdToCallbackMap = new Map();
+			let rafRequestId;
+			let isBodyOnScreen;
+
+			function rAFHandler( time ) {
+
+				rafRequestId = undefined;
+				const ids = [ ...fakeRAFIdToCallbackMap.keys() ]; // WTF! Map.keys() iterates over live keys!
+				for ( const id of ids ) {
+
+					const callback = fakeRAFIdToCallbackMap.get( id );
+					fakeRAFIdToCallbackMap.delete( id );
+					if ( callback ) {
+
+						callback( time );
+
+					}
+
+				}
+
+			}
+
+			function startRAFIfIntersectingAndNeeded() {
+
+				if ( ! rafRequestId && isBodyOnScreen && fakeRAFIdToCallbackMap.size > 0 ) {
+
+					rafRequestId = oldRAF( rAFHandler );
+
+				}
+
+			}
+
+			function stopRAF() {
+
+				if ( rafRequestId ) {
+
+					oldCancelRAF( rafRequestId );
+					rafRequestId = undefined;
+
+				}
+
+			}
+
+			function initIntersectionObserver() {
+
+				const intersectionObserver = new IntersectionObserver( ( entries ) => {
+
+					entries.forEach( entry => {
+
+						isBodyOnScreen = entry.isIntersecting;
+
+					} );
+					if ( isBodyOnScreen ) {
+
+						startRAFIfIntersectingAndNeeded();
+
+					} else {
+
+						stopRAF();
+
+					}
+
+				} );
+				intersectionObserver.observe( document.body );
+
+			}
+
+			function betterRAF( callback ) {
+
+				const fakeRAFId = nextFakeRAFId ++;
+				fakeRAFIdToCallbackMap.set( fakeRAFId, callback );
+				startRAFIfIntersectingAndNeeded();
+				return fakeRAFId;
+
+			}
+
+			function betterCancelRAF( id ) {
+
+				fakeRAFIdToCallbackMap.delete( id );
+
+			}
+
+			topWindow.cancelAnimationFrame = betterCancelRAF;
+
+			return function ( callback ) {
+
+				// we need to lazy init this because this code gets parsed
+				// before body exists. We could fix it by moving lesson-helper.js
+				// after <body> but that would require changing 100s of examples
+				initIntersectionObserver();
+				topWindow.requestAnimationFrame = betterRAF;
+				return betterRAF( callback );
+
+			};
+
+		}( topWindow.requestAnimationFrame, topWindow.cancelAnimationFrame ) );
+
+	}
+
+	updateCSSIfInIFrame();
+
+	function captureJSErrors() {
+
+		// capture JavaScript Errors
+		window.addEventListener( 'error', function ( e ) {
+
+			const msg = e.message || e.error;
+			const url = e.filename;
+			const lineNo = e.lineno || 1;
+			const colNo = e.colno || 1;
+			reportJSError( url, lineNo, colNo, msg );
+			origConsole.error( e.error );
+
+		} );
+
+	}
+
+	// adapted from http://stackoverflow.com/a/2401861/128511
+	function getBrowser() {
+
+		const userAgent = navigator.userAgent;
+		let m = userAgent.match( /(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i ) || [];
+		if ( /trident/i.test( m[ 1 ] ) ) {
+
+			m = /\brv[ :]+(\d+)/g.exec( userAgent ) || [];
+			return {
+				name: 'IE',
+				version: m[ 1 ],
+			};
+
+		}
+
+		if ( m[ 1 ] === 'Chrome' ) {
+
+			const temp = userAgent.match( /\b(OPR|Edge)\/(\d+)/ );
+			if ( temp ) {
+
+				return {
+					name: temp[ 1 ].replace( 'OPR', 'Opera' ),
+					version: temp[ 2 ],
+				};
+
+			}
+
+		}
+
+		m = m[ 2 ] ? [ m[ 1 ], m[ 2 ] ] : [ navigator.appName, navigator.appVersion, '-?' ];
+		const version = userAgent.match( /version\/(\d+)/i );
+		if ( version ) {
+
+			m.splice( 1, 1, version[ 1 ] );
+
+		}
+
+		return {
+			name: m[ 0 ],
+			version: m[ 1 ],
+		};
+
+	}
+
+	const canvasesToTimeoutMap = new Map();
+	const isWebGLRE = /^(webgl|webgl2|experimental-webgl)$/i;
+	const isWebGL2RE = /^webgl2$/i;
+	function installWebGLLessonSetup() {
+
+		HTMLCanvasElement.prototype.getContext = ( function ( oldFn ) {
+
+			return function () {
+
+				const timeoutId = canvasesToTimeoutMap.get( this );
+				if ( timeoutId ) {
+
+					clearTimeout( timeoutId );
+
+				}
+
+				const type = arguments[ 0 ];
+				const isWebGL1or2 = isWebGLRE.test( type );
+				const isWebGL2 = isWebGL2RE.test( type );
+				if ( isWebGL1or2 ) {
+
+					setupLesson( this );
+
+				}
+
+				const args = [].slice.apply( arguments );
+				args[ 1 ] = {
+					powerPreference: 'low-power',
+					...args[ 1 ],
+				};
+				const ctx = oldFn.apply( this, args );
+				if ( ! ctx ) {
+
+					if ( isWebGL2 ) {
+
+						// three tries webgl2 then webgl1
+						// so wait 1/2 a second before showing the failure
+						// message. If we get success on the same canvas
+						// we'll cancel this.
+						canvasesToTimeoutMap.set( this, setTimeout( () => {
+
+							canvasesToTimeoutMap.delete( this );
+							showNeedWebGL( this );
+
+						}, 500 ) );
+
+					} else {
+
+						showNeedWebGL( this );
+
+					}
+
+				}
+
+				return ctx;
+
+			};
+
+		}( HTMLCanvasElement.prototype.getContext ) );
+
+	}
+
+	function installWebGLDebugContextCreator() {
+
+		if ( ! self.webglDebugHelper ) {
+
+			return;
+
+		}
+
+		const {
+			makeDebugContext,
+			glFunctionArgToString,
+			glEnumToString,
+		} = self.webglDebugHelper;
+
+		// capture GL errors
+		HTMLCanvasElement.prototype.getContext = ( function ( oldFn ) {
+
+			return function () {
+
+				let ctx = oldFn.apply( this, arguments );
+				// Using bindTexture to see if it's WebGL. Could check for instanceof WebGLRenderingContext
+				// but that might fail if wrapped by debugging extension
+				if ( ctx && ctx.bindTexture ) {
+
+					ctx = makeDebugContext( ctx, {
+						maxDrawCalls: 100,
+						errorFunc: function ( err, funcName, args ) {
+
+							const numArgs = args.length;
+							const enumedArgs = [].map.call( args, function ( arg, ndx ) {
+
+								let str = glFunctionArgToString( funcName, numArgs, ndx, arg );
+								// shorten because of long arrays
+								if ( str.length > 200 ) {
+
+									str = str.substring( 0, 200 ) + '...';
+
+								}
+
+								return str;
+
+							} );
+							const errorMsg = `WebGL error ${glEnumToString( err )} in ${funcName}(${enumedArgs.join( ', ' )})`;
+							const errorInfo = parseStack( ( new Error() ).stack );
+							if ( errorInfo ) {
+
+								reportJSError( errorInfo.url, errorInfo.lineNo, errorInfo.colNo, errorMsg );
+
+							} else {
 
-    const {
-      makeDebugContext,
-      glFunctionArgToString,
-      glEnumToString,
-    } = self.webglDebugHelper;
-
-    // capture GL errors
-    HTMLCanvasElement.prototype.getContext = (function(oldFn) {
-      return function() {
-        let ctx = oldFn.apply(this, arguments);
-        // Using bindTexture to see if it's WebGL. Could check for instanceof WebGLRenderingContext
-        // but that might fail if wrapped by debugging extension
-        if (ctx && ctx.bindTexture) {
-          ctx = makeDebugContext(ctx, {
-            maxDrawCalls: 100,
-            errorFunc: function(err, funcName, args) {
-              const numArgs = args.length;
-              const enumedArgs = [].map.call(args, function(arg, ndx) {
-                let str = glFunctionArgToString(funcName, numArgs, ndx, arg);
-                // shorten because of long arrays
-                if (str.length > 200) {
-                  str = str.substring(0, 200) + '...';
-                }
-                return str;
-              });
-              const errorMsg = `WebGL error ${glEnumToString(err)} in ${funcName}(${enumedArgs.join(', ')})`;
-              const errorInfo = parseStack((new Error()).stack);
-              if (errorInfo) {
-                reportJSError(errorInfo.url, errorInfo.lineNo, errorInfo.colNo, errorMsg);
-              } else {
                 console.error(errorMsg)  // eslint-disable-line
-              }
-            },
-          });
-        }
-        return ctx;
-      };
-    }(HTMLCanvasElement.prototype.getContext));
-  }
-
-  installWebGLLessonSetup();
-
-  if (isInEditor()) {
-    setupWorkerSupport();
-    setupConsole();
-    captureJSErrors();
-    if (lessonSettings.glDebug !== false) {
-      installWebGLDebugContextCreator();
-    }
-  }
+							}
+
+						},
+					} );
+
+				}
+
+				return ctx;
+
+			};
+
+		}( HTMLCanvasElement.prototype.getContext ) );
+
+	}
+
+	installWebGLLessonSetup();
+
+	if ( isInEditor() ) {
+
+		setupWorkerSupport();
+		setupConsole();
+		captureJSErrors();
+		if ( lessonSettings.glDebug !== false ) {
+
+			installWebGLDebugContextCreator();
+
+		}
+
+	}
 
-  return {
-    setupLesson: setupLesson,
-    showNeedWebGL: showNeedWebGL,
-  };
+	return {
+		setupLesson: setupLesson,
+		showNeedWebGL: showNeedWebGL,
+	};
 
-}));
+} ) );
 

+ 201 - 148
manual/examples/resources/lessons-worker-helper.js

@@ -33,157 +33,210 @@
 
 'use strict';  // eslint-disable-line
 
-(function() {
-
-  const lessonSettings = self.lessonSettings || {};
-
-  function isInEditor() {
-    return self.location.href.substring(0, 4) === 'blob';
-  }
-
-  function sendMessage(data) {
-    self.postMessage({
-      type: '__editor__',
-      data,
-    });
-  }
-
-  const origConsole = {};
-
-  function setupConsole() {
-    function wrapFunc(obj, logType) {
-      const origFunc = obj[logType].bind(obj);
-      origConsole[logType] = origFunc;
-      return function(...args) {
-        origFunc(...args);
-        sendMessage({
-          type: 'log',
-          logType,
-          msg: [...args].join(' '),
-        });
-      };
-    }
-    self.console.log = wrapFunc(self.console, 'log');
-    self.console.warn = wrapFunc(self.console, 'warn');
-    self.console.error = wrapFunc(self.console, 'error');
-  }
-
-  /**
+( function () {
+
+	const lessonSettings = self.lessonSettings || {};
+
+	function isInEditor() {
+
+		return self.location.href.substring( 0, 4 ) === 'blob';
+
+	}
+
+	function sendMessage( data ) {
+
+		self.postMessage( {
+			type: '__editor__',
+			data,
+		} );
+
+	}
+
+	const origConsole = {};
+
+	function setupConsole() {
+
+		function wrapFunc( obj, logType ) {
+
+			const origFunc = obj[ logType ].bind( obj );
+			origConsole[ logType ] = origFunc;
+			return function ( ...args ) {
+
+				origFunc( ...args );
+				sendMessage( {
+					type: 'log',
+					logType,
+					msg: [ ...args ].join( ' ' ),
+				} );
+
+			};
+
+		}
+
+		self.console.log = wrapFunc( self.console, 'log' );
+		self.console.warn = wrapFunc( self.console, 'warn' );
+		self.console.error = wrapFunc( self.console, 'error' );
+
+	}
+
+	/**
    * Gets a WebGL context.
    * makes its backing store the size it is displayed.
    * @param {OffscreenCanvas} canvas a canvas element.
    * @memberOf module:webgl-utils
    */
-  let setupLesson = function(canvas) {
-    // only once
-    setupLesson = function() {};
-
-    if (canvas) {
-      canvas.addEventListener('webglcontextlost', function() {
-          // the default is to do nothing. Preventing the default
-          // means allowing context to be restored
-          // e.preventDefault();  // can't do this because firefox bug - https://bugzilla.mozilla.org/show_bug.cgi?id=1633280
-          sendMessage({
-            type: 'lostContext',
-          });
-      });
-    }
-
-  };
-
-  function captureJSErrors() {
-    // capture JavaScript Errors
-    self.addEventListener('error', function(e) {
-      const msg = e.message || e.error;
-      const url = e.filename;
-      const lineNo = e.lineno || 1;
-      const colNo = e.colno || 1;
-      sendMessage({
-        type: 'jsError',
-        lineNo,
-        colNo,
-        url,
-        msg,
-      });
-    });
-  }
-
-
-  const isWebGLRE = /^(webgl|webgl2|experimental-webgl)$/i;
-  function installWebGLLessonSetup() {
-    OffscreenCanvas.prototype.getContext = (function(oldFn) {
-      return function() {
-        const type = arguments[0];
-        const isWebGL = isWebGLRE.test(type);
-        if (isWebGL) {
-          setupLesson(this);
-        }
-        const args = [].slice.apply(arguments);
-        args[1] = {
-          powerPreference: 'low-power',
-          ...args[1],
-        };
-        return oldFn.apply(this, args);
-      };
-    }(OffscreenCanvas.prototype.getContext));
-  }
-
-  function installWebGLDebugContextCreator() {
-    if (!self.webglDebugHelper) {
-      return;
-    }
-
-    const {
-      makeDebugContext,
-      glFunctionArgToString,
-      glEnumToString,
-    } = self.webglDebugHelper;
-
-    // capture GL errors
-    OffscreenCanvas.prototype.getContext = (function(oldFn) {
-      return function() {
-        let ctx = oldFn.apply(this, arguments);
-        // Using bindTexture to see if it's WebGL. Could check for instanceof WebGLRenderingContext
-        // but that might fail if wrapped by debugging extension
-        if (ctx && ctx.bindTexture) {
-          ctx = makeDebugContext(ctx, {
-            maxDrawCalls: 100,
-            errorFunc: function(err, funcName, args) {
-              const numArgs = args.length;
-              const enumedArgs = [].map.call(args, function(arg, ndx) {
-                let str = glFunctionArgToString(funcName, numArgs, ndx, arg);
-                // shorten because of long arrays
-                if (str.length > 200) {
-                  str = str.substring(0, 200) + '...';
-                }
-                return str;
-              });
-
-              {
-                const error = new Error();
-                sendMessage({
-                  type: 'jsErrorWithStack',
-                  stack: error.stack,
-                  msg: `${glEnumToString(err)} in ${funcName}(${enumedArgs.join(', ')})`,
-                });
-              }
-            },
-          });
-        }
-        return ctx;
-      };
-    }(OffscreenCanvas.prototype.getContext));
-  }
-
-  installWebGLLessonSetup();
-
-  if (isInEditor()) {
-    setupConsole();
-    captureJSErrors();
-    if (lessonSettings.glDebug !== false) {
-      installWebGLDebugContextCreator();
-    }
-  }
-
-}());
+	let setupLesson = function ( canvas ) {
+
+		// only once
+		setupLesson = function () {};
+
+		if ( canvas ) {
+
+			canvas.addEventListener( 'webglcontextlost', function () {
+
+				// the default is to do nothing. Preventing the default
+				// means allowing context to be restored
+				// e.preventDefault();  // can't do this because firefox bug - https://bugzilla.mozilla.org/show_bug.cgi?id=1633280
+				sendMessage( {
+					type: 'lostContext',
+				} );
+
+			} );
+
+		}
+
+	};
+
+	function captureJSErrors() {
+
+		// capture JavaScript Errors
+		self.addEventListener( 'error', function ( e ) {
+
+			const msg = e.message || e.error;
+			const url = e.filename;
+			const lineNo = e.lineno || 1;
+			const colNo = e.colno || 1;
+			sendMessage( {
+				type: 'jsError',
+				lineNo,
+				colNo,
+				url,
+				msg,
+			} );
+
+		} );
+
+	}
+
+
+	const isWebGLRE = /^(webgl|webgl2|experimental-webgl)$/i;
+	function installWebGLLessonSetup() {
+
+		OffscreenCanvas.prototype.getContext = ( function ( oldFn ) { // eslint-disable-line compat/compat
+
+			return function () {
+
+				const type = arguments[ 0 ];
+				const isWebGL = isWebGLRE.test( type );
+				if ( isWebGL ) {
+
+					setupLesson( this );
+
+				}
+
+				const args = [].slice.apply( arguments );
+				args[ 1 ] = {
+					powerPreference: 'low-power',
+					...args[ 1 ],
+				};
+				return oldFn.apply( this, args );
+
+			};
+
+		}( OffscreenCanvas.prototype.getContext ) ); // eslint-disable-line compat/compat
+
+	}
+
+	function installWebGLDebugContextCreator() {
+
+		if ( ! self.webglDebugHelper ) {
+
+			return;
+
+		}
+
+		const {
+			makeDebugContext,
+			glFunctionArgToString,
+			glEnumToString,
+		} = self.webglDebugHelper;
+
+		// capture GL errors
+		OffscreenCanvas.prototype.getContext = ( function ( oldFn ) { // eslint-disable-line compat/compat
+
+			return function () {
+
+				let ctx = oldFn.apply( this, arguments );
+				// Using bindTexture to see if it's WebGL. Could check for instanceof WebGLRenderingContext
+				// but that might fail if wrapped by debugging extension
+				if ( ctx && ctx.bindTexture ) {
+
+					ctx = makeDebugContext( ctx, {
+						maxDrawCalls: 100,
+						errorFunc: function ( err, funcName, args ) {
+
+							const numArgs = args.length;
+							const enumedArgs = [].map.call( args, function ( arg, ndx ) {
+
+								let str = glFunctionArgToString( funcName, numArgs, ndx, arg );
+								// shorten because of long arrays
+								if ( str.length > 200 ) {
+
+									str = str.substring( 0, 200 ) + '...';
+
+								}
+
+								return str;
+
+							} );
+
+							{
+
+								const error = new Error();
+								sendMessage( {
+									type: 'jsErrorWithStack',
+									stack: error.stack,
+									msg: `${glEnumToString( err )} in ${funcName}(${enumedArgs.join( ', ' )})`,
+								} );
+
+							}
+
+						},
+					} );
+
+				}
+
+				return ctx;
+
+			};
+
+		}( OffscreenCanvas.prototype.getContext ) ); // eslint-disable-line compat/compat
+
+	}
+
+	installWebGLLessonSetup();
+
+	if ( isInEditor() ) {
+
+		setupConsole();
+		captureJSErrors();
+		if ( lessonSettings.glDebug !== false ) {
+
+			installWebGLDebugContextCreator();
+
+		}
+
+	}
+
+}() );
 

+ 328 - 221
manual/examples/resources/lut-reader.js

@@ -1,244 +1,351 @@
-function splitOnSpaceHandleQuotesWithEscapes(str, splits = ' \t\n\r') {
-  const strings = [];
-  let quoteType;
-  let escape;
-  let s = [];
-  for (let i = 0; i < str.length; ++i) {
-    const c = str[i];
-    if (escape) {
-      escape = false;
-      s.push(c);
-    } else {
-      if (quoteType) {  // we're inside quotes
-        if (c === quoteType) {
-          quoteType = undefined;
-          strings.push(s.join(''));
-          s = [];
-        } else if (c === '\\') {
-          escape = true;
-        } else {
-          s.push(c);
-        }
-      } else {  // we're not in quotes
-        if (splits.indexOf(c) >= 0) {
-          if (s.length) {
-            strings.push(s.join(''));
-            s = [];
-          }
-        } else if (c === '"' || c === '\'') {
-          if (s.length) {  // its in th middle of a word
-            s.push(c);
-          } else {
-            quoteType = c;
-          }
-        } else {
-          s.push(c);
-        }
-      }
-    }
-  }
-  if (s.length || strings.length === 0) {
-    strings.push(s.join(''));
-  }
-  return strings;
+function splitOnSpaceHandleQuotesWithEscapes( str, splits = ' \t\n\r' ) {
+
+	const strings = [];
+	let quoteType;
+	let escape;
+	let s = [];
+	for ( let i = 0; i < str.length; ++ i ) {
+
+		const c = str[ i ];
+		if ( escape ) {
+
+			escape = false;
+			s.push( c );
+
+		} else {
+
+			if ( quoteType ) { // we're inside quotes
+
+				if ( c === quoteType ) {
+
+					quoteType = undefined;
+					strings.push( s.join( '' ) );
+					s = [];
+
+				} else if ( c === '\\' ) {
+
+					escape = true;
+
+				} else {
+
+					s.push( c );
+
+				}
+
+			} else { // we're not in quotes
+
+				if ( splits.indexOf( c ) >= 0 ) {
+
+					if ( s.length ) {
+
+						strings.push( s.join( '' ) );
+						s = [];
+
+					}
+
+				} else if ( c === '"' || c === '\'' ) {
+
+					if ( s.length ) { // its in th middle of a word
+
+						s.push( c );
+
+					} else {
+
+						quoteType = c;
+
+					}
+
+				} else {
+
+					s.push( c );
+
+				}
+
+			}
+
+		}
+
+	}
+
+	if ( s.length || strings.length === 0 ) {
+
+		strings.push( s.join( '' ) );
+
+	}
+
+	return strings;
+
 }
 
 const startWhitespaceRE = /^\s/;
 const intRE = /^\d+$/;
-const isNum = s => intRE.test(s);
+const isNum = s => intRE.test( s );
 
 const quotesRE = /^".*"$/;
-function trimQuotes(s) {
-  return quotesRE.test(s) ? s.slice(1, -1) : s;
+function trimQuotes( s ) {
+
+	return quotesRE.test( s ) ? s.slice( 1, - 1 ) : s;
+
 }
 
-const splitToNumbers = s => s.split(' ').map(parseFloat);
-
-export function parseCSP(str) {
-  const data = [];
-  const lut = {
-    name: 'unknown',
-    type: '1D',
-    size: 0,
-    data,
-    min: [0, 0, 0],
-    max: [1, 1, 1],
-  };
-
-  const lines = str.split('\n').map(s => s.trim()).filter(s => s.length > 0 && !startWhitespaceRE.test(s));
-
-  // check header
-  lut.type = lines[1];
-  if (lines[0] !== 'CSPLUTV100' ||
-       (lut.type !== '1D' && lut.type !== '3D')) {
-    throw new Error('not CSP');
-  }
-
-  // skip meta (read to first number)
-  let lineNdx = 2;
-  for (; lineNdx < lines.length; ++lineNdx) {
-    const line = lines[lineNdx];
-    if (isNum(line)) {
-      break;
-    }
-    if (line.startsWith('TITLE ')) {
-      lut.name = trimQuotes(line.slice(6).trim());
-    }
-  }
-
-  // read ranges
-  for (let i = 0; i < 3; ++i) {
-    ++lineNdx;
-    const input = splitToNumbers(lines[lineNdx++]);
-    const output = splitToNumbers(lines[lineNdx++]);
-    if (input.length !== 2 || output.length !== 2 ||
-        input[0] !== 0 || input[1] !==  1 ||
-        output[0] !== 0 || output[1] !== 1) {
-      throw new Error('mapped ranges not support');
-    }
-  }
-
-  // read sizes
-  const sizes = splitToNumbers(lines[lineNdx++]);
-  if (sizes[0] !== sizes[1] || sizes[0] !== sizes[2]) {
-    throw new Error('only cubic sizes supported');
-  }
-  lut.size = sizes[0];
-
-  // read data
-  for (; lineNdx < lines.length; ++lineNdx) {
-    const parts = splitToNumbers(lines[lineNdx]);
-    if (parts.length !== 3) {
-      throw new Error('malformed file');
-    }
-    data.push(...parts);
-  }
-
-  return lut;
+const splitToNumbers = s => s.split( ' ' ).map( parseFloat );
+
+export function parseCSP( str ) {
+
+	const data = [];
+	const lut = {
+		name: 'unknown',
+		type: '1D',
+		size: 0,
+		data,
+		min: [ 0, 0, 0 ],
+		max: [ 1, 1, 1 ],
+	};
+
+	const lines = str.split( '\n' ).map( s => s.trim() ).filter( s => s.length > 0 && ! startWhitespaceRE.test( s ) );
+
+	// check header
+	lut.type = lines[ 1 ];
+	if ( lines[ 0 ] !== 'CSPLUTV100' ||
+       ( lut.type !== '1D' && lut.type !== '3D' ) ) {
+
+		throw new Error( 'not CSP' );
+
+	}
+
+	// skip meta (read to first number)
+	let lineNdx = 2;
+	for ( ; lineNdx < lines.length; ++ lineNdx ) {
+
+		const line = lines[ lineNdx ];
+		if ( isNum( line ) ) {
+
+			break;
+
+		}
+
+		if ( line.startsWith( 'TITLE ' ) ) {
+
+			lut.name = trimQuotes( line.slice( 6 ).trim() );
+
+		}
+
+	}
+
+	// read ranges
+	for ( let i = 0; i < 3; ++ i ) {
+
+		++ lineNdx;
+		const input = splitToNumbers( lines[ lineNdx ++ ] );
+		const output = splitToNumbers( lines[ lineNdx ++ ] );
+		if ( input.length !== 2 || output.length !== 2 ||
+        input[ 0 ] !== 0 || input[ 1 ] !== 1 ||
+        output[ 0 ] !== 0 || output[ 1 ] !== 1 ) {
+
+			throw new Error( 'mapped ranges not support' );
+
+		}
+
+	}
+
+	// read sizes
+	const sizes = splitToNumbers( lines[ lineNdx ++ ] );
+	if ( sizes[ 0 ] !== sizes[ 1 ] || sizes[ 0 ] !== sizes[ 2 ] ) {
+
+		throw new Error( 'only cubic sizes supported' );
+
+	}
+
+	lut.size = sizes[ 0 ];
+
+	// read data
+	for ( ; lineNdx < lines.length; ++ lineNdx ) {
+
+		const parts = splitToNumbers( lines[ lineNdx ] );
+		if ( parts.length !== 3 ) {
+
+			throw new Error( 'malformed file' );
+
+		}
+
+		data.push( ...parts );
+
+	}
+
+	return lut;
+
 }
 
-export function parseCUBE(str) {
-  const data = [];
-  const lut = {
-    name: 'unknown',
-    type: '1D',
-    size: 0,
-    data,
-    min: [0, 0, 0],
-    max: [1, 1, 1],
-  };
-
-  const lines = str.split('\n');
-  for (const origLine of lines) {
-    const hashNdx = origLine.indexOf('#');
-    const line = hashNdx >= 0 ? origLine.substring(0, hashNdx) : origLine;
-    const parts = splitOnSpaceHandleQuotesWithEscapes(line);
-    switch (parts[0].toUpperCase()) {
-      case 'TITLE':
-        lut.name = parts[1];
-        break;
-      case 'LUT_1D_SIZE':
-        lut.size = parseInt(parts[1]);
-        lut.type = '1D';
-        break;
-      case 'LUT_3D_SIZE':
-        lut.size = parseInt(parts[1]);
-        lut.type = '3D';
-        break;
-      case 'DOMAIN_MIN':
-        lut.min = parts.slice(1).map(parseFloat);
-        break;
-      case 'DOMAIN_MAX':
-        lut.max = parts.slice(1).map(parseFloat);
-        break;
-      default:
-        if (parts.length === 3) {
-          data.push(...parts.map(parseFloat));
-        }
-        break;
-    }
-  }
-
-  if (!lut.size) {
-    lut.size = lut.type === '1D'
-        ? (data.length / 3)
-        : Math.cbrt(data.length / 3);
-  }
-
-  return lut;
+export function parseCUBE( str ) {
+
+	const data = [];
+	const lut = {
+		name: 'unknown',
+		type: '1D',
+		size: 0,
+		data,
+		min: [ 0, 0, 0 ],
+		max: [ 1, 1, 1 ],
+	};
+
+	const lines = str.split( '\n' );
+	for ( const origLine of lines ) {
+
+		const hashNdx = origLine.indexOf( '#' );
+		const line = hashNdx >= 0 ? origLine.substring( 0, hashNdx ) : origLine;
+		const parts = splitOnSpaceHandleQuotesWithEscapes( line );
+		switch ( parts[ 0 ].toUpperCase() ) {
+
+			case 'TITLE':
+				lut.name = parts[ 1 ];
+				break;
+			case 'LUT_1D_SIZE':
+				lut.size = parseInt( parts[ 1 ] );
+				lut.type = '1D';
+				break;
+			case 'LUT_3D_SIZE':
+				lut.size = parseInt( parts[ 1 ] );
+				lut.type = '3D';
+				break;
+			case 'DOMAIN_MIN':
+				lut.min = parts.slice( 1 ).map( parseFloat );
+				break;
+			case 'DOMAIN_MAX':
+				lut.max = parts.slice( 1 ).map( parseFloat );
+				break;
+			default:
+				if ( parts.length === 3 ) {
+
+					data.push( ...parts.map( parseFloat ) );
+
+				}
+
+				break;
+
+		}
+
+	}
+
+	if ( ! lut.size ) {
+
+		lut.size = lut.type === '1D'
+			? ( data.length / 3 )
+			: Math.cbrt( data.length / 3 );
+
+	}
+
+	return lut;
+
 }
 
-function lerp(a, b, t) {
-  return a + (b - a) * t;
+function lerp( a, b, t ) {
+
+	return a + ( b - a ) * t;
+
 }
 
-function lut1Dto3D(lut) {
-  let src = lut.data;
-  if (src.length / 3 !== lut.size) {
-    src = [];
-    for (let i = 0; i < lut.size; ++i) {
-      const u = i / lut.size * lut.data.length;
-      const i0 = (u | 0) * 3;
-      const i1 = i0 + 3;
-      const t = u % 1;
-      src.push(
-        lerp(lut.data[i0 + 0], lut.data[i1 + 0], t),
-        lerp(lut.data[i0 + 0], lut.data[i1 + 1], t),
-        lerp(lut.data[i0 + 0], lut.data[i1 + 2], t),
-      );
-    }
-  }
-  const data = [];
-  for (let i = 0; i < lut.size * lut.size; ++i) {
-    data.push(...src);
-  }
-  return {...lut, data};
+function lut1Dto3D( lut ) {
+
+	let src = lut.data;
+	if ( src.length / 3 !== lut.size ) {
+
+		src = [];
+		for ( let i = 0; i < lut.size; ++ i ) {
+
+			const u = i / lut.size * lut.data.length;
+			const i0 = ( u | 0 ) * 3;
+			const i1 = i0 + 3;
+			const t = u % 1;
+			src.push(
+				lerp( lut.data[ i0 + 0 ], lut.data[ i1 + 0 ], t ),
+				lerp( lut.data[ i0 + 0 ], lut.data[ i1 + 1 ], t ),
+				lerp( lut.data[ i0 + 0 ], lut.data[ i1 + 2 ], t ),
+			);
+
+		}
+
+	}
+
+	const data = [];
+	for ( let i = 0; i < lut.size * lut.size; ++ i ) {
+
+		data.push( ...src );
+
+	}
+
+	return { ...lut, data };
+
 }
 
 const parsers = {
-  'cube': parseCUBE,
-  'csp': parseCSP,
+	'cube': parseCUBE,
+	'csp': parseCSP,
 };
 
 // for backward compatibility
-export function parse(str, format = 'cube') {
-  const parser = parsers[format.toLowerCase()];
-  if (!parser) {
-    throw new Error(`no parser for format: ${format}`);
-  }
-  return parser(str);
+export function parse( str, format = 'cube' ) {
+
+	const parser = parsers[ format.toLowerCase() ];
+	if ( ! parser ) {
+
+		throw new Error( `no parser for format: ${format}` );
+
+	}
+
+	return parser( str );
+
 }
 
-export function lutTo2D3Drgba8(lut) {
-  if (lut.type === '1D') {
-    lut = lut1Dto3D(lut);
-  }
-  const {min, max, size} = lut;
-  const range = min.map((min, ndx) => {
-    return max[ndx] - min;
-  });
-  const src = lut.data;
-  const data = new Uint8Array(size*size*size * 4);
-  const srcOffset = (offX, offY, offZ) => {
-    return (offX + offY * size + offZ * size * size) * 3;
-  };
-  const dOffset = (offX, offY, offZ) => {
-    return (offX + offY * size + offZ * size * size) * 4;
-  };
-  for (let dz = 0; dz < size; ++dz) {
-    for (let dy = 0; dy < size; ++dy) {
-      for (let dx = 0; dx < size; ++dx) {
-        const sx = dx;
-        const sy = dz;
-        const sz = dy;
-        const sOff = srcOffset(sx, sy, sz);
-        const dOff = dOffset(dx, dy, dz);
-        data[dOff + 0] = (src[sOff + 0] - min[0]) / range[0] * 255;
-        data[dOff + 1] = (src[sOff + 1] - min[1]) / range[1] * 255;
-        data[dOff + 2] = (src[sOff + 2] - min[2]) / range[2] * 255;
-        data[dOff + 3] = 255;
-      }
-    }
-  }
-  return {...lut, data};
+export function lutTo2D3Drgba8( lut ) {
+
+	if ( lut.type === '1D' ) {
+
+		lut = lut1Dto3D( lut );
+
+	}
+
+	const { min, max, size } = lut;
+	const range = min.map( ( min, ndx ) => {
+
+		return max[ ndx ] - min;
+
+	} );
+	const src = lut.data;
+	const data = new Uint8Array( size * size * size * 4 );
+	const srcOffset = ( offX, offY, offZ ) => {
+
+		return ( offX + offY * size + offZ * size * size ) * 3;
+
+	};
+
+	const dOffset = ( offX, offY, offZ ) => {
+
+		return ( offX + offY * size + offZ * size * size ) * 4;
+
+	};
+
+	for ( let dz = 0; dz < size; ++ dz ) {
+
+		for ( let dy = 0; dy < size; ++ dy ) {
+
+			for ( let dx = 0; dx < size; ++ dx ) {
+
+				const sx = dx;
+				const sy = dz;
+				const sz = dy;
+				const sOff = srcOffset( sx, sy, sz );
+				const dOff = dOffset( dx, dy, dz );
+				data[ dOff + 0 ] = ( src[ sOff + 0 ] - min[ 0 ] ) / range[ 0 ] * 255;
+				data[ dOff + 1 ] = ( src[ sOff + 1 ] - min[ 1 ] ) / range[ 1 ] * 255;
+				data[ dOff + 2 ] = ( src[ sOff + 2 ] - min[ 2 ] ) / range[ 2 ] * 255;
+				data[ dOff + 3 ] = 255;
+
+			}
+
+		}
+
+	}
+
+	return { ...lut, data };
+
 }

+ 42 - 29
manual/examples/resources/threejs-utils.js

@@ -32,21 +32,29 @@
 /* global define */
 
 (function(root, factory) {  // eslint-disable-line
-  if (typeof define === 'function' && define.amd) {
-    // AMD. Register as an anonymous module.
-    define([], function() {
-      return factory.call(root);
-    });
-  } else {
-    // Browser globals
-    root.threejsUtils = factory.call(root);
-  }
-}(this, function() {
+	if ( typeof define === 'function' && define.amd ) {
+
+		// AMD. Register as an anonymous module.
+		define( [], function () {
+
+			return factory.call( root );
+
+		} );
+
+	} else {
+
+		// Browser globals
+		root.threejsUtils = factory.call( root );
+
+	}
+
+}( this, function () {
+
   'use strict';  // eslint-disable-line
 
-  /** @module threejs-utils */
+	/** @module threejs-utils */
 
-  /**
+	/**
    * Resize a canvas to match the size its displayed.
    * @param {HTMLCanvasElement} canvas The canvas to resize.
    * @param {number} [multiplier] amount to multiply by.
@@ -54,21 +62,26 @@
    * @return {boolean} true if the canvas was resized.
    * @memberOf module:webgl-utils
    */
-  function resizeCanvasToDisplaySize(canvas, multiplier) {
-    multiplier = multiplier || 1;
-    const width  = canvas.clientWidth  * multiplier | 0;
-    const height = canvas.clientHeight * multiplier | 0;
-    if (canvas.width !== width ||  canvas.height !== height) {
-      canvas.width  = width;
-      canvas.height = height;
-      return true;
-    }
-    return false;
-  }
-
-  return {
-    resizeCanvasToDisplaySize: resizeCanvasToDisplaySize,
-  };
-
-}));
+	function resizeCanvasToDisplaySize( canvas, multiplier ) {
+
+		multiplier = multiplier || 1;
+		const width = canvas.clientWidth * multiplier | 0;
+		const height = canvas.clientHeight * multiplier | 0;
+		if ( canvas.width !== width || canvas.height !== height ) {
+
+			canvas.width = width;
+			canvas.height = height;
+			return true;
+
+		}
+
+		return false;
+
+	}
+
+	return {
+		resizeCanvasToDisplaySize: resizeCanvasToDisplaySize,
+	};
+
+} ) );
 

+ 558 - 437
manual/examples/resources/webgl-debug-helper.js

@@ -32,21 +32,29 @@
 /* global define, globalThis */
 
 (function(root, factory) {  // eslint-disable-line
-  if (typeof define === 'function' && define.amd) {
-    // AMD. Register as an anonymous module.
-    define([], function() {
-      return factory.call(root);
-    });
-  } else {
-    // Browser globals
-    root.webglDebugHelper = factory.call(root);
-  }
-}(this || globalThis, function() {
+	if ( typeof define === 'function' && define.amd ) {
+
+		// AMD. Register as an anonymous module.
+		define( [], function () {
+
+			return factory.call( root );
+
+		} );
+
+	} else {
+
+		// Browser globals
+		root.webglDebugHelper = factory.call( root );
+
+	}
+
+}( this || globalThis, function () {
+
   'use strict';  // eslint-disable-line
 
-  //------------ [ from https://github.com/KhronosGroup/WebGLDeveloperTools ]
+	//------------ [ from https://github.com/KhronosGroup/WebGLDeveloperTools ]
 
-  /*
+	/*
   ** Copyright (c) 2012 The Khronos Group Inc.
   **
   ** Permission is hereby granted, free of charge, to any person obtaining a
@@ -69,78 +77,104 @@
   ** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
   */
 
-  /**
+	/**
    * Types of contexts we have added to map
    */
-  const mappedContextTypes = {};
+	const mappedContextTypes = {};
 
-  /**
+	/**
    * Map of numbers to names.
    * @type {Object}
    */
-  const glEnums = {};
+	const glEnums = {};
 
-  /**
+	/**
    * Map of names to numbers.
    * @type {Object}
    */
-  const enumStringToValue = {};
+	const enumStringToValue = {};
 
-  /**
+	/**
    * Initializes this module. Safe to call more than once.
    * @param {!WebGLRenderingContext} ctx A WebGL context. If
    *    you have more than one context it doesn't matter which one
    *    you pass in, it is only used to pull out constants.
    */
-  function addEnumsForContext(ctx, type) {
-    if (!mappedContextTypes[type]) {
-      mappedContextTypes[type] = true;
-      for (const propertyName in ctx) {
-        if (typeof ctx[propertyName] === 'number') {
-          glEnums[ctx[propertyName]] = propertyName;
-          enumStringToValue[propertyName] = ctx[propertyName];
-        }
-      }
-    }
-  }
-
-  function enumArrayToString(enums) {
-    const enumStrings = [];
-    if (enums.length) {
-      for (let i = 0; i < enums.length; ++i) {
+	function addEnumsForContext( ctx, type ) {
+
+		if ( ! mappedContextTypes[ type ] ) {
+
+			mappedContextTypes[ type ] = true;
+			for ( const propertyName in ctx ) {
+
+				if ( typeof ctx[ propertyName ] === 'number' ) {
+
+					glEnums[ ctx[ propertyName ] ] = propertyName;
+					enumStringToValue[ propertyName ] = ctx[ propertyName ];
+
+				}
+
+			}
+
+		}
+
+	}
+
+	function enumArrayToString( enums ) {
+
+		const enumStrings = [];
+		if ( enums.length ) {
+
+			for ( let i = 0; i < enums.length; ++ i ) {
+
         enums.push(glEnumToString(enums[i]));  // eslint-disable-line
-      }
-      return '[' + enumStrings.join(', ') + ']';
-    }
-    return enumStrings.toString();
-  }
-
-  function makeBitFieldToStringFunc(enums) {
-    return function(value) {
-      let orResult = 0;
-      const orEnums = [];
-      for (let i = 0; i < enums.length; ++i) {
-        const enumValue = enumStringToValue[enums[i]];
-        if ((value & enumValue) !== 0) {
-          orResult |= enumValue;
+			}
+
+			return '[' + enumStrings.join( ', ' ) + ']';
+
+		}
+
+		return enumStrings.toString();
+
+	}
+
+	function makeBitFieldToStringFunc( enums ) {
+
+		return function ( value ) {
+
+			let orResult = 0;
+			const orEnums = [];
+			for ( let i = 0; i < enums.length; ++ i ) {
+
+				const enumValue = enumStringToValue[ enums[ i ] ];
+				if ( ( value & enumValue ) !== 0 ) {
+
+					orResult |= enumValue;
           orEnums.push(glEnumToString(enumValue));  // eslint-disable-line
-        }
-      }
-      if (orResult === value) {
-        return orEnums.join(' | ');
-      } else {
+				}
+
+			}
+
+			if ( orResult === value ) {
+
+				return orEnums.join( ' | ' );
+
+			} else {
+
         return glEnumToString(value);  // eslint-disable-line
-      }
-    };
-  }
+			}
+
+		};
 
-  const destBufferBitFieldToString = makeBitFieldToStringFunc([
-    'COLOR_BUFFER_BIT',
-    'DEPTH_BUFFER_BIT',
-    'STENCIL_BUFFER_BIT',
-  ]);
+	}
 
-  /**
+	const destBufferBitFieldToString = makeBitFieldToStringFunc( [
+		'COLOR_BUFFER_BIT',
+		'DEPTH_BUFFER_BIT',
+		'STENCIL_BUFFER_BIT',
+	] );
+
+	/**
    * Which arguments are enums based on the number of arguments to the function.
    * So
    *    'texImage2D': {
@@ -154,207 +188,207 @@
    *
    * @type {!Object.<number, (!Object.<number, string>|function)}
    */
-  const glValidEnumContexts = {
-    // Generic setters and getters
-
-    'enable': {1: { 0:true }},
-    'disable': {1: { 0:true }},
-    'getParameter': {1: { 0:true }},
-
-    // Rendering
-
-    'drawArrays': {3:{ 0:true }},
-    'drawElements': {4:{ 0:true, 2:true }},
-    'drawArraysInstanced': {4: { 0:true }},
-    'drawElementsInstanced': {5: {0:true, 2: true }},
-    'drawRangeElements': {6: {0:true, 4: true }},
-
-    // Shaders
-
-    'createShader': {1: { 0:true }},
-    'getShaderParameter': {2: { 1:true }},
-    'getProgramParameter': {2: { 1:true }},
-    'getShaderPrecisionFormat': {2: { 0: true, 1:true }},
-
-    // Vertex attributes
-
-    'getVertexAttrib': {2: { 1:true }},
-    'vertexAttribPointer': {6: { 2:true }},
-    'vertexAttribIPointer': {5: { 2:true }},  // WebGL2
-
-    // Textures
-
-    'bindTexture': {2: { 0:true }},
-    'activeTexture': {1: { 0:true }},
-    'getTexParameter': {2: { 0:true, 1:true }},
-    'texParameterf': {3: { 0:true, 1:true }},
-    'texParameteri': {3: { 0:true, 1:true, 2:true }},
-    'texImage2D': {
-      9: { 0:true, 2:true, 6:true, 7:true },
-      6: { 0:true, 2:true, 3:true, 4:true },
-      10: { 0:true, 2:true, 6:true, 7:true },  // WebGL2
-    },
-    'texImage3D': {
-      10: { 0:true, 2:true, 7:true, 8:true },  // WebGL2
-      11: { 0:true, 2:true, 7:true, 8:true },  // WebGL2
-    },
-    'texSubImage2D': {
-      9: { 0:true, 6:true, 7:true },
-      7: { 0:true, 4:true, 5:true },
-      10: { 0:true, 6:true, 7:true },  // WebGL2
-    },
-    'texSubImage3D': {
-      11: { 0:true, 8:true, 9:true },  // WebGL2
-      12: { 0:true, 8:true, 9:true },  // WebGL2
-    },
-    'texStorage2D': { 5: { 0:true, 2:true }},  // WebGL2
-    'texStorage3D': { 6: { 0:true, 2:true }},  // WebGL2
-    'copyTexImage2D': {8: { 0:true, 2:true }},
-    'copyTexSubImage2D': {8: { 0:true }},
-    'copyTexSubImage3D': {9: { 0:true }},  // WebGL2
-    'generateMipmap': {1: { 0:true }},
-    'compressedTexImage2D': {
-      7: { 0: true, 2:true },
-      8: { 0: true, 2:true },  // WebGL2
-    },
-    'compressedTexSubImage2D': {
-      8: { 0: true, 6:true },
-      9: { 0: true, 6:true },  // WebGL2
-    },
-    'compressedTexImage3D': {
-      8: { 0: true, 2: true, },  // WebGL2
-      9: { 0: true, 2: true, },  // WebGL2
-    },
-    'compressedTexSubImage3D': {
-      9: { 0: true, 8: true, },  // WebGL2
-      10: { 0: true, 8: true, },  // WebGL2
-    },
-
-    // Buffer objects
-
-    'bindBuffer': {2: { 0:true }},
-    'bufferData': {
-      3: { 0:true, 2:true },
-      4: { 0:true, 2:true },  // WebGL2
-      5: { 0:true, 2:true },  // WebGL2
-    },
-    'bufferSubData': {
-      3: { 0:true },
-      4: { 0:true },  // WebGL2
-      5: { 0:true },  // WebGL2
-    },
-    'copyBufferSubData': {
-      5: { 0:true },  // WeBGL2
-    },
-    'getBufferParameter': {2: { 0:true, 1:true }},
-    'getBufferSubData': {
-      3: { 0: true, },  // WebGL2
-      4: { 0: true, },  // WebGL2
-      5: { 0: true, },  // WebGL2
-    },
-
-    // Renderbuffers and framebuffers
-
-    'pixelStorei': {2: { 0:true, 1:true }},
-    'readPixels': {
-      7: { 4:true, 5:true },
-      8: { 4:true, 5:true },  // WebGL2
-    },
-    'bindRenderbuffer': {2: { 0:true }},
-    'bindFramebuffer': {2: { 0:true }},
-    'blitFramebuffer': {10: { 8: destBufferBitFieldToString, 9:true }},  // WebGL2
-    'checkFramebufferStatus': {1: { 0:true }},
-    'framebufferRenderbuffer': {4: { 0:true, 1:true, 2:true }},
-    'framebufferTexture2D': {5: { 0:true, 1:true, 2:true }},
-    'framebufferTextureLayer': {5: {0:true, 1:true }},  // WebGL2
-    'getFramebufferAttachmentParameter': {3: { 0:true, 1:true, 2:true }},
-    'getInternalformatParameter': {3: {0:true, 1:true, 2:true }},  // WebGL2
-    'getRenderbufferParameter': {2: { 0:true, 1:true }},
-    'invalidateFramebuffer': {2: { 0:true, 1: enumArrayToString, }},  // WebGL2
-    'invalidateSubFramebuffer': {6: {0: true, 1: enumArrayToString, }},  // WebGL2
-    'readBuffer': {1: {0: true}},  // WebGL2
-    'renderbufferStorage': {4: { 0:true, 1:true }},
-    'renderbufferStorageMultisample': {5: { 0: true, 2: true }},  // WebGL2
-
-    // Frame buffer operations (clear, blend, depth test, stencil)
-
-    'clear': {1: { 0: destBufferBitFieldToString }},
-    'depthFunc': {1: { 0:true }},
-    'blendFunc': {2: { 0:true, 1:true }},
-    'blendFuncSeparate': {4: { 0:true, 1:true, 2:true, 3:true }},
-    'blendEquation': {1: { 0:true }},
-    'blendEquationSeparate': {2: { 0:true, 1:true }},
-    'stencilFunc': {3: { 0:true }},
-    'stencilFuncSeparate': {4: { 0:true, 1:true }},
-    'stencilMaskSeparate': {2: { 0:true }},
-    'stencilOp': {3: { 0:true, 1:true, 2:true }},
-    'stencilOpSeparate': {4: { 0:true, 1:true, 2:true, 3:true }},
-
-    // Culling
-
-    'cullFace': {1: { 0:true }},
-    'frontFace': {1: { 0:true }},
-
-    // ANGLE_instanced_arrays extension
-
-    'drawArraysInstancedANGLE': {4: { 0:true }},
-    'drawElementsInstancedANGLE': {5: { 0:true, 2:true }},
-
-    // EXT_blend_minmax extension
-
-    'blendEquationEXT': {1: { 0:true }},
-
-    // Multiple Render Targets
-
-    'drawBuffersWebGL': {1: {0: enumArrayToString, }},  // WEBGL_draw_bufers
-    'drawBuffers': {1: {0: enumArrayToString, }},  // WebGL2
-    'clearBufferfv': {
-      4: {0: true },  // WebGL2
-      5: {0: true },  // WebGL2
-    },
-    'clearBufferiv': {
-      4: {0: true },  // WebGL2
-      5: {0: true },  // WebGL2
-    },
-    'clearBufferuiv': {
-      4: {0: true },  // WebGL2
-      5: {0: true },  // WebGL2
-    },
-    'clearBufferfi': { 4: {0: true}},  // WebGL2
-
-    // QueryObjects
-
-    'beginQuery': { 2: { 0: true }},  // WebGL2
-    'endQuery': { 1: { 0: true }},  // WebGL2
-    'getQuery': { 2: { 0: true, 1: true }},  // WebGL2
-    'getQueryParameter': { 2: { 1: true }},  // WebGL2
-
-    //  Sampler Objects
-
-    'samplerParameteri': { 3: { 1: true }},  // WebGL2
-    'samplerParameterf': { 3: { 1: true }},  // WebGL2
-    'getSamplerParameter': { 2: { 1: true }},  // WebGL2
-
-    //  Sync objects
-
-    'clientWaitSync': { 3: { 1: makeBitFieldToStringFunc(['SYNC_FLUSH_COMMANDS_BIT']) }},  // WebGL2
-    'fenceSync': { 2: { 0: true }},  // WebGL2
-    'getSyncParameter': { 2: { 1: true }},  // WebGL2
-
-    //  Transform Feedback
-
-    'bindTransformFeedback': { 2: { 0: true }},  // WebGL2
-    'beginTransformFeedback': { 1: { 0: true }},  // WebGL2
-
-    // Uniform Buffer Objects and Transform Feedback Buffers
-    'bindBufferBase': { 3: { 0: true }},  // WebGL2
-    'bindBufferRange': { 5: { 0: true }},  // WebGL2
-    'getIndexedParameter': { 2: { 0: true }},  // WebGL2
-    'getActiveUniforms': { 3: { 2: true }},  // WebGL2
-    'getActiveUniformBlockParameter': { 3: { 2: true }},  // WebGL2
-  };
-
-  /**
+	const glValidEnumContexts = {
+		// Generic setters and getters
+
+		'enable': { 1: { 0: true } },
+		'disable': { 1: { 0: true } },
+		'getParameter': { 1: { 0: true } },
+
+		// Rendering
+
+		'drawArrays': { 3: { 0: true } },
+		'drawElements': { 4: { 0: true, 2: true } },
+		'drawArraysInstanced': { 4: { 0: true } },
+		'drawElementsInstanced': { 5: { 0: true, 2: true } },
+		'drawRangeElements': { 6: { 0: true, 4: true } },
+
+		// Shaders
+
+		'createShader': { 1: { 0: true } },
+		'getShaderParameter': { 2: { 1: true } },
+		'getProgramParameter': { 2: { 1: true } },
+		'getShaderPrecisionFormat': { 2: { 0: true, 1: true } },
+
+		// Vertex attributes
+
+		'getVertexAttrib': { 2: { 1: true } },
+		'vertexAttribPointer': { 6: { 2: true } },
+		'vertexAttribIPointer': { 5: { 2: true } }, // WebGL2
+
+		// Textures
+
+		'bindTexture': { 2: { 0: true } },
+		'activeTexture': { 1: { 0: true } },
+		'getTexParameter': { 2: { 0: true, 1: true } },
+		'texParameterf': { 3: { 0: true, 1: true } },
+		'texParameteri': { 3: { 0: true, 1: true, 2: true } },
+		'texImage2D': {
+			9: { 0: true, 2: true, 6: true, 7: true },
+			6: { 0: true, 2: true, 3: true, 4: true },
+			10: { 0: true, 2: true, 6: true, 7: true }, // WebGL2
+		},
+		'texImage3D': {
+			10: { 0: true, 2: true, 7: true, 8: true }, // WebGL2
+			11: { 0: true, 2: true, 7: true, 8: true }, // WebGL2
+		},
+		'texSubImage2D': {
+			9: { 0: true, 6: true, 7: true },
+			7: { 0: true, 4: true, 5: true },
+			10: { 0: true, 6: true, 7: true }, // WebGL2
+		},
+		'texSubImage3D': {
+			11: { 0: true, 8: true, 9: true }, // WebGL2
+			12: { 0: true, 8: true, 9: true }, // WebGL2
+		},
+		'texStorage2D': { 5: { 0: true, 2: true } }, // WebGL2
+		'texStorage3D': { 6: { 0: true, 2: true } }, // WebGL2
+		'copyTexImage2D': { 8: { 0: true, 2: true } },
+		'copyTexSubImage2D': { 8: { 0: true } },
+		'copyTexSubImage3D': { 9: { 0: true } }, // WebGL2
+		'generateMipmap': { 1: { 0: true } },
+		'compressedTexImage2D': {
+			7: { 0: true, 2: true },
+			8: { 0: true, 2: true }, // WebGL2
+		},
+		'compressedTexSubImage2D': {
+			8: { 0: true, 6: true },
+			9: { 0: true, 6: true }, // WebGL2
+		},
+		'compressedTexImage3D': {
+			8: { 0: true, 2: true, }, // WebGL2
+			9: { 0: true, 2: true, }, // WebGL2
+		},
+		'compressedTexSubImage3D': {
+			9: { 0: true, 8: true, }, // WebGL2
+			10: { 0: true, 8: true, }, // WebGL2
+		},
+
+		// Buffer objects
+
+		'bindBuffer': { 2: { 0: true } },
+		'bufferData': {
+			3: { 0: true, 2: true },
+			4: { 0: true, 2: true }, // WebGL2
+			5: { 0: true, 2: true }, // WebGL2
+		},
+		'bufferSubData': {
+			3: { 0: true },
+			4: { 0: true }, // WebGL2
+			5: { 0: true }, // WebGL2
+		},
+		'copyBufferSubData': {
+			5: { 0: true }, // WeBGL2
+		},
+		'getBufferParameter': { 2: { 0: true, 1: true } },
+		'getBufferSubData': {
+			3: { 0: true, }, // WebGL2
+			4: { 0: true, }, // WebGL2
+			5: { 0: true, }, // WebGL2
+		},
+
+		// Renderbuffers and framebuffers
+
+		'pixelStorei': { 2: { 0: true, 1: true } },
+		'readPixels': {
+			7: { 4: true, 5: true },
+			8: { 4: true, 5: true }, // WebGL2
+		},
+		'bindRenderbuffer': { 2: { 0: true } },
+		'bindFramebuffer': { 2: { 0: true } },
+		'blitFramebuffer': { 10: { 8: destBufferBitFieldToString, 9: true } }, // WebGL2
+		'checkFramebufferStatus': { 1: { 0: true } },
+		'framebufferRenderbuffer': { 4: { 0: true, 1: true, 2: true } },
+		'framebufferTexture2D': { 5: { 0: true, 1: true, 2: true } },
+		'framebufferTextureLayer': { 5: { 0: true, 1: true } }, // WebGL2
+		'getFramebufferAttachmentParameter': { 3: { 0: true, 1: true, 2: true } },
+		'getInternalformatParameter': { 3: { 0: true, 1: true, 2: true } }, // WebGL2
+		'getRenderbufferParameter': { 2: { 0: true, 1: true } },
+		'invalidateFramebuffer': { 2: { 0: true, 1: enumArrayToString, } }, // WebGL2
+		'invalidateSubFramebuffer': { 6: { 0: true, 1: enumArrayToString, } }, // WebGL2
+		'readBuffer': { 1: { 0: true } }, // WebGL2
+		'renderbufferStorage': { 4: { 0: true, 1: true } },
+		'renderbufferStorageMultisample': { 5: { 0: true, 2: true } }, // WebGL2
+
+		// Frame buffer operations (clear, blend, depth test, stencil)
+
+		'clear': { 1: { 0: destBufferBitFieldToString } },
+		'depthFunc': { 1: { 0: true } },
+		'blendFunc': { 2: { 0: true, 1: true } },
+		'blendFuncSeparate': { 4: { 0: true, 1: true, 2: true, 3: true } },
+		'blendEquation': { 1: { 0: true } },
+		'blendEquationSeparate': { 2: { 0: true, 1: true } },
+		'stencilFunc': { 3: { 0: true } },
+		'stencilFuncSeparate': { 4: { 0: true, 1: true } },
+		'stencilMaskSeparate': { 2: { 0: true } },
+		'stencilOp': { 3: { 0: true, 1: true, 2: true } },
+		'stencilOpSeparate': { 4: { 0: true, 1: true, 2: true, 3: true } },
+
+		// Culling
+
+		'cullFace': { 1: { 0: true } },
+		'frontFace': { 1: { 0: true } },
+
+		// ANGLE_instanced_arrays extension
+
+		'drawArraysInstancedANGLE': { 4: { 0: true } },
+		'drawElementsInstancedANGLE': { 5: { 0: true, 2: true } },
+
+		// EXT_blend_minmax extension
+
+		'blendEquationEXT': { 1: { 0: true } },
+
+		// Multiple Render Targets
+
+		'drawBuffersWebGL': { 1: { 0: enumArrayToString, } }, // WEBGL_draw_bufers
+		'drawBuffers': { 1: { 0: enumArrayToString, } }, // WebGL2
+		'clearBufferfv': {
+			4: { 0: true }, // WebGL2
+			5: { 0: true }, // WebGL2
+		},
+		'clearBufferiv': {
+			4: { 0: true }, // WebGL2
+			5: { 0: true }, // WebGL2
+		},
+		'clearBufferuiv': {
+			4: { 0: true }, // WebGL2
+			5: { 0: true }, // WebGL2
+		},
+		'clearBufferfi': { 4: { 0: true } }, // WebGL2
+
+		// QueryObjects
+
+		'beginQuery': { 2: { 0: true } }, // WebGL2
+		'endQuery': { 1: { 0: true } }, // WebGL2
+		'getQuery': { 2: { 0: true, 1: true } }, // WebGL2
+		'getQueryParameter': { 2: { 1: true } }, // WebGL2
+
+		//  Sampler Objects
+
+		'samplerParameteri': { 3: { 1: true } }, // WebGL2
+		'samplerParameterf': { 3: { 1: true } }, // WebGL2
+		'getSamplerParameter': { 2: { 1: true } }, // WebGL2
+
+		//  Sync objects
+
+		'clientWaitSync': { 3: { 1: makeBitFieldToStringFunc( [ 'SYNC_FLUSH_COMMANDS_BIT' ] ) } }, // WebGL2
+		'fenceSync': { 2: { 0: true } }, // WebGL2
+		'getSyncParameter': { 2: { 1: true } }, // WebGL2
+
+		//  Transform Feedback
+
+		'bindTransformFeedback': { 2: { 0: true } }, // WebGL2
+		'beginTransformFeedback': { 1: { 0: true } }, // WebGL2
+
+		// Uniform Buffer Objects and Transform Feedback Buffers
+		'bindBufferBase': { 3: { 0: true } }, // WebGL2
+		'bindBufferRange': { 5: { 0: true } }, // WebGL2
+		'getIndexedParameter': { 2: { 0: true } }, // WebGL2
+		'getActiveUniforms': { 3: { 2: true } }, // WebGL2
+		'getActiveUniformBlockParameter': { 3: { 2: true } }, // WebGL2
+	};
+
+	/**
    * Gets an string version of an WebGL enum.
    *
    * Example:
@@ -363,14 +397,16 @@
    * @param {number} value Value to return an enum for
    * @return {string} The string version of the enum.
    */
-  function glEnumToString(value) {
-    const name = glEnums[value];
-    return (name !== undefined)
-        ? `gl.${name}`
-        : `/*UNKNOWN WebGL ENUM*/ 0x${value.toString(16)}`;
-  }
-
-  /**
+	function glEnumToString( value ) {
+
+		const name = glEnums[ value ];
+		return ( name !== undefined )
+			? `gl.${name}`
+			: `/*UNKNOWN WebGL ENUM*/ 0x${value.toString( 16 )}`;
+
+	}
+
+	/**
    * Returns the string version of a WebGL argument.
    * Attempts to convert enum arguments to strings.
    * @param {string} functionName the name of the WebGL function.
@@ -379,31 +415,50 @@
    * @param {*} value The value of the argument.
    * @return {string} The value as a string.
    */
-  function glFunctionArgToString(functionName, numArgs, argumentIndex, value) {
-    const funcInfos = glValidEnumContexts[functionName];
-    if (funcInfos !== undefined) {
-      const funcInfo = funcInfos[numArgs];
-      if (funcInfo !== undefined) {
-        const argType = funcInfo[argumentIndex];
-        if (argType) {
-          if (typeof argType === 'function') {
-            return argType(value);
-          } else {
-            return glEnumToString(value);
-          }
-        }
-      }
-    }
-    if (value === null) {
-      return 'null';
-    } else if (value === undefined) {
-      return 'undefined';
-    } else {
-      return value.toString();
-    }
-  }
-
-  /**
+	function glFunctionArgToString( functionName, numArgs, argumentIndex, value ) {
+
+		const funcInfos = glValidEnumContexts[ functionName ];
+		if ( funcInfos !== undefined ) {
+
+			const funcInfo = funcInfos[ numArgs ];
+			if ( funcInfo !== undefined ) {
+
+				const argType = funcInfo[ argumentIndex ];
+				if ( argType ) {
+
+					if ( typeof argType === 'function' ) {
+
+						return argType( value );
+
+					} else {
+
+						return glEnumToString( value );
+
+					}
+
+				}
+
+			}
+
+		}
+
+		if ( value === null ) {
+
+			return 'null';
+
+		} else if ( value === undefined ) {
+
+			return 'undefined';
+
+		} else {
+
+			return value.toString();
+
+		}
+
+	}
+
+	/**
    * Converts the arguments of a WebGL function to a string.
    * Attempts to convert enum arguments to strings.
    *
@@ -411,28 +466,37 @@
    * @param {number} args The arguments.
    * @return {string} The arguments as a string.
    */
-  function glFunctionArgsToString(functionName, args) {
-    // apparently we can't do args.join(",");
-    const argStrs = [];
-    const numArgs = args.length;
-    for (let ii = 0; ii < numArgs; ++ii) {
-      argStrs.push(glFunctionArgToString(functionName, numArgs, ii, args[ii]));
-    }
-    return argStrs.join(', ');
-  }
-
-  function makePropertyWrapper(wrapper, original, propertyName) {
+	function glFunctionArgsToString( functionName, args ) {
+
+		// apparently we can't do args.join(",");
+		const argStrs = [];
+		const numArgs = args.length;
+		for ( let ii = 0; ii < numArgs; ++ ii ) {
+
+			argStrs.push( glFunctionArgToString( functionName, numArgs, ii, args[ ii ] ) );
+
+		}
+
+		return argStrs.join( ', ' );
+
+	}
+
+	function makePropertyWrapper( wrapper, original, propertyName ) {
+
     wrapper.__defineGetter__(propertyName, function() {  // eslint-disable-line
-      return original[propertyName];
-    });
-    // TODO(gmane): this needs to handle properties that take more than
-    // one value?
+			return original[ propertyName ];
+
+		} );
+		// TODO(gmane): this needs to handle properties that take more than
+		// one value?
     wrapper.__defineSetter__(propertyName, function(value) {  // eslint-disable-line
-      original[propertyName] = value;
-    });
-  }
+			original[ propertyName ] = value;
+
+		} );
 
-  /**
+	}
+
+	/**
    * Given a WebGL context returns a wrapped context that calls
    * gl.getError after every command and calls a function if the
    * result is not gl.NO_ERROR.
@@ -449,125 +513,182 @@
    * @param {!WebGLRenderingContext} opt_err_ctx The webgl context
    *        to call getError on if different than ctx.
    */
-  function makeDebugContext(ctx, options) {
-    options = options || {};
-    const errCtx = options.errCtx || ctx;
-    const onFunc = options.funcFunc;
-    const sharedState = options.sharedState || {
-      numDrawCallsRemaining: options.maxDrawCalls || -1,
-      wrappers: {},
-    };
-    options.sharedState = sharedState;
-
-    const errorFunc = options.errorFunc || function(err, functionName, args) {
-      console.error(`WebGL error ${glEnumToString(err)} in ${functionName}(${glFunctionArgsToString(functionName, args)})`);  /* eslint-disable-line no-console */
-    };
-
-    // Holds booleans for each GL error so after we get the error ourselves
-    // we can still return it to the client app.
-    const glErrorShadow = { };
-    const wrapper = {};
-
-    function removeChecks() {
-      Object.keys(sharedState.wrappers).forEach(function(name) {
-        const pair = sharedState.wrappers[name];
-        const wrapper = pair.wrapper;
-        const orig = pair.orig;
-        for (const propertyName in wrapper) {
-          if (typeof wrapper[propertyName] === 'function') {
-            wrapper[propertyName] = orig[propertyName].bind(orig);
-          }
-        }
-      });
-    }
-
-    function checkMaxDrawCalls() {
-      if (sharedState.numDrawCallsRemaining === 0) {
-        removeChecks();
-      }
-      --sharedState.numDrawCallsRemaining;
-    }
-
-    function noop() {
-    }
-
-    // Makes a function that calls a WebGL function and then calls getError.
-    function makeErrorWrapper(ctx, functionName) {
-      const check = functionName.substring(0, 4) === 'draw' ? checkMaxDrawCalls : noop;
-      return function() {
-        if (onFunc) {
-          onFunc(functionName, arguments);
-        }
-        const result = ctx[functionName].apply(ctx, arguments);
-        const err = errCtx.getError();
-        if (err !== 0) {
-          glErrorShadow[err] = true;
-          errorFunc(err, functionName, arguments);
-        }
-        check();
-        return result;
-      };
-    }
-
-    function makeGetExtensionWrapper(ctx, wrapped) {
-      return function() {
-        const extensionName = arguments[0];
-        let ext = sharedState.wrappers[extensionName];
-        if (!ext) {
-          ext = wrapped.apply(ctx, arguments);
-          if (ext) {
-            const origExt = ext;
-            ext = makeDebugContext(ext, {...options, errCtx: ctx});
-            sharedState.wrappers[extensionName] = { wrapper: ext, orig: origExt };
-            addEnumsForContext(origExt, extensionName);
-          }
-        }
-        return ext;
-      };
-    }
-
-    // Make a an object that has a copy of every property of the WebGL context
-    // but wraps all functions.
-    for (const propertyName in ctx) {
-      if (typeof ctx[propertyName] === 'function') {
-        if (propertyName !== 'getExtension') {
-          wrapper[propertyName] = makeErrorWrapper(ctx, propertyName);
-        } else {
-          const wrapped = makeErrorWrapper(ctx, propertyName);
-          wrapper[propertyName] = makeGetExtensionWrapper(ctx, wrapped);
-        }
-      } else {
-        makePropertyWrapper(wrapper, ctx, propertyName);
-      }
-    }
-
-    // Override the getError function with one that returns our saved results.
-    if (wrapper.getError) {
-      wrapper.getError = function() {
-        for (const err of Object.keys(glErrorShadow)) {
-          if (glErrorShadow[err]) {
-            glErrorShadow[err] = false;
-            return err;
-          }
-        }
-        return ctx.NO_ERROR;
-      };
-    }
-
-    if (wrapper.bindBuffer) {
-      sharedState.wrappers['webgl'] = { wrapper: wrapper, orig: ctx };
-      addEnumsForContext(ctx, ctx.bindBufferBase ? 'WebGL2' : 'WebGL');
-    }
-
-    return wrapper;
-  }
-
-  return {
-    makeDebugContext,
-    glFunctionArgsToString,
-    glFunctionArgToString,
-    glEnumToString,
-  };
-
-}));
+	function makeDebugContext( ctx, options ) {
+
+		options = options || {};
+		const errCtx = options.errCtx || ctx;
+		const onFunc = options.funcFunc;
+		const sharedState = options.sharedState || {
+			numDrawCallsRemaining: options.maxDrawCalls || - 1,
+			wrappers: {},
+		};
+		options.sharedState = sharedState;
+
+		const errorFunc = options.errorFunc || function ( err, functionName, args ) {
+
+			console.error( `WebGL error ${glEnumToString( err )} in ${functionName}(${glFunctionArgsToString( functionName, args )})` ); /* eslint-disable-line no-console */
+
+		};
+
+		// Holds booleans for each GL error so after we get the error ourselves
+		// we can still return it to the client app.
+		const glErrorShadow = { };
+		const wrapper = {};
+
+		function removeChecks() {
+
+			Object.keys( sharedState.wrappers ).forEach( function ( name ) {
+
+				const pair = sharedState.wrappers[ name ];
+				const wrapper = pair.wrapper;
+				const orig = pair.orig;
+				for ( const propertyName in wrapper ) {
+
+					if ( typeof wrapper[ propertyName ] === 'function' ) {
+
+						wrapper[ propertyName ] = orig[ propertyName ].bind( orig );
+
+					}
+
+				}
+
+			} );
+
+		}
+
+		function checkMaxDrawCalls() {
+
+			if ( sharedState.numDrawCallsRemaining === 0 ) {
+
+				removeChecks();
+
+			}
+
+			-- sharedState.numDrawCallsRemaining;
+
+		}
+
+		function noop() {
+		}
+
+		// Makes a function that calls a WebGL function and then calls getError.
+		function makeErrorWrapper( ctx, functionName ) {
+
+			const check = functionName.substring( 0, 4 ) === 'draw' ? checkMaxDrawCalls : noop;
+			return function () {
+
+				if ( onFunc ) {
+
+					onFunc( functionName, arguments );
+
+				}
+
+				const result = ctx[ functionName ].apply( ctx, arguments );
+				const err = errCtx.getError();
+				if ( err !== 0 ) {
+
+					glErrorShadow[ err ] = true;
+					errorFunc( err, functionName, arguments );
+
+				}
+
+				check();
+				return result;
+
+			};
+
+		}
+
+		function makeGetExtensionWrapper( ctx, wrapped ) {
+
+			return function () {
+
+				const extensionName = arguments[ 0 ];
+				let ext = sharedState.wrappers[ extensionName ];
+				if ( ! ext ) {
+
+					ext = wrapped.apply( ctx, arguments );
+					if ( ext ) {
+
+						const origExt = ext;
+						ext = makeDebugContext( ext, { ...options, errCtx: ctx } );
+						sharedState.wrappers[ extensionName ] = { wrapper: ext, orig: origExt };
+						addEnumsForContext( origExt, extensionName );
+
+					}
+
+				}
+
+				return ext;
+
+			};
+
+		}
+
+		// Make a an object that has a copy of every property of the WebGL context
+		// but wraps all functions.
+		for ( const propertyName in ctx ) {
+
+			if ( typeof ctx[ propertyName ] === 'function' ) {
+
+				if ( propertyName !== 'getExtension' ) {
+
+					wrapper[ propertyName ] = makeErrorWrapper( ctx, propertyName );
+
+				} else {
+
+					const wrapped = makeErrorWrapper( ctx, propertyName );
+					wrapper[ propertyName ] = makeGetExtensionWrapper( ctx, wrapped );
+
+				}
+
+			} else {
+
+				makePropertyWrapper( wrapper, ctx, propertyName );
+
+			}
+
+		}
+
+		// Override the getError function with one that returns our saved results.
+		if ( wrapper.getError ) {
+
+			wrapper.getError = function () {
+
+				for ( const err of Object.keys( glErrorShadow ) ) {
+
+					if ( glErrorShadow[ err ] ) {
+
+						glErrorShadow[ err ] = false;
+						return err;
+
+					}
+
+				}
+
+				return ctx.NO_ERROR;
+
+			};
+
+		}
+
+		if ( wrapper.bindBuffer ) {
+
+			sharedState.wrappers[ 'webgl' ] = { wrapper: wrapper, orig: ctx };
+			addEnumsForContext( ctx, ctx.bindBufferBase ? 'WebGL2' : 'WebGL' );
+
+		}
+
+		return wrapper;
+
+	}
+
+	return {
+		makeDebugContext,
+		glFunctionArgsToString,
+		glFunctionArgToString,
+		glEnumToString,
+	};
+
+} ) );
 

+ 99 - 82
manual/examples/shared-cubes.js

@@ -1,88 +1,105 @@
 import * as THREE from 'https://cdn.skypack.dev/[email protected]/build/three.module.js';
 
 export const state = {
-  width: 300,   // canvas default
-  height: 150,  // canvas default
+	width: 300, // canvas default
+	height: 150, // canvas default
 };
 
-export function init(data) {  /* eslint-disable-line no-unused-vars */
-  const {canvas} = data;
-  const renderer = new THREE.WebGLRenderer({canvas});
-
-  state.width = canvas.width;
-  state.height = canvas.height;
-
-  const fov = 75;
-  const aspect = 2; // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 4;
-
-  const scene = new THREE.Scene();
-
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(-1, 2, 4);
-    scene.add(light);
-  }
-
-  const boxWidth = 1;
-  const boxHeight = 1;
-  const boxDepth = 1;
-  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
-
-  function makeInstance(geometry, color, x) {
-    const material = new THREE.MeshPhongMaterial({
-      color,
-    });
-
-    const cube = new THREE.Mesh(geometry, material);
-    scene.add(cube);
-
-    cube.position.x = x;
-
-    return cube;
-  }
-
-  const cubes = [
-    makeInstance(geometry, 0x44aa88, 0),
-    makeInstance(geometry, 0x8844aa, -2),
-    makeInstance(geometry, 0xaa8844, 2),
-  ];
-
-  function resizeRendererToDisplaySize(renderer) {
-    const canvas = renderer.domElement;
-    const width = state.width;
-    const height = state.height;
-    const needResize = canvas.width !== width || canvas.height !== height;
-    if (needResize) {
-      renderer.setSize(width, height, false);
-    }
-    return needResize;
-  }
-
-  function render(time) {
-    time *= 0.001;
-
-    if (resizeRendererToDisplaySize(renderer)) {
-      camera.aspect = state.width / state.height;
-      camera.updateProjectionMatrix();
-    }
-
-    cubes.forEach((cube, ndx) => {
-      const speed = 1 + ndx * .1;
-      const rot = time * speed;
-      cube.rotation.x = rot;
-      cube.rotation.y = rot;
-    });
-
-    renderer.render(scene, camera);
-
-    requestAnimationFrame(render);
-  }
-
-  requestAnimationFrame(render);
+export function init( data ) { /* eslint-disable-line no-unused-vars */
+
+	const { canvas } = data;
+	const renderer = new THREE.WebGLRenderer( { canvas } );
+
+	state.width = canvas.width;
+	state.height = canvas.height;
+
+	const fov = 75;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 4;
+
+	const scene = new THREE.Scene();
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 1;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( - 1, 2, 4 );
+		scene.add( light );
+
+	}
+
+	const boxWidth = 1;
+	const boxHeight = 1;
+	const boxDepth = 1;
+	const geometry = new THREE.BoxGeometry( boxWidth, boxHeight, boxDepth );
+
+	function makeInstance( geometry, color, x ) {
+
+		const material = new THREE.MeshPhongMaterial( {
+			color,
+		} );
+
+		const cube = new THREE.Mesh( geometry, material );
+		scene.add( cube );
+
+		cube.position.x = x;
+
+		return cube;
+
+	}
+
+	const cubes = [
+		makeInstance( geometry, 0x44aa88, 0 ),
+		makeInstance( geometry, 0x8844aa, - 2 ),
+		makeInstance( geometry, 0xaa8844, 2 ),
+	];
+
+	function resizeRendererToDisplaySize( renderer ) {
+
+		const canvas = renderer.domElement;
+		const width = state.width;
+		const height = state.height;
+		const needResize = canvas.width !== width || canvas.height !== height;
+		if ( needResize ) {
+
+			renderer.setSize( width, height, false );
+
+		}
+
+		return needResize;
+
+	}
+
+	function render( time ) {
+
+		time *= 0.001;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			camera.aspect = state.width / state.height;
+			camera.updateProjectionMatrix();
+
+		}
+
+		cubes.forEach( ( cube, ndx ) => {
+
+			const speed = 1 + ndx * .1;
+			const rot = time * speed;
+			cube.rotation.x = rot;
+			cube.rotation.y = rot;
+
+		} );
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
+
 }

+ 191 - 154
manual/examples/shared-orbitcontrols.js

@@ -1,158 +1,195 @@
 import * as THREE from 'https://cdn.skypack.dev/[email protected]/build/three.module.js';
 import { OrbitControls } from 'https://cdn.skypack.dev/[email protected]/examples/jsm/controls/OrbitControls.js';
 
-export function init(data) {   /* eslint-disable-line no-unused-vars */
-  const {canvas, inputElement} = data;
-  const renderer = new THREE.WebGLRenderer({canvas});
-
-  const fov = 75;
-  const aspect = 2; // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 4;
-
-  const controls = new OrbitControls(camera, inputElement);
-  controls.target.set(0, 0, 0);
-  controls.update();
-
-  const scene = new THREE.Scene();
-
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(-1, 2, 4);
-    scene.add(light);
-  }
-
-  const boxWidth = 1;
-  const boxHeight = 1;
-  const boxDepth = 1;
-  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
-
-  function makeInstance(geometry, color, x) {
-    const material = new THREE.MeshPhongMaterial({
-      color,
-    });
-
-    const cube = new THREE.Mesh(geometry, material);
-    scene.add(cube);
-
-    cube.position.x = x;
-
-    return cube;
-  }
-
-  const cubes = [
-    makeInstance(geometry, 0x44aa88, 0),
-    makeInstance(geometry, 0x8844aa, -2),
-    makeInstance(geometry, 0xaa8844, 2),
-  ];
-
-  class PickHelper {
-    constructor() {
-      this.raycaster = new THREE.Raycaster();
-      this.pickedObject = null;
-      this.pickedObjectSavedColor = 0;
-    }
-    pick(normalizedPosition, scene, camera, time) {
-      // restore the color if there is a picked object
-      if (this.pickedObject) {
-        this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
-        this.pickedObject = undefined;
-      }
-
-      // cast a ray through the frustum
-      this.raycaster.setFromCamera(normalizedPosition, camera);
-      // get the list of objects the ray intersected
-      const intersectedObjects = this.raycaster.intersectObjects(scene.children);
-      if (intersectedObjects.length) {
-        // pick the first object. It's the closest one
-        this.pickedObject = intersectedObjects[0].object;
-        // save its color
-        this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
-        // set its emissive color to flashing red/yellow
-        this.pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000);
-      }
-    }
-  }
-
-  const pickPosition = {x: -2, y: -2};
-  const pickHelper = new PickHelper();
-  clearPickPosition();
-
-  function resizeRendererToDisplaySize(renderer) {
-    const canvas = renderer.domElement;
-    const width = inputElement.clientWidth;
-    const height = inputElement.clientHeight;
-    const needResize = canvas.width !== width || canvas.height !== height;
-    if (needResize) {
-      renderer.setSize(width, height, false);
-    }
-    return needResize;
-  }
-
-  function render(time) {
-    time *= 0.001;
-
-    if (resizeRendererToDisplaySize(renderer)) {
-      camera.aspect = inputElement.clientWidth / inputElement.clientHeight;
-      camera.updateProjectionMatrix();
-    }
-
-    cubes.forEach((cube, ndx) => {
-      const speed = 1 + ndx * .1;
-      const rot = time * speed;
-      cube.rotation.x = rot;
-      cube.rotation.y = rot;
-    });
-
-    pickHelper.pick(pickPosition, scene, camera, time);
-
-    renderer.render(scene, camera);
-
-    requestAnimationFrame(render);
-  }
-
-  requestAnimationFrame(render);
-
-  function getCanvasRelativePosition(event) {
-    const rect = inputElement.getBoundingClientRect();
-    return {
-      x: event.clientX - rect.left,
-      y: event.clientY - rect.top,
-    };
-  }
-
-  function setPickPosition(event) {
-    const pos = getCanvasRelativePosition(event);
-    pickPosition.x = (pos.x / inputElement.clientWidth ) *  2 - 1;
-    pickPosition.y = (pos.y / inputElement.clientHeight) * -2 + 1;  // note we flip Y
-  }
-
-  function clearPickPosition() {
-    // unlike the mouse which always has a position
-    // if the user stops touching the screen we want
-    // to stop picking. For now we just pick a value
-    // unlikely to pick something
-    pickPosition.x = -100000;
-    pickPosition.y = -100000;
-  }
-
-  inputElement.addEventListener('mousemove', setPickPosition);
-  inputElement.addEventListener('mouseout', clearPickPosition);
-  inputElement.addEventListener('mouseleave', clearPickPosition);
-
-  inputElement.addEventListener('touchstart', (event) => {
-    // prevent the window from scrolling
-    event.preventDefault();
-    setPickPosition(event.touches[0]);
-  }, {passive: false});
-
-  inputElement.addEventListener('touchmove', (event) => {
-    setPickPosition(event.touches[0]);
-  });
-
-  inputElement.addEventListener('touchend', clearPickPosition);
+export function init( data ) { /* eslint-disable-line no-unused-vars */
+
+	const { canvas, inputElement } = data;
+	const renderer = new THREE.WebGLRenderer( { canvas } );
+
+	const fov = 75;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 4;
+
+	const controls = new OrbitControls( camera, inputElement );
+	controls.target.set( 0, 0, 0 );
+	controls.update();
+
+	const scene = new THREE.Scene();
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 1;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( - 1, 2, 4 );
+		scene.add( light );
+
+	}
+
+	const boxWidth = 1;
+	const boxHeight = 1;
+	const boxDepth = 1;
+	const geometry = new THREE.BoxGeometry( boxWidth, boxHeight, boxDepth );
+
+	function makeInstance( geometry, color, x ) {
+
+		const material = new THREE.MeshPhongMaterial( {
+			color,
+		} );
+
+		const cube = new THREE.Mesh( geometry, material );
+		scene.add( cube );
+
+		cube.position.x = x;
+
+		return cube;
+
+	}
+
+	const cubes = [
+		makeInstance( geometry, 0x44aa88, 0 ),
+		makeInstance( geometry, 0x8844aa, - 2 ),
+		makeInstance( geometry, 0xaa8844, 2 ),
+	];
+
+	class PickHelper {
+
+		constructor() {
+
+			this.raycaster = new THREE.Raycaster();
+			this.pickedObject = null;
+			this.pickedObjectSavedColor = 0;
+
+		}
+		pick( normalizedPosition, scene, camera, time ) {
+
+			// restore the color if there is a picked object
+			if ( this.pickedObject ) {
+
+				this.pickedObject.material.emissive.setHex( this.pickedObjectSavedColor );
+				this.pickedObject = undefined;
+
+			}
+
+			// cast a ray through the frustum
+			this.raycaster.setFromCamera( normalizedPosition, camera );
+			// get the list of objects the ray intersected
+			const intersectedObjects = this.raycaster.intersectObjects( scene.children );
+			if ( intersectedObjects.length ) {
+
+				// pick the first object. It's the closest one
+				this.pickedObject = intersectedObjects[ 0 ].object;
+				// save its color
+				this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
+				// set its emissive color to flashing red/yellow
+				this.pickedObject.material.emissive.setHex( ( time * 8 ) % 2 > 1 ? 0xFFFF00 : 0xFF0000 );
+
+			}
+
+		}
+
+	}
+
+	const pickPosition = { x: - 2, y: - 2 };
+	const pickHelper = new PickHelper();
+	clearPickPosition();
+
+	function resizeRendererToDisplaySize( renderer ) {
+
+		const canvas = renderer.domElement;
+		const width = inputElement.clientWidth;
+		const height = inputElement.clientHeight;
+		const needResize = canvas.width !== width || canvas.height !== height;
+		if ( needResize ) {
+
+			renderer.setSize( width, height, false );
+
+		}
+
+		return needResize;
+
+	}
+
+	function render( time ) {
+
+		time *= 0.001;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			camera.aspect = inputElement.clientWidth / inputElement.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		cubes.forEach( ( cube, ndx ) => {
+
+			const speed = 1 + ndx * .1;
+			const rot = time * speed;
+			cube.rotation.x = rot;
+			cube.rotation.y = rot;
+
+		} );
+
+		pickHelper.pick( pickPosition, scene, camera, time );
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
+
+	function getCanvasRelativePosition( event ) {
+
+		const rect = inputElement.getBoundingClientRect();
+		return {
+			x: event.clientX - rect.left,
+			y: event.clientY - rect.top,
+		};
+
+	}
+
+	function setPickPosition( event ) {
+
+		const pos = getCanvasRelativePosition( event );
+		pickPosition.x = ( pos.x / inputElement.clientWidth ) * 2 - 1;
+		pickPosition.y = ( pos.y / inputElement.clientHeight ) * - 2 + 1; // note we flip Y
+
+	}
+
+	function clearPickPosition() {
+
+		// unlike the mouse which always has a position
+		// if the user stops touching the screen we want
+		// to stop picking. For now we just pick a value
+		// unlikely to pick something
+		pickPosition.x = - 100000;
+		pickPosition.y = - 100000;
+
+	}
+
+	inputElement.addEventListener( 'mousemove', setPickPosition );
+	inputElement.addEventListener( 'mouseout', clearPickPosition );
+	inputElement.addEventListener( 'mouseleave', clearPickPosition );
+
+	inputElement.addEventListener( 'touchstart', ( event ) => {
+
+		// prevent the window from scrolling
+		event.preventDefault();
+		setPickPosition( event.touches[ 0 ] );
+
+	}, { passive: false } );
+
+	inputElement.addEventListener( 'touchmove', ( event ) => {
+
+		setPickPosition( event.touches[ 0 ] );
+
+	} );
+
+	inputElement.addEventListener( 'touchend', clearPickPosition );
+
 }

+ 143 - 116
manual/examples/shared-picking.js

@@ -1,122 +1,149 @@
 import * as THREE from 'https://cdn.skypack.dev/[email protected]/build/three.module.js';
 
 export const state = {
-  width: 300,   // canvas default
-  height: 150,  // canvas default
+	width: 300, // canvas default
+	height: 150, // canvas default
 };
 
-export const pickPosition = {x: 0, y: 0};
-
-export function init(data) {  // eslint-disable-line no-unused-vars
-  const {canvas} = data;
-  const renderer = new THREE.WebGLRenderer({canvas});
-
-  state.width = canvas.width;
-  state.height = canvas.height;
-
-  const fov = 75;
-  const aspect = 2; // the canvas default
-  const near = 0.1;
-  const far = 100;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 4;
-
-  const scene = new THREE.Scene();
-
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(-1, 2, 4);
-    scene.add(light);
-  }
-
-  const boxWidth = 1;
-  const boxHeight = 1;
-  const boxDepth = 1;
-  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
-
-  function makeInstance(geometry, color, x) {
-    const material = new THREE.MeshPhongMaterial({
-      color,
-    });
-
-    const cube = new THREE.Mesh(geometry, material);
-    scene.add(cube);
-
-    cube.position.x = x;
-
-    return cube;
-  }
-
-  const cubes = [
-    makeInstance(geometry, 0x44aa88, 0),
-    makeInstance(geometry, 0x8844aa, -2),
-    makeInstance(geometry, 0xaa8844, 2),
-  ];
-
-  class PickHelper {
-    constructor() {
-      this.raycaster = new THREE.Raycaster();
-      this.pickedObject = null;
-      this.pickedObjectSavedColor = 0;
-    }
-    pick(normalizedPosition, scene, camera, time) {
-      // restore the color if there is a picked object
-      if (this.pickedObject) {
-        this.pickedObject.material.emissive.setHex(this.pickedObjectSavedColor);
-        this.pickedObject = undefined;
-      }
-
-      // cast a ray through the frustum
-      this.raycaster.setFromCamera(normalizedPosition, camera);
-      // get the list of objects the ray intersected
-      const intersectedObjects = this.raycaster.intersectObjects(scene.children);
-      if (intersectedObjects.length) {
-        // pick the first object. It's the closest one
-        this.pickedObject = intersectedObjects[0].object;
-        // save its color
-        this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
-        // set its emissive color to flashing red/yellow
-        this.pickedObject.material.emissive.setHex((time * 8) % 2 > 1 ? 0xFFFF00 : 0xFF0000);
-      }
-    }
-  }
-
-  const pickHelper = new PickHelper();
-
-  function resizeRendererToDisplaySize(renderer) {
-    const canvas = renderer.domElement;
-    const width = state.width;
-    const height = state.height;
-    const needResize = canvas.width !== width || canvas.height !== height;
-    if (needResize) {
-      renderer.setSize(width, height, false);
-    }
-    return needResize;
-  }
-
-  function render(time) {
-    time *= 0.001;
-
-    if (resizeRendererToDisplaySize(renderer)) {
-      camera.aspect = state.width / state.height;
-      camera.updateProjectionMatrix();
-    }
-
-    cubes.forEach((cube, ndx) => {
-      const speed = 1 + ndx * .1;
-      const rot = time * speed;
-      cube.rotation.x = rot;
-      cube.rotation.y = rot;
-    });
-
-    pickHelper.pick(pickPosition, scene, camera, time);
-
-    renderer.render(scene, camera);
-
-    requestAnimationFrame(render);
-  }
-
-  requestAnimationFrame(render);
+export const pickPosition = { x: 0, y: 0 };
+
+export function init( data ) { // eslint-disable-line no-unused-vars
+
+	const { canvas } = data;
+	const renderer = new THREE.WebGLRenderer( { canvas } );
+
+	state.width = canvas.width;
+	state.height = canvas.height;
+
+	const fov = 75;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 100;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 4;
+
+	const scene = new THREE.Scene();
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 1;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( - 1, 2, 4 );
+		scene.add( light );
+
+	}
+
+	const boxWidth = 1;
+	const boxHeight = 1;
+	const boxDepth = 1;
+	const geometry = new THREE.BoxGeometry( boxWidth, boxHeight, boxDepth );
+
+	function makeInstance( geometry, color, x ) {
+
+		const material = new THREE.MeshPhongMaterial( {
+			color,
+		} );
+
+		const cube = new THREE.Mesh( geometry, material );
+		scene.add( cube );
+
+		cube.position.x = x;
+
+		return cube;
+
+	}
+
+	const cubes = [
+		makeInstance( geometry, 0x44aa88, 0 ),
+		makeInstance( geometry, 0x8844aa, - 2 ),
+		makeInstance( geometry, 0xaa8844, 2 ),
+	];
+
+	class PickHelper {
+
+		constructor() {
+
+			this.raycaster = new THREE.Raycaster();
+			this.pickedObject = null;
+			this.pickedObjectSavedColor = 0;
+
+		}
+		pick( normalizedPosition, scene, camera, time ) {
+
+			// restore the color if there is a picked object
+			if ( this.pickedObject ) {
+
+				this.pickedObject.material.emissive.setHex( this.pickedObjectSavedColor );
+				this.pickedObject = undefined;
+
+			}
+
+			// cast a ray through the frustum
+			this.raycaster.setFromCamera( normalizedPosition, camera );
+			// get the list of objects the ray intersected
+			const intersectedObjects = this.raycaster.intersectObjects( scene.children );
+			if ( intersectedObjects.length ) {
+
+				// pick the first object. It's the closest one
+				this.pickedObject = intersectedObjects[ 0 ].object;
+				// save its color
+				this.pickedObjectSavedColor = this.pickedObject.material.emissive.getHex();
+				// set its emissive color to flashing red/yellow
+				this.pickedObject.material.emissive.setHex( ( time * 8 ) % 2 > 1 ? 0xFFFF00 : 0xFF0000 );
+
+			}
+
+		}
+
+	}
+
+	const pickHelper = new PickHelper();
+
+	function resizeRendererToDisplaySize( renderer ) {
+
+		const canvas = renderer.domElement;
+		const width = state.width;
+		const height = state.height;
+		const needResize = canvas.width !== width || canvas.height !== height;
+		if ( needResize ) {
+
+			renderer.setSize( width, height, false );
+
+		}
+
+		return needResize;
+
+	}
+
+	function render( time ) {
+
+		time *= 0.001;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			camera.aspect = state.width / state.height;
+			camera.updateProjectionMatrix();
+
+		}
+
+		cubes.forEach( ( cube, ndx ) => {
+
+			const speed = 1 + ndx * .1;
+			const rot = time * speed;
+			cube.rotation.x = rot;
+			cube.rotation.y = rot;
+
+		} );
+
+		pickHelper.pick( pickPosition, scene, camera, time );
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
+
 }

+ 92 - 75
manual/examples/threejs-responsive.js

@@ -1,81 +1,98 @@
 import * as THREE from 'three';
 
 function main() {
-  const canvas = document.querySelector('#c');
-  const renderer = new THREE.WebGLRenderer({canvas});
-
-  const fov = 75;
-  const aspect = 2;  // the canvas default
-  const near = 0.1;
-  const far = 5;
-  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
-  camera.position.z = 2;
-
-  const scene = new THREE.Scene();
-
-  {
-    const color = 0xFFFFFF;
-    const intensity = 1;
-    const light = new THREE.DirectionalLight(color, intensity);
-    light.position.set(-1, 2, 4);
-    scene.add(light);
-  }
-
-  const boxWidth = 1;
-  const boxHeight = 1;
-  const boxDepth = 1;
-  const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
-
-  function makeInstance(geometry, color, x) {
-    const material = new THREE.MeshPhongMaterial({color});
-
-    const cube = new THREE.Mesh(geometry, material);
-    scene.add(cube);
-
-    cube.position.x = x;
-
-    return cube;
-  }
-
-  const cubes = [
-    makeInstance(geometry, 0x44aa88,  0),
-    makeInstance(geometry, 0x8844aa, -2),
-    makeInstance(geometry, 0xaa8844,  2),
-  ];
-
-  function resizeRendererToDisplaySize(renderer) {
-    const canvas = renderer.domElement;
-    const width = canvas.clientWidth;
-    const height = canvas.clientHeight;
-    const needResize = canvas.width !== width || canvas.height !== height;
-    if (needResize) {
-      renderer.setSize(width, height, false);
-    }
-    return needResize;
-  }
-
-  function render(time) {
-    time *= 0.001;
-
-    if (resizeRendererToDisplaySize(renderer)) {
-      const canvas = renderer.domElement;
-      camera.aspect = canvas.clientWidth / canvas.clientHeight;
-      camera.updateProjectionMatrix();
-    }
-
-    cubes.forEach((cube, ndx) => {
-      const speed = 1 + ndx * .1;
-      const rot = time * speed;
-      cube.rotation.x = rot;
-      cube.rotation.y = rot;
-    });
-
-    renderer.render(scene, camera);
-
-    requestAnimationFrame(render);
-  }
-
-  requestAnimationFrame(render);
+
+	const canvas = document.querySelector( '#c' );
+	const renderer = new THREE.WebGLRenderer( { canvas } );
+
+	const fov = 75;
+	const aspect = 2; // the canvas default
+	const near = 0.1;
+	const far = 5;
+	const camera = new THREE.PerspectiveCamera( fov, aspect, near, far );
+	camera.position.z = 2;
+
+	const scene = new THREE.Scene();
+
+	{
+
+		const color = 0xFFFFFF;
+		const intensity = 1;
+		const light = new THREE.DirectionalLight( color, intensity );
+		light.position.set( - 1, 2, 4 );
+		scene.add( light );
+
+	}
+
+	const boxWidth = 1;
+	const boxHeight = 1;
+	const boxDepth = 1;
+	const geometry = new THREE.BoxGeometry( boxWidth, boxHeight, boxDepth );
+
+	function makeInstance( geometry, color, x ) {
+
+		const material = new THREE.MeshPhongMaterial( { color } );
+
+		const cube = new THREE.Mesh( geometry, material );
+		scene.add( cube );
+
+		cube.position.x = x;
+
+		return cube;
+
+	}
+
+	const cubes = [
+		makeInstance( geometry, 0x44aa88, 0 ),
+		makeInstance( geometry, 0x8844aa, - 2 ),
+		makeInstance( geometry, 0xaa8844, 2 ),
+	];
+
+	function resizeRendererToDisplaySize( renderer ) {
+
+		const canvas = renderer.domElement;
+		const width = canvas.clientWidth;
+		const height = canvas.clientHeight;
+		const needResize = canvas.width !== width || canvas.height !== height;
+		if ( needResize ) {
+
+			renderer.setSize( width, height, false );
+
+		}
+
+		return needResize;
+
+	}
+
+	function render( time ) {
+
+		time *= 0.001;
+
+		if ( resizeRendererToDisplaySize( renderer ) ) {
+
+			const canvas = renderer.domElement;
+			camera.aspect = canvas.clientWidth / canvas.clientHeight;
+			camera.updateProjectionMatrix();
+
+		}
+
+		cubes.forEach( ( cube, ndx ) => {
+
+			const speed = 1 + ndx * .1;
+			const rot = time * speed;
+			cube.rotation.x = rot;
+			cube.rotation.y = rot;
+
+		} );
+
+		renderer.render( scene, camera );
+
+		requestAnimationFrame( render );
+
+	}
+
+	requestAnimationFrame( render );
+
 }
 
 main();

+ 165 - 114
manual/resources/canvas-wrapper.js

@@ -32,121 +32,172 @@
 /* global define */
 
 (function(root, factory) {  // eslint-disable-line
-  if (typeof define === 'function' && define.amd) {
-    // AMD. Register as an anonymous module.
-    define(['./transform'], factory);
-  } else {
-    // Browser globals
-    const lib = factory();
-    root.wrapCanvasRenderingContext2D = lib.wrap;
-  }
-}(this, function() {
+	if ( typeof define === 'function' && define.amd ) {
+
+		// AMD. Register as an anonymous module.
+		define( [ './transform' ], factory );
+
+	} else {
+
+		// Browser globals
+		const lib = factory();
+		root.wrapCanvasRenderingContext2D = lib.wrap;
+
+	}
+
+}( this, function () {
+
   'use strict';  // eslint-disable-line
 
-  function duplicate(src) {
-    const d = new window.DOMMatrix();
-    d.a = src.a;
-    d.b = src.b;
-    d.c = src.c;
-    d.d = src.d;
-    d.e = src.e;
-    d.f = src.f;
-    return d;
-  }
-
-  function patchCurrentTransform(ctx) {
-
-    if (ctx.currentTransform) {
-      return ctx;
-    }
-
-    const stack = [];
-
-    ctx.scale = function(scale) {
-      return function(x, y) {
-        ctx.currentTransform.scaleSelf(x, y);
-        scale(x, y);
-      };
-    }(ctx.scale.bind(ctx));
-
-    ctx.rotate = function(rotate) {
-      return function(r) {
-        ctx.currentTransform.rotateSelf(r * 180 / Math.PI);
-        rotate(r);
-      };
-    }(ctx.rotate.bind(ctx));
-
-    ctx.translate = function(translate) {
-      return function(x, y) {
-        ctx.currentTransform.translateSelf(x, y);
-        translate(x, y);
-      };
-    }(ctx.translate.bind(ctx));
-
-    ctx.save = function(save) {
-      return function() {
-        stack.push(duplicate(ctx.currentTransform));
-        save();
-      };
-    }(ctx.save.bind(ctx));
-
-    ctx.restore = function(restore) {
-      return function() {
-        if (stack.length) {
-          ctx.currentTransform = stack.pop();
-        } else {
-          throw new Error('"transform stack empty!');
-        }
-        restore();
-      };
-    }(ctx.restore.bind(ctx));
-
-    ctx.transform = function(transform) {
-      return function(m11, m12, m21, m22, dx, dy) {
-        const m = new DOMMatrix();
-        m.a = m11;
-        m.b = m12;
-        m.c = m21;
-        m.d = m22;
-        m.e = dx;
-        m.f = dy;
-        ctx.currentTransform.multiplySelf(m);
-        transform(m11, m12, m21, m22, dx, dy);
-      };
-    }(ctx.transform.bind(ctx));
-
-    ctx.setTransform = function(setTransform) {
-      return function(m11, m12, m21, m22, dx, dy) {
-        const d = ctx.currentTransform;
-        d.a = m11;
-        d.b = m12;
-        d.c = m21;
-        d.d = m22;
-        d.e = dx;
-        d.f = dy;
-        setTransform(m11, m12, m21, m22, dx, dy);
-      };
-    }(ctx.setTransform.bind(ctx));
-
-    ctx.currentTransform = new DOMMatrix();
-
-    ctx.validateTransformStack = function() {
-      if (stack.length !== 0) {
-        throw new Error('transform stack not 0');
-      }
-    };
-
-    return ctx;
-  }
-
-  function wrap(ctx) {
-    //patchDOMMatrix();
-    return patchCurrentTransform(ctx);
-  }
-
-  return {
-    wrap: wrap,
-  };
-}));
+	function duplicate( src ) {
+
+		const d = new window.DOMMatrix();
+		d.a = src.a;
+		d.b = src.b;
+		d.c = src.c;
+		d.d = src.d;
+		d.e = src.e;
+		d.f = src.f;
+		return d;
+
+	}
+
+	function patchCurrentTransform( ctx ) {
+
+		if ( ctx.currentTransform ) {
+
+			return ctx;
+
+		}
+
+		const stack = [];
+
+		ctx.scale = function ( scale ) {
+
+			return function ( x, y ) {
+
+				ctx.currentTransform.scaleSelf( x, y );
+				scale( x, y );
+
+			};
+
+		}( ctx.scale.bind( ctx ) );
+
+		ctx.rotate = function ( rotate ) {
+
+			return function ( r ) {
+
+				ctx.currentTransform.rotateSelf( r * 180 / Math.PI );
+				rotate( r );
+
+			};
+
+		}( ctx.rotate.bind( ctx ) );
+
+		ctx.translate = function ( translate ) {
+
+			return function ( x, y ) {
+
+				ctx.currentTransform.translateSelf( x, y );
+				translate( x, y );
+
+			};
+
+		}( ctx.translate.bind( ctx ) );
+
+		ctx.save = function ( save ) {
+
+			return function () {
+
+				stack.push( duplicate( ctx.currentTransform ) );
+				save();
+
+			};
+
+		}( ctx.save.bind( ctx ) );
+
+		ctx.restore = function ( restore ) {
+
+			return function () {
+
+				if ( stack.length ) {
+
+					ctx.currentTransform = stack.pop();
+
+				} else {
+
+					throw new Error( '"transform stack empty!' );
+
+				}
+
+				restore();
+
+			};
+
+		}( ctx.restore.bind( ctx ) );
+
+		ctx.transform = function ( transform ) {
+
+			return function ( m11, m12, m21, m22, dx, dy ) {
+
+				const m = new DOMMatrix();
+				m.a = m11;
+				m.b = m12;
+				m.c = m21;
+				m.d = m22;
+				m.e = dx;
+				m.f = dy;
+				ctx.currentTransform.multiplySelf( m );
+				transform( m11, m12, m21, m22, dx, dy );
+
+			};
+
+		}( ctx.transform.bind( ctx ) );
+
+		ctx.setTransform = function ( setTransform ) {
+
+			return function ( m11, m12, m21, m22, dx, dy ) {
+
+				const d = ctx.currentTransform;
+				d.a = m11;
+				d.b = m12;
+				d.c = m21;
+				d.d = m22;
+				d.e = dx;
+				d.f = dy;
+				setTransform( m11, m12, m21, m22, dx, dy );
+
+			};
+
+		}( ctx.setTransform.bind( ctx ) );
+
+		ctx.currentTransform = new DOMMatrix();
+
+		ctx.validateTransformStack = function () {
+
+			if ( stack.length !== 0 ) {
+
+				throw new Error( 'transform stack not 0' );
+
+			}
+
+		};
+
+		return ctx;
+
+	}
+
+	function wrap( ctx ) {
+
+		//patchDOMMatrix();
+		return patchCurrentTransform( ctx );
+
+	}
+
+	return {
+		wrap: wrap,
+	};
+
+} ) );
 
 

+ 68 - 46
manual/resources/lesson.js

@@ -2,51 +2,73 @@
 /* eslint-disable strict */
 'use strict';  // eslint-disable-line
 
-(function(){
-
-  if (window.frameElement) {
-    // in iframe
-    document.querySelectorAll('a').forEach(a => {
-      // we have to send all links to the parent
-      // otherwise we'll end up with 3rd party
-      // sites under the frame.
-      a.addEventListener('click', e => {
-        // opening a new tab?
-        if (a.target === '_blank') {
-          return;
-        }
-        // change changing hashes?
-        if (a.origin !== window.location.origin || a.pathname !== window.location.pathname) {
-          e.preventDefault();
-        }
-        window.parent.setUrl(a.href);
-      });
-    });
-    window.parent.setTitle(document.title);
-  } else {
-    if (window.location.protocol !== 'file:') {
-      const re = /^(.*?\/manual\/)(.*?)$/;
-      const [,baseURL, articlePath] = re.exec(window.location.href);
-      const href = `${baseURL}#${articlePath.replace('.html', '')}`;
-      window.location.replace(href);  // lgtm[js/client-side-unvalidated-url-redirection]
-    }
-  }
-
-  if (window.prettyPrint) {
-    window.prettyPrint();
-  }
-
-  // help translation services translate comments.
-  document.querySelectorAll('span[class=com]').forEach(elem => {
-    elem.classList.add('translate', 'yestranslate');
-    elem.setAttribute('translate', 'yes');
-  });
-
-  if (window.threejsLessonUtils) {
-    window.threejsLessonUtils.afterPrettify();
-  }
-
-}());
+( function () {
+
+	if ( window.frameElement ) {
+
+		// in iframe
+		document.querySelectorAll( 'a' ).forEach( a => {
+
+			// we have to send all links to the parent
+			// otherwise we'll end up with 3rd party
+			// sites under the frame.
+			a.addEventListener( 'click', e => {
+
+				// opening a new tab?
+				if ( a.target === '_blank' ) {
+
+					return;
+
+				}
+
+				// change changing hashes?
+				if ( a.origin !== window.location.origin || a.pathname !== window.location.pathname ) {
+
+					e.preventDefault();
+
+				}
+
+				window.parent.setUrl( a.href );
+
+			} );
+
+		} );
+		window.parent.setTitle( document.title );
+
+	} else {
+
+		if ( window.location.protocol !== 'file:' ) {
+
+			const re = /^(.*?\/manual\/)(.*?)$/;
+			const [ , baseURL, articlePath ] = re.exec( window.location.href );
+			const href = `${baseURL}#${articlePath.replace( '.html', '' )}`;
+			window.location.replace( href ); // lgtm[js/client-side-unvalidated-url-redirection]
+
+		}
+
+	}
+
+	if ( window.prettyPrint ) {
+
+		window.prettyPrint();
+
+	}
+
+	// help translation services translate comments.
+	document.querySelectorAll( 'span[class=com]' ).forEach( elem => {
+
+		elem.classList.add( 'translate', 'yestranslate' );
+		elem.setAttribute( 'translate', 'yes' );
+
+	} );
+
+	if ( window.threejsLessonUtils ) {
+
+		window.threejsLessonUtils.afterPrettify();
+
+	}
+
+}() );
 
 // ios needs this to allow touch events in an iframe
-window.addEventListener('touchstart', {});
+window.addEventListener( 'touchstart', {} );

+ 364 - 279
manual/resources/threejs-align-html-elements-to-3d.js

@@ -1,284 +1,369 @@
-import {GUI} from '../../examples/jsm/libs/lil-gui.module.min.js';
+import { GUI } from '../../examples/jsm/libs/lil-gui.module.min.js';
 
 {
-  function outlineText(ctx, msg, x, y) {
-    ctx.strokeText(msg, x, y);
-    ctx.fillText(msg, x, y);
-  }
-
-  function arrow(ctx, x1, y1, x2, y2, start, end, size) {
-    size = size || 1;
-    const dx = x1 - x2;
-    const dy = y1 - y2;
-    const rot = -Math.atan2(dx, dy);
-    const len = Math.sqrt(dx * dx + dy * dy);
-    ctx.save();
-    {
-      ctx.translate(x1, y1);
-      ctx.rotate(rot);
-      ctx.beginPath();
-      ctx.moveTo(0, 0);
-      ctx.lineTo(0, -(len - 10 * size));
-      ctx.stroke();
-    }
-    ctx.restore();
-    if (start) {
-      arrowHead(ctx, x1, y1, rot, size);
-    }
-    if (end) {
-      arrowHead(ctx, x2, y2, rot + Math.PI, size);
-    }
-  }
-
-  function arrowHead(ctx, x, y, rot, size) {
-    ctx.save();
-    {
-      ctx.translate(x, y);
-      ctx.rotate(rot);
-      ctx.scale(size, size);
-      ctx.translate(0, -10);
-      ctx.beginPath();
-      ctx.moveTo(0, 0);
-      ctx.lineTo(-5, -2);
-      ctx.lineTo(0,  10);
-      ctx.lineTo(5, -2);
-      ctx.closePath();
-      ctx.fill();
-    }
-    ctx.restore();
-  }
-
-  const THREE = {
-    MathUtils: {
-      radToDeg(rad) {
-        return rad * 180 / Math.PI;
-      },
-      degToRad(deg) {
-        return deg * Math.PI / 180;
-      },
-    },
-  };
-
-  class DegRadHelper {
-    constructor(obj, prop) {
-      this.obj = obj;
-      this.prop = prop;
-    }
-    get value() {
-      return THREE.MathUtils.radToDeg(this.obj[this.prop]);
-    }
-    set value(v) {
-      this.obj[this.prop] = THREE.MathUtils.degToRad(v);
-    }
-  }
-
-  function dot(x1, y1, x2, y2) {
-    return x1 * x2 + y1 * y2;
-  }
-
-  function distance(x1, y1, x2, y2) {
-    const dx = x1 - x2;
-    const dy = y1 - y2;
-    return Math.sqrt(dx * dx + dy * dy);
-  }
-
-  function normalize(x, y) {
-    const l = distance(0, 0, x, y);
-    if (l > 0.00001) {
-      return [x / l, y / l];
-    } else {
-      return [0, 0];
-    }
-  }
-
-  function resizeCanvasToDisplaySize(canvas, pixelRatio = 1) {
-    const width  = canvas.clientWidth  * pixelRatio | 0;
-    const height = canvas.clientHeight * pixelRatio | 0;
-    const needResize = canvas.width !== width || canvas.height !== height;
-    if (needResize) {
-      canvas.width = width;
-      canvas.height = height;
-    }
-    return needResize;
-  }
-
-  const diagrams = {
-    dotProduct: {
-      create(info) {
-        const {elem} = info;
-        const div = document.createElement('div');
-        div.style.position = 'relative';
-        div.style.width = '100%';
-        div.style.height = '100%';
-        elem.appendChild(div);
-
-        const ctx = document.createElement('canvas').getContext('2d');
-        div.appendChild(ctx.canvas);
-        const settings = {
-          rotation: 0.3,
-        };
-
-        const gui = new GUI({autoPlace: false});
-        gui.add(new DegRadHelper(settings, 'rotation'), 'value', -180, 180).name('rotation').onChange(render);
-        gui.domElement.style.position = 'absolute';
-        gui.domElement.style.top = '0';
-        gui.domElement.style.right = '0';
-        div.appendChild(gui.domElement);
-
-        const darkColors = {
-          globe: 'green',
-          camera: '#AAA',
-          base: '#DDD',
-          label: '#0FF',
-        };
-        const lightColors = {
-          globe: '#0C0',
-          camera: 'black',
-          base: '#000',
-          label: 'blue',
-        };
-
-        const darkMatcher = window.matchMedia('(prefers-color-scheme: dark)');
-        darkMatcher.addEventListener('change', render);
-
-        function render() {
-          const {rotation} = settings;
-          const isDarkMode = darkMatcher.matches;
-          const colors = isDarkMode ? darkColors : lightColors;
-
-          const pixelRatio = window.devicePixelRatio;
-          resizeCanvasToDisplaySize(ctx.canvas, pixelRatio);
-
-          ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
-          ctx.save();
-          {
-            const width = ctx.canvas.width / pixelRatio;
-            const height = ctx.canvas.height / pixelRatio;
-            const min = Math.min(width, height);
-            const half = min / 2;
-
-            const r = half * 0.4;
-            const x = r * Math.sin(-rotation);
-            const y = r * Math.cos(-rotation);
-
-            const camDX = x - 0;
-            const camDY = y - (half - 40);
-
-            const labelDir = normalize(x, y);
-            const camToLabelDir = normalize(camDX, camDY);
-
-            const dp = dot(...camToLabelDir, ...labelDir);
-
-            ctx.scale(pixelRatio, pixelRatio);
-            ctx.save();
-            {
-              {
-                ctx.translate(width / 2, height / 2);
-                ctx.beginPath();
-                ctx.arc(0, 0, half * 0.4, 0, Math.PI * 2);
-                ctx.fillStyle = colors.globe;
-                ctx.fill();
-
-                ctx.save();
-                {
-                  ctx.fillStyle = colors.camera;
-                  ctx.translate(0, half);
-                  ctx.fillRect(-15, -30, 30, 30);
-                  ctx.beginPath();
-                  ctx.moveTo(0, -25);
-                  ctx.lineTo(-25, -50);
-                  ctx.lineTo( 25, -50);
-                  ctx.closePath();
-                  ctx.fill();
-                }
-                ctx.restore();
-
-                ctx.save();
-                {
-                  ctx.lineWidth = 4;
-                  ctx.strokeStyle = colors.camera;
-                  ctx.fillStyle = colors.camera;
-                  arrow(ctx, 0, half - 40, x, y, false, true, 2);
-
-                  ctx.save();
-                  {
-                    ctx.strokeStyle = colors.label;
-                    ctx.fillStyle = colors.label;
-                    arrow(ctx, 0, 0, x, y, false, true, 2);
-                  }
-                  ctx.restore();
-
-                  {
-                    ctx.lineWidth = 3;
-                    ctx.strokeStyle = 'black';
-                    ctx.fillStyle = dp < 0 ? 'white' : 'red';
-                    ctx.font = '20px sans-serif';
-                    ctx.textAlign = 'center';
-                    ctx.textBaseline = 'middle';
-                    outlineText(ctx, 'label', x, y);
-                  }
-                }
-                ctx.restore();
-
-              }
-              ctx.restore();
-            }
-
-            ctx.lineWidth = 3;
-            ctx.font = '24px sans-serif';
-            ctx.strokeStyle = 'black';
-            ctx.textAlign = 'left';
-            ctx.textBaseline = 'middle';
-            ctx.save();
-            {
-              ctx.translate(width / 4, 80);
-              const textColor = dp < 0 ? colors.base : 'red';
-              advanceText(ctx, textColor, 'dot( ');
-              ctx.save();
-              {
-                ctx.fillStyle = colors.camera;
-                ctx.strokeStyle = colors.camera;
-                ctx.rotate(Math.atan2(camDY, camDX));
-                arrow(ctx, -8, 0, 8, 0, false, true, 1);
-              }
-              ctx.restore();
-              advanceText(ctx, textColor, ' ,  ');
-              ctx.save();
-              {
-                ctx.fillStyle = colors.label;
-                ctx.strokeStyle = colors.label;
-                ctx.rotate(rotation + Math.PI * 0.5);
-                arrow(ctx, -8, 0, 8, 0, false, true, 1);
-              }
-              ctx.restore();
-              advanceText(ctx, textColor, ` ) = ${dp.toFixed(2)}`);
-            }
-            ctx.restore();
-          }
-          ctx.restore();
-        }
-        render();
-        window.addEventListener('resize', render);
-      },
-    },
-  };
-
-  function advanceText(ctx, color, str) {
-    ctx.fillStyle = color;
-    ctx.fillText(str, 0, 0);
-    ctx.translate(ctx.measureText(str).width, 0);
-  }
-
-  [...document.querySelectorAll('[data-diagram]')].forEach(createDiagram);
-
-  function createDiagram(base) {
-    const name = base.dataset.diagram;
-    const info = diagrams[name];
-    if (!info) {
-      throw new Error(`no diagram ${name}`);
-    }
-    info.create({elem:base});
-  }
+
+	function outlineText( ctx, msg, x, y ) {
+
+		ctx.strokeText( msg, x, y );
+		ctx.fillText( msg, x, y );
+
+	}
+
+	function arrow( ctx, x1, y1, x2, y2, start, end, size ) {
+
+		size = size || 1;
+		const dx = x1 - x2;
+		const dy = y1 - y2;
+		const rot = - Math.atan2( dx, dy );
+		const len = Math.sqrt( dx * dx + dy * dy );
+		ctx.save();
+		{
+
+			ctx.translate( x1, y1 );
+			ctx.rotate( rot );
+			ctx.beginPath();
+			ctx.moveTo( 0, 0 );
+			ctx.lineTo( 0, - ( len - 10 * size ) );
+			ctx.stroke();
+
+		}
+
+		ctx.restore();
+		if ( start ) {
+
+			arrowHead( ctx, x1, y1, rot, size );
+
+		}
+
+		if ( end ) {
+
+			arrowHead( ctx, x2, y2, rot + Math.PI, size );
+
+		}
+
+	}
+
+	function arrowHead( ctx, x, y, rot, size ) {
+
+		ctx.save();
+		{
+
+			ctx.translate( x, y );
+			ctx.rotate( rot );
+			ctx.scale( size, size );
+			ctx.translate( 0, - 10 );
+			ctx.beginPath();
+			ctx.moveTo( 0, 0 );
+			ctx.lineTo( - 5, - 2 );
+			ctx.lineTo( 0, 10 );
+			ctx.lineTo( 5, - 2 );
+			ctx.closePath();
+			ctx.fill();
+
+		}
+
+		ctx.restore();
+
+	}
+
+	const THREE = {
+		MathUtils: {
+			radToDeg( rad ) {
+
+				return rad * 180 / Math.PI;
+
+			},
+			degToRad( deg ) {
+
+				return deg * Math.PI / 180;
+
+			},
+		},
+	};
+
+	class DegRadHelper {
+
+		constructor( obj, prop ) {
+
+			this.obj = obj;
+			this.prop = prop;
+
+		}
+		get value() {
+
+			return THREE.MathUtils.radToDeg( this.obj[ this.prop ] );
+
+		}
+		set value( v ) {
+
+			this.obj[ this.prop ] = THREE.MathUtils.degToRad( v );
+
+		}
+
+	}
+
+	function dot( x1, y1, x2, y2 ) {
+
+		return x1 * x2 + y1 * y2;
+
+	}
+
+	function distance( x1, y1, x2, y2 ) {
+
+		const dx = x1 - x2;
+		const dy = y1 - y2;
+		return Math.sqrt( dx * dx + dy * dy );
+
+	}
+
+	function normalize( x, y ) {
+
+		const l = distance( 0, 0, x, y );
+		if ( l > 0.00001 ) {
+
+			return [ x / l, y / l ];
+
+		} else {
+
+			return [ 0, 0 ];
+
+		}
+
+	}
+
+	function resizeCanvasToDisplaySize( canvas, pixelRatio = 1 ) {
+
+		const width = canvas.clientWidth * pixelRatio | 0;
+		const height = canvas.clientHeight * pixelRatio | 0;
+		const needResize = canvas.width !== width || canvas.height !== height;
+		if ( needResize ) {
+
+			canvas.width = width;
+			canvas.height = height;
+
+		}
+
+		return needResize;
+
+	}
+
+	const diagrams = {
+		dotProduct: {
+			create( info ) {
+
+				const { elem } = info;
+				const div = document.createElement( 'div' );
+				div.style.position = 'relative';
+				div.style.width = '100%';
+				div.style.height = '100%';
+				elem.appendChild( div );
+
+				const ctx = document.createElement( 'canvas' ).getContext( '2d' );
+				div.appendChild( ctx.canvas );
+				const settings = {
+					rotation: 0.3,
+				};
+
+				const gui = new GUI( { autoPlace: false } );
+				gui.add( new DegRadHelper( settings, 'rotation' ), 'value', - 180, 180 ).name( 'rotation' ).onChange( render );
+				gui.domElement.style.position = 'absolute';
+				gui.domElement.style.top = '0';
+				gui.domElement.style.right = '0';
+				div.appendChild( gui.domElement );
+
+				const darkColors = {
+					globe: 'green',
+					camera: '#AAA',
+					base: '#DDD',
+					label: '#0FF',
+				};
+				const lightColors = {
+					globe: '#0C0',
+					camera: 'black',
+					base: '#000',
+					label: 'blue',
+				};
+
+				const darkMatcher = window.matchMedia( '(prefers-color-scheme: dark)' );
+				darkMatcher.addEventListener( 'change', render );
+
+				function render() {
+
+					const { rotation } = settings;
+					const isDarkMode = darkMatcher.matches;
+					const colors = isDarkMode ? darkColors : lightColors;
+
+					const pixelRatio = window.devicePixelRatio;
+					resizeCanvasToDisplaySize( ctx.canvas, pixelRatio );
+
+					ctx.clearRect( 0, 0, ctx.canvas.width, ctx.canvas.height );
+					ctx.save();
+					{
+
+						const width = ctx.canvas.width / pixelRatio;
+						const height = ctx.canvas.height / pixelRatio;
+						const min = Math.min( width, height );
+						const half = min / 2;
+
+						const r = half * 0.4;
+						const x = r * Math.sin( - rotation );
+						const y = r * Math.cos( - rotation );
+
+						const camDX = x - 0;
+						const camDY = y - ( half - 40 );
+
+						const labelDir = normalize( x, y );
+						const camToLabelDir = normalize( camDX, camDY );
+
+						const dp = dot( ...camToLabelDir, ...labelDir );
+
+						ctx.scale( pixelRatio, pixelRatio );
+						ctx.save();
+						{
+
+							{
+
+								ctx.translate( width / 2, height / 2 );
+								ctx.beginPath();
+								ctx.arc( 0, 0, half * 0.4, 0, Math.PI * 2 );
+								ctx.fillStyle = colors.globe;
+								ctx.fill();
+
+								ctx.save();
+								{
+
+									ctx.fillStyle = colors.camera;
+									ctx.translate( 0, half );
+									ctx.fillRect( - 15, - 30, 30, 30 );
+									ctx.beginPath();
+									ctx.moveTo( 0, - 25 );
+									ctx.lineTo( - 25, - 50 );
+									ctx.lineTo( 25, - 50 );
+									ctx.closePath();
+									ctx.fill();
+
+								}
+
+								ctx.restore();
+
+								ctx.save();
+								{
+
+									ctx.lineWidth = 4;
+									ctx.strokeStyle = colors.camera;
+									ctx.fillStyle = colors.camera;
+									arrow( ctx, 0, half - 40, x, y, false, true, 2 );
+
+									ctx.save();
+									{
+
+										ctx.strokeStyle = colors.label;
+										ctx.fillStyle = colors.label;
+										arrow( ctx, 0, 0, x, y, false, true, 2 );
+
+									}
+
+									ctx.restore();
+
+									{
+
+										ctx.lineWidth = 3;
+										ctx.strokeStyle = 'black';
+										ctx.fillStyle = dp < 0 ? 'white' : 'red';
+										ctx.font = '20px sans-serif';
+										ctx.textAlign = 'center';
+										ctx.textBaseline = 'middle';
+										outlineText( ctx, 'label', x, y );
+
+									}
+
+								}
+
+								ctx.restore();
+
+							}
+
+							ctx.restore();
+
+						}
+
+						ctx.lineWidth = 3;
+						ctx.font = '24px sans-serif';
+						ctx.strokeStyle = 'black';
+						ctx.textAlign = 'left';
+						ctx.textBaseline = 'middle';
+						ctx.save();
+						{
+
+							ctx.translate( width / 4, 80 );
+							const textColor = dp < 0 ? colors.base : 'red';
+							advanceText( ctx, textColor, 'dot( ' );
+							ctx.save();
+							{
+
+								ctx.fillStyle = colors.camera;
+								ctx.strokeStyle = colors.camera;
+								ctx.rotate( Math.atan2( camDY, camDX ) );
+								arrow( ctx, - 8, 0, 8, 0, false, true, 1 );
+
+							}
+
+							ctx.restore();
+							advanceText( ctx, textColor, ' ,  ' );
+							ctx.save();
+							{
+
+								ctx.fillStyle = colors.label;
+								ctx.strokeStyle = colors.label;
+								ctx.rotate( rotation + Math.PI * 0.5 );
+								arrow( ctx, - 8, 0, 8, 0, false, true, 1 );
+
+							}
+
+							ctx.restore();
+							advanceText( ctx, textColor, ` ) = ${dp.toFixed( 2 )}` );
+
+						}
+
+						ctx.restore();
+
+					}
+
+					ctx.restore();
+
+				}
+
+				render();
+				window.addEventListener( 'resize', render );
+
+			},
+		},
+	};
+
+	function advanceText( ctx, color, str ) {
+
+		ctx.fillStyle = color;
+		ctx.fillText( str, 0, 0 );
+		ctx.translate( ctx.measureText( str ).width, 0 );
+
+	}
+
+	[ ...document.querySelectorAll( '[data-diagram]' ) ].forEach( createDiagram );
+
+	function createDiagram( base ) {
+
+		const name = base.dataset.diagram;
+		const info = diagrams[ name ];
+		if ( ! info ) {
+
+			throw new Error( `no diagram ${name}` );
+
+		}
+
+		info.create( { elem: base } );
+
+	}
+
 }
 
 

+ 81 - 67
manual/resources/threejs-cameras.js

@@ -1,70 +1,84 @@
 import * as THREE from 'three';
-import {threejsLessonUtils} from './threejs-lesson-utils.js';
+import { threejsLessonUtils } from './threejs-lesson-utils.js';
 
 {
-  function addShape(color, geometry) {
-    const material = new THREE.MeshPhongMaterial({color});
-    return new THREE.Mesh(geometry, material);
-  }
-
-  threejsLessonUtils.addDiagrams({
-    shapeCube: {
-      create() {
-        const width = 8;
-        const height = 8;
-        const depth = 8;
-        return addShape('hsl(150,100%,40%)', new THREE.BoxGeometry(width, height, depth));
-      },
-    },
-    shapeCone: {
-      create() {
-        const radius = 6;
-        const height = 8;
-        const segments = 24;
-        return addShape('hsl(160,100%,40%)', new THREE.ConeGeometry(radius, height, segments));
-      },
-    },
-    shapeCylinder: {
-      create() {
-        const radiusTop = 4;
-        const radiusBottom = 4;
-        const height = 8;
-        const radialSegments = 24;
-        return addShape('hsl(170,100%,40%)', new THREE.CylinderGeometry(radiusTop, radiusBottom, height, radialSegments));
-      },
-    },
-    shapeSphere: {
-      create() {
-        const radius = 5;
-        const widthSegments = 24;
-        const heightSegments = 16;
-        return addShape('hsl(180,100%,40%)', new THREE.SphereGeometry(radius, widthSegments, heightSegments));
-      },
-    },
-    shapeFrustum: {
-      create() {
-        const width = 8;
-        const height = 8;
-        const depth = 8;
-        const geometry = new THREE.BoxGeometry(width, height, depth);
-        const perspMat = new THREE.Matrix4();
-        perspMat.makePerspective(-3, 3, -3, 3, 4, 12);
-        const inMat = new THREE.Matrix4();
-        inMat.makeTranslation(0, 0, 8);
-
-        const mat = new THREE.Matrix4();
-        mat.multiply(perspMat);
-        mat.multiply(inMat);
-
-        geometry.applyMatrix4(mat);
-        geometry.computeBoundingBox();
-        geometry.center();
-        geometry.scale(3, 3, 3);
-        geometry.rotateY(Math.PI);
-        geometry.computeVertexNormals();
-
-        return addShape('hsl(190,100%,40%)', geometry);
-      },
-    },
-  });
-}
+
+	function addShape( color, geometry ) {
+
+		const material = new THREE.MeshPhongMaterial( { color } );
+		return new THREE.Mesh( geometry, material );
+
+	}
+
+	threejsLessonUtils.addDiagrams( {
+		shapeCube: {
+			create() {
+
+				const width = 8;
+				const height = 8;
+				const depth = 8;
+				return addShape( 'hsl(150,100%,40%)', new THREE.BoxGeometry( width, height, depth ) );
+
+			},
+		},
+		shapeCone: {
+			create() {
+
+				const radius = 6;
+				const height = 8;
+				const segments = 24;
+				return addShape( 'hsl(160,100%,40%)', new THREE.ConeGeometry( radius, height, segments ) );
+
+			},
+		},
+		shapeCylinder: {
+			create() {
+
+				const radiusTop = 4;
+				const radiusBottom = 4;
+				const height = 8;
+				const radialSegments = 24;
+				return addShape( 'hsl(170,100%,40%)', new THREE.CylinderGeometry( radiusTop, radiusBottom, height, radialSegments ) );
+
+			},
+		},
+		shapeSphere: {
+			create() {
+
+				const radius = 5;
+				const widthSegments = 24;
+				const heightSegments = 16;
+				return addShape( 'hsl(180,100%,40%)', new THREE.SphereGeometry( radius, widthSegments, heightSegments ) );
+
+			},
+		},
+		shapeFrustum: {
+			create() {
+
+				const width = 8;
+				const height = 8;
+				const depth = 8;
+				const geometry = new THREE.BoxGeometry( width, height, depth );
+				const perspMat = new THREE.Matrix4();
+				perspMat.makePerspective( - 3, 3, - 3, 3, 4, 12 );
+				const inMat = new THREE.Matrix4();
+				inMat.makeTranslation( 0, 0, 8 );
+
+				const mat = new THREE.Matrix4();
+				mat.multiply( perspMat );
+				mat.multiply( inMat );
+
+				geometry.applyMatrix4( mat );
+				geometry.computeBoundingBox();
+				geometry.center();
+				geometry.scale( 3, 3, 3 );
+				geometry.rotateY( Math.PI );
+				geometry.computeVertexNormals();
+
+				return addShape( 'hsl(190,100%,40%)', geometry );
+
+			},
+		},
+	} );
+
+}

+ 77 - 66
manual/resources/threejs-custom-buffergeometry.js

@@ -1,69 +1,80 @@
 import * as THREE from 'three';
-import {threejsLessonUtils} from './threejs-lesson-utils.js';
+import { threejsLessonUtils } from './threejs-lesson-utils.js';
 
 {
-  const loader = new THREE.TextureLoader();
-  const texture = loader.load('/manual/examples/resources/images/star-light.png');
-  texture.wrapS = THREE.RepeatWrapping;
-  texture.wrapT = THREE.RepeatWrapping;
-  texture.repeat.set(3, 1);
-
-  function makeMesh(geometry) {
-    const material = new THREE.MeshPhongMaterial({
-      color: 'hsl(300,50%,50%)',
-      side: THREE.DoubleSide,
-      map: texture,
-    });
-    return new THREE.Mesh(geometry, material);
-  }
-
-  threejsLessonUtils.addDiagrams({
-    geometryCylinder: {
-      create() {
-        return new THREE.Object3D();
-      },
-    },
-    bufferGeometryCylinder: {
-      create() {
-        const numSegments = 24;
-        const positions = [];
-        const uvs = [];
-        for (let s = 0; s <= numSegments; ++s) {
-          const u = s / numSegments;
-          const a = u * Math.PI * 2;
-          const x = Math.sin(a);
-          const z = Math.cos(a);
-          positions.push(x, -1, z);
-          positions.push(x,  1, z);
-          uvs.push(u, 0);
-          uvs.push(u, 1);
-        }
-
-        const indices = [];
-        for (let s = 0; s < numSegments; ++s) {
-          const ndx = s * 2;
-          indices.push(
-            ndx, ndx + 2, ndx + 1,
-            ndx + 1, ndx + 2, ndx + 3,
-          );
-        }
-
-        const positionNumComponents = 3;
-        const uvNumComponents = 2;
-        const geometry = new THREE.BufferGeometry();
-        geometry.setAttribute(
-            'position',
-            new THREE.BufferAttribute(new Float32Array(positions), positionNumComponents));
-        geometry.setAttribute(
-            'uv',
-            new THREE.BufferAttribute(new Float32Array(uvs), uvNumComponents));
-
-        geometry.setIndex(indices);
-        geometry.computeVertexNormals();
-        geometry.scale(5, 5, 5);
-        return makeMesh(geometry);
-      },
-    },
-  });
-
-}
+
+	const loader = new THREE.TextureLoader();
+	const texture = loader.load( '/manual/examples/resources/images/star-light.png' );
+	texture.wrapS = THREE.RepeatWrapping;
+	texture.wrapT = THREE.RepeatWrapping;
+	texture.repeat.set( 3, 1 );
+
+	function makeMesh( geometry ) {
+
+		const material = new THREE.MeshPhongMaterial( {
+			color: 'hsl(300,50%,50%)',
+			side: THREE.DoubleSide,
+			map: texture,
+		} );
+		return new THREE.Mesh( geometry, material );
+
+	}
+
+	threejsLessonUtils.addDiagrams( {
+		geometryCylinder: {
+			create() {
+
+				return new THREE.Object3D();
+
+			},
+		},
+		bufferGeometryCylinder: {
+			create() {
+
+				const numSegments = 24;
+				const positions = [];
+				const uvs = [];
+				for ( let s = 0; s <= numSegments; ++ s ) {
+
+					const u = s / numSegments;
+					const a = u * Math.PI * 2;
+					const x = Math.sin( a );
+					const z = Math.cos( a );
+					positions.push( x, - 1, z );
+					positions.push( x, 1, z );
+					uvs.push( u, 0 );
+					uvs.push( u, 1 );
+
+				}
+
+				const indices = [];
+				for ( let s = 0; s < numSegments; ++ s ) {
+
+					const ndx = s * 2;
+					indices.push(
+						ndx, ndx + 2, ndx + 1,
+						ndx + 1, ndx + 2, ndx + 3,
+					);
+
+				}
+
+				const positionNumComponents = 3;
+				const uvNumComponents = 2;
+				const geometry = new THREE.BufferGeometry();
+				geometry.setAttribute(
+					'position',
+					new THREE.BufferAttribute( new Float32Array( positions ), positionNumComponents ) );
+				geometry.setAttribute(
+					'uv',
+					new THREE.BufferAttribute( new Float32Array( uvs ), uvNumComponents ) );
+
+				geometry.setIndex( indices );
+				geometry.computeVertexNormals();
+				geometry.scale( 5, 5, 5 );
+				return makeMesh( geometry );
+
+			},
+		},
+	} );
+
+}

+ 201 - 157
manual/resources/threejs-fog.js

@@ -1,161 +1,205 @@
 import * as THREE from 'three';
-import {GLTFLoader} from '../../examples/jsm/loaders/GLTFLoader.js';
-import {threejsLessonUtils} from './threejs-lesson-utils.js';
+import { GLTFLoader } from '../../examples/jsm/loaders/GLTFLoader.js';
+import { threejsLessonUtils } from './threejs-lesson-utils.js';
 
 {
-  const darkColors = {
-    background: '#333',
-  };
-  const lightColors = {
-    background: '#FFF',
-  };
-  const darkMatcher = window.matchMedia('(prefers-color-scheme: dark)');
-
-  function fogExample(scene, fog, update) {
-    scene.fog = fog;
-    const width = 4;
-    const height = 3;
-    const depth = 10;
-    const geometry = new THREE.BoxGeometry(width, height, depth);
-    const material = new THREE.MeshPhongMaterial({color: 'hsl(130,50%,50%)'});
-    return {
-      obj3D: new THREE.Mesh(geometry, material),
-      update,
-    };
-  }
-
-  function houseScene(props, fogInHouse) {
-    const {scene, camera} = props;
-    scene.background = new THREE.Color('#FFF');
-    camera.far = 200;
-    const loader = new GLTFLoader();
-    const settings = {
-      shininess: 0,
-      roughness: 1,
-      metalness: 0,
-    };
-    loader.load('/manual/examples/resources/models/simple_house_scene/scene.gltf', (gltf) => {
-      const hackGeometry = new THREE.CircleGeometry(0.5, 32);
-      const box = new THREE.Box3();
-      const size = new THREE.Vector3();
-      const center = new THREE.Vector3();
-      const materials = new Set();
-      gltf.scene.traverse((node) => {
-        const material = node.material;
-        if (material) {
-          // hack in the bottom of the trees since I don't have
-          // the model file
-          if (node.name === 'mesh_11' || node.name === 'mesh_6') {
-            node.updateWorldMatrix(true, false);
-            box.setFromObject(node);
-            box.getSize(size);
-            box.getCenter(center);
-            const hackMesh = new THREE.Mesh(hackGeometry, node.material);
-            scene.add(hackMesh);
-            hackMesh.position.copy(center);
-            hackMesh.rotation.x = Math.PI * 0.5;
-            hackMesh.position.y -= size.y / 2;
-            hackMesh.scale.set(size.x, size.z, 1);
-          }
-          (Array.isArray(material) ? material : [material]).forEach((material) => {
-            if (!materials.has(material)) {
-              materials.add(material);
-              for (const [key, value] of Object.entries(settings)) {
-                if (material[key] !== undefined) {
-                  material[key] = value;
-                }
-              }
-              if (!fogInHouse && material.name.startsWith('fogless')) {
-                material.fog = false;
-              }
-            }
-          });
-        }
-      });
-      scene.add(gltf.scene);
-    });
-
-    camera.fov = 45;
-    camera.position.set(0.4, 1, 1.7);
-    camera.lookAt(1, 1, 0.7);
-
-    const color = 0xFFFFFF;
-    const near = 1.5;
-    const far = 5;
-    scene.fog = new THREE.Fog(color, near, far);
-
-    const light = new THREE.PointLight(0xFFFFFF, 1);
-    light.position.copy(camera.position);
-    light.position.y += 0.2;
-    scene.add(light);
-
-    const target = [1, 1, 0.7];
-    return {
-      trackball: false,
-      obj3D: new THREE.Object3D(),
-      update: (time) => {
-        camera.lookAt(target[0] + Math.sin(time * .25) * .5, target[1], target[2]);
-      },
-    };
-  }
-
-  function createLightDarkFogUpdater(fog) {
-    return function() {
-      const isDarkMode = darkMatcher.matches;
-      const colors = isDarkMode ? darkColors : lightColors;
-      fog.color.set(colors.background);
-    };
-  }
-
-  threejsLessonUtils.addDiagrams({
-    fog: {
-      create(props) {
-        const {scene} = props;
-        const color = 0xFFFFFF;
-        const near = 12;
-        const far = 18;
-        const fog = new THREE.Fog(color, near, far);
-        return fogExample(scene, fog, createLightDarkFogUpdater(fog));
-      },
-    },
-    fogExp2: {
-      create(props) {
-        const {scene} = props;
-        const color = 0xFFFFFF;
-        const density = 0.1;
-        const fog = new THREE.FogExp2(color, density);
-        return fogExample(scene, fog, createLightDarkFogUpdater(fog));
-      },
-    },
-    fogBlueBackgroundRed: {
-      create(props) {
-        const {scene} = props;
-        scene.background = new THREE.Color('#F00');
-        const color = '#00F';
-        const near = 12;
-        const far = 18;
-        return fogExample(scene, new THREE.Fog(color, near, far));
-      },
-    },
-    fogBlueBackgroundBlue: {
-      create(props) {
-        const {scene} = props;
-        scene.background = new THREE.Color('#00F');
-        const color = '#00F';
-        const near = 12;
-        const far = 18;
-        return fogExample(scene, new THREE.Fog(color, near, far));
-      },
-    },
-    fogHouseAll: {
-      create(props) {
-        return houseScene(props, true);
-      },
-    },
-    fogHouseInsideNoFog: {
-      create(props) {
-        return houseScene(props, false);
-      },
-    },
-  });
+
+	const darkColors = {
+		background: '#333',
+	};
+	const lightColors = {
+		background: '#FFF',
+	};
+	const darkMatcher = window.matchMedia( '(prefers-color-scheme: dark)' );
+
+	function fogExample( scene, fog, update ) {
+
+		scene.fog = fog;
+		const width = 4;
+		const height = 3;
+		const depth = 10;
+		const geometry = new THREE.BoxGeometry( width, height, depth );
+		const material = new THREE.MeshPhongMaterial( { color: 'hsl(130,50%,50%)' } );
+		return {
+			obj3D: new THREE.Mesh( geometry, material ),
+			update,
+		};
+
+	}
+
+	function houseScene( props, fogInHouse ) {
+
+		const { scene, camera } = props;
+		scene.background = new THREE.Color( '#FFF' );
+		camera.far = 200;
+		const loader = new GLTFLoader();
+		const settings = {
+			shininess: 0,
+			roughness: 1,
+			metalness: 0,
+		};
+		loader.load( '/manual/examples/resources/models/simple_house_scene/scene.gltf', ( gltf ) => {
+
+			const hackGeometry = new THREE.CircleGeometry( 0.5, 32 );
+			const box = new THREE.Box3();
+			const size = new THREE.Vector3();
+			const center = new THREE.Vector3();
+			const materials = new Set();
+			gltf.scene.traverse( ( node ) => {
+
+				const material = node.material;
+				if ( material ) {
+
+					// hack in the bottom of the trees since I don't have
+					// the model file
+					if ( node.name === 'mesh_11' || node.name === 'mesh_6' ) {
+
+						node.updateWorldMatrix( true, false );
+						box.setFromObject( node );
+						box.getSize( size );
+						box.getCenter( center );
+						const hackMesh = new THREE.Mesh( hackGeometry, node.material );
+						scene.add( hackMesh );
+						hackMesh.position.copy( center );
+						hackMesh.rotation.x = Math.PI * 0.5;
+						hackMesh.position.y -= size.y / 2;
+						hackMesh.scale.set( size.x, size.z, 1 );
+
+					}
+
+					( Array.isArray( material ) ? material : [ material ] ).forEach( ( material ) => {
+
+						if ( ! materials.has( material ) ) {
+
+							materials.add( material );
+							for ( const [ key, value ] of Object.entries( settings ) ) {
+
+								if ( material[ key ] !== undefined ) {
+
+									material[ key ] = value;
+
+								}
+
+							}
+
+							if ( ! fogInHouse && material.name.startsWith( 'fogless' ) ) {
+
+								material.fog = false;
+
+							}
+
+						}
+
+					} );
+
+				}
+
+			} );
+			scene.add( gltf.scene );
+
+		} );
+
+		camera.fov = 45;
+		camera.position.set( 0.4, 1, 1.7 );
+		camera.lookAt( 1, 1, 0.7 );
+
+		const color = 0xFFFFFF;
+		const near = 1.5;
+		const far = 5;
+		scene.fog = new THREE.Fog( color, near, far );
+
+		const light = new THREE.PointLight( 0xFFFFFF, 1 );
+		light.position.copy( camera.position );
+		light.position.y += 0.2;
+		scene.add( light );
+
+		const target = [ 1, 1, 0.7 ];
+		return {
+			trackball: false,
+			obj3D: new THREE.Object3D(),
+			update: ( time ) => {
+
+				camera.lookAt( target[ 0 ] + Math.sin( time * .25 ) * .5, target[ 1 ], target[ 2 ] );
+
+			},
+		};
+
+	}
+
+	function createLightDarkFogUpdater( fog ) {
+
+		return function () {
+
+			const isDarkMode = darkMatcher.matches;
+			const colors = isDarkMode ? darkColors : lightColors;
+			fog.color.set( colors.background );
+
+		};
+
+	}
+
+	threejsLessonUtils.addDiagrams( {
+		fog: {
+			create( props ) {
+
+				const { scene } = props;
+				const color = 0xFFFFFF;
+				const near = 12;
+				const far = 18;
+				const fog = new THREE.Fog( color, near, far );
+				return fogExample( scene, fog, createLightDarkFogUpdater( fog ) );
+
+			},
+		},
+		fogExp2: {
+			create( props ) {
+
+				const { scene } = props;
+				const color = 0xFFFFFF;
+				const density = 0.1;
+				const fog = new THREE.FogExp2( color, density );
+				return fogExample( scene, fog, createLightDarkFogUpdater( fog ) );
+
+			},
+		},
+		fogBlueBackgroundRed: {
+			create( props ) {
+
+				const { scene } = props;
+				scene.background = new THREE.Color( '#F00' );
+				const color = '#00F';
+				const near = 12;
+				const far = 18;
+				return fogExample( scene, new THREE.Fog( color, near, far ) );
+
+			},
+		},
+		fogBlueBackgroundBlue: {
+			create( props ) {
+
+				const { scene } = props;
+				scene.background = new THREE.Color( '#00F' );
+				const color = '#00F';
+				const near = 12;
+				const far = 18;
+				return fogExample( scene, new THREE.Fog( color, near, far ) );
+
+			},
+		},
+		fogHouseAll: {
+			create( props ) {
+
+				return houseScene( props, true );
+
+			},
+		},
+		fogHouseInsideNoFog: {
+			create( props ) {
+
+				return houseScene( props, false );
+
+			},
+		},
+	} );
+
 }

+ 342 - 268
manual/resources/threejs-lesson-utils.js

@@ -1,274 +1,348 @@
 import * as THREE from 'three';
-import {OrbitControls} from '../../examples/jsm/controls/OrbitControls.js';
+import { OrbitControls } from '../../examples/jsm/controls/OrbitControls.js';
 
 export const threejsLessonUtils = {
-  _afterPrettifyFuncs: [],
-  init(options = {threejsOptions:{}}) {
-    if (this.renderer) {
-      return;
-    }
-
-    const canvas = document.createElement('canvas');
-    canvas.id = 'c';
-    document.body.appendChild(canvas);
-    const renderer = new THREE.WebGLRenderer({
-      canvas,
-      alpha: true,
-      powerPreference: 'low-power',
-      ...options.threejsOptions,
-    });
-    this.pixelRatio = window.devicePixelRatio;
-
-    this.renderer = renderer;
-    this.elemToRenderFuncMap = new Map();
-
-    const resizeRendererToDisplaySize = (renderer) => {
-      const canvas = renderer.domElement;
-      const width = canvas.clientWidth * this.pixelRatio | 0;
-      const height = canvas.clientHeight * this.pixelRatio | 0;
-      const needResize = canvas.width !== width || canvas.height !== height;
-      if (needResize) {
-        renderer.setSize(width, height, false);
-      }
-      return needResize;
-    };
-
-    const clearColor = new THREE.Color('#000');
-    let needsUpdate = true;
-    let rafRequestId;
-    let rafRunning;
-
-    const render = (time) => {
-      rafRequestId = undefined;
-      time *= 0.001;
-
-      const resized = resizeRendererToDisplaySize(renderer);
-
-      // only update if we drew last time
-      // so the browser will not recomposite the page
-      // of nothing is being drawn.
-      if (needsUpdate) {
-        needsUpdate = false;
-
-        renderer.setScissorTest(false);
-        renderer.setClearColor(clearColor, 0);
-        renderer.clear(true, true);
-        renderer.setScissorTest(true);
-      }
-
-      this.elementsOnScreen.forEach(elem => {
-        const fn = this.elemToRenderFuncMap.get(elem);
-        const wasRendered = fn(renderer, time, resized);
-        needsUpdate = needsUpdate || wasRendered;
-      });
-
-      if (needsUpdate) {
-        // maybe there is another way. Originally I used `position: fixed`
-        // but the problem is if we can't render as fast as the browser
-        // scrolls then our shapes lag. 1 or 2 frames of lag isn't too
-        // horrible but iOS would often been 1/2 a second or worse.
-        // By doing it this way the canvas will scroll which means the
-        // worse that happens is part of the shapes scrolling on don't
-        // get drawn for a few frames but the shapes that are on the screen
-        // scroll perfectly.
-        //
-        // I'm using `transform` on the voodoo that it doesn't affect
-        // layout as much as `top` since AFAIK setting `top` is in
-        // the flow but `transform` is not though thinking about it
-        // the given we're `position: absolute` maybe there's no difference?
-        const transform = `translateY(${window.scrollY}px)`;
-        renderer.domElement.style.transform = transform;
-      }
-
-      if (rafRunning) {
-        startRAFLoop();
-      }
-    };
-
-    function startRAFLoop() {
-      rafRunning = true;
-      if (!rafRequestId) {
-        rafRequestId = requestAnimationFrame(render);
-      }
-    }
-
-    this.elementsOnScreen = new Set();
-    this.intersectionObserver = new IntersectionObserver((entries) => {
-      entries.forEach(entry => {
-        if (entry.isIntersecting) {
-          this.elementsOnScreen.add(entry.target);
-        } else {
-          this.elementsOnScreen.delete(entry.target);
-        }
-        // Each entry describes an intersection change for one observed
-        // target element:
-        //   entry.boundingClientRect
-        //   entry.intersectionRatio
-        //   entry.intersectionRect
-        //   entry.isIntersecting
-        //   entry.rootBounds
-        //   entry.target
-        //   entry.time
-      });
-      if (this.elementsOnScreen.size > 0) {
-        startRAFLoop();
-      } else {
-        rafRunning = false;
-      }
-    });
-
-
-  },
-  addDiagrams(diagrams) {
-    [...document.querySelectorAll('[data-diagram]')].forEach((elem) => {
-      const name = elem.dataset.diagram;
-      const info = diagrams[name];
-      if (!info) {
-        throw new Error(`no diagram: ${name}`);
-      }
-      this.addDiagram(elem, info);
-    });
-  },
-  addDiagram(elem, info) {
-    this.init();
-
-    const scene = new THREE.Scene();
-    let targetFOVDeg = 60;
-    const aspect = 1;
-    const near = 0.1;
-    const far = 50;
-    let camera = new THREE.PerspectiveCamera(targetFOVDeg, aspect, near, far);
-    camera.position.z = 15;
-    scene.add(camera);
-
-    const root = new THREE.Object3D();
-    scene.add(root);
-
-    const renderInfo = {
-      pixelRatio: this.pixelRatio,
-      camera,
-      scene,
-      root,
-      renderer: this.renderer,
-      elem,
-    };
-
-    const obj3D = info.create({scene, camera, renderInfo});
-    const promise = (obj3D instanceof Promise) ? obj3D : Promise.resolve(obj3D);
-
-    const updateFunctions = [];
-    const resizeFunctions = [];
-
-    const settings = {
-      lights: true,
-      trackball: true,
-      // resize(renderInfo) {
-      // },
-      // update(time, renderInfo) {
-      // },
-      render(renderInfo) {
-        renderInfo.renderer.render(renderInfo.scene, renderInfo.camera);
-      },
-    };
-
-    promise.then((result) => {
-      const info = result instanceof THREE.Object3D ? {
-        obj3D: result,
-      } : result;
-      if (info.obj3D) {
-        root.add(info.obj3D);
-      }
-      if (info.update) {
-        updateFunctions.push(info.update);
-      }
-      if (info.resize) {
-        resizeFunctions.push(info.resize);
-      }
-      if (info.camera) {
-        camera = info.camera;
-        renderInfo.camera = camera;
-      }
-
-      Object.assign(settings, info);
-      targetFOVDeg = camera.fov;
-
-      if (settings.trackball !== false) {
-        const controls = new OrbitControls(camera, elem);
-        controls.rotateSpeed = 1 / 6;
-        controls.enableZoom = false;
-        controls.enablePan = false;
-        elem.removeAttribute('tabIndex');
-        //resizeFunctions.push(controls.handleResize.bind(controls));
-        updateFunctions.push(controls.update.bind(controls));
-      }
-
-      // add the lights as children of the camera.
-      // this is because TrackballControls move the camera.
-      // We really want to rotate the object itself but there's no
-      // controls for that so we fake it by putting all the lights
-      // on the camera so they move with it.
-      if (settings.lights !== false) {
-        camera.add(new THREE.HemisphereLight(0xaaaaaa, 0x444444, .5));
-        const light = new THREE.DirectionalLight(0xffffff, 1);
-        light.position.set(-1, 2, 4 - 15);
-        camera.add(light);
-      }
-    });
-
-    let oldWidth = -1;
-    let oldHeight = -1;
-
-    const render = (renderer, time) => {
-      root.rotation.x = time * .1;
-      root.rotation.y = time * .11;
-
-      const rect = elem.getBoundingClientRect();
-      if (rect.bottom < 0 || rect.top  > renderer.domElement.clientHeight ||
-          rect.right  < 0 || rect.left > renderer.domElement.clientWidth) {
-        return false;
-      }
-
-      renderInfo.width = rect.width * this.pixelRatio;
-      renderInfo.height = rect.height * this.pixelRatio;
-      renderInfo.left = rect.left * this.pixelRatio;
-      renderInfo.bottom = (renderer.domElement.clientHeight - rect.bottom) * this.pixelRatio;
-
-      if (renderInfo.width !== oldWidth || renderInfo.height !== oldHeight) {
-        oldWidth = renderInfo.width;
-        oldHeight = renderInfo.height;
-        resizeFunctions.forEach(fn => fn(renderInfo));
-      }
-
-      updateFunctions.forEach(fn => fn(time, renderInfo));
-
-      const aspect = renderInfo.width / renderInfo.height;
-      const fovDeg = aspect >= 1
-        ? targetFOVDeg
-        : THREE.MathUtils.radToDeg(2 * Math.atan(Math.tan(THREE.MathUtils.degToRad(targetFOVDeg) * .5) / aspect));
-
-      camera.fov = fovDeg;
-      camera.aspect = aspect;
-      camera.updateProjectionMatrix();
-
-      renderer.setViewport(renderInfo.left, renderInfo.bottom, renderInfo.width, renderInfo.height);
-      renderer.setScissor(renderInfo.left, renderInfo.bottom, renderInfo.width, renderInfo.height);
-
-      settings.render(renderInfo);
-
-      return true;
-    };
-
-    this.intersectionObserver.observe(elem);
-    this.elemToRenderFuncMap.set(elem, render);
-  },
-  onAfterPrettify(fn) {
-    this._afterPrettifyFuncs.push(fn);
-  },
-  afterPrettify() {
-    this._afterPrettifyFuncs.forEach((fn) => {
-      fn();
-    });
-  },
+	_afterPrettifyFuncs: [],
+	init( options = { threejsOptions: {} } ) {
+
+		if ( this.renderer ) {
+
+			return;
+
+		}
+
+		const canvas = document.createElement( 'canvas' );
+		canvas.id = 'c';
+		document.body.appendChild( canvas );
+		const renderer = new THREE.WebGLRenderer( {
+			canvas,
+			alpha: true,
+			powerPreference: 'low-power',
+			...options.threejsOptions,
+		} );
+		this.pixelRatio = window.devicePixelRatio;
+
+		this.renderer = renderer;
+		this.elemToRenderFuncMap = new Map();
+
+		const resizeRendererToDisplaySize = ( renderer ) => {
+
+			const canvas = renderer.domElement;
+			const width = canvas.clientWidth * this.pixelRatio | 0;
+			const height = canvas.clientHeight * this.pixelRatio | 0;
+			const needResize = canvas.width !== width || canvas.height !== height;
+			if ( needResize ) {
+
+				renderer.setSize( width, height, false );
+
+			}
+
+			return needResize;
+
+		};
+
+		const clearColor = new THREE.Color( '#000' );
+		let needsUpdate = true;
+		let rafRequestId;
+		let rafRunning;
+
+		const render = ( time ) => {
+
+			rafRequestId = undefined;
+			time *= 0.001;
+
+			const resized = resizeRendererToDisplaySize( renderer );
+
+			// only update if we drew last time
+			// so the browser will not recomposite the page
+			// of nothing is being drawn.
+			if ( needsUpdate ) {
+
+				needsUpdate = false;
+
+				renderer.setScissorTest( false );
+				renderer.setClearColor( clearColor, 0 );
+				renderer.clear( true, true );
+				renderer.setScissorTest( true );
+
+			}
+
+			this.elementsOnScreen.forEach( elem => {
+
+				const fn = this.elemToRenderFuncMap.get( elem );
+				const wasRendered = fn( renderer, time, resized );
+				needsUpdate = needsUpdate || wasRendered;
+
+			} );
+
+			if ( needsUpdate ) {
+
+				// maybe there is another way. Originally I used `position: fixed`
+				// but the problem is if we can't render as fast as the browser
+				// scrolls then our shapes lag. 1 or 2 frames of lag isn't too
+				// horrible but iOS would often been 1/2 a second or worse.
+				// By doing it this way the canvas will scroll which means the
+				// worse that happens is part of the shapes scrolling on don't
+				// get drawn for a few frames but the shapes that are on the screen
+				// scroll perfectly.
+				//
+				// I'm using `transform` on the voodoo that it doesn't affect
+				// layout as much as `top` since AFAIK setting `top` is in
+				// the flow but `transform` is not though thinking about it
+				// the given we're `position: absolute` maybe there's no difference?
+				const transform = `translateY(${window.scrollY}px)`;
+				renderer.domElement.style.transform = transform;
+
+			}
+
+			if ( rafRunning ) {
+
+				startRAFLoop();
+
+			}
+
+		};
+
+		function startRAFLoop() {
+
+			rafRunning = true;
+			if ( ! rafRequestId ) {
+
+				rafRequestId = requestAnimationFrame( render );
+
+			}
+
+		}
+
+		this.elementsOnScreen = new Set();
+		this.intersectionObserver = new IntersectionObserver( ( entries ) => {
+
+			entries.forEach( entry => {
+
+				if ( entry.isIntersecting ) {
+
+					this.elementsOnScreen.add( entry.target );
+
+				} else {
+
+					this.elementsOnScreen.delete( entry.target );
+
+				}
+				// Each entry describes an intersection change for one observed
+				// target element:
+				//   entry.boundingClientRect
+				//   entry.intersectionRatio
+				//   entry.intersectionRect
+				//   entry.isIntersecting
+				//   entry.rootBounds
+				//   entry.target
+				//   entry.time
+
+			} );
+			if ( this.elementsOnScreen.size > 0 ) {
+
+				startRAFLoop();
+
+			} else {
+
+				rafRunning = false;
+
+			}
+
+		} );
+
+
+	},
+	addDiagrams( diagrams ) {
+
+		[ ...document.querySelectorAll( '[data-diagram]' ) ].forEach( ( elem ) => {
+
+			const name = elem.dataset.diagram;
+			const info = diagrams[ name ];
+			if ( ! info ) {
+
+				throw new Error( `no diagram: ${name}` );
+
+			}
+
+			this.addDiagram( elem, info );
+
+		} );
+
+	},
+	addDiagram( elem, info ) {
+
+		this.init();
+
+		const scene = new THREE.Scene();
+		let targetFOVDeg = 60;
+		const aspect = 1;
+		const near = 0.1;
+		const far = 50;
+		let camera = new THREE.PerspectiveCamera( targetFOVDeg, aspect, near, far );
+		camera.position.z = 15;
+		scene.add( camera );
+
+		const root = new THREE.Object3D();
+		scene.add( root );
+
+		const renderInfo = {
+			pixelRatio: this.pixelRatio,
+			camera,
+			scene,
+			root,
+			renderer: this.renderer,
+			elem,
+		};
+
+		const obj3D = info.create( { scene, camera, renderInfo } );
+		const promise = ( obj3D instanceof Promise ) ? obj3D : Promise.resolve( obj3D );
+
+		const updateFunctions = [];
+		const resizeFunctions = [];
+
+		const settings = {
+			lights: true,
+			trackball: true,
+			// resize(renderInfo) {
+			// },
+			// update(time, renderInfo) {
+			// },
+			render( renderInfo ) {
+
+				renderInfo.renderer.render( renderInfo.scene, renderInfo.camera );
+
+			},
+		};
+
+		promise.then( ( result ) => {
+
+			const info = result instanceof THREE.Object3D ? {
+				obj3D: result,
+			} : result;
+			if ( info.obj3D ) {
+
+				root.add( info.obj3D );
+
+			}
+
+			if ( info.update ) {
+
+				updateFunctions.push( info.update );
+
+			}
+
+			if ( info.resize ) {
+
+				resizeFunctions.push( info.resize );
+
+			}
+
+			if ( info.camera ) {
+
+				camera = info.camera;
+				renderInfo.camera = camera;
+
+			}
+
+			Object.assign( settings, info );
+			targetFOVDeg = camera.fov;
+
+			if ( settings.trackball !== false ) {
+
+				const controls = new OrbitControls( camera, elem );
+				controls.rotateSpeed = 1 / 6;
+				controls.enableZoom = false;
+				controls.enablePan = false;
+				elem.removeAttribute( 'tabIndex' );
+				//resizeFunctions.push(controls.handleResize.bind(controls));
+				updateFunctions.push( controls.update.bind( controls ) );
+
+			}
+
+			// add the lights as children of the camera.
+			// this is because TrackballControls move the camera.
+			// We really want to rotate the object itself but there's no
+			// controls for that so we fake it by putting all the lights
+			// on the camera so they move with it.
+			if ( settings.lights !== false ) {
+
+				camera.add( new THREE.HemisphereLight( 0xaaaaaa, 0x444444, .5 ) );
+				const light = new THREE.DirectionalLight( 0xffffff, 1 );
+				light.position.set( - 1, 2, 4 - 15 );
+				camera.add( light );
+
+			}
+
+		} );
+
+		let oldWidth = - 1;
+		let oldHeight = - 1;
+
+		const render = ( renderer, time ) => {
+
+			root.rotation.x = time * .1;
+			root.rotation.y = time * .11;
+
+			const rect = elem.getBoundingClientRect();
+			if ( rect.bottom < 0 || rect.top > renderer.domElement.clientHeight ||
+          rect.right < 0 || rect.left > renderer.domElement.clientWidth ) {
+
+				return false;
+
+			}
+
+			renderInfo.width = rect.width * this.pixelRatio;
+			renderInfo.height = rect.height * this.pixelRatio;
+			renderInfo.left = rect.left * this.pixelRatio;
+			renderInfo.bottom = ( renderer.domElement.clientHeight - rect.bottom ) * this.pixelRatio;
+
+			if ( renderInfo.width !== oldWidth || renderInfo.height !== oldHeight ) {
+
+				oldWidth = renderInfo.width;
+				oldHeight = renderInfo.height;
+				resizeFunctions.forEach( fn => fn( renderInfo ) );
+
+			}
+
+			updateFunctions.forEach( fn => fn( time, renderInfo ) );
+
+			const aspect = renderInfo.width / renderInfo.height;
+			const fovDeg = aspect >= 1
+				? targetFOVDeg
+				: THREE.MathUtils.radToDeg( 2 * Math.atan( Math.tan( THREE.MathUtils.degToRad( targetFOVDeg ) * .5 ) / aspect ) );
+
+			camera.fov = fovDeg;
+			camera.aspect = aspect;
+			camera.updateProjectionMatrix();
+
+			renderer.setViewport( renderInfo.left, renderInfo.bottom, renderInfo.width, renderInfo.height );
+			renderer.setScissor( renderInfo.left, renderInfo.bottom, renderInfo.width, renderInfo.height );
+
+			settings.render( renderInfo );
+
+			return true;
+
+		};
+
+		this.intersectionObserver.observe( elem );
+		this.elemToRenderFuncMap.set( elem, render );
+
+	},
+	onAfterPrettify( fn ) {
+
+		this._afterPrettifyFuncs.push( fn );
+
+	},
+	afterPrettify() {
+
+		this._afterPrettifyFuncs.forEach( ( fn ) => {
+
+			fn();
+
+		} );
+
+	},
 };
 
 window.threejsLessonUtils = threejsLessonUtils;

+ 138 - 104
manual/resources/threejs-lights.js

@@ -1,108 +1,142 @@
 import * as THREE from 'three';
-import {OrbitControls} from '../../examples/jsm/controls/OrbitControls.js';
-import {threejsLessonUtils} from './threejs-lesson-utils.js';
+import { OrbitControls } from '../../examples/jsm/controls/OrbitControls.js';
+import { threejsLessonUtils } from './threejs-lesson-utils.js';
 
 {
-  function makeCheckerTexture(repeats) {
-    const data = new Uint8Array([
-      0x88, 0x88, 0x88, 0xFF, 0xCC, 0xCC, 0xCC, 0xFF,
-      0xCC, 0xCC, 0xCC, 0xFF, 0x88, 0x88, 0x88, 0xFF
-    ]);
-    const width = 2;
-    const height = 2;
-    const texture = new THREE.DataTexture(data, width, height);
-    texture.needsUpdate = true;
-    texture.wrapS = THREE.RepeatWrapping;
-    texture.wrapT = THREE.RepeatWrapping;
-    texture.repeat.set(repeats / 2, repeats / 2);
-    return texture;
-  }
-
-  const makeScene = function() {
-
-    const cubeSize = 4;
-    const cubeGeo = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
-    const cubeMat = new THREE.MeshPhongMaterial({color: '#8AC'});
-
-    const sphereRadius = 3;
-    const sphereWidthDivisions = 32;
-    const sphereHeightDivisions = 16;
-    const sphereGeo = new THREE.SphereGeometry(sphereRadius, sphereWidthDivisions, sphereHeightDivisions);
-    const sphereMat = new THREE.MeshPhongMaterial({color: '#CA8'});
-
-    const planeSize = 40;
-    const planeGeo = new THREE.PlaneGeometry(planeSize, planeSize);
-    const planeMat = new THREE.MeshPhongMaterial({
-      map: makeCheckerTexture(planeSize),
-      side: THREE.DoubleSide,
-    });
-
-    return function(renderInfo) {
-      const {scene, camera, elem} = renderInfo;
-      const controls = new OrbitControls(camera, elem);
-      controls.enableDamping = true;
-      controls.enablePanning = false;
-      scene.background = new THREE.Color('black');
-      {
-        const mesh = new THREE.Mesh(cubeGeo, cubeMat);
-        mesh.position.set(cubeSize + 1, cubeSize / 2, -cubeSize - 1);
-        scene.add(mesh);
-      }
-      {
-        const mesh = new THREE.Mesh(sphereGeo, sphereMat);
-        mesh.position.set(-sphereRadius - 1, sphereRadius + 2, -sphereRadius + 1);
-        scene.add(mesh);
-      }
-      {
-        const mesh = new THREE.Mesh(planeGeo, planeMat);
-        mesh.rotation.x = Math.PI * -.5;
-        scene.add(mesh);
-      }
-      return {
-        trackball: false,
-        lights: false,
-        update() {
-          controls.update();
-        },
-      };
-    };
-  }();
-
-  threejsLessonUtils.addDiagrams({
-    directionalOnly: {
-      create(props) {
-        const {scene, renderInfo} = props;
-        const result = makeScene(renderInfo);
-        {
-          const light = new THREE.DirectionalLight(0xFFFFFF, 1);
-          light.position.set(5, 10, 0);
-          scene.add(light);
-        }
-        {
-          const light = new THREE.AmbientLight(0xFFFFFF, .6);
-          scene.add(light);
-        }
-        return result;
-      },
-    },
-    directionalPlusHemisphere: {
-      create(props) {
-        const {scene, renderInfo} = props;
-        const result = makeScene(renderInfo);
-        {
-          const light = new THREE.DirectionalLight(0xFFFFFF, 1);
-          light.position.set(5, 10, 0);
-          scene.add(light);
-        }
-        {
-          const skyColor = 0xB1E1FF;  // light blue
-          const groundColor = 0xB97A20;  // brownish orange
-          const intensity = .6;
-          const light = new THREE.HemisphereLight(skyColor, groundColor, intensity);
-          scene.add(light);
-        }
-        return result;
-      },
-    },
-  });
+
+	function makeCheckerTexture( repeats ) {
+
+		const data = new Uint8Array( [
+			0x88, 0x88, 0x88, 0xFF, 0xCC, 0xCC, 0xCC, 0xFF,
+			0xCC, 0xCC, 0xCC, 0xFF, 0x88, 0x88, 0x88, 0xFF
+		] );
+		const width = 2;
+		const height = 2;
+		const texture = new THREE.DataTexture( data, width, height );
+		texture.needsUpdate = true;
+		texture.wrapS = THREE.RepeatWrapping;
+		texture.wrapT = THREE.RepeatWrapping;
+		texture.repeat.set( repeats / 2, repeats / 2 );
+		return texture;
+
+	}
+
+	const makeScene = function () {
+
+		const cubeSize = 4;
+		const cubeGeo = new THREE.BoxGeometry( cubeSize, cubeSize, cubeSize );
+		const cubeMat = new THREE.MeshPhongMaterial( { color: '#8AC' } );
+
+		const sphereRadius = 3;
+		const sphereWidthDivisions = 32;
+		const sphereHeightDivisions = 16;
+		const sphereGeo = new THREE.SphereGeometry( sphereRadius, sphereWidthDivisions, sphereHeightDivisions );
+		const sphereMat = new THREE.MeshPhongMaterial( { color: '#CA8' } );
+
+		const planeSize = 40;
+		const planeGeo = new THREE.PlaneGeometry( planeSize, planeSize );
+		const planeMat = new THREE.MeshPhongMaterial( {
+			map: makeCheckerTexture( planeSize ),
+			side: THREE.DoubleSide,
+		} );
+
+		return function ( renderInfo ) {
+
+			const { scene, camera, elem } = renderInfo;
+			const controls = new OrbitControls( camera, elem );
+			controls.enableDamping = true;
+			controls.enablePanning = false;
+			scene.background = new THREE.Color( 'black' );
+			{
+
+				const mesh = new THREE.Mesh( cubeGeo, cubeMat );
+				mesh.position.set( cubeSize + 1, cubeSize / 2, - cubeSize - 1 );
+				scene.add( mesh );
+
+			}
+
+			{
+
+				const mesh = new THREE.Mesh( sphereGeo, sphereMat );
+				mesh.position.set( - sphereRadius - 1, sphereRadius + 2, - sphereRadius + 1 );
+				scene.add( mesh );
+
+			}
+
+			{
+
+				const mesh = new THREE.Mesh( planeGeo, planeMat );
+				mesh.rotation.x = Math.PI * - .5;
+				scene.add( mesh );
+
+			}
+
+			return {
+				trackball: false,
+				lights: false,
+				update() {
+
+					controls.update();
+
+				},
+			};
+
+		};
+
+	}();
+
+	threejsLessonUtils.addDiagrams( {
+		directionalOnly: {
+			create( props ) {
+
+				const { scene, renderInfo } = props;
+				const result = makeScene( renderInfo );
+				{
+
+					const light = new THREE.DirectionalLight( 0xFFFFFF, 1 );
+					light.position.set( 5, 10, 0 );
+					scene.add( light );
+
+				}
+
+				{
+
+					const light = new THREE.AmbientLight( 0xFFFFFF, .6 );
+					scene.add( light );
+
+				}
+
+				return result;
+
+			},
+		},
+		directionalPlusHemisphere: {
+			create( props ) {
+
+				const { scene, renderInfo } = props;
+				const result = makeScene( renderInfo );
+				{
+
+					const light = new THREE.DirectionalLight( 0xFFFFFF, 1 );
+					light.position.set( 5, 10, 0 );
+					scene.add( light );
+
+				}
+
+				{
+
+					const skyColor = 0xB1E1FF; // light blue
+					const groundColor = 0xB97A20; // brownish orange
+					const intensity = .6;
+					const light = new THREE.HemisphereLight( skyColor, groundColor, intensity );
+					scene.add( light );
+
+				}
+
+				return result;
+
+			},
+		},
+	} );
+
 }

+ 136 - 107
manual/resources/threejs-lots-of-objects.js

@@ -1,113 +1,142 @@
 import * as THREE from 'three';
-import {threejsLessonUtils} from './threejs-lesson-utils.js';
-import {GUI} from '../../examples/jsm/libs/lil-gui.module.min.js';
+import { threejsLessonUtils } from './threejs-lesson-utils.js';
+import { GUI } from '../../examples/jsm/libs/lil-gui.module.min.js';
 
 {
-  class DegRadHelper {
-    constructor(obj, prop) {
-      this.obj = obj;
-      this.prop = prop;
-    }
-    get value() {
-      return THREE.MathUtils.radToDeg(this.obj[this.prop]);
-    }
-    set value(v) {
-      this.obj[this.prop] = THREE.MathUtils.degToRad(v);
-    }
-  }
-
-  function scaleCube(zOffset) {
-    const root = new THREE.Object3D();
-
-    const size = 3;
-    const geometry = new THREE.BoxGeometry(size, size, size);
-    geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(0, 0, zOffset * size));
-    const material = new THREE.MeshBasicMaterial({
-      color: 'red',
-    });
-    const cube = new THREE.Mesh(geometry, material);
-    root.add(cube);
-    cube.add(new THREE.LineSegments(
-        new THREE.EdgesGeometry(geometry),
-        new THREE.LineBasicMaterial({color: 'white'})));
-
-    [[0, 0], [1, 0], [0, 1]].forEach((rot) => {
-      const size = 10;
-      const divisions = 10;
-      const gridHelper = new THREE.GridHelper(size, divisions);
-      root.add(gridHelper);
-      gridHelper.rotation.x = rot[0] * Math.PI * .5;
-      gridHelper.rotation.z = rot[1] * Math.PI * .5;
-    });
-
-    return {
-      obj3D: root,
-      update: (time) => {
-        const s = THREE.MathUtils.lerp(0.5, 2, Math.sin(time) * .5 + .5);
-        cube.scale.set(s, s, s);
-      },
-    };
-  }
-
-  threejsLessonUtils.addDiagrams({
-    scaleCenter: {
-      create() {
-        return scaleCube(0);
-      },
-    },
-    scalePositiveZ: {
-      create() {
-        return scaleCube(.5);
-      },
-    },
-    lonLatPos: {
-      create(info) {
-        const {scene, camera, renderInfo} = info;
-        const size = 10;
-        const divisions = 10;
-        const gridHelper = new THREE.GridHelper(size, divisions);
-        scene.add(gridHelper);
-
-        const geometry = new THREE.BoxGeometry(1, 1, 1);
-
-        const lonHelper = new THREE.Object3D();
-        scene.add(lonHelper);
-        const latHelper = new THREE.Object3D();
-        lonHelper.add(latHelper);
-        const positionHelper = new THREE.Object3D();
-        latHelper.add(positionHelper);
-
-        {
-          const lonMesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({color: 'green'}));
-          lonMesh.scale.set(0.2, 1, 0.2);
-          lonHelper.add(lonMesh);
-        }
-        {
-          const latMesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({color: 'blue'}));
-          latMesh.scale.set(1, 0.25, 0.25);
-          latHelper.add(latMesh);
-        }
-        {
-          const geometry = new THREE.SphereGeometry(0.1, 24, 12);
-          const posMesh = new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({color: 'red'}));
-          posMesh.position.z = 1;
-          positionHelper.add(posMesh);
-        }
-
-        camera.position.set(1, 1.5, 1.5);
-        camera.lookAt(0, 0, 0);
-
-        const gui = new GUI({autoPlace: false});
-        renderInfo.elem.appendChild(gui.domElement);
-        gui.add(new DegRadHelper(lonHelper.rotation, 'y'), 'value', -180, 180).name('lonHelper x rotation');
-        gui.add(new DegRadHelper(latHelper.rotation, 'x'), 'value', -90, 90).name('latHelper y rotation');
-
-        return {
-          trackball: false,
-        };
-      },
-    },
-  });
+
+	class DegRadHelper {
+
+		constructor( obj, prop ) {
+
+			this.obj = obj;
+			this.prop = prop;
+
+		}
+		get value() {
+
+			return THREE.MathUtils.radToDeg( this.obj[ this.prop ] );
+
+		}
+		set value( v ) {
+
+			this.obj[ this.prop ] = THREE.MathUtils.degToRad( v );
+
+		}
+
+	}
+
+	function scaleCube( zOffset ) {
+
+		const root = new THREE.Object3D();
+
+		const size = 3;
+		const geometry = new THREE.BoxGeometry( size, size, size );
+		geometry.applyMatrix4( new THREE.Matrix4().makeTranslation( 0, 0, zOffset * size ) );
+		const material = new THREE.MeshBasicMaterial( {
+			color: 'red',
+		} );
+		const cube = new THREE.Mesh( geometry, material );
+		root.add( cube );
+		cube.add( new THREE.LineSegments(
+			new THREE.EdgesGeometry( geometry ),
+			new THREE.LineBasicMaterial( { color: 'white' } ) ) );
+
+		[[ 0, 0 ], [ 1, 0 ], [ 0, 1 ]].forEach( ( rot ) => {
+
+			const size = 10;
+			const divisions = 10;
+			const gridHelper = new THREE.GridHelper( size, divisions );
+			root.add( gridHelper );
+			gridHelper.rotation.x = rot[ 0 ] * Math.PI * .5;
+			gridHelper.rotation.z = rot[ 1 ] * Math.PI * .5;
+
+		} );
+
+		return {
+			obj3D: root,
+			update: ( time ) => {
+
+				const s = THREE.MathUtils.lerp( 0.5, 2, Math.sin( time ) * .5 + .5 );
+				cube.scale.set( s, s, s );
+
+			},
+		};
+
+	}
+
+	threejsLessonUtils.addDiagrams( {
+		scaleCenter: {
+			create() {
+
+				return scaleCube( 0 );
+
+			},
+		},
+		scalePositiveZ: {
+			create() {
+
+				return scaleCube( .5 );
+
+			},
+		},
+		lonLatPos: {
+			create( info ) {
+
+				const { scene, camera, renderInfo } = info;
+				const size = 10;
+				const divisions = 10;
+				const gridHelper = new THREE.GridHelper( size, divisions );
+				scene.add( gridHelper );
+
+				const geometry = new THREE.BoxGeometry( 1, 1, 1 );
+
+				const lonHelper = new THREE.Object3D();
+				scene.add( lonHelper );
+				const latHelper = new THREE.Object3D();
+				lonHelper.add( latHelper );
+				const positionHelper = new THREE.Object3D();
+				latHelper.add( positionHelper );
+
+				{
+
+					const lonMesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { color: 'green' } ) );
+					lonMesh.scale.set( 0.2, 1, 0.2 );
+					lonHelper.add( lonMesh );
+
+				}
+
+				{
+
+					const latMesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { color: 'blue' } ) );
+					latMesh.scale.set( 1, 0.25, 0.25 );
+					latHelper.add( latMesh );
+
+				}
+
+				{
+
+					const geometry = new THREE.SphereGeometry( 0.1, 24, 12 );
+					const posMesh = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial( { color: 'red' } ) );
+					posMesh.position.z = 1;
+					positionHelper.add( posMesh );
+
+				}
+
+				camera.position.set( 1, 1.5, 1.5 );
+				camera.lookAt( 0, 0, 0 );
+
+				const gui = new GUI( { autoPlace: false } );
+				renderInfo.elem.appendChild( gui.domElement );
+				gui.add( new DegRadHelper( lonHelper.rotation, 'y' ), 'value', - 180, 180 ).name( 'lonHelper x rotation' );
+				gui.add( new DegRadHelper( latHelper.rotation, 'x' ), 'value', - 90, 90 ).name( 'latHelper y rotation' );
+
+				return {
+					trackball: false,
+				};
+
+			},
+		},
+	} );
 
 }
 

+ 420 - 333
manual/resources/threejs-materials.js

@@ -1,338 +1,425 @@
 import * as THREE from 'three';
-import {threejsLessonUtils} from './threejs-lesson-utils.js';
+import { threejsLessonUtils } from './threejs-lesson-utils.js';
 
 {
-  function makeSphere(widthDivisions, heightDivisions) {
-    const radius = 7;
-    return new THREE.SphereGeometry(radius, widthDivisions, heightDivisions);
-  }
-
-  const highPolySphereGeometry = function() {
-    const widthDivisions = 100;
-    const heightDivisions = 50;
-    return makeSphere(widthDivisions, heightDivisions);
-  }();
-
-  const lowPolySphereGeometry = function() {
-    const widthDivisions = 12;
-    const heightDivisions = 9;
-    return makeSphere(widthDivisions, heightDivisions);
-  }();
-
-  function smoothOrFlat(flatShading, radius = 7) {
-    const widthDivisions = 12;
-    const heightDivisions = 9;
-    const geometry = new THREE.SphereGeometry(radius, widthDivisions, heightDivisions);
-    const material = new THREE.MeshPhongMaterial({
-      flatShading,
-      color: 'hsl(300,50%,50%)',
-    });
-    return new THREE.Mesh(geometry, material);
-  }
-
-  function basicLambertPhongExample(MaterialCtor, lowPoly, params = {}) {
-    const geometry = lowPoly ? lowPolySphereGeometry : highPolySphereGeometry;
-    const material = new MaterialCtor({
-      color: 'hsl(210,50%,50%)',
-      ...params,
-    });
-    return {
-      obj3D: new THREE.Mesh(geometry, material),
-      trackball: lowPoly,
-    };
-  }
-
-  function sideExample(side) {
-    const base = new THREE.Object3D();
-    const size = 6;
-    const geometry = new THREE.PlaneGeometry(size, size);
-    [
-      { position: [ -1, 0, 0], up: [0,  1, 0], },
-      { position: [  1, 0, 0], up: [0, -1, 0], },
-      { position: [ 0, -1, 0], up: [0, 0, -1], },
-      { position: [ 0,  1, 0], up: [0, 0,  1], },
-      { position: [ 0, 0, -1], up: [ 1, 0, 0], },
-      { position: [ 0, 0,  1], up: [-1, 0, 0], },
-    ].forEach((settings, ndx) => {
-      const material = new THREE.MeshBasicMaterial({side});
-      material.color.setHSL(ndx / 6, .5, .5);
-      const mesh = new THREE.Mesh(geometry, material);
-      mesh.up.set(...settings.up);
-      mesh.lookAt(...settings.position);
-      mesh.position.set(...settings.position).multiplyScalar(size * .75);
-      base.add(mesh);
-    });
-    return base;
-  }
-
-  function makeStandardPhysicalMaterialGrid(elem, physical, update) {
-    const numMetal = 5;
-    const numRough = 7;
-    const meshes = [];
-    const MatCtor = physical ? THREE.MeshPhysicalMaterial : THREE.MeshStandardMaterial;
-    const color = physical ? 'hsl(160,50%,50%)' : 'hsl(140,50%,50%)';
-    for (let m = 0; m < numMetal; ++m) {
-      const row = [];
-      for (let r = 0; r < numRough; ++r) {
-        const material = new MatCtor({
-          color,
-          roughness: r / (numRough - 1),
-          metalness: 1 - m / (numMetal - 1),
-        });
-        const mesh = new THREE.Mesh(highPolySphereGeometry, material);
-        row.push(mesh);
-      }
-      meshes.push(row);
-    }
-    return {
-      obj3D: null,
-      trackball: false,
-      render(renderInfo) {
-        const {camera, scene, renderer} = renderInfo;
-        const rect = elem.getBoundingClientRect();
-
-        const width = (rect.right - rect.left) * renderInfo.pixelRatio;
-        const height = (rect.bottom - rect.top) * renderInfo.pixelRatio;
-        const left = rect.left * renderInfo.pixelRatio;
-        const bottom = (renderer.domElement.clientHeight - rect.bottom) * renderInfo.pixelRatio;
-
-        const cellSize = Math.min(width / numRough, height / numMetal) | 0;
-        const xOff = (width - cellSize * numRough) / 2;
-        const yOff = (height - cellSize * numMetal) / 2;
-
-        camera.aspect = 1;
-        camera.updateProjectionMatrix();
-
-        if (update) {
-          update(meshes);
-        }
-
-        for (let m = 0; m < numMetal; ++m) {
-          for (let r = 0; r < numRough; ++r) {
-            const x = left + xOff + r * cellSize;
-            const y = bottom + yOff + m * cellSize;
-            renderer.setViewport(x, y, cellSize, cellSize);
-            renderer.setScissor(x, y, cellSize, cellSize);
-            const mesh = meshes[m][r];
-            scene.add(mesh);
-            renderer.render(scene, camera);
-            scene.remove(mesh);
-          }
-        }
-      },
-    };
-  }
-
-  threejsLessonUtils.addDiagrams({
-    smoothShading: {
-      create() {
-        return smoothOrFlat(false);
-      },
-    },
-    flatShading: {
-      create() {
-        return smoothOrFlat(true);
-      },
-    },
-    MeshBasicMaterial: {
-      create() {
-        return basicLambertPhongExample(THREE.MeshBasicMaterial);
-      },
-    },
-    MeshLambertMaterial: {
-      create() {
-        return basicLambertPhongExample(THREE.MeshLambertMaterial);
-      },
-    },
-    MeshPhongMaterial: {
-      create() {
-        return basicLambertPhongExample(THREE.MeshPhongMaterial);
-      },
-    },
-    MeshBasicMaterialLowPoly: {
-      create() {
-        return basicLambertPhongExample(THREE.MeshBasicMaterial, true);
-      },
-    },
-    MeshLambertMaterialLowPoly: {
-      create() {
-        return basicLambertPhongExample(THREE.MeshLambertMaterial, true);
-      },
-    },
-    MeshPhongMaterialLowPoly: {
-      create() {
-        return basicLambertPhongExample(THREE.MeshPhongMaterial, true);
-      },
-    },
-    MeshPhongMaterialShininess0: {
-      create() {
-        return basicLambertPhongExample(THREE.MeshPhongMaterial, false, {
-          color: 'red',
-          shininess: 0,
-        });
-      },
-    },
-    MeshPhongMaterialShininess30: {
-      create() {
-        return basicLambertPhongExample(THREE.MeshPhongMaterial, false, {
-          color: 'red',
-          shininess: 30,
-        });
-      },
-    },
-    MeshPhongMaterialShininess150: {
-      create() {
-        return basicLambertPhongExample(THREE.MeshPhongMaterial, false, {
-          color: 'red',
-          shininess: 150,
-        });
-      },
-    },
-    MeshBasicMaterialCompare: {
-      create() {
-        return basicLambertPhongExample(THREE.MeshBasicMaterial, false, {
-          color: 'purple',
-        });
-      },
-    },
-    MeshLambertMaterialCompare: {
-      create() {
-        return basicLambertPhongExample(THREE.MeshLambertMaterial, false, {
-          color: 'black',
-          emissive: 'purple',
-        });
-      },
-    },
-    MeshPhongMaterialCompare: {
-      create() {
-        return basicLambertPhongExample(THREE.MeshPhongMaterial, false, {
-          color: 'black',
-          emissive: 'purple',
-          shininess: 0,
-        });
-      },
-    },
-    MeshToonMaterial: {
-      create() {
-        return basicLambertPhongExample(THREE.MeshToonMaterial);
-      },
-    },
-    MeshStandardMaterial: {
-      create(props) {
-        return makeStandardPhysicalMaterialGrid(props.renderInfo.elem, false);
-      },
-    },
-    MeshPhysicalMaterial: {
-      create(props) {
-        const settings = {
-          clearcoat: .5,
-          clearcoatRoughness: 0,
-        };
-
-        function addElem(parent, type, style = {}) {
-          const elem = document.createElement(type);
-          Object.assign(elem.style, style);
-          parent.appendChild(elem);
-          return elem;
-        }
-
-        function addRange(elem, obj, prop, min, max) {
-          const outer = addElem(elem, 'div', {
-            width: '100%',
-            textAlign: 'center',
-            'font-family': 'monospace',
-          });
-
-          const div = addElem(outer, 'div', {
-            textAlign: 'left',
-            display: 'inline-block',
-          });
-
-          const label = addElem(div, 'label', {
-            display: 'inline-block',
-            width: '12em',
-          });
-          label.textContent = prop;
-
-          const num = addElem(div, 'div', {
-            display: 'inline-block',
-            width: '3em',
-          });
-
-          function updateNum() {
-            num.textContent = obj[prop].toFixed(2);
-          }
-          updateNum();
-
-          const input = addElem(div, 'input', {
-          });
-          Object.assign(input, {
-            type: 'range',
-            min: 0,
-            max: 100,
-            value: (obj[prop] - min) / (max - min) * 100,
-          });
-          input.addEventListener('input', () => {
-            obj[prop] = min + (max - min) * input.value / 100;
-            updateNum();
-          });
-        }
-
-        const {elem} = props.renderInfo;
-        addRange(elem, settings, 'clearcoat', 0, 1);
-        addRange(elem, settings, 'clearcoatRoughness', 0, 1);
-        const area = addElem(elem, 'div', {
-          width: '100%',
-          height: '400px',
-        });
-
-        return makeStandardPhysicalMaterialGrid(area, true, (meshes) => {
-          meshes.forEach(row => row.forEach(mesh => {
-            mesh.material.clearcoat = settings.clearcoat;
-            mesh.material.clearcoatRoughness = settings.clearcoatRoughness;
-          }));
-        });
-      },
-    },
-    MeshDepthMaterial: {
-      create(props) {
-        const {camera} = props;
-        const radius = 4;
-        const tube = 1.5;
-        const radialSegments = 8;
-        const tubularSegments = 64;
-        const p = 2;
-        const q = 3;
-        const geometry = new THREE.TorusKnotGeometry(radius, tube, tubularSegments, radialSegments, p, q);
-        const material = new THREE.MeshDepthMaterial();
-        camera.near = 7;
-        camera.far = 20;
-        return new THREE.Mesh(geometry, material);
-      },
-    },
-    MeshNormalMaterial: {
-      create() {
-        const radius = 4;
-        const tube = 1.5;
-        const radialSegments = 8;
-        const tubularSegments = 64;
-        const p = 2;
-        const q = 3;
-        const geometry = new THREE.TorusKnotGeometry(radius, tube, tubularSegments, radialSegments, p, q);
-        const material = new THREE.MeshNormalMaterial();
-        return new THREE.Mesh(geometry, material);
-      },
-    },
-    sideDefault: {
-      create() {
-        return sideExample(THREE.FrontSide);
-      },
-    },
-    sideDouble: {
-      create() {
-        return sideExample(THREE.DoubleSide);
-      },
-    },
-  });
+
+	function makeSphere( widthDivisions, heightDivisions ) {
+
+		const radius = 7;
+		return new THREE.SphereGeometry( radius, widthDivisions, heightDivisions );
+
+	}
+
+	const highPolySphereGeometry = function () {
+
+		const widthDivisions = 100;
+		const heightDivisions = 50;
+		return makeSphere( widthDivisions, heightDivisions );
+
+	}();
+
+	const lowPolySphereGeometry = function () {
+
+		const widthDivisions = 12;
+		const heightDivisions = 9;
+		return makeSphere( widthDivisions, heightDivisions );
+
+	}();
+
+	function smoothOrFlat( flatShading, radius = 7 ) {
+
+		const widthDivisions = 12;
+		const heightDivisions = 9;
+		const geometry = new THREE.SphereGeometry( radius, widthDivisions, heightDivisions );
+		const material = new THREE.MeshPhongMaterial( {
+			flatShading,
+			color: 'hsl(300,50%,50%)',
+		} );
+		return new THREE.Mesh( geometry, material );
+
+	}
+
+	function basicLambertPhongExample( MaterialCtor, lowPoly, params = {} ) {
+
+		const geometry = lowPoly ? lowPolySphereGeometry : highPolySphereGeometry;
+		const material = new MaterialCtor( {
+			color: 'hsl(210,50%,50%)',
+			...params,
+		} );
+		return {
+			obj3D: new THREE.Mesh( geometry, material ),
+			trackball: lowPoly,
+		};
+
+	}
+
+	function sideExample( side ) {
+
+		const base = new THREE.Object3D();
+		const size = 6;
+		const geometry = new THREE.PlaneGeometry( size, size );
+		[
+			{ position: [ - 1, 0, 0 ], up: [ 0, 1, 0 ], },
+			{ position: [ 1, 0, 0 ], up: [ 0, - 1, 0 ], },
+			{ position: [ 0, - 1, 0 ], up: [ 0, 0, - 1 ], },
+			{ position: [ 0, 1, 0 ], up: [ 0, 0, 1 ], },
+			{ position: [ 0, 0, - 1 ], up: [ 1, 0, 0 ], },
+			{ position: [ 0, 0, 1 ], up: [ - 1, 0, 0 ], },
+		].forEach( ( settings, ndx ) => {
+
+			const material = new THREE.MeshBasicMaterial( { side } );
+			material.color.setHSL( ndx / 6, .5, .5 );
+			const mesh = new THREE.Mesh( geometry, material );
+			mesh.up.set( ...settings.up );
+			mesh.lookAt( ...settings.position );
+			mesh.position.set( ...settings.position ).multiplyScalar( size * .75 );
+			base.add( mesh );
+
+		} );
+		return base;
+
+	}
+
+	function makeStandardPhysicalMaterialGrid( elem, physical, update ) {
+
+		const numMetal = 5;
+		const numRough = 7;
+		const meshes = [];
+		const MatCtor = physical ? THREE.MeshPhysicalMaterial : THREE.MeshStandardMaterial;
+		const color = physical ? 'hsl(160,50%,50%)' : 'hsl(140,50%,50%)';
+		for ( let m = 0; m < numMetal; ++ m ) {
+
+			const row = [];
+			for ( let r = 0; r < numRough; ++ r ) {
+
+				const material = new MatCtor( {
+					color,
+					roughness: r / ( numRough - 1 ),
+					metalness: 1 - m / ( numMetal - 1 ),
+				} );
+				const mesh = new THREE.Mesh( highPolySphereGeometry, material );
+				row.push( mesh );
+
+			}
+
+			meshes.push( row );
+
+		}
+
+		return {
+			obj3D: null,
+			trackball: false,
+			render( renderInfo ) {
+
+				const { camera, scene, renderer } = renderInfo;
+				const rect = elem.getBoundingClientRect();
+
+				const width = ( rect.right - rect.left ) * renderInfo.pixelRatio;
+				const height = ( rect.bottom - rect.top ) * renderInfo.pixelRatio;
+				const left = rect.left * renderInfo.pixelRatio;
+				const bottom = ( renderer.domElement.clientHeight - rect.bottom ) * renderInfo.pixelRatio;
+
+				const cellSize = Math.min( width / numRough, height / numMetal ) | 0;
+				const xOff = ( width - cellSize * numRough ) / 2;
+				const yOff = ( height - cellSize * numMetal ) / 2;
+
+				camera.aspect = 1;
+				camera.updateProjectionMatrix();
+
+				if ( update ) {
+
+					update( meshes );
+
+				}
+
+				for ( let m = 0; m < numMetal; ++ m ) {
+
+					for ( let r = 0; r < numRough; ++ r ) {
+
+						const x = left + xOff + r * cellSize;
+						const y = bottom + yOff + m * cellSize;
+						renderer.setViewport( x, y, cellSize, cellSize );
+						renderer.setScissor( x, y, cellSize, cellSize );
+						const mesh = meshes[ m ][ r ];
+						scene.add( mesh );
+						renderer.render( scene, camera );
+						scene.remove( mesh );
+
+					}
+
+				}
+
+			},
+		};
+
+	}
+
+	threejsLessonUtils.addDiagrams( {
+		smoothShading: {
+			create() {
+
+				return smoothOrFlat( false );
+
+			},
+		},
+		flatShading: {
+			create() {
+
+				return smoothOrFlat( true );
+
+			},
+		},
+		MeshBasicMaterial: {
+			create() {
+
+				return basicLambertPhongExample( THREE.MeshBasicMaterial );
+
+			},
+		},
+		MeshLambertMaterial: {
+			create() {
+
+				return basicLambertPhongExample( THREE.MeshLambertMaterial );
+
+			},
+		},
+		MeshPhongMaterial: {
+			create() {
+
+				return basicLambertPhongExample( THREE.MeshPhongMaterial );
+
+			},
+		},
+		MeshBasicMaterialLowPoly: {
+			create() {
+
+				return basicLambertPhongExample( THREE.MeshBasicMaterial, true );
+
+			},
+		},
+		MeshLambertMaterialLowPoly: {
+			create() {
+
+				return basicLambertPhongExample( THREE.MeshLambertMaterial, true );
+
+			},
+		},
+		MeshPhongMaterialLowPoly: {
+			create() {
+
+				return basicLambertPhongExample( THREE.MeshPhongMaterial, true );
+
+			},
+		},
+		MeshPhongMaterialShininess0: {
+			create() {
+
+				return basicLambertPhongExample( THREE.MeshPhongMaterial, false, {
+					color: 'red',
+					shininess: 0,
+				} );
+
+			},
+		},
+		MeshPhongMaterialShininess30: {
+			create() {
+
+				return basicLambertPhongExample( THREE.MeshPhongMaterial, false, {
+					color: 'red',
+					shininess: 30,
+				} );
+
+			},
+		},
+		MeshPhongMaterialShininess150: {
+			create() {
+
+				return basicLambertPhongExample( THREE.MeshPhongMaterial, false, {
+					color: 'red',
+					shininess: 150,
+				} );
+
+			},
+		},
+		MeshBasicMaterialCompare: {
+			create() {
+
+				return basicLambertPhongExample( THREE.MeshBasicMaterial, false, {
+					color: 'purple',
+				} );
+
+			},
+		},
+		MeshLambertMaterialCompare: {
+			create() {
+
+				return basicLambertPhongExample( THREE.MeshLambertMaterial, false, {
+					color: 'black',
+					emissive: 'purple',
+				} );
+
+			},
+		},
+		MeshPhongMaterialCompare: {
+			create() {
+
+				return basicLambertPhongExample( THREE.MeshPhongMaterial, false, {
+					color: 'black',
+					emissive: 'purple',
+					shininess: 0,
+				} );
+
+			},
+		},
+		MeshToonMaterial: {
+			create() {
+
+				return basicLambertPhongExample( THREE.MeshToonMaterial );
+
+			},
+		},
+		MeshStandardMaterial: {
+			create( props ) {
+
+				return makeStandardPhysicalMaterialGrid( props.renderInfo.elem, false );
+
+			},
+		},
+		MeshPhysicalMaterial: {
+			create( props ) {
+
+				const settings = {
+					clearcoat: .5,
+					clearcoatRoughness: 0,
+				};
+
+				function addElem( parent, type, style = {} ) {
+
+					const elem = document.createElement( type );
+					Object.assign( elem.style, style );
+					parent.appendChild( elem );
+					return elem;
+
+				}
+
+				function addRange( elem, obj, prop, min, max ) {
+
+					const outer = addElem( elem, 'div', {
+						width: '100%',
+						textAlign: 'center',
+						'font-family': 'monospace',
+					} );
+
+					const div = addElem( outer, 'div', {
+						textAlign: 'left',
+						display: 'inline-block',
+					} );
+
+					const label = addElem( div, 'label', {
+						display: 'inline-block',
+						width: '12em',
+					} );
+					label.textContent = prop;
+
+					const num = addElem( div, 'div', {
+						display: 'inline-block',
+						width: '3em',
+					} );
+
+					function updateNum() {
+
+						num.textContent = obj[ prop ].toFixed( 2 );
+
+					}
+
+					updateNum();
+
+					const input = addElem( div, 'input', {
+					} );
+					Object.assign( input, {
+						type: 'range',
+						min: 0,
+						max: 100,
+						value: ( obj[ prop ] - min ) / ( max - min ) * 100,
+					} );
+					input.addEventListener( 'input', () => {
+
+						obj[ prop ] = min + ( max - min ) * input.value / 100;
+						updateNum();
+
+					} );
+
+				}
+
+				const { elem } = props.renderInfo;
+				addRange( elem, settings, 'clearcoat', 0, 1 );
+				addRange( elem, settings, 'clearcoatRoughness', 0, 1 );
+				const area = addElem( elem, 'div', {
+					width: '100%',
+					height: '400px',
+				} );
+
+				return makeStandardPhysicalMaterialGrid( area, true, ( meshes ) => {
+
+					meshes.forEach( row => row.forEach( mesh => {
+
+						mesh.material.clearcoat = settings.clearcoat;
+						mesh.material.clearcoatRoughness = settings.clearcoatRoughness;
+
+					} ) );
+
+				} );
+
+			},
+		},
+		MeshDepthMaterial: {
+			create( props ) {
+
+				const { camera } = props;
+				const radius = 4;
+				const tube = 1.5;
+				const radialSegments = 8;
+				const tubularSegments = 64;
+				const p = 2;
+				const q = 3;
+				const geometry = new THREE.TorusKnotGeometry( radius, tube, tubularSegments, radialSegments, p, q );
+				const material = new THREE.MeshDepthMaterial();
+				camera.near = 7;
+				camera.far = 20;
+				return new THREE.Mesh( geometry, material );
+
+			},
+		},
+		MeshNormalMaterial: {
+			create() {
+
+				const radius = 4;
+				const tube = 1.5;
+				const radialSegments = 8;
+				const tubularSegments = 64;
+				const p = 2;
+				const q = 3;
+				const geometry = new THREE.TorusKnotGeometry( radius, tube, tubularSegments, radialSegments, p, q );
+				const material = new THREE.MeshNormalMaterial();
+				return new THREE.Mesh( geometry, material );
+
+			},
+		},
+		sideDefault: {
+			create() {
+
+				return sideExample( THREE.FrontSide );
+
+			},
+		},
+		sideDouble: {
+			create() {
+
+				return sideExample( THREE.DoubleSide );
+
+			},
+		},
+	} );
+
 }
 

+ 138 - 98
manual/resources/threejs-post-processing-3dlut.js

@@ -1,101 +1,141 @@
 {
-    class Waiter {
-      constructor() {
-        this.promise = new Promise((resolve) => {
-          this.resolve = resolve;
-        });
-      }
-    }
-
-    async function getSVGDocument(elem) {
-      const data = elem.data;
-      elem.data = '';
-      elem.data = data;
-      const waiter = new Waiter();
-      elem.addEventListener('load', waiter.resolve);
-      await waiter.promise;
-      return elem.getSVGDocument();
-    }
-
-  const diagrams = {
-    lookup: {
-      async init(elem) {
-        const svg = await getSVGDocument(elem);
-        const partsByName = {};
-        [
-          '[id$=-Input]',
-          '[id$=-Output]',
-          '[id$=-Result]',
-        ].forEach((selector) => {
-          [...svg.querySelectorAll('[id^=Effect]')].forEach((elem) => {
-            // because affinity designer doesn't export blend modes (T_T)
-            // and because I'd prefer not to have to manually fix things as I edit.
-            // I suppose I could add a build process.
-            elem.style.mixBlendMode = elem.id.split('-')[1];
-          });
-          [...svg.querySelectorAll(selector)].forEach((elem) => {
-            const [name, type] = elem.id.split('-');
-            partsByName[name] = partsByName[name] || {};
-            partsByName[name][type] = elem;
-            elem.style.visibility = 'hidden';
-          });
-        });
-        const parts = Object.keys(partsByName).sort().map(k => partsByName[k]);
-        let ndx = 0;
-        let step = 0;
-        let delay = 0;
-        setInterval(() => {
-          const part = parts[ndx];
-          switch (step) {
-            case 0:
-              part.Input.style.visibility = '';
-              ++step;
-              break;
-            case 1:
-              part.Output.style.visibility = '';
-              ++step;
-              break;
-            case 2:
-              part.Result.style.visibility = '';
-              ++step;
-              break;
-            case 3:
-              part.Input.style.visibility = 'hidden';
-              part.Output.style.visibility = 'hidden';
-              ndx = (ndx + 1) % parts.length;
-              if (ndx === 0) {
-                step = 4;
-                delay = 4;
-              } else {
-                step = 0;
-              }
-              break;
-            case 4:
-              --delay;
-              if (delay <= 0) {
-                for (const part of parts) {
-                  for (const elem of Object.values(part)) {
-                    elem.style.visibility = 'hidden';
-                  }
-                }
-                step = 0;
-              }
-              break;
-          }
-        }, 500);
-      },
-    },
-  };
-
-  [...document.querySelectorAll('[data-diagram]')].forEach(createDiagram);
-
-  function createDiagram(base) {
-    const name = base.dataset.diagram;
-    const info = diagrams[name];
-    if (!info) {
-      throw new Error(`no diagram ${name}`);
-    }
-    info.init(base);
-  }
+
+	class Waiter {
+
+		constructor() {
+
+			this.promise = new Promise( ( resolve ) => {
+
+				this.resolve = resolve;
+
+			} );
+
+		}
+
+	}
+
+	async function getSVGDocument( elem ) {
+
+		const data = elem.data;
+		elem.data = '';
+		elem.data = data;
+		const waiter = new Waiter();
+		elem.addEventListener( 'load', waiter.resolve );
+		await waiter.promise;
+		return elem.getSVGDocument();
+
+	}
+
+	const diagrams = {
+		lookup: {
+			async init( elem ) {
+
+				const svg = await getSVGDocument( elem );
+				const partsByName = {};
+				[
+					'[id$=-Input]',
+					'[id$=-Output]',
+					'[id$=-Result]',
+				].forEach( ( selector ) => {
+
+					[ ...svg.querySelectorAll( '[id^=Effect]' ) ].forEach( ( elem ) => {
+
+						// because affinity designer doesn't export blend modes (T_T)
+						// and because I'd prefer not to have to manually fix things as I edit.
+						// I suppose I could add a build process.
+						elem.style.mixBlendMode = elem.id.split( '-' )[ 1 ];
+
+					} );
+					[ ...svg.querySelectorAll( selector ) ].forEach( ( elem ) => {
+
+						const [ name, type ] = elem.id.split( '-' );
+						partsByName[ name ] = partsByName[ name ] || {};
+						partsByName[ name ][ type ] = elem;
+						elem.style.visibility = 'hidden';
+
+					} );
+
+				} );
+				const parts = Object.keys( partsByName ).sort().map( k => partsByName[ k ] );
+				let ndx = 0;
+				let step = 0;
+				let delay = 0;
+				setInterval( () => {
+
+					const part = parts[ ndx ];
+					switch ( step ) {
+
+						case 0:
+							part.Input.style.visibility = '';
+							++ step;
+							break;
+						case 1:
+							part.Output.style.visibility = '';
+							++ step;
+							break;
+						case 2:
+							part.Result.style.visibility = '';
+							++ step;
+							break;
+						case 3:
+							part.Input.style.visibility = 'hidden';
+							part.Output.style.visibility = 'hidden';
+							ndx = ( ndx + 1 ) % parts.length;
+							if ( ndx === 0 ) {
+
+								step = 4;
+								delay = 4;
+
+							} else {
+
+								step = 0;
+
+							}
+
+							break;
+						case 4:
+							-- delay;
+							if ( delay <= 0 ) {
+
+								for ( const part of parts ) {
+
+									for ( const elem of Object.values( part ) ) {
+
+										elem.style.visibility = 'hidden';
+
+									}
+
+								}
+
+								step = 0;
+
+							}
+
+							break;
+
+					}
+
+				}, 500 );
+
+			},
+		},
+	};
+
+	[ ...document.querySelectorAll( '[data-diagram]' ) ].forEach( createDiagram );
+
+	function createDiagram( base ) {
+
+		const name = base.dataset.diagram;
+		const info = diagrams[ name ];
+		if ( ! info ) {
+
+			throw new Error( `no diagram ${name}` );
+
+		}
+
+		info.init( base );
+
+	}
+
 }
 

Diff do ficheiro suprimidas por serem muito extensas
+ 580 - 481
manual/resources/threejs-primitives.js


+ 290 - 244
manual/resources/threejs-textures.js

@@ -1,249 +1,295 @@
 import * as THREE from 'three';
-import {threejsLessonUtils} from './threejs-lesson-utils.js';
+import { threejsLessonUtils } from './threejs-lesson-utils.js';
 
 {
-  const loader = new THREE.TextureLoader();
-
-  function loadTextureAndPromise(url) {
-    let textureResolve;
-    const promise = new Promise((resolve) => {
-      textureResolve = resolve;
-    });
-    const texture = loader.load(url, (texture) => {
-      textureResolve(texture);
-    });
-    return {
-      texture,
-      promise,
-    };
-  }
-
-  const filterTextureInfo = loadTextureAndPromise('/manual/resources/images/mip-example.png');
-  const filterTexture = filterTextureInfo.texture;
-  const filterTexturePromise = filterTextureInfo.promise;
-
-  function filterCube(scale, texture) {
-    const size = 8;
-    const geometry = new THREE.BoxGeometry(size, size, size);
-    const material = new THREE.MeshBasicMaterial({
-      map: texture || filterTexture,
-    });
-    const mesh = new THREE.Mesh(geometry, material);
-    mesh.scale.set(scale, scale, scale);
-    return mesh;
-  }
-
-  function lowResCube(scale, pixelSize = 16) {
-    const mesh = filterCube(scale);
-    const renderTarget = new THREE.WebGLRenderTarget(1, 1, {
-      magFilter: THREE.NearestFilter,
-      minFilter: THREE.NearestFilter,
-    });
-
-    const planeScene = new THREE.Scene();
-
-    const plane = new THREE.PlaneGeometry(1, 1);
-    const planeMaterial = new THREE.MeshBasicMaterial({
-      map: renderTarget.texture,
-    });
-    const planeMesh = new THREE.Mesh(plane, planeMaterial);
-    planeScene.add(planeMesh);
-
-    const planeCamera = new THREE.OrthographicCamera(0, 1, 0, 1, -1, 1);
-    planeCamera.position.z = 1;
-
-    return {
-      obj3D: mesh,
-      update(time, renderInfo) {
-        const { width, height, scene, camera, renderer, pixelRatio } = renderInfo;
-        const rtWidth = Math.ceil(width / pixelRatio / pixelSize);
-        const rtHeight = Math.ceil(height / pixelRatio / pixelSize);
-        renderTarget.setSize(rtWidth, rtHeight);
-
-        camera.aspect = rtWidth / rtHeight;
-        camera.updateProjectionMatrix();
-
-        renderer.setRenderTarget(renderTarget);
-
-        renderer.render(scene, camera);
-        renderer.setRenderTarget(null);
-      },
-      render(renderInfo) {
-        const { width, height, renderer, pixelRatio } = renderInfo;
-        const viewWidth = width / pixelRatio / pixelSize;
-        const viewHeight = height / pixelRatio / pixelSize;
-        planeCamera.left = -viewWidth / 2;
-        planeCamera.right = viewWidth / 2;
-        planeCamera.top = viewHeight / 2;
-        planeCamera.bottom = -viewHeight / 2;
-        planeCamera.updateProjectionMatrix();
-
-        // compute the difference between our renderTarget size
-        // and the view size. The renderTarget is a multiple pixels magnified pixels
-        // so for example if the view is 15 pixels wide and the magnified pixel size is 10
-        // the renderTarget will be 20 pixels wide. We only want to display 15 of those 20
-        // pixels so
-
-        planeMesh.scale.set(renderTarget.width, renderTarget.height, 1);
-
-        renderer.render(planeScene, planeCamera);
-      },
-    };
-  }
-
-  function createMip(level, numLevels, scale) {
-    const u = level / numLevels;
-    const size = 2 ** (numLevels - level - 1);
-    const halfSize = Math.ceil(size / 2);
-    const ctx = document.createElement('canvas').getContext('2d');
-    ctx.canvas.width = size * scale;
-    ctx.canvas.height = size * scale;
-    ctx.scale(scale, scale);
-    ctx.fillStyle = `hsl(${180 + u * 360 | 0},100%,20%)`;
-    ctx.fillRect(0, 0, size, size);
-    ctx.fillStyle = `hsl(${u * 360 | 0},100%,50%)`;
-    ctx.fillRect(0, 0, halfSize, halfSize);
-    ctx.fillRect(halfSize, halfSize, halfSize, halfSize);
-    return ctx.canvas;
-  }
-
-  threejsLessonUtils.init({
-    threejsOptions: {antialias: false},
-  });
-  threejsLessonUtils.addDiagrams({
-    filterCube: {
-      create() {
-        return filterCube(1);
-      },
-    },
-    filterCubeSmall: {
-      create(info) {
-        return lowResCube(.1, info.renderInfo.pixelRatio);
-      },
-    },
-    filterCubeSmallLowRes: {
-      create() {
-        return lowResCube(1);
-      },
-    },
-    filterCubeMagNearest: {
-      async create() {
-        const texture = await filterTexturePromise;
-        const newTexture = texture.clone();
-        newTexture.magFilter = THREE.NearestFilter;
-        newTexture.needsUpdate = true;
-        return filterCube(1, newTexture);
-      },
-    },
-    filterCubeMagLinear: {
-      async create() {
-        const texture = await filterTexturePromise;
-        const newTexture = texture.clone();
-        newTexture.magFilter = THREE.LinearFilter;
-        newTexture.needsUpdate = true;
-        return filterCube(1, newTexture);
-      },
-    },
-    filterModes: {
-      async create(props) {
-        const { scene, camera, renderInfo } = props;
-        scene.background = new THREE.Color('black');
-        camera.far = 150;
-        const texture = await filterTexturePromise;
-        const root = new THREE.Object3D();
-        const depth = 50;
-        const plane = new THREE.PlaneGeometry(1, depth);
-        const mipmap = [];
-        const numMips = 7;
-        for (let i = 0; i < numMips; ++i) {
-          mipmap.push(createMip(i, numMips, 1));
-        }
-
-        // Is this a design flaw in three.js?
-        // AFAIK there's no way to clone a texture really
-        // Textures can share an image and I guess deep down
-        // if the image is the same they might share a WebGLTexture
-        // but no checks for mipmaps I'm guessing. It seems like
-        // they shouldn't be checking for same image, the should be
-        // checking for same WebGLTexture. Given there is more than
-        // WebGL to support maybe they need to abtract WebGLTexture to
-        // PlatformTexture or something?
-
-        const meshInfos = [
-          { x: -1, y:  1, minFilter: THREE.NearestFilter,              magFilter: THREE.NearestFilter },
-          { x:  0, y:  1, minFilter: THREE.LinearFilter,               magFilter: THREE.LinearFilter },
-          { x:  1, y:  1, minFilter: THREE.NearestMipmapNearestFilter, magFilter: THREE.LinearFilter },
-          { x: -1, y: -1, minFilter: THREE.NearestMipmapLinearFilter,  magFilter: THREE.LinearFilter },
-          { x:  0, y: -1, minFilter: THREE.LinearMipmapNearestFilter,  magFilter: THREE.LinearFilter },
-          { x:  1, y: -1, minFilter: THREE.LinearMipmapLinearFilter,   magFilter: THREE.LinearFilter },
-        ].map((info) => {
-          const copyTexture = texture.clone();
-          copyTexture.minFilter = info.minFilter;
-          copyTexture.magFilter = info.magFilter;
-          copyTexture.wrapT = THREE.RepeatWrapping;
-          copyTexture.repeat.y = depth;
-          copyTexture.needsUpdate = true;
-
-          const mipTexture = new THREE.CanvasTexture(mipmap[0]);
-          mipTexture.mipmaps = mipmap;
-          mipTexture.minFilter = info.minFilter;
-          mipTexture.magFilter = info.magFilter;
-          mipTexture.wrapT = THREE.RepeatWrapping;
-          mipTexture.repeat.y = depth;
-
-          const material = new THREE.MeshBasicMaterial({
-            map: copyTexture,
-          });
-
-          const mesh = new THREE.Mesh(plane, material);
-          mesh.rotation.x = Math.PI * .5 * info.y;
-          mesh.position.x = info.x * 1.5;
-          mesh.position.y = info.y;
-          root.add(mesh);
-          return {
-            material,
-            copyTexture,
-            mipTexture,
-          };
-        });
-        scene.add(root);
-
-        renderInfo.elem.addEventListener('click', () => {
-          for (const meshInfo of meshInfos) {
-            const { material, copyTexture, mipTexture } = meshInfo;
-            material.map = material.map === copyTexture ? mipTexture : copyTexture;
-          }
-        });
-
-        return {
-          update(time, renderInfo) {
-            const {camera} = renderInfo;
-            camera.position.y = Math.sin(time * .2) * .5;
-          },
-          trackball: false,
-        };
-      },
-    },
-  });
-
-  const textureDiagrams = {
-    differentColoredMips(parent) {
-      const numMips = 7;
-      for (let i = 0; i < numMips; ++i) {
-        const elem = createMip(i, numMips, 4);
-        elem.className = 'border';
-        elem.style.margin = '1px';
-        parent.appendChild(elem);
-      }
-    },
-  };
-
-  function createTextureDiagram(elem) {
-    const name = elem.dataset.textureDiagram;
-    const info = textureDiagrams[name];
-    info(elem);
-  }
-
-  [...document.querySelectorAll('[data-texture-diagram]')].forEach(createTextureDiagram);
+
+	const loader = new THREE.TextureLoader();
+
+	function loadTextureAndPromise( url ) {
+
+		let textureResolve;
+		const promise = new Promise( ( resolve ) => {
+
+			textureResolve = resolve;
+
+		} );
+		const texture = loader.load( url, ( texture ) => {
+
+			textureResolve( texture );
+
+		} );
+		return {
+			texture,
+			promise,
+		};
+
+	}
+
+	const filterTextureInfo = loadTextureAndPromise( '/manual/resources/images/mip-example.png' );
+	const filterTexture = filterTextureInfo.texture;
+	const filterTexturePromise = filterTextureInfo.promise;
+
+	function filterCube( scale, texture ) {
+
+		const size = 8;
+		const geometry = new THREE.BoxGeometry( size, size, size );
+		const material = new THREE.MeshBasicMaterial( {
+			map: texture || filterTexture,
+		} );
+		const mesh = new THREE.Mesh( geometry, material );
+		mesh.scale.set( scale, scale, scale );
+		return mesh;
+
+	}
+
+	function lowResCube( scale, pixelSize = 16 ) {
+
+		const mesh = filterCube( scale );
+		const renderTarget = new THREE.WebGLRenderTarget( 1, 1, {
+			magFilter: THREE.NearestFilter,
+			minFilter: THREE.NearestFilter,
+		} );
+
+		const planeScene = new THREE.Scene();
+
+		const plane = new THREE.PlaneGeometry( 1, 1 );
+		const planeMaterial = new THREE.MeshBasicMaterial( {
+			map: renderTarget.texture,
+		} );
+		const planeMesh = new THREE.Mesh( plane, planeMaterial );
+		planeScene.add( planeMesh );
+
+		const planeCamera = new THREE.OrthographicCamera( 0, 1, 0, 1, - 1, 1 );
+		planeCamera.position.z = 1;
+
+		return {
+			obj3D: mesh,
+			update( time, renderInfo ) {
+
+				const { width, height, scene, camera, renderer, pixelRatio } = renderInfo;
+				const rtWidth = Math.ceil( width / pixelRatio / pixelSize );
+				const rtHeight = Math.ceil( height / pixelRatio / pixelSize );
+				renderTarget.setSize( rtWidth, rtHeight );
+
+				camera.aspect = rtWidth / rtHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setRenderTarget( renderTarget );
+
+				renderer.render( scene, camera );
+				renderer.setRenderTarget( null );
+
+			},
+			render( renderInfo ) {
+
+				const { width, height, renderer, pixelRatio } = renderInfo;
+				const viewWidth = width / pixelRatio / pixelSize;
+				const viewHeight = height / pixelRatio / pixelSize;
+				planeCamera.left = - viewWidth / 2;
+				planeCamera.right = viewWidth / 2;
+				planeCamera.top = viewHeight / 2;
+				planeCamera.bottom = - viewHeight / 2;
+				planeCamera.updateProjectionMatrix();
+
+				// compute the difference between our renderTarget size
+				// and the view size. The renderTarget is a multiple pixels magnified pixels
+				// so for example if the view is 15 pixels wide and the magnified pixel size is 10
+				// the renderTarget will be 20 pixels wide. We only want to display 15 of those 20
+				// pixels so
+
+				planeMesh.scale.set( renderTarget.width, renderTarget.height, 1 );
+
+				renderer.render( planeScene, planeCamera );
+
+			},
+		};
+
+	}
+
+	function createMip( level, numLevels, scale ) {
+
+		const u = level / numLevels;
+		const size = 2 ** ( numLevels - level - 1 );
+		const halfSize = Math.ceil( size / 2 );
+		const ctx = document.createElement( 'canvas' ).getContext( '2d' );
+		ctx.canvas.width = size * scale;
+		ctx.canvas.height = size * scale;
+		ctx.scale( scale, scale );
+		ctx.fillStyle = `hsl(${180 + u * 360 | 0},100%,20%)`;
+		ctx.fillRect( 0, 0, size, size );
+		ctx.fillStyle = `hsl(${u * 360 | 0},100%,50%)`;
+		ctx.fillRect( 0, 0, halfSize, halfSize );
+		ctx.fillRect( halfSize, halfSize, halfSize, halfSize );
+		return ctx.canvas;
+
+	}
+
+	threejsLessonUtils.init( {
+		threejsOptions: { antialias: false },
+	} );
+	threejsLessonUtils.addDiagrams( {
+		filterCube: {
+			create() {
+
+				return filterCube( 1 );
+
+			},
+		},
+		filterCubeSmall: {
+			create( info ) {
+
+				return lowResCube( .1, info.renderInfo.pixelRatio );
+
+			},
+		},
+		filterCubeSmallLowRes: {
+			create() {
+
+				return lowResCube( 1 );
+
+			},
+		},
+		filterCubeMagNearest: {
+			async create() {
+
+				const texture = await filterTexturePromise;
+				const newTexture = texture.clone();
+				newTexture.magFilter = THREE.NearestFilter;
+				newTexture.needsUpdate = true;
+				return filterCube( 1, newTexture );
+
+			},
+		},
+		filterCubeMagLinear: {
+			async create() {
+
+				const texture = await filterTexturePromise;
+				const newTexture = texture.clone();
+				newTexture.magFilter = THREE.LinearFilter;
+				newTexture.needsUpdate = true;
+				return filterCube( 1, newTexture );
+
+			},
+		},
+		filterModes: {
+			async create( props ) {
+
+				const { scene, camera, renderInfo } = props;
+				scene.background = new THREE.Color( 'black' );
+				camera.far = 150;
+				const texture = await filterTexturePromise;
+				const root = new THREE.Object3D();
+				const depth = 50;
+				const plane = new THREE.PlaneGeometry( 1, depth );
+				const mipmap = [];
+				const numMips = 7;
+				for ( let i = 0; i < numMips; ++ i ) {
+
+					mipmap.push( createMip( i, numMips, 1 ) );
+
+				}
+
+				// Is this a design flaw in three.js?
+				// AFAIK there's no way to clone a texture really
+				// Textures can share an image and I guess deep down
+				// if the image is the same they might share a WebGLTexture
+				// but no checks for mipmaps I'm guessing. It seems like
+				// they shouldn't be checking for same image, the should be
+				// checking for same WebGLTexture. Given there is more than
+				// WebGL to support maybe they need to abtract WebGLTexture to
+				// PlatformTexture or something?
+
+				const meshInfos = [
+					{ x: - 1, y: 1, minFilter: THREE.NearestFilter, magFilter: THREE.NearestFilter },
+					{ x: 0, y: 1, minFilter: THREE.LinearFilter, magFilter: THREE.LinearFilter },
+					{ x: 1, y: 1, minFilter: THREE.NearestMipmapNearestFilter, magFilter: THREE.LinearFilter },
+					{ x: - 1, y: - 1, minFilter: THREE.NearestMipmapLinearFilter, magFilter: THREE.LinearFilter },
+					{ x: 0, y: - 1, minFilter: THREE.LinearMipmapNearestFilter, magFilter: THREE.LinearFilter },
+					{ x: 1, y: - 1, minFilter: THREE.LinearMipmapLinearFilter, magFilter: THREE.LinearFilter },
+				].map( ( info ) => {
+
+					const copyTexture = texture.clone();
+					copyTexture.minFilter = info.minFilter;
+					copyTexture.magFilter = info.magFilter;
+					copyTexture.wrapT = THREE.RepeatWrapping;
+					copyTexture.repeat.y = depth;
+					copyTexture.needsUpdate = true;
+
+					const mipTexture = new THREE.CanvasTexture( mipmap[ 0 ] );
+					mipTexture.mipmaps = mipmap;
+					mipTexture.minFilter = info.minFilter;
+					mipTexture.magFilter = info.magFilter;
+					mipTexture.wrapT = THREE.RepeatWrapping;
+					mipTexture.repeat.y = depth;
+
+					const material = new THREE.MeshBasicMaterial( {
+						map: copyTexture,
+					} );
+
+					const mesh = new THREE.Mesh( plane, material );
+					mesh.rotation.x = Math.PI * .5 * info.y;
+					mesh.position.x = info.x * 1.5;
+					mesh.position.y = info.y;
+					root.add( mesh );
+					return {
+						material,
+						copyTexture,
+						mipTexture,
+					};
+
+				} );
+				scene.add( root );
+
+				renderInfo.elem.addEventListener( 'click', () => {
+
+					for ( const meshInfo of meshInfos ) {
+
+						const { material, copyTexture, mipTexture } = meshInfo;
+						material.map = material.map === copyTexture ? mipTexture : copyTexture;
+
+					}
+
+				} );
+
+				return {
+					update( time, renderInfo ) {
+
+						const { camera } = renderInfo;
+						camera.position.y = Math.sin( time * .2 ) * .5;
+
+					},
+					trackball: false,
+				};
+
+			},
+		},
+	} );
+
+	const textureDiagrams = {
+		differentColoredMips( parent ) {
+
+			const numMips = 7;
+			for ( let i = 0; i < numMips; ++ i ) {
+
+				const elem = createMip( i, numMips, 4 );
+				elem.className = 'border';
+				elem.style.margin = '1px';
+				parent.appendChild( elem );
+
+			}
+
+		},
+	};
+
+	function createTextureDiagram( elem ) {
+
+		const name = elem.dataset.textureDiagram;
+		const info = textureDiagrams[ name ];
+		info( elem );
+
+	}
+
+	[ ...document.querySelectorAll( '[data-texture-diagram]' ) ].forEach( createTextureDiagram );
+
 }
 

+ 71 - 58
manual/resources/threejs-voxel-geometry.js

@@ -1,64 +1,77 @@
 import * as THREE from 'three';
 import * as BufferGeometryUtils from '../../examples/jsm/utils/BufferGeometryUtils.js';
-import {threejsLessonUtils} from './threejs-lesson-utils.js';
+import { threejsLessonUtils } from './threejs-lesson-utils.js';
 
 {
-  const darkMatcher = window.matchMedia('(prefers-color-scheme: dark)');
-  const isDarkMode = darkMatcher.matches;
-
-  const darkColors = {
-    wire: '#DDD',
-  };
-  const lightColors = {
-    wire: '#000',
-  };
-  const colors = isDarkMode ? darkColors : lightColors;
-
-
-  threejsLessonUtils.addDiagrams({
-    mergedCubes: {
-      create() {
-        const geometries = [];
-        const width = 3;
-        const height = 2;
-        const depth = 2;
-        for (let y = 0; y < height; ++y) {
-          for (let z = 0; z < depth; ++z) {
-            for (let x = 0; x < width; ++x) {
-              const geometry = new THREE.BoxGeometry(1, 1, 1);
-              geometry.applyMatrix4((new THREE.Matrix4()).makeTranslation(x, y, z));
-              geometries.push(geometry);
-            }
-          }
-        }
-        const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries(geometries, false);
-        const material = new THREE.MeshBasicMaterial({
-          color: colors.wire,
-          wireframe: true,
-        });
-        const mesh = new THREE.Mesh(mergedGeometry, material);
-        mesh.position.set(
-            0.5 - width / 2,
-            0.5 - height / 2,
-            0.5 - depth / 2);
-        const base = new THREE.Object3D();
-        base.add(mesh);
-        base.scale.setScalar(3.5);
-        return base;
-      },
-    },
-    culledCubes: {
-      create() {
-        const geometry = new THREE.BoxGeometry(3, 2, 2, 3, 2, 2);
-        const material = new THREE.MeshBasicMaterial({
-          color: colors.wire,
-          wireframe: true,
-        });
-        const mesh = new THREE.Mesh(geometry, material);
-        mesh.scale.setScalar(3.5);
-        return mesh;
-      },
-    },
-  });
+
+	const darkMatcher = window.matchMedia( '(prefers-color-scheme: dark)' );
+	const isDarkMode = darkMatcher.matches;
+
+	const darkColors = {
+		wire: '#DDD',
+	};
+	const lightColors = {
+		wire: '#000',
+	};
+	const colors = isDarkMode ? darkColors : lightColors;
+
+
+	threejsLessonUtils.addDiagrams( {
+		mergedCubes: {
+			create() {
+
+				const geometries = [];
+				const width = 3;
+				const height = 2;
+				const depth = 2;
+				for ( let y = 0; y < height; ++ y ) {
+
+					for ( let z = 0; z < depth; ++ z ) {
+
+						for ( let x = 0; x < width; ++ x ) {
+
+							const geometry = new THREE.BoxGeometry( 1, 1, 1 );
+							geometry.applyMatrix4( ( new THREE.Matrix4() ).makeTranslation( x, y, z ) );
+							geometries.push( geometry );
+
+						}
+
+					}
+
+				}
+
+				const mergedGeometry = BufferGeometryUtils.mergeBufferGeometries( geometries, false );
+				const material = new THREE.MeshBasicMaterial( {
+					color: colors.wire,
+					wireframe: true,
+				} );
+				const mesh = new THREE.Mesh( mergedGeometry, material );
+				mesh.position.set(
+					0.5 - width / 2,
+					0.5 - height / 2,
+					0.5 - depth / 2 );
+				const base = new THREE.Object3D();
+				base.add( mesh );
+				base.scale.setScalar( 3.5 );
+				return base;
+
+			},
+		},
+		culledCubes: {
+			create() {
+
+				const geometry = new THREE.BoxGeometry( 3, 2, 2, 3, 2, 2 );
+				const material = new THREE.MeshBasicMaterial( {
+					color: colors.wire,
+					wireframe: true,
+				} );
+				const mesh = new THREE.Mesh( geometry, material );
+				mesh.scale.setScalar( 3.5 );
+				return mesh;
+
+			},
+		},
+	} );
+
 }
 

+ 30 - 20
manual/resources/tools/geo-picking/make-geo-picking-texture-ogc.js

@@ -2,31 +2,41 @@
 /* global process */
 /* global parseGeom */
 /* global Buffer */
-const fs = require('fs');
-const path = require('path');
-require('./ogc-parser');
+const fs = require( 'fs' );
+const path = require( 'path' );
+require( './ogc-parser' );
 
-const baseDir = process.argv[2];
+const baseDir = process.argv[ 2 ];
+
+function readJSON( name ) {
+
+	return JSON.parse( fs.readFileSync( path.join( baseDir, name ), { encoding: 'utf-8' } ) );
 
-function readJSON(name) {
-  return JSON.parse(fs.readFileSync(path.join(baseDir, name), {encoding: 'utf-8'}));
 }
 
 function main() {
-  const areas = readJSON('level1.json');
-  areas.forEach((area, ndx) => {
-    console.log(ndx);
-    try {
-      const buf = new Uint8Array(Buffer.from(area.geom, 'base64'));
-      area.geom = parseGeom(buf);
-    } catch (e) {
-      console.log('ERROR:', e);
-      console.log(JSON.stringify(area, null, 2));
-      throw e;
-    }
-  });
-
-  console.log(JSON.stringify(areas, null, 2));
+
+	const areas = readJSON( 'level1.json' );
+	areas.forEach( ( area, ndx ) => {
+
+		console.log( ndx );
+		try {
+
+			const buf = new Uint8Array( Buffer.from( area.geom, 'base64' ) );
+			area.geom = parseGeom( buf );
+
+		} catch ( e ) {
+
+			console.log( 'ERROR:', e );
+			console.log( JSON.stringify( area, null, 2 ) );
+			throw e;
+
+		}
+
+	} );
+
+	console.log( JSON.stringify( areas, null, 2 ) );
+
 }
 
 main();

+ 298 - 228
manual/resources/tools/geo-picking/make-geo-picking-texture.js

@@ -6,236 +6,306 @@
 /* eslint no-unused-vars: off */
 
 async function main() {
-  const size = 4096;
-  const pickCtx = document.querySelector('#pick').getContext('2d');
-  pickCtx.canvas.width = size;
-  pickCtx.canvas.height = size;
-
-  const outlineCtx = document.querySelector('#outline').getContext('2d');
-  outlineCtx.canvas.width = size;
-  outlineCtx.canvas.height = size;
-  outlineCtx.translate(outlineCtx.canvas.width / 2, outlineCtx.canvas.height / 2);
-  outlineCtx.scale(outlineCtx.canvas.width / 360, outlineCtx.canvas.height / -180);
-  outlineCtx.strokeStyle = '#FFF';
-
-  const workCtx = document.createElement('canvas').getContext('2d');
-  workCtx.canvas.width = size;
-  workCtx.canvas.height = size;
-
-  let id = 1;
-  const countryData = {};
-  const countriesById = [];
-  let min;
-  let max;
-
-  function resetMinMax() {
-    min = [ 10000,  10000];
-    max = [-10000, -10000];
-  }
-
-  function minMax(p) {
-    min[0] = Math.min(min[0], p[0]);
-    min[1] = Math.min(min[1], p[1]);
-    max[0] = Math.max(max[0], p[0]);
-    max[1] = Math.max(max[1], p[1]);
-  }
-
-  const geoHandlers = {
-    'MultiPolygon': multiPolygonArea,
-    'Polygon': polygonArea,
-  };
-
-  function multiPolygonArea(ctx, geo, drawFn) {
-    const {coordinates} = geo;
-    for (const polygon of coordinates) {
-      ctx.beginPath();
-      for (const ring of polygon) {
-        ring.forEach(minMax);
-        ctx.moveTo(...ring[0]);
-        for (let i = 0; i < ring.length; ++i) {
-          ctx.lineTo(...ring[i]);
-        }
-        ctx.closePath();
-      }
-      drawFn(ctx);
-    }
-  }
-
-  function polygonArea(ctx, geo, drawFn) {
-    const {coordinates} = geo;
-    ctx.beginPath();
-    for (const ring of coordinates) {
-      ring.forEach(minMax);
-      ctx.moveTo(...ring[0]);
-      for (let i = 0; i < ring.length; ++i) {
-        ctx.lineTo(...ring[i]);
-      }
-      ctx.closePath();
-    }
-    drawFn(ctx);
-  }
-
-  function fill(ctx) {
-    ctx.fill('evenodd');
-  }
-
-  // function stroke(ctx) {
-  //   ctx.save();
-  //   ctx.setTransform(1, 0, 0, 1, 0, 0);
-  //   ctx.stroke();
-  //   ctx.restore();
-  // }
-
-  function draw(area) {
-    const {properties, geometry} = area;
-    const {type} = geometry;
-    const name = properties.NAME;
-
-    console.log(name);
-
-    if (!countryData[name]) {
-      const r = (id >>  0) & 0xFF;
-      const g = (id >>  8) & 0xFF;
-      const b = (id >> 16) & 0xFF;
-
-      countryData[name] = {
-        color: [r, g, b],
-        id: id++,
-      };
-      countriesById.push({name});
-    }
-    const countryInfo = countriesById[countryData[name].id - 1];
-
-    const handler = geoHandlers[type];
-    if (!handler) {
-      throw new Error('unknown geometry type.');
-    }
-
-    resetMinMax();
-
-    workCtx.save();
-    workCtx.clearRect(0, 0, workCtx.canvas.width, workCtx.canvas.height);
-    workCtx.fillStyle = '#000';
-    workCtx.strokeStyle = '#000';
-    workCtx.translate(workCtx.canvas.width / 2, workCtx.canvas.height / 2);
-    workCtx.scale(workCtx.canvas.width / 360, workCtx.canvas.height / -180);
-
-    handler(workCtx, geometry, fill);
-
-    workCtx.restore();
-
-    countryInfo.min = min;
-    countryInfo.max = max;
-    countryInfo.area = properties.AREA;
-    countryInfo.lat = properties.LAT;
-    countryInfo.lon = properties.LON;
-    countryInfo.population = {
-      '2005': properties.POP2005,
-    };
-
-    //
-    const left   = Math.floor(( min[0] + 180) * workCtx.canvas.width  / 360);
-    const bottom = Math.floor((-min[1] +  90) * workCtx.canvas.height / 180);
-    const right  = Math.ceil( ( max[0] + 180) * workCtx.canvas.width  / 360);
-    const top    = Math.ceil( (-max[1] +  90) * workCtx.canvas.height / 180);
-    const width  = right - left + 1;
-    const height = Math.max(1, bottom - top + 1);
-
-    const color = countryData[name].color;
-    const src = workCtx.getImageData(left, top, width, height);
-    for (let y = 0; y < height; ++y) {
-      for (let x = 0; x < width; ++x) {
-        const off = (y * width + x) * 4;
-        if (src.data[off + 3]) {
-          src.data[off + 0] = color[0];
-          src.data[off + 1] = color[1];
-          src.data[off + 2] = color[2];
-          src.data[off + 3] = 255;
-        }
-      }
-    }
-    workCtx.putImageData(src, left, top);
-    pickCtx.drawImage(workCtx.canvas, 0, 0);
-
-//    handler(outlineCtx, geometry, stroke);
-  }
-
-  const source = await shapefile.open('TM_WORLD_BORDERS-0.3.shp');
-  const areas = [];
-  for (let i = 0; ; ++i) {
-    const {done, value} = await source.read();
-    if (done) {
-      break;
-    }
-    areas.push(value);
-    draw(value);
-    if (i % 20 === 19) {
-      await wait();
-    }
-  }
-  console.log(JSON.stringify(areas));
-
-  console.log('min', min);
-  console.log('max', max);
-
-  console.log(JSON.stringify(countriesById, null, 2));
-
-  const pick = pickCtx.getImageData(0, 0, pickCtx.canvas.width, pickCtx.canvas.height);
-  const outline = outlineCtx.getImageData(0, 0, outlineCtx.canvas.width, outlineCtx.canvas.height);
-
-  function getId(imageData, x, y) {
-    const off = (((y + imageData.height) % imageData.height) * imageData.width + ((x + imageData.width) % imageData.width)) * 4;
-    return imageData.data[off + 0] +
-           imageData.data[off + 1] * 256 +
-           imageData.data[off + 2] * 256 * 256 +
-           imageData.data[off + 3] * 256 * 256 * 256;
-  }
-
-  function putPixel(imageData, x, y, color) {
-    const off = (y * imageData.width + x) * 4;
-    imageData.data.set(color, off);
-  }
-
-
-  for (let y = 0; y < pick.height; ++y) {
-    for (let x = 0; x < pick.width; ++x) {
-      const s = getId(pick, x, y);
-      const r = getId(pick, x + 1, y);
-      const d = getId(pick, x, y + 1);
-      let v = 0;
-      if (s !== r || s !== d) {
-        v = 255;
-      }
-      putPixel(outline, x, y, [v, v, v, v]);
-    }
-  }
-
-  for (let y = 0; y < outline.height; ++y) {
-    for (let x = 0; x < outline.width; ++x) {
-      const s = getId(outline, x, y);
-      const l = getId(outline, x - 1, y);
-      const u = getId(outline, x, y - 1);
-      const r = getId(outline, x + 1, y);
-      const d = getId(outline, x, y + 1);
-      //const rd = getId(outline, x + 1, y + 1);
-      let v = s;
-      if ((s && r && d) ||
-          (s && l && d) ||
-          (s && r && u) ||
-          (s && l && u)) {
-        v = 0;
-      }
-      putPixel(outline, x, y, [v, v, v, v]);
-    }
-  }
-
-  outlineCtx.putImageData(outline, 0, 0);
+
+	const size = 4096;
+	const pickCtx = document.querySelector( '#pick' ).getContext( '2d' );
+	pickCtx.canvas.width = size;
+	pickCtx.canvas.height = size;
+
+	const outlineCtx = document.querySelector( '#outline' ).getContext( '2d' );
+	outlineCtx.canvas.width = size;
+	outlineCtx.canvas.height = size;
+	outlineCtx.translate( outlineCtx.canvas.width / 2, outlineCtx.canvas.height / 2 );
+	outlineCtx.scale( outlineCtx.canvas.width / 360, outlineCtx.canvas.height / - 180 );
+	outlineCtx.strokeStyle = '#FFF';
+
+	const workCtx = document.createElement( 'canvas' ).getContext( '2d' );
+	workCtx.canvas.width = size;
+	workCtx.canvas.height = size;
+
+	let id = 1;
+	const countryData = {};
+	const countriesById = [];
+	let min;
+	let max;
+
+	function resetMinMax() {
+
+		min = [ 10000, 10000 ];
+		max = [ - 10000, - 10000 ];
+
+	}
+
+	function minMax( p ) {
+
+		min[ 0 ] = Math.min( min[ 0 ], p[ 0 ] );
+		min[ 1 ] = Math.min( min[ 1 ], p[ 1 ] );
+		max[ 0 ] = Math.max( max[ 0 ], p[ 0 ] );
+		max[ 1 ] = Math.max( max[ 1 ], p[ 1 ] );
+
+	}
+
+	const geoHandlers = {
+		'MultiPolygon': multiPolygonArea,
+		'Polygon': polygonArea,
+	};
+
+	function multiPolygonArea( ctx, geo, drawFn ) {
+
+		const { coordinates } = geo;
+		for ( const polygon of coordinates ) {
+
+			ctx.beginPath();
+			for ( const ring of polygon ) {
+
+				ring.forEach( minMax );
+				ctx.moveTo( ...ring[ 0 ] );
+				for ( let i = 0; i < ring.length; ++ i ) {
+
+					ctx.lineTo( ...ring[ i ] );
+
+				}
+
+				ctx.closePath();
+
+			}
+
+			drawFn( ctx );
+
+		}
+
+	}
+
+	function polygonArea( ctx, geo, drawFn ) {
+
+		const { coordinates } = geo;
+		ctx.beginPath();
+		for ( const ring of coordinates ) {
+
+			ring.forEach( minMax );
+			ctx.moveTo( ...ring[ 0 ] );
+			for ( let i = 0; i < ring.length; ++ i ) {
+
+				ctx.lineTo( ...ring[ i ] );
+
+			}
+
+			ctx.closePath();
+
+		}
+
+		drawFn( ctx );
+
+	}
+
+	function fill( ctx ) {
+
+		ctx.fill( 'evenodd' );
+
+	}
+
+	// function stroke(ctx) {
+	//   ctx.save();
+	//   ctx.setTransform(1, 0, 0, 1, 0, 0);
+	//   ctx.stroke();
+	//   ctx.restore();
+	// }
+
+	function draw( area ) {
+
+		const { properties, geometry } = area;
+		const { type } = geometry;
+		const name = properties.NAME;
+
+		console.log( name );
+
+		if ( ! countryData[ name ] ) {
+
+			const r = ( id >> 0 ) & 0xFF;
+			const g = ( id >> 8 ) & 0xFF;
+			const b = ( id >> 16 ) & 0xFF;
+
+			countryData[ name ] = {
+				color: [ r, g, b ],
+				id: id ++,
+			};
+			countriesById.push( { name } );
+
+		}
+
+		const countryInfo = countriesById[ countryData[ name ].id - 1 ];
+
+		const handler = geoHandlers[ type ];
+		if ( ! handler ) {
+
+			throw new Error( 'unknown geometry type.' );
+
+		}
+
+		resetMinMax();
+
+		workCtx.save();
+		workCtx.clearRect( 0, 0, workCtx.canvas.width, workCtx.canvas.height );
+		workCtx.fillStyle = '#000';
+		workCtx.strokeStyle = '#000';
+		workCtx.translate( workCtx.canvas.width / 2, workCtx.canvas.height / 2 );
+		workCtx.scale( workCtx.canvas.width / 360, workCtx.canvas.height / - 180 );
+
+		handler( workCtx, geometry, fill );
+
+		workCtx.restore();
+
+		countryInfo.min = min;
+		countryInfo.max = max;
+		countryInfo.area = properties.AREA;
+		countryInfo.lat = properties.LAT;
+		countryInfo.lon = properties.LON;
+		countryInfo.population = {
+			'2005': properties.POP2005,
+		};
+
+		//
+		const left = Math.floor( ( min[ 0 ] + 180 ) * workCtx.canvas.width / 360 );
+		const bottom = Math.floor( ( - min[ 1 ] + 90 ) * workCtx.canvas.height / 180 );
+		const right = Math.ceil( ( max[ 0 ] + 180 ) * workCtx.canvas.width / 360 );
+		const top = Math.ceil( ( - max[ 1 ] + 90 ) * workCtx.canvas.height / 180 );
+		const width = right - left + 1;
+		const height = Math.max( 1, bottom - top + 1 );
+
+		const color = countryData[ name ].color;
+		const src = workCtx.getImageData( left, top, width, height );
+		for ( let y = 0; y < height; ++ y ) {
+
+			for ( let x = 0; x < width; ++ x ) {
+
+				const off = ( y * width + x ) * 4;
+				if ( src.data[ off + 3 ] ) {
+
+					src.data[ off + 0 ] = color[ 0 ];
+					src.data[ off + 1 ] = color[ 1 ];
+					src.data[ off + 2 ] = color[ 2 ];
+					src.data[ off + 3 ] = 255;
+
+				}
+
+			}
+
+		}
+
+		workCtx.putImageData( src, left, top );
+		pickCtx.drawImage( workCtx.canvas, 0, 0 );
+
+		//    handler(outlineCtx, geometry, stroke);
+
+	}
+
+	const source = await shapefile.open( 'TM_WORLD_BORDERS-0.3.shp' );
+	const areas = [];
+	for ( let i = 0; ; ++ i ) {
+
+		const { done, value } = await source.read();
+		if ( done ) {
+
+			break;
+
+		}
+
+		areas.push( value );
+		draw( value );
+		if ( i % 20 === 19 ) {
+
+			await wait();
+
+		}
+
+	}
+
+	console.log( JSON.stringify( areas ) );
+
+	console.log( 'min', min );
+	console.log( 'max', max );
+
+	console.log( JSON.stringify( countriesById, null, 2 ) );
+
+	const pick = pickCtx.getImageData( 0, 0, pickCtx.canvas.width, pickCtx.canvas.height );
+	const outline = outlineCtx.getImageData( 0, 0, outlineCtx.canvas.width, outlineCtx.canvas.height );
+
+	function getId( imageData, x, y ) {
+
+		const off = ( ( ( y + imageData.height ) % imageData.height ) * imageData.width + ( ( x + imageData.width ) % imageData.width ) ) * 4;
+		return imageData.data[ off + 0 ] +
+           imageData.data[ off + 1 ] * 256 +
+           imageData.data[ off + 2 ] * 256 * 256 +
+           imageData.data[ off + 3 ] * 256 * 256 * 256;
+
+	}
+
+	function putPixel( imageData, x, y, color ) {
+
+		const off = ( y * imageData.width + x ) * 4;
+		imageData.data.set( color, off );
+
+	}
+
+
+	for ( let y = 0; y < pick.height; ++ y ) {
+
+		for ( let x = 0; x < pick.width; ++ x ) {
+
+			const s = getId( pick, x, y );
+			const r = getId( pick, x + 1, y );
+			const d = getId( pick, x, y + 1 );
+			let v = 0;
+			if ( s !== r || s !== d ) {
+
+				v = 255;
+
+			}
+
+			putPixel( outline, x, y, [ v, v, v, v ] );
+
+		}
+
+	}
+
+	for ( let y = 0; y < outline.height; ++ y ) {
+
+		for ( let x = 0; x < outline.width; ++ x ) {
+
+			const s = getId( outline, x, y );
+			const l = getId( outline, x - 1, y );
+			const u = getId( outline, x, y - 1 );
+			const r = getId( outline, x + 1, y );
+			const d = getId( outline, x, y + 1 );
+			//const rd = getId(outline, x + 1, y + 1);
+			let v = s;
+			if ( ( s && r && d ) ||
+          ( s && l && d ) ||
+          ( s && r && u ) ||
+          ( s && l && u ) ) {
+
+				v = 0;
+
+			}
+
+			putPixel( outline, x, y, [ v, v, v, v ] );
+
+		}
+
+	}
+
+	outlineCtx.putImageData( outline, 0, 0 );
+
 }
 
-function wait(ms = 0) {
-  return new Promise((resolve) => {
-    setTimeout(resolve, ms);
-  });
+function wait( ms = 0 ) {
+
+	return new Promise( ( resolve ) => {
+
+		setTimeout( resolve, ms );
+
+	} );
+
 }
 
 main();

+ 225 - 162
manual/resources/tools/geo-picking/ogc-parser.js

@@ -1,18 +1,26 @@
 'use strict';
 
 const assert = {
-  strictEqual(actual, expected, ...args) {
-    args = args || [];
-    if (actual !== expected) {
-      throw new Error(`${actual} (actual) should equal ${expected} (expected): ${[...args].join(' ')}`);
-    }
-  },
-  notStrictEqual(actual, expected, ...args) {
-    args = args || [];
-    if (actual === expected) {
-      throw new Error(`${actual} (actual) should NOT equal ${expected} (expected): ${[...args].join(' ')}`);
-    }
-  },
+	strictEqual( actual, expected, ...args ) {
+
+		args = args || [];
+		if ( actual !== expected ) {
+
+			throw new Error( `${actual} (actual) should equal ${expected} (expected): ${[ ...args ].join( ' ' )}` );
+
+		}
+
+	},
+	notStrictEqual( actual, expected, ...args ) {
+
+		args = args || [];
+		if ( actual === expected ) {
+
+			throw new Error( `${actual} (actual) should NOT equal ${expected} (expected): ${[ ...args ].join( ' ' )}` );
+
+		}
+
+	},
 };
 
 /*
@@ -33,35 +41,36 @@ function dumpBuf(buf) {
 }
 */
 
-function parse(buf) {
-  assert.strictEqual(buf[0], 0x47, 'bad header');
-  assert.strictEqual(buf[1], 0x50, 'bad header');
-  assert.strictEqual(buf[2], 0, 'unknown version');  // version
-  const flags = buf[3];
+function parse( buf ) {
 
-  const flag_x         = (flags >> 5) & 1;
-  // const flag_empty_geo = (flags >> 4) & 1;  // 1 = empty, 0 non-empty
-  const flag_byteOrder = (flags >> 0) & 1;  // 1 = little endian, 0 = big
-  const flag_envelope  = (flags >> 1) & 7;
+	assert.strictEqual( buf[ 0 ], 0x47, 'bad header' );
+	assert.strictEqual( buf[ 1 ], 0x50, 'bad header' );
+	assert.strictEqual( buf[ 2 ], 0, 'unknown version' ); // version
+	const flags = buf[ 3 ];
 
-  assert.strictEqual(flag_x, 0, 'x must be 0');
+	const flag_x = ( flags >> 5 ) & 1;
+	// const flag_empty_geo = (flags >> 4) & 1;  // 1 = empty, 0 non-empty
+	const flag_byteOrder = ( flags >> 0 ) & 1; // 1 = little endian, 0 = big
+	const flag_envelope = ( flags >> 1 ) & 7;
 
-  const envelopeSizes = [
-    0,  // 0: non
-    4,  // 1: minx, maxx, miny, maxy
-    6,  // 2: minx, maxx, miny, maxy, minz, maxz
-    6,  // 3: minx, maxx, miny, maxy, minm, maxm
-    8,  // 4: minx, maxx, miny, maxy, minz, maxz, minm, maxm
-  ];
+	assert.strictEqual( flag_x, 0, 'x must be 0' );
 
-  const envelopeSize = envelopeSizes[flag_envelope];
-  assert.notStrictEqual(envelopeSize, undefined);
+	const envelopeSizes = [
+		0, // 0: non
+		4, // 1: minx, maxx, miny, maxy
+		6, // 2: minx, maxx, miny, maxy, minz, maxz
+		6, // 3: minx, maxx, miny, maxy, minm, maxm
+		8, // 4: minx, maxx, miny, maxy, minz, maxz, minm, maxm
+	];
 
-  const headerSize = 8;
-  let cursor = headerSize;
+	const envelopeSize = envelopeSizes[ flag_envelope ];
+	assert.notStrictEqual( envelopeSize, undefined );
 
-  const dataView = new DataView(buf.buffer);
-  /*
+	const headerSize = 8;
+	let cursor = headerSize;
+
+	const dataView = new DataView( buf.buffer );
+	/*
   const readBE = {
     getDouble() { const v = buf.readDoubleBE(cursor); cursor += 8 ; return v; },
     getFloat()  { const v = buf.readFloatBE(cursor);  cursor += 4 ; return v; },
@@ -85,148 +94,202 @@ function parse(buf) {
   };
   */
 
-  let littleEndian;
-  const endianStack = [];
+	let littleEndian;
+	const endianStack = [];
 
-  function pushByteOrder(byteOrder) {
-    endianStack.push(littleEndian);
-    littleEndian = byteOrder;
-  }
+	function pushByteOrder( byteOrder ) {
 
-  function popByteOrder() {
-    littleEndian = endianStack.pop();
-  }
+		endianStack.push( littleEndian );
+		littleEndian = byteOrder;
 
-  const getDouble = () => { const v = dataView.getFloat64(cursor, littleEndian); cursor += 8 ; return v; };
-  // const getFloat =  () => { const v = dataView.getFloat32(cursor, littleEndian); cursor += 4 ; return v; };
-  const getInt8 =   () => { const v = dataView.getInt8(cursor);    cursor += 1 ; return v; };
-  // const getUint8 =  () => { const v = dataView.getUint8(cursor, littleEndian);   cursor += 1 ; return v; };
-  // const getInt16 =  () => { const v = dataView.getInt16(cursor, littleEndian);   cursor += 2 ; return v; };
-  // const getUint16 = () => { const v = dataView.getUint16(cursor, littleEndian);  cursor += 2 ; return v; };
-  // const getInt32 =  () => { const v = dataView.getInt32(cursor, littleEndian);   cursor += 4 ; return v; };
-  const getUint32 = () => { const v = dataView.getUint32(cursor, littleEndian);  cursor += 4 ; return v; };
+	}
 
-  pushByteOrder(flag_byteOrder);
+	function popByteOrder() {
 
-  const envelope = [];
-  for (let i = 0; i < envelopeSize; ++i) {
-    envelope.push(getDouble());
-  }
+		littleEndian = endianStack.pop();
 
-  const primitives = [];
+	}
 
-  function getPoints(num) {
-    const points = [];
-    for (let i = 0; i < num; ++i) {
-      points.push(getDouble(), getDouble());
-    }
-    return points;
-  }
+	const getDouble = () => {
 
-  function getRings(num) {
-    const rings = [];
-    for (let i = 0; i < num; ++i) {
-      rings.push(getPoints(getUint32()));
-    }
-    return rings;
-  }
+		const v = dataView.getFloat64( cursor, littleEndian ); cursor += 8; return v;
 
-  function pointHandler() {
-    return {
-      type: 'point',
-      point: getPoints(1),
-    };
-  }
+	};
 
-  function lineStringHandler() {
-    return {
-      type: 'lineString',
-      points: getPoints(getUint32()),
-    };
-  }
+	// const getFloat =  () => { const v = dataView.getFloat32(cursor, littleEndian); cursor += 4 ; return v; };
+	const getInt8 = () => {
 
-  function polygonHandler() {
-    return {
-      type: 'polygon',
-      rings: getRings(getUint32()),
-    };
-  }
+		const v = dataView.getInt8( cursor ); cursor += 1; return v;
 
-  function multiPointHandler() {
-    // WTF?
-    const points = [];
-    const num = getUint32();
-    for (let i = 0; i < num; ++i) {
-      pushByteOrder(getInt8());
-      const type = getUint32();
-      assert.strictEqual(type, 1);  // must be point
-      points.push(getDouble(), getDouble());
-      popByteOrder();
-    }
-    return {
-      type: 'multiPoint',
-      points,
-    };
-  }
+	};
 
-  function multiLineStringHandler() {
-    // WTF?
-    const lineStrings = [];
-    const num = getUint32();
-    for (let i = 0; i < num; ++i) {
-      pushByteOrder(getInt8());
-      const type = getUint32();
-      assert.strictEqual(type, 2);  // must be lineString
-      lineStrings.push(getPoints(getUint32()));
-      popByteOrder();
-    }
-    return {
-      type: 'multiLineString',
-      lineStrings,
-    };
-  }
+	// const getUint8 =  () => { const v = dataView.getUint8(cursor, littleEndian);   cursor += 1 ; return v; };
+	// const getInt16 =  () => { const v = dataView.getInt16(cursor, littleEndian);   cursor += 2 ; return v; };
+	// const getUint16 = () => { const v = dataView.getUint16(cursor, littleEndian);  cursor += 2 ; return v; };
+	// const getInt32 =  () => { const v = dataView.getInt32(cursor, littleEndian);   cursor += 4 ; return v; };
+	const getUint32 = () => {
 
-  function multiPolygonHandler() {
-    // WTF?
-    const polygons = [];
-    const num = getUint32();
-    for (let i = 0; i < num; ++i) {
-      pushByteOrder(getInt8());
-      const type = getUint32();
-      assert.strictEqual(type, 3);  // must be polygon
-      polygons.push(getRings(getUint32()));
-      popByteOrder();
-    }
-    return {
-      type: 'multiPolygon',
-      polygons,
-    };
-  }
+		const v = dataView.getUint32( cursor, littleEndian ); cursor += 4; return v;
 
-  const typeHandlers = [
-    undefined,              // 0
-    pointHandler,           // 1
-    lineStringHandler,      // 2
-    polygonHandler,         // 3
-    multiPointHandler,      // 4
-    multiLineStringHandler, // 5,
-    multiPolygonHandler,    // 6,
-  ];
-
-  const end = buf.length;
-  while (cursor < end) {
-    pushByteOrder(getInt8());
-    const type = getUint32();
-    const handler = typeHandlers[type];
-    assert.notStrictEqual(handler, undefined, 'unknown type');
-    primitives.push(handler());
-    popByteOrder();
-  }
+	};
+
+	pushByteOrder( flag_byteOrder );
+
+	const envelope = [];
+	for ( let i = 0; i < envelopeSize; ++ i ) {
+
+		envelope.push( getDouble() );
+
+	}
+
+	const primitives = [];
+
+	function getPoints( num ) {
+
+		const points = [];
+		for ( let i = 0; i < num; ++ i ) {
+
+			points.push( getDouble(), getDouble() );
+
+		}
+
+		return points;
+
+	}
+
+	function getRings( num ) {
+
+		const rings = [];
+		for ( let i = 0; i < num; ++ i ) {
+
+			rings.push( getPoints( getUint32() ) );
+
+		}
+
+		return rings;
+
+	}
+
+	function pointHandler() {
+
+		return {
+			type: 'point',
+			point: getPoints( 1 ),
+		};
+
+	}
+
+	function lineStringHandler() {
+
+		return {
+			type: 'lineString',
+			points: getPoints( getUint32() ),
+		};
+
+	}
+
+	function polygonHandler() {
+
+		return {
+			type: 'polygon',
+			rings: getRings( getUint32() ),
+		};
+
+	}
+
+	function multiPointHandler() {
+
+		// WTF?
+		const points = [];
+		const num = getUint32();
+		for ( let i = 0; i < num; ++ i ) {
+
+			pushByteOrder( getInt8() );
+			const type = getUint32();
+			assert.strictEqual( type, 1 ); // must be point
+			points.push( getDouble(), getDouble() );
+			popByteOrder();
+
+		}
+
+		return {
+			type: 'multiPoint',
+			points,
+		};
+
+	}
+
+	function multiLineStringHandler() {
+
+		// WTF?
+		const lineStrings = [];
+		const num = getUint32();
+		for ( let i = 0; i < num; ++ i ) {
+
+			pushByteOrder( getInt8() );
+			const type = getUint32();
+			assert.strictEqual( type, 2 ); // must be lineString
+			lineStrings.push( getPoints( getUint32() ) );
+			popByteOrder();
+
+		}
+
+		return {
+			type: 'multiLineString',
+			lineStrings,
+		};
+
+	}
+
+	function multiPolygonHandler() {
+
+		// WTF?
+		const polygons = [];
+		const num = getUint32();
+		for ( let i = 0; i < num; ++ i ) {
+
+			pushByteOrder( getInt8() );
+			const type = getUint32();
+			assert.strictEqual( type, 3 ); // must be polygon
+			polygons.push( getRings( getUint32() ) );
+			popByteOrder();
+
+		}
+
+		return {
+			type: 'multiPolygon',
+			polygons,
+		};
+
+	}
+
+	const typeHandlers = [
+		undefined, // 0
+		pointHandler, // 1
+		lineStringHandler, // 2
+		polygonHandler, // 3
+		multiPointHandler, // 4
+		multiLineStringHandler, // 5,
+		multiPolygonHandler, // 6,
+	];
+
+	const end = buf.length;
+	while ( cursor < end ) {
+
+		pushByteOrder( getInt8() );
+		const type = getUint32();
+		const handler = typeHandlers[ type ];
+		assert.notStrictEqual( handler, undefined, 'unknown type' );
+		primitives.push( handler() );
+		popByteOrder();
+
+	}
+
+	return {
+		envelope,
+		primitives,
+	};
 
-  return {
-    envelope,
-    primitives,
-  };
 }
 
-window.ogcParser = {parse};
+window.ogcParser = { parse };

+ 1 - 1
package.json

@@ -110,7 +110,7 @@
     "lint-test": "eslint test --ignore-pattern vendor",
     "lint-utils": "eslint utils",
     "lint": "npm run lint-core",
-    "lint-fix": "npm run lint-core -- --fix && npm run lint-addons -- --fix && npm run lint-examples -- --fix && npm run lint-docs -- --fix && npm run lint-editor -- --fix && npm run lint-test -- --fix && npm run lint-utils -- --fix",
+    "lint-fix": "npm run lint-core -- --fix && npm run lint-addons -- --fix && npm run lint-examples -- --fix && npm run lint-docs -- --fix && npm run lint-editor -- --fix && npm run lint-manual -- --fix && npm run lint-test -- --fix && npm run lint-utils -- --fix",
     "test-unit": "npm run unit --prefix test",
     "test-e2e": "node test/e2e/puppeteer.js",
     "test-e2e-cov": "node test/e2e/check-coverage.js",

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff