@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,692 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { createBoundedJsonValueSnapshot, deepFreezeJson } from "../code/json-snapshot.js"
|
|
3
|
+
import {
|
|
4
|
+
SPATIAL_SCENE_LIMITS, SpatialAssetIdSchema, SpatialCameraIdSchema, SpatialDigestSchema,
|
|
5
|
+
SpatialEntityIdSchema, SpatialPlacementSchema, SpatialSceneIdSchema, SpatialTimeUsSchema,
|
|
6
|
+
type SpatialEntity,
|
|
7
|
+
} from "./contracts.js"
|
|
8
|
+
import { auditSpatialSceneInContext, SPATIAL_AUDIT_LIMITS, SpatialAuditBoundsSchema } from "./audit.js"
|
|
9
|
+
import { createSpatialEvaluationContext, type SpatialEvaluationContext } from "./evaluate.js"
|
|
10
|
+
import {
|
|
11
|
+
parseSpatialValue, sortSpatialBy, SpatialSceneError,
|
|
12
|
+
} from "./identity.js"
|
|
13
|
+
import type { Bounds } from "./math.js"
|
|
14
|
+
|
|
15
|
+
export const SPATIAL_RENDERED_AUDIT_LIMITS = Object.freeze({
|
|
16
|
+
/** Decoded object-ID frames per audit; the geometric audit owns sample cadence. */
|
|
17
|
+
samples: SPATIAL_AUDIT_LIMITS.samples,
|
|
18
|
+
/** Retained findings; any excess is counted in `omittedFindings`. */
|
|
19
|
+
findings: SPATIAL_AUDIT_LIMITS.findings,
|
|
20
|
+
/** Joint bound on entities × frames so reports stay bounded. */
|
|
21
|
+
entitySamples: SPATIAL_AUDIT_LIMITS.entitySamples,
|
|
22
|
+
/** Canonical report byte bound checked before return. */
|
|
23
|
+
reportBytes: SPATIAL_AUDIT_LIMITS.reportBytes,
|
|
24
|
+
/** Selection codes occupy [1, 4096] in the RGB big-endian uint24 pass. */
|
|
25
|
+
selectionIds: 4_096,
|
|
26
|
+
/** A single decoded object-ID frame stays inside the renderer's pixel budget. */
|
|
27
|
+
frameDimension: 8_192,
|
|
28
|
+
framePixels: 33_554_432,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const ENTITY_KINDS = ["group", "mesh", "image", "diagram", "video", "text", "light", "splat", "environment"] as const
|
|
32
|
+
const ELIGIBILITY = ["renderable", "proxy-coverage", "view-masked", "no-surface", "unsupported-kind"] as const
|
|
33
|
+
const BOUNDS_UNKNOWN_REASONS = ["requires-asset-decoding", "requires-text-layout", "no-surface"] as const
|
|
34
|
+
const FINDING_KINDS = [
|
|
35
|
+
"never-rendered", "unsupported-kind", "proxy-coverage", "occluded", "unattributed-pixels", "empty-render", "bounds-unknown",
|
|
36
|
+
] as const
|
|
37
|
+
const SAMPLE_NOTES = ["out-of-range", "other-camera", "unlowered-expected"] as const
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The lowering-evidence representation label for a splat lowered as its
|
|
41
|
+
* bounding-box proxy. The host derives the box from the prepared splat's
|
|
42
|
+
* decoded position bounds, so the emitted pixels are approximate coverage —
|
|
43
|
+
* never splat pixel truth. The string is part of the lowering↔audit contract;
|
|
44
|
+
* frame evidence uses it so this analyzer can tell an honest proxy from a
|
|
45
|
+
* surface that must not claim splat attribution.
|
|
46
|
+
*/
|
|
47
|
+
export const SPATIAL_SPLAT_PROXY_REPRESENTATION =
|
|
48
|
+
"splat-bounding-box-proxy;spz-position-bounds;approximate-not-pixel-truth"
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The coverage policy rendered by the host. Mirrored from the overlay batch
|
|
52
|
+
* contract rather than imported so this boundary stays portable: alpha-threshold
|
|
53
|
+
* admits every surface whose effective alpha reaches the threshold; opaque
|
|
54
|
+
* requires exactly opaque materials and textures.
|
|
55
|
+
*/
|
|
56
|
+
export const SpatialRenderedAuditCoverageSchema = z.discriminatedUnion("kind", [
|
|
57
|
+
z.strictObject({ kind: z.literal("opaque") }),
|
|
58
|
+
z.strictObject({ kind: z.literal("alpha-threshold"), threshold: z.number().finite().gt(0).max(1) }),
|
|
59
|
+
])
|
|
60
|
+
export type SpatialRenderedAuditCoverage = z.infer<typeof SpatialRenderedAuditCoverageSchema>
|
|
61
|
+
/** The default audit pass counts a pixel when effective alpha reaches one half. */
|
|
62
|
+
export const SPATIAL_RENDERED_AUDIT_COVERAGE: SpatialRenderedAuditCoverage = Object.freeze({ kind: "alpha-threshold", threshold: 0.5 })
|
|
63
|
+
|
|
64
|
+
const selectionIdSchema = z.number().int().min(1).max(SPATIAL_RENDERED_AUDIT_LIMITS.selectionIds)
|
|
65
|
+
const selectionKeyPattern = /^(?:0|[1-9]\d{0,3})$/u
|
|
66
|
+
const pixelCountSchema = z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.framePixels)
|
|
67
|
+
|
|
68
|
+
/** One entity-to-selection evidence row from the rendered batch metadata. */
|
|
69
|
+
export const SpatialRenderedAuditObjectSchema = z.strictObject({
|
|
70
|
+
entityId: SpatialEntityIdSchema,
|
|
71
|
+
selectionId: selectionIdSchema,
|
|
72
|
+
representation: z.string().min(1).max(256),
|
|
73
|
+
placement: z.enum(["world", "view"]),
|
|
74
|
+
assetManifestSha256: SpatialDigestSchema.optional(),
|
|
75
|
+
/** Lowered instance count; the object-ID pass attributes every instance to this entity's selection code. */
|
|
76
|
+
instances: z.number().int().min(1).max(SPATIAL_SCENE_LIMITS.entities).optional(),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
/** One decoded object-ID frame: per-selection pixel counts plus lowering evidence. */
|
|
80
|
+
export const SpatialRenderedAuditFrameSchema = z.strictObject({
|
|
81
|
+
timeUs: SpatialTimeUsSchema,
|
|
82
|
+
width: z.number().int().min(1).max(SPATIAL_RENDERED_AUDIT_LIMITS.frameDimension),
|
|
83
|
+
height: z.number().int().min(1).max(SPATIAL_RENDERED_AUDIT_LIMITS.frameDimension),
|
|
84
|
+
/** Digest of the exact rendered bytes these counts were decoded from. */
|
|
85
|
+
pngSha256: SpatialDigestSchema,
|
|
86
|
+
counts: z.record(z.string().regex(selectionKeyPattern), pixelCountSchema),
|
|
87
|
+
objects: z.array(SpatialRenderedAuditObjectSchema).max(SPATIAL_SCENE_LIMITS.entities),
|
|
88
|
+
}).superRefine((frame, context) => {
|
|
89
|
+
const pixels = frame.width * frame.height
|
|
90
|
+
if (pixels > SPATIAL_RENDERED_AUDIT_LIMITS.framePixels) {
|
|
91
|
+
context.addIssue({ code: "custom", message: "Object-ID frame exceeds the pixel budget." })
|
|
92
|
+
}
|
|
93
|
+
let total = 0
|
|
94
|
+
for (const [key, count] of Object.entries(frame.counts)) {
|
|
95
|
+
const code = Number(key)
|
|
96
|
+
if (!Number.isInteger(code) || code < 1 || code > SPATIAL_RENDERED_AUDIT_LIMITS.selectionIds) {
|
|
97
|
+
context.addIssue({ code: "custom", message: "Object-ID counts must key selection codes in [1,4096]." })
|
|
98
|
+
}
|
|
99
|
+
total += count
|
|
100
|
+
}
|
|
101
|
+
if (total > pixels) {
|
|
102
|
+
context.addIssue({ code: "custom", message: "Object-ID counts exceed the frame pixel count." })
|
|
103
|
+
}
|
|
104
|
+
const entityIds = new Set<string>(), selectionIds = new Set<number>()
|
|
105
|
+
for (const object of frame.objects) {
|
|
106
|
+
if (entityIds.has(object.entityId) || selectionIds.has(object.selectionId)) {
|
|
107
|
+
context.addIssue({ code: "custom", message: "Frame evidence must name each entity and selection code once." })
|
|
108
|
+
}
|
|
109
|
+
entityIds.add(object.entityId); selectionIds.add(object.selectionId)
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
export const SpatialRenderedAuditSampleSchema = z.strictObject({
|
|
114
|
+
timeUs: SpatialTimeUsSchema,
|
|
115
|
+
/** The entity was eligible and effectively visible under the audited camera. */
|
|
116
|
+
expected: z.boolean(),
|
|
117
|
+
/** The renderer lowered the entity into this frame's object-ID pass. */
|
|
118
|
+
lowered: z.boolean(),
|
|
119
|
+
/** At least one frame pixel was attributed to this entity. */
|
|
120
|
+
rendered: z.boolean(),
|
|
121
|
+
pixels: z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.framePixels),
|
|
122
|
+
framePercent: z.number().finite().min(0).max(100),
|
|
123
|
+
/** Geometric corner-projection estimate in px² when bounds exist. */
|
|
124
|
+
geometricPixels: z.number().finite().min(0).max(1e15).optional(),
|
|
125
|
+
/** rendered pixels ÷ geometricPixels; absent when the estimate is zero. */
|
|
126
|
+
coverageRatio: z.number().finite().min(0).max(1e15).optional(),
|
|
127
|
+
note: z.enum(SAMPLE_NOTES).optional(),
|
|
128
|
+
})
|
|
129
|
+
export const SpatialRenderedAuditEntitySchema = z.strictObject({
|
|
130
|
+
entityId: SpatialEntityIdSchema,
|
|
131
|
+
name: z.string().min(1).max(256),
|
|
132
|
+
kind: z.enum(ENTITY_KINDS),
|
|
133
|
+
placement: SpatialPlacementSchema,
|
|
134
|
+
/** Whether the object-ID pass can attribute pixels to this entity at all. */
|
|
135
|
+
eligibility: z.enum(ELIGIBILITY),
|
|
136
|
+
selectionId: selectionIdSchema,
|
|
137
|
+
enclosure: z.discriminatedUnion("status", [
|
|
138
|
+
z.strictObject({ status: z.literal("bounded") }),
|
|
139
|
+
z.strictObject({ status: z.literal("unknown"), reason: z.enum(BOUNDS_UNKNOWN_REASONS) }),
|
|
140
|
+
]),
|
|
141
|
+
/** Present on mesh entities only when local-space instances are declared. */
|
|
142
|
+
instances: z.number().int().min(1).max(SPATIAL_SCENE_LIMITS.entities).optional(),
|
|
143
|
+
samples: z.array(SpatialRenderedAuditSampleSchema).max(SPATIAL_RENDERED_AUDIT_LIMITS.samples),
|
|
144
|
+
totals: z.strictObject({
|
|
145
|
+
expected: z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.samples),
|
|
146
|
+
lowered: z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.samples),
|
|
147
|
+
rendered: z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.samples),
|
|
148
|
+
pixels: z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.framePixels * SPATIAL_RENDERED_AUDIT_LIMITS.samples),
|
|
149
|
+
maxPixels: z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.framePixels),
|
|
150
|
+
maxFramePercent: z.number().finite().min(0).max(100),
|
|
151
|
+
}),
|
|
152
|
+
})
|
|
153
|
+
export const SpatialRenderedAuditFrameReportSchema = z.strictObject({
|
|
154
|
+
timeUs: SpatialTimeUsSchema,
|
|
155
|
+
pngSha256: SpatialDigestSchema,
|
|
156
|
+
renderedPixels: pixelCountSchema,
|
|
157
|
+
/** Pixels carrying selection codes absent from the frame's attributable evidence. */
|
|
158
|
+
unattributedPixels: pixelCountSchema,
|
|
159
|
+
loweredEntities: z.number().int().min(0).max(SPATIAL_SCENE_LIMITS.entities),
|
|
160
|
+
})
|
|
161
|
+
export const SpatialRenderedAuditFindingSchema = z.strictObject({
|
|
162
|
+
severity: z.enum(["info", "warning"]),
|
|
163
|
+
kind: z.enum(FINDING_KINDS),
|
|
164
|
+
entityId: SpatialEntityIdSchema.optional(),
|
|
165
|
+
timeUs: SpatialTimeUsSchema.optional(),
|
|
166
|
+
detail: z.string().min(1).max(1_024),
|
|
167
|
+
})
|
|
168
|
+
export const SpatialRenderedAuditReportSchema = z.strictObject({
|
|
169
|
+
kind: z.literal("slopcamera.spatial-rendered-audit"),
|
|
170
|
+
schemaVersion: z.literal(1),
|
|
171
|
+
sceneId: SpatialSceneIdSchema,
|
|
172
|
+
sceneSha256: SpatialDigestSchema,
|
|
173
|
+
cameraId: SpatialCameraIdSchema,
|
|
174
|
+
durationUs: SpatialTimeUsSchema,
|
|
175
|
+
/** Ascending unique sample times, one decoded frame each. */
|
|
176
|
+
timesUs: z.array(SpatialTimeUsSchema).min(1).max(SPATIAL_RENDERED_AUDIT_LIMITS.samples),
|
|
177
|
+
mode: z.strictObject({ kind: z.literal("object-id"), coverage: SpatialRenderedAuditCoverageSchema }),
|
|
178
|
+
frame: z.strictObject({
|
|
179
|
+
width: z.number().int().min(1).max(SPATIAL_RENDERED_AUDIT_LIMITS.frameDimension),
|
|
180
|
+
height: z.number().int().min(1).max(SPATIAL_RENDERED_AUDIT_LIMITS.frameDimension),
|
|
181
|
+
pixels: pixelCountSchema,
|
|
182
|
+
}),
|
|
183
|
+
summary: z.strictObject({
|
|
184
|
+
entities: z.strictObject({
|
|
185
|
+
total: z.number().int().min(0).max(SPATIAL_SCENE_LIMITS.entities),
|
|
186
|
+
renderable: z.number().int().min(0).max(SPATIAL_SCENE_LIMITS.entities),
|
|
187
|
+
/** Splats lowered as bounding-box proxies — approximate coverage, never splat pixel truth. */
|
|
188
|
+
proxyCoverage: z.number().int().min(0).max(SPATIAL_SCENE_LIMITS.entities).optional(),
|
|
189
|
+
viewMasked: z.number().int().min(0).max(SPATIAL_SCENE_LIMITS.entities),
|
|
190
|
+
noSurface: z.number().int().min(0).max(SPATIAL_SCENE_LIMITS.entities),
|
|
191
|
+
unsupported: z.number().int().min(0).max(SPATIAL_SCENE_LIMITS.entities),
|
|
192
|
+
}),
|
|
193
|
+
/** Authored-visible renderable or view entities that produced zero attributed pixels. */
|
|
194
|
+
entitiesNeverRendered: z.array(SpatialEntityIdSchema).max(SPATIAL_SCENE_LIMITS.entities),
|
|
195
|
+
entitiesUnsupported: z.array(SpatialEntityIdSchema).max(SPATIAL_SCENE_LIMITS.entities),
|
|
196
|
+
entitiesViewMasked: z.array(SpatialEntityIdSchema).max(SPATIAL_SCENE_LIMITS.entities),
|
|
197
|
+
entitiesProxyCoverage: z.array(SpatialEntityIdSchema).max(SPATIAL_SCENE_LIMITS.entities).optional(),
|
|
198
|
+
renderedPixels: z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.framePixels * SPATIAL_RENDERED_AUDIT_LIMITS.samples),
|
|
199
|
+
unattributedPixels: z.number().int().min(0).max(SPATIAL_RENDERED_AUDIT_LIMITS.framePixels * SPATIAL_RENDERED_AUDIT_LIMITS.samples),
|
|
200
|
+
}),
|
|
201
|
+
entities: z.array(SpatialRenderedAuditEntitySchema).max(SPATIAL_SCENE_LIMITS.entities),
|
|
202
|
+
frames: z.array(SpatialRenderedAuditFrameReportSchema).min(1).max(SPATIAL_RENDERED_AUDIT_LIMITS.samples),
|
|
203
|
+
findings: z.array(SpatialRenderedAuditFindingSchema).max(SPATIAL_RENDERED_AUDIT_LIMITS.findings),
|
|
204
|
+
omittedFindings: z.number().int().min(0),
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Structural report types mirror the schemas above rather than inferring from
|
|
209
|
+
* them, following `audit.ts`: recursive readonly mappers over the strict-object
|
|
210
|
+
* graph measurably slow every authored-source typecheck that reaches this
|
|
211
|
+
* module through the `./code` surface.
|
|
212
|
+
*/
|
|
213
|
+
export interface SpatialRenderedAuditObject {
|
|
214
|
+
readonly entityId: string
|
|
215
|
+
readonly selectionId: number
|
|
216
|
+
readonly representation: string
|
|
217
|
+
readonly placement: "world" | "view"
|
|
218
|
+
readonly assetManifestSha256?: string | undefined
|
|
219
|
+
readonly instances?: number | undefined
|
|
220
|
+
}
|
|
221
|
+
export interface SpatialRenderedAuditFrame {
|
|
222
|
+
readonly timeUs: number
|
|
223
|
+
readonly width: number
|
|
224
|
+
readonly height: number
|
|
225
|
+
readonly pngSha256: string
|
|
226
|
+
readonly counts: Readonly<Record<string, number>>
|
|
227
|
+
readonly objects: readonly SpatialRenderedAuditObject[]
|
|
228
|
+
}
|
|
229
|
+
export interface SpatialRenderedAuditSample {
|
|
230
|
+
readonly timeUs: number
|
|
231
|
+
readonly expected: boolean
|
|
232
|
+
readonly lowered: boolean
|
|
233
|
+
readonly rendered: boolean
|
|
234
|
+
readonly pixels: number
|
|
235
|
+
readonly framePercent: number
|
|
236
|
+
readonly geometricPixels?: number | undefined
|
|
237
|
+
readonly coverageRatio?: number | undefined
|
|
238
|
+
readonly note?: (typeof SAMPLE_NOTES)[number] | undefined
|
|
239
|
+
}
|
|
240
|
+
export interface SpatialRenderedAuditEntity {
|
|
241
|
+
readonly entityId: string
|
|
242
|
+
readonly name: string
|
|
243
|
+
readonly kind: (typeof ENTITY_KINDS)[number]
|
|
244
|
+
readonly placement: SpatialEntity["placement"]
|
|
245
|
+
readonly eligibility: (typeof ELIGIBILITY)[number]
|
|
246
|
+
readonly selectionId: number
|
|
247
|
+
readonly enclosure:
|
|
248
|
+
| { readonly status: "bounded" }
|
|
249
|
+
| { readonly status: "unknown"; readonly reason: (typeof BOUNDS_UNKNOWN_REASONS)[number] }
|
|
250
|
+
readonly instances?: number | undefined
|
|
251
|
+
readonly samples: readonly SpatialRenderedAuditSample[]
|
|
252
|
+
readonly totals: {
|
|
253
|
+
readonly expected: number
|
|
254
|
+
readonly lowered: number
|
|
255
|
+
readonly rendered: number
|
|
256
|
+
readonly pixels: number
|
|
257
|
+
readonly maxPixels: number
|
|
258
|
+
readonly maxFramePercent: number
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
export interface SpatialRenderedAuditFinding {
|
|
262
|
+
readonly severity: "info" | "warning"
|
|
263
|
+
readonly kind: (typeof FINDING_KINDS)[number]
|
|
264
|
+
readonly entityId?: string | undefined
|
|
265
|
+
readonly timeUs?: number | undefined
|
|
266
|
+
readonly detail: string
|
|
267
|
+
}
|
|
268
|
+
export interface SpatialRenderedAuditReport {
|
|
269
|
+
readonly kind: "slopcamera.spatial-rendered-audit"
|
|
270
|
+
readonly schemaVersion: 1
|
|
271
|
+
readonly sceneId: string
|
|
272
|
+
readonly sceneSha256: string
|
|
273
|
+
readonly cameraId: string
|
|
274
|
+
readonly durationUs: number
|
|
275
|
+
readonly timesUs: readonly number[]
|
|
276
|
+
readonly mode: { readonly kind: "object-id"; readonly coverage: SpatialRenderedAuditCoverage }
|
|
277
|
+
readonly frame: { readonly width: number; readonly height: number; readonly pixels: number }
|
|
278
|
+
readonly summary: {
|
|
279
|
+
readonly entities: {
|
|
280
|
+
readonly total: number
|
|
281
|
+
readonly renderable: number
|
|
282
|
+
readonly proxyCoverage?: number | undefined
|
|
283
|
+
readonly viewMasked: number
|
|
284
|
+
readonly noSurface: number
|
|
285
|
+
readonly unsupported: number
|
|
286
|
+
}
|
|
287
|
+
readonly entitiesNeverRendered: readonly string[]
|
|
288
|
+
readonly entitiesUnsupported: readonly string[]
|
|
289
|
+
readonly entitiesViewMasked: readonly string[]
|
|
290
|
+
readonly entitiesProxyCoverage?: readonly string[] | undefined
|
|
291
|
+
readonly renderedPixels: number
|
|
292
|
+
readonly unattributedPixels: number
|
|
293
|
+
}
|
|
294
|
+
readonly entities: readonly SpatialRenderedAuditEntity[]
|
|
295
|
+
readonly frames: readonly {
|
|
296
|
+
readonly timeUs: number
|
|
297
|
+
readonly pngSha256: string
|
|
298
|
+
readonly renderedPixels: number
|
|
299
|
+
readonly unattributedPixels: number
|
|
300
|
+
readonly loweredEntities: number
|
|
301
|
+
}[]
|
|
302
|
+
readonly findings: readonly SpatialRenderedAuditFinding[]
|
|
303
|
+
readonly omittedFindings: number
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export const SpatialRenderedAuditOptionsSchema = z.strictObject({
|
|
307
|
+
cameraId: SpatialCameraIdSchema,
|
|
308
|
+
assetBounds: z.record(SpatialAssetIdSchema, SpatialAuditBoundsSchema).optional(),
|
|
309
|
+
/** Records which coverage policy produced the decoded frames. */
|
|
310
|
+
coverage: SpatialRenderedAuditCoverageSchema.optional(),
|
|
311
|
+
})
|
|
312
|
+
export interface SpatialRenderedAuditOptions {
|
|
313
|
+
readonly cameraId: string
|
|
314
|
+
readonly assetBounds?: Readonly<Record<string, Bounds>>
|
|
315
|
+
readonly coverage?: SpatialRenderedAuditCoverage
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Decodes one binary-validity object-ID frame into per-selection pixel counts.
|
|
320
|
+
* No-hit is exactly [0,0,0,0]; every drawn pixel must carry alpha 255 and a
|
|
321
|
+
* selection code in [1,4096]. Anything else is not a qualified pass output.
|
|
322
|
+
*/
|
|
323
|
+
export function decodeObjectIdPixels(rgba: Uint8Array, width: number, height: number): Record<string, number> {
|
|
324
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1
|
|
325
|
+
|| width > SPATIAL_RENDERED_AUDIT_LIMITS.frameDimension || height > SPATIAL_RENDERED_AUDIT_LIMITS.frameDimension
|
|
326
|
+
|| width * height > SPATIAL_RENDERED_AUDIT_LIMITS.framePixels) {
|
|
327
|
+
throw new RangeError("Object-ID frame dimensions exceed their bound.")
|
|
328
|
+
}
|
|
329
|
+
if (rgba.length !== width * height * 4) {
|
|
330
|
+
throw new RangeError("Object-ID pixels must be exactly width × height RGBA8.")
|
|
331
|
+
}
|
|
332
|
+
const counts: Record<string, number> = {}
|
|
333
|
+
for (let index = 0; index < rgba.length; index += 4) {
|
|
334
|
+
const red = rgba[index]!, green = rgba[index + 1]!, blue = rgba[index + 2]!, alpha = rgba[index + 3]!
|
|
335
|
+
if (alpha === 0) {
|
|
336
|
+
if (red !== 0 || green !== 0 || blue !== 0) {
|
|
337
|
+
throw new RangeError("Object-ID no-hit pixels must be exactly [0,0,0,0].")
|
|
338
|
+
}
|
|
339
|
+
continue
|
|
340
|
+
}
|
|
341
|
+
const code = red * 65_536 + green * 256 + blue
|
|
342
|
+
if (alpha !== 255 || code === 0 || code > SPATIAL_RENDERED_AUDIT_LIMITS.selectionIds) {
|
|
343
|
+
throw new RangeError("Object-ID pixels must be alpha-255 selection codes in [1,4096].")
|
|
344
|
+
}
|
|
345
|
+
counts[String(code)] = (counts[String(code)] ?? 0) + 1
|
|
346
|
+
}
|
|
347
|
+
return counts
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const round3 = (value: number): number => Math.round(value * 1_000) / 1_000
|
|
351
|
+
|
|
352
|
+
function eligibility(entity: SpatialEntity, assetBounds: Readonly<Record<string, Bounds>>): (typeof ELIGIBILITY)[number] {
|
|
353
|
+
if (entity.kind === "splat") {
|
|
354
|
+
// A world-placed splat with supplied bounds lowers its bounding-box proxy
|
|
355
|
+
// in the object-ID pass — approximate coverage, never pixel truth. A view
|
|
356
|
+
// placement or absent bounds keeps the honest unsupported report.
|
|
357
|
+
return entity.placement.kind === "world" && assetBounds[entity.assetId] !== undefined
|
|
358
|
+
? "proxy-coverage" : "unsupported-kind"
|
|
359
|
+
}
|
|
360
|
+
if (entity.kind === "group" || entity.kind === "light" || entity.kind === "environment") return "no-surface"
|
|
361
|
+
if (entity.placement.kind === "view") return "view-masked"
|
|
362
|
+
return "renderable"
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** The manifest-bearing asset an entity's lowered representation binds, if any. */
|
|
366
|
+
function entityAssetId(entity: SpatialEntity): string | undefined {
|
|
367
|
+
switch (entity.kind) {
|
|
368
|
+
case "mesh": return entity.geometry.kind === "asset" ? entity.geometry.assetId : entity.material.kind !== "pbr" ? entity.material.map : undefined
|
|
369
|
+
case "image": case "diagram": case "video": case "environment": return entity.assetId
|
|
370
|
+
case "text": return entity.fontAssetId
|
|
371
|
+
case "splat": return entity.assetId
|
|
372
|
+
default: return undefined
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
type Finding = SpatialRenderedAuditFinding
|
|
377
|
+
const findingOrder = (finding: Finding): string =>
|
|
378
|
+
`${finding.kind}:${finding.entityId ?? ""}:${String(finding.timeUs ?? -1).padStart(12, "0")}`
|
|
379
|
+
|
|
380
|
+
interface FrameDraft {
|
|
381
|
+
readonly timeUs: number
|
|
382
|
+
readonly width: number
|
|
383
|
+
readonly height: number
|
|
384
|
+
readonly pngSha256: string
|
|
385
|
+
/** selectionId → attributed entity pixels for every declared evidence code. */
|
|
386
|
+
readonly attributed: ReadonlyMap<number, number>
|
|
387
|
+
readonly lowered: ReadonlyMap<string, SpatialRenderedAuditObject>
|
|
388
|
+
readonly renderedPixels: number
|
|
389
|
+
readonly unattributedPixels: number
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Reconciles decoded object-ID frames against per-frame lowering evidence and
|
|
394
|
+
* the geometric audit's expectations. Pure and deterministic: the renderer,
|
|
395
|
+
* decoding and file IO all live in the host; this module sees only counts,
|
|
396
|
+
* evidence rows, and the parsed scene. Unsupported kinds are reported, never
|
|
397
|
+
* estimated; a splat with supplied bounds reports `proxy-coverage` — the proxy
|
|
398
|
+
* attribution is real while its drawn shape stays an approximation.
|
|
399
|
+
*/
|
|
400
|
+
export function auditSpatialSceneRendered(
|
|
401
|
+
sceneInput: unknown,
|
|
402
|
+
framesInput: unknown,
|
|
403
|
+
options: SpatialRenderedAuditOptions,
|
|
404
|
+
): SpatialRenderedAuditReport {
|
|
405
|
+
return auditSpatialSceneRenderedInContext(createSpatialEvaluationContext(sceneInput), framesInput, options)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The rendered audit against a shared evaluation context. The embedded
|
|
410
|
+
* geometric audit consumes the same context, so one parse and index pass
|
|
411
|
+
* covers frame reconciliation and every geometric sample.
|
|
412
|
+
*/
|
|
413
|
+
export function auditSpatialSceneRenderedInContext(
|
|
414
|
+
context: SpatialEvaluationContext,
|
|
415
|
+
framesInput: unknown,
|
|
416
|
+
options: SpatialRenderedAuditOptions,
|
|
417
|
+
): SpatialRenderedAuditReport {
|
|
418
|
+
const scene = context.scene
|
|
419
|
+
const captured = parseSpatialValue(SpatialRenderedAuditOptionsSchema, options, "rendered audit options")
|
|
420
|
+
const coverage = captured.coverage ?? SPATIAL_RENDERED_AUDIT_COVERAGE
|
|
421
|
+
const cameraId = captured.cameraId
|
|
422
|
+
if (!context.camerasById.has(cameraId)) {
|
|
423
|
+
throw new SpatialSceneError("not-found", `Unknown camera ${cameraId}.`, "cameraId")
|
|
424
|
+
}
|
|
425
|
+
const assetIds = new Set(scene.assets.map(asset => asset.assetId))
|
|
426
|
+
for (const assetId of Object.keys(captured.assetBounds ?? {})) {
|
|
427
|
+
if (!assetIds.has(assetId)) throw new SpatialSceneError("invalid-data", `Asset bounds reference unknown ${assetId}.`, "assetBounds")
|
|
428
|
+
}
|
|
429
|
+
const frames = parseSpatialValue(
|
|
430
|
+
z.array(SpatialRenderedAuditFrameSchema).min(1).max(SPATIAL_RENDERED_AUDIT_LIMITS.samples),
|
|
431
|
+
framesInput, "rendered audit frames",
|
|
432
|
+
)
|
|
433
|
+
const timeSet = new Set(frames.map(frame => frame.timeUs))
|
|
434
|
+
if (timeSet.size !== frames.length) {
|
|
435
|
+
throw new SpatialSceneError("invalid-data", "Rendered audit frames must have unique sample times.", "frames")
|
|
436
|
+
}
|
|
437
|
+
const timesUs = [...timeSet].sort((a, b) => a - b)
|
|
438
|
+
for (const timeUs of timesUs) {
|
|
439
|
+
if (timeUs > scene.durationUs) throw new SpatialSceneError("invalid-data", "Rendered audit sample time exceeds scene duration.", "frames")
|
|
440
|
+
}
|
|
441
|
+
if (scene.entities.length * timesUs.length > SPATIAL_RENDERED_AUDIT_LIMITS.entitySamples) {
|
|
442
|
+
throw new SpatialSceneError("invalid-data", "Rendered audit entity-sample budget exceeded; pass fewer frames.", "frames")
|
|
443
|
+
}
|
|
444
|
+
const first = frames[0]!
|
|
445
|
+
for (const frame of frames) {
|
|
446
|
+
if (frame.width !== first.width || frame.height !== first.height) {
|
|
447
|
+
throw new SpatialSceneError("invalid-data", "Rendered audit frames must share one calibrated dimension.", "frames")
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
const framePixels = first.width * first.height
|
|
451
|
+
|
|
452
|
+
// Frame evidence must be internally consistent with the parsed scene: the
|
|
453
|
+
// selection code is the canonical entity index and an asset digest must match
|
|
454
|
+
// the declared manifest closure.
|
|
455
|
+
const entityIndex = new Map(scene.entities.map((entity, index) => [entity.entityId, index + 1]))
|
|
456
|
+
const manifestDigests = context.assetDigests
|
|
457
|
+
const frameDrafts = new Map<number, FrameDraft>()
|
|
458
|
+
for (const frame of frames) {
|
|
459
|
+
const attributed = new Map<number, number>()
|
|
460
|
+
const lowered = new Map<string, SpatialRenderedAuditObject>()
|
|
461
|
+
for (const object of frame.objects) {
|
|
462
|
+
const entity = scene.entities.find(candidate => candidate.entityId === object.entityId)
|
|
463
|
+
if (entity === undefined) {
|
|
464
|
+
throw new SpatialSceneError("invalid-data", `Frame evidence names unknown entity ${object.entityId}.`, "frames")
|
|
465
|
+
}
|
|
466
|
+
if (object.selectionId !== entityIndex.get(object.entityId)!) {
|
|
467
|
+
throw new SpatialSceneError("invalid-data", `Frame evidence selection id differs from the canonical index for ${object.entityId}.`, "frames")
|
|
468
|
+
}
|
|
469
|
+
if (object.assetManifestSha256 !== undefined) {
|
|
470
|
+
const assetId = entityAssetId(entity)
|
|
471
|
+
if (assetId === undefined || manifestDigests[assetId] !== object.assetManifestSha256) {
|
|
472
|
+
throw new SpatialSceneError("invalid-data", `Frame evidence asset digest does not match the declared manifest for ${object.entityId}.`, "frames")
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (entity.kind === "splat") {
|
|
476
|
+
// A lowered splat is exactly its supplied-bounds box under the normal
|
|
477
|
+
// selection code; any other representation must not claim attribution.
|
|
478
|
+
if (object.representation !== SPATIAL_SPLAT_PROXY_REPRESENTATION || object.placement !== "world"
|
|
479
|
+
|| entity.placement.kind !== "world" || captured.assetBounds?.[entity.assetId] === undefined) {
|
|
480
|
+
throw new SpatialSceneError("invalid-data", `Frame evidence may lower splat ${object.entityId} only as its supplied-bounds bounding-box proxy.`, "frames")
|
|
481
|
+
}
|
|
482
|
+
} else if (object.representation === SPATIAL_SPLAT_PROXY_REPRESENTATION) {
|
|
483
|
+
throw new SpatialSceneError("invalid-data", `Frame evidence must not mark non-splat ${object.entityId} as a splat bounding-box proxy.`, "frames")
|
|
484
|
+
}
|
|
485
|
+
const declaredInstances = entity.kind === "mesh" && entity.instances !== undefined ? entity.instances.length : undefined
|
|
486
|
+
if (object.instances !== declaredInstances) {
|
|
487
|
+
throw new SpatialSceneError("invalid-data", `Frame evidence instance count differs from the authored declaration for ${object.entityId}.`, "frames")
|
|
488
|
+
}
|
|
489
|
+
lowered.set(object.entityId, object)
|
|
490
|
+
// The object-ID pass writes the declared selection code for every lowered
|
|
491
|
+
// surface, including view placement; only the depth pass masks view to
|
|
492
|
+
// no-hit. Codes without evidence remain unattributed.
|
|
493
|
+
attributed.set(object.selectionId, 0)
|
|
494
|
+
}
|
|
495
|
+
let renderedPixels = 0, unattributedPixels = 0
|
|
496
|
+
for (const [key, count] of Object.entries(frame.counts)) {
|
|
497
|
+
const code = Number(key)
|
|
498
|
+
if (!attributed.has(code)) { unattributedPixels += count; continue }
|
|
499
|
+
attributed.set(code, count)
|
|
500
|
+
renderedPixels += count
|
|
501
|
+
}
|
|
502
|
+
frameDrafts.set(frame.timeUs, {
|
|
503
|
+
timeUs: frame.timeUs, width: frame.width, height: frame.height, pngSha256: frame.pngSha256,
|
|
504
|
+
attributed, lowered, renderedPixels, unattributedPixels,
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// The geometric audit supplies effective visibility and corner-projection
|
|
509
|
+
// coverage; the rendered audit compares, never substitutes, its estimate.
|
|
510
|
+
const geometric = auditSpatialSceneInContext(context, {
|
|
511
|
+
cameraId, timesUs,
|
|
512
|
+
...(captured.assetBounds === undefined ? {} : { assetBounds: captured.assetBounds }),
|
|
513
|
+
})
|
|
514
|
+
const geometricEntities = new Map(geometric.entities.map(entity => [entity.entityId, entity]))
|
|
515
|
+
const geometricSamples = new Map(geometric.entities.map(entity => [
|
|
516
|
+
entity.entityId, new Map(entity.samples.map(sample => [sample.timeUs, sample])),
|
|
517
|
+
]))
|
|
518
|
+
|
|
519
|
+
const findings: Finding[] = []
|
|
520
|
+
const entities: SpatialRenderedAuditEntity[] = []
|
|
521
|
+
const entitiesNeverRendered: string[] = []
|
|
522
|
+
const entitiesUnsupported: string[] = []
|
|
523
|
+
const entitiesViewMasked: string[] = []
|
|
524
|
+
const entitiesProxyCoverage: string[] = []
|
|
525
|
+
const suppliedAssetBounds = captured.assetBounds ?? {}
|
|
526
|
+
const eligibilityCounts = { renderable: 0, "proxy-coverage": 0, "view-masked": 0, "no-surface": 0, "unsupported-kind": 0 }
|
|
527
|
+
for (const entity of scene.entities) {
|
|
528
|
+
const entityEligibility = eligibility(entity, suppliedAssetBounds)
|
|
529
|
+
eligibilityCounts[entityEligibility]++
|
|
530
|
+
if (entityEligibility === "unsupported-kind") entitiesUnsupported.push(entity.entityId)
|
|
531
|
+
if (entityEligibility === "view-masked") entitiesViewMasked.push(entity.entityId)
|
|
532
|
+
if (entityEligibility === "proxy-coverage") entitiesProxyCoverage.push(entity.entityId)
|
|
533
|
+
const selectionId = entityIndex.get(entity.entityId)!
|
|
534
|
+
const geometricEntity = geometricEntities.get(entity.entityId)!
|
|
535
|
+
const geoSamples = geometricSamples.get(entity.entityId)!
|
|
536
|
+
const samples: SpatialRenderedAuditSample[] = []
|
|
537
|
+
for (const timeUs of timesUs) {
|
|
538
|
+
const frame = frameDrafts.get(timeUs)!
|
|
539
|
+
const geo = geoSamples.get(timeUs)!
|
|
540
|
+
const evidence = frame.lowered.get(entity.entityId)
|
|
541
|
+
const expected = (entityEligibility === "renderable" || entityEligibility === "view-masked" || entityEligibility === "proxy-coverage")
|
|
542
|
+
&& geo.visible && geo.note !== "other-camera"
|
|
543
|
+
const lowered = evidence !== undefined
|
|
544
|
+
const pixels = evidence !== undefined ? frame.attributed.get(selectionId) ?? 0 : 0
|
|
545
|
+
const note: (typeof SAMPLE_NOTES)[number] | undefined = expected && !lowered ? "unlowered-expected" : geo.note
|
|
546
|
+
samples.push({
|
|
547
|
+
timeUs, expected, lowered, rendered: pixels > 0, pixels,
|
|
548
|
+
framePercent: round3(pixels * 100 / framePixels),
|
|
549
|
+
...(geo.frustum === undefined ? {} : { geometricPixels: geo.frustum.pixelFootprint }),
|
|
550
|
+
...(geo.frustum !== undefined && geo.frustum.pixelFootprint > 0
|
|
551
|
+
? { coverageRatio: round3(pixels / geo.frustum.pixelFootprint) } : {}),
|
|
552
|
+
...(note === undefined ? {} : { note }),
|
|
553
|
+
})
|
|
554
|
+
}
|
|
555
|
+
const totals = {
|
|
556
|
+
expected: samples.filter(sample => sample.expected).length,
|
|
557
|
+
lowered: samples.filter(sample => sample.lowered).length,
|
|
558
|
+
rendered: samples.filter(sample => sample.rendered).length,
|
|
559
|
+
pixels: samples.reduce((sum, sample) => sum + sample.pixels, 0),
|
|
560
|
+
maxPixels: samples.reduce((maximum, sample) => Math.max(maximum, sample.pixels), 0),
|
|
561
|
+
maxFramePercent: samples.reduce((maximum, sample) => Math.max(maximum, sample.framePercent), 0),
|
|
562
|
+
}
|
|
563
|
+
entities.push({
|
|
564
|
+
entityId: entity.entityId, name: entity.name, kind: entity.kind, placement: entity.placement,
|
|
565
|
+
eligibility: entityEligibility, selectionId, enclosure: geometricEntity.enclosure,
|
|
566
|
+
...(entity.kind === "mesh" && entity.instances !== undefined ? { instances: entity.instances.length } : {}),
|
|
567
|
+
samples, totals,
|
|
568
|
+
})
|
|
569
|
+
if (entityEligibility === "unsupported-kind") {
|
|
570
|
+
findings.push({
|
|
571
|
+
severity: "info", kind: "unsupported-kind", entityId: entity.entityId,
|
|
572
|
+
detail: entity.placement.kind === "world"
|
|
573
|
+
? "The object-ID pass cannot lower this splat's bounding-box proxy without decoded splat-position bounds; retained collider evidence is approximate, never pixel truth."
|
|
574
|
+
: "The object-ID pass cannot lower a view-placed splat; bounding-box proxies require world placement.",
|
|
575
|
+
})
|
|
576
|
+
if (geometricEntity.enclosure.status === "unknown" && geometricEntity.enclosure.reason !== "no-surface") {
|
|
577
|
+
findings.push({
|
|
578
|
+
severity: "info", kind: "bounds-unknown", entityId: entity.entityId,
|
|
579
|
+
detail: "Bounds require decoded asset data; supply assetBounds so the object-ID pass can lower a bounding-box proxy.",
|
|
580
|
+
})
|
|
581
|
+
}
|
|
582
|
+
continue
|
|
583
|
+
}
|
|
584
|
+
if (entityEligibility === "no-surface") continue
|
|
585
|
+
if (entityEligibility === "proxy-coverage") {
|
|
586
|
+
findings.push({
|
|
587
|
+
severity: "info", kind: "proxy-coverage", entityId: entity.entityId,
|
|
588
|
+
detail: "Object-ID coverage counts this entity's bounding-box proxy built from supplied splat-position bounds — approximate coverage, never splat pixel truth.",
|
|
589
|
+
})
|
|
590
|
+
}
|
|
591
|
+
if (entityEligibility === "view-masked"
|
|
592
|
+
&& entity.placement.kind === "view" && entity.placement.cameraId === cameraId && totals.lowered > 0) {
|
|
593
|
+
findings.push({
|
|
594
|
+
severity: "info", kind: "unsupported-kind", entityId: entity.entityId,
|
|
595
|
+
detail: "View surfaces draw after world content and write their own selection code; their pixels are attributed to the entity and can occlude world counts.",
|
|
596
|
+
})
|
|
597
|
+
}
|
|
598
|
+
if (entity.visible && totals.rendered === 0) {
|
|
599
|
+
entitiesNeverRendered.push(entity.entityId)
|
|
600
|
+
const covered = samples.filter(sample => sample.expected && (sample.geometricPixels ?? 0) > 0).length
|
|
601
|
+
const maximum = samples.reduce((value, sample) => Math.max(value, sample.geometricPixels ?? 0), 0)
|
|
602
|
+
findings.push({
|
|
603
|
+
severity: totals.expected === 0 ? "info" : "warning",
|
|
604
|
+
kind: "never-rendered", entityId: entity.entityId,
|
|
605
|
+
detail: totals.expected === 0
|
|
606
|
+
? "Authored visible but effectively invisible at every sampled time; zero rendered pixels is consistent."
|
|
607
|
+
: covered === 0
|
|
608
|
+
? `Effectively visible at ${String(totals.expected)} sampled time${totals.expected === 1 ? "" : "s"} but its geometric estimate never covered the frame; zero pixels is consistent.`
|
|
609
|
+
: `Zero pixels at every expected sample despite up to ${String(maximum)} px² of geometric coverage; fully occluded, masked, or below the coverage threshold.`,
|
|
610
|
+
})
|
|
611
|
+
} else if (totals.rendered > 0) {
|
|
612
|
+
const occluded = samples.filter(sample => sample.expected && (sample.geometricPixels ?? 0) > 0 && sample.pixels === 0).length
|
|
613
|
+
if (occluded > 0) {
|
|
614
|
+
findings.push({
|
|
615
|
+
severity: "info", kind: "occluded", entityId: entity.entityId,
|
|
616
|
+
detail: `Zero rendered pixels at ${String(occluded)} of ${String(totals.expected)} expected samples despite positive geometric coverage; occluded, masked, or below the coverage threshold.`,
|
|
617
|
+
})
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (geometricEntity.enclosure.status === "unknown" && totals.expected > 0 && geometricEntity.enclosure.reason !== "no-surface") {
|
|
621
|
+
findings.push({
|
|
622
|
+
severity: "info", kind: "bounds-unknown", entityId: entity.entityId,
|
|
623
|
+
detail: geometricEntity.enclosure.reason === "requires-asset-decoding"
|
|
624
|
+
? "Bounds require decoded asset data; supply assetBounds for a geometric footprint delta."
|
|
625
|
+
: "Text bounds require font layout; the rendered footprint has no geometric delta.",
|
|
626
|
+
})
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const frameReports = timesUs.map(timeUs => {
|
|
631
|
+
const frame = frameDrafts.get(timeUs)!
|
|
632
|
+
return {
|
|
633
|
+
timeUs, pngSha256: frame.pngSha256,
|
|
634
|
+
renderedPixels: frame.renderedPixels, unattributedPixels: frame.unattributedPixels,
|
|
635
|
+
loweredEntities: frame.lowered.size,
|
|
636
|
+
}
|
|
637
|
+
})
|
|
638
|
+
const renderedPixels = frameReports.reduce((sum, frame) => sum + frame.renderedPixels, 0)
|
|
639
|
+
const unattributedPixels = frameReports.reduce((sum, frame) => sum + frame.unattributedPixels, 0)
|
|
640
|
+
if (unattributedPixels > 0) {
|
|
641
|
+
const framesWithUnattributed = frameReports.filter(frame => frame.unattributedPixels > 0).length
|
|
642
|
+
findings.push({
|
|
643
|
+
severity: "warning", kind: "unattributed-pixels",
|
|
644
|
+
detail: `${String(unattributedPixels)} pixel${unattributedPixels === 1 ? "" : "s"} across ${String(framesWithUnattributed)} frame${framesWithUnattributed === 1 ? "" : "s"} carried selection codes absent from attributable evidence.`,
|
|
645
|
+
})
|
|
646
|
+
}
|
|
647
|
+
if (renderedPixels === 0) {
|
|
648
|
+
const expectedSamples = entities.reduce((sum, entity) => sum + entity.totals.expected, 0)
|
|
649
|
+
findings.push({
|
|
650
|
+
severity: "warning", kind: "empty-render",
|
|
651
|
+
detail: `No attributed object-ID pixel appeared in any sampled frame (${String(expectedSamples)} expected entity-samples).`,
|
|
652
|
+
})
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const sortedFindings = sortSpatialBy(findings, findingOrder)
|
|
656
|
+
const retainedFindings = sortedFindings.slice(0, SPATIAL_RENDERED_AUDIT_LIMITS.findings)
|
|
657
|
+
const report = {
|
|
658
|
+
kind: "slopcamera.spatial-rendered-audit" as const, schemaVersion: 1 as const,
|
|
659
|
+
sceneId: scene.sceneId, sceneSha256: context.sceneSha256, cameraId,
|
|
660
|
+
durationUs: scene.durationUs, timesUs,
|
|
661
|
+
mode: { kind: "object-id" as const, coverage },
|
|
662
|
+
frame: { width: first.width, height: first.height, pixels: framePixels },
|
|
663
|
+
summary: {
|
|
664
|
+
entities: {
|
|
665
|
+
total: scene.entities.length,
|
|
666
|
+
renderable: eligibilityCounts.renderable,
|
|
667
|
+
proxyCoverage: eligibilityCounts["proxy-coverage"],
|
|
668
|
+
viewMasked: eligibilityCounts["view-masked"],
|
|
669
|
+
noSurface: eligibilityCounts["no-surface"],
|
|
670
|
+
unsupported: eligibilityCounts["unsupported-kind"],
|
|
671
|
+
},
|
|
672
|
+
entitiesNeverRendered: sortSpatialBy(entitiesNeverRendered, id => id),
|
|
673
|
+
entitiesUnsupported: sortSpatialBy(entitiesUnsupported, id => id),
|
|
674
|
+
entitiesViewMasked: sortSpatialBy(entitiesViewMasked, id => id),
|
|
675
|
+
entitiesProxyCoverage: sortSpatialBy(entitiesProxyCoverage, id => id),
|
|
676
|
+
renderedPixels, unattributedPixels,
|
|
677
|
+
},
|
|
678
|
+
entities, frames: frameReports,
|
|
679
|
+
findings: retainedFindings,
|
|
680
|
+
omittedFindings: sortedFindings.length - retainedFindings.length,
|
|
681
|
+
}
|
|
682
|
+
const parsed: SpatialRenderedAuditReport = SpatialRenderedAuditReportSchema.parse(report)
|
|
683
|
+
try {
|
|
684
|
+
createBoundedJsonValueSnapshot(parsed, SPATIAL_RENDERED_AUDIT_LIMITS.reportBytes, "rendered audit report", {
|
|
685
|
+
maximumDepth: SPATIAL_SCENE_LIMITS.sourceDepth + 8,
|
|
686
|
+
maximumValues: SPATIAL_SCENE_LIMITS.sourceValues + SPATIAL_RENDERED_AUDIT_LIMITS.entitySamples * 16,
|
|
687
|
+
})
|
|
688
|
+
} catch (error) {
|
|
689
|
+
throw new SpatialSceneError("invalid-data", error instanceof Error ? error.message : "Rendered audit report exceeds its bounded size.", "rendered audit")
|
|
690
|
+
}
|
|
691
|
+
return deepFreezeJson(parsed)
|
|
692
|
+
}
|