@deepblend/dsh-blender-ui 0.3.4 → 0.3.6
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/lib/client.js +360 -20
- package/lib/index.js +10 -2
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -488,6 +488,34 @@ window.__ModuleLoader__.load({
|
|
|
488
488
|
'recipes.license': '作者与许可',
|
|
489
489
|
'recipes.unavailable': '部分配方无法载入;可继续使用下方可用的配方。',
|
|
490
490
|
'projects.creating': '创建中…',
|
|
491
|
+
'projects.incomplete': '尚未完成创建',
|
|
492
|
+
'projects.recovered': '已打开之前创建的 {id},未重复创建或渲染',
|
|
493
|
+
'projects.replyMissing': '未能收到创建结果。输入已保留;重试上次创建会检查同一次请求。',
|
|
494
|
+
'projects.environmentFailed': 'Blender 环境尚未就绪。输入已保留;请先检查安装和路径,修复后再重试。',
|
|
495
|
+
'projects.creationFailed': '未能确认创建结果。输入已保留;可重试上次创建,或刷新列表检查已保存的项目。',
|
|
496
|
+
'projects.retryCreation': '重试上次创建',
|
|
497
|
+
'projects.originalTitle': '上次创建:{title}',
|
|
498
|
+
'projects.drafts': '继续创建草稿',
|
|
499
|
+
'projects.draftHint': '创建草稿保存在此浏览器。恢复只填写输入,不会自动创建项目。',
|
|
500
|
+
'projects.draftSaved': '当前创建草稿已保存到此浏览器。',
|
|
501
|
+
'projects.draftUnavailable': '此浏览器暂时无法保存草稿。当前输入仍可使用,但重载或关闭页面后可能丢失。',
|
|
502
|
+
'projects.draftFull': '创建草稿已满。删除不用的浏览器草稿后可继续保存,当前输入仍保留。',
|
|
503
|
+
'projects.draftTooLarge': '当前创建草稿过大,无法保存到浏览器。当前输入仍保留。',
|
|
504
|
+
'projects.draftInvalid': '这份浏览器草稿无法读取,原记录没有被更改。',
|
|
505
|
+
'projects.draftMissingRecipe': '原配方 {id}@{version} 当前不可用。原创建请求仍可重试;新建作品前请选择当前配方。',
|
|
506
|
+
'projects.draftOldParameters': '原配方参数',
|
|
507
|
+
'projects.draftRestored': '已恢复创建草稿。检查输入后继续;上次请求需要明确重试。',
|
|
508
|
+
'projects.draftRestore': '继续这份草稿',
|
|
509
|
+
'projects.draftDelete': '删除浏览器草稿',
|
|
510
|
+
'projects.draftClear': '清空当前创建草稿',
|
|
511
|
+
'projects.draftUncertain': '创建结果尚未确认',
|
|
512
|
+
'projects.untitledDraft': '未命名创建草稿',
|
|
513
|
+
'projects.damagedDraft': '无法读取的创建草稿',
|
|
514
|
+
'projects.refreshList': '刷新项目列表',
|
|
515
|
+
'projects.creationDetails': '详细诊断',
|
|
516
|
+
'projects.environmentGuide': '查看环境检查指南',
|
|
517
|
+
'projects.creationConflict': '上次创建记录需要检查。输入已保留;请刷新列表并查看详细诊断,确认项目状态。',
|
|
518
|
+
'projects.hostNeedsRestart': '当前运行的 Host 尚不支持创建恢复。请更新 DeepBlend 并重启 DSH,输入仍保留在当前页面。',
|
|
491
519
|
'projects.createButton': '创建',
|
|
492
520
|
'scene.currentRevision': '当前 revision',
|
|
493
521
|
'scene.revisionCount': 'revision 数',
|
|
@@ -1025,6 +1053,34 @@ window.__ModuleLoader__.load({
|
|
|
1025
1053
|
'recipes.license': 'Author and license',
|
|
1026
1054
|
'recipes.unavailable': 'Some recipes could not be loaded. Available recipes are shown below.',
|
|
1027
1055
|
'projects.creating': 'Creating…',
|
|
1056
|
+
'projects.incomplete': 'Creation is not complete',
|
|
1057
|
+
'projects.recovered': 'Opened the previously created {id}; no duplicate creation or render',
|
|
1058
|
+
'projects.replyMissing': 'The creation result could not be received. Your inputs are kept; retry the previous creation to check the same request.',
|
|
1059
|
+
'projects.environmentFailed': 'Blender is not ready. Your inputs are kept; check its installation and path, then retry.',
|
|
1060
|
+
'projects.creationFailed': 'The creation result could not be confirmed. Your inputs are kept; retry the previous creation, or refresh the list to check saved projects.',
|
|
1061
|
+
'projects.retryCreation': 'Retry previous creation',
|
|
1062
|
+
'projects.originalTitle': 'Previous creation: {title}',
|
|
1063
|
+
'projects.drafts': 'Continue a creation draft',
|
|
1064
|
+
'projects.draftHint': 'Creation drafts stay in this browser. Restoring fills the inputs and does not create a project automatically.',
|
|
1065
|
+
'projects.draftSaved': 'The current creation draft is saved in this browser.',
|
|
1066
|
+
'projects.draftUnavailable': 'This browser cannot save the draft right now. Current inputs still work, but reloading or closing the page may lose them.',
|
|
1067
|
+
'projects.draftFull': 'Creation draft storage is full. Delete unused browser drafts to save more; current inputs remain.',
|
|
1068
|
+
'projects.draftTooLarge': 'This creation draft is too large to save in the browser. Current inputs remain.',
|
|
1069
|
+
'projects.draftInvalid': 'This browser draft cannot be read. Its original record has not been changed.',
|
|
1070
|
+
'projects.draftMissingRecipe': 'Recipe {id}@{version} is currently unavailable. You can still retry its original creation request; choose a current recipe before starting a new project.',
|
|
1071
|
+
'projects.draftOldParameters': 'Original recipe parameters',
|
|
1072
|
+
'projects.draftRestored': 'Creation draft restored. Check the inputs before continuing; retry the previous request explicitly.',
|
|
1073
|
+
'projects.draftRestore': 'Continue this draft',
|
|
1074
|
+
'projects.draftDelete': 'Delete browser draft',
|
|
1075
|
+
'projects.draftClear': 'Clear current creation draft',
|
|
1076
|
+
'projects.draftUncertain': 'Creation result unconfirmed',
|
|
1077
|
+
'projects.untitledDraft': 'Untitled creation draft',
|
|
1078
|
+
'projects.damagedDraft': 'Unreadable creation draft',
|
|
1079
|
+
'projects.refreshList': 'Refresh project list',
|
|
1080
|
+
'projects.creationDetails': 'Diagnostic details',
|
|
1081
|
+
'projects.environmentGuide': 'Open the environment check guide',
|
|
1082
|
+
'projects.creationConflict': 'The previous creation record needs inspection. Your inputs are kept; refresh the list and read the diagnostic details to check its state.',
|
|
1083
|
+
'projects.hostNeedsRestart': 'The running Host does not support creation recovery. Update DeepBlend and restart DSH; your inputs remain on this page.',
|
|
1028
1084
|
'projects.createButton': 'Create',
|
|
1029
1085
|
'scene.currentRevision': 'current revision',
|
|
1030
1086
|
'scene.revisionCount': 'revisions',
|
|
@@ -1267,6 +1323,7 @@ window.__ModuleLoader__.load({
|
|
|
1267
1323
|
const PANEL_LABEL = 'Blender'
|
|
1268
1324
|
/** The host API this half was written against; a mismatch is a deployment state. */
|
|
1269
1325
|
const EXPECTED_HOST_API = 6
|
|
1326
|
+
const CREATION_REQUEST_PROTOCOL = 'deepblend.creation-request/v1'
|
|
1270
1327
|
/** Polling cadence while something is live; M3 writes progress once a second. */
|
|
1271
1328
|
const POLL_LIVE_MS = 1500
|
|
1272
1329
|
/** Polling cadence when nothing is running. */
|
|
@@ -1329,6 +1386,92 @@ window.__ModuleLoader__.load({
|
|
|
1329
1386
|
return `${base}${path}?v=${version}`
|
|
1330
1387
|
}
|
|
1331
1388
|
|
|
1389
|
+
const creationSignature = body => JSON.stringify(body, (_, value) => value && typeof value === 'object' && !Array.isArray(value)
|
|
1390
|
+
? Object.fromEntries(Object.keys(value).sort().map(key => [key, value[key]])) : value)
|
|
1391
|
+
let creationKeySequence = 0
|
|
1392
|
+
function newCreationKey() {
|
|
1393
|
+
const bytes = new Uint32Array(4)
|
|
1394
|
+
if (globalThis.crypto?.getRandomValues) globalThis.crypto.getRandomValues(bytes)
|
|
1395
|
+
else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 4294967296)
|
|
1396
|
+
return `create-${Date.now().toString(36)}-${(++creationKeySequence).toString(36)}-${[...bytes].map(n => n.toString(16).padStart(8, '0')).join('')}`
|
|
1397
|
+
}
|
|
1398
|
+
const CREATION_DRAFT_VERSION = 'deepblend.creation-draft/v1'
|
|
1399
|
+
const CREATION_DRAFT_BYTES = 262144
|
|
1400
|
+
const creationDraftPrefix = root => `${CREATION_DRAFT_VERSION}:${encodeURIComponent(root)}:`
|
|
1401
|
+
const creationRecipeReference = recipe => recipe ? { id: recipe.id, version: recipe.version, digest: recipe.digest } : null
|
|
1402
|
+
function creationDraftJson(value) {
|
|
1403
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return
|
|
1404
|
+
if (typeof value === 'number' && Number.isFinite(value)) return
|
|
1405
|
+
if (!value || typeof value !== 'object') throw new Error('Draft input is not JSON')
|
|
1406
|
+
for (const item of Object.values(value)) creationDraftJson(item)
|
|
1407
|
+
}
|
|
1408
|
+
function creationDraftForms(forms) {
|
|
1409
|
+
const snapshot = { title: forms.title, goal: forms.goal,
|
|
1410
|
+
recipe: creationRecipeReference(forms.recipe), recipeParameters: forms.recipeParameters }
|
|
1411
|
+
creationDraftJson(snapshot)
|
|
1412
|
+
return editorClone(snapshot)
|
|
1413
|
+
}
|
|
1414
|
+
const creationRequestBody = forms => editorClone({ title: forms.title.trim(),
|
|
1415
|
+
goal: forms.goal.length > 0 ? forms.goal : undefined, renderPreview: Boolean(forms.recipe),
|
|
1416
|
+
recipe: forms.recipe ? { ...creationRecipeReference(forms.recipe), parameters: forms.recipeParameters } : undefined })
|
|
1417
|
+
function validCreationRecipeReference(recipe) {
|
|
1418
|
+
return recipe === null || Boolean(recipe && typeof recipe === 'object' && !Array.isArray(recipe)
|
|
1419
|
+
&& typeof recipe.id === 'string' && recipe.id && typeof recipe.version === 'string' && recipe.version
|
|
1420
|
+
&& /^[a-f0-9]{64}$/.test(recipe.digest || ''))
|
|
1421
|
+
}
|
|
1422
|
+
function parseCreationDraft(raw, root, id) {
|
|
1423
|
+
try {
|
|
1424
|
+
if (typeof raw !== 'string' || new Blob([raw]).size > CREATION_DRAFT_BYTES) return null
|
|
1425
|
+
const draft = JSON.parse(raw), forms = draft.forms
|
|
1426
|
+
creationDraftJson(forms)
|
|
1427
|
+
if (draft.schemaVersion !== CREATION_DRAFT_VERSION || draft.projectsRoot !== root || draft.id !== id
|
|
1428
|
+
|| (draft.ownerActive !== undefined && typeof draft.ownerActive !== 'boolean')
|
|
1429
|
+
|| !Number.isFinite(Date.parse(draft.savedAt)) || !forms || typeof forms.title !== 'string'
|
|
1430
|
+
|| typeof forms.goal !== 'string' || !validCreationRecipeReference(forms.recipe)
|
|
1431
|
+
|| Object.keys(forms).some(key => !['title', 'goal', 'recipe', 'recipeParameters'].includes(key))
|
|
1432
|
+
|| !forms.recipeParameters || typeof forms.recipeParameters !== 'object' || Array.isArray(forms.recipeParameters)) return null
|
|
1433
|
+
const attempt = draft.attempt
|
|
1434
|
+
if (attempt !== null) {
|
|
1435
|
+
const body = attempt?.body
|
|
1436
|
+
creationDraftJson(body)
|
|
1437
|
+
if (!attempt || typeof attempt.key !== 'string' || !attempt.key.trim() || attempt.key.length > 128 || /[\x00-\x1f\x7f]/.test(attempt.key)
|
|
1438
|
+
|| !body || typeof body.title !== 'string' || !body.title.trim() || typeof body.renderPreview !== 'boolean'
|
|
1439
|
+
|| (body.goal !== undefined && typeof body.goal !== 'string')
|
|
1440
|
+
|| (body.recipe !== undefined && (!body.recipe || !validCreationRecipeReference(body.recipe)
|
|
1441
|
+
|| !body.recipe.parameters || typeof body.recipe.parameters !== 'object' || Array.isArray(body.recipe.parameters)))
|
|
1442
|
+
|| body.renderPreview !== Boolean(body.recipe)
|
|
1443
|
+
|| Object.keys(body).some(key => !['title', 'goal', 'recipe', 'renderPreview'].includes(key))
|
|
1444
|
+
|| attempt.signature !== creationSignature(body)) return null
|
|
1445
|
+
}
|
|
1446
|
+
return draft
|
|
1447
|
+
} catch { return null }
|
|
1448
|
+
}
|
|
1449
|
+
function creationDraftStorage(settings) {
|
|
1450
|
+
try { return settings.creationDraftStorage ?? globalThis.localStorage } catch { return undefined }
|
|
1451
|
+
}
|
|
1452
|
+
function readCreationDrafts(storage, root) {
|
|
1453
|
+
if (!root) return { entries: [], available: false }
|
|
1454
|
+
try {
|
|
1455
|
+
if (!storage || typeof storage.key !== 'function' || !Number.isSafeInteger(storage.length)) return { entries: [], available: false }
|
|
1456
|
+
const prefix = creationDraftPrefix(root), entries = []
|
|
1457
|
+
for (let index = 0; index < storage.length; index++) {
|
|
1458
|
+
const key = storage.key(index)
|
|
1459
|
+
if (typeof key !== 'string' || !key.startsWith(prefix)) continue
|
|
1460
|
+
const id = key.slice(prefix.length), raw = storage.getItem(key), draft = parseCreationDraft(raw, root, id)
|
|
1461
|
+
entries.push({ id, raw, draft })
|
|
1462
|
+
}
|
|
1463
|
+
entries.sort((a, b) => String(b.draft?.savedAt || '').localeCompare(String(a.draft?.savedAt || '')))
|
|
1464
|
+
return { entries, available: true }
|
|
1465
|
+
} catch { return { entries: [], available: false } }
|
|
1466
|
+
}
|
|
1467
|
+
function creationErrorMessage(code) {
|
|
1468
|
+
if (code === 'UI_FETCH_FAILED' || /^HTTP_/.test(code)) return t('projects.replyMissing')
|
|
1469
|
+
if (code === 'BLENDER_NOT_FOUND' || code === 'RUNTIME_UNAVAILABLE') return t('projects.environmentFailed')
|
|
1470
|
+
if (code === 'CREATION_REQUEST_CONFLICT') return t('projects.creationConflict')
|
|
1471
|
+
if (code === 'UI_HOST_API_STALE') return t('projects.hostNeedsRestart')
|
|
1472
|
+
return t('projects.creationFailed')
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1332
1475
|
const imageDownloadKey = (projectId, artifact) => JSON.stringify([projectId, artifact?.path, artifact?.sha256])
|
|
1333
1476
|
function imageDownloadValid(artifact) {
|
|
1334
1477
|
return Boolean(artifact && typeof artifact.path === 'string' && /\.png$/i.test(artifact.path)
|
|
@@ -2436,6 +2579,11 @@ window.__ModuleLoader__.load({
|
|
|
2436
2579
|
diffError: null,
|
|
2437
2580
|
previewBusy: false,
|
|
2438
2581
|
imageDownloads: {},
|
|
2582
|
+
creationWork: null,
|
|
2583
|
+
creationRequestProtocol: null,
|
|
2584
|
+
creationDrafts: [],
|
|
2585
|
+
creationDraftStorageStatus: null,
|
|
2586
|
+
creationDraftMissingRecipe: null,
|
|
2439
2587
|
inspectionForms: {},
|
|
2440
2588
|
inspectionWork: {},
|
|
2441
2589
|
inspectionRevisions: {},
|
|
@@ -2496,6 +2644,12 @@ window.__ModuleLoader__.load({
|
|
|
2496
2644
|
let tick = 0
|
|
2497
2645
|
let live = false
|
|
2498
2646
|
let loadSequence = 0
|
|
2647
|
+
let creationAttempt = null
|
|
2648
|
+
const draftStorage = creationDraftStorage(settings)
|
|
2649
|
+
let creationDraftId = newCreationKey(), creationDraftRoot = null, persistedCreationSignature = null
|
|
2650
|
+
let restoredCreationSource = null, creationDraftWriting = false
|
|
2651
|
+
let creationDraftClosed = false
|
|
2652
|
+
const draftLifecycle = settings.creationDraftLifecycle ?? globalThis.window
|
|
2499
2653
|
let fileChoiceSignature = null, fileChoicesStopped = false
|
|
2500
2654
|
const photographyControllers = new Map()
|
|
2501
2655
|
const photographyWork = (projectId, changes) => set({ photographyWork: { ...data.photographyWork, [projectId]: { ...data.photographyWork[projectId], ...changes } } })
|
|
@@ -2539,6 +2693,7 @@ window.__ModuleLoader__.load({
|
|
|
2539
2693
|
const assetWork = (projectId, changes) => set({ assetWork: { ...data.assetWork, [projectId]: { ...data.assetWork[projectId], ...changes } } })
|
|
2540
2694
|
|
|
2541
2695
|
const notify = () => {
|
|
2696
|
+
persistCreationDraft()
|
|
2542
2697
|
const signature = JSON.stringify([data.activeProjectId, data.selected?.scene?.revision, data.view, ...FILE_FIELDS.map(field => fileChoiceAllowed(data, field))])
|
|
2543
2698
|
if (signature !== fileChoiceSignature) { data.fileChoiceGeneration += 1; fileChoiceSignature = signature }
|
|
2544
2699
|
snapshot = { ...data, forms: { ...data.forms }, busy: { ...data.busy }, notices: { ...data.notices } }
|
|
@@ -2549,8 +2704,58 @@ window.__ModuleLoader__.load({
|
|
|
2549
2704
|
/** Change one nested table. @param {'forms'|'busy'|'notices'} table @param {string} key @param {any} value */
|
|
2550
2705
|
const setIn = (table, key, value) => { data = { ...data, [table]: { ...data[table], [key]: value } }; notify() }
|
|
2551
2706
|
|
|
2707
|
+
function refreshCreationDrafts() {
|
|
2708
|
+
const reading = readCreationDrafts(draftStorage, creationDraftRoot)
|
|
2709
|
+
data.creationDrafts = reading.entries.filter(entry => entry.id !== creationDraftId).map(entry => ({ id: entry.id,
|
|
2710
|
+
title: entry.draft?.forms.title || t('projects.untitledDraft'), savedAt: entry.draft?.savedAt || null,
|
|
2711
|
+
uncertain: Boolean(entry.draft?.attempt), invalid: !entry.draft }))
|
|
2712
|
+
return reading
|
|
2713
|
+
}
|
|
2714
|
+
function persistCreationDraft() {
|
|
2715
|
+
if (creationDraftClosed || creationDraftWriting || !creationDraftRoot || data.projectsRoot !== creationDraftRoot) return
|
|
2716
|
+
creationDraftWriting = true
|
|
2717
|
+
try {
|
|
2718
|
+
const forms = { ...creationDraftForms(data.forms), recipe: creationRecipeReference(data.forms.recipe) || data.creationDraftMissingRecipe }, signature = JSON.stringify([forms, creationAttempt])
|
|
2719
|
+
if (signature === persistedCreationSignature) return
|
|
2720
|
+
const reading = refreshCreationDrafts()
|
|
2721
|
+
if (!reading.available) { data.creationDraftStorageStatus = 'unavailable'; return }
|
|
2722
|
+
const key = creationDraftPrefix(creationDraftRoot) + creationDraftId
|
|
2723
|
+
if (!forms.title && !forms.goal && !forms.recipe && !Object.keys(forms.recipeParameters).length && !creationAttempt) {
|
|
2724
|
+
draftStorage.removeItem(key); data.creationDraftStorageStatus = null
|
|
2725
|
+
} else {
|
|
2726
|
+
if (!reading.entries.some(entry => entry.id === creationDraftId) && reading.entries.length >= 20) {
|
|
2727
|
+
data.creationDraftStorageStatus = 'full'; return
|
|
2728
|
+
}
|
|
2729
|
+
const raw = JSON.stringify({ schemaVersion: CREATION_DRAFT_VERSION, projectsRoot: creationDraftRoot,
|
|
2730
|
+
id: creationDraftId, ownerActive: true, savedAt: new Date().toISOString(), forms, attempt: creationAttempt })
|
|
2731
|
+
if (new Blob([raw]).size > CREATION_DRAFT_BYTES) { data.creationDraftStorageStatus = 'tooLarge'; return }
|
|
2732
|
+
draftStorage.setItem(key, raw)
|
|
2733
|
+
if (draftStorage.getItem(key) !== raw) throw new Error('Draft write did not persist')
|
|
2734
|
+
data.creationDraftStorageStatus = 'saved'
|
|
2735
|
+
}
|
|
2736
|
+
persistedCreationSignature = signature
|
|
2737
|
+
refreshCreationDrafts()
|
|
2738
|
+
} catch { data.creationDraftStorageStatus = 'unavailable' }
|
|
2739
|
+
finally { creationDraftWriting = false }
|
|
2740
|
+
}
|
|
2741
|
+
function closeCreationDraft() {
|
|
2742
|
+
if (creationDraftClosed) return
|
|
2743
|
+
persistCreationDraft()
|
|
2744
|
+
creationDraftClosed = true
|
|
2745
|
+
if (!creationDraftRoot) return
|
|
2746
|
+
try {
|
|
2747
|
+
const key = creationDraftPrefix(creationDraftRoot) + creationDraftId
|
|
2748
|
+
const saved = parseCreationDraft(draftStorage?.getItem(key), creationDraftRoot, creationDraftId)
|
|
2749
|
+
if (saved) draftStorage.setItem(key, JSON.stringify({ ...saved, ownerActive: false }))
|
|
2750
|
+
} catch { /* Current inputs were already saved or visibly reported as unsaved. */ }
|
|
2751
|
+
}
|
|
2752
|
+
function reopenCreationDraft() {
|
|
2753
|
+
creationDraftClosed = false; persistedCreationSignature = null
|
|
2754
|
+
persistCreationDraft()
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2552
2757
|
/** The project every per-project route is addressed to. */
|
|
2553
|
-
const target = () => data.projectId ??
|
|
2758
|
+
const target = () => data.projectId ?? data.projects.find(project => !project.creationPending)?.projectId ?? null
|
|
2554
2759
|
|
|
2555
2760
|
/** Read everything the panel shows, in one pass. */
|
|
2556
2761
|
const load = async () => {
|
|
@@ -2564,7 +2769,8 @@ window.__ModuleLoader__.load({
|
|
|
2564
2769
|
return
|
|
2565
2770
|
}
|
|
2566
2771
|
const payload = stateResult.payload
|
|
2567
|
-
const
|
|
2772
|
+
const pending = payload.projects?.some(project => project.projectId === projectId && project.creationPending)
|
|
2773
|
+
const active = pending ? null : projectId ?? payload.projects?.find(project => !project.creationPending)?.projectId ?? null
|
|
2568
2774
|
const patch = {
|
|
2569
2775
|
status: 'ok',
|
|
2570
2776
|
error: null,
|
|
@@ -2572,6 +2778,7 @@ window.__ModuleLoader__.load({
|
|
|
2572
2778
|
projects: payload.projects || [],
|
|
2573
2779
|
recipeCatalog: payload.recipeCatalog ?? { recipes: [], errors: [] },
|
|
2574
2780
|
projectsRoot: payload.projectsRoot ?? null,
|
|
2781
|
+
creationRequestProtocol: payload.creationRequestProtocol ?? null,
|
|
2575
2782
|
selected: payload.selected ?? null,
|
|
2576
2783
|
activeProjectId: active,
|
|
2577
2784
|
currentRevision: payload.selected ? payload.selected.currentRevision : null,
|
|
@@ -2621,6 +2828,17 @@ window.__ModuleLoader__.load({
|
|
|
2621
2828
|
const next = { ...comparison, afterPreviews: editorClone(afterPreviews) }
|
|
2622
2829
|
patch.editorComparison = { ...next, ...editorPreviewPair(next), resolved: afterPreviews.length > 0 }
|
|
2623
2830
|
}
|
|
2831
|
+
if (patch.projectsRoot !== creationDraftRoot) {
|
|
2832
|
+
// Switching stores cannot carry an old store's request identity into a new one.
|
|
2833
|
+
if (creationDraftRoot !== null) {
|
|
2834
|
+
creationAttempt = null; restoredCreationSource = null; creationDraftId = newCreationKey()
|
|
2835
|
+
patch.creationWork = null; patch.creationDraftMissingRecipe = null
|
|
2836
|
+
patch.forms = { ...data.forms, title: '', goal: '', recipe: null, recipeParameters: {} }
|
|
2837
|
+
}
|
|
2838
|
+
creationDraftRoot = typeof patch.projectsRoot === 'string' && patch.projectsRoot ? patch.projectsRoot : null
|
|
2839
|
+
persistedCreationSignature = null
|
|
2840
|
+
}
|
|
2841
|
+
refreshCreationDrafts()
|
|
2624
2842
|
set(patch)
|
|
2625
2843
|
arm()
|
|
2626
2844
|
}
|
|
@@ -2637,6 +2855,51 @@ window.__ModuleLoader__.load({
|
|
|
2637
2855
|
/** A write just happened: re-run every read, including the view-specific ones. */
|
|
2638
2856
|
const reload = () => { tick += 1; void load() }
|
|
2639
2857
|
|
|
2858
|
+
const creationBody = () => creationRequestBody(data.forms)
|
|
2859
|
+
|
|
2860
|
+
async function submitCreation(attempt) {
|
|
2861
|
+
if (data.creationRequestProtocol !== CREATION_REQUEST_PROTOCOL) {
|
|
2862
|
+
set({ creationWork: { status: 'error', title: attempt.body.title, code: 'UI_HOST_API_STALE' } })
|
|
2863
|
+
setIn('notices', 'projects', { kind: 'creation', ok: false, message: t('projects.hostNeedsRestart') })
|
|
2864
|
+
return
|
|
2865
|
+
}
|
|
2866
|
+
setIn('busy', 'create', true)
|
|
2867
|
+
const attemptRoot = creationDraftRoot
|
|
2868
|
+
set({ creationWork: { status: 'busy', title: attempt.body.title }, previewResult: null })
|
|
2869
|
+
setIn('notices', 'projects', null)
|
|
2870
|
+
setIn('notices', 'preview', null)
|
|
2871
|
+
const outcome = await postJson(fetchImpl, ROUTES.projects, { ...editorClone(attempt.body), creationKey: attempt.key })
|
|
2872
|
+
setIn('busy', 'create', false)
|
|
2873
|
+
if (attemptRoot !== creationDraftRoot) { reload(); return }
|
|
2874
|
+
if (outcome.ok) {
|
|
2875
|
+
const sameInputs = creationSignature(creationBody()) === attempt.signature
|
|
2876
|
+
set({ ...(sameInputs ? { forms: { ...data.forms, title: '', goal: '', recipe: null, recipeParameters: {} } } : {}),
|
|
2877
|
+
creationWork: null, projectId: outcome.payload.project.projectId,
|
|
2878
|
+
...(attempt.body.renderPreview ? { view: 'preview', compareMode: 'result' } : {}) })
|
|
2879
|
+
if (creationAttempt === attempt) creationAttempt = null
|
|
2880
|
+
if (restoredCreationSource && creationDraftRoot) {
|
|
2881
|
+
try {
|
|
2882
|
+
const sourceKey = creationDraftPrefix(creationDraftRoot) + restoredCreationSource.id
|
|
2883
|
+
const source = parseCreationDraft(restoredCreationSource.raw, creationDraftRoot, restoredCreationSource.id)
|
|
2884
|
+
if (source && creationSignature(creationRequestBody(source.forms)) === attempt.signature
|
|
2885
|
+
&& draftStorage?.getItem(sourceKey) === restoredCreationSource.raw) draftStorage.removeItem(sourceKey)
|
|
2886
|
+
} catch { data.creationDraftStorageStatus = 'unavailable' }
|
|
2887
|
+
}
|
|
2888
|
+
restoredCreationSource = null
|
|
2889
|
+
persistedCreationSignature = null
|
|
2890
|
+
setIn('notices', attempt.body.renderPreview ? 'preview' : 'projects', { kind: 'creation', projectId: outcome.payload.project.projectId, ok: true, message: outcome.payload.project.creationReplayed
|
|
2891
|
+
? t('projects.recovered', { id: outcome.payload.project.projectId })
|
|
2892
|
+
: t('projects.created', { id: outcome.payload.project.projectId }) })
|
|
2893
|
+
} else {
|
|
2894
|
+
set({ creationWork: { status: 'error', title: attempt.body.title, code: outcome.error.code } })
|
|
2895
|
+
setIn('notices', 'projects', { kind: 'creation', ok: false, message: creationErrorMessage(outcome.error.code),
|
|
2896
|
+
technicalDetails: `${outcome.error.code}: ${outcome.error.message}` })
|
|
2897
|
+
}
|
|
2898
|
+
// A failed response may follow a successful Host commit. Refreshing is
|
|
2899
|
+
// read-only; retries remain explicit and reuse the captured request key.
|
|
2900
|
+
reload()
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2640
2903
|
/** Run one write, report it in this view's notice, then reload. */
|
|
2641
2904
|
const write = async (view, path, body, describe) => {
|
|
2642
2905
|
const outcome = await postJson(fetchImpl, path, body)
|
|
@@ -2934,7 +3197,7 @@ window.__ModuleLoader__.load({
|
|
|
2934
3197
|
} else assetWork(projectId, { error: `${outcome.error.code}: ${outcome.error.message}` })
|
|
2935
3198
|
reload()
|
|
2936
3199
|
},
|
|
2937
|
-
selectRecipe: recipe => set({ forms: { ...data.forms, recipe,
|
|
3200
|
+
selectRecipe: recipe => set({ creationDraftMissingRecipe: null, forms: { ...data.forms, recipe,
|
|
2938
3201
|
recipeParameters: recipe ? Object.fromEntries(recipe.parameters.map(parameter => [parameter.id, parameter.default])) : {},
|
|
2939
3202
|
title: !data.forms.title || data.forms.title === data.forms.recipe?.title ? recipe?.title || '' : data.forms.title,
|
|
2940
3203
|
} }),
|
|
@@ -3281,25 +3544,49 @@ window.__ModuleLoader__.load({
|
|
|
3281
3544
|
|
|
3282
3545
|
createProject: async () => {
|
|
3283
3546
|
if (data.busy.create) return
|
|
3547
|
+
if (data.creationDraftMissingRecipe) return
|
|
3284
3548
|
if (!recipeSelectionCurrent(data) || !recipeValuesValid(data.forms.recipe, data.forms.recipeParameters)) return
|
|
3285
|
-
const
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
if (
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3549
|
+
const body = creationBody()
|
|
3550
|
+
const signature = creationSignature(body)
|
|
3551
|
+
if (!creationAttempt || creationAttempt.signature !== signature) creationAttempt = { body, signature, key: newCreationKey() }
|
|
3552
|
+
return submitCreation(creationAttempt)
|
|
3553
|
+
},
|
|
3554
|
+
retryCreation: () => data.busy.create || !creationAttempt ? undefined : submitCreation(creationAttempt),
|
|
3555
|
+
restoreCreationDraft: id => {
|
|
3556
|
+
if (data.busy.create || !creationDraftRoot) return
|
|
3557
|
+
const reading = readCreationDrafts(draftStorage, creationDraftRoot), source = reading.entries.find(entry => entry.id === id)
|
|
3558
|
+
if (!source?.draft) { set({ creationDraftStorageStatus: 'invalid' }); return }
|
|
3559
|
+
const saved = source.draft, ref = saved.forms.recipe
|
|
3560
|
+
const recipe = ref ? data.recipeCatalog.recipes.find(recipe => recipe.id === ref.id && recipe.version === ref.version && recipe.digest === ref.digest) : null
|
|
3561
|
+
creationDraftId = newCreationKey(); restoredCreationSource = { id, raw: source.raw }; persistedCreationSignature = null
|
|
3562
|
+
creationAttempt = saved.attempt ? editorClone(saved.attempt) : null
|
|
3563
|
+
set({ view: 'projects', forms: { ...data.forms, ...editorClone(saved.forms), recipe: recipe || null },
|
|
3564
|
+
creationDraftMissingRecipe: ref && !recipe ? ref : null,
|
|
3565
|
+
creationWork: creationAttempt ? { status: 'error', title: creationAttempt.body.title, code: 'UI_FETCH_FAILED' } : null,
|
|
3566
|
+
notices: { ...data.notices, projects: { kind: 'creation', ok: true, message: t('projects.draftRestored') } } })
|
|
3567
|
+
// A closed page's unchanged record can be consumed after the copy
|
|
3568
|
+
// persisted. Live pages retain their independent saved record.
|
|
3569
|
+
if (saved.ownerActive === false && data.creationDraftStorageStatus === 'saved') {
|
|
3570
|
+
try {
|
|
3571
|
+
const sourceKey = creationDraftPrefix(creationDraftRoot) + id
|
|
3572
|
+
if (draftStorage.getItem(sourceKey) === source.raw) draftStorage.removeItem(sourceKey)
|
|
3573
|
+
refreshCreationDrafts(); notify()
|
|
3574
|
+
} catch { set({ creationDraftStorageStatus: 'unavailable' }) }
|
|
3301
3575
|
}
|
|
3302
3576
|
},
|
|
3577
|
+
deleteCreationDraft: id => {
|
|
3578
|
+
if (!creationDraftRoot || id === creationDraftId) return
|
|
3579
|
+
const entry = readCreationDrafts(draftStorage, creationDraftRoot).entries.find(entry => entry.id === id)
|
|
3580
|
+
if (!entry) return
|
|
3581
|
+
try { draftStorage.removeItem(creationDraftPrefix(creationDraftRoot) + id); refreshCreationDrafts(); notify() }
|
|
3582
|
+
catch { set({ creationDraftStorageStatus: 'unavailable' }) }
|
|
3583
|
+
},
|
|
3584
|
+
clearCreationDraft: () => {
|
|
3585
|
+
if (data.busy.create) return
|
|
3586
|
+
creationAttempt = null; restoredCreationSource = null; persistedCreationSignature = null
|
|
3587
|
+
set({ forms: { ...data.forms, title: '', goal: '', recipe: null, recipeParameters: {} }, creationWork: null,
|
|
3588
|
+
creationDraftMissingRecipe: null, notices: { ...data.notices, projects: null } })
|
|
3589
|
+
},
|
|
3303
3590
|
|
|
3304
3591
|
applyPatch: async () => {
|
|
3305
3592
|
const projectId = data.activeProjectId
|
|
@@ -3365,6 +3652,7 @@ window.__ModuleLoader__.load({
|
|
|
3365
3652
|
},
|
|
3366
3653
|
|
|
3367
3654
|
renderPreview: async () => {
|
|
3655
|
+
setIn('notices', 'preview', null)
|
|
3368
3656
|
set({ previewBusy: true, previewResult: null })
|
|
3369
3657
|
const outcome = await postJson(fetchImpl, projectRoute(target(), '/preview'), { revision: data.selected?.scene?.revision })
|
|
3370
3658
|
set({ previewBusy: false, ...(outcome.ok ? { compareMode: 'renders' } : {}) })
|
|
@@ -3442,10 +3730,16 @@ window.__ModuleLoader__.load({
|
|
|
3442
3730
|
start() {
|
|
3443
3731
|
if (running) return
|
|
3444
3732
|
running = true
|
|
3733
|
+
reopenCreationDraft()
|
|
3734
|
+
draftLifecycle?.addEventListener?.('pagehide', closeCreationDraft)
|
|
3735
|
+
draftLifecycle?.addEventListener?.('pageshow', reopenCreationDraft)
|
|
3445
3736
|
fileChoicesStopped = false
|
|
3446
3737
|
void load()
|
|
3447
3738
|
},
|
|
3448
3739
|
stop() {
|
|
3740
|
+
closeCreationDraft()
|
|
3741
|
+
draftLifecycle?.removeEventListener?.('pagehide', closeCreationDraft)
|
|
3742
|
+
draftLifecycle?.removeEventListener?.('pageshow', reopenCreationDraft)
|
|
3449
3743
|
fileChoicesStopped = true
|
|
3450
3744
|
data = { ...data, fileChoiceGeneration: data.fileChoiceGeneration + 1 }
|
|
3451
3745
|
running = false
|
|
@@ -3475,6 +3769,35 @@ window.__ModuleLoader__.load({
|
|
|
3475
3769
|
// touches React, the DOM, or the network.
|
|
3476
3770
|
|
|
3477
3771
|
/** 项目: the list, the create form, and the selected project's summary. */
|
|
3772
|
+
function CreationDraftsView({ state, actions }) {
|
|
3773
|
+
const messages = { saved: t('projects.draftSaved'), unavailable: t('projects.draftUnavailable'), full: t('projects.draftFull'),
|
|
3774
|
+
tooLarge: t('projects.draftTooLarge'), invalid: t('projects.draftInvalid') }
|
|
3775
|
+
const hasInputs = Boolean(state.forms.title || state.forms.goal || state.forms.recipe || state.creationWork || state.creationDraftMissingRecipe || Object.keys(state.forms.recipeParameters).length)
|
|
3776
|
+
return el('div', { 'data-creation-drafts': true },
|
|
3777
|
+
el('p', { className: 'db-muted' }, t('projects.draftHint')),
|
|
3778
|
+
(hasInputs || state.creationDraftStorageStatus !== 'saved') && messages[state.creationDraftStorageStatus] ? el('p', {
|
|
3779
|
+
className: state.creationDraftStorageStatus === 'saved' ? 'db-muted' : 'db-error',
|
|
3780
|
+
'data-creation-draft-status': state.creationDraftStorageStatus,
|
|
3781
|
+
}, messages[state.creationDraftStorageStatus]) : null,
|
|
3782
|
+
state.creationDraftMissingRecipe ? el('div', { 'data-creation-draft-recipe-missing': true },
|
|
3783
|
+
el('p', { className: 'db-error' }, t('projects.draftMissingRecipe', state.creationDraftMissingRecipe)),
|
|
3784
|
+
el('details', null, el('summary', null, t('projects.draftOldParameters')),
|
|
3785
|
+
el('pre', { className: 'db-pre' }, JSON.stringify(state.forms.recipeParameters, null, 2)))) : null,
|
|
3786
|
+
hasInputs ? Button({ action: 'clear-creation-draft', disabled: state.busy.create, onClick: actions.clearCreationDraft, children: t('projects.draftClear') }) : null,
|
|
3787
|
+
state.creationDrafts.length ? el('div', { style: { marginTop: '8px' } },
|
|
3788
|
+
el('h5', null, t('projects.drafts')),
|
|
3789
|
+
...state.creationDrafts.map(draft => el('div', { className: 'db-card', 'data-creation-draft': draft.id },
|
|
3790
|
+
el('strong', null, draft.invalid ? t('projects.damagedDraft') : draft.title),
|
|
3791
|
+
draft.savedAt ? el('span', { className: 'db-muted', style: { marginLeft: '8px' } }, formatTime(draft.savedAt)) : null,
|
|
3792
|
+
draft.uncertain ? el('p', { className: 'db-muted' }, t('projects.draftUncertain')) : null,
|
|
3793
|
+
el('div', { className: 'db-inline' },
|
|
3794
|
+
Button({ action: `restore-creation-draft:${draft.id}`, disabled: state.busy.create || draft.invalid,
|
|
3795
|
+
onClick: () => actions.restoreCreationDraft(draft.id), children: t('projects.draftRestore') }),
|
|
3796
|
+
Button({ action: `delete-creation-draft:${draft.id}`, onClick: () => actions.deleteCreationDraft(draft.id), children: t('projects.draftDelete') })),
|
|
3797
|
+
))) : null,
|
|
3798
|
+
)
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3478
3801
|
function ProjectsView(ctx) {
|
|
3479
3802
|
const state = ctx.state
|
|
3480
3803
|
const actions = ctx.actions
|
|
@@ -3489,10 +3812,12 @@ window.__ModuleLoader__.load({
|
|
|
3489
3812
|
Button({
|
|
3490
3813
|
tone: project.projectId === state.activeProjectId ? 'primary' : undefined,
|
|
3491
3814
|
action: `select-project:${project.projectId}`,
|
|
3815
|
+
disabled: project.creationPending,
|
|
3492
3816
|
onClick: () => actions.selectProject(project.projectId),
|
|
3493
3817
|
children: project.title || project.projectId,
|
|
3494
3818
|
}),
|
|
3495
3819
|
el('span', { className: 'db-muted db-mono' }, project.projectId),
|
|
3820
|
+
project.creationPending ? Badge({ tone: 'live', name: 'creation-pending', children: t('projects.incomplete') }) : null,
|
|
3496
3821
|
project.unfinishedJobs > 0 ? Badge({ tone: 'live', name: 'unfinished', children: t('projects.unfinishedJobs', { count: project.unfinishedJobs }) }) : null,
|
|
3497
3822
|
el('span', { className: 'db-muted' }, t('projects.revisionCount', { count: project.revisionCount })),
|
|
3498
3823
|
),
|
|
@@ -3503,6 +3828,7 @@ window.__ModuleLoader__.load({
|
|
|
3503
3828
|
|
|
3504
3829
|
el('div', { className: 'db-card db-project-create' },
|
|
3505
3830
|
el('h4', null, t('projects.create')),
|
|
3831
|
+
CreationDraftsView(ctx),
|
|
3506
3832
|
!state.projects.length ? el('p', { className: 'db-muted' }, t('projects.startHint')) : null,
|
|
3507
3833
|
RecipesView(ctx),
|
|
3508
3834
|
el('div', { className: 'db-create-footer' },
|
|
@@ -3526,12 +3852,24 @@ window.__ModuleLoader__.load({
|
|
|
3526
3852
|
Button({
|
|
3527
3853
|
tone: 'primary',
|
|
3528
3854
|
action: 'create-project',
|
|
3529
|
-
disabled: state.busy.create || state.forms.title.trim().length === 0 || !recipeSelectionCurrent(state) || !recipeValuesValid(state.forms.recipe, state.forms.recipeParameters),
|
|
3855
|
+
disabled: state.busy.create || Boolean(state.creationDraftMissingRecipe) || state.creationRequestProtocol !== CREATION_REQUEST_PROTOCOL || state.forms.title.trim().length === 0 || !recipeSelectionCurrent(state) || !recipeValuesValid(state.forms.recipe, state.forms.recipeParameters),
|
|
3530
3856
|
onClick: actions.createProject,
|
|
3531
3857
|
children: state.busy.create ? (state.forms.recipe ? t('recipes.creatingPreview') : t('projects.creating')) : (state.forms.recipe ? t('recipes.createPreview') : t('projects.createButton')),
|
|
3532
3858
|
}),
|
|
3533
3859
|
),
|
|
3534
3860
|
state.notices.projects ? Notice(state.notices.projects, { marginTop: '8px' }) : null,
|
|
3861
|
+
state.status === 'ok' && state.creationRequestProtocol !== CREATION_REQUEST_PROTOCOL
|
|
3862
|
+
? el('p', { className: 'db-muted', 'data-creation-host-stale': true }, t('projects.hostNeedsRestart')) : null,
|
|
3863
|
+
state.creationWork?.status === 'error' ? el('div', { 'data-creation-recovery': true, style: { marginTop: '8px' } },
|
|
3864
|
+
el('p', { className: 'db-muted' }, t('projects.originalTitle', { title: state.creationWork.title })),
|
|
3865
|
+
el('div', { className: 'db-inline' },
|
|
3866
|
+
Button({ action: 'retry-creation', disabled: state.busy.create, onClick: actions.retryCreation, children: t('projects.retryCreation') }),
|
|
3867
|
+
Button({ action: 'refresh-creation-projects', onClick: actions.reload, children: t('projects.refreshList') }),
|
|
3868
|
+
['BLENDER_NOT_FOUND', 'RUNTIME_UNAVAILABLE'].includes(state.creationWork.code)
|
|
3869
|
+
? el('a', { href: 'https://github.com/pearjelly/deep-blend/blob/main/deepblend/docs/environment-check.md', target: '_blank', rel: 'noreferrer' }, t('projects.environmentGuide')) : null),
|
|
3870
|
+
state.notices.projects?.technicalDetails ? el('details', { 'data-creation-diagnostics': true },
|
|
3871
|
+
el('summary', null, t('projects.creationDetails')), el('pre', { className: 'db-pre' }, state.notices.projects.technicalDetails)) : null,
|
|
3872
|
+
) : null,
|
|
3535
3873
|
),
|
|
3536
3874
|
el('details', { className: 'db-storage' }, el('summary', null, t('projects.storage')),
|
|
3537
3875
|
el('span', { className: 'db-muted db-mono' }, `projectsRoot: ${state.projectsRoot || '—'}`)),
|
|
@@ -4546,6 +4884,8 @@ window.__ModuleLoader__.load({
|
|
|
4546
4884
|
children: state.previewBusy ? t('preview.rendering') : t('preview.render'),
|
|
4547
4885
|
}),
|
|
4548
4886
|
Notice(state.previewResult, { border: 0, padding: '0 6px', marginBottom: 0 }),
|
|
4887
|
+
state.notices.preview?.kind === 'creation' && state.notices.preview.projectId === state.activeProjectId
|
|
4888
|
+
? Notice(state.notices.preview, { border: 0, padding: '0 6px', marginBottom: 0 }) : null,
|
|
4549
4889
|
),
|
|
4550
4890
|
el('div', { className: 'db-tabs' },
|
|
4551
4891
|
el('span', { className: 'db-muted' }, t('preview.compare')),
|
package/lib/index.js
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
BlenderError,
|
|
33
33
|
BlenderErrorCode,
|
|
34
34
|
HOST_API_VERSION,
|
|
35
|
+
CREATION_REQUEST_VERSION,
|
|
35
36
|
UI_PANEL_ID,
|
|
36
37
|
UI_ROUTES,
|
|
37
38
|
UI_ROUTE_PREFIX,
|
|
@@ -419,6 +420,7 @@ export default class BlenderUiHost extends Service {
|
|
|
419
420
|
export function statusForError(error) {
|
|
420
421
|
if (error?.code === 'UI_HOST_API_STALE') return 503
|
|
421
422
|
const referenceStatuses = { ASSET_REQUEST_INVALID: 400, ASSET_CONTENT_MISMATCH: 415, ASSET_TOO_LARGE: 413,
|
|
423
|
+
CREATION_REQUEST_INVALID: 400, CREATION_REQUEST_CONFLICT: 409, PROJECT_ID_INVALID: 400,
|
|
422
424
|
ASSET_FORMAT_UNAVAILABLE: 415, SCENE_VALIDATION_FAILED: 422, SCENE_SPEC_INVALID: 422,
|
|
423
425
|
PATH_SEGMENT_INVALID: 400, ASSET_HASH_MISMATCH: 409, ASSET_SOURCE_NOT_FOUND: 404 }
|
|
424
426
|
if (referenceStatuses[error.code]) return referenceStatuses[error.code]
|
|
@@ -623,13 +625,15 @@ export function createHandlers(ctx) {
|
|
|
623
625
|
/** Everything the panel needs to render itself from scratch, in one request. */
|
|
624
626
|
state: async ({ query }) => {
|
|
625
627
|
const listed = await studio().listProjects()
|
|
626
|
-
const projectId = query.projectId ?? listed.projects
|
|
628
|
+
const projectId = query.projectId ?? listed.projects.find(project => !project.creationPending)?.projectId ?? null
|
|
629
|
+
const creationPending = listed.projects.some(project => project.projectId === projectId && project.creationPending)
|
|
627
630
|
return {
|
|
628
631
|
panelId: UI_PANEL_ID,
|
|
629
632
|
recipeCatalog: studio().listRecipes?.() ?? { recipes: [], errors: [] },
|
|
630
633
|
projects: listed.projects.map(record => buildProjectView(record)),
|
|
631
634
|
projectsRoot: listed.projectsRoot,
|
|
632
|
-
|
|
635
|
+
creationRequestProtocol: studio().creationRequestProtocol ?? null,
|
|
636
|
+
selected: projectId === null || creationPending
|
|
633
637
|
? null
|
|
634
638
|
: await buildProjectState(studio(), projectId, query.revision ?? undefined, approvalThreshold()),
|
|
635
639
|
}
|
|
@@ -644,12 +648,16 @@ export function createHandlers(ctx) {
|
|
|
644
648
|
},
|
|
645
649
|
|
|
646
650
|
'projects.create': async ({ body }) => {
|
|
651
|
+
if (body.creationKey !== undefined && studio().creationRequestProtocol !== CREATION_REQUEST_VERSION) {
|
|
652
|
+
throw new BlenderError('UI_HOST_API_STALE', 'The running Host does not support creation recovery. Restart the profile with the updated DeepBlend packages before retrying.')
|
|
653
|
+
}
|
|
647
654
|
const created = await studio().createProject({
|
|
648
655
|
title: body.title,
|
|
649
656
|
goal: body.goal,
|
|
650
657
|
sceneSpec: body.sceneSpec,
|
|
651
658
|
recipe: body.recipe,
|
|
652
659
|
projectId: body.projectId,
|
|
660
|
+
creationKey: body.creationKey,
|
|
653
661
|
saveCheckpoint: true,
|
|
654
662
|
renderPreview: body.renderPreview === true,
|
|
655
663
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepblend/dsh-blender-ui",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DeepBlend workbench UI: Host half (typed HTTP API) and Web Client half (sidebar, panels, tool cards).",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@deepblend/dsh-blender-contracts": "0.3.
|
|
22
|
+
"@deepblend/dsh-blender-contracts": "0.3.6"
|
|
23
23
|
},
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"repository": {
|