Govern task context and advance execution pointer
This commit is contained in:
@@ -377,6 +377,68 @@ export interface VFontResourceIR {
|
||||
sha256?: string;
|
||||
}
|
||||
|
||||
export interface SoundResourceIR {
|
||||
id: string;
|
||||
name: string;
|
||||
sourcePath: string;
|
||||
packed: boolean;
|
||||
packedByteLength?: number;
|
||||
volume: number;
|
||||
pitch: number;
|
||||
audioChannels: number;
|
||||
sampleRate: number;
|
||||
}
|
||||
|
||||
export interface SpeakerResourceIR {
|
||||
id: string;
|
||||
name: string;
|
||||
soundId?: string;
|
||||
volumeMax: number;
|
||||
volumeMin: number;
|
||||
distanceMax: number;
|
||||
distanceReference: number;
|
||||
attenuation: number;
|
||||
coneAngleOuter: number;
|
||||
coneAngleInner: number;
|
||||
coneVolumeOuter: number;
|
||||
volume: number;
|
||||
pitch: number;
|
||||
}
|
||||
|
||||
export interface TextResourceIR {
|
||||
id: string;
|
||||
name: string;
|
||||
source: string;
|
||||
sourceSha256: string;
|
||||
byteLength: number;
|
||||
lineCount: number;
|
||||
internal: boolean;
|
||||
sourcePath?: string;
|
||||
isDirty: boolean;
|
||||
useModule: boolean;
|
||||
}
|
||||
|
||||
export interface TextureResourceIR {
|
||||
id: string;
|
||||
name: string;
|
||||
type: number;
|
||||
noiseScale: number;
|
||||
noiseDepth: number;
|
||||
intensity: number;
|
||||
contrast: number;
|
||||
saturation: number;
|
||||
}
|
||||
|
||||
export interface WindowManagerResourceIR {
|
||||
id: string;
|
||||
name: string;
|
||||
presetName: string;
|
||||
windowCount: number;
|
||||
interfaceLocked: boolean;
|
||||
}
|
||||
|
||||
export interface WorkspaceResourceIR { id: string; name: string; screenCount: number }
|
||||
|
||||
export interface NonMeshVolumePropertiesIR {
|
||||
displayDensity: number;
|
||||
interpolation: "NEAREST" | "LINEAR";
|
||||
@@ -488,6 +550,9 @@ export interface SceneSnapshotIR {
|
||||
lights: LightIR[];
|
||||
worlds: WorldIR[];
|
||||
images: ImageIR[];
|
||||
brushes?: BrushIR[];
|
||||
lineStyles?: FreestyleLineStyleIR[];
|
||||
lattices?: LatticeIR[];
|
||||
nonMeshData?: NonMeshDataIR[];
|
||||
vfonts?: VFontResourceIR[];
|
||||
greasePencils?: GreasePencilDataIR[];
|
||||
@@ -499,6 +564,13 @@ export interface SceneSnapshotIR {
|
||||
scriptSources?: ScriptSourceInventoryIR;
|
||||
scriptSourceStatus?: "AVAILABLE" | "BLOCKED";
|
||||
physicsSimulation?: PhysicsSimulationManifestIR;
|
||||
particleSettings?: ParticleSettingsIR[];
|
||||
sounds?: SoundResourceIR[];
|
||||
speakers?: SpeakerResourceIR[];
|
||||
texts?: TextResourceIR[];
|
||||
textures?: TextureResourceIR[];
|
||||
windowManager?: WindowManagerResourceIR;
|
||||
workspaces?: WorkspaceResourceIR[];
|
||||
geometryNodeGraphs?: GeometryNodeGraphIR[];
|
||||
libraries?: Array<{
|
||||
id: string;
|
||||
@@ -520,6 +592,66 @@ export interface SceneSnapshotIR {
|
||||
frame: { current: number; start: number; end: number };
|
||||
}
|
||||
|
||||
export interface ParticleSettingsIR {
|
||||
id: string;
|
||||
name: string;
|
||||
type: number;
|
||||
from: number;
|
||||
distribution: number;
|
||||
physicsType: number;
|
||||
totalParticles: number;
|
||||
start: number;
|
||||
end: number;
|
||||
lifetime: number;
|
||||
size: number;
|
||||
drawSize: number;
|
||||
}
|
||||
|
||||
export interface BrushIR {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
alpha: number;
|
||||
hardness: number;
|
||||
spacing: number;
|
||||
jitter: number;
|
||||
sculptTool: number;
|
||||
}
|
||||
|
||||
export interface FreestyleLineStyleIR {
|
||||
id: string;
|
||||
name: string;
|
||||
color: [number, number, number];
|
||||
alpha: number;
|
||||
thickness: number;
|
||||
chaining: number;
|
||||
caps: number;
|
||||
dashed: boolean;
|
||||
modifierCounts: { color: number; alpha: number; thickness: number; geometry: number };
|
||||
}
|
||||
|
||||
export type LatticeInterpolationIR = "KEY_LINEAR" | "KEY_CARDINAL" | "KEY_BSPLINE" | "KEY_CATMULL_ROM";
|
||||
|
||||
export interface LatticePointIR {
|
||||
coDeform: [number, number, number];
|
||||
weight: number;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
export interface LatticeIR {
|
||||
id: string;
|
||||
name: string;
|
||||
dimensions: [number, number, number];
|
||||
pointCount: number;
|
||||
interpolation: [LatticeInterpolationIR, LatticeInterpolationIR, LatticeInterpolationIR];
|
||||
useOutside: boolean;
|
||||
activePoint: number;
|
||||
vertexGroup: string;
|
||||
status: "AVAILABLE" | "BLOCKED";
|
||||
points?: LatticePointIR[];
|
||||
errorCode?: "LATTICE_DIMENSIONS_INVALID" | "LATTICE_POINTS_UNREADABLE";
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
@@ -564,7 +696,218 @@ export function parseSceneSnapshotIR(value: unknown): SceneSnapshotIR {
|
||||
requireArray(value.lights, "lights");
|
||||
requireArray(value.worlds, "worlds");
|
||||
requireArray(value.images, "images");
|
||||
if (value.brushes !== undefined) {
|
||||
const brushes = requireArray(value.brushes, "brushes");
|
||||
for (const [index, brush] of brushes.entries()) {
|
||||
if (!isRecord(brush)) throw new Error(`SceneIR.brushes[${index}] must be an object`);
|
||||
requireString(brush.id, `brushes[${index}].id`);
|
||||
requireString(brush.name, `brushes[${index}].name`);
|
||||
const size = requireNumber(brush.size, `brushes[${index}].size`);
|
||||
const alpha = requireNumber(brush.alpha, `brushes[${index}].alpha`);
|
||||
const hardness = requireNumber(brush.hardness, `brushes[${index}].hardness`);
|
||||
const spacing = requireNumber(brush.spacing, `brushes[${index}].spacing`);
|
||||
const jitter = requireNumber(brush.jitter, `brushes[${index}].jitter`);
|
||||
const sculptTool = requireNumber(brush.sculptTool, `brushes[${index}].sculptTool`);
|
||||
if (!Number.isSafeInteger(size) || size < 0 || size > 100000 || alpha < 0 || alpha > 1 || hardness < 0 || hardness > 1 || !Number.isSafeInteger(spacing) || spacing < 1 || spacing > 1000 || jitter < 0 || jitter > 1 || !Number.isSafeInteger(sculptTool) || sculptTool < 0) {
|
||||
throw new Error(`SceneIR.brushes[${index}] is outside the bounded brush contract`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value.lineStyles !== undefined) {
|
||||
const lineStyles = requireArray(value.lineStyles, "lineStyles");
|
||||
for (const [index, lineStyle] of lineStyles.entries()) {
|
||||
if (!isRecord(lineStyle)) throw new Error(`SceneIR.lineStyles[${index}] must be an object`);
|
||||
requireString(lineStyle.id, `lineStyles[${index}].id`);
|
||||
requireString(lineStyle.name, `lineStyles[${index}].name`);
|
||||
requireTuple(lineStyle.color, 3, `lineStyles[${index}].color`);
|
||||
const alpha = requireNumber(lineStyle.alpha, `lineStyles[${index}].alpha`);
|
||||
const thickness = requireNumber(lineStyle.thickness, `lineStyles[${index}].thickness`);
|
||||
requireNumber(lineStyle.chaining, `lineStyles[${index}].chaining`);
|
||||
requireNumber(lineStyle.caps, `lineStyles[${index}].caps`);
|
||||
requireBoolean(lineStyle.dashed, `lineStyles[${index}].dashed`);
|
||||
if (!isRecord(lineStyle.modifierCounts)) throw new Error(`SceneIR.lineStyles[${index}].modifierCounts must be an object`);
|
||||
for (const field of ["color", "alpha", "thickness", "geometry"] as const) {
|
||||
const count = requireNumber(lineStyle.modifierCounts[field], `lineStyles[${index}].modifierCounts.${field}`);
|
||||
if (!Number.isSafeInteger(count) || count < 0 || count > 1024) throw new Error(`SceneIR.lineStyles[${index}].modifierCounts.${field} is outside budget`);
|
||||
}
|
||||
if (alpha < 0 || alpha > 1 || thickness < 0 || thickness > 10000) throw new Error(`SceneIR.lineStyles[${index}] is outside bounds`);
|
||||
}
|
||||
}
|
||||
if (value.lattices !== undefined) {
|
||||
const lattices = requireArray(value.lattices, "lattices");
|
||||
if (lattices.length > 4096) throw new Error("SceneIR.lattices exceeds the data-block budget");
|
||||
const ids = new Set<string>();
|
||||
for (const [index, lattice] of lattices.entries()) {
|
||||
if (!isRecord(lattice)) throw new Error(`SceneIR.lattices[${index}] must be an object`);
|
||||
const id = requireString(lattice.id, `lattices[${index}].id`);
|
||||
if (!id.startsWith("lattice:") || ids.has(id)) throw new Error(`SceneIR.lattices[${index}].id is invalid`);
|
||||
ids.add(id);
|
||||
requireString(lattice.name, `lattices[${index}].name`);
|
||||
if (!Array.isArray(lattice.dimensions) || lattice.dimensions.length !== 3 || lattice.dimensions.some((item) => !Number.isSafeInteger(item) || item < 1 || item > 64)) throw new Error(`SceneIR.lattices[${index}].dimensions is invalid`);
|
||||
const pointCount = requireNumber(lattice.pointCount, `lattices[${index}].pointCount`);
|
||||
const expectedCount = (lattice.dimensions as number[]).reduce((product, item) => product * item, 1);
|
||||
if (!Number.isSafeInteger(pointCount) || pointCount !== expectedCount || pointCount > 64 * 64 * 64) throw new Error(`SceneIR.lattices[${index}].pointCount is invalid`);
|
||||
const interpolation = ["KEY_LINEAR", "KEY_CARDINAL", "KEY_BSPLINE", "KEY_CATMULL_ROM"];
|
||||
if (!Array.isArray(lattice.interpolation) || lattice.interpolation.length !== 3 || lattice.interpolation.some((item) => !interpolation.includes(item as string))) throw new Error(`SceneIR.lattices[${index}].interpolation is invalid`);
|
||||
requireBoolean(lattice.useOutside, `lattices[${index}].useOutside`);
|
||||
const activePoint = requireNumber(lattice.activePoint, `lattices[${index}].activePoint`);
|
||||
if (!Number.isSafeInteger(activePoint) || activePoint < -1 || activePoint >= pointCount) throw new Error(`SceneIR.lattices[${index}].activePoint is invalid`);
|
||||
requireString(lattice.vertexGroup, `lattices[${index}].vertexGroup`);
|
||||
if (lattice.status !== "AVAILABLE" && lattice.status !== "BLOCKED") throw new Error(`SceneIR.lattices[${index}].status is invalid`);
|
||||
if (lattice.status === "AVAILABLE") {
|
||||
if (!Array.isArray(lattice.points) || lattice.points.length !== pointCount) throw new Error(`SceneIR.lattices[${index}].points is incomplete`);
|
||||
for (const [pointIndex, point] of lattice.points.entries()) {
|
||||
if (!isRecord(point)) throw new Error(`SceneIR.lattices[${index}].points[${pointIndex}] is invalid`);
|
||||
requireTuple(point.coDeform, 3, `lattices[${index}].points[${pointIndex}].coDeform`);
|
||||
const weight = requireNumber(point.weight, `lattices[${index}].points[${pointIndex}].weight`);
|
||||
if (weight < 0.01 || weight > 100) throw new Error(`SceneIR.lattices[${index}].points[${pointIndex}].weight is invalid`);
|
||||
requireBoolean(point.selected, `lattices[${index}].points[${pointIndex}].selected`);
|
||||
}
|
||||
if (lattice.errorCode !== undefined) throw new Error(`SceneIR.lattices[${index}] available data cannot contain errorCode`);
|
||||
}
|
||||
else if (!lattice.errorCode || lattice.points !== undefined) {
|
||||
throw new Error(`SceneIR.lattices[${index}] blocked data is incomplete`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value.nonMeshData !== undefined) requireArray(value.nonMeshData, "nonMeshData");
|
||||
if (value.particleSettings !== undefined) {
|
||||
const settings = requireArray(value.particleSettings, "particleSettings");
|
||||
if (settings.length > 4096) throw new Error("SceneIR.particleSettings exceeds the data-block budget");
|
||||
const ids = new Set<string>();
|
||||
for (const [index, setting] of settings.entries()) {
|
||||
if (!isRecord(setting)) throw new Error(`SceneIR.particleSettings[${index}] must be an object`);
|
||||
const id = requireString(setting.id, `particleSettings[${index}].id`);
|
||||
if (!id.startsWith("particle-settings:") || ids.has(id)) throw new Error(`SceneIR.particleSettings[${index}].id is invalid`);
|
||||
ids.add(id);
|
||||
requireString(setting.name, `particleSettings[${index}].name`);
|
||||
for (const field of ["type", "from", "distribution", "physicsType", "totalParticles"] as const) {
|
||||
const number = requireNumber(setting[field], `particleSettings[${index}].${field}`);
|
||||
if (!Number.isSafeInteger(number) || number < 0) throw new Error(`SceneIR.particleSettings[${index}].${field} is invalid`);
|
||||
}
|
||||
for (const field of ["start", "end", "lifetime", "size", "drawSize"] as const) {
|
||||
const number = requireNumber(setting[field], `particleSettings[${index}].${field}`);
|
||||
if (number < 0 || number > 1_000_000) throw new Error(`SceneIR.particleSettings[${index}].${field} is invalid`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value.sounds !== undefined) {
|
||||
const sounds = requireArray(value.sounds, "sounds");
|
||||
if (sounds.length > 4096) throw new Error("SceneIR.sounds exceeds the data-block budget");
|
||||
const ids = new Set<string>();
|
||||
for (const [index, sound] of sounds.entries()) {
|
||||
if (!isRecord(sound)) throw new Error(`SceneIR.sounds[${index}] must be an object`);
|
||||
const id = requireString(sound.id, `sounds[${index}].id`);
|
||||
if (!id.startsWith("sound:") || ids.has(id)) throw new Error(`SceneIR.sounds[${index}].id is invalid`);
|
||||
ids.add(id);
|
||||
requireString(sound.name, `sounds[${index}].name`);
|
||||
requireString(sound.sourcePath, `sounds[${index}].sourcePath`);
|
||||
requireBoolean(sound.packed, `sounds[${index}].packed`);
|
||||
for (const field of ["volume", "pitch"] as const) {
|
||||
const number = requireNumber(sound[field], `sounds[${index}].${field}`);
|
||||
if (number < 0 || number > 1000) throw new Error(`SceneIR.sounds[${index}].${field} is outside bounds`);
|
||||
}
|
||||
for (const field of ["audioChannels", "sampleRate"] as const) {
|
||||
const number = requireNumber(sound[field], `sounds[${index}].${field}`);
|
||||
if (!Number.isSafeInteger(number) || number < 0 || number > 1_000_000) throw new Error(`SceneIR.sounds[${index}].${field} is invalid`);
|
||||
}
|
||||
if (sound.packedByteLength !== undefined && (!Number.isSafeInteger(sound.packedByteLength) || (sound.packedByteLength as number) < 0)) throw new Error(`SceneIR.sounds[${index}].packedByteLength is invalid`);
|
||||
if (sound.packed && sound.packedByteLength === undefined) throw new Error(`SceneIR.sounds[${index}] packed identity is incomplete`);
|
||||
}
|
||||
}
|
||||
if (value.speakers !== undefined) {
|
||||
const speakers = requireArray(value.speakers, "speakers");
|
||||
if (speakers.length > 4096) throw new Error("SceneIR.speakers exceeds the data-block budget");
|
||||
const ids = new Set<string>();
|
||||
for (const [index, speaker] of speakers.entries()) {
|
||||
if (!isRecord(speaker)) throw new Error(`SceneIR.speakers[${index}] must be an object`);
|
||||
const id = requireString(speaker.id, `speakers[${index}].id`);
|
||||
if (!id.startsWith("speaker:") || ids.has(id)) throw new Error(`SceneIR.speakers[${index}].id is invalid`);
|
||||
ids.add(id);
|
||||
requireString(speaker.name, `speakers[${index}].name`);
|
||||
if (speaker.soundId !== undefined && (!Array.isArray(value.sounds) || typeof speaker.soundId !== "string" || !speaker.soundId.startsWith("sound:"))) {
|
||||
throw new Error(`SceneIR.speakers[${index}].soundId is invalid`);
|
||||
}
|
||||
for (const field of ["volumeMax", "volumeMin", "distanceMax", "distanceReference", "attenuation"] as const) {
|
||||
const number = requireNumber(speaker[field], `speakers[${index}].${field}`);
|
||||
if (number < 0 || number > 1_000_000_000) throw new Error(`SceneIR.speakers[${index}].${field} is outside bounds`);
|
||||
}
|
||||
for (const field of ["coneAngleOuter", "coneAngleInner"] as const) {
|
||||
const number = requireNumber(speaker[field], `speakers[${index}].${field}`);
|
||||
if (number < 0 || number > 360) throw new Error(`SceneIR.speakers[${index}].${field} is outside bounds`);
|
||||
}
|
||||
const coneVolumeOuter = requireNumber(speaker.coneVolumeOuter, `speakers[${index}].coneVolumeOuter`);
|
||||
if (coneVolumeOuter < 0 || coneVolumeOuter > 1) throw new Error(`SceneIR.speakers[${index}].coneVolumeOuter is outside bounds`);
|
||||
for (const field of ["volume", "pitch"] as const) {
|
||||
const number = requireNumber(speaker[field], `speakers[${index}].${field}`);
|
||||
if (number < 0 || number > 1000) throw new Error(`SceneIR.speakers[${index}].${field} is outside bounds`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value.texts !== undefined) {
|
||||
const texts = requireArray(value.texts, "texts");
|
||||
if (texts.length > 4096) throw new Error("SceneIR.texts exceeds the data-block budget");
|
||||
const ids = new Set<string>();
|
||||
for (const [index, text] of texts.entries()) {
|
||||
if (!isRecord(text)) throw new Error(`SceneIR.texts[${index}] must be an object`);
|
||||
const id = requireString(text.id, `texts[${index}].id`);
|
||||
if (!id.startsWith("text:") || ids.has(id)) throw new Error(`SceneIR.texts[${index}].id is invalid`);
|
||||
ids.add(id);
|
||||
requireString(text.name, `texts[${index}].name`);
|
||||
const source = requireString(text.source, `texts[${index}].source`);
|
||||
const sourceSha256 = requireString(text.sourceSha256, `texts[${index}].sourceSha256`);
|
||||
if (!/^[a-f0-9]{64}$/.test(sourceSha256)) throw new Error(`SceneIR.texts[${index}].sourceSha256 is invalid`);
|
||||
const byteLength = requireNumber(text.byteLength, `texts[${index}].byteLength`);
|
||||
const lineCount = requireNumber(text.lineCount, `texts[${index}].lineCount`);
|
||||
if (!Number.isSafeInteger(byteLength) || byteLength !== new TextEncoder().encode(source).byteLength || byteLength > 1_048_576) throw new Error(`SceneIR.texts[${index}].byteLength is invalid`);
|
||||
if (!Number.isSafeInteger(lineCount) || lineCount < 1 || lineCount > 65_536) throw new Error(`SceneIR.texts[${index}].lineCount is invalid`);
|
||||
requireBoolean(text.internal, `texts[${index}].internal`);
|
||||
if (text.sourcePath !== undefined) requireString(text.sourcePath, `texts[${index}].sourcePath`);
|
||||
requireBoolean(text.isDirty, `texts[${index}].isDirty`);
|
||||
requireBoolean(text.useModule, `texts[${index}].useModule`);
|
||||
}
|
||||
}
|
||||
if (value.textures !== undefined) {
|
||||
const textures = requireArray(value.textures, "textures");
|
||||
if (textures.length > 4096) throw new Error("SceneIR.textures exceeds the data-block budget");
|
||||
const ids = new Set<string>();
|
||||
for (const [index, texture] of textures.entries()) {
|
||||
if (!isRecord(texture)) throw new Error(`SceneIR.textures[${index}] must be an object`);
|
||||
const id = requireString(texture.id, `textures[${index}].id`);
|
||||
if (!id.startsWith("texture:") || ids.has(id)) throw new Error(`SceneIR.textures[${index}].id is invalid`);
|
||||
ids.add(id);
|
||||
requireString(texture.name, `textures[${index}].name`);
|
||||
const type = requireNumber(texture.type, `textures[${index}].type`);
|
||||
const noiseDepth = requireNumber(texture.noiseDepth, `textures[${index}].noiseDepth`);
|
||||
if (!Number.isSafeInteger(type) || type < 0 || !Number.isSafeInteger(noiseDepth) || noiseDepth < 0 || noiseDepth > 30) throw new Error(`SceneIR.textures[${index}] integer fields are invalid`);
|
||||
for (const field of ["noiseScale", "intensity", "contrast", "saturation"] as const) {
|
||||
const number = requireNumber(texture[field], `textures[${index}].${field}`);
|
||||
if (number < 0 || number > 1000) throw new Error(`SceneIR.textures[${index}].${field} is outside bounds`);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (value.windowManager !== undefined) {
|
||||
if (!isRecord(value.windowManager)) throw new Error("SceneIR.windowManager must be an object");
|
||||
const manager = value.windowManager;
|
||||
const id = requireString(manager.id, "windowManager.id");
|
||||
if (id !== "window-manager:WinMan") throw new Error("SceneIR.windowManager.id is invalid");
|
||||
requireString(manager.name, "windowManager.name");
|
||||
requireString(manager.presetName, "windowManager.presetName");
|
||||
const windowCount = requireNumber(manager.windowCount, "windowManager.windowCount");
|
||||
if (!Number.isSafeInteger(windowCount) || windowCount < 0 || windowCount > 1024) throw new Error("SceneIR.windowManager.windowCount is invalid");
|
||||
requireBoolean(manager.interfaceLocked, "windowManager.interfaceLocked");
|
||||
}
|
||||
if (value.workspaces !== undefined) {
|
||||
const workspaces = requireArray(value.workspaces, "workspaces");
|
||||
const ids = new Set<string>();
|
||||
for (const [index, workspace] of workspaces.entries()) {
|
||||
if (!isRecord(workspace)) throw new Error(`SceneIR.workspaces[${index}] must be an object`);
|
||||
const id = requireString(workspace.id, `workspaces[${index}].id`);
|
||||
if (!id.startsWith("workspace:") || ids.has(id)) throw new Error(`SceneIR.workspaces[${index}].id is invalid`); ids.add(id);
|
||||
requireString(workspace.name, `workspaces[${index}].name`); const count = requireNumber(workspace.screenCount, `workspaces[${index}].screenCount`);
|
||||
if (!Number.isSafeInteger(count) || count < 0 || count > 64) throw new Error(`SceneIR.workspaces[${index}].screenCount is invalid`);
|
||||
}
|
||||
}
|
||||
if (value.greasePencils !== undefined) requireArray(value.greasePencils, "greasePencils");
|
||||
if (value.libraries !== undefined) requireArray(value.libraries, "libraries");
|
||||
requireArray(value.animations, "animations");
|
||||
|
||||
Reference in New Issue
Block a user