@wodzik/cubecore 0.1.0 → 0.1.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.
- package/analyze/roux.d.ts +4 -2
- package/analyze/roux.js +12 -5
- package/element/model.d.ts +6 -1
- package/element/model.js +11 -1
- package/element/player.d.ts +11 -0
- package/element/player.js +82 -10
- package/element/styles.d.ts +6 -6
- package/element/styles.js +17 -12
- package/package.json +1 -1
- package/solve/twophase.js +51 -2
- package/timeline/timings.js +2 -1
package/analyze/roux.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ import { type RouxSide } from "../roux/index.js";
|
|
|
18
18
|
export interface RouxAnalyzeOptions {
|
|
19
19
|
/** Physical faces the first block may go on (its left side); default all six. */
|
|
20
20
|
sides?: readonly Face[];
|
|
21
|
+
/** Physical faces the blocks' bottom may be on (colour neutrality, like CFOP's `crosses`: e.g. ["U", "D"] = white / yellow); default all six. */
|
|
22
|
+
bottoms?: readonly Face[];
|
|
21
23
|
/** Case ids the solver knows ("Sune Left Bar"…) — the CMLL step gets `known`. */
|
|
22
24
|
known?: Iterable<string>;
|
|
23
25
|
}
|
|
@@ -59,8 +61,8 @@ export interface RouxAnalysisResult {
|
|
|
59
61
|
bySide: RouxAnalysis[];
|
|
60
62
|
best: RouxAnalysis;
|
|
61
63
|
}
|
|
62
|
-
/** One side (and its best bottom): the whole way through, as held. */
|
|
63
|
-
export declare function analyzeRouxSide(scrambled: State, side: Face, known?: Set<string> | null): RouxAnalysis;
|
|
64
|
+
/** One side (and its best bottom, out of `bottoms` if given): the whole way through, as held. */
|
|
65
|
+
export declare function analyzeRouxSide(scrambled: State, side: Face, known?: Set<string> | null, bottoms?: readonly Face[]): RouxAnalysis;
|
|
64
66
|
/** Analyse a scramble for Roux, for each side the first block can go on. */
|
|
65
67
|
export declare function analyzeRoux(scramble: string | readonly Move[] | State, options?: RouxAnalyzeOptions): RouxAnalysisResult;
|
|
66
68
|
/** Does following it (as held) solve the cube? */
|
package/analyze/roux.js
CHANGED
|
@@ -19,12 +19,15 @@ import { STAGES, lseSolver, stageSolver } from "../solve/index.js";
|
|
|
19
19
|
import { rotationFor } from "./rotations.js";
|
|
20
20
|
const FB_FIXED = { ...ROUX_TRAINERS.fb(), name: "roux-fb-fixed", neutral: undefined };
|
|
21
21
|
const SIDES = ["front", "back"];
|
|
22
|
-
/** One side (and its best bottom): the whole way through, as held. */
|
|
23
|
-
export function analyzeRouxSide(scrambled, side, known = null) {
|
|
22
|
+
/** One side (and its best bottom, out of `bottoms` if given): the whole way through, as held. */
|
|
23
|
+
export function analyzeRouxSide(scrambled, side, known = null, bottoms) {
|
|
24
24
|
// The best bottom colour for the first block on this side.
|
|
25
25
|
const fb = stageSolver(FB_FIXED);
|
|
26
26
|
let best = null;
|
|
27
|
-
|
|
27
|
+
const frames = FRAMES.filter((f) => f.face.L === side && (!bottoms || bottoms.includes(f.face.D)));
|
|
28
|
+
if (!frames.length)
|
|
29
|
+
throw new Error(`No allowed bottom next to side ${side}`);
|
|
30
|
+
for (const frame of frames) {
|
|
28
31
|
const sol = fb.solve(reframe(scrambled, frame), { maxDepth: 12 })[0];
|
|
29
32
|
if (sol && (!best || sol.length < best.moves.length))
|
|
30
33
|
best = { frame, moves: sol };
|
|
@@ -82,8 +85,12 @@ export function analyzeRouxSide(scrambled, side, known = null) {
|
|
|
82
85
|
export function analyzeRoux(scramble, options = {}) {
|
|
83
86
|
const state = scramble instanceof Uint8Array ? scramble : applyMoves(solvedState(), typeof scramble === "string" ? parseAlg(scramble) : scramble);
|
|
84
87
|
const known = options.known ? new Set(options.known) : null;
|
|
85
|
-
const
|
|
86
|
-
|
|
88
|
+
const bottoms = options.bottoms;
|
|
89
|
+
// A side is possible only with an allowed bottom next to it (not on the side itself or opposite it).
|
|
90
|
+
const sides = (options.sides ?? ["L", "R", "F", "B", "U", "D"]).filter((side) => FRAMES.some((f) => f.face.L === side && (!bottoms || bottoms.includes(f.face.D))));
|
|
91
|
+
if (!sides.length)
|
|
92
|
+
throw new Error("No side has an allowed bottom");
|
|
93
|
+
const bySide = sides.map((side) => analyzeRouxSide(state, side, known, bottoms)).sort((a, b) => a.length - b.length);
|
|
87
94
|
return { bySide, best: bySide[0] };
|
|
88
95
|
}
|
|
89
96
|
/** Does following it (as held) solve the cube? */
|
package/element/model.d.ts
CHANGED
|
@@ -32,5 +32,10 @@ export declare const fraction: (time: number, duration: number) => number;
|
|
|
32
32
|
* inverse, so playing it ends solved (or back at the setup).
|
|
33
33
|
*/
|
|
34
34
|
export declare function startMoves(setup: string | readonly Move[], alg: readonly Move[], anchor?: "start" | "end"): Move[];
|
|
35
|
-
/**
|
|
35
|
+
/** A segment's popup in two lines: the heading ("OLL · 2.64 s") and the details ("recognition 1.55 · execution 1.09 · 7 moves"). */
|
|
36
|
+
export declare function segmentLines(s: Segment): {
|
|
37
|
+
title: string;
|
|
38
|
+
details: string;
|
|
39
|
+
};
|
|
40
|
+
/** Default tooltip text of a segment (one line): "F2L 2 · FL · 3.21 s (recognition 0.80 · execution 2.41) · 9 moves". */
|
|
36
41
|
export declare function segmentText(s: Segment): string;
|
package/element/model.js
CHANGED
|
@@ -51,7 +51,17 @@ export function startMoves(setup, alg, anchor = "start") {
|
|
|
51
51
|
const s = typeof setup === "string" ? parseAlg(setup) : [...setup];
|
|
52
52
|
return anchor === "end" ? [...s, ...invert(alg)] : s;
|
|
53
53
|
}
|
|
54
|
-
/**
|
|
54
|
+
/** A segment's popup in two lines: the heading ("OLL · 2.64 s") and the details ("recognition 1.55 · execution 1.09 · 7 moves"). */
|
|
55
|
+
export function segmentLines(s) {
|
|
56
|
+
const title = [s.label, s.detail, `${formatTime(s.end - s.start)} s`].filter(Boolean).join(" · ");
|
|
57
|
+
const details = [];
|
|
58
|
+
if (s.split !== undefined)
|
|
59
|
+
details.push(`recognition ${formatTime(s.split - s.start)}`, `execution ${formatTime(s.end - s.split)}`);
|
|
60
|
+
if (s.moves !== undefined)
|
|
61
|
+
details.push(`${s.moves} move${s.moves === 1 ? "" : "s"}`);
|
|
62
|
+
return { title, details: details.join(" · ") };
|
|
63
|
+
}
|
|
64
|
+
/** Default tooltip text of a segment (one line): "F2L 2 · FL · 3.21 s (recognition 0.80 · execution 2.41) · 9 moves". */
|
|
55
65
|
export function segmentText(s) {
|
|
56
66
|
const parts = [s.label];
|
|
57
67
|
if (s.detail)
|
package/element/player.d.ts
CHANGED
|
@@ -74,6 +74,8 @@ export declare class CubePlayer extends ElementBase {
|
|
|
74
74
|
private liveSource;
|
|
75
75
|
private turnArrows;
|
|
76
76
|
private clock;
|
|
77
|
+
/** How long a move animates (recording ms) — the clock's, and a step's. */
|
|
78
|
+
private maxAnimMs;
|
|
77
79
|
private unsubscribe;
|
|
78
80
|
private rec;
|
|
79
81
|
private start;
|
|
@@ -144,8 +146,17 @@ export declare class CubePlayer extends ElementBase {
|
|
|
144
146
|
get applied(): number;
|
|
145
147
|
/** The moves being played (the algorithm, or the recording's moves). */
|
|
146
148
|
get moves(): Move[];
|
|
149
|
+
/** The next move, animated (as it turns when playing, at the current speed). */
|
|
147
150
|
stepForward(): void;
|
|
151
|
+
/** The last move undone, animated backwards. */
|
|
148
152
|
stepBack(): void;
|
|
153
|
+
private stepFrame;
|
|
154
|
+
private stepEnd;
|
|
155
|
+
/** Run the clock from `from` to `to` (recording ms) in real time, then settle on `end`. */
|
|
156
|
+
private animateStep;
|
|
157
|
+
/** A step still animating: jump to where it ends (another step, or anything else, comes next). */
|
|
158
|
+
private finishStep;
|
|
159
|
+
private stopStep;
|
|
149
160
|
toStart(): void;
|
|
150
161
|
toEnd(): void;
|
|
151
162
|
/** Follow a smart cube: its moves animate, resyncs jump, its gyro turns the cube. Returns detach. */
|
package/element/player.js
CHANGED
|
@@ -42,8 +42,8 @@ import { applyMoves, parseAlg, solvedState, spinsAfter } from "../core/index.js"
|
|
|
42
42
|
import { renderSvg } from "../image/index.js";
|
|
43
43
|
import { CubeRenderer, showPosition } from "../render/index.js";
|
|
44
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";
|
|
45
|
+
import { ReplayClock, compressPauses, moveWindows, segmentAt, segmentPlayed, stageSegments } from "../timeline/index.js";
|
|
46
|
+
import { formatTime, fraction, segmentLines, segmentText, startMoves, stepTime, tempoRecording } from "./model.js";
|
|
47
47
|
import { ICONS, STYLES } from "./styles.js";
|
|
48
48
|
import { ElementBase } from "./base.js";
|
|
49
49
|
const TEMPLATE = `
|
|
@@ -83,6 +83,8 @@ export class CubePlayer extends ElementBase {
|
|
|
83
83
|
liveSource = null;
|
|
84
84
|
turnArrows = null;
|
|
85
85
|
clock = null;
|
|
86
|
+
/** How long a move animates (recording ms) — the clock's, and a step's. */
|
|
87
|
+
maxAnimMs = 150;
|
|
86
88
|
unsubscribe = null;
|
|
87
89
|
rec = { scramble: [], moves: [], totalMs: 0 };
|
|
88
90
|
start = solvedState();
|
|
@@ -122,6 +124,7 @@ export class CubePlayer extends ElementBase {
|
|
|
122
124
|
}
|
|
123
125
|
disconnectedCallback() {
|
|
124
126
|
this.connected = false;
|
|
127
|
+
this.stopStep();
|
|
125
128
|
this.darkQuery?.removeEventListener("change", this.onScheme);
|
|
126
129
|
this.clock?.pause();
|
|
127
130
|
this.unsubscribe?.();
|
|
@@ -282,6 +285,7 @@ export class CubePlayer extends ElementBase {
|
|
|
282
285
|
play() {
|
|
283
286
|
if (!this.clock || this.playing)
|
|
284
287
|
return;
|
|
288
|
+
this.finishStep();
|
|
285
289
|
this.clock.play();
|
|
286
290
|
this.dispatchEvent(new Event("play"));
|
|
287
291
|
this.updateUi(this.currentTime);
|
|
@@ -300,6 +304,7 @@ export class CubePlayer extends ElementBase {
|
|
|
300
304
|
this.play();
|
|
301
305
|
}
|
|
302
306
|
seek(ms) {
|
|
307
|
+
this.stopStep();
|
|
303
308
|
this.clock?.seek(ms);
|
|
304
309
|
}
|
|
305
310
|
/** Jump to the moment `k` moves are done (0 = the start). */
|
|
@@ -315,13 +320,67 @@ export class CubePlayer extends ElementBase {
|
|
|
315
320
|
get moves() {
|
|
316
321
|
return this.rec.moves.map((m) => m.move);
|
|
317
322
|
}
|
|
323
|
+
/** The next move, animated (as it turns when playing, at the current speed). */
|
|
318
324
|
stepForward() {
|
|
319
325
|
this.pause();
|
|
320
|
-
this.
|
|
321
|
-
|
|
326
|
+
this.finishStep();
|
|
327
|
+
const time = this.currentTime;
|
|
328
|
+
const target = stepTime(this.rec, time, 1);
|
|
329
|
+
const k = this.rec.moves.findIndex((m) => m.t === target);
|
|
330
|
+
if (k < 0)
|
|
331
|
+
return this.seek(target);
|
|
332
|
+
const [from, to] = moveWindows(this.rec, { maxAnimMs: this.maxAnimMs })[k];
|
|
333
|
+
this.animateStep(Math.max(time, from), to, to);
|
|
334
|
+
}
|
|
335
|
+
/** The last move undone, animated backwards. */
|
|
322
336
|
stepBack() {
|
|
323
337
|
this.pause();
|
|
324
|
-
this.
|
|
338
|
+
this.finishStep();
|
|
339
|
+
const time = this.currentTime;
|
|
340
|
+
const target = stepTime(this.rec, time, -1);
|
|
341
|
+
// The move being undone: the last one finished by now.
|
|
342
|
+
let k = -1;
|
|
343
|
+
this.rec.moves.forEach((m, i) => {
|
|
344
|
+
if (m.t <= time + 0.5)
|
|
345
|
+
k = i;
|
|
346
|
+
});
|
|
347
|
+
if (k < 0)
|
|
348
|
+
return this.seek(target);
|
|
349
|
+
const [from, to] = moveWindows(this.rec, { maxAnimMs: this.maxAnimMs })[k];
|
|
350
|
+
this.animateStep(Math.min(time, to), from, target);
|
|
351
|
+
}
|
|
352
|
+
stepFrame = 0;
|
|
353
|
+
stepEnd = null;
|
|
354
|
+
/** Run the clock from `from` to `to` (recording ms) in real time, then settle on `end`. */
|
|
355
|
+
animateStep(from, to, end) {
|
|
356
|
+
const clock = this.clock;
|
|
357
|
+
if (!clock)
|
|
358
|
+
return;
|
|
359
|
+
clock.seek(from);
|
|
360
|
+
// At least a quarter second: a step is for looking at the move.
|
|
361
|
+
const duration = Math.max(250, Math.abs(to - from) / (this._rate || 1));
|
|
362
|
+
const started = performance.now();
|
|
363
|
+
this.stepEnd = end;
|
|
364
|
+
const tick = () => {
|
|
365
|
+
const k = Math.min(1, (performance.now() - started) / duration);
|
|
366
|
+
if (k >= 1)
|
|
367
|
+
return this.finishStep();
|
|
368
|
+
clock.seek(from + (to - from) * k);
|
|
369
|
+
this.stepFrame = requestAnimationFrame(tick);
|
|
370
|
+
};
|
|
371
|
+
this.stepFrame = requestAnimationFrame(tick);
|
|
372
|
+
}
|
|
373
|
+
/** A step still animating: jump to where it ends (another step, or anything else, comes next). */
|
|
374
|
+
finishStep() {
|
|
375
|
+
if (this.stepEnd === null)
|
|
376
|
+
return;
|
|
377
|
+
const end = this.stepEnd;
|
|
378
|
+
this.stopStep();
|
|
379
|
+
this.clock?.seek(end);
|
|
380
|
+
}
|
|
381
|
+
stopStep() {
|
|
382
|
+
cancelAnimationFrame(this.stepFrame);
|
|
383
|
+
this.stepEnd = null;
|
|
325
384
|
}
|
|
326
385
|
toStart() {
|
|
327
386
|
this.pause();
|
|
@@ -400,6 +459,7 @@ export class CubePlayer extends ElementBase {
|
|
|
400
459
|
this.clock?.pause();
|
|
401
460
|
this.unsubscribe?.();
|
|
402
461
|
let maxAnimMs = 150;
|
|
462
|
+
this.stopStep();
|
|
403
463
|
let solves = [];
|
|
404
464
|
try {
|
|
405
465
|
if (this._recording) {
|
|
@@ -426,6 +486,7 @@ export class CubePlayer extends ElementBase {
|
|
|
426
486
|
this.start = this._setupState ?? solvedState();
|
|
427
487
|
this.dispatchEvent(new CustomEvent("error", { detail: { message: err instanceof Error ? err.message : String(err) } }));
|
|
428
488
|
}
|
|
489
|
+
this.maxAnimMs = maxAnimMs;
|
|
429
490
|
this.clock = new ReplayClock(this.rec, { maxAnimMs });
|
|
430
491
|
this.clock.rate = this._rate;
|
|
431
492
|
this.unsubscribe = this.clock.onChange((time, pos) => this.onTime(time, pos));
|
|
@@ -552,11 +613,22 @@ export class CubePlayer extends ElementBase {
|
|
|
552
613
|
}
|
|
553
614
|
const s = this.segs[index];
|
|
554
615
|
tip.innerHTML = "";
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
616
|
+
const line = (cls, part, content) => {
|
|
617
|
+
const el = document.createElement("span");
|
|
618
|
+
el.className = cls;
|
|
619
|
+
el.setAttribute("part", part);
|
|
620
|
+
el.textContent = content;
|
|
621
|
+
tip.appendChild(el);
|
|
622
|
+
};
|
|
623
|
+
if (this.formatSegment === segmentText) {
|
|
624
|
+
// The default: a heading and a quieter line of details.
|
|
625
|
+
const { title, details } = segmentLines(s);
|
|
626
|
+
line("tip tip-title", "tooltip-text tooltip-title", title);
|
|
627
|
+
if (details)
|
|
628
|
+
line("tip tip-details", "tooltip-text tooltip-details", details);
|
|
629
|
+
}
|
|
630
|
+
else
|
|
631
|
+
line("tip", "tooltip-text", this.formatSegment(s));
|
|
560
632
|
tip.hidden = false;
|
|
561
633
|
const track = this.$(".track").getBoundingClientRect();
|
|
562
634
|
const centre = ((s.start + s.end) / 2 / (this.duration || 1)) * track.width;
|
package/element/styles.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - ::part() for anything else: stage, flat (the 2D picture), progress, progress-track,
|
|
13
13
|
* progress-fill, progress-thumb, progress-marker, segment (+ segment-<id>),
|
|
14
14
|
* segment-played, segment-recognition, segment-labels, segment-label
|
|
15
|
-
* (+ segment-label-<id>), tooltip, tooltip-text, controls, buttons, button,
|
|
15
|
+
* (+ segment-label-<id>), tooltip, tooltip-text (tooltip-title, tooltip-details), controls, buttons, button,
|
|
16
16
|
* button-start, button-back, button-play, button-forward, button-end,
|
|
17
17
|
* button-speed, time.
|
|
18
18
|
* - Replace the controls entirely: put your own element in slot="controls"
|
|
@@ -20,13 +20,13 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Colours derive from `currentColor`, so the defaults fit light and dark pages.
|
|
22
22
|
*/
|
|
23
|
-
export declare const STYLES = "\n:host {\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-fg: currentColor;\n --cc-control-radius: 10px;\n --cc-button-size: 34px;\n --cc-controls-gap: 5px;\n --cc-progress-height: 6px;\n --cc-progress-track: color-mix(in srgb, currentColor 14%, transparent);\n --cc-progress-fill: var(--cc-accent);\n --cc-marker: color-mix(in srgb, currentColor 55%, transparent);\n --cc-thumb-size: 14px;\n --cc-font: ui-monospace, \"SF Mono\", Menlo, monospace;\n /* sections of the progress bar */\n --cc-segment-1: #4f8cff;\n --cc-segment-2: #22c55e;\n --cc-segment-3: #f59e0b;\n --cc-segment-4: #ef4444;\n --cc-segment-5: #a855f7;\n --cc-segment-6: #14b8a6;\n --cc-segment-7: #ec4899;\n --cc-segment-8: #eab308;\n --cc-segment-height: calc(var(--cc-progress-height) + 2px);\n --cc-segment-gap: 2px;\n --cc-segment-unplayed: 30%;\n /* popup:
|
|
23
|
+
export declare const STYLES = "\n:host {\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-fg: currentColor;\n --cc-control-radius: 10px;\n --cc-button-size: 34px;\n --cc-controls-gap: 5px;\n --cc-progress-height: 6px;\n --cc-progress-track: color-mix(in srgb, currentColor 14%, transparent);\n --cc-progress-fill: var(--cc-accent);\n --cc-marker: color-mix(in srgb, currentColor 55%, transparent);\n --cc-thumb-size: 14px;\n --cc-font: ui-monospace, \"SF Mono\", Menlo, monospace;\n /* sections of the progress bar */\n --cc-segment-1: #4f8cff;\n --cc-segment-2: #22c55e;\n --cc-segment-3: #f59e0b;\n --cc-segment-4: #ef4444;\n --cc-segment-5: #a855f7;\n --cc-segment-6: #14b8a6;\n --cc-segment-7: #ec4899;\n --cc-segment-8: #eab308;\n --cc-segment-height: calc(var(--cc-progress-height) + 2px);\n --cc-segment-gap: 2px;\n --cc-segment-unplayed: 30%;\n /* popup: dark and solid by default \u2014 readable on light and dark pages and over the cube */\n --cc-tooltip-bg: rgb(17 20 28 / 0.97);\n --cc-tooltip-fg: #f4f5f7;\n --cc-tooltip-muted: #a9afbb;\n --cc-tooltip-border: rgb(255 255 255 / 0.12);\n display: flex;\n flex-direction: column;\n gap: 10px;\n min-width: 200px;\n min-height: 200px;\n outline: none;\n container-type: inline-size;\n}\n:host([hidden]) { display: none; }\n.stage { position: relative; flex: 1; min-height: 0; }\n.flat { position: absolute; inset: 0; display: grid; place-items: center; }\n.flat[hidden] { display: none; }\n.flat svg { width: 100%; height: 100%; }\n\n/* \u2500\u2500\u2500 progress bar \u2500\u2500\u2500 */\n.progress { display: none; padding: 6px 2px; cursor: pointer; touch-action: none; }\n:host([progress]) .progress { display: block; }\n.track {\n position: relative;\n height: var(--cc-progress-height);\n border-radius: 999px;\n background: var(--cc-progress-track);\n}\n.fill {\n position: absolute; inset: 0 auto 0 0;\n width: 0;\n border-radius: inherit;\n background: var(--cc-progress-fill);\n}\n.thumb {\n position: absolute; top: 50%; left: 0;\n width: var(--cc-thumb-size); height: var(--cc-thumb-size);\n border-radius: 50%;\n background: var(--cc-progress-fill);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--cc-progress-fill) 25%, transparent);\n transform: translate(-50%, -50%) scale(0.85);\n transition: transform 0.12s ease;\n}\n.progress:hover .thumb, .progress.dragging .thumb { transform: translate(-50%, -50%) scale(1); }\n/* Sections: coloured parts of the bar, dim until played, recognition hatched. */\n.progress { position: relative; }\n.progress.has-segments .fill { display: none; }\n.segments { position: absolute; inset: 0; }\n.segment {\n position: absolute; top: 50%;\n height: var(--cc-segment-height);\n transform: translateY(-50%);\n overflow: hidden;\n background: color-mix(in srgb, var(--seg) var(--cc-segment-unplayed), transparent);\n clip-path: inset(0 calc(var(--cc-segment-gap) / 2) 0 calc(var(--cc-segment-gap) / 2) round 999px);\n}\n.segment .played { position: absolute; inset: 0 auto 0 0; width: 0; background: var(--seg); }\n.segment .recognition {\n position: absolute; inset: 0 auto 0 0;\n background: repeating-linear-gradient(135deg, color-mix(in srgb, Canvas 50%, transparent) 0 2px, transparent 2px 5px);\n}\n.progress.has-segments .thumb { background: currentColor; box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 20%, transparent); }\n.tooltip {\n position: absolute; bottom: calc(100% + 4px); left: 0;\n transform: translateX(-50%);\n width: max-content; max-width: min(320px, 100%);\n padding: 6px 9px;\n border-radius: 8px;\n background: var(--cc-tooltip-bg);\n border: 1px solid var(--cc-tooltip-border);\n font: 500 12px/1.4 system-ui, sans-serif;\n pointer-events: none;\n z-index: 2;\n box-shadow: 0 8px 24px -8px rgb(0 0 0 / 0.6);\n display: flex; flex-direction: column; gap: 1px;\n}\n.tooltip .tip { color: var(--cc-tooltip-fg); }\n.tooltip .tip-title { font-weight: 650; font-variant-numeric: tabular-nums; }\n.tooltip .tip-details { color: var(--cc-tooltip-muted); font-size: 11px; font-variant-numeric: tabular-nums; }\n.tooltip[hidden] { display: none; }\n.labels { display: none; position: relative; height: 18px; margin-top: -4px; }\n:host([segment-labels]) .labels { display: block; }\n.label {\n position: absolute; top: 0;\n min-width: 0; height: 18px; padding: 0 2px;\n background: none; border-radius: 4px;\n color: inherit; opacity: 0.6;\n font: 500 11px/18px system-ui, sans-serif;\n overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n}\n.label:hover { background: var(--cc-control-bg); opacity: 1; transform: none; }\n.label:active { transform: none; }\n.label.current { opacity: 1; font-weight: 700; }\n.marker {\n position: absolute; top: 50%;\n width: 2px; height: calc(var(--cc-progress-height) + 8px);\n border-radius: 1px;\n background: var(--cc-marker);\n transform: translate(-50%, -50%);\n pointer-events: none;\n}\n:host(:not([markers])) .marker { display: none; }\n.progress:focus-visible .track { outline: 2px solid var(--cc-accent); outline-offset: 4px; }\n\n/* \u2500\u2500\u2500 controls \u2500\u2500\u2500 */\n/* time | buttons | speed \u2014 the buttons stay centred whatever the time text's width */\n.controls { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: var(--cc-controls-gap); }\n.buttons { display: flex; gap: var(--cc-controls-gap); }\n.time { justify-self: start; }\n.speed { justify-self: end; }\n/* Narrow players: buttons on top, time and speed below. */\n@container (max-width: 300px) {\n .controls { grid-template-columns: 1fr 1fr; grid-template-areas: \"buttons buttons\" \"time speed\"; }\n .buttons { grid-area: buttons; justify-content: center; }\n .time { grid-area: time; }\n .speed { grid-area: speed; }\n}\n:host([controls=\"none\"]) .controls { display: none; }\n/* Live (following a smart cube): nothing to replay. */\n:host([live]) .controls, :host([live]) .progress { display: none; }\nbutton {\n display: inline-grid; place-items: center;\n min-width: var(--cc-button-size); height: var(--cc-button-size);\n padding: 0 8px;\n border: 0;\n border-radius: var(--cc-control-radius);\n background: var(--cc-control-bg);\n color: var(--cc-control-fg);\n font: 600 12px/1 var(--cc-font);\n cursor: pointer;\n transition: background 0.12s ease, transform 0.08s ease;\n}\nbutton:hover { background: var(--cc-control-bg-hover); }\nbutton:active { transform: scale(0.94); }\nbutton:focus-visible { outline: 2px solid var(--cc-accent); outline-offset: 2px; }\nbutton:disabled { opacity: 0.4; cursor: default; transform: none; }\n/* Icons never take the click: it always lands on the button itself, even if the icon changes mid-click. */\nbutton svg { width: 18px; height: 18px; display: block; fill: currentColor; pointer-events: none; }\n.play { min-width: calc(var(--cc-button-size) * 1.25); background: var(--cc-accent); color: #fff; }\n.play:hover { background: color-mix(in srgb, var(--cc-accent) 85%, #000); }\n.time { font: 500 12px/1 var(--cc-font); opacity: 0.75; white-space: nowrap; font-variant-numeric: tabular-nums; }\n";
|
|
24
24
|
/** Inline icons (no icon font, no network). */
|
|
25
25
|
export declare const ICONS: {
|
|
26
|
-
readonly start: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"
|
|
27
|
-
readonly back: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"
|
|
26
|
+
readonly start: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M4 5h2v14H4zM13 6.3v11.4a.5.5 0 0 1-.8.4L6.9 12.4a.5.5 0 0 1 0-.8l5.3-5.7a.5.5 0 0 1 .8.4zM20.5 6.3v11.4a.5.5 0 0 1-.8.4l-5.3-5.7a.5.5 0 0 1 0-.8l5.3-5.7a.5.5 0 0 1 .8.4z\"/></svg>";
|
|
27
|
+
readonly back: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M16 6.2v11.6a.5.5 0 0 1-.8.4l-7.7-5.8a.5.5 0 0 1 0-.8l7.7-5.8a.5.5 0 0 1 .8.4z\"/></svg>";
|
|
28
28
|
readonly play: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M8 5.6v12.8a.6.6 0 0 0 .92.5l10.1-6.4a.6.6 0 0 0 0-1L8.92 5.1a.6.6 0 0 0-.92.5z\"/></svg>";
|
|
29
29
|
readonly pause: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><rect x=\"6.5\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\"/><rect x=\"13.5\" y=\"5\" width=\"4\" height=\"14\" rx=\"1\"/></svg>";
|
|
30
|
-
readonly forward: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"
|
|
31
|
-
readonly end: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"
|
|
30
|
+
readonly forward: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M8 6.2v11.6a.5.5 0 0 0 .8.4l7.7-5.8a.5.5 0 0 0 0-.8L8.8 5.8a.5.5 0 0 0-.8.4z\"/></svg>";
|
|
31
|
+
readonly end: "<svg viewBox=\"0 0 24 24\" aria-hidden=\"true\"><path d=\"M18 5h2v14h-2zM3.5 6.3v11.4a.5.5 0 0 0 .8.4l5.3-5.7a.5.5 0 0 0 0-.8L4.3 5.9a.5.5 0 0 0-.8.4zM11 6.3v11.4a.5.5 0 0 0 .8.4l5.3-5.7a.5.5 0 0 0 0-.8l-5.3-5.7a.5.5 0 0 0-.8.4z\"/></svg>";
|
|
32
32
|
};
|
package/element/styles.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - ::part() for anything else: stage, flat (the 2D picture), progress, progress-track,
|
|
13
13
|
* progress-fill, progress-thumb, progress-marker, segment (+ segment-<id>),
|
|
14
14
|
* segment-played, segment-recognition, segment-labels, segment-label
|
|
15
|
-
* (+ segment-label-<id>), tooltip, tooltip-text, controls, buttons, button,
|
|
15
|
+
* (+ segment-label-<id>), tooltip, tooltip-text (tooltip-title, tooltip-details), controls, buttons, button,
|
|
16
16
|
* button-start, button-back, button-play, button-forward, button-end,
|
|
17
17
|
* button-speed, time.
|
|
18
18
|
* - Replace the controls entirely: put your own element in slot="controls"
|
|
@@ -47,9 +47,11 @@ export const STYLES = /* css */ `
|
|
|
47
47
|
--cc-segment-height: calc(var(--cc-progress-height) + 2px);
|
|
48
48
|
--cc-segment-gap: 2px;
|
|
49
49
|
--cc-segment-unplayed: 30%;
|
|
50
|
-
/* popup:
|
|
51
|
-
--cc-tooltip-bg:
|
|
52
|
-
--cc-tooltip-fg:
|
|
50
|
+
/* popup: dark and solid by default — readable on light and dark pages and over the cube */
|
|
51
|
+
--cc-tooltip-bg: rgb(17 20 28 / 0.97);
|
|
52
|
+
--cc-tooltip-fg: #f4f5f7;
|
|
53
|
+
--cc-tooltip-muted: #a9afbb;
|
|
54
|
+
--cc-tooltip-border: rgb(255 255 255 / 0.12);
|
|
53
55
|
display: flex;
|
|
54
56
|
flex-direction: column;
|
|
55
57
|
gap: 10px;
|
|
@@ -113,15 +115,17 @@ export const STYLES = /* css */ `
|
|
|
113
115
|
width: max-content; max-width: min(320px, 100%);
|
|
114
116
|
padding: 6px 9px;
|
|
115
117
|
border-radius: 8px;
|
|
116
|
-
/* colour stays the page's text colour here, so the background can be derived from it… */
|
|
117
118
|
background: var(--cc-tooltip-bg);
|
|
118
|
-
|
|
119
|
+
border: 1px solid var(--cc-tooltip-border);
|
|
120
|
+
font: 500 12px/1.4 system-ui, sans-serif;
|
|
119
121
|
pointer-events: none;
|
|
120
122
|
z-index: 2;
|
|
121
|
-
box-shadow: 0
|
|
123
|
+
box-shadow: 0 8px 24px -8px rgb(0 0 0 / 0.6);
|
|
124
|
+
display: flex; flex-direction: column; gap: 1px;
|
|
122
125
|
}
|
|
123
|
-
/* …and the text inverts it. */
|
|
124
126
|
.tooltip .tip { color: var(--cc-tooltip-fg); }
|
|
127
|
+
.tooltip .tip-title { font-weight: 650; font-variant-numeric: tabular-nums; }
|
|
128
|
+
.tooltip .tip-details { color: var(--cc-tooltip-muted); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
125
129
|
.tooltip[hidden] { display: none; }
|
|
126
130
|
.labels { display: none; position: relative; height: 18px; margin-top: -4px; }
|
|
127
131
|
:host([segment-labels]) .labels { display: block; }
|
|
@@ -187,10 +191,11 @@ button svg { width: 18px; height: 18px; display: block; fill: currentColor; poin
|
|
|
187
191
|
`;
|
|
188
192
|
/** Inline icons (no icon font, no network). */
|
|
189
193
|
export const ICONS = {
|
|
190
|
-
|
|
191
|
-
|
|
194
|
+
// To the start / end: two triangles against a bar; one move back / on: a single triangle (as cubing.js).
|
|
195
|
+
start: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5h2v14H4zM13 6.3v11.4a.5.5 0 0 1-.8.4L6.9 12.4a.5.5 0 0 1 0-.8l5.3-5.7a.5.5 0 0 1 .8.4zM20.5 6.3v11.4a.5.5 0 0 1-.8.4l-5.3-5.7a.5.5 0 0 1 0-.8l5.3-5.7a.5.5 0 0 1 .8.4z"/></svg>',
|
|
196
|
+
back: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16 6.2v11.6a.5.5 0 0 1-.8.4l-7.7-5.8a.5.5 0 0 1 0-.8l7.7-5.8a.5.5 0 0 1 .8.4z"/></svg>',
|
|
192
197
|
play: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5.6v12.8a.6.6 0 0 0 .92.5l10.1-6.4a.6.6 0 0 0 0-1L8.92 5.1a.6.6 0 0 0-.92.5z"/></svg>',
|
|
193
198
|
pause: '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="6.5" y="5" width="4" height="14" rx="1"/><rect x="13.5" y="5" width="4" height="14" rx="1"/></svg>',
|
|
194
|
-
forward: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="
|
|
195
|
-
end: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="
|
|
199
|
+
forward: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 6.2v11.6a.5.5 0 0 0 .8.4l7.7-5.8a.5.5 0 0 0 0-.8L8.8 5.8a.5.5 0 0 0-.8.4z"/></svg>',
|
|
200
|
+
end: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M18 5h2v14h-2zM3.5 6.3v11.4a.5.5 0 0 0 .8.4l5.3-5.7a.5.5 0 0 0 0-.8L4.3 5.9a.5.5 0 0 0-.8.4zM11 6.3v11.4a.5.5 0 0 0 .8.4l5.3-5.7a.5.5 0 0 0 0-.8l-5.3-5.7a.5.5 0 0 0-.8.4z"/></svg>',
|
|
196
201
|
};
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.1.
|
|
6
|
+
"version": "0.1.2",
|
|
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>",
|
package/solve/twophase.js
CHANGED
|
@@ -79,6 +79,54 @@ function moveTable(n, make, coord, moves) {
|
|
|
79
79
|
}
|
|
80
80
|
return t;
|
|
81
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* A move table over permutations of `n` pieces, straight on ranks: for each
|
|
84
|
+
* rank, the permutation once, then per move `next[i] = perm[map[i]]` (map =
|
|
85
|
+
* where the move takes each position from, restricted to these `n` pieces)
|
|
86
|
+
* and its rank — no objects per entry (8! × 18 entries: this is most of the
|
|
87
|
+
* build time otherwise).
|
|
88
|
+
*/
|
|
89
|
+
function permMoveTable(n, maps) {
|
|
90
|
+
const count = maps.length;
|
|
91
|
+
let size = 1;
|
|
92
|
+
for (let i = 2; i <= n; i++)
|
|
93
|
+
size *= i;
|
|
94
|
+
const t = new Uint16Array(size * count);
|
|
95
|
+
const perm = new Int8Array(n), next = new Int8Array(n), pool = new Int8Array(n);
|
|
96
|
+
for (let r = 0; r < size; r++) {
|
|
97
|
+
// unrank (Lehmer code, same order as core permRank / permUnrank)
|
|
98
|
+
let rank = r;
|
|
99
|
+
for (let i = 0; i < n; i++)
|
|
100
|
+
pool[i] = i;
|
|
101
|
+
let left = n;
|
|
102
|
+
for (let i = 0; i < n; i++) {
|
|
103
|
+
let f = 1;
|
|
104
|
+
for (let k = 2; k < n - i; k++)
|
|
105
|
+
f *= k;
|
|
106
|
+
const d = Math.floor(rank / f);
|
|
107
|
+
rank -= d * f;
|
|
108
|
+
perm[i] = pool[d];
|
|
109
|
+
for (let k = d; k < left - 1; k++)
|
|
110
|
+
pool[k] = pool[k + 1];
|
|
111
|
+
left--;
|
|
112
|
+
}
|
|
113
|
+
for (let m = 0; m < count; m++) {
|
|
114
|
+
const map = maps[m];
|
|
115
|
+
for (let i = 0; i < n; i++)
|
|
116
|
+
next[i] = perm[map[i]];
|
|
117
|
+
let out = 0;
|
|
118
|
+
for (let i = 0; i < n; i++) {
|
|
119
|
+
let smaller = 0;
|
|
120
|
+
for (let j = i + 1; j < n; j++)
|
|
121
|
+
if (next[j] < next[i])
|
|
122
|
+
smaller++;
|
|
123
|
+
out = out * (n - i) + smaller;
|
|
124
|
+
}
|
|
125
|
+
t[r * count + m] = out;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return t;
|
|
129
|
+
}
|
|
82
130
|
/** Breadth-first distances over a product of two coordinates. */
|
|
83
131
|
function pruneTable(n1, n2, move1, move2, moveCount, start) {
|
|
84
132
|
const dist = new Int8Array(n1 * n2).fill(-1);
|
|
@@ -104,8 +152,9 @@ function buildTables() {
|
|
|
104
152
|
const twistMove = moveTable(N_TWIST, withTwist, twistOf, all18);
|
|
105
153
|
const flipMove = moveTable(N_FLIP, withFlip, flipOf, all18);
|
|
106
154
|
const sliceMove = moveTable(N_SLICE, withSlice, sliceOf, all18);
|
|
107
|
-
const cornerMove =
|
|
108
|
-
|
|
155
|
+
const cornerMove = permMoveTable(8, all18.map((m) => MOVE_CC[m].cp));
|
|
156
|
+
// P2 moves keep the U / D edges (positions 0–7) among themselves.
|
|
157
|
+
const udEdgeMove = permMoveTable(8, P2_MOVES.map((m) => MOVE_CC[m].ep.slice(0, 8)));
|
|
109
158
|
const slicePermMove = Uint8Array.from(moveTable(N_SLICEPERM, (i) => ({ ...SOLVED_CC, ep: [0, 1, 2, 3, 4, 5, 6, 7, ...permUnrank(i, 4).map((e) => e + 8)] }), (c) => permRank(c.ep.slice(8).map((e) => e - 8)), P2_MOVES));
|
|
110
159
|
// Phase-2 pruning walks P2 moves: pick the corner-table columns for them.
|
|
111
160
|
const cornerP2 = new Uint16Array(N_PERM8 * P2_MOVES.length);
|
package/timeline/timings.js
CHANGED
|
@@ -17,7 +17,8 @@ export function stageTimings(method, rec) {
|
|
|
17
17
|
// A move's timestamp is when it completed; its start is ~the previous move's time.
|
|
18
18
|
const firstStart = moveCount > 0 ? (prevIndex === 0 ? 0 : rec.moves[prevIndex - 1].t) : endMs;
|
|
19
19
|
const firstDone = moveCount > 0 ? rec.moves[prevIndex].t : endMs;
|
|
20
|
-
|
|
20
|
+
// AUF: a U turn or two — nothing to recognise, the whole stage is turning.
|
|
21
|
+
const recognitionMs = moveCount > 0 && b.stage !== "auf" ? Math.max(0, firstDone - firstStart - estimateTurnMs(rec)) : 0;
|
|
21
22
|
const t = {
|
|
22
23
|
stage: b.stage,
|
|
23
24
|
label: labels.get(b.stage) ?? b.stage,
|