great stuff

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

57
src/ui/App.svelte Normal file
View 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
View 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
View File

@@ -0,0 +1,53 @@
<script lang="ts">
import {polycubes} from "../store";
import CubeInput from "./CubeInput.svelte";
import SolutionViewer from "./SolutionViewer.svelte";
$: numCubes = $polycubes.length;
</script>
<div class="viewport">
<div class="input-container">
{#each {length: numCubes} as _, cubeNo}
<div class="cube-input">
<div class="padder">
<CubeInput
cubeNo={cubeNo}
/>
</div>
</div>
{/each}
</div>
<div class="threedee">
<SolutionViewer/>
</div>
</div>
<style>
.threedee {
flex: 1 1 auto;
display: flex;
flex-direction: column;
align-content: center;
justify-content: center;
text-align: center;
}
.padder {
padding: 1em;
}
.cube-input {
margin: auto;
}
.input-container {
flex: 0 1 fit-content;
overflow-x: scroll;
display: flex;
flex-flow: row;
}
.viewport {
display: flex;
height: 100%;
align-content: center;
justify-content: center;
flex-direction: column;
}
</style>

196
src/ui/Sidebar.svelte Normal file
View File

@@ -0,0 +1,196 @@
<script lang="ts">
import {isMaxPolycubes, isMinPolycubes, somaDimension, polycubes, solutions} from "../store";
import SomaSolution from "../SomaSolution";
import SolutionList from "./SolutionList.svelte";
import VoxelSpace from "../VoxelSpace";
$: numCubes = $polycubes.length;
$: cubes = $polycubes;
let noEmpties: boolean;
let enoughSubcubes: boolean;
let readyToSolve: boolean;
let size: number;
$: {
const dim = $somaDimension as number;
const polycubes: VoxelSpace[] = cubes.map(cubeInput => new VoxelSpace(0, [dim, dim, dim], cubeInput.rep));
size = polycubes.reduce((prev, cube) => cube.size() + prev, 0);
noEmpties = polycubes.reduce((prev, cube) => (cube.size() !== 0) && prev, true);
enoughSubcubes = size === dim**3;
readyToSolve = size === dim**3 && noEmpties;
}
let solving = false;
const worker = new Worker('../solver/main.js', {type: "module"});
async function respondWasm(event: MessageEvent) {
const dim = $somaDimension as number;
solutions.set(event.data.map((wasmSolution) => {
const solnObj = new SomaSolution(dim);
const spaceReps = wasmSolution.split(",");
for (let i = 0; i < spaceReps.length; i++) {
solnObj.addSpace(new VoxelSpace(i, [dim, dim, dim], BigInt(parseInt(spaceReps[i]))));
}
return solnObj;
}));
solving = false;
}
function respondJs(event: MessageEvent) {
solutions.set(event.data.map(solnData => {
const solution = new SomaSolution(solnData.dim);
solnData.solutionSpaces.forEach((voxelSpace, i) => solution.addSpace(new VoxelSpace(i, [solnData.dim, solnData.dim, solnData.dim], voxelSpace.space)));
return solution;
}));
solving = false;
}
function solveJs() {
worker.onmessage = (e) => respondJs(e);
const polycubes = cubes.map(cubeInput => cubeInput.rep);
solving = true;
worker.postMessage({type: 'js', polycubes, dims: $somaDimension});
}
function solveWasm() {
worker.onmessage = (e) => respondWasm(e);
const polycubes = cubes.map(cubeInput => cubeInput.rep);
console.log(polycubes);
solving = true;
worker.postMessage({type: 'wasm', polycubes, dims: $somaDimension});
}
function genTooltip() {
let messages = [];
if (!enoughSubcubes) {
messages.push(`You have not input enough subcubes to form a cube with a side length of ${$somaDimension}. Needed: ${$somaDimension**3}, current: ${size}.`);
}
if (!noEmpties) {
messages.push("You have left some of the polycube inputs empty. Remove them to solve.");
}
return messages.join("\n");
}
</script>
<div class="container">
<h1>Somaesque</h1>
<div class="widgets">
<div class="option">
<p>Dimension:</p>
<div class="choice">
<button
class:selected={$somaDimension === 2}
on:click={() => somaDimension.set(2)}
disabled={$somaDimension === 2}>
2
</button>
<button
class:selected={$somaDimension === 3}
on:click={() => somaDimension.set(3)}
disabled={$somaDimension === 3}>
3
</button>
<button
class:selected={$somaDimension === 4}
on:click={() => somaDimension.set(4)}
disabled={$somaDimension === 4}>
4
</button>
</div>
</div>
<div class="option">
<p>Cubes:</p>
<div class="choice">
<p>{numCubes}</p>
<button on:click={polycubes.removeCube} disabled={$isMinPolycubes}>-</button>
<button on:click={polycubes.addCube} disabled={$isMaxPolycubes}>+</button>
</div>
</div>
<div class="option">
<button
class="solve"
on:click={solveWasm}
title="{genTooltip(enoughSubcubes, noEmpties, size)}"
disabled="{solving || !readyToSolve}">
{solving ? "Solving..." : "Solve!"}
</button>
</div>
</div>
<h3>Solutions: {$solutions.length}</h3>
<SolutionList/>
</div>
<style>
p {
margin: 0;
display: inline-block;
}
.choice {
display: block;
text-align: center;
margin-top: 1em;
}
button {
display: inline-block;
background-color: #999999;
width: 2em;
height: 2em;
border-style: none;
}
.selected:disabled {
color: white;
background-color: #ff3e00;
}
button:hover:not(:disabled) {
cursor: pointer;
background-color: #c1c1c1;
}
button:disabled {
color: #a7a7a7;
background-color: #616161;
}
button.solve {
width: auto;
color: white;
background-color: #ff3e00;
font-size: 2em;
border-radius: 0.5em;
border-style: none;
margin: 0;
}
button.solve:disabled {
width: auto;
color: #999999;
background-color: #a36754;
font-size: 2em;
}
.container {
display: flex;
align-items: center;
flex-direction: column;
height: 100vh;
overflow: hidden;
background-color: #333333;
padding: 1em;
text-align: center;
}
.widgets {
width: 100%;
}
.widgets:first-child {
padding-top: 0;
}
.widgets:last-child {
padding-bottom: 0;
}
.widgets > * {
padding-top: 1em;
padding-bottom: 1em;
}
h1 {
margin: 0;
color: #ff3e00;
font-size: 3em;
font-weight: 100;
}
</style>

91
src/ui/Solution2D.svelte Normal file
View File

@@ -0,0 +1,91 @@
<script lang="ts">
import {polycubes, activeSolution, showingSolution, solutions} from "../store";
import SomaSolution from "../SomaSolution";
$: solutionDisplayed = $solutions[$activeSolution];
$: dimension = (solutionDisplayed && solutionDisplayed.getDims?.()[0]) ?? 3;
function colorAt(soln: SomaSolution, x: number, y: number, z: number) {
return $polycubes[soln.at(z, dimension-1-x, y)].color;
}
</script>
{#if $activeSolution !== null}
<div
class="cube"
class:active={$showingSolution}
style="--dimension: {dimension};"
on:click={() => showingSolution.set(true)}
>
<h1>Solution #{$activeSolution + 1}</h1>
<div class="center">
{#each {length: dimension} as _, x}
<div class="layer">
{#each {length: dimension} as _, y}
<div class="row">
{#each {length: dimension} as _, z}
<div
class="cell"
style="background-color:{colorAt(solutionDisplayed, x, y, z)}; border-color: {colorAt(solutionDisplayed, x, y, z)}"
class:filled={true}
/>
{/each}
</div>
{/each}
</div>
{/each}
</div>
</div>
{/if}
<style>
* {
--cell-size: 30px;
}
.center {
text-align: center;
}
h1 {
font-size: 1em;
text-align: center;
}
.cube.active {
border: 3px solid #ff3e00;
}
.cube:hover:not(.active) {
transform: scale(1.03);
filter: brightness(1.1);
}
.cube {
border-radius: 1em;
border: 3px solid transparent;
background-color: #666666;
cursor: pointer;
transition: transform 200ms;
padding: 1em 2em 1em 2em;
user-select: none;
}
.cell {
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
text-align: center;
cursor: pointer;
display: inline-block;
border: 1px var(--color) solid;
border-radius: 4px;
height: var(--cell-size);
width: var(--cell-size);
background-color: #aaaaaa;
margin: 1px;
}
.row {
display: flex;
margin: 0;
justify-content: center;
}
.layer {
margin-top: 10px;
}
.filled {
background: var(--color);
}
</style>

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import {solutions, activeSolution, showingSolution} from "../store";
function selectSolution(i: number) {
activeSolution.set(i);
showingSolution.set(true);
}
</script>
<ul>
{#if $solutions.length === 0}
<li>No solutions yet...</li>
{/if}
{#each $solutions as soln, i}
<li class:active={$activeSolution === i} on:click={() => selectSolution(i)}>
Solution #{i + 1}
</li>
{/each}
</ul>
<style>
li:hover:not(.active) {
background-color: #aaaaaa;
}
li {
transition: background-color 100ms;
cursor: pointer;
list-style: none;
height: 2em;
line-height: 2em;
}
ul {
width: 100%;
overflow-y: scroll;
flex: 1;
padding: 0.5em;
margin: 0;
text-align: center;
background-color: #666;
}
.active {
background-color: #ff3e00;
}
</style>

View File

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

View File

@@ -0,0 +1,144 @@
import type * as THREE from "three";
import {OBJLoader} from "three/examples/jsm/loaders/OBJLoader";
export enum SomaesqueGeometry {
c000000 = 'c000000',
c000001 = 'c000001',
c000011 = 'c000011',
c000111 = 'c000111',
c001001 = 'c001001',
c001011 = 'c001011',
c001111 = 'c001111',
c011011 = 'c011011',
}
const MESH_ROT_MAP = [
"000000", // 000000
"000001", // 000001
"000001z", // 000010
"000011", // 000011
"000001b", // 000100
"000011x", // 000101
"000011b", // 000110
"000111", // 000111
"000001yy", // 001000
"001001", // 001001
"000011yy", // 001010
"001011", // 001011
"000011zx", // 001100
"001011x", // 001101
"000111y", // 001110
"001111", // 001111
"000001ba", // 010000
"000011xx", // 010001
"001001ya", // 010010
"001011zb", // 010011
"000011yx", // 010100
"000111x", // 010101
"001011zb", // 010110
"001111zb", // 010111
"000011zz", // 011000
"001011xx", // 011001
"001011z", // 011010
"011011", // 011011
"000111yx", // 011100
"001111x", // 011101
"001111yx", // 011110
"011011", // 011111
"000001y", // 100000
"000011a", // 100001
"000011b", // 100010
"000111b", // 100011
"001001b", // 100100
"001011yc", // 100101 //---
"001011b", // 100110
"001111b", // 100111
"000011ccx",// 101000
"001011a", // 101001
"000111bb", // 101010
"001111a", // 101011
"001011yz", // 101100 //---
"011011x", // 101101 //---
"001111y", // 101110 //---
"011011", // 101111
"000011xz", // 110000 //---
"000111ba", // 110001 //---
"001011ba", // 110010 //---
"001111ba", // 110011
"001011bx", // 110100
"001111bx", // 110101
"011011b", // 110110
"011011", // 110111
"000111bba",// 111000
"001111xx", // 111001
"001111ya", // 111010
"011011", // 111011
"001111yz", // 111100
"011011", // 111101
"011011", // 111110
"011011", // 111111
];
const ROT_CODE_MAP = {
x(mesh: THREE.Object3D) { mesh.rotateX(Math.PI/2); },
y(mesh: THREE.Object3D) { mesh.rotateY(Math.PI/2); },
z(mesh: THREE.Object3D) { mesh.rotateZ(Math.PI/2); },
a(mesh: THREE.Object3D) { mesh.rotateX(-Math.PI/2); },
b(mesh: THREE.Object3D) { mesh.rotateY(-Math.PI/2); },
c(mesh: THREE.Object3D) { mesh.rotateZ(-Math.PI/2); },
} as const;
type GeomRecord = Record<SomaesqueGeometry, THREE.BufferGeometry>;
export default class GeometryManager {
private readonly root: string = "";
private geometryRecord: GeomRecord = {} as GeomRecord;
constructor(root: string, onReadyCb: (error?: string) => any) {
this.root = root;
Promise.allSettled(Object.keys(SomaesqueGeometry).map(geomId =>
this.loadCubeGeometry(geomId as SomaesqueGeometry),
)).then(() => onReadyCb()).catch((err) => onReadyCb(err));
}
private async loadCubeGeometry(id: SomaesqueGeometry): Promise<THREE.BufferGeometry> {
const onLoaded = (obj: THREE.Group, resolve: (geom: THREE.BufferGeometry) => any) => {
const geom = (obj.children[0] as THREE.Mesh).geometry;
this.geometryRecord[id] = geom;
resolve(geom);
};
const load = (resolve: (geom: THREE.BufferGeometry) => any, reject: (err: string) => any) => {
const loader = new OBJLoader();
loader.load(
`${this.root}${id}.obj`,
obj => onLoaded(obj, resolve),
() => {},
(err) => reject(`Error loading OBJ file: ${err}`),
);
};
return new Promise(load);
}
retrieve(geometry: SomaesqueGeometry) {
let requestedGeom = this.geometryRecord[geometry];
if (requestedGeom) {
return requestedGeom;
} else {
throw new Error(`Geometry with id: ${geometry} does not exist!`);
}
}
retrieveCubeGeometry(neighbourProfile: number) {
return this.geometryRecord.c000000;
// let requestedGeom = this.geometryRecord[`c${MESH_ROT_MAP[neighbourProfile].substr(0, 6)}`];
// const rotations = MESH_ROT_MAP[neighbourProfile].substr(6);
// if (!requestedGeom) {
// throw new Error(`No similar cube found for the neighbour profile: ${neighbourProfile}`)
// } else if (rotations) {
// requestedGeom = requestedGeom.clone();
// for (let i = 0; i < rotations.length; i++) {
// ROT_CODE_MAP[rotations[i]](requestedGeom);
// }
// }
// return requestedGeom;
}
}

View File

@@ -0,0 +1,91 @@
import * as THREE from "three";
import type VoxelSpace from "../../VoxelSpace";
import type GeometryManager from "./GeometryManager";
export default class PolycubeMesh {
private static geometryManager: GeometryManager;
private group: THREE.Group;
private meshes: THREE.Mesh[] = [];
private currentPolycube: bigint = 0n;
private material: THREE.MeshPhongMaterial;
private numActiveCubes: number = 0;
private flyDirection: THREE.Vector3 = new THREE.Vector3();
constructor(polycube: VoxelSpace, color: string) {
this.material = new THREE.MeshPhongMaterial({color: 'red', shininess: 100, reflectivity: 100});
this.group = new THREE.Group();
this.swapColor(color);
this.swapPolycube(polycube);
}
static setManager(manager: GeometryManager) {
PolycubeMesh.geometryManager = manager;
}
swapColor(color: string) {
this.material.color.set(color);
}
swapPolycube(polycube: VoxelSpace) {
if (polycube.getRaw() === this.currentPolycube) {
return;
}
this.numActiveCubes = polycube.size();
this.meshes = [];
this.group.clear();
this.group.position.set(0, 0, 0);
polycube.forEachCell((val: boolean, x: number, y: number, z: number) => {
if (val) {
this.addCube(polycube, x, y, z);
}
});
this.currentPolycube = polycube.getRaw();
this.flyDirection = this.middlePosOfGroup().normalize();
}
private addCube(refPolycube: VoxelSpace, x: number, y: number, z: number) {
const dims = refPolycube.getDims();
const neighbourProfile = refPolycube.getDirectNeighbourProfile(x, y, z);
const mesh = new THREE.Mesh(
PolycubeMesh.geometryManager.retrieveCubeGeometry(neighbourProfile),
this.material
);
mesh.position.set(
-((dims[0] - 1)/2) + x,
-((dims[1] - 1)/2) + y,
-((dims[2] - 1)/2) + z,
);
this.meshes.push(mesh);
this.group.add(mesh);
}
center() {
const mid = this.middlePosOfGroup();
this.group.children.forEach(child => child.position.sub(mid));
}
private middlePosOfGroup() {
return this.group.children.reduce(
(prev, child) => prev.add(child.position),
new THREE.Vector3()
).divideScalar(this.group.children.length);
}
flyBy(factor: number) {
const movementVector = this.flyDirection.clone().multiplyScalar(factor);
const targetPos = this.group.position.clone().add(movementVector);
const willMoveBehindStartingPosition = targetPos.clone().sub(this.flyDirection).dot(this.flyDirection) < -1;
if (!willMoveBehindStartingPosition) {
const distanceFromOrigin = targetPos.distanceTo(new THREE.Vector3());
if (distanceFromOrigin >= 0 && distanceFromOrigin < 3) {
this.group.position.add(movementVector);
}
} else {
this.group.position.set(0, 0, 0);
}
}
asObj3D(): THREE.Object3D {
return this.group;
}
}

View File

@@ -0,0 +1,87 @@
import * as THREE from 'three';
import type SomaSolution from "../../SomaSolution";
import RotationControl from "./RotationControl";
import PolycubeMesh from "./PolycubeMesh";
import VoxelSpace, {DimensionDef} from "../../VoxelSpace";
import GeometryManager from "./GeometryManager";
export default class PolycubeScene {
private renderer: THREE.WebGLRenderer;
private camera: THREE.Camera;
private mainScene: THREE.Scene;
private polycubeMeshes: PolycubeMesh[] = [];
private controls: RotationControl;
private light: THREE.Light;
private cubeScene: THREE.Scene;
private geomManager: GeometryManager;
constructor(el: HTMLCanvasElement, onReady: () => any, onError: (err: Error) => any) {
this.init(el).then(onReady).catch(onError);
}
private async init(el: HTMLCanvasElement) {
this.renderer = new THREE.WebGLRenderer({canvas: el, antialias: true});
this.setupCamera(el.clientWidth / el.clientHeight);
this.setupLight();
this.mainScene = new THREE.Scene();
this.cubeScene = new THREE.Scene();
this.mainScene.add(this.cubeScene, this.camera, this.light);
this.cubeScene.rotateX(Math.PI/4);
this.cubeScene.rotateY(Math.PI/4);
this.controls = new RotationControl(this.cubeScene, this.polycubeMeshes, this.camera, el);
this.geomManager = await new GeometryManager('../resources/', () => {
requestAnimationFrame((timestamp) => this.render(timestamp));
});
PolycubeMesh.setManager(this.geomManager);
}
private setupCamera(aspect: number) {
const fov = 60;
const near = 0.1;
const far = 15;
this.camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
this.camera.position.z = 6;
this.camera.lookAt(0, 0, 0);
}
private showPolycube(polycube: bigint, dims: number, color: string) {
this.controls.disableFly();
const voxelSpace = new VoxelSpace(0, [dims, dims, dims], polycube, true);
this.clearScene();
this.addPolycube(voxelSpace, color);
this.polycubeMeshes[0].center();
}
private showSolution(solution: SomaSolution, colorMap: Record<number, string>) {
this.controls.enableFly();
this.clearScene();
const pieces = solution.getPieces();
for (let i = 0; i < pieces.length; i++) {
this.addPolycube(pieces[i], colorMap[i]);
}
}
private clearScene() {
this.polycubeMeshes.splice(0, this.polycubeMeshes.length);
this.cubeScene.clear();
}
private addPolycube(voxelSpace: VoxelSpace, color: string) {
const newMesh = new PolycubeMesh(voxelSpace, color);
this.polycubeMeshes.push(newMesh);
this.cubeScene.add(newMesh.asObj3D());
}
private setupLight() {
const color = 0xFFFFFF;
const intensity = 1;
this.light = new THREE.DirectionalLight(color, intensity);
this.light.position.set(4, 6, 24);
this.light.lookAt(0,0,0);
}
private render(time: number) {
this.renderer.render(this.mainScene, this.camera);
requestAnimationFrame((time: number) => this.render(time));
}
}

View File

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