Made responsive
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
totalVolume
|
||||
} from "./store";
|
||||
|
||||
const worker = new Worker('./solver/main.js', {type: 'module'});
|
||||
const worker = new Worker('./worker.js', {type: 'module'});
|
||||
async function respondWasm(event: MessageEvent) {
|
||||
solutions.set(event.data.map((wasmSolution) => {
|
||||
const solnObj = new SomaSolution(somaDimX.currentVal(), somaDimY.currentVal(), somaDimZ.currentVal());
|
||||
|
||||
32
src/store.ts
32
src/store.ts
@@ -26,7 +26,14 @@ export const isMinPolycubes = derived(
|
||||
($polycubes: VoxelSpaceBigInt[]) => $polycubes.length <= 1
|
||||
);
|
||||
|
||||
export const examples = [
|
||||
type Save = {
|
||||
name: string,
|
||||
dimX: number,
|
||||
dimY: number,
|
||||
dimZ: number,
|
||||
cubes: {space: bigint | string, color: string}[],
|
||||
};
|
||||
const builtInExamples: Save[] = [
|
||||
{
|
||||
name: "Standard Soma Cube",
|
||||
dimX: 3,
|
||||
@@ -114,17 +121,34 @@ export const examples = [
|
||||
{space: 120n, color: "#0000ff"},
|
||||
],
|
||||
},
|
||||
].concat(deserealiseSaves());
|
||||
];
|
||||
|
||||
function deserealiseSaves() {
|
||||
export const examples = writable(builtInExamples.concat(deserealiseSaves()));
|
||||
|
||||
function deserealiseSaves(): Save[] {
|
||||
return localStorage.getItem("saves")?.split("@").map(save => JSON.parse(save)) ?? [];
|
||||
}
|
||||
|
||||
export function serialiseCurrentInput() {
|
||||
function serialiseCurrentInput(): Save {
|
||||
return {
|
||||
name: "",
|
||||
dimX: somaDimX.currentVal(),
|
||||
dimY: somaDimY.currentVal(),
|
||||
dimZ: somaDimZ.currentVal(),
|
||||
cubes: polycubes.currentVal().map(cube => ({space: cube.getRaw().toString(), color: cube.getColor()})),
|
||||
};
|
||||
}
|
||||
|
||||
export function save(name: string) {
|
||||
const save = serialiseCurrentInput();
|
||||
save.name = name;
|
||||
const saveString = JSON.stringify(save);
|
||||
let oldSaves = localStorage.getItem("saves");
|
||||
if (oldSaves !== null) {
|
||||
oldSaves += "@";
|
||||
} else {
|
||||
oldSaves = "";
|
||||
}
|
||||
localStorage.setItem("saves", oldSaves + saveString);
|
||||
examples.update(examples => examples.concat(save));
|
||||
}
|
||||
32
src/ui/ActionButton.svelte
Normal file
32
src/ui/ActionButton.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
export let onClick: () => void = () => {};
|
||||
export let tooltip: string | null = null;
|
||||
export let disabled: boolean = false;
|
||||
export let text: string;
|
||||
</script>
|
||||
|
||||
<button
|
||||
on:click={onClick}
|
||||
title="{tooltip}"
|
||||
disabled={disabled}>
|
||||
{text}
|
||||
</button>
|
||||
|
||||
<style>
|
||||
button {
|
||||
width: auto;
|
||||
color: white;
|
||||
background-color: #ff3e00;
|
||||
border-radius: 0.5em;
|
||||
border-style: none;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #ff6b3e;
|
||||
}
|
||||
button:disabled {
|
||||
color: #999999;
|
||||
background-color: #a36754;
|
||||
}
|
||||
</style>
|
||||
@@ -10,13 +10,14 @@
|
||||
<div class="sidebarContainer">
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div class="solutionBodyContainer">
|
||||
<div class="stage">
|
||||
<Stage scene="{scene}" />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
main {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
@@ -25,28 +26,41 @@
|
||||
background-color: black;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 20%;
|
||||
max-width: 20%;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
}
|
||||
.solutionBodyContainer {
|
||||
.stage {
|
||||
flex: 1;
|
||||
background-color: grey;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 80%;
|
||||
height: 100%;
|
||||
width: 80%;
|
||||
display: inline-block;
|
||||
}
|
||||
:global(body) {
|
||||
color: white;
|
||||
background: #333333;
|
||||
}
|
||||
@media(max-width: 1200px) {
|
||||
.solutionBodyContainer {
|
||||
width: calc(100% - 18em);
|
||||
}
|
||||
@media(width: 1200px) {
|
||||
.sidebarContainer {
|
||||
width: 18em;
|
||||
}
|
||||
.stage {
|
||||
width: calc(80% - 18em);
|
||||
}
|
||||
}
|
||||
@media(max-width: 1024px) {
|
||||
.stage {
|
||||
left: 2em;
|
||||
position: absolute;
|
||||
width: calc(100% - 2em);
|
||||
}
|
||||
.sidebarContainer {
|
||||
position: absolute;
|
||||
max-width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
}
|
||||
.colorPickerBtn {
|
||||
align-self: center;
|
||||
background-image: url("../resources/ColorWheel.png");
|
||||
background-image: url("./ColorWheel.png");
|
||||
background-size: cover;
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
|
||||
@@ -26,9 +26,9 @@
|
||||
margin: auto;
|
||||
}
|
||||
.container {
|
||||
flex: 1 1 auto;
|
||||
overflow-x: scroll;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
flex-flow: row;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
@@ -1,67 +1,65 @@
|
||||
<script lang="ts">
|
||||
import List from "./List.svelte";
|
||||
import {polycubes, examples, serialiseCurrentInput} from "../store";
|
||||
import {polycubes, examples, save} from "../store";
|
||||
import VoxelSpaceBigInt from "../VoxelSpaceBigInt";
|
||||
import ActionButton from "./ActionButton.svelte";
|
||||
|
||||
const placeholder = "Give your puzzle a name";
|
||||
let untouchedInput = true;
|
||||
let currentName = placeholder;
|
||||
let lastClickedExample = 0;
|
||||
let untouchedInput: boolean = true;
|
||||
let currentName: string = placeholder;
|
||||
let lastClickedExample: number | null = null;
|
||||
|
||||
function hydrateExample(exNo: number) {
|
||||
const example = examples[exNo];
|
||||
const example = $examples[exNo];
|
||||
polycubes.setCubes(example.cubes.map((cube, i) => new VoxelSpaceBigInt({
|
||||
id: i,
|
||||
dims: [example.dimX, example.dimY, example.dimZ],
|
||||
space: cube.space,
|
||||
space: BigInt(cube.space),
|
||||
color: cube.color,
|
||||
cullEmpty: false,
|
||||
})));
|
||||
lastClickedExample = exNo;
|
||||
}
|
||||
|
||||
function checkUntouched() {
|
||||
if (currentName === "") {
|
||||
untouchedInput = true;
|
||||
} else {
|
||||
untouchedInput = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onInput(e: InputEvent) {
|
||||
currentName = (e.target as HTMLInputElement).value;
|
||||
checkUntouched();
|
||||
}
|
||||
|
||||
function onFocusText(e: FocusEvent) {
|
||||
if (untouchedInput) {
|
||||
currentName = "";
|
||||
untouchedInput = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onBlurText(e: FocusEvent) {
|
||||
if (currentName === "") {
|
||||
if (untouchedInput) {
|
||||
currentName = placeholder;
|
||||
untouchedInput = true;
|
||||
}
|
||||
}
|
||||
|
||||
function save() {
|
||||
const save = serialiseCurrentInput();
|
||||
save["name"] = currentName;
|
||||
const saveString = JSON.stringify(save);
|
||||
let oldSaves = window.localStorage.getItem("saves");
|
||||
if (oldSaves !== null) {
|
||||
oldSaves += "@";
|
||||
} else {
|
||||
oldSaves = "";
|
||||
}
|
||||
window.localStorage.setItem("saves", oldSaves + saveString);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<List
|
||||
defaultText="No examples found..."
|
||||
items="{examples.map(example => example.name)}"
|
||||
items="{$examples.map(example => example.name)}"
|
||||
activeItem={lastClickedExample}
|
||||
onClick={(i) => hydrateExample(i)}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<button on:click={save}>Save as...</button>
|
||||
<div class="save">
|
||||
<ActionButton
|
||||
onClick={() => save(currentName)}
|
||||
text={"Save as..."}
|
||||
disabled={untouchedInput}/>
|
||||
<input
|
||||
class:untouchedInput
|
||||
value="{currentName}"
|
||||
@@ -75,11 +73,22 @@
|
||||
.untouchedInput {
|
||||
color: grey;
|
||||
}
|
||||
button {
|
||||
.save {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 1em;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
margin-top: 1em;
|
||||
}
|
||||
input {
|
||||
flex-basis: 10em;
|
||||
flex: 1;
|
||||
max-width: 100%;
|
||||
min-width: 50%;
|
||||
margin: 0 0 0 0.5em;
|
||||
width: 100%;
|
||||
}
|
||||
.container {
|
||||
height: 10em;
|
||||
|
||||
@@ -32,9 +32,6 @@
|
||||
downDisabled="{$somaDimZ <= PolycubeStore.MIN_DIMS}"
|
||||
down="{() => somaDimZ.set($somaDimZ - 1)}"
|
||||
/>
|
||||
{#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 class="option">
|
||||
@@ -49,7 +46,4 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.warn {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
@@ -29,6 +29,10 @@
|
||||
list-style: none;
|
||||
height: 2em;
|
||||
line-height: 2em;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
ul {
|
||||
position: absolute;
|
||||
|
||||
@@ -7,32 +7,46 @@
|
||||
import ExamplesList from "./ExamplesList.svelte";
|
||||
import Tabs from "./Tabs.svelte";
|
||||
import SolveButton from "./SolveButton.svelte";
|
||||
import ActionButton from "./ActionButton.svelte";
|
||||
|
||||
let hidden: boolean = true;
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="title">
|
||||
<img class="logo" src="../favicon.png"/><h1>Somaesque</h1>
|
||||
</div>
|
||||
<div class="widgets">
|
||||
<div>
|
||||
<Tabs
|
||||
selectedTab={"Parameters"}
|
||||
tabs={{
|
||||
"Parameters": InputParameters,
|
||||
"Examples": ExamplesList,
|
||||
}}/>
|
||||
<div class="controls" class:hidden>
|
||||
<div class="title">
|
||||
<img class="logo" src="./resources/favicon.png"/>
|
||||
<h1>Somaesque</h1>
|
||||
</div>
|
||||
<div>
|
||||
<SolveButton/>
|
||||
<div class="widgets">
|
||||
<div>
|
||||
<Tabs
|
||||
selectedTab={"Parameters"}
|
||||
tabs={{
|
||||
"Parameters": InputParameters,
|
||||
"Examples": ExamplesList,
|
||||
}}/>
|
||||
</div>
|
||||
<div>
|
||||
<SolveButton/>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Solutions: {$solutions.length}</h3>
|
||||
<div class="solns">
|
||||
<SolutionList/>
|
||||
</div>
|
||||
</div>
|
||||
<h3>Solutions: {$solutions.length}</h3>
|
||||
<div class="solns">
|
||||
<SolutionList/>
|
||||
<div class="showHideBtn">
|
||||
<ActionButton text={hidden ? ">" : "<"} onClick={() => {hidden = !hidden}}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: row;
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -41,12 +55,16 @@
|
||||
margin-right: 1em;
|
||||
display: inline-block;
|
||||
height: 3em;
|
||||
align-self: center;
|
||||
background-image: url("../resources/favicon.png");
|
||||
background-size: cover;
|
||||
width: 3em;
|
||||
}
|
||||
.container {
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: #333333;
|
||||
padding: 1em;
|
||||
@@ -76,4 +94,34 @@
|
||||
font-size: 3em;
|
||||
font-weight: 100;
|
||||
}
|
||||
.showHideBtn {
|
||||
background-color: #333333;
|
||||
text-align: center;
|
||||
line-height: 100%;
|
||||
width: 2em;
|
||||
height: 100%;
|
||||
padding: 0.25em;
|
||||
flex: 0 0 auto;
|
||||
display: none;
|
||||
}
|
||||
@media(max-width: 1600px) {
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
||||
@media(max-width: 1024px) {
|
||||
.controls {
|
||||
overflow: scroll;
|
||||
padding-right: 0;
|
||||
}
|
||||
.showHideBtn {
|
||||
display: inline-block;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
.container.hidden {
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -20,5 +20,6 @@
|
||||
<style>
|
||||
.container {
|
||||
height: 100%;
|
||||
min-height: 10em;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import {polycubes, solving, somaDimX, somaDimY, somaDimZ, totalVolume} from "../store";
|
||||
import {solve} from "../solve";
|
||||
import ActionButton from "./ActionButton.svelte";
|
||||
|
||||
$: cubes = $polycubes;
|
||||
let noEmpties: boolean;
|
||||
@@ -26,29 +27,40 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="solve"
|
||||
on:click={solve}
|
||||
title="{genTooltip(enoughSubcubes, noEmpties, size)}"
|
||||
disabled="{$solving || !readyToSolve}">
|
||||
{$solving ? "Solving..." : "Solve!"}
|
||||
</button>
|
||||
<div class="container">
|
||||
<div class="solve">
|
||||
<ActionButton
|
||||
onClick={solve}
|
||||
tooltip={genTooltip(enoughSubcubes, noEmpties, size)}
|
||||
disabled={$solving || !readyToSolve}
|
||||
text={$solving ? "Solving..." : "Solve!"}/>
|
||||
</div>
|
||||
{#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>
|
||||
|
||||
<style>
|
||||
button.solve {
|
||||
width: auto;
|
||||
color: white;
|
||||
background-color: #ff3e00;
|
||||
font-size: 2em;
|
||||
border-radius: 0.5em;
|
||||
border-style: none;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
}
|
||||
button.solve:disabled {
|
||||
.warn {
|
||||
flex-basis: 7em;
|
||||
flex: 1;
|
||||
min-width: 7em;
|
||||
margin-left: 1em;
|
||||
color: red;
|
||||
text-align: left;
|
||||
}
|
||||
.solve {
|
||||
height: min-content;
|
||||
width: auto;
|
||||
color: #999999;
|
||||
background-color: #a36754;
|
||||
font-size: 2em;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import SolutionViewer from "./SolutionViewer.svelte";
|
||||
import ThreeDee from "./ThreeDee.svelte";
|
||||
import CubeInputSet from "./CubeInputSet.svelte";
|
||||
import PolycubeScene from "./threedee/PolycubeScene";
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
}
|
||||
|
||||
const queryListWidth = window.matchMedia("(max-width: 1200px)");
|
||||
const queryListHeight = window.matchMedia("(max-height: 920px)");
|
||||
const queryListHeight = window.matchMedia("(max-height: 900px)");
|
||||
queryListWidth.addEventListener("change", onMediaChange);
|
||||
queryListHeight.addEventListener("change", onMediaChange);
|
||||
onMediaChange();
|
||||
@@ -27,11 +27,11 @@
|
||||
{#if showInput}
|
||||
<CubeInputSet/>
|
||||
{:else}
|
||||
<SolutionViewer scene="{scene}"/>
|
||||
<ThreeDee scene="{scene}"/>
|
||||
{/if}
|
||||
{:else}
|
||||
<CubeInputSet/>
|
||||
<SolutionViewer scene="{scene}"/>
|
||||
<ThreeDee scene="{scene}"/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -59,9 +59,9 @@
|
||||
border-width: 1px 0 0 0;
|
||||
}
|
||||
.viewport {
|
||||
overflow: scroll;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
align-content: center;
|
||||
justify-content: flex-start;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import {onMount} from "svelte";
|
||||
import {polycubes, solutions, activeSolution, showingSolution} from "../store";
|
||||
import Solution2D from "./Solution2D.svelte";
|
||||
import CubeInput from "./CubeInput.svelte";
|
||||
|
||||
export let scene: PolycubeScene;
|
||||
const selectedStore = polycubes.selected();
|
||||
@@ -10,17 +11,32 @@
|
||||
$: cube = $polycubes[selectedCube];
|
||||
$: soln = $solutions[$activeSolution];
|
||||
let el: HTMLDivElement;
|
||||
let containerEl: HTMLDivElement;
|
||||
let loaded: boolean = false;
|
||||
|
||||
const canvasStyle: Partial<CSSStyleDeclaration> = {
|
||||
borderRadius: "1em",
|
||||
};
|
||||
|
||||
function updateDims() {
|
||||
if (window.innerWidth < 1200) {
|
||||
if (containerEl.clientHeight < containerEl.clientWidth * (3 / 4)) {
|
||||
scene.updateDims({width: (containerEl.clientHeight - 50) * (4 / 3), height: containerEl.clientHeight - 50});
|
||||
} else {
|
||||
scene.updateDims({width: containerEl.scrollWidth - 50, height: (containerEl.scrollWidth - 50) * (3 / 4)});
|
||||
}
|
||||
} else {
|
||||
scene.resetDims();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
scene.onLoaded(() => {
|
||||
scene.mount(el);
|
||||
Object.assign((el.children.item(0) as HTMLElement).style, canvasStyle);
|
||||
loaded = true;
|
||||
window.addEventListener("resize", () => updateDims());
|
||||
updateDims();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,11 +51,15 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
{#if $activeSolution !== null}
|
||||
<div class="container" bind:this={containerEl}>
|
||||
{#if $showingSolution && ($activeSolution !== null)}
|
||||
<div class="soln2d-container">
|
||||
<Solution2D/>
|
||||
</div>
|
||||
{:else if !$showingSolution}
|
||||
<div class="soln2d-container">
|
||||
<CubeInput cubeNo="{selectedCube}"/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="stage" bind:this={el}></div>
|
||||
</div>
|
||||
@@ -49,6 +69,11 @@
|
||||
flex: 0 1 auto;
|
||||
display: inline-block;
|
||||
}
|
||||
@media (max-width: 1200px) {
|
||||
.soln2d-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
@@ -57,5 +82,6 @@
|
||||
justify-content: space-evenly;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -21,8 +21,10 @@ export default class PolycubeScene {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private loadedCb: () => void = () => {};
|
||||
private loaded: boolean = false;
|
||||
private windowDims: {width: number, height: number};
|
||||
|
||||
constructor() {
|
||||
constructor(windowDims?: {width: number, height: number}) {
|
||||
this.windowDims = windowDims ?? {width: DEFAULT_WIDTH, height: DEFAULT_HEIGHT};
|
||||
this.init().then(() => this.loadedCb()).catch(e => console.log(e));
|
||||
}
|
||||
|
||||
@@ -56,12 +58,24 @@ export default class PolycubeScene {
|
||||
}
|
||||
|
||||
mount(el: HTMLDivElement) {
|
||||
this.canvas.width = DEFAULT_WIDTH;
|
||||
this.canvas.height = DEFAULT_HEIGHT;
|
||||
this.updateDims(this.windowDims);
|
||||
el.append(this.canvas);
|
||||
}
|
||||
|
||||
updateDims(windowDims: {width: number, height: number}) {
|
||||
this.windowDims.width = windowDims.width;
|
||||
this.windowDims.height = windowDims.height;
|
||||
this.canvas.width = this.windowDims.width;
|
||||
this.canvas.height = this.windowDims.height;
|
||||
this.camera.aspect = this.canvas.width / this.canvas.height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(this.canvas.width, this.canvas.height);
|
||||
el.append(this.canvas);
|
||||
}
|
||||
|
||||
resetDims() {
|
||||
this.windowDims.width = DEFAULT_WIDTH;
|
||||
this.windowDims.height = DEFAULT_HEIGHT;
|
||||
this.updateDims(this.windowDims);
|
||||
}
|
||||
|
||||
onLoaded(cb: () => void) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type * as THREE from 'three';
|
||||
import {MOUSE} from "three";
|
||||
|
||||
interface Fliable {
|
||||
flyBy(factor: number);
|
||||
@@ -17,6 +18,10 @@ export default class RotationControls {
|
||||
private start: THREE.Euler;
|
||||
private fliables: Fliable[];
|
||||
private hovered: boolean = false;
|
||||
private scrolling: boolean = false;
|
||||
private lastTouch: {x: number, y: number} = {x: 0, y: 0};
|
||||
private lastScroll1: {x: number, y: number} = {x: 0, y: 0};
|
||||
private lastScroll2: {x: number, y: number} = {x: 0, y: 0};
|
||||
|
||||
constructor(object: THREE.Object3D, fliables: Fliable[], camera: THREE.Camera, element: HTMLCanvasElement) {
|
||||
this.object = object;
|
||||
@@ -28,23 +33,29 @@ export default class RotationControls {
|
||||
this.xAxis = this.xAxis.clone().cross(this.yAxis.clone());
|
||||
this.start = this.object.rotation.clone();
|
||||
|
||||
this.element.addEventListener('touchstart', (ev) => this.handleTouchStart(ev));
|
||||
this.element.addEventListener("touchcancel", (ev) => this.handleTouchEnd(ev));
|
||||
window.addEventListener('touchmove', (ev) => this.handleTouchMove(ev));
|
||||
window.addEventListener('touchend', (ev) => this.handleTouchEnd(ev));
|
||||
this.element.addEventListener('wheel', (ev) => this.handleScroll(ev));
|
||||
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;
|
||||
}
|
||||
});
|
||||
this.element.addEventListener('mousedown', (ev) => this.handleMouseDown(ev));
|
||||
window.addEventListener('mousemove', (ev) => this.handleMove(ev));
|
||||
window.addEventListener('mouseup', () => this.dragging = false);
|
||||
}
|
||||
|
||||
private handleMouseDown(event: MouseEvent) {
|
||||
if (event.button === 1) {
|
||||
this.object.setRotationFromEuler(this.start);
|
||||
}
|
||||
if (!this.dragging) {
|
||||
this.lastX = event.x;
|
||||
this.lastY = event.y;
|
||||
this.dragging = true;
|
||||
}
|
||||
}
|
||||
|
||||
private handleMove(event: MouseEvent) {
|
||||
if (this.dragging) {
|
||||
const xDiff = event.movementX * RotationControls.ROTATION_FACTOR;
|
||||
@@ -63,6 +74,56 @@ export default class RotationControls {
|
||||
}
|
||||
}
|
||||
|
||||
private handleTouchMove(event: TouchEvent) {
|
||||
if (this.dragging) {
|
||||
const newTouchX = event.touches.item(0).clientX;
|
||||
const newTouchY = event.touches.item(0).clientY;
|
||||
const touchDiffX = newTouchX - this.lastTouch.x;
|
||||
const touchDiffY = newTouchY - this.lastTouch.y;
|
||||
const xDiff = touchDiffX * RotationControls.ROTATION_FACTOR;
|
||||
const yDiff = touchDiffY * RotationControls.ROTATION_FACTOR;
|
||||
this.object.rotateOnAxis(this.yAxis, xDiff);
|
||||
this.object.rotateOnWorldAxis(this.xAxis, yDiff);
|
||||
this.lastTouch.x = newTouchX;
|
||||
this.lastTouch.y = newTouchY;
|
||||
} else if (this.scrolling) {
|
||||
if (this.flyingEnabled && this.hovered) {
|
||||
const newTouchX1 = event.touches.item(0).clientX;
|
||||
const newTouchX2 = event.touches.item(1).clientX;
|
||||
const newTouchY1 = event.touches.item(0).clientY;
|
||||
const newTouchY2 = event.touches.item(1).clientY;
|
||||
const lastDist = Math.sqrt((this.lastScroll1.x - this.lastScroll2.x) ** 2 + (this.lastScroll1.y - this.lastScroll2.y) ** 2);
|
||||
const newDist = Math.sqrt((newTouchX1 - newTouchX2) ** 2 + (newTouchY1 - newTouchY2) ** 2)
|
||||
const delta = newDist - lastDist;
|
||||
for (const fliable of this.fliables) {
|
||||
const direction = delta / Math.abs(delta);
|
||||
fliable.flyBy(direction / 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleTouchStart(event: TouchEvent) {
|
||||
if (event.touches.length === 1) {
|
||||
this.lastTouch.x = event.touches.item(0).clientX;
|
||||
this.lastTouch.y = event.touches.item(0).clientY;
|
||||
this.dragging = true;
|
||||
} else if (event.touches.length === 2) {
|
||||
this.lastScroll1.x = event.touches.item(0).clientX;
|
||||
this.lastScroll1.y = event.touches.item(0).clientY;
|
||||
this.lastScroll2.x = event.touches.item(1).clientX;
|
||||
this.lastScroll2.y = event.touches.item(1).clientY;
|
||||
this.scrolling = true;
|
||||
}
|
||||
this.hovered = true;
|
||||
}
|
||||
|
||||
private handleTouchEnd(event: TouchEvent) {
|
||||
this.dragging = false;
|
||||
this.scrolling = false;
|
||||
this.hovered = false;
|
||||
}
|
||||
|
||||
enableFly() {
|
||||
this.flyingEnabled = true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user