You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
856 lines
16 KiB
JavaScript
856 lines
16 KiB
JavaScript
import * as THREE from 'three';
|
|
|
|
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
|
|
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
|
|
import { TGALoader } from 'three/addons/loaders/TGALoader.js';
|
|
import { FullScreenQuad } from 'three/addons/postprocessing/Pass.js';
|
|
|
|
import { UISpan, UIDiv, UIRow, UIButton, UICheckbox, UIText, UINumber } from './ui.js';
|
|
import { MoveObjectCommand } from '../commands/MoveObjectCommand.js';
|
|
|
|
const cache = new Map();
|
|
|
|
class UITexture extends UISpan {
|
|
|
|
constructor( editor ) {
|
|
|
|
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();
|
|
|
|
const hash = `${file.lastModified}_${file.size}_${file.name}`;
|
|
|
|
if ( cache.has( hash ) ) {
|
|
|
|
const texture = cache.get( hash );
|
|
|
|
scope.setValue( texture );
|
|
|
|
if ( scope.onChangeCallback ) scope.onChangeCallback( texture );
|
|
|
|
} else if ( extension === 'hdr' || extension === 'pic' ) {
|
|
|
|
reader.addEventListener( 'load', function ( event ) {
|
|
|
|
// assuming RGBE/Radiance HDR image format
|
|
|
|
const loader = new RGBELoader();
|
|
loader.load( event.target.result, function ( hdrTexture ) {
|
|
|
|
hdrTexture.sourceFile = file.name;
|
|
|
|
cache.set( hash, hdrTexture );
|
|
|
|
scope.setValue( hdrTexture );
|
|
|
|
if ( scope.onChangeCallback ) scope.onChangeCallback( hdrTexture );
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
reader.readAsDataURL( file );
|
|
|
|
} else if ( extension === 'tga' ) {
|
|
|
|
reader.addEventListener( 'load', function ( event ) {
|
|
|
|
const loader = new TGALoader();
|
|
loader.load( event.target.result, function ( texture ) {
|
|
|
|
texture.colorSpace = THREE.SRGBColorSpace;
|
|
texture.sourceFile = file.name;
|
|
|
|
cache.set( hash, texture );
|
|
|
|
scope.setValue( texture );
|
|
|
|
if ( scope.onChangeCallback ) scope.onChangeCallback( texture );
|
|
|
|
|
|
} );
|
|
|
|
}, false );
|
|
|
|
reader.readAsDataURL( file );
|
|
|
|
} else if ( extension === 'ktx2' ) {
|
|
|
|
reader.addEventListener( 'load', function ( event ) {
|
|
|
|
const arrayBuffer = event.target.result;
|
|
const blobURL = URL.createObjectURL( new Blob( [ arrayBuffer ] ) );
|
|
const ktx2Loader = new KTX2Loader();
|
|
ktx2Loader.setTranscoderPath( '../../examples/jsm/libs/basis/' );
|
|
editor.signals.rendererDetectKTX2Support.dispatch( ktx2Loader );
|
|
|
|
ktx2Loader.load( blobURL, function ( texture ) {
|
|
|
|
texture.colorSpace = THREE.SRGBColorSpace;
|
|
texture.sourceFile = file.name;
|
|
texture.needsUpdate = true;
|
|
|
|
cache.set( hash, texture );
|
|
|
|
scope.setValue( texture );
|
|
|
|
if ( scope.onChangeCallback ) scope.onChangeCallback( texture );
|
|
ktx2Loader.dispose();
|
|
|
|
} );
|
|
|
|
} );
|
|
|
|
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 );
|
|
texture.sourceFile = file.name;
|
|
texture.needsUpdate = true;
|
|
|
|
cache.set( hash, texture );
|
|
|
|
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 !== null && image.width > 0 ) {
|
|
|
|
canvas.title = texture.sourceFile;
|
|
const scale = canvas.width / image.width;
|
|
|
|
if ( texture.isDataTexture || texture.isCompressedTexture ) {
|
|
|
|
const canvas2 = renderToCanvas( texture );
|
|
context.drawImage( canvas2, 0, 0, image.width * scale, image.height * scale );
|
|
|
|
} else {
|
|
|
|
context.drawImage( image, 0, 0, image.width * scale, image.height * scale );
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
canvas.title = texture.sourceFile + ' (error)';
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
canvas.title = 'empty';
|
|
|
|
}
|
|
|
|
this.texture = texture;
|
|
|
|
}
|
|
|
|
setColorSpace( colorSpace ) {
|
|
|
|
const texture = this.getValue();
|
|
|
|
if ( texture !== null ) {
|
|
|
|
texture.colorSpace = colorSpace;
|
|
|
|
}
|
|
|
|
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.code ) {
|
|
|
|
case 'ArrowUp':
|
|
case 'ArrowDown':
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
break;
|
|
|
|
}
|
|
|
|
} );
|
|
|
|
// Keybindings to support arrow navigation
|
|
this.dom.addEventListener( 'keyup', function ( event ) {
|
|
|
|
switch ( event.code ) {
|
|
|
|
case 'ArrowUp':
|
|
scope.selectIndex( scope.selectedIndex - 1 );
|
|
break;
|
|
case 'ArrowDown':
|
|
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 = new Event( 'change', { bubbles: true, cancelable: 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 = new Event( 'change', { bubbles: true, cancelable: 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 = new Event( 'change', { bubbles: true, cancelable: 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;
|
|
|
|
this.update = () => { // bind lexical this
|
|
|
|
if ( this.onChangeCallback !== null ) {
|
|
|
|
this.onChangeCallback();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
onChange( callback ) {
|
|
|
|
this.onChangeCallback = callback;
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
clear() {
|
|
|
|
for ( let i = this.pointsUI.length - 1; i >= 0; -- i ) {
|
|
|
|
this.deletePointRow( i, false );
|
|
|
|
}
|
|
|
|
this.lastPointIdx = 0;
|
|
|
|
}
|
|
|
|
deletePointRow( idx, needsUpdate = true ) {
|
|
|
|
if ( ! this.pointsUI[ idx ] ) return;
|
|
|
|
this.pointsList.remove( this.pointsUI[ idx ].row );
|
|
|
|
this.pointsUI.splice( idx, 1 );
|
|
|
|
if ( needsUpdate === 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, needsUpdate = true ) {
|
|
|
|
this.clear();
|
|
|
|
for ( let i = 0; i < points.length; i ++ ) {
|
|
|
|
const point = points[ i ];
|
|
this.pointsList.add( this.createPointRow( point.x, point.y ) );
|
|
|
|
}
|
|
|
|
if ( needsUpdate === true ) 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, needsUpdate = true ) {
|
|
|
|
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 ) );
|
|
|
|
}
|
|
|
|
if ( needsUpdate === true ) 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, fsQuad;
|
|
|
|
function renderToCanvas( texture ) {
|
|
|
|
if ( renderer === undefined ) {
|
|
|
|
renderer = new THREE.WebGLRenderer();
|
|
|
|
}
|
|
|
|
if ( fsQuad === undefined ) {
|
|
|
|
fsQuad = new FullScreenQuad( new THREE.MeshBasicMaterial() );
|
|
|
|
}
|
|
|
|
const image = texture.image;
|
|
|
|
renderer.setSize( image.width, image.height, false );
|
|
|
|
fsQuad.material.map = texture;
|
|
fsQuad.render( renderer );
|
|
|
|
return renderer.domElement;
|
|
|
|
}
|
|
|
|
export { UITexture, UIOutliner, UIPoints, UIPoints2, UIPoints3, UIBoolean };
|