Govern task context and advance execution pointer
This commit is contained in:
BIN
web/app/public/vendor/blender/web_engine.wasm
vendored
BIN
web/app/public/vendor/blender/web_engine.wasm
vendored
Binary file not shown.
@@ -873,7 +873,7 @@ function Properties({ snapshot, selectedFaceIndices, selectedVertexIndices, grea
|
||||
</div>
|
||||
<div className="property-section"><h3>Transform</h3><label>位置 <output>0.000, 0.000, 0.000</output></label><label>旋转 <output>0°, 0°, 0°</output></label><label>缩放 <output>1.000, 1.000, 1.000</output></label></div>
|
||||
{activeNode ? <div className="property-section"><h3>Object & Hierarchy</h3><label>名称 <input aria-label="对象名称" value={renameValue} onChange={(event) => setRenameValue(event.target.value)} /></label><div className="property-actions"><button type="button" onClick={() => renameValue && onCommand({ type: "renameId", id: activeNode.id, name: renameValue })}>重命名</button><button type="button" onClick={() => onCommand({ type: "applyObjectTransform", objectId: activeNode.id })}>应用变换</button><button type="button" onClick={() => onCommand({ type: "setObjectOrigin", objectId: activeNode.id, mode: "GEOMETRY" })}>原点到几何体</button></div><label>Collection <select aria-label="移动到 Collection" value="" onChange={(event) => event.target.value && onCommand({ type: "moveObjectToCollection", objectId: activeNode.id, collectionId: event.target.value })}><option value="">选择...</option>{snapshot?.collections.map((collection) => <option key={collection.id} value={collection.id}>{collection.name}</option>)}</select></label></div> : null}
|
||||
<div className="property-section"><h3>Viewport Display</h3><label>显示颜色 <span className="swatch" /></label><label>可见性 <input type="checkbox" defaultChecked /></label></div>
|
||||
<div className="property-section"><h3>Viewport Display</h3><label>显示颜色 <span className="swatch" /></label><label>可见性 <input aria-label="可见性" type="checkbox" defaultChecked /></label></div>
|
||||
{activeCurve && curveTopologyOperatorGate("TOGGLE_CYCLIC").status === "READY" ? <div className="property-section" data-testid="curve-topology-editor" data-ready-operators="TOGGLE_CYCLIC" data-curve-id={activeCurve.id} data-cyclic-u={(activeCurve.cyclicU ?? []).join(",")} data-spline-types={(activeCurve.splineTypes ?? []).join(",")} data-point-count={activeCurve.pointCount}><h3>Curve Topology</h3><label>Spline <select aria-label="Curve spline" value={Math.min(curveSplineIndex, Math.max(0, activeCurve.splineCount - 1))} onChange={(event) => setCurveSplineIndex(Number(event.target.value))}>{Array.from({ length: activeCurve.splineCount }, (_, index) => <option key={index} value={index}>{index + 1} {activeCurve.splineTypes?.[index] ?? "UNKNOWN"}</option>)}</select></label><label>Cyclic U <input aria-label="Curve cyclic U" type="checkbox" checked={activeCurve.cyclicU?.[curveSplineIndex] ?? false} onChange={toggleCurveCyclic} /></label><output data-testid="curve-cyclic-state">{activeCurve.cyclicU?.[curveSplineIndex] ? "Cyclic" : "Open"}</output>{curveTopologyError ? <output role="alert">{curveTopologyError}</output> : null}</div> : null}
|
||||
{activeMesh ? <div className="property-section"><h3>UV Maps</h3><label>活动 UV <select aria-label="活动 UV Map" value={activeMesh.activeUVMap ?? ""} onChange={(event) => event.target.value && onCommand({ type: "setActiveUVMap", meshId: activeMesh.id, name: event.target.value })}><option value="">None</option>{activeMesh.uvLayers?.map((layer) => <option key={layer.name} value={layer.name}>{layer.name}</option>)}</select></label><div className="property-actions"><button type="button" onClick={() => onCommand({ type: "createUVMap", meshId: activeMesh.id, name: `UVMap.${(activeMesh.uvLayers?.length ?? 0) + 1}` })}>新增 UV</button><button type="button" disabled={selectedFaceIndices.length === 0} onClick={() => onCommand({ type: "unwrapUV", meshId: activeMesh.id, faceIndices: selectedFaceIndices, method: "PLANAR" })}>Planar</button><button type="button" disabled={selectedFaceIndices.length === 0} onClick={() => onCommand({ type: "unwrapUV", meshId: activeMesh.id, faceIndices: selectedFaceIndices, method: "CUBE" })}>Cube</button></div></div> : null}
|
||||
{activeGreasePencil ? <div
|
||||
|
||||
@@ -80,7 +80,7 @@ button:focus-visible, input:focus-visible, select:focus-visible, textarea:focus-
|
||||
.operator-results { display: grid; gap: 2px; margin-top: 6px; }.operator-results button { padding: 8px 10px; text-align: left; color: #d9dce1; background: transparent; border-radius: 3px; }.operator-results button:hover { color: #fff; background: #a55325; }
|
||||
.outliner-tools input:focus-visible, .operator-search input:focus-visible { outline: 2px solid #f2a15b; outline-offset: 1px; }
|
||||
.file-button:has(input:focus-visible) { outline: 2px solid #f2a15b; outline-offset: 1px; }
|
||||
@media (max-width: 800px) {
|
||||
@media (max-width: 900px) {
|
||||
.topbar { min-width: 0; overflow-x: auto; scrollbar-width: none; }
|
||||
.topbar::-webkit-scrollbar { display: none; }
|
||||
.topbar > *, .topbar-actions button { flex: none; white-space: nowrap; }
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { OffscreenViewportRequest, OffscreenViewportResponse } from "./offs
|
||||
import { PBR_PROFILE, PBR_SHADOW_PROFILE, PBR_TONE_MAPPING } from "./pbr";
|
||||
import { resolveViewportPixelMetrics, viewportNDC } from "../../../protocol/viewport-dpr";
|
||||
import { observePointerEvent } from "../../../protocol/pointer-contract";
|
||||
import { createInputModalState, reduceInputModal, type InputModalState } from "../../../protocol/input-modal";
|
||||
import type { NonMeshElementKind } from "./nonmesh";
|
||||
import type { GreasePencilPointPreview, GreasePencilPointRef } from "./grease-pencil";
|
||||
import type { CurveGizmoFrameIR, CurveGizmoHandleIR } from "../../../protocol/nonmesh-interaction";
|
||||
@@ -88,6 +89,8 @@ export class OffscreenViewportRenderer implements ViewportBackend {
|
||||
private readonly onGreasePencilPointSelect?: (point: GreasePencilPointRef, additive: boolean, baseSelectionRevision: number) => void;
|
||||
private readonly onGreasePencilMarqueeSelect?: (result: GreasePencilMarqueeResultIR, additive: boolean) => void;
|
||||
private pointer: { id: number; x: number; y: number; moved: boolean } | null = null;
|
||||
private inputModal: InputModalState = createInputModalState();
|
||||
private touchNavigationActive = false;
|
||||
private lastSnapshot: SceneSnapshotIR | null = null;
|
||||
private lastGeometryBuffers: MeshGeometryBuffer[] | null = null;
|
||||
private lastNonMeshGeometryBuffers: NonMeshGeometryChunk[] | null = null;
|
||||
@@ -134,6 +137,7 @@ export class OffscreenViewportRenderer implements ViewportBackend {
|
||||
canvas.dataset.pbrProfile = PBR_PROFILE;
|
||||
canvas.dataset.toneMapping = PBR_TONE_MAPPING;
|
||||
canvas.dataset.shadowMap = PBR_SHADOW_PROFILE;
|
||||
this.publishInputModal();
|
||||
}
|
||||
|
||||
setSnapshot(snapshot: SceneSnapshotIR, geometryBuffers: MeshGeometryBuffer[] = [], nonMeshGeometryBuffers: NonMeshGeometryChunk[] = []): void {
|
||||
@@ -252,8 +256,8 @@ export class OffscreenViewportRenderer implements ViewportBackend {
|
||||
}
|
||||
|
||||
private pointerDown = (event: PointerEvent): void => {
|
||||
try { this.canvas.dataset.lastPointer = JSON.stringify(observePointerEvent(event)); }
|
||||
catch { this.canvas.dataset.lastPointer = "BLOCKED"; }
|
||||
const observation = this.recordInputModalEvent(event);
|
||||
if (observation?.pointerType === "touch" && this.inputModal.activePointerIds.length >= 2) this.touchNavigationActive = true;
|
||||
this.pointer = { id: event.pointerId, x: event.clientX, y: event.clientY, moved: false };
|
||||
try {
|
||||
this.canvas.setPointerCapture(event.pointerId);
|
||||
@@ -274,8 +278,17 @@ export class OffscreenViewportRenderer implements ViewportBackend {
|
||||
};
|
||||
|
||||
private pointerUp = (event: PointerEvent): void => {
|
||||
try { this.canvas.dataset.lastPointer = JSON.stringify(observePointerEvent(event)); }
|
||||
catch { this.canvas.dataset.lastPointer = "BLOCKED"; }
|
||||
const observation = this.recordInputModalEvent(event);
|
||||
const wasTouchNavigation = this.touchNavigationActive;
|
||||
if (observation?.pointerType === "touch" && this.inputModal.activePointerIds.length === 0) this.touchNavigationActive = false;
|
||||
if (!observation) {
|
||||
this.pointer = null;
|
||||
return;
|
||||
}
|
||||
if (event.type === "pointercancel" || observation.pointerType === "pen" || wasTouchNavigation) {
|
||||
this.pointer = null;
|
||||
return;
|
||||
}
|
||||
if (!this.pointer || this.pointer.id !== event.pointerId) return;
|
||||
if (!this.pointer.moved) {
|
||||
const bounds = this.canvas.getBoundingClientRect();
|
||||
@@ -285,6 +298,37 @@ export class OffscreenViewportRenderer implements ViewportBackend {
|
||||
this.pointer = null;
|
||||
};
|
||||
|
||||
private recordInputModalEvent(event: PointerEvent): ReturnType<typeof observePointerEvent> | null {
|
||||
try {
|
||||
const observation = observePointerEvent(event);
|
||||
this.canvas.dataset.lastPointer = JSON.stringify(observation);
|
||||
if (observation.pointerType === "touch" || observation.pointerType === "pen") {
|
||||
this.inputModal = reduceInputModal(this.inputModal, {
|
||||
type: event.type as "pointerdown" | "pointerup" | "pointercancel",
|
||||
pointerType: observation.pointerType,
|
||||
pointerId: observation.pointerId,
|
||||
});
|
||||
this.publishInputModal();
|
||||
}
|
||||
return observation;
|
||||
}
|
||||
catch {
|
||||
this.canvas.dataset.lastPointer = "BLOCKED";
|
||||
this.canvas.dataset.inputModalError = "INPUT_MODAL_EVENT_BLOCKED";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private publishInputModal(): void {
|
||||
this.canvas.dataset.inputModalSchemaVersion = String(this.inputModal.schemaVersion);
|
||||
this.canvas.dataset.inputModalKind = this.inputModal.kind;
|
||||
this.canvas.dataset.inputModalActivePointerIds = this.inputModal.activePointerIds.join(",");
|
||||
this.canvas.dataset.inputModalCancelled = this.inputModal.cancelled ? "1" : "0";
|
||||
this.canvas.dataset.inputModalNavigationRevision = String(this.inputModal.navigationRevision);
|
||||
this.canvas.dataset.inputModalMainCommitCount = String(this.inputModal.mainCommitCount);
|
||||
this.canvas.dataset.inputModalError = "";
|
||||
}
|
||||
|
||||
private wheel = (event: WheelEvent): void => {
|
||||
event.preventDefault();
|
||||
this.worker.postMessage({ type: "orbit", deltaX: 0, deltaY: 0, zoom: event.deltaY } satisfies OffscreenViewportRequest);
|
||||
|
||||
@@ -71,6 +71,7 @@ import { validatePaintDepthVisibilityRequest, type PaintDepthVisibilityRequestIR
|
||||
import { samplePaintDepthVisibilityGPU } from "./paint-depth-visibility";
|
||||
import { resolveViewportPixelMetrics, viewportNDC } from "../../../protocol/viewport-dpr";
|
||||
import { observePointerEvent } from "../../../protocol/pointer-contract";
|
||||
import { createInputModalState, reduceInputModal, type InputModalState } from "../../../protocol/input-modal";
|
||||
|
||||
export function collectMeshInstanceGroups(snapshot: SceneSnapshotIR, minimumSize = 2): Map<string, string[]> {
|
||||
const groups = new Map<string, string[]>();
|
||||
@@ -104,6 +105,7 @@ export class ViewportRenderer {
|
||||
private readonly textureStore = new GPUTextureStore("THREE_WEBGL2");
|
||||
private readonly raycaster = new Raycaster();
|
||||
private readonly pointer = new Vector2();
|
||||
private inputModal: InputModalState = createInputModalState();
|
||||
private readonly onSelect?: (objectId: string, additive: boolean) => void;
|
||||
private readonly onElementSelect?: (meshId: string, mode: MeshElementMode, index: number, additive: boolean, nonMeshKind?: NonMeshElementKind) => void;
|
||||
private readonly onGreasePencilPointSelect?: (point: GreasePencilPointRef, additive: boolean, baseSelectionRevision: number) => void;
|
||||
@@ -173,12 +175,14 @@ export class ViewportRenderer {
|
||||
this.resizeObserver.observe(canvas);
|
||||
this.canvas.addEventListener("click", this.handleClick);
|
||||
this.canvas.addEventListener("pointerdown", this.handlePointerObservation);
|
||||
this.canvas.addEventListener("pointerup", this.handlePointerObservation);
|
||||
this.canvas.addEventListener("pointercancel", this.handlePointerObservation);
|
||||
this.canvas.addEventListener("webglcontextlost", this.handleContextLost);
|
||||
this.canvas.addEventListener("webglcontextrestored", this.handleContextRestored);
|
||||
this.resize();
|
||||
this.controls.update();
|
||||
this.publishCameraState();
|
||||
this.publishInputModal();
|
||||
this.renderLoop();
|
||||
}
|
||||
|
||||
@@ -821,6 +825,8 @@ export class ViewportRenderer {
|
||||
if ("pointerType" in event) {
|
||||
try { this.canvas.dataset.lastPointer = JSON.stringify(observePointerEvent(event as MouseEvent & { pointerType?: string; pointerId?: number; pressure?: number; tiltX?: number; tiltY?: number; buttons?: number; type?: string })); }
|
||||
catch { this.canvas.dataset.lastPointer = "BLOCKED"; }
|
||||
const pointerType = (event as MouseEvent & { pointerType?: string }).pointerType;
|
||||
if ((pointerType === "touch" || pointerType === "pen") && this.inputModal.cancelled) return;
|
||||
}
|
||||
const bounds = this.canvas.getBoundingClientRect();
|
||||
if (bounds.width <= 0 || bounds.height <= 0) return;
|
||||
@@ -897,10 +903,34 @@ export class ViewportRenderer {
|
||||
};
|
||||
|
||||
private handlePointerObservation = (event: PointerEvent): void => {
|
||||
try { this.canvas.dataset.lastPointer = JSON.stringify(observePointerEvent(event)); }
|
||||
catch { this.canvas.dataset.lastPointer = "BLOCKED"; }
|
||||
try {
|
||||
const observation = observePointerEvent(event);
|
||||
this.canvas.dataset.lastPointer = JSON.stringify(observation);
|
||||
if (observation.pointerType === "touch" || observation.pointerType === "pen") {
|
||||
this.inputModal = reduceInputModal(this.inputModal, {
|
||||
type: event.type as "pointerdown" | "pointerup" | "pointercancel",
|
||||
pointerType: observation.pointerType,
|
||||
pointerId: observation.pointerId,
|
||||
});
|
||||
this.publishInputModal();
|
||||
}
|
||||
}
|
||||
catch {
|
||||
this.canvas.dataset.lastPointer = "BLOCKED";
|
||||
this.canvas.dataset.inputModalError = "INPUT_MODAL_EVENT_BLOCKED";
|
||||
}
|
||||
};
|
||||
|
||||
private publishInputModal(): void {
|
||||
this.canvas.dataset.inputModalSchemaVersion = String(this.inputModal.schemaVersion);
|
||||
this.canvas.dataset.inputModalKind = this.inputModal.kind;
|
||||
this.canvas.dataset.inputModalActivePointerIds = this.inputModal.activePointerIds.join(",");
|
||||
this.canvas.dataset.inputModalCancelled = this.inputModal.cancelled ? "1" : "0";
|
||||
this.canvas.dataset.inputModalNavigationRevision = String(this.inputModal.navigationRevision);
|
||||
this.canvas.dataset.inputModalMainCommitCount = String(this.inputModal.mainCommitCount);
|
||||
this.canvas.dataset.inputModalError = "";
|
||||
}
|
||||
|
||||
private renderLoop = (): void => {
|
||||
if (this.disposed) return;
|
||||
if (!this.contextLost) {
|
||||
@@ -965,6 +995,7 @@ export class ViewportRenderer {
|
||||
this.resizeObserver.disconnect();
|
||||
this.canvas.removeEventListener("click", this.handleClick);
|
||||
this.canvas.removeEventListener("pointerdown", this.handlePointerObservation);
|
||||
this.canvas.removeEventListener("pointerup", this.handlePointerObservation);
|
||||
this.canvas.removeEventListener("pointercancel", this.handlePointerObservation);
|
||||
this.canvas.removeEventListener("webglcontextlost", this.handleContextLost);
|
||||
this.canvas.removeEventListener("webglcontextrestored", this.handleContextRestored);
|
||||
|
||||
BIN
web/app/src/vendor/blender/web_engine.wasm
vendored
BIN
web/app/src/vendor/blender/web_engine.wasm
vendored
Binary file not shown.
Reference in New Issue
Block a user