@slithy/prim-lib 0.3.1 → 0.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.
- package/README.md +19 -9
- package/dist/index.d.ts +15 -8
- package/dist/index.js +57 -11
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -10,15 +10,15 @@ Reconstructs an image by iteratively placing geometric shapes. Each step evaluat
|
|
|
10
10
|
|
|
11
11
|
### Classes
|
|
12
12
|
|
|
13
|
-
- **`Canvas`** — wraps `HTMLCanvasElement
|
|
14
|
-
- **`Optimizer`** — runs the
|
|
13
|
+
- **`Canvas`** — wraps `HTMLCanvasElement` or `OffscreenCanvas` (environment-detected); handles image loading, pixel reads, drawing steps, and SVG output
|
|
14
|
+
- **`Optimizer`** — runs the step loop; calls `onStep` after each shape is placed. Accepts an optional `schedule` function (defaults to `requestAnimationFrame`; pass `fn => setTimeout(fn, 0)` for worker contexts)
|
|
15
15
|
- **`State`** — holds the current canvas and its distance from the target
|
|
16
16
|
- **`Step`** — a single candidate shape placement; computes best color and difference change
|
|
17
17
|
|
|
18
18
|
### Shapes
|
|
19
19
|
|
|
20
20
|
- **`Shape`** — abstract base
|
|
21
|
-
- **`Triangle`**, **`Rectangle`**, **`Ellipse`**, **`Square`**, **`Hexagon`**, **`Glyph`**, **`Debug`**
|
|
21
|
+
- **`Triangle`**, **`Rectangle`**, **`Ellipse`**, **`Circle`**, **`Square`**, **`Hexagon`**, **`Glyph`**, **`Debug`**
|
|
22
22
|
|
|
23
23
|
### Types
|
|
24
24
|
|
|
@@ -38,12 +38,14 @@ interface Cfg {
|
|
|
38
38
|
allowUpscale?: boolean // allow output larger than source image (default: false)
|
|
39
39
|
scale?: number // viewSize / computeSize ratio (set by Canvas.original)
|
|
40
40
|
shapeTypes: Array<new (w: number, h: number) => ShapeInterface>
|
|
41
|
+
shapeWeights?: number[] // per-shape selection weights (same length as shapeTypes)
|
|
41
42
|
fill: 'auto' | string
|
|
42
43
|
}
|
|
43
44
|
```
|
|
44
45
|
|
|
45
46
|
- **`PreCfg`** — `Cfg` with optional `width`/`height`; used before image dimensions are known
|
|
46
47
|
- **`ShapeInterface`** — structural interface for shapes; includes `toData(alpha, color): StepData`
|
|
48
|
+
- **`Ctx2D`** — `CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D`; used for canvas operations that work in both main-thread and worker contexts
|
|
47
49
|
- **`Bbox`**, **`Point`**, **`ImageDataLike`**, **`ShapeImageData`**
|
|
48
50
|
|
|
49
51
|
**`RGB`** — `[number, number, number]` tuple
|
|
@@ -55,6 +57,7 @@ type StepData =
|
|
|
55
57
|
| { t: 't'; a: number; c: RGB; pts: [number, number][] } // Triangle
|
|
56
58
|
| { t: 'r'; a: number; c: RGB; pts: [number, number][] } // Rectangle
|
|
57
59
|
| { t: 'e'; a: number; c: RGB; cx: number; cy: number; rx: number; ry: number } // Ellipse
|
|
60
|
+
| { t: 'c'; a: number; c: RGB; cx: number; cy: number; r: number } // Circle
|
|
58
61
|
| { t: 's'; a: number; c: RGB; cx: number; cy: number; r: number } // Square
|
|
59
62
|
| { t: 'h'; a: number; c: RGB; cx: number; cy: number; r: number; angle: number } // Hexagon
|
|
60
63
|
| { t: 'sm'; a: number; c: RGB; cx: number; cy: number; fs: number; text: string } // Glyph
|
|
@@ -77,7 +80,7 @@ interface SerializedOutput {
|
|
|
77
80
|
|
|
78
81
|
```ts
|
|
79
82
|
interface ReplayResult {
|
|
80
|
-
raster: HTMLCanvasElement
|
|
83
|
+
raster: HTMLCanvasElement | OffscreenCanvas
|
|
81
84
|
svg: SVGSVGElement
|
|
82
85
|
svgString: string
|
|
83
86
|
}
|
|
@@ -87,6 +90,10 @@ interface ReplayResult {
|
|
|
87
90
|
|
|
88
91
|
**`replayOutput(data: SerializedOutput): ReplayResult`** — reconstructs canvas and SVG natively from serialized output, without re-running the optimizer. Useful for restoring saved results from localStorage or a database.
|
|
89
92
|
|
|
93
|
+
**`renderStepToCtx(data: StepData, ctx: Ctx2D): void`** — renders a single `StepData` onto a 2D canvas context. Used internally by `replayOutput` and by `runWorker` to incrementally apply worker-posted steps to the display canvas.
|
|
94
|
+
|
|
95
|
+
**`stepDataToSVGElement(data: StepData): SVGElement`** — creates a DOM `SVGElement` from a `StepData`. Used internally by `replayOutput` and by `runWorker` to build the live SVG incrementally on the main thread.
|
|
96
|
+
|
|
90
97
|
### Utilities
|
|
91
98
|
|
|
92
99
|
- **`getFill(data: ImageDataLike): string`** — computes a fill color (average of corner pixels) from image data
|
|
@@ -101,14 +108,17 @@ interface ReplayResult {
|
|
|
101
108
|
- **ESM only** — no CommonJS; no `importScripts()`
|
|
102
109
|
- **Canvas reuse** — the original creates a new `<canvas>` element for every shape rasterization call (~58,000 per run at default settings). `prim-lib` reuses a single module-level canvas, growing it only when a larger bbox is seen. This delivered a ~27× speedup (37 s → ~0.8 s rasterize time per run)
|
|
103
110
|
- **`willReadFrequently: true`** — canvas contexts used for `getImageData` are created with this flag, keeping pixel data in CPU memory and suppressing browser warnings
|
|
104
|
-
- **
|
|
111
|
+
- **Worker-compatible** — `Canvas` detects its environment: in a browser context it creates `HTMLCanvasElement`; in a worker it creates `OffscreenCanvas` (same 2D API). `Canvas.fromBitmap()` loads an image from an `ImageBitmap` (worker-compatible) rather than `new Image()`. `Optimizer` accepts an injectable `schedule` function so callers can substitute `setTimeout` for `requestAnimationFrame` in worker contexts. The original included `importScripts()`-based worker stubs (not ESM-compatible) which were removed; worker support is now done properly via `prim-interface`'s `runWorker()`
|
|
105
112
|
- **Architecture split** — the original is a single-layer library. Here, `prim-lib` is the pure algorithm (no knowledge of how images arrive or where results go), and `prim-interface` is the adapter that wires it to the browser
|
|
106
113
|
- **`PreCfg` / `Cfg` distinction** — `Canvas.original()` accepts `PreCfg` (optional `width`/`height`) and resolves to a fully-populated `Cfg`, making the config lifecycle explicit in the types
|
|
107
114
|
- **Serialization** — `StepData` / `SerializedOutput` allow completed runs to be stored compactly and replayed via `replayOutput()` without re-running the optimizer
|
|
115
|
+
- **Additional shapes** — `Circle`, `Square`, `Hexagon`, and `Glyph` are not in the original; `Circle` is a uniform-radius variant of `Ellipse`
|
|
116
|
+
- **Shape weighting** — `shapeWeights` allows biased shape selection with a guaranteed distribution: the optimizer pre-allocates exact per-shape step counts (largest-remainder rounding) and shuffles them, so the final mix always matches the requested weights
|
|
108
117
|
|
|
109
118
|
## Architecture notes
|
|
110
119
|
|
|
111
|
-
- `Shape.rasterize()` reuses a single module-level canvas (`_rasterCanvas`) grown to the max shape size seen;
|
|
112
|
-
- `Cfg.shapeTypes` holds shape constructors; shapes are chosen randomly each step
|
|
113
|
-
- `
|
|
114
|
-
- `
|
|
120
|
+
- `Shape.rasterize()` reuses a single module-level canvas (`_rasterCanvas`) grown to the max shape size seen; in a worker, this is an `OffscreenCanvas` (environment-detected by the `Canvas` constructor). Avoids per-call canvas creation which was the dominant cost (27x speedup)
|
|
121
|
+
- `Cfg.shapeTypes` holds shape constructors; shapes are chosen randomly each step unless `shapeWeights` is set
|
|
122
|
+
- When `shapeWeights` is provided (same length as `shapeTypes`), `Optimizer` builds a step plan at construction time: each shape type is allocated an exact number of slots proportional to its weight (using largest-remainder rounding), then the plan is Fisher-Yates shuffled. This guarantees the final shape distribution matches the weights, rather than just biasing random selection
|
|
123
|
+
- `PreCfg` exists to bridge the gap between call time (dimensions unknown) and runtime (dimensions set by `Canvas.original()` or `Canvas.fromBitmap()`)
|
|
124
|
+
- `Canvas.svgRoot()` is a static helper shared by `Canvas.empty()` and `replayOutput()` to create the SVG root with clip path and background fill; it uses DOM APIs and is main-thread only
|
package/dist/index.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ interface ShapeInterface {
|
|
|
30
30
|
rasterize(alpha: number): {
|
|
31
31
|
getImageData(): ImageDataLike;
|
|
32
32
|
};
|
|
33
|
-
render(ctx:
|
|
33
|
+
render(ctx: Ctx2D): void;
|
|
34
34
|
}
|
|
35
35
|
interface Cfg {
|
|
36
36
|
width: number;
|
|
@@ -57,6 +57,7 @@ type PreCfg = Omit<Cfg, 'width' | 'height'> & {
|
|
|
57
57
|
height?: number;
|
|
58
58
|
};
|
|
59
59
|
type RGB = [number, number, number];
|
|
60
|
+
type Ctx2D = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
|
|
60
61
|
type StepData = {
|
|
61
62
|
t: 't';
|
|
62
63
|
a: number;
|
|
@@ -119,17 +120,18 @@ interface DrawableStep {
|
|
|
119
120
|
alpha: number;
|
|
120
121
|
color: string;
|
|
121
122
|
shape: {
|
|
122
|
-
render(ctx:
|
|
123
|
+
render(ctx: Ctx2D): void;
|
|
123
124
|
};
|
|
124
125
|
}
|
|
125
126
|
declare class Canvas {
|
|
126
|
-
node: HTMLCanvasElement;
|
|
127
|
-
ctx:
|
|
127
|
+
node: HTMLCanvasElement | OffscreenCanvas;
|
|
128
|
+
ctx: Ctx2D;
|
|
128
129
|
_imageData: ImageData | null;
|
|
129
130
|
static svgRoot(width: number, height: number, fill: string): SVGSVGElement;
|
|
130
131
|
static empty(cfg: Cfg, svg: true): SVGSVGElement;
|
|
131
132
|
static empty(cfg: Cfg, svg?: false): Canvas;
|
|
132
133
|
static original(url: string, cfg: PreCfg): Promise<Canvas>;
|
|
134
|
+
static fromBitmap(bitmap: ImageBitmap, cfg: PreCfg): Canvas;
|
|
133
135
|
static test(cfg: PreCfg): Canvas;
|
|
134
136
|
constructor(width: number, height: number, willReadFrequently?: boolean);
|
|
135
137
|
clone(): Canvas;
|
|
@@ -167,6 +169,7 @@ declare class Step {
|
|
|
167
169
|
mutate(): Step;
|
|
168
170
|
}
|
|
169
171
|
|
|
172
|
+
type ShapeCtor = new (w: number, h: number) => ShapeInterface;
|
|
170
173
|
declare class Optimizer {
|
|
171
174
|
cfg: Cfg;
|
|
172
175
|
state: State;
|
|
@@ -174,7 +177,9 @@ declare class Optimizer {
|
|
|
174
177
|
_steps: number;
|
|
175
178
|
_stopped: boolean;
|
|
176
179
|
_paused: boolean;
|
|
177
|
-
|
|
180
|
+
_stepPlan: ShapeCtor[];
|
|
181
|
+
_schedule: (fn: () => void) => void;
|
|
182
|
+
constructor(original: Canvas, cfg: Cfg, schedule?: (fn: () => void) => void);
|
|
178
183
|
start(): void;
|
|
179
184
|
stop(): void;
|
|
180
185
|
pause(): void;
|
|
@@ -196,7 +201,7 @@ declare class Shape implements ShapeInterface {
|
|
|
196
201
|
rasterize(alpha: number): {
|
|
197
202
|
getImageData(): ImageDataLike;
|
|
198
203
|
};
|
|
199
|
-
render(_ctx:
|
|
204
|
+
render(_ctx: Ctx2D): void;
|
|
200
205
|
}
|
|
201
206
|
declare class Polygon extends Shape {
|
|
202
207
|
points: Point[];
|
|
@@ -294,11 +299,13 @@ declare function computeColorAndDifferenceChange(offset: Bbox, imageData: ShapeI
|
|
|
294
299
|
differenceChange: number;
|
|
295
300
|
};
|
|
296
301
|
|
|
302
|
+
declare function renderStepToCtx(data: StepData, ctx: Ctx2D): void;
|
|
303
|
+
declare function stepDataToSVGElement(data: StepData): SVGElement;
|
|
297
304
|
interface ReplayResult {
|
|
298
|
-
raster: HTMLCanvasElement;
|
|
305
|
+
raster: HTMLCanvasElement | OffscreenCanvas;
|
|
299
306
|
svg: SVGSVGElement;
|
|
300
307
|
svgString: string;
|
|
301
308
|
}
|
|
302
309
|
declare function replayOutput(data: SerializedOutput): ReplayResult;
|
|
303
310
|
|
|
304
|
-
export { type Bbox, Canvas, type Cfg, Circle, Debug, Ellipse, Glyph, Hexagon, type ImageDataLike, Optimizer, type Point, type PreCfg, type RGB, Rectangle, type ReplayResult, SVGNS, type SerializedOutput, Shape, type ShapeImageData, type ShapeInterface, Square, State, Step, type StepData, Triangle, clamp, clampColor, computeColorAndDifferenceChange, difference, differenceToDistance, distanceToDifference, getFill, parseColor, replayOutput, stepPerf };
|
|
311
|
+
export { type Bbox, Canvas, type Cfg, Circle, type Ctx2D, Debug, Ellipse, Glyph, Hexagon, type ImageDataLike, Optimizer, type Point, type PreCfg, type RGB, Rectangle, type ReplayResult, SVGNS, type SerializedOutput, Shape, type ShapeImageData, type ShapeInterface, Square, State, Step, type StepData, Triangle, clamp, clampColor, computeColorAndDifferenceChange, difference, differenceToDistance, distanceToDifference, getFill, parseColor, renderStepToCtx, replayOutput, stepDataToSVGElement, stepPerf };
|
package/dist/index.js
CHANGED
|
@@ -190,6 +190,22 @@ var Canvas = class _Canvas {
|
|
|
190
190
|
};
|
|
191
191
|
});
|
|
192
192
|
}
|
|
193
|
+
static fromBitmap(bitmap, cfg) {
|
|
194
|
+
const w = bitmap.width;
|
|
195
|
+
const h = bitmap.height;
|
|
196
|
+
const computeScale = getScale(w, h, cfg.computeSize, cfg.allowUpscale);
|
|
197
|
+
cfg.width = w / computeScale;
|
|
198
|
+
cfg.height = h / computeScale;
|
|
199
|
+
const viewScale = getScale(w, h, cfg.viewSize, cfg.allowUpscale);
|
|
200
|
+
cfg.scale = computeScale / viewScale;
|
|
201
|
+
const fullCfg = { ...cfg, width: cfg.width, height: cfg.height };
|
|
202
|
+
const canvas = this.empty(fullCfg);
|
|
203
|
+
canvas.ctx.drawImage(bitmap, 0, 0, fullCfg.width, fullCfg.height);
|
|
204
|
+
if (cfg.fill === "auto") {
|
|
205
|
+
cfg.fill = getFill(canvas.getImageData());
|
|
206
|
+
}
|
|
207
|
+
return canvas;
|
|
208
|
+
}
|
|
193
209
|
static test(cfg) {
|
|
194
210
|
cfg.width = cfg.computeSize;
|
|
195
211
|
cfg.height = cfg.computeSize;
|
|
@@ -216,10 +232,17 @@ var Canvas = class _Canvas {
|
|
|
216
232
|
return canvas;
|
|
217
233
|
}
|
|
218
234
|
constructor(width, height, willReadFrequently = false) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
235
|
+
if (typeof document !== "undefined") {
|
|
236
|
+
const el = document.createElement("canvas");
|
|
237
|
+
el.width = width;
|
|
238
|
+
el.height = height;
|
|
239
|
+
this.node = el;
|
|
240
|
+
this.ctx = el.getContext("2d", { willReadFrequently });
|
|
241
|
+
} else {
|
|
242
|
+
const el = new OffscreenCanvas(width, height);
|
|
243
|
+
this.node = el;
|
|
244
|
+
this.ctx = el.getContext("2d", { willReadFrequently });
|
|
245
|
+
}
|
|
223
246
|
this._imageData = null;
|
|
224
247
|
}
|
|
225
248
|
clone() {
|
|
@@ -854,6 +877,22 @@ var Debug = class extends Shape {
|
|
|
854
877
|
};
|
|
855
878
|
|
|
856
879
|
// src/optimizer.ts
|
|
880
|
+
function buildStepPlan(cfg) {
|
|
881
|
+
const { shapeTypes, shapeWeights, steps } = cfg;
|
|
882
|
+
if (!shapeWeights || shapeWeights.length !== shapeTypes.length) return [];
|
|
883
|
+
const total = shapeWeights.reduce((sum, w) => sum + w, 0);
|
|
884
|
+
const floats = shapeWeights.map((w) => w / total * steps);
|
|
885
|
+
const floors = floats.map(Math.floor);
|
|
886
|
+
const remainder = steps - floors.reduce((s, n) => s + n, 0);
|
|
887
|
+
const indices = floats.map((f, i) => ({ i, frac: f - floors[i] })).sort((a, b) => b.frac - a.frac).slice(0, remainder).map((x) => x.i);
|
|
888
|
+
const counts = floors.map((n, i) => indices.includes(i) ? n + 1 : n);
|
|
889
|
+
const plan = shapeTypes.flatMap((ctor, i) => Array(counts[i]).fill(ctor));
|
|
890
|
+
for (let i = plan.length - 1; i > 0; i--) {
|
|
891
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
892
|
+
[plan[i], plan[j]] = [plan[j], plan[i]];
|
|
893
|
+
}
|
|
894
|
+
return plan;
|
|
895
|
+
}
|
|
857
896
|
var Optimizer = class {
|
|
858
897
|
cfg;
|
|
859
898
|
state;
|
|
@@ -861,7 +900,9 @@ var Optimizer = class {
|
|
|
861
900
|
_steps;
|
|
862
901
|
_stopped;
|
|
863
902
|
_paused;
|
|
864
|
-
|
|
903
|
+
_stepPlan;
|
|
904
|
+
_schedule;
|
|
905
|
+
constructor(original, cfg, schedule = (fn) => requestAnimationFrame(fn)) {
|
|
865
906
|
this.cfg = cfg;
|
|
866
907
|
this.state = new State(original, Canvas.empty(cfg));
|
|
867
908
|
this._steps = 0;
|
|
@@ -869,6 +910,8 @@ var Optimizer = class {
|
|
|
869
910
|
this._paused = false;
|
|
870
911
|
this.onStep = () => {
|
|
871
912
|
};
|
|
913
|
+
this._stepPlan = buildStepPlan(cfg);
|
|
914
|
+
this._schedule = schedule;
|
|
872
915
|
}
|
|
873
916
|
start() {
|
|
874
917
|
this._stopped = false;
|
|
@@ -904,15 +947,16 @@ var Optimizer = class {
|
|
|
904
947
|
return;
|
|
905
948
|
}
|
|
906
949
|
if (this._steps < this.cfg.steps) {
|
|
907
|
-
|
|
950
|
+
this._schedule(() => this._addShape());
|
|
908
951
|
}
|
|
909
952
|
}
|
|
910
953
|
_findBestStep() {
|
|
911
954
|
const LIMIT = this.cfg.shapes;
|
|
955
|
+
const ctor = this._stepPlan[this._steps];
|
|
912
956
|
let bestStep = null;
|
|
913
957
|
const promises = [];
|
|
914
958
|
for (let i = 0; i < LIMIT; i++) {
|
|
915
|
-
const shape = Shape.create(this.cfg);
|
|
959
|
+
const shape = ctor ? new ctor(this.cfg.width, this.cfg.height) : Shape.create(this.cfg);
|
|
916
960
|
const promise = new Step(shape, this.cfg).compute(this.state).then((step) => {
|
|
917
961
|
if (!bestStep || step.distance < bestStep.distance) {
|
|
918
962
|
bestStep = step;
|
|
@@ -957,7 +1001,7 @@ function hexPoints(cx, cy, r, angle) {
|
|
|
957
1001
|
return [~~(cx + r * Math.cos(a)), ~~(cy + r * Math.sin(a))];
|
|
958
1002
|
});
|
|
959
1003
|
}
|
|
960
|
-
function
|
|
1004
|
+
function renderStepToCtx(data, ctx) {
|
|
961
1005
|
ctx.globalAlpha = data.a;
|
|
962
1006
|
ctx.fillStyle = rgbString(data.c);
|
|
963
1007
|
switch (data.t) {
|
|
@@ -1002,7 +1046,7 @@ function renderStep(data, ctx) {
|
|
|
1002
1046
|
}
|
|
1003
1047
|
}
|
|
1004
1048
|
}
|
|
1005
|
-
function
|
|
1049
|
+
function stepDataToSVGElement(data) {
|
|
1006
1050
|
const color = rgbString(data.c);
|
|
1007
1051
|
const opacity = data.a.toFixed(2);
|
|
1008
1052
|
let node;
|
|
@@ -1070,8 +1114,8 @@ function replayOutput(data) {
|
|
|
1070
1114
|
svg.setAttribute("width", String(vw));
|
|
1071
1115
|
svg.setAttribute("height", String(vh));
|
|
1072
1116
|
for (const step of data.steps) {
|
|
1073
|
-
|
|
1074
|
-
svg.appendChild(
|
|
1117
|
+
renderStepToCtx(step, raster.ctx);
|
|
1118
|
+
svg.appendChild(stepDataToSVGElement(step));
|
|
1075
1119
|
}
|
|
1076
1120
|
const svgString = new XMLSerializer().serializeToString(svg);
|
|
1077
1121
|
return { raster: raster.node, svg, svgString };
|
|
@@ -1099,6 +1143,8 @@ export {
|
|
|
1099
1143
|
distanceToDifference,
|
|
1100
1144
|
getFill,
|
|
1101
1145
|
parseColor,
|
|
1146
|
+
renderStepToCtx,
|
|
1102
1147
|
replayOutput,
|
|
1148
|
+
stepDataToSVGElement,
|
|
1103
1149
|
stepPerf
|
|
1104
1150
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slithy/prim-lib",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Core engine for primitive-based image reconstruction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -14,11 +14,11 @@
|
|
|
14
14
|
],
|
|
15
15
|
"sideEffects": false,
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@vitest/coverage-v8": "^4.1.
|
|
18
|
-
"jsdom": "^
|
|
17
|
+
"@vitest/coverage-v8": "^4.1.2",
|
|
18
|
+
"jsdom": "^29.0.1",
|
|
19
19
|
"tsup": "^8",
|
|
20
20
|
"typescript": "^5",
|
|
21
|
-
"vitest": "^4",
|
|
21
|
+
"vitest": "^4.1.2",
|
|
22
22
|
"@slithy/eslint-config": "0.0.0",
|
|
23
23
|
"@slithy/tsconfig": "0.0.0"
|
|
24
24
|
},
|