diff --git a/blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp b/blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp index 7435f32af..9b64ead07 100644 --- a/blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp +++ b/blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp @@ -14182,9 +14182,21 @@ bool web_engine_blend_main_set_transform(WebBlendMainState *state, } for (int axis = 0; axis < 3; axis++) { object->loc[axis] = translation[axis]; - object->rot[axis] = rotation_euler[axis]; object->scale[axis] = scale[axis]; } + float quaternion[4]; + eul_to_quat(quaternion, rotation_euler.data()); + switch (object->rotmode) { + case ROT_MODE_QUAT: + copy_qt_qt(object->quat, quaternion); + break; + case ROT_MODE_AXISANGLE: + quat_to_axis_angle(object->rotAxis, &object->rotAngle, quaternion); + break; + default: + copy_v3_v3(object->rot, rotation_euler.data()); + break; + } object->id.recalc |= ID_RECALC_TRANSFORM; return true; } diff --git a/web/app/public/engine-manifest.json b/web/app/public/engine-manifest.json index 5a0bbc3b0..e9cc178c9 100644 --- a/web/app/public/engine-manifest.json +++ b/web/app/public/engine-manifest.json @@ -21,7 +21,7 @@ "wasm": { "fileName": "web_engine.wasm", "url": "/vendor/blender/web_engine.wasm", - "sha256": "db9291b06a0fe5a1a0cc5020d521fe9fbfd8c4178f00603f0eba8cdb721c8af5" + "sha256": "5bf6900550fda1076149c2f26e2dd1928a39faa6254a1cc052e85ec589b4ca74" } } }, diff --git a/web/app/public/vendor/blender/web_engine.wasm b/web/app/public/vendor/blender/web_engine.wasm index 8e761d65a..d9e6197ae 100644 Binary files a/web/app/public/vendor/blender/web_engine.wasm and b/web/app/public/vendor/blender/web_engine.wasm differ diff --git a/web/app/public/wasm-module-manifest.json b/web/app/public/wasm-module-manifest.json index c62961cf8..548b2fa83 100644 --- a/web/app/public/wasm-module-manifest.json +++ b/web/app/public/wasm-module-manifest.json @@ -10,7 +10,7 @@ "single": { "fileName": "web_engine.wasm", "url": "/vendor/blender/web_engine.wasm", - "sha256": "db9291b06a0fe5a1a0cc5020d521fe9fbfd8c4178f00603f0eba8cdb721c8af5" + "sha256": "5bf6900550fda1076149c2f26e2dd1928a39faa6254a1cc052e85ec589b4ca74" }, "pthread": { "fileName": "web_engine.wasm", @@ -27,7 +27,7 @@ "single": { "fileName": "web_engine.wasm", "url": "/vendor/blender/web_engine.wasm", - "sha256": "db9291b06a0fe5a1a0cc5020d521fe9fbfd8c4178f00603f0eba8cdb721c8af5" + "sha256": "5bf6900550fda1076149c2f26e2dd1928a39faa6254a1cc052e85ec589b4ca74" }, "pthread": { "fileName": "web_engine.wasm", @@ -44,7 +44,7 @@ "single": { "fileName": "web_engine.wasm", "url": "/vendor/blender/web_engine.wasm", - "sha256": "db9291b06a0fe5a1a0cc5020d521fe9fbfd8c4178f00603f0eba8cdb721c8af5" + "sha256": "5bf6900550fda1076149c2f26e2dd1928a39faa6254a1cc052e85ec589b4ca74" }, "pthread": { "fileName": "web_engine.wasm", @@ -61,7 +61,7 @@ "single": { "fileName": "web_engine.wasm", "url": "/vendor/blender/web_engine.wasm", - "sha256": "db9291b06a0fe5a1a0cc5020d521fe9fbfd8c4178f00603f0eba8cdb721c8af5" + "sha256": "5bf6900550fda1076149c2f26e2dd1928a39faa6254a1cc052e85ec589b4ca74" }, "pthread": { "fileName": "web_engine.wasm", diff --git a/web/app/src/app/App.tsx b/web/app/src/app/App.tsx index cde216c5d..a58cb2302 100644 --- a/web/app/src/app/App.tsx +++ b/web/app/src/app/App.tsx @@ -109,7 +109,8 @@ import { exportGLB } from "../../../protocol/glb-export"; import {RobotTool} from '../robot/RobotTool'; import {DesktopMenu,DesktopDialog,type DesktopMenuItem} from './DesktopMenus'; import { AutosaveRecoveryDialog, type RecoveryPointSelection } from './AutosaveRecoveryDialog'; -import type {BlenderViewportCamera,ViewportNavigationCommand} from '../../../protocol/viewport-camera'; +import {eulerFromQuaternion,type BlenderViewportCamera,type ViewportNavigationCommand} from '../../../protocol/viewport-camera'; +import {sceneCameraFrame} from '../three-adapter/scene-camera'; import { mapEvaluatedNonMeshForExport } from "../../../protocol/nonmesh-export"; import { normalizeProjectAssetPath } from "../../../protocol/asset-path"; import { applyCurveGizmoDelta, curveGizmoAxisDelta, deriveCurveHandleGizmoFrame, type CurveGizmoFrameIR, type CurveGizmoHandleIR, type CurveGizmoScreenFrameIR } from "../../../protocol/nonmesh-interaction"; @@ -138,7 +139,8 @@ import { } from "../volume/nanovdb-viewport"; import { composePaintColorPatch, composePaintWeightPatch, createSculptMeshAttributes, createSculptStroke } from "../../../protocol/paint"; import type { SculptBrush, SculptStrokeSampleIR } from "../../../protocol/sculpt"; -import { BLENDER_WORKSPACE_IDS, createDefaultWebWorkspaceState, reduceUICommand, type EditorType, type UICommand, type WorkspaceId } from "../../../protocol/ui-schema"; +import { BLENDER_WORKSPACE_IDS, createDefaultWebWorkspaceState, reduceUICommand, type AreaIR, type EditorType, type UICommand, type WorkspaceId } from "../../../protocol/ui-schema"; +import { applyAreaOperation, findAreaNeighbor, measureAreaRects, type AreaDirection, type AreaOperation, type AreaSplitDirection } from "../../../protocol/area-layout"; import { EDITOR_MODES, parseEditorWorkflow, type EditorWorkflowIR, type EditorAreaIR, type EditorContextEdit, type NativeTimelineEdit, type NativeUVEdit } from "../../../protocol/editor-workflow"; import { importExternalVFontIntoMain } from "../fonts/external-vfont-import"; import { createIMECompositionState, reduceIMEComposition, shouldBlockOperatorShortcuts, type IMECompositionEvent } from "../../../protocol/ime-composition"; @@ -208,7 +210,7 @@ function sceneCameraPose(snapshot: SceneSnapshotIR | null): BlenderViewportCamer w = (m10 - m01) / s; x = (m02 + m20) / s; y = (m12 + m21) / s; z = 0.25 * s; } const fov = camera.projection === "ORTHOGRAPHIC" ? 45 : Math.min(170, Math.max(5, 2 * Math.atan(18 / Math.max(1, camera.lensMm)) * 180 / Math.PI)); - return { position, target, orientation: [w, x, y, z], projection: camera.projection === "ORTHOGRAPHIC" ? "ORTHOGRAPHIC" : "PERSPECTIVE", fov, near: Math.max(0.0001, camera.near), far: Math.max(camera.near + 0.001, camera.far), zoom: 1, orthoHeight: Math.max(0.001, camera.orthoScale ?? 5) }; + return { position, target, orientation: [x, y, z, w], projection: camera.projection === "ORTHOGRAPHIC" ? "ORTHOGRAPHIC" : "PERSPECTIVE", fov, near: Math.max(0.0001, camera.near), far: Math.max(camera.near + 0.001, camera.far), zoom: 1, orthoHeight: Math.max(0.001, camera.orthoScale ?? 5) }; } const IO_FORMAT_UI_REGISTRY = parseIOFormatUIRegistry(ioFormatUIRegistryJSON); @@ -1214,7 +1216,8 @@ function rememberLastProjectId(projectId: string): void { interface AreaProps { className?: string; editor: EditorType; - children: React.ReactNode; + children?: React.ReactNode; + areaId?: string; } const AREA_EDITOR_CHOICES: EditorType[] = [ @@ -1227,6 +1230,12 @@ interface AreaEditorContextValue { overrides: Record; setOverride: (areaKey: string, editor: EditorType | null) => void; render: (editor: EditorType, areaKey: string) => React.ReactNode; + layoutFor: (areaKey: string) => AreaIR | null; + customLayout: boolean; + neighbors: (areaId: string) => Record; + areas: AreaIR[]; + dispatchArea: (areaId: string, operation: AreaOperation) => void; + resetLayout: () => void; } const AreaEditorContext = createContext(null); @@ -1258,23 +1267,71 @@ const EDITOR_ICONS: Record = { }; -function Area({ className = "", editor, children }: AreaProps) { +function Area({ className = "", editor, children, areaId }: AreaProps) { const context = useContext(AreaEditorContext); - const areaKey = className || editor; - const override = className ? context?.overrides[areaKey] : undefined; - const activeEditor = override ?? editor; + const areaKey = areaId ?? (className || editor); + const layout = context?.layoutFor(areaKey) ?? null; + const activeDefault = layout?.editor ?? editor; + const override = className ? context?.overrides[className] : context?.overrides[areaKey]; + const activeEditor = override ?? activeDefault; + const custom = Boolean(context?.customLayout); + if (custom && !layout) return null; + if (custom && layout && !layout.maximized && context?.areas.some((area) => area.maximized)) return null; + const rect = layout?.rect; + const style = custom && layout?.maximized ? { left: "0%", top: "0%", width: "100%", height: "100%" } : custom && rect ? { + left: `${rect.x * 100}%`, + top: `${(1 - rect.y - rect.height) * 100}%`, + width: `${rect.width * 100}%`, + height: `${rect.height * 100}%`, + } : undefined; + const targetId = layout?.id ?? areaKey; + const neighbors = context?.neighbors(targetId) ?? { LEFT: null, RIGHT: null, UP: null, DOWN: null }; + const directionLabel: Record = { LEFT: "左", RIGHT: "右", UP: "上", DOWN: "下" }; + const areaMenuItems: DesktopMenuItem[] = context ? [ + { label: "水平分割(上下)", action: () => context.dispatchArea(targetId, { type: "SPLIT_AREA", areaId: targetId, direction: "HORIZONTAL" as AreaSplitDirection, factor: 0.5 }) }, + { label: "垂直分割(左右)", action: () => context.dispatchArea(targetId, { type: "SPLIT_AREA", areaId: targetId, direction: "VERTICAL" as AreaSplitDirection, factor: 0.5 }) }, + { label: "", separator: true }, + { label: "合并 / 停靠", children: (Object.keys(directionLabel) as AreaDirection[]).map((direction) => ({ + label: `合并到${directionLabel[direction]}侧`, + disabled: !neighbors[direction], + reason: "该方向没有相邻区域", + action: () => { const neighbor = neighbors[direction]; if (neighbor) context.dispatchArea(targetId, { type: "JOIN_AREA", areaId: targetId, targetAreaId: neighbor.id }); }, + })) }, + { label: "交换编辑器", children: context.areas.filter((area) => area.id !== targetId).map((area) => ({ + label: `${area.editor} · ${area.id}`, + action: () => context.dispatchArea(targetId, { type: "SWAP_AREAS", areaId: targetId, targetAreaId: area.id }), + })) }, + { label: "复制区域", action: () => context.dispatchArea(targetId, { type: "DUPLICATE_AREA", areaId: targetId }) }, + { label: "", separator: true }, + { label: layout?.maximized ? "还原大小" : "最大化区域", action: () => context.dispatchArea(targetId, { type: "TOGGLE_MAXIMIZE_AREA", areaId: targetId }) }, + { label: "聚焦模式", action: () => context.dispatchArea(targetId, { type: "TOGGLE_FULLSCREEN_AREA", areaId: targetId }) }, + { label: "关闭区域", disabled: context.areas.length <= 1, reason: "至少保留一个区域", action: () => context.dispatchArea(targetId, { type: "CLOSE_AREA", areaId: targetId }) }, + { label: "", separator: true }, + { label: "重置工作区布局", action: () => context.resetLayout() }, + ] : []; + const newAreaContent = children === undefined && context ? context.render(activeEditor, areaKey) : children; + const useDefaultContent = override ? false : layout ? (layout.editor === editor && children !== undefined) : children !== undefined; return ( -
- {className && context ? ( -