@johnmorrisdotca/uchusen-rockets 0.19.0 → 0.19.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.19.1] - 2026-11-12
6
+
7
+ - **The concept build budget holds work on CI** (MODULES-1134): `every concept builds inside the budget` failed on a shared runner at 3059 ms against 3000. On the Mac it still holds the time budget. Under `CI` it holds what does not vary, the pieces and triangles each concept builds (within 15 per cent), plus a cap of five times the budget on the time.
8
+
9
+ - **The WebGL probe runs once per page, and only when a view is about to draw** (MODULES-1102): every element asked at connect, and every 3D view made another test context for the name of the renderer, each a few seconds of the main thread in software drawing, and in headless WebKit a load now and then stalled for minutes. The page now makes one test context, when the first element comes on screen, answers both questions from it (does WebGL work, does it draw in software) and lets it go at once. The hero says `3d` where WebGL works and `fallback` where it does not, as before; an element far off screen no longer asks, so it says `picture` until it nears. A test holds the count at one.
10
+ - **The played-timeline freeze test counts the graphics card at rest** (MODULES-853). "Grid fins, legs and flaps" read 210 buffers before its forty rounds and 213 after in about one run in seven in the CI image. The three were the pad's smoke, one instanced draw kept for the flight, which reaches the card a frame or two after the first draw at ignition: a slow run moved on before it did, and a later round made it. The warm-up now holds at ignition until the count stands still, and both counts wait for the camera's glide and the left-behind craft to end. The allowance is unchanged; 15 of 15 in the CI image.
11
+
5
12
  ## [0.19.0] - 2026-11-11
6
13
 
7
14
  - **A tap on the build bar's Pause is no longer lost in Safari** (MODULES-1066): the playing bar redrew the button's icon and word every tenth of a second, and WebKit sends no click when the node a press began on has been replaced before the finger lifts. The bar now leaves a button, a text or the stills alone when a redraw would not change them. A test holds a press across redraws.
package/README.md CHANGED
@@ -442,7 +442,7 @@ variantOf("starship", "2019").facts.diameter.value; // null
442
442
 
443
443
  ## How to cite
444
444
 
445
- > Morris, J. (2026). *Uchusen Rockets* (version 0.19.0) [Computer software]. https://github.com/johnmorrisdotca/uchusen-rockets
445
+ > Morris, J. (2026). *Uchusen Rockets* (version 0.19.1) [Computer software]. https://github.com/johnmorrisdotca/uchusen-rockets
446
446
 
447
447
  GitHub's "Cite this repository" button reads [CITATION.cff](CITATION.cff) and gives BibTeX and APA. Cite the version you used (`npm ls @johnmorrisdotca/uchusen-rockets`), and credit the people behind the models and the numbers: [NOTICE.md](NOTICE.md) says who made each one and under what terms.
448
448
 
@@ -494,7 +494,7 @@ pnpm test:usage # every way to use it (CDN, Vite, webpack, React, Vue, Svelt
494
494
  pnpm test:usage # every way to use it (CDN, Vite, webpack, React, Vue, Svelte, Angular, iframe, TypeScript, Node), built and opened from the package as npm packs it
495
495
  pnpm docs:check # the reference site's checks: every export documented, every example run
496
496
  pnpm test:demo # the demo in a real browser
497
- pnpm dev # the demo on http://127.0.0.1:7900/uchusen-rockets/
497
+ pnpm dev # the demo on http://127.0.0.1:8275/uchusen-rockets/
498
498
  pnpm screenshots # the pictures in docs/images
499
499
  ```
500
500
 
@@ -0,0 +1,15 @@
1
+ /** What one look at the browser's WebGL said. */
2
+ export interface GlProbe {
3
+ /** Whether a WebGL context can be made at all. */
4
+ works: boolean;
5
+ /** Whether the context draws in software (SwiftShader, llvmpipe, softpipe), where smooth edges cost a frame. */
6
+ software: boolean;
7
+ }
8
+ /** How many test contexts this page has made: 1 once the answer is known, 0 before it is asked for. */
9
+ export declare function probeCount(): number;
10
+ /**
11
+ * Says whether WebGL works here and whether it draws in software. The first call makes one test context and releases it at once; every later call, from any element or viewer, gets the same answer without making another.
12
+ *
13
+ * @returns The kept answer.
14
+ */
15
+ export declare function glProbe(): GlProbe;
@@ -0,0 +1,46 @@
1
+ // What the browser's WebGL is, asked once per page. A test context in software drawing takes seconds of the main thread, so the answer is kept for every element and every viewer on the page, and the context is let go the moment it has answered.
2
+ let answer = null;
3
+ let asked = 0;
4
+ /** How many test contexts this page has made: 1 once the answer is known, 0 before it is asked for. */
5
+ export function probeCount() {
6
+ return asked;
7
+ }
8
+ /**
9
+ * Says whether WebGL works here and whether it draws in software. The first call makes one test context and releases it at once; every later call, from any element or viewer, gets the same answer without making another.
10
+ *
11
+ * @returns The kept answer.
12
+ */
13
+ export function glProbe() {
14
+ if (answer)
15
+ return answer;
16
+ asked++;
17
+ let works = false;
18
+ let software = false;
19
+ let canvas = null;
20
+ try {
21
+ canvas = document.createElement("canvas");
22
+ const two = canvas.getContext("webgl2");
23
+ const gl = two ?? canvas.getContext("webgl");
24
+ works = Boolean(gl);
25
+ if (gl) {
26
+ try {
27
+ const info = gl.getExtension("WEBGL_debug_renderer_info");
28
+ software = /swiftshader|llvmpipe|softpipe/i.test(info ? String(gl.getParameter(info.UNMASKED_RENDERER_WEBGL)) : "");
29
+ }
30
+ catch {
31
+ // The name is only a hint for smooth edges.
32
+ }
33
+ gl.getExtension("WEBGL_lose_context")?.loseContext();
34
+ }
35
+ }
36
+ catch {
37
+ works = false;
38
+ }
39
+ if (canvas) {
40
+ // A canvas of no size lets its drawing buffer go with it.
41
+ canvas.width = 0;
42
+ canvas.height = 0;
43
+ }
44
+ answer = { works, software };
45
+ return answer;
46
+ }
@@ -31,6 +31,7 @@ import { placeNameLabels } from "./namePlace.js";
31
31
  import { plinthGroup } from "./plinth.js";
32
32
  import { PartPlumes, PlumeKit } from "./plumes.js";
33
33
  import { ThrusterLayer } from "./thrusterLayer.js";
34
+ import { glProbe } from "./probe.js";
34
35
  import { craftJets, endQuads } from "../thrusters.js";
35
36
  /** The ships whose lamps turn the sun down in the Lit look. Kit modules stay in daylight. */
36
37
  const NIGHT_SHIPS = new Set(["fleet-ship-01", "fleet-kyotori", "fleet-drone", "fleet-ship-03", "fleet-ship-ariake", "fleet-02", "fleet-02-original", "fleet-kaminari", "fleet-emiake", "fleet-romaryu"]);
@@ -55,15 +56,8 @@ const COMPILE_MS = 4000;
55
56
  /** A change of craft, section or Fit eases the camera for this long, even in scale. */
56
57
  const SIZE_EASE_MS = 800;
57
58
  const autoRotateOf = (spin) => (spin.seconds <= 0 ? 0 : (60 / spin.seconds) * (spin.way === "cw" ? -1 : 1));
58
- /** A real graphics card keeps smooth edges. Software drawing spends the frame on them, and a heavy ship then misses a frame of the wait. */
59
- const smoothEdges = () => {
60
- const canvas = document.createElement("canvas");
61
- const gl = canvas.getContext("webgl2");
62
- const info = gl?.getExtension("WEBGL_debug_renderer_info");
63
- const name = info && gl ? String(gl.getParameter(info.UNMASKED_RENDERER_WEBGL)) : "";
64
- gl?.getExtension("WEBGL_lose_context")?.loseContext();
65
- return !/swiftshader|llvmpipe|softpipe/i.test(name);
66
- };
59
+ /** A real graphics card keeps smooth edges. Software drawing spends the frame on them, and a heavy ship then misses a frame of the wait. The page's one WebGL probe says which this is. */
60
+ const smoothEdges = () => !glProbe().software;
67
61
  const LINE_VERTEX = /* glsl */ `
68
62
  varying vec3 vNormal;
69
63
  varying vec2 vUv;
package/dist/element.d.ts CHANGED
@@ -65,6 +65,8 @@ export declare class UchusenRocket extends HTMLBase {
65
65
  private stateNow;
66
66
  private shownAt;
67
67
  private starting;
68
+ /** Whether this element has asked the page's WebGL answer since it started watching. */
69
+ private glAsked;
68
70
  private key;
69
71
  private driven;
70
72
  private placeFrame;
package/dist/element.js CHANGED
@@ -21,6 +21,7 @@ import { ROCKETS, SLOT_NAMES } from "./rockets.js";
21
21
  import { tiltMode, tiltTurn } from "./tilt.js";
22
22
  import { skyQuery } from "./element/solarGroups.js";
23
23
  import { Panel } from "./element/panel.js";
24
+ import { glProbe } from "./element/probe.js";
24
25
  import { spinOf } from "./spin.js";
25
26
  const HTMLBase = typeof HTMLElement === "undefined" ? class {
26
27
  } : HTMLElement;
@@ -94,29 +95,13 @@ function adopt(root, css, holder) {
94
95
  const rocketSheet = { sheet: null };
95
96
  const lineupSheet = { sheet: null };
96
97
  const media = (query) => typeof matchMedia === "function" && matchMedia(query).matches;
97
- let webgl = null;
98
- function webglWorks() {
99
- if (webgl !== null)
100
- return webgl;
101
- try {
102
- const probe = document.createElement("canvas");
103
- const gl = probe.getContext("webgl2") ?? probe.getContext("webgl");
104
- webgl = Boolean(gl);
105
- gl?.getExtension("WEBGL_lose_context")?.loseContext();
106
- }
107
- catch {
108
- webgl = false;
109
- }
110
- return webgl;
111
- }
98
+ /** Why the 3D view is not wanted, from what is known without making a WebGL context. Whether WebGL works is asked once, when the first element comes on screen. */
112
99
  function whyNot(mode) {
113
100
  if (mode === "drawing")
114
101
  return "off";
115
102
  const connection = navigator.connection;
116
103
  if (connection?.saveData || media("(prefers-reduced-data: reduce)"))
117
104
  return "reduced-data";
118
- if (!webglWorks())
119
- return "no-webgl";
120
105
  return "";
121
106
  }
122
107
  /**
@@ -154,6 +139,8 @@ export class UchusenRocket extends HTMLBase {
154
139
  stateNow = "picture";
155
140
  shownAt = 0;
156
141
  starting = false;
142
+ /** Whether this element has asked the page's WebGL answer since it started watching. */
143
+ glAsked = false;
157
144
  key = "";
158
145
  driven = null;
159
146
  placeFrame = 0;
@@ -900,7 +887,8 @@ export class UchusenRocket extends HTMLBase {
900
887
  }
901
888
  startWatching() {
902
889
  this.watch?.disconnect();
903
- const reason = whyNot(this.getAttribute("mode") ?? "auto");
890
+ let reason = whyNot(this.getAttribute("mode") ?? "auto");
891
+ this.glAsked = false;
904
892
  if (reason) {
905
893
  this.setState(reason === "off" ? "picture" : "fallback", reason);
906
894
  if (reason !== "off")
@@ -916,6 +904,16 @@ export class UchusenRocket extends HTMLBase {
916
904
  this.kickFlight();
917
905
  if (reason)
918
906
  return;
907
+ if (seen.isIntersecting && !this.glAsked) {
908
+ // The probe makes a context, which software drawing takes seconds over: it waits until a view is about to draw, and is made once for the whole page.
909
+ this.glAsked = true;
910
+ if (!glProbe().works) {
911
+ reason = "no-webgl";
912
+ this.setState("fallback", reason);
913
+ this.emit("uchusen-fallback", { reason });
914
+ return;
915
+ }
916
+ }
919
917
  if (seen.isIntersecting) {
920
918
  this.shownAt ||= performance.now();
921
919
  window.clearTimeout(this.releaseTimer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/uchusen-rockets",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
4
4
  "description": "Uchusen Rockets 宇宙船: line drawings and 3D views of rockets, as a web component. Every variant with its own sourced sizes, a typed way to assemble a rocket from parts, stack and separate. No logos. three.js is a peer dependency.",
5
5
  "keywords": [
6
6
  "rocket",