aboutsummaryrefslogtreecommitdiff
path: root/src_js/vt
diff options
context:
space:
mode:
authorbozo.kopic <bozo@kopic.xyz>2022-11-13 03:39:02 +0100
committerbozo.kopic <bozo@kopic.xyz>2022-11-13 03:39:02 +0100
commitb30a00a9713fd52865129132317beb6fa875017c (patch)
treec367859b2cfaf820c88eb4822e566d06144ecd4b /src_js/vt
parent26b2dee4ef3f0a00bd6fb2989ada48ad9b054972 (diff)
type script
Diffstat (limited to 'src_js/vt')
-rw-r--r--src_js/vt/index.ts52
-rw-r--r--src_js/vt/input.ts86
-rw-r--r--src_js/vt/params.ts282
-rw-r--r--src_js/vt/result.ts84
-rw-r--r--src_js/vt/svg.ts138
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)
+ ])
+ ];
+ })
+ ];
+}