@deepblend/dsh-blender-ui 0.2.20 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +3044 -114
- package/lib/index.js +126 -5
- package/package.json +2 -2
package/lib/index.js
CHANGED
|
@@ -253,7 +253,17 @@ export default class BlenderUiHost extends Service {
|
|
|
253
253
|
return
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
+
let releaseSignal = () => {}
|
|
256
257
|
try {
|
|
258
|
+
if (matched.route.id === 'recipes.preview') {
|
|
259
|
+
const preview = await this.ctx.blenderStudio.readRecipePreview({ id: matched.params.recipeId, version: matched.params.version, digest: url.searchParams.get('digest') })
|
|
260
|
+
response.setHeader('content-type', preview.contentType)
|
|
261
|
+
response.setHeader('x-content-type-options', 'nosniff')
|
|
262
|
+
response.setHeader('cache-control', 'no-store')
|
|
263
|
+
response.setHeader('content-length', String(preview.size))
|
|
264
|
+
response.end(request.method === 'HEAD' ? undefined : preview.bytes)
|
|
265
|
+
return
|
|
266
|
+
}
|
|
257
267
|
if (matched.route.id === 'artifacts.open') {
|
|
258
268
|
await this._sendArtifact(request, response, matched.params)
|
|
259
269
|
return
|
|
@@ -265,11 +275,26 @@ export default class BlenderUiHost extends Service {
|
|
|
265
275
|
this._sendWorkbenchPage(request, response)
|
|
266
276
|
return
|
|
267
277
|
}
|
|
268
|
-
const
|
|
278
|
+
const cancellable = ['project.assets.upload', 'project.assets.preview', 'project.preview',
|
|
279
|
+
'project.assetUploads.create', 'project.assetUploads.file', 'project.assetUploads.complete'].includes(matched.route.id)
|
|
280
|
+
let signal
|
|
281
|
+
if (cancellable) {
|
|
282
|
+
const controller = new AbortController()
|
|
283
|
+
const abort = () => controller.abort()
|
|
284
|
+
const closed = () => { if (!response.writableEnded) abort() }
|
|
285
|
+
request.once?.('aborted', abort)
|
|
286
|
+
response.once?.('close', closed)
|
|
287
|
+
if (request.aborted) abort()
|
|
288
|
+
releaseSignal = () => { request.off?.('aborted', abort); response.off?.('close', closed) }
|
|
289
|
+
signal = controller.signal
|
|
290
|
+
}
|
|
291
|
+
const body = ['project.referenceImage.upload', 'project.assets.upload', 'project.assetUploads.file'].includes(matched.route.id) ? {} : await readRequestBody(request)
|
|
269
292
|
const result = await this._handlers[matched.route.id]({
|
|
270
293
|
params: matched.params,
|
|
271
294
|
query: Object.fromEntries(url.searchParams.entries()),
|
|
272
295
|
body,
|
|
296
|
+
request,
|
|
297
|
+
signal,
|
|
273
298
|
})
|
|
274
299
|
this._sendJson(response, 200, { ok: true, route: matched.route.id, hostApiVersion: HOST_API_VERSION, ...result })
|
|
275
300
|
} catch (cause) {
|
|
@@ -285,6 +310,8 @@ export default class BlenderUiHost extends Service {
|
|
|
285
310
|
hostApiVersion: HOST_API_VERSION,
|
|
286
311
|
error: error.toJSON(),
|
|
287
312
|
})
|
|
313
|
+
} finally {
|
|
314
|
+
releaseSignal()
|
|
288
315
|
}
|
|
289
316
|
}
|
|
290
317
|
|
|
@@ -390,6 +417,14 @@ export default class BlenderUiHost extends Service {
|
|
|
390
417
|
* @returns {number}
|
|
391
418
|
*/
|
|
392
419
|
export function statusForError(error) {
|
|
420
|
+
if (error?.code === 'UI_HOST_API_STALE') return 503
|
|
421
|
+
const referenceStatuses = { ASSET_REQUEST_INVALID: 400, ASSET_CONTENT_MISMATCH: 415, ASSET_TOO_LARGE: 413,
|
|
422
|
+
ASSET_FORMAT_UNAVAILABLE: 415, SCENE_VALIDATION_FAILED: 422, SCENE_SPEC_INVALID: 422,
|
|
423
|
+
PATH_SEGMENT_INVALID: 400, ASSET_HASH_MISMATCH: 409, ASSET_SOURCE_NOT_FOUND: 404 }
|
|
424
|
+
if (referenceStatuses[error.code]) return referenceStatuses[error.code]
|
|
425
|
+
if (error.code === 'RECIPE_NOT_FOUND') return 404
|
|
426
|
+
if (error.code === 'RECIPE_CHANGED' || error.code === 'RECIPE_ID_CONFLICT') return 409
|
|
427
|
+
if (error.code?.startsWith('RECIPE_')) return 400
|
|
393
428
|
switch (error.code) {
|
|
394
429
|
case BlenderErrorCode.PROJECT_NOT_FOUND:
|
|
395
430
|
case BlenderErrorCode.REVISION_NOT_FOUND:
|
|
@@ -401,6 +436,7 @@ export function statusForError(error) {
|
|
|
401
436
|
case BlenderErrorCode.SCENE_PATCH_INVALID:
|
|
402
437
|
return 400
|
|
403
438
|
case BlenderErrorCode.RENDER_JOB_CONFLICT:
|
|
439
|
+
case BlenderErrorCode.REVISION_CONFLICT:
|
|
404
440
|
return 409
|
|
405
441
|
default:
|
|
406
442
|
return 500
|
|
@@ -458,6 +494,13 @@ async function readRequestBody(request) {
|
|
|
458
494
|
*/
|
|
459
495
|
export function createHandlers(ctx) {
|
|
460
496
|
const studio = () => ctx.blenderStudio
|
|
497
|
+
const assetStudio = method => {
|
|
498
|
+
const service = studio(), version = service?.hostApiVersion?.()
|
|
499
|
+
if (typeof service?.[method] !== 'function' || typeof version === 'number' && version < 5) {
|
|
500
|
+
throw new BlenderError('UI_HOST_API_STALE', 'The running Blender Host does not support the asset library. Restart with Host API 5 or newer.')
|
|
501
|
+
}
|
|
502
|
+
return service
|
|
503
|
+
}
|
|
461
504
|
const approvalThreshold = () => studio().config?.requireApprovalAboveFrames ?? Number.POSITIVE_INFINITY
|
|
462
505
|
|
|
463
506
|
return {
|
|
@@ -583,6 +626,7 @@ export function createHandlers(ctx) {
|
|
|
583
626
|
const projectId = query.projectId ?? listed.projects[0]?.projectId ?? null
|
|
584
627
|
return {
|
|
585
628
|
panelId: UI_PANEL_ID,
|
|
629
|
+
recipeCatalog: studio().listRecipes?.() ?? { recipes: [], errors: [] },
|
|
586
630
|
projects: listed.projects.map(record => buildProjectView(record)),
|
|
587
631
|
projectsRoot: listed.projectsRoot,
|
|
588
632
|
selected: projectId === null
|
|
@@ -591,6 +635,9 @@ export function createHandlers(ctx) {
|
|
|
591
635
|
}
|
|
592
636
|
},
|
|
593
637
|
|
|
638
|
+
'recipes.list': async () => studio().listRecipes(),
|
|
639
|
+
'recipes.preview': async ({ params, query }) => studio().readRecipePreview({ id: params.recipeId, version: params.version, digest: query.digest }),
|
|
640
|
+
|
|
594
641
|
'projects.list': async () => {
|
|
595
642
|
const listed = await studio().listProjects()
|
|
596
643
|
return { projects: listed.projects.map(record => buildProjectView(record)), projectsRoot: listed.projectsRoot }
|
|
@@ -601,6 +648,7 @@ export function createHandlers(ctx) {
|
|
|
601
648
|
title: body.title,
|
|
602
649
|
goal: body.goal,
|
|
603
650
|
sceneSpec: body.sceneSpec,
|
|
651
|
+
recipe: body.recipe,
|
|
604
652
|
projectId: body.projectId,
|
|
605
653
|
saveCheckpoint: true,
|
|
606
654
|
renderPreview: body.renderPreview === true,
|
|
@@ -619,6 +667,7 @@ export function createHandlers(ctx) {
|
|
|
619
667
|
revision: detail.revision,
|
|
620
668
|
digest: detail.digest,
|
|
621
669
|
compiled: detail.compiledSpec ?? null,
|
|
670
|
+
assetParts: detail.assetParts ?? [],
|
|
622
671
|
}),
|
|
623
672
|
}
|
|
624
673
|
},
|
|
@@ -661,7 +710,13 @@ export function createHandlers(ctx) {
|
|
|
661
710
|
return { previews: sets, artifactBase: `${UI_ROUTE_PREFIX}/artifacts/${encodeURIComponent(params.projectId)}/` }
|
|
662
711
|
},
|
|
663
712
|
|
|
664
|
-
'project.preview': async ({ params, body }) => {
|
|
713
|
+
'project.preview': async ({ params, body, signal }) => {
|
|
714
|
+
if (body.mode !== undefined) {
|
|
715
|
+
const version = studio()?.hostApiVersion?.()
|
|
716
|
+
if (!Number.isFinite(version) || version < 6) throw new BlenderError('UI_HOST_API_STALE', 'Fixed-view inspections require Blender Host API 6 or newer. Restart the Host.')
|
|
717
|
+
if (!['beauty', 'clay'].includes(body.mode)) throw new BlenderError(BlenderErrorCode.SCENE_PATCH_INVALID, 'Unknown inspection mode.')
|
|
718
|
+
requiredRevision(body.revision)
|
|
719
|
+
}
|
|
665
720
|
// `renderViews` answers with the rendered PNG bytes in `pngs`, which is not
|
|
666
721
|
// lossless JSON and is not what the browser needs: the panel displays the
|
|
667
722
|
// images through the artifact route, so the response carries the paths,
|
|
@@ -669,6 +724,9 @@ export function createHandlers(ctx) {
|
|
|
669
724
|
const result = await studio().renderViews({
|
|
670
725
|
projectId: params.projectId,
|
|
671
726
|
revision: body.revision,
|
|
727
|
+
...(body.mode === undefined ? {} : { mode: body.mode }),
|
|
728
|
+
...(body.views === undefined ? {} : { views: Array.isArray(body.views) ? body.views.map(view => ({ id: view?.id, cameraId: view?.cameraId, frame: view?.frame })) : body.views }),
|
|
729
|
+
signal,
|
|
672
730
|
samples: numberOrUndefined(body.samples),
|
|
673
731
|
maxViews: numberOrUndefined(body.maxViews),
|
|
674
732
|
reason: body.reason ?? 'preview rendered from the workbench UI',
|
|
@@ -676,6 +734,9 @@ export function createHandlers(ctx) {
|
|
|
676
734
|
return {
|
|
677
735
|
preview: {
|
|
678
736
|
projectId: params.projectId,
|
|
737
|
+
...(result.mode ? { mode: result.mode } : {}),
|
|
738
|
+
...(result.sourceRevision ? { sourceRevision: result.sourceRevision } : {}),
|
|
739
|
+
...(result.sourceDigest ? { sourceDigest: result.sourceDigest } : {}),
|
|
679
740
|
revision: result.revision ?? body.revision ?? null,
|
|
680
741
|
digest: result.digest ?? null,
|
|
681
742
|
profile: result.profile ?? null,
|
|
@@ -698,6 +759,56 @@ export function createHandlers(ctx) {
|
|
|
698
759
|
}
|
|
699
760
|
},
|
|
700
761
|
|
|
762
|
+
'project.referenceImage.upload': async ({ params, query, request }) => studio().uploadReferenceImage({
|
|
763
|
+
projectId: params.projectId, name: query.name, mediaType: request?.headers?.['content-type'], stream: request,
|
|
764
|
+
}),
|
|
765
|
+
|
|
766
|
+
'project.assets.list': async ({ params, query }) => assetStudio('listAssets').listAssets({
|
|
767
|
+
projectId: params.projectId, ...(query.revision ? { revision: requiredRevision(query.revision) } : {}),
|
|
768
|
+
}),
|
|
769
|
+
'project.assets.upload': async ({ params, query, request, signal }) => assetStudio('uploadAsset').uploadAsset({
|
|
770
|
+
projectId: params.projectId, name: query.name, mediaType: request?.headers?.['content-type'],
|
|
771
|
+
...(query.license === undefined ? {} : { license: query.license }), stream: request, signal,
|
|
772
|
+
}),
|
|
773
|
+
'project.assetUploads.create': async ({ params, body, signal }) => assetStudio('createAssetUpload').createAssetUpload({
|
|
774
|
+
projectId: params.projectId, entrypoint: body.entrypoint,
|
|
775
|
+
files: Array.isArray(body.files) ? body.files.map(file => ({ path: file?.path, bytes: file?.bytes })) : body.files,
|
|
776
|
+
...(body.license === undefined ? {} : { license: body.license }), signal,
|
|
777
|
+
}),
|
|
778
|
+
'project.assetUploads.get': async ({ params }) => assetStudio('getAssetUpload').getAssetUpload({
|
|
779
|
+
projectId: params.projectId, uploadId: params.uploadId,
|
|
780
|
+
}),
|
|
781
|
+
'project.assetUploads.file': async ({ params, request, signal }) => assetStudio('uploadAssetFile').uploadAssetFile({
|
|
782
|
+
projectId: params.projectId, uploadId: params.uploadId, fileId: params.fileId, stream: request, signal,
|
|
783
|
+
}),
|
|
784
|
+
'project.assetUploads.complete': async ({ params, signal }) => assetStudio('completeAssetUpload').completeAssetUpload({
|
|
785
|
+
projectId: params.projectId, uploadId: params.uploadId, signal,
|
|
786
|
+
}),
|
|
787
|
+
'project.assetUploads.cancel': async ({ params }) => assetStudio('cancelAssetUpload').cancelAssetUpload({
|
|
788
|
+
projectId: params.projectId, uploadId: params.uploadId,
|
|
789
|
+
}),
|
|
790
|
+
'project.assets.preview': async ({ params, body, signal }) => assetStudio('previewAsset').previewAsset({
|
|
791
|
+
projectId: params.projectId, assetId: params.assetId, sha256: body.sha256, assetPath: body.assetPath, signal,
|
|
792
|
+
}),
|
|
793
|
+
|
|
794
|
+
'project.review': async ({ params, body }) => {
|
|
795
|
+
const revision = requiredRevision(body.revision)
|
|
796
|
+
const result = await studio().visualReview({ projectId: params.projectId, revision, consultReviewer: true,
|
|
797
|
+
width: 640, height: 480, samples: 16 })
|
|
798
|
+
return { revision: result.revision ?? revision,
|
|
799
|
+
review: buildQaView({ projectId: params.projectId, revision, review: result }).visual }
|
|
800
|
+
},
|
|
801
|
+
|
|
802
|
+
'project.autofix': async ({ params, body }) => {
|
|
803
|
+
const revision = requiredRevision(body.revision)
|
|
804
|
+
const maxIterations = body.maxIterations ?? 1
|
|
805
|
+
if (!Number.isInteger(maxIterations) || maxIterations < 1 || maxIterations > 3) {
|
|
806
|
+
throw new BlenderError(BlenderErrorCode.SCENE_PATCH_INVALID, 'maxIterations must be an integer from 1 to 3.')
|
|
807
|
+
}
|
|
808
|
+
return { run: await studio().visualLoop({ projectId: params.projectId, revision, autoFix: true,
|
|
809
|
+
maxIterations, width: 640, height: 480, samples: 16 }) }
|
|
810
|
+
},
|
|
811
|
+
|
|
701
812
|
'project.patch': async ({ params, body }) => {
|
|
702
813
|
// The patch is passed through verbatim: the Host validates it against the
|
|
703
814
|
// ScenePatch schema and the base revision, and it is the only writer.
|
|
@@ -710,6 +821,7 @@ export function createHandlers(ctx) {
|
|
|
710
821
|
const result = await studio().restoreRevision({
|
|
711
822
|
projectId: params.projectId,
|
|
712
823
|
revision: body.revision,
|
|
824
|
+
expectedCurrentRevision: body.expectedCurrentRevision,
|
|
713
825
|
reason: body.reason ?? 'restored from the workbench UI',
|
|
714
826
|
actor: body.actor ?? 'ui',
|
|
715
827
|
})
|
|
@@ -748,7 +860,8 @@ export function createHandlers(ctx) {
|
|
|
748
860
|
frameStart: numberOrUndefined(body.frameStart),
|
|
749
861
|
frameEnd: numberOrUndefined(body.frameEnd),
|
|
750
862
|
samples: numberOrUndefined(body.samples),
|
|
751
|
-
|
|
863
|
+
// Keep the HTTP field stable while using the Host's actual parameter.
|
|
864
|
+
profileName: body.profile,
|
|
752
865
|
reason: body.reason ?? 'started from the workbench UI',
|
|
753
866
|
}
|
|
754
867
|
// A resume is a different host entry point, and the UI must not have to
|
|
@@ -763,7 +876,7 @@ export function createHandlers(ctx) {
|
|
|
763
876
|
// sitting right there in the panel.
|
|
764
877
|
const result = resumeJobId === null
|
|
765
878
|
? await studio().startFinalRender(request)
|
|
766
|
-
: await studio().resumeRenderJob({
|
|
879
|
+
: await studio().resumeRenderJob({ projectId: params.projectId, jobId: resumeJobId })
|
|
767
880
|
return { job: result, resumed: resumeJobId !== null }
|
|
768
881
|
},
|
|
769
882
|
}
|
|
@@ -798,7 +911,7 @@ async function buildProjectState(studio, projectId, revision, threshold) {
|
|
|
798
911
|
project: buildProjectView({
|
|
799
912
|
projectId,
|
|
800
913
|
title: overview.title,
|
|
801
|
-
goal:
|
|
914
|
+
goal: scene.spec?.project?.goal ?? '',
|
|
802
915
|
currentRevision: overview.currentRevision,
|
|
803
916
|
revisionCount: overview.revisionCount,
|
|
804
917
|
createdAt: record?.createdAt ?? null,
|
|
@@ -811,6 +924,7 @@ async function buildProjectState(studio, projectId, revision, threshold) {
|
|
|
811
924
|
revision: scene.revision,
|
|
812
925
|
digest: scene.digest,
|
|
813
926
|
compiled: scene.compiledSpec ?? null,
|
|
927
|
+
assetParts: scene.assetParts ?? [],
|
|
814
928
|
}),
|
|
815
929
|
revisions: overview.revisions ?? [],
|
|
816
930
|
jobs: jobs.jobs.map(job => buildJobView(job, { threshold })),
|
|
@@ -842,3 +956,10 @@ function numberOrUndefined(value) {
|
|
|
842
956
|
* @returns {Record<string, unknown>}
|
|
843
957
|
*/
|
|
844
958
|
export { buildSettingsCard }
|
|
959
|
+
|
|
960
|
+
function requiredRevision(value) {
|
|
961
|
+
if (typeof value !== 'string' || !/^r[0-9]+$/.test(value)) {
|
|
962
|
+
throw new BlenderError(BlenderErrorCode.SCENE_PATCH_INVALID, 'Select a saved revision before reviewing it.')
|
|
963
|
+
}
|
|
964
|
+
return value
|
|
965
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepblend/dsh-blender-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "DeepBlend workbench UI: Host half (typed HTTP API) and Web Client half (sidebar, panels, tool cards).",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
}
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@deepblend/dsh-blender-contracts": "0.
|
|
22
|
+
"@deepblend/dsh-blender-contracts": "0.3.1"
|
|
23
23
|
},
|
|
24
24
|
"license": "MIT",
|
|
25
25
|
"repository": {
|