Updated action output styling

This commit is contained in:
Lucas Dower 2022-09-09 20:59:14 +01:00
parent 2881d0f957
commit c887e7ff5c
12 changed files with 219 additions and 112 deletions

View File

@ -1,6 +1,6 @@
import { UI } from './ui/layout';
import { Renderer } from './renderer';
import { StatusHandler } from './status';
import { StatusHandler, StatusMessage } from './status';
import { UIMessageBuilder } from './ui/misc';
import { ArcballCamera } from './camera';
@ -11,6 +11,7 @@ import { LOG } from './util/log_util';
import { ASSERT } from './util/error_util';
import { EAction } from './util';
import { AppConfig } from './config';
import { OutputStyle } from './ui/elements/output';
export class AppContext {
private _ui: UI;
@ -27,74 +28,90 @@ export class AppContext {
this._ui.disable(EAction.Voxelise);
this._workerController = new WorkerController(path.resolve(__dirname, 'worker_interface.js'));
Renderer.Get.toggleIsAxesEnabled();
ArcballCamera.Get.setCameraMode('perspective');
ArcballCamera.Get.toggleAngleSnap();
}
public do(action: EAction) {
LOG(`Doing ${action}`);
this._ui.disable(action + 1);
this._ui.disable(action);
this._ui.cacheValues(action);
StatusHandler.Get.clear();
const actionCommand = this._getWorkerJob(action);
const workerJob = this._getWorkerJob(action);
const uiOutput = this._ui.getActionOutput(action);
const jobCallback = (payload: TFromWorkerMessage) => {
if (payload.action === 'KnownError') {
uiOutput.setMessage(UIMessageBuilder.fromString(payload.error.message), 'error');
} else if (payload.action === 'UnknownError') {
uiOutput.setMessage(UIMessageBuilder.fromString('Something went wrong...'), 'error');
} else {
// The job was successful
const infoStatuses = payload.statusMessages
.filter(x => x.status === 'info')
.map(x => x.message);
const hasInfos = infoStatuses.length > 0;
const warningStatuses = payload.statusMessages
.filter(x => x.status === 'warning')
.map(x => x.message);
const hasWarnings = warningStatuses.length > 0;
const builder = new UIMessageBuilder();
builder.addBold(StatusHandler.Get.getDefaultSuccessMessage(action) + (hasInfos ? ':' : ''));
builder.addItem(...infoStatuses);
if (hasWarnings) {
builder.addHeading('There were some warnings:');
builder.addItem(...warningStatuses);
this._ui.enable(action);
switch (payload.action) {
case 'KnownError': {
const builder = uiOutput.getMessage();
{
builder.clear('action');
builder.addHeading('action', StatusHandler.Get.getDefaultFailureMessage(action), 'error')
builder.addItem('action', [ payload.error.message ], 'error');
}
uiOutput.setMessage(builder);
break;
}
case 'UnknownError': {
const builder = uiOutput.getMessage();
{
builder.clear('action');
builder.addHeading('action', StatusHandler.Get.getDefaultFailureMessage(action), 'error')
builder.addItem('action', [ 'Something unexpectedly went wrong...' ], 'error');
}
uiOutput.setMessage(builder);
break;
}
default: {
this._ui.enable(action + 1);
const { builder, style } = this._getActionMessageBuilder(action, payload.statusMessages);
uiOutput.setMessage(builder, style as OutputStyle);
this._ui.getActionButton(action)
.removeLabelOverride()
.stopLoading();
uiOutput.setMessage(builder, hasWarnings ? 'warning' : 'success');
this._ui.getActionButton(action)
.removeLabelOverride()
.stopLoading();
this._ui.enable(action);
this._ui.enable(action + 1);
if (actionCommand.callback) {
actionCommand.callback(payload);
if (workerJob.callback) {
workerJob.callback(payload);
}
}
}
}
this._workerController.addJob({
id: actionCommand.id,
payload: actionCommand.payload,
id: workerJob.id,
payload: workerJob.payload,
callback: jobCallback,
});
}
this._ui.getActionButton(action)
.setLabelOverride('Loading...')
.startLoading();
this._ui.disable(action);
private _getActionMessageBuilder(action: EAction, statusMessages: StatusMessage[]) {
const infoStatuses = statusMessages
.filter(x => x.status === 'info')
.map(x => x.message);
const hasInfos = infoStatuses.length > 0;
const warningStatuses = statusMessages
.filter(x => x.status === 'warning')
.map(x => x.message);
const hasWarnings = warningStatuses.length > 0;
const builder = new UIMessageBuilder();
builder.addBold('action', [StatusHandler.Get.getDefaultSuccessMessage(action) + (hasInfos ? ':' : '')], 'success');
builder.addItem('action', infoStatuses, 'success');
if (hasWarnings) {
builder.addHeading('action', 'There were some warnings:', 'warning');
builder.addItem('action', warningStatuses, 'warning');
}
return { builder: builder, style: hasWarnings ? 'warning' : 'success' };
}
private _getWorkerJob(action: EAction): TWorkerJob {
switch(action) {
switch (action) {
case EAction.Import:
return this._import();
}
@ -106,7 +123,7 @@ export class AppContext {
const payload: TToWorkerMessage = {
action: 'Import',
params: {
params: {
filepath: uiElements.input.getCachedValue()
}
};
@ -118,13 +135,24 @@ export class AppContext {
if (payload.result.triangleCount < AppConfig.RENDER_TRIANGLE_THRESHOLD) {
this._workerController.addJob(this._renderMesh());
const builder = this._ui.getActionOutput(EAction.Import).getMessage();
builder.clear('render');
builder.addTask('render', `Rendering mesh...`);
this._ui.getActionOutput(EAction.Import).setMessage(builder);
} else {
this._ui.getActionOutput(EAction.Import)
.addMessage(UIMessageBuilder.fromString(`Will not render mesh as its over ${AppConfig.RENDER_TRIANGLE_THRESHOLD} triangles.`))
.setStyle('warning');
const builder = this._ui.getActionOutput(EAction.Import).getMessage();
builder.clear('render');
builder.addHeading('render', 'Render', 'warning');
builder.addItem('render', [`Will not render mesh as its over ${AppConfig.RENDER_TRIANGLE_THRESHOLD} triangles.`], 'warning');
this._ui.getActionOutput(EAction.Import).setMessage(builder);
}
};
const builder = new UIMessageBuilder();
builder.addTask('action', 'Loading mesh...');
this._ui.getActionOutput(EAction.Import).setMessage(builder, 'none');
return { id: 'Import', payload: payload, callback: callback };
}
@ -138,27 +166,28 @@ export class AppContext {
// This callback is not managed through `AppContext::do`, therefore
// we need to check the payload is not an error
if (payload.action === 'KnownError') {
const builder = UIMessageBuilder
.create()
.addHeading('Could not draw the mesh')
.addItem(payload.error.message);
const builder = this._ui.getActionOutput(EAction.Import).getMessage()
.clear('render')
.addHeading('render', 'Could not draw the mesh', 'error')
.addItem('render', [payload.error.message], 'error');
this._ui.getActionOutput(EAction.Import)
.addMessage(builder)
.setStyle('warning');
this._ui.getActionOutput(EAction.Import).setMessage(builder, 'warning');
} else if (payload.action === 'UnknownError') {
const builder = UIMessageBuilder
.create()
.addBold('Could not draw the mesh')
this._ui.getActionOutput(EAction.Import)
.addMessage(builder)
.setStyle('warning');
const builder = this._ui.getActionOutput(EAction.Import).getMessage()
.clear('render')
.addBold('render', ['Could not draw the mesh'], 'error');
this._ui.getActionOutput(EAction.Import).setMessage(builder, 'warning');
} else {
const builder = this._ui.getActionOutput(EAction.Import).getMessage()
.clear('render')
.addBold('render', [ 'Rendered mesh' ], 'success');
this._ui.getActionOutput(EAction.Import).setMessage(builder, 'success');
ASSERT(payload.action === 'RenderMesh');
Renderer.Get.useMesh(payload.result.buffers);
Renderer.Get.useMesh(payload.result);
}
};

View File

@ -12,6 +12,7 @@ import { RGBA, RGBAUtil } from './colour';
import { Texture } from './texture';
import { LOG } from './util/log_util';
import { TMeshBufferDescription } from './buffer';
import { RenderMeshParams } from './worker_types';
/* eslint-disable */
export enum MeshType {
@ -166,11 +167,11 @@ export class Renderer {
this.setModelToUse(MeshType.None);
}
public useMesh(meshDescription: TMeshBufferDescription[]) {
public useMesh(params: RenderMeshParams.Output) {
LOG('Using mesh');
this._materialBuffers = [];
for (const { material, buffer, numElements } of meshDescription) {
for (const { material, buffer, numElements } of params.buffers) {
if (material.type === MaterialType.solid) {
this._materialBuffers.push({
buffer: twgl.createBufferInfoFromArrays(this._gl, buffer),
@ -199,12 +200,9 @@ export class Renderer {
}
}
// TODO: const dimensions = mesh.getBounds().getDimensions();
const dimensions = new Vector3(1, 1, 1);
this._gridBuffers.x[MeshType.TriangleMesh] = DebugGeometryTemplates.gridX(dimensions);
this._gridBuffers.y[MeshType.TriangleMesh] = DebugGeometryTemplates.gridY(dimensions);
this._gridBuffers.z[MeshType.TriangleMesh] = DebugGeometryTemplates.gridZ(dimensions);
this._gridBuffers.x[MeshType.TriangleMesh] = DebugGeometryTemplates.gridX(params.dimensions);
this._gridBuffers.y[MeshType.TriangleMesh] = DebugGeometryTemplates.gridY(params.dimensions);
this._gridBuffers.z[MeshType.TriangleMesh] = DebugGeometryTemplates.gridZ(params.dimensions);
this._modelsAvailable = 1;
this.setModelToUse(MeshType.TriangleMesh);

View File

@ -46,15 +46,15 @@ export class StatusHandler {
public getDefaultSuccessMessage(action: EAction): string {
switch (action) {
case EAction.Import:
return 'Successfully loaded mesh';
return 'Loaded mesh';
case EAction.Voxelise:
return 'Successfully voxelised mesh';
return 'Voxelised mesh';
case EAction.Assign:
return 'Successfully assigned blocks';
return 'Assigned blocks';
case EAction.Export:
return 'Successfully exported mesh';
return 'Exported mesh';
default:
return 'Successfully performed action';
return 'Performed action';
}
}

View File

@ -13,7 +13,6 @@ export abstract class LabelledElement<Type> extends BaseUIElement<Type> {
public generateHTML() {
return `
${this._labelElement.generateHTML()}
<div class="divider"></div>
<div class="prop-right">
${this.generateInnerHTML()}
</div>

View File

@ -1,7 +1,7 @@
import { ASSERT } from "../../util/error_util";
import { UIMessageBuilder } from '../misc';
export type OutputStyle = 'success' | 'warning' | 'error';
export type OutputStyle = 'success' | 'warning' | 'error' | 'none';
export class OutputElement {
private _id: string;
@ -30,7 +30,7 @@ export class OutputElement {
private _message: UIMessageBuilder;
public setMessage(message: UIMessageBuilder, style: OutputStyle) {
public setMessage(message: UIMessageBuilder, style?: OutputStyle) {
const element = document.getElementById(this._id) as HTMLDivElement;
ASSERT(element !== null);
@ -40,17 +40,22 @@ export class OutputElement {
element.innerHTML = this._message.toString();
switch (style) {
case 'success':
element.classList.add('border-success');
//element.classList.add('border-success');
break;
case 'warning':
element.classList.add('border-warning');
//element.classList.add('border-warning');
break;
case 'error':
element.classList.add('border-error');
//element.classList.add('border-error');
break;
}
}
public getMessage() {
return this._message;
}
/*
public addMessage(message: UIMessageBuilder) {
this._message.join(message);
@ -60,6 +65,7 @@ export class OutputElement {
element.innerHTML = this._message.toString();
return this;
}
*/
public setStyle(style: OutputStyle) {
const element = document.getElementById(this._id) as HTMLDivElement;

View File

@ -502,7 +502,11 @@ export class UI {
}
}
public disable(action: EAction) {
public disableAll() {
this.disable(EAction.Import, false);
}
public disable(action: EAction, clearOutput: boolean = true) {
if (action < 0) {
return;
}
@ -514,7 +518,9 @@ export class UI {
group.elements[compName].setEnabled(false);
}
group.submitButton.setEnabled(false);
group.output.clearMessage();
if (clearOutput) {
group.output.clearMessage();
}
if (group.postElements) {
LOG(group.label, 'has post-element');
ASSERT(group.postElementsOrder);

View File

@ -1,5 +1,12 @@
import { OutputStyle } from './elements/output';
type TMessage = {
groupId: string,
body: string,
}
export class UIMessageBuilder {
private _messages: string[];
private _messages: TMessage[];
public constructor() {
this._messages = [];
@ -9,43 +16,80 @@ export class UIMessageBuilder {
return new UIMessageBuilder();
}
public addHeading(message: string) {
this.addBold(message + ':');
public addHeading(groupId: string, message: string, style: OutputStyle) {
this.addBold(groupId, [message + ':'], style);
return this;
}
public addBold(...messages: string[]) {
public addBold(groupId: string, messages: string[], style: OutputStyle) {
for (const message of messages) {
this._messages.push(`<b>${message}</b>`);
const cssColourClass = this._getStatusCSSClass(style);
this._messages.push({ groupId: groupId, body: `
<div style="display: flex; align-items: center;" ${cssColourClass ? `class="${cssColourClass}"` : ''}>
<div style="margin-right: 8px;" class="loader-circle"></div>
<b>${message}</b>
</div>
`});
}
return this;
}
public add(...messages: string[]) {
public addItem(groupId: string, messages: string[], style: OutputStyle) {
for (const message of messages) {
this._messages.push(message);
const cssColourClass = this._getStatusCSSClass(style);
this._messages.push({ groupId: groupId, body: `
<div style="padding-left: 16px;" ${cssColourClass ? `class="${cssColourClass}"` : ''}> - ${message}</div>
`});
}
return this;
}
public addItem(...messages: string[]) {
for (const message of messages) {
this._messages.push('• ' + message);
}
public addTask(groupId: string, message: string) {
this._messages.push({ groupId: groupId, body: `
<div style="display: flex; align-items: center; color: var(--text-standard)">
<div style="margin-right: 8px;" class="loader-circle spin"></div>
<b class="spin">${message}</b>
</div>
`});
return this;
}
public clear(groupId: string) {
this._messages = this._messages.filter((x) => x.groupId !== groupId);
return this;
}
public toString(): string {
return this._messages.join('<br>');
// Put together in a flexbox
const divs = this._messages
.map((x) => x.body)
.join('');
return `
<div style="display: flex; flex-direction: column">
${divs}
</div>
`;
}
public static fromString(string: string): UIMessageBuilder {
public static fromString(groupId: string, string: string, style: OutputStyle): UIMessageBuilder {
const builder = new UIMessageBuilder();
builder.addItem(string);
builder.addItem(groupId, [string], style);
return builder;
}
public join(builder: UIMessageBuilder) {
this._messages.push(...builder._messages);
}
private _getStatusCSSClass(status?: OutputStyle) {
switch (status) {
case 'success':
return 'status-success';
case 'warning':
return 'status-warning';
case 'error':
return 'status-error';
}
}
}

View File

@ -29,7 +29,8 @@ export class WorkerClient {
ASSERT(this._loadedMesh !== undefined);
return {
buffers: BufferGenerator.fromMesh(this._loadedMesh)
buffers: BufferGenerator.fromMesh(this._loadedMesh),
dimensions: this._loadedMesh.getBounds().getDimensions(),
};
}
}

View File

@ -5,7 +5,7 @@ import { TFromWorkerMessage, TToWorkerMessage } from './worker_types';
export type TWorkerJob = {
id: string,
payload: TToWorkerMessage,
callback?: (payload: TFromWorkerMessage) => void, // Called only if the job is successful
callback?: (payload: TFromWorkerMessage) => void, // Called with the payload of the next message received by the worker
}
export class WorkerController {

View File

@ -5,6 +5,6 @@ addEventListener('message', (e) => {
// TODO: Remove
setTimeout(() => {
postMessage(workerInstance.doWork(e.data));
}, 2000);
}, 3000);
//postMessage(workerInstance.doWork(e.data));
});

View File

@ -1,6 +1,7 @@
import { TMeshBufferDescription } from "./buffer"
import { StatusMessage } from "./status"
import { AppError } from "./util/error_util"
import { Vector3 } from "./vector"
export namespace ImportParams {
export type Input = {
@ -19,6 +20,7 @@ export namespace RenderMeshParams {
export type Output = {
buffers: TMeshBufferDescription[],
dimensions: Vector3
}
}

View File

@ -314,21 +314,18 @@ select:disabled {
background: rgba(255, 255, 255, 0.2);
}
.border-warning {
border: 1.5px solid orange;
transition-duration: 1s;
.status-warning {
transition-duration: 0.5s;
color: orange;
}
.border-success {
border: 1.5px solid green;
transition-duration: 1s;
color: green;
.status-success {
transition-duration: 0.5s;
color: var(--prop-accent-standard);
}
.border-error {
border: 1.5px solid rgb(156, 27, 27);
transition-duration: 1s;
.status-error {
transition-duration: 0.5s;
color: rgb(156, 27, 27);
}
@ -553,4 +550,29 @@ svg {
100% {
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
}
}
.loader-circle {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: currentColor;
}
.spin {
animation: blinker 0.75s ease-in-out infinite;
}
@keyframes blinker {
0% {
opacity: 100%;
}
50% {
opacity: 50%;
}
100% {
opacity: 100%;
}
}