@deepblend/dsh-blender-ui 0.3.9 → 0.5.0
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 +215 -16
- package/lib/index.js +3 -0
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -333,6 +333,24 @@ window.__ModuleLoader__.load({
|
|
|
333
333
|
"photo.cameraChanges": "相机",
|
|
334
334
|
"photo.lightChanges": "灯光",
|
|
335
335
|
"photo.restoreFailed": "恢复失败,已保留摄影记录:{message}",
|
|
336
|
+
"photo.reuseUnavailable": "无法读取完整摄影设置,请重新选择来源。",
|
|
337
|
+
"photo.reuseTitle": "复用已保存作品的摄影设置",
|
|
338
|
+
"photo.reuseHelp": "原坐标以米复用,不自动缩放或取景。替换本作品全部灯光和所选相机的可编辑设置;模型、材质、世界、动画、其他相机与交付设置保留。套用后检查构图,再明确保存。",
|
|
339
|
+
"photo.reuseProject": "来源作品",
|
|
340
|
+
"photo.reuseRevision": "来源保存版本",
|
|
341
|
+
"photo.reuseCamera": "来源相机",
|
|
342
|
+
"photo.reuseTarget": "瞄准目标作品中的对象",
|
|
343
|
+
"photo.reuseReading": "正在读取已保存摄影设置…",
|
|
344
|
+
"photo.reuseSource": "来源:{project} · {revision} · {digest} · {count} 盏灯",
|
|
345
|
+
"photo.reuseReplace": "替换当前未提交的摄影草稿(其他编辑保留)",
|
|
346
|
+
"photo.reuseStale": "目标草稿来源已变化,请重新读取摄影设置。",
|
|
347
|
+
"photo.reuseApplied": "已套用到草稿,尚未保存。请检查构图与灯光。",
|
|
348
|
+
"photo.reuseSame": "当前摄影设置已与来源一致,无需再次保存。",
|
|
349
|
+
"photo.reuseReload": "重新读取来源",
|
|
350
|
+
"photo.reuseApply": "套用到摄影草稿",
|
|
351
|
+
"photo.reuseUpgrade": "更新插件并重启 DSH 后即可复用摄影设置。",
|
|
352
|
+
"photo.reuseNote": "摄影设置从 {project} · {revision} · {digest} · 相机 {camera} 开始复用;目标对象 {target}。保存前可继续调整草稿。",
|
|
353
|
+
"photo.reuseTargetRequired": "请选择目标作品中的瞄准对象。",
|
|
336
354
|
'editor.title': '编辑对象',
|
|
337
355
|
'editor.unknownSettings': '这张预览缺少实测渲染设置,无法比较编辑差异。',
|
|
338
356
|
'editor.missingBaseline': '没有同相机、同帧和同渲染设置的编辑前预览,暂时无法比较差异。',
|
|
@@ -353,6 +371,9 @@ window.__ModuleLoader__.load({
|
|
|
353
371
|
'editor.animated': '动画控制的通道已禁用。',
|
|
354
372
|
'editor.invalid': '{field} 的值无效。',
|
|
355
373
|
'editor.size': '尺寸(mm)',
|
|
374
|
+
'editor.boxWidth': '宽(mm)',
|
|
375
|
+
'editor.boxDepth': '深(mm)',
|
|
376
|
+
'editor.boxHeight': '高(mm)',
|
|
356
377
|
'editor.radius': '半径(mm)',
|
|
357
378
|
'editor.handledCup': '带把手杯体',
|
|
358
379
|
'editor.wallThickness': '杯壁厚度(mm)',
|
|
@@ -903,6 +924,24 @@ window.__ModuleLoader__.load({
|
|
|
903
924
|
"photo.cameraChanges": "cameras",
|
|
904
925
|
"photo.lightChanges": "lighting",
|
|
905
926
|
"photo.restoreFailed": "Restore failed; the photography record is retained: {message}",
|
|
927
|
+
"photo.reuseUnavailable": "Complete photography settings are unavailable. Select the source again.",
|
|
928
|
+
"photo.reuseTitle": "Reuse photography from saved work",
|
|
929
|
+
"photo.reuseHelp": "Reuse absolute coordinates in metres, without automatic scaling or framing. Replace all lights and the selected camera’s editable settings. Keep models, materials, world, animation, other cameras and delivery settings. Check the framing before explicitly saving.",
|
|
930
|
+
"photo.reuseProject": "Source work",
|
|
931
|
+
"photo.reuseRevision": "Saved source version",
|
|
932
|
+
"photo.reuseCamera": "Source camera",
|
|
933
|
+
"photo.reuseTarget": "Aim at an object in the destination work",
|
|
934
|
+
"photo.reuseReading": "Reading saved photography settings…",
|
|
935
|
+
"photo.reuseSource": "Source: {project} · {revision} · {digest} · {count} lights",
|
|
936
|
+
"photo.reuseReplace": "Replace the unsaved photography draft (keep other edits)",
|
|
937
|
+
"photo.reuseStale": "The destination draft source changed. Read the photography settings again.",
|
|
938
|
+
"photo.reuseApplied": "Applied to the draft; not saved yet. Check the framing and lighting.",
|
|
939
|
+
"photo.reuseSame": "The current photography matches the source; no new save is needed.",
|
|
940
|
+
"photo.reuseReload": "Read source again",
|
|
941
|
+
"photo.reuseApply": "Apply to photography draft",
|
|
942
|
+
"photo.reuseUpgrade": "Update the plugin and restart DSH to reuse photography settings.",
|
|
943
|
+
"photo.reuseNote": "Photography started from {project} · {revision} · {digest} · camera {camera}; destination aim object {target}. The draft may be adjusted before saving.",
|
|
944
|
+
"photo.reuseTargetRequired": "Choose an aim target in the destination work.",
|
|
906
945
|
'editor.title': 'Edit object',
|
|
907
946
|
'editor.unknownSettings': 'Measured render settings are missing. This preview cannot establish the edit difference.',
|
|
908
947
|
'editor.missingBaseline': 'No before preview has the same camera, frame and measured render settings. The edit difference is unavailable.',
|
|
@@ -923,6 +962,9 @@ window.__ModuleLoader__.load({
|
|
|
923
962
|
'editor.animated': 'Channels driven by animation are disabled.',
|
|
924
963
|
'editor.invalid': 'Invalid value for {field}.',
|
|
925
964
|
'editor.size': 'Size (mm)',
|
|
965
|
+
'editor.boxWidth': 'Width (mm)',
|
|
966
|
+
'editor.boxDepth': 'Depth (mm)',
|
|
967
|
+
'editor.boxHeight': 'Height (mm)',
|
|
926
968
|
'editor.radius': 'Radius (mm)',
|
|
927
969
|
'editor.handledCup': 'Handled cup',
|
|
928
970
|
'editor.wallThickness': 'Wall thickness (mm)',
|
|
@@ -2018,7 +2060,13 @@ window.__ModuleLoader__.load({
|
|
|
2018
2060
|
}
|
|
2019
2061
|
const generator = draft.entity.generator
|
|
2020
2062
|
if (generator) {
|
|
2063
|
+
if (generator.dimensions !== undefined) {
|
|
2064
|
+
if (!['cube', 'rounded_box'].includes(generator.shape) || generator.size !== undefined
|
|
2065
|
+
|| !Array.isArray(generator.dimensions) || generator.dimensions.length !== 3) bad('dimensions')
|
|
2066
|
+
else generator.dimensions.forEach((value, axis) => scalar(value, `dimensions.${EDITOR_AXES[axis]}`, Number.MIN_VALUE))
|
|
2067
|
+
}
|
|
2021
2068
|
for (const key of GENERATOR_FIELDS[generator.shape] || []) {
|
|
2069
|
+
if (key === 'size' && generator.dimensions !== undefined) continue
|
|
2022
2070
|
const bounds = generatorIntegerBounds(generator, key)
|
|
2023
2071
|
if (bounds?.[2] && generator[key] % bounds[2]) bad(key)
|
|
2024
2072
|
scalar(generator[key], key, key === 'rootTension' ? 1 : bounds ? bounds[0] : Number.MIN_VALUE,
|
|
@@ -2148,10 +2196,10 @@ window.__ModuleLoader__.load({
|
|
|
2148
2196
|
return state.editorDrafts?.[editorKey(state.activeProjectId, state.editorEntityId)] || null
|
|
2149
2197
|
}
|
|
2150
2198
|
|
|
2151
|
-
function createPhotographyDraft(scene, projectId, previous = {}) {
|
|
2199
|
+
function createPhotographyDraft(scene, projectId, previous = {}, projectsRoot = null) {
|
|
2152
2200
|
const cameras = (scene.nodes.cameras || []).map(item => editorClone(item.definition)).filter(Boolean)
|
|
2153
2201
|
const lights = (scene.nodes.lights || []).map(item => editorClone(item.definition)).filter(Boolean)
|
|
2154
|
-
return { projectId, baseRevision: scene.revision, cameras, lights, original: editorClone({ cameras, lights }),
|
|
2202
|
+
return { projectId, projectsRoot, baseRevision: scene.revision, cameras, lights, original: editorClone({ cameras, lights }),
|
|
2155
2203
|
entities: editorClone(scene.nodes.entities), tracks: editorClone(scene.nodes.animationTracks || []),
|
|
2156
2204
|
cameraId: cameras.some(item => item.id === previous.cameraId) ? previous.cameraId : scene.project.activeCamera || cameras[0]?.id || '',
|
|
2157
2205
|
lightId: lights.some(item => item.id === previous.lightId) ? previous.lightId : lights[0]?.id || '',
|
|
@@ -2161,6 +2209,8 @@ window.__ModuleLoader__.load({
|
|
|
2161
2209
|
const photographyDraftFor = state => state.photographyDrafts?.[state.activeProjectId] || null
|
|
2162
2210
|
const photographyBusy = work => Boolean(work?.saving || work?.previewing || work?.restoring)
|
|
2163
2211
|
const photographyAim = camera => camera?.targetEntityId !== undefined ? 'entity' : camera?.targetPoint !== undefined ? 'point' : 'free'
|
|
2212
|
+
const photographySameLights = (a, b) => creationSignature(Object.fromEntries(a.map(item => [item.id, item])))
|
|
2213
|
+
=== creationSignature(Object.fromEntries(b.map(item => [item.id, item])))
|
|
2164
2214
|
const photographyAimLocked = (draft, camera) => EDITOR_AXES.some(axis => editorTrackLocked(draft, 'camera', camera.id, `rotationEuler.${axis}`))
|
|
2165
2215
|
function photographyErrors(draft) {
|
|
2166
2216
|
const errors = [], bad = key => errors.push(key), number = (value, min, inclusive = true) => Number.isFinite(value) && (inclusive ? value >= min : value > min)
|
|
@@ -2181,14 +2231,14 @@ window.__ModuleLoader__.load({
|
|
|
2181
2231
|
if (item.targetPoint !== undefined && !vector(item.targetPoint)) bad(`${item.id}.targetPoint`)
|
|
2182
2232
|
if (item.targetEntityId !== undefined && item.targetPoint !== undefined) bad(`${item.id}.target`)
|
|
2183
2233
|
const targetChanged = !editorEqual([item.targetEntityId, item.targetPoint], [original?.targetEntityId, original?.targetPoint])
|
|
2184
|
-
if (targetChanged &&
|
|
2234
|
+
if (targetChanged && photographyAimLocked(draft, item)) bad(`${item.id}.target`)
|
|
2185
2235
|
} else {
|
|
2186
2236
|
if (!number(editorNumberDefault(item.energy, 100), 0)) bad(`${item.id}.energy`)
|
|
2187
2237
|
if (!Array.isArray(item.color ?? [1, 1, 1]) || ![3, 4].includes((item.color ?? [1, 1, 1]).length)
|
|
2188
2238
|
|| !(item.color ?? [1, 1, 1]).every(value => number(value, 0) && value <= 1)) bad(`${item.id}.color`)
|
|
2189
2239
|
if (item.type !== 'sun' && !number(editorNumberDefault(item.size, item.type === 'area' ? 1 : .25), 0, false)) bad(`${item.id}.size`)
|
|
2190
|
-
if (!original && item.type !== 'area') bad(`${item.id}.type`)
|
|
2191
|
-
if (
|
|
2240
|
+
if (!draft.reusedLights && ((!original && item.type !== 'area') || (original && item.type !== original.type))) bad(`${item.id}.type`)
|
|
2241
|
+
if (!['area', 'point', 'spot', 'sun'].includes(item.type)) bad(`${item.id}.type`)
|
|
2192
2242
|
}
|
|
2193
2243
|
}
|
|
2194
2244
|
if (!draft.cameras.some(camera => camera.id === draft.cameraId)) bad('camera')
|
|
@@ -2200,11 +2250,18 @@ window.__ModuleLoader__.load({
|
|
|
2200
2250
|
const errors = photographyErrors(draft)
|
|
2201
2251
|
if (errors.length) throw new Error(t('photo.invalid', { fields: errors.join(', ') }))
|
|
2202
2252
|
const operations = []
|
|
2253
|
+
const replaceLights = draft.reusedLights && !photographySameLights(draft.lights, draft.original.lights)
|
|
2203
2254
|
for (const [kind, items, fields] of [['camera', draft.cameras, ['lens', 'targetEntityId', 'targetPoint']], ['light', draft.lights, ['energy', 'color', 'size']]]) {
|
|
2255
|
+
if (kind === 'light' && replaceLights) {
|
|
2256
|
+
for (const light of draft.original.lights) operations.push({ op: 'light.remove', lightId: light.id })
|
|
2257
|
+
for (const light of items) operations.push({ op: 'light.add', light: editorClone(light) })
|
|
2258
|
+
continue
|
|
2259
|
+
}
|
|
2204
2260
|
for (const item of items) {
|
|
2205
2261
|
const original = draft.original[`${kind}s`].find(old => old.id === item.id)
|
|
2206
2262
|
if (!original) { operations.push({ op: 'light.add', light: editorClone(item) }); continue }
|
|
2207
2263
|
const changes = {}
|
|
2264
|
+
if (kind === 'camera' && photographyAim(item) === 'free' && photographyAim(original) !== 'free') changes.clearTarget = true
|
|
2208
2265
|
for (const key of fields) if (item[key] !== undefined && !editorEqual(item[key], original[key])) changes[key] = editorClone(item[key])
|
|
2209
2266
|
const transform = {}
|
|
2210
2267
|
for (const key of ['location', 'rotationEuler']) if (!editorEqual(item.transform?.[key], original.transform?.[key])) transform[key] = editorClone(item.transform[key])
|
|
@@ -2212,10 +2269,40 @@ window.__ModuleLoader__.load({
|
|
|
2212
2269
|
if (Object.keys(changes).length) operations.push({ op: `${kind}.update`, [`${kind}Id`]: item.id, ...changes })
|
|
2213
2270
|
}
|
|
2214
2271
|
}
|
|
2215
|
-
|
|
2272
|
+
const note = draft.reuseSource ? t('photo.reuseNote', draft.reuseSource) : null
|
|
2273
|
+
return { projectId: draft.projectId, baseRevision: draft.baseRevision, actor: 'ui', saveCheckpoint: true, renderPreview: false, operations, ...(note ? { note } : {}) }
|
|
2216
2274
|
}
|
|
2217
2275
|
function photographyDirty(draft) {
|
|
2218
|
-
return Boolean(draft &&
|
|
2276
|
+
return Boolean(draft && (creationSignature(draft.cameras) !== creationSignature(draft.original.cameras)
|
|
2277
|
+
|| !photographySameLights(draft.lights, draft.original.lights)))
|
|
2278
|
+
}
|
|
2279
|
+
function reusePhotography(draft, source, cameraId, targetEntityId) {
|
|
2280
|
+
const camera = source?.nodes?.cameras?.find(item => item.id === cameraId)?.definition
|
|
2281
|
+
const target = draft?.cameras?.find(item => item.id === draft.cameraId)
|
|
2282
|
+
if (!camera || !target || !Array.isArray(source.nodes.lights)) throw new Error(t('photo.reuseUnavailable'))
|
|
2283
|
+
const next = editorClone(draft), selected = next.cameras.find(item => item.id === next.cameraId)
|
|
2284
|
+
selected.lens = editorNumberDefault(camera.lens, 50)
|
|
2285
|
+
selected.transform ||= {}
|
|
2286
|
+
selected.transform.location = editorClone(camera.transform?.location || [0, 0, 0])
|
|
2287
|
+
delete selected.targetEntityId; delete selected.targetPoint
|
|
2288
|
+
if (photographyAim(camera) === 'entity') {
|
|
2289
|
+
if (!next.entities.some(item => item.id === targetEntityId)) throw new Error(t('photo.reuseTargetRequired'))
|
|
2290
|
+
selected.targetEntityId = targetEntityId
|
|
2291
|
+
} else if (photographyAim(camera) === 'point') selected.targetPoint = editorClone(camera.targetPoint)
|
|
2292
|
+
if (photographyAim(camera) === 'free') selected.transform.rotationEuler = editorClone(camera.transform?.rotationEuler || [0, 0, 0])
|
|
2293
|
+
else {
|
|
2294
|
+
const original = next.original.cameras.find(item => item.id === selected.id)
|
|
2295
|
+
if (original?.transform?.rotationEuler) selected.transform.rotationEuler = editorClone(original.transform.rotationEuler)
|
|
2296
|
+
else delete selected.transform.rotationEuler
|
|
2297
|
+
}
|
|
2298
|
+
next.lights = source.nodes.lights.map(item => editorClone(item.definition))
|
|
2299
|
+
if (next.lights.some(item => !item)) throw new Error(t('photo.reuseUnavailable'))
|
|
2300
|
+
next.lightId = next.lights.some(item => item.id === next.lightId) ? next.lightId : next.lights[0]?.id || ''
|
|
2301
|
+
next.reusedLights = true
|
|
2302
|
+
next.reuseSource = { project: source.project.id, revision: source.revision, digest: source.digest, camera: cameraId, target: targetEntityId || '—' }
|
|
2303
|
+
const errors = photographyErrors(next)
|
|
2304
|
+
if (errors.length) throw new Error(t('photo.invalid', { fields: errors.join(', ') }))
|
|
2305
|
+
return next
|
|
2219
2306
|
}
|
|
2220
2307
|
function inspectionReceiptValid(receipt, request, digest) {
|
|
2221
2308
|
const artifacts = receipt?.artifacts, view = request.views[0]
|
|
@@ -2622,6 +2709,7 @@ window.__ModuleLoader__.load({
|
|
|
2622
2709
|
imageDownloads: {},
|
|
2623
2710
|
creationWork: null,
|
|
2624
2711
|
creationRequestProtocol: null,
|
|
2712
|
+
cameraTargetResetSupported: false,
|
|
2625
2713
|
creationDrafts: [],
|
|
2626
2714
|
creationDraftStorageStatus: null,
|
|
2627
2715
|
creationDraftMissingRecipe: null,
|
|
@@ -2643,6 +2731,7 @@ window.__ModuleLoader__.load({
|
|
|
2643
2731
|
photographyDrafts: {},
|
|
2644
2732
|
photographyWork: {},
|
|
2645
2733
|
photographyEdits: {},
|
|
2734
|
+
photographyReuse: {},
|
|
2646
2735
|
editorEntityId: null,
|
|
2647
2736
|
editorDrafts: {},
|
|
2648
2737
|
editorLastEdits: {},
|
|
@@ -2711,6 +2800,8 @@ window.__ModuleLoader__.load({
|
|
|
2711
2800
|
const draftLifecycle = settings.creationDraftLifecycle ?? globalThis.window
|
|
2712
2801
|
let fileChoiceSignature = null, fileChoicesStopped = false
|
|
2713
2802
|
const photographyControllers = new Map()
|
|
2803
|
+
const photographySourceControllers = new Map()
|
|
2804
|
+
let photographySourceSequence = 0
|
|
2714
2805
|
const photographyWork = (projectId, changes) => set({ photographyWork: { ...data.photographyWork, [projectId]: { ...data.photographyWork[projectId], ...changes } } })
|
|
2715
2806
|
const inspectionControllers = new Map()
|
|
2716
2807
|
const imageDownloadControllers = new Map()
|
|
@@ -2765,7 +2856,9 @@ window.__ModuleLoader__.load({
|
|
|
2765
2856
|
// Automatic edit images are source snapshots too. They cannot override
|
|
2766
2857
|
// fresh rows from another store, even when its project/revision IDs match.
|
|
2767
2858
|
if (Object.hasOwn(patch, 'projectsRoot') && patch.projectsRoot !== data.projectsRoot) {
|
|
2768
|
-
|
|
2859
|
+
for (const controller of photographySourceControllers.values()) controller.abort()
|
|
2860
|
+
photographySourceControllers.clear()
|
|
2861
|
+
patch = { ...patch, editorComparison: null, photographyReuse: {} }
|
|
2769
2862
|
}
|
|
2770
2863
|
// Retire the old source before selection/creation navigation can expose
|
|
2771
2864
|
// it with a new project. A load supplies the complete new source atomically.
|
|
@@ -2862,6 +2955,7 @@ window.__ModuleLoader__.load({
|
|
|
2862
2955
|
recipeCatalog: payload.recipeCatalog ?? { recipes: [], errors: [] },
|
|
2863
2956
|
projectsRoot: payload.projectsRoot ?? null,
|
|
2864
2957
|
creationRequestProtocol: payload.creationRequestProtocol ?? null,
|
|
2958
|
+
cameraTargetResetSupported: payload.cameraTargetResetSupported === true,
|
|
2865
2959
|
selected: payload.selected ?? null,
|
|
2866
2960
|
activeProjectId: active,
|
|
2867
2961
|
currentRevision: payload.selected ? payload.selected.currentRevision : null,
|
|
@@ -2909,7 +3003,7 @@ window.__ModuleLoader__.load({
|
|
|
2909
3003
|
if (patch.selected?.scene && active) {
|
|
2910
3004
|
const photography = data.photographyDrafts[active]
|
|
2911
3005
|
if ((!photography || !photographyDirty(photography)) && !data.photographyWork[active]?.saving) {
|
|
2912
|
-
patch.photographyDrafts = { ...data.photographyDrafts, [active]: createPhotographyDraft(patch.selected.scene, active, photography) }
|
|
3006
|
+
patch.photographyDrafts = { ...data.photographyDrafts, [active]: createPhotographyDraft(patch.selected.scene, active, photography, patch.projectsRoot) }
|
|
2913
3007
|
}
|
|
2914
3008
|
const draft = data.briefDrafts[active], work = data.briefWork[active]
|
|
2915
3009
|
if (!draft || !briefDirty(draft) && !work?.uploading && !work?.saving) {
|
|
@@ -3036,6 +3130,46 @@ window.__ModuleLoader__.load({
|
|
|
3036
3130
|
|
|
3037
3131
|
const acceptsFileChoice = (source, field) => !fileChoicesStopped && typeof source === 'string'
|
|
3038
3132
|
&& FILE_FIELDS.includes(field) && source === fileChoiceKey(data, field) && fileChoiceAllowed(data, field)
|
|
3133
|
+
const loadPhotographySource = async requestedRevision => {
|
|
3134
|
+
const draft = photographyDraftFor(data), projectId = data.activeProjectId, selection = data.photographyReuse[projectId]
|
|
3135
|
+
if (!data.cameraTargetResetSupported || !draft || !selection?.sourceProjectId || photographyBusy(data.photographyWork[projectId])
|
|
3136
|
+
|| draft.baseRevision !== data.selected?.scene?.revision || draft.projectsRoot !== data.projectsRoot) return
|
|
3137
|
+
photographySourceControllers.get(projectId)?.abort()
|
|
3138
|
+
const controller = new AbortController(), sequence = ++photographySourceSequence
|
|
3139
|
+
photographySourceControllers.set(projectId, controller)
|
|
3140
|
+
const root = data.projectsRoot, baseRevision = draft.baseRevision, sourceProjectId = selection.sourceProjectId
|
|
3141
|
+
const alive = () => running && !controller.signal.aborted && photographySourceControllers.get(projectId) === controller && data.projectsRoot === root
|
|
3142
|
+
const update = changes => set({ photographyReuse: { ...data.photographyReuse, [projectId]: { ...data.photographyReuse[projectId], ...changes } } })
|
|
3143
|
+
update({ source: null, loading: true, error: null, applied: false, unchanged: false, replaceDraft: false, projectsRoot: root, baseRevision, sequence })
|
|
3144
|
+
const deadline = setTimeout(() => controller.abort(), 30000)
|
|
3145
|
+
const read = (path, route) => readRoute((url, options) => fetchImpl(url, { ...options, signal: controller.signal }), path, route)
|
|
3146
|
+
try {
|
|
3147
|
+
const listed = await read(projectRoute(sourceProjectId, '/revisions'), 'project.revisions')
|
|
3148
|
+
if (!alive()) return
|
|
3149
|
+
const list = listed.payload
|
|
3150
|
+
if (listed.status !== 'ok' || list?.projectId !== sourceProjectId || !Array.isArray(list.revisions)
|
|
3151
|
+
|| list.revisions.some(item => !/^r[0-9]{4,}$/.test(item.revision))) throw new Error(listed.error?.message || t('photo.reuseUnavailable'))
|
|
3152
|
+
const revision = requestedRevision || list.currentRevision, saved = list.revisions.find(item => item.revision === revision)
|
|
3153
|
+
if (!saved) throw new Error(t('photo.reuseUnavailable'))
|
|
3154
|
+
update({ revisions: editorClone(list.revisions), revision })
|
|
3155
|
+
const result = await read(`${projectRoute(sourceProjectId, '/scene')}?revision=${encodeURIComponent(revision)}`, 'project.scene')
|
|
3156
|
+
if (!alive()) return
|
|
3157
|
+
const payload = result.payload, scene = payload?.scene
|
|
3158
|
+
if (result.status !== 'ok' || payload?.projectId !== sourceProjectId || payload.revision !== revision
|
|
3159
|
+
|| scene?.revision !== revision || scene?.project?.id !== sourceProjectId || scene.digest !== payload.digest
|
|
3160
|
+
|| !/^[a-f0-9]{64}$/.test(payload.digest || '') || (saved.digest && saved.digest !== payload.digest)
|
|
3161
|
+
|| !Array.isArray(scene.nodes?.cameras) || !Array.isArray(scene.nodes?.lights)) throw new Error(result.error?.message || t('photo.reuseUnavailable'))
|
|
3162
|
+
update({ source: editorClone(scene), cameraId: scene.nodes.cameras.find(item => item.id === scene.project.activeCamera)?.id || scene.nodes.cameras[0]?.id || '', targetEntityId: '', error: null })
|
|
3163
|
+
} catch (error) {
|
|
3164
|
+
if (alive()) update({ source: null, error: error.message || String(error) })
|
|
3165
|
+
} finally {
|
|
3166
|
+
clearTimeout(deadline)
|
|
3167
|
+
if (photographySourceControllers.get(projectId) === controller) {
|
|
3168
|
+
photographySourceControllers.delete(projectId)
|
|
3169
|
+
if (running && data.projectsRoot === root) update({ loading: false, ...(controller.signal.aborted ? { source: null, error: t('photo.reuseUnavailable') } : {}) })
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3039
3173
|
const actions = {
|
|
3040
3174
|
acceptsFileChoice,
|
|
3041
3175
|
setAppearance: value => {
|
|
@@ -3439,7 +3573,43 @@ window.__ModuleLoader__.load({
|
|
|
3439
3573
|
resetPhotography: () => {
|
|
3440
3574
|
const projectId = data.activeProjectId, scene = data.selected?.scene
|
|
3441
3575
|
if (!scene || photographyBusy(data.photographyWork[projectId])) return
|
|
3442
|
-
set({ photographyDrafts: { ...data.photographyDrafts, [projectId]: createPhotographyDraft(scene, projectId, photographyDraftFor(data)) }
|
|
3576
|
+
set({ photographyDrafts: { ...data.photographyDrafts, [projectId]: createPhotographyDraft(scene, projectId, photographyDraftFor(data), data.projectsRoot) },
|
|
3577
|
+
photographyReuse: { ...data.photographyReuse, ...(data.photographyReuse[projectId] ? { [projectId]: { ...data.photographyReuse[projectId], applied: false, unchanged: false, replaceDraft: false } } : {}) } })
|
|
3578
|
+
},
|
|
3579
|
+
setPhotographySource: (field, value) => {
|
|
3580
|
+
const projectId = data.activeProjectId, selection = data.photographyReuse[projectId] || {}
|
|
3581
|
+
if (!data.cameraTargetResetSupported || !photographyDraftFor(data) || photographyBusy(data.photographyWork[projectId])) return
|
|
3582
|
+
if (field === 'project') {
|
|
3583
|
+
if (value && !data.projects.some(item => item.projectId === value && !item.creationPending)) return
|
|
3584
|
+
photographySourceControllers.get(projectId)?.abort(); photographySourceControllers.delete(projectId)
|
|
3585
|
+
set({ photographyReuse: { ...data.photographyReuse, [projectId]: { sourceProjectId: value, source: null, revisions: [], revision: '', cameraId: '', targetEntityId: '', replaceDraft: false } } })
|
|
3586
|
+
if (value) return loadPhotographySource()
|
|
3587
|
+
} else if (field === 'revision') {
|
|
3588
|
+
if (!selection.revisions?.some(item => item.revision === value)) return
|
|
3589
|
+
return loadPhotographySource(value)
|
|
3590
|
+
} else if (field === 'camera' && selection.source?.nodes.cameras.some(item => item.id === value)) {
|
|
3591
|
+
set({ photographyReuse: { ...data.photographyReuse, [projectId]: { ...selection, cameraId: value, targetEntityId: '', applied: false, unchanged: false } } })
|
|
3592
|
+
} else if (field === 'target' && (!value || photographyDraftFor(data).entities.some(item => item.id === value))) {
|
|
3593
|
+
set({ photographyReuse: { ...data.photographyReuse, [projectId]: { ...selection, targetEntityId: value, applied: false, unchanged: false } } })
|
|
3594
|
+
} else if (field === 'replace' && typeof value === 'boolean') {
|
|
3595
|
+
set({ photographyReuse: { ...data.photographyReuse, [projectId]: { ...selection, replaceDraft: value } } })
|
|
3596
|
+
}
|
|
3597
|
+
},
|
|
3598
|
+
reloadPhotographySource: () => loadPhotographySource(data.photographyReuse[data.activeProjectId]?.revision),
|
|
3599
|
+
reusePhotography: () => {
|
|
3600
|
+
const projectId = data.activeProjectId, selection = data.photographyReuse[projectId], draft = photographyDraftFor(data)
|
|
3601
|
+
if (!data.cameraTargetResetSupported || !draft || !selection?.source || selection.loading || photographyBusy(data.photographyWork[projectId])
|
|
3602
|
+
|| selection.projectsRoot !== data.projectsRoot || draft.projectsRoot !== data.projectsRoot
|
|
3603
|
+
|| selection.baseRevision !== draft.baseRevision || draft.baseRevision !== data.selected?.scene?.revision
|
|
3604
|
+
|| (photographyDirty(draft) && !selection.replaceDraft)) return
|
|
3605
|
+
try {
|
|
3606
|
+
const next = reusePhotography(draft, selection.source, selection.cameraId, selection.targetEntityId)
|
|
3607
|
+
set({ photographyDrafts: { ...data.photographyDrafts, [projectId]: next }, photographyReuse: {
|
|
3608
|
+
...data.photographyReuse, [projectId]: { ...selection, replaceDraft: false, error: null, applied: photographyDirty(next), unchanged: !photographyDirty(next) },
|
|
3609
|
+
} })
|
|
3610
|
+
} catch (error) {
|
|
3611
|
+
set({ photographyReuse: { ...data.photographyReuse, [projectId]: { ...selection, applied: false, error: error.message || String(error) } } })
|
|
3612
|
+
}
|
|
3443
3613
|
},
|
|
3444
3614
|
updatePhotography: (kind, path, value) => {
|
|
3445
3615
|
const current = photographyDraftFor(data)
|
|
@@ -3456,7 +3626,7 @@ window.__ModuleLoader__.load({
|
|
|
3456
3626
|
} else if (kind === 'camera' && path.length === 1 && path[0] === 'aim') {
|
|
3457
3627
|
if (!['free', 'entity', 'point'].includes(value) || photographyAimLocked(draft, item)) return
|
|
3458
3628
|
const original = draft.original.cameras.find(camera => camera.id === item.id)
|
|
3459
|
-
if (value === 'free' && photographyAim(original) !== 'free') return
|
|
3629
|
+
if (value === 'free' && photographyAim(original) !== 'free' && !data.cameraTargetResetSupported) return
|
|
3460
3630
|
delete item.targetEntityId; delete item.targetPoint
|
|
3461
3631
|
if (value === 'entity') item.targetEntityId = draft.entities.find(entity => entity.id === data.editorEntityId)?.id || data.selected?.scene?.project?.reviewSubjectId || draft.entities[0]?.id || ''
|
|
3462
3632
|
if (value === 'point') item.targetPoint = [0, 0, 0]
|
|
@@ -3472,7 +3642,8 @@ window.__ModuleLoader__.load({
|
|
|
3472
3642
|
else if (kind === 'light' && path[0] === 'color' && path.length === 2 && [0, 1, 2].includes(path[1])) { item.color ||= [1, 1, 1]; item.color[path[1]] = value }
|
|
3473
3643
|
else return
|
|
3474
3644
|
}
|
|
3475
|
-
set({ photographyDrafts: { ...data.photographyDrafts, [current.projectId]: draft }
|
|
3645
|
+
set({ photographyDrafts: { ...data.photographyDrafts, [current.projectId]: draft },
|
|
3646
|
+
photographyReuse: { ...data.photographyReuse, ...(data.photographyReuse[current.projectId] ? { [current.projectId]: { ...data.photographyReuse[current.projectId], unchanged: false } } : {}) } })
|
|
3476
3647
|
},
|
|
3477
3648
|
addPhotographyLight: () => {
|
|
3478
3649
|
const current = photographyDraftFor(data)
|
|
@@ -3496,7 +3667,9 @@ window.__ModuleLoader__.load({
|
|
|
3496
3667
|
setIn('busy', 'editor', false)
|
|
3497
3668
|
if (outcome.ok) {
|
|
3498
3669
|
const saved = outcome.payload.revision, drafts = { ...data.photographyDrafts, [projectId]: { ...editorClone(draft), baseRevision: outcome.payload.revision.revision, original: editorClone({ cameras: draft.cameras, lights: draft.lights }) } }
|
|
3499
|
-
set({ photographyDrafts: drafts,
|
|
3670
|
+
set({ photographyDrafts: drafts,
|
|
3671
|
+
photographyReuse: { ...data.photographyReuse, ...(data.photographyReuse[projectId] ? { [projectId]: { ...data.photographyReuse[projectId], baseRevision: saved.revision, applied: false, unchanged: false, replaceDraft: false } } : {}) },
|
|
3672
|
+
photographyEdits: { ...data.photographyEdits, [projectId]: {
|
|
3500
3673
|
before: draft.baseRevision, after: saved.revision, digest: saved.digest, cameraId: draft.cameraId, frame: draft.frame, samples: draft.samples,
|
|
3501
3674
|
changes: [...new Set(patch.operations.map(operation => operation.op.split('.')[0]))] } } })
|
|
3502
3675
|
photographyWork(projectId, { saving: false, error: null, artifact: null })
|
|
@@ -3896,6 +4069,8 @@ window.__ModuleLoader__.load({
|
|
|
3896
4069
|
if (timer !== null) { clearInterval(timer); timer = null }
|
|
3897
4070
|
for (const controller of photographyControllers.values()) controller.abort()
|
|
3898
4071
|
photographyControllers.clear()
|
|
4072
|
+
for (const controller of photographySourceControllers.values()) controller.abort()
|
|
4073
|
+
photographySourceControllers.clear()
|
|
3899
4074
|
for (const controller of inspectionControllers.values()) controller.abort()
|
|
3900
4075
|
inspectionControllers.clear()
|
|
3901
4076
|
for (const controller of imageDownloadControllers.values()) controller.abort('stopped')
|
|
@@ -4292,7 +4467,11 @@ window.__ModuleLoader__.load({
|
|
|
4292
4467
|
el('p', { className: 'db-muted', key: 'local-size' }, t('editor.localDimensions', { scale: (draft.entity.transform.scale || [1, 1, 1]).join(' × ') })),
|
|
4293
4468
|
geometry.shape === 'rounded_box' ? el('p', { className: 'db-muted', key: 'rounded' }, t('editor.roundedRequired')) : null,
|
|
4294
4469
|
geometry.shape === 'handled_cup' ? el('p', { className: 'db-muted', key: 'cup-help' }, t('editor.cupHelp')) : null,
|
|
4295
|
-
...(GENERATOR_FIELDS[geometry.shape] || []).filter(key =>
|
|
4470
|
+
...(GENERATOR_FIELDS[geometry.shape] || []).filter(key => !(key === 'size' && geometry.dimensions !== undefined)
|
|
4471
|
+
&& (geometry.shape !== 'handled_cup' || !CUP_ADVANCED_FIELDS.includes(key))).map(geometryField),
|
|
4472
|
+
...(geometry.dimensions !== undefined ? [t('editor.boxWidth'), t('editor.boxDepth'), t('editor.boxHeight')].map((label, index) => numeric(
|
|
4473
|
+
label, `editor-generator-dimensions-${EDITOR_AXES[index]}`, geometry.dimensions[index],
|
|
4474
|
+
value => actions.updateEditor('generator', ['dimensions', index], value), { factor: 1000, min: Number.MIN_VALUE })) : []),
|
|
4296
4475
|
geometry.shape === 'handled_cup' ? el('details', { key: 'cup-advanced' },
|
|
4297
4476
|
el('summary', null, t('editor.cupAdvanced')), ...CUP_ADVANCED_FIELDS.map(geometryField)) : null,
|
|
4298
4477
|
toggle(t('editor.bevel'), 'editor-generator-bevel', Boolean(geometry.bevel), value => actions.updateEditor('generator', ['bevel'], value ? { width: 0.001, segments: 3 } : undefined), geometry.shape === 'rounded_box'),
|
|
@@ -4530,18 +4709,38 @@ window.__ModuleLoader__.load({
|
|
|
4530
4709
|
{ factor, locked: locked || editorTrackLocked(draft, kind, kind === 'camera' ? camera.id : light.id, `${field}.${axis}`) })))
|
|
4531
4710
|
const aim = photographyAim(camera), originalCamera = draft?.original.cameras.find(item => item.id === camera?.id)
|
|
4532
4711
|
const aimLocked = camera && photographyAimLocked(draft, camera)
|
|
4712
|
+
const reuse = state.photographyReuse?.[projectId] || {}, sourceCamera = reuse.source?.nodes.cameras.find(item => item.id === reuse.cameraId)?.definition
|
|
4713
|
+
const reuseStale = reuse.source && (reuse.projectsRoot !== state.projectsRoot || reuse.baseRevision !== draft?.baseRevision || draft?.projectsRoot !== state.projectsRoot)
|
|
4533
4714
|
return el('section', { className: 'db-card', 'data-photography-editor': projectId },
|
|
4534
4715
|
el('h4', null, t('photo.title')), el('p', { className: 'db-muted' }, t('photo.help')),
|
|
4535
4716
|
draft ? el('div', null,
|
|
4536
4717
|
el('p', { className: 'db-muted', 'data-photography-base': draft.baseRevision }, t('photo.base', { revision: draft.baseRevision })),
|
|
4537
4718
|
conflict ? el('p', { className: 'db-error', 'data-photography-conflict': true }, t('photo.conflict', { before: draft.baseRevision, current: state.selected?.scene?.revision })) : null,
|
|
4719
|
+
state.cameraTargetResetSupported ? el('div', { 'data-photography-reuse': true },
|
|
4720
|
+
el('h4', null, t('photo.reuseTitle')), el('p', { className: 'db-muted' }, t('photo.reuseHelp')),
|
|
4721
|
+
choose(t('photo.reuseProject'), 'source-project', reuse.sourceProjectId || '', [['', '—'], ...state.projects.filter(item => !item.creationPending).map(item => [item.projectId, item.title || item.projectId])], value => actions.setPhotographySource('project', value)),
|
|
4722
|
+
choose(t('photo.reuseRevision'), 'source-revision', reuse.revision || '', [['', '—'], ...(reuse.revisions || []).map(item => [item.revision, item.revision])], value => actions.setPhotographySource('revision', value), reuse.loading || !reuse.revisions?.length),
|
|
4723
|
+
reuse.loading ? el('p', { role: 'status' }, t('photo.reuseReading')) : null,
|
|
4724
|
+
reuse.source ? el('div', { 'data-photography-source': `${reuse.sourceProjectId}:${reuse.revision}` },
|
|
4725
|
+
el('p', { className: 'db-muted' }, t('photo.reuseSource', { project: reuse.sourceProjectId, revision: reuse.revision, digest: shortDigest(reuse.source.digest), count: reuse.source.nodes.lights.length })),
|
|
4726
|
+
choose(t('photo.reuseCamera'), 'source-camera', reuse.cameraId, reuse.source.nodes.cameras.map(item => [item.id, item.id]), value => actions.setPhotographySource('camera', value)),
|
|
4727
|
+
photographyAim(sourceCamera) === 'entity' ? choose(t('photo.reuseTarget'), 'source-target', reuse.targetEntityId || '', [['', '—'], ...draft.entities.map(item => [item.id, item.id])], value => actions.setPhotographySource('target', value)) : null,
|
|
4728
|
+
photographyDirty(draft) ? el('label', { className: 'db-row' }, el('input', { type: 'checkbox', 'data-field': 'photo-source-replace', checked: Boolean(reuse.replaceDraft), disabled: disabled || reuse.loading,
|
|
4729
|
+
onChange: event => actions.setPhotographySource('replace', Boolean(event.target.checked)) }), t('photo.reuseReplace')) : null,
|
|
4730
|
+
reuseStale ? el('p', { className: 'db-error' }, t('photo.reuseStale')) : null,
|
|
4731
|
+
reuse.applied ? el('p', { role: 'status', 'data-photography-reused': true }, t('photo.reuseApplied')) : null,
|
|
4732
|
+
reuse.unchanged ? el('p', { role: 'status', 'data-photography-reuse-unchanged': true }, t('photo.reuseSame')) : null) : null,
|
|
4733
|
+
reuse.error ? el('p', { className: 'db-error', role: 'status', 'data-photography-source-error': true }, reuse.error) : null,
|
|
4734
|
+
el('div', { className: 'db-inline' },
|
|
4735
|
+
Button({ action: 'photo-source-reload', disabled: disabled || !reuse.sourceProjectId || reuse.loading, onClick: actions.reloadPhotographySource, children: t('photo.reuseReload') }),
|
|
4736
|
+
Button({ action: 'photo-reuse', disabled: disabled || reuse.loading || !reuse.source || reuseStale || (photographyDirty(draft) && !reuse.replaceDraft) || (photographyAim(sourceCamera) === 'entity' && !reuse.targetEntityId), onClick: actions.reusePhotography, children: t('photo.reuseApply') }))) : el('p', { className: 'db-muted' }, t('photo.reuseUpgrade')),
|
|
4538
4737
|
el('div', { className: 'db-grid' },
|
|
4539
4738
|
el('div', null,
|
|
4540
4739
|
choose(t('photo.camera'), 'camera', draft.cameraId, draft.cameras.map(item => [item.id, item.id]), id => actions.selectPhotography('camera', id)),
|
|
4541
4740
|
camera ? el('div', null,
|
|
4542
4741
|
numeric(t('photo.lens'), 'camera-lens', editorNumberDefault(camera.lens, 50), update('camera', ['lens']), { min: .001 }),
|
|
4543
4742
|
vector('camera', 'location', camera.transform?.location, t('photo.position')),
|
|
4544
|
-
choose(t('photo.aim'), 'camera-aim', aim, [...(photographyAim(originalCamera) === 'free' ? [['free', t('photo.free')]] : []), ['entity', t('photo.entity')], ['point', t('photo.point')]], update('camera', ['aim']), aimLocked),
|
|
4743
|
+
choose(t('photo.aim'), 'camera-aim', aim, [...(state.cameraTargetResetSupported || photographyAim(originalCamera) === 'free' ? [['free', t('photo.free')]] : []), ['entity', t('photo.entity')], ['point', t('photo.point')]], update('camera', ['aim']), aimLocked),
|
|
4545
4744
|
aim === 'entity' ? choose(t('photo.target'), 'camera-target', camera.targetEntityId, draft.entities.map(item => [item.id, item.id]), update('camera', ['targetEntityId']), aimLocked) : null,
|
|
4546
4745
|
aim === 'point' ? vector('camera', 'targetPoint', camera.targetPoint, t('photo.targetPoint'), 1, aimLocked) : null,
|
|
4547
4746
|
vector('camera', 'rotationEuler', camera.transform?.rotationEuler, t('photo.rotation'), 180 / Math.PI, aim !== 'free'),
|
|
@@ -5955,7 +6154,7 @@ window.__ModuleLoader__.load({
|
|
|
5955
6154
|
inspection: { form: inspectionForm, valid: inspectionFormValid },
|
|
5956
6155
|
assetLibrary: { selection: assetBundleSelection, createDraft: createAssetDraft, buildPatch: buildAssetPatch, error: assetDraftError, key: assetKey },
|
|
5957
6156
|
referenceBrief: { createDraft: createBriefDraft, buildPatch: buildBriefPatch, dirty: briefDirty, valid: briefValid },
|
|
5958
|
-
photographyEditor: { createDraft: createPhotographyDraft, buildPatch: buildPhotographyPatch, errors: photographyErrors, dirty: photographyDirty, draftFor: photographyDraftFor },
|
|
6157
|
+
photographyEditor: { createDraft: createPhotographyDraft, buildPatch: buildPhotographyPatch, errors: photographyErrors, dirty: photographyDirty, draftFor: photographyDraftFor, reuse: reusePhotography },
|
|
5959
6158
|
sceneEditor: { createDraft: createEditorDraft, buildPatch: buildEditorPatch, errors: editorErrors, dirty: editorDirty, previewPair: editorPreviewPair, draftFor: editorDraftFor },
|
|
5960
6159
|
renderView,
|
|
5961
6160
|
mountStandalone,
|
package/lib/index.js
CHANGED
|
@@ -439,6 +439,7 @@ export function statusForError(error) {
|
|
|
439
439
|
return 400
|
|
440
440
|
case BlenderErrorCode.RENDER_JOB_CONFLICT:
|
|
441
441
|
case BlenderErrorCode.REVISION_CONFLICT:
|
|
442
|
+
case BlenderErrorCode.SCENE_FEATURE_UNSUPPORTED:
|
|
442
443
|
return 409
|
|
443
444
|
default:
|
|
444
445
|
return 500
|
|
@@ -633,6 +634,7 @@ export function createHandlers(ctx) {
|
|
|
633
634
|
projects: listed.projects.map(record => buildProjectView(record)),
|
|
634
635
|
projectsRoot: listed.projectsRoot,
|
|
635
636
|
creationRequestProtocol: studio().creationRequestProtocol ?? null,
|
|
637
|
+
cameraTargetResetSupported: studio().cameraTargetResetSupported === true,
|
|
636
638
|
selected: projectId === null || creationPending
|
|
637
639
|
? null
|
|
638
640
|
: await buildProjectState(studio(), projectId, query.revision ?? undefined, approvalThreshold()),
|
|
@@ -669,6 +671,7 @@ export function createHandlers(ctx) {
|
|
|
669
671
|
'project.scene': async ({ params, query }) => {
|
|
670
672
|
const detail = await studio().getScene(params.projectId, { revision: query.revision, full: true })
|
|
671
673
|
return {
|
|
674
|
+
projectId: params.projectId,
|
|
672
675
|
revision: detail.revision,
|
|
673
676
|
digest: detail.digest,
|
|
674
677
|
scene: buildSceneTree(detail.spec, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepblend/dsh-blender-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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.5.0"
|
|
23
23
|
},
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"repository": {
|