@deepblend/dsh-blender-ui 0.8.1 → 0.9.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 CHANGED
@@ -376,6 +376,22 @@ window.__ModuleLoader__.load({
376
376
  'editor.conflict': '场景已更新。草稿保留,请重新载入后编辑。',
377
377
  'editor.pending': '尚未应用;输入不会自动启动渲染。',
378
378
  'editor.clean': '尚无修改',
379
+ 'geometry.title': '保存模型的实际尺寸',
380
+ 'geometry.measure': '检查此部件',
381
+ 'geometry.busy': '正在检查…',
382
+ 'geometry.cancel': '取消尺寸检查',
383
+ 'geometry.cancelled': '尺寸检查已取消,已保存模型保持不变。',
384
+ 'geometry.frame': '检查帧',
385
+ 'geometry.source': '保存版本 {revision} · 第 {frame} 帧',
386
+ 'geometry.help': '各世界方向的包围尺寸,包含旋转、把手和突出部分;不是壁厚、装配间隙或质量评审。',
387
+ 'geometry.draft': '检查已保存模型;尚未应用的草稿不在结果中。',
388
+ 'geometry.stale': '保存版本或检查帧已变化,请重新检查。',
389
+ 'geometry.invalid': '返回结果与所选部件、版本或帧不一致,未采用尺寸。',
390
+ 'geometry.unavailable': '此部件不可测:{reason}',
391
+ 'geometry.viewport-disabled': '视口中未启用',
392
+ 'geometry.not-mesh': '不是单个网格部件',
393
+ 'geometry.empty-or-invalid-geometry': '没有有效网格',
394
+ 'geometry.mesh': '网格:{vertices} 个顶点 · {faces} 个面',
379
395
  'editor.animated': '动画控制的通道已禁用。',
380
396
  'editor.invalid': '{field} 的值无效。',
381
397
  'editor.size': '尺寸(mm)',
@@ -975,6 +991,22 @@ window.__ModuleLoader__.load({
975
991
  'editor.conflict': 'The scene changed. Your draft is kept; reload before editing again.',
976
992
  'editor.pending': 'Not applied yet. Typing does not start a render.',
977
993
  'editor.clean': 'No changes yet',
994
+ 'geometry.title': 'Actual saved model dimensions',
995
+ 'geometry.measure': 'Inspect this part',
996
+ 'geometry.busy': 'Inspecting…',
997
+ 'geometry.cancel': 'Cancel measurement',
998
+ 'geometry.cancelled': 'Measurement cancelled. The saved model is unchanged.',
999
+ 'geometry.frame': 'Inspection frame',
1000
+ 'geometry.source': 'Saved revision {revision} · frame {frame}',
1001
+ 'geometry.help': 'World-axis bounding spans include rotation, handles and protrusions. They do not establish wall thickness, assembly clearance or quality approval.',
1002
+ 'geometry.draft': 'Measures the saved model; unapplied drafts are excluded.',
1003
+ 'geometry.stale': 'The saved revision or inspection frame changed. Inspect again.',
1004
+ 'geometry.invalid': 'The result does not match the selected part, revision or frame. No dimensions accepted.',
1005
+ 'geometry.unavailable': 'This part cannot be measured: {reason}',
1006
+ 'geometry.viewport-disabled': 'disabled in the viewport',
1007
+ 'geometry.not-mesh': 'not a single mesh part',
1008
+ 'geometry.empty-or-invalid-geometry': 'no valid mesh',
1009
+ 'geometry.mesh': 'Mesh: {vertices} vertices · {faces} faces',
978
1010
  'editor.animated': 'Channels driven by animation are disabled.',
979
1011
  'editor.invalid': 'Invalid value for {field}.',
980
1012
  'editor.size': 'Size (mm)',
@@ -2763,6 +2795,7 @@ window.__ModuleLoader__.load({
2763
2795
  photographyReuse: {},
2764
2796
  editorEntityId: null,
2765
2797
  editorDrafts: {},
2798
+ geometryMeasurements: {},
2766
2799
  editorLastEdits: {},
2767
2800
  editorComparison: null,
2768
2801
  assemblyDrafts: {},
@@ -2835,6 +2868,7 @@ window.__ModuleLoader__.load({
2835
2868
  let photographySourceSequence = 0
2836
2869
  const photographyWork = (projectId, changes) => set({ photographyWork: { ...data.photographyWork, [projectId]: { ...data.photographyWork[projectId], ...changes } } })
2837
2870
  const inspectionControllers = new Map()
2871
+ const geometryControllers = new Map()
2838
2872
  const imageDownloadControllers = new Map()
2839
2873
  const assetControllers = new Map()
2840
2874
  const assetListSequences = new Map()
@@ -3784,6 +3818,47 @@ window.__ModuleLoader__.load({
3784
3818
  const draft = data.editorDrafts[key] || createEditorDraft(scene, projectId, entityId)
3785
3819
  if (draft) set({ editorEntityId: entityId, editorDrafts: { ...data.editorDrafts, [key]: draft } })
3786
3820
  },
3821
+ setGeometryFrame: value => {
3822
+ const draft = editorDraftFor(data)
3823
+ if (!draft) return
3824
+ const key = editorKey(draft.projectId, draft.entityId), previous = data.geometryMeasurements[key]
3825
+ if (previous?.busy) return
3826
+ set({ geometryMeasurements: { ...data.geometryMeasurements, [key]: { ...previous, frame: value } } })
3827
+ },
3828
+ cancelGeometry: () => {
3829
+ const draft = editorDraftFor(data)
3830
+ if (draft) geometryControllers.get(editorKey(draft.projectId, draft.entityId))?.abort()
3831
+ },
3832
+ measureGeometry: async () => {
3833
+ const draft = editorDraftFor(data), scene = data.selected?.scene
3834
+ if (!draft || !scene || draft.baseRevision !== scene.revision || data.busy.editor) return
3835
+ const key = editorKey(draft.projectId, draft.entityId), previous = data.geometryMeasurements[key]
3836
+ const frame = previous && Object.hasOwn(previous, 'frame') ? previous.frame : scene.project.frameStart
3837
+ if (previous?.busy || !Number.isSafeInteger(frame) || frame < scene.project.frameStart || frame > scene.project.frameEnd) return
3838
+ const projectId = draft.projectId, entityId = draft.entityId, revision = scene.revision, digest = scene.digest, projectsRoot = data.projectsRoot
3839
+ const controller = new AbortController(); geometryControllers.set(key, controller)
3840
+ const update = changes => set({ geometryMeasurements: { ...data.geometryMeasurements,
3841
+ [key]: { ...data.geometryMeasurements[key], ...changes } } })
3842
+ update({ frame, projectsRoot, busy: true, error: null, receipt: null })
3843
+ try {
3844
+ const result = await readRoute((url, options) => fetchImpl(url, { ...options, signal: controller.signal }),
3845
+ `${projectRoute(projectId, '/scene')}?revision=${encodeURIComponent(revision)}&geometryEntityId=${encodeURIComponent(entityId)}&geometryFrame=${frame}`, 'project.scene')
3846
+ if (controller.signal.aborted) throw Error(t('geometry.cancelled'))
3847
+ if (data.projectsRoot !== projectsRoot) throw Error(t('geometry.stale'))
3848
+ if (result.status !== 'ok') throw Error(`${result.error?.code || 'UI_READ_FAILED'}: ${result.error?.message || t('geometry.invalid')}`)
3849
+ const receipt = result.payload.geometryInspection, object = receipt?.objects?.[0]
3850
+ if (result.payload.revision !== revision || result.payload.digest !== digest || receipt?.schemaVersion !== 'deepblend.scene-geometry/v1' ||
3851
+ receipt.projectId !== projectId || receipt.sourceRevision !== revision || receipt.sourceDigest !== digest || receipt.frame !== frame ||
3852
+ receipt.sourceUnchanged !== true || !/^[a-f0-9]{64}$/.test(receipt.sourceCheckpointSha256 || '') ||
3853
+ receipt.objects?.length !== 1 || object?.entityId !== entityId ||
3854
+ (object.available !== true && object.available !== false) ||
3855
+ (object.available === true && (!Array.isArray(object.worldAxisExtentMm) || object.worldAxisExtentMm.length !== 3 || !object.worldAxisExtentMm.every(x => Number.isFinite(x) && x >= 0)))) {
3856
+ throw Error(t('geometry.invalid'))
3857
+ }
3858
+ update({ receipt })
3859
+ } catch (error) { update({ error: controller.signal.aborted ? t('geometry.cancelled') : error.message || String(error) }) }
3860
+ finally { geometryControllers.delete(key); update({ busy: false }) }
3861
+ },
3787
3862
  resetEditor: () => {
3788
3863
  const current = editorDraftFor(data), scene = data.selected?.scene
3789
3864
  if (!current || !scene || data.busy.editor) return
@@ -4173,6 +4248,8 @@ window.__ModuleLoader__.load({
4173
4248
  photographySourceControllers.clear()
4174
4249
  for (const controller of inspectionControllers.values()) controller.abort()
4175
4250
  inspectionControllers.clear()
4251
+ for (const controller of geometryControllers.values()) controller.abort()
4252
+ geometryControllers.clear()
4176
4253
  for (const controller of imageDownloadControllers.values()) controller.abort('stopped')
4177
4254
  for (const controller of assetControllers.values()) controller.abort()
4178
4255
  assetControllers.clear()
@@ -4576,10 +4653,35 @@ window.__ModuleLoader__.load({
4576
4653
  || entity.materialBindings?.some(binding => binding.materialId === draft.material.id)
4577
4654
  || entity.assetParts?.some(item => item.materialSlots?.some(slot => slot.materialId === draft.material.id))).map(entity => entity.id)
4578
4655
  const canModify = draft.entity.kind === 'generator' || (draft.entity.kind === 'asset-instance' && parts.length === 1)
4656
+ const measured = state.geometryMeasurements?.[editorKey(draft.projectId, draft.entityId)] || {}
4657
+ const frame = Object.hasOwn(measured, 'frame') ? measured.frame : state.selected.scene.project.frameStart, receipt = measured.receipt
4658
+ const measurementCurrent = measured.projectsRoot === state.projectsRoot && receipt?.sourceRevision === state.selected.scene.revision && receipt.sourceDigest === state.selected.scene.digest && receipt.frame === frame
4659
+ const object = measurementCurrent ? receipt.objects[0] : null
4660
+ const unavailableReason = { 'viewport-disabled': t('geometry.viewport-disabled'), 'not-mesh': t('geometry.not-mesh'),
4661
+ 'empty-or-invalid-geometry': t('geometry.empty-or-invalid-geometry') }[object?.unavailableReason]
4579
4662
  return el('div', { className: 'db-card', 'data-scene-editor': true, 'data-editor-entity': draft.entityId, 'data-editor-project': draft.projectId,
4580
4663
  'data-editor-base-revision': draft.baseRevision, 'data-editor-dirty': String(dirty), 'data-editor-conflict': String(conflict) },
4581
4664
  el('h4', null, `${t('editor.title')} · ${draft.entityId}`),
4582
4665
  el('div', { className: conflict ? 'db-error' : 'db-muted' }, conflict ? t('editor.conflict') : dirty ? t('editor.pending') : t('editor.clean')),
4666
+ el('div', { className: 'db-card', 'data-native-geometry': true },
4667
+ el('h4', null, t('geometry.title')), el('p', { className: 'db-muted' }, t('geometry.help')),
4668
+ dirty ? el('p', { className: 'db-muted' }, t('geometry.draft')) : null,
4669
+ el('label', { className: 'db-row' }, t('geometry.frame'), el('input', { className: 'db-input', type: 'number',
4670
+ 'data-field': 'geometry-frame', 'aria-label': t('geometry.frame'), value: frame ?? '', min: state.selected.scene.project.frameStart,
4671
+ max: state.selected.scene.project.frameEnd, step: 1, disabled: measured.busy, style: { width: '125px' },
4672
+ onChange: event => actions.setGeometryFrame(event.target.value === '' ? null : Number(event.target.value)) })),
4673
+ Button({ action: 'geometry-measure', children: measured.busy ? t('geometry.busy') : t('geometry.measure'),
4674
+ disabled: disabled || measured.busy || !Number.isSafeInteger(frame) || frame < state.selected.scene.project.frameStart || frame > state.selected.scene.project.frameEnd,
4675
+ onClick: actions.measureGeometry }),
4676
+ measured.busy ? Button({ action: 'geometry-cancel', children: t('geometry.cancel'), onClick: actions.cancelGeometry }) : null,
4677
+ measured.error ? el('div', { className: 'db-error', role: 'alert' }, measured.error) : null,
4678
+ receipt && !measurementCurrent ? el('p', { className: 'db-muted' }, t('geometry.stale')) : null,
4679
+ object ? el('div', { 'data-measured-revision': receipt.sourceRevision, 'data-measured-frame': receipt.frame },
4680
+ el('p', { className: 'db-muted' }, t('geometry.source', { revision: receipt.sourceRevision, frame: receipt.frame })),
4681
+ object.available ? [el('div', { className: 'db-inline', 'data-native-spans': true }, object.worldAxisExtentMm.map((value, axis) =>
4682
+ el('span', { key: axis }, `${['X','Y','Z'][axis]}: ${Number(value.toFixed(3))} mm`))),
4683
+ el('p', { className: 'db-muted' }, t('geometry.mesh', { vertices: object.vertexCount, faces: object.polygonCount }))]
4684
+ : el('p', null, t('geometry.unavailable', { reason: unavailableReason }))) : null),
4583
4685
  el('div', { className: 'db-grid', style: { marginTop: '12px' } },
4584
4686
  el('div', { className: 'db-card' },
4585
4687
  vector(t('editor.position'), 'editor-location', draft.entity.transform.location, (axis, value) => actions.updateEditor('transform', ['location', axis], value), 1000,
package/lib/index.js CHANGED
@@ -47,6 +47,8 @@ import {
47
47
  DIAGNOSTICS_FAILURE_LIMIT,
48
48
  matchUiRoute,
49
49
  assertKnownConfigKeys,
50
+ validateGeometrySelection,
51
+ verifyGeometryReport,
50
52
  } from '@deepblend/dsh-blender-contracts'
51
53
 
52
54
  /** Route path serving the settings card document (M0's surface, kept verbatim). */
@@ -672,12 +674,32 @@ export function createHandlers(ctx) {
672
674
 
673
675
  'project.overview': async ({ params, query }) => buildProjectState(studio(), params.projectId, query.revision, approvalThreshold()),
674
676
 
675
- 'project.scene': async ({ params, query }) => {
676
- const detail = await studio().getScene(params.projectId, { revision: query.revision, full: true })
677
+ 'project.scene': async ({ params, query, signal }) => {
678
+ let geometry
679
+ if (query.geometryEntityId !== undefined || query.geometryFrame !== undefined) {
680
+ if (typeof query.revision !== 'string' || !query.revision || !/^\d+$/.test(query.geometryFrame ?? '')) {
681
+ throw new BlenderError(BlenderErrorCode.SCRIPT_ERROR, 'Native measurement requires a saved revision, one entity and an explicit integer frame.')
682
+ }
683
+ geometry = { entityIds: [query.geometryEntityId], frame: Number(query.geometryFrame) }
684
+ validateGeometrySelection({ ...geometry, checkpointSha256: '0'.repeat(64) })
685
+ }
686
+ const detail = await studio().getScene(params.projectId, { revision: query.revision, full: true,
687
+ ...(geometry ? { geometry, signal } : {}) })
688
+ if (geometry) {
689
+ const receipt = detail.geometryInspection
690
+ if (!receipt || receipt.schemaVersion !== 'deepblend.scene-geometry/v1' || receipt.projectId !== params.projectId ||
691
+ receipt.sourceRevision !== query.revision || receipt.sourceRevision !== detail.revision || receipt.sourceDigest !== detail.digest || receipt.sourceUnchanged !== true) {
692
+ throw new BlenderError('UI_HOST_API_STALE', 'The Host did not return native measurements for this saved source. Update DeepBlend before measuring.')
693
+ }
694
+ verifyGeometryReport({ frame: receipt.frame, blenderVersion: receipt.blenderVersion, objects: receipt.objects, schemaVersion: 'deepblend.geometry-inspection/v1',
695
+ checkpointSha256: receipt.sourceCheckpointSha256, checkpointUnchanged: receipt.sourceUnchanged },
696
+ { ...geometry, checkpointSha256: receipt.sourceCheckpointSha256 })
697
+ }
677
698
  return {
678
699
  projectId: params.projectId,
679
700
  revision: detail.revision,
680
701
  digest: detail.digest,
702
+ ...(geometry ? { geometryInspection: detail.geometryInspection } : {}),
681
703
  scene: buildSceneTree(detail.spec, {
682
704
  revision: detail.revision,
683
705
  digest: detail.digest,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepblend/dsh-blender-ui",
3
- "version": "0.8.1",
3
+ "version": "0.9.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.8.1"
22
+ "@deepblend/dsh-blender-contracts": "0.9.0"
23
23
  },
24
24
  "license": "MIT",
25
25
  "repository": {