update
This commit is contained in:
@@ -4,7 +4,8 @@ WORKDIR /app
|
|||||||
COPY frontend ./frontend
|
COPY frontend ./frontend
|
||||||
COPY backend ./backend
|
COPY backend ./backend
|
||||||
# Install wasm-pack
|
# Install wasm-pack
|
||||||
RUN curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
# Install wasm-pack via cargo (fallback for network issues)
|
||||||
|
RUN cargo install wasm-pack
|
||||||
WORKDIR /app/frontend
|
WORKDIR /app/frontend
|
||||||
# Build frontend
|
# Build frontend
|
||||||
RUN wasm-pack build --target web --out-name wasm --out-dir ../backend/static
|
RUN wasm-pack build --target web --out-name wasm --out-dir ../backend/static
|
||||||
|
|||||||
@@ -573,6 +573,51 @@
|
|||||||
0 0 4px rgba(255, 255, 255, 0.9);
|
0 0 4px rgba(255, 255, 255, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Road type based sizing - larger roads get larger fonts */
|
||||||
|
.label-street-motorway {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-street-primary {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-street-secondary {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #5e5e63;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-street-tertiary {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-street-residential {
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #7e7e83;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* One-way direction arrows */
|
||||||
|
.oneway-arrow {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: rgba(100, 100, 100, 0.5);
|
||||||
|
pointer-events: none;
|
||||||
|
text-shadow: none;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-theme="dark"] .oneway-arrow {
|
||||||
|
color: rgba(180, 180, 180, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
/* Apple Maps-style POI labels */
|
/* Apple Maps-style POI labels */
|
||||||
.label-poi {
|
.label-poi {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@@ -1279,7 +1324,7 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import init from './wasm.js?v=fixed_labels_v6';
|
import init from './wasm.js?v=fixed_labels_v20';
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -199,29 +199,32 @@ pub fn update_labels(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process ways for street labels
|
// Process ways for street labels - deduplicate by name
|
||||||
let client_width = window.inner_width().ok().and_then(|v| v.as_f64()).unwrap_or(width);
|
let client_width = window.inner_width().ok().and_then(|v| v.as_f64()).unwrap_or(width);
|
||||||
let client_height = window.inner_height().ok().and_then(|v| v.as_f64()).unwrap_or(height);
|
let client_height = window.inner_height().ok().and_then(|v| v.as_f64()).unwrap_or(height);
|
||||||
|
|
||||||
|
// Collect all street ways and group by name to deduplicate
|
||||||
|
use std::collections::HashMap;
|
||||||
|
let mut street_ways: HashMap<String, Vec<(&crate::types::MapWay, i32)>> = HashMap::new();
|
||||||
|
|
||||||
for tile in &visible_tiles {
|
for tile in &visible_tiles {
|
||||||
if let Some(ways) = state.ways.get(tile) {
|
if let Some(ways) = state.ways.get(tile) {
|
||||||
for way in ways {
|
for way in ways {
|
||||||
// Check if road has a name
|
|
||||||
let name: Option<&str> = way.tags.get("name").map(|s| s.as_str());
|
let name: Option<&str> = way.tags.get("name").map(|s| s.as_str());
|
||||||
let highway: Option<&str> = way.tags.get("highway").map(|s| s.as_str());
|
let highway: Option<&str> = way.tags.get("highway").map(|s| s.as_str());
|
||||||
|
|
||||||
if let (Some(name), Some(highway_type)) = (name, highway) {
|
if let (Some(name), Some(highway_type)) = (name, highway) {
|
||||||
// Skip unnamed or minor roads
|
|
||||||
if name.is_empty() { continue; }
|
if name.is_empty() { continue; }
|
||||||
|
|
||||||
// Zoom filtering
|
// Zoom filtering - only show labels when roads are clearly visible
|
||||||
|
// Higher number = requires more zoom (more zoomed in)
|
||||||
let min_zoom = match highway_type {
|
let min_zoom = match highway_type {
|
||||||
"motorway" | "trunk" => 200.0,
|
"motorway" | "trunk" => 5000.0, // Show early (major highways)
|
||||||
"primary" => 500.0,
|
"primary" => 20000.0, // Medium zoom
|
||||||
"secondary" => 1500.0,
|
"secondary" => 50000.0, // Need more zoom
|
||||||
"tertiary" => 3000.0,
|
"tertiary" => 100000.0, // Even more zoom
|
||||||
"residential" | "unclassified" => 6000.0,
|
"residential" | "unclassified" => 300000.0, // Only when very zoomed in
|
||||||
_ => 10000.0,
|
_ => 500000.0, // Minor roads - extremely zoomed in
|
||||||
};
|
};
|
||||||
if zoom < min_zoom { continue; }
|
if zoom < min_zoom { continue; }
|
||||||
|
|
||||||
@@ -234,7 +237,21 @@ pub fn update_labels(
|
|||||||
_ => 10,
|
_ => 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse road points to find midpoint and angle
|
street_ways.entry(name.to_string())
|
||||||
|
.or_insert_with(Vec::new)
|
||||||
|
.push((way, priority));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For each unique street name, pick the longest segment as representative
|
||||||
|
for (street_name, ways_with_priority) in street_ways {
|
||||||
|
// Find the way with the most points (longest segment)
|
||||||
|
let best_way = ways_with_priority.iter()
|
||||||
|
.max_by_key(|(way, _)| way.points.len());
|
||||||
|
|
||||||
|
if let Some((way, priority)) = best_way {
|
||||||
let points = &way.points;
|
let points = &way.points;
|
||||||
if points.len() < 16 { continue; }
|
if points.len() < 16 { continue; }
|
||||||
|
|
||||||
@@ -248,50 +265,78 @@ pub fn update_labels(
|
|||||||
|
|
||||||
if parsed_points.len() < 2 { continue; }
|
if parsed_points.len() < 2 { continue; }
|
||||||
|
|
||||||
// Calculate midpoint
|
// Calculate midpoint for label placement
|
||||||
let mid_idx = parsed_points.len() / 2;
|
let mid_idx = parsed_points.len() / 2;
|
||||||
let mid_point = parsed_points[mid_idx];
|
let mid_point = parsed_points[mid_idx];
|
||||||
|
|
||||||
// Calculate angle from road direction
|
// Get two points around midpoint for direction calculation
|
||||||
let p1 = if mid_idx > 0 { parsed_points[mid_idx - 1] } else { parsed_points[0] };
|
let p1_idx = mid_idx.saturating_sub(1);
|
||||||
let p2 = if mid_idx + 1 < parsed_points.len() { parsed_points[mid_idx + 1] } else { parsed_points[mid_idx] };
|
let p2_idx = (mid_idx + 1).min(parsed_points.len() - 1);
|
||||||
|
|
||||||
let (x1, y1) = project(p1[0], p1[1]);
|
let p1 = parsed_points[p1_idx];
|
||||||
let (x2, y2) = project(p2[0], p2[1]);
|
let p2 = parsed_points[p2_idx];
|
||||||
let dx = x2 - x1;
|
|
||||||
let dy = -(y2 - y1);
|
|
||||||
|
|
||||||
let mut angle_deg = (dy as f64).atan2(dx as f64).to_degrees();
|
// Project all three points to screen space (0-1 range)
|
||||||
|
let (mx_proj, my_proj) = project(mid_point[0], mid_point[1]);
|
||||||
|
let (x1_proj, y1_proj) = project(p1[0], p1[1]);
|
||||||
|
let (x2_proj, y2_proj) = project(p2[0], p2[1]);
|
||||||
|
|
||||||
// Keep text readable
|
// Apply camera transformation to get clip space (-1 to 1)
|
||||||
|
let cx = mx_proj * uniforms.params[0] + uniforms.params[2];
|
||||||
|
let cy = my_proj * uniforms.params[1] + uniforms.params[3];
|
||||||
|
let cx1 = x1_proj * uniforms.params[0] + uniforms.params[2];
|
||||||
|
let cy1 = y1_proj * uniforms.params[1] + uniforms.params[3];
|
||||||
|
let cx2 = x2_proj * uniforms.params[0] + uniforms.params[2];
|
||||||
|
let cy2 = y2_proj * uniforms.params[1] + uniforms.params[3];
|
||||||
|
|
||||||
|
// Clip check on midpoint
|
||||||
|
if cx < -1.5 || cx > 1.5 || cy < -1.5 || cy > 1.5 { continue; }
|
||||||
|
|
||||||
|
// Convert to CSS screen coordinates
|
||||||
|
// CSS: origin top-left, x right, y down
|
||||||
|
let css_x = (cx as f64 + 1.0) * 0.5 * client_width;
|
||||||
|
let css_y = (1.0 - cy as f64) * 0.5 * client_height;
|
||||||
|
let css_x1 = (cx1 as f64 + 1.0) * 0.5 * client_width;
|
||||||
|
let css_y1 = (1.0 - cy1 as f64) * 0.5 * client_height;
|
||||||
|
let css_x2 = (cx2 as f64 + 1.0) * 0.5 * client_width;
|
||||||
|
let css_y2 = (1.0 - cy2 as f64) * 0.5 * client_height;
|
||||||
|
|
||||||
|
// Calculate angle in CSS screen coordinates
|
||||||
|
// In CSS: x increases right, y increases down
|
||||||
|
// CSS rotate() is clockwise from positive x-axis
|
||||||
|
let dx = css_x2 - css_x1;
|
||||||
|
let dy = css_y2 - css_y1;
|
||||||
|
|
||||||
|
// atan2(dy, dx) gives angle from positive x-axis
|
||||||
|
// For dy > 0 (going down), we get positive angle, which rotates text clockwise
|
||||||
|
let mut angle_deg = dy.atan2(dx).to_degrees();
|
||||||
|
|
||||||
|
// Keep text readable (flip if upside down: angle between -90 and 90)
|
||||||
if angle_deg > 90.0 { angle_deg -= 180.0; }
|
if angle_deg > 90.0 { angle_deg -= 180.0; }
|
||||||
if angle_deg < -90.0 { angle_deg += 180.0; }
|
if angle_deg < -90.0 { angle_deg += 180.0; }
|
||||||
|
|
||||||
// Project midpoint to screen
|
// Determine highway type for CSS class (for font sizing)
|
||||||
let (mx, my) = project(mid_point[0], mid_point[1]);
|
let highway_type = way.tags.get("highway").map(|s| s.as_str()).unwrap_or("residential");
|
||||||
let cx = mx * uniforms.params[0] + uniforms.params[2];
|
let road_class = match highway_type {
|
||||||
let cy = my * uniforms.params[1] + uniforms.params[3];
|
"motorway" | "trunk" => "motorway",
|
||||||
|
"primary" => "primary",
|
||||||
// Clip check
|
"secondary" => "secondary",
|
||||||
if cx < -1.5 || cx > 1.5 || cy < -1.5 || cy > 1.5 { continue; }
|
"tertiary" => "tertiary",
|
||||||
|
_ => "residential",
|
||||||
let css_x = (cx as f64 + 1.0) * 0.5 * client_width;
|
};
|
||||||
let css_y = (1.0 - cy as f64) * 0.5 * client_height;
|
|
||||||
|
|
||||||
candidates.push(LabelCandidate {
|
candidates.push(LabelCandidate {
|
||||||
name: name.to_string(),
|
name: street_name,
|
||||||
x: css_x,
|
x: css_x,
|
||||||
y: css_y,
|
y: css_y,
|
||||||
priority,
|
priority: *priority,
|
||||||
is_country: false,
|
is_country: false,
|
||||||
rotation: angle_deg,
|
rotation: angle_deg,
|
||||||
label_type: LabelType::Street,
|
label_type: LabelType::Street,
|
||||||
category: "street".to_string(),
|
category: road_class.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Sort by Priority (High to Low)
|
// 4. Sort by Priority (High to Low)
|
||||||
candidates.sort_by(|a, b| b.priority.cmp(&a.priority));
|
candidates.sort_by(|a, b| b.priority.cmp(&a.priority));
|
||||||
@@ -334,7 +379,7 @@ pub fn update_labels(
|
|||||||
let class_name = match candidate.label_type {
|
let class_name = match candidate.label_type {
|
||||||
LabelType::Country => "label label-country".to_string(),
|
LabelType::Country => "label label-country".to_string(),
|
||||||
LabelType::City => "label label-city".to_string(),
|
LabelType::City => "label label-city".to_string(),
|
||||||
LabelType::Street => "label label-street".to_string(),
|
LabelType::Street => format!("label label-street label-street-{}", candidate.category),
|
||||||
LabelType::Poi => format!("label label-poi label-poi-{}", candidate.category),
|
LabelType::Poi => format!("label label-poi label-poi-{}", candidate.category),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -34,18 +34,23 @@ use labels::update_labels;
|
|||||||
use pipelines::{
|
use pipelines::{
|
||||||
Vertex,
|
Vertex,
|
||||||
ColoredVertex,
|
ColoredVertex,
|
||||||
create_simple_pipeline,
|
RoadVertex,
|
||||||
create_building_pipeline,
|
create_colored_building_pipeline,
|
||||||
create_water_pipeline,
|
create_water_pipeline,
|
||||||
create_water_line_pipeline,
|
create_water_line_pipeline,
|
||||||
|
create_road_motorway_outline_pipeline,
|
||||||
create_road_motorway_pipeline,
|
create_road_motorway_pipeline,
|
||||||
|
create_road_primary_outline_pipeline,
|
||||||
create_road_primary_pipeline,
|
create_road_primary_pipeline,
|
||||||
|
create_road_secondary_outline_pipeline,
|
||||||
create_road_secondary_pipeline,
|
create_road_secondary_pipeline,
|
||||||
|
create_road_residential_outline_pipeline,
|
||||||
create_road_residential_pipeline,
|
create_road_residential_pipeline,
|
||||||
create_landuse_green_pipeline,
|
create_landuse_green_pipeline,
|
||||||
create_landuse_residential_pipeline,
|
create_landuse_residential_pipeline,
|
||||||
create_sand_pipeline,
|
create_sand_pipeline,
|
||||||
create_railway_pipeline,
|
create_railway_pipeline,
|
||||||
|
generate_road_geometry,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[wasm_bindgen(start)]
|
#[wasm_bindgen(start)]
|
||||||
@@ -79,13 +84,40 @@ pub async fn run() {
|
|||||||
None,
|
None,
|
||||||
).await.unwrap();
|
).await.unwrap();
|
||||||
|
|
||||||
let size = window.inner_size();
|
let win = web_sys::window().unwrap();
|
||||||
|
let dpr = win.device_pixel_ratio();
|
||||||
|
let inner_width = win.inner_width().unwrap().as_f64().unwrap();
|
||||||
|
let inner_height = win.inner_height().unwrap().as_f64().unwrap();
|
||||||
|
|
||||||
|
let width = (inner_width * dpr) as u32;
|
||||||
|
let height = (inner_height * dpr) as u32;
|
||||||
|
|
||||||
let max_dim = device.limits().max_texture_dimension_2d;
|
let max_dim = device.limits().max_texture_dimension_2d;
|
||||||
let width = size.width.max(1).min(max_dim);
|
let width = width.max(1).min(max_dim);
|
||||||
let height = size.height.max(1).min(max_dim);
|
let height = height.max(1).min(max_dim);
|
||||||
|
|
||||||
|
// Explicitly resize the canvas/backing store to match physical pixels
|
||||||
|
if let Some(canvas) = window.canvas() {
|
||||||
|
canvas.set_width(width);
|
||||||
|
canvas.set_height(height);
|
||||||
|
}
|
||||||
|
|
||||||
let mut config = surface.get_default_config(&adapter, width, height).unwrap();
|
let mut config = surface.get_default_config(&adapter, width, height).unwrap();
|
||||||
surface.configure(&device, &config);
|
surface.configure(&device, &config);
|
||||||
|
|
||||||
|
// Initial MSAA Texture
|
||||||
|
let mut msaa_texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some("Multisampled Texture"),
|
||||||
|
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 4,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: config.format,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
let mut msaa_view = msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
// Initial Camera Setup
|
// Initial Camera Setup
|
||||||
let camera = Arc::new(Mutex::new(Camera {
|
let camera = Arc::new(Mutex::new(Camera {
|
||||||
x: 0.5307617,
|
x: 0.5307617,
|
||||||
@@ -309,14 +341,23 @@ pub async fn run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create Pipelines
|
// Create Pipelines
|
||||||
let building_pipeline = create_building_pipeline(&device, &config.format, &camera_bind_group_layout);
|
let building_pipeline = create_colored_building_pipeline(&device, &config.format, &camera_bind_group_layout);
|
||||||
let water_pipeline = create_water_pipeline(&device, &config.format, &camera_bind_group_layout);
|
let water_pipeline = create_water_pipeline(&device, &config.format, &camera_bind_group_layout);
|
||||||
let water_line_pipeline = create_water_line_pipeline(&device, &config.format, &camera_bind_group_layout);
|
let water_line_pipeline = create_water_line_pipeline(&device, &config.format, &camera_bind_group_layout);
|
||||||
let railway_pipeline = create_railway_pipeline(&device, &config.format, &camera_bind_group_layout);
|
let railway_pipeline = create_railway_pipeline(&device, &config.format, &camera_bind_group_layout);
|
||||||
let motorway_pipeline = create_road_motorway_pipeline(&device, &config.format, &camera_bind_group_layout);
|
|
||||||
let primary_pipeline = create_road_primary_pipeline(&device, &config.format, &camera_bind_group_layout);
|
// Road Pipelines (Outline & Fill)
|
||||||
let secondary_pipeline = create_road_secondary_pipeline(&device, &config.format, &camera_bind_group_layout);
|
let motorway_outline = create_road_motorway_outline_pipeline(&device, &config.format, &camera_bind_group_layout);
|
||||||
let residential_pipeline = create_road_residential_pipeline(&device, &config.format, &camera_bind_group_layout);
|
let motorway_fill = create_road_motorway_pipeline(&device, &config.format, &camera_bind_group_layout);
|
||||||
|
|
||||||
|
let primary_outline = create_road_primary_outline_pipeline(&device, &config.format, &camera_bind_group_layout);
|
||||||
|
let primary_fill = create_road_primary_pipeline(&device, &config.format, &camera_bind_group_layout);
|
||||||
|
|
||||||
|
let secondary_outline = create_road_secondary_outline_pipeline(&device, &config.format, &camera_bind_group_layout);
|
||||||
|
let secondary_fill = create_road_secondary_pipeline(&device, &config.format, &camera_bind_group_layout);
|
||||||
|
|
||||||
|
let residential_outline = create_road_residential_outline_pipeline(&device, &config.format, &camera_bind_group_layout);
|
||||||
|
let residential_fill = create_road_residential_pipeline(&device, &config.format, &camera_bind_group_layout);
|
||||||
let landuse_green_pipeline = create_landuse_green_pipeline(&device, &config.format, &camera_bind_group_layout);
|
let landuse_green_pipeline = create_landuse_green_pipeline(&device, &config.format, &camera_bind_group_layout);
|
||||||
let landuse_residential_pipeline = create_landuse_residential_pipeline(&device, &config.format, &camera_bind_group_layout);
|
let landuse_residential_pipeline = create_landuse_residential_pipeline(&device, &config.format, &camera_bind_group_layout);
|
||||||
let sand_pipeline = create_sand_pipeline(&device, &config.format, &camera_bind_group_layout);
|
let sand_pipeline = create_sand_pipeline(&device, &config.format, &camera_bind_group_layout);
|
||||||
@@ -327,13 +368,41 @@ pub async fn run() {
|
|||||||
|
|
||||||
match event {
|
match event {
|
||||||
Event::WindowEvent { event, .. } => match event {
|
Event::WindowEvent { event, .. } => match event {
|
||||||
WindowEvent::Resized(new_size) => {
|
WindowEvent::Resized(_) => { // Ignore winit size, calculate manually
|
||||||
|
let win = web_sys::window().unwrap();
|
||||||
|
let dpr = win.device_pixel_ratio();
|
||||||
|
let inner_width = win.inner_width().unwrap().as_f64().unwrap();
|
||||||
|
let inner_height = win.inner_height().unwrap().as_f64().unwrap();
|
||||||
|
|
||||||
|
let width = (inner_width * dpr) as u32;
|
||||||
|
let height = (inner_height * dpr) as u32;
|
||||||
|
|
||||||
let max_dim = device.limits().max_texture_dimension_2d;
|
let max_dim = device.limits().max_texture_dimension_2d;
|
||||||
let width = new_size.width.max(1).min(max_dim);
|
let width = width.max(1).min(max_dim);
|
||||||
let height = new_size.height.max(1).min(max_dim);
|
let height = height.max(1).min(max_dim);
|
||||||
|
|
||||||
|
// Explicitly resize the canvas/backing store to match physical pixels
|
||||||
|
if let Some(canvas) = window.canvas() {
|
||||||
|
canvas.set_width(width);
|
||||||
|
canvas.set_height(height);
|
||||||
|
}
|
||||||
|
|
||||||
config.width = width;
|
config.width = width;
|
||||||
config.height = height;
|
config.height = height;
|
||||||
surface.configure(&device, &config);
|
surface.configure(&device, &config);
|
||||||
|
|
||||||
|
// Recreate MSAA texture
|
||||||
|
msaa_texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
|
label: Some("Multisampled Texture"),
|
||||||
|
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 4,
|
||||||
|
dimension: wgpu::TextureDimension::D2,
|
||||||
|
format: config.format,
|
||||||
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
view_formats: &[],
|
||||||
|
});
|
||||||
|
msaa_view = msaa_texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
camera.lock().unwrap().aspect = width as f32 / height as f32;
|
camera.lock().unwrap().aspect = width as f32 / height as f32;
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -412,12 +481,12 @@ pub async fn run() {
|
|||||||
|
|
||||||
for tile in tiles_to_process {
|
for tile in tiles_to_process {
|
||||||
|
|
||||||
// Build vertex data for each feature type
|
// Build vertex data for each feature type - roads use RoadVertex
|
||||||
let mut road_motorway_vertex_data: Vec<Vertex> = Vec::new();
|
let mut road_motorway_vertex_data: Vec<RoadVertex> = Vec::new();
|
||||||
let mut road_primary_vertex_data: Vec<Vertex> = Vec::new();
|
let mut road_primary_vertex_data: Vec<RoadVertex> = Vec::new();
|
||||||
let mut road_secondary_vertex_data: Vec<Vertex> = Vec::new();
|
let mut road_secondary_vertex_data: Vec<RoadVertex> = Vec::new();
|
||||||
let mut road_residential_vertex_data: Vec<Vertex> = Vec::new();
|
let mut road_residential_vertex_data: Vec<RoadVertex> = Vec::new();
|
||||||
let mut building_vertex_data: Vec<Vertex> = Vec::new();
|
let mut building_vertex_data: Vec<ColoredVertex> = Vec::new();
|
||||||
let mut landuse_green_vertex_data: Vec<Vertex> = Vec::new();
|
let mut landuse_green_vertex_data: Vec<Vertex> = Vec::new();
|
||||||
let mut landuse_residential_vertex_data: Vec<Vertex> = Vec::new();
|
let mut landuse_residential_vertex_data: Vec<Vertex> = Vec::new();
|
||||||
let mut landuse_sand_vertex_data: Vec<Vertex> = Vec::new();
|
let mut landuse_sand_vertex_data: Vec<Vertex> = Vec::new();
|
||||||
@@ -425,47 +494,99 @@ pub async fn run() {
|
|||||||
let mut railway_vertex_data: Vec<ColoredVertex> = Vec::new();
|
let mut railway_vertex_data: Vec<ColoredVertex> = Vec::new();
|
||||||
let mut water_line_vertex_data: Vec<Vertex> = Vec::new();
|
let mut water_line_vertex_data: Vec<Vertex> = Vec::new();
|
||||||
|
|
||||||
// Process ways (roads)
|
// Process ways (roads) - generate quad geometry for zoom-aware width
|
||||||
if let Some(ways) = state_guard.ways.get(&tile) {
|
if let Some(ways) = state_guard.ways.get(&tile) {
|
||||||
for way in ways {
|
for way in ways {
|
||||||
let highway = way.tags.get("highway").map(|s| s.as_str());
|
let highway = way.tags.get("highway").map(|s| s.as_str());
|
||||||
if highway.is_none() { continue; }
|
if highway.is_none() { continue; }
|
||||||
|
|
||||||
let highway_type = highway.unwrap();
|
let highway_type = highway.unwrap();
|
||||||
let target = match highway_type {
|
|
||||||
|
// Road type: 0=motorway, 1=primary, 2=secondary, 3=residential
|
||||||
|
let road_type: f32 = match highway_type {
|
||||||
|
"motorway" | "motorway_link" | "trunk" | "trunk_link" => 0.0,
|
||||||
|
"primary" | "primary_link" => 1.0,
|
||||||
|
"secondary" | "secondary_link" => 2.0,
|
||||||
|
_ => 3.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let target: &mut Vec<RoadVertex> = match highway_type {
|
||||||
"motorway" | "motorway_link" | "trunk" | "trunk_link" => &mut road_motorway_vertex_data,
|
"motorway" | "motorway_link" | "trunk" | "trunk_link" => &mut road_motorway_vertex_data,
|
||||||
"primary" | "primary_link" => &mut road_primary_vertex_data,
|
"primary" | "primary_link" => &mut road_primary_vertex_data,
|
||||||
"secondary" | "secondary_link" => &mut road_secondary_vertex_data,
|
"secondary" | "secondary_link" => &mut road_secondary_vertex_data,
|
||||||
_ => &mut road_residential_vertex_data,
|
_ => &mut road_residential_vertex_data,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parse all points first
|
// Parse lanes (default based on road type)
|
||||||
let mut road_points: Vec<Vertex> = Vec::new();
|
let default_lanes: f32 = match highway_type {
|
||||||
|
"motorway" | "trunk" => 4.0,
|
||||||
|
"motorway_link" | "trunk_link" | "primary" => 2.0,
|
||||||
|
_ => 2.0,
|
||||||
|
};
|
||||||
|
let lanes: f32 = way.tags.get("lanes")
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(default_lanes);
|
||||||
|
|
||||||
|
// Parse all road centerline points
|
||||||
|
let mut centers: Vec<[f32; 2]> = Vec::new();
|
||||||
for chunk in way.points.chunks(8) {
|
for chunk in way.points.chunks(8) {
|
||||||
if chunk.len() < 8 { break; }
|
if chunk.len() < 8 { break; }
|
||||||
let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0u8; 4]));
|
let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0u8; 4]));
|
||||||
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4]));
|
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4]));
|
||||||
let (x, y) = project(lat as f64, lon as f64);
|
let (x, y) = project(lat as f64, lon as f64);
|
||||||
road_points.push(Vertex { position: [x, y] });
|
centers.push([x, y]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For LineList: push pairs of vertices for each segment (A-B, B-C, C-D, ...)
|
// Generate connected geometry with miter joins
|
||||||
for i in 0..road_points.len().saturating_sub(1) {
|
let geom = pipelines::roads::generate_road_geometry(¢ers, lanes, road_type);
|
||||||
target.push(road_points[i]);
|
target.extend(geom);
|
||||||
target.push(road_points[i + 1]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process buildings
|
// Process buildings with type-based colors
|
||||||
if let Some(buildings) = state_guard.buildings.get(&tile) {
|
if let Some(buildings) = state_guard.buildings.get(&tile) {
|
||||||
for building in buildings {
|
for building in buildings {
|
||||||
|
// Get building type from tags
|
||||||
|
let building_type = building.tags.get("building").map(|s| s.as_str()).unwrap_or("yes");
|
||||||
|
|
||||||
|
// Also check amenity tag for public buildings (hospitals, schools, etc.)
|
||||||
|
let amenity_type = building.tags.get("amenity").map(|s| s.as_str());
|
||||||
|
let is_public_amenity = matches!(amenity_type,
|
||||||
|
Some("hospital") | Some("school") | Some("university") | Some("college") |
|
||||||
|
Some("kindergarten") | Some("library") | Some("place_of_worship") |
|
||||||
|
Some("community_centre") | Some("townhall") | Some("police") |
|
||||||
|
Some("fire_station") | Some("courthouse") | Some("embassy")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assign color based on building type (light theme colors)
|
||||||
|
let color: [f32; 3] = if is_public_amenity {
|
||||||
|
// Public/Civic: light gray #e0ddd4
|
||||||
|
[0.88, 0.87, 0.83]
|
||||||
|
} else {
|
||||||
|
match building_type {
|
||||||
|
// Residential: cream #f2efe9
|
||||||
|
"house" | "apartments" | "residential" | "detached" | "semidetached_house" | "terrace" | "dormitory" =>
|
||||||
|
[0.95, 0.94, 0.91],
|
||||||
|
// Commercial: tan #e8e4dc
|
||||||
|
"commercial" | "retail" | "office" | "supermarket" | "kiosk" | "hotel" =>
|
||||||
|
[0.91, 0.89, 0.86],
|
||||||
|
// Industrial: gray-tan #d9d5cc
|
||||||
|
"industrial" | "warehouse" | "factory" | "manufacture" =>
|
||||||
|
[0.85, 0.84, 0.80],
|
||||||
|
// Public/Civic: light gray #e0ddd4
|
||||||
|
"school" | "hospital" | "university" | "government" | "public" | "church" | "cathedral" | "mosque" | "synagogue" | "temple" | "train_station" | "fire_station" | "police" | "library" | "kindergarten" =>
|
||||||
|
[0.88, 0.87, 0.83],
|
||||||
|
// Default: gray #d9d9d9
|
||||||
|
_ => [0.85, 0.85, 0.85],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for chunk in building.points.chunks(8) {
|
for chunk in building.points.chunks(8) {
|
||||||
if chunk.len() < 8 { break; }
|
if chunk.len() < 8 { break; }
|
||||||
let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0u8; 4]));
|
let lat = f32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0u8; 4]));
|
||||||
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4]));
|
let lon = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0u8; 4]));
|
||||||
let (x, y) = project(lat as f64, lon as f64);
|
let (x, y) = project(lat as f64, lon as f64);
|
||||||
building_vertex_data.push(Vertex { position: [x, y] });
|
building_vertex_data.push(ColoredVertex { position: [x, y], color });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -732,16 +853,16 @@ pub async fn run() {
|
|||||||
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
label: None,
|
label: None,
|
||||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||||
view: &view,
|
view: &msaa_view,
|
||||||
resolve_target: None,
|
resolve_target: Some(&view),
|
||||||
ops: wgpu::Operations {
|
ops: wgpu::Operations {
|
||||||
load: wgpu::LoadOp::Clear(wgpu::Color {
|
load: wgpu::LoadOp::Clear(wgpu::Color {
|
||||||
r: if is_dark { 0.05 } else { 0.95 },
|
r: if is_dark { 0.05 } else { 0.957 },
|
||||||
g: if is_dark { 0.05 } else { 0.95 },
|
g: if is_dark { 0.05 } else { 0.957 },
|
||||||
b: if is_dark { 0.05 } else { 0.95 },
|
b: if is_dark { 0.05 } else { 0.957 },
|
||||||
a: 1.0,
|
a: 1.0,
|
||||||
}),
|
}),
|
||||||
store: wgpu::StoreOp::Store,
|
store: wgpu::StoreOp::Discard,
|
||||||
},
|
},
|
||||||
})],
|
})],
|
||||||
depth_stencil_attachment: None,
|
depth_stencil_attachment: None,
|
||||||
@@ -799,9 +920,56 @@ pub async fn run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Draw Roads (Layered by importance)
|
// 4. Draw Roads (Layered by importance with Outline/Fill pass for seamless joins)
|
||||||
// Residential
|
|
||||||
rpass.set_pipeline(&residential_pipeline);
|
// --- PASS 1: OUTLINES (Bottom Layer) ---
|
||||||
|
// Order: Residential -> Secondary -> Primary -> Motorway
|
||||||
|
|
||||||
|
// Residential Outline
|
||||||
|
// rpass.set_pipeline(&residential_outline);
|
||||||
|
// rpass.set_bind_group(0, &camera_bind_group, &[]);
|
||||||
|
// for buffers in &tiles_to_render {
|
||||||
|
// if buffers.road_residential_vertex_count > 0 {
|
||||||
|
// rpass.set_vertex_buffer(0, buffers.road_residential_vertex_buffer.slice(..));
|
||||||
|
// rpass.draw(0..buffers.road_residential_vertex_count, 0..1);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Secondary Outline
|
||||||
|
// rpass.set_pipeline(&secondary_outline);
|
||||||
|
// rpass.set_bind_group(0, &camera_bind_group, &[]);
|
||||||
|
// for buffers in &tiles_to_render {
|
||||||
|
// if buffers.road_secondary_vertex_count > 0 {
|
||||||
|
// rpass.set_vertex_buffer(0, buffers.road_secondary_vertex_buffer.slice(..));
|
||||||
|
// rpass.draw(0..buffers.road_secondary_vertex_count, 0..1);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Primary Outline
|
||||||
|
// rpass.set_pipeline(&primary_outline);
|
||||||
|
// rpass.set_bind_group(0, &camera_bind_group, &[]);
|
||||||
|
// for buffers in &tiles_to_render {
|
||||||
|
// if buffers.road_primary_vertex_count > 0 {
|
||||||
|
// rpass.set_vertex_buffer(0, buffers.road_primary_vertex_buffer.slice(..));
|
||||||
|
// rpass.draw(0..buffers.road_primary_vertex_count, 0..1);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Motorway Outline
|
||||||
|
// rpass.set_pipeline(&motorway_outline);
|
||||||
|
// rpass.set_bind_group(0, &camera_bind_group, &[]);
|
||||||
|
// for buffers in &tiles_to_render {
|
||||||
|
// if buffers.road_motorway_vertex_count > 0 {
|
||||||
|
// rpass.set_vertex_buffer(0, buffers.road_motorway_vertex_buffer.slice(..));
|
||||||
|
// rpass.draw(0..buffers.road_motorway_vertex_count, 0..1);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// --- PASS 2: FILLS (Top Layer) ---
|
||||||
|
// Order: Residential -> Secondary -> Primary -> Motorway
|
||||||
|
|
||||||
|
// Residential Fill
|
||||||
|
rpass.set_pipeline(&residential_fill);
|
||||||
rpass.set_bind_group(0, &camera_bind_group, &[]);
|
rpass.set_bind_group(0, &camera_bind_group, &[]);
|
||||||
for buffers in &tiles_to_render {
|
for buffers in &tiles_to_render {
|
||||||
if buffers.road_residential_vertex_count > 0 {
|
if buffers.road_residential_vertex_count > 0 {
|
||||||
@@ -809,8 +977,9 @@ pub async fn run() {
|
|||||||
rpass.draw(0..buffers.road_residential_vertex_count, 0..1);
|
rpass.draw(0..buffers.road_residential_vertex_count, 0..1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Secondary
|
|
||||||
rpass.set_pipeline(&secondary_pipeline);
|
// Secondary Fill
|
||||||
|
rpass.set_pipeline(&secondary_fill);
|
||||||
rpass.set_bind_group(0, &camera_bind_group, &[]);
|
rpass.set_bind_group(0, &camera_bind_group, &[]);
|
||||||
for buffers in &tiles_to_render {
|
for buffers in &tiles_to_render {
|
||||||
if buffers.road_secondary_vertex_count > 0 {
|
if buffers.road_secondary_vertex_count > 0 {
|
||||||
@@ -818,8 +987,9 @@ pub async fn run() {
|
|||||||
rpass.draw(0..buffers.road_secondary_vertex_count, 0..1);
|
rpass.draw(0..buffers.road_secondary_vertex_count, 0..1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Primary
|
|
||||||
rpass.set_pipeline(&primary_pipeline);
|
// Primary Fill
|
||||||
|
rpass.set_pipeline(&primary_fill);
|
||||||
rpass.set_bind_group(0, &camera_bind_group, &[]);
|
rpass.set_bind_group(0, &camera_bind_group, &[]);
|
||||||
for buffers in &tiles_to_render {
|
for buffers in &tiles_to_render {
|
||||||
if buffers.road_primary_vertex_count > 0 {
|
if buffers.road_primary_vertex_count > 0 {
|
||||||
@@ -827,8 +997,9 @@ pub async fn run() {
|
|||||||
rpass.draw(0..buffers.road_primary_vertex_count, 0..1);
|
rpass.draw(0..buffers.road_primary_vertex_count, 0..1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Motorway
|
|
||||||
rpass.set_pipeline(&motorway_pipeline);
|
// Motorway Fill
|
||||||
|
rpass.set_pipeline(&motorway_fill);
|
||||||
rpass.set_bind_group(0, &camera_bind_group, &[]);
|
rpass.set_bind_group(0, &camera_bind_group, &[]);
|
||||||
for buffers in &tiles_to_render {
|
for buffers in &tiles_to_render {
|
||||||
if buffers.road_motorway_vertex_count > 0 {
|
if buffers.road_motorway_vertex_count > 0 {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Building render pipeline
|
//! Building render pipeline
|
||||||
|
|
||||||
use super::common::Vertex;
|
use super::common::{Vertex, ColoredVertex};
|
||||||
|
|
||||||
pub fn create_building_pipeline(
|
pub fn create_building_pipeline(
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
@@ -36,10 +36,6 @@ pub fn create_building_pipeline(
|
|||||||
let x = world_pos.x * camera.params.x + camera.params.z;
|
let x = world_pos.x * camera.params.x + camera.params.z;
|
||||||
let y = world_pos.y * camera.params.y + camera.params.w;
|
let y = world_pos.y * camera.params.y + camera.params.w;
|
||||||
|
|
||||||
// Globe Effect: Spherize
|
|
||||||
// let r2 = x*x + y*y;
|
|
||||||
// let w = 1.0 + r2 * 0.5;
|
|
||||||
|
|
||||||
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -48,7 +44,7 @@ pub fn create_building_pipeline(
|
|||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
// Buildings: Light: #d9d9d9 (0.85), Dark: #333333 (0.2)
|
// Buildings: Light: #d9d9d9 (0.85), Dark: #333333 (0.2)
|
||||||
let is_dark = camera.theme.x;
|
let is_dark = camera.theme.x;
|
||||||
let color = mix(vec3<f32>(0.85, 0.85, 0.85), vec3<f32>(0.2, 0.2, 0.2), is_dark);
|
let color = mix(vec3<f32>(0.92, 0.91, 0.90), vec3<f32>(0.2, 0.2, 0.2), is_dark);
|
||||||
return vec4<f32>(color, 1.0);
|
return vec4<f32>(color, 1.0);
|
||||||
}
|
}
|
||||||
"#)),
|
"#)),
|
||||||
@@ -89,7 +85,102 @@ pub fn create_building_pipeline(
|
|||||||
conservative: false,
|
conservative: false,
|
||||||
},
|
},
|
||||||
depth_stencil: None,
|
depth_stencil: None,
|
||||||
multisample: wgpu::MultisampleState::default(),
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: 4,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
|
multiview: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Colored building pipeline - uses per-vertex color for different building types
|
||||||
|
pub fn create_colored_building_pipeline(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
format: &wgpu::TextureFormat,
|
||||||
|
bind_group_layout: &wgpu::BindGroupLayout
|
||||||
|
) -> wgpu::RenderPipeline {
|
||||||
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: None,
|
||||||
|
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(r#"
|
||||||
|
struct CameraUniform {
|
||||||
|
params: vec4<f32>,
|
||||||
|
theme: vec4<f32>,
|
||||||
|
};
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> camera: CameraUniform;
|
||||||
|
|
||||||
|
struct VertexInput {
|
||||||
|
@location(0) position: vec2<f32>,
|
||||||
|
@location(1) color: vec3<f32>, // Per-vertex color (light theme)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) v_color: vec3<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(model: VertexInput) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
|
||||||
|
let x = model.position.x * camera.params.x + camera.params.z;
|
||||||
|
let y = model.position.y * camera.params.y + camera.params.w;
|
||||||
|
|
||||||
|
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
||||||
|
out.v_color = model.color;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let is_dark = camera.theme.x;
|
||||||
|
// Dark theme: darken the light theme color
|
||||||
|
let dark_color = in.v_color * 0.5;
|
||||||
|
let color = mix(in.v_color, dark_color, is_dark);
|
||||||
|
return vec4<f32>(color, 1.0);
|
||||||
|
}
|
||||||
|
"#)),
|
||||||
|
});
|
||||||
|
|
||||||
|
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("Colored Building Pipeline Layout"),
|
||||||
|
bind_group_layouts: &[bind_group_layout],
|
||||||
|
push_constant_ranges: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("Colored Building Pipeline"),
|
||||||
|
layout: Some(&pipeline_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: "vs_main",
|
||||||
|
buffers: &[ColoredVertex::desc()],
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &shader,
|
||||||
|
entry_point: "fs_main",
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: *format,
|
||||||
|
blend: Some(wgpu::BlendState::REPLACE),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||||
|
strip_index_format: None,
|
||||||
|
front_face: wgpu::FrontFace::Ccw,
|
||||||
|
cull_mode: None,
|
||||||
|
unclipped_depth: false,
|
||||||
|
polygon_mode: wgpu::PolygonMode::Fill,
|
||||||
|
conservative: false,
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: 4,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
multiview: None,
|
multiview: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,48 @@ impl ColoredVertex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GPU vertex for roads with center position, normal offset, and lanes
|
||||||
|
/// The shader will apply: final_position = center + normal * width_scale
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||||
|
pub struct RoadVertex {
|
||||||
|
pub center: [f32; 2], // Road centerline position
|
||||||
|
pub normal: [f32; 2], // Perpendicular offset direction (normalized, ±1 for each side)
|
||||||
|
pub lanes: f32, // Number of lanes (1-8)
|
||||||
|
pub road_type: f32, // 0=motorway, 1=primary, 2=secondary, 3=residential
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RoadVertex {
|
||||||
|
pub fn desc() -> wgpu::VertexBufferLayout<'static> {
|
||||||
|
wgpu::VertexBufferLayout {
|
||||||
|
array_stride: std::mem::size_of::<RoadVertex>() as wgpu::BufferAddress,
|
||||||
|
step_mode: wgpu::VertexStepMode::Vertex,
|
||||||
|
attributes: &[
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: 0,
|
||||||
|
shader_location: 0,
|
||||||
|
format: wgpu::VertexFormat::Float32x2, // center
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress,
|
||||||
|
shader_location: 1,
|
||||||
|
format: wgpu::VertexFormat::Float32x2, // normal
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: (std::mem::size_of::<[f32; 2]>() * 2) as wgpu::BufferAddress,
|
||||||
|
shader_location: 2,
|
||||||
|
format: wgpu::VertexFormat::Float32, // lanes
|
||||||
|
},
|
||||||
|
wgpu::VertexAttribute {
|
||||||
|
offset: (std::mem::size_of::<[f32; 2]>() * 2 + std::mem::size_of::<f32>()) as wgpu::BufferAddress,
|
||||||
|
shader_location: 3,
|
||||||
|
format: wgpu::VertexFormat::Float32, // road_type
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a simple render pipeline with standard configuration
|
/// Create a simple render pipeline with standard configuration
|
||||||
pub fn create_simple_pipeline(
|
pub fn create_simple_pipeline(
|
||||||
device: &wgpu::Device,
|
device: &wgpu::Device,
|
||||||
@@ -94,7 +136,62 @@ pub fn create_simple_pipeline(
|
|||||||
conservative: false,
|
conservative: false,
|
||||||
},
|
},
|
||||||
depth_stencil: None,
|
depth_stencil: None,
|
||||||
multisample: wgpu::MultisampleState::default(),
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: 4,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
|
multiview: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a road render pipeline that uses RoadVertex with lanes attribute
|
||||||
|
pub fn create_road_pipeline(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
format: &wgpu::TextureFormat,
|
||||||
|
bind_group_layout: &wgpu::BindGroupLayout,
|
||||||
|
shader: &wgpu::ShaderModule,
|
||||||
|
label: &str,
|
||||||
|
topology: wgpu::PrimitiveTopology,
|
||||||
|
) -> wgpu::RenderPipeline {
|
||||||
|
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some(label),
|
||||||
|
bind_group_layouts: &[bind_group_layout],
|
||||||
|
push_constant_ranges: &[],
|
||||||
|
});
|
||||||
|
|
||||||
|
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some(label),
|
||||||
|
layout: Some(&pipeline_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: shader,
|
||||||
|
entry_point: "vs_main",
|
||||||
|
buffers: &[RoadVertex::desc()],
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: shader,
|
||||||
|
entry_point: "fs_main",
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: *format,
|
||||||
|
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology,
|
||||||
|
strip_index_format: None,
|
||||||
|
front_face: wgpu::FrontFace::Ccw,
|
||||||
|
cull_mode: None,
|
||||||
|
unclipped_depth: false,
|
||||||
|
polygon_mode: wgpu::PolygonMode::Fill,
|
||||||
|
conservative: false,
|
||||||
|
},
|
||||||
|
depth_stencil: None,
|
||||||
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: 4,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
multiview: None,
|
multiview: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ pub fn create_landuse_green_pipeline(
|
|||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
let is_dark = camera.theme.x;
|
let is_dark = camera.theme.x;
|
||||||
// Green: Light #cdebb0, Dark #2d4a2d
|
// Green: Light #cdebb0, Dark #2d4a2d
|
||||||
let color = mix(vec3<f32>(0.80, 0.92, 0.69), vec3<f32>(0.18, 0.29, 0.18), is_dark);
|
let color = mix(vec3<f32>(0.804, 0.922, 0.690), vec3<f32>(0.18, 0.29, 0.18), is_dark);
|
||||||
return vec4<f32>(color, 1.0);
|
return vec4<f32>(color, 1.0);
|
||||||
}
|
}
|
||||||
"#)),
|
"#)),
|
||||||
|
|||||||
@@ -7,15 +7,19 @@ pub mod roads;
|
|||||||
pub mod landuse;
|
pub mod landuse;
|
||||||
pub mod railway;
|
pub mod railway;
|
||||||
|
|
||||||
pub use common::{Vertex, ColoredVertex, create_simple_pipeline};
|
pub use common::{Vertex, ColoredVertex, RoadVertex, create_simple_pipeline, create_road_pipeline};
|
||||||
pub use building::create_building_pipeline;
|
pub use building::{create_building_pipeline, create_colored_building_pipeline};
|
||||||
pub use water::{create_water_pipeline, create_water_line_pipeline};
|
pub use water::{create_water_pipeline, create_water_line_pipeline};
|
||||||
pub use roads::{
|
pub use roads::{
|
||||||
|
create_road_motorway_outline_pipeline,
|
||||||
create_road_motorway_pipeline,
|
create_road_motorway_pipeline,
|
||||||
|
create_road_primary_outline_pipeline,
|
||||||
create_road_primary_pipeline,
|
create_road_primary_pipeline,
|
||||||
|
create_road_secondary_outline_pipeline,
|
||||||
create_road_secondary_pipeline,
|
create_road_secondary_pipeline,
|
||||||
|
create_road_residential_outline_pipeline,
|
||||||
create_road_residential_pipeline,
|
create_road_residential_pipeline,
|
||||||
create_road_mesh,
|
generate_road_geometry,
|
||||||
};
|
};
|
||||||
pub use landuse::{create_landuse_green_pipeline, create_landuse_residential_pipeline, create_sand_pipeline};
|
pub use landuse::{create_landuse_green_pipeline, create_landuse_residential_pipeline, create_sand_pipeline};
|
||||||
pub use railway::create_railway_pipeline;
|
pub use railway::create_railway_pipeline;
|
||||||
|
|||||||
@@ -95,7 +95,11 @@ pub fn create_railway_pipeline(
|
|||||||
conservative: false,
|
conservative: false,
|
||||||
},
|
},
|
||||||
depth_stencil: None,
|
depth_stencil: None,
|
||||||
multisample: wgpu::MultisampleState::default(),
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: 4,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
multiview: None,
|
multiview: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,176 @@
|
|||||||
//! Road render pipelines (motorway, primary, secondary, residential)
|
//! Road render pipelines (motorway, primary, secondary, residential)
|
||||||
|
//! Now split into Outline and Fill passes for correct layering.
|
||||||
|
|
||||||
use super::common::{Vertex, create_simple_pipeline};
|
use super::common::{create_road_pipeline};
|
||||||
use crate::geo::project;
|
// use crate::geo::project; // Unused
|
||||||
|
|
||||||
/// Create road mesh geometry (thick lines as triangles)
|
// Helper for vector math
|
||||||
#[allow(dead_code)]
|
fn normalize(v: [f32; 2]) -> [f32; 2] {
|
||||||
pub fn create_road_mesh(points: &[[f64; 2]], width: f32) -> Vec<Vertex> {
|
let len = (v[0]*v[0] + v[1]*v[1]).sqrt();
|
||||||
|
if len < 0.00001 { [0.0, 0.0] } else { [v[0]/len, v[1]/len] }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dot(a: [f32; 2], b: [f32; 2]) -> f32 {
|
||||||
|
a[0]*b[0] + a[1]*b[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate properly connected road geometry with miter joins
|
||||||
|
pub fn generate_road_geometry(points: &[[f32; 2]], lanes: f32, road_type: f32) -> Vec<super::common::RoadVertex> {
|
||||||
let mut vertices = Vec::new();
|
let mut vertices = Vec::new();
|
||||||
if points.len() < 2 { return vertices; }
|
if points.len() < 2 { return vertices; }
|
||||||
|
|
||||||
for i in 0..points.len() - 1 {
|
// Computes normals for each segment
|
||||||
|
let mut segment_normals = Vec::with_capacity(points.len() - 1);
|
||||||
|
for i in 0..points.len()-1 {
|
||||||
let p1 = points[i];
|
let p1 = points[i];
|
||||||
let p2 = points[i+1];
|
let p2 = points[i+1];
|
||||||
|
let dx = p2[0] - p1[0];
|
||||||
// Convert to projected coordinates (0..1)
|
let dy = p2[1] - p1[1];
|
||||||
let (x1, y1) = project(p1[0], p1[1]);
|
let len = (dx*dx + dy*dy).sqrt();
|
||||||
let (x2, y2) = project(p2[0], p2[1]);
|
if len < 0.000001 {
|
||||||
|
segment_normals.push([0.0, 0.0]); // Degenerate
|
||||||
let dx = x2 - x1;
|
} else {
|
||||||
let dy = y2 - y1;
|
segment_normals.push([-dy/len, dx/len]);
|
||||||
let len = (dx * dx + dy * dy).sqrt();
|
|
||||||
|
|
||||||
// Skip invalid segments:
|
|
||||||
// 1. Very short segments that would create degenerate geometry
|
|
||||||
// 2. Segments where width > length (creates giant rectangles instead of roads)
|
|
||||||
if len < 0.00001 || len < (width * 2.0) as f32 { continue; }
|
|
||||||
|
|
||||||
// Normal vector scaled by width
|
|
||||||
let nx = -dy / len * width;
|
|
||||||
let ny = dx / len * width;
|
|
||||||
|
|
||||||
// 4 corners
|
|
||||||
let v1 = Vertex { position: [x1 + nx, y1 + ny] };
|
|
||||||
let v2 = Vertex { position: [x1 - nx, y1 - ny] };
|
|
||||||
let v3 = Vertex { position: [x2 + nx, y2 + ny] };
|
|
||||||
let v4 = Vertex { position: [x2 - nx, y2 - ny] };
|
|
||||||
|
|
||||||
// Triangle 1
|
|
||||||
vertices.push(v1);
|
|
||||||
vertices.push(v2);
|
|
||||||
vertices.push(v3);
|
|
||||||
|
|
||||||
// Triangle 2
|
|
||||||
vertices.push(v2);
|
|
||||||
vertices.push(v4);
|
|
||||||
vertices.push(v3);
|
|
||||||
}
|
}
|
||||||
vertices
|
}
|
||||||
|
|
||||||
|
// Generate vertices (left/right pairs) for each point
|
||||||
|
// Store temporarily
|
||||||
|
let mut point_pairs = Vec::with_capacity(points.len() * 2);
|
||||||
|
|
||||||
|
for i in 0..points.len() {
|
||||||
|
// Find normal for this vertex (miter)
|
||||||
|
let normal: [f32; 2];
|
||||||
|
let mut miter_len = 1.0f32;
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
normal = segment_normals[0];
|
||||||
|
} else if i == points.len() - 1 {
|
||||||
|
normal = segment_normals[i-1];
|
||||||
|
} else {
|
||||||
|
let n1 = segment_normals[i-1];
|
||||||
|
let n2 = segment_normals[i];
|
||||||
|
|
||||||
|
if dot(n1, n1) == 0.0 { normal = n2; }
|
||||||
|
else if dot(n2, n2) == 0.0 { normal = n1; }
|
||||||
|
else {
|
||||||
|
let sum = [n1[0] + n2[0], n1[1] + n2[1]];
|
||||||
|
let miter = normalize(sum);
|
||||||
|
let d = dot(miter, n1);
|
||||||
|
|
||||||
|
if d.abs() < 0.1 { // Too sharp
|
||||||
|
normal = n1;
|
||||||
|
} else {
|
||||||
|
miter_len = 1.0 / d;
|
||||||
|
if miter_len > 4.0 { miter_len = 4.0; } // Relaxed clamp for sharper corners
|
||||||
|
normal = miter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let p = points[i];
|
||||||
|
let nx = normal[0] * miter_len;
|
||||||
|
let ny = normal[1] * miter_len;
|
||||||
|
|
||||||
|
point_pairs.push(super::common::RoadVertex {
|
||||||
|
center: p,
|
||||||
|
normal: [nx, ny],
|
||||||
|
lanes,
|
||||||
|
road_type,
|
||||||
|
});
|
||||||
|
point_pairs.push(super::common::RoadVertex {
|
||||||
|
center: p,
|
||||||
|
normal: [-nx, -ny],
|
||||||
|
lanes,
|
||||||
|
road_type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Triangulate
|
||||||
|
let mut triangle_vertices = Vec::with_capacity((points.len() - 1) * 6);
|
||||||
|
for i in 0..points.len()-1 {
|
||||||
|
// Skip degenerate segment
|
||||||
|
if dot(segment_normals[i], segment_normals[i]) == 0.0 { continue; }
|
||||||
|
|
||||||
|
// Indices into point_pairs
|
||||||
|
let i_base = 2*i;
|
||||||
|
let j_base = 2*(i+1);
|
||||||
|
|
||||||
|
let v1 = point_pairs[i_base]; // Left curr
|
||||||
|
let v2 = point_pairs[i_base+1]; // Right curr
|
||||||
|
let v3 = point_pairs[j_base]; // Left next
|
||||||
|
let v4 = point_pairs[j_base+1]; // Right next
|
||||||
|
|
||||||
|
// Tri 1: v1, v2, v3
|
||||||
|
triangle_vertices.push(v1);
|
||||||
|
triangle_vertices.push(v2);
|
||||||
|
triangle_vertices.push(v3);
|
||||||
|
|
||||||
|
// Tri 2: v2, v4, v3
|
||||||
|
triangle_vertices.push(v2);
|
||||||
|
triangle_vertices.push(v4);
|
||||||
|
triangle_vertices.push(v3);
|
||||||
|
}
|
||||||
|
|
||||||
|
triangle_vertices
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================================================
|
||||||
|
// MOTORWAY
|
||||||
|
// ======================================================================================
|
||||||
|
|
||||||
|
pub fn create_road_motorway_outline_pipeline(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
format: &wgpu::TextureFormat,
|
||||||
|
bind_group_layout: &wgpu::BindGroupLayout
|
||||||
|
) -> wgpu::RenderPipeline {
|
||||||
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: None,
|
||||||
|
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(r#"
|
||||||
|
struct CameraUniform {
|
||||||
|
params: vec4<f32>,
|
||||||
|
theme: vec4<f32>,
|
||||||
|
};
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> camera: CameraUniform;
|
||||||
|
struct VertexInput {
|
||||||
|
@location(0) center: vec2<f32>,
|
||||||
|
@location(1) normal: vec2<f32>,
|
||||||
|
@location(2) lanes: f32,
|
||||||
|
@location(3) road_type: f32,
|
||||||
|
};
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) v_normal: vec2<f32>,
|
||||||
|
};
|
||||||
|
@vertex
|
||||||
|
fn vs_main(model: VertexInput) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
// Motorways: Outline
|
||||||
|
let base_pixels = 4.0;
|
||||||
|
let lane_factor = model.lanes / 2.0;
|
||||||
|
let width = (base_pixels * lane_factor) / (camera.params.x * 4000.0);
|
||||||
|
|
||||||
|
let normal = model.normal;
|
||||||
|
let offset = normal * width;
|
||||||
|
let world_pos = model.center + offset;
|
||||||
|
let x = world_pos.x * camera.params.x + camera.params.z;
|
||||||
|
let y = world_pos.y * camera.params.y + camera.params.w;
|
||||||
|
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
||||||
|
// Highlight miter/anti-alias edge data if needed
|
||||||
|
out.v_normal = vec2<f32>(0.0, 0.0); // Placeholder
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let is_dark = camera.theme.x;
|
||||||
|
// Outline Color: Subtle Grey (Google Maps style)
|
||||||
|
let color = mix(vec3<f32>(0.8, 0.8, 0.8), vec3<f32>(0.3, 0.3, 0.3), is_dark);
|
||||||
|
return vec4<f32>(color, 1.0);
|
||||||
|
}
|
||||||
|
"#)),
|
||||||
|
});
|
||||||
|
create_road_pipeline(device, format, bind_group_layout, &shader, "Motorway Outline Pipeline", wgpu::PrimitiveTopology::TriangleList)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_road_motorway_pipeline(
|
pub fn create_road_motorway_pipeline(
|
||||||
@@ -64,30 +188,95 @@ pub fn create_road_motorway_pipeline(
|
|||||||
@group(0) @binding(0)
|
@group(0) @binding(0)
|
||||||
var<uniform> camera: CameraUniform;
|
var<uniform> camera: CameraUniform;
|
||||||
struct VertexInput {
|
struct VertexInput {
|
||||||
@location(0) position: vec2<f32>,
|
@location(0) center: vec2<f32>,
|
||||||
|
@location(1) normal: vec2<f32>,
|
||||||
|
@location(2) lanes: f32,
|
||||||
|
@location(3) road_type: f32,
|
||||||
};
|
};
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
@builtin(position) clip_position: vec4<f32>,
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) v_normal: vec2<f32>,
|
||||||
};
|
};
|
||||||
@vertex
|
@vertex
|
||||||
fn vs_main(model: VertexInput) -> VertexOutput {
|
fn vs_main(model: VertexInput) -> VertexOutput {
|
||||||
var out: VertexOutput;
|
var out: VertexOutput;
|
||||||
let world_pos = model.position;
|
// Motorways: Fill
|
||||||
|
let base_pixels = 3.0;
|
||||||
|
let lane_factor = model.lanes / 2.0;
|
||||||
|
let width = (base_pixels * lane_factor) / (camera.params.x * 4000.0);
|
||||||
|
|
||||||
|
let world_pos = model.center + model.normal * width;
|
||||||
let x = world_pos.x * camera.params.x + camera.params.z;
|
let x = world_pos.x * camera.params.x + camera.params.z;
|
||||||
let y = world_pos.y * camera.params.y + camera.params.w;
|
let y = world_pos.y * camera.params.y + camera.params.w;
|
||||||
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
||||||
|
out.v_normal = model.normal;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@fragment
|
@fragment
|
||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
let is_dark = camera.theme.x;
|
let is_dark = camera.theme.x;
|
||||||
// Light: #e990a0, Dark: #d97080 (slightly darker/richer)
|
// Fill Color: Slight off-white/warm #fffcfa
|
||||||
let color = mix(vec3<f32>(0.91, 0.56, 0.63), vec3<f32>(0.85, 0.44, 0.50), is_dark);
|
let color = mix(vec3<f32>(1.0, 0.99, 0.98), vec3<f32>(0.5, 0.5, 0.5), is_dark);
|
||||||
return vec4<f32>(color, 1.0);
|
return vec4<f32>(color, 1.0);
|
||||||
}
|
}
|
||||||
"#)),
|
"#)),
|
||||||
});
|
});
|
||||||
create_simple_pipeline(device, format, bind_group_layout, &shader, "Motorway Pipeline", wgpu::PrimitiveTopology::LineList)
|
create_road_pipeline(device, format, bind_group_layout, &shader, "Motorway Fill Pipeline", wgpu::PrimitiveTopology::TriangleList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================================================
|
||||||
|
// PRIMARY
|
||||||
|
// ======================================================================================
|
||||||
|
|
||||||
|
pub fn create_road_primary_outline_pipeline(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
format: &wgpu::TextureFormat,
|
||||||
|
bind_group_layout: &wgpu::BindGroupLayout
|
||||||
|
) -> wgpu::RenderPipeline {
|
||||||
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: None,
|
||||||
|
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(r#"
|
||||||
|
struct CameraUniform {
|
||||||
|
params: vec4<f32>,
|
||||||
|
theme: vec4<f32>,
|
||||||
|
};
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> camera: CameraUniform;
|
||||||
|
struct VertexInput {
|
||||||
|
@location(0) center: vec2<f32>,
|
||||||
|
@location(1) normal: vec2<f32>,
|
||||||
|
@location(2) lanes: f32,
|
||||||
|
@location(3) road_type: f32,
|
||||||
|
};
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) v_normal: vec2<f32>,
|
||||||
|
};
|
||||||
|
@vertex
|
||||||
|
fn vs_main(model: VertexInput) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
// Primary: Outline
|
||||||
|
let base_pixels = 3.5;
|
||||||
|
let lane_factor = model.lanes / 2.0;
|
||||||
|
let width = (base_pixels * lane_factor) / (camera.params.x * 2000.0);
|
||||||
|
|
||||||
|
let world_pos = model.center + model.normal * width;
|
||||||
|
let x = world_pos.x * camera.params.x + camera.params.z;
|
||||||
|
let y = world_pos.y * camera.params.y + camera.params.w;
|
||||||
|
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
||||||
|
out.v_normal = model.normal;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let is_dark = camera.theme.x;
|
||||||
|
// Outline: Subtle Grey
|
||||||
|
let color = mix(vec3<f32>(0.8, 0.8, 0.8), vec3<f32>(0.3, 0.3, 0.3), is_dark);
|
||||||
|
return vec4<f32>(color, 1.0);
|
||||||
|
}
|
||||||
|
"#)),
|
||||||
|
});
|
||||||
|
create_road_pipeline(device, format, bind_group_layout, &shader, "Primary Road Outline Pipeline", wgpu::PrimitiveTopology::TriangleList)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_road_primary_pipeline(
|
pub fn create_road_primary_pipeline(
|
||||||
@@ -105,30 +294,97 @@ pub fn create_road_primary_pipeline(
|
|||||||
@group(0) @binding(0)
|
@group(0) @binding(0)
|
||||||
var<uniform> camera: CameraUniform;
|
var<uniform> camera: CameraUniform;
|
||||||
struct VertexInput {
|
struct VertexInput {
|
||||||
@location(0) position: vec2<f32>,
|
@location(0) center: vec2<f32>,
|
||||||
|
@location(1) normal: vec2<f32>,
|
||||||
|
@location(2) lanes: f32,
|
||||||
|
@location(3) road_type: f32,
|
||||||
};
|
};
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
@builtin(position) clip_position: vec4<f32>,
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) v_normal: vec2<f32>,
|
||||||
};
|
};
|
||||||
@vertex
|
@vertex
|
||||||
fn vs_main(model: VertexInput) -> VertexOutput {
|
fn vs_main(model: VertexInput) -> VertexOutput {
|
||||||
var out: VertexOutput;
|
var out: VertexOutput;
|
||||||
let world_pos = model.position;
|
// Primary: Fill
|
||||||
|
let base_pixels = 2.5;
|
||||||
|
let lane_factor = model.lanes / 2.0;
|
||||||
|
let width = (base_pixels * lane_factor) / (camera.params.x * 2000.0);
|
||||||
|
|
||||||
|
let world_pos = model.center + model.normal * width;
|
||||||
let x = world_pos.x * camera.params.x + camera.params.z;
|
let x = world_pos.x * camera.params.x + camera.params.z;
|
||||||
let y = world_pos.y * camera.params.y + camera.params.w;
|
let y = world_pos.y * camera.params.y + camera.params.w;
|
||||||
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
||||||
|
out.v_normal = model.normal;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@fragment
|
@fragment
|
||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
let is_dark = camera.theme.x;
|
let is_dark = camera.theme.x;
|
||||||
// Light: #fdbf6f, Dark: #e09f3f
|
// Fill
|
||||||
|
// Fill
|
||||||
let color = mix(vec3<f32>(0.99, 0.75, 0.44), vec3<f32>(0.88, 0.62, 0.25), is_dark);
|
let color = mix(vec3<f32>(0.99, 0.75, 0.44), vec3<f32>(0.88, 0.62, 0.25), is_dark);
|
||||||
return vec4<f32>(color, 1.0);
|
return vec4<f32>(color, 1.0);
|
||||||
}
|
}
|
||||||
"#)),
|
"#)),
|
||||||
});
|
});
|
||||||
create_simple_pipeline(device, format, bind_group_layout, &shader, "Primary Road Pipeline", wgpu::PrimitiveTopology::LineList)
|
create_road_pipeline(device, format, bind_group_layout, &shader, "Primary Road Fill Pipeline", wgpu::PrimitiveTopology::TriangleList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================================================
|
||||||
|
// SECONDARY
|
||||||
|
// ======================================================================================
|
||||||
|
|
||||||
|
pub fn create_road_secondary_outline_pipeline(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
format: &wgpu::TextureFormat,
|
||||||
|
bind_group_layout: &wgpu::BindGroupLayout
|
||||||
|
) -> wgpu::RenderPipeline {
|
||||||
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: None,
|
||||||
|
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(r#"
|
||||||
|
struct CameraUniform {
|
||||||
|
params: vec4<f32>,
|
||||||
|
theme: vec4<f32>,
|
||||||
|
};
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> camera: CameraUniform;
|
||||||
|
struct VertexInput {
|
||||||
|
@location(0) center: vec2<f32>,
|
||||||
|
@location(1) normal: vec2<f32>,
|
||||||
|
@location(2) lanes: f32,
|
||||||
|
@location(3) road_type: f32,
|
||||||
|
};
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) v_normal: vec2<f32>,
|
||||||
|
};
|
||||||
|
@vertex
|
||||||
|
fn vs_main(model: VertexInput) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
// Secondary: Outline
|
||||||
|
let base_pixels = 4.0;
|
||||||
|
let lane_factor = model.lanes / 2.0;
|
||||||
|
let width = (base_pixels * lane_factor) / (camera.params.x * 500.0);
|
||||||
|
|
||||||
|
let world_pos = model.center + model.normal * width;
|
||||||
|
let x = world_pos.x * camera.params.x + camera.params.z;
|
||||||
|
let y = world_pos.y * camera.params.y + camera.params.w;
|
||||||
|
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
||||||
|
out.v_normal = model.normal;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let is_dark = camera.theme.x;
|
||||||
|
// Outline: Gray
|
||||||
|
// Outline: Gray
|
||||||
|
let color = mix(vec3<f32>(0.5, 0.5, 0.5), vec3<f32>(0.2, 0.2, 0.2), is_dark);
|
||||||
|
return vec4<f32>(color, 1.0);
|
||||||
|
}
|
||||||
|
"#)),
|
||||||
|
});
|
||||||
|
create_road_pipeline(device, format, bind_group_layout, &shader, "Secondary Road Outline Pipeline", wgpu::PrimitiveTopology::TriangleList)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_road_secondary_pipeline(
|
pub fn create_road_secondary_pipeline(
|
||||||
@@ -146,30 +402,97 @@ pub fn create_road_secondary_pipeline(
|
|||||||
@group(0) @binding(0)
|
@group(0) @binding(0)
|
||||||
var<uniform> camera: CameraUniform;
|
var<uniform> camera: CameraUniform;
|
||||||
struct VertexInput {
|
struct VertexInput {
|
||||||
@location(0) position: vec2<f32>,
|
@location(0) center: vec2<f32>,
|
||||||
|
@location(1) normal: vec2<f32>,
|
||||||
|
@location(2) lanes: f32,
|
||||||
|
@location(3) road_type: f32,
|
||||||
};
|
};
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
@builtin(position) clip_position: vec4<f32>,
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) v_normal: vec2<f32>,
|
||||||
};
|
};
|
||||||
@vertex
|
@vertex
|
||||||
fn vs_main(model: VertexInput) -> VertexOutput {
|
fn vs_main(model: VertexInput) -> VertexOutput {
|
||||||
var out: VertexOutput;
|
var out: VertexOutput;
|
||||||
let world_pos = model.position;
|
// Secondary: Fill
|
||||||
|
let base_pixels = 3.0;
|
||||||
|
let lane_factor = model.lanes / 2.0;
|
||||||
|
let width = (base_pixels * lane_factor) / (camera.params.x * 500.0);
|
||||||
|
|
||||||
|
let world_pos = model.center + model.normal * width;
|
||||||
let x = world_pos.x * camera.params.x + camera.params.z;
|
let x = world_pos.x * camera.params.x + camera.params.z;
|
||||||
let y = world_pos.y * camera.params.y + camera.params.w;
|
let y = world_pos.y * camera.params.y + camera.params.w;
|
||||||
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
||||||
|
out.v_normal = model.normal;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@fragment
|
@fragment
|
||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
let is_dark = camera.theme.x;
|
let is_dark = camera.theme.x;
|
||||||
// Light: White, Dark: #444444
|
// Fill: White/Dark Gray
|
||||||
let color = mix(vec3<f32>(1.0, 1.0, 1.0), vec3<f32>(0.27, 0.27, 0.27), is_dark);
|
// Fill: White/Dark Gray
|
||||||
|
let color = mix(vec3<f32>(1.0, 1.0, 1.0), vec3<f32>(0.35, 0.35, 0.35), is_dark);
|
||||||
return vec4<f32>(color, 1.0);
|
return vec4<f32>(color, 1.0);
|
||||||
}
|
}
|
||||||
"#)),
|
"#)),
|
||||||
});
|
});
|
||||||
create_simple_pipeline(device, format, bind_group_layout, &shader, "Secondary Road Pipeline", wgpu::PrimitiveTopology::LineList)
|
create_road_pipeline(device, format, bind_group_layout, &shader, "Secondary Road Fill Pipeline", wgpu::PrimitiveTopology::TriangleList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ======================================================================================
|
||||||
|
// RESIDENTIAL
|
||||||
|
// ======================================================================================
|
||||||
|
|
||||||
|
pub fn create_road_residential_outline_pipeline(
|
||||||
|
device: &wgpu::Device,
|
||||||
|
format: &wgpu::TextureFormat,
|
||||||
|
bind_group_layout: &wgpu::BindGroupLayout
|
||||||
|
) -> wgpu::RenderPipeline {
|
||||||
|
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: None,
|
||||||
|
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(r#"
|
||||||
|
struct CameraUniform {
|
||||||
|
params: vec4<f32>,
|
||||||
|
theme: vec4<f32>,
|
||||||
|
};
|
||||||
|
@group(0) @binding(0)
|
||||||
|
var<uniform> camera: CameraUniform;
|
||||||
|
struct VertexInput {
|
||||||
|
@location(0) center: vec2<f32>,
|
||||||
|
@location(1) normal: vec2<f32>,
|
||||||
|
@location(2) lanes: f32,
|
||||||
|
@location(3) road_type: f32,
|
||||||
|
};
|
||||||
|
struct VertexOutput {
|
||||||
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) v_normal: vec2<f32>,
|
||||||
|
};
|
||||||
|
@vertex
|
||||||
|
fn vs_main(model: VertexInput) -> VertexOutput {
|
||||||
|
var out: VertexOutput;
|
||||||
|
// Residential: Outline
|
||||||
|
let base_pixels = 3.2;
|
||||||
|
let lane_factor = model.lanes / 2.0;
|
||||||
|
let width = (base_pixels * lane_factor) / (camera.params.x * 250.0);
|
||||||
|
|
||||||
|
let world_pos = model.center + model.normal * width;
|
||||||
|
let x = world_pos.x * camera.params.x + camera.params.z;
|
||||||
|
let y = world_pos.y * camera.params.y + camera.params.w;
|
||||||
|
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
||||||
|
out.v_normal = model.normal;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let is_dark = camera.theme.x;
|
||||||
|
// Outline: Gray
|
||||||
|
// Outline: Gray
|
||||||
|
let color = mix(vec3<f32>(0.5, 0.5, 0.5), vec3<f32>(0.2, 0.2, 0.2), is_dark);
|
||||||
|
return vec4<f32>(color, 1.0);
|
||||||
|
}
|
||||||
|
"#)),
|
||||||
|
});
|
||||||
|
create_road_pipeline(device, format, bind_group_layout, &shader, "Residential Road Outline Pipeline", wgpu::PrimitiveTopology::TriangleList)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_road_residential_pipeline(
|
pub fn create_road_residential_pipeline(
|
||||||
@@ -187,28 +510,39 @@ pub fn create_road_residential_pipeline(
|
|||||||
@group(0) @binding(0)
|
@group(0) @binding(0)
|
||||||
var<uniform> camera: CameraUniform;
|
var<uniform> camera: CameraUniform;
|
||||||
struct VertexInput {
|
struct VertexInput {
|
||||||
@location(0) position: vec2<f32>,
|
@location(0) center: vec2<f32>,
|
||||||
|
@location(1) normal: vec2<f32>,
|
||||||
|
@location(2) lanes: f32,
|
||||||
|
@location(3) road_type: f32,
|
||||||
};
|
};
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
@builtin(position) clip_position: vec4<f32>,
|
@builtin(position) clip_position: vec4<f32>,
|
||||||
|
@location(0) v_normal: vec2<f32>,
|
||||||
};
|
};
|
||||||
@vertex
|
@vertex
|
||||||
fn vs_main(model: VertexInput) -> VertexOutput {
|
fn vs_main(model: VertexInput) -> VertexOutput {
|
||||||
var out: VertexOutput;
|
var out: VertexOutput;
|
||||||
let world_pos = model.position;
|
// Residential: Fill
|
||||||
|
let base_pixels = 2.2;
|
||||||
|
let lane_factor = model.lanes / 2.0;
|
||||||
|
let width = (base_pixels * lane_factor) / (camera.params.x * 250.0);
|
||||||
|
|
||||||
|
let world_pos = model.center + model.normal * width;
|
||||||
let x = world_pos.x * camera.params.x + camera.params.z;
|
let x = world_pos.x * camera.params.x + camera.params.z;
|
||||||
let y = world_pos.y * camera.params.y + camera.params.w;
|
let y = world_pos.y * camera.params.y + camera.params.w;
|
||||||
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
out.clip_position = vec4<f32>(x, y, 0.0, 1.0);
|
||||||
|
out.v_normal = model.normal;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@fragment
|
@fragment
|
||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
let is_dark = camera.theme.x;
|
let is_dark = camera.theme.x;
|
||||||
// Light: White, Dark: #333333
|
// Fill: White/Dark Gray
|
||||||
let color = mix(vec3<f32>(1.0, 1.0, 1.0), vec3<f32>(0.2, 0.2, 0.2), is_dark);
|
// Fill: White/Dark Gray
|
||||||
|
let color = mix(vec3<f32>(1.0, 1.0, 1.0), vec3<f32>(0.35, 0.35, 0.35), is_dark);
|
||||||
return vec4<f32>(color, 1.0);
|
return vec4<f32>(color, 1.0);
|
||||||
}
|
}
|
||||||
"#)),
|
"#)),
|
||||||
});
|
});
|
||||||
create_simple_pipeline(device, format, bind_group_layout, &shader, "Residential Road Pipeline", wgpu::PrimitiveTopology::LineList)
|
create_road_pipeline(device, format, bind_group_layout, &shader, "Residential Road Fill Pipeline", wgpu::PrimitiveTopology::TriangleList)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ pub fn create_water_pipeline(
|
|||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
// Water: Light: #9ecaff, Dark: #1a2639
|
// Water: Light: #9ecaff, Dark: #1a2639
|
||||||
let is_dark = camera.theme.x;
|
let is_dark = camera.theme.x;
|
||||||
let color = mix(vec3<f32>(0.62, 0.79, 1.0), vec3<f32>(0.1, 0.15, 0.22), is_dark);
|
let color = mix(vec3<f32>(0.647, 0.749, 0.867), vec3<f32>(0.1, 0.15, 0.22), is_dark);
|
||||||
return vec4<f32>(color, 1.0);
|
return vec4<f32>(color, 1.0);
|
||||||
}
|
}
|
||||||
"#)),
|
"#)),
|
||||||
@@ -99,7 +99,11 @@ pub fn create_water_pipeline(
|
|||||||
conservative: false,
|
conservative: false,
|
||||||
},
|
},
|
||||||
depth_stencil: None,
|
depth_stencil: None,
|
||||||
multisample: wgpu::MultisampleState::default(),
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: 4,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
multiview: None,
|
multiview: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -137,7 +141,7 @@ pub fn create_water_line_pipeline(
|
|||||||
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
let is_dark = camera.theme.x;
|
let is_dark = camera.theme.x;
|
||||||
// Light: #a5bfdd (same/similar to water), Dark: #4a6fa5
|
// Light: #a5bfdd (same/similar to water), Dark: #4a6fa5
|
||||||
let color = mix(vec3<f32>(0.66, 0.82, 0.96), vec3<f32>(0.29, 0.44, 0.65), is_dark);
|
let color = mix(vec3<f32>(0.647, 0.749, 0.867), vec3<f32>(0.29, 0.44, 0.65), is_dark);
|
||||||
return vec4<f32>(color, 1.0);
|
return vec4<f32>(color, 1.0);
|
||||||
}
|
}
|
||||||
"#)),
|
"#)),
|
||||||
|
|||||||
@@ -632,10 +632,10 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
// Apply simplification based on zoom level
|
// Apply simplification based on zoom level
|
||||||
let base_epsilon = match zoom {
|
let base_epsilon = match zoom {
|
||||||
2 => 0.0001,
|
2 => 0.01, // Was 0.0001 (~11m) -> Now ~1km
|
||||||
4 => 0.00005,
|
4 => 0.002, // Was 0.00005 (~5m) -> Now ~200m
|
||||||
6 => 0.00002,
|
6 => 0.0005, // Was 0.00002 (~2m) -> Now ~50m
|
||||||
9 => 0.00001,
|
9 => 0.0001, // Was 0.00001 (~1m) -> Now ~10m
|
||||||
12 => 0.000005,
|
12 => 0.000005,
|
||||||
_ => 0.0,
|
_ => 0.0,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user