diff --git a/doc/cube-directions.png b/doc/cube-directions.png new file mode 100644 index 00000000..e04854b2 Binary files /dev/null and b/doc/cube-directions.png differ diff --git a/doc/cubes.png b/doc/cubes.png new file mode 100644 index 00000000..2d98dff5 Binary files /dev/null and b/doc/cubes.png differ diff --git a/package-lock.json b/package-lock.json index 62f1d28f..bf4859b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "@fontsource/fira-mono": "^4.5.0", "cookie": "^0.4.1", "normalize-wheel": "^1.0.1", - "randomcolor": "0.6.2" + "randomcolor": "0.6.2", + "transformation-matrix": "^2.15.0" }, "devDependencies": { "@sveltejs/adapter-auto": "next", @@ -3857,6 +3858,14 @@ "node": ">=12" } }, + "node_modules/transformation-matrix": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/transformation-matrix/-/transformation-matrix-2.15.0.tgz", + "integrity": "sha512-HN3kCvvH4ug3Xm/ycOfCFQOOktg5htxlC4Ih1Z7Wb6BMtQho+q+irOdGo10ARRKpqkRBXgBzQFw/AVmR0oIf0g==", + "funding": { + "url": "https://github.com/sponsors/chrvadala" + } + }, "node_modules/type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", @@ -7020,6 +7029,11 @@ "punycode": "^2.1.1" } }, + "transformation-matrix": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/transformation-matrix/-/transformation-matrix-2.15.0.tgz", + "integrity": "sha512-HN3kCvvH4ug3Xm/ycOfCFQOOktg5htxlC4Ih1Z7Wb6BMtQho+q+irOdGo10ARRKpqkRBXgBzQFw/AVmR0oIf0g==" + }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", diff --git a/package.json b/package.json index 3fddd9ce..a201a3f0 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,8 @@ }, "devDependencies": { "@sveltejs/adapter-auto": "next", - "@sveltejs/adapter-vercel": "next", "@sveltejs/adapter-static": "*", + "@sveltejs/adapter-vercel": "next", "@sveltejs/kit": "^1.0.0", "@testing-library/svelte": "^3.2.1", "@types/cookie": "^0.5.1", @@ -37,6 +37,7 @@ "@fontsource/fira-mono": "^4.5.0", "cookie": "^0.4.1", "normalize-wheel": "^1.0.1", - "randomcolor": "0.6.2" + "randomcolor": "0.6.2", + "transformation-matrix": "^2.15.0" } } diff --git a/src/lib/header/ExampleTile.svelte b/src/lib/header/ExampleTile.svelte index 90be777b..b8083303 100644 --- a/src/lib/header/ExampleTile.svelte +++ b/src/lib/header/ExampleTile.svelte @@ -14,14 +14,18 @@ let path = grid.getPipesPath(tile, i); const isSink = grid.getDirections(tile, 0, i).length === 1; + + const tile_transform = grid.getTileTransformCSS(i) || '' + - + - + diff --git a/src/lib/puzzle/EdgeMarks.svelte b/src/lib/puzzle/EdgeMarks.svelte index e0caba68..7808bab6 100644 --- a/src/lib/puzzle/EdgeMarks.svelte +++ b/src/lib/puzzle/EdgeMarks.svelte @@ -12,11 +12,25 @@ let state = game.tileStates[i]; + let tile_transform = null; + $: tile_transform = game.grid.getTileTransformCSS(i) || ''; /** * * @param {import('$lib/puzzle/game').EdgeMark[]} marks */ function visibleMarks(marks) { + let reflectMarks = null; + if (game.grid.EDGEMARK_REFLECTS) { + reflectMarks = game.grid.EDGEMARK_REFLECTS.map(direction => { + const {neighbour} = game.grid.find_neighbour(i, direction); + if (neighbour === -1) { + return 'none'; + } + const oppositeDir = game.grid.OPPOSITE.get(direction); + const oppositeIndex = game.grid.EDGEMARK_DIRECTIONS.indexOf(oppositeDir); + return game.tileStates[neighbour].data.edgeMarks[oppositeIndex]; + }); + } /** * @type {{x1:number, x2: number, y1: number, y2:number, state: import('$lib/puzzle/game').EdgeMark, direction:number}[]} */ @@ -26,7 +40,7 @@ return; } const direction = game.grid.EDGEMARK_DIRECTIONS[index]; - const { x1, y1, x2, y2 } = game.grid.getEdgemarkLine(direction, i); + const { x1, y1, x2, y2 } = game.grid.getEdgemarkLine(direction, state === 'wall', i); visible.push({ x1, y1, @@ -36,13 +50,30 @@ direction }); }); + if (reflectMarks) { + reflectMarks.forEach((state, index) => { + if (state === 'none' || state === 'empty' || state === 'wall') { + return; + } + const direction = game.grid.EDGEMARK_REFLECTS[index]; + const { x1, y1, x2, y2 } = game.grid.getEdgemarkLine(direction, state === 'wall', i); + visible.push({ + x1, + y1, + x2, + y2, + state, + direction + }); + }); + } return visible; } $: visibleEdgeMarks = visibleMarks($state.edgeMarks); - + {#each visibleEdgeMarks as { x1, y1, x2, y2, state, direction } (direction)} - + - + {#if controlMode === 'orient_lock' && !$state.locked && !solved} diff --git a/src/lib/puzzle/controls.js b/src/lib/puzzle/controls.js index 50e367ed..f543cdfc 100644 --- a/src/lib/puzzle/controls.js +++ b/src/lib/puzzle/controls.js @@ -265,11 +265,10 @@ export function controls(node, game) { } else if (currentSettings.controlMode === 'orient_lock') { if (leftButton) { const { tileX, tileY } = mouseDownOrigin; - const angle = Math.atan2(tileY - y, x - tileX); const timesRotate = game.grid.clickOrientTile( tileState.data.tile, tileState.data.rotations, - angle, + x - tileX, y - tileY, tileIndex ); game.rotateTile(tileIndex, timesRotate); diff --git a/src/lib/puzzle/game.js b/src/lib/puzzle/game.js index 071c495e..b57a5766 100644 --- a/src/lib/puzzle/game.js +++ b/src/lib/puzzle/game.js @@ -402,6 +402,12 @@ export function PipesGame(grid, tiles, savedProgress) { tileState.data.edgeMarks[index] = mark; } tileState.set(tileState.data); + if (self.grid.EDGEMARK_REFLECTS) { + // force redraw of the neighbour because drawing of connection edgemarks + // is driven partly by neighbours' state + const neighbourState = self.tileStates[neighbour]; + neighbourState.set(neighbourState.data); + } if (tileState.data.edgeMarks[index] !== 'empty' && assistant) { self.rotateToMatchMarks(tileIndex); self.rotateToMatchMarks(neighbour); diff --git a/src/lib/puzzle/grids/cubegrid.js b/src/lib/puzzle/grids/cubegrid.js new file mode 100644 index 00000000..492895d3 --- /dev/null +++ b/src/lib/puzzle/grids/cubegrid.js @@ -0,0 +1,308 @@ +import { RegularPolygonTile, TransformedPolygonTile } from '$lib/puzzle/grids/polygonutils'; +import { HexaGrid, EAST, NORTHEAST, NORTHWEST, WEST, SOUTHWEST, SOUTHEAST } from './hexagrid'; + +const DIRA = 1; +const DIRB = 2; +const DIRC = 4; +const DIRD = 8; + +const YSTEP = Math.sqrt(3) / 2; +const RIGHT_FACE = new TransformedPolygonTile(4, 0, 0.5, [], 0.01, 1/Math.sqrt(3), 0.5, Math.PI / 6, 0, -Math.PI / 2); +const TOP_FACE = new TransformedPolygonTile(4, 0, 0.5, [], 0.01, 1/Math.sqrt(3), 0.5, Math.PI / 6, 0, 5 * Math.PI / 6); +const LEFT_FACE = new TransformedPolygonTile(4, 0, 0.5, [], 0.01, 1/Math.sqrt(3), 0.5, Math.PI / 6, 0, Math.PI / 6); + +const RHOMB_DIRS = new Map([ + [0, -Math.PI / 6], + [1, Math.PI / 2], + [2, -Math.PI * 5 / 6] +]); +const RHOMB_OFFSETS = new Map( + [...RHOMB_DIRS.entries()].map(([rh, dir]) => [rh, [Math.cos(dir)*Math.sqrt(3)/6, -Math.sin(dir)*Math.sqrt(3)/6]]) +); + +export class CubeGrid { + DIRECTIONS = [DIRA, DIRB, DIRC, DIRD]; + EDGEMARK_DIRECTIONS = [DIRB, DIRC]; + EDGEMARK_REFLECTS = [DIRD, DIRA]; + OPPOSITE = new Map([ + [DIRA, DIRB], + [DIRB, DIRA], + [DIRC, DIRD], + [DIRD, DIRC] + ]); + #RHOMB_NEIGHBOURS = new Map([ + [0, new Map([ + [DIRA, [0, 1]], + [DIRB, [0, 2]], + [DIRC, [SOUTHEAST, 1]], + [DIRD, [EAST, 2]] + ])], + [1, new Map([ + [DIRA, [0, 2]], + [DIRB, [0, 0]], + [DIRC, [NORTHEAST, 2]], + [DIRD, [NORTHWEST, 0]] + ])], + [2, new Map([ + [DIRA, [0, 0]], + [DIRB, [0, 1]], + [DIRC, [WEST, 0]], + [DIRD, [SOUTHWEST, 1]] + ])] + ]); + NUM_DIRECTIONS = 4; + KIND = 'cube'; + PIPE_WIDTH = 0.15; + STROKE_WIDTH = 0.06; + PIPE_LENGTH = 0.5; + SINK_RADIUS = 0.2; + + /** @type {Set} - indices of empty cells */ + emptyCells; + /** @type {Number} - total number of cells including empties */ + total; + + /** + * + * @param {Number} width + * @param {Number} height + * @param {Boolean} wrap + * @param {Number[]} tiles + */ + constructor(width, height, wrap, tiles = []) { + this.width = width; + this.height = height; + this.wrap = wrap; + + this.hexagrid = new HexaGrid(width, height, wrap); + if (!wrap) { + this.hexagrid.useShape('hexagon'); + } + + this.lineJoin = 'bevel'; + + this.emptyCells = new Set(); + this.hexagrid.emptyCells.forEach(index => { + for (let rh = 0; rh < 3; rh++) { + this.emptyCells.add(index*3 + rh); + } + }) + tiles.forEach((tile, index) => { + if (tile === 0) { + this.emptyCells.add(index); + } + }); + this.total = width * height * 3; + + this.XMIN = this.hexagrid.XMIN; + this.XMAX = this.hexagrid.XMAX; + this.YMIN = this.hexagrid.YMIN; + this.YMAX = this.hexagrid.YMAX; + } + + /** + * @param {Number} angle + */ + angle_to_rhomb(angle) { + /* Counter-clockwise from lower right of "right side up" cube */ + return (Math.floor((angle + Math.PI/2) * 3 / (2 * Math.PI)) + 3) % 3; + } + + /** + * Determines which tile a point at (x, y) belongs to + * Returns tile index and tile center coordinates + * If the point is over empty space then tileIndex is -1 + * @param {Number} x + * @param {Number} y + * @returns {{index: Number, x:Number, y: Number, rh: Number}} + */ + which_tile_at(x, y) { + const {index: index0, x: x0, y: y0} = this.hexagrid.which_tile_at(x, y) + const rhomb0 = this.angle_to_rhomb(Math.atan2(-(y - y0), x - x0)); + const index = index0 >= 0 ? 3 * index0 + rhomb0 : -1; + const ofs = RHOMB_OFFSETS.get(rhomb0); + return {index, x: x0 + ofs[0], y: y0 + ofs[1], rh: rhomb0}; + } + + /** + * @param {Number} index + * @param {Number} direction + * @returns {{neighbour: Number, empty: boolean}} - neighbour index, is the neighbour an empty cell or outside the board + */ + find_neighbour(index, direction) { + const rhomb = index % 3; + const cubei = (index - rhomb) / 3; + let c = cubei % this.width; + let r = (cubei - c) / this.width; + let neighbour = -1; + + const [hexdir, rh] = this.#RHOMB_NEIGHBOURS.get(rhomb)?.get(direction) || [0, 0]; + if (hexdir != 0) { + const { neighbour, empty } = this.hexagrid.find_neighbour(cubei, hexdir); + const cubeNeighbour = neighbour === -1 ? -1 : neighbour * 3 + rh; + const cubeEmpty = empty || this.emptyCells.has(cubeNeighbour); + return {neighbour: cubeNeighbour, empty: cubeEmpty}; + } + const cubeNeighbour = index - rhomb + rh; + const empty = this.emptyCells.has(cubeNeighbour); + return { neighbour: cubeNeighbour, empty }; + } + + /** + * Get index of tile located at row r column c rhomb b + * @param {Number} r + * @param {Number} c + * @param {Number} b + * @returns {Number} + */ + rcb_to_index(r, c, b) { + const index = this.hexagrid.rc_to_index(r, c); + return index * 3 + b; + } + + /** + * Makes cell at index empty + * @param {Number} index + */ + makeEmpty(index) { + this.emptyCells.add(index); + } + + /** + * @param {Number} index + * @returns {RegularPolygonTile} + */ + polygon_at(index) { + return [RIGHT_FACE, TOP_FACE, LEFT_FACE][index % 3]; + } + + /** + * Compute tile orientation after a number of rotations + * @param {Number} tile + * @param {Number} rotations + * @returns + */ + rotate(tile, rotations) { + return LEFT_FACE.rotate(tile, rotations); + } + + /** + * Get angle for displaying rotated pipes state + * @param {Number} rotations + * @param {Number} index + * @returns + */ + getAngle(rotations, index) { + return this.polygon_at(index).get_angle(rotations); + } + + /** + * Get CSS transform function parameters for this tile + * @param {Number} index + */ + getTileTransformCSS(index) { + return this.polygon_at(index).transformCSS; + } + + /** + * + * @param {Number} tile + * @param {Number} rotations + * @returns {Number[]} + */ + getDirections(tile, rotations = 0) { + return LEFT_FACE.get_directions(tile, rotations); + } + + /** + * @param {import('$lib/puzzle/viewbox').ViewBox} box + * @returns {import('$lib/puzzle/viewbox').VisibleTile[]} + */ + getVisibleTiles(box) { + const vishex = this.hexagrid.getVisibleTiles(box); + const visibleTiles = []; + for (const vt of vishex) { + for (let b = 0; b < 3; ++b) { + const {x, y} = vt; + const ofs = RHOMB_OFFSETS.get(b); + const key = `${Math.round(10 * x)}_${Math.round(10 * y)}_${b}`; + visibleTiles.push({ + index: vt.index * 3 + b, + x: x + ofs[0], + y: y + ofs[1], + key + }); + } + } + return visibleTiles; + } + + /** + * Tile contour path for svg drawing + * @param {Number} index + * @returns + */ + getTilePath(index) { + return this.polygon_at(index).contour_path; + } + + /** + * Pipes lines path + * @param {Number} tile + * @param {Number} index + */ + getPipesPath(tile, index) { + return this.polygon_at(index).get_pipes_path(tile); + } + + /** + * Computes position for drawing the tile guiding dot + * @param {Number} tile + * * @param {Number} index + * @returns {Number[]} + */ + getGuideDotPosition(tile, index) { + const [dx, dy] = this.polygon_at(index).get_guide_dot_position(tile); + return [0.8 * dx, 0.8 * dy]; + } + /** + * Compute number of rotations for orienting a tile with "click to orient" control mode + * @param {Number} tile + * @param {Number} old_rotations + * @param {Number} tx + * @param {Number} ty + * @param {Number} index + */ + clickOrientTile(tile, old_rotations, tx, ty, index = 0) { + return this.polygon_at(index).click_orient_tile(tile, old_rotations, tx, ty); + } + /** + * Returns coordinates of endpoints of edgemark line + * @param {Number} direction + * @param {Boolean} isWall + * @param {Number} index + * @returns + */ + getEdgemarkLine(direction, isWall, index = 0) { + return this.polygon_at(index).get_edgemark_line(direction, isWall); + } + /** + * Check if a drag gesture resembles drawing an edge mark + * @param {Number} tile_index + * @param {Number} x1 + * @param {Number} x2 + * @param {Number} y1 + * @param {Number} y2 + */ + detectEdgemarkGesture(tile_index, tile_x, tile_y, x1, x2, y1, y2) { + return this.polygon_at(tile_index).detect_edgemark_gesture(x1 - tile_x, x2 - tile_x, y1 - tile_y, y2 - tile_y); + } + + /** + * Tells if a point is close to one of tile's edges + * @param {import('$lib/puzzle/controls').PointerOrigin} point + */ + whichEdge(point) { + return this.polygon_at(point.tileIndex).is_close_to_edge(point.x - point.tileX, point.y - point.tileY); + } +} diff --git a/src/lib/puzzle/grids/cubegrid.test.js b/src/lib/puzzle/grids/cubegrid.test.js new file mode 100644 index 00000000..d3463bc4 --- /dev/null +++ b/src/lib/puzzle/grids/cubegrid.test.js @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest'; +import { CubeGrid } from './cubegrid'; + +describe('Test making a cell empty', () => { + const grid = new CubeGrid(3, 3, false); + grid.makeEmpty(14); + + it('Reports an empty neighbour', () => { + const { neighbour, empty } = grid.find_neighbour(9, 8); + expect(neighbour).toBe(14); + expect(empty).toBe(true); + }); + + it('Reports another empty neighbour', () => { + const { neighbour, empty } = grid.find_neighbour(12, 2); + expect(neighbour).toBe(14); + expect(empty).toBe(true); + }); + + it('Reports a non-empty neighbour', () => { + const { neighbour, empty } = grid.find_neighbour(9, 4); + expect(neighbour).toBe(22); + expect(empty).toBe(false); + }); + + it('Reports a non-neighbour when going outside the grid', () => { + const { neighbour, empty } = grid.find_neighbour(18, 4); + expect(neighbour).toBe(-1); + expect(empty).toBe(true); + }); + + it('Reports a non-neighbour when going outside the grid', () => { + const { neighbour, empty } = grid.find_neighbour(4, 4); + expect(neighbour).toBe(-1); + expect(empty).toBe(true); + }); + + it('Reports a non-neighbour when going outside the grid', () => { + const { neighbour, empty } = grid.find_neighbour(4, 8); + expect(neighbour).toBe(-1); + expect(empty).toBe(true); + }); + + it('Reports a non-neighbour when going outside the grid', () => { + const { neighbour, empty } = grid.find_neighbour(11, 4); + expect(neighbour).toBe(-1); + expect(empty).toBe(true); + }); + + it('Reports a non-neighbour when going outside the grid', () => { + const { neighbour, empty } = grid.find_neighbour(24, 8); + expect(neighbour).toBe(-1); + expect(empty).toBe(true); + }); +}); diff --git a/src/lib/puzzle/grids/etratgrid.js b/src/lib/puzzle/grids/etratgrid.js index 0cacb363..72f63efa 100644 --- a/src/lib/puzzle/grids/etratgrid.js +++ b/src/lib/puzzle/grids/etratgrid.js @@ -252,6 +252,14 @@ export class EtratGrid { return this.polygon_at(index).get_angle(rotations); } + /** + * Get CSS transform function parameters for this tile + * @param {Number} index + */ + getTileTransformCSS(index) { + return null; + } + /** * * @param {Number} tile @@ -337,24 +345,27 @@ export class EtratGrid { const [dx, dy] = this.polygon_at(index).get_guide_dot_position(tile); return [0.8 * dx, 0.8 * dy]; } + /** * Compute number of rotations for orienting a tile with "click to orient" control mode * @param {Number} tile * @param {Number} old_rotations - * @param {Number} new_angle + * @param {Number} tx + * @param {Number} ty * @param {Number} index */ - clickOrientTile(tile, old_rotations, new_angle, index = 0) { - return this.polygon_at(index).click_orient_tile(tile, old_rotations, new_angle); + clickOrientTile(tile, old_rotations, tx, ty, index = 0) { + return this.polygon_at(index).click_orient_tile(tile, old_rotations, Math.atan2(-ty, tx)); } /** * Returns coordinates of endpoints of edgemark line * @param {Number} direction + * @param {Boolean} isWall * @param {Number} index * @returns */ - getEdgemarkLine(direction, index = 0) { + getEdgemarkLine(direction, isWall, index = 0) { return this.polygon_at(index).get_edgemark_line(direction); } diff --git a/src/lib/puzzle/grids/grids.js b/src/lib/puzzle/grids/grids.js index afc1839c..ad341992 100644 --- a/src/lib/puzzle/grids/grids.js +++ b/src/lib/puzzle/grids/grids.js @@ -2,13 +2,14 @@ import { HexaGrid } from '$lib/puzzle/grids/hexagrid'; import { SquareGrid } from '$lib/puzzle/grids/squaregrid'; import { OctaGrid } from '$lib/puzzle/grids/octagrid'; import { EtratGrid } from '$lib/puzzle/grids/etratgrid'; +import { CubeGrid } from '$lib/puzzle/grids/cubegrid'; /** - * @typedef {'hexagonal'|'square'|'octagonal'|'etrat'} GridKind + * @typedef {'hexagonal'|'square'|'octagonal'|'etrat'|'cube'} GridKind */ /** - * @typedef {'hexagonal'|'hexagonal-wrap'|'square'|'square-wrap'|'octagonal'|'octagonal-wrap'|'etrat'|'etrat-wrap'} GridCategory + * @typedef {'hexagonal'|'hexagonal-wrap'|'square'|'square-wrap'|'octagonal'|'octagonal-wrap'|'etrat'|'etrat-wrap'|'cube'|'cube-wrap'} GridCategory */ /** @@ -43,6 +44,8 @@ export function createGrid(kind, width, height, wrap, tiles = undefined) { grid = new SquareGrid(width, height, wrap, tiles); } else if (kind === 'etrat') { grid = new EtratGrid(width, height, wrap, tiles); + } else if (kind === 'cube') { + grid = new CubeGrid(width, height, wrap, tiles); } else { throw `Unknown grid kind ${kind}`; } @@ -50,7 +53,7 @@ export function createGrid(kind, width, height, wrap, tiles = undefined) { } /** @type {GridKind[]} */ -export const gridKinds = ['hexagonal', 'square', 'octagonal', 'etrat']; +export const gridKinds = ['hexagonal', 'square', 'octagonal', 'etrat', 'cube']; export const gridInfo = { hexagonal: { @@ -80,5 +83,16 @@ export const gridInfo = { wrap: true, exampleGrid: new EtratGrid(3, 3, false), exampleTiles: [0, 9, 2, 0, 7, 6, 0, 2, 6, 9, 6, 0, 12, 7, 0, 4, 2, 0] + }, + cube: { + title: 'Cube', + url: 'cube', + wrap: true, + exampleGrid: new CubeGrid(3, 3, false), + exampleTiles: [ + 3, 1, 3, 10, 4, 1, 5, 12, 5, + 13, 7, 11, 13, 7, 11, 8, 1, 10, + 9, 11, 8, 4, 4, 2, 1, 9, 7 + ] } }; diff --git a/src/lib/puzzle/grids/hexagrid.js b/src/lib/puzzle/grids/hexagrid.js index b2b38655..82c5e837 100644 --- a/src/lib/puzzle/grids/hexagrid.js +++ b/src/lib/puzzle/grids/hexagrid.js @@ -1,11 +1,11 @@ import { RegularPolygonTile } from '$lib/puzzle/grids/polygonutils'; -const EAST = 1; -const NORTHEAST = 2; -const NORTHWEST = 4; -const WEST = 8; -const SOUTHWEST = 16; -const SOUTHEAST = 32; +export const EAST = 1; +export const NORTHEAST = 2; +export const NORTHWEST = 4; +export const WEST = 8; +export const SOUTHWEST = 16; +export const SOUTHEAST = 32; const YSTEP = Math.sqrt(3) / 2; @@ -310,6 +310,14 @@ export class HexaGrid { return HEXAGON.get_angle(rotations); } + /** + * Get CSS transform function parameters for this tile + * @param {Number} index + */ + getTileTransformCSS(index) { + return null; + } + /** * * @param {Number} tile @@ -477,24 +485,27 @@ export class HexaGrid { const [dx, dy] = HEXAGON.get_guide_dot_position(tile); return [0.8 * dx, 0.8 * dy]; } + /** * Compute number of rotations for orienting a tile with "click to orient" control mode * @param {Number} tile * @param {Number} old_rotations - * @param {Number} new_angle + * @param {Number} tx + * @param {Number} ty * @param {Number} index */ - clickOrientTile(tile, old_rotations, new_angle, index = 0) { - return HEXAGON.click_orient_tile(tile, old_rotations, new_angle); + clickOrientTile(tile, old_rotations, tx, ty, index = 0) { + return HEXAGON.click_orient_tile(tile, old_rotations, Math.atan2(-ty, tx)); } /** * Returns coordinates of endpoints of edgemark line * @param {Number} direction + * @param {Boolean} isWall * @param {Number} index * @returns */ - getEdgemarkLine(direction, index = 0) { + getEdgemarkLine(direction, isWall, index = 0) { return HEXAGON.get_edgemark_line(direction); } diff --git a/src/lib/puzzle/grids/octagrid.js b/src/lib/puzzle/grids/octagrid.js index d9da2de9..bc15dfb0 100644 --- a/src/lib/puzzle/grids/octagrid.js +++ b/src/lib/puzzle/grids/octagrid.js @@ -330,6 +330,13 @@ export class OctaGrid { return this.polygon_at(index).get_angle(rotations); } + /** + * @param {Number} index + */ + getTileTransformCSS(index) { + return null; + } + /** * * @param {Number} tile @@ -374,20 +381,22 @@ export class OctaGrid { * Compute number of rotations for orienting a tile with "click to orient" control mode * @param {Number} tile * @param {Number} old_rotations - * @param {Number} new_angle + * @param {Number} tx + * @param {Number} ty * @param {Number} index */ - clickOrientTile(tile, old_rotations, new_angle, index = 0) { - return this.polygon_at(index).click_orient_tile(tile, old_rotations, new_angle); + clickOrientTile(tile, old_rotations, tx, ty, index = 0) { + return this.polygon_at(index).click_orient_tile(tile, old_rotations, Math.atan2(-ty, tx)); } /** * Returns coordinates of endpoints of edgemark line * @param {Number} direction + * @param {Boolean} isWall * @param {Number} index * @returns */ - getEdgemarkLine(direction, index = 0) { + getEdgemarkLine(direction, isWall, index = 0) { return this.polygon_at(index).get_edgemark_line(direction); } diff --git a/src/lib/puzzle/grids/polygonutils.js b/src/lib/puzzle/grids/polygonutils.js index b6f57297..334e2d34 100644 --- a/src/lib/puzzle/grids/polygonutils.js +++ b/src/lib/puzzle/grids/polygonutils.js @@ -1,3 +1,5 @@ +import { scale, skew, rotate, compose, inverse, applyToPoint } from 'transformation-matrix'; + export class TileType { /** * @@ -360,8 +362,8 @@ export class RegularPolygonTile { * Returns coordinates for drawing edgemark line relative to tile center * @param {Number} direction */ - get_edgemark_line(direction) { - const cached = this.cache.edgemark_line.get(direction); + get_edgemark_line(direction, extendOut = true) { + const cached = this.cache.edgemark_line.get(`${direction}-${extendOut}`);; if (cached !== undefined) { return cached; } @@ -376,10 +378,44 @@ export class RegularPolygonTile { const line = { x1: offset_x - dx, y1: -offset_y + dy, - x2: offset_x + dx, - y2: -offset_y - dy + x2: offset_x + (extendOut ? dx : 0), + y2: -offset_y - (extendOut ? dy : 0) }; - this.cache.edgemark_line.set(direction, line); + this.cache.edgemark_line.set(`${direction}-${extendOut}`, line); return line; } } + + +export class TransformedPolygonTile extends RegularPolygonTile { + constructor(num_directions, angle_offset, radius_in, directions, border_width, scaleX, scaleY, skewX, skewY, rotateTh) { + super(num_directions, angle_offset, radius_in, directions, border_width); + // we could instead simplify the CSS & matrix construction + scaleX = scaleX || 1; + scaleY = scaleY || 1; + skewX = skewX || 0; + skewY = skewY || 0; + rotateTh = rotateTh || 0; + this.transformCSS = `rotate(${rotateTh}rad) skew(${skewX}rad, ${skewY}rad) scale(${scaleX}, ${scaleY})`; + this.transformMatrix = compose(rotate(rotateTh), skew(skewX, skewY), scale(scaleX, scaleY)); + this.transformInverse = inverse(this.transformMatrix); + } + + click_orient_tile(tile, old_rotations, tx, ty) { + const {x, y} = applyToPoint(this.transformInverse, {x: tx, y: ty}); + return super.click_orient_tile(tile, old_rotations, Math.atan2(-y, x)); + } + + detect_edgemark_gesture(tx1, tx2, ty1, ty2) { + const gridTileDownPt = { x: tx1, y: ty1 }; + const gridTileUpPt = { x: tx2, y: ty2 }; + const polygonDownPt = applyToPoint(this.transformInverse, gridTileDownPt); + const polygonUpPt = applyToPoint(this.transformInverse, gridTileUpPt); + return super.detect_edgemark_gesture(polygonDownPt.x, polygonUpPt.x, -polygonDownPt.y, -polygonUpPt.y); + } + + is_close_to_edge(tx, ty) { + const polyPt = applyToPoint(this.transformInverse, { x: tx, y: ty }); + return super.is_close_to_edge(polyPt.x, -polyPt.y); + } +} \ No newline at end of file diff --git a/src/lib/puzzle/grids/squaregrid.js b/src/lib/puzzle/grids/squaregrid.js index 88cf2e0e..d54c8259 100644 --- a/src/lib/puzzle/grids/squaregrid.js +++ b/src/lib/puzzle/grids/squaregrid.js @@ -192,6 +192,14 @@ export class SquareGrid { return SQUARE.get_angle(rotations); } + /** + * Get CSS transform function parameters for this tile + * @param {Number} index + */ + getTileTransformCSS(index) { + return null; + } + /** * * @param {Number} tile @@ -268,23 +276,27 @@ export class SquareGrid { const [dx, dy] = SQUARE.get_guide_dot_position(tile); return [0.8 * dx, 0.8 * dy]; } + /** * Compute number of rotations for orienting a tile with "click to orient" control mode * @param {Number} tile * @param {Number} old_rotations - * @param {Number} new_angle + * @param {Number} tx + * @param {Number} ty * @param {Number} index */ - clickOrientTile(tile, old_rotations, new_angle, index = 0) { - return SQUARE.click_orient_tile(tile, old_rotations, new_angle); + clickOrientTile(tile, old_rotations, tx, ty, index = 0) { + return SQUARE.click_orient_tile(tile, old_rotations, Math.atan2(-ty, tx)); } + /** * Returns coordinates of endpoints of edgemark line * @param {Number} direction + * @param {Boolean} isWall * @param {Number} index * @returns */ - getEdgemarkLine(direction, index = 0) { + getEdgemarkLine(direction, isWall, index = 0) { return SQUARE.get_edgemark_line(direction); } diff --git a/src/lib/puzzle/solver.js b/src/lib/puzzle/solver.js index 5fd30710..6c256991 100644 --- a/src/lib/puzzle/solver.js +++ b/src/lib/puzzle/solver.js @@ -356,7 +356,7 @@ export function Solver(tiles, grid) { walls += direction; } neighbourTiles.push( - self.grid.polygon_at(neighbour).tileTypes.get(self.tiles[neighbour]) || null + neighbour >= 0 && self.grid.polygon_at(neighbour).tileTypes.get(self.tiles[neighbour]) || null ); } // remove orientations that contradict outer walls diff --git a/src/lib/puzzleWrapper/PuzzleInstanceWrapper.svelte b/src/lib/puzzleWrapper/PuzzleInstanceWrapper.svelte index f92379d1..f7401c30 100644 --- a/src/lib/puzzleWrapper/PuzzleInstanceWrapper.svelte +++ b/src/lib/puzzleWrapper/PuzzleInstanceWrapper.svelte @@ -93,7 +93,7 @@ let branchingAmount = 0.6; let avoidObvious = 0; let avoidStraights = 0; - if (gridKind === 'square' || gridKind === 'etrat') { + if (gridKind === 'square' || gridKind === 'etrat' || gridKind === 'cube') { branchingAmount = Math.random() * 0.5 + 0.5; // 0.5 to 1 avoidObvious = Math.random() * 0.5 + 0.1; // 0.1 to 0.6 avoidStraights = Math.random() * 0.5 + 0.25; // 0.25 to 0.75 diff --git a/src/params/gridkind.js b/src/params/gridkind.js index af564398..ca3a3e36 100644 --- a/src/params/gridkind.js +++ b/src/params/gridkind.js @@ -1,6 +1,6 @@ /** @type {import('@sveltejs/kit').ParamMatcher} */ export function match(param) { - return /hexagonal|hexagonal-wrap|square|square-wrap|octagonal|octagonal-wrap|etrat|etrat-wrap/.test( + return /hexagonal|hexagonal-wrap|square|square-wrap|octagonal|octagonal-wrap|etrat|etrat-wrap|cube|cube-wrap/.test( param ); } diff --git a/src/routes/custom/+page.svelte b/src/routes/custom/+page.svelte index 032e12e2..6c817a20 100644 --- a/src/routes/custom/+page.svelte +++ b/src/routes/custom/+page.svelte @@ -178,6 +178,9 @@ +