diff options
Diffstat (limited to 'src_js/vt')
| -rw-r--r-- | src_js/vt/index.ts | 52 | ||||
| -rw-r--r-- | src_js/vt/input.ts | 86 | ||||
| -rw-r--r-- | src_js/vt/params.ts | 282 | ||||
| -rw-r--r-- | src_js/vt/result.ts | 84 | ||||
| -rw-r--r-- | src_js/vt/svg.ts | 138 |
5 files changed, 642 insertions, 0 deletions
diff --git a/src_js/vt/index.ts b/src_js/vt/index.ts new file mode 100644 index 0000000..74f571b --- /dev/null +++ b/src_js/vt/index.ts @@ -0,0 +1,52 @@ +import * as u from '@hat-open/util'; + +import * as dragger from '../dragger'; + +import * as params from './params'; +import * as result from './result'; +import * as svg from './svg'; + + +export function main(): u.VNode { + return ['div.main', + ['div.left-panel', params.main()], + leftPanelResizer(), + ['div.center-panel', svg.main() ?? []], + rightPanelResizer(), + ['div.right-panel', result.main()] + ]; +} + + +function leftPanelResizer(): u.VNode { + return ['div.panel-resizer', { + on: { + mousedown: dragger.mouseDownHandler(evt => { + const panel = (evt.target as HTMLElement).parentNode?.querySelector('.left-panel') as HTMLElement | null; + if (panel == null) + return () => {}; // eslint-disable-line + const width = panel.clientWidth; + return (_, dx) => { + panel.style.width = `${width + dx}px`; + }; + }) + } + }]; +} + + +function rightPanelResizer(): u.VNode { + return ['div.panel-resizer', { + on: { + mousedown: dragger.mouseDownHandler(evt => { + const panel = (evt.target as HTMLElement).parentNode?.querySelector('.right-panel') as HTMLElement | null; + if (panel == null) + return () => {}; // eslint-disable-line + const width = panel.clientWidth; + return (_, dx) => { + panel.style.width = `${width - dx}px`; + }; + }) + } + }]; +} diff --git a/src_js/vt/input.ts b/src_js/vt/input.ts new file mode 100644 index 0000000..749edbb --- /dev/null +++ b/src_js/vt/input.ts @@ -0,0 +1,86 @@ +import * as u from '@hat-open/util'; + + +export function text( + value: string, + validator: ((val: string) => boolean) | null, + onChange: (val: string) => void +): u.VNode { + return ['input', { + props: { + type: 'text', + value: value + }, + class: { + invalid: validator && !validator(value) + }, + on: { + change: (evt: Event) => onChange((evt.target as HTMLInputElement).value) + } + }]; +} + + +export function number( + value: number, + validator: ((val: number) => boolean) | null, + onChange: (val: number) => void +): u.VNode { + return ['input', { + props: { + type: 'number', + value: value + }, + class: { + invalid: validator && !validator(value) + }, + on: { + change: (evt: Event) => onChange((evt.target as HTMLInputElement).valueAsNumber) + } + }]; +} + + +export function checkbox( + label: string | null, + value: boolean, + onChange: (val: boolean) => void +): u.VNode { + const input: u.VNode = ['input', { + props: { + type: 'checkbox', + checked: value + }, + on: { + change: (evt: Event) => onChange((evt.target as HTMLInputElement).checked) + } + }]; + + if (!label) + return input; + + return ['label', + input, + ` ${label}` + ]; +} + + +export function select( + selected: string, + values: [string, string][], + onChange: (val: string) => void +): u.VNode { + return ['select', { + on: { + change: (evt: Event) => onChange((evt.target as HTMLSelectElement).value) + }}, + values.map(([value, label]) => ['option', { + props: { + selected: value == selected, + value: value + }}, + label + ]) + ]; +} diff --git a/src_js/vt/params.ts b/src_js/vt/params.ts new file mode 100644 index 0000000..3ac8848 --- /dev/null +++ b/src_js/vt/params.ts @@ -0,0 +1,282 @@ +import r from '@hat-open/renderer'; +import * as u from '@hat-open/util'; + +import * as common from '../common'; + +import * as input from './input'; + + +export function main(): u.VNodeChild[] { + return [ + ['div.header', + ['span.title', 'OPCUT'], + ['a.github', { + props: { + title: 'GitHub', + href: 'https://github.com/bozokopic/opcut' + }}, + ['span.fa.fa-github'] + ] + ], + ['div.form', + ['label', 'Method'], + input.select( + r.get('form', 'method') as string, + [['forward_greedy', 'Forward greedy'], + ['greedy', 'Greedy'], + ['forward_greedy_native', 'Forward greedy (native)'], + ['greedy_native', 'Greedy (native)']], + val => r.set(['form', 'method'], val) + ), + ['label', 'Cut width'], + input.number( + r.get('form', 'cut_width') as number, + u.isNumber, + val => r.set(['form', 'cut_width'], val) + ), + ['label'], + input.checkbox( + 'Minimize initial panel usage', + r.get('form', 'min_initial_usage') as boolean, + val => r.set(['form', 'min_initial_usage'], val) + ) + ], + ['div.content', + panels(), + items() + ], + ['button.calculate', { + props: { + disabled: r.get('calculating') + }, + on: { + click: common.calculate + }}, + 'Calculate' + ] + ]; +} + + +function panels(): u.VNode { + const panelsPath = ['form', 'panels']; + + const panelNames = new Set<string>(); + const nameValidator = (name: string) => { + const valid = !panelNames.has(name); + panelNames.add(name); + return valid; + }; + + 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) as common.FormPanel[]).map((panel, index) => ['tr', + ['td.col-name', + ['div', + input.text( + panel.name, + nameValidator, + val => r.set([panelsPath, index, 'name'], val) + ) + ] + ], + ['td.col-quantity', + ['div', + input.number( + panel.quantity, + u.isInteger, + val => r.set([panelsPath, index, 'quantity'], val) + ) + ] + ], + ['td.col-height', + ['div', + input.number( + panel.height, + u.isNumber, + val => r.set([panelsPath, index, 'height'], val) + ) + ] + ], + ['td.col-width', + ['div', + input.number( + panel.width, + u.isNumber, + 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 items(): u.VNode { + const itemsPath = ['form', 'items']; + + const itemNames = new Set<string>(); + const nameValidator = (name: string) => { + const valid = !itemNames.has(name); + itemNames.add(name); + return valid; + }; + + 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) as common.FormItem[]).map((item, index) => ['tr', + ['td.col-name', + ['div', + input.text( + item.name, + nameValidator, + val => r.set([itemsPath, index, 'name'], val) + ) + ] + ], + ['td.col-quantity', + ['div', + input.number( + item.quantity, + u.isInteger, + val => r.set([itemsPath, index, 'quantity'], val) + ) + ] + ], + ['td.col-height', + ['div', + input.number( + item.height, + u.isNumber, + val => r.set([itemsPath, index, 'height'], val) + ) + ] + ], + ['td.col-width', + ['div', + input.number( + item.width, + u.isNumber, + val => r.set([itemsPath, index, 'width'], val) + ) + ] + ], + ['td.col-rotate', + ['div', + input.checkbox( + null, + 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' + ] + ] + ] + ] + ] + ] + ]; +} diff --git a/src_js/vt/result.ts b/src_js/vt/result.ts new file mode 100644 index 0000000..fc23089 --- /dev/null +++ b/src_js/vt/result.ts @@ -0,0 +1,84 @@ +import r from '@hat-open/renderer'; +import * as u from '@hat-open/util'; + +import * as common from '../common'; + +import * as input from './input'; + + +export function main(): u.VNodeChild[] { + const result = r.get('result') as common.Result | null; + if (result == null) + return []; + return [ + ['div.form', + ['label', 'Export'], + ['div', + ['button', { + on: { + click: common.generate + }}, + ['span.fa.fa-file-pdf-o'], + ' PDF' + ] + ], + ['label', 'Font size'], + input.select( + r.get('svg', 'font_size') as string, + [['0.5', 'Small'], + ['1', 'Medium'], + ['1.5', 'Large']], + val => r.set(['svg', 'font_size'], val) + ), + ['label'], + input.checkbox( + 'Show names', + r.get('svg', 'show_names') as boolean, + val => r.set(['svg', 'show_names'], val) + ), + ['label'], + input.checkbox( + 'Show dimensions', + r.get('svg', 'show_dimensions') as boolean, + val => r.set(['svg', 'show_dimensions'], val) + ) + ], + Object.keys(result.params.panels).map(panelResult) + ]; +} + + +function panelResult(panel: string): u.VNode { + const isSelected = (item: string | null) => 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') as common.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) + ] + ]) + ]; +} diff --git a/src_js/vt/svg.ts b/src_js/vt/svg.ts new file mode 100644 index 0000000..1108e87 --- /dev/null +++ b/src_js/vt/svg.ts @@ -0,0 +1,138 @@ +import r from '@hat-open/renderer'; +import * as u from '@hat-open/util'; + +import * as common from '../common'; + + +export function main(): u.VNode | null { + const result = r.get('result') as common.Result | null; + const selected = r.get('selected') as { + panel: string | null, + item: string | null + }; + if (!result || !selected.panel) + return null; + + 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 * + u.strictParseFloat(r.get('svg', 'font_size') as string) + ); + const showNames = r.get('svg', 'show_names'); + const showDimensions = r.get('svg', 'show_dimensions'); + + return ['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); + const click = () => r.set(['selected', 'item'], used.item); + return [ + (!showNames ? [] : ['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: click + }}, + used.item + (used.rotate ? ' \u293E' : '') + ]), + (!showDimensions ? [] : ['text', { + props: { + style: 'cursor: pointer' + }, + attrs: { + x: String(used.x + width / 2), + y: String(used.y + height), + 'alignment-baseline': 'baseline', + 'text-anchor': 'middle', + 'font-size': fontSize + }, + on: { + click: click + }}, + String(width) + ]), + (!showDimensions ? [] : ['text', { + props: { + style: 'cursor: pointer' + }, + attrs: { + x: String(used.x + width), + y: String(used.y + height / 2), + 'transform': `rotate(-90, ${used.x + width}, ${used.y + height / 2})`, + 'alignment-baseline': 'baseline', + 'text-anchor': 'middle', + 'font-size': fontSize + }, + on: { + click: click + }}, + String(height) + ]) + ]; + }) + ]; +} |
