@deepblend/dsh-blender-ui 0.3.9 → 0.4.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 +198 -15
- package/lib/index.js +2 -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': '没有同相机、同帧和同渲染设置的编辑前预览,暂时无法比较差异。',
|
|
@@ -903,6 +921,24 @@ window.__ModuleLoader__.load({
|
|
|
903
921
|
"photo.cameraChanges": "cameras",
|
|
904
922
|
"photo.lightChanges": "lighting",
|
|
905
923
|
"photo.restoreFailed": "Restore failed; the photography record is retained: {message}",
|
|
924
|
+
"photo.reuseUnavailable": "Complete photography settings are unavailable. Select the source again.",
|
|
925
|
+
"photo.reuseTitle": "Reuse photography from saved work",
|
|
926
|
+
"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.",
|
|
927
|
+
"photo.reuseProject": "Source work",
|
|
928
|
+
"photo.reuseRevision": "Saved source version",
|
|
929
|
+
"photo.reuseCamera": "Source camera",
|
|
930
|
+
"photo.reuseTarget": "Aim at an object in the destination work",
|
|
931
|
+
"photo.reuseReading": "Reading saved photography settings…",
|
|
932
|
+
"photo.reuseSource": "Source: {project} · {revision} · {digest} · {count} lights",
|
|
933
|
+
"photo.reuseReplace": "Replace the unsaved photography draft (keep other edits)",
|
|
934
|
+
"photo.reuseStale": "The destination draft source changed. Read the photography settings again.",
|
|
935
|
+
"photo.reuseApplied": "Applied to the draft; not saved yet. Check the framing and lighting.",
|
|
936
|
+
"photo.reuseSame": "The current photography matches the source; no new save is needed.",
|
|
937
|
+
"photo.reuseReload": "Read source again",
|
|
938
|
+
"photo.reuseApply": "Apply to photography draft",
|
|
939
|
+
"photo.reuseUpgrade": "Update the plugin and restart DSH to reuse photography settings.",
|
|
940
|
+
"photo.reuseNote": "Photography started from {project} · {revision} · {digest} · camera {camera}; destination aim object {target}. The draft may be adjusted before saving.",
|
|
941
|
+
"photo.reuseTargetRequired": "Choose an aim target in the destination work.",
|
|
906
942
|
'editor.title': 'Edit object',
|
|
907
943
|
'editor.unknownSettings': 'Measured render settings are missing. This preview cannot establish the edit difference.',
|
|
908
944
|
'editor.missingBaseline': 'No before preview has the same camera, frame and measured render settings. The edit difference is unavailable.',
|
|
@@ -2148,10 +2184,10 @@ window.__ModuleLoader__.load({
|
|
|
2148
2184
|
return state.editorDrafts?.[editorKey(state.activeProjectId, state.editorEntityId)] || null
|
|
2149
2185
|
}
|
|
2150
2186
|
|
|
2151
|
-
function createPhotographyDraft(scene, projectId, previous = {}) {
|
|
2187
|
+
function createPhotographyDraft(scene, projectId, previous = {}, projectsRoot = null) {
|
|
2152
2188
|
const cameras = (scene.nodes.cameras || []).map(item => editorClone(item.definition)).filter(Boolean)
|
|
2153
2189
|
const lights = (scene.nodes.lights || []).map(item => editorClone(item.definition)).filter(Boolean)
|
|
2154
|
-
return { projectId, baseRevision: scene.revision, cameras, lights, original: editorClone({ cameras, lights }),
|
|
2190
|
+
return { projectId, projectsRoot, baseRevision: scene.revision, cameras, lights, original: editorClone({ cameras, lights }),
|
|
2155
2191
|
entities: editorClone(scene.nodes.entities), tracks: editorClone(scene.nodes.animationTracks || []),
|
|
2156
2192
|
cameraId: cameras.some(item => item.id === previous.cameraId) ? previous.cameraId : scene.project.activeCamera || cameras[0]?.id || '',
|
|
2157
2193
|
lightId: lights.some(item => item.id === previous.lightId) ? previous.lightId : lights[0]?.id || '',
|
|
@@ -2161,6 +2197,8 @@ window.__ModuleLoader__.load({
|
|
|
2161
2197
|
const photographyDraftFor = state => state.photographyDrafts?.[state.activeProjectId] || null
|
|
2162
2198
|
const photographyBusy = work => Boolean(work?.saving || work?.previewing || work?.restoring)
|
|
2163
2199
|
const photographyAim = camera => camera?.targetEntityId !== undefined ? 'entity' : camera?.targetPoint !== undefined ? 'point' : 'free'
|
|
2200
|
+
const photographySameLights = (a, b) => creationSignature(Object.fromEntries(a.map(item => [item.id, item])))
|
|
2201
|
+
=== creationSignature(Object.fromEntries(b.map(item => [item.id, item])))
|
|
2164
2202
|
const photographyAimLocked = (draft, camera) => EDITOR_AXES.some(axis => editorTrackLocked(draft, 'camera', camera.id, `rotationEuler.${axis}`))
|
|
2165
2203
|
function photographyErrors(draft) {
|
|
2166
2204
|
const errors = [], bad = key => errors.push(key), number = (value, min, inclusive = true) => Number.isFinite(value) && (inclusive ? value >= min : value > min)
|
|
@@ -2181,14 +2219,14 @@ window.__ModuleLoader__.load({
|
|
|
2181
2219
|
if (item.targetPoint !== undefined && !vector(item.targetPoint)) bad(`${item.id}.targetPoint`)
|
|
2182
2220
|
if (item.targetEntityId !== undefined && item.targetPoint !== undefined) bad(`${item.id}.target`)
|
|
2183
2221
|
const targetChanged = !editorEqual([item.targetEntityId, item.targetPoint], [original?.targetEntityId, original?.targetPoint])
|
|
2184
|
-
if (targetChanged &&
|
|
2222
|
+
if (targetChanged && photographyAimLocked(draft, item)) bad(`${item.id}.target`)
|
|
2185
2223
|
} else {
|
|
2186
2224
|
if (!number(editorNumberDefault(item.energy, 100), 0)) bad(`${item.id}.energy`)
|
|
2187
2225
|
if (!Array.isArray(item.color ?? [1, 1, 1]) || ![3, 4].includes((item.color ?? [1, 1, 1]).length)
|
|
2188
2226
|
|| !(item.color ?? [1, 1, 1]).every(value => number(value, 0) && value <= 1)) bad(`${item.id}.color`)
|
|
2189
2227
|
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 (
|
|
2228
|
+
if (!draft.reusedLights && ((!original && item.type !== 'area') || (original && item.type !== original.type))) bad(`${item.id}.type`)
|
|
2229
|
+
if (!['area', 'point', 'spot', 'sun'].includes(item.type)) bad(`${item.id}.type`)
|
|
2192
2230
|
}
|
|
2193
2231
|
}
|
|
2194
2232
|
if (!draft.cameras.some(camera => camera.id === draft.cameraId)) bad('camera')
|
|
@@ -2200,11 +2238,18 @@ window.__ModuleLoader__.load({
|
|
|
2200
2238
|
const errors = photographyErrors(draft)
|
|
2201
2239
|
if (errors.length) throw new Error(t('photo.invalid', { fields: errors.join(', ') }))
|
|
2202
2240
|
const operations = []
|
|
2241
|
+
const replaceLights = draft.reusedLights && !photographySameLights(draft.lights, draft.original.lights)
|
|
2203
2242
|
for (const [kind, items, fields] of [['camera', draft.cameras, ['lens', 'targetEntityId', 'targetPoint']], ['light', draft.lights, ['energy', 'color', 'size']]]) {
|
|
2243
|
+
if (kind === 'light' && replaceLights) {
|
|
2244
|
+
for (const light of draft.original.lights) operations.push({ op: 'light.remove', lightId: light.id })
|
|
2245
|
+
for (const light of items) operations.push({ op: 'light.add', light: editorClone(light) })
|
|
2246
|
+
continue
|
|
2247
|
+
}
|
|
2204
2248
|
for (const item of items) {
|
|
2205
2249
|
const original = draft.original[`${kind}s`].find(old => old.id === item.id)
|
|
2206
2250
|
if (!original) { operations.push({ op: 'light.add', light: editorClone(item) }); continue }
|
|
2207
2251
|
const changes = {}
|
|
2252
|
+
if (kind === 'camera' && photographyAim(item) === 'free' && photographyAim(original) !== 'free') changes.clearTarget = true
|
|
2208
2253
|
for (const key of fields) if (item[key] !== undefined && !editorEqual(item[key], original[key])) changes[key] = editorClone(item[key])
|
|
2209
2254
|
const transform = {}
|
|
2210
2255
|
for (const key of ['location', 'rotationEuler']) if (!editorEqual(item.transform?.[key], original.transform?.[key])) transform[key] = editorClone(item.transform[key])
|
|
@@ -2212,10 +2257,40 @@ window.__ModuleLoader__.load({
|
|
|
2212
2257
|
if (Object.keys(changes).length) operations.push({ op: `${kind}.update`, [`${kind}Id`]: item.id, ...changes })
|
|
2213
2258
|
}
|
|
2214
2259
|
}
|
|
2215
|
-
|
|
2260
|
+
const note = draft.reuseSource ? t('photo.reuseNote', draft.reuseSource) : null
|
|
2261
|
+
return { projectId: draft.projectId, baseRevision: draft.baseRevision, actor: 'ui', saveCheckpoint: true, renderPreview: false, operations, ...(note ? { note } : {}) }
|
|
2216
2262
|
}
|
|
2217
2263
|
function photographyDirty(draft) {
|
|
2218
|
-
return Boolean(draft &&
|
|
2264
|
+
return Boolean(draft && (creationSignature(draft.cameras) !== creationSignature(draft.original.cameras)
|
|
2265
|
+
|| !photographySameLights(draft.lights, draft.original.lights)))
|
|
2266
|
+
}
|
|
2267
|
+
function reusePhotography(draft, source, cameraId, targetEntityId) {
|
|
2268
|
+
const camera = source?.nodes?.cameras?.find(item => item.id === cameraId)?.definition
|
|
2269
|
+
const target = draft?.cameras?.find(item => item.id === draft.cameraId)
|
|
2270
|
+
if (!camera || !target || !Array.isArray(source.nodes.lights)) throw new Error(t('photo.reuseUnavailable'))
|
|
2271
|
+
const next = editorClone(draft), selected = next.cameras.find(item => item.id === next.cameraId)
|
|
2272
|
+
selected.lens = editorNumberDefault(camera.lens, 50)
|
|
2273
|
+
selected.transform ||= {}
|
|
2274
|
+
selected.transform.location = editorClone(camera.transform?.location || [0, 0, 0])
|
|
2275
|
+
delete selected.targetEntityId; delete selected.targetPoint
|
|
2276
|
+
if (photographyAim(camera) === 'entity') {
|
|
2277
|
+
if (!next.entities.some(item => item.id === targetEntityId)) throw new Error(t('photo.reuseTargetRequired'))
|
|
2278
|
+
selected.targetEntityId = targetEntityId
|
|
2279
|
+
} else if (photographyAim(camera) === 'point') selected.targetPoint = editorClone(camera.targetPoint)
|
|
2280
|
+
if (photographyAim(camera) === 'free') selected.transform.rotationEuler = editorClone(camera.transform?.rotationEuler || [0, 0, 0])
|
|
2281
|
+
else {
|
|
2282
|
+
const original = next.original.cameras.find(item => item.id === selected.id)
|
|
2283
|
+
if (original?.transform?.rotationEuler) selected.transform.rotationEuler = editorClone(original.transform.rotationEuler)
|
|
2284
|
+
else delete selected.transform.rotationEuler
|
|
2285
|
+
}
|
|
2286
|
+
next.lights = source.nodes.lights.map(item => editorClone(item.definition))
|
|
2287
|
+
if (next.lights.some(item => !item)) throw new Error(t('photo.reuseUnavailable'))
|
|
2288
|
+
next.lightId = next.lights.some(item => item.id === next.lightId) ? next.lightId : next.lights[0]?.id || ''
|
|
2289
|
+
next.reusedLights = true
|
|
2290
|
+
next.reuseSource = { project: source.project.id, revision: source.revision, digest: source.digest, camera: cameraId, target: targetEntityId || '—' }
|
|
2291
|
+
const errors = photographyErrors(next)
|
|
2292
|
+
if (errors.length) throw new Error(t('photo.invalid', { fields: errors.join(', ') }))
|
|
2293
|
+
return next
|
|
2219
2294
|
}
|
|
2220
2295
|
function inspectionReceiptValid(receipt, request, digest) {
|
|
2221
2296
|
const artifacts = receipt?.artifacts, view = request.views[0]
|
|
@@ -2622,6 +2697,7 @@ window.__ModuleLoader__.load({
|
|
|
2622
2697
|
imageDownloads: {},
|
|
2623
2698
|
creationWork: null,
|
|
2624
2699
|
creationRequestProtocol: null,
|
|
2700
|
+
cameraTargetResetSupported: false,
|
|
2625
2701
|
creationDrafts: [],
|
|
2626
2702
|
creationDraftStorageStatus: null,
|
|
2627
2703
|
creationDraftMissingRecipe: null,
|
|
@@ -2643,6 +2719,7 @@ window.__ModuleLoader__.load({
|
|
|
2643
2719
|
photographyDrafts: {},
|
|
2644
2720
|
photographyWork: {},
|
|
2645
2721
|
photographyEdits: {},
|
|
2722
|
+
photographyReuse: {},
|
|
2646
2723
|
editorEntityId: null,
|
|
2647
2724
|
editorDrafts: {},
|
|
2648
2725
|
editorLastEdits: {},
|
|
@@ -2711,6 +2788,8 @@ window.__ModuleLoader__.load({
|
|
|
2711
2788
|
const draftLifecycle = settings.creationDraftLifecycle ?? globalThis.window
|
|
2712
2789
|
let fileChoiceSignature = null, fileChoicesStopped = false
|
|
2713
2790
|
const photographyControllers = new Map()
|
|
2791
|
+
const photographySourceControllers = new Map()
|
|
2792
|
+
let photographySourceSequence = 0
|
|
2714
2793
|
const photographyWork = (projectId, changes) => set({ photographyWork: { ...data.photographyWork, [projectId]: { ...data.photographyWork[projectId], ...changes } } })
|
|
2715
2794
|
const inspectionControllers = new Map()
|
|
2716
2795
|
const imageDownloadControllers = new Map()
|
|
@@ -2765,7 +2844,9 @@ window.__ModuleLoader__.load({
|
|
|
2765
2844
|
// Automatic edit images are source snapshots too. They cannot override
|
|
2766
2845
|
// fresh rows from another store, even when its project/revision IDs match.
|
|
2767
2846
|
if (Object.hasOwn(patch, 'projectsRoot') && patch.projectsRoot !== data.projectsRoot) {
|
|
2768
|
-
|
|
2847
|
+
for (const controller of photographySourceControllers.values()) controller.abort()
|
|
2848
|
+
photographySourceControllers.clear()
|
|
2849
|
+
patch = { ...patch, editorComparison: null, photographyReuse: {} }
|
|
2769
2850
|
}
|
|
2770
2851
|
// Retire the old source before selection/creation navigation can expose
|
|
2771
2852
|
// it with a new project. A load supplies the complete new source atomically.
|
|
@@ -2862,6 +2943,7 @@ window.__ModuleLoader__.load({
|
|
|
2862
2943
|
recipeCatalog: payload.recipeCatalog ?? { recipes: [], errors: [] },
|
|
2863
2944
|
projectsRoot: payload.projectsRoot ?? null,
|
|
2864
2945
|
creationRequestProtocol: payload.creationRequestProtocol ?? null,
|
|
2946
|
+
cameraTargetResetSupported: payload.cameraTargetResetSupported === true,
|
|
2865
2947
|
selected: payload.selected ?? null,
|
|
2866
2948
|
activeProjectId: active,
|
|
2867
2949
|
currentRevision: payload.selected ? payload.selected.currentRevision : null,
|
|
@@ -2909,7 +2991,7 @@ window.__ModuleLoader__.load({
|
|
|
2909
2991
|
if (patch.selected?.scene && active) {
|
|
2910
2992
|
const photography = data.photographyDrafts[active]
|
|
2911
2993
|
if ((!photography || !photographyDirty(photography)) && !data.photographyWork[active]?.saving) {
|
|
2912
|
-
patch.photographyDrafts = { ...data.photographyDrafts, [active]: createPhotographyDraft(patch.selected.scene, active, photography) }
|
|
2994
|
+
patch.photographyDrafts = { ...data.photographyDrafts, [active]: createPhotographyDraft(patch.selected.scene, active, photography, patch.projectsRoot) }
|
|
2913
2995
|
}
|
|
2914
2996
|
const draft = data.briefDrafts[active], work = data.briefWork[active]
|
|
2915
2997
|
if (!draft || !briefDirty(draft) && !work?.uploading && !work?.saving) {
|
|
@@ -3036,6 +3118,46 @@ window.__ModuleLoader__.load({
|
|
|
3036
3118
|
|
|
3037
3119
|
const acceptsFileChoice = (source, field) => !fileChoicesStopped && typeof source === 'string'
|
|
3038
3120
|
&& FILE_FIELDS.includes(field) && source === fileChoiceKey(data, field) && fileChoiceAllowed(data, field)
|
|
3121
|
+
const loadPhotographySource = async requestedRevision => {
|
|
3122
|
+
const draft = photographyDraftFor(data), projectId = data.activeProjectId, selection = data.photographyReuse[projectId]
|
|
3123
|
+
if (!data.cameraTargetResetSupported || !draft || !selection?.sourceProjectId || photographyBusy(data.photographyWork[projectId])
|
|
3124
|
+
|| draft.baseRevision !== data.selected?.scene?.revision || draft.projectsRoot !== data.projectsRoot) return
|
|
3125
|
+
photographySourceControllers.get(projectId)?.abort()
|
|
3126
|
+
const controller = new AbortController(), sequence = ++photographySourceSequence
|
|
3127
|
+
photographySourceControllers.set(projectId, controller)
|
|
3128
|
+
const root = data.projectsRoot, baseRevision = draft.baseRevision, sourceProjectId = selection.sourceProjectId
|
|
3129
|
+
const alive = () => running && !controller.signal.aborted && photographySourceControllers.get(projectId) === controller && data.projectsRoot === root
|
|
3130
|
+
const update = changes => set({ photographyReuse: { ...data.photographyReuse, [projectId]: { ...data.photographyReuse[projectId], ...changes } } })
|
|
3131
|
+
update({ source: null, loading: true, error: null, applied: false, unchanged: false, replaceDraft: false, projectsRoot: root, baseRevision, sequence })
|
|
3132
|
+
const deadline = setTimeout(() => controller.abort(), 30000)
|
|
3133
|
+
const read = (path, route) => readRoute((url, options) => fetchImpl(url, { ...options, signal: controller.signal }), path, route)
|
|
3134
|
+
try {
|
|
3135
|
+
const listed = await read(projectRoute(sourceProjectId, '/revisions'), 'project.revisions')
|
|
3136
|
+
if (!alive()) return
|
|
3137
|
+
const list = listed.payload
|
|
3138
|
+
if (listed.status !== 'ok' || list?.projectId !== sourceProjectId || !Array.isArray(list.revisions)
|
|
3139
|
+
|| list.revisions.some(item => !/^r[0-9]{4,}$/.test(item.revision))) throw new Error(listed.error?.message || t('photo.reuseUnavailable'))
|
|
3140
|
+
const revision = requestedRevision || list.currentRevision, saved = list.revisions.find(item => item.revision === revision)
|
|
3141
|
+
if (!saved) throw new Error(t('photo.reuseUnavailable'))
|
|
3142
|
+
update({ revisions: editorClone(list.revisions), revision })
|
|
3143
|
+
const result = await read(`${projectRoute(sourceProjectId, '/scene')}?revision=${encodeURIComponent(revision)}`, 'project.scene')
|
|
3144
|
+
if (!alive()) return
|
|
3145
|
+
const payload = result.payload, scene = payload?.scene
|
|
3146
|
+
if (result.status !== 'ok' || payload?.projectId !== sourceProjectId || payload.revision !== revision
|
|
3147
|
+
|| scene?.revision !== revision || scene?.project?.id !== sourceProjectId || scene.digest !== payload.digest
|
|
3148
|
+
|| !/^[a-f0-9]{64}$/.test(payload.digest || '') || (saved.digest && saved.digest !== payload.digest)
|
|
3149
|
+
|| !Array.isArray(scene.nodes?.cameras) || !Array.isArray(scene.nodes?.lights)) throw new Error(result.error?.message || t('photo.reuseUnavailable'))
|
|
3150
|
+
update({ source: editorClone(scene), cameraId: scene.nodes.cameras.find(item => item.id === scene.project.activeCamera)?.id || scene.nodes.cameras[0]?.id || '', targetEntityId: '', error: null })
|
|
3151
|
+
} catch (error) {
|
|
3152
|
+
if (alive()) update({ source: null, error: error.message || String(error) })
|
|
3153
|
+
} finally {
|
|
3154
|
+
clearTimeout(deadline)
|
|
3155
|
+
if (photographySourceControllers.get(projectId) === controller) {
|
|
3156
|
+
photographySourceControllers.delete(projectId)
|
|
3157
|
+
if (running && data.projectsRoot === root) update({ loading: false, ...(controller.signal.aborted ? { source: null, error: t('photo.reuseUnavailable') } : {}) })
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3039
3161
|
const actions = {
|
|
3040
3162
|
acceptsFileChoice,
|
|
3041
3163
|
setAppearance: value => {
|
|
@@ -3439,7 +3561,43 @@ window.__ModuleLoader__.load({
|
|
|
3439
3561
|
resetPhotography: () => {
|
|
3440
3562
|
const projectId = data.activeProjectId, scene = data.selected?.scene
|
|
3441
3563
|
if (!scene || photographyBusy(data.photographyWork[projectId])) return
|
|
3442
|
-
set({ photographyDrafts: { ...data.photographyDrafts, [projectId]: createPhotographyDraft(scene, projectId, photographyDraftFor(data)) }
|
|
3564
|
+
set({ photographyDrafts: { ...data.photographyDrafts, [projectId]: createPhotographyDraft(scene, projectId, photographyDraftFor(data), data.projectsRoot) },
|
|
3565
|
+
photographyReuse: { ...data.photographyReuse, ...(data.photographyReuse[projectId] ? { [projectId]: { ...data.photographyReuse[projectId], applied: false, unchanged: false, replaceDraft: false } } : {}) } })
|
|
3566
|
+
},
|
|
3567
|
+
setPhotographySource: (field, value) => {
|
|
3568
|
+
const projectId = data.activeProjectId, selection = data.photographyReuse[projectId] || {}
|
|
3569
|
+
if (!data.cameraTargetResetSupported || !photographyDraftFor(data) || photographyBusy(data.photographyWork[projectId])) return
|
|
3570
|
+
if (field === 'project') {
|
|
3571
|
+
if (value && !data.projects.some(item => item.projectId === value && !item.creationPending)) return
|
|
3572
|
+
photographySourceControllers.get(projectId)?.abort(); photographySourceControllers.delete(projectId)
|
|
3573
|
+
set({ photographyReuse: { ...data.photographyReuse, [projectId]: { sourceProjectId: value, source: null, revisions: [], revision: '', cameraId: '', targetEntityId: '', replaceDraft: false } } })
|
|
3574
|
+
if (value) return loadPhotographySource()
|
|
3575
|
+
} else if (field === 'revision') {
|
|
3576
|
+
if (!selection.revisions?.some(item => item.revision === value)) return
|
|
3577
|
+
return loadPhotographySource(value)
|
|
3578
|
+
} else if (field === 'camera' && selection.source?.nodes.cameras.some(item => item.id === value)) {
|
|
3579
|
+
set({ photographyReuse: { ...data.photographyReuse, [projectId]: { ...selection, cameraId: value, targetEntityId: '', applied: false, unchanged: false } } })
|
|
3580
|
+
} else if (field === 'target' && (!value || photographyDraftFor(data).entities.some(item => item.id === value))) {
|
|
3581
|
+
set({ photographyReuse: { ...data.photographyReuse, [projectId]: { ...selection, targetEntityId: value, applied: false, unchanged: false } } })
|
|
3582
|
+
} else if (field === 'replace' && typeof value === 'boolean') {
|
|
3583
|
+
set({ photographyReuse: { ...data.photographyReuse, [projectId]: { ...selection, replaceDraft: value } } })
|
|
3584
|
+
}
|
|
3585
|
+
},
|
|
3586
|
+
reloadPhotographySource: () => loadPhotographySource(data.photographyReuse[data.activeProjectId]?.revision),
|
|
3587
|
+
reusePhotography: () => {
|
|
3588
|
+
const projectId = data.activeProjectId, selection = data.photographyReuse[projectId], draft = photographyDraftFor(data)
|
|
3589
|
+
if (!data.cameraTargetResetSupported || !draft || !selection?.source || selection.loading || photographyBusy(data.photographyWork[projectId])
|
|
3590
|
+
|| selection.projectsRoot !== data.projectsRoot || draft.projectsRoot !== data.projectsRoot
|
|
3591
|
+
|| selection.baseRevision !== draft.baseRevision || draft.baseRevision !== data.selected?.scene?.revision
|
|
3592
|
+
|| (photographyDirty(draft) && !selection.replaceDraft)) return
|
|
3593
|
+
try {
|
|
3594
|
+
const next = reusePhotography(draft, selection.source, selection.cameraId, selection.targetEntityId)
|
|
3595
|
+
set({ photographyDrafts: { ...data.photographyDrafts, [projectId]: next }, photographyReuse: {
|
|
3596
|
+
...data.photographyReuse, [projectId]: { ...selection, replaceDraft: false, error: null, applied: photographyDirty(next), unchanged: !photographyDirty(next) },
|
|
3597
|
+
} })
|
|
3598
|
+
} catch (error) {
|
|
3599
|
+
set({ photographyReuse: { ...data.photographyReuse, [projectId]: { ...selection, applied: false, error: error.message || String(error) } } })
|
|
3600
|
+
}
|
|
3443
3601
|
},
|
|
3444
3602
|
updatePhotography: (kind, path, value) => {
|
|
3445
3603
|
const current = photographyDraftFor(data)
|
|
@@ -3456,7 +3614,7 @@ window.__ModuleLoader__.load({
|
|
|
3456
3614
|
} else if (kind === 'camera' && path.length === 1 && path[0] === 'aim') {
|
|
3457
3615
|
if (!['free', 'entity', 'point'].includes(value) || photographyAimLocked(draft, item)) return
|
|
3458
3616
|
const original = draft.original.cameras.find(camera => camera.id === item.id)
|
|
3459
|
-
if (value === 'free' && photographyAim(original) !== 'free') return
|
|
3617
|
+
if (value === 'free' && photographyAim(original) !== 'free' && !data.cameraTargetResetSupported) return
|
|
3460
3618
|
delete item.targetEntityId; delete item.targetPoint
|
|
3461
3619
|
if (value === 'entity') item.targetEntityId = draft.entities.find(entity => entity.id === data.editorEntityId)?.id || data.selected?.scene?.project?.reviewSubjectId || draft.entities[0]?.id || ''
|
|
3462
3620
|
if (value === 'point') item.targetPoint = [0, 0, 0]
|
|
@@ -3472,7 +3630,8 @@ window.__ModuleLoader__.load({
|
|
|
3472
3630
|
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
3631
|
else return
|
|
3474
3632
|
}
|
|
3475
|
-
set({ photographyDrafts: { ...data.photographyDrafts, [current.projectId]: draft }
|
|
3633
|
+
set({ photographyDrafts: { ...data.photographyDrafts, [current.projectId]: draft },
|
|
3634
|
+
photographyReuse: { ...data.photographyReuse, ...(data.photographyReuse[current.projectId] ? { [current.projectId]: { ...data.photographyReuse[current.projectId], unchanged: false } } : {}) } })
|
|
3476
3635
|
},
|
|
3477
3636
|
addPhotographyLight: () => {
|
|
3478
3637
|
const current = photographyDraftFor(data)
|
|
@@ -3496,7 +3655,9 @@ window.__ModuleLoader__.load({
|
|
|
3496
3655
|
setIn('busy', 'editor', false)
|
|
3497
3656
|
if (outcome.ok) {
|
|
3498
3657
|
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,
|
|
3658
|
+
set({ photographyDrafts: drafts,
|
|
3659
|
+
photographyReuse: { ...data.photographyReuse, ...(data.photographyReuse[projectId] ? { [projectId]: { ...data.photographyReuse[projectId], baseRevision: saved.revision, applied: false, unchanged: false, replaceDraft: false } } : {}) },
|
|
3660
|
+
photographyEdits: { ...data.photographyEdits, [projectId]: {
|
|
3500
3661
|
before: draft.baseRevision, after: saved.revision, digest: saved.digest, cameraId: draft.cameraId, frame: draft.frame, samples: draft.samples,
|
|
3501
3662
|
changes: [...new Set(patch.operations.map(operation => operation.op.split('.')[0]))] } } })
|
|
3502
3663
|
photographyWork(projectId, { saving: false, error: null, artifact: null })
|
|
@@ -3896,6 +4057,8 @@ window.__ModuleLoader__.load({
|
|
|
3896
4057
|
if (timer !== null) { clearInterval(timer); timer = null }
|
|
3897
4058
|
for (const controller of photographyControllers.values()) controller.abort()
|
|
3898
4059
|
photographyControllers.clear()
|
|
4060
|
+
for (const controller of photographySourceControllers.values()) controller.abort()
|
|
4061
|
+
photographySourceControllers.clear()
|
|
3899
4062
|
for (const controller of inspectionControllers.values()) controller.abort()
|
|
3900
4063
|
inspectionControllers.clear()
|
|
3901
4064
|
for (const controller of imageDownloadControllers.values()) controller.abort('stopped')
|
|
@@ -4530,18 +4693,38 @@ window.__ModuleLoader__.load({
|
|
|
4530
4693
|
{ factor, locked: locked || editorTrackLocked(draft, kind, kind === 'camera' ? camera.id : light.id, `${field}.${axis}`) })))
|
|
4531
4694
|
const aim = photographyAim(camera), originalCamera = draft?.original.cameras.find(item => item.id === camera?.id)
|
|
4532
4695
|
const aimLocked = camera && photographyAimLocked(draft, camera)
|
|
4696
|
+
const reuse = state.photographyReuse?.[projectId] || {}, sourceCamera = reuse.source?.nodes.cameras.find(item => item.id === reuse.cameraId)?.definition
|
|
4697
|
+
const reuseStale = reuse.source && (reuse.projectsRoot !== state.projectsRoot || reuse.baseRevision !== draft?.baseRevision || draft?.projectsRoot !== state.projectsRoot)
|
|
4533
4698
|
return el('section', { className: 'db-card', 'data-photography-editor': projectId },
|
|
4534
4699
|
el('h4', null, t('photo.title')), el('p', { className: 'db-muted' }, t('photo.help')),
|
|
4535
4700
|
draft ? el('div', null,
|
|
4536
4701
|
el('p', { className: 'db-muted', 'data-photography-base': draft.baseRevision }, t('photo.base', { revision: draft.baseRevision })),
|
|
4537
4702
|
conflict ? el('p', { className: 'db-error', 'data-photography-conflict': true }, t('photo.conflict', { before: draft.baseRevision, current: state.selected?.scene?.revision })) : null,
|
|
4703
|
+
state.cameraTargetResetSupported ? el('div', { 'data-photography-reuse': true },
|
|
4704
|
+
el('h4', null, t('photo.reuseTitle')), el('p', { className: 'db-muted' }, t('photo.reuseHelp')),
|
|
4705
|
+
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)),
|
|
4706
|
+
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),
|
|
4707
|
+
reuse.loading ? el('p', { role: 'status' }, t('photo.reuseReading')) : null,
|
|
4708
|
+
reuse.source ? el('div', { 'data-photography-source': `${reuse.sourceProjectId}:${reuse.revision}` },
|
|
4709
|
+
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 })),
|
|
4710
|
+
choose(t('photo.reuseCamera'), 'source-camera', reuse.cameraId, reuse.source.nodes.cameras.map(item => [item.id, item.id]), value => actions.setPhotographySource('camera', value)),
|
|
4711
|
+
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,
|
|
4712
|
+
photographyDirty(draft) ? el('label', { className: 'db-row' }, el('input', { type: 'checkbox', 'data-field': 'photo-source-replace', checked: Boolean(reuse.replaceDraft), disabled: disabled || reuse.loading,
|
|
4713
|
+
onChange: event => actions.setPhotographySource('replace', Boolean(event.target.checked)) }), t('photo.reuseReplace')) : null,
|
|
4714
|
+
reuseStale ? el('p', { className: 'db-error' }, t('photo.reuseStale')) : null,
|
|
4715
|
+
reuse.applied ? el('p', { role: 'status', 'data-photography-reused': true }, t('photo.reuseApplied')) : null,
|
|
4716
|
+
reuse.unchanged ? el('p', { role: 'status', 'data-photography-reuse-unchanged': true }, t('photo.reuseSame')) : null) : null,
|
|
4717
|
+
reuse.error ? el('p', { className: 'db-error', role: 'status', 'data-photography-source-error': true }, reuse.error) : null,
|
|
4718
|
+
el('div', { className: 'db-inline' },
|
|
4719
|
+
Button({ action: 'photo-source-reload', disabled: disabled || !reuse.sourceProjectId || reuse.loading, onClick: actions.reloadPhotographySource, children: t('photo.reuseReload') }),
|
|
4720
|
+
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
4721
|
el('div', { className: 'db-grid' },
|
|
4539
4722
|
el('div', null,
|
|
4540
4723
|
choose(t('photo.camera'), 'camera', draft.cameraId, draft.cameras.map(item => [item.id, item.id]), id => actions.selectPhotography('camera', id)),
|
|
4541
4724
|
camera ? el('div', null,
|
|
4542
4725
|
numeric(t('photo.lens'), 'camera-lens', editorNumberDefault(camera.lens, 50), update('camera', ['lens']), { min: .001 }),
|
|
4543
4726
|
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),
|
|
4727
|
+
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
4728
|
aim === 'entity' ? choose(t('photo.target'), 'camera-target', camera.targetEntityId, draft.entities.map(item => [item.id, item.id]), update('camera', ['targetEntityId']), aimLocked) : null,
|
|
4546
4729
|
aim === 'point' ? vector('camera', 'targetPoint', camera.targetPoint, t('photo.targetPoint'), 1, aimLocked) : null,
|
|
4547
4730
|
vector('camera', 'rotationEuler', camera.transform?.rotationEuler, t('photo.rotation'), 180 / Math.PI, aim !== 'free'),
|
|
@@ -5955,7 +6138,7 @@ window.__ModuleLoader__.load({
|
|
|
5955
6138
|
inspection: { form: inspectionForm, valid: inspectionFormValid },
|
|
5956
6139
|
assetLibrary: { selection: assetBundleSelection, createDraft: createAssetDraft, buildPatch: buildAssetPatch, error: assetDraftError, key: assetKey },
|
|
5957
6140
|
referenceBrief: { createDraft: createBriefDraft, buildPatch: buildBriefPatch, dirty: briefDirty, valid: briefValid },
|
|
5958
|
-
photographyEditor: { createDraft: createPhotographyDraft, buildPatch: buildPhotographyPatch, errors: photographyErrors, dirty: photographyDirty, draftFor: photographyDraftFor },
|
|
6141
|
+
photographyEditor: { createDraft: createPhotographyDraft, buildPatch: buildPhotographyPatch, errors: photographyErrors, dirty: photographyDirty, draftFor: photographyDraftFor, reuse: reusePhotography },
|
|
5959
6142
|
sceneEditor: { createDraft: createEditorDraft, buildPatch: buildEditorPatch, errors: editorErrors, dirty: editorDirty, previewPair: editorPreviewPair, draftFor: editorDraftFor },
|
|
5960
6143
|
renderView,
|
|
5961
6144
|
mountStandalone,
|
package/lib/index.js
CHANGED
|
@@ -633,6 +633,7 @@ export function createHandlers(ctx) {
|
|
|
633
633
|
projects: listed.projects.map(record => buildProjectView(record)),
|
|
634
634
|
projectsRoot: listed.projectsRoot,
|
|
635
635
|
creationRequestProtocol: studio().creationRequestProtocol ?? null,
|
|
636
|
+
cameraTargetResetSupported: studio().cameraTargetResetSupported === true,
|
|
636
637
|
selected: projectId === null || creationPending
|
|
637
638
|
? null
|
|
638
639
|
: await buildProjectState(studio(), projectId, query.revision ?? undefined, approvalThreshold()),
|
|
@@ -669,6 +670,7 @@ export function createHandlers(ctx) {
|
|
|
669
670
|
'project.scene': async ({ params, query }) => {
|
|
670
671
|
const detail = await studio().getScene(params.projectId, { revision: query.revision, full: true })
|
|
671
672
|
return {
|
|
673
|
+
projectId: params.projectId,
|
|
672
674
|
revision: detail.revision,
|
|
673
675
|
digest: detail.digest,
|
|
674
676
|
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.4.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.4.0"
|
|
23
23
|
},
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"repository": {
|