You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

952 lines
17 KiB

2 years ago
import * as THREE from 'three';
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
import { TGALoader } from 'three/addons/loaders/TGALoader.js';
import { UIElement, UISpan, UIDiv, UIRow, UIButton, UICheckbox, UIText, UINumber } from './ui.js';
import { MoveObjectCommand } from '../commands/MoveObjectCommand.js';
class UITexture extends UISpan {
constructor( mapping ) {
super();
const scope = this;
const form = document.createElement( 'form' );
const input = document.createElement( 'input' );
input.type = 'file';
input.addEventListener( 'change', function ( event ) {
loadFile( event.target.files[ 0 ] );
} );
form.appendChild( input );
const canvas = document.createElement( 'canvas' );
canvas.width = 32;
canvas.height = 16;
canvas.style.cursor = 'pointer';
canvas.style.marginRight = '5px';
canvas.style.border = '1px solid #888';
canvas.addEventListener( 'click', function () {
input.click();
} );
canvas.addEventListener( 'drop', function ( event ) {
event.preventDefault();
event.stopPropagation();
loadFile( event.dataTransfer.files[ 0 ] );
} );
this.dom.appendChild( canvas );
function loadFile( file ) {
const extension = file.name.split( '.' ).pop().toLowerCase();
const reader = new FileReader();
if ( extension === 'hdr' || extension === 'pic' ) {
reader.addEventListener( 'load', function ( event ) {
// assuming RGBE/Radiance HDR iamge format
const loader = new RGBELoader();
loader.load( event.target.result, function ( hdrTexture ) {
hdrTexture.sourceFile = file.name;
hdrTexture.isHDRTexture = true;
scope.setValue( hdrTexture );
if ( scope.onChangeCallback ) scope.onChangeCallback( hdrTexture );
} );
} );
reader.readAsDataURL( file );
} else if ( extension === 'tga' ) {
reader.addEventListener( 'load', function ( event ) {
const canvas = new TGALoader().parse( event.target.result );
const texture = new THREE.CanvasTexture( canvas, mapping );
texture.sourceFile = file.name;
scope.setValue( texture );
if ( scope.onChangeCallback ) scope.onChangeCallback( texture );
}, false );
reader.readAsArrayBuffer( file );
} else if ( file.type.match( 'image.*' ) ) {
reader.addEventListener( 'load', function ( event ) {
const image = document.createElement( 'img' );
image.addEventListener( 'load', function () {
const texture = new THREE.Texture( this, mapping );
texture.sourceFile = file.name;
texture.needsUpdate = true;
scope.setValue( texture );
if ( scope.onChangeCallback ) scope.onChangeCallback( texture );
}, false );
image.src = event.target.result;
}, false );
reader.readAsDataURL( file );
}
form.reset();
}
this.texture = null;
this.onChangeCallback = null;
}
getValue() {
return this.texture;
}
setValue( texture ) {
const canvas = this.dom.children[ 0 ];
const context = canvas.getContext( '2d' );
// Seems like context can be null if the canvas is not visible
if ( context ) {
// Always clear the context before set new texture, because new texture may has transparency
context.clearRect( 0, 0, canvas.width, canvas.height );
}
if ( texture !== null ) {
const image = texture.image;
if ( image !== undefined && image.width > 0 ) {
canvas.title = texture.sourceFile;
const scale = canvas.width / image.width;
if ( image.data === undefined ) {
context.drawImage( image, 0, 0, image.width * scale, image.height * scale );
} else {
const canvas2 = renderToCanvas( texture );
context.drawImage( canvas2, 0, 0, image.width * scale, image.height * scale );
}
} else {
canvas.title = texture.sourceFile + ' (error)';
}
} else {
canvas.title = 'empty';
}
this.texture = texture;
}
setEncoding( encoding ) {
const texture = this.getValue();
if ( texture !== null ) {
texture.encoding = encoding;
}
return this;
}
onChange( callback ) {
this.onChangeCallback = callback;
return this;
}
}
class UICubeTexture extends UIElement {
constructor() {
const container = new UIDiv();
super( container.dom );
this.cubeTexture = null;
this.onChangeCallback = null;
this.textures = [];
const scope = this;
const pRow = new UIRow();
const nRow = new UIRow();
pRow.add( new UIText( 'P:' ).setWidth( '35px' ) );
nRow.add( new UIText( 'N:' ).setWidth( '35px' ) );
const posXTexture = new UITexture().onChange( onTextureChanged );
const negXTexture = new UITexture().onChange( onTextureChanged );
const posYTexture = new UITexture().onChange( onTextureChanged );
const negYTexture = new UITexture().onChange( onTextureChanged );
const posZTexture = new UITexture().onChange( onTextureChanged );
const negZTexture = new UITexture().onChange( onTextureChanged );
this.textures.push( posXTexture, negXTexture, posYTexture, negYTexture, posZTexture, negZTexture );
pRow.add( posXTexture );
pRow.add( posYTexture );
pRow.add( posZTexture );
nRow.add( negXTexture );
nRow.add( negYTexture );
nRow.add( negZTexture );
container.add( pRow, nRow );
function onTextureChanged() {
const images = [];
for ( let i = 0; i < scope.textures.length; i ++ ) {
const texture = scope.textures[ i ].getValue();
if ( texture !== null ) {
images.push( texture.isHDRTexture ? texture : texture.image );
}
}
if ( images.length === 6 ) {
const cubeTexture = new THREE.CubeTexture( images );
cubeTexture.needsUpdate = true;
if ( images[ 0 ].isHDRTexture ) cubeTexture.isHDRTexture = true;
scope.cubeTexture = cubeTexture;
if ( scope.onChangeCallback ) scope.onChangeCallback( cubeTexture );
}
}
}
setEncoding( encoding ) {
const cubeTexture = this.getValue();
if ( cubeTexture !== null ) {
cubeTexture.encoding = encoding;
}
return this;
}
getValue() {
return this.cubeTexture;
}
setValue( cubeTexture ) {
this.cubeTexture = cubeTexture;
if ( cubeTexture !== null ) {
const images = cubeTexture.image;
if ( Array.isArray( images ) === true && images.length === 6 ) {
for ( let i = 0; i < images.length; i ++ ) {
const image = images[ i ];
const texture = new THREE.Texture( image );
this.textures[ i ].setValue( texture );
}
}
} else {
const textures = this.textures;
for ( let i = 0; i < textures.length; i ++ ) {
textures[ i ].setValue( null );
}
}
return this;
}
onChange( callback ) {
this.onChangeCallback = callback;
return this;
}
}
class UIOutliner extends UIDiv {
constructor( editor ) {
super();
this.dom.className = 'Outliner';
this.dom.tabIndex = 0; // keyup event is ignored without setting tabIndex
const scope = this;
// hack
this.scene = editor.scene;
// Prevent native scroll behavior
this.dom.addEventListener( 'keydown', function ( event ) {
switch ( event.keyCode ) {
case 38: // up
case 40: // down
event.preventDefault();
event.stopPropagation();
break;
}
} );
// Keybindings to support arrow navigation
this.dom.addEventListener( 'keyup', function ( event ) {
switch ( event.keyCode ) {
case 38: // up
scope.selectIndex( scope.selectedIndex - 1 );
break;
case 40: // down
scope.selectIndex( scope.selectedIndex + 1 );
break;
}
} );
this.editor = editor;
this.options = [];
this.selectedIndex = - 1;
this.selectedValue = null;
}
selectIndex( index ) {
if ( index >= 0 && index < this.options.length ) {
this.setValue( this.options[ index ].value );
const changeEvent = document.createEvent( 'HTMLEvents' );
changeEvent.initEvent( 'change', true, true );
this.dom.dispatchEvent( changeEvent );
}
}
setOptions( options ) {
const scope = this;
while ( scope.dom.children.length > 0 ) {
scope.dom.removeChild( scope.dom.firstChild );
}
function onClick() {
scope.setValue( this.value );
const changeEvent = document.createEvent( 'HTMLEvents' );
changeEvent.initEvent( 'change', true, true );
scope.dom.dispatchEvent( changeEvent );
}
// Drag
let currentDrag;
function onDrag() {
currentDrag = this;
}
function onDragStart( event ) {
event.dataTransfer.setData( 'text', 'foo' );
}
function onDragOver( event ) {
if ( this === currentDrag ) return;
const area = event.offsetY / this.clientHeight;
if ( area < 0.25 ) {
this.className = 'option dragTop';
} else if ( area > 0.75 ) {
this.className = 'option dragBottom';
} else {
this.className = 'option drag';
}
}
function onDragLeave() {
if ( this === currentDrag ) return;
this.className = 'option';
}
function onDrop( event ) {
if ( this === currentDrag || currentDrag === undefined ) return;
this.className = 'option';
const scene = scope.scene;
const object = scene.getObjectById( currentDrag.value );
const area = event.offsetY / this.clientHeight;
if ( area < 0.25 ) {
const nextObject = scene.getObjectById( this.value );
moveObject( object, nextObject.parent, nextObject );
} else if ( area > 0.75 ) {
let nextObject, parent;
if ( this.nextSibling !== null ) {
nextObject = scene.getObjectById( this.nextSibling.value );
parent = nextObject.parent;
} else {
// end of list (no next object)
nextObject = null;
parent = scene.getObjectById( this.value ).parent;
}
moveObject( object, parent, nextObject );
} else {
const parentObject = scene.getObjectById( this.value );
moveObject( object, parentObject );
}
}
function moveObject( object, newParent, nextObject ) {
if ( nextObject === null ) nextObject = undefined;
let newParentIsChild = false;
object.traverse( function ( child ) {
if ( child === newParent ) newParentIsChild = true;
} );
if ( newParentIsChild ) return;
const editor = scope.editor;
editor.execute( new MoveObjectCommand( editor, object, newParent, nextObject ) );
const changeEvent = document.createEvent( 'HTMLEvents' );
changeEvent.initEvent( 'change', true, true );
scope.dom.dispatchEvent( changeEvent );
}
//
scope.options = [];
for ( let i = 0; i < options.length; i ++ ) {
const div = options[ i ];
div.className = 'option';
scope.dom.appendChild( div );
scope.options.push( div );
div.addEventListener( 'click', onClick );
if ( div.draggable === true ) {
div.addEventListener( 'drag', onDrag );
div.addEventListener( 'dragstart', onDragStart ); // Firefox needs this
div.addEventListener( 'dragover', onDragOver );
div.addEventListener( 'dragleave', onDragLeave );
div.addEventListener( 'drop', onDrop );
}
}
return scope;
}
getValue() {
return this.selectedValue;
}
setValue( value ) {
for ( let i = 0; i < this.options.length; i ++ ) {
const element = this.options[ i ];
if ( element.value === value ) {
element.classList.add( 'active' );
// scroll into view
const y = element.offsetTop - this.dom.offsetTop;
const bottomY = y + element.offsetHeight;
const minScroll = bottomY - this.dom.offsetHeight;
if ( this.dom.scrollTop > y ) {
this.dom.scrollTop = y;
} else if ( this.dom.scrollTop < minScroll ) {
this.dom.scrollTop = minScroll;
}
this.selectedIndex = i;
} else {
element.classList.remove( 'active' );
}
}
this.selectedValue = value;
return this;
}
}
class UIPoints extends UISpan {
constructor() {
super();
this.dom.style.display = 'inline-block';
this.pointsList = new UIDiv();
this.add( this.pointsList );
this.pointsUI = [];
this.lastPointIdx = 0;
this.onChangeCallback = null;
// TODO Remove this bind() stuff
this.update = function () {
if ( this.onChangeCallback !== null ) {
this.onChangeCallback();
}
}.bind( this );
}
onChange( callback ) {
this.onChangeCallback = callback;
return this;
}
clear() {
for ( let i = 0; i < this.pointsUI.length; ++ i ) {
if ( this.pointsUI[ i ] ) {
this.deletePointRow( i, true );
}
}
this.lastPointIdx = 0;
}
deletePointRow( idx, dontUpdate ) {
if ( ! this.pointsUI[ idx ] ) return;
this.pointsList.remove( this.pointsUI[ idx ].row );
this.pointsUI.splice( idx, 1 );
if ( dontUpdate !== true ) {
this.update();
}
this.lastPointIdx --;
}
}
class UIPoints2 extends UIPoints {
constructor() {
super();
const row = new UIRow();
this.add( row );
const addPointButton = new UIButton( '+' );
addPointButton.onClick( () => {
if ( this.pointsUI.length === 0 ) {
this.pointsList.add( this.createPointRow( 0, 0 ) );
} else {
const point = this.pointsUI[ this.pointsUI.length - 1 ];
this.pointsList.add( this.createPointRow( point.x.getValue(), point.y.getValue() ) );
}
this.update();
} );
row.add( addPointButton );
}
getValue() {
const points = [];
let count = 0;
for ( let i = 0; i < this.pointsUI.length; i ++ ) {
const pointUI = this.pointsUI[ i ];
if ( ! pointUI ) continue;
points.push( new THREE.Vector2( pointUI.x.getValue(), pointUI.y.getValue() ) );
++ count;
pointUI.lbl.setValue( count );
}
return points;
}
setValue( points ) {
this.clear();
for ( let i = 0; i < points.length; i ++ ) {
const point = points[ i ];
this.pointsList.add( this.createPointRow( point.x, point.y ) );
}
this.update();
return this;
}
createPointRow( x, y ) {
const pointRow = new UIDiv();
const lbl = new UIText( this.lastPointIdx + 1 ).setWidth( '20px' );
const txtX = new UINumber( x ).setWidth( '30px' ).onChange( this.update );
const txtY = new UINumber( y ).setWidth( '30px' ).onChange( this.update );
const scope = this;
const btn = new UIButton( '-' ).onClick( function () {
if ( scope.isEditing ) return;
const idx = scope.pointsList.getIndexOfChild( pointRow );
scope.deletePointRow( idx );
} );
this.pointsUI.push( { row: pointRow, lbl: lbl, x: txtX, y: txtY } );
++ this.lastPointIdx;
pointRow.add( lbl, txtX, txtY, btn );
return pointRow;
}
}
class UIPoints3 extends UIPoints {
constructor() {
super();
const row = new UIRow();
this.add( row );
const addPointButton = new UIButton( '+' );
addPointButton.onClick( () => {
if ( this.pointsUI.length === 0 ) {
this.pointsList.add( this.createPointRow( 0, 0, 0 ) );
} else {
const point = this.pointsUI[ this.pointsUI.length - 1 ];
this.pointsList.add( this.createPointRow( point.x.getValue(), point.y.getValue(), point.z.getValue() ) );
}
this.update();
} );
row.add( addPointButton );
}
getValue() {
const points = [];
let count = 0;
for ( let i = 0; i < this.pointsUI.length; i ++ ) {
const pointUI = this.pointsUI[ i ];
if ( ! pointUI ) continue;
points.push( new THREE.Vector3( pointUI.x.getValue(), pointUI.y.getValue(), pointUI.z.getValue() ) );
++ count;
pointUI.lbl.setValue( count );
}
return points;
}
setValue( points ) {
this.clear();
for ( let i = 0; i < points.length; i ++ ) {
const point = points[ i ];
this.pointsList.add( this.createPointRow( point.x, point.y, point.z ) );
}
this.update();
return this;
}
createPointRow( x, y, z ) {
const pointRow = new UIDiv();
const lbl = new UIText( this.lastPointIdx + 1 ).setWidth( '20px' );
const txtX = new UINumber( x ).setWidth( '30px' ).onChange( this.update );
const txtY = new UINumber( y ).setWidth( '30px' ).onChange( this.update );
const txtZ = new UINumber( z ).setWidth( '30px' ).onChange( this.update );
const scope = this;
const btn = new UIButton( '-' ).onClick( function () {
if ( scope.isEditing ) return;
const idx = scope.pointsList.getIndexOfChild( pointRow );
scope.deletePointRow( idx );
} );
this.pointsUI.push( { row: pointRow, lbl: lbl, x: txtX, y: txtY, z: txtZ } );
++ this.lastPointIdx;
pointRow.add( lbl, txtX, txtY, txtZ, btn );
return pointRow;
}
}
class UIBoolean extends UISpan {
constructor( boolean, text ) {
super();
this.setMarginRight( '4px' );
this.checkbox = new UICheckbox( boolean );
this.text = new UIText( text ).setMarginLeft( '3px' );
this.add( this.checkbox );
this.add( this.text );
}
getValue() {
return this.checkbox.getValue();
}
setValue( value ) {
return this.checkbox.setValue( value );
}
}
let renderer;
function renderToCanvas( texture ) {
if ( renderer === undefined ) {
renderer = new THREE.WebGLRenderer();
renderer.outputEncoding = THREE.sRGBEncoding;
}
const image = texture.image;
renderer.setSize( image.width, image.height, false );
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera( - 1, 1, 1, - 1, 0, 1 );
const material = new THREE.MeshBasicMaterial( { map: texture } );
const quad = new THREE.PlaneGeometry( 2, 2 );
const mesh = new THREE.Mesh( quad, material );
scene.add( mesh );
renderer.render( scene, camera );
return renderer.domElement;
}
export { UITexture, UICubeTexture, UIOutliner, UIPoints, UIPoints2, UIPoints3, UIBoolean };