From 603f78b77f4bb689c133c99cc5013c9b6c5c9ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=93=E9=9B=A8=E9=B9=8F?= <846149189@qq.com> Date: Sun, 14 Jun 2026 11:13:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(pointview):=20=E6=96=B0=E5=A2=9E=E3=80=8C?= =?UTF-8?q?=E5=9C=BA=E6=99=AF/=E7=82=B9=E4=BD=8D=E3=80=8D=E9=A1=B5?= =?UTF-8?q?=E7=AD=BE=E2=80=94=E2=80=94=E6=AD=A3=E4=BA=A4=E4=BF=AF=E8=A7=86?= =?UTF-8?q?=E7=9C=9F=E5=AE=9E=E5=9C=BA=E6=99=AF=E5=BA=95=E5=9B=BE=20+=20?= =?UTF-8?q?=E7=82=B9=E4=BD=8D=E7=B2=BE=E7=A1=AE=E5=8F=A0=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 第三个页签(与海选审核/演出配置平级),只读查看每个点位集里各点的真实 位置/朝向,配 move.to/camera.focus 时对照用,不必回 Unity 翻 json。 - pointview.js: 独立白模点位查看器(按 kind 上色/朝向箭头/悬停坐标/侧栏清单); 有底图则把正交俯视真实场景图当画布底图、点位按 shot.bounds 线性投上去 (像素级对齐家具),带显隐开关;无底图回退黑底白模。 - app.py: /api/pointsets 给有底图的点位集附 shot{url,bounds};新增 /sceneshot/{name}.png 路由(防目录穿越)。 - Dockerfile/compose: 加 STORY_SCENESHOTS_DIR(/sceneshots) env + 挂载点与注释。 底图由 SGame 仓新增 Editor 工具「剧情场景俯视抓拍」产出 ({name}.png + {name}.shot.json,map-local 覆盖范围)。 --- web/Dockerfile | 5 +- web/app.py | 36 +++++ web/docker-compose.gitsync.yml | 5 + web/docker-compose.nas.yml | 3 + web/docker-compose.yml | 3 + web/static/app.js | 34 ++++- web/static/index.html | 14 ++ web/static/pointview.js | 269 +++++++++++++++++++++++++++++++++ web/static/style.css | 49 ++++++ 9 files changed, 414 insertions(+), 4 deletions(-) create mode 100644 web/static/pointview.js diff --git a/web/Dockerfile b/web/Dockerfile index b9865fe..cc7621a 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -21,8 +21,9 @@ COPY web ./web # 注意:镜像里不埋任何口令默认值。STORY_WEB_USERS(名字:口令,…)必须运行时注入, # 未配置时 app 启动即退出(拒绝弱口令裸奔)。 ENV STORY_DB_PATH=/data/story_events.db \ - STORY_POINTSETS_DIR=/pointsets -RUN mkdir -p /data /pointsets + STORY_POINTSETS_DIR=/pointsets \ + STORY_SCENESHOTS_DIR=/sceneshots +RUN mkdir -p /data /pointsets /sceneshots EXPOSE 8787 WORKDIR /app/web diff --git a/web/app.py b/web/app.py index b962135..2e746bd 100644 --- a/web/app.py +++ b/web/app.py @@ -34,6 +34,9 @@ _DICT_PATH = os.path.join(_AUTHORING, "ir_dictionary.json") # 点位集目录:容器内用 STORY_POINTSETS_DIR 指向挂载卷;本地默认指向项目 Assets。 _POINTSETS_DIR = os.environ.get("STORY_POINTSETS_DIR") or \ os.path.join(_PROJ, "Assets", "StreamingAssets", "Story", "PointSets") +# 场景俯视底图目录:Unity「剧情场景俯视抓拍」产出 {name}.png + {name}.shot.json(含覆盖的 map-local 范围)。 +_SCENESHOTS_DIR = os.environ.get("STORY_SCENESHOTS_DIR") or \ + os.path.join(_PROJ, "Assets", "StreamingAssets", "Story", "SceneShots") _STATIC_DIR = os.path.join(_HERE, "static") COOKIE = "story_auth" @@ -162,6 +165,25 @@ async def dictionary(): return json.load(f) +def _load_shot(name): + """读 {name}.shot.json(含俯视底图覆盖的 map-local 范围);图与 sidecar 都在才算有效。""" + try: + meta = os.path.join(_SCENESHOTS_DIR, name + ".shot.json") + png = os.path.join(_SCENESHOTS_DIR, name + ".png") + if not (os.path.isfile(meta) and os.path.isfile(png)): + return None + with open(meta, encoding="utf-8") as f: + m = json.load(f) + b = m.get("bounds") + if not (isinstance(b, list) and len(b) == 4): + return None + # 缓存击穿:附 png mtime,图更新后前端能拿到新图 + return {"url": "/sceneshot/" + name + ".png?v=" + str(int(os.path.getmtime(png))), + "bounds": b, "w": m.get("w"), "h": m.get("h")} + except Exception: + return None + + @app.get("/api/pointsets") async def pointsets(): out = {} @@ -188,11 +210,25 @@ async def pointsets(): for p in pts ], } + shot = _load_shot(name) + if shot: + out[name]["shot"] = shot # 有正交俯视底图 → 场景/点位页叠真实场景 except Exception as e: out[name] = {"error": str(e)} return out +@app.get("/sceneshot/{name}.png") +async def sceneshot(name: str): + # 防目录穿越:只认纯文件名 + if not name or "/" in name or "\\" in name or ".." in name: + return JSONResponse({"error": "bad name"}, status_code=400) + p = os.path.join(_SCENESHOTS_DIR, name + ".png") + if not os.path.isfile(p): + return JSONResponse({"error": "not found"}, status_code=404) + return FileResponse(p, media_type="image/png") + + # ---------- 事件 CRUD ---------- @app.get("/api/events") async def events(status: str = "all"): diff --git a/web/docker-compose.gitsync.yml b/web/docker-compose.gitsync.yml index 52cbee9..766d085 100644 --- a/web/docker-compose.gitsync.yml +++ b/web/docker-compose.gitsync.yml @@ -49,3 +49,8 @@ services: # 整个同步仓库盖在 /app(只读):web/ 前端+后端、ir_core、ir_dictionary.json 全部 # 来自 git 同步目录;镜像内的 COPY 副本只作无挂载时的兜底。路径按上面共享路径,一般不用改。 - "/SATA存储11/Docker/story-edit-web-src:/app:ro" + # 点位集 + 场景俯视底图(可选):这两个来自 SGame 仓 Assets/StreamingAssets/Story/, + # 不在本同步仓里。要让线上「演出配置」有真实坐标、「场景/点位」有底图,把这两目录同步到 + # NAS 后取消下面两行注释并改成实际路径(缺失时分别降级为:坐标校验警告 / 白模回退): + # - "/SATA存储11/Docker/story-assets/PointSets:/pointsets:ro" + # - "/SATA存储11/Docker/story-assets/SceneShots:/sceneshots:ro" diff --git a/web/docker-compose.nas.yml b/web/docker-compose.nas.yml index d75fec7..db0cb49 100644 --- a/web/docker-compose.nas.yml +++ b/web/docker-compose.nas.yml @@ -20,4 +20,7 @@ services: # 点位集(可选):先不挂 = 坐标校验降级为警告,能正常用。 # 把 Assets/StreamingAssets/Story/PointSets 同步到 NAS 某目录后取消下一行注释并改成实际路径: # - /vol1/docker/story/pointsets:/pointsets:ro + # 场景俯视底图(可选):先不挂 =「场景/点位」页回退黑底白模。 + # 把 Assets/StreamingAssets/Story/SceneShots 同步到 NAS 某目录后取消下一行注释并改成实际路径: + # - /vol1/docker/story/sceneshots:/sceneshots:ro restart: unless-stopped diff --git a/web/docker-compose.yml b/web/docker-compose.yml index ba64b28..013517f 100644 --- a/web/docker-compose.yml +++ b/web/docker-compose.yml @@ -22,6 +22,9 @@ services: - ./data:/data # 点位集(开发侧产出,只读引用;缺失时坐标校验降级为警告) - ../../../Assets/StreamingAssets/Story/PointSets:/pointsets:ro + # 场景正交俯视底图(Unity「剧情场景俯视抓拍」产出 {name}.png + {name}.shot.json); + # 缺失时「场景/点位」页回退黑底白模 + - ../../../Assets/StreamingAssets/Story/SceneShots:/sceneshots:ro # 前端静态文件热挂载:改 static/* 后刷新浏览器即生效,无需重建镜像 - ./static:/app/web/static:ro restart: unless-stopped diff --git a/web/static/app.js b/web/static/app.js index bdb57eb..4d7130f 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -9,7 +9,7 @@ status: null, selectedNode: null, dirty: false, - mode: "review", // review=海选审核 / perform=演出配置 + mode: "review", // review=海选审核 / perform=演出配置 / points=场景点位 by: localStorage.getItem("story_by") || "匿名", }; window.App = App; @@ -196,20 +196,50 @@ // ---------- 试走 ---------- $("btn-playtest").onclick = () => Playtest.open(App.ir, App.dict); - // ---------- 模式切换:海选审核 / 演出配置 ---------- + // ---------- 模式切换:海选审核 / 演出配置 / 场景点位 ---------- function setMode(m) { App.mode = m; $("mode-review").classList.toggle("active", m === "review"); $("mode-perform").classList.toggle("active", m === "perform"); + $("mode-points").classList.toggle("active", m === "points"); $("wrap").classList.toggle("hidden", m !== "review"); $("perform-wrap").classList.toggle("hidden", m !== "perform"); + $("points-wrap").classList.toggle("hidden", m !== "points"); $("review-toolbar").style.display = m === "review" ? "" : "none"; document.body.classList.toggle("perform-mode", m === "perform"); // 切背景色调 + document.body.classList.toggle("points-mode", m === "points"); Timeline.stop(); + if (m === "points") PointView.clear(); if (m === "perform") performLoadList(); + if (m === "points") pointsLoadList(); } $("mode-review").onclick = () => setMode("review"); $("mode-perform").onclick = () => setMode("perform"); + $("mode-points").onclick = () => setMode("points"); + + // ---------- 场景点位页:点位集列表 + 只读白模查看器 ---------- + let pointsCurrent = null; + function pointsLoadList() { + const names = Object.keys(App.pointsets || {}).sort(); + const host = $("points-set-list"); host.innerHTML = ""; + if (!names.length) { host.innerHTML = '