lecodes-cli 0.18.2 → 0.19.2

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 (224) hide show
  1. package/dist/index.js +1866 -301
  2. package/package.json +14 -12
  3. package/runtime/scene-harness.json +1 -1
  4. package/runtime/sdk-types.json +1 -1
  5. package/src/api.ts +302 -0
  6. package/src/browserAuth.ts +87 -0
  7. package/src/cmgenTool.ts +104 -0
  8. package/src/commands/app.ts +891 -0
  9. package/src/commands/appAndroid.ts +603 -0
  10. package/src/commands/appDesktop.ts +326 -0
  11. package/src/commands/appDesktopMac.ts +470 -0
  12. package/src/commands/appIcon.ts +187 -0
  13. package/src/commands/appShared.ts +448 -0
  14. package/src/commands/appTemplates.ts +883 -0
  15. package/src/commands/appTemplatesAndroid.ts +599 -0
  16. package/src/commands/appTemplatesGradlew.ts +9 -0
  17. package/src/commands/assets.ts +28 -0
  18. package/src/commands/clone.ts +60 -0
  19. package/src/commands/compile.ts +253 -0
  20. package/src/commands/create.ts +59 -0
  21. package/src/commands/design.ts +863 -0
  22. package/src/commands/designTemplates.ts +11 -0
  23. package/src/commands/desktop.ts +122 -0
  24. package/src/commands/dev.ts +214 -0
  25. package/src/commands/diff.ts +59 -0
  26. package/src/commands/init.ts +191 -0
  27. package/src/commands/install.ts +198 -0
  28. package/src/commands/lightmap.ts +290 -0
  29. package/src/commands/link.ts +147 -0
  30. package/src/commands/login.ts +24 -0
  31. package/src/commands/navmesh.ts +225 -0
  32. package/src/commands/pn.ts +305 -0
  33. package/src/commands/projectTemplates.ts +129 -0
  34. package/src/commands/pull.ts +109 -0
  35. package/src/commands/push.ts +173 -0
  36. package/src/commands/render.ts +414 -0
  37. package/src/commands/scene.ts +148 -0
  38. package/src/commands/shaders.ts +157 -0
  39. package/src/commands/status.ts +29 -0
  40. package/src/commands/test.ts +370 -0
  41. package/src/commands/thumbs.ts +178 -0
  42. package/src/commands/types.ts +87 -0
  43. package/src/commands/update.ts +190 -0
  44. package/src/compile/assetIcons.ts +214 -0
  45. package/src/compile/collect.ts +46 -0
  46. package/src/compile/collectLocal.ts +37 -0
  47. package/src/compile/designCompile.ts +104 -0
  48. package/src/compile/fonts.ts +156 -0
  49. package/src/compile/headlessBundle.ts +129 -0
  50. package/src/compile/nativeStack.ts +21 -0
  51. package/src/compile/projectCompile.ts +109 -0
  52. package/src/compile/sceneCompile.ts +113 -0
  53. package/src/compile/screenEntry.ts +127 -0
  54. package/src/compile/shaders.ts +245 -0
  55. package/src/config.ts +42 -0
  56. package/src/designMeta.ts +35 -0
  57. package/src/desktopRenderer.ts +532 -0
  58. package/src/desktopScript.ts +276 -0
  59. package/src/dev/clientTemplates.ts +160 -0
  60. package/src/dev/devServer.ts +292 -0
  61. package/src/dev/wsServer.ts +144 -0
  62. package/src/distRoot.ts +20 -0
  63. package/src/ignore.ts +163 -0
  64. package/src/index.ts +491 -0
  65. package/src/lecodes-3d-editor.d.ts +41 -0
  66. package/src/lecodes-assets.d.ts +7 -0
  67. package/src/lecodes-design.d.ts +191 -0
  68. package/src/lecodes-renderer.d.ts +131 -0
  69. package/src/localFiles.ts +144 -0
  70. package/src/manifest.ts +46 -0
  71. package/src/matcTool.ts +137 -0
  72. package/src/peers.ts +40 -0
  73. package/src/project.ts +61 -0
  74. package/src/projectEnv.ts +94 -0
  75. package/src/qrcode-terminal.d.ts +9 -0
  76. package/src/releases.ts +125 -0
  77. package/src/serverDiff.ts +78 -0
  78. package/src/textDiff.ts +103 -0
  79. package/src/types.ts +0 -0
  80. package/src/util.ts +146 -0
  81. package/runtime/sdk/animate/animate.ts +0 -238
  82. package/runtime/sdk/animate/bezier.ts +0 -138
  83. package/runtime/sdk/animate/easings.ts +0 -126
  84. package/runtime/sdk/canvas/Canvas.ts +0 -305
  85. package/runtime/sdk/core/Aspect.ts +0 -512
  86. package/runtime/sdk/core/InspectorUI.ts +0 -212
  87. package/runtime/sdk/core/color.ts +0 -66
  88. package/runtime/sdk/core/compWrite.ts +0 -42
  89. package/runtime/sdk/core/events.ts +0 -38
  90. package/runtime/sdk/core/fields.ts +0 -120
  91. package/runtime/sdk/core/registry.ts +0 -23
  92. package/runtime/sdk/core/signals.ts +0 -277
  93. package/runtime/sdk/core/time.ts +0 -81
  94. package/runtime/sdk/g2/Camera2D.ts +0 -40
  95. package/runtime/sdk/g2/CharacterController2D.ts +0 -276
  96. package/runtime/sdk/g2/Node2D.ts +0 -267
  97. package/runtime/sdk/g2/OneWay2D.ts +0 -66
  98. package/runtime/sdk/g2/Physics2D.ts +0 -346
  99. package/runtime/sdk/g2/Scene2D.ts +0 -209
  100. package/runtime/sdk/g2/Shape2D.ts +0 -259
  101. package/runtime/sdk/g2/Sprite.ts +0 -89
  102. package/runtime/sdk/g2/SpriteAnimation.ts +0 -171
  103. package/runtime/sdk/g2/SpriteSheet.ts +0 -166
  104. package/runtime/sdk/g2/Texture2D.ts +0 -47
  105. package/runtime/sdk/g2/Tilemap.ts +0 -41
  106. package/runtime/sdk/g2/Tileset.ts +0 -71
  107. package/runtime/sdk/g2/Trigger2D.ts +0 -77
  108. package/runtime/sdk/g2/autotile.ts +0 -433
  109. package/runtime/sdk/g2/cells.ts +0 -91
  110. package/runtime/sdk/g2/defineScene2d.ts +0 -381
  111. package/runtime/sdk/g2/groups2d.ts +0 -106
  112. package/runtime/sdk/g2/loop.ts +0 -50
  113. package/runtime/sdk/g2/scenarios2d.ts +0 -69
  114. package/runtime/sdk/g2/touch.ts +0 -83
  115. package/runtime/sdk/gl/Camera.ts +0 -160
  116. package/runtime/sdk/gl/CameraPlace.ts +0 -52
  117. package/runtime/sdk/gl/CharacterController.ts +0 -238
  118. package/runtime/sdk/gl/Gearbox.ts +0 -212
  119. package/runtime/sdk/gl/Geometry.ts +0 -279
  120. package/runtime/sdk/gl/IK.ts +0 -193
  121. package/runtime/sdk/gl/InstancedMesh.ts +0 -132
  122. package/runtime/sdk/gl/Light.ts +0 -99
  123. package/runtime/sdk/gl/Lightmap.ts +0 -179
  124. package/runtime/sdk/gl/Material.ts +0 -245
  125. package/runtime/sdk/gl/Mesh.ts +0 -83
  126. package/runtime/sdk/gl/Model.ts +0 -64
  127. package/runtime/sdk/gl/Node.ts +0 -350
  128. package/runtime/sdk/gl/Noise.ts +0 -30
  129. package/runtime/sdk/gl/Particles.ts +0 -676
  130. package/runtime/sdk/gl/Physics.ts +0 -222
  131. package/runtime/sdk/gl/Plane.ts +0 -53
  132. package/runtime/sdk/gl/Ray.ts +0 -16
  133. package/runtime/sdk/gl/Scene.ts +0 -479
  134. package/runtime/sdk/gl/Shape.ts +0 -377
  135. package/runtime/sdk/gl/Texture.ts +0 -46
  136. package/runtime/sdk/gl/Trigger.ts +0 -45
  137. package/runtime/sdk/gl/Vehicle.ts +0 -473
  138. package/runtime/sdk/gl/Wheel.ts +0 -240
  139. package/runtime/sdk/gl/animation/AnimationClip.ts +0 -204
  140. package/runtime/sdk/gl/animation/Animator.ts +0 -87
  141. package/runtime/sdk/gl/animation/Layer.ts +0 -29
  142. package/runtime/sdk/gl/animation/Loop.ts +0 -25
  143. package/runtime/sdk/gl/animation/Playback.ts +0 -43
  144. package/runtime/sdk/gl/animation/core.ts +0 -294
  145. package/runtime/sdk/gl/controls.ts +0 -95
  146. package/runtime/sdk/gl/physicsEvents.ts +0 -20
  147. package/runtime/sdk/gl/scenarios.ts +0 -291
  148. package/runtime/sdk/gl/state.ts +0 -6
  149. package/runtime/sdk/gl/touch.ts +0 -68
  150. package/runtime/sdk/inject.ts +0 -186
  151. package/runtime/sdk/math/Mathf.ts +0 -118
  152. package/runtime/sdk/math/mat4.ts +0 -278
  153. package/runtime/sdk/math/quat.ts +0 -232
  154. package/runtime/sdk/math/vec.ts +0 -255
  155. package/runtime/sdk/plugins/camera.ts +0 -81
  156. package/runtime/sdk/plugins/geolocation.ts +0 -123
  157. package/runtime/sdk/plugins/oauth.ts +0 -61
  158. package/runtime/sdk/plugins/permission.ts +0 -7
  159. package/runtime/sdk/plugins/push.ts +0 -132
  160. package/runtime/sdk/plugins/qr.ts +0 -73
  161. package/runtime/sdk/plugins/service.ts +0 -47
  162. package/runtime/sdk/runtime/app.ts +0 -101
  163. package/runtime/sdk/runtime/appEvents.ts +0 -54
  164. package/runtime/sdk/runtime/channel.ts +0 -61
  165. package/runtime/sdk/runtime/clipboard.ts +0 -20
  166. package/runtime/sdk/runtime/datetime.ts +0 -329
  167. package/runtime/sdk/runtime/device.ts +0 -293
  168. package/runtime/sdk/runtime/fetch.ts +0 -77
  169. package/runtime/sdk/runtime/files.ts +0 -23
  170. package/runtime/sdk/runtime/input.ts +0 -175
  171. package/runtime/sdk/runtime/media.ts +0 -111
  172. package/runtime/sdk/runtime/misc.ts +0 -16
  173. package/runtime/sdk/runtime/net.ts +0 -36
  174. package/runtime/sdk/runtime/rpc.ts +0 -218
  175. package/runtime/sdk/runtime/service.ts +0 -83
  176. package/runtime/sdk/runtime/share.ts +0 -9
  177. package/runtime/sdk/runtime/storage.ts +0 -13
  178. package/runtime/sdk/runtime/touch.ts +0 -76
  179. package/runtime/sdk/scene/defineScene.ts +0 -1227
  180. package/runtime/sdk/scene/editorPlugins.ts +0 -92
  181. package/runtime/sdk/scene/gizmos.ts +0 -148
  182. package/runtime/sdk/scene/grammar.ts +0 -120
  183. package/runtime/sdk/scene/material.ts +0 -188
  184. package/runtime/sdk/server/auth/appConfig.ts +0 -12
  185. package/runtime/sdk/server/auth/global.ts +0 -80
  186. package/runtime/sdk/server/auth/host.ts +0 -318
  187. package/runtime/sdk/server/auth/models.ts +0 -83
  188. package/runtime/sdk/server/auth/types.ts +0 -50
  189. package/runtime/sdk/server/channel.ts +0 -56
  190. package/runtime/sdk/server/context.ts +0 -36
  191. package/runtime/sdk/server/db/defineDb.ts +0 -237
  192. package/runtime/sdk/server/db/fields.ts +0 -132
  193. package/runtime/sdk/server/db/httpTransport.ts +0 -93
  194. package/runtime/sdk/server/db/index.ts +0 -7
  195. package/runtime/sdk/server/db/marci/query.ts +0 -412
  196. package/runtime/sdk/server/db/types.ts +0 -202
  197. package/runtime/sdk/server/errors.ts +0 -12
  198. package/runtime/sdk/server/host.ts +0 -74
  199. package/runtime/sdk/server/inject.ts +0 -13
  200. package/runtime/sdk/server/runtime.ts +0 -133
  201. package/runtime/sdk/server/validate.ts +0 -87
  202. package/runtime/sdk/ui/NativeView.ts +0 -142
  203. package/runtime/sdk/ui/UI.ts +0 -39
  204. package/runtime/sdk/ui/UIBottomSheet.ts +0 -139
  205. package/runtime/sdk/ui/UIButton.ts +0 -101
  206. package/runtime/sdk/ui/UIContainer.ts +0 -60
  207. package/runtime/sdk/ui/UIImage.ts +0 -83
  208. package/runtime/sdk/ui/UIInput.ts +0 -185
  209. package/runtime/sdk/ui/UIModal.ts +0 -139
  210. package/runtime/sdk/ui/UINode.ts +0 -826
  211. package/runtime/sdk/ui/UIPager.ts +0 -362
  212. package/runtime/sdk/ui/UIPopover.ts +0 -100
  213. package/runtime/sdk/ui/UIScreen.ts +0 -123
  214. package/runtime/sdk/ui/UIScrollable.ts +0 -87
  215. package/runtime/sdk/ui/UISpacer.ts +0 -14
  216. package/runtime/sdk/ui/UITabs.ts +0 -236
  217. package/runtime/sdk/ui/UIText.ts +0 -51
  218. package/runtime/sdk/ui/UIVideo.ts +0 -88
  219. package/runtime/sdk/ui/UIVirtualizedList.ts +0 -241
  220. package/runtime/sdk/ui/UIWidget.ts +0 -127
  221. package/runtime/sdk/ui/fonts.ts +0 -13
  222. package/runtime/sdk/ui/presentable.ts +0 -117
  223. package/runtime/sdk/ui/router.ts +0 -132
  224. package/runtime/sdk/ui/theme.ts +0 -84
@@ -1,329 +0,0 @@
1
- // Locale-aware dates, dayjs-style: `date(value).format(...)`. Pure JS (no `Intl` — QuickJS hosts
2
- // don't ship it reliably), so output is byte-identical on every platform. Two locales: English and
3
- // Russian.
4
- //
5
- // ── Designed for chisel fusion ──────────────────────────────────────────────────────────────────
6
- // A date is a *single-scalar* value (epoch milliseconds), which is the 1-component analog of a Vec3
7
- // chain. The wrapper is written so a future chisel date-fuser can lower `date(root).<chain>()` to
8
- // scalar ms arithmetic and drop the wrapper allocation, the same way Vec3 chains fuse:
9
- // • IMMUTABLE — every method returns a NEW `DateValue`, never mutates `t` (aliasing safety).
10
- // • ONE canonical scalar — `t` (epoch ms), also reachable via `valueOf()`; that's the "component".
11
- // • Terminals DELEGATE to the module-private free functions `formatImpl` / `timeAgoImpl`, taking
12
- // `(ms, …)`. So `date(x).format(p)` can fuse to `formatImpl(toMs(x), p)` with no wrapper.
13
- // • Linear-unit arithmetic (ms…week) is closed-form scalar math → fusable; calendar units
14
- // (month/year) and local-tz `startOf`/`endOf` use `Date` and are left to the real method.
15
- // Even before that pass lands, each method is a class method, so chisel's method-granular DCE drops
16
- // whichever ones a project doesn't use.
17
-
18
- export type Locale = "en" | "ru"
19
- export type DateInput = Date | number | string | DateValue
20
- export type Unit = "ms" | "second" | "minute" | "hour" | "day" | "week" | "month" | "year"
21
-
22
- // ---- locale data -------------------------------------------------------------------------------
23
- // Russian needs the month in two grammatical cases: genitive ("5 сентября") when a day number is
24
- // present, nominative ("Сентябрь 2024") when it stands alone. `format` picks by whether the pattern
25
- // contains a day (`D`) token — the same rule day.js's ru locale uses.
26
-
27
- type LocaleData = {
28
- months: readonly string[]
29
- monthsGenitive: readonly string[]
30
- monthsShort: readonly string[]
31
- weekdays: readonly string[] // Sunday..Saturday (JS getDay() order)
32
- weekdaysShort: readonly string[]
33
- am: string
34
- pm: string
35
- }
36
-
37
- const EN: LocaleData = {
38
- months: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
39
- monthsGenitive: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
40
- monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
41
- weekdays: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
42
- weekdaysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
43
- am: "AM",
44
- pm: "PM",
45
- }
46
-
47
- const RU: LocaleData = {
48
- months: ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"],
49
- monthsGenitive: ["января", "февраля", "марта", "апреля", "мая", "июня", "июля", "августа", "сентября", "октября", "ноября", "декабря"],
50
- monthsShort: ["янв", "фев", "мар", "апр", "мая", "июн", "июл", "авг", "сен", "окт", "ноя", "дек"],
51
- weekdays: ["Воскресенье", "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"],
52
- weekdaysShort: ["вс", "пн", "вт", "ср", "чт", "пт", "сб"],
53
- am: "AM",
54
- pm: "PM",
55
- }
56
-
57
- const DATA: Record<Locale, LocaleData> = { en: EN, ru: RU }
58
-
59
- const resolveLocale = (locale?: Locale): Locale => {
60
- if (locale) return locale === "ru" ? "ru" : "en"
61
- const lang = typeof _creatorUtils !== "undefined" ? _creatorUtils.language : ""
62
- return lang && lang.toLowerCase().indexOf("ru") === 0 ? "ru" : "en"
63
- }
64
-
65
- // Milliseconds per linear unit. Absent keys (month/year) mark the non-fusable calendar units.
66
- const MS: Partial<Record<Unit, number>> = {
67
- ms: 1,
68
- second: 1000,
69
- minute: 60_000,
70
- hour: 3_600_000,
71
- day: 86_400_000,
72
- week: 604_800_000,
73
- }
74
-
75
- const toMs = (value: DateInput): number => {
76
- if (value instanceof DateValue) return value.t
77
- if (value instanceof Date) return value.getTime()
78
- if (typeof value === "number") return value
79
- return new Date(value).getTime()
80
- }
81
-
82
- // ---- formatting (the fusion escape target: takes a scalar ms, holds the heavy tables) -----------
83
-
84
- const pad2 = (n: number): string => (n < 10 ? "0" + n : "" + n)
85
-
86
- // Longest tokens first so `MMMM` matches before `MM`, `YYYY` before `YY`, etc.
87
- const TOKEN = /YYYY|YY|MMMM|MMM|MM|M|DD|D|dddd|ddd|HH|H|hh|h|mm|m|ss|s|A|a/g
88
-
89
- const formatImpl = (ms: number, pattern: string, locale?: Locale): string => {
90
- const d = new Date(ms)
91
- const L = DATA[resolveLocale(locale)]
92
- const hasDay = /D/.test(pattern) // genitive month only when a day number accompanies it
93
- const months = hasDay ? L.monthsGenitive : L.months
94
-
95
- const year = d.getFullYear()
96
- const month = d.getMonth()
97
- const day = d.getDate()
98
- const weekday = d.getDay()
99
- const hours = d.getHours()
100
- const hour12 = hours % 12 || 12
101
- const minutes = d.getMinutes()
102
- const seconds = d.getSeconds()
103
-
104
- return pattern.replace(TOKEN, (t) => {
105
- switch (t) {
106
- case "YYYY": return "" + year
107
- case "YY": return pad2(year % 100)
108
- case "MMMM": return months[month]
109
- case "MMM": return L.monthsShort[month]
110
- case "MM": return pad2(month + 1)
111
- case "M": return "" + (month + 1)
112
- case "DD": return pad2(day)
113
- case "D": return "" + day
114
- case "dddd": return L.weekdays[weekday]
115
- case "ddd": return L.weekdaysShort[weekday]
116
- case "HH": return pad2(hours)
117
- case "H": return "" + hours
118
- case "hh": return pad2(hour12)
119
- case "h": return "" + hour12
120
- case "mm": return pad2(minutes)
121
- case "m": return "" + minutes
122
- case "ss": return pad2(seconds)
123
- case "s": return "" + seconds
124
- case "A": return hours < 12 ? L.am : L.pm
125
- case "a": return (hours < 12 ? L.am : L.pm).toLowerCase()
126
- default: return t
127
- }
128
- })
129
- }
130
-
131
- // Russian plural picks one of three forms by the number: 1 минута / 2 минуты / 5 минут.
132
- const ruPlural = (n: number, one: string, few: string, many: string): string => {
133
- const mod10 = n % 10
134
- const mod100 = n % 100
135
- if (mod10 === 1 && mod100 !== 11) return one
136
- if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) return few
137
- return many
138
- }
139
-
140
- type RelUnit = { limit: number; secs: number; en: [string, string]; ru: [string, string, string] }
141
-
142
- // Ordered thresholds. `limit` is the upper bound (in seconds) this unit covers; `secs` is the unit's
143
- // length used to compute the count.
144
- const REL_UNITS: readonly RelUnit[] = [
145
- { limit: 60, secs: 1, en: ["second", "seconds"], ru: ["секунду", "секунды", "секунд"] },
146
- { limit: 3600, secs: 60, en: ["minute", "minutes"], ru: ["минуту", "минуты", "минут"] },
147
- { limit: 86400, secs: 3600, en: ["hour", "hours"], ru: ["час", "часа", "часов"] },
148
- { limit: 2592000, secs: 86400, en: ["day", "days"], ru: ["день", "дня", "дней"] },
149
- { limit: 31536000, secs: 2592000, en: ["month", "months"], ru: ["месяц", "месяца", "месяцев"] },
150
- { limit: Infinity, secs: 31536000, en: ["year", "years"], ru: ["год", "года", "лет"] },
151
- ]
152
-
153
- const timeAgoImpl = (ms: number, locale: Locale | undefined, nowMs: number): string => {
154
- const loc = resolveLocale(locale)
155
- const diffSecs = (nowMs - ms) / 1000
156
- const future = diffSecs < 0
157
- const abs = Math.abs(diffSecs)
158
-
159
- if (abs < 45) return loc === "ru" ? "только что" : "just now"
160
-
161
- let count = 1
162
- let unit = REL_UNITS[REL_UNITS.length - 1]
163
- for (const u of REL_UNITS) {
164
- if (abs < u.limit) { unit = u; count = Math.max(1, Math.round(abs / u.secs)); break }
165
- }
166
-
167
- if (loc === "ru") {
168
- const word = ruPlural(count, unit.ru[0], unit.ru[1], unit.ru[2])
169
- return future ? `через ${count} ${word}` : `${count} ${word} назад`
170
- }
171
- const word = count === 1 ? unit.en[0] : unit.en[1]
172
- return future ? `in ${count} ${word}` : `${count} ${word} ago`
173
- }
174
-
175
- // ---- the immutable wrapper ---------------------------------------------------------------------
176
-
177
- /**
178
- * An immutable moment in time — the value returned by [`date()`]. Wraps a single epoch-millisecond
179
- * scalar (`t`); every method returns a new `DateValue` or a plain scalar, never mutating. Construct
180
- * it with `date(...)`, not `new`.
181
- */
182
- export class DateValue {
183
- /** Epoch milliseconds — the one canonical scalar. Prefer `valueOf()`; this is public so `date(a)`
184
- * can read it back cheaply and so tooling can treat it as the value's single component. */
185
- readonly t: number
186
-
187
- constructor(ms: number) {
188
- this.t = ms
189
- }
190
-
191
- // -- materialize (delegate to the free functions; the fuser can inline these to `*Impl(t, …)`) --
192
-
193
- /**
194
- * Format against a `day.js`-style token pattern (default `"D MMMM YYYY"`). Locale defaults to the
195
- * current device language.
196
- *
197
- * Tokens: `YYYY`/`YY` year · `MMMM`/`MMM`/`MM`/`M` month · `DD`/`D` day · `dddd`/`ddd` weekday ·
198
- * `HH`/`H` 24-hour · `hh`/`h` 12-hour · `mm`/`m` minute · `ss`/`s` second · `A`/`a` AM/PM.
199
- *
200
- * ```ts
201
- * date().format() // "10 July 2026" / "10 июля 2026"
202
- * date(ts).format("DD.MM.YYYY HH:mm") // "10.07.2026 14:05"
203
- * date(ts).format("MMMM YYYY", "ru") // "Июль 2026" (nominative — no day token)
204
- * ```
205
- */
206
- format(pattern = "D MMMM YYYY", locale?: Locale): string {
207
- return formatImpl(this.t, pattern, locale)
208
- }
209
-
210
- /**
211
- * Human relative time vs `now` (default: the current time) — "5 minutes ago", "in 2 days",
212
- * "5 минут назад", "через 2 дня". Reads as "just now" under ~45 seconds; handles Russian number
213
- * agreement (`1 минуту` / `2 минуты` / `5 минут`). Locale defaults to the device language.
214
- */
215
- timeAgo(locale?: Locale, now: DateInput = Date.now()): string {
216
- return timeAgoImpl(this.t, locale, toMs(now))
217
- }
218
-
219
- // -- arithmetic (pure, chainable; linear units are closed-form scalar math → fusable) -----------
220
-
221
- /** Add `n` of a unit, returning a new value. Linear units (`ms`…`week`) are scalar ms math;
222
- * `month`/`year` are calendar-aware (clamp on short months, like day.js). */
223
- add(n: number, unit: Unit): DateValue {
224
- const ms = MS[unit]
225
- if (ms !== undefined) return new DateValue(this.t + n * ms)
226
- // Calendar units: shift month/year but clamp the day so Jan 31 + 1 month → Feb 28 (not Mar 3).
227
- const d = new Date(this.t)
228
- const day = d.getDate()
229
- d.setDate(1)
230
- if (unit === "year") d.setFullYear(d.getFullYear() + n)
231
- else d.setMonth(d.getMonth() + n) // "month"
232
- const daysInMonth = new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate()
233
- d.setDate(Math.min(day, daysInMonth))
234
- return new DateValue(d.getTime())
235
- }
236
-
237
- /** Subtract `n` of a unit. Equivalent to `add(-n, unit)`. */
238
- subtract(n: number, unit: Unit): DateValue {
239
- return this.add(-n, unit)
240
- }
241
-
242
- /** Snap down to the start of a unit (local time): start of day/hour/month/year, etc. */
243
- startOf(unit: Unit): DateValue {
244
- const d = new Date(this.t)
245
- switch (unit) {
246
- case "year": d.setMonth(0); d.setDate(1); d.setHours(0, 0, 0, 0); break
247
- case "month": d.setDate(1); d.setHours(0, 0, 0, 0); break
248
- case "day": d.setHours(0, 0, 0, 0); break
249
- case "hour": d.setMinutes(0, 0, 0); break
250
- case "minute": d.setSeconds(0, 0); break
251
- case "second": d.setMilliseconds(0); break
252
- case "week": {
253
- d.setHours(0, 0, 0, 0)
254
- d.setDate(d.getDate() - d.getDay()) // week starts Sunday
255
- break
256
- }
257
- }
258
- return new DateValue(d.getTime())
259
- }
260
-
261
- /** Snap up to the end of a unit (local time): the last millisecond of the day/hour/month/… */
262
- endOf(unit: Unit): DateValue {
263
- return this.startOf(unit).add(1, unit).add(-1, "ms")
264
- }
265
-
266
- // -- scalar terminals (bare number / boolean → allocate nothing when fused) ---------------------
267
-
268
- /** Epoch milliseconds. Enables `+date(x)`, `date(a) < date(b)`, `date(a) - date(b)` via coercion. */
269
- valueOf(): number {
270
- return this.t
271
- }
272
-
273
- /** Whole seconds since the epoch (Unix time). */
274
- unix(): number {
275
- return Math.floor(this.t / 1000)
276
- }
277
-
278
- /** Signed difference to `other`, in whole `unit`s (default `ms`), truncated toward zero. */
279
- diff(other: DateInput, unit: Unit = "ms"): number {
280
- const raw = this.t - toMs(other)
281
- const per = MS[unit]
282
- if (per !== undefined) return Math.trunc(raw / per)
283
- // month/year: count calendar steps.
284
- const a = new Date(this.t)
285
- const b = new Date(toMs(other))
286
- let months = (a.getFullYear() - b.getFullYear()) * 12 + (a.getMonth() - b.getMonth())
287
- if (a.getDate() < b.getDate()) months -= Math.sign(months) || 0
288
- return unit === "year" ? Math.trunc(months / 12) : months
289
- }
290
-
291
- isBefore(other: DateInput): boolean { return this.t < toMs(other) }
292
- isAfter(other: DateInput): boolean { return this.t > toMs(other) }
293
- isSame(other: DateInput): boolean { return this.t === toMs(other) }
294
-
295
- // -- field accessors (scalar terminals) ---------------------------------------------------------
296
-
297
- year(): number { return new Date(this.t).getFullYear() }
298
- /** 0-based month (0 = January), matching `Date.getMonth`. */
299
- month(): number { return new Date(this.t).getMonth() }
300
- /** Day of the month, 1–31. */
301
- day(): number { return new Date(this.t).getDate() }
302
- /** Day of the week, 0 (Sunday)–6 (Saturday), matching `Date.getDay`. */
303
- weekday(): number { return new Date(this.t).getDay() }
304
- hour(): number { return new Date(this.t).getHours() }
305
- minute(): number { return new Date(this.t).getMinutes() }
306
- second(): number { return new Date(this.t).getSeconds() }
307
-
308
- /** A native `Date` snapshot of this value. */
309
- toDate(): Date {
310
- return new Date(this.t)
311
- }
312
- }
313
-
314
- /**
315
- * Create a `DateValue` — a locale-aware, immutable moment, dayjs-style. Accepts a `Date`, a
316
- * millisecond timestamp (`Date.now()`), a parseable string, or another `DateValue`; with no argument
317
- * it's the current time. Chain arithmetic and finish with `format` / `timeAgo`, both of which
318
- * understand **English** and **Russian** and default to the current device language
319
- * ([`device.language`](../runtime/device.ts)).
320
- *
321
- * ```ts
322
- * date().format("dddd, D MMMM") // "Friday, 10 July" / "пятница, 10 июля"
323
- * date(ts).timeAgo() // "5 minutes ago" / "5 минут назад"
324
- * date().add(3, "day").startOf("day").format("D MMMM")
325
- * date(a).diff(b, "hour") // signed whole hours
326
- * date(a).isBefore(b) // boolean
327
- * ```
328
- */
329
- export const date = (value: DateInput = Date.now()): DateValue => new DateValue(toMs(value))
@@ -1,293 +0,0 @@
1
- // Platform info + the host resize event. The resize listener registers with the host lazily on the
2
- // first subscription (no top-level side effect, so tree-shaking can drop this entirely if unused).
3
-
4
- import { Quat } from "../math/quat"
5
- import { Vec3 } from "../math/vec"
6
- import { appEventsOn, appEventsOff } from "./appEvents"
7
-
8
- type ResizeCallback = (width: number, height: number) => void
9
-
10
- type HdrChangeCallback = (headroom: number) => void
11
-
12
- type DeviceEventMap = {
13
- resize: ResizeCallback
14
- /** Connectivity came back (best-effort, navigator.onLine semantics). */
15
- online: () => void
16
- /** Connectivity was lost. */
17
- offline: () => void
18
- /** The headroom the 3D engine renders to moved (`device.hdr.headroom`): the display ramped up to
19
- * its peak after launch, brightness changed, the window went to another screen. */
20
- hdrchange: HdrChangeCallback
21
- }
22
-
23
- const resizeListeners: ResizeCallback[] = []
24
- let resizeRegistered = false
25
-
26
- const onResize = (width: number, height: number): void => {
27
- for (const cb of resizeListeners.slice()) cb(width, height)
28
- }
29
-
30
- // --- device.hdr state ---
31
- // The 3D bridge is a free global that a 2D-only bundle never defines — reach it defensively. Hosts
32
- // without the methods are SDR by definition; the look setters are remembered so the getters read
33
- // back what the app asked for.
34
- const gl = (): typeof _creator | undefined => (typeof _creator !== "undefined" ? _creator : undefined)
35
- let hdrStrengthLocal = 0.35
36
- let hdrPaperWhiteLocal = 1
37
- // `hdrchange` is a poll: the engine quantises the headroom to half-stops, so a change is rare and
38
- // a 4 Hz timer (only while someone listens) is cheaper than a per-frame bridge call.
39
- const hdrListeners: HdrChangeCallback[] = []
40
- let hdrPollTimer: ReturnType<typeof setInterval> | undefined
41
- let hdrLastHeadroom = 1
42
- const pollHdr = (): void => {
43
- const h = device.hdr.headroom
44
- if (h === hdrLastHeadroom) return
45
- hdrLastHeadroom = h
46
- for (const cb of hdrListeners.slice()) cb(h)
47
- }
48
-
49
- /** Semantic haptic styles for `device.vibrate`. Impact styles (`light`/`medium`/`heavy`/`soft`/
50
- * `rigid`) are a physical "tap" of varying weight; notification styles (`success`/`warning`/`error`)
51
- * cue an outcome; `selection` is a light tick for a value change. Chosen to map 1:1 onto iOS
52
- * `UIFeedbackGenerator` and, on Android, `HapticFeedbackConstants` / `VibrationEffect` — so the same
53
- * call feels native everywhere, rather than a duration that only web/Android can honor. */
54
- export type HapticStyle =
55
- | "light" | "medium" | "heavy" | "soft" | "rigid"
56
- | "success" | "warning" | "error"
57
- | "selection"
58
-
59
- /** Options for `device.motion.start`. */
60
- export interface MotionOptions {
61
- /** Sensor update interval in seconds (default 1/60). The sensor fuses at ≥ this rate in the
62
- * background; you poll the freshest sample each frame, so this is a floor, not a sync. */
63
- interval?: number
64
- /** Frame the readables are delivered in. "world" (default): engine Y-up, screen-oriented — drops
65
- * straight into `camera.quaternion` / any node. "device": the raw sensor frame, no conversion. */
66
- frame?: "world" | "device"
67
- }
68
-
69
- // --- device.motion state (pull-per-frame model) ---
70
- let motionEnabled = false
71
- let motionFrame: "world" | "device" = "world"
72
- let motionYawOffset = 0 // radians about world up, captured by recenter()
73
-
74
- // Calibration constants — VERIFY ON A PHYSICAL DEVICE (the simulator reports no motion).
75
- // • CM_TO_WORLD: Core Motion's reference frame is Z-up; the engine world is Y-up. If a level horizon
76
- // reads tilted ~90°, this is the term to check.
77
- // • ORIENTATION_ROLL: roll about the view axis so camera-up tracks screen-up, indexed by the native
78
- // interface-orientation code [portrait, landscapeLeft, landscapeRight, upsideDown]. If landscape is
79
- // upside-down or rolled the wrong way, flip the ±π/2 signs.
80
- // If the whole panorama turns the wrong way (look right → view goes left), conjugate `raw` below.
81
- const CM_TO_WORLD = Quat.fromAxisAngle([1, 0, 0], -Math.PI / 2)
82
- const ORIENTATION_ROLL = [0, Math.PI / 2, -Math.PI / 2, Math.PI]
83
-
84
- // Device attitude → engine world frame, with the interface-orientation roll folded in (yaw NOT yet
85
- // recentered). Shared by `attitude` (which adds the recenter) and `gravity` (which is yaw-invariant).
86
- const orientedAttitude = (s: ArrayLike<number>): Quat => {
87
- const raw = new Quat(s[0], s[1], s[2], s[3])
88
- const roll = Quat.fromAxisAngle([0, 0, 1], ORIENTATION_ROLL[s[7]] ?? 0)
89
- return CM_TO_WORLD.mul(raw).mul(roll)
90
- }
91
-
92
- export const device = {
93
- /** Which INPUT MODEL the app is running under — the thing to branch on when a build needs
94
- * mouse-look instead of on-screen sticks. It is not the OS name: a Mac reports `"desktop"`,
95
- * like Windows and Linux do (the OS/version detail is host-side, not here). */
96
- get platform(): "web" | "android" | "ios" | "desktop" | string {
97
- return _creatorUtils.platform
98
- },
99
- get language(): string {
100
- return _creatorUtils.language
101
- },
102
- /** Display device-pixel ratio (physical px per logical/CSS px): 1 on standard displays, 2–3 on
103
- * retina / iOS. Bake a Canvas at this ratio for crisp output without hardcoding, e.g.
104
- * `new Canvas(w, h, { pixelRatio: device.pixelRatio })`. NOTE: the web 2D/GL surface currently
105
- * renders at logical resolution, so on web this only helps UI canvases / is forward-looking for
106
- * 2D; on iOS (physical surface) it makes 2D canvases crisp. Falls back to 1 if the host omits it. */
107
- get pixelRatio(): number {
108
- return _creatorUtils.pixelRatio || 1
109
- },
110
- /** Current display width in logical px — the same value the "resize" event delivers, but readable at
111
- * any time (not only inside the listener). 0 until the host has reported a size. */
112
- get width(): number {
113
- return _creatorUI.getDisplaySize?.()?.[0] ?? 0
114
- },
115
- /** Current display height in logical px — the same value the "resize" event delivers, readable at any
116
- * time. 0 until the host has reported a size. */
117
- get height(): number {
118
- return _creatorUI.getDisplaySize?.()?.[1] ?? 0
119
- },
120
- /** Opt into the precise-touch system where the platform supports it. OFF by default. When on, fast
121
- * strokes are sampled at the touch digitizer's full rate (iOS coalesced touches, ≈120–240 Hz) instead
122
- * of once per display frame (~60 Hz), so a pointer-heavy app (drawing, handwriting, dragging) gets more
123
- * points and smoother lines. Leave it off for tap/button UIs. No-op on hosts without a coalesced-input
124
- * concept (web already coalesces pointermove; headless). */
125
- setPreciseTouch(enabled: boolean): void {
126
- _creatorUtils.setPreciseTouch?.(enabled)
127
- },
128
- /** Fire a one-shot haptic of the given semantic `style` (default `"medium"`) — a physical tap on
129
- * supported hardware (iOS Taptic Engine, Android vibrator). Chosen by meaning, not duration, so it
130
- * feels native on each platform; see {@link HapticStyle}. Host-gated: a silent no-op where there's
131
- * no haptic hardware (iPad, older iPhones, web, headless). */
132
- vibrate(style: HapticStyle = "medium"): void {
133
- _creatorUtils.vibrate?.(style)
134
- },
135
- /** Device-orientation sensor (gyro + accelerometer, fused) for tilt/steering and magic-window /
136
- * 360° panoramas. Poll `attitude` / `gravity` inside setLoop; they return the freshest fused
137
- * sample, so the sensor rate need not match your frame rate. Host-gated: a no-op with no sensor. */
138
- motion: {
139
- /** Whether this device has the motion sensors at all (no gyro → false; iPad/older, web, headless). */
140
- get available(): boolean {
141
- return _creatorUtils.motionAvailable?.() ?? false
142
- },
143
- /** Whether updates are currently running (start succeeded and stop hasn't been called). */
144
- get enabled(): boolean {
145
- return motionEnabled
146
- },
147
- /** Begin sensor updates. Resolves to whether it actually started (false = no sensor / denied).
148
- * Async so a web host can await its permission prompt; native resolves immediately. */
149
- async start(options: MotionOptions = {}): Promise<boolean> {
150
- motionFrame = options.frame ?? "world"
151
- motionEnabled = !!(await (_creatorUtils.motionStart?.(options.interval ?? 1 / 60) ?? false))
152
- return motionEnabled
153
- },
154
- /** Stop sensor updates and release the sensor (battery). */
155
- stop(): void {
156
- _creatorUtils.motionStop?.()
157
- motionEnabled = false
158
- },
159
- /** Capture the current heading as "forward" — a yaw-only recenter (pitch/roll stay gravity-
160
- * referenced, so the horizon stays level). No-op in the "device" frame. */
161
- recenter(): void {
162
- const s = _creatorUtils.getMotionSample?.()
163
- if (!s || motionFrame === "device") { motionYawOffset = 0; return }
164
- const f = orientedAttitude(s).rotateVec3([0, 0, -1]) // world-space camera forward
165
- motionYawOffset = Math.atan2(-f.x, -f.z) // heading about world up
166
- },
167
- /** The device's current orientation as a `Quat`. In the "world" frame (default) it's engine Y-up
168
- * and screen-oriented, so `camera.quaternion = device.motion.attitude` is a complete magic-window
169
- * / panorama camera. `Quat.identity` until the first sample arrives / when not running. */
170
- get attitude(): Quat {
171
- const s = _creatorUtils.getMotionSample?.()
172
- if (!s) return Quat.identity
173
- if (motionFrame === "device") return new Quat(s[0], s[1], s[2], s[3])
174
- const q = orientedAttitude(s)
175
- return Quat.fromAxisAngle([0, 1, 0], -motionYawOffset).mul(q) // yaw recenter (world-frame)
176
- },
177
- /** Gravity direction for tilt controls. In the "world" frame it's SCREEN space (x → right, y →
178
- * down, matching clientX/clientY), orientation-aware — a 2D game reads `gravity.x / gravity.y`.
179
- * `(0,0,0)` when not running. */
180
- get gravity(): Vec3 {
181
- const s = _creatorUtils.getMotionSample?.()
182
- if (!s) return new Vec3(0, 0, 0)
183
- if (motionFrame === "device") return new Vec3(s[4], s[5], s[6])
184
- // Re-express the raw device-frame gravity in screen space: undo the interface roll (about the
185
- // screen normal), then flip Y to screen-down. Uses the unambiguous CMDeviceMotion.gravity, so
186
- // it stays correct regardless of the attitude-quaternion convention above.
187
- const unroll = Quat.fromAxisAngle([0, 0, 1], -(ORIENTATION_ROLL[s[7]] ?? 0))
188
- const g = new Vec3(s[4], s[5], s[6]).rotate(unroll)
189
- return new Vec3(g.x, -g.y, g.z)
190
- },
191
- },
192
- /** The display's extended dynamic range — HDR. On a screen with headroom above SDR white (Apple's
193
- * XDR panels; the macOS host today) the 3D surface is float and the engine renders past 1.0:
194
- * 1.0 is the white of the UI and of a diffuse white surface, and the sun, emissives and speculars
195
- * climb up to `headroom` times that. Everywhere else it is SDR: `available` false, `headroom` 1,
196
- * the look setters remembered but invisible.
197
- *
198
- * Branch content on `available`, not on `headroom` — the live value ramps up from 1 over the
199
- * first seconds after launch and follows the brightness keys. Particle `emissive` is the thing
200
- * to raise: it is in post-exposure units (1 = white on screen) and everything above that clips on
201
- * SDR, so an HDR display is the only place a fireball peaking at 8 reads as one. */
202
- hdr: {
203
- /** Whether the surface can show anything above SDR white at all (`maxHeadroom > 1`). */
204
- get available(): boolean {
205
- return device.hdr.maxHeadroom > 1
206
- },
207
- /** The peak the display can reach, as a multiple of SDR white (a 1600-nit XDR panel reports up
208
- * to 16). Constant for the surface; 1 on SDR. */
209
- get maxHeadroom(): number {
210
- const v = gl()?.getDisplayMaxHeadroom?.() ?? 1
211
- return v > 1 ? v : 1
212
- },
213
- /** The headroom the engine renders to right now — the screen's current peak as a multiple of SDR
214
- * white, quantised to half-stops. Moves with brightness and the screen under the window
215
- * (`hdrchange` event); never below 1. */
216
- get headroom(): number {
217
- const v = gl()?.getDisplayHeadroom?.() ?? 1
218
- return v > 1 ? v : 1
219
- },
220
- /** How much of the picture reaches for the headroom, 0..1 (default 0.35). 0 touches only what
221
- * SDR clipped — faithful to the SDR grade, but a mostly-mid-tone frame then looks flat next to
222
- * the one bright spot; 1 lifts nearly everything above black toward the peak. A sunlit exterior
223
- * takes 1, a dim interior wants less. A host may pin it (`LECODES_HDR_STRENGTH`). */
224
- get strength(): number {
225
- return gl()?.getHdrStrength?.() ?? hdrStrengthLocal
226
- },
227
- set strength(v: number) {
228
- const s = Number.isFinite(v) ? Math.min(1, Math.max(0, v)) : 0.35
229
- hdrStrengthLocal = s
230
- gl()?.setHdrStrength?.(s)
231
- },
232
- /** Where white lands, as a multiple of SDR white, 1..8 (default 1) — the "HDR brightness" of a
233
- * console game's calibration screen, where paper white sits at ~200 nits against SDR's ~100.
234
- * It scales the WHOLE picture: at 1 a white wall is as bright as the UI's white, at 2 it is
235
- * twice that, and highlights still climb above it (the engine keeps `headroom / paperWhite` for
236
- * them, clamped at the peak). `strength` decides how much of the picture reaches for the
237
- * peak; this decides where the picture starts. 1.5–2 is the console norm; a game that wants to
238
- * read as "HDR on" rather than "SDR with a brighter sun" wants this over `strength`. A host may
239
- * pin it (`LECODES_HDR_PAPER_WHITE`). */
240
- get paperWhite(): number {
241
- return gl()?.getHdrPaperWhite?.() ?? hdrPaperWhiteLocal
242
- },
243
- set paperWhite(v: number) {
244
- const p = Number.isFinite(v) ? Math.min(8, Math.max(1, v)) : 1
245
- hdrPaperWhiteLocal = p
246
- gl()?.setHdrPaperWhite?.(p)
247
- },
248
- },
249
- /** Current connectivity — best-effort navigator.onLine semantics: `false` only when the platform
250
- * is sure there's no network. `true` on hosts that don't track it. Change events: `"online"` /
251
- * `"offline"`. */
252
- get online(): boolean {
253
- return _creatorUtils.isOnline?.() ?? true
254
- },
255
- addEventListener<E extends keyof DeviceEventMap>(channel: E, callback: DeviceEventMap[E]): void {
256
- if (channel === "online" || channel === "offline") {
257
- appEventsOn(channel, callback as () => void)
258
- return
259
- }
260
- if (channel === "hdrchange") {
261
- hdrListeners.push(callback as HdrChangeCallback)
262
- if (hdrPollTimer === undefined) {
263
- hdrLastHeadroom = device.hdr.headroom
264
- hdrPollTimer = setInterval(pollHdr, 250)
265
- }
266
- return
267
- }
268
- if (channel !== "resize") return
269
- resizeListeners.push(callback as ResizeCallback)
270
- if (!resizeRegistered) {
271
- resizeRegistered = true
272
- _creatorUI.registerResizeEvent(onResize)
273
- }
274
- },
275
- removeEventListener<E extends keyof DeviceEventMap>(channel: E, callback: DeviceEventMap[E]): void {
276
- if (channel === "online" || channel === "offline") {
277
- appEventsOff(channel, callback as () => void)
278
- return
279
- }
280
- if (channel === "hdrchange") {
281
- const i = hdrListeners.indexOf(callback as HdrChangeCallback)
282
- if (i >= 0) hdrListeners.splice(i, 1)
283
- if (hdrListeners.length === 0 && hdrPollTimer !== undefined) {
284
- clearInterval(hdrPollTimer)
285
- hdrPollTimer = undefined
286
- }
287
- return
288
- }
289
- if (channel !== "resize") return
290
- const i = resizeListeners.indexOf(callback as ResizeCallback)
291
- if (i >= 0) resizeListeners.splice(i, 1)
292
- },
293
- }