@ne1410s/griddler
Version:
Complete package for creating, sharing and solving griddler grids!
1,109 lines (1,085 loc) • 70.2 kB
JavaScript
'use strict';
var custElems = require('@ne1410s/cust-elems');
require('@ne1410s/menu');
var dom = require('@ne1410s/dom');
var popup = require('@ne1410s/popup');
require('@ne1410s/pxl8r');
/** The state of a cell. Values are unicode representations of the state. */
var CellState;
(function (CellState) {
CellState[CellState["Blank"] = 9723] = "Blank";
CellState[CellState["Marked"] = 9635] = "Marked";
CellState[CellState["Filled"] = 9724] = "Filled";
})(CellState || (CellState = {}));
/** The type of a set of cells. */
var SetType;
(function (SetType) {
SetType[SetType["Column"] = 0] = "Column";
SetType[SetType["Row"] = 1] = "Row";
})(SetType || (SetType = {}));
class Label {
/** Gets the minimum total size for a set of separated values. */
static minSize(values) {
return values.reduce((tot, curr) => {
return tot > 0 ? curr + tot + 1 : curr + tot;
}, 0);
}
constructor(value, index) {
this.value = value;
this.index = index;
this.indexRef = `L${this.index}`;
}
}
class LabelSetLink {
constructor(labelIndex, setIndex, known) {
this.labelIndex = labelIndex;
this.setIndex = setIndex;
this.known = known;
}
}
/** A contiguous set of cells. */
class CellSetBase {
constructor(start, type, index, size) {
this.start = start;
this.type = type;
this.index = index;
this.size = size;
this.end = this.start + this.size - 1;
}
}
/** A set of consecutive 'filled' cells. */
class BlockSet extends CellSetBase {
constructor(start, type, index, size, spaceIndex) {
super(start, type, index, size);
this.start = start;
this.type = type;
this.index = index;
this.size = size;
this.spaceIndex = spaceIndex;
}
}
/** A set of unmarked cells. */
class SpaceSet extends CellSetBase {
}
/** A complete set of cells - representing a column or row. */
class FullSet extends CellSetBase {
get stateRef() {
return `${this.cells.map((state) => String.fromCharCode(state)).join('')}`;
}
get consoleRef() {
return `${this.indexRef}: ${this.stateRef} ${this.labelRef}`;
}
get labelRef() {
return this.labels.map((l) => l.value).join('.');
}
get labelsRef() {
return this.labels.map((l, i) => this.getLabelRef(i)).join(' / ');
}
get solved() {
return !this.cells.some((state) => state === CellState.Blank);
}
constructor(start, type, index, cells, labelValues) {
super(start, type, index, cells.length);
this.start = start;
this.type = type;
this.index = index;
this.cells = cells;
this.labelSpaceMap = [];
this.labelBlockMap = [];
this.spaces = [];
this.blocks = [];
this.altType = this.type === SetType.Row ? SetType.Column : SetType.Row;
this.labels = labelValues.map((v, i) => new Label(v, i));
this.indexRef = `${SetType[this.type].substr(0, 3)} ${this.index}`;
this.performCellPass(true);
this.performCellPass(false);
this.setLabelSpaces();
this.setLabelBlocks();
let linksChanged;
do {
linksChanged = this.updateMaps();
this.applyBlockValueRanges();
linksChanged = linksChanged || this.applyBlockPositionRanges();
linksChanged = linksChanged || this.applyDistinctBlockPairing();
} while (linksChanged);
}
/** Marks and fills all appropriate blocks, returning indices of the mark and fill cells. */
solve() {
const blanx = this.cells
.map((cell, idx) => ({ c: cell, i: idx }))
.filter((ic) => ic.c === CellState.Blank);
const mIdx = this.labels
.reduce((ac, l) => ac.filter((ib) => ib.i < l.earliest || ib.i > l.latest), blanx)
.map((mark) => mark.i);
const fIdx = this.labels
.reduce((ac, l) => ac.concat(blanx.filter((ib) => ib.i < l.earliest + l.value && ib.i > l.latest - l.value)), [])
.map((fill) => fill.i);
this.blocks // Add fills for edged-out blocks
.filter((b) => b.rightEdge !== b.end || b.leftEdge !== b.start)
.forEach((b) => blanx
.filter((ic) => ic.i >= b.leftEdge && ic.i <= b.rightEdge)
.forEach((ci) => fIdx.push(ci.i)));
this.blocks // Add marks to blocks at their maximum
.filter((b) => 1 + b.rightEdge - b.leftEdge === b.maxSize)
.forEach((b) => {
mIdx.push(b.leftEdge - 1);
mIdx.push(b.rightEdge + 1);
});
const mIdxFilt = blanx // mIdx was originally a Set to prevent duplicates, but array means es5.
.filter((ic) => mIdx.indexOf(ic.i) !== -1)
.map((ic) => ic.i)
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
const fIdxFilt = blanx // fIdx was originally a Set to prevent duplicates; but array means es5.
.filter((ic) => fIdx.indexOf(ic.i) !== -1)
.map((ic) => ic.i)
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
mIdxFilt.forEach((mInd) => (this.cells[mInd] = CellState.Marked));
fIdxFilt.forEach((fInd) => (this.cells[fInd] = CellState.Filled));
return { marks: mIdxFilt, fills: fIdxFilt };
}
getLabelRef(index) {
const label = this.labels[index];
const sLinks = this.getLinksForLabel(index, true).map((sl) => sl.setIndex + (sl.known ? 'K' : 'M'));
const bLinks = this.getLinksForLabel(index, false).map((bl) => bl.setIndex + (bl.known ? 'K' : 'M'));
return `${label.indexRef}: R=${label.earliest}-${label.latest} S=${sLinks} B=${bLinks}`;
}
getLinksForLabel(lIndex, forSpace) {
return (forSpace ? this.labelSpaceMap : this.labelBlockMap).filter((mi) => mi.labelIndex === lIndex);
}
getLinksForSet(setIndex, forSpace) {
return (forSpace ? this.labelSpaceMap : this.labelBlockMap).filter((mi) => mi.setIndex === setIndex);
}
deleteLink(lIndex, setIndex, forSpace) {
const mapName = forSpace ? 'labelSpaceMap' : 'labelBlockMap';
this[mapName] = this[mapName].filter((mi) => mi.labelIndex !== lIndex || mi.setIndex !== setIndex);
}
upsertLink(lIndex, setIndex, forSpace, known) {
const map = forSpace ? this.labelSpaceMap : this.labelBlockMap;
const mapItems = map.filter((mi) => mi.labelIndex === lIndex && mi.setIndex === setIndex);
if (mapItems.length === 1) {
mapItems[0].known = known;
}
else {
map.push(new LabelSetLink(lIndex, setIndex, known));
}
}
upsertLinks(lIndex, forSpace, sets, known) {
sets.forEach((set) => this.upsertLink(lIndex, set.index, forSpace, known));
}
/**
* Iterates cells in a particular direction. The forward pass sets spaces and blocks for the
* set as well as earliest label positions. The backward pass sets labels latest positions only.
* NB: This method is not reasonably capable of managing block-block interactions.
*/
performCellPass(forwards) {
let spaceStart = -1;
let blockStart = -1;
let blockIndex = -1;
let labelStart = -1;
let blocks = [];
let labelIndex = forwards ? 0 : this.labels.length - 1;
const allCells = forwards ? this.cells.slice(0) : this.cells.slice(0).reverse();
const labelIncrementor = forwards ? 1 : -1;
// Clone the array and explicitly terminate it by a marked cell
allCells.concat([CellState.Marked]).forEach((cell, i) => {
// START SPACE AND LABEL
if (spaceStart === -1 && cell !== CellState.Marked) {
spaceStart = i;
labelStart = i;
}
// START BLOCK
if (blockStart === -1 && cell === CellState.Filled) {
blockStart = i;
}
// LABEL
const label = this.labels[labelIndex];
// If the label has reached its end
if (label && labelStart !== -1 && i - labelStart >= label.value) {
// If the left-bunched label ends with a block...
if (blockStart !== -1) {
labelStart = i - label.value;
}
else {
if (forwards) {
label.earliest = labelStart;
}
else {
label.latest = this.cells.length - (labelStart + 1);
}
labelStart += 1 + label.value;
labelIndex += labelIncrementor;
}
}
// END BLOCK
const spaceIndex = this.spaces.length;
if (blockStart !== -1 && cell !== CellState.Filled) {
const blockLen = i - blockStart;
blocks.push(new BlockSet(blockStart, this.type, ++blockIndex, blockLen, spaceIndex));
// Closing a block whose length exceeds the label value must reset label start
if (label && blockLen > label.value) {
labelStart = i + 1;
}
// If at the end of block, and we are still on the same label and it fitted ok
if (label && i - labelStart === label.value) {
if (forwards) {
label.earliest = labelStart;
}
else {
label.latest = this.cells.length - (labelStart + 1);
}
labelIndex += labelIncrementor;
labelStart += 1 + label.value;
}
blockStart = -1;
}
// END SPACE
if (spaceStart !== -1 && cell === CellState.Marked) {
if (forwards) {
const space = new SpaceSet(spaceStart, this.type, spaceIndex, i - spaceStart);
this.spaces.push(space);
this.blocks.push(...blocks);
}
// If at the end of space, and we are still on the same label and it fitted ok
if (label && label.index === labelIndex && i - labelStart >= label.value) {
if (forwards) {
label.earliest = labelStart;
}
else {
label.latest = this.cells.length - (labelStart + 1);
}
labelIndex += labelIncrementor;
}
blocks = [];
spaceStart = -1;
labelStart = this.cells.length;
}
});
}
setLabelSpaces() {
this.labels.forEach((l) => {
const spaces = this.spaces.filter((s) => l.earliest <= s.end && l.latest >= s.start);
if (spaces.length === 0) {
const msg = `At least one label could not be assigned`;
throw new RangeError(`${msg} - ${this.consoleRef}`);
}
this.upsertLinks(l.index, true, spaces, spaces.length === 1);
});
}
setLabelBlocks() {
this.labels.forEach((l) => {
const labelSpaces = this.getLinksForLabel(l.index, true);
const labelBlocks = labelSpaces.reduce((acc, curr) => {
// Blocks not exceeding label value, within range
const ranged = this.blocks.filter((b) => b.start >= l.earliest && b.end <= l.latest && b.size <= l.value);
return acc.concat(ranged);
}, []);
this.upsertLinks(l.index, false, labelBlocks, false);
});
}
/** For each block with only 1 linked label, make the linkage known */
updateMaps() {
let linksChanged = false;
this.blocks.forEach((block) => {
const links = this.labelBlockMap.filter((bl) => bl.setIndex === block.index);
if (links.length === 1) {
const lIdx = links[0].labelIndex;
const label = this.labels[lIdx];
// Make a block-label link 'known'
this.upsertLink(lIdx, block.index, false, true);
// Which requires: Updating earliest and latest values
const space = this.spaces.filter((s) => s.index === block.spaceIndex)[0];
label.earliest = Math.max(label.earliest, space.start, 1 + block.end - label.value);
label.latest = Math.min(label.latest, space.end, block.start + label.value - 1);
// And: Removing space-links no longer in range
this.getLinksForLabel(lIdx, true)
.map((ls) => this.spaces[ls.setIndex])
.filter((s) => label.earliest > s.end || label.latest < s.start)
.forEach((deadLink) => this.deleteLink(lIdx, deadLink.index, true));
// Which itself requires: If one maybe space, making this known
this.getLinksForLabel(lIdx, true)
.filter((ls, i, arr) => arr.length === 1)
.forEach((knownLink) => this.upsertLink(lIdx, knownLink.setIndex, true, true));
// And finally: Removing block-links no longer in range
this.labelBlockMap
.filter((lb) => lb.setIndex !== block.index && lb.labelIndex === lIdx)
.map((lb) => this.blocks[lb.setIndex])
.filter((b) => b.start > label.latest || b.end < label.earliest)
.forEach((deadLink) => {
linksChanged = true;
this.deleteLink(lIdx, deadLink.index, false);
});
}
});
return linksChanged;
}
/** Now the maps are good set min and max values based on labels. */
applyBlockValueRanges() {
this.blocks.forEach((block) => {
block.minSize = this.cells.length;
block.maxSize = 0;
this.getLinksForSet(block.index, false)
.map((bl) => this.labels[bl.labelIndex])
.forEach((l) => {
block.minSize = Math.min(block.minSize, l.value);
block.maxSize = Math.max(block.maxSize, l.value);
});
});
}
/** Now min and max are good, inspect for unbridgable blocks and return whether links have changed as a result. */
applyBlockPositionRanges() {
let linksChanged = false;
this.blocks.forEach((block, bIdx) => {
const space = this.spaces.filter((s) => s.index === block.spaceIndex)[0];
const sibBlocks = this.blocks.filter((b) => b.spaceIndex === block.spaceIndex);
// Neighbouring unbridgable blocks
const prevUnBlk = sibBlocks.filter((b) => b.index === bIdx - 1 && 1 + block.start - b.maxSize > b.end)[0];
const nextUnBlk = sibBlocks.filter((b) => b.index === bIdx + 1 && block.end + b.maxSize - 1 < b.start)[0];
const unlEdge = prevUnBlk == null ? space.start : prevUnBlk.end + 2;
const unrEdge = nextUnBlk == null ? space.end : nextUnBlk.start - 2;
if (prevUnBlk) {
linksChanged = linksChanged || this.tryRemoveLinks(bIdx, false);
}
if (nextUnBlk) {
linksChanged = linksChanged || this.tryRemoveLinks(bIdx, true);
}
// Edging away from calculated edges
block.leftEdge = Math.min(block.start, 1 + unrEdge - block.minSize);
block.rightEdge = Math.max(block.end, unlEdge + block.minSize - 1);
// If edged out, update known labels of the block according to new limits
if (block.leftEdge < block.start || block.rightEdge > block.end) {
this.labelBlockMap
.filter((bl) => bl.setIndex === block.index && bl.known)
.forEach((bl) => {
const label = this.labels[bl.labelIndex];
label.earliest = Math.max(label.earliest, space.start, 1 + block.rightEdge - label.value);
label.latest = Math.min(label.latest, space.end, block.leftEdge + label.value - 1);
});
}
});
return linksChanged;
}
/** Checks whether block and neighbouring unbridgable block need labels removing. */
tryRemoveLinks(blockIndex, forNext) {
const neighbourIndex = forNext ? blockIndex + 1 : blockIndex - 1;
const blockLabelLinks = this.getLinksForSet(blockIndex, false);
const blockLabelIdx = this.getLinksForSet(blockIndex, false)
.map((li) => li.labelIndex)
.join(',');
const unbrLabelIdx = this.getLinksForSet(neighbourIndex, false)
.map((li) => li.labelIndex)
.join(',');
if (blockLabelLinks.length === 2 && unbrLabelIdx === blockLabelIdx) {
this.deleteLink(blockLabelLinks[forNext ? 0 : 1].labelIndex, neighbourIndex, false);
this.deleteLink(blockLabelLinks[forNext ? 1 : 0].labelIndex, blockIndex, false);
return true;
}
return false;
}
/** Removes block/label links where label count matches distinct block count */
applyDistinctBlockPairing() {
let linksChanged;
let prevBlock;
let prevInReach;
let labelIndex;
const labelAssignment = [];
// Assemble distinct block count by label index
this.blocks.forEach((currBlock, blockIndex) => {
prevBlock = this.blocks[blockIndex - 1];
prevInReach =
prevBlock &&
prevBlock.spaceIndex === currBlock.spaceIndex &&
prevBlock.start + prevBlock.maxSize - 1 >= currBlock.rightEdge;
labelIndex = prevInReach ? labelAssignment.length - 1 : labelAssignment.length;
labelAssignment[labelIndex] = labelAssignment[labelIndex] || [];
labelAssignment[labelIndex].push(blockIndex);
});
// If counts match, remove block links except for those assembled
if (labelAssignment.length === this.labels.length) {
this.labels.forEach((lbl) => {
this.getLinksForLabel(lbl.index, false)
.filter((ln) => labelAssignment[lbl.index].indexOf(ln.setIndex) === -1)
.forEach((dl) => {
linksChanged = true;
this.deleteLink(dl.labelIndex, dl.setIndex, false);
});
});
return linksChanged;
}
return false;
}
}
/** The outcome of running a 'solve' grid method. */
class SolveResult {
constructor(gridObject, solved, solvedMs) {
this.solved = solved;
this.solvedMs = solvedMs;
if (solved) {
this.grid = gridObject;
}
else {
this.brokenGrid = gridObject;
}
}
}
class Utils {
/** Returns a new array of the specified size filled with the specified value. */
static FillArray(size, valuer) {
const retVal = new Array(size);
for (let i = 0; i < size; i++) {
retVal[i] = valuer();
}
return retVal;
}
/** Pools multiple events, firing once per delay cycle. */
static Throttle(func, delay = 200) {
let active;
return function (args) {
if (!active) {
if (active == null)
func.call(this, args);
active = true;
const that = this;
setTimeout(() => {
active = !!func.call(that, args);
setTimeout(() => (active = active || null), delay / 10);
}, delay);
}
};
}
/** Pools multiple events, firing once after the delay period. */
static Debounce(func, delay = 200) {
let timeout;
return function (arg) {
clearTimeout(timeout);
const that = this;
timeout = setTimeout(() => func.call(that, arg), delay);
};
}
}
/** A griddler grid. */
class Grid {
static load(gridObject) {
const grid = new Grid(gridObject.columns.length, gridObject.rows.length);
gridObject.columns.forEach((col, colIdx) => {
grid.setLabels(SetType.Column, colIdx, col.labels);
});
gridObject.rows.forEach((row, rowIdx) => {
grid.setLabels(SetType.Row, rowIdx, row.labels);
(row.cells || [])
.map((cell, cellIdx) => ({ oState: cell, idx: cellIdx }))
.filter((obj) => obj.oState !== 0)
.forEach((obj) => {
const state = obj.oState === 1 ? CellState.Filled : CellState.Marked;
grid.setState(SetType.Row, rowIdx, obj.idx, state);
});
});
return grid;
}
get consoleRef() {
return this._rowLabelCache.map((n, r) => this.getFullSet(SetType.Row, r).stateRef).join('\r\n');
}
get unsolvedCellCount() {
return this._cellCache
.reduce((ac, cur) => ac.concat(cur), [])
.filter((state) => state === CellState.Blank).length;
}
get solved() {
return !this._cellCache
.reduce((ac, cur) => ac.concat(cur), [])
.some((state) => state === CellState.Blank);
}
get gridObject() {
return {
columns: this._columnLabelCache.map((n, c) => ({
labels: this.getLabels(SetType.Column, c),
})),
rows: this._rowLabelCache.map((n, r) => {
return {
labels: this.getLabels(SetType.Row, r),
cells: this.getFullSet(SetType.Row, r).cells.map((c) => c === CellState.Marked ? 2 : CellState.Filled ? 1 : 0),
};
}),
};
}
constructor(width, height) {
this.width = width;
this.height = height;
this._rowLabelCache = Utils.FillArray(this.height, () => []);
this._columnLabelCache = Utils.FillArray(this.width, () => []);
this._cellCache = this._columnLabelCache.map(() => Utils.FillArray(this.height, () => CellState.Blank));
}
nextHint() {
const allCols = Utils.FillArray(this.width, () => 0).map((x, i) => i);
const allRows = Utils.FillArray(this.height, () => 0).map((x, i) => i);
const colsrows = this.solveSetsRecursively([allCols, allRows], true);
let result = null;
if (colsrows) {
const pass = colsrows[263 % colsrows.length];
result = { type: SetType[pass.type], idx: pass.idx };
}
return result;
}
solve() {
const t0 = new Date().getTime();
const allCols = Utils.FillArray(this.width, () => 0).map((x, i) => i);
const allRows = Utils.FillArray(this.height, () => 0).map((x, i) => i);
this.solveSetsRecursively([allCols, allRows]);
const t1 = new Date().getTime();
return new SolveResult(this.gridObject, this.solved, t1 - t0);
}
setState(setType, setIndex, cellIndex, state) {
if (setType === SetType.Row) {
this._cellCache[cellIndex][setIndex] = state;
}
else {
this._cellCache[setIndex][cellIndex] = state;
}
}
setLabels(type, index, values) {
const setRef = `${SetType[type].substr(0, 3)} ${index}`;
const target = type === SetType.Row ? this._rowLabelCache : this._columnLabelCache;
if (target[index] == null) {
const msg = 'Not found';
throw new RangeError(`${setRef}: ${msg}`);
}
const setSize = type === SetType.Row ? this.width : this.height;
const minSize = Label.minSize(values);
if (minSize > setSize) {
const msg = `The minimum total label size (${minSize}) exceeds the set length (${setSize})`;
throw new RangeError(`${setRef}: ${msg}`);
}
target[index] = values;
}
getFullSet(type, index) {
const cells = type === SetType.Row ? this._cellCache.map((val) => val[index]) : this._cellCache[index];
return new FullSet(0, type, index, cells, this.getLabels(type, index));
}
getLabels(type, index) {
return type === SetType.Row ? this._rowLabelCache[index] : this._columnLabelCache[index];
}
solveSetsRecursively(colsrows, shallow = false) {
const allUnsolvedHintworthy = colsrows[0]
.map((c) => this.getFullSet(SetType.Column, c))
.concat(colsrows[1].map((r) => this.getFullSet(SetType.Row, r)))
.filter((set) => !set.solved)
.map((us) => {
const cr = us.solve();
cr.marks.forEach((m) => this.setState(us.type, us.index, m, CellState.Marked));
cr.fills.forEach((f) => this.setState(us.type, us.index, f, CellState.Filled));
return {
type: us.type,
idx: us.index, // for hints
crType: us.altType,
crIdx: cr.marks.concat(cr.fills), // for solving
};
})
.filter((obj) => obj.crIdx.length !== 0);
if (shallow) {
return allUnsolvedHintworthy;
}
const allUnsolved = allUnsolvedHintworthy
.reduce((ac, obj) => {
ac[obj.crType].push(...obj.crIdx);
return ac;
}, [[], []])
.map((arr) => arr.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)).filter((n, i, x) => !i || n !== x[i - 1]));
if (allUnsolved[0].length + allUnsolved[1].length !== 0) {
this.solveSetsRecursively([allUnsolved[0], allUnsolved[1]]);
}
return null;
}
}
class XGrid {
static AsPlain(grid) {
const g = grid;
const retVal = grid instanceof ImageData
? XGrid.FromImage(grid)
: g.x && g.y
? XGrid.CreatePlain(g.x, g.y)
: g.c && g.r
? XGrid.ToPlain(grid)
: g.rows && g.columns
? grid
: null;
if (retVal == null) {
throw new RangeError('Unable to interpret as a plain grid.');
}
return retVal;
}
static ToDense(plain) {
const derive = (ds) => {
if (!ds.cells || (ds.cells.indexOf(1) === -1 && ds.cells.indexOf(2) === -1)) {
return '';
}
const data = ds.cells
.reduce((acc, cur, i) => {
const symbol = cur === 2 ? 'm' : cur === 1 ? 'f' : 'e';
const isLast = i === ds.cells.length - 1;
if (!acc.symbol) {
acc.symbol = symbol;
acc.count = 0;
}
if (acc.symbol === symbol) {
acc.count++;
}
if (acc.count !== 0 && (acc.symbol !== symbol || isLast)) {
if (acc.symbol !== symbol) {
acc.items.push(`${acc.symbol}${acc.count === 1 ? '' : acc.count}`);
acc.count = 1;
acc.symbol = symbol;
}
if (isLast) {
acc.items.push(`${acc.symbol}${acc.count === 1 ? '' : acc.count}`);
}
}
return acc;
}, { symbol: '', count: 0, items: [] })
.items.join('');
return data ? `.${data}` : '';
};
return {
c: plain.columns.map((c) => (c.labels || []).join('.')).join('|'),
r: plain.rows.map((r) => `${(r.labels || []).join('.')}${derive(r)}`).join('|'),
};
}
static OverlayResult(ref, result) {
const palette = {
fill: {
good: { r: 0, g: 0, b: 0, a: 255 },
bad: { r: 255, g: 0, b: 0, a: 255 },
},
mark: {
good: { r: 0, g: 0, b: 255, a: 32 },
bad: { r: 127, g: 0, b: 0, a: 255 },
},
};
const plain = result.solved ? result.grid : result.brokenGrid;
for (let x = 3; x < ref.data.length; x += 4) {
const rowNum = Math.floor((x - 3) / 4 / ref.width);
const colNum = ((x - 3) / 4) % ref.width;
const state = plain.rows[rowNum].cells[colNum];
const paletteState = state === 1 ? palette.fill : state === 2 && !result.solved ? palette.mark : null;
if (paletteState) {
// blocks and (unsolved) marks
const wasBlock = ref.data[x - 3] === 0 && ref.data[x - 2] === 0 && ref.data[x - 1] === 0;
const stateRef = (state === 1 && wasBlock) || (state === 2 && !wasBlock) ? 'good' : 'bad';
const rgba = paletteState[stateRef];
ref.data[x - 3] = rgba.r;
ref.data[x - 2] = rgba.g;
ref.data[x - 1] = rgba.b;
ref.data[x] = rgba.a;
}
}
}
static WipeCells(plain) {
const emptyRow = Utils.FillArray(plain.columns.length, () => 0);
plain.rows.forEach((r) => (r.cells = emptyRow.slice()));
}
static WipeLabels(plain) {
plain.rows.forEach((r) => (r.labels = []));
plain.columns.forEach((c) => (c.labels = []));
}
static ScrapeLabels(plain) {
XGrid.ScrapeColumnLabels(plain);
const denseRows = XGrid.ToDense(plain).r.split('|');
denseRows.forEach((row, i) => {
plain.rows[i].labels = (row.match(/f\d*/g) || []).map((fd) => parseInt(fd.substring(1) || '1'));
});
}
static CreatePlain(columns, rows) {
const emptyRow = Utils.FillArray(columns, () => 0);
return {
columns: Utils.FillArray(columns, () => ({ labels: [] })),
rows: Utils.FillArray(rows, () => ({ labels: [], cells: emptyRow.slice() })),
};
}
static ToPlain(dense) {
const cols = dense.c.split('|');
const rows = dense.r.split('|');
const retVal = XGrid.CreatePlain(cols.length, rows.length);
retVal.columns.forEach((c, i) => {
const labels = cols[i]
.split('.')
.map((l) => parseInt(l))
.filter((n) => !isNaN(n));
c.labels = labels.length > 0 ? labels : [];
});
retVal.rows.forEach((r, i) => {
const dataArray = rows[i].split('.');
const labels = dataArray.map((l) => parseInt(l)).filter((n) => !isNaN(n));
r.labels = labels.length > 0 ? labels : [];
if (labels.length === 0 || dataArray.length === labels.length + 1) {
r.cells = dataArray
.pop()
.split(/(?=[mfe]\d*)/)
.reduce((acc, cur) => {
const numero = cur[0] === 'm' ? 2 : cur[0] === 'f' ? 1 : 0;
const freq = cur ? parseInt(cur.substring(1)) || 1 : cols.length;
acc = acc.concat(Utils.FillArray(freq, () => numero));
return acc;
}, []);
}
});
return retVal;
}
static FromImage(img) {
const retVal = XGrid.CreatePlain(img.width, img.height);
for (let x = 3; x < img.data.length; x += 4) {
const rowNum = Math.floor((x - 3) / 4 / img.width);
const colNum = ((x - 3) / 4) % img.width;
const isBlock = img.data[x - 3] === 0 && img.data[x - 2] === 0 && img.data[x - 1] === 0;
if (isBlock)
retVal.rows[rowNum].cells[colNum] = 1;
}
return retVal;
}
/** Scrapes column labels from cell state */
static ScrapeColumnLabels(plain) {
plain.columns.forEach((col, c) => {
col.labels = [];
let run = 0;
for (let r = 0; r < plain.rows.length; r++) {
const isBlock = plain.rows[r].cells[c] === 1;
if (isBlock)
run++;
if (run > 0 && (!isBlock || r === plain.rows.length - 1)) {
col.labels.push(run);
run = 0;
}
}
});
}
}
var markupUrl$3 = "data:text/html;base64,PGRpdiBjbGFzcz0iYm9keSI+DQogIDxoMSBpZD0idGl0bGUiPjwvaDE+DQogIDxkaXYgaWQ9InpvbmUiPjwvZGl2Pg0KICA8ZGl2IGlkPSJlcnJvcnMiPjwvZGl2Pg0KICA8ZGl2IGlkPSJidXR0b25zIj4NCiAgICA8aW5wdXQgdHlwZT0iYnV0dG9uIiBpZD0iYnRuQ2FuY2VsIiB2YWx1ZT0iQ2FuY2VsIiAvPg0KICAgIDxpbnB1dCB0eXBlPSJidXR0b24iIGlkPSJidG5TYXZlIiB2YWx1ZT0iU2F2ZSIgLz4NCiAgPC9kaXY+DQo8L2Rpdj4NCg==";
var stylesUrl$3 = "data:text/css;base64,LmJvZHkgew0KICBwYWRkaW5nOiAwLjVlbTsNCiAgYm94LXNpemluZzogYm9yZGVyLWJveDsNCiAgY29sb3I6ICM1NTU7DQogIGZvbnQtZmFtaWx5OiAnQ291cmllciBOZXcnLCBtb25vc3BhY2U7DQogIHBvaW50ZXItZXZlbnRzOiBub25lOw0KICBoZWlnaHQ6IDEwMCU7DQogIGRpc3BsYXk6IGZsZXg7DQogIGZsZXgtZGlyZWN0aW9uOiBjb2x1bW47DQp9DQoNCmlucHV0IHsNCiAgZm9udDogaW5oZXJpdDsNCiAgcG9pbnRlci1ldmVudHM6IGFsbDsNCn0NCg0KI3RpdGxlIHsNCiAgZm9udC1zaXplOiAxLjNlbTsNCiAgbWFyZ2luOiAwLjI1ZW0gMDsNCn0NCiNlcnJvcnMgew0KICBjb2xvcjogI2YwMDsNCiAgZm9udC1zaXplOiAwLjhlbTsNCiAgZmxleC1ncm93OiAxOw0KfQ0KI2Vycm9ycyB1bCB7DQogIHBhZGRpbmctbGVmdDogMmVtOw0KfQ0KI2J1dHRvbnMgew0KICB0ZXh0LWFsaWduOiByaWdodDsNCiAgbWFyZ2luOiAwLjVlbTsNCn0NCg0KI3pvbmUgew0KICBwb2ludGVyLWV2ZW50czogYWxsOw0KICBvdmVyZmxvdy15OiBhdXRvOw0KfQ0KI3pvbmUgaW5wdXQuZXJyIHsNCiAgYm9yZGVyLWNvbG9yOiAjZjAwOw0KfQ0KI3pvbmUgaW5wdXQgew0KICBib3JkZXI6IDJweCBzb2xpZCAjZWVlOw0KICBib3JkZXItcmFkaXVzOiAycHg7DQogIHdpZHRoOiAyLjVlbTsNCiAgcGFkZGluZzogMC4yZW07DQogIHRleHQtYWxpZ246IHJpZ2h0Ow0KICBtYXJnaW46IDAuMmVtOw0KfQ0KDQojYnRuU2F2ZSB7DQogIGNvbG9yOiAjZmZmOw0KICBiYWNrZ3JvdW5kOiAjMGQwOw0KfQ0KI2J0blNhdmU6ZGlzYWJsZWQgew0KICBvcGFjaXR5OiAwLjM7DQp9DQoNCmlucHV0W3R5cGU9J2J1dHRvbiddOm5vdCg6ZGlzYWJsZWQpIHsNCiAgY3Vyc29yOiBwb2ludGVyOw0KfQ0KaW5wdXRbdHlwZT0nYnV0dG9uJ10gew0KICBib3JkZXI6IDFweCBzb2xpZCAjYWFhOw0KfQ0K";
class GriddlerPopupBase extends popup.Popup {
constructor(zoneMarkupUrl, moreStylesUrl, move = true, resize = true) {
super();
if (move)
dom.q(this).attr('move', '');
if (resize)
dom.q(this).attr('resize', '');
dom.q(this).on('open', () => this.onOpen());
const styleTagParams = [{ tag: 'style', text: custElems.reduceCss(custElems.decode(stylesUrl$3)) }];
if (moreStylesUrl)
styleTagParams.push({ tag: 'style', text: custElems.reduceCss(custElems.decode(moreStylesUrl)) });
this.$body = dom.q(this.root)
.find('.fore')
.append(custElems.reduceHtml(custElems.decode(markupUrl$3)))
.append(...styleTagParams)
.find('.body');
// Clicking cancel bypasses dismiss() handling by closing regardless
this.$body.first('#btnCancel').on('click', () => this.close());
this.$body.first('#btnSave').on('click', () => this.confirm());
this.$zone = this.$body.first('#zone');
if (zoneMarkupUrl) {
this.$zone.append(custElems.reduceHtml(custElems.decode(zoneMarkupUrl)));
}
this.confirmCallback = () => this.validate();
this.dismissCallback = () => !this.dirty || window.confirm('Abandon changes?');
}
set titleText(value) {
this.$body.first('#title').elements[0].textContent = value;
}
set errors(value) {
const $err = this.$body.first('#errors').empty();
if (value.length > 0) {
const $list = $err.appendIn({ tag: 'ul' });
value.forEach((it) => $list.append({ tag: 'li', text: it }));
}
}
onOpen() {
this.dirty = false;
this.renderZone();
this.validate();
}
}
class SettingsPopup extends GriddlerPopupBase {
constructor() {
super();
this.titleText = 'Settings';
// ...
}
renderZone() {
// ...
}
validate() {
// ...
return true;
}
}
var markupUrl$2 = "data:text/html;base64,PGRpdiBjbGFzcz0idGFibGUiPjwvZGl2Pg0K";
var stylesUrl$2 = "data:text/css;base64,LnRhYmxlIC5yb3cgew0KICBkaXNwbGF5OiBmbGV4Ow0KfQ0KLnRhYmxlIC5yb3cuc2VsZWN0ZWQgew0KICBiYWNrZ3JvdW5kLWNvbG9yOiAjODhlZWNjOw0KfQ0K";
class HistoryPopup extends GriddlerPopupBase {
constructor() {
super(markupUrl$2, stylesUrl$2);
this.titleText = 'Change History';
}
renderZone() {
const $table = this.$zone
.first('.table')
.empty()
.append('<div class="header row"><p>Type</p><p>Date</p></div>');
this.historyItems.forEach((item, i) => {
$table
.appendIn(`<div><p>${item.type}</p><p>${item.date}</p></div>`)
.attr('class', i === this.historyIndex ? 'selected row' : 'row');
});
}
validate() {
// ...
return true;
}
}
class EditLabelPopup extends GriddlerPopupBase {
constructor() {
super();
}
get grace() {
return this.capacity / 5;
}
renderZone() {
const typeName = this.setType === 'columns' ? 'Column' : 'Row';
this.titleText = `${typeName} ${this.setIndex + 1}`;
this.$zone.empty();
const minBoxes = Math.max(this.labels.length + 1, this.grace);
for (let i = 0; i < minBoxes; i++) {
this.addLabel(this.labels[i]);
}
}
validate() {
const ranged = this.$zone.find('input').elements.reduce((acc, cur) => {
const val = parseInt(cur.value);
cur.className = '';
if (val) {
if (acc.tot)
acc.tot++;
acc.tot += val;
acc.res.push(val);
cur.className = acc.tot > this.capacity ? 'err' : '';
}
return acc;
}, { tot: 0, res: [], err: [] });
const diff = ranged.tot - this.capacity;
if (diff > 0) {
ranged.err.unshift(`${ranged.tot} is ${diff} too many! (max: ${this.capacity})`);
}
this.labels = ranged.res;
this.errors = ranged.err;
const valid = ranged.err.length === 0;
const btnSave = this.$body.first('#btnSave').elements[0];
btnSave.disabled = !valid;
return valid;
}
addLabel(value) {
this.$zone.append({
tag: 'input',
evts: { input: () => this.onLabelInput() },
attr: {
type: 'number',
value: value ? `${value}` : '',
min: '0',
max: `${this.capacity}`,
},
});
}
onLabelInput() {
this.dirty = true;
this.validate();
}
}
var markupUrl$1 = "data:text/html;base64,PG5lMTQtcHhsOHIgZmlsdGVyPSJidyIgcmVzb2x1dGlvbj0iMzUiPjwvbmUxNC1weGw4cj4NCg==";
var stylesUrl$1 = "data:text/css;base64,LmJhY2sub3BlbiAuZm9yZSB7DQogIG1pbi13aWR0aDogY2FsYyg1MHZ3KTsNCiAgbWF4LXdpZHRoOiBjYWxjKDEwMHZ3IC0gM2VtKTsNCiAgbWF4LWhlaWdodDogY2FsYygxMDB2aCAtIDNlbSk7DQp9DQo=";
class PixelsPopup extends GriddlerPopupBase {
get labelGrid() {
return this._labelGrid;
}
constructor() {
super(markupUrl$1, stylesUrl$1);
this.validate = () => this.visualTest();
this.titleText = 'Pixels';
this.pxl8r = this.$zone
.first('ne14-pxl8r')
.on('render', Utils.Debounce((e) => this.onControlRender(e), 100))
.get(0);
}
renderZone() {
// ...
}
onControlRender(event) {
this.renderData = event.detail;
this._labelGrid = XGrid.AsPlain(this.renderData);
XGrid.ScrapeLabels(this._labelGrid);
XGrid.WipeCells(this._labelGrid);
this.visualTest();
}
visualTest() {
let retVal = false;
if (this._labelGrid != null) {
const result = Grid.load(this._labelGrid).solve();
retVal = result.solved;
XGrid.OverlayResult(this.renderData, result);
this.pxl8r.overlay(this.renderData);
}
return retVal;
}
}
var resolution = 2;
var gridSize = {
min: 5,
max: 1000,
"default": 5,
step: 5
};
var cellSize = {
min: 5,
max: 50,
"default": 20,
step: 1
};
var palette = {
major: "#bbbbbbff",
minor: "#eeeeeeff",
label: "#000000ff",
cells: "#000000ff"
};
var hilite = {
"default": "#0000ff33",
filling: "#00ff0033",
marking: "#ffff0033"
};
var markupUrl = "data:text/html;base64,PGRpdiBjbGFzcz0iZ3JpZC16b25lIGRyb3Atem9uZSI+DQogIDxjYW52YXMgaWQ9ImdyaWQiPjwvY2FudmFzPg0KICA8Y2FudmFzIGlkPSJoaWxpdGUiPjwvY2FudmFzPg0KICA8bmUxNC1tZW51IGNsYXNzPSJkYXJrIj4NCiAgICA8bGkgaWQ9ImNlbGwiPg0KICAgICAgPHA+Q2VsbDwvcD4NCiAgICAgIDx1bD48L3VsPg0KICAgIDwvbGk+DQogICAgPGxpIGlkPSJjb2x1bW5zIiBjbGFzcz0ic2V0Ij4NCiAgICAgIDxwPkNvbHVtbjwvcD4NCiAgICAgIDx1bD4NCiAgICAgICAgPGxpIGNsYXNzPSJtYXJrLW91dCI+TWFyayBvdXQ8L2xpPg0KICAgICAgICA8bGkgY2xhc3M9ImZpbGwtb3V0Ij5GaWxsIG91dDwvbGk+DQogICAgICAgIDxsaSBjbGFzcz0ic29sdmUiPlNvbHZlPC9saT4NCiAgICAgICAgPGxpIGNsYXNzPSJjbGVhci1zZXQiPkNsZWFyPC9saT4NCiAgICAgICAgPGxpIGNsYXNzPSJzcGxpdCI+PC9saT4NCiAgICAgICAgPGxpIGNsYXNzPSJsYWJlbHMiPg0KICAgICAgICAgIDxwPkxhYmVsczwvcD4NCiAgICAgICAgICA8dWw+DQogICAgICAgICAgICA8bGkgY2xhc3M9InNwYXduIj5TcGF3bjwvbGk+DQogICAgICAgICAgICA8bGkgY2xhc3M9ImVkaXQiPkVkaXQuLi48L2xpPg0KICAgICAgICAgICAgPGxpIGNsYXNzPSJjbGVhciI+Q2xlYXI8L2xpPg0KICAgICAgICAgIDwvdWw+DQogICAgICAgIDwvbGk+DQogICAgICA8L3VsPg0KICAgIDwvbGk+DQogICAgPGxpIGlkPSJyb3dzIiBjbGFzcz0ic2V0Ij4NCiAgICAgIDxwPlJvdzwvcD4NCiAgICAgIDx1bD4NCiAgICAgICAgPGxpIGNsYXNzPSJtYXJrLW91dCI+TWFyayBvdXQ8L2xpPg0KICAgICAgICA8bGkgY2xhc3M9ImZpbGwtb3V0Ij5GaWxsIG91dDwvbGk+DQogICAgICAgIDxsaSBjbGFzcz0ic29sdmUiPlNvbHZlPC9saT4NCiAgICAgICAgPGxpIGNsYXNzPSJjbGVhci1zZXQiPkNsZWFyPC9saT4NCiAgICAgICAgPGxpIGNsYXNzPSJzcGxpdCI+PC9saT4NCiAgICAgICAgPGxpIGNsYXNzPSJsYWJlbHMiPg0KICAgICAgICAgIDxwPkxhYmVsczwvcD4NCiAgICAgICAgICA8dWw+DQogICAgICAgICAgICA8bGkgY2xhc3M9InNwYXduIj5TcGF3bjwvbGk+DQogICAgICAgICAgICA8bGkgY2xhc3M9ImVkaXQiPkVkaXQuLi48L2xpPg0KICAgICAgICAgICAgPGxpIGNsYXNzPSJjbGVhciI+Q2xlYXI8L2xpPg0KICAgICAgICAgIDwvdWw+DQogICAgICAgIDwvbGk+DQogICAgICA8L3VsPg0KICAgIDwvbGk+DQogICAgPGxpIGlkPSJncmlkIj4NCiAgICAgIDxwPkdyaWQ8L3A+DQogICAgICA8dWw+DQogICAgICAgIDxsaSBjbGFzcz0iaGludCI+SGludDwvbGk+DQogICAgICAgIDxsaSBjbGFzcz0ic29sdmUiPlNvbHZlPC9saT4NCiAgICAgICAgPGxpIGNsYXNzPSJjbGVhci1ncmlkIj5DbGVhcjwvbGk+DQogICAgICAgIDxsaSBjbGFzcz0ic3BsaXQiPjwvbGk+DQogICAgICAgIDxsaSBjbGFzcz0ibGFiZWxzIj4NCiAgICAgICAgICA8cD5MYWJlbHM8L3A+DQogICAgICAgICAgPHVsPg0KICAgICAgICAgICAgPGxpIGNsYXNzPSJzcGF3biI+U3Bhd248L2xpPg0KICAgICAgICAgICAgPGxpIGNsYXNzPSJjbGVhciI+Q2xlYXI8L2xpPg0KICAgICAgICAgIDwvdWw+DQogICAgICAgIDwvbGk+DQogICAgICA8L3VsPg0KICAgIDwvbGk+DQogICAgPGxpIGNsYXNzPSJzcGxpdCI+PC9saT4NCiAgICA8bGkgaWQ9ImltcG9ydCI+DQogICAgICA8cD5JbXBvcnQ8L3A+DQogICAgICA8dWw+DQogICAgICAgIDxsaSBjbGFzcz0ianNvbiI+DQogICAgICAgICAgPHA+RmlsZS4uLjwvcD4NCiAgICAgICAgICA8aW5wdXQgdHlwZT0iZmlsZSIgLz4NCiAgICAgICAgPC9saT4NCiAgICAgICAgPGxpIGNsYXNzPSJwaXhlbCI+SW1hZ2UgcGl4ZWxhdG9yLi4uPC9saT4NCiAgICAgIDwvdWw+DQogICAgPC9saT4NCiAgICA8bGkgaWQ9ImV4cG9ydCI+DQogICAgICA8cD5FeHBvcnQ8L3A+DQogICAgICA8dWw+DQogICAgICAgIDxsaSBjbGFzcz0ianNvbiI+QXMgZmlsZTwvbGk+DQogICAgICAgIDxsaSBjbGFzcz0iaW1hZ2UiPkFzIGltYWdlPC9saT4NCiAgICAgICAgPGxpIGFyaWEta2V5c2hvcnRjdXRzPSJDdHJsK1AiIGNsYXNzPSJwcmludCI+UHJpbnQ8L2xpPg0KICAgICAgPC91bD4NCiAgICA8L2xpPg0KICAgIDxsaSBpZD0ic2V0dGluZ3MiPlNldHRpbmdzLi4uPC9saT4NCiAgICA8bGkgY2xhc3M9InNwbGl0Ij48L2xpPg0KICAgIDxsaSBpZD0iY2hhbmdlcyI+DQogICAgICA8cD5BY3Rpb25zPC9wPg0KICAgICAgPHVsPg0KICAgICAgICA8bGkgYXJpYS1rZXlzaG9ydGN1dHM9IkN0cmwrWiIgY2xhc3M9InVuZG8iPlVuZG88L2xpPg0KICAgICAgICA8bGkgYXJpYS1rZXlzaG9ydGN1dHM9IkN0cmwrWSIgY2xhc3M9InJlZG8iPlJlZG88L2xpPg0KICAgICAgICA8bGkgY2xhc3M9Imhpc3RvcnkiPlZpZXcgbG9nLi4uPC9saT4NCiAgICAgIDwvdWw+DQogICAgPC9saT4NCiAgPC9uZTE0LW1lbnU+DQo8L2Rpdj4NCg==";
var stylesUrl = "data:text/css;base64,LmdyaWQtem9uZSB7DQogIHBvc2l0aW9uOiByZWxhdGl2ZTsNCn0NCg0KY2FudmFzIHsNCiAgd2lkdGg6IDEwMCU7DQp9DQpjYW52YXMjaGlsaXRlIHsNCiAgcG9zaXRpb246IGFic29sdXRlOw0KICB0b3A6IDA7DQogIHJpZ2h0OiAwOw0KICBib3R0b206IDA7DQogIGxlZnQ6IDA7DQogIHBvaW50ZXItZXZlbnRzOiBub25lOw0KfQ0KDQpuZTE0LW1lbnUuZGFyayB7DQogIC0tYmc6ICMzMzM7DQogIC0tYm9yZGVyOiAxcHggc29saWQgIzg4ODsNCiAgLS1ib3gtc2hhZG93OiAycHggMnB4IDNweCAjMzMzOw0KICAtLWhvdmVyLWl0ZW0tYmc6ICM2NjY7DQogIC0tZmc6ICNlZWU7DQogIC0tZGlzYWJsZWQtZmc6ICM2NjY7DQp9DQoNCkBtZWRpYSBwcmludCB7DQogICNoaWxpdGUsDQogIG5lMTQtbWVudSB7DQogICAgZGlzcGxheTogbm9uZTsNCiAgfQ0KfQ0K";
class Griddler extends custElems.CustomElementBase {
get totalColumns() {
return this._grid.columns.length;
}
get totalRows() {
return this._grid.rows.length;
}
get totalWidth() {
return this.$grid.elements[0].width;
}
set totalWidth(value) {
this.$grid.prop('width', value);
this.$lite.prop('width', value);
}
get totalHeight() {
return this.$grid.elements[0].height;
}
set totalHeight(value) {
this.$grid.prop('height', value);
this.$lite.prop('height', value);
}
get isBlank() {
return !this._grid.rows.some((r) => r.cells && /[12]/.test(r.cells + ''));
}
get isFull() {
return this._grid.rows.every((r) => /^[12,]+$/.test(r.cells + ''));
}
get anyLabels() {
return (this._grid.rows.some((r) => { var _a; return (_a = r.labels) === null || _a === void 0 ? void 0 : _a.length; }) ||
this._grid.columns.some((c) => { var _a; return (_a = c.labels) === null || _a === void 0 ? void 0 : _a.length; }));
}
toString() {
return JSON.stringify(XGrid.ToDense(this._grid));
}
get textDataUrl() {
const encoded = window.encodeURIComponent(this.toString());
return `data:text/plain;charset=utf-8,${encoded}`;
}
get imageDataUrl() {
return this.$grid.get(0).toDataURL();
}
constructor() {
super(Griddler.Css, Griddler.Html);
this._settingsPopup = new SettingsPopup();
this._historyPopup = new HistoryPopup();
this._editLabelPopup = new EditLabelPopup();
this._pixelsPopup = new PixelsPopup();
this._size = cellSize.default * resolution;
this._grid = XGrid.AsPlain({ x: gridSize.default, y: gridSize.default });
this._history = [];
this._historyIndex = 0;
this._fontSize = this._size * 0.55;
this.$root = dom.q(this.root);
this.$menu = this.$root.first('ne14-menu');
this.handleMenuEvents();
this.$grid = this.$root.first('canvas#grid');
this._ctxGrid = this.$grid.get(0).getContext('2d');
this._ctxGrid.imageSmoothingEnabled = false;
this.$lite = this.$root.first('canvas#hilite');
this._ctxLite = this.$lite.get(0).getContext('2d');
this._ctxLite.imageSmoothingEnabled = false;
this.$grid.on('mouseleave', () => {
if (!this._downCoords)
this.clearContext(this._ctxLite);
});
this.$grid.on('mousemove', (e) => {
var _a, _b;
const moveCoords = this.getCoords(e);
this.highlight(!this._downCoords ? moveCoords : null);
// Check for dragging on initiating sets
const isColDrag = ((_a = this._downCoords) === null || _a === void 0 ? void 0 : _a.x) === moveCoords.x;
const isRowDrag = ((_b = this._downCoords) === null || _b === void 0 ? void 0 : _b.y) === moveCoords.y;
// If ripe for the paintin'
if ((isColDrag || isRowDrag) && moveCoords.state === 0) {
// Use initial state (or 0 -> 1 fill blanks)
this.setState(moveCoords, this._downCoords.state || 1);
}
});
this.$grid.on('mousedown', (e) => {
const coords = this.getCoords(e, true);
if (e.which === 3) {
this._menuCoords = coords;
this.updateMenuContext();
}
else {
this._downCoords = coords;
this.highlight();
}
});
this.$grid.on('mouseup', (e) => {
e.stopImmediatePropagation();
const upCoords = this.getCoords(e);
this.highlight(upCoords);
if (this._downCoords) {
if (upCoords.x === this._downCoords.x && upCoords.y === this._downCoords.y) {
if (upCoords.x != null && upCoords.y != null) {
// cell
let state;
switch (this._downCoords.which) {
case 'left':
state = ((this._downCoords.state + 1) % 3);
break;
case 'right':
state = this._downCoords.state === 2 ? 0 : 2;
break;
}
this.setState(this._downCoords, state);
}
else if (upCoords.x != null)
this.showLabelModal('columns', upCoords.x);
else if (upCoords.y != null)
this.showLabelModal('rows', upCoords.y);
}
if (this._downCoords.pending) {
this.addToHistory('paint', this._downCoords.snapshot);
}
this._downCoords = null;
}
});
dom.q(window).on('mouseup', () => {
this._downCoords = null;
this.clearContext(this._ctxLite);
});
this.$root.find('.drop-zone').on('dragover', (event) => event.preventDefault());
this.$root.find('.drop-zone').on('drop', (event) => {
event.preventDefault();
this.read(event.dataTransfer.files[0]);
});
this.$root
.appendIn(this._settingsPopup)
.on('confirmaccept', () => console.log('handle settings change!'));
this.$root
.appendIn(this._h