diff options
| author | bozo.kopic <bozo@kopic.xyz> | 2021-11-07 15:52:44 +0100 |
|---|---|---|
| committer | bozo.kopic <bozo@kopic.xyz> | 2021-12-18 02:38:50 +0100 |
| commit | 0702d13263bf501c1db074ce1544e60b95161210 (patch) | |
| tree | ebca76946cead0ffcc742a64c15dd6f5e79958fa /src_js | |
| parent | 56a75fcb8f5a9e4c05ccec8eb4a3345a115da441 (diff) | |
major rewritev0.3.0
Diffstat (limited to 'src_js')
| -rw-r--r-- | src_js/common.js | 227 | ||||
| -rw-r--r-- | src_js/dragger.js | 28 | ||||
| -rw-r--r-- | src_js/main.js (renamed from src_js/opcut/main.js) | 10 | ||||
| -rw-r--r-- | src_js/opcut/common.js | 139 | ||||
| -rw-r--r-- | src_js/opcut/fs.js | 71 | ||||
| -rw-r--r-- | src_js/opcut/grid.js | 285 | ||||
| -rw-r--r-- | src_js/opcut/states.js | 33 | ||||
| -rw-r--r-- | src_js/opcut/validators.js | 63 | ||||
| -rw-r--r-- | src_js/opcut/vt.js | 287 | ||||
| -rw-r--r-- | src_js/states.js | 31 | ||||
| -rw-r--r-- | src_js/vt.js | 469 |
11 files changed, 760 insertions, 883 deletions
diff --git a/src_js/common.js b/src_js/common.js new file mode 100644 index 0000000..6e164f9 --- /dev/null +++ b/src_js/common.js @@ -0,0 +1,227 @@ +import * as URI from 'uri-js'; +import FileSaver from 'file-saver'; +import iziToast from 'izitoast'; +import Papa from 'papaparse'; + +import r from '@hat-open/renderer'; +import * as u from '@hat-open/util'; + +import * as states from './states'; + + +const calculateUrl = URI.resolve(window.location.href, './calculate'); +const generateOutputUrl = URI.resolve(window.location.href, './generate_output'); + +let panelCounter = 0; +let itemCounter = 0; + + +export async function calculate() { + try { + const method = r.get('form', 'method'); + const params = createCalculateParams(); + const res = await fetch(`${calculateUrl}?method=${method}`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(params) + }); + const result = await res.json(); + r.change(u.pipe( + u.set('result', result), + u.set('selected', states.main.selected) + )); + if (!result) + throw 'Could not resolve calculation'; + showNotification('success', 'New calculation available'); + } catch (e) { + showNotification('error', e); + } +} + + +export async function generateOutput() { + try { + const result = r.get('result'); + const res = await fetch(`${generateOutputUrl}?output_type=pdf`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(result) + }); + const blob = await res.blob(); + FileSaver.saveAs(blob, 'output.pdf'); + } catch (e) { + showNotification('error', e); + } +} + + +export async function csvImportPanels() { + const panels = await csvImport({ + name: String, + quantity: u.strictParseInt, + width: u.strictParseFloat, + height: u.strictParseFloat + }); + r.change(['form', 'panels'], x => x.concat(panels)); +} + + +export function csvExportPanels() { + const panels = r.get('form', 'panels'); + const csvData = Papa.unparse(panels); + const blob = new Blob([csvData], {type: 'text/csv'}); + FileSaver.saveAs(blob, 'panels.csv'); +} + + +export async function csvImportItems() { + const items = await csvImport({ + name: String, + quantity: u.strictParseInt, + width: u.strictParseFloat, + height: u.strictParseFloat, + can_rotate: u.equals('true') + }); + r.change(['form', 'items'], x => x.concat(items)); +} + + +export function csvExportItems() { + const items = r.get('form', 'items'); + const csvData = Papa.unparse(items); + const blob = new Blob([csvData], {type: 'text/csv'}); + FileSaver.saveAs(blob, 'items.csv'); +} + + +export function addPanel() { + panelCounter += 1; + const panel = u.set('name', `Panel${panelCounter}`, states.panel); + r.change(['form', 'panels'], u.append(panel)); +} + + +export function addItem() { + itemCounter += 1; + const item = u.set('name', `Item${itemCounter}`, states.item); + r.change(['form', 'items'], u.append(item)); +} + + +function createCalculateParams() { + const cutWidth = u.strictParseFloat(r.get('form', 'cut_width')); + if (cutWidth < 0) + throw 'Invalid cut width'; + + const panels = {}; + for (const panel of r.get('form', 'panels')) { + if (!panel.name) + throw 'Invalid panel name'; + if (panel.quantity < 1) + throw 'Invalid quantity for panel ' + panel.name; + if (panel.width <= 0) + throw 'Invalid width for panel ' + panel.name; + if (panel.height <= 0) + throw 'Invalid height for panel ' + panel.name; + for (let i = 1; i <= panel.quantity; ++i) { + const name = panel.name + ' ' + String(i); + if (name in panels) + throw 'Duplicate panel name ' + name; + panels[name] = {width: panel.width, height: panel.height}; + } + } + if (u.equals(panels, {})) + throw 'No panels defined'; + + const items = {}; + for (const item of r.get('form', 'items')) { + if (!item.name) + throw 'Invalid item name'; + if (item.quantity < 1) + throw 'Invalid quantity for item ' + item.name; + if (item.width <= 0) + throw 'Invalid width for item ' + item.name; + if (item.height <= 0) + throw 'Invalid height for item ' + item.name; + for (let i = 1; i <= item.quantity; ++i) { + const name = item.name + ' ' + String(i); + if (name in items) { + throw 'Duplicate item name ' + name; + } + items[name] = { + width: item.width, + height: item.width, + can_rotate: item.can_rotate + }; + } + } + if (u.equals(items, {})) + throw 'No items defined'; + + return { + cut_width: cutWidth, + panels: panels, + items: items + }; +} + + +function showNotification(type, message) { + iziToast[type]({message: message}); +} + + +async function csvImport(header) { + const file = await loadFile('.csv'); + if (!file) + return []; + + const data = await new Promise(resolve => { + Papa.parse(file, { + header: true, + complete: result => resolve(result.data) + }); + }); + + const result = []; + for (const i of data) { + let element = {}; + for (const [k, v] of Object.entries(header)) { + if (!(k in i)) { + element = null; + break; + } + element[k] = v(i[k]); + } + if (element) + result.push(element); + } + return result; +} + + +async function loadFile(ext) { + const el = document.createElement('input'); + el.style = 'display: none'; + el.type = 'file'; + el.accept = ext; + document.body.appendChild(el); + + const promise = new Promise(resolve => { + const listener = evt => { + resolve(u.get(['files', 0], evt.target)); + }; + el.addEventListener('change', listener); + + // TODO blur not fired on close??? + el.addEventListener('blur', listener); + + el.click(); + }); + + try { + return await promise; + } finally { + document.body.removeChild(el); + } +} diff --git a/src_js/dragger.js b/src_js/dragger.js new file mode 100644 index 0000000..e591be6 --- /dev/null +++ b/src_js/dragger.js @@ -0,0 +1,28 @@ + + +const draggers = []; + + +export function mouseDownHandler(createMoveHandlerCb) { + return evt => { + draggers.push({ + initX: evt.screenX, + initY: evt.screenY, + moveHandler: createMoveHandlerCb(evt) + }); + }; +} + + +document.addEventListener('mousemove', evt => { + for (let dragger of draggers) { + const dx = evt.screenX - dragger.initX; + const dy = evt.screenY - dragger.initY; + dragger.moveHandler(evt, dx, dy); + } +}); + + +document.addEventListener('mouseup', evt => { + draggers.splice(0); +}); diff --git a/src_js/opcut/main.js b/src_js/main.js index 3cf91b0..140064d 100644 --- a/src_js/opcut/main.js +++ b/src_js/main.js @@ -1,10 +1,10 @@ -import * as u from '@hat-core/util'; -import r from '@hat-core/renderer'; +import * as u from '@hat-open/util'; +import r from '@hat-open/renderer'; -import * as states from 'opcut/states'; -import * as vt from 'opcut/vt'; +import * as states from './states'; +import * as vt from './vt'; -import 'main.scss'; +import '../src_scss/main.scss'; function main() { diff --git a/src_js/opcut/common.js b/src_js/opcut/common.js deleted file mode 100644 index 00c7950..0000000 --- a/src_js/opcut/common.js +++ /dev/null @@ -1,139 +0,0 @@ -import * as URI from 'uri-js'; -import iziToast from 'izitoast'; - -import r from '@hat-core/renderer'; -import * as u from '@hat-core/util'; - -import * as states from 'opcut/states'; -import * as fs from 'opcut/fs'; - - -const calculateUrl = URI.resolve(window.location.href, './calculate'); -const generateOutputUrl = URI.resolve(window.location.href, './generate_output'); - - -export function calculate() { - try { - let msg = createValidateRequest(); - send(calculateUrl, msg).then(parseCalculateResponse); - } catch (e) { - showNotification(e, 'error'); - } -} - - -export function generateOutput(output_type, panel) { - const msg = { - output_type: output_type, - result: r.get('result'), - panel: panel - }; - send(generateOutputUrl, msg).then(msg => parseGenerateOutputResponse(msg, output_type)); -} - - -function send(url, msg) { - return new Promise(resolve => { - const req = new XMLHttpRequest(); - req.onload = () => { resolve(JSON.parse(req.responseText)); }; - req.open('POST', url); - req.setRequestHeader('Content-Type', 'application/json'); - req.send(JSON.stringify(msg)); - }); -} - - -function createValidateRequest() { - const cutWidth = u.strictParseFloat(r.get('form', 'cut_width')); - if (!Number.isFinite(cutWidth) || cutWidth < 0) - throw 'Invalid cut width'; - - const panels = {}; - for (let panel of r.get('form', 'panels', 'items')) { - const quantity = u.strictParseInt(panel.quantity); - const width = u.strictParseFloat(panel.width); - const height = u.strictParseFloat(panel.height); - if (!panel.name) - throw 'Invalid panel name'; - if (!Number.isFinite(quantity) || quantity < 1) - throw 'Invalid quantity for panel ' + panel.name; - if (!Number.isFinite(width) || width <= 0) - throw 'Invalid width for panel ' + panel.name; - if (!Number.isFinite(height) || height <= 0) - throw 'Invalid height for panel ' + panel.name; - for (let i = 1; i <= quantity; ++i) { - const name = panel.name + ' ' + String(i); - if (name in panels) { - throw 'Duplicate panel name ' + name; - } - panels[name] = {width: width, height: height}; - } - } - if (u.equals(panels, {})) - throw 'No panels defined'; - - const items = {}; - for (let item of r.get('form', 'items', 'items')) { - const quantity = u.strictParseInt(item.quantity); - const width = u.strictParseFloat(item.width); - const height = u.strictParseFloat(item.height); - if (!item.name) - throw 'Invalid item name'; - if (!Number.isFinite(quantity) || quantity < 1) - throw 'Invalid quantity for item ' + item.name; - if (!Number.isFinite(width) || width <= 0) - throw 'Invalid width for item ' + item.name; - if (!Number.isFinite(height) || height <= 0) - throw 'Invalid height for item ' + item.name; - for (let i = 1; i <= quantity; ++i) { - const name = item.name + ' ' + String(i); - if (name in items) { - throw 'Duplicate item name ' + name; - } - items[name] = { - width: width, - height: height, - can_rotate: item.can_rotate - }; - } - } - if (u.equals(items, {})) - throw 'No items defined'; - - return { - method: r.get('form', 'method'), - params: { - cut_width: cutWidth, - panels: panels, - items: items - } - }; -} - - -function parseCalculateResponse(msg) { - r.change(u.pipe( - u.set('result', msg.result), - u.set('selected', states.main.selected) - )); - if (msg.result) { - showNotification('New calculation available', 'success'); - } else { - showNotification('Could not resolve calculation', 'error'); - } -} - - -function parseGenerateOutputResponse(msg, output_type) { - if (output_type == 'PDF' && msg.data) { - const fileName = 'output.pdf'; - fs.saveB64Data(msg.data, fileName); - } else { - showNotification('Error generating output', 'error'); - } -} - - -function showNotification(message, type) { - iziToast[type]({message: message}); -} diff --git a/src_js/opcut/fs.js b/src_js/opcut/fs.js deleted file mode 100644 index 6b3c579..0000000 --- a/src_js/opcut/fs.js +++ /dev/null @@ -1,71 +0,0 @@ -import h from 'hyperscript'; -import FileSaver from 'file-saver'; - -import * as u from '@hat-core/util'; - - -export function loadText(ext) { - const el = h('input', { - style: 'display: none', - type: 'file', - accept: ext}); - const promise = new Promise(resolve => { - el.addEventListener('change', evt => { - const file = u.get(['files', 0], evt.target); - if (!file) - return; - const fileReader = new FileReader(); - fileReader.onload = () => { - const data = fileReader.result; - resolve(data); - }; - fileReader.readAsText(file); - }); - el.click(); - }); - return promise; -} - - -export function saveText(text, fileName) { - const blob = stringToBlob(text); - FileSaver.saveAs(blob, fileName); -} - - -export function saveB64Data(b64Data, fileName) { - const blob = b64ToBlob(b64Data); - FileSaver.saveAs(blob, fileName); -} - - -function stringToBlob(strData, contentType) { - contentType = contentType || ''; - return new Blob([strData], {type: contentType}); -} - - -// http://stackoverflow.com/a/16245768 -function b64ToBlob(b64Data, contentType, sliceSize) { - contentType = contentType || ''; - sliceSize = sliceSize || 512; - - var byteCharacters = atob(b64Data); - var byteArrays = []; - - for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) { - var slice = byteCharacters.slice(offset, offset + sliceSize); - - var byteNumbers = new Array(slice.length); - for (var i = 0; i < slice.length; i++) { - byteNumbers[i] = slice.charCodeAt(i); - } - - var byteArray = new Uint8Array(byteNumbers); - - byteArrays.push(byteArray); - } - - var blob = new Blob(byteArrays, {type: contentType}); - return blob; -} diff --git a/src_js/opcut/grid.js b/src_js/opcut/grid.js deleted file mode 100644 index e882093..0000000 --- a/src_js/opcut/grid.js +++ /dev/null @@ -1,285 +0,0 @@ -import Papa from 'papaparse'; - -import r from '@hat-core/renderer'; -import * as u from '@hat-core/util'; - -import * as fs from 'opcut/fs'; - - -export const state = { - items: [], - selectedItem: null -}; - - -export function clearState(state) { - return u.set('selectedItem', null, state); -} - - -export function tbody(gridPath, columns, validators) { - const gridState = r.get(gridPath); - return ['tbody', gridState.items.map((row, rowIndex) => - ['tr', u.zip(columns || [], validators || []).map(([column, validator]) => { - let content = ''; - if (typeof column == 'function') { - content = column(row, rowIndex); - } else { - const selected = u.equals(gridState.selectedItem, [rowIndex, column]); - content = u.get(column, row); - if (typeof content == 'boolean') { - content = checkboxColumn(gridPath, column)(row, rowIndex); - } else if (selected) { - content = ['input.grid-input', { - props: { - type: 'text', - value: content - }, - on: { - change: evt => r.set([gridPath, 'items', rowIndex, column], - evt.target.value), - blur: _ => { - if (u.equals(gridState.selectedItem, [rowIndex, column])) - r.set([gridPath, 'selectedItem'], null); - }, - keyup: evt => { - switch (evt.key) { - case 'Enter': - evt.target.blur(); - break; - case 'Escape': - evt.target.value = u.get(column, row); - evt.target.blur(); - break; - } - } - }} - ]; - } - } - const title = (validator ? validator(u.get(column, row), row) : null); - return ['td' + (typeof column == 'function' ? '' : '.grid-col-' + column), { - class: { - invalid: title - }, - props: { - title: (title ? title : '') - }, - on: { - click: evt => { - if (u.equals(gridState.selectedItem, [rowIndex, column])) - return; - r.set([gridPath, 'selectedItem'], [rowIndex, column]); - r.render(); - if (evt.target.firstChild && evt.target.firstChild.focus) - evt.target.firstChild.focus(); - } - }}, - content]; - })] - )]; -} - - -export function tfoot(gridPath, colspan, newItem, csvColumns) { - const itemsPath = [gridPath, 'items']; - return ['tfoot', - ['tr', - ['td', { - props: { - colSpan: colspan - }}, - ['div', - ['button', { - on: { - click: () => r.change(itemsPath, u.append(newItem)) - }}, - ['span.fa.fa-plus'], - ' Add' - ], - ['span.spacer'], - (!csvColumns ? - [] : - [ - ['button', { - on: { - click: () => { - importCsv(csvColumns, newItem).then(items => { - r.change(itemsPath, state => state.concat(items)); - }); - } - }}, - ['span.fa.fa-download'], - ' CSV Import' - ], - ['button', { - on: { - click: () => exportCsv(r.get(itemsPath), csvColumns) - }}, - ['span.fa.fa-upload'], - ' CSV Export' - ] - ] - ) - ] - ] - ] - ]; -} - - -export function createStringCsvColumns(...columns) { - return u.pipe( - u.map(i => [i, { - toString: u.get(i), - toItem: u.set(i) - }]), - u.fromPairs - )(columns); -} - - -export function createBooleanCsvColumns(...columns) { - return u.pipe( - u.map(i => [i, { - toString: u.pipe(u.get(i), x => x ? 'true' : 'false'), - toItem: (x, acc) => u.set(i, x == 'true', acc) - }]), - u.fromPairs - )(columns); -} - - -export function deleteColumn(gridPath, showUpDown, onDeleteCb) { - const itemsPath = [gridPath, 'items']; - return (i, index) => [ - (!showUpDown ? [] : [ - ['button', { - on: { - click: () => { - const gridState = r.get(gridPath); - if (index < 1) - return; - r.change(itemsPath, u.pipe( - u.set(index, gridState.items[index-1]), - u.set(index-1, i) - )); - } - }}, - ['span.fa.fa-arrow-up'] - ], - ['button', { - on: { - click: () => { - const gridState = r.get(gridPath); - if (index > gridState.items.length - 2) - return; - r.change(itemsPath, u.pipe( - u.set(index, gridState.items[index+1]), - u.set(index+1, i) - )); - } - }}, - ['span.fa.fa-arrow-down'] - ] - ]), - ['button', { - on: { - click: () => { - const item = r.get(itemsPath, index); - r.change(itemsPath, u.omit(index)); - if (onDeleteCb) - onDeleteCb(item); - } - }}, - ['span.fa.fa-minus'] - ] - ]; -} - - -export function checkboxColumn(gridPath, column) { - return (i, index) => { - const columnPath = [gridPath, 'items', index, column]; - return ['div', { - props: { - style: 'text-align: center' - }}, - ['input', { - props: { - type: 'checkbox', - checked: r.get(columnPath) - }, - on: { - change: evt => r.set(columnPath, evt.target.checked) - }} - ] - ]; - }; -} - - -export function selectColumn(gridPath, column, values) { - return (i, index) => { - const columnPath = [gridPath, 'items', index, column]; - const selectedValue = r.get(columnPath); - const invalid = values.find( - i => u.equals(selectedValue, (u.isArray(i) ? i[0] : i))) === undefined; - const allValues = (invalid ? u.append(selectedValue, values) : values); - return ['select' + (invalid ? '.invalid' : ''), { - class: { - invalid: invalid - }, - props: { - title: (invalid ? 'invalid value' : ''), - style: 'width: 100%' - }, - on: { - change: evt => r.set(columnPath, evt.target.value) - }}, - allValues.map(i => { - const value = (u.isArray(i) ? i[0] : i); - const label = (u.isArray(i) ? i[1] : i); - return ['option', { - selected: selectedValue == value, - value: value}, - label - ]; - }) - ]; - }; -} - - -function importCsv(csvColumns, newItem) { - return new Promise(resolve => { - fs.loadText('csv').then(csvData => { - const result = Papa.parse(csvData, { - delimiter: ';', - skipEmptyLines: true, - header: true - }); - const items = []; - for (let i of result.data) { - if (!Object.keys(i).every(k => u.contains(k, Object.keys(csvColumns)))) - continue; - const item = u.reduce( - (acc, [name, column]) => column.toItem(i[name], acc), - newItem, - u.toPairs(csvColumns)); - items.push(item); - } - resolve(items); - }); - }); -} - - -function exportCsv(items, csvColumns) { - const csvData = Papa.unparse( - items.map(item => u.map(column => column.toString(item))(csvColumns)), { - delimiter: ';', - skipEmptyLines: true, - header: true}); - fs.saveText(csvData, 'data.csv'); -} diff --git a/src_js/opcut/states.js b/src_js/opcut/states.js deleted file mode 100644 index ffc8666..0000000 --- a/src_js/opcut/states.js +++ /dev/null @@ -1,33 +0,0 @@ -import * as grid from 'opcut/grid'; - - -export const main = { - form: { - method: 'FORWARD_GREEDY', - cut_width: '0.3', - panels: grid.state, - items: grid.state - }, - result: null, - selected: { - panel: null, - item: null - }, -}; - - -export const panelsItem = { - name: 'Panel', - quantity: '1', - width: '100', - height: '100' -}; - - -export const itemsItem = { - name: 'Item', - quantity: '1', - width: '10', - height: '10', - can_rotate: true -}; diff --git a/src_js/opcut/validators.js b/src_js/opcut/validators.js deleted file mode 100644 index e5a18c4..0000000 --- a/src_js/opcut/validators.js +++ /dev/null @@ -1,63 +0,0 @@ -import * as u from '@hat-core/util'; - - -export function notEmptyValidator(value) { - if (!value) - return 'invalid value'; -} - - -export function floatValidator(value) { - const floatValue = u.strictParseFloat(value); - if (!Number.isFinite(floatValue)) - return 'not valid number'; -} - - -export function integerValidator(value) { - const intValue = u.strictParseInt(value); - if (!Number.isFinite(intValue)) - return 'not valid number'; -} - - -export function tcpPortValidator(value) { - const intValue = u.strictParseInt(value); - if (!Number.isFinite(intValue) || intValue < 0 || intValue > 0xFFFF) - return 'not valid TCP port'; -} - - -export function createChainValidator(...validators) { - return value => { - for (let validator of validators) { - let result = validator(value); - if (result) - return result; - } - }; -} - - -export function createUniqueValidator() { - const values = new Set(); - return value => { - if (values.has(value)) - return 'duplicate value'; - values.add(value); - }; -} - - -export function dimensionValidator(value) { - const floatValue = u.strictParseFloat(value); - if (!Number.isFinite(floatValue) || floatValue <= 0) - return 'not valid dimension'; -} - - -export function quantityValidator(value) { - const intValue = u.strictParseInt(value); - if (!Number.isFinite(intValue) || intValue < 1) - return 'not valid quantity'; -} diff --git a/src_js/opcut/vt.js b/src_js/opcut/vt.js deleted file mode 100644 index d2f3b7b..0000000 --- a/src_js/opcut/vt.js +++ /dev/null @@ -1,287 +0,0 @@ -import r from '@hat-core/renderer'; -import * as u from '@hat-core/util'; - -import * as grid from 'opcut/grid'; -import * as common from 'opcut/common'; -import * as states from 'opcut/states'; -import * as validators from 'opcut/validators'; - - -export function main() { - return ['div.window', - leftPanel(), - centerPanel(), - rightPanel() - ]; -} - - -function leftPanel() { - const methodPath = ['form', 'method']; - const cutWidthPath = ['form', 'cut_width']; - const cutWidthTitle = (cutWidth => - (Number.isFinite(cutWidth) && cutWidth >= 0) ? '' : 'not valid cut width' - )(u.strictParseFloat(r.get(cutWidthPath))); - return ['div.left-panel', - ['div.header', - ['div.title', 'OPCUT'], - ['a', { - props: { - title: 'GitHub', - href: 'https://github.com/bozokopic/opcut' - }}, - ['span.fa.fa-github'] - ] - ], - ['div.group', - ['label', 'Method'], - ['select', - ['FORWARD_GREEDY', 'GREEDY'].map(method => - ['option', { - props: { - value: method, - selected: r.get(methodPath) == method - }, - on: { - change: evt => r.set(methodPath, evt.target.value) - }}, - method - ]) - ] - ], - ['div.group', - ['label', 'Cut width'], - ['input', { - class: { - invalid: cutWidthTitle - }, - props: { - value: r.get(cutWidthPath), - title: cutWidthTitle - }, - on: { - change: evt => r.set(cutWidthPath, evt.target.value) - }} - ] - ], - ['div.content', - leftPanelPanels(), - leftPanelItems() - ], - ['button.calculate', { - on: { - click: common.calculate - }}, - 'Calculate' - ] - ]; -} - - -function leftPanelPanels() { - const panelsPath = ['form', 'panels']; - const deleteColumn = grid.deleteColumn(panelsPath); - const nameValidator = validators.createChainValidator( - validators.notEmptyValidator, - validators.createUniqueValidator()); - const quantityValidator = validators.quantityValidator; - const widthValidator = validators.dimensionValidator; - const heightValidator = validators.dimensionValidator; - const csvColumns = grid.createStringCsvColumns('name', 'quantity', 'width', 'height'); - return ['div', - ['table.grid', - ['thead', - ['tr', - ['th', 'Panel name'], - ['th.fixed', 'Quantity'], - ['th.fixed', 'Height'], - ['th.fixed', 'Width'], - ['th.fixed'] - ] - ], - grid.tbody(panelsPath, - ['name', 'quantity', 'height', 'width', deleteColumn], - [nameValidator, quantityValidator, heightValidator, widthValidator]), - grid.tfoot(panelsPath, 5, states.panelsItem, csvColumns) - ] - ]; -} - - -function leftPanelItems() { - const itemsPath = ['form', 'items']; - const rotateColumn = grid.checkboxColumn(itemsPath, 'can_rotate'); - const deleteColumn = grid.deleteColumn(itemsPath); - const nameValidator = validators.createChainValidator( - validators.notEmptyValidator, - validators.createUniqueValidator()); - const quantityValidator = validators.quantityValidator; - const widthValidator = validators.dimensionValidator; - const heightValidator = validators.dimensionValidator; - const csvColumns = u.merge( - grid.createStringCsvColumns('name', 'quantity', 'width', 'height'), - grid.createBooleanCsvColumns('can_rotate')); - return ['div', - ['table.grid', - ['thead', - ['tr', - ['th', 'Item name'], - ['th.fixed', 'Quantity'], - ['th.fixed', 'Height'], - ['th.fixed', 'Width'], - ['th.fixed', 'Rotate'], - ['th.fixed'] - ] - ], - grid.tbody(itemsPath, - ['name', 'quantity', 'height', 'width', rotateColumn, deleteColumn], - [nameValidator, quantityValidator, heightValidator, widthValidator]), - grid.tfoot(itemsPath, 6, states.itemsItem, csvColumns) - ] - ]; -} - - -function rightPanel() { - const result = r.get('result'); - return ['div.right-panel', (!result ? - [] : - [ - ['div.toolbar', - ['button', { - on: { - click: () => common.generateOutput('PDF', null) - }}, - ['span.fa.fa-file-pdf-o'], - ' PDF' - ] - ], - Object.keys(result.params.panels).map(rightPanelPanel) - ]) - ]; -} - - -function rightPanelPanel(panel) { - const isSelected = item => u.equals(r.get('selected'), {panel: panel, item: item}); - return ['div.panel', - ['div.panel-name', { - class: { - selected: isSelected(null) - }, - on: { - click: () => r.set('selected', {panel: panel, item: null}) - }}, - panel - ], - u.filter(used => used.panel == panel)(r.get('result', 'used')).map(used => - ['div.item', { - class: { - selected: isSelected(used.item) - }, - on: { - click: () => r.set('selected', {panel: panel, item: used.item}) - }}, - ['div.item-name', used.item], - (used.rotate ? ['span.item-rotate.fa.fa-refresh'] : []), - ['div.item-x', - 'X:', - String(Math.round(used.x * 100) / 100) - ], - ['div.item-y', - 'Y:', - String(Math.round(used.y * 100) / 100) - ] - ]) - ]; -} - - -function centerPanel() { - const result = r.get('result'); - const selected = r.get('selected'); - if (!result || !selected.panel) - return ['div.center-panel']; - const panel = result.params.panels[selected.panel]; - const used = u.filter(i => i.panel == selected.panel, result.used); - const unused = u.filter(i => i.panel == selected.panel, result.unused); - const panelColor = 'rgb(100,100,100)'; - const itemColor = 'rgb(250,250,250)'; - const selectedItemColor = 'rgb(200,140,140)'; - const unusedColor = 'rgb(238,238,238)'; - const fontSize = String(Math.max(panel.height, panel.width) * 0.02); - return ['div.center-panel', - ['svg', { - attrs: { - width: '100%', - height: '100%', - viewBox: [0, 0, panel.width, panel.height].join(' '), - preserveAspectRatio: 'xMidYMid' - }}, - ['rect', { - attrs: { - x: '0', - y: '0', - width: String(panel.width), - height: String(panel.height), - 'stroke-width': '0', - fill: panelColor - }} - ], - used.map(used => { - const item = result.params.items[used.item]; - const width = (used.rotate ? item.height : item.width); - const height = (used.rotate ? item.width : item.height); - return ['rect', { - props: { - style: 'cursor: pointer' - }, - attrs: { - x: String(used.x), - y: String(used.y), - width: String(width), - height: String(height), - 'stroke-width': '0', - fill: (used.item == selected.item ? selectedItemColor : itemColor) - }, - on: { - click: () => r.set(['selected', 'item'], used.item) - }} - ]; - }), - unused.map(unused => { - return ['rect', { - attrs: { - x: String(unused.x), - y: String(unused.y), - width: String(unused.width), - height: String(unused.height), - 'stroke-width': '0', - fill: unusedColor - }} - ]; - }), - used.map(used => { - const item = result.params.items[used.item]; - const width = (used.rotate ? item.height : item.width); - const height = (used.rotate ? item.width : item.height); - return ['text', { - props: { - style: 'cursor: pointer' - }, - attrs: { - x: String(used.x + width / 2), - y: String(used.y + height / 2), - 'alignment-baseline': 'middle', - 'text-anchor': 'middle', - 'font-size': fontSize - }, - on: { - click: () => r.set(['selected', 'item'], used.item) - }}, - used.item + (used.rotate ? ' \u293E' : '') - ]; - }) - ] - ]; -} diff --git a/src_js/states.js b/src_js/states.js new file mode 100644 index 0000000..f55716f --- /dev/null +++ b/src_js/states.js @@ -0,0 +1,31 @@ + +export const main = { + form: { + method: 'forward_greedy', + cut_width: 0.3, + panels: [], + items: [] + }, + result: null, + selected: { + panel: null, + item: null + }, +}; + + +export const panel = { + name: 'Panel', + quantity: 1, + width: 100, + height: 100 +}; + + +export const item = { + name: 'Item', + quantity: 1, + width: 10, + height: 10, + can_rotate: true +}; diff --git a/src_js/vt.js b/src_js/vt.js new file mode 100644 index 0000000..1dc467e --- /dev/null +++ b/src_js/vt.js @@ -0,0 +1,469 @@ +import r from '@hat-open/renderer'; +import * as u from '@hat-open/util'; + +import * as common from './common'; +import * as dragger from './dragger'; +import * as states from './states'; + + +export function main() { + return ['div.main', + leftPanel(), + leftPanelResizer(), + centerPanel(), + rightPanelResizer(), + rightPanel() + ]; +} + + +function leftPanelResizer() { + return ['div.panel-resizer', { + on: { + mousedown: dragger.mouseDownHandler(evt => { + const panel = evt.target.parentNode.querySelector('.left-panel'); + const width = panel.clientWidth; + return (_, dx, __) => { + panel.style.width = `${width + dx}px`; + }; + }) + } + }]; +} + + +function rightPanelResizer() { + return ['div.panel-resizer', { + on: { + mousedown: dragger.mouseDownHandler(evt => { + const panel = evt.target.parentNode.querySelector('.right-panel'); + const width = panel.clientWidth; + return (_, dx, __) => { + panel.style.width = `${width - dx}px`; + }; + }) + } + }]; +} + + +function leftPanel() { + return ['div.left-panel', + ['div.header', + ['span.title', 'OPCUT'], + ['a.github', { + props: { + title: 'GitHub', + href: 'https://github.com/bozokopic/opcut' + }}, + ['span.fa.fa-github'] + ] + ], + ['div.group', + ['label', 'Method'], + selectInput(r.get('form', 'method'), + ['forward_greedy', 'greedy'], + val => r.set(['form', 'method'], val)) + ], + ['div.group', + ['label', 'Cut width'], + numberInput(r.get('form', 'cut_width'), + val => r.set(['form', 'cut_width'], val)) + ], + ['div.content', + leftPanelPanels(), + leftPanelItems() + ], + ['button.calculate', { + on: { + click: common.calculate + }}, + 'Calculate' + ] + ]; +} + + +function leftPanelPanels() { + const panelsPath = ['form', 'panels']; + + return ['div', + ['table', + ['thead', + ['tr', + ['th.col-name', 'Panel name'], + ['th.col-quantity', 'Quantity'], + ['th.col-height', 'Height'], + ['th.col-width', 'Width'], + ['th.col-delete'] + ] + ], + ['tbody', + r.get(panelsPath).map((panel, index) => ['tr', + ['td.col-name', + ['div', + textInput(panel.name, + val => r.set([panelsPath, index, 'name'], val)) + ] + ], + ['td.col-quantity', + ['div', + numberInput(panel.quantity, + val => r.set([panelsPath, index, 'quantity'], val)) + ] + ], + ['td.col-height', + ['div', + numberInput(panel.height, + val => r.set([panelsPath, index, 'height'], val)) + ] + ], + ['td.col-width', + ['div', + numberInput(panel.width, + val => r.set([panelsPath, index, 'width'], val)) + ] + ], + ['td.col-delete', + ['button', { + on: { + click: _ => r.change(panelsPath, u.omit(index)) + }}, + ['span.fa.fa-minus'] + ] + ] + ]) + ], + ['tfoot', + ['tr', + ['td', { + props: { + colSpan: 5 + }}, + ['div', + ['button', { + on: { + click: common.addPanel + }}, + ['span.fa.fa-plus'], + ' Add' + ], + ['span.spacer'], + ['button', { + on: { + click: common.csvImportPanels + }}, + ['span.fa.fa-download'], + ' CSV Import' + ], + ['button', { + on: { + click: common.csvExportPanels + }}, + ['span.fa.fa-upload'], + ' CSV Export' + ] + ] + ] + ] + ] + ] + ]; +} + + +function leftPanelItems() { + const itemsPath = ['form', 'items']; + + return ['div', + ['table', + ['thead', + ['tr', + ['th.col-name', 'Item name'], + ['th.col-quantity', 'Quantity'], + ['th.col-height', 'Height'], + ['th.col-width', 'Width'], + ['th.col-rotate', 'Rotate'], + ['th.col-delete'] + ] + ], + ['tbody', + r.get(itemsPath).map((item, index) => ['tr', + ['td.col-name', + ['div', + textInput(item.name, + val => r.set([itemsPath, index, 'name'], val)) + ] + ], + ['td.col-quantity', + ['div', + numberInput(item.quantity, + val => r.set([itemsPath, index, 'quantity'], val)) + ] + ], + ['td.col-height', + ['div', + numberInput(item.height, + val => r.set([itemsPath, index, 'height'], val)) + ] + ], + ['td.col-width', + ['div', + numberInput(item.width, + val => r.set([itemsPath, index, 'width'], val)) + ] + ], + ['td.col-rotate', + ['div', + checkboxInput(item.can_rotate, + val => r.set([itemsPath, index, 'can_rotate'], val)) + ] + ], + ['td.col-delete', + ['button', { + on: { + click: _ => r.change(itemsPath, u.omit(index)) + }}, + ['span.fa.fa-minus'] + ] + ] + ]) + ], + ['tfoot', + ['tr', + ['td', { + props: { + colSpan: 6 + }}, + ['div', + ['button', { + on: { + click: common.addItem + }}, + ['span.fa.fa-plus'], + ' Add' + ], + ['span.spacer'], + ['button', { + on: { + click: common.csvImportItems + }}, + ['span.fa.fa-download'], + ' CSV Import' + ], + ['button', { + on: { + click: common.csvExportItems + }}, + ['span.fa.fa-upload'], + ' CSV Export' + ] + ] + ] + ] + ] + ] + ]; +} + + +function rightPanel() { + const result = r.get('result'); + return ['div.right-panel', (!result ? + [] : + [ + ['div.toolbar', + ['button', { + on: { + click: common.generateOutput + }}, + ['span.fa.fa-file-pdf-o'], + ' PDF' + ] + ], + Object.keys(result.params.panels).map(rightPanelPanel) + ]) + ]; +} + + +function rightPanelPanel(panel) { + const isSelected = item => u.equals(r.get('selected'), {panel: panel, item: item}); + + return ['div.panel', + ['div.panel-name', { + class: { + selected: isSelected(null) + }, + on: { + click: () => r.set('selected', {panel: panel, item: null}) + }}, + panel + ], + u.filter(used => used.panel == panel)(r.get('result', 'used')).map(used => + ['div.item', { + class: { + selected: isSelected(used.item) + }, + on: { + click: () => r.set('selected', {panel: panel, item: used.item}) + }}, + ['div.item-name', used.item], + (used.rotate ? ['span.item-rotate.fa.fa-refresh'] : []), + ['div.item-x', + 'X:', + String(Math.round(used.x * 100) / 100) + ], + ['div.item-y', + 'Y:', + String(Math.round(used.y * 100) / 100) + ] + ]) + ]; +} + + +function centerPanel() { + const result = r.get('result'); + const selected = r.get('selected'); + if (!result || !selected.panel) + return ['div.center-panel']; + + const panel = result.params.panels[selected.panel]; + const used = u.filter(i => i.panel == selected.panel, result.used); + const unused = u.filter(i => i.panel == selected.panel, result.unused); + const panelColor = 'rgb(100,100,100)'; + const itemColor = 'rgb(250,250,250)'; + const selectedItemColor = 'rgb(200,140,140)'; + const unusedColor = 'rgb(238,238,238)'; + const fontSize = String(Math.max(panel.height, panel.width) * 0.02); + + return ['div.center-panel', + ['svg', { + attrs: { + width: '100%', + height: '100%', + viewBox: [0, 0, panel.width, panel.height].join(' '), + preserveAspectRatio: 'xMidYMid' + }}, + ['rect', { + attrs: { + x: '0', + y: '0', + width: String(panel.width), + height: String(panel.height), + 'stroke-width': '0', + fill: panelColor + }} + ], + used.map(used => { + const item = result.params.items[used.item]; + const width = (used.rotate ? item.height : item.width); + const height = (used.rotate ? item.width : item.height); + return ['rect', { + props: { + style: 'cursor: pointer' + }, + attrs: { + x: String(used.x), + y: String(used.y), + width: String(width), + height: String(height), + 'stroke-width': '0', + fill: (used.item == selected.item ? selectedItemColor : itemColor) + }, + on: { + click: () => r.set(['selected', 'item'], used.item) + }} + ]; + }), + unused.map(unused => { + return ['rect', { + attrs: { + x: String(unused.x), + y: String(unused.y), + width: String(unused.width), + height: String(unused.height), + 'stroke-width': '0', + fill: unusedColor + }} + ]; + }), + used.map(used => { + const item = result.params.items[used.item]; + const width = (used.rotate ? item.height : item.width); + const height = (used.rotate ? item.width : item.height); + return ['text', { + props: { + style: 'cursor: pointer' + }, + attrs: { + x: String(used.x + width / 2), + y: String(used.y + height / 2), + 'alignment-baseline': 'middle', + 'text-anchor': 'middle', + 'font-size': fontSize + }, + on: { + click: () => r.set(['selected', 'item'], used.item) + }}, + used.item + (used.rotate ? ' \u293E' : '') + ]; + }) + ] + ]; +} + + +function textInput(value, onChange) { + return ['input', { + props: { + type: 'text', + value: value + }, + on: { + change: evt => onChange(evt.target.value) + } + }]; +} + + +function numberInput(value, onChange) { + return ['input', { + props: { + type: 'number', + value: value + }, + on: { + change: evt => onChange(evt.target.valueAsNumber) + } + }]; +} + + +function checkboxInput(value, onChange) { + return ['input', { + props: { + type: 'checkbox', + checked: value + }, + on: { + change: evt => onChange(evt.target.checked) + } + }]; +} + + +function selectInput(selected, values, onChange) { + return ['select', { + on: { + change: evt => onChange(evt.target.value) + }}, + values.map(i => ['option', { + props: { + selected: i == selected + }}, + i + ]) + ]; +} |
