@driftengine/core 4.5.0 → 4.6.1

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.
Files changed (209) hide show
  1. package/CHANGELOG.json +62 -0
  2. package/README.md +1 -1
  3. package/dist/geometry/boxCsg.d.ts +28 -0
  4. package/dist/geometry/boxCsg.js +139 -0
  5. package/dist/geometry/csg.d.ts +36 -0
  6. package/dist/geometry/csg.js +329 -0
  7. package/dist/geometry/meshBuilder.js +13 -0
  8. package/dist/geometry/meshConcat.js +1 -0
  9. package/dist/geometry/pixelFont.js +2 -0
  10. package/dist/geometry/solid.d.ts +61 -0
  11. package/dist/geometry/solid.js +152 -0
  12. package/dist/geometry/solidFlat.d.ts +35 -0
  13. package/dist/geometry/solidFlat.js +428 -0
  14. package/dist/geometry/solidHarness.d.ts +19 -0
  15. package/dist/geometry/solidHarness.js +66 -0
  16. package/dist/geometry/solidNormals.d.ts +17 -0
  17. package/dist/geometry/solidNormals.js +86 -0
  18. package/dist/geometry/solidRound.d.ts +41 -0
  19. package/dist/geometry/solidRound.js +323 -0
  20. package/dist/geometry/solidWriter.d.ts +16 -0
  21. package/dist/geometry/solidWriter.js +42 -0
  22. package/dist/index.d.ts +23 -2
  23. package/dist/index.js +13 -1
  24. package/dist/render/backend/api.d.ts +17 -3
  25. package/dist/render/backend/drawFog.d.ts +26 -0
  26. package/dist/render/backend/drawFog.js +30 -0
  27. package/dist/render/backend/materialChanges.d.ts +2 -1
  28. package/dist/render/backend/materialChanges.js +6 -1
  29. package/dist/render/backend/webgl2/instanced.d.ts +24 -5
  30. package/dist/render/backend/webgl2/instanced.js +72 -15
  31. package/dist/render/backend/webgl2/renderer.d.ts +122 -8
  32. package/dist/render/backend/webgl2/renderer.js +249 -128
  33. package/dist/render/backend/webgpu/buffers.d.ts +17 -0
  34. package/dist/render/backend/webgpu/buffers.js +34 -3
  35. package/dist/render/backend/webgpu/depthPass.js +5 -1
  36. package/dist/render/backend/webgpu/drawCommand.d.ts +5 -0
  37. package/dist/render/backend/webgpu/drawCommand.js +1 -0
  38. package/dist/render/backend/webgpu/flatPass.d.ts +4 -1
  39. package/dist/render/backend/webgpu/flatPass.js +38 -9
  40. package/dist/render/backend/webgpu/glassTintPass.js +2 -1
  41. package/dist/render/backend/webgpu/instanceCullPass.d.ts +34 -0
  42. package/dist/render/backend/webgpu/instanceCullPass.js +144 -0
  43. package/dist/render/backend/webgpu/instanced.d.ts +11 -1
  44. package/dist/render/backend/webgpu/instanced.js +21 -3
  45. package/dist/render/backend/webgpu/latePass.d.ts +6 -2
  46. package/dist/render/backend/webgpu/latePass.js +11 -4
  47. package/dist/render/backend/webgpu/motionPass.js +2 -1
  48. package/dist/render/backend/webgpu/postPass.d.ts +1 -1
  49. package/dist/render/backend/webgpu/renderer.d.ts +100 -10
  50. package/dist/render/backend/webgpu/renderer.js +478 -214
  51. package/dist/render/backend/webgpu/sdfTextPass.js +6 -1
  52. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.d.ts +18 -0
  53. package/dist/render/backend/webgpu/shaders/instanceCull.wgsl.js +59 -0
  54. package/dist/render/backend/webgpu/surfaceTexturePass.d.ts +17 -3
  55. package/dist/render/backend/webgpu/surfaceTexturePass.js +77 -38
  56. package/dist/render/driftLight/bake.d.ts +7 -0
  57. package/dist/render/driftLight/bake.js +61 -56
  58. package/dist/render/driftLight/denseField.d.ts +46 -0
  59. package/dist/render/driftLight/denseField.js +109 -0
  60. package/dist/render/driftLight/lightField.d.ts +10 -5
  61. package/dist/render/driftLight/lightField.js +25 -19
  62. package/dist/render/driftLight/presence.d.ts +50 -0
  63. package/dist/render/driftLight/presence.js +43 -0
  64. package/dist/render/driftLight/uniforms.d.ts +3 -3
  65. package/dist/render/driftLight/uniforms.js +5 -6
  66. package/dist/render/driftLight/worldLightField.d.ts +41 -0
  67. package/dist/render/driftLight/worldLightField.js +66 -0
  68. package/dist/render/fixtureAtlas.d.ts +20 -0
  69. package/dist/render/fixtureAtlas.js +49 -0
  70. package/dist/render/instanceCull.d.ts +63 -0
  71. package/dist/render/instanceCull.js +160 -0
  72. package/dist/render/instances.d.ts +15 -0
  73. package/dist/render/lightBudget.d.ts +22 -33
  74. package/dist/render/lightBudget.js +24 -35
  75. package/dist/render/lightGrid.d.ts +42 -0
  76. package/dist/render/lightGrid.js +87 -0
  77. package/dist/render/mesh.d.ts +23 -24
  78. package/dist/render/mesh.js +56 -68
  79. package/dist/render/occlusion.d.ts +10 -0
  80. package/dist/render/occlusion.js +36 -7
  81. package/dist/render/pointLightSelection.d.ts +7 -0
  82. package/dist/render/pointLightSelection.js +29 -4
  83. package/dist/render/rainField.d.ts +13 -0
  84. package/dist/render/rainField.js +27 -5
  85. package/dist/render/recon/lateDepth.d.ts +11 -0
  86. package/dist/render/recon/lateDepth.js +70 -0
  87. package/dist/render/recon/resolve.d.ts +34 -4
  88. package/dist/render/recon/resolve.js +161 -20
  89. package/dist/render/rendererHarness.js +4 -0
  90. package/dist/render/sceneTarget.d.ts +2 -0
  91. package/dist/render/sceneTarget.js +2 -0
  92. package/dist/render/shaders/depth.js +12 -9
  93. package/dist/render/shaders/flat/driftLight.d.ts +1 -1
  94. package/dist/render/shaders/flat/driftLight.js +32 -7
  95. package/dist/render/shaders/flat/index.js +5 -3
  96. package/dist/render/shaders/flat/main.d.ts +1 -1
  97. package/dist/render/shaders/flat/main.js +75 -23
  98. package/dist/render/shaders/flat/preamble.js +75 -20
  99. package/dist/render/shaders/flat/surfaceEffects.d.ts +29 -0
  100. package/dist/render/shaders/flat/surfaceEffects.js +152 -0
  101. package/dist/render/shaders/generated/depth.wgsl.d.ts +8 -8
  102. package/dist/render/shaders/generated/depth.wgsl.js +8 -8
  103. package/dist/render/shaders/generated/flat.wgsl.d.ts +1320 -1320
  104. package/dist/render/shaders/generated/flat.wgsl.js +1359 -1345
  105. package/dist/render/shaders/generated/lightVolume.wgsl.js +18 -4
  106. package/dist/render/shaders/generated/rush.wgsl.d.ts +21 -16
  107. package/dist/render/shaders/generated/rush.wgsl.js +21 -16
  108. package/dist/render/shaders/generated/sdfText.wgsl.d.ts +2 -2
  109. package/dist/render/shaders/generated/sdfText.wgsl.js +2 -2
  110. package/dist/render/shaders/generated/text.wgsl.d.ts +1 -1
  111. package/dist/render/shaders/generated/text.wgsl.js +1 -1
  112. package/dist/render/shaders/gi/driftLight.wgsl.d.ts +1 -1
  113. package/dist/render/shaders/gi/driftLight.wgsl.js +27 -4
  114. package/dist/render/shaders/recon/lateDepth.wgsl.js +38 -13
  115. package/dist/render/shaders/recon/resolve.wgsl.d.ts +8 -7
  116. package/dist/render/shaders/recon/resolve.wgsl.js +161 -27
  117. package/dist/render/shaders/rush.js +11 -1
  118. package/dist/render/shaders/sdfText.d.ts +1 -1
  119. package/dist/render/shaders/sdfText.js +3 -2
  120. package/dist/render/shaders/text.d.ts +1 -1
  121. package/dist/render/shaders/text.js +18 -9
  122. package/dist/render/surfaceEffects.d.ts +83 -0
  123. package/dist/render/surfaceEffects.js +75 -0
  124. package/dist/render/surfaceScene.d.ts +8 -0
  125. package/dist/render/surfaceScene.js +8 -0
  126. package/dist/render/surfaceTexture.d.ts +18 -2
  127. package/dist/render/surfaceTexture.js +68 -15
  128. package/dist/render/temporalAa.d.ts +25 -6
  129. package/dist/render/temporalAa.js +45 -15
  130. package/dist/render/textureSource.d.ts +30 -0
  131. package/dist/render/textureSource.js +53 -0
  132. package/dist/render/vertexDefaults.js +2 -0
  133. package/dist/world/hlod.d.ts +104 -0
  134. package/dist/world/hlod.js +214 -0
  135. package/package.json +6 -6
  136. package/src/geometry/boxCsg.ts +186 -0
  137. package/src/geometry/csg.ts +384 -0
  138. package/src/geometry/meshBuilder.ts +15 -0
  139. package/src/geometry/meshConcat.ts +1 -0
  140. package/src/geometry/pixelFont.ts +2 -0
  141. package/src/geometry/solid.ts +189 -0
  142. package/src/geometry/solidFlat.ts +522 -0
  143. package/src/geometry/solidHarness.ts +83 -0
  144. package/src/geometry/solidNormals.ts +103 -0
  145. package/src/geometry/solidRound.ts +443 -0
  146. package/src/geometry/solidWriter.ts +58 -0
  147. package/src/index.ts +51 -1
  148. package/src/render/backend/api.ts +21 -2
  149. package/src/render/backend/drawFog.ts +31 -0
  150. package/src/render/backend/materialChanges.ts +9 -2
  151. package/src/render/backend/webgl2/instanced.ts +80 -14
  152. package/src/render/backend/webgl2/renderer.ts +311 -150
  153. package/src/render/backend/webgpu/buffers.ts +49 -3
  154. package/src/render/backend/webgpu/depthPass.ts +5 -1
  155. package/src/render/backend/webgpu/drawCommand.ts +6 -0
  156. package/src/render/backend/webgpu/flatPass.ts +41 -11
  157. package/src/render/backend/webgpu/glassTintPass.ts +2 -1
  158. package/src/render/backend/webgpu/instanceCullPass.ts +165 -0
  159. package/src/render/backend/webgpu/instanced.ts +21 -3
  160. package/src/render/backend/webgpu/latePass.ts +12 -3
  161. package/src/render/backend/webgpu/motionPass.ts +2 -1
  162. package/src/render/backend/webgpu/renderer.ts +556 -239
  163. package/src/render/backend/webgpu/sdfTextPass.ts +6 -1
  164. package/src/render/backend/webgpu/shaders/instanceCull.wgsl.ts +61 -0
  165. package/src/render/backend/webgpu/surfaceTexturePass.ts +90 -50
  166. package/src/render/driftLight/bake.ts +85 -62
  167. package/src/render/driftLight/denseField.ts +149 -0
  168. package/src/render/driftLight/lightField.ts +33 -18
  169. package/src/render/driftLight/presence.ts +69 -0
  170. package/src/render/driftLight/uniforms.ts +8 -9
  171. package/src/render/driftLight/worldLightField.ts +83 -0
  172. package/src/render/fixtureAtlas.ts +63 -0
  173. package/src/render/instanceCull.ts +205 -0
  174. package/src/render/instances.ts +16 -0
  175. package/src/render/lightBudget.ts +25 -36
  176. package/src/render/lightGrid.ts +130 -0
  177. package/src/render/mesh.ts +64 -72
  178. package/src/render/occlusion.ts +51 -7
  179. package/src/render/pointLightSelection.ts +72 -4
  180. package/src/render/rainField.ts +39 -5
  181. package/src/render/recon/lateDepth.ts +83 -0
  182. package/src/render/recon/resolve.ts +206 -20
  183. package/src/render/rendererHarness.ts +4 -0
  184. package/src/render/sceneTarget.ts +4 -0
  185. package/src/render/shaders/depth.ts +12 -9
  186. package/src/render/shaders/flat/driftLight.ts +32 -7
  187. package/src/render/shaders/flat/index.ts +5 -3
  188. package/src/render/shaders/flat/main.ts +75 -23
  189. package/src/render/shaders/flat/preamble.ts +75 -20
  190. package/src/render/shaders/flat/surfaceEffects.ts +152 -0
  191. package/src/render/shaders/generated/depth.wgsl.ts +8 -8
  192. package/src/render/shaders/generated/flat.wgsl.ts +1361 -1345
  193. package/src/render/shaders/generated/lightVolume.wgsl.ts +20 -4
  194. package/src/render/shaders/generated/rush.wgsl.ts +21 -16
  195. package/src/render/shaders/generated/sdfText.wgsl.ts +2 -2
  196. package/src/render/shaders/generated/text.wgsl.ts +1 -1
  197. package/src/render/shaders/gi/driftLight.wgsl.ts +27 -4
  198. package/src/render/shaders/recon/lateDepth.wgsl.ts +38 -13
  199. package/src/render/shaders/recon/resolve.wgsl.ts +161 -27
  200. package/src/render/shaders/rush.ts +11 -1
  201. package/src/render/shaders/sdfText.ts +3 -2
  202. package/src/render/shaders/text.ts +18 -9
  203. package/src/render/surfaceEffects.ts +132 -0
  204. package/src/render/surfaceScene.ts +16 -0
  205. package/src/render/surfaceTexture.ts +110 -19
  206. package/src/render/temporalAa.ts +78 -14
  207. package/src/render/textureSource.ts +72 -0
  208. package/src/render/vertexDefaults.ts +2 -0
  209. package/src/world/hlod.ts +290 -0
package/CHANGELOG.json CHANGED
@@ -1,4 +1,66 @@
1
1
  [
2
+ {
3
+ "version": "4.6.1",
4
+ "date": "2026-10-01",
5
+ "entries": [
6
+ {
7
+ "kind": "fixed",
8
+ "short": "A mesh with no triangles is skipped by every pass instead of drawn as nothing, so a game's console no longer fills with WebGPU warnings about empty draws.",
9
+ "text": "**A mesh with no triangles draws nothing, in every pass, on both backends.** A game's world can hold an empty mesh, for a level with none of that kind of geometry, and every pass it reached issued a draw of zero indices: WebGPU answers each with a warning, five on one game's first screen (the frame, its reflection, the sun's static and peeled maps and a lamp's shadow faces), and WebGL2 counted draws that drew nothing. `GpuMesh.indexCount` and `Mesh.indexCount` carry the rule, and every place a mesh enters a pass skips an empty one beside its other checks: meshes, translucent meshes, instanced batches, light volumes, shadow casters, glass depth and the motion pass. A game that already skipped its empty meshes itself can keep doing so."
10
+ }
11
+ ]
12
+ },
13
+ {
14
+ "version": "4.6.0",
15
+ "date": "2026-10-01",
16
+ "entries": [
17
+ {
18
+ "kind": "added",
19
+ "short": "A whole city streamed from one container: regions arrive near the eye first, each drawn at the detail its distance asks for, with its collision, navigation and entities.",
20
+ "text": "**Container 1.23 carries a world built from a kit.** An assembly is painted copies of kit pieces rather than its own vertices, a region (`REGN`) names the assemblies, instance groups, occluders and collision of one square of the world, and a world's finest level is paged: `writeDrft` writes the regions nearest a starting point first, and `texturesFirst` puts the pictures ahead of the geometry so every region can wear them as it lands. `buildKit`, `buildAssembly`, `expandAssembly` and `planRegions` are the format's halves, and a mesh's sway, sky and opacity channel now survives a bake.\n\n**`DrftLoader` hands a streamed world over a region at a time**, never merged into its parts, as `LoadedRegion`s with their levels, collision, navigation and entities; `onMesh` takes the meshes no region holds and `onImage` each picture as it decodes. `StaticRegions` collides a scene's scenery a region at a time. **`HlodSet` picks each region's level of detail by its geometric error on screen**, with a band no still eye can cross, and `setDitherFade` crossfades two levels with a screen-door dither on both backends.\n\nMeasured on a two-kilometre city of 484 regions and 18.8 million triangles at its finest: the 14 regions within 150 m of the start are whole after 13 MB of a 130 MB file."
21
+ },
22
+ {
23
+ "kind": "added",
24
+ "short": "Instancing that culls: a batch spread across a city is tested against the view and the occluders whole, then instance by instance on the GPU, and small batches are drawn whole.",
25
+ "text": "**`createInstanced(mesh, capacity, { cull: true })`** tests the whole batch against the view and the declared box occluders (`occludedBox`) on the CPU, then keeps each instance only where its sphere meets the view: as compute into an indirect draw on WebGPU, on the CPU on WebGL2. A scene may hold as many batches of one mesh as it has regions.\n\n**A batch too small to repay that is drawn whole** (`cullsInstances`, 16,384 indices). Chrome's GPU process validates each indirect draw at about 8 µs, so a per-instance cull of a few lamps cost more than drawing them: on a city street, 633 indirect draws a frame became 108 and the frame went from 19.5 to 10.8 ms, pixel-identical. Shadow, motion and blended draws still see every instance."
26
+ },
27
+ {
28
+ "kind": "added",
29
+ "short": "Texture arrays per face, and per-layer surface effects: rooms behind windows, windows lit by night, wear, scrolling and flickering signs, and rain: a whole block of facades in one draw.",
30
+ "text": "**`createSurfaceTextureArray(sources, { effects })`**: every surface texture is an array, and a mesh names which layer each face wears, so one merged mesh wearing many images is one draw. Each layer may carry a `SurfaceLayerEffect`: **rooms behind its windows** (interior mapping from a room layer in the same array), **windows lit by night** (`Environment.litWindows`, with `lateWindows` the share that stays lit latest, and the light itself carried per building on the vertices so a style shared by forty facades still lights each its own way), **wear** (dust, grime, streaks), **animation** (scroll, pulse, flicker, flipbook frames and a fade with distance, on the caller's `surfaceTime` so a held clock holds it) and **rain** (`Environment.wetness`, with `dry` for a covered surface).\n\nThe effects are a texel row per layer (`packSurfaceEffects`, `SURFACE_EFFECT_TEXELS`), the same on both backends."
31
+ },
32
+ {
33
+ "kind": "added",
34
+ "short": "Every lamp in a city lit: a world's lights summed offline into one dense volume read past the exact lights, and the exact ones chosen through a grid instead of a scan.",
35
+ "text": "**`bakeDenseField`** sums a whole world's fixed lights offline into one dense volume, sample for sample what the bricks hold, carried in the container as `LVOL` in runs of dark, solid and lit samples. **`WorldLightField`** (`createWorldLightField`) takes it whole and lights every surface past the exact lights on both backends, so a street three blocks away is lit by its own lamps without spending a shadow or a slot on them.\n\n**`createLightGrid` and `selectGridLights`** find the frame's exact lights through a grid, choosing exactly what the full scan chose field for field, so a city of seven thousand lamps picks its nearest without visiting them all. A tie for a shadow map now goes to the lamp listed first rather than to the way its distance rounded."
36
+ },
37
+ {
38
+ "kind": "added",
39
+ "short": "Solids and booleans: closed, outward solids from a box to a torus, transforms that keep normals true, and exact booleans that split only what the other operand reaches.",
40
+ "text": "**Solids in the barrel**: `solidBox`, `solidCylinder`, `solidSphere`, `solidTorus`, `solidLathe`, `solidExtrude`, `solidSweep` and the rest, each closed and wound outward, with `transformSolid` keeping normals true under any scale, `mergeSolids`, `smoothSolidNormals` and `solidToMesh`.\n\n**Booleans**: `solidUnion`, `solidSubtract` and `solidIntersect` through a BSP that splits only what the other operand reaches, and `solidBoxBoolean` exact on axis-aligned boxes, which is most of what a building is cut from."
41
+ },
42
+ {
43
+ "kind": "added",
44
+ "short": "A translucent draw can add its light instead of covering, and a glow lying on a surface can be reconstructed with it; added light now fades in fog instead of greying.",
45
+ "text": "**`TranslucentMeshOptions.additive`**: the surface's colour times its opacity is added and nothing is darkened, as a glow or a pool of light under a lamp is. Order does not matter to a sum, so an additive draw is never held for the order-independent pass. **Added light fades in the medium** (`uFogEnabled` at 2): mixed toward the fog's colour and then added, a glow put the haze into the frame twice, and every lamp's light cone drew by day as a grey solid, greyer the thicker the air.\n\n**`TranslucentMeshOptions.reconstructed`**: a translucent surface that moves with what it lies on, such as a glow on a wall or a pane in a window, is drawn into the picture the reconstruction resolves rather than after it, so it is resolved with the edge it lies against. A street's glows went from 34,757 pixels changing between still frames to 2,999 (2,470 with the reconstruction off)."
46
+ },
47
+ {
48
+ "kind": "fixed",
49
+ "short": "Still frames hold still under reconstruction: thin edges, rails and glows no longer shimmer, bloom halos and ambient occlusion no longer pulse with the jitter.",
50
+ "text": "**DriftTR keeps the temporal resolve's anti-flicker, over its own period.** Each output pixel keeps a record of its gathered luma, and once two periods running repeat while still its box widens by its own spread and it keeps more history; the width follows the period (`flickerRule`), since eighteen phases wear a spread down further between catches than eight. **A still pixel whose depth has been seen to swing is not a disocclusion**: a held peak of how far last frame's depth missed widens its depth tolerance, so a rail finer than a texel no longer drops its history every period. **The camera's motion goes through one reprojection matrix multiplied in double precision**: the single-precision round trip through a world position read a few hundredths of a pixel of motion on a still camera, crept every still history and kept every pixel from counting as still. A still city view: 1,156 pixels changing between frames to 355 (184 native).\n\n**Glows and panes after the upscale** are tested against each render texel's surface carried to the pixel's centre, not the nearest of four, which let a glow along an edge through on one frame in two. **A reconstruction blooms the resolved picture**, not the jittered render, whose sub-texel bright points pulsed their halos. **The composite reads ambient occlusion where the frame's jitter put it**, under the temporal resolve and the reconstruction alike. **An interface drawn after the present is not jittered and opens no temporal frame**: every consumer drawing its HUD after `endFrame` had half the temporal resolve's jitter sequence and no anti-flicker at all."
51
+ },
52
+ {
53
+ "kind": "changed",
54
+ "short": "The generated WGSL stores each shared function once: core's WebGPU shaders are 185 KB smaller.",
55
+ "text": "**Each shared item of the generated WGSL is stored once** and renumbered per function, where every permutation carried its own copy: the lit-shader helpers cost once, and core's WebGPU shader source is 185 KB smaller. Nothing about what a device compiles changes."
56
+ },
57
+ {
58
+ "kind": "fixed",
59
+ "short": "Smaller fixes: one-cell pixel-font strokes, a WebGPU inset after the present, materials on instanced batches, a kerb a character walks off, and no rain bars across the frame.",
60
+ "text": "- **The pixel font's strokes are one cell wide wherever the line is drawn**, and its brackets are whole; text under a perspective tilt divides about each cell's centre.\n- **An inset opened after the present on WebGPU** draws, as it did on WebGL2.\n- **A material setter reaches every WebGL2 flat program**, so an instanced batch wears what its caller set rather than the defaults.\n- **A material with no maps leaves an array stand-in on every WebGL2 surface unit**, and sprites borrow the last unit of the pool, not one the lit pass binds.\n- **A character stands on a triangle mesh's kerb and walks on**, and a round body near a mesh edge reports its real gap.\n- **A raindrop passing the eye is not drawn** (`RainFieldOptions.clearM`, a metre by default): a streak is a world-space width, so a drop 30 cm from the camera drew fourteen pixels thick and as long as the frame. It keeps falling, as a sheltered drop does; pass 0 to draw drops in macro."
61
+ }
62
+ ]
63
+ },
2
64
  {
3
65
  "version": "4.5.0",
4
66
  "date": "2026-09-29",
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The runtime everything needs: renderer, loop, camera, input, geometry. Collision and dynamics live in [`@driftengine/physics`](../physics/README.md), which this package depends on and re-exports whole, so reaching for them through this barrel works as it always did.
4
4
 
5
- **Cost: 887.4 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
5
+ **Cost: 699.0 KB gzipped, importing `createRenderer`.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
6
6
  3% — the number is a fact about the build rather than a claim in a document.
7
7
 
8
8
  Part of [DriftEngine](../../README.md). See [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md)
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Booleans on axis-aligned boxes, exactly and with as few faces as the result has.
3
+ *
4
+ * **Why beside `csg.ts` rather than in it.** A BSP boolean splits every face along every plane that
5
+ * crosses it, unrelated cutters included, and never re-merges: a wall of seven boxes with twenty
6
+ * window openings cut out of it came back as 325,187 triangles and took a minute. Boxes that share
7
+ * their axes need none of that. Every face of the result lies on a plane some box already has, so
8
+ * the space splits into the grid of all their coordinates, each cell is in or out by applying the
9
+ * operations to its centre in order, and the surface is the faces between cells that differ —
10
+ * merged greedily into the largest rectangles that fit. The same wall is a few hundred triangles,
11
+ * in milliseconds.
12
+ *
13
+ * **What it gives up.** Only boxes, only on the three axes: a box turned off them is the BSP's.
14
+ * Merged rectangles meet with T-junctions, as the BSP's pieces do, so the result is closed as a
15
+ * surface — its volume exact — but not edge for edge. Texture coordinates run 0..1 across the
16
+ * result's own extent on each face's plane, which is what `solidBox` gives a single box. The grid
17
+ * costs a byte a cell, so a caller with hundreds of boxes should check `boxBooleanCells` first.
18
+ */
19
+ import type { Solid } from './solid.ts';
20
+ export interface BoxOperation {
21
+ /** Minimum and maximum corners: `[x0, y0, z0, x1, y1, z1]`. */
22
+ readonly box: readonly [number, number, number, number, number, number];
23
+ readonly op: 'union' | 'subtract' | 'intersect';
24
+ }
25
+ /** How many grid cells `solidBoxBoolean` would allocate for `operations`. */
26
+ export declare function boxBooleanCells(operations: readonly BoxOperation[]): number;
27
+ /** The solid the operations make, applied left to right from nothing. */
28
+ export declare function solidBoxBoolean(operations: readonly BoxOperation[]): Solid;
@@ -0,0 +1,139 @@
1
+ import { emptySolid } from './solid.js';
2
+ import { SolidWriter } from './solidWriter.js';
3
+ /** Coordinates within this distance are one. */
4
+ const SAME = 1e-6;
5
+ function axis(operations, a) {
6
+ const values = operations
7
+ .flatMap((o) => [o.box[a], o.box[a + 3]])
8
+ .sort((p, q) => p - q);
9
+ const out = [];
10
+ for (const v of values)
11
+ if (out.length === 0 || v - out[out.length - 1] > SAME)
12
+ out.push(v);
13
+ return out;
14
+ }
15
+ /** How many grid cells `solidBoxBoolean` would allocate for `operations`. */
16
+ export function boxBooleanCells(operations) {
17
+ return ((axis(operations, 0).length - 1) *
18
+ (axis(operations, 1).length - 1) *
19
+ (axis(operations, 2).length - 1));
20
+ }
21
+ /** The solid the operations make, applied left to right from nothing. */
22
+ export function solidBoxBoolean(operations) {
23
+ if (operations.length === 0)
24
+ return emptySolid();
25
+ const xs = axis(operations, 0);
26
+ const ys = axis(operations, 1);
27
+ const zs = axis(operations, 2);
28
+ const nx = xs.length - 1;
29
+ const ny = ys.length - 1;
30
+ const nz = zs.length - 1;
31
+ if (nx <= 0 || ny <= 0 || nz <= 0)
32
+ return emptySolid();
33
+ const inside = new Uint8Array(nx * ny * nz);
34
+ const at = (i, j, k) => (k * ny + j) * nx + i;
35
+ for (let k = 0; k < nz; k += 1) {
36
+ const cz = (zs[k] + zs[k + 1]) / 2;
37
+ for (let j = 0; j < ny; j += 1) {
38
+ const cy = (ys[j] + ys[j + 1]) / 2;
39
+ for (let i = 0; i < nx; i += 1) {
40
+ const cx = (xs[i] + xs[i + 1]) / 2;
41
+ let state = false;
42
+ for (const o of operations) {
43
+ const b = o.box;
44
+ const hit = cx > b[0] && cx < b[3] && cy > b[1] && cy < b[4] && cz > b[2] && cz < b[5];
45
+ if (o.op === 'union')
46
+ state ||= hit;
47
+ else if (o.op === 'subtract')
48
+ state &&= !hit;
49
+ else
50
+ state &&= hit;
51
+ }
52
+ inside[at(i, j, k)] = state ? 1 : 0;
53
+ }
54
+ }
55
+ }
56
+ const filled = (i, j, k) => i >= 0 && j >= 0 && k >= 0 && i < nx && j < ny && k < nz && inside[at(i, j, k)] === 1;
57
+ const out = new SolidWriter();
58
+ const coords = [xs, ys, zs];
59
+ const counts = [nx, ny, nz];
60
+ const lo = [xs[0], ys[0], zs[0]];
61
+ const span = [
62
+ xs[nx] - lo[0],
63
+ ys[ny] - lo[1],
64
+ zs[nz] - lo[2],
65
+ ];
66
+ for (let d = 0; d < 3; d += 1) {
67
+ /* The face plane's two in-plane axes, ordered so (u × v) is +d. */
68
+ const u = (d + 1) % 3;
69
+ const v = (d + 2) % 3;
70
+ const nu = counts[u];
71
+ const nv = counts[v];
72
+ for (let s = 0; s <= counts[d]; s += 1) {
73
+ for (const sign of [1, -1]) {
74
+ /* Faces on plane s facing `sign`: the cell behind is in, the one in front is out. */
75
+ const mask = new Uint8Array(nu * nv);
76
+ for (let b = 0; b < nv; b += 1) {
77
+ for (let a = 0; a < nu; a += 1) {
78
+ const cell = [0, 0, 0];
79
+ cell[u] = a;
80
+ cell[v] = b;
81
+ cell[d] = sign > 0 ? s - 1 : s;
82
+ const front = [...cell];
83
+ front[d] = sign > 0 ? s : s - 1;
84
+ const inCell = filled(cell[0], cell[1], cell[2]);
85
+ const inFront = filled(front[0], front[1], front[2]);
86
+ if (inCell && !inFront)
87
+ mask[b * nu + a] = 1;
88
+ }
89
+ }
90
+ for (let b = 0; b < nv; b += 1) {
91
+ for (let a = 0; a < nu; a += 1) {
92
+ if (mask[b * nu + a] !== 1)
93
+ continue;
94
+ let a1 = a + 1;
95
+ while (a1 < nu && mask[b * nu + a1] === 1)
96
+ a1 += 1;
97
+ let b1 = b + 1;
98
+ grow: while (b1 < nv) {
99
+ for (let x = a; x < a1; x += 1)
100
+ if (mask[b1 * nu + x] !== 1)
101
+ break grow;
102
+ b1 += 1;
103
+ }
104
+ for (let y = b; y < b1; y += 1)
105
+ for (let x = a; x < a1; x += 1)
106
+ mask[y * nu + x] = 0;
107
+ quad(out, d, u, v, sign, coords[d][s], [
108
+ coords[u][a],
109
+ coords[u][a1],
110
+ coords[v][b],
111
+ coords[v][b1],
112
+ ], lo, span);
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
118
+ return out.finish();
119
+ }
120
+ function quad(out, d, u, v, sign, plane, [u0, u1, v0, v1], lo, span) {
121
+ const corner = (cu, cv) => {
122
+ const p = [0, 0, 0];
123
+ p[d] = plane;
124
+ p[u] = cu;
125
+ p[v] = cv;
126
+ const n = [0, 0, 0];
127
+ n[d] = sign;
128
+ return out.vertex(p[0], p[1], p[2], n[0], n[1], n[2], (cu - lo[u]) / (span[u] || 1), (cv - lo[v]) / (span[v] || 1));
129
+ };
130
+ const a = corner(u0, v0);
131
+ const b = corner(u1, v0);
132
+ const c = corner(u1, v1);
133
+ const e = corner(u0, v1);
134
+ /* Counter-clockwise from outside: (u × v) is +d, so a facing +d winds a b c, a facing −d reverses. */
135
+ if (sign > 0)
136
+ out.quad(a, b, c, e);
137
+ else
138
+ out.quad(a, e, c, b);
139
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Boolean operations on closed solids — union, subtraction, intersection — by binary space
3
+ * partitioning.
4
+ *
5
+ * The algorithm is the one Evan Wallace's csg.js made well known, implemented afresh here — named
6
+ * at the point it is used, as `CREDITS.md` asks of a published method: each operand becomes a BSP tree of its own polygons, each tree clips the
7
+ * other's polygons to the region it keeps, and the survivors are rebuilt into one tree. Polygons
8
+ * carry position, normal and texture coordinates, interpolated where a plane splits them, so a cut
9
+ * face keeps the texture of the face it was cut from.
10
+ *
11
+ * **Only what the other operand reaches is split.** Each tree is used for its planes alone: an
12
+ * operand's own polygons are clipped as they arrived, never as its tree's construction cut them,
13
+ * and a polygon whose bounds clear the other operand's is passed through whole — kept or dropped
14
+ * as the operation says of everything outside that operand. The result is the two clipped lists
15
+ * side by side, where the published method rebuilds one tree from them and splits the second list
16
+ * along every plane of the first on the way. Neither changes the surface; together they are what
17
+ * kept a wall cut five times with its end faces at two triangles each instead of eight, and a
18
+ * city's worth of cut towers out of the millions.
19
+ *
20
+ * **What it gives up.** A polygon the other operand does reach is still split along every plane of
21
+ * its tree that crosses it, and nothing is re-merged, so a heavily cut solid grows, and the pieces
22
+ * meet with T-junctions: closed as a surface — its volume exact — but not edge for edge. Welding
23
+ * and simplification are the caller's afterwards. The tree is built iteratively, so a large
24
+ * operand cannot overflow the stack; clipping recurses to the tree's depth, which a solid of a few
25
+ * thousand faces keeps shallow.
26
+ *
27
+ * **Empty operands are answered before any tree is built**, because clipping against an empty tree
28
+ * keeps everything — which is the right answer for a union and the wrong one for an intersection.
29
+ */
30
+ import type { Solid } from './solid.ts';
31
+ /** Everything inside either solid. */
32
+ export declare function solidUnion(a: Solid, b: Solid): Solid;
33
+ /** Everything inside `a` and outside `b`. */
34
+ export declare function solidSubtract(a: Solid, b: Solid): Solid;
35
+ /** Everything inside both solids. */
36
+ export declare function solidIntersect(a: Solid, b: Solid): Solid;
@@ -0,0 +1,329 @@
1
+ import { emptySolid } from './solid.js';
2
+ import { SolidWriter } from './solidWriter.js';
3
+ /** Distance within which a point counts as on a plane. */
4
+ const EPSILON = 1e-5;
5
+ const COPLANAR = 0;
6
+ const FRONT = 1;
7
+ const BACK = 2;
8
+ const SPANNING = 3;
9
+ function planeFrom(a, b, c) {
10
+ const ux = b.x - a.x;
11
+ const uy = b.y - a.y;
12
+ const uz = b.z - a.z;
13
+ const vx = c.x - a.x;
14
+ const vy = c.y - a.y;
15
+ const vz = c.z - a.z;
16
+ let nx = uy * vz - uz * vy;
17
+ let ny = uz * vx - ux * vz;
18
+ let nz = ux * vy - uy * vx;
19
+ const len = Math.hypot(nx, ny, nz);
20
+ if (len < 1e-12)
21
+ return null;
22
+ nx /= len;
23
+ ny /= len;
24
+ nz /= len;
25
+ return { nx, ny, nz, w: nx * a.x + ny * a.y + nz * a.z };
26
+ }
27
+ function flipPolygon(p) {
28
+ const vertices = p.vertices
29
+ .slice()
30
+ .reverse()
31
+ .map((v) => ({ ...v, nx: -v.nx, ny: -v.ny, nz: -v.nz }));
32
+ return { vertices, plane: { nx: -p.plane.nx, ny: -p.plane.ny, nz: -p.plane.nz, w: -p.plane.w } };
33
+ }
34
+ function lerp(a, b, t) {
35
+ let nx = a.nx + (b.nx - a.nx) * t;
36
+ let ny = a.ny + (b.ny - a.ny) * t;
37
+ let nz = a.nz + (b.nz - a.nz) * t;
38
+ const len = Math.hypot(nx, ny, nz) || 1;
39
+ nx /= len;
40
+ ny /= len;
41
+ nz /= len;
42
+ return {
43
+ x: a.x + (b.x - a.x) * t,
44
+ y: a.y + (b.y - a.y) * t,
45
+ z: a.z + (b.z - a.z) * t,
46
+ nx,
47
+ ny,
48
+ nz,
49
+ u: a.u + (b.u - a.u) * t,
50
+ v: a.v + (b.v - a.v) * t,
51
+ };
52
+ }
53
+ /**
54
+ * Sort `polygon` against `plane` into the four lists, splitting it where it spans. A coplanar
55
+ * polygon goes to the front list when it faces the same way as the plane, the back one otherwise.
56
+ */
57
+ function split(plane, polygon, coplanarFront, coplanarBack, front, back) {
58
+ let kind = 0;
59
+ const types = [];
60
+ for (const v of polygon.vertices) {
61
+ const t = plane.nx * v.x + plane.ny * v.y + plane.nz * v.z - plane.w;
62
+ const type = t < -EPSILON ? BACK : t > EPSILON ? FRONT : COPLANAR;
63
+ kind |= type;
64
+ types.push(type);
65
+ }
66
+ if (kind === COPLANAR) {
67
+ const facing = plane.nx * polygon.plane.nx + plane.ny * polygon.plane.ny + plane.nz * polygon.plane.nz;
68
+ (facing > 0 ? coplanarFront : coplanarBack).push(polygon);
69
+ }
70
+ else if (kind === FRONT) {
71
+ front.push(polygon);
72
+ }
73
+ else if (kind === BACK) {
74
+ back.push(polygon);
75
+ }
76
+ else if (kind === SPANNING) {
77
+ const f = [];
78
+ const b = [];
79
+ const n = polygon.vertices.length;
80
+ for (let i = 0; i < n; i++) {
81
+ const j = (i + 1) % n;
82
+ const ti = types[i] ?? COPLANAR;
83
+ const tj = types[j] ?? COPLANAR;
84
+ const vi = polygon.vertices[i];
85
+ const vj = polygon.vertices[j];
86
+ if (!vi || !vj)
87
+ continue;
88
+ if (ti !== BACK)
89
+ f.push(vi);
90
+ if (ti !== FRONT)
91
+ b.push(vi);
92
+ if ((ti | tj) === SPANNING) {
93
+ const di = plane.nx * vi.x + plane.ny * vi.y + plane.nz * vi.z;
94
+ const dj = plane.nx * vj.x + plane.ny * vj.y + plane.nz * vj.z;
95
+ const t = (plane.w - di) / (dj - di);
96
+ const mid = lerp(vi, vj, t);
97
+ f.push(mid);
98
+ b.push({ ...mid });
99
+ }
100
+ }
101
+ if (f.length >= 3)
102
+ front.push({ vertices: f, plane: polygon.plane });
103
+ if (b.length >= 3)
104
+ back.push({ vertices: b, plane: polygon.plane });
105
+ }
106
+ }
107
+ /** A BSP tree of an operand's planes. Its polygons only choose the planes and are not kept. */
108
+ class Node {
109
+ plane = null;
110
+ front = null;
111
+ back = null;
112
+ /** Add `polygons`' planes to the tree, splitting down it, without recursion. */
113
+ build(polygons) {
114
+ const work = [[this, polygons]];
115
+ const coplanar = [];
116
+ while (work.length > 0) {
117
+ const item = work.pop();
118
+ if (!item)
119
+ break;
120
+ const [node, list] = item;
121
+ if (list.length === 0)
122
+ continue;
123
+ const first = list[0];
124
+ if (!node.plane && first)
125
+ node.plane = { ...first.plane };
126
+ const plane = node.plane;
127
+ if (!plane)
128
+ continue;
129
+ const f = [];
130
+ const b = [];
131
+ for (const p of list)
132
+ split(plane, p, coplanar, coplanar, f, b);
133
+ coplanar.length = 0;
134
+ if (f.length > 0) {
135
+ node.front ??= new Node();
136
+ work.push([node.front, f]);
137
+ }
138
+ if (b.length > 0) {
139
+ node.back ??= new Node();
140
+ work.push([node.back, b]);
141
+ }
142
+ }
143
+ }
144
+ /** Turn the tree inside out: every plane reversed, front and back swapped. */
145
+ invert() {
146
+ const stack = [this];
147
+ while (stack.length > 0) {
148
+ const node = stack.pop();
149
+ if (!node)
150
+ break;
151
+ if (node.plane) {
152
+ node.plane = {
153
+ nx: -node.plane.nx,
154
+ ny: -node.plane.ny,
155
+ nz: -node.plane.nz,
156
+ w: -node.plane.w,
157
+ };
158
+ }
159
+ const f = node.front;
160
+ node.front = node.back;
161
+ node.back = f;
162
+ if (node.front)
163
+ stack.push(node.front);
164
+ if (node.back)
165
+ stack.push(node.back);
166
+ }
167
+ return this;
168
+ }
169
+ /** The parts of `polygons` outside this tree's solid. */
170
+ clipPolygons(polygons) {
171
+ if (!this.plane)
172
+ return polygons.slice();
173
+ let f = [];
174
+ let b = [];
175
+ for (const p of polygons)
176
+ split(this.plane, p, f, b, f, b);
177
+ if (this.front)
178
+ f = this.front.clipPolygons(f);
179
+ b = this.back ? this.back.clipPolygons(b) : [];
180
+ return f.concat(b);
181
+ }
182
+ }
183
+ function boundsOf(polygons) {
184
+ const min = [Infinity, Infinity, Infinity];
185
+ const max = [-Infinity, -Infinity, -Infinity];
186
+ for (const p of polygons) {
187
+ for (const v of p.vertices) {
188
+ if (v.x < min[0])
189
+ min[0] = v.x;
190
+ if (v.y < min[1])
191
+ min[1] = v.y;
192
+ if (v.z < min[2])
193
+ min[2] = v.z;
194
+ if (v.x > max[0])
195
+ max[0] = v.x;
196
+ if (v.y > max[1])
197
+ max[1] = v.y;
198
+ if (v.z > max[2])
199
+ max[2] = v.z;
200
+ }
201
+ }
202
+ return { min, max };
203
+ }
204
+ /**
205
+ * `polygons` split into those that may meet `bounds` and those clear of it by more than the plane
206
+ * tolerance on some axis — which are outside the solid `bounds` encloses, with no tree to ask.
207
+ */
208
+ function partition(polygons, bounds) {
209
+ const near = [];
210
+ const clear = [];
211
+ const { min, max } = bounds;
212
+ for (const p of polygons) {
213
+ let lx = Infinity;
214
+ let ly = Infinity;
215
+ let lz = Infinity;
216
+ let hx = -Infinity;
217
+ let hy = -Infinity;
218
+ let hz = -Infinity;
219
+ for (const v of p.vertices) {
220
+ lx = Math.min(lx, v.x);
221
+ ly = Math.min(ly, v.y);
222
+ lz = Math.min(lz, v.z);
223
+ hx = Math.max(hx, v.x);
224
+ hy = Math.max(hy, v.y);
225
+ hz = Math.max(hz, v.z);
226
+ }
227
+ const apart = hx < min[0] - EPSILON ||
228
+ hy < min[1] - EPSILON ||
229
+ hz < min[2] - EPSILON ||
230
+ lx > max[0] + EPSILON ||
231
+ ly > max[1] + EPSILON ||
232
+ lz > max[2] + EPSILON;
233
+ (apart ? clear : near).push(p);
234
+ }
235
+ return [near, clear];
236
+ }
237
+ const flipAll = (polygons) => polygons.map(flipPolygon);
238
+ function toPolygons(solid) {
239
+ const out = [];
240
+ const p = solid.positions;
241
+ const n = solid.normals;
242
+ const uv = solid.uvs;
243
+ const vertex = (i) => ({
244
+ x: p[i * 3] ?? 0,
245
+ y: p[i * 3 + 1] ?? 0,
246
+ z: p[i * 3 + 2] ?? 0,
247
+ nx: n[i * 3] ?? 0,
248
+ ny: n[i * 3 + 1] ?? 0,
249
+ nz: n[i * 3 + 2] ?? 0,
250
+ u: uv[i * 2] ?? 0,
251
+ v: uv[i * 2 + 1] ?? 0,
252
+ });
253
+ for (let t = 0; t < solid.indices.length; t += 3) {
254
+ const a = vertex(solid.indices[t] ?? 0);
255
+ const b = vertex(solid.indices[t + 1] ?? 0);
256
+ const c = vertex(solid.indices[t + 2] ?? 0);
257
+ const plane = planeFrom(a, b, c);
258
+ if (plane)
259
+ out.push({ vertices: [a, b, c], plane });
260
+ }
261
+ return out;
262
+ }
263
+ /** Polygons back to a solid, each fanned from its first corner — they are convex. */
264
+ function fromPolygons(polygons) {
265
+ const out = new SolidWriter();
266
+ for (const polygon of polygons) {
267
+ const base = out.vertexCount;
268
+ for (const v of polygon.vertices)
269
+ out.vertex(v.x, v.y, v.z, v.nx, v.ny, v.nz, v.u, v.v);
270
+ for (let i = 1; i + 1 < polygon.vertices.length; i++)
271
+ out.triangle(base, base + i, base + i + 1);
272
+ }
273
+ return out.finish();
274
+ }
275
+ function tree(polygons) {
276
+ const node = new Node();
277
+ node.build(polygons);
278
+ return node;
279
+ }
280
+ const isEmpty = (s) => s.indices.length === 0;
281
+ /**
282
+ * Both operands' polygons, each split by the other's bounds. The three operations below are the
283
+ * published method's steps written over these lists, with the clear polygons settled by the rule
284
+ * each operation applies to everything outside the other operand.
285
+ */
286
+ function operands(a, b) {
287
+ const pa = toPolygons(a);
288
+ const pb = toPolygons(b);
289
+ const [aNear, aClear] = partition(pa, boundsOf(pb));
290
+ const [bNear, bClear] = partition(pb, boundsOf(pa));
291
+ return { pa, pb, aNear, aClear, bNear, bClear };
292
+ }
293
+ /** Everything inside either solid. */
294
+ export function solidUnion(a, b) {
295
+ if (isEmpty(b))
296
+ return a;
297
+ if (isEmpty(a))
298
+ return b;
299
+ const { pa, pb, aNear, aClear, bNear, bClear } = operands(a, b);
300
+ const ta = tree(pa);
301
+ const aKept = tree(pb).clipPolygons(aNear);
302
+ /* B outside A, then without the faces lying on A's facing the same way, which A already has. */
303
+ const bKept = flipAll(ta.clipPolygons(flipAll(ta.clipPolygons(bNear))));
304
+ return fromPolygons([...aClear, ...aKept, ...bClear, ...bKept]);
305
+ }
306
+ /** Everything inside `a` and outside `b`. */
307
+ export function solidSubtract(a, b) {
308
+ if (isEmpty(a))
309
+ return emptySolid();
310
+ if (isEmpty(b))
311
+ return a;
312
+ const { pa, pb, aNear, aClear, bNear } = operands(a, b);
313
+ const outside = tree(pa).invert();
314
+ const aKept = flipAll(tree(pb).clipPolygons(flipAll(aNear)));
315
+ /* B inside A, turned to face into the cavity it leaves. */
316
+ const bKept = outside.clipPolygons(flipAll(outside.clipPolygons(bNear)));
317
+ return fromPolygons([...aClear, ...aKept, ...bKept]);
318
+ }
319
+ /** Everything inside both solids. */
320
+ export function solidIntersect(a, b) {
321
+ if (isEmpty(a) || isEmpty(b))
322
+ return emptySolid();
323
+ const { pa, pb, aNear, bNear } = operands(a, b);
324
+ const outsideA = tree(pa).invert();
325
+ const outsideB = tree(pb).invert();
326
+ const aKept = flipAll(outsideB.clipPolygons(flipAll(aNear)));
327
+ const bKept = flipAll(outsideA.clipPolygons(flipAll(outsideA.clipPolygons(bNear))));
328
+ return fromPolygons([...aKept, ...bKept]);
329
+ }
@@ -109,6 +109,7 @@ export class MeshBuilder {
109
109
  * inverse transpose, and nothing here has wanted that yet.
110
110
  */
111
111
  addMesh(mesh, x, y, z, scale = 1) {
112
+ refuseLayers(mesh);
112
113
  const base = this.vertexCount;
113
114
  const count = mesh.positions.length / 3;
114
115
  for (let i = 0; i < count; i++) {
@@ -157,6 +158,7 @@ export class MeshBuilder {
157
158
  * @param scale uniform, applied in the local frame before the basis.
158
159
  */
159
160
  addOrientedMesh(mesh, origin, right, up, forward, scale = 1) {
161
+ refuseLayers(mesh);
160
162
  assertOrthonormal(right, up, forward);
161
163
  const base = this.vertexCount;
162
164
  const count = mesh.positions.length / 3;
@@ -1186,3 +1188,14 @@ const CORNERS = [
1186
1188
  [1, 1],
1187
1189
  [-1, 1],
1188
1190
  ];
1191
+ /**
1192
+ * A builder has no layer channel, so merging a layered mesh would keep its texture coordinates and
1193
+ * silently put every face on layer 0 — the first image of the array, which reads as the wrong
1194
+ * facade everywhere rather than as an error. Refused instead; `concatMeshes` joins layered meshes.
1195
+ */
1196
+ function refuseLayers(mesh) {
1197
+ if (mesh.layers === undefined)
1198
+ return;
1199
+ throw new Error('MeshBuilder: this mesh names texture-array layers, which a builder does not carry — merging ' +
1200
+ 'it here would put every face on layer 0. Join layered meshes with concatMeshes instead.');
1201
+ }
@@ -26,6 +26,7 @@ const OPTIONAL = [
26
26
  ['channel', 4],
27
27
  ['joints', 4],
28
28
  ['weights', 4],
29
+ ['layers', 1],
29
30
  ];
30
31
  /**
31
32
  * `mesh` scaled uniformly and then moved by `x, y, z`. The same object when the fit is the
@@ -66,6 +66,8 @@ const GLYPHS = {
66
66
  "'": [0b00100, 0b00100, 0, 0, 0, 0, 0],
67
67
  '(': [0b00010, 0b00100, 0b01000, 0b01000, 0b01000, 0b00100, 0b00010],
68
68
  ')': [0b01000, 0b00100, 0b00010, 0b00010, 0b00010, 0b00100, 0b01000],
69
+ '[': [0b01110, 0b01000, 0b01000, 0b01000, 0b01000, 0b01000, 0b01110],
70
+ ']': [0b01110, 0b00010, 0b00010, 0b00010, 0b00010, 0b00010, 0b01110],
69
71
  '%': [0b11001, 0b11010, 0b00010, 0b00100, 0b01000, 0b01011, 0b10011],
70
72
  '·': [0, 0, 0, 0b01100, 0b01100, 0, 0],
71
73
  '×': [0, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0],