feat: parameterize complex junction geometry with Gaode reference
- 高德 GeoJSON 参考流程: `scripts/lib/gaode-junction-reference.js` 与 `scripts/inspect-junction-reference.js` 将 GCJ-02 参考转换为 WGS84, 按 node id/最近距离关联 OSM, 支持普通路口面和 `complex-cluster` 两种匹配。 - 复合路口模板 `complex-junction-v1`: `scripts/lib/complex-junction.js` 用参考 几何校准 core 半径, 生成路口面、进口路面、斑马线、停止线、角部圆角与安全岛; 拓扑/信号/连接全部沿用 OSM/native。 - 车道中心线控制要素避让: `compileLaneCenterlines` 现接收模板已产出的斑马线/停止线, 新增 `trimLaneOutsideControls` 按到路口中心的半径定向裁剪; 标线源几何同步裁剪, 不再 越过斑马线继续画到核心区。拓扑几何不变, connector 集合前后一致。 - 复合路口人行道转角: `buildComplexJunctionGeometry` 沿已定义的路缘生成 2m 宽转角带, 复用圆角曲线, 通过 `islands` 通道并入 `sidewalk_surface`; 自交或坐标非有限时报 `complex-junction-sidewalk-corner-fallback` 并跳过。 - 新增诊断: `complex-junction-configured-radius-ignored`、 `lane-centerline-fully-inside-control`、`complex-junction-sidewalk-corner-fallback`。 - 死码清理: 移除未被调用的 `clusterApproachRing`。 - spec 更新: `.trellis/spec/pipeline/cli-and-stages.md` 复合路口小节补充控制要素 避让顺序、人行道转角契约、Validation 矩阵三行; 索引新增导航。 - 任务产物 `08-19-gaode-junction-reference`: 8 条验收标准全部实测记录, Scope Drift / Verification Log / Known Gaps 三节沉淀本次工作。 Regression: test:native-road / test:road-workbench / test:preflight / test:native-preview-traffic / test:package-contract / test:traffic-signals / test:gaode-junction-reference 全绿; road:check ok=true, errors=[]。
This commit is contained in:
@@ -16,6 +16,11 @@ import sys
|
||||
import bpy
|
||||
import numpy as np
|
||||
|
||||
try:
|
||||
from tqdm import tqdm
|
||||
except ImportError:
|
||||
tqdm = None
|
||||
|
||||
# --factory-startup does not put the script's own directory on sys.path, so the
|
||||
# osmassets package next to this file is not importable without this.
|
||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -603,9 +608,39 @@ def semantic_asset_specs(glb_path, meshes):
|
||||
return specs
|
||||
|
||||
|
||||
class ExportProgress:
|
||||
"""Use tqdm when available, while keeping Blender builds dependency-free."""
|
||||
|
||||
def __init__(self):
|
||||
self.percent = 0
|
||||
self.bar = tqdm(total=100, desc="Cesium export", unit="%",
|
||||
dynamic_ncols=True, mininterval=0.2,
|
||||
disable=tqdm is None) if tqdm else None
|
||||
|
||||
def update(self, stage, percent, detail=""):
|
||||
percent = max(self.percent, min(100, float(percent)))
|
||||
delta = percent - self.percent
|
||||
self.percent = percent
|
||||
if self.bar:
|
||||
self.bar.set_description("Cesium export | %s" % stage)
|
||||
self.bar.set_postfix_str(detail, refresh=False)
|
||||
if delta:
|
||||
self.bar.update(delta)
|
||||
self.bar.refresh()
|
||||
return
|
||||
suffix = " - " + detail if detail else ""
|
||||
print("CESIUM_EXPORT_PROGRESS %3.0f%% [%s]%s" % (percent, stage, suffix), flush=True)
|
||||
|
||||
def close(self):
|
||||
if self.bar:
|
||||
self.bar.close()
|
||||
|
||||
|
||||
def export(args):
|
||||
if not os.path.exists(args["blend"]):
|
||||
raise FileNotFoundError(args["blend"])
|
||||
progress = ExportProgress()
|
||||
progress.update("load scene", 1, os.path.basename(args["blend"]))
|
||||
bpy.ops.wm.open_mainfile(filepath=args["blend"])
|
||||
|
||||
material_map = {}
|
||||
@@ -613,13 +648,13 @@ def export(args):
|
||||
dynamic_meshes = []
|
||||
countdown_meshes = {0: [], 1: []}
|
||||
unwrapped = set()
|
||||
for obj in bpy.context.scene.objects:
|
||||
if obj.type != "MESH":
|
||||
continue
|
||||
if obj.name == "Ground Plane":
|
||||
continue
|
||||
if obj.hide_viewport or obj.hide_render:
|
||||
continue
|
||||
scene_objects = list(bpy.context.scene.objects)
|
||||
candidates = [obj for obj in scene_objects
|
||||
if obj.type == "MESH" and obj.name != "Ground Plane"
|
||||
and not obj.hide_viewport and not obj.hide_render]
|
||||
total_candidates = max(1, len(candidates))
|
||||
progress.update("prepare scene", 3, "%d mesh objects" % len(candidates))
|
||||
for index, obj in enumerate(candidates, 1):
|
||||
if any(c.name == "06_TrafficSignalsDynamic" for c in obj.users_collection):
|
||||
groups = {slot.material.name for slot in obj.material_slots if slot.material}
|
||||
group = (0 if any("Countdown Group 0" in name for name in groups)
|
||||
@@ -650,21 +685,36 @@ def export(args):
|
||||
if source.name not in material_map:
|
||||
material_map[source.name] = make_export_material(source)
|
||||
slot.material = material_map[source.name]
|
||||
if index == total_candidates or index % max(1, total_candidates // 50) == 0:
|
||||
progress.update("prepare scene", 3 + 42 * index / total_candidates,
|
||||
"%d/%d objects, %d materials" % (index, total_candidates, len(material_map)))
|
||||
|
||||
export_glb(args["glb"], meshes)
|
||||
semantic_assets = semantic_asset_specs(args["glb"], meshes)
|
||||
exports = [(args["glb"], meshes, "main scene")]
|
||||
if args.get("dynamic_glb"):
|
||||
if not dynamic_meshes:
|
||||
raise RuntimeError("Dynamic traffic signal collection is empty")
|
||||
export_glb(args["dynamic_glb"], dynamic_meshes)
|
||||
exports.append((args["dynamic_glb"], dynamic_meshes, "dynamic signals"))
|
||||
for group, key in ((0, "countdown_0_glb"), (1, "countdown_1_glb")):
|
||||
if not args.get(key):
|
||||
continue
|
||||
if not countdown_meshes[group]:
|
||||
raise RuntimeError("Traffic countdown collection %d is empty" % group)
|
||||
export_glb(args[key], countdown_meshes[group])
|
||||
semantic_assets = semantic_asset_specs(args["glb"], meshes)
|
||||
exports.append((args[key], countdown_meshes[group], "countdown group %d" % group))
|
||||
for asset in semantic_assets:
|
||||
export_glb(asset["path"], asset["meshes"])
|
||||
exports.append((asset["path"], asset["meshes"], "%s layer" % asset["id"]))
|
||||
|
||||
total_export_objects = max(1, sum(len(objects) for _, objects, _ in exports))
|
||||
completed_export_objects = 0
|
||||
for filepath, objects, label in exports:
|
||||
start_percent = 45 + 50 * completed_export_objects / total_export_objects
|
||||
end_percent = 45 + 50 * (completed_export_objects + len(objects)) / total_export_objects
|
||||
progress.update("export GLB", start_percent,
|
||||
"%s (%d objects)" % (label, len(objects)))
|
||||
export_glb(filepath, objects)
|
||||
completed_export_objects += len(objects)
|
||||
progress.update("export GLB", end_percent, "%s complete" % label)
|
||||
progress.update("write metadata", 97)
|
||||
|
||||
scene = bpy.context.scene
|
||||
try:
|
||||
@@ -711,6 +761,8 @@ def export(args):
|
||||
json.dump(metadata, handle, ensure_ascii=False, indent=2)
|
||||
handle.write("\n")
|
||||
|
||||
progress.update("complete", 100, "all assets exported")
|
||||
progress.close()
|
||||
print("CESIUM_EXPORT_DONE", json.dumps({
|
||||
"glb": args["glb"], "metadata": args["metadata"],
|
||||
"meshes": len(meshes), "materials": len(material_map),
|
||||
|
||||
Reference in New Issue
Block a user