From efce33557860f5f2d9ba323fd035f90ff3341ea8 Mon Sep 17 00:00:00 2001 From: Wx-2025 <351320169@qq.com> Date: Sun, 28 Sep 2025 17:31:17 +0800 Subject: [PATCH] Add files via upload --- WorldEditor/WorldEditor.css | 344 +++++++++++++++++++++++ WorldEditor/WorldEditor.js | 525 ++++++++++++++++++++++++++++++++++++ 2 files changed, 869 insertions(+) create mode 100644 WorldEditor/WorldEditor.css create mode 100644 WorldEditor/WorldEditor.js diff --git a/WorldEditor/WorldEditor.css b/WorldEditor/WorldEditor.css new file mode 100644 index 0000000..abfad63 --- /dev/null +++ b/WorldEditor/WorldEditor.css @@ -0,0 +1,344 @@ +/* 世界书编辑器样式 */ +#world-editor-container .world-editor { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + color: #ffffff; + overflow: hidden; + display: flex; + flex-direction: column; +} + +#world-editor-container .world-editor-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + background-color: #2d2d2d; + border-bottom: 1px solid #555; + flex-shrink: 0; +} + +#world-editor-container .world-editor-header h1 { + margin: 0; + color: #68b7ff; + font-size: 20px; +} + +#world-editor-container .world-editor-header-controls { + display: flex; + gap: 10px; + align-items: center; + flex-wrap: wrap; +} + +#world-editor-container .world-editor-btn { + padding: 6px 12px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + transition: background-color 0.3s; +} + +#world-editor-container .world-editor-btn-primary { + background-color: #4a90e2; + color: white; +} + +#world-editor-container .world-editor-btn-primary:hover { + background-color: #357abd; +} + +#world-editor-container .world-editor-btn-success { + background-color: #4CAF50; + color: white; +} + +#world-editor-container .world-editor-btn-success:hover { + background-color: #45a049; +} + +#world-editor-container .world-editor-btn-danger { + background-color: #f44336; + color: white; +} + +#world-editor-container .world-editor-btn-danger:hover { + background-color: #da190b; +} + +#world-editor-container .world-editor-btn-warning { + background-color: #ff9800; + color: white; +} + +#world-editor-container .world-editor-btn-warning:hover { + background-color: #e68900; +} + +#world-editor-container .world-editor-content { + flex: 1; + padding: 20px; + overflow: auto; +} + +#world-editor-container .world-editor-selector { + margin-bottom: 15px; + padding: 15px; + background-color: #2d2d2d; + border-radius: 8px; +} + +#world-editor-container .world-editor-selector select { + width: 100%; + padding: 8px; + background-color: #404040; + color: white; + border: 1px solid #555; + border-radius: 4px; +} + +#world-editor-container .world-editor-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + padding: 10px 15px; + background-color: #2d2d2d; + border-radius: 8px; +} + +#world-editor-container .world-editor-toolbar-left { + display: flex; + gap: 10px; + align-items: center; +} + +#world-editor-container .world-editor-toolbar-right { + display: flex; + gap: 10px; + align-items: center; +} + +#world-editor-container .world-editor-search-box { + padding: 6px 10px; + background-color: #404040; + color: white; + border: 1px solid #555; + border-radius: 4px; + min-width: 200px; +} + +#world-editor-container .world-editor-entries-container { + background-color: #2d2d2d; + border-radius: 8px; + overflow-y: auto; + max-height: calc(100vh - 300px); /* Adjust as needed */ + position: relative; +} + +#world-editor-container .world-editor-entries-header { + display: grid; + grid-template-columns: 40px 50px 50px 120px 1fr 100px 80px 80px; + background-color: #404040; + padding: 10px; + border-bottom: 1px solid #555; + font-weight: bold; + font-size: 12px; + position: sticky; + top: 0; + z-index: 1; +} + +#world-editor-container .world-editor-entries-header > div[data-sort] { + cursor: pointer; + user-select: none; +} + +#world-editor-container .world-editor-entries-header > div[data-sort]:hover { + color: #68b7ff; +} + +#world-editor-container .world-editor-entry-row { + display: grid; + grid-template-columns: 40px 50px 50px 120px 1fr 100px 80px 80px; + padding: 8px 10px; + border-bottom: 1px solid #333; + align-items: center; + transition: background-color 0.2s; + min-height: 40px; + cursor: pointer; +} + + +#world-editor-container .world-editor-entry-row:hover { + background-color: #3a3a3a; +} + +#world-editor-container .world-editor-entry-row.selected { + background-color: rgba(74, 74, 74, 0.5); +} + +#world-editor-container .world-editor-entry-checkbox { + width: 16px; + height: 16px; + justify-self: center; +} + +#world-editor-container .world-editor-entry-status { + text-align: center; + font-size: 18px; + cursor: pointer; +} + +#world-editor-container .world-editor-status-enabled { + color: #4CAF50; +} + +#world-editor-container .world-editor-status-disabled { + color: #f44336; +} + +#world-editor-container .fa-toggle-on { + color: #68b7ff; +} + +#world-editor-container .fa-toggle-off { + color: #777; +} + +#world-editor-container .world-editor-entry-activation { + text-align: center; + font-size: 16px; + cursor: pointer; +} + +#world-editor-container .world-editor-activation-constant { + color: #2196F3; +} + +#world-editor-container .world-editor-activation-selective { + color: #4CAF50; +} + +#world-editor-container .world-editor-entry-keys { + font-size: 11px; + color: #aaa; + max-width: 140px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#world-editor-container .world-editor-entry-content { + font-size: 11px; + color: #ccc; + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#world-editor-container .world-editor-entry-position { + font-size: 10px; + color: #999; + text-align: center; +} + +#world-editor-container .world-editor-entry-depth { + font-size: 12px; + color: #fff; + text-align: center; + background-color: #555; + border-radius: 3px; + padding: 2px 6px; + cursor: pointer; +} + +#world-editor-container .world-editor-entry-order { + font-size: 12px; + color: #fff; + text-align: center; +} + +/* 行内编辑样式 */ +#world-editor-container .inline-edit { + background-color: transparent; + border: 1px solid transparent; + color: #ccc; + font-family: inherit; + font-size: 11px; + padding: 2px 4px; + width: 100%; + box-sizing: border-box; + border-radius: 3px; + transition: border-color 0.2s, background-color 0.2s; +} + +#world-editor-container .inline-edit:hover { + border-color: #555; +} + +#world-editor-container .inline-edit:focus { + background-color: #404040; + border-color: #68b7ff; + outline: none; +} + +#world-editor-container .inline-edit[disabled] { + background-color: rgba(0,0,0,0.2); + color: #777; + cursor: not-allowed; +} + +#world-editor-container .inline-toggle { + cursor: pointer; + text-align: center; +} + +#world-editor-container .world-editor-batch-actions { + display: none; + padding: 15px; + background-color: #4a4a4a; + border-bottom: 1px solid #555; + gap: 10px; + align-items: center; + flex-wrap: wrap; +} + +#world-editor-container .world-editor-batch-actions.active { + display: flex; +} + +#world-editor-container .world-editor-selected-count { + color: #68b7ff; + font-weight: bold; +} + + +#world-editor-container .world-editor-loading { + text-align: center; + padding: 40px; + color: #999; +} + +#world-editor-container .world-editor-empty-state { + text-align: center; + padding: 40px; + color: #999; +} + +#world-editor-container .world-editor-error { + background-color: #d32f2f; + color: white; + padding: 10px; + border-radius: 4px; + margin-bottom: 15px; +} + +#world-editor-container .world-editor-success { + background-color: #388e3c; + color: white; + padding: 10px; + border-radius: 4px; + margin-bottom: 15px; +} diff --git a/WorldEditor/WorldEditor.js b/WorldEditor/WorldEditor.js new file mode 100644 index 0000000..ec82c2f --- /dev/null +++ b/WorldEditor/WorldEditor.js @@ -0,0 +1,525 @@ +/** + * 世界书编辑器 - 最终稳定版 + */ +import { world_names, loadWorldInfo, saveWorldInfo } from "/scripts/world-info.js"; +import { eventSource, event_types } from '/script.js'; +import { showHtmlModal } from '/scripts/extensions/third-party/ST-Amily2-Chat-Optimisation/ui/page-window.js'; +const { SillyTavern, TavernHelper } = window; + +class WorldEditor { + constructor() { + this.currentWorldBook = null; + this.entries = []; + this.selectedEntries = new Set(); + this.filteredEntries = []; + this.isLoading = false; + this.currentEditingEntry = null; + this.sortState = { key: 'order', asc: true }; + this.init(); + } + + init() { + if (!this.initializeComponents()) { + console.error('[世界书编辑器] 组件初始化失败,5秒后重试...'); + setTimeout(() => this.init(), 5000); + return; + } + this.bindEvents(); + this.loadAvailableWorldBooks(); + this.bindExternalEvents(); // 绑定外部事件监听 + } + + initializeComponents() { + const ids = [ + 'world-editor-world-select', 'world-editor-refresh-btn', 'world-editor-create-entry-btn', + 'world-editor-search-box', 'world-editor-search-btn', 'world-editor-entry-count', + 'world-editor-select-all', 'world-editor-selected-count', 'world-editor-batch-actions', + 'world-editor-entries-container', + 'world-editor-enable-selected-btn', 'world-editor-disable-selected-btn', + 'world-editor-set-blue-btn', 'world-editor-set-green-btn', 'world-editor-delete-selected-btn', + 'world-editor-set-disable-recursion-btn', 'world-editor-set-prevent-recursion-btn' + ]; + this.elements = {}; + let missing = false; + for (const id of ids) { + const camelCaseId = id.replace(/-(\w)/g, (_, c) => c.toUpperCase()); + this.elements[camelCaseId] = document.getElementById(id); + if (!this.elements[camelCaseId] && id.endsWith('container')) { // Only container is critical + console.error(`[世界书编辑器] 关键元素缺失: ${id}`); + missing = true; + } + } + return !missing; + } + + bindEvents() { + this.elements.worldEditorWorldSelect.addEventListener('change', (e) => this.loadWorldBookEntries(e.target.value)); + this.elements.worldEditorRefreshBtn.addEventListener('click', () => this.loadAvailableWorldBooks()); + document.querySelector('#world-editor-container .world-editor-entries-header').addEventListener('click', (e) => { + if (e.target.dataset.sort) { + this.sortEntries(e.target.dataset.sort); + } + }); + this.elements.worldEditorCreateEntryBtn.addEventListener('click', () => this.openCreateModal()); + this.elements.worldEditorSearchBox.addEventListener('input', () => this.filterEntries()); + this.elements.worldEditorSearchBtn.addEventListener('click', () => this.filterEntries()); + this.elements.worldEditorSelectAll.addEventListener('change', (e) => this.toggleSelectAll(e.target.checked)); + this.elements.worldEditorEnableSelectedBtn.addEventListener('click', () => this.batchUpdateEntries({ enabled: true })); + this.elements.worldEditorDisableSelectedBtn.addEventListener('click', () => this.batchUpdateEntries({ enabled: false })); + this.elements.worldEditorSetBlueBtn.addEventListener('click', () => this.batchUpdateEntries({ type: 'constant' })); + this.elements.worldEditorSetGreenBtn.addEventListener('click', () => this.batchUpdateEntries({ type: 'selective' })); + this.elements.worldEditorDeleteSelectedBtn.addEventListener('click', () => this.batchDeleteEntries()); + this.elements.worldEditorSetDisableRecursionBtn.addEventListener('click', () => this.toggleBatchRecursion('exclude_recursion', '不可递归')); + this.elements.worldEditorSetPreventRecursionBtn.addEventListener('click', () => this.toggleBatchRecursion('prevent_recursion', '防止递归')); + } + + async loadAvailableWorldBooks() { + this.setLoading(true); + try { + const books = await this.getAllWorldBooks(); + const select = this.elements.worldEditorWorldSelect; + select.innerHTML = ''; + books.forEach(book => { + const option = document.createElement('option'); + option.value = book.name; + option.textContent = book.name; + select.appendChild(option); + }); + await this.selectCurrentCharacterWorldBook(); + } catch (error) { + this.showError('加载世界书列表失败: ' + error.message); + } finally { + this.setLoading(false); + } + } + + async getAllWorldBooks() { + if (TavernHelper?.getLorebooks) { + const books = await TavernHelper.getLorebooks(); + if (Array.isArray(books) && books.length > 0) return books.map(name => ({ name })); + } + return (world_names || []).map(name => ({ name })); + } + + async selectCurrentCharacterWorldBook() { + if (TavernHelper?.getCurrentCharPrimaryLorebook) { + const primaryBook = await TavernHelper.getCurrentCharPrimaryLorebook(); + if (primaryBook) { + this.elements.worldEditorWorldSelect.value = primaryBook; + await this.loadWorldBookEntries(primaryBook); + } + } + } + + async loadWorldBookEntries(worldBookName) { + if (!worldBookName) { this.entries = []; this.renderEntries(); return; } + this.setLoading(true); + this.currentWorldBook = worldBookName; + try { + let rawEntries = await TavernHelper?.getLorebookEntries?.(worldBookName); + if (!rawEntries || rawEntries.length === 0) { + const bookData = await loadWorldInfo(worldBookName); + if (bookData?.entries) { + rawEntries = Object.entries(bookData.entries).map(([uid, entry]) => ({ + uid: parseInt(uid), enabled: !entry.disable, type: entry.constant ? 'constant' : 'selective', + keys: entry.key || [], content: entry.content || '', position: this.convertPositionFromNative(entry.position), + depth: entry.depth, order: entry.order, comment: entry.comment || '' + })); + } + } + this.entries = (rawEntries || []).map(e => ({ + uid: e.uid, enabled: e.enabled, type: e.type || (e.constant ? 'constant' : 'selective'), + keys: e.keys || [], content: e.content || '', position: e.position || 'before_character_definition', + depth: (e.position?.startsWith('at_depth')) ? e.depth : null, order: e.order || 100, comment: e.comment || '', + exclude_recursion: e.exclude_recursion, prevent_recursion: e.prevent_recursion + })); + this.filteredEntries = [...this.entries]; + this.renderEntries(); + this.updateEntryCount(); + } catch (error) { + this.showError(`加载条目失败: ${error.message}`); + this.entries = []; this.renderEntries(); + } finally { + this.setLoading(false); + } + } + + convertPositionFromNative(pos) { + const map = { 0: 'before_character_definition', 1: 'after_character_definition', 2: 'before_author_note', 3: 'after_author_note', 4: 'at_depth' }; + return map[pos] || 'at_depth'; + } + + renderEntries() { + const container = this.elements.worldEditorEntriesContainer; + const header = container.querySelector('.world-editor-entries-header'); + + // Clear only the entry rows, not the header + while (container.firstChild && container.firstChild !== header) { + container.removeChild(container.firstChild); + } + while (header && header.nextSibling) { + container.removeChild(header.nextSibling); + } + + this.sortFilteredEntries(); + + if (this.filteredEntries.length === 0) { + const emptyState = document.createElement('div'); + emptyState.className = 'world-editor-empty-state'; + emptyState.innerHTML = '

没有条目

'; + container.appendChild(emptyState); + return; + } + + const fragment = document.createDocumentFragment(); + this.filteredEntries.forEach(e => { + const row = document.createElement('div'); + row.innerHTML = this.renderEntryRow(e).trim(); + fragment.appendChild(row.firstChild); + }); + container.appendChild(fragment); + this.bindEntryEvents(); + } + + renderEntryRow(entry) { + const positionOptions = { + 'before_character_definition': '角色前', 'after_character_definition': '角色后', + 'before_author_note': '注释前', 'after_author_note': '注释后', + 'at_depth': '@D深度', 'at_depth_as_system': '@D深度' + }; + const positionSelect = ``; + + return ` +
+
+
+
${entry.type === 'constant' ? '🔵' : '🟢'}
+
+
${entry.content || ''}
+
${positionSelect}
+
+
+
`; + } + + bindEntryEvents() { + this.elements.worldEditorEntriesContainer.querySelectorAll('.world-editor-entry-row').forEach(row => { + const uid = parseInt(row.dataset.uid); + + // Checkbox + const checkbox = row.querySelector('.world-editor-entry-checkbox'); + checkbox.addEventListener('change', (e) => { + if (e.target.checked) this.selectedEntries.add(uid); else this.selectedEntries.delete(uid); + row.classList.toggle('selected', e.target.checked); + this.updateSelectionUI(); + }); + + // Content click to open modal (for longer edits) + row.querySelector('[data-action="open-editor"]').addEventListener('click', () => this.openEditModal(uid)); + + // Inline toggles (enabled, type) + row.querySelectorAll('.inline-toggle').forEach(toggle => { + toggle.addEventListener('click', (e) => { + e.stopPropagation(); + const field = toggle.dataset.field; + const entry = this.entries.find(e => e.uid === uid); + let newValue; + if (field === 'enabled') newValue = !entry.enabled; + if (field === 'type') newValue = entry.type === 'constant' ? 'selective' : 'constant'; + this.updateSingleEntry(uid, { [field]: newValue }); + }); + }); + + // Inline edits (inputs, selects) + row.querySelectorAll('.inline-edit').forEach(input => { + input.addEventListener('change', (e) => { + e.stopPropagation(); + const field = input.dataset.field; + let value = input.value; + if (input.type === 'number') value = parseInt(value, 10); + // The 'keys' field is no longer inline editable, so that specific logic can be removed. + + const updates = { [field]: value }; + // If position changes, re-evaluate depth disable state + if (field === 'position') { + const depthInput = row.querySelector('[data-field="depth"]'); + if (depthInput) depthInput.disabled = !value.startsWith('at_depth'); + } + + this.updateSingleEntry(uid, updates); + }); + input.addEventListener('click', e => e.stopPropagation()); // Prevent row selection when clicking input + }); + }); + } + + async updateSingleEntry(uid, updates) { + const entry = this.entries.find(e => e.uid === uid); + if (!entry) return; + + // Optimistic UI update + Object.assign(entry, updates); + this.renderEntries(); // Re-render to reflect changes immediately + + try { + await TavernHelper.setLorebookEntries(this.currentWorldBook, [{ ...entry, ...updates }]); + } catch (error) { + this.showError(`更新失败: ${error.message}`); + // Revert on failure + this.loadWorldBookEntries(this.currentWorldBook); + } + } + + async batchUpdateEntries(updates, confirmation = null) { + if (this.selectedEntries.size === 0) return; + if (confirmation && !confirm(confirmation)) return; + + const entries = this.entries.filter(e => this.selectedEntries.has(e.uid)).map(e => ({ ...e, ...updates })); + await TavernHelper.setLorebookEntries(this.currentWorldBook, entries); + this.loadWorldBookEntries(this.currentWorldBook); // Refresh + if (window.toastr) window.toastr.success('批量更新成功!'); + } + + toggleBatchRecursion(field, fieldName) { + if (this.selectedEntries.size === 0) return; + const selected = this.entries.filter(e => this.selectedEntries.has(e.uid)); + const enabledCount = selected.filter(e => e[field]).length; + const shouldEnable = enabledCount <= selected.length / 2; + const action = shouldEnable ? '启用' : '禁用'; + const confirmation = `确定为 ${this.selectedEntries.size} 个条目 ${action} "${fieldName}" 吗?`; + this.batchUpdateEntries({ [field]: shouldEnable }, confirmation); + } + + async batchDeleteEntries() { + if (this.selectedEntries.size === 0 || !confirm(`删除 ${this.selectedEntries.size} 个条目?`)) return; + await TavernHelper.deleteLorebookEntries(this.currentWorldBook, Array.from(this.selectedEntries)); + this.selectedEntries.clear(); + this.loadWorldBookEntries(this.currentWorldBook); // Refresh + } + + toggleSelectAll(checked) { + this.selectedEntries.clear(); + if (checked) this.filteredEntries.forEach(e => this.selectedEntries.add(e.uid)); + this.renderEntries(); + this.updateSelectionUI(); + } + + updateSelectionUI() { + const count = this.selectedEntries.size; + this.elements.worldEditorSelectedCount.textContent = `已选择 ${count} 项`; + this.elements.worldEditorBatchActions.classList.toggle('active', count > 0); + this.elements.worldEditorSelectAll.checked = count > 0 && count === this.filteredEntries.length; + this.elements.worldEditorSelectAll.indeterminate = count > 0 && count < this.filteredEntries.length; + } + + updateEntryCount() { this.elements.worldEditorEntryCount.textContent = `条目:${this.entries.length}`; } + + filterEntries() { + const term = this.elements.worldEditorSearchBox.value.toLowerCase(); + const searchType = document.getElementById('world-editor-search-type').value; + + if (!term) { + this.filteredEntries = [...this.entries]; + } else { + this.filteredEntries = this.entries.filter(e => { + const targetField = e[searchType] || ''; + return targetField.toLowerCase().includes(term); + }); + } + this.renderEntries(); + } + + openCreateModal() { + this.currentEditingEntry = null; + const entry = { enabled: true, type: 'selective', keys: [], content: '', position: 'at_depth', depth: 4, order: 100, comment: '' }; + this.showEditModal('创建新条目', entry); + } + + openEditModal(uid) { + const entry = this.entries.find(e => e.uid === uid); + if (!entry) return; + this.currentEditingEntry = entry; + this.showEditModal('编辑条目', entry); + } + + showEditModal(title, entry) { + const formHtml = this.getEditFormHtml(entry); + showHtmlModal(title, formHtml, { + onOk: (dialog) => { + this.saveEntry(dialog); + return true; // Close the modal + } + }); + } + + getEditFormHtml(entry) { + return ` + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ +
+ `; + } + + async saveEntry(dialog) { + const formData = this.getFormDataFromModal(dialog); + if (this.currentEditingEntry) { + await TavernHelper.setLorebookEntries(this.currentWorldBook, [{ ...this.currentEditingEntry, ...formData }]); + } else { + await TavernHelper.createLorebookEntries(this.currentWorldBook, [formData]); + } + this.loadWorldBookEntries(this.currentWorldBook); + } + + getFormDataFromModal(dialog) { + const data = {}; + data.enabled = dialog.find('#world-editor-entry-enabled').is(':checked'); + data.type = dialog.find('#world-editor-entry-type').val(); + data.keys = dialog.find('#world-editor-entry-keys').val().split('\n').map(k => k.trim()).filter(Boolean); + data.content = dialog.find('#world-editor-entry-content').val(); + data.position = dialog.find('#world-editor-entry-position').val(); + data.depth = parseInt(dialog.find('#world-editor-entry-depth').val()); + data.order = parseInt(dialog.find('#world-editor-entry-order').val()); + data.comment = dialog.find('#world-editor-entry-comment').val(); + data.exclude_recursion = dialog.find('#world-editor-entry-disable-recursion').is(':checked'); + data.prevent_recursion = dialog.find('#world-editor-entry-prevent-recursion').is(':checked'); + return data; + } + + setLoading(loading) { this.elements.worldEditorEntriesContainer.classList.toggle('loading', loading); } + showError(msg) { if (window.toastr) window.toastr.error(msg); console.error(msg); } + + sortEntries(key) { + if (this.sortState.key === key) { + this.sortState.asc = !this.sortState.asc; + } else { + this.sortState.key = key; + this.sortState.asc = true; + } + this.renderEntries(); + } + + sortFilteredEntries() { + const { key, asc } = this.sortState; + this.filteredEntries.sort((a, b) => { + let valA = a[key]; + let valB = b[key]; + + if (typeof valA === 'string') valA = valA.toLowerCase(); + if (typeof valB === 'string') valB = valB.toLowerCase(); + + if (valA < valB) return asc ? -1 : 1; + if (valA > valB) return asc ? 1 : -1; + return 0; + }); + } + + bindExternalEvents() { + eventSource.on(event_types.CHAT_CHANGED, () => { + console.log('[世界书编辑器] 检测到聊天变更 (CHAT_CHANGED),将自动刷新。'); + this.loadAvailableWorldBooks(); + }); + + console.log('[世界书编辑器] 已成功绑定外部事件监听器。'); + } +} + +function initializeWorldEditorWhenVisible() { + const panel = document.getElementById('amily2_world_editor_panel'); + if (!panel) { console.error('[WorldEditor] Panel not found!'); return; } + const observer = new MutationObserver(() => { + if (panel.style.display !== 'none' && !window.worldEditorInstance) { + window.worldEditorInstance = new WorldEditor(); + observer.disconnect(); + } + }); + observer.observe(panel, { attributes: true, attributeFilter: ['style'] }); + if (panel.style.display !== 'none') { // Check initial state + if (!window.worldEditorInstance) window.worldEditorInstance = new WorldEditor(); + observer.disconnect(); + } +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initializeWorldEditorWhenVisible); +} else { + initializeWorldEditorWhenVisible(); +}