UNPKG

@ne1410s/griddler

Version:

Complete package for creating, sharing and solving griddler grids!

1,109 lines (1,085 loc) 70.2 kB
'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