@a2f0/skyline 0.1.7 → 0.2.2
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 +3 -3
- package/docs/package.md +163 -25
- package/lib/skyline-package.d.ts +26 -18
- package/lib/skyline-package.js +10 -26
- package/lib/skyline-shell.d.ts +69 -0
- package/lib/skyline-shell.js +115 -0
- package/lib/skyline-three.d.ts +2 -0
- package/lib/skyline-three.js +10 -0
- package/lib/three-engine.d.ts +18 -0
- package/lib/three-engine.js +12 -0
- package/package.json +15 -4
- package/site/building-detail-page.js +16 -0
- package/site/building-detail.html +45 -38
- package/site/building-detail.js +25 -19
- package/site/index.html +21 -373
- package/site/markup.js +36 -0
- package/site/skyline-3d-page.js +12 -0
- package/site/skyline-3d.html +7 -11
- package/site/skyline-3d.js +396 -310
- package/site/skyline-shell.js +115 -0
- package/site/skyline-viewer.js +321 -0
- package/site/stars.svg +8 -6
- package/site/study-loader.js +0 -2
- package/site/study-viewer.js +97 -37
- package/site/study.css +57 -30
- package/site/three-engine.js +12 -0
- package/site/vendor/three-r186.bundle.js +4228 -0
- package/site/vendor/three-r186.js +11 -4228
- package/site/viewer-page.js +15 -0
- package/site/viewer.css +147 -0
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ WebGL and the 3D pages need it.
|
|
|
22
22
|
|
|
23
23
|
| Page | Shows | Docs |
|
|
24
24
|
| --- | --- | --- |
|
|
25
|
-
| `index.html` | The viewer: opens on the 3D skyline, with the enhanced drawing, the original drawing, and a WebGL prototype one button away | [The viewer](docs/viewer.md) |
|
|
25
|
+
| `index.html` | The viewer, mounted as the package mounts it: opens on the 3D skyline, with the enhanced drawing, the original drawing, and a WebGL prototype one button away | [The viewer](docs/viewer.md) |
|
|
26
26
|
| `skyline-3d.html` | The full-screen 3D skyline on its own: camera views, streets, footprints, celebratory lights, and a building menu | [Full-screen 3D skyline](docs/viewer.md#full-screen-3d-skyline) |
|
|
27
27
|
| `building-detail.html?building=<id>` | One mapped building alone on its platform | [Building menu and detail](docs/viewer.md#building-menu-and-detail) |
|
|
28
28
|
| `skyline-study.html` | The 3D scene over the drawing, in the geographic layout or the original drawing's fit | [3D studies](docs/studies.md#skyline-study) |
|
|
@@ -32,8 +32,8 @@ WebGL and the 3D pages need it.
|
|
|
32
32
|
## Package
|
|
33
33
|
|
|
34
34
|
The repository also builds [`@a2f0/skyline`](https://www.npmjs.com/package/@a2f0/skyline), an ESM
|
|
35
|
-
package
|
|
36
|
-
TypeScript declarations. Every merge to `main` publishes a new version to npm; run
|
|
35
|
+
package that mounts the complete viewer into a host page's own document, in a shadow root, with
|
|
36
|
+
reusable Three.js scenes and building models, and TypeScript declarations. Every merge to `main` publishes a new version to npm; run
|
|
37
37
|
`bun run build:package` or `bun pm pack` to build it locally. [The package guide](docs/package.md)
|
|
38
38
|
covers installing, asset copying, mounting, imports, and releases.
|
|
39
39
|
|
package/docs/package.md
CHANGED
|
@@ -20,7 +20,8 @@ provenance linking it to the commit and workflow run that built it.
|
|
|
20
20
|
|
|
21
21
|
From this repository:
|
|
22
22
|
|
|
23
|
-
Use the
|
|
23
|
+
Use the Bun and Node.js LTS versions pinned in `mise.toml`, npm (included with Node),
|
|
24
|
+
and Google Chrome for checks.
|
|
24
25
|
|
|
25
26
|
```sh
|
|
26
27
|
bun install --ignore-scripts
|
|
@@ -81,15 +82,16 @@ await copySkylineAssets('./public/skyline');
|
|
|
81
82
|
|
|
82
83
|
Serve that directory over HTTP at `/skyline/`, preserving all its relative paths.
|
|
83
84
|
The helper copies files into the supplied directory; it does not clear unrelated
|
|
84
|
-
files already there. Use a dedicated directory for these assets
|
|
85
|
-
|
|
85
|
+
files already there. Use a dedicated directory for these assets, and clear it before
|
|
86
|
+
copying an upgrade, so files a new version dropped do not linger. All the viewer's
|
|
87
|
+
code, CSS, markup, SVGs and models, and its vendored Three.js, travel together. Copy
|
|
88
|
+
them again whenever you upgrade the package, so the loader you bundle and the viewer
|
|
89
|
+
you serve match.
|
|
86
90
|
|
|
87
|
-
In `~/github/devopsrockstars`,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
then use `/static/skyline/` as the browser URL. Keep the generated directory out of
|
|
92
|
-
that application's Git history.
|
|
91
|
+
In `~/github/devopsrockstars`, `packages/frontend/tooling/build.ts` calls the helper
|
|
92
|
+
when the `skyline3d` flag is on and serves the copy at `/static/skyline/`, the browser
|
|
93
|
+
URL the viewer mounts with. Keep the generated directory out of that application's
|
|
94
|
+
Git history.
|
|
93
95
|
|
|
94
96
|
## Mount the complete viewer
|
|
95
97
|
|
|
@@ -100,17 +102,41 @@ const skyline = mountSkyline(container, {
|
|
|
100
102
|
assetsUrl: '/skyline/',
|
|
101
103
|
title: 'Interactive Chicago skyline',
|
|
102
104
|
});
|
|
105
|
+
await skyline.ready; // optional: the 3D skyline has drawn its first frame
|
|
103
106
|
|
|
104
107
|
// When the page or component leaves:
|
|
105
108
|
skyline.destroy();
|
|
106
109
|
```
|
|
107
110
|
|
|
108
|
-
The
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
`
|
|
113
|
-
|
|
111
|
+
The viewer renders into the host's own document, inside a shadow root that keeps its
|
|
112
|
+
styles and the host's apart. `mountSkyline` returns at once with an instance:
|
|
113
|
+
|
|
114
|
+
- `element` is the `HTMLElement` it appended to `container`: a region named by the
|
|
115
|
+
`title` option (default "Interactive Chicago skyline"), with the viewer in its open
|
|
116
|
+
shadow root. It fills the container, which sets the viewer's size and placement:
|
|
117
|
+
give the container an explicit height. The element is `aria-busy` until `ready`
|
|
118
|
+
settles.
|
|
119
|
+
- `ready` resolves once the 3D skyline has drawn its first frame: use it where an
|
|
120
|
+
iframe's `load` event was used. It rejects when the viewer can't start: when its
|
|
121
|
+
assets fail to load, the element says why in the viewer's place; when the 3D scene
|
|
122
|
+
can't start, as without WebGL 2, the scene says why and the original drawing stays
|
|
123
|
+
one press away. The error is also logged, so a host that ignores `ready` sees no
|
|
124
|
+
unhandled rejection. A destroyed instance's `ready` never settles.
|
|
125
|
+
- `destroy()` removes the element and releases everything the viewer holds: its WebGL
|
|
126
|
+
renderers and contexts (disposed, then lost on purpose, so remounts never exhaust
|
|
127
|
+
the browser's contexts), animation frames, resize observers, media-query, pointer
|
|
128
|
+
and keyboard listeners, the building detail, and loads still in flight. Calling it
|
|
129
|
+
again does nothing, and destroying while the viewer still loads is safe. Unlike a
|
|
130
|
+
frame, the viewer doesn't stop when its element merely leaves the page: always call
|
|
131
|
+
`destroy`.
|
|
132
|
+
|
|
133
|
+
The viewer shows what the embedded viewer always has: the twinkling stars and the
|
|
134
|
+
traced loading silhouette, the 3D skyline with its control bar, the OpenStreetMap
|
|
135
|
+
credit, a small **show original** button until the first 3D frame (and again if the
|
|
136
|
+
graphics context is lost), so the original drawing stays reachable even if WebGL or
|
|
137
|
+
the scene's code cannot load, building details in a panel over the skyline, and
|
|
138
|
+
reduced motion. `F`, pressed inside the viewer, shows its element fullscreen.
|
|
139
|
+
|
|
114
140
|
The scene's control bar starts folded behind its star; set `controls: 'open'` to
|
|
115
141
|
start it open, with no fold, from its first frame. Visitors can still fold it.
|
|
116
142
|
Open, the bar shows the camera hint and the OpenStreetMap credit for the streets
|
|
@@ -118,14 +144,34 @@ and footprints. Set `attribution: false` to leave the credit out. OpenStreetMap'
|
|
|
118
144
|
[licence](https://www.openstreetmap.org/copyright) requires crediting its contributors
|
|
119
145
|
wherever its data is shown, so do this only when the host page credits
|
|
120
146
|
© OpenStreetMap contributors itself.
|
|
121
|
-
|
|
122
|
-
3D frame succeeds, and reappears if the graphics context is lost. The SVG stays
|
|
123
|
-
reachable even if WebGL, the scene document, or its bootstrap script cannot load.
|
|
124
|
-
Unmounting removes the entire document, its event handlers, and its WebGL context.
|
|
147
|
+
|
|
125
148
|
Imports themselves do not access `window` or `document`, so server rendering and
|
|
126
149
|
lazy loading are safe. Asset URLs may be relative or absolute HTTP(S) directories.
|
|
127
150
|
|
|
128
|
-
|
|
151
|
+
### Living in the host's page
|
|
152
|
+
|
|
153
|
+
- The host's styles stay out: the element sets `all: initial` and `direction: ltr` on
|
|
154
|
+
itself, so nothing the host's rules set is inherited into the viewer, and the
|
|
155
|
+
viewer sets its own type, colours and custom properties. Its styles stay in its
|
|
156
|
+
shadow root. It lays itself out by its own size, through container queries, not the
|
|
157
|
+
window's: a small window in a desktop gets the narrow layout.
|
|
158
|
+
- Presses and keys bubble out to the host, as any other content's do: a press inside
|
|
159
|
+
the viewer reaches the host's `pointerdown` and `mousedown` listeners, so a host's
|
|
160
|
+
window can raise itself and its menus close. Its keys act only where they are
|
|
161
|
+
pressed: `F`, `Escape` and its arrow, zoom and menu keys work while focus is inside
|
|
162
|
+
that viewer, and arrive with `defaultPrevented` set, so two viewers, or a viewer and
|
|
163
|
+
the host, never take each other's keys.
|
|
164
|
+
- The viewer never scrolls the host page: it moves focus with `preventScroll`, and
|
|
165
|
+
its building detail is a non-modal panel (`role="dialog"`, not `aria-modal`), so the
|
|
166
|
+
rest of the page stays usable.
|
|
167
|
+
- It works wherever the host puts it: in the host's own shadow root, open or closed, and
|
|
168
|
+
under a host's CSS transform, where its menus and tooltip still open at the pointer.
|
|
169
|
+
- Each instance is independent: two viewers can run side by side on one page.
|
|
170
|
+
|
|
171
|
+
### React
|
|
172
|
+
|
|
173
|
+
An effect owns the lifecycle. Mounting is synchronous, so React Strict Mode's mount,
|
|
174
|
+
destroy and mount again in development works:
|
|
129
175
|
|
|
130
176
|
```tsx
|
|
131
177
|
import React, { useEffect, useRef } from 'react';
|
|
@@ -136,6 +182,8 @@ export default function Skyline() {
|
|
|
136
182
|
useEffect(() => {
|
|
137
183
|
const skyline = mountSkyline(container.current!, {
|
|
138
184
|
assetsUrl: '/static/skyline/',
|
|
185
|
+
// The page already runs Three.js: share it (see below).
|
|
186
|
+
three: import('@a2f0/skyline/three'),
|
|
139
187
|
});
|
|
140
188
|
return () => skyline.destroy();
|
|
141
189
|
}, []);
|
|
@@ -144,15 +192,95 @@ export default function Skyline() {
|
|
|
144
192
|
}
|
|
145
193
|
```
|
|
146
194
|
|
|
147
|
-
Keep the host's header, footer, sizing, and stacking in that application.
|
|
148
|
-
|
|
149
|
-
|
|
195
|
+
Keep the host's header, footer, sizing, and stacking in that application.
|
|
196
|
+
|
|
197
|
+
### Share the page's Three.js
|
|
198
|
+
|
|
199
|
+
The viewer's 3D scene renders with Three.js r186. By default it loads the copy in the
|
|
200
|
+
assets, `vendor/three-r186.bundle.js` (about 140 KB compressed), only when a scene
|
|
201
|
+
first needs it, so a host needs no `three` dependency. A page that already runs
|
|
202
|
+
Three.js would then run two engines, and Three.js warns "Multiple instances of
|
|
203
|
+
Three.js being imported". Such a page can share its own instead, through the `three`
|
|
204
|
+
option:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
mountSkyline(container, { assetsUrl: '/static/skyline/', three: import('@a2f0/skyline/three') });
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`@a2f0/skyline/three` re-exports from the host's `three` (the optional peer,
|
|
211
|
+
`~0.186.0`) exactly the classes the viewer uses, and `OrbitControls`, so a bundler keeps
|
|
212
|
+
no more of the engine than the viewer needs; the viewer then never requests its own
|
|
213
|
+
copy. Pass the module, or a promise of it as above, which keeps it out of the host's
|
|
214
|
+
first bundle when the viewer mounts later. The page's first scene takes the engine
|
|
215
|
+
when its code first loads, and later instances on the page share it; if the promise
|
|
216
|
+
rejects, the viewer warns and loads its own copy. A host without `three` simply leaves
|
|
217
|
+
the option out.
|
|
218
|
+
|
|
219
|
+
### How the viewer's code loads
|
|
220
|
+
|
|
221
|
+
The package's module (`lib/`) is a small loader that a host bundles (under 4 KB). When
|
|
222
|
+
it mounts, it imports the viewer's code from the assets (`skyline-viewer.js`) with a
|
|
223
|
+
dynamic `import()` that carries `webpackIgnore`, `turbopackIgnore` and `@vite-ignore`
|
|
224
|
+
comments, so webpack (including Next.js with `--webpack`), Turbopack and Vite leave it
|
|
225
|
+
to the browser; Rollup, esbuild and Bun leave an import of a variable alone anyway. A
|
|
226
|
+
bundler that tried to resolve it would fail to build: tell it to ignore that import.
|
|
227
|
+
The viewer then loads its stylesheets, the scene's markup from `skyline-3d.html`, its
|
|
228
|
+
stars from `stars.svg`, and the scene's modules and models from the same directory.
|
|
229
|
+
Each module loads once per page, however many viewers mount.
|
|
230
|
+
|
|
231
|
+
### Content security policy and other origins
|
|
232
|
+
|
|
233
|
+
The viewer runs in the host's page, under the host's content security policy. A host
|
|
234
|
+
with one must allow the assets' origin (usually `'self'`) in `script-src`,
|
|
235
|
+
`style-src` and `connect-src` (the viewer fetches its markup and stars), and inline
|
|
236
|
+
styles in `style-src`, which the viewer sets on its elements. Serve the assets from
|
|
237
|
+
the host's origin when you can; on another origin, every file must allow the page's
|
|
238
|
+
origin through CORS. The viewer's code runs with the host page's privileges wherever
|
|
239
|
+
it is served from: serve it only from an origin you trust.
|
|
240
|
+
|
|
241
|
+
### For tests
|
|
242
|
+
|
|
243
|
+
`element.shadowRoot` holds the viewer: its 3D canvas is `#building` and the scene's
|
|
244
|
+
loading indicator `#loading`, hidden at the first frame. The region drops `aria-busy`
|
|
245
|
+
when `ready` settles. WebDriver and Playwright reach into open shadow roots; code run
|
|
246
|
+
in the page reads `element.shadowRoot.querySelector(...)`.
|
|
247
|
+
|
|
248
|
+
### From 0.1 (iframe) to 0.2
|
|
249
|
+
|
|
250
|
+
Before 0.2, the viewer ran in an iframe. Hosts upgrading:
|
|
251
|
+
|
|
252
|
+
- `element` is the viewer's region element, not an `HTMLIFrameElement`: wait for
|
|
253
|
+
`ready` instead of the frame's `load` event, and reach the viewer through
|
|
254
|
+
`element.shadowRoot` instead of `contentDocument`. There are no nested frames: the
|
|
255
|
+
3D scene and its building detail render in the same shadow root (the detail in a
|
|
256
|
+
shadow root of its own, inside the scene's panel), and they no longer post
|
|
257
|
+
`skyline:` messages to each other.
|
|
258
|
+
- `navigation` is gone. It showed the site's own toolbar, which only the site's
|
|
259
|
+
`index.html` shows now. The viewer's options are `assetsUrl`, `title`, `controls`,
|
|
260
|
+
`attribution` and `three`.
|
|
261
|
+
- Presses inside the viewer reach the host as any other press does, so a workaround
|
|
262
|
+
that raised a window when a frame took focus is no longer needed.
|
|
263
|
+
- The viewer no longer has its own window: `window.__buildingStudy`, which tests read
|
|
264
|
+
inside the scene's frame, is not set on the host's window.
|
|
265
|
+
- Copy the assets again: the viewer's code is new files (`skyline-viewer.js`,
|
|
266
|
+
`viewer.css` and others).
|
|
267
|
+
|
|
268
|
+
### Limitations
|
|
269
|
+
|
|
270
|
+
- Removing the element without calling `destroy()` leaves the viewer running.
|
|
271
|
+
- If the viewer's or scene's code fails to load, as on a dropped connection, the
|
|
272
|
+
browser keeps that failure for the page: later mounts on the same page fail the same
|
|
273
|
+
way until it reloads. A frame used to retry with a new document.
|
|
274
|
+
- The site's own `index.html` shows two prototypes from its toolbar, the hover-scripted
|
|
275
|
+
enhanced drawing and the WebGL prototype, as pages of their own in frames; a host's
|
|
276
|
+
viewer never shows them.
|
|
150
277
|
|
|
151
278
|
## Import a scene or a building
|
|
152
279
|
|
|
153
280
|
For an application that owns its own renderer, install `three` from the r186
|
|
154
281
|
series (`~0.186.0`). `devopsrockstars` already uses a compatible version. The
|
|
155
|
-
|
|
282
|
+
mounted viewer needs this peer only to share the host's engine; scene/model imports
|
|
283
|
+
always do.
|
|
156
284
|
|
|
157
285
|
```ts
|
|
158
286
|
import { Group } from 'three';
|
|
@@ -205,7 +333,14 @@ overlap, and when merges land together only the newest pending run starts, so in
|
|
|
205
333
|
versions can be skipped on npm.
|
|
206
334
|
|
|
207
335
|
The workflow first runs the full `bun run check`, browser suites included, in the runner's
|
|
208
|
-
Google Chrome
|
|
336
|
+
Google Chrome with the pinned Node.js LTS. The dependency audit is part of that
|
|
337
|
+
check: it needs the registry network, and any advisory in the locked dependency
|
|
338
|
+
graph fails the check and blocks publishing until addressed.
|
|
339
|
+
If that changes after merge, report the failed release, fix the dependency or
|
|
340
|
+
registry failure, then re-dispatch the workflow to release the merged version.
|
|
341
|
+
It also runs the
|
|
342
|
+
tarball and viewer package suite on Node.js 22 to verify the supported minimum.
|
|
343
|
+
Only when those pass does it build and pack the tarball; both jobs run without
|
|
209
344
|
publish rights, and the publish job receives only that tarball and runs
|
|
210
345
|
`npm publish --ignore-scripts`. Dispatching the workflow on another branch
|
|
211
346
|
(`gh workflow run npm-publish.yml --ref <branch>`) runs only the tests, in a queue of its own,
|
|
@@ -224,6 +359,9 @@ Then, in the package's settings on npmjs.com, add a GitHub Actions trusted publi
|
|
|
224
359
|
repository `a2f0/skyline`, workflow `npm-publish.yml`, and environment `npm`, and re-run
|
|
225
360
|
the workflow; it reports the version as already published.
|
|
226
361
|
|
|
362
|
+
The Sharp override in the tarball is repository development tooling policy; npm
|
|
363
|
+
and Bun consumers use their own root overrides, so it does not pin their dependencies.
|
|
364
|
+
|
|
227
365
|
The package is ESM only, with no CommonJS entrypoint. Building requires this repository's
|
|
228
366
|
Bun/TypeScript tools; consuming the published package requires neither Bun nor build
|
|
229
367
|
scripts. Metadata uses `UNLICENSED` and preserves the third-party notices in
|
package/lib/skyline-package.d.ts
CHANGED
|
@@ -1,31 +1,39 @@
|
|
|
1
|
+
import { type SkylineInstance } from "./skyline-shell.js";
|
|
2
|
+
import type { SkylineThree } from "./three-engine.js";
|
|
1
3
|
export type { BuildingModel, Vec2, Vec3 } from "./models/building-kit.js";
|
|
2
4
|
export type { FitBox, PlatformOptions, ShadowCameraOptions, StudyView, StudyLayout, StudyLabel } from "./study-types.js";
|
|
3
5
|
export type { GeographicSkyline } from "./skyline-scene.js";
|
|
6
|
+
export type { SkylineInstance } from "./skyline-shell.js";
|
|
7
|
+
export type { SkylineThree } from "./three-engine.js";
|
|
4
8
|
export interface SkylineOptions {
|
|
5
|
-
/**
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The directory serving the package's viewer assets, as copied by `copySkylineAssets`
|
|
11
|
+
* from `@a2f0/skyline/build`; for example "/static/skyline/". Relative URLs resolve
|
|
12
|
+
* against the page. The viewer's code, styles, drawings and models load from it.
|
|
13
|
+
*/
|
|
14
|
+
readonly assetsUrl: string | URL;
|
|
15
|
+
/** The viewer's accessible name: `element` is a region with this label. */
|
|
16
|
+
readonly title?: string;
|
|
11
17
|
/** Start the 3D scene's control bar open, or folded behind its star. Closed by default. */
|
|
12
|
-
controls?: "open" | "closed";
|
|
18
|
+
readonly controls?: "open" | "closed";
|
|
13
19
|
/**
|
|
14
20
|
* Show the OpenStreetMap credit with the 3D scene's controls. Shown by default. OpenStreetMap's
|
|
15
21
|
* licence requires that credit wherever its data appears, so hide it only when the host page
|
|
16
22
|
* credits OpenStreetMap contributors itself.
|
|
17
23
|
*/
|
|
18
|
-
attribution?: boolean;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
readonly attribution?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* The host's own Three.js, for a page that already runs it: pass `@a2f0/skyline/three`, or a
|
|
27
|
+
* dynamic import of it, and the scene renders with the host's engine instead of downloading
|
|
28
|
+
* the copy in the assets. The page's first scene takes the engine; leave it out to use the
|
|
29
|
+
* assets' copy, which needs no `three` dependency.
|
|
30
|
+
*/
|
|
31
|
+
readonly three?: SkylineThree | PromiseLike<SkylineThree>;
|
|
25
32
|
}
|
|
26
33
|
/**
|
|
27
|
-
* Mounts the complete interactive viewer inside a
|
|
28
|
-
*
|
|
29
|
-
*
|
|
34
|
+
* Mounts the complete interactive viewer filling `container`, inside a shadow root. It
|
|
35
|
+
* returns at once; `ready` settles when the 3D skyline shows. The container sets the viewer's
|
|
36
|
+
* size and placement: give it an explicit height. Call `destroy` when the hosting component
|
|
37
|
+
* leaves.
|
|
30
38
|
*/
|
|
31
|
-
export declare function mountSkyline(container: HTMLElement, { assetsUrl, title,
|
|
39
|
+
export declare function mountSkyline(container: HTMLElement, { assetsUrl, title, controls, attribution, three }: SkylineOptions): SkylineInstance;
|
package/lib/skyline-package.js
CHANGED
|
@@ -1,31 +1,15 @@
|
|
|
1
|
+
// The browser entrypoint of the @a2f0/skyline package: mounts the viewer in a host-owned
|
|
2
|
+
// container, inside a shadow root in the host's own document. Importing it never touches the
|
|
3
|
+
// DOM, so server rendering and lazy loading are safe. docs/package.md describes the package.
|
|
4
|
+
import { mountViewer } from "./skyline-shell.js";
|
|
1
5
|
/**
|
|
2
|
-
* Mounts the complete interactive viewer inside a
|
|
3
|
-
*
|
|
4
|
-
*
|
|
6
|
+
* Mounts the complete interactive viewer filling `container`, inside a shadow root. It
|
|
7
|
+
* returns at once; `ready` settles when the 3D skyline shows. The container sets the viewer's
|
|
8
|
+
* size and placement: give it an explicit height. Call `destroy` when the hosting component
|
|
9
|
+
* leaves.
|
|
5
10
|
*/
|
|
6
|
-
export function mountSkyline(container, { assetsUrl, title = "Interactive Chicago skyline",
|
|
11
|
+
export function mountSkyline(container, { assetsUrl, title = "Interactive Chicago skyline", controls = "closed", attribution = true, three }) {
|
|
7
12
|
if (controls !== "open" && controls !== "closed")
|
|
8
13
|
throw new TypeError('Skyline controls must be "open" or "closed".');
|
|
9
|
-
|
|
10
|
-
const base = new URL(assetsUrl, document.baseURI);
|
|
11
|
-
if (!/^(https?:)$/.test(base.protocol))
|
|
12
|
-
throw new TypeError("Skyline assets must be served over HTTP or HTTPS.");
|
|
13
|
-
if (base.search || base.hash)
|
|
14
|
-
throw new TypeError("Skyline assetsUrl must be a directory URL without a query or fragment.");
|
|
15
|
-
if (!base.pathname.endsWith("/"))
|
|
16
|
-
base.pathname += "/";
|
|
17
|
-
const element = document.createElement("iframe");
|
|
18
|
-
element.title = title;
|
|
19
|
-
const viewerUrl = new URL("index.html", base);
|
|
20
|
-
if (!navigation)
|
|
21
|
-
viewerUrl.searchParams.set("embed", "1");
|
|
22
|
-
if (controls === "open")
|
|
23
|
-
viewerUrl.searchParams.set("controls", "open");
|
|
24
|
-
if (!attribution)
|
|
25
|
-
viewerUrl.searchParams.set("attribution", "hidden");
|
|
26
|
-
element.src = viewerUrl.href;
|
|
27
|
-
element.allowFullscreen = true;
|
|
28
|
-
element.style.cssText = "display:block;width:100%;height:100%;border:0";
|
|
29
|
-
container.append(element);
|
|
30
|
-
return { element, destroy() { element.remove(); } };
|
|
14
|
+
return mountViewer(container, { assetsUrl, title, controls, attribution, navigation: false, page: false, three });
|
|
31
15
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { SkylineThree } from "./three-engine.js";
|
|
2
|
+
/** What the viewer module is told when it mounts. */
|
|
3
|
+
export interface ViewerContext {
|
|
4
|
+
/** The slash-terminated directory the assets are served from. */
|
|
5
|
+
readonly assets: URL;
|
|
6
|
+
/**
|
|
7
|
+
* Aborts when the instance is destroyed, or the viewer fails to start. The viewer then
|
|
8
|
+
* releases everything it holds: its WebGL contexts, animation frames, observers, listeners
|
|
9
|
+
* outside its root and pending loads. The shell removes the DOM.
|
|
10
|
+
*/
|
|
11
|
+
readonly signal: AbortSignal;
|
|
12
|
+
/** Start the 3D scene's control bar open, rather than folded behind its star. */
|
|
13
|
+
readonly controls: "open" | "closed";
|
|
14
|
+
/** Show the OpenStreetMap credit with the scene's controls. */
|
|
15
|
+
readonly attribution: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* The site's own toolbar, along the viewer's top: fullscreen, the drawings, the WebGL
|
|
18
|
+
* prototype, the studies and the stars' debug motion. Only the site's index.html shows it.
|
|
19
|
+
*/
|
|
20
|
+
readonly navigation: boolean;
|
|
21
|
+
/** The viewer is its page's whole content, so the page's keys are its keys. */
|
|
22
|
+
readonly page: boolean;
|
|
23
|
+
/** The host's Three.js, for the scene to share (three-engine.ts). */
|
|
24
|
+
readonly three?: SkylineThree | PromiseLike<SkylineThree> | undefined;
|
|
25
|
+
}
|
|
26
|
+
/** What the viewer module exports. */
|
|
27
|
+
export interface ViewerModule {
|
|
28
|
+
/**
|
|
29
|
+
* Renders the viewer into `root`, whose stylesheets have loaded, and settles once the 3D
|
|
30
|
+
* skyline has drawn its first frame: a rejection is a scene that can't start, and the
|
|
31
|
+
* viewer then says why in place and keeps the drawing one press away.
|
|
32
|
+
*/
|
|
33
|
+
mount(root: ShadowRoot, context: ViewerContext): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
/** A mounted viewer. */
|
|
36
|
+
export interface SkylineInstance {
|
|
37
|
+
/**
|
|
38
|
+
* The element holding the viewer, appended to the container. The viewer renders into its
|
|
39
|
+
* open shadow root, which keeps the viewer's styles and the host's apart. It is a region
|
|
40
|
+
* named by the `title` option, filling the container, and is `aria-busy` until `ready`
|
|
41
|
+
* settles.
|
|
42
|
+
*/
|
|
43
|
+
readonly element: HTMLElement;
|
|
44
|
+
/**
|
|
45
|
+
* Resolves once the 3D skyline has drawn its first frame; replaces an iframe's `load`
|
|
46
|
+
* event. Rejects when it can't start, as when its assets fail to load or WebGL is
|
|
47
|
+
* unavailable: the viewer then says why, in place, and still offers the original drawing
|
|
48
|
+
* if its own code loaded. Stays pending if the instance is destroyed first.
|
|
49
|
+
*/
|
|
50
|
+
readonly ready: Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Removes the viewer and releases its WebGL contexts, animation frames, observers,
|
|
53
|
+
* listeners, timers and pending loads. Calling it again does nothing.
|
|
54
|
+
*/
|
|
55
|
+
destroy(): void;
|
|
56
|
+
}
|
|
57
|
+
export interface ViewerOptions extends Omit<ViewerContext, "assets" | "signal"> {
|
|
58
|
+
/** The assets' directory; relative URLs resolve against the container's document. */
|
|
59
|
+
readonly assetsUrl: string | URL;
|
|
60
|
+
/** The viewer's accessible name: `element` is a region with this label. */
|
|
61
|
+
readonly title: string;
|
|
62
|
+
}
|
|
63
|
+
/** The validated, slash-terminated directory the assets are served from. */
|
|
64
|
+
export declare function assetsBase(document: Document, assetsUrl: string | URL): URL;
|
|
65
|
+
/**
|
|
66
|
+
* Mounts the viewer in a new element appended to `container`, and returns at once: the
|
|
67
|
+
* element is in place, and `ready` settles once the 3D skyline shows or can't start.
|
|
68
|
+
*/
|
|
69
|
+
export declare function mountViewer(container: HTMLElement, { assetsUrl, title, ...options }: ViewerOptions): SkylineInstance;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// The viewer's code and styles, relative to the assets.
|
|
2
|
+
const VIEWER = "skyline-viewer.js";
|
|
3
|
+
const STYLESHEETS = ["study.css", "viewer.css"];
|
|
4
|
+
/** The validated, slash-terminated directory the assets are served from. */
|
|
5
|
+
export function assetsBase(document, assetsUrl) {
|
|
6
|
+
const base = new URL(assetsUrl, document.baseURI);
|
|
7
|
+
if (base.protocol !== "http:" && base.protocol !== "https:")
|
|
8
|
+
throw new TypeError("Skyline assets must be served over HTTP or HTTPS.");
|
|
9
|
+
if (base.search || base.hash)
|
|
10
|
+
throw new TypeError("Skyline assetsUrl must be a directory URL without a query or fragment.");
|
|
11
|
+
if (!base.pathname.endsWith("/"))
|
|
12
|
+
base.pathname += "/";
|
|
13
|
+
return base;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Imports the viewer module from the assets. Hosts bundle this file, but the module is
|
|
17
|
+
* served with the assets, so the comments ask webpack, Turbopack and Vite to leave the
|
|
18
|
+
* import to the browser; Rollup, esbuild and Bun leave an import of a variable alone.
|
|
19
|
+
*/
|
|
20
|
+
function importViewer(url) {
|
|
21
|
+
return import(/* webpackIgnore: true */ /* @vite-ignore */ /* turbopackIgnore: true */ url);
|
|
22
|
+
}
|
|
23
|
+
/** Resolves once `link` has loaded its stylesheet. */
|
|
24
|
+
function loaded(link) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
link.addEventListener("load", () => resolve(), { once: true });
|
|
27
|
+
link.addEventListener("error", () => reject(new Error(`Couldn't load ${link.href}.`)), { once: true });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
// Set on the element itself, where it outranks the host page's rules: `all: initial` keeps
|
|
31
|
+
// the host's rules for its elements (a `div` or `*` rule) off it, so nothing they set is
|
|
32
|
+
// inherited into the viewer, `direction` (which `all` leaves) keeps a right-to-left page from
|
|
33
|
+
// mirroring the viewer's layout, and the rest sizes it to fill the container.
|
|
34
|
+
const HOST_STYLE = "all:initial;direction:ltr;display:block;position:relative;isolation:isolate;width:100%;height:100%;overflow:hidden";
|
|
35
|
+
// The failure message's own look, since the viewer's stylesheets may be what failed.
|
|
36
|
+
const FAILURE_STYLE = "box-sizing:border-box;height:100%;margin:0;padding:24px;background:#000;color:#aaa;font:12px/1.5 ui-monospace,monospace";
|
|
37
|
+
/** Shows why the viewer couldn't start, in place of the viewer. */
|
|
38
|
+
function showFailure(root, error) {
|
|
39
|
+
const message = root.ownerDocument.createElement("p");
|
|
40
|
+
message.style.cssText = FAILURE_STYLE;
|
|
41
|
+
message.setAttribute("role", "alert");
|
|
42
|
+
message.textContent = `The Chicago skyline couldn't start: ${error instanceof Error ? error.message : String(error)}`;
|
|
43
|
+
root.replaceChildren(...[...root.querySelectorAll("link")].filter((link) => link.sheet), message);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Mounts the viewer in a new element appended to `container`, and returns at once: the
|
|
47
|
+
* element is in place, and `ready` settles once the 3D skyline shows or can't start.
|
|
48
|
+
*/
|
|
49
|
+
export function mountViewer(container, { assetsUrl, title, ...options }) {
|
|
50
|
+
const document = container.ownerDocument;
|
|
51
|
+
const assets = assetsBase(document, assetsUrl);
|
|
52
|
+
const element = document.createElement("div");
|
|
53
|
+
element.style.cssText = HOST_STYLE;
|
|
54
|
+
element.setAttribute("role", "region");
|
|
55
|
+
element.setAttribute("aria-label", title);
|
|
56
|
+
element.setAttribute("aria-busy", "true");
|
|
57
|
+
const root = element.attachShadow({ mode: "open" });
|
|
58
|
+
const stylesheets = STYLESHEETS.map((name) => {
|
|
59
|
+
const link = document.createElement("link");
|
|
60
|
+
link.rel = "stylesheet";
|
|
61
|
+
link.href = new URL(name, assets).href;
|
|
62
|
+
return link;
|
|
63
|
+
});
|
|
64
|
+
root.append(...stylesheets);
|
|
65
|
+
container.append(element);
|
|
66
|
+
// A host's engine that fails to load is the viewer's to report, once its scene asks for it
|
|
67
|
+
// (three-engine.ts); until then, or if the instance never gets that far, its rejection is
|
|
68
|
+
// handled here rather than left unhandled.
|
|
69
|
+
if (options.three)
|
|
70
|
+
Promise.resolve(options.three).catch(() => { });
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
let destroyed = false, mounted = false;
|
|
73
|
+
const context = { ...options, assets, signal: controller.signal };
|
|
74
|
+
const ready = new Promise((resolve, reject) => {
|
|
75
|
+
Promise.all([importViewer(new URL(VIEWER, assets).href), ...stylesheets.map(loaded)])
|
|
76
|
+
.then(async ([viewer]) => {
|
|
77
|
+
if (destroyed)
|
|
78
|
+
return;
|
|
79
|
+
mounted = true;
|
|
80
|
+
await viewer.mount(root, context);
|
|
81
|
+
})
|
|
82
|
+
.then(() => {
|
|
83
|
+
if (destroyed)
|
|
84
|
+
return;
|
|
85
|
+
element.removeAttribute("aria-busy");
|
|
86
|
+
resolve();
|
|
87
|
+
}, (error) => {
|
|
88
|
+
if (destroyed)
|
|
89
|
+
return;
|
|
90
|
+
element.removeAttribute("aria-busy");
|
|
91
|
+
// A viewer that mounted says why its scene can't start, and keeps its drawing;
|
|
92
|
+
// one that never loaded is replaced by the reason.
|
|
93
|
+
if (!mounted) {
|
|
94
|
+
controller.abort(error);
|
|
95
|
+
showFailure(root, error);
|
|
96
|
+
}
|
|
97
|
+
console.error("The Chicago skyline couldn't start:", error);
|
|
98
|
+
reject(error);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
// The element and the console report a failure, so a host that ignores `ready` sees no
|
|
102
|
+
// unhandled rejection.
|
|
103
|
+
ready.catch(() => { });
|
|
104
|
+
return {
|
|
105
|
+
element,
|
|
106
|
+
ready,
|
|
107
|
+
destroy() {
|
|
108
|
+
if (destroyed)
|
|
109
|
+
return;
|
|
110
|
+
destroyed = true;
|
|
111
|
+
controller.abort(new DOMException("The Chicago skyline was destroyed.", "AbortError"));
|
|
112
|
+
element.remove();
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { AmbientLight, BoxGeometry, BufferGeometry, Color, DataTexture, DirectionalLight, EdgesGeometry, Float32BufferAttribute, Group, LineBasicMaterial, LineSegments, Mesh, MeshBasicMaterial, MeshToonMaterial, NearestFilter, PCFShadowMap, PCFSoftShadowMap, OrthographicCamera, PerspectiveCamera, Raycaster, RedFormat, Scene, Vector2, Vector3, WebGLRenderer, } from "three";
|
|
2
|
+
export { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// `@a2f0/skyline/three`: the part of the host's own Three.js the viewer uses, for
|
|
2
|
+
// `mountSkyline`'s `three` option. Importing it needs the `three` peer (~0.186.0). Pass the
|
|
3
|
+
// module, or a dynamic import of it, so the viewer shares the host's engine:
|
|
4
|
+
//
|
|
5
|
+
// mountSkyline(container, { assetsUrl, three: import("@a2f0/skyline/three") });
|
|
6
|
+
//
|
|
7
|
+
// PCFSoftShadowMap, which r186 removed, is deprecated here: the viewer shadows with
|
|
8
|
+
// PCFShadowMap, and the old name stays exported so hosts that import it keep building.
|
|
9
|
+
export { AmbientLight, BoxGeometry, BufferGeometry, Color, DataTexture, DirectionalLight, EdgesGeometry, Float32BufferAttribute, Group, LineBasicMaterial, LineSegments, Mesh, MeshBasicMaterial, MeshToonMaterial, NearestFilter, PCFShadowMap, PCFSoftShadowMap, OrthographicCamera, PerspectiveCamera, Raycaster, RedFormat, Scene, Vector2, Vector3, WebGLRenderer, } from "three";
|
|
10
|
+
export { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type * as THREE from "three";
|
|
2
|
+
import type { OrbitControls } from "three/addons/controls/OrbitControls.js";
|
|
3
|
+
/**
|
|
4
|
+
* The part of Three.js the viewer uses: `@a2f0/skyline/three` exports exactly this from the
|
|
5
|
+
* host's `three`, so a host's bundler keeps no more of its engine than the viewer needs.
|
|
6
|
+
* The viewer shadows with `PCFShadowMap`; `PCFSoftShadowMap`, which r186 removed, stays
|
|
7
|
+
* listed only because earlier releases exported it.
|
|
8
|
+
*/
|
|
9
|
+
export type SkylineThree = Pick<typeof THREE, "AmbientLight" | "BoxGeometry" | "BufferGeometry" | "Color" | "DataTexture" | "DirectionalLight" | "EdgesGeometry" | "Float32BufferAttribute" | "Group" | "LineBasicMaterial" | "LineSegments" | "Mesh" | "MeshBasicMaterial" | "MeshToonMaterial" | "NearestFilter" | "PCFShadowMap" | "PCFSoftShadowMap" | "OrthographicCamera" | "PerspectiveCamera" | "Raycaster" | "RedFormat" | "Scene" | "Vector2" | "Vector3" | "WebGLRenderer"> & {
|
|
10
|
+
readonly OrbitControls: typeof OrbitControls;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Offers a host's engine to the scene. The page's first scene takes the engine once, when its
|
|
14
|
+
* code first loads: an engine offered later, or by a second instance, is not used.
|
|
15
|
+
*/
|
|
16
|
+
export declare function provideThree(three: SkylineThree | PromiseLike<SkylineThree>): void;
|
|
17
|
+
/** The host's engine, if one was offered. */
|
|
18
|
+
export declare function providedThree(): Promise<SkylineThree> | undefined;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
let provided;
|
|
2
|
+
/**
|
|
3
|
+
* Offers a host's engine to the scene. The page's first scene takes the engine once, when its
|
|
4
|
+
* code first loads: an engine offered later, or by a second instance, is not used.
|
|
5
|
+
*/
|
|
6
|
+
export function provideThree(three) {
|
|
7
|
+
provided ??= Promise.resolve(three);
|
|
8
|
+
}
|
|
9
|
+
/** The host's engine, if one was offered. */
|
|
10
|
+
export function providedThree() {
|
|
11
|
+
return provided;
|
|
12
|
+
}
|