Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve UX #32

Merged
merged 4 commits into from
Dec 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 26 additions & 4 deletions HoloPrint.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export async function makePack(structureFiles, config = {}, resourcePackStack, p
let blockGeoMaker = await new BlockGeoMaker(config);
// makeBoneTemplate() is an impure function and adds texture references to the textureRefs set property.
let boneTemplatePalette = blockPalette.map(block => blockGeoMaker.makeBoneTemplate(block));
console.info("Finished making block geometry templates!");
console.log("Block geo maker:", blockGeoMaker);
console.log("Bone template palette:", structuredClone(boneTemplatePalette));

Expand Down Expand Up @@ -205,6 +206,7 @@ export async function makePack(structureFiles, config = {}, resourcePackStack, p
);
let entityDescription = entityFile["minecraft:client_entity"]["description"];

let totalBlockCount = 0;
let totalBlocksToValidateByStructure = [];
let uniqueBlocksToValidate = new Set();

Expand Down Expand Up @@ -334,6 +336,7 @@ export async function makePack(structureFiles, config = {}, resourcePackStack, p
uniqueBlocksToValidate.add(blockName);
}
firstBoneForThisBlock = false;
totalBlockCount++;
});
}
}
Expand Down Expand Up @@ -708,10 +711,29 @@ export async function makePack(structureFiles, config = {}, resourcePackStack, p
});
console.info(`Finished creating pack in ${(performance.now() - startTime).toFixed(0) / 1000}s!`);

if(totalMaterialCount < config.PREVIEW_BLOCK_LIMIT && previewCont) {
hologramGeo["minecraft:geometry"].filter(geo => geo["description"]["identifier"].startsWith("geometry.armor_stand.hologram_")).map(geo => {
(new PreviewRenderer(previewCont, textureAtlas, geo, hologramAnimations, config.SHOW_PREVIEW_SKYBOX)).catch(e => console.error("Preview renderer error:", e)); // is async but we won't wait for it
});
if(previewCont) {
let showPreview = () => {
hologramGeo["minecraft:geometry"].filter(geo => geo["description"]["identifier"].startsWith("geometry.armor_stand.hologram_")).map(geo => {
(new PreviewRenderer(previewCont, textureAtlas, geo, hologramAnimations, config.SHOW_PREVIEW_SKYBOX)).catch(e => console.error("Preview renderer error:", e)); // is async but we won't wait for it
});
};
if(totalBlockCount < config.PREVIEW_BLOCK_LIMIT) {
showPreview();
} else {
let message = document.createElement("div");
message.classList.add("previewMessage", "clickToView");
let p = document.createElement("p");
p.dataset.translationSubstitutions = JSON.stringify({
"{TOTAL_BLOCK_COUNT}": totalBlockCount
});
p.dataset.translate = "preview.click_to_view";
message.appendChild(p);
message.onEvent("click", () => {
message.remove();
showPreview();
});
previewCont.appendChild(message);
}
}

return new File([zippedPack], `${packName}.holoprint.mcpack`);
Expand Down
27 changes: 24 additions & 3 deletions PreviewRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,36 @@ export default class PreviewRenderer {
*/
constructor(cont, textureAtlas, geo, animations, showSkybox = true) {
return (async () => {
let loadingMessage = document.createElement("div");
loadingMessage.classList.add("previewMessage");
let p = document.createElement("p");
let span = document.createElement("span");
span.dataset.translate = "preview.loading";
p.appendChild(span);
let loader = document.createElement("div");
loader.classList.add("loader");
p.appendChild(loader);
loadingMessage.appendChild(p);
cont.appendChild(loadingMessage);

THREE ??= await import("https://esm.run/[email protected]"); // @bridge-editor/model-viewer uses this version :(
StandaloneModelViewer ??= (await import("https://esm.run/@bridge-editor/model-viewer")).StandaloneModelViewer;

let can = document.createElement("canvas");
(new MutationObserver(mutations => {
mutations.forEach(mutation => {
if(mutation.attributeName == "style") {
can.removeAttribute("style"); // @bridge-editor/model-viewer adds the style attribute at the start, and when the viewport is resized :(
}
});
})).observe(can, {
attributes: true
});
let imageBlob = textureAtlas.imageBlobs.at(-1)[1];
let imageUrl = URL.createObjectURL(imageBlob);
this.viewer = new StandaloneModelViewer(can, geo, imageUrl, {
width: window.innerWidth * 0.4,
height: window.innerWidth * 0.4,
width: window.innerWidth * 0.6,
height: window.innerWidth * 0.6,
antialias: true,
alpha: !showSkybox
});
Expand All @@ -37,7 +58,7 @@ export default class PreviewRenderer {
URL.revokeObjectURL(imageUrl);
this.viewer.positionCamera(1.7);
this.viewer.requestRendering();
cont.appendChild(can);
loadingMessage.replaceWith(can);
this.viewer.controls.minDistance = 10;
this.viewer.controls.maxDistance = 1000;

Expand Down
6 changes: 3 additions & 3 deletions SimpleLogger.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@
background: #334;
}
.warning > .logText::before {
content: "\26A0\FE0F";
content: "\26A0\FE0F ";
}
.error > .logText::before {
content: "\1F6A8";
content: "\1F6A8 ";
}
.info > .logText::before {
content: "\2139\FE0F";
content: "\2139\FE0F ";
}
.timestamp {
margin-right: 5px;
Expand Down
29 changes: 18 additions & 11 deletions SimpleLogger.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,6 @@ export default class SimpleLogger {
document.head.appendChild(l);
}
}
log(text) {
this.#genericLogWithClass(text);
}
warn(text) {
if(this.#genericLogWithClass(text, "warning")) {
this.#warningCountNode.innerText = `\u26A0\uFE0F${++this.#warningCount}`;
Expand All @@ -62,20 +59,26 @@ export default class SimpleLogger {
info(text) {
this.#genericLogWithClass(text, "info");
}
debug(text) {
this.#genericLogWithClass(text, "debug");
}
setOriginTime(originTime) {
this.#originTime = originTime;
}
#genericLogWithClass(text, logLevel) {
let stackTrace = getStackTrace().slice(2);
let currentURLOrigin = location.href.slice(0, location.href.lastIndexOf("/")); // location.origin is null on Firefox when on local files
if(stackTrace.some(loc => !loc.includes(currentURLOrigin))) {
return;
}
this.allLogs.push({
text,
level: logLevel,
stackTrace
});
let currentURLOrigin = location.href.slice(0, location.href.lastIndexOf("/")); // location.origin is null on Firefox when on local files
if(stackTrace.some(loc => !loc.includes(currentURLOrigin) && !loc.includes("<anonymous>"))) {
return;
}
if(logLevel == "debug") {
return;
}
let el = document.createElement("p");
el.classList.add("log");
if(logLevel) {
Expand Down Expand Up @@ -107,18 +110,22 @@ export default class SimpleLogger {
}

patchConsoleMethods() {
[console._warn, console._error, console._info] = [console.warn, console.error, console.info];
[console._warn, console._error, console._info, console._debug] = [console.warn, console.error, console.info, console.debug];
console.warn = (...args) => {
this.warn(...args);
this.warn(args.join(" "));
return console._warn.apply(console, [getStackTrace()[1].match(/\/([^\/]+\.[^\.]+:\d+:\d+)/)[1] + "\n", ...args]);
};
console.error = (...args) => {
this.error(...args);
this.error(args.join(" "));
return console._error.apply(console, [getStackTrace()[1].match(/\/([^\/]+\.[^\.]+:\d+:\d+)/)[1] + "\n", ...args]);
};
console.info = (...args) => {
this.info(...args);
this.info(args.join(" "));
return console._info.apply(console, [getStackTrace()[1].match(/\/([^\/]+\.[^\.]+:\d+:\d+)/)[1] + "\n", ...args]);
};
console.debug = (...args) => {
this.debug(args.join(" "));
return console._debug.apply(console, [getStackTrace()[1].match(/\/([^\/]+\.[^\.]+:\d+:\d+)/)[1] + "\n", ...args]);
};
}
}
12 changes: 10 additions & 2 deletions TextureAtlas.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default class TextureAtlas {

/**
* Creates a texture atlas for loading images from texture references and stitching them together.
* @param {Object} config
* @param {HoloprintConfig} config
* @param {ResourcePackStack} resourcePackStack
*/
constructor(config, resourcePackStack) {
Expand Down Expand Up @@ -333,7 +333,12 @@ export default class TextureAtlas {
imageData = this.#setImageDataOpacity(imageData, opacity);
}
let { width: imageW, height: imageH } = imageData;
let image = await imageData.toImage();
let image = await imageData.toImage().catch(e => {
console.error(`Failed to decode image data from ${texturePath}: ${e}`);
sourceUv = [0, 0];
uvSize = [1, 1];
return stringToImageData(`Failed to decode ${texturePath}`).toImage(); // hopefully it's an issue with the image loading not the decoding
});

if(this.#flipbookTexturesAndSizes.has(texturePath)) {
let size = this.#flipbookTexturesAndSizes.get(texturePath);
Expand Down Expand Up @@ -594,4 +599,7 @@ export default class TextureAtlas {
*/
/**
* @typedef {import("./HoloPrint.js").ImageFragment} ImageFragment
*/
/**
* @typedef {import("./HoloPrint.js").HoloPrintConfig} HoloprintConfig
*/
45 changes: 39 additions & 6 deletions essential.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,22 @@ Response.prototype.toImage = async function() {
let imageUrl = URL.createObjectURL(imageBlob);
let image = new Image();
image.src = imageUrl;
await image.decode();
try {
await image.decode();
} catch { // Chrome puts arbitrary limits on decoding images because "an error doesn't necessarily mean that the image was invalid": https://issues.chromium.org/issues/40676514 🤦‍♂️
return new Promise((res, rej) => { // possibly https://github.com/chromium/chromium/blob/874a0ba26635507d1e847600fd8a512f4a10e1f8/cc/tiles/gpu_image_decode_cache.cc#L91
let image2 = new Image();
image2.onEvent("load", () => {
URL.revokeObjectURL(imageUrl);
res(image2);
});
image2.onEvent("error", e => {
URL.revokeObjectURL(imageUrl);
rej(`Failed to load image from response with status ${this.status} from URL ${this.url}: ${e}`);
});
image2.src = imageUrl;
});
}
URL.revokeObjectURL(imageUrl);
return image;
};
Expand All @@ -70,7 +85,22 @@ ImageData.prototype.toImage = async function() {
let imageUrl = URL.createObjectURL(blob);
let image = new Image();
image.src = imageUrl;
await image.decode();
try {
await image.decode();
} catch {
return new Promise((res, rej) => {
let image2 = new Image();
image2.onEvent("load", () => {
URL.revokeObjectURL(imageUrl);
res(image2);
});
image2.onEvent("error", e => {
URL.revokeObjectURL(imageUrl);
rej(`Failed to decode ImageData with dimensions ${this.width}x${this.height}: ${e}`);
});
image2.src = imageUrl;
});
}
URL.revokeObjectURL(imageUrl);
return image;
};
Expand Down Expand Up @@ -143,15 +173,18 @@ export function stringToImageData(text, textCol = "black", backgroundCol = "whit
* Looks up a translation from translations/`language`.json
* @param {String} translationKey
* @param {String} language
* @returns {Promise<String>}
* @returns {Promise<String|undefined>}
*/
export async function translate(translationKey, language) {
translate[language] ??= await fetch(`translations/${language}.json`).then(res => res.jsonc()).catch(() => ({}));
translate[language] ??= await fetch(`translations/${language}.json`).then(res => res.jsonc()).catch(() => {
console.warn(`Failed to load language ${language} for translations!`);
return {};
});
return translate[language]?.[translationKey]?.replaceAll(/`([^`]+)`/g, "<code>$1</code>")?.replaceAll(/\[([^\]]+)\]\(([^\)]+)\)/g, `<a href="$2" target="_blank">$1</a>`);
}

export function getStackTrace() {
return (new Error()).stack.split("\n").slice(1).removeFalsies();
export function getStackTrace(e = new Error()) {
return e.stack.split("\n").slice(1).removeFalsies();
}

/**
Expand Down
Loading
Loading