@forgeax/engine-geometry 0.1.30 → 0.1.32

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
@@ -7,10 +7,11 @@ forgeax-engine. A **leaf** package: depends on `@forgeax/engine-ecs` (`Result`),
7
7
 
8
8
  ## 30-second self-introduction
9
9
 
10
- - **Surface**: 7 Three.js-r184-aligned 3D procedural factories
10
+ - **Surface**: 10 reusable 3D procedural factories
11
11
  (`createBoxGeometry` / `createCapsuleGeometry` / `createConeGeometry` /
12
12
  `createCylinderGeometry` / `createPlaneGeometry` / `createSphereGeometry` /
13
- `createTorusGeometry`),
13
+ `createTorusGeometry` / `createExtrusionGeometry` / `createSweepGeometry` /
14
+ `createRevolutionGeometry`),
14
15
  each returning `Result<MeshAsset, AssetError>`; the vertex-attribute-layout
15
16
  SSOT (`deriveVertexBufferLayout` / `buildMeshAttributeMapForUvSets` /
16
17
  `GpuVertexBufferLayoutEntry`); and the tangent + interleave helpers
@@ -125,7 +126,13 @@ const sphere = createSphereGeometry(1, 32, 24);
125
126
  // then hand the resulting Handle<MeshAsset> to MeshFilter.assetHandle.
126
127
  ```
127
128
 
128
- The 7 factories cover the most common procedural primitives. For an imported
129
+ The primitive factories cover the common built-in shapes. For reusable authored
130
+ geometry, `createExtrusionGeometry` ear-clips a simple concave 2D contour and
131
+ adds cap and side normals, `createSweepGeometry` follows a 3D path with a
132
+ circular profile, and `createRevolutionGeometry` turns a `(radius, height)`
133
+ profile around the Y axis. Each operation validates finite input before
134
+ allocating a MeshAsset and returns through the same tangent, layout, and AABB
135
+ owners. For an imported
129
136
  glTF mesh, use `@forgeax/engine-gltf`; FBX follows the same source-plus-Meta
130
137
  route through `@forgeax/engine-fbx`. Runtime consumption is
131
138
  `@forgeax/engine-assets-runtime` after the owning importer has cooked the asset.
@@ -154,7 +161,7 @@ if (!filled.ok || !ring.ok) return;
154
161
 
155
162
  ## API surface
156
163
 
157
- ### 7 procedural geometry factories
164
+ ### 10 procedural geometry factories
158
165
 
159
166
  Each returns `Result<MeshAsset, AssetError>`. Interleaved vertex layout:
160
167
  position (3xf32) + normal (3xf32) + uv (2xf32) = 8 floats/vertex at creation
@@ -170,6 +177,9 @@ time; expanded to the 12-float runtime layout (adds tangent vec4) by
170
177
  | `createPlaneGeometry` | `(w, h, wSeg?, hSeg?)` | 1 / dim; XY plane, +Z normal |
171
178
  | `createSphereGeometry` | `(radius, wSeg?, hSeg?)` | wSeg >= 3, hSeg >= 2 |
172
179
  | `createTorusGeometry` | `(radius, tube, radSeg?, tubSeg?)` | radSeg >= 3, tubSeg >= 3 |
180
+ | `createExtrusionGeometry` | `(contour, depth)` | 3+ finite simple contour points, depth > 0 |
181
+ | `createSweepGeometry` | `(path, radius, radialSeg?)` | 2+ distinct finite 3D points, radius > 0, radialSeg >= 3 |
182
+ | `createRevolutionGeometry` | `(profile, radialSeg?)` | 2+ finite non-negative-radius points spanning height, radialSeg >= 3 |
173
183
 
174
184
  All factories populate `position` / `normal` / `uv` attributes with lowercase
175
185
  Three.js-r184 key naming. `VertexAttributeMap` also accepts optional `color`
@@ -233,7 +243,7 @@ them in sync.
233
243
 
234
244
  ## Three.js r184 mental alignment
235
245
 
236
- The 6 factory signatures mirror Three.js `BufferGeometry` constructors
246
+ The seven primitive factory signatures mirror Three.js `BufferGeometry` constructors
237
247
  byte-for-byte: parameter order, attribute key lowercasing (`position` / `normal`
238
248
  / `uv`), and segment-count defaults. AI users migrating from Three.js can swap
239
249
  the import path and the return-shape check:
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=procedural.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"procedural.unit.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/procedural.unit.test.ts"],"names":[],"mappings":""}
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export { compute2dBounds, create2dGeometry, create2dRingGeometry, type Shape2d,
10
10
  export { createEdgesGeometry, createWireframeGeometry } from './edges';
11
11
  export { createMeshBuilder, type MeshBuilder, type MeshBuilderOptions, type MeshBuilderSubmesh, } from './mesh-builder';
12
12
  export { createPlaneGeometry } from './plane';
13
+ export { createExtrusionGeometry, createRevolutionGeometry, createSweepGeometry, type Vec2Point, type Vec3Point, } from './procedural';
13
14
  export { createSphereGeometry } from './sphere';
14
15
  export { computeTangentVec4 } from './tangent';
15
16
  export { createTeapotGeometry, type TeapotMeshAsset, type TeapotProvenance, } from './teapot';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,YAAY,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC9E,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC/F,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,iBAAiB,GACvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,4BAA4B,GAC7B,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,oBAAoB,EACpB,KAAK,OAAO,EACZ,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,KAAK,IAAI,GACV,MAAM,QAAQ,CAAC;AAEhB,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AACvE,OAAO,EACL,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EACL,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EACL,8BAA8B,EAC9B,4BAA4B,EAC5B,wBAAwB,EACxB,sCAAsC,EACtC,iBAAiB,EACjB,4BAA4B,EAC5B,oCAAoC,EACpC,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,+BAA+B,EAC/B,yBAAyB,EACzB,wBAAwB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,+BAA+B,EACpC,KAAK,+BAA+B,GACrC,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,YAAY,EAAE,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC9E,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC/F,OAAO,EACL,mBAAmB,EACnB,oBAAoB,EACpB,KAAK,iBAAiB,GACvB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,4BAA4B,GAC7B,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,QAAQ,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,oBAAoB,EACpB,KAAK,OAAO,EACZ,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,KAAK,IAAI,GACV,MAAM,QAAQ,CAAC;AAEhB,OAAO,EAAE,mBAAmB,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AACvE,OAAO,EACL,iBAAiB,EACjB,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,GACxB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EACL,uBAAuB,EACvB,wBAAwB,EACxB,mBAAmB,EACnB,KAAK,SAAS,EACd,KAAK,SAAS,GACf,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EACL,oBAAoB,EACpB,KAAK,eAAe,EACpB,KAAK,gBAAgB,GACtB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EACL,8BAA8B,EAC9B,4BAA4B,EAC5B,wBAAwB,EACxB,sCAAsC,EACtC,iBAAiB,EACjB,4BAA4B,EAC5B,oCAAoC,EACpC,KAAK,0BAA0B,EAC/B,KAAK,sBAAsB,EAC3B,+BAA+B,EAC/B,yBAAyB,EACzB,wBAAwB,EACxB,KAAK,sBAAsB,EAC3B,KAAK,+BAA+B,EACpC,KAAK,+BAA+B,GACrC,MAAM,2BAA2B,CAAC"}
package/dist/index.mjs CHANGED
@@ -475,13 +475,13 @@ function deriveVertexLayoutProjectionFromMask(mask) {
475
475
  return ok(deriveVertexLayoutProjection(map));
476
476
  }
477
477
  function packInterleavedVertexAttributes(map, vertexCount) {
478
- const invalid2 = (detail) => err(new VertexAttributePackError(detail));
478
+ const invalid3 = (detail) => err(new VertexAttributePackError(detail));
479
479
  if (!Number.isInteger(vertexCount) || vertexCount < 0) {
480
- return invalid2({ field: "vertexCount", reason: "vertex-count-invalid", actual: vertexCount });
480
+ return invalid3({ field: "vertexCount", reason: "vertex-count-invalid", actual: vertexCount });
481
481
  }
482
482
  const projection = deriveVertexLayoutProjection(map);
483
483
  if (projection.attributes.length === 0) {
484
- return invalid2({ field: "attributes", reason: "attributes-empty", actualCount: 0 });
484
+ return invalid3({ field: "attributes", reason: "attributes-empty", actualCount: 0 });
485
485
  }
486
486
  const output = new ArrayBuffer(projection.arrayStride * vertexCount);
487
487
  const outputFloats = new Float32Array(output);
@@ -493,7 +493,7 @@ function packInterleavedVertexAttributes(map, vertexCount) {
493
493
  const components = entry.byteLength / (uint16 ? 2 : 4);
494
494
  const bytesPerComponent = uint16 ? 2 : 4;
495
495
  if (sourceValue instanceof ArrayBuffer && sourceValue.byteLength % bytesPerComponent !== 0) {
496
- return invalid2({
496
+ return invalid3({
497
497
  field: entry.key,
498
498
  reason: "attribute-cardinality-mismatch",
499
499
  vertexCount,
@@ -504,7 +504,7 @@ function packInterleavedVertexAttributes(map, vertexCount) {
504
504
  }
505
505
  const source = elementView(sourceValue, entry.format);
506
506
  if (source === void 0 || (uint16 ? storageOf(sourceValue) !== "uint16" && storageOf(sourceValue) !== "array-buffer" : storageOf(sourceValue) !== "float32" && storageOf(sourceValue) !== "array-buffer")) {
507
- return invalid2({
507
+ return invalid3({
508
508
  field: entry.key,
509
509
  reason: "attribute-storage-invalid",
510
510
  expectedStorage: uint16 ? "uint16" : "float32",
@@ -513,7 +513,7 @@ function packInterleavedVertexAttributes(map, vertexCount) {
513
513
  }
514
514
  const expectedLength = vertexCount * components;
515
515
  if (source.length !== expectedLength) {
516
- return invalid2({
516
+ return invalid3({
517
517
  field: entry.key,
518
518
  reason: "attribute-cardinality-mismatch",
519
519
  vertexCount,
@@ -526,7 +526,7 @@ function packInterleavedVertexAttributes(map, vertexCount) {
526
526
  for (let elementIndex = 0; elementIndex < source.length; elementIndex += 1) {
527
527
  const component = source[elementIndex];
528
528
  if (!Number.isFinite(component)) {
529
- return invalid2({
529
+ return invalid3({
530
530
  field: "color",
531
531
  reason: "attribute-non-finite",
532
532
  elementIndex,
@@ -2706,6 +2706,356 @@ function createMeshBuilder(options = {}) {
2706
2706
  for (const submesh of options.submeshes ?? []) submeshes.push({ ...submesh });
2707
2707
  return { appendVertices, appendIndices, addSubmesh, build };
2708
2708
  }
2709
+ function invalid2(field, detail) {
2710
+ return err(
2711
+ new AssetError({
2712
+ code: "asset-parse-failed",
2713
+ expected: `valid procedural geometry input for ${field}`,
2714
+ hint: ASSET_ERROR_HINTS["asset-parse-failed"],
2715
+ detail: { field, value: detail, reason: detail }
2716
+ })
2717
+ );
2718
+ }
2719
+ function finitePoint2(point) {
2720
+ return Number.isFinite(point.x) && Number.isFinite(point.y);
2721
+ }
2722
+ function cross2(a, b, c) {
2723
+ return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);
2724
+ }
2725
+ function signedArea(points) {
2726
+ let value = 0;
2727
+ for (let i = 0; i < points.length; i++) {
2728
+ const a = points[i];
2729
+ const b = points[(i + 1) % points.length];
2730
+ value += a.x * b.y - b.x * a.y;
2731
+ }
2732
+ return value / 2;
2733
+ }
2734
+ function onSegment(a, b, p) {
2735
+ return Math.min(a.x, b.x) <= p.x + 1e-8 && p.x <= Math.max(a.x, b.x) + 1e-8 && Math.min(a.y, b.y) <= p.y + 1e-8 && p.y <= Math.max(a.y, b.y) + 1e-8;
2736
+ }
2737
+ function edgesCross(a, b, c, d) {
2738
+ const ab = cross2(a, b, c);
2739
+ const abD = cross2(a, b, d);
2740
+ const cdA = cross2(c, d, a);
2741
+ const cdB = cross2(c, d, b);
2742
+ if (Math.abs(ab) < 1e-8 && onSegment(a, b, c)) return true;
2743
+ if (Math.abs(abD) < 1e-8 && onSegment(a, b, d)) return true;
2744
+ if (Math.abs(cdA) < 1e-8 && onSegment(c, d, a)) return true;
2745
+ if (Math.abs(cdB) < 1e-8 && onSegment(c, d, b)) return true;
2746
+ return ab > 0 !== abD > 0 && cdA > 0 !== cdB > 0;
2747
+ }
2748
+ function selfIntersects(points) {
2749
+ for (let i = 0; i < points.length; i++) {
2750
+ const a = points[i];
2751
+ const b = points[(i + 1) % points.length];
2752
+ for (let j = i + 1; j < points.length; j++) {
2753
+ if (j === i || (j + 1) % points.length === i || (i + 1) % points.length === j) continue;
2754
+ const c = points[j];
2755
+ const d = points[(j + 1) % points.length];
2756
+ if (edgesCross(a, b, c, d)) return true;
2757
+ }
2758
+ }
2759
+ return false;
2760
+ }
2761
+ function insideTriangle(a, b, c, point) {
2762
+ return cross2(a, b, point) >= -1e-8 && cross2(b, c, point) >= -1e-8 && cross2(c, a, point) >= -1e-8;
2763
+ }
2764
+ function triangulate(points) {
2765
+ const order = points.map((_, index) => index);
2766
+ if (signedArea(points) < 0) order.reverse();
2767
+ const triangles = [];
2768
+ let guard = 0;
2769
+ while (order.length > 3 && guard++ < points.length * points.length) {
2770
+ let clipped = false;
2771
+ for (let i = 0; i < order.length; i++) {
2772
+ const previous = order[(i + order.length - 1) % order.length];
2773
+ const current = order[i];
2774
+ const next = order[(i + 1) % order.length];
2775
+ if (cross2(
2776
+ points[previous],
2777
+ points[current],
2778
+ points[next]
2779
+ ) <= 1e-8)
2780
+ continue;
2781
+ let contains = false;
2782
+ for (const candidate of order) {
2783
+ if (candidate !== previous && candidate !== current && candidate !== next && insideTriangle(
2784
+ points[previous],
2785
+ points[current],
2786
+ points[next],
2787
+ points[candidate]
2788
+ )) {
2789
+ contains = true;
2790
+ break;
2791
+ }
2792
+ }
2793
+ if (contains) continue;
2794
+ triangles.push(previous, current, next);
2795
+ order.splice(i, 1);
2796
+ clipped = true;
2797
+ break;
2798
+ }
2799
+ if (!clipped) return void 0;
2800
+ }
2801
+ if (order.length !== 3) return void 0;
2802
+ triangles.push(order[0], order[1], order[2]);
2803
+ return triangles;
2804
+ }
2805
+ function pushVertex(vertices, position, normal, uv) {
2806
+ const index = vertices.length / FACTORY_FLOATS_PER_VERTEX;
2807
+ vertices.push(
2808
+ position[0],
2809
+ position[1],
2810
+ position[2],
2811
+ normal[0],
2812
+ normal[1],
2813
+ normal[2],
2814
+ uv[0],
2815
+ uv[1]
2816
+ );
2817
+ return index;
2818
+ }
2819
+ function pushTriangle(vertices, indices, a, b, c) {
2820
+ indices.push(
2821
+ pushVertex(vertices, a.p, a.n, a.uv),
2822
+ pushVertex(vertices, b.p, b.n, b.uv),
2823
+ pushVertex(vertices, c.p, c.n, c.uv)
2824
+ );
2825
+ }
2826
+ function createExtrusionGeometry(contour, depth) {
2827
+ if (!Number.isFinite(depth) || depth <= 0) return invalid2("depth", "must be positive and finite");
2828
+ const points = contour.length > 1 && contour[0]?.x === contour[contour.length - 1]?.x && contour[0]?.y === contour[contour.length - 1]?.y ? contour.slice(0, -1) : [...contour];
2829
+ if (points.length < 3 || points.some((point) => !finitePoint2(point)))
2830
+ return invalid2("contour", "needs at least three finite points");
2831
+ const area2 = signedArea(points);
2832
+ if (Math.abs(area2) < 1e-8) return invalid2("contour", "area must be non-zero");
2833
+ if (selfIntersects(points)) return invalid2("contour", "must not self-intersect");
2834
+ const capTriangles = triangulate(points);
2835
+ if (capTriangles === void 0) return invalid2("contour", "could not be triangulated");
2836
+ const vertices = [];
2837
+ const indices = [];
2838
+ const half = depth / 2;
2839
+ for (let i = 0; i < capTriangles.length; i += 3) {
2840
+ const a = points[capTriangles[i]];
2841
+ const b = points[capTriangles[i + 1]];
2842
+ const c = points[capTriangles[i + 2]];
2843
+ pushTriangle(
2844
+ vertices,
2845
+ indices,
2846
+ { p: [a.x, a.y, half], n: [0, 0, 1], uv: [a.x, a.y] },
2847
+ { p: [b.x, b.y, half], n: [0, 0, 1], uv: [b.x, b.y] },
2848
+ { p: [c.x, c.y, half], n: [0, 0, 1], uv: [c.x, c.y] }
2849
+ );
2850
+ pushTriangle(
2851
+ vertices,
2852
+ indices,
2853
+ { p: [c.x, c.y, -half], n: [0, 0, -1], uv: [c.x, c.y] },
2854
+ { p: [b.x, b.y, -half], n: [0, 0, -1], uv: [b.x, b.y] },
2855
+ { p: [a.x, a.y, -half], n: [0, 0, -1], uv: [a.x, a.y] }
2856
+ );
2857
+ }
2858
+ let perimeter = 0;
2859
+ for (let i = 0; i < points.length; i++) {
2860
+ const a = points[i];
2861
+ const b = points[(i + 1) % points.length];
2862
+ perimeter += Math.hypot(b.x - a.x, b.y - a.y);
2863
+ }
2864
+ let distance = 0;
2865
+ for (let i = 0; i < points.length; i++) {
2866
+ const a = points[i];
2867
+ const b = points[(i + 1) % points.length];
2868
+ const edge = Math.hypot(b.x - a.x, b.y - a.y);
2869
+ const u0 = perimeter === 0 ? 0 : distance / perimeter;
2870
+ const u1 = perimeter === 0 ? 1 : (distance + edge) / perimeter;
2871
+ const nx = b.y - a.y;
2872
+ const ny = -(b.x - a.x);
2873
+ const length = Math.hypot(nx, ny) || 1;
2874
+ const normal = area2 >= 0 ? [nx / length, ny / length, 0] : [-nx / length, -ny / length, 0];
2875
+ if (area2 >= 0) {
2876
+ pushTriangle(
2877
+ vertices,
2878
+ indices,
2879
+ { p: [a.x, a.y, -half], n: normal, uv: [u0, 0] },
2880
+ { p: [b.x, b.y, -half], n: normal, uv: [u1, 0] },
2881
+ { p: [b.x, b.y, half], n: normal, uv: [u1, 1] }
2882
+ );
2883
+ pushTriangle(
2884
+ vertices,
2885
+ indices,
2886
+ { p: [a.x, a.y, -half], n: normal, uv: [u0, 0] },
2887
+ { p: [b.x, b.y, half], n: normal, uv: [u1, 1] },
2888
+ { p: [a.x, a.y, half], n: normal, uv: [u0, 1] }
2889
+ );
2890
+ } else {
2891
+ pushTriangle(
2892
+ vertices,
2893
+ indices,
2894
+ { p: [a.x, a.y, -half], n: normal, uv: [u0, 0] },
2895
+ { p: [b.x, b.y, half], n: normal, uv: [u1, 1] },
2896
+ { p: [b.x, b.y, -half], n: normal, uv: [u1, 0] }
2897
+ );
2898
+ pushTriangle(
2899
+ vertices,
2900
+ indices,
2901
+ { p: [a.x, a.y, -half], n: normal, uv: [u0, 0] },
2902
+ { p: [a.x, a.y, half], n: normal, uv: [u0, 1] },
2903
+ { p: [b.x, b.y, half], n: normal, uv: [u1, 1] }
2904
+ );
2905
+ }
2906
+ distance += edge;
2907
+ }
2908
+ return meshFromInterleaved(new Float32Array(vertices), new Uint32Array(indices));
2909
+ }
2910
+ function createSweepGeometry(path, radius, radialSegments = 12) {
2911
+ if (!Number.isFinite(radius) || radius <= 0)
2912
+ return invalid2("radius", "must be positive and finite");
2913
+ const segments = radialSegments | 0;
2914
+ if (path.length < 2 || path.some((point) => point.length !== 3 || point.some((value) => !Number.isFinite(value))))
2915
+ return invalid2("path", "needs at least two finite 3D points");
2916
+ if (segments < 3) return invalid2("radialSegments", "must be at least 3");
2917
+ const first = path[0];
2918
+ const last = path[path.length - 1];
2919
+ const closed = path.length > 3 && Math.hypot(first[0] - last[0], first[1] - last[1], first[2] - last[2]) <= 1e-8;
2920
+ for (let left = 0; left < path.length; left++) {
2921
+ const a = path[left];
2922
+ for (let right = left + 1; right < path.length; right++) {
2923
+ const b = path[right];
2924
+ if (closed && left === 0 && right === path.length - 1) continue;
2925
+ if (Math.hypot(a[0] - b[0], a[1] - b[1], a[2] - b[2]) <= 1e-8) {
2926
+ return invalid2("path", "must not contain repeated points");
2927
+ }
2928
+ }
2929
+ }
2930
+ const lengths = [0];
2931
+ for (let i = 1; i < path.length; i++) {
2932
+ const previous = path[i - 1];
2933
+ const current = path[i];
2934
+ lengths.push(
2935
+ lengths[i - 1] + Math.hypot(current[0] - previous[0], current[1] - previous[1], current[2] - previous[2])
2936
+ );
2937
+ }
2938
+ const total = lengths[lengths.length - 1];
2939
+ if (!(total > 0)) return invalid2("path", "must contain distinct points");
2940
+ const vertices = [];
2941
+ const indices = [];
2942
+ for (let row = 0; row < path.length; row++) {
2943
+ const point = path[row];
2944
+ const previous = path[Math.max(0, row - 1)];
2945
+ const next = path[Math.min(path.length - 1, row + 1)];
2946
+ let tx = next[0] - previous[0];
2947
+ let ty = next[1] - previous[1];
2948
+ let tz = next[2] - previous[2];
2949
+ const tangentLength = Math.hypot(tx, ty, tz) || 1;
2950
+ tx /= tangentLength;
2951
+ ty /= tangentLength;
2952
+ tz /= tangentLength;
2953
+ const reference = Math.abs(ty) < 0.95 ? [0, 1, 0] : [1, 0, 0];
2954
+ let nx = ty * reference[2] - tz * reference[1];
2955
+ let ny = tz * reference[0] - tx * reference[2];
2956
+ let nz = tx * reference[1] - ty * reference[0];
2957
+ const nLength = Math.hypot(nx, ny, nz) || 1;
2958
+ nx /= nLength;
2959
+ ny /= nLength;
2960
+ nz /= nLength;
2961
+ const bx = ty * nz - tz * ny;
2962
+ const by = tz * nx - tx * nz;
2963
+ const bz = tx * ny - ty * nx;
2964
+ for (let column = 0; column <= segments; column++) {
2965
+ const angle = column / segments * Math.PI * 2;
2966
+ const radialX = Math.cos(angle) * nx + Math.sin(angle) * bx;
2967
+ const radialY = Math.cos(angle) * ny + Math.sin(angle) * by;
2968
+ const radialZ = Math.cos(angle) * nz + Math.sin(angle) * bz;
2969
+ pushVertex(
2970
+ vertices,
2971
+ [point[0] + radialX * radius, point[1] + radialY * radius, point[2] + radialZ * radius],
2972
+ [radialX, radialY, radialZ],
2973
+ [column / segments, lengths[row] / total]
2974
+ );
2975
+ }
2976
+ }
2977
+ const stride = segments + 1;
2978
+ const position = (index) => [
2979
+ vertices[index * 8],
2980
+ vertices[index * 8 + 1],
2981
+ vertices[index * 8 + 2]
2982
+ ];
2983
+ for (let row = 0; row < path.length - 1; row++) {
2984
+ for (let column = 0; column < segments; column++) {
2985
+ const a = row * stride + column;
2986
+ const b = a + 1;
2987
+ const c = (row + 1) * stride + column + 1;
2988
+ const d = (row + 1) * stride + column;
2989
+ const p = position(a);
2990
+ const q = position(b);
2991
+ const r = position(c);
2992
+ const ab = [q[0] - p[0], q[1] - p[1], q[2] - p[2]];
2993
+ const ac = [r[0] - p[0], r[1] - p[1], r[2] - p[2]];
2994
+ const normal = [
2995
+ ab[1] * ac[2] - ab[2] * ac[1],
2996
+ ab[2] * ac[0] - ab[0] * ac[2],
2997
+ ab[0] * ac[1] - ab[1] * ac[0]
2998
+ ];
2999
+ const center = path[row];
3000
+ const outward = [p[0] - center[0], p[1] - center[1], p[2] - center[2]];
3001
+ const dot = normal[0] * outward[0] + normal[1] * outward[1] + normal[2] * outward[2];
3002
+ if (dot > 0) indices.push(a, b, c, a, c, d);
3003
+ else indices.push(a, c, b, a, d, c);
3004
+ }
3005
+ }
3006
+ return meshFromInterleaved(new Float32Array(vertices), new Uint32Array(indices));
3007
+ }
3008
+ function createRevolutionGeometry(profile, radialSegments = 24) {
3009
+ const segments = radialSegments | 0;
3010
+ if (profile.length < 2 || profile.some((point) => !finitePoint2(point) || point.x < 0))
3011
+ return invalid2("profile", "needs at least two finite points with non-negative radius");
3012
+ if (segments < 3) return invalid2("radialSegments", "must be at least 3");
3013
+ const first = profile[0];
3014
+ if (first === void 0 || !profile.some((point) => point.y !== first.y))
3015
+ return invalid2("profile", "must span a non-zero height");
3016
+ for (let left = 0; left < profile.length; left++) {
3017
+ const a = profile[left];
3018
+ for (let right = left + 1; right < profile.length; right++) {
3019
+ const b = profile[right];
3020
+ if (Math.hypot(a.x - b.x, a.y - b.y) <= 1e-8) {
3021
+ return invalid2("profile", "must not contain repeated points");
3022
+ }
3023
+ }
3024
+ }
3025
+ const vertices = [];
3026
+ const indices = [];
3027
+ for (let row = 0; row < profile.length; row++) {
3028
+ const point = profile[row];
3029
+ const previous = profile[Math.max(0, row - 1)];
3030
+ const next = profile[Math.min(profile.length - 1, row + 1)];
3031
+ const dr = next.x - previous.x;
3032
+ const dy = next.y - previous.y;
3033
+ const normalLength = Math.hypot(dy, dr) || 1;
3034
+ for (let column = 0; column <= segments; column++) {
3035
+ const u = column / segments;
3036
+ const angle = u * Math.PI * 2;
3037
+ const c = Math.cos(angle);
3038
+ const s = Math.sin(angle);
3039
+ pushVertex(
3040
+ vertices,
3041
+ [point.x * c, point.y, point.x * s],
3042
+ [c * dy / normalLength, -dr / normalLength, s * dy / normalLength],
3043
+ [u, row / (profile.length - 1)]
3044
+ );
3045
+ }
3046
+ }
3047
+ const stride = segments + 1;
3048
+ for (let row = 0; row < profile.length - 1; row++) {
3049
+ for (let column = 0; column < segments; column++) {
3050
+ const a = row * stride + column;
3051
+ const b = a + 1;
3052
+ const c = (row + 1) * stride + column + 1;
3053
+ const d = (row + 1) * stride + column;
3054
+ indices.push(a, d, b, b, d, c);
3055
+ }
3056
+ }
3057
+ return meshFromInterleaved(new Float32Array(vertices), new Uint32Array(indices));
3058
+ }
2709
3059
  var PATCHES = new Uint16Array([
2710
3060
  0,
2711
3061
  1,
@@ -4137,7 +4487,7 @@ function evaluatePatch(surface, s, t, fitLid, blinn) {
4137
4487
  }
4138
4488
  return { point, ds, dt };
4139
4489
  }
4140
- function cross2(a, b) {
4490
+ function cross3(a, b) {
4141
4491
  return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
4142
4492
  }
4143
4493
  function normalize(value) {
@@ -4179,7 +4529,7 @@ function createTeapotGeometry(size = 0.8, segments = 18, bottom = true, lid = tr
4179
4529
  for (let tStep = 0; tStep <= steps; tStep++) {
4180
4530
  const t = tStep / steps;
4181
4531
  const evaluated = evaluatePatch(surface, s, t, fitLid, blinn);
4182
- const normalRaw = cross2(evaluated.dt, evaluated.ds);
4532
+ const normalRaw = cross3(evaluated.dt, evaluated.ds);
4183
4533
  const normal = evaluated.point[0] === 0 && evaluated.point[1] === 0 ? [0, evaluated.point[2] > maxHeight2 ? 1 : -1, 0] : normalize([normalRaw[0], normalRaw[2], -normalRaw[1]]);
4184
4534
  const position = [
4185
4535
  Math.fround(trueSize * evaluated.point[0]),
@@ -4282,6 +4632,6 @@ function createTorusGeometry(radius, tube, radialSegments = 8, tubularSegments =
4282
4632
  return meshFromInterleaved(vertices, indices);
4283
4633
  }
4284
4634
 
4285
- export { DEFAULT_VERTEX_ATTRIBUTE_MAP, PROCEDURAL_FLOATS_PER_VERTEX, SKIN_VERTEX_ATTRIBUTE_MAP, VertexAttributePackError, buildMeshAttributeMapForUvSets, compute2dBounds, computeTangentVec4, create2dGeometry, create2dRingGeometry, createBoxGeometry, createCapsuleGeometry, createConeGeometry, createCylinderGeometry, createEdgesGeometry, createMeshBuilder, createPlaneGeometry, createPrimitiveMesh, createProceduralMesh, createSphereGeometry, createTeapotGeometry, createTorusGeometry, createWireframeGeometry, decodeMeshBinary, deriveVertexBufferLayout, deriveVertexBufferLayoutFromProjection, deriveVertexCount, deriveVertexLayoutProjection, deriveVertexLayoutProjectionFromMask, meshAssetContribution, meshAssetDecoder, meshAssetKind, meshFromInterleaved, normalizeMeshPayload, packInterleavedVertexAttributes };
4635
+ export { DEFAULT_VERTEX_ATTRIBUTE_MAP, PROCEDURAL_FLOATS_PER_VERTEX, SKIN_VERTEX_ATTRIBUTE_MAP, VertexAttributePackError, buildMeshAttributeMapForUvSets, compute2dBounds, computeTangentVec4, create2dGeometry, create2dRingGeometry, createBoxGeometry, createCapsuleGeometry, createConeGeometry, createCylinderGeometry, createEdgesGeometry, createExtrusionGeometry, createMeshBuilder, createPlaneGeometry, createPrimitiveMesh, createProceduralMesh, createRevolutionGeometry, createSphereGeometry, createSweepGeometry, createTeapotGeometry, createTorusGeometry, createWireframeGeometry, decodeMeshBinary, deriveVertexBufferLayout, deriveVertexBufferLayoutFromProjection, deriveVertexCount, deriveVertexLayoutProjection, deriveVertexLayoutProjectionFromMask, meshAssetContribution, meshAssetDecoder, meshAssetKind, meshFromInterleaved, normalizeMeshPayload, packInterleavedVertexAttributes };
4286
4636
  //# sourceMappingURL=index.mjs.map
4287
4637
  //# sourceMappingURL=index.mjs.map