@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.
- package/LICENSE +373 -0
- package/README.md +118 -0
- package/analyze/client.d.ts +18 -0
- package/analyze/client.js +42 -0
- package/analyze/index.d.ts +114 -0
- package/analyze/index.js +151 -0
- package/analyze/rotations.d.ts +3 -0
- package/analyze/rotations.js +15 -0
- package/analyze/roux.d.ts +67 -0
- package/analyze/roux.js +95 -0
- package/analyze/worker.d.ts +6 -0
- package/analyze/worker.js +31 -0
- package/analyze/zz.d.ts +47 -0
- package/analyze/zz.js +102 -0
- package/bld/index.d.ts +14 -0
- package/bld/index.js +14 -0
- package/bld/memo.d.ts +66 -0
- package/bld/memo.js +139 -0
- package/bld/scheme.d.ts +30 -0
- package/bld/scheme.js +35 -0
- package/bld/skin.d.ts +26 -0
- package/bld/skin.js +46 -0
- package/bld/tracker.d.ts +68 -0
- package/bld/tracker.js +145 -0
- package/bluetooth/clock.d.ts +22 -0
- package/bluetooth/clock.js +37 -0
- package/bluetooth/cubeSkins.d.ts +31 -0
- package/bluetooth/cubeSkins.js +32 -0
- package/bluetooth/gyro.d.ts +31 -0
- package/bluetooth/gyro.js +46 -0
- package/bluetooth/index.d.ts +5 -0
- package/bluetooth/index.js +5 -0
- package/bluetooth/session.d.ts +140 -0
- package/bluetooth/session.js +201 -0
- package/bluetooth/simulated.d.ts +52 -0
- package/bluetooth/simulated.js +80 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-definitions.d.ts +38 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-definitions.js +51 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-encrypter.d.ts +35 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-encrypter.js +70 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-protocol.d.ts +273 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-cube-protocol.js +846 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen1.d.ts +38 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen1.js +340 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen234-packet-validate.d.ts +11 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-gen234-packet-validate.js +118 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-mac-salt.d.ts +2 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-mac-salt.js +15 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-cube.d.ts +18 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-cube.js +152 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-timer.d.ts +74 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-smart-timer.js +161 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-write-queue.d.ts +7 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gan-write-queue.js +11 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gatt-characteristic-write.d.ts +2 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/gatt-characteristic-write.js +36 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/index.d.ts +4 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/index.js +4 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.d.ts +12 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/address-hints.js +122 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/build-picker-options.d.ts +11 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/build-picker-options.js +55 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/default-optional-services.d.ts +21 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/default-optional-services.js +47 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/device-name-scan-filters.d.ts +6 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/device-name-scan-filters.js +24 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/gatt-snapshot.d.ts +6 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/gatt-snapshot.js +57 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-candidates.d.ts +11 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-candidates.js +67 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-moyu32.d.ts +8 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-moyu32.js +98 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-qiyi.d.ts +8 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/mac-probe-qiyi.js +128 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/moyu32-session-crypto.d.ts +5 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/moyu32-session-crypto.js +60 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/normalize-uuid.d.ts +2 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/normalize-uuid.js +8 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/packet-sanity.d.ts +7 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/packet-sanity.js +142 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/profile-rank.d.ts +6 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/profile-rank.js +21 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/types.d.ts +32 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/attachment/types.js +1 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/ble-utils.d.ts +6 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/ble-utils.js +68 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/connect.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/connect.js +178 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/cubie-cube.d.ts +16 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/cubie-cube.js +141 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/index.d.ts +11 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/index.js +10 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocol.d.ts +26 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocol.js +8 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gan.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gan.js +265 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/giiker.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/giiker.js +345 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gocube.d.ts +15 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/gocube.js +382 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-mhc.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-mhc.js +352 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-v1.d.ts +57 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu-v1.js +243 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu32.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/moyu32.js +454 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/qiyi.d.ts +3 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/protocols/qiyi.js +424 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/types.d.ts +78 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/smartcube/types.js +1 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/utils.d.ts +45 -0
- package/bluetooth/vendor/smartcube-web-bluetooth/utils.js +150 -0
- package/cfop/caseData.d.ts +16 -0
- package/cfop/caseData.js +88 -0
- package/cfop/cases.d.ts +42 -0
- package/cfop/cases.js +116 -0
- package/cfop/f2l.d.ts +28 -0
- package/cfop/f2l.js +88 -0
- package/cfop/f2lData.d.ts +15 -0
- package/cfop/f2lData.js +49 -0
- package/cfop/index.d.ts +41 -0
- package/cfop/index.js +110 -0
- package/cfop/slots.d.ts +2 -0
- package/cfop/slots.js +1 -0
- package/core/arrows.d.ts +24 -0
- package/core/arrows.js +22 -0
- package/core/bytes.d.ts +27 -0
- package/core/bytes.js +145 -0
- package/core/centers.d.ts +32 -0
- package/core/centers.js +83 -0
- package/core/checks.d.ts +53 -0
- package/core/checks.js +85 -0
- package/core/cubies.d.ts +45 -0
- package/core/cubies.js +169 -0
- package/core/frames.d.ts +61 -0
- package/core/frames.js +126 -0
- package/core/geometry.d.ts +58 -0
- package/core/geometry.js +132 -0
- package/core/index.d.ts +19 -0
- package/core/index.js +19 -0
- package/core/masks.d.ts +32 -0
- package/core/masks.js +39 -0
- package/core/method.d.ts +35 -0
- package/core/method.js +30 -0
- package/core/moves.d.ts +48 -0
- package/core/moves.js +88 -0
- package/core/notation.d.ts +48 -0
- package/core/notation.js +226 -0
- package/core/physical.d.ts +53 -0
- package/core/physical.js +99 -0
- package/core/practice.d.ts +62 -0
- package/core/practice.js +87 -0
- package/core/sequence.d.ts +90 -0
- package/core/sequence.js +157 -0
- package/core/stages.d.ts +90 -0
- package/core/stages.js +21 -0
- package/core/state.d.ts +55 -0
- package/core/state.js +140 -0
- package/core/stateCodec.d.ts +12 -0
- package/core/stateCodec.js +51 -0
- package/core/stream.d.ts +45 -0
- package/core/stream.js +75 -0
- package/core/tracker.d.ts +62 -0
- package/core/tracker.js +121 -0
- package/element/alg.d.ts +45 -0
- package/element/alg.js +150 -0
- package/element/base.d.ts +2 -0
- package/element/base.js +3 -0
- package/element/bld.d.ts +82 -0
- package/element/bld.js +222 -0
- package/element/index.d.ts +8 -0
- package/element/index.js +21 -0
- package/element/model.d.ts +36 -0
- package/element/model.js +64 -0
- package/element/player.d.ts +196 -0
- package/element/player.js +680 -0
- package/element/practice.d.ts +75 -0
- package/element/practice.js +150 -0
- package/element/scramble.d.ts +50 -0
- package/element/scramble.js +107 -0
- package/element/sequence.d.ts +126 -0
- package/element/sequence.js +254 -0
- package/element/styles.d.ts +32 -0
- package/element/styles.js +196 -0
- package/image/cache.d.ts +21 -0
- package/image/cache.js +66 -0
- package/image/index.d.ts +2 -0
- package/image/index.js +2 -0
- package/image/png-node.d.ts +6 -0
- package/image/png-node.js +10 -0
- package/image/svg.d.ts +36 -0
- package/image/svg.js +215 -0
- package/lbl/index.d.ts +10 -0
- package/lbl/index.js +31 -0
- package/methods/index.d.ts +20 -0
- package/methods/index.js +33 -0
- package/package.json +133 -0
- package/petrus/index.d.ts +11 -0
- package/petrus/index.js +27 -0
- package/react/index.d.ts +176 -0
- package/react/index.js +228 -0
- package/render/arrows.d.ts +68 -0
- package/render/arrows.js +190 -0
- package/render/build/attachments.d.ts +35 -0
- package/render/build/attachments.js +94 -0
- package/render/build/models.d.ts +9 -0
- package/render/build/models.js +27 -0
- package/render/build/tiles.d.ts +48 -0
- package/render/build/tiles.js +194 -0
- package/render/gltf.d.ts +10 -0
- package/render/gltf.js +106 -0
- package/render/index.d.ts +12 -0
- package/render/index.js +12 -0
- package/render/layers.d.ts +21 -0
- package/render/layers.js +24 -0
- package/render/pictures.d.ts +46 -0
- package/render/pictures.js +80 -0
- package/render/pieceModels.d.ts +38 -0
- package/render/pieceModels.js +43 -0
- package/render/renderer.d.ts +165 -0
- package/render/renderer.js +647 -0
- package/render/tile.d.ts +114 -0
- package/render/tile.js +390 -0
- package/render/viewports.d.ts +25 -0
- package/render/viewports.js +26 -0
- package/roux/cmll.d.ts +35 -0
- package/roux/cmll.js +121 -0
- package/roux/cmllData.d.ts +12 -0
- package/roux/cmllData.js +49 -0
- package/roux/index.d.ts +52 -0
- package/roux/index.js +175 -0
- package/skin/attachments.d.ts +82 -0
- package/skin/attachments.js +110 -0
- package/skin/index.d.ts +3 -0
- package/skin/index.js +3 -0
- package/skin/shapes.d.ts +112 -0
- package/skin/shapes.js +147 -0
- package/skin/skin.d.ts +911 -0
- package/skin/skin.js +412 -0
- package/solve/client.d.ts +58 -0
- package/solve/client.js +56 -0
- package/solve/handle.d.ts +6 -0
- package/solve/handle.js +38 -0
- package/solve/index.d.ts +8 -0
- package/solve/index.js +7 -0
- package/solve/lse.d.ts +42 -0
- package/solve/lse.js +151 -0
- package/solve/pieces.d.ts +18 -0
- package/solve/pieces.js +24 -0
- package/solve/protocol.d.ts +70 -0
- package/solve/protocol.js +16 -0
- package/solve/scramble.d.ts +76 -0
- package/solve/scramble.js +187 -0
- package/solve/stage.d.ts +108 -0
- package/solve/stage.js +532 -0
- package/solve/twophase.d.ts +36 -0
- package/solve/twophase.js +229 -0
- package/solve/worker.d.ts +6 -0
- package/solve/worker.js +15 -0
- package/timeline/codec.d.ts +23 -0
- package/timeline/codec.js +84 -0
- package/timeline/index.d.ts +7 -0
- package/timeline/index.js +7 -0
- package/timeline/recording.d.ts +19 -0
- package/timeline/recording.js +20 -0
- package/timeline/replay.d.ts +68 -0
- package/timeline/replay.js +145 -0
- package/timeline/segments.d.ts +31 -0
- package/timeline/segments.js +39 -0
- package/timeline/share.d.ts +30 -0
- package/timeline/share.js +62 -0
- package/timeline/stats.d.ts +34 -0
- package/timeline/stats.js +56 -0
- package/timeline/timings.d.ts +30 -0
- package/timeline/timings.js +54 -0
- package/zz/index.d.ts +29 -0
- package/zz/index.js +68 -0
- package/zz/seeds.d.ts +9 -0
- package/zz/seeds.js +29 -0
package/react/index.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React bindings — thin wrappers around the cubecore elements plus hooks for
|
|
3
|
+
* a smart cube and the solver worker. No styling opinions: the elements'
|
|
4
|
+
* CSS variables and ::part() work as usual (className / style pass through).
|
|
5
|
+
*
|
|
6
|
+
* <CubePlayer alg="R U R' U'" progress segmentLabels onEnded={…} ref={player} />
|
|
7
|
+
* const cube = useSmartCube(); // cube.connect() / cube.simulate(); cube.state, cube.session
|
|
8
|
+
* <CubePlayer live={cube.session} autoSkin />
|
|
9
|
+
* <CubeScramble scramble={moves} source={cube.session} editable onComplete={start} />
|
|
10
|
+
* <CubeAlgPractice alg="R U R' U R U2 R'" source={cube.session} reveal="done" onComplete={…} />
|
|
11
|
+
*
|
|
12
|
+
* Works with React 18 and 19: attributes are passed as attributes (booleans
|
|
13
|
+
* only when true), objects are set as properties from effects, events are
|
|
14
|
+
* listened to on the element.
|
|
15
|
+
*/
|
|
16
|
+
import { createElement, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState, } from "react";
|
|
17
|
+
import "../element/index.js"; // registers the elements (in the browser)
|
|
18
|
+
/** Listen to element events from an effect; handlers may change every render. */
|
|
19
|
+
function useEvents(ref, handlers) {
|
|
20
|
+
const latest = useRef(handlers);
|
|
21
|
+
latest.current = handlers;
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const el = ref.current;
|
|
24
|
+
if (!el)
|
|
25
|
+
return;
|
|
26
|
+
const names = Object.keys(latest.current);
|
|
27
|
+
const listeners = names.map((name) => {
|
|
28
|
+
const l = (e) => latest.current[name]?.(e.detail);
|
|
29
|
+
el.addEventListener(name, l);
|
|
30
|
+
return [name, l];
|
|
31
|
+
});
|
|
32
|
+
return () => listeners.forEach(([name, l]) => el.removeEventListener(name, l));
|
|
33
|
+
}, [ref]);
|
|
34
|
+
}
|
|
35
|
+
const flag = (on) => (on ? "" : undefined);
|
|
36
|
+
export const CubePlayer = forwardRef(function CubePlayer(p, ref) {
|
|
37
|
+
const el = useRef(null);
|
|
38
|
+
useImperativeHandle(ref, () => el.current, []);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (el.current)
|
|
41
|
+
el.current.recording = p.recording ?? null;
|
|
42
|
+
}, [p.recording]);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (el.current)
|
|
45
|
+
el.current.method = p.method ?? null;
|
|
46
|
+
}, [p.method]);
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (el.current && p.segments !== undefined)
|
|
49
|
+
el.current.segments = p.segments;
|
|
50
|
+
}, [p.segments]);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (el.current)
|
|
53
|
+
el.current.mask = p.mask ?? null;
|
|
54
|
+
}, [p.mask]);
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (el.current && p.skin && typeof p.skin !== "string")
|
|
57
|
+
el.current.skin = p.skin;
|
|
58
|
+
}, [p.skin]);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (el.current && p.formatSegment)
|
|
61
|
+
el.current.formatSegment = p.formatSegment;
|
|
62
|
+
}, [p.formatSegment]);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!el.current || !p.live)
|
|
65
|
+
return;
|
|
66
|
+
return el.current.attach(p.live, { autoSkin: p.autoSkin });
|
|
67
|
+
}, [p.live, p.autoSkin]);
|
|
68
|
+
useEvents(el, { timeupdate: p.onTimeUpdate, play: p.onPlay, pause: p.onPause, ended: p.onEnded, segmentchange: p.onSegmentChange, error: p.onError });
|
|
69
|
+
return createElement("cube-player", {
|
|
70
|
+
ref: el,
|
|
71
|
+
alg: p.alg,
|
|
72
|
+
setup: p.setup,
|
|
73
|
+
anchor: p.anchor,
|
|
74
|
+
tempo: p.tempo,
|
|
75
|
+
skin: typeof p.skin === "string" ? p.skin : undefined,
|
|
76
|
+
theme: p.theme,
|
|
77
|
+
visualization: p.visualization,
|
|
78
|
+
"back-view": p.backView,
|
|
79
|
+
controls: p.controls,
|
|
80
|
+
progress: flag(p.progress),
|
|
81
|
+
markers: flag(p.markers),
|
|
82
|
+
"segment-labels": flag(p.segmentLabels),
|
|
83
|
+
tooltips: p.tooltips === false ? "off" : undefined,
|
|
84
|
+
"max-pause": p.maxPause,
|
|
85
|
+
id: p.id,
|
|
86
|
+
className: p.className,
|
|
87
|
+
style: p.style,
|
|
88
|
+
}, p.children);
|
|
89
|
+
});
|
|
90
|
+
export const CubeScramble = forwardRef(function CubeScramble(p, ref) {
|
|
91
|
+
const el = useRef(null);
|
|
92
|
+
useImperativeHandle(ref, () => el.current, []);
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (!el.current || !p.source)
|
|
95
|
+
return;
|
|
96
|
+
return el.current.attach(p.source);
|
|
97
|
+
}, [p.source]);
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (el.current)
|
|
100
|
+
el.current.scramble = p.scramble;
|
|
101
|
+
}, [p.scramble]);
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (el.current && p.messages)
|
|
104
|
+
el.current.messages = p.messages;
|
|
105
|
+
}, [p.messages]);
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (el.current)
|
|
108
|
+
el.current.player = p.player?.current ?? null;
|
|
109
|
+
});
|
|
110
|
+
useEvents(el, { progress: p.onProgress, complete: p.onComplete, change: p.onChange });
|
|
111
|
+
return createElement("cube-scramble", { ref: el, editable: p.editable ? "" : undefined, arrows: p.arrows ? "" : undefined, "arrow-shape": p.arrowShape, id: p.id, className: p.className, style: p.style });
|
|
112
|
+
});
|
|
113
|
+
export const CubeAlgPractice = forwardRef(function CubeAlgPractice(p, ref) {
|
|
114
|
+
const el = useRef(null);
|
|
115
|
+
useImperativeHandle(ref, () => el.current, []);
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!el.current || !p.source)
|
|
118
|
+
return;
|
|
119
|
+
return el.current.attach(p.source);
|
|
120
|
+
}, [p.source]);
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (el.current)
|
|
123
|
+
el.current.alg = p.alg;
|
|
124
|
+
}, [p.alg]);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (el.current && p.messages)
|
|
127
|
+
el.current.messages = p.messages;
|
|
128
|
+
}, [p.messages]);
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (el.current)
|
|
131
|
+
el.current.player = p.player?.current ?? null;
|
|
132
|
+
});
|
|
133
|
+
useEvents(el, { progress: p.onProgress, mistake: p.onMistake, complete: p.onComplete });
|
|
134
|
+
return createElement("cube-alg-practice", {
|
|
135
|
+
ref: el,
|
|
136
|
+
reveal: p.reveal,
|
|
137
|
+
arrows: p.arrows ? "" : undefined,
|
|
138
|
+
"arrow-shape": p.arrowShape,
|
|
139
|
+
"hint-on-mistake": p.hintOnMistake === false ? "off" : undefined,
|
|
140
|
+
controls: p.controls,
|
|
141
|
+
headless: p.headless ? "" : undefined,
|
|
142
|
+
id: p.id,
|
|
143
|
+
className: p.className,
|
|
144
|
+
style: p.style,
|
|
145
|
+
}, p.children);
|
|
146
|
+
});
|
|
147
|
+
export const CubeBld = forwardRef(function CubeBld(p, ref) {
|
|
148
|
+
const el = useRef(null);
|
|
149
|
+
useImperativeHandle(ref, () => el.current, []);
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
if (!el.current || !p.source)
|
|
152
|
+
return;
|
|
153
|
+
return el.current.attach(p.source);
|
|
154
|
+
}, [p.source]);
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (el.current && p.messages)
|
|
157
|
+
el.current.messages = p.messages;
|
|
158
|
+
}, [p.messages]);
|
|
159
|
+
useEvents(el, { progress: p.onProgress, letter: p.onLetter, wrong: p.onWrong, complete: p.onComplete });
|
|
160
|
+
return createElement("cube-bld", { ref: el, scheme: p.scheme, rotation: p.rotation, reveal: p.reveal, headless: p.headless ? "" : undefined, id: p.id, className: p.className, style: p.style }, p.children);
|
|
161
|
+
});
|
|
162
|
+
export const CubeAlg = forwardRef(function CubeAlg(p, ref) {
|
|
163
|
+
const el = useRef(null);
|
|
164
|
+
useImperativeHandle(ref, () => el.current, []);
|
|
165
|
+
useEffect(() => {
|
|
166
|
+
if (el.current && p.player?.current)
|
|
167
|
+
el.current.attach(p.player.current);
|
|
168
|
+
});
|
|
169
|
+
return createElement("cube-alg", { ref: el, for: p.for, alg: p.alg, id: p.id, className: p.className, style: p.style });
|
|
170
|
+
});
|
|
171
|
+
/** A smart cube as React state: the session, its state after every move, battery, connection. */
|
|
172
|
+
export function useSmartCube() {
|
|
173
|
+
const [session, setSession] = useState(null);
|
|
174
|
+
const [state, setState] = useState(null);
|
|
175
|
+
const [battery, setBattery] = useState(null);
|
|
176
|
+
const [error, setError] = useState(null);
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
if (!session)
|
|
179
|
+
return;
|
|
180
|
+
setState(session.state);
|
|
181
|
+
setBattery(session.battery);
|
|
182
|
+
const offs = [
|
|
183
|
+
session.on("state", (e) => setState(e.state)),
|
|
184
|
+
session.on("battery", (b) => setBattery(b)),
|
|
185
|
+
session.on("disconnect", () => setSession(null)),
|
|
186
|
+
];
|
|
187
|
+
return () => offs.forEach((off) => off());
|
|
188
|
+
}, [session]);
|
|
189
|
+
useEffect(() => () => void session?.disconnect(), [session]);
|
|
190
|
+
const connect = useCallback(async () => {
|
|
191
|
+
setError(null);
|
|
192
|
+
try {
|
|
193
|
+
const { SmartCubeSession } = await import("../bluetooth/index.js");
|
|
194
|
+
setSession(await SmartCubeSession.connect({ enableAddressSearch: true }));
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
setError(e instanceof Error ? e.message : String(e));
|
|
198
|
+
}
|
|
199
|
+
}, []);
|
|
200
|
+
const simulate = useCallback(async () => {
|
|
201
|
+
const { SimulatedCube, SmartCubeSession } = await import("../bluetooth/index.js");
|
|
202
|
+
setSession(new SmartCubeSession(new SimulatedCube()));
|
|
203
|
+
}, []);
|
|
204
|
+
const disconnect = useCallback(async () => {
|
|
205
|
+
await session?.disconnect();
|
|
206
|
+
setSession(null);
|
|
207
|
+
}, [session]);
|
|
208
|
+
return { session, state, battery, connected: !!session, error, connect, simulate, disconnect };
|
|
209
|
+
}
|
|
210
|
+
/** The solver worker for this component's lifetime (null until created, e.g. during SSR). */
|
|
211
|
+
export function useSolverWorker(url) {
|
|
212
|
+
const [client, setClient] = useState(null);
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
let c = null;
|
|
215
|
+
let cancelled = false;
|
|
216
|
+
import("../solve/index.js").then(({ createSolverWorker }) => {
|
|
217
|
+
if (cancelled)
|
|
218
|
+
return;
|
|
219
|
+
c = createSolverWorker(url);
|
|
220
|
+
setClient(c);
|
|
221
|
+
});
|
|
222
|
+
return () => {
|
|
223
|
+
cancelled = true;
|
|
224
|
+
c?.terminate();
|
|
225
|
+
};
|
|
226
|
+
}, [String(url ?? "")]);
|
|
227
|
+
return client;
|
|
228
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn arrows in 3D: a ribbon with an arrowhead hovering just above the
|
|
3
|
+
* layer that is about to turn — centred on its row, flat side to the cube,
|
|
4
|
+
* rounding the cube's edges — the way the layer moves.
|
|
5
|
+
*
|
|
6
|
+
* One length for every arrow; two per layer on opposite sides, travelling
|
|
7
|
+
* round the layer the way they point (so one is always in view, and the
|
|
8
|
+
* motion shows the direction); as many heads as quarter turns (1–3).
|
|
9
|
+
* Shapes: "box" follows the faces at an even height, "circle" is an arc of
|
|
10
|
+
* a circle over them (clear of the corners).
|
|
11
|
+
*
|
|
12
|
+
* The direction is the one written (R2' the other way than R2).
|
|
13
|
+
*
|
|
14
|
+
* The path around a layer is the cube's square cross-section pushed out by
|
|
15
|
+
* HOVER (straight over the faces, round over the edges), measured in
|
|
16
|
+
* quarters t: t = k at the middle of side k (side 0 faces +e1, side 1 +e2…),
|
|
17
|
+
* t = k + 0.5 at the edge between sides k and k+1; increasing t is
|
|
18
|
+
* counter-clockwise about +axis — the same sense as TurnArrow.quarters.
|
|
19
|
+
*/
|
|
20
|
+
import { Group, Vector3 } from "three";
|
|
21
|
+
import type { Axis, TurnArrow } from "../core/index.js";
|
|
22
|
+
export interface ArrowStyle {
|
|
23
|
+
/** Arrow colour. Default a clear blue. */
|
|
24
|
+
color?: string;
|
|
25
|
+
/** 0..1. Default 0.95. */
|
|
26
|
+
opacity?: number;
|
|
27
|
+
/** Size factor (ribbon width, heads). Default 1. */
|
|
28
|
+
scale?: number;
|
|
29
|
+
/** "box": even height over the faces, round over the edges (default); "circle": an arc over them. */
|
|
30
|
+
shape?: ArrowShape;
|
|
31
|
+
/**
|
|
32
|
+
* How fast the arrows travel round the layer the way they point, in
|
|
33
|
+
* quarter turns per second (default 0.35); 0 keeps them still.
|
|
34
|
+
*/
|
|
35
|
+
speed?: number;
|
|
36
|
+
}
|
|
37
|
+
/** Default travel speed of the arrows (quarter turns per second). */
|
|
38
|
+
export declare const ARROW_SPEED = 0.35;
|
|
39
|
+
export type ArrowShape = "box" | "circle";
|
|
40
|
+
/** How far arrows reach from the centre (for fitting the camera). */
|
|
41
|
+
export declare const ARROW_REACH: number;
|
|
42
|
+
/** In-plane basis (e1, e2) with e1 × e2 = +axis. */
|
|
43
|
+
export declare const PLANE: Record<Axis, [Vector3, Vector3, Vector3]>;
|
|
44
|
+
/** Point of the path at `t` quarters, in plane coordinates (u along e1, v along e2). */
|
|
45
|
+
export declare function pathPoint(t: number, shape?: ArrowShape): [number, number];
|
|
46
|
+
/**
|
|
47
|
+
* Where an arrow goes for a viewer at `eye` (cube coordinates): over the
|
|
48
|
+
* best-seen side, leaning towards its better-seen neighbour.
|
|
49
|
+
*/
|
|
50
|
+
export declare function arrowCentre(arrow: TurnArrow, eye: Vector3): number;
|
|
51
|
+
/** Tail → tip in quarters along the path (the same length for every turn). */
|
|
52
|
+
export declare function arrowSpan(quarters: number, centre: number): {
|
|
53
|
+
from: number;
|
|
54
|
+
to: number;
|
|
55
|
+
};
|
|
56
|
+
/** A strip along the path: cross-sections [t, half width] from one end to the other. */
|
|
57
|
+
export type Strip = [number, number][];
|
|
58
|
+
/** One arrow as strips: the ribbon, then one per head (as many heads as quarter turns, up to 3). */
|
|
59
|
+
export declare function arrowStrips(quarters: number, centre: number, scale?: number, shape?: ArrowShape): Strip[];
|
|
60
|
+
/**
|
|
61
|
+
* Meshes for `arrows`: for each turning layer two arrows on opposite sides,
|
|
62
|
+
* the first at `centres[i]` (quarters along the path). Move them with
|
|
63
|
+
* `moveArrows`; dispose with `disposeArrows`.
|
|
64
|
+
*/
|
|
65
|
+
export declare function buildArrows(arrows: readonly TurnArrow[], centres: readonly number[], style?: ArrowStyle): Group;
|
|
66
|
+
/** Slide every arrow `travel` quarters along its layer, the way it points (a travelling arrow shows the direction). */
|
|
67
|
+
export declare function moveArrows(group: Group, travel: number): void;
|
|
68
|
+
export declare function disposeArrows(group: Group): void;
|
package/render/arrows.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn arrows in 3D: a ribbon with an arrowhead hovering just above the
|
|
3
|
+
* layer that is about to turn — centred on its row, flat side to the cube,
|
|
4
|
+
* rounding the cube's edges — the way the layer moves.
|
|
5
|
+
*
|
|
6
|
+
* One length for every arrow; two per layer on opposite sides, travelling
|
|
7
|
+
* round the layer the way they point (so one is always in view, and the
|
|
8
|
+
* motion shows the direction); as many heads as quarter turns (1–3).
|
|
9
|
+
* Shapes: "box" follows the faces at an even height, "circle" is an arc of
|
|
10
|
+
* a circle over them (clear of the corners).
|
|
11
|
+
*
|
|
12
|
+
* The direction is the one written (R2' the other way than R2).
|
|
13
|
+
*
|
|
14
|
+
* The path around a layer is the cube's square cross-section pushed out by
|
|
15
|
+
* HOVER (straight over the faces, round over the edges), measured in
|
|
16
|
+
* quarters t: t = k at the middle of side k (side 0 faces +e1, side 1 +e2…),
|
|
17
|
+
* t = k + 0.5 at the edge between sides k and k+1; increasing t is
|
|
18
|
+
* counter-clockwise about +axis — the same sense as TurnArrow.quarters.
|
|
19
|
+
*/
|
|
20
|
+
import { BufferGeometry, Color, DoubleSide, Float32BufferAttribute, Group, Matrix4, Mesh, MeshBasicMaterial, Vector3 } from "three";
|
|
21
|
+
/** Default travel speed of the arrows (quarter turns per second). */
|
|
22
|
+
export const ARROW_SPEED = 0.35;
|
|
23
|
+
const HALF = 1.5; // half the cube
|
|
24
|
+
/** Height of the ribbon above the faces. */
|
|
25
|
+
const HOVER = 0.3;
|
|
26
|
+
const CORNER = (Math.PI / 2) * HOVER;
|
|
27
|
+
const QUARTER = 2 * HALF + CORNER; // path length of one quarter
|
|
28
|
+
const WIDTH = 0.34;
|
|
29
|
+
const HEAD_WIDTH = 0.7;
|
|
30
|
+
const HEAD_LENGTH = 0.5;
|
|
31
|
+
/** Each further head this many head lengths behind the one before. */
|
|
32
|
+
const HEAD_GAP = 0.9;
|
|
33
|
+
/** Length of every arrow, in quarters of the way round. */
|
|
34
|
+
const SPAN = 1.3;
|
|
35
|
+
/** Arrows sit this far (quarters) from the best-seen face's middle towards the next face seen best. */
|
|
36
|
+
const LEAN = 0.25;
|
|
37
|
+
/** The circle clears the corners (1.5·√2 ≈ 2.12) by a little. */
|
|
38
|
+
const CIRCLE = HALF * Math.SQRT2 + 0.2;
|
|
39
|
+
const CIRCLE_QUARTER = (Math.PI / 2) * CIRCLE;
|
|
40
|
+
/** How far arrows reach from the centre (for fitting the camera). */
|
|
41
|
+
export const ARROW_REACH = Math.hypot(HALF * Math.SQRT2 + 0.3, 1 + HEAD_WIDTH / 2);
|
|
42
|
+
/** In-plane basis (e1, e2) with e1 × e2 = +axis. */
|
|
43
|
+
export const PLANE = {
|
|
44
|
+
0: [new Vector3(0, 1, 0), new Vector3(0, 0, 1), new Vector3(1, 0, 0)],
|
|
45
|
+
1: [new Vector3(0, 0, 1), new Vector3(1, 0, 0), new Vector3(0, 1, 0)],
|
|
46
|
+
2: [new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, 1)],
|
|
47
|
+
};
|
|
48
|
+
/** Point of the path at `t` quarters, in plane coordinates (u along e1, v along e2). */
|
|
49
|
+
export function pathPoint(t, shape = "box") {
|
|
50
|
+
if (shape === "circle")
|
|
51
|
+
return [CIRCLE * Math.cos((t * Math.PI) / 2), CIRCLE * Math.sin((t * Math.PI) / 2)];
|
|
52
|
+
const k = Math.floor(t);
|
|
53
|
+
const s = (t - k) * QUARTER;
|
|
54
|
+
let u, v;
|
|
55
|
+
if (s < HALF)
|
|
56
|
+
[u, v] = [HALF + HOVER, s];
|
|
57
|
+
else if (s < HALF + CORNER) {
|
|
58
|
+
const a = (s - HALF) / HOVER;
|
|
59
|
+
[u, v] = [HALF + HOVER * Math.cos(a), HALF + HOVER * Math.sin(a)];
|
|
60
|
+
}
|
|
61
|
+
else
|
|
62
|
+
[u, v] = [HALF - (s - HALF - CORNER), HALF + HOVER];
|
|
63
|
+
for (let i = 0; i < ((k % 4) + 4) % 4; i++)
|
|
64
|
+
[u, v] = [-v, u]; // k quarter turns counter-clockwise
|
|
65
|
+
return [u, v];
|
|
66
|
+
}
|
|
67
|
+
/** Outward normal of side k of the layer (cube coordinates). */
|
|
68
|
+
function sideNormal(axis, k) {
|
|
69
|
+
const [e1, e2] = PLANE[axis];
|
|
70
|
+
const q = ((k % 4) + 4) % 4;
|
|
71
|
+
return [e1, e2, e1.clone().negate(), e2.clone().negate()][q].clone();
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Where an arrow goes for a viewer at `eye` (cube coordinates): over the
|
|
75
|
+
* best-seen side, leaning towards its better-seen neighbour.
|
|
76
|
+
*/
|
|
77
|
+
export function arrowCentre(arrow, eye) {
|
|
78
|
+
const seen = [0, 1, 2, 3].map((k) => sideNormal(arrow.axis, k).dot(eye));
|
|
79
|
+
const best = seen.indexOf(Math.max(...seen));
|
|
80
|
+
return best + (seen[(best + 1) % 4] >= seen[(best + 3) % 4] ? LEAN : -LEAN);
|
|
81
|
+
}
|
|
82
|
+
/** Tail → tip in quarters along the path (the same length for every turn). */
|
|
83
|
+
export function arrowSpan(quarters, centre) {
|
|
84
|
+
const half = SPAN / 2;
|
|
85
|
+
return quarters > 0 ? { from: centre - half, to: centre + half } : { from: centre + half, to: centre - half };
|
|
86
|
+
}
|
|
87
|
+
/** One arrow as strips: the ribbon, then one per head (as many heads as quarter turns, up to 3). */
|
|
88
|
+
export function arrowStrips(quarters, centre, scale = 1, shape = "box") {
|
|
89
|
+
const { from, to } = arrowSpan(quarters, centre);
|
|
90
|
+
const dir = Math.sign(to - from);
|
|
91
|
+
const heads = Math.min(3, Math.max(1, Math.abs(quarters)));
|
|
92
|
+
const hl = (HEAD_LENGTH * scale) / (shape === "circle" ? CIRCLE_QUARTER : QUARTER);
|
|
93
|
+
const strip = (t0, t1, w0, w1, n) => Array.from({ length: n + 1 }, (_, i) => [t0 + ((t1 - t0) * i) / n, w0 + ((w1 - w0) * i) / n]);
|
|
94
|
+
const end = to - dir * hl * (1 + HEAD_GAP * (heads - 1));
|
|
95
|
+
const strips = [strip(from, end, (WIDTH * scale) / 2, (WIDTH * scale) / 2, 64)];
|
|
96
|
+
for (let h = 0; h < heads; h++) {
|
|
97
|
+
const tip = to - dir * hl * HEAD_GAP * h;
|
|
98
|
+
strips.push(strip(tip - dir * hl, tip, (HEAD_WIDTH * scale) / 2, 0, 6));
|
|
99
|
+
}
|
|
100
|
+
return strips;
|
|
101
|
+
}
|
|
102
|
+
/** Vertex positions of strips around the layer (layer frame: axis = z, the row's middle at z = 0); width along the axis. */
|
|
103
|
+
function positionsOf(strips, shape) {
|
|
104
|
+
const pos = [];
|
|
105
|
+
for (const strip of strips) {
|
|
106
|
+
for (const [t, half] of strip) {
|
|
107
|
+
const [u, v] = pathPoint(t, shape);
|
|
108
|
+
pos.push(u, v, half, u, v, -half);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return pos;
|
|
112
|
+
}
|
|
113
|
+
function wrap(strips, shape) {
|
|
114
|
+
const index = [];
|
|
115
|
+
let first = 0;
|
|
116
|
+
for (const strip of strips) {
|
|
117
|
+
for (let i = 0; i < strip.length - 1; i++) {
|
|
118
|
+
const k = first + 2 * i;
|
|
119
|
+
index.push(k, k + 1, k + 2, k + 1, k + 3, k + 2);
|
|
120
|
+
}
|
|
121
|
+
first += 2 * strip.length;
|
|
122
|
+
}
|
|
123
|
+
const g = new BufferGeometry();
|
|
124
|
+
g.setAttribute("position", new Float32BufferAttribute(positionsOf(strips, shape), 3));
|
|
125
|
+
g.setIndex(index);
|
|
126
|
+
g.computeBoundingSphere();
|
|
127
|
+
return g;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Meshes for `arrows`: for each turning layer two arrows on opposite sides,
|
|
131
|
+
* the first at `centres[i]` (quarters along the path). Move them with
|
|
132
|
+
* `moveArrows`; dispose with `disposeArrows`.
|
|
133
|
+
*/
|
|
134
|
+
export function buildArrows(arrows, centres, style = {}) {
|
|
135
|
+
const group = new Group();
|
|
136
|
+
const material = new MeshBasicMaterial({
|
|
137
|
+
color: new Color(style.color ?? "#2f8bff"),
|
|
138
|
+
transparent: true,
|
|
139
|
+
opacity: style.opacity ?? 0.95,
|
|
140
|
+
side: DoubleSide,
|
|
141
|
+
depthWrite: false,
|
|
142
|
+
toneMapped: false, // exactly the colour asked for
|
|
143
|
+
});
|
|
144
|
+
const shape = style.shape ?? "box";
|
|
145
|
+
const scale = style.scale ?? 1;
|
|
146
|
+
arrows.forEach((arrow, i) => {
|
|
147
|
+
const [e1, e2, n] = PLANE[arrow.axis];
|
|
148
|
+
for (const centre of [centres[i], centres[i] + 2]) {
|
|
149
|
+
for (const layer of arrow.layers) {
|
|
150
|
+
// Own geometry per mesh: each is moved along the path separately.
|
|
151
|
+
const mesh = new Mesh(wrap(arrowStrips(arrow.quarters, centre, scale, shape), shape), material);
|
|
152
|
+
mesh.matrixAutoUpdate = false;
|
|
153
|
+
mesh.matrix.copy(new Matrix4().makeBasis(e1, e2, n).setPosition(n.clone().multiplyScalar(layer)));
|
|
154
|
+
mesh.renderOrder = 3; // after the cube and its decals (2), so the see-through ribbon blends over both
|
|
155
|
+
mesh.userData.arrow = { quarters: arrow.quarters, centre, scale, shape };
|
|
156
|
+
mesh.frustumCulled = false; // it moves; its bounding sphere would go stale
|
|
157
|
+
group.add(mesh);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return group;
|
|
162
|
+
}
|
|
163
|
+
/** Slide every arrow `travel` quarters along its layer, the way it points (a travelling arrow shows the direction). */
|
|
164
|
+
export function moveArrows(group, travel) {
|
|
165
|
+
for (const o of group.children) {
|
|
166
|
+
const m = o;
|
|
167
|
+
const d = m.userData.arrow;
|
|
168
|
+
if (!d)
|
|
169
|
+
continue;
|
|
170
|
+
const strips = arrowStrips(d.quarters, d.centre + Math.sign(d.quarters) * (travel % 4), d.scale, d.shape);
|
|
171
|
+
const attr = m.geometry.getAttribute("position");
|
|
172
|
+
attr.array.set(positionsOf(strips, d.shape));
|
|
173
|
+
attr.needsUpdate = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
export function disposeArrows(group) {
|
|
177
|
+
const done = new Set();
|
|
178
|
+
group.traverse((o) => {
|
|
179
|
+
const m = o;
|
|
180
|
+
if (!m.isMesh)
|
|
181
|
+
return;
|
|
182
|
+
for (const r of [m.geometry, m.material]) {
|
|
183
|
+
if (done.has(r))
|
|
184
|
+
continue;
|
|
185
|
+
done.add(r);
|
|
186
|
+
r.dispose();
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
group.removeFromParent();
|
|
190
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachments in 3D — decals and features (see @cubecore/skin
|
|
3
|
+
* attachments.ts) as three.js objects that ride on their STICKER.
|
|
4
|
+
*
|
|
5
|
+
* Tiles are built per position and only recoloured as the state changes, so
|
|
6
|
+
* each frame an attachment is re-parented onto the tile mesh where its
|
|
7
|
+
* sticker currently is, and turned by that sticker's in-plane turn (core
|
|
8
|
+
* `stickerTurn`: centre spins, piece orientation). Being a child of the tile
|
|
9
|
+
* it moves with layer animations and hides with the tile.
|
|
10
|
+
*/
|
|
11
|
+
import * as THREE from "three";
|
|
12
|
+
import { type CenterSpins, type Mask, type State } from "../../core/index.js";
|
|
13
|
+
import { type PieceKind, type Skin } from "../../skin/index.js";
|
|
14
|
+
export interface Attachment {
|
|
15
|
+
/** Home facelet index of the sticker it belongs to. */
|
|
16
|
+
sticker: number;
|
|
17
|
+
/** Pivot in tile-local coordinates (z = 0 on the tile top); rotated by the sticker's turn. */
|
|
18
|
+
object: THREE.Group;
|
|
19
|
+
/** Hidden while the sticker isn't shown in full colour. */
|
|
20
|
+
onlyRegular: boolean;
|
|
21
|
+
/** Height of its tile's top (the tile thickness of its piece kind). */
|
|
22
|
+
lift: number;
|
|
23
|
+
}
|
|
24
|
+
export interface AttachmentSet {
|
|
25
|
+
items: Attachment[];
|
|
26
|
+
dispose(): void;
|
|
27
|
+
}
|
|
28
|
+
/** Build every decal and feature of `skin`, one object per selected sticker. `onLoad` fires as images arrive. */
|
|
29
|
+
export declare function buildAttachments(skin: Skin, kit: {
|
|
30
|
+
sideOf(kind: PieceKind): number;
|
|
31
|
+
thicknessOf(kind: PieceKind): number;
|
|
32
|
+
}, onLoad: () => void): AttachmentSet;
|
|
33
|
+
/** Put each attachment on the tile where its sticker is now, turned with the sticker. */
|
|
34
|
+
/** `surface`: one height for every tile (glTF models), or null for each tile's own thickness. */
|
|
35
|
+
export declare function placeAttachments(set: AttachmentSet, state: State, spins: CenterSpins, tiles: readonly THREE.Object3D[], surface: number | null, mask: Mask | null): void;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachments in 3D — decals and features (see @cubecore/skin
|
|
3
|
+
* attachments.ts) as three.js objects that ride on their STICKER.
|
|
4
|
+
*
|
|
5
|
+
* Tiles are built per position and only recoloured as the state changes, so
|
|
6
|
+
* each frame an attachment is re-parented onto the tile mesh where its
|
|
7
|
+
* sticker currently is, and turned by that sticker's in-plane turn (core
|
|
8
|
+
* `stickerTurn`: centre spins, piece orientation). Being a child of the tile
|
|
9
|
+
* it moves with layer animations and hides with the tile.
|
|
10
|
+
*/
|
|
11
|
+
import * as THREE from "three";
|
|
12
|
+
import { maskStateAt, stickerTurn } from "../../core/index.js";
|
|
13
|
+
import { featureDef, imageUrl, kindOfSticker, selectedStickers } from "../../skin/index.js";
|
|
14
|
+
/** Build every decal and feature of `skin`, one object per selected sticker. `onLoad` fires as images arrive. */
|
|
15
|
+
export function buildAttachments(skin, kit, onLoad) {
|
|
16
|
+
const items = [];
|
|
17
|
+
const textures = new Map();
|
|
18
|
+
const disposables = [];
|
|
19
|
+
const pivot = (sticker, onlyRegular) => {
|
|
20
|
+
const object = new THREE.Group();
|
|
21
|
+
items.push({ sticker, object, onlyRegular, lift: kit.thicknessOf(kindOfSticker(sticker)) });
|
|
22
|
+
return object;
|
|
23
|
+
};
|
|
24
|
+
// A sticker is always on the same kind of piece, so its tile size is known up front.
|
|
25
|
+
const planes = new Map();
|
|
26
|
+
for (const decal of skin.decals ?? []) {
|
|
27
|
+
const url = imageUrl(decal.image);
|
|
28
|
+
let texture = textures.get(url);
|
|
29
|
+
if (!texture) {
|
|
30
|
+
texture = new THREE.TextureLoader().load(url, onLoad);
|
|
31
|
+
texture.colorSpace = THREE.SRGBColorSpace;
|
|
32
|
+
texture.anisotropy = 4;
|
|
33
|
+
textures.set(url, texture);
|
|
34
|
+
}
|
|
35
|
+
const multiply = decal.blend === "multiply";
|
|
36
|
+
const material = new THREE.MeshBasicMaterial({
|
|
37
|
+
map: texture,
|
|
38
|
+
transparent: true,
|
|
39
|
+
depthWrite: false,
|
|
40
|
+
blending: multiply ? THREE.MultiplyBlending : THREE.NormalBlending,
|
|
41
|
+
premultipliedAlpha: multiply,
|
|
42
|
+
toneMapped: false,
|
|
43
|
+
});
|
|
44
|
+
disposables.push(material);
|
|
45
|
+
const [ox, oy] = decal.offset ?? [0, 0];
|
|
46
|
+
for (const sticker of selectedStickers(decal.select)) {
|
|
47
|
+
const side = kit.sideOf(kindOfSticker(sticker));
|
|
48
|
+
const key = `${side * decal.size}`;
|
|
49
|
+
let plane = planes.get(key);
|
|
50
|
+
if (!plane) {
|
|
51
|
+
planes.set(key, (plane = new THREE.PlaneGeometry(side * decal.size, side * decal.size)));
|
|
52
|
+
disposables.push(plane);
|
|
53
|
+
}
|
|
54
|
+
const mesh = new THREE.Mesh(plane, material);
|
|
55
|
+
mesh.position.set((ox * side) / 2, (oy * side) / 2, 0.0015);
|
|
56
|
+
mesh.rotation.z = ((decal.rotate ?? 0) * Math.PI) / 2;
|
|
57
|
+
mesh.renderOrder = 2;
|
|
58
|
+
pivot(sticker, decal.onlyRegular ?? true).add(mesh);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const use of skin.features ?? []) {
|
|
62
|
+
const def = featureDef(use.type);
|
|
63
|
+
if (!def?.build3d)
|
|
64
|
+
continue; // unknown type or 2D-only: nothing to draw here
|
|
65
|
+
for (const sticker of selectedStickers(use.select)) {
|
|
66
|
+
const kind = kindOfSticker(sticker);
|
|
67
|
+
pivot(sticker, false).add(def.build3d({ three: THREE, side: kit.sideOf(kind), thickness: kit.thicknessOf(kind), params: use.params ?? {} }));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
items,
|
|
72
|
+
dispose() {
|
|
73
|
+
for (const t of textures.values())
|
|
74
|
+
t.dispose();
|
|
75
|
+
for (const d of disposables)
|
|
76
|
+
d.dispose();
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Put each attachment on the tile where its sticker is now, turned with the sticker. */
|
|
81
|
+
/** `surface`: one height for every tile (glTF models), or null for each tile's own thickness. */
|
|
82
|
+
export function placeAttachments(set, state, spins, tiles, surface, mask) {
|
|
83
|
+
for (const a of set.items) {
|
|
84
|
+
const pos = state.indexOf(a.sticker);
|
|
85
|
+
const tile = tiles[pos];
|
|
86
|
+
if (!tile)
|
|
87
|
+
continue;
|
|
88
|
+
if (a.object.parent !== tile)
|
|
89
|
+
tile.add(a.object);
|
|
90
|
+
a.object.position.set(0, 0, surface ?? a.lift);
|
|
91
|
+
a.object.rotation.set(0, 0, (stickerTurn(state, a.sticker, spins) * Math.PI) / 2);
|
|
92
|
+
a.object.visible = !a.onlyRegular || !mask || maskStateAt(mask, state, pos) === "regular";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading piece models (glTF / GLB) once per URL. The renderer draws the
|
|
3
|
+
* built-in pieces until a model is ready, then rebuilds with it.
|
|
4
|
+
*/
|
|
5
|
+
import type { Object3D } from "three";
|
|
6
|
+
/** The model at `url` if it has loaded (null while loading or after a failure). */
|
|
7
|
+
export declare const readyModel: (url: string | undefined) => Object3D | null;
|
|
8
|
+
/** Load (once) and resolve with the model's scene, or null on failure (logged). */
|
|
9
|
+
export declare function loadModel(url: string): Promise<Object3D | null>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loading piece models (glTF / GLB) once per URL. The renderer draws the
|
|
3
|
+
* built-in pieces until a model is ready, then rebuilds with it.
|
|
4
|
+
*/
|
|
5
|
+
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
6
|
+
const pending = new Map();
|
|
7
|
+
const ready = new Map();
|
|
8
|
+
/** The model at `url` if it has loaded (null while loading or after a failure). */
|
|
9
|
+
export const readyModel = (url) => (url ? (ready.get(url) ?? null) : null);
|
|
10
|
+
/** Load (once) and resolve with the model's scene, or null on failure (logged). */
|
|
11
|
+
export function loadModel(url) {
|
|
12
|
+
let p = pending.get(url);
|
|
13
|
+
if (!p) {
|
|
14
|
+
p = new GLTFLoader()
|
|
15
|
+
.loadAsync(url)
|
|
16
|
+
.then((gltf) => {
|
|
17
|
+
ready.set(url, gltf.scene);
|
|
18
|
+
return gltf.scene;
|
|
19
|
+
})
|
|
20
|
+
.catch((err) => {
|
|
21
|
+
console.warn(`cubecore: could not load piece model ${url}`, err);
|
|
22
|
+
return null;
|
|
23
|
+
});
|
|
24
|
+
pending.set(url, p);
|
|
25
|
+
}
|
|
26
|
+
return p;
|
|
27
|
+
}
|