@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/render/tile.d.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tile solids — pure geometry (no three.js), shared by the renderer and its
|
|
3
|
+
* tests. A tile is its outline (from @cubecore/skin: rounded shape, custom
|
|
4
|
+
* SVG path or stickerless) swept through a cross-section:
|
|
5
|
+
*
|
|
6
|
+
* top (flat) ── bevel (quarter round, `bevel` radius) ─┐
|
|
7
|
+
* │ wall
|
|
8
|
+
* z = −sink ─────────────────────────────────────────────── ┘ (into the body)
|
|
9
|
+
*
|
|
10
|
+
* Sides lying on the cube's outer edge (stickerless `fillOuter`) are not
|
|
11
|
+
* bevelled: they are mitred at 45° instead, so the tiles of two faces of one
|
|
12
|
+
* piece meet on the edge line and their colours touch, like on a real
|
|
13
|
+
* stickerless cube. With `edgeRadius` the cube edge is rounded: each of the
|
|
14
|
+
* two tiles curves over its half (45°) of a quarter round, meeting the other
|
|
15
|
+
* on the mitre plane — colour runs round the edge, no seam. Normals are given explicitly — smooth round the bevel,
|
|
16
|
+
* straight up on the top — so a lit ("plastic") material shades it cleanly.
|
|
17
|
+
*/
|
|
18
|
+
export type Pt = readonly [number, number];
|
|
19
|
+
export interface TileProfile {
|
|
20
|
+
/** Height of the tile above the cubie face (cubie units). */
|
|
21
|
+
thickness: number;
|
|
22
|
+
/** Radius of the rounded top edge (≤ thickness + sink). 0 = sharp. */
|
|
23
|
+
bevel: number;
|
|
24
|
+
/** Segments of the bevel's quarter round. */
|
|
25
|
+
segments: number;
|
|
26
|
+
/** How deep the tile reaches below the cubie face — into a body shrunk by `bodyInset`. */
|
|
27
|
+
sink: number;
|
|
28
|
+
/** Rounding of the cube's outer edges (stickerless, cubie units): each tile takes half of a quarter round. */
|
|
29
|
+
edgeRadius?: number;
|
|
30
|
+
/**
|
|
31
|
+
* Corner relief reaching into the tile: below `z` (height above the face,
|
|
32
|
+
* ≤ where the bevel starts) the tile is cut back by `r` round each of `at`,
|
|
33
|
+
* leaving a thinner overhanging corner above.
|
|
34
|
+
*/
|
|
35
|
+
undercut?: {
|
|
36
|
+
at: readonly Pt[];
|
|
37
|
+
r: number;
|
|
38
|
+
z: number;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export interface Mitre {
|
|
42
|
+
/** Which local sides lie on the cube's outer edge: -1 / 0 / +1 along x (u) and y (v). */
|
|
43
|
+
u: number;
|
|
44
|
+
v: number;
|
|
45
|
+
/** Coordinate of the cube edge at z = 0 (the outline reaches it there). */
|
|
46
|
+
edge: number;
|
|
47
|
+
/** Width of the zone next to the edge that blends from mitred to bevelled. */
|
|
48
|
+
ramp: number;
|
|
49
|
+
}
|
|
50
|
+
export interface TileSolid {
|
|
51
|
+
positions: Float32Array;
|
|
52
|
+
normals: Float32Array;
|
|
53
|
+
/** Side triangles (indices into positions). */
|
|
54
|
+
sides: Uint32Array;
|
|
55
|
+
/** Index of the first top-ring vertex; the top cap is triangulated from `topRing` by the caller. */
|
|
56
|
+
topStart: number;
|
|
57
|
+
/** The top outline (2D), for the caller's triangulator; its vertices follow `topStart` in positions. */
|
|
58
|
+
topRing: Pt[];
|
|
59
|
+
}
|
|
60
|
+
/** Drop consecutive duplicate points (rounded corners with radius 0 repeat their corner). */
|
|
61
|
+
export declare function dedupe(outline: readonly Pt[], eps?: number): Pt[];
|
|
62
|
+
export declare function tileSolid(outline: readonly Pt[], profile: TileProfile, mitre: Mitre | null): TileSolid;
|
|
63
|
+
/**
|
|
64
|
+
* The piece's plastic under a tile: the tile outline continued down into the
|
|
65
|
+
* cubie from `top` to `depth` (both below the face, positive numbers),
|
|
66
|
+
* narrowing by `taper` on the inner sides — the front of a piece is larger
|
|
67
|
+
* than its back, and a centre piece is as round as its tile. With `wall` the
|
|
68
|
+
* sides first run straight down to that depth (as real pieces: a flat bit of
|
|
69
|
+
* wall behind the tile, then the narrowing). Outer sides
|
|
70
|
+
* stay on the mitre, so the skirts of one piece's faces close its outside.
|
|
71
|
+
* No caps: the top is covered by the tile, the bottom faces the core.
|
|
72
|
+
*/
|
|
73
|
+
export declare function skirtSolid(outline: readonly Pt[], shape: {
|
|
74
|
+
top: number;
|
|
75
|
+
depth: number;
|
|
76
|
+
taper: number;
|
|
77
|
+
wall?: number;
|
|
78
|
+
relief?: {
|
|
79
|
+
at: readonly Pt[];
|
|
80
|
+
radius: number;
|
|
81
|
+
depth: number;
|
|
82
|
+
start?: number;
|
|
83
|
+
};
|
|
84
|
+
/** Extra evenly spaced rings from the wall down (for cutting the solid afterwards, e.g. a chamfer). */
|
|
85
|
+
steps?: number;
|
|
86
|
+
}, mitre: Mitre | null): TileSolid;
|
|
87
|
+
/**
|
|
88
|
+
* A domed top (e.g. QiYi's centre caps): flat inside a circle of `flat` ×
|
|
89
|
+
* the tile's reach, then falling straight to `drop` lower at the outline's
|
|
90
|
+
* farthest points — a round plateau at the tile's height, the corners
|
|
91
|
+
* lower. The tile's rings are lowered in proportion to their height above
|
|
92
|
+
* its bottom (`sink` below the face, which must be deeper than `drop`), and
|
|
93
|
+
* the flat cap is replaced by a polar mesh with a ring exactly on the
|
|
94
|
+
* circle, so the plateau's edge is round.
|
|
95
|
+
*/
|
|
96
|
+
export declare function applyDome(solid: TileSolid, dome: {
|
|
97
|
+
flat: number;
|
|
98
|
+
drop: number;
|
|
99
|
+
}, sink: number, spokes?: number): TileSolid;
|
|
100
|
+
/** The outline resampled to `count` points evenly along its length (dense enough for a domed top to be round). */
|
|
101
|
+
export declare function densify(outlineIn: readonly Pt[], count?: number): Pt[];
|
|
102
|
+
/**
|
|
103
|
+
* Cut a solid flat: points behind the plane through `c + d·n` square to `n`
|
|
104
|
+
* (unit, pointing away from `c`) are moved onto it and face back along `−n` —
|
|
105
|
+
* a piece's inside cut square to its direction from the cube's centre, clear
|
|
106
|
+
* of the mechanism ball of radius `d`. Needs rings dense enough where the plane crosses them.
|
|
107
|
+
*/
|
|
108
|
+
export declare function planeCut(solid: TileSolid, c: readonly [number, number, number], n: readonly [number, number, number], d: number): TileSolid;
|
|
109
|
+
/**
|
|
110
|
+
* Round a cube corner: points of a corner piece's solid beyond the ball of
|
|
111
|
+
* radius `r` at `c` (in all three outward directions `dir`) are pulled onto
|
|
112
|
+
* it — a cube corner rounder than its edges.
|
|
113
|
+
*/
|
|
114
|
+
export declare function cornerRound(solid: TileSolid, c: readonly [number, number, number], dir: readonly [number, number, number], r: number): TileSolid;
|
package/render/tile.js
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tile solids — pure geometry (no three.js), shared by the renderer and its
|
|
3
|
+
* tests. A tile is its outline (from @cubecore/skin: rounded shape, custom
|
|
4
|
+
* SVG path or stickerless) swept through a cross-section:
|
|
5
|
+
*
|
|
6
|
+
* top (flat) ── bevel (quarter round, `bevel` radius) ─┐
|
|
7
|
+
* │ wall
|
|
8
|
+
* z = −sink ─────────────────────────────────────────────── ┘ (into the body)
|
|
9
|
+
*
|
|
10
|
+
* Sides lying on the cube's outer edge (stickerless `fillOuter`) are not
|
|
11
|
+
* bevelled: they are mitred at 45° instead, so the tiles of two faces of one
|
|
12
|
+
* piece meet on the edge line and their colours touch, like on a real
|
|
13
|
+
* stickerless cube. With `edgeRadius` the cube edge is rounded: each of the
|
|
14
|
+
* two tiles curves over its half (45°) of a quarter round, meeting the other
|
|
15
|
+
* on the mitre plane — colour runs round the edge, no seam. Normals are given explicitly — smooth round the bevel,
|
|
16
|
+
* straight up on the top — so a lit ("plastic") material shades it cleanly.
|
|
17
|
+
*/
|
|
18
|
+
/** Drop consecutive duplicate points (rounded corners with radius 0 repeat their corner). */
|
|
19
|
+
export function dedupe(outline, eps = 1e-7) {
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const p of outline) {
|
|
22
|
+
const q = out[out.length - 1];
|
|
23
|
+
if (!q || Math.abs(p[0] - q[0]) > eps || Math.abs(p[1] - q[1]) > eps)
|
|
24
|
+
out.push(p);
|
|
25
|
+
}
|
|
26
|
+
const first = out[0], last = out[out.length - 1];
|
|
27
|
+
if (out.length > 1 && Math.abs(first[0] - last[0]) <= eps && Math.abs(first[1] - last[1]) <= eps)
|
|
28
|
+
out.pop();
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
/** Outward unit normal at each vertex (outline counter-clockwise) and the miter factor keeping offsets parallel. */
|
|
32
|
+
function vertexNormals(pts) {
|
|
33
|
+
const count = pts.length;
|
|
34
|
+
const edgeNormal = (i) => {
|
|
35
|
+
const a = pts[i], b = pts[(i + 1) % count];
|
|
36
|
+
const dx = b[0] - a[0], dy = b[1] - a[1];
|
|
37
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
38
|
+
return [dy / len, -dx / len];
|
|
39
|
+
};
|
|
40
|
+
return pts.map((_, i) => {
|
|
41
|
+
const e1 = edgeNormal((i + count - 1) % count), e2 = edgeNormal(i);
|
|
42
|
+
let nx = e1[0] + e2[0], ny = e1[1] + e2[1];
|
|
43
|
+
const len = Math.hypot(nx, ny) || 1;
|
|
44
|
+
nx /= len;
|
|
45
|
+
ny /= len;
|
|
46
|
+
const cos = nx * e2[0] + ny * e2[1];
|
|
47
|
+
return { n: [nx, ny], scale: 1 / Math.max(0.5, cos) };
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/** How strongly a point belongs to the cube's outer edge: 1 on it, 0 beyond `ramp` from it. */
|
|
51
|
+
function edgeWeight(c, dir, mitre) {
|
|
52
|
+
if (dir === 0)
|
|
53
|
+
return 0;
|
|
54
|
+
return Math.max(0, 1 - (mitre.edge - c * dir) / mitre.ramp);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Cut a disc of radius `r` round `c` out of a closed ring of points (in place,
|
|
58
|
+
* keeping the point count): the run of points inside the disc is laid out
|
|
59
|
+
* along the arc between where the ring enters and leaves it, with normals of
|
|
60
|
+
* a cone facing its axis and downwards (`slope`: how fast the radius grows with depth).
|
|
61
|
+
*/
|
|
62
|
+
function cutCorner(pts, nrm, c, r, slope) {
|
|
63
|
+
const n = pts.length;
|
|
64
|
+
const inside = pts.map(([x, y]) => Math.hypot(x - c[0], y - c[1]) < r);
|
|
65
|
+
if (inside.every(Boolean) || !inside.some(Boolean))
|
|
66
|
+
return;
|
|
67
|
+
// Find each run of inside points (the ring is closed).
|
|
68
|
+
let start = inside.findIndex((v, i) => v && !inside[(i - 1 + n) % n]);
|
|
69
|
+
const visited = new Set();
|
|
70
|
+
while (start >= 0 && !visited.has(start)) {
|
|
71
|
+
visited.add(start);
|
|
72
|
+
const run = [];
|
|
73
|
+
for (let i = start; inside[i % n] && run.length < n; i++)
|
|
74
|
+
run.push(i % n);
|
|
75
|
+
const before = pts[(start - 1 + n) % n], after = pts[(run[run.length - 1] + 1) % n];
|
|
76
|
+
// Where the ring crosses the circle: between the last outside point and the first inside one (and back).
|
|
77
|
+
const cross = (p, q) => {
|
|
78
|
+
let lo = 0, hi = 1; // p outside, q inside
|
|
79
|
+
for (let k = 0; k < 30; k++) {
|
|
80
|
+
const m = (lo + hi) / 2, x = p[0] + (q[0] - p[0]) * m, y = p[1] + (q[1] - p[1]) * m;
|
|
81
|
+
if (Math.hypot(x - c[0], y - c[1]) < r)
|
|
82
|
+
hi = m;
|
|
83
|
+
else
|
|
84
|
+
lo = m;
|
|
85
|
+
}
|
|
86
|
+
const m = (lo + hi) / 2;
|
|
87
|
+
return Math.atan2(p[1] + (q[1] - p[1]) * m - c[1], p[0] + (q[0] - p[0]) * m - c[0]);
|
|
88
|
+
};
|
|
89
|
+
const a0 = cross(before, pts[run[0]]);
|
|
90
|
+
let a1 = cross(after, pts[run[run.length - 1]]);
|
|
91
|
+
// The arc away from the corner: the shorter way round (the removed bit is less than half the disc).
|
|
92
|
+
let da = a1 - a0;
|
|
93
|
+
while (da > Math.PI)
|
|
94
|
+
da -= 2 * Math.PI;
|
|
95
|
+
while (da < -Math.PI)
|
|
96
|
+
da += 2 * Math.PI;
|
|
97
|
+
a1 = a0 + da;
|
|
98
|
+
// Place each point by its distance along the original outline (entry → exit), so neighbouring rings stay in step.
|
|
99
|
+
const z = pts[run[0]][2], nl = Math.hypot(1, slope);
|
|
100
|
+
const path = [before, ...run.map((i) => pts[i]), after];
|
|
101
|
+
const cum = [0];
|
|
102
|
+
for (let k = 1; k < path.length; k++)
|
|
103
|
+
cum.push(cum[k - 1] + Math.hypot(path[k][0] - path[k - 1][0], path[k][1] - path[k - 1][1]));
|
|
104
|
+
const total = cum[cum.length - 1] || 1;
|
|
105
|
+
run.forEach((idx, k) => {
|
|
106
|
+
const a = a0 + (cum[k + 1] / total) * (a1 - a0);
|
|
107
|
+
pts[idx] = [c[0] + r * Math.cos(a), c[1] + r * Math.sin(a), z];
|
|
108
|
+
nrm[idx] = [-Math.cos(a) / nl, -Math.sin(a) / nl, -slope / nl]; // the tile overhangs the cut: it faces down
|
|
109
|
+
});
|
|
110
|
+
const next = inside.findIndex((v, i) => i > run[run.length - 1] && v && !inside[(i - 1 + n) % n]);
|
|
111
|
+
start = next;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Sweep `outline` through `rings` (bottom up); optionally close the top with a flat cap. */
|
|
115
|
+
function sweep(outlineIn, rings, mitre, cap) {
|
|
116
|
+
const outline = dedupe(outlineIn);
|
|
117
|
+
const vn = vertexNormals(outline);
|
|
118
|
+
const n = outline.length;
|
|
119
|
+
const positions = [];
|
|
120
|
+
const normals = [];
|
|
121
|
+
const weights = outline.map(([x, y]) => ({
|
|
122
|
+
ku: mitre ? edgeWeight(x, mitre.u, mitre) : 0,
|
|
123
|
+
kv: mitre ? edgeWeight(y, mitre.v, mitre) : 0,
|
|
124
|
+
}));
|
|
125
|
+
// Deep rings shrink the outline towards the tile centre (so its inner sides move in by `d`): unlike an
|
|
126
|
+
// offset along the normals, this can't turn a corner inside out when `d` exceeds the corner's radius.
|
|
127
|
+
const innerHalf = Math.min(...[0, 1].flatMap((k) => [Math.max(...outline.map((p) => p[k])), -Math.min(...outline.map((p) => p[k]))]));
|
|
128
|
+
const place = (i, ring) => {
|
|
129
|
+
let [x, y] = outline[i];
|
|
130
|
+
if (ring.clamp) {
|
|
131
|
+
const k = Math.max(0, 1 - ring.d / innerHalf);
|
|
132
|
+
x *= k;
|
|
133
|
+
y *= k;
|
|
134
|
+
if (mitre) {
|
|
135
|
+
const limit = mitre.edge + ring.z;
|
|
136
|
+
if (mitre.u !== 0)
|
|
137
|
+
x = mitre.u > 0 ? Math.min(x, limit) : Math.max(x, -limit);
|
|
138
|
+
if (mitre.v !== 0)
|
|
139
|
+
y = mitre.v > 0 ? Math.min(y, limit) : Math.max(y, -limit);
|
|
140
|
+
}
|
|
141
|
+
return [x, y, ring.z];
|
|
142
|
+
}
|
|
143
|
+
const { ku, kv } = weights[i];
|
|
144
|
+
// The inner offset fades out towards the outer edge; outer sides follow the mitre / edge round.
|
|
145
|
+
const innerWeight = (1 - ku) * (1 - kv);
|
|
146
|
+
x -= vn[i].n[0] * ring.d * vn[i].scale * innerWeight;
|
|
147
|
+
y -= vn[i].n[1] * ring.d * vn[i].scale * innerWeight;
|
|
148
|
+
if (mitre) {
|
|
149
|
+
x += mitre.u * ring.off * ku;
|
|
150
|
+
y += mitre.v * ring.off * kv;
|
|
151
|
+
}
|
|
152
|
+
const k = Math.max(ku, kv);
|
|
153
|
+
return [x, y, ring.z * (1 - k) + ring.oz * k];
|
|
154
|
+
};
|
|
155
|
+
const normal = (i, ring) => {
|
|
156
|
+
const { ku, kv } = weights[i];
|
|
157
|
+
const k = Math.max(ku, kv);
|
|
158
|
+
const c = Math.cos(ring.phi), s = Math.sin(ring.phi);
|
|
159
|
+
let nx = vn[i].n[0] * c * (1 - k), ny = vn[i].n[1] * c * (1 - k), nz = s * (1 - k);
|
|
160
|
+
if (k > 0 && mitre) {
|
|
161
|
+
const ou = mitre.u * ku, ov = mitre.v * kv, ol = Math.hypot(ou, ov) || 1;
|
|
162
|
+
nx += (ou / ol) * Math.cos(ring.ophi) * k;
|
|
163
|
+
ny += (ov / ol) * Math.cos(ring.ophi) * k;
|
|
164
|
+
nz += Math.sin(ring.ophi) * k;
|
|
165
|
+
}
|
|
166
|
+
const l = Math.hypot(nx, ny, nz) || 1;
|
|
167
|
+
return [nx / l, ny / l, nz / l];
|
|
168
|
+
};
|
|
169
|
+
for (const ring of rings) {
|
|
170
|
+
const pts = outline.map((_, i) => place(i, ring));
|
|
171
|
+
const nrm = outline.map((_, i) => normal(i, ring));
|
|
172
|
+
if (ring.cut && ring.cut.r > 0)
|
|
173
|
+
for (const c of ring.cut.at)
|
|
174
|
+
cutCorner(pts, nrm, c, ring.cut.r, ring.cut.slope);
|
|
175
|
+
for (let i = 0; i < n; i++) {
|
|
176
|
+
positions.push(...pts[i]);
|
|
177
|
+
normals.push(...nrm[i]);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const sides = [];
|
|
181
|
+
for (let j = 0; j + 1 < rings.length; j++) {
|
|
182
|
+
for (let i = 0; i < n; i++) {
|
|
183
|
+
const a = j * n + i, bb = j * n + ((i + 1) % n), c = (j + 1) * n + ((i + 1) % n), d = (j + 1) * n + i;
|
|
184
|
+
sides.push(a, bb, c, a, c, d);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Top cap: its own copy of the top ring, facing straight up.
|
|
188
|
+
const topStart = positions.length / 3;
|
|
189
|
+
const topRing = [];
|
|
190
|
+
if (cap) {
|
|
191
|
+
const top = rings[rings.length - 1];
|
|
192
|
+
for (let i = 0; i < n; i++) {
|
|
193
|
+
const p = place(i, top);
|
|
194
|
+
positions.push(...p);
|
|
195
|
+
normals.push(0, 0, 1);
|
|
196
|
+
topRing.push([p[0], p[1]]);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { positions: new Float32Array(positions), normals: new Float32Array(normals), sides: new Uint32Array(sides), topStart, topRing };
|
|
200
|
+
}
|
|
201
|
+
export function tileSolid(outline, profile, mitre) {
|
|
202
|
+
const t = profile.thickness;
|
|
203
|
+
const b = Math.max(0, Math.min(profile.bevel, t + profile.sink));
|
|
204
|
+
// The rounded cube edge dips 0.29·R below the top where the two tiles meet; keep that above the bottom.
|
|
205
|
+
const R = mitre ? Math.max(0, Math.min(profile.edgeRadius ?? 0, (t + profile.sink) / (1 - Math.SQRT1_2))) : 0;
|
|
206
|
+
const segs = b > 0 || R > 0 ? Math.max(1, profile.segments) : 0;
|
|
207
|
+
const arcStart = t - R * (1 - Math.SQRT1_2); // where the edge round meets the mitre plane
|
|
208
|
+
const flat = (z) => ({ z, d: 0, phi: 0, oz: z, off: z, ophi: 0 });
|
|
209
|
+
const rings = [flat(-profile.sink)];
|
|
210
|
+
const uc = profile.undercut;
|
|
211
|
+
if (uc && uc.r > 0 && uc.at.length) {
|
|
212
|
+
// Cut back from the bottom up to z, then a ledge (the underside of the overhang) back out to the outline.
|
|
213
|
+
const z = Math.max(-profile.sink, Math.min(uc.z, t - b));
|
|
214
|
+
const cut = { at: uc.at, r: uc.r, slope: 0 };
|
|
215
|
+
rings[0] = { ...rings[0], cut };
|
|
216
|
+
rings.push({ ...flat(z), cut }, flat(z));
|
|
217
|
+
}
|
|
218
|
+
if (segs > 0) {
|
|
219
|
+
rings.push({ z: t - b, d: 0, phi: 0, oz: arcStart, off: arcStart, ophi: 0 });
|
|
220
|
+
for (let k = 1; k <= segs; k++) {
|
|
221
|
+
const s = k / segs;
|
|
222
|
+
const th = s * (Math.PI / 2);
|
|
223
|
+
const op = Math.PI / 4 + s * (Math.PI / 4); // 45° (on the mitre) → 90° (flat top)
|
|
224
|
+
rings.push({
|
|
225
|
+
z: t - b + b * Math.sin(th),
|
|
226
|
+
d: b * (1 - Math.cos(th)),
|
|
227
|
+
phi: th,
|
|
228
|
+
oz: t - R + R * Math.sin(op),
|
|
229
|
+
off: t - R + R * Math.cos(op),
|
|
230
|
+
ophi: op,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else
|
|
235
|
+
rings.push({ z: t, d: 0, phi: 0, oz: t, off: t, ophi: 0 });
|
|
236
|
+
return sweep(outline, rings, mitre, true);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* The piece's plastic under a tile: the tile outline continued down into the
|
|
240
|
+
* cubie from `top` to `depth` (both below the face, positive numbers),
|
|
241
|
+
* narrowing by `taper` on the inner sides — the front of a piece is larger
|
|
242
|
+
* than its back, and a centre piece is as round as its tile. With `wall` the
|
|
243
|
+
* sides first run straight down to that depth (as real pieces: a flat bit of
|
|
244
|
+
* wall behind the tile, then the narrowing). Outer sides
|
|
245
|
+
* stay on the mitre, so the skirts of one piece's faces close its outside.
|
|
246
|
+
* No caps: the top is covered by the tile, the bottom faces the core.
|
|
247
|
+
*/
|
|
248
|
+
export function skirtSolid(outline, shape, mitre) {
|
|
249
|
+
// Straight walls along the tile outline down to `wall`, then leaning in by `taper` at `depth` (a crisp crease between).
|
|
250
|
+
const wall = Math.min(Math.max(shape.wall ?? shape.top, shape.top), shape.depth);
|
|
251
|
+
const h = Math.max(1e-6, shape.depth - wall);
|
|
252
|
+
const phi = -Math.atan2(shape.taper, h); // walls lean inwards going down: normals tilt downwards
|
|
253
|
+
const inset = (z) => (z <= wall ? 0 : ((z - wall) / h) * shape.taper);
|
|
254
|
+
// Corner relief: a cone cut from the tile's corner(s) next to the centre, `start` wide at the tile, `radius` at `depth` below it.
|
|
255
|
+
const relief = shape.relief && shape.relief.radius > 0 && shape.relief.at.length ? shape.relief : null;
|
|
256
|
+
const r0 = relief?.start ?? 0;
|
|
257
|
+
const cutAt = (z) => relief
|
|
258
|
+
? { at: relief.at, r: r0 + (relief.radius - r0) * Math.min(1, Math.max(0, (z - shape.top) / relief.depth)), slope: (relief.radius - r0) / relief.depth }
|
|
259
|
+
: undefined;
|
|
260
|
+
const ring = (z, tilt) => ({ z: -z, d: inset(z), phi: tilt, oz: -z, off: -z, ophi: 0, clamp: true, cut: cutAt(z) });
|
|
261
|
+
const levels = new Set([shape.top, wall, shape.depth]);
|
|
262
|
+
if (relief)
|
|
263
|
+
for (let k = 1; k <= 16; k++)
|
|
264
|
+
levels.add(Math.min(shape.depth, shape.top + (relief.depth * k) / 16));
|
|
265
|
+
for (let k = 1; k < (shape.steps ?? 0); k++)
|
|
266
|
+
levels.add(wall + ((shape.depth - wall) * k) / (shape.steps ?? 1));
|
|
267
|
+
const zs = [...levels].sort((a, b) => b - a); // bottom up
|
|
268
|
+
const rings = [];
|
|
269
|
+
for (const z of zs) {
|
|
270
|
+
if (z === wall && wall > shape.top)
|
|
271
|
+
rings.push(ring(z, phi), ring(z, 0)); // the crease: two normals
|
|
272
|
+
else
|
|
273
|
+
rings.push(ring(z, wall > shape.top && z <= wall ? 0 : phi));
|
|
274
|
+
}
|
|
275
|
+
return sweep(outline, rings, mitre, false);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* A domed top (e.g. QiYi's centre caps): flat inside a circle of `flat` ×
|
|
279
|
+
* the tile's reach, then falling straight to `drop` lower at the outline's
|
|
280
|
+
* farthest points — a round plateau at the tile's height, the corners
|
|
281
|
+
* lower. The tile's rings are lowered in proportion to their height above
|
|
282
|
+
* its bottom (`sink` below the face, which must be deeper than `drop`), and
|
|
283
|
+
* the flat cap is replaced by a polar mesh with a ring exactly on the
|
|
284
|
+
* circle, so the plateau's edge is round.
|
|
285
|
+
*/
|
|
286
|
+
export function applyDome(solid, dome, sink, spokes = 10) {
|
|
287
|
+
const ring = solid.topRing;
|
|
288
|
+
if (!ring.length)
|
|
289
|
+
return solid;
|
|
290
|
+
const reach = Math.max(...ring.map(([x, y]) => Math.hypot(x, y)));
|
|
291
|
+
const r0 = dome.flat * reach;
|
|
292
|
+
const slope = dome.drop / Math.max(1e-6, reach - r0);
|
|
293
|
+
const lowerBy = (r) => Math.max(0, r - r0) * slope;
|
|
294
|
+
const zTop = solid.positions[solid.topStart * 3 + 2];
|
|
295
|
+
// Sides: lowered in proportion to height, so their bottom stays put.
|
|
296
|
+
const positions = Array.from(solid.positions.subarray(0, solid.topStart * 3));
|
|
297
|
+
const normals = Array.from(solid.normals.subarray(0, solid.topStart * 3));
|
|
298
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
299
|
+
const k = (positions[i + 2] + sink) / (zTop + sink);
|
|
300
|
+
positions[i + 2] -= lowerBy(Math.hypot(positions[i], positions[i + 1])) * Math.max(0, Math.min(1, k));
|
|
301
|
+
}
|
|
302
|
+
// Cap: a ray from the middle to each outline point — flat out to the circle, then down the slope.
|
|
303
|
+
const sides = Array.from(solid.sides);
|
|
304
|
+
const start = positions.length / 3;
|
|
305
|
+
const n = ring.length;
|
|
306
|
+
const samples = spokes * 2 + 1;
|
|
307
|
+
for (const [px, py] of ring) {
|
|
308
|
+
const R = Math.hypot(px, py) || 1e-9;
|
|
309
|
+
const ux = px / R, uy = py / R;
|
|
310
|
+
const rc = Math.min(r0, R);
|
|
311
|
+
for (let k = 0; k < samples; k++) {
|
|
312
|
+
const r = k <= spokes ? (rc * k) / spokes : rc + ((R - rc) * (k - spokes)) / spokes;
|
|
313
|
+
positions.push(ux * r, uy * r, zTop - lowerBy(r));
|
|
314
|
+
if (r > r0 + 1e-9) {
|
|
315
|
+
const l = Math.hypot(slope, 1);
|
|
316
|
+
normals.push((ux * slope) / l, (uy * slope) / l, 1 / l);
|
|
317
|
+
}
|
|
318
|
+
else
|
|
319
|
+
normals.push(0, 0, 1);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
for (let i = 0; i < n; i++) {
|
|
323
|
+
const j = (i + 1) % n;
|
|
324
|
+
for (let k = 0; k + 1 < samples; k++) {
|
|
325
|
+
const a = start + i * samples + k, b = start + i * samples + k + 1, c = start + j * samples + k + 1, d = start + j * samples + k;
|
|
326
|
+
sides.push(a, b, c, a, c, d);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return { positions: new Float32Array(positions), normals: new Float32Array(normals), sides: new Uint32Array(sides), topStart: positions.length / 3, topRing: [] };
|
|
330
|
+
}
|
|
331
|
+
/** The outline resampled to `count` points evenly along its length (dense enough for a domed top to be round). */
|
|
332
|
+
export function densify(outlineIn, count = 128) {
|
|
333
|
+
const pts = dedupe(outlineIn);
|
|
334
|
+
const n = pts.length;
|
|
335
|
+
const seg = pts.map((p, i) => Math.hypot(pts[(i + 1) % n][0] - p[0], pts[(i + 1) % n][1] - p[1]));
|
|
336
|
+
const total = seg.reduce((a, b) => a + b, 0);
|
|
337
|
+
const out = [];
|
|
338
|
+
let i = 0, acc = 0;
|
|
339
|
+
for (let k = 0; k < count; k++) {
|
|
340
|
+
const target = (k / count) * total;
|
|
341
|
+
while (acc + seg[i] < target && i < n - 1)
|
|
342
|
+
acc += seg[i++];
|
|
343
|
+
const t = seg[i] > 0 ? (target - acc) / seg[i] : 0;
|
|
344
|
+
const a = pts[i], b = pts[(i + 1) % n];
|
|
345
|
+
out.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]);
|
|
346
|
+
}
|
|
347
|
+
return out;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Cut a solid flat: points behind the plane through `c + d·n` square to `n`
|
|
351
|
+
* (unit, pointing away from `c`) are moved onto it and face back along `−n` —
|
|
352
|
+
* a piece's inside cut square to its direction from the cube's centre, clear
|
|
353
|
+
* of the mechanism ball of radius `d`. Needs rings dense enough where the plane crosses them.
|
|
354
|
+
*/
|
|
355
|
+
export function planeCut(solid, c, n, d) {
|
|
356
|
+
const positions = solid.positions.slice();
|
|
357
|
+
const normals = solid.normals.slice();
|
|
358
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
359
|
+
const s = (positions[i] - c[0]) * n[0] + (positions[i + 1] - c[1]) * n[1] + (positions[i + 2] - c[2]) * n[2] - d;
|
|
360
|
+
if (s >= 0)
|
|
361
|
+
continue;
|
|
362
|
+
for (let k = 0; k < 3; k++) {
|
|
363
|
+
positions[i + k] -= s * n[k];
|
|
364
|
+
normals[i + k] = -n[k];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return { ...solid, positions, normals };
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Round a cube corner: points of a corner piece's solid beyond the ball of
|
|
371
|
+
* radius `r` at `c` (in all three outward directions `dir`) are pulled onto
|
|
372
|
+
* it — a cube corner rounder than its edges.
|
|
373
|
+
*/
|
|
374
|
+
export function cornerRound(solid, c, dir, r) {
|
|
375
|
+
const positions = solid.positions.slice();
|
|
376
|
+
const normals = solid.normals.slice();
|
|
377
|
+
for (let i = 0; i < positions.length; i += 3) {
|
|
378
|
+
const d = [positions[i] - c[0], positions[i + 1] - c[1], positions[i + 2] - c[2]];
|
|
379
|
+
if (d[0] * dir[0] <= 0 || d[1] * dir[1] <= 0 || d[2] * dir[2] <= 0)
|
|
380
|
+
continue;
|
|
381
|
+
const l = Math.hypot(d[0], d[1], d[2]);
|
|
382
|
+
if (l <= r)
|
|
383
|
+
continue;
|
|
384
|
+
for (let k = 0; k < 3; k++) {
|
|
385
|
+
positions[i + k] = c[k] + (d[k] / l) * r;
|
|
386
|
+
normals[i + k] = d[k] / l;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return { ...solid, positions, normals };
|
|
390
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Back view — a second camera on the opposite side of the cube, showing the
|
|
3
|
+
* three faces the main view hides (like cubing.js `backView`). Pure layout
|
|
4
|
+
* maths, shared by the renderer and its tests.
|
|
5
|
+
*
|
|
6
|
+
* "none" — main view only
|
|
7
|
+
* "side-by-side" — main view left, back view right, each half the canvas
|
|
8
|
+
* "top-right" — main view full size, back view as an inset in the corner
|
|
9
|
+
*/
|
|
10
|
+
export type BackView = "none" | "side-by-side" | "top-right";
|
|
11
|
+
/** Pixel rectangle, WebGL convention: origin bottom-left. */
|
|
12
|
+
export interface Rect {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
w: number;
|
|
16
|
+
h: number;
|
|
17
|
+
}
|
|
18
|
+
/** Inset size relative to the canvas' shorter side. */
|
|
19
|
+
export declare const INSET = 0.36;
|
|
20
|
+
export declare function viewports(mode: BackView, w: number, h: number): {
|
|
21
|
+
main: Rect;
|
|
22
|
+
back: Rect | null;
|
|
23
|
+
};
|
|
24
|
+
/** The back camera sits opposite the main one (through the cube's centre): it sees exactly the hidden faces. */
|
|
25
|
+
export declare function backPosition(main: readonly [number, number, number]): [number, number, number];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Back view — a second camera on the opposite side of the cube, showing the
|
|
3
|
+
* three faces the main view hides (like cubing.js `backView`). Pure layout
|
|
4
|
+
* maths, shared by the renderer and its tests.
|
|
5
|
+
*
|
|
6
|
+
* "none" — main view only
|
|
7
|
+
* "side-by-side" — main view left, back view right, each half the canvas
|
|
8
|
+
* "top-right" — main view full size, back view as an inset in the corner
|
|
9
|
+
*/
|
|
10
|
+
/** Inset size relative to the canvas' shorter side. */
|
|
11
|
+
export const INSET = 0.36;
|
|
12
|
+
export function viewports(mode, w, h) {
|
|
13
|
+
if (mode === "side-by-side") {
|
|
14
|
+
const half = Math.floor(w / 2);
|
|
15
|
+
return { main: { x: 0, y: 0, w: half, h }, back: { x: half, y: 0, w: w - half, h } };
|
|
16
|
+
}
|
|
17
|
+
if (mode === "top-right") {
|
|
18
|
+
const s = Math.max(1, Math.round(Math.min(w, h) * INSET));
|
|
19
|
+
return { main: { x: 0, y: 0, w, h }, back: { x: w - s, y: h - s, w: s, h: s } };
|
|
20
|
+
}
|
|
21
|
+
return { main: { x: 0, y: 0, w, h }, back: null };
|
|
22
|
+
}
|
|
23
|
+
/** The back camera sits opposite the main one (through the cube's centre): it sees exactly the hidden faces. */
|
|
24
|
+
export function backPosition(main) {
|
|
25
|
+
return [-main[0], -main[1], -main[2]];
|
|
26
|
+
}
|
package/roux/cmll.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CMLL recognition — the last-layer corners with both blocks built, colour
|
|
3
|
+
* neutral and scheme-free. Read relative to the BLOCKS (the M slice may be
|
|
4
|
+
* off): L / R colours from the side centres, front / back / bottom from the
|
|
5
|
+
* first block's own stickers, the top colour is the one left. Corners only
|
|
6
|
+
* (the edges are LSE), compared up to the angle (pre-AUF) and a final U turn.
|
|
7
|
+
*
|
|
8
|
+
* recognizeCmll(state) // { id: "Sune Left Bar", group: "Sune", preAuf: "U", alg } | null (skip) — cube held blocks L/R, bottom D
|
|
9
|
+
*/
|
|
10
|
+
import { FRAMES, type State } from "../core/index.js";
|
|
11
|
+
export { CMLL_CASES, type CmllCase } from "./cmllData.js";
|
|
12
|
+
export interface CmllMatch {
|
|
13
|
+
id: string;
|
|
14
|
+
group: string;
|
|
15
|
+
/** The U turn before the case's algorithm, as the cube is held now. */
|
|
16
|
+
preAuf: string;
|
|
17
|
+
alg: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The CMLL case of a cube held with the blocks on L / R and their bottom
|
|
21
|
+
* on D (both blocks solved) — null when the corners are already solved (up
|
|
22
|
+
* to AUF) or it isn't a CMLL state.
|
|
23
|
+
*/
|
|
24
|
+
export declare function recognizeCmll(s: State): CmllMatch | null;
|
|
25
|
+
/** CMLL of a cube with both Roux blocks built in any orientation (colour neutral): the frame, or null. */
|
|
26
|
+
export declare function recognizeCmllAnywhere(state: State, blocksBuilt: (s: State) => boolean): {
|
|
27
|
+
match: CmllMatch | "skip";
|
|
28
|
+
frame: (typeof FRAMES)[number];
|
|
29
|
+
} | null;
|
|
30
|
+
/**
|
|
31
|
+
* A cube to practise one CMLL case on: both blocks solved, the corners in
|
|
32
|
+
* the case (from a random angle), the last six edges random. Make a
|
|
33
|
+
* scramble to it with @cubecore/solve `scrambleTo(state, { from })`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function cmllCaseState(id: string, random?: () => number): State;
|