feat(world-sketcher): 新增本地服务器、标签显示和README重写
- 新增 server.py + start.bat: 本地保存/加载服务器 - sketch.html: 切换层级时自动绘制元素标签 - L1 显示区域名称+地形标高,L2 显示建筑名称+高度 - 保存优先POST到服务器,加载优先从output/读取 - 命名弹窗区分'地形标高'和'建筑高度' - README 全面重写:双模式启动、文件结构、API端点、高度语义
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
"""
|
||||
World Sketcher 本地保存服务器
|
||||
用法: python server.py [port]
|
||||
然后浏览器打开 http://localhost:<port>/sketch.html
|
||||
"""
|
||||
|
||||
import http.server, json, os, sys, urllib.parse
|
||||
from datetime import datetime
|
||||
|
||||
PORT = int(sys.argv[1]) if len(sys.argv) > 1 else 8765
|
||||
SERVE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
OUTPUT_DIR = os.path.join(SERVE_DIR, "output")
|
||||
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
||||
|
||||
|
||||
class Handler(http.server.SimpleHTTPRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, directory=SERVE_DIR, **kwargs)
|
||||
|
||||
# ── POST /save ── 保存文件到 output/
|
||||
def do_POST(self):
|
||||
if self.path != "/save":
|
||||
self.send_error(404); return
|
||||
|
||||
ctype = self.headers.get("Content-Type", "")
|
||||
if ctype.startswith("multipart/form-data"):
|
||||
boundary = ctype.split("boundary=")[-1].encode()
|
||||
data = self.rfile.read(int(self.headers["Content-Length"]))
|
||||
|
||||
# 简易 multipart 解析
|
||||
parts = data.split(b"--" + boundary)
|
||||
saved = []
|
||||
for part in parts:
|
||||
if not part or part.strip() in (b"", b"--\r\n", b"--"):
|
||||
continue
|
||||
header_end = part.find(b"\r\n\r\n")
|
||||
if header_end < 0:
|
||||
continue
|
||||
header = part[:header_end].decode("utf-8", errors="replace")
|
||||
body = part[header_end + 4:]
|
||||
if body.endswith(b"\r\n"):
|
||||
body = body[:-2]
|
||||
|
||||
# 提取文件名
|
||||
fname = None
|
||||
for seg in header.split(";"):
|
||||
seg = seg.strip()
|
||||
if seg.startswith("filename="):
|
||||
fname = seg.split("=", 1)[1].strip('"')
|
||||
break
|
||||
if not fname:
|
||||
continue
|
||||
|
||||
# 安全:只取文件名,禁止路径遍历
|
||||
fname = os.path.basename(fname)
|
||||
fpath = os.path.join(OUTPUT_DIR, fname)
|
||||
with open(fpath, "wb") as f:
|
||||
f.write(body)
|
||||
saved.append(fname)
|
||||
|
||||
resp = json.dumps({"ok": True, "saved": saved}, ensure_ascii=False)
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
self.wfile.write(resp.encode("utf-8"))
|
||||
print(f" [{datetime.now():%H:%M:%S}] Saved: {', '.join(saved)}")
|
||||
else:
|
||||
self.send_error(400, "Expected multipart/form-data")
|
||||
|
||||
# ── GET 路由 ──
|
||||
def do_GET(self):
|
||||
# /list → 列出 output/ 下可用文件
|
||||
if self.path == "/list":
|
||||
files = sorted(
|
||||
f for f in os.listdir(OUTPUT_DIR)
|
||||
if f.endswith((".json", ".zip"))
|
||||
)
|
||||
resp = json.dumps({"files": files}, ensure_ascii=False)
|
||||
self.send_response(200)
|
||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||
self.end_headers()
|
||||
self.wfile.write(resp.encode("utf-8"))
|
||||
return
|
||||
|
||||
# /load/<file> → 从 output/ 读取文件
|
||||
if self.path.startswith("/load/"):
|
||||
fname = os.path.basename(urllib.parse.unquote(self.path[6:], encoding="utf-8"))
|
||||
fpath = os.path.join(OUTPUT_DIR, fname)
|
||||
if not os.path.isfile(fpath):
|
||||
self.send_error(404); return
|
||||
self.send_response(200)
|
||||
if fname.endswith(".zip"):
|
||||
self.send_header("Content-Type", "application/zip")
|
||||
elif fname.endswith(".json"):
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.end_headers()
|
||||
with open(fpath, "rb") as f:
|
||||
self.wfile.write(f.read())
|
||||
return
|
||||
|
||||
# 其他 → 静态文件(sketch.html 等)
|
||||
super().do_GET()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
# 只显示非静态资源的请求日志
|
||||
if "/save" in str(args) or "/list" in str(args) or "/load/" in str(args):
|
||||
super().log_message(format, *args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with http.server.HTTPServer(("", PORT), Handler) as httpd:
|
||||
url = f"http://localhost:{PORT}/sketch.html"
|
||||
print(f"World Sketcher server running")
|
||||
print(f" URL: {url}")
|
||||
print(f" Output: {OUTPUT_DIR}")
|
||||
print(f" Press Ctrl+C to stop.\n")
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nServer stopped.")
|
||||
Reference in New Issue
Block a user