@deepblend/dsh-blender-contracts 0.1.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/asset-content.js +134 -0
- package/lib/bitmap-font.js +149 -0
- package/lib/canonical.js +98 -0
- package/lib/config-surface.js +88 -0
- package/lib/contact-sheet.js +233 -0
- package/lib/deployment-paths.js +153 -0
- package/lib/index.js +829 -0
- package/lib/json-schema.js +337 -0
- package/lib/png.js +452 -0
- package/lib/projections.js +209 -0
- package/lib/redact.js +53 -0
- package/lib/render-job.js +459 -0
- package/lib/scene-patch.js +947 -0
- package/lib/scene-spec.js +901 -0
- package/lib/schemas/job-result.schema.json +295 -0
- package/lib/schemas/scene-patch.schema.json +1307 -0
- package/lib/schemas/scene-spec.schema.json +791 -0
- package/lib/ui-api.js +695 -0
- package/lib/visual-composition.js +574 -0
- package/lib/visual-issue.js +738 -0
- package/lib/visual-loop.js +401 -0
- package/package.json +19 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @deepblend/dsh-blender-contracts
|
|
3
|
+
*
|
|
4
|
+
* Service Definition and shared types for DeepBlend Studio (SPEC §5.1).
|
|
5
|
+
*
|
|
6
|
+
* This package is the frozen vocabulary shared by every other DeepBlend package:
|
|
7
|
+
* the `BlenderRuntime` Service interface, the `BlenderStudio` facade interface,
|
|
8
|
+
* the Blender bootstrap protocol envelope, stable error codes, and the
|
|
9
|
+
* deepblend-specific artifact boundaries.
|
|
10
|
+
*
|
|
11
|
+
* It publishes NO service and registers NO tool — it is pure data, so it is safe
|
|
12
|
+
* to consume from a Host composition, an Agent preset, or a Web Client half.
|
|
13
|
+
*
|
|
14
|
+
* Owner: DeepBlend Studio — M0
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Blender bootstrap protocol version. Bumped only on a breaking wire change. */
|
|
18
|
+
export const BLENDER_PROTOCOL_VERSION = 'deepblend.blender/v1'
|
|
19
|
+
|
|
20
|
+
/** SceneSpec schema version. Defined in M1; declared here so M0 can reference it. */
|
|
21
|
+
export const SCENE_SCHEMA_VERSION = 'deepblend.scene/v1'
|
|
22
|
+
|
|
23
|
+
/** ScenePatch document version. */
|
|
24
|
+
export const SCENE_PATCH_VERSION = 'deepblend.scene-patch/v1'
|
|
25
|
+
|
|
26
|
+
/** Revision manifest version written beside every revision directory. */
|
|
27
|
+
export const REVISION_MANIFEST_VERSION = 'deepblend.revision-manifest/v1'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The job document a revision TRANSACTION writes (SPEC §9.3 result.json). NOT the durable render-job
|
|
31
|
+
* record, which is `RENDER_JOB_VERSION` — this comment said §10.2 for years while six call sites wrote
|
|
32
|
+
* this string by hand, which is how the two were mistaken for each other.
|
|
33
|
+
*/
|
|
34
|
+
export const JOB_RECORD_VERSION = 'deepblend.job/v1'
|
|
35
|
+
|
|
36
|
+
/** Project record version. */
|
|
37
|
+
export const PROJECT_RECORD_VERSION = 'deepblend.project/v1'
|
|
38
|
+
|
|
39
|
+
/** Settings namespace owned by the DeepBlend host service. */
|
|
40
|
+
export const BLENDER_SETTINGS_NAMESPACE = 'deepblend'
|
|
41
|
+
|
|
42
|
+
/** Canonical telemetry/log scope marker used in warnings and logs. */
|
|
43
|
+
export const LOG_SCOPE = 'deepblend'
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Stable error codes (SPEC §11.1: "每个错误有稳定 errorCode").
|
|
47
|
+
*
|
|
48
|
+
* These are part of the wire contract with the model and the UI. Never renumber
|
|
49
|
+
* or rename one; add a new code instead.
|
|
50
|
+
*/
|
|
51
|
+
export const BlenderErrorCode = Object.freeze({
|
|
52
|
+
/** No Blender executable could be resolved from config or PATH. */
|
|
53
|
+
NOT_FOUND: 'BLENDER_NOT_FOUND',
|
|
54
|
+
/** A configured executable path exists but is not executable / is a directory. */
|
|
55
|
+
EXECUTABLE_NOT_EXECUTABLE: 'BLENDER_EXECUTABLE_NOT_EXECUTABLE',
|
|
56
|
+
/** A configured executable path escapes the allowlist (SPEC §15.2). */
|
|
57
|
+
EXECUTABLE_OUTSIDE_ALLOWLIST: 'BLENDER_EXECUTABLE_OUTSIDE_ALLOWLIST',
|
|
58
|
+
/** ctx.subprocess.spawn threw before a handle existed. */
|
|
59
|
+
SPAWN_FAILED: 'BLENDER_SPAWN_FAILED',
|
|
60
|
+
/** Blender exited with a non-zero code. */
|
|
61
|
+
NONZERO_EXIT: 'BLENDER_NONZERO_EXIT',
|
|
62
|
+
/** The probe exceeded its configured deadline. */
|
|
63
|
+
TIMEOUT: 'BLENDER_TIMEOUT',
|
|
64
|
+
/** The caller's AbortSignal fired. */
|
|
65
|
+
ABORTED: 'BLENDER_ABORTED',
|
|
66
|
+
/** bootstrap.py never wrote its result file. */
|
|
67
|
+
RESULT_MISSING: 'BLENDER_RESULT_MISSING',
|
|
68
|
+
/** The result file was not valid JSON. */
|
|
69
|
+
RESULT_UNPARSEABLE: 'BLENDER_RESULT_UNPARSEABLE',
|
|
70
|
+
/** bootstrap.py wrote a well-formed error envelope. */
|
|
71
|
+
SCRIPT_ERROR: 'BLENDER_SCRIPT_ERROR',
|
|
72
|
+
/** bootstrap.py reported a different protocolVersion. */
|
|
73
|
+
PROTOCOL_VERSION_MISMATCH: 'BLENDER_PROTOCOL_VERSION_MISMATCH',
|
|
74
|
+
/** bootstrap.py reported an action it does not implement. */
|
|
75
|
+
UNSUPPORTED_ACTION: 'BLENDER_UNSUPPORTED_ACTION',
|
|
76
|
+
/** The collection of capabilities threw unexpectedly inside Blender. */
|
|
77
|
+
CAPABILITY_PROBE_FAILED: 'BLENDER_CAPABILITY_PROBE_FAILED',
|
|
78
|
+
/** The configured bootstrap script is missing or unreadable. */
|
|
79
|
+
BOOTSTRAP_MISSING: 'BLENDER_BOOTSTRAP_MISSING',
|
|
80
|
+
/** A requested render engine is not available in this runtime (D1/D2). */
|
|
81
|
+
ENGINE_UNAVAILABLE: 'BLENDER_ENGINE_UNAVAILABLE',
|
|
82
|
+
/** Reserved: no Blender implementation was injected at all. */
|
|
83
|
+
RUNTIME_UNAVAILABLE: 'BLENDER_RUNTIME_UNAVAILABLE',
|
|
84
|
+
|
|
85
|
+
// ---- M1: project, revision and SceneSpec failures ------------------------
|
|
86
|
+
//
|
|
87
|
+
// These are grouped and named so the model can tell apart the three things a
|
|
88
|
+
// failed write can mean: the input was wrong, the caller's view was stale, or
|
|
89
|
+
// the machine failed. Only the middle one is worth retrying by re-reading.
|
|
90
|
+
|
|
91
|
+
/** The project id does not exist in the project store. */
|
|
92
|
+
PROJECT_NOT_FOUND: 'PROJECT_NOT_FOUND',
|
|
93
|
+
/** A project directory exists but its record is missing or unreadable. */
|
|
94
|
+
PROJECT_CORRUPT: 'PROJECT_CORRUPT',
|
|
95
|
+
/** A project with this id already exists. */
|
|
96
|
+
PROJECT_EXISTS: 'PROJECT_EXISTS',
|
|
97
|
+
/** A project id (or a derived slug) is not usable as a directory name. */
|
|
98
|
+
PROJECT_ID_INVALID: 'PROJECT_ID_INVALID',
|
|
99
|
+
|
|
100
|
+
/** The named revision does not exist in this project. */
|
|
101
|
+
REVISION_NOT_FOUND: 'REVISION_NOT_FOUND',
|
|
102
|
+
/** The SceneSpec stored in a revision is missing or no longer parses. */
|
|
103
|
+
REVISION_CORRUPT: 'REVISION_CORRUPT',
|
|
104
|
+
/** A checkpoint was required by the requested action but this revision has none. */
|
|
105
|
+
REVISION_CHECKPOINT_MISSING: 'REVISION_CHECKPOINT_MISSING',
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* `baseRevision` is not the current revision. The caller read a stale view;
|
|
109
|
+
* the write is refused rather than merged, because an implicit merge is how a
|
|
110
|
+
* concurrent editor silently loses someone else's work (SPEC §8.5).
|
|
111
|
+
*/
|
|
112
|
+
REVISION_CONFLICT: 'REVISION_CONFLICT',
|
|
113
|
+
/** No revision could be allocated (clock/sequence guard). */
|
|
114
|
+
REVISION_ALLOCATION_FAILED: 'REVISION_ALLOCATION_FAILED',
|
|
115
|
+
/** The requested revision id does not match the `r0001` grammar. */
|
|
116
|
+
REVISION_ID_INVALID: 'REVISION_ID_INVALID',
|
|
117
|
+
|
|
118
|
+
/** The SceneSpec failed structural or semantic validation. */
|
|
119
|
+
SCENE_SPEC_INVALID: 'SCENE_SPEC_INVALID',
|
|
120
|
+
/** The ScenePatch failed structural validation. */
|
|
121
|
+
SCENE_PATCH_INVALID: 'SCENE_PATCH_INVALID',
|
|
122
|
+
/** The ScenePatch was structurally fine but referenced something absent. */
|
|
123
|
+
SCENE_PATCH_REJECTED: 'SCENE_PATCH_REJECTED',
|
|
124
|
+
/** The compiled scene failed technical validation in Blender. */
|
|
125
|
+
SCENE_VALIDATION_FAILED: 'SCENE_VALIDATION_FAILED',
|
|
126
|
+
/** No camera could be selected for the requested preview. */
|
|
127
|
+
SCENE_CAMERA_MISSING: 'SCENE_CAMERA_MISSING',
|
|
128
|
+
|
|
129
|
+
/** A generated id, path or key would escape the workspace or project root. */
|
|
130
|
+
PATH_OUTSIDE_WORKSPACE: 'PATH_OUTSIDE_WORKSPACE',
|
|
131
|
+
/** A file name or id segment contained a separator or traversal. */
|
|
132
|
+
PATH_SEGMENT_INVALID: 'PATH_SEGMENT_INVALID',
|
|
133
|
+
|
|
134
|
+
/** A referenced asset file is not present in the project. */
|
|
135
|
+
ASSET_MISSING: 'ASSET_MISSING',
|
|
136
|
+
/** This Blender build cannot import the asset's format (D10). */
|
|
137
|
+
ASSET_FORMAT_UNAVAILABLE: 'ASSET_FORMAT_UNAVAILABLE',
|
|
138
|
+
/** The asset hash does not match the recorded sha256. */
|
|
139
|
+
ASSET_HASH_MISMATCH: 'ASSET_HASH_MISMATCH',
|
|
140
|
+
|
|
141
|
+
// ---- M5: the producer side of an asset (SPEC §11 blender_asset_ingest) -----
|
|
142
|
+
//
|
|
143
|
+
// The four above describe a DECLARED asset the consumer cannot use; these describe
|
|
144
|
+
// a source the producer was asked to bring in. They are separate on purpose: "the
|
|
145
|
+
// file you pointed me at is not there" and "the file this scene declares is not
|
|
146
|
+
// there" are different problems with different fixes.
|
|
147
|
+
/** The ingest request itself is malformed — both sources given, or neither. */
|
|
148
|
+
ASSET_REQUEST_INVALID: 'ASSET_REQUEST_INVALID',
|
|
149
|
+
/** The local source path does not name an existing file. */
|
|
150
|
+
ASSET_SOURCE_NOT_FOUND: 'ASSET_SOURCE_NOT_FOUND',
|
|
151
|
+
/** The source is larger than `assetMaxBytes` (SPEC §15). */
|
|
152
|
+
ASSET_TOO_LARGE: 'ASSET_TOO_LARGE',
|
|
153
|
+
/** A remote source could not be fetched, or exceeded the byte cap mid-download. */
|
|
154
|
+
ASSET_FETCH_FAILED: 'ASSET_FETCH_FAILED',
|
|
155
|
+
/**
|
|
156
|
+
* The bytes do not match the extension (SPEC §15.2 "MIME 与扩展名双重校验").
|
|
157
|
+
*
|
|
158
|
+
* Refused only when the content POSITIVELY CONTRADICTS the name — a known signature for
|
|
159
|
+
* another format, a NUL in a format that must be text, or an empty file. A file whose
|
|
160
|
+
* shape this gate cannot read passes, because deciding what a model file really is stays
|
|
161
|
+
* Blender's job (D10) and this only refuses the ones that say out loud that they are
|
|
162
|
+
* something else. See `contracts/lib/asset-content.js`.
|
|
163
|
+
*/
|
|
164
|
+
ASSET_CONTENT_MISMATCH: 'ASSET_CONTENT_MISMATCH',
|
|
165
|
+
/**
|
|
166
|
+
* The volume holding the project ran out of room.
|
|
167
|
+
*
|
|
168
|
+
* Its own code rather than `SCRIPT_ERROR`, because it is an operational event and not a bug:
|
|
169
|
+
* the frames already written are kept, the renderer is stopped, and the job resumes once there
|
|
170
|
+
* is space. MEASURED with `tools/disk-full-probe.mjs` on a 24 MiB volume — before this code
|
|
171
|
+
* existed, the full disk killed the HOST process instead (milestone-status.md §35).
|
|
172
|
+
*/
|
|
173
|
+
DISK_FULL: 'DISK_FULL',
|
|
174
|
+
/**
|
|
175
|
+
* The compiled scene carries more polygons than `maxMeshPolygons` (SPEC §15.2
|
|
176
|
+
* "Mesh 面数限制").
|
|
177
|
+
*
|
|
178
|
+
* A separate code from the timeout, because they are different facts with different
|
|
179
|
+
* fixes: a timeout says "this took too long — try again, or raise the deadline", while
|
|
180
|
+
* this says "this scene is this heavy, and it will be this heavy every time". The count
|
|
181
|
+
* comes from the compile report the provider already produces for the revision manifest,
|
|
182
|
+
* so the refusal names the number instead of guessing at it.
|
|
183
|
+
*/
|
|
184
|
+
SCENE_TOO_HEAVY: 'SCENE_TOO_HEAVY',
|
|
185
|
+
/**
|
|
186
|
+
* A REMOTE ingest needs a grant first (SPEC §11 "本地自动,网络需审批").
|
|
187
|
+
*
|
|
188
|
+
* Enforced in the host for the same reason the render threshold is: every caller
|
|
189
|
+
* passes through the host, and only the tool plane can ask. A local path needs no
|
|
190
|
+
* approval — it is already inside the machine the operator controls.
|
|
191
|
+
*/
|
|
192
|
+
ASSET_APPROVAL_REQUIRED: 'ASSET_APPROVAL_REQUIRED',
|
|
193
|
+
|
|
194
|
+
/** The requested render profile is not defined in the SceneSpec. */
|
|
195
|
+
RENDER_PROFILE_MISSING: 'RENDER_PROFILE_MISSING',
|
|
196
|
+
/** A render was refused because its estimated cost exceeds the configured budget. */
|
|
197
|
+
RENDER_BUDGET_EXCEEDED: 'RENDER_BUDGET_EXCEEDED',
|
|
198
|
+
/** Blender rendered but produced no image file. */
|
|
199
|
+
RENDER_NO_OUTPUT: 'RENDER_NO_OUTPUT',
|
|
200
|
+
|
|
201
|
+
// ---- M3: persistent render jobs, resumption, delivery -------------------
|
|
202
|
+
|
|
203
|
+
/** No durable render job exists under that id in this project. */
|
|
204
|
+
RENDER_JOB_NOT_FOUND: 'RENDER_JOB_NOT_FOUND',
|
|
205
|
+
/** A render job already owns this project's delivery slot. */
|
|
206
|
+
RENDER_JOB_CONFLICT: 'RENDER_JOB_CONFLICT',
|
|
207
|
+
/** The render job is not in a state from which this operation is legal. */
|
|
208
|
+
RENDER_JOB_STATE_INVALID: 'RENDER_JOB_STATE_INVALID',
|
|
209
|
+
/** The requested frame range is empty, inverted, or outside the project's own. */
|
|
210
|
+
RENDER_RANGE_INVALID: 'RENDER_RANGE_INVALID',
|
|
211
|
+
/** A frame sequence finished without every frame of its range. */
|
|
212
|
+
RENDER_FRAMES_INCOMPLETE: 'RENDER_FRAMES_INCOMPLETE',
|
|
213
|
+
/** The encoder executable could not be resolved. */
|
|
214
|
+
ENCODER_NOT_FOUND: 'ENCODER_NOT_FOUND',
|
|
215
|
+
/** ffmpeg ran and failed. */
|
|
216
|
+
ENCODE_FAILED: 'ENCODE_FAILED',
|
|
217
|
+
/** The encoded file does not have the properties the manifest claims. */
|
|
218
|
+
ENCODE_VERIFY_FAILED: 'ENCODE_VERIFY_FAILED',
|
|
219
|
+
/** ffprobe could not be resolved or could not read the file. */
|
|
220
|
+
PROBE_FAILED: 'PROBE_FAILED',
|
|
221
|
+
/** A delivery was requested for a job that has not produced every frame. */
|
|
222
|
+
DELIVERY_INCOMPLETE: 'DELIVERY_INCOMPLETE',
|
|
223
|
+
/**
|
|
224
|
+
* A job is already being encoded, so this delivery would put two encoders on one file.
|
|
225
|
+
*
|
|
226
|
+
* `encodedPath` is one path per job, and an encode is not instantaneous — two deliveries of the same job write
|
|
227
|
+
* the same MP4, and each then verifies a file the other is still writing. MEASURED before this code existed:
|
|
228
|
+
* both callers failed with `ENCODE_VERIFY_FAILED`, a message that blames the video's properties rather than the
|
|
229
|
+
* collision. The refusal names the call that is already running, and the caller loses nothing: the encode it
|
|
230
|
+
* was about to duplicate is happening anyway.
|
|
231
|
+
*/
|
|
232
|
+
EXPORT_IN_PROGRESS: 'EXPORT_IN_PROGRESS',
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* A delivery render is large enough that the operator must approve it first
|
|
236
|
+
* (SPEC §15.1 "高成本最终渲染达阈值审批").
|
|
237
|
+
*
|
|
238
|
+
* Reported as a branchable refusal rather than started-and-warned, because the
|
|
239
|
+
* whole point of a threshold is to PREVENT the cost, not to describe it
|
|
240
|
+
* afterwards. The caller asks the approval service and re-issues with
|
|
241
|
+
* `approved: true`; the detail carries `{frames, threshold}` so the request can
|
|
242
|
+
* state the actual size.
|
|
243
|
+
*/
|
|
244
|
+
RENDER_APPROVAL_REQUIRED: 'RENDER_APPROVAL_REQUIRED',
|
|
245
|
+
|
|
246
|
+
// ---- M4 ----------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
/** A path the browser asked for belongs to no DeepBlend route. */
|
|
249
|
+
UI_ROUTE_NOT_FOUND: 'UI_ROUTE_NOT_FOUND',
|
|
250
|
+
/**
|
|
251
|
+
* An artifact the UI asked to display does not exist.
|
|
252
|
+
*
|
|
253
|
+
* Separate from `REVISION_CORRUPT` on purpose: a preview that was never
|
|
254
|
+
* rendered is a normal state the panel renders as "nothing to show yet", while
|
|
255
|
+
* a revision whose files are inconsistent is a real fault. One code for both
|
|
256
|
+
* would make the UI unable to tell them apart.
|
|
257
|
+
*/
|
|
258
|
+
ARTIFACT_NOT_FOUND: 'ARTIFACT_NOT_FOUND',
|
|
259
|
+
/** A UI request failed inside the Host for a reason with no more specific code. */
|
|
260
|
+
UI_REQUEST_FAILED: 'UI_REQUEST_FAILED',
|
|
261
|
+
/**
|
|
262
|
+
* The running Host service is older than the UI half asking for it.
|
|
263
|
+
*
|
|
264
|
+
* The same half-upgraded deployment M3 taught the tools to name (D59): the
|
|
265
|
+
* packages on disk are newer than the `blenderStudio` the process constructed.
|
|
266
|
+
* The panel must say so, because the alternative is a blank panel over a
|
|
267
|
+
* `TypeError` that names neither the cause nor the fix.
|
|
268
|
+
*/
|
|
269
|
+
UI_HOST_API_STALE: 'UI_HOST_API_STALE',
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
/** Warning codes surface on the successful path, where nothing threw. */
|
|
273
|
+
export const BlenderWarningCode = Object.freeze({
|
|
274
|
+
/** Detection is unavailable; the caller is looking at a degraded result (D1). */
|
|
275
|
+
BLENDER_NOT_INSTALLED: 'BLENDER_NOT_INSTALLED',
|
|
276
|
+
/** A preferred engine IS reachable but is silently absent from the static enum (D1). */
|
|
277
|
+
ENGINE_NOT_IN_STATIC_ENUM: 'ENGINE_NOT_IN_STATIC_ENUM',
|
|
278
|
+
/** A configured/likely-required engine is genuinely unavailable (D2). */
|
|
279
|
+
ENGINE_UNAVAILABLE: 'ENGINE_UNAVAILABLE',
|
|
280
|
+
/** A render profile was downgraded to a reachable engine (D2). */
|
|
281
|
+
ENGINE_DOWNGRADED: 'ENGINE_DOWNGRADED',
|
|
282
|
+
/** CPU-only: no GPU device was detected. */
|
|
283
|
+
GPU_UNAVAILABLE: 'GPU_UNAVAILABLE',
|
|
284
|
+
/** A format the product wants is not provided by this Blender build (SPEC §2.2 gap). */
|
|
285
|
+
FORMAT_UNAVAILABLE: 'FORMAT_UNAVAILABLE',
|
|
286
|
+
/** An add-on could not be enabled. */
|
|
287
|
+
ADDON_ENABLE_FAILED: 'ADDON_ENABLE_FAILED',
|
|
288
|
+
/** A warning passed through verbatim from bootstrap.py. */
|
|
289
|
+
PROBE_WARNING: 'PROBE_WARNING',
|
|
290
|
+
/** Result derived from a stale cache entry. */
|
|
291
|
+
STALE_CAPABILITIES: 'STALE_CAPABILITIES',
|
|
292
|
+
|
|
293
|
+
// ---- M1 ----------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
/** A write succeeded but changed nothing (the same intent was already applied). */
|
|
296
|
+
SCENE_PATCH_NO_CHANGE: 'SCENE_PATCH_NO_CHANGE',
|
|
297
|
+
/** A render profile was downgraded to a reachable engine. */
|
|
298
|
+
RENDER_ENGINE_DOWNGRADED: 'RENDER_ENGINE_DOWNGRADED',
|
|
299
|
+
/** Preview rendered at a lower sample count than the profile asked for. */
|
|
300
|
+
RENDER_SAMPLES_REDUCED: 'RENDER_SAMPLES_REDUCED',
|
|
301
|
+
/** The compiler made a decision the author did not spell out (see notices). */
|
|
302
|
+
SCENE_COMPILER_DECISION: 'SCENE_COMPILER_DECISION',
|
|
303
|
+
/** A referenced asset is declared but its file has not been ingested yet. */
|
|
304
|
+
SCENE_ASSET_NOT_INGESTED: 'SCENE_ASSET_NOT_INGESTED',
|
|
305
|
+
/** An animation track targets a property whose keyframes were clamped or dropped. */
|
|
306
|
+
SCENE_ANIMATION_KEYFRAMES_ADJUSTED: 'SCENE_ANIMATION_KEYFRAMES_ADJUSTED',
|
|
307
|
+
|
|
308
|
+
// ---- M3 ----------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* A render could not be registered as a DSH background job, so it will not
|
|
312
|
+
* appear in the harness job list. The render itself is unaffected and its
|
|
313
|
+
* durable record is still authoritative — but the caller must be able to see
|
|
314
|
+
* why the job it asked for is not where it expected it.
|
|
315
|
+
*/
|
|
316
|
+
JOB_PROJECTION_UNAVAILABLE: 'JOB_PROJECTION_UNAVAILABLE',
|
|
317
|
+
/** A delivery had to be verified against a claim the runtime could not re-derive. */
|
|
318
|
+
DELIVERY_CLAIM_UNAVAILABLE: 'DELIVERY_CLAIM_UNAVAILABLE',
|
|
319
|
+
/**
|
|
320
|
+
* An attempt at this job was cut off mid-line in its event journal, so the LAST event the renderer
|
|
321
|
+
* sent never arrived.
|
|
322
|
+
*
|
|
323
|
+
* It is not an error and not a reason to distrust any number in the record: progress and the resumed
|
|
324
|
+
* frame set are read from the frame FILES, byte by byte (`frame-ledger.js`), and the journal was
|
|
325
|
+
* never the authority for either. What it means is that the journal is not a complete account of
|
|
326
|
+
* what the renderer did — which is the whole reason the ledger does not count from it.
|
|
327
|
+
*
|
|
328
|
+
* Recorded on the job because the alternative is silence: the same fact used to be only a line in
|
|
329
|
+
* the harness job output, which is drained by whoever reads it first and is gone after a restart.
|
|
330
|
+
*/
|
|
331
|
+
JOURNAL_INCOMPLETE: 'JOURNAL_INCOMPLETE',
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* The host API version the packages in this build implement.
|
|
336
|
+
*
|
|
337
|
+
* WHY A NUMBER AND NOT A METHOD PROBE
|
|
338
|
+
* -----------------------------------
|
|
339
|
+
* A tool plane newer than the host plane is a REAL deployment state, not a
|
|
340
|
+
* hypothetical: a Cordis service keeps the code it was constructed from, and Node's
|
|
341
|
+
* ESM module cache is process-level, so replacing the packages on disk does not
|
|
342
|
+
* replace the running `blenderStudio`. MEASURED while M3 was written — the `dsh web`
|
|
343
|
+
* process then running had started five hours before the M3 commit.
|
|
344
|
+
*
|
|
345
|
+
* Probing for methods catches that state for `resumeRenderJob`, `listJobs` and
|
|
346
|
+
* `reconcileRenderJobs` (they are simply absent), and MISSES it for
|
|
347
|
+
* `startFinalRender` and `exportProject`, because M1 implemented those as stubs and
|
|
348
|
+
* so `typeof` is `'function'` on both the old and the new host. The failure mode of
|
|
349
|
+
* the probe is therefore the worst kind: it guards four entry points and silently
|
|
350
|
+
* lets the other two through. A number answers the question the probe was trying to
|
|
351
|
+
* answer.
|
|
352
|
+
*
|
|
353
|
+
* Bump this when the tool plane starts depending on a host method that did not exist
|
|
354
|
+
* before. 3 is M3 (the persistent render job). 4 is M4, which added the UI plane's
|
|
355
|
+
* read/aggregate methods — the browser half faces the same half-upgraded deployment
|
|
356
|
+
* the tools do, and it has to say so instead of painting a blank panel over a
|
|
357
|
+
* `TypeError` (D59).
|
|
358
|
+
*/
|
|
359
|
+
export const HOST_API_VERSION = 4
|
|
360
|
+
|
|
361
|
+
/** Formats the product intends to support (SPEC §2.2). Used to emit warnings. */
|
|
362
|
+
export const EXPECTED_IMPORT_FORMATS = Object.freeze(['gltf', 'fbx', 'obj', 'usd'])
|
|
363
|
+
/** Formats the product intends to be able to export. */
|
|
364
|
+
export const EXPECTED_EXPORT_FORMATS = Object.freeze(['gltf', 'fbx', 'usd'])
|
|
365
|
+
|
|
366
|
+
/** Candidate render engines probed behaviorally. Never assume one exists (D1). */
|
|
367
|
+
export const CANDIDATE_RENDER_ENGINES = Object.freeze([
|
|
368
|
+
'BLENDER_EEVEE',
|
|
369
|
+
'CYCLES',
|
|
370
|
+
'BLENDER_WORKBENCH',
|
|
371
|
+
])
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* A typed, serializable DeepBlend failure.
|
|
375
|
+
*
|
|
376
|
+
* Carries a stable `code` from {@link BlenderErrorCode} so the model, the UI and
|
|
377
|
+
* the audit log can branch on it without parsing prose.
|
|
378
|
+
*/
|
|
379
|
+
export class BlenderError extends Error {
|
|
380
|
+
/**
|
|
381
|
+
* @param {string} code - a {@link BlenderErrorCode} value.
|
|
382
|
+
* @param {string} message - human-readable, safe to show to the model.
|
|
383
|
+
* @param {{ detail?: unknown, cause?: unknown }} [options]
|
|
384
|
+
*/
|
|
385
|
+
constructor(code, message, options = {}) {
|
|
386
|
+
super(message, options.cause !== undefined ? { cause: options.cause } : undefined)
|
|
387
|
+
this.name = 'BlenderError'
|
|
388
|
+
this.code = code
|
|
389
|
+
if (options.detail !== undefined) this.detail = options.detail
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** Canonical JSON form for tool results and API responses. */
|
|
393
|
+
toJSON() {
|
|
394
|
+
return {
|
|
395
|
+
code: this.code,
|
|
396
|
+
message: this.message,
|
|
397
|
+
...this.detail !== undefined ? { detail: this.detail } : {},
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Narrowing helper for callers that receive unknown thrown values. */
|
|
403
|
+
export function isBlenderError(value) {
|
|
404
|
+
return value instanceof BlenderError
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Build a warning entry. Warnings never throw; they degrade a successful result.
|
|
409
|
+
* @param {string} code - a {@link BlenderWarningCode} value.
|
|
410
|
+
* @param {string} message - human-readable explanation.
|
|
411
|
+
* @param {Record<string, unknown>} [detail]
|
|
412
|
+
*/
|
|
413
|
+
export function warning(code, message, detail) {
|
|
414
|
+
return { code, message, ...detail !== undefined ? { detail } : {} }
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* @typedef {object} BlenderExecutableReport
|
|
419
|
+
* @property {string} requested - exactly what the operator configured.
|
|
420
|
+
* @property {string|null} resolved - canonical absolute path actually used, or null.
|
|
421
|
+
* @property {boolean} found - whether a usable executable was resolved.
|
|
422
|
+
* @property {string|null} [advice] - what to do about it when `found` is false: the sentence the
|
|
423
|
+
* settings card and the capability text both show, composed once in the provider so the two cannot
|
|
424
|
+
* disagree. Null when there is nothing to advise.
|
|
425
|
+
*/
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* @typedef {object} BlenderEngineProbe
|
|
429
|
+
* @property {boolean} assignable - whether assigning the identifier actually took effect.
|
|
430
|
+
* @property {string|null} readback - what `scene.render.engine` read back as.
|
|
431
|
+
* @property {string|null} error - exception text when the probe threw.
|
|
432
|
+
*/
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* One GPU compute backend's support status.
|
|
436
|
+
*
|
|
437
|
+
* An unsupported backend *raises* on assignment rather than returning an empty
|
|
438
|
+
* device list (`TypeError: enum "OPTIX" not found in ('NONE', 'METAL')`), so
|
|
439
|
+
* `supported: false` is a normal, expected outcome — never a probe failure.
|
|
440
|
+
*
|
|
441
|
+
* @typedef {object} BlenderGpuBackend
|
|
442
|
+
* @property {boolean} supported
|
|
443
|
+
* @property {string[]} gpuDevices
|
|
444
|
+
* @property {string[]} cpuDevices
|
|
445
|
+
* @property {string|null} error
|
|
446
|
+
*/
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* The GPU report produced by `bootstrap.py`, normalised by the provider.
|
|
450
|
+
*
|
|
451
|
+
* @typedef {object} BlenderGpuReport
|
|
452
|
+
* @property {string[]} availableBackends - backends that reported at least one GPU device.
|
|
453
|
+
* @property {string|null} preferredBackend
|
|
454
|
+
* @property {string[]} gpuDeviceNames - flattened GPU device names across available backends.
|
|
455
|
+
* @property {string[]} cpuDeviceNames
|
|
456
|
+
* @property {Record<string, BlenderGpuBackend>} backendSupport
|
|
457
|
+
*/
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* @typedef {object} BlenderRenderSmokeTest
|
|
461
|
+
* @property {boolean} attempted
|
|
462
|
+
* @property {string|null} engine
|
|
463
|
+
* @property {boolean} ok
|
|
464
|
+
* @property {number} bytes
|
|
465
|
+
* @property {string|null} error
|
|
466
|
+
*/
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* The behavioral capability report produced by `bootstrap.py` (D1).
|
|
470
|
+
*
|
|
471
|
+
* `renderEngines` is deliberately a *behavioral* probe result rather than the
|
|
472
|
+
* static `engine` enum: on Blender 5.2.1 the enum reports only `BLENDER_EEVEE`
|
|
473
|
+
* even though assigning `CYCLES` succeeds. `renderEngineEnumItems` is retained
|
|
474
|
+
* as an informational diagnostic and must never be used to gate availability.
|
|
475
|
+
*
|
|
476
|
+
* @typedef {object} BlenderCapabilities
|
|
477
|
+
* @property {string} protocolVersion
|
|
478
|
+
* @property {boolean} installed
|
|
479
|
+
* @property {BlenderExecutableReport} executable
|
|
480
|
+
* @property {string|null} blenderVersion
|
|
481
|
+
* @property {number[]|null} blenderVersionTuple
|
|
482
|
+
* @property {string|null} pythonVersion
|
|
483
|
+
* @property {string|null} buildHash
|
|
484
|
+
* @property {string|null} binaryPath
|
|
485
|
+
* @property {Record<string, BlenderEngineProbe>} renderEngines
|
|
486
|
+
* @property {string|null} bestAvailableEngine - preferred engine per the probe's own ordering.
|
|
487
|
+
* @property {string[]} renderEngineEnumItems - diagnostic only (D1).
|
|
488
|
+
* @property {BlenderGpuReport} gpu
|
|
489
|
+
* @property {boolean} gpuAvailable
|
|
490
|
+
* @property {string[]} exportFormats
|
|
491
|
+
* @property {string[]} importFormats
|
|
492
|
+
* @property {string[]} unavailableFormats - attribute-present but unregistered operators.
|
|
493
|
+
* @property {BlenderRenderSmokeTest|null} renderSmokeTest
|
|
494
|
+
* @property {BlenderRenderSmokeTest|null} cyclesSmokeTest
|
|
495
|
+
* @property {boolean} textBlockApi
|
|
496
|
+
* @property {boolean} frameApi
|
|
497
|
+
* @property {string|null} hostPlatform
|
|
498
|
+
* @property {{ code: string, message: string, detail?: unknown }[]} warnings
|
|
499
|
+
* @property {number} probedAt - epoch ms.
|
|
500
|
+
* @property {number} durationMs
|
|
501
|
+
* @property {string|null} commandLine - for audit; never contains secrets.
|
|
502
|
+
*/
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* The Blender execution seam (SPEC §7.1).
|
|
506
|
+
*
|
|
507
|
+
* Implementations own process launch, protocol framing and result validation.
|
|
508
|
+
* They hold NO business state machine — orchestration belongs to
|
|
509
|
+
* `BlenderOrchestrator` and persistence to the project/revision stores.
|
|
510
|
+
*
|
|
511
|
+
* M0 implements `getCapabilities` and the shared `runBootstrap` transport.
|
|
512
|
+
* The remaining methods are declared here as the frozen target shape and are
|
|
513
|
+
* implemented in M1+. Calling an unimplemented one throws
|
|
514
|
+
* `BlenderErrorCode.UNSUPPORTED_ACTION`, never silently succeeds.
|
|
515
|
+
*
|
|
516
|
+
* @typedef {object} BlenderRuntime
|
|
517
|
+
* @property {(signal?: AbortSignal) => Promise<BlenderCapabilities>} getCapabilities
|
|
518
|
+
* @property {(request: BlenderBootstrapRequest, options?: { signal?: AbortSignal }) => Promise<BlenderBootstrapEnvelope>} runBootstrap
|
|
519
|
+
* @property {() => void} dispose
|
|
520
|
+
*/
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* @typedef {object} BlenderBootstrapRequest
|
|
524
|
+
* @property {string} action - bootstrap action name, e.g. `get_capabilities`.
|
|
525
|
+
* @property {Record<string, unknown>} [payload] - action-specific fields.
|
|
526
|
+
* @property {string} [jobId] - correlation id for logs and the result envelope.
|
|
527
|
+
*/
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* The envelope `bootstrap.py` writes to its result file.
|
|
531
|
+
*
|
|
532
|
+
* @typedef {object} BlenderBootstrapEnvelope
|
|
533
|
+
* @property {'success'|'error'} status
|
|
534
|
+
* @property {string} protocolVersion
|
|
535
|
+
* @property {string|null} jobId
|
|
536
|
+
* @property {BlenderCapabilities|null} capabilities
|
|
537
|
+
* @property {{ code: string, message: string }|null} error
|
|
538
|
+
* @property {string[]} warnings
|
|
539
|
+
*/
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* The model- and UI-facing business facade (SPEC §7.2).
|
|
543
|
+
*
|
|
544
|
+
* M0 implements `getCapabilities` only. Everything else is the frozen M1+ target
|
|
545
|
+
* and throws `UNSUPPORTED_ACTION` until implemented, so the model can never
|
|
546
|
+
* believe a not-yet-built capability worked.
|
|
547
|
+
*
|
|
548
|
+
* @typedef {object} BlenderStudio
|
|
549
|
+
* @property {(request?: { refresh?: boolean }) => Promise<BlenderCapabilities>} getCapabilities
|
|
550
|
+
*/
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Project a raw capabilities object into the canonical, stable-order JSON that
|
|
554
|
+
* the tool returns and the UI renders.
|
|
555
|
+
*
|
|
556
|
+
* Key order is fixed so transcripts diff cleanly and tests can compare output
|
|
557
|
+
* (SPEC §19.5 golden scenes rely on deterministic serialization).
|
|
558
|
+
*
|
|
559
|
+
* @param {BlenderCapabilities} capabilities
|
|
560
|
+
* @returns {Record<string, unknown>}
|
|
561
|
+
*/
|
|
562
|
+
export function toCanonicalCapabilities(capabilities) {
|
|
563
|
+
return {
|
|
564
|
+
protocolVersion: capabilities.protocolVersion,
|
|
565
|
+
installed: capabilities.installed,
|
|
566
|
+
executable: {
|
|
567
|
+
requested: capabilities.executable.requested,
|
|
568
|
+
resolved: capabilities.executable.resolved,
|
|
569
|
+
found: capabilities.executable.found,
|
|
570
|
+
// What to DO when it was not found — composed once by the provider, read by the settings card
|
|
571
|
+
// (a human) and by the capability text (a model). A canonical projection is where a field is
|
|
572
|
+
// DECLARED, which is exactly why the first version of this change worked in the provider and
|
|
573
|
+
// vanished here: the value existed and the contract had no room for it.
|
|
574
|
+
//
|
|
575
|
+
// ABSENT rather than null when there is nothing to advise: a healthy install has no "what to do
|
|
576
|
+
// about it", and the fixture round-trip in `contract/contracts.test.mjs` pins the canonical shape
|
|
577
|
+
// exactly — a null field would change the shape of every healthy payload to carry a non-fact.
|
|
578
|
+
...(typeof capabilities.executable.advice === 'string' && capabilities.executable.advice.length > 0
|
|
579
|
+
? { advice: capabilities.executable.advice }
|
|
580
|
+
: {}),
|
|
581
|
+
},
|
|
582
|
+
version: capabilities.blenderVersion,
|
|
583
|
+
versionTuple: capabilities.blenderVersionTuple,
|
|
584
|
+
pythonVersion: capabilities.pythonVersion,
|
|
585
|
+
buildHash: capabilities.buildHash,
|
|
586
|
+
binaryPath: capabilities.binaryPath,
|
|
587
|
+
// Behavioral availability — the only field a caller may branch on (D1).
|
|
588
|
+
engines: Object.fromEntries(
|
|
589
|
+
Object.entries(capabilities.renderEngines).map(([id, probe]) => [
|
|
590
|
+
id,
|
|
591
|
+
{ available: probe.assignable, readback: probe.readback, error: probe.error },
|
|
592
|
+
]),
|
|
593
|
+
),
|
|
594
|
+
bestAvailableEngine: capabilities.bestAvailableEngine,
|
|
595
|
+
// Diagnostic only. Present so operators can see the enum lie for themselves.
|
|
596
|
+
engineEnumItems: capabilities.renderEngineEnumItems,
|
|
597
|
+
gpu: {
|
|
598
|
+
available: capabilities.gpuAvailable,
|
|
599
|
+
backends: capabilities.gpu.availableBackends,
|
|
600
|
+
preferredBackend: capabilities.gpu.preferredBackend,
|
|
601
|
+
devices: capabilities.gpu.gpuDeviceNames,
|
|
602
|
+
cpuDevices: capabilities.gpu.cpuDeviceNames,
|
|
603
|
+
backendSupport: capabilities.gpu.backendSupport,
|
|
604
|
+
},
|
|
605
|
+
formats: {
|
|
606
|
+
import: capabilities.importFormats,
|
|
607
|
+
export: capabilities.exportFormats,
|
|
608
|
+
// Attribute-present but unregistered operators: the trap that makes
|
|
609
|
+
// `hasattr(bpy.ops.export_scene, 'obj')` an unsafe availability test.
|
|
610
|
+
unavailable: capabilities.unavailableFormats,
|
|
611
|
+
},
|
|
612
|
+
renderSmokeTest: capabilities.renderSmokeTest,
|
|
613
|
+
cyclesSmokeTest: capabilities.cyclesSmokeTest,
|
|
614
|
+
api: {
|
|
615
|
+
textBlocks: capabilities.textBlockApi,
|
|
616
|
+
frameRange: capabilities.frameApi,
|
|
617
|
+
},
|
|
618
|
+
hostPlatform: capabilities.hostPlatform,
|
|
619
|
+
warnings: capabilities.warnings,
|
|
620
|
+
probedAt: capabilities.probedAt,
|
|
621
|
+
durationMs: capabilities.durationMs,
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ---------------------------------------------------------------------------
|
|
626
|
+
// M1 surface
|
|
627
|
+
//
|
|
628
|
+
// The M0 section above is frozen. Everything this milestone added lives in its
|
|
629
|
+
// own module and is re-exported here, so every consumer keeps importing exactly
|
|
630
|
+
// one package name (SPEC §5.1: contracts is the shared vocabulary) and the
|
|
631
|
+
// dependency graph stays acyclic.
|
|
632
|
+
// ---------------------------------------------------------------------------
|
|
633
|
+
|
|
634
|
+
export {
|
|
635
|
+
canonicalStringify,
|
|
636
|
+
canonicalPretty,
|
|
637
|
+
sortValue,
|
|
638
|
+
sha256,
|
|
639
|
+
sha256Canonical,
|
|
640
|
+
shortDigest,
|
|
641
|
+
} from './canonical.js'
|
|
642
|
+
|
|
643
|
+
export {
|
|
644
|
+
SchemaDefinitionError,
|
|
645
|
+
compileSchema,
|
|
646
|
+
formatIssues,
|
|
647
|
+
} from './json-schema.js'
|
|
648
|
+
|
|
649
|
+
export {
|
|
650
|
+
DEFAULT_WORLD,
|
|
651
|
+
ANIMATION_PROPERTIES_BY_KIND,
|
|
652
|
+
TRANSFORM_ANIMATION_PROPERTIES,
|
|
653
|
+
MATERIAL_ANIMATION_PROPERTIES,
|
|
654
|
+
collectionForKind,
|
|
655
|
+
collectionNameForKind,
|
|
656
|
+
SCENE_ENGINES,
|
|
657
|
+
BLENDER_ENGINE_BY_KEY,
|
|
658
|
+
IMPORT_OPERATOR_BY_ASSET_TYPE,
|
|
659
|
+
validateSceneSpec,
|
|
660
|
+
compileSceneSpec,
|
|
661
|
+
entityBoundingRadius,
|
|
662
|
+
sceneProjection,
|
|
663
|
+
sceneSpecDigest,
|
|
664
|
+
specHash,
|
|
665
|
+
summarizeSceneSpec,
|
|
666
|
+
sceneSpecCanonicalText,
|
|
667
|
+
} from './scene-spec.js'
|
|
668
|
+
|
|
669
|
+
export { ASSET_HEAD_BYTES, assetContentVerdict, describeAssetContent } from './asset-content.js'
|
|
670
|
+
export { redactUrl } from './redact.js'
|
|
671
|
+
export { assertKnownConfigKeys, declaredConfigKeys, describeUnknownConfigKeys } from './config-surface.js'
|
|
672
|
+
|
|
673
|
+
export {
|
|
674
|
+
CAMERA_UPDATE_FIELDS,
|
|
675
|
+
SCENE_OPERATION_NAMES,
|
|
676
|
+
validateScenePatch,
|
|
677
|
+
applyPatchToSpec,
|
|
678
|
+
buildOperationManifest,
|
|
679
|
+
} from './scene-patch.js'
|
|
680
|
+
|
|
681
|
+
export {
|
|
682
|
+
toCanonicalProjectSummary,
|
|
683
|
+
toCanonicalRevisionSummary,
|
|
684
|
+
toCanonicalPreviewArtifact,
|
|
685
|
+
toCanonicalQAReport,
|
|
686
|
+
toCanonicalJobRecord,
|
|
687
|
+
toCanonicalFailure,
|
|
688
|
+
} from './projections.js'
|
|
689
|
+
|
|
690
|
+
// ---------------------------------------------------------------------------
|
|
691
|
+
// M2 surface
|
|
692
|
+
//
|
|
693
|
+
// The visual loop's vocabulary: how a render is measured into a score, what a
|
|
694
|
+
// finding is, and how repeated findings are detected. Kept in its own module for
|
|
695
|
+
// the same reason as the M1 block above.
|
|
696
|
+
// ---------------------------------------------------------------------------
|
|
697
|
+
|
|
698
|
+
export {
|
|
699
|
+
VISUAL_REVIEW_VERSION,
|
|
700
|
+
VISUAL_ISSUE_CATEGORIES,
|
|
701
|
+
VISUAL_SEVERITIES,
|
|
702
|
+
VISUAL_PASS_SCORE,
|
|
703
|
+
SUBJECT_PART_TAG,
|
|
704
|
+
isSubjectPart,
|
|
705
|
+
LUMINANCE_BUCKETS,
|
|
706
|
+
COVERAGE_BUCKETS,
|
|
707
|
+
CENTERING_BUCKETS,
|
|
708
|
+
quantise,
|
|
709
|
+
issueFingerprint,
|
|
710
|
+
scoreView,
|
|
711
|
+
scoreReview,
|
|
712
|
+
validateFindings,
|
|
713
|
+
seedFingerprintCounters,
|
|
714
|
+
shouldHandOver,
|
|
715
|
+
updateFingerprintCounters,
|
|
716
|
+
} from './visual-issue.js'
|
|
717
|
+
|
|
718
|
+
export {
|
|
719
|
+
decodePng,
|
|
720
|
+
encodePng,
|
|
721
|
+
createImage,
|
|
722
|
+
fillRect,
|
|
723
|
+
blendInto,
|
|
724
|
+
} from './png.js'
|
|
725
|
+
|
|
726
|
+
export {
|
|
727
|
+
GLYPH_WIDTH,
|
|
728
|
+
GLYPH_HEIGHT,
|
|
729
|
+
textWidth,
|
|
730
|
+
drawText,
|
|
731
|
+
} from './bitmap-font.js'
|
|
732
|
+
|
|
733
|
+
export {
|
|
734
|
+
CAPTION_BAND_COLOR,
|
|
735
|
+
composeContactSheet,
|
|
736
|
+
viewCaption,
|
|
737
|
+
} from './contact-sheet.js'
|
|
738
|
+
|
|
739
|
+
export {
|
|
740
|
+
runVisualLoop,
|
|
741
|
+
} from './visual-loop.js'
|
|
742
|
+
|
|
743
|
+
// ---------------------------------------------------------------------------
|
|
744
|
+
// M3 surface
|
|
745
|
+
//
|
|
746
|
+
// The persistent render job: the durable record's vocabulary, the frame ledger
|
|
747
|
+
// that decides what still has to be rendered, and the delivery manifest's own
|
|
748
|
+
// completeness and video-property checks. Pure, like the M1/M2 blocks above, so
|
|
749
|
+
// the acceptance suite can exercise a restart's decision rules with no Blender
|
|
750
|
+
// and no ffmpeg present.
|
|
751
|
+
// ---------------------------------------------------------------------------
|
|
752
|
+
|
|
753
|
+
export {
|
|
754
|
+
RENDER_JOB_VERSION,
|
|
755
|
+
FRAME_PLAN_VERSION,
|
|
756
|
+
PROCESS_IDENTITY_VERSION,
|
|
757
|
+
DELIVERY_MANIFEST_VERSION,
|
|
758
|
+
RENDER_JOB_STATUSES,
|
|
759
|
+
RENDER_JOB_TERMINAL_STATUSES,
|
|
760
|
+
RENDER_JOB_TYPES,
|
|
761
|
+
FRAME_FILE_PREFIX,
|
|
762
|
+
FRAME_FILE_PADDING,
|
|
763
|
+
MIN_FRAME_BYTES,
|
|
764
|
+
isTerminalRenderJobStatus,
|
|
765
|
+
canTransitionRenderJob,
|
|
766
|
+
checkTransition,
|
|
767
|
+
frameFileName,
|
|
768
|
+
frameNumbers,
|
|
769
|
+
inspectFrameSample,
|
|
770
|
+
inspectFrameBytes,
|
|
771
|
+
resolveFrameLedger,
|
|
772
|
+
renderProgressPercent,
|
|
773
|
+
estimateRemaining,
|
|
774
|
+
verifyVideoProperties,
|
|
775
|
+
deliveryCompleteness,
|
|
776
|
+
describeRenderJob,
|
|
777
|
+
} from './render-job.js'
|
|
778
|
+
|
|
779
|
+
export {
|
|
780
|
+
VIEW_ROLES,
|
|
781
|
+
buildViewPlan,
|
|
782
|
+
trackedObjects,
|
|
783
|
+
subjectParts,
|
|
784
|
+
resolveSubject,
|
|
785
|
+
resolveSubjectId,
|
|
786
|
+
buildVisualReview,
|
|
787
|
+
} from './visual-composition.js'
|
|
788
|
+
|
|
789
|
+
// The workbench UI's contract (M4): the HTTP route table, the panel vocabulary
|
|
790
|
+
// and every view model the browser renders. Pure data and pure functions, so the
|
|
791
|
+
// UI's own rules are covered by contract tests rather than by clicking.
|
|
792
|
+
export {
|
|
793
|
+
UI_ROUTE_PREFIX,
|
|
794
|
+
UI_REST_MARKER,
|
|
795
|
+
UI_ROUTES,
|
|
796
|
+
UI_PANEL_VIEWS,
|
|
797
|
+
PREVIEW_SHEET_SLOTS,
|
|
798
|
+
UI_PANEL_ID,
|
|
799
|
+
UI_PANEL_LABEL,
|
|
800
|
+
UI_TOOL_CARD_KEYS,
|
|
801
|
+
matchUiRoute,
|
|
802
|
+
writeRouteIds,
|
|
803
|
+
buildRevisionDiff,
|
|
804
|
+
buildSceneTree,
|
|
805
|
+
buildQaView,
|
|
806
|
+
buildApprovalView,
|
|
807
|
+
buildJobView,
|
|
808
|
+
buildProjectView,
|
|
809
|
+
buildSettingsCard,
|
|
810
|
+
describeJobForHuman,
|
|
811
|
+
formatDuration,
|
|
812
|
+
parseToolCallTarget,
|
|
813
|
+
} from './ui-api.js'
|
|
814
|
+
|
|
815
|
+
// Where a deployment keeps its state and finds its managed Blender (M5).
|
|
816
|
+
// Shared by the plugin schemas and by the repository's own tools, so that "the
|
|
817
|
+
// project create-demo-project.mjs writes is the one a restarted profile finds"
|
|
818
|
+
// is true by construction rather than by a comment claiming it (see the module).
|
|
819
|
+
export {
|
|
820
|
+
DEEPBLEND_STATE_DIRECTORY,
|
|
821
|
+
MANAGED_TOOLS_DIRECTORY,
|
|
822
|
+
MANAGED_BLENDER_RELATIVE_PATHS,
|
|
823
|
+
expandHome,
|
|
824
|
+
resolveDshHome,
|
|
825
|
+
resolveDefaultWorkspaceRoot,
|
|
826
|
+
resolveWorkspaceRoot,
|
|
827
|
+
resolveProjectsRoot,
|
|
828
|
+
managedBlenderCandidates,
|
|
829
|
+
} from './deployment-paths.js'
|