bireactive 0.2.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.
Files changed (225) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +81 -0
  3. package/dist/animation/anim.d.ts +57 -0
  4. package/dist/animation/anim.js +318 -0
  5. package/dist/animation/combinators.d.ts +39 -0
  6. package/dist/animation/combinators.js +113 -0
  7. package/dist/animation/easings.d.ts +5 -0
  8. package/dist/animation/easings.js +5 -0
  9. package/dist/animation/index.d.ts +3 -0
  10. package/dist/animation/index.js +3 -0
  11. package/dist/assert/algebra.d.ts +20 -0
  12. package/dist/assert/algebra.js +79 -0
  13. package/dist/assert/claim.d.ts +40 -0
  14. package/dist/assert/claim.js +129 -0
  15. package/dist/assert/index.d.ts +7 -0
  16. package/dist/assert/index.js +19 -0
  17. package/dist/assert/predicates.d.ts +18 -0
  18. package/dist/assert/predicates.js +43 -0
  19. package/dist/assert/record.d.ts +20 -0
  20. package/dist/assert/record.js +78 -0
  21. package/dist/assert/scope.d.ts +42 -0
  22. package/dist/assert/scope.js +233 -0
  23. package/dist/assert/span.d.ts +37 -0
  24. package/dist/assert/span.js +68 -0
  25. package/dist/assert/tree.d.ts +22 -0
  26. package/dist/assert/tree.js +65 -0
  27. package/dist/code/code.d.ts +70 -0
  28. package/dist/code/code.js +361 -0
  29. package/dist/code/index.d.ts +2 -0
  30. package/dist/code/index.js +9 -0
  31. package/dist/code/morph.d.ts +5 -0
  32. package/dist/code/morph.js +194 -0
  33. package/dist/code/tokenize.d.ts +8 -0
  34. package/dist/code/tokenize.js +51 -0
  35. package/dist/constraints/cluster.d.ts +83 -0
  36. package/dist/constraints/cluster.js +213 -0
  37. package/dist/constraints/drivers.d.ts +15 -0
  38. package/dist/constraints/drivers.js +40 -0
  39. package/dist/constraints/factories.d.ts +73 -0
  40. package/dist/constraints/factories.js +248 -0
  41. package/dist/constraints/index.d.ts +11 -0
  42. package/dist/constraints/index.js +39 -0
  43. package/dist/constraints/interaction.d.ts +21 -0
  44. package/dist/constraints/interaction.js +148 -0
  45. package/dist/constraints/linalg.d.ts +18 -0
  46. package/dist/constraints/linalg.js +141 -0
  47. package/dist/constraints/phases.d.ts +21 -0
  48. package/dist/constraints/phases.js +60 -0
  49. package/dist/constraints/physics.d.ts +34 -0
  50. package/dist/constraints/physics.js +128 -0
  51. package/dist/constraints/rigid.d.ts +210 -0
  52. package/dist/constraints/rigid.js +835 -0
  53. package/dist/constraints/solver.d.ts +107 -0
  54. package/dist/constraints/solver.js +510 -0
  55. package/dist/constraints/term.d.ts +50 -0
  56. package/dist/constraints/term.js +80 -0
  57. package/dist/constraints/terms.d.ts +80 -0
  58. package/dist/constraints/terms.js +302 -0
  59. package/dist/constraints/world.d.ts +31 -0
  60. package/dist/constraints/world.js +245 -0
  61. package/dist/core/aggregates.d.ts +64 -0
  62. package/dist/core/aggregates.js +198 -0
  63. package/dist/core/anim.d.ts +84 -0
  64. package/dist/core/anim.js +301 -0
  65. package/dist/core/index.d.ts +38 -0
  66. package/dist/core/index.js +38 -0
  67. package/dist/core/introspect.d.ts +5 -0
  68. package/dist/core/introspect.js +31 -0
  69. package/dist/core/lenses/closed-form-policies.d.ts +64 -0
  70. package/dist/core/lenses/closed-form-policies.js +452 -0
  71. package/dist/core/lenses/domain-aggregates.d.ts +54 -0
  72. package/dist/core/lenses/domain-aggregates.js +259 -0
  73. package/dist/core/lenses/factor-lens.d.ts +42 -0
  74. package/dist/core/lenses/factor-lens.js +419 -0
  75. package/dist/core/lenses/index.d.ts +5 -0
  76. package/dist/core/lenses/index.js +16 -0
  77. package/dist/core/lenses/memory.d.ts +47 -0
  78. package/dist/core/lenses/memory.js +102 -0
  79. package/dist/core/lenses/typed-factor.d.ts +45 -0
  80. package/dist/core/lenses/typed-factor.js +376 -0
  81. package/dist/core/network-utils.d.ts +14 -0
  82. package/dist/core/network-utils.js +62 -0
  83. package/dist/core/new-primitives.d.ts +33 -0
  84. package/dist/core/new-primitives.js +113 -0
  85. package/dist/core/signal.d.ts +254 -0
  86. package/dist/core/signal.js +1349 -0
  87. package/dist/core/traits.d.ts +61 -0
  88. package/dist/core/traits.js +56 -0
  89. package/dist/core/tree.d.ts +23 -0
  90. package/dist/core/tree.js +62 -0
  91. package/dist/core/values/anchor.d.ts +23 -0
  92. package/dist/core/values/anchor.js +23 -0
  93. package/dist/core/values/audio.d.ts +33 -0
  94. package/dist/core/values/audio.js +107 -0
  95. package/dist/core/values/bool.d.ts +37 -0
  96. package/dist/core/values/bool.js +75 -0
  97. package/dist/core/values/box.d.ts +77 -0
  98. package/dist/core/values/box.js +211 -0
  99. package/dist/core/values/canvas.d.ts +71 -0
  100. package/dist/core/values/canvas.js +495 -0
  101. package/dist/core/values/color.d.ts +49 -0
  102. package/dist/core/values/color.js +106 -0
  103. package/dist/core/values/flags.d.ts +18 -0
  104. package/dist/core/values/flags.js +50 -0
  105. package/dist/core/values/gpu.d.ts +74 -0
  106. package/dist/core/values/gpu.js +426 -0
  107. package/dist/core/values/matrix.d.ts +53 -0
  108. package/dist/core/values/matrix.js +140 -0
  109. package/dist/core/values/num.d.ts +62 -0
  110. package/dist/core/values/num.js +166 -0
  111. package/dist/core/values/pose.d.ts +31 -0
  112. package/dist/core/values/pose.js +83 -0
  113. package/dist/core/values/range.d.ts +83 -0
  114. package/dist/core/values/range.js +167 -0
  115. package/dist/core/values/str.d.ts +76 -0
  116. package/dist/core/values/str.js +346 -0
  117. package/dist/core/values/template.d.ts +49 -0
  118. package/dist/core/values/template.js +148 -0
  119. package/dist/core/values/transform.d.ts +49 -0
  120. package/dist/core/values/transform.js +115 -0
  121. package/dist/core/values/tri.d.ts +31 -0
  122. package/dist/core/values/tri.js +95 -0
  123. package/dist/core/values/vec.d.ts +72 -0
  124. package/dist/core/values/vec.js +219 -0
  125. package/dist/core/writable.d.ts +15 -0
  126. package/dist/core/writable.js +29 -0
  127. package/dist/ext/events.d.ts +10 -0
  128. package/dist/ext/events.js +31 -0
  129. package/dist/ext/index.d.ts +4 -0
  130. package/dist/ext/index.js +4 -0
  131. package/dist/ext/snapshot.d.ts +8 -0
  132. package/dist/ext/snapshot.js +29 -0
  133. package/dist/ext/timeline.d.ts +56 -0
  134. package/dist/ext/timeline.js +94 -0
  135. package/dist/ext/waapi.d.ts +25 -0
  136. package/dist/ext/waapi.js +198 -0
  137. package/dist/index.d.ts +8 -0
  138. package/dist/index.js +10 -0
  139. package/dist/propagators/index.d.ts +6 -0
  140. package/dist/propagators/index.js +6 -0
  141. package/dist/propagators/layout.d.ts +68 -0
  142. package/dist/propagators/layout.js +336 -0
  143. package/dist/propagators/network.d.ts +52 -0
  144. package/dist/propagators/network.js +185 -0
  145. package/dist/propagators/propagator.d.ts +12 -0
  146. package/dist/propagators/propagator.js +16 -0
  147. package/dist/propagators/range.d.ts +45 -0
  148. package/dist/propagators/range.js +147 -0
  149. package/dist/propagators/relations.d.ts +60 -0
  150. package/dist/propagators/relations.js +343 -0
  151. package/dist/shapes/annular-sector.d.ts +15 -0
  152. package/dist/shapes/annular-sector.js +64 -0
  153. package/dist/shapes/button.d.ts +14 -0
  154. package/dist/shapes/button.js +31 -0
  155. package/dist/shapes/choreographers.d.ts +22 -0
  156. package/dist/shapes/choreographers.js +69 -0
  157. package/dist/shapes/circle.d.ts +17 -0
  158. package/dist/shapes/circle.js +57 -0
  159. package/dist/shapes/clip.d.ts +5 -0
  160. package/dist/shapes/clip.js +31 -0
  161. package/dist/shapes/connect.d.ts +16 -0
  162. package/dist/shapes/connect.js +70 -0
  163. package/dist/shapes/curve.d.ts +60 -0
  164. package/dist/shapes/curve.js +285 -0
  165. package/dist/shapes/dashed.d.ts +16 -0
  166. package/dist/shapes/dashed.js +142 -0
  167. package/dist/shapes/debug.d.ts +43 -0
  168. package/dist/shapes/debug.js +97 -0
  169. package/dist/shapes/group.d.ts +5 -0
  170. package/dist/shapes/group.js +10 -0
  171. package/dist/shapes/handle.d.ts +32 -0
  172. package/dist/shapes/handle.js +88 -0
  173. package/dist/shapes/index.d.ts +23 -0
  174. package/dist/shapes/index.js +23 -0
  175. package/dist/shapes/interaction.d.ts +32 -0
  176. package/dist/shapes/interaction.js +187 -0
  177. package/dist/shapes/label.d.ts +20 -0
  178. package/dist/shapes/label.js +42 -0
  179. package/dist/shapes/layout.d.ts +29 -0
  180. package/dist/shapes/layout.js +74 -0
  181. package/dist/shapes/line.d.ts +21 -0
  182. package/dist/shapes/line.js +79 -0
  183. package/dist/shapes/list.d.ts +18 -0
  184. package/dist/shapes/list.js +51 -0
  185. package/dist/shapes/mount.d.ts +7 -0
  186. package/dist/shapes/mount.js +10 -0
  187. package/dist/shapes/path.d.ts +77 -0
  188. package/dist/shapes/path.js +227 -0
  189. package/dist/shapes/rect.d.ts +30 -0
  190. package/dist/shapes/rect.js +131 -0
  191. package/dist/shapes/shape.d.ts +132 -0
  192. package/dist/shapes/shape.js +306 -0
  193. package/dist/shapes/text.d.ts +24 -0
  194. package/dist/shapes/text.js +53 -0
  195. package/dist/shapes/tokens.d.ts +28 -0
  196. package/dist/shapes/tokens.js +27 -0
  197. package/dist/shapes/transitions.d.ts +23 -0
  198. package/dist/shapes/transitions.js +62 -0
  199. package/dist/tex/decorations.d.ts +26 -0
  200. package/dist/tex/decorations.js +116 -0
  201. package/dist/tex/index.d.ts +5 -0
  202. package/dist/tex/index.js +5 -0
  203. package/dist/tex/marker.d.ts +17 -0
  204. package/dist/tex/marker.js +63 -0
  205. package/dist/tex/motion.d.ts +43 -0
  206. package/dist/tex/motion.js +290 -0
  207. package/dist/tex/parts.d.ts +65 -0
  208. package/dist/tex/parts.js +149 -0
  209. package/dist/tex/tex.d.ts +45 -0
  210. package/dist/tex/tex.js +244 -0
  211. package/dist/web/attr.d.ts +16 -0
  212. package/dist/web/attr.js +98 -0
  213. package/dist/web/diagram.d.ts +49 -0
  214. package/dist/web/diagram.js +260 -0
  215. package/dist/web/index.d.ts +6 -0
  216. package/dist/web/index.js +6 -0
  217. package/dist/web/md-marker.d.ts +6 -0
  218. package/dist/web/md-marker.js +39 -0
  219. package/dist/web/md-tex.d.ts +6 -0
  220. package/dist/web/md-tex.js +61 -0
  221. package/dist/web/raf.d.ts +6 -0
  222. package/dist/web/raf.js +24 -0
  223. package/dist/web/viewport.d.ts +7 -0
  224. package/dist/web/viewport.js +13 -0
  225. package/package.json +87 -0
@@ -0,0 +1,16 @@
1
+ import { type Val, type Vec } from "../core/index.js";
2
+ import { Line, type LineOpts } from "./line.js";
3
+ import { Shape } from "./shape.js";
4
+ /** Line between two shapes/points; shape endpoints meet the analytic
5
+ * boundary. */
6
+ export declare function connect(a: Shape | Vec, b: Shape | Vec, opts?: LineOpts): Line;
7
+ export interface ArrowOpts extends LineOpts {
8
+ /** Standoff between visible line and source/target. Default 4. */
9
+ gap?: Val<number>;
10
+ }
11
+ /** Arrow from `a` to `b`. Endpoints are adjusted so the round cap lines
12
+ * up gap-ish past the source and the tip lands gap-ish before the
13
+ * target (the marker extends past the line end). */
14
+ export declare function arrow(a: Shape | Vec, b: Shape | Vec, opts?: ArrowOpts): Line;
15
+ /** Idempotently install the arrow `<marker>` in this SVG's `<defs>`. */
16
+ export declare function ensureArrowMarker(svg: SVGSVGElement): void;
@@ -0,0 +1,70 @@
1
+ // Connectors. Uses `shape.boundary` so analytic edges work without
2
+ // per-kind dispatch.
3
+ import { derive, reader } from "../core/index.js";
4
+ import { Line } from "./line.js";
5
+ import { Shape, SVG_NS } from "./shape.js";
6
+ import { tokens } from "./tokens.js";
7
+ const ARROW_ID = "bireactive-arrow";
8
+ const ARROW_W = 10;
9
+ const ARROW_GAP_DEFAULT = 4;
10
+ /** Line between two shapes/points; shape endpoints meet the analytic
11
+ * boundary. */
12
+ export function connect(a, b, opts) {
13
+ const aP = a instanceof Shape ? a.boundary(b instanceof Shape ? b.center : b) : a;
14
+ const bP = b instanceof Shape ? b.boundary(a instanceof Shape ? a.center : a) : b;
15
+ return new Line(aP, bP, opts);
16
+ }
17
+ /** Arrow from `a` to `b`. Endpoints are adjusted so the round cap lines
18
+ * up gap-ish past the source and the tip lands gap-ish before the
19
+ * target (the marker extends past the line end). */
20
+ export function arrow(a, b, opts = {}) {
21
+ const aBase = a instanceof Shape ? a.boundary(b instanceof Shape ? b.center : b) : a;
22
+ const bBase = b instanceof Shape ? b.boundary(a instanceof Shape ? a.center : a) : b;
23
+ const gap = reader(opts.gap ?? ARROW_GAP_DEFAULT);
24
+ const dir = bBase.sub(aBase).normalize();
25
+ const aP = aBase.add(dir.scale(derive(() => gap() + tokens.weight)));
26
+ const bP = bBase.sub(dir.scale(derive(() => gap() + ARROW_W)));
27
+ const line = new Line(aP, bP, opts);
28
+ line.attr("marker-end", `url(#${ARROW_ID})`);
29
+ return line;
30
+ }
31
+ /** Idempotently install the arrow `<marker>` in this SVG's `<defs>`. */
32
+ export function ensureArrowMarker(svg) {
33
+ let defs = svg.querySelector(":scope > defs");
34
+ if (!defs) {
35
+ defs = document.createElementNS(SVG_NS, "defs");
36
+ svg.insertBefore(defs, svg.firstChild);
37
+ }
38
+ if (defs.querySelector(`#${ARROW_ID}`))
39
+ return;
40
+ const marker = document.createElementNS(SVG_NS, "marker");
41
+ marker.id = ARROW_ID;
42
+ marker.setAttribute("markerWidth", "10");
43
+ marker.setAttribute("markerHeight", "7");
44
+ marker.setAttribute("refX", "0");
45
+ marker.setAttribute("refY", "3.5");
46
+ marker.setAttribute("orient", "auto");
47
+ marker.setAttribute("markerUnits", "userSpaceOnUse");
48
+ // Triangle with rounded vertices: trace each edge from its rounded
49
+ // start to its rounded end, then a short Q through each vertex.
50
+ const r = 0.9;
51
+ const v0 = { x: 0, y: 0 };
52
+ const v1 = { x: 10, y: 3.5 };
53
+ const v2 = { x: 0, y: 7 };
54
+ const corner = (v, prev, next) => {
55
+ const dPrev = Math.hypot(prev.x - v.x, prev.y - v.y);
56
+ const dNext = Math.hypot(next.x - v.x, next.y - v.y);
57
+ return {
58
+ approach: { x: v.x + (r * (prev.x - v.x)) / dPrev, y: v.y + (r * (prev.y - v.y)) / dPrev },
59
+ depart: { x: v.x + (r * (next.x - v.x)) / dNext, y: v.y + (r * (next.y - v.y)) / dNext },
60
+ };
61
+ };
62
+ const c0 = corner(v0, v2, v1);
63
+ const c1 = corner(v1, v0, v2);
64
+ const c2 = corner(v2, v1, v0);
65
+ const path = document.createElementNS(SVG_NS, "path");
66
+ path.setAttribute("d", `M ${c0.depart.x} ${c0.depart.y} L ${c1.approach.x} ${c1.approach.y} Q ${v1.x} ${v1.y} ${c1.depart.x} ${c1.depart.y} L ${c2.approach.x} ${c2.approach.y} Q ${v2.x} ${v2.y} ${c2.depart.x} ${c2.depart.y} L ${c0.approach.x} ${c0.approach.y} Q ${v0.x} ${v0.y} ${c0.depart.x} ${c0.depart.y} Z`);
67
+ path.setAttribute("fill", tokens.stroke);
68
+ marker.appendChild(path);
69
+ defs.appendChild(marker);
70
+ }
@@ -0,0 +1,60 @@
1
+ import { type Cell, type Inner, type Val, Vec } from "../core/index.js";
2
+ import { type CommonOpts, Shape } from "./shape.js";
3
+ type V = Inner<Vec>;
4
+ export interface CurveOpts extends CommonOpts {
5
+ closed?: boolean;
6
+ }
7
+ /** One curve piece: an SVG path command plus closed-form sampling by `s ∈ [0,1]`. */
8
+ export type CurveSegment = {
9
+ kind: "line";
10
+ from: V;
11
+ to: V;
12
+ } | {
13
+ kind: "ellipseArc";
14
+ /** Centre of the (possibly rotated) ellipse. */
15
+ center: V;
16
+ /** Semi-axes along the rotated frame. */
17
+ a: number;
18
+ b: number;
19
+ /** Rotation of the ellipse's major axis from +x, in radians. */
20
+ rotation: number;
21
+ /** Start / end parameter angles in the rotated frame, radians. */
22
+ a0: number;
23
+ a1: number;
24
+ };
25
+ /** Initialiser: static array (writable, fluent) or reactive builder
26
+ * (computed, immutable). */
27
+ export type CurveInit = readonly CurveSegment[] | (() => readonly CurveSegment[]);
28
+ /** Piecewise curve with line and ellipse-arc segments. */
29
+ export declare class Curve<O extends CurveOpts = CurveOpts> extends Shape<O> {
30
+ private readonly _segments;
31
+ /** True if segments are reactive (computed); fluent methods then no-op. */
32
+ private readonly _reactive;
33
+ readonly closed: boolean;
34
+ readonly length: Cell<number>;
35
+ constructor(init?: CurveInit, opts?: O);
36
+ /** Untracked snapshot of the segment list. */
37
+ get segs(): readonly CurveSegment[];
38
+ /** Segments for the dashed renderer: lines pass through, circular arcs
39
+ * (`a ≈ b`, rotation ≈ 0) become native arcs, other ellipse arcs tessellate. */
40
+ segments(): import("./shape.js").Segment[];
41
+ /** Sample at `t ∈ [0, 1]` along arc length. */
42
+ pointAt(t: Val<number>): Vec;
43
+ /** Unit tangent at `t ∈ [0, 1]`. */
44
+ tangentAt(t: Val<number>): Vec;
45
+ private push;
46
+ private get last();
47
+ /** Append a line segment to `p`. */
48
+ to(p: V): this;
49
+ /** Append an elliptic-arc segment. `a0`, `a1` are parametric angles
50
+ * in the ellipse's rotated frame. */
51
+ ellipseArc(center: V, a: number, b: number, rotation: number, a0: number, a1: number): this;
52
+ }
53
+ /** Start a piecewise curve: `curve()` fluent, `curve([segs])` explicit, or
54
+ * `curve(() => segs)` reactive. */
55
+ export declare function curve<const O extends CurveOpts>(init?: CurveInit, opts?: O): Curve<O>;
56
+ /** A closed ellipse centred at `center` with semi-axes `a, b` and
57
+ * optional `rotation` (radians). All four args accept `Val<>`, so the
58
+ * ellipse re-renders when any input changes. */
59
+ export declare function ellipse<O extends CurveOpts>(center: Val<V>, a: Val<number>, b: Val<number>, rotation?: Val<number>, opts?: O): Curve<O>;
60
+ export {};
@@ -0,0 +1,285 @@
1
+ // curve.ts — reactive piecewise curve with line + elliptic-arc segments.
2
+ //
3
+ // Segments render as SVG `d` chunks (`L`/`A`) and sample in closed form (arc
4
+ // length exact for lines, 16-sample for ellipse arcs). Static mode takes an
5
+ // array with fluent `.to`/`.ellipseArc`; reactive mode takes a builder fn.
6
+ import { cell, derive, reader, readNow, Vec, } from "../core/index.js";
7
+ import { Shape } from "./shape.js";
8
+ const TAU = Math.PI * 2;
9
+ const clamp01 = (v) => (v < 0 ? 0 : v > 1 ? 1 : v);
10
+ const ARC_SAMPLES = 16;
11
+ /** Endpoint of a segment at local s ∈ [0, 1]. Closed-form. */
12
+ function sampleSegment(seg, s) {
13
+ if (seg.kind === "line") {
14
+ return {
15
+ x: seg.from.x + (seg.to.x - seg.from.x) * s,
16
+ y: seg.from.y + (seg.to.y - seg.from.y) * s,
17
+ };
18
+ }
19
+ const t = seg.a0 + (seg.a1 - seg.a0) * s;
20
+ const cosT = Math.cos(t);
21
+ const sinT = Math.sin(t);
22
+ const cosR = Math.cos(seg.rotation);
23
+ const sinR = Math.sin(seg.rotation);
24
+ return {
25
+ x: seg.center.x + seg.a * cosT * cosR - seg.b * sinT * sinR,
26
+ y: seg.center.y + seg.a * cosT * sinR + seg.b * sinT * cosR,
27
+ };
28
+ }
29
+ /** Unit tangent at local s. Closed-form derivative of `sampleSegment`. */
30
+ function tangentSegment(seg, s) {
31
+ if (seg.kind === "line") {
32
+ const dx = seg.to.x - seg.from.x;
33
+ const dy = seg.to.y - seg.from.y;
34
+ const len = Math.hypot(dx, dy) || 1;
35
+ return { x: dx / len, y: dy / len };
36
+ }
37
+ const t = seg.a0 + (seg.a1 - seg.a0) * s;
38
+ const cosT = Math.cos(t);
39
+ const sinT = Math.sin(t);
40
+ const cosR = Math.cos(seg.rotation);
41
+ const sinR = Math.sin(seg.rotation);
42
+ let dx = -seg.a * sinT * cosR - seg.b * cosT * sinR;
43
+ let dy = -seg.a * sinT * sinR + seg.b * cosT * cosR;
44
+ const len = Math.hypot(dx, dy) || 1;
45
+ dx /= len;
46
+ dy /= len;
47
+ return { x: dx, y: dy };
48
+ }
49
+ /** Arc length of one segment. Exact for lines, 16-sample for ellipse arcs. */
50
+ function segmentLength(seg) {
51
+ if (seg.kind === "line") {
52
+ return Math.hypot(seg.to.x - seg.from.x, seg.to.y - seg.from.y);
53
+ }
54
+ const N = ARC_SAMPLES;
55
+ let acc = 0;
56
+ let prev = sampleSegment(seg, 0);
57
+ for (let i = 1; i <= N; i++) {
58
+ const p = sampleSegment(seg, i / N);
59
+ acc += Math.hypot(p.x - prev.x, p.y - prev.y);
60
+ prev = p;
61
+ }
62
+ return acc;
63
+ }
64
+ /** SVG `d` chunk for one segment. Ellipse arcs spanning more than π
65
+ * are split at the midpoint so SVG's `large-arc-flag` stays unambiguous. */
66
+ function segmentD(seg) {
67
+ if (seg.kind === "line") {
68
+ return `L ${seg.to.x} ${seg.to.y}`;
69
+ }
70
+ const { a, b, rotation, a0, a1 } = seg;
71
+ const sweep = a1 >= a0 ? 1 : 0;
72
+ const span = Math.abs(a1 - a0);
73
+ const rotDeg = (rotation * 180) / Math.PI;
74
+ if (span <= Math.PI + 1e-9) {
75
+ const large = span > Math.PI ? 1 : 0;
76
+ const end = sampleSegment(seg, 1);
77
+ return `A ${a} ${b} ${rotDeg} ${large} ${sweep} ${end.x} ${end.y}`;
78
+ }
79
+ const mid = sampleSegment(seg, 0.5);
80
+ const end = sampleSegment(seg, 1);
81
+ return (`A ${a} ${b} ${rotDeg} 0 ${sweep} ${mid.x} ${mid.y} ` +
82
+ `A ${a} ${b} ${rotDeg} 0 ${sweep} ${end.x} ${end.y}`);
83
+ }
84
+ function segmentStart(seg) {
85
+ return seg.kind === "line" ? seg.from : sampleSegment(seg, 0);
86
+ }
87
+ /** Piecewise curve with line and ellipse-arc segments. */
88
+ export class Curve extends Shape {
89
+ _segments;
90
+ /** True if segments are reactive (computed); fluent methods then no-op. */
91
+ _reactive;
92
+ closed;
93
+ length;
94
+ constructor(init = [], opts = {}) {
95
+ const reactive = typeof init === "function";
96
+ const segs = reactive
97
+ ? derive(init)
98
+ : cell(init);
99
+ const closed = opts.closed ?? false;
100
+ const cumLen = derive(() => {
101
+ const arr = segs.value;
102
+ const out = [0];
103
+ for (let i = 0; i < arr.length; i++) {
104
+ out.push(out[i] + segmentLength(arr[i]));
105
+ }
106
+ return out;
107
+ });
108
+ const total = derive(() => {
109
+ const c = cumLen.value;
110
+ return c[c.length - 1] ?? 0;
111
+ });
112
+ super("path", () => {
113
+ const arr = segs.value;
114
+ if (arr.length === 0)
115
+ return { x: 0, y: 0, w: 0, h: 0 };
116
+ let xMin = Number.POSITIVE_INFINITY, yMin = Number.POSITIVE_INFINITY, xMax = Number.NEGATIVE_INFINITY, yMax = Number.NEGATIVE_INFINITY;
117
+ for (const seg of arr) {
118
+ const N = seg.kind === "line" ? 1 : ARC_SAMPLES;
119
+ for (let i = 0; i <= N; i++) {
120
+ const p = sampleSegment(seg, i / N);
121
+ if (p.x < xMin)
122
+ xMin = p.x;
123
+ if (p.y < yMin)
124
+ yMin = p.y;
125
+ if (p.x > xMax)
126
+ xMax = p.x;
127
+ if (p.y > yMax)
128
+ yMax = p.y;
129
+ }
130
+ }
131
+ return { x: xMin, y: yMin, w: xMax - xMin, h: yMax - yMin };
132
+ }, opts, {
133
+ origin: derive(() => {
134
+ const arr = segs.value;
135
+ return arr.length > 0 ? segmentStart(arr[0]) : { x: 0, y: 0 };
136
+ }),
137
+ });
138
+ this._segments = segs;
139
+ this._reactive = reactive;
140
+ this.closed = closed;
141
+ this.length = total;
142
+ this.stroke(opts, closed, {
143
+ d: derive(() => {
144
+ const arr = segs.value;
145
+ if (arr.length === 0)
146
+ return "";
147
+ const start = segmentStart(arr[0]);
148
+ const parts = [`M ${start.x} ${start.y}`];
149
+ for (const seg of arr)
150
+ parts.push(segmentD(seg));
151
+ if (closed)
152
+ parts.push("Z");
153
+ return parts.join(" ");
154
+ }),
155
+ });
156
+ }
157
+ /** Untracked snapshot of the segment list. */
158
+ get segs() {
159
+ return this._segments.peek();
160
+ }
161
+ /** Segments for the dashed renderer: lines pass through, circular arcs
162
+ * (`a ≈ b`, rotation ≈ 0) become native arcs, other ellipse arcs tessellate. */
163
+ segments() {
164
+ const arr = this._segments.value;
165
+ const out = [];
166
+ for (const seg of arr) {
167
+ if (seg.kind === "line") {
168
+ out.push({ type: "line", from: seg.from, to: seg.to });
169
+ }
170
+ else if (Math.abs(seg.a - seg.b) < 1e-6 && Math.abs(seg.rotation) < 1e-6) {
171
+ const cx = seg.center.x;
172
+ const cy = seg.center.y;
173
+ const r = seg.a;
174
+ const a0 = seg.a0;
175
+ const a1 = seg.a1;
176
+ out.push({
177
+ type: "arc",
178
+ cx: () => cx,
179
+ cy: () => cy,
180
+ r: () => r,
181
+ a0: () => a0,
182
+ a1: () => a1,
183
+ });
184
+ }
185
+ else {
186
+ const N = 64;
187
+ let prev = sampleSegment(seg, 0);
188
+ for (let i = 1; i <= N; i++) {
189
+ const p = sampleSegment(seg, i / N);
190
+ out.push({ type: "line", from: prev, to: p });
191
+ prev = p;
192
+ }
193
+ }
194
+ }
195
+ return out;
196
+ }
197
+ /** Sample at `t ∈ [0, 1]` along arc length. */
198
+ pointAt(t) {
199
+ const ts = reader(t);
200
+ return Vec.derive(() => {
201
+ const arr = this._segments.value;
202
+ if (arr.length === 0)
203
+ return { x: 0, y: 0 };
204
+ const target = clamp01(ts()) * this.length.value;
205
+ let acc = 0;
206
+ for (const seg of arr) {
207
+ const segLen = segmentLength(seg);
208
+ if (target <= acc + segLen || seg === arr[arr.length - 1]) {
209
+ const s = segLen > 0 ? (target - acc) / segLen : 0;
210
+ return sampleSegment(seg, s);
211
+ }
212
+ acc += segLen;
213
+ }
214
+ return sampleSegment(arr[arr.length - 1], 1);
215
+ });
216
+ }
217
+ /** Unit tangent at `t ∈ [0, 1]`. */
218
+ tangentAt(t) {
219
+ const ts = reader(t);
220
+ return Vec.derive(() => {
221
+ const arr = this._segments.value;
222
+ if (arr.length === 0)
223
+ return { x: 1, y: 0 };
224
+ const target = clamp01(ts()) * this.length.value;
225
+ let acc = 0;
226
+ for (const seg of arr) {
227
+ const segLen = segmentLength(seg);
228
+ if (target <= acc + segLen || seg === arr[arr.length - 1]) {
229
+ const s = segLen > 0 ? (target - acc) / segLen : 0;
230
+ return tangentSegment(seg, s);
231
+ }
232
+ acc += segLen;
233
+ }
234
+ return tangentSegment(arr[arr.length - 1], 1);
235
+ });
236
+ }
237
+ push(seg) {
238
+ if (this._reactive) {
239
+ throw new Error("Curve(builder): fluent .to/.ellipseArc unavailable on reactive curves");
240
+ }
241
+ this._segments.value = [
242
+ ...this._segments.peek(),
243
+ seg,
244
+ ];
245
+ return this;
246
+ }
247
+ get last() {
248
+ const arr = this._segments.peek();
249
+ if (arr.length === 0)
250
+ return undefined;
251
+ return sampleSegment(arr[arr.length - 1], 1);
252
+ }
253
+ /** Append a line segment to `p`. */
254
+ to(p) {
255
+ const from = this.last ?? { x: 0, y: 0 };
256
+ return this.push({ kind: "line", from, to: p });
257
+ }
258
+ /** Append an elliptic-arc segment. `a0`, `a1` are parametric angles
259
+ * in the ellipse's rotated frame. */
260
+ ellipseArc(center, a, b, rotation, a0, a1) {
261
+ return this.push({ kind: "ellipseArc", center, a, b, rotation, a0, a1 });
262
+ }
263
+ }
264
+ /** Start a piecewise curve: `curve()` fluent, `curve([segs])` explicit, or
265
+ * `curve(() => segs)` reactive. */
266
+ export function curve(init, opts) {
267
+ return new Curve(init ?? [], opts);
268
+ }
269
+ /** A closed ellipse centred at `center` with semi-axes `a, b` and
270
+ * optional `rotation` (radians). All four args accept `Val<>`, so the
271
+ * ellipse re-renders when any input changes. */
272
+ export function ellipse(center, a, b, rotation = 0, opts) {
273
+ const o = { ...(opts ?? {}), closed: true };
274
+ return new Curve(() => [
275
+ {
276
+ kind: "ellipseArc",
277
+ center: readNow(center),
278
+ a: readNow(a),
279
+ b: readNow(b),
280
+ rotation: readNow(rotation),
281
+ a0: 0,
282
+ a1: TAU,
283
+ },
284
+ ], o);
285
+ }
@@ -0,0 +1,16 @@
1
+ import type { Segment } from "./shape.js";
2
+ declare const TWO_PI: number;
3
+ interface DashOpts {
4
+ closed?: boolean;
5
+ /** Target dash length. Default 4. */
6
+ dashSize?: number;
7
+ /** Target gap length. Default 3. */
8
+ gapSize?: number;
9
+ /** Optical compensation for round caps — extends each dash so the
10
+ * visible length matches `dashSize`. */
11
+ capExtension?: number;
12
+ }
13
+ /** SVG path `d` for a dashed stroke through `segments` — one `M…L…/A…`
14
+ * run per dash. Use with `<path fill="none">`. */
15
+ export declare function dashedPath(segments: Segment[], opts?: DashOpts): string;
16
+ export { TWO_PI };
@@ -0,0 +1,142 @@
1
+ // Pixel-perfect dashing via explicit `<path>` commands (sidesteps `stroke-dasharray` corner artifacts).
2
+ const TWO_PI = Math.PI * 2;
3
+ function evalSegments(segments) {
4
+ const out = [];
5
+ for (const s of segments) {
6
+ if (s.type === "line") {
7
+ const a = s.from;
8
+ const b = s.to;
9
+ const dx = b.x - a.x;
10
+ const dy = b.y - a.y;
11
+ const len = Math.hypot(dx, dy);
12
+ out.push({
13
+ type: "line",
14
+ length: len,
15
+ x1: a.x,
16
+ y1: a.y,
17
+ x2: b.x,
18
+ y2: b.y,
19
+ });
20
+ }
21
+ else {
22
+ const cx = s.cx(), cy = s.cy(), r = s.r(), a0 = s.a0(), a1 = s.a1();
23
+ out.push({
24
+ type: "arc",
25
+ length: Math.abs(a1 - a0) * r,
26
+ cx,
27
+ cy,
28
+ r,
29
+ a0,
30
+ a1,
31
+ });
32
+ }
33
+ }
34
+ return out;
35
+ }
36
+ function pointAt(s, t) {
37
+ if (s.type === "line") {
38
+ return { x: s.x1 + (s.x2 - s.x1) * t, y: s.y1 + (s.y2 - s.y1) * t };
39
+ }
40
+ const a = s.a0 + (s.a1 - s.a0) * t;
41
+ return { x: s.cx + s.r * Math.cos(a), y: s.cy + s.r * Math.sin(a) };
42
+ }
43
+ function computeDashGeom(length, closed, dashTarget, gapTarget) {
44
+ if (length <= 0)
45
+ return { dashSize: 0, gapSize: 0, N: 0 };
46
+ const period = dashTarget + gapTarget;
47
+ if (!closed) {
48
+ let N = Math.max(2, Math.round((length + gapTarget) / period));
49
+ let gap = (length - N * dashTarget) / (N - 1);
50
+ while (gap < 0 && N > 2) {
51
+ N--;
52
+ gap = (length - N * dashTarget) / (N - 1);
53
+ }
54
+ if (length < 2 * dashTarget || gap < 0) {
55
+ return { dashSize: length, gapSize: 0, N: 1 };
56
+ }
57
+ return { dashSize: dashTarget, gapSize: gap, N };
58
+ }
59
+ let N = Math.max(1, Math.round(length / period));
60
+ let gap = length / N - dashTarget;
61
+ while (gap < 0 && N > 1) {
62
+ N--;
63
+ gap = length / N - dashTarget;
64
+ }
65
+ if (gap < 0)
66
+ return { dashSize: length, gapSize: 0, N: 1 };
67
+ return { dashSize: dashTarget, gapSize: gap, N };
68
+ }
69
+ function pathFromTo(segs, start, end) {
70
+ let i = 0;
71
+ let pos = 0;
72
+ while (i < segs.length && pos + segs[i].length < start) {
73
+ pos += segs[i].length;
74
+ i++;
75
+ }
76
+ if (i >= segs.length)
77
+ return "";
78
+ const segLen = segs[i].length;
79
+ const tStart = segLen > 0 ? (start - pos) / segLen : 0;
80
+ const startPt = pointAt(segs[i], tStart);
81
+ let d = `M ${startPt.x},${startPt.y}`;
82
+ let remaining = end - start;
83
+ let curT = tStart;
84
+ while (remaining > 0 && i < segs.length) {
85
+ const seg = segs[i];
86
+ const lenLeft = (1 - curT) * seg.length;
87
+ if (remaining <= lenLeft) {
88
+ const endT = curT + (seg.length > 0 ? remaining / seg.length : 0);
89
+ const endPt = pointAt(seg, endT);
90
+ if (seg.type === "line") {
91
+ d += ` L ${endPt.x},${endPt.y}`;
92
+ }
93
+ else {
94
+ const sweepAngle = (seg.a1 - seg.a0) * (endT - curT);
95
+ const largeArc = Math.abs(sweepAngle) > Math.PI ? 1 : 0;
96
+ const sweepFlag = sweepAngle > 0 ? 1 : 0;
97
+ d += ` A ${seg.r},${seg.r} 0 ${largeArc} ${sweepFlag} ${endPt.x},${endPt.y}`;
98
+ }
99
+ return d;
100
+ }
101
+ const segEnd = pointAt(seg, 1);
102
+ if (seg.type === "line") {
103
+ d += ` L ${segEnd.x},${segEnd.y}`;
104
+ }
105
+ else {
106
+ const sweepAngle = (seg.a1 - seg.a0) * (1 - curT);
107
+ const largeArc = Math.abs(sweepAngle) > Math.PI ? 1 : 0;
108
+ const sweepFlag = sweepAngle > 0 ? 1 : 0;
109
+ d += ` A ${seg.r},${seg.r} 0 ${largeArc} ${sweepFlag} ${segEnd.x},${segEnd.y}`;
110
+ }
111
+ remaining -= lenLeft;
112
+ i++;
113
+ curT = 0;
114
+ }
115
+ return d;
116
+ }
117
+ /** SVG path `d` for a dashed stroke through `segments` — one `M…L…/A…`
118
+ * run per dash. Use with `<path fill="none">`. */
119
+ export function dashedPath(segments, opts = {}) {
120
+ const segs = evalSegments(segments);
121
+ if (segs.length === 0)
122
+ return "";
123
+ const closed = opts.closed ?? false;
124
+ const ext = opts.capExtension ?? 0;
125
+ const dashTarget = Math.max(0.001, (opts.dashSize ?? 4) - ext);
126
+ const gapTarget = (opts.gapSize ?? 3) + ext;
127
+ const total = segs.reduce((sum, s) => sum + s.length, 0);
128
+ const { dashSize, gapSize, N } = computeDashGeom(total, closed, dashTarget, gapTarget);
129
+ if (N === 0)
130
+ return "";
131
+ if (N === 1)
132
+ return pathFromTo(segs, 0, total);
133
+ const period = dashSize + gapSize;
134
+ const out = [];
135
+ for (let i = 0; i < N; i++) {
136
+ const start = i * period;
137
+ const end = start + dashSize;
138
+ out.push(pathFromTo(segs, start, end));
139
+ }
140
+ return out.join(" ");
141
+ }
142
+ export { TWO_PI };
@@ -0,0 +1,43 @@
1
+ import { Box, Vec } from "../core/index.js";
2
+ import type { Path } from "./path.js";
3
+ import { type AnyShape, Shape } from "./shape.js";
4
+ /** debug.* — diagnostic overlays. All are `aside: true` so they don't
5
+ * infect autofit. Drop in during development, remove when done. */
6
+ export declare const debug: {
7
+ box: (b: Shape | Box) => import("./rect.js").Rect<{
8
+ aside: boolean;
9
+ opacity: number;
10
+ stroke: string;
11
+ fill: string;
12
+ thin: boolean;
13
+ dashed: boolean;
14
+ }>;
15
+ dot: (p: Vec | Shape | Box, r?: number) => import("./circle.js").Circle<{
16
+ readonly aside: boolean;
17
+ readonly opacity: number;
18
+ readonly fill: "var(--bireactive-debug, #c026d3)";
19
+ readonly stroke: "none";
20
+ }>;
21
+ origin: (s: Shape, size?: number) => Shape<{
22
+ readonly aside: true;
23
+ readonly opacity: 0.75;
24
+ }>;
25
+ anchors: (b: Shape | Box, r?: number) => Shape<{
26
+ readonly aside: true;
27
+ readonly opacity: 0.7;
28
+ }>;
29
+ connect: (a: AnyShape | Vec, b: AnyShape | Vec) => import("./line.js").Line<{
30
+ readonly aside: boolean;
31
+ readonly opacity: number;
32
+ readonly stroke: "var(--bireactive-debug, #c026d3)";
33
+ readonly thin: true;
34
+ readonly dashed: true;
35
+ }>;
36
+ distance: (a: AnyShape | Vec, b: AnyShape | Vec) => Shape<{
37
+ readonly aside: true;
38
+ }>;
39
+ path: (p: Path, ticks?: number) => Shape<{
40
+ readonly aside: true;
41
+ readonly opacity: 0.75;
42
+ }>;
43
+ };