@wodzik/cubecore 0.1.0

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 (279) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +118 -0
  3. package/analyze/client.d.ts +18 -0
  4. package/analyze/client.js +42 -0
  5. package/analyze/index.d.ts +114 -0
  6. package/analyze/index.js +151 -0
  7. package/analyze/rotations.d.ts +3 -0
  8. package/analyze/rotations.js +15 -0
  9. package/analyze/roux.d.ts +67 -0
  10. package/analyze/roux.js +95 -0
  11. package/analyze/worker.d.ts +6 -0
  12. package/analyze/worker.js +31 -0
  13. package/analyze/zz.d.ts +47 -0
  14. package/analyze/zz.js +102 -0
  15. package/bld/index.d.ts +14 -0
  16. package/bld/index.js +14 -0
  17. package/bld/memo.d.ts +66 -0
  18. package/bld/memo.js +139 -0
  19. package/bld/scheme.d.ts +30 -0
  20. package/bld/scheme.js +35 -0
  21. package/bld/skin.d.ts +26 -0
  22. package/bld/skin.js +46 -0
  23. package/bld/tracker.d.ts +68 -0
  24. package/bld/tracker.js +145 -0
  25. package/bluetooth/clock.d.ts +22 -0
  26. package/bluetooth/clock.js +37 -0
  27. package/bluetooth/cubeSkins.d.ts +31 -0
  28. package/bluetooth/cubeSkins.js +32 -0
  29. package/bluetooth/gyro.d.ts +31 -0
  30. package/bluetooth/gyro.js +46 -0
  31. package/bluetooth/index.d.ts +5 -0
  32. package/bluetooth/index.js +5 -0
  33. package/bluetooth/session.d.ts +140 -0
  34. package/bluetooth/session.js +201 -0
  35. package/bluetooth/simulated.d.ts +52 -0
  36. package/bluetooth/simulated.js +80 -0
  37. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-definitions.d.ts +38 -0
  38. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-definitions.js +51 -0
  39. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-encrypter.d.ts +35 -0
  40. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-encrypter.js +70 -0
  41. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-protocol.d.ts +273 -0
  42. package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-protocol.js +846 -0
  43. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen1.d.ts +38 -0
  44. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen1.js +340 -0
  45. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen234-packet-validate.d.ts +11 -0
  46. package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen234-packet-validate.js +118 -0
  47. package/bluetooth/vendor/smartcube-web-bluetooth/gan-mac-salt.d.ts +2 -0
  48. package/bluetooth/vendor/smartcube-web-bluetooth/gan-mac-salt.js +15 -0
  49. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-cube.d.ts +18 -0
  50. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-cube.js +152 -0
  51. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-timer.d.ts +74 -0
  52. package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-timer.js +161 -0
  53. package/bluetooth/vendor/smartcube-web-bluetooth/gan-write-queue.d.ts +7 -0
  54. package/bluetooth/vendor/smartcube-web-bluetooth/gan-write-queue.js +11 -0
  55. package/bluetooth/vendor/smartcube-web-bluetooth/gatt-characteristic-write.d.ts +2 -0
  56. package/bluetooth/vendor/smartcube-web-bluetooth/gatt-characteristic-write.js +36 -0
  57. package/bluetooth/vendor/smartcube-web-bluetooth/index.d.ts +4 -0
  58. package/bluetooth/vendor/smartcube-web-bluetooth/index.js +4 -0
  59. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.d.ts +12 -0
  60. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.js +122 -0
  61. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/build-picker-options.d.ts +11 -0
  62. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/build-picker-options.js +55 -0
  63. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/default-optional-services.d.ts +21 -0
  64. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/default-optional-services.js +47 -0
  65. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/device-name-scan-filters.d.ts +6 -0
  66. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/device-name-scan-filters.js +24 -0
  67. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/gatt-snapshot.d.ts +6 -0
  68. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/gatt-snapshot.js +57 -0
  69. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-candidates.d.ts +11 -0
  70. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-candidates.js +67 -0
  71. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-moyu32.d.ts +8 -0
  72. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-moyu32.js +98 -0
  73. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-qiyi.d.ts +8 -0
  74. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-qiyi.js +128 -0
  75. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/moyu32-session-crypto.d.ts +5 -0
  76. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/moyu32-session-crypto.js +60 -0
  77. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/normalize-uuid.d.ts +2 -0
  78. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/normalize-uuid.js +8 -0
  79. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/packet-sanity.d.ts +7 -0
  80. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/packet-sanity.js +142 -0
  81. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/profile-rank.d.ts +6 -0
  82. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/profile-rank.js +21 -0
  83. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/types.d.ts +32 -0
  84. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/types.js +1 -0
  85. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/ble-utils.d.ts +6 -0
  86. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/ble-utils.js +68 -0
  87. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/connect.d.ts +3 -0
  88. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/connect.js +178 -0
  89. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/cubie-cube.d.ts +16 -0
  90. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/cubie-cube.js +141 -0
  91. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/index.d.ts +11 -0
  92. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/index.js +10 -0
  93. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocol.d.ts +26 -0
  94. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocol.js +8 -0
  95. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gan.d.ts +3 -0
  96. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gan.js +265 -0
  97. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/giiker.d.ts +3 -0
  98. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/giiker.js +345 -0
  99. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gocube.d.ts +15 -0
  100. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gocube.js +382 -0
  101. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-mhc.d.ts +3 -0
  102. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-mhc.js +352 -0
  103. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-v1.d.ts +57 -0
  104. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-v1.js +243 -0
  105. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu32.d.ts +3 -0
  106. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu32.js +454 -0
  107. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/qiyi.d.ts +3 -0
  108. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/qiyi.js +424 -0
  109. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/types.d.ts +78 -0
  110. package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/types.js +1 -0
  111. package/bluetooth/vendor/smartcube-web-bluetooth/utils.d.ts +45 -0
  112. package/bluetooth/vendor/smartcube-web-bluetooth/utils.js +150 -0
  113. package/cfop/caseData.d.ts +16 -0
  114. package/cfop/caseData.js +88 -0
  115. package/cfop/cases.d.ts +42 -0
  116. package/cfop/cases.js +116 -0
  117. package/cfop/f2l.d.ts +28 -0
  118. package/cfop/f2l.js +88 -0
  119. package/cfop/f2lData.d.ts +15 -0
  120. package/cfop/f2lData.js +49 -0
  121. package/cfop/index.d.ts +41 -0
  122. package/cfop/index.js +110 -0
  123. package/cfop/slots.d.ts +2 -0
  124. package/cfop/slots.js +1 -0
  125. package/core/arrows.d.ts +24 -0
  126. package/core/arrows.js +22 -0
  127. package/core/bytes.d.ts +27 -0
  128. package/core/bytes.js +145 -0
  129. package/core/centers.d.ts +32 -0
  130. package/core/centers.js +83 -0
  131. package/core/checks.d.ts +53 -0
  132. package/core/checks.js +85 -0
  133. package/core/cubies.d.ts +45 -0
  134. package/core/cubies.js +169 -0
  135. package/core/frames.d.ts +61 -0
  136. package/core/frames.js +126 -0
  137. package/core/geometry.d.ts +58 -0
  138. package/core/geometry.js +132 -0
  139. package/core/index.d.ts +19 -0
  140. package/core/index.js +19 -0
  141. package/core/masks.d.ts +32 -0
  142. package/core/masks.js +39 -0
  143. package/core/method.d.ts +35 -0
  144. package/core/method.js +30 -0
  145. package/core/moves.d.ts +48 -0
  146. package/core/moves.js +88 -0
  147. package/core/notation.d.ts +48 -0
  148. package/core/notation.js +226 -0
  149. package/core/physical.d.ts +53 -0
  150. package/core/physical.js +99 -0
  151. package/core/practice.d.ts +62 -0
  152. package/core/practice.js +87 -0
  153. package/core/sequence.d.ts +90 -0
  154. package/core/sequence.js +157 -0
  155. package/core/stages.d.ts +90 -0
  156. package/core/stages.js +21 -0
  157. package/core/state.d.ts +55 -0
  158. package/core/state.js +140 -0
  159. package/core/stateCodec.d.ts +12 -0
  160. package/core/stateCodec.js +51 -0
  161. package/core/stream.d.ts +45 -0
  162. package/core/stream.js +75 -0
  163. package/core/tracker.d.ts +62 -0
  164. package/core/tracker.js +121 -0
  165. package/element/alg.d.ts +45 -0
  166. package/element/alg.js +150 -0
  167. package/element/base.d.ts +2 -0
  168. package/element/base.js +3 -0
  169. package/element/bld.d.ts +82 -0
  170. package/element/bld.js +222 -0
  171. package/element/index.d.ts +8 -0
  172. package/element/index.js +21 -0
  173. package/element/model.d.ts +36 -0
  174. package/element/model.js +64 -0
  175. package/element/player.d.ts +196 -0
  176. package/element/player.js +680 -0
  177. package/element/practice.d.ts +75 -0
  178. package/element/practice.js +150 -0
  179. package/element/scramble.d.ts +50 -0
  180. package/element/scramble.js +107 -0
  181. package/element/sequence.d.ts +126 -0
  182. package/element/sequence.js +254 -0
  183. package/element/styles.d.ts +32 -0
  184. package/element/styles.js +196 -0
  185. package/image/cache.d.ts +21 -0
  186. package/image/cache.js +66 -0
  187. package/image/index.d.ts +2 -0
  188. package/image/index.js +2 -0
  189. package/image/png-node.d.ts +6 -0
  190. package/image/png-node.js +10 -0
  191. package/image/svg.d.ts +36 -0
  192. package/image/svg.js +215 -0
  193. package/lbl/index.d.ts +10 -0
  194. package/lbl/index.js +31 -0
  195. package/methods/index.d.ts +20 -0
  196. package/methods/index.js +33 -0
  197. package/package.json +133 -0
  198. package/petrus/index.d.ts +11 -0
  199. package/petrus/index.js +27 -0
  200. package/react/index.d.ts +176 -0
  201. package/react/index.js +228 -0
  202. package/render/arrows.d.ts +68 -0
  203. package/render/arrows.js +190 -0
  204. package/render/build/attachments.d.ts +35 -0
  205. package/render/build/attachments.js +94 -0
  206. package/render/build/models.d.ts +9 -0
  207. package/render/build/models.js +27 -0
  208. package/render/build/tiles.d.ts +48 -0
  209. package/render/build/tiles.js +194 -0
  210. package/render/gltf.d.ts +10 -0
  211. package/render/gltf.js +106 -0
  212. package/render/index.d.ts +12 -0
  213. package/render/index.js +12 -0
  214. package/render/layers.d.ts +21 -0
  215. package/render/layers.js +24 -0
  216. package/render/pictures.d.ts +46 -0
  217. package/render/pictures.js +80 -0
  218. package/render/pieceModels.d.ts +38 -0
  219. package/render/pieceModels.js +43 -0
  220. package/render/renderer.d.ts +165 -0
  221. package/render/renderer.js +647 -0
  222. package/render/tile.d.ts +114 -0
  223. package/render/tile.js +390 -0
  224. package/render/viewports.d.ts +25 -0
  225. package/render/viewports.js +26 -0
  226. package/roux/cmll.d.ts +35 -0
  227. package/roux/cmll.js +121 -0
  228. package/roux/cmllData.d.ts +12 -0
  229. package/roux/cmllData.js +49 -0
  230. package/roux/index.d.ts +52 -0
  231. package/roux/index.js +175 -0
  232. package/skin/attachments.d.ts +82 -0
  233. package/skin/attachments.js +110 -0
  234. package/skin/index.d.ts +3 -0
  235. package/skin/index.js +3 -0
  236. package/skin/shapes.d.ts +112 -0
  237. package/skin/shapes.js +147 -0
  238. package/skin/skin.d.ts +911 -0
  239. package/skin/skin.js +412 -0
  240. package/solve/client.d.ts +58 -0
  241. package/solve/client.js +56 -0
  242. package/solve/handle.d.ts +6 -0
  243. package/solve/handle.js +38 -0
  244. package/solve/index.d.ts +8 -0
  245. package/solve/index.js +7 -0
  246. package/solve/lse.d.ts +42 -0
  247. package/solve/lse.js +151 -0
  248. package/solve/pieces.d.ts +18 -0
  249. package/solve/pieces.js +24 -0
  250. package/solve/protocol.d.ts +70 -0
  251. package/solve/protocol.js +16 -0
  252. package/solve/scramble.d.ts +76 -0
  253. package/solve/scramble.js +187 -0
  254. package/solve/stage.d.ts +108 -0
  255. package/solve/stage.js +532 -0
  256. package/solve/twophase.d.ts +36 -0
  257. package/solve/twophase.js +229 -0
  258. package/solve/worker.d.ts +6 -0
  259. package/solve/worker.js +15 -0
  260. package/timeline/codec.d.ts +23 -0
  261. package/timeline/codec.js +84 -0
  262. package/timeline/index.d.ts +7 -0
  263. package/timeline/index.js +7 -0
  264. package/timeline/recording.d.ts +19 -0
  265. package/timeline/recording.js +20 -0
  266. package/timeline/replay.d.ts +68 -0
  267. package/timeline/replay.js +145 -0
  268. package/timeline/segments.d.ts +31 -0
  269. package/timeline/segments.js +39 -0
  270. package/timeline/share.d.ts +30 -0
  271. package/timeline/share.js +62 -0
  272. package/timeline/stats.d.ts +34 -0
  273. package/timeline/stats.js +56 -0
  274. package/timeline/timings.d.ts +30 -0
  275. package/timeline/timings.js +54 -0
  276. package/zz/index.d.ts +29 -0
  277. package/zz/index.js +68 -0
  278. package/zz/seeds.d.ts +9 -0
  279. package/zz/seeds.js +29 -0
@@ -0,0 +1,680 @@
1
+ /**
2
+ * <cube-player> — a cube that plays an algorithm or a recorded solve, with
3
+ * optional controls and progress bar. Three levels of customisation:
4
+ *
5
+ * 1. use it as is: <cube-player alg="R U R' U'" progress></cube-player>
6
+ * 2. restyle it: CSS variables (--cc-accent…) and ::part(button-play)… — see styles.ts
7
+ * 3. bring your own controls: <cube-player controls="none"> or a child with slot="controls",
8
+ * driven by play() / pause() / seek() / stepForward()… and events.
9
+ *
10
+ * Attributes: alg, setup, anchor ("start": play the algorithm from the
11
+ * (set-up) cube — default; "end": the algorithm SOLVES it, so the cube
12
+ * starts at setup + inverse of the algorithm), tempo (moves per second,
13
+ * default 2), skin (preset name), theme ("light" | "dark" | "auto" — the
14
+ * skin's adjustments for the page; auto follows the system), back-view, visualization ("3d" | "net" |
15
+ * "top" — the 2D views are SVG pictures from @cubecore/image), controls
16
+ * ("default" | "none"), max-pause (ms: recorded pauses longer than this are
17
+ * shortened in the replay), progress (show the bar), markers (ticks at section
18
+ * ends), segment-labels (section names under the bar — click one to jump to
19
+ * it), tooltips ("off" to hide the section popup).
20
+ *
21
+ * Sections: `segments` (any [{ start, end, label, id?, detail?, split?,
22
+ * moves?, color? }] — see @cubecore/timeline Segment), or `method`, which
23
+ * computes them from the recording (stageSegments: any Method, colour
24
+ * neutral). The bar colours each section (palette --cc-segment-1…8 or the
25
+ * segment's colour), hatches the recognition part (before `split`), and
26
+ * shows a popup over the section under the pointer / at the playhead
27
+ * (`formatSegment` for your own text). PageUp / PageDown jump between sections.
28
+ * Properties: recording (a timed solve — plays in real time), alg, setup,
29
+ * skin, mask, method (sections from it), segments, formatSegment, markers, speeds, rate,
30
+ * currentTime, duration, playing, renderer.
31
+ * Events: timeupdate {time, duration, applied}, play, pause, ended,
32
+ * error {message} (e.g. an algorithm that doesn't parse — the cube then shows the setup),
33
+ * segmentchange {index, segment} (the playhead entered another section),
34
+ * load (what plays changed — alg, setup, recording…).
35
+ *
36
+ * Live: `attach(session)` follows a smart cube (a @cubecore/bluetooth
37
+ * SmartCubeSession, or anything with the same events) — its moves animate,
38
+ * resyncs jump, the gyro turns the cube; replay controls hide (`live`
39
+ * attribute). `detach()` goes back to playback.
40
+ */
41
+ import { applyMoves, parseAlg, solvedState, spinsAfter } from "../core/index.js";
42
+ import { renderSvg } from "../image/index.js";
43
+ import { CubeRenderer, showPosition } from "../render/index.js";
44
+ import { SKINS } from "../skin/index.js";
45
+ import { ReplayClock, compressPauses, segmentAt, segmentPlayed, stageSegments } from "../timeline/index.js";
46
+ import { formatTime, fraction, segmentText, startMoves, stepTime, tempoRecording } from "./model.js";
47
+ import { ICONS, STYLES } from "./styles.js";
48
+ import { ElementBase } from "./base.js";
49
+ const TEMPLATE = `
50
+ <style>${STYLES}</style>
51
+ <div class="stage" part="stage"><div class="flat" part="flat" hidden></div></div>
52
+ <div class="progress" part="progress" role="slider" tabindex="0" aria-label="Position" aria-valuemin="0">
53
+ <div class="tooltip" part="tooltip" role="tooltip" hidden></div>
54
+ <div class="track" part="progress-track">
55
+ <div class="fill" part="progress-fill"></div>
56
+ <div class="segments"></div>
57
+ <div class="markers"></div>
58
+ <div class="thumb" part="progress-thumb"></div>
59
+ </div>
60
+ </div>
61
+ <div class="labels" part="segment-labels"></div>
62
+ <slot name="controls">
63
+ <div class="controls" part="controls">
64
+ <span class="time" part="time">0.00 / 0.00</span>
65
+ <div class="buttons" part="buttons">
66
+ <button class="start" part="button button-start" data-action="start" aria-label="To start" title="To start (Home)">${ICONS.start}</button>
67
+ <button class="back" part="button button-back" data-action="back" aria-label="Previous move" title="Previous move (←)">${ICONS.back}</button>
68
+ <button class="play" part="button button-play" data-action="toggle" aria-label="Play" title="Play / pause (Space)">${ICONS.play}</button>
69
+ <button class="forward" part="button button-forward" data-action="forward" aria-label="Next move" title="Next move (→)">${ICONS.forward}</button>
70
+ <button class="end" part="button button-end" data-action="end" aria-label="To end" title="To end (End)">${ICONS.end}</button>
71
+ </div>
72
+ <button class="speed" part="button button-speed" data-action="speed" aria-label="Speed" title="Playback speed">1×</button>
73
+ </div>
74
+ </slot>`;
75
+ export class CubePlayer extends ElementBase {
76
+ static observedAttributes = ["alg", "setup", "anchor", "tempo", "skin", "back-view", "visualization", "theme", "max-pause"];
77
+ darkQuery = typeof matchMedia === "function" ? matchMedia("(prefers-color-scheme: dark)") : null;
78
+ onScheme = () => this.applyTheme();
79
+ root;
80
+ _renderer = null;
81
+ connected = false;
82
+ /** The smart cube followed (attach), if any. */
83
+ liveSource = null;
84
+ turnArrows = null;
85
+ clock = null;
86
+ unsubscribe = null;
87
+ rec = { scramble: [], moves: [], totalMs: 0 };
88
+ start = solvedState();
89
+ startMoves = [];
90
+ _recording = null;
91
+ _skin = null;
92
+ _mask = null;
93
+ _method = null;
94
+ _markers = null;
95
+ _segments = null;
96
+ segs = [];
97
+ currentSegment = -1;
98
+ tooltipHide = 0;
99
+ /** Tooltip text of a section (default: label · detail · time (recognition · execution) · moves). */
100
+ formatSegment = segmentText;
101
+ _speeds = [0.5, 1, 2];
102
+ _rate = 1;
103
+ _setupState = null;
104
+ liveOff = null;
105
+ constructor() {
106
+ super();
107
+ this.root = this.attachShadow({ mode: "open" });
108
+ this.root.innerHTML = TEMPLATE;
109
+ this.root.addEventListener("click", (e) => this.onClick(e));
110
+ this.addEventListener("keydown", (e) => this.onKey(e));
111
+ this.wireProgress();
112
+ }
113
+ connectedCallback() {
114
+ if (!this.hasAttribute("tabindex"))
115
+ this.tabIndex = 0;
116
+ if (!this.connected) {
117
+ this.connected = true;
118
+ this.darkQuery?.addEventListener("change", this.onScheme);
119
+ }
120
+ this.syncRenderer();
121
+ this.load();
122
+ }
123
+ disconnectedCallback() {
124
+ this.connected = false;
125
+ this.darkQuery?.removeEventListener("change", this.onScheme);
126
+ this.clock?.pause();
127
+ this.unsubscribe?.();
128
+ this._renderer?.dispose();
129
+ this._renderer = null;
130
+ }
131
+ /**
132
+ * The WebGL renderer exists only while the 3D view is shown: a page with
133
+ * many 2D pictures (case lists) must not use up the browser's WebGL
134
+ * contexts (~16 — past that the oldest canvas goes blank).
135
+ */
136
+ syncRenderer() {
137
+ const want = this.connected && !is2dView(this.getAttribute("visualization"));
138
+ if (want && !this._renderer) {
139
+ const r = new CubeRenderer(this.$(".stage"), { skin: this.resolveSkin(), theme: this.resolvedTheme() });
140
+ this._renderer = r;
141
+ const bv = this.getAttribute("back-view");
142
+ if (bv)
143
+ r.setBackView(bv);
144
+ if (this._mask)
145
+ r.setMask(this._mask);
146
+ if (this.turnArrows)
147
+ r.setTurnArrows(this.turnArrows.arrows, this.turnArrows.style);
148
+ if (this.liveSource)
149
+ r.setState(this.liveSource.state);
150
+ else if (this.rec)
151
+ showPosition(r, this.start, this.rec.moves.map((m) => m.move), this.lastPos, spinsAfter(solvedState(), this.startMoves));
152
+ }
153
+ else if (!want && this._renderer) {
154
+ this._renderer.dispose();
155
+ this._renderer = null;
156
+ }
157
+ }
158
+ attributeChangedCallback(name) {
159
+ if (!this.connected)
160
+ return;
161
+ if (name === "skin") {
162
+ this._renderer?.setSkin(this.resolveSkin());
163
+ this.redraw();
164
+ }
165
+ else if (name === "visualization") {
166
+ this.syncRenderer();
167
+ this.redraw();
168
+ }
169
+ else if (name === "theme")
170
+ this.applyTheme();
171
+ else if (name === "back-view")
172
+ this._renderer?.setBackView(this.getAttribute("back-view") ?? "none");
173
+ else
174
+ this.load();
175
+ }
176
+ // ─── data ───
177
+ /** A timed solve: plays in real time (pauses stay pauses). Takes precedence over `alg`. */
178
+ get recording() {
179
+ return this._recording;
180
+ }
181
+ set recording(r) {
182
+ this._recording = r;
183
+ this.load();
184
+ }
185
+ get alg() {
186
+ return this.getAttribute("alg") ?? "";
187
+ }
188
+ set alg(v) {
189
+ this.setAttribute("alg", v);
190
+ }
191
+ /** Moves (or a state) that set the cube up before it plays. */
192
+ get setup() {
193
+ return this.getAttribute("setup") ?? "";
194
+ }
195
+ set setup(v) {
196
+ if (typeof v === "string") {
197
+ this._setupState = null;
198
+ this.setAttribute("setup", v);
199
+ }
200
+ else {
201
+ this._setupState = new Uint8Array(v);
202
+ this.removeAttribute("setup");
203
+ this.load();
204
+ }
205
+ }
206
+ get skin() {
207
+ return this.resolveSkin();
208
+ }
209
+ set skin(s) {
210
+ if (typeof s === "string") {
211
+ this._skin = null;
212
+ this.setAttribute("skin", s);
213
+ }
214
+ else {
215
+ this._skin = s;
216
+ this._renderer?.setSkin(s);
217
+ this.redraw();
218
+ }
219
+ }
220
+ get mask() {
221
+ return this._mask;
222
+ }
223
+ set mask(m) {
224
+ this._mask = m;
225
+ this._renderer?.setMask(m);
226
+ this.redraw();
227
+ }
228
+ /** Sections come from this method's analysis of the recording (colour neutral), unless `segments` are given. */
229
+ get method() {
230
+ return this._method;
231
+ }
232
+ set method(m) {
233
+ this._method = m;
234
+ this.renderSections();
235
+ }
236
+ /** Explicit sections of the bar (override `method`). */
237
+ get segments() {
238
+ return [...this.segs];
239
+ }
240
+ set segments(s) {
241
+ this._segments = s;
242
+ this.renderSections();
243
+ }
244
+ /** Explicit tick marks (default: the ends of the sections). */
245
+ get markers() {
246
+ return this._markers;
247
+ }
248
+ set markers(m) {
249
+ this._markers = m;
250
+ this.renderSections();
251
+ }
252
+ /** Speeds the speed button cycles through. */
253
+ get speeds() {
254
+ return [...this._speeds];
255
+ }
256
+ set speeds(v) {
257
+ this._speeds = v.length ? [...v] : [1];
258
+ }
259
+ /** The underlying renderer, for anything the element doesn't expose. */
260
+ get renderer() {
261
+ return this._renderer;
262
+ }
263
+ // ─── playback API ───
264
+ get playing() {
265
+ return this.clock?.isPlaying ?? false;
266
+ }
267
+ get currentTime() {
268
+ return this.clock?.currentTime ?? 0;
269
+ }
270
+ get duration() {
271
+ return this.rec.totalMs;
272
+ }
273
+ get rate() {
274
+ return this._rate;
275
+ }
276
+ set rate(r) {
277
+ this._rate = r;
278
+ if (this.clock)
279
+ this.clock.rate = r;
280
+ this.$(".speed").textContent = `${r}×`;
281
+ }
282
+ play() {
283
+ if (!this.clock || this.playing)
284
+ return;
285
+ this.clock.play();
286
+ this.dispatchEvent(new Event("play"));
287
+ this.updateUi(this.currentTime);
288
+ }
289
+ pause() {
290
+ if (!this.clock || !this.playing)
291
+ return;
292
+ this.clock.pause();
293
+ this.dispatchEvent(new Event("pause"));
294
+ this.updateUi(this.currentTime);
295
+ }
296
+ toggle() {
297
+ if (this.playing)
298
+ this.pause();
299
+ else
300
+ this.play();
301
+ }
302
+ seek(ms) {
303
+ this.clock?.seek(ms);
304
+ }
305
+ /** Jump to the moment `k` moves are done (0 = the start). */
306
+ seekToMove(k) {
307
+ this.pause();
308
+ this.seek(k <= 0 ? 0 : (this.rec.moves[Math.min(k, this.rec.moves.length) - 1]?.t ?? 0));
309
+ }
310
+ /** Moves done at the current time. */
311
+ get applied() {
312
+ return this.lastPos.applied;
313
+ }
314
+ /** The moves being played (the algorithm, or the recording's moves). */
315
+ get moves() {
316
+ return this.rec.moves.map((m) => m.move);
317
+ }
318
+ stepForward() {
319
+ this.pause();
320
+ this.seek(stepTime(this.rec, this.currentTime, 1));
321
+ }
322
+ stepBack() {
323
+ this.pause();
324
+ this.seek(stepTime(this.rec, this.currentTime, -1));
325
+ }
326
+ toStart() {
327
+ this.pause();
328
+ this.seek(0);
329
+ }
330
+ toEnd() {
331
+ this.pause();
332
+ this.seek(this.duration);
333
+ }
334
+ // ─── live ───
335
+ /** Follow a smart cube: its moves animate, resyncs jump, its gyro turns the cube. Returns detach. */
336
+ attach(source, options = {}) {
337
+ this.detach();
338
+ this.pause();
339
+ this.unsubscribe?.();
340
+ this.unsubscribe = null;
341
+ this.setAttribute("live", "");
342
+ if (options.autoSkin && source.suggestedSkin)
343
+ this.skin = source.suggestedSkin;
344
+ const r = () => this._renderer;
345
+ this.liveSource = source;
346
+ r()?.setState(source.state);
347
+ const offs = [
348
+ source.on("move", (e) => void r()?.animate(e.move)),
349
+ source.on("state", (e) => {
350
+ if (e.reason !== "move")
351
+ r()?.setState(e.state);
352
+ }),
353
+ ];
354
+ if (options.gyro ?? true)
355
+ offs.push(source.on("orientation", (q) => r()?.setOrientation(q, options.gyroSmoothing ?? 0.6)));
356
+ this.liveOff = () => offs.forEach((off) => off());
357
+ return () => this.detach();
358
+ }
359
+ /** Stop following a live cube and go back to playback. */
360
+ detach() {
361
+ if (!this.liveOff)
362
+ return;
363
+ this.liveOff();
364
+ this.liveOff = null;
365
+ this.liveSource = null;
366
+ this.removeAttribute("live");
367
+ this._renderer?.setOrientation(null);
368
+ this.load();
369
+ }
370
+ /**
371
+ * Arrows for the next turn on the 3D cube (a tracker's `nextTurn().arrows`).
372
+ * `owner` (e.g. the <cube-scramble> setting them): clearing with null only
373
+ * works for the one that set them last, so two elements can share a player.
374
+ */
375
+ showTurnArrows(arrows, style = {}, owner = null) {
376
+ if (!arrows && this.turnArrows && owner !== null && this.turnArrows.owner !== owner)
377
+ return;
378
+ this.turnArrows = arrows?.length ? { arrows, style, owner } : null;
379
+ this._renderer?.setTurnArrows(arrows, style);
380
+ }
381
+ /** Animate one move now (live input without a session). */
382
+ pushMove(move) {
383
+ for (const m of typeof move === "string" ? parseAlg(move) : [move])
384
+ void this._renderer?.animate(m);
385
+ }
386
+ // ─── internals ───
387
+ $(sel) {
388
+ return this.root.querySelector(sel);
389
+ }
390
+ resolveSkin() {
391
+ if (this._skin)
392
+ return this._skin;
393
+ const name = this.getAttribute("skin");
394
+ return (name && SKINS[name]) || SKINS.default;
395
+ }
396
+ load() {
397
+ if (!this.connected || this.liveOff)
398
+ return;
399
+ const wasPlaying = this.playing;
400
+ this.clock?.pause();
401
+ this.unsubscribe?.();
402
+ let maxAnimMs = 150;
403
+ let solves = [];
404
+ try {
405
+ if (this._recording) {
406
+ // max-pause: long recognitions shortened (ms), so replays keep moving.
407
+ const cap = Number(this.getAttribute("max-pause"));
408
+ this.rec = cap > 0 ? compressPauses(this._recording, cap) : this._recording;
409
+ }
410
+ else {
411
+ const tempo = Number(this.getAttribute("tempo") ?? 2) || 2;
412
+ const t = tempoRecording(this.alg, tempo);
413
+ this.rec = t.recording;
414
+ maxAnimMs = t.interval * 0.8;
415
+ // anchor="end": the algorithm solves the cube — start from its inverse.
416
+ solves = startMoves([], t.recording.moves.map((m) => m.move), this.getAttribute("anchor") === "end" ? "end" : "start");
417
+ }
418
+ const setup = this._setupState ? [] : parseAlg(this.setup);
419
+ this.startMoves = [...setup, ...solves, ...this.rec.scramble];
420
+ this.start = this._setupState ? applyMoves(this._setupState, [...solves, ...this.rec.scramble]) : applyMoves(solvedState(), this.startMoves);
421
+ }
422
+ catch (err) {
423
+ // Bad notation: show the cube as set up (or solved) and tell the page.
424
+ this.rec = { scramble: [], moves: [], totalMs: 0 };
425
+ this.startMoves = [];
426
+ this.start = this._setupState ?? solvedState();
427
+ this.dispatchEvent(new CustomEvent("error", { detail: { message: err instanceof Error ? err.message : String(err) } }));
428
+ }
429
+ this.clock = new ReplayClock(this.rec, { maxAnimMs });
430
+ this.clock.rate = this._rate;
431
+ this.unsubscribe = this.clock.onChange((time, pos) => this.onTime(time, pos));
432
+ this.renderSections();
433
+ this.clock.seek(0);
434
+ this.dispatchEvent(new Event("load"));
435
+ if (wasPlaying)
436
+ this.play();
437
+ }
438
+ lastPos = { applied: 0 };
439
+ /** Play / pause icon currently shown (null: not drawn yet). */
440
+ shownPlaying = null;
441
+ onTime(time, pos) {
442
+ this.lastPos = pos;
443
+ if (this._renderer)
444
+ showPosition(this._renderer, this.start, this.rec.moves.map((m) => m.move), pos, spinsAfter(solvedState(), this.startMoves));
445
+ this.redraw();
446
+ this.updateUi(time);
447
+ this.dispatchEvent(new CustomEvent("timeupdate", { detail: { time, duration: this.duration, applied: pos.applied } }));
448
+ // The clock reports the final frame while still playing, then stops: that frame is the end.
449
+ if (time >= this.duration && this.duration > 0 && this.playing) {
450
+ queueMicrotask(() => {
451
+ this.updateUi(time);
452
+ this.dispatchEvent(new Event("ended"));
453
+ });
454
+ }
455
+ }
456
+ /** "light" / "dark" from the theme attribute; "auto" (default) follows the system setting. */
457
+ resolvedTheme() {
458
+ const t = this.getAttribute("theme");
459
+ if (t === "light" || t === "dark")
460
+ return t;
461
+ return this.darkQuery && !this.darkQuery.matches ? "light" : "dark";
462
+ }
463
+ applyTheme() {
464
+ this._renderer?.setTheme(this.resolvedTheme());
465
+ this.redraw();
466
+ }
467
+ /** 2D visualizations: an SVG of the position (moves applied so far) with the same skin and mask. */
468
+ redraw() {
469
+ const kind = this.getAttribute("visualization") ?? "3d";
470
+ const flat = this.$(".flat");
471
+ const canvas = this._renderer?.canvas;
472
+ const is2d = is2dView(kind);
473
+ flat.hidden = !is2d;
474
+ if (canvas)
475
+ canvas.style.visibility = is2d ? "hidden" : "";
476
+ if (!is2d)
477
+ return;
478
+ const played = this.rec.moves.slice(0, this.lastPos.applied).map((m) => m.move);
479
+ const state = applyMoves(this.start, played);
480
+ const spins = spinsAfter(solvedState(), [...this.startMoves, ...played]);
481
+ flat.innerHTML = renderSvg(state, { view: kind, skin: this.resolveSkin(), theme: this.resolvedTheme(), spins, ...(this._mask ? { mask: this._mask } : {}) });
482
+ }
483
+ updateUi(time) {
484
+ const f = fraction(time, this.duration);
485
+ this.$(".fill").style.width = `${f * 100}%`;
486
+ this.$(".thumb").style.left = `${f * 100}%`;
487
+ const progress = this.$(".progress");
488
+ progress.setAttribute("aria-valuemax", String(Math.round(this.duration)));
489
+ progress.setAttribute("aria-valuenow", String(Math.round(time)));
490
+ progress.setAttribute("aria-valuetext", `${formatTime(time)} of ${formatTime(this.duration)}`);
491
+ this.$(".time").textContent = `${formatTime(time)} / ${formatTime(this.duration)}`;
492
+ // Swap the icon only when the state changes: rewriting it every frame replaced the element under the
493
+ // pointer mid-click, and the browser then drops the click (pause "sometimes didn't work").
494
+ if (this.shownPlaying !== this.playing) {
495
+ this.shownPlaying = this.playing;
496
+ const play = this.$(".play");
497
+ play.innerHTML = this.playing ? ICONS.pause : ICONS.play;
498
+ play.setAttribute("aria-label", this.playing ? "Pause" : "Play");
499
+ }
500
+ const atStart = time <= 0, atEnd = time >= this.duration;
501
+ this.$(".start").disabled = atStart;
502
+ this.$(".back").disabled = atStart;
503
+ this.$(".forward").disabled = atEnd;
504
+ this.$(".end").disabled = atEnd;
505
+ this.updateSections(time);
506
+ }
507
+ // ─── sections ───
508
+ renderSections() {
509
+ this.segs = this._segments ?? (this._method && this.rec.moves.length ? stageSegments(this._method, this.rec) : []);
510
+ this.currentSegment = -1;
511
+ const pct = (t) => fraction(t, this.duration) * 100;
512
+ const partId = (s) => (s.id ? ` segment-${s.id.replace(/[^\w-]/g, "")}` : "");
513
+ this.$(".progress").classList.toggle("has-segments", this.segs.length > 0);
514
+ this.$(".segments").innerHTML = this.segs
515
+ .map((s, i) => {
516
+ const colour = s.color ?? `var(--cc-segment-${(i % 8) + 1})`;
517
+ const rec = s.split !== undefined && s.end > s.start ? ((s.split - s.start) / (s.end - s.start)) * 100 : 0;
518
+ return `<div class="segment" part="segment${partId(s)}" style="left:${pct(s.start)}%;width:${pct(s.end) - pct(s.start)}%;--seg:${colour}">
519
+ <div class="played" part="segment-played"></div>
520
+ ${rec > 0 ? `<div class="recognition" part="segment-recognition" style="width:${rec}%"></div>` : ""}
521
+ </div>`;
522
+ })
523
+ .join("");
524
+ this.$(".labels").innerHTML = this.segs
525
+ .map((s, i) => `<button class="label" part="segment-label${s.id ? ` segment-label-${s.id.replace(/[^\w-]/g, "")}` : ""}" data-segment="${i}" style="left:${pct(s.start)}%;width:${pct(s.end) - pct(s.start)}%" title="${this.formatSegment(s).replace(/"/g, "&quot;")}">${s.label}</button>`)
526
+ .join("");
527
+ // Ticks: explicit markers, else the ends of the sections.
528
+ const marks = this._markers ?? this.segs.slice(0, -1).map((s) => ({ time: s.end, label: s.label }));
529
+ this.$(".markers").innerHTML = marks
530
+ .map((m) => `<div class="marker" part="progress-marker" title="${`${m.label} · ${formatTime(m.time)}`.replace(/"/g, "&quot;")}" style="left:${pct(m.time)}%"></div>`)
531
+ .join("");
532
+ this.updateSections(this.currentTime);
533
+ }
534
+ updateSections(time) {
535
+ const els = this.root.querySelectorAll(".segment .played");
536
+ this.segs.forEach((s, i) => els[i] && (els[i].style.width = `${segmentPlayed(s, time) * 100}%`));
537
+ const index = segmentAt(this.segs, time);
538
+ if (index === this.currentSegment)
539
+ return;
540
+ this.currentSegment = index;
541
+ this.root.querySelectorAll(".label").forEach((el, i) => el.classList.toggle("current", i === index));
542
+ if (index >= 0)
543
+ this.dispatchEvent(new CustomEvent("segmentchange", { detail: { index, segment: this.segs[index] } }));
544
+ }
545
+ /** Show the popup of section `index` (−1 hides it). */
546
+ showTooltip(index, autoHideMs = 0) {
547
+ const tip = this.$(".tooltip");
548
+ clearTimeout(this.tooltipHide);
549
+ if (index < 0 || this.getAttribute("tooltips") === "off" || !this.segs[index]) {
550
+ tip.hidden = true;
551
+ return;
552
+ }
553
+ const s = this.segs[index];
554
+ tip.innerHTML = "";
555
+ const text = document.createElement("span");
556
+ text.className = "tip";
557
+ text.setAttribute("part", "tooltip-text");
558
+ text.textContent = this.formatSegment(s);
559
+ tip.appendChild(text);
560
+ tip.hidden = false;
561
+ const track = this.$(".track").getBoundingClientRect();
562
+ const centre = ((s.start + s.end) / 2 / (this.duration || 1)) * track.width;
563
+ const half = tip.offsetWidth / 2;
564
+ tip.style.left = `${Math.min(Math.max(centre, half), Math.max(half, track.width - half))}px`;
565
+ if (autoHideMs)
566
+ this.tooltipHide = window.setTimeout(() => (tip.hidden = true), autoHideMs);
567
+ }
568
+ /** Start of the next (+1) / this-or-previous (−1) section. */
569
+ jumpSection(direction) {
570
+ if (!this.segs.length)
571
+ return;
572
+ const t = this.currentTime;
573
+ const i = Math.max(0, segmentAt(this.segs, t));
574
+ const target = direction > 0 ? (this.segs[i + 1]?.start ?? this.duration) : t - this.segs[i].start > 250 ? this.segs[i].start : (this.segs[i - 1]?.start ?? 0);
575
+ this.pause();
576
+ this.seek(target);
577
+ }
578
+ onClick(e) {
579
+ const action = e.target.closest("button")?.dataset.action;
580
+ if (action === "toggle")
581
+ this.toggle();
582
+ else if (action === "back")
583
+ this.stepBack();
584
+ else if (action === "forward")
585
+ this.stepForward();
586
+ else if (action === "start")
587
+ this.toStart();
588
+ else if (action === "end")
589
+ this.toEnd();
590
+ else if (action === "speed") {
591
+ const i = this._speeds.indexOf(this._rate);
592
+ this.rate = this._speeds[(i + 1) % this._speeds.length];
593
+ }
594
+ }
595
+ onKey(e) {
596
+ // Leave typing alone (inputs in custom controls).
597
+ const target = e.composedPath()[0];
598
+ if (target.closest?.("input, textarea, select, [contenteditable]"))
599
+ return;
600
+ const keys = {
601
+ " ": () => this.toggle(),
602
+ k: () => this.toggle(),
603
+ ArrowRight: () => this.stepForward(),
604
+ ArrowLeft: () => this.stepBack(),
605
+ Home: () => this.toStart(),
606
+ End: () => this.toEnd(),
607
+ PageDown: () => this.jumpSection(1),
608
+ PageUp: () => this.jumpSection(-1),
609
+ };
610
+ const run = keys[e.key];
611
+ if (!run)
612
+ return;
613
+ e.preventDefault();
614
+ run();
615
+ }
616
+ wireProgress() {
617
+ const bar = this.$(".progress");
618
+ const seekTo = (clientX) => {
619
+ const r = this.$(".track").getBoundingClientRect();
620
+ this.seek(fraction(clientX - r.left, r.width) * this.duration);
621
+ };
622
+ let resume = false;
623
+ bar.addEventListener("pointerdown", (e) => {
624
+ resume = this.playing;
625
+ this.pause();
626
+ bar.setPointerCapture(e.pointerId);
627
+ bar.classList.add("dragging");
628
+ seekTo(e.clientX);
629
+ });
630
+ const timeAt = (clientX) => {
631
+ const r = this.$(".track").getBoundingClientRect();
632
+ return fraction(clientX - r.left, r.width) * this.duration;
633
+ };
634
+ bar.addEventListener("pointermove", (e) => {
635
+ if (bar.hasPointerCapture(e.pointerId))
636
+ seekTo(e.clientX);
637
+ // Popup over the section under the pointer (mouse hover, or while dragging).
638
+ if (e.pointerType === "mouse" || bar.hasPointerCapture(e.pointerId))
639
+ this.showTooltip(segmentAt(this.segs, timeAt(e.clientX)));
640
+ });
641
+ bar.addEventListener("pointerleave", (e) => {
642
+ if (!bar.hasPointerCapture(e.pointerId) && this.shadowRoot?.activeElement !== bar)
643
+ this.showTooltip(-1);
644
+ });
645
+ bar.addEventListener("focus", () => this.showTooltip(segmentAt(this.segs, this.currentTime)));
646
+ bar.addEventListener("blur", () => this.showTooltip(-1));
647
+ this.addEventListener("segmentchange", (e) => {
648
+ if (this.shadowRoot?.activeElement === bar || bar.classList.contains("dragging"))
649
+ this.showTooltip(e.detail.index);
650
+ });
651
+ const end = (e) => {
652
+ if (!bar.hasPointerCapture(e.pointerId))
653
+ return;
654
+ bar.releasePointerCapture(e.pointerId);
655
+ bar.classList.remove("dragging");
656
+ // Touch: keep the popup a moment after lifting the finger.
657
+ if (e.pointerType !== "mouse")
658
+ this.showTooltip(segmentAt(this.segs, this.currentTime), 1500);
659
+ if (resume)
660
+ this.play();
661
+ };
662
+ // Section names under the bar: jump to the start of that section.
663
+ this.$(".labels").addEventListener("click", (e) => {
664
+ const i = Number(e.target.closest(".label")?.dataset.segment);
665
+ if (!Number.isFinite(i) || !this.segs[i])
666
+ return;
667
+ this.pause();
668
+ this.seek(this.segs[i].start);
669
+ });
670
+ bar.addEventListener("pointerup", end);
671
+ bar.addEventListener("pointercancel", end);
672
+ }
673
+ }
674
+ /** Register the element (idempotent). */
675
+ export function defineCubePlayer(tag = "cube-player") {
676
+ if (!customElements.get(tag))
677
+ customElements.define(tag, class extends CubePlayer {
678
+ });
679
+ }
680
+ const is2dView = (kind) => kind === "net" || kind === "top" || kind === "iso";