kingkont 0.20.11 → 0.20.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/renderer/board.js +106 -15
- package/renderer/styles.css +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kingkont",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.13",
|
|
4
4
|
"description": "KingKont \u00b7 Chatium \u2014 \u043d\u043e\u0434-\u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0441\u0446\u0435\u043d \u0441 AI-\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0435\u0439 (\u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438/\u0432\u0438\u0434\u0435\u043e/\u0433\u043e\u043b\u043e\u0441/SFX/\u043c\u0443\u0437\u044b\u043a\u0430/\u0442\u0435\u043a\u0441\u0442)",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"bin": {
|
package/renderer/board.js
CHANGED
|
@@ -3144,6 +3144,65 @@ $('newLocation').addEventListener('click', async () => {
|
|
|
3144
3144
|
}
|
|
3145
3145
|
});
|
|
3146
3146
|
// =================== Board (универсально для серии и персонажа) ===================
|
|
3147
|
+
// === Link-to-scene: нода может содержать ссылку на другую сцену/борд. ====
|
|
3148
|
+
// Юзер: «из ноды можно сделать ссылку на сцену; ПКМ — сделать ссылку,
|
|
3149
|
+
// убрать ссылку; если есть ссылка — показываем значком, дабл-клик
|
|
3150
|
+
// открывает нужную сцену».
|
|
3151
|
+
// node.linkedBoard = { kind: 'episode'|'character'|'location', name: '<имя>' }
|
|
3152
|
+
// handle резолвится в момент перехода (через listEpisodes/listCharacters
|
|
3153
|
+
// /listLocations), хранить нельзя — FSAH-handle не сериализуется в JSON.
|
|
3154
|
+
function refreshNodeLinkBadge(el, node) {
|
|
3155
|
+
const badge = el.querySelector('.link-badge');
|
|
3156
|
+
if (!badge) return;
|
|
3157
|
+
if (node.linkedBoard?.name) {
|
|
3158
|
+
badge.style.display = '';
|
|
3159
|
+
badge.title = `Дабл-клик / клик: открыть → ${node.linkedBoard.name}`;
|
|
3160
|
+
} else {
|
|
3161
|
+
badge.style.display = 'none';
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
async function _resolveLinkedBoardHandle(target) {
|
|
3165
|
+
if (!state.filmHandle || !target?.name) return null;
|
|
3166
|
+
const list = target.kind === 'character' ? await listCharacters(state.filmHandle)
|
|
3167
|
+
: target.kind === 'location' ? await listLocations(state.filmHandle)
|
|
3168
|
+
: await listEpisodes(state.filmHandle);
|
|
3169
|
+
return list.find(b => b.name === target.name) || null;
|
|
3170
|
+
}
|
|
3171
|
+
async function goToLinkedBoard(node) {
|
|
3172
|
+
const t = node.linkedBoard;
|
|
3173
|
+
if (!t?.name) return;
|
|
3174
|
+
const found = await _resolveLinkedBoardHandle(t);
|
|
3175
|
+
if (!found) {
|
|
3176
|
+
alert(`Целевая сцена не найдена: ${t.name} (возможно, переименована или удалена)`);
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
await selectBoard({ kind: t.kind || 'episode', ...found });
|
|
3180
|
+
}
|
|
3181
|
+
async function pickBoardForLink(node) {
|
|
3182
|
+
if (!state.filmHandle) { alert('Сначала откройте проект'); return; }
|
|
3183
|
+
const eps = await listEpisodes(state.filmHandle);
|
|
3184
|
+
const chars = await listCharacters(state.filmHandle).catch(() => []);
|
|
3185
|
+
const locs = await listLocations(state.filmHandle).catch(() => []);
|
|
3186
|
+
// Объединённый picker: «Сцены» сверху (основной кейс), потом chars/locs.
|
|
3187
|
+
// Excluding текущей доски — линковать саму на себя бессмысленно.
|
|
3188
|
+
const items = [];
|
|
3189
|
+
const curKey = state.currentBoard ? `${state.currentBoard.kind}::${state.currentBoard.name}` : '';
|
|
3190
|
+
for (const b of eps) if (`episode::${b.name}` !== curKey) items.push({ kind: 'episode', name: b.name, label: '🎬 ' + b.name });
|
|
3191
|
+
for (const b of chars) if (`character::${b.name}` !== curKey) items.push({ kind: 'character', name: b.name, label: '👤 ' + b.name });
|
|
3192
|
+
for (const b of locs) if (`location::${b.name}` !== curKey) items.push({ kind: 'location', name: b.name, label: '📍 ' + b.name });
|
|
3193
|
+
if (!items.length) { alert('Нет других досок для ссылки'); return; }
|
|
3194
|
+
const choice = await askChoice('Куда ведёт ссылка?', items.map(i => i.label), null, { vertical: true });
|
|
3195
|
+
if (!choice) return;
|
|
3196
|
+
const picked = items.find(i => i.label === choice);
|
|
3197
|
+
if (!picked) return;
|
|
3198
|
+
node.linkedBoard = { kind: picked.kind, name: picked.name };
|
|
3199
|
+
scheduleSave();
|
|
3200
|
+
const el = canvas.querySelector(`.node[data-id="${node.id}"]`);
|
|
3201
|
+
if (el) refreshNodeLinkBadge(el, node);
|
|
3202
|
+
}
|
|
3203
|
+
window.goToLinkedBoard = goToLinkedBoard;
|
|
3204
|
+
window.pickBoardForLink = pickBoardForLink;
|
|
3205
|
+
|
|
3147
3206
|
// Выделить ноду + проскроллить view так, чтобы она была в центре viewport'а.
|
|
3148
3207
|
// Юзер: «после создания ноды (любой) нужно её выделять и перемещаться к ней».
|
|
3149
3208
|
// Используется после addText/addLabel/genNode'а — иначе если spot оказался
|
|
@@ -3175,27 +3234,31 @@ function _autoScrollToNodesIfHidden(padX, padY) {
|
|
|
3175
3234
|
const nodes = board.metadata.nodes || [];
|
|
3176
3235
|
if (!nodes.length)
|
|
3177
3236
|
return;
|
|
3178
|
-
// Считаем bbox в canvas-coords.
|
|
3179
|
-
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
3180
|
-
for (const n of nodes) {
|
|
3181
|
-
const w = n.width || 280, h = n.height || 220;
|
|
3182
|
-
minX = Math.min(minX, n.x);
|
|
3183
|
-
minY = Math.min(minY, n.y);
|
|
3184
|
-
maxX = Math.max(maxX, n.x + w);
|
|
3185
|
-
maxY = Math.max(maxY, n.y + h);
|
|
3186
|
-
}
|
|
3187
3237
|
const z = state.zoom || 1;
|
|
3188
|
-
// Видимое окно в canvas-coords (viewport / zoom).
|
|
3189
3238
|
const wrap = canvasWrap;
|
|
3239
|
+
// Видимое окно в canvas-coords (viewport / zoom).
|
|
3190
3240
|
const viewLeft = (wrap.scrollLeft - padX) / z;
|
|
3191
3241
|
const viewTop = (wrap.scrollTop - padY) / z;
|
|
3192
3242
|
const viewRight = viewLeft + wrap.clientWidth / z;
|
|
3193
3243
|
const viewBottom = viewTop + wrap.clientHeight / z;
|
|
3194
|
-
//
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3244
|
+
// Раньше проверяли пересечение viewport'а с BBOX всех нод — для нод
|
|
3245
|
+
// в углах (0,0) и (5000,5000) bbox покрывал ВЕСЬ canvas → false-positive
|
|
3246
|
+
// «не trigger'им». Теперь спрашиваем: есть ли ХОТЯ БЫ ОДНА нода хоть
|
|
3247
|
+
// частично внутри viewport'а? Если нет — центрируем на bbox-у нод.
|
|
3248
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
3249
|
+
let anyVisible = false;
|
|
3250
|
+
for (const n of nodes) {
|
|
3251
|
+
const w = n.width || 280, h = n.height || 220;
|
|
3252
|
+
const nL = n.x, nR = n.x + w, nT = n.y, nB = n.y + h;
|
|
3253
|
+
minX = Math.min(minX, nL);
|
|
3254
|
+
minY = Math.min(minY, nT);
|
|
3255
|
+
maxX = Math.max(maxX, nR);
|
|
3256
|
+
maxY = Math.max(maxY, nB);
|
|
3257
|
+
if (!(nR < viewLeft || nL > viewRight || nB < viewTop || nT > viewBottom)) {
|
|
3258
|
+
anyVisible = true;
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
if (anyVisible) return;
|
|
3199
3262
|
const cx = (minX + maxX) / 2;
|
|
3200
3263
|
const cy = (minY + maxY) / 2;
|
|
3201
3264
|
wrap.scrollLeft = cx * z + padX - wrap.clientWidth / 2;
|
|
@@ -3772,9 +3835,25 @@ async function createNodeEl(node) {
|
|
|
3772
3835
|
el.appendChild(anchor);
|
|
3773
3836
|
attachAnchor(node, el, anchor);
|
|
3774
3837
|
attachDrag(el, node);
|
|
3838
|
+
// Link-badge: показывает 🔗 если у ноды есть linkedBoard (ссылка на сцену).
|
|
3839
|
+
// Сам badge кликабельный — переход выполняется также через dblclick по ноде.
|
|
3840
|
+
const linkBadge = document.createElement('div');
|
|
3841
|
+
linkBadge.className = 'link-badge';
|
|
3842
|
+
linkBadge.textContent = '🔗';
|
|
3843
|
+
el.appendChild(linkBadge);
|
|
3844
|
+
linkBadge.addEventListener('click', e => {
|
|
3845
|
+
e.stopPropagation();
|
|
3846
|
+
if (node.linkedBoard?.name) goToLinkedBoard(node);
|
|
3847
|
+
});
|
|
3848
|
+
refreshNodeLinkBadge(el, node);
|
|
3775
3849
|
el.addEventListener('dblclick', e => {
|
|
3776
3850
|
if (e.target.closest('textarea, input, video, button, .delete, .anchor, .resize-handle, [contenteditable]'))
|
|
3777
3851
|
return;
|
|
3852
|
+
// Если у ноды есть link на сцену — переход важнее остальных действий.
|
|
3853
|
+
if (node.linkedBoard?.name) {
|
|
3854
|
+
goToLinkedBoard(node);
|
|
3855
|
+
return;
|
|
3856
|
+
}
|
|
3778
3857
|
if (node.type === 'audio' && node.file) {
|
|
3779
3858
|
regenerateNode(node);
|
|
3780
3859
|
return;
|
|
@@ -3981,6 +4060,18 @@ function showNodeContextMenu(node, clientX, clientY) {
|
|
|
3981
4060
|
loadAllCharactersInfo().catch(() => { });
|
|
3982
4061
|
});
|
|
3983
4062
|
}
|
|
4063
|
+
// Link-to-scene
|
|
4064
|
+
if (node.linkedBoard?.name) {
|
|
4065
|
+
add(`🔗 Перейти → ${node.linkedBoard.name}`, () => goToLinkedBoard(node));
|
|
4066
|
+
add('⛓️💥 Убрать ссылку', () => {
|
|
4067
|
+
delete node.linkedBoard;
|
|
4068
|
+
scheduleSave();
|
|
4069
|
+
const el = canvas.querySelector(`.node[data-id="${node.id}"]`);
|
|
4070
|
+
if (el) refreshNodeLinkBadge(el, node);
|
|
4071
|
+
});
|
|
4072
|
+
} else {
|
|
4073
|
+
add('🔗 Сделать ссылку на сцену…', () => pickBoardForLink(node));
|
|
4074
|
+
}
|
|
3984
4075
|
add('⎘ Скопировать (Cmd+C)', () => copyNodeToClipboard(node));
|
|
3985
4076
|
add('✂ Вырезать (Cmd+X)', async () => {
|
|
3986
4077
|
await copyNodeToClipboard(node);
|
package/renderer/styles.css
CHANGED
|
@@ -110,6 +110,24 @@
|
|
|
110
110
|
width: 36px; height: 36px; flex-shrink: 0; object-fit: contain;
|
|
111
111
|
background: #1a1a1a; border-radius: 8px; padding: 4px;
|
|
112
112
|
}
|
|
113
|
+
/* Link-badge: значок 🔗 на ноде когда у неё есть linkedBoard. Лежит в
|
|
114
|
+
правом-верхнем углу, кликабельный (= dblclick: открыть target-сцену).
|
|
115
|
+
По умолчанию скрыт (display:none); refreshNodeLinkBadge показывает. */
|
|
116
|
+
.node .link-badge {
|
|
117
|
+
position: absolute; top: -10px; right: 8px;
|
|
118
|
+
width: 22px; height: 22px;
|
|
119
|
+
background: #2a3854; border: 1px solid #4a6a9a;
|
|
120
|
+
border-radius: 50%; cursor: pointer;
|
|
121
|
+
font-size: 12px; line-height: 20px; text-align: center;
|
|
122
|
+
box-shadow: 0 1px 4px rgba(0,0,0,0.4);
|
|
123
|
+
z-index: 5;
|
|
124
|
+
transition: background 0.12s, transform 0.12s;
|
|
125
|
+
}
|
|
126
|
+
.node .link-badge:hover { background: #3a5a8a; transform: scale(1.1); }
|
|
127
|
+
/* В drawing-нодах кружок поверх stroke'а смотрится плохо — смещаем
|
|
128
|
+
к левому-верху самой bbox'ы. */
|
|
129
|
+
.node.drawing-node .link-badge { top: 4px; right: 4px; }
|
|
130
|
+
|
|
113
131
|
/* Brand-logo и welcome-logo всегда кликабельны (на главной → ничего,
|
|
114
132
|
в проекте → возврат на welcome, dblclick → настройки). cursor:pointer
|
|
115
133
|
ставится и в HTML inline, но в web chatium-минифайер иногда срывает
|