From 275e89a7a34c2ff7c52eb3824f3c1a61cade4763 Mon Sep 17 00:00:00 2001 From: Ewan Howell <48070995+ewanhowell5195@users.noreply.github.com> Date: Sun, 12 Jul 2026 02:41:28 +0100 Subject: [PATCH] CEM Template Loader v9.2.0 --- plugins.json | 2 +- .../cem_template_loader.js | 1245 +++++++++-------- plugins/cem_template_loader/changelog.json | 22 + 3 files changed, 698 insertions(+), 571 deletions(-) diff --git a/plugins.json b/plugins.json index 0d1212de6..f752d5a52 100644 --- a/plugins.json +++ b/plugins.json @@ -247,7 +247,7 @@ "author": "Ewan Howell", "description": "Load template Java Edition entity models for use with OptiFine CEM. Also includes an animation editor, so that you can create custom entity animations.", "tags": ["Minecraft: Java Edition", "OptiFine", "Templates"], - "version": "9.1.1", + "version": "9.2.0", "min_version": "5.0.0", "variant": "both", "creation_date": "2020-02-02", diff --git a/plugins/cem_template_loader/cem_template_loader.js b/plugins/cem_template_loader/cem_template_loader.js index eb92999d0..a3b32aaa9 100644 --- a/plugins/cem_template_loader/cem_template_loader.js +++ b/plugins/cem_template_loader/cem_template_loader.js @@ -41,7 +41,7 @@ author: "Ewan Howell", description: description + " Also includes an animation editor, so that you can create custom entity animations.", tags: ["Minecraft: Java Edition", "OptiFine", "Templates"], - version: "9.1.1", + version: "9.2.0", min_version: "5.0.0", variant: "both", creation_date: "2020-02-02", @@ -63,10 +63,10 @@ } async function fetchData(path, fallback) { - // if (path === "json/cem_template_models.json") { - // const fs = require("fs") - // return JSON.parse(fs.readFileSync("E:/Programming/GitHub/wynem/src/assets/json/cem_template_models.json")) - // } + if (isApp && path === "json/cem_template_models.json" && Plugins.installed.some(e => e.id === id && e.path === "E:\\Programming\\Javascript\\Blockbench\\cem_template_loader\\cem_template_loader.js")) { + const fs = require("fs") + return JSON.parse(fs.readFileSync("E:/Programming/GitHub/wynem/src/assets/json/cem_template_models.json", "utf8")) + } const currentRoot = connection.rootIndex try { const r = await fetch(`${root}/${path}`) @@ -107,7 +107,6 @@ loading: true, categories: {}, category: null, - loadTexture: true, entity: null, subentity: null, search: "", @@ -195,6 +194,211 @@ margin-bottom: auto !important; } } + #cem_template_loader > .dialog_wrapper { + grid-template-rows: auto 0px; + grid-template-columns: 160px auto; + overflow: hidden; + height: 512px; + display: grid; + + &:not(.has_sidebar) { + grid-template-columns: auto; + } + } + #cem_template_loader .dialog_sidebar_pages { + margin-bottom: 66px; + overflow-y: auto; + } + #cem_template_loader .dialog_sidebar_pages::-webkit-scrollbar-track, .cem-models::-webkit-scrollbar-track { + background-color: var(--color-back); + } + #cem_template_loader .dialog_content { + margin: 0; + max-height: initial; + } + #cem_template_loader h1 { + margin: 0; + } + #cem_template_loader .hidden { + display: none !important; + } + #cem-report-issues { + position: absolute; + width: 160px; + left: 0; + bottom: 0; + padding: 12px 0; + border-top: 2px solid var(--color-border); + z-index: 1; + display: none; + } + #cem-report-issues * { + cursor: pointer + } + .has_sidebar #cem-report-issues { + display: initial; + } + #cem-report-issues > a { + display: flex; + text-decoration: none; + justify-content: center; + align-items: center; + gap: 10px; + } + #cem-report-issues > a > div { + text-decoration: underline; + } + #cem-footer { + display: flex; + padding: 8px 8px 8px 16px; + gap: 10px; + flex-wrap: wrap; + } + #cem-buttons { + display: flex; + margin-left: auto; + gap: 4px; + } + #cem-buttons > :disabled { + opacity: 0.5; + cursor: not-allowed; + background: var(--color-button); + color: var(--color-text) !important; + } + #cem-buttons > :first-child:not(:disabled) { + background-color: var(--color-accent); + } + #cem-header { + padding: 0 8px 8px 16px; + } + #cem-details { + display: flex; + gap: 8px; + flex-wrap: wrap; + } + #cem-search { + position: relative; + min-width: min(100%, 256px); + display: flex; + justify-content: end; + margin-left: auto; + height: 30px; + + > input { + width: min(100%, 256px); + padding-right: 32px; + } + + > i { + position: absolute; + right: 6px; + top: 50%; + transform: translateY(-50%); + pointer-events: none; + + &.active { + cursor: pointer; + pointer-events: initial; + } + } + } + #cem-description { + min-width: min(100%, 256px); + flex: 1 1 0px; + } + .cem-models { + display: flex; + flex-wrap: wrap; + gap: 10px; + overflow-y: auto; + padding: 0 8px 0 16px; + margin-right: 8px; + scrollbar-width: initial; + scrollbar-color: initial; + } + .cem-model { + min-width: 128px; + border: 2px solid transparent; + flex: 1; + background-color: var(--color-back); + cursor: pointer; + padding: 2px; + display: flex; + flex-direction: column; + align-items: center; + position: relative; + + &.cem-variant { + background-color: color-mix(in srgb, var(--color-back), black 15%); + } + + &:hover { + background-color: var(--color-button); + } + + &.selected { + border: 2px solid var(--color-accent); + background-color: var(--color-selected); + } + + &.child-selected { + border: 2px solid transparent; + } + + > img { + height: 66px; + width: 100%; + object-fit: contain; + pointer-events: none; + } + + > div { + text-align: center; + flex: 1; + display: flex; + align-items: center; + max-height: 24px; + min-height: 24px; + line-height: 16px; + margin: -2px 0 2px; + pointer-events: none; + } + + > i { + position: absolute; + top: 4px; + right: 4px; + } + } + + .cem-spacer { + display: flex; + justify-content: center; + align-items: center; + flex: 1; + } + + .cem-model-heading { + min-width: 100%; + font-size: 24px; + } + + .cem-other-matches { + flex: 1; + align-self: center; + text-align: left; + font-size: 12px; + color: var(--color-subtle_text); + } + + .cem-other-matches a { + color: var(--color-accent); + cursor: pointer; + } + + .cem-other-matches a:hover { + text-decoration: underline; + } `) loader = new ModelLoader(id, { name, @@ -262,212 +466,12 @@ loaderDialog.content_vue.$refs.entry.focus() } }, - lines: [``], component: { data: loaderData, methods: { async load() { this.loading = true - await loadModel(this.subentity ?? this.entity, this.loadTexture) + await loadModel(this.subentity ?? this.entity) loaderDialog.close() }, reload() { @@ -494,13 +498,87 @@ this.subentity = null } }, - areAllNextItemsHidden(entities, heading) { - const index = entities.indexOf(heading) - for (let i = index + 1; i < entities.length; i++) { - if (entities[i].type === "heading") break - if (entities[i].id.includes(this.search)) return false + searchMatches(text) { + if (!text) return false + return this.search.split(/[\s_]+/).filter(Boolean).every(t => text.includes(t)) + }, + switchCategory(name) { + this.category = name + loaderDialog.sidebar.page = name + loaderDialog.sidebar.build() + this.$refs.entry.focus() + }, + matches(item) { + return this.searchMatches(entitySearchText(item)) + }, + displayName(item) { + return item.name ?? (item.file ?? item.id).replace(/_/g, " ") + }, + itemRank(item) { + const tokens = this.search.split(/[\s_]+/).filter(Boolean) + if (!tokens.length) return [0, 0] + const nameWords = this.displayName(item).toLowerCase().split(/\s+/) + const idWords = item.id.toLowerCase().split(/[\s_]+/) + let worstTier = 0 + let positionSum = 0 + for (const token of tokens) { + let best = null + const consider = (tier, position) => { + if (!best || tier < best[0] || (tier === best[0] && position < best[1])) best = [tier, position] + } + nameWords.forEach((word, i) => { + if (word.startsWith(token)) consider(0, i) + else if (word.includes(token)) consider(1, i) + }) + idWords.forEach((word, i) => { + if (word.startsWith(token)) consider(2, i) + else if (word.includes(token)) consider(3, i) + }) + if (!best) return null + worstTier = Math.max(worstTier, best[0]) + positionSum += best[1] + } + return [worstTier, positionSum] + }, + entityRank(model) { + const ranks = [this.itemRank(model), ...(model.variants ?? []).map(v => this.itemRank(v))].filter(Boolean) + if (!ranks.length) return null + ranks.sort((a, b) => a[0] - b[0] || a[1] - b[1]) + return ranks[0] + }, + categoryBlocks(c) { + const blocks = [] + let current = { heading: null, items: [] } + blocks.push(current) + for (const item of c.entities) { + if (item.type === "heading") { + current = { heading: item, items: [] } + blocks.push(current) + } else { + current.items.push(item) + } } - return true + if (this.search) { + for (const block of blocks) { + block.items = block.items.map(m => [m, this.entityRank(m)]).sort((a, b) => { + if (!a[1] || !b[1]) return (a[1] ? 0 : 1) - (b[1] ? 0 : 1) + return a[1][0] - b[1][0] || a[1][1] - b[1][1] || this.displayName(a[0]).localeCompare(this.displayName(b[0])) + }).map(e => e[0]) + } + } + return blocks.filter(b => b.heading || b.items.length) + }, + blockHidden(block) { + return !block.items.some(m => this.matches(m) || (m.variants ?? []).some(v => this.matches(v))) + } + }, + computed: { + otherCategoryMatches() { + if (!this.search) return [] + return Object.keys(this.categories).map(name => { + const count = name === this.category ? 0 : this.entities.filter(e => e[0] === name && this.searchMatches(e[1])).length + return { name, count } + }).filter(c => c.count).sort((a, b) => b.count - a.count) } }, template: ` @@ -526,16 +604,16 @@
-