@wodzik/cubecore 0.1.11 → 0.1.13

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.
@@ -9,9 +9,21 @@
9
9
  */
10
10
  export declare class ClockSync {
11
11
  private readonly window;
12
+ private readonly jump;
12
13
  private samples;
13
- /** @param window how many recent samples the offset is estimated from (the cube clock drifts slowly). */
14
- constructor(window?: number);
14
+ /**
15
+ * @param window how many recent samples the offset is estimated from (the cube clock drifts slowly).
16
+ * @param jump a cube clock that lost time (GAN gen2's 16-bit move intervals roll over after
17
+ * ~65 s idle): this many samples in a row that agree with each other (within `agree` ms)
18
+ * and are all over `jumpMs` later than the offset — the cube clock jumped, start again
19
+ * from them. A burst of late notifications (a backgrounded tab) doesn't agree: its
20
+ * samples spread over the moves' real intervals.
21
+ */
22
+ constructor(window?: number, jump?: {
23
+ samples: number;
24
+ jumpMs: number;
25
+ agree: number;
26
+ });
15
27
  /** Offset (local − cube) estimated so far, or null before the first sample. */
16
28
  get offset(): number | null;
17
29
  /** Feed one event: its cube time and, if it has one, the local time it arrived. */
@@ -9,10 +9,19 @@
9
9
  */
10
10
  export class ClockSync {
11
11
  window;
12
+ jump;
12
13
  samples = [];
13
- /** @param window how many recent samples the offset is estimated from (the cube clock drifts slowly). */
14
- constructor(window = 64) {
14
+ /**
15
+ * @param window how many recent samples the offset is estimated from (the cube clock drifts slowly).
16
+ * @param jump a cube clock that lost time (GAN gen2's 16-bit move intervals roll over after
17
+ * ~65 s idle): this many samples in a row that agree with each other (within `agree` ms)
18
+ * and are all over `jumpMs` later than the offset — the cube clock jumped, start again
19
+ * from them. A burst of late notifications (a backgrounded tab) doesn't agree: its
20
+ * samples spread over the moves' real intervals.
21
+ */
22
+ constructor(window = 64, jump = { samples: 5, jumpMs: 1000, agree: 200 }) {
15
23
  this.window = window;
24
+ this.jump = jump;
16
25
  }
17
26
  /** Offset (local − cube) estimated so far, or null before the first sample. */
18
27
  get offset() {
@@ -25,6 +34,14 @@ export class ClockSync {
25
34
  this.samples.push(localTime - cubeTime);
26
35
  if (this.samples.length > this.window)
27
36
  this.samples.shift();
37
+ const { samples: n, jumpMs, agree } = this.jump;
38
+ if (this.samples.length <= n)
39
+ return;
40
+ const recent = this.samples.slice(-n);
41
+ const best = Math.min(...this.samples.slice(0, -n));
42
+ const lo = Math.min(...recent);
43
+ if (lo - best > jumpMs && Math.max(...recent) - lo <= agree)
44
+ this.samples = recent;
28
45
  }
29
46
  /** The cube time on the page's clock (null until an offset is known). */
30
47
  toLocal(cubeTime) {
@@ -202,6 +202,7 @@ declare class GanCubeClassicConnection implements GanCubeConnection, GanCubeRawC
202
202
  declare class GanGen2ProtocolDriver implements GanProtocolDriver {
203
203
  private lastSerial;
204
204
  private lastMoveTimestamp;
205
+ private hadMove;
205
206
  private cubeTimestamp;
206
207
  createCommandMessage(command: GanCubeCommand): Uint8Array | undefined;
207
208
  handleStateEvent(conn: GanCubeRawConnection, eventMessage: Uint8Array): Promise<GanCubeEvent[]>;
@@ -124,6 +124,7 @@ class GanProtocolMessageView {
124
124
  class GanGen2ProtocolDriver {
125
125
  lastSerial = -1;
126
126
  lastMoveTimestamp = 0;
127
+ hadMove = false;
127
128
  cubeTimestamp = 0;
128
129
  createCommandMessage(command) {
129
130
  var msg = new Uint8Array(20).fill(0);
@@ -190,6 +191,14 @@ class GanGen2ProtocolDriver {
190
191
  if (elapsed == 0) { // In case of 16-bit cube timestamp register overflow
191
192
  elapsed = timestamp - this.lastMoveTimestamp;
192
193
  }
194
+ // After more than the 16-bit register holds (~65 s idle), the interval
195
+ // before the first of these moves rolls over: the local one is right.
196
+ // (cubecore: GAN i3 times ran a minute or more off after the cube lay idle)
197
+ if (i == diff - 1 && this.hadMove) {
198
+ const local = timestamp - this.lastMoveTimestamp;
199
+ if (local > 0xFFFF && elapsed < local)
200
+ elapsed = local;
201
+ }
193
202
  this.cubeTimestamp += elapsed;
194
203
  cubeEvents.push({
195
204
  type: "MOVE",
@@ -203,6 +212,7 @@ class GanGen2ProtocolDriver {
203
212
  });
204
213
  }
205
214
  this.lastMoveTimestamp = timestamp;
215
+ this.hadMove = true;
206
216
  }
207
217
  }
208
218
  }
@@ -5,7 +5,8 @@
5
5
  * It matches CUBE STATES, not move strings, so everything that reaches the
6
6
  * same state counts:
7
7
  * - a half turn done as two quarter turns, either way (half done = `partial`);
8
- * - opposite faces in the other order (R L done as L R);
8
+ * - moves on one axis in any order, even interleaved (R2 L2 done as
9
+ * L R L R, or L2 R2): they commute — each face just has to get there;
9
10
  * - an algorithm written with rotations, slices and wide moves (converted
10
11
  * with toFaceTurns: what the smart cube will actually report).
11
12
  * Off the path, `undo` is the way back to the last matching point (the
@@ -13,7 +14,7 @@
13
14
  */
14
15
  import { type TurnArrow } from "./arrows.js";
15
16
  import type { Frame } from "./frames.js";
16
- import type { Move } from "./moves.js";
17
+ import { type Move } from "./moves.js";
17
18
  import { type State } from "./state.js";
18
19
  export interface SequenceStep {
19
20
  /** The face turn the cube will report. */
@@ -27,7 +28,10 @@ export interface SequenceProgress {
27
28
  /** Face-turn steps fully done. */
28
29
  done: number;
29
30
  total: number;
30
- /** The next step is a half turn and half of it is done. */
31
+ /**
32
+ * Part way through the next steps: half of a half turn, or moves on one
33
+ * axis (R2 L2) done out of order / each part of the way.
34
+ */
31
35
  partial: boolean;
32
36
  /** Status of each written move (for display). */
33
37
  tokens: TokenStatus[];
@@ -42,11 +46,13 @@ export interface SequenceProgress {
42
46
  /** What to turn now — for arrows on a 3D cube (see arrows.ts). */
43
47
  export interface NextTurn {
44
48
  /**
45
- * "next": the next move of the sequence; "undo": the first move back after
46
- * a slip; "wrong-way": the same, when the slip was the right face turned
47
- * the wrong way (or too far) — worth a colour of its own.
49
+ * "next": the next move of the sequence; "finish": the rest of a half turn
50
+ * already started (with R2 L2 both at once, when both are half done);
51
+ * "undo": the first move back after a slip; "wrong-way": the same, when
52
+ * the slip was the right face turned the wrong way (or too far) — worth a
53
+ * colour of its own.
48
54
  */
49
- kind: "next" | "undo" | "wrong-way";
55
+ kind: "next" | "finish" | "undo" | "wrong-way";
50
56
  /** In the cube's own coordinates. Usually one; the rest of an M done half-way can be two. */
51
57
  arrows: TurnArrow[];
52
58
  /** The written move it belongs to (null for undo). */
@@ -69,8 +75,11 @@ export declare class SequenceTracker {
69
75
  readonly frames: Frame[];
70
76
  private readonly path;
71
77
  private cur;
78
+ private readonly groups;
79
+ private readonly groupOf;
72
80
  private anchor;
73
- private anchorPartial;
81
+ /** At the anchor, inside an axis group: how far each of its faces got (quarters). */
82
+ private anchorGroup;
74
83
  private offPath;
75
84
  private readonly maxCorrection;
76
85
  constructor(target: string | readonly Move[], start: State, options?: SequenceOptions);
@@ -78,6 +87,13 @@ export declare class SequenceTracker {
78
87
  /** A face turn reported by the cube. */
79
88
  push(move: Move): SequenceProgress;
80
89
  get progress(): SequenceProgress;
90
+ /** A step at the anchor: done, part way (half of a half turn), or not started. */
91
+ private stepStatus;
92
+ /**
93
+ * The only slip is a face due (the next step, or one on its axis still to
94
+ * do) turned the wrong way or too far — the step it belongs to, else null.
95
+ */
96
+ private wrongWayStep;
81
97
  /**
82
98
  * What to turn now, for an arrow: after a slip the first undo move; half way
83
99
  * through a half turn the remaining quarter (the way it was started); else
@@ -85,6 +101,10 @@ export declare class SequenceTracker {
85
101
  * middle one — placed where it is on the cube as now held. Null when complete.
86
102
  */
87
103
  get nextTurn(): NextTurn | null;
88
- /** Where the current state is on the path, if anywhere: after `done` steps (+ half of the next one). */
104
+ /**
105
+ * Where the current state is on the path, if anywhere: after `done` steps —
106
+ * or inside an axis group, each of its faces some way to its target (a
107
+ * half turn: any part, either way; a quarter: not yet or done).
108
+ */
89
109
  private locate;
90
110
  }
package/core/sequence.js CHANGED
@@ -5,13 +5,15 @@
5
5
  * It matches CUBE STATES, not move strings, so everything that reaches the
6
6
  * same state counts:
7
7
  * - a half turn done as two quarter turns, either way (half done = `partial`);
8
- * - opposite faces in the other order (R L done as L R);
8
+ * - moves on one axis in any order, even interleaved (R2 L2 done as
9
+ * L R L R, or L2 R2): they commute — each face just has to get there;
9
10
  * - an algorithm written with rotations, slices and wide moves (converted
10
11
  * with toFaceTurns: what the smart cube will actually report).
11
12
  * Off the path, `undo` is the way back to the last matching point (the
12
13
  * slip, simplified: R R' cancels) and `needsReset` says it's too long.
13
14
  */
14
15
  import { turnArrow } from "./arrows.js";
16
+ import { amountQuarters } from "./moves.js";
15
17
  import { invert, parseAlg, simplify } from "./notation.js";
16
18
  import { OrientationTracker } from "./physical.js";
17
19
  import { applyMove, statesEqual } from "./state.js";
@@ -22,8 +24,11 @@ export class SequenceTracker {
22
24
  frames;
23
25
  path; // state after k steps
24
26
  cur;
27
+ groups = [];
28
+ groupOf = []; // step index → its group
25
29
  anchor = 0; // last matching step index
26
- anchorPartial = false;
30
+ /** At the anchor, inside an axis group: how far each of its faces got (quarters). */
31
+ anchorGroup = null;
27
32
  offPath = []; // moves since the last matching point
28
33
  maxCorrection;
29
34
  constructor(target, start, options = {}) {
@@ -39,6 +44,22 @@ export class SequenceTracker {
39
44
  for (const s of this.steps)
40
45
  this.path.push(applyMove(this.path[this.path.length - 1], s.move));
41
46
  this.cur = new Uint8Array(start);
47
+ for (let i = 0; i < this.steps.length;) {
48
+ let j = i + 1;
49
+ while (j < this.steps.length && sameAxis(this.steps[i].move, this.steps[j].move))
50
+ j++;
51
+ const group = { start: i, end: j, faces: [], target: [] };
52
+ for (let k = i; k < j; k++) {
53
+ const { family, amount } = this.steps[k].move;
54
+ let f = group.faces.indexOf(family);
55
+ if (f < 0)
56
+ f = group.faces.push(family) - 1;
57
+ group.target[f] = ((group.target[f] ?? 0) + amountQuarters(amount)) % 4;
58
+ this.groupOf[k] = this.groups.length;
59
+ }
60
+ this.groups.push(group);
61
+ i = j;
62
+ }
42
63
  }
43
64
  get state() {
44
65
  return this.cur;
@@ -49,7 +70,7 @@ export class SequenceTracker {
49
70
  const on = this.locate();
50
71
  if (on) {
51
72
  this.anchor = on.done;
52
- this.anchorPartial = on.partial;
73
+ this.anchorGroup = on.group;
53
74
  this.offPath = [];
54
75
  }
55
76
  else {
@@ -61,24 +82,46 @@ export class SequenceTracker {
61
82
  const total = this.steps.length;
62
83
  const undo = this.offPath.length ? invert(simplify(this.offPath)) : [];
63
84
  const done = this.anchor;
64
- const partial = this.anchorPartial && !this.offPath.length;
85
+ const partial = this.anchorGroup !== null && !this.offPath.length;
65
86
  const next = done < total ? this.steps[done].token : this.written.length;
66
- const expected = this.steps[done]?.move;
67
- const wrongWay = undo.length === 1 && expected !== undefined && undo[0].family === expected.family;
87
+ const ww = this.wrongWayStep(undo);
88
+ const status = (i) => (this.offPath.length ? (i < done ? "done" : "todo") : this.stepStatus(i));
68
89
  const tokens = this.written.map((_, t) => {
69
90
  const idx = this.steps.map((s, i) => (s.token === t ? i : -1)).filter((i) => i >= 0);
70
91
  if (!idx.length)
71
92
  return t < next ? "done" : "todo"; // a rotation: done once the cube gets past it
72
- const first = idx[0], last = idx[idx.length - 1];
73
- if (last < done)
93
+ const st = idx.map(status);
94
+ if (st.every((x) => x === "done"))
74
95
  return "done";
75
- if (first < done || (first === done && partial))
76
- return "partial"; // M half done, or half of an R2
77
- if (first === done && wrongWay)
96
+ if (ww !== null && idx.includes(ww))
78
97
  return "wrong-way";
79
- return first === done ? "current" : "todo";
98
+ if (st.some((x) => x !== "todo"))
99
+ return "partial"; // half of an R2, R2 L2 started either way, M half done
100
+ return idx[0] === done ? "current" : "todo";
80
101
  });
81
- return { done, total, partial, tokens, undo, needsReset: undo.length > this.maxCorrection, wrongWay, complete: done === total && !this.offPath.length };
102
+ return { done, total, partial, tokens, undo, needsReset: undo.length > this.maxCorrection, wrongWay: ww !== null, complete: done === total && !this.offPath.length };
103
+ }
104
+ /** A step at the anchor: done, part way (half of a half turn), or not started. */
105
+ stepStatus(i) {
106
+ const g = this.anchorGroup;
107
+ if (g && i >= g.group.start && i < g.group.end) {
108
+ const f = g.group.faces.indexOf(this.steps[i].move.family);
109
+ return g.got[f] === g.group.target[f] ? "done" : g.got[f] === 0 ? "todo" : "partial";
110
+ }
111
+ return i < this.anchor ? "done" : "todo";
112
+ }
113
+ /**
114
+ * The only slip is a face due (the next step, or one on its axis still to
115
+ * do) turned the wrong way or too far — the step it belongs to, else null.
116
+ */
117
+ wrongWayStep(undo) {
118
+ if (undo.length !== 1 || this.anchor >= this.steps.length)
119
+ return null;
120
+ const end = this.groups[this.groupOf[this.anchor]].end;
121
+ for (let i = this.anchor; i < end; i++)
122
+ if (this.steps[i].move.family === undo[0].family && this.stepStatus(i) !== "done")
123
+ return i;
124
+ return null;
82
125
  }
83
126
  /**
84
127
  * What to turn now, for an arrow: after a slip the first undo move; half way
@@ -90,17 +133,18 @@ export class SequenceTracker {
90
133
  const p = this.progress;
91
134
  if (p.complete)
92
135
  return null;
93
- if (p.wrongWay) {
136
+ const ww = this.wrongWayStep(p.undo);
137
+ if (ww !== null) {
94
138
  // The face due went the wrong way (or too far): the turn to where it should be —
95
139
  // back and on in one go (U instead of U': a U2 the way U' goes).
96
- const expected = this.steps[p.done].move;
140
+ const expected = this.steps[ww].move;
97
141
  const back = p.undo[0];
98
142
  const signed = (m) => (m.amount === 2 ? 2 * Math.sign(m.written ?? 2) : m.amount);
99
143
  let q = signed(back) + (expected.amount === 2 ? 2 * (Math.sign(signed(back)) || 1) : expected.amount);
100
144
  q = ((q % 4) + 4) % 4;
101
145
  const amount = (q === 3 ? -1 : q);
102
146
  const fix = amount === 2 ? { family: expected.family, amount: 2, written: 2 * (Math.sign(signed(expected)) || 1) } : { family: expected.family, amount };
103
- return { kind: "wrong-way", arrows: [turnArrow(fix)], token: this.steps[p.done].token };
147
+ return { kind: "wrong-way", arrows: [turnArrow(fix)], token: this.steps[ww].token };
104
148
  }
105
149
  if (p.undo.length) {
106
150
  return { kind: "undo", arrows: [turnArrow(p.undo[0])], token: null };
@@ -110,15 +154,18 @@ export class SequenceTracker {
110
154
  if (!step)
111
155
  return null;
112
156
  const next = (moves) => ({ kind: "next", arrows: moves.map((m) => turnArrow(m)), token: step.token });
113
- if (p.partial) {
114
- if (step.move.amount === 2) {
115
- for (const amount of [1, -1]) {
116
- const half = { family: step.move.family, amount };
117
- if (statesEqual(applyMove(this.path[k], half), this.cur))
118
- return next([half]);
119
- }
157
+ const g = this.anchorGroup;
158
+ if (g) {
159
+ // Half turns started (R2 L2: one or both half done): the rest of each, the way it was started.
160
+ const halves = g.group.faces.flatMap((family, f) => {
161
+ const got = g.got[f];
162
+ return got !== 0 && got !== g.group.target[f] ? [{ family, amount: got === 1 ? 1 : -1 }] : [];
163
+ });
164
+ if (halves.length) {
165
+ const first = this.steps.findIndex((s, i) => i >= g.group.start && s.move.family === halves[0].family);
166
+ return { kind: "finish", arrows: halves.map((m) => turnArrow(m)), token: this.steps[first].token };
120
167
  }
121
- return next([step.move]); // the opposite face went first: this one is left
168
+ // Else a face of the axis went first (R L done as L R): the next one left, as written.
122
169
  }
123
170
  const first = this.steps.findIndex((s) => s.token === step.token);
124
171
  if (first < k) {
@@ -127,31 +174,45 @@ export class SequenceTracker {
127
174
  }
128
175
  return { kind: "next", arrows: [turnArrow(this.written[step.token], this.frames[step.token])], token: step.token };
129
176
  }
130
- /** Where the current state is on the path, if anywhere: after `done` steps (+ half of the next one). */
177
+ /**
178
+ * Where the current state is on the path, if anywhere: after `done` steps —
179
+ * or inside an axis group, each of its faces some way to its target (a
180
+ * half turn: any part, either way; a quarter: not yet or done).
181
+ */
131
182
  locate() {
132
183
  for (let k = this.path.length - 1; k >= 0; k--)
133
184
  if (statesEqual(this.path[k], this.cur))
134
- return { done: k, partial: false };
135
- for (let k = 0; k < this.steps.length; k++) {
136
- const s = this.steps[k].move;
137
- // Half of a half turn, either way.
138
- if (s.amount === 2) {
139
- for (const amount of [1, -1]) {
140
- if (statesEqual(applyMove(this.path[k], { family: s.family, amount }), this.cur))
141
- return { done: k, partial: true };
185
+ return { done: k, group: null };
186
+ for (let gi = this.groups.length - 1; gi >= 0; gi--) {
187
+ const group = this.groups[gi];
188
+ const options = group.target.map((t) => (t === 2 ? [0, 1, 2, 3] : t === 0 ? [0] : [0, t]));
189
+ for (const got of combinations(options)) {
190
+ if (got.every((q) => q === 0) || got.every((q, f) => q === group.target[f]))
191
+ continue; // on the path: found above
192
+ let s = this.path[group.start];
193
+ got.forEach((q, f) => {
194
+ if (q)
195
+ s = applyMove(s, { family: group.faces[f], amount: q === 3 ? -1 : q });
196
+ });
197
+ if (!statesEqual(s, this.cur))
198
+ continue;
199
+ const at = { group, got };
200
+ let done = group.start;
201
+ while (done < group.end) {
202
+ const f = group.faces.indexOf(this.steps[done].move.family);
203
+ if (got[f] !== group.target[f])
204
+ break;
205
+ done++;
142
206
  }
143
- }
144
- // The next step on the opposite face done first (R L done as L R): count as progress on k.
145
- const n = this.steps[k + 1];
146
- if (n && opposite(s, n.move)) {
147
- if (statesEqual(applyMove(this.path[k], n.move), this.cur))
148
- return { done: k, partial: true };
207
+ return { done, group: at };
149
208
  }
150
209
  }
151
210
  return null;
152
211
  }
153
212
  }
154
213
  const AXIS = { U: 1, D: 1, R: 0, L: 0, F: 2, B: 2 };
155
- function opposite(a, b) {
156
- return a.family !== b.family && AXIS[a.family] !== undefined && AXIS[a.family] === AXIS[b.family];
214
+ const sameAxis = (a, b) => AXIS[a.family] !== undefined && AXIS[a.family] === AXIS[b.family];
215
+ /** Every pick of one value from each list. */
216
+ function combinations(options) {
217
+ return options.reduce((acc, xs) => acc.flatMap((a) => xs.map((x) => [...a, x])), [[]]);
157
218
  }
@@ -25,7 +25,9 @@
25
25
  * or the `player` property), the 3D cube shows the next turn — one arrow per
26
26
  * turning layer (two for a wide r), as many heads as quarter turns, in
27
27
  * --cc-arrow; after a slip the undo move in --cc-arrow-undo, or in
28
- * --cc-arrow-wrong-way when the right face went the wrong way.
28
+ * --cc-arrow-wrong-way when the right face went the wrong way; the rest of
29
+ * a half turn already started (R2 L2 half done, in any order) in
30
+ * --cc-arrow-finish.
29
31
  * `arrow-shape="circle"` for arcs instead of ribbons along the faces.
30
32
  *
31
33
  * After a slip the moves give way to the way back: the undo moves in their
@@ -65,7 +67,7 @@ export interface SequenceMessages {
65
67
  reset: string;
66
68
  complete: string;
67
69
  }
68
- export declare const SEQUENCE_STYLES = "\n:host {\n --cc-seq-font: ui-monospace, \"SF Mono\", Menlo, monospace;\n --cc-seq-size: 22px;\n --cc-seq-gap: 0.35em;\n --cc-seq-done: color-mix(in srgb, currentColor 35%, transparent);\n --cc-seq-current: currentColor;\n --cc-seq-current-bg: color-mix(in srgb, var(--cc-accent) 24%, transparent);\n --cc-seq-partial: var(--cc-accent);\n --cc-seq-todo: color-mix(in srgb, currentColor 82%, transparent);\n --cc-seq-hidden: color-mix(in srgb, currentColor 45%, transparent);\n --cc-seq-undo: #ff5a5a;\n --cc-seq-wrong-way: #ff9a1f;\n --cc-seq-decoration: color-mix(in srgb, currentColor 70%, transparent);\n --cc-arrow: #2f8bff;\n --cc-arrow-undo: #ff4545;\n --cc-arrow-wrong-way: #ff9a1f;\n --cc-accent: #4f8cff;\n --cc-control-bg: color-mix(in srgb, currentColor 8%, transparent);\n --cc-control-bg-hover: color-mix(in srgb, currentColor 15%, transparent);\n --cc-control-radius: 8px;\n display: block;\n}\n:host([headless]) .container { display: none; }\n.group { display: inline-flex; align-items: baseline; }\n.decoration { color: var(--cc-seq-decoration); }\n.container { display: flex; flex-direction: column; gap: 0.5em; }\n.container.slip .moves { display: none; }\n.container.slip .undo { font-size: var(--cc-seq-size); line-height: 1.5; }\n.moves { display: flex; flex-wrap: wrap; gap: 0.1em var(--cc-seq-gap); font: 600 var(--cc-seq-size) / 1.5 var(--cc-seq-font); }\n.move { padding: 0 0.18em; border-radius: 0.25em; transition: color 0.15s ease, background 0.15s ease; }\n.done { color: var(--cc-seq-done); }\n.current { color: var(--cc-seq-current); background: var(--cc-seq-current-bg); }\n.partial { color: var(--cc-seq-partial); background: var(--cc-seq-current-bg); }\n.wrong-way { color: var(--cc-seq-wrong-way); background: var(--cc-seq-current-bg); }\n.todo { color: var(--cc-seq-todo); }\n.hidden { color: var(--cc-seq-hidden); }\n.undo { display: none; color: var(--cc-seq-undo); gap: var(--cc-seq-gap); flex-wrap: wrap; align-items: baseline; font: 600 calc(var(--cc-seq-size) * 0.85) / 1.4 var(--cc-seq-font); }\n.undo.on { display: flex; }\n.undo-label { font: 600 0.65em/1 system-ui, sans-serif; text-transform: uppercase; letter-spacing: 0.06em; margin-right: 0.3em; }\n.message { display: none; font: 500 13px/1.4 system-ui, sans-serif; opacity: 0.85; }\n.message.on { display: block; }\n.controls { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }\n.controls:empty { display: none; }\n:host([controls=\"none\"]) .controls { display: none; }\nbutton {\n border: 0; border-radius: var(--cc-control-radius);\n background: var(--cc-control-bg); color: inherit;\n padding: 6px 11px; font: 600 12px/1 system-ui, sans-serif; cursor: pointer;\n}\nbutton:hover { background: var(--cc-control-bg-hover); }\nbutton:focus-visible, input:focus-visible { outline: 2px solid var(--cc-accent); outline-offset: 2px; }\nbutton[aria-pressed=\"true\"] { background: color-mix(in srgb, var(--cc-accent) 25%, transparent); }\n";
70
+ export declare const SEQUENCE_STYLES = "\n:host {\n --cc-seq-font: ui-monospace, \"SF Mono\", Menlo, monospace;\n --cc-seq-size: 22px;\n --cc-seq-gap: 0.35em;\n --cc-seq-done: color-mix(in srgb, currentColor 35%, transparent);\n --cc-seq-current: currentColor;\n --cc-seq-current-bg: color-mix(in srgb, var(--cc-accent) 24%, transparent);\n --cc-seq-partial: var(--cc-accent);\n --cc-seq-todo: color-mix(in srgb, currentColor 82%, transparent);\n --cc-seq-hidden: color-mix(in srgb, currentColor 45%, transparent);\n --cc-seq-undo: #ff5a5a;\n --cc-seq-wrong-way: #ff9a1f;\n --cc-seq-decoration: color-mix(in srgb, currentColor 70%, transparent);\n --cc-arrow: #2f8bff;\n --cc-arrow-undo: #ff4545;\n --cc-arrow-wrong-way: #ff9a1f;\n --cc-arrow-finish: #ff9a1f;\n --cc-accent: #4f8cff;\n --cc-control-bg: color-mix(in srgb, currentColor 8%, transparent);\n --cc-control-bg-hover: color-mix(in srgb, currentColor 15%, transparent);\n --cc-control-radius: 8px;\n display: block;\n}\n:host([headless]) .container { display: none; }\n.group { display: inline-flex; align-items: baseline; }\n.decoration { color: var(--cc-seq-decoration); }\n.container { display: flex; flex-direction: column; gap: 0.5em; }\n.container.slip .moves { display: none; }\n.container.slip .undo { font-size: var(--cc-seq-size); line-height: 1.5; }\n.moves { display: flex; flex-wrap: wrap; gap: 0.1em var(--cc-seq-gap); font: 600 var(--cc-seq-size) / 1.5 var(--cc-seq-font); }\n.move { padding: 0 0.18em; border-radius: 0.25em; transition: color 0.15s ease, background 0.15s ease; }\n.done { color: var(--cc-seq-done); }\n.current { color: var(--cc-seq-current); background: var(--cc-seq-current-bg); }\n.partial { color: var(--cc-seq-partial); background: var(--cc-seq-current-bg); }\n.wrong-way { color: var(--cc-seq-wrong-way); background: var(--cc-seq-current-bg); }\n.todo { color: var(--cc-seq-todo); }\n.hidden { color: var(--cc-seq-hidden); }\n.undo { display: none; color: var(--cc-seq-undo); gap: var(--cc-seq-gap); flex-wrap: wrap; align-items: baseline; font: 600 calc(var(--cc-seq-size) * 0.85) / 1.4 var(--cc-seq-font); }\n.undo.on { display: flex; }\n.undo-label { font: 600 0.65em/1 system-ui, sans-serif; text-transform: uppercase; letter-spacing: 0.06em; margin-right: 0.3em; }\n.message { display: none; font: 500 13px/1.4 system-ui, sans-serif; opacity: 0.85; }\n.message.on { display: block; }\n.controls { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }\n.controls:empty { display: none; }\n:host([controls=\"none\"]) .controls { display: none; }\nbutton {\n border: 0; border-radius: var(--cc-control-radius);\n background: var(--cc-control-bg); color: inherit;\n padding: 6px 11px; font: 600 12px/1 system-ui, sans-serif; cursor: pointer;\n}\nbutton:hover { background: var(--cc-control-bg-hover); }\nbutton:focus-visible, input:focus-visible { outline: 2px solid var(--cc-accent); outline-offset: 2px; }\nbutton[aria-pressed=\"true\"] { background: color-mix(in srgb, var(--cc-accent) 25%, transparent); }\n";
69
71
  export declare abstract class CubeSequenceElement extends ElementBase {
70
72
  protected readonly root: ShadowRoot;
71
73
  protected source: MoveSource | null;
@@ -25,7 +25,9 @@
25
25
  * or the `player` property), the 3D cube shows the next turn — one arrow per
26
26
  * turning layer (two for a wide r), as many heads as quarter turns, in
27
27
  * --cc-arrow; after a slip the undo move in --cc-arrow-undo, or in
28
- * --cc-arrow-wrong-way when the right face went the wrong way.
28
+ * --cc-arrow-wrong-way when the right face went the wrong way; the rest of
29
+ * a half turn already started (R2 L2 half done, in any order) in
30
+ * --cc-arrow-finish.
29
31
  * `arrow-shape="circle"` for arcs instead of ribbons along the faces.
30
32
  *
31
33
  * After a slip the moves give way to the way back: the undo moves in their
@@ -53,6 +55,7 @@ export const SEQUENCE_STYLES = /* css */ `
53
55
  --cc-arrow: #2f8bff;
54
56
  --cc-arrow-undo: #ff4545;
55
57
  --cc-arrow-wrong-way: #ff9a1f;
58
+ --cc-arrow-finish: #ff9a1f;
56
59
  --cc-accent: #4f8cff;
57
60
  --cc-control-bg: color-mix(in srgb, currentColor 8%, transparent);
58
61
  --cc-control-bg-hover: color-mix(in srgb, currentColor 15%, transparent);
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.11",
6
+ "version": "0.1.13",
7
7
  "description": "A 3×3×3 cube library for the web, built around smart (Bluetooth) cubes: state, notation, methods, solvers and scrambles, a three.js renderer with skins, pictures, web components, React bindings.",
8
8
  "license": "MPL-2.0",
9
9
  "author": "Tomasz Wodzikowski <wodziszczakow@gmail.com>",