@solidrt/3d 0.0.51 → 0.0.52

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/src/sweep.ts CHANGED
@@ -8,8 +8,8 @@
8
8
  // Uint32Array by vertex count - filleted profiles times many path points
9
9
  // make dense outputs routine here.
10
10
 
11
- import { FLOATS_PER_VERTEX, packIndices } from "./geometry.ts"
12
- import type { Geometry } from "./geometry.ts"
11
+ import { STANDARD_FLOATS, packGeometry } from "./geometry.ts"
12
+ import type { Geometry, GeometryOptions } from "./geometry.ts"
13
13
  import { add, cross, dot, normalize, scale, sub } from "./math.ts"
14
14
  import type { Vec3 } from "./math.ts"
15
15
  import { earClip, normalizeProfile, profileBounds, profileRing } from "./profile.ts"
@@ -58,7 +58,7 @@ function emitCap(
58
58
  mirrorU: boolean,
59
59
  flip: boolean,
60
60
  ): void {
61
- let base = verts.length / FLOATS_PER_VERTEX
61
+ let base = verts.length / STANDARD_FLOATS
62
62
  for (let i = 0; i < px.length; i++) {
63
63
  let p = place(px[i]!, py[i]!)
64
64
  let u = mirrorU ? (b.maxX - px[i]!) / b.w : (px[i]! - b.minX) / b.w
@@ -81,13 +81,10 @@ function emitCap(
81
81
  * unit square like plane(); the -z cap mirrors u so its texture reads
82
82
  * unmirrored from outside.
83
83
  */
84
- export function extrude(
85
- profile: Profile,
86
- depth = 1,
87
- bevel = 0,
88
- bevelSegments = 4,
89
- label?: string,
90
- ): Geometry {
84
+ export type ExtrudeOptions = GeometryOptions & { depth?: number; bevel?: number; bevelSegments?: number }
85
+
86
+ export function extrude(profile: Profile, options: ExtrudeOptions = {}): Geometry {
87
+ let { depth = 1, bevel = 0, bevelSegments = 4 } = options
91
88
  let pts = normalizeProfile(profile)
92
89
  let { entries, miterX, miterY } = profileRing(pts)
93
90
  let h = depth / 2
@@ -155,11 +152,7 @@ export function extrude(
155
152
  emitCap(verts, indices, cx, cy, tris, bounds, (x, y) => [x, y, h], [0, 0, 1], false, false)
156
153
  emitCap(verts, indices, cx, cy, tris, bounds, (x, y) => [x, y, -h], [0, 0, -1], true, true)
157
154
 
158
- return {
159
- vertices: new Float32Array(verts),
160
- indices: packIndices(indices, verts.length / FLOATS_PER_VERTEX),
161
- label,
162
- }
155
+ return packGeometry(verts, indices, options)
163
156
  }
164
157
 
165
158
  /**
@@ -172,13 +165,10 @@ export function extrude(
172
165
  * duplicated like torus), v = normalized distance around the profile;
173
166
  * caps map the profile's bounding box, the end cap mirrored in u.
174
167
  */
175
- export function lathe(
176
- profile: Profile,
177
- segments = 32,
178
- angle = Math.PI * 2,
179
- start = 0,
180
- label?: string,
181
- ): Geometry {
168
+ export type LatheOptions = GeometryOptions & { segments?: number; angle?: number; start?: number }
169
+
170
+ export function lathe(profile: Profile, options: LatheOptions = {}): Geometry {
171
+ let { segments = 32, angle = Math.PI * 2, start = 0 } = options
182
172
  if (!(angle > 0) || angle > Math.PI * 2 + 1e-9) throw new Error("Lathe angle must be in (0, 2*PI]")
183
173
  let pts = normalizeProfile(profile)
184
174
  let { entries } = profileRing(pts)
@@ -215,11 +205,7 @@ export function lathe(
215
205
  }
216
206
  }
217
207
 
218
- return {
219
- vertices: new Float32Array(verts),
220
- indices: packIndices(indices, verts.length / FLOATS_PER_VERTEX),
221
- label,
222
- }
208
+ return packGeometry(verts, indices, options)
223
209
  }
224
210
 
225
211
  /** A sweep-path point: `p` in world space, `smooth` to share one ring
@@ -351,7 +337,7 @@ export function pathFrames(path: SweepPath): PathFrames {
351
337
  * mirrored in u. A near-reversal joint clamps its mitre stretch (4x,
352
338
  * like the profile's own miter clamp) instead of flinging vertices.
353
339
  */
354
- export function sweep(profile: Profile, path: SweepPath, label?: string): Geometry {
340
+ export function sweep(profile: Profile, path: SweepPath, options: GeometryOptions = {}): Geometry {
355
341
  let pts = normalizeProfile(profile)
356
342
  let { entries } = profileRing(pts)
357
343
  let p = normalizePath(path)
@@ -362,7 +348,7 @@ export function sweep(profile: Profile, path: SweepPath, label?: string): Geomet
362
348
  let verts: number[] = []
363
349
  let indices: number[] = []
364
350
  let emitRing = (pos: Vec3[], normals: Vec3[], v: number): number => {
365
- let base = verts.length / FLOATS_PER_VERTEX
351
+ let base = verts.length / STANDARD_FLOATS
366
352
  for (let k = 0; k < entries.length; k++) {
367
353
  let q = pos[k]!
368
354
  let m = normals[k]!
@@ -436,11 +422,7 @@ export function sweep(profile: Profile, path: SweepPath, label?: string): Geomet
436
422
  emitCap(verts, indices, px, py, tris, bounds, capPlace(p[0]!.p, 0), scale(tangents[0]!, -1), false, false)
437
423
  emitCap(verts, indices, px, py, tris, bounds, capPlace(p[n - 1]!.p, n - 2), tangents[n - 2]!, true, true)
438
424
 
439
- return {
440
- vertices: new Float32Array(verts),
441
- indices: packIndices(indices, verts.length / FLOATS_PER_VERTEX),
442
- label,
443
- }
425
+ return packGeometry(verts, indices, options)
444
426
  }
445
427
 
446
428
  /**
@@ -450,11 +432,14 @@ export function sweep(profile: Profile, path: SweepPath, label?: string): Geomet
450
432
  * bend, smooth = continuous), and both ends get flat caps. UVs: u around
451
433
  * the tube, v along the path.
452
434
  */
453
- export function tube(path: SweepPath, radius = 0.5, radialSegments = 12, label?: string): Geometry {
435
+ export type TubeOptions = GeometryOptions & { radius?: number; radialSegments?: number }
436
+
437
+ export function tube(path: SweepPath, options: TubeOptions = {}): Geometry {
438
+ let { radius = 0.5, radialSegments = 12, ...rest } = options
454
439
  let profile: Profile = []
455
440
  for (let i = 0; i < radialSegments; i++) {
456
441
  let a = (i / radialSegments) * Math.PI * 2
457
442
  profile.push({ p: [Math.cos(a) * radius, Math.sin(a) * radius], smooth: true })
458
443
  }
459
- return sweep(profile, path, label)
444
+ return sweep(profile, path, rest)
460
445
  }
package/src/bvh.ts DELETED
@@ -1,258 +0,0 @@
1
- // A dynamic bounding-volume hierarchy - the physics-broadphase AABB tree
2
- // (Box2D/Bullet lineage): a binary tree over the items' world boxes, NOT a
3
- // subdivision of space. Leaves store FAT boxes (a margin around the tight
4
- // bounds) so an item moving a little refits nothing; an item escaping its
5
- // fat box is removed and re-inserted along the cheapest path (least area
6
- // growth). A ray query walks O(log n) nodes instead of testing every item,
7
- // which is what keeps per-pointer-move picking off the O(meshes) path.
8
- //
9
- // The scene keeps the tree current from its own sync walk: transforms that
10
- // changed are exactly the leaves to update, so maintenance is O(changed)
11
- // per frame - the same delta discipline as rendering, and a static scene
12
- // pays nothing. Storage is flat parallel arrays indexed by node id (no
13
- // per-node objects, no allocation at steady state past tree growth).
14
- //
15
- // Pure module by design: no GUI imports, so the differential check rig
16
- // (checks/pick-check.ts) runs it headless on flux against a linear oracle.
17
-
18
- /** Fat-margin fraction of a leaf's largest extent. Bigger = fewer
19
- * re-inserts while moving, worse query pruning; 5% is the usual trade. */
20
- const MARGIN = 0.05
21
-
22
- type Visit<T> = (item: T) => void
23
-
24
- export type Bvh<T> = {
25
- /** Insert an item with its tight world box; returns the leaf handle. */
26
- insert(item: T, minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number): number
27
- /** Update a leaf's tight box. Free while it stays inside the fat box;
28
- * otherwise the leaf re-inserts. Returns true when it moved. */
29
- update(leaf: number, minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number): boolean
30
- /** Remove a leaf (the handle is dead afterwards). */
31
- remove(leaf: number): void
32
- /** Visit every item whose FAT box the ray hits (t >= 0). Broadphase
33
- * only: the caller narrowphases against its own tight volumes. */
34
- raycast(ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, visit: Visit<T>): void
35
- }
36
-
37
- /**
38
- * Entry distance of a ray against a box: the smallest t >= 0 with
39
- * origin + t * direction inside [min, max] (0 when the origin starts
40
- * inside), or -1 for a miss. The direction need not be normalized - t is
41
- * in units of its length, which is what keeps a ray transformed into a
42
- * mesh's local space reporting world distances. Shared by the tree's
43
- * broadphase and the scene's narrowphase.
44
- */
45
- export function rayBoxDistance(
46
- ox: number, oy: number, oz: number,
47
- dx: number, dy: number, dz: number,
48
- minX: number, minY: number, minZ: number,
49
- maxX: number, maxY: number, maxZ: number,
50
- ): number {
51
- let tNear = 0
52
- let tFar = Infinity
53
- // Per axis: a zero direction component never crosses the slab, so the
54
- // origin must already be inside it (the multiply-by-inverse shortcut
55
- // turns that case into NaN, hence the explicit branch).
56
- if (dx === 0) {
57
- if (ox < minX || ox > maxX) return -1
58
- } else {
59
- let inv = 1 / dx
60
- let t1 = (minX - ox) * inv
61
- let t2 = (maxX - ox) * inv
62
- if (t1 > t2) { let t = t1; t1 = t2; t2 = t }
63
- if (t1 > tNear) tNear = t1
64
- if (t2 < tFar) tFar = t2
65
- }
66
- if (dy === 0) {
67
- if (oy < minY || oy > maxY) return -1
68
- } else {
69
- let inv = 1 / dy
70
- let t1 = (minY - oy) * inv
71
- let t2 = (maxY - oy) * inv
72
- if (t1 > t2) { let t = t1; t1 = t2; t2 = t }
73
- if (t1 > tNear) tNear = t1
74
- if (t2 < tFar) tFar = t2
75
- }
76
- if (dz === 0) {
77
- if (oz < minZ || oz > maxZ) return -1
78
- } else {
79
- let inv = 1 / dz
80
- let t1 = (minZ - oz) * inv
81
- let t2 = (maxZ - oz) * inv
82
- if (t1 > t2) { let t = t1; t1 = t2; t2 = t }
83
- if (t1 > tNear) tNear = t1
84
- if (t2 < tFar) tFar = t2
85
- }
86
- return tFar >= tNear ? tNear : -1
87
- }
88
-
89
- export function createBvh<T>(): Bvh<T> {
90
- // Node storage, 6 bounds floats per node; child1 === -1 marks a leaf.
91
- let bounds: number[] = []
92
- let parent: number[] = []
93
- let child1: number[] = []
94
- let child2: number[] = []
95
- let items: (T | undefined)[] = []
96
- let free: number[] = []
97
- let root = -1
98
- // Traversal stack, reused across queries.
99
- let stack: number[] = []
100
-
101
- let allocate = (): number => {
102
- let node = free.pop()
103
- if (node === undefined) {
104
- node = parent.length
105
- bounds.push(0, 0, 0, 0, 0, 0)
106
- parent.push(-1)
107
- child1.push(-1)
108
- child2.push(-1)
109
- items.push(undefined)
110
- }
111
- return node
112
- }
113
-
114
- // Half the surface area - the SAH cost of a node's box. Degenerate
115
- // (flat) boxes still cost via their other faces, which is what makes
116
- // the heuristic sane for planes.
117
- let area = (n: number): number => {
118
- let i = n * 6
119
- let w = bounds[i + 3]! - bounds[i]!
120
- let h = bounds[i + 4]! - bounds[i + 1]!
121
- let d = bounds[i + 5]! - bounds[i + 2]!
122
- return w * (h + d) + h * d
123
- }
124
-
125
- let unionArea = (n: number, minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number): number => {
126
- let i = n * 6
127
- let w = Math.max(bounds[i + 3]!, maxX) - Math.min(bounds[i]!, minX)
128
- let h = Math.max(bounds[i + 4]!, maxY) - Math.min(bounds[i + 1]!, minY)
129
- let d = Math.max(bounds[i + 5]!, maxZ) - Math.min(bounds[i + 2]!, minZ)
130
- return w * (h + d) + h * d
131
- }
132
-
133
- // Recompute an internal node's box as the union of its children.
134
- let refit = (n: number): void => {
135
- let a = child1[n]! * 6
136
- let b = child2[n]! * 6
137
- let i = n * 6
138
- bounds[i] = Math.min(bounds[a]!, bounds[b]!)
139
- bounds[i + 1] = Math.min(bounds[a + 1]!, bounds[b + 1]!)
140
- bounds[i + 2] = Math.min(bounds[a + 2]!, bounds[b + 2]!)
141
- bounds[i + 3] = Math.max(bounds[a + 3]!, bounds[b + 3]!)
142
- bounds[i + 4] = Math.max(bounds[a + 4]!, bounds[b + 4]!)
143
- bounds[i + 5] = Math.max(bounds[a + 5]!, bounds[b + 5]!)
144
- }
145
-
146
- let insertLeaf = (leaf: number): void => {
147
- if (root === -1) {
148
- root = leaf
149
- parent[leaf] = -1
150
- return
151
- }
152
- let i = leaf * 6
153
- let minX = bounds[i]!, minY = bounds[i + 1]!, minZ = bounds[i + 2]!
154
- let maxX = bounds[i + 3]!, maxY = bounds[i + 4]!, maxZ = bounds[i + 5]!
155
- // Descend toward the sibling that grows the least (the classic
156
- // incremental surface-area heuristic).
157
- let sibling = root
158
- while (child1[sibling] !== -1) {
159
- let a = child1[sibling]!
160
- let b = child2[sibling]!
161
- let costA = unionArea(a, minX, minY, minZ, maxX, maxY, maxZ) - area(a)
162
- let costB = unionArea(b, minX, minY, minZ, maxX, maxY, maxZ) - area(b)
163
- sibling = costA < costB ? a : b
164
- }
165
- let oldParent = parent[sibling]!
166
- let newParent = allocate()
167
- parent[newParent] = oldParent
168
- child1[newParent] = sibling
169
- child2[newParent] = leaf
170
- items[newParent] = undefined
171
- parent[sibling] = newParent
172
- parent[leaf] = newParent
173
- if (oldParent === -1) {
174
- root = newParent
175
- } else if (child1[oldParent] === sibling) {
176
- child1[oldParent] = newParent
177
- } else {
178
- child2[oldParent] = newParent
179
- }
180
- for (let n = newParent; n !== -1; n = parent[n]!) refit(n)
181
- }
182
-
183
- let removeLeaf = (leaf: number): void => {
184
- if (leaf === root) {
185
- root = -1
186
- return
187
- }
188
- let p = parent[leaf]!
189
- let sibling = child1[p] === leaf ? child2[p]! : child1[p]!
190
- let grand = parent[p]!
191
- parent[sibling] = grand
192
- if (grand === -1) {
193
- root = sibling
194
- } else {
195
- if (child1[grand] === p) child1[grand] = sibling
196
- else child2[grand] = sibling
197
- for (let n = grand; n !== -1; n = parent[n]!) refit(n)
198
- }
199
- child1[p] = -1
200
- items[p] = undefined
201
- free.push(p)
202
- }
203
-
204
- let setFat = (leaf: number, minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number): void => {
205
- let m = MARGIN * Math.max(maxX - minX, maxY - minY, maxZ - minZ)
206
- let i = leaf * 6
207
- bounds[i] = minX - m
208
- bounds[i + 1] = minY - m
209
- bounds[i + 2] = minZ - m
210
- bounds[i + 3] = maxX + m
211
- bounds[i + 4] = maxY + m
212
- bounds[i + 5] = maxZ + m
213
- }
214
-
215
- return {
216
- insert(item, minX, minY, minZ, maxX, maxY, maxZ) {
217
- let leaf = allocate()
218
- items[leaf] = item
219
- setFat(leaf, minX, minY, minZ, maxX, maxY, maxZ)
220
- insertLeaf(leaf)
221
- return leaf
222
- },
223
- update(leaf, minX, minY, minZ, maxX, maxY, maxZ) {
224
- let i = leaf * 6
225
- if (
226
- bounds[i]! <= minX && bounds[i + 1]! <= minY && bounds[i + 2]! <= minZ &&
227
- bounds[i + 3]! >= maxX && bounds[i + 4]! >= maxY && bounds[i + 5]! >= maxZ
228
- ) {
229
- return false
230
- }
231
- removeLeaf(leaf)
232
- setFat(leaf, minX, minY, minZ, maxX, maxY, maxZ)
233
- insertLeaf(leaf)
234
- return true
235
- },
236
- remove(leaf) {
237
- removeLeaf(leaf)
238
- items[leaf] = undefined
239
- free.push(leaf)
240
- },
241
- raycast(ox, oy, oz, dx, dy, dz, visit) {
242
- if (root === -1) return
243
- stack.length = 0
244
- stack.push(root)
245
- while (stack.length > 0) {
246
- let n = stack.pop()!
247
- let i = n * 6
248
- let t = rayBoxDistance(ox, oy, oz, dx, dy, dz, bounds[i]!, bounds[i + 1]!, bounds[i + 2]!, bounds[i + 3]!, bounds[i + 4]!, bounds[i + 5]!)
249
- if (t < 0) continue
250
- if (child1[n] === -1) {
251
- visit(items[n] as T)
252
- } else {
253
- stack.push(child1[n]!, child2[n]!)
254
- }
255
- }
256
- },
257
- }
258
- }