kingkont 0.20.10 → 0.20.12

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.20.10",
3
+ "version": "0.20.12",
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,26 @@ $('newLocation').addEventListener('click', async () => {
3144
3144
  }
3145
3145
  });
3146
3146
  // =================== Board (универсально для серии и персонажа) ===================
3147
+ // Выделить ноду + проскроллить view так, чтобы она была в центре viewport'а.
3148
+ // Юзер: «после создания ноды (любой) нужно её выделять и перемещаться к ней».
3149
+ // Используется после addText/addLabel/genNode'а — иначе если spot оказался
3150
+ // за пределами текущего scroll'а, юзер не понимает что что-то создалось.
3151
+ function selectAndPanToNode(node) {
3152
+ if (!node || !state.currentBoard) return;
3153
+ try {
3154
+ clearSelection();
3155
+ state.selectedNodeIds.add(node.id);
3156
+ renderSelection();
3157
+ } catch {}
3158
+ const { padX, padY } = _getFramePadding();
3159
+ const z = state.zoom || 1;
3160
+ const w = node.width || 280, h = node.height || 220;
3161
+ const cx = node.x + w / 2;
3162
+ const cy = node.y + h / 2;
3163
+ canvasWrap.scrollLeft = cx * z + padX - canvasWrap.clientWidth / 2;
3164
+ canvasWrap.scrollTop = cy * z + padY - canvasWrap.clientHeight / 2;
3165
+ }
3166
+ window.selectAndPanToNode = selectAndPanToNode;
3147
3167
  // Если ни одна нода не попадает в видимую область canvas-wrap, скроллим
3148
3168
  // view на центр bbox всех нод. Используется после selectBoard / openFilm
3149
3169
  // — типичный кейс: юзер открывает старый проект, scroll был сохранён в
@@ -3155,27 +3175,31 @@ function _autoScrollToNodesIfHidden(padX, padY) {
3155
3175
  const nodes = board.metadata.nodes || [];
3156
3176
  if (!nodes.length)
3157
3177
  return;
3158
- // Считаем bbox в canvas-coords.
3159
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3160
- for (const n of nodes) {
3161
- const w = n.width || 280, h = n.height || 220;
3162
- minX = Math.min(minX, n.x);
3163
- minY = Math.min(minY, n.y);
3164
- maxX = Math.max(maxX, n.x + w);
3165
- maxY = Math.max(maxY, n.y + h);
3166
- }
3167
3178
  const z = state.zoom || 1;
3168
- // Видимое окно в canvas-coords (viewport / zoom).
3169
3179
  const wrap = canvasWrap;
3180
+ // Видимое окно в canvas-coords (viewport / zoom).
3170
3181
  const viewLeft = (wrap.scrollLeft - padX) / z;
3171
3182
  const viewTop = (wrap.scrollTop - padY) / z;
3172
3183
  const viewRight = viewLeft + wrap.clientWidth / z;
3173
3184
  const viewBottom = viewTop + wrap.clientHeight / z;
3174
- // Перекрытие? Если bbox нод хоть как-то пересекается с viewport не трогаем.
3175
- const intersects = !(maxX < viewLeft || minX > viewRight || maxY < viewTop || minY > viewBottom);
3176
- if (intersects)
3177
- return;
3178
- // Иначе центрируем на bbox.
3185
+ // Раньше проверяли пересечение viewport'а с BBOX всех ноддля нод
3186
+ // в углах (0,0) и (5000,5000) bbox покрывал ВЕСЬ canvas false-positive
3187
+ // «не trigger'им». Теперь спрашиваем: есть ли ХОТЯ БЫ ОДНА нода хоть
3188
+ // частично внутри viewport'а? Если нет — центрируем на bbox-у нод.
3189
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
3190
+ let anyVisible = false;
3191
+ for (const n of nodes) {
3192
+ const w = n.width || 280, h = n.height || 220;
3193
+ const nL = n.x, nR = n.x + w, nT = n.y, nB = n.y + h;
3194
+ minX = Math.min(minX, nL);
3195
+ minY = Math.min(minY, nT);
3196
+ maxX = Math.max(maxX, nR);
3197
+ maxY = Math.max(maxY, nB);
3198
+ if (!(nR < viewLeft || nL > viewRight || nB < viewTop || nT > viewBottom)) {
3199
+ anyVisible = true;
3200
+ }
3201
+ }
3202
+ if (anyVisible) return;
3179
3203
  const cx = (minX + maxX) / 2;
3180
3204
  const cy = (minY + maxY) / 2;
3181
3205
  wrap.scrollLeft = cx * z + padX - wrap.clientWidth / 2;
@@ -228,9 +228,18 @@ document.querySelectorAll('#addMenu button').forEach(btn => {
228
228
 
229
229
  async function addTextAt(pos) {
230
230
  if (!state.currentBoard) return;
231
- const rect = canvas.getBoundingClientRect();
232
- const x = pos ? (pos.clientX - rect.left) / state.zoom : canvasWrap.scrollLeft / state.zoom + 80;
233
- const y = pos ? (pos.clientY - rect.top) / state.zoom : canvasWrap.scrollTop / state.zoom + 80;
231
+ let x, y;
232
+ if (pos) {
233
+ const rect = canvas.getBoundingClientRect();
234
+ x = (pos.clientX - rect.left) / state.zoom;
235
+ y = (pos.clientY - rect.top) / state.zoom;
236
+ } else {
237
+ // Без позиции (клик по «Написать» в toolbar) — кладём в свободное место
238
+ // ВИДИМОЙ зоны. Раньше использовали scrollLeft+80 без учёта canvas-padding
239
+ // → нода уезжала за вьюпорт (padX=2000 в canvas-frame).
240
+ const spot = findFreeSpot();
241
+ x = spot.x; y = spot.y;
242
+ }
234
243
  const dir = await getOrCreateBoardSubdir(state.currentBoard.handle, 'texts');
235
244
  const mdName = await uniqueName(dir, 'text.md');
236
245
  await writeFile(dir, mdName, '');
@@ -238,6 +247,7 @@ async function addTextAt(pos) {
238
247
  state.currentBoard.metadata.nodes.push(node);
239
248
  canvas.appendChild(await createNodeEl(node));
240
249
  scheduleSave();
250
+ if (typeof selectAndPanToNode === 'function') selectAndPanToNode(node);
241
251
  }
242
252
 
243
253
  // Label-нода: лёгкая надпись поверх холста, без файла. Текст хранится
@@ -245,9 +255,15 @@ async function addTextAt(pos) {
245
255
  // удаляется при сериализации).
246
256
  async function addLabelAt(pos) {
247
257
  if (!state.currentBoard) return;
248
- const rect = canvas.getBoundingClientRect();
249
- const x = pos ? (pos.clientX - rect.left) / state.zoom : canvasWrap.scrollLeft / state.zoom + 80;
250
- const y = pos ? (pos.clientY - rect.top) / state.zoom : canvasWrap.scrollTop / state.zoom + 80;
258
+ let x, y;
259
+ if (pos) {
260
+ const rect = canvas.getBoundingClientRect();
261
+ x = (pos.clientX - rect.left) / state.zoom;
262
+ y = (pos.clientY - rect.top) / state.zoom;
263
+ } else {
264
+ const spot = findFreeSpot();
265
+ x = spot.x; y = spot.y;
266
+ }
251
267
  const node = {
252
268
  id: crypto.randomUUID(),
253
269
  type: 'label',
@@ -261,6 +277,7 @@ async function addLabelAt(pos) {
261
277
  const el = await createNodeEl(node);
262
278
  canvas.appendChild(el);
263
279
  scheduleSave();
280
+ if (typeof selectAndPanToNode === 'function') selectAndPanToNode(node);
264
281
  // Сразу входим в режим редактирования и выделяем весь текст —
265
282
  // юзер начинает печатать, и «Подпись» заменяется на введённое.
266
283
  setTimeout(() => {
@@ -524,6 +541,7 @@ $('textGenSubmit').addEventListener('click', async () => {
524
541
  canvas.appendChild(await createNodeEl(node));
525
542
  if (pendingFrom) addConnection(pendingFrom, node.id);
526
543
  scheduleSave();
544
+ if (typeof selectAndPanToNode === 'function') selectAndPanToNode(node);
527
545
  $('textGenModal').classList.add('hidden');
528
546
  // Фоновая генерация — модалку уже закрыли.
529
547
  runTextJob(node, resolvedPrompt, model, state.currentBoard.handle, state.currentBoard.key, imageRefs);
@@ -731,6 +749,7 @@ $('sfxSubmit').addEventListener('click', async () => {
731
749
  state.currentBoard.metadata.nodes.push(node);
732
750
  canvas.appendChild(await createNodeEl(node));
733
751
  scheduleSave();
752
+ if (typeof selectAndPanToNode === 'function') selectAndPanToNode(node);
734
753
  runSfxJob(node, text, durationSeconds, state.currentBoard.handle, state.currentBoard.key);
735
754
  });
736
755
 
@@ -863,6 +882,7 @@ $('musicSubmit').addEventListener('click', async () => {
863
882
  state.currentBoard.metadata.nodes.push(node);
864
883
  canvas.appendChild(await createNodeEl(node));
865
884
  scheduleSave();
885
+ if (typeof selectAndPanToNode === 'function') selectAndPanToNode(node);
866
886
  runMusicJob(node, prompt, durationMs, state.currentBoard.handle, state.currentBoard.key);
867
887
  });
868
888
 
@@ -1633,6 +1653,7 @@ $('genSubmit').addEventListener('click', async () => {
1633
1653
  addConnection(state.pendingConnectionFrom, node.id);
1634
1654
  state.pendingConnectionFrom = null;
1635
1655
  }
1656
+ if (typeof selectAndPanToNode === 'function') selectAndPanToNode(node);
1636
1657
  if (saveOnly) {
1637
1658
  scheduleSave();
1638
1659
  $('genModal').classList.add('hidden');
@@ -1785,6 +1806,7 @@ $('genSubmit').addEventListener('click', async () => {
1785
1806
  if (saveOnly) node.status = 'draft';
1786
1807
  state.currentBoard.metadata.nodes.push(node);
1787
1808
  canvas.appendChild(await createNodeEl(node));
1809
+ if (typeof selectAndPanToNode === 'function') selectAndPanToNode(node);
1788
1810
 
1789
1811
  // Async auto-name через LLM. НЕ блокируем — нода уже на холсте.
1790
1812
  // Когда LLM ответит — обновим node.name + DOM. Дедуп через uniqueNodeName.