@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.
- package/README.md +29 -9
- package/apps/desktop/application/context.ts +6 -0
- package/apps/desktop/application/default-registry.ts +26 -0
- package/apps/desktop/application/operation.ts +14 -1
- package/apps/desktop/application/operations/index.ts +4 -0
- package/apps/desktop/application/operations/spatial-behavior.ts +151 -0
- package/apps/desktop/application/operations/spatial-direction.ts +222 -0
- package/apps/desktop/application/operations/spatial-rendered-audit.ts +174 -0
- package/apps/desktop/application/operations/spatial-review.ts +194 -0
- package/apps/desktop/application/operations/spatial-scene.ts +25 -1
- package/apps/desktop/application/registry.ts +2 -2
- package/apps/desktop/application/spatial-asset-admission.ts +201 -0
- package/apps/desktop/application/spatial-assets.ts +160 -31
- package/apps/desktop/application/spatial-render.ts +217 -20
- package/apps/desktop/application/spatial-rendered-audit.ts +273 -0
- package/apps/desktop/application/spatial-review.ts +361 -0
- package/apps/desktop/application/spatial-spz.ts +48 -3
- package/apps/desktop/application/spatial-world-import.ts +26 -5
- package/apps/desktop/application/spatial-world-metadata.ts +105 -0
- package/apps/desktop/cli/args.ts +514 -11
- package/apps/desktop/cli/capability-manifest.ts +374 -0
- package/apps/desktop/cli/cinema-renderer.ts +608 -0
- package/apps/desktop/cli/command-host-resources.ts +29 -1
- package/apps/desktop/cli/commands.ts +1188 -4
- package/apps/desktop/cli/gateway-review-provider.ts +334 -0
- package/apps/desktop/cli/help.ts +172 -6
- package/apps/desktop/cli/main.ts +3 -3
- package/apps/desktop/cli/portable-surface.ts +6 -1
- package/apps/desktop/cli/spatial-asset-service.ts +124 -0
- package/apps/desktop/cli/spatial-design-service.ts +237 -0
- package/apps/desktop/cli/spatial-generate-service.ts +370 -0
- package/apps/desktop/cli/spatial-scene-service.ts +268 -4
- package/apps/desktop/cli/workflow-code.ts +1 -1
- package/apps/desktop/code/semantic-builder.ts +24 -0
- package/apps/desktop/contracts/cinema.ts +736 -0
- package/apps/desktop/contracts/index.ts +1 -0
- package/apps/desktop/contracts/spatial-asset.ts +16 -0
- package/apps/desktop/contracts/spatial-world.ts +26 -3
- package/apps/desktop/core/cinema-plan.ts +1320 -0
- package/apps/desktop/core/index.ts +1 -0
- package/apps/desktop/core/storage.ts +75 -0
- package/apps/desktop/dist/cli/main.js +725 -273
- package/apps/desktop/html-overlay/spatial.ts +619 -57
- package/apps/desktop/workflows/cinematic-world.ts +127 -0
- package/apps/desktop/workflows/index.ts +6 -0
- package/dist/cli.js +25 -429
- package/dist/code/advanced.js +1 -269
- package/dist/code/index.js +16 -2239
- package/dist/generate.js +1 -23
- package/dist/host-resources.js +1 -25
- package/dist/index-0bbesn4m.js +35 -0
- package/dist/index-296qhpf2.js +3 -0
- package/dist/index-3cxf2c58.js +3 -0
- package/dist/index-77fjfg9f.js +3 -0
- package/dist/index-80a36bc0.js +3 -0
- package/dist/index-a3vvs3rf.js +3 -0
- package/dist/index-b3geqczn.js +5 -0
- package/dist/index-cgpkyjqa.js +3 -0
- package/dist/index-f0ptv8dm.js +7 -0
- package/dist/index-grk47tgs.js +3 -0
- package/dist/index-qry58nj2.js +10 -0
- package/dist/index.js +1 -226
- package/dist/operations.js +1 -28
- package/dist/skill-install-gx7afxsd.js +2 -0
- package/dist/vectorize/worker.js +1 -141
- package/dist/workflow.js +1 -15
- package/docs/README.md +5 -1
- package/package.json +7 -5
- package/skills/slopcamera/SKILL.md +6 -1
- package/skills/slopcamera/references/directed-scenes.md +22 -2
- package/skills/slopcamera/references/gateway-media.md +15 -1
- package/skills/slopcamera/references/image-galleries.md +166 -0
- package/skills/slopcamera/references/install.md +1 -1
- package/skills/slopcamera/references/parametric-design.md +14 -0
- package/skills/slopcamera/references/scene-building.md +32 -0
- package/skills/slopcamera/references/social-collage-banners.md +204 -0
- package/skills/slopcamera/references/workflows-sdk.md +3 -3
- package/skills/slopcamera/scripts/compose-social-collage-banner.ts +728 -0
- package/src/capability-manifest.ts +361 -0
- package/src/cli.ts +222 -4
- package/src/code/canonical-json.ts +9 -0
- package/src/generate.ts +2 -2
- package/src/icon.ts +1111 -0
- package/src/image-gallery.ts +1049 -0
- package/src/index.ts +11 -0
- package/src/mcp/boundary.ts +11 -11
- package/src/mcp/index.ts +3 -0
- package/src/mcp/server.ts +2 -1
- package/src/mcp/tools.ts +1642 -130
- package/src/mcp/types.ts +19 -0
- package/src/operations.ts +412 -0
- package/src/portable-capability-manifest.ts +67 -0
- package/src/scene-gallery.ts +184 -0
- package/src/spatial-scene/asset-admission.ts +183 -0
- package/src/spatial-scene/audit-rendered.ts +692 -0
- package/src/spatial-scene/audit.ts +617 -0
- package/src/spatial-scene/behavior-audit.ts +252 -0
- package/src/spatial-scene/behavior-bake.ts +233 -0
- package/src/spatial-scene/behavior-fns.ts +585 -0
- package/src/spatial-scene/behavior-gallery.ts +109 -0
- package/src/spatial-scene/behavior-stdlib.ts +323 -0
- package/src/spatial-scene/behavior-trace.ts +325 -0
- package/src/spatial-scene/behavior.ts +618 -0
- package/src/spatial-scene/build.ts +632 -0
- package/src/spatial-scene/camera-rig.ts +125 -0
- package/src/spatial-scene/camera-track.ts +4 -2
- package/src/spatial-scene/character.ts +124 -0
- package/src/spatial-scene/contracts.ts +75 -7
- package/src/spatial-scene/design-templates.ts +351 -0
- package/src/spatial-scene/design.ts +327 -0
- package/src/spatial-scene/direction-compile.ts +584 -0
- package/src/spatial-scene/direction.ts +122 -0
- package/src/spatial-scene/effects.ts +149 -0
- package/src/spatial-scene/evaluate.ts +109 -29
- package/src/spatial-scene/gallery.ts +277 -0
- package/src/spatial-scene/generate.ts +239 -0
- package/src/spatial-scene/geometry-native.ts +165 -0
- package/src/spatial-scene/geometry.ts +1216 -0
- package/src/spatial-scene/gltf.ts +600 -77
- package/src/spatial-scene/identity.ts +55 -2
- package/src/spatial-scene/index.ts +36 -0
- package/src/spatial-scene/inspect.ts +35 -6
- package/src/spatial-scene/material-lighting.ts +609 -0
- package/src/spatial-scene/math.ts +21 -4
- package/src/spatial-scene/motion-evidence.ts +88 -0
- package/src/spatial-scene/parametric.ts +765 -0
- package/src/spatial-scene/particle-preparation.ts +183 -0
- package/src/spatial-scene/particle.ts +195 -0
- package/src/spatial-scene/patch.ts +109 -15
- package/src/spatial-scene/performance.ts +1613 -0
- package/src/spatial-scene/probe.ts +137 -0
- package/src/spatial-scene/recipe-pack.ts +86 -0
- package/src/spatial-scene/render-effects.ts +240 -0
- package/src/spatial-scene/review.ts +639 -0
- package/src/spatial-scene/simulation-bake.ts +206 -0
- package/src/spatial-scene/simulation.ts +187 -0
- package/src/spatial-scene/solve.ts +366 -0
- package/src/spatial-scene/temporal-audit.ts +354 -0
- package/src/version.ts +1 -1
- package/dist/index-42zsesc1.js +0 -2257
- package/dist/index-7131pg1c.js +0 -1820
- package/dist/index-7308egqr.js +0 -48
- package/dist/index-8txs6fkn.js +0 -712
- package/dist/index-fava6pge.js +0 -192
- package/dist/index-h1k0fnjq.js +0 -2363
- package/dist/index-r7gdhmsp.js +0 -424
- package/dist/index-sh6xbav6.js +0 -1219
- package/dist/index-z1w83f81.js +0 -4
- package/dist/index-zfnddgay.js +0 -2029
- 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.
|
|
1
|
+
export const SLOPCAMERA_VERSION = "3.3.1" as const
|