@expofp/renderer 0.0.0-experimental.2d6c681

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 (280) hide show
  1. package/README.md +145 -0
  2. package/dist/Renderer.d.ts +102 -0
  3. package/dist/Renderer.js +96 -0
  4. package/dist/controls/CameraControls.d.ts +108 -0
  5. package/dist/controls/CameraControls.js +203 -0
  6. package/dist/controls/Controls.d.ts +53 -0
  7. package/dist/controls/Controls.js +54 -0
  8. package/dist/controls/camera-api.d.ts +226 -0
  9. package/dist/controls/camera-api.js +179 -0
  10. package/dist/controls/gestures.d.ts +71 -0
  11. package/dist/controls/gestures.js +98 -0
  12. package/dist/controls/handlers/DoubleTapZoom.d.ts +23 -0
  13. package/dist/controls/handlers/DoubleTapZoom.js +82 -0
  14. package/dist/controls/handlers/Inertia.d.ts +60 -0
  15. package/dist/controls/handlers/Inertia.js +222 -0
  16. package/dist/controls/handlers/Pan.d.ts +23 -0
  17. package/dist/controls/handlers/Pan.js +212 -0
  18. package/dist/controls/handlers/Pitch.d.ts +19 -0
  19. package/dist/controls/handlers/Pitch.js +200 -0
  20. package/dist/controls/handlers/Roll.d.ts +30 -0
  21. package/dist/controls/handlers/Roll.js +196 -0
  22. package/dist/controls/handlers/Zoom.d.ts +28 -0
  23. package/dist/controls/handlers/Zoom.js +34 -0
  24. package/dist/controls/hooks.d.ts +136 -0
  25. package/dist/controls/hooks.js +200 -0
  26. package/dist/controls/index.d.ts +9 -0
  27. package/dist/controls/index.js +7 -0
  28. package/dist/controls/zoom-to.d.ts +89 -0
  29. package/dist/controls/zoom-to.js +91 -0
  30. package/dist/core/InstanceUniforms.d.ts +108 -0
  31. package/dist/core/InstanceUniforms.js +298 -0
  32. package/dist/core/SquareDataTexture.d.ts +277 -0
  33. package/dist/core/SquareDataTexture.js +476 -0
  34. package/dist/core/batch/BatchedMesh.d.ts +74 -0
  35. package/dist/core/batch/BatchedMesh.js +155 -0
  36. package/dist/core/instanced/FrustumCulling.d.ts +141 -0
  37. package/dist/core/instanced/FrustumCulling.js +246 -0
  38. package/dist/core/instanced/InstancedMesh.d.ts +302 -0
  39. package/dist/core/instanced/InstancedMesh.js +497 -0
  40. package/dist/core/instanced/Instances.d.ts +119 -0
  41. package/dist/core/instanced/Instances.js +230 -0
  42. package/dist/core/instanced/Raycasting.d.ts +118 -0
  43. package/dist/core/instanced/Raycasting.js +134 -0
  44. package/dist/core/instanced/instanced-material.d.ts +77 -0
  45. package/dist/core/instanced/instanced-material.js +228 -0
  46. package/dist/core/lines/BatchedLines.d.ts +137 -0
  47. package/dist/core/lines/BatchedLines.js +215 -0
  48. package/dist/core/lines/FrustumCullingLines.d.ts +44 -0
  49. package/dist/core/lines/FrustumCullingLines.js +85 -0
  50. package/dist/core/lines/RaycastingLines.d.ts +45 -0
  51. package/dist/core/lines/RaycastingLines.js +91 -0
  52. package/dist/core/lines/lines-material.d.ts +47 -0
  53. package/dist/core/lines/lines-material.js +124 -0
  54. package/dist/core/shader-source.d.ts +70 -0
  55. package/dist/core/shader-source.js +110 -0
  56. package/dist/core/sprites/BatchedSprites.d.ts +115 -0
  57. package/dist/core/sprites/BatchedSprites.js +173 -0
  58. package/dist/core/sprites/RaycastingSprites.d.ts +41 -0
  59. package/dist/core/sprites/RaycastingSprites.js +71 -0
  60. package/dist/core/sprites/sprite-material.d.ts +59 -0
  61. package/dist/core/sprites/sprite-material.js +192 -0
  62. package/dist/core/text/BatchedText.d.ts +233 -0
  63. package/dist/core/text/BatchedText.js +473 -0
  64. package/dist/core/text/MemberTexture.d.ts +90 -0
  65. package/dist/core/text/MemberTexture.js +289 -0
  66. package/dist/core/text/Text.d.ts +88 -0
  67. package/dist/core/text/Text.js +230 -0
  68. package/dist/core/text/batched-text-material.d.ts +56 -0
  69. package/dist/core/text/batched-text-material.js +123 -0
  70. package/dist/core/text/glyph-packing.d.ts +17 -0
  71. package/dist/core/text/glyph-packing.js +61 -0
  72. package/dist/core/typed-array.d.ts +16 -0
  73. package/dist/core/typed-array.js +20 -0
  74. package/dist/debug/components/DebugPanelProvider.d.ts +20 -0
  75. package/dist/debug/components/DebugPanelProvider.js +50 -0
  76. package/dist/debug/components/DebugTools.d.ts +22 -0
  77. package/dist/debug/components/DebugTools.js +28 -0
  78. package/dist/debug/components/Gui.d.ts +22 -0
  79. package/dist/debug/components/Gui.js +42 -0
  80. package/dist/debug/components/MemoryInfo.d.ts +22 -0
  81. package/dist/debug/components/MemoryInfo.js +42 -0
  82. package/dist/debug/components/Panel.d.ts +31 -0
  83. package/dist/debug/components/Panel.js +104 -0
  84. package/dist/debug/components/Stats.d.ts +9 -0
  85. package/dist/debug/components/Stats.js +24 -0
  86. package/dist/debug/debug-chunk.d.ts +26 -0
  87. package/dist/debug/debug-chunk.js +32 -0
  88. package/dist/debug/debug-context.d.ts +41 -0
  89. package/dist/debug/debug-context.js +59 -0
  90. package/dist/debug/index.d.ts +41 -0
  91. package/dist/debug/index.js +43 -0
  92. package/dist/events/LayoutDriver.d.ts +36 -0
  93. package/dist/events/LayoutDriver.js +192 -0
  94. package/dist/events/PointerMissed.d.ts +23 -0
  95. package/dist/events/PointerMissed.js +58 -0
  96. package/dist/events/def-culling.d.ts +46 -0
  97. package/dist/events/def-culling.js +69 -0
  98. package/dist/events/layout-targets.d.ts +151 -0
  99. package/dist/events/layout-targets.js +397 -0
  100. package/dist/events/placement/box.d.ts +59 -0
  101. package/dist/events/placement/box.js +96 -0
  102. package/dist/events/placement/grid.d.ts +161 -0
  103. package/dist/events/placement/grid.js +235 -0
  104. package/dist/events/placement/index.d.ts +2 -0
  105. package/dist/events/placement/index.js +1 -0
  106. package/dist/events/placement/pass.d.ts +70 -0
  107. package/dist/events/placement/pass.js +617 -0
  108. package/dist/events/pointer.d.ts +194 -0
  109. package/dist/events/pointer.js +356 -0
  110. package/dist/external/ExternalCanvas.d.ts +56 -0
  111. package/dist/external/ExternalCanvas.js +177 -0
  112. package/dist/external/external-camera.d.ts +18 -0
  113. package/dist/external/external-camera.js +103 -0
  114. package/dist/external/external-events.d.ts +14 -0
  115. package/dist/external/external-events.js +33 -0
  116. package/dist/index.d.ts +13 -0
  117. package/dist/index.js +14 -0
  118. package/dist/objects/Background.d.ts +31 -0
  119. package/dist/objects/Background.js +37 -0
  120. package/dist/objects/containers/SlotContainer.d.ts +312 -0
  121. package/dist/objects/containers/SlotContainer.js +311 -0
  122. package/dist/objects/containers/hooks.d.ts +61 -0
  123. package/dist/objects/containers/hooks.js +196 -0
  124. package/dist/objects/containers/index.d.ts +6 -0
  125. package/dist/objects/containers/index.js +4 -0
  126. package/dist/objects/containers/keys.d.ts +10 -0
  127. package/dist/objects/containers/keys.js +18 -0
  128. package/dist/objects/containers/mesh-writes.d.ts +106 -0
  129. package/dist/objects/containers/mesh-writes.js +97 -0
  130. package/dist/objects/containers/props.d.ts +33 -0
  131. package/dist/objects/containers/props.js +1 -0
  132. package/dist/objects/images/ImageInstance.d.ts +29 -0
  133. package/dist/objects/images/ImageInstance.js +152 -0
  134. package/dist/objects/images/Images.d.ts +49 -0
  135. package/dist/objects/images/Images.js +394 -0
  136. package/dist/objects/images/atlas-packer.d.ts +129 -0
  137. package/dist/objects/images/atlas-packer.js +287 -0
  138. package/dist/objects/images/image-container.d.ts +74 -0
  139. package/dist/objects/images/image-container.js +100 -0
  140. package/dist/objects/images/index.d.ts +2 -0
  141. package/dist/objects/images/index.js +1 -0
  142. package/dist/objects/images/pack-sources.d.ts +190 -0
  143. package/dist/objects/images/pack-sources.js +272 -0
  144. package/dist/objects/images/texture-budget.d.ts +120 -0
  145. package/dist/objects/images/texture-budget.js +215 -0
  146. package/dist/objects/images/worker/atlas-worker.d.ts +56 -0
  147. package/dist/objects/images/worker/atlas-worker.js +123 -0
  148. package/dist/objects/images/worker/client.d.ts +62 -0
  149. package/dist/objects/images/worker/client.js +249 -0
  150. package/dist/objects/images/worker/index.d.ts +10 -0
  151. package/dist/objects/images/worker/index.js +1 -0
  152. package/dist/objects/layer/Layer.d.ts +28 -0
  153. package/dist/objects/layer/Layer.js +134 -0
  154. package/dist/objects/layer/index.d.ts +2 -0
  155. package/dist/objects/layer/index.js +1 -0
  156. package/dist/objects/layer/layer-context.d.ts +74 -0
  157. package/dist/objects/layer/layer-context.js +82 -0
  158. package/dist/objects/layer/mount-scheduler.d.ts +131 -0
  159. package/dist/objects/layer/mount-scheduler.js +289 -0
  160. package/dist/objects/layer/visibility-gui.d.ts +19 -0
  161. package/dist/objects/layer/visibility-gui.js +93 -0
  162. package/dist/objects/lighting/Lighting.d.ts +92 -0
  163. package/dist/objects/lighting/Lighting.js +384 -0
  164. package/dist/objects/lighting/OcclusionPasses.d.ts +23 -0
  165. package/dist/objects/lighting/OcclusionPasses.js +271 -0
  166. package/dist/objects/lighting/index.d.ts +11 -0
  167. package/dist/objects/lighting/index.js +10 -0
  168. package/dist/objects/lighting/lighting-gui.d.ts +24 -0
  169. package/dist/objects/lighting/lighting-gui.js +114 -0
  170. package/dist/objects/lighting/occluder.d.ts +34 -0
  171. package/dist/objects/lighting/occluder.js +62 -0
  172. package/dist/objects/lighting/occlusion-targets.d.ts +143 -0
  173. package/dist/objects/lighting/occlusion-targets.js +276 -0
  174. package/dist/objects/lines/LineInstance.d.ts +21 -0
  175. package/dist/objects/lines/LineInstance.js +73 -0
  176. package/dist/objects/lines/Lines.d.ts +17 -0
  177. package/dist/objects/lines/Lines.js +31 -0
  178. package/dist/objects/lines/index.d.ts +2 -0
  179. package/dist/objects/lines/index.js +1 -0
  180. package/dist/objects/lines/line-container.d.ts +35 -0
  181. package/dist/objects/lines/line-container.js +40 -0
  182. package/dist/objects/materials/ColorMaterial.d.ts +92 -0
  183. package/dist/objects/materials/ColorMaterial.js +124 -0
  184. package/dist/objects/materials/LineMaterial.d.ts +44 -0
  185. package/dist/objects/materials/LineMaterial.js +74 -0
  186. package/dist/objects/materials/ModelMaterial.d.ts +53 -0
  187. package/dist/objects/materials/ModelMaterial.js +111 -0
  188. package/dist/objects/materials/TextureMaterial.d.ts +33 -0
  189. package/dist/objects/materials/TextureMaterial.js +48 -0
  190. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.d.ts +77 -0
  191. package/dist/objects/materials/effects/ao/ambient-occlusion-pass.js +296 -0
  192. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.d.ts +29 -0
  193. package/dist/objects/materials/effects/ao/ambient-occlusion-patch.js +115 -0
  194. package/dist/objects/materials/effects/ao/blur-pass.d.ts +61 -0
  195. package/dist/objects/materials/effects/ao/blur-pass.js +101 -0
  196. package/dist/objects/materials/effects/ao/index.d.ts +129 -0
  197. package/dist/objects/materials/effects/ao/index.js +72 -0
  198. package/dist/objects/materials/effects/color-dim.d.ts +115 -0
  199. package/dist/objects/materials/effects/color-dim.js +206 -0
  200. package/dist/objects/materials/effects/contact-shadow.d.ts +100 -0
  201. package/dist/objects/materials/effects/contact-shadow.js +167 -0
  202. package/dist/objects/materials/effects/effects-context.d.ts +53 -0
  203. package/dist/objects/materials/effects/effects-context.js +24 -0
  204. package/dist/objects/materials/shared.d.ts +24 -0
  205. package/dist/objects/materials/shared.js +72 -0
  206. package/dist/objects/models/ModelInstance.d.ts +22 -0
  207. package/dist/objects/models/ModelInstance.js +33 -0
  208. package/dist/objects/models/Models.d.ts +22 -0
  209. package/dist/objects/models/Models.js +186 -0
  210. package/dist/objects/models/gltf-cache.d.ts +17 -0
  211. package/dist/objects/models/gltf-cache.js +50 -0
  212. package/dist/objects/models/index.d.ts +2 -0
  213. package/dist/objects/models/index.js +1 -0
  214. package/dist/objects/models/model-asset.d.ts +48 -0
  215. package/dist/objects/models/model-asset.js +77 -0
  216. package/dist/objects/models/model-container.d.ts +123 -0
  217. package/dist/objects/models/model-container.js +207 -0
  218. package/dist/objects/shapes/ShapeInstance.d.ts +26 -0
  219. package/dist/objects/shapes/ShapeInstance.js +39 -0
  220. package/dist/objects/shapes/Shapes.d.ts +21 -0
  221. package/dist/objects/shapes/Shapes.js +131 -0
  222. package/dist/objects/shapes/index.d.ts +2 -0
  223. package/dist/objects/shapes/index.js +1 -0
  224. package/dist/objects/shapes/shape-container.d.ts +141 -0
  225. package/dist/objects/shapes/shape-container.js +242 -0
  226. package/dist/objects/shapes/shape-geometry.d.ts +27 -0
  227. package/dist/objects/shapes/shape-geometry.js +94 -0
  228. package/dist/objects/text/TextInstance.d.ts +22 -0
  229. package/dist/objects/text/TextInstance.js +95 -0
  230. package/dist/objects/text/Texts.d.ts +30 -0
  231. package/dist/objects/text/Texts.js +43 -0
  232. package/dist/objects/text/index.d.ts +2 -0
  233. package/dist/objects/text/index.js +1 -0
  234. package/dist/objects/text/text-container.d.ts +121 -0
  235. package/dist/objects/text/text-container.js +229 -0
  236. package/dist/objects/text/text-layout.d.ts +92 -0
  237. package/dist/objects/text/text-layout.js +180 -0
  238. package/dist/space/Camera.d.ts +103 -0
  239. package/dist/space/Camera.js +321 -0
  240. package/dist/space/Scene.d.ts +47 -0
  241. package/dist/space/Scene.js +127 -0
  242. package/dist/space/Space.d.ts +325 -0
  243. package/dist/space/Space.js +359 -0
  244. package/dist/space/VisibleRect.d.ts +45 -0
  245. package/dist/space/VisibleRect.js +129 -0
  246. package/dist/space/camera-handle.d.ts +67 -0
  247. package/dist/space/camera-handle.js +63 -0
  248. package/dist/space/camera-kind.d.ts +26 -0
  249. package/dist/space/camera-kind.js +30 -0
  250. package/dist/space/canvas-size.d.ts +24 -0
  251. package/dist/space/canvas-size.js +20 -0
  252. package/dist/space/clipping.d.ts +69 -0
  253. package/dist/space/clipping.js +130 -0
  254. package/dist/space/layout-measure.d.ts +67 -0
  255. package/dist/space/layout-measure.js +175 -0
  256. package/dist/space/viewport.d.ts +112 -0
  257. package/dist/space/viewport.js +67 -0
  258. package/dist/types/defs.d.ts +234 -0
  259. package/dist/types/defs.js +1 -0
  260. package/dist/types/events.d.ts +198 -0
  261. package/dist/types/events.js +1 -0
  262. package/dist/types/guards.d.ts +21 -0
  263. package/dist/types/guards.js +52 -0
  264. package/dist/update/index.d.ts +3 -0
  265. package/dist/update/index.js +2 -0
  266. package/dist/update/slot-writers.d.ts +30 -0
  267. package/dist/update/slot-writers.js +76 -0
  268. package/dist/update/store.d.ts +41 -0
  269. package/dist/update/store.js +104 -0
  270. package/dist/utils/arrays.d.ts +13 -0
  271. package/dist/utils/arrays.js +20 -0
  272. package/dist/utils/colors.d.ts +19 -0
  273. package/dist/utils/colors.js +27 -0
  274. package/dist/utils/hot-state.d.ts +41 -0
  275. package/dist/utils/hot-state.js +37 -0
  276. package/dist/utils/logging.d.ts +29 -0
  277. package/dist/utils/logging.js +55 -0
  278. package/dist/utils/math.d.ts +52 -0
  279. package/dist/utils/math.js +76 -0
  280. package/package.json +79 -0
@@ -0,0 +1,121 @@
1
+ import { type BatchedTextImpl } from '../../core/text/BatchedText';
2
+ import { TextImpl } from '../../core/text/Text';
3
+ import { type RenderableDef, type TextDef } from '../../types/defs';
4
+ import { SlotContainer } from '../containers';
5
+ /** One def's members: a `TextImpl` per line, and the batch slot each of them occupies. */
6
+ export interface LineGroup {
7
+ /** The members, in the def's line order. */
8
+ readonly texts: readonly TextImpl[];
9
+ /** Each member's batch slot, index-parallel to {@link texts}. */
10
+ readonly slots: readonly number[];
11
+ }
12
+ /**
13
+ * One text layer's bookkeeping: a group of members per def, and the paint order across them.
14
+ *
15
+ * The slot a def claims here is a **group**, not a batch member — a `TextDef` holds an array of
16
+ * lines and each line is its own `TextImpl`. That is the one place this differs from the shape and
17
+ * image containers, and it falls out of the def type rather than being a choice: the lines of one
18
+ * def are laid out together (a centered block needs every line's height before it can place the
19
+ * first), so they cannot be independent instances.
20
+ *
21
+ * Group size is the slot's resource, so a def that gains or loses a line re-claims automatically —
22
+ * which is what makes `TextDef.lines` updatable here, where the imperative renderer required it to
23
+ * be fixed.
24
+ *
25
+ * Ordering differs too: a text batch draws through packed glyph attributes rather than a sortable
26
+ * draw list, so {@link applyOrder} hands the batch a rebuilt member list instead of labeling slots,
27
+ * and the inherited `customSort` goes unused.
28
+ */
29
+ export declare class TextContainer extends SlotContainer<number, BatchedTextImpl, TextDef> {
30
+ /** Groups by slot, with holes for released ones. */
31
+ private readonly groups;
32
+ private readonly freeGroups;
33
+ /**
34
+ * Batch member to the group holding it, so a hit — which names a member — resolves to a def. The
35
+ * batch reuses a released member's slot, so entries are dropped with the group that owned them.
36
+ */
37
+ private readonly groupByMember;
38
+ private groupCount;
39
+ /** The last order applied to the batch, kept so an unchanged one can skip the repack. */
40
+ private appliedOrder;
41
+ private syncScheduled;
42
+ private readonly invalidate;
43
+ /**
44
+ * Builds a text layer's bookkeeping. The batch itself is R3F's — the component renders
45
+ * `<BatchedText>` and hands it over through {@link SlotContainer.bind} — so R3F disposes it on
46
+ * unmount.
47
+ * @param invalidate - R3F's frame request, called when an asynchronous sync completes. Stable for
48
+ * the lifetime of a canvas, so capturing it here is safe.
49
+ */
50
+ constructor(invalidate: () => void);
51
+ /**
52
+ * Binds the batch, and asks for a frame each time it finishes a sync.
53
+ *
54
+ * {@link scheduleSync}'s callback closes the loop only for a sync it starts. A write made inside a
55
+ * frame callback — a layout rule's, or a placement report's drained before the render — flags the
56
+ * batch before that frame renders, so the batch's own render hook starts the sync, with no
57
+ * callback, and the coalesced one that follows finds nothing left to do. Its glyphs then land
58
+ * after the last frame anyone asked for, and the old text stays on screen until something else
59
+ * happens to draw. A repack that runs inside the render hook is a frame late for the same reason:
60
+ * three has uploaded the batch's attributes by then. The batch's own completion is the one signal
61
+ * both paths share.
62
+ * @param mesh - The mounted batch, or the ref-cleanup `null`.
63
+ */
64
+ bind(mesh: BatchedTextImpl | null): void;
65
+ /**
66
+ * The members backing one def.
67
+ * @param slot - The def's group.
68
+ * @returns Its members and their batch slots.
69
+ */
70
+ linesAt(slot: number): LineGroup;
71
+ /**
72
+ * Requests one batch sync, coalescing every request made in the same tick.
73
+ *
74
+ * Eager rather than waiting for `onBeforeRender`, because syncing is asynchronous — troika loads
75
+ * and parses the font — and under on-demand rendering a sync started during a frame has nothing to
76
+ * request the *next* frame when it completes. The callback carries R3F's `invalidate`, which is
77
+ * what closes that loop; without it new text appears only when something else happens to
78
+ * invalidate. A sync this did not start is covered by the batch's own completion, which
79
+ * {@link bind} listens for.
80
+ */
81
+ scheduleSync(): void;
82
+ claim(def: RenderableDef, lineCount: number): number;
83
+ setVisibleAt(slot: number, visible: boolean): void;
84
+ /**
85
+ * Sets the draw order for the whole layer, then syncs.
86
+ *
87
+ * The sync rides along so one structural change costs exactly one sync — a mount writes content
88
+ * through its instances in the same effect flush, and every request in the tick coalesces.
89
+ * @param defs - The layer's texts, in paint order.
90
+ * @param layerDimmed - Whether the layer is dimmed, inherited by defs without a `dim` of their own.
91
+ */
92
+ setPaintOrder(defs: readonly TextDef[], layerDimmed: boolean): void;
93
+ /**
94
+ * The batch reports the member it was hit on, and a def owns one member per line — so unlike every
95
+ * other container's, this one's slots are not its mesh's instances, and a member resolves to a def
96
+ * only through the group holding it.
97
+ * @param member - The member the batch reported.
98
+ * @returns The def, or `undefined` when no claimed group holds that member.
99
+ */
100
+ defAt(member: number | undefined): RenderableDef | undefined;
101
+ /**
102
+ * Rebuilds the flat member order from the current defs and hands it to the batch, unless nothing
103
+ * moved.
104
+ *
105
+ * Skipping an unchanged order matters more here than for the other containers: a text batch has no
106
+ * `customSort`, so reordering means repacking the glyph attributes, and `useStagedChildren` hands out
107
+ * a fresh snapshot on *every* layer update — including ones that moved nothing.
108
+ */
109
+ protected applyOrder(): void;
110
+ /**
111
+ * Per member, not per def: a def's lines are separate batch members, and the uniform row is
112
+ * addressed by member. One `dim` on the def therefore writes each of its lines, exactly as
113
+ * visibility does.
114
+ * @param slot - The def's group.
115
+ * @param amount - The encoded amount.
116
+ */
117
+ protected writeDim(slot: number, amount: number): void;
118
+ protected addInstance(lineCount: number): number;
119
+ protected removeInstance(slot: number): void;
120
+ }
121
+ //# sourceMappingURL=text-container.d.ts.map
@@ -0,0 +1,229 @@
1
+ import { TextImpl } from '../../core/text/Text';
2
+ import { sameSequence } from '../../utils/arrays';
3
+ import { SlotContainer } from '../containers';
4
+ import { isDimmed } from '../materials/effects/color-dim';
5
+ import { LAYOUT_FONT_SIZE, LINE_HEIGHT } from './text-layout';
6
+ /**
7
+ * One text layer's bookkeeping: a group of members per def, and the paint order across them.
8
+ *
9
+ * The slot a def claims here is a **group**, not a batch member — a `TextDef` holds an array of
10
+ * lines and each line is its own `TextImpl`. That is the one place this differs from the shape and
11
+ * image containers, and it falls out of the def type rather than being a choice: the lines of one
12
+ * def are laid out together (a centered block needs every line's height before it can place the
13
+ * first), so they cannot be independent instances.
14
+ *
15
+ * Group size is the slot's resource, so a def that gains or loses a line re-claims automatically —
16
+ * which is what makes `TextDef.lines` updatable here, where the imperative renderer required it to
17
+ * be fixed.
18
+ *
19
+ * Ordering differs too: a text batch draws through packed glyph attributes rather than a sortable
20
+ * draw list, so {@link applyOrder} hands the batch a rebuilt member list instead of labeling slots,
21
+ * and the inherited `customSort` goes unused.
22
+ */
23
+ export class TextContainer extends SlotContainer {
24
+ /** Groups by slot, with holes for released ones. */
25
+ groups = [];
26
+ freeGroups = [];
27
+ /**
28
+ * Batch member to the group holding it, so a hit — which names a member — resolves to a def. The
29
+ * batch reuses a released member's slot, so entries are dropped with the group that owned them.
30
+ */
31
+ groupByMember = new Map();
32
+ groupCount = 0;
33
+ /** The last order applied to the batch, kept so an unchanged one can skip the repack. */
34
+ appliedOrder = [];
35
+ syncScheduled = false;
36
+ invalidate;
37
+ /**
38
+ * Builds a text layer's bookkeeping. The batch itself is R3F's — the component renders
39
+ * `<BatchedText>` and hands it over through {@link SlotContainer.bind} — so R3F disposes it on
40
+ * unmount.
41
+ * @param invalidate - R3F's frame request, called when an asynchronous sync completes. Stable for
42
+ * the lifetime of a canvas, so capturing it here is safe.
43
+ */
44
+ constructor(invalidate) {
45
+ super();
46
+ this.invalidate = invalidate;
47
+ }
48
+ /**
49
+ * Binds the batch, and asks for a frame each time it finishes a sync.
50
+ *
51
+ * {@link scheduleSync}'s callback closes the loop only for a sync it starts. A write made inside a
52
+ * frame callback — a layout rule's, or a placement report's drained before the render — flags the
53
+ * batch before that frame renders, so the batch's own render hook starts the sync, with no
54
+ * callback, and the coalesced one that follows finds nothing left to do. Its glyphs then land
55
+ * after the last frame anyone asked for, and the old text stays on screen until something else
56
+ * happens to draw. A repack that runs inside the render hook is a frame late for the same reason:
57
+ * three has uploaded the batch's attributes by then. The batch's own completion is the one signal
58
+ * both paths share.
59
+ * @param mesh - The mounted batch, or the ref-cleanup `null`.
60
+ */
61
+ bind(mesh) {
62
+ // `mesh` reads the bound batch, which is undefined until the first bind; the base ignores a
63
+ // re-bind, so the listener is added once too.
64
+ const first = mesh !== null && this.mesh === undefined;
65
+ super.bind(mesh);
66
+ if (first)
67
+ mesh.addEventListener('synccomplete', () => this.invalidate());
68
+ }
69
+ /**
70
+ * The members backing one def.
71
+ * @param slot - The def's group.
72
+ * @returns Its members and their batch slots.
73
+ */
74
+ linesAt(slot) {
75
+ return this.groups[slot] ?? EMPTY_GROUP;
76
+ }
77
+ /**
78
+ * Requests one batch sync, coalescing every request made in the same tick.
79
+ *
80
+ * Eager rather than waiting for `onBeforeRender`, because syncing is asynchronous — troika loads
81
+ * and parses the font — and under on-demand rendering a sync started during a frame has nothing to
82
+ * request the *next* frame when it completes. The callback carries R3F's `invalidate`, which is
83
+ * what closes that loop; without it new text appears only when something else happens to
84
+ * invalidate. A sync this did not start is covered by the batch's own completion, which
85
+ * {@link bind} listens for.
86
+ */
87
+ scheduleSync() {
88
+ if (this.syncScheduled)
89
+ return;
90
+ this.syncScheduled = true;
91
+ // `Promise.resolve().then`, not `queueMicrotask`: the SDK targets Android WebViews as old as
92
+ // Chrome 66, which predates it. Same microtask timing.
93
+ void Promise.resolve().then(() => {
94
+ this.syncScheduled = false;
95
+ // Unmounted between the request and the microtask; the batch is R3F's and already disposed.
96
+ if (!this.mesh)
97
+ return;
98
+ this.mesh.sync(this.invalidate);
99
+ });
100
+ }
101
+ claim(def, lineCount) {
102
+ const slot = super.claim(def, lineCount);
103
+ // `addText` appends to the batch's order, so a first mount draws in claim order and the layer's
104
+ // own pass relabels it in the same effect flush. A *re*-claim after that pass has to re-run it,
105
+ // or the new members would stay stuck at the end of the paint order.
106
+ if (this.layerDefs.length)
107
+ this.applyOrder();
108
+ return slot;
109
+ }
110
+ setVisibleAt(slot, visible) {
111
+ const group = this.groups[slot];
112
+ if (!group)
113
+ return;
114
+ for (const member of group.slots) {
115
+ this.mesh.setVisibleAt(member, visible);
116
+ }
117
+ }
118
+ /**
119
+ * Sets the draw order for the whole layer, then syncs.
120
+ *
121
+ * The sync rides along so one structural change costs exactly one sync — a mount writes content
122
+ * through its instances in the same effect flush, and every request in the tick coalesces.
123
+ * @param defs - The layer's texts, in paint order.
124
+ * @param layerDimmed - Whether the layer is dimmed, inherited by defs without a `dim` of their own.
125
+ */
126
+ setPaintOrder(defs, layerDimmed) {
127
+ super.setPaintOrder(defs, layerDimmed);
128
+ this.scheduleSync();
129
+ }
130
+ /**
131
+ * The batch reports the member it was hit on, and a def owns one member per line — so unlike every
132
+ * other container's, this one's slots are not its mesh's instances, and a member resolves to a def
133
+ * only through the group holding it.
134
+ * @param member - The member the batch reported.
135
+ * @returns The def, or `undefined` when no claimed group holds that member.
136
+ */
137
+ defAt(member) {
138
+ return super.defAt(member === undefined ? undefined : this.groupByMember.get(member));
139
+ }
140
+ /**
141
+ * Rebuilds the flat member order from the current defs and hands it to the batch, unless nothing
142
+ * moved.
143
+ *
144
+ * Skipping an unchanged order matters more here than for the other containers: a text batch has no
145
+ * `customSort`, so reordering means repacking the glyph attributes, and `useStagedChildren` hands out
146
+ * a fresh snapshot on *every* layer update — including ones that moved nothing.
147
+ */
148
+ applyOrder() {
149
+ const order = [];
150
+ for (const def of this.orderedDefs()) {
151
+ const slot = this.slotOf(def);
152
+ if (slot === undefined)
153
+ continue;
154
+ const group = this.groups[slot];
155
+ if (!group)
156
+ continue;
157
+ order.push(...group.texts);
158
+ this.dimmedBySlot[slot] = isDimmed(def.dim, this.layerDimmed);
159
+ }
160
+ if (sameSequence(order, this.appliedOrder))
161
+ return;
162
+ this.appliedOrder = order;
163
+ this.mesh.setOrder(order);
164
+ }
165
+ /**
166
+ * Per member, not per def: a def's lines are separate batch members, and the uniform row is
167
+ * addressed by member. One `dim` on the def therefore writes each of its lines, exactly as
168
+ * visibility does.
169
+ * @param slot - The def's group.
170
+ * @param amount - The encoded amount.
171
+ */
172
+ writeDim(slot, amount) {
173
+ const group = this.groups[slot];
174
+ if (!group)
175
+ return;
176
+ for (const member of group.slots) {
177
+ super.writeDim(member, amount);
178
+ }
179
+ }
180
+ addInstance(lineCount) {
181
+ const slot = this.freeGroups.pop() ?? this.groupCount++;
182
+ const texts = [];
183
+ const slots = [];
184
+ for (let line = 0; line < lineCount; line++) {
185
+ const text = makeMember();
186
+ texts.push(text);
187
+ slots.push(this.mesh.addText(text));
188
+ }
189
+ this.groups[slot] = { texts, slots };
190
+ for (const member of slots)
191
+ this.groupByMember.set(member, slot);
192
+ return slot;
193
+ }
194
+ removeInstance(slot) {
195
+ const group = this.groups[slot];
196
+ if (!group)
197
+ return;
198
+ for (const member of group.slots)
199
+ this.groupByMember.delete(member);
200
+ for (const text of group.texts) {
201
+ this.mesh.removeText(text);
202
+ // The member's own `GlyphsGeometry`, which only ever held its bounds — the batch draws from
203
+ // its own. Nothing was uploaded, so this is bookkeeping, not a GPU free.
204
+ text.dispose();
205
+ }
206
+ this.groups[slot] = undefined;
207
+ this.freeGroups.push(slot);
208
+ // Members left the batch's order with them; drop the stale copy so the next pass reapplies.
209
+ this.appliedOrder = [];
210
+ }
211
+ }
212
+ /**
213
+ * Builds a batch member with the properties that never change for its lifetime.
214
+ *
215
+ * All three are syncable, so writing them once here — rather than per update — is what keeps a
216
+ * layout change from re-running troika's async layout and repacking the batch.
217
+ * @returns The member.
218
+ */
219
+ function makeMember() {
220
+ const text = new TextImpl();
221
+ text.fontSize = LAYOUT_FONT_SIZE;
222
+ text.lineHeight = LINE_HEIGHT;
223
+ // Wrapping is the floorplan's job: it measures and breaks lines itself, then hands over the
224
+ // result with explicit newlines. Letting troika re-wrap on top would fight that.
225
+ text.whiteSpace = 'nowrap';
226
+ return text;
227
+ }
228
+ /** Returned for an unclaimed slot, so a caller reading one iterates nothing rather than throwing. */
229
+ const EMPTY_GROUP = { texts: [], slots: [] };
@@ -0,0 +1,92 @@
1
+ import { type Rect } from '@expofp/geometry';
2
+ import { type TextImpl } from '../../core/text/Text';
3
+ import { type TextDef, type TextLineDef } from '../../types/defs';
4
+ /**
5
+ * The em size every member lays out at, fixed for its lifetime.
6
+ *
7
+ * `fontSize` is a syncable property: changing it re-runs troika's async layout and repacks the whole
8
+ * batch. A label's size changes on every zoom step, so paying that would make zooming a sequence of
9
+ * repacks. Laying out at a constant 1 em and carrying the real size in the member's `scale` costs a
10
+ * matrix write instead — the scale is not syncable, so nothing re-lays-out.
11
+ *
12
+ * One, not zero: troika reads `this.fontSize || 0.1`, so a member left at zero lays out at a tenth of
13
+ * an em and every scale below would have to carry a compensating factor of ten.
14
+ */
15
+ export declare const LAYOUT_FONT_SIZE = 1;
16
+ /** Line box height as a multiple of the font size, matching the `lineHeight` members are given. */
17
+ export declare const LINE_HEIGHT = 1.1;
18
+ /**
19
+ * A line's placement **in its def's own unrotated frame**, as an offset from the bounds' center.
20
+ *
21
+ * Layout stops here on purpose: where the bounds sit in the plan, and how they are turned, is a
22
+ * separate step ({@link applyPlacement}). Keeping the two apart is what lets the stacking be
23
+ * asserted as plain numbers, with no trigonometry in the test.
24
+ */
25
+ export interface LaidOutLine {
26
+ /** Which horizontal point of the line's box sits at its origin. */
27
+ anchorX: 'left' | 'center' | 'right';
28
+ /** Which vertical point of the line's box sits at its origin; only the outer edges are used. */
29
+ anchorY: 'top' | 'bottom';
30
+ /** How wrapped rows align against each other within this line. */
31
+ textAlign: 'left' | 'center' | 'right';
32
+ /** Offset from the bounds' center along the bounds' own x axis. */
33
+ offsetX: number;
34
+ /** Offset from the bounds' center along the bounds' own y axis. */
35
+ offsetY: number;
36
+ /** Horizontal scale: the line's font size, which is its em height in plan units. */
37
+ scaleX: number;
38
+ /** Vertical scale: negative, because plan space runs y-down and text lays out y-up. */
39
+ scaleY: number;
40
+ /** `[minX, minY, maxX, maxY]` in the line's own em space, clipping it to the def's bounds. */
41
+ clipRect: [number, number, number, number];
42
+ }
43
+ /**
44
+ * Stacks a text def's lines inside its bounds, aligned as the def asks.
45
+ *
46
+ * Works entirely in the bounds' unrotated frame, so a rotated def lays out identically to an
47
+ * unrotated one — the rotation is applied afterwards, to the whole stack at once. That is also what
48
+ * lets the clip rect be a plain offset from the center: the member carries the same rotation, so the
49
+ * rect turns with it.
50
+ *
51
+ * **The y flip lives in `scaleY`.** Plan space runs y-down; troika lays text out y-up. Rather than
52
+ * troika's `orientation` — a `lookAt` rotation, which would leave the clip rect to be flipped
53
+ * separately — the flip rides on the scale the lines need anyway for their font size, so the clip
54
+ * rect, which divides by that scale, inherits it for free.
55
+ *
56
+ * **Plan units throughout**, font sizes included. A plan that wants a label to hold an on-screen size
57
+ * converts for itself, against the pixel size the renderer publishes — the same way it sizes every
58
+ * other pixel-authored thing it draws.
59
+ * @param def - The text to lay out.
60
+ * @returns One placement per line, in the def's line order.
61
+ */
62
+ export declare function layoutText(def: TextDef): LaidOutLine[];
63
+ /**
64
+ * Writes a laid-out line onto its member: the bounds rotation and position applied to the local
65
+ * offset, the line's scale, and its clip rect.
66
+ *
67
+ * The post-transform half of `layoutText`. `updateMatrix` is what reports the move to the batch,
68
+ * because members are not in the scene graph and nothing else would.
69
+ *
70
+ * A screen-space def also gets its sprite anchor. That is the def's bounds center, shared by every
71
+ * line: each line's member sits at its own plan position, and pivoting each about itself would
72
+ * compress the stack under tilt. The anchor carries no elevation, because every line already sits at
73
+ * the def's own elevation on `position`, and `MemberTexture` reads it from there instead.
74
+ * `MemberTexture` then composes the member per draw, so `matrixAutoUpdate` goes off and the
75
+ * position, rotation and scale written here become the inputs that compose reads rather than the
76
+ * matrix itself.
77
+ * @param text - The member to place.
78
+ * @param line - The line's local placement.
79
+ * @param bounds - The def's bounds, supplying the rotation, center and elevation.
80
+ * @param screenSpace - Whether the def faces the camera.
81
+ */
82
+ export declare function applyPlacement(text: TextImpl, line: LaidOutLine, bounds: Rect, screenSpace: boolean): void;
83
+ /**
84
+ * The height one line's box occupies.
85
+ *
86
+ * A single `TextLineDef` can hold embedded newlines — the floorplan joins an exhibitor's name rows
87
+ * into one def — so its box is as many line boxes tall as it has rows.
88
+ * @param line - The line to measure.
89
+ * @returns The height, in plan units.
90
+ */
91
+ export declare function lineHeight(line: TextLineDef): number;
92
+ //# sourceMappingURL=text-layout.d.ts.map
@@ -0,0 +1,180 @@
1
+ import { Vector2 } from 'three';
2
+ /**
3
+ * The em size every member lays out at, fixed for its lifetime.
4
+ *
5
+ * `fontSize` is a syncable property: changing it re-runs troika's async layout and repacks the whole
6
+ * batch. A label's size changes on every zoom step, so paying that would make zooming a sequence of
7
+ * repacks. Laying out at a constant 1 em and carrying the real size in the member's `scale` costs a
8
+ * matrix write instead — the scale is not syncable, so nothing re-lays-out.
9
+ *
10
+ * One, not zero: troika reads `this.fontSize || 0.1`, so a member left at zero lays out at a tenth of
11
+ * an em and every scale below would have to carry a compensating factor of ten.
12
+ */
13
+ export const LAYOUT_FONT_SIZE = 1;
14
+ /** Line box height as a multiple of the font size, matching the `lineHeight` members are given. */
15
+ export const LINE_HEIGHT = 1.1;
16
+ /**
17
+ * Stacks a text def's lines inside its bounds, aligned as the def asks.
18
+ *
19
+ * Works entirely in the bounds' unrotated frame, so a rotated def lays out identically to an
20
+ * unrotated one — the rotation is applied afterwards, to the whole stack at once. That is also what
21
+ * lets the clip rect be a plain offset from the center: the member carries the same rotation, so the
22
+ * rect turns with it.
23
+ *
24
+ * **The y flip lives in `scaleY`.** Plan space runs y-down; troika lays text out y-up. Rather than
25
+ * troika's `orientation` — a `lookAt` rotation, which would leave the clip rect to be flipped
26
+ * separately — the flip rides on the scale the lines need anyway for their font size, so the clip
27
+ * rect, which divides by that scale, inherits it for free.
28
+ *
29
+ * **Plan units throughout**, font sizes included. A plan that wants a label to hold an on-screen size
30
+ * converts for itself, against the pixel size the renderer publishes — the same way it sizes every
31
+ * other pixel-authored thing it draws.
32
+ * @param def - The text to lay out.
33
+ * @returns One placement per line, in the def's line order.
34
+ */
35
+ export function layoutText(def) {
36
+ const { bounds, lines, alignment, padding } = def;
37
+ const half = _half.set(bounds.size.x, bounds.size.y).multiplyScalar(0.5);
38
+ // Where the first line's anchor sits. A centered block starts half the leftover space in from the
39
+ // top edge, so the stack ends up centered as a whole; every other alignment starts at the padded
40
+ // edge itself.
41
+ const offset = _offset.copy(half);
42
+ if (alignment.vertical === 'center') {
43
+ let stackHeight = 0;
44
+ for (const line of lines)
45
+ stackHeight += lineHeight(line);
46
+ offset.y -= (bounds.size.y - stackHeight) / 2;
47
+ }
48
+ else {
49
+ offset.sub(_padding.fromArray(padding));
50
+ }
51
+ offset.multiply(alignmentFactors(alignment));
52
+ const placed = [];
53
+ for (const line of lines) {
54
+ const scaleX = line.fontSize;
55
+ // Negative: the y flip. Also why the clip rect below comes out correctly ordered — dividing by a
56
+ // negative scale swaps its min and max back.
57
+ //
58
+ // `TextImpl.orientation` looks like the right tool for this flip, but it only reaches the shader
59
+ // as a uniform. `BatchedTextImpl.raycast` and `isDrawn` pick and cull through `text.matrix` and
60
+ // its geometry's bounds on the CPU, which never read it, so using `orientation` here draws
61
+ // correctly and picks the wrong region.
62
+ const scaleY = -scaleX;
63
+ placed.push({
64
+ anchorX: alignment.horizontal,
65
+ // Only the outer edges: a middle-anchored line would stack against its own center, so the
66
+ // block's vertical alignment is carried by the starting offset above instead.
67
+ anchorY: alignment.vertical === 'bottom' ? 'bottom' : 'top',
68
+ textAlign: alignment.text ?? alignment.horizontal,
69
+ offsetX: offset.x,
70
+ offsetY: offset.y,
71
+ scaleX,
72
+ scaleY,
73
+ // The bounds' corners relative to this line's origin, converted from plan units into the
74
+ // line's em space. A zero-size line would divide by zero, but those are hidden before they
75
+ // draw (see `TextInstance`), so the clip rect is never read.
76
+ clipRect: [
77
+ (-half.x - offset.x) / scaleX,
78
+ (half.y - offset.y) / scaleY,
79
+ (half.x - offset.x) / scaleX,
80
+ (-half.y - offset.y) / scaleY,
81
+ ],
82
+ });
83
+ offset.y += lineHeight(line);
84
+ }
85
+ return placed;
86
+ }
87
+ /**
88
+ * Writes a laid-out line onto its member: the bounds rotation and position applied to the local
89
+ * offset, the line's scale, and its clip rect.
90
+ *
91
+ * The post-transform half of `layoutText`. `updateMatrix` is what reports the move to the batch,
92
+ * because members are not in the scene graph and nothing else would.
93
+ *
94
+ * A screen-space def also gets its sprite anchor. That is the def's bounds center, shared by every
95
+ * line: each line's member sits at its own plan position, and pivoting each about itself would
96
+ * compress the stack under tilt. The anchor carries no elevation, because every line already sits at
97
+ * the def's own elevation on `position`, and `MemberTexture` reads it from there instead.
98
+ * `MemberTexture` then composes the member per draw, so `matrixAutoUpdate` goes off and the
99
+ * position, rotation and scale written here become the inputs that compose reads rather than the
100
+ * matrix itself.
101
+ * @param text - The member to place.
102
+ * @param line - The line's local placement.
103
+ * @param bounds - The def's bounds, supplying the rotation, center and elevation.
104
+ * @param screenSpace - Whether the def faces the camera.
105
+ */
106
+ export function applyPlacement(text, line, bounds, screenSpace) {
107
+ const { rotation } = bounds;
108
+ const position = _position
109
+ .set(line.offsetX, line.offsetY)
110
+ .rotateAround(ORIGIN, rotation)
111
+ .add(bounds.center);
112
+ text.position.set(position.x, position.y, bounds.elevation);
113
+ text.rotation.set(0, 0, rotation);
114
+ text.scale.set(line.scaleX, line.scaleY, 1);
115
+ if (screenSpace) {
116
+ // Mutated in place rather than replaced, so a per-update write allocates nothing.
117
+ const anchor = text.spriteAnchor ?? new Vector2();
118
+ anchor.set(bounds.center.x, bounds.center.y);
119
+ text.spriteAnchor = anchor;
120
+ // The compose owns the matrix from here. Nothing may recompose it from the three fields above.
121
+ text.matrixAutoUpdate = false;
122
+ }
123
+ else {
124
+ text.spriteAnchor = null;
125
+ text.matrixAutoUpdate = true;
126
+ }
127
+ text.updateMatrix();
128
+ // Held as one array for the member's life: `clipRect` is not a syncable property, so this is a
129
+ // plain per-frame read and mutating it in place avoids a per-update allocation.
130
+ const clipRect = (text.clipRect ??= [0, 0, 0, 0]);
131
+ clipRect[0] = line.clipRect[0];
132
+ clipRect[1] = line.clipRect[1];
133
+ clipRect[2] = line.clipRect[2];
134
+ clipRect[3] = line.clipRect[3];
135
+ }
136
+ /**
137
+ * The height one line's box occupies.
138
+ *
139
+ * A single `TextLineDef` can hold embedded newlines — the floorplan joins an exhibitor's name rows
140
+ * into one def — so its box is as many line boxes tall as it has rows.
141
+ * @param line - The line to measure.
142
+ * @returns The height, in plan units.
143
+ */
144
+ export function lineHeight(line) {
145
+ return line.fontSize * LINE_HEIGHT * line.text.split('\n').length;
146
+ }
147
+ /**
148
+ * Which way each axis is offset from the bounds' center for a given alignment.
149
+ *
150
+ * Vertical `center` shares `top`'s factor deliberately: a centered block still stacks downward from
151
+ * a start position, and it is that start position the caller moves.
152
+ * @param alignment - The def's alignment.
153
+ * @returns The scratch factors, valid until the next call.
154
+ */
155
+ function alignmentFactors(alignment) {
156
+ return _factors.set(HORIZONTAL_FACTORS[alignment.horizontal], VERTICAL_FACTORS[alignment.vertical]);
157
+ }
158
+ /** Which edge a horizontal alignment offsets toward: `-1` left, `0` centered, `1` right. */
159
+ const HORIZONTAL_FACTORS = {
160
+ left: -1,
161
+ center: 0,
162
+ right: 1,
163
+ };
164
+ /** Which edge a vertical alignment offsets toward; `center` shares `top`'s sign (see above). */
165
+ const VERTICAL_FACTORS = {
166
+ top: -1,
167
+ center: -1,
168
+ bottom: 1,
169
+ };
170
+ /**
171
+ * Scratch reused across calls: layout runs once per def per update and never escapes a synchronous
172
+ * block, so it allocates nothing.
173
+ */
174
+ const _half = new Vector2();
175
+ const _offset = new Vector2();
176
+ const _padding = new Vector2();
177
+ const _factors = new Vector2();
178
+ const _position = new Vector2();
179
+ /** The point a rotated block turns about — its own bounds center, which the offsets are relative to. */
180
+ const ORIGIN = new Vector2(0, 0);