@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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tile and piece geometry for a skin — everything the renderer needs to
|
|
3
|
+
* build one cubie, as cached three.js geometries. Pure construction, no
|
|
4
|
+
* scene state: swap this module (or wrap it) to give a skin different solids.
|
|
5
|
+
*
|
|
6
|
+
* Per facelet POSITION, a tile's shape only depends on its piece kind and
|
|
7
|
+
* where it sits (corner / edge / centre, which way the centre is), so tiles
|
|
8
|
+
* are built per position and cached per distinct shape. Anything that must
|
|
9
|
+
* follow a particular STICKER (logos, holes, charging ports) is an
|
|
10
|
+
* attachment — see attachments.ts.
|
|
11
|
+
*/
|
|
12
|
+
import { BufferGeometry } from "three";
|
|
13
|
+
import { type PieceKind, type Skin } from "../../skin/index.js";
|
|
14
|
+
import { type TileSolid } from "../tile.js";
|
|
15
|
+
export interface TileKit {
|
|
16
|
+
/** Tile side (cubie units) — the usual one; `sideOf` per piece kind. */
|
|
17
|
+
side: number;
|
|
18
|
+
/** Height of a tile's top above the cubie face — the usual one; `thicknessOf` per piece kind. */
|
|
19
|
+
thickness: number;
|
|
20
|
+
sideOf(kind: PieceKind): number;
|
|
21
|
+
thicknessOf(kind: PieceKind): number;
|
|
22
|
+
/** Distance from a cubie's centre to the plane its tiles sit on. */
|
|
23
|
+
faceOffset: number;
|
|
24
|
+
/** The cubie body (a rounded box, or the small core when pieces are shaped). */
|
|
25
|
+
body: BufferGeometry;
|
|
26
|
+
/** A centre cubie's body: smaller when the centre cap is domed, so it stays under the cap's lowered corners. */
|
|
27
|
+
centerBody: BufferGeometry;
|
|
28
|
+
/**
|
|
29
|
+
* Shaped pieces: the mechanism in the middle of the cube (a dark ball round
|
|
30
|
+
* the one cell with no cubie), so gaps never show through to the far side.
|
|
31
|
+
* Null otherwise.
|
|
32
|
+
*/
|
|
33
|
+
mechanism: BufferGeometry | null;
|
|
34
|
+
/** The tile at facelet position `fi`, in its face's local frame (x = a, y = b, z = outward). */
|
|
35
|
+
tile(fi: number): BufferGeometry;
|
|
36
|
+
/** The sticker lying on the tile at `fi` (`stickers.overlay`), in the tile's frame, or null. */
|
|
37
|
+
overlay(fi: number): BufferGeometry | null;
|
|
38
|
+
/** With `overlay.pedestal`: the flat print on top of the (black) sticker pedestal at `fi`, or null. */
|
|
39
|
+
overlayCap(fi: number): BufferGeometry | null;
|
|
40
|
+
/** The piece's plastic under that tile (`skin.pieces`), or null. */
|
|
41
|
+
skirt(fi: number): BufferGeometry | null;
|
|
42
|
+
/** The flat floating "back" sticker for position `fi`. */
|
|
43
|
+
hint(fi: number): BufferGeometry;
|
|
44
|
+
dispose(): void;
|
|
45
|
+
}
|
|
46
|
+
export declare function tileKit(skin: Skin): TileKit;
|
|
47
|
+
/** three.js geometry of a swept solid (see tile.ts): sides plus a triangulated flat top, if any. */
|
|
48
|
+
export declare function solidToGeometry(solid: TileSolid): BufferGeometry;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tile and piece geometry for a skin — everything the renderer needs to
|
|
3
|
+
* build one cubie, as cached three.js geometries. Pure construction, no
|
|
4
|
+
* scene state: swap this module (or wrap it) to give a skin different solids.
|
|
5
|
+
*
|
|
6
|
+
* Per facelet POSITION, a tile's shape only depends on its piece kind and
|
|
7
|
+
* where it sits (corner / edge / centre, which way the centre is), so tiles
|
|
8
|
+
* are built per position and cached per distinct shape. Anything that must
|
|
9
|
+
* follow a particular STICKER (logos, holes, charging ports) is an
|
|
10
|
+
* attachment — see attachments.ts.
|
|
11
|
+
*/
|
|
12
|
+
import { BufferGeometry, Float32BufferAttribute, Shape, ShapeGeometry, ShapeUtils, SphereGeometry, Vector2 } from "three";
|
|
13
|
+
import { RoundedBoxGeometry } from "three/examples/jsm/geometries/RoundedBoxGeometry.js";
|
|
14
|
+
import { SVGLoader } from "three/examples/jsm/loaders/SVGLoader.js";
|
|
15
|
+
import { overlayOutline, reachEdge, roundedOutline, stickerLayout, stickerlessOutline, tileSize, tileThickness } from "../../skin/index.js";
|
|
16
|
+
import { applyDome, cornerRound, densify, planeCut, skirtSolid, tileSolid } from "../tile.js";
|
|
17
|
+
/** Default radius of the mechanism ball (cubie units). */
|
|
18
|
+
const MECHANISM = 1.15;
|
|
19
|
+
export function tileKit(skin) {
|
|
20
|
+
const size = skin.cubieSize;
|
|
21
|
+
const inset = skin.bodyInset ?? 0;
|
|
22
|
+
const side = skin.stickers.size * size;
|
|
23
|
+
const thickness = skin.stickers.thickness ?? 0;
|
|
24
|
+
const sideOf = (kind) => tileSize(skin, kind) * size;
|
|
25
|
+
const thicknessOf = (kind) => tileThickness(skin, kind);
|
|
26
|
+
const faceOffset = size / 2 + 0.002;
|
|
27
|
+
const edge = faceOffset;
|
|
28
|
+
const shape = skin.stickers.shape ?? {
|
|
29
|
+
corner: { inner: skin.stickers.radius, outer: skin.stickers.radius },
|
|
30
|
+
edge: { inner: skin.stickers.radius, outer: skin.stickers.radius },
|
|
31
|
+
center: skin.stickers.radius,
|
|
32
|
+
};
|
|
33
|
+
// With shaped pieces the box is only the hidden core; the visible plastic is the skirts under the tiles.
|
|
34
|
+
const bodySize = skin.pieces ? skin.pieces.core * size : size - 2 * inset;
|
|
35
|
+
const box = (s) => new RoundedBoxGeometry(s, s, s, 3, Math.min(skin.cubieRadius * size, s / 2));
|
|
36
|
+
const body = box(bodySize);
|
|
37
|
+
const dome = skin.stickers.kinds?.center?.dome;
|
|
38
|
+
// The core's top must stay under the dome's lowest point (its corners, `drop` below the face).
|
|
39
|
+
const centerBody = skin.pieces && dome ? box(Math.min(bodySize, size - 2 * (dome.drop + inset + 0.01))) : body;
|
|
40
|
+
// A ball: the same from every side, so layers turn round it; only its dark surface shows deep in the gaps.
|
|
41
|
+
const mechanism = skin.pieces ? new SphereGeometry(skin.pieces.mechanism ?? MECHANISM, 48, 32) : null;
|
|
42
|
+
// Corner-cutting relief: the tile corner(s) next to the face centre (corner pieces; edges if asked).
|
|
43
|
+
const reliefAt = (layout, s) => {
|
|
44
|
+
const relief = skin.pieces?.relief;
|
|
45
|
+
if (!relief)
|
|
46
|
+
return [];
|
|
47
|
+
const h = s / 2, su = -Math.sign(layout.u), sv = -Math.sign(layout.v);
|
|
48
|
+
if (layout.kind === "corner")
|
|
49
|
+
return [[su * h, sv * h]];
|
|
50
|
+
if (layout.kind === "edge" && relief.edges)
|
|
51
|
+
return layout.u ? [[su * h, h], [su * h, -h]] : [[h, sv * h], [-h, sv * h]];
|
|
52
|
+
return [];
|
|
53
|
+
};
|
|
54
|
+
// Cube corners rounder than the edges: a ball at each corner (in a corner tile's frame).
|
|
55
|
+
const roundCorner = (solid, layout) => {
|
|
56
|
+
const R = skin.stickers.cornerRound;
|
|
57
|
+
if (!R || layout.kind !== "corner")
|
|
58
|
+
return solid;
|
|
59
|
+
const top = thicknessOf("corner");
|
|
60
|
+
return cornerRound(solid, [layout.u * (edge - R), layout.v * (edge - R), top - R], [layout.u, layout.v, 1], R);
|
|
61
|
+
};
|
|
62
|
+
const outlineFor = (fi) => {
|
|
63
|
+
const layout = stickerLayout(fi, shape);
|
|
64
|
+
const path = skin.stickers.paths?.[layout.kind];
|
|
65
|
+
const fill = skin.stickers.fillOuter === true;
|
|
66
|
+
const s = sideOf(layout.kind), t = thicknessOf(layout.kind);
|
|
67
|
+
const outline = path
|
|
68
|
+
? fill
|
|
69
|
+
? reachEdge(pathOutline(path, s, layout.pathQuarters), layout, s / 2, edge)
|
|
70
|
+
: pathOutline(path, s, layout.pathQuarters)
|
|
71
|
+
: fill
|
|
72
|
+
? stickerlessOutline(layout, s, edge)
|
|
73
|
+
: roundedOutline(s, layout.radii);
|
|
74
|
+
const mitre = fill ? { u: layout.u, v: layout.v, edge, ramp: Math.max(t * 4, 0.03) } : null;
|
|
75
|
+
const key = (path ? `p|${layout.kind}|${layout.pathQuarters}` : `r|${layout.radii.join(",")}`) + `${fill ? `|${layout.u},${layout.v}` : ""}|${s}|${t}`;
|
|
76
|
+
return { outline, mitre, key, layout, thickness: t, side: s };
|
|
77
|
+
};
|
|
78
|
+
const cache = new Map();
|
|
79
|
+
const cached = (key, make) => {
|
|
80
|
+
let g = cache.get(key);
|
|
81
|
+
if (!g)
|
|
82
|
+
cache.set(key, (g = make()));
|
|
83
|
+
return g;
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
side,
|
|
87
|
+
thickness,
|
|
88
|
+
sideOf,
|
|
89
|
+
// Where things on a sticker sit: on the tile, or on the sticker lying on it.
|
|
90
|
+
thicknessOf: (kind) => thicknessOf(kind) + (skin.stickers.overlay?.thickness ?? 0),
|
|
91
|
+
faceOffset,
|
|
92
|
+
body,
|
|
93
|
+
centerBody,
|
|
94
|
+
mechanism,
|
|
95
|
+
tile(fi) {
|
|
96
|
+
const { outline, mitre, key, thickness: t, layout, side: s } = outlineFor(fi);
|
|
97
|
+
const relief = skin.pieces?.relief;
|
|
98
|
+
const at = relief?.tile !== undefined ? reliefAt(layout, s) : [];
|
|
99
|
+
const undercut = relief && at.length ? { at, r: relief.start ?? 0, z: relief.tile } : undefined;
|
|
100
|
+
const kind = skin.stickers.kinds?.[layout.kind];
|
|
101
|
+
const bevel = Math.min(kind?.bevel ?? skin.stickers.bevel ?? 0, t);
|
|
102
|
+
const dome = kind?.dome;
|
|
103
|
+
// A domed tile reaches deeper, so its lowered corners stay above its bottom.
|
|
104
|
+
const sink = inset + 0.002 + (dome ? dome.drop : 0);
|
|
105
|
+
return cached(`tile|${key}|${bevel}|${dome ? `${dome.flat},${dome.drop}` : ""}|${undercut ? "uc" : ""}`, () => t > 0
|
|
106
|
+
? solidToGeometry(((solid) => roundCorner(dome ? applyDome(solid, dome, sink) : solid, layout))(tileSolid(dome || undercut || (skin.stickers.cornerRound && layout.kind === "corner") ? densify(outline, undercut ? 192 : 128) : outline, { thickness: t, bevel, segments: 4, sink, edgeRadius: skin.stickers.edgeRadius ?? 0, undercut }, mitre)))
|
|
107
|
+
: new ShapeGeometry(new Shape(outline.map(([x, y]) => new Vector2(x, y)))));
|
|
108
|
+
},
|
|
109
|
+
overlay(fi) {
|
|
110
|
+
const ov = skin.stickers.overlay;
|
|
111
|
+
if (!ov)
|
|
112
|
+
return null;
|
|
113
|
+
const { layout } = outlineFor(fi);
|
|
114
|
+
const base = thicknessOf(layout.kind);
|
|
115
|
+
return cached(`overlay|${fi}`, () => {
|
|
116
|
+
const g = solidToGeometry(tileSolid(overlayOutline(fi, ov), { thickness: ov.thickness, bevel: Math.min(ov.bevel ?? 0.004, ov.thickness), segments: 2, sink: 0.002 }, null));
|
|
117
|
+
g.translate(0, 0, base);
|
|
118
|
+
return g;
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
overlayCap(fi) {
|
|
122
|
+
const ov = skin.stickers.overlay;
|
|
123
|
+
if (!ov?.pedestal)
|
|
124
|
+
return null;
|
|
125
|
+
const { layout } = outlineFor(fi);
|
|
126
|
+
const z = thicknessOf(layout.kind) + ov.thickness + 0.0008;
|
|
127
|
+
return cached(`cap|${fi}`, () => {
|
|
128
|
+
const pts = overlayOutline(fi, ov, 8, Math.min(ov.bevel ?? 0.004, ov.thickness));
|
|
129
|
+
const g = new ShapeGeometry(new Shape(pts.map(([x, y]) => new Vector2(x, y))));
|
|
130
|
+
g.translate(0, 0, z);
|
|
131
|
+
return g;
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
skirt(fi) {
|
|
135
|
+
const pieces = skin.pieces;
|
|
136
|
+
if (!pieces)
|
|
137
|
+
return null;
|
|
138
|
+
const { outline, mitre, key, layout, side: s } = outlineFor(fi);
|
|
139
|
+
// Solid pieces: through the whole cubie. The walls lean in, so where two meet inside the
|
|
140
|
+
// piece the one from the nearer tile is outermost — the colours split on the diagonals.
|
|
141
|
+
const depth = pieces.fill === "solid" ? size : pieces.depth;
|
|
142
|
+
const at = reliefAt(layout, s);
|
|
143
|
+
const relief = pieces.relief && at.length ? { at, ...pieces.relief } : undefined;
|
|
144
|
+
// The inside is cut flat, square to the piece's direction from the cube's centre and touching the mechanism
|
|
145
|
+
// ball: a corner's inner point becomes a small triangle, an edge's inner edge a flat strip, a centre's back flat.
|
|
146
|
+
// (In this tile's frame the cube's centre is one step in along the face normal and −u / −v across.)
|
|
147
|
+
const ball = pieces.mechanism ?? MECHANISM;
|
|
148
|
+
const centre = [-layout.u, -layout.v, -1 - faceOffset];
|
|
149
|
+
const len = Math.hypot(layout.u, layout.v, 1);
|
|
150
|
+
const dir = [layout.u / len, layout.v / len, 1 / len];
|
|
151
|
+
return cached(`skirt|${key}|${depth}`, () => solidToGeometry(roundCorner(planeCut(skirtSolid(densify(outline, 192), { top: inset + 0.002, depth, taper: pieces.taper, wall: pieces.wall, relief, steps: 32 }, mitre), centre, dir, ball), layout)));
|
|
152
|
+
},
|
|
153
|
+
hint(fi) {
|
|
154
|
+
const { layout, side: s } = outlineFor(fi);
|
|
155
|
+
return cached(`hint|${layout.radii.join(",")}|${s}`, () => new ShapeGeometry(new Shape(roundedOutline(s * 0.92, layout.radii).map(([x, y]) => new Vector2(x, y)))));
|
|
156
|
+
},
|
|
157
|
+
dispose() {
|
|
158
|
+
body.dispose();
|
|
159
|
+
for (const g of cache.values())
|
|
160
|
+
g.dispose();
|
|
161
|
+
cache.clear();
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** three.js geometry of a swept solid (see tile.ts): sides plus a triangulated flat top, if any. */
|
|
166
|
+
export function solidToGeometry(solid) {
|
|
167
|
+
const cap = solid.topRing.length ? ShapeUtils.triangulateShape(solid.topRing.map(([x, y]) => new Vector2(x, y)), []) : [];
|
|
168
|
+
const index = [...solid.sides];
|
|
169
|
+
for (const [a, b, c] of cap)
|
|
170
|
+
index.push(solid.topStart + a, solid.topStart + b, solid.topStart + c);
|
|
171
|
+
const g = new BufferGeometry();
|
|
172
|
+
g.setAttribute("position", new Float32BufferAttribute(solid.positions, 3));
|
|
173
|
+
g.setAttribute("normal", new Float32BufferAttribute(solid.normals, 3));
|
|
174
|
+
g.setIndex(index);
|
|
175
|
+
g.computeBoundingSphere();
|
|
176
|
+
return g;
|
|
177
|
+
}
|
|
178
|
+
/** Outline of a custom SVG path (0..1 box, y down) scaled to `side`, rotated by `quarters` CCW, centred. */
|
|
179
|
+
function pathOutline(d, side, quarters) {
|
|
180
|
+
const data = new SVGLoader().parse(`<svg xmlns="http://www.w3.org/2000/svg"><path d="${d}"/></svg>`);
|
|
181
|
+
const shape = SVGLoader.createShapes(data.paths[0])[0];
|
|
182
|
+
if (!shape)
|
|
183
|
+
throw new Error("Sticker path has no closed shape");
|
|
184
|
+
const pts = shape.getPoints(8).map((p) => [(p.x - 0.5) * side, (0.5 - p.y) * side]);
|
|
185
|
+
const c = Math.cos((quarters * Math.PI) / 2), s = Math.sin((quarters * Math.PI) / 2);
|
|
186
|
+
const rotated = pts.map(([x, y]) => [Math.round((x * c - y * s) * 1e6) / 1e6, Math.round((x * s + y * c) * 1e6) / 1e6]);
|
|
187
|
+
// Keep the outline counter-clockwise (y flip reversed SVG's winding).
|
|
188
|
+
let area = 0;
|
|
189
|
+
for (let i = 0; i < rotated.length; i++) {
|
|
190
|
+
const [x1, y1] = rotated[i], [x2, y2] = rotated[(i + 1) % rotated.length];
|
|
191
|
+
area += x1 * y2 - x2 * y1;
|
|
192
|
+
}
|
|
193
|
+
return area < 0 ? rotated.reverse() : rotated;
|
|
194
|
+
}
|
package/render/gltf.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* glTF templates of a skin's pieces — the built-in geometry written out in
|
|
3
|
+
* the piece-model convention (pieceModels.ts), as a starting point for model
|
|
4
|
+
* authors (open in Blender, reshape, export as GLB) and as ready-made
|
|
5
|
+
* models. Materials: `body` and `sticker-U` / `sticker-F` / `sticker-R`.
|
|
6
|
+
*/
|
|
7
|
+
import type { Skin } from "../skin/index.js";
|
|
8
|
+
import { type ModelKind } from "./pieceModels.js";
|
|
9
|
+
/** The skin's corner, edge and centre as .gltf documents (JSON strings) in the piece-model convention. */
|
|
10
|
+
export declare function pieceTemplates(skin: Skin): Record<ModelKind, string>;
|
package/render/gltf.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* glTF templates of a skin's pieces — the built-in geometry written out in
|
|
3
|
+
* the piece-model convention (pieceModels.ts), as a starting point for model
|
|
4
|
+
* authors (open in Blender, reshape, export as GLB) and as ready-made
|
|
5
|
+
* models. Materials: `body` and `sticker-U` / `sticker-F` / `sticker-R`.
|
|
6
|
+
*/
|
|
7
|
+
import { Matrix4, Vector3 } from "three";
|
|
8
|
+
import { FACELETS, FACE_BASIS, faceletAt, FACE_NORMAL } from "../core/index.js";
|
|
9
|
+
import { tileKit } from "./build/tiles.js";
|
|
10
|
+
import { CANONICAL } from "./pieceModels.js";
|
|
11
|
+
function toBase64(bytes) {
|
|
12
|
+
let s = "";
|
|
13
|
+
for (let i = 0; i < bytes.length; i += 0x8000)
|
|
14
|
+
s += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
|
15
|
+
return btoa(s);
|
|
16
|
+
}
|
|
17
|
+
const hexToLinear = (hex) => {
|
|
18
|
+
const c = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
|
|
19
|
+
return c.map((v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));
|
|
20
|
+
};
|
|
21
|
+
/** A minimal glTF 2.0 document (embedded buffer) with one node per part. */
|
|
22
|
+
function writeGltf(parts, materials) {
|
|
23
|
+
const chunks = [];
|
|
24
|
+
let offset = 0;
|
|
25
|
+
const bufferViews = [];
|
|
26
|
+
const accessors = [];
|
|
27
|
+
const push = (data, target) => {
|
|
28
|
+
const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
29
|
+
const pad = (4 - (offset % 4)) % 4;
|
|
30
|
+
if (pad) {
|
|
31
|
+
chunks.push(new Uint8Array(pad));
|
|
32
|
+
offset += pad;
|
|
33
|
+
}
|
|
34
|
+
bufferViews.push({ buffer: 0, byteOffset: offset, byteLength: bytes.length, target });
|
|
35
|
+
chunks.push(bytes);
|
|
36
|
+
offset += bytes.length;
|
|
37
|
+
return bufferViews.length - 1;
|
|
38
|
+
};
|
|
39
|
+
const materialNames = Object.keys(materials);
|
|
40
|
+
const meshes = parts.map((p) => {
|
|
41
|
+
const pos = p.geometry.getAttribute("position").array;
|
|
42
|
+
const nor = p.geometry.getAttribute("normal").array;
|
|
43
|
+
const index = p.geometry.getIndex()?.array ?? Uint32Array.from({ length: pos.length / 3 }, (_, i) => i);
|
|
44
|
+
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
|
|
45
|
+
for (let i = 0; i < pos.length; i += 3)
|
|
46
|
+
for (let k = 0; k < 3; k++) {
|
|
47
|
+
min[k] = Math.min(min[k], pos[i + k]);
|
|
48
|
+
max[k] = Math.max(max[k], pos[i + k]);
|
|
49
|
+
}
|
|
50
|
+
const count = pos.length / 3;
|
|
51
|
+
accessors.push({ bufferView: push(Float32Array.from(pos), 34962), componentType: 5126, count, type: "VEC3", min, max });
|
|
52
|
+
const posAcc = accessors.length - 1;
|
|
53
|
+
accessors.push({ bufferView: push(Float32Array.from(nor), 34962), componentType: 5126, count, type: "VEC3" });
|
|
54
|
+
const norAcc = accessors.length - 1;
|
|
55
|
+
accessors.push({ bufferView: push(Uint32Array.from(index), 34963), componentType: 5125, count: index.length, type: "SCALAR" });
|
|
56
|
+
return { primitives: [{ attributes: { POSITION: posAcc, NORMAL: norAcc }, indices: accessors.length - 1, material: materialNames.indexOf(p.material) }] };
|
|
57
|
+
});
|
|
58
|
+
const buffer = new Uint8Array(offset);
|
|
59
|
+
let at = 0;
|
|
60
|
+
for (const c of chunks) {
|
|
61
|
+
buffer.set(c, at);
|
|
62
|
+
at += c.length;
|
|
63
|
+
}
|
|
64
|
+
return JSON.stringify({
|
|
65
|
+
asset: { version: "2.0", generator: "cubecore piece templates" },
|
|
66
|
+
scene: 0,
|
|
67
|
+
scenes: [{ nodes: parts.map((_, i) => i) }],
|
|
68
|
+
nodes: parts.map((p, i) => ({ mesh: i, matrix: p.matrix.elements.map((v) => Math.round(v * 1e6) / 1e6 + 0) })),
|
|
69
|
+
meshes,
|
|
70
|
+
materials: materialNames.map((name) => ({
|
|
71
|
+
name,
|
|
72
|
+
pbrMetallicRoughness: { baseColorFactor: [...hexToLinear(materials[name].color), 1], metallicFactor: 0, roughnessFactor: materials[name].roughness },
|
|
73
|
+
})),
|
|
74
|
+
accessors,
|
|
75
|
+
bufferViews,
|
|
76
|
+
buffers: [{ byteLength: buffer.length, uri: `data:application/octet-stream;base64,${toBase64(buffer)}` }],
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** The skin's corner, edge and centre as .gltf documents (JSON strings) in the piece-model convention. */
|
|
80
|
+
export function pieceTemplates(skin) {
|
|
81
|
+
const kit = tileKit(skin);
|
|
82
|
+
const roughness = skin.stickers.roughness ?? 0.4;
|
|
83
|
+
const colors = skin.stickers.colors;
|
|
84
|
+
const out = {};
|
|
85
|
+
for (const kind of ["corner", "edge", "center"]) {
|
|
86
|
+
const { pos, faces } = CANONICAL[kind];
|
|
87
|
+
const parts = [{ geometry: kit.body, matrix: new Matrix4(), material: "body" }];
|
|
88
|
+
for (const face of faces) {
|
|
89
|
+
const fi = faceletAt(pos, FACE_NORMAL[face]);
|
|
90
|
+
const f = FACELETS[fi];
|
|
91
|
+
const { a, b, n } = FACE_BASIS[f.face];
|
|
92
|
+
const m = new Matrix4().makeBasis(new Vector3(...a), new Vector3(...b), new Vector3(...n));
|
|
93
|
+
m.setPosition(new Vector3(...f.pos).sub(new Vector3(...pos)).addScaledVector(new Vector3(...n), kit.faceOffset));
|
|
94
|
+
parts.push({ geometry: kit.tile(fi), matrix: m, material: `sticker-${face}` });
|
|
95
|
+
const skirt = kit.skirt(fi);
|
|
96
|
+
if (skirt)
|
|
97
|
+
parts.push({ geometry: skirt, matrix: m.clone(), material: "body" });
|
|
98
|
+
}
|
|
99
|
+
const mats = { body: { color: skin.body, roughness: 0.85 } };
|
|
100
|
+
for (const face of faces)
|
|
101
|
+
mats[`sticker-${face}`] = { color: colors["URFDLB".indexOf(face)], roughness };
|
|
102
|
+
out[kind] = writeGltf(parts, mats);
|
|
103
|
+
}
|
|
104
|
+
kit.dispose();
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./layers.js";
|
|
2
|
+
export * from "../skin/index.js";
|
|
3
|
+
export * from "./renderer.js";
|
|
4
|
+
export * from "./pictures.js";
|
|
5
|
+
export * from "./arrows.js";
|
|
6
|
+
export * from "./viewports.js";
|
|
7
|
+
export * from "./tile.js";
|
|
8
|
+
export * from "./build/tiles.js";
|
|
9
|
+
export * from "./build/attachments.js";
|
|
10
|
+
export * from "./pieceModels.js";
|
|
11
|
+
export * from "./gltf.js";
|
|
12
|
+
export * from "./build/models.js";
|
package/render/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./layers.js";
|
|
2
|
+
export * from "../skin/index.js";
|
|
3
|
+
export * from "./renderer.js";
|
|
4
|
+
export * from "./pictures.js";
|
|
5
|
+
export * from "./arrows.js";
|
|
6
|
+
export * from "./viewports.js";
|
|
7
|
+
export * from "./tile.js";
|
|
8
|
+
export * from "./build/tiles.js";
|
|
9
|
+
export * from "./build/attachments.js";
|
|
10
|
+
export * from "./pieceModels.js";
|
|
11
|
+
export * from "./gltf.js";
|
|
12
|
+
export * from "./build/models.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure geometry for animation (no three.js): which cubies a move turns, about
|
|
3
|
+
* which axis, and by what angle. Shared by the renderer and its tests.
|
|
4
|
+
*
|
|
5
|
+
* Animation model: while move m animates from state S, the renderer shows S's
|
|
6
|
+
* colours with m's layer rotated by `progress × angle`; at progress 1 it
|
|
7
|
+
* switches to S·m with nothing rotated. The two pictures coincide, so there is
|
|
8
|
+
* no pop — and the scene never has to track which physical cubie is where.
|
|
9
|
+
*/
|
|
10
|
+
import { type Move, type Vec3 } from "../core/index.js";
|
|
11
|
+
export interface LayerTurn {
|
|
12
|
+
/** 0 = x, 1 = y, 2 = z. */
|
|
13
|
+
axis: 0 | 1 | 2;
|
|
14
|
+
/** Radians, counter-clockwise about +axis (right-hand rule). */
|
|
15
|
+
angle: number;
|
|
16
|
+
/** Does this move turn the cubie at `pos`? */
|
|
17
|
+
turns(pos: Vec3): boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function layerTurn(move: Move): LayerTurn;
|
|
20
|
+
/** Default animation length for a move, scaled by how far it turns (half turns take longer). */
|
|
21
|
+
export declare function defaultDuration(move: Move, quarterMs?: number): number;
|
package/render/layers.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure geometry for animation (no three.js): which cubies a move turns, about
|
|
3
|
+
* which axis, and by what angle. Shared by the renderer and its tests.
|
|
4
|
+
*
|
|
5
|
+
* Animation model: while move m animates from state S, the renderer shows S's
|
|
6
|
+
* colours with m's layer rotated by `progress × angle`; at progress 1 it
|
|
7
|
+
* switches to S·m with nothing rotated. The two pictures coincide, so there is
|
|
8
|
+
* no pop — and the scene never has to track which physical cubie is where.
|
|
9
|
+
*/
|
|
10
|
+
import { FAMILY, amountQuarters } from "../core/index.js";
|
|
11
|
+
export function layerTurn(move) {
|
|
12
|
+
const def = FAMILY[move.family];
|
|
13
|
+
// A family's clockwise turn is `def.q` CCW quarters about +axis; the amount multiplies it.
|
|
14
|
+
const quarters = def.q * (move.amount === -1 ? -1 : move.amount === 2 ? 2 : 1);
|
|
15
|
+
return {
|
|
16
|
+
axis: def.axis,
|
|
17
|
+
angle: (quarters * Math.PI) / 2,
|
|
18
|
+
turns: (pos) => def.layers.includes(pos[def.axis]),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Default animation length for a move, scaled by how far it turns (half turns take longer). */
|
|
22
|
+
export function defaultDuration(move, quarterMs = 120) {
|
|
23
|
+
return amountQuarters(move.amount) === 2 ? quarterMs * 1.5 : quarterMs;
|
|
24
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Still pictures of cubes from ONE WebGL renderer — for pages with many
|
|
3
|
+
* cubes (a list of cases). Browsers keep only ~16 WebGL contexts, so a live
|
|
4
|
+
* renderer per cube stops working past that; here a single hidden renderer
|
|
5
|
+
* draws each picture in turn (the same 3D look, perspective and camera) and
|
|
6
|
+
* hands back an image URL, cached by `key`.
|
|
7
|
+
*
|
|
8
|
+
* const pictures = sharedPictures();
|
|
9
|
+
* img.src = pictures.draw({ state, mask, skin, camera: { latitude: 20, longitude: 25 } }, key);
|
|
10
|
+
*/
|
|
11
|
+
import type { Mask, State } from "../core/index.js";
|
|
12
|
+
import { type Skin, type Theme } from "../skin/index.js";
|
|
13
|
+
import { type CameraOptions } from "./renderer.js";
|
|
14
|
+
export interface PictureOptions {
|
|
15
|
+
state: State;
|
|
16
|
+
mask?: Mask | null;
|
|
17
|
+
skin?: Skin;
|
|
18
|
+
theme?: Theme;
|
|
19
|
+
camera?: Partial<CameraOptions>;
|
|
20
|
+
/** Turn the whole cube (e.g. as held: white down) — see CubeRenderer.setOrientation. */
|
|
21
|
+
orientation?: {
|
|
22
|
+
x: number;
|
|
23
|
+
y: number;
|
|
24
|
+
z: number;
|
|
25
|
+
w: number;
|
|
26
|
+
} | null;
|
|
27
|
+
}
|
|
28
|
+
export declare class CubePictures {
|
|
29
|
+
private readonly size;
|
|
30
|
+
private readonly maxCached;
|
|
31
|
+
private host;
|
|
32
|
+
private renderer;
|
|
33
|
+
private skin;
|
|
34
|
+
private theme;
|
|
35
|
+
private readonly cache;
|
|
36
|
+
/** `size`: picture side in CSS px (drawn at the device pixel ratio, up to 2×). */
|
|
37
|
+
constructor(size?: number, maxCached?: number);
|
|
38
|
+
/** An image URL of the cube (a PNG data URL); same `key` → the cached one. */
|
|
39
|
+
draw(options: PictureOptions, key?: string): string;
|
|
40
|
+
/** Forget cached pictures (e.g. after the look changed). */
|
|
41
|
+
clear(): void;
|
|
42
|
+
dispose(): void;
|
|
43
|
+
private ensure;
|
|
44
|
+
}
|
|
45
|
+
/** The page's shared picture renderer (one WebGL context for all of them). */
|
|
46
|
+
export declare function sharedPictures(): CubePictures;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Still pictures of cubes from ONE WebGL renderer — for pages with many
|
|
3
|
+
* cubes (a list of cases). Browsers keep only ~16 WebGL contexts, so a live
|
|
4
|
+
* renderer per cube stops working past that; here a single hidden renderer
|
|
5
|
+
* draws each picture in turn (the same 3D look, perspective and camera) and
|
|
6
|
+
* hands back an image URL, cached by `key`.
|
|
7
|
+
*
|
|
8
|
+
* const pictures = sharedPictures();
|
|
9
|
+
* img.src = pictures.draw({ state, mask, skin, camera: { latitude: 20, longitude: 25 } }, key);
|
|
10
|
+
*/
|
|
11
|
+
import { SKINS } from "../skin/index.js";
|
|
12
|
+
import { CubeRenderer } from "./renderer.js";
|
|
13
|
+
export class CubePictures {
|
|
14
|
+
size;
|
|
15
|
+
maxCached;
|
|
16
|
+
host = null;
|
|
17
|
+
renderer = null;
|
|
18
|
+
skin = null;
|
|
19
|
+
theme = null;
|
|
20
|
+
cache = new Map();
|
|
21
|
+
/** `size`: picture side in CSS px (drawn at the device pixel ratio, up to 2×). */
|
|
22
|
+
constructor(size = 240, maxCached = 500) {
|
|
23
|
+
this.size = size;
|
|
24
|
+
this.maxCached = maxCached;
|
|
25
|
+
}
|
|
26
|
+
/** An image URL of the cube (a PNG data URL); same `key` → the cached one. */
|
|
27
|
+
draw(options, key) {
|
|
28
|
+
if (key !== undefined) {
|
|
29
|
+
const hit = this.cache.get(key);
|
|
30
|
+
if (hit)
|
|
31
|
+
return hit;
|
|
32
|
+
}
|
|
33
|
+
const r = this.ensure();
|
|
34
|
+
const skin = options.skin ?? SKINS.default;
|
|
35
|
+
if (skin !== this.skin)
|
|
36
|
+
r.setSkin((this.skin = skin));
|
|
37
|
+
const theme = options.theme ?? "dark";
|
|
38
|
+
if (theme !== this.theme)
|
|
39
|
+
r.setTheme((this.theme = theme));
|
|
40
|
+
r.setCamera({ latitude: 30, longitude: 35, ...options.camera });
|
|
41
|
+
r.setOrientation(options.orientation ?? null);
|
|
42
|
+
r.setMask(options.mask ?? null);
|
|
43
|
+
r.setState(options.state);
|
|
44
|
+
const url = r.snapshot();
|
|
45
|
+
if (key !== undefined) {
|
|
46
|
+
if (this.cache.size >= this.maxCached)
|
|
47
|
+
this.cache.delete(this.cache.keys().next().value);
|
|
48
|
+
this.cache.set(key, url);
|
|
49
|
+
}
|
|
50
|
+
return url;
|
|
51
|
+
}
|
|
52
|
+
/** Forget cached pictures (e.g. after the look changed). */
|
|
53
|
+
clear() {
|
|
54
|
+
this.cache.clear();
|
|
55
|
+
}
|
|
56
|
+
dispose() {
|
|
57
|
+
this.renderer?.dispose();
|
|
58
|
+
this.host?.remove();
|
|
59
|
+
this.renderer = null;
|
|
60
|
+
this.host = null;
|
|
61
|
+
this.cache.clear();
|
|
62
|
+
}
|
|
63
|
+
ensure() {
|
|
64
|
+
if (this.renderer)
|
|
65
|
+
return this.renderer;
|
|
66
|
+
const host = document.createElement("div");
|
|
67
|
+
host.setAttribute("aria-hidden", "true");
|
|
68
|
+
host.style.cssText = `position:fixed;left:-10000px;top:0;width:${this.size}px;height:${this.size}px;pointer-events:none;`;
|
|
69
|
+
document.body.append(host);
|
|
70
|
+
this.host = host;
|
|
71
|
+
this.renderer = new CubeRenderer(host, { dragToRotate: false });
|
|
72
|
+
return this.renderer;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
let shared = null;
|
|
76
|
+
/** The page's shared picture renderer (one WebGL context for all of them). */
|
|
77
|
+
export function sharedPictures() {
|
|
78
|
+
shared ??= new CubePictures();
|
|
79
|
+
return shared;
|
|
80
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Piece models — pure maths for placing one model per piece kind on all 26
|
|
3
|
+
* cubies (no three.js here; the renderer and the glTF exporter use it).
|
|
4
|
+
*
|
|
5
|
+
* Convention for model authors (glTF / GLB, 1 unit = one cubie, centred on
|
|
6
|
+
* the cubie's centre, Y up, Z towards the viewer — same axes as cubecore):
|
|
7
|
+
*
|
|
8
|
+
* corner modelled as the UFR corner: at +x +y +z, stickers on U, F and R
|
|
9
|
+
* edge modelled as the UF edge: at 0 +y +z, stickers on U and F
|
|
10
|
+
* center modelled as the U centre: at 0 +y 0, sticker on U
|
|
11
|
+
*
|
|
12
|
+
* Meshes whose material is named `sticker-U`, `sticker-F`, `sticker-R` are
|
|
13
|
+
* the coloured parts (recoloured by state and mask); everything else — the
|
|
14
|
+
* `body`, screws, logos you bake in — is drawn as authored. The renderer
|
|
15
|
+
* rotates each model onto every position of its kind and maps its sticker
|
|
16
|
+
* materials to the facelets there.
|
|
17
|
+
*/
|
|
18
|
+
import { type Cubie, type Face, type Mat3, type Vec3 } from "../core/index.js";
|
|
19
|
+
export type ModelKind = "corner" | "edge" | "center";
|
|
20
|
+
/** Where each kind's model is authored, and which of its faces carry stickers. */
|
|
21
|
+
export declare const CANONICAL: Record<ModelKind, {
|
|
22
|
+
pos: Vec3;
|
|
23
|
+
faces: readonly Face[];
|
|
24
|
+
}>;
|
|
25
|
+
export interface PiecePlacement {
|
|
26
|
+
cubie: Cubie;
|
|
27
|
+
kind: ModelKind;
|
|
28
|
+
/** Rotation taking the canonical piece onto this cubie (rows; canonical → cube coordinates). */
|
|
29
|
+
rotation: Mat3;
|
|
30
|
+
/** Model sticker face (canonical letter) → facelet index at this cubie. */
|
|
31
|
+
stickers: Partial<Record<Face, number>>;
|
|
32
|
+
}
|
|
33
|
+
/** How the model of `cubie`'s kind sits on it. */
|
|
34
|
+
export declare function placePiece(cubie: Cubie): PiecePlacement;
|
|
35
|
+
/** Placements of all 26 cubies. */
|
|
36
|
+
export declare const PLACEMENTS: readonly PiecePlacement[];
|
|
37
|
+
/** `sticker-U` → "U"; anything else → null. */
|
|
38
|
+
export declare function stickerFaceOf(materialName: string | undefined): Face | null;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Piece models — pure maths for placing one model per piece kind on all 26
|
|
3
|
+
* cubies (no three.js here; the renderer and the glTF exporter use it).
|
|
4
|
+
*
|
|
5
|
+
* Convention for model authors (glTF / GLB, 1 unit = one cubie, centred on
|
|
6
|
+
* the cubie's centre, Y up, Z towards the viewer — same axes as cubecore):
|
|
7
|
+
*
|
|
8
|
+
* corner modelled as the UFR corner: at +x +y +z, stickers on U, F and R
|
|
9
|
+
* edge modelled as the UF edge: at 0 +y +z, stickers on U and F
|
|
10
|
+
* center modelled as the U centre: at 0 +y 0, sticker on U
|
|
11
|
+
*
|
|
12
|
+
* Meshes whose material is named `sticker-U`, `sticker-F`, `sticker-R` are
|
|
13
|
+
* the coloured parts (recoloured by state and mask); everything else — the
|
|
14
|
+
* `body`, screws, logos you bake in — is drawn as authored. The renderer
|
|
15
|
+
* rotates each model onto every position of its kind and maps its sticker
|
|
16
|
+
* materials to the facelets there.
|
|
17
|
+
*/
|
|
18
|
+
import { CUBIES, FACE_NORMAL, FRAMES, apply, faceletAt } from "../core/index.js";
|
|
19
|
+
/** Where each kind's model is authored, and which of its faces carry stickers. */
|
|
20
|
+
export const CANONICAL = {
|
|
21
|
+
corner: { pos: [1, 1, 1], faces: ["U", "F", "R"] },
|
|
22
|
+
edge: { pos: [0, 1, 1], faces: ["U", "F"] },
|
|
23
|
+
center: { pos: [0, 1, 0], faces: ["U"] },
|
|
24
|
+
};
|
|
25
|
+
const same = (a, b) => a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
26
|
+
/** How the model of `cubie`'s kind sits on it. */
|
|
27
|
+
export function placePiece(cubie) {
|
|
28
|
+
const kind = cubie.kind;
|
|
29
|
+
const canon = CANONICAL[kind];
|
|
30
|
+
// The first of the 24 cube rotations that brings the canonical piece here (identity first).
|
|
31
|
+
const frame = FRAMES.find((f) => same(apply(f.matrix, canon.pos), cubie.pos));
|
|
32
|
+
const stickers = {};
|
|
33
|
+
for (const face of canon.faces)
|
|
34
|
+
stickers[face] = faceletAt(cubie.pos, apply(frame.matrix, FACE_NORMAL[face]));
|
|
35
|
+
return { cubie, kind, rotation: frame.matrix, stickers };
|
|
36
|
+
}
|
|
37
|
+
/** Placements of all 26 cubies. */
|
|
38
|
+
export const PLACEMENTS = CUBIES.map(placePiece);
|
|
39
|
+
/** `sticker-U` → "U"; anything else → null. */
|
|
40
|
+
export function stickerFaceOf(materialName) {
|
|
41
|
+
const m = /^sticker-([URFDLB])$/i.exec(materialName ?? "");
|
|
42
|
+
return m ? m[1].toUpperCase() : null;
|
|
43
|
+
}
|