@glissade/scene 0.37.0-pre.1 → 0.38.0-pre.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/dist/describe.js CHANGED
@@ -23,7 +23,7 @@ import { easings, listValueTypes } from "@glissade/core";
23
23
  * never pulled onto the base embed path — a scene that never calls `describe()`
24
24
  * pays zero bytes for it.
25
25
  */
26
- const RAW_VERSION = "0.37.0-pre.1";
26
+ const RAW_VERSION = "0.38.0-pre.0";
27
27
  const PACKAGE_VERSION = RAW_VERSION.includes("GLISSADE_".concat("VERSION")) ? "0.0.0-dev" : RAW_VERSION;
28
28
  /** Arity of a value type's numeric repr: vec2/vec2-arc → 2, number → 1; others (color/paint/path/string/boolean) carry no scalar arity. */
29
29
  function arityOf(type) {
@@ -435,6 +435,18 @@ const HELPERS = [
435
435
  import: "@glissade/scene/component",
436
436
  usage: "defineComponent({ name, props: { <p>: { type, required? } }, build(props, childId): Group }): (props & { id }) => { node: Group, id, childId(sub?), targets(child, prop) }"
437
437
  },
438
+ {
439
+ name: "Gauge",
440
+ summary: "Build-time radial gauge (data-viz, like Chart): a spec → N categorical stroked-arc zones + boundary ticks + a needle + separate labels, returning a Group. Angle deg: 0=up, +=clockwise. Needle takes AUTHORED keys (tl on targets(\"needle\",\"rotation\")) OR value→angle (Meter mode). Zones/ticks/needle/labels are each addressable sub-ids (zone-{i}, tick-{i}, needle, label-{i}, glow); labels draw z-above zones so a zone dim never crushes a label. Tree-shaken off the base scene index.",
441
+ import: "@glissade/scene/gauge",
442
+ usage: "Gauge({ id, radius, zones: { extent: [start,end], color, label?, labelStyle?: { family?, size?, fill?, weight? } }[], thickness?, gap?, needle?, needleAngle?, value?, domain?, sweep?, ticks?, apexEmphasis?: boolean | number, glow?, position? }): { node: Group, id, childId(sub?), targets(sub, prop): string[] }"
443
+ },
444
+ {
445
+ name: "Meter",
446
+ summary: "The Gauge value preset: a value (or () => value signal) mapped through domain across the sweep → the needle angle. Same result shape + sub-ids as Gauge. A function value binds live (the needle follows the signal). On the @glissade/scene/gauge subpath.",
447
+ import: "@glissade/scene/gauge",
448
+ usage: "Meter({ id, radius, zones, value: number | (() => number), domain?, sweep?, … }): { node: Group, id, childId, targets }"
449
+ },
438
450
  {
439
451
  name: "linearScale",
440
452
  summary: "A serializable linear scale (value axis): maps a numeric domain onto a pixel/unit range. Pair with Chart({ yScale }). On the @glissade/scene/chart subpath.",
@@ -0,0 +1,104 @@
1
+ import { a as Group } from "./nodes.js";
2
+
3
+ //#region src/gauge.d.ts
4
+
5
+ declare class GaugeError extends Error {
6
+ constructor(message: string);
7
+ }
8
+ /** One categorical zone: an angular `extent` (degrees), a color, an optional label. */
9
+ interface GaugeZone {
10
+ /** `[start, end]` in degrees (0 = up, + = clockwise). start < end. */
11
+ readonly extent: readonly [number, number];
12
+ /** the zone arc's stroke color. */
13
+ readonly color: string;
14
+ /** optional label drawn at the zone's mid-angle (its own addressable node). */
15
+ readonly label?: string;
16
+ /**
17
+ * per-label style override (family / size / fill / weight) — layered OVER the
18
+ * computed default (uniform size/fill + the apex size-up). Lets a consumer keep
19
+ * a per-episode text override AND restyle any single `label-{i}`.
20
+ */
21
+ readonly labelStyle?: {
22
+ readonly family?: string;
23
+ readonly size?: number;
24
+ readonly fill?: string;
25
+ readonly weight?: number;
26
+ };
27
+ }
28
+ interface GaugeSpec {
29
+ /** stable id — every child is namespaced under it. */
30
+ id: string;
31
+ /** arc radius (px), measured to the centerline of the zone band. */
32
+ radius: number;
33
+ /** the categorical zones, in angular order. At least one. */
34
+ zones: readonly GaugeZone[];
35
+ /** zone-arc stroke width (px). Default `radius * 0.14`. */
36
+ thickness?: number;
37
+ /** degrees trimmed off EACH side of every zone boundary (a visual gap). Default 0. */
38
+ gap?: number;
39
+ /** draw a needle (default true). Object form overrides its geometry. */
40
+ needle?: boolean | {
41
+ length?: number;
42
+ width?: number;
43
+ color?: string;
44
+ };
45
+ /** authored initial needle angle (deg, 0 = up). Ignored when `value` is set. */
46
+ needleAngle?: number;
47
+ /**
48
+ * Meter mode: a value (or `() => value` signal) mapped through `domain` across
49
+ * the gauge's total sweep → the needle angle. A function binds live (the needle
50
+ * follows the signal); a number sets the initial angle. Omit for authored mode.
51
+ */
52
+ value?: number | (() => number);
53
+ /** value domain for Meter mode (default `[0, 1]`). */
54
+ domain?: readonly [number, number];
55
+ /** total angular sweep `[start, end]` the value maps across. Default = zone union. */
56
+ sweep?: readonly [number, number];
57
+ /** draw boundary ticks at each distinct zone edge (default true). */
58
+ ticks?: boolean;
59
+ /** label font size (px). Default `radius * 0.13`. The mid zone's label is a size up. */
60
+ labelSize?: number;
61
+ /** label color. Default `#eaf1ff`. */
62
+ labelFill?: string;
63
+ /** label font family. Default `sans-serif`. */
64
+ fontFamily?: string;
65
+ /**
66
+ * emphasize the apex zone's label (the one straddling straight-up): `true`
67
+ * (default) = ×1.18 size + bold; `false` = no size-up (portrait-safe — the
68
+ * narrower stage can't afford the bump); a number = a custom size scale + bold.
69
+ * Gauge is a build-time factory and can't see the stage aspect, so gate this on
70
+ * your own `isPortrait(size)`.
71
+ */
72
+ apexEmphasis?: boolean | number;
73
+ /** add a center glow Circle (`glow` sub-id, opacity 0 — author animates it). */
74
+ glow?: boolean | {
75
+ color?: string;
76
+ radius?: number;
77
+ };
78
+ /** where to place the gauge center in the parent (default the parent origin). */
79
+ position?: readonly [number, number];
80
+ }
81
+ interface GaugeResult {
82
+ /** the built subtree (a Group whose id === the gauge id). */
83
+ readonly node: Group;
84
+ readonly id: string;
85
+ /** namespace a child id: `childId('needle')` → `'<id>/needle'`; no arg → the root id. */
86
+ childId(sub?: string): string;
87
+ /** ready-to-bind track targets: `targets('needle','rotation')` → `['<id>/needle/rotation']`. */
88
+ targets(sub: string, prop: string): string[];
89
+ }
90
+ /**
91
+ * Build a radial arc gauge. Returns the subtree + the child-id/target helpers.
92
+ * Pure: runs once at construction, emits ordinary nodes, nothing at play time.
93
+ */
94
+ declare function Gauge(spec: GaugeSpec): GaugeResult;
95
+ /**
96
+ * `Meter()` — the Gauge value preset: a single value (or signal) mapped through a
97
+ * domain to the needle across a colored track. Convenience over
98
+ * `Gauge({ value, domain, zones })`; the same result shape + sub-ids.
99
+ */
100
+ declare function Meter(spec: Omit<GaugeSpec, 'value'> & {
101
+ value: number | (() => number);
102
+ }): GaugeResult;
103
+ //#endregion
104
+ export { Gauge, GaugeError, GaugeResult, GaugeSpec, GaugeZone, Meter };
package/dist/gauge.js ADDED
@@ -0,0 +1,171 @@
1
+ import { a as Path, o as Rect, r as Group, s as Text, t as Circle } from "./nodes.js";
2
+ //#region src/gauge.ts
3
+ var GaugeError = class extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = "GaugeError";
7
+ }
8
+ };
9
+ const DEG = Math.PI / 180;
10
+ /** Linear remap of `v` from `domain` onto `range` (the value→angle map, Meter mode). */
11
+ function mapLinear(v, domain, range) {
12
+ const [d0, d1] = domain;
13
+ const [r0, r1] = range;
14
+ return d1 === d0 ? r0 : r0 + (v - d0) / (d1 - d0) * (r1 - r0);
15
+ }
16
+ /** A point on the gauge circle at angle `g` (deg, 0 = up, + = clockwise). */
17
+ function pointAt(radius, g) {
18
+ const r = g * DEG;
19
+ return [radius * Math.sin(r), -radius * Math.cos(r)];
20
+ }
21
+ /** An open polyline contour tracing the arc `a→b` at `radius`, sampled ≤ `step` deg. */
22
+ function arcContour(radius, a, b, step = 2) {
23
+ const n = Math.max(1, Math.ceil(Math.abs(b - a) / step));
24
+ const v = [];
25
+ for (let k = 0; k <= n; k++) v.push(pointAt(radius, a + (b - a) * k / n));
26
+ const zero = v.map(() => [0, 0]);
27
+ return [{
28
+ closed: false,
29
+ v,
30
+ in: zero,
31
+ out: zero
32
+ }];
33
+ }
34
+ /** A closed triangle pointing up (−y), tip at `(0,-len)`, base width `w`. */
35
+ function needleContour(len, w) {
36
+ const v = [
37
+ [0, -len],
38
+ [-w / 2, 0],
39
+ [w / 2, 0]
40
+ ];
41
+ const zero = v.map(() => [0, 0]);
42
+ return [{
43
+ closed: true,
44
+ v,
45
+ in: zero,
46
+ out: zero
47
+ }];
48
+ }
49
+ /**
50
+ * Build a radial arc gauge. Returns the subtree + the child-id/target helpers.
51
+ * Pure: runs once at construction, emits ordinary nodes, nothing at play time.
52
+ */
53
+ function Gauge(spec) {
54
+ const { id } = spec;
55
+ if (!id) throw new GaugeError("Gauge needs a stable id (every child is namespaced under it)");
56
+ if (!(spec.radius > 0)) throw new GaugeError(`Gauge '${id}': radius must be > 0 (got ${spec.radius})`);
57
+ if (!spec.zones || spec.zones.length === 0) throw new GaugeError(`Gauge '${id}': needs at least one zone`);
58
+ const radius = spec.radius;
59
+ const thickness = spec.thickness ?? radius * .14;
60
+ const gap = spec.gap ?? 0;
61
+ const cid = (sub) => sub === void 0 || sub === "" ? id : `${id}/${sub}`;
62
+ let lo = Infinity;
63
+ let hi = -Infinity;
64
+ spec.zones.forEach((z, i) => {
65
+ const [a, b] = z.extent;
66
+ if (!(a < b)) throw new GaugeError(`Gauge '${id}': zone ${i} extent must be [start < end] (got [${a}, ${b}])`);
67
+ lo = Math.min(lo, a);
68
+ hi = Math.max(hi, b);
69
+ });
70
+ const children = [];
71
+ if (spec.glow) {
72
+ const g = typeof spec.glow === "object" ? spec.glow : {};
73
+ children.push(new Circle({
74
+ id: cid("glow"),
75
+ radius: g.radius ?? radius * .5,
76
+ fill: g.color ?? "#ffffff",
77
+ opacity: 0
78
+ }));
79
+ }
80
+ spec.zones.forEach((z, i) => {
81
+ const [a, b] = z.extent;
82
+ children.push(new Path({
83
+ id: cid(`zone-${i}`),
84
+ data: arcContour(radius, a + gap / 2, b - gap / 2),
85
+ stroke: z.color,
86
+ strokeWidth: thickness
87
+ }));
88
+ });
89
+ if (spec.ticks !== false) {
90
+ const edges = /* @__PURE__ */ new Set();
91
+ spec.zones.forEach((z) => {
92
+ edges.add(z.extent[0]);
93
+ edges.add(z.extent[1]);
94
+ });
95
+ let t = 0;
96
+ for (const g of [...edges].sort((x, y) => x - y)) {
97
+ const [x, y] = pointAt(radius, g);
98
+ children.push(new Rect({
99
+ id: cid(`tick-${t++}`),
100
+ anchor: "center",
101
+ position: [x, y],
102
+ width: Math.max(1, thickness * .12),
103
+ height: thickness * 1.4,
104
+ rotation: g,
105
+ fill: "#ffffff"
106
+ }));
107
+ }
108
+ }
109
+ if (spec.needle !== false) {
110
+ const n = typeof spec.needle === "object" ? spec.needle : {};
111
+ const len = n.length ?? radius * .92;
112
+ const w = n.width ?? Math.max(4, radius * .05);
113
+ let rotation;
114
+ if (spec.value !== void 0) {
115
+ const sweep = spec.sweep ?? [lo, hi];
116
+ const domain = spec.domain ?? [0, 1];
117
+ rotation = typeof spec.value === "function" ? (() => mapLinear(spec.value(), domain, sweep)) : mapLinear(spec.value, domain, sweep);
118
+ } else rotation = spec.needleAngle ?? 0;
119
+ children.push(new Path({
120
+ id: cid("needle"),
121
+ data: needleContour(len, w),
122
+ fill: n.color ?? "#eaf1ff",
123
+ rotation
124
+ }));
125
+ }
126
+ const labelSize = spec.labelSize ?? radius * .13;
127
+ const labelFill = spec.labelFill ?? "#eaf1ff";
128
+ const fontFamily = spec.fontFamily ?? "sans-serif";
129
+ const apex = spec.apexEmphasis ?? true;
130
+ const apexScale = apex === false ? 1 : apex === true ? 1.18 : apex;
131
+ const apexWeight = apex === false ? 400 : 700;
132
+ spec.zones.forEach((z, i) => {
133
+ if (z.label === void 0) return;
134
+ const mid = (z.extent[0] + z.extent[1]) / 2;
135
+ const [x, y] = pointAt(radius + thickness / 2 + labelSize * .9, mid);
136
+ const isApex = z.extent[0] < 0 && z.extent[1] > 0;
137
+ const st = z.labelStyle ?? {};
138
+ children.push(new Text({
139
+ id: cid(`label-${i}`),
140
+ text: z.label,
141
+ position: [x, y],
142
+ anchor: "center",
143
+ align: "center",
144
+ fill: st.fill ?? labelFill,
145
+ fontFamily: st.family ?? fontFamily,
146
+ fontSize: st.size ?? (isApex ? labelSize * apexScale : labelSize),
147
+ fontWeight: st.weight ?? (isApex ? apexWeight : 400),
148
+ box: { valign: "center" }
149
+ }));
150
+ });
151
+ return {
152
+ node: new Group({
153
+ id,
154
+ children,
155
+ ...spec.position ? { position: [spec.position[0], spec.position[1]] } : {}
156
+ }),
157
+ id,
158
+ childId: cid,
159
+ targets: (sub, prop) => [`${cid(sub)}/${prop}`]
160
+ };
161
+ }
162
+ /**
163
+ * `Meter()` — the Gauge value preset: a single value (or signal) mapped through a
164
+ * domain to the needle across a colored track. Convenience over
165
+ * `Gauge({ value, domain, zones })`; the same result shape + sub-ids.
166
+ */
167
+ function Meter(spec) {
168
+ return Gauge(spec);
169
+ }
170
+ //#endregion
171
+ export { Gauge, GaugeError, Meter };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/scene",
3
- "version": "0.37.0-pre.1",
3
+ "version": "0.38.0-pre.0",
4
4
  "description": "glissade scene graph: nodes, transforms, DisplayList emission. Renderer-agnostic; zero DOM/Node dependencies.",
5
5
  "license": "Apache-2.0",
6
6
  "engines": {
@@ -31,6 +31,10 @@
31
31
  "types": "./dist/chart.d.ts",
32
32
  "default": "./dist/chart.js"
33
33
  },
34
+ "./gauge": {
35
+ "types": "./dist/gauge.d.ts",
36
+ "default": "./dist/gauge.js"
37
+ },
34
38
  "./component": {
35
39
  "types": "./dist/component.d.ts",
36
40
  "default": "./dist/component.js"
@@ -73,7 +77,7 @@
73
77
  ],
74
78
  "dependencies": {
75
79
  "yoga-layout": "^3.2.1",
76
- "@glissade/core": "0.37.0-pre.1"
80
+ "@glissade/core": "0.38.0-pre.0"
77
81
  },
78
82
  "repository": {
79
83
  "type": "git",