@a2f0/skyline 0.2.59 → 0.2.61

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/docs/package.md CHANGED
@@ -255,7 +255,7 @@ the option out.
255
255
 
256
256
  ### How the viewer's code loads
257
257
 
258
- The package's module (`lib/`) is a small loader that a host bundles (under 4 KB). When
258
+ The package's module (`lib/`) is a small loader that a host bundles (under 4.5 KB). When
259
259
  it mounts, it imports the viewer's code from the assets (`skyline-viewer.js`) with a
260
260
  dynamic `import()` that carries `webpackIgnore`, `turbopackIgnore` and `@vite-ignore`
261
261
  comments, so webpack (including Next.js with `--webpack`), Turbopack and Vite leave it
@@ -263,13 +263,17 @@ to the browser; Rollup, esbuild and Bun leave an import of a variable alone anyw
263
263
  bundler that tried to resolve it would fail to build: tell it to ignore that import.
264
264
  The viewer then loads its stylesheets, the scene's markup from `skyline-3d.html`, its
265
265
  stars from `stars.svg`, and the scene's modules and models from the same directory.
266
- Each module loads once per page, however many viewers mount.
266
+ Each module loads once per page, however many viewers mount. So that those modules
267
+ arrive together rather than one import at a time, the loader also reads
268
+ `viewer-modules.json`, which the build writes, and adds a `modulepreload` link for each
269
+ to the document's head, leaving out the vendored engine when the host shares its own;
270
+ the links leave the head once the viewer is ready, fails, or is destroyed.
267
271
 
268
272
  ### Content security policy and other origins
269
273
 
270
274
  The viewer runs in the host's page, under the host's content security policy. A host
271
275
  with one must allow the assets' origin (usually `'self'`) in `script-src`,
272
- `style-src` and `connect-src` (the viewer fetches its markup and stars), and inline
276
+ `style-src` and `connect-src` (the viewer fetches its markup, stars and module list), and inline
273
277
  styles in `style-src`, which the viewer sets on its elements. Serve the assets from
274
278
  the host's origin when you can; on another origin, every file must allow the page's
275
279
  origin through CORS. The viewer's code runs with the host page's privileges wherever
@@ -44,3 +44,13 @@ export interface SkylineColour {
44
44
  refresh(): void;
45
45
  }
46
46
  export declare function createSkylineColour(models: readonly BuildingModel[]): SkylineColour;
47
+ /** A colour layer that is made on first use; see `deferSkylineColour`. */
48
+ export interface DeferredSkylineColour extends SkylineColour {
49
+ /**
50
+ * Makes the layer now, if it isn't made yet. Call it before anything rewrites the buildings'
51
+ * colours, such as a celebration's window illumination: the layer reads their greys once,
52
+ * when it is made.
53
+ */
54
+ prepare(): void;
55
+ }
56
+ export declare function deferSkylineColour(models: readonly BuildingModel[]): DeferredSkylineColour;
@@ -506,3 +506,43 @@ export function createSkylineColour(models) {
506
506
  },
507
507
  };
508
508
  }
509
+ // Making the layer reads every vertex of every building, a large share of the scene's start-up
510
+ // time, and changes nothing on screen until colour, a previewed time or a celebration needs it:
511
+ // its facade shaders install only then (colour-reflections.ts). So this one is made on first
512
+ // use, or by `prepare`, from the same buildings and with the sky and night it was given
513
+ // meanwhile, and acts from then on exactly as one made at once.
514
+ export function deferSkylineColour(models) {
515
+ let layer;
516
+ let sky, night = null;
517
+ const made = () => {
518
+ if (layer)
519
+ return layer;
520
+ layer = createSkylineColour(models);
521
+ if (sky !== undefined)
522
+ layer.setSky(sky);
523
+ if (night)
524
+ layer.setNight(night);
525
+ return layer;
526
+ };
527
+ return {
528
+ get enabled() { return layer?.enabled ?? false; },
529
+ prepare() { made(); },
530
+ set(enabled) {
531
+ if (layer || enabled)
532
+ made().set(enabled);
533
+ },
534
+ setSky(colours) {
535
+ sky = colours;
536
+ layer?.setSky(colours);
537
+ },
538
+ setNight(lighting) {
539
+ night = lighting;
540
+ if (layer)
541
+ layer.setNight(lighting);
542
+ else if (lighting)
543
+ made();
544
+ },
545
+ // Until it is made, nothing shows colour or a previewed night for it to refresh.
546
+ refresh() { layer?.refresh(); },
547
+ };
548
+ }
@@ -1,6 +1,12 @@
1
1
  // The viewer's code and styles, relative to the assets.
2
2
  const VIEWER = "skyline-viewer.js";
3
3
  const STYLESHEETS = ["study.css", "viewer.css"];
4
+ // The modules the viewer's first frame needs, which the build lists (scripts/build/viewer-modules.ts).
5
+ const MODULES = "viewer-modules.json";
6
+ // The asset directories whose viewers were offered the host's engine: each directory's first
7
+ // scene keeps the engine it took (three-engine.ts), so its later viewers never fetch the
8
+ // vendored one either.
9
+ const sharedEngines = new Set();
4
10
  /** The validated, slash-terminated directory the assets are served from. */
5
11
  export function assetsBase(document, assetsUrl) {
6
12
  const base = new URL(assetsUrl, document.baseURI);
@@ -20,6 +26,19 @@ export function assetsBase(document, assetsUrl) {
20
26
  function importViewer(url) {
21
27
  return import(/* webpackIgnore: true */ /* @vite-ignore */ /* turbopackIgnore: true */ url);
22
28
  }
29
+ /**
30
+ * Asks the browser to fetch every module the viewer's first frame needs at once, rather than
31
+ * each only once the module importing it has arrived, a chain several round trips deep. The
32
+ * links go in the document's head, where modulepreload applies, and leave it once the viewer
33
+ * settles or is destroyed. The vendored engine is fetched only when no viewer from the same
34
+ * assets offered the host's Three.js. Preloading only saves time, so a list that fails to load changes nothing else.
35
+ */
36
+ function preloadModules(document, assets, hostEngine, signal) {
37
+ return fetch(new URL(MODULES, assets), { signal })
38
+ .then((response) => response.json())
39
+ .then(({ modules, engine }) => signal.aborted ? [] : [...(hostEngine || !engine ? [] : [engine]), ...modules].map((file) => document.head.appendChild(Object.assign(document.createElement("link"), { rel: "modulepreload", href: new URL(file, assets).href }))))
40
+ .catch(() => []);
41
+ }
23
42
  /** Resolves once `link` has loaded its stylesheet. */
24
43
  function loaded(link) {
25
44
  return new Promise((resolve, reject) => {
@@ -79,6 +98,11 @@ export function mountViewer(container, { assetsUrl, title, ...options }) {
79
98
  const controller = new AbortController();
80
99
  let destroyed = false, mounted = false;
81
100
  const context = { ...options, theme, assets, signal: controller.signal };
101
+ if (options.three)
102
+ sharedEngines.add(assets.href);
103
+ const preloads = preloadModules(document, assets, sharedEngines.has(assets.href), controller.signal);
104
+ const dropPreloads = () => void preloads.then((links) => links.forEach((link) => link.remove()));
105
+ controller.signal.addEventListener("abort", dropPreloads, { once: true });
82
106
  const ready = new Promise((resolve, reject) => {
83
107
  Promise.all([importViewer(new URL(VIEWER, assets).href), ...stylesheets.map(loaded)])
84
108
  .then(async ([viewer]) => {
@@ -108,7 +132,7 @@ export function mountViewer(container, { assetsUrl, title, ...options }) {
108
132
  });
109
133
  // The element and the console report a failure, so a host that ignores `ready` sees no
110
134
  // unhandled rejection.
111
- ready.catch(() => { });
135
+ ready.then(dropPreloads, dropPreloads);
112
136
  return {
113
137
  element,
114
138
  ready,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a2f0/skyline",
3
- "version": "0.2.59",
3
+ "version": "0.2.61",
4
4
  "description": "Interactive Chicago skyline viewer and reusable Three.js building models",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
package/site/index.html CHANGED
@@ -10,8 +10,11 @@
10
10
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
11
11
  <title>Chicago Skyline — Living Night</title>
12
12
  <!-- viewer-page.js mounts the viewer from skyline-viewer.js, its styles and skyline-3d.html,
13
- and starts the scene's code; fetch them alongside it. -->
13
+ and starts the scene's code; fetch them alongside it, with the list of the scene's
14
+ modules that the viewer's shell preloads. -->
15
+ <link rel="modulepreload" href="skyline-shell.js" />
14
16
  <link rel="modulepreload" href="skyline-viewer.js" />
17
+ <link rel="preload" href="viewer-modules.json" as="fetch" crossorigin="anonymous" />
15
18
  <link rel="preload" href="viewer.css" as="style" />
16
19
  <link rel="preload" href="study.css" as="style" />
17
20
  <style>
@@ -506,3 +506,43 @@ export function createSkylineColour(models) {
506
506
  },
507
507
  };
508
508
  }
509
+ // Making the layer reads every vertex of every building, a large share of the scene's start-up
510
+ // time, and changes nothing on screen until colour, a previewed time or a celebration needs it:
511
+ // its facade shaders install only then (colour-reflections.ts). So this one is made on first
512
+ // use, or by `prepare`, from the same buildings and with the sky and night it was given
513
+ // meanwhile, and acts from then on exactly as one made at once.
514
+ export function deferSkylineColour(models) {
515
+ let layer;
516
+ let sky, night = null;
517
+ const made = () => {
518
+ if (layer)
519
+ return layer;
520
+ layer = createSkylineColour(models);
521
+ if (sky !== undefined)
522
+ layer.setSky(sky);
523
+ if (night)
524
+ layer.setNight(night);
525
+ return layer;
526
+ };
527
+ return {
528
+ get enabled() { return layer?.enabled ?? false; },
529
+ prepare() { made(); },
530
+ set(enabled) {
531
+ if (layer || enabled)
532
+ made().set(enabled);
533
+ },
534
+ setSky(colours) {
535
+ sky = colours;
536
+ layer?.setSky(colours);
537
+ },
538
+ setNight(lighting) {
539
+ night = lighting;
540
+ if (layer)
541
+ layer.setNight(lighting);
542
+ else if (lighting)
543
+ made();
544
+ },
545
+ // Until it is made, nothing shows colour or a previewed night for it to refresh.
546
+ refresh() { layer?.refresh(); },
547
+ };
548
+ }
@@ -4,7 +4,7 @@ import { chicagoDay, chicagoTime } from "./solar-time.js";
4
4
  import { createTimeLighting } from "./time-of-day.js";
5
5
  import { createGeographicRoads, geographicOrigin } from "./models/skyline-geography.js";
6
6
  import { celebrations } from "./models/celebrations.js";
7
- import { createDaylight, createSkylineColour, daylightGround } from "./models/colour-materials.js";
7
+ import { createDaylight, daylightGround, deferSkylineColour } from "./models/colour-materials.js";
8
8
  import { loadMarkup, loadStylesheet } from "./markup.js";
9
9
  // The skyline viewer's full-screen 3D mode: every mapped building, seen through the
10
10
  // drawing's own camera and framed as the viewer frames the drawing, so each tower stands
@@ -60,8 +60,9 @@ export function startSkyline3d({ root, frame, assets, signal, colour: initiallyC
60
60
  enablePan: true,
61
61
  minimumCameraHeight: 1,
62
62
  });
63
- // The colour trial's layer (models/colour-materials.ts), which the colour toggle turns on.
64
- const colour = createSkylineColour(models);
63
+ // The colour trial's layer (models/colour-materials.ts), which the colour toggle turns on. It
64
+ // is made on first use rather than before the first frame, which it does not change.
65
+ const colour = deferSkylineColour(models);
65
66
  // A toolbar badge applies one celebration across the skyline, using each
66
67
  // building's own supported wording. Context menus change only their building.
67
68
  const illuminated = models.filter((model) => model.illumination);
@@ -69,6 +70,8 @@ export function startSkyline3d({ root, frame, assets, signal, colour: initiallyC
69
70
  const celebrationAnnouncement = root.querySelector("#celebration-announcement");
70
71
  const celebrationButtons = [...root.querySelectorAll("[data-celebration]")];
71
72
  function setCelebration(targets, next) {
73
+ // The colour layer reads the buildings' greys before the lights rewrite them.
74
+ colour.prepare();
72
75
  targets.forEach((model) => model.illumination.set(model.illumination.presets.some(({ id }) => id === next) ? next : null));
73
76
  colour.refresh();
74
77
  for (const button of celebrationButtons) {
@@ -1,6 +1,12 @@
1
1
  // The viewer's code and styles, relative to the assets.
2
2
  const VIEWER = "skyline-viewer.js";
3
3
  const STYLESHEETS = ["study.css", "viewer.css"];
4
+ // The modules the viewer's first frame needs, which the build lists (scripts/build/viewer-modules.ts).
5
+ const MODULES = "viewer-modules.json";
6
+ // The asset directories whose viewers were offered the host's engine: each directory's first
7
+ // scene keeps the engine it took (three-engine.ts), so its later viewers never fetch the
8
+ // vendored one either.
9
+ const sharedEngines = new Set();
4
10
  /** The validated, slash-terminated directory the assets are served from. */
5
11
  export function assetsBase(document, assetsUrl) {
6
12
  const base = new URL(assetsUrl, document.baseURI);
@@ -20,6 +26,19 @@ export function assetsBase(document, assetsUrl) {
20
26
  function importViewer(url) {
21
27
  return import(/* webpackIgnore: true */ /* @vite-ignore */ /* turbopackIgnore: true */ url);
22
28
  }
29
+ /**
30
+ * Asks the browser to fetch every module the viewer's first frame needs at once, rather than
31
+ * each only once the module importing it has arrived, a chain several round trips deep. The
32
+ * links go in the document's head, where modulepreload applies, and leave it once the viewer
33
+ * settles or is destroyed. The vendored engine is fetched only when no viewer from the same
34
+ * assets offered the host's Three.js. Preloading only saves time, so a list that fails to load changes nothing else.
35
+ */
36
+ function preloadModules(document, assets, hostEngine, signal) {
37
+ return fetch(new URL(MODULES, assets), { signal })
38
+ .then((response) => response.json())
39
+ .then(({ modules, engine }) => signal.aborted ? [] : [...(hostEngine || !engine ? [] : [engine]), ...modules].map((file) => document.head.appendChild(Object.assign(document.createElement("link"), { rel: "modulepreload", href: new URL(file, assets).href }))))
40
+ .catch(() => []);
41
+ }
23
42
  /** Resolves once `link` has loaded its stylesheet. */
24
43
  function loaded(link) {
25
44
  return new Promise((resolve, reject) => {
@@ -79,6 +98,11 @@ export function mountViewer(container, { assetsUrl, title, ...options }) {
79
98
  const controller = new AbortController();
80
99
  let destroyed = false, mounted = false;
81
100
  const context = { ...options, theme, assets, signal: controller.signal };
101
+ if (options.three)
102
+ sharedEngines.add(assets.href);
103
+ const preloads = preloadModules(document, assets, sharedEngines.has(assets.href), controller.signal);
104
+ const dropPreloads = () => void preloads.then((links) => links.forEach((link) => link.remove()));
105
+ controller.signal.addEventListener("abort", dropPreloads, { once: true });
82
106
  const ready = new Promise((resolve, reject) => {
83
107
  Promise.all([importViewer(new URL(VIEWER, assets).href), ...stylesheets.map(loaded)])
84
108
  .then(async ([viewer]) => {
@@ -108,7 +132,7 @@ export function mountViewer(container, { assetsUrl, title, ...options }) {
108
132
  });
109
133
  // The element and the console report a failure, so a host that ignores `ready` sees no
110
134
  // unhandled rejection.
111
- ready.catch(() => { });
135
+ ready.then(dropPreloads, dropPreloads);
112
136
  return {
113
137
  element,
114
138
  ready,
@@ -0,0 +1,62 @@
1
+ {
2
+ "modules": [
3
+ "markup.js",
4
+ "three-engine.js",
5
+ "skyline-3d.js",
6
+ "study-viewer.js",
7
+ "vendor/three-r186.js",
8
+ "skyline-scene.js",
9
+ "models/skyline-geography-data.js",
10
+ "models/skyline-geography.js",
11
+ "models/building-kit.js",
12
+ "models/aon-geographic.js",
13
+ "models/aon-tower.js",
14
+ "models/facade-grid.js",
15
+ "models/blue-cross-geographic.js",
16
+ "models/window-illumination.js",
17
+ "models/celebrations.js",
18
+ "models/borg-warner-geographic.js",
19
+ "models/buckingham-geographic.js",
20
+ "models/chicago-athletic-association-geographic.js",
21
+ "models/crain-geographic.js",
22
+ "models/crain-tower.js",
23
+ "models/gage-geographic.js",
24
+ "models/heritage-geographic.js",
25
+ "models/hyatt-west-tower-geographic.js",
26
+ "models/keith-ascher-geographic.js",
27
+ "models/kemper-geographic.js",
28
+ "models/lake-view-geographic.js",
29
+ "models/maclean-center-geographic.js",
30
+ "models/michigan-boulevard-geographic.js",
31
+ "models/michigan-plaza-south-geographic.js",
32
+ "models/millennium-park-plaza-geographic.js",
33
+ "models/monroe-geographic.js",
34
+ "models/north-michigan-180-geographic.js",
35
+ "models/north-wabash-geographic.js",
36
+ "models/on-the-park-geographic.js",
37
+ "models/one-prudential-geographic.js",
38
+ "models/one-prudential-tower.js",
39
+ "models/peoples-gas-geographic.js",
40
+ "models/railway-exchange-geographic.js",
41
+ "models/river-plaza-geographic.js",
42
+ "models/sheraton-grand-geographic.js",
43
+ "models/six-north-michigan-geographic.js",
44
+ "models/swissotel-geographic.js",
45
+ "models/three-illinois-center-geographic.js",
46
+ "models/trump-geographic.js",
47
+ "models/trump-tower.js",
48
+ "models/two-illinois-center-geographic.js",
49
+ "models/two-prudential-geographic.js",
50
+ "models/two-prudential-tower.js",
51
+ "models/university-club-geographic.js",
52
+ "models/willoughby-tower-geographic.js",
53
+ "solar-time.js",
54
+ "time-of-day.js",
55
+ "models/colour-materials.js",
56
+ "models/colour-palette.js",
57
+ "models/colour-reflections.js",
58
+ "models/night-windows.js",
59
+ "models/material-names.js"
60
+ ],
61
+ "engine": "vendor/three-r186.bundle.js"
62
+ }