aboutsummaryrefslogtreecommitdiff
path: root/src_js
diff options
context:
space:
mode:
Diffstat (limited to 'src_js')
-rw-r--r--src_js/common.js227
-rw-r--r--src_js/dragger.js28
-rw-r--r--src_js/main.js (renamed from src_js/opcut/main.js)10
-rw-r--r--src_js/opcut/common.js139
-rw-r--r--src_js/opcut/fs.js71
-rw-r--r--src_js/opcut/grid.js285
-rw-r--r--src_js/opcut/states.js33
-rw-r--r--src_js/opcut/validators.js63
-rw-r--r--src_js/opcut/vt.js287
-rw-r--r--src_js/states.js31
-rw-r--r--src_js/vt.js469
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
+ ])
+ ];
+}