@devdogsuga/devtools 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/bin/devtools-ci-bare.mjs +48 -0
- package/bin/devtools-ci.mjs +17 -0
- package/bin/devtools.mjs +27 -0
- package/dist/args.d.ts +26 -0
- package/dist/args.js +78 -0
- package/dist/bws/bw.d.ts +9 -0
- package/dist/bws/bw.js +49 -0
- package/dist/bws/client.d.ts +43 -0
- package/dist/bws/client.js +363 -0
- package/dist/bws/environments.d.ts +96 -0
- package/dist/bws/environments.js +111 -0
- package/dist/bws/pace.d.ts +27 -0
- package/dist/bws/pace.js +43 -0
- package/dist/bws/retry.d.ts +12 -0
- package/dist/bws/retry.js +39 -0
- package/dist/bws/store.d.ts +1 -0
- package/dist/bws/store.js +32 -0
- package/dist/bws/token.d.ts +31 -0
- package/dist/bws/token.js +88 -0
- package/dist/bws/vault.d.ts +29 -0
- package/dist/bws/vault.js +200 -0
- package/dist/catalog.d.ts +55 -0
- package/dist/catalog.js +81 -0
- package/dist/cf/commands.d.ts +1 -0
- package/dist/cf/commands.js +182 -0
- package/dist/cf/local-env.d.ts +44 -0
- package/dist/cf/local-env.js +148 -0
- package/dist/ci.d.ts +1 -0
- package/dist/ci.js +319 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +1077 -0
- package/dist/commands.d.ts +255 -0
- package/dist/commands.js +1279 -0
- package/dist/completions.d.ts +4 -0
- package/dist/completions.js +110 -0
- package/dist/cron/commands.d.ts +47 -0
- package/dist/cron/commands.js +372 -0
- package/dist/cron/discovery.d.ts +55 -0
- package/dist/cron/discovery.js +85 -0
- package/dist/cron/schema.d.ts +45 -0
- package/dist/cron/schema.js +35 -0
- package/dist/db/config-push.d.ts +8 -0
- package/dist/db/config-push.js +11 -0
- package/dist/db/connection.d.ts +79 -0
- package/dist/db/connection.js +114 -0
- package/dist/db/exec.d.ts +1 -0
- package/dist/db/exec.js +11 -0
- package/dist/db/generate-migration.d.ts +1 -0
- package/dist/db/generate-migration.js +67 -0
- package/dist/db/generate-types.d.ts +1 -0
- package/dist/db/generate-types.js +4 -0
- package/dist/db/introspect.d.ts +1 -0
- package/dist/db/introspect.js +113 -0
- package/dist/db/local-env.d.ts +65 -0
- package/dist/db/local-env.js +23 -0
- package/dist/db/new-migration.d.ts +1 -0
- package/dist/db/new-migration.js +8 -0
- package/dist/db/run.d.ts +70 -0
- package/dist/db/run.js +158 -0
- package/dist/db/seed-buckets.d.ts +2 -0
- package/dist/db/seed-buckets.js +4 -0
- package/dist/db/seed-production.d.ts +8 -0
- package/dist/db/seed-production.js +73 -0
- package/dist/db/seed-roles.d.ts +7 -0
- package/dist/db/seed-roles.js +31 -0
- package/dist/db/session-refresh.d.ts +43 -0
- package/dist/db/session-refresh.js +40 -0
- package/dist/deploy/migrations.d.ts +20 -0
- package/dist/deploy/migrations.js +68 -0
- package/dist/deploy/orphans.d.ts +20 -0
- package/dist/deploy/orphans.js +177 -0
- package/dist/deploy/preflight.d.ts +53 -0
- package/dist/deploy/preflight.js +189 -0
- package/dist/deploy/report.d.ts +26 -0
- package/dist/deploy/report.js +70 -0
- package/dist/deploy/require-planner.d.ts +2 -0
- package/dist/deploy/require-planner.js +60 -0
- package/dist/deploy/require-token.d.ts +15 -0
- package/dist/deploy/require-token.js +78 -0
- package/dist/deploy/secrets-file.d.ts +17 -0
- package/dist/deploy/secrets-file.js +167 -0
- package/dist/deploy/write-env.d.ts +40 -0
- package/dist/deploy/write-env.js +393 -0
- package/dist/docs/index-pages.d.ts +36 -0
- package/dist/docs/index-pages.js +184 -0
- package/dist/doctor.d.ts +62 -0
- package/dist/doctor.js +225 -0
- package/dist/emails/commands.d.ts +15 -0
- package/dist/emails/commands.js +130 -0
- package/dist/emails/email-types.d.ts +13 -0
- package/dist/emails/email-types.js +1 -0
- package/dist/emails/fixtures.d.ts +20 -0
- package/dist/emails/fixtures.js +20 -0
- package/dist/env/audit.d.ts +206 -0
- package/dist/env/audit.js +436 -0
- package/dist/env/cloudflare.d.ts +21 -0
- package/dist/env/cloudflare.js +61 -0
- package/dist/env/commands.d.ts +99 -0
- package/dist/env/commands.js +612 -0
- package/dist/env/discovery.d.ts +15 -0
- package/dist/env/discovery.js +226 -0
- package/dist/env/document.d.ts +119 -0
- package/dist/env/document.js +323 -0
- package/dist/env/example.d.ts +68 -0
- package/dist/env/example.js +603 -0
- package/dist/env/selection.d.ts +105 -0
- package/dist/env/selection.js +226 -0
- package/dist/environment.d.ts +92 -0
- package/dist/environment.js +206 -0
- package/dist/fingerprint.d.ts +10 -0
- package/dist/fingerprint.js +18 -0
- package/dist/gen/campus-map.d.ts +1 -0
- package/dist/gen/campus-map.js +473 -0
- package/dist/gen/commands.d.ts +1 -0
- package/dist/gen/commands.js +25 -0
- package/dist/gen/hypno.d.ts +1 -0
- package/dist/gen/hypno.js +134 -0
- package/dist/gh/client.d.ts +80 -0
- package/dist/gh/client.js +258 -0
- package/dist/gh/environments.d.ts +83 -0
- package/dist/gh/environments.js +178 -0
- package/dist/gh/rulesets/actors.d.ts +14 -0
- package/dist/gh/rulesets/actors.js +68 -0
- package/dist/gh/rulesets/api.d.ts +15 -0
- package/dist/gh/rulesets/api.js +99 -0
- package/dist/gh/rulesets/commands.d.ts +3 -0
- package/dist/gh/rulesets/commands.js +203 -0
- package/dist/gh/rulesets/desired.d.ts +112 -0
- package/dist/gh/rulesets/desired.js +247 -0
- package/dist/gh/rulesets/diff.d.ts +65 -0
- package/dist/gh/rulesets/diff.js +134 -0
- package/dist/gh/rulesets/types.d.ts +76 -0
- package/dist/gh/rulesets/types.js +13 -0
- package/dist/gh/settings/api.d.ts +40 -0
- package/dist/gh/settings/api.js +172 -0
- package/dist/gh/settings/commands.d.ts +1 -0
- package/dist/gh/settings/commands.js +245 -0
- package/dist/gh/settings/desired.d.ts +53 -0
- package/dist/gh/settings/desired.js +39 -0
- package/dist/gh/settings/diff.d.ts +57 -0
- package/dist/gh/settings/diff.js +156 -0
- package/dist/gh/settings/types.d.ts +92 -0
- package/dist/gh/settings/types.js +11 -0
- package/dist/gh/settings/workflows.d.ts +41 -0
- package/dist/gh/settings/workflows.js +124 -0
- package/dist/grantRoot.d.ts +42 -0
- package/dist/grantRoot.js +89 -0
- package/dist/help.d.ts +18 -0
- package/dist/help.js +169 -0
- package/dist/images/commands.d.ts +35 -0
- package/dist/images/commands.js +233 -0
- package/dist/images/events.d.ts +36 -0
- package/dist/images/events.js +152 -0
- package/dist/images/graphics.d.ts +70 -0
- package/dist/images/graphics.js +233 -0
- package/dist/images/open-graph-types.d.ts +75 -0
- package/dist/images/open-graph-types.js +1 -0
- package/dist/images/prompts.d.ts +34 -0
- package/dist/images/prompts.js +100 -0
- package/dist/images/render.d.ts +21 -0
- package/dist/images/render.js +57 -0
- package/dist/images/select.d.ts +64 -0
- package/dist/images/select.js +96 -0
- package/dist/instance.d.ts +67 -0
- package/dist/instance.js +129 -0
- package/dist/invocation.d.ts +49 -0
- package/dist/invocation.js +78 -0
- package/dist/launch-ci.d.ts +1 -0
- package/dist/launch-ci.js +86 -0
- package/dist/launch.d.ts +29 -0
- package/dist/launch.js +371 -0
- package/dist/menu.d.ts +31 -0
- package/dist/menu.js +277 -0
- package/dist/newsletter/commands.d.ts +16 -0
- package/dist/newsletter/commands.js +311 -0
- package/dist/newsletter/imap.d.ts +17 -0
- package/dist/newsletter/imap.js +127 -0
- package/dist/newsletter/loopback.d.ts +24 -0
- package/dist/newsletter/loopback.js +100 -0
- package/dist/newsletter/oauth.d.ts +26 -0
- package/dist/newsletter/oauth.js +130 -0
- package/dist/newsletter/smtp.d.ts +23 -0
- package/dist/newsletter/smtp.js +133 -0
- package/dist/oauth/browser.d.ts +1 -0
- package/dist/oauth/browser.js +13 -0
- package/dist/oauth/config.d.ts +24 -0
- package/dist/oauth/config.js +24 -0
- package/dist/oauth/connect-url.d.ts +17 -0
- package/dist/oauth/connect-url.js +23 -0
- package/dist/oauth/db.d.ts +37 -0
- package/dist/oauth/db.js +108 -0
- package/dist/oauth/device.d.ts +93 -0
- package/dist/oauth/device.js +190 -0
- package/dist/oauth/discovery.d.ts +28 -0
- package/dist/oauth/discovery.js +54 -0
- package/dist/oauth/env-file.d.ts +6 -0
- package/dist/oauth/env-file.js +35 -0
- package/dist/oauth/exchange.d.ts +43 -0
- package/dist/oauth/exchange.js +87 -0
- package/dist/oauth/label.d.ts +1 -0
- package/dist/oauth/label.js +25 -0
- package/dist/oauth/loopback.d.ts +52 -0
- package/dist/oauth/loopback.js +162 -0
- package/dist/oauth/pkce.d.ts +6 -0
- package/dist/oauth/pkce.js +34 -0
- package/dist/oauth/target.d.ts +54 -0
- package/dist/oauth/target.js +102 -0
- package/dist/oauth/transport.d.ts +41 -0
- package/dist/oauth/transport.js +32 -0
- package/dist/oauth/wizard.d.ts +33 -0
- package/dist/oauth/wizard.js +571 -0
- package/dist/pick.d.ts +6 -0
- package/dist/pick.js +64 -0
- package/dist/planner/checks.d.ts +26 -0
- package/dist/planner/checks.js +104 -0
- package/dist/planner/commands.d.ts +10 -0
- package/dist/planner/commands.js +367 -0
- package/dist/planner/db.d.ts +15 -0
- package/dist/planner/db.js +33 -0
- package/dist/planner/role.d.ts +33 -0
- package/dist/planner/role.js +68 -0
- package/dist/repo/peers.d.ts +24 -0
- package/dist/repo/peers.js +114 -0
- package/dist/repo/preflight.d.ts +109 -0
- package/dist/repo/preflight.js +266 -0
- package/dist/repo/resolve.d.ts +41 -0
- package/dist/repo/resolve.js +152 -0
- package/dist/repo/root.d.ts +23 -0
- package/dist/repo/root.js +91 -0
- package/dist/repo/source.d.ts +14 -0
- package/dist/repo/source.js +54 -0
- package/dist/repo/tsx-loader.d.ts +6 -0
- package/dist/repo/tsx-loader.js +41 -0
- package/dist/run/pick.d.ts +60 -0
- package/dist/run/pick.js +559 -0
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +152 -0
- package/dist/stack.d.ts +48 -0
- package/dist/stack.js +290 -0
- package/dist/telemetry.d.ts +34 -0
- package/dist/telemetry.js +93 -0
- package/dist/tier.d.ts +40 -0
- package/dist/tier.js +93 -0
- package/dist/tsconfig.typecheck.tsbuildinfo +1 -0
- package/dist/ui.d.ts +25 -0
- package/dist/ui.js +51 -0
- package/dist/workers.d.ts +5 -0
- package/dist/workers.js +35 -0
- package/dist/workflows/commands.d.ts +44 -0
- package/dist/workflows/commands.js +668 -0
- package/env.ts +158 -0
- package/package.json +80 -0
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `devtools gen campus-map`
|
|
3
|
+
*
|
|
4
|
+
* Regenerates `apps/platform/src/components/EventsSection/FindUs/campusMapData.ts`
|
|
5
|
+
* (and the few bytes in `campusMapMeta.ts` beside it) from live OpenStreetMap
|
|
6
|
+
* data.
|
|
7
|
+
*
|
|
8
|
+
* The Directions dialog's campus map is real geometry, not a sketch: every
|
|
9
|
+
* building footprint and road in the frame, pulled via Overpass and projected
|
|
10
|
+
* equirectangular into an SVG viewBox. The DLW's own footprint is exported
|
|
11
|
+
* separately so the component can highlight it, along with its centroid in
|
|
12
|
+
* lat/lon, the pin the Google/Apple Maps links drop, since the building is too
|
|
13
|
+
* new for a name search to resolve reliably. Rerun this when OSM improves the
|
|
14
|
+
* area (the DLW is brand new) or to reframe.
|
|
15
|
+
*
|
|
16
|
+
* Street names come out of here already placed, on the centreline they name and
|
|
17
|
+
* turned to match it. Building labels do not: they live in CampusMap.tsx,
|
|
18
|
+
* positioned by eye, because a building's name belongs beside its footprint and
|
|
19
|
+
* which side is free depends on its neighbours. So re-check those against the
|
|
20
|
+
* rendered map after regenerating. The command prints every labelled building's
|
|
21
|
+
* centroid and every street's placement to help.
|
|
22
|
+
*
|
|
23
|
+
* Two output files because the map is loaded on demand. The path strings are the
|
|
24
|
+
* weight, so they live in campusMapData.ts behind a dynamic import; the viewBox
|
|
25
|
+
* and the DLW's coordinates, needed by the eagerly loaded dialog for the
|
|
26
|
+
* placeholder and the map links, go in campusMapMeta.ts.
|
|
27
|
+
*
|
|
28
|
+
* OSM data is ODbL, so the map must keep its visible "© OpenStreetMap"
|
|
29
|
+
* attribution (FindUs.tsx renders it in the SVG corner).
|
|
30
|
+
*/
|
|
31
|
+
import { writeFileSync } from "node:fs";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
import { findRepoRoot } from "../repo/root.js";
|
|
34
|
+
/** Query bbox (S, W, N, E), generous so roads at the frame's edge arrive whole
|
|
35
|
+
* instead of clipped mid-way. */
|
|
36
|
+
const QUERY_BBOX = [33.9355, -83.39, 33.957, -83.363];
|
|
37
|
+
const S = 33.9372, N = 33.9552, W = -83.3861, E = -83.3668;
|
|
38
|
+
const VW = 480;
|
|
39
|
+
const MAJOR_HIGHWAYS = new Set([
|
|
40
|
+
"primary",
|
|
41
|
+
"secondary",
|
|
42
|
+
"tertiary",
|
|
43
|
+
"residential",
|
|
44
|
+
"unclassified",
|
|
45
|
+
]);
|
|
46
|
+
const MINOR_HIGHWAYS = new Set(["service", "pedestrian"]);
|
|
47
|
+
const HIGHLIGHTS = [
|
|
48
|
+
{ key: "DLW", osm: "Dining, Learning and Well-being Center", via: "way" },
|
|
49
|
+
{ key: "Driftmier", osm: "Driftmier Engineering Center", via: "relation" },
|
|
50
|
+
{ key: "Plant Sciences", osm: "Miller Plant Science", via: "way" },
|
|
51
|
+
{ key: "Boyd", osm: "Boyd Graduate Research Center", via: "way" },
|
|
52
|
+
{ key: "MLC", osm: "Zell B. Miller Student Learning Center", via: "way" },
|
|
53
|
+
{
|
|
54
|
+
key: "Science Learning Center",
|
|
55
|
+
osm: "Science Learning Center",
|
|
56
|
+
via: "way",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: "Science Library",
|
|
60
|
+
osm: "Shirley Mathis McBay Science Library",
|
|
61
|
+
via: "way",
|
|
62
|
+
},
|
|
63
|
+
{ key: "Poultry Science", osm: "Poultry Science Building", via: "way" },
|
|
64
|
+
{ key: "Main Library", osm: "UGA Main Library", via: "way" },
|
|
65
|
+
{ key: "Tate", osm: "Tate Student Center", via: "way" },
|
|
66
|
+
];
|
|
67
|
+
const LANDMARKS = [
|
|
68
|
+
"Brumby Hall",
|
|
69
|
+
"Russell Hall",
|
|
70
|
+
"Creswell Hall",
|
|
71
|
+
"Bolton Dining Commons",
|
|
72
|
+
"Tate Center Parking Deck",
|
|
73
|
+
"Oglethorpe House",
|
|
74
|
+
"Physics Building",
|
|
75
|
+
"Snelling Dining Commons",
|
|
76
|
+
"Journalism Building",
|
|
77
|
+
"Sanford Stadium",
|
|
78
|
+
"Stegeman Coliseum",
|
|
79
|
+
"Aderhold Hall",
|
|
80
|
+
"Food Science Building",
|
|
81
|
+
"Paul D. Coverdell Center",
|
|
82
|
+
"Conner Hall",
|
|
83
|
+
"Memorial Hall",
|
|
84
|
+
];
|
|
85
|
+
const STREETS = [
|
|
86
|
+
{ osm: "Baxter Street", text: "Baxter St", at: 0.35 },
|
|
87
|
+
{ osm: "South Lumpkin Street", text: "S. Lumpkin St", at: 0.65 },
|
|
88
|
+
{ osm: "East Campus Road", text: "East Campus Rd", at: 0.5 },
|
|
89
|
+
{ osm: "East Cloverhurst Avenue", text: "E. Cloverhurst Ave", at: 0.35 },
|
|
90
|
+
{ osm: "Sanford Drive", text: "Sanford Dr", at: 0.5 },
|
|
91
|
+
{ osm: "D. W. Brooks Drive", text: "D.W. Brooks Dr", at: 0.35 },
|
|
92
|
+
];
|
|
93
|
+
function makeProjection() {
|
|
94
|
+
const latMid = (S + N) / 2;
|
|
95
|
+
const kx = Math.cos((latMid * Math.PI) / 180);
|
|
96
|
+
const wDeg = (E - W) * kx;
|
|
97
|
+
const hDeg = N - S;
|
|
98
|
+
const VH = Math.round((VW * hDeg) / wDeg);
|
|
99
|
+
const px = (lon, lat) => [
|
|
100
|
+
(((lon - W) * kx) / wDeg) * VW,
|
|
101
|
+
((N - lat) / hDeg) * VH,
|
|
102
|
+
];
|
|
103
|
+
const fmt = (n) => Math.round(n * 10) / 10;
|
|
104
|
+
return { VH, px, fmt };
|
|
105
|
+
}
|
|
106
|
+
function simplify(pts) {
|
|
107
|
+
const kept = [pts[0]];
|
|
108
|
+
for (const p of pts.slice(1)) {
|
|
109
|
+
const [lx, ly] = kept[kept.length - 1];
|
|
110
|
+
if (Math.hypot(p[0] - lx, p[1] - ly) > 2.5)
|
|
111
|
+
kept.push(p);
|
|
112
|
+
}
|
|
113
|
+
const last = pts[pts.length - 1];
|
|
114
|
+
const tail = kept[kept.length - 1];
|
|
115
|
+
if (tail[0] !== last[0] || tail[1] !== last[1])
|
|
116
|
+
kept.push(last);
|
|
117
|
+
return kept;
|
|
118
|
+
}
|
|
119
|
+
function makeGeomHelpers(px, fmt, VH) {
|
|
120
|
+
const proj = (geom) => geom.map((g) => px(g.lon, g.lat).map(fmt));
|
|
121
|
+
const toPath = (geom, close) => "M" +
|
|
122
|
+
simplify(proj(geom))
|
|
123
|
+
.map((p) => p.join(" "))
|
|
124
|
+
.join("L") +
|
|
125
|
+
(close ? "Z" : "");
|
|
126
|
+
const inFrame = (geom) => geom.some((g) => {
|
|
127
|
+
const [x, y] = px(g.lon, g.lat);
|
|
128
|
+
return x > -8 && x < VW + 8 && y > -8 && y < VH + 8;
|
|
129
|
+
});
|
|
130
|
+
const area = (geom) => {
|
|
131
|
+
const pts = proj(geom);
|
|
132
|
+
let a = 0;
|
|
133
|
+
for (let i = 0, j = pts.length - 1; i < pts.length; j = i++) {
|
|
134
|
+
a += (pts[j][0] + pts[i][0]) * (pts[j][1] - pts[i][1]);
|
|
135
|
+
}
|
|
136
|
+
return Math.abs(a / 2);
|
|
137
|
+
};
|
|
138
|
+
return { proj, toPath, inFrame, area };
|
|
139
|
+
}
|
|
140
|
+
const lineLength = (line) => line
|
|
141
|
+
.slice(1)
|
|
142
|
+
.reduce((a, p, i) => a + Math.hypot(p[0] - line[i][0], p[1] - line[i][1]), 0);
|
|
143
|
+
function chain(lines) {
|
|
144
|
+
const key = (p) => `${p[0]},${p[1]}`;
|
|
145
|
+
const pool = lines.filter((l) => l.length > 1);
|
|
146
|
+
const runs = [];
|
|
147
|
+
while (pool.length > 0) {
|
|
148
|
+
const run = pool.pop();
|
|
149
|
+
for (let grew = true; grew;) {
|
|
150
|
+
grew = false;
|
|
151
|
+
for (let i = 0; i < pool.length; i++) {
|
|
152
|
+
const c = pool[i];
|
|
153
|
+
const head = key(run[0]);
|
|
154
|
+
const tail = key(run[run.length - 1]);
|
|
155
|
+
if (tail === key(c[0]))
|
|
156
|
+
run.push(...c.slice(1));
|
|
157
|
+
else if (tail === key(c[c.length - 1]))
|
|
158
|
+
run.push(...c.slice(0, -1).reverse());
|
|
159
|
+
else if (head === key(c[c.length - 1]))
|
|
160
|
+
run.unshift(...c.slice(0, -1));
|
|
161
|
+
else if (head === key(c[0]))
|
|
162
|
+
run.unshift(...c.slice(1).reverse());
|
|
163
|
+
else
|
|
164
|
+
continue;
|
|
165
|
+
pool.splice(i, 1);
|
|
166
|
+
grew = true;
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
runs.push(run);
|
|
171
|
+
}
|
|
172
|
+
return runs;
|
|
173
|
+
}
|
|
174
|
+
function clipToFrame(line, VH, margin = 8) {
|
|
175
|
+
const [minX, minY] = [margin, margin];
|
|
176
|
+
const [maxX, maxY] = [VW - margin, VH - margin];
|
|
177
|
+
const runs = [];
|
|
178
|
+
let run = [];
|
|
179
|
+
const flush = () => {
|
|
180
|
+
if (run.length > 1)
|
|
181
|
+
runs.push(run);
|
|
182
|
+
run = [];
|
|
183
|
+
};
|
|
184
|
+
for (let i = 1; i < line.length; i++) {
|
|
185
|
+
const a = line[i - 1];
|
|
186
|
+
const b = line[i];
|
|
187
|
+
const [dx, dy] = [b[0] - a[0], b[1] - a[1]];
|
|
188
|
+
let t0 = 0, t1 = 1, hit = true;
|
|
189
|
+
for (const [p, q] of [
|
|
190
|
+
[-dx, a[0] - minX],
|
|
191
|
+
[dx, maxX - a[0]],
|
|
192
|
+
[-dy, a[1] - minY],
|
|
193
|
+
[dy, maxY - a[1]],
|
|
194
|
+
]) {
|
|
195
|
+
if (p === 0) {
|
|
196
|
+
if (q < 0)
|
|
197
|
+
hit = false;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const r = q / p;
|
|
201
|
+
if (p < 0 ? r > t1 : r < t0)
|
|
202
|
+
hit = false;
|
|
203
|
+
else if (p < 0)
|
|
204
|
+
t0 = Math.max(t0, r);
|
|
205
|
+
else
|
|
206
|
+
t1 = Math.min(t1, r);
|
|
207
|
+
if (!hit)
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
if (!hit) {
|
|
211
|
+
flush();
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const at = (t) => [a[0] + dx * t, a[1] + dy * t];
|
|
215
|
+
if (t0 > 0)
|
|
216
|
+
flush();
|
|
217
|
+
if (run.length === 0)
|
|
218
|
+
run.push(at(t0));
|
|
219
|
+
run.push(at(t1));
|
|
220
|
+
if (t1 < 1)
|
|
221
|
+
flush();
|
|
222
|
+
}
|
|
223
|
+
flush();
|
|
224
|
+
return runs;
|
|
225
|
+
}
|
|
226
|
+
function placeAlong(run, frac, span, fmt) {
|
|
227
|
+
const seg = run
|
|
228
|
+
.slice(1)
|
|
229
|
+
.map((p, i) => Math.hypot(p[0] - run[i][0], p[1] - run[i][1]));
|
|
230
|
+
const total = seg.reduce((a, b) => a + b, 0);
|
|
231
|
+
const at = (dist) => {
|
|
232
|
+
let d = Math.max(0, Math.min(total, dist));
|
|
233
|
+
for (let i = 0; i < seg.length; i++) {
|
|
234
|
+
if (d <= seg[i]) {
|
|
235
|
+
const t = seg[i] === 0 ? 0 : d / seg[i];
|
|
236
|
+
return [
|
|
237
|
+
run[i][0] + (run[i + 1][0] - run[i][0]) * t,
|
|
238
|
+
run[i][1] + (run[i + 1][1] - run[i][1]) * t,
|
|
239
|
+
];
|
|
240
|
+
}
|
|
241
|
+
d -= seg[i];
|
|
242
|
+
}
|
|
243
|
+
return run[run.length - 1];
|
|
244
|
+
};
|
|
245
|
+
const mid = total * frac;
|
|
246
|
+
const half = Math.min(span, total) / 2;
|
|
247
|
+
const [x, y] = at(mid);
|
|
248
|
+
const [ax, ay] = at(mid - half);
|
|
249
|
+
const [bx, by] = at(mid + half);
|
|
250
|
+
let angle = (Math.atan2(by - ay, bx - ax) * 180) / Math.PI;
|
|
251
|
+
if (angle > 90)
|
|
252
|
+
angle -= 180;
|
|
253
|
+
if (angle < -90)
|
|
254
|
+
angle += 180;
|
|
255
|
+
return { x: fmt(x), y: fmt(y), angle: fmt(angle) };
|
|
256
|
+
}
|
|
257
|
+
async function overpass(query) {
|
|
258
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
259
|
+
if (attempt > 0)
|
|
260
|
+
await new Promise((r) => setTimeout(r, 8000 * attempt));
|
|
261
|
+
const res = await fetch("https://overpass-api.de/api/interpreter", {
|
|
262
|
+
method: "POST",
|
|
263
|
+
headers: {
|
|
264
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
265
|
+
"User-Agent": "devdogs-platform-campus-map (devdogsuga.org)",
|
|
266
|
+
},
|
|
267
|
+
body: "data=" + encodeURIComponent(query),
|
|
268
|
+
});
|
|
269
|
+
const text = await res.text();
|
|
270
|
+
if (!res.ok || text.startsWith("<")) {
|
|
271
|
+
console.warn(` overpass attempt ${attempt + 1} failed (${res.status})${text.startsWith("<") ? " — XML error body" : ""}`);
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
return JSON.parse(text).elements;
|
|
275
|
+
}
|
|
276
|
+
throw new Error("Overpass did not answer after 4 attempts");
|
|
277
|
+
}
|
|
278
|
+
async function fetchOsm() {
|
|
279
|
+
const bbox = QUERY_BBOX.join(",");
|
|
280
|
+
const ways = await overpass(`[out:json][timeout:90];
|
|
281
|
+
(
|
|
282
|
+
way["highway"~"${[...MAJOR_HIGHWAYS, ...MINOR_HIGHWAYS].join("|")}"](${bbox});
|
|
283
|
+
way["building"](${bbox});
|
|
284
|
+
way["leisure"="stadium"](${bbox});
|
|
285
|
+
);
|
|
286
|
+
out geom tags;`);
|
|
287
|
+
const relWays = await overpass(`[out:json][timeout:90];
|
|
288
|
+
relation["building"](${bbox});
|
|
289
|
+
way(r);
|
|
290
|
+
out geom;`);
|
|
291
|
+
const seen = new Set(ways.map((w) => w.id));
|
|
292
|
+
for (const w of relWays) {
|
|
293
|
+
if (seen.has(w.id))
|
|
294
|
+
continue;
|
|
295
|
+
seen.add(w.id);
|
|
296
|
+
ways.push({ ...w, tags: { ...w.tags, building: "yes" } });
|
|
297
|
+
}
|
|
298
|
+
return ways;
|
|
299
|
+
}
|
|
300
|
+
async function fetchRelationWays(name) {
|
|
301
|
+
return overpass(`[out:json][timeout:90];
|
|
302
|
+
relation["building"]["name"="${name}"](${QUERY_BBOX.join(",")});
|
|
303
|
+
way(r);
|
|
304
|
+
out geom;`);
|
|
305
|
+
}
|
|
306
|
+
function centroid(geom) {
|
|
307
|
+
let a = 0, cx = 0, cy = 0;
|
|
308
|
+
for (let i = 0, j = geom.length - 1; i < geom.length; j = i++) {
|
|
309
|
+
const p = geom[j], q = geom[i];
|
|
310
|
+
const f = p.lon * q.lat - q.lon * p.lat;
|
|
311
|
+
a += f;
|
|
312
|
+
cx += (p.lon + q.lon) * f;
|
|
313
|
+
cy += (p.lat + q.lat) * f;
|
|
314
|
+
}
|
|
315
|
+
const round = (n) => Math.round(n * 1e6) / 1e6;
|
|
316
|
+
return { lat: round(cy / (3 * a)), lon: round(cx / (3 * a)) };
|
|
317
|
+
}
|
|
318
|
+
export async function runGenCampusMap() {
|
|
319
|
+
const { VH, px, fmt } = makeProjection();
|
|
320
|
+
const { proj, toPath, inFrame, area } = makeGeomHelpers(px, fmt, VH);
|
|
321
|
+
console.log("fetching OSM…");
|
|
322
|
+
const elements = await fetchOsm();
|
|
323
|
+
const roadPaths = { major: [], minor: [] };
|
|
324
|
+
const namedRoads = new Map();
|
|
325
|
+
for (const e of elements) {
|
|
326
|
+
const hw = e.tags?.highway;
|
|
327
|
+
if (!hw || !inFrame(e.geometry))
|
|
328
|
+
continue;
|
|
329
|
+
if (e.tags?.area === "yes")
|
|
330
|
+
continue;
|
|
331
|
+
if (MAJOR_HIGHWAYS.has(hw))
|
|
332
|
+
roadPaths.major.push(toPath(e.geometry, false));
|
|
333
|
+
else if (MINOR_HIGHWAYS.has(hw))
|
|
334
|
+
roadPaths.minor.push(toPath(e.geometry, false));
|
|
335
|
+
const name = e.tags?.name;
|
|
336
|
+
if (name === undefined)
|
|
337
|
+
continue;
|
|
338
|
+
const parts = namedRoads.get(name) ?? [];
|
|
339
|
+
parts.push(proj(e.geometry));
|
|
340
|
+
namedRoads.set(name, parts);
|
|
341
|
+
}
|
|
342
|
+
const roadLabels = STREETS.map((street) => {
|
|
343
|
+
const parts = namedRoads.get(street.osm);
|
|
344
|
+
if (parts === undefined)
|
|
345
|
+
throw new Error(`no OSM way named "${street.osm}" reaches the frame`);
|
|
346
|
+
const runs = chain(parts).flatMap((line) => clipToFrame(line, VH));
|
|
347
|
+
if (runs.length === 0)
|
|
348
|
+
throw new Error(`"${street.osm}" has no centreline inside the frame`);
|
|
349
|
+
const longest = runs.reduce((a, r) => lineLength(r) > lineLength(a) ? r : a);
|
|
350
|
+
const width = street.text.length * 3.6;
|
|
351
|
+
if (lineLength(longest) < width)
|
|
352
|
+
console.warn(` ! "${street.text}" is wider than its longest visible run (${fmt(lineLength(longest))}px) — it will overhang both ends`);
|
|
353
|
+
return {
|
|
354
|
+
text: street.text,
|
|
355
|
+
...placeAlong(longest, street.at, width, fmt),
|
|
356
|
+
run: fmt(lineLength(longest)),
|
|
357
|
+
};
|
|
358
|
+
});
|
|
359
|
+
const footprints = [];
|
|
360
|
+
for (const e of elements) {
|
|
361
|
+
if (!(e.tags?.building ?? e.tags?.leisure === "stadium"))
|
|
362
|
+
continue;
|
|
363
|
+
if (!inFrame(e.geometry) || area(e.geometry) < 12)
|
|
364
|
+
continue;
|
|
365
|
+
footprints.push(toPath(e.geometry, true));
|
|
366
|
+
}
|
|
367
|
+
const highlights = {};
|
|
368
|
+
for (const b of HIGHLIGHTS) {
|
|
369
|
+
const geoms = b.via === "relation"
|
|
370
|
+
? (await fetchRelationWays(b.osm)).map((w) => w.geometry)
|
|
371
|
+
: elements
|
|
372
|
+
.filter((e) => (e.tags?.building ?? e.tags?.leisure === "stadium") &&
|
|
373
|
+
e.tags?.name === b.osm)
|
|
374
|
+
.map((e) => e.geometry);
|
|
375
|
+
if (geoms.length === 0)
|
|
376
|
+
throw new Error(`no OSM footprint for "${b.osm}"`);
|
|
377
|
+
const biggest = geoms.reduce((a, g) => (area(g) > area(a) ? g : a));
|
|
378
|
+
const center = centroid(biggest);
|
|
379
|
+
const [pinX] = px(center.lon, center.lat).map(fmt);
|
|
380
|
+
const rings = geoms.map((g) => simplify(proj(g)));
|
|
381
|
+
const ys = rings.flat().map((pt) => pt[1]);
|
|
382
|
+
const crossings = rings.flatMap((ring) => {
|
|
383
|
+
const hits = [];
|
|
384
|
+
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
385
|
+
const [ax, ay] = ring[j];
|
|
386
|
+
const [bx, by] = ring[i];
|
|
387
|
+
if (ax === bx)
|
|
388
|
+
continue;
|
|
389
|
+
if (pinX < Math.min(ax, bx) || pinX >= Math.max(ax, bx))
|
|
390
|
+
continue;
|
|
391
|
+
hits.push(ay + ((pinX - ax) / (bx - ax)) * (by - ay));
|
|
392
|
+
}
|
|
393
|
+
return hits;
|
|
394
|
+
});
|
|
395
|
+
const spans = crossings.length > 0 ? crossings : ys;
|
|
396
|
+
highlights[b.key] = {
|
|
397
|
+
path: geoms.map((g) => toPath(g, true)).join(""),
|
|
398
|
+
center,
|
|
399
|
+
pin: {
|
|
400
|
+
x: pinX,
|
|
401
|
+
top: fmt(Math.min(...ys)),
|
|
402
|
+
bottom: fmt(Math.max(...ys)),
|
|
403
|
+
tipTop: fmt(Math.min(...spans)),
|
|
404
|
+
tipBottom: fmt(Math.max(...spans)),
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const labeled = [...HIGHLIGHTS.map((b) => b.osm), ...LANDMARKS].map((name) => {
|
|
409
|
+
const b = elements.find((e) => (e.tags?.building ?? e.tags?.leisure === "stadium") &&
|
|
410
|
+
e.tags?.name === name);
|
|
411
|
+
if (!b) {
|
|
412
|
+
const h = HIGHLIGHTS.find((x) => x.osm === name);
|
|
413
|
+
const pin = h ? highlights[h.key]?.pin : undefined;
|
|
414
|
+
if (!pin)
|
|
415
|
+
throw new Error(`no OSM building found for "${name}"`);
|
|
416
|
+
return { name, cx: pin.x, cy: fmt((pin.top + pin.bottom) / 2) };
|
|
417
|
+
}
|
|
418
|
+
const c = b.geometry.reduce((a, g) => [a[0] + g.lon, a[1] + g.lat], [0, 0]);
|
|
419
|
+
const [cx, cy] = px(c[0] / b.geometry.length, c[1] / b.geometry.length).map(fmt);
|
|
420
|
+
return { name, cx, cy };
|
|
421
|
+
});
|
|
422
|
+
const header = `// GENERATED by devtools gen campus-map. Do not edit by hand.\n// Map data © OpenStreetMap contributors, ODbL: openstreetmap.org/copyright\n// Frame: lat ${S}..${N}, lon ${W}..${E}, equirectangular, ${VW}x${VH}.\n`;
|
|
423
|
+
const keys = HIGHLIGHTS.map((b) => b.key);
|
|
424
|
+
const entries = (pick) => JSON.stringify(Object.fromEntries(keys.map((k) => [k, pick(highlights[k])])), null, 2);
|
|
425
|
+
const meta = `${header}
|
|
426
|
+
/** The map's viewBox, also what sizes its placeholder before it loads. */
|
|
427
|
+
export const VIEW = { w: ${VW}, h: ${VH} };
|
|
428
|
+
|
|
429
|
+
/** Every building a meeting can name. The map highlights exactly one of them. */
|
|
430
|
+
export const BUILDING_KEYS = ${JSON.stringify(keys)} as const;
|
|
431
|
+
|
|
432
|
+
export type BuildingKey = (typeof BUILDING_KEYS)[number];
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Each building's footprint centroid: where the Google/Apple Maps links drop
|
|
436
|
+
* their pin.
|
|
437
|
+
*/
|
|
438
|
+
export const BUILDING_CENTERS: Record<BuildingKey, { lat: number; lon: number }> = ${entries((h) => h.center)};
|
|
439
|
+
`;
|
|
440
|
+
const out = `${header}
|
|
441
|
+
/** Cased streets, one subpath per OSM way. */
|
|
442
|
+
export const MAJOR_ROADS = ${JSON.stringify(roadPaths.major.join(""))};
|
|
443
|
+
|
|
444
|
+
/** Service drives and pedestrian connectors, drawn thin. */
|
|
445
|
+
export const MINOR_ROADS = ${JSON.stringify(roadPaths.minor.join(""))};
|
|
446
|
+
|
|
447
|
+
/** Every building footprint in the frame, highlightable ones included. */
|
|
448
|
+
export const FOOTPRINTS = ${JSON.stringify(footprints.join(""))};
|
|
449
|
+
|
|
450
|
+
/** The footprint the map paints on top, one per building a meeting can name. */
|
|
451
|
+
export const HIGHLIGHT_PATHS: Record<string, string> = ${entries((h) => h.path)};
|
|
452
|
+
|
|
453
|
+
export const HIGHLIGHT_PINS: Record<string, { x: number; top: number; bottom: number; tipTop: number; tipBottom: number }> = ${entries((h) => h.pin)};
|
|
454
|
+
|
|
455
|
+
/** Street names, on the centreline they name and turned to match it. */
|
|
456
|
+
export const ROAD_LABELS: { text: string; x: number; y: number; angle: number }[] = ${JSON.stringify(roadLabels.map(({ text, x, y, angle }) => ({ text, x, y, angle })), null, 2)};
|
|
457
|
+
`;
|
|
458
|
+
const dir = join(findRepoRoot(), "apps", "platform", "src", "components", "EventsSection", "FindUs");
|
|
459
|
+
writeFileSync(join(dir, "campusMapData.ts"), out);
|
|
460
|
+
writeFileSync(join(dir, "campusMapMeta.ts"), meta);
|
|
461
|
+
console.log(`wrote ${dir}/campusMapData.ts (${(out.length / 1024).toFixed(1)}KB) + campusMapMeta.ts`);
|
|
462
|
+
console.log("run prettier --write on both before committing them");
|
|
463
|
+
console.log(`viewBox 0 0 ${VW} ${VH} | ${roadPaths.major.length} major ways, ${roadPaths.minor.length} minor ways, ${footprints.length} footprints`);
|
|
464
|
+
for (const b of labeled)
|
|
465
|
+
console.log(` LABEL ${b.name} → ${b.cx},${b.cy}`);
|
|
466
|
+
for (const k of keys) {
|
|
467
|
+
const { x, top, bottom, tipTop, tipBottom } = highlights[k].pin;
|
|
468
|
+
console.log(` PIN ${k} → x ${x}, y ${top}..${bottom}, tip ${tipTop}..${tipBottom}`);
|
|
469
|
+
}
|
|
470
|
+
for (const r of roadLabels)
|
|
471
|
+
console.log(` ROAD ${r.text} → ${r.x},${r.y} @ ${r.angle}° (${r.run}px visible)`);
|
|
472
|
+
return 0;
|
|
473
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runGen(argv: readonly string[]): Promise<number>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatch for `devtools gen *`.
|
|
3
|
+
*
|
|
4
|
+
* `gen og-assets` and `gen email-templates` delegate to the owning package
|
|
5
|
+
* scripts via pnpm. `gen campus-map` and `gen hypno` are devtools-owned.
|
|
6
|
+
*/
|
|
7
|
+
import { run } from "../db/run.js";
|
|
8
|
+
import { runGenCampusMap } from "./campus-map.js";
|
|
9
|
+
import { runGenHypno } from "./hypno.js";
|
|
10
|
+
export async function runGen(argv) {
|
|
11
|
+
const sub = argv[0];
|
|
12
|
+
if (sub === "campus-map")
|
|
13
|
+
return runGenCampusMap();
|
|
14
|
+
if (sub === "hypno")
|
|
15
|
+
return runGenHypno();
|
|
16
|
+
if (sub === "og-assets") {
|
|
17
|
+
return run(["--filter", "@devdogsuga/open-graph", "run", "generate"]);
|
|
18
|
+
}
|
|
19
|
+
if (sub === "email-templates") {
|
|
20
|
+
return run(["--filter", "@devdogsuga/email", "run", "compile"]);
|
|
21
|
+
}
|
|
22
|
+
process.stderr.write(`devtools gen: unknown subcommand "${sub ?? "(none)"}". ` +
|
|
23
|
+
"Expected: campus-map, hypno, og-assets, email-templates.\n");
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runGenHypno(): Promise<number>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* `devtools gen hypno`
|
|
4
|
+
*
|
|
5
|
+
* Bakes the hero's spiral into a pre-blurred raster at
|
|
6
|
+
* `apps/platform/src/assets/hypno.webp`.
|
|
7
|
+
*
|
|
8
|
+
* Why this exists: the spiral used to be an inline 24-path SVG, ~3,530 CSS px
|
|
9
|
+
* square, rotating forever under a `blur-sm` filter. Measured on a production
|
|
10
|
+
* build, that single element held the page to 36.8 FPS at idle; hiding it
|
|
11
|
+
* restored 60.4. The cost is the live filter, not the geometry — shrinking the
|
|
12
|
+
* surface 6x bought 3 FPS, removing the blur at full size bought 25. Rotating a
|
|
13
|
+
* texture that already has the blur painted into it is free by comparison, and
|
|
14
|
+
* it keeps both the motion and the exact hazy look.
|
|
15
|
+
*
|
|
16
|
+
* Two details make the bake match the original:
|
|
17
|
+
*
|
|
18
|
+
* 1. The source sets `vector-effect: non-scaling-stroke`, so its 2px stroke is
|
|
19
|
+
* 2 *device* px no matter how far the SVG is scaled up. A raster does not
|
|
20
|
+
* work that way — its stroke scales with the image. So the stroke is
|
|
21
|
+
* converted into viewBox units for the size the element actually renders at
|
|
22
|
+
* (2 * 1926.25 / RENDER_PX), which reproduces the same apparent weight.
|
|
23
|
+
* 2. At the raster sizes worth shipping, both the stroke and the blur land
|
|
24
|
+
* below one pixel, and Skia's box-approximated blur is inaccurate at
|
|
25
|
+
* sub-pixel sigma. Everything is therefore drawn at SS x the target and
|
|
26
|
+
* downsampled, which resolves both correctly.
|
|
27
|
+
*/
|
|
28
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
import { createRequire } from "node:module";
|
|
31
|
+
import { findRepoRoot } from "../repo/root.js";
|
|
32
|
+
// Lazy — computed inside `runGenHypno`/`loadChromium`, not at module load.
|
|
33
|
+
// This file used to compute these as top-level `const`s, which called
|
|
34
|
+
// `findRepoRoot()` (and threw `RepoNotFoundError`) the instant anything
|
|
35
|
+
// imported this module — including `devtools --help` from outside a repo,
|
|
36
|
+
// since the command tree pulls every command module in transitively. See
|
|
37
|
+
// `workers.ts`'s header for the general pattern.
|
|
38
|
+
function srcPath() {
|
|
39
|
+
return join(findRepoRoot(), "apps", "platform", "src", "components", "HeroSection", "Hypno.tsx");
|
|
40
|
+
}
|
|
41
|
+
function outPath() {
|
|
42
|
+
return join(findRepoRoot(), "apps", "platform", "src", "assets", "hypno.webp");
|
|
43
|
+
}
|
|
44
|
+
const RENDER_PX = 3536;
|
|
45
|
+
const VIEWBOX = 1926.25;
|
|
46
|
+
const BLUR_CSS_PX = 8;
|
|
47
|
+
const SS = 3;
|
|
48
|
+
const STROKE = "rgba(216,180,254,0.75)";
|
|
49
|
+
const BUDGET = 60_000;
|
|
50
|
+
async function loadChromium() {
|
|
51
|
+
const require_ = createRequire(import.meta.url);
|
|
52
|
+
for (const spec of [
|
|
53
|
+
"playwright",
|
|
54
|
+
"playwright-core",
|
|
55
|
+
join(findRepoRoot(), "node_modules/.pnpm/playwright@1.62.1/node_modules/playwright"),
|
|
56
|
+
]) {
|
|
57
|
+
try {
|
|
58
|
+
const mod = await import(require_.resolve(spec));
|
|
59
|
+
const chromium = mod.chromium ?? mod.default?.chromium;
|
|
60
|
+
if (chromium)
|
|
61
|
+
return chromium;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* try the next one */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
throw new Error("playwright not resolvable. Run `pnpm dlx playwright@1.62 install chromium` " +
|
|
68
|
+
"or run this command from a workspace where playwright is installed.");
|
|
69
|
+
}
|
|
70
|
+
export async function runGenHypno() {
|
|
71
|
+
const src = readFileSync(srcPath(), "utf8");
|
|
72
|
+
const paths = [...src.matchAll(/<path d="([^"]+)"/g)].map((m) => m[1]);
|
|
73
|
+
if (paths.length !== 24) {
|
|
74
|
+
process.stderr.write(`expected 24 paths in Hypno.tsx, found ${paths.length}\n`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
const strokeW = (2 * VIEWBOX) / RENDER_PX;
|
|
78
|
+
const svg = (px) => `<svg xmlns="http://www.w3.org/2000/svg" width="${px}" height="${px}"
|
|
79
|
+
viewBox="0 0 ${VIEWBOX} ${VIEWBOX}">
|
|
80
|
+
<g fill="none" stroke="${STROKE}" stroke-width="${strokeW}" stroke-linecap="round" stroke-linejoin="round">
|
|
81
|
+
${paths.map((d) => `<path d="${d}"/>`).join("\n")}
|
|
82
|
+
</g></svg>`;
|
|
83
|
+
const chromium = await loadChromium();
|
|
84
|
+
const browser = await chromium.launch();
|
|
85
|
+
const page = await browser.newPage();
|
|
86
|
+
await page.goto("data:text/html,<html><body></body></html>");
|
|
87
|
+
const bake = async (target, quality) => page.evaluate(async ({ markup, target, ss, blur, quality, }) => {
|
|
88
|
+
const big = target * ss;
|
|
89
|
+
const img = new Image();
|
|
90
|
+
img.src =
|
|
91
|
+
"data:image/svg+xml;charset=utf-8," + encodeURIComponent(markup);
|
|
92
|
+
await new Promise((r) => {
|
|
93
|
+
img.onload = r;
|
|
94
|
+
img.onerror = r;
|
|
95
|
+
});
|
|
96
|
+
const hi = document.createElement("canvas");
|
|
97
|
+
hi.width = hi.height = big;
|
|
98
|
+
const hx = hi.getContext("2d");
|
|
99
|
+
hx.filter = `blur(${blur * ss}px)`;
|
|
100
|
+
hx.drawImage(img, 0, 0, big, big);
|
|
101
|
+
const lo = document.createElement("canvas");
|
|
102
|
+
lo.width = lo.height = target;
|
|
103
|
+
const lx = lo.getContext("2d");
|
|
104
|
+
lx.imageSmoothingEnabled = true;
|
|
105
|
+
lx.imageSmoothingQuality = "high";
|
|
106
|
+
lx.drawImage(hi, 0, 0, target, target);
|
|
107
|
+
return lo.toDataURL("image/webp", quality);
|
|
108
|
+
}, {
|
|
109
|
+
markup: svg(target * SS),
|
|
110
|
+
target,
|
|
111
|
+
ss: SS,
|
|
112
|
+
blur: (BLUR_CSS_PX * target) / RENDER_PX,
|
|
113
|
+
quality,
|
|
114
|
+
});
|
|
115
|
+
const bytes = (dataUrl) => Buffer.from(dataUrl.split(",")[1], "base64").byteLength;
|
|
116
|
+
console.log("size quality bytes");
|
|
117
|
+
const results = [];
|
|
118
|
+
for (const target of [512, 768, 1024]) {
|
|
119
|
+
for (const q of [0.5, 0.65, 0.8]) {
|
|
120
|
+
const url = await bake(target, q);
|
|
121
|
+
const b = bytes(url);
|
|
122
|
+
results.push({ target, q, b, url });
|
|
123
|
+
console.log(`${String(target).padStart(4)} ${q.toFixed(2)} ${String(b).padStart(7)}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const pick = results
|
|
127
|
+
.filter((r) => r.b <= BUDGET)
|
|
128
|
+
.sort((a, b) => b.target - a.target || b.q - a.q)[0] ??
|
|
129
|
+
results.sort((a, b) => a.b - b.b)[0];
|
|
130
|
+
writeFileSync(outPath(), Buffer.from(pick.url.split(",")[1], "base64"));
|
|
131
|
+
console.log(`\nwrote ${outPath()}\n ${pick.target}px @ q=${pick.q} -> ${pick.b} bytes (budget ${BUDGET})`);
|
|
132
|
+
await browser.close();
|
|
133
|
+
return 0;
|
|
134
|
+
}
|