poppygl 0.0.19 → 0.0.21
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 +2 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +199 -47
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -53,6 +53,8 @@ const png = await renderGLTFToPNGBufferFromGLBBuffer(glb, {
|
|
|
53
53
|
- `supersampling`: render at `width * supersampling` / `height * supersampling`, then downsample (default `1`).
|
|
54
54
|
- `fov`: vertical field of view in degrees (defaults to `35`).
|
|
55
55
|
- `camPos` and `lookAt`: override the auto-framed camera position and target.
|
|
56
|
+
- `up`: choose the world-up axis for the camera with `"y+" | "y-" | "x+" | "x-" | "z+" | "z-"`.
|
|
57
|
+
- `cameraRotation`: apply Euler degrees `{ x, y, z }` to define camera orientation directly. When set, it takes precedence over `lookAt`.
|
|
56
58
|
- `debugPoints`: optional `{ label, position }[]` overlay rendered on top of the final PNG for world-space debugging markers.
|
|
57
59
|
- `debugFontSize`: optional pixel size for `debugPoints` labels; when omitted the renderer derives a size from the image dimensions.
|
|
58
60
|
- `debugPointColor`: optional RGB tuple for debug point markers.
|
package/dist/index.d.ts
CHANGED
|
@@ -61,6 +61,12 @@ interface DebugPoint {
|
|
|
61
61
|
z: number;
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
+
type CameraUp = "y+" | "y-" | "x+" | "x-" | "z+" | "z-";
|
|
65
|
+
interface CameraRotation {
|
|
66
|
+
x: number;
|
|
67
|
+
y: number;
|
|
68
|
+
z: number;
|
|
69
|
+
}
|
|
64
70
|
/**
|
|
65
71
|
* Convert a hex color string to RGB array with values 0-1
|
|
66
72
|
* @param hex - Hex color string like "#ffffff" or "ffffff"
|
|
@@ -78,6 +84,8 @@ interface RenderOptions {
|
|
|
78
84
|
lightDir: readonly [number, number, number];
|
|
79
85
|
camPos?: readonly [number, number, number] | null;
|
|
80
86
|
lookAt?: readonly [number, number, number] | null;
|
|
87
|
+
up: CameraUp;
|
|
88
|
+
cameraRotation: CameraRotation | null;
|
|
81
89
|
backgroundColor?: readonly [number, number, number] | string | null;
|
|
82
90
|
grid?: boolean | GridOptions;
|
|
83
91
|
debugPoints?: DebugPoint[] | null;
|
|
@@ -93,7 +101,7 @@ interface Camera {
|
|
|
93
101
|
view: mat4;
|
|
94
102
|
proj: mat4;
|
|
95
103
|
}
|
|
96
|
-
declare function buildCamera(drawCalls: DrawCall[], width: number, height: number, fovDeg: number, camPos: readonly [number, number, number] | null | undefined, lookAt: readonly [number, number, number] | null | undefined): Camera;
|
|
104
|
+
declare function buildCamera(drawCalls: DrawCall[], width: number, height: number, fovDeg: number, camPos: readonly [number, number, number] | null | undefined, lookAt: readonly [number, number, number] | null | undefined, up?: CameraUp | null | undefined, cameraRotation?: CameraRotation | null | undefined): Camera;
|
|
97
105
|
|
|
98
106
|
interface RenderResult {
|
|
99
107
|
bitmap: BitmapLike;
|
|
@@ -200,4 +208,4 @@ declare function renderGLTFToPNGBuffer(gltfPathOrString: string, options?: Rende
|
|
|
200
208
|
|
|
201
209
|
declare function renderSceneFromGLTF(scene: GLTFScene, options?: RenderOptionsInput, imageFactory?: ImageFactory): RenderResult;
|
|
202
210
|
|
|
203
|
-
export { type BitmapLike, type Camera, DEFAULT_LIGHT_DIR, DEFAULT_RENDER_OPTIONS, type DebugPoint, type DrawCall, type FetchLike, type GLTFResources, type GLTFScene, type GridOptions, type ImageFactory, type LightSettings, type LoadGLTFWithResourcesFromURLOptions, type Material, type MutableRGBA, type RGBA, type RenderGLTFToPNGBufferFromGLBBufferOptions, type RenderGLTFToPNGBufferFromURLOptions, type RenderOptions, type RenderOptionsInput, type RenderResult, SoftwareRenderer, bufferFromDataURI, buildCamera, computeSmoothNormals, computeWorldAABB, createGrid, createInfiniteGrid, createSceneFromGLTF, createUint8Bitmap, decodeImageFromBuffer, drawInfiniteGrid, encodePNGToBuffer, getDefaultRenderOptions, hexToRgb, isJPG, isPNG, loadGLTFWithResourcesFromURL, pureImageFactory, renderDrawCalls, renderGLTFToPNGBuffer, renderGLTFToPNGBufferFromGLBBuffer, renderGLTFToPNGBufferFromURL, renderSceneFromGLTF, resolveRenderOptions };
|
|
211
|
+
export { type BitmapLike, type Camera, type CameraRotation, type CameraUp, DEFAULT_LIGHT_DIR, DEFAULT_RENDER_OPTIONS, type DebugPoint, type DrawCall, type FetchLike, type GLTFResources, type GLTFScene, type GridOptions, type ImageFactory, type LightSettings, type LoadGLTFWithResourcesFromURLOptions, type Material, type MutableRGBA, type RGBA, type RenderGLTFToPNGBufferFromGLBBufferOptions, type RenderGLTFToPNGBufferFromURLOptions, type RenderOptions, type RenderOptionsInput, type RenderResult, SoftwareRenderer, bufferFromDataURI, buildCamera, computeSmoothNormals, computeWorldAABB, createGrid, createInfiniteGrid, createSceneFromGLTF, createUint8Bitmap, decodeImageFromBuffer, drawInfiniteGrid, encodePNGToBuffer, getDefaultRenderOptions, hexToRgb, isJPG, isPNG, loadGLTFWithResourcesFromURL, pureImageFactory, renderDrawCalls, renderGLTFToPNGBuffer, renderGLTFToPNGBufferFromGLBBuffer, renderGLTFToPNGBufferFromURL, renderSceneFromGLTF, resolveRenderOptions };
|
package/dist/index.js
CHANGED
|
@@ -36,49 +36,173 @@ function computeWorldAABB(drawCalls) {
|
|
|
36
36
|
var toRad = (degrees) => degrees * Math.PI / 180;
|
|
37
37
|
|
|
38
38
|
// lib/camera/buildCamera.ts
|
|
39
|
-
function
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
39
|
+
function getWorldUpVector(up) {
|
|
40
|
+
switch (up) {
|
|
41
|
+
case "y-":
|
|
42
|
+
return vec3.fromValues(0, -1, 0);
|
|
43
|
+
case "x+":
|
|
44
|
+
return vec3.fromValues(1, 0, 0);
|
|
45
|
+
case "x-":
|
|
46
|
+
return vec3.fromValues(-1, 0, 0);
|
|
47
|
+
case "z+":
|
|
48
|
+
return vec3.fromValues(0, 0, 1);
|
|
49
|
+
case "z-":
|
|
50
|
+
return vec3.fromValues(0, 0, -1);
|
|
51
|
+
case "y+":
|
|
52
|
+
default:
|
|
53
|
+
return vec3.fromValues(0, 1, 0);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function buildCameraWorldMatrix(eye, forward, up) {
|
|
57
|
+
const normalizedForward = vec3.normalize(vec3.create(), forward);
|
|
58
|
+
const right = vec3.cross(vec3.create(), normalizedForward, up);
|
|
59
|
+
if (vec3.squaredLength(right) === 0) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
vec3.normalize(right, right);
|
|
63
|
+
const correctedUp = vec3.cross(vec3.create(), right, normalizedForward);
|
|
64
|
+
if (vec3.squaredLength(correctedUp) === 0) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
vec3.normalize(correctedUp, correctedUp);
|
|
68
|
+
return mat4.fromValues(
|
|
69
|
+
right[0],
|
|
70
|
+
right[1],
|
|
71
|
+
right[2],
|
|
72
|
+
0,
|
|
73
|
+
correctedUp[0],
|
|
74
|
+
correctedUp[1],
|
|
75
|
+
correctedUp[2],
|
|
76
|
+
0,
|
|
77
|
+
-normalizedForward[0],
|
|
78
|
+
-normalizedForward[1],
|
|
79
|
+
-normalizedForward[2],
|
|
80
|
+
0,
|
|
81
|
+
eye[0],
|
|
82
|
+
eye[1],
|
|
83
|
+
eye[2],
|
|
84
|
+
1
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
function getDefaultForwardForUp(up) {
|
|
88
|
+
const zForward = vec3.fromValues(0, 0, -1);
|
|
89
|
+
if (Math.abs(vec3.dot(up, zForward)) < 0.999) {
|
|
90
|
+
return zForward;
|
|
91
|
+
}
|
|
92
|
+
return vec3.fromValues(1, 0, 0);
|
|
93
|
+
}
|
|
94
|
+
function buildViewFromRotation(eye, up, rotation) {
|
|
95
|
+
const cameraWorld = buildCameraWorldMatrix(
|
|
96
|
+
eye,
|
|
97
|
+
getDefaultForwardForUp(up),
|
|
98
|
+
up
|
|
99
|
+
);
|
|
100
|
+
if (!cameraWorld) {
|
|
101
|
+
return mat4.create();
|
|
102
|
+
}
|
|
103
|
+
if (rotation.x !== 0) {
|
|
104
|
+
mat4.rotateX(cameraWorld, cameraWorld, toRad(rotation.x));
|
|
105
|
+
}
|
|
106
|
+
if (rotation.y !== 0) {
|
|
107
|
+
mat4.rotateY(cameraWorld, cameraWorld, toRad(rotation.y));
|
|
108
|
+
}
|
|
109
|
+
if (rotation.z !== 0) {
|
|
110
|
+
mat4.rotateZ(cameraWorld, cameraWorld, toRad(rotation.z));
|
|
111
|
+
}
|
|
112
|
+
const view = mat4.create();
|
|
113
|
+
mat4.invert(view, cameraWorld);
|
|
114
|
+
return view;
|
|
115
|
+
}
|
|
116
|
+
function buildViewFromLookAt(eye, center, up) {
|
|
117
|
+
const forward = vec3.subtract(vec3.create(), center, eye);
|
|
118
|
+
if (vec3.squaredLength(forward) === 0) {
|
|
119
|
+
return mat4.create();
|
|
120
|
+
}
|
|
121
|
+
const cameraWorld = buildCameraWorldMatrix(eye, forward, up);
|
|
122
|
+
if (!cameraWorld) {
|
|
123
|
+
return mat4.create();
|
|
124
|
+
}
|
|
125
|
+
const view = mat4.create();
|
|
126
|
+
mat4.invert(view, cameraWorld);
|
|
127
|
+
return view;
|
|
128
|
+
}
|
|
129
|
+
function getAutoDistance(drawCalls, fovDeg) {
|
|
130
|
+
const aabb = computeWorldAABB(drawCalls);
|
|
131
|
+
const diag = vec3.distance(
|
|
132
|
+
vec3.fromValues(aabb.min[0], aabb.min[1], aabb.min[2]),
|
|
133
|
+
vec3.fromValues(aabb.max[0], aabb.max[1], aabb.max[2])
|
|
134
|
+
);
|
|
135
|
+
const radius = diag * 0.5;
|
|
136
|
+
const fov = toRad(fovDeg);
|
|
137
|
+
return radius / Math.tan(fov * 0.5) + radius * 0.5;
|
|
138
|
+
}
|
|
139
|
+
function getAABBCornerDistances(aabb, eye) {
|
|
140
|
+
const xs = [aabb.min[0], aabb.max[0]];
|
|
141
|
+
const ys = [aabb.min[1], aabb.max[1]];
|
|
142
|
+
const zs = [aabb.min[2], aabb.max[2]];
|
|
143
|
+
let nearest = Infinity;
|
|
144
|
+
let farthest = 0;
|
|
145
|
+
for (const x of xs) {
|
|
146
|
+
for (const y of ys) {
|
|
147
|
+
for (const z of zs) {
|
|
148
|
+
const distance = vec3.distance(eye, vec3.fromValues(x, y, z));
|
|
149
|
+
nearest = Math.min(nearest, distance);
|
|
150
|
+
farthest = Math.max(farthest, distance);
|
|
151
|
+
}
|
|
58
152
|
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
153
|
+
}
|
|
154
|
+
return { nearest, farthest };
|
|
155
|
+
}
|
|
156
|
+
function getCameraForwardFromView(view) {
|
|
157
|
+
const cameraWorld = mat4.create();
|
|
158
|
+
mat4.invert(cameraWorld, view);
|
|
159
|
+
return vec3.fromValues(-cameraWorld[8], -cameraWorld[9], -cameraWorld[10]);
|
|
160
|
+
}
|
|
161
|
+
function buildCamera(drawCalls, width, height, fovDeg, camPos, lookAt, up = "y+", cameraRotation = null) {
|
|
162
|
+
const aspect = width / height;
|
|
163
|
+
const worldUp = getWorldUpVector(up);
|
|
164
|
+
const aabb = computeWorldAABB(drawCalls);
|
|
165
|
+
const center = vec3.fromValues(
|
|
166
|
+
0.5 * (aabb.min[0] + aabb.max[0]),
|
|
167
|
+
0.5 * (aabb.min[1] + aabb.max[1]),
|
|
168
|
+
0.5 * (aabb.min[2] + aabb.max[2])
|
|
169
|
+
);
|
|
170
|
+
const autoDistance = getAutoDistance(drawCalls, fovDeg);
|
|
171
|
+
if (cameraRotation) {
|
|
172
|
+
const provisionalEye = camPos ? vec3.fromValues(camPos[0], camPos[1], camPos[2]) : vec3.clone(center);
|
|
173
|
+
const provisionalView = buildViewFromRotation(
|
|
174
|
+
provisionalEye,
|
|
175
|
+
worldUp,
|
|
176
|
+
cameraRotation
|
|
69
177
|
);
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
center[1] + dist * 0.3,
|
|
76
|
-
center[2] + dist
|
|
178
|
+
const eye2 = camPos ? provisionalEye : vec3.scaleAndAdd(
|
|
179
|
+
vec3.create(),
|
|
180
|
+
center,
|
|
181
|
+
getCameraForwardFromView(provisionalView),
|
|
182
|
+
-autoDistance
|
|
77
183
|
);
|
|
184
|
+
const { nearest: nearest2, farthest: farthest2 } = getAABBCornerDistances(aabb, eye2);
|
|
185
|
+
const near2 = Math.max(0.01, nearest2 * 0.5);
|
|
186
|
+
const far2 = Math.max(1e3, farthest2 * 1.1);
|
|
187
|
+
const proj2 = mat4.create();
|
|
188
|
+
mat4.perspective(proj2, toRad(fovDeg), aspect, near2, far2);
|
|
189
|
+
return {
|
|
190
|
+
view: buildViewFromRotation(eye2, worldUp, cameraRotation),
|
|
191
|
+
proj: proj2
|
|
192
|
+
};
|
|
78
193
|
}
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
194
|
+
const eye = camPos ? vec3.fromValues(camPos[0], camPos[1], camPos[2]) : vec3.fromValues(
|
|
195
|
+
center[0] + autoDistance,
|
|
196
|
+
center[1] + autoDistance * 0.3,
|
|
197
|
+
center[2] + autoDistance
|
|
198
|
+
);
|
|
199
|
+
const target = lookAt ? vec3.fromValues(lookAt[0], lookAt[1], lookAt[2]) : center;
|
|
200
|
+
const { nearest, farthest } = getAABBCornerDistances(aabb, eye);
|
|
201
|
+
const near = Math.max(0.01, nearest * 0.5);
|
|
202
|
+
const far = Math.max(1e3, farthest * 1.1);
|
|
203
|
+
const proj = mat4.create();
|
|
204
|
+
mat4.perspective(proj, toRad(fovDeg), aspect, near, far);
|
|
205
|
+
const view = buildViewFromLookAt(eye, target, worldUp);
|
|
82
206
|
return { view, proj };
|
|
83
207
|
}
|
|
84
208
|
|
|
@@ -274,6 +398,8 @@ var DEFAULT_RENDER_OPTIONS = {
|
|
|
274
398
|
lightDir: DEFAULT_LIGHT_DIR,
|
|
275
399
|
camPos: null,
|
|
276
400
|
lookAt: null,
|
|
401
|
+
up: "y+",
|
|
402
|
+
cameraRotation: null,
|
|
277
403
|
backgroundColor: null,
|
|
278
404
|
grid: false,
|
|
279
405
|
debugPoints: null,
|
|
@@ -484,8 +610,8 @@ var SoftwareRenderer = class {
|
|
|
484
610
|
const ndcX = c[0] * invW;
|
|
485
611
|
const ndcY = c[1] * invW;
|
|
486
612
|
const ndcZ = c[2] * invW;
|
|
487
|
-
const sx =
|
|
488
|
-
const sy =
|
|
613
|
+
const sx = (ndcX * 0.5 + 0.5) * (this.width - 1);
|
|
614
|
+
const sy = (1 - (ndcY * 0.5 + 0.5)) * (this.height - 1);
|
|
489
615
|
vScreen[i] = [sx, sy];
|
|
490
616
|
vInvW[i] = invW;
|
|
491
617
|
vNDCz[i] = ndcZ;
|
|
@@ -515,10 +641,16 @@ var SoftwareRenderer = class {
|
|
|
515
641
|
const area = edge(v0, v1, v2);
|
|
516
642
|
if (area === 0) continue;
|
|
517
643
|
if (cullBackFaces && area < 0) continue;
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
644
|
+
const minX = Math.max(0, Math.floor(Math.min(v0[0], v1[0], v2[0])));
|
|
645
|
+
const maxX = Math.min(
|
|
646
|
+
this.width - 1,
|
|
647
|
+
Math.ceil(Math.max(v0[0], v1[0], v2[0]))
|
|
648
|
+
);
|
|
649
|
+
const minY = Math.max(0, Math.floor(Math.min(v0[1], v1[1], v2[1])));
|
|
650
|
+
const maxY = Math.min(
|
|
651
|
+
this.height - 1,
|
|
652
|
+
Math.ceil(Math.max(v0[1], v1[1], v2[1]))
|
|
653
|
+
);
|
|
522
654
|
const invW = [
|
|
523
655
|
vInvW[i0],
|
|
524
656
|
vInvW[i1],
|
|
@@ -557,6 +689,7 @@ var SoftwareRenderer = class {
|
|
|
557
689
|
const l2 = w2 * invArea;
|
|
558
690
|
const zndc = l0 * ndcZ[0] + l1 * ndcZ[1] + l2 * ndcZ[2];
|
|
559
691
|
const z01 = zndc * 0.5 + 0.5;
|
|
692
|
+
if (z01 < 0 || z01 > 1) continue;
|
|
560
693
|
const di = y * this.width + x;
|
|
561
694
|
const depth = this.depth;
|
|
562
695
|
if (z01 >= depth[di]) continue;
|
|
@@ -973,6 +1106,21 @@ function drawInfiniteGrid(software_renderer, params) {
|
|
|
973
1106
|
}
|
|
974
1107
|
|
|
975
1108
|
// lib/render/resolveRenderOptions.ts
|
|
1109
|
+
var CAMERA_UP_VALUES = /* @__PURE__ */ new Set(["y+", "y-", "x+", "x-", "z+", "z-"]);
|
|
1110
|
+
function resolveCameraUp(up) {
|
|
1111
|
+
return up != null && CAMERA_UP_VALUES.has(up) ? up : DEFAULT_RENDER_OPTIONS.up;
|
|
1112
|
+
}
|
|
1113
|
+
function resolveCameraRotation(cameraRotation) {
|
|
1114
|
+
if (cameraRotation == null) {
|
|
1115
|
+
return DEFAULT_RENDER_OPTIONS.cameraRotation;
|
|
1116
|
+
}
|
|
1117
|
+
const resolveAxis = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1118
|
+
return {
|
|
1119
|
+
x: resolveAxis(cameraRotation.x),
|
|
1120
|
+
y: resolveAxis(cameraRotation.y),
|
|
1121
|
+
z: resolveAxis(cameraRotation.z)
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
976
1124
|
function resolveRenderOptions(options = {}) {
|
|
977
1125
|
const supersampling = typeof options.supersampling === "number" && Number.isFinite(options.supersampling) ? Math.max(1, Math.floor(options.supersampling)) : DEFAULT_RENDER_OPTIONS.supersampling;
|
|
978
1126
|
return {
|
|
@@ -981,7 +1129,9 @@ function resolveRenderOptions(options = {}) {
|
|
|
981
1129
|
supersampling,
|
|
982
1130
|
lightDir: options.lightDir != null ? options.lightDir : DEFAULT_RENDER_OPTIONS.lightDir,
|
|
983
1131
|
camPos: options.camPos !== void 0 ? options.camPos : DEFAULT_RENDER_OPTIONS.camPos,
|
|
984
|
-
lookAt: options.lookAt !== void 0 ? options.lookAt : DEFAULT_RENDER_OPTIONS.lookAt
|
|
1132
|
+
lookAt: options.lookAt !== void 0 ? options.lookAt : DEFAULT_RENDER_OPTIONS.lookAt,
|
|
1133
|
+
up: resolveCameraUp(options.up),
|
|
1134
|
+
cameraRotation: resolveCameraRotation(options.cameraRotation)
|
|
985
1135
|
};
|
|
986
1136
|
}
|
|
987
1137
|
|
|
@@ -996,7 +1146,9 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
|
|
|
996
1146
|
renderHeight,
|
|
997
1147
|
options.fov,
|
|
998
1148
|
options.camPos ?? null,
|
|
999
|
-
options.lookAt ?? null
|
|
1149
|
+
options.lookAt ?? null,
|
|
1150
|
+
options.up,
|
|
1151
|
+
options.cameraRotation ?? null
|
|
1000
1152
|
);
|
|
1001
1153
|
const renderer = new SoftwareRenderer(renderWidth, renderHeight, imageFactory);
|
|
1002
1154
|
let rgb = null;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "poppygl",
|
|
3
3
|
"main": "dist/index.js",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.21",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/tscircuit/poppygl.git"
|
|
@@ -15,12 +15,14 @@
|
|
|
15
15
|
"build": "tsup-node ./lib/index.ts --format esm --dts",
|
|
16
16
|
"format": "biome format --write .",
|
|
17
17
|
"format:check": "biome format .",
|
|
18
|
-
"test": "bun test"
|
|
18
|
+
"test": "bun test",
|
|
19
|
+
"test:playwright": "playwright test"
|
|
19
20
|
},
|
|
20
21
|
"type": "module",
|
|
21
22
|
"devDependencies": {
|
|
22
23
|
"@biomejs/biome": "^2.2.4",
|
|
23
|
-
"@
|
|
24
|
+
"@playwright/test": "^1.60.0",
|
|
25
|
+
"@tscircuit/alphabet": "0.0.23",
|
|
24
26
|
"@types/bun": "^1.2.23",
|
|
25
27
|
"@types/readable-stream": "^4.0.21",
|
|
26
28
|
"looks-same": "^10.0.1",
|