@invinite-org/chartlang-adapter-kit 1.3.0 → 1.4.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 (143) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +7 -0
  3. package/dist/canvas/index.d.ts +5 -0
  4. package/dist/canvas/index.d.ts.map +1 -0
  5. package/dist/canvas/index.js +5 -0
  6. package/dist/canvas/index.js.map +1 -0
  7. package/dist/canvas/mockContext.d.ts +168 -0
  8. package/dist/canvas/mockContext.d.ts.map +1 -0
  9. package/dist/canvas/mockContext.js +198 -0
  10. package/dist/canvas/mockContext.js.map +1 -0
  11. package/dist/canvas/paintPrimitive.d.ts +35 -0
  12. package/dist/canvas/paintPrimitive.d.ts.map +1 -0
  13. package/dist/canvas/paintPrimitive.js +171 -0
  14. package/dist/canvas/paintPrimitive.js.map +1 -0
  15. package/dist/canvas/renderCtx.d.ts +40 -0
  16. package/dist/canvas/renderCtx.d.ts.map +1 -0
  17. package/dist/canvas/renderCtx.js +4 -0
  18. package/dist/canvas/renderCtx.js.map +1 -0
  19. package/dist/geometry/_lib/arrowhead.d.ts +18 -0
  20. package/dist/geometry/_lib/arrowhead.d.ts.map +1 -0
  21. package/dist/geometry/_lib/arrowhead.js +38 -0
  22. package/dist/geometry/_lib/arrowhead.js.map +1 -0
  23. package/dist/geometry/_lib/bezier.d.ts +57 -0
  24. package/dist/geometry/_lib/bezier.d.ts.map +1 -0
  25. package/dist/geometry/_lib/bezier.js +84 -0
  26. package/dist/geometry/_lib/bezier.js.map +1 -0
  27. package/dist/geometry/_lib/chevron.d.ts +29 -0
  28. package/dist/geometry/_lib/chevron.d.ts.map +1 -0
  29. package/dist/geometry/_lib/chevron.js +37 -0
  30. package/dist/geometry/_lib/chevron.js.map +1 -0
  31. package/dist/geometry/_lib/dash.d.ts +30 -0
  32. package/dist/geometry/_lib/dash.d.ts.map +1 -0
  33. package/dist/geometry/_lib/dash.js +40 -0
  34. package/dist/geometry/_lib/dash.js.map +1 -0
  35. package/dist/geometry/_lib/fibLevels.d.ts +36 -0
  36. package/dist/geometry/_lib/fibLevels.d.ts.map +1 -0
  37. package/dist/geometry/_lib/fibLevels.js +44 -0
  38. package/dist/geometry/_lib/fibLevels.js.map +1 -0
  39. package/dist/geometry/_lib/gannLevels.d.ts +54 -0
  40. package/dist/geometry/_lib/gannLevels.d.ts.map +1 -0
  41. package/dist/geometry/_lib/gannLevels.js +88 -0
  42. package/dist/geometry/_lib/gannLevels.js.map +1 -0
  43. package/dist/geometry/_lib/lineExtend.d.ts +31 -0
  44. package/dist/geometry/_lib/lineExtend.d.ts.map +1 -0
  45. package/dist/geometry/_lib/lineExtend.js +48 -0
  46. package/dist/geometry/_lib/lineExtend.js.map +1 -0
  47. package/dist/geometry/_lib/namedPolyline.d.ts +25 -0
  48. package/dist/geometry/_lib/namedPolyline.d.ts.map +1 -0
  49. package/dist/geometry/_lib/namedPolyline.js +64 -0
  50. package/dist/geometry/_lib/namedPolyline.js.map +1 -0
  51. package/dist/geometry/_lib/pitchforkGeom.d.ts +46 -0
  52. package/dist/geometry/_lib/pitchforkGeom.d.ts.map +1 -0
  53. package/dist/geometry/_lib/pitchforkGeom.js +70 -0
  54. package/dist/geometry/_lib/pitchforkGeom.js.map +1 -0
  55. package/dist/geometry/_lib/shapeStyle.d.ts +21 -0
  56. package/dist/geometry/_lib/shapeStyle.d.ts.map +1 -0
  57. package/dist/geometry/_lib/shapeStyle.js +41 -0
  58. package/dist/geometry/_lib/shapeStyle.js.map +1 -0
  59. package/dist/geometry/_lib/strokeStyle.d.ts +34 -0
  60. package/dist/geometry/_lib/strokeStyle.d.ts.map +1 -0
  61. package/dist/geometry/_lib/strokeStyle.js +26 -0
  62. package/dist/geometry/_lib/strokeStyle.js.map +1 -0
  63. package/dist/geometry/_lib/textStyle.d.ts +70 -0
  64. package/dist/geometry/_lib/textStyle.d.ts.map +1 -0
  65. package/dist/geometry/_lib/textStyle.js +78 -0
  66. package/dist/geometry/_lib/textStyle.js.map +1 -0
  67. package/dist/geometry/decompose.d.ts +28 -0
  68. package/dist/geometry/decompose.d.ts.map +1 -0
  69. package/dist/geometry/decompose.js +176 -0
  70. package/dist/geometry/decompose.js.map +1 -0
  71. package/dist/geometry/index.d.ts +4 -0
  72. package/dist/geometry/index.d.ts.map +1 -0
  73. package/dist/geometry/index.js +5 -0
  74. package/dist/geometry/index.js.map +1 -0
  75. package/dist/geometry/kinds/annotations.d.ts +77 -0
  76. package/dist/geometry/kinds/annotations.d.ts.map +1 -0
  77. package/dist/geometry/kinds/annotations.js +219 -0
  78. package/dist/geometry/kinds/annotations.js.map +1 -0
  79. package/dist/geometry/kinds/boxes.d.ts +116 -0
  80. package/dist/geometry/kinds/boxes.d.ts.map +1 -0
  81. package/dist/geometry/kinds/boxes.js +285 -0
  82. package/dist/geometry/kinds/boxes.js.map +1 -0
  83. package/dist/geometry/kinds/channels.d.ts +72 -0
  84. package/dist/geometry/kinds/channels.d.ts.map +1 -0
  85. package/dist/geometry/kinds/channels.js +148 -0
  86. package/dist/geometry/kinds/channels.js.map +1 -0
  87. package/dist/geometry/kinds/containers.d.ts +54 -0
  88. package/dist/geometry/kinds/containers.d.ts.map +1 -0
  89. package/dist/geometry/kinds/containers.js +268 -0
  90. package/dist/geometry/kinds/containers.js.map +1 -0
  91. package/dist/geometry/kinds/curves.d.ts +53 -0
  92. package/dist/geometry/kinds/curves.d.ts.map +1 -0
  93. package/dist/geometry/kinds/curves.js +110 -0
  94. package/dist/geometry/kinds/curves.js.map +1 -0
  95. package/dist/geometry/kinds/cycles.d.ts +52 -0
  96. package/dist/geometry/kinds/cycles.d.ts.map +1 -0
  97. package/dist/geometry/kinds/cycles.js +158 -0
  98. package/dist/geometry/kinds/cycles.js.map +1 -0
  99. package/dist/geometry/kinds/elliott.d.ts +73 -0
  100. package/dist/geometry/kinds/elliott.d.ts.map +1 -0
  101. package/dist/geometry/kinds/elliott.js +116 -0
  102. package/dist/geometry/kinds/elliott.js.map +1 -0
  103. package/dist/geometry/kinds/fibonacci.d.ts +166 -0
  104. package/dist/geometry/kinds/fibonacci.d.ts.map +1 -0
  105. package/dist/geometry/kinds/fibonacci.js +458 -0
  106. package/dist/geometry/kinds/fibonacci.js.map +1 -0
  107. package/dist/geometry/kinds/freehand.d.ts +53 -0
  108. package/dist/geometry/kinds/freehand.d.ts.map +1 -0
  109. package/dist/geometry/kinds/freehand.js +115 -0
  110. package/dist/geometry/kinds/freehand.js.map +1 -0
  111. package/dist/geometry/kinds/gann.d.ts +63 -0
  112. package/dist/geometry/kinds/gann.d.ts.map +1 -0
  113. package/dist/geometry/kinds/gann.js +153 -0
  114. package/dist/geometry/kinds/gann.js.map +1 -0
  115. package/dist/geometry/kinds/lines.d.ts +90 -0
  116. package/dist/geometry/kinds/lines.d.ts.map +1 -0
  117. package/dist/geometry/kinds/lines.js +201 -0
  118. package/dist/geometry/kinds/lines.js.map +1 -0
  119. package/dist/geometry/kinds/marker.d.ts +21 -0
  120. package/dist/geometry/kinds/marker.d.ts.map +1 -0
  121. package/dist/geometry/kinds/marker.js +47 -0
  122. package/dist/geometry/kinds/marker.js.map +1 -0
  123. package/dist/geometry/kinds/patterns.d.ts +85 -0
  124. package/dist/geometry/kinds/patterns.d.ts.map +1 -0
  125. package/dist/geometry/kinds/patterns.js +133 -0
  126. package/dist/geometry/kinds/patterns.js.map +1 -0
  127. package/dist/geometry/kinds/pitchforks.d.ts +36 -0
  128. package/dist/geometry/kinds/pitchforks.d.ts.map +1 -0
  129. package/dist/geometry/kinds/pitchforks.js +109 -0
  130. package/dist/geometry/kinds/pitchforks.js.map +1 -0
  131. package/dist/geometry/project.d.ts +50 -0
  132. package/dist/geometry/project.d.ts.map +1 -0
  133. package/dist/geometry/project.js +62 -0
  134. package/dist/geometry/project.js.map +1 -0
  135. package/dist/geometry/types.d.ts +146 -0
  136. package/dist/geometry/types.d.ts.map +1 -0
  137. package/dist/geometry/types.js +4 -0
  138. package/dist/geometry/types.js.map +1 -0
  139. package/dist/index.d.ts +2 -0
  140. package/dist/index.d.ts.map +1 -1
  141. package/dist/index.js +1 -0
  142. package/dist/index.js.map +1 -1
  143. package/package.json +8 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # @invinite-org/chartlang-adapter-kit
2
2
 
3
+ ## 1.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 03f59bf: Complete the `adapter-kit` geometry layer with the final 23 drawing-kind
8
+ decomposers — 4 gann (`gann-box`, `gann-square-fixed`, `gann-square`,
9
+ `gann-fan`), 2 pitchforks (`pitchfork`, `pitchfan`), 6 harmonic patterns
10
+ (`xabcd-pattern`, `cypher-pattern`, `head-and-shoulders`, `abcd-pattern`,
11
+ `triangle-pattern`, `three-drives-pattern`), 5 elliott waves
12
+ (`elliott-impulse-wave`, `elliott-correction-wave`, `elliott-triangle-wave`,
13
+ `elliott-double-combo`, `elliott-triple-combo`), 3 cycles (`cyclic-lines`,
14
+ `time-cycles`, `sine-line`), and 3 containers (`group`, `frame`, `table`).
15
+
16
+ `decomposeDrawing` is now **exhaustive over all 63 `DrawingKind`s**: its
17
+ `default` arm is a `const _exhaustive: never` guard, so adding a future kind to
18
+ core fails `pnpm typecheck` until a decomposer is added. The `table` kind
19
+ decomposes in CSS-pixel/viewport space (it resolves `position` against the
20
+ `Viewport` rather than world coordinates).
21
+
22
+ Move the shared `gannLevels` (`GANN_LEVELS` / `GANN_FAN_RATIOS` /
23
+ `GANN_FAN_LABELS` / `formatGannRatio`) and `pitchforkGeom`
24
+ (`medianOriginFor` / `medianTargetFor`) helpers into package-private
25
+ `geometry/_lib/`, reused by the gann and pitchfork decomposers.
26
+
27
+ - 03f59bf: Extend the `adapter-kit` geometry layer with 20 more drawing-kind decomposers —
28
+ 3 curves (`arc`, `curve`, `double-curve`), 3 freehand (`pen`, `highlighter`,
29
+ `brush`), 4 channels (`trend-channel`, `flat-top-bottom`, `disjoint-channel`,
30
+ `regression-trend`), and 10 fibonacci (`fib-retracement`, `fib-trend-extension`,
31
+ `fib-channel`, `fib-time-zone`, `fib-wedge`, `fib-speed-fan`, `fib-speed-arcs`,
32
+ `fib-spiral`, `fib-circles`, `fib-trend-time`). `decomposeDrawing` now covers 40
33
+ of the 63 kinds; the remaining 23 return `[]` until Task 3.
34
+
35
+ Add an optional `StrokeStyle.alpha` IR field (backward-compatible — omitted
36
+ strokes are byte-identical to before): `paintPrimitive` brackets the `stroke()`
37
+ in `globalAlpha` when set, expressing the `highlighter` translucency.
38
+
39
+ Move the shared `FIB_LEVELS` ratio array + `formatLevel` label formatter into a
40
+ package-private `geometry/_lib/fibLevels.ts`, reused by every fib decomposer.
41
+
42
+ - 03f59bf: Add a renderer-agnostic geometry layer to `adapter-kit`: the `Viewport` +
43
+ projection helpers (`timeToX`, `priceToY`, `worldPointToPixel`), the
44
+ `DrawPrimitive` IR (`polyline` / `arc` / `text` / `marker` with `StrokeStyle` /
45
+ `FillStyle`), and `decomposeDrawing(emission, viewport)` covering the 20 basic
46
+ drawing kinds (lines / rays, boxes / shapes incl. `fill-between`, annotations,
47
+ marker, text). The remaining 43 kinds return `[]` until Tasks 2–3 land their
48
+ decomposers.
49
+
50
+ Also ships the canvas-family sink under the new `./canvas` sub-path:
51
+ `RenderCtx`, `paintPrimitive(ctx, prim)`, the generalised `MockCanvasContext`
52
+ (records every method + setter), and `hashCallLog` for deterministic call-log
53
+ hashing.
54
+
3
55
  ## 1.3.0
4
56
 
5
57
  ### Minor Changes
package/README.md CHANGED
@@ -35,6 +35,13 @@ pnpm add @invinite-org/chartlang-adapter-kit
35
35
  `dep-error`, `dep-cycle`, `dep-unknown-output`,
36
36
  `dep-invalid-input-override`, `dep-dynamic`, `dep-output-not-titled` for
37
37
  the new `.output(...)` / `.withInputs(...)` surface.
38
+ - Geometry layer: `timeToX`, `priceToY`, `worldPointToPixel`,
39
+ `decomposeDrawing(emission, viewport) -> DrawPrimitive[]` (covers the basic
40
+ drawing kinds today; the rest land incrementally), plus the `Viewport`,
41
+ `Point2`, `DrawPrimitive`, `StrokeStyle`, and `FillStyle` types.
42
+ - Canvas sink (`@invinite-org/chartlang-adapter-kit/canvas`): `RenderCtx`,
43
+ `paintPrimitive(ctx, prim)`, `MockCanvasContext`, `RecordedCall`, and
44
+ `hashCallLog(mock.calls)` for deterministic, headless call-log hashing.
38
45
 
39
46
  ## Minimum-viable API call
40
47
 
@@ -0,0 +1,5 @@
1
+ export { hashCallLog, MockCanvasContext } from "./mockContext.js";
2
+ export type { RecordedCall } from "./mockContext.js";
3
+ export { paintPrimitive } from "./paintPrimitive.js";
4
+ export type { RenderCtx } from "./renderCtx.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/canvas/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAClE,YAAY,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,5 @@
1
+ // Copyright (c) 2026 Invinite. Licensed under the MIT License.
2
+ // See the LICENSE file in the repo root for full license text.
3
+ export { hashCallLog, MockCanvasContext } from "./mockContext.js";
4
+ export { paintPrimitive } from "./paintPrimitive.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/canvas/index.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,+DAA+D;AAE/D,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAElE,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC","sourcesContent":["// Copyright (c) 2026 Invinite. Licensed under the MIT License.\n// See the LICENSE file in the repo root for full license text.\n\nexport { hashCallLog, MockCanvasContext } from \"./mockContext.js\";\nexport type { RecordedCall } from \"./mockContext.js\";\nexport { paintPrimitive } from \"./paintPrimitive.js\";\nexport type { RenderCtx } from \"./renderCtx.js\";\n"]}
@@ -0,0 +1,168 @@
1
+ /**
2
+ * One recorded call against {@link MockCanvasContext}. Tests inspect
3
+ * the array to assert a paint sequence. The union covers every method
4
+ * and setter on {@link import("./renderCtx").RenderCtx}; adding a new
5
+ * draw call means extending both the mock and this union together.
6
+ *
7
+ * @since 1.3
8
+ * @stable
9
+ * @example
10
+ * const call: RecordedCall = { kind: "clearRect", x: 0, y: 0, w: 100, h: 100 };
11
+ * void call;
12
+ */
13
+ export type RecordedCall = {
14
+ readonly kind: "clearRect";
15
+ readonly x: number;
16
+ readonly y: number;
17
+ readonly w: number;
18
+ readonly h: number;
19
+ } | {
20
+ readonly kind: "translate";
21
+ readonly x: number;
22
+ readonly y: number;
23
+ } | {
24
+ readonly kind: "save";
25
+ } | {
26
+ readonly kind: "restore";
27
+ } | {
28
+ readonly kind: "beginPath";
29
+ } | {
30
+ readonly kind: "moveTo";
31
+ readonly x: number;
32
+ readonly y: number;
33
+ } | {
34
+ readonly kind: "lineTo";
35
+ readonly x: number;
36
+ readonly y: number;
37
+ } | {
38
+ readonly kind: "stroke";
39
+ } | {
40
+ readonly kind: "fillRect";
41
+ readonly x: number;
42
+ readonly y: number;
43
+ readonly w: number;
44
+ readonly h: number;
45
+ } | {
46
+ readonly kind: "fill";
47
+ } | {
48
+ readonly kind: "arc";
49
+ readonly x: number;
50
+ readonly y: number;
51
+ readonly radius: number;
52
+ readonly start: number;
53
+ readonly end: number;
54
+ } | {
55
+ readonly kind: "closePath";
56
+ } | {
57
+ readonly kind: "setLineDash";
58
+ readonly segments: ReadonlyArray<number>;
59
+ } | {
60
+ readonly kind: "fillText";
61
+ readonly text: string;
62
+ readonly x: number;
63
+ readonly y: number;
64
+ } | {
65
+ readonly kind: "set";
66
+ readonly prop: "strokeStyle";
67
+ readonly value: string;
68
+ } | {
69
+ readonly kind: "set";
70
+ readonly prop: "fillStyle";
71
+ readonly value: string;
72
+ } | {
73
+ readonly kind: "set";
74
+ readonly prop: "lineWidth";
75
+ readonly value: number;
76
+ } | {
77
+ readonly kind: "set";
78
+ readonly prop: "globalAlpha";
79
+ readonly value: number;
80
+ } | {
81
+ readonly kind: "set";
82
+ readonly prop: "font";
83
+ readonly value: string;
84
+ } | {
85
+ readonly kind: "set";
86
+ readonly prop: "textAlign";
87
+ readonly value: string;
88
+ } | {
89
+ readonly kind: "set";
90
+ readonly prop: "textBaseline";
91
+ readonly value: string;
92
+ };
93
+ /**
94
+ * Hand-rolled Canvas 2D mock that satisfies the canvas sink's
95
+ * {@link import("./renderCtx").RenderCtx} structural type. Every method
96
+ * and setter appends a typed record to `calls` so tests can assert the
97
+ * exact paint sequence without standing up `node-canvas` or a real
98
+ * browser. The canvas2d adapter re-exports this class as
99
+ * `MockCanvas2DContext` (implementation shared, public name unchanged).
100
+ *
101
+ * Property setters are tracked via accessor pairs — the mock stores the
102
+ * most recent value in private fields so `ctx.strokeStyle = "#x"` both
103
+ * records the call and survives a subsequent read.
104
+ *
105
+ * @since 1.3
106
+ * @stable
107
+ * @example
108
+ * const ctx = new MockCanvasContext();
109
+ * ctx.clearRect(0, 0, 100, 100);
110
+ * // ctx.calls[0] === { kind: "clearRect", x: 0, y: 0, w: 100, h: 100 }
111
+ * void ctx;
112
+ */
113
+ export declare class MockCanvasContext {
114
+ readonly calls: RecordedCall[];
115
+ private _strokeStyle;
116
+ private _fillStyle;
117
+ private _lineWidth;
118
+ private _globalAlpha;
119
+ private _font;
120
+ private _textAlign;
121
+ private _textBaseline;
122
+ clearRect(x: number, y: number, w: number, h: number): void;
123
+ translate(x: number, y: number): void;
124
+ save(): void;
125
+ restore(): void;
126
+ beginPath(): void;
127
+ moveTo(x: number, y: number): void;
128
+ lineTo(x: number, y: number): void;
129
+ stroke(): void;
130
+ fillRect(x: number, y: number, w: number, h: number): void;
131
+ fill(): void;
132
+ arc(x: number, y: number, radius: number, start: number, end: number): void;
133
+ closePath(): void;
134
+ setLineDash(segments: ReadonlyArray<number>): void;
135
+ fillText(text: string, x: number, y: number): void;
136
+ get strokeStyle(): string;
137
+ set strokeStyle(value: string);
138
+ get fillStyle(): string;
139
+ set fillStyle(value: string);
140
+ get lineWidth(): number;
141
+ set lineWidth(value: number);
142
+ get globalAlpha(): number;
143
+ set globalAlpha(value: number);
144
+ get font(): string;
145
+ set font(value: string);
146
+ get textAlign(): "start" | "center" | "end" | "left" | "right";
147
+ set textAlign(value: "start" | "center" | "end" | "left" | "right");
148
+ get textBaseline(): "top" | "middle" | "bottom" | "alphabetic" | "hanging";
149
+ set textBaseline(value: "top" | "middle" | "bottom" | "alphabetic" | "hanging");
150
+ }
151
+ /**
152
+ * Hash a recorded call log into a stable SHA-256 hex string. Floats are
153
+ * rounded to four decimal places and re-serialised in canonical-key
154
+ * JSON so a microscopic floating-point drift does not re-hash the log.
155
+ * Pass `mock.calls`. Adapters pin their integration render output
156
+ * against a single golden constant with this.
157
+ *
158
+ * @since 1.3
159
+ * @stable
160
+ * @example
161
+ * const ctx = new MockCanvasContext();
162
+ * ctx.clearRect(0, 0, 100, 100);
163
+ * const h = hashCallLog(ctx.calls);
164
+ * // h is a 64-char hex string
165
+ * void h;
166
+ */
167
+ export declare function hashCallLog(calls: ReadonlyArray<RecordedCall>): string;
168
+ //# sourceMappingURL=mockContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mockContext.d.ts","sourceRoot":"","sources":["../../src/canvas/mockContext.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,198 @@
1
+ // Copyright (c) 2026 Invinite. Licensed under the MIT License.
2
+ // See the LICENSE file in the repo root for full license text.
3
+ import { createHash } from "node:crypto";
4
+ /**
5
+ * Hand-rolled Canvas 2D mock that satisfies the canvas sink's
6
+ * {@link import("./renderCtx").RenderCtx} structural type. Every method
7
+ * and setter appends a typed record to `calls` so tests can assert the
8
+ * exact paint sequence without standing up `node-canvas` or a real
9
+ * browser. The canvas2d adapter re-exports this class as
10
+ * `MockCanvas2DContext` (implementation shared, public name unchanged).
11
+ *
12
+ * Property setters are tracked via accessor pairs — the mock stores the
13
+ * most recent value in private fields so `ctx.strokeStyle = "#x"` both
14
+ * records the call and survives a subsequent read.
15
+ *
16
+ * @since 1.3
17
+ * @stable
18
+ * @example
19
+ * const ctx = new MockCanvasContext();
20
+ * ctx.clearRect(0, 0, 100, 100);
21
+ * // ctx.calls[0] === { kind: "clearRect", x: 0, y: 0, w: 100, h: 100 }
22
+ * void ctx;
23
+ */
24
+ export class MockCanvasContext {
25
+ calls = [];
26
+ _strokeStyle = "#000000";
27
+ _fillStyle = "#000000";
28
+ _lineWidth = 1;
29
+ _globalAlpha = 1;
30
+ _font = "10px sans-serif";
31
+ _textAlign = "start";
32
+ _textBaseline = "alphabetic";
33
+ clearRect(x, y, w, h) {
34
+ this.calls.push({ kind: "clearRect", x, y, w, h });
35
+ }
36
+ translate(x, y) {
37
+ this.calls.push({ kind: "translate", x, y });
38
+ }
39
+ save() {
40
+ this.calls.push({ kind: "save" });
41
+ }
42
+ restore() {
43
+ this.calls.push({ kind: "restore" });
44
+ }
45
+ beginPath() {
46
+ this.calls.push({ kind: "beginPath" });
47
+ }
48
+ moveTo(x, y) {
49
+ this.calls.push({ kind: "moveTo", x, y });
50
+ }
51
+ lineTo(x, y) {
52
+ this.calls.push({ kind: "lineTo", x, y });
53
+ }
54
+ stroke() {
55
+ this.calls.push({ kind: "stroke" });
56
+ }
57
+ fillRect(x, y, w, h) {
58
+ this.calls.push({ kind: "fillRect", x, y, w, h });
59
+ }
60
+ fill() {
61
+ this.calls.push({ kind: "fill" });
62
+ }
63
+ arc(x, y, radius, start, end) {
64
+ this.calls.push({ kind: "arc", x, y, radius, start, end });
65
+ }
66
+ closePath() {
67
+ this.calls.push({ kind: "closePath" });
68
+ }
69
+ setLineDash(segments) {
70
+ this.calls.push({ kind: "setLineDash", segments: segments.slice() });
71
+ }
72
+ fillText(text, x, y) {
73
+ this.calls.push({ kind: "fillText", text, x, y });
74
+ }
75
+ get strokeStyle() {
76
+ return this._strokeStyle;
77
+ }
78
+ set strokeStyle(value) {
79
+ this._strokeStyle = value;
80
+ this.calls.push({ kind: "set", prop: "strokeStyle", value });
81
+ }
82
+ get fillStyle() {
83
+ return this._fillStyle;
84
+ }
85
+ set fillStyle(value) {
86
+ this._fillStyle = value;
87
+ this.calls.push({ kind: "set", prop: "fillStyle", value });
88
+ }
89
+ get lineWidth() {
90
+ return this._lineWidth;
91
+ }
92
+ set lineWidth(value) {
93
+ this._lineWidth = value;
94
+ this.calls.push({ kind: "set", prop: "lineWidth", value });
95
+ }
96
+ get globalAlpha() {
97
+ return this._globalAlpha;
98
+ }
99
+ set globalAlpha(value) {
100
+ this._globalAlpha = value;
101
+ this.calls.push({ kind: "set", prop: "globalAlpha", value });
102
+ }
103
+ get font() {
104
+ return this._font;
105
+ }
106
+ set font(value) {
107
+ this._font = value;
108
+ this.calls.push({ kind: "set", prop: "font", value });
109
+ }
110
+ get textAlign() {
111
+ return this._textAlign;
112
+ }
113
+ set textAlign(value) {
114
+ this._textAlign = value;
115
+ this.calls.push({ kind: "set", prop: "textAlign", value });
116
+ }
117
+ get textBaseline() {
118
+ return this._textBaseline;
119
+ }
120
+ set textBaseline(value) {
121
+ this._textBaseline = value;
122
+ this.calls.push({ kind: "set", prop: "textBaseline", value });
123
+ }
124
+ }
125
+ const FLOAT_DECIMALS = 4;
126
+ function roundFloat(n) {
127
+ if (!Number.isFinite(n))
128
+ return String(n);
129
+ return Number(n.toFixed(FLOAT_DECIMALS));
130
+ }
131
+ function canonicalise(call) {
132
+ switch (call.kind) {
133
+ case "clearRect":
134
+ case "fillRect":
135
+ return {
136
+ kind: call.kind,
137
+ x: roundFloat(call.x),
138
+ y: roundFloat(call.y),
139
+ w: roundFloat(call.w),
140
+ h: roundFloat(call.h),
141
+ };
142
+ case "moveTo":
143
+ case "lineTo":
144
+ case "translate":
145
+ return { kind: call.kind, x: roundFloat(call.x), y: roundFloat(call.y) };
146
+ case "arc":
147
+ return {
148
+ kind: call.kind,
149
+ x: roundFloat(call.x),
150
+ y: roundFloat(call.y),
151
+ radius: roundFloat(call.radius),
152
+ start: roundFloat(call.start),
153
+ end: roundFloat(call.end),
154
+ };
155
+ case "setLineDash":
156
+ return { kind: call.kind, segments: call.segments.map((s) => roundFloat(s)) };
157
+ case "fillText":
158
+ return {
159
+ kind: call.kind,
160
+ text: call.text,
161
+ x: roundFloat(call.x),
162
+ y: roundFloat(call.y),
163
+ };
164
+ case "set": {
165
+ const value = typeof call.value === "number" ? roundFloat(call.value) : call.value;
166
+ return { kind: call.kind, prop: call.prop, value };
167
+ }
168
+ case "beginPath":
169
+ case "save":
170
+ case "restore":
171
+ case "stroke":
172
+ case "fill":
173
+ case "closePath":
174
+ return { kind: call.kind };
175
+ }
176
+ }
177
+ /**
178
+ * Hash a recorded call log into a stable SHA-256 hex string. Floats are
179
+ * rounded to four decimal places and re-serialised in canonical-key
180
+ * JSON so a microscopic floating-point drift does not re-hash the log.
181
+ * Pass `mock.calls`. Adapters pin their integration render output
182
+ * against a single golden constant with this.
183
+ *
184
+ * @since 1.3
185
+ * @stable
186
+ * @example
187
+ * const ctx = new MockCanvasContext();
188
+ * ctx.clearRect(0, 0, 100, 100);
189
+ * const h = hashCallLog(ctx.calls);
190
+ * // h is a 64-char hex string
191
+ * void h;
192
+ */
193
+ export function hashCallLog(calls) {
194
+ const payload = calls.map(canonicalise);
195
+ const serialised = JSON.stringify(payload);
196
+ return createHash("sha256").update(serialised).digest("hex");
197
+ }
198
+ //# sourceMappingURL=mockContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mockContext.js","sourceRoot":"","sources":["../../src/canvas/mockContext.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) 2026 Invinite. Licensed under the MIT License.\n// See the LICENSE file in the repo root for full license text.\n\nimport { createHash } from \"node:crypto\";\n\n/**\n * One recorded call against {@link MockCanvasContext}. Tests inspect\n * the array to assert a paint sequence. The union covers every method\n * and setter on {@link import(\"./renderCtx\").RenderCtx}; adding a new\n * draw call means extending both the mock and this union together.\n *\n * @since 1.3\n * @stable\n * @example\n * const call: RecordedCall = { kind: \"clearRect\", x: 0, y: 0, w: 100, h: 100 };\n * void call;\n */\nexport type RecordedCall =\n | {\n readonly kind: \"clearRect\";\n readonly x: number;\n readonly y: number;\n readonly w: number;\n readonly h: number;\n }\n | { readonly kind: \"translate\"; readonly x: number; readonly y: number }\n | { readonly kind: \"save\" }\n | { readonly kind: \"restore\" }\n | { readonly kind: \"beginPath\" }\n | { readonly kind: \"moveTo\"; readonly x: number; readonly y: number }\n | { readonly kind: \"lineTo\"; readonly x: number; readonly y: number }\n | { readonly kind: \"stroke\" }\n | {\n readonly kind: \"fillRect\";\n readonly x: number;\n readonly y: number;\n readonly w: number;\n readonly h: number;\n }\n | { readonly kind: \"fill\" }\n | {\n readonly kind: \"arc\";\n readonly x: number;\n readonly y: number;\n readonly radius: number;\n readonly start: number;\n readonly end: number;\n }\n | { readonly kind: \"closePath\" }\n | { readonly kind: \"setLineDash\"; readonly segments: ReadonlyArray<number> }\n | { readonly kind: \"fillText\"; readonly text: string; readonly x: number; readonly y: number }\n | { readonly kind: \"set\"; readonly prop: \"strokeStyle\"; readonly value: string }\n | { readonly kind: \"set\"; readonly prop: \"fillStyle\"; readonly value: string }\n | { readonly kind: \"set\"; readonly prop: \"lineWidth\"; readonly value: number }\n | { readonly kind: \"set\"; readonly prop: \"globalAlpha\"; readonly value: number }\n | { readonly kind: \"set\"; readonly prop: \"font\"; readonly value: string }\n | { readonly kind: \"set\"; readonly prop: \"textAlign\"; readonly value: string }\n | { readonly kind: \"set\"; readonly prop: \"textBaseline\"; readonly value: string };\n\n/**\n * Hand-rolled Canvas 2D mock that satisfies the canvas sink's\n * {@link import(\"./renderCtx\").RenderCtx} structural type. Every method\n * and setter appends a typed record to `calls` so tests can assert the\n * exact paint sequence without standing up `node-canvas` or a real\n * browser. The canvas2d adapter re-exports this class as\n * `MockCanvas2DContext` (implementation shared, public name unchanged).\n *\n * Property setters are tracked via accessor pairs — the mock stores the\n * most recent value in private fields so `ctx.strokeStyle = \"#x\"` both\n * records the call and survives a subsequent read.\n *\n * @since 1.3\n * @stable\n * @example\n * const ctx = new MockCanvasContext();\n * ctx.clearRect(0, 0, 100, 100);\n * // ctx.calls[0] === { kind: \"clearRect\", x: 0, y: 0, w: 100, h: 100 }\n * void ctx;\n */\nexport class MockCanvasContext {\n readonly calls: RecordedCall[] = [];\n private _strokeStyle = \"#000000\";\n private _fillStyle = \"#000000\";\n private _lineWidth = 1;\n private _globalAlpha = 1;\n private _font = \"10px sans-serif\";\n private _textAlign: \"start\" | \"center\" | \"end\" | \"left\" | \"right\" = \"start\";\n private _textBaseline: \"top\" | \"middle\" | \"bottom\" | \"alphabetic\" | \"hanging\" = \"alphabetic\";\n\n clearRect(x: number, y: number, w: number, h: number): void {\n this.calls.push({ kind: \"clearRect\", x, y, w, h });\n }\n\n translate(x: number, y: number): void {\n this.calls.push({ kind: \"translate\", x, y });\n }\n\n save(): void {\n this.calls.push({ kind: \"save\" });\n }\n\n restore(): void {\n this.calls.push({ kind: \"restore\" });\n }\n\n beginPath(): void {\n this.calls.push({ kind: \"beginPath\" });\n }\n\n moveTo(x: number, y: number): void {\n this.calls.push({ kind: \"moveTo\", x, y });\n }\n\n lineTo(x: number, y: number): void {\n this.calls.push({ kind: \"lineTo\", x, y });\n }\n\n stroke(): void {\n this.calls.push({ kind: \"stroke\" });\n }\n\n fillRect(x: number, y: number, w: number, h: number): void {\n this.calls.push({ kind: \"fillRect\", x, y, w, h });\n }\n\n fill(): void {\n this.calls.push({ kind: \"fill\" });\n }\n\n arc(x: number, y: number, radius: number, start: number, end: number): void {\n this.calls.push({ kind: \"arc\", x, y, radius, start, end });\n }\n\n closePath(): void {\n this.calls.push({ kind: \"closePath\" });\n }\n\n setLineDash(segments: ReadonlyArray<number>): void {\n this.calls.push({ kind: \"setLineDash\", segments: segments.slice() });\n }\n\n fillText(text: string, x: number, y: number): void {\n this.calls.push({ kind: \"fillText\", text, x, y });\n }\n\n get strokeStyle(): string {\n return this._strokeStyle;\n }\n\n set strokeStyle(value: string) {\n this._strokeStyle = value;\n this.calls.push({ kind: \"set\", prop: \"strokeStyle\", value });\n }\n\n get fillStyle(): string {\n return this._fillStyle;\n }\n\n set fillStyle(value: string) {\n this._fillStyle = value;\n this.calls.push({ kind: \"set\", prop: \"fillStyle\", value });\n }\n\n get lineWidth(): number {\n return this._lineWidth;\n }\n\n set lineWidth(value: number) {\n this._lineWidth = value;\n this.calls.push({ kind: \"set\", prop: \"lineWidth\", value });\n }\n\n get globalAlpha(): number {\n return this._globalAlpha;\n }\n\n set globalAlpha(value: number) {\n this._globalAlpha = value;\n this.calls.push({ kind: \"set\", prop: \"globalAlpha\", value });\n }\n\n get font(): string {\n return this._font;\n }\n\n set font(value: string) {\n this._font = value;\n this.calls.push({ kind: \"set\", prop: \"font\", value });\n }\n\n get textAlign(): \"start\" | \"center\" | \"end\" | \"left\" | \"right\" {\n return this._textAlign;\n }\n\n set textAlign(value: \"start\" | \"center\" | \"end\" | \"left\" | \"right\") {\n this._textAlign = value;\n this.calls.push({ kind: \"set\", prop: \"textAlign\", value });\n }\n\n get textBaseline(): \"top\" | \"middle\" | \"bottom\" | \"alphabetic\" | \"hanging\" {\n return this._textBaseline;\n }\n\n set textBaseline(value: \"top\" | \"middle\" | \"bottom\" | \"alphabetic\" | \"hanging\") {\n this._textBaseline = value;\n this.calls.push({ kind: \"set\", prop: \"textBaseline\", value });\n }\n}\n\nconst FLOAT_DECIMALS = 4;\n\nfunction roundFloat(n: number): number | string {\n if (!Number.isFinite(n)) return String(n);\n return Number(n.toFixed(FLOAT_DECIMALS));\n}\n\nfunction canonicalise(call: RecordedCall): Record<string, unknown> {\n switch (call.kind) {\n case \"clearRect\":\n case \"fillRect\":\n return {\n kind: call.kind,\n x: roundFloat(call.x),\n y: roundFloat(call.y),\n w: roundFloat(call.w),\n h: roundFloat(call.h),\n };\n case \"moveTo\":\n case \"lineTo\":\n case \"translate\":\n return { kind: call.kind, x: roundFloat(call.x), y: roundFloat(call.y) };\n case \"arc\":\n return {\n kind: call.kind,\n x: roundFloat(call.x),\n y: roundFloat(call.y),\n radius: roundFloat(call.radius),\n start: roundFloat(call.start),\n end: roundFloat(call.end),\n };\n case \"setLineDash\":\n return { kind: call.kind, segments: call.segments.map((s) => roundFloat(s)) };\n case \"fillText\":\n return {\n kind: call.kind,\n text: call.text,\n x: roundFloat(call.x),\n y: roundFloat(call.y),\n };\n case \"set\": {\n const value = typeof call.value === \"number\" ? roundFloat(call.value) : call.value;\n return { kind: call.kind, prop: call.prop, value };\n }\n case \"beginPath\":\n case \"save\":\n case \"restore\":\n case \"stroke\":\n case \"fill\":\n case \"closePath\":\n return { kind: call.kind };\n }\n}\n\n/**\n * Hash a recorded call log into a stable SHA-256 hex string. Floats are\n * rounded to four decimal places and re-serialised in canonical-key\n * JSON so a microscopic floating-point drift does not re-hash the log.\n * Pass `mock.calls`. Adapters pin their integration render output\n * against a single golden constant with this.\n *\n * @since 1.3\n * @stable\n * @example\n * const ctx = new MockCanvasContext();\n * ctx.clearRect(0, 0, 100, 100);\n * const h = hashCallLog(ctx.calls);\n * // h is a 64-char hex string\n * void h;\n */\nexport function hashCallLog(calls: ReadonlyArray<RecordedCall>): string {\n const payload = calls.map(canonicalise);\n const serialised = JSON.stringify(payload);\n return createHash(\"sha256\").update(serialised).digest(\"hex\");\n}\n"]}
@@ -0,0 +1,35 @@
1
+ import type { DrawPrimitive } from "../geometry/types.js";
2
+ import type { RenderCtx } from "./renderCtx.js";
3
+ /**
4
+ * Paint one {@link DrawPrimitive} into a {@link RenderCtx}. The canvas
5
+ * sink shared by the canvas2d, lightweight-charts, and uplot adapters.
6
+ * For each primitive the painter applies the stroke style (when set),
7
+ * builds the path, fills before stroking (so the outline draws on top
8
+ * of the band), and resets `setLineDash([])` + `globalAlpha = 1` after
9
+ * use so downstream draws are unaffected — matching the per-kind
10
+ * renderer conventions the IR replaces. A `stroke.alpha` (the
11
+ * `highlighter` translucency) brackets the `stroke()` in `globalAlpha`;
12
+ * an omitted `alpha` emits no `globalAlpha` mutation, keeping the
13
+ * sequence byte-identical to a Task-1 stroke.
14
+ *
15
+ * `text.bgColor` is carried on the IR but NOT painted here (the
16
+ * structural `RenderCtx` exposes neither `measureText` nor a
17
+ * background-rect path), mirroring the source renderers. The IR
18
+ * `marker` primitive is painted as a sized glyph; no basic kind emits
19
+ * it today, but adapters / future kinds rely on the painter covering
20
+ * every IR shape.
21
+ *
22
+ * @since 1.3
23
+ * @stable
24
+ * @example
25
+ * declare const ctx: RenderCtx;
26
+ * paintPrimitive(ctx, {
27
+ * kind: "polyline",
28
+ * points: [{ x: 0, y: 0 }, { x: 10, y: 10 }],
29
+ * closed: false,
30
+ * stroke: { color: "#000000", width: 1, dash: [] },
31
+ * });
32
+ * void paintPrimitive;
33
+ */
34
+ export declare function paintPrimitive(ctx: RenderCtx, p: DrawPrimitive): void;
35
+ //# sourceMappingURL=paintPrimitive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paintPrimitive.d.ts","sourceRoot":"","sources":["../../src/canvas/paintPrimitive.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAA0B,MAAM,sBAAsB,CAAC;AAClF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAgDhD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,SAAS,EAAE,CAAC,EAAE,aAAa,GAAG,IAAI,CA8CrE"}