poppygl 0.0.26 → 0.0.27

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 CHANGED
@@ -67,6 +67,30 @@ const png = await renderGLTFToPNGFromGLB(glb, {
67
67
 
68
68
  You can inspect the defaults via `getDefaultRenderOptions()` or reuse the internal merge logic with `resolveRenderOptions()`.
69
69
 
70
+ ### Hidden edges for selected sub-models
71
+
72
+ Set `showHiddenEdges` in a glTF node's `extras` to render that node with solid
73
+ visible edges and faint dashed edges where its geometry is occluded:
74
+
75
+ ```json
76
+ {
77
+ "nodes": [
78
+ { "name": "Housing", "mesh": 0 },
79
+ {
80
+ "name": "InternalBracket",
81
+ "mesh": 1,
82
+ "extras": { "showHiddenEdges": true }
83
+ }
84
+ ]
85
+ }
86
+ ```
87
+
88
+ The namespaced form `{ "extras": { "poppygl": { "showHiddenEdges": true } } }`
89
+ is also supported. The setting can be placed on a mesh or primitive; primitive
90
+ extras take precedence over node extras, which take precedence over mesh extras.
91
+ PoppyGL derives boundary and crease edges from triangle geometry, so no line
92
+ primitives need to be added to the asset.
93
+
70
94
  ## If you already have the GLTF JSON
71
95
 
72
96
  When the GLTF JSON object is already in memory (for example, bundled with your app), skip the network loader and supply resources directly:
package/dist/index.d.ts CHANGED
@@ -25,6 +25,7 @@ interface DrawCall {
25
25
  material: Material;
26
26
  colors?: Float32Array | null;
27
27
  mode?: number;
28
+ showHiddenEdges?: boolean;
28
29
  }
29
30
  interface GridOptions {
30
31
  size?: number | readonly [number, number, number];
@@ -114,6 +115,13 @@ declare function resolveRenderOptions(options?: RenderOptionsInput): RenderOptio
114
115
 
115
116
  declare function createGrid(options?: GridOptions): DrawCall;
116
117
 
118
+ /**
119
+ * Builds a line draw call from the boundary and crease edges of a triangle
120
+ * mesh. Vertices are welded by position so glTF meshes that duplicate vertices
121
+ * for normals or UV seams do not produce coplanar triangulation lines.
122
+ */
123
+ declare function createEdgeDrawCall(mesh: DrawCall, creaseAngleDegrees?: number): DrawCall | null;
124
+
117
125
  /**
118
126
  * Creates an infinite grid that matches the appearance of the 3d-viewer reference.
119
127
  * This grid uses a special rendering approach with procedural generation and fading.
@@ -163,6 +171,14 @@ interface LightSettings {
163
171
  dir: readonly [number, number, number];
164
172
  ambient: number;
165
173
  }
174
+ interface LineRenderOptions {
175
+ depthCompare?: "less" | "less-equal" | "greater";
176
+ depthWrite?: boolean;
177
+ depthBias?: number;
178
+ dashed?: boolean;
179
+ dashLength?: number;
180
+ opacity?: number;
181
+ }
166
182
  declare class SoftwareRenderer {
167
183
  readonly width: number;
168
184
  readonly height: number;
@@ -172,7 +188,7 @@ declare class SoftwareRenderer {
172
188
  get buffer(): Uint8Array<ArrayBufferLike> | Uint8ClampedArray<ArrayBufferLike>;
173
189
  clear(colorRGBA?: [number, number, number, number]): void;
174
190
  setPixel(x: number, y: number, r: number, g: number, b: number, a: number): void;
175
- drawLines(mesh: DrawCall, camera: Camera, gammaOut?: boolean): void;
191
+ drawLines(mesh: DrawCall, camera: Camera, gammaOut?: boolean, options?: LineRenderOptions): void;
176
192
  sampleTextureNearest(img: BitmapLike | null, u: number, v: number): MutableRGBA;
177
193
  perspInterp(attrs: number[][], invWs: number[], lambdas: number[]): number[];
178
194
  drawMesh(mesh: DrawCall, camera: Camera, light: LightSettings, material: Material, cullBackFaces?: boolean, gammaOut?: boolean): void;
@@ -204,4 +220,4 @@ declare function renderGLTFToPNGFromGLB(glb: ArrayBuffer | Uint8Array, options?:
204
220
 
205
221
  declare function renderSceneFromGLTF(scene: GLTFScene, options?: RenderOptionsInput, imageFactory?: ImageFactory): RenderResult;
206
222
 
207
- 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 RenderGLTFToPNGFromGLBOptions, type RenderGLTFToPNGFromURLOptions, type RenderOptions, type RenderOptionsInput, type RenderResult, SoftwareRenderer, bufferFromDataURI, buildCamera, computeSmoothNormals, computeWorldAABB, createGrid, createInfiniteGrid, createSceneFromGLTF, createUint8Bitmap, decodeImageFromBuffer, drawInfiniteGrid, encodePNG, getDefaultRenderOptions, hexToRgb, isJPG, isPNG, loadGLTFWithResourcesFromURL, renderDrawCalls, renderGLTFToPNGFromGLB, renderGLTFToPNGFromURL, renderSceneFromGLTF, resolveRenderOptions };
223
+ 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 LineRenderOptions, type LoadGLTFWithResourcesFromURLOptions, type Material, type MutableRGBA, type RGBA, type RenderGLTFToPNGFromGLBOptions, type RenderGLTFToPNGFromURLOptions, type RenderOptions, type RenderOptionsInput, type RenderResult, SoftwareRenderer, bufferFromDataURI, buildCamera, computeSmoothNormals, computeWorldAABB, createEdgeDrawCall, createGrid, createInfiniteGrid, createSceneFromGLTF, createUint8Bitmap, decodeImageFromBuffer, drawInfiniteGrid, encodePNG, getDefaultRenderOptions, hexToRgb, isJPG, isPNG, loadGLTFWithResourcesFromURL, renderDrawCalls, renderGLTFToPNGFromGLB, renderGLTFToPNGFromURL, renderSceneFromGLTF, resolveRenderOptions };
package/dist/index.js CHANGED
@@ -249,6 +249,87 @@ function createGrid(options = {}) {
249
249
  };
250
250
  }
251
251
 
252
+ // lib/gltf/createEdgeDrawCall.ts
253
+ var DEFAULT_CREASE_ANGLE_DEGREES = 30;
254
+ function positionKey(positions, index) {
255
+ return `${positions[index * 3 + 0]},${positions[index * 3 + 1]},${positions[index * 3 + 2]}`;
256
+ }
257
+ function triangleNormal(positions, i0, i1, i2) {
258
+ const ax = positions[i1 * 3 + 0] - positions[i0 * 3 + 0];
259
+ const ay = positions[i1 * 3 + 1] - positions[i0 * 3 + 1];
260
+ const az = positions[i1 * 3 + 2] - positions[i0 * 3 + 2];
261
+ const bx = positions[i2 * 3 + 0] - positions[i0 * 3 + 0];
262
+ const by = positions[i2 * 3 + 1] - positions[i0 * 3 + 1];
263
+ const bz = positions[i2 * 3 + 2] - positions[i0 * 3 + 2];
264
+ const nx = ay * bz - az * by;
265
+ const ny = az * bx - ax * bz;
266
+ const nz = ax * by - ay * bx;
267
+ const length = Math.hypot(nx, ny, nz);
268
+ if (length === 0) return null;
269
+ return [nx / length, ny / length, nz / length];
270
+ }
271
+ function addEdge(edges, positions, i0, i1, normal) {
272
+ const key0 = positionKey(positions, i0);
273
+ const key1 = positionKey(positions, i1);
274
+ const isForward = key0 < key1;
275
+ const key = isForward ? `${key0}|${key1}` : `${key1}|${key0}`;
276
+ const indices = isForward ? [i0, i1] : [i1, i0];
277
+ const existing = edges.get(key);
278
+ if (existing) {
279
+ if (normal) existing.normals.push(normal);
280
+ } else {
281
+ edges.set(key, { indices, normals: normal ? [normal] : [] });
282
+ }
283
+ }
284
+ function hasCrease(normals, minimumDot) {
285
+ if (normals.length < 2) return true;
286
+ for (let i = 0; i < normals.length; i++) {
287
+ for (let j = i + 1; j < normals.length; j++) {
288
+ const a = normals[i];
289
+ const b = normals[j];
290
+ const dot = a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
291
+ if (dot < minimumDot) return true;
292
+ }
293
+ }
294
+ return false;
295
+ }
296
+ function createEdgeDrawCall(mesh, creaseAngleDegrees = DEFAULT_CREASE_ANGLE_DEGREES) {
297
+ if (mesh.mode != null && mesh.mode !== 4) return null;
298
+ const vertexCount = Math.floor(mesh.positions.length / 3);
299
+ const triangleIndices = mesh.indices ?? Uint32Array.from({ length: vertexCount }, (_, i) => i);
300
+ if (triangleIndices.length < 3) return null;
301
+ const edges = /* @__PURE__ */ new Map();
302
+ for (let i = 0; i + 2 < triangleIndices.length; i += 3) {
303
+ const i0 = triangleIndices[i];
304
+ const i1 = triangleIndices[i + 1];
305
+ const i2 = triangleIndices[i + 2];
306
+ const normal = triangleNormal(mesh.positions, i0, i1, i2);
307
+ addEdge(edges, mesh.positions, i0, i1, normal);
308
+ addEdge(edges, mesh.positions, i1, i2, normal);
309
+ addEdge(edges, mesh.positions, i2, i0, normal);
310
+ }
311
+ const minimumDot = Math.cos(creaseAngleDegrees * Math.PI / 180);
312
+ const edgeIndices = [];
313
+ for (const edge2 of edges.values()) {
314
+ if (!hasCrease(edge2.normals, minimumDot)) continue;
315
+ edgeIndices.push(edge2.indices[0], edge2.indices[1]);
316
+ }
317
+ if (edgeIndices.length === 0) return null;
318
+ return {
319
+ positions: mesh.positions,
320
+ normals: null,
321
+ uvs: null,
322
+ indices: new Uint32Array(edgeIndices),
323
+ model: mesh.model,
324
+ material: {
325
+ baseColorFactor: [0, 0, 0, 1],
326
+ baseColorTexture: null,
327
+ alphaMode: "BLEND"
328
+ },
329
+ mode: 1
330
+ };
331
+ }
332
+
252
333
  // lib/image/createUint8Bitmap.ts
253
334
  var createUint8Bitmap = (width, height) => ({
254
335
  width,
@@ -459,7 +540,7 @@ var SoftwareRenderer = class {
459
540
  this.buffer[idx + 2] = b;
460
541
  this.buffer[idx + 3] = a;
461
542
  }
462
- drawLines(mesh, camera, gammaOut = true) {
543
+ drawLines(mesh, camera, gammaOut = true, options = {}) {
463
544
  const { positions, indices, model, material } = mesh;
464
545
  if (!indices) return;
465
546
  const view = camera.view;
@@ -507,7 +588,12 @@ var SoftwareRenderer = class {
507
588
  const r255 = r * 255 | 0;
508
589
  const g255 = g * 255 | 0;
509
590
  const b255 = b * 255 | 0;
510
- const a255 = clamp(a, 0, 1) * 255 | 0;
591
+ const opacity = clamp(options.opacity ?? 1, 0, 1);
592
+ const sourceAlpha = clamp(a, 0, 1) * opacity;
593
+ const depthCompare = options.depthCompare ?? "less";
594
+ const depthWrite = options.depthWrite ?? true;
595
+ const depthBias = options.depthBias ?? 0;
596
+ const dashLength = Math.max(1, options.dashLength ?? 4);
511
597
  for (let i = 0; i < indices.length; i += 2) {
512
598
  const i0 = indices[i + 0];
513
599
  const i1 = indices[i + 1];
@@ -534,13 +620,28 @@ var SoftwareRenderer = class {
534
620
  let y = y0;
535
621
  let z = z0_01;
536
622
  for (let k = 0; k <= steps; k++) {
623
+ if (options.dashed && Math.floor(k / dashLength) % 2 === 1) {
624
+ x += xinc;
625
+ y += yinc;
626
+ z += zinc;
627
+ continue;
628
+ }
537
629
  const xi = Math.round(x);
538
630
  const yi = Math.round(y);
539
631
  if (xi >= 0 && xi < this.width && yi >= 0 && yi < this.height && z >= 0 && z <= 1) {
540
632
  const di = yi * this.width + xi;
541
- if (z < this.depth[di]) {
542
- this.depth[di] = z;
543
- this.setPixel(xi, yi, r255, g255, b255, a255);
633
+ const currentDepth = this.depth[di];
634
+ const passesDepth = depthCompare === "greater" ? z > currentDepth + depthBias : depthCompare === "less-equal" ? z <= currentDepth + depthBias : z < currentDepth - depthBias;
635
+ if (passesDepth) {
636
+ if (depthWrite) this.depth[di] = z;
637
+ const pixelIndex = di * 4;
638
+ const destinationAlpha = this.buffer[pixelIndex + 3] / 255;
639
+ const oneMinusSourceAlpha = 1 - sourceAlpha;
640
+ const outAlpha = sourceAlpha + destinationAlpha * oneMinusSourceAlpha;
641
+ const outR = outAlpha === 0 ? 0 : (r255 * sourceAlpha + this.buffer[pixelIndex + 0] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
642
+ const outG = outAlpha === 0 ? 0 : (g255 * sourceAlpha + this.buffer[pixelIndex + 1] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
643
+ const outB = outAlpha === 0 ? 0 : (b255 * sourceAlpha + this.buffer[pixelIndex + 2] * destinationAlpha * oneMinusSourceAlpha) / outAlpha;
644
+ this.setPixel(xi, yi, outR, outG, outB, outAlpha * 255);
544
645
  }
545
646
  }
546
647
  x += xinc;
@@ -1249,6 +1350,24 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
1249
1350
  });
1250
1351
  }
1251
1352
  renderGroup(blendDraws);
1353
+ for (const dc of drawCalls) {
1354
+ if (!dc.showHiddenEdges) continue;
1355
+ const edgeDrawCall = createEdgeDrawCall(dc);
1356
+ if (!edgeDrawCall) continue;
1357
+ renderer.drawLines(edgeDrawCall, camera, options.gamma, {
1358
+ depthCompare: "less-equal",
1359
+ depthWrite: false,
1360
+ depthBias: 1e-4
1361
+ });
1362
+ renderer.drawLines(edgeDrawCall, camera, options.gamma, {
1363
+ depthCompare: "greater",
1364
+ depthWrite: false,
1365
+ depthBias: 1e-4,
1366
+ dashed: true,
1367
+ dashLength: 4 * options.supersampling,
1368
+ opacity: 0.35
1369
+ });
1370
+ }
1252
1371
  if (options.debugPoints?.length) {
1253
1372
  drawDebugPoints(
1254
1373
  renderer,
@@ -1363,6 +1482,15 @@ var NUM_COMPONENTS = {
1363
1482
  MAT3: 9,
1364
1483
  MAT4: 16
1365
1484
  };
1485
+ function readShowHiddenEdges(extras) {
1486
+ if (typeof extras?.poppygl?.showHiddenEdges === "boolean") {
1487
+ return extras.poppygl.showHiddenEdges;
1488
+ }
1489
+ if (typeof extras?.showHiddenEdges === "boolean") {
1490
+ return extras.showHiddenEdges;
1491
+ }
1492
+ return void 0;
1493
+ }
1366
1494
  function getBufferViewSlice(ctx, bufferViewIndex, byteOffset) {
1367
1495
  const { gltf, buffers } = ctx;
1368
1496
  const bufferView = gltf.bufferViews?.[bufferViewIndex];
@@ -1596,7 +1724,8 @@ function createSceneFromGLTF(gltf, resources) {
1596
1724
  indices,
1597
1725
  model: world,
1598
1726
  material,
1599
- colors
1727
+ colors,
1728
+ showHiddenEdges: readShowHiddenEdges(primitive.extras) ?? readShowHiddenEdges(node.extras) ?? readShowHiddenEdges(mesh.extras) ?? false
1600
1729
  });
1601
1730
  }
1602
1731
  }
@@ -1954,6 +2083,7 @@ export {
1954
2083
  buildCamera,
1955
2084
  computeSmoothNormals,
1956
2085
  computeWorldAABB,
2086
+ createEdgeDrawCall,
1957
2087
  createGrid,
1958
2088
  createInfiniteGrid,
1959
2089
  createSceneFromGLTF,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "poppygl",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.26",
4
+ "version": "0.0.27",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tscircuit/poppygl.git"