great stuff
This commit is contained in:
911
src/OBJLoader.js
911
src/OBJLoader.js
@@ -1,911 +0,0 @@
|
||||
import {
|
||||
BufferGeometry,
|
||||
FileLoader,
|
||||
Float32BufferAttribute,
|
||||
Group,
|
||||
LineBasicMaterial,
|
||||
LineSegments,
|
||||
Loader,
|
||||
Material,
|
||||
Mesh,
|
||||
MeshPhongMaterial,
|
||||
Points,
|
||||
PointsMaterial,
|
||||
Vector3
|
||||
} from 'three';
|
||||
|
||||
// o object_name | g group_name
|
||||
const _object_pattern = /^[og]\s*(.+)?/;
|
||||
// mtllib file_reference
|
||||
const _material_library_pattern = /^mtllib /;
|
||||
// usemtl material_name
|
||||
const _material_use_pattern = /^usemtl /;
|
||||
// usemap map_name
|
||||
const _map_use_pattern = /^usemap /;
|
||||
|
||||
const _vA = new Vector3();
|
||||
const _vB = new Vector3();
|
||||
const _vC = new Vector3();
|
||||
|
||||
const _ab = new Vector3();
|
||||
const _cb = new Vector3();
|
||||
|
||||
function ParserState() {
|
||||
|
||||
const state = {
|
||||
objects: [],
|
||||
object: {},
|
||||
|
||||
vertices: [],
|
||||
normals: [],
|
||||
colors: [],
|
||||
uvs: [],
|
||||
|
||||
materials: {},
|
||||
materialLibraries: [],
|
||||
|
||||
startObject: function ( name, fromDeclaration ) {
|
||||
|
||||
// If the current object (initial from reset) is not from a g/o declaration in the parsed
|
||||
// file. We need to use it for the first parsed g/o to keep things in sync.
|
||||
if ( this.object && this.object.fromDeclaration === false ) {
|
||||
|
||||
this.object.name = name;
|
||||
this.object.fromDeclaration = ( fromDeclaration !== false );
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
const previousMaterial = ( this.object && typeof this.object.currentMaterial === 'function' ? this.object.currentMaterial() : undefined );
|
||||
|
||||
if ( this.object && typeof this.object._finalize === 'function' ) {
|
||||
|
||||
this.object._finalize( true );
|
||||
|
||||
}
|
||||
|
||||
this.object = {
|
||||
name: name || '',
|
||||
fromDeclaration: ( fromDeclaration !== false ),
|
||||
|
||||
geometry: {
|
||||
vertices: [],
|
||||
normals: [],
|
||||
colors: [],
|
||||
uvs: [],
|
||||
hasUVIndices: false
|
||||
},
|
||||
materials: [],
|
||||
smooth: true,
|
||||
|
||||
startMaterial: function ( name, libraries ) {
|
||||
|
||||
const previous = this._finalize( false );
|
||||
|
||||
// New usemtl declaration overwrites an inherited material, except if faces were declared
|
||||
// after the material, then it must be preserved for proper MultiMaterial continuation.
|
||||
if ( previous && ( previous.inherited || previous.groupCount <= 0 ) ) {
|
||||
|
||||
this.materials.splice( previous.index, 1 );
|
||||
|
||||
}
|
||||
|
||||
const material = {
|
||||
index: this.materials.length,
|
||||
name: name || '',
|
||||
mtllib: ( Array.isArray( libraries ) && libraries.length > 0 ? libraries[ libraries.length - 1 ] : '' ),
|
||||
smooth: ( previous !== undefined ? previous.smooth : this.smooth ),
|
||||
groupStart: ( previous !== undefined ? previous.groupEnd : 0 ),
|
||||
groupEnd: - 1,
|
||||
groupCount: - 1,
|
||||
inherited: false,
|
||||
|
||||
clone: function ( index ) {
|
||||
|
||||
const cloned = {
|
||||
index: ( typeof index === 'number' ? index : this.index ),
|
||||
name: this.name,
|
||||
mtllib: this.mtllib,
|
||||
smooth: this.smooth,
|
||||
groupStart: 0,
|
||||
groupEnd: - 1,
|
||||
groupCount: - 1,
|
||||
inherited: false
|
||||
};
|
||||
cloned.clone = this.clone.bind( cloned );
|
||||
return cloned;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
this.materials.push( material );
|
||||
|
||||
return material;
|
||||
|
||||
},
|
||||
|
||||
currentMaterial: function () {
|
||||
|
||||
if ( this.materials.length > 0 ) {
|
||||
|
||||
return this.materials[ this.materials.length - 1 ];
|
||||
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
||||
},
|
||||
|
||||
_finalize: function ( end ) {
|
||||
|
||||
const lastMultiMaterial = this.currentMaterial();
|
||||
if ( lastMultiMaterial && lastMultiMaterial.groupEnd === - 1 ) {
|
||||
|
||||
lastMultiMaterial.groupEnd = this.geometry.vertices.length / 3;
|
||||
lastMultiMaterial.groupCount = lastMultiMaterial.groupEnd - lastMultiMaterial.groupStart;
|
||||
lastMultiMaterial.inherited = false;
|
||||
|
||||
}
|
||||
|
||||
// Ignore objects tail materials if no face declarations followed them before a new o/g started.
|
||||
if ( end && this.materials.length > 1 ) {
|
||||
|
||||
for ( let mi = this.materials.length - 1; mi >= 0; mi -- ) {
|
||||
|
||||
if ( this.materials[ mi ].groupCount <= 0 ) {
|
||||
|
||||
this.materials.splice( mi, 1 );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Guarantee at least one empty material, this makes the creation later more straight forward.
|
||||
if ( end && this.materials.length === 0 ) {
|
||||
|
||||
this.materials.push( {
|
||||
name: '',
|
||||
smooth: this.smooth
|
||||
} );
|
||||
|
||||
}
|
||||
|
||||
return lastMultiMaterial;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
// Inherit previous objects material.
|
||||
// Spec tells us that a declared material must be set to all objects until a new material is declared.
|
||||
// If a usemtl declaration is encountered while this new object is being parsed, it will
|
||||
// overwrite the inherited material. Exception being that there was already face declarations
|
||||
// to the inherited material, then it will be preserved for proper MultiMaterial continuation.
|
||||
|
||||
if ( previousMaterial && previousMaterial.name && typeof previousMaterial.clone === 'function' ) {
|
||||
|
||||
const declared = previousMaterial.clone( 0 );
|
||||
declared.inherited = true;
|
||||
this.object.materials.push( declared );
|
||||
|
||||
}
|
||||
|
||||
this.objects.push( this.object );
|
||||
|
||||
},
|
||||
|
||||
finalize: function () {
|
||||
|
||||
if ( this.object && typeof this.object._finalize === 'function' ) {
|
||||
|
||||
this.object._finalize( true );
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
parseVertexIndex: function ( value, len ) {
|
||||
|
||||
const index = parseInt( value, 10 );
|
||||
return ( index >= 0 ? index - 1 : index + len / 3 ) * 3;
|
||||
|
||||
},
|
||||
|
||||
parseNormalIndex: function ( value, len ) {
|
||||
|
||||
const index = parseInt( value, 10 );
|
||||
return ( index >= 0 ? index - 1 : index + len / 3 ) * 3;
|
||||
|
||||
},
|
||||
|
||||
parseUVIndex: function ( value, len ) {
|
||||
|
||||
const index = parseInt( value, 10 );
|
||||
return ( index >= 0 ? index - 1 : index + len / 2 ) * 2;
|
||||
|
||||
},
|
||||
|
||||
addVertex: function ( a, b, c ) {
|
||||
|
||||
const src = this.vertices;
|
||||
const dst = this.object.geometry.vertices;
|
||||
|
||||
dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
|
||||
dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
|
||||
dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );
|
||||
|
||||
},
|
||||
|
||||
addVertexPoint: function ( a ) {
|
||||
|
||||
const src = this.vertices;
|
||||
const dst = this.object.geometry.vertices;
|
||||
|
||||
dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
|
||||
|
||||
},
|
||||
|
||||
addVertexLine: function ( a ) {
|
||||
|
||||
const src = this.vertices;
|
||||
const dst = this.object.geometry.vertices;
|
||||
|
||||
dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
|
||||
|
||||
},
|
||||
|
||||
addNormal: function ( a, b, c ) {
|
||||
|
||||
const src = this.normals;
|
||||
const dst = this.object.geometry.normals;
|
||||
|
||||
dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
|
||||
dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
|
||||
dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );
|
||||
|
||||
},
|
||||
|
||||
addFaceNormal: function ( a, b, c ) {
|
||||
|
||||
const src = this.vertices;
|
||||
const dst = this.object.geometry.normals;
|
||||
|
||||
_vA.fromArray( src, a );
|
||||
_vB.fromArray( src, b );
|
||||
_vC.fromArray( src, c );
|
||||
|
||||
_cb.subVectors( _vC, _vB );
|
||||
_ab.subVectors( _vA, _vB );
|
||||
_cb.cross( _ab );
|
||||
|
||||
_cb.normalize();
|
||||
|
||||
dst.push( _cb.x, _cb.y, _cb.z );
|
||||
dst.push( _cb.x, _cb.y, _cb.z );
|
||||
dst.push( _cb.x, _cb.y, _cb.z );
|
||||
|
||||
},
|
||||
|
||||
addColor: function ( a, b, c ) {
|
||||
|
||||
const src = this.colors;
|
||||
const dst = this.object.geometry.colors;
|
||||
|
||||
if ( src[ a ] !== undefined ) dst.push( src[ a + 0 ], src[ a + 1 ], src[ a + 2 ] );
|
||||
if ( src[ b ] !== undefined ) dst.push( src[ b + 0 ], src[ b + 1 ], src[ b + 2 ] );
|
||||
if ( src[ c ] !== undefined ) dst.push( src[ c + 0 ], src[ c + 1 ], src[ c + 2 ] );
|
||||
|
||||
},
|
||||
|
||||
addUV: function ( a, b, c ) {
|
||||
|
||||
const src = this.uvs;
|
||||
const dst = this.object.geometry.uvs;
|
||||
|
||||
dst.push( src[ a + 0 ], src[ a + 1 ] );
|
||||
dst.push( src[ b + 0 ], src[ b + 1 ] );
|
||||
dst.push( src[ c + 0 ], src[ c + 1 ] );
|
||||
|
||||
},
|
||||
|
||||
addDefaultUV: function () {
|
||||
|
||||
const dst = this.object.geometry.uvs;
|
||||
|
||||
dst.push( 0, 0 );
|
||||
dst.push( 0, 0 );
|
||||
dst.push( 0, 0 );
|
||||
|
||||
},
|
||||
|
||||
addUVLine: function ( a ) {
|
||||
|
||||
const src = this.uvs;
|
||||
const dst = this.object.geometry.uvs;
|
||||
|
||||
dst.push( src[ a + 0 ], src[ a + 1 ] );
|
||||
|
||||
},
|
||||
|
||||
addFace: function ( a, b, c, ua, ub, uc, na, nb, nc ) {
|
||||
|
||||
const vLen = this.vertices.length;
|
||||
|
||||
let ia = this.parseVertexIndex( a, vLen );
|
||||
let ib = this.parseVertexIndex( b, vLen );
|
||||
let ic = this.parseVertexIndex( c, vLen );
|
||||
|
||||
this.addVertex( ia, ib, ic );
|
||||
this.addColor( ia, ib, ic );
|
||||
|
||||
// normals
|
||||
|
||||
if ( na !== undefined && na !== '' ) {
|
||||
|
||||
const nLen = this.normals.length;
|
||||
|
||||
ia = this.parseNormalIndex( na, nLen );
|
||||
ib = this.parseNormalIndex( nb, nLen );
|
||||
ic = this.parseNormalIndex( nc, nLen );
|
||||
|
||||
this.addNormal( ia, ib, ic );
|
||||
|
||||
} else {
|
||||
|
||||
this.addFaceNormal( ia, ib, ic );
|
||||
|
||||
}
|
||||
|
||||
// uvs
|
||||
|
||||
if ( ua !== undefined && ua !== '' ) {
|
||||
|
||||
const uvLen = this.uvs.length;
|
||||
|
||||
ia = this.parseUVIndex( ua, uvLen );
|
||||
ib = this.parseUVIndex( ub, uvLen );
|
||||
ic = this.parseUVIndex( uc, uvLen );
|
||||
|
||||
this.addUV( ia, ib, ic );
|
||||
|
||||
this.object.geometry.hasUVIndices = true;
|
||||
|
||||
} else {
|
||||
|
||||
// add placeholder values (for inconsistent face definitions)
|
||||
|
||||
this.addDefaultUV();
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
addPointGeometry: function ( vertices ) {
|
||||
|
||||
this.object.geometry.type = 'Points';
|
||||
|
||||
const vLen = this.vertices.length;
|
||||
|
||||
for ( let vi = 0, l = vertices.length; vi < l; vi ++ ) {
|
||||
|
||||
const index = this.parseVertexIndex( vertices[ vi ], vLen );
|
||||
|
||||
this.addVertexPoint( index );
|
||||
this.addColor( index );
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
addLineGeometry: function ( vertices, uvs ) {
|
||||
|
||||
this.object.geometry.type = 'Line';
|
||||
|
||||
const vLen = this.vertices.length;
|
||||
const uvLen = this.uvs.length;
|
||||
|
||||
for ( let vi = 0, l = vertices.length; vi < l; vi ++ ) {
|
||||
|
||||
this.addVertexLine( this.parseVertexIndex( vertices[ vi ], vLen ) );
|
||||
|
||||
}
|
||||
|
||||
for ( let uvi = 0, l = uvs.length; uvi < l; uvi ++ ) {
|
||||
|
||||
this.addUVLine( this.parseUVIndex( uvs[ uvi ], uvLen ) );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
state.startObject( '', false );
|
||||
|
||||
return state;
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
class OBJLoader extends Loader {
|
||||
|
||||
constructor( manager ) {
|
||||
|
||||
super( manager );
|
||||
|
||||
this.materials = null;
|
||||
|
||||
}
|
||||
|
||||
load( url, onLoad, onProgress, onError ) {
|
||||
|
||||
const scope = this;
|
||||
|
||||
const loader = new FileLoader( this.manager );
|
||||
loader.setPath( this.path );
|
||||
loader.setRequestHeader( this.requestHeader );
|
||||
loader.setWithCredentials( this.withCredentials );
|
||||
loader.load( url, function ( text ) {
|
||||
|
||||
try {
|
||||
|
||||
onLoad( scope.parse( text ) );
|
||||
|
||||
} catch ( e ) {
|
||||
|
||||
if ( onError ) {
|
||||
|
||||
onError( e );
|
||||
|
||||
} else {
|
||||
|
||||
console.error( e );
|
||||
|
||||
}
|
||||
|
||||
scope.manager.itemError( url );
|
||||
|
||||
}
|
||||
|
||||
}, onProgress, onError );
|
||||
|
||||
}
|
||||
|
||||
setMaterials( materials ) {
|
||||
|
||||
this.materials = materials;
|
||||
|
||||
return this;
|
||||
|
||||
}
|
||||
|
||||
parse( text ) {
|
||||
|
||||
const state = new ParserState();
|
||||
|
||||
if ( text.indexOf( '\r\n' ) !== - 1 ) {
|
||||
|
||||
// This is faster than String.split with regex that splits on both
|
||||
text = text.replace( /\r\n/g, '\n' );
|
||||
|
||||
}
|
||||
|
||||
if ( text.indexOf( '\\\n' ) !== - 1 ) {
|
||||
|
||||
// join lines separated by a line continuation character (\)
|
||||
text = text.replace( /\\\n/g, '' );
|
||||
|
||||
}
|
||||
|
||||
const lines = text.split( '\n' );
|
||||
let line = '', lineFirstChar = '';
|
||||
let lineLength = 0;
|
||||
let result = [];
|
||||
|
||||
// Faster to just trim left side of the line. Use if available.
|
||||
const trimLeft = ( typeof ''.trimLeft === 'function' );
|
||||
|
||||
for ( let i = 0, l = lines.length; i < l; i ++ ) {
|
||||
|
||||
line = lines[ i ];
|
||||
|
||||
line = trimLeft ? line.trimLeft() : line.trim();
|
||||
|
||||
lineLength = line.length;
|
||||
|
||||
if ( lineLength === 0 ) continue;
|
||||
|
||||
lineFirstChar = line.charAt( 0 );
|
||||
|
||||
// @todo invoke passed in handler if any
|
||||
if ( lineFirstChar === '#' ) continue;
|
||||
|
||||
if ( lineFirstChar === 'v' ) {
|
||||
|
||||
const data = line.split( /\s+/ );
|
||||
|
||||
switch ( data[ 0 ] ) {
|
||||
|
||||
case 'v':
|
||||
state.vertices.push(
|
||||
parseFloat( data[ 1 ] ),
|
||||
parseFloat( data[ 2 ] ),
|
||||
parseFloat( data[ 3 ] )
|
||||
);
|
||||
if ( data.length >= 7 ) {
|
||||
|
||||
state.colors.push(
|
||||
parseFloat( data[ 4 ] ),
|
||||
parseFloat( data[ 5 ] ),
|
||||
parseFloat( data[ 6 ] )
|
||||
|
||||
);
|
||||
|
||||
} else {
|
||||
|
||||
// if no colors are defined, add placeholders so color and vertex indices match
|
||||
|
||||
state.colors.push( undefined, undefined, undefined );
|
||||
|
||||
}
|
||||
|
||||
break;
|
||||
case 'vn':
|
||||
state.normals.push(
|
||||
parseFloat( data[ 1 ] ),
|
||||
parseFloat( data[ 2 ] ),
|
||||
parseFloat( data[ 3 ] )
|
||||
);
|
||||
break;
|
||||
case 'vt':
|
||||
state.uvs.push(
|
||||
parseFloat( data[ 1 ] ),
|
||||
parseFloat( data[ 2 ] )
|
||||
);
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
} else if ( lineFirstChar === 'f' ) {
|
||||
|
||||
const lineData = line.substr( 1 ).trim();
|
||||
const vertexData = lineData.split( /\s+/ );
|
||||
const faceVertices = [];
|
||||
|
||||
// Parse the face vertex data into an easy to work with format
|
||||
|
||||
for ( let j = 0, jl = vertexData.length; j < jl; j ++ ) {
|
||||
|
||||
const vertex = vertexData[ j ];
|
||||
|
||||
if ( vertex.length > 0 ) {
|
||||
|
||||
const vertexParts = vertex.split( '/' );
|
||||
faceVertices.push( vertexParts );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Draw an edge between the first vertex and all subsequent vertices to form an n-gon
|
||||
|
||||
const v1 = faceVertices[ 0 ];
|
||||
|
||||
for ( let j = 1, jl = faceVertices.length - 1; j < jl; j ++ ) {
|
||||
|
||||
const v2 = faceVertices[ j ];
|
||||
const v3 = faceVertices[ j + 1 ];
|
||||
|
||||
state.addFace(
|
||||
v1[ 0 ], v2[ 0 ], v3[ 0 ],
|
||||
v1[ 1 ], v2[ 1 ], v3[ 1 ],
|
||||
v1[ 2 ], v2[ 2 ], v3[ 2 ]
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
} else if ( lineFirstChar === 'l' ) {
|
||||
|
||||
const lineParts = line.substring( 1 ).trim().split( ' ' );
|
||||
let lineVertices = [];
|
||||
const lineUVs = [];
|
||||
|
||||
if ( line.indexOf( '/' ) === - 1 ) {
|
||||
|
||||
lineVertices = lineParts;
|
||||
|
||||
} else {
|
||||
|
||||
for ( let li = 0, llen = lineParts.length; li < llen; li ++ ) {
|
||||
|
||||
const parts = lineParts[ li ].split( '/' );
|
||||
|
||||
if ( parts[ 0 ] !== '' ) lineVertices.push( parts[ 0 ] );
|
||||
if ( parts[ 1 ] !== '' ) lineUVs.push( parts[ 1 ] );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
state.addLineGeometry( lineVertices, lineUVs );
|
||||
|
||||
} else if ( lineFirstChar === 'p' ) {
|
||||
|
||||
const lineData = line.substr( 1 ).trim();
|
||||
const pointData = lineData.split( ' ' );
|
||||
|
||||
state.addPointGeometry( pointData );
|
||||
|
||||
} else if ( ( result = _object_pattern.exec( line ) ) !== null ) {
|
||||
|
||||
// o object_name
|
||||
// or
|
||||
// g group_name
|
||||
|
||||
// WORKAROUND: https://bugs.chromium.org/p/v8/issues/detail?id=2869
|
||||
// let name = result[ 0 ].substr( 1 ).trim();
|
||||
const name = ( ' ' + result[ 0 ].substr( 1 ).trim() ).substr( 1 );
|
||||
|
||||
state.startObject( name );
|
||||
|
||||
} else if ( _material_use_pattern.test( line ) ) {
|
||||
|
||||
// material
|
||||
|
||||
state.object.startMaterial( line.substring( 7 ).trim(), state.materialLibraries );
|
||||
|
||||
} else if ( _material_library_pattern.test( line ) ) {
|
||||
|
||||
// mtl file
|
||||
|
||||
state.materialLibraries.push( line.substring( 7 ).trim() );
|
||||
|
||||
} else if ( _map_use_pattern.test( line ) ) {
|
||||
|
||||
// the line is parsed but ignored since the loader assumes textures are defined MTL files
|
||||
// (according to https://www.okino.com/conv/imp_wave.htm, 'usemap' is the old-style Wavefront texture reference method)
|
||||
|
||||
console.warn( 'THREE.OBJLoader: Rendering identifier "usemap" not supported. Textures must be defined in MTL files.' );
|
||||
|
||||
} else if ( lineFirstChar === 's' ) {
|
||||
|
||||
result = line.split( ' ' );
|
||||
|
||||
// smooth shading
|
||||
|
||||
// @todo Handle files that have varying smooth values for a set of faces inside one geometry,
|
||||
// but does not define a usemtl for each face set.
|
||||
// This should be detected and a dummy material created (later MultiMaterial and geometry groups).
|
||||
// This requires some care to not create extra material on each smooth value for "normal" obj files.
|
||||
// where explicit usemtl defines geometry groups.
|
||||
// Example asset: examples/models/obj/cerberus/Cerberus.obj
|
||||
|
||||
/*
|
||||
* http://paulbourke.net/dataformats/obj/
|
||||
* or
|
||||
* http://www.cs.utah.edu/~boulos/cs3505/obj_spec.pdf
|
||||
*
|
||||
* From chapter "Grouping" Syntax explanation "s group_number":
|
||||
* "group_number is the smoothing group number. To turn off smoothing groups, use a value of 0 or off.
|
||||
* Polygonal elements use group numbers to put elements in different smoothing groups. For free-form
|
||||
* surfaces, smoothing groups are either turned on or off; there is no difference between values greater
|
||||
* than 0."
|
||||
*/
|
||||
if ( result.length > 1 ) {
|
||||
|
||||
const value = result[ 1 ].trim().toLowerCase();
|
||||
state.object.smooth = ( value !== '0' && value !== 'off' );
|
||||
|
||||
} else {
|
||||
|
||||
// ZBrush can produce "s" lines #11707
|
||||
state.object.smooth = true;
|
||||
|
||||
}
|
||||
|
||||
const material = state.object.currentMaterial();
|
||||
if ( material ) material.smooth = state.object.smooth;
|
||||
|
||||
} else {
|
||||
|
||||
// Handle null terminated files without exception
|
||||
if ( line === '\0' ) continue;
|
||||
|
||||
console.warn( 'THREE.OBJLoader: Unexpected line: "' + line + '"' );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
state.finalize();
|
||||
|
||||
const container = new Group();
|
||||
container.materialLibraries = [].concat( state.materialLibraries );
|
||||
|
||||
const hasPrimitives = ! ( state.objects.length === 1 && state.objects[ 0 ].geometry.vertices.length === 0 );
|
||||
|
||||
if ( hasPrimitives === true ) {
|
||||
|
||||
for ( let i = 0, l = state.objects.length; i < l; i ++ ) {
|
||||
|
||||
const object = state.objects[ i ];
|
||||
const geometry = object.geometry;
|
||||
const materials = object.materials;
|
||||
const isLine = ( geometry.type === 'Line' );
|
||||
const isPoints = ( geometry.type === 'Points' );
|
||||
let hasVertexColors = false;
|
||||
|
||||
// Skip o/g line declarations that did not follow with any faces
|
||||
if ( geometry.vertices.length === 0 ) continue;
|
||||
|
||||
const buffergeometry = new BufferGeometry();
|
||||
|
||||
buffergeometry.setAttribute( 'position', new Float32BufferAttribute( geometry.vertices, 3 ) );
|
||||
|
||||
if ( geometry.normals.length > 0 ) {
|
||||
|
||||
buffergeometry.setAttribute( 'normal', new Float32BufferAttribute( geometry.normals, 3 ) );
|
||||
|
||||
}
|
||||
|
||||
if ( geometry.colors.length > 0 ) {
|
||||
|
||||
hasVertexColors = true;
|
||||
buffergeometry.setAttribute( 'color', new Float32BufferAttribute( geometry.colors, 3 ) );
|
||||
|
||||
}
|
||||
|
||||
if ( geometry.hasUVIndices === true ) {
|
||||
|
||||
buffergeometry.setAttribute( 'uv', new Float32BufferAttribute( geometry.uvs, 2 ) );
|
||||
|
||||
}
|
||||
|
||||
// Create materials
|
||||
|
||||
const createdMaterials = [];
|
||||
|
||||
for ( let mi = 0, miLen = materials.length; mi < miLen; mi ++ ) {
|
||||
|
||||
const sourceMaterial = materials[ mi ];
|
||||
const materialHash = sourceMaterial.name + '_' + sourceMaterial.smooth + '_' + hasVertexColors;
|
||||
let material = state.materials[ materialHash ];
|
||||
|
||||
if ( this.materials !== null ) {
|
||||
|
||||
material = this.materials.create( sourceMaterial.name );
|
||||
|
||||
// mtl etc. loaders probably can't create line materials correctly, copy properties to a line material.
|
||||
if ( isLine && material && ! ( material instanceof LineBasicMaterial ) ) {
|
||||
|
||||
const materialLine = new LineBasicMaterial();
|
||||
Material.prototype.copy.call( materialLine, material );
|
||||
materialLine.color.copy( material.color );
|
||||
material = materialLine;
|
||||
|
||||
} else if ( isPoints && material && ! ( material instanceof PointsMaterial ) ) {
|
||||
|
||||
const materialPoints = new PointsMaterial( { size: 10, sizeAttenuation: false } );
|
||||
Material.prototype.copy.call( materialPoints, material );
|
||||
materialPoints.color.copy( material.color );
|
||||
materialPoints.map = material.map;
|
||||
material = materialPoints;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ( material === undefined ) {
|
||||
|
||||
if ( isLine ) {
|
||||
|
||||
material = new LineBasicMaterial();
|
||||
|
||||
} else if ( isPoints ) {
|
||||
|
||||
material = new PointsMaterial( { size: 1, sizeAttenuation: false } );
|
||||
|
||||
} else {
|
||||
|
||||
material = new MeshPhongMaterial();
|
||||
|
||||
}
|
||||
|
||||
material.name = sourceMaterial.name;
|
||||
material.flatShading = sourceMaterial.smooth ? false : true;
|
||||
material.vertexColors = hasVertexColors;
|
||||
|
||||
state.materials[ materialHash ] = material;
|
||||
|
||||
}
|
||||
|
||||
createdMaterials.push( material );
|
||||
|
||||
}
|
||||
|
||||
// Create mesh
|
||||
|
||||
let mesh;
|
||||
|
||||
if ( createdMaterials.length > 1 ) {
|
||||
|
||||
for ( let mi = 0, miLen = materials.length; mi < miLen; mi ++ ) {
|
||||
|
||||
const sourceMaterial = materials[ mi ];
|
||||
buffergeometry.addGroup( sourceMaterial.groupStart, sourceMaterial.groupCount, mi );
|
||||
|
||||
}
|
||||
|
||||
if ( isLine ) {
|
||||
|
||||
mesh = new LineSegments( buffergeometry, createdMaterials );
|
||||
|
||||
} else if ( isPoints ) {
|
||||
|
||||
mesh = new Points( buffergeometry, createdMaterials );
|
||||
|
||||
} else {
|
||||
|
||||
mesh = new Mesh( buffergeometry, createdMaterials );
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if ( isLine ) {
|
||||
|
||||
mesh = new LineSegments( buffergeometry, createdMaterials[ 0 ] );
|
||||
|
||||
} else if ( isPoints ) {
|
||||
|
||||
mesh = new Points( buffergeometry, createdMaterials[ 0 ] );
|
||||
|
||||
} else {
|
||||
|
||||
mesh = new Mesh( buffergeometry, createdMaterials[ 0 ] );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
mesh.name = object.name;
|
||||
|
||||
container.add( mesh );
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// if there is only the default parser state object with no geometry data, interpret data as point cloud
|
||||
|
||||
if ( state.vertices.length > 0 ) {
|
||||
|
||||
const material = new PointsMaterial( { size: 1, sizeAttenuation: false } );
|
||||
|
||||
const buffergeometry = new BufferGeometry();
|
||||
|
||||
buffergeometry.setAttribute( 'position', new Float32BufferAttribute( state.vertices, 3 ) );
|
||||
|
||||
if ( state.colors.length > 0 && state.colors[ 0 ] !== undefined ) {
|
||||
|
||||
buffergeometry.setAttribute( 'color', new Float32BufferAttribute( state.colors, 3 ) );
|
||||
material.vertexColors = true;
|
||||
|
||||
}
|
||||
|
||||
const points = new Points( buffergeometry, material );
|
||||
container.add( points );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return container;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { OBJLoader };
|
||||
@@ -1,165 +0,0 @@
|
||||
import * as THREE from 'three';
|
||||
import { OBJLoader } from './OBJLoader.js';
|
||||
import VoxelSpace from './solver/VoxelSpace';
|
||||
import type SomaSolution from "./solver/SomaSolution";
|
||||
import RotationControl from "./RotationControl";
|
||||
|
||||
export default class PolycubeScene {
|
||||
private renderer: THREE.WebGLRenderer;
|
||||
private camera: THREE.Camera;
|
||||
private mainScene: THREE.Scene;
|
||||
private polycubeMeshes: THREE.Mesh[] = [];
|
||||
private controls: RotationControl;
|
||||
private light: THREE.Light;
|
||||
private lastDims: number = 0;
|
||||
private lastColor: string = "#FF0000";
|
||||
private lastPolycube: bigint = 0n;
|
||||
private cubeMaterial: THREE.MeshPhongMaterial;
|
||||
private materials: Record<number, THREE.MeshPhongMaterial> = {};
|
||||
private cubeGeometry: THREE.BufferGeometry;
|
||||
private cubeScene: THREE.Scene;
|
||||
|
||||
constructor(el: HTMLCanvasElement, onReady: () => any, onError: (err: Error) => any) {
|
||||
this.init(el).then(onReady).catch(onError);
|
||||
}
|
||||
|
||||
private async init(el: HTMLCanvasElement) {
|
||||
this.renderer = new THREE.WebGLRenderer({canvas: el});
|
||||
this.setupCamera(el.clientWidth / el.clientHeight);
|
||||
this.setupLight();
|
||||
try {
|
||||
await this.createCubeGeometry();
|
||||
} catch (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
this.createCubeMaterial("red");
|
||||
this.mainScene = new THREE.Scene();
|
||||
this.cubeScene = new THREE.Scene();
|
||||
this.mainScene.add(this.cubeScene, this.camera);
|
||||
this.camera.add(this.light);
|
||||
this.cubeScene.rotateX(Math.PI/4);
|
||||
this.cubeScene.rotateY(Math.PI/4);
|
||||
this.controls = new RotationControl(this.cubeScene, this.camera, el);
|
||||
requestAnimationFrame((timestamp) => this.render(timestamp));
|
||||
}
|
||||
|
||||
private setupCamera(aspect: number) {
|
||||
const fov = 60;
|
||||
const near = 0.1;
|
||||
const far = 15;
|
||||
this.camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
|
||||
this.camera.position.z = 6;
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
}
|
||||
|
||||
private setPolycube(polycube: bigint, dims: number, color: string) {
|
||||
if (dims !== this.lastDims) {
|
||||
this.updateCubesFromDims(dims);
|
||||
}
|
||||
|
||||
if (polycube !== this.lastPolycube) {
|
||||
let i = 0;
|
||||
const voxelSpace = new VoxelSpace(0, [dims, dims, dims], polycube, true);
|
||||
const newDims = voxelSpace.getDims();
|
||||
this.polycubeMeshes.forEach(mesh => {
|
||||
mesh.position.set(1000, 1000, 1000);
|
||||
mesh.material = this.cubeMaterial;
|
||||
});
|
||||
voxelSpace.forEachCell((val: boolean, x: number, y: number, z: number) => {
|
||||
if (val) {
|
||||
this.polycubeMeshes[i].position.set(
|
||||
-((newDims[2] - 1)/2) + z,
|
||||
((newDims[0] - 1)/2) - x,
|
||||
-((newDims[1] - 1)/2) + y,
|
||||
);
|
||||
}
|
||||
i++;
|
||||
});
|
||||
this.lastPolycube = polycube;
|
||||
}
|
||||
|
||||
if (color !== this.lastColor) {
|
||||
this.cubeMaterial.color.set(color);
|
||||
this.lastColor = color;
|
||||
}
|
||||
}
|
||||
|
||||
private updateCubesFromDims(newDims: number) {
|
||||
const requiredCubes = newDims**3;
|
||||
if (this.polycubeMeshes.length < requiredCubes) {
|
||||
for (let i = this.polycubeMeshes.length; i < requiredCubes; i++) {
|
||||
const newCube = new THREE.Mesh(this.cubeGeometry, this.cubeMaterial);
|
||||
this.cubeScene.add(newCube);
|
||||
this.polycubeMeshes.push(newCube);
|
||||
}
|
||||
}
|
||||
if (newDims < this.lastDims || this.lastDims === 0) {
|
||||
this.polycubeMeshes.forEach(mesh => mesh.position.set(1000, 1000, 1000));
|
||||
}
|
||||
this.lastDims = newDims;
|
||||
}
|
||||
|
||||
private setSolution(solution: SomaSolution, colorMap: Record<number, string>) {
|
||||
const dims = solution.getDims();
|
||||
if (dims[0] !== this.lastDims) {
|
||||
this.updateCubesFromDims(dims[0]);
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
this.polycubeMeshes.forEach(mesh => mesh.position.set(1000, 1000, 1000));
|
||||
Object.keys(colorMap).forEach(key => {
|
||||
if (!this.materials[key]) {
|
||||
this.materials[key] = this.newCubeMaterial(colorMap[key]);
|
||||
}
|
||||
})
|
||||
solution.forEachCell((val: number, x: number, y: number, z: number) => {
|
||||
this.polycubeMeshes[i].position.set(
|
||||
-((dims[2] - 1)/2) + z,
|
||||
((dims[0] - 1)/2) - x,
|
||||
-((dims[1] - 1)/2) + y,
|
||||
);
|
||||
this.polycubeMeshes[i].material = this.materials[val];
|
||||
i++;
|
||||
});
|
||||
}
|
||||
|
||||
private setupLight() {
|
||||
const color = 0xFFFFFF;
|
||||
const intensity = 1;
|
||||
this.light = new THREE.DirectionalLight(color, intensity);
|
||||
this.light.position.set(-1, 2, 4);
|
||||
}
|
||||
|
||||
private render(time: number) {
|
||||
this.renderer.render(this.mainScene, this.camera);
|
||||
requestAnimationFrame((time: number) => this.render(time));
|
||||
}
|
||||
|
||||
private async createCubeGeometry(): Promise<void> {
|
||||
const onLoaded = (obj: THREE.Mesh, resolve: () => any) => {
|
||||
this.cubeGeometry = (obj.children[0] as THREE.Mesh).geometry;
|
||||
this.cubeGeometry.computeVertexNormals();
|
||||
this.cubeGeometry.computeBoundingSphere();
|
||||
this.cubeGeometry.scale(1/this.cubeGeometry.boundingSphere.radius, 1/this.cubeGeometry.boundingSphere.radius, 1/this.cubeGeometry.boundingSphere.radius);
|
||||
resolve();
|
||||
};
|
||||
const load = (resolve: () => any, reject: (err: string) => any) => {
|
||||
const loader = new OBJLoader();
|
||||
loader.load(
|
||||
'../resources/bevel_cube.obj',
|
||||
obj => onLoaded(obj, resolve),
|
||||
() => {},
|
||||
(err) => reject(`Error loading OBJ file: ${err}`),
|
||||
);
|
||||
};
|
||||
return new Promise<void>(load);
|
||||
}
|
||||
|
||||
private newCubeMaterial(color: string) {
|
||||
return new THREE.MeshPhongMaterial({color});
|
||||
}
|
||||
|
||||
private createCubeMaterial(color: string) {
|
||||
this.cubeMaterial = this.newCubeMaterial(color);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {isMaxDimension, isMinDimension, isMaxPolycubes, isMinPolycubes, somaDimension, polycubes, solutions} from "./store";
|
||||
import SomaSolution from "./solver/SomaSolution";
|
||||
import SolutionList from "./SolutionList.svelte";
|
||||
import VoxelSpace from "./solver/VoxelSpace";
|
||||
|
||||
$: numCubes = $polycubes.length;
|
||||
$: cubes = $polycubes;
|
||||
let consoleOutput = "Press the solve button!";
|
||||
let solving = false;
|
||||
|
||||
function solve() {
|
||||
consoleOutput = "Solving\n";
|
||||
const polycubes = cubes.map(cubeInput => cubeInput.rep);
|
||||
const worker = new Worker('../solver/main.js', {type: "module"});
|
||||
solving = true;
|
||||
worker.addEventListener('message', (event) => {
|
||||
solutions.set(event.data.map(solnData => {
|
||||
const solution = new SomaSolution(solnData.dim);
|
||||
solnData.solutionSpaces.forEach((voxelSpace, i) => solution.addSpace(new VoxelSpace(i, [solnData.dim, solnData.dim, solnData.dim], voxelSpace.space)));
|
||||
return solution;
|
||||
}));
|
||||
solving = false;
|
||||
});
|
||||
worker.postMessage({polycubes, dims: $somaDimension});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<h1>Somaesque</h1>
|
||||
<h3>Settings</h3>
|
||||
|
||||
<div class="option">
|
||||
<p>Cube Dimension: {$somaDimension}</p>
|
||||
<button on:click={somaDimension.dec} disabled={$isMinDimension}>-</button>
|
||||
<button on:click={somaDimension.inc} disabled={$isMaxDimension}>+</button>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p>Cubes: {numCubes}</p>
|
||||
<button on:click={polycubes.removeCube} disabled={$isMinPolycubes}>-</button>
|
||||
<button on:click={polycubes.addCube} disabled={$isMaxPolycubes}>+</button>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<button on:click={solve}>{solving ? 'Solving...' : 'Solve'}</button>
|
||||
</div>
|
||||
|
||||
<SolutionList/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
p {
|
||||
display: inline-block;
|
||||
}
|
||||
button {
|
||||
display: inline-block;
|
||||
}
|
||||
.container {
|
||||
height: 100%;
|
||||
background-color: #333333;
|
||||
padding: 1em;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
color: #ff3e00;
|
||||
font-size: 3em;
|
||||
font-weight: 100;
|
||||
}
|
||||
</style>
|
||||
@@ -1,33 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {polycubes} from "./store";
|
||||
import CubeInput from "./CubeInput.svelte";
|
||||
import Polycube3D from "./Polycube3D.svelte";
|
||||
$: numCubes = $polycubes.length;
|
||||
</script>
|
||||
|
||||
<div class="input-container">
|
||||
{#each {length: numCubes} as _, cubeNo}
|
||||
<div class="cube-input">
|
||||
<CubeInput
|
||||
cubeNo={cubeNo}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="threedee">
|
||||
<Polycube3D/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.threedee {
|
||||
text-align: center;
|
||||
}
|
||||
.cube-input {
|
||||
}
|
||||
.input-container {
|
||||
padding: 1em;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,30 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {solutions, activeSolution, showingSolution} from "./store";
|
||||
|
||||
function selectSolution(i: number) {
|
||||
activeSolution.set(i);
|
||||
showingSolution.set(true);
|
||||
}
|
||||
</script>
|
||||
|
||||
<h3>Solutions: {$solutions.length}</h3>
|
||||
<ul>
|
||||
{#each $solutions as soln, i}
|
||||
<li class:active={$activeSolution === i} on:click={() => selectSolution(i)}>
|
||||
Solution #{i + 1}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
li:hover:not(.active) {
|
||||
background-color: #666666;
|
||||
}
|
||||
li {
|
||||
transition: background-color 200ms;
|
||||
cursor: pointer;
|
||||
}
|
||||
.active {
|
||||
background-color: #ff3e00;
|
||||
}
|
||||
</style>
|
||||
@@ -18,7 +18,7 @@ export default class SomaSolution {
|
||||
const uniqueSolns = [solutions[0]];
|
||||
for (const solution of solutions) {
|
||||
let foundMatch = false;
|
||||
for (const rotation of solution.getUniqueRotations()) {
|
||||
for (const rotation of solution.getRotations()) {
|
||||
let end = uniqueSolns.length;
|
||||
for (let i = 0; i < end; i++) {
|
||||
if (rotation.matches(uniqueSolns[i])) {
|
||||
@@ -33,7 +33,7 @@ export default class SomaSolution {
|
||||
return uniqueSolns;
|
||||
}
|
||||
|
||||
getUniqueRotations(): SomaSolution[] {
|
||||
getRotations(): SomaSolution[] {
|
||||
if (this.solutionSpaces.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -110,4 +110,8 @@ export default class SomaSolution {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getPieces() {
|
||||
return this.solutionSpaces.slice();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
export type DimensionDef = [number, number, number];
|
||||
|
||||
const enum NeighbourDirection {
|
||||
POSX,
|
||||
POSY,
|
||||
POSZ,
|
||||
NEGX,
|
||||
NEGY,
|
||||
NEGZ,
|
||||
}
|
||||
|
||||
export default class VoxelSpace {
|
||||
private dims: DimensionDef;
|
||||
private length: number;
|
||||
@@ -299,4 +308,27 @@ export default class VoxelSpace {
|
||||
});
|
||||
return size;
|
||||
}
|
||||
|
||||
getDirectNeighbourProfile(x: number, y: number, z: number): number {
|
||||
let result = 0;
|
||||
if (x < this.dims[0] - 1 && this.at(x + 1, y, z)) {
|
||||
result += 1;
|
||||
}
|
||||
if (y < this.dims[1] - 1 && this.at(x, y + 1, z)) {
|
||||
result += 2;
|
||||
}
|
||||
if (z < this.dims[2] - 1 && this.at(x, y, z + 1)) {
|
||||
result += 4;
|
||||
}
|
||||
if (x > 0 && this.at(x - 1, y, z)) {
|
||||
result += 8;
|
||||
}
|
||||
if (y > 0 && this.at(x, y - 1, z)) {
|
||||
result += 16;
|
||||
}
|
||||
if (z > 0 && this.at(x, y, z - 1)) {
|
||||
result += 32;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import App from './App.svelte';
|
||||
import App from './ui/App.svelte';
|
||||
|
||||
const app = new App({
|
||||
target: document.body,
|
||||
|
||||
20
src/solver/asconfig.json
Normal file
20
src/solver/asconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"targets": {
|
||||
"debug": {
|
||||
"binaryFile": "build/untouched.wasm",
|
||||
"textFile": "build/untouched.wat",
|
||||
"sourceMap": true,
|
||||
"debug": true
|
||||
},
|
||||
"release": {
|
||||
"binaryFile": "../../public/solver/main.wasm",
|
||||
"textFile": "../../public/solver/main.wat",
|
||||
"sourceMap": false,
|
||||
"optimizeLevel": 3,
|
||||
"shrinkLevel": 1,
|
||||
"converge": true,
|
||||
"noAssert": true
|
||||
}
|
||||
},
|
||||
"options": {}
|
||||
}
|
||||
89
src/solver/assembly/SomaSolution.ts
Normal file
89
src/solver/assembly/SomaSolution.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import VoxelSpace from "./VoxelSpace";
|
||||
|
||||
export default class SomaSolution {
|
||||
private solutionSpaces: VoxelSpace[];
|
||||
private dim: i32;
|
||||
constructor(dim: i32) {
|
||||
if (dim < 0 || dim % 1 !== 0) {
|
||||
throw new Error("Dimension must be a whole positive integer!");
|
||||
}
|
||||
this.dim = dim;
|
||||
this.solutionSpaces = [];
|
||||
}
|
||||
|
||||
static filterUnique(solutions: SomaSolution[]): SomaSolution[] {
|
||||
const uniqueSolns = new Array<SomaSolution>();
|
||||
if (solutions.length == 0) {
|
||||
return uniqueSolns;
|
||||
}
|
||||
uniqueSolns.push(solutions[0]);
|
||||
for (let iSoln = 0; iSoln < solutions.length; iSoln++) {
|
||||
const rots = solutions[iSoln].getRotations();
|
||||
let foundMatch = false;
|
||||
for (let iRot = 0; iRot < rots.length; iRot++) {
|
||||
let end = uniqueSolns.length;
|
||||
for (let i = 0; i < end; i++) {
|
||||
if (rots[iRot].matches(uniqueSolns[i])) {
|
||||
foundMatch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!foundMatch) {
|
||||
uniqueSolns.push(solutions[iSoln]);
|
||||
}
|
||||
}
|
||||
return uniqueSolns;
|
||||
}
|
||||
|
||||
getRotations(): SomaSolution[] {
|
||||
const result: SomaSolution[] = new Array<SomaSolution>();
|
||||
if (this.solutionSpaces.length == 0) {
|
||||
return result;
|
||||
}
|
||||
const allRots: VoxelSpace[][] = this.solutionSpaces.map<VoxelSpace[]>(space => space.getAllRotations());
|
||||
for (let i = 0; i < allRots[0].length; i++) {
|
||||
const solnRot = new SomaSolution(this.dim);
|
||||
for (let j = 0; j < allRots.length; j++) {
|
||||
solnRot.addSpace(allRots[j][i]);
|
||||
}
|
||||
result.push(solnRot);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
matches(solution: SomaSolution): boolean {
|
||||
for (let i = 0; i < this.solutionSpaces.length; i++) {
|
||||
if (!this.solutionSpaces[i].matches(solution.solutionSpaces[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
addSpace(space: VoxelSpace): void {
|
||||
this.solutionSpaces.push(space);
|
||||
}
|
||||
|
||||
at(x: i32, y: i32, z: i32): i32 {
|
||||
for (let i = 0; i < this.solutionSpaces.length; i++) {
|
||||
if (this.solutionSpaces[i].at(x, y, z)) {
|
||||
return this.solutionSpaces[i].getId();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
clone(): SomaSolution {
|
||||
const clone = new SomaSolution(this.dim);
|
||||
clone.solutionSpaces = this.solutionSpaces.slice(0, this.solutionSpaces.length);
|
||||
return clone;
|
||||
}
|
||||
|
||||
getDims(): i32[] {
|
||||
return [this.dim, this.dim, this.dim];
|
||||
}
|
||||
|
||||
getPieces(): VoxelSpace[] {
|
||||
return this.solutionSpaces;
|
||||
}
|
||||
}
|
||||
58
src/solver/assembly/SomaSolver.ts
Normal file
58
src/solver/assembly/SomaSolver.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import VoxelSpace from "./VoxelSpace";
|
||||
import SomaSolution from "./SomaSolution";
|
||||
|
||||
export default class SomaSolver {
|
||||
private solutionCube: VoxelSpace;
|
||||
private dim: i32;
|
||||
private solutions: SomaSolution[] = new Array<SomaSolution>();
|
||||
private iterations: i32 = 0;
|
||||
constructor(dimension: i32) {
|
||||
if (dimension % 1 !== 0 || dimension < 0) {
|
||||
throw new Error("The argument 'dimension' must be a positive whole number");
|
||||
}
|
||||
this.dim = dimension;
|
||||
this.solutionCube = new VoxelSpace(0, dimension, dimension, dimension, 0);
|
||||
}
|
||||
|
||||
solve(polycubes: VoxelSpace[]): void {
|
||||
if (polycubes.length === 0) {
|
||||
throw new Error("You must pass at least one polycube to solve the puzzle.");
|
||||
}
|
||||
let cumulativeSize = polycubes.reduce((prev, curr) => prev + curr.size(), 0);
|
||||
if (cumulativeSize !== this.dim**3) {
|
||||
throw new Error(`The polycubes passed do not add up to exactly enough units to form a cube of dimension ${this.dim}! Got: ${cumulativeSize}, need: ${this.dim**3}`);
|
||||
}
|
||||
this.solutions.splice(0, this.solutions.length);
|
||||
const combosWithRots: VoxelSpace[][] = new Array<Array<VoxelSpace>>();
|
||||
for (let i = 1; i < polycubes.length; i++) {
|
||||
combosWithRots.push(polycubes[i].getAllPermutationsInCubeOfSize(this.dim));
|
||||
}
|
||||
let combos: VoxelSpace[][] = new Array<Array<VoxelSpace>>();
|
||||
combos.push(polycubes[0].getAllPositionsInCube(this.dim));
|
||||
combos = combos.concat(combosWithRots);
|
||||
this.backtrackSolve(this.solutionCube, combos, new SomaSolution(this.dim));
|
||||
this.solutions = SomaSolution.filterUnique(this.solutions);
|
||||
}
|
||||
|
||||
getSolutions(): SomaSolution[] {
|
||||
return this.solutions.slice(0, this.solutions.length);
|
||||
}
|
||||
|
||||
private backtrackSolve(workingSolution: VoxelSpace, polycubes: VoxelSpace[][], currentSoln: SomaSolution, depth: i32 = 0): void {
|
||||
const nextCubeGroup = polycubes[0];
|
||||
for (let i = 0; i < nextCubeGroup.length; i++) {
|
||||
const fusionAttempt = workingSolution.plus(nextCubeGroup[i]);
|
||||
if (fusionAttempt) {
|
||||
const nextSoln = currentSoln.clone();
|
||||
nextSoln.addSpace(nextCubeGroup[i]);
|
||||
if (polycubes.length == 1) {
|
||||
this.solutions.push(nextSoln);
|
||||
currentSoln = new SomaSolution(this.dim);
|
||||
return;
|
||||
} else {
|
||||
this.backtrackSolve(fusionAttempt, polycubes.slice(1), nextSoln, depth + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
327
src/solver/assembly/VoxelSpace.ts
Normal file
327
src/solver/assembly/VoxelSpace.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
class Extrema {
|
||||
constructor(
|
||||
public xMax: i32,
|
||||
public xMin: i32,
|
||||
public yMax: i32,
|
||||
public yMin: i32,
|
||||
public zMax: i32,
|
||||
public zMin: i32,
|
||||
) {}
|
||||
}
|
||||
|
||||
export default class VoxelSpace {
|
||||
private length: i32;
|
||||
private space: i64;
|
||||
private id: i32;
|
||||
private dimx: i32;
|
||||
private dimy: i32;
|
||||
private dimz: i32;
|
||||
|
||||
constructor(id: i32, dimx: i32, dimy: i32, dimz: i32, space: i64 = 0, cullEmpty: boolean = false) {
|
||||
if (!space) {
|
||||
space = 0;
|
||||
}
|
||||
this.id = id;
|
||||
this.length = dimx * dimy * dimz;
|
||||
this.dimx = dimx;
|
||||
this.dimy = dimy;
|
||||
this.dimz = dimz;
|
||||
this.space = space;
|
||||
if (cullEmpty) {
|
||||
this.cullEmptySpace();
|
||||
}
|
||||
}
|
||||
|
||||
getExtrema(): Extrema {
|
||||
const extrema = new Extrema(
|
||||
0,
|
||||
i32.MAX_VALUE,
|
||||
0,
|
||||
i32.MAX_VALUE,
|
||||
0,
|
||||
i32.MAX_VALUE,
|
||||
);
|
||||
for (let x = 0; x < this.dimx; x++) {
|
||||
for (let y = 0; y < this.dimy; y++) {
|
||||
for (let z = 0; z < this.dimz; z++) {
|
||||
const val = this.at(x, y, z);
|
||||
if (val) {
|
||||
extrema.xMax = Math.max(extrema.xMax, x) as i32;
|
||||
extrema.xMin = Math.min(extrema.xMin, x) as i32;
|
||||
extrema.yMax = Math.max(extrema.yMax, y) as i32;
|
||||
extrema.yMin = Math.min(extrema.yMin, y) as i32;
|
||||
extrema.zMax = Math.max(extrema.zMax, z) as i32;
|
||||
extrema.zMin = Math.min(extrema.zMin, z) as i32;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return extrema;
|
||||
}
|
||||
|
||||
private cullEmptySpace(): void {
|
||||
const extrema = this.getExtrema();
|
||||
let index = 0;
|
||||
let newSpace = 0;
|
||||
for (let x = extrema.xMin; x <= extrema.xMax; x++) {
|
||||
for (let y = extrema.yMin; y <= extrema.yMax; y++) {
|
||||
for (let z = extrema.zMin; z <= extrema.zMax; z++) {
|
||||
if (this.at(x, y, z)) {
|
||||
newSpace |= 1 << index;
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.dimx = extrema.xMax - extrema.xMin + 1;
|
||||
this.dimy = extrema.yMax - extrema.yMin + 1;
|
||||
this.dimz = extrema.zMax - extrema.zMin + 1;
|
||||
this.space = newSpace;
|
||||
}
|
||||
|
||||
getId(): i32 {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
getUniqueRotations(): VoxelSpace[] {
|
||||
const rotations: VoxelSpace[] = new Array<VoxelSpace>();
|
||||
const refSpace = this.clone();
|
||||
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getXAxisSpins());
|
||||
refSpace.rot90Y();
|
||||
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getXAxisSpins());
|
||||
refSpace.rot90Y();
|
||||
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getXAxisSpins());
|
||||
refSpace.rot90Y();
|
||||
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getXAxisSpins());
|
||||
refSpace.rot90Z();
|
||||
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getXAxisSpins());
|
||||
refSpace.rot90Z();
|
||||
refSpace.rot90Z();
|
||||
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getXAxisSpins());
|
||||
return rotations;
|
||||
}
|
||||
|
||||
getAllRotations(): VoxelSpace[] {
|
||||
let rotations: VoxelSpace[] = new Array<VoxelSpace>();
|
||||
const refSpace = this.clone();
|
||||
rotations = rotations.concat(refSpace.getXAxisSpins());
|
||||
refSpace.rot90Y();
|
||||
rotations = rotations.concat(refSpace.getXAxisSpins());
|
||||
refSpace.rot90Y();
|
||||
rotations = rotations.concat(refSpace.getXAxisSpins());
|
||||
refSpace.rot90Y();
|
||||
rotations = rotations.concat(refSpace.getXAxisSpins());
|
||||
refSpace.rot90Z();
|
||||
rotations = rotations.concat(refSpace.getXAxisSpins());
|
||||
refSpace.rot90Z();
|
||||
refSpace.rot90Z();
|
||||
rotations = rotations.concat(refSpace.getXAxisSpins());
|
||||
return rotations;
|
||||
}
|
||||
|
||||
protected static pushNewUniqueSpaces(existingSpaces: VoxelSpace[], newSpaces: VoxelSpace[]): void {
|
||||
for (let iNew = 0; iNew < newSpaces.length; iNew++) {
|
||||
let matchFound = false;
|
||||
for (let iExisting = 0; iExisting < existingSpaces.length; iExisting++) {
|
||||
if (newSpaces[iNew].matches(existingSpaces[iExisting])) {
|
||||
matchFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matchFound) {
|
||||
existingSpaces.push(newSpaces[iNew]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getAllPositionsInCube(cubeDim: i32): VoxelSpace[] {
|
||||
if ((cubeDim > 0) && (cubeDim % 1 == 0)) {
|
||||
const cubePositions: VoxelSpace[] = [];
|
||||
for (let x = 0; x < cubeDim - this.dimx + 1; x++) {
|
||||
for (let y = 0; y < cubeDim - this.dimy + 1; y++) {
|
||||
for (let z = 0; z < cubeDim - this.dimz + 1; z++) {
|
||||
const cubePos = new VoxelSpace(this.id, cubeDim, cubeDim, cubeDim);
|
||||
for (let rotX = 0; rotX < this.dimx; rotX++) {
|
||||
for (let rotY = 0; rotY < this.dimy; rotY++) {
|
||||
for (let rotZ = 0; rotZ < this.dimz; rotZ++) {
|
||||
cubePos.set(x + rotX, y + rotY, z + rotZ, this.at(rotX, rotY, rotZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
cubePositions.push(cubePos);
|
||||
}
|
||||
}
|
||||
}
|
||||
return cubePositions;
|
||||
} else {
|
||||
throw new Error("cubeDim must be a positive integer.");
|
||||
}
|
||||
}
|
||||
|
||||
matches(space: VoxelSpace): boolean {
|
||||
if (space.dimx !== this.dimx) {
|
||||
return false;
|
||||
}
|
||||
if (space.dimy !== this.dimy) {
|
||||
return false;
|
||||
}
|
||||
if (space.dimz !== this.dimz) {
|
||||
return false;
|
||||
}
|
||||
return this.space == space.getRaw();
|
||||
}
|
||||
|
||||
clone(): VoxelSpace {
|
||||
return new VoxelSpace(this.id, this.dimx, this.dimy, this.dimz, this.getRaw());
|
||||
}
|
||||
|
||||
private getXAxisSpins(): VoxelSpace[] {
|
||||
const rotations: Array<VoxelSpace> = new Array<VoxelSpace>();
|
||||
rotations.push(this.clone());
|
||||
for (let i = 0; i < 3; i++) {
|
||||
rotations.push(rotations[i].rotated90X());
|
||||
}
|
||||
return rotations;
|
||||
}
|
||||
|
||||
getRaw(): i64 {
|
||||
return this.space;
|
||||
}
|
||||
|
||||
// [1, 0, 0] [x] [ x]
|
||||
// [0, 0, -1] * [y] = [-z]
|
||||
// [0, 1, 0] [z] [ y]
|
||||
private newIndexRotX(x: i32, y: i32, z: i32): i32 {
|
||||
return this.dimz * this.dimy * x + this.dimy * (this.dimz - 1 - z) + y;
|
||||
}
|
||||
|
||||
// [ 0, 0, 1] [x] [ z]
|
||||
// [ 0, 1, 0] * [y] = [ y]
|
||||
// [-1, 0, 0] [z] [-x]
|
||||
private newIndexRotY(x: i32, y: i32, z: i32): i32 {
|
||||
return this.dimy * this.dimx * z + this.dimx * y + (this.dimx - 1 - x);
|
||||
}
|
||||
|
||||
// [0, -1, 0] [x] [-y]
|
||||
// [1, 0, 0] * [y] = [ x]
|
||||
// [0, 0, 1] [z] [ z]
|
||||
private newIndexRotZ(x: i32, y: i32, z: i32): i32 {
|
||||
return this.dimx * this.dimz * (this.dimy - 1 - y) + this.dimz * x + z;
|
||||
}
|
||||
|
||||
at(x: i32, y: i32, z: i32): boolean {
|
||||
const mask = 1 << (this.dimy * this.dimz * x + this.dimz * y + z);
|
||||
return (this.space & mask) !== 0;
|
||||
}
|
||||
|
||||
toggle(x: i32, y: i32, z: i32): void {
|
||||
const mask = 1 << this.dimy * this.dimz * x + this.dimz * y + z;
|
||||
this.space ^= mask;
|
||||
}
|
||||
|
||||
set(x: i32, y: i32, z: i32, val: boolean): void {
|
||||
const mask = 1 << this.dimy * this.dimz * x + this.dimz * y + z;
|
||||
if (val) {
|
||||
this.space |= mask;
|
||||
} else {
|
||||
this.space &= ~mask;
|
||||
}
|
||||
}
|
||||
|
||||
rotated90X(): VoxelSpace {
|
||||
let newSpace = 0;
|
||||
for (let x = 0; x < this.dimx; x++) {
|
||||
for (let y = 0; y < this.dimy; y++) {
|
||||
for (let z = 0; z < this.dimz; z++) {
|
||||
if (this.at(x, y, z)) {
|
||||
newSpace |= 1 << this.newIndexRotX(x, y, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new VoxelSpace(this.id, this.dimx, this.dimz, this.dimy, newSpace);
|
||||
}
|
||||
|
||||
rotated90Y(): VoxelSpace {
|
||||
let newSpace = 0;
|
||||
for (let x = 0; x < this.dimx; x++) {
|
||||
for (let y = 0; y < this.dimy; y++) {
|
||||
for (let z = 0; z < this.dimz; z++) {
|
||||
if (this.at(x, y, z)) {
|
||||
newSpace |= 1 << this.newIndexRotY(x, y, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new VoxelSpace(this.id, this.dimz, this.dimy, this.dimx, newSpace);
|
||||
}
|
||||
|
||||
rotated90Z(): VoxelSpace {
|
||||
let newSpace = 0;
|
||||
for (let x = 0; x < this.dimx; x++) {
|
||||
for (let y = 0; y < this.dimy; y++) {
|
||||
for (let z = 0; z < this.dimz; z++) {
|
||||
if (this.at(x, y, z)) {
|
||||
newSpace |= 1 << this.newIndexRotZ(x, y, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new VoxelSpace(this.id, this.dimy, this.dimx, this.dimz, newSpace);
|
||||
}
|
||||
|
||||
rot90X(): void {
|
||||
const rot = this.rotated90X();
|
||||
this.space = rot.getRaw();
|
||||
this.dimx = rot.dimx;
|
||||
this.dimy = rot.dimy;
|
||||
this.dimz = rot.dimz;
|
||||
}
|
||||
|
||||
rot90Y(): void {
|
||||
const rot = this.rotated90Y();
|
||||
this.space = rot.getRaw();
|
||||
this.dimx = rot.dimx;
|
||||
this.dimy = rot.dimy;
|
||||
this.dimz = rot.dimz;
|
||||
}
|
||||
|
||||
rot90Z(): void {
|
||||
const rot = this.rotated90Z();
|
||||
this.space = rot.getRaw();
|
||||
this.dimx = rot.dimx;
|
||||
this.dimy = rot.dimy;
|
||||
this.dimz = rot.dimz;
|
||||
}
|
||||
|
||||
plus(space: VoxelSpace): VoxelSpace | null {
|
||||
const otherSpace = space.getRaw();
|
||||
if ((this.space | otherSpace) == (this.space ^ otherSpace)) {
|
||||
return new VoxelSpace(this.id, this.dimx, this.dimy, this.dimz, otherSpace | this.space);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
size(): i32 {
|
||||
let size = 0;
|
||||
for (let x = 0; x < this.dimx; x++) {
|
||||
for (let y = 0; y < this.dimy; y++) {
|
||||
for (let z = 0; z < this.dimz; z++) {
|
||||
if (this.at(x, y, z)) {
|
||||
size++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
getAllPermutationsInCubeOfSize(dim: i32): VoxelSpace[] {
|
||||
const rotations = this.getUniqueRotations();
|
||||
let result = new Array<VoxelSpace>();
|
||||
for (let i = 0; i < rotations.length; i++) {
|
||||
result = result.concat(rotations[i].getAllPositionsInCube(dim));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
22
src/solver/assembly/index.ts
Normal file
22
src/solver/assembly/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import SomaSolver from "./SomaSolver";
|
||||
import VoxelSpace from "./VoxelSpace";
|
||||
|
||||
|
||||
export function solve(polycubes: Array<i64>, dim: i32): Int64Array[] {
|
||||
const solver = new SomaSolver(dim);
|
||||
const voxelSpaces = new Array<VoxelSpace>();
|
||||
for (let i = 0; i < polycubes.length; i++) {
|
||||
voxelSpaces.push(new VoxelSpace(i, dim, dim, dim, polycubes[i], true));
|
||||
}
|
||||
solver.solve(voxelSpaces);
|
||||
const solutions = solver.getSolutions();
|
||||
let output: Int64Array[] = new Array<Int64Array>();
|
||||
for (let i = 0; i < solutions.length; i++) {
|
||||
const pieces = solutions[i].getPieces();
|
||||
output.push(new Int64Array(pieces.length));
|
||||
for (let j = 0; j < pieces.length; j++) {
|
||||
output[i][j] = pieces[j].getRaw();
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
6
src/solver/assembly/tsconfig.json
Normal file
6
src/solver/assembly/tsconfig.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "assemblyscript/std/assembly.json",
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
13
src/solver/index.js
Normal file
13
src/solver/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const AsBind = require("as-bind/dist/as-bind.cjs.js");
|
||||
const fs = require("fs");
|
||||
const wasm = fs.readFileSync("./build/untouched.wasm");
|
||||
const asyncTask = async () => {
|
||||
const asBindInstance = await AsBind.instantiate(wasm);
|
||||
|
||||
// You can now use your wasm / as-bind instance!
|
||||
const response = asBindInstance.exports.solve(
|
||||
[16875584n, 16810176n, 65688n, 77952n, 12296n, 2109456n, 4184n], 3
|
||||
);
|
||||
console.log(response); // AsBind: Hello World!
|
||||
};
|
||||
asyncTask();
|
||||
@@ -1,12 +0,0 @@
|
||||
import SomaSolver from "./SomaSolver";
|
||||
import VoxelSpace from "./VoxelSpace";
|
||||
|
||||
type SolveStartMessageData = {polycubes: bigint[], dims: number};
|
||||
|
||||
self.addEventListener('message', (event) => {
|
||||
const {polycubes, dims} = event.data as SolveStartMessageData;
|
||||
const solver = new SomaSolver(event.data.dims);
|
||||
solver.solve(polycubes.map((cubeRep, i) => new VoxelSpace(i, [dims, dims, dims], cubeRep)));
|
||||
(self as unknown as Worker).postMessage(solver.getSolutions());
|
||||
});
|
||||
|
||||
60
src/solver/package-lock.json
generated
Normal file
60
src/solver/package-lock.json
generated
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"requires": true,
|
||||
"lockfileVersion": 1,
|
||||
"dependencies": {
|
||||
"@assemblyscript/loader": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.19.1.tgz",
|
||||
"integrity": "sha512-3vpqYxOY7o8SNj2riGNF3wSZsqWippWTs7YwyTPPyxvjbrT1ZJnMMoGm4HSpbZ0QmKphzsaM4trR+BtxLFynDA=="
|
||||
},
|
||||
"as-bind": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/as-bind/-/as-bind-0.7.1.tgz",
|
||||
"integrity": "sha512-x/tfZZcyObwvAohhVYaKqLSvroMHqop3l9gkUO5JM0bBEdhI3BWXjkG3DZIuWj0YFzhQ8OiWG3FCvQQq/5yB3A==",
|
||||
"requires": {
|
||||
"visitor-as": "^0.5.0"
|
||||
}
|
||||
},
|
||||
"assemblyscript": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/assemblyscript/-/assemblyscript-0.19.1.tgz",
|
||||
"integrity": "sha512-unWcmJsw5H0H2GrTf25GlDJCaNzAveeFYPH5XhP54m540+26KJIurTEHN+xf/EI3MdK7IhThpGCE+pNqiNuLmA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"binaryen": "101.0.0-nightly.20210527",
|
||||
"long": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"binaryen": {
|
||||
"version": "101.0.0-nightly.20210527",
|
||||
"resolved": "https://registry.npmjs.org/binaryen/-/binaryen-101.0.0-nightly.20210527.tgz",
|
||||
"integrity": "sha512-dbKentJwA6H0LfI+pRuzNNzAooJwYFNrg1L8rRw8j6rlfkU815ytNLO+uDzGNcltYehUa5ERZFJHPIdqX12n0w==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
|
||||
},
|
||||
"long": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
|
||||
"integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==",
|
||||
"dev": true
|
||||
},
|
||||
"ts-mixer": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-5.4.1.tgz",
|
||||
"integrity": "sha512-Zo9HgPCtNouDgJ+LGtrzVOjSg8+7WGQktIKLwAfaNrlOK1mWGlz1ejsAF/YqUEqAGjUTeB5fEg8gH9Aui6w9xA=="
|
||||
},
|
||||
"visitor-as": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/visitor-as/-/visitor-as-0.5.0.tgz",
|
||||
"integrity": "sha512-U2P13pa7BAnfj6IEbP4feS1Rci6NT4GlDcwpqkk90u7LGalc5jH9aMuWnxTC8RJJ92iZzDQ8Lea5/OnLDsgzlw==",
|
||||
"requires": {
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"ts-mixer": "^5.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/solver/package.json
Normal file
15
src/solver/package.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"scripts": {
|
||||
"asbuild:untouched": "asc assembly/index.ts --target debug",
|
||||
"asbuild:optimized": "asc assembly/index.ts --target release",
|
||||
"asbuild": "npm run asbuild:untouched && npm run asbuild:optimized",
|
||||
"test": "node tests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@assemblyscript/loader": "^0.19.1",
|
||||
"as-bind": "^0.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"assemblyscript": "^0.19.1"
|
||||
}
|
||||
}
|
||||
12
src/store.ts
12
src/store.ts
@@ -1,6 +1,6 @@
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { get } from 'svelte/store';
|
||||
import type SomaSolution from "./solver/SomaSolution";
|
||||
import type SomaSolution from "./SomaSolution";
|
||||
|
||||
type PolycubeInput = {
|
||||
color: string,
|
||||
@@ -23,7 +23,7 @@ export const isMaxPolycubes = derived(
|
||||
([$polycubes, $somaDimension]: [PolycubeInput[], number]) => $polycubes.length >= $somaDimension ** 3);
|
||||
export const isMinPolycubes = derived(store.polycubes, ($polycubes: PolycubeInput[]) => $polycubes.length <= 1);
|
||||
export const solutions = writable([] as SomaSolution[]);
|
||||
export const activeSolution = writable(0);
|
||||
export const activeSolution = writable<number | null>(null);
|
||||
export const showingSolution = writable(false);
|
||||
|
||||
export const somaDimension = {
|
||||
@@ -43,6 +43,12 @@ export const somaDimension = {
|
||||
return dims - 1;
|
||||
});
|
||||
}
|
||||
},
|
||||
set(dims: number) {
|
||||
if (dims <= MAX_DIMS && dims >= MIN_DIMS) {
|
||||
polycubes.reset(dims);
|
||||
store.somaDimension.set(dims);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -107,5 +113,5 @@ function colorFromIndex(index: number) {
|
||||
let hue = spacing * (index % 6) + offset;
|
||||
const saturation = 100;
|
||||
const lightness = 1 / (2 + darknessCycle) * 100;
|
||||
return `hsl(${hue},${saturation}%,${lightness}%)`;
|
||||
return `hsl(${hue},${saturation}%,${Math.round(lightness)}%)`;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Sidebar from "./Sidebar.svelte";
|
||||
import SolutionInteractor from "./SolutionInteractor.svelte";
|
||||
import SolutionInteractor from "./Interactor.svelte";
|
||||
</script>
|
||||
|
||||
<main>
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import {somaDimension, polycubes, selectedCube, showingSolution} from "./store";
|
||||
import {somaDimension, polycubes, selectedCube, showingSolution} from "../store";
|
||||
export let cubeNo: number;
|
||||
|
||||
$: dimension = $somaDimension;
|
||||
$: cube = $polycubes[cubeNo];
|
||||
$: cubeColor = cube.color;
|
||||
$: currentlyVisualised = $selectedCube === cubeNo;
|
||||
$: currentlyVisualised = $selectedCube === cubeNo && !$showingSolution;
|
||||
let cellStartDragInitialVal: boolean = false;
|
||||
let cellStartDrag: number = 0;
|
||||
let cellDragStartPos: {x: number, y: number} = {x: 0, y: 0};
|
||||
@@ -24,6 +24,7 @@
|
||||
function onMouseOverCell(event: MouseEvent, x: number, y: number, z: number) {
|
||||
if (event.buttons !== 0) {
|
||||
polycubes.set(cubeNo, event.buttons === 1, x, y, z);
|
||||
selectedCube.set(cubeNo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,10 +75,10 @@
|
||||
{#each {length: dimension} as _, z}
|
||||
<div
|
||||
class="cell"
|
||||
class:filled={at(cube.rep, x, y, z)}
|
||||
on:mousemove={(event) => onMouseOverCell(event, x, y, z)}
|
||||
on:mousedown={(event) => onMouseDownCell(event, x, y, z)}
|
||||
on:mouseup={(event) => onMouseUpCell(event, x, y, z)}
|
||||
class:filled={at(cube.rep, z, dimension-1-x, y)}
|
||||
on:mousemove={(event) => onMouseOverCell(event, z, dimension-1-x, y)}
|
||||
on:mousedown={(event) => onMouseDownCell(event, z, dimension-1-x, y)}
|
||||
on:mouseup={(event) => onMouseUpCell(event, z, dimension-1-x, y)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -90,8 +91,8 @@
|
||||
* {
|
||||
--cell-size: 30px;
|
||||
}
|
||||
.active {
|
||||
border: 1px solid red;
|
||||
.cube.active {
|
||||
border: 3px solid #ff3e00;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1em;
|
||||
@@ -103,6 +104,7 @@
|
||||
}
|
||||
.cube {
|
||||
border-radius: 1em;
|
||||
border: 3px solid transparent;
|
||||
background-color: #666666;
|
||||
cursor: pointer;
|
||||
transition: transform 200ms;
|
||||
53
src/ui/Interactor.svelte
Normal file
53
src/ui/Interactor.svelte
Normal file
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import {polycubes} from "../store";
|
||||
import CubeInput from "./CubeInput.svelte";
|
||||
import SolutionViewer from "./SolutionViewer.svelte";
|
||||
$: numCubes = $polycubes.length;
|
||||
</script>
|
||||
|
||||
<div class="viewport">
|
||||
<div class="input-container">
|
||||
{#each {length: numCubes} as _, cubeNo}
|
||||
<div class="cube-input">
|
||||
<div class="padder">
|
||||
<CubeInput
|
||||
cubeNo={cubeNo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="threedee">
|
||||
<SolutionViewer/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.threedee {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
.padder {
|
||||
padding: 1em;
|
||||
}
|
||||
.cube-input {
|
||||
margin: auto;
|
||||
}
|
||||
.input-container {
|
||||
flex: 0 1 fit-content;
|
||||
overflow-x: scroll;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
.viewport {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
196
src/ui/Sidebar.svelte
Normal file
196
src/ui/Sidebar.svelte
Normal file
@@ -0,0 +1,196 @@
|
||||
<script lang="ts">
|
||||
import {isMaxPolycubes, isMinPolycubes, somaDimension, polycubes, solutions} from "../store";
|
||||
import SomaSolution from "../SomaSolution";
|
||||
import SolutionList from "./SolutionList.svelte";
|
||||
import VoxelSpace from "../VoxelSpace";
|
||||
|
||||
$: numCubes = $polycubes.length;
|
||||
$: cubes = $polycubes;
|
||||
let noEmpties: boolean;
|
||||
let enoughSubcubes: boolean;
|
||||
let readyToSolve: boolean;
|
||||
let size: number;
|
||||
$: {
|
||||
const dim = $somaDimension as number;
|
||||
const polycubes: VoxelSpace[] = cubes.map(cubeInput => new VoxelSpace(0, [dim, dim, dim], cubeInput.rep));
|
||||
size = polycubes.reduce((prev, cube) => cube.size() + prev, 0);
|
||||
noEmpties = polycubes.reduce((prev, cube) => (cube.size() !== 0) && prev, true);
|
||||
enoughSubcubes = size === dim**3;
|
||||
readyToSolve = size === dim**3 && noEmpties;
|
||||
}
|
||||
let solving = false;
|
||||
const worker = new Worker('../solver/main.js', {type: "module"});
|
||||
|
||||
async function respondWasm(event: MessageEvent) {
|
||||
const dim = $somaDimension as number;
|
||||
solutions.set(event.data.map((wasmSolution) => {
|
||||
const solnObj = new SomaSolution(dim);
|
||||
const spaceReps = wasmSolution.split(",");
|
||||
for (let i = 0; i < spaceReps.length; i++) {
|
||||
solnObj.addSpace(new VoxelSpace(i, [dim, dim, dim], BigInt(parseInt(spaceReps[i]))));
|
||||
}
|
||||
return solnObj;
|
||||
}));
|
||||
solving = false;
|
||||
}
|
||||
|
||||
function respondJs(event: MessageEvent) {
|
||||
solutions.set(event.data.map(solnData => {
|
||||
const solution = new SomaSolution(solnData.dim);
|
||||
solnData.solutionSpaces.forEach((voxelSpace, i) => solution.addSpace(new VoxelSpace(i, [solnData.dim, solnData.dim, solnData.dim], voxelSpace.space)));
|
||||
return solution;
|
||||
}));
|
||||
solving = false;
|
||||
}
|
||||
|
||||
function solveJs() {
|
||||
worker.onmessage = (e) => respondJs(e);
|
||||
const polycubes = cubes.map(cubeInput => cubeInput.rep);
|
||||
solving = true;
|
||||
worker.postMessage({type: 'js', polycubes, dims: $somaDimension});
|
||||
}
|
||||
|
||||
function solveWasm() {
|
||||
worker.onmessage = (e) => respondWasm(e);
|
||||
const polycubes = cubes.map(cubeInput => cubeInput.rep);
|
||||
console.log(polycubes);
|
||||
solving = true;
|
||||
worker.postMessage({type: 'wasm', polycubes, dims: $somaDimension});
|
||||
}
|
||||
|
||||
function genTooltip() {
|
||||
let messages = [];
|
||||
if (!enoughSubcubes) {
|
||||
messages.push(`You have not input enough subcubes to form a cube with a side length of ${$somaDimension}. Needed: ${$somaDimension**3}, current: ${size}.`);
|
||||
}
|
||||
if (!noEmpties) {
|
||||
messages.push("You have left some of the polycube inputs empty. Remove them to solve.");
|
||||
}
|
||||
return messages.join("\n");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<h1>Somaesque</h1>
|
||||
<div class="widgets">
|
||||
<div class="option">
|
||||
<p>Dimension:</p>
|
||||
<div class="choice">
|
||||
<button
|
||||
class:selected={$somaDimension === 2}
|
||||
on:click={() => somaDimension.set(2)}
|
||||
disabled={$somaDimension === 2}>
|
||||
2
|
||||
</button>
|
||||
<button
|
||||
class:selected={$somaDimension === 3}
|
||||
on:click={() => somaDimension.set(3)}
|
||||
disabled={$somaDimension === 3}>
|
||||
3
|
||||
</button>
|
||||
<button
|
||||
class:selected={$somaDimension === 4}
|
||||
on:click={() => somaDimension.set(4)}
|
||||
disabled={$somaDimension === 4}>
|
||||
4
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p>Cubes:</p>
|
||||
<div class="choice">
|
||||
<p>{numCubes}</p>
|
||||
<button on:click={polycubes.removeCube} disabled={$isMinPolycubes}>-</button>
|
||||
<button on:click={polycubes.addCube} disabled={$isMaxPolycubes}>+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<button
|
||||
class="solve"
|
||||
on:click={solveWasm}
|
||||
title="{genTooltip(enoughSubcubes, noEmpties, size)}"
|
||||
disabled="{solving || !readyToSolve}">
|
||||
{solving ? "Solving..." : "Solve!"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Solutions: {$solutions.length}</h3>
|
||||
<SolutionList/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
p {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
.choice {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 1em;
|
||||
}
|
||||
button {
|
||||
display: inline-block;
|
||||
background-color: #999999;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
border-style: none;
|
||||
}
|
||||
.selected:disabled {
|
||||
color: white;
|
||||
background-color: #ff3e00;
|
||||
}
|
||||
button:hover:not(:disabled) {
|
||||
cursor: pointer;
|
||||
background-color: #c1c1c1;
|
||||
}
|
||||
button:disabled {
|
||||
color: #a7a7a7;
|
||||
background-color: #616161;
|
||||
}
|
||||
button.solve {
|
||||
width: auto;
|
||||
color: white;
|
||||
background-color: #ff3e00;
|
||||
font-size: 2em;
|
||||
border-radius: 0.5em;
|
||||
border-style: none;
|
||||
margin: 0;
|
||||
}
|
||||
button.solve:disabled {
|
||||
width: auto;
|
||||
color: #999999;
|
||||
background-color: #a36754;
|
||||
font-size: 2em;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: #333333;
|
||||
padding: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
.widgets {
|
||||
width: 100%;
|
||||
}
|
||||
.widgets:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
.widgets:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.widgets > * {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
color: #ff3e00;
|
||||
font-size: 3em;
|
||||
font-weight: 100;
|
||||
}
|
||||
</style>
|
||||
91
src/ui/Solution2D.svelte
Normal file
91
src/ui/Solution2D.svelte
Normal file
@@ -0,0 +1,91 @@
|
||||
<script lang="ts">
|
||||
import {polycubes, activeSolution, showingSolution, solutions} from "../store";
|
||||
import SomaSolution from "../SomaSolution";
|
||||
|
||||
$: solutionDisplayed = $solutions[$activeSolution];
|
||||
$: dimension = (solutionDisplayed && solutionDisplayed.getDims?.()[0]) ?? 3;
|
||||
|
||||
function colorAt(soln: SomaSolution, x: number, y: number, z: number) {
|
||||
return $polycubes[soln.at(z, dimension-1-x, y)].color;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $activeSolution !== null}
|
||||
<div
|
||||
class="cube"
|
||||
class:active={$showingSolution}
|
||||
style="--dimension: {dimension};"
|
||||
on:click={() => showingSolution.set(true)}
|
||||
>
|
||||
<h1>Solution #{$activeSolution + 1}</h1>
|
||||
<div class="center">
|
||||
{#each {length: dimension} as _, x}
|
||||
<div class="layer">
|
||||
{#each {length: dimension} as _, y}
|
||||
<div class="row">
|
||||
{#each {length: dimension} as _, z}
|
||||
<div
|
||||
class="cell"
|
||||
style="background-color:{colorAt(solutionDisplayed, x, y, z)}; border-color: {colorAt(solutionDisplayed, x, y, z)}"
|
||||
class:filled={true}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
* {
|
||||
--cell-size: 30px;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
.cube.active {
|
||||
border: 3px solid #ff3e00;
|
||||
}
|
||||
.cube:hover:not(.active) {
|
||||
transform: scale(1.03);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
.cube {
|
||||
border-radius: 1em;
|
||||
border: 3px solid transparent;
|
||||
background-color: #666666;
|
||||
cursor: pointer;
|
||||
transition: transform 200ms;
|
||||
padding: 1em 2em 1em 2em;
|
||||
user-select: none;
|
||||
}
|
||||
.cell {
|
||||
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
border: 1px var(--color) solid;
|
||||
border-radius: 4px;
|
||||
height: var(--cell-size);
|
||||
width: var(--cell-size);
|
||||
background-color: #aaaaaa;
|
||||
margin: 1px;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
.layer {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.filled {
|
||||
background: var(--color);
|
||||
}
|
||||
</style>
|
||||
44
src/ui/SolutionList.svelte
Normal file
44
src/ui/SolutionList.svelte
Normal file
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import {solutions, activeSolution, showingSolution} from "../store";
|
||||
|
||||
function selectSolution(i: number) {
|
||||
activeSolution.set(i);
|
||||
showingSolution.set(true);
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
{#if $solutions.length === 0}
|
||||
<li>No solutions yet...</li>
|
||||
{/if}
|
||||
{#each $solutions as soln, i}
|
||||
<li class:active={$activeSolution === i} on:click={() => selectSolution(i)}>
|
||||
Solution #{i + 1}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
li:hover:not(.active) {
|
||||
background-color: #aaaaaa;
|
||||
}
|
||||
li {
|
||||
transition: background-color 100ms;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
height: 2em;
|
||||
line-height: 2em;
|
||||
}
|
||||
ul {
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
flex: 1;
|
||||
padding: 0.5em;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
background-color: #666;
|
||||
}
|
||||
.active {
|
||||
background-color: #ff3e00;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import PolycubeScene from "./PolycubeScene.ts";
|
||||
import PolycubeScene from "./threedee/PolycubeScene.ts";
|
||||
import {onMount} from "svelte";
|
||||
import {polycubes, somaDimension, selectedCube, solutions, activeSolution, showingSolution} from "./store";
|
||||
import {polycubes, somaDimension, selectedCube, solutions, activeSolution, showingSolution} from "../store";
|
||||
import Solution2D from "./Solution2D.svelte";
|
||||
|
||||
$: cube = $polycubes[$selectedCube];
|
||||
$: soln = $solutions[$activeSolution];
|
||||
@@ -18,22 +19,36 @@
|
||||
if ($showingSolution) {
|
||||
const colorMap = {};
|
||||
$polycubes.forEach((polycube, i) => colorMap[i] = polycube.color);
|
||||
threeTest?.setSolution(soln, colorMap);
|
||||
threeTest?.showSolution(soln, colorMap);
|
||||
} else {
|
||||
threeTest?.setPolycube(cube.rep, $somaDimension, cube.color);
|
||||
threeTest?.showPolycube(cube.rep, $somaDimension, cube.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<canvas
|
||||
bind:this={el}
|
||||
width="640"
|
||||
height="480"
|
||||
></canvas>
|
||||
<div class="top">
|
||||
<div class="soln2d-container">
|
||||
<Solution2D/>
|
||||
</div>
|
||||
<canvas
|
||||
bind:this={el}
|
||||
width="640"
|
||||
height="480"
|
||||
></canvas>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.top {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
}
|
||||
.soln2d-container {
|
||||
display: inline-block;
|
||||
}
|
||||
canvas {
|
||||
display: inline-block;
|
||||
border-radius: 1em;
|
||||
}
|
||||
</style>
|
||||
144
src/ui/threedee/GeometryManager.ts
Normal file
144
src/ui/threedee/GeometryManager.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import type * as THREE from "three";
|
||||
import {OBJLoader} from "three/examples/jsm/loaders/OBJLoader";
|
||||
|
||||
export enum SomaesqueGeometry {
|
||||
c000000 = 'c000000',
|
||||
c000001 = 'c000001',
|
||||
c000011 = 'c000011',
|
||||
c000111 = 'c000111',
|
||||
c001001 = 'c001001',
|
||||
c001011 = 'c001011',
|
||||
c001111 = 'c001111',
|
||||
c011011 = 'c011011',
|
||||
}
|
||||
|
||||
const MESH_ROT_MAP = [
|
||||
"000000", // 000000
|
||||
"000001", // 000001
|
||||
"000001z", // 000010
|
||||
"000011", // 000011
|
||||
"000001b", // 000100
|
||||
"000011x", // 000101
|
||||
"000011b", // 000110
|
||||
"000111", // 000111
|
||||
"000001yy", // 001000
|
||||
"001001", // 001001
|
||||
"000011yy", // 001010
|
||||
"001011", // 001011
|
||||
"000011zx", // 001100
|
||||
"001011x", // 001101
|
||||
"000111y", // 001110
|
||||
"001111", // 001111
|
||||
"000001ba", // 010000
|
||||
"000011xx", // 010001
|
||||
"001001ya", // 010010
|
||||
"001011zb", // 010011
|
||||
"000011yx", // 010100
|
||||
"000111x", // 010101
|
||||
"001011zb", // 010110
|
||||
"001111zb", // 010111
|
||||
"000011zz", // 011000
|
||||
"001011xx", // 011001
|
||||
"001011z", // 011010
|
||||
"011011", // 011011
|
||||
"000111yx", // 011100
|
||||
"001111x", // 011101
|
||||
"001111yx", // 011110
|
||||
"011011", // 011111
|
||||
"000001y", // 100000
|
||||
"000011a", // 100001
|
||||
"000011b", // 100010
|
||||
"000111b", // 100011
|
||||
"001001b", // 100100
|
||||
"001011yc", // 100101 //---
|
||||
"001011b", // 100110
|
||||
"001111b", // 100111
|
||||
"000011ccx",// 101000
|
||||
"001011a", // 101001
|
||||
"000111bb", // 101010
|
||||
"001111a", // 101011
|
||||
"001011yz", // 101100 //---
|
||||
"011011x", // 101101 //---
|
||||
"001111y", // 101110 //---
|
||||
"011011", // 101111
|
||||
"000011xz", // 110000 //---
|
||||
"000111ba", // 110001 //---
|
||||
"001011ba", // 110010 //---
|
||||
"001111ba", // 110011
|
||||
"001011bx", // 110100
|
||||
"001111bx", // 110101
|
||||
"011011b", // 110110
|
||||
"011011", // 110111
|
||||
"000111bba",// 111000
|
||||
"001111xx", // 111001
|
||||
"001111ya", // 111010
|
||||
"011011", // 111011
|
||||
"001111yz", // 111100
|
||||
"011011", // 111101
|
||||
"011011", // 111110
|
||||
"011011", // 111111
|
||||
];
|
||||
|
||||
const ROT_CODE_MAP = {
|
||||
x(mesh: THREE.Object3D) { mesh.rotateX(Math.PI/2); },
|
||||
y(mesh: THREE.Object3D) { mesh.rotateY(Math.PI/2); },
|
||||
z(mesh: THREE.Object3D) { mesh.rotateZ(Math.PI/2); },
|
||||
a(mesh: THREE.Object3D) { mesh.rotateX(-Math.PI/2); },
|
||||
b(mesh: THREE.Object3D) { mesh.rotateY(-Math.PI/2); },
|
||||
c(mesh: THREE.Object3D) { mesh.rotateZ(-Math.PI/2); },
|
||||
} as const;
|
||||
|
||||
type GeomRecord = Record<SomaesqueGeometry, THREE.BufferGeometry>;
|
||||
|
||||
export default class GeometryManager {
|
||||
private readonly root: string = "";
|
||||
private geometryRecord: GeomRecord = {} as GeomRecord;
|
||||
constructor(root: string, onReadyCb: (error?: string) => any) {
|
||||
this.root = root;
|
||||
Promise.allSettled(Object.keys(SomaesqueGeometry).map(geomId =>
|
||||
this.loadCubeGeometry(geomId as SomaesqueGeometry),
|
||||
)).then(() => onReadyCb()).catch((err) => onReadyCb(err));
|
||||
}
|
||||
|
||||
private async loadCubeGeometry(id: SomaesqueGeometry): Promise<THREE.BufferGeometry> {
|
||||
const onLoaded = (obj: THREE.Group, resolve: (geom: THREE.BufferGeometry) => any) => {
|
||||
const geom = (obj.children[0] as THREE.Mesh).geometry;
|
||||
this.geometryRecord[id] = geom;
|
||||
resolve(geom);
|
||||
};
|
||||
const load = (resolve: (geom: THREE.BufferGeometry) => any, reject: (err: string) => any) => {
|
||||
const loader = new OBJLoader();
|
||||
loader.load(
|
||||
`${this.root}${id}.obj`,
|
||||
obj => onLoaded(obj, resolve),
|
||||
() => {},
|
||||
(err) => reject(`Error loading OBJ file: ${err}`),
|
||||
);
|
||||
};
|
||||
return new Promise(load);
|
||||
}
|
||||
|
||||
retrieve(geometry: SomaesqueGeometry) {
|
||||
let requestedGeom = this.geometryRecord[geometry];
|
||||
if (requestedGeom) {
|
||||
return requestedGeom;
|
||||
} else {
|
||||
throw new Error(`Geometry with id: ${geometry} does not exist!`);
|
||||
}
|
||||
}
|
||||
|
||||
retrieveCubeGeometry(neighbourProfile: number) {
|
||||
return this.geometryRecord.c000000;
|
||||
// let requestedGeom = this.geometryRecord[`c${MESH_ROT_MAP[neighbourProfile].substr(0, 6)}`];
|
||||
// const rotations = MESH_ROT_MAP[neighbourProfile].substr(6);
|
||||
// if (!requestedGeom) {
|
||||
// throw new Error(`No similar cube found for the neighbour profile: ${neighbourProfile}`)
|
||||
// } else if (rotations) {
|
||||
// requestedGeom = requestedGeom.clone();
|
||||
// for (let i = 0; i < rotations.length; i++) {
|
||||
// ROT_CODE_MAP[rotations[i]](requestedGeom);
|
||||
// }
|
||||
// }
|
||||
// return requestedGeom;
|
||||
}
|
||||
}
|
||||
91
src/ui/threedee/PolycubeMesh.ts
Normal file
91
src/ui/threedee/PolycubeMesh.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import * as THREE from "three";
|
||||
import type VoxelSpace from "../../VoxelSpace";
|
||||
import type GeometryManager from "./GeometryManager";
|
||||
|
||||
export default class PolycubeMesh {
|
||||
private static geometryManager: GeometryManager;
|
||||
private group: THREE.Group;
|
||||
private meshes: THREE.Mesh[] = [];
|
||||
private currentPolycube: bigint = 0n;
|
||||
private material: THREE.MeshPhongMaterial;
|
||||
private numActiveCubes: number = 0;
|
||||
private flyDirection: THREE.Vector3 = new THREE.Vector3();
|
||||
|
||||
constructor(polycube: VoxelSpace, color: string) {
|
||||
this.material = new THREE.MeshPhongMaterial({color: 'red', shininess: 100, reflectivity: 100});
|
||||
this.group = new THREE.Group();
|
||||
this.swapColor(color);
|
||||
this.swapPolycube(polycube);
|
||||
}
|
||||
|
||||
static setManager(manager: GeometryManager) {
|
||||
PolycubeMesh.geometryManager = manager;
|
||||
}
|
||||
|
||||
swapColor(color: string) {
|
||||
this.material.color.set(color);
|
||||
}
|
||||
|
||||
swapPolycube(polycube: VoxelSpace) {
|
||||
if (polycube.getRaw() === this.currentPolycube) {
|
||||
return;
|
||||
}
|
||||
this.numActiveCubes = polycube.size();
|
||||
this.meshes = [];
|
||||
this.group.clear();
|
||||
this.group.position.set(0, 0, 0);
|
||||
polycube.forEachCell((val: boolean, x: number, y: number, z: number) => {
|
||||
if (val) {
|
||||
this.addCube(polycube, x, y, z);
|
||||
}
|
||||
});
|
||||
this.currentPolycube = polycube.getRaw();
|
||||
this.flyDirection = this.middlePosOfGroup().normalize();
|
||||
}
|
||||
|
||||
private addCube(refPolycube: VoxelSpace, x: number, y: number, z: number) {
|
||||
const dims = refPolycube.getDims();
|
||||
const neighbourProfile = refPolycube.getDirectNeighbourProfile(x, y, z);
|
||||
const mesh = new THREE.Mesh(
|
||||
PolycubeMesh.geometryManager.retrieveCubeGeometry(neighbourProfile),
|
||||
this.material
|
||||
);
|
||||
mesh.position.set(
|
||||
-((dims[0] - 1)/2) + x,
|
||||
-((dims[1] - 1)/2) + y,
|
||||
-((dims[2] - 1)/2) + z,
|
||||
);
|
||||
this.meshes.push(mesh);
|
||||
this.group.add(mesh);
|
||||
}
|
||||
|
||||
center() {
|
||||
const mid = this.middlePosOfGroup();
|
||||
this.group.children.forEach(child => child.position.sub(mid));
|
||||
}
|
||||
|
||||
private middlePosOfGroup() {
|
||||
return this.group.children.reduce(
|
||||
(prev, child) => prev.add(child.position),
|
||||
new THREE.Vector3()
|
||||
).divideScalar(this.group.children.length);
|
||||
}
|
||||
|
||||
flyBy(factor: number) {
|
||||
const movementVector = this.flyDirection.clone().multiplyScalar(factor);
|
||||
const targetPos = this.group.position.clone().add(movementVector);
|
||||
const willMoveBehindStartingPosition = targetPos.clone().sub(this.flyDirection).dot(this.flyDirection) < -1;
|
||||
if (!willMoveBehindStartingPosition) {
|
||||
const distanceFromOrigin = targetPos.distanceTo(new THREE.Vector3());
|
||||
if (distanceFromOrigin >= 0 && distanceFromOrigin < 3) {
|
||||
this.group.position.add(movementVector);
|
||||
}
|
||||
} else {
|
||||
this.group.position.set(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
asObj3D(): THREE.Object3D {
|
||||
return this.group;
|
||||
}
|
||||
}
|
||||
87
src/ui/threedee/PolycubeScene.ts
Normal file
87
src/ui/threedee/PolycubeScene.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import * as THREE from 'three';
|
||||
import type SomaSolution from "../../SomaSolution";
|
||||
import RotationControl from "./RotationControl";
|
||||
import PolycubeMesh from "./PolycubeMesh";
|
||||
import VoxelSpace, {DimensionDef} from "../../VoxelSpace";
|
||||
import GeometryManager from "./GeometryManager";
|
||||
|
||||
export default class PolycubeScene {
|
||||
private renderer: THREE.WebGLRenderer;
|
||||
private camera: THREE.Camera;
|
||||
private mainScene: THREE.Scene;
|
||||
private polycubeMeshes: PolycubeMesh[] = [];
|
||||
private controls: RotationControl;
|
||||
private light: THREE.Light;
|
||||
private cubeScene: THREE.Scene;
|
||||
private geomManager: GeometryManager;
|
||||
|
||||
constructor(el: HTMLCanvasElement, onReady: () => any, onError: (err: Error) => any) {
|
||||
this.init(el).then(onReady).catch(onError);
|
||||
}
|
||||
|
||||
private async init(el: HTMLCanvasElement) {
|
||||
this.renderer = new THREE.WebGLRenderer({canvas: el, antialias: true});
|
||||
this.setupCamera(el.clientWidth / el.clientHeight);
|
||||
this.setupLight();
|
||||
this.mainScene = new THREE.Scene();
|
||||
this.cubeScene = new THREE.Scene();
|
||||
this.mainScene.add(this.cubeScene, this.camera, this.light);
|
||||
this.cubeScene.rotateX(Math.PI/4);
|
||||
this.cubeScene.rotateY(Math.PI/4);
|
||||
this.controls = new RotationControl(this.cubeScene, this.polycubeMeshes, this.camera, el);
|
||||
this.geomManager = await new GeometryManager('../resources/', () => {
|
||||
requestAnimationFrame((timestamp) => this.render(timestamp));
|
||||
});
|
||||
PolycubeMesh.setManager(this.geomManager);
|
||||
}
|
||||
|
||||
private setupCamera(aspect: number) {
|
||||
const fov = 60;
|
||||
const near = 0.1;
|
||||
const far = 15;
|
||||
this.camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
|
||||
this.camera.position.z = 6;
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
}
|
||||
|
||||
private showPolycube(polycube: bigint, dims: number, color: string) {
|
||||
this.controls.disableFly();
|
||||
const voxelSpace = new VoxelSpace(0, [dims, dims, dims], polycube, true);
|
||||
this.clearScene();
|
||||
this.addPolycube(voxelSpace, color);
|
||||
this.polycubeMeshes[0].center();
|
||||
}
|
||||
|
||||
private showSolution(solution: SomaSolution, colorMap: Record<number, string>) {
|
||||
this.controls.enableFly();
|
||||
this.clearScene();
|
||||
const pieces = solution.getPieces();
|
||||
for (let i = 0; i < pieces.length; i++) {
|
||||
this.addPolycube(pieces[i], colorMap[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private clearScene() {
|
||||
this.polycubeMeshes.splice(0, this.polycubeMeshes.length);
|
||||
this.cubeScene.clear();
|
||||
}
|
||||
|
||||
private addPolycube(voxelSpace: VoxelSpace, color: string) {
|
||||
const newMesh = new PolycubeMesh(voxelSpace, color);
|
||||
this.polycubeMeshes.push(newMesh);
|
||||
this.cubeScene.add(newMesh.asObj3D());
|
||||
}
|
||||
|
||||
private setupLight() {
|
||||
const color = 0xFFFFFF;
|
||||
const intensity = 1;
|
||||
this.light = new THREE.DirectionalLight(color, intensity);
|
||||
this.light.position.set(4, 6, 24);
|
||||
this.light.lookAt(0,0,0);
|
||||
}
|
||||
|
||||
private render(time: number) {
|
||||
this.renderer.render(this.mainScene, this.camera);
|
||||
requestAnimationFrame((time: number) => this.render(time));
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,26 @@
|
||||
import type * as THREE from 'three';
|
||||
|
||||
interface Fliable {
|
||||
flyBy(factor: number);
|
||||
}
|
||||
|
||||
export default class RotationControls {
|
||||
private static ROTATION_FACTOR = 1/200;
|
||||
private object: THREE.Object3D;
|
||||
private element: HTMLCanvasElement;
|
||||
private respondToMovement: boolean = false;
|
||||
private dragging: boolean = false;
|
||||
private flyingEnabled = true;
|
||||
private lastX: number = 0;
|
||||
private lastY: number = 0;
|
||||
private yAxis: THREE.Vector3;
|
||||
private xAxis: THREE.Vector3;
|
||||
private start: THREE.Euler;
|
||||
private fliables: Fliable[];
|
||||
private hovered: boolean = false;
|
||||
|
||||
constructor(object: THREE.Object3D, camera: THREE.Camera, element: HTMLCanvasElement) {
|
||||
constructor(object: THREE.Object3D, fliables: Fliable[], camera: THREE.Camera, element: HTMLCanvasElement) {
|
||||
this.object = object;
|
||||
this.fliables = fliables;
|
||||
this.element = element;
|
||||
this.yAxis = object.worldToLocal(camera.up);
|
||||
this.xAxis = object.position.sub(camera.position);
|
||||
@@ -20,26 +28,50 @@ export default class RotationControls {
|
||||
this.xAxis = this.xAxis.clone().cross(this.yAxis.clone());
|
||||
this.start = this.object.rotation.clone();
|
||||
|
||||
this.element.addEventListener('mouseover', () => this.hovered = true);
|
||||
this.element.addEventListener('mouseout', () => this.hovered = false);
|
||||
this.element.addEventListener('wheel', (ev) => this.handleScroll(ev));
|
||||
this.element.addEventListener('mousedown', (event) => {
|
||||
if (event.button === 1) {
|
||||
this.object.setRotationFromEuler(this.start);
|
||||
}
|
||||
if (!this.respondToMovement) {
|
||||
if (!this.dragging) {
|
||||
this.lastX = event.x;
|
||||
this.lastY = event.y;
|
||||
this.respondToMovement = true;
|
||||
this.dragging = true;
|
||||
}
|
||||
});
|
||||
window.addEventListener('mousemove', (ev) => this.handleMove(ev));
|
||||
window.addEventListener('mouseup', () => this.respondToMovement = false);
|
||||
window.addEventListener('mouseup', () => this.dragging = false);
|
||||
}
|
||||
|
||||
private handleMove(event: MouseEvent) {
|
||||
if (this.respondToMovement) {
|
||||
if (this.dragging) {
|
||||
const xDiff = event.movementX * RotationControls.ROTATION_FACTOR;
|
||||
const yDiff = event.movementY * RotationControls.ROTATION_FACTOR;
|
||||
this.object.rotateOnAxis(this.yAxis, xDiff);
|
||||
this.object.rotateOnWorldAxis(this.xAxis, yDiff);
|
||||
}
|
||||
}
|
||||
|
||||
private handleScroll(event: WheelEvent) {
|
||||
if (this.flyingEnabled && this.hovered) {
|
||||
for (const fliable of this.fliables) {
|
||||
const direction = event.deltaY / Math.abs(event.deltaY);
|
||||
fliable.flyBy(direction / 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enableFly() {
|
||||
this.flyingEnabled = true;
|
||||
}
|
||||
|
||||
disableFly() {
|
||||
this.flyingEnabled = false;
|
||||
}
|
||||
|
||||
private static isMesh(object: THREE.Object3D): object is THREE.Mesh {
|
||||
return (object as THREE.Mesh).isMesh;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user