It all works....

This commit is contained in:
Daniel Ledda
2021-07-03 20:00:13 +02:00
parent c8f37d0d98
commit c950631b5e
45 changed files with 10537 additions and 903 deletions

View File

@@ -1,13 +1,16 @@
import type VoxelSpace from "./VoxelSpace";
import type VoxelSpaceBoolean from "./VoxelSpaceBoolean";
import type VoxelSpaceBigInt from "./VoxelSpaceBigInt";
export default class SomaSolution {
private solutionSpaces: VoxelSpace[];
private dim: number;
constructor(dim: number) {
if (dim < 0 || dim % 1 !== 0) {
throw new Error("Dimension must be a whole positive integer!");
}
this.dim = dim;
private solutionSpaces: (VoxelSpaceBoolean | VoxelSpaceBigInt)[];
private dimX: number;
private dimY: number;
private dimZ: number;
constructor(dimX: number, dimY: number, dimZ: number) {
this.dimX = dimX;
this.dimY = dimY;
this.dimZ = dimZ;
this.solutionSpaces = [];
}
@@ -40,7 +43,7 @@ export default class SomaSolution {
const result: SomaSolution[] = [];
const allRots = this.solutionSpaces.map(space => space.getAllRotations());
for (let i = 0; i < allRots[0].length; i++) {
const solnRot = new SomaSolution(this.dim);
const solnRot = new SomaSolution(this.dimX, this.dimY, this.dimZ);
allRots.forEach(rotGroup => solnRot.addSpace(rotGroup[i]));
result.push(solnRot);
}
@@ -56,16 +59,16 @@ export default class SomaSolution {
return true;
}
addSpace(space: VoxelSpace) {
addSpace(space: VoxelSpaceBoolean | VoxelSpaceBigInt) {
this.solutionSpaces.push(space);
}
print() {
let accum = "";
console.log("---");
for (let x = 0; x < this.dim; x++) {
for (let y = 0; y < this.dim; y++) {
for (let z = 0; z < this.dim; z++) {
for (let x = 0; x < this.dimX; x++) {
for (let y = 0; y < this.dimY; y++) {
for (let z = 0; z < this.dimZ; z++) {
for (const space of this.solutionSpaces) {
if (space.at(x, y, z)) {
accum += space.getId();
@@ -75,7 +78,7 @@ export default class SomaSolution {
console.log(accum);
accum = "";
}
if (x !== this.dim - 1) {
if (x !== this.dimX - 1) {
console.log("-");
}
}
@@ -92,19 +95,19 @@ export default class SomaSolution {
}
clone() {
const clone = new SomaSolution(this.dim);
const clone = new SomaSolution(this.dimX, this.dimY, this.dimZ);
clone.solutionSpaces = this.solutionSpaces.slice();
return clone;
}
getDims() {
return [this.dim, this.dim, this.dim];
return [this.dimX, this.dimY, this.dimZ];
}
forEachCell(cb: (val: number, x: number, y: number, z: number) => any) {
loopStart: for (let x = 0; x < this.dim; x++) {
for (let y = 0; y < this.dim; y++) {
for (let z = 0; z < this.dim; z++) {
loopStart: for (let x = 0; x < this.dimX; x++) {
for (let y = 0; y < this.dimY; y++) {
for (let z = 0; z < this.dimZ; z++) {
cb(this.at(x, y, z), x, y, z);
}
}

View File

@@ -1,53 +0,0 @@
import VoxelSpace from "./VoxelSpace";
import SomaSolution from "./SomaSolution";
export default class SomaSolver {
private solutionCube: VoxelSpace;
private dim: number;
private solutions: SomaSolution[] = [];
private iterations: number = 0;
constructor(dimension: number) {
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], Array(dimension**3).fill(0));
}
async solve(polycubes: VoxelSpace[]) {
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 = [];
const combosWithRots = polycubes.slice(1).map(polycube => polycube.getUniqueRotations().map((rot: VoxelSpace) => rot.getAllPositionsInCube(this.dim)).flat());
const combos = [polycubes[0].getAllPositionsInCube(this.dim), ...combosWithRots];
this.backtrackSolve(this.solutionCube, combos, new SomaSolution(this.dim));
this.solutions = SomaSolution.filterUnique(this.solutions);
}
getSolutions() {
return this.solutions.slice();
}
private backtrackSolve(workingSolution: VoxelSpace, polycubes: VoxelSpace[][], currentSoln: SomaSolution, depth = 0) {
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

@@ -1,3 +1,5 @@
import type VoxelSpaceBoolean from "./VoxelSpaceBoolean";
export type DimensionDef = [number, number, number];
const enum NeighbourDirection {
@@ -9,30 +11,28 @@ const enum NeighbourDirection {
NEGZ,
}
export default class VoxelSpace {
export default class VoxelSpaceBigInt {
private dims: DimensionDef;
private length: number;
private space: bigint;
private id: number;
constructor(id: number, dims: DimensionDef, space?: boolean[] | bigint, cullEmpty?: boolean) {
if (!space) {
space = 0n;
} else if (Array.isArray(space)) {
if (space.length !== dims[0] * dims[1] * dims[2]) {
throw new Error("Vals don't fit in given dimensions.");
}
space = VoxelSpace.boolArrayToBigInt(space)
private color: string;
constructor(options: {id: number, dims: DimensionDef, space?: bigint, cullEmpty: boolean, color?: string}) {
if (!options.space) {
options.space = 0n;
}
this.id = id;
this.length = dims[0] * dims[1] * dims[2];
this.dims = dims;
this.space = space;
if (cullEmpty) {
this.id = options.id;
this.length = options.dims[0] * options.dims[1] * options.dims[2];
this.dims = options.dims;
this.space = BigInt(options.space);
this.color = options.color ?? "red";
if (options.cullEmpty !== false) {
this.cullEmptySpace();
}
}
private static boolArrayToBigInt(boolArray: boolean[]): bigint {
static boolArrayToBigInt(boolArray: boolean[]) {
let result = 0n;
for (let i = 0; i < boolArray.length; i++) {
if (boolArray[i]) {
@@ -42,18 +42,26 @@ export default class VoxelSpace {
return result;
}
setColor(color: string) {
this.color = color;
}
getColor() {
return this.color;
}
binaryRep() {
return this.space.toString(2);
}
getExtrema() {
const extrema = {
xMax: -Infinity,
xMin: Infinity,
yMax: -Infinity,
yMin: Infinity,
zMax: -Infinity,
zMin: Infinity,
xMax: 0,
xMin: this.dims[0],
yMax: 0,
yMin: this.dims[1],
zMax: 0,
zMin: this.dims[2],
};
this.forEachCell((val, x, y, z) => {
if (val) {
@@ -68,7 +76,7 @@ export default class VoxelSpace {
return extrema;
}
private cullEmptySpace() {
cullEmptySpace() {
const extrema = this.getExtrema();
let index = 0n;
let newSpace = 0n;
@@ -123,25 +131,25 @@ export default class VoxelSpace {
}
getUniqueRotations() {
const rotations: VoxelSpace[] = [];
const rotations: VoxelSpaceBigInt[] = [];
const refSpace = this.clone();
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
VoxelSpaceBigInt.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('y');
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
VoxelSpaceBigInt.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('y');
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
VoxelSpaceBigInt.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('y');
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
VoxelSpaceBigInt.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('z');
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
VoxelSpaceBigInt.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('z');
refSpace.rot90('z');
VoxelSpace.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
VoxelSpaceBigInt.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
return rotations;
}
getAllRotations() {
const rotations: VoxelSpace[] = [];
const rotations: VoxelSpaceBigInt[] = [];
const refSpace = this.clone();
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('y');
@@ -158,7 +166,7 @@ export default class VoxelSpace {
return rotations;
}
protected static pushNewUniqueSpaces(existingSpaces: VoxelSpace[], newSpaces: VoxelSpace[]) {
protected static pushNewUniqueSpaces(existingSpaces: VoxelSpaceBigInt[], newSpaces: VoxelSpaceBigInt[]) {
for (const newSpace of newSpaces) {
let matchFound = false;
for (const existingSpace of existingSpaces) {
@@ -173,41 +181,44 @@ export default class VoxelSpace {
}
}
getAllPositionsInCube(cubeDim: number): VoxelSpace[] {
if ((cubeDim > 0) && (cubeDim % 1 === 0)) {
const cubePositions: VoxelSpace[] = [];
for (let x = 0; x < cubeDim - this.dims[0] + 1; x++) {
for (let y = 0; y < cubeDim - this.dims[1] + 1; y++) {
for (let z = 0; z < cubeDim - this.dims[2] + 1; z++) {
const cubePos = new VoxelSpace(this.id, [cubeDim, cubeDim, cubeDim]);
this.forEachCell((val, rotX, rotY, rotZ) => {
cubePos.set(x + rotX, y + rotY, z + rotZ, val);
});
cubePositions.push(cubePos);
}
getAllPositionsInPrism(cubeDimX: number, cubeDimY: number, cubeDimZ: number): VoxelSpaceBigInt[] {
const cubePositions: VoxelSpaceBigInt[] = [];
if (this.dims[0] > cubeDimX || this.dims[1] > cubeDimY || this.dims[2] > cubeDimZ) {
return cubePositions;
}
for (let xOffset = 0; xOffset < (cubeDimX - this.dims[0] + 1); xOffset++) {
for (let yOffset = 0; yOffset < (cubeDimY - this.dims[1] + 1); yOffset++) {
for (let zOffset = 0; zOffset < (cubeDimZ - this.dims[2] + 1); zOffset++) {
const cubePos = new VoxelSpaceBigInt({id: this.id, dims: [cubeDimX, cubeDimY, cubeDimZ], color: this.color, cullEmpty: false});
this.forEachCell((val, x, y, z) => {
cubePos.set(xOffset + x, yOffset + y, zOffset + z, val);
});
cubePositions.push(cubePos);
}
}
return cubePositions;
} else {
throw new Error("cubeDim must be a positive integer.");
}
return cubePositions;
}
matches(space: VoxelSpace) {
matches(space: VoxelSpaceBigInt | VoxelSpaceBoolean) {
const otherDims = space.getDims();
for (let i = 0; i < this.dims.length; i++) {
if (otherDims[i] !== this.dims[i]) {
return false;
}
}
return this.space === space.getRaw();
if (typeof space.getRaw() === "bigint") {
return this.space === space.getRaw();
} else {
return this.binaryRep() === space.binaryRep();
}
}
clone() {
return new VoxelSpace(this.id, this.getDims(), this.getRaw());
return new VoxelSpaceBigInt({id: this.id, dims: this.getDims(), space: this.getRaw(), color: this.getColor(), cullEmpty: false});
}
private getAxisSpins(axis: 'x' | 'y' | 'z'): VoxelSpace[] {
private getAxisSpins(axis: 'x' | 'y' | 'z'): VoxelSpaceBigInt[] {
const rotations = [this.clone()];
for (let i = 0; i < 3; i++) {
rotations.push(rotations[i].rotated90(axis));
@@ -250,30 +261,33 @@ export default class VoxelSpace {
}
toggle(x: number, y: number, z: number) {
const mask = BigInt(1 << this.dims[1] * this.dims[2] * x + this.dims[2] * y + z);
const mask = 1n << BigInt(this.dims[1] * this.dims[2] * x + this.dims[2] * y + z);
this.space ^= mask;
}
set(x: number, y: number, z: number, val: boolean) {
const mask = BigInt(1 << this.dims[1] * this.dims[2] * x + this.dims[2] * y + z);
const mask = 1n << BigInt(this.dims[1] * this.dims[2] * x + this.dims[2] * y + z);
if (val) {
this.space |= mask;
} else {
}
else {
this.space &= ~mask;
}
}
rotated90(dim: 'x' | 'y' | 'z') {
let newSpace = 0n;
let newDims: DimensionDef;
let rotIndex: (i: number, j: number, k: number) => number;
let newDims;
let rotIndex;
if (dim === 'x') {
newDims = [this.dims[0], this.dims[2], this.dims[1]];
rotIndex = this.newIndexRotX.bind(this);
} else if (dim === 'y') {
}
else if (dim === 'y') {
newDims = [this.dims[2], this.dims[1], this.dims[0]];
rotIndex = this.newIndexRotY.bind(this);
} else {
}
else {
newDims = [this.dims[1], this.dims[0], this.dims[2]];
rotIndex = this.newIndexRotZ.bind(this);
}
@@ -281,8 +295,8 @@ export default class VoxelSpace {
if (val) {
newSpace |= BigInt(1 << rotIndex(i, j, k));
}
})
return new VoxelSpace(this.id, newDims, newSpace);
});
return new VoxelSpaceBigInt({ id: this.id, dims: newDims, space: newSpace, color: this.color, cullEmpty: false });
}
rot90(dim: 'x' | 'y' | 'z') {
@@ -291,10 +305,10 @@ export default class VoxelSpace {
this.dims = rot.getDims();
}
plus(space: VoxelSpace): VoxelSpace | null {
plus(space: VoxelSpaceBigInt): VoxelSpaceBigInt | null {
const otherSpace = space.getRaw();
if ((this.space | otherSpace) === (this.space ^ otherSpace)) {
return new VoxelSpace(this.id, this.dims, otherSpace | this.space);
return new VoxelSpaceBigInt({ id: this.id, dims: this.getDims(), space: otherSpace | this.space, color: this.color, cullEmpty: false });
}
return null;
}
@@ -331,4 +345,13 @@ export default class VoxelSpace {
}
return result;
}
getAllPermutationsInPrism(prismDimX: number, prismDimY: number, prismDimZ: number): VoxelSpaceBigInt[] {
const rotations = this.getUniqueRotations();
let result = new Array<VoxelSpaceBigInt>();
for (let i = 0; i < rotations.length; i++) {
result = result.concat(rotations[i].getAllPositionsInPrism(prismDimX, prismDimY, prismDimZ));
}
return result;
}
}

366
src/VoxelSpaceBoolean.ts Normal file
View File

@@ -0,0 +1,366 @@
import type VoxelSpaceBigInt from "./VoxelSpaceBigInt";
export type DimensionDef = [number, number, number];
const enum NeighbourDirection {
POSX,
POSY,
POSZ,
NEGX,
NEGY,
NEGZ,
}
export default class VoxelSpaceBoolean {
private dims: DimensionDef;
private length: number;
private space: boolean[];
private id: number;
private color: string;
constructor(options: {id: number, dims: DimensionDef, space?: boolean[] | bigint, cullEmpty: boolean, color?: string}) {
this.length = options.dims[0] * options.dims[1] * options.dims[2];
if (!options.space) {
options.space = new Array<boolean>(options.dims[0] * options.dims[1] * options.dims[2]);
options.space.fill(false);
} else if (!Array.isArray(options.space)) {
const newSpace = [];
for (let i = 0; i < this.length; i++) {
const mask = 1n << BigInt(i);
newSpace.push((options.space & mask) !== 0n);
}
options.space = newSpace;
}
this.id = options.id;
this.dims = options.dims;
this.space = options.space;
this.color = options.color ?? "red";
if (options.cullEmpty !== false) {
this.cullEmptySpace();
}
}
setColor(color: string) {
this.color = color;
}
getColor() {
return this.color;
}
binaryRep() {
return this.space.reduce((prev, curr) => prev + (curr ? "1" : "0"), "");
}
getExtrema() {
const extrema = {
xMax: 0,
xMin: this.dims[0],
yMax: 0,
yMin: this.dims[1],
zMax: 0,
zMin: this.dims[2],
};
this.forEachCell((val, x, y, z) => {
if (val) {
extrema.xMax = Math.max(extrema.xMax, x);
extrema.xMin = Math.min(extrema.xMin, x);
extrema.yMax = Math.max(extrema.yMax, y);
extrema.yMin = Math.min(extrema.yMin, y);
extrema.zMax = Math.max(extrema.zMax, z);
extrema.zMin = Math.min(extrema.zMin, z);
}
});
return extrema;
}
cullEmptySpace() {
const extrema = this.getExtrema();
const newX = extrema.xMax - extrema.xMin + 1;
const newY = extrema.yMax - extrema.yMin + 1;
const newZ = extrema.zMax - extrema.zMin + 1;
const newSpace = new Array<boolean>(newX * newY * newZ);
newSpace.fill(false);
let index = 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[index] = true;
}
index++;
}
}
}
this.dims[0] = newX;
this.dims[1] = newY;
this.dims[2] = newZ;
this.space = newSpace;
}
forEachCell(cb: (val: boolean, x: number, y: number, z: number) => any) {
loopStart: for (let x = 0; x < this.dims[0]; x++) {
for (let y = 0; y < this.dims[1]; y++) {
for (let z = 0; z < this.dims[2]; z++) {
if (cb(this.at(x, y, z), x, y, z) === 0) {
break loopStart;
}
}
}
}
}
getId() {
return this.id;
}
print() {
let accum = "";
console.log("---");
for (let i = 0; i < this.dims[0]; i++) {
for (let j = 0; j < this.dims[1]; j++) {
for (let k = 0; k < this.dims[2]; k++) {
accum += this.at(i, j, k) ? '#' : 'O';
}
console.log(accum);
accum = "";
}
if (i !== this.dims[0] - 1) {
console.log("-");
}
}
console.log("---");
}
getUniqueRotations() {
const rotations: VoxelSpaceBoolean[] = [];
const refSpace = this.clone();
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('y');
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('y');
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('y');
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('z');
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('z');
refSpace.rot90('z');
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
return rotations;
}
getAllRotations() {
const rotations: VoxelSpaceBoolean[] = [];
const refSpace = this.clone();
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('y');
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('y');
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('y');
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('z');
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('z');
refSpace.rot90('z');
rotations.push(...refSpace.getAxisSpins('x'));
return rotations;
}
protected static pushNewUniqueSpaces(existingSpaces: VoxelSpaceBoolean[], newSpaces: VoxelSpaceBoolean[]) {
for (const newSpace of newSpaces) {
let matchFound = false;
for (const existingSpace of existingSpaces) {
if (newSpace.matches(existingSpace)) {
matchFound = true;
break;
}
}
if (!matchFound) {
existingSpaces.push(newSpace);
}
}
}
getAllPositionsInPrism(cubeDimX: number, cubeDimY: number, cubeDimZ: number): VoxelSpaceBoolean[] {
const cubePositions: VoxelSpaceBoolean[] = [];
if (this.dims[0] > cubeDimX || this.dims[1] > cubeDimY || this.dims[2] > cubeDimZ) {
return cubePositions;
}
for (let xOffset = 0; xOffset < (cubeDimX - this.dims[0] + 1); xOffset++) {
for (let yOffset = 0; yOffset < (cubeDimY - this.dims[1] + 1); yOffset++) {
for (let zOffset = 0; zOffset < (cubeDimZ - this.dims[2] + 1); zOffset++) {
const cubePos = new VoxelSpaceBoolean({id: this.id, dims: [cubeDimX, cubeDimY, cubeDimZ], color: this.color, cullEmpty: false});
this.forEachCell((val, x, y, z) => {
cubePos.set(xOffset + x, yOffset + y, zOffset + z, val);
});
cubePositions.push(cubePos);
}
}
}
return cubePositions;
}
matches(space: VoxelSpaceBoolean | VoxelSpaceBigInt) {
const otherDims = space.getDims();
for (let i = 0; i < this.dims.length; i++) {
if (otherDims[i] !== this.dims[i]) {
return false;
}
}
const otherRaw = space.getRaw();
if (typeof otherRaw === "bigint") {
return space.binaryRep() === this.binaryRep();
}
return this.space.reduce((prev, unit, i) => (unit === otherRaw[i]) && prev, true);
}
clone() {
return new VoxelSpaceBoolean({id: this.id, dims: this.getDims(), space: this.getRaw(), color: this.getColor(), cullEmpty: false});
}
private getAxisSpins(axis: 'x' | 'y' | 'z'): VoxelSpaceBoolean[] {
const rotations = [this.clone()];
for (let i = 0; i < 3; i++) {
rotations.push(rotations[i].rotated90(axis));
}
return rotations;
}
getDims(): DimensionDef {
return this.dims.slice() as DimensionDef;
}
getRaw() {
return this.space.slice();
}
// [1, 0, 0] [x] [ x]
// [0, 0, -1] * [y] = [-z]
// [0, 1, 0] [z] [ y]
private newIndexRotX(x: number, y: number, z: number) {
return this.dims[2] * this.dims[1] * x + this.dims[1] * (this.dims[2] - 1 - z) + y;
}
// [ 0, 0, 1] [x] [ z]
// [ 0, 1, 0] * [y] = [ y]
// [-1, 0, 0] [z] [-x]
private newIndexRotY(x: number, y: number, z: number) {
return this.dims[1] * this.dims[0] * z + this.dims[0] * y + (this.dims[0] - 1 - x);
}
// [0, -1, 0] [x] [-y]
// [1, 0, 0] * [y] = [ x]
// [0, 0, 1] [z] [ z]
private newIndexRotZ(x: number, y: number, z: number) {
return this.dims[0] * this.dims[2] * (this.dims[1] - 1 - y) + this.dims[2] * x + z;
}
at(x: number, y: number, z: number) {
return this.space[this.index(x, y, z)];
}
private index(x: number, y: number, z: number) {
return this.dims[1] * this.dims[2] * x + this.dims[2] * y + z;
}
toggle(x: number, y: number, z: number) {
const index = this.index(x, y, z);
this.space[index] = !this.space[index];
}
set(x: number, y: number, z: number, val: boolean) {
this.space[this.index(x, y, z)] = val;
}
rotated90(dim: 'x' | 'y' | 'z') {
const newSpace = new Array<boolean>(this.dims[0] * this.dims[1] * this.dims[2]);
newSpace.fill(false);
let newDims: DimensionDef;
let rotIndex: (i: number, j: number, k: number) => number;
if (dim === 'x') {
newDims = [this.dims[0], this.dims[2], this.dims[1]];
rotIndex = this.newIndexRotX.bind(this);
} else if (dim === 'y') {
newDims = [this.dims[2], this.dims[1], this.dims[0]];
rotIndex = this.newIndexRotY.bind(this);
} else {
newDims = [this.dims[1], this.dims[0], this.dims[2]];
rotIndex = this.newIndexRotZ.bind(this);
}
this.forEachCell((val, i, j, k) => {
if (val) {
newSpace[rotIndex(i, j, k)] = true;
}
})
return new VoxelSpaceBoolean({id: this.id, dims: newDims, space: newSpace, color: this.color, cullEmpty: false});
}
rot90(dim: 'x' | 'y' | 'z') {
const rot = this.rotated90(dim);
this.space = rot.getRaw();
this.dims = rot.getDims();
}
plus(space: VoxelSpaceBoolean): VoxelSpaceBoolean | null {
const newSpace = new Array<boolean>(this.dims[0] * this.dims[1] * this.dims[2]);
newSpace.fill(false);
let clash = false;
space.forEachCell((val, x, y, z) => {
if (this.at(x, y, z) !== val) {
newSpace[this.index(x, y, z)] = true;
} else {
if (val) {
clash = true;
}
}
});
if (!clash) {
return new VoxelSpaceBoolean({id: this.id, dims: this.getDims(), space: newSpace, color: this.color, cullEmpty: false});
}
return null;
}
size() {
let size = 0;
this.forEachCell((val) => {
if (val) {
size++;
}
});
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;
}
getAllPermutationsInPrism(prismDimX: number, prismDimY: number, prismDimZ: number): VoxelSpaceBoolean[] {
const rotations = this.getUniqueRotations();
let result = new Array<VoxelSpaceBoolean>();
for (let i = 0; i < rotations.length; i++) {
result = result.concat(rotations[i].getAllPositionsInPrism(prismDimX, prismDimY, prismDimZ));
}
return result;
}
}

0
src/desktop/build.js Normal file
View File

30
src/desktop/main.js Normal file
View File

@@ -0,0 +1,30 @@
const { app, BrowserWindow } = require('electron');
const path = require('path');
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
}
});
win.loadFile(path.join(__dirname, '../../public/index.html'));
}
app.whenReady().then(() => {
createWindow();
app.on('activate', function() {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
})
});
app.on('window-all-close', function() {
if (process.platform !== 'darwin') {
app.quit();
}
});

12
src/desktop/preload.js Normal file
View File

@@ -0,0 +1,12 @@
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector);
if (element) {
element.innerText = text;
}
}
for (const dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency]);
}
});

View File

@@ -0,0 +1,102 @@
export default class SomaSolution {
constructor(dimX, dimY, dimZ) {
this.dimX = dimX;
this.dimY = dimY;
this.dimZ = dimZ;
this.solutionSpaces = [];
}
static filterUnique(solutions) {
if (solutions.length === 0) {
return [];
}
const uniqueSolns = [solutions[0]];
for (const solution of solutions) {
let foundMatch = false;
for (const rotation of solution.getRotations()) {
let end = uniqueSolns.length;
for (let i = 0; i < end; i++) {
if (rotation.matches(uniqueSolns[i])) {
foundMatch = true;
}
}
}
if (!foundMatch) {
uniqueSolns.push(solution);
}
}
return uniqueSolns;
}
getRotations() {
if (this.solutionSpaces.length === 0) {
return [];
}
const result = [];
const allRots = this.solutionSpaces.map(space => space.getAllRotations());
for (let i = 0; i < allRots[0].length; i++) {
const solnRot = new SomaSolution(this.dimX, this.dimY, this.dimZ);
allRots.forEach(rotGroup => solnRot.addSpace(rotGroup[i]));
result.push(solnRot);
}
return result;
}
matches(solution) {
for (let i = 0; i < this.solutionSpaces.length; i++) {
if (!this.solutionSpaces[i].matches(solution.solutionSpaces[i])) {
return false;
}
}
return true;
}
addSpace(space) {
this.solutionSpaces.push(space);
}
print() {
let accum = "";
console.log("---");
for (let x = 0; x < this.dimX; x++) {
for (let y = 0; y < this.dimY; y++) {
for (let z = 0; z < this.dimZ; z++) {
for (const space of this.solutionSpaces) {
if (space.at(x, y, z)) {
accum += space.getId();
}
}
}
console.log(accum);
accum = "";
}
if (x !== this.dimX - 1) {
console.log("-");
}
}
console.log("---");
}
at(x, y, z) {
for (const space of this.solutionSpaces) {
if (space.at(x, y, z)) {
return space.getId();
}
}
return 0;
}
clone() {
const clone = new SomaSolution(this.dimX, this.dimY, this.dimZ);
clone.solutionSpaces = this.solutionSpaces.slice();
return clone;
}
getDims() {
return [this.dimX, this.dimY, this.dimZ];
}
forEachCell(cb) {
loopStart: for (let x = 0; x < this.dimX; x++) {
for (let y = 0; y < this.dimY; y++) {
for (let z = 0; z < this.dimZ; z++) {
cb(this.at(x, y, z), x, y, z);
}
}
}
}
getPieces() {
return this.solutionSpaces.slice();
}
}

View File

@@ -0,0 +1,119 @@
import type VoxelSpaceBoolean from "./VoxelSpaceBoolean";
export default class SomaSolution {
private solutionSpaces: VoxelSpaceBoolean[];
private dimX: number;
private dimY: number;
private dimZ: number;
constructor(dimX: number, dimY: number, dimZ: number) {
this.dimX = dimX;
this.dimY = dimY;
this.dimZ = dimZ;
this.solutionSpaces = [];
}
static filterUnique(solutions: SomaSolution[]): SomaSolution[] {
if (solutions.length === 0) {
return [];
}
const uniqueSolns = [solutions[0]];
for (const solution of solutions) {
let foundMatch = false;
for (const rotation of solution.getRotations()) {
let end = uniqueSolns.length;
for (let i = 0; i < end; i++) {
if (rotation.matches(uniqueSolns[i])) {
foundMatch = true;
}
}
}
if (!foundMatch) {
uniqueSolns.push(solution);
}
}
return uniqueSolns;
}
getRotations(): SomaSolution[] {
if (this.solutionSpaces.length === 0) {
return [];
}
const result: SomaSolution[] = [];
const allRots = this.solutionSpaces.map(space => space.getAllRotations());
for (let i = 0; i < allRots[0].length; i++) {
const solnRot = new SomaSolution(this.dimX, this.dimY, this.dimZ);
allRots.forEach(rotGroup => solnRot.addSpace(rotGroup[i]));
result.push(solnRot);
}
return result;
}
matches(solution: SomaSolution) {
for (let i = 0; i < this.solutionSpaces.length; i++) {
if (!this.solutionSpaces[i].matches(solution.solutionSpaces[i])) {
return false;
}
}
return true;
}
addSpace(space: VoxelSpaceBoolean) {
this.solutionSpaces.push(space);
}
print() {
let accum = "";
console.log("---");
for (let x = 0; x < this.dimX; x++) {
for (let y = 0; y < this.dimY; y++) {
for (let z = 0; z < this.dimZ; z++) {
for (const space of this.solutionSpaces) {
if (space.at(x, y, z)) {
accum += space.getId();
}
}
}
console.log(accum);
accum = "";
}
if (x !== this.dimX - 1) {
console.log("-");
}
}
console.log("---");
}
at(x: number, y: number, z: number) {
for (const space of this.solutionSpaces) {
if (space.at(x, y, z)) {
return space.getId();
}
}
return 0;
}
clone() {
const clone = new SomaSolution(this.dimX, this.dimY, this.dimZ);
clone.solutionSpaces = this.solutionSpaces.slice();
return clone;
}
getDims() {
return [this.dimX, this.dimY, this.dimZ];
}
forEachCell(cb: (val: number, x: number, y: number, z: number) => any) {
loopStart: for (let x = 0; x < this.dimX; x++) {
for (let y = 0; y < this.dimY; y++) {
for (let z = 0; z < this.dimZ; z++) {
cb(this.at(x, y, z), x, y, z);
}
}
}
}
getPieces() {
return this.solutionSpaces.slice();
}
}

View File

@@ -0,0 +1,60 @@
import VoxelSpaceBoolean from "./VoxelSpaceBoolean";
import SomaSolution from "./SomaSolution";
export default class SomaSolver {
constructor(dimX, dimY, dimZ) {
this.visualiser = { async showSoln(soln) { }, async showSpace(cube) { } };
this.solutions = new Array();
this.iterations = 0;
this.dimX = dimX;
this.dimY = dimY;
this.dimZ = dimZ;
this.solutionCube = new VoxelSpaceBoolean({ id: 0, dims: [dimX, dimY, dimZ], cullEmpty: false });
}
setDebug(visualiser) {
this.visualiser = visualiser;
}
async solve(polycubes) {
if (polycubes.length === 0) {
throw new Error("You must pass at least one polycube to solve the puzzle.");
}
this.solutions.splice(0, this.solutions.length);
const combosWithRots = new Array();
for (let i = 1; i < polycubes.length; i++) {
const rots = polycubes[i].getAllPermutationsInPrism(this.dimX, this.dimY, this.dimZ);
combosWithRots.push(rots);
}
let combos = new Array();
combos.push(polycubes[0].getAllPositionsInPrism(this.dimX, this.dimY, this.dimZ));
combos = combos.concat(combosWithRots);
for (const combo of combos) {
for (const rot of combo) {
await this.visualiser.showSpace(rot);
}
}
await this.backtrackSolve(this.solutionCube, combos, new SomaSolution(this.dimX, this.dimY, this.dimZ));
this.solutions = SomaSolution.filterUnique(this.solutions);
}
getSolutions() {
return this.solutions.slice();
}
async backtrackSolve(workingSolution, polycubes, currentSoln, depth = 0) {
const nextCubeGroup = polycubes[0];
for (let i = 0; i < nextCubeGroup.length; i++) {
const fusionAttempt = workingSolution.plus(nextCubeGroup[i]);
++this.iterations;
if (fusionAttempt) {
const nextSoln = currentSoln.clone();
nextSoln.addSpace(nextCubeGroup[i]);
await this.visualiser.showSoln(nextSoln);
if (polycubes.length === 1) {
this.solutions.push(nextSoln);
currentSoln = new SomaSolution(this.dimX, this.dimY, this.dimZ);
return;
}
else {
await this.backtrackSolve(fusionAttempt, polycubes.slice(1), nextSoln, depth + 1);
}
}
}
}
}

View File

@@ -0,0 +1,73 @@
import VoxelSpaceBoolean from "./VoxelSpaceBoolean";
import SomaSolution from "./SomaSolution";
interface DebugVisualiser {
showSoln(soln: SomaSolution): Promise<void>;
showSpace(cube: VoxelSpaceBoolean): Promise<void>;
}
export default class SomaSolver {
private solutionCube: VoxelSpaceBoolean;
private dimX: number;
private dimY: number;
private dimZ: number;
private visualiser: DebugVisualiser = {async showSoln(soln: SomaSolution) {}, async showSpace(cube: VoxelSpaceBoolean) {}};
private solutions: SomaSolution[] = new Array<SomaSolution>();
private iterations: number = 0;
constructor(dimX: number, dimY: number, dimZ: number) {
this.dimX = dimX;
this.dimY = dimY;
this.dimZ = dimZ;
this.solutionCube = new VoxelSpaceBoolean({id: 0, dims: [dimX, dimY, dimZ], cullEmpty: false});
}
setDebug(visualiser: DebugVisualiser) {
this.visualiser = visualiser;
}
async solve(polycubes: VoxelSpaceBoolean[]) {
if (polycubes.length === 0) {
throw new Error("You must pass at least one polycube to solve the puzzle.");
}
this.solutions.splice(0, this.solutions.length);
const combosWithRots: VoxelSpaceBoolean[][] = new Array<Array<VoxelSpaceBoolean>>();
for (let i = 1; i < polycubes.length; i++) {
const rots = polycubes[i].getAllPermutationsInPrism(this.dimX, this.dimY, this.dimZ);
combosWithRots.push(rots);
}
let combos: VoxelSpaceBoolean[][] = new Array<Array<VoxelSpaceBoolean>>();
combos.push(polycubes[0].getAllPositionsInPrism(this.dimX, this.dimY, this.dimZ));
combos = combos.concat(combosWithRots);
for (const combo of combos) {
for (const rot of combo) {
await this.visualiser.showSpace(rot);
}
}
await this.backtrackSolve(this.solutionCube, combos, new SomaSolution(this.dimX, this.dimY, this.dimZ));
this.solutions = SomaSolution.filterUnique(this.solutions);
}
getSolutions() {
return this.solutions.slice();
}
private async backtrackSolve(workingSolution: VoxelSpaceBoolean, polycubes: VoxelSpaceBoolean[][], currentSoln: SomaSolution, depth = 0) {
const nextCubeGroup = polycubes[0];
for (let i = 0; i < nextCubeGroup.length; i++) {
const fusionAttempt = workingSolution.plus(nextCubeGroup[i]);
++this.iterations;
if (fusionAttempt) {
const nextSoln = currentSoln.clone();
nextSoln.addSpace(nextCubeGroup[i]);
await this.visualiser.showSoln(nextSoln);
if (polycubes.length === 1) {
this.solutions.push(nextSoln);
currentSoln = new SomaSolution(this.dimX, this.dimY, this.dimZ);
return;
} else {
await this.backtrackSolve(fusionAttempt, polycubes.slice(1), nextSoln, depth + 1);
}
}
}
}
}

View File

@@ -0,0 +1,321 @@
export default class VoxelSpaceBoolean {
constructor(options) {
this.length = options.dims[0] * options.dims[1] * options.dims[2];
if (!options.space) {
options.space = new Array(options.dims[0] * options.dims[1] * options.dims[2]);
options.space.fill(false);
}
else if (!Array.isArray(options.space)) {
const newSpace = [];
for (let i = 0; i < this.length; i++) {
const mask = 1n << BigInt(i);
newSpace.push((options.space & mask) !== 0n);
}
options.space = newSpace;
}
this.id = options.id;
this.dims = options.dims;
this.space = options.space;
this.color = options.color ?? "red";
if (options.cullEmpty !== false) {
this.cullEmptySpace();
}
}
setColor(color) {
this.color = color;
}
getColor() {
return this.color;
}
binaryRep() {
return this.space.reduce((prev, curr) => prev + (curr ? "1" : "0"), "");
}
getExtrema() {
const extrema = {
xMax: 0,
xMin: this.dims[0],
yMax: 0,
yMin: this.dims[1],
zMax: 0,
zMin: this.dims[2],
};
this.forEachCell((val, x, y, z) => {
if (val) {
extrema.xMax = Math.max(extrema.xMax, x);
extrema.xMin = Math.min(extrema.xMin, x);
extrema.yMax = Math.max(extrema.yMax, y);
extrema.yMin = Math.min(extrema.yMin, y);
extrema.zMax = Math.max(extrema.zMax, z);
extrema.zMin = Math.min(extrema.zMin, z);
}
});
return extrema;
}
cullEmptySpace() {
const extrema = this.getExtrema();
const newX = extrema.xMax - extrema.xMin + 1;
const newY = extrema.yMax - extrema.yMin + 1;
const newZ = extrema.zMax - extrema.zMin + 1;
const newSpace = new Array(newX * newY * newZ);
newSpace.fill(false);
let index = 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[index] = true;
}
index++;
}
}
}
this.dims[0] = newX;
this.dims[1] = newY;
this.dims[2] = newZ;
this.space = newSpace;
}
forEachCell(cb) {
loopStart: for (let x = 0; x < this.dims[0]; x++) {
for (let y = 0; y < this.dims[1]; y++) {
for (let z = 0; z < this.dims[2]; z++) {
if (cb(this.at(x, y, z), x, y, z) === 0) {
break loopStart;
}
}
}
}
}
getId() {
return this.id;
}
print() {
let accum = "";
console.log("---");
for (let i = 0; i < this.dims[0]; i++) {
for (let j = 0; j < this.dims[1]; j++) {
for (let k = 0; k < this.dims[2]; k++) {
accum += this.at(i, j, k) ? '#' : 'O';
}
console.log(accum);
accum = "";
}
if (i !== this.dims[0] - 1) {
console.log("-");
}
}
console.log("---");
}
getUniqueRotations() {
const rotations = [];
const refSpace = this.clone();
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('y');
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('y');
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('y');
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('z');
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('z');
refSpace.rot90('z');
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
return rotations;
}
getAllRotations() {
const rotations = [];
const refSpace = this.clone();
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('y');
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('y');
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('y');
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('z');
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('z');
refSpace.rot90('z');
rotations.push(...refSpace.getAxisSpins('x'));
return rotations;
}
static pushNewUniqueSpaces(existingSpaces, newSpaces) {
for (const newSpace of newSpaces) {
let matchFound = false;
for (const existingSpace of existingSpaces) {
if (newSpace.matches(existingSpace)) {
matchFound = true;
break;
}
}
if (!matchFound) {
existingSpaces.push(newSpace);
}
}
}
getAllPositionsInPrism(cubeDimX, cubeDimY, cubeDimZ) {
const cubePositions = [];
if (this.dims[0] > cubeDimX || this.dims[1] > cubeDimY || this.dims[2] > cubeDimZ) {
return cubePositions;
}
for (let xOffset = 0; xOffset < (cubeDimX - this.dims[0] + 1); xOffset++) {
for (let yOffset = 0; yOffset < (cubeDimY - this.dims[1] + 1); yOffset++) {
for (let zOffset = 0; zOffset < (cubeDimZ - this.dims[2] + 1); zOffset++) {
const cubePos = new VoxelSpaceBoolean({ id: this.id, dims: [cubeDimX, cubeDimY, cubeDimZ], color: this.color, cullEmpty: false });
this.forEachCell((val, x, y, z) => {
cubePos.set(xOffset + x, yOffset + y, zOffset + z, val);
});
cubePositions.push(cubePos);
}
}
}
return cubePositions;
}
matches(space) {
const otherDims = space.getDims();
for (let i = 0; i < this.dims.length; i++) {
if (otherDims[i] !== this.dims[i]) {
return false;
}
}
const otherRaw = space.getRaw();
if (typeof otherRaw === "bigint") {
return space.binaryRep() === this.binaryRep();
}
return this.space.reduce((prev, unit, i) => (unit === otherRaw[i]) && prev, true);
}
clone() {
return new VoxelSpaceBoolean({ id: this.id, dims: this.getDims(), space: this.getRaw(), color: this.getColor(), cullEmpty: false });
}
getAxisSpins(axis) {
const rotations = [this.clone()];
for (let i = 0; i < 3; i++) {
rotations.push(rotations[i].rotated90(axis));
}
return rotations;
}
getDims() {
return this.dims.slice();
}
getRaw() {
return this.space.slice();
}
// [1, 0, 0] [x] [ x]
// [0, 0, -1] * [y] = [-z]
// [0, 1, 0] [z] [ y]
newIndexRotX(x, y, z) {
return this.dims[2] * this.dims[1] * x + this.dims[1] * (this.dims[2] - 1 - z) + y;
}
// [ 0, 0, 1] [x] [ z]
// [ 0, 1, 0] * [y] = [ y]
// [-1, 0, 0] [z] [-x]
newIndexRotY(x, y, z) {
return this.dims[1] * this.dims[0] * z + this.dims[0] * y + (this.dims[0] - 1 - x);
}
// [0, -1, 0] [x] [-y]
// [1, 0, 0] * [y] = [ x]
// [0, 0, 1] [z] [ z]
newIndexRotZ(x, y, z) {
return this.dims[0] * this.dims[2] * (this.dims[1] - 1 - y) + this.dims[2] * x + z;
}
at(x, y, z) {
return this.space[this.index(x, y, z)];
}
index(x, y, z) {
return this.dims[1] * this.dims[2] * x + this.dims[2] * y + z;
}
toggle(x, y, z) {
const index = this.index(x, y, z);
this.space[index] = !this.space[index];
}
set(x, y, z, val) {
this.space[this.index(x, y, z)] = val;
}
rotated90(dim) {
const newSpace = new Array(this.dims[0] * this.dims[1] * this.dims[2]);
newSpace.fill(false);
let newDims;
let rotIndex;
if (dim === 'x') {
newDims = [this.dims[0], this.dims[2], this.dims[1]];
rotIndex = this.newIndexRotX.bind(this);
}
else if (dim === 'y') {
newDims = [this.dims[2], this.dims[1], this.dims[0]];
rotIndex = this.newIndexRotY.bind(this);
}
else {
newDims = [this.dims[1], this.dims[0], this.dims[2]];
rotIndex = this.newIndexRotZ.bind(this);
}
this.forEachCell((val, i, j, k) => {
if (val) {
newSpace[rotIndex(i, j, k)] = true;
}
});
return new VoxelSpaceBoolean({ id: this.id, dims: newDims, space: newSpace, color: this.color, cullEmpty: false });
}
rot90(dim) {
const rot = this.rotated90(dim);
this.space = rot.getRaw();
this.dims = rot.getDims();
}
plus(space) {
const newSpace = new Array(this.dims[0] * this.dims[1] * this.dims[2]);
newSpace.fill(false);
let clash = false;
space.forEachCell((val, x, y, z) => {
if (this.at(x, y, z) !== val) {
newSpace[this.index(x, y, z)] = true;
}
else {
if (val) {
clash = true;
}
}
});
if (!clash) {
return new VoxelSpaceBoolean({ id: this.id, dims: this.getDims(), space: newSpace, color: this.color, cullEmpty: false });
}
return null;
}
size() {
let size = 0;
this.forEachCell((val) => {
if (val) {
size++;
}
});
return size;
}
getDirectNeighbourProfile(x, y, z) {
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;
}
getAllPermutationsInPrism(prismDimX, prismDimY, prismDimZ) {
const rotations = this.getUniqueRotations();
let result = new Array();
for (let i = 0; i < rotations.length; i++) {
result = result.concat(rotations[i].getAllPositionsInPrism(prismDimX, prismDimY, prismDimZ));
}
return result;
}
}

View File

@@ -0,0 +1,364 @@
export type DimensionDef = [number, number, number];
const enum NeighbourDirection {
POSX,
POSY,
POSZ,
NEGX,
NEGY,
NEGZ,
}
export default class VoxelSpaceBoolean {
private dims: DimensionDef;
private length: number;
private space: boolean[];
private id: number;
private color: string;
constructor(options: {id: number, dims: DimensionDef, space?: boolean[] | bigint, cullEmpty: boolean, color?: string}) {
this.length = options.dims[0] * options.dims[1] * options.dims[2];
if (!options.space) {
options.space = new Array<boolean>(options.dims[0] * options.dims[1] * options.dims[2]);
options.space.fill(false);
} else if (!Array.isArray(options.space)) {
const newSpace = [];
for (let i = 0; i < this.length; i++) {
const mask = 1n << BigInt(i);
newSpace.push((options.space & mask) !== 0n);
}
options.space = newSpace;
}
this.id = options.id;
this.dims = options.dims;
this.space = options.space;
this.color = options.color ?? "red";
if (options.cullEmpty !== false) {
this.cullEmptySpace();
}
}
setColor(color: string) {
this.color = color;
}
getColor() {
return this.color;
}
binaryRep() {
return this.space.reduce((prev, curr) => prev + (curr ? "1" : "0"), "");
}
getExtrema() {
const extrema = {
xMax: 0,
xMin: this.dims[0],
yMax: 0,
yMin: this.dims[1],
zMax: 0,
zMin: this.dims[2],
};
this.forEachCell((val, x, y, z) => {
if (val) {
extrema.xMax = Math.max(extrema.xMax, x);
extrema.xMin = Math.min(extrema.xMin, x);
extrema.yMax = Math.max(extrema.yMax, y);
extrema.yMin = Math.min(extrema.yMin, y);
extrema.zMax = Math.max(extrema.zMax, z);
extrema.zMin = Math.min(extrema.zMin, z);
}
});
return extrema;
}
cullEmptySpace() {
const extrema = this.getExtrema();
const newX = extrema.xMax - extrema.xMin + 1;
const newY = extrema.yMax - extrema.yMin + 1;
const newZ = extrema.zMax - extrema.zMin + 1;
const newSpace = new Array<boolean>(newX * newY * newZ);
newSpace.fill(false);
let index = 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[index] = true;
}
index++;
}
}
}
this.dims[0] = newX;
this.dims[1] = newY;
this.dims[2] = newZ;
this.space = newSpace;
}
forEachCell(cb: (val: boolean, x: number, y: number, z: number) => any) {
loopStart: for (let x = 0; x < this.dims[0]; x++) {
for (let y = 0; y < this.dims[1]; y++) {
for (let z = 0; z < this.dims[2]; z++) {
if (cb(this.at(x, y, z), x, y, z) === 0) {
break loopStart;
}
}
}
}
}
getId() {
return this.id;
}
print() {
let accum = "";
console.log("---");
for (let i = 0; i < this.dims[0]; i++) {
for (let j = 0; j < this.dims[1]; j++) {
for (let k = 0; k < this.dims[2]; k++) {
accum += this.at(i, j, k) ? '#' : 'O';
}
console.log(accum);
accum = "";
}
if (i !== this.dims[0] - 1) {
console.log("-");
}
}
console.log("---");
}
getUniqueRotations() {
const rotations: VoxelSpaceBoolean[] = [];
const refSpace = this.clone();
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('y');
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('y');
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('y');
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('z');
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
refSpace.rot90('z');
refSpace.rot90('z');
VoxelSpaceBoolean.pushNewUniqueSpaces(rotations, refSpace.getAxisSpins('x'));
return rotations;
}
getAllRotations() {
const rotations: VoxelSpaceBoolean[] = [];
const refSpace = this.clone();
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('y');
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('y');
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('y');
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('z');
rotations.push(...refSpace.getAxisSpins('x'));
refSpace.rot90('z');
refSpace.rot90('z');
rotations.push(...refSpace.getAxisSpins('x'));
return rotations;
}
protected static pushNewUniqueSpaces(existingSpaces: VoxelSpaceBoolean[], newSpaces: VoxelSpaceBoolean[]) {
for (const newSpace of newSpaces) {
let matchFound = false;
for (const existingSpace of existingSpaces) {
if (newSpace.matches(existingSpace)) {
matchFound = true;
break;
}
}
if (!matchFound) {
existingSpaces.push(newSpace);
}
}
}
getAllPositionsInPrism(cubeDimX: number, cubeDimY: number, cubeDimZ: number): VoxelSpaceBoolean[] {
const cubePositions: VoxelSpaceBoolean[] = [];
if (this.dims[0] > cubeDimX || this.dims[1] > cubeDimY || this.dims[2] > cubeDimZ) {
return cubePositions;
}
for (let xOffset = 0; xOffset < (cubeDimX - this.dims[0] + 1); xOffset++) {
for (let yOffset = 0; yOffset < (cubeDimY - this.dims[1] + 1); yOffset++) {
for (let zOffset = 0; zOffset < (cubeDimZ - this.dims[2] + 1); zOffset++) {
const cubePos = new VoxelSpaceBoolean({id: this.id, dims: [cubeDimX, cubeDimY, cubeDimZ], color: this.color, cullEmpty: false});
this.forEachCell((val, x, y, z) => {
cubePos.set(xOffset + x, yOffset + y, zOffset + z, val);
});
cubePositions.push(cubePos);
}
}
}
return cubePositions;
}
matches(space: VoxelSpaceBoolean) {
const otherDims = space.getDims();
for (let i = 0; i < this.dims.length; i++) {
if (otherDims[i] !== this.dims[i]) {
return false;
}
}
const otherRaw = space.getRaw();
if (typeof otherRaw === "bigint") {
return space.binaryRep() === this.binaryRep();
}
return this.space.reduce((prev, unit, i) => (unit === otherRaw[i]) && prev, true);
}
clone() {
return new VoxelSpaceBoolean({id: this.id, dims: this.getDims(), space: this.getRaw(), color: this.getColor(), cullEmpty: false});
}
private getAxisSpins(axis: 'x' | 'y' | 'z'): VoxelSpaceBoolean[] {
const rotations = [this.clone()];
for (let i = 0; i < 3; i++) {
rotations.push(rotations[i].rotated90(axis));
}
return rotations;
}
getDims(): DimensionDef {
return this.dims.slice() as DimensionDef;
}
getRaw() {
return this.space.slice();
}
// [1, 0, 0] [x] [ x]
// [0, 0, -1] * [y] = [-z]
// [0, 1, 0] [z] [ y]
private newIndexRotX(x: number, y: number, z: number) {
return this.dims[2] * this.dims[1] * x + this.dims[1] * (this.dims[2] - 1 - z) + y;
}
// [ 0, 0, 1] [x] [ z]
// [ 0, 1, 0] * [y] = [ y]
// [-1, 0, 0] [z] [-x]
private newIndexRotY(x: number, y: number, z: number) {
return this.dims[1] * this.dims[0] * z + this.dims[0] * y + (this.dims[0] - 1 - x);
}
// [0, -1, 0] [x] [-y]
// [1, 0, 0] * [y] = [ x]
// [0, 0, 1] [z] [ z]
private newIndexRotZ(x: number, y: number, z: number) {
return this.dims[0] * this.dims[2] * (this.dims[1] - 1 - y) + this.dims[2] * x + z;
}
at(x: number, y: number, z: number) {
return this.space[this.index(x, y, z)];
}
private index(x: number, y: number, z: number) {
return this.dims[1] * this.dims[2] * x + this.dims[2] * y + z;
}
toggle(x: number, y: number, z: number) {
const index = this.index(x, y, z);
this.space[index] = !this.space[index];
}
set(x: number, y: number, z: number, val: boolean) {
this.space[this.index(x, y, z)] = val;
}
rotated90(dim: 'x' | 'y' | 'z') {
const newSpace = new Array<boolean>(this.dims[0] * this.dims[1] * this.dims[2]);
newSpace.fill(false);
let newDims: DimensionDef;
let rotIndex: (i: number, j: number, k: number) => number;
if (dim === 'x') {
newDims = [this.dims[0], this.dims[2], this.dims[1]];
rotIndex = this.newIndexRotX.bind(this);
} else if (dim === 'y') {
newDims = [this.dims[2], this.dims[1], this.dims[0]];
rotIndex = this.newIndexRotY.bind(this);
} else {
newDims = [this.dims[1], this.dims[0], this.dims[2]];
rotIndex = this.newIndexRotZ.bind(this);
}
this.forEachCell((val, i, j, k) => {
if (val) {
newSpace[rotIndex(i, j, k)] = true;
}
})
return new VoxelSpaceBoolean({id: this.id, dims: newDims, space: newSpace, color: this.color, cullEmpty: false});
}
rot90(dim: 'x' | 'y' | 'z') {
const rot = this.rotated90(dim);
this.space = rot.getRaw();
this.dims = rot.getDims();
}
plus(space: VoxelSpaceBoolean): VoxelSpaceBoolean | null {
const newSpace = new Array<boolean>(this.dims[0] * this.dims[1] * this.dims[2]);
newSpace.fill(false);
let clash = false;
space.forEachCell((val, x, y, z) => {
if (this.at(x, y, z) !== val) {
newSpace[this.index(x, y, z)] = true;
} else {
if (val) {
clash = true;
}
}
});
if (!clash) {
return new VoxelSpaceBoolean({id: this.id, dims: this.getDims(), space: newSpace, color: this.color, cullEmpty: false});
}
return null;
}
size() {
let size = 0;
this.forEachCell((val) => {
if (val) {
size++;
}
});
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;
}
getAllPermutationsInPrism(prismDimX: number, prismDimY: number, prismDimZ: number): VoxelSpaceBoolean[] {
const rotations = this.getUniqueRotations();
let result = new Array<VoxelSpaceBoolean>();
for (let i = 0; i < rotations.length; i++) {
result = result.concat(rotations[i].getAllPositionsInPrism(prismDimX, prismDimY, prismDimZ));
}
return result;
}
}

16
src/solver/js/main.js Normal file
View File

@@ -0,0 +1,16 @@
import SomaSolver from "./SomaSolver";
import VoxelSpaceBoolean from "./VoxelSpaceBoolean";
export function solve(polycubes, dimX, dimY, dimZ) {
const solver = new SomaSolver(dimX, dimY, dimZ);
const voxelSpaces = new Array();
for (let i = 0; i < polycubes.length; i++) {
voxelSpaces.push(new VoxelSpaceBoolean({
id: i,
dims: [dimX, dimY, dimZ],
space: polycubes[i],
cullEmpty: true
}));
}
solver.solve(voxelSpaces);
return solver.getSolutions();
}

18
src/solver/js/main.ts Normal file
View File

@@ -0,0 +1,18 @@
import SomaSolver from "./SomaSolver";
import VoxelSpaceBoolean from "./VoxelSpaceBoolean";
import type SomaSolution from "./SomaSolution";
export function solve(polycubes: bigint[], dimX: number, dimY: number, dimZ: number): SomaSolution[] {
const solver = new SomaSolver(dimX, dimY, dimZ);
const voxelSpaces = new Array<VoxelSpaceBoolean>();
for (let i = 0; i < polycubes.length; i++) {
voxelSpaces.push(new VoxelSpaceBoolean({
id: i,
dims: [dimX, dimY, dimZ],
space: polycubes[i],
cullEmpty: true
}));
}
solver.solve(voxelSpaces);
return solver.getSolutions();
}

View File

@@ -0,0 +1,11 @@
{
"name": "soma-solve-js",
"version": "1.0.0",
"description": "",
"main": "index.ts",
"scripts": {
"build": "npx tsc main.ts --target ES2020 --moduleResolution node"
},
"author": "Daniel Ledda",
"license": "ISC"
}

View File

@@ -7,8 +7,8 @@
"debug": true
},
"release": {
"binaryFile": "../../public/solver/main.wasm",
"textFile": "../../public/solver/main.wat",
"binaryFile": "../../../public/solver/main.wasm",
"textFile": "../../../public/solver/main.wat",
"sourceMap": false,
"optimizeLevel": 3,
"shrinkLevel": 1,

View File

@@ -2,12 +2,14 @@ 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;
private dimX: i32;
private dimY: i32;
private dimZ: i32;
constructor(dimX: i32, dimY: i32, dimZ: i32) {
this.dimX = dimX;
this.dimY = dimY;
this.dimZ = dimZ;
this.solutionSpaces = [];
}
@@ -42,7 +44,7 @@ export default class SomaSolution {
}
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);
const solnRot = new SomaSolution(this.dimX, this.dimY, this.dimZ);
for (let j = 0; j < allRots.length; j++) {
solnRot.addSpace(allRots[j][i]);
}
@@ -74,15 +76,11 @@ export default class SomaSolution {
}
clone(): SomaSolution {
const clone = new SomaSolution(this.dim);
const clone = new SomaSolution(this.dimX, this.dimY, this.dimZ);
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

@@ -3,34 +3,30 @@ import SomaSolution from "./SomaSolution";
export default class SomaSolver {
private solutionCube: VoxelSpace;
private dim: i32;
private dimX: i32;
private dimY: i32;
private dimZ: 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);
constructor(dimX: i32, dimY: i32, dimZ: i32) {
this.dimX = dimX;
this.dimY = dimY;
this.dimZ = dimZ;
this.solutionCube = new VoxelSpace(0, dimX, dimY, dimZ, 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));
combosWithRots.push(polycubes[i].getAllPermutationsInPrism(this.dimX, this.dimY, this.dimZ));
}
let combos: VoxelSpace[][] = new Array<Array<VoxelSpace>>();
combos.push(polycubes[0].getAllPositionsInCube(this.dim));
combos.push(polycubes[0].getAllPositionsInPrism(this.dimX, this.dimY, this.dimZ));
combos = combos.concat(combosWithRots);
this.backtrackSolve(this.solutionCube, combos, new SomaSolution(this.dim));
this.backtrackSolve(this.solutionCube, combos, new SomaSolution(this.dimX, this.dimY, this.dimZ));
this.solutions = SomaSolution.filterUnique(this.solutions);
}
@@ -47,7 +43,7 @@ export default class SomaSolver {
nextSoln.addSpace(nextCubeGroup[i]);
if (polycubes.length == 1) {
this.solutions.push(nextSoln);
currentSoln = new SomaSolution(this.dim);
currentSoln = new SomaSolution(this.dimX, this.dimY, this.dimZ);
return;
} else {
this.backtrackSolve(fusionAttempt, polycubes.slice(1), nextSoln, depth + 1);

View File

@@ -13,19 +13,16 @@ export default class VoxelSpace {
private length: i32;
private space: i64;
private id: i32;
private dimx: i32;
private dimy: i32;
private dimz: 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.dimX = dimx;
this.dimY = dimy;
this.dimZ = dimz;
this.space = space;
if (cullEmpty) {
this.cullEmptySpace();
@@ -35,15 +32,15 @@ export default class VoxelSpace {
getExtrema(): Extrema {
const extrema = new Extrema(
0,
i32.MAX_VALUE,
this.dimX,
0,
i32.MAX_VALUE,
this.dimY,
0,
i32.MAX_VALUE,
this.dimZ,
);
for (let x = 0; x < this.dimx; x++) {
for (let y = 0; y < this.dimy; y++) {
for (let z = 0; z < this.dimz; z++) {
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;
@@ -61,8 +58,8 @@ export default class VoxelSpace {
private cullEmptySpace(): void {
const extrema = this.getExtrema();
let index = 0;
let newSpace = 0;
let index: i32 = 0;
let newSpace: i64 = 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++) {
@@ -73,9 +70,9 @@ export default class VoxelSpace {
}
}
}
this.dimx = extrema.xMax - extrema.xMin + 1;
this.dimy = extrema.yMax - extrema.yMin + 1;
this.dimz = extrema.zMax - extrema.zMin + 1;
this.dimX = extrema.xMax - extrema.xMin + 1;
this.dimY = extrema.yMax - extrema.yMin + 1;
this.dimZ = extrema.zMax - extrema.zMin + 1;
this.space = newSpace;
}
@@ -134,45 +131,44 @@ export default class VoxelSpace {
}
}
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));
}
getAllPositionsInPrism(cubeDimX: i32, cubeDimY: i32, cubeDimZ: i32): VoxelSpace[] {
const cubePositions: VoxelSpace[] = [];
if (this.dimX > cubeDimX || this.dimY > cubeDimY || this.dimZ > cubeDimZ) {
return cubePositions;
}
for (let x = 0; x < (cubeDimX - this.dimX + 1); x++) {
for (let y = 0; y < (cubeDimY - this.dimY + 1); y++) {
for (let z = 0; z < (cubeDimZ - this.dimZ + 1); z++) {
const cubePos = new VoxelSpace(this.id, cubeDimX, cubeDimY, cubeDimZ);
for (let posX = 0; posX < this.dimX; posX++) {
for (let posY = 0; posY < this.dimY; posY++) {
for (let posZ = 0; posZ < this.dimZ; posZ++) {
cubePos.set(x + posX, y + posY, z + posZ, this.at(posX, posY, posZ));
}
}
cubePositions.push(cubePos);
}
cubePositions.push(cubePos);
}
}
return cubePositions;
} else {
throw new Error("cubeDim must be a positive integer.");
}
return cubePositions;
}
matches(space: VoxelSpace): boolean {
if (space.dimx !== this.dimx) {
if (space.dimX !== this.dimX) {
return false;
}
if (space.dimy !== this.dimy) {
if (space.dimY !== this.dimY) {
return false;
}
if (space.dimz !== this.dimz) {
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());
return new VoxelSpace(this.id, this.dimX, this.dimY, this.dimZ, this.getRaw());
}
private getXAxisSpins(): VoxelSpace[] {
@@ -192,35 +188,35 @@ export default class VoxelSpace {
// [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;
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);
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;
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);
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;
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;
const mask = 1 << this.dimY * this.dimZ * x + this.dimZ * y + z;
if (val) {
this.space |= mask;
} else {
@@ -230,83 +226,83 @@ export default class VoxelSpace {
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++) {
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);
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++) {
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);
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++) {
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);
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;
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;
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;
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 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++) {
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++;
}
@@ -316,11 +312,11 @@ export default class VoxelSpace {
return size;
}
getAllPermutationsInCubeOfSize(dim: i32): VoxelSpace[] {
getAllPermutationsInPrism(prismDimX: i32, prismDimY: i32, prismDimZ: 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));
result = result.concat(rotations[i].getAllPositionsInPrism(prismDimX, prismDimY, prismDimZ));
}
return result;
}

View File

@@ -2,11 +2,11 @@ import SomaSolver from "./SomaSolver";
import VoxelSpace from "./VoxelSpace";
export function solve(polycubes: Array<i64>, dim: i32): Int64Array[] {
const solver = new SomaSolver(dim);
export function solve(polycubes: Array<i64>, dimX: i32, dimY: i32, dimZ: i32): Int64Array[] {
const solver = new SomaSolver(dimX, dimY, dimZ);
const voxelSpaces = new Array<VoxelSpace>();
for (let i = 0; i < polycubes.length; i++) {
voxelSpaces.push(new VoxelSpace(i, dim, dim, dim, polycubes[i], true));
voxelSpaces.push(new VoxelSpace(i, dimX, dimY, dimZ, polycubes[i], true));
}
solver.solve(voxelSpaces);
const solutions = solver.getSolutions();

View File

@@ -6,7 +6,7 @@ const asyncTask = async () => {
// You can now use your wasm / as-bind instance!
const response = asBindInstance.exports.solve(
[16875584n, 16810176n, 65688n, 77952n, 12296n, 2109456n, 4184n], 3
[16875584n, 16810176n, 65688n, 77952n, 12296n, 2109456n, 4184n], 3, 3, 3
);
console.log(response); // AsBind: Hello World!
};

View File

@@ -1,7 +1,7 @@
{
"scripts": {
"asbuild:untouched": "asc assembly/index.ts --target debug",
"asbuild:optimized": "asc assembly/index.ts --target release",
"asbuild:untouched": "asc assembly/index.ts --exportRuntime --transform as-bind --target debug",
"asbuild:optimized": "asc assembly/index.ts --exportRuntime --transform as-bind --target release",
"asbuild": "npm run asbuild:untouched && npm run asbuild:optimized",
"test": "node tests"
},

View File

@@ -1,111 +1,119 @@
import { derived, writable } from 'svelte/store';
import { get } from 'svelte/store';
import type SomaSolution from "./SomaSolution";
import SomaSolution from "./SomaSolution";
import VoxelSpaceBigInt from "./VoxelSpaceBigInt";
import type {DimensionDef} from "./VoxelSpaceBoolean";
type PolycubeInput = {
color: string,
rep: bigint,
}
const MAX_DIMS = 5;
const MIN_DIMS = 2;
const store = {
polycubes: writable<PolycubeInput[]>([{rep: BigInt(0), color: colorFromIndex(0)}]),
somaDimension: writable(3),
};
const MAX_DIMS = 20;
const MIN_DIMS = 1;
export const solving = writable(false);
export const debug = writable(false);
export const somaDimX = dimStore(3);
export const somaDimY = dimStore(3);
export const somaDimZ = dimStore(3);
export const polycubes = polycubeStore();
export const selectedCube = writable(0);
export const isMaxDimension = derived(store.somaDimension, ($somaDimension: number) => $somaDimension >= MAX_DIMS);
export const isMinDimension = derived(store.somaDimension, ($somaDimension: number) => $somaDimension <= MIN_DIMS);
export const isMaxPolycubes = derived(
[store.polycubes, store.somaDimension],
([$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<number | null>(null);
export const showingSolution = writable(false);
export const totalVolume = derived(
[somaDimX, somaDimY, somaDimZ],
([$dimX, $dimY, $dimZ]: [number, number, number]) => $dimX*$dimY*$dimZ
);
export const isMaxPolycubes = derived(
[polycubes, totalVolume],
([$cubes, $vol]: [VoxelSpaceBigInt[], number]) => $cubes.length >= $vol
);
export const isMinPolycubes = derived(
polycubes,
($polycubes: VoxelSpaceBigInt[]) => $polycubes.length <= 1
);
export const somaDimension = {
subscribe: store.somaDimension.subscribe,
inc() {
if (!get(isMaxDimension)) {
store.somaDimension.update((dims: number) => {
polycubes.reset(dims + 1);
return dims + 1;
});
}
},
dec() {
if (!get(isMinDimension)) {
store.somaDimension.update((dims: number) => {
polycubes.reset(dims - 1);
return dims - 1;
});
}
},
set(dims: number) {
if (dims <= MAX_DIMS && dims >= MIN_DIMS) {
polycubes.reset(dims);
store.somaDimension.set(dims);
}
}
};
export const polycubes = {
subscribe: store.polycubes.subscribe,
addCube() {
const isMaxPolycubes = get(store.polycubes).length >= get(store.somaDimension) ** 3;
if (!isMaxPolycubes) {
store.polycubes.update((polycubes: PolycubeInput[]) => polycubes.concat({
rep: BigInt(0),
color: colorFromIndex(polycubes.length),
}));
}
},
removeCube() {
const isMinPolycubes = get(store.polycubes).length <= 1;
if (!isMinPolycubes) {
store.polycubes.update((polycubes: PolycubeInput[]) => polycubes.splice(0, polycubes.length - 1));
}
const newLength = get(store.polycubes).length;
if (newLength <= get(selectedCube)) {
selectedCube.set(newLength - 1);
}
},
toggle(cubeIndex: number, x: number, y: number, z: number) {
const dims = get(store.somaDimension);
const mask = BigInt(1) << BigInt(dims ** 2 * x + dims * y + z);
const cubes = get(store.polycubes);
cubes[cubeIndex].rep ^= mask;
store.polycubes.set(cubes);
},
set(cubeIndex: number, val: boolean, x: number, y: number, z: number) {
const dims = get(store.somaDimension);
const mask = BigInt(1) << BigInt(dims ** 2 * x + dims * y + z);
const cubes = get(store.polycubes);
if (val) {
cubes[cubeIndex].rep |= mask
} else {
cubes[cubeIndex].rep &= ~mask
}
store.polycubes.set(cubes);
},
reset(dims: number) {
store.polycubes.update((polycubes: PolycubeInput[]) => {
const result: PolycubeInput[] = [];
for (let i = 0; i < Math.min(polycubes.length, dims**3); i++) {
result.push({
rep: BigInt(0),
color: colorFromIndex(i),
});
function dimStore(init: number) {
const dimStore = writable(init);
return {
subscribe: dimStore.subscribe,
set(dim: number) {
if (dim > MAX_DIMS || dim < MIN_DIMS) {
return;
}
return result;
dimStore.set(dim);
polycubes.reset();
},
}
}
function polycubeStore() {
function freshCube(id: number) {
return new VoxelSpaceBigInt({
id: id,
dims: [get(somaDimX), get(somaDimY), get(somaDimZ)],
color: colorFromIndex(id),
cullEmpty: false
});
}
};
const polycubeStore = writable<VoxelSpaceBigInt[]>([freshCube(0)]);
return {
subscribe: polycubeStore.subscribe,
setColor(cubeIndex: number, color: string) {
const cubes = get(polycubeStore);
cubes[cubeIndex].setColor(color);
polycubeStore.set(cubes);
},
addCube() {
if (!get(isMaxPolycubes)) {
polycubeStore.update((polycubes: VoxelSpaceBigInt[]) =>
polycubes.concat(freshCube(polycubes.length)));
}
},
removeCube() {
if (!get(isMinPolycubes)) {
polycubeStore.update((polycubes: VoxelSpaceBigInt[]) => polycubes.splice(0, polycubes.length - 1));
}
const newLength = get(polycubeStore).length;
if (newLength <= get(selectedCube)) {
selectedCube.set(newLength - 1);
}
},
toggle(cubeIndex: number, x: number, y: number, z: number) {
const cubes = get(polycubeStore);
cubes[cubeIndex].toggle(x, y, z);
polycubeStore.set(cubes);
},
set(cubeIndex: number, val: boolean, x: number, y: number, z: number) {
const cubes = get(polycubeStore);
cubes[cubeIndex].set(x, y, z, val);
polycubeStore.set(cubes);
},
reset() {
polycubeStore.update((polycubes: VoxelSpaceBigInt[]) => {
const result: VoxelSpaceBigInt[] = [];
for (let i = 0; i < Math.min(polycubes.length, get(totalVolume)); i++) {
result.push(freshCube(i));
}
return result;
});
}
}
}
function colorFromIndex(index: number) {
function rgbToHex(rgbStr: string): string {
const sep = rgbStr.indexOf(",") > -1 ? "," : " ";
const rgb = rgbStr.substr(4).split(")")[0].split(sep);
const r = (+rgb[0]).toString(16).padStart(2, "0");
const g = (+rgb[1]).toString(16).padStart(2, "0");
const b = (+rgb[2]).toString(16).padStart(2, "0");
return "#" + r + g + b;
}
function hslToRgb(hslStr: string): string {
const opt = new Option();
opt.style.color = hslStr;
return opt.style.color;
}
export function colorFromIndex(index: number): string {
const colorWheelCycle = Math.floor(index / 6);
const darknessCycle = Math.floor(index / 12);
const spacing = (360 / 6);
@@ -113,5 +121,121 @@ function colorFromIndex(index: number) {
let hue = spacing * (index % 6) + offset;
const saturation = 100;
const lightness = 1 / (2 + darknessCycle) * 100;
return `hsl(${hue},${saturation}%,${Math.round(lightness)}%)`;
}
return rgbToHex(hslToRgb(`hsl(${hue},${saturation}%,${Math.round(lightness)}%)`));
}
const worker = new Worker('../solver/main.js', {type: "module"});
async function respondWasm(event: MessageEvent) {
solutions.set(event.data.map((wasmSolution) => {
const solnObj = new SomaSolution(get(somaDimX), get(somaDimY), get(somaDimZ));
const spaceReps = wasmSolution.split(",");
for (let i = 0; i < spaceReps.length; i++) {
solnObj.addSpace(new VoxelSpaceBigInt({
id: i,
dims: [get(somaDimX), get(somaDimY), get(somaDimZ)] as DimensionDef,
space: BigInt(parseInt(spaceReps[i])),
color: get(polycubes)[i].getColor(),
cullEmpty: false,
}));
}
return solnObj;
}));
if (event.data.length > 0) {
activeSolution.set(0);
showingSolution.set(true);
} else {
showingSolution.set(false);
activeSolution.set(null);
}
solving.set(false);
}
function respondJs(event: MessageEvent) {
solutions.set(event.data.solns.map(solnSpaces => {
const solnObj = new SomaSolution(get(somaDimX), get(somaDimY), get(somaDimZ));
for (let i = 0; i < solnSpaces.length; i++) {
solnObj.addSpace(new VoxelSpaceBigInt({
id: i,
dims: [get(somaDimX), get(somaDimY), get(somaDimZ)] as DimensionDef,
space: BigInt(`0b${ solnSpaces[i] }`),
color: get(polycubes)[i].getColor(),
cullEmpty: false,
}));
}
return solnObj;
}));
if (event.data.length > 0) {
activeSolution.set(0);
showingSolution.set(true);
} else {
showingSolution.set(false);
activeSolution.set(null);
}
solving.set(false);
}
export function solve() {
const doWasm = get(totalVolume) <= 32;
let inputCubes;
if (doWasm) {
worker.onmessage = (e) => respondWasm(e);
} else {
worker.onmessage = (e) => respondJs(e);
}
inputCubes = get(polycubes).map(cubeInput => cubeInput.getRaw());
solving.set(true);
worker.postMessage({
type: doWasm ? 'wasm' : 'js',
polycubes: inputCubes,
dimX: get(somaDimX),
dimY: get(somaDimY),
dimZ: get(somaDimZ)
});
}
// async function solveSync() {
// const solver = new SomaSolver(get(somaDimX), get(somaDimY), get(somaDimZ));
// function showSolutionWaitUserFeedback(soln: SomaSolution) {
// activeSolution.set(0);
// solutions.set([soln]);
// showingSolution.set(true);
// return new Promise<void>((resolve) => {
// const callback = (e: KeyboardEvent) => {
// resolve();
// window.removeEventListener("keydown", callback);
// };
// window.addEventListener("keydown", callback);
// });
// }
// if (get(debug)) {
// solver.setDebug({
// showSoln(soln: SomaSolution) {
// return showSolutionWaitUserFeedback(soln);
// },
// showSpace(cube: VoxelSpaceBoolean) {
// const testSoln = new SomaSolution(get(somaDimX), get(somaDimY), get(somaDimZ));
// testSoln.addSpace(cube);
// return showSolutionWaitUserFeedback(testSoln);
// }
// });
// }
// solving.set(true);
// await solver.solve(get(polycubes).map(cubeInput => new VoxelSpaceBoolean({
// id: cubeInput.getId(),
// dims: cubeInput.getDims(),
// space: cubeInput.getRaw(),
// color: cubeInput.getColor(),
// cullEmpty: true
// })));
// const solns = solver.getSolutions();
//
// if (solns.length > 0) {
// activeSolution.set(0);
// solutions.set(solns);
// showingSolution.set(true);
// } else {
// showingSolution.set(false);
// activeSolution.set(null);
// }
// solving.set(false);
// }

View File

@@ -1,24 +1,24 @@
<script lang="ts">
import {somaDimension, polycubes, selectedCube, showingSolution} from "../store";
import {somaDimX, somaDimY, somaDimZ, polycubes, selectedCube, showingSolution} from "../store";
import VoxelSpaceBoolean from "../VoxelSpaceBoolean";
export let cubeNo: number;
$: dimension = $somaDimension;
$: cube = $polycubes[cubeNo];
$: cubeColor = cube.color;
$: cube = $polycubes[cubeNo] as VoxelSpaceBoolean;
$: cubeColor = cube.getColor();
$: currentlyVisualised = $selectedCube === cubeNo && !$showingSolution;
let cellStartDragInitialVal: boolean = false;
let cellStartDrag: number = 0;
let cellDragStartPos: {x: number, y: number} = {x: 0, y: 0};
let cellEndDrag: number = 0;
let cellDragEndPos: {x: number, y: number} = {x: 0, y: 0};
let picker: HTMLInputElement;
function cellNo(x: number, y: number, z: number) {
return dimension ** 2 * x + dimension * y + z;
return $somaDimY * $somaDimZ * x + $somaDimZ * y + z;
}
function at(rep: bigint, x: number, y: number, z: number) {
const mask = BigInt(1) << BigInt(cellNo(x, y, z));
return (rep & mask) !== BigInt(0);
function at(cube: VoxelSpaceBoolean, x: number, y: number, z: number) {
return cube.at(x, y, z);
}
function onMouseOverCell(event: MouseEvent, x: number, y: number, z: number) {
@@ -30,7 +30,7 @@
function onMouseDownCell(event: MouseEvent, x: number, y: number, z: number) {
cellStartDrag = cellNo(x, y, z);
cellStartDragInitialVal = at(cube.rep, x, y, z);
cellStartDragInitialVal = at(cube, x, y, z);
cellDragStartPos.x = event.clientX;
cellDragStartPos.y = event.clientY;
}
@@ -56,29 +56,43 @@
function onClickCube() {
showingSolution.set(false);
selectedCube.set(cubeNo)
selectedCube.set(cubeNo);
}
function onColorChange(event: InputEvent) {
polycubes.setColor(cubeNo, event.target.value);
}
</script>
<div
class="cube"
class:active={currentlyVisualised}
style="--color: {cubeColor}; --dimension: {dimension};"
style="--color: {cubeColor};"
on:contextmenu|preventDefault
on:mousedown={onClickCube}
>
<h1>Cube: {cubeNo + 1}</h1>
{#each {length: dimension} as _, x}
<div class="header">
<h1>Cube: {cubeNo + 1}</h1>
<div class="colorPickerBtn" on:click={picker.click()}>
<input
bind:this={picker}
class="colorPicker"
type="color"
value="{cubeColor}"
on:change={(event) => onColorChange(event)}/>
</div>
</div>
{#each {length: $somaDimX} as _, x}
<div class="layer">
{#each {length: dimension} as _, y}
{#each {length: $somaDimY} as _, y}
<div class="row">
{#each {length: dimension} as _, z}
{#each {length: $somaDimZ} as _, z}
<div
class="cell"
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)}
class:filled={at(cube, 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)}
/>
{/each}
</div>
@@ -91,6 +105,27 @@
* {
--cell-size: 30px;
}
.header {
text-align: center;
display: flex;
align-content: center;
justify-content: space-between;
}
.header > * {
display: inline-block;
}
.colorPicker {
visibility: hidden;
width: 0;
height: 0;
}
.colorPickerBtn {
align-self: center;
background-image: url("../resources/ColorWheel.png");
background-size: cover;
width: 1.5em;
height: 1.5em;
}
.cube.active {
border: 3px solid #ff3e00;
}
@@ -125,6 +160,8 @@
}
.row {
display: flex;
align-content: center;
justify-content: center;
margin: 0;
}
.layer {

View File

@@ -1,8 +1,13 @@
<script lang="ts">
import {isMaxPolycubes, isMinPolycubes, somaDimension, polycubes, solutions} from "../store";
import SomaSolution from "../SomaSolution";
import {
isMaxPolycubes,
isMinPolycubes,
polycubes,
solutions,
colorFromIndex,
activeSolution, showingSolution, totalVolume, somaDimX, somaDimY, somaDimZ, debug
} from "../store";
import SolutionList from "./SolutionList.svelte";
import VoxelSpace from "../VoxelSpace";
$: numCubes = $polycubes.length;
$: cubes = $polycubes;
@@ -11,57 +16,16 @@
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});
size = cubes.reduce((prev, cube) => cube.size() + prev, 0);
noEmpties = cubes.reduce((prev, cube) => (cube.size() !== 0) && prev, true);
enoughSubcubes = size === $totalVolume;
readyToSolve = enoughSubcubes && noEmpties;
}
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}.`);
messages.push(`You have not input enough subcubes to form a rectangular prism with side lengths ${$somaDimX}, ${$somaDimY}, and ${$somaDimZ}. Needed: ${$totalVolume}, current: ${size}.`);
}
if (!noEmpties) {
messages.push("You have left some of the polycube inputs empty. Remove them to solve.");
@@ -74,34 +38,34 @@
<h1>Somaesque</h1>
<div class="widgets">
<div class="option">
<p>Dimension:</p>
<p>Dimensions:</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>
X
<input
type="number"
value="3"
on:input={(e) => somaDimX.set(e.target.valueAsNumber)}/>
Y
<input
type="number"
value="3"
on:input={(e) => somaDimY.set(e.target.valueAsNumber)}/>
Z
<input
type="number"
value="3"
on:input={(e) => somaDimZ.set(e.target.valueAsNumber)}/>
{#if $totalVolume > 32}
<p class="warn">The total number of units exceeds 32. Attempting to solve puzzles with more than 32 units results in significantly slower computation time.</p>
{/if}
</div>
</div>
<div class="option">
<p>Cubes:</p>
<div class="choice">
<p>{numCubes}</p>
<button on:click={polycubes.removeCube} disabled={$isMinPolycubes}>-</button>
<p>{numCubes}</p>
<button on:click={polycubes.addCube} disabled={$isMaxPolycubes}>+</button>
</div>
</div>
@@ -109,7 +73,7 @@
<div class="option">
<button
class="solve"
on:click={solveWasm}
on:click={solve}
title="{genTooltip(enoughSubcubes, noEmpties, size)}"
disabled="{solving || !readyToSolve}">
{solving ? "Solving..." : "Solve!"}
@@ -121,6 +85,9 @@
</div>
<style>
.warn {
color: red;
}
p {
margin: 0;
display: inline-block;
@@ -130,10 +97,10 @@
text-align: center;
margin-top: 1em;
}
button {
input {
display: inline-block;
background-color: #999999;
width: 2em;
width: 3em;
height: 2em;
border-style: none;
}

View File

@@ -1,12 +1,12 @@
<script lang="ts">
import {polycubes, activeSolution, showingSolution, solutions} from "../store";
import {activeSolution, showingSolution, solutions} from "../store";
import SomaSolution from "../SomaSolution";
$: solutionDisplayed = $solutions[$activeSolution];
$: dimension = (solutionDisplayed && solutionDisplayed.getDims?.()[0]) ?? 3;
$: solnToShow = $solutions[$activeSolution];
$: dims = (solnToShow?.getDims?.()) ?? [3, 3, 3];
function colorAt(soln: SomaSolution, x: number, y: number, z: number) {
return $polycubes[soln.at(z, dimension-1-x, y)].color;
return solnToShow.getPieces()[soln.at(x, y, z)]?.getColor?.() ?? "red";
}
</script>
@@ -14,19 +14,18 @@
<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}
{#each {length: dims[0]} as _, x}
<div class="layer">
{#each {length: dimension} as _, y}
{#each {length: dims[1]} as _, y}
<div class="row">
{#each {length: dimension} as _, z}
{#each {length: dims[2]} as _, z}
<div
class="cell"
style="background-color:{colorAt(solutionDisplayed, x, y, z)}; border-color: {colorAt(solutionDisplayed, x, y, z)}"
style="background-color:{colorAt(solnToShow, x, y, z)}; border-color: {colorAt(solnToShow, x, y, z)}"
class:filled={true}
/>
{/each}

View File

@@ -1,36 +1,49 @@
<script lang="ts">
import PolycubeScene from "./threedee/PolycubeScene.ts";
import PolycubeScene from "./threedee/PolycubeScene";
import {onMount} from "svelte";
import {polycubes, somaDimension, selectedCube, solutions, activeSolution, showingSolution} from "../store";
import {polycubes, selectedCube, solutions, activeSolution, showingSolution, somaDimX, somaDimY, somaDimZ} from "../store";
import Solution2D from "./Solution2D.svelte";
import VoxelSpaceBoolean from "../VoxelSpaceBoolean";
$: cube = $polycubes[$selectedCube];
$: soln = $solutions[$activeSolution];
let el: HTMLCanvasElement;
let threeTest: PolycubeScene;
let scene: PolycubeScene;
let loaded: boolean = false;
onMount(() => {
threeTest = new PolycubeScene(el, () => loaded = true, console.log);
scene = new PolycubeScene(el, () => loaded = true, console.log);
});
window.getPermutations = () => {
const newCube: VoxelSpaceBoolean = cube.clone() as VoxelSpaceBoolean;
(newCube as VoxelSpaceBoolean).cullEmptySpace();
return (newCube as VoxelSpaceBoolean).getAllPermutationsInPrism($somaDimX, $somaDimY, $somaDimZ);
}
window.showRot = (rot: VoxelSpaceBoolean) => {
scene?.showPolycube(rot);
}
$: {
if (loaded) {
if ($showingSolution) {
const colorMap = {};
$polycubes.forEach((polycube, i) => colorMap[i] = polycube.color);
threeTest?.showSolution(soln, colorMap);
scene?.showSolution(soln);
} else {
threeTest?.showPolycube(cube.rep, $somaDimension, cube.color);
scene?.showPolycube(cube);
}
}
}
</script>
<div class="top">
<div class="soln2d-container">
<Solution2D/>
</div>
{#if $activeSolution !== null}
<div class="soln2d-container">
<Solution2D/>
</div>
{/if}
<canvas
bind:this={el}
width="640"

View File

@@ -1,20 +1,20 @@
import * as THREE from "three";
import type VoxelSpace from "../../VoxelSpace";
import type VoxelSpaceBoolean from "../../VoxelSpaceBoolean";
import type GeometryManager from "./GeometryManager";
import type VoxelSpaceBigInt from "../../VoxelSpaceBigInt";
export default class PolycubeMesh {
private static geometryManager: GeometryManager;
private group: THREE.Group;
private meshes: THREE.Mesh[] = [];
private currentPolycube: bigint = 0n;
private currentPolycube: boolean[] | bigint = [];
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});
constructor(polycube: VoxelSpaceBoolean | VoxelSpaceBigInt) {
this.material = new THREE.MeshPhongMaterial({color: polycube.getColor(), shininess: 100, reflectivity: 100});
this.group = new THREE.Group();
this.swapColor(color);
this.swapPolycube(polycube);
}
@@ -22,11 +22,7 @@ export default class PolycubeMesh {
PolycubeMesh.geometryManager = manager;
}
swapColor(color: string) {
this.material.color.set(color);
}
swapPolycube(polycube: VoxelSpace) {
swapPolycube(polycube: VoxelSpaceBoolean | VoxelSpaceBigInt) {
if (polycube.getRaw() === this.currentPolycube) {
return;
}
@@ -40,10 +36,11 @@ export default class PolycubeMesh {
}
});
this.currentPolycube = polycube.getRaw();
this.material.color.set(polycube.getColor());
this.flyDirection = this.middlePosOfGroup().normalize();
}
private addCube(refPolycube: VoxelSpace, x: number, y: number, z: number) {
private addCube(refPolycube: VoxelSpaceBoolean | VoxelSpaceBigInt, x: number, y: number, z: number) {
const dims = refPolycube.getDims();
const neighbourProfile = refPolycube.getDirectNeighbourProfile(x, y, z);
const mesh = new THREE.Mesh(
@@ -51,9 +48,9 @@ export default class PolycubeMesh {
this.material
);
mesh.position.set(
-((dims[0] - 1)/2) + x,
-((dims[1] - 1)/2) + y,
-((dims[2] - 1)/2) + z,
((dims[0] - 1)/2) - x,
-((dims[1] - 1)/2) + y,
);
this.meshes.push(mesh);
this.group.add(mesh);

View File

@@ -2,7 +2,8 @@ import * as THREE from 'three';
import type SomaSolution from "../../SomaSolution";
import RotationControl from "./RotationControl";
import PolycubeMesh from "./PolycubeMesh";
import VoxelSpace, {DimensionDef} from "../../VoxelSpace";
import type VoxelSpaceBoolean from "../../VoxelSpaceBoolean";
import type VoxelSpaceBigInt from "../../VoxelSpaceBigInt";
import GeometryManager from "./GeometryManager";
export default class PolycubeScene {
@@ -44,20 +45,19 @@ export default class PolycubeScene {
this.camera.lookAt(0, 0, 0);
}
private showPolycube(polycube: bigint, dims: number, color: string) {
showPolycube(voxelSpace: VoxelSpaceBoolean) {
this.controls.disableFly();
const voxelSpace = new VoxelSpace(0, [dims, dims, dims], polycube, true);
this.clearScene();
this.addPolycube(voxelSpace, color);
this.addPolycube(voxelSpace);
this.polycubeMeshes[0].center();
}
private showSolution(solution: SomaSolution, colorMap: Record<number, string>) {
showSolution(solution: SomaSolution) {
this.controls.enableFly();
this.clearScene();
const pieces = solution.getPieces();
for (let i = 0; i < pieces.length; i++) {
this.addPolycube(pieces[i], colorMap[i]);
this.addPolycube(pieces[i]);
}
}
@@ -66,8 +66,8 @@ export default class PolycubeScene {
this.cubeScene.clear();
}
private addPolycube(voxelSpace: VoxelSpace, color: string) {
const newMesh = new PolycubeMesh(voxelSpace, color);
private addPolycube(voxelSpace: VoxelSpaceBoolean | VoxelSpaceBigInt) {
const newMesh = new PolycubeMesh(voxelSpace);
this.polycubeMeshes.push(newMesh);
this.cubeScene.add(newMesh.asObj3D());
}