great stuff

This commit is contained in:
Daniel Ledda
2021-06-08 17:29:29 +02:00
parent e7b8ae6120
commit c8f37d0d98
65 changed files with 3309 additions and 3331 deletions

View File

@@ -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 };

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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
View 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": {}
}

View 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;
}
}

View 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);
}
}
}
}
}

View 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;
}
}

View 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;
}

View File

@@ -0,0 +1,6 @@
{
"extends": "assemblyscript/std/assembly.json",
"include": [
"./**/*.ts"
]
}

13
src/solver/index.js Normal file
View 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();

View File

@@ -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
View 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
View 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"
}
}

View File

@@ -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)}%)`;
}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import Sidebar from "./Sidebar.svelte";
import SolutionInteractor from "./SolutionInteractor.svelte";
import SolutionInteractor from "./Interactor.svelte";
</script>
<main>

View File

@@ -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
View 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
View 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
View 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>

View 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>

View File

@@ -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>

View 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;
}
}

View 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;
}
}

View 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));
}
}

View File

@@ -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;
}
}