Complete V1 RC deployment capability gates
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#202124" />
|
||||
<title>Blender Web Editor</title>
|
||||
<title>Web Blender Modeler V1</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -302,7 +302,7 @@ function ViewportPlaceholder({ snapshot, geometryBuffers, nonMeshGeometryBuffers
|
||||
<canvas ref={canvasRef} className="viewport-canvas" aria-label="Three.js WebGL2 视口" />
|
||||
<div className="axis-gizmo" aria-hidden="true"><span className="axis-x">X</span><span className="axis-y">Y</span><span className="axis-z">Z</span></div>
|
||||
{viewportError || !snapshot || snapshot.nodes.length === 0 ? <div className="viewport-message">
|
||||
<strong>Blender Web Viewport</strong>
|
||||
<strong>3D Viewport</strong>
|
||||
{viewportError ? <span>WebGL 不可用,已保留场景编辑界面:{viewportError}</span> : <span>Three.js WebGL2 适配器</span>}
|
||||
</div> : null}
|
||||
<div className="viewport-toolbar" aria-label="视口工具">
|
||||
@@ -1344,7 +1344,7 @@ export function App() {
|
||||
return (
|
||||
<main className="blender-app" data-workspace={workspace} data-ui-revision={uiState.context.revision}>
|
||||
<header className="topbar">
|
||||
<div className="brand"><span className="brand-mark" aria-hidden="true">◈</span><span>Blender Web</span></div>
|
||||
<div className="brand"><span className="brand-mark" aria-hidden="true">◈</span><span>Web Blender Modeler V1</span></div>
|
||||
<nav className="menu-bar" aria-label="主菜单"><button type="button" onClick={() => fileInputRef.current?.click()}>文件</button><button type="button">编辑</button><button type="button">渲染</button><button type="button">窗口</button><button type="button">帮助</button></nav>
|
||||
<nav className="workspace-tabs" aria-label="工作区">
|
||||
{(["Layout", "Modeling", "Animation"] as WorkspaceId[]).map((item) => <button key={item} className={item === workspace ? "workspace-tab active" : "workspace-tab"} type="button" onClick={() => dispatchUI({ type: "switchWorkspace", workspaceId: item })}>{item}</button>)}
|
||||
@@ -1360,7 +1360,7 @@ export function App() {
|
||||
<Area className="timeline-area" editor="Timeline"><Timeline snapshot={snapshot} frame={frame} start={frameRange.start} end={frameRange.end} onFrameChange={(value) => void applyEditCommand({ type: "setFrame", frame: value })} onCommand={(command) => void applyEditCommand(command)} /></Area>
|
||||
</div>
|
||||
{uiState.operatorSearchOpen ? <OperatorSearch commands={operatorCommands} onClose={() => dispatchUI({ type: "toggleOperatorSearch", open: false })} /> : null}
|
||||
<footer className="status-bar"><span>Blender Web 0.1.0</span><span data-testid="scene-stats">Objects {objectCount} · Vertices {vertexCount} · Faces {faceCount}</span>{openProgress ? <span data-testid="open-progress">{openProgress.message ?? "Opening"}</span> : null}<span className="status-spacer" /><span>{manifestStatus}</span><span>{wasmStatus}</span><span data-testid="engine-status">{engineStatus}</span><span>{storageStatus}</span></footer>
|
||||
<footer className="status-bar"><span>Web Blender Modeler V1</span><span data-testid="scene-stats">Objects {objectCount} · Vertices {vertexCount} · Faces {faceCount}</span>{openProgress ? <span data-testid="open-progress">{openProgress.message ?? "Opening"}</span> : null}<span className="status-spacer" /><span>{manifestStatus}</span><span>{wasmStatus}</span><span data-testid="engine-status">{engineStatus}</span><span>{storageStatus}</span></footer>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
export interface BrowserCapabilities {
|
||||
import { blockedGate, capabilityIssue, readyGate, type CapabilityGateResult } from "../../../protocol/capability-gates";
|
||||
|
||||
export interface WasmThreadingCapabilities {
|
||||
crossOriginIsolated: boolean;
|
||||
sharedArrayBuffer: boolean;
|
||||
worker: boolean;
|
||||
}
|
||||
|
||||
export interface BrowserCapabilities extends WasmThreadingCapabilities {
|
||||
webgl2: boolean;
|
||||
offscreenCanvas: boolean;
|
||||
opfs: boolean;
|
||||
sharedArrayBuffer: boolean;
|
||||
worker: boolean;
|
||||
wasm: boolean;
|
||||
wasmSimd: boolean;
|
||||
wasmThreads: boolean;
|
||||
@@ -11,6 +17,34 @@ export interface BrowserCapabilities {
|
||||
indexedDb: boolean;
|
||||
}
|
||||
|
||||
export function gateWasmThreadingCapability(capabilities: WasmThreadingCapabilities): CapabilityGateResult {
|
||||
const issues = [];
|
||||
if (!capabilities.crossOriginIsolated) {
|
||||
issues.push(capabilityIssue(
|
||||
"PLATFORM_CAPABILITY_UNAVAILABLE",
|
||||
"Cross-origin isolation is required for the pthread WASM engine",
|
||||
"crossOriginIsolated",
|
||||
));
|
||||
}
|
||||
if (!capabilities.sharedArrayBuffer) {
|
||||
issues.push(capabilityIssue(
|
||||
"PLATFORM_CAPABILITY_UNAVAILABLE",
|
||||
"SharedArrayBuffer is required for the pthread WASM engine",
|
||||
"sharedArrayBuffer",
|
||||
));
|
||||
}
|
||||
if (!capabilities.worker) {
|
||||
issues.push(capabilityIssue(
|
||||
"PLATFORM_CAPABILITY_UNAVAILABLE",
|
||||
"Worker is required for the pthread WASM engine",
|
||||
"worker",
|
||||
));
|
||||
}
|
||||
return issues.length > 0
|
||||
? blockedGate("M6-02", "WASM_PTHREAD_ENGINE", issues)
|
||||
: readyGate("M6-02", "WASM_PTHREAD_ENGINE");
|
||||
}
|
||||
|
||||
function supportsWasmFeature(feature: "simd" | "threads"): boolean {
|
||||
if (typeof WebAssembly === "undefined") return false;
|
||||
|
||||
@@ -40,6 +74,7 @@ export function detectBrowserCapabilities(): BrowserCapabilities {
|
||||
const storage = typeof navigator === "undefined" ? undefined : navigator.storage;
|
||||
|
||||
return {
|
||||
crossOriginIsolated: globalThis.crossOriginIsolated === true,
|
||||
webgl2,
|
||||
offscreenCanvas: typeof OffscreenCanvas !== "undefined",
|
||||
opfs: Boolean(storage?.getDirectory),
|
||||
|
||||
@@ -135,6 +135,10 @@ export class StorageClient {
|
||||
return this.request({ type: "listSimulationCaches", projectId }) as Promise<StorageSimulationCacheListResult>;
|
||||
}
|
||||
|
||||
getPendingRequestCount(): number {
|
||||
return this.pending.size;
|
||||
}
|
||||
|
||||
private request(command: StorageRequest["command"], transfer: Transferable[] = []): Promise<NonNullable<StorageResponse["result"]>> {
|
||||
const requestId = `storage-${++this.counter}`;
|
||||
const request: StorageRequest = { requestId, command };
|
||||
|
||||
@@ -18,7 +18,7 @@ type Request =
|
||||
type Response =
|
||||
| { type: "ready"; index: { stripCount: number; bucketCount: number; referenceCount: number; estimatedBytes: number } }
|
||||
| { type: "seekResult"; requestId: string; result: LongMediaSeekResultIR }
|
||||
| { type: "disposed"; cacheBytes: number }
|
||||
| { type: "disposed"; releasedCacheBytes: number; cacheBytesAfter: number }
|
||||
| { type: "error"; requestId?: string; message: string };
|
||||
|
||||
const scope = self as unknown as {
|
||||
@@ -71,9 +71,9 @@ scope.onmessage = (event): void => {
|
||||
return;
|
||||
}
|
||||
if (message.type === "cancel") { session?.cancel(); return; }
|
||||
const cacheBytes = session?.cache.stats().bytes ?? 0;
|
||||
const releasedCacheBytes = session?.cache.stats().bytes ?? 0;
|
||||
session?.dispose();
|
||||
session = null;
|
||||
assets.clear();
|
||||
scope.postMessage({ type: "disposed", cacheBytes });
|
||||
scope.postMessage({ type: "disposed", releasedCacheBytes, cacheBytesAfter: 0 });
|
||||
};
|
||||
|
||||
@@ -4,12 +4,13 @@ import react from "@vitejs/plugin-react";
|
||||
import fs from "node:fs";
|
||||
// @ts-expect-error The runtime is Node; this project intentionally avoids a browser dependency on Node types.
|
||||
import path from "node:path";
|
||||
// @ts-expect-error The runtime is Node; this project intentionally avoids a browser dependency on Node types.
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const isolationHeaders = {
|
||||
"Cross-Origin-Opener-Policy": "same-origin",
|
||||
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||
"Cross-Origin-Resource-Policy": "same-origin",
|
||||
};
|
||||
const deploymentContract = JSON.parse(
|
||||
fs.readFileSync(path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../docs/web/deployment-contract.json"), "utf8"),
|
||||
) as { responseHeaders: { allResponses: Record<string, string> } };
|
||||
const isolationHeaders = deploymentContract.responseHeaders.allResponses;
|
||||
|
||||
function preserveIsolationHeaders(): Plugin {
|
||||
const install = (server: { middlewares: { use: (handler: (request: unknown, response: { setHeader: (name: string, value: string) => void }, next: () => void) => void) => void } }) => {
|
||||
|
||||
Reference in New Issue
Block a user