@needle-tools/engine
Version:
Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.
238 lines (216 loc) • 11.2 kB
text/typescript
import { getParam } from "./engine_utils.js";
const showProgressLogs = getParam("debugprogress");
/** Gets the date formatted as 20240220-161993. When no Date is passed in, the current local date is used. */
export function getFormattedDate(date?: Date) {
date = date || new Date();
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours();
const min = date.getMinutes();
const sec = date.getSeconds();
const s_month = (month < 10 ? "0" : "") + month;
const s_day = (day < 10 ? "0" : "") + day;
const s_hour = (hour < 10 ? "0" : "") + hour;
const s_min = (min < 10 ? "0" : "") + min;
const s_sec = (sec < 10 ? "0" : "") + sec;
return date.getFullYear() + s_month + s_day + "-" + s_hour + s_min + s_sec;
}
declare type ProgressOptions = {
message?: string,
progress?: number,
autoStep?: boolean | number;
currentStep?: number,
totalSteps?: number
};
declare type ProgressStartOptions = {
/** This progress scope will be nested below parentScope */
parentScope?: string,
/** Callback with progress in 0..1 range. */
onProgress?: (progress: number) => void,
/** Log timings using console.time() and console.timeLog(). */
logTimings?: boolean,
};
/** Progress reporting utility.
* See `Progress.start` for usage examples.
*/
export class Progress {
/** Start a new progress reporting scope. Make sure to close it with Progress.end.
* @param scope The scope to start progress reporting for.
* @param options Parent scope, onProgress callback and logging. If only a string is provided, it's used as parentScope.
* @example
* // Manual usage:
* Progress.start("export-usdz", undefined, (progress) => console.log("Progress: " + progress));
* Progress.report("export-usdz", { message: "Exporting object 1", currentStep: 1, totalSteps: 3 });
* Progress.report("export-usdz", { message: "Exporting object 2", currentStep: 2, totalSteps: 3 });
* Progress.report("export-usdz", { message: "Exporting object 3", currentStep: 3, totalSteps: 3 });
*
* // Auto step usage:
* Progress.start("export-usdz", undefined, (progress) => console.log("Progress: " + progress));
* Progress.report("export-usdz", { message: "Exporting objects", autoStep: true, totalSteps: 3 });
* Progress.report("export-usdz", "Exporting object 1");
* Progress.report("export-usdz", "Exporting object 2");
* Progress.report("export-usdz", "Exporting object 3");
* Progress.end("export-usdz");
*
* // Auto step with weights:
* Progress.start("export-usdz", undefined, (progress) => console.log("Progress: " + progress));
* Progress.report("export-usdz", { message: "Exporting objects", autoStep: true, totalSteps: 10 });
* Progress.report("export-usdz", { message: "Exporting object 1", autoStep: 8 }); // will advance to 80% progress
* Progress.report("export-usdz", "Exporting object 2"); // 90%
* Progress.report("export-usdz", "Exporting object 3"); // 100%
*
* // Child scopes:
* Progress.start("export-usdz", undefined, (progress) => console.log("Progress: " + progress));
* Progress.report("export-usdz", { message: "Overall export", autoStep: true, totalSteps: 2 });
* Progress.start("export-usdz-objects", "export-usdz");
* Progress.report("export-usdz-objects", { message: "Exporting objects", autoStep: true, totalSteps: 3 });
* Progress.report("export-usdz-objects", "Exporting object 1");
* Progress.report("export-usdz-objects", "Exporting object 2");
* Progress.report("export-usdz-objects", "Exporting object 3");
* Progress.end("export-usdz-objects");
* Progress.report("export-usdz", "Exporting materials");
* Progress.end("export-usdz");
*
* // Enable console logging:
* Progress.start("export-usdz", { logTimings: true });
*/
static start(scope: string, options?: ProgressStartOptions | string) {
if (typeof options === "string") options = { parentScope: options };
const p = new ProgressEntry(scope, options);
progressCache.set(scope, p);
}
/** Report progress for a formerly started scope.
* @param scope The scope to report progress for.
* @param options Options for the progress report. If a string is passed, it will be used as the message.
* @example
* // auto step and show a message
* Progress.report("export-usdz", "Exporting object 1");
* // same as above
* Progress.report("export-usdz", { message: "Exporting object 1", autoStep: true });
* // show the current step and total steps and implicitly calculate progress as 10%
* Progress.report("export-usdz", { currentStep: 1, totalSteps: 10 });
* // enable auto step mode, following calls that have autoStep true will increase currentStep automatically.
* Progress.report("export-usdz", { totalSteps: 20, autoStep: true });
* // show the progress as 50%
* Progress.report("export-usdz", { progress: 0.5 });
* // give this step a weight of 20, which changes how progress is calculated. Useful for steps that take longer and/or have child scopes.
* Progress.report("export-usdz", { message. "Long process", autoStep: 20 });
* // show the current step and total steps and implicitly calculate progress as 10%
* Progress.report("export-usdz", { currentStep: 1, totalSteps: 10 });
*/
static report(scope: string, options?: ProgressOptions | string) {
const p = progressCache.get(scope);
if (!p) {
console.warn("Reporting progress for non-existing scope", scope);
return;
}
if (typeof options === "string") options = { message: options, autoStep: true };
p.report(options);
}
/** End a formerly started scope. This will also report the progress as 100%.
* @remarks Will warn if any child scope is still running (progress < 1).
*/
static end(scope: string) {
const p = progressCache.get(scope);
if (!p) return;
p.end();
progressCache.delete(scope);
}
}
const progressCache: Map<string, ProgressEntry> = new Map<string, ProgressEntry>();
/** Internal class that handles Progress instances and their parent/child relationship. */
class ProgressEntry {
private scopeLabel: string;
private parentScope?: ProgressEntry;
private childScopes: Array<ProgressEntry> = [];
private parentDepth = 0;
private lastStep? = 0;
private lastAutoStepWeight = 1;
private lastTotalSteps? = 0;
private onProgress?: (progress: number) => void;
private showLogs: boolean = false;
selfProgress: number = 0;
totalProgress: number = 0;
selfReports: number = 0;
totalReports: number = 0;
constructor(scope: string, options?: ProgressStartOptions) {
this.parentScope = options?.parentScope ? progressCache.get(options.parentScope) : undefined;
if (this.parentScope) {
this.parentScope.childScopes.push(this);
this.parentDepth = this.parentScope.parentDepth + 1;
}
this.scopeLabel = " ".repeat(this.parentDepth * 2) + scope;
this.showLogs = options?.logTimings ?? !!showProgressLogs;
if (this.showLogs) console.time(this.scopeLabel);
this.onProgress = options?.onProgress;
}
report(options?: ProgressOptions, indirect: boolean = false) {
if (options) {
if (options.totalSteps !== undefined)
this.lastTotalSteps = options.totalSteps;
if (options.currentStep !== undefined)
this.lastStep = options.currentStep;
if (options.autoStep !== undefined) {
if (options.currentStep === undefined) {
if (this.lastStep === undefined) this.lastStep = 0;
const stepIncrease = typeof options.autoStep === "number" ? options.autoStep : 1;
this.lastStep += this.lastAutoStepWeight;
this.lastAutoStepWeight = stepIncrease;
options.currentStep = this.lastStep;
}
options.totalSteps = this.lastTotalSteps;
}
if (options.progress !== undefined)
this.selfProgress = options.progress;
else if (options.currentStep !== undefined && options.totalSteps !== undefined) {
this.selfProgress = options.currentStep / options.totalSteps;
}
}
if (this.childScopes.length > 0) {
let avgChildProgress = 0;
let sumChildWeight = 0;
for (const c of this.childScopes)
{
avgChildProgress += c.selfProgress;
sumChildWeight += 1;
}
if (sumChildWeight > 0)
avgChildProgress /= sumChildWeight;
const stepWeight = this.lastAutoStepWeight / (this.lastTotalSteps ?? 1);
// not entirely sure about this formula – idea is that a step should be weighted by the progress of the children
this.totalProgress = this.selfProgress + avgChildProgress * stepWeight;
}
else {
this.totalProgress = this.selfProgress;
}
// sanitize values
this.selfProgress = Math.min(1, this.selfProgress);
this.totalProgress = Math.min(1, this.totalProgress);
let msg = (this.totalProgress * 100).toFixed(3) + "%"
if (this.childScopes.length > 0) msg += " (" + (this.selfProgress * 100).toFixed(3) + "% self)";
if (options?.message) msg = options.message + " – " + msg;
if (this.lastStep !== undefined && this.lastTotalSteps !== undefined)
msg = "Step " + (this.lastStep + (this.lastAutoStepWeight != 1 ? "–" + (this.lastStep + this.lastAutoStepWeight) : "") + "/" + this.lastTotalSteps) + " " + msg;
if (indirect) this.totalReports++;
else { this.selfReports++; this.totalReports++; }
if (this.showLogs) console.timeLog(this.scopeLabel, msg);
if (this.onProgress) this.onProgress(this.totalProgress);
if (this.parentScope) this.parentScope.report(undefined, true);
}
end() {
this.report({ progress: 1, autoStep: true }, true);
if (this.showLogs) {
console.timeLog(this.scopeLabel, "Total reports: " + this.totalReports, "Self reports: " + this.selfReports);
console.timeEnd(this.scopeLabel);
}
let anyRunningChildProgress = false;
for (const c of this.childScopes) {
if (c.selfProgress >= 1) continue;
anyRunningChildProgress = true;
break;
}
if (anyRunningChildProgress)
console.warn("Progress end with child scopes that are still running", this);
this.onProgress = undefined;
}
}