Major UI refactor

This commit is contained in:
Lucas Dower 2022-12-10 18:18:20 +00:00
parent b04f381de5
commit a34089d339
No known key found for this signature in database
GPG Key ID: B3EE6B8499593605
15 changed files with 826 additions and 560 deletions

View File

@ -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({

View File

@ -1,49 +0,0 @@
import { ASSERT } from '../../util/error_util';
export abstract class BaseUIElement<Type> {
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;
}
}

View File

@ -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<T> {
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;
}

View File

@ -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<any> {
export class ButtonElement extends BaseUIElement<HTMLDivElement> {
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 `
<div class="button" id="${this._id}">
<div class="button-label">${this._label}</div>
<div class="button-progress" id="${this._id}-progress"></div>
</div>
`;
}
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<any> {
* @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 `
<div class="button" id="${this._getId()}">
<div class="button-label">${this._label}</div>
<div class="button-progress" id="${this._getProgressBarId()}"></div>
</div>
`;
}
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';
}
}

View File

@ -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<boolean> {
private _checkboxId: string;
private _checkboxPipId: string;
private _checkboxTextId: string;
private _onText: string;
private _offText: string;
export class CheckboxElement extends ConfigUIElement<boolean, HTMLSelectElement> {
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 `
<div class="checkbox" id="${this._checkboxId}">
<svg id="${this._checkboxPipId}" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M5 12l5 5l10 -10" />
</svg>
</div>
<div class="checkbox-text" id="${this._checkboxTextId}">${this.getValue() ? this._onText : this._offText}</div>
`;
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 `
<div class="checkbox" id="${this._getId()}">
<svg id="${this._getPipId()}" xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-check" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#2c3e50" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M5 12l5 5l10 -10" />
</svg>
</div>
<div class="checkbox-text" id="${this._getLabelId()}">${this.getValue() ? this._labelChecked : this._labelUnchecked}</div>
`;
}
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<boolean> {
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';
}
}

View File

@ -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<T> = {
id: T;
payload: T;
displayText: string;
tooltip?: string;
}
export class ComboBoxElement<T> extends LabelledElement<T> {
export class ComboBoxElement<T> extends ConfigUIElement<T, HTMLSelectElement> {
private _items: ComboBoxItem<T>[];
public constructor(id: string, items: ComboBoxItem<T>[]) {
super(id);
this._items = items;
public constructor() {
super();
this._items = [];
}
public generateInnerHTML() {
public addItems(items: ComboBoxItem<T>[]) {
items.forEach((item) => {
this.addItem(item);
});
return this;
}
public addItem(item: ComboBoxItem<T>) {
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 += `<option value="${item.id}" title="${item.tooltip || ''}">${item.displayText}</option>`;
itemsHTML += `<option value="${item.payload}" title="${item.tooltip || ''}">${item.displayText}</option>`;
}
return `
<select name="${this._id}" id="${this._id}">
<select name="${this._getId()}" id="${this._getId()}">
${itemsHTML}
</select>
`;
}
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 {
}
}

View File

@ -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<T, F> extends BaseUIElement<F> {
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()}
<div class="prop-value-container">
${this._generateInnerHTML()}
</div>
`;
}
/**
* 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;
}

View File

@ -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<string> {
private _fileExtension: string;
export class FileInputElement extends ConfigUIElement<string, HTMLDivElement> {
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 `
<div class="input-file" id="${this._id}">
<div class="input-file" id="${this._getId()}">
${this._loadedFilePath}
</div>
`;
}
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<string> {
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');
}
}
}

View File

@ -1,11 +1,12 @@
import { BaseUIElement } from './base';
import { BaseUIElement } from './base_element';
import { LabelElement } from './label';
export abstract class LabelledElement<Type> extends BaseUIElement<Type> {
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<Type> extends BaseUIElement<Type> {
protected abstract generateInnerHTML(): string;
protected _onEnabledChanged() {
this._labelElement.setEnabled(this._isEnabled);
this._labelElement.setEnabled(this.getEnabled());
}
public addDescription(text: string) {

View File

@ -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) {

View File

@ -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<number> {
export type TSliderParams = {
min: number,
max: number,
value: number,
decimals: number,
step: number,
}
export class SliderElement extends ConfigUIElement<number, HTMLDivElement> {
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 `
<input type="number" id="${this._id}-value" min="${this._min}" max="${this._max}" step="${this._step}" value="${this.getValue().toFixed(this._decimals)}">
<div class="new-slider" id="${this._id}" style="flex-grow: 1;">
<div class="new-slider-bar" id="${this._id}-bar"style="width: ${norm * 100}%;">
</div>
</div>
`;
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<number> {
});
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<number> {
});
}
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 `
<input type="number" id="${this._getSliderValueId()}" min="${this._min}" max="${this._max}" step="${this._step}" value="${this.getValue().toFixed(this._decimals)}">
<div class="new-slider" id="${this._getId()}" style="flex-grow: 1;">
<div class="new-slider-bar" id="${this._getSliderBarId()}" style="width: ${norm * 100}%;">
</div>
</div>
`;
}
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<number> {
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';
}
}

View File

@ -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');
}
}

View File

@ -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<Vector3> {
export class VectorSpinboxElement extends ConfigElement<Vector3> {
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 += '<div class="spinbox-main-container">';
html += `
<div class="spinbox-element-container">
<div class="spinbox-key" id="${this._id}-kx">X</div>
<div class="spinbox-value" id="${this._id}-vx">
${this._value.x}
ASSERT(this._value, 'Value not found');
return `
<div style="display: flex; flex-direction: row;">
<div style="display: flex; flex-direction: row; width: 33%">
<div class="spinbox-key" id="${this._id}-kx" style="background-color: #FF4C4C;">X</div>
<div class="spinbox-value" id="${this._id}-vx">
${this._value.x}
</div>
</div>
</div>
`;
if (this._showY) {
html += `
<div class="spinbox-element-container">
<div class="spinbox-key" id="${this._id}-ky">Y</div>
<div class="invis-divider"></div>
<div style="display: flex; flex-direction: row; width: 33%"">
<div class="spinbox-key" id="${this._id}-ky" style="background-color: #34BF49;">Y</div>
<div class="spinbox-value" id="${this._id}-vy">
${this._value.y}
</div>
</div>
`;
}
html += `
<div class="spinbox-element-container">
<div class="spinbox-key" id="${this._id}-kz">Z</div>
<div class="spinbox-value" id="${this._id}-vz">
${this._value.z}
<div class="invis-divider"></div>
<div style="display: flex; flex-direction: row; width: 33%"">
<div class="spinbox-key" id="${this._id}-kz" style="background-color: #0099E5;">Z</div>
<div class="spinbox-value" id="${this._id}-vz">
${this._value.z}
</div>
</div>
</div>
`;
html += '</div>';
return html;
}
private _registerAxis(axis: EAxis) {
@ -103,9 +94,7 @@ export class VectorSpinboxElement extends LabelledElement<Vector3> {
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<Vector3> {
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<Vector3> {
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<Vector3> {
}
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<Vector3> {
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');
}
}
}
}
*/

View File

@ -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<any> };
elements: { [key: string]: ConfigUIElement<any, any> };
elementsOrder: string[];
submitButton: ButtonElement;
output: OutputElement;
postElements?: { [key: string]: BaseUIElement<any> };
postElements?: { [key: string]: ConfigUIElement<any, any> };
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<TAxis>('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<TVoxelisers>('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<TVoxelOverlapRule>('Voxel overlap', [
{
id: 'average',
'constraintAxis': new ComboBoxElement<TAxis>()
.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<TVoxelisers>()
.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<TVoxelOverlapRule>()
.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<TDithering>('Dithering', [
{ id: 'ordered', displayText: 'Ordered' },
{ id: 'random', displayText: 'Random' },
{ id: 'off', displayText: 'Off' },
]),
'fallable': new ComboBoxElement('Fallable blocks', [
'textureAtlas': new ComboBoxElement<string>()
.addItems(this._getTextureAtlases())
.setLabel('Texture atlas'),
'blockPalette': new ComboBoxElement<string>()
.addItems(this._getBlockPalettes())
.setLabel('Block palette'),
'dithering': new ComboBoxElement<TDithering>()
.addItems([{
displayText: 'Ordered',
payload: 'ordered',
},
{
id: 'replace-falling',
displayText: 'Random',
payload: 'random',
},
{
displayText: 'Off',
payload: 'off',
}])
.setLabel('Dithering'),
'fallable': new ComboBoxElement<FallableBehaviour>()
.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<TExporters>('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<TExporters>()
.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<any>) {
private _buildSubcomponent(element: ConfigUIElement<any, any>) {
return `
<div class="property">
${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,
});
}

9
src/util/ui_util.ts Normal file
View File

@ -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;
}
}