diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 9b7c156..78b757b 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -26,14 +26,25 @@ + + + + + + + - + + + + + @@ -95,6 +107,13 @@ + + + + + + + @@ -102,13 +121,6 @@ - - - - - - - @@ -156,6 +168,7 @@ + @@ -218,10 +231,10 @@ - - + + - + diff --git a/package.json b/package.json index be14ae9..5985e28 100644 --- a/package.json +++ b/package.json @@ -49,10 +49,7 @@ "linux": { "target": ["AppImage"] }, - "extraResources": [ - "public/solver" - ], - "icon": "./public/resources/soma_icon.png", + "icon": "./public/favicon.png", "directories": { "output": "desktop-dist" } diff --git a/public/ColorWheel.png b/public/ColorWheel.png new file mode 100644 index 0000000..65767af Binary files /dev/null and b/public/ColorWheel.png differ diff --git a/public/resources/favicon.png b/public/resources/favicon.png new file mode 100644 index 0000000..e69de29 diff --git a/src/solve.ts b/src/solve.ts index 9de804a..7996760 100644 --- a/src/solve.ts +++ b/src/solve.ts @@ -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()); diff --git a/src/store.ts b/src/store.ts index 581fc5e..f4b199e 100644 --- a/src/store.ts +++ b/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)); } \ No newline at end of file diff --git a/src/ui/ActionButton.svelte b/src/ui/ActionButton.svelte new file mode 100644 index 0000000..cf2d916 --- /dev/null +++ b/src/ui/ActionButton.svelte @@ -0,0 +1,32 @@ + + + + + \ No newline at end of file diff --git a/src/ui/App.svelte b/src/ui/App.svelte index ab400d8..7daeaea 100644 --- a/src/ui/App.svelte +++ b/src/ui/App.svelte @@ -10,13 +10,14 @@
-
+
diff --git a/src/ui/CubeInput.svelte b/src/ui/CubeInput.svelte index dac918a..d3dc57b 100644 --- a/src/ui/CubeInput.svelte +++ b/src/ui/CubeInput.svelte @@ -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; diff --git a/src/ui/CubeInputSet.svelte b/src/ui/CubeInputSet.svelte index 4076119..2fff590 100644 --- a/src/ui/CubeInputSet.svelte +++ b/src/ui/CubeInputSet.svelte @@ -26,9 +26,9 @@ margin: auto; } .container { - flex: 1 1 auto; overflow-x: scroll; display: flex; + width: 100%; flex-flow: row; margin: auto; } diff --git a/src/ui/ExamplesList.svelte b/src/ui/ExamplesList.svelte index 0cc0069..8f5e330 100644 --- a/src/ui/ExamplesList.svelte +++ b/src/ui/ExamplesList.svelte @@ -1,67 +1,65 @@
hydrateExample(i)} />
-
- +
+ save(currentName)} + text={"Save as..."} + disabled={untouchedInput}/> - {#if $totalVolume > 32} -

The total number of units exceeds 32. Attempting to solve puzzles with more than 32 units results in significantly slower computation time.

- {/if}
@@ -49,7 +46,4 @@
\ No newline at end of file diff --git a/src/ui/List.svelte b/src/ui/List.svelte index c8cb278..6fed011 100644 --- a/src/ui/List.svelte +++ b/src/ui/List.svelte @@ -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; diff --git a/src/ui/Sidebar.svelte b/src/ui/Sidebar.svelte index 59645ef..5abc139 100644 --- a/src/ui/Sidebar.svelte +++ b/src/ui/Sidebar.svelte @@ -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;
-
-

Somaesque

-
-
-
- +
+
+ +

Somaesque

-
- +
+
+ +
+
+ +
+
+

Solutions: {$solutions.length}

+
+
-

Solutions: {$solutions.length}

-
- +
+ " : "<"} onClick={() => {hidden = !hidden}}/>
\ No newline at end of file diff --git a/src/ui/SolutionList.svelte b/src/ui/SolutionList.svelte index 00df79b..a72399a 100644 --- a/src/ui/SolutionList.svelte +++ b/src/ui/SolutionList.svelte @@ -20,5 +20,6 @@ \ No newline at end of file diff --git a/src/ui/SolveButton.svelte b/src/ui/SolveButton.svelte index 3a2f6bc..cb55a9a 100644 --- a/src/ui/SolveButton.svelte +++ b/src/ui/SolveButton.svelte @@ -1,6 +1,7 @@ - +
+
+ +
+ {#if $totalVolume > 32} +

The total number of units exceeds 32. Attempting to solve puzzles with more than 32 units results in significantly slower computation time.

+ {/if} +
\ No newline at end of file diff --git a/src/ui/Stage.svelte b/src/ui/Stage.svelte index ae227af..0fb41a2 100644 --- a/src/ui/Stage.svelte +++ b/src/ui/Stage.svelte @@ -1,5 +1,5 @@ -
- {#if $activeSolution !== null} +
+ {#if $showingSolution && ($activeSolution !== null)}
+ {:else if !$showingSolution} +
+ +
{/if}
@@ -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; } \ No newline at end of file diff --git a/src/ui/threedee/PolycubeScene.ts b/src/ui/threedee/PolycubeScene.ts index c196a41..7c98f6c 100644 --- a/src/ui/threedee/PolycubeScene.ts +++ b/src/ui/threedee/PolycubeScene.ts @@ -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) { diff --git a/src/ui/threedee/RotationControl.ts b/src/ui/threedee/RotationControl.ts index fc45967..98242e3 100644 --- a/src/ui/threedee/RotationControl.ts +++ b/src/ui/threedee/RotationControl.ts @@ -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; }