great stuff
This commit is contained in:
57
src/ui/App.svelte
Normal file
57
src/ui/App.svelte
Normal file
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import Sidebar from "./Sidebar.svelte";
|
||||
import SolutionInteractor from "./Interactor.svelte";
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<div class="sidebarContainer">
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div class="solutionBodyContainer">
|
||||
<SolutionInteractor />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.sidebarContainer {
|
||||
background-color: black;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 20%;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
.solutionBodyContainer {
|
||||
background-color: grey;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 80%;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
:global(body) {
|
||||
color: white;
|
||||
background: #333333;
|
||||
}
|
||||
@media(max-width: 1600px) {
|
||||
.solutionBodyContainer {
|
||||
width: 100%;
|
||||
}
|
||||
.sidebarContainer {
|
||||
width: 20%;
|
||||
}
|
||||
}
|
||||
@media(max-width: 1200px) {
|
||||
.solutionBodyContainer {
|
||||
width: 100%;
|
||||
}
|
||||
.sidebarContainer {
|
||||
width: 15em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
136
src/ui/CubeInput.svelte
Normal file
136
src/ui/CubeInput.svelte
Normal file
@@ -0,0 +1,136 @@
|
||||
<script lang="ts">
|
||||
import {somaDimension, polycubes, selectedCube, showingSolution} from "../store";
|
||||
export let cubeNo: number;
|
||||
|
||||
$: dimension = $somaDimension;
|
||||
$: cube = $polycubes[cubeNo];
|
||||
$: cubeColor = cube.color;
|
||||
$: 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};
|
||||
|
||||
function cellNo(x: number, y: number, z: number) {
|
||||
return dimension ** 2 * x + dimension * 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 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);
|
||||
}
|
||||
}
|
||||
|
||||
function onMouseDownCell(event: MouseEvent, x: number, y: number, z: number) {
|
||||
cellStartDrag = cellNo(x, y, z);
|
||||
cellStartDragInitialVal = at(cube.rep, x, y, z);
|
||||
cellDragStartPos.x = event.clientX;
|
||||
cellDragStartPos.y = event.clientY;
|
||||
}
|
||||
|
||||
function onMouseUpCell(event: MouseEvent, x: number, y: number, z: number) {
|
||||
cellEndDrag = cellNo(x, y, z);
|
||||
cellDragEndPos.x = event.clientX;
|
||||
cellDragEndPos.y = event.clientY;
|
||||
if (cellStartDrag === cellEndDrag && dragDist() < 30) {
|
||||
let val;
|
||||
if (event.button === 0) {
|
||||
val = !cellStartDragInitialVal;
|
||||
} else if (event.button === 2) {
|
||||
val = false;
|
||||
}
|
||||
polycubes.set(cubeNo, val, x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
function dragDist() {
|
||||
return Math.sqrt((cellDragStartPos.x - cellDragEndPos.x) ** 2 + (cellDragStartPos.y - cellDragEndPos.y) ** 2);
|
||||
}
|
||||
|
||||
function onClickCube() {
|
||||
showingSolution.set(false);
|
||||
selectedCube.set(cubeNo)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="cube"
|
||||
class:active={currentlyVisualised}
|
||||
style="--color: {cubeColor}; --dimension: {dimension};"
|
||||
on:contextmenu|preventDefault
|
||||
on:mousedown={onClickCube}
|
||||
>
|
||||
<h1>Cube: {cubeNo + 1}</h1>
|
||||
{#each {length: dimension} as _, x}
|
||||
<div class="layer">
|
||||
{#each {length: dimension} as _, y}
|
||||
<div class="row">
|
||||
{#each {length: dimension} 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)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
* {
|
||||
--cell-size: 30px;
|
||||
}
|
||||
.cube.active {
|
||||
border: 3px solid #ff3e00;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.layer {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.filled {
|
||||
background: var(--color);
|
||||
}
|
||||
</style>
|
||||
53
src/ui/Interactor.svelte
Normal file
53
src/ui/Interactor.svelte
Normal file
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import {polycubes} from "../store";
|
||||
import CubeInput from "./CubeInput.svelte";
|
||||
import SolutionViewer from "./SolutionViewer.svelte";
|
||||
$: numCubes = $polycubes.length;
|
||||
</script>
|
||||
|
||||
<div class="viewport">
|
||||
<div class="input-container">
|
||||
{#each {length: numCubes} as _, cubeNo}
|
||||
<div class="cube-input">
|
||||
<div class="padder">
|
||||
<CubeInput
|
||||
cubeNo={cubeNo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="threedee">
|
||||
<SolutionViewer/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.threedee {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
.padder {
|
||||
padding: 1em;
|
||||
}
|
||||
.cube-input {
|
||||
margin: auto;
|
||||
}
|
||||
.input-container {
|
||||
flex: 0 1 fit-content;
|
||||
overflow-x: scroll;
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
.viewport {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
196
src/ui/Sidebar.svelte
Normal file
196
src/ui/Sidebar.svelte
Normal file
@@ -0,0 +1,196 @@
|
||||
<script lang="ts">
|
||||
import {isMaxPolycubes, isMinPolycubes, somaDimension, polycubes, solutions} from "../store";
|
||||
import SomaSolution from "../SomaSolution";
|
||||
import SolutionList from "./SolutionList.svelte";
|
||||
import VoxelSpace from "../VoxelSpace";
|
||||
|
||||
$: numCubes = $polycubes.length;
|
||||
$: cubes = $polycubes;
|
||||
let noEmpties: boolean;
|
||||
let enoughSubcubes: boolean;
|
||||
let readyToSolve: boolean;
|
||||
let size: number;
|
||||
$: {
|
||||
const dim = $somaDimension as number;
|
||||
const polycubes: VoxelSpace[] = cubes.map(cubeInput => new VoxelSpace(0, [dim, dim, dim], cubeInput.rep));
|
||||
size = polycubes.reduce((prev, cube) => cube.size() + prev, 0);
|
||||
noEmpties = polycubes.reduce((prev, cube) => (cube.size() !== 0) && prev, true);
|
||||
enoughSubcubes = size === dim**3;
|
||||
readyToSolve = size === dim**3 && noEmpties;
|
||||
}
|
||||
let solving = false;
|
||||
const worker = new Worker('../solver/main.js', {type: "module"});
|
||||
|
||||
async function respondWasm(event: MessageEvent) {
|
||||
const dim = $somaDimension as number;
|
||||
solutions.set(event.data.map((wasmSolution) => {
|
||||
const solnObj = new SomaSolution(dim);
|
||||
const spaceReps = wasmSolution.split(",");
|
||||
for (let i = 0; i < spaceReps.length; i++) {
|
||||
solnObj.addSpace(new VoxelSpace(i, [dim, dim, dim], BigInt(parseInt(spaceReps[i]))));
|
||||
}
|
||||
return solnObj;
|
||||
}));
|
||||
solving = false;
|
||||
}
|
||||
|
||||
function respondJs(event: MessageEvent) {
|
||||
solutions.set(event.data.map(solnData => {
|
||||
const solution = new SomaSolution(solnData.dim);
|
||||
solnData.solutionSpaces.forEach((voxelSpace, i) => solution.addSpace(new VoxelSpace(i, [solnData.dim, solnData.dim, solnData.dim], voxelSpace.space)));
|
||||
return solution;
|
||||
}));
|
||||
solving = false;
|
||||
}
|
||||
|
||||
function solveJs() {
|
||||
worker.onmessage = (e) => respondJs(e);
|
||||
const polycubes = cubes.map(cubeInput => cubeInput.rep);
|
||||
solving = true;
|
||||
worker.postMessage({type: 'js', polycubes, dims: $somaDimension});
|
||||
}
|
||||
|
||||
function solveWasm() {
|
||||
worker.onmessage = (e) => respondWasm(e);
|
||||
const polycubes = cubes.map(cubeInput => cubeInput.rep);
|
||||
console.log(polycubes);
|
||||
solving = true;
|
||||
worker.postMessage({type: 'wasm', polycubes, dims: $somaDimension});
|
||||
}
|
||||
|
||||
function genTooltip() {
|
||||
let messages = [];
|
||||
if (!enoughSubcubes) {
|
||||
messages.push(`You have not input enough subcubes to form a cube with a side length of ${$somaDimension}. Needed: ${$somaDimension**3}, current: ${size}.`);
|
||||
}
|
||||
if (!noEmpties) {
|
||||
messages.push("You have left some of the polycube inputs empty. Remove them to solve.");
|
||||
}
|
||||
return messages.join("\n");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<h1>Somaesque</h1>
|
||||
<div class="widgets">
|
||||
<div class="option">
|
||||
<p>Dimension:</p>
|
||||
<div class="choice">
|
||||
<button
|
||||
class:selected={$somaDimension === 2}
|
||||
on:click={() => somaDimension.set(2)}
|
||||
disabled={$somaDimension === 2}>
|
||||
2
|
||||
</button>
|
||||
<button
|
||||
class:selected={$somaDimension === 3}
|
||||
on:click={() => somaDimension.set(3)}
|
||||
disabled={$somaDimension === 3}>
|
||||
3
|
||||
</button>
|
||||
<button
|
||||
class:selected={$somaDimension === 4}
|
||||
on:click={() => somaDimension.set(4)}
|
||||
disabled={$somaDimension === 4}>
|
||||
4
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<p>Cubes:</p>
|
||||
<div class="choice">
|
||||
<p>{numCubes}</p>
|
||||
<button on:click={polycubes.removeCube} disabled={$isMinPolycubes}>-</button>
|
||||
<button on:click={polycubes.addCube} disabled={$isMaxPolycubes}>+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option">
|
||||
<button
|
||||
class="solve"
|
||||
on:click={solveWasm}
|
||||
title="{genTooltip(enoughSubcubes, noEmpties, size)}"
|
||||
disabled="{solving || !readyToSolve}">
|
||||
{solving ? "Solving..." : "Solve!"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Solutions: {$solutions.length}</h3>
|
||||
<SolutionList/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
p {
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
.choice {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 1em;
|
||||
}
|
||||
button {
|
||||
display: inline-block;
|
||||
background-color: #999999;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
border-style: none;
|
||||
}
|
||||
.selected:disabled {
|
||||
color: white;
|
||||
background-color: #ff3e00;
|
||||
}
|
||||
button:hover:not(:disabled) {
|
||||
cursor: pointer;
|
||||
background-color: #c1c1c1;
|
||||
}
|
||||
button:disabled {
|
||||
color: #a7a7a7;
|
||||
background-color: #616161;
|
||||
}
|
||||
button.solve {
|
||||
width: auto;
|
||||
color: white;
|
||||
background-color: #ff3e00;
|
||||
font-size: 2em;
|
||||
border-radius: 0.5em;
|
||||
border-style: none;
|
||||
margin: 0;
|
||||
}
|
||||
button.solve:disabled {
|
||||
width: auto;
|
||||
color: #999999;
|
||||
background-color: #a36754;
|
||||
font-size: 2em;
|
||||
}
|
||||
.container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
background-color: #333333;
|
||||
padding: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
.widgets {
|
||||
width: 100%;
|
||||
}
|
||||
.widgets:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
.widgets:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.widgets > * {
|
||||
padding-top: 1em;
|
||||
padding-bottom: 1em;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
color: #ff3e00;
|
||||
font-size: 3em;
|
||||
font-weight: 100;
|
||||
}
|
||||
</style>
|
||||
91
src/ui/Solution2D.svelte
Normal file
91
src/ui/Solution2D.svelte
Normal file
@@ -0,0 +1,91 @@
|
||||
<script lang="ts">
|
||||
import {polycubes, activeSolution, showingSolution, solutions} from "../store";
|
||||
import SomaSolution from "../SomaSolution";
|
||||
|
||||
$: solutionDisplayed = $solutions[$activeSolution];
|
||||
$: dimension = (solutionDisplayed && solutionDisplayed.getDims?.()[0]) ?? 3;
|
||||
|
||||
function colorAt(soln: SomaSolution, x: number, y: number, z: number) {
|
||||
return $polycubes[soln.at(z, dimension-1-x, y)].color;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $activeSolution !== null}
|
||||
<div
|
||||
class="cube"
|
||||
class:active={$showingSolution}
|
||||
style="--dimension: {dimension};"
|
||||
on:click={() => showingSolution.set(true)}
|
||||
>
|
||||
<h1>Solution #{$activeSolution + 1}</h1>
|
||||
<div class="center">
|
||||
{#each {length: dimension} as _, x}
|
||||
<div class="layer">
|
||||
{#each {length: dimension} as _, y}
|
||||
<div class="row">
|
||||
{#each {length: dimension} as _, z}
|
||||
<div
|
||||
class="cell"
|
||||
style="background-color:{colorAt(solutionDisplayed, x, y, z)}; border-color: {colorAt(solutionDisplayed, x, y, z)}"
|
||||
class:filled={true}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
* {
|
||||
--cell-size: 30px;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
.cube.active {
|
||||
border: 3px solid #ff3e00;
|
||||
}
|
||||
.cube:hover:not(.active) {
|
||||
transform: scale(1.03);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
.cube {
|
||||
border-radius: 1em;
|
||||
border: 3px solid transparent;
|
||||
background-color: #666666;
|
||||
cursor: pointer;
|
||||
transition: transform 200ms;
|
||||
padding: 1em 2em 1em 2em;
|
||||
user-select: none;
|
||||
}
|
||||
.cell {
|
||||
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
border: 1px var(--color) solid;
|
||||
border-radius: 4px;
|
||||
height: var(--cell-size);
|
||||
width: var(--cell-size);
|
||||
background-color: #aaaaaa;
|
||||
margin: 1px;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
.layer {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.filled {
|
||||
background: var(--color);
|
||||
}
|
||||
</style>
|
||||
44
src/ui/SolutionList.svelte
Normal file
44
src/ui/SolutionList.svelte
Normal file
@@ -0,0 +1,44 @@
|
||||
<script lang="ts">
|
||||
import {solutions, activeSolution, showingSolution} from "../store";
|
||||
|
||||
function selectSolution(i: number) {
|
||||
activeSolution.set(i);
|
||||
showingSolution.set(true);
|
||||
}
|
||||
</script>
|
||||
|
||||
<ul>
|
||||
{#if $solutions.length === 0}
|
||||
<li>No solutions yet...</li>
|
||||
{/if}
|
||||
{#each $solutions as soln, i}
|
||||
<li class:active={$activeSolution === i} on:click={() => selectSolution(i)}>
|
||||
Solution #{i + 1}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<style>
|
||||
li:hover:not(.active) {
|
||||
background-color: #aaaaaa;
|
||||
}
|
||||
li {
|
||||
transition: background-color 100ms;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
height: 2em;
|
||||
line-height: 2em;
|
||||
}
|
||||
ul {
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
flex: 1;
|
||||
padding: 0.5em;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
background-color: #666;
|
||||
}
|
||||
.active {
|
||||
background-color: #ff3e00;
|
||||
}
|
||||
</style>
|
||||
54
src/ui/SolutionViewer.svelte
Normal file
54
src/ui/SolutionViewer.svelte
Normal file
@@ -0,0 +1,54 @@
|
||||
<script lang="ts">
|
||||
import PolycubeScene from "./threedee/PolycubeScene.ts";
|
||||
import {onMount} from "svelte";
|
||||
import {polycubes, somaDimension, selectedCube, solutions, activeSolution, showingSolution} from "../store";
|
||||
import Solution2D from "./Solution2D.svelte";
|
||||
|
||||
$: cube = $polycubes[$selectedCube];
|
||||
$: soln = $solutions[$activeSolution];
|
||||
let el: HTMLCanvasElement;
|
||||
let threeTest: PolycubeScene;
|
||||
let loaded: boolean = false;
|
||||
|
||||
onMount(() => {
|
||||
threeTest = new PolycubeScene(el, () => loaded = true, console.log);
|
||||
});
|
||||
|
||||
$: {
|
||||
if (loaded) {
|
||||
if ($showingSolution) {
|
||||
const colorMap = {};
|
||||
$polycubes.forEach((polycube, i) => colorMap[i] = polycube.color);
|
||||
threeTest?.showSolution(soln, colorMap);
|
||||
} else {
|
||||
threeTest?.showPolycube(cube.rep, $somaDimension, cube.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="top">
|
||||
<div class="soln2d-container">
|
||||
<Solution2D/>
|
||||
</div>
|
||||
<canvas
|
||||
bind:this={el}
|
||||
width="640"
|
||||
height="480"
|
||||
></canvas>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.top {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
}
|
||||
.soln2d-container {
|
||||
display: inline-block;
|
||||
}
|
||||
canvas {
|
||||
display: inline-block;
|
||||
border-radius: 1em;
|
||||
}
|
||||
</style>
|
||||
144
src/ui/threedee/GeometryManager.ts
Normal file
144
src/ui/threedee/GeometryManager.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import type * as THREE from "three";
|
||||
import {OBJLoader} from "three/examples/jsm/loaders/OBJLoader";
|
||||
|
||||
export enum SomaesqueGeometry {
|
||||
c000000 = 'c000000',
|
||||
c000001 = 'c000001',
|
||||
c000011 = 'c000011',
|
||||
c000111 = 'c000111',
|
||||
c001001 = 'c001001',
|
||||
c001011 = 'c001011',
|
||||
c001111 = 'c001111',
|
||||
c011011 = 'c011011',
|
||||
}
|
||||
|
||||
const MESH_ROT_MAP = [
|
||||
"000000", // 000000
|
||||
"000001", // 000001
|
||||
"000001z", // 000010
|
||||
"000011", // 000011
|
||||
"000001b", // 000100
|
||||
"000011x", // 000101
|
||||
"000011b", // 000110
|
||||
"000111", // 000111
|
||||
"000001yy", // 001000
|
||||
"001001", // 001001
|
||||
"000011yy", // 001010
|
||||
"001011", // 001011
|
||||
"000011zx", // 001100
|
||||
"001011x", // 001101
|
||||
"000111y", // 001110
|
||||
"001111", // 001111
|
||||
"000001ba", // 010000
|
||||
"000011xx", // 010001
|
||||
"001001ya", // 010010
|
||||
"001011zb", // 010011
|
||||
"000011yx", // 010100
|
||||
"000111x", // 010101
|
||||
"001011zb", // 010110
|
||||
"001111zb", // 010111
|
||||
"000011zz", // 011000
|
||||
"001011xx", // 011001
|
||||
"001011z", // 011010
|
||||
"011011", // 011011
|
||||
"000111yx", // 011100
|
||||
"001111x", // 011101
|
||||
"001111yx", // 011110
|
||||
"011011", // 011111
|
||||
"000001y", // 100000
|
||||
"000011a", // 100001
|
||||
"000011b", // 100010
|
||||
"000111b", // 100011
|
||||
"001001b", // 100100
|
||||
"001011yc", // 100101 //---
|
||||
"001011b", // 100110
|
||||
"001111b", // 100111
|
||||
"000011ccx",// 101000
|
||||
"001011a", // 101001
|
||||
"000111bb", // 101010
|
||||
"001111a", // 101011
|
||||
"001011yz", // 101100 //---
|
||||
"011011x", // 101101 //---
|
||||
"001111y", // 101110 //---
|
||||
"011011", // 101111
|
||||
"000011xz", // 110000 //---
|
||||
"000111ba", // 110001 //---
|
||||
"001011ba", // 110010 //---
|
||||
"001111ba", // 110011
|
||||
"001011bx", // 110100
|
||||
"001111bx", // 110101
|
||||
"011011b", // 110110
|
||||
"011011", // 110111
|
||||
"000111bba",// 111000
|
||||
"001111xx", // 111001
|
||||
"001111ya", // 111010
|
||||
"011011", // 111011
|
||||
"001111yz", // 111100
|
||||
"011011", // 111101
|
||||
"011011", // 111110
|
||||
"011011", // 111111
|
||||
];
|
||||
|
||||
const ROT_CODE_MAP = {
|
||||
x(mesh: THREE.Object3D) { mesh.rotateX(Math.PI/2); },
|
||||
y(mesh: THREE.Object3D) { mesh.rotateY(Math.PI/2); },
|
||||
z(mesh: THREE.Object3D) { mesh.rotateZ(Math.PI/2); },
|
||||
a(mesh: THREE.Object3D) { mesh.rotateX(-Math.PI/2); },
|
||||
b(mesh: THREE.Object3D) { mesh.rotateY(-Math.PI/2); },
|
||||
c(mesh: THREE.Object3D) { mesh.rotateZ(-Math.PI/2); },
|
||||
} as const;
|
||||
|
||||
type GeomRecord = Record<SomaesqueGeometry, THREE.BufferGeometry>;
|
||||
|
||||
export default class GeometryManager {
|
||||
private readonly root: string = "";
|
||||
private geometryRecord: GeomRecord = {} as GeomRecord;
|
||||
constructor(root: string, onReadyCb: (error?: string) => any) {
|
||||
this.root = root;
|
||||
Promise.allSettled(Object.keys(SomaesqueGeometry).map(geomId =>
|
||||
this.loadCubeGeometry(geomId as SomaesqueGeometry),
|
||||
)).then(() => onReadyCb()).catch((err) => onReadyCb(err));
|
||||
}
|
||||
|
||||
private async loadCubeGeometry(id: SomaesqueGeometry): Promise<THREE.BufferGeometry> {
|
||||
const onLoaded = (obj: THREE.Group, resolve: (geom: THREE.BufferGeometry) => any) => {
|
||||
const geom = (obj.children[0] as THREE.Mesh).geometry;
|
||||
this.geometryRecord[id] = geom;
|
||||
resolve(geom);
|
||||
};
|
||||
const load = (resolve: (geom: THREE.BufferGeometry) => any, reject: (err: string) => any) => {
|
||||
const loader = new OBJLoader();
|
||||
loader.load(
|
||||
`${this.root}${id}.obj`,
|
||||
obj => onLoaded(obj, resolve),
|
||||
() => {},
|
||||
(err) => reject(`Error loading OBJ file: ${err}`),
|
||||
);
|
||||
};
|
||||
return new Promise(load);
|
||||
}
|
||||
|
||||
retrieve(geometry: SomaesqueGeometry) {
|
||||
let requestedGeom = this.geometryRecord[geometry];
|
||||
if (requestedGeom) {
|
||||
return requestedGeom;
|
||||
} else {
|
||||
throw new Error(`Geometry with id: ${geometry} does not exist!`);
|
||||
}
|
||||
}
|
||||
|
||||
retrieveCubeGeometry(neighbourProfile: number) {
|
||||
return this.geometryRecord.c000000;
|
||||
// let requestedGeom = this.geometryRecord[`c${MESH_ROT_MAP[neighbourProfile].substr(0, 6)}`];
|
||||
// const rotations = MESH_ROT_MAP[neighbourProfile].substr(6);
|
||||
// if (!requestedGeom) {
|
||||
// throw new Error(`No similar cube found for the neighbour profile: ${neighbourProfile}`)
|
||||
// } else if (rotations) {
|
||||
// requestedGeom = requestedGeom.clone();
|
||||
// for (let i = 0; i < rotations.length; i++) {
|
||||
// ROT_CODE_MAP[rotations[i]](requestedGeom);
|
||||
// }
|
||||
// }
|
||||
// return requestedGeom;
|
||||
}
|
||||
}
|
||||
91
src/ui/threedee/PolycubeMesh.ts
Normal file
91
src/ui/threedee/PolycubeMesh.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import * as THREE from "three";
|
||||
import type VoxelSpace from "../../VoxelSpace";
|
||||
import type GeometryManager from "./GeometryManager";
|
||||
|
||||
export default class PolycubeMesh {
|
||||
private static geometryManager: GeometryManager;
|
||||
private group: THREE.Group;
|
||||
private meshes: THREE.Mesh[] = [];
|
||||
private currentPolycube: bigint = 0n;
|
||||
private material: THREE.MeshPhongMaterial;
|
||||
private numActiveCubes: number = 0;
|
||||
private flyDirection: THREE.Vector3 = new THREE.Vector3();
|
||||
|
||||
constructor(polycube: VoxelSpace, color: string) {
|
||||
this.material = new THREE.MeshPhongMaterial({color: 'red', shininess: 100, reflectivity: 100});
|
||||
this.group = new THREE.Group();
|
||||
this.swapColor(color);
|
||||
this.swapPolycube(polycube);
|
||||
}
|
||||
|
||||
static setManager(manager: GeometryManager) {
|
||||
PolycubeMesh.geometryManager = manager;
|
||||
}
|
||||
|
||||
swapColor(color: string) {
|
||||
this.material.color.set(color);
|
||||
}
|
||||
|
||||
swapPolycube(polycube: VoxelSpace) {
|
||||
if (polycube.getRaw() === this.currentPolycube) {
|
||||
return;
|
||||
}
|
||||
this.numActiveCubes = polycube.size();
|
||||
this.meshes = [];
|
||||
this.group.clear();
|
||||
this.group.position.set(0, 0, 0);
|
||||
polycube.forEachCell((val: boolean, x: number, y: number, z: number) => {
|
||||
if (val) {
|
||||
this.addCube(polycube, x, y, z);
|
||||
}
|
||||
});
|
||||
this.currentPolycube = polycube.getRaw();
|
||||
this.flyDirection = this.middlePosOfGroup().normalize();
|
||||
}
|
||||
|
||||
private addCube(refPolycube: VoxelSpace, x: number, y: number, z: number) {
|
||||
const dims = refPolycube.getDims();
|
||||
const neighbourProfile = refPolycube.getDirectNeighbourProfile(x, y, z);
|
||||
const mesh = new THREE.Mesh(
|
||||
PolycubeMesh.geometryManager.retrieveCubeGeometry(neighbourProfile),
|
||||
this.material
|
||||
);
|
||||
mesh.position.set(
|
||||
-((dims[0] - 1)/2) + x,
|
||||
-((dims[1] - 1)/2) + y,
|
||||
-((dims[2] - 1)/2) + z,
|
||||
);
|
||||
this.meshes.push(mesh);
|
||||
this.group.add(mesh);
|
||||
}
|
||||
|
||||
center() {
|
||||
const mid = this.middlePosOfGroup();
|
||||
this.group.children.forEach(child => child.position.sub(mid));
|
||||
}
|
||||
|
||||
private middlePosOfGroup() {
|
||||
return this.group.children.reduce(
|
||||
(prev, child) => prev.add(child.position),
|
||||
new THREE.Vector3()
|
||||
).divideScalar(this.group.children.length);
|
||||
}
|
||||
|
||||
flyBy(factor: number) {
|
||||
const movementVector = this.flyDirection.clone().multiplyScalar(factor);
|
||||
const targetPos = this.group.position.clone().add(movementVector);
|
||||
const willMoveBehindStartingPosition = targetPos.clone().sub(this.flyDirection).dot(this.flyDirection) < -1;
|
||||
if (!willMoveBehindStartingPosition) {
|
||||
const distanceFromOrigin = targetPos.distanceTo(new THREE.Vector3());
|
||||
if (distanceFromOrigin >= 0 && distanceFromOrigin < 3) {
|
||||
this.group.position.add(movementVector);
|
||||
}
|
||||
} else {
|
||||
this.group.position.set(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
asObj3D(): THREE.Object3D {
|
||||
return this.group;
|
||||
}
|
||||
}
|
||||
87
src/ui/threedee/PolycubeScene.ts
Normal file
87
src/ui/threedee/PolycubeScene.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import * as THREE from 'three';
|
||||
import type SomaSolution from "../../SomaSolution";
|
||||
import RotationControl from "./RotationControl";
|
||||
import PolycubeMesh from "./PolycubeMesh";
|
||||
import VoxelSpace, {DimensionDef} from "../../VoxelSpace";
|
||||
import GeometryManager from "./GeometryManager";
|
||||
|
||||
export default class PolycubeScene {
|
||||
private renderer: THREE.WebGLRenderer;
|
||||
private camera: THREE.Camera;
|
||||
private mainScene: THREE.Scene;
|
||||
private polycubeMeshes: PolycubeMesh[] = [];
|
||||
private controls: RotationControl;
|
||||
private light: THREE.Light;
|
||||
private cubeScene: THREE.Scene;
|
||||
private geomManager: GeometryManager;
|
||||
|
||||
constructor(el: HTMLCanvasElement, onReady: () => any, onError: (err: Error) => any) {
|
||||
this.init(el).then(onReady).catch(onError);
|
||||
}
|
||||
|
||||
private async init(el: HTMLCanvasElement) {
|
||||
this.renderer = new THREE.WebGLRenderer({canvas: el, antialias: true});
|
||||
this.setupCamera(el.clientWidth / el.clientHeight);
|
||||
this.setupLight();
|
||||
this.mainScene = new THREE.Scene();
|
||||
this.cubeScene = new THREE.Scene();
|
||||
this.mainScene.add(this.cubeScene, this.camera, this.light);
|
||||
this.cubeScene.rotateX(Math.PI/4);
|
||||
this.cubeScene.rotateY(Math.PI/4);
|
||||
this.controls = new RotationControl(this.cubeScene, this.polycubeMeshes, this.camera, el);
|
||||
this.geomManager = await new GeometryManager('../resources/', () => {
|
||||
requestAnimationFrame((timestamp) => this.render(timestamp));
|
||||
});
|
||||
PolycubeMesh.setManager(this.geomManager);
|
||||
}
|
||||
|
||||
private setupCamera(aspect: number) {
|
||||
const fov = 60;
|
||||
const near = 0.1;
|
||||
const far = 15;
|
||||
this.camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
|
||||
this.camera.position.z = 6;
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
}
|
||||
|
||||
private showPolycube(polycube: bigint, dims: number, color: string) {
|
||||
this.controls.disableFly();
|
||||
const voxelSpace = new VoxelSpace(0, [dims, dims, dims], polycube, true);
|
||||
this.clearScene();
|
||||
this.addPolycube(voxelSpace, color);
|
||||
this.polycubeMeshes[0].center();
|
||||
}
|
||||
|
||||
private showSolution(solution: SomaSolution, colorMap: Record<number, string>) {
|
||||
this.controls.enableFly();
|
||||
this.clearScene();
|
||||
const pieces = solution.getPieces();
|
||||
for (let i = 0; i < pieces.length; i++) {
|
||||
this.addPolycube(pieces[i], colorMap[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private clearScene() {
|
||||
this.polycubeMeshes.splice(0, this.polycubeMeshes.length);
|
||||
this.cubeScene.clear();
|
||||
}
|
||||
|
||||
private addPolycube(voxelSpace: VoxelSpace, color: string) {
|
||||
const newMesh = new PolycubeMesh(voxelSpace, color);
|
||||
this.polycubeMeshes.push(newMesh);
|
||||
this.cubeScene.add(newMesh.asObj3D());
|
||||
}
|
||||
|
||||
private setupLight() {
|
||||
const color = 0xFFFFFF;
|
||||
const intensity = 1;
|
||||
this.light = new THREE.DirectionalLight(color, intensity);
|
||||
this.light.position.set(4, 6, 24);
|
||||
this.light.lookAt(0,0,0);
|
||||
}
|
||||
|
||||
private render(time: number) {
|
||||
this.renderer.render(this.mainScene, this.camera);
|
||||
requestAnimationFrame((time: number) => this.render(time));
|
||||
}
|
||||
}
|
||||
77
src/ui/threedee/RotationControl.ts
Normal file
77
src/ui/threedee/RotationControl.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
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 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, 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);
|
||||
this.xAxis.divideScalar(Math.sqrt(this.xAxis.getComponent(0)**2 + this.xAxis.getComponent(1)**2 + this.xAxis.getComponent(2)**2));
|
||||
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.dragging) {
|
||||
this.lastX = event.x;
|
||||
this.lastY = event.y;
|
||||
this.dragging = true;
|
||||
}
|
||||
});
|
||||
window.addEventListener('mousemove', (ev) => this.handleMove(ev));
|
||||
window.addEventListener('mouseup', () => this.dragging = false);
|
||||
}
|
||||
|
||||
private handleMove(event: MouseEvent) {
|
||||
if (this.dragging) {
|
||||
const xDiff = event.movementX * RotationControls.ROTATION_FACTOR;
|
||||
const yDiff = event.movementY * RotationControls.ROTATION_FACTOR;
|
||||
this.object.rotateOnAxis(this.yAxis, xDiff);
|
||||
this.object.rotateOnWorldAxis(this.xAxis, yDiff);
|
||||
}
|
||||
}
|
||||
|
||||
private handleScroll(event: WheelEvent) {
|
||||
if (this.flyingEnabled && this.hovered) {
|
||||
for (const fliable of this.fliables) {
|
||||
const direction = event.deltaY / Math.abs(event.deltaY);
|
||||
fliable.flyBy(direction / 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enableFly() {
|
||||
this.flyingEnabled = true;
|
||||
}
|
||||
|
||||
disableFly() {
|
||||
this.flyingEnabled = false;
|
||||
}
|
||||
|
||||
private static isMesh(object: THREE.Object3D): object is THREE.Mesh {
|
||||
return (object as THREE.Mesh).isMesh;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user