Govern task context and advance execution pointer
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-08-20 06:02:43 -04:00
parent 380cbed4ff
commit 10640aeb3c
984 changed files with 543475 additions and 327 deletions

Binary file not shown.

View File

@@ -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

View File

@@ -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; }

View File

@@ -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);

View File

@@ -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);

Binary file not shown.