diff --git a/src/app_context.ts b/src/app_context.ts index 17c4c84..8a04fb9 100644 --- a/src/app_context.ts +++ b/src/app_context.ts @@ -11,7 +11,6 @@ import { ExporterFactory, TExporters } from './exporters/exporters'; import { MaterialMap, MaterialType, SolidMaterial, TexturedMaterial } from './mesh'; import { Renderer } from './renderer'; import { StatusHandler, StatusMessage } from './status'; -import { TextureFiltering } from './texture'; import { SolidMaterialUIElement, TextureMaterialUIElement } from './ui/elements/material'; import { OutputStyle } from './ui/elements/output'; import { UI } from './ui/layout'; @@ -185,7 +184,7 @@ export class AppContext { const payload: TToWorkerMessage = { action: 'Import', params: { - filepath: uiElements.input.getCachedValue(), + filepath: uiElements.input.getValue(), }, }; @@ -443,12 +442,12 @@ export class AppContext { const payload: TToWorkerMessage = { action: 'Voxelise', params: { - constraintAxis: uiElements.constraintAxis.getCachedValue(), - voxeliser: uiElements.voxeliser.getCachedValue(), - size: uiElements.size.getCachedValue(), - useMultisampleColouring: uiElements.multisampleColouring.getCachedValue(), - enableAmbientOcclusion: uiElements.ambientOcclusion.getCachedValue(), - voxelOverlapRule: uiElements.voxelOverlapRule.getCachedValue(), + constraintAxis: uiElements.constraintAxis.getValue(), + voxeliser: uiElements.voxeliser.getValue(), + size: uiElements.size.getValue(), + useMultisampleColouring: uiElements.multisampleColouring.getValue(), + enableAmbientOcclusion: uiElements.ambientOcclusion.getValue(), + voxelOverlapRule: uiElements.voxelOverlapRule.getValue(), }, }; @@ -471,8 +470,8 @@ export class AppContext { const payload: TToWorkerMessage = { action: 'RenderNextVoxelMeshChunk', params: { - enableAmbientOcclusion: uiElements.ambientOcclusion.getCachedValue(), - desiredHeight: uiElements.size.getCachedValue(), + enableAmbientOcclusion: uiElements.ambientOcclusion.getValue(), + desiredHeight: uiElements.size.getValue(), }, }; @@ -522,21 +521,21 @@ export class AppContext { this._ui.getActionOutput(EAction.Assign) .setTaskInProgress('action', '[Block Mesh]: Loading...'); - Renderer.Get.setLightingAvailable(uiElements.calculateLighting.getCachedValue()); + Renderer.Get.setLightingAvailable(uiElements.calculateLighting.getValue()); const payload: TToWorkerMessage = { action: 'Assign', params: { - textureAtlas: uiElements.textureAtlas.getCachedValue(), - blockPalette: uiElements.blockPalette.getCachedValue(), - dithering: uiElements.dithering.getCachedValue(), + textureAtlas: uiElements.textureAtlas.getValue(), + blockPalette: uiElements.blockPalette.getValue(), + dithering: uiElements.dithering.getValue(), colourSpace: ColourSpace.RGB, - fallable: uiElements.fallable.getCachedValue() as FallableBehaviour, - resolution: Math.pow(2, uiElements.colourAccuracy.getCachedValue()), - calculateLighting: uiElements.calculateLighting.getCachedValue(), - lightThreshold: uiElements.lightThreshold.getCachedValue(), - contextualAveraging: uiElements.contextualAveraging.getCachedValue(), - errorWeight: uiElements.errorWeight.getCachedValue() / 10, + fallable: uiElements.fallable.getValue() as FallableBehaviour, + resolution: Math.pow(2, uiElements.colourAccuracy.getValue()), + calculateLighting: uiElements.calculateLighting.getValue(), + lightThreshold: uiElements.lightThreshold.getValue(), + contextualAveraging: uiElements.contextualAveraging.getValue(), + errorWeight: uiElements.errorWeight.getValue() / 10, }, }; @@ -560,7 +559,7 @@ export class AppContext { const payload: TToWorkerMessage = { action: 'RenderNextBlockMeshChunk', params: { - textureAtlas: uiElements.textureAtlas.getCachedValue(), + textureAtlas: uiElements.textureAtlas.getValue(), }, }; @@ -605,7 +604,7 @@ export class AppContext { } private _export(): (TWorkerJob | undefined) { - const exporterID: TExporters = this._ui.layout.export.elements.export.getCachedValue(); + const exporterID: TExporters = this._ui.layout.export.elements.export.getValue(); const exporter: IExporter = ExporterFactory.GetExporter(exporterID); const filepath = remote.dialog.showSaveDialogSync({ diff --git a/src/ui/elements/base.ts b/src/ui/elements/base.ts deleted file mode 100644 index f24e22a..0000000 --- a/src/ui/elements/base.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { ASSERT } from '../../util/error_util'; - -export abstract class BaseUIElement { - protected _id: string; - protected _label: string; - protected _isEnabled: boolean; - protected _value?: Type; - protected _cachedValue?: any; - - constructor(label: string) { - this._id = '_' + Math.random().toString(16); - this._label = label; - this._isEnabled = true; - } - - public setEnabled(isEnabled: boolean, isGroupEnable: boolean = true) { - if (isEnabled && isGroupEnable && !this._obeyGroupEnables) { - return; - } - this._isEnabled = isEnabled; - this._onEnabledChanged(); - } - - public getCachedValue(): Type { - ASSERT(this._cachedValue !== undefined, 'Attempting to access value before cached'); - return this._cachedValue as Type; - } - - protected getValue(): Type { - ASSERT(this._value !== undefined); - return this._value; - } - - public cacheValue() { - this._cachedValue = this.getValue(); - } - - public abstract generateHTML(): string; - public abstract registerEvents(): void; - - - protected abstract _onEnabledChanged(): void; - - private _obeyGroupEnables: boolean = true; - public setObeyGroupEnables(shouldListen: boolean) { - this._obeyGroupEnables = shouldListen; - return this; - } -} diff --git a/src/ui/elements/base_element.ts b/src/ui/elements/base_element.ts new file mode 100644 index 0000000..81fe972 --- /dev/null +++ b/src/ui/elements/base_element.ts @@ -0,0 +1,85 @@ +import { getRandomID } from '../../util'; +import { UIUtil } from '../../util/ui_util'; + +/** + * The base UI class from which user interactable DOM elements are built from. + * Each `BaseUIElement` can be enabled/disabled. + */ +export abstract class BaseUIElement { + private _id: string; + private _isEnabled: boolean; + private _obeyGroupEnables: boolean; + + public constructor() { + this._id = getRandomID(); + this._isEnabled = true; + this._obeyGroupEnables = true; + } + + /** + * Get whether or not this UI element is interactable. + */ + public getEnabled() { + return this._isEnabled; + } + + /** + * Set whether or not this UI element is interactable. + */ + public setEnabled(isEnabled: boolean, isGroupEnable: boolean = true) { + if (isGroupEnable && !this._obeyGroupEnables) { + return; + } + this._isEnabled = isEnabled; + this._onEnabledChanged(); + } + + /** + * Sets whether or not this element should be enabled when the group + * is it apart of becomes enabled. This is useful if an element should + * only be enabled if another element has a particular value. If this is + * false then there needs to be a some event added to manually enable this + * element. + */ + public setShouldObeyGroupEnables(obey: boolean) { + this._obeyGroupEnables = obey; + } + + /** + * The actual HTML that represents this UI element. It is recommended to + * give the outermost element that ID generated for this BaseUIElement so + * that `getElement()` returns all elements created here. + */ + public abstract generateHTML(): string; + + /** + * A delegate that is called after the UI element has been added to the DOM. + * Calls to `addEventListener` should be placed here. + */ + public abstract registerEvents(): void; + + public finalise(): void { + this._onEnabledChanged(); + } + + /** + * Returns the actual DOM element that this BaseUIElement refers to. + * Calling this before the element is created (i.e. before `generateHTML`) + * is called will throw an error. + */ + protected _getElement() { + return UIUtil.getElementById(this._id) as T; + } + + /** + * Each BaseUIElement is assignd an ID that can be used a DOM element with. + */ + protected _getId() { + return this._id; + } + + /** + * A delegate that is called when the enabled status is changed. + */ + protected abstract _onEnabledChanged(): void; +} diff --git a/src/ui/elements/button.ts b/src/ui/elements/button.ts index 707e3a9..ea6c4f5 100644 --- a/src/ui/elements/button.ts +++ b/src/ui/elements/button.ts @@ -1,59 +1,53 @@ -import { ASSERT } from '../../util/error_util'; -import { BaseUIElement } from './base'; +import { UIUtil } from '../../util/ui_util'; +import { BaseUIElement } from './base_element'; -export class ButtonElement extends BaseUIElement { +export class ButtonElement extends BaseUIElement { + private _label: string; private _onClick: () => void; - public constructor(label: string, onClick: () => void) { - super(label); - this._onClick = onClick; - this._isEnabled = true; + public constructor() { + super(); + this._label = 'Unknown'; + this._onClick = () => { }; } - public generateHTML() { - return ` -
-
${this._label}
-
-
- `; - } - - public registerEvents(): void { - const element = document.getElementById(this._id) as HTMLDivElement; - ASSERT(element !== null); - - element.addEventListener('click', () => { - if (this._isEnabled) { - this._onClick(); - } - }); - } - - protected _onEnabledChanged() { - const element = document.getElementById(this._id) as HTMLDivElement; - ASSERT(element !== null); - - if (this._isEnabled) { - element.classList.remove('button-disabled'); - } else { - element.classList.add('button-disabled'); - } - } - - public setLabelOverride(label: string) { - const element = document.getElementById(this._id) as HTMLDivElement; - ASSERT(element !== null, 'Updating label override of element that does not exist'); - - element.innerHTML = label; + /** + * Sets the delegate that is called when this button is clicked. + */ + public setOnClick(delegate: () => void) { + this._onClick = delegate; return this; } - public removeLabelOverride() { - const element = document.getElementById(this._id) as HTMLDivElement; - ASSERT(element !== null, 'Removing label override of element that does not exist'); + /** + * Sets the label of this button. + */ + public setLabel(label: string) { + this._label = label; + return this; + } - element.innerHTML = this._label; + /** + * Override the current label with a new value. + */ + public setLabelOverride(label: string) { + this._getElement().innerHTML = label; + return this; + } + + /** + * Remove the label override and set the label back to its default + */ + public removeLabelOverride() { + this._getElement().innerHTML = this._label; + return this; + } + + /** + * Start the loading animation + */ + public startLoading() { + this._getElement().classList.add('button-loading'); return this; } @@ -62,26 +56,48 @@ export class ButtonElement extends BaseUIElement { * @param progress A number between 0.0 and 1.0 inclusive. */ public setProgress(progress: number) { - const element = document.getElementById(this._id + '-progress') as HTMLDivElement; - ASSERT(element !== null); - - element.style.width = `${progress * 100}%`; - return this; - } - - public startLoading() { - const element = document.getElementById(this._id) as HTMLDivElement; - ASSERT(element !== null); - - element.classList.add('button-loading'); + const progressBarElement = UIUtil.getElementById(this._getProgressBarId()); + progressBarElement.style.width = `${progress * 100}%`; return this; } + /** + * Stop the loading animation + */ public stopLoading() { - const element = document.getElementById(this._id) as HTMLDivElement; - ASSERT(element !== null); - - element.classList.remove('button-loading'); + this._getElement().classList.remove('button-loading'); return this; } + + public override generateHTML() { + return ` +
+
${this._label}
+
+
+ `; + } + + public override registerEvents(): void { + this._getElement().addEventListener('click', () => { + if (this.getEnabled()) { + this._onClick?.(); + } + }); + } + + protected override _onEnabledChanged() { + if (this.getEnabled()) { + this._getElement().classList.remove('button-disabled'); + } else { + this._getElement().classList.add('button-disabled'); + } + } + + /** + * Gets the ID of the DOM element for the button's progress bar. + */ + private _getProgressBarId() { + return this._getId() + '-progress'; + } } diff --git a/src/ui/elements/checkbox.ts b/src/ui/elements/checkbox.ts index dfabdf9..d4cec2b 100644 --- a/src/ui/elements/checkbox.ts +++ b/src/ui/elements/checkbox.ts @@ -1,95 +1,86 @@ -import { getRandomID } from '../../util'; -import { ASSERT } from '../../util/error_util'; -import { LabelledElement } from './labelled_element'; +import { UIUtil } from '../../util/ui_util'; +import { ConfigUIElement } from './config_element'; -export class CheckboxElement extends LabelledElement { - private _checkboxId: string; - private _checkboxPipId: string; - private _checkboxTextId: string; - private _onText: string; - private _offText: string; +export class CheckboxElement extends ConfigUIElement { + private _labelChecked: string; + private _labelUnchecked: string; - public constructor(label: string, value: boolean, onText: string, offText: string) { - super(label); - this._checkboxId = getRandomID(); - this._checkboxPipId = getRandomID(); - this._checkboxTextId = getRandomID(); - this._value = value; - this._onText = onText; - this._offText = offText; + public constructor() { + super(false); + this._labelChecked = 'On'; + this._labelUnchecked = 'Off'; } - protected generateInnerHTML(): string { - return ` -
- - - - -
-
${this.getValue() ? this._onText : this._offText}
- `; + public setCheckedText(label: string) { + this._labelChecked = label; + return this; } - public registerEvents(): void { - const checkboxElement = document.getElementById(this._checkboxId); - const checkboxPipElement = document.getElementById(this._checkboxPipId); - ASSERT(checkboxElement !== null && checkboxPipElement !== null); + public setUncheckedText(label: string) { + this._labelUnchecked = label; + return this; + } - checkboxElement?.addEventListener('mouseenter', () => { - if (this._isEnabled) { + public override registerEvents(): void { + const checkboxElement = this._getElement(); + const checkboxPipElement = UIUtil.getElementById(this._getPipId()); + + checkboxElement.addEventListener('mouseenter', () => { + if (this.getEnabled()) { checkboxElement.classList.add('checkbox-hover'); checkboxPipElement.classList.add('checkbox-pip-hover'); } }); - checkboxElement?.addEventListener('mouseleave', () => { - if (this._isEnabled) { + checkboxElement.addEventListener('mouseleave', () => { + if (this.getEnabled()) { checkboxElement.classList.remove('checkbox-hover'); checkboxPipElement.classList.remove('checkbox-pip-hover'); } }); checkboxElement.addEventListener('click', () => { - if (this._isEnabled) { - this._value = !this._value; - this._onValueChanged(); + if (this.getEnabled()) { + this._setValue(!this.getValue()); } }); } - private _onValueChanged() { - const checkboxElement = document.getElementById(this._checkboxId); - const checkboxPipElement = document.getElementById(this._checkboxPipId); - ASSERT(checkboxElement !== null && checkboxPipElement !== null); - const checkboxTextElement = document.getElementById(this._checkboxTextId); - ASSERT(checkboxTextElement !== null); + protected override _generateInnerHTML(): string { + return ` +
+ + + + +
+
${this.getValue() ? this._labelChecked : this._labelUnchecked}
+ `; + } - checkboxTextElement.innerHTML = this.getValue() ? this._onText : this._offText; + protected override _onValueChanged(): void { + const checkboxElement = this._getElement(); + const checkboxPipElement = UIUtil.getElementById(this._getPipId()); + const checkboxTextElement = UIUtil.getElementById(this._getLabelId()); + + checkboxTextElement.innerHTML = this.getValue() ? this._labelChecked : this._labelUnchecked; checkboxPipElement.style.visibility = this.getValue() ? 'visible' : 'hidden'; - if (this._isEnabled) { + if (this.getEnabled()) { checkboxElement.classList.remove('checkbox-disabled'); } else { checkboxElement.classList.add('checkbox-disabled'); } - - this._onValueChangedDelegate?.(this._value!); } - protected _onEnabledChanged(): void { + protected override _onEnabledChanged(): void { super._onEnabledChanged(); - const checkboxElement = document.getElementById(this._checkboxId); - const checkboxPipElement = document.getElementById(this._checkboxPipId); - ASSERT(checkboxElement !== null && checkboxPipElement !== null); - const checkboxTextElement = document.getElementById(this._checkboxTextId); - ASSERT(checkboxTextElement !== null); + const checkboxElement = this._getElement(); + const checkboxPipElement = UIUtil.getElementById(this._getPipId()); + const checkboxTextElement = UIUtil.getElementById(this._getLabelId()); - checkboxTextElement.innerHTML = this.getValue() ? this._onText : this._offText; - checkboxPipElement.style.visibility = this.getValue() ? 'visible' : 'hidden'; - - if (this._isEnabled) { + if (this.getEnabled()) { checkboxElement.classList.remove('checkbox-disabled'); checkboxTextElement.classList.remove('checkbox-text-disabled'); checkboxPipElement.classList.remove('checkbox-pip-disabled'); @@ -98,13 +89,13 @@ export class CheckboxElement extends LabelledElement { checkboxTextElement.classList.add('checkbox-text-disabled'); checkboxPipElement.classList.add('checkbox-pip-disabled'); } - - this._onValueChangedDelegate?.(this._value!); } - private _onValueChangedDelegate?: (value: boolean) => void; - public onValueChanged(delegate: (value: boolean) => void) { - this._onValueChangedDelegate = delegate; - return this; + private _getPipId() { + return this._getId() + '-pip'; + } + + private _getLabelId() { + return this._getId() + '-label'; } } diff --git a/src/ui/elements/combobox.ts b/src/ui/elements/combobox.ts index c190715..c65e402 100644 --- a/src/ui/elements/combobox.ts +++ b/src/ui/elements/combobox.ts @@ -1,65 +1,61 @@ -import { EAppEvent, EventManager } from '../../event'; import { ASSERT } from '../../util/error_util'; -import { LabelledElement } from './labelled_element'; +import { ConfigUIElement } from './config_element'; export type ComboBoxItem = { - id: T; + payload: T; displayText: string; tooltip?: string; } -export class ComboBoxElement extends LabelledElement { +export class ComboBoxElement extends ConfigUIElement { private _items: ComboBoxItem[]; - public constructor(id: string, items: ComboBoxItem[]) { - super(id); - this._items = items; + public constructor() { + super(); + this._items = []; } - public generateInnerHTML() { + public addItems(items: ComboBoxItem[]) { + items.forEach((item) => { + this.addItem(item); + }); + return this; + } + + public addItem(item: ComboBoxItem) { + this._items.push(item); + this._setValue(this._items[0].payload); + return this; + } + + public override registerEvents(): void { + this._getElement().addEventListener('onchange', (e: Event) => { + const selectedValue = this._items[this._getElement().selectedIndex].payload; + this._setValue(selectedValue); + }); + } + + protected override _generateInnerHTML() { + ASSERT(this._items.length > 0); + let itemsHTML = ''; for (const item of this._items) { - itemsHTML += ``; + itemsHTML += ``; } return ` - ${itemsHTML} `; } - public registerEvents(): void { - const element = document.getElementById(this._id) as HTMLSelectElement; - ASSERT(element !== null); - - element.addEventListener('change', () => { - if (this._onValueChangedDelegate) { - //EventManager.Get.broadcast(EAppEvent.onComboBoxChanged, element.value); - this._onValueChangedDelegate(element.value); - } - }); - } - - protected getValue() { - const element = document.getElementById(this._id) as HTMLSelectElement; - ASSERT(element !== null); - return this._items[element.selectedIndex].id; - } - - protected _onEnabledChanged() { + protected override _onEnabledChanged() { super._onEnabledChanged(); - const element = document.getElementById(this._id) as HTMLSelectElement; - ASSERT(element !== null); - element.disabled = !this._isEnabled; - - this._onValueChangedDelegate?.(element.value); + this._getElement().disabled = !this.getEnabled(); } - private _onValueChangedDelegate?: (value: any) => void; - public onValueChanged(delegate: (value: any) => void) { - this._onValueChangedDelegate = delegate; - return this; + protected override _onValueChanged(): void { } } diff --git a/src/ui/elements/config_element.ts b/src/ui/elements/config_element.ts new file mode 100644 index 0000000..f8cf032 --- /dev/null +++ b/src/ui/elements/config_element.ts @@ -0,0 +1,114 @@ +import { ASSERT } from '../../util/error_util'; +import { BaseUIElement } from './base_element'; +import { LabelElement } from './label'; + +/** + * A `ConfigUIElement` is a UI element that has a value the user can change. + * For example, sliders, comboboxes and checkboxes are `ConfigUIElement`. + */ +export abstract class ConfigUIElement extends BaseUIElement { + private _label: string; + private _description?: string; + private _labelElement: LabelElement; + private _value?: T; + private _cachedValue?: T; + private _onValueChangedListeners: Array<(newValue: T) => void>; + + public constructor(defaultValue?: T) { + super(); + this._value = defaultValue; + this._label = 'unknown'; + this._labelElement = new LabelElement(this._label, this._description); + this._onValueChangedListeners = []; + } + + public setDefaultValue(value: T) { + this._value = value; + return this; + } + + public setLabel(label: string) { + this._label = label; + this._labelElement = new LabelElement(this._label, this._description); + return this; + } + + public setDescription(text: string) { + this._labelElement = new LabelElement(this._label, text); + return this; + } + + /** + * Caches the current value. + */ + public cacheValue() { + this._cachedValue = this._value; + } + + /** + * Returns whether the value stored is different from the cached value. + */ + public hasChanged() { + return this._cachedValue !== this._value; + } + + /** + * Get the currently set value of this UI element. + */ + public getValue(): T { + ASSERT(this._value !== undefined, 'this._value is undefined'); + return this._value; + } + + /** + * Add a delegate that will be called when the value changes. + */ + public addValueChangedListener(delegate: (newValue: T) => void) { + this._onValueChangedListeners.push(delegate); + return this; + } + + public override finalise(): void { + super.finalise(); + + this._onValueChanged(); + this._onValueChangedListeners.forEach((listener) => { + listener(this._value!); + }); + } + + public override generateHTML() { + return ` + ${this._labelElement.generateHTML()} +
+ ${this._generateInnerHTML()} +
+ `; + } + + /** + * The UI element that this label is describing. + */ + protected abstract _generateInnerHTML(): string; + + protected override _onEnabledChanged() { + this._labelElement.setEnabled(this.getEnabled()); + } + + /** + * Set the value of this UI element. + */ + protected _setValue(value: T) { + this._value = value; + + this._onValueChanged(); + this._onValueChangedListeners.forEach((listener) => { + listener(this._value!); + }); + } + + /** + * A delegate that is called when the value of this element changes. + */ + protected abstract _onValueChanged(): void; +} diff --git a/src/ui/elements/file_input.ts b/src/ui/elements/file_input.ts index 269ebd9..d3e5aec 100644 --- a/src/ui/elements/file_input.ts +++ b/src/ui/elements/file_input.ts @@ -1,43 +1,49 @@ import { remote } from 'electron'; import * as path from 'path'; -import { ASSERT } from '../../util/error_util'; -import { LabelledElement } from './labelled_element'; +import { ConfigUIElement } from './config_element'; -export class FileInputElement extends LabelledElement { - private _fileExtension: string; +export class FileInputElement extends ConfigUIElement { + private _fileExtensions: string[]; private _loadedFilePath: string; private _hovering: boolean; - public constructor(label: string, fileExtension: string) { - super(label); - this._fileExtension = fileExtension; + public constructor() { + super(); + this._fileExtensions = []; this._loadedFilePath = ''; this._hovering = false; } - public generateInnerHTML() { + /** + * Set the allow list of file extensions that can be uploaded. + */ + public setFileExtensions(extensions: string[]) { + this._fileExtensions = extensions; + return this; + } + + protected override _generateInnerHTML() { return ` -
+
${this._loadedFilePath}
`; } - public registerEvents(): void { - const element = document.getElementById(this._id) as HTMLDivElement; - ASSERT(element !== null); - - element.onmouseenter = () => { + public override registerEvents(): void { + this._getElement().addEventListener('mouseenter', () => { this._hovering = true; - }; + this._updateStyle(); + }); - element.onmouseleave = () => { + this._getElement().addEventListener('mouseleave', () => { this._hovering = false; - }; + this._updateStyle(); + }); - element.onclick = () => { - if (!this._isEnabled) { + this._getElement().addEventListener('click', () => { + if (!this.getEnabled()) { return; } @@ -45,43 +51,48 @@ export class FileInputElement extends LabelledElement { title: 'Load file', buttonLabel: 'Load', filters: [{ - name: 'Waveform obj file', - extensions: [`${this._fileExtension}`], + name: 'Model file', + extensions: this._fileExtensions, }], }); - if (files && files.length === 1) { + + if (files && files[0] !== undefined) { const filePath = files[0]; this._loadedFilePath = filePath; - this._value = filePath; + this._setValue(filePath); } - const parsedPath = path.parse(this._loadedFilePath); - element.innerHTML = parsedPath.name + parsedPath.ext; - }; + }); - document.onmousemove = () => { - element.classList.remove('input-file-disabled'); - element.classList.remove('input-file-hover'); - - if (this._isEnabled) { - if (this._hovering) { - element.classList.add('input-file-hover'); - } - } else { - element.classList.add('input-file-disabled'); - } - }; + this._getElement().addEventListener('mousemove', () => { + this._updateStyle(); + }); } - protected _onEnabledChanged() { + protected override _onEnabledChanged() { super._onEnabledChanged(); - const element = document.getElementById(this._id) as HTMLDivElement; - ASSERT(element !== null); - - if (this._isEnabled) { - element.classList.remove('input-file-disabled'); + if (this.getEnabled()) { + this._getElement().classList.remove('input-file-disabled'); } else { - element.classList.add('input-file-disabled'); + this._getElement().classList.add('input-file-disabled'); + } + } + + protected override _onValueChanged(): void { + const parsedPath = path.parse(this._loadedFilePath); + this._getElement().innerHTML = parsedPath.name + parsedPath.ext; + } + + private _updateStyle() { + this._getElement().classList.remove('input-file-disabled'); + this._getElement().classList.remove('input-file-hover'); + + if (this.getEnabled()) { + if (this._hovering) { + this._getElement().classList.add('input-file-hover'); + } + } else { + this._getElement().classList.add('input-file-disabled'); } } } diff --git a/src/ui/elements/labelled_element.ts b/src/ui/elements/labelled_element.ts index 419cae0..e2b3696 100644 --- a/src/ui/elements/labelled_element.ts +++ b/src/ui/elements/labelled_element.ts @@ -1,11 +1,12 @@ -import { BaseUIElement } from './base'; +import { BaseUIElement } from './base_element'; import { LabelElement } from './label'; export abstract class LabelledElement extends BaseUIElement { + private _label: string; private _labelElement: LabelElement; public constructor(label: string) { - super(label); + super(); this._label = label; this._labelElement = new LabelElement(label); } @@ -22,7 +23,7 @@ export abstract class LabelledElement extends BaseUIElement { protected abstract generateInnerHTML(): string; protected _onEnabledChanged() { - this._labelElement.setEnabled(this._isEnabled); + this._labelElement.setEnabled(this.getEnabled()); } public addDescription(text: string) { diff --git a/src/ui/elements/output.ts b/src/ui/elements/output.ts index c6bf53a..6d5ce64 100644 --- a/src/ui/elements/output.ts +++ b/src/ui/elements/output.ts @@ -18,20 +18,6 @@ export class OutputElement { `; } - public registerEvents() { - const toggler = document.getElementsByClassName('caret'); - - for (let i = 0; i < toggler.length; i++) { - const temp = toggler[i]; - temp.addEventListener('click', function () { - temp.parentElement?.querySelector('.nested')?.classList.toggle('active'); - temp.classList.toggle('caret-down'); - }); - } - - this._message.postBuild(); - } - private _message: UIMessageBuilder; public setMessage(message: UIMessageBuilder, style?: OutputStyle) { @@ -74,7 +60,6 @@ export class OutputElement { ASSERT(element !== null); element.innerHTML = this._message.toString(); - this.registerEvents(); } public setStyle(style: OutputStyle) { diff --git a/src/ui/elements/slider.ts b/src/ui/elements/slider.ts index d3063be..892b163 100644 --- a/src/ui/elements/slider.ts +++ b/src/ui/elements/slider.ts @@ -1,46 +1,82 @@ import { clamp, mapRange, wayThrough } from '../../math'; import { ASSERT } from '../../util/error_util'; -import { LabelledElement } from './labelled_element'; +import { UIUtil } from '../../util/ui_util'; +import { ConfigUIElement } from './config_element'; -export class SliderElement extends LabelledElement { +export type TSliderParams = { + min: number, + max: number, + value: number, + decimals: number, + step: number, +} + +export class SliderElement extends ConfigUIElement { private _min: number; private _max: number; private _decimals: number; - private _dragging: boolean; private _step: number; + private _dragging: boolean; private _hovering: boolean; + private _internalValue: number; - public constructor(label: string, min: number, max: number, decimals: number, value: number, step: number) { - super(label); - this._min = min; - this._max = max; - this._decimals = decimals; - this._value = value; - this._step = step; + public constructor() { + super(); + this._min = 0; + this._max = 1; + this._decimals = 1; + this._step = 0.1; + this._internalValue = 0.5; this._dragging = false; this._hovering = false; } - public generateInnerHTML() { - const norm = (this.getValue() - this._min) / (this._max - this._min); - return ` - -
-
-
-
- `; + public override setDefaultValue(value: number) { + super.setDefaultValue(value); + this._internalValue = value; + return this; } - public registerEvents() { - const element = document.getElementById(this._id) as HTMLDivElement; - const elementBar = document.getElementById(this._id + '-bar') as HTMLDivElement; - const elementValue = document.getElementById(this._id + '-value') as HTMLInputElement; - ASSERT(element !== null); + /** + * Set the minimum value the slider can be set to. + */ + public setMin(min: number) { + this._min = min; + return this; + } + + /** + * Set the maximum value the slider can be set to. + */ + public setMax(max: number) { + this._max = max; + return this; + } + + /** + * Set the number of decimals to display the value to. + */ + public setDecimals(decimals: number) { + this._decimals = decimals; + return this; + } + + /** + * Set the step the value is increased/decreased by. + */ + public setStep(step: number) { + this._step = step; + return this; + } + + public override registerEvents() { + const element = this._getElement(); + const elementBar = UIUtil.getElementById(this._getSliderBarId()); + const elementValue = UIUtil.getElementById(this._getSliderValueId()) as HTMLInputElement; element.onmouseenter = () => { this._hovering = true; - if (this._isEnabled) { + if (this.getEnabled()) { element.classList.add('new-slider-hover'); elementBar.classList.add('new-slider-bar-hover'); } @@ -76,7 +112,7 @@ export class SliderElement extends LabelledElement { }); element.addEventListener('wheel', (e: WheelEvent) => { - if (!this._dragging && this._isEnabled) { + if (!this._dragging && this.getEnabled()) { e.preventDefault(); this._onScrollSlider(e); } @@ -87,75 +123,26 @@ export class SliderElement extends LabelledElement { }); } - public setMax(value: number) { - this._max = value; - this._value = clamp(this._value!, this._min, this._max); - this._onValueUpdated(); + protected override _generateInnerHTML() { + const norm = (this._internalValue - this._min) / (this._max - this._min); + + return ` + +
+
+
+
+ `; } - private _onTypedValue() { - const elementValue = document.getElementById(this._id + '-value') as HTMLInputElement; - const typedNumber = parseFloat(elementValue.value); - if (!isNaN(typedNumber)) { - this._value = clamp(typedNumber, this._min, this._max); - } - - this._onValueUpdated(); - } - - private _onScrollSlider(e: WheelEvent) { - if (!this._isEnabled) { - return; - } - ASSERT(this._value !== undefined); - - this._value -= (e.deltaY / 150) * this._step; - this._value = clamp(this._value, this._min, this._max); - - this._onValueUpdated(); - } - - private _onDragSlider(e: MouseEvent) { - if (!this._isEnabled) { - return; - } - - const element = document.getElementById(this._id) as HTMLDivElement; - ASSERT(element !== null); - - const box = element.getBoundingClientRect(); - const left = box.x; - const right = box.x + box.width; - - this._value = mapRange(e.clientX, left, right, this._min, this._max); - this._value = clamp(this._value, this._min, this._max); - - this._onValueUpdated(); - } - - private _onValueUpdated() { - const elementBar = document.getElementById(this._id + '-bar') as HTMLDivElement; - const elementValue = document.getElementById(this._id + '-value') as HTMLInputElement; - ASSERT(elementBar !== null && elementValue !== null); - - const norm = wayThrough(this.getValue(), this._min, this._max); - elementBar.style.width = `${norm * 100}%`; - elementValue.value = this.getValue().toFixed(this._decimals); - } - - public getDisplayValue() { - return parseFloat(this.getValue().toFixed(this._decimals)); - } - - protected _onEnabledChanged() { + protected override _onEnabledChanged() { super._onEnabledChanged(); - const element = document.getElementById(this._id) as HTMLDivElement; - const elementBar = document.getElementById(this._id + '-bar') as HTMLDivElement; - const elementValue = document.getElementById(this._id + '-value') as HTMLInputElement; - ASSERT(element !== null && elementBar !== null && elementValue !== null); + const element = this._getElement(); + const elementBar = UIUtil.getElementById(this._getSliderBarId()); + const elementValue = UIUtil.getElementById(this._getSliderValueId()) as HTMLInputElement; - if (this._isEnabled) { + if (this.getEnabled()) { element.classList.remove('new-slider-disabled'); elementBar.classList.remove('new-slider-bar-disabled'); elementValue.disabled = false; @@ -165,4 +152,67 @@ export class SliderElement extends LabelledElement { elementValue.disabled = true; } } + + protected override _onValueChanged(): void { + const percentage = wayThrough(this.getValue(), this._min, this._max); + ASSERT(percentage >= 0.0 && percentage <= 1.0); + + UIUtil.getElementById(this._getSliderBarId()).style.width = `${percentage * 100}%`; + (UIUtil.getElementById(this._getSliderValueId()) as HTMLInputElement).value = this.getValue().toFixed(this._decimals); + } + + private _onScrollSlider(e: WheelEvent) { + if (!this.getEnabled()) { + return; + } + + this._internalValue -= (e.deltaY / 150) * this._step; + this._internalValue = clamp(this._internalValue, this._min, this._max); + + this._onInternalValueUpdated(); + } + + private _onDragSlider(e: MouseEvent) { + if (!this.getEnabled()) { + return; + } + + const box = this._getElement().getBoundingClientRect(); + const left = box.x; + const right = box.x + box.width; + + this._internalValue = mapRange(e.clientX, left, right, this._min, this._max); + this._internalValue = clamp(this._internalValue, this._min, this._max); + + this._onInternalValueUpdated(); + } + + private _onTypedValue() { + const elementValue = UIUtil.getElementById(this._getSliderValueId()) as HTMLInputElement; + + const typedNumber = parseFloat(elementValue.value); + if (!isNaN(typedNumber)) { + this._internalValue = clamp(typedNumber, this._min, this._max); + } + this._onInternalValueUpdated(); + } + + private _onInternalValueUpdated() { + const displayString = this._internalValue!.toFixed(this._decimals); + this._setValue(parseFloat(displayString)); + } + + /** + * Gets the ID of the DOM element for the slider's value. + */ + private _getSliderValueId() { + return this._getId() + '-value'; + } + + /** + * Gets the ID of the DOM element for the slider's bar. + */ + private _getSliderBarId() { + return this._getId() + '-bar'; + } } diff --git a/src/ui/elements/toolbar_item.ts b/src/ui/elements/toolbar_item.ts index 3793a64..689cb05 100644 --- a/src/ui/elements/toolbar_item.ts +++ b/src/ui/elements/toolbar_item.ts @@ -98,10 +98,8 @@ export class ToolbarItemElement { element.classList.remove('toolbar-item-disabled'); element.classList.remove('toolbar-item-active'); - element.classList.remove('toolbar-item-disabled-active'); svgElement.classList.remove('icon-disabled'); svgElement.classList.remove('icon-active'); - svgElement.classList.remove('icon-disabled-active'); if (this._isEnabled) { if (this._isActive) { @@ -109,13 +107,8 @@ export class ToolbarItemElement { svgElement.classList.add('icon-active'); } } else { - if (this._isActive) { - element.classList.add('toolbar-item-disabled-active'); - svgElement.classList.add('icon-disabled-active'); - } else { - element.classList.add('toolbar-item-disabled'); - svgElement.classList.add('icon-disabled'); - } + element.classList.add('toolbar-item-disabled'); + svgElement.classList.add('icon-disabled'); } } diff --git a/src/ui/elements/vector_spinbox.ts b/src/ui/elements/vector_spinbox.ts index 7e063db..de2d758 100644 --- a/src/ui/elements/vector_spinbox.ts +++ b/src/ui/elements/vector_spinbox.ts @@ -1,64 +1,55 @@ +/* import { ASSERT } from '../../util/error_util'; import { LOG } from '../../util/log_util'; import { Vector3 } from '../../vector'; -import { LabelledElement } from './labelled_element'; +import { ConfigElement } from './config_element'; -/* eslint-disable */ enum EAxis { None = 'none', X = 'x', Y = 'y', Z = 'z', }; -/* eslint-enable */ -export class VectorSpinboxElement extends LabelledElement { +export class VectorSpinboxElement extends ConfigElement { private _mouseover: EAxis; private _dragging: EAxis; private _lastClientX: number; - private _showY: boolean; - public constructor(label: string, decimals: number, value: Vector3, showY: boolean) { + public constructor(label: string, decimals: number, value: Vector3) { super(label); this._value = value; this._mouseover = EAxis.None; this._dragging = EAxis.None; this._lastClientX = 0.0; - this._showY = showY; } public generateInnerHTML() { - ASSERT(this._value !== undefined, 'Value not found'); - let html = ''; - html += '
'; - html += ` -
-
X
-
- ${this._value.x} + ASSERT(this._value, 'Value not found'); + return ` +
+
+
X
+
+ ${this._value.x} +
-
- `; - if (this._showY) { - html += ` -
-
Y
+
+
+
Y
${this._value.y}
- `; - } - html += ` -
-
Z
-
- ${this._value.z} +
+
+
Z
+
+ ${this._value.z} +
`; - html += '
'; - return html; } private _registerAxis(axis: EAxis) { @@ -103,9 +94,7 @@ export class VectorSpinboxElement extends LabelledElement { public registerEvents() { this._registerAxis(EAxis.X); - if (this._showY) { - this._registerAxis(EAxis.Y); - } + this._registerAxis(EAxis.Y); this._registerAxis(EAxis.Z); document.addEventListener('mousedown', (e: any) => { @@ -123,20 +112,20 @@ export class VectorSpinboxElement extends LabelledElement { document.addEventListener('mouseup', () => { const elementXK = document.getElementById(this._id + '-kx') as HTMLDivElement; - const elementYK = document.getElementById(this._id + '-ky') as (HTMLDivElement | undefined); + const elementYK = document.getElementById(this._id + '-ky') as HTMLDivElement; const elementZK = document.getElementById(this._id + '-kz') as HTMLDivElement; const elementXV = document.getElementById(this._id + '-vx') as HTMLDivElement; - const elementYV = document.getElementById(this._id + '-vy') as (HTMLDivElement | undefined); + const elementYV = document.getElementById(this._id + '-vy') as HTMLDivElement; const elementZV = document.getElementById(this._id + '-vz') as HTMLDivElement; switch (this._dragging) { case EAxis.X: - elementXK?.classList.remove('spinbox-key-hover'); + elementXK.classList.remove('spinbox-key-hover'); elementXV.classList.remove('spinbox-value-hover'); break; case EAxis.Y: - elementYK?.classList.remove('spinbox-key-hover'); - elementYV?.classList.remove('spinbox-value-hover'); + elementYK.classList.remove('spinbox-key-hover'); + elementYV.classList.remove('spinbox-value-hover'); break; case EAxis.Z: elementZK.classList.remove('spinbox-key-hover'); @@ -150,7 +139,7 @@ export class VectorSpinboxElement extends LabelledElement { private _updateValue(e: MouseEvent) { ASSERT(this._isEnabled, 'Not enabled'); ASSERT(this._dragging !== EAxis.None, 'Dragging nothing'); - ASSERT(this._value !== undefined, 'No value to update'); + ASSERT(this._value, 'No value to update'); const deltaX = e.clientX - this._lastClientX; this._lastClientX = e.clientX; @@ -168,12 +157,10 @@ export class VectorSpinboxElement extends LabelledElement { } const elementXV = document.getElementById(this._id + '-vx') as HTMLDivElement; - const elementYV = document.getElementById(this._id + '-vy') as (HTMLDivElement | undefined); + const elementYV = document.getElementById(this._id + '-vy') as HTMLDivElement; const elementZV = document.getElementById(this._id + '-vz') as HTMLDivElement; elementXV.innerHTML = this._value.x.toString(); - if (elementYV) { - elementYV.innerHTML = this._value.y.toString(); - } + elementYV.innerHTML = this._value.y.toString(); elementZV.innerHTML = this._value.z.toString(); } @@ -184,29 +171,30 @@ export class VectorSpinboxElement extends LabelledElement { const keyElements = [ document.getElementById(this._id + '-kx') as HTMLDivElement, - document.getElementById(this._id + '-ky') as (HTMLDivElement | undefined), + document.getElementById(this._id + '-ky') as HTMLDivElement, document.getElementById(this._id + '-kz') as HTMLDivElement, ]; const valueElements = [ document.getElementById(this._id + '-vx') as HTMLDivElement, - document.getElementById(this._id + '-vy') as (HTMLDivElement | undefined), + document.getElementById(this._id + '-vy') as HTMLDivElement, document.getElementById(this._id + '-vz') as HTMLDivElement, ]; if (this._isEnabled) { for (const keyElement of keyElements) { - keyElement?.classList.remove('spinbox-key-disabled'); + keyElement.classList.remove('spinbox-key-disabled'); } for (const valueElement of valueElements) { - valueElement?.classList.remove('spinbox-value-disabled'); + valueElement.classList.remove('spinbox-value-disabled'); } } else { for (const keyElement of keyElements) { - keyElement?.classList.add('spinbox-key-disabled'); + keyElement.classList.add('spinbox-key-disabled'); } for (const valueElement of valueElements) { - valueElement?.classList.add('spinbox-value-disabled'); + valueElement.classList.add('spinbox-value-disabled'); } } } } +*/ diff --git a/src/ui/layout.ts b/src/ui/layout.ts index 80cfda7..b122a55 100644 --- a/src/ui/layout.ts +++ b/src/ui/layout.ts @@ -1,6 +1,7 @@ import fs from 'fs'; import { AppContext } from '../app_context'; +import { FallableBehaviour } from '../block_mesh'; import { ArcballCamera } from '../camera'; import { AppConfig } from '../config'; import { EAppEvent, EventManager } from '../event'; @@ -15,10 +16,11 @@ import { TAxis, TTexelExtension } from '../util/type_util'; import { TDithering } from '../util/type_util'; import { TVoxelOverlapRule } from '../voxel_mesh'; import { TVoxelisers } from '../voxelisers/voxelisers'; -import { BaseUIElement } from './elements/base'; +import { BaseUIElement } from './elements/base_element'; import { ButtonElement } from './elements/button'; import { CheckboxElement } from './elements/checkbox'; import { ComboBoxElement, ComboBoxItem } from './elements/combobox'; +import { ConfigUIElement } from './elements/config_element'; import { FileInputElement } from './elements/file_input'; import { OutputElement } from './elements/output'; import { SliderElement } from './elements/slider'; @@ -26,11 +28,11 @@ import { ToolbarItemElement } from './elements/toolbar_item'; export interface Group { label: string; - elements: { [key: string]: BaseUIElement }; + elements: { [key: string]: ConfigUIElement }; elementsOrder: string[]; submitButton: ButtonElement; output: OutputElement; - postElements?: { [key: string]: BaseUIElement }; + postElements?: { [key: string]: ConfigUIElement }; postElementsOrder?: string[]; } @@ -45,148 +47,222 @@ export class UI { 'import': { label: 'Import', elements: { - 'input': new FileInputElement('Wavefront .obj file', 'obj'), + 'input': new FileInputElement() + .setFileExtensions(['obj']) + .setLabel('Wavefront .obj file'), }, elementsOrder: ['input'], - submitButton: new ButtonElement('Load mesh', () => { - this._appContext.do(EAction.Import); - }), + submitButton: new ButtonElement() + .setOnClick(() => { + this._appContext.do(EAction.Import); + }) + .setLabel('Load mesh'), output: new OutputElement(), }, 'voxelise': { label: 'Voxelise', elements: { - 'constraintAxis': new ComboBoxElement('Constraint axis', [ - { - id: 'y', - displayText: 'Y (height) (green)', - }, - { - id: 'x', - displayText: 'X (width) (red)', - }, - { - id: 'z', - displayText: 'Z (depth) (blue)', - }, - ]).onValueChanged((value: string) => { - if (value === 'x') { - this._ui.voxelise.elements.size.setMax(this._appContext.maxConstraint?.x ?? 400); - } else if (value === 'y') { - this._ui.voxelise.elements.size.setMax(this._appContext.maxConstraint?.y ?? AppConfig.Get.CONSTRAINT_MAXIMUM_HEIGHT); - } else { - this._ui.voxelise.elements.size.setMax(this._appContext.maxConstraint?.z ?? 400); - } - }), - 'size': new SliderElement('Size', 3, AppConfig.Get.CONSTRAINT_MAXIMUM_HEIGHT, 0, 80, 1), - 'voxeliser': new ComboBoxElement('Algorithm', [ - { - id: 'bvh-ray', - displayText: 'BVH Ray-based', - }, - { - id: 'bvh-ray-plus-thickness', - displayText: 'BVH Ray-based (thicker walls)', - }, - { - id: 'ncrb', - displayText: 'NCRB', - }, - { - id: 'ray-based', - displayText: 'Ray-based (legacy)', - }, - ]), - 'ambientOcclusion': new CheckboxElement('Ambient occlusion', true, 'On (recommended)', 'Off (faster)'), - 'multisampleColouring': new CheckboxElement('Multisampling', true, 'On (recommended)', 'Off (faster)'), - 'voxelOverlapRule': new ComboBoxElement('Voxel overlap', [ - { - id: 'average', + 'constraintAxis': new ComboBoxElement() + .addItem({ payload: 'y', displayText: 'Y (height) (green)' }) + .addItem({ payload: 'x', displayText: 'X (width) (red)' }) + .addItem({ payload: 'z', displayText: 'Z (depth) (blue)' }) + .setLabel('Constraint axis') + .addValueChangedListener((value: TAxis) => { + /* + switch (value) { + case 'x': + this._ui.voxelise.elements.size.setMax(this._appContext.maxConstraint?.x ?? 400); + break; + case 'y': + this._ui.voxelise.elements.size.setMax(this._appContext.maxConstraint?.y ?? AppConfig.Get.CONSTRAINT_MAXIMUM_HEIGHT); + break; + case 'z': + this._ui.voxelise.elements.size.setMax(this._appContext.maxConstraint?.z ?? 400); + break; + } + */ + }), + 'size': new SliderElement() + .setMin(3) + .setMax(380) + .setDefaultValue(80) + .setDecimals(0) + .setStep(1) + .setLabel('Size'), + 'voxeliser': new ComboBoxElement() + .addItem({ payload: 'bvh-ray', displayText: 'BVH Ray-based' }) + .addItem({ payload: 'ncrb', displayText: 'NCRB' }) + .addItem({ payload: 'ray-based', displayText: 'Ray-based (legacy)' }) + .setLabel('Algorithm'), + 'ambientOcclusion': new CheckboxElement() + .setCheckedText('On (recommended)') + .setUncheckedText('Off (faster)') + .setDefaultValue(true) + .setLabel('Ambient occlusion'), + 'multisampleColouring': new CheckboxElement() + .setCheckedText('On (recommended)') + .setUncheckedText('Off (faster)') + .setDefaultValue(true) + .setLabel('Multisampling'), + 'voxelOverlapRule': new ComboBoxElement() + .addItem({ displayText: 'Average (recommended)', + payload: 'average', tooltip: 'If multiple voxels are placed in the same location, take the average of their colours', - }, - { - id: 'first', + }) + .addItem({ displayText: 'First', + payload: 'first', tooltip: 'If multiple voxels are placed in the same location, use the first voxel\'s colour', - }, - ]), + }) + .setLabel('Voxel overlap'), }, - elementsOrder: ['constraintAxis', 'size', 'voxeliser', 'ambientOcclusion', 'multisampleColouring', 'voxelOverlapRule'], - submitButton: new ButtonElement('Voxelise mesh', () => { - this._appContext.do(EAction.Voxelise); - }), + elementsOrder: [ + 'constraintAxis', + 'size', + 'voxeliser', + 'ambientOcclusion', + 'multisampleColouring', + 'voxelOverlapRule', + ], + submitButton: new ButtonElement() + .setOnClick(() => { + this._appContext.do(EAction.Voxelise); + }) + .setLabel('Voxelise mesh'), output: new OutputElement(), }, 'assign': { label: 'Assign', elements: { - 'textureAtlas': new ComboBoxElement('Texture atlas', this._getTextureAtlases()), - 'blockPalette': new ComboBoxElement('Block palette', this._getBlockPalettes()), - 'dithering': new ComboBoxElement('Dithering', [ - { id: 'ordered', displayText: 'Ordered' }, - { id: 'random', displayText: 'Random' }, - { id: 'off', displayText: 'Off' }, - ]), - 'fallable': new ComboBoxElement('Fallable blocks', [ + 'textureAtlas': new ComboBoxElement() + .addItems(this._getTextureAtlases()) + .setLabel('Texture atlas'), + 'blockPalette': new ComboBoxElement() + .addItems(this._getBlockPalettes()) + .setLabel('Block palette'), + 'dithering': new ComboBoxElement() + .addItems([{ + displayText: 'Ordered', + payload: 'ordered', + }, { - id: 'replace-falling', + displayText: 'Random', + payload: 'random', + }, + { + displayText: 'Off', + payload: 'off', + }]) + .setLabel('Dithering'), + 'fallable': new ComboBoxElement() + .addItems([{ displayText: 'Replace falling with solid', + payload: 'replace-falling', + tooltip: 'Replace all blocks that will fall with solid blocks', + }, + { + displayText: 'Replace fallable with solid', + payload: 'replace-fallable', tooltip: 'Replace all blocks that can fall with solid blocks', }, { - id: 'replace-fallable', - displayText: 'Replace fallable with solid', - tooltip: 'Replace all blocks that will fall with solid blocks', - }, - /* - { - id: 'place-string', - displayText: 'Place string under', - tooltip: 'Place string blocks under all blocks that would fall otherwise', - }, - */ - { - id: 'do-nothing', displayText: 'Do nothing', + payload: 'do-nothing', tooltip: 'Let the block fall', - }, - ]), - 'colourAccuracy': new SliderElement('Colour accuracy', 1, 8, 1, 5, 0.1), - 'contextualAveraging': new CheckboxElement('Smart averaging', true, 'On (recommended)', 'Off (faster)'), - 'errorWeight': new SliderElement('Smoothness', 0.0, AppConfig.Get.SMOOTHNESS_MAX, 2, 0.2, 0.01), - 'calculateLighting': new CheckboxElement('Calculate lighting', false, 'On', 'Off') - .onValueChanged((value: boolean) => { + }]) + .setLabel('Fallable blocks'), + 'colourAccuracy': new SliderElement() + .setMin(1) + .setMax(8) + .setDefaultValue(5) + .setDecimals(1) + .setStep(0.1) + .setLabel('Colour accuracy'), + 'contextualAveraging': new CheckboxElement() + .setCheckedText('On (recommended)') + .setUncheckedText('Off (faster)') + .setDefaultValue(true) + .setLabel('Smart averaging'), + 'errorWeight': new SliderElement() + .setMin(0.0) + .setMax(2.0) + .setDefaultValue(0.2) + .setDecimals(2) + .setStep(0.01) + .setLabel('Smoothness'), + 'calculateLighting': new CheckboxElement() + .setCheckedText('On') + .setUncheckedText('Off') + .setDefaultValue(false) + .setLabel('Calculate lighting') + .addValueChangedListener((value: boolean) => { if (value) { this._ui.assign.elements.lightThreshold.setEnabled(true, false); } else { this._ui.assign.elements.lightThreshold.setEnabled(false, false); } }), - 'lightThreshold': new SliderElement('Light threshold', 0, 14, 0, 0, 1) - .setObeyGroupEnables(false), + 'lightThreshold': new SliderElement() + .setMin(0) + .setMax(14) + .setDefaultValue(1) + .setDecimals(0) + .setStep(1) + .setLabel('Light threshold'), }, - elementsOrder: ['textureAtlas', 'blockPalette', 'dithering', 'fallable', 'colourAccuracy', 'contextualAveraging', 'errorWeight', 'calculateLighting', 'lightThreshold'], - submitButton: new ButtonElement('Assign blocks', () => { - this._appContext.do(EAction.Assign); - }), + elementsOrder: [ + 'textureAtlas', + 'blockPalette', + 'dithering', + 'fallable', + 'colourAccuracy', + 'contextualAveraging', + 'errorWeight', + 'calculateLighting', + 'lightThreshold', + ], + submitButton: new ButtonElement() + .setOnClick(() => { + this._appContext.do(EAction.Assign); + }) + .setLabel('Assign blocks'), output: new OutputElement(), }, 'export': { label: 'Export', elements: { - 'export': new ComboBoxElement('File format', [ - { id: 'litematic', displayText: 'Litematic (.litematic)' }, - { id: 'schematic', displayText: 'Schematic (.schematic)' }, - { id: 'obj', displayText: 'Wavefront OBJ (.obj)' }, - { id: 'schem', displayText: 'Sponge Schematic (.schem)' }, - { id: 'nbt', displayText: 'Structure blocks (.nbt)' }, - ]), + 'export': new ComboBoxElement() + .addItems([ + { + displayText: 'Litematic (.litematic)', + payload: 'litematic', + }, + { + displayText: 'Schematic (.schematic)', + payload: 'schematic', + }, + { + displayText: 'Wavefront OBJ (.obj)', + payload: 'obj', + }, + { + displayText: 'Sponge Schematic (.schem)', + payload: 'schem', + }, + { + displayText: 'Structure blocks (.nbt)', + payload: 'nbt', + }, + ]) + .setLabel('Exporter'), }, elementsOrder: ['export'], - submitButton: new ButtonElement('Export structure', () => { - this._appContext.do(EAction.Export); - }), + submitButton: new ButtonElement() + .setLabel('Export structure') + .setOnClick(() => { + this._appContext.do(EAction.Export); + }), output: new OutputElement(), }, }; @@ -326,9 +402,6 @@ export class UI { constructor(appContext: AppContext) { this._appContext = appContext; - - this._ui.assign.elements.textureAtlas.addDescription('Textures to use and colour-match with'); - this._ui.assign.elements.fallable.addDescription('Read tooltips for more info'); } public tick(isBusy: boolean) { @@ -426,6 +499,7 @@ export class UI { let groupHTML = ''; for (const elementName of group.elementsOrder) { const element = group.elements[elementName]; + ASSERT(element !== undefined, `No element for: ${elementName}`); groupHTML += this._buildSubcomponent(element); } @@ -434,6 +508,7 @@ export class UI { ASSERT(group.postElementsOrder, 'No post elements order'); for (const elementName of group.postElementsOrder) { const element = group.postElements[elementName]; + ASSERT(element !== undefined, `No element for: ${elementName}`); postGroupHTML += this._buildSubcomponent(element); } } @@ -452,7 +527,7 @@ export class UI { `; } - private _buildSubcomponent(element: BaseUIElement) { + private _buildSubcomponent(element: ConfigUIElement) { return `
${element.generateHTML()} @@ -476,6 +551,7 @@ export class UI { for (const elementName in group.elements) { const element = group.elements[elementName]; element.registerEvents(); + element.finalise(); } group.submitButton.registerEvents(); if (group.postElements) { @@ -483,6 +559,7 @@ export class UI { for (const elementName in group.postElements) { const element = group.postElements[elementName]; element.registerEvents(); + element.finalise(); } } } @@ -590,7 +667,7 @@ export class UI { const paletteID = file.split('.')[0]; let paletteName = paletteID.replace('-', ' ').toLowerCase(); paletteName = paletteName.charAt(0).toUpperCase() + paletteName.slice(1); - textureAtlases.push({ id: paletteID, displayText: paletteName }); + textureAtlases.push({ payload: paletteID, displayText: paletteName }); } }); @@ -603,7 +680,7 @@ export class UI { const palettes = PaletteManager.getPalettesInfo(); for (const palette of palettes) { blockPalettes.push({ - id: palette.paletteID, + payload: palette.paletteID, displayText: palette.paletteDisplayName, }); } diff --git a/src/util/ui_util.ts b/src/util/ui_util.ts new file mode 100644 index 0000000..29aa512 --- /dev/null +++ b/src/util/ui_util.ts @@ -0,0 +1,9 @@ +import { ASSERT } from './error_util'; + +export namespace UIUtil { + export function getElementById(id: string) { + const element = document.getElementById(id); + ASSERT(element !== null, `Attempting to getElement of nonexistent element: ${id}`); + return element as HTMLElement; + } +}