@hraness/slopcamera 3.2.8 → 3.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.
Files changed (150) hide show
  1. package/README.md +29 -9
  2. package/apps/desktop/application/context.ts +6 -0
  3. package/apps/desktop/application/default-registry.ts +26 -0
  4. package/apps/desktop/application/operation.ts +14 -1
  5. package/apps/desktop/application/operations/index.ts +4 -0
  6. package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
  7. package/apps/desktop/application/operations/spatial-direction.ts +222 -0
  8. package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
  9. package/apps/desktop/application/operations/spatial-review.ts +194 -0
  10. package/apps/desktop/application/operations/spatial-scene.ts +25 -1
  11. package/apps/desktop/application/registry.ts +2 -2
  12. package/apps/desktop/application/spatial-asset-admission.ts +201 -0
  13. package/apps/desktop/application/spatial-assets.ts +160 -31
  14. package/apps/desktop/application/spatial-render.ts +217 -20
  15. package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
  16. package/apps/desktop/application/spatial-review.ts +361 -0
  17. package/apps/desktop/application/spatial-spz.ts +48 -3
  18. package/apps/desktop/application/spatial-world-import.ts +26 -5
  19. package/apps/desktop/application/spatial-world-metadata.ts +105 -0
  20. package/apps/desktop/cli/args.ts +514 -11
  21. package/apps/desktop/cli/capability-manifest.ts +374 -0
  22. package/apps/desktop/cli/cinema-renderer.ts +608 -0
  23. package/apps/desktop/cli/command-host-resources.ts +29 -1
  24. package/apps/desktop/cli/commands.ts +1188 -4
  25. package/apps/desktop/cli/gateway-review-provider.ts +334 -0
  26. package/apps/desktop/cli/help.ts +172 -6
  27. package/apps/desktop/cli/main.ts +3 -3
  28. package/apps/desktop/cli/portable-surface.ts +6 -1
  29. package/apps/desktop/cli/spatial-asset-service.ts +124 -0
  30. package/apps/desktop/cli/spatial-design-service.ts +237 -0
  31. package/apps/desktop/cli/spatial-generate-service.ts +370 -0
  32. package/apps/desktop/cli/spatial-scene-service.ts +268 -4
  33. package/apps/desktop/cli/workflow-code.ts +1 -1
  34. package/apps/desktop/code/semantic-builder.ts +24 -0
  35. package/apps/desktop/contracts/cinema.ts +736 -0
  36. package/apps/desktop/contracts/index.ts +1 -0
  37. package/apps/desktop/contracts/spatial-asset.ts +16 -0
  38. package/apps/desktop/contracts/spatial-world.ts +26 -3
  39. package/apps/desktop/core/cinema-plan.ts +1320 -0
  40. package/apps/desktop/core/index.ts +1 -0
  41. package/apps/desktop/core/storage.ts +75 -0
  42. package/apps/desktop/dist/cli/main.js +725 -273
  43. package/apps/desktop/html-overlay/spatial.ts +619 -57
  44. package/apps/desktop/workflows/cinematic-world.ts +127 -0
  45. package/apps/desktop/workflows/index.ts +6 -0
  46. package/dist/cli.js +25 -429
  47. package/dist/code/advanced.js +1 -269
  48. package/dist/code/index.js +16 -2239
  49. package/dist/generate.js +1 -23
  50. package/dist/host-resources.js +1 -25
  51. package/dist/index-0bbesn4m.js +35 -0
  52. package/dist/index-296qhpf2.js +3 -0
  53. package/dist/index-3cxf2c58.js +3 -0
  54. package/dist/index-77fjfg9f.js +3 -0
  55. package/dist/index-80a36bc0.js +3 -0
  56. package/dist/index-a3vvs3rf.js +3 -0
  57. package/dist/index-b3geqczn.js +5 -0
  58. package/dist/index-cgpkyjqa.js +3 -0
  59. package/dist/index-f0ptv8dm.js +7 -0
  60. package/dist/index-grk47tgs.js +3 -0
  61. package/dist/index-qry58nj2.js +10 -0
  62. package/dist/index.js +1 -226
  63. package/dist/operations.js +1 -28
  64. package/dist/skill-install-gx7afxsd.js +2 -0
  65. package/dist/vectorize/worker.js +1 -141
  66. package/dist/workflow.js +1 -15
  67. package/docs/README.md +5 -1
  68. package/package.json +7 -5
  69. package/skills/slopcamera/SKILL.md +6 -1
  70. package/skills/slopcamera/references/directed-scenes.md +22 -2
  71. package/skills/slopcamera/references/gateway-media.md +15 -1
  72. package/skills/slopcamera/references/image-galleries.md +166 -0
  73. package/skills/slopcamera/references/install.md +1 -1
  74. package/skills/slopcamera/references/parametric-design.md +14 -0
  75. package/skills/slopcamera/references/scene-building.md +32 -0
  76. package/skills/slopcamera/references/social-collage-banners.md +204 -0
  77. package/skills/slopcamera/references/workflows-sdk.md +3 -3
  78. package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
  79. package/src/capability-manifest.ts +361 -0
  80. package/src/cli.ts +222 -4
  81. package/src/code/canonical-json.ts +9 -0
  82. package/src/generate.ts +2 -2
  83. package/src/icon.ts +1111 -0
  84. package/src/image-gallery.ts +1049 -0
  85. package/src/index.ts +11 -0
  86. package/src/mcp/boundary.ts +11 -11
  87. package/src/mcp/index.ts +3 -0
  88. package/src/mcp/server.ts +2 -1
  89. package/src/mcp/tools.ts +1642 -130
  90. package/src/mcp/types.ts +19 -0
  91. package/src/operations.ts +412 -0
  92. package/src/portable-capability-manifest.ts +67 -0
  93. package/src/scene-gallery.ts +184 -0
  94. package/src/spatial-scene/asset-admission.ts +183 -0
  95. package/src/spatial-scene/audit-rendered.ts +692 -0
  96. package/src/spatial-scene/audit.ts +617 -0
  97. package/src/spatial-scene/behavior-audit.ts +252 -0
  98. package/src/spatial-scene/behavior-bake.ts +233 -0
  99. package/src/spatial-scene/behavior-fns.ts +585 -0
  100. package/src/spatial-scene/behavior-gallery.ts +109 -0
  101. package/src/spatial-scene/behavior-stdlib.ts +323 -0
  102. package/src/spatial-scene/behavior-trace.ts +325 -0
  103. package/src/spatial-scene/behavior.ts +618 -0
  104. package/src/spatial-scene/build.ts +632 -0
  105. package/src/spatial-scene/camera-rig.ts +125 -0
  106. package/src/spatial-scene/camera-track.ts +4 -2
  107. package/src/spatial-scene/character.ts +124 -0
  108. package/src/spatial-scene/contracts.ts +75 -7
  109. package/src/spatial-scene/design-templates.ts +351 -0
  110. package/src/spatial-scene/design.ts +327 -0
  111. package/src/spatial-scene/direction-compile.ts +584 -0
  112. package/src/spatial-scene/direction.ts +122 -0
  113. package/src/spatial-scene/effects.ts +149 -0
  114. package/src/spatial-scene/evaluate.ts +109 -29
  115. package/src/spatial-scene/gallery.ts +277 -0
  116. package/src/spatial-scene/generate.ts +239 -0
  117. package/src/spatial-scene/geometry-native.ts +165 -0
  118. package/src/spatial-scene/geometry.ts +1216 -0
  119. package/src/spatial-scene/gltf.ts +600 -77
  120. package/src/spatial-scene/identity.ts +55 -2
  121. package/src/spatial-scene/index.ts +36 -0
  122. package/src/spatial-scene/inspect.ts +35 -6
  123. package/src/spatial-scene/material-lighting.ts +609 -0
  124. package/src/spatial-scene/math.ts +21 -4
  125. package/src/spatial-scene/motion-evidence.ts +88 -0
  126. package/src/spatial-scene/parametric.ts +765 -0
  127. package/src/spatial-scene/particle-preparation.ts +183 -0
  128. package/src/spatial-scene/particle.ts +195 -0
  129. package/src/spatial-scene/patch.ts +109 -15
  130. package/src/spatial-scene/performance.ts +1613 -0
  131. package/src/spatial-scene/probe.ts +137 -0
  132. package/src/spatial-scene/recipe-pack.ts +86 -0
  133. package/src/spatial-scene/render-effects.ts +240 -0
  134. package/src/spatial-scene/review.ts +639 -0
  135. package/src/spatial-scene/simulation-bake.ts +206 -0
  136. package/src/spatial-scene/simulation.ts +187 -0
  137. package/src/spatial-scene/solve.ts +366 -0
  138. package/src/spatial-scene/temporal-audit.ts +354 -0
  139. package/src/version.ts +1 -1
  140. package/dist/index-42zsesc1.js +0 -2257
  141. package/dist/index-7131pg1c.js +0 -1820
  142. package/dist/index-7308egqr.js +0 -48
  143. package/dist/index-8txs6fkn.js +0 -712
  144. package/dist/index-fava6pge.js +0 -192
  145. package/dist/index-h1k0fnjq.js +0 -2363
  146. package/dist/index-r7gdhmsp.js +0 -424
  147. package/dist/index-sh6xbav6.js +0 -1219
  148. package/dist/index-z1w83f81.js +0 -4
  149. package/dist/index-zfnddgay.js +0 -2029
  150. package/dist/skill-install-dh0ntqba.js +0 -10
@@ -0,0 +1,354 @@
1
+ import { z } from "zod"
2
+
3
+ import { deepFreezeJson } from "../code/json-snapshot.js"
4
+ import {
5
+ EvaluatedSpatialSceneSchema,
6
+ SpatialCameraIdSchema,
7
+ SpatialDigestSchema,
8
+ SpatialEntityIdSchema,
9
+ SpatialTimeUsSchema,
10
+ type EvaluatedSpatialScene,
11
+ } from "./contracts.js"
12
+ import { parseSpatialValue, SpatialSceneError, spatialValueSha256 } from "./identity.js"
13
+
14
+ /**
15
+ * Temporal evidence audit for Phase 12.
16
+ *
17
+ * Consumes an ordered run of evaluated snapshots (derived state only — never
18
+ * renders) and reports exact sample/time evidence for flicker, transform
19
+ * discontinuity, planted-contact sliding, camera acceleration/jerk and angular
20
+ * velocity, exposure/focus jumps, and resource spikes. Findings reference the
21
+ * snapshot index and integer microsecond time at which the violation was
22
+ * measured so reviewers can re-evaluate the exact frame.
23
+ */
24
+
25
+ export const SPATIAL_TEMPORAL_AUDIT_LIMITS = Object.freeze({
26
+ samples: 256,
27
+ contacts: 64,
28
+ cutBoundaries: 64,
29
+ findings: 512,
30
+ })
31
+
32
+ export const SpatialTemporalContactSchema = z.strictObject({
33
+ entityId: SpatialEntityIdSchema,
34
+ groundY: z.number().finite().min(-1_000_000).max(1_000_000),
35
+ startUs: SpatialTimeUsSchema,
36
+ endUs: SpatialTimeUsSchema,
37
+ }).superRefine((contact, context) => {
38
+ if (contact.endUs <= contact.startUs) {
39
+ context.addIssue({ code: "custom", path: ["endUs"], message: "Contact windows must have positive duration." })
40
+ }
41
+ })
42
+ export type SpatialTemporalContact = Readonly<z.infer<typeof SpatialTemporalContactSchema>>
43
+
44
+ /** Companion file for ground-contact evidence supplied alongside a scene. */
45
+ export const SpatialTemporalContactsFileSchema = z.strictObject({
46
+ contacts: z.array(SpatialTemporalContactSchema).max(SPATIAL_TEMPORAL_AUDIT_LIMITS.contacts),
47
+ })
48
+ export type SpatialTemporalContactsFile = Readonly<z.infer<typeof SpatialTemporalContactsFileSchema>>
49
+
50
+ export const SpatialTemporalAuditOptionsSchema = z.strictObject({
51
+ contacts: z.array(SpatialTemporalContactSchema).max(SPATIAL_TEMPORAL_AUDIT_LIMITS.contacts).default([]),
52
+ cutBeforeUs: z.array(SpatialTimeUsSchema).max(SPATIAL_TEMPORAL_AUDIT_LIMITS.cutBoundaries).default([]),
53
+ maxPositionJumpM: z.number().finite().positive().max(1_000_000).default(5),
54
+ maxFootSlideMps: z.number().finite().positive().max(1_000).default(0.05),
55
+ maxCameraAccelerationMps2: z.number().finite().positive().max(1_000_000).default(50),
56
+ maxCameraJerkMps3: z.number().finite().positive().max(1_000_000_000).default(400),
57
+ maxAngularVelocityRadps: z.number().finite().positive().max(100_000).default(4),
58
+ maxExposureJumpEv: z.number().finite().positive().max(64).default(1),
59
+ maxFocusJumpM: z.number().finite().positive().max(1_000_000).default(0.5),
60
+ maxNewAssetsPerSample: z.number().int().min(1).max(1_024).default(8),
61
+ maxEntityCountDelta: z.number().int().min(1).max(1_024).default(16),
62
+ })
63
+ export type SpatialTemporalAuditOptions = Readonly<z.infer<typeof SpatialTemporalAuditOptionsSchema>>
64
+
65
+ export const SpatialTemporalFindingKindSchema = z.enum([
66
+ "visibility-flicker",
67
+ "transform-discontinuity",
68
+ "foot-slide",
69
+ "camera-acceleration",
70
+ "camera-jerk",
71
+ "camera-angular-velocity",
72
+ "exposure-jump",
73
+ "focus-jump",
74
+ "resource-spike",
75
+ ])
76
+ export type SpatialTemporalFindingKind = z.infer<typeof SpatialTemporalFindingKindSchema>
77
+
78
+ export const SpatialTemporalFindingSchema = z.strictObject({
79
+ kind: SpatialTemporalFindingKindSchema,
80
+ sampleIndex: z.number().int().min(0).max(SPATIAL_TEMPORAL_AUDIT_LIMITS.samples - 1),
81
+ timeUs: SpatialTimeUsSchema,
82
+ entityId: SpatialEntityIdSchema.optional(),
83
+ cameraId: SpatialCameraIdSchema.optional(),
84
+ measured: z.number().finite(),
85
+ limit: z.number().finite(),
86
+ detail: z.string().min(1).max(240),
87
+ })
88
+ export type SpatialTemporalFinding = Readonly<z.infer<typeof SpatialTemporalFindingSchema>>
89
+
90
+ export const SpatialTemporalAuditReportSchema = z.strictObject({
91
+ kind: z.literal("slopcamera.spatial-temporal-audit-report"),
92
+ schemaVersion: z.literal(1),
93
+ sceneSha256: SpatialDigestSchema,
94
+ cameraId: SpatialCameraIdSchema,
95
+ sampleCount: z.number().int().min(2).max(SPATIAL_TEMPORAL_AUDIT_LIMITS.samples),
96
+ firstTimeUs: SpatialTimeUsSchema,
97
+ lastTimeUs: SpatialTimeUsSchema,
98
+ findings: z.array(SpatialTemporalFindingSchema).max(SPATIAL_TEMPORAL_AUDIT_LIMITS.findings),
99
+ omittedFindings: z.number().int().min(0),
100
+ thresholds: SpatialTemporalAuditOptionsSchema.omit({ contacts: true, cutBeforeUs: true }),
101
+ contactCount: z.number().int().min(0).max(SPATIAL_TEMPORAL_AUDIT_LIMITS.contacts),
102
+ })
103
+ export type SpatialTemporalAuditReport = Readonly<z.infer<typeof SpatialTemporalAuditReportSchema>>
104
+
105
+ export function spatialTemporalAuditReportSha256(report: SpatialTemporalAuditReport): string {
106
+ return spatialValueSha256(report)
107
+ }
108
+
109
+ const auditInputSchema = z.strictObject({
110
+ snapshots: z.array(z.unknown()).min(2).max(SPATIAL_TEMPORAL_AUDIT_LIMITS.samples),
111
+ options: z.unknown().optional(),
112
+ })
113
+
114
+ const KIND_ORDER: Readonly<Record<SpatialTemporalFindingKind, number>> = {
115
+ "visibility-flicker": 0,
116
+ "transform-discontinuity": 1,
117
+ "foot-slide": 2,
118
+ "camera-acceleration": 3,
119
+ "camera-jerk": 4,
120
+ "camera-angular-velocity": 5,
121
+ "exposure-jump": 6,
122
+ "focus-jump": 7,
123
+ "resource-spike": 8,
124
+ }
125
+
126
+ const translation = (snapshot: EvaluatedSpatialScene, entityId: string): [number, number, number] | undefined => {
127
+ const evaluated = snapshot.entities.find((item) => item.entity.entityId === entityId)
128
+ if (evaluated === undefined) return undefined
129
+ return [evaluated.worldMatrix[12]!, evaluated.worldMatrix[13]!, evaluated.worldMatrix[14]!]
130
+ }
131
+
132
+ const distance = (a: readonly number[], b: readonly number[]): number =>
133
+ Math.hypot(a[0]! - b[0]!, a[1]! - b[1]!, a[2]! - b[2]!)
134
+
135
+ const horizontalDistance = (a: readonly number[], b: readonly number[]): number =>
136
+ Math.hypot(a[0]! - b[0]!, a[2]! - b[2]!)
137
+
138
+ const seconds = (later: EvaluatedSpatialScene, earlier: EvaluatedSpatialScene): number =>
139
+ (later.timeUs - earlier.timeUs) / 1_000_000
140
+
141
+ function quaternionAngleRadians(a: readonly number[], b: readonly number[]): number {
142
+ const dot = Math.min(1, Math.max(-1, Math.abs(a.reduce((sum, value, index) => sum + value * b[index]!, 0))))
143
+ return 2 * Math.acos(dot)
144
+ }
145
+
146
+ /**
147
+ * Audits one ordered run of snapshots. Snapshots must share one scene digest
148
+ * and camera identity and be strictly increasing in time; callers derive them
149
+ * with `evaluateSpatialScene` so evidence stays reproducible.
150
+ */
151
+ export function auditSpatialTemporalEvidence(input: unknown): SpatialTemporalAuditReport {
152
+ const value = parseSpatialValue(auditInputSchema, input, "temporal audit")
153
+ const snapshots = value.snapshots.map((snapshot) => parseSpatialValue(EvaluatedSpatialSceneSchema, snapshot, "snapshot"))
154
+ const options = parseSpatialValue(SpatialTemporalAuditOptionsSchema, value.options ?? {}, "temporal audit options")
155
+
156
+ const sceneSha256 = snapshots[0]!.sceneSha256
157
+ const cameraId = snapshots[0]!.camera.cameraId
158
+ for (const [index, snapshot] of snapshots.entries()) {
159
+ if (snapshot.sceneSha256 !== sceneSha256) {
160
+ throw new SpatialSceneError("invalid-data", `Snapshot ${index} belongs to a different scene digest.`, "snapshots")
161
+ }
162
+ if (snapshot.camera.cameraId !== cameraId) {
163
+ throw new SpatialSceneError("invalid-data", `Snapshot ${index} uses a different camera.`, "snapshots")
164
+ }
165
+ if (index > 0 && snapshot.timeUs <= snapshots[index - 1]!.timeUs) {
166
+ throw new SpatialSceneError("invalid-data", "Snapshots must be strictly increasing in time.", "snapshots")
167
+ }
168
+ }
169
+
170
+ const findings: SpatialTemporalFinding[] = []
171
+ const cutTimes = new Set(options.cutBeforeUs)
172
+ const isCutBoundary = (snapshot: EvaluatedSpatialScene): boolean => cutTimes.has(snapshot.timeUs)
173
+ const add = (finding: SpatialTemporalFinding): void => {
174
+ if (findings.length < SPATIAL_TEMPORAL_AUDIT_LIMITS.findings) findings.push(finding)
175
+ }
176
+
177
+ const entityIds = [...new Set(snapshots.flatMap((snapshot) => snapshot.entities.map((item) => item.entity.entityId)))].sort()
178
+
179
+ // Visibility flicker: an entity toggling hidden↔visible more than once in the run.
180
+ for (const entityId of entityIds) {
181
+ const states = snapshots.map((snapshot) => snapshot.entities.find((item) => item.entity.entityId === entityId)?.visible)
182
+ let flips = 0
183
+ let firstFlipIndex = -1
184
+ for (let index = 1; index < states.length; index += 1) {
185
+ if (states[index] !== undefined && states[index - 1] !== undefined && states[index] !== states[index - 1]) {
186
+ flips += 1
187
+ if (firstFlipIndex === -1) firstFlipIndex = index
188
+ }
189
+ }
190
+ if (flips >= 2) {
191
+ add({
192
+ kind: "visibility-flicker", sampleIndex: firstFlipIndex, timeUs: snapshots[firstFlipIndex]!.timeUs,
193
+ entityId, measured: flips, limit: 1,
194
+ detail: `Entity ${entityId} toggles visibility ${flips} times across the sampled run.`,
195
+ })
196
+ }
197
+ }
198
+
199
+ // Transform discontinuity and foot slide across consecutive samples.
200
+ for (const entityId of entityIds) {
201
+ for (let index = 1; index < snapshots.length; index += 1) {
202
+ const previous = translation(snapshots[index - 1]!, entityId)
203
+ const current = translation(snapshots[index]!, entityId)
204
+ if (previous === undefined || current === undefined) continue
205
+ if (!isCutBoundary(snapshots[index]!)) {
206
+ const jump = distance(previous, current)
207
+ if (jump > options.maxPositionJumpM) {
208
+ add({
209
+ kind: "transform-discontinuity", sampleIndex: index, timeUs: snapshots[index]!.timeUs,
210
+ entityId, measured: jump, limit: options.maxPositionJumpM,
211
+ detail: `Entity ${entityId} teleports ${jump.toFixed(3)}m between samples ${index - 1} and ${index} outside a declared cut.`,
212
+ })
213
+ }
214
+ }
215
+ const dt = seconds(snapshots[index]!, snapshots[index - 1]!)
216
+ if (dt <= 0) continue
217
+ for (const contact of options.contacts) {
218
+ if (contact.entityId !== entityId) continue
219
+ const inWindow = snapshots[index - 1]!.timeUs >= contact.startUs && snapshots[index]!.timeUs <= contact.endUs
220
+ if (!inWindow) continue
221
+ const slideMps = horizontalDistance(previous, current) / dt
222
+ if (slideMps > options.maxFootSlideMps) {
223
+ add({
224
+ kind: "foot-slide", sampleIndex: index, timeUs: snapshots[index]!.timeUs,
225
+ entityId, measured: slideMps, limit: options.maxFootSlideMps,
226
+ detail: `Entity ${entityId} slides ${slideMps.toFixed(3)}m/s horizontally while planted near groundY ${contact.groundY} during [${contact.startUs}, ${contact.endUs}]us.`,
227
+ })
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ // Camera dynamics: position acceleration/jerk and angular velocity.
234
+ for (let index = 1; index < snapshots.length; index += 1) {
235
+ const current = snapshots[index]!
236
+ const previous = snapshots[index - 1]!
237
+ const dt = seconds(current, previous)
238
+ if (dt > 0) {
239
+ const angular = quaternionAngleRadians(current.camera.pose.rotation, previous.camera.pose.rotation) / dt
240
+ if (angular > options.maxAngularVelocityRadps) {
241
+ add({
242
+ kind: "camera-angular-velocity", sampleIndex: index, timeUs: current.timeUs,
243
+ cameraId, measured: angular, limit: options.maxAngularVelocityRadps,
244
+ detail: `Camera rotates at ${angular.toFixed(3)}rad/s between samples ${index - 1} and ${index}.`,
245
+ })
246
+ }
247
+ const focusNow = current.camera.lens?.focusDistanceM
248
+ const focusBefore = previous.camera.lens?.focusDistanceM
249
+ if (focusNow !== undefined && focusBefore !== undefined && Math.abs(focusNow - focusBefore) > options.maxFocusJumpM) {
250
+ add({
251
+ kind: "focus-jump", sampleIndex: index, timeUs: current.timeUs,
252
+ cameraId, measured: Math.abs(focusNow - focusBefore), limit: options.maxFocusJumpM,
253
+ detail: `Focus distance jumps ${Math.abs(focusNow - focusBefore).toFixed(3)}m between samples ${index - 1} and ${index}.`,
254
+ })
255
+ }
256
+ const evNow = current.camera.lens?.exposureEv
257
+ const evBefore = previous.camera.lens?.exposureEv
258
+ if (evNow !== undefined && evBefore !== undefined && Math.abs(evNow - evBefore) > options.maxExposureJumpEv) {
259
+ add({
260
+ kind: "exposure-jump", sampleIndex: index, timeUs: current.timeUs,
261
+ cameraId, measured: Math.abs(evNow - evBefore), limit: options.maxExposureJumpEv,
262
+ detail: `Exposure jumps ${Math.abs(evNow - evBefore).toFixed(3)}EV between samples ${index - 1} and ${index}.`,
263
+ })
264
+ }
265
+ }
266
+ if (index >= 2) {
267
+ const a = snapshots[index - 2]!, b = snapshots[index - 1]!
268
+ const dt0 = seconds(b, a), dt1 = seconds(current, b)
269
+ if (dt0 > 0 && dt1 > 0) {
270
+ const acceleration = Math.hypot(...([0, 1, 2].map((axis) =>
271
+ (current.camera.pose.position[axis]! - b.camera.pose.position[axis]!) / dt1
272
+ - (b.camera.pose.position[axis]! - a.camera.pose.position[axis]!) / dt0,
273
+ ) as [number, number, number])) / ((dt0 + dt1) / 2)
274
+ if (acceleration > options.maxCameraAccelerationMps2) {
275
+ add({
276
+ kind: "camera-acceleration", sampleIndex: index, timeUs: current.timeUs,
277
+ cameraId, measured: acceleration, limit: options.maxCameraAccelerationMps2,
278
+ detail: `Camera accelerates at ${acceleration.toFixed(3)}m/s² at sample ${index}.`,
279
+ })
280
+ }
281
+ }
282
+ }
283
+ if (index >= 3) {
284
+ const a = snapshots[index - 3]!, b = snapshots[index - 2]!, c = snapshots[index - 1]!
285
+ const dta = seconds(b, a), dtb = seconds(c, b), dtc = seconds(current, c)
286
+ if (dta > 0 && dtb > 0 && dtc > 0) {
287
+ // Window accelerations use the same centered-difference convention as
288
+ // the camera-acceleration check; jerk is their time difference.
289
+ const accel = (p1: EvaluatedSpatialScene, p0: EvaluatedSpatialScene, q1: EvaluatedSpatialScene, q0: EvaluatedSpatialScene, d1: number, d0: number): [number, number, number] =>
290
+ [0, 1, 2].map((axis) => (
291
+ (p1.camera.pose.position[axis]! - p0.camera.pose.position[axis]!) / d1
292
+ - (q1.camera.pose.position[axis]! - q0.camera.pose.position[axis]!) / d0
293
+ ) / ((d0 + d1) / 2)) as [number, number, number]
294
+ const a0 = accel(c, b, b, a, dtb, dta)
295
+ const a1 = accel(current, c, c, b, dtc, dtb)
296
+ const jerk = Math.hypot(a1[0] - a0[0], a1[1] - a0[1], a1[2] - a0[2]) / ((dtb + dtc) / 2)
297
+ if (jerk > options.maxCameraJerkMps3) {
298
+ add({
299
+ kind: "camera-jerk", sampleIndex: index, timeUs: current.timeUs,
300
+ cameraId, measured: jerk, limit: options.maxCameraJerkMps3,
301
+ detail: `Camera jerk reaches ${jerk.toFixed(3)}m/s³ at sample ${index}.`,
302
+ })
303
+ }
304
+ }
305
+ }
306
+ }
307
+
308
+ // Resource spikes: new asset digests appearing and entity-count swings per sample.
309
+ for (let index = 1; index < snapshots.length; index += 1) {
310
+ const previous = snapshots[index - 1]!
311
+ const current = snapshots[index]!
312
+ const previousAssets = new Set(previous.assets.map((asset) => asset.assetId))
313
+ const newAssets = current.assets.filter((asset) => !previousAssets.has(asset.assetId)).length
314
+ if (newAssets > options.maxNewAssetsPerSample) {
315
+ add({
316
+ kind: "resource-spike", sampleIndex: index, timeUs: current.timeUs,
317
+ measured: newAssets, limit: options.maxNewAssetsPerSample,
318
+ detail: `${newAssets} assets enter the scene between samples ${index - 1} and ${index}.`,
319
+ })
320
+ }
321
+ const entityDelta = Math.abs(current.entities.length - previous.entities.length)
322
+ if (entityDelta > options.maxEntityCountDelta) {
323
+ add({
324
+ kind: "resource-spike", sampleIndex: index, timeUs: current.timeUs,
325
+ measured: entityDelta, limit: options.maxEntityCountDelta,
326
+ detail: `Entity count changes by ${entityDelta} between samples ${index - 1} and ${index}.`,
327
+ })
328
+ }
329
+ }
330
+
331
+ const all = findings.slice()
332
+ all.sort((a, b) =>
333
+ a.sampleIndex - b.sampleIndex
334
+ || KIND_ORDER[a.kind] - KIND_ORDER[b.kind]
335
+ || (a.entityId ?? "").localeCompare(b.entityId ?? "")
336
+ || (a.cameraId ?? "").localeCompare(b.cameraId ?? ""),
337
+ )
338
+ const returned = all.slice(0, SPATIAL_TEMPORAL_AUDIT_LIMITS.findings)
339
+ const { contacts: _contacts, cutBeforeUs: _cutBeforeUs, ...thresholds } = options
340
+
341
+ return deepFreezeJson(SpatialTemporalAuditReportSchema.parse({
342
+ kind: "slopcamera.spatial-temporal-audit-report",
343
+ schemaVersion: 1,
344
+ sceneSha256,
345
+ cameraId,
346
+ sampleCount: snapshots.length,
347
+ firstTimeUs: snapshots[0]!.timeUs,
348
+ lastTimeUs: snapshots[snapshots.length - 1]!.timeUs,
349
+ findings: returned,
350
+ omittedFindings: all.length - returned.length,
351
+ thresholds,
352
+ contactCount: options.contacts.length,
353
+ }))
354
+ }
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const SLOPCAMERA_VERSION = "3.2.8" as const
1
+ export const SLOPCAMERA_VERSION = "3.3.1" as const