@deepblend/dsh-blender-ui 0.3.6 → 0.3.7

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.
Files changed (2) hide show
  1. package/lib/client.js +78 -16
  2. package/package.json +2 -2
package/lib/client.js CHANGED
@@ -478,7 +478,7 @@ window.__ModuleLoader__.load({
478
478
  'recipes.select': '使用这个配方',
479
479
  'recipes.selected': '已选择',
480
480
  'recipes.parameters': '调整配方',
481
- 'recipes.previewNote': '示例图展示默认参数。创建后会渲染你的设置,耗时取决于设备。',
481
+ 'recipes.previewNote': '示例图展示默认参数。切换配方会保留各自的调整,恢复默认值只影响当前配方。创建后会渲染你的设置,耗时取决于设备。',
482
482
  'recipes.createPreview': '创建并生成预览',
483
483
  'recipes.creatingPreview': '正在创建并渲染…',
484
484
  'recipes.stale': '选中的配方已更新或不可用,请重新选择。',
@@ -1043,7 +1043,7 @@ window.__ModuleLoader__.load({
1043
1043
  'recipes.select': 'Use this recipe',
1044
1044
  'recipes.selected': 'Selected',
1045
1045
  'recipes.parameters': 'Customize recipe',
1046
- 'recipes.previewNote': 'Images show default parameters. Creating a project renders your settings; time depends on your device.',
1046
+ 'recipes.previewNote': 'Images show default parameters. Switching recipes keeps each set of edits; reset affects only the current recipe. Creating a project renders your settings; time depends on your device.',
1047
1047
  'recipes.createPreview': 'Create and preview',
1048
1048
  'recipes.creatingPreview': 'Creating and rendering…',
1049
1049
  'recipes.stale': 'The selected recipe changed or is unavailable. Please select it again.',
@@ -1399,6 +1399,34 @@ window.__ModuleLoader__.load({
1399
1399
  const CREATION_DRAFT_BYTES = 262144
1400
1400
  const creationDraftPrefix = root => `${CREATION_DRAFT_VERSION}:${encodeURIComponent(root)}:`
1401
1401
  const creationRecipeReference = recipe => recipe ? { id: recipe.id, version: recipe.version, digest: recipe.digest } : null
1402
+ const recipeChoiceIdentity = recipe => creationSignature(creationRecipeReference(recipe))
1403
+ const copyRecipeParameters = values => Object.fromEntries(Object.entries(values).map(([key, value]) => [key, Array.isArray(value) ? [...value] : value]))
1404
+ const rememberRecipeChoice = (choices, recipe, parameters) => !recipe ? choices : [
1405
+ ...choices.filter(choice => recipeChoiceIdentity(choice.recipe) !== recipeChoiceIdentity(recipe)),
1406
+ { recipe: creationRecipeReference(recipe), parameters: copyRecipeParameters(parameters) },
1407
+ ]
1408
+ function validRecipeChoices(choices) {
1409
+ if (choices === undefined) return true // Existing 0.3.6 browser drafts contain only their selected recipe.
1410
+ if (!Array.isArray(choices)) return false
1411
+ const seen = new Set()
1412
+ return choices.every(choice => {
1413
+ if (!choice || !choice.recipe || !validCreationRecipeReference(choice.recipe)
1414
+ || Object.keys(choice).some(key => !['recipe', 'parameters'].includes(key))
1415
+ || !choice.parameters || typeof choice.parameters !== 'object' || Array.isArray(choice.parameters)) return false
1416
+ const key = recipeChoiceIdentity(choice.recipe)
1417
+ if (seen.has(key)) return false
1418
+ seen.add(key)
1419
+ return Object.values(choice.parameters).every(value => value === '' || Number.isFinite(value)
1420
+ || Array.isArray(value) && value.length === 3 && value.every(v => Number.isFinite(v) && v >= 0 && v <= 1))
1421
+ })
1422
+ }
1423
+ function recipeChoiceFits(recipe, parameters) {
1424
+ return Object.keys(parameters).length === recipe.parameters.length && recipe.parameters.every(parameter => {
1425
+ const value = parameters[parameter.id]
1426
+ return parameter.type === 'color' ? Array.isArray(value) && value.length === 3 && value.every(v => Number.isFinite(v) && v >= 0 && v <= 1)
1427
+ : value === '' || Number.isFinite(value)
1428
+ })
1429
+ }
1402
1430
  function creationDraftJson(value) {
1403
1431
  if (value === null || typeof value === 'string' || typeof value === 'boolean') return
1404
1432
  if (typeof value === 'number' && Number.isFinite(value)) return
@@ -1429,7 +1457,8 @@ window.__ModuleLoader__.load({
1429
1457
  || !Number.isFinite(Date.parse(draft.savedAt)) || !forms || typeof forms.title !== 'string'
1430
1458
  || typeof forms.goal !== 'string' || !validCreationRecipeReference(forms.recipe)
1431
1459
  || Object.keys(forms).some(key => !['title', 'goal', 'recipe', 'recipeParameters'].includes(key))
1432
- || !forms.recipeParameters || typeof forms.recipeParameters !== 'object' || Array.isArray(forms.recipeParameters)) return null
1460
+ || !forms.recipeParameters || typeof forms.recipeParameters !== 'object' || Array.isArray(forms.recipeParameters)
1461
+ || !validRecipeChoices(draft.recipeChoices)) return null
1433
1462
  const attempt = draft.attempt
1434
1463
  if (attempt !== null) {
1435
1464
  const body = attempt?.body
@@ -2607,6 +2636,7 @@ window.__ModuleLoader__.load({
2607
2636
  editorLastEdits: {},
2608
2637
  editorComparison: null,
2609
2638
  patchDrafts: {},
2639
+ recipeChoices: [],
2610
2640
  forms: { title: '', goal: '', recipe: null, recipeParameters: {}, patch: null, frameStart: '', frameEnd: '', profile: 'preview' },
2611
2641
  busy: { create: false, patch: false, editor: false, render: false, restore: false },
2612
2642
  notices: { projects: null, scene: null, preview: null, jobs: null, revisions: null },
@@ -2693,6 +2723,8 @@ window.__ModuleLoader__.load({
2693
2723
  const assetWork = (projectId, changes) => set({ assetWork: { ...data.assetWork, [projectId]: { ...data.assetWork[projectId], ...changes } } })
2694
2724
 
2695
2725
  const notify = () => {
2726
+ if (!creationDraftClosed) data.recipeChoices = rememberRecipeChoice(data.recipeChoices,
2727
+ data.forms.recipe || data.creationDraftMissingRecipe, data.forms.recipeParameters)
2696
2728
  persistCreationDraft()
2697
2729
  const signature = JSON.stringify([data.activeProjectId, data.selected?.scene?.revision, data.view, ...FILE_FIELDS.map(field => fileChoiceAllowed(data, field))])
2698
2730
  if (signature !== fileChoiceSignature) { data.fileChoiceGeneration += 1; fileChoiceSignature = signature }
@@ -2715,19 +2747,21 @@ window.__ModuleLoader__.load({
2715
2747
  if (creationDraftClosed || creationDraftWriting || !creationDraftRoot || data.projectsRoot !== creationDraftRoot) return
2716
2748
  creationDraftWriting = true
2717
2749
  try {
2718
- const forms = { ...creationDraftForms(data.forms), recipe: creationRecipeReference(data.forms.recipe) || data.creationDraftMissingRecipe }, signature = JSON.stringify([forms, creationAttempt])
2750
+ const forms = { ...creationDraftForms(data.forms), recipe: creationRecipeReference(data.forms.recipe) || data.creationDraftMissingRecipe }
2751
+ creationDraftJson(data.recipeChoices)
2752
+ const signature = JSON.stringify([forms, creationAttempt, data.recipeChoices])
2719
2753
  if (signature === persistedCreationSignature) return
2720
2754
  const reading = refreshCreationDrafts()
2721
2755
  if (!reading.available) { data.creationDraftStorageStatus = 'unavailable'; return }
2722
2756
  const key = creationDraftPrefix(creationDraftRoot) + creationDraftId
2723
- if (!forms.title && !forms.goal && !forms.recipe && !Object.keys(forms.recipeParameters).length && !creationAttempt) {
2757
+ if (!forms.title && !forms.goal && !forms.recipe && !Object.keys(forms.recipeParameters).length && !creationAttempt && !data.recipeChoices.length) {
2724
2758
  draftStorage.removeItem(key); data.creationDraftStorageStatus = null
2725
2759
  } else {
2726
2760
  if (!reading.entries.some(entry => entry.id === creationDraftId) && reading.entries.length >= 20) {
2727
2761
  data.creationDraftStorageStatus = 'full'; return
2728
2762
  }
2729
2763
  const raw = JSON.stringify({ schemaVersion: CREATION_DRAFT_VERSION, projectsRoot: creationDraftRoot,
2730
- id: creationDraftId, ownerActive: true, savedAt: new Date().toISOString(), forms, attempt: creationAttempt })
2764
+ id: creationDraftId, ownerActive: true, savedAt: new Date().toISOString(), forms, attempt: creationAttempt, recipeChoices: data.recipeChoices })
2731
2765
  if (new Blob([raw]).size > CREATION_DRAFT_BYTES) { data.creationDraftStorageStatus = 'tooLarge'; return }
2732
2766
  draftStorage.setItem(key, raw)
2733
2767
  if (draftStorage.getItem(key) !== raw) throw new Error('Draft write did not persist')
@@ -2832,7 +2866,7 @@ window.__ModuleLoader__.load({
2832
2866
  // Switching stores cannot carry an old store's request identity into a new one.
2833
2867
  if (creationDraftRoot !== null) {
2834
2868
  creationAttempt = null; restoredCreationSource = null; creationDraftId = newCreationKey()
2835
- patch.creationWork = null; patch.creationDraftMissingRecipe = null
2869
+ patch.creationWork = null; patch.creationDraftMissingRecipe = null; patch.recipeChoices = []
2836
2870
  patch.forms = { ...data.forms, title: '', goal: '', recipe: null, recipeParameters: {} }
2837
2871
  }
2838
2872
  creationDraftRoot = typeof patch.projectsRoot === 'string' && patch.projectsRoot ? patch.projectsRoot : null
@@ -2873,6 +2907,9 @@ window.__ModuleLoader__.load({
2873
2907
  if (attemptRoot !== creationDraftRoot) { reload(); return }
2874
2908
  if (outcome.ok) {
2875
2909
  const sameInputs = creationSignature(creationBody()) === attempt.signature
2910
+ if (attempt.body.recipe) data.recipeChoices = data.recipeChoices.filter(choice =>
2911
+ recipeChoiceIdentity(choice.recipe) !== recipeChoiceIdentity(attempt.body.recipe)
2912
+ || creationSignature(choice.parameters) !== creationSignature(attempt.body.recipe.parameters))
2876
2913
  set({ ...(sameInputs ? { forms: { ...data.forms, title: '', goal: '', recipe: null, recipeParameters: {} } } : {}),
2877
2914
  creationWork: null, projectId: outcome.payload.project.projectId,
2878
2915
  ...(attempt.body.renderPreview ? { view: 'preview', compareMode: 'result' } : {}) })
@@ -2882,7 +2919,17 @@ window.__ModuleLoader__.load({
2882
2919
  const sourceKey = creationDraftPrefix(creationDraftRoot) + restoredCreationSource.id
2883
2920
  const source = parseCreationDraft(restoredCreationSource.raw, creationDraftRoot, restoredCreationSource.id)
2884
2921
  if (source && creationSignature(creationRequestBody(source.forms)) === attempt.signature
2885
- && draftStorage?.getItem(sourceKey) === restoredCreationSource.raw) draftStorage.removeItem(sourceKey)
2922
+ && draftStorage?.getItem(sourceKey) === restoredCreationSource.raw) {
2923
+ const remaining = (source.recipeChoices || []).filter(choice => !attempt.body.recipe
2924
+ || recipeChoiceIdentity(choice.recipe) !== recipeChoiceIdentity(attempt.body.recipe)
2925
+ || creationSignature(choice.parameters) !== creationSignature(attempt.body.recipe.parameters))
2926
+ if (remaining.length) {
2927
+ const raw = JSON.stringify({ ...source, savedAt: new Date().toISOString(), attempt: null, recipeChoices: remaining,
2928
+ forms: { title: '', goal: '', recipe: null, recipeParameters: {} } })
2929
+ draftStorage.setItem(sourceKey, raw)
2930
+ if (draftStorage.getItem(sourceKey) !== raw) throw new Error('Draft write did not persist')
2931
+ } else draftStorage.removeItem(sourceKey)
2932
+ }
2886
2933
  } catch { data.creationDraftStorageStatus = 'unavailable' }
2887
2934
  }
2888
2935
  restoredCreationSource = null
@@ -3197,10 +3244,21 @@ window.__ModuleLoader__.load({
3197
3244
  } else assetWork(projectId, { error: `${outcome.error.code}: ${outcome.error.message}` })
3198
3245
  reload()
3199
3246
  },
3200
- selectRecipe: recipe => set({ creationDraftMissingRecipe: null, forms: { ...data.forms, recipe,
3201
- recipeParameters: recipe ? Object.fromEntries(recipe.parameters.map(parameter => [parameter.id, parameter.default])) : {},
3202
- title: !data.forms.title || data.forms.title === data.forms.recipe?.title ? recipe?.title || '' : data.forms.title,
3203
- } }),
3247
+ selectRecipe: recipe => {
3248
+ if (recipe && data.forms.recipe && recipeChoiceIdentity(recipe) === recipeChoiceIdentity(data.forms.recipe)) return
3249
+ const saved = recipe ? data.recipeChoices.find(choice => recipeChoiceIdentity(choice.recipe) === recipeChoiceIdentity(recipe)) : null
3250
+ if (saved && !recipeChoiceFits(recipe, saved.parameters)) { set({ creationDraftStorageStatus: 'invalid' }); return }
3251
+ set({ creationDraftMissingRecipe: null, forms: { ...data.forms, recipe,
3252
+ recipeParameters: recipe ? saved ? copyRecipeParameters(saved.parameters)
3253
+ : Object.fromEntries(recipe.parameters.map(parameter => [parameter.id, Array.isArray(parameter.default) ? [...parameter.default] : parameter.default])) : {},
3254
+ title: !data.forms.title || data.forms.title === data.forms.recipe?.title ? recipe?.title || '' : data.forms.title,
3255
+ } })
3256
+ },
3257
+ resetRecipe: () => {
3258
+ const recipe = data.forms.recipe
3259
+ if (recipe) set({ forms: { ...data.forms, recipeParameters: Object.fromEntries(recipe.parameters.map(parameter =>
3260
+ [parameter.id, Array.isArray(parameter.default) ? [...parameter.default] : parameter.default])) } })
3261
+ },
3204
3262
  setRecipeParameter: (id, value) => setIn('forms', 'recipeParameters', { ...data.forms.recipeParameters, [id]: value }),
3205
3263
  setCompareMode: (compareMode) => set({ compareMode }),
3206
3264
  pickCompare: (side, revision) => set(side === 'left'
@@ -3557,10 +3615,14 @@ window.__ModuleLoader__.load({
3557
3615
  const reading = readCreationDrafts(draftStorage, creationDraftRoot), source = reading.entries.find(entry => entry.id === id)
3558
3616
  if (!source?.draft) { set({ creationDraftStorageStatus: 'invalid' }); return }
3559
3617
  const saved = source.draft, ref = saved.forms.recipe
3618
+ if ((saved.recipeChoices || []).some(choice => {
3619
+ const available = data.recipeCatalog.recipes.find(recipe => recipeChoiceIdentity(recipe) === recipeChoiceIdentity(choice.recipe))
3620
+ return available && !recipeChoiceFits(available, choice.parameters)
3621
+ })) { set({ creationDraftStorageStatus: 'invalid' }); return }
3560
3622
  const recipe = ref ? data.recipeCatalog.recipes.find(recipe => recipe.id === ref.id && recipe.version === ref.version && recipe.digest === ref.digest) : null
3561
3623
  creationDraftId = newCreationKey(); restoredCreationSource = { id, raw: source.raw }; persistedCreationSignature = null
3562
3624
  creationAttempt = saved.attempt ? editorClone(saved.attempt) : null
3563
- set({ view: 'projects', forms: { ...data.forms, ...editorClone(saved.forms), recipe: recipe || null },
3625
+ set({ view: 'projects', recipeChoices: editorClone(saved.recipeChoices || []), forms: { ...data.forms, ...editorClone(saved.forms), recipe: recipe || null },
3564
3626
  creationDraftMissingRecipe: ref && !recipe ? ref : null,
3565
3627
  creationWork: creationAttempt ? { status: 'error', title: creationAttempt.body.title, code: 'UI_FETCH_FAILED' } : null,
3566
3628
  notices: { ...data.notices, projects: { kind: 'creation', ok: true, message: t('projects.draftRestored') } } })
@@ -3584,7 +3646,7 @@ window.__ModuleLoader__.load({
3584
3646
  clearCreationDraft: () => {
3585
3647
  if (data.busy.create) return
3586
3648
  creationAttempt = null; restoredCreationSource = null; persistedCreationSignature = null
3587
- set({ forms: { ...data.forms, title: '', goal: '', recipe: null, recipeParameters: {} }, creationWork: null,
3649
+ set({ recipeChoices: [], forms: { ...data.forms, title: '', goal: '', recipe: null, recipeParameters: {} }, creationWork: null,
3588
3650
  creationDraftMissingRecipe: null, notices: { ...data.notices, projects: null } })
3589
3651
  },
3590
3652
 
@@ -3772,7 +3834,7 @@ window.__ModuleLoader__.load({
3772
3834
  function CreationDraftsView({ state, actions }) {
3773
3835
  const messages = { saved: t('projects.draftSaved'), unavailable: t('projects.draftUnavailable'), full: t('projects.draftFull'),
3774
3836
  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)
3837
+ const hasInputs = Boolean(state.forms.title || state.forms.goal || state.forms.recipe || state.creationWork || state.creationDraftMissingRecipe || state.recipeChoices.length || Object.keys(state.forms.recipeParameters).length)
3776
3838
  return el('div', { 'data-creation-drafts': true },
3777
3839
  el('p', { className: 'db-muted' }, t('projects.draftHint')),
3778
3840
  (hasInputs || state.creationDraftStorageStatus !== 'saved') && messages[state.creationDraftStorageStatus] ? el('p', {
@@ -3942,7 +4004,7 @@ window.__ModuleLoader__.load({
3942
4004
  })),
3943
4005
  selected ? el('fieldset', { className: 'db-recipe-parameters' },
3944
4006
  el('legend', null, t('recipes.parameters')),
3945
- Button({ action: 'reset-recipe', onClick: () => actions.selectRecipe(selected), children: t('recipes.reset') }),
4007
+ Button({ action: 'reset-recipe', onClick: actions.resetRecipe, children: t('recipes.reset') }),
3946
4008
  el('div', { className: 'db-inline', style: { flexWrap: 'wrap' } }, selected.parameters.map(parameter =>
3947
4009
  el('label', { key: parameter.id, style: { display: 'flex', flexDirection: 'column', gap: '5px' } },
3948
4010
  parameter.title,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepblend/dsh-blender-ui",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
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.6"
22
+ "@deepblend/dsh-blender-contracts": "0.3.7"
23
23
  },
24
24
  "license": "MIT",
25
25
  "repository": {