Remove generated cars and increase map area limits
This commit is contained in:
1 parent
9487bc23bd
commit
e7500c8ef9
7 files changed
+12
-57
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
Выделяете на карте небольшой город или район → по данным OSM строится подробная карта
|
||||
с видом строго сверху: дороги реальной ширины с тротуарами и разметкой, парковки
|
||||
с машинами, деревья, и **у каждого здания — этажи, комнаты, двери, окна, лестницы
|
||||
без машин, деревья, и **у каждого здания — этажи, комнаты, двери, окна, лестницы
|
||||
и мебель**, нарисованные простыми геометрическими фигурами, как на плане квартиры.
|
||||
|
||||
Всё работает в браузере, без сервера: данные берутся из Overpass API, генерация
|
||||
@@ -88,7 +88,9 @@ Overpass JSON ─► city.ts ──────────────► зо
|
||||
|
||||
## Ограничения MVP
|
||||
|
||||
- Область до 6 км². Публичные серверы Overpass рассчитаны на умеренную нагрузку; для
|
||||
- Область до 25 км²; предупреждение о времени генерации — свыше 10 км².
|
||||
Скорость и расход памяти зависят от плотности застройки и этажности.
|
||||
Публичные серверы Overpass рассчитаны на умеренную нагрузку; для
|
||||
публичной игры понадобится своё зеркало или заранее нарезанные данные (например, экстракт
|
||||
Geofabrik → статические файлы).
|
||||
- Подвалы, мансарды, балконы, береговая линия морей пока не генерируются.
|
||||
|
||||
@@ -186,13 +186,6 @@ export function meetingTable(L: number): Def {
|
||||
export const filing = (): Def => ({ kind: 'filing_cabinet', w: 0.5, d: 0.6, tall: true, front: 0.6, shapes: [R(0, 0, 0.5, 0.6, 'metal', 0.02), Ln(-0.2, 0.0, 0.2, 0.0, 'dark')] });
|
||||
export const mailboxes = (): Def => ({ kind: 'mailboxes', w: 1.2, d: 0.3, shapes: [R(0, 0, 1.2, 0.3, 'metal', 0.02), Ln(-0.3, -0.14, -0.3, 0.14, 'dark'), Ln(0, -0.14, 0, 0.14, 'dark'), Ln(0.3, -0.14, 0.3, 0.14, 'dark')] });
|
||||
|
||||
export function carDef(): Def {
|
||||
return {
|
||||
kind: 'car', w: 1.8, d: 4.4, tags: ['vehicle'],
|
||||
shapes: [R(0, 0, 1.8, 4.4, 'car', 0.45), R(0, -0.75, 1.5, 0.55, 'glass', 0.15), R(0, 1.25, 1.45, 0.4, 'glass', 0.12), R(0, 0.25, 1.5, 1.4, 'dark', 0.2)],
|
||||
};
|
||||
}
|
||||
|
||||
export function shift(s: Shape, dx: number, dy: number): Shape {
|
||||
if (s.t === 'line') return { ...s, x1: s.x1 + dx, x2: s.x2 + dx, y1: s.y1 + dy, y2: s.y2 + dy };
|
||||
return { ...s, x: s.x + dx, y: s.y + dy };
|
||||
|
||||
+3
-32
@@ -11,7 +11,7 @@ import {
|
||||
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';
|
||||
import { treeProp } from './props';
|
||||
|
||||
export interface InteriorJob {
|
||||
building: Building;
|
||||
@@ -332,9 +332,7 @@ export function buildCity(elements: OsmElement[], bbox: BBox, name: string, seed
|
||||
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 м, ...
|
||||
// полоса: ряд мест 5 м, проезд 6 м, ряд 5 м, ...
|
||||
let y = lb[1] + 0.5;
|
||||
let row = 0;
|
||||
while (y + 5 <= lb[3]) {
|
||||
@@ -343,40 +341,13 @@ export function buildCity(elements: OsmElement[], bbox: BBox, name: string, seed
|
||||
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);
|
||||
|
||||
@@ -9,7 +9,6 @@ import * as K from './catalog';
|
||||
import type { Def } from './catalog';
|
||||
import { Rng } from '../util/rng';
|
||||
import { distToSegment } from '../util/geom';
|
||||
import { CAR_COLORS } from './props';
|
||||
|
||||
export interface PropL {
|
||||
prop: Prop;
|
||||
@@ -463,7 +462,6 @@ export function furnishFloor(ctx: Ctx): PropL[] {
|
||||
break;
|
||||
}
|
||||
case 'garage': {
|
||||
if (a > 14 && shortSide(room) >= 2.6) inMiddle(room, K.carDef(), 0, undefined, rng.pick(CAR_COLORS));
|
||||
atWall(room, K.workbench(1.6));
|
||||
atWall(room, K.shelf(1.2));
|
||||
break;
|
||||
|
||||
@@ -1,17 +1,6 @@
|
||||
import type { Prop, Shape, Vec } from '../types';
|
||||
import { Rng } from '../util/rng';
|
||||
|
||||
export const CAR_COLORS = ['#c94c4c', '#3d6fb6', '#e8e8e8', '#2b2b2b', '#8c9298', '#d9c27a', '#4f8a5b', '#b5b9bd', '#6b4a8c', '#e07b39'];
|
||||
|
||||
export function carShapes(): Shape[] {
|
||||
return [
|
||||
{ t: 'rect', x: 0, y: 0, w: 1.8, h: 4.4, r: 0.45, s: 'car' },
|
||||
{ t: 'rect', x: 0, y: -0.75, w: 1.5, h: 0.55, r: 0.15, s: 'glass' }, // лобовое
|
||||
{ t: 'rect', x: 0, y: 1.25, w: 1.45, h: 0.4, r: 0.12, s: 'glass' }, // заднее
|
||||
{ t: 'rect', x: 0, y: 0.25, w: 1.5, h: 1.4, r: 0.2, s: 'dark' }, // крыша
|
||||
];
|
||||
}
|
||||
|
||||
export function treeProp(id: string, p: Vec, rng: Rng, t: Record<string, string>): Prop {
|
||||
const scrub = !!t.scrub;
|
||||
const r0 = scrub ? rng.range(0.8, 1.6) : t.orchard ? rng.range(1.6, 2.4) : rng.range(2.2, 4.2);
|
||||
|
||||
+3
-3
@@ -5,8 +5,8 @@ import { generateWorld } from './gen/pipeline';
|
||||
import { downloadWorld, hasWorld, loadWorld, readJsonFile, saveWorld } from './store';
|
||||
import type { World } from './types';
|
||||
|
||||
const MAX_KM2 = 6;
|
||||
const WARN_KM2 = 2.5;
|
||||
const MAX_KM2 = 25;
|
||||
const WARN_KM2 = 10;
|
||||
|
||||
const app = document.getElementById('app')!;
|
||||
let viewer: Viewer | null = null;
|
||||
@@ -98,7 +98,7 @@ async function showPicker(initial?: BBox) {
|
||||
return;
|
||||
}
|
||||
const s = bboxSizeKm(b);
|
||||
info.textContent = `${s.w.toFixed(2)} × ${s.h.toFixed(2)} км · ${s.km2.toFixed(2)} км²` + (s.km2 > MAX_KM2 ? ' — слишком большая область' : s.km2 > WARN_KM2 ? ' — большая область, генерация займёт время' : '');
|
||||
info.textContent = `${s.w.toFixed(2)} × ${s.h.toFixed(2)} км · ${s.km2.toFixed(2)} км²` + (s.km2 > MAX_KM2 ? ` — лимит ${MAX_KM2} км², уменьшите выделение` : s.km2 > WARN_KM2 ? ' — генерация плотной застройки может занять больше времени' : '');
|
||||
info.className = 'sel-info' + (s.km2 > MAX_KM2 ? ' bad' : s.km2 > WARN_KM2 ? ' warn' : ' ok');
|
||||
goBtn.disabled = s.km2 > MAX_KM2;
|
||||
});
|
||||
|
||||
@@ -148,6 +148,8 @@ export class Renderer {
|
||||
private propLayers(props: Prop[]): Layer[] {
|
||||
const map = new Map<string, Layer>();
|
||||
for (const p of props) {
|
||||
// Старые сохранённые миры также показываем без машин.
|
||||
if (p.kind === 'car') continue;
|
||||
const shapes = this.shapesOf(p);
|
||||
const c = Math.cos(p.rot), s = Math.sin(p.rot);
|
||||
const T = (x: number, y: number): Vec => [p.pos[0] + x * c - y * s, p.pos[1] + x * s + y * c];
|
||||
|
||||
Reference in new issue
Block a user