Files
citygen/src/gen/city.ts
T

413 lines
21 KiB
TypeScript

// Уровень города: зоны, дороги, контуры зданий, уличные объекты.
// Интерьеры генерируются отдельно (interior.ts) — в воркерах.
import type { OsmElement, BBox } from '../osm/overpass';
import type { Area, AreaKind, Building, Prop, Ring, Road, RoadKind, Vec, World } from '../types';
import { Projector } from './project';
import {
bboxOf, centroid, cleanRing, dist, distToSegment, minAreaRectAngle,
pointInRing, pointInRings, ringArea, rotate, signedArea,
} from '../util/geom';
import { Rng, hashString } from '../util/rng';
import { SpatialGrid } from '../util/spatial';
import { classify, clampLevels, defaultLevels, parseLevels } from './classify';
import { carShapes, CAR_COLORS, treeProp } from './props';
export interface InteriorJob {
building: Building;
/** Ближайшие к зданию участки дорог: [a, b, вес]. Вход ставится к дороге. */
roadSegs: [Vec, Vec, number][];
}
const LANDUSE: Record<string, AreaKind> = {
residential: 'residential', commercial: 'commercial', retail: 'retail', industrial: 'industrial',
farmland: 'farmland', farm: 'farmland', farmyard: 'farmyard', grass: 'grass', village_green: 'grass',
recreation_ground: 'grass', meadow: 'meadow', forest: 'forest', orchard: 'orchard', vineyard: 'vineyard',
cemetery: 'cemetery', construction: 'construction', brownfield: 'construction', greenfield: 'meadow',
railway: 'railway', allotments: 'garden', basin: 'water', reservoir: 'water', plant_nursery: 'garden',
flowerbed: 'garden', garages: 'industrial', religious: 'grass', education: 'school',
};
const NATURAL: Record<string, AreaKind> = {
water: 'water', wood: 'forest', scrub: 'scrub', heath: 'scrub', grassland: 'meadow', wetland: 'wetland',
beach: 'beach', sand: 'sand', bare_rock: 'sand', scree: 'sand',
};
const LEISURE: Record<string, AreaKind> = {
park: 'park', garden: 'garden', pitch: 'pitch', track: 'pitch', playground: 'playground',
golf_course: 'grass', swimming_pool: 'water', dog_park: 'grass', common: 'grass',
};
const AMENITY_AREA: Record<string, AreaKind> = {
parking: 'parking', school: 'school', kindergarten: 'school', college: 'school', university: 'school',
hospital: 'hospital', grave_yard: 'cemetery', marketplace: 'pedestrian', fuel: 'pedestrian',
};
const AREA_TIER: Record<AreaKind, number> = {
residential: 0, commercial: 0, industrial: 0, retail: 0, farmland: 0, railway: 0, school: 0, hospital: 0, construction: 0,
farmyard: 1, forest: 1, meadow: 1, grass: 1, park: 1, scrub: 1, orchard: 1, vineyard: 1, cemetery: 1, garden: 1,
wetland: 1, beach: 1, sand: 1,
pitch: 2, playground: 2, parking: 2, pedestrian: 2, water: 3,
};
const ROAD_WIDTH: Record<string, number> = {
motorway: 11, trunk: 10, primary: 9, secondary: 8, tertiary: 7, residential: 6, unclassified: 5.5,
living_street: 5, service: 3.6, pedestrian: 5, track: 3, footway: 1.8, cycleway: 2, path: 1.2, steps: 2,
bridleway: 1.5, road: 5,
};
const SIDEWALK: Record<string, number> = { primary: 2.2, secondary: 2, tertiary: 1.8, residential: 1.5, living_street: 0, trunk: 0 };
function areaKindOf(t: Record<string, string>): AreaKind | null {
if (t.building) return null;
if (t.landuse && LANDUSE[t.landuse]) return LANDUSE[t.landuse];
if (t.natural && NATURAL[t.natural]) return NATURAL[t.natural];
if (t.waterway === 'riverbank' || t.waterway === 'dock') return 'water';
if (t.leisure && LEISURE[t.leisure]) return LEISURE[t.leisure];
if (t.amenity && AMENITY_AREA[t.amenity]) return AMENITY_AREA[t.amenity];
if (t.place === 'square' || t['area:highway'] || (t.highway === 'pedestrian' && t.area === 'yes')) return 'pedestrian';
return null;
}
/** Сборка колец мультиполигона из кусков путей. */
function assembleRings(parts: Vec[][]): Vec[][] {
const pool = parts.filter((p) => p.length >= 2).map((p) => p.slice());
const rings: Vec[][] = [];
const same = (a: Vec, b: Vec) => Math.abs(a[0] - b[0]) < 1e-6 && Math.abs(a[1] - b[1]) < 1e-6;
while (pool.length) {
let cur = pool.shift()!;
let grew = true;
while (!same(cur[0], cur[cur.length - 1]) && grew) {
grew = false;
for (let i = 0; i < pool.length; i++) {
const p = pool[i];
const end = cur[cur.length - 1];
if (same(end, p[0])) cur = cur.concat(p.slice(1));
else if (same(end, p[p.length - 1])) cur = cur.concat(p.slice(0, -1).reverse());
else if (same(cur[0], p[p.length - 1])) cur = p.slice(0, -1).concat(cur);
else if (same(cur[0], p[0])) cur = p.slice(1).reverse().concat(cur);
else continue;
pool.splice(i, 1);
grew = true;
break;
}
}
if (cur.length >= 4) rings.push(cur);
}
return rings;
}
interface Poly {
outer: Ring;
holes: Ring[];
}
function polysOfElement(el: OsmElement, proj: Projector): Poly[] {
if (el.type === 'way' && el.geometry) {
const g = el.geometry;
if (g.length < 4) return [];
const a = g[0], b = g[g.length - 1];
if (a.lat !== b.lat || a.lon !== b.lon) return [];
return [{ outer: g.map((p) => proj.toXY(p.lat, p.lon)), holes: [] }];
}
if (el.type === 'relation' && el.members) {
const outers = assembleRings(el.members.filter((m) => m.role !== 'inner' && m.geometry).map((m) => m.geometry!.map((p) => proj.toXY(p.lat, p.lon))));
const inners = assembleRings(el.members.filter((m) => m.role === 'inner' && m.geometry).map((m) => m.geometry!.map((p) => proj.toXY(p.lat, p.lon))));
return outers.map((o) => ({ outer: o, holes: inners.filter((h) => pointInRing(h[0], o)) }));
}
return [];
}
/** Ориентируем кольца единообразно: внешнее — по часовой (в осях y-вниз), дыры — против. */
function orient(r: Ring, outer: boolean): Ring {
const a = signedArea(r);
return (a > 0) === outer ? r : r.slice().reverse();
}
export function buildCity(elements: OsmElement[], bbox: BBox, name: string, seed: number): { world: World; jobs: InteriorJob[] } {
const lat0 = (bbox[0] + bbox[2]) / 2, lon0 = (bbox[1] + bbox[3]) / 2;
const proj = new Projector(lat0, lon0);
const p0 = proj.toXY(bbox[2], bbox[1]);
const p1 = proj.toXY(bbox[0], bbox[3]);
const bounds = { minX: p0[0], minY: p0[1], maxX: p1[0], maxY: p1[1] };
const inBounds = (p: Vec, m = 0) => p[0] >= bounds.minX - m && p[0] <= bounds.maxX + m && p[1] >= bounds.minY - m && p[1] <= bounds.maxY + m;
const rng = new Rng(seed);
const areas: Area[] = [];
const roads: Road[] = [];
const props: Prop[] = [];
const pois: { p: Vec; tags: Record<string, string> }[] = [];
const buildingEls: { el: OsmElement; poly: Poly }[] = [];
for (const el of elements) {
const t = el.tags ?? {};
if (el.type === 'node') {
if (el.lat == null || el.lon == null) continue;
const p = proj.toXY(el.lat, el.lon);
if (!inBounds(p, 5)) continue;
if (t.natural === 'tree') props.push(treeProp(`t${el.id}`, p, rng, t));
else if (t.highway === 'street_lamp') props.push({ id: `l${el.id}`, kind: 'lamp', pos: p, rot: 0, w: 0.4, d: 0.4, shapes: [{ t: 'circle', x: 0, y: 0, r: 0.22, s: 'metal' }], blocksMove: true, blocksSight: false });
else if (t.amenity === 'bench') props.push({ id: `b${el.id}`, kind: 'bench', pos: p, rot: rng.range(0, Math.PI), w: 1.8, d: 0.6, shapes: [{ t: 'rect', x: 0, y: 0, w: 1.8, h: 0.55, s: 'wood' }, { t: 'rect', x: 0, y: -0.22, w: 1.8, h: 0.12, s: 'dark' }], blocksMove: true, blocksSight: false, tags: ['sit'] });
else if (t.amenity === 'fountain') props.push({ id: `f${el.id}`, kind: 'fountain', pos: p, rot: 0, w: 4, d: 4, shapes: [{ t: 'circle', x: 0, y: 0, r: 2, s: 'base' }, { t: 'circle', x: 0, y: 0, r: 1.7, s: 'water' }], blocksMove: true, blocksSight: false });
if (t.shop || t.amenity || t.office || t.craft || t.tourism || t.healthcare) pois.push({ p, tags: t });
continue;
}
if (t.building && t.building !== 'no' && t.building !== 'roof' && !t['building:part']) {
for (const poly of polysOfElement(el, proj)) buildingEls.push({ el, poly });
continue;
}
// Дороги, рельсы, водотоки
const hw = t.highway, rw = t.railway, ww = t.waterway;
const isLinear = el.type === 'way' && el.geometry && (hw || rw || ww) && !(t.area === 'yes') && !(ww === 'riverbank');
if (isLinear && ROAD_WIDTH[hw] !== undefined || (isLinear && (rw || ww))) {
const pts = el.geometry!.map((g) => proj.toXY(g.lat, g.lon));
let kind: RoadKind | null = null;
let width = 0, sidewalk = 0;
if (hw && ROAD_WIDTH[hw] !== undefined) {
kind = (['motorway', 'trunk', 'primary', 'secondary', 'tertiary', 'residential', 'unclassified', 'service', 'living_street', 'pedestrian', 'track', 'footway', 'cycleway', 'path', 'steps'] as const).find((k) => hw.startsWith(k)) ?? (hw === 'bridleway' ? 'path' : 'unclassified');
width = ROAD_WIDTH[hw.replace('_link', '')] ?? 5;
if (hw.endsWith('_link')) width *= 0.75;
if (hw === 'service' && (t.service === 'parking_aisle')) width = 5;
if (hw === 'service' && (t.service === 'driveway')) width = 3;
const lanes = parseInt(t.lanes);
if (isFinite(lanes) && lanes > 0 && ROAD_WIDTH[hw] >= 5) width = Math.max(width * 0.6, lanes * 3.2);
const w = parseFloat(t.width);
if (isFinite(w) && w > 0.5 && w < 60) width = w;
sidewalk = SIDEWALK[hw] ?? 0;
if (t.sidewalk === 'no' || t.sidewalk === 'none' || t.sidewalk === 'separate') sidewalk = 0;
else if (t.sidewalk && sidewalk === 0 && width >= 5) sidewalk = 1.5;
} else if (rw) {
if (rw === 'platform') continue;
kind = rw === 'tram' ? 'tram' : 'rail';
width = 1.6;
} else if (ww) {
kind = ww === 'river' ? 'river' : ww === 'canal' ? 'canal' : ww === 'stream' ? 'stream' : 'ditch';
width = { river: 14, canal: 8, stream: 2.5, ditch: 1.2 }[kind as 'river'];
const w = parseFloat(t.width);
if (isFinite(w) && w > 0.3 && w < 300) width = w;
if (t.tunnel === 'culvert') continue;
}
if (!kind) continue;
roads.push({
id: `r${el.id}`, osmId: `way/${el.id}`, kind, pts, width, sidewalk,
lanes: parseInt(t.lanes) || 0, oneway: t.oneway === 'yes',
layer: parseInt(t.layer) || 0, bridge: !!t.bridge && t.bridge !== 'no', tunnel: !!t.tunnel && t.tunnel !== 'no',
name: t.name,
});
continue;
}
const ak = areaKindOf(t);
if (ak) {
for (const poly of polysOfElement(el, proj)) {
const bb = bboxOf(poly.outer);
if (bb[2] < bounds.minX || bb[0] > bounds.maxX || bb[3] < bounds.minY || bb[1] > bounds.maxY) continue;
areas.push({ id: `a${el.type[0]}${el.id}_${areas.length}`, osmId: `${el.type}/${el.id}`, kind: ak, rings: [orient(cleanRing(poly.outer, 0.01), true), ...poly.holes.map((h) => orient(h, false))], name: t.name });
}
}
if (t.natural === 'tree_row' && el.type === 'way' && el.geometry) {
const pts = el.geometry.map((g) => proj.toXY(g.lat, g.lon));
let acc = 0;
for (let i = 1; i < pts.length; i++) {
const L = dist(pts[i - 1], pts[i]);
for (let s = acc; s < L; s += 8) {
const q: Vec = [pts[i - 1][0] + ((pts[i][0] - pts[i - 1][0]) * s) / L, pts[i - 1][1] + ((pts[i][1] - pts[i - 1][1]) * s) / L];
if (inBounds(q)) props.push(treeProp(`tr${el.id}_${props.length}`, q, rng, {}));
}
acc = (acc - L) % 8;
if (acc < 0) acc += 8;
}
}
}
// Порядок отрисовки зон: крупные подложки → зелень → площадки → вода
areas.sort((a, b) => AREA_TIER[a.kind] - AREA_TIER[b.kind] || ringArea(b.rings[0]) - ringArea(a.rings[0]));
// ---- Индексы для запросов
const landGrid = new SpatialGrid<Area>(100);
for (const a of areas) if (AREA_TIER[a.kind] <= 1) landGrid.insert(a, bboxOf(a.rings[0]));
type Seg = { a: Vec; b: Vec; road: Road };
const segGrid = new SpatialGrid<Seg>(40);
for (const r of roads) {
if (r.tunnel) continue;
for (let i = 1; i < r.pts.length; i++) {
const a = r.pts[i - 1], b = r.pts[i];
const m = r.width / 2 + r.sidewalk + 1;
segGrid.insert({ a, b, road: r }, [Math.min(a[0], b[0]) - m, Math.min(a[1], b[1]) - m, Math.max(a[0], b[0]) + m, Math.max(a[1], b[1]) + m]);
}
}
const poiGrid = new SpatialGrid<{ p: Vec; tags: Record<string, string> }>(30);
for (const p of pois) poiGrid.insert(p, [p.p[0], p.p[1], p.p[0], p.p[1]]);
// ---- Здания
const buildings: Building[] = [];
const bGrid = new SpatialGrid<Building>(40);
for (const { el, poly } of buildingEls) {
let outer = cleanRing(poly.outer, 0.08);
if (outer.length < 3) continue;
const area = ringArea(outer) - poly.holes.reduce((s, h) => s + ringArea(h), 0);
if (area < 6 || area > 40000) continue;
const c = centroid(outer);
if (!inBounds(c)) continue;
outer = orient(outer, true);
const t = el.tags ?? {};
const id = `b${el.type[0]}${el.id}`;
const b: Building = {
id: buildings.some((x) => x.id === id) ? `${id}_${buildings.length}` : id,
osmId: `${el.type}/${el.id}`,
kind: 'house', footprint: outer, holes: poly.holes.map((h) => orient(cleanRing(h, 0.08), false)).filter((h) => h.length >= 3),
area, levels: 1, levelsSource: 'estimate', frame: { origin: [0, 0], angle: minAreaRectAngle(outer) },
floors: [], units: [], beds: 0, seed: hashString(`${el.type}/${el.id}`) ^ seed,
name: t.name,
address: t['addr:street'] ? `${t['addr:street']} ${t['addr:housenumber'] ?? ''}`.trim() : t['addr:housenumber'],
};
(b as any)._tags = t;
buildings.push(b);
bGrid.insert(b, bboxOf(outer));
}
for (const b of buildings) {
const t = (b as any)._tags as Record<string, string>;
const brng = new Rng(b.seed);
const bb = bboxOf(b.footprint);
const inside = poiGrid.query(bb).filter((p) => pointInRing(p.p, b.footprint)).map((p) => p.tags);
const c = centroid(b.footprint);
let landuse: string | undefined;
for (const a of landGrid.query([c[0], c[1], c[0], c[1]])) {
if (AREA_TIER[a.kind] === 0 && pointInRings(c, a.rings)) landuse = a.kind;
if (a.kind === 'farmyard' && pointInRings(c, a.rings)) landuse = 'farmyard';
}
const near = bGrid.query([bb[0] - 15, bb[1] - 15, bb[2] + 15, bb[3] + 15]);
const nearBigger = near.some((o) => o !== b && o.area > b.area * 2.5 && o.area > 60);
const cls = classify({ tags: t, pois: inside, area: b.area, landuse, nearBigger }, brng);
b.kind = cls.kind;
b.use = cls.use;
if (!b.name) b.name = inside.find((p) => p.name)?.name;
const lv = parseLevels(t);
if (lv) {
b.levels = lv.levels;
b.levelsSource = lv.source;
}
}
// Этажность без тегов: медиана соседей с тегами, иначе по виду
const tagged = buildings.filter((b) => b.levelsSource !== 'estimate');
const tGrid = new SpatialGrid<Building>(100);
for (const b of tagged) tGrid.insert(b, bboxOf(b.footprint));
for (const b of buildings) {
if (b.levelsSource !== 'estimate') continue;
const brng = new Rng(b.seed ^ 0x51ed);
const c = centroid(b.footprint);
const ns = tGrid.query([c[0] - 120, c[1] - 120, c[0] + 120, c[1] + 120])
.filter((o) => o.kind === b.kind || (o.kind === 'apartments' && b.kind === 'mixed') || (o.kind === 'mixed' && b.kind === 'apartments'))
.map((o) => o.levels).sort((x, y) => x - y);
b.levels = ns.length >= 3 ? clampLevels(b.kind, ns[ns.length >> 1]) : defaultLevels(b.kind, b.area, brng);
if (b.area < 45) b.levels = Math.min(b.levels, 1);
}
for (const b of buildings) delete (b as any)._tags;
// ---- Уличные объекты: деревья в лесах/парках, машины на парковках
const blockedByBuilding = (p: Vec, m: number) =>
bGrid.query([p[0] - m, p[1] - m, p[0] + m, p[1] + m]).some((b) => pointInRing(p, b.footprint) || distToRing(p, b.footprint) < m);
const nearRoad = (p: Vec, m: number) =>
segGrid.query([p[0] - m, p[1] - m, p[0] + m, p[1] + m]).some((s) => distToSegment(p, s.a, s.b) < s.road.width / 2 + s.road.sidewalk + m);
let treeBudget = 40000;
for (const a of areas) {
const spacing = a.kind === 'forest' ? 6 : a.kind === 'park' ? 14 : a.kind === 'orchard' ? 7 : a.kind === 'cemetery' ? 16 : a.kind === 'scrub' ? 8 : 0;
if (!spacing) continue;
const bb = bboxOf(a.rings[0]);
const x0 = Math.max(bb[0], bounds.minX), x1 = Math.min(bb[2], bounds.maxX);
const y0 = Math.max(bb[1], bounds.minY), y1 = Math.min(bb[3], bounds.maxY);
const ar = new Rng(hashString(a.id) ^ seed);
for (let y = y0; y < y1 && treeBudget > 0; y += spacing)
for (let x = x0; x < x1 && treeBudget > 0; x += spacing) {
const p: Vec = [x + ar.range(0, spacing * 0.8), y + ar.range(0, spacing * 0.8)];
if (a.kind === 'park' && ar.chance(0.35)) continue;
if (!pointInRings(p, a.rings)) continue;
if (blockedByBuilding(p, 2) || nearRoad(p, 1.5)) continue;
props.push(treeProp(`ta${a.id}_${treeBudget}`, p, ar, a.kind === 'scrub' ? { scrub: '1' } : a.kind === 'orchard' ? { orchard: '1' } : {}));
treeBudget--;
}
}
// Парковки: ряды мест 2.5×5 м с проездами 6 м
for (const a of areas) {
if (a.kind !== 'parking') continue;
const outer = a.rings[0];
if (ringArea(outer) < 60) continue;
const ang = minAreaRectAngle(outer);
const local = outer.map((p) => rotate(p, -ang));
const lb = bboxOf(local);
const pr = new Rng(hashString(a.id) ^ seed);
const fill = pr.range(0.45, 0.85);
// полоса: ряд машин 5 м, проезд 6 м, ряд 5 м, ...
let y = lb[1] + 0.5;
let row = 0;
while (y + 5 <= lb[3]) {
for (let x = lb[0] + 0.3; x + 2.5 <= lb[2]; x += 2.5) {
const corners: Vec[] = [[x, y], [x + 2.5, y], [x + 2.5, y + 5], [x, y + 5]].map((q) => rotate(q as Vec, ang));
if (!corners.every((q) => pointInRings(q, a.rings))) continue;
const cc = rotate([x + 1.25, y + 2.5], ang);
if (!inBounds(cc) || blockedByBuilding(cc, 2)) continue;
// разметка места рисуем всегда, машину — с вероятностью
props.push({ id: `ps${a.id}_${props.length}`, kind: 'parking_spot', pos: cc, rot: ang, w: 2.5, d: 5, shapes: [{ t: 'line', x1: -1.25, y1: -2.5, x2: -1.25, y2: 2.5, s: 'line' }, { t: 'line', x1: 1.25, y1: -2.5, x2: 1.25, y2: 2.5, s: 'line' }], blocksMove: false, blocksSight: false });
if (pr.chance(fill)) {
const flip = row % 2 === 1 ? Math.PI : 0;
props.push({ id: `c${a.id}_${props.length}`, kind: 'car', pos: cc, rot: ang + flip + pr.range(-0.04, 0.04), w: 1.8, d: 4.4, shapes: carShapes(), blocksMove: true, blocksSight: false, color: pr.pick(CAR_COLORS), tags: ['vehicle'] });
}
}
y += row % 2 === 0 ? 5 + 6 : 5 + 0.0;
row++;
}
}
// Машины вдоль жилых улиц (параллельная парковка)
for (const r of roads) {
if (!['residential', 'tertiary', 'living_street', 'unclassified'].includes(r.kind) || r.tunnel || r.bridge) continue;
const rr = new Rng(hashString(r.id) ^ seed);
if (!rr.chance(0.6)) continue;
for (let i = 1; i < r.pts.length; i++) {
const a = r.pts[i - 1], b = r.pts[i];
const L = dist(a, b);
if (L < 12) continue;
const dir: Vec = [(b[0] - a[0]) / L, (b[1] - a[1]) / L];
const nrm: Vec = [-dir[1], dir[0]];
for (const side of [1, -1]) {
for (let s = 6; s < L - 6; s += 6) {
if (!rr.chance(0.28)) continue;
const off = r.width / 2 - 1.1;
const p: Vec = [a[0] + dir[0] * s + nrm[0] * off * side, a[1] + dir[1] * s + nrm[1] * off * side];
if (!inBounds(p) || blockedByBuilding(p, 1.5)) continue;
props.push({ id: `cs${r.id}_${props.length}`, kind: 'car', pos: p, rot: Math.atan2(dir[1], dir[0]) + (side > 0 ? Math.PI / 2 : -Math.PI / 2) + rr.range(-0.03, 0.03), w: 1.8, d: 4.4, shapes: carShapes(), blocksMove: true, blocksSight: false, color: rr.pick(CAR_COLORS), tags: ['vehicle'] });
}
}
}
}
// ---- Задания на интерьеры: здание + ближайшие дороги
const jobs: InteriorJob[] = buildings.map((b) => {
const bb = bboxOf(b.footprint);
const segs = segGrid.query([bb[0] - 40, bb[1] - 40, bb[2] + 40, bb[3] + 40])
.filter((s) => !['rail', 'tram', 'river', 'stream', 'canal', 'ditch'].includes(s.road.kind))
.map((s): [Vec, Vec, number] => [s.a, s.b, s.road.kind === 'footway' || s.road.kind === 'path' || s.road.kind === 'cycleway' ? 0.6 : s.road.kind === 'service' ? 0.8 : 1]);
return { building: b, roadSegs: segs.slice(0, 200) };
});
const world: World = {
version: 1,
id: `w${Date.now().toString(36)}`,
name,
createdAt: new Date().toISOString(),
seed,
origin: { lat: lat0, lon: lon0 },
bbox,
bounds,
areas,
roads: roads.filter((r) => r.pts.some((p) => inBounds(p, 200))),
buildings,
props: props.filter((p) => inBounds(p.pos, 2)),
models: {},
attribution: '© OpenStreetMap contributors (ODbL)',
};
return { world, jobs };
}
function distToRing(p: Vec, r: Ring): number {
let d = Infinity;
for (let i = 0; i < r.length; i++) d = Math.min(d, distToSegment(p, r[i], r[(i + 1) % r.length]));
return d;
}