@deepblend/dsh-blender-tool 0.9.0 → 0.10.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.
@@ -0,0 +1,36 @@
1
+ /** Compare two already verified reports for the same explicit surface selection. */
2
+ export function compareSurfaceReports(reference, current) {
3
+ const chords = [], separations = [], chordPercentages = [], separationPercentages = []
4
+ const samples = current.surfaceSamples.map((after, i) => {
5
+ const before = reference.surfaceSamples[i]
6
+ const values = sample => ({ status: sample.status,
7
+ ...(sample.status === 'sampled' ? { normalSurfaceChordMm: sample.normalSurfaceChordMm } : {}),
8
+ ...(sample.gap ? { targetStatus: sample.gap.status,
9
+ ...(sample.gap.status === 'sampled' ? { normalRaySeparationMm: sample.gap.normalRaySeparationMm } : {}) } : {}) })
10
+ const change = { sampleId: after.sampleId, entityId: after.entityId,
11
+ ...(after.targetEntityId ? { targetEntityId: after.targetEntityId } : {}), before: values(before), after: values(after) }
12
+ if (before.status === 'sampled' && after.status === 'sampled') {
13
+ change.normalSurfaceChordChangeMm = after.normalSurfaceChordMm - before.normalSurfaceChordMm
14
+ change.normalSurfaceChordChangePercent = change.normalSurfaceChordChangeMm / before.normalSurfaceChordMm * 100
15
+ chords.push(Math.abs(change.normalSurfaceChordChangeMm))
16
+ chordPercentages.push(Math.abs(change.normalSurfaceChordChangePercent))
17
+ }
18
+ if (before.gap?.status === 'sampled' && after.gap?.status === 'sampled') {
19
+ change.normalRaySeparationChangeMm = after.gap.normalRaySeparationMm - before.gap.normalRaySeparationMm
20
+ change.normalRaySeparationChangePercent = change.normalRaySeparationChangeMm / before.gap.normalRaySeparationMm * 100
21
+ separations.push(Math.abs(change.normalRaySeparationChangeMm))
22
+ separationPercentages.push(Math.abs(change.normalRaySeparationChangePercent))
23
+ }
24
+ return change
25
+ })
26
+ return { schemaVersion: 'deepblend.surface-comparison/v1', referenceInspection: reference, samples,
27
+ summary: { requestedSamples: samples.length, comparedChords: chords.length, comparedSeparations: separations.length,
28
+ ...(chords.length ? { maxAbsoluteChordChangeMm: Math.max(...chords), maxAbsoluteChordChangePercent: Math.max(...chordPercentages) } : {}),
29
+ ...(separations.length ? { maxAbsoluteSeparationChangeMm: Math.max(...separations), maxAbsoluteSeparationChangePercent: Math.max(...separationPercentages) } : {}) },
30
+ limitations: [
31
+ 'Changes are current minus reference for identical explicit world seeds, parts and frame. These are not necessarily the same material points.',
32
+ 'Maxima cover every comparable requested sample, including chord-only samples. Unavailable pairs have no numeric change and are not zero.',
33
+ 'Percent changes use each positive reference measurement as denominator. Chord and separation percentages are separate; do not substitute one maximum for the other or derive them from rounded display values.',
34
+ 'Observed changes do not establish their cause, a global minimum, manufacturing tolerance, symmetry, collision absence or artistic approval.',
35
+ ] }
36
+ }
package/lib/shared.js CHANGED
@@ -112,7 +112,7 @@ export async function persistImage(attachments, bytes, name) {
112
112
  *
113
113
  * @param {string} headline
114
114
  * @param {object} data
115
- * @param {{ notes?: string[], warnings?: object[] }} [options]
115
+ * @param {{ notes?: string[], warnings?: object[], compact?: boolean }} [options]
116
116
  * @returns {string}
117
117
  */
118
118
  export function renderSuccess(headline, data, options = {}) {
@@ -126,7 +126,7 @@ export function renderSuccess(headline, data, options = {}) {
126
126
  lines.push('')
127
127
  }
128
128
  lines.push('Canonical JSON:')
129
- lines.push(JSON.stringify(data, null, 2))
129
+ lines.push(JSON.stringify(data, null, options.compact ? undefined : 2))
130
130
  return lines.join('\n')
131
131
  }
132
132
 
package/lib/tools.js CHANGED
@@ -53,6 +53,7 @@ import {
53
53
  requestApproval,
54
54
  resolveStudio,
55
55
  } from './shared.js'
56
+ import { compareSurfaceReports } from './geometry-comparison.js'
56
57
 
57
58
  /** A short parameter description of a SceneSpec engine key. */
58
59
  const ENGINE_KEY_DESCRIPTION =
@@ -393,7 +394,13 @@ function sceneGet(ctx) {
393
394
  'To measure saved native parts, pass geometryEntityIds (1–8 IDs from this scene) and an explicit geometryFrame. ' +
394
395
  'This returns source-verified world-axis spans in millimeters and transforms without rendering or creating a revision. ' +
395
396
  'Spans include rotation, handles and protrusions; they are not body diameter, wall thickness, clearance or quality approval. ' +
396
- 'Do not subtract shell/liner spans or radii to claim a gap, or use unchanged height/scale to certify wall thickness. Those surface conditions remain unmeasured. ' +
397
+ 'Do not subtract shell/liner spans or radii to claim a gap, or use unchanged height/scale to certify wall thickness. ' +
398
+ 'Optional geometrySurfaceSamples measures 1–8 explicit local surface chords and pair separations on the same saved source. ' +
399
+ 'The chord runs from source entry to exit; separation runs from source EXIT to target entry and excludes that chord. ' +
400
+ 'Use geometryOnly:true for a compact verified measurement receipt without the scene catalogue. ' +
401
+ 'When comparing revisions, add geometryCompareRevision with identical surface samples; the tool computes per-sample changes and maxima, including chord-only samples. ' +
402
+ 'Choose outside world seeds and directions from the actual scene/bounds; missed or unsupported samples remain unavailable. ' +
403
+ 'Local samples do not prove full-surface minimum wall thickness, whole-assembly clearance, no collision or quality approval. ' +
397
404
  'ALWAYS use the revision this returns as `baseRevision` for your next blender_scene_patch.',
398
405
  parameters: {
399
406
  projectId: { type: 'string', required: true, description: 'The project id.' },
@@ -402,6 +409,17 @@ function sceneGet(ctx) {
402
409
  description: 'Optional: measure 1–8 distinct saved native entity IDs. Requires geometryFrame. Hidden viewport objects and non-mesh roots are explicitly unavailable.' },
403
410
  geometryFrame: { type: 'integer',
404
411
  description: 'Explicit integer frame from 0 to 1000000 for native geometry measurement. Use the frame of the preview being checked. Requires geometryEntityIds.' },
412
+ geometryOnly: { type: 'boolean', description: 'Return only source identity and verified geometry evidence, without the scene catalogue. Requires geometryEntityIds and geometryFrame; cannot be combined with full:true. Ordinary reads keep their existing shape.' },
413
+ geometryCompareRevision: { type: 'string', description: 'Optional explicit reference revision (e.g. r0004). Requires geometrySurfaceSamples. Re-measures that saved source with identical parts, seeds and frame, and computes current-minus-reference changes and maxima for all comparable samples. Unavailable pairs remain unavailable. No global minima or change cause is established.' },
414
+ geometrySurfaceSamples: { type: 'array', description: 'Optional 1–8 local surface samples. Requires geometryEntityIds including every source/target and geometryFrame. Numeric results are local evidence only.',
415
+ items: { type: 'object', additionalProperties: false, properties: {
416
+ sampleId: { type: 'string', required: true, description: 'Distinct short letters/numbers/underscore/hyphen identifier (1–64 characters).' },
417
+ entityId: { type: 'string', required: true, description: 'Source mesh ID in geometryEntityIds.' },
418
+ targetEntityId: { type: 'string', description: 'Optional distinct selected mesh for separation after the source exit.' },
419
+ originMeters: { type: 'array', items: { type: 'number' }, required: true, description: 'Three finite world metre coordinates outside the source. Do not use declared wall or gap values as measurements.' },
420
+ direction: { type: 'array', items: { type: 'number' }, required: true, description: 'Three finite non-zero direction components toward the source; normalized by the runtime.' },
421
+ searchDistanceMm: { type: 'number', required: true, description: 'Explicit seed search limit from 0.01 to 500 mm. Normal chord and target searches are separately limited to 100 mm.' },
422
+ } } },
405
423
  full: {
406
424
  type: 'boolean',
407
425
  description: 'Also return the complete SceneSpec and its compiled form. Costs a lot of context — ' +
@@ -413,27 +431,63 @@ function sceneGet(ctx) {
413
431
  const resolved = resolveStudio(ctx)
414
432
  if (resolved.unavailable !== undefined) return { ok: false, ...resolved.unavailable }
415
433
  try {
434
+ if (args.geometryOnly !== undefined && typeof args.geometryOnly !== 'boolean') {
435
+ throw new BlenderError(BlenderErrorCode.SCRIPT_ERROR, 'geometryOnly must be boolean.')
436
+ }
437
+ if (args.geometryOnly === true && (args.geometryEntityIds === undefined || args.full === true)) {
438
+ throw new BlenderError(BlenderErrorCode.SCRIPT_ERROR, 'geometryOnly requires a native measurement selection and cannot be combined with full:true.')
439
+ }
416
440
  if ((args.geometryEntityIds === undefined) !== (args.geometryFrame === undefined)) {
417
441
  throw new BlenderError(BlenderErrorCode.SCRIPT_ERROR, 'Native measurements require both geometryEntityIds and geometryFrame.')
418
442
  }
443
+ if (args.geometrySurfaceSamples !== undefined && args.geometryEntityIds === undefined) {
444
+ throw new BlenderError(BlenderErrorCode.SCRIPT_ERROR, 'Surface samples require geometryEntityIds and geometryFrame.')
445
+ }
446
+ if (args.geometryCompareRevision !== undefined && (typeof args.geometryCompareRevision !== 'string' ||
447
+ !/^r\d{4,}$/.test(args.geometryCompareRevision) || args.geometrySurfaceSamples === undefined)) {
448
+ throw new BlenderError(BlenderErrorCode.SCRIPT_ERROR, 'geometryCompareRevision requires an explicit saved revision and surface samples.')
449
+ }
419
450
  if (args.geometryEntityIds !== undefined) validateGeometrySelection({ entityIds: args.geometryEntityIds,
420
- frame: args.geometryFrame, checkpointSha256: '0'.repeat(64) })
451
+ frame: args.geometryFrame, checkpointSha256: '0'.repeat(64), ...(args.geometrySurfaceSamples === undefined ? {} : { surfaceSamples: args.geometrySurfaceSamples }) })
421
452
  const { data, canonicalWarnings } = await canonicalCall(resolved.studio.getScene(args.projectId, {
422
453
  ...definedFields({ revision: args.revision }),
423
454
  full: args.full === true,
424
- ...(args.geometryEntityIds === undefined ? {} : { geometry: { entityIds: args.geometryEntityIds, frame: args.geometryFrame }, signal: exec?.signal }),
455
+ ...(args.geometryEntityIds === undefined ? {} : { geometry: { entityIds: args.geometryEntityIds, frame: args.geometryFrame,
456
+ ...(args.geometrySurfaceSamples === undefined ? {} : { surfaceSamples: args.geometrySurfaceSamples }) }, signal: exec?.signal }),
425
457
  }), warning)
426
- if (args.geometryEntityIds !== undefined && (!data.geometryInspection || data.geometryInspection.schemaVersion !== 'deepblend.scene-geometry/v1' ||
427
- data.geometryInspection.projectId !== args.projectId || data.geometryInspection.sourceRevision !== data.revision ||
428
- data.geometryInspection.sourceDigest !== data.digest || data.geometryInspection.sourceUnchanged !== true || data.geometryInspection.frame !== args.geometryFrame ||
429
- !Array.isArray(data.geometryInspection.limitations) ||
430
- JSON.stringify(data.geometryInspection.objects?.map(o => o.entityId)) !== JSON.stringify(args.geometryEntityIds))) {
431
- throw new BlenderError(BlenderErrorCode.RUNTIME_UNAVAILABLE, 'The Host did not return measurements for the requested source and frame. Upgrade DeepBlend before measuring; ordinary scene reads remain available.')
458
+ const hasSamples = args.geometrySurfaceSamples !== undefined
459
+ const verifyScene = (data, expectedRevision) => {
460
+ if (expectedRevision !== undefined && data.revision !== expectedRevision) {
461
+ throw new BlenderError(BlenderErrorCode.RUNTIME_UNAVAILABLE, 'The Host returned a different saved revision than requested.')
462
+ }
463
+ if (args.geometryEntityIds !== undefined && (!data.geometryInspection || data.geometryInspection.schemaVersion !== (hasSamples ? 'deepblend.scene-geometry/v2' : 'deepblend.scene-geometry/v1') ||
464
+ data.geometryInspection.projectId !== args.projectId || data.geometryInspection.sourceRevision !== data.revision ||
465
+ data.geometryInspection.sourceDigest !== data.digest || data.geometryInspection.sourceUnchanged !== true || data.geometryInspection.frame !== args.geometryFrame ||
466
+ !Array.isArray(data.geometryInspection.limitations) ||
467
+ JSON.stringify(data.geometryInspection.objects?.map(o => o.entityId)) !== JSON.stringify(args.geometryEntityIds))) {
468
+ throw new BlenderError(BlenderErrorCode.RUNTIME_UNAVAILABLE, 'The Host did not return measurements for the requested source and frame. Upgrade DeepBlend before measuring; ordinary scene reads remain available.')
469
+ }
470
+ if (data.geometryInspection) verifyGeometryReport({ frame: data.geometryInspection.frame, blenderVersion: data.geometryInspection.blenderVersion, objects: data.geometryInspection.objects,
471
+ schemaVersion: hasSamples ? 'deepblend.geometry-inspection/v2' : 'deepblend.geometry-inspection/v1',
472
+ ...(hasSamples ? { surfaceSamples: data.geometryInspection.surfaceSamples } : {}), checkpointSha256: data.geometryInspection.sourceCheckpointSha256,
473
+ checkpointUnchanged: data.geometryInspection.sourceUnchanged }, { entityIds: args.geometryEntityIds, frame: args.geometryFrame,
474
+ checkpointSha256: data.geometryInspection.sourceCheckpointSha256, ...(hasSamples ? { surfaceSamples: args.geometrySurfaceSamples } : {}) })
475
+ }
476
+ verifyScene(data, args.revision)
477
+ if (args.geometryCompareRevision !== undefined) {
478
+ const reference = await canonicalCall(resolved.studio.getScene(args.projectId, { revision: args.geometryCompareRevision,
479
+ full: false, geometry: { entityIds: args.geometryEntityIds, frame: args.geometryFrame, surfaceSamples: args.geometrySurfaceSamples }, signal: exec?.signal }), warning)
480
+ verifyScene(reference.data, args.geometryCompareRevision)
481
+ canonicalWarnings.push(...reference.canonicalWarnings)
482
+ data.geometryComparison = compareSurfaceReports(reference.data.geometryInspection, data.geometryInspection)
483
+ }
484
+ if (args.geometryOnly === true) {
485
+ const receipt = { projectId: args.projectId, revision: data.revision, digest: data.digest,
486
+ isCurrent: data.isCurrent, geometryInspection: data.geometryInspection,
487
+ ...(data.geometryComparison ? { geometryComparison: data.geometryComparison } : {}) }
488
+ return { ok: true, text: renderSuccess(`Saved native measurement receipt for "${args.projectId}" at ${data.revision}.`, receipt,
489
+ { warnings: canonicalWarnings, compact: true }), data: receipt }
432
490
  }
433
- if (data.geometryInspection) verifyGeometryReport({ frame: data.geometryInspection.frame, blenderVersion: data.geometryInspection.blenderVersion, objects: data.geometryInspection.objects,
434
- schemaVersion: 'deepblend.geometry-inspection/v1', checkpointSha256: data.geometryInspection.sourceCheckpointSha256,
435
- checkpointUnchanged: data.geometryInspection.sourceUnchanged }, { entityIds: args.geometryEntityIds, frame: args.geometryFrame,
436
- checkpointSha256: data.geometryInspection.sourceCheckpointSha256 })
437
491
  const counts = data.counts ?? {}
438
492
  const notes = [
439
493
  `Revision ${data.revision}${data.isCurrent ? ' (current)' : ' (NOT current — patches against it will be refused)'}`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepblend/dsh-blender-tool",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "description": "Agent-preset tool consumer: registers the model-visible DeepBlend Blender tools. Publishes no service.",
6
6
  "main": "lib/index.js",
@@ -11,7 +11,7 @@
11
11
  "./package.json": "./package.json"
12
12
  },
13
13
  "dependencies": {
14
- "@deepblend/dsh-blender-contracts": "0.9.0"
14
+ "@deepblend/dsh-blender-contracts": "0.10.0"
15
15
  },
16
16
  "license": "MIT",
17
17
  "repository": {