poppygl 0.0.36 → 0.0.38

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
@@ -245,6 +245,28 @@ Pass `explosionLines` to any render entrypoint to draw polylines between assembl
245
245
  and displaced part positions. The caller moves the geometry and supplies the
246
246
  points in the same glTF scene frame and units (normally right-handed, +Y up).
247
247
 
248
+ The selected glTF scene can embed the same setting in `extras.poppygl.explosionLines`:
249
+
250
+ ```json
251
+ {
252
+ "scene": 0,
253
+ "scenes": [{
254
+ "nodes": [0],
255
+ "extras": {
256
+ "poppygl": {
257
+ "explosionLines": [{ "points": [[0, 0, 0], [0, 20, 0]], "dashed": true }]
258
+ }
259
+ }
260
+ }]
261
+ }
262
+ ```
263
+
264
+ `renderSceneFromGLTF`, the URL renderer, and the GLB renderer use these guides
265
+ when `explosionLines` is omitted. An explicit list replaces the embedded guides;
266
+ `null` or `[]` hides them. Points are already in glTF world coordinates and are
267
+ not transformed by individual nodes. No guide meshes are required, and other
268
+ glTF viewers may ignore this optional metadata.
269
+
248
270
  This lamp is adapted from [core's reference-surface lamp](https://github.com/tscircuit/core/blob/81bef4337570e3ac4d7dd6d30ab63481304e8147/tests/assembly/fixtures/reference-surface-lamp.tsx).
249
271
  Its base, hollow blue stem, and bulb stay in place while the hollow shade,
250
272
  mounting collar, and three spokes move together by **+60 mm on scene Y**. The
@@ -295,3 +317,57 @@ controls both the shaft and arrowhead stroke width. Omit `arrow` or set it to
295
317
  - `encodePNG` packs any `BitmapLike` into a `Uint8Array` PNG that can be written to disk or served over the network.
296
318
 
297
319
  Happy rendering!
320
+
321
+ ### Outline rendering
322
+
323
+ ```ts
324
+ const png = await renderGLTFToPNGFromURL("https://example.com/housing.glb", {
325
+ renderStyle: "outline",
326
+ width: 1200,
327
+ height: 900,
328
+ supersampling: 2,
329
+ camPos: [80, 60, 100],
330
+ lookAt: [0, 0, 0],
331
+ showHiddenEdges: true,
332
+ edgeWidth: 1.5,
333
+ })
334
+ ```
335
+
336
+ `renderStyle: "outline"` uses white opaque surfaces, black boundary/crease
337
+ edges and view-dependent silhouettes, and orthographic projection by default.
338
+ Hidden edges are omitted by default, including model-level hidden-edge flags;
339
+ `showHiddenEdges: true` adds dashed occluded strokes with `hiddenEdgeOpacity`.
340
+ Materials, textures, vertex colors, transparency and lighting are ignored in this
341
+ style. Input meshes are not modified. Existing shaded rendering defaults stay
342
+ unchanged. The same options work with `renderDrawCalls`, GLB bytes, and glTF/GLB URLs.
343
+
344
+ `projection: "orthographic"` also works independently of outline style.
345
+ `orthographicHeight` sets the vertical field of view in model scene units; omit it
346
+ to fit the transformed scene bounds with a margin. Orthographic projection preserves
347
+ parallel edges and apparent size with distance. `camPos`, `lookAt`, `up` and
348
+ `cameraRotation` keep their existing meanings. For an axis-aligned view choose a
349
+ camera on that axis and an `up` axis that is not parallel to it.
350
+
351
+ Section views are available in outline style:
352
+
353
+ ```ts
354
+ const png = await renderGLTFToPNGFromGLB(glbBytes, {
355
+ renderStyle: "outline",
356
+ up: "y+",
357
+ section: { axis: "x", offset: 5, keepSide: "less", hatchSpacing: 1 },
358
+ })
359
+ ```
360
+
361
+ The plane is specified in **world glTF coordinates**, normally +Y up, using model
362
+ scene units. `less` retains axis coordinates <= offset; `greater` retains >= offset.
363
+ Closed cut surfaces receive diagonal hatching in the section plane (spacing defaults
364
+ to one scene unit). Nested loops preserve cavities. Open cut chains remain uncapped; non-manifold inputs are unsupported. `createOutlineSection` exposes the prepared draw calls and
365
+ `openChains` count for callers that need to report incomplete caps. Primitives are
366
+ capped independently; surfaces split across separate primitives cannot form a cap.
367
+ Sections outside or tangent to a mesh do not create artificial caps. Section cuts
368
+ currently require outline style; shaded sections are not supported.
369
+
370
+ These are raster outline illustrations, not CAD/B-rep exports: silhouettes
371
+ follow the input tessellation, crease extraction uses a 30-degree threshold, and
372
+ hidden lines are resolved against raster depth. Dimension annotations, paper sheet
373
+ layout and TSX/core integration are separate downstream work.
package/dist/index.d.ts CHANGED
@@ -1,64 +1,21 @@
1
1
  import { mat4 } from 'gl-matrix';
2
2
 
3
- type RGBA = readonly [number, number, number, number];
4
- type MutableRGBA = [number, number, number, number];
5
- interface BitmapLike {
6
- width: number;
7
- height: number;
8
- data: Uint8Array | Uint8ClampedArray;
3
+ type SectionAxis = "x" | "y" | "z";
4
+ /** Axis-aligned plane in world/glTF coordinates, in model scene units. */
5
+ interface OutlineSection {
6
+ axis: SectionAxis;
7
+ offset: number;
8
+ keepSide: "less" | "greater";
9
+ /** Distance between diagonal hatch strokes in scene units; defaults to 1. */
10
+ hatchSpacing?: number;
9
11
  }
10
- type ImageFactory = (width: number, height: number) => BitmapLike;
11
- declare const createUint8Bitmap: ImageFactory;
12
-
13
- interface Material {
14
- baseColorFactor: [number, number, number, number];
15
- baseColorTexture: BitmapLike | null;
16
- metallicFactor?: number;
17
- roughnessFactor?: number;
18
- emissiveFactor?: [number, number, number];
19
- emissiveTexture?: BitmapLike | null;
20
- emissiveStrength?: number;
21
- alphaMode?: "OPAQUE" | "MASK" | "BLEND";
22
- alphaCutoff?: number;
23
- }
24
- interface DrawCall {
25
- positions: Float32Array;
26
- normals: Float32Array | null;
27
- uvs: Float32Array | null;
28
- indices: Uint32Array | null;
29
- model: mat4;
30
- material: Material;
31
- colors?: Float32Array | null;
32
- mode?: number;
33
- showHiddenEdges?: boolean;
34
- /** Source triangle third vertices for each extracted edge segment. */
35
- edgeAdjacentVertices?: number[][];
36
- }
37
- interface GridOptions {
38
- size?: number | readonly [number, number, number];
39
- divisions?: number;
40
- color?: readonly [number, number, number];
41
- offset?: Partial<{
42
- x: number;
43
- y: number;
44
- z: number;
45
- }>;
46
- infiniteGrid?: boolean;
47
- cellSize?: number;
48
- sectionSize?: number;
49
- fadeDistance?: number;
50
- fadeStrength?: number;
51
- gridColor?: readonly [number, number, number];
52
- sectionColor?: readonly [number, number, number];
53
- }
54
- interface GLTFResources {
55
- buffers: Uint8Array[];
56
- images: BitmapLike[];
57
- }
58
- interface GLTFScene {
12
+ /** Prepare opaque monochrome section geometry without modifying the input.
13
+ * Separate primitives are capped independently; open surfaces remain uncapped.
14
+ */
15
+ declare function createOutlineSection(drawCalls: DrawCall[], spec: OutlineSection): {
59
16
  drawCalls: DrawCall[];
60
- gltf: any;
61
- }
17
+ openChains: number;
18
+ };
62
19
 
63
20
  declare const DEFAULT_LIGHT_DIR: readonly [-0.4, -0.9, -0.2];
64
21
  interface DebugPoint {
@@ -97,6 +54,14 @@ interface CameraRotation {
97
54
  */
98
55
  declare function hexToRgb(hex: string): [number, number, number] | null;
99
56
  interface RenderOptions {
57
+ /** White opaque surfaces with black boundary, crease and silhouette edges. */
58
+ renderStyle: "shaded" | "outline";
59
+ /** Section cuts are supported in outline style only. */
60
+ section: OutlineSection | null;
61
+ /** Outline defaults to orthographic; existing renders stay perspective. */
62
+ projection: "perspective" | "orthographic";
63
+ /** Vertical span in scene units. Null automatically fits the world bounds. */
64
+ orthographicHeight: number | null;
100
65
  /** Opt into metallic/roughness studio lighting and geometry-based soft shadows. */
101
66
  realistic: boolean;
102
67
  width: number;
@@ -133,11 +98,77 @@ type RenderOptionsInput = Partial<RenderOptions>;
133
98
  declare const DEFAULT_RENDER_OPTIONS: RenderOptions;
134
99
  declare function getDefaultRenderOptions(): RenderOptions;
135
100
 
101
+ type RGBA = readonly [number, number, number, number];
102
+ type MutableRGBA = [number, number, number, number];
103
+ interface BitmapLike {
104
+ width: number;
105
+ height: number;
106
+ data: Uint8Array | Uint8ClampedArray;
107
+ }
108
+ type ImageFactory = (width: number, height: number) => BitmapLike;
109
+ declare const createUint8Bitmap: ImageFactory;
110
+
111
+ interface Material {
112
+ baseColorFactor: [number, number, number, number];
113
+ baseColorTexture: BitmapLike | null;
114
+ /** Repeat rather than clamp the base-color texture (used by section hatching). */
115
+ baseColorTextureWrap?: "repeat" | "clamp";
116
+ metallicFactor?: number;
117
+ roughnessFactor?: number;
118
+ emissiveFactor?: [number, number, number];
119
+ emissiveTexture?: BitmapLike | null;
120
+ emissiveStrength?: number;
121
+ alphaMode?: "OPAQUE" | "MASK" | "BLEND";
122
+ alphaCutoff?: number;
123
+ }
124
+ interface DrawCall {
125
+ /** Generated section cap; preserve its hatch material in outline style. */
126
+ outlineSectionCap?: boolean;
127
+ positions: Float32Array;
128
+ normals: Float32Array | null;
129
+ uvs: Float32Array | null;
130
+ indices: Uint32Array | null;
131
+ model: mat4;
132
+ material: Material;
133
+ colors?: Float32Array | null;
134
+ mode?: number;
135
+ showHiddenEdges?: boolean;
136
+ /** Source triangle third vertices for each extracted edge segment. */
137
+ edgeAdjacentVertices?: number[][];
138
+ }
139
+ interface GridOptions {
140
+ size?: number | readonly [number, number, number];
141
+ divisions?: number;
142
+ color?: readonly [number, number, number];
143
+ offset?: Partial<{
144
+ x: number;
145
+ y: number;
146
+ z: number;
147
+ }>;
148
+ infiniteGrid?: boolean;
149
+ cellSize?: number;
150
+ sectionSize?: number;
151
+ fadeDistance?: number;
152
+ fadeStrength?: number;
153
+ gridColor?: readonly [number, number, number];
154
+ sectionColor?: readonly [number, number, number];
155
+ }
156
+ interface GLTFResources {
157
+ buffers: Uint8Array[];
158
+ images: BitmapLike[];
159
+ }
160
+ interface GLTFScene {
161
+ /** Selected scene extras.poppygl.explosionLines, in glTF world coordinates. */
162
+ explosionLines?: readonly ExplosionLine[];
163
+ drawCalls: DrawCall[];
164
+ gltf: any;
165
+ }
166
+
136
167
  interface Camera {
137
168
  view: mat4;
138
169
  proj: mat4;
139
170
  }
140
- 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;
171
+ 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, projection?: "perspective" | "orthographic", orthographicHeight?: number | null): Camera;
141
172
 
142
173
  interface RenderResult {
143
174
  bitmap: BitmapLike;
@@ -155,7 +186,12 @@ declare function createGrid(options?: GridOptions): DrawCall;
155
186
  * mesh. Vertices are welded by position so glTF meshes that duplicate vertices
156
187
  * for normals or UV seams do not produce coplanar triangulation lines.
157
188
  */
158
- declare function createEdgeDrawCall(mesh: DrawCall, creaseAngleDegrees?: number): DrawCall | null;
189
+ declare function createEdgeDrawCall(mesh: DrawCall, creaseAngleDegrees?: number,
190
+ /** Include view-dependent silhouettes, with crease angles measured after transforms. */
191
+ camera?: {
192
+ view: mat4;
193
+ projection: "perspective" | "orthographic";
194
+ }): DrawCall | null;
159
195
 
160
196
  /**
161
197
  * Creates an infinite grid that matches the appearance of the 3d-viewer reference.
@@ -277,6 +313,9 @@ declare function renderGLTFToPNGFromURL(url: string, options?: RenderGLTFToPNGFr
277
313
  type RenderGLTFToPNGFromGLBOptions = RenderOptionsInput;
278
314
  declare function renderGLTFToPNGFromGLB(glb: ArrayBuffer | Uint8Array, options?: RenderGLTFToPNGFromGLBOptions): Promise<Uint8Array>;
279
315
 
316
+ /** Render the selected glTF scene's guides in scene-world coordinates.
317
+ * Explicit options override extras; null or [] disables embedded guides.
318
+ */
280
319
  declare function renderSceneFromGLTF(scene: GLTFScene, options?: RenderOptionsInput, imageFactory?: ImageFactory): RenderResult;
281
320
 
282
- export { type BitmapLike, type Camera, type CameraRotation, type CameraUp, DEFAULT_LIGHT_DIR, DEFAULT_RENDER_OPTIONS, type DebugPoint, type DrawCall, type ExplosionLine, 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 };
321
+ export { type BitmapLike, type Camera, type CameraRotation, type CameraUp, DEFAULT_LIGHT_DIR, DEFAULT_RENDER_OPTIONS, type DebugPoint, type DrawCall, type ExplosionLine, type FetchLike, type GLTFResources, type GLTFScene, type GridOptions, type ImageFactory, type LightSettings, type LineRenderOptions, type LoadGLTFWithResourcesFromURLOptions, type Material, type MutableRGBA, type OutlineSection, type RGBA, type RenderGLTFToPNGFromGLBOptions, type RenderGLTFToPNGFromURLOptions, type RenderOptions, type RenderOptionsInput, type RenderResult, SoftwareRenderer, bufferFromDataURI, buildCamera, computeSmoothNormals, computeWorldAABB, createEdgeDrawCall, createGrid, createInfiniteGrid, createOutlineSection, createSceneFromGLTF, createUint8Bitmap, decodeImageFromBuffer, drawInfiniteGrid, encodePNG, getDefaultRenderOptions, hexToRgb, isJPG, isPNG, loadGLTFWithResourcesFromURL, renderDrawCalls, renderGLTFToPNGFromGLB, renderGLTFToPNGFromURL, renderSceneFromGLTF, resolveRenderOptions };
package/dist/index.js CHANGED
@@ -1,5 +1,276 @@
1
- // lib/camera/buildCamera.ts
1
+ // lib/render/createOutlineSection.ts
2
2
  import { mat4, vec3 } from "gl-matrix";
3
+ import earcut from "earcut";
4
+
5
+ // lib/image/createUint8Bitmap.ts
6
+ var createUint8Bitmap = (width, height) => ({
7
+ width,
8
+ height,
9
+ data: new Uint8ClampedArray(width * height * 4)
10
+ });
11
+
12
+ // lib/render/createOutlineSection.ts
13
+ function findSectionLoops(input) {
14
+ const { positions, axis, offset, epsilon = 1e-5 } = input;
15
+ const k = axis === "x" ? 0 : axis === "y" ? 1 : 2;
16
+ let minimum = Infinity, maximum = -Infinity;
17
+ for (let i = k; i < positions.length; i += 3) {
18
+ minimum = Math.min(minimum, positions[i]);
19
+ maximum = Math.max(maximum, positions[i]);
20
+ }
21
+ if (offset <= minimum + epsilon || offset >= maximum - epsilon)
22
+ return { loops: [], openChains: 0 };
23
+ const u = (k + 1) % 3;
24
+ const v = (k + 2) % 3;
25
+ const points = /* @__PURE__ */ new Map();
26
+ const adjacency = /* @__PURE__ */ new Map();
27
+ const edges = /* @__PURE__ */ new Set();
28
+ const key = (p) => `${Math.round(p[0] / epsilon)},${Math.round(p[1] / epsilon)}`;
29
+ for (let i = 0; i < positions.length; i += 9) {
30
+ const hits = [];
31
+ const distances = [0, 1, 2].map((j) => positions[i + j * 3 + k] - offset);
32
+ const onPlane = distances.filter((d) => Math.abs(d) <= epsilon).length;
33
+ if (onPlane === 3) continue;
34
+ if (!(distances.some((d) => d > epsilon) && distances.some((d) => d < -epsilon)) && !(onPlane === 2 && distances.some((d) => d < -epsilon)))
35
+ continue;
36
+ for (let j = 0; j < 3; j++) {
37
+ const a2 = i + j * 3;
38
+ const b2 = i + (j + 1) % 3 * 3;
39
+ const da = distances[j];
40
+ const db = distances[(j + 1) % 3];
41
+ if (Math.abs(da) <= epsilon)
42
+ hits.push([positions[a2 + u], positions[a2 + v]]);
43
+ else if (da > 0 !== db > 0 && Math.abs(db) > epsilon) {
44
+ const t = da / (da - db);
45
+ hits.push([
46
+ positions[a2 + u] + t * (positions[b2 + u] - positions[a2 + u]),
47
+ positions[a2 + v] + t * (positions[b2 + v] - positions[a2 + v])
48
+ ]);
49
+ }
50
+ }
51
+ const unique = [...new Map(hits.map((p) => [key(p), p])).values()];
52
+ if (unique.length !== 2) continue;
53
+ const [a, b] = unique.map(key);
54
+ if (a === b) continue;
55
+ const edge2 = a < b ? `${a}|${b}` : `${b}|${a}`;
56
+ if (edges.has(edge2)) continue;
57
+ edges.add(edge2);
58
+ points.set(a, unique[0]);
59
+ points.set(b, unique[1]);
60
+ if (!adjacency.has(a)) adjacency.set(a, /* @__PURE__ */ new Set());
61
+ if (!adjacency.has(b)) adjacency.set(b, /* @__PURE__ */ new Set());
62
+ adjacency.get(a).add(b);
63
+ adjacency.get(b).add(a);
64
+ }
65
+ const loops = [];
66
+ let openChains = 0;
67
+ while (edges.size) {
68
+ const first = edges.values().next().value;
69
+ const [start, next] = first.split("|");
70
+ const path = [start];
71
+ let previous = start;
72
+ let current = next;
73
+ const remove = (a, b) => {
74
+ edges.delete(a < b ? `${a}|${b}` : `${b}|${a}`);
75
+ adjacency.get(a)?.delete(b);
76
+ adjacency.get(b)?.delete(a);
77
+ };
78
+ remove(start, next);
79
+ while (current !== start) {
80
+ path.push(current);
81
+ const candidates = [...adjacency.get(current) ?? []];
82
+ if (!candidates.length) break;
83
+ const p = points.get(previous);
84
+ const c = points.get(current);
85
+ candidates.sort((a, b) => {
86
+ const score = (s) => {
87
+ const n = points.get(s);
88
+ return ((c[0] - p[0]) * (n[0] - c[0]) + (c[1] - p[1]) * (n[1] - c[1])) / (Math.hypot(n[0] - c[0], n[1] - c[1]) || 1);
89
+ };
90
+ return score(b) - score(a);
91
+ });
92
+ const chosen = candidates[0];
93
+ remove(current, chosen);
94
+ previous = current;
95
+ current = chosen;
96
+ }
97
+ if (current === start && path.length >= 3)
98
+ loops.push(path.map((p) => points.get(p)));
99
+ else openChains++;
100
+ }
101
+ return { loops, openChains };
102
+ }
103
+ function contains(p, polygon) {
104
+ let inside = false;
105
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
106
+ const a = polygon[i], b = polygon[j];
107
+ if (a[1] > p[1] !== b[1] > p[1] && p[0] < (b[0] - a[0]) * (p[1] - a[1]) / (b[1] - a[1]) + a[0])
108
+ inside = !inside;
109
+ }
110
+ return inside;
111
+ }
112
+ function createOutlineSection(drawCalls, spec) {
113
+ const k = spec.axis === "x" ? 0 : spec.axis === "y" ? 1 : 2;
114
+ const u = (k + 1) % 3, v = (k + 2) % 3;
115
+ const spacing = spec.hatchSpacing ?? 1;
116
+ if (!["x", "y", "z"].includes(spec.axis) || !Number.isFinite(spec.offset) || !["less", "greater"].includes(spec.keepSide) || !Number.isFinite(spacing) || spacing <= 0)
117
+ throw new Error("Invalid outline section plane or hatch spacing");
118
+ const output = [];
119
+ let openChains = 0;
120
+ const texture = createUint8Bitmap(128, 128);
121
+ for (let y = 0; y < 128; y++)
122
+ for (let x = 0; x < 128; x++) {
123
+ const c = (x + 127 - y) % 16 < 2 ? 0 : 255;
124
+ texture.data.set([c, c, c, 255], (y * 128 + x) * 4);
125
+ }
126
+ const sign = spec.keepSide === "less" ? 1 : -1;
127
+ for (const dc of drawCalls) {
128
+ const points = [];
129
+ for (let i = 0; i < dc.positions.length; i += 3)
130
+ points.push(
131
+ Array.from(
132
+ vec3.transformMat4(
133
+ vec3.create(),
134
+ dc.positions.subarray(i, i + 3),
135
+ dc.model
136
+ )
137
+ )
138
+ );
139
+ const indices = dc.indices ?? Uint32Array.from(points, (_, i) => i);
140
+ const clipped = [], triangles = [];
141
+ const distance = (p) => sign * (p[k] - spec.offset);
142
+ const intersection = (a, b) => {
143
+ const t = distance(a) / (distance(a) - distance(b));
144
+ return a.map((n, j) => n + t * (b[j] - n));
145
+ };
146
+ if (dc.mode === 1) {
147
+ for (let i = 0; i + 1 < indices.length; i += 2) {
148
+ let a = points[indices[i]], b = points[indices[i + 1]];
149
+ if (distance(a) > 0 && distance(b) > 0) continue;
150
+ if (distance(a) > 0) a = intersection(a, b);
151
+ else if (distance(b) > 0) b = intersection(a, b);
152
+ clipped.push(...a, ...b);
153
+ }
154
+ output.push({
155
+ ...dc,
156
+ positions: new Float32Array(clipped),
157
+ indices: null,
158
+ model: mat4.create()
159
+ });
160
+ continue;
161
+ }
162
+ if (dc.mode != null && dc.mode !== 4) continue;
163
+ for (let i = 0; i + 2 < indices.length; i += 3) {
164
+ const triangle = [
165
+ points[indices[i]],
166
+ points[indices[i + 1]],
167
+ points[indices[i + 2]]
168
+ ];
169
+ triangles.push(...triangle.flat());
170
+ const polygon = [];
171
+ for (let j = 0; j < 3; j++) {
172
+ const a = triangle[j], b = triangle[(j + 1) % 3];
173
+ if (distance(a) <= 0) polygon.push(a);
174
+ if (distance(a) < 0 && distance(b) > 0 || distance(a) > 0 && distance(b) < 0)
175
+ polygon.push(intersection(a, b));
176
+ }
177
+ for (let j = 1; j + 1 < polygon.length; j++)
178
+ clipped.push(...polygon[0], ...polygon[j], ...polygon[j + 1]);
179
+ }
180
+ const white = {
181
+ baseColorFactor: [1, 1, 1, 1],
182
+ baseColorTexture: null
183
+ };
184
+ const mesh = {
185
+ ...dc,
186
+ positions: new Float32Array(clipped),
187
+ indices: null,
188
+ model: mat4.create(),
189
+ normals: null,
190
+ colors: null,
191
+ uvs: null,
192
+ material: white
193
+ };
194
+ output.push(mesh);
195
+ const extent = Math.max(
196
+ ...[0, 1, 2].map((axis) => {
197
+ let min = Infinity, max = -Infinity;
198
+ for (const p of points) {
199
+ min = Math.min(min, p[axis]);
200
+ max = Math.max(max, p[axis]);
201
+ }
202
+ return max - min;
203
+ })
204
+ );
205
+ const result = findSectionLoops({
206
+ positions: new Float32Array(triangles),
207
+ axis: spec.axis,
208
+ offset: spec.offset,
209
+ epsilon: Math.max(extent * 1e-7, 1e-9)
210
+ });
211
+ openChains += result.openChains;
212
+ const rings = result.loops.map((points2) => ({
213
+ points: points2,
214
+ area: Math.abs(
215
+ points2.reduce((a, p, i) => {
216
+ const q = points2[(i + 1) % points2.length];
217
+ return a + p[0] * q[1] - q[0] * p[1];
218
+ }, 0)
219
+ ),
220
+ parent: -1,
221
+ depth: 0
222
+ })).sort((a, b) => b.area - a.area);
223
+ rings.forEach((ring, i) => {
224
+ for (let j = 0; j < i; j++)
225
+ if (contains(ring.points[0], rings[j].points)) ring.parent = j;
226
+ ring.depth = ring.parent < 0 ? 0 : rings[ring.parent].depth + 1;
227
+ });
228
+ const cap = [], uv = [];
229
+ rings.forEach((ring, i) => {
230
+ if (ring.depth % 2) return;
231
+ const holes = rings.filter((r) => r.parent === i && r.depth % 2);
232
+ const flat = [...ring.points], starts = [];
233
+ for (const hole of holes) {
234
+ starts.push(flat.length);
235
+ flat.push(...hole.points);
236
+ }
237
+ const faces = earcut(flat.flat(), starts, 2);
238
+ for (let j = 0; j < faces.length; j += 3) {
239
+ const ids = faces.slice(j, j + 3);
240
+ const a = flat[ids[0]], b = flat[ids[1]], c = flat[ids[2]];
241
+ if (((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])) * sign < 0)
242
+ ids.reverse();
243
+ for (const id of ids) {
244
+ const p = flat[id], xyz = [0, 0, 0];
245
+ xyz[k] = spec.offset;
246
+ xyz[u] = p[0];
247
+ xyz[v] = p[1];
248
+ cap.push(...xyz);
249
+ uv.push(
250
+ p[0] / (spacing * 8 * Math.SQRT2),
251
+ p[1] / (spacing * 8 * Math.SQRT2)
252
+ );
253
+ }
254
+ }
255
+ });
256
+ if (cap.length)
257
+ output.push({
258
+ ...mesh,
259
+ outlineSectionCap: true,
260
+ positions: new Float32Array(cap),
261
+ uvs: new Float32Array(uv),
262
+ material: {
263
+ ...white,
264
+ baseColorTexture: texture,
265
+ baseColorTextureWrap: "repeat"
266
+ }
267
+ });
268
+ }
269
+ return { drawCalls: output, openChains };
270
+ }
271
+
272
+ // lib/camera/buildCamera.ts
273
+ import { mat4 as mat42, vec3 as vec32 } from "gl-matrix";
3
274
 
4
275
  // lib/gltf/computeWorldAABB.ts
5
276
  import { vec4 } from "gl-matrix";
@@ -39,33 +310,33 @@ var toRad = (degrees) => degrees * Math.PI / 180;
39
310
  function getWorldUpVector(up) {
40
311
  switch (up) {
41
312
  case "y-":
42
- return vec3.fromValues(0, -1, 0);
313
+ return vec32.fromValues(0, -1, 0);
43
314
  case "x+":
44
- return vec3.fromValues(1, 0, 0);
315
+ return vec32.fromValues(1, 0, 0);
45
316
  case "x-":
46
- return vec3.fromValues(-1, 0, 0);
317
+ return vec32.fromValues(-1, 0, 0);
47
318
  case "z+":
48
- return vec3.fromValues(0, 0, 1);
319
+ return vec32.fromValues(0, 0, 1);
49
320
  case "z-":
50
- return vec3.fromValues(0, 0, -1);
321
+ return vec32.fromValues(0, 0, -1);
51
322
  case "y+":
52
323
  default:
53
- return vec3.fromValues(0, 1, 0);
324
+ return vec32.fromValues(0, 1, 0);
54
325
  }
55
326
  }
56
327
  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) {
328
+ const normalizedForward = vec32.normalize(vec32.create(), forward);
329
+ const right = vec32.cross(vec32.create(), normalizedForward, up);
330
+ if (vec32.squaredLength(right) === 0) {
60
331
  return null;
61
332
  }
62
- vec3.normalize(right, right);
63
- const correctedUp = vec3.cross(vec3.create(), right, normalizedForward);
64
- if (vec3.squaredLength(correctedUp) === 0) {
333
+ vec32.normalize(right, right);
334
+ const correctedUp = vec32.cross(vec32.create(), right, normalizedForward);
335
+ if (vec32.squaredLength(correctedUp) === 0) {
65
336
  return null;
66
337
  }
67
- vec3.normalize(correctedUp, correctedUp);
68
- return mat4.fromValues(
338
+ vec32.normalize(correctedUp, correctedUp);
339
+ return mat42.fromValues(
69
340
  right[0],
70
341
  right[1],
71
342
  right[2],
@@ -85,11 +356,11 @@ function buildCameraWorldMatrix(eye, forward, up) {
85
356
  );
86
357
  }
87
358
  function getDefaultForwardForUp(up) {
88
- const zForward = vec3.fromValues(0, 0, -1);
89
- if (Math.abs(vec3.dot(up, zForward)) < 0.999) {
359
+ const zForward = vec32.fromValues(0, 0, -1);
360
+ if (Math.abs(vec32.dot(up, zForward)) < 0.999) {
90
361
  return zForward;
91
362
  }
92
- return vec3.fromValues(1, 0, 0);
363
+ return vec32.fromValues(1, 0, 0);
93
364
  }
94
365
  function buildViewFromRotation(eye, up, rotation) {
95
366
  const cameraWorld = buildCameraWorldMatrix(
@@ -98,39 +369,39 @@ function buildViewFromRotation(eye, up, rotation) {
98
369
  up
99
370
  );
100
371
  if (!cameraWorld) {
101
- return mat4.create();
372
+ return mat42.create();
102
373
  }
103
374
  if (rotation.x !== 0) {
104
- mat4.rotateX(cameraWorld, cameraWorld, toRad(rotation.x));
375
+ mat42.rotateX(cameraWorld, cameraWorld, toRad(rotation.x));
105
376
  }
106
377
  if (rotation.y !== 0) {
107
- mat4.rotateY(cameraWorld, cameraWorld, toRad(rotation.y));
378
+ mat42.rotateY(cameraWorld, cameraWorld, toRad(rotation.y));
108
379
  }
109
380
  if (rotation.z !== 0) {
110
- mat4.rotateZ(cameraWorld, cameraWorld, toRad(rotation.z));
381
+ mat42.rotateZ(cameraWorld, cameraWorld, toRad(rotation.z));
111
382
  }
112
- const view = mat4.create();
113
- mat4.invert(view, cameraWorld);
383
+ const view = mat42.create();
384
+ mat42.invert(view, cameraWorld);
114
385
  return view;
115
386
  }
116
387
  function buildViewFromLookAt(eye, center, up) {
117
- const forward = vec3.subtract(vec3.create(), center, eye);
118
- if (vec3.squaredLength(forward) === 0) {
119
- return mat4.create();
388
+ const forward = vec32.subtract(vec32.create(), center, eye);
389
+ if (vec32.squaredLength(forward) === 0) {
390
+ return mat42.create();
120
391
  }
121
392
  const cameraWorld = buildCameraWorldMatrix(eye, forward, up);
122
393
  if (!cameraWorld) {
123
- return mat4.create();
394
+ return mat42.create();
124
395
  }
125
- const view = mat4.create();
126
- mat4.invert(view, cameraWorld);
396
+ const view = mat42.create();
397
+ mat42.invert(view, cameraWorld);
127
398
  return view;
128
399
  }
129
400
  function getAutoDistance(drawCalls, fovDeg) {
130
401
  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])
402
+ const diag = vec32.distance(
403
+ vec32.fromValues(aabb.min[0], aabb.min[1], aabb.min[2]),
404
+ vec32.fromValues(aabb.max[0], aabb.max[1], aabb.max[2])
134
405
  );
135
406
  const radius = diag * 0.5;
136
407
  const fov = toRad(fovDeg);
@@ -149,7 +420,7 @@ function getAABBCornerDistances(aabb, eye) {
149
420
  for (const x of xs) {
150
421
  for (const y of ys) {
151
422
  for (const z of zs) {
152
- const distance = vec3.distance(eye, vec3.fromValues(x, y, z));
423
+ const distance = vec32.distance(eye, vec32.fromValues(x, y, z));
153
424
  farthest = Math.max(farthest, distance);
154
425
  }
155
426
  }
@@ -157,29 +428,29 @@ function getAABBCornerDistances(aabb, eye) {
157
428
  return { nearest, farthest };
158
429
  }
159
430
  function getCameraForwardFromView(view) {
160
- const cameraWorld = mat4.create();
161
- mat4.invert(cameraWorld, view);
162
- return vec3.fromValues(-cameraWorld[8], -cameraWorld[9], -cameraWorld[10]);
431
+ const cameraWorld = mat42.create();
432
+ mat42.invert(cameraWorld, view);
433
+ return vec32.fromValues(-cameraWorld[8], -cameraWorld[9], -cameraWorld[10]);
163
434
  }
164
- function buildCamera(drawCalls, width, height, fovDeg, camPos, lookAt, up = "y+", cameraRotation = null) {
435
+ function buildCamera(drawCalls, width, height, fovDeg, camPos, lookAt, up = "y+", cameraRotation = null, projection = "perspective", orthographicHeight = null) {
165
436
  const aspect = width / height;
166
437
  const worldUp = getWorldUpVector(up);
167
438
  const aabb = computeWorldAABB(drawCalls);
168
- const center = vec3.fromValues(
439
+ const center = vec32.fromValues(
169
440
  0.5 * (aabb.min[0] + aabb.max[0]),
170
441
  0.5 * (aabb.min[1] + aabb.max[1]),
171
442
  0.5 * (aabb.min[2] + aabb.max[2])
172
443
  );
173
444
  const autoDistance = getAutoDistance(drawCalls, fovDeg);
174
445
  if (cameraRotation) {
175
- const provisionalEye = camPos ? vec3.fromValues(camPos[0], camPos[1], camPos[2]) : vec3.clone(center);
446
+ const provisionalEye = camPos ? vec32.fromValues(camPos[0], camPos[1], camPos[2]) : vec32.clone(center);
176
447
  const provisionalView = buildViewFromRotation(
177
448
  provisionalEye,
178
449
  worldUp,
179
450
  cameraRotation
180
451
  );
181
- const eye2 = camPos ? provisionalEye : vec3.scaleAndAdd(
182
- vec3.create(),
452
+ const eye2 = camPos ? provisionalEye : vec32.scaleAndAdd(
453
+ vec32.create(),
183
454
  center,
184
455
  getCameraForwardFromView(provisionalView),
185
456
  -autoDistance
@@ -187,30 +458,78 @@ function buildCamera(drawCalls, width, height, fovDeg, camPos, lookAt, up = "y+"
187
458
  const { nearest: nearest2, farthest: farthest2 } = getAABBCornerDistances(aabb, eye2);
188
459
  const near2 = Math.max(0.01, nearest2 * 0.5);
189
460
  const far2 = Math.max(1e3, farthest2 * 1.1);
190
- const proj2 = mat4.create();
191
- mat4.perspective(proj2, toRad(fovDeg), aspect, near2, far2);
461
+ const proj2 = mat42.create();
462
+ const view2 = buildViewFromRotation(eye2, worldUp, cameraRotation);
463
+ setProjection(
464
+ proj2,
465
+ view2,
466
+ aabb,
467
+ projection,
468
+ orthographicHeight,
469
+ fovDeg,
470
+ aspect,
471
+ near2,
472
+ far2
473
+ );
192
474
  return {
193
- view: buildViewFromRotation(eye2, worldUp, cameraRotation),
475
+ view: view2,
194
476
  proj: proj2
195
477
  };
196
478
  }
197
- const eye = camPos ? vec3.fromValues(camPos[0], camPos[1], camPos[2]) : vec3.fromValues(
479
+ const eye = camPos ? vec32.fromValues(camPos[0], camPos[1], camPos[2]) : vec32.fromValues(
198
480
  center[0] + autoDistance,
199
481
  center[1] + autoDistance * 0.3,
200
482
  center[2] + autoDistance
201
483
  );
202
- const target = lookAt ? vec3.fromValues(lookAt[0], lookAt[1], lookAt[2]) : center;
484
+ const target = lookAt ? vec32.fromValues(lookAt[0], lookAt[1], lookAt[2]) : center;
203
485
  const { nearest, farthest } = getAABBCornerDistances(aabb, eye);
204
486
  const near = Math.max(0.01, nearest * 0.5);
205
487
  const far = Math.max(1e3, farthest * 1.1);
206
- const proj = mat4.create();
207
- mat4.perspective(proj, toRad(fovDeg), aspect, near, far);
488
+ const proj = mat42.create();
208
489
  const view = buildViewFromLookAt(eye, target, worldUp);
490
+ setProjection(
491
+ proj,
492
+ view,
493
+ aabb,
494
+ projection,
495
+ orthographicHeight,
496
+ fovDeg,
497
+ aspect,
498
+ near,
499
+ far
500
+ );
209
501
  return { view, proj };
210
502
  }
503
+ function setProjection(proj, view, aabb, projection, height, fov, aspect, near, far) {
504
+ if (projection === "perspective") {
505
+ mat42.perspective(proj, toRad(fov), aspect, near, far);
506
+ return;
507
+ }
508
+ let halfHeight = 0;
509
+ for (const x of [aabb.min[0], aabb.max[0]])
510
+ for (const y of [aabb.min[1], aabb.max[1]])
511
+ for (const z of [aabb.min[2], aabb.max[2]]) {
512
+ const p = vec32.transformMat4(vec32.create(), [x, y, z], view);
513
+ halfHeight = Math.max(
514
+ halfHeight,
515
+ Math.abs(p[1]),
516
+ Math.abs(p[0]) / aspect
517
+ );
518
+ }
519
+ halfHeight = height != null && Number.isFinite(height) && height > 0 ? height / 2 : Math.max(halfHeight * 1.1, 1e-3);
520
+ mat42.ortho(
521
+ proj,
522
+ -halfHeight * aspect,
523
+ halfHeight * aspect,
524
+ -halfHeight,
525
+ halfHeight,
526
+ near,
527
+ far
528
+ );
529
+ }
211
530
 
212
531
  // lib/gltf/createGrid.ts
213
- import { mat4 as mat42 } from "gl-matrix";
532
+ import { mat4 as mat43 } from "gl-matrix";
214
533
  function createGrid(options = {}) {
215
534
  const size = options.size ?? 10;
216
535
  const sizeX = typeof size === "number" ? size : size[0];
@@ -235,8 +554,8 @@ function createGrid(options = {}) {
235
554
  positions.push(halfSizeX, 0, pZ);
236
555
  indices.push(vertexIndex++, vertexIndex++);
237
556
  }
238
- const model = mat42.create();
239
- mat42.fromTranslation(model, [offset.x, offset.y, offset.z]);
557
+ const model = mat43.create();
558
+ mat43.fromTranslation(model, [offset.x, offset.y, offset.z]);
240
559
  return {
241
560
  positions: new Float32Array(positions),
242
561
  indices: new Uint32Array(indices),
@@ -253,6 +572,7 @@ function createGrid(options = {}) {
253
572
  }
254
573
 
255
574
  // lib/gltf/createEdgeDrawCall.ts
575
+ import { mat4 as mat44, vec3 as vec33 } from "gl-matrix";
256
576
  var DEFAULT_CREASE_ANGLE_DEGREES = 30;
257
577
  function positionKey(positions, index) {
258
578
  return `${positions[index * 3 + 0]},${positions[index * 3 + 1]},${positions[index * 3 + 2]}`;
@@ -271,7 +591,7 @@ function triangleNormal(positions, i0, i1, i2) {
271
591
  if (length === 0) return null;
272
592
  return [nx / length, ny / length, nz / length];
273
593
  }
274
- function addEdge(edges, positions, i0, i1, thirdVertex, normal) {
594
+ function addEdge(edges, positions, i0, i1, thirdVertex, normal, facing) {
275
595
  const key0 = positionKey(positions, i0);
276
596
  const key1 = positionKey(positions, i1);
277
597
  const isForward = key0 < key1;
@@ -281,11 +601,13 @@ function addEdge(edges, positions, i0, i1, thirdVertex, normal) {
281
601
  if (existing) {
282
602
  if (normal) existing.normals.push(normal);
283
603
  existing.adjacentVertices.push(thirdVertex);
604
+ existing.facings.push(facing);
284
605
  } else {
285
606
  edges.set(key, {
286
607
  indices,
287
608
  normals: normal ? [normal] : [],
288
- adjacentVertices: [thirdVertex]
609
+ adjacentVertices: [thirdVertex],
610
+ facings: [facing]
289
611
  });
290
612
  }
291
613
  }
@@ -301,26 +623,41 @@ function hasCrease(normals, minimumDot) {
301
623
  }
302
624
  return false;
303
625
  }
304
- function createEdgeDrawCall(mesh, creaseAngleDegrees = DEFAULT_CREASE_ANGLE_DEGREES) {
626
+ function createEdgeDrawCall(mesh, creaseAngleDegrees = DEFAULT_CREASE_ANGLE_DEGREES, camera) {
305
627
  if (mesh.mode != null && mesh.mode !== 4) return null;
306
628
  const vertexCount = Math.floor(mesh.positions.length / 3);
307
629
  const triangleIndices = mesh.indices ?? Uint32Array.from({ length: vertexCount }, (_, i) => i);
308
630
  if (triangleIndices.length < 3) return null;
631
+ let normalPositions = mesh.positions;
632
+ if (camera) {
633
+ const modelView = mat44.multiply(mat44.create(), camera.view, mesh.model);
634
+ normalPositions = new Float32Array(mesh.positions.length);
635
+ for (let i = 0; i < vertexCount; i++) {
636
+ const point = vec33.transformMat4(
637
+ vec33.create(),
638
+ mesh.positions.subarray(i * 3, i * 3 + 3),
639
+ modelView
640
+ );
641
+ normalPositions.set(point, i * 3);
642
+ }
643
+ }
309
644
  const edges = /* @__PURE__ */ new Map();
310
645
  for (let i = 0; i + 2 < triangleIndices.length; i += 3) {
311
646
  const i0 = triangleIndices[i];
312
647
  const i1 = triangleIndices[i + 1];
313
648
  const i2 = triangleIndices[i + 2];
314
- const normal = triangleNormal(mesh.positions, i0, i1, i2);
315
- addEdge(edges, mesh.positions, i0, i1, i2, normal);
316
- addEdge(edges, mesh.positions, i1, i2, i0, normal);
317
- addEdge(edges, mesh.positions, i2, i0, i1, normal);
649
+ const normal = triangleNormal(normalPositions, i0, i1, i2);
650
+ const facing = !normal ? 0 : camera?.projection === "perspective" ? -(normal[0] * normalPositions[i0 * 3] + normal[1] * normalPositions[i0 * 3 + 1] + normal[2] * normalPositions[i0 * 3 + 2]) : normal[2];
651
+ addEdge(edges, mesh.positions, i0, i1, i2, normal, facing);
652
+ addEdge(edges, mesh.positions, i1, i2, i0, normal, facing);
653
+ addEdge(edges, mesh.positions, i2, i0, i1, normal, facing);
318
654
  }
319
655
  const minimumDot = Math.cos(creaseAngleDegrees * Math.PI / 180);
320
656
  const edgeIndices = [];
321
657
  const edgeAdjacentVertices = [];
322
658
  for (const edge2 of edges.values()) {
323
- if (!hasCrease(edge2.normals, minimumDot)) continue;
659
+ const silhouette = camera && edge2.facings.some((n) => n > 1e-7) && edge2.facings.some((n) => n <= 1e-7);
660
+ if (!hasCrease(edge2.normals, minimumDot) && !silhouette) continue;
324
661
  edgeIndices.push(edge2.indices[0], edge2.indices[1]);
325
662
  edgeAdjacentVertices.push(edge2.adjacentVertices);
326
663
  }
@@ -341,13 +678,6 @@ function createEdgeDrawCall(mesh, creaseAngleDegrees = DEFAULT_CREASE_ANGLE_DEGR
341
678
  };
342
679
  }
343
680
 
344
- // lib/image/createUint8Bitmap.ts
345
- var createUint8Bitmap = (width, height) => ({
346
- width,
347
- height,
348
- data: new Uint8ClampedArray(width * height * 4)
349
- });
350
-
351
681
  // lib/render/downsampleBitmap.ts
352
682
  function downsampleBitmap(source, targetWidth, targetHeight, imageFactory) {
353
683
  const target = imageFactory(targetWidth, targetHeight);
@@ -396,10 +726,10 @@ import {
396
726
  glyphLineAlphabet,
397
727
  textMetrics
398
728
  } from "@tscircuit/alphabet";
399
- import { mat4 as mat44, vec4 as vec43 } from "gl-matrix";
729
+ import { mat4 as mat46, vec4 as vec43 } from "gl-matrix";
400
730
 
401
731
  // lib/render/SoftwareRenderer.ts
402
- import { mat3, mat4 as mat43, vec3 as vec32, vec4 as vec42 } from "gl-matrix";
732
+ import { mat3, mat4 as mat45, vec3 as vec34, vec4 as vec42 } from "gl-matrix";
403
733
 
404
734
  // lib/gltf/computeSmoothNormals.ts
405
735
  function computeSmoothNormals(positions, indices) {
@@ -480,6 +810,10 @@ function hexToRgb(hex) {
480
810
  return [r, g, b];
481
811
  }
482
812
  var DEFAULT_RENDER_OPTIONS = {
813
+ renderStyle: "shaded",
814
+ section: null,
815
+ projection: "perspective",
816
+ orthographicHeight: null,
483
817
  realistic: false,
484
818
  width: 800,
485
819
  height: 600,
@@ -831,8 +1165,8 @@ var SoftwareRenderer = class {
831
1165
  if (!indices) return;
832
1166
  const view = camera.view;
833
1167
  const proj = camera.proj;
834
- const mvp = mat43.create();
835
- mat43.multiply(mvp, proj, mat43.multiply(mat43.create(), view, model));
1168
+ const mvp = mat45.create();
1169
+ mat45.multiply(mvp, proj, mat45.multiply(mat45.create(), view, model));
836
1170
  const vertexCount = positions.length / 3 | 0;
837
1171
  const vScreen = new Array(vertexCount);
838
1172
  const vInvW = new Float32Array(vertexCount);
@@ -1022,14 +1356,14 @@ var SoftwareRenderer = class {
1022
1356
  const materialId = this.denoiser?.materialId(material) ?? 0;
1023
1357
  const lightDir = light.dir ?? DEFAULT_LIGHT_DIR;
1024
1358
  const ambient = clamp(light.ambient ?? DEFAULT_RENDER_OPTIONS.ambient, 0, 1);
1025
- const L = studio ? null : vec32.normalize(
1026
- vec32.create(),
1027
- vec32.fromValues(lightDir[0], lightDir[1], lightDir[2])
1359
+ const L = studio ? null : vec34.normalize(
1360
+ vec34.create(),
1361
+ vec34.fromValues(lightDir[0], lightDir[1], lightDir[2])
1028
1362
  );
1029
1363
  const view = camera.view;
1030
1364
  const proj = camera.proj;
1031
- const mvp = mat43.create();
1032
- mat43.multiply(mvp, proj, mat43.multiply(mat43.create(), view, model));
1365
+ const mvp = mat45.create();
1366
+ mat45.multiply(mvp, proj, mat45.multiply(mat45.create(), view, model));
1033
1367
  const normalMat = mat3.create();
1034
1368
  mat3.normalFromMat4(normalMat, model);
1035
1369
  let vertexCount = positions.length / 3 | 0;
@@ -1086,13 +1420,13 @@ var SoftwareRenderer = class {
1086
1420
  vInvW[i] = invW;
1087
1421
  vNDCz[i] = ndcZ;
1088
1422
  if (depthOnly) continue;
1089
- const n = vec32.fromValues(
1423
+ const n = vec34.fromValues(
1090
1424
  useNormals[i * 3 + 0],
1091
1425
  useNormals[i * 3 + 1],
1092
1426
  useNormals[i * 3 + 2]
1093
1427
  );
1094
- const nw = vec32.create();
1095
- vec32.transformMat3(nw, n, normalMat);
1428
+ const nw = vec34.create();
1429
+ vec34.transformMat3(nw, n, normalMat);
1096
1430
  vWorldN[i] = [nw[0], nw[1], nw[2]];
1097
1431
  if (colors && colors.length >= (i + 1) * 3) {
1098
1432
  vColor[i] = [colors[i * 3 + 0], colors[i * 3 + 1], colors[i * 3 + 2]];
@@ -1182,8 +1516,8 @@ var SoftwareRenderer = class {
1182
1516
  const uvp = this.perspInterp(uv, invW, [l0, l1, l2]);
1183
1517
  const texel = this.sampleTextureNearest(
1184
1518
  material.baseColorTexture,
1185
- uvp[0],
1186
- uvp[1]
1519
+ material.baseColorTextureWrap === "repeat" ? uvp[0] - Math.floor(uvp[0]) : uvp[0],
1520
+ material.baseColorTextureWrap === "repeat" ? uvp[1] - Math.floor(uvp[1]) : uvp[1]
1187
1521
  );
1188
1522
  baseColor = mulColor(baseColor, [
1189
1523
  srgbDecodeToLinear01(texel[0]),
@@ -1333,8 +1667,8 @@ var DEFAULT_TEXT_COLOR = [
1333
1667
  var CHARACTER_WIDTH_RATIO = 0.62;
1334
1668
  function drawDebugPoints(renderer, camera, debugPoints, debugFontSize, debugPointColor, debugLabelColor) {
1335
1669
  if (debugPoints.length === 0) return;
1336
- const viewProj = mat44.create();
1337
- mat44.multiply(viewProj, camera.proj, camera.view);
1670
+ const viewProj = mat46.create();
1671
+ mat46.multiply(viewProj, camera.proj, camera.view);
1338
1672
  const markerColor = toColorRGBA(debugPointColor, DEFAULT_MARKER_COLOR);
1339
1673
  const textColor = toColorRGBA(debugLabelColor, DEFAULT_TEXT_COLOR);
1340
1674
  const labelScale = typeof debugFontSize === "number" && Number.isFinite(debugFontSize) ? Math.max(1, Math.round(debugFontSize)) : Math.max(
@@ -1487,7 +1821,7 @@ function toColorRGBA(color, fallback) {
1487
1821
  }
1488
1822
 
1489
1823
  // lib/render/drawInfiniteGrid.ts
1490
- import { mat4 as mat45, vec4 as vec44 } from "gl-matrix";
1824
+ import { mat4 as mat47, vec4 as vec44 } from "gl-matrix";
1491
1825
  function computeGridLineIntensity(params) {
1492
1826
  const rx = params.world_x / params.cell_size;
1493
1827
  const rz = params.world_z / params.cell_size;
@@ -1523,15 +1857,15 @@ function drawInfiniteGrid(software_renderer, params) {
1523
1857
  const camera = params.camera;
1524
1858
  const view = camera.view;
1525
1859
  const proj = camera.proj;
1526
- const inv_view = mat45.create();
1527
- mat45.invert(inv_view, view);
1860
+ const inv_view = mat47.create();
1861
+ mat47.invert(inv_view, view);
1528
1862
  const cam_pos_x = inv_view[12];
1529
1863
  const cam_pos_y = inv_view[13];
1530
1864
  const cam_pos_z = inv_view[14];
1531
- const vp = mat45.create();
1532
- mat45.multiply(vp, proj, view);
1533
- const inv_vp = mat45.create();
1534
- mat45.invert(inv_vp, vp);
1865
+ const vp = mat47.create();
1866
+ mat47.multiply(vp, proj, view);
1867
+ const inv_vp = mat47.create();
1868
+ mat47.invert(inv_vp, vp);
1535
1869
  for (let y = 0; y < software_renderer.height; y++) {
1536
1870
  for (let x = 0; x < software_renderer.width; x++) {
1537
1871
  const ndc_x = x / (software_renderer.width - 1) * 2 - 1;
@@ -1649,17 +1983,28 @@ function resolveCameraRotation(cameraRotation) {
1649
1983
  }
1650
1984
  function resolveRenderOptions(options = {}) {
1651
1985
  const supersampling = typeof options.supersampling === "number" && Number.isFinite(options.supersampling) ? Math.max(1, Math.floor(options.supersampling)) : DEFAULT_RENDER_OPTIONS.supersampling;
1986
+ const outline = options.renderStyle === "outline";
1652
1987
  return {
1653
1988
  ...DEFAULT_RENDER_OPTIONS,
1654
1989
  ...options,
1990
+ renderStyle: outline ? "outline" : "shaded",
1991
+ projection: options.projection === "orthographic" || outline && options.projection !== "perspective" ? "orthographic" : "perspective",
1992
+ orthographicHeight: typeof options.orthographicHeight === "number" && Number.isFinite(options.orthographicHeight) && options.orthographicHeight > 0 ? options.orthographicHeight : null,
1993
+ ...outline ? {
1994
+ realistic: false,
1995
+ ambient: 1,
1996
+ gamma: false,
1997
+ cull: false,
1998
+ backgroundColor: options.backgroundColor ?? "#ffffff"
1999
+ } : {},
1655
2000
  supersampling,
1656
2001
  lightDir: options.lightDir != null ? options.lightDir : DEFAULT_RENDER_OPTIONS.lightDir,
1657
2002
  camPos: options.camPos !== void 0 ? options.camPos : DEFAULT_RENDER_OPTIONS.camPos,
1658
2003
  lookAt: options.lookAt !== void 0 ? options.lookAt : DEFAULT_RENDER_OPTIONS.lookAt,
1659
2004
  up: resolveCameraUp(options.up),
1660
2005
  cameraRotation: resolveCameraRotation(options.cameraRotation),
1661
- showEdges: options.showEdges === true,
1662
- showHiddenEdges: typeof options.showHiddenEdges === "boolean" ? options.showHiddenEdges : null,
2006
+ showEdges: options.showEdges ?? outline,
2007
+ showHiddenEdges: typeof options.showHiddenEdges === "boolean" ? options.showHiddenEdges : outline ? false : null,
1663
2008
  edgeWidth: resolveWidth(options.edgeWidth),
1664
2009
  lineWidth: resolveWidth(options.lineWidth),
1665
2010
  hiddenEdgeOpacity: typeof options.hiddenEdgeOpacity === "number" && Number.isFinite(options.hiddenEdgeOpacity) ? Math.max(0, Math.min(1, options.hiddenEdgeOpacity)) : DEFAULT_RENDER_OPTIONS.hiddenEdgeOpacity
@@ -1670,7 +2015,7 @@ function resolveWidth(width) {
1670
2015
  }
1671
2016
 
1672
2017
  // lib/render/StudioLighting.ts
1673
- import { mat4 as mat46 } from "gl-matrix";
2018
+ import { mat4 as mat48 } from "gl-matrix";
1674
2019
 
1675
2020
  // lib/render/studio-environment.json
1676
2021
  var studio_environment_default = {
@@ -1683,7 +2028,7 @@ var studio_environment_default = {
1683
2028
  };
1684
2029
 
1685
2030
  // lib/render/RayOcclusion.ts
1686
- import { vec3 as vec33 } from "gl-matrix";
2031
+ import { vec3 as vec35 } from "gl-matrix";
1687
2032
  var RayOcclusion = class {
1688
2033
  bounds = new Float64Array(0);
1689
2034
  escapes = new Uint32Array(0);
@@ -1706,8 +2051,8 @@ var RayOcclusion = class {
1706
2051
  const count = dc.indices?.length ?? dc.positions.length / 3;
1707
2052
  const worldVertices = [];
1708
2053
  for (let i = 0; i < dc.positions.length; i += 3) {
1709
- const p = vec33.transformMat4(
1710
- vec33.create(),
2054
+ const p = vec35.transformMat4(
2055
+ vec35.create(),
1711
2056
  [dc.positions[i], dc.positions[i + 1], dc.positions[i + 2]],
1712
2057
  dc.model
1713
2058
  );
@@ -2167,7 +2512,7 @@ var StudioLighting = class {
2167
2512
  }
2168
2513
  constructor(drawCalls, camera) {
2169
2514
  this.occlusion = new RayOcclusion(drawCalls);
2170
- const world = mat46.invert(mat46.create(), camera.view);
2515
+ const world = mat48.invert(mat48.create(), camera.view);
2171
2516
  if (!world)
2172
2517
  throw new Error("Realistic rendering requires an invertible camera view.");
2173
2518
  this.eye = [world[12], world[13], world[14]];
@@ -2371,7 +2716,7 @@ var StudioLighting = class {
2371
2716
  };
2372
2717
 
2373
2718
  // lib/render/createExplosionLineDrawCall.ts
2374
- import { mat4 as mat47, vec4 as vec45 } from "gl-matrix";
2719
+ import { mat4 as mat49, vec4 as vec45 } from "gl-matrix";
2375
2720
  function createExplosionLineDrawCall(line) {
2376
2721
  if (line.points.length < 2 || line.points.some(
2377
2722
  (point) => point.length !== 3 || !point.every(Number.isFinite)
@@ -2395,13 +2740,13 @@ function createExplosionLineDrawCall(line) {
2395
2740
  indices: new Uint32Array(indices),
2396
2741
  normals: null,
2397
2742
  uvs: null,
2398
- model: mat47.create(),
2743
+ model: mat49.create(),
2399
2744
  material: { baseColorFactor: [...color, 1], baseColorTexture: null },
2400
2745
  mode: 1
2401
2746
  };
2402
2747
  }
2403
2748
  function clipExplosionLineDrawCall(drawCall, camera) {
2404
- const viewProjection = mat47.multiply(mat47.create(), camera.proj, camera.view);
2749
+ const viewProjection = mat49.multiply(mat49.create(), camera.proj, camera.view);
2405
2750
  const positions = [];
2406
2751
  const indices = [];
2407
2752
  const distances = (point) => [
@@ -2465,7 +2810,7 @@ function clipExplosionLineDrawCall(drawCall, camera) {
2465
2810
  }
2466
2811
 
2467
2812
  // lib/render/createExplosionArrowDrawCall.ts
2468
- import { mat4 as mat48, vec4 as vec46 } from "gl-matrix";
2813
+ import { mat4 as mat410, vec4 as vec46 } from "gl-matrix";
2469
2814
  function createExplosionArrowDrawCall(drawCall, camera, width, height, supersampling) {
2470
2815
  if (width <= 1 || height <= 1) return null;
2471
2816
  const points = drawCall.positions;
@@ -2475,7 +2820,7 @@ function createExplosionArrowDrawCall(drawCall, camera, width, height, supersamp
2475
2820
  start -= 3;
2476
2821
  }
2477
2822
  if (start < 0) return null;
2478
- const viewProjection = mat48.multiply(mat48.create(), camera.proj, camera.view);
2823
+ const viewProjection = mat410.multiply(mat410.create(), camera.proj, camera.view);
2479
2824
  const tip = vec46.transformMat4(
2480
2825
  vec46.create(),
2481
2826
  vec46.fromValues(points[end], points[end + 1], points[end + 2], 1),
@@ -2518,7 +2863,7 @@ function createExplosionArrowDrawCall(drawCall, camera, width, height, supersamp
2518
2863
  const arrowLength = Math.min(10 * supersampling, length / 2);
2519
2864
  const ux = dx / length;
2520
2865
  const uy = dy / length;
2521
- const inverse = mat48.invert(mat48.create(), viewProjection);
2866
+ const inverse = mat410.invert(mat410.create(), viewProjection);
2522
2867
  if (!inverse) return null;
2523
2868
  const positions = [points[end], points[end + 1], points[end + 2]];
2524
2869
  for (const side of [-1, 1]) {
@@ -2547,6 +2892,11 @@ function createExplosionArrowDrawCall(drawCall, camera, width, height, supersamp
2547
2892
  // lib/render/renderDrawCalls.ts
2548
2893
  function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint8Bitmap) {
2549
2894
  const options = resolveRenderOptions(optionsInput);
2895
+ if (options.section) {
2896
+ if (options.renderStyle !== "outline")
2897
+ throw new Error("section requires renderStyle: outline");
2898
+ drawCalls = createOutlineSection(drawCalls, options.section).drawCalls;
2899
+ }
2550
2900
  const renderWidth = options.width * options.supersampling;
2551
2901
  const renderHeight = options.height * options.supersampling;
2552
2902
  const explosionDrawCalls = (options.explosionLines ?? []).map(
@@ -2560,7 +2910,9 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
2560
2910
  options.camPos ?? null,
2561
2911
  options.lookAt ?? null,
2562
2912
  options.up,
2563
- options.cameraRotation ?? null
2913
+ options.cameraRotation ?? null,
2914
+ options.projection,
2915
+ options.orthographicHeight
2564
2916
  );
2565
2917
  const renderer = new SoftwareRenderer(
2566
2918
  renderWidth,
@@ -2587,7 +2939,19 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
2587
2939
  } else {
2588
2940
  renderer.clear([0, 0, 0, 0]);
2589
2941
  }
2590
- const allDrawCalls = [...drawCalls];
2942
+ const allDrawCalls = options.renderStyle === "outline" ? drawCalls.map(
2943
+ (dc) => dc.mode === 1 || dc.outlineSectionCap ? dc : {
2944
+ ...dc,
2945
+ normals: null,
2946
+ colors: null,
2947
+ uvs: null,
2948
+ material: {
2949
+ baseColorFactor: [1, 1, 1, 1],
2950
+ baseColorTexture: null,
2951
+ alphaMode: "OPAQUE"
2952
+ }
2953
+ }
2954
+ ) : [...drawCalls];
2591
2955
  let infiniteGridOptions = null;
2592
2956
  if (options.grid) {
2593
2957
  const userGridOptions = typeof options.grid === "boolean" ? {} : options.grid;
@@ -2693,7 +3057,11 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
2693
3057
  for (const dc of drawCalls) {
2694
3058
  const showHiddenEdges = options.showHiddenEdges ?? dc.showHiddenEdges;
2695
3059
  if (!options.showEdges && !showHiddenEdges) continue;
2696
- const edgeDrawCall = createEdgeDrawCall(dc);
3060
+ const edgeDrawCall = createEdgeDrawCall(
3061
+ dc,
3062
+ 30,
3063
+ options.renderStyle === "outline" ? { view: camera.view, projection: options.projection } : void 0
3064
+ );
2697
3065
  if (!edgeDrawCall) continue;
2698
3066
  renderer.drawLines(edgeDrawCall, camera, options.gamma, {
2699
3067
  depthCompare: "less-equal",
@@ -2769,7 +3137,7 @@ function renderDrawCalls(drawCalls, optionsInput = {}, imageFactory = createUint
2769
3137
  }
2770
3138
 
2771
3139
  // lib/gltf/createInfiniteGrid.ts
2772
- import { mat4 as mat49 } from "gl-matrix";
3140
+ import { mat4 as mat411 } from "gl-matrix";
2773
3141
  function createInfiniteGrid(options = {}) {
2774
3142
  const cellSize = options.cellSize ?? 1;
2775
3143
  const sectionSize = options.sectionSize ?? 10;
@@ -2793,9 +3161,9 @@ function createInfiniteGrid(options = {}) {
2793
3161
  size
2794
3162
  ]);
2795
3163
  const indices = new Uint32Array([0, 1, 2, 0, 2, 3]);
2796
- const model = mat49.create();
3164
+ const model = mat411.create();
2797
3165
  const offset = options.offset ?? {};
2798
- mat49.fromTranslation(model, [offset.x ?? 0, offset.y ?? 0, offset.z ?? 0]);
3166
+ mat411.fromTranslation(model, [offset.x ?? 0, offset.y ?? 0, offset.z ?? 0]);
2799
3167
  return {
2800
3168
  positions,
2801
3169
  indices,
@@ -2820,7 +3188,7 @@ function createInfiniteGrid(options = {}) {
2820
3188
  }
2821
3189
 
2822
3190
  // lib/gltf/createSceneFromGLTF.ts
2823
- import { mat4 as mat410 } from "gl-matrix";
3191
+ import { mat4 as mat412 } from "gl-matrix";
2824
3192
  var COMPONENT_INFO = {
2825
3193
  5120: {
2826
3194
  name: "BYTE",
@@ -3027,17 +3395,17 @@ function readIndices(ctx, accessorIndex) {
3027
3395
  }
3028
3396
  function nodeLocalMatrix(node) {
3029
3397
  if (node.matrix) {
3030
- const m2 = mat410.create();
3398
+ const m2 = mat412.create();
3031
3399
  for (let i = 0; i < 16; i++) m2[i] = node.matrix[i];
3032
3400
  return m2;
3033
3401
  }
3034
3402
  const translation = node.translation || [0, 0, 0];
3035
3403
  const rotation = node.rotation || [0, 0, 0, 1];
3036
3404
  const scale = node.scale || [1, 1, 1];
3037
- const m = mat410.create();
3038
- const tmp = mat410.create();
3039
- mat410.fromRotationTranslationScale(tmp, rotation, translation, scale);
3040
- mat410.copy(m, tmp);
3405
+ const m = mat412.create();
3406
+ const tmp = mat412.create();
3407
+ mat412.fromRotationTranslationScale(tmp, rotation, translation, scale);
3408
+ mat412.copy(m, tmp);
3041
3409
  return m;
3042
3410
  }
3043
3411
  function getMaterial(gltf, textures, images, materialIndex) {
@@ -3080,8 +3448,8 @@ function createSceneFromGLTF(gltf, resources) {
3080
3448
  const node = nodes[nodeIndex];
3081
3449
  if (!node) return;
3082
3450
  const local = nodeLocalMatrix(node);
3083
- const world = mat410.create();
3084
- mat410.multiply(world, parentMatrix, local);
3451
+ const world = mat412.create();
3452
+ mat412.multiply(world, parentMatrix, local);
3085
3453
  if (Number.isInteger(node.mesh)) {
3086
3454
  const mesh = meshes[node.mesh];
3087
3455
  if (mesh) {
@@ -3120,11 +3488,15 @@ function createSceneFromGLTF(gltf, resources) {
3120
3488
  traverse(childIndex, world);
3121
3489
  }
3122
3490
  }
3123
- const identity = mat410.create();
3491
+ const identity = mat412.create();
3124
3492
  for (const rootIndex of scene.nodes || []) {
3125
3493
  traverse(rootIndex, identity);
3126
3494
  }
3127
- return { gltf, drawCalls };
3495
+ return {
3496
+ gltf,
3497
+ drawCalls,
3498
+ explosionLines: scene.extras?.poppygl?.explosionLines
3499
+ };
3128
3500
  }
3129
3501
 
3130
3502
  // lib/gltf/resourceUtils.ts
@@ -3364,6 +3736,18 @@ async function encodePNG(image) {
3364
3736
  });
3365
3737
  }
3366
3738
 
3739
+ // lib/render/renderSceneFromGLTF.ts
3740
+ function renderSceneFromGLTF(scene, options = {}, imageFactory) {
3741
+ return renderDrawCalls(
3742
+ scene.drawCalls,
3743
+ {
3744
+ ...options,
3745
+ explosionLines: options.explosionLines === void 0 ? scene.explosionLines : options.explosionLines
3746
+ },
3747
+ imageFactory
3748
+ );
3749
+ }
3750
+
3367
3751
  // lib/render/renderGLTFToPNGFromURL.ts
3368
3752
  async function renderGLTFToPNGFromURL(url, options = {}) {
3369
3753
  const { fetchImpl, ...renderOptions } = options;
@@ -3371,8 +3755,8 @@ async function renderGLTFToPNGFromURL(url, options = {}) {
3371
3755
  fetchImpl
3372
3756
  });
3373
3757
  const scene = createSceneFromGLTF(gltf, resources);
3374
- const { bitmap } = renderDrawCalls(
3375
- scene.drawCalls,
3758
+ const { bitmap } = renderSceneFromGLTF(
3759
+ scene,
3376
3760
  renderOptions,
3377
3761
  createUint8Bitmap
3378
3762
  );
@@ -3449,18 +3833,9 @@ async function renderGLTFToPNGFromGLB(glb, options = {}) {
3449
3833
  })
3450
3834
  );
3451
3835
  const scene = createSceneFromGLTF(gltf, { buffers, images });
3452
- const { bitmap } = renderDrawCalls(
3453
- scene.drawCalls,
3454
- options,
3455
- createUint8Bitmap
3456
- );
3836
+ const { bitmap } = renderSceneFromGLTF(scene, options, createUint8Bitmap);
3457
3837
  return encodePNG(bitmap);
3458
3838
  }
3459
-
3460
- // lib/index.ts
3461
- function renderSceneFromGLTF(scene, options, imageFactory) {
3462
- return renderDrawCalls(scene.drawCalls, options, imageFactory);
3463
- }
3464
3839
  export {
3465
3840
  DEFAULT_LIGHT_DIR,
3466
3841
  DEFAULT_RENDER_OPTIONS,
@@ -3472,6 +3847,7 @@ export {
3472
3847
  createEdgeDrawCall,
3473
3848
  createGrid,
3474
3849
  createInfiniteGrid,
3850
+ createOutlineSection,
3475
3851
  createSceneFromGLTF,
3476
3852
  createUint8Bitmap,
3477
3853
  decodeImageFromBuffer,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "poppygl",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.36",
4
+ "version": "0.0.38",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tscircuit/poppygl.git"
@@ -42,6 +42,7 @@
42
42
  "fast-png": "^8.0.0",
43
43
  "gl-matrix": "^3.4.4",
44
44
  "pureimage": "^0.4.18",
45
- "readable-stream": "^4.7.0"
45
+ "readable-stream": "^4.7.0",
46
+ "earcut": "^3.2.4"
46
47
  }
47
48
  }