Advance Blender WebEngine N-015 through N-022 parity
This commit is contained in:
@@ -1,15 +1,65 @@
|
||||
import { gateSelectionInteraction, parseRaycastSelectionHit, recordSelection, SELECTION_HISTORY_BUDGET, stepSelectionHistory } from "../../../protocol/selection-history";
|
||||
import {
|
||||
gateSelectionInteraction,
|
||||
parseRaycastSelectionHit,
|
||||
parseSelectionHistory,
|
||||
patchSelectionRanges,
|
||||
recordSelection,
|
||||
SELECTION_HISTORY_BUDGET,
|
||||
stepSelectionHistory,
|
||||
} from "../../../protocol/selection-history";
|
||||
|
||||
const empty = { activeObjectId: null, objectIds: [], targets: [] };
|
||||
const selected = {
|
||||
activeObjectId: "object:1",
|
||||
objectIds: ["object:1", "object:2"],
|
||||
targets: [
|
||||
{ objectId: "object:1", dataId: "curve:1", mode: "VERT", nonMeshKind: "HANDLE_LEFT", indices: [3, 1] },
|
||||
{ objectId: "object:2", dataId: "curve:2", mode: "VERT", nonMeshKind: "HANDLE_RIGHT", indices: [2] },
|
||||
],
|
||||
};
|
||||
const base = { schemaVersion: 2, revision: 0, cursor: 0, entries: [empty] };
|
||||
|
||||
const empty = { activeObjectId: null, objectIds: [], meshId: null, elementMode: "FACE", elementIndices: [] };
|
||||
const selected = { activeObjectId: "object:1", objectIds: ["object:1"], meshId: "mesh:1", elementMode: "VERT", elementIndices: [3, 1] };
|
||||
const base = { schemaVersion: 1, revision: 0, cursor: 0, entries: [empty] };
|
||||
self.onmessage = () => {
|
||||
const result: Record<string, unknown> = {};
|
||||
try { const next = recordSelection(base, 0, { ...selected, nonMeshKind: "HANDLE_LEFT" }); const undone = stepSelectionHistory(next, 1, "UNDO"); const redone = stepSelectionHistory(undone, 2, "REDO"); result.history = [redone.revision, redone.cursor, redone.entries[redone.cursor].elementIndices, redone.entries[redone.cursor].nonMeshKind]; } catch (error) { result.history = error instanceof Error ? error.message : String(error); }
|
||||
try { recordSelection(base, 4, selected); } catch (error) { result.revision = error instanceof Error ? error.message : String(error); }
|
||||
try { recordSelection(base, 0, { ...selected, elementIndices: new Array(SELECTION_HISTORY_BUDGET.maxElements + 1).fill(1) }); } catch (error) { result.budget = error instanceof Error ? error.message : String(error); }
|
||||
try { parseRaycastSelectionHit({ sourceRevision: 2, dataId: "mesh:1", mode: "VERT", index: 0, distance: 1, point: [0, 0, 0] }, 3); } catch (error) { result.raycast = error instanceof Error ? error.message : String(error); }
|
||||
try { result.handleHit = parseRaycastSelectionHit({ sourceRevision: 3, dataId: "curve:1", mode: "VERT", index: 2, distance: 1, point: [0, 0, 0], nonMeshKind: "HANDLE_RIGHT" }, 3).nonMeshKind; } catch (error) { result.handleHit = error instanceof Error ? error.message : String(error); }
|
||||
try {
|
||||
const next = recordSelection(base, 0, selected);
|
||||
const undone = stepSelectionHistory(next, 1, "UNDO");
|
||||
const redone = stepSelectionHistory(undone, 2, "REDO");
|
||||
result.history = [redone.revision, redone.cursor, redone.entries[redone.cursor].targets.map((target) => [target.dataId, target.indices, target.nonMeshKind])];
|
||||
}
|
||||
catch (error) { result.history = error instanceof Error ? error.message : String(error); }
|
||||
try { recordSelection(base, 4, selected); }
|
||||
catch (error) { result.revision = error instanceof Error ? error.message : String(error); }
|
||||
try {
|
||||
recordSelection(base, 0, { ...selected, targets: [{ ...selected.targets[0], indices: new Array(SELECTION_HISTORY_BUDGET.maxElements + 1).fill(1) }] });
|
||||
}
|
||||
catch (error) { result.budget = error instanceof Error ? error.message : String(error); }
|
||||
try { parseRaycastSelectionHit({ sourceRevision: 2, dataId: "mesh:1", mode: "VERT", index: 0, distance: 1, point: [0, 0, 0] }, 3); }
|
||||
catch (error) { result.raycast = error instanceof Error ? error.message : String(error); }
|
||||
try {
|
||||
const hit = parseRaycastSelectionHit({ sourceRevision: 3, objectId: "object:1", dataId: "curve:1", mode: "VERT", index: 2, distance: 1, point: [0, 0, 0], nonMeshKind: "HANDLE_RIGHT" }, 3);
|
||||
result.handleHit = [hit.objectId, hit.nonMeshKind];
|
||||
}
|
||||
catch (error) { result.handleHit = error instanceof Error ? error.message : String(error); }
|
||||
try {
|
||||
const patched = patchSelectionRanges(selected, [
|
||||
{ objectId: "object:1", dataId: "curve:1", mode: "VERT", nonMeshKind: "HANDLE_LEFT", start: 2, end: 4, selected: true },
|
||||
{ objectId: "object:1", dataId: "curve:1", mode: "VERT", nonMeshKind: "HANDLE_LEFT", start: 3, end: 3, selected: false },
|
||||
{ objectId: "object:2", dataId: "curve:2", mode: "VERT", nonMeshKind: "HANDLE_RIGHT", start: 2, end: 2, selected: false },
|
||||
]);
|
||||
result.rangePatch = patched.targets.map((target) => [target.dataId, target.indices]);
|
||||
}
|
||||
catch (error) { result.rangePatch = error instanceof Error ? error.message : String(error); }
|
||||
try {
|
||||
const migrated = parseSelectionHistory({
|
||||
schemaVersion: 1,
|
||||
revision: 0,
|
||||
cursor: 0,
|
||||
entries: [{ activeObjectId: "object:1", objectIds: ["object:1"], meshId: "mesh:1", elementMode: "FACE", elementIndices: [4] }],
|
||||
});
|
||||
result.migrated = [migrated.schemaVersion, migrated.entries[0].targets[0].dataId];
|
||||
}
|
||||
catch (error) { result.migrated = error instanceof Error ? error.message : String(error); }
|
||||
result.gates = [gateSelectionInteraction("RAYCAST").status, gateSelectionInteraction("HISTORY").status, gateSelectionInteraction("GIZMO").status];
|
||||
self.postMessage(result);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { StorageAssetListResult, StorageAssetPutResult, StorageAssetReadResult, StorageAssetRecord, StorageInfoResult, StorageLODManifestListResult, StorageLODManifestResult, StorageLODPruneResult, StorageLODReadResult, StorageLODResult, StorageOperationListResult, StorageOperationPruneResult, StorageOperationRecord, StorageOperationResult, StorageProjectReadResult, StorageProjectResult, StorageRecoveryResult, StorageRequest, StorageResponse, StorageSaveResult, StorageSimulationCacheListResult, StorageSimulationCacheReadResult, StorageSimulationCacheResult, StorageSnapshotListResult, StorageSnapshotReadResult, StorageSnapshotResult } from "../../../protocol/storage";
|
||||
import type { StorageAssetListResult, StorageAssetPutResult, StorageAssetReadResult, StorageAssetRecord, StorageInfoResult, StorageLODManifestListResult, StorageLODManifestResult, StorageLODPruneResult, StorageLODReadResult, StorageLODResult, StorageOperationListResult, StorageOperationPruneResult, StorageOperationRecord, StorageOperationResult, StorageProjectReadResult, StorageProjectResult, StorageRecoveryResult, StorageRequest, StorageResponse, StorageSaveResult, StorageSimulationCacheFrameReadResult, StorageSimulationCacheListResult, StorageSimulationCacheReadResult, StorageSimulationCacheResult, StorageSnapshotListResult, StorageSnapshotReadResult, StorageSnapshotResult } from "../../../protocol/storage";
|
||||
import { normalizeProjectAssetPath } from "../../../protocol/asset-path";
|
||||
import { parseLODCacheRecord, type LODCacheRecord } from "../../../protocol/lod";
|
||||
import { parseSimulationCacheManifest, simulationCacheKey, SimulationCacheValidationError, verifySimulationCache, type SimulationCacheManifestIR } from "../../../protocol/simulation-cache";
|
||||
import { parseSimulationCacheManifest, selectSimulationCacheFrame, simulationCacheKey, SimulationCacheValidationError, verifySimulationCache, verifySimulationCacheFrame, type SimulationCacheManifestIR } from "../../../protocol/simulation-cache";
|
||||
import { STORAGE_DATABASE_NAME, STORAGE_SCHEMA_VERSION, STORAGE_STORES, upgradeStorageSchema } from "../storage/migrations";
|
||||
import { deleteLodCache, ensureProjectLayout, projectLayout, readContentAsset, readLodCache, readProjectBlend, recoverProjectBlend, validateSha256, writeContentAsset, writeLodCache, writeProjectBlend, type ProjectSaveFault } from "../storage/opfs-files";
|
||||
import { deleteLodCache, ensureProjectLayout, projectLayout, readContentAsset, readContentAssetRange, readLodCache, readProjectBlend, recoverProjectBlend, validateSha256, writeContentAsset, writeLodCache, writeProjectBlend, type ProjectSaveFault } from "../storage/opfs-files";
|
||||
|
||||
const scope = self as unknown as {
|
||||
onmessage: ((event: MessageEvent<StorageRequest>) => void) | null;
|
||||
@@ -724,6 +724,36 @@ async function readSimulationCache(projectId: string, cacheKey: string): Promise
|
||||
return { projectId, cacheKey, persisted: true, manifest, path: row.path, data: asset.data };
|
||||
}
|
||||
|
||||
async function readSimulationCacheFrame(projectId: string, cacheKey: string, frame: number): Promise<StorageSimulationCacheFrameReadResult> {
|
||||
projectLayout(projectId);
|
||||
const row = await readSimulationRow(projectId, cacheKey);
|
||||
if (!row) throw new SimulationCacheValidationError("SIMULATION_CACHE_MISSING", "Simulation cache manifest is missing");
|
||||
const manifest = parseSimulationCacheManifest(row.manifest);
|
||||
if (simulationCacheKey(manifest) !== cacheKey) {
|
||||
throw new SimulationCacheValidationError("SIMULATION_CACHE_INVALID", "Simulation cache manifest key is inconsistent");
|
||||
}
|
||||
const selected = selectSimulationCacheFrame(manifest, frame);
|
||||
const asset = await readAssetRow(projectId, manifest.cacheSha256);
|
||||
if (!asset || asset.bytes !== manifest.byteLength) {
|
||||
throw new SimulationCacheValidationError("SIMULATION_CACHE_MISSING", "Simulation cache payload is missing or truncated");
|
||||
}
|
||||
const data = asset.buffer ?
|
||||
asset.buffer.slice(selected.byteOffset, selected.byteOffset + selected.byteLength) :
|
||||
await readContentAssetRange(projectId, manifest.cacheSha256, selected.byteOffset, selected.byteLength, manifest.byteLength);
|
||||
await verifySimulationCacheFrame(manifest, frame, data);
|
||||
return {
|
||||
projectId,
|
||||
cacheKey,
|
||||
persisted: true,
|
||||
manifest,
|
||||
path: row.path,
|
||||
frame,
|
||||
byteOffset: selected.byteOffset,
|
||||
byteLength: selected.byteLength,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
async function listSimulationCaches(projectId: string): Promise<StorageSimulationCacheListResult> {
|
||||
projectLayout(projectId);
|
||||
const db = await openDatabase();
|
||||
@@ -773,6 +803,7 @@ scope.onmessage = async (event) => {
|
||||
else if (command.type === "pruneLOD") result = await pruneLOD(command.projectId, command.maxBytes);
|
||||
else if (command.type === "putSimulationCache") result = await withProjectTransaction(command.projectId, () => putSimulationCache(command.projectId, command.manifest, command.data));
|
||||
else if (command.type === "readSimulationCache") result = await readSimulationCache(command.projectId, command.cacheKey);
|
||||
else if (command.type === "readSimulationCacheFrame") result = await readSimulationCacheFrame(command.projectId, command.cacheKey, command.frame);
|
||||
else if (command.type === "listSimulationCaches") result = await listSimulationCaches(command.projectId);
|
||||
else throw new Error("Unknown storage command");
|
||||
if (result && "data" in result && result.data instanceof ArrayBuffer) scope.postMessage({ requestId: event.data.requestId, ok: true, result }, [result.data]);
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
setPBRMaterialSelected,
|
||||
} from "../three-adapter/pbr";
|
||||
import { GPUTextureStore } from "../three-adapter/texture-assets";
|
||||
import { applyNonMeshTransform, createNonMeshObject } from "../three-adapter/nonmesh";
|
||||
import { applyNonMeshElementSelection, applyNonMeshTransform, createNonMeshObject } from "../three-adapter/nonmesh";
|
||||
import { applyGreasePencilTransform, createGreasePencilObject } from "../three-adapter/grease-pencil";
|
||||
|
||||
const workerScope = self as unknown as {
|
||||
@@ -276,6 +276,7 @@ function setSnapshot(snapshot: SceneSnapshotIR, buffers: MeshGeometryBuffer[], n
|
||||
const greasePencilsById = new Map((snapshot.greasePencils ?? []).map((data) => [data.id, data]));
|
||||
let greasePencilCount = 0;
|
||||
let greasePencilBlockedCount = 0;
|
||||
let greasePencilOnionStrokeCount = 0;
|
||||
for (const node of snapshot.nodes) {
|
||||
if (node.type !== "GREASE_PENCIL" || !node.visible || !node.dataId) continue;
|
||||
const data = greasePencilsById.get(node.dataId);
|
||||
@@ -288,9 +289,10 @@ function setSnapshot(snapshot: SceneSnapshotIR, buffers: MeshGeometryBuffer[], n
|
||||
applyGreasePencilTransform(object, node);
|
||||
root.add(object);
|
||||
objectById.set(node.id, object);
|
||||
greasePencilOnionStrokeCount += Number(object.userData.greasePencilOnionStrokeCount ?? 0);
|
||||
greasePencilCount++;
|
||||
}
|
||||
post({ type: "snapshotStatus", nonMeshCount, nonMeshBlockedCount, greasePencilCount, greasePencilBlockedCount });
|
||||
post({ type: "snapshotStatus", nonMeshCount, nonMeshBlockedCount, greasePencilCount, greasePencilBlockedCount, greasePencilOnionStrokeCount });
|
||||
const world = snapshot.worlds.find((candidate) => candidate.id === snapshot.scenes[0]?.worldId) ?? snapshot.worlds[0];
|
||||
const sceneDefinition = snapshot.scenes.find((candidate) => candidate.id === snapshot.sceneId) ?? snapshot.scenes[0];
|
||||
scene.background = world ? new Color().setRGB(...world.color) : new Color(0x25272b);
|
||||
@@ -311,7 +313,7 @@ function setSnapshot(snapshot: SceneSnapshotIR, buffers: MeshGeometryBuffer[], n
|
||||
render();
|
||||
}
|
||||
|
||||
function setSelection(ids: string[]): void {
|
||||
function setSelection(ids: string[], elements: Array<{ dataId: string; kind: NonMeshElementKind; index: number }>): void {
|
||||
const selected = new Set(ids);
|
||||
const visited = new Set<Object3D>();
|
||||
for (const [id, object] of objectById) {
|
||||
@@ -329,6 +331,15 @@ function setSelection(ids: string[]): void {
|
||||
}
|
||||
}
|
||||
}
|
||||
const selection = new Map<string, Map<NonMeshElementKind, Set<number>>>();
|
||||
for (const element of elements) {
|
||||
const kinds = selection.get(element.dataId) ?? new Map<NonMeshElementKind, Set<number>>();
|
||||
const indices = kinds.get(element.kind) ?? new Set<number>();
|
||||
indices.add(element.index);
|
||||
kinds.set(element.kind, indices);
|
||||
selection.set(element.dataId, kinds);
|
||||
}
|
||||
if (root) applyNonMeshElementSelection(root, selection);
|
||||
render();
|
||||
}
|
||||
|
||||
@@ -411,7 +422,7 @@ workerScope.onmessage = (event): void => {
|
||||
else if (message.type === "snapshot") setSnapshot(message.snapshot, message.geometryBuffers, message.nonMeshGeometryBuffers);
|
||||
else if (message.type === "textureAssets") applyTextureAssets(message.assets);
|
||||
else if (message.type === "resize") resize(message.width, message.height, message.pixelRatio);
|
||||
else if (message.type === "selection") setSelection(message.objectIds);
|
||||
else if (message.type === "selection") setSelection(message.objectIds, message.elements);
|
||||
else if (message.type === "interaction") {
|
||||
editMode = message.editMode;
|
||||
selectionMode = message.selectionMode;
|
||||
|
||||
@@ -366,6 +366,30 @@ function assertFutureCapability(payload: Extract<WebEngineRequest["command"], {
|
||||
for (const field of ["useTemperature", "castsShadow"] as const) if (properties[field] !== undefined && typeof properties[field] !== "boolean") throw report("RENDER_PROPERTY_INVALID", `Light ${field} must be boolean`);
|
||||
return;
|
||||
}
|
||||
case "setCameraProperties": {
|
||||
const camera = currentSnapshot?.cameras.find((candidate) => candidate.id === payload.dataId);
|
||||
const properties = payload.properties;
|
||||
if (!camera || !validPropertyKeys(properties, ["projection", "lensMm", "sensorWidthMm", "sensorHeightMm", "sensorFit", "shift", "near", "far", "orthoScale", "depthOfField"])) throw report("RENDER_PROPERTY_INVALID", "Camera target or properties are invalid");
|
||||
if (properties.projection !== undefined && !["PERSPECTIVE", "ORTHOGRAPHIC"].includes(properties.projection)) throw report("RENDER_PROPERTY_INVALID", "Camera projection is invalid");
|
||||
for (const [field, minimum, maximum] of [["lensMm", 0.1, 10_000], ["sensorWidthMm", 0.1, 10_000], ["sensorHeightMm", 0.1, 10_000], ["near", 0.0001, 1e9], ["far", 0.0002, 1e12], ["orthoScale", 0.0001, 1e9]] as const) {
|
||||
const value = properties[field];
|
||||
if (value !== undefined && !boundedNumber(value, minimum, maximum)) throw report("RENDER_PROPERTY_INVALID", `Camera ${field} is outside the bounded range`);
|
||||
}
|
||||
if (properties.near !== undefined && properties.far !== undefined && properties.far <= properties.near) throw report("RENDER_PROPERTY_INVALID", "Camera far clip must exceed near clip");
|
||||
if (properties.sensorFit !== undefined && ![0, 1, 2].includes(properties.sensorFit)) throw report("RENDER_PROPERTY_INVALID", "Camera sensorFit is invalid");
|
||||
if (properties.shift !== undefined && (!Array.isArray(properties.shift) || properties.shift.length !== 2 || properties.shift.some((value) => !boundedNumber(value, -1000, 1000)))) throw report("RENDER_PROPERTY_INVALID", "Camera shift is invalid");
|
||||
if (properties.depthOfField !== undefined) {
|
||||
const dof = properties.depthOfField;
|
||||
if (!validPropertyKeys(dof, ["enabled", "focusDistance", "apertureFStop", "apertureBlades", "apertureRotation", "apertureRatio"])) throw report("RENDER_PROPERTY_INVALID", "Camera depthOfField is invalid");
|
||||
if (dof.enabled !== undefined && typeof dof.enabled !== "boolean") throw report("RENDER_PROPERTY_INVALID", "Camera depthOfField.enabled must be boolean");
|
||||
for (const [field, minimum, maximum] of [["focusDistance", 0, 1e9], ["apertureFStop", 0.01, 1000], ["apertureRotation", -Math.PI * 2, Math.PI * 2], ["apertureRatio", 0.01, 100]] as const) {
|
||||
const value = dof[field];
|
||||
if (value !== undefined && !boundedNumber(value, minimum, maximum)) throw report("RENDER_PROPERTY_INVALID", `Camera depthOfField.${field} is outside the bounded range`);
|
||||
}
|
||||
if (dof.apertureBlades !== undefined && (!Number.isSafeInteger(dof.apertureBlades) || dof.apertureBlades < 0 || dof.apertureBlades > 64)) throw report("RENDER_PROPERTY_INVALID", "Camera depthOfField.apertureBlades is outside the bounded range");
|
||||
}
|
||||
return;
|
||||
}
|
||||
case "setWorldProperties": {
|
||||
const world = currentSnapshot?.worlds.find((candidate) => candidate.id === payload.dataId);
|
||||
const properties = payload.properties;
|
||||
|
||||
Reference in New Issue
Block a user