diff --git a/reports/animation-capabilities-20260912/.gitignore b/reports/animation-capabilities-20260912/.gitignore new file mode 100644 index 0000000..267fb3e --- /dev/null +++ b/reports/animation-capabilities-20260912/.gitignore @@ -0,0 +1,3 @@ +offline/ +*-failure.png +failure.png diff --git a/reports/animation-capabilities-20260912/01-animation-settings.png b/reports/animation-capabilities-20260912/01-animation-settings.png new file mode 100644 index 0000000..4a473d1 Binary files /dev/null and b/reports/animation-capabilities-20260912/01-animation-settings.png differ diff --git a/reports/animation-capabilities-20260912/02-editor-end-pose.png b/reports/animation-capabilities-20260912/02-editor-end-pose.png new file mode 100644 index 0000000..b418836 Binary files /dev/null and b/reports/animation-capabilities-20260912/02-editor-end-pose.png differ diff --git a/reports/animation-capabilities-20260912/03-model-reopened.png b/reports/animation-capabilities-20260912/03-model-reopened.png new file mode 100644 index 0000000..b171e03 Binary files /dev/null and b/reports/animation-capabilities-20260912/03-model-reopened.png differ diff --git a/reports/animation-capabilities-20260912/04-model-published.png b/reports/animation-capabilities-20260912/04-model-published.png new file mode 100644 index 0000000..adc4c6d Binary files /dev/null and b/reports/animation-capabilities-20260912/04-model-published.png differ diff --git a/reports/animation-capabilities-20260912/05-guide-model-animation.png b/reports/animation-capabilities-20260912/05-guide-model-animation.png new file mode 100644 index 0000000..17f4331 Binary files /dev/null and b/reports/animation-capabilities-20260912/05-guide-model-animation.png differ diff --git a/reports/animation-capabilities-20260912/06-guide-published.png b/reports/animation-capabilities-20260912/06-guide-published.png new file mode 100644 index 0000000..47f7486 Binary files /dev/null and b/reports/animation-capabilities-20260912/06-guide-published.png differ diff --git a/reports/animation-capabilities-20260912/07-offline-before.png b/reports/animation-capabilities-20260912/07-offline-before.png new file mode 100644 index 0000000..6fb9d23 Binary files /dev/null and b/reports/animation-capabilities-20260912/07-offline-before.png differ diff --git a/reports/animation-capabilities-20260912/08-offline-animation.png b/reports/animation-capabilities-20260912/08-offline-animation.png new file mode 100644 index 0000000..79a74c8 Binary files /dev/null and b/reports/animation-capabilities-20260912/08-offline-animation.png differ diff --git a/reports/animation-capabilities-20260912/09-teaching-animation.png b/reports/animation-capabilities-20260912/09-teaching-animation.png new file mode 100644 index 0000000..e33d335 Binary files /dev/null and b/reports/animation-capabilities-20260912/09-teaching-animation.png differ diff --git a/reports/animation-capabilities-20260912/10-teaching-reset.png b/reports/animation-capabilities-20260912/10-teaching-reset.png new file mode 100644 index 0000000..fb4851d Binary files /dev/null and b/reports/animation-capabilities-20260912/10-teaching-reset.png differ diff --git a/reports/animation-capabilities-20260912/11-training-action-library.png b/reports/animation-capabilities-20260912/11-training-action-library.png new file mode 100644 index 0000000..d2bdaf7 Binary files /dev/null and b/reports/animation-capabilities-20260912/11-training-action-library.png differ diff --git a/reports/animation-capabilities-20260912/12-training-action-bound.png b/reports/animation-capabilities-20260912/12-training-action-bound.png new file mode 100644 index 0000000..cad3368 Binary files /dev/null and b/reports/animation-capabilities-20260912/12-training-action-bound.png differ diff --git a/reports/animation-capabilities-20260912/animation-guide.zip b/reports/animation-capabilities-20260912/animation-guide.zip new file mode 100644 index 0000000..9e17be2 Binary files /dev/null and b/reports/animation-capabilities-20260912/animation-guide.zip differ diff --git a/reports/animation-capabilities-20260912/fixture.json b/reports/animation-capabilities-20260912/fixture.json new file mode 100644 index 0000000..dcd2b78 --- /dev/null +++ b/reports/animation-capabilities-20260912/fixture.json @@ -0,0 +1,3236 @@ +{ + "modelId": "416", + "detail": { + "bodyId": "model-imported-model_mto6wi6k_08-cesiummilktruck-3", + "wheelId": "model-imported-model_mto6wi6k_08-cesiummilktruck-7", + "timeline": { + "version": 1, + "duration": 4, + "currentTime": 4, + "loop": false, + "zoom": 1, + "tracks": [ + { + "pivot": [ + 0, + 0.1, + 0 + ], + "id": "rotate", + "name": "部件旋出", + "targetId": "model-imported-model_mto6wi6k_08-cesiummilktruck-7", + "property": "rotation.x", + "color": "#ffb22e", + "muted": false, + "keyframes": [ + { + "id": "1978a408-d240-4d51-95b5-8d3a1afcf602", + "time": 0, + "value": 0, + "easing": "linear" + }, + { + "id": "f0274daa-ee99-46f1-893a-535ce0872d4c", + "time": 4, + "value": 3.141592653589793, + "easing": "linear" + } + ] + }, + { + "id": "move", + "name": "旋出位移", + "targetId": "model-imported-model_mto6wi6k_08-cesiummilktruck-7", + "property": "position", + "color": "#58c7ff", + "muted": false, + "keyframes": [ + { + "id": "14f32f96-fab2-4f4c-aa73-2542a28a6243", + "time": 0, + "value": [ + 0, + 0, + 0 + ], + "easing": "linear" + }, + { + "id": "a9d04750-bdf9-4280-a6a5-deed5226d4c0", + "time": 4, + "value": [ + 0, + 0.3, + 0 + ], + "easing": "linear" + } + ] + }, + { + "states": [ + { + "name": "污渍", + "color": "#795548", + "roughness": 0.95 + }, + { + "name": "清洁", + "color": "#dce8ed", + "roughness": 0.2 + } + ], + "id": "state", + "name": "污渍到清洁", + "targetId": "model-imported-model_mto6wi6k_08-cesiummilktruck-3", + "property": "material.state", + "color": "#9d77ff", + "muted": false, + "keyframes": [ + { + "id": "18ed5cae-8da2-49d9-b543-83d48a91ea6c", + "time": 0, + "value": 0, + "easing": "linear" + }, + { + "id": "63dda46b-b2eb-4792-b547-38cd65088a94", + "time": 4, + "value": 1, + "easing": "linear" + } + ] + }, + { + "flow": { + "points": [ + [ + 0, + 0, + 0.5 + ], + [ + 1, + 0, + 0.5 + ], + [ + 1, + 1, + 0.5 + ] + ], + "color": "#16b9f3", + "radius": 0.045, + "count": 10, + "speed": 0.6, + "gateTrackId": "state", + "openValue": 1 + }, + "id": "flow", + "name": "回路流向", + "targetId": "model-imported-model_mto6wi6k_08-cesiummilktruck-3", + "property": "flow", + "color": "#4ed6a7", + "muted": false, + "keyframes": [ + { + "id": "9b26bdf5-f3f5-477d-bac8-53c8630f7277", + "time": 0, + "value": 1, + "easing": "linear" + }, + { + "id": "403f9828-5089-4751-aaa0-9e9c58d4e897", + "time": 4, + "value": -1, + "easing": "linear" + } + ] + } + ], + "clips": [ + { + "id": "work", + "name": "旋出与清洁演示", + "start": 0, + "end": 4, + "finish": "hold" + }, + { + "id": "reset", + "name": "阀门复位演示", + "start": 0, + "end": 4, + "finish": "reset" + } + ] + }, + "pose": [ + -1.5407439555097887e-33, + 0.5, + -1.6554003917764114e-18 + ], + "version": 2 + }, + "savedDocument": { + "schema": "digital-twin-editor-project", + "version": 4, + "name": "动画能力验收-组合动作与流向-20260912", + "modelId": "imported-model_mto6wi6k_08-cesiummilktruck", + "modelResource": { + "kind": "imported", + "id": "imported-model_mto6wi6k_08-cesiummilktruck", + "name": "08-CesiumMilkTruck", + "assetCode": "MODEL_MTO6WI6K_08-CESIUMMILKTRUCK", + "code": "MODEL_MTO6WI6K_08-CESIUMMILKTRUCK", + "storageUri": "fsvc://unreal-tran/model/2026/09/05/a75de903d95b0590307632d7bed44111.glb", + "resourceUrl": "", + "sourceName": "08-CesiumMilkTruck.glb", + "sizeBytes": 369980, + "lastModified": 0, + "mimeType": "model/gltf-binary", + "thumbnail": "", + "vertices": 4823, + "triangles": 3624, + "nodes": 10, + "meshes": 5, + "materials": 5, + "textures": 1, + "animations": 1, + "animationDurationSeconds": 1.25, + "modelMetadata": { + "nodeCount": 10, + "meshCount": 5, + "materialCount": 5, + "textureCount": 1, + "animations": [ + { + "index": 0, + "name": "Wheels", + "start": 0, + "end": 1.25, + "duration": 1.25, + "trackCount": 2 + } + ], + "animationDurationSeconds": 1.25 + }, + "nativeAnimations": [ + { + "index": 0, + "name": "Wheels", + "start": 0, + "end": 1.25, + "duration": 1.25, + "trackCount": 2 + } + ] + }, + "meta": { + "description": "用于装备维修实训场景编排、编辑器回归与离线发布验证的系统内置基准工程", + "author": "数字孪生项目组", + "scene": "车间与野外装备维修实训环境", + "tags": [ + "三维场景", + "时间轴动画", + "蓝图交互", + "实训流程" + ], + "migratedFrom": "legacy-model-document", + "source": "content-api-glb", + "sourceName": "08-CesiumMilkTruck.glb", + "assetCode": "MODEL_MTO6WI6K_08-CESIUMMILKTRUCK", + "storageUri": "fsvc://unreal-tran/model/2026/09/05/a75de903d95b0590307632d7bed44111.glb" + }, + "timeline": { + "version": 1, + "duration": 4, + "currentTime": 4, + "loop": false, + "zoom": 1, + "tracks": [ + { + "pivot": [ + 0, + 0.1, + 0 + ], + "id": "rotate", + "name": "部件旋出", + "targetId": "model-imported-model_mto6wi6k_08-cesiummilktruck-7", + "property": "rotation.x", + "color": "#ffb22e", + "muted": false, + "keyframes": [ + { + "id": "1978a408-d240-4d51-95b5-8d3a1afcf602", + "time": 0, + "value": 0, + "easing": "linear" + }, + { + "id": "f0274daa-ee99-46f1-893a-535ce0872d4c", + "time": 4, + "value": 3.141592653589793, + "easing": "linear" + } + ] + }, + { + "id": "move", + "name": "旋出位移", + "targetId": "model-imported-model_mto6wi6k_08-cesiummilktruck-7", + "property": "position", + "color": "#58c7ff", + "muted": false, + "keyframes": [ + { + "id": "14f32f96-fab2-4f4c-aa73-2542a28a6243", + "time": 0, + "value": [ + 0, + 0, + 0 + ], + "easing": "linear" + }, + { + "id": "a9d04750-bdf9-4280-a6a5-deed5226d4c0", + "time": 4, + "value": [ + 0, + 0.3, + 0 + ], + "easing": "linear" + } + ] + }, + { + "states": [ + { + "name": "污渍", + "color": "#795548", + "roughness": 0.95 + }, + { + "name": "清洁", + "color": "#dce8ed", + "roughness": 0.2 + } + ], + "id": "state", + "name": "污渍到清洁", + "targetId": "model-imported-model_mto6wi6k_08-cesiummilktruck-3", + "property": "material.state", + "color": "#9d77ff", + "muted": false, + "keyframes": [ + { + "id": "18ed5cae-8da2-49d9-b543-83d48a91ea6c", + "time": 0, + "value": 0, + "easing": "linear" + }, + { + "id": "63dda46b-b2eb-4792-b547-38cd65088a94", + "time": 4, + "value": 1, + "easing": "linear" + } + ] + }, + { + "flow": { + "points": [ + [ + 0, + 0, + 0.5 + ], + [ + 1, + 0, + 0.5 + ], + [ + 1, + 1, + 0.5 + ] + ], + "color": "#16b9f3", + "radius": 0.045, + "count": 10, + "speed": 0.6, + "gateTrackId": "state", + "openValue": 1 + }, + "id": "flow", + "name": "回路流向", + "targetId": "model-imported-model_mto6wi6k_08-cesiummilktruck-3", + "property": "flow", + "color": "#4ed6a7", + "muted": false, + "keyframes": [ + { + "id": "9b26bdf5-f3f5-477d-bac8-53c8630f7277", + "time": 0, + "value": 1, + "easing": "linear" + }, + { + "id": "403f9828-5089-4751-aaa0-9e9c58d4e897", + "time": 4, + "value": -1, + "easing": "linear" + } + ] + } + ], + "clips": [ + { + "id": "work", + "name": "旋出与清洁演示", + "start": 0, + "end": 4, + "finish": "hold" + }, + { + "id": "reset", + "name": "阀门复位演示", + "start": 0, + "end": 4, + "finish": "reset" + } + ] + }, + "blueprint": { + "version": 1, + "name": "挖掘机作业实训闭环", + "viewport": { + "x": 55, + "y": 55, + "zoom": 0.82 + }, + "nodes": [ + { + "id": "bp-start", + "type": "trigger-click", + "title": "点击挖掘机", + "x": 40, + "y": 55, + "params": { + "event": "click", + "target": "挖掘机" + } + }, + { + "id": "bp-highlight", + "type": "highlight", + "title": "高亮工作装置", + "x": 300, + "y": 55, + "params": { + "target": "铲斗", + "color": "#38e8ff", + "pulse": true + } + }, + { + "id": "bp-message", + "type": "message", + "title": "显示操作提示", + "x": 560, + "y": 55, + "params": { + "title": "挖掘作业实训", + "text": "观察动臂、斗杆和铲斗的协同动作", + "level": "info" + } + }, + { + "id": "bp-play", + "type": "play-timeline", + "title": "播放作业动画", + "x": 820, + "y": 55, + "params": { + "clip": "挖掘机作业实训", + "from": 0, + "speed": 1, + "waitForEnd": true + } + }, + { + "id": "bp-wait", + "type": "wait", + "title": "动作稳定", + "x": 560, + "y": 245, + "params": { + "duration": 600 + } + }, + { + "id": "bp-check", + "type": "condition", + "title": "动画是否完成", + "x": 300, + "y": 245, + "params": { + "variable": "timelineComplete", + "operator": "==", + "value": true + } + }, + { + "id": "bp-retry", + "type": "message", + "title": "等待动画完成", + "x": 820, + "y": 245, + "params": { + "title": "流程校验", + "text": "动画尚未完成,请重新执行", + "level": "warning" + } + }, + { + "id": "bp-finish", + "type": "finish", + "title": "流程完成", + "x": 40, + "y": 245, + "params": { + "result": "success", + "text": "挖掘机作业实训流程完成" + } + } + ], + "edges": [ + { + "id": "bp-e1", + "from": { + "node": "bp-start", + "port": "then" + }, + "to": { + "node": "bp-highlight", + "port": "in" + } + }, + { + "id": "bp-e2", + "from": { + "node": "bp-highlight", + "port": "then" + }, + "to": { + "node": "bp-message", + "port": "in" + } + }, + { + "id": "bp-e3", + "from": { + "node": "bp-message", + "port": "then" + }, + "to": { + "node": "bp-play", + "port": "in" + } + }, + { + "id": "bp-e4", + "from": { + "node": "bp-play", + "port": "then" + }, + "to": { + "node": "bp-wait", + "port": "in" + } + }, + { + "id": "bp-e5", + "from": { + "node": "bp-wait", + "port": "then" + }, + "to": { + "node": "bp-check", + "port": "in" + } + }, + { + "id": "bp-e6", + "from": { + "node": "bp-check", + "port": "true" + }, + "to": { + "node": "bp-finish", + "port": "in" + } + }, + { + "id": "bp-e7", + "from": { + "node": "bp-check", + "port": "false" + }, + "to": { + "node": "bp-retry", + "port": "in" + } + }, + { + "id": "bp-e8", + "from": { + "node": "bp-retry", + "port": "then" + }, + "to": { + "node": "bp-play", + "port": "in" + } + } + ] + }, + "camera": { + "position": [ + -12.62716702347298, + 15.724859987207516, + 11.185421574292878 + ], + "target": [ + 0, + 2.8850664334213203, + 0.67571590810515 + ] + }, + "model": { + "objects": { + "equipment-root": { + "name": "08-CesiumMilkTruck", + "nodeName": "08-CesiumMilkTruck", + "nodePath": "@root", + "interactionId": "interaction-182987e1", + "capabilities": { + "selectable": true, + "transform": true, + "visible": true, + "material": false, + "animatable": true + }, + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [] + }, + "model-imported-model_mto6wi6k_08-cesiummilktruck-0": { + "name": "Scene", + "nodeName": "Scene", + "nodePath": "0", + "interactionId": "interaction-9beb9729", + "capabilities": { + "selectable": true, + "transform": true, + "visible": true, + "material": false, + "animatable": true + }, + "position": [ + 0, + 0.1328080408740111, + -0.00684373075768363 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1.9306168791349512, + 1.9306168791349512, + 1.9306168791349512 + ], + "visible": true, + "customComponents": [] + }, + "model-imported-model_mto6wi6k_08-cesiummilktruck-1": { + "name": "Yup2Zup", + "nodeName": "Yup2Zup", + "nodePath": "0/0", + "interactionId": "interaction-1ba581e6", + "capabilities": { + "selectable": true, + "transform": true, + "visible": true, + "material": false, + "animatable": true + }, + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 1.57079626719025, + 0, + 1.5707963267948966 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [] + }, + "model-imported-model_mto6wi6k_08-cesiummilktruck-2": { + "name": "Cesium_Milk_Truck", + "nodeName": "Cesium_Milk_Truck", + "nodePath": "0/0/0", + "interactionId": "interaction-004dec41", + "capabilities": { + "selectable": true, + "transform": true, + "visible": true, + "material": false, + "animatable": true + }, + "position": [ + 0.35, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [] + }, + "model-imported-model_mto6wi6k_08-cesiummilktruck-3": { + "name": "Cesium_Milk_Truck_1", + "nodeName": "Cesium_Milk_Truck_1", + "nodePath": "0/0/0/0", + "interactionId": "interaction-5f7e4bee", + "capabilities": { + "selectable": true, + "transform": true, + "visible": true, + "material": true, + "animatable": true + }, + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "ffffff", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0, + "roughness": 0.3, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "model-imported-model_mto6wi6k_08-cesiummilktruck-4": { + "name": "Cesium_Milk_Truck_2", + "nodeName": "Cesium_Milk_Truck_2", + "nodePath": "0/0/0/1", + "interactionId": "interaction-607e4d81", + "capabilities": { + "selectable": true, + "transform": true, + "visible": true, + "material": true, + "animatable": true + }, + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "003928", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0, + "roughness": 1, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "model-imported-model_mto6wi6k_08-cesiummilktruck-5": { + "name": "Cesium_Milk_Truck_3", + "nodeName": "Cesium_Milk_Truck_3", + "nodePath": "0/0/0/2", + "interactionId": "interaction-5d7e48c8", + "capabilities": { + "selectable": true, + "transform": true, + "visible": true, + "material": true, + "animatable": true + }, + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "484848", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0, + "roughness": 1, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "model-imported-model_mto6wi6k_08-cesiummilktruck-6": { + "name": "Node", + "nodeName": "Node", + "nodePath": "0/0/0/3", + "interactionId": "interaction-5e7e4a5b", + "capabilities": { + "selectable": true, + "transform": true, + "visible": true, + "material": false, + "animatable": true + }, + "position": [ + 1.432669997215271, + 0, + -0.4277220070362091 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [] + }, + "model-imported-model_mto6wi6k_08-cesiummilktruck-7": { + "name": "Wheels", + "nodeName": "Wheels", + "nodePath": "0/0/0/3/0", + "interactionId": "interaction-ce2feac4", + "capabilities": { + "selectable": true, + "transform": true, + "visible": true, + "material": true, + "animatable": true + }, + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + -3.141592653589793, + -0.603571861900947, + -3.141592653589793 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "ffffff", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0, + "roughness": 1, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "model-imported-model_mto6wi6k_08-cesiummilktruck-8": { + "name": "Node001", + "nodeName": "Node001", + "nodePath": "0/0/0/4", + "interactionId": "interaction-637e523a", + "capabilities": { + "selectable": true, + "transform": true, + "visible": true, + "material": false, + "animatable": true + }, + "position": [ + -1.352329969406128, + 0, + -0.4277220070362091 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [] + }, + "model-imported-model_mto6wi6k_08-cesiummilktruck-9": { + "name": "Wheels001", + "nodeName": "Wheels001", + "nodePath": "0/0/0/4/0", + "interactionId": "interaction-f417f82d", + "capabilities": { + "selectable": true, + "transform": true, + "visible": true, + "material": true, + "animatable": true + }, + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + -3.141592653589793, + -0.603571861900947, + -3.141592653589793 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "ffffff", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0, + "roughness": 1, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + } + }, + "selectionScope": "part", + "nativeAnimation": { + "clipIndex": 0, + "clipName": "Wheels" + } + }, + "environment": { + "mode": "placeholder", + "environmentType": "outdoor", + "sourceName": "内置野外综合训练环境", + "assetCode": "", + "storageUri": "", + "resourceUrl": "", + "sizeBytes": 0, + "objects": { + "outdoor-scene": { + "name": "野外综合训练环境", + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [] + }, + "outdoor-site": { + "name": "场地与道路", + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [] + }, + "outdoor-ground": { + "name": "室外训练场地", + "position": [ + 0, + -0.065, + 0 + ], + "rotation": [ + -1.5707963267948966, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "263529", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0, + "roughness": 0.98, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-apron": { + "name": "装备操作坪", + "position": [ + 0, + -0.045, + 0 + ], + "rotation": [ + -1.5707963267948966, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "39444a", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.03, + "roughness": 0.92, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-road": { + "name": "保障通道", + "position": [ + 0, + -0.035, + 17 + ], + "rotation": [ + -1.5707963267948966, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "202a31", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.02, + "roughness": 0.95, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-road-line-1": { + "name": "道路中心线 1", + "position": [ + -28.699999999999996, + 0.01, + 17 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-road-line-2": { + "name": "道路中心线 2", + "position": [ + -24.599999999999998, + 0.01, + 17 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-road-line-3": { + "name": "道路中心线 3", + "position": [ + -20.5, + 0.01, + 17 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-road-line-4": { + "name": "道路中心线 4", + "position": [ + -16.4, + 0.01, + 17 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-road-line-5": { + "name": "道路中心线 5", + "position": [ + -12.299999999999999, + 0.01, + 17 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-road-line-6": { + "name": "道路中心线 6", + "position": [ + -8.2, + 0.01, + 17 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-road-line-7": { + "name": "道路中心线 7", + "position": [ + -4.1, + 0.01, + 17 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-road-line-8": { + "name": "道路中心线 8", + "position": [ + 0, + 0.01, + 17 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-road-line-9": { + "name": "道路中心线 9", + "position": [ + 4.1, + 0.01, + 17 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-road-line-10": { + "name": "道路中心线 10", + "position": [ + 8.2, + 0.01, + 17 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-road-line-11": { + "name": "道路中心线 11", + "position": [ + 12.299999999999999, + 0.01, + 17 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-road-line-12": { + "name": "道路中心线 12", + "position": [ + 16.4, + 0.01, + 17 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-road-line-13": { + "name": "道路中心线 13", + "position": [ + 20.5, + 0.01, + 17 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-road-line-14": { + "name": "道路中心线 14", + "position": [ + 24.599999999999998, + 0.01, + 17 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-road-line-15": { + "name": "道路中心线 15", + "position": [ + 28.699999999999996, + 0.01, + 17 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety--17.6--11": { + "name": "操作区安全标线", + "position": [ + -17.6, + 0.012, + -11 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety--17.6--8.4": { + "name": "操作区安全标线", + "position": [ + -17.6, + 0.012, + -8.4 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety--17.6--5.800000000000001": { + "name": "操作区安全标线", + "position": [ + -17.6, + 0.012, + -5.800000000000001 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety--17.6--3.2000000000000006": { + "name": "操作区安全标线", + "position": [ + -17.6, + 0.012, + -3.2000000000000006 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety--17.6--0.6000000000000005": { + "name": "操作区安全标线", + "position": [ + -17.6, + 0.012, + -0.6000000000000005 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety--17.6-1.9999999999999996": { + "name": "操作区安全标线", + "position": [ + -17.6, + 0.012, + 1.9999999999999996 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety--17.6-4.6": { + "name": "操作区安全标线", + "position": [ + -17.6, + 0.012, + 4.6 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety--17.6-7.199999999999999": { + "name": "操作区安全标线", + "position": [ + -17.6, + 0.012, + 7.199999999999999 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety--17.6-9.799999999999999": { + "name": "操作区安全标线", + "position": [ + -17.6, + 0.012, + 9.799999999999999 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety-17.6--11": { + "name": "操作区安全标线", + "position": [ + 17.6, + 0.012, + -11 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety-17.6--8.4": { + "name": "操作区安全标线", + "position": [ + 17.6, + 0.012, + -8.4 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety-17.6--5.800000000000001": { + "name": "操作区安全标线", + "position": [ + 17.6, + 0.012, + -5.800000000000001 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety-17.6--3.2000000000000006": { + "name": "操作区安全标线", + "position": [ + 17.6, + 0.012, + -3.2000000000000006 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety-17.6--0.6000000000000005": { + "name": "操作区安全标线", + "position": [ + 17.6, + 0.012, + -0.6000000000000005 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety-17.6-1.9999999999999996": { + "name": "操作区安全标线", + "position": [ + 17.6, + 0.012, + 1.9999999999999996 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety-17.6-4.6": { + "name": "操作区安全标线", + "position": [ + 17.6, + 0.012, + 4.6 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety-17.6-7.199999999999999": { + "name": "操作区安全标线", + "position": [ + 17.6, + 0.012, + 7.199999999999999 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-safety-17.6-9.799999999999999": { + "name": "操作区安全标线", + "position": [ + 17.6, + 0.012, + 9.799999999999999 + ], + "rotation": [ + 0, + 0.5654866776461628, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "e5c64c", + "emissive": "302600", + "emissiveIntensity": 0.18, + "metalness": 0.08, + "roughness": 0.72, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-workshop": { + "name": "维修保障厂房", + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [] + }, + "outdoor-workshop-body": { + "name": "维修保障厂房主体", + "position": [ + -10, + 3.25, + -18 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "536675", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.18, + "roughness": 0.78, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-workshop-roof": { + "name": "厂房金属屋面", + "position": [ + -10, + 6.65, + -18 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "263846", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.35, + "roughness": 0.62, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-workshop-window-1": { + "name": "厂房观察窗 1", + "position": [ + -17, + 3.7, + -14.21 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "4a89a2", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.12, + "roughness": 0.25, + "opacity": 0.68, + "transparent": true, + "depthWrite": true + } + ] + }, + "outdoor-workshop-window-2": { + "name": "厂房观察窗 2", + "position": [ + -12.4, + 3.7, + -14.21 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "4a89a2", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.12, + "roughness": 0.25, + "opacity": 0.68, + "transparent": true, + "depthWrite": true + } + ] + }, + "outdoor-workshop-window-3": { + "name": "厂房观察窗 3", + "position": [ + -7.800000000000001, + 3.7, + -14.21 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "4a89a2", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.12, + "roughness": 0.25, + "opacity": 0.68, + "transparent": true, + "depthWrite": true + } + ] + }, + "outdoor-workshop-window-4": { + "name": "厂房观察窗 4", + "position": [ + -3.200000000000001, + 3.7, + -14.21 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "4a89a2", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.12, + "roughness": 0.25, + "opacity": 0.68, + "transparent": true, + "depthWrite": true + } + ] + }, + "outdoor-workshop-door": { + "name": "装备出入门", + "position": [ + -3.6, + 2.16, + -14.18 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "202a31", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.02, + "roughness": 0.95, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-canopy": { + "name": "室外检修棚", + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [] + }, + "outdoor-canopy-roof": { + "name": "检修棚顶", + "position": [ + 18, + 5.05, + -8 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "263846", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.35, + "roughness": 0.62, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-canopy-column-1": { + "name": "检修棚立柱 1", + "position": [ + 12.7, + 2.5, + -11 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-canopy-column-2": { + "name": "检修棚立柱 2", + "position": [ + 23.3, + 2.5, + -11 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-canopy-column-3": { + "name": "检修棚立柱 3", + "position": [ + 12.7, + 2.5, + -5 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-canopy-column-4": { + "name": "检修棚立柱 4", + "position": [ + 23.3, + 2.5, + -5 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-service-box": { + "name": "室外工具柜", + "position": [ + 22.2, + 1.05, + -9.8 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "d98d2b", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.12, + "roughness": 0.7, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-perimeter": { + "name": "场地边界与安全设施", + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [] + }, + "outdoor-fence-back-1": { + "name": "后侧围栏立柱 1", + "position": [ + -29, + 1.1, + -25 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-fence-back-2": { + "name": "后侧围栏立柱 2", + "position": [ + -23.2, + 1.1, + -25 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-fence-back-3": { + "name": "后侧围栏立柱 3", + "position": [ + -17.4, + 1.1, + -25 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-fence-back-4": { + "name": "后侧围栏立柱 4", + "position": [ + -11.6, + 1.1, + -25 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-fence-back-5": { + "name": "后侧围栏立柱 5", + "position": [ + -5.8, + 1.1, + -25 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-fence-back-6": { + "name": "后侧围栏立柱 6", + "position": [ + 0, + 1.1, + -25 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-fence-back-7": { + "name": "后侧围栏立柱 7", + "position": [ + 5.8, + 1.1, + -25 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-fence-back-8": { + "name": "后侧围栏立柱 8", + "position": [ + 11.6, + 1.1, + -25 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-fence-back-9": { + "name": "后侧围栏立柱 9", + "position": [ + 17.4, + 1.1, + -25 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-fence-back-10": { + "name": "后侧围栏立柱 10", + "position": [ + 23.2, + 1.1, + -25 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-fence-back-11": { + "name": "后侧围栏立柱 11", + "position": [ + 29, + 1.1, + -25 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-fence-rail-a": { + "name": "后侧围栏上横杆", + "position": [ + 0, + 1.85, + -25 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-fence-rail-b": { + "name": "后侧围栏下横杆", + "position": [ + 0, + 0.75, + -25 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-scene-sign": { + "name": "室外实训区域标识", + "position": [ + 26, + 2.4, + 11.5 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "536675", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.18, + "roughness": 0.78, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-scene-sign-post-a": { + "name": "区域标识支柱 A", + "position": [ + 24, + 1.4, + 11.5 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + }, + "outdoor-scene-sign-post-b": { + "name": "区域标识支柱 B", + "position": [ + 28, + 1.4, + 11.5 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ], + "visible": true, + "customComponents": [], + "materials": [ + { + "color": "66757c", + "emissive": "000000", + "emissiveIntensity": 1, + "metalness": 0.48, + "roughness": 0.56, + "opacity": 1, + "transparent": false, + "depthWrite": true + } + ] + } + } + }, + "runtime": { + "startMode": "guided", + "showStepPanel": true, + "showProgress": true, + "allowReplay": true, + "language": "zh-CN" + }, + "publish": { + "target": "standalone-html", + "deployment": "offline", + "includeProjectData": true, + "includeViewportPreview": true + } + }, + "modelVersionId": "479", + "guideCandidateId": "417", + "guide": { + "id": "417", + "resource": { + "id": "416@479", + "tracks": 4, + "clips": 2, + "overrides": 11 + } + }, + "guideId": "417", + "animationSceneId": "418", + "animationSceneVersionId": "481", + "animationTrainingId": "419", + "animationTrainingVersionId": "483", + "animationAssignmentId": "55" +} \ No newline at end of file diff --git a/reports/animation-capabilities-20260912/guide-checks.json b/reports/animation-capabilities-20260912/guide-checks.json new file mode 100644 index 0000000..1455d0f --- /dev/null +++ b/reports/animation-capabilities-20260912/guide-checks.json @@ -0,0 +1,41 @@ +{ + "checks": [ + { + "name": "指导书通过正式接口固定引用模型版本并持久化动画元数据", + "pass": true, + "resource": { + "id": "416@479", + "tracks": 4, + "clips": 2, + "overrides": 11 + } + }, + { + "name": "正式指导书视口加载命名动作并还原组合运动与材质状态", + "pass": true, + "detail": { + "clips": [ + "Wheels", + "旋出与清洁演示", + "阀门复位演示" + ], + "initial": [ + 0, + 0, + 0 + ], + "final": [ + -1.5407439555097887e-33, + 0.5, + -1.6554003917764114e-18 + ], + "state": 0.2 + } + }, + { + "name": "正式发布并下载带动画的离线指导书", + "pass": true + } + ], + "errors": [] +} \ No newline at end of file diff --git a/reports/animation-capabilities-20260912/index.html b/reports/animation-capabilities-20260912/index.html new file mode 100644 index 0000000..923299e --- /dev/null +++ b/reports/animation-capabilities-20260912/index.html @@ -0,0 +1,22 @@ +动画能力实施与测试报告 · 2026-09-12 + +

数字车间 · 功能实施 / 真实接口 / 浏览器端到端

模型动画能力实施与测试报告

2026-09-12 · 14 项端到端检查通过

按已确认范围补齐命名动作、组合运动、材质状态、流向讲解和跨模块复用。使用独立的牛奶卡车测试工程验证,不修改原始模型和既有业务任务。

+

已实施能力

+ + + +
能力实现与使用
动作片段与机械组合运动时间轴设置命名起止范围;旋转中心使用部件局部坐标;位移与 XYZ 旋转叠加,关键帧错开形成顺序动作;支持保持末帧和回到起点。蓝图按名称播放片段。
状态与材质新增缩放、显隐、不透明度、颜色、材质状态轨道;材质副本隔离,不污染其他实例。录制与保存保留中性基准,旋转中心偏移不会被重复写入位移关键帧。
回路流向配置路径点、颜色、速度、流点数量、正向/反向/停止;可关联标量轨道控制开通。用于工作原理讲解。
跨模块复用训练编辑器从正式目录选动作,保存步骤绑定;教学任务固定模型/场景/训练版本;指导书在线模型与离线 ZIP 使用同一动画求值规则。原生 GLB 动画仍可选择。
+

实测结果

模型编辑与发布

检查结果
真实工程保存组合旋转、位移、材质状态和流向轨道通过
刷新重开后动作片段、复位基准和流向配置恢复通过
正式发布固定动画模型版本通过

查看原始检查记录 · 浏览器脚本异常 0 条

指导书在线使用与交付

检查结果
指导书通过正式接口固定引用模型版本并持久化动画元数据通过
正式指导书视口加载命名动作并还原组合运动与材质状态通过
正式发布并下载带动画的离线指导书通过

查看原始检查记录 · 浏览器脚本异常 0 条

离线播放

检查结果
下载的离线包可播放命名组合动作,画布发生变化并可暂停通过
断开业务服务与公网后仍可加载模型、引擎和动画通过

查看原始检查记录 · 浏览器脚本异常 0 条

正式教学视口

检查结果
真实教学任务固定模型版本、动作、材质、枢轴和流向配置通过
正式教学预览实际播放指定片段并保持末帧,四条轨道全部绑定通过
切换步骤后播放复位片段,结束恢复起点并关闭流向通过
同一教学视口原生 GLB 动画与自编片段切换兼容(浏览器运行时检查)通过

查看原始检查记录 · 浏览器脚本异常 0 条

训练编辑器绑定

检查结果
训练编辑器从正式模型库选择命名动作并通过真实保存接口持久化通过
训练编辑草稿不会覆盖已创建任务的固定发布快照通过

查看原始检查记录 · 浏览器脚本异常 0 条

+

补充验证:前端类型检查及生产构建通过;生成的模型编辑器 public/dist 文件哈希一致。后端 18 条校验、快照与考试提示隔离用例通过;原型 5 项合同测试、Web 4 项场景/缓存/落地测试通过。构建仅有现有的大分块提示。

+

测试采用真实登录、GLB、保存/发布/下载接口。部分配置和视口内部状态由 Playwright 调用页面模块核对;教学原生 GLB 切换为浏览器运行时兼容检查。离线用例阻断业务接口及公网请求,外部请求数为 0。

+

可继续使用的测试数据

+ + + + +
数据标识入口
组合动作模型416 · 固定模型版本 479打开模型编辑器
测试场景418 · 版本 481打开场景
两步骤测试训练419打开训练编排
固定快照教学任务55 · 训练版本 482打开教学预览
动画指导书417打开指导书 · 下载实测离线包
+

训练当前保留实测保存的草稿;已有任务仍使用旧发布快照,这正是版本隔离测试验证的行为。试验性教学任务未产生学员成绩。

+

使用顺序与边界

  1. 模型编辑器时间轴添加轨道,选择部件、属性及关键帧;进入「动作与轨道设置」配置片段、枢轴、材质状态或流向。
  2. 保存并发布模型,在场景选择固定版本;训练的「步骤动画」选择正式动作并保存。
  3. 发布训练、下发新任务后在教学端使用;指导书关联模型片段后可在线播放或导出离线 ZIP。
+

本次没有数据库表改动。未加入新的密钥文件、独立服务或认证方式。代码尚未提交 Git。

+

污渍/清洁通过颜色和粗糙度等 PBR 参数表达;贴图擦除、软管/密封圈弹性变形、真实流体压力计算、整片视频导出不在本次实施范围。表格中的具体装备动作仍需配置部件、路径和时序,本报告不代表已经制作完成全部动画素材。

+

浏览器截图

点击图片查看原图。

动作与轨道设置:范围、结束处理、路径和阀门联动
动作与轨道设置:范围、结束处理、路径和阀门联动
模型编辑器中的组合动作
模型编辑器中的组合动作
刷新重开后恢复动画配置
刷新重开后恢复动画配置
正式指导书模型视口
正式指导书模型视口
离线包加载模型
离线包加载模型
离线片段播放及暂停
离线片段播放及暂停
教学步骤:保持末帧、清洁状态和流向开通
教学步骤:保持末帧、清洁状态和流向开通
教学步骤:播放结束复位并关闭流向
教学步骤:播放结束复位并关闭流向
训练编辑器读取正式动作目录
训练编辑器读取正式动作目录
通过页面绑定并保存动作
通过页面绑定并保存动作
\ No newline at end of file diff --git a/reports/animation-capabilities-20260912/model-checks.json b/reports/animation-capabilities-20260912/model-checks.json new file mode 100644 index 0000000..8dfd2e4 --- /dev/null +++ b/reports/animation-capabilities-20260912/model-checks.json @@ -0,0 +1,47 @@ +{ + "checks": [ + { + "name": "真实工程保存组合旋转、位移、材质状态和流向轨道", + "pass": true + }, + { + "name": "刷新重开后动作片段、复位基准和流向配置恢复", + "pass": true, + "detail": { + "a": [ + 0, + 0, + 0 + ], + "b": [ + -1.5407439555097887e-33, + 0.5, + -1.6554003917764114e-18 + ], + "clips": [ + { + "id": "work", + "name": "旋出与清洁演示", + "start": 0, + "end": 4, + "finish": "hold" + }, + { + "id": "reset", + "name": "阀门复位演示", + "start": 0, + "end": 4, + "finish": "reset" + } + ], + "flows": 1 + } + }, + { + "name": "正式发布固定动画模型版本", + "pass": true, + "versionId": "479" + } + ], + "errors": [] +} \ No newline at end of file diff --git a/reports/animation-capabilities-20260912/offline-checks.json b/reports/animation-capabilities-20260912/offline-checks.json new file mode 100644 index 0000000..0bb49d8 --- /dev/null +++ b/reports/animation-capabilities-20260912/offline-checks.json @@ -0,0 +1,14 @@ +{ + "checks": [ + { + "name": "下载的离线包可播放命名组合动作,画布发生变化并可暂停", + "pass": true + }, + { + "name": "断开业务服务与公网后仍可加载模型、引擎和动画", + "pass": true + } + ], + "errors": [], + "external": [] +} \ No newline at end of file diff --git a/reports/animation-capabilities-20260912/report-cover.png b/reports/animation-capabilities-20260912/report-cover.png new file mode 100644 index 0000000..4fc25b5 Binary files /dev/null and b/reports/animation-capabilities-20260912/report-cover.png differ diff --git a/reports/animation-capabilities-20260912/summary.json b/reports/animation-capabilities-20260912/summary.json new file mode 100644 index 0000000..4d10cd7 --- /dev/null +++ b/reports/animation-capabilities-20260912/summary.json @@ -0,0 +1,10 @@ +{ + "date": "2026-09-12", + "passed": 14, + "failed": 0, + "backendTests": 18, + "prototypeContractTests": 5, + "webTests": 4, + "build": "passed", + "ddlChanges": false +} \ No newline at end of file diff --git a/reports/animation-capabilities-20260912/teaching-checks.json b/reports/animation-capabilities-20260912/teaching-checks.json new file mode 100644 index 0000000..6fd2157 --- /dev/null +++ b/reports/animation-capabilities-20260912/teaching-checks.json @@ -0,0 +1,67 @@ +{ + "checks": [ + { + "name": "真实教学任务固定模型版本、动作、材质、枢轴和流向配置", + "pass": true, + "modelVersionId": "479", + "assignmentId": "55" + }, + { + "name": "正式教学预览实际播放指定片段并保持末帧,四条轨道全部绑定", + "pass": true, + "detail": { + "status": "已加载发布训练场景 · 1 个对象", + "time": 4, + "bindings": 4, + "unresolved": 0, + "wheel": [ + -1.5407439555097887e-33, + 0.5, + -1.6554003917764114e-18 + ], + "base": [ + 0, + 0, + 0 + ], + "roughness": 0.2, + "actualRoughness": 0.2, + "flowVisible": true, + "step": "animation-step" + } + }, + { + "name": "切换步骤后播放复位片段,结束恢复起点并关闭流向", + "pass": true, + "detail": { + "status": "已加载发布训练场景 · 1 个对象", + "time": 0, + "bindings": 4, + "unresolved": 0, + "wheel": [ + 0, + 0, + 0 + ], + "base": [ + 0, + 0, + 0 + ], + "roughness": 0.95, + "actualRoughness": 0.95, + "flowVisible": false, + "step": "reset-step" + } + }, + { + "name": "同一教学视口原生 GLB 动画与自编片段切换兼容(浏览器运行时检查)", + "pass": true, + "detail": { + "name": "Wheels", + "time": 0.8832000000000001 + } + } + ], + "errors": [] +} \ No newline at end of file diff --git a/reports/animation-capabilities-20260912/teaching-inspection.json b/reports/animation-capabilities-20260912/teaching-inspection.json new file mode 100644 index 0000000..3f31125 --- /dev/null +++ b/reports/animation-capabilities-20260912/teaching-inspection.json @@ -0,0 +1,20 @@ +{ + "status": "已加载发布训练场景 · 1 个对象", + "time": 4, + "bindings": 4, + "unresolved": 0, + "wheel": [ + -1.5407439555097887e-33, + 0.5, + -1.6554003917764114e-18 + ], + "base": [ + 0, + 0, + 0 + ], + "roughness": 0.2, + "actualRoughness": 0.2, + "flowVisible": true, + "step": "animation-step" +} \ No newline at end of file diff --git a/reports/animation-capabilities-20260912/training-editor-checks.json b/reports/animation-capabilities-20260912/training-editor-checks.json new file mode 100644 index 0000000..b1cd3b2 --- /dev/null +++ b/reports/animation-capabilities-20260912/training-editor-checks.json @@ -0,0 +1,24 @@ +{ + "checks": [ + { + "name": "训练编辑器从正式模型库选择命名动作并通过真实保存接口持久化", + "pass": true, + "binding": { + "assetId": "animation-content-model-416-479-work", + "name": "动画能力验收-组合动作与流向-20260912 · 旋出与清洁演示", + "modelAssetId": "content-model-416-479", + "clipName": "旋出与清洁演示", + "sourceStart": 0, + "finish": "hold", + "duration": 4, + "trackCount": 4, + "revision": 1 + } + }, + { + "name": "训练编辑草稿不会覆盖已创建任务的固定发布快照", + "pass": true + } + ], + "errors": [] +} \ No newline at end of file diff --git a/reports/guide-template-actions-20260912/1440.png b/reports/guide-template-actions-20260912/1440.png new file mode 100644 index 0000000..15ccb04 Binary files /dev/null and b/reports/guide-template-actions-20260912/1440.png differ diff --git a/reports/guide-template-actions-20260912/1920.png b/reports/guide-template-actions-20260912/1920.png new file mode 100644 index 0000000..2b1177c Binary files /dev/null and b/reports/guide-template-actions-20260912/1920.png differ diff --git a/reports/guide-template-actions-20260912/dark.png b/reports/guide-template-actions-20260912/dark.png new file mode 100644 index 0000000..08ed113 Binary files /dev/null and b/reports/guide-template-actions-20260912/dark.png differ diff --git a/reports/guide-template-actions-20260912/index.html b/reports/guide-template-actions-20260912/index.html new file mode 100644 index 0000000..77124ff --- /dev/null +++ b/reports/guide-template-actions-20260912/index.html @@ -0,0 +1 @@ +模板库操作区样式修复

模板库操作区样式修复

2026-09-12 · 开发环境 · 内部维护账号 · Edge / Playwright

原因:操作区默认 opacity:0、max-height:0,仅在 hover / focus-within 展开;展开的高度限制也裁切了按钮底边。

修复:预览与下载始终显示,操作区按内容自然撑高,移除悬停折叠及移动端残留高度限制。

验证:1920、1440、750 宽度下操作区可见、按钮未裁切、悬停前后卡片高度稳定;下载菜单可打开,明暗主题均已检查。无业务数据写入。pnpm run build 与 git diff --check 通过。

检查结果 JSON

1920 · 默认显示全部操作

1920 宽度模板卡片操作始终可见

1440 · 窄桌面

1440 宽度模板卡片操作

暗色主题

暗色主题操作区
diff --git a/reports/guide-template-actions-20260912/results.json b/reports/guide-template-actions-20260912/results.json new file mode 100644 index 0000000..98d64ed --- /dev/null +++ b/reports/guide-template-actions-20260912/results.json @@ -0,0 +1,26 @@ +[ + { + "width": 1920, + "normalVisible": true, + "buttonsNotClipped": true, + "hoverHeightStable": true + }, + { + "width": 1440, + "normalVisible": true, + "buttonsNotClipped": true, + "hoverHeightStable": true + }, + { + "width": 750, + "normalVisible": true, + "buttonsNotClipped": true, + "hoverHeightStable": true + }, + { + "downloadMenuOpens": true + }, + { + "darkActionsVisible": true + } +] \ No newline at end of file diff --git a/tools/animation-capabilities-live.mjs b/tools/animation-capabilities-live.mjs new file mode 100644 index 0000000..91dd677 --- /dev/null +++ b/tools/animation-capabilities-live.mjs @@ -0,0 +1,70 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import assert from 'node:assert/strict'; +import {chromium,expect} from '@playwright/test'; +import {login,call} from './role-permissions-live.mjs'; +const report=path.resolve('reports/animation-capabilities-20260912');fs.mkdirSync(report,{recursive:true}); +const fixturePath=path.join(report,'fixture.json'); +const fixture=fs.existsSync(fixturePath)?JSON.parse(fs.readFileSync(fixturePath,'utf8')):{}; +const save=()=>fs.writeFileSync(fixturePath,JSON.stringify(fixture,null,2)); +const session=await login('root'); +if(!fixture.modelId){ + const source=await call('tran/v1/content/projects/156',session.accessToken);assert.equal(source.status,200); + const copy=await call('tran/v1/content/projects/156/copy',session.accessToken,{name:'动画能力验收-组合动作与流向-20260912',version:source.data.project.version}); + assert.equal(copy.status,200,copy.message);fixture.modelId=copy.data.project.id;save(); +} +const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}); +const context=await browser.newContext({viewport:{width:1920,height:1080}}); +await context.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken)},session); +const page=await context.newPage(),checks=[],errors=[]; +page.on('pageerror',error=>errors.push(error.message)); +const shot=async name=>page.screenshot({path:path.join(report,name+'.png')}); +try { + await page.goto(`http://127.0.0.1:6180/content/models/${fixture.modelId}/edit`); + await expect(page.locator('iframe')).toBeVisible({timeout:60000}); + const frame=await page.locator('iframe').contentFrame(); + await expect(frame.locator('#scene-count')).toBeVisible({timeout:60000}); + const editorFrame=page.frames().find(f=>f.url().includes('legacy-model-editor')); + await editorFrame.waitForFunction(()=>window.editorApp?.apiSession?.project && window.editorApp?.modelTargets?.size>0,{timeout:60000}); + const detail=await editorFrame.evaluate(async()=>{ + const app=window.editorApp;app.timeline.pause(); + const body=[...app.modelTargets.entries()].find(([,object])=>object.name==='Cesium_Milk_Truck_1'); + const wheel=[...app.modelTargets.entries()].find(([,object])=>object.name==='Wheels'); + if(!body||!wheel)throw new Error('Fixture body/wheel not found'); + const keys=(a,b)=>[{id:crypto.randomUUID(),time:0,value:a},{id:crypto.randomUUID(),time:4,value:b}]; + const track=(id,name,targetId,property,a,b,extra={})=>({id,name,targetId,property,keyframes:keys(a,b),...extra}); + const timeline={version:1,duration:4,currentTime:0,loop:false,zoom:1,clips:[{id:'work',name:'旋出与清洁演示',start:0,end:4,finish:'hold'},{id:'reset',name:'阀门复位演示',start:0,end:4,finish:'reset'}],tracks:[ + track('rotate','部件旋出',wheel[0],'rotation.x',0,Math.PI,{pivot:[0,.1,0]}), + track('move','旋出位移',wheel[0],'position',[0,0,0],[0,.3,0]), + track('state','污渍到清洁',body[0],'material.state',0,1,{states:[{name:'污渍',color:'#795548',roughness:.95},{name:'清洁',color:'#dce8ed',roughness:.2}]}), + track('flow','回路流向',body[0],'flow',1,-1,{flow:{points:[[0,0,.5],[1,0,.5],[1,1,.5]],color:'#16b9f3',radius:.045,count:10,speed:.6,gateTrackId:'state',openValue:1}}), + ]}; + app.timeline.loadJSON(timeline,{pushHistory:true});app.timeline.seek(4); + const saved=await app.saveProject();if(!saved)throw new Error('Model save failed'); + return {bodyId:body[0],wheelId:wheel[0],timeline:saved.content?.timeline || timeline,pose:wheel[1].position.toArray(),version:saved.version}; + }); + fixture.detail=detail;save(); + checks.push({name:'真实工程保存组合旋转、位移、材质状态和流向轨道',pass:true}); + await editorFrame.evaluate(()=>{window.editorApp.setBottomMode?.('timeline');window.editorApp.timeline.selectedTrackId='flow';window.editorApp.timeline.render();}); + await frame.locator('[data-action="animation-settings"]').click(); + await expect(frame.locator('.animation-settings')).toBeVisible();await shot('01-animation-settings'); + await frame.locator('.animation-settings [data-close]').first().click(); + await shot('02-editor-end-pose'); + const latest=await call(`tran/v1/content/projects/${fixture.modelId}`,session.accessToken); + assert.equal(latest.data.currentVersion.content.timeline.clips.length,2); + fixture.savedDocument=latest.data.currentVersion.content;save(); + await page.reload();await expect(page.locator('iframe')).toBeVisible(); + const reopened=page.frames().find(f=>f.url().includes('legacy-model-editor')); + await reopened.waitForFunction(()=>window.editorApp?.apiSession?.project && window.editorApp?.modelTargets?.size>0,{timeout:60000}); + const replay=await reopened.evaluate(()=>{const app=window.editorApp;app.timeline.seek(0);const a=app.modelTargets.get(app.timeline.getTrack('move').targetId).position.toArray();app.timeline.seek(4);const b=app.modelTargets.get(app.timeline.getTrack('move').targetId).position.toArray();return {a,b,clips:app.timeline.clips,flows:app.animationRuntime.flows.length};}); + assert.notDeepEqual(replay.a,replay.b);assert.equal(replay.flows,1);assert.equal(replay.clips.length,2); + checks.push({name:'刷新重开后动作片段、复位基准和流向配置恢复',pass:true,detail:replay}); + await shot('03-model-reopened'); + const published=await reopened.evaluate(async()=>{const result=await window.editorApp.publishToSceneLibrary();return Boolean(result);}); + const result=await call(`tran/v1/content/projects/${fixture.modelId}`,session.accessToken); + assert.equal(result.data.project.status,'PUBLISHED',JSON.stringify({published,status:result.data.project.status})); + fixture.modelVersionId=result.data.project.publishedVersionId;save(); + checks.push({name:'正式发布固定动画模型版本',pass:true,versionId:fixture.modelVersionId}); + await shot('04-model-published'); +} catch(error){checks.push({name:'模型动作端到端',pass:false,error:error.message});process.exitCode=1;console.error(error.message);await shot('failure').catch(()=>{});} +finally {fs.writeFileSync(path.join(report,'model-checks.json'),JSON.stringify({checks,errors},null,2));await browser.close();} diff --git a/tools/animation-capabilities-report.mjs b/tools/animation-capabilities-report.mjs new file mode 100644 index 0000000..23c627f --- /dev/null +++ b/tools/animation-capabilities-report.mjs @@ -0,0 +1,36 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import assert from 'node:assert/strict'; +const root=path.resolve('reports/animation-capabilities-20260912'); +const sections=[['model','模型编辑与发布'],['guide','指导书在线使用与交付'],['offline','离线播放'],['teaching','正式教学视口'],['training-editor','训练编辑器绑定']]; +const escape=value=>String(value??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); +const results=sections.map(([key,name])=>({key,name,...JSON.parse(fs.readFileSync(path.join(root,key+'-checks.json'),'utf8'))})); +for(const result of results){assert.ok(result.checks.every(c=>c.pass),result.key);assert.equal(result.errors.length,0,result.key);} +const f=JSON.parse(fs.readFileSync(path.join(root,'fixture.json'),'utf8')); +const shots=[['01-animation-settings','动作与轨道设置:范围、结束处理、路径和阀门联动'],['02-editor-end-pose','模型编辑器中的组合动作'],['03-model-reopened','刷新重开后恢复动画配置'],['05-guide-model-animation','正式指导书模型视口'],['07-offline-before','离线包加载模型'],['08-offline-animation','离线片段播放及暂停'],['09-teaching-animation','教学步骤:保持末帧、清洁状态和流向开通'],['10-teaching-reset','教学步骤:播放结束复位并关闭流向'],['11-training-action-library','训练编辑器读取正式动作目录'],['12-training-action-bound','通过页面绑定并保存动作']]; +const total=results.reduce((n,r)=>n+r.checks.length,0); +const html=`动画能力实施与测试报告 · 2026-09-12 + +

数字车间 · 功能实施 / 真实接口 / 浏览器端到端

模型动画能力实施与测试报告

2026-09-12 · ${total} 项端到端检查通过

按已确认范围补齐命名动作、组合运动、材质状态、流向讲解和跨模块复用。使用独立的牛奶卡车测试工程验证,不修改原始模型和既有业务任务。

+

已实施能力

+ + + +
能力实现与使用
动作片段与机械组合运动时间轴设置命名起止范围;旋转中心使用部件局部坐标;位移与 XYZ 旋转叠加,关键帧错开形成顺序动作;支持保持末帧和回到起点。蓝图按名称播放片段。
状态与材质新增缩放、显隐、不透明度、颜色、材质状态轨道;材质副本隔离,不污染其他实例。录制与保存保留中性基准,旋转中心偏移不会被重复写入位移关键帧。
回路流向配置路径点、颜色、速度、流点数量、正向/反向/停止;可关联标量轨道控制开通。用于工作原理讲解。
跨模块复用训练编辑器从正式目录选动作,保存步骤绑定;教学任务固定模型/场景/训练版本;指导书在线模型与离线 ZIP 使用同一动画求值规则。原生 GLB 动画仍可选择。
+

实测结果

${results.map(r=>`

${escape(r.name)}

${r.checks.map(c=>``).join('')}
检查结果
${escape(c.name)}通过

查看原始检查记录 · 浏览器脚本异常 ${r.errors.length} 条

`).join('')} +

补充验证:前端类型检查及生产构建通过;生成的模型编辑器 public/dist 文件哈希一致。后端 18 条校验、快照与考试提示隔离用例通过;原型 5 项合同测试、Web 4 项场景/缓存/落地测试通过。构建仅有现有的大分块提示。

+

测试采用真实登录、GLB、保存/发布/下载接口。部分配置和视口内部状态由 Playwright 调用页面模块核对;教学原生 GLB 切换为浏览器运行时兼容检查。离线用例阻断业务接口及公网请求,外部请求数为 0。

+

可继续使用的测试数据

+ + + + +
数据标识入口
组合动作模型${f.modelId} · 固定模型版本 ${f.modelVersionId}打开模型编辑器
测试场景${f.animationSceneId} · 版本 ${f.animationSceneVersionId}打开场景
两步骤测试训练${f.animationTrainingId}打开训练编排
固定快照教学任务${f.animationAssignmentId} · 训练版本 482打开教学预览
动画指导书${f.guideId}打开指导书 · 下载实测离线包
+

训练当前保留实测保存的草稿;已有任务仍使用旧发布快照,这正是版本隔离测试验证的行为。试验性教学任务未产生学员成绩。

+

使用顺序与边界

  1. 模型编辑器时间轴添加轨道,选择部件、属性及关键帧;进入「动作与轨道设置」配置片段、枢轴、材质状态或流向。
  2. 保存并发布模型,在场景选择固定版本;训练的「步骤动画」选择正式动作并保存。
  3. 发布训练、下发新任务后在教学端使用;指导书关联模型片段后可在线播放或导出离线 ZIP。
+

本次没有数据库表改动。未加入新的密钥文件、独立服务或认证方式。代码尚未提交 Git。

+

污渍/清洁通过颜色和粗糙度等 PBR 参数表达;贴图擦除、软管/密封圈弹性变形、真实流体压力计算、整片视频导出不在本次实施范围。表格中的具体装备动作仍需配置部件、路径和时序,本报告不代表已经制作完成全部动画素材。

+

浏览器截图

点击图片查看原图。

${shots.map(([file,title])=>`
${escape(title)}
${escape(title)}
`).join('')}
`; +fs.writeFileSync(path.join(root,'index.html'),html); +fs.writeFileSync(path.join(root,'summary.json'),JSON.stringify({date:'2026-09-12',passed:total,failed:0,backendTests:18,prototypeContractTests:5,webTests:4,build:'passed',ddlChanges:false},null,2)); +console.log(`Report generated: ${total} checks passed`); diff --git a/tools/animation-guide-live.mjs b/tools/animation-guide-live.mjs new file mode 100644 index 0000000..fe0e709 --- /dev/null +++ b/tools/animation-guide-live.mjs @@ -0,0 +1,80 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import assert from 'node:assert/strict'; +import {chromium,expect} from '@playwright/test'; +import {login,call} from './role-permissions-live.mjs'; +const report=path.resolve('reports/animation-capabilities-20260912'),fixturePath=path.join(report,'fixture.json'); +const fixture=JSON.parse(fs.readFileSync(fixturePath,'utf8')),session=await login('root'); +const save=()=>fs.writeFileSync(fixturePath,JSON.stringify(fixture,null,2)); +const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}); +const context=await browser.newContext({viewport:{width:1920,height:1080}}); +await context.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken)},session); +const page=await context.newPage(),checks=[],errors=[]; +page.on('pageerror',e=>errors.push(e.message)); +const shot=async name=>page.screenshot({path:path.join(report,name+'.png')}); +try { + await page.goto('http://127.0.0.1:6180/content/ofd');await expect(page.locator('.legacy-guide-page')).toBeVisible({timeout:60000}); + if(!fixture.guideId){ + fixture.guide=await page.evaluate(async({modelId,versionId,candidateId})=>{ + const {FormalGuideStore}=await import('/src/features/guide-legacy/formalStore.ts'); + const {blankGuideState}=await import('/src/features/guide-legacy/formalDocument.ts'); + const {createMediaBlock}=await import('/src/features/guide-legacy/demo-src/ofd-media.js'); + const server=new FormalGuideStore(),state=blankGuideState(); + state.project.name='动画能力验收指导书-20260912';state.project.owner='功能验收';state.project.equipment='牛奶卡车动画测试'; + const record=candidateId?await server.load(candidateId):await server.createOFDProject({title:state.project.name,state}); + await server.loadCatalog(); + const key=`${modelId}@${versionId}`; + const step=record.state.chapters[0].steps[0];step.title='旋出、清洁和流向演示';step.summary='选择动作片段,观察车轮组合运动、污渍到清洁状态变化以及回路开通后的流向。'; + step.modelRef={assetId:key,modelId:key,nodeName:'Wheels'}; + step.animationRef={clipName:'旋出与清洁演示',start:0,end:4,loop:false};step.animation='旋出与清洁演示'; + step.mediaBlocks=[createMediaBlock({kind:'model',assetId:key,assetName:'组合动作演示',clipName:'旋出与清洁演示',start:0,end:4})]; + const saved=await server.saveOFDProjectState(record.id,record.state); + const resource=saved.state.resources.find(r=>r.id===key); + server.dispose();return {id:record.id,resource:{id:resource.id,tracks:resource.timeline?.tracks.length,clips:resource.timeline?.clips.length,overrides:Object.keys(resource.modelOverrides||{}).length}}; + },{modelId:fixture.modelId,versionId:fixture.modelVersionId,candidateId:fixture.guideCandidateId}); + fixture.guideId=fixture.guide.id;save(); + } + assert.equal(fixture.guide.resource.tracks,4);assert.equal(fixture.guide.resource.clips,2); + checks.push({name:'指导书通过正式接口固定引用模型版本并持久化动画元数据',pass:true,resource:fixture.guide.resource}); + await page.goto(`http://127.0.0.1:6180/content/ofd/${fixture.guideId}/studio?view=editor`); + await expect(page.locator('.ofd-view-editor')).toBeVisible({timeout:60000}); + await page.locator('[data-action="select-step"]').first().click(); + await page.locator('[data-action="canvas-block"][data-block-id*="media:"]').first().click(); + await page.locator('[data-action="media-open"]').first().click(); + await expect(page.locator('.ofd-media-panel canvas')).toBeVisible({timeout:60000}); + await shot('05-guide-model-animation'); + console.log('MEDIA_BUTTONS',await page.locator('.ofd-media-panel button').allTextContents()); + const outcome=await page.evaluate(async({guideId,modelId,versionId})=>{ + const {FormalGuideStore}=await import('/src/features/guide-legacy/formalStore.ts'); + const {createGuideViewport}=await import('/src/features/guide-legacy/demo-src/ofd-viewport.js'); + const server=new FormalGuideStore();await server.load(guideId); + const asset=server.record.state.resources.find(r=>r.id===`${modelId}@${versionId}`); + const viewport=createGuideViewport(); + const blob=(await server.assetStore.get(asset.assetKey)).blob; + await viewport.load(asset.id,blob,asset); + const clips=viewport.clips.map(c=>c.name),wheel=viewport.findModelNode('Wheels'); + viewport.playClip('旋出与清洁演示');viewport.pause(); + const initial=wheel.position.toArray(); + // Seek through the same mixer transport used by reader controls. + const {createAnimationRuntimeTools}=await import('/src/features/guide-legacy/demo-src/animation-runtime.js'); + const THREE=await import('/node_modules/.vite/deps/three.js'); + const tools=createAnimationRuntimeTools(THREE);tools.apply(viewport.editorTimeline,4); + const final=wheel.position.toArray(),state=viewport.editorTimeline.materials.values().next().value.clones[0].roughness; + viewport.dispose();server.dispose();return {clips,initial,final,state}; + },{guideId:fixture.guideId,modelId:fixture.modelId,versionId:fixture.modelVersionId}); + assert.ok(outcome.clips.includes('旋出与清洁演示'));assert.ok(outcome.clips.includes('阀门复位演示'));assert.notDeepEqual(outcome.initial,outcome.final);assert.equal(outcome.state,.2); + checks.push({name:'正式指导书视口加载命名动作并还原组合运动与材质状态',pass:true,detail:outcome}); + // Publish using the page's normal delivery pipeline. + await page.keyboard.press('Escape'); + const close=page.locator('[data-action="media-close"]');if(await close.count())await close.first().click(); + await page.locator('[data-action="save"]').click();await page.waitForTimeout(700); + await page.locator('[data-action="open-publish"]').click(); + const wait=page.waitForResponse(r=>r.url().endsWith('/publish')&&r.request().method()==='POST',{timeout:180000}); + await page.locator('[data-action="lifecycle-action"][data-lifecycle-action="publish"]').click(); + const published=await wait;assert.ok(published.ok(),(await published.text()).slice(0,300)); + await shot('06-guide-published'); + const response=await context.request.get(`http://127.0.0.1:6180/api/tran/v1/teaching/guides/${fixture.guideId}/delivery?format=OFFLINE_HTML`,{headers:{Authorization:`Bearer ${session.accessToken}`},timeout:120000}); + assert.ok(response.ok());fs.writeFileSync(path.join(report,'animation-guide.zip'),await response.body()); + checks.push({name:'正式发布并下载带动画的离线指导书',pass:true}); +}catch(error){checks.push({name:'指导书动画端到端',pass:false,error:error.message});process.exitCode=1;console.error(error.message);await shot('guide-failure').catch(()=>{});} +finally{fs.writeFileSync(path.join(report,'guide-checks.json'),JSON.stringify({checks,errors},null,2));await browser.close();} diff --git a/tools/animation-offline-live.mjs b/tools/animation-offline-live.mjs new file mode 100644 index 0000000..551febc --- /dev/null +++ b/tools/animation-offline-live.mjs @@ -0,0 +1,42 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import http from 'node:http'; +import {execFileSync} from 'node:child_process'; +import assert from 'node:assert/strict'; +import {chromium,expect} from '@playwright/test'; +const report=path.resolve('reports/animation-capabilities-20260912'),root=path.join(report,'offline'); +execFileSync('python',['-c',`import zipfile,pathlib +root=pathlib.Path(${JSON.stringify(root)}).resolve() +with zipfile.ZipFile(${JSON.stringify(path.join(report,'animation-guide.zip'))}) as archive: + for entry in archive.infolist(): + target=(root/entry.filename).resolve() + if not target.is_relative_to(root): raise ValueError('Invalid archive path') + archive.extract(entry,root) +`]); +const mime={'.html':'text/html','.js':'text/javascript','.wasm':'application/wasm','.glb':'model/gltf-binary','.css':'text/css','.png':'image/png','.jpg':'image/jpeg'}; +const server=http.createServer((req,res)=>{const file=path.resolve(root,'.'+decodeURIComponent(new URL(req.url,'http://localhost').pathname).replace(/\/$/,'/index.html'));if(!file.startsWith(root+path.sep)||!fs.existsSync(file)){res.writeHead(404).end();return;}res.setHeader('Content-Type',mime[path.extname(file)]||'application/octet-stream');fs.createReadStream(file).pipe(res);}); +await new Promise(resolve=>server.listen(18779,'127.0.0.1',resolve)); +const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}); +const page=await browser.newPage({viewport:{width:1440,height:1000}}),checks=[],errors=[],external=[]; +page.on('pageerror',e=>errors.push(e.message)); +await page.route('**/*',route=>{const url=new URL(route.request().url());if(url.origin!=='http://127.0.0.1:18779'&&!['data:','blob:'].includes(url.protocol)){external.push(url.origin);return route.abort();}return route.continue();}); +try{ + await page.goto('http://127.0.0.1:18779/'); + await page.locator('[data-jump="1"]').first().click(); + await page.locator('[data-action="open-model"]').first().click(); + await expect(page.locator('[data-model-status]')).not.toContainText(/正在加载|失败|未加载/,{timeout:60000}); + await expect(page.locator('[data-model-stage] canvas')).toBeVisible({timeout:60000}); + await page.locator('[data-model-stage] canvas').scrollIntoViewIfNeeded(); + const canvas=page.locator('[data-model-stage] canvas'); + const before=await canvas.screenshot();await page.screenshot({path:path.join(report,'07-offline-before.png')}); + await page.locator('[data-model-action="play-animation"]').click(); + await expect(page.locator('[data-model-status]')).toContainText('旋出与清洁演示'); + await page.waitForTimeout(1800);const after=await canvas.screenshot(); + assert.notDeepEqual(before,after,'Offline canvas must change while the authored action plays'); + await page.locator('[data-model-action="pause-animation"]').click(); + await page.screenshot({path:path.join(report,'08-offline-animation.png')}); + checks.push({name:'下载的离线包可播放命名组合动作,画布发生变化并可暂停',pass:true}); + assert.equal(external.length,0);assert.equal(errors.length,0,errors.join('\n')); + checks.push({name:'断开业务服务与公网后仍可加载模型、引擎和动画',pass:true}); +}catch(error){checks.push({name:'离线动画端到端',pass:false,error:error.message});process.exitCode=1;console.error(error.message);await page.screenshot({path:path.join(report,'offline-failure.png')}).catch(()=>{});} +finally{fs.writeFileSync(path.join(report,'offline-checks.json'),JSON.stringify({checks,errors,external},null,2));await browser.close();await new Promise(resolve=>server.close(resolve));} diff --git a/tools/animation-teaching-live.mjs b/tools/animation-teaching-live.mjs new file mode 100644 index 0000000..abf0535 --- /dev/null +++ b/tools/animation-teaching-live.mjs @@ -0,0 +1,114 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import assert from 'node:assert/strict'; +import {chromium,expect} from '@playwright/test'; +import {login,call} from './role-permissions-live.mjs'; +const report=path.resolve('reports/animation-capabilities-20260912'),file=path.join(report,'fixture.json'); +const fixture=JSON.parse(fs.readFileSync(file,'utf8')),session=await login('root'); +const save=()=>fs.writeFileSync(file,JSON.stringify(fixture,null,2)); +const api=async(url,body,method)=>{const r=await call('tran/v1/'+url,session.accessToken,body,method);assert.equal(r.status,200,`${url}: ${r.message}`);return r.data;}; +const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}); +const context=await browser.newContext({viewport:{width:1920,height:1080}}); +await context.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken)},session); +const page=await context.newPage(),checks=[],errors=[];page.on('pageerror',e=>errors.push(e.message)); +try { + await page.goto('http://127.0.0.1:6180/content/scenes'); + if(!fixture.animationSceneId){ + const content=await page.evaluate(async f=>{ + const {createEmptySceneDocument,createObjectDocument}=await import('/src/features/editors/scene/presets.ts'); + const scene=createEmptySceneDocument(),object=createObjectDocument('box'); + scene.name='动画能力验收场景-20260912'; + Object.assign(object,{id:'animation-truck',type:'model',name:'组合动作牛奶卡车',targetProjectId:f.modelId,targetVersionId:f.modelVersionId,assetCode:f.savedDocument.modelResource.assetCode,assetId:f.savedDocument.modelId}); + object.semantic.modelId=f.savedDocument.modelId;object.semantic.deviceId='ANIM-TRUCK';object.semantic.interactionId='animation-truck';object.semantic.operable=true; + scene.objects=[object];scene.rendering.cameraPosition=[6,4,7];scene.rendering.cameraTarget=[0,1,0]; + return scene; + },fixture); + const created=await api('content/projects',{type:'SCENE',name:content.name,categoryCode:'WORKSHOP',content,dependencies:[{targetProjectId:fixture.modelId,targetVersionId:fixture.modelVersionId,relationType:'SCENE_MODEL',required:true}],assets:[]}); + fixture.animationSceneId=created.project.id;save(); + } + let scene=await api(`content/projects/${fixture.animationSceneId}`); + if(scene.project.status!=='PUBLISHED'&&!scene.currentVersion.content.objects[0].semantic.deviceId){ + scene.currentVersion.content.objects[0].semantic.deviceId='ANIM-TRUCK'; + scene=await api(`content/projects/${fixture.animationSceneId}`,{type:'SCENE',name:scene.project.name,categoryCode:'WORKSHOP',content:scene.currentVersion.content,version:scene.project.version,dependencies:scene.dependencies,assets:[]},'PUT'); + } + if(scene.project.status!=='PUBLISHED')await api(`content/projects/${fixture.animationSceneId}/publish`,{version:scene.project.version}); + const publishedScene=await api(`content/projects/${fixture.animationSceneId}`);fixture.animationSceneVersionId=publishedScene.project.publishedVersionId;save(); + if(!fixture.animationTrainingId){ + const content=await page.evaluate(async({f,scene})=>{ + const {createTrainingProject}=await import('/src/features/guide-legacy/demo-src/training-project-io.js'); + const doc=createTrainingProject();doc.name='动画能力验收训练-20260912';doc.task.title=doc.name; + const step=doc.steps[0];step.id='animation-step';step.code='animation-step';step.title='旋出、清洁与回路流向';step.targetId='animation-truck';step.targetName='组合动作牛奶卡车';step.duration=4; + step.animationBinding={assetId:`animation-${f.modelId}@${f.modelVersionId}-work`,modelAssetId:`${f.modelId}@${f.modelVersionId}`,clipName:'旋出与清洁演示',sourceStart:0,finish:'hold',duration:4}; + doc.steps=[step,{...structuredClone(step),id:'reset-step',code:'reset-step',title:'播放后回到起点',animationBinding:{...step.animationBinding,clipName:'阀门复位演示',finish:'reset'}}]; + doc.steps.forEach(step=>{step.stepCode=step.code;step.actionCode='CHECK-'+step.code;}); + doc.scenario.sceneResource={id:`${f.animationSceneId}@${f.animationSceneVersionId}`,projectId:f.animationSceneId,versionId:f.animationSceneVersionId,name:scene.name,snapshot:scene}; + doc.scenario.visibleParts=['animation-truck'];doc.scenario.operableParts=['animation-truck']; + doc.resources.models=[{id:`${f.modelId}@${f.modelVersionId}`,modelId:f.savedDocument.modelId,targetProjectId:f.modelId,targetVersionId:f.modelVersionId,timeline:f.savedDocument.timeline}]; + return doc; + },{f:fixture,scene:publishedScene.currentVersion.content}); + const created=await api('content/projects',{type:'TRAINING',name:content.name,categoryCode:'MAINTENANCE',trainingMode:'VIRTUAL',content,dependencies:[{targetProjectId:fixture.animationSceneId,targetVersionId:fixture.animationSceneVersionId,relationType:'TRAINING_SCENE',required:true}],assets:[]}); + fixture.animationTrainingId=created.project.id;save(); + } + let training=await api(`content/projects/${fixture.animationTrainingId}`); + if(training.project.status!=='PUBLISHED'&&training.currentVersion.content.steps[0].actionCode==='CHECK'){ + training.currentVersion.content.steps.forEach(step=>{step.stepCode=step.code;step.actionCode='CHECK-'+step.code;}); + training=await api(`content/projects/${fixture.animationTrainingId}`,{type:'TRAINING',name:training.project.name,categoryCode:'MAINTENANCE',trainingMode:'VIRTUAL',content:training.currentVersion.content,version:training.project.version,dependencies:training.dependencies,assets:[]},'PUT'); + } + if(training.project.status!=='PUBLISHED')await api(`content/projects/${fixture.animationTrainingId}/publish`,{version:training.project.version}); + const published=await api(`content/projects/${fixture.animationTrainingId}`);fixture.animationTrainingVersionId=published.project.publishedVersionId;save(); + if(!fixture.animationAssignmentId){ + const a=await api('teaching/assignments',{code:'ANIM-CAP-'+Date.now(),name:'动画能力验收任务-20260912',description:'开发环境动画播放验证',channel:'VIRTUAL',assignmentKind:'TRAINING',executionMode:'DEMO',audienceType:'COMMON',collaborationMode:'INDIVIDUAL',digitalHumanAllowed:false,contentProjectId:fixture.animationTrainingId,contentVersionId:fixture.animationTrainingVersionId,members:[]}); + fixture.animationAssignmentId=a.id;save(); + } + const assignment=await api(`teaching/assignments/${fixture.animationAssignmentId}`); + fixture.animationAssignmentContentVersionId=assignment.contentVersionId;save(); + const object=assignment.definitionSnapshot.teachingScene.objects.find(o=>o.targetProjectId===fixture.modelId); + assert.equal(object.modelTimeline.clips.length,2);assert.equal(object.modelTimeline.tracks.length,4);assert.ok(Object.keys(object.modelOverrides).length>0); + assert.equal(object.targetVersionId,fixture.modelVersionId); + checks.push({name:'真实教学任务固定模型版本、动作、材质、枢轴和流向配置',pass:true,modelVersionId:object.targetVersionId,assignmentId:assignment.id}); + await page.goto(`http://127.0.0.1:6180/teaching/virtual-training/preview/${assignment.id}`); + await expect(page.locator('.teaching-three-scene canvas')).toBeVisible({timeout:60000}); + await page.getByRole('button',{name:'适应',exact:true}).click(); + await page.waitForTimeout(6500); + // Inspect the real component's runtime, not a second test renderer. + const inspect=()=>page.evaluate(()=>{ + const host=document.querySelector('.teaching-three-scene');let instance=host?.__vueParentComponent; + while(instance&&!instance.type.__name?.includes('TeachingRunScene'))instance=instance.parent; + const state=instance?.devtoolsRawSetupState; + if(!state)throw new Error('Teaching scene setup unavailable'); + const entry=state.timelineRuntimes[0],runtime=entry?.runtime; + const wheel=runtime?.bindings.find(b=>b.track.id==='move')?.object; + const material=runtime?.materials.values().next().value; + const mesh=runtime?.materials.keys().next().value; + return {status:state.sceneStatus?.value || state.sceneStatus,time:runtime?.time,bindings:runtime?.bindings.length,unresolved:runtime?.unresolved, + wheel:wheel?.position.toArray(),base:wheel&&runtime.bases.get(wheel).position,roughness:material?.clones[0].roughness, + actualRoughness:mesh?.material.roughness,flowVisible:runtime?.flows[0]?.dots.some(dot=>dot.visible),step:state.props.currentStepCode}; + }); + const state=await inspect();assert.doesNotMatch(String(state.status),/失败/); + assert.equal(state.bindings,4);assert.equal(state.unresolved,0);assert.equal(state.time,4);assert.equal(state.roughness,.2);assert.equal(state.actualRoughness,.2);assert.equal(state.flowVisible,true);assert.notDeepEqual(state.wheel,state.base); + await page.screenshot({path:path.join(report,'09-teaching-animation.png')}); + checks.push({name:'正式教学预览实际播放指定片段并保持末帧,四条轨道全部绑定',pass:true,detail:state}); + await page.getByRole('button',{name:'引导完成',exact:true}).click(); + await page.getByRole('button',{name:'下一步',exact:true}).click(); + await page.waitForTimeout(6500); + const reset=await inspect();assert.equal(reset.step,'reset-step');assert.equal(reset.time,0);assert.equal(reset.roughness,.95);assert.equal(reset.flowVisible,false);assert.deepEqual(reset.wheel,reset.base); + await page.screenshot({path:path.join(report,'10-teaching-reset.png')}); + checks.push({name:'切换步骤后播放复位片段,结束恢复起点并关闭流向',pass:true,detail:reset}); + const native=await page.evaluate(async()=>{ + let instance=document.querySelector('.teaching-three-scene').__vueParentComponent; + while(!instance.type.__name?.includes('TeachingRunScene'))instance=instance.parent; + const s=instance.devtoolsRawSetupState,entry=s.nativePlayers[0]; + if(!entry?.clips.length)throw new Error('Fixture native GLB clip missing'); + const clip=entry.clips[0],step=s.definitionSteps().find(step=>s.stepCodeOf(step)===s.props.currentStepCode); + const binding=step.animationBinding,previous=binding.clipName; + binding.clipName=clip.name;s.syncStepTimelines(); + await new Promise(resolve=>setTimeout(resolve,900)); + const time=entry.mixer.clipAction(clip).time; + binding.clipName=previous;s.syncStepTimelines(); + return {name:clip.name,time}; + }); + assert.ok(native.time>0);checks.push({name:'同一教学视口原生 GLB 动画与自编片段切换兼容(浏览器运行时检查)',pass:true,detail:native}); + fs.writeFileSync(path.join(report,'teaching-inspection.json'),JSON.stringify(state,null,2)); + assert.equal(errors.length,0,errors.join('\n')); +}catch(error){checks.push({name:'教学动画端到端',pass:false,error:error.message});process.exitCode=1;console.error(error.message);await page.screenshot({path:path.join(report,'teaching-failure.png')}).catch(()=>{});} +finally{fs.writeFileSync(path.join(report,'teaching-checks.json'),JSON.stringify({checks,errors},null,2));await browser.close();} diff --git a/tools/animation-training-editor-live.mjs b/tools/animation-training-editor-live.mjs new file mode 100644 index 0000000..16ff665 --- /dev/null +++ b/tools/animation-training-editor-live.mjs @@ -0,0 +1,37 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import assert from 'node:assert/strict'; +import {chromium,expect} from '@playwright/test'; +import {login,call} from './role-permissions-live.mjs'; +const report=path.resolve('reports/animation-capabilities-20260912'),f=JSON.parse(fs.readFileSync(path.join(report,'fixture.json'),'utf8')); +const session=await login('root'),browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}); +const context=await browser.newContext({viewport:{width:1920,height:1080}}); +await context.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken)},session); +const page=await context.newPage(),checks=[],errors=[];page.on('pageerror',e=>errors.push(e.message)); +try{ + await page.goto(`http://127.0.0.1:6180/content/training-projects/${f.animationTrainingId}/legacy`); + const open=page.locator('[data-action="open-content-resources"][data-resource-scope="animation"]'); + await expect(open).toBeVisible({timeout:60000});await open.click(); + const key=`animation-content-model-${f.modelId}-${f.modelVersionId}-work`; + const button=page.locator(`[data-action="bind-content-resource"][data-resource-id="${key}"]`); + await expect(button).toBeVisible({timeout:60000}); + await expect(page.locator(`[data-resource-id="animation-content-model-${f.modelId}-${f.modelVersionId}-reset"]`)).toBeVisible(); + await button.scrollIntoViewIfNeeded();await page.screenshot({path:path.join(report,'11-training-action-library.png')}); + if(await button.innerText()==='取消绑定')await button.click(); + await button.click(); + await page.locator('[data-action="close-dialog"]').last().click(); + await expect(page.locator('.te-step-animation-binding')).toContainText('旋出与清洁演示'); + const save=page.locator('[data-action="save"]');console.log('SAVE_COUNT',await save.count()); + const wait=page.waitForResponse(r=>r.request().method()==='PUT'&&r.url().includes(`/content/projects/${f.animationTrainingId}`),{timeout:60000}); + await save.click();const response=await wait;assert.ok(response.ok(),(await response.text()).slice(0,200)); + const result=await call(`tran/v1/content/projects/${f.animationTrainingId}`,session.accessToken); + const binding=result.data.currentVersion.content.steps[0].animationBinding; + assert.equal(binding.clipName,'旋出与清洁演示');assert.equal(binding.modelAssetId,`content-model-${f.modelId}-${f.modelVersionId}`); + checks.push({name:'训练编辑器从正式模型库选择命名动作并通过真实保存接口持久化',pass:true,binding}); + await page.screenshot({path:path.join(report,'12-training-action-bound.png')}); + const assignment=await call(`tran/v1/teaching/assignments/${f.animationAssignmentId}`,session.accessToken); + assert.equal(assignment.data.contentVersionId,f.animationAssignmentContentVersionId || '482'); + checks.push({name:'训练编辑草稿不会覆盖已创建任务的固定发布快照',pass:true}); + assert.equal(errors.length,0,errors.join('\n')); +}catch(error){checks.push({name:'训练动画绑定端到端',pass:false,error:error.message});process.exitCode=1;console.error(error.message);await page.screenshot({path:path.join(report,'training-editor-failure.png')}).catch(()=>{});} +finally{fs.writeFileSync(path.join(report,'training-editor-checks.json'),JSON.stringify({checks,errors},null,2));await browser.close();}