From 60939ac5a22f0fa6fa21ca5346f659f2d6745c6c Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Sun, 31 May 2026 02:46:25 +0900 Subject: [PATCH 01/19] Add SB Worn Display Editor plugin Adds a Custom Slot row to the Display panel so users can edit custom item display keys (currently sophisticatedbackpacks:worn, the_four_primitives_and_weapons:back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots. Source: https://github.com/hrmcngs/sb-worn-display-blockbench --- plugins.json | 14 + plugins/sb_worn_display/LICENSE.MD | 21 ++ plugins/sb_worn_display/about.md | 48 +++ plugins/sb_worn_display/members.yml | 2 + plugins/sb_worn_display/sb_worn_display.js | 353 +++++++++++++++++++++ 5 files changed, 438 insertions(+) create mode 100644 plugins/sb_worn_display/LICENSE.MD create mode 100644 plugins/sb_worn_display/about.md create mode 100644 plugins/sb_worn_display/members.yml create mode 100644 plugins/sb_worn_display/sb_worn_display.js diff --git a/plugins.json b/plugins.json index a6f1651e..a43355df 100644 --- a/plugins.json +++ b/plugins.json @@ -1534,5 +1534,19 @@ "website": "https://svdex.moe", "min_version": "4.8.0", "has_changelog": true + }, + "sb_worn_display": { + "title": "SB Worn Display Editor", + "author": "hrmcngs", + "icon": "backpack", + "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.", + "tags": ["Minecraft: Java Edition", "Modeling"], + "version": "4.2.1", + "min_version": "4.8.0", + "variant": "both", + "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", + "repository": "https://github.com/hrmcngs/sb-worn-display-blockbench", + "bug_tracker": "https://github.com/hrmcngs/sb-worn-display-blockbench/issues", + "creation_date": "2026-05-31" } } diff --git a/plugins/sb_worn_display/LICENSE.MD b/plugins/sb_worn_display/LICENSE.MD new file mode 100644 index 00000000..49625d4c --- /dev/null +++ b/plugins/sb_worn_display/LICENSE.MD @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Hiromichi Nagase + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/sb_worn_display/about.md b/plugins/sb_worn_display/about.md new file mode 100644 index 00000000..ae926b0f --- /dev/null +++ b/plugins/sb_worn_display/about.md @@ -0,0 +1,48 @@ +# SB Worn Display Editor + +Adds a **Custom Slot** row to Blockbench's Display panel so you can edit custom item display keys defined by Forge mods — visually, in the 3D viewport, with the same sliders you use for the vanilla `head` / `gui` / `ground` slots. + +Built specifically for these keys, but trivially extensible to any custom key: + +| Key | Used by | +|---|---| +| `sophisticatedbackpacks:worn` | Sophisticated Backpacks — backpack worn on Curios "back" slot | +| `the_four_primitives_and_weapons:back` | MAW saya worn on Curios "back" slot | +| `the_four_primitives_and_weapons:belt` | MAW saya worn on Curios "belt" slot | + +## Why + +Blockbench ships with the 8 vanilla Java display contexts (`thirdperson_*`, `firstperson_*`, `head`, `gui`, `ground`, `fixed`). Custom display contexts defined by Forge mods via `ItemDisplayContext.create(...)` are preserved in the model JSON, but the default UI gives you no way to edit them visually — and worse, **values for unknown keys are silently dropped on save** because the exporter iterates `DisplayMode.slots`. + +This plugin: + +1. **Registers the custom keys** in `DisplayMode.slots` so values round-trip safely through save / reload. +2. **Adds a "Custom Slot" row** under Reference Model using Blockbench's own native UI markup (`panel_toolbar_label` + `bar tabs_small icon_bar` + `label.tool`) — visually identical in style to the vanilla Slot row. +3. Provides a fallback **numeric Edit dialog** under the Tools menu for typing exact values. + +## Usage + +1. Open a Java Item Model JSON +2. Switch to **Display** mode +3. In the right panel, find the new **Custom Slot** row under Reference Model +4. Click any of the three icons (backpack / ruler / belt) +5. Adjust rotation, translation, and scale with the standard sliders +6. Ctrl+S to save + +The plugin uses `head`'s camera angle and player reference model as a visual proxy. **Scale and rotation translate accurately** to in-game appearance. **Translation values** are pixel-relative offsets from the anchor point set by the mod's renderer (back / belt), which is not the head — so iterate by testing in-game for translation tuning. + +## Adding more keys + +Source the plugin from its [GitHub repo](https://github.com/hrmcngs/sb-worn-display-blockbench) and edit the `TARGETS` array at the top of `sb_worn_display.js`. + +## How it works + +The Display panel's Slot row in Blockbench's `DisplayModePanel.vue` is hardcoded (no `v-for` loop), so there is no official extension API. This plugin works around that by injecting a new section into the panel DOM and using a MutationObserver to keep it present after Vue re-renders. Click handlers reuse `DisplayMode.loadHead()` to set up the camera and reference model, then override `DisplayMode.slot` to the custom key. + +This approach is inherently fragile — a future Blockbench update that restructures `DisplayModePanel.vue` will break the injected row. The Tools menu Edit dialogs remain as a stable fallback. + +## Source + +- Repository: +- Issues: +- License: MIT diff --git a/plugins/sb_worn_display/members.yml b/plugins/sb_worn_display/members.yml new file mode 100644 index 00000000..39406e70 --- /dev/null +++ b/plugins/sb_worn_display/members.yml @@ -0,0 +1,2 @@ +maintainers: + - hrmcngs diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js new file mode 100644 index 00000000..f4930eef --- /dev/null +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -0,0 +1,353 @@ +/** + * SB Worn Display Editor — Custom Display Reference Editor for Blockbench + * + * Display パネル内、Reference Model の下に "Custom Slot" 行を追加し、 + * 本体の Slot 行と同じ見た目 (panel_toolbar_label + tabs_small icon_bar + + * label.tool + material-icons) でカスタム display key を選択・編集できる + * ようにする。スライダーは本体の Display パネルそのままを流用。 + * + * カスタム display key: + * - sophisticatedbackpacks:worn (SB の Curios back 装備時) + * - the_four_primitives_and_weapons:back (MAW saya の Curios back 装備時) + * - the_four_primitives_and_weapons:belt (MAW saya の Curios belt 装備時) + * + * 仕組み: + * 1. displayReferenceObjects.slots (= DisplayMode.slots) に key を push + * → JSON 保存/読込で必須 + * 2. Display パネルの DOM に Blockbench 標準書式の "Custom Slot" 行を注入 + * ─ label の class / icon_bar 構造は本体と同一なので追加 CSS 不要 + * ─ Reference Model の下に出るので折り返しに埋もれない + * 3. ボタン click 時は DisplayMode.loadHead() を踏み台にカメラ/Reference + * バーをセットアップ、その後 DisplayMode.slot をカスタムキーに上書き + * 4. MutationObserver で Vue 再レンダ時も自動再注入 + * + * Author: hrmcngs + * Source: https://github.com/hrmcngs/sb-worn-display-blockbench + * License: MIT + */ +(function () { + const PLUGIN_ID = 'sb_worn_display'; + + const TARGETS = [ + { + key: 'sophisticatedbackpacks:worn', + tooltip: 'SB Worn (背中・SB) — sophisticatedbackpacks:worn', + icon: 'backpack', + }, + { + key: 'the_four_primitives_and_weapons:back', + tooltip: 'MAW Saya Back (背中・MAW鞘) — the_four_primitives_and_weapons:back', + icon: 'straighten', + }, + { + key: 'the_four_primitives_and_weapons:belt', + tooltip: 'MAW Saya Belt (ベルト・MAW鞘) — the_four_primitives_and_weapons:belt', + icon: 'linear_scale', + }, + ]; + + const REF_BAR_ID = 'display_ref_bar'; + const CUSTOM_BAR_ID = 'sb_custom_display_bar'; + const CUSTOM_LABEL_ID = 'sb_custom_display_label'; + const INJECTED_ATTR = 'data-sb-custom-slot'; + + const actions = []; + let observer = null; + let modeListener = null; + + function safeId(key) { + return 'sbcd_' + key.replace(/[^a-z0-9]/gi, '_').toLowerCase(); + } + + function getProject() { + return (typeof Project !== 'undefined') ? Project : null; + } + + // ─── custom slot loader ──────────────────────────────────────────── + // DisplayMode.loadHead() を踏み台にカメラ/Reference バーを設定し + // その後 slot をカスタムキーに差し替える。 + function loadCustomSlot(target) { + const p = getProject(); + if (!p) { + Blockbench.showQuickMessage('モデルを開いてください', 1500); + return; + } + if (typeof DisplayMode === 'undefined' || !DisplayMode.loadHead) { + Blockbench.showQuickMessage('DisplayMode が利用できません', 1500); + return; + } + + if (!p.display_settings) p.display_settings = {}; + if (!p.display_settings[target.key]) { + p.display_settings[target.key] = new DisplaySlot(target.key); + } + + // Display モードに居ない場合は切替 + try { + if (typeof Modes !== 'undefined' && Modes.options && Modes.options.display && !Modes.display) { + Modes.options.display.select(); + } + } catch (e) { } + + try { + DisplayMode.loadHead(); + } catch (e) { + console.warn('[' + PLUGIN_ID + '] loadHead failed', e); + } + + DisplayMode.display_slot = target.key; + DisplayMode.slot = p.display_settings[target.key]; + if (DisplayMode.vue && DisplayMode.vue._data) { + DisplayMode.vue._data.slot = p.display_settings[target.key]; + } + + try { DisplayMode.updateDisplayBase(); } catch (e) { } + try { if (DisplayMode.vue && DisplayMode.vue.$forceUpdate) DisplayMode.vue.$forceUpdate(); } catch (e) { } + + // radio の checked 状態を同期 (Blockbench 本体の :checked ハイライトに乗る) + const radio = document.getElementById(safeId(target.key)); + if (radio) radio.checked = true; + } + + // ─── DOM injection: standard Blockbench slot-row format ──────────── + // 本体の Display パネル DisplayModePanel.vue と同じ書式: + //

+ //
+ // + // + // … + //
+ + function buildCustomBar() { + const label = document.createElement('p'); + label.id = CUSTOM_LABEL_ID; + label.className = 'panel_toolbar_label'; + label.setAttribute(INJECTED_ATTR, 'label'); + label.textContent = 'Custom Slot'; + + const bar = document.createElement('div'); + bar.id = CUSTOM_BAR_ID; + bar.className = 'bar tabs_small icon_bar'; + bar.setAttribute(INJECTED_ATTR, 'bar'); + + TARGETS.forEach((target) => { + const id = safeId(target.key); + + const input = document.createElement('input'); + input.type = 'radio'; + input.name = 'display'; + input.id = id; + input.className = 'hidden'; + input.setAttribute(INJECTED_ATTR, target.key); + + const tool = document.createElement('label'); + tool.className = 'tool'; + tool.htmlFor = id; + tool.setAttribute(INJECTED_ATTR, target.key); + + const tip = document.createElement('div'); + tip.className = 'tooltip'; + tip.textContent = target.tooltip; + tool.appendChild(tip); + + const icon = document.createElement('i'); + icon.className = 'material-icons'; + icon.textContent = target.icon; + tool.appendChild(icon); + + tool.addEventListener('click', () => loadCustomSlot(target)); + + bar.appendChild(input); + bar.appendChild(tool); + }); + + return { label, bar }; + } + + function injectCustomBar() { + if (document.getElementById(CUSTOM_BAR_ID)) return; + const refBar = document.getElementById(REF_BAR_ID); + if (!refBar || !refBar.parentNode) return; + const { label, bar } = buildCustomBar(); + // Reference Model の直後に label → bar の順で挿入 + refBar.parentNode.insertBefore(label, refBar.nextSibling); + label.parentNode.insertBefore(bar, label.nextSibling); + // 現在の display_slot がカスタムキーなら radio を checked に + try { + if (typeof DisplayMode !== 'undefined' && DisplayMode.display_slot) { + const radio = document.getElementById(safeId(DisplayMode.display_slot)); + if (radio) radio.checked = true; + } + } catch (e) { } + } + + function removeInjected() { + document.querySelectorAll('[' + INJECTED_ATTR + ']').forEach((el) => el.remove()); + } + + function setupObserver() { + if (observer) return; + observer = new MutationObserver(() => { + if (!document.getElementById(REF_BAR_ID)) return; + if (!document.getElementById(CUSTOM_BAR_ID)) injectCustomBar(); + }); + observer.observe(document.body, { childList: true, subtree: true }); + } + + function teardownObserver() { + if (observer) { + observer.disconnect(); + observer = null; + } + } + + // ─── Edit dialog (Tools menu, numeric direct entry) ──────────────── + + function getSlotValues(key) { + const p = getProject(); + const def = { rotation: [0, 0, 0], translation: [0, 0, 0], scale: [1, 1, 1] }; + if (!p || !p.display_settings) return def; + const s = p.display_settings[key]; + if (!s) return def; + return { + rotation: (s.rotation || [0, 0, 0]).slice(), + translation: (s.translation || [0, 0, 0]).slice(), + scale: (s.scale || [1, 1, 1]).slice(), + }; + } + + function setSlotValues(key, v) { + const p = getProject(); + if (!p) { + Blockbench.showQuickMessage('モデルを開いてください', 1500); + return false; + } + if (!p.display_settings) p.display_settings = {}; + if (!p.display_settings[key]) p.display_settings[key] = new DisplaySlot(key); + const slot = p.display_settings[key]; + slot.rotation = v.rotation.slice(); + slot.translation = v.translation.slice(); + slot.scale = v.scale.slice(); + if (p.saved !== undefined) p.saved = false; + try { DisplayMode.updateDisplayBase(); } catch (e) { } + return true; + } + + function openEditDialog(target) { + const cur = getSlotValues(target.key); + const dlg = new Dialog({ + id: 'edit_' + safeId(target.key), + title: 'Edit: ' + target.tooltip, + width: 480, + form: { + _info: { type: 'info', text: target.tooltip + '\n\n' + target.key }, + _div1: '_', + rotX: { label: 'Rotation X', type: 'number', value: cur.rotation[0], step: 1 }, + rotY: { label: 'Rotation Y', type: 'number', value: cur.rotation[1], step: 1 }, + rotZ: { label: 'Rotation Z', type: 'number', value: cur.rotation[2], step: 1 }, + _div2: '_', + transX: { label: 'Translation X', type: 'number', value: cur.translation[0], step: 0.1 }, + transY: { label: 'Translation Y', type: 'number', value: cur.translation[1], step: 0.1 }, + transZ: { label: 'Translation Z', type: 'number', value: cur.translation[2], step: 0.1 }, + _div3: '_', + scaleX: { label: 'Scale X', type: 'number', value: cur.scale[0], step: 0.05 }, + scaleY: { label: 'Scale Y', type: 'number', value: cur.scale[1], step: 0.05 }, + scaleZ: { label: 'Scale Z', type: 'number', value: cur.scale[2], step: 0.05 }, + }, + buttons: ['dialog.confirm', 'dialog.cancel'], + onConfirm(result) { + setSlotValues(target.key, { + rotation: [result.rotX, result.rotY, result.rotZ], + translation: [result.transX, result.transY, result.transZ], + scale: [result.scaleX, result.scaleY, result.scaleZ], + }); + Blockbench.showQuickMessage(target.tooltip.split(' — ')[0] + ' を更新しました (Ctrl+S で保存)', 2000); + dlg.hide(); + }, + }); + dlg.show(); + } + + // ─── DisplayMode.slots registration ──────────────────────────────── + // 保存/読込時に DisplayMode.slots に含まれる key だけが処理されるので + // ここで push しておかないと開き直したとき値が消える。 + + function registerSlotsInDisplayMode() { + if (typeof DisplayMode === 'undefined' || !Array.isArray(DisplayMode.slots)) return; + TARGETS.forEach((t) => { + if (!DisplayMode.slots.includes(t.key)) DisplayMode.slots.push(t.key); + }); + } + + function unregisterSlotsFromDisplayMode() { + if (typeof DisplayMode === 'undefined' || !Array.isArray(DisplayMode.slots)) return; + TARGETS.forEach((t) => { + const i = DisplayMode.slots.indexOf(t.key); + if (i >= 0) DisplayMode.slots.splice(i, 1); + }); + } + + // ─── plugin registration ─────────────────────────────────────────── + + Plugin.register(PLUGIN_ID, { + title: 'SB Worn Display Editor', + author: 'hrmcngs', + icon: 'backpack', + description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.', + tags: ['Minecraft: Java Edition', 'Modeling'], + version: '4.2.1', + min_version: '4.8.0', + variant: 'both', + website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', + repository: 'https://github.com/hrmcngs/sb-worn-display-blockbench', + bug_tracker: 'https://github.com/hrmcngs/sb-worn-display-blockbench/issues', + creation_date: '2026-05-31', + + onload() { + registerSlotsInDisplayMode(); + + // Tools メニューに Edit ダイアログ (fallback / 数値入力用) + TARGETS.forEach((target, idx) => { + const aEdit = new Action('custom_disp_edit_' + safeId(target.key), { + name: '[' + (idx + 1) + '] Edit (numbers): ' + target.tooltip.split(' — ')[0], + description: 'ダイアログで ' + target.key + ' を数値編集', + icon: 'tune', + category: 'edit', + click() { openEditDialog(target); }, + }); + try { MenuBar.addAction(aEdit, 'tools'); } catch (e) { } + actions.push(aEdit); + }); + + setupObserver(); + injectCustomBar(); + + try { + modeListener = () => setTimeout(injectCustomBar, 50); + Blockbench.on('select_mode', modeListener); + Blockbench.on('select_project', modeListener); + } catch (e) { } + + console.log('[' + PLUGIN_ID + '] v4.2.1 loaded — ' + + TARGETS.length + ' custom display slots available'); + }, + + onunload() { + teardownObserver(); + removeInjected(); + unregisterSlotsFromDisplayMode(); + try { + if (modeListener) { + Blockbench.removeListener('select_mode', modeListener); + Blockbench.removeListener('select_project', modeListener); + } + } catch (e) { } + modeListener = null; + actions.forEach((a) => { try { a.delete(); } catch (e) { } }); + actions.length = 0; + }, + }); +})(); From db76fcbc70b6b282233eb786aa7c09e49e82b751 Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Mon, 1 Jun 2026 20:48:43 +0900 Subject: [PATCH 02/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.2.2=20=E2=80=94?= =?UTF-8?q?=20fix=20viewport=20break=20on=20tab=20switch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a custom slot was active and the user switched between project tabs, DisplayMode.slot kept pointing at the previous project's slot object. updateDisplayBase then read stale data and the model rendered floating off-center with the reference model gone. Fix: on select_project, silently rebind DisplayMode.slot to the new project's display_settings[key] without resetting the camera or forcing display mode. --- plugins.json | 2 +- plugins/sb_worn_display/sb_worn_display.js | 59 ++++++++++++++++------ 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/plugins.json b/plugins.json index a43355df..ad29f3d3 100644 --- a/plugins.json +++ b/plugins.json @@ -1541,7 +1541,7 @@ "icon": "backpack", "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.2.1", + "version": "4.2.2", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index f4930eef..da1902a9 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -66,14 +66,19 @@ // ─── custom slot loader ──────────────────────────────────────────── // DisplayMode.loadHead() を踏み台にカメラ/Reference バーを設定し // その後 slot をカスタムキーに差し替える。 - function loadCustomSlot(target) { + function loadCustomSlot(target, options) { + // options.silent : true なら通知メッセージを出さない + // options.autoEnterDisplay : false なら Display モードへの自動切替を抑止 + // options.skipCameraReset : true なら DisplayMode.loadHead() をスキップ + // (タブ切替時に視点が暴れるのを防ぐ) + const opts = options || {}; const p = getProject(); if (!p) { - Blockbench.showQuickMessage('モデルを開いてください', 1500); + if (!opts.silent) Blockbench.showQuickMessage('モデルを開いてください', 1500); return; } if (typeof DisplayMode === 'undefined' || !DisplayMode.loadHead) { - Blockbench.showQuickMessage('DisplayMode が利用できません', 1500); + if (!opts.silent) Blockbench.showQuickMessage('DisplayMode が利用できません', 1500); return; } @@ -82,17 +87,20 @@ p.display_settings[target.key] = new DisplaySlot(target.key); } - // Display モードに居ない場合は切替 - try { - if (typeof Modes !== 'undefined' && Modes.options && Modes.options.display && !Modes.display) { - Modes.options.display.select(); - } - } catch (e) { } + if (opts.autoEnterDisplay !== false) { + try { + if (typeof Modes !== 'undefined' && Modes.options && Modes.options.display && !Modes.display) { + Modes.options.display.select(); + } + } catch (e) { } + } - try { - DisplayMode.loadHead(); - } catch (e) { - console.warn('[' + PLUGIN_ID + '] loadHead failed', e); + if (!opts.skipCameraReset) { + try { + DisplayMode.loadHead(); + } catch (e) { + console.warn('[' + PLUGIN_ID + '] loadHead failed', e); + } } DisplayMode.display_slot = target.key; @@ -109,6 +117,22 @@ if (radio) radio.checked = true; } + // タブ切替 (select_project) 用: 旧プロジェクトの slot オブジェクトを参照 + // したまま残ると DisplayMode.updateDisplayBase が古い値を読みに行き、 + // モデルが変な位置に飛ぶ。新プロジェクトの display_settings に再バインド + // するだけの軽量パス。Display モードに居なければ何もしない。 + function rebindActiveCustomSlot() { + if (typeof DisplayMode === 'undefined') return; + if (typeof Modes === 'undefined' || !Modes.display) return; + const target = TARGETS.find((t) => t.key === DisplayMode.display_slot); + if (!target) return; + loadCustomSlot(target, { + silent: true, + autoEnterDisplay: false, + skipCameraReset: true, + }); + } + // ─── DOM injection: standard Blockbench slot-row format ──────────── // 本体の Display パネル DisplayModePanel.vue と同じ書式: //

@@ -298,7 +322,7 @@ icon: 'backpack', description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.2.1', + version: '4.2.2', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', @@ -326,12 +350,15 @@ injectCustomBar(); try { - modeListener = () => setTimeout(injectCustomBar, 50); + modeListener = () => setTimeout(() => { + injectCustomBar(); + rebindActiveCustomSlot(); + }, 50); Blockbench.on('select_mode', modeListener); Blockbench.on('select_project', modeListener); } catch (e) { } - console.log('[' + PLUGIN_ID + '] v4.2.1 loaded — ' + console.log('[' + PLUGIN_ID + '] v4.2.2 loaded — ' + TARGETS.length + ' custom display slots available'); }, From b99874bdf38b010a25d21650c62c56244e6dc192 Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Tue, 2 Jun 2026 01:05:43 +0900 Subject: [PATCH 03/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.2.3=20=E2=80=94?= =?UTF-8?q?=20restore=20Reference=20Model=20on=20tab=20switch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v4.2.2 fixed the value-loss bug by skipping DisplayMode.loadHead() on project switch, but loadHead is also what re-populates the reference-model bar (player/zombie/armor_stand). Skipping it caused the reference figure to vanish after every tab switch. Now: call loadHead (so the reference bar is correctly re-populated), but save+restore the camera position/target around it so the user's viewpoint stays put. --- plugins.json | 2 +- plugins/sb_worn_display/sb_worn_display.js | 43 ++++++++++++++++++---- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/plugins.json b/plugins.json index ad29f3d3..ad1073fc 100644 --- a/plugins.json +++ b/plugins.json @@ -1541,7 +1541,7 @@ "icon": "backpack", "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.2.2", + "version": "4.2.3", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index da1902a9..c012c760 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -117,20 +117,49 @@ if (radio) radio.checked = true; } - // タブ切替 (select_project) 用: 旧プロジェクトの slot オブジェクトを参照 - // したまま残ると DisplayMode.updateDisplayBase が古い値を読みに行き、 - // モデルが変な位置に飛ぶ。新プロジェクトの display_settings に再バインド - // するだけの軽量パス。Display モードに居なければ何もしない。 + // タブ切替 (select_project) 用: 旧プロジェクトの slot オブジェクトを + // 参照したまま残ると updateDisplayBase が古い値を読みに行き、モデルが + // 変な位置に飛ぶ。新プロジェクトの display_settings に再バインドする。 + // + // 過去版 (v4.2.2) では loadHead をスキップしてカメラリセット回避をして + // いたが、loadHead は同時に Reference Model バーの再ポピュレートも + // やるため、スキップすると人型モデル (player 等) が消える副作用が出た。 + // 現実装: loadHead は呼ぶが、その前後でカメラ位置を save/restore して + // ユーザーの視点だけは維持する。 function rebindActiveCustomSlot() { if (typeof DisplayMode === 'undefined') return; if (typeof Modes === 'undefined' || !Modes.display) return; const target = TARGETS.find((t) => t.key === DisplayMode.display_slot); if (!target) return; + + // カメラ状態を一時退避 + let savedPos = null, savedTarget = null; + try { + if (typeof display_preview !== 'undefined' + && display_preview.camPers && display_preview.controls) { + savedPos = display_preview.camPers.position.toArray(); + savedTarget = display_preview.controls.target.toArray(); + } + } catch (e) { } + + // フル再セットアップ (slot + Vue + Reference Model バー) loadCustomSlot(target, { silent: true, autoEnterDisplay: false, - skipCameraReset: true, + skipCameraReset: false, }); + + // カメラ視点だけ復元 + try { + if (savedPos && savedTarget + && typeof display_preview !== 'undefined' + && display_preview.camPers && display_preview.controls) { + display_preview.camPers.position.fromArray(savedPos); + display_preview.controls.target.fromArray(savedTarget); + if (display_preview.controls.update) display_preview.controls.update(); + if (display_preview.render) display_preview.render(); + } + } catch (e) { } } // ─── DOM injection: standard Blockbench slot-row format ──────────── @@ -322,7 +351,7 @@ icon: 'backpack', description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.2.2', + version: '4.2.3', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', @@ -358,7 +387,7 @@ Blockbench.on('select_project', modeListener); } catch (e) { } - console.log('[' + PLUGIN_ID + '] v4.2.2 loaded — ' + console.log('[' + PLUGIN_ID + '] v4.2.3 loaded — ' + TARGETS.length + ' custom display slots available'); }, From f24014c24e3516ae25c6085b90f4f2eb86d61c9f Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Tue, 2 Jun 2026 12:48:07 +0900 Subject: [PATCH 04/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.3.0=20=E2=80=94?= =?UTF-8?q?=20import=20display=20values=20from=20another=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New Tools action "Import display values from another model…" opens a JSON/bbmodel file, parses its display section, and shows a dialog where the user picks source slot, target slot, and which fields to overwrite via 3 independent checkboxes (Rotation / Translation / Scale). Supports partial replace — e.g. copy only sizing from a sibling model without touching rotation. --- plugins.json | 2 +- plugins/sb_worn_display/sb_worn_display.js | 179 ++++++++++++++++++++- 2 files changed, 178 insertions(+), 3 deletions(-) diff --git a/plugins.json b/plugins.json index ad1073fc..8968218e 100644 --- a/plugins.json +++ b/plugins.json @@ -1541,7 +1541,7 @@ "icon": "backpack", "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.2.3", + "version": "4.3.0", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index c012c760..4df4218f 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -324,6 +324,168 @@ dlg.show(); } + // ─── Import display values from another model file ──────────────── + // 別のモデルファイル (.json / .bbmodel) を開き、その display 値を + // 現在のプロジェクトのカスタムスロットに取り込む。 + // ダイアログで「ソーススロット」「ターゲットスロット」「Rotation/ + // Translation/Scale のどれを取り込むか」を選択できるので、部分置換可。 + + function formatSlotPreview(slot) { + if (!slot) return '(empty)'; + const fmt = (a, def) => { + const arr = Array.isArray(a) ? a : def; + return '[' + arr.map((n) => { + const num = Number(n); + return isFinite(num) ? Number(num.toFixed(3)) : 0; + }).join(', ') + ']'; + }; + return [ + 'rotation: ' + fmt(slot.rotation, [0, 0, 0]), + 'translation: ' + fmt(slot.translation, [0, 0, 0]), + 'scale: ' + fmt(slot.scale, [1, 1, 1]), + ].join('\n'); + } + + function applyImport(sourceDisplay, result) { + const p = getProject(); + if (!p) { + Blockbench.showQuickMessage('モデルを開いてください', 1500); + return; + } + const src = sourceDisplay[result.sourceKey]; + if (!src) { + Blockbench.showQuickMessage('ソースに対象キー無し: ' + result.sourceKey, 1800); + return; + } + if (!p.display_settings) p.display_settings = {}; + if (!p.display_settings[result.targetKey]) { + p.display_settings[result.targetKey] = new DisplaySlot(result.targetKey); + } + const dst = p.display_settings[result.targetKey]; + const applied = []; + if (result.useRotation && Array.isArray(src.rotation)) { + dst.rotation = src.rotation.slice(); + applied.push('rotation'); + } + if (result.useTranslation && Array.isArray(src.translation)) { + dst.translation = src.translation.slice(); + applied.push('translation'); + } + if (result.useScale && Array.isArray(src.scale)) { + dst.scale = src.scale.slice(); + applied.push('scale'); + } + if (applied.length === 0) { + Blockbench.showQuickMessage('何もチェックされていません', 1500); + return; + } + if (p.saved !== undefined) p.saved = false; + try { DisplayMode.updateDisplayBase(); } catch (e) { } + try { if (DisplayMode.vue && DisplayMode.vue.$forceUpdate) DisplayMode.vue.$forceUpdate(); } catch (e) { } + Blockbench.showQuickMessage( + result.targetKey + ' ← ' + result.sourceKey + ' (' + applied.join(' + ') + ')', + 2800); + } + + function openImportDialog(sourceDisplay, sourceFileName) { + const sourceKeys = Object.keys(sourceDisplay); + const customKeys = TARGETS.map((t) => t.key); + + // Default target = 現在 Display パネルで選択中のカスタムキー (あれば) + let defaultTarget = customKeys[0]; + if (typeof DisplayMode !== 'undefined' + && customKeys.includes(DisplayMode.display_slot)) { + defaultTarget = DisplayMode.display_slot; + } + // Default source = ターゲットと同名キーがあればそれ、無ければ先頭 + let defaultSource = sourceKeys.includes(defaultTarget) ? defaultTarget : sourceKeys[0]; + + const sourceOptions = {}; + sourceKeys.forEach((k) => { sourceOptions[k] = k; }); + const targetOptions = {}; + customKeys.forEach((k) => { targetOptions[k] = k; }); + + const dlg = new Dialog({ + id: 'sb_import_display_dialog', + title: 'Import display values from ' + sourceFileName, + width: 560, + form: { + _src_info: { + type: 'info', + text: 'Source file: ' + sourceFileName + + '\nKeys with display data: ' + sourceKeys.join(', '), + }, + sourceKey: { + label: 'Source slot', + type: 'select', + options: sourceOptions, + value: defaultSource, + }, + targetKey: { + label: 'Target slot', + type: 'select', + options: targetOptions, + value: defaultTarget, + }, + _div1: '_', + _fields_label: { + type: 'info', + text: 'Replace which fields (checked fields will overwrite):', + }, + useRotation: { label: 'Rotation (X / Y / Z)', type: 'checkbox', value: true }, + useTranslation: { label: 'Translation (X / Y / Z)', type: 'checkbox', value: true }, + useScale: { label: 'Scale (X / Y / Z)', type: 'checkbox', value: true }, + _div2: '_', + _preview: { + type: 'info', + text: 'Source values preview (' + defaultSource + '):\n' + + formatSlotPreview(sourceDisplay[defaultSource]), + }, + }, + buttons: ['dialog.confirm', 'dialog.cancel'], + onConfirm(result) { + applyImport(sourceDisplay, result); + dlg.hide(); + }, + }); + dlg.show(); + } + + function importDisplayFromFile() { + if (typeof Blockbench === 'undefined' || !Blockbench.import) { + Blockbench.showQuickMessage('Blockbench.import が利用できません', 1500); + return; + } + Blockbench.import({ + resource_id: 'sb_import_display', + extensions: ['json', 'bbmodel'], + type: 'JSON / Blockbench model', + readtype: 'text', + }, function (files) { + if (!files || !files[0] || !files[0].content) return; + const file = files[0]; + let parsed; + try { parsed = JSON.parse(file.content); } + catch (e) { + Blockbench.showMessageBox({ + title: 'JSON parse error', + message: 'Failed to parse file:\n' + (e && e.message ? e.message : e), + }); + return; + } + const displayData = parsed.display || parsed.display_settings; + if (!displayData || typeof displayData !== 'object' + || Object.keys(displayData).length === 0) { + Blockbench.showMessageBox({ + title: 'No display data', + message: 'This file has no `display` section.', + }); + return; + } + openImportDialog(displayData, file.name || 'unknown'); + }); + } + // ─── DisplayMode.slots registration ──────────────────────────────── // 保存/読込時に DisplayMode.slots に含まれる key だけが処理されるので // ここで push しておかないと開き直したとき値が消える。 @@ -351,7 +513,7 @@ icon: 'backpack', description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.2.3', + version: '4.3.0', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', @@ -375,6 +537,19 @@ actions.push(aEdit); }); + // Tools メニューに Import アクション (別モデルから display 値を取り込み) + const aImport = new Action('custom_disp_import', { + name: 'Import display values from another model…', + description: '別の .json / .bbmodel ファイルを開いて display 値 ' + + '(Rotation / Translation / Scale) を取り込む。' + + 'ソース/ターゲットのスロットと取り込む項目をダイアログで選択。', + icon: 'file_download', + category: 'edit', + click() { importDisplayFromFile(); }, + }); + try { MenuBar.addAction(aImport, 'tools'); } catch (e) { } + actions.push(aImport); + setupObserver(); injectCustomBar(); @@ -387,7 +562,7 @@ Blockbench.on('select_project', modeListener); } catch (e) { } - console.log('[' + PLUGIN_ID + '] v4.2.3 loaded — ' + console.log('[' + PLUGIN_ID + '] v4.3.0 loaded — ' + TARGETS.length + ' custom display slots available'); }, From f6f480ab5862dda3aa6d44de5b79447a0d9c555f Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Tue, 2 Jun 2026 12:58:13 +0900 Subject: [PATCH 05/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.4.0=20=E2=80=94?= =?UTF-8?q?=20Center=20Model=20/=20Center=20View=20+=20outliner=20ctx=20me?= =?UTF-8?q?nu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new actions, available from both Tools menu and the outliner right-click context menu (Cube / Group / Mesh): - Center Model at Origin (Undo-able, modifies geometry): shifts every element + group origin so model bbox center is (0,0,0). Display rotation then orbits around the model center. - Center View on Selection (non-destructive): sets active preview's camera target to selection bbox center. Works in Edit and Display modes. --- plugins.json | 2 +- plugins/sb_worn_display/sb_worn_display.js | 255 ++++++++++++++++++++- 2 files changed, 254 insertions(+), 3 deletions(-) diff --git a/plugins.json b/plugins.json index 8968218e..343e698f 100644 --- a/plugins.json +++ b/plugins.json @@ -1541,7 +1541,7 @@ "icon": "backpack", "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.3.0", + "version": "4.4.0", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index 4df4218f..29a7c600 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -486,6 +486,219 @@ }); } + // ─── Center model at origin ─────────────────────────────────────── + // 全要素の bounding box 中心を (0,0,0) に揃える。Display モードの + // 回転はワールド原点を軸に回るので、モデルが原点に居る = 回転が + // モデル中心軸で行われる (= 実質的にピボットが中心になる)。 + // + // 対応する要素タイプ: + // - Cube : from / to / origin + // - Mesh : vertices (record) / origin + // - Locator/Null : position + // - Group : origin (グループ全体のピボット) + + function computeModelBBox() { + if (!Array.isArray(Project.elements) || Project.elements.length === 0) return null; + let minX = Infinity, minY = Infinity, minZ = Infinity; + let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity; + let any = false; + const expand = (x, y, z) => { + if (!isFinite(x) || !isFinite(y) || !isFinite(z)) return; + if (x < minX) minX = x; if (x > maxX) maxX = x; + if (y < minY) minY = y; if (y > maxY) maxY = y; + if (z < minZ) minZ = z; if (z > maxZ) maxZ = z; + any = true; + }; + Project.elements.forEach((el) => { + if (Array.isArray(el.from) && Array.isArray(el.to)) { + expand(el.from[0], el.from[1], el.from[2]); + expand(el.to[0], el.to[1], el.to[2]); + } + if (el.vertices && typeof el.vertices === 'object') { + Object.values(el.vertices).forEach((v) => { + if (Array.isArray(v) && v.length >= 3) expand(v[0], v[1], v[2]); + }); + } + if (Array.isArray(el.position) + && !Array.isArray(el.from) && !el.vertices) { + expand(el.position[0], el.position[1], el.position[2]); + } + }); + if (!any) return null; + return { + min: [minX, minY, minZ], + max: [maxX, maxY, maxZ], + center: [(minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2], + size: [maxX - minX, maxY - minY, maxZ - minZ], + }; + } + + function applyCenterModel(cx, cy, cz) { + const elements = Project.elements || []; + const groups = (typeof Group !== 'undefined' && Group.all) ? Group.all : []; + + try { + if (typeof Undo !== 'undefined' && Undo.initEdit) { + Undo.initEdit({ elements: elements, group: groups }); + } + } catch (e) { } + + const sub = (a) => [a[0] - cx, a[1] - cy, a[2] - cz]; + + elements.forEach((el) => { + if (Array.isArray(el.from)) el.from = sub(el.from); + if (Array.isArray(el.to)) el.to = sub(el.to); + if (Array.isArray(el.origin)) el.origin = sub(el.origin); + if (Array.isArray(el.position)) el.position = sub(el.position); + if (el.vertices && typeof el.vertices === 'object') { + Object.keys(el.vertices).forEach((k) => { + const v = el.vertices[k]; + if (Array.isArray(v) && v.length >= 3) { + el.vertices[k] = [v[0] - cx, v[1] - cy, v[2] - cz]; + } + }); + } + try { if (typeof el.preview_controller !== 'undefined' && el.preview_controller && el.preview_controller.updateGeometry) el.preview_controller.updateGeometry(el); } catch (e) { } + }); + + groups.forEach((g) => { + if (Array.isArray(g.origin)) g.origin = sub(g.origin); + }); + + try { Canvas.updateAll(); } catch (e) { } + try { if (typeof Canvas !== 'undefined' && Canvas.updateAllPositions) Canvas.updateAllPositions(); } catch (e) { } + + try { + if (typeof Undo !== 'undefined' && Undo.finishEdit) { + Undo.finishEdit('Center model at origin'); + } + } catch (e) { } + + if (Project && Project.saved !== undefined) Project.saved = false; + } + + // ─── Center view on selection (or all if none selected) ─────────── + // 選択中の要素 (無ければ全要素) の bbox 中心にカメラの注視点を合わせる。 + // 表示モードとエディットモードのどちらでも、現在アクティブな preview + // の controls.target を書き換える。ジオメトリは一切いじらない。 + + function computeBBoxOf(elements) { + if (!Array.isArray(elements) || elements.length === 0) return null; + let minX = Infinity, minY = Infinity, minZ = Infinity; + let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity; + let any = false; + const expand = (x, y, z) => { + if (!isFinite(x) || !isFinite(y) || !isFinite(z)) return; + if (x < minX) minX = x; if (x > maxX) maxX = x; + if (y < minY) minY = y; if (y > maxY) maxY = y; + if (z < minZ) minZ = z; if (z > maxZ) maxZ = z; + any = true; + }; + elements.forEach((el) => { + if (Array.isArray(el.from) && Array.isArray(el.to)) { + expand(el.from[0], el.from[1], el.from[2]); + expand(el.to[0], el.to[1], el.to[2]); + } else if (el.vertices && typeof el.vertices === 'object') { + Object.values(el.vertices).forEach((v) => { + if (Array.isArray(v) && v.length >= 3) expand(v[0], v[1], v[2]); + }); + } else if (Array.isArray(el.position)) { + expand(el.position[0], el.position[1], el.position[2]); + } else if (Array.isArray(el.origin)) { + expand(el.origin[0], el.origin[1], el.origin[2]); + } + }); + if (!any) return null; + return { + center: [(minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2], + size: [maxX - minX, maxY - minY, maxZ - minZ], + }; + } + + function getActivePreview() { + if (typeof Modes !== 'undefined' && Modes.display + && typeof display_preview !== 'undefined') { + return display_preview; + } + if (typeof Preview !== 'undefined' && Preview.selected) return Preview.selected; + if (typeof main_preview !== 'undefined') return main_preview; + if (typeof display_preview !== 'undefined') return display_preview; + return null; + } + + function centerViewOnSelection() { + const preview = getActivePreview(); + if (!preview || !preview.controls || !preview.controls.target) { + Blockbench.showQuickMessage('プレビューが見つかりません', 1500); + return; + } + // 選択優先、無ければグループ展開した全要素 + let elements = []; + if (typeof selected !== 'undefined' && Array.isArray(selected) && selected.length > 0) { + elements = selected.slice(); + } + // selected はメッシュ片や cube 個別なので、グループも処理対象に + if (typeof Group !== 'undefined' && Group.selected && elements.length === 0) { + elements = (Group.selected.children || []).slice(); + } + if (elements.length === 0) { + elements = (Project && Array.isArray(Project.elements)) ? Project.elements : []; + } + const bbox = computeBBoxOf(elements); + if (!bbox) { + Blockbench.showQuickMessage('bbox 計算可能な要素無し', 1500); + return; + } + const [cx, cy, cz] = bbox.center; + try { + preview.controls.target.set(cx, cy, cz); + if (preview.controls.update) preview.controls.update(); + if (preview.render) preview.render(); + } catch (e) { + console.warn('[' + PLUGIN_ID + '] centerView failed', e); + Blockbench.showQuickMessage('カメラ操作に失敗', 1500); + return; + } + Blockbench.showQuickMessage( + 'View 中心 → [' + cx.toFixed(2) + ', ' + cy.toFixed(2) + ', ' + cz.toFixed(2) + ']', + 1800); + } + + function centerModelAtOrigin() { + const p = getProject(); + if (!p) { + Blockbench.showQuickMessage('モデルを開いてください', 1500); + return; + } + const bbox = computeModelBBox(); + if (!bbox) { + Blockbench.showQuickMessage('要素が見つかりません', 1500); + return; + } + const [cx, cy, cz] = bbox.center; + if (Math.abs(cx) < 0.001 && Math.abs(cy) < 0.001 && Math.abs(cz) < 0.001) { + Blockbench.showQuickMessage('既に原点中心です', 1500); + return; + } + const fmt = (n) => (n >= 0 ? '+' : '') + n.toFixed(3); + Blockbench.showMessageBox({ + title: 'Center Model at Origin', + message: 'BBox 中心: [' + cx.toFixed(3) + ', ' + cy.toFixed(3) + ', ' + cz.toFixed(3) + ']\n' + + 'BBox サイズ: [' + bbox.size[0].toFixed(3) + ', ' + bbox.size[1].toFixed(3) + ', ' + bbox.size[2].toFixed(3) + ']\n\n' + + '全要素 (cube / mesh / locator) とグループ origin を以下だけ平行移動します:\n' + + ' [' + fmt(-cx) + ', ' + fmt(-cy) + ', ' + fmt(-cz) + ']\n\n' + + 'これでモデル中心が (0,0,0) に揃い、Display モードの Rotation が\n' + + 'モデル中心軸を中心に回るようになります。\n\n' + + '(ジオメトリを直接書き換えます。Ctrl+Z で取り消し可能)', + buttons: ['Apply', 'Cancel'], + }, function (btn) { + if (btn !== 0) return; + applyCenterModel(cx, cy, cz); + Blockbench.showQuickMessage( + 'モデル中心化完了: [' + fmt(-cx) + ', ' + fmt(-cy) + ', ' + fmt(-cz) + ']', 2500); + }); + } + // ─── DisplayMode.slots registration ──────────────────────────────── // 保存/読込時に DisplayMode.slots に含まれる key だけが処理されるので // ここで push しておかないと開き直したとき値が消える。 @@ -513,7 +726,7 @@ icon: 'backpack', description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.3.0', + version: '4.4.0', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', @@ -550,6 +763,43 @@ try { MenuBar.addAction(aImport, 'tools'); } catch (e) { } actions.push(aImport); + // Center Model アクション (ジオメトリを動かす破壊的操作・Undo 可) + const aCenter = new Action('custom_disp_center_model', { + name: 'Center Model at Origin', + description: 'モデル全要素 (cube / mesh / locator) の bounding box ' + + '中心を (0,0,0) に揃え、Display モードの Rotation がモデル ' + + '中心を軸に回るようにする。Ctrl+Z で取り消し可能。', + icon: 'center_focus_strong', + category: 'edit', + click() { centerModelAtOrigin(); }, + }); + try { MenuBar.addAction(aCenter, 'tools'); } catch (e) { } + actions.push(aCenter); + + // Center View アクション (カメラ注視点を選択要素中心へ移動・非破壊) + const aCenterView = new Action('custom_disp_center_view', { + name: 'Center View on Selection', + description: '選択要素 (無ければ全要素) の bbox 中心にカメラの ' + + '注視点を合わせる。ジオメトリは変更しない非破壊操作。', + icon: 'filter_center_focus', + category: 'view', + click() { centerViewOnSelection(); }, + }); + try { MenuBar.addAction(aCenterView, 'tools'); } catch (e) { } + actions.push(aCenterView); + + // Outliner コンテキストメニュー (Cube / Group / Mesh の右クリック) にも追加 + const ctxMenuTargets = []; + try { if (typeof Cube !== 'undefined' && Cube.prototype && Cube.prototype.menu) ctxMenuTargets.push(Cube.prototype.menu); } catch (e) { } + try { if (typeof Group !== 'undefined' && Group.prototype && Group.prototype.menu) ctxMenuTargets.push(Group.prototype.menu); } catch (e) { } + try { if (typeof Mesh !== 'undefined' && Mesh.prototype && Mesh.prototype.menu) ctxMenuTargets.push(Mesh.prototype.menu); } catch (e) { } + ctxMenuTargets.forEach((menu) => { + if (menu && typeof menu.addAction === 'function') { + try { menu.addAction(aCenterView); } catch (e) { } + try { menu.addAction(aCenter); } catch (e) { } + } + }); + setupObserver(); injectCustomBar(); @@ -562,7 +812,8 @@ Blockbench.on('select_project', modeListener); } catch (e) { } - console.log('[' + PLUGIN_ID + '] v4.3.0 loaded — ' + console.log('[' + PLUGIN_ID + '] v4.4.0 loaded — ' + + '(Center Model + Center View also in outliner context menu) — ' + TARGETS.length + ' custom display slots available'); }, From 497dc2a0ec62bcf0972c8131e57d0cc36faa4f6d Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Tue, 2 Jun 2026 13:10:19 +0900 Subject: [PATCH 06/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.5.0=20=E2=80=94?= =?UTF-8?q?=20fix=20Undo,=20add=20Center=20Pivot,=20native=20Center=20View?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - fix: Center Model at Origin's Undo entry was incomplete because Undo.initEdit was called with {group: [...]} which Blockbench doesn't recognize as an array. Switched to {outliner: true} which snapshots all group origins. Ctrl/Cmd+Z now reverts in one step. - add: Center Pivot of Groups — sets each group's origin to its children's bbox center, non-destructive. Tools menu + outliner right-click menu. Undo-able. - chg: Center View in outliner ctx menu now references Blockbench's built-in focus_on_selection action ('センタービュー' in JP) — 1:1 with View menu / preview ctx menu behavior. Custom impl removed. --- plugins.json | 2 +- plugins/sb_worn_display/sb_worn_display.js | 138 +++++++++++---------- 2 files changed, 74 insertions(+), 66 deletions(-) diff --git a/plugins.json b/plugins.json index 343e698f..fba17f1d 100644 --- a/plugins.json +++ b/plugins.json @@ -1541,7 +1541,7 @@ "icon": "backpack", "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.4.0", + "version": "4.5.0", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index 29a7c600..1028cb6d 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -537,11 +537,15 @@ const elements = Project.elements || []; const groups = (typeof Group !== 'undefined' && Group.all) ? Group.all : []; + // Undo: outliner: true でグループ origin の差分まで含めて snapshot を取る。 + // (v4.4.0 では group: groups 渡しで Undo が効かなかったので修正) try { if (typeof Undo !== 'undefined' && Undo.initEdit) { - Undo.initEdit({ elements: elements, group: groups }); + Undo.initEdit({ elements: elements, outliner: true }); } - } catch (e) { } + } catch (e) { + console.warn('[' + PLUGIN_ID + '] Undo.initEdit failed', e); + } const sub = (a) => [a[0] - cx, a[1] - cy, a[2] - cz]; @@ -558,7 +562,7 @@ } }); } - try { if (typeof el.preview_controller !== 'undefined' && el.preview_controller && el.preview_controller.updateGeometry) el.preview_controller.updateGeometry(el); } catch (e) { } + try { if (el.preview_controller && el.preview_controller.updateGeometry) el.preview_controller.updateGeometry(el); } catch (e) { } }); groups.forEach((g) => { @@ -572,15 +576,14 @@ if (typeof Undo !== 'undefined' && Undo.finishEdit) { Undo.finishEdit('Center model at origin'); } - } catch (e) { } + } catch (e) { + console.warn('[' + PLUGIN_ID + '] Undo.finishEdit failed', e); + } if (Project && Project.saved !== undefined) Project.saved = false; } - // ─── Center view on selection (or all if none selected) ─────────── - // 選択中の要素 (無ければ全要素) の bbox 中心にカメラの注視点を合わせる。 - // 表示モードとエディットモードのどちらでも、現在アクティブな preview - // の controls.target を書き換える。ジオメトリは一切いじらない。 + // ─── BBox helpers (shared by Center Model + Center Pivot) ──────── function computeBBoxOf(elements) { if (!Array.isArray(elements) || elements.length === 0) return null; @@ -604,8 +607,6 @@ }); } else if (Array.isArray(el.position)) { expand(el.position[0], el.position[1], el.position[2]); - } else if (Array.isArray(el.origin)) { - expand(el.origin[0], el.origin[1], el.origin[2]); } }); if (!any) return null; @@ -615,53 +616,52 @@ }; } - function getActivePreview() { - if (typeof Modes !== 'undefined' && Modes.display - && typeof display_preview !== 'undefined') { - return display_preview; - } - if (typeof Preview !== 'undefined' && Preview.selected) return Preview.selected; - if (typeof main_preview !== 'undefined') return main_preview; - if (typeof display_preview !== 'undefined') return display_preview; - return null; + // ─── Center pivot of groups (non-destructive) ───────────────────── + // 各グループの origin (ピボット) を、その子要素の bbox 中心に設定する。 + // モデル自体のジオメトリは移動しない。Blockbench 標準の "Center Pivot" + // と同じだが、Tools / outliner ctx menu からまとめて呼べるようにする。 + + function applyCenterPivots(targetGroups) { + if (!Array.isArray(targetGroups) || targetGroups.length === 0) return 0; + try { Undo.initEdit({ outliner: true, group: targetGroups }); } catch (e) { } + let count = 0; + targetGroups.forEach((g) => { + if (!g || !Array.isArray(g.children)) return; + const bbox = computeBBoxOf(g.children); + if (!bbox) return; + g.origin = bbox.center.slice(); + count++; + }); + try { Canvas.updateAll(); } catch (e) { } + try { Undo.finishEdit('Center pivot of groups'); } catch (e) { } + if (Project && Project.saved !== undefined && count > 0) Project.saved = false; + return count; } - function centerViewOnSelection() { - const preview = getActivePreview(); - if (!preview || !preview.controls || !preview.controls.target) { - Blockbench.showQuickMessage('プレビューが見つかりません', 1500); + function centerPivotOfGroups() { + const p = getProject(); + if (!p) { + Blockbench.showQuickMessage('モデルを開いてください', 1500); return; } - // 選択優先、無ければグループ展開した全要素 - let elements = []; - if (typeof selected !== 'undefined' && Array.isArray(selected) && selected.length > 0) { - elements = selected.slice(); - } - // selected はメッシュ片や cube 個別なので、グループも処理対象に - if (typeof Group !== 'undefined' && Group.selected && elements.length === 0) { - elements = (Group.selected.children || []).slice(); - } - if (elements.length === 0) { - elements = (Project && Array.isArray(Project.elements)) ? Project.elements : []; - } - const bbox = computeBBoxOf(elements); - if (!bbox) { - Blockbench.showQuickMessage('bbox 計算可能な要素無し', 1500); + if (typeof Group === 'undefined') { + Blockbench.showQuickMessage('Group API が利用できません', 1500); return; } - const [cx, cy, cz] = bbox.center; - try { - preview.controls.target.set(cx, cy, cz); - if (preview.controls.update) preview.controls.update(); - if (preview.render) preview.render(); - } catch (e) { - console.warn('[' + PLUGIN_ID + '] centerView failed', e); - Blockbench.showQuickMessage('カメラ操作に失敗', 1500); + // 選択中グループあればそれ、無ければ全グループ + let targets = []; + if (Group.selected) { + targets = [Group.selected]; + } else if (Array.isArray(Group.all)) { + targets = Group.all.slice(); + } + if (targets.length === 0) { + Blockbench.showQuickMessage('対象グループがありません', 1500); return; } + const count = applyCenterPivots(targets); Blockbench.showQuickMessage( - 'View 中心 → [' + cx.toFixed(2) + ', ' + cy.toFixed(2) + ', ' + cz.toFixed(2) + ']', - 1800); + count + ' グループのピボットを子 bbox 中心に設定しました', 2200); } function centerModelAtOrigin() { @@ -726,7 +726,7 @@ icon: 'backpack', description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.4.0', + version: '4.5.0', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', @@ -776,28 +776,36 @@ try { MenuBar.addAction(aCenter, 'tools'); } catch (e) { } actions.push(aCenter); - // Center View アクション (カメラ注視点を選択要素中心へ移動・非破壊) - const aCenterView = new Action('custom_disp_center_view', { - name: 'Center View on Selection', - description: '選択要素 (無ければ全要素) の bbox 中心にカメラの ' - + '注視点を合わせる。ジオメトリは変更しない非破壊操作。', - icon: 'filter_center_focus', - category: 'view', - click() { centerViewOnSelection(); }, + // Center Pivot of Groups アクション (各グループの origin を子 bbox 中心に・非破壊) + const aCenterPivot = new Action('custom_disp_center_pivots', { + name: 'Center Pivot of Groups', + description: '選択中のグループ (無ければ全グループ) の origin ' + + 'を子要素の bbox 中心に設定。モデルジオメトリは動かさず、' + + '回転ピボットだけモデル中心へ移す。Ctrl+Z で取り消し可能。', + icon: 'gps_fixed', + category: 'edit', + click() { centerPivotOfGroups(); }, }); - try { MenuBar.addAction(aCenterView, 'tools'); } catch (e) { } - actions.push(aCenterView); + try { MenuBar.addAction(aCenterPivot, 'tools'); } catch (e) { } + actions.push(aCenterPivot); - // Outliner コンテキストメニュー (Cube / Group / Mesh の右クリック) にも追加 + // Outliner コンテキストメニュー (Cube / Group / Mesh の右クリック) にも追加。 + // Center View は Blockbench 本体の "focus_on_selection" (= 'センタービュー') + // をそのまま参照するので自前実装はしない。 const ctxMenuTargets = []; try { if (typeof Cube !== 'undefined' && Cube.prototype && Cube.prototype.menu) ctxMenuTargets.push(Cube.prototype.menu); } catch (e) { } try { if (typeof Group !== 'undefined' && Group.prototype && Group.prototype.menu) ctxMenuTargets.push(Group.prototype.menu); } catch (e) { } try { if (typeof Mesh !== 'undefined' && Mesh.prototype && Mesh.prototype.menu) ctxMenuTargets.push(Mesh.prototype.menu); } catch (e) { } + + const builtInCenterView = (typeof BarItems !== 'undefined') ? BarItems.focus_on_selection : null; + ctxMenuTargets.forEach((menu) => { - if (menu && typeof menu.addAction === 'function') { - try { menu.addAction(aCenterView); } catch (e) { } - try { menu.addAction(aCenter); } catch (e) { } + if (!menu || typeof menu.addAction !== 'function') return; + if (builtInCenterView) { + try { menu.addAction(builtInCenterView); } catch (e) { } } + try { menu.addAction(aCenter); } catch (e) { } + try { menu.addAction(aCenterPivot); } catch (e) { } }); setupObserver(); @@ -812,8 +820,8 @@ Blockbench.on('select_project', modeListener); } catch (e) { } - console.log('[' + PLUGIN_ID + '] v4.4.0 loaded — ' - + '(Center Model + Center View also in outliner context menu) — ' + console.log('[' + PLUGIN_ID + '] v4.5.0 loaded — ' + + '(Center Model + Center Pivot + built-in Center View in outliner ctx menu) — ' + TARGETS.length + ' custom display slots available'); }, From ac4c11cea95782f1af87427ac6d3a6f55f44f714 Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Tue, 2 Jun 2026 19:19:07 +0900 Subject: [PATCH 07/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.6.0=20=E2=80=94?= =?UTF-8?q?=20true=20bulk-replace=20import=20+=20slider=20refresh=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Import dialog redesigned: one checkbox per TARGETS slot (auto-checked when source file contains the key) + shared Rotation / Translation / Scale checkboxes. One Confirm now overwrites every checked slot × every checked field. v4.3.0 only did one slot per Confirm. - fix: after import, if the active display_slot is among the replaced keys, rebind DisplayMode.slot + vue._data.slot + $forceUpdate so sliders show the new values immediately. --- plugins.json | 2 +- plugins/sb_worn_display/sb_worn_display.js | 189 ++++++++++++--------- 2 files changed, 107 insertions(+), 84 deletions(-) diff --git a/plugins.json b/plugins.json index fba17f1d..e447e4bd 100644 --- a/plugins.json +++ b/plugins.json @@ -1541,7 +1541,7 @@ "icon": "backpack", "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.5.0", + "version": "4.6.0", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index 1028cb6d..2dc8ef08 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -346,105 +346,127 @@ ].join('\n'); } - function applyImport(sourceDisplay, result) { + // 全 TARGETS をループしてチェック済み slot × チェック済み field を一括置換。 + // ソース側のキーは TARGETS と同名 (= match by key) を採用。 + // 編集中スロットが置換対象だった場合は Vue を再バインドして slider にも反映。 + function applyBulkImport(sourceDisplay, result) { const p = getProject(); if (!p) { Blockbench.showQuickMessage('モデルを開いてください', 1500); return; } - const src = sourceDisplay[result.sourceKey]; - if (!src) { - Blockbench.showQuickMessage('ソースに対象キー無し: ' + result.sourceKey, 1800); - return; - } if (!p.display_settings) p.display_settings = {}; - if (!p.display_settings[result.targetKey]) { - p.display_settings[result.targetKey] = new DisplaySlot(result.targetKey); - } - const dst = p.display_settings[result.targetKey]; - const applied = []; - if (result.useRotation && Array.isArray(src.rotation)) { - dst.rotation = src.rotation.slice(); - applied.push('rotation'); - } - if (result.useTranslation && Array.isArray(src.translation)) { - dst.translation = src.translation.slice(); - applied.push('translation'); - } - if (result.useScale && Array.isArray(src.scale)) { - dst.scale = src.scale.slice(); - applied.push('scale'); + + const useFields = { + rotation: !!result.useRotation, + translation: !!result.useTranslation, + scale: !!result.useScale, + }; + if (!useFields.rotation && !useFields.translation && !useFields.scale) { + Blockbench.showQuickMessage('置換するフィールドが選択されていません', 1800); + return; } - if (applied.length === 0) { - Blockbench.showQuickMessage('何もチェックされていません', 1500); + + const detail = []; + const replacedKeys = []; + + TARGETS.forEach((target) => { + const flag = result['slot_' + safeId(target.key)]; + if (!flag) return; + const src = sourceDisplay[target.key]; + if (!src) return; + if (!p.display_settings[target.key]) { + p.display_settings[target.key] = new DisplaySlot(target.key); + } + const dst = p.display_settings[target.key]; + const applied = []; + ['rotation', 'translation', 'scale'].forEach((f) => { + if (useFields[f] && Array.isArray(src[f])) { + dst[f] = src[f].slice(); + applied.push(f); + } + }); + if (applied.length > 0) { + detail.push(target.key + ' ← (' + applied.join(' + ') + ')'); + replacedKeys.push(target.key); + } + }); + + if (replacedKeys.length === 0) { + Blockbench.showQuickMessage('置換対象がありませんでした (ソースに該当キーが無いか、何もチェックされていない)', 2500); return; } + if (p.saved !== undefined) p.saved = false; + + // 編集中スロットが置換対象だった場合、Vue 側の slot 参照も同じオブジェクト + // に張り直して slider 値を即時更新する。 + try { + if (typeof DisplayMode !== 'undefined' && DisplayMode.display_slot + && replacedKeys.includes(DisplayMode.display_slot)) { + const active = p.display_settings[DisplayMode.display_slot]; + DisplayMode.slot = active; + if (DisplayMode.vue && DisplayMode.vue._data) { + DisplayMode.vue._data.slot = active; + } + if (DisplayMode.vue && DisplayMode.vue.$forceUpdate) DisplayMode.vue.$forceUpdate(); + } + } catch (e) { } try { DisplayMode.updateDisplayBase(); } catch (e) { } - try { if (DisplayMode.vue && DisplayMode.vue.$forceUpdate) DisplayMode.vue.$forceUpdate(); } catch (e) { } + Blockbench.showQuickMessage( - result.targetKey + ' ← ' + result.sourceKey + ' (' + applied.join(' + ') + ')', - 2800); + replacedKeys.length + ' slot 置換完了: ' + detail.join(' / '), + 3500); } function openImportDialog(sourceDisplay, sourceFileName) { const sourceKeys = Object.keys(sourceDisplay); - const customKeys = TARGETS.map((t) => t.key); - // Default target = 現在 Display パネルで選択中のカスタムキー (あれば) - let defaultTarget = customKeys[0]; - if (typeof DisplayMode !== 'undefined' - && customKeys.includes(DisplayMode.display_slot)) { - defaultTarget = DisplayMode.display_slot; - } - // Default source = ターゲットと同名キーがあればそれ、無ければ先頭 - let defaultSource = sourceKeys.includes(defaultTarget) ? defaultTarget : sourceKeys[0]; + // TARGETS 各キーごとのチェックボックス + ソース有無表示 + const slotFields = {}; + const slotInfoLines = []; + TARGETS.forEach((target) => { + const has = sourceKeys.includes(target.key); + const id = 'slot_' + safeId(target.key); + slotFields[id] = { + label: target.key + (has ? '' : ' — source に無し'), + type: 'checkbox', + value: has, // ソースに同名キーがあれば初期 ON + }; + slotInfoLines.push((has ? '✓ ' : '✗ ') + target.key + + (has ? ' — ' + formatSlotPreview(sourceDisplay[target.key]).split('\n').map(s => s.trim()).join(' / ') : '')); + }); - const sourceOptions = {}; - sourceKeys.forEach((k) => { sourceOptions[k] = k; }); - const targetOptions = {}; - customKeys.forEach((k) => { targetOptions[k] = k; }); + const formDef = { + _src_info: { + type: 'info', + text: 'Source: ' + sourceFileName + '\n\n' + + 'カスタムキー対応状況:\n ' + slotInfoLines.join('\n '), + }, + _div0: '_', + _fields_label: { + type: 'info', + text: '取り込むフィールド (全対象スロットに共通):', + }, + useRotation: { label: 'Rotation (X / Y / Z)', type: 'checkbox', value: true }, + useTranslation: { label: 'Translation (X / Y / Z)', type: 'checkbox', value: true }, + useScale: { label: 'Scale (X / Y / Z)', type: 'checkbox', value: true }, + _div1: '_', + _slots_label: { + type: 'info', + text: '置換するターゲットスロット (同名キーから自動取り込み):', + }, + }; + Object.assign(formDef, slotFields); const dlg = new Dialog({ id: 'sb_import_display_dialog', - title: 'Import display values from ' + sourceFileName, - width: 560, - form: { - _src_info: { - type: 'info', - text: 'Source file: ' + sourceFileName - + '\nKeys with display data: ' + sourceKeys.join(', '), - }, - sourceKey: { - label: 'Source slot', - type: 'select', - options: sourceOptions, - value: defaultSource, - }, - targetKey: { - label: 'Target slot', - type: 'select', - options: targetOptions, - value: defaultTarget, - }, - _div1: '_', - _fields_label: { - type: 'info', - text: 'Replace which fields (checked fields will overwrite):', - }, - useRotation: { label: 'Rotation (X / Y / Z)', type: 'checkbox', value: true }, - useTranslation: { label: 'Translation (X / Y / Z)', type: 'checkbox', value: true }, - useScale: { label: 'Scale (X / Y / Z)', type: 'checkbox', value: true }, - _div2: '_', - _preview: { - type: 'info', - text: 'Source values preview (' + defaultSource + '):\n' - + formatSlotPreview(sourceDisplay[defaultSource]), - }, - }, + title: 'Bulk import display values from ' + sourceFileName, + width: 600, + form: formDef, buttons: ['dialog.confirm', 'dialog.cancel'], onConfirm(result) { - applyImport(sourceDisplay, result); + applyBulkImport(sourceDisplay, result); dlg.hide(); }, }); @@ -726,7 +748,7 @@ icon: 'backpack', description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.5.0', + version: '4.6.0', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', @@ -752,10 +774,11 @@ // Tools メニューに Import アクション (別モデルから display 値を取り込み) const aImport = new Action('custom_disp_import', { - name: 'Import display values from another model…', - description: '別の .json / .bbmodel ファイルを開いて display 値 ' - + '(Rotation / Translation / Scale) を取り込む。' - + 'ソース/ターゲットのスロットと取り込む項目をダイアログで選択。', + name: 'Bulk import display values from another model…', + description: '別の .json / .bbmodel ファイルを開き、複数の ' + + 'カスタムスロット (SB Worn / MAW Back / MAW Belt) の display ' + + '値を一括置換する。スロットごとのチェックと取り込みフィールド ' + + '(Rotation / Translation / Scale) はダイアログで選択。', icon: 'file_download', category: 'edit', click() { importDisplayFromFile(); }, @@ -820,8 +843,8 @@ Blockbench.on('select_project', modeListener); } catch (e) { } - console.log('[' + PLUGIN_ID + '] v4.5.0 loaded — ' - + '(Center Model + Center Pivot + built-in Center View in outliner ctx menu) — ' + console.log('[' + PLUGIN_ID + '] v4.6.0 loaded — ' + + '(bulk import + Center Model + Center Pivot + built-in Center View) — ' + TARGETS.length + ' custom display slots available'); }, From 72552415d984b7a442edf0238c244ecf2a861bd5 Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Tue, 2 Jun 2026 19:47:18 +0900 Subject: [PATCH 08/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.6.1=20=E2=80=94?= =?UTF-8?q?=20import=20button=20in=20Custom=20Slot=20section=20header?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bulk-import was Tools-menu only. Add a download-icon button to the right of the "Custom Slot" label in the Display panel, styled like Blockbench's native .tool.head_right buttons (e.g. Reset arrow next to Rotation). Click opens the same dialog as the Tools menu action. --- plugins.json | 2 +- plugins/sb_worn_display/sb_worn_display.js | 36 +++++++++++++++++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/plugins.json b/plugins.json index e447e4bd..a8dc5199 100644 --- a/plugins.json +++ b/plugins.json @@ -1541,7 +1541,7 @@ "icon": "backpack", "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.6.0", + "version": "4.6.1", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index 2dc8ef08..9f3cf65f 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -175,11 +175,37 @@ // function buildCustomBar() { - const label = document.createElement('p'); + // 本体の "Rotation" 横にある reset アイコン (.tool.head_right) と同じ + // パターン:
で + // panel_toolbar_label と head_right ボタンを横並びにする。 + const label = document.createElement('div'); label.id = CUSTOM_LABEL_ID; - label.className = 'panel_toolbar_label'; + label.className = 'bar display_slot_section_bar'; label.setAttribute(INJECTED_ATTR, 'label'); - label.textContent = 'Custom Slot'; + + const labelText = document.createElement('p'); + labelText.className = 'panel_toolbar_label'; + labelText.textContent = 'Custom Slot'; + label.appendChild(labelText); + + // 別モデルから値をインポート (Tools メニューと同じ動線・ダイアログ) + const importBtn = document.createElement('div'); + importBtn.className = 'tool head_right'; + importBtn.setAttribute(INJECTED_ATTR, 'import-btn'); + const importTip = document.createElement('div'); + importTip.className = 'tooltip'; + importTip.textContent = '別モデルから display 値を一括 import'; + importBtn.appendChild(importTip); + const importIcon = document.createElement('i'); + importIcon.className = 'material-icons'; + importIcon.textContent = 'file_download'; + importBtn.appendChild(importIcon); + importBtn.addEventListener('click', (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + importDisplayFromFile(); + }); + label.appendChild(importBtn); const bar = document.createElement('div'); bar.id = CUSTOM_BAR_ID; @@ -748,7 +774,7 @@ icon: 'backpack', description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.6.0', + version: '4.6.1', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', @@ -843,7 +869,7 @@ Blockbench.on('select_project', modeListener); } catch (e) { } - console.log('[' + PLUGIN_ID + '] v4.6.0 loaded — ' + console.log('[' + PLUGIN_ID + '] v4.6.1 loaded — ' + '(bulk import + Center Model + Center Pivot + built-in Center View) — ' + TARGETS.length + ' custom display slots available'); }, From 62c31c9233e75c7ab3e3a837746069d5a88435fa Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Tue, 2 Jun 2026 19:53:42 +0900 Subject: [PATCH 09/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.7.0=20=E2=80=94?= =?UTF-8?q?=20true=20per-axis=20partial=20replace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 3 channel checkboxes (Rotation/Translation/Scale) split into 9 per-axis checkboxes. Each axis can be toggled independently — copy just Scale Y from another model without touching anything else, etc. Combined with the v4.6.0 per-slot checkboxes, the bulk-import dialog now exposes (3 slots × 9 axes) = 27 independent decisions per Confirm. Result message reports exactly which axes were written: 'sophisticatedbackpacks:worn ← rotation.XZ + scale.Y'. --- plugins.json | 2 +- plugins/sb_worn_display/sb_worn_display.js | 81 +++++++++++++++------- 2 files changed, 57 insertions(+), 26 deletions(-) diff --git a/plugins.json b/plugins.json index a8dc5199..5ce04087 100644 --- a/plugins.json +++ b/plugins.json @@ -1541,7 +1541,7 @@ "icon": "backpack", "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.6.1", + "version": "4.7.0", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index 9f3cf65f..1c2b31af 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -372,7 +372,9 @@ ].join('\n'); } - // 全 TARGETS をループしてチェック済み slot × チェック済み field を一括置換。 + // 全 TARGETS をループしてチェック済み slot × チェック済み axis を一括置換。 + // 軸単位 (rotation.X / rotation.Y / rotation.Z / translation.X / … / scale.Z) + // 計 9 軸を独立にチェックできる = 真の部分置換。 // ソース側のキーは TARGETS と同名 (= match by key) を採用。 // 編集中スロットが置換対象だった場合は Vue を再バインドして slider にも反映。 function applyBulkImport(sourceDisplay, result) { @@ -383,15 +385,22 @@ } if (!p.display_settings) p.display_settings = {}; - const useFields = { - rotation: !!result.useRotation, - translation: !!result.useTranslation, - scale: !!result.useScale, + // チャンネル × 軸の chosen フラグ + const channelAxisFlags = { + rotation: [!!result.useRotX, !!result.useRotY, !!result.useRotZ], + translation: [!!result.useTransX, !!result.useTransY, !!result.useTransZ], + scale: [!!result.useScaleX, !!result.useScaleY, !!result.useScaleZ], }; - if (!useFields.rotation && !useFields.translation && !useFields.scale) { - Blockbench.showQuickMessage('置換するフィールドが選択されていません', 1800); + const anyAxisChecked = Object.values(channelAxisFlags) + .some((arr) => arr.some(Boolean)); + if (!anyAxisChecked) { + Blockbench.showQuickMessage('置換する軸が1つも選択されていません', 1800); return; } + const channelDefaults = { + rotation: [0, 0, 0], translation: [0, 0, 0], scale: [1, 1, 1], + }; + const axisLetters = ['X', 'Y', 'Z']; const detail = []; const replacedKeys = []; @@ -405,15 +414,31 @@ p.display_settings[target.key] = new DisplaySlot(target.key); } const dst = p.display_settings[target.key]; - const applied = []; - ['rotation', 'translation', 'scale'].forEach((f) => { - if (useFields[f] && Array.isArray(src[f])) { - dst[f] = src[f].slice(); - applied.push(f); + const channelSummary = []; + + Object.keys(channelAxisFlags).forEach((channel) => { + const axisFlags = channelAxisFlags[channel]; + if (!axisFlags.some(Boolean)) return; // この channel 全 OFF + if (!Array.isArray(src[channel])) return; // source に値なし + if (!Array.isArray(dst[channel])) { + dst[channel] = channelDefaults[channel].slice(); + } + const next = dst[channel].slice(); + const writtenAxes = []; + for (let i = 0; i < 3; i++) { + if (axisFlags[i] && typeof src[channel][i] === 'number') { + next[i] = src[channel][i]; + writtenAxes.push(axisLetters[i]); + } + } + if (writtenAxes.length > 0) { + dst[channel] = next; // 配列差替で Vue 反応 + channelSummary.push(channel + '.' + writtenAxes.join('')); } }); - if (applied.length > 0) { - detail.push(target.key + ' ← (' + applied.join(' + ') + ')'); + + if (channelSummary.length > 0) { + detail.push(target.key + ' ← ' + channelSummary.join(' + ')); replacedKeys.push(target.key); } }); @@ -463,6 +488,7 @@ + (has ? ' — ' + formatSlotPreview(sourceDisplay[target.key]).split('\n').map(s => s.trim()).join(' / ') : '')); }); + // 9 軸チェックボックス (Rotation/Translation/Scale × X/Y/Z) で部分置換可 const formDef = { _src_info: { type: 'info', @@ -470,13 +496,18 @@ + 'カスタムキー対応状況:\n ' + slotInfoLines.join('\n '), }, _div0: '_', - _fields_label: { - type: 'info', - text: '取り込むフィールド (全対象スロットに共通):', - }, - useRotation: { label: 'Rotation (X / Y / Z)', type: 'checkbox', value: true }, - useTranslation: { label: 'Translation (X / Y / Z)', type: 'checkbox', value: true }, - useScale: { label: 'Scale (X / Y / Z)', type: 'checkbox', value: true }, + _rot_label: { type: 'info', text: '【Rotation】 取り込む軸:' }, + useRotX: { label: 'Rotation X', type: 'checkbox', value: true }, + useRotY: { label: 'Rotation Y', type: 'checkbox', value: true }, + useRotZ: { label: 'Rotation Z', type: 'checkbox', value: true }, + _trans_label: { type: 'info', text: '【Translation】 取り込む軸:' }, + useTransX: { label: 'Translation X', type: 'checkbox', value: true }, + useTransY: { label: 'Translation Y', type: 'checkbox', value: true }, + useTransZ: { label: 'Translation Z', type: 'checkbox', value: true }, + _scale_label: { type: 'info', text: '【Scale】 取り込む軸:' }, + useScaleX: { label: 'Scale X', type: 'checkbox', value: true }, + useScaleY: { label: 'Scale Y', type: 'checkbox', value: true }, + useScaleZ: { label: 'Scale Z', type: 'checkbox', value: true }, _div1: '_', _slots_label: { type: 'info', @@ -774,7 +805,7 @@ icon: 'backpack', description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.6.1', + version: '4.7.0', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', @@ -803,8 +834,8 @@ name: 'Bulk import display values from another model…', description: '別の .json / .bbmodel ファイルを開き、複数の ' + 'カスタムスロット (SB Worn / MAW Back / MAW Belt) の display ' - + '値を一括置換する。スロットごとのチェックと取り込みフィールド ' - + '(Rotation / Translation / Scale) はダイアログで選択。', + + '値を一括置換する。スロット × 9 軸 (Rotation/Translation/Scale ' + + '各 X/Y/Z) を個別にチェック可能 — 真の部分置換。', icon: 'file_download', category: 'edit', click() { importDisplayFromFile(); }, @@ -869,7 +900,7 @@ Blockbench.on('select_project', modeListener); } catch (e) { } - console.log('[' + PLUGIN_ID + '] v4.6.1 loaded — ' + console.log('[' + PLUGIN_ID + '] v4.7.0 loaded — ' + '(bulk import + Center Model + Center Pivot + built-in Center View) — ' + TARGETS.length + ' custom display slots available'); }, From 61e0a1f2a1f5bef7ca244aa69110580b207b738c Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Tue, 2 Jun 2026 20:29:25 +0900 Subject: [PATCH 10/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.8.0=20=E2=80=94?= =?UTF-8?q?=20bulk=20import=20now=20covers=20vanilla=20slots?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the bulk-import dialog only listed the 3 plugin-registered custom slots. Extended to ALL keys present in source.display: vanilla (head/gui/ground/firstperson_*/thirdperson_*/fixed/on_shelf/ embedded) + custom + any other namespaced keys. Keys ordered vanilla → custom → other; each tagged in the dialog. All default checked = still works as one-click "import everything". Per-axis 9-checkbox partial replace (v4.7.0) applies uniformly. --- plugins.json | 2 +- plugins/sb_worn_display/sb_worn_display.js | 71 +++++++++++++--------- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/plugins.json b/plugins.json index 5ce04087..b2eaaa22 100644 --- a/plugins.json +++ b/plugins.json @@ -1541,7 +1541,7 @@ "icon": "backpack", "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.7.0", + "version": "4.8.0", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index 1c2b31af..3f3ff91b 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -372,10 +372,9 @@ ].join('\n'); } - // 全 TARGETS をループしてチェック済み slot × チェック済み axis を一括置換。 - // 軸単位 (rotation.X / rotation.Y / rotation.Z / translation.X / … / scale.Z) - // 計 9 軸を独立にチェックできる = 真の部分置換。 - // ソース側のキーは TARGETS と同名 (= match by key) を採用。 + // ソースに存在する全 display キー (vanilla = gui/head/ground/firstperson_*/ + // thirdperson_*/fixed/on_shelf/embedded + custom = TARGETS の key) を + // チェック候補とし、チェック済み slot × チェック済み 9 軸を一括置換。 // 編集中スロットが置換対象だった場合は Vue を再バインドして slider にも反映。 function applyBulkImport(sourceDisplay, result) { const p = getProject(); @@ -405,15 +404,15 @@ const detail = []; const replacedKeys = []; - TARGETS.forEach((target) => { - const flag = result['slot_' + safeId(target.key)]; + Object.keys(sourceDisplay).forEach((key) => { + const flag = result['slot_' + safeId(key)]; if (!flag) return; - const src = sourceDisplay[target.key]; - if (!src) return; - if (!p.display_settings[target.key]) { - p.display_settings[target.key] = new DisplaySlot(target.key); + const src = sourceDisplay[key]; + if (!src || typeof src !== 'object') return; + if (!p.display_settings[key]) { + p.display_settings[key] = new DisplaySlot(key); } - const dst = p.display_settings[target.key]; + const dst = p.display_settings[key]; const channelSummary = []; Object.keys(channelAxisFlags).forEach((channel) => { @@ -438,8 +437,8 @@ }); if (channelSummary.length > 0) { - detail.push(target.key + ' ← ' + channelSummary.join(' + ')); - replacedKeys.push(target.key); + detail.push(key + ' ← ' + channelSummary.join(' + ')); + replacedKeys.push(key); } }); @@ -473,19 +472,34 @@ function openImportDialog(sourceDisplay, sourceFileName) { const sourceKeys = Object.keys(sourceDisplay); - // TARGETS 各キーごとのチェックボックス + ソース有無表示 + // vanilla を先 (Blockbench の Slot 行と同じ順序)、その後 custom (TARGETS)、 + // その他 (未知の名前空間など) は末尾。ソース内のキーすべてが対象候補。 + const VANILLA_ORDER = [ + 'thirdperson_righthand', 'thirdperson_lefthand', + 'firstperson_righthand', 'firstperson_lefthand', + 'head', 'gui', 'ground', 'fixed', 'on_shelf', 'embedded', + ]; + const customKeys = TARGETS.map((t) => t.key); + const orderedKeys = []; + VANILLA_ORDER.forEach((k) => { if (sourceKeys.includes(k)) orderedKeys.push(k); }); + customKeys.forEach((k) => { if (sourceKeys.includes(k) && !orderedKeys.includes(k)) orderedKeys.push(k); }); + sourceKeys.forEach((k) => { if (!orderedKeys.includes(k)) orderedKeys.push(k); }); + + // 各キーのチェックボックスを生成。デフォルト ON で「全部置換」をワンクリック化。 const slotFields = {}; const slotInfoLines = []; - TARGETS.forEach((target) => { - const has = sourceKeys.includes(target.key); - const id = 'slot_' + safeId(target.key); + orderedKeys.forEach((key) => { + const id = 'slot_' + safeId(key); + const isCustom = customKeys.includes(key); + const isVanilla = VANILLA_ORDER.includes(key); + const tag = isVanilla ? '(vanilla)' : (isCustom ? '(custom)' : '(other)'); slotFields[id] = { - label: target.key + (has ? '' : ' — source に無し'), + label: key + ' ' + tag, type: 'checkbox', - value: has, // ソースに同名キーがあれば初期 ON + value: true, }; - slotInfoLines.push((has ? '✓ ' : '✗ ') + target.key - + (has ? ' — ' + formatSlotPreview(sourceDisplay[target.key]).split('\n').map(s => s.trim()).join(' / ') : '')); + slotInfoLines.push('✓ ' + key + ' ' + tag + + ' — ' + formatSlotPreview(sourceDisplay[key]).split('\n').map((s) => s.trim()).join(' / ')); }); // 9 軸チェックボックス (Rotation/Translation/Scale × X/Y/Z) で部分置換可 @@ -511,7 +525,7 @@ _div1: '_', _slots_label: { type: 'info', - text: '置換するターゲットスロット (同名キーから自動取り込み):', + text: '置換するターゲットスロット (ソースに存在する vanilla + custom 全部):', }, }; Object.assign(formDef, slotFields); @@ -805,7 +819,7 @@ icon: 'backpack', description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.7.0', + version: '4.8.0', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', @@ -832,10 +846,11 @@ // Tools メニューに Import アクション (別モデルから display 値を取り込み) const aImport = new Action('custom_disp_import', { name: 'Bulk import display values from another model…', - description: '別の .json / .bbmodel ファイルを開き、複数の ' - + 'カスタムスロット (SB Worn / MAW Back / MAW Belt) の display ' - + '値を一括置換する。スロット × 9 軸 (Rotation/Translation/Scale ' - + '各 X/Y/Z) を個別にチェック可能 — 真の部分置換。', + description: '別の .json / .bbmodel ファイルを開き、ソース内の ' + + '全 display スロット (vanilla: head/gui/ground/firstperson_*/' + + 'thirdperson_*/fixed/on_shelf/embedded + custom: SB Worn / ' + + 'MAW Back / MAW Belt 等) を一括置換する。スロット × 9 軸 ' + + '(Rotation/Translation/Scale 各 X/Y/Z) を個別にチェック可能。', icon: 'file_download', category: 'edit', click() { importDisplayFromFile(); }, @@ -900,7 +915,7 @@ Blockbench.on('select_project', modeListener); } catch (e) { } - console.log('[' + PLUGIN_ID + '] v4.7.0 loaded — ' + console.log('[' + PLUGIN_ID + '] v4.8.0 loaded — ' + '(bulk import + Center Model + Center Pivot + built-in Center View) — ' + TARGETS.length + ' custom display slots available'); }, From 242e7866ab157fec699fc73143115af470540270 Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Wed, 3 Jun 2026 10:12:41 +0900 Subject: [PATCH 11/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.9.0=20=E2=80=94?= =?UTF-8?q?=20per-slot=20anchorY=20for=20preview=20position?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each TARGETS entry now has an optional anchorY: number that overrides where the model sits on the player reference in Display mode. loadHead() puts it at head height (~24); for a backpack or belt slot that's unnatural, so default to: SB Worn → 18 (chest) MAW Back → 18 (back/chest center) MAW Belt → 12 (waist) Applied right after loadHead() by writing the global display_area .position.y. Falls back to DisplayMode.display_area / display_base if the global isn't available. --- plugins.json | 2 +- plugins/sb_worn_display/sb_worn_display.js | 39 ++++++++++++++++++++-- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/plugins.json b/plugins.json index b2eaaa22..6bffd1c6 100644 --- a/plugins.json +++ b/plugins.json @@ -1541,7 +1541,7 @@ "icon": "backpack", "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.8.0", + "version": "4.9.0", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index 3f3ff91b..058513e1 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -28,21 +28,34 @@ (function () { const PLUGIN_ID = 'sb_worn_display'; + // anchorY: Display プレビューで co (display_area) を置く Y 座標。 + // loadHead() のデフォルトは y=28 (顔の中心) だが、custom slot ごとに + // 別のリファレンス点に動かしたい場合 anchorY で上書きする。 + // プレイヤーリファレンス (32px tall Steve) の基準: + // y=28: 顔の中心 + // y=24: 首・頭の付け根 (head bone pivot) + // y=18: 胸・腕の中心 + // y=12: 腰・ベルト位置 (body bone pivot in Minecraft) + // y= 8: 太もも + // y= 0: 足元 const TARGETS = [ { key: 'sophisticatedbackpacks:worn', tooltip: 'SB Worn (背中・SB) — sophisticatedbackpacks:worn', icon: 'backpack', + anchorY: 18, // 胸 (バックパック装着位置) }, { key: 'the_four_primitives_and_weapons:back', tooltip: 'MAW Saya Back (背中・MAW鞘) — the_four_primitives_and_weapons:back', icon: 'straighten', + anchorY: 18, // 胸〜背中の中心 }, { key: 'the_four_primitives_and_weapons:belt', tooltip: 'MAW Saya Belt (ベルト・MAW鞘) — the_four_primitives_and_weapons:belt', icon: 'linear_scale', + anchorY: 12, // 腰・ベルト位置 }, ]; @@ -110,6 +123,28 @@ } try { DisplayMode.updateDisplayBase(); } catch (e) { } + + // custom slot 固有の anchor Y を適用 (loadHead は y=24+ で「顔」基準に + // 置くが、ベルトや背中の slot ならもっと下の腰・胸あたりが基準として + // 自然なので、ここで display_area (= モデルが乗ってる Three.js Object3D) + // の Y を上書きする)。 + // display_area は Blockbench 本体ではモジュールスコープのグローバル変数 + // として定義されているので window.display_area から取る。念のため + // DisplayMode.display_area / DisplayMode.display_base もフォールバック。 + if (typeof target.anchorY === 'number') { + const da = (typeof display_area !== 'undefined') ? display_area + : (DisplayMode.display_area || DisplayMode.display_base || null); + if (da && da.position) { + try { + da.position.y = target.anchorY; + if (typeof da.updateMatrixWorld === 'function') da.updateMatrixWorld(); + if (typeof Transformer !== 'undefined' && Transformer.center) Transformer.center(); + } catch (e) { + console.warn('[' + PLUGIN_ID + '] anchorY apply failed', e); + } + } + } + try { if (DisplayMode.vue && DisplayMode.vue.$forceUpdate) DisplayMode.vue.$forceUpdate(); } catch (e) { } // radio の checked 状態を同期 (Blockbench 本体の :checked ハイライトに乗る) @@ -819,7 +854,7 @@ icon: 'backpack', description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.8.0', + version: '4.9.0', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', @@ -915,7 +950,7 @@ Blockbench.on('select_project', modeListener); } catch (e) { } - console.log('[' + PLUGIN_ID + '] v4.8.0 loaded — ' + console.log('[' + PLUGIN_ID + '] v4.9.0 loaded — ' + '(bulk import + Center Model + Center Pivot + built-in Center View) — ' + TARGETS.length + ' custom display slots available'); }, From 60c6f33b2e0a78b30fc2619a7d29dfa6c449abef Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Wed, 3 Jun 2026 13:44:12 +0900 Subject: [PATCH 12/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.9.1=20=E2=80=94?= =?UTF-8?q?=20rotate=20preview=20180=C2=B0=20on=20Z=20to=20match=20in-game?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Minecraft's LivingEntityRenderer applies scale(-1,-1,1) when drawing entity-attached items. Blockbench doesn't replicate this, so display values that look right in-game appeared rolled 180° in the preview. Z-axis 180° rotation = (x,y,z) → (-x,-y,z) = equivalent to that scale flip. Applied on display_area alongside the per-slot anchorY write so preview now matches in-game 1:1. --- plugins.json | 2 +- plugins/sb_worn_display/sb_worn_display.js | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/plugins.json b/plugins.json index 6bffd1c6..6fa26a91 100644 --- a/plugins.json +++ b/plugins.json @@ -1541,7 +1541,7 @@ "icon": "backpack", "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.9.0", + "version": "4.9.1", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index 058513e1..053a36fd 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -128,19 +128,26 @@ // 置くが、ベルトや背中の slot ならもっと下の腰・胸あたりが基準として // 自然なので、ここで display_area (= モデルが乗ってる Three.js Object3D) // の Y を上書きする)。 - // display_area は Blockbench 本体ではモジュールスコープのグローバル変数 - // として定義されているので window.display_area から取る。念のため - // DisplayMode.display_area / DisplayMode.display_base もフォールバック。 + // + // 同時に da.rotation.z = π (180°) を入れる。Minecraft の + // LivingEntityRenderer は entity 描画時に scale(-1, -1, 1) で X+Y を + // 反転させており、Blockbench はそれを適用していないため display 値が + // 同じでも見え方が点対称になる (= roll 180° ずれる)。Z軸 180° 回転は + // (x,y,z) → (-x,-y,z) の変換になり scale(-1,-1,1) と等価で、これで + // Blockbench プレビューが in-game と一致する。 if (typeof target.anchorY === 'number') { const da = (typeof display_area !== 'undefined') ? display_area : (DisplayMode.display_area || DisplayMode.display_base || null); if (da && da.position) { try { da.position.y = target.anchorY; + if (da.rotation) { + da.rotation.z = Math.PI; // = 180° (Minecraft の scale(-1,-1,1) 相殺) + } if (typeof da.updateMatrixWorld === 'function') da.updateMatrixWorld(); if (typeof Transformer !== 'undefined' && Transformer.center) Transformer.center(); } catch (e) { - console.warn('[' + PLUGIN_ID + '] anchorY apply failed', e); + console.warn('[' + PLUGIN_ID + '] anchorY/rotation apply failed', e); } } } @@ -854,7 +861,7 @@ icon: 'backpack', description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.9.0', + version: '4.9.1', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', From 4e02b83ca61a4b8810fbda79bb6f4ca5d842918c Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Wed, 3 Jun 2026 14:35:06 +0900 Subject: [PATCH 13/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.9.2=20=E2=80=94?= =?UTF-8?q?=20verify=20release.sh=20end-to-end=20after=20fixing=20eval/awk?= =?UTF-8?q?=20bugs=20(no=20plugin=20code=20change)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins.json | 2 +- plugins/sb_worn_display/sb_worn_display.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins.json b/plugins.json index 6fa26a91..8d28732e 100644 --- a/plugins.json +++ b/plugins.json @@ -1541,7 +1541,7 @@ "icon": "backpack", "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.9.1", + "version": "4.9.2", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index 053a36fd..358bd8c9 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -861,7 +861,7 @@ icon: 'backpack', description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.9.1', + version: '4.9.2', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', From 5d3e1367b6cbfedc81ce6d760aa1d6a86a7a1589 Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Thu, 4 Jun 2026 16:49:30 +0900 Subject: [PATCH 14/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.10.0=20?= =?UTF-8?q?=E2=80=94=20sync=20SB=20Worn=20and=20MAW=20Back=20via=20syncGro?= =?UTF-8?q?up=20(size=20/=20translation=20/=20rotation=20mirror)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins.json | 2 +- plugins/sb_worn_display/sb_worn_display.js | 142 ++++++++++++++++++++- 2 files changed, 142 insertions(+), 2 deletions(-) diff --git a/plugins.json b/plugins.json index 8d28732e..9203484d 100644 --- a/plugins.json +++ b/plugins.json @@ -1541,7 +1541,7 @@ "icon": "backpack", "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.9.2", + "version": "4.10.0", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index 358bd8c9..44d84f1c 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -38,24 +38,31 @@ // y=12: 腰・ベルト位置 (body bone pivot in Minecraft) // y= 8: 太もも // y= 0: 足元 + // syncGroup: 同じ値 (rotation / translation / scale) を共有させたいスロット + // を同じ文字列でグルーピングする。例えば 'back' を付けた slot は、 + // どれかが編集されると 200ms 以内に他の同 group メンバーに値が伝播する。 + // 背中装着の SB worn と MAW back saya は同じ位置なので同期させる。 const TARGETS = [ { key: 'sophisticatedbackpacks:worn', tooltip: 'SB Worn (背中・SB) — sophisticatedbackpacks:worn', icon: 'backpack', anchorY: 18, // 胸 (バックパック装着位置) + syncGroup: 'back', }, { key: 'the_four_primitives_and_weapons:back', tooltip: 'MAW Saya Back (背中・MAW鞘) — the_four_primitives_and_weapons:back', icon: 'straighten', anchorY: 18, // 胸〜背中の中心 + syncGroup: 'back', }, { key: 'the_four_primitives_and_weapons:belt', tooltip: 'MAW Saya Belt (ベルト・MAW鞘) — the_four_primitives_and_weapons:belt', icon: 'linear_scale', anchorY: 12, // 腰・ベルト位置 + // (belt は独立) }, ]; @@ -76,6 +83,120 @@ return (typeof Project !== 'undefined') ? Project : null; } + // ─── syncGroup: cross-slot value mirroring ───────────────────────── + // 同じ syncGroup を持つ TARGETS は rotation/translation/scale を共有する。 + // 編集中スロットを 200ms 周期で polling、変更があれば peer slot にコピー。 + // 切替時には「非デフォルトの値を持つ peer」を canonical として採用し、 + // 全 group メンバーに伝播してから polling 開始。 + + let syncTimer = null; + let syncLastSnap = null; + let syncActiveKey = null; + + function snapshotSlotData(slot) { + if (!slot) return ''; + return JSON.stringify({ + r: (slot.rotation || []).slice(), + t: (slot.translation || []).slice(), + s: (slot.scale || []).slice(), + }); + } + + function isDefaultSlot(slot) { + if (!slot) return true; + const eq = (a, def) => Array.isArray(a) && a.length === def.length + && a.every((v, i) => v === def[i]); + return eq(slot.rotation, [0, 0, 0]) + && eq(slot.translation, [0, 0, 0]) + && eq(slot.scale, [1, 1, 1]); + } + + function applySlotDataTo(key, source) { + if (!source) return; + const p = getProject(); + if (!p) return; + if (!p.display_settings) p.display_settings = {}; + if (!p.display_settings[key]) { + p.display_settings[key] = new DisplaySlot(key); + } + const dst = p.display_settings[key]; + if (Array.isArray(source.rotation)) dst.rotation = source.rotation.slice(); + if (Array.isArray(source.translation)) dst.translation = source.translation.slice(); + if (Array.isArray(source.scale)) dst.scale = source.scale.slice(); + } + + function propagateToSyncPeers(target) { + if (!target || !target.syncGroup) return; + const p = getProject(); + if (!p || !p.display_settings) return; + const src = p.display_settings[target.key]; + if (!src) return; + TARGETS + .filter((t) => t.syncGroup === target.syncGroup && t.key !== target.key) + .forEach((peer) => applySlotDataTo(peer.key, src)); + } + + // syncGroup 内で「canonical」(基準にすべき) slot を選ぶ: + // 1. 引数 preferKey の slot が非デフォルト値を持つならそれ + // 2. 同 group の他 peer のうち非デフォルト値を持つ最初のもの + // 3. 全部デフォルトなら preferKey + function pickCanonicalInGroup(syncGroup, preferKey) { + const p = getProject(); + if (!p || !p.display_settings) return preferKey; + const peers = TARGETS.filter((t) => t.syncGroup === syncGroup); + const preferred = peers.find((t) => t.key === preferKey); + if (preferred) { + const d = p.display_settings[preferred.key]; + if (d && !isDefaultSlot(d)) return preferred.key; + } + for (const t of peers) { + const d = p.display_settings[t.key]; + if (d && !isDefaultSlot(d)) return t.key; + } + return preferKey; + } + + function stopSync() { + if (syncTimer) { + clearInterval(syncTimer); + syncTimer = null; + } + syncLastSnap = null; + syncActiveKey = null; + } + + function startSyncFor(target) { + stopSync(); + if (!target || !target.syncGroup) return; + const peers = TARGETS.filter((t) => t.syncGroup === target.syncGroup && t.key !== target.key); + if (peers.length === 0) return; + syncActiveKey = target.key; + const p = getProject(); + if (!p || !p.display_settings) return; + syncLastSnap = snapshotSlotData(p.display_settings[target.key]); + + syncTimer = setInterval(() => { + try { + // アクティブな slot が他に切り替わってたら polling 終了 + if (typeof DisplayMode === 'undefined' + || DisplayMode.display_slot !== syncActiveKey) { + stopSync(); + return; + } + const cp = getProject(); + if (!cp || !cp.display_settings) return; + const cur = cp.display_settings[syncActiveKey]; + if (!cur) return; + const snap = snapshotSlotData(cur); + if (snap === syncLastSnap) return; + syncLastSnap = snap; + propagateToSyncPeers(target); + } catch (e) { + console.warn('[' + PLUGIN_ID + '] sync poll failed', e); + } + }, 200); + } + // ─── custom slot loader ──────────────────────────────────────────── // DisplayMode.loadHead() を踏み台にカメラ/Reference バーを設定し // その後 slot をカスタムキーに差し替える。 @@ -152,6 +273,24 @@ } } + // syncGroup 設定があれば、まず canonical を pick して値を揃え、 + // その後 polling 開始 (アクティブ slot の編集が peer に伝播する)。 + if (target.syncGroup) { + const canonicalKey = pickCanonicalInGroup(target.syncGroup, target.key); + if (canonicalKey && canonicalKey !== target.key && p.display_settings[canonicalKey]) { + applySlotDataTo(target.key, p.display_settings[canonicalKey]); + DisplayMode.slot = p.display_settings[target.key]; + if (DisplayMode.vue && DisplayMode.vue._data) { + DisplayMode.vue._data.slot = p.display_settings[target.key]; + } + try { DisplayMode.updateDisplayBase(); } catch (e) { } + } + propagateToSyncPeers(target); + startSyncFor(target); + } else { + stopSync(); + } + try { if (DisplayMode.vue && DisplayMode.vue.$forceUpdate) DisplayMode.vue.$forceUpdate(); } catch (e) { } // radio の checked 状態を同期 (Blockbench 本体の :checked ハイライトに乗る) @@ -861,7 +1000,7 @@ icon: 'backpack', description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.9.2', + version: '4.10.0', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', @@ -963,6 +1102,7 @@ }, onunload() { + stopSync(); teardownObserver(); removeInjected(); unregisterSlotsFromDisplayMode(); From 052f22c985de731dc2998db996445a3cf4f8b18f Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Thu, 4 Jun 2026 17:07:58 +0900 Subject: [PATCH 15/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.11.0=20?= =?UTF-8?q?=E2=80=94=20add=20backpack=5Farsenal:chestplate=20target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New custom display key for Backpack-Arsenal's chestplate-style worn rendering. anchorY=18 (chest), syncGroup='back' so values mirror SB Worn and MAW Back. plugins.json description updated to match the JS manifest. --- plugins.json | 4 ++-- plugins/sb_worn_display/sb_worn_display.js | 25 +++++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/plugins.json b/plugins.json index 9203484d..d9d36bab 100644 --- a/plugins.json +++ b/plugins.json @@ -1539,9 +1539,9 @@ "title": "SB Worn Display Editor", "author": "hrmcngs", "icon": "backpack", - "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.", + "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt, Backpack-Arsenal chestplate) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.10.0", + "version": "4.11.0", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index 44d84f1c..53338117 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -7,9 +7,10 @@ * ようにする。スライダーは本体の Display パネルそのままを流用。 * * カスタム display key: - * - sophisticatedbackpacks:worn (SB の Curios back 装備時) - * - the_four_primitives_and_weapons:back (MAW saya の Curios back 装備時) - * - the_four_primitives_and_weapons:belt (MAW saya の Curios belt 装備時) + * - sophisticatedbackpacks:worn (SB の Curios back 装備時) + * - the_four_primitives_and_weapons:back (MAW saya の Curios back 装備時) + * - the_four_primitives_and_weapons:belt (MAW saya の Curios belt 装備時) + * - backpack_arsenal:chestplate (Backpack-Arsenal の胸甲スタイル装着時) * * 仕組み: * 1. displayReferenceObjects.slots (= DisplayMode.slots) に key を push @@ -64,6 +65,20 @@ anchorY: 12, // 腰・ベルト位置 // (belt は独立) }, + { + // Backpack-Arsenal のカスタムバックパックを「胸甲 (chestplate) スタイル」で + // 装着・描画する用の display context。mod 側で + // ItemDisplayContext.create("backpack_arsenal:chestplate", ...) + // を登録し、armor / curios chest スロット描画時にこの context を指定して + // baked model の applyTransform を呼べば、ここで編集した値が反映される。 + // syncGroup='back' にしてあるので、sb worn / MAW back と同じ値を共有する + // (背中側の rotation/translation/scale を流用するため)。 + key: 'backpack_arsenal:chestplate', + tooltip: 'Backpack Arsenal Chestplate (胸甲・カスタムバックパック) — backpack_arsenal:chestplate', + icon: 'shield', + anchorY: 18, // 胸 — 体幹中央 + syncGroup: 'back', + }, ]; const REF_BAR_ID = 'display_ref_bar'; @@ -998,9 +1013,9 @@ title: 'SB Worn Display Editor', author: 'hrmcngs', icon: 'backpack', - description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt) visually in the 3D viewport, using the same sliders as the vanilla slots.', + description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt, Backpack-Arsenal chestplate) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.10.0', + version: '4.11.0', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', From 290d0cb821af17be4a3c93e761118053de0affd4 Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Thu, 4 Jun 2026 22:19:38 +0900 Subject: [PATCH 16/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.12.0=20?= =?UTF-8?q?=E2=80=94=20per-target=20X/Y/Z=20preview=20rotation,=20anchor?= =?UTF-8?q?=20&=20scale=20tuning=20for=20backpack=20slots,=20drop=20syncGr?= =?UTF-8?q?oup=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins.json | 2 +- plugins/sb_worn_display/sb_worn_display.js | 103 ++++++++++++++++----- 2 files changed, 81 insertions(+), 24 deletions(-) diff --git a/plugins.json b/plugins.json index d9d36bab..764b37fc 100644 --- a/plugins.json +++ b/plugins.json @@ -1541,7 +1541,7 @@ "icon": "backpack", "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt, Backpack-Arsenal chestplate) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.11.0", + "version": "4.12.0", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index 53338117..7209f9da 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -43,27 +43,50 @@ // を同じ文字列でグルーピングする。例えば 'back' を付けた slot は、 // どれかが編集されると 200ms 以内に他の同 group メンバーに値が伝播する。 // 背中装着の SB worn と MAW back saya は同じ位置なので同期させる。 + // + // preview transform (display_area の Three.js Object3D を上書きするための値): + // anchorX / anchorY / anchorZ : preview 上の display_area の position + // - 指定しない軸は 0 にリセット (前 slot の値の残留を防ぐ) + // - anchorY だけ指定すれば「水平 (X/Z) は body center、垂直は anchorY」 + // previewRotationX / previewRotationY / previewRotationZ : display_area の回転 (rad) + // - in-game レンダラ (SB / MAW / armor 等) が JSON display 値の前に独自の + // 回転を被せてくる場合があるので、preview でそれを模倣して合わせる + // - 例: SB の BackpackLayerRenderer は内部で Y-180° flip しているので + // SB worn 系の slot は previewRotationY = Math.PI を指定 + // - 例: MAW saya は LivingEntityRenderer の scale(-1,-1,1) 経由で Z-180° + // 相当の flip があるので previewRotationZ = Math.PI + // - 指定無しの軸は 0 にリセット (前 slot からの残留防止) + // previewScale : display_area の uniform scale + // - loadHead 直後は 0.625 (head 用) になっているので、フルサイズの + // backpack を player ボディに重ねたいときは 1.0 で上書き + // - 指定しないと loadHead の 0.625 が残る (saya は今までそれで OK だった) const TARGETS = [ { key: 'sophisticatedbackpacks:worn', tooltip: 'SB Worn (背中・SB) — sophisticatedbackpacks:worn', icon: 'backpack', - anchorY: 18, // 胸 (バックパック装着位置) - syncGroup: 'back', + anchorY: 13, // 胸中心 (model center を Y≈18 の胸に合わせる用に低めに) + anchorZ: 2, // 後ろ寄りすぎたので前に補正 (in-game の SB renderer 模倣) + previewScale: 0.7, // SB renderer 内部の縮小 (1.0 だと浮く・0.5 だと小さすぎた) + previewRotationY: Math.PI, // SB renderer の内部 Y-180 flip を模倣 + // syncGroup なし — 各 slot 独立 (saya / backpack / chestplate は別 renderer) }, { key: 'the_four_primitives_and_weapons:back', tooltip: 'MAW Saya Back (背中・MAW鞘) — the_four_primitives_and_weapons:back', icon: 'straighten', anchorY: 18, // 胸〜背中の中心 - syncGroup: 'back', + // syncGroup なし — 各 slot 独立 (saya / backpack / chestplate は別 renderer) + previewRotationZ: Math.PI, // saya は LivingEntityRenderer の scale(-1,-1,1) 相殺 + // previewScale 無し → loadHead の 0.625 を継承 (saya は今までこれで OK) }, { key: 'the_four_primitives_and_weapons:belt', tooltip: 'MAW Saya Belt (ベルト・MAW鞘) — the_four_primitives_and_weapons:belt', icon: 'linear_scale', anchorY: 12, // 腰・ベルト位置 - // (belt は独立) + previewRotationZ: Math.PI, // saya は LivingEntityRenderer の scale(-1,-1,1) 相殺 + // (belt は同期独立、previewScale は loadHead 継承) }, { // Backpack-Arsenal のカスタムバックパックを「胸甲 (chestplate) スタイル」で @@ -71,13 +94,14 @@ // ItemDisplayContext.create("backpack_arsenal:chestplate", ...) // を登録し、armor / curios chest スロット描画時にこの context を指定して // baked model の applyTransform を呼べば、ここで編集した値が反映される。 - // syncGroup='back' にしてあるので、sb worn / MAW back と同じ値を共有する - // (背中側の rotation/translation/scale を流用するため)。 key: 'backpack_arsenal:chestplate', tooltip: 'Backpack Arsenal Chestplate (胸甲・カスタムバックパック) — backpack_arsenal:chestplate', icon: 'shield', - anchorY: 18, // 胸 — 体幹中央 - syncGroup: 'back', + anchorY: 13, // 胸中心 (SB worn と同じ調整値) + anchorZ: 2, // SB worn と同じ前向き補正 + previewScale: 0.7, // SB Curios renderer の内部縮小に合わせる + previewRotationY: Math.PI, // SB の Curios renderer 経由で描画されるので Y-180 を模倣 + // syncGroup なし — 各 slot 独立 (saya / backpack / chestplate は別 renderer) }, ]; @@ -260,30 +284,63 @@ try { DisplayMode.updateDisplayBase(); } catch (e) { } - // custom slot 固有の anchor Y を適用 (loadHead は y=24+ で「顔」基準に - // 置くが、ベルトや背中の slot ならもっと下の腰・胸あたりが基準として - // 自然なので、ここで display_area (= モデルが乗ってる Three.js Object3D) - // の Y を上書きする)。 + // custom slot 固有の preview transform を適用。 + // + // loadHead 直後の display_area の状態: + // position = (0, ~28, ~0) ← 顔の前 + // rotation = (0, 0, 0) + // scale = (0.625, 0.625, 0.625) ← head 用の縮小 + // + // 我々の override: + // anchorX/Y/Z → position 上書き (指定の無い軸はリセットせず loadHead 値を維持) + // ※ anchorY を指定した slot は X/Z も 0 に下書きされる + // (body center に置きたい == 「めり込む」感じ) + // previewRotationZ → rotation.z 上書き (未指定なら 0 にリセット — saya の π が + // 次の slot に残るのを防ぐ) + // previewScale → scale を uniform で上書き (未指定なら loadHead 値継承) // - // 同時に da.rotation.z = π (180°) を入れる。Minecraft の - // LivingEntityRenderer は entity 描画時に scale(-1, -1, 1) で X+Y を - // 反転させており、Blockbench はそれを適用していないため display 値が - // 同じでも見え方が点対称になる (= roll 180° ずれる)。Z軸 180° 回転は - // (x,y,z) → (-x,-y,z) の変換になり scale(-1,-1,1) と等価で、これで - // Blockbench プレビューが in-game と一致する。 - if (typeof target.anchorY === 'number') { + // display_area は Blockbench 本体ではモジュールスコープのグローバル変数 + // として定義されているので window.display_area から取る。念のため + // DisplayMode.display_area / DisplayMode.display_base もフォールバック。 + const hasAnyOverride = + typeof target.anchorY === 'number' + || typeof target.anchorX === 'number' + || typeof target.anchorZ === 'number' + || typeof target.previewRotationX === 'number' + || typeof target.previewRotationY === 'number' + || typeof target.previewRotationZ === 'number' + || typeof target.previewScale === 'number'; + if (hasAnyOverride) { const da = (typeof display_area !== 'undefined') ? display_area : (DisplayMode.display_area || DisplayMode.display_base || null); if (da && da.position) { try { - da.position.y = target.anchorY; + // anchorY を指定した slot は X/Z も 0 = body center にデフォルト + // 寄せる (個別に anchorX / anchorZ で上書き可能)。 + if (typeof target.anchorY === 'number') { + da.position.x = (typeof target.anchorX === 'number') ? target.anchorX : 0; + da.position.y = target.anchorY; + da.position.z = (typeof target.anchorZ === 'number') ? target.anchorZ : 0; + } else { + if (typeof target.anchorX === 'number') da.position.x = target.anchorX; + if (typeof target.anchorZ === 'number') da.position.z = target.anchorZ; + } if (da.rotation) { - da.rotation.z = Math.PI; // = 180° (Minecraft の scale(-1,-1,1) 相殺) + // 各軸とも未指定なら 0 にリセット (前 slot の残留防止) + da.rotation.x = (typeof target.previewRotationX === 'number') + ? target.previewRotationX : 0; + da.rotation.y = (typeof target.previewRotationY === 'number') + ? target.previewRotationY : 0; + da.rotation.z = (typeof target.previewRotationZ === 'number') + ? target.previewRotationZ : 0; + } + if (typeof target.previewScale === 'number' && da.scale && da.scale.set) { + da.scale.set(target.previewScale, target.previewScale, target.previewScale); } if (typeof da.updateMatrixWorld === 'function') da.updateMatrixWorld(); if (typeof Transformer !== 'undefined' && Transformer.center) Transformer.center(); } catch (e) { - console.warn('[' + PLUGIN_ID + '] anchorY/rotation apply failed', e); + console.warn('[' + PLUGIN_ID + '] preview transform apply failed', e); } } } @@ -1015,7 +1072,7 @@ icon: 'backpack', description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt, Backpack-Arsenal chestplate) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.11.0', + version: '4.12.0', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', From 0ffcd9ecc56ab84a0dcc46eca187e2788a5c4d50 Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Fri, 5 Jun 2026 12:52:20 +0900 Subject: [PATCH 17/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.12.1=20?= =?UTF-8?q?=E2=80=94=20match=20BA=20chestplate=20preview=20to=20in-game?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replicates SB BackpackLayerRenderer.translateRotateAndScale + outer LivingEntityRenderer transforms in the plugin's preview: mulPose(YP.180) + mulPose(ZP.180) + translate(0,-0.25,-0.3) blocks outer translate(0,1.5,0) + scale(-1,-1,1) = vertex (x,y,z) → (-x, y+20, -z+4.8) pixels = Y-axis 180° rotation + translate(0, 20, 4.8) Plugin TARGETS for sb_worn / BA chestplate updated to: anchorY=20, anchorZ=4.8, previewScale=1.0, previewRotationY=π (no previewRotationZ — SB's Z-180 cancels with the X/Y flip) Preview now shows the backpack at the exact same position/orientation/ size as in-game when the user has tuned display values for in-game. --- plugins.json | 2 +- plugins/sb_worn_display/sb_worn_display.js | 28 +++++++++++++++------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/plugins.json b/plugins.json index 764b37fc..137b7d50 100644 --- a/plugins.json +++ b/plugins.json @@ -1541,7 +1541,7 @@ "icon": "backpack", "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt, Backpack-Arsenal chestplate) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.12.0", + "version": "4.12.1", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index 7209f9da..0ac459bb 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -65,10 +65,18 @@ key: 'sophisticatedbackpacks:worn', tooltip: 'SB Worn (背中・SB) — sophisticatedbackpacks:worn', icon: 'backpack', - anchorY: 13, // 胸中心 (model center を Y≈18 の胸に合わせる用に低めに) - anchorZ: 2, // 後ろ寄りすぎたので前に補正 (in-game の SB renderer 模倣) - previewScale: 0.7, // SB renderer 内部の縮小 (1.0 だと浮く・0.5 だと小さすぎた) - previewRotationY: Math.PI, // SB renderer の内部 Y-180 flip を模倣 + // SB の BackpackLayerRenderer.translateRotateAndScale をそのまま再現: + // mulPose(YP.180) + mulPose(ZP.180) + translate(0, -0.25, -0.3) + // + 外側 LivingEntityRenderer の translate(0,1.5,0) + scale(-1,-1,1) も合成 + // = vertex (x,y,z) → (-x, y+20, -z+4.8) ピクセル単位 + // = Y軸 180° 回転 + translate(0, 20, 4.8) と等価 + // これで preview がプレイヤーの胸・上部体幹に正しく embed され、 + // in-game の SB worn と同じ位置・向きになる。 + anchorY: 20, // 胸上部 (SB の合成 Y オフセット) + anchorZ: 4.8, // 前方への合成 Z オフセット + previewScale: 1.0, // SB は内部 scale 無し + previewRotationY: Math.PI, // SB 合成回転 = Y-180 のみ + // previewRotationZ 不要 (SB の Z-180 は LivingEntityRenderer scale flip と相殺) // syncGroup なし — 各 slot 独立 (saya / backpack / chestplate は別 renderer) }, { @@ -97,10 +105,12 @@ key: 'backpack_arsenal:chestplate', tooltip: 'Backpack Arsenal Chestplate (胸甲・カスタムバックパック) — backpack_arsenal:chestplate', icon: 'shield', - anchorY: 13, // 胸中心 (SB worn と同じ調整値) - anchorZ: 2, // SB worn と同じ前向き補正 - previewScale: 0.7, // SB Curios renderer の内部縮小に合わせる - previewRotationY: Math.PI, // SB の Curios renderer 経由で描画されるので Y-180 を模倣 + // BA は SB の BackpackCurioRenderer を流用しているので、SB worn と + // 完全に同じ transform を再現 (SB のソース translateRotateAndScale から計算済) + anchorY: 20, + anchorZ: 4.8, + previewScale: 1.0, + previewRotationY: Math.PI, // syncGroup なし — 各 slot 独立 (saya / backpack / chestplate は別 renderer) }, ]; @@ -1072,7 +1082,7 @@ icon: 'backpack', description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt, Backpack-Arsenal chestplate) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.12.0', + version: '4.12.1', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', From 5702ad03b8f031cd6cbb00f9db00ec094e3e8b28 Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Mon, 8 Jun 2026 20:37:27 +0900 Subject: [PATCH 18/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.12.2=20?= =?UTF-8?q?=E2=80=94=20match=20MAW=20saya=20back/belt=20preview=20to=20in-?= =?UTF-8?q?game=20by=20replicating=20ScabbardCurioRenderer=20transforms=20?= =?UTF-8?q?(scale=202/3=20+=20Z-180=20+=20anchor=20Y=3D18=20back=20/=20Y?= =?UTF-8?q?=3D12=20belt)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins.json | 2 +- plugins/sb_worn_display/sb_worn_display.js | 26 ++++++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/plugins.json b/plugins.json index 137b7d50..f6dc519c 100644 --- a/plugins.json +++ b/plugins.json @@ -1541,7 +1541,7 @@ "icon": "backpack", "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt, Backpack-Arsenal chestplate) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.12.1", + "version": "4.12.2", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index 0ac459bb..607ddcba 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -83,18 +83,30 @@ key: 'the_four_primitives_and_weapons:back', tooltip: 'MAW Saya Back (背中・MAW鞘) — the_four_primitives_and_weapons:back', icon: 'straighten', - anchorY: 18, // 胸〜背中の中心 + // MAW の ScabbardCurioRenderer (back slot) をそのまま再現: + // body.translateAndRotate + translate(0, 0.375, 0) blocks + scale(2/3) + // + 外側 LivingEntityRenderer の translate(0, 1.5, 0) + scale(-1,-1,1) + // = vertex (x,y,z) → (-2x/3, 18-2y/3, 2z/3) ピクセル単位 + // = Z軸 180° 回転 + scale 2/3 + translate(0, 18, 0) と等価 + anchorY: 18, // 体幹の中央 (合成 Y オフセット) + anchorZ: 0, + previewScale: 2 / 3, // MAW renderer の scale(2/3, 2/3, 2/3) + previewRotationZ: Math.PI, // X+Y flip (= Z 軸 180° 回転) を再現 // syncGroup なし — 各 slot 独立 (saya / backpack / chestplate は別 renderer) - previewRotationZ: Math.PI, // saya は LivingEntityRenderer の scale(-1,-1,1) 相殺 - // previewScale 無し → loadHead の 0.625 を継承 (saya は今までこれで OK) }, { key: 'the_four_primitives_and_weapons:belt', tooltip: 'MAW Saya Belt (ベルト・MAW鞘) — the_four_primitives_and_weapons:belt', icon: 'linear_scale', - anchorY: 12, // 腰・ベルト位置 - previewRotationZ: Math.PI, // saya は LivingEntityRenderer の scale(-1,-1,1) 相殺 - // (belt は同期独立、previewScale は loadHead 継承) + // MAW の ScabbardCurioRenderer (belt slot) をそのまま再現: + // body.translateAndRotate + translate(0, 0.75, 0) blocks + scale(2/3) + // + 外側 LivingEntityRenderer の translate(0, 1.5, 0) + scale(-1,-1,1) + // = vertex (x,y,z) → (-2x/3, 12-2y/3, 2z/3) ピクセル単位 + // = Z軸 180° 回転 + scale 2/3 + translate(0, 12, 0) と等価 + anchorY: 12, // 腰・ベルト位置 (合成 Y オフセット) + anchorZ: 0, + previewScale: 2 / 3, // MAW renderer の scale(2/3, 2/3, 2/3) + previewRotationZ: Math.PI, // X+Y flip (= Z 軸 180° 回転) を再現 }, { // Backpack-Arsenal のカスタムバックパックを「胸甲 (chestplate) スタイル」で @@ -1082,7 +1094,7 @@ icon: 'backpack', description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt, Backpack-Arsenal chestplate) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.12.1', + version: '4.12.2', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench', From 022483a2177689d62ce98eef9a43957c52a59b9a Mon Sep 17 00:00:00 2001 From: Hiromichi Nagase Date: Wed, 10 Jun 2026 00:02:10 +0900 Subject: [PATCH 19/19] =?UTF-8?q?sb=5Fworn=5Fdisplay:=20v4.13.0=20?= =?UTF-8?q?=E2=80=94=20add=20backpack=5Farsenal:placed=20+=20referenceMode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New target backpack_arsenal:placed for the placed-block display context. Introduces TARGETS.referenceMode with the following modes: 'fixed' — loadFixed + hide item-frame siblings (preview the bare model with no reference clutter) 'placed_block' — fixed + custom block guide (16x16 floor plane + center marker + XYZ axes, all rendered with THREE in display_area's parent) 'ground' / 'gui' — delegate to loadGround / loadGUI 'block' — fixed + custom dirt-floor cube (legacy variant) (unset) — keep loadHead (default — worn-style preview) backpack_arsenal:placed uses 'placed_block' so the user sees the backpack centered on a tinted floor with world-axes arrows, matching the in-game "placed as a block" rendering convention. Description in plugins.json updated to "chestplate / placed" to match the JS file. --- plugins.json | 4 +- plugins/sb_worn_display/sb_worn_display.js | 294 ++++++++++++++++++++- 2 files changed, 291 insertions(+), 7 deletions(-) diff --git a/plugins.json b/plugins.json index f6dc519c..a7f87951 100644 --- a/plugins.json +++ b/plugins.json @@ -1539,9 +1539,9 @@ "title": "SB Worn Display Editor", "author": "hrmcngs", "icon": "backpack", - "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt, Backpack-Arsenal chestplate) visually in the 3D viewport, using the same sliders as the vanilla slots.", + "description": "Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt, Backpack-Arsenal chestplate / placed) visually in the 3D viewport, using the same sliders as the vanilla slots.", "tags": ["Minecraft: Java Edition", "Modeling"], - "version": "4.12.2", + "version": "4.13.0", "min_version": "4.8.0", "variant": "both", "website": "https://github.com/hrmcngs/sb-worn-display-blockbench", diff --git a/plugins/sb_worn_display/sb_worn_display.js b/plugins/sb_worn_display/sb_worn_display.js index 607ddcba..fb48019e 100644 --- a/plugins/sb_worn_display/sb_worn_display.js +++ b/plugins/sb_worn_display/sb_worn_display.js @@ -125,6 +125,32 @@ previewRotationY: Math.PI, // syncGroup なし — 各 slot 独立 (saya / backpack / chestplate は別 renderer) }, + { + // Backpack-Arsenal のバックパックを「ワールドにブロックとして設置」した時の + // 描画用 display context。SB の BackpackBlockEntityRenderer は FIXED を + // 使うが、 BA は自前 BER (ArsenalBackpackBlockEntityRenderer) で + // ItemDisplayContext.create("backpack_arsenal:placed", ..., FIXED) + // を指定して描画する。 fallback が FIXED なので JSON に display.placed が + // 無ければ FIXED と同じ見た目になる。 + // + // BER の transform は translate(0.5, 0, 0.5) + Y軸 facing 回転のみで、 + // 内部 scale や rotation の追加は無し (= 純粋に display 値だけが効く)。 + // よって preview transform は identity 相当でよい。 + // + // referenceMode: 'fixed' でプレイヤー reference を消し、アイテムフレーム風の + // ベースに切り替える。 装着系 (worn / back / belt / chestplate) は身体と + // 重ねて見たいので loadHead のままだが、 設置ブロックは player と無関係なので + // プレイヤーが映ると邪魔になる。 + key: 'backpack_arsenal:placed', + tooltip: 'Backpack Arsenal Placed Block (設置ブロック) — backpack_arsenal:placed', + icon: 'view_in_ar', // 3D box icon (block らしさ) + referenceMode: 'placed_block', // 'placed_block' = loadFixed + 額縁 hide + ブロック guide + // (床平面 + 中心マーカー + 16x16x16 輪郭) + // 床位置とブロック中心が一目でわかるプレビュー + // anchor / previewScale / previewRotation は未指定 (= loadFixed のデフォルト + // item frame ライクな表示をそのまま使う) + // syncGroup なし — 設置ブロックは独立 (装着系とは別 transform 系統) + }, ]; const REF_BAR_ID = 'display_ref_bar'; @@ -258,8 +284,256 @@ }, 200); } + // target.referenceMode に応じて DisplayMode の reference loader を選ぶ。 + // 'fixed' → loadFixed + 額縁 mesh を非表示 (素のモデルだけ見たい placed block 用) + // 'ground' → loadGround (ドロップ品風) + // 'gui' → loadGUI (GUI 表示) + // 'block' → loadFixed + 自作の "床ブロック" reference (上記とは別の世界感プレビュー) + // 未指定 → loadHead (デフォルト、プレイヤーの体に重ねて見る装着系用) + // 指定があっても該当 loader が Blockbench に存在しない場合は loadHead に + // フォールバック (古い Blockbench でもクラッシュしない)。 + function pickReferenceLoader(target) { + const mode = target && target.referenceMode; + if (mode === 'fixed' && typeof DisplayMode.loadFixed === 'function') { + // loadFixed は item frame mesh を画面に出してしまうので、 frame だけ + // 隠してモデル単体プレビューにする (placed block の見た目を素で見たい用)。 + return function () { + DisplayMode.loadFixed(); + hideReferenceSiblingsExceptDisplayArea(); + }; + } + if (mode === 'placed_block' && typeof DisplayMode.loadFixed === 'function') { + // 'fixed' + ブロック guide。 床平面 + 中心マーカー + 16x16x16 輪郭で + // 「ブロックとして置かれた時の足元 / 中心 / 占有範囲」が一目でわかる。 + return function () { + DisplayMode.loadFixed(); + hideReferenceSiblingsExceptDisplayArea(); + addBlockGuide(); + }; + } + if (mode === 'ground' && typeof DisplayMode.loadGround === 'function') return DisplayMode.loadGround; + if (mode === 'gui' && typeof DisplayMode.loadGUI === 'function') return DisplayMode.loadGUI; + if (mode === 'block') { + // ベース: loadFixed → frame mesh hide → 床ブロック追加 (旧版) + return function () { + if (typeof DisplayMode.loadFixed === 'function') { + DisplayMode.loadFixed(); + hideReferenceSiblingsExceptDisplayArea(); + } else { + DisplayMode.loadHead(); + } + addBlockFloorReference(); + }; + } + return DisplayMode.loadHead; + } + + // ─── reference sibling hider ────────────────────────────────────── + // loadFixed が display_area の親シーンに追加する item frame mesh 等を hide する。 + // display_area 自身 (= 編集モデル) は触らない。 + // 元の visible は {@link hiddenSiblings} に退避し、 次の slot 切替時に + // {@link restoreHiddenSiblings} で復元する。 + + let hiddenSiblings = []; + + function hideReferenceSiblingsExceptDisplayArea() { + try { + const da = (typeof display_area !== 'undefined') ? display_area + : (DisplayMode.display_area || DisplayMode.display_base || null); + if (!da || !da.parent || !Array.isArray(da.parent.children)) return; + da.parent.children.forEach((child) => { + if (child !== da && child.visible !== false) { + hiddenSiblings.push({ obj: child, prev: child.visible }); + child.visible = false; + } + }); + } catch (e) { + console.warn('[' + PLUGIN_ID + '] hideReferenceSiblings failed', e); + } + } + + function restoreHiddenSiblings() { + try { + hiddenSiblings.forEach(({ obj, prev }) => { + if (obj) obj.visible = prev !== false; + }); + } catch (e) { + console.warn('[' + PLUGIN_ID + '] restoreHiddenSiblings failed', e); + } + hiddenSiblings = []; + } + + // ─── block guide (placed_block 用) ───────────────────────────────── + // MC で 1 ブロックは model 座標 0..16 を占める。 設置ブロック表示の編集中、 + // 「ブロックが置かれる床」「床の中心(モデルがどこを基準に立つか)」と + // 「MC ワールド座標系の軸方向」 を視覚化する。 + // + // 1. 床平面 : y=0 に 16x16 の半透明 plane (青系) — 設置時の足元の高さ + // 2. 中心マーカー: (8, 0, 8) に小さな赤球 — 床の中心 (= 設置時の基準点) + // 3. XYZ 軸 : 床の中心から各 +8 unit に伸びる色付き矢印 + // Red = +X (East), Green = +Y (Up), Blue = +Z (South) + // MC convention に合わせる。 + // + // 全部 1 つの Group に入れて名前で識別 / 一括削除する。 + const BLOCK_GUIDE_NAME = 'sb_custom_block_guide'; + + function addBlockGuide() { + if (typeof THREE === 'undefined') return; + try { + const scene = getDisplayScene(); + if (!scene || !scene.add) return; + removeBlockGuide(); // 二重追加防止 + + const guide = new THREE.Group(); + guide.name = BLOCK_GUIDE_NAME; + + // (1) 床平面 — y=0 で 16x16 + const floorGeo = new THREE.PlaneGeometry(16, 16); + const floorMat = new THREE.MeshBasicMaterial({ + color: 0x4488ff, + transparent: true, + opacity: 0.18, + side: THREE.DoubleSide, + depthWrite: false, // モデル裏にあっても見える + }); + const floor = new THREE.Mesh(floorGeo, floorMat); + floor.rotation.x = -Math.PI / 2; // XY 平面 → XZ 平面 (水平) + floor.position.set(8, 0, 8); // ブロック中心 X/Z, 高さ 0 + guide.add(floor); + + // (2) 中心マーカー — 床の中心 (8, 0, 8) に半径 0.4 の赤い球 + // 「ブロック中心 (8,8,8)」 ではなく 「床に面した中心」 を示すので y=0。 + const centerGeo = new THREE.SphereGeometry(0.4, 12, 8); + const centerMat = new THREE.MeshBasicMaterial({ + color: 0xff3344, + transparent: true, + opacity: 0.9, + depthWrite: false, + }); + const centerMarker = new THREE.Mesh(centerGeo, centerMat); + centerMarker.position.set(8, 0, 8); + guide.add(centerMarker); + + // (3) XYZ 軸 — 床の中心から +8 unit (= 0.5 block) の矢印 3本 + // +X = East (赤) + // +Y = Up (緑) + // +Z = South (青) + // Three.js の ArrowHelper は (dir, origin, length, color, headLength, headWidth)。 + // origin は (8, 0, 8) 床中心。 length=8 で半ブロック分。 + const axisOrigin = new THREE.Vector3(8, 0, 8); + const axisLength = 8; + const headLen = 1.6; + const headWid = 1.0; + + const xArrow = new THREE.ArrowHelper( + new THREE.Vector3(1, 0, 0), axisOrigin, axisLength, + 0xff4444, headLen, headWid); + const yArrow = new THREE.ArrowHelper( + new THREE.Vector3(0, 1, 0), axisOrigin, axisLength, + 0x44dd44, headLen, headWid); + const zArrow = new THREE.ArrowHelper( + new THREE.Vector3(0, 0, 1), axisOrigin, axisLength, + 0x4488ff, headLen, headWid); + + // ArrowHelper の構成要素 (line + cone) の material を depthWrite=false に + // して、 モデル裏に隠れた軸も薄く透けて見えるようにする。 + [xArrow, yArrow, zArrow].forEach((arrow) => { + if (arrow.line && arrow.line.material) { + arrow.line.material.depthWrite = false; + arrow.line.material.transparent = true; + arrow.line.material.opacity = 0.95; + } + if (arrow.cone && arrow.cone.material) { + arrow.cone.material.depthWrite = false; + arrow.cone.material.transparent = true; + arrow.cone.material.opacity = 0.95; + } + }); + + guide.add(xArrow); + guide.add(yArrow); + guide.add(zArrow); + + scene.add(guide); + } catch (e) { + console.warn('[' + PLUGIN_ID + '] addBlockGuide failed', e); + } + } + + function removeBlockGuide() { + try { + const scene = getDisplayScene(); + if (!scene) return; + const existing = scene.getObjectByName + ? scene.getObjectByName(BLOCK_GUIDE_NAME) + : null; + if (!existing) return; + if (scene.remove) scene.remove(existing); + existing.traverse && existing.traverse((obj) => { + if (obj.geometry && obj.geometry.dispose) obj.geometry.dispose(); + if (obj.material && obj.material.dispose) obj.material.dispose(); + }); + } catch (e) { + console.warn('[' + PLUGIN_ID + '] removeBlockGuide failed', e); + } + } + + // ─── block floor reference (custom) ──────────────────────────────── + // Blockbench に「ワールドに置かれたブロックの足元」reference は無いので、 + // 単純な土色 cube を display_area の親シーンに足す。 + // 名前で識別して二重追加・他 slot 残留を防ぐ。 + const BLOCK_FLOOR_NAME = 'sb_custom_block_floor'; + + function getDisplayScene() { + const da = (typeof display_area !== 'undefined') ? display_area + : (DisplayMode.display_area || DisplayMode.display_base || null); + if (!da) return null; + // display_area の親 (= 表示シーン直下) に追加する。 + // da.parent が無い場合 (loader 前) はそのまま da を返す (later add される)。 + return da.parent || da; + } + + function removeBlockFloorReference() { + try { + const scene = getDisplayScene(); + if (!scene) return; + const existing = scene.getObjectByName + ? scene.getObjectByName(BLOCK_FLOOR_NAME) + : null; + if (existing) { + if (scene.remove) scene.remove(existing); + if (existing.geometry && existing.geometry.dispose) existing.geometry.dispose(); + if (existing.material && existing.material.dispose) existing.material.dispose(); + } + } catch (e) { + console.warn('[' + PLUGIN_ID + '] removeBlockFloorReference failed', e); + } + } + + function addBlockFloorReference() { + if (typeof THREE === 'undefined') return; + try { + const scene = getDisplayScene(); + if (!scene || !scene.add) return; + // 既存があれば一旦消す (slot 切替時に loadCustomSlot からも remove するが念のため)。 + removeBlockFloorReference(); + + // モデル座標 (0..16) に対して床: y=-1 で 16x1x16 の cube。 + // grass topっぽい緑緑灰 / dirt 系の褐色を反映した dual-material はやり過ぎなので + // 単色 (土・草の中間色) で十分。 + const geo = new THREE.BoxGeometry(16, 1, 16); + const mat = new THREE.MeshLambertMaterial({ color: 0x7da34d, transparent: false }); + const mesh = new THREE.Mesh(geo, mat); + mesh.position.set(8, -0.5, 8); // モデル中央 (8,_,8) に合わせて足元 y=-0.5 + mesh.name = BLOCK_FLOOR_NAME; + scene.add(mesh); + } catch (e) { + console.warn('[' + PLUGIN_ID + '] addBlockFloorReference failed', e); + } + } + // ─── custom slot loader ──────────────────────────────────────────── - // DisplayMode.loadHead() を踏み台にカメラ/Reference バーを設定し + // pickReferenceLoader() を踏み台にカメラ/Reference バーを設定し // その後 slot をカスタムキーに差し替える。 function loadCustomSlot(target, options) { // options.silent : true なら通知メッセージを出さない @@ -290,11 +564,21 @@ } catch (e) { } } + // slot 切替の度に、前 slot が残した自作 reference (床ブロック等) を確実に消す。 + // referenceMode === 'block' の slot に切り替わる場合は pickReferenceLoader + // 内で再度 add される。それ以外の slot に切り替わる場合はこれで消えたまま。 + removeBlockFloorReference(); + removeBlockGuide(); + // 前 slot で hide した item frame / player などの reference を一旦復元する。 + // referenceMode === 'fixed' / 'placed_block' の slot に切り替わる場合は + // pickReferenceLoader 内で再度 hide される。 + restoreHiddenSiblings(); + if (!opts.skipCameraReset) { try { - DisplayMode.loadHead(); + pickReferenceLoader(target)(); } catch (e) { - console.warn('[' + PLUGIN_ID + '] loadHead failed', e); + console.warn('[' + PLUGIN_ID + '] reference loader failed', e); } } @@ -1092,9 +1376,9 @@ title: 'SB Worn Display Editor', author: 'hrmcngs', icon: 'backpack', - description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt, Backpack-Arsenal chestplate) visually in the 3D viewport, using the same sliders as the vanilla slots.', + description: 'Adds a Custom Slot row to the Display panel so you can edit custom item display keys (Sophisticated Backpacks worn, MAW saya back/belt, Backpack-Arsenal chestplate / placed) visually in the 3D viewport, using the same sliders as the vanilla slots.', tags: ['Minecraft: Java Edition', 'Modeling'], - version: '4.12.2', + version: '4.13.0', min_version: '4.8.0', variant: 'both', website: 'https://github.com/hrmcngs/sb-worn-display-blockbench',