@dice-o-rolla/dice-geometry 0.3.1 → 0.5.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/README.md CHANGED
@@ -13,3 +13,11 @@ const d20 = getDieGeometry('d20');
13
13
  ```
14
14
 
15
15
  Licensed under Apache-2.0. See `LICENSE`, `NOTICE`, and `THIRD_PARTY_NOTICES.md` in the package.
16
+
17
+ ## Connected net API
18
+
19
+ `createStandardDiceNet('d6')` returns a `StandardDiceNet` with `geometryId` and complete per-corner
20
+ UV coordinates, normalized to `[0, 1]` with a bottom-left origin. Keys are physical face values and
21
+ corner order follows `face.indices`. `StandardDiceNetType` covers d4, d6, d8, d10, d12 and d20. The
22
+ layouts are deterministic and preserve polygon proportions; d6 uses a cross. d100/d66 artwork
23
+ variants reuse d10/d6 geometry. No Three.js, native image library or filesystem is required.
package/dist/index.d.ts CHANGED
@@ -9,3 +9,4 @@ export { getDieGeometry, getRegisteredDieTypes, hasDieGeometry } from './geometr
9
9
  export { resolveFace } from './resolve-face.js';
10
10
  export type { FaceDefinition, PolygonDefinition, PolyhedronDefinition, Vector3Tuple, } from './types.js';
11
11
  export { assertValidPolyhedronDefinition, getPolyhedronDefinitionIssues } from './validation.js';
12
+ export { createStandardDiceNet, type StandardDiceNet, type StandardDiceNetType, } from './standard-dice-net.js';
package/dist/index.js CHANGED
@@ -8,3 +8,4 @@ export { calculateFaceNormal } from './face-normal.js';
8
8
  export { getDieGeometry, getRegisteredDieTypes, hasDieGeometry } from './geometry-registry.js';
9
9
  export { resolveFace } from './resolve-face.js';
10
10
  export { assertValidPolyhedronDefinition, getPolyhedronDefinitionIssues } from './validation.js';
11
+ export { createStandardDiceNet, } from './standard-dice-net.js';
@@ -0,0 +1,8 @@
1
+ export type StandardDiceNetType = 'd4' | 'd6' | 'd8' | 'd10' | 'd12' | 'd20';
2
+ export interface StandardDiceNet {
3
+ readonly geometryId: StandardDiceNetType;
4
+ /** Complete per-corner UVs, normalized with a bottom-left origin. */
5
+ readonly faces: Readonly<Record<number, readonly (readonly [u: number, v: number])[]>>;
6
+ }
7
+ /** Deterministic edge unfolding with bounded backtracking; d6 uses a fixed cross. */
8
+ export declare function createStandardDiceNet(type: StandardDiceNetType): StandardDiceNet;
@@ -0,0 +1,102 @@
1
+ import { calculateFaceNormal } from './face-normal.js';
2
+ import { getDieGeometry } from './geometry-registry.js';
3
+ const dot = (a, b) => a.reduce((sum, value, index) => sum + value * b[index], 0);
4
+ const sub = (a, b) => [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
5
+ const unit = (a) => {
6
+ const length = Math.hypot(...a);
7
+ return [a[0] / length, a[1] / length, a[2] / length];
8
+ };
9
+ const cross = (a, b) => [
10
+ a[1] * b[2] - a[2] * b[1],
11
+ a[2] * b[0] - a[0] * b[2],
12
+ a[0] * b[1] - a[1] * b[0],
13
+ ];
14
+ /** Positive-area intersection only: shared edges and vertices are allowed. */
15
+ function polygonsOverlap(a, b) {
16
+ for (const polygon of [a, b]) {
17
+ for (let i = 0; i < polygon.length; i++) {
18
+ const p = polygon[i], q = polygon[(i + 1) % polygon.length];
19
+ const axis = [p[1] - q[1], q[0] - p[0]];
20
+ const project = (points) => points.map((v) => v[0] * axis[0] + v[1] * axis[1]);
21
+ const x = project(a), y = project(b);
22
+ if (Math.min(Math.max(...x), Math.max(...y)) - Math.max(Math.min(...x), Math.min(...y)) <
23
+ 1e-9)
24
+ return false;
25
+ }
26
+ }
27
+ return true;
28
+ }
29
+ function projectFace(definition, faceIndex, edge, start, end) {
30
+ const face = definition.faces[faceIndex];
31
+ const origin = definition.vertices[face.indices[edge]];
32
+ const next = definition.vertices[face.indices[(edge + 1) % face.indices.length]];
33
+ const horizontal = unit(sub(next, origin));
34
+ const normal = calculateFaceNormal(definition, face);
35
+ const vertical = unit(cross(normal, horizontal));
36
+ const length = Math.hypot(end[0] - start[0], end[1] - start[1]);
37
+ const x = (end[0] - start[0]) / length, y = (end[1] - start[1]) / length;
38
+ return face.indices.map((index) => {
39
+ const offset = sub(definition.vertices[index], origin);
40
+ const u = dot(offset, horizontal), v = dot(offset, vertical);
41
+ return [start[0] + u * x - v * y, start[1] + u * y + v * x];
42
+ });
43
+ }
44
+ /** Deterministic edge unfolding with bounded backtracking; d6 uses a fixed cross. */
45
+ export function createStandardDiceNet(type) {
46
+ const definition = getDieGeometry(type);
47
+ const first = definition.faces[0];
48
+ const length = Math.hypot(...sub(definition.vertices[first.indices[1]], definition.vertices[first.indices[0]]));
49
+ const placed = new Map([
50
+ [0, projectFace(definition, 0, 0, [0, 0], [length, 0])],
51
+ ]);
52
+ let attempts = 0;
53
+ const search = () => {
54
+ if (placed.size === definition.faces.length)
55
+ return true;
56
+ if (++attempts > 100_000)
57
+ throw new Error(`Cannot unfold ${definition.id}`);
58
+ const parents = Array.from(placed);
59
+ for (const [parentIndex, parentPoints] of parents) {
60
+ const parent = definition.faces[parentIndex];
61
+ for (let childIndex = 0; childIndex < definition.faces.length; childIndex++) {
62
+ if (placed.has(childIndex))
63
+ continue;
64
+ const child = definition.faces[childIndex];
65
+ if (definition.id === 'd6' && (child.value === 6 ? parent.value !== 3 : parent.value !== 1))
66
+ continue;
67
+ for (let edge = 0; edge < child.indices.length; edge++) {
68
+ const a = parent.indices.indexOf(child.indices[edge]);
69
+ const b = parent.indices.indexOf(child.indices[(edge + 1) % child.indices.length]);
70
+ if (a < 0 || b < 0 || (b + 1) % parent.indices.length !== a)
71
+ continue;
72
+ const polygon = projectFace(definition, childIndex, edge, parentPoints[a], parentPoints[b]);
73
+ if ([...placed.values()].some((other) => polygonsOverlap(polygon, other)))
74
+ continue;
75
+ placed.set(childIndex, polygon);
76
+ if (search())
77
+ return true;
78
+ placed.delete(childIndex);
79
+ }
80
+ }
81
+ }
82
+ return false;
83
+ };
84
+ if (!search())
85
+ throw new Error(`No connected net for ${definition.id}`);
86
+ const points = [...placed.values()].flat();
87
+ const minX = Math.min(...points.map((p) => p[0])), minY = Math.min(...points.map((p) => p[1]));
88
+ const width = Math.max(...points.map((p) => p[0])) - minX, height = Math.max(...points.map((p) => p[1])) - minY;
89
+ const extent = Math.max(width, height) / 0.9;
90
+ return {
91
+ geometryId: type,
92
+ faces: Object.fromEntries(definition.faces.map((face, index) => [
93
+ face.value,
94
+ placed
95
+ .get(index)
96
+ .map(([x, y]) => [
97
+ (x - minX + (extent - width) / 2) / extent,
98
+ (y - minY + (extent - height) / 2) / extent,
99
+ ]),
100
+ ])),
101
+ };
102
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dice-o-rolla/dice-geometry",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "Immutable polyhedral dice geometry and settled-face resolution.",
5
5
  "keywords": [
6
6
  "dice",
@@ -40,7 +40,7 @@
40
40
  "registry": "https://registry.npmjs.org/"
41
41
  },
42
42
  "dependencies": {
43
- "@dice-o-rolla/dice-core": "0.3.1"
43
+ "@dice-o-rolla/dice-core": "0.5.0"
44
44
  },
45
45
  "engines": {
46
46
  "node": ">=20.0.0"