@deepblend/dsh-blender-ui 0.5.0 → 0.6.1
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 +108 -3
- package/lib/index.js +23 -2
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -355,6 +355,14 @@ window.__ModuleLoader__.load({
|
|
|
355
355
|
'editor.unknownSettings': '这张预览缺少实测渲染设置,无法比较编辑差异。',
|
|
356
356
|
'editor.missingBaseline': '没有同相机、同帧和同渲染设置的编辑前预览,暂时无法比较差异。',
|
|
357
357
|
'editor.choose': '选择一个对象,调整后应用并查看真实预览。',
|
|
358
|
+
'assembly.title': '调整整套模型',
|
|
359
|
+
'assembly.help': '尺寸会联动配方中的部件,保留独立修改的材质、倒角和摄影设置。保存后生成新版本和预览。',
|
|
360
|
+
'assembly.save': '保存整套尺寸并预览',
|
|
361
|
+
'assembly.retry': '重试同一次保存',
|
|
362
|
+
'assembly.conflict': '部分受控几何已被单独修改,请先恢复对应部件;整套尺寸尚未保存。',
|
|
363
|
+
'assembly.stale': '当前作品已有新版本。草稿已保留,请重新载入后再修改。',
|
|
364
|
+
'assembly.otherDraft': '请先保存或放弃其他未提交的编辑。',
|
|
365
|
+
'assembly.photoKept': '已保留自定义摄影,请确认新尺寸下的构图。',
|
|
358
366
|
'editor.position': '位置(mm)',
|
|
359
367
|
'editor.rotation': '旋转(°)',
|
|
360
368
|
'editor.geometry': '形状与轮廓',
|
|
@@ -946,6 +954,14 @@ window.__ModuleLoader__.load({
|
|
|
946
954
|
'editor.unknownSettings': 'Measured render settings are missing. This preview cannot establish the edit difference.',
|
|
947
955
|
'editor.missingBaseline': 'No before preview has the same camera, frame and measured render settings. The edit difference is unavailable.',
|
|
948
956
|
'editor.choose': 'Select an object, adjust its settings, then apply and view a real preview.',
|
|
957
|
+
'assembly.title': 'Resize the assembly',
|
|
958
|
+
'assembly.help': 'Recipe parts follow the dimensions. Independent materials, bevels and photography are retained. Saving creates a new revision and preview.',
|
|
959
|
+
'assembly.save': 'Save assembly dimensions and preview',
|
|
960
|
+
'assembly.retry': 'Retry the same save',
|
|
961
|
+
'assembly.conflict': 'Controlled geometry was edited separately. Restore those parts before resizing; nothing has been saved.',
|
|
962
|
+
'assembly.stale': 'The work has a newer revision. Your draft is retained; reload it before editing.',
|
|
963
|
+
'assembly.otherDraft': 'Save or discard other unsaved edits first.',
|
|
964
|
+
'assembly.photoKept': 'Custom photography was preserved. Check framing at the new dimensions.',
|
|
949
965
|
'editor.position': 'Position (mm)',
|
|
950
966
|
'editor.rotation': 'Rotation (°)',
|
|
951
967
|
'editor.geometry': 'Shape and profile',
|
|
@@ -1392,7 +1408,7 @@ window.__ModuleLoader__.load({
|
|
|
1392
1408
|
|
|
1393
1409
|
/** Every DeepBlend wire tool name this package draws a card for. */
|
|
1394
1410
|
const TOOL_CARD_KEYS = [
|
|
1395
|
-
'blender_capabilities', 'blender_recipe_list', 'blender_project_create', 'blender_project_get',
|
|
1411
|
+
'blender_capabilities', 'blender_recipe_list', 'blender_recipe_parameters', 'blender_project_create', 'blender_project_get',
|
|
1396
1412
|
'blender_scene_get', 'blender_scene_patch', 'blender_preview_render',
|
|
1397
1413
|
'blender_scene_validate', 'blender_revision_restore', 'blender_asset_ingest',
|
|
1398
1414
|
'blender_preview_views', 'blender_visual_review',
|
|
@@ -2490,6 +2506,19 @@ window.__ModuleLoader__.load({
|
|
|
2490
2506
|
|| photographyDirty(photographyDraftFor(state))
|
|
2491
2507
|
|| Boolean(state.assetDrafts?.[state.activeProjectId])
|
|
2492
2508
|
|| state.forms.patch !== null
|
|
2509
|
+
|| assemblyDirty(assemblyDraftFor(state))
|
|
2510
|
+
}
|
|
2511
|
+
const assemblyKey = state => JSON.stringify([state.projectsRoot, state.activeProjectId])
|
|
2512
|
+
const assemblyDraftFor = state => state.assemblyDrafts?.[assemblyKey(state)] || null
|
|
2513
|
+
const assemblyDirty = draft => Boolean(draft && Object.entries(draft.values).some(([id, value]) => value === '' || Number(value) !== draft.original[id]))
|
|
2514
|
+
const assemblyInvalid = draft => !draft || draft.parameters.some(parameter => {
|
|
2515
|
+
const raw = draft.values[parameter.id], value = Number(raw)
|
|
2516
|
+
return raw === '' || !Number.isFinite(value) || value < parameter.minimum || value > parameter.maximum
|
|
2517
|
+
})
|
|
2518
|
+
function createAssemblyDraft(source, projectsRoot) {
|
|
2519
|
+
const values = Object.fromEntries(source.parameters.map(parameter => [parameter.id, parameter.value]))
|
|
2520
|
+
return { projectId: source.projectId, projectsRoot, baseRevision: source.revision,
|
|
2521
|
+
parameters: editorClone(source.parameters), values, original: { ...values }, idempotencyKey: newCreationKey(), attempt: null }
|
|
2493
2522
|
}
|
|
2494
2523
|
function briefValid(draft) {
|
|
2495
2524
|
return Boolean(draft && typeof draft.goal === 'string' && draft.goal.length <= 2000 && draft.referenceImages.length <= 4
|
|
@@ -2736,10 +2765,11 @@ window.__ModuleLoader__.load({
|
|
|
2736
2765
|
editorDrafts: {},
|
|
2737
2766
|
editorLastEdits: {},
|
|
2738
2767
|
editorComparison: null,
|
|
2768
|
+
assemblyDrafts: {},
|
|
2739
2769
|
patchDrafts: {},
|
|
2740
2770
|
recipeChoices: [],
|
|
2741
2771
|
forms: { title: '', goal: '', recipe: null, recipeParameters: {}, patch: null, frameStart: '', frameEnd: '', profile: 'preview' },
|
|
2742
|
-
busy: { create: false, patch: false, editor: false, render: false, restore: false },
|
|
2772
|
+
busy: { create: false, patch: false, editor: false, assembly: false, render: false, restore: false },
|
|
2743
2773
|
notices: { projects: null, scene: null, preview: null, jobs: null, revisions: null },
|
|
2744
2774
|
}
|
|
2745
2775
|
}
|
|
@@ -3001,6 +3031,13 @@ window.__ModuleLoader__.load({
|
|
|
3001
3031
|
if (sequence !== loadSequence || projectId !== target()) return
|
|
3002
3032
|
if (active !== data.activeProjectId) patch.forms = { ...data.forms, patch: data.patchDrafts[active] ?? null }
|
|
3003
3033
|
if (patch.selected?.scene && active) {
|
|
3034
|
+
const source = patch.selected.recipeParameters
|
|
3035
|
+
if (source?.available === true && source.projectId === active && source.revision === patch.selected.scene.revision) {
|
|
3036
|
+
const key = JSON.stringify([patch.projectsRoot, active]), draft = data.assemblyDrafts[key]
|
|
3037
|
+
if ((!draft || !assemblyDirty(draft)) && !data.busy.assembly) {
|
|
3038
|
+
patch.assemblyDrafts = { ...data.assemblyDrafts, [key]: createAssemblyDraft(source, patch.projectsRoot) }
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3004
3041
|
const photography = data.photographyDrafts[active]
|
|
3005
3042
|
if ((!photography || !photographyDirty(photography)) && !data.photographyWork[active]?.saving) {
|
|
3006
3043
|
patch.photographyDrafts = { ...data.photographyDrafts, [active]: createPhotographyDraft(patch.selected.scene, active, photography, patch.projectsRoot) }
|
|
@@ -3171,6 +3208,48 @@ window.__ModuleLoader__.load({
|
|
|
3171
3208
|
}
|
|
3172
3209
|
}
|
|
3173
3210
|
const actions = {
|
|
3211
|
+
setAssemblyValue: (id, value) => {
|
|
3212
|
+
const draft = assemblyDraftFor(data)
|
|
3213
|
+
if (!draft || data.busy.assembly || draft.attempt || !draft.parameters.some(parameter => parameter.id === id)) return
|
|
3214
|
+
set({ assemblyDrafts: { ...data.assemblyDrafts, [assemblyKey(data)]: { ...draft, values: { ...draft.values, [id]: value } } } })
|
|
3215
|
+
},
|
|
3216
|
+
resetAssembly: () => {
|
|
3217
|
+
const source = data.selected?.recipeParameters
|
|
3218
|
+
if (!source?.available || data.busy.assembly) return
|
|
3219
|
+
set({ assemblyDrafts: { ...data.assemblyDrafts, [assemblyKey(data)]: createAssemblyDraft(source, data.projectsRoot) } })
|
|
3220
|
+
},
|
|
3221
|
+
saveAssembly: async () => {
|
|
3222
|
+
const draft = assemblyDraftFor(data), source = data.selected?.recipeParameters
|
|
3223
|
+
if (!draft || data.busy.assembly || assemblyInvalid(draft) || !assemblyDirty(draft)) return
|
|
3224
|
+
if (!draft.attempt && (draft.baseRevision !== source?.revision || source?.conflicts?.length
|
|
3225
|
+
|| projectHasUnsavedDrafts({ ...data, assemblyDrafts: {} }))) return
|
|
3226
|
+
const key = assemblyKey(data), body = draft.attempt || { baseRevision: draft.baseRevision,
|
|
3227
|
+
projectsRoot: draft.projectsRoot, idempotencyKey: draft.idempotencyKey,
|
|
3228
|
+
parameters: Object.fromEntries(Object.entries(draft.values).map(([id, value]) => [id, Number(value)])) }
|
|
3229
|
+
set({ assemblyDrafts: { ...data.assemblyDrafts, [key]: { ...draft, attempt: body } } })
|
|
3230
|
+
setIn('busy', 'assembly', true)
|
|
3231
|
+
const outcome = await postJson(fetchImpl, projectRoute(draft.projectId, '/recipe-parameters'), body)
|
|
3232
|
+
setIn('busy', 'assembly', false)
|
|
3233
|
+
if (outcome.ok) {
|
|
3234
|
+
const drafts = { ...data.assemblyDrafts }; delete drafts[key]
|
|
3235
|
+
set({ assemblyDrafts: drafts })
|
|
3236
|
+
if (target() === draft.projectId && data.projectsRoot === draft.projectsRoot) {
|
|
3237
|
+
const revision = outcome.payload.revision.revision
|
|
3238
|
+
const notice = { ok: true, kind: 'recipe-parameters', message: t('scene.committed', { revision,
|
|
3239
|
+
digest: shortDigest(outcome.payload.revision.digest) }) + (outcome.payload.revision.preservedCameraIds?.length ? ` ${t('assembly.photoKept')}` : '') }
|
|
3240
|
+
set({ view: 'preview', compareMode: 'revisions', compareLeft: draft.baseRevision, compareRight: revision,
|
|
3241
|
+
notices: { ...data.notices, scene: notice, preview: notice } })
|
|
3242
|
+
}
|
|
3243
|
+
reload()
|
|
3244
|
+
} else {
|
|
3245
|
+
const uncertain = outcome.error.code === 'UI_FETCH_FAILED' || /^HTTP_5/.test(outcome.error.code)
|
|
3246
|
+
set({ assemblyDrafts: { ...data.assemblyDrafts, [key]: { ...draft, attempt: uncertain ? body : null } } })
|
|
3247
|
+
if (target() === draft.projectId && data.projectsRoot === draft.projectsRoot) {
|
|
3248
|
+
setIn('notices', 'scene', { ok: false, message: `${outcome.error.code}: ${outcome.error.message}` })
|
|
3249
|
+
if (outcome.error.code === 'REVISION_CONFLICT') reload()
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
},
|
|
3174
3253
|
acceptsFileChoice,
|
|
3175
3254
|
setAppearance: value => {
|
|
3176
3255
|
if (!APPEARANCES.includes(value)) return
|
|
@@ -4370,6 +4449,32 @@ window.__ModuleLoader__.load({
|
|
|
4370
4449
|
}, result.message)
|
|
4371
4450
|
}
|
|
4372
4451
|
|
|
4452
|
+
function AssemblyEditor(ctx) {
|
|
4453
|
+
const { state, actions } = ctx, source = state.selected?.recipeParameters, draft = assemblyDraftFor(state)
|
|
4454
|
+
if (!source) return null
|
|
4455
|
+
if (source.error) return el('div', { className: 'db-card', 'data-assembly-editor': true },
|
|
4456
|
+
el('h4', null, t('assembly.title')), ErrorBox({ error: source.error }))
|
|
4457
|
+
if (!source.available || !draft) return null
|
|
4458
|
+
const stale = draft.baseRevision !== state.selected.scene.revision
|
|
4459
|
+
const otherDirty = projectHasUnsavedDrafts({ ...state, assemblyDrafts: {} })
|
|
4460
|
+
const blocked = state.busy.assembly || !draft.attempt && (stale || source.conflicts?.length || otherDirty)
|
|
4461
|
+
return el('div', { className: 'db-card', 'data-assembly-editor': true,
|
|
4462
|
+
'data-assembly-base-revision': draft.baseRevision, 'data-assembly-dirty': String(assemblyDirty(draft)) },
|
|
4463
|
+
el('h4', null, t('assembly.title')), el('p', { className: 'db-muted' }, t('assembly.help')),
|
|
4464
|
+
source.conflicts?.length ? el('div', { className: 'db-error' }, t('assembly.conflict'),
|
|
4465
|
+
el('div', { className: 'db-inline' }, [...new Set(source.conflicts.map(conflict => conflict.entityId))].map(entityId =>
|
|
4466
|
+
Button({ key: entityId, action: `assembly-conflict:${entityId}`, onClick: () => actions.selectEditorEntity(entityId), children: entityId })))) : null,
|
|
4467
|
+
stale ? el('div', { className: 'db-error' }, t('assembly.stale')) : null,
|
|
4468
|
+
otherDirty ? el('div', { className: 'db-muted' }, t('assembly.otherDraft')) : null,
|
|
4469
|
+
el('div', { className: 'db-grid' }, draft.parameters.map(parameter => el('label', { key: parameter.id },
|
|
4470
|
+
parameter.title, el('input', { type: 'number', className: 'db-input', 'data-field': `assembly-${parameter.id}`,
|
|
4471
|
+
min: parameter.minimum, max: parameter.maximum, step: 'any', value: draft.values[parameter.id],
|
|
4472
|
+
disabled: state.busy.assembly || Boolean(draft.attempt), onInput: event => actions.setAssemblyValue(parameter.id, event.currentTarget.value) })))),
|
|
4473
|
+
el('div', { className: 'db-inline' }, Button({ action: 'assembly-save', disabled: Boolean(blocked) || assemblyInvalid(draft) || !assemblyDirty(draft),
|
|
4474
|
+
onClick: actions.saveAssembly, children: draft.attempt ? t('assembly.retry') : t('assembly.save') }),
|
|
4475
|
+
Button({ action: 'assembly-reset', disabled: state.busy.assembly, onClick: actions.resetAssembly, children: t('editor.reset') })))
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4373
4478
|
function SceneEditor(ctx) {
|
|
4374
4479
|
const { state, actions } = ctx, draft = editorDraftFor(state)
|
|
4375
4480
|
const lastEdit = state.editorLastEdits?.[state.activeProjectId]
|
|
@@ -4855,7 +4960,7 @@ window.__ModuleLoader__.load({
|
|
|
4855
4960
|
el('span', { className: 'db-muted db-mono' }, String(asset.path || '')))),
|
|
4856
4961
|
)),
|
|
4857
4962
|
el('section', { className: 'db-scene-inspector', 'aria-label': t('scene.inspector') },
|
|
4858
|
-
SceneEditor(ctx),
|
|
4963
|
+
AssemblyEditor(ctx), SceneEditor(ctx),
|
|
4859
4964
|
PhotographyEditor(ctx),
|
|
4860
4965
|
AssetsView(ctx),
|
|
4861
4966
|
el('details', { key: advancedDisclosure, 'data-disclosure': advancedDisclosure, open: state.disclosures?.[advancedDisclosure] ?? false,
|
package/lib/index.js
CHANGED
|
@@ -78,7 +78,7 @@ export const WORKBENCH_PAGE_ROOT_ID = 'deepblend-workbench'
|
|
|
78
78
|
* because this page supplies no platform modules: React is a console seed
|
|
79
79
|
* word, the workbench core does not use it, and `client.js` reads it lazily
|
|
80
80
|
* precisely so that this call can pass `{}`.
|
|
81
|
-
* 3.
|
|
81
|
+
* 3. Import the mounted bundle client, or the independently installed UI client — the console's own graph row,
|
|
82
82
|
* fetched from the console's own bundle route. There is no second copy of
|
|
83
83
|
* the workbench anywhere for this to find.
|
|
84
84
|
* 4. `mountStandalone(root)` — the one function that differs between the faces.
|
|
@@ -95,7 +95,10 @@ export function workbenchPageBootstrap(rootId) {
|
|
|
95
95
|
try {
|
|
96
96
|
await globalThis.__DSH_BOOT_READY__?.promise
|
|
97
97
|
const modules = window.__ModuleLoader__.create({ boot: window.__DSH_BOOT__, staticModules: {} })
|
|
98
|
-
const
|
|
98
|
+
const ownsBundleClient = (window.__DSH_BOOT__.entries || []).some(entry => entry.id === '@deepblend/dsh-blender-bundle')
|
|
99
|
+
const bundle = ownsBundleClient
|
|
100
|
+
? await modules.import('@deepblend/dsh-blender-bundle')
|
|
101
|
+
: await modules.import('@deepblend/dsh-blender-ui')
|
|
99
102
|
await bundle.mountStandalone(root)
|
|
100
103
|
root.dataset.deepblendStandalone = 'ready'
|
|
101
104
|
} catch (error) {
|
|
@@ -426,6 +429,7 @@ export function statusForError(error) {
|
|
|
426
429
|
if (referenceStatuses[error.code]) return referenceStatuses[error.code]
|
|
427
430
|
if (error.code === 'RECIPE_NOT_FOUND') return 404
|
|
428
431
|
if (error.code === 'RECIPE_CHANGED' || error.code === 'RECIPE_ID_CONFLICT') return 409
|
|
432
|
+
if (error.code === 'RECIPE_PARAMETER_CONFLICT') return 409
|
|
429
433
|
if (error.code?.startsWith('RECIPE_')) return 400
|
|
430
434
|
switch (error.code) {
|
|
431
435
|
case BlenderErrorCode.PROJECT_NOT_FOUND:
|
|
@@ -828,6 +832,16 @@ export function createHandlers(ctx) {
|
|
|
828
832
|
return { revision: result }
|
|
829
833
|
},
|
|
830
834
|
|
|
835
|
+
'project.recipeParameters': async ({ params, query }) => {
|
|
836
|
+
if (studio().recipeParameterEditingSupported !== true) throw new BlenderError('UI_HOST_API_STALE', 'Update the Host to edit assembly dimensions')
|
|
837
|
+
return studio().getRecipeParameters(params.projectId, { revision: query.revision })
|
|
838
|
+
},
|
|
839
|
+
'project.recipeParameters.update': async ({ params, body }) => {
|
|
840
|
+
if (studio().recipeParameterEditingSupported !== true) throw new BlenderError('UI_HOST_API_STALE', 'Update the Host to edit assembly dimensions')
|
|
841
|
+
return { revision: await studio().updateRecipeParameters({ ...body, projectId: params.projectId,
|
|
842
|
+
saveCheckpoint: true, renderPreview: true }) }
|
|
843
|
+
},
|
|
844
|
+
|
|
831
845
|
'project.restore': async ({ params, body }) => {
|
|
832
846
|
const result = await studio().restoreRevision({
|
|
833
847
|
projectId: params.projectId,
|
|
@@ -918,7 +932,14 @@ async function buildProjectState(studio, projectId, revision, threshold) {
|
|
|
918
932
|
const scene = await studio.getScene(projectId, { revision: requested, full: true })
|
|
919
933
|
const jobs = await studio.listJobs({ projectId })
|
|
920
934
|
const qa = await studio.getQaRecord({ projectId, revision: scene.revision })
|
|
935
|
+
let recipeParameters = null
|
|
936
|
+
if (studio.recipeParameterEditingSupported === true) {
|
|
937
|
+
try { recipeParameters = await studio.getRecipeParameters(projectId, { revision: scene.revision }) }
|
|
938
|
+
catch (error) { recipeParameters = { projectId, revision: scene.revision, available: false,
|
|
939
|
+
error: { code: error.code ?? 'RECIPE_LOCK_INVALID', message: error.message } } }
|
|
940
|
+
}
|
|
921
941
|
return {
|
|
942
|
+
recipeParameters,
|
|
922
943
|
project: buildProjectView({
|
|
923
944
|
projectId,
|
|
924
945
|
title: overview.title,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepblend/dsh-blender-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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.
|
|
22
|
+
"@deepblend/dsh-blender-contracts": "0.6.1"
|
|
23
23
|
},
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"repository": {
|