@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 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. `import('@deepblend/dsh-blender-ui')` — the console's own graph row,
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 bundle = await modules.import('@deepblend/dsh-blender-ui')
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.5.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.5.0"
22
+ "@deepblend/dsh-blender-contracts": "0.6.1"
23
23
  },
24
24
  "license": "MIT",
25
25
  "repository": {