diff --git a/.trellis/tasks/08-17-native-road-edge-markings/check.jsonl b/.trellis/tasks/08-17-native-road-edge-markings/check.jsonl new file mode 100644 index 0000000..9dd3234 --- /dev/null +++ b/.trellis/tasks/08-17-native-road-edge-markings/check.jsonl @@ -0,0 +1 @@ +{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."} diff --git a/.trellis/tasks/08-17-native-road-edge-markings/implement.jsonl b/.trellis/tasks/08-17-native-road-edge-markings/implement.jsonl new file mode 100644 index 0000000..9dd3234 --- /dev/null +++ b/.trellis/tasks/08-17-native-road-edge-markings/implement.jsonl @@ -0,0 +1 @@ +{"_example": "Fill with {\"file\": \"\", \"reason\": \"\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."} diff --git a/.trellis/tasks/08-17-native-road-edge-markings/prd.md b/.trellis/tasks/08-17-native-road-edge-markings/prd.md new file mode 100644 index 0000000..24df8c7 --- /dev/null +++ b/.trellis/tasks/08-17-native-road-edge-markings/prd.md @@ -0,0 +1,19 @@ +# Native road edge markings + +## Goal + +TBD. + +## Requirements + +- TBD + +## Acceptance Criteria + +- [ ] TBD + +## Notes + +- Keep `prd.md` focused on requirements, constraints, and acceptance criteria. +- Lightweight tasks can remain PRD-only. +- For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`. diff --git a/.trellis/tasks/08-17-native-road-edge-markings/task.json b/.trellis/tasks/08-17-native-road-edge-markings/task.json new file mode 100644 index 0000000..180c4df --- /dev/null +++ b/.trellis/tasks/08-17-native-road-edge-markings/task.json @@ -0,0 +1,26 @@ +{ + "id": "native-road-edge-markings", + "name": "native-road-edge-markings", + "title": "Native road edge markings", + "description": "", + "status": "planning", + "dev_type": null, + "scope": null, + "package": null, + "priority": "P2", + "creator": "dingkang", + "assignee": "dingkang", + "createdAt": "2026-08-17", + "completedAt": null, + "branch": null, + "base_branch": "feature/native-road-compiler", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "", + "meta": {} +} \ No newline at end of file diff --git a/blender/osmassets/catalog.py b/blender/osmassets/catalog.py index 7042dc3..b6800f2 100644 --- a/blender/osmassets/catalog.py +++ b/blender/osmassets/catalog.py @@ -52,6 +52,7 @@ SCENE_STYLE_FILE = "osm2streets_scene_style.json" # It is a provider adapter, not a second scene-layer registry. NATIVE_ROAD_LAYERS = ( {"source": "road_surface", "material_layer": "road_surface"}, + {"source": "edge_lines", "material_layer": "lane_separators"}, {"source": "intersection_surface", "material_layer": "intersection_surface"}, {"source": "sidewalk_surface", "material_layer": "sidewalks"}, {"source": "lane_separators", "material_layer": "lane_separators"}, diff --git a/scripts/build-area.js b/scripts/build-area.js index 59008ca..ba4a1f1 100755 --- a/scripts/build-area.js +++ b/scripts/build-area.js @@ -292,7 +292,7 @@ function buildBlenderScene(area, roadProvider) { function ensureNativeRoadLayers(area) { ensureFile(path.join(area.outputs.nativeRoadDir, "compiled.json"), "Native road compilation"); - for (const file of ["road_surface.geojson", "intersection_surface.geojson", "sidewalk_surface.geojson", "lane_separators.geojson", "center_lines.geojson", "direction_arrows.geojson", "turn_arrows.geojson", "crosswalks.geojson", "vehicle_stop_lines.geojson"]) { + for (const file of ["road_surface.geojson", "edge_lines.geojson", "intersection_surface.geojson", "sidewalk_surface.geojson", "lane_separators.geojson", "center_lines.geojson", "direction_arrows.geojson", "turn_arrows.geojson", "crosswalks.geojson", "vehicle_stop_lines.geojson"]) { ensureFile(path.join(area.outputs.nativeRoadDir, "layers", file), `Native road layer ${file}`); } } @@ -302,6 +302,7 @@ function nativeRoadRecords(area) { return { nativeRoadCompiled: fileRecord(path.join(area.outputs.nativeRoadDir, "compiled.json")), nativeRoadSurface: fileRecord(path.join(root, "road_surface.geojson")), + nativeEdgeLines: fileRecord(path.join(root, "edge_lines.geojson")), nativeIntersectionSurface: fileRecord(path.join(root, "intersection_surface.geojson")), nativeSidewalkSurface: fileRecord(path.join(root, "sidewalk_surface.geojson")), nativeLaneSeparators: fileRecord(path.join(root, "lane_separators.geojson")), diff --git a/scripts/compile-native-roads.js b/scripts/compile-native-roads.js index 811d2d9..a152628 100644 --- a/scripts/compile-native-roads.js +++ b/scripts/compile-native-roads.js @@ -34,13 +34,14 @@ function compileArea(configPath) { model: { roads: model.roads, endpoints: model.endpoints, connections: model.connections }, movements: compiled.movements, diagnostics: compiled.diagnostics, - layers: { roadSurface: "layers/road_surface.geojson", sidewalkSurface: "layers/sidewalk_surface.geojson", intersectionSurface: "layers/intersection_surface.geojson", laneCenterlines: "layers/lane_centerlines.geojson", laneSeparators: "layers/lane_separators.geojson", centerLines: "layers/center_lines.geojson", directionArrows: "layers/direction_arrows.geojson", turnArrows: "layers/turn_arrows.geojson", crosswalks: "layers/crosswalks.geojson", vehicleStopLines: "layers/vehicle_stop_lines.geojson", connectors: "layers/connectors.geojson" }, + layers: { roadSurface: "layers/road_surface.geojson", edgeLines: "layers/edge_lines.geojson", sidewalkSurface: "layers/sidewalk_surface.geojson", intersectionSurface: "layers/intersection_surface.geojson", laneCenterlines: "layers/lane_centerlines.geojson", laneSeparators: "layers/lane_separators.geojson", centerLines: "layers/center_lines.geojson", directionArrows: "layers/direction_arrows.geojson", turnArrows: "layers/turn_arrows.geojson", crosswalks: "layers/crosswalks.geojson", vehicleStopLines: "layers/vehicle_stop_lines.geojson", connectors: "layers/connectors.geojson" }, }; const comparison = compareOsm2Streets(area, result.model, compiled); writeJsonAtomic(path.join(staging, "compiled.json"), result); writeJsonAtomic(path.join(staging, "diagnostics.json"), { schema: "native-road-diagnostics/v1", diagnostics: compiled.diagnostics }); writeJsonAtomic(path.join(staging, "comparison.json"), comparison); writeJsonAtomic(path.join(staging, "layers", "road_surface.geojson"), compiled.roadSurface); + writeJsonAtomic(path.join(staging, "layers", "edge_lines.geojson"), compiled.edgeLines); writeJsonAtomic(path.join(staging, "layers", "sidewalk_surface.geojson"), compiled.sidewalkSurface); writeJsonAtomic(path.join(staging, "layers", "intersection_surface.geojson"), compiled.intersectionSurface); writeJsonAtomic(path.join(staging, "layers", "lane_centerlines.geojson"), compiled.laneCenterlines); diff --git a/scripts/lib/native-road.js b/scripts/lib/native-road.js index 880789b..e2aae17 100644 --- a/scripts/lib/native-road.js +++ b/scripts/lib/native-road.js @@ -255,6 +255,7 @@ function compileGeometry(model, overrides = { overrides: [] }) { features.push({ type: "Feature", properties: { native_id: surfaceId, directional_road_ids: directions.map((item) => item.id).join(","), osm_way_ids: road.osmWayIds.join(","), source_road_id: road.sourceRoadId, width_m: totalWidth, lane_count: directions.reduce((sum, item) => item.laneCount + sum, 0), provenance: JSON.stringify(directions.map((item) => item.provenance)), override_ids: directions.flatMap((item) => item.appliedOverrideIds).join(",") }, geometry: { type: "Polygon", coordinates: [ring] } }); } const lanes = compileLaneCenterlines(model, diagnostics, junctionPlans); + const edgeLines = compileEdgeLines(model, junctionPlans); const controls = compileControlMarkings(model, lanes, diagnostics); const centerLines = compileCenterLines(model, overrides, junctionPlans, controls, diagnostics); const markings = compileLaneMarkings(model, overrides, lanes, diagnostics, junctionPlans, controls); @@ -262,9 +263,11 @@ function compileGeometry(model, overrides = { overrides: [] }) { const connectorResult = compileConnectors(model, lanes, diagnostics, overrides); const junctionFeatures = compileJunctionSurfaces(model, junctionPlans, connectorResult.features, connectorResult.movements, diagnostics); validateConnectorContainment(connectorResult.features, junctionFeatures, diagnostics); - return { roadSurface: { type: "FeatureCollection", features }, sidewalkSurface: { type: "FeatureCollection", features: sidewalks }, intersectionSurface: { type: "FeatureCollection", features: junctionFeatures }, laneCenterlines: { type: "FeatureCollection", features: lanes.features }, laneSeparators: { type: "FeatureCollection", features: markings.separators }, centerLines: { type: "FeatureCollection", features: centerLines }, directionArrows: { type: "FeatureCollection", features: markings.directionArrows }, turnArrows: { type: "FeatureCollection", features: markings.turnArrows }, crosswalks: { type: "FeatureCollection", features: controls.crosswalks }, vehicleStopLines: { type: "FeatureCollection", features: controls.stopLines }, connectors: { type: "FeatureCollection", features: connectorResult.features }, movements: connectorResult.movements, diagnostics }; + return { roadSurface: { type: "FeatureCollection", features }, edgeLines: { type: "FeatureCollection", features: edgeLines }, sidewalkSurface: { type: "FeatureCollection", features: sidewalks }, intersectionSurface: { type: "FeatureCollection", features: junctionFeatures }, laneCenterlines: { type: "FeatureCollection", features: lanes.features }, laneSeparators: { type: "FeatureCollection", features: markings.separators }, centerLines: { type: "FeatureCollection", features: centerLines }, directionArrows: { type: "FeatureCollection", features: markings.directionArrows }, turnArrows: { type: "FeatureCollection", features: markings.turnArrows }, crosswalks: { type: "FeatureCollection", features: controls.crosswalks }, vehicleStopLines: { type: "FeatureCollection", features: controls.stopLines }, connectors: { type: "FeatureCollection", features: connectorResult.features }, movements: connectorResult.movements, diagnostics }; } +function compileEdgeLines(model, junctionPlans) { const features=[]; for (const road of model.roads) { const line=trimLineAtJunctions(road.centerline, road.sourceNodeIds, junctionPlans); for (const side of [-1,1]) { const ring=roadRing(offsetLine(line, side * road.widthMeters / 2), .12); if (ring) features.push({type:"Feature",properties:{native_id:`edge-line:${road.id}:${side<0?"right":"left"}`,road_id:road.id,side:side<0?"right":"left",color:"white",pattern:"solid",provenance:"native-road-edge-line/v1"},geometry:{type:"Polygon",coordinates:[ring]}}); } } return features; } + function compileCenterLines(model, overrides, junctionPlans, controls, diagnostics) { const features = []; const controlFeatures = [...controls.crosswalks, ...controls.stopLines]; diff --git a/scripts/road-workbench.js b/scripts/road-workbench.js index e1a06ab..120e5a9 100644 --- a/scripts/road-workbench.js +++ b/scripts/road-workbench.js @@ -48,7 +48,7 @@ function handle(request, response, area, configPath) { function state(area) { const nativeDir = area.outputs.nativeRoadDir; const osm2streetsRoadSurface = path.join(area.outputs.geojsonDir, "road_surface.geojson"); - return { areaId: area.id, compiled: readCompiled(area), overrides: loadOverrides(area.outputs.nativeRoadOverrides), comparison: readJson(path.join(nativeDir, "comparison.json")), layers: { nativeRoadSurface: readLayer(path.join(nativeDir, "layers", "road_surface.geojson")), nativeSidewalkSurface: readLayer(path.join(nativeDir, "layers", "sidewalk_surface.geojson")), nativeIntersectionSurface: readLayer(path.join(nativeDir, "layers", "intersection_surface.geojson")), laneCenterlines: readLayer(path.join(nativeDir, "layers", "lane_centerlines.geojson")), laneSeparators: readLayer(path.join(nativeDir, "layers", "lane_separators.geojson")), centerLines: readLayer(path.join(nativeDir, "layers", "center_lines.geojson")), directionArrows: readLayer(path.join(nativeDir, "layers", "direction_arrows.geojson")), turnArrows: readLayer(path.join(nativeDir, "layers", "turn_arrows.geojson")), crosswalks: readLayer(path.join(nativeDir, "layers", "crosswalks.geojson")), vehicleStopLines: readLayer(path.join(nativeDir, "layers", "vehicle_stop_lines.geojson")), connectors: readLayer(path.join(nativeDir, "layers", "connectors.geojson")), osm2streetsRoadSurface: fs.existsSync(osm2streetsRoadSurface) ? readLayer(osm2streetsRoadSurface) : null } }; + return { areaId: area.id, compiled: readCompiled(area), overrides: loadOverrides(area.outputs.nativeRoadOverrides), comparison: readJson(path.join(nativeDir, "comparison.json")), layers: { nativeRoadSurface: readLayer(path.join(nativeDir, "layers", "road_surface.geojson")), edgeLines: readLayer(path.join(nativeDir, "layers", "edge_lines.geojson")), nativeSidewalkSurface: readLayer(path.join(nativeDir, "layers", "sidewalk_surface.geojson")), nativeIntersectionSurface: readLayer(path.join(nativeDir, "layers", "intersection_surface.geojson")), laneCenterlines: readLayer(path.join(nativeDir, "layers", "lane_centerlines.geojson")), laneSeparators: readLayer(path.join(nativeDir, "layers", "lane_separators.geojson")), centerLines: readLayer(path.join(nativeDir, "layers", "center_lines.geojson")), directionArrows: readLayer(path.join(nativeDir, "layers", "direction_arrows.geojson")), turnArrows: readLayer(path.join(nativeDir, "layers", "turn_arrows.geojson")), crosswalks: readLayer(path.join(nativeDir, "layers", "crosswalks.geojson")), vehicleStopLines: readLayer(path.join(nativeDir, "layers", "vehicle_stop_lines.geojson")), connectors: readLayer(path.join(nativeDir, "layers", "connectors.geojson")), osm2streetsRoadSurface: fs.existsSync(osm2streetsRoadSurface) ? readLayer(osm2streetsRoadSurface) : null } }; } function readCompiled(area) { return readJson(path.join(area.outputs.nativeRoadDir, "compiled.json")); } function readJson(file) { return JSON.parse(fs.readFileSync(file, "utf8")); } diff --git a/scripts/workbench/app.js b/scripts/workbench/app.js index 3b07103..113b1c4 100644 --- a/scripts/workbench/app.js +++ b/scripts/workbench/app.js @@ -50,9 +50,11 @@ const markingsToggle = document.createElement("label"); markingsToggle.innerHTML = ' 车道分隔线与路口转向箭头'; const centerLinesToggle = document.createElement("label"); centerLinesToggle.innerHTML = ' 道路中心虚线'; +const edgeLinesToggle = document.createElement("label"); +edgeLinesToggle.innerHTML = ' 道路外缘线'; const controlsToggle = document.createElement("label"); controlsToggle.innerHTML = ' 斑马线与停止线'; -document.querySelector('[data-layer="lanes"]').closest("label").after(directionArrowsToggle, markingsToggle, centerLinesToggle, controlsToggle); +document.querySelector('[data-layer="lanes"]').closest("label").after(directionArrowsToggle, markingsToggle, centerLinesToggle, edgeLinesToggle, controlsToggle); let state; let selectedRoad = null; @@ -68,6 +70,7 @@ const source = () => new VectorSource(); const layers = { reference: new VectorLayer({ source: source(), visible: false, style: new Style({ fill: new Fill({ color: "rgba(123, 140, 148, .28)" }), stroke: new Stroke({ color: "#8999a0", width: 1 }) }) }), native: new VectorLayer({ source: source(), style: nativeSurfaceStyle }), + edgeLines: new VectorLayer({ source: source(), style: markingStyle }), sidewalks: new VectorLayer({ source: source(), style: sidewalkSurfaceStyle }), osm: new VectorLayer({ source: source(), style: (feature) => new Style({ stroke: new Stroke({ color: feature.get("road_id") === selectedRoad?.id ? "#006e91" : "#263630", width: feature.get("road_id") === selectedRoad?.id ? 5 : 2 }) }) }), lanes: new VectorLayer({ source: source(), style: laneStyle }), @@ -81,8 +84,8 @@ const layers = { selectedRoad: new VectorLayer({ source: source(), style: new Style({ stroke: new Stroke({ color: "#00a5cf", width: 8 }) }), zIndex: 10 }), selectedMovement: new VectorLayer({ source: source(), style: new Style({ stroke: new Stroke({ color: "#f0b323", width: 6 }) }), zIndex: 12 }), }; -const map = new Map({ target: "map", layers: [layers.reference, layers.native, layers.sidewalks, layers.osm, layers.lanes, layers.directionArrows, layers.markings, layers.centerLines, layers.controls, layers.connectors, layers.diagnostics, layers.selectedRoad, layers.osmDirection, layers.selectedMovement], view: new View({ center: [0, 0], zoom: 2 }) }); -const select = new Select({ condition: click, layers: (layer) => manualFromEndpoint ? layer === layers.osm : [layers.osm, layers.lanes, layers.directionArrows, layers.markings, layers.centerLines, layers.controls, layers.connectors, layers.native, layers.diagnostics].includes(layer), hitTolerance: 8, style: new Style({ stroke: new Stroke({ color: "#005e89", width: 5 }), fill: new Fill({ color: "rgba(0, 94, 137, .18)" }) }) }); +const map = new Map({ target: "map", layers: [layers.reference, layers.native, layers.edgeLines, layers.sidewalks, layers.osm, layers.lanes, layers.directionArrows, layers.markings, layers.centerLines, layers.controls, layers.connectors, layers.diagnostics, layers.selectedRoad, layers.osmDirection, layers.selectedMovement], view: new View({ center: [0, 0], zoom: 2 }) }); +const select = new Select({ condition: click, layers: (layer) => manualFromEndpoint ? layer === layers.osm : [layers.osm, layers.lanes, layers.directionArrows, layers.markings, layers.centerLines, layers.edgeLines, layers.controls, layers.connectors, layers.native, layers.diagnostics].includes(layer), hitTolerance: 8, style: new Style({ stroke: new Stroke({ color: "#005e89", width: 5 }), fill: new Fill({ color: "rgba(0, 94, 137, .18)" }) }) }); map.addInteraction(select); select.on("select", ({ selected }) => { const feature = selected[0]; @@ -170,6 +173,7 @@ function updateSources() { layers.sidewalks.getSource().clear(); layers.sidewalks.getSource().addFeatures(readFeatures(state.layers.nativeSidewalkSurface)); layers.osm.getSource().clear(); layers.osm.getSource().addFeatures(rawRoadFeatures()); layers.lanes.getSource().clear(); layers.lanes.getSource().addFeatures(readFeatures(state.layers.laneCenterlines)); + layers.edgeLines.getSource().clear(); layers.edgeLines.getSource().addFeatures(readFeatures(state.layers.edgeLines)); layers.directionArrows.getSource().clear(); layers.directionArrows.getSource().addFeatures(readFeatures(state.layers.directionArrows)); layers.markings.getSource().clear(); layers.markings.getSource().addFeatures([...readFeatures(state.layers.laneSeparators), ...readFeatures(state.layers.turnArrows)]); layers.centerLines.getSource().clear(); layers.centerLines.getSource().addFeatures(readFeatures(state.layers.centerLines));