@deepblend/dsh-blender-tool 0.11.0 → 0.12.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,87 @@
1
+ /** Endpoint origins from verified native reports; these are not surface clearances. */
2
+ import { BlenderError, BlenderErrorCode } from '@deepblend/dsh-blender-contracts'
3
+
4
+ const refuse = message => { throw new BlenderError(BlenderErrorCode.SCRIPT_ERROR, `Native assembly origins: ${message}`) }
5
+ const finite = values => {
6
+ if (!values.every(Number.isFinite)) refuse('the derived origin/span values exceed the finite numeric range.')
7
+ return values
8
+ }
9
+ const subtract = (a, b) => finite(a.map((value, axis) => value - b[axis]))
10
+ const subtractMm = (a, b) => finite(subtract(a, b).map(value => value * 1000))
11
+ const identity = report => ({ projectId: report.projectId, revision: report.sourceRevision,
12
+ checkpointSha256: report.sourceCheckpointSha256, frame: report.frame })
13
+ const limitations = [
14
+ 'Offsets join evaluated native OBJECT ORIGINS in world XYZ axes, not surface points. They are not protrusion, contact, seating depth or clearance.',
15
+ 'World-axis offsets and spans change under rotation; they are not intrinsic dimensions or anchor-local coordinates.',
16
+ 'Only the selected parts, ordered pairs and explicit frame are measured. Unavailable parts have no numeric values, never zero.',
17
+ 'No whole-assembly attachment, collision absence, minimum clearance, manufacturing tolerance or artistic approval is established.',
18
+ ]
19
+
20
+ export function validateAssemblyPairs(pairs, entityIds) {
21
+ if (!Array.isArray(entityIds) || !Array.isArray(pairs) || pairs.length < 1 || pairs.length > 8) refuse('select 1–8 explicit ordered pairs within geometryEntityIds.')
22
+ const ids = new Set()
23
+ for (const pair of pairs) {
24
+ if (!pair || typeof pair !== 'object' || Array.isArray(pair) ||
25
+ Object.keys(pair).some(key => !['pairId', 'entityId', 'anchorEntityId'].includes(key)) ||
26
+ typeof pair.pairId !== 'string' || !/^[a-zA-Z0-9_-]{1,64}$/.test(pair.pairId) || ids.has(pair.pairId) ||
27
+ !entityIds.includes(pair.entityId) || !entityIds.includes(pair.anchorEntityId) || pair.entityId === pair.anchorEntityId) {
28
+ refuse('each pair requires a distinct pairId and two different selected entity/anchor IDs; arbitrary fields are refused.')
29
+ }
30
+ ids.add(pair.pairId)
31
+ }
32
+ }
33
+
34
+ function origin(object) {
35
+ const matrix = object.worldMatrix
36
+ if (!Array.isArray(matrix) || matrix.length !== 4 || matrix.some(row => !Array.isArray(row) || row.length !== 4 || row.some(value => !Number.isFinite(value))) ||
37
+ matrix[3].some((value, axis) => value !== (axis === 3 ? 1 : 0))) refuse('the native world matrix is not finite affine geometry.')
38
+ return matrix.slice(0, 3).map(row => row[3])
39
+ }
40
+
41
+ export function readAssemblyOrigins(report, pairs) {
42
+ validateAssemblyPairs(pairs, report.objects.map(object => object.entityId))
43
+ const objects = new Map(report.objects.map(object => [object.entityId, object]))
44
+ return { schemaVersion: 'deepblend.assembly-origins/v1', source: identity(report), coordinateSystem: 'world',
45
+ pairs: pairs.map(pair => {
46
+ const entity = objects.get(pair.entityId), anchor = objects.get(pair.anchorEntityId)
47
+ if (!entity.available || !anchor.available) return { ...pair, status: 'unavailable',
48
+ unavailableParts: [entity, anchor].filter(object => !object.available).map(object => ({ entityId: object.entityId, reason: object.unavailableReason })) }
49
+ const entityOriginMeters = origin(entity), anchorOriginMeters = origin(anchor)
50
+ return { ...pair, status: 'measured', entityOriginMeters, anchorOriginMeters,
51
+ worldOriginOffsetMm: subtractMm(entityOriginMeters, anchorOriginMeters) }
52
+ }), limitations }
53
+ }
54
+
55
+ export function compareAssemblyOrigins(before, after) {
56
+ if (before.source.projectId !== after.source.projectId || before.source.frame !== after.source.frame || before.pairs.length !== after.pairs.length) refuse('comparison requires the same project, frame and ordered pairs.')
57
+ const pairs = after.pairs.map((current, index) => {
58
+ const reference = before.pairs[index]
59
+ if (['pairId', 'entityId', 'anchorEntityId'].some(key => reference[key] !== current[key])) refuse('comparison pair identities differ.')
60
+ return { pairId: current.pairId, entityId: current.entityId, anchorEntityId: current.anchorEntityId,
61
+ before: reference, after: current,
62
+ ...(reference.status === 'measured' && current.status === 'measured' ? { worldOriginOffsetChangeMm: subtract(current.worldOriginOffsetMm, reference.worldOriginOffsetMm) } : {}) }
63
+ })
64
+ return { schemaVersion: 'deepblend.assembly-origin-comparison/v1', referenceSource: before.source, currentSource: after.source,
65
+ pairs, comparedPairs: pairs.filter(pair => pair.worldOriginOffsetChangeMm !== undefined).length,
66
+ limitations: ['Changes are current minus reference; offset preservation does not establish surface contact or attachment correctness.', ...limitations] }
67
+ }
68
+
69
+ /** Inputs have already passed source/selection/frame and native report verification. */
70
+ export function compareGeometryReports(reference, current) {
71
+ if (reference.projectId !== current.projectId || reference.frame !== current.frame || reference.objects.length !== current.objects.length) refuse('comparison requires the same project, frame and ordered entities.')
72
+ const objects = current.objects.map((after, index) => {
73
+ const before = reference.objects[index]
74
+ if (before.entityId !== after.entityId) refuse('comparison entity identities differ.')
75
+ const values = object => object.available ? { status: 'measured', worldAxisExtentMm: object.worldAxisExtentMm, worldOriginMeters: origin(object) } :
76
+ { status: 'unavailable', reason: object.unavailableReason }
77
+ const row = { entityId: after.entityId, before: values(before), after: values(after) }
78
+ if (before.available && after.available) {
79
+ row.worldAxisExtentChangeMm = subtract(after.worldAxisExtentMm, before.worldAxisExtentMm)
80
+ row.worldOriginChangeMm = subtractMm(row.after.worldOriginMeters, row.before.worldOriginMeters)
81
+ }
82
+ return row
83
+ })
84
+ return { schemaVersion: 'deepblend.geometry-comparison/v1', referenceInspection: reference, currentSource: identity(current), objects,
85
+ summary: { requestedEntities: objects.length, comparedEntities: objects.filter(object => object.worldAxisExtentChangeMm !== undefined).length },
86
+ limitations: ['Changes are current minus reference for the same selected native parts and frame; their cause is not established.', ...limitations] }
87
+ }
package/lib/tools.js CHANGED
@@ -54,6 +54,7 @@ import {
54
54
  resolveStudio,
55
55
  } from './shared.js'
56
56
  import { compareSurfaceReports } from './geometry-comparison.js'
57
+ import { validateAssemblyPairs, readAssemblyOrigins, compareAssemblyOrigins, compareGeometryReports } from './assembly-origins.js'
57
58
 
58
59
  /** A short parameter description of a SceneSpec engine key. */
59
60
  const ENGINE_KEY_DESCRIPTION =
@@ -398,7 +399,7 @@ function sceneGet(ctx) {
398
399
  'Optional geometrySurfaceSamples measures 1–8 explicit local surface chords and pair separations on the same saved source. ' +
399
400
  'The chord runs from source entry to exit; separation runs from source EXIT to target entry and excludes that chord. ' +
400
401
  '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
+ 'Add geometryAssemblyPairs to measure explicit ordered object-origin offsets in world XYZ, not surface clearances. When comparing revisions, geometryCompareRevision computes current-minus-reference native spans and origin moves; if surface samples are supplied, their existing local comparison is used. ' +
402
403
  'Choose outside world seeds and directions from the actual scene/bounds; missed or unsupported samples remain unavailable. ' +
403
404
  'Local samples do not prove full-surface minimum wall thickness, whole-assembly clearance, no collision or quality approval. ' +
404
405
  'ALWAYS use the revision this returns as `baseRevision` for your next blender_scene_patch.',
@@ -410,7 +411,13 @@ function sceneGet(ctx) {
410
411
  geometryFrame: { type: 'integer',
411
412
  description: 'Explicit integer frame from 0 to 1000000 for native geometry measurement. Use the frame of the preview being checked. Requires geometryEntityIds.' },
412
413
  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
+ geometryCompareRevision: { type: 'string', description: 'Optional explicit reference revision (e.g. r0004). Requires geometryEntityIds and geometryFrame. Re-measures identical selected parts/frame and computes current-minus-reference native world-axis spans and origin moves. If geometrySurfaceSamples is supplied, compares those same local samples instead. Requested geometryAssemblyPairs also return origin-offset changes. Unavailable evidence stays unavailable; no clearance, global minima or change cause is established.' },
415
+ geometryAssemblyPairs: { type: 'array', description: 'Optional 1–8 ordered native object-origin pairs. Requires geometryEntityIds containing both ends and geometryFrame. Offsets use world XYZ in millimeters; they are not surface protrusion, contact, clearance or whole-assembly certification.',
416
+ items: { type: 'object', additionalProperties: false, properties: {
417
+ pairId: { type: 'string', required: true, description: 'Distinct letters/numbers/underscore/hyphen identifier (1–64 characters).' },
418
+ entityId: { type: 'string', required: true, description: 'Selected native mesh whose object origin is measured.' },
419
+ anchorEntityId: { type: 'string', required: true, description: 'Different selected mesh origin used as anchor; offset is entity minus anchor in world XYZ.' },
420
+ } } },
414
421
  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
422
  items: { type: 'object', additionalProperties: false, properties: {
416
423
  sampleId: { type: 'string', required: true, description: 'Distinct short letters/numbers/underscore/hyphen identifier (1–64 characters).' },
@@ -444,9 +451,10 @@ function sceneGet(ctx) {
444
451
  throw new BlenderError(BlenderErrorCode.SCRIPT_ERROR, 'Surface samples require geometryEntityIds and geometryFrame.')
445
452
  }
446
453
  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.')
454
+ !/^r\d{4,}$/.test(args.geometryCompareRevision) || args.geometryEntityIds === undefined)) {
455
+ throw new BlenderError(BlenderErrorCode.SCRIPT_ERROR, 'geometryCompareRevision requires an explicit saved revision, geometryEntityIds and geometryFrame.')
449
456
  }
457
+ if (args.geometryAssemblyPairs !== undefined) validateAssemblyPairs(args.geometryAssemblyPairs, args.geometryEntityIds)
450
458
  if (args.geometryEntityIds !== undefined) validateGeometrySelection({ entityIds: args.geometryEntityIds,
451
459
  frame: args.geometryFrame, checkpointSha256: '0'.repeat(64), ...(args.geometrySurfaceSamples === undefined ? {} : { surfaceSamples: args.geometrySurfaceSamples }) })
452
460
  const { data, canonicalWarnings } = await canonicalCall(resolved.studio.getScene(args.projectId, {
@@ -474,16 +482,20 @@ function sceneGet(ctx) {
474
482
  checkpointSha256: data.geometryInspection.sourceCheckpointSha256, ...(hasSamples ? { surfaceSamples: args.geometrySurfaceSamples } : {}) })
475
483
  }
476
484
  verifyScene(data, args.revision)
485
+ if (args.geometryAssemblyPairs !== undefined) data.geometryAssemblyOrigins = readAssemblyOrigins(data.geometryInspection, args.geometryAssemblyPairs)
477
486
  if (args.geometryCompareRevision !== undefined) {
478
487
  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)
488
+ full: false, geometry: { entityIds: args.geometryEntityIds, frame: args.geometryFrame, ...(hasSamples ? { surfaceSamples: args.geometrySurfaceSamples } : {}) }, signal: exec?.signal }), warning)
480
489
  verifyScene(reference.data, args.geometryCompareRevision)
481
490
  canonicalWarnings.push(...reference.canonicalWarnings)
482
- data.geometryComparison = compareSurfaceReports(reference.data.geometryInspection, data.geometryInspection)
491
+ data.geometryComparison = hasSamples ? compareSurfaceReports(reference.data.geometryInspection, data.geometryInspection) : compareGeometryReports(reference.data.geometryInspection, data.geometryInspection)
492
+ if (args.geometryAssemblyPairs !== undefined) data.geometryComparison.assemblyOriginComparison = compareAssemblyOrigins(
493
+ readAssemblyOrigins(reference.data.geometryInspection, args.geometryAssemblyPairs), data.geometryAssemblyOrigins)
483
494
  }
484
495
  if (args.geometryOnly === true) {
485
496
  const receipt = { projectId: args.projectId, revision: data.revision, digest: data.digest,
486
497
  isCurrent: data.isCurrent, geometryInspection: data.geometryInspection,
498
+ ...(data.geometryAssemblyOrigins ? { geometryAssemblyOrigins: data.geometryAssemblyOrigins } : {}),
487
499
  ...(data.geometryComparison ? { geometryComparison: data.geometryComparison } : {}) }
488
500
  return { ok: true, text: renderSuccess(`Saved native measurement receipt for "${args.projectId}" at ${data.revision}.`, receipt,
489
501
  { warnings: canonicalWarnings, compact: true }), data: receipt }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepblend/dsh-blender-tool",
3
- "version": "0.11.0",
3
+ "version": "0.12.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.11.0"
14
+ "@deepblend/dsh-blender-contracts": "0.12.0"
15
15
  },
16
16
  "license": "MIT",
17
17
  "repository": {