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 +76 -0
- package/dist/index.d.ts +99 -60
- package/dist/index.js +507 -131
- package/package.json +3 -2
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
|
|
4
|
-
|
|
5
|
-
interface
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
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
|
|
313
|
+
return vec32.fromValues(0, -1, 0);
|
|
43
314
|
case "x+":
|
|
44
|
-
return
|
|
315
|
+
return vec32.fromValues(1, 0, 0);
|
|
45
316
|
case "x-":
|
|
46
|
-
return
|
|
317
|
+
return vec32.fromValues(-1, 0, 0);
|
|
47
318
|
case "z+":
|
|
48
|
-
return
|
|
319
|
+
return vec32.fromValues(0, 0, 1);
|
|
49
320
|
case "z-":
|
|
50
|
-
return
|
|
321
|
+
return vec32.fromValues(0, 0, -1);
|
|
51
322
|
case "y+":
|
|
52
323
|
default:
|
|
53
|
-
return
|
|
324
|
+
return vec32.fromValues(0, 1, 0);
|
|
54
325
|
}
|
|
55
326
|
}
|
|
56
327
|
function buildCameraWorldMatrix(eye, forward, up) {
|
|
57
|
-
const normalizedForward =
|
|
58
|
-
const right =
|
|
59
|
-
if (
|
|
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
|
-
|
|
63
|
-
const correctedUp =
|
|
64
|
-
if (
|
|
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
|
-
|
|
68
|
-
return
|
|
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 =
|
|
89
|
-
if (Math.abs(
|
|
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
|
|
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
|
|
372
|
+
return mat42.create();
|
|
102
373
|
}
|
|
103
374
|
if (rotation.x !== 0) {
|
|
104
|
-
|
|
375
|
+
mat42.rotateX(cameraWorld, cameraWorld, toRad(rotation.x));
|
|
105
376
|
}
|
|
106
377
|
if (rotation.y !== 0) {
|
|
107
|
-
|
|
378
|
+
mat42.rotateY(cameraWorld, cameraWorld, toRad(rotation.y));
|
|
108
379
|
}
|
|
109
380
|
if (rotation.z !== 0) {
|
|
110
|
-
|
|
381
|
+
mat42.rotateZ(cameraWorld, cameraWorld, toRad(rotation.z));
|
|
111
382
|
}
|
|
112
|
-
const view =
|
|
113
|
-
|
|
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 =
|
|
118
|
-
if (
|
|
119
|
-
return
|
|
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
|
|
394
|
+
return mat42.create();
|
|
124
395
|
}
|
|
125
|
-
const view =
|
|
126
|
-
|
|
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 =
|
|
132
|
-
|
|
133
|
-
|
|
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 =
|
|
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 =
|
|
161
|
-
|
|
162
|
-
return
|
|
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 =
|
|
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 ?
|
|
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 :
|
|
182
|
-
|
|
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 =
|
|
191
|
-
|
|
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:
|
|
475
|
+
view: view2,
|
|
194
476
|
proj: proj2
|
|
195
477
|
};
|
|
196
478
|
}
|
|
197
|
-
const eye = camPos ?
|
|
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 ?
|
|
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 =
|
|
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
|
|
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 =
|
|
239
|
-
|
|
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(
|
|
315
|
-
|
|
316
|
-
addEdge(edges, mesh.positions, i1, i2,
|
|
317
|
-
addEdge(edges, mesh.positions, i2, i0,
|
|
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
|
-
|
|
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
|
|
729
|
+
import { mat4 as mat46, vec4 as vec43 } from "gl-matrix";
|
|
400
730
|
|
|
401
731
|
// lib/render/SoftwareRenderer.ts
|
|
402
|
-
import { mat3, mat4 as
|
|
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 =
|
|
835
|
-
|
|
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 :
|
|
1026
|
-
|
|
1027
|
-
|
|
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 =
|
|
1032
|
-
|
|
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 =
|
|
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 =
|
|
1095
|
-
|
|
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 =
|
|
1337
|
-
|
|
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
|
|
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 =
|
|
1527
|
-
|
|
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 =
|
|
1532
|
-
|
|
1533
|
-
const inv_vp =
|
|
1534
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
1710
|
-
|
|
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 =
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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 =
|
|
3164
|
+
const model = mat411.create();
|
|
2797
3165
|
const offset = options.offset ?? {};
|
|
2798
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
3038
|
-
const tmp =
|
|
3039
|
-
|
|
3040
|
-
|
|
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 =
|
|
3084
|
-
|
|
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 =
|
|
3491
|
+
const identity = mat412.create();
|
|
3124
3492
|
for (const rootIndex of scene.nodes || []) {
|
|
3125
3493
|
traverse(rootIndex, identity);
|
|
3126
3494
|
}
|
|
3127
|
-
return {
|
|
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 } =
|
|
3375
|
-
scene
|
|
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 } =
|
|
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.
|
|
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
|
}
|