@matboks/utilities 0.0.1
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/dist/constants.d.ts +1 -0
- package/dist/constants.js +1 -0
- package/dist/geometry/aabb.d.ts +40 -0
- package/dist/geometry/aabb.js +143 -0
- package/dist/geometry/cut-polygon.d.ts +7 -0
- package/dist/geometry/cut-polygon.js +116 -0
- package/dist/geometry/hollowgon.d.ts +24 -0
- package/dist/geometry/hollowgon.js +156 -0
- package/dist/geometry/index.d.ts +13 -0
- package/dist/geometry/index.js +14 -0
- package/dist/geometry/line.d.ts +10 -0
- package/dist/geometry/line.js +26 -0
- package/dist/geometry/polygon-operations.d.ts +31 -0
- package/dist/geometry/polygon-operations.js +159 -0
- package/dist/geometry/polygon.d.ts +55 -0
- package/dist/geometry/polygon.js +353 -0
- package/dist/geometry/polyline.d.ts +23 -0
- package/dist/geometry/polyline.js +100 -0
- package/dist/geometry/ray.d.ts +6 -0
- package/dist/geometry/ray.js +6 -0
- package/dist/geometry/ray3.d.ts +7 -0
- package/dist/geometry/ray3.js +10 -0
- package/dist/geometry/segment.d.ts +16 -0
- package/dist/geometry/segment.js +50 -0
- package/dist/geometry/shared.d.ts +5 -0
- package/dist/geometry/shared.js +60 -0
- package/dist/geometry/transformations.d.ts +7 -0
- package/dist/geometry/transformations.js +28 -0
- package/dist/geometry/vec2.d.ts +71 -0
- package/dist/geometry/vec2.js +225 -0
- package/dist/geometry/vec3.d.ts +102 -0
- package/dist/geometry/vec3.js +256 -0
- package/dist/geometry/vec4.d.ts +71 -0
- package/dist/geometry/vec4.js +238 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/math/clamp.d.ts +1 -0
- package/dist/math/clamp.js +5 -0
- package/dist/math/fract.d.ts +1 -0
- package/dist/math/fract.js +3 -0
- package/dist/math/index.d.ts +8 -0
- package/dist/math/index.js +8 -0
- package/dist/math/mix.d.ts +1 -0
- package/dist/math/mix.js +3 -0
- package/dist/math/mod.d.ts +1 -0
- package/dist/math/mod.js +5 -0
- package/dist/math/signed.d.ts +1 -0
- package/dist/math/signed.js +3 -0
- package/dist/math/smoothstep.d.ts +3 -0
- package/dist/math/smoothstep.js +24 -0
- package/dist/math/split-into-int-and-fract.d.ts +1 -0
- package/dist/math/split-into-int-and-fract.js +5 -0
- package/dist/math/units.d.ts +2 -0
- package/dist/math/units.js +6 -0
- package/dist/noise/idw.d.ts +11 -0
- package/dist/noise/idw.js +10 -0
- package/dist/noise/index.d.ts +6 -0
- package/dist/noise/index.js +6 -0
- package/dist/noise/perlin.d.ts +5 -0
- package/dist/noise/perlin.js +29 -0
- package/dist/noise/random.d.ts +13 -0
- package/dist/noise/random.js +39 -0
- package/dist/noise/value.d.ts +4 -0
- package/dist/noise/value.js +20 -0
- package/dist/noise/voronoise.d.ts +10 -0
- package/dist/noise/voronoise.js +26 -0
- package/dist/noise/worley.d.ts +4 -0
- package/dist/noise/worley.js +39 -0
- package/dist/shader-modules/index.d.ts +2 -0
- package/dist/shader-modules/index.js +2 -0
- package/dist/shader-modules/library.d.ts +2 -0
- package/dist/shader-modules/library.js +36 -0
- package/dist/shader-modules/modules/camera.d.ts +1 -0
- package/dist/shader-modules/modules/camera.js +43 -0
- package/dist/shader-modules/modules/color/blend.d.ts +1 -0
- package/dist/shader-modules/modules/color/blend.js +108 -0
- package/dist/shader-modules/modules/color/index.d.ts +1 -0
- package/dist/shader-modules/modules/color/index.js +135 -0
- package/dist/shader-modules/modules/constants.d.ts +1 -0
- package/dist/shader-modules/modules/constants.js +14 -0
- package/dist/shader-modules/modules/geometry.d.ts +1 -0
- package/dist/shader-modules/modules/geometry.js +110 -0
- package/dist/shader-modules/modules/math.d.ts +1 -0
- package/dist/shader-modules/modules/math.js +19 -0
- package/dist/shader-modules/modules/noise.d.ts +1 -0
- package/dist/shader-modules/modules/noise.js +410 -0
- package/dist/shader-modules/modules/random.d.ts +1 -0
- package/dist/shader-modules/modules/random.js +147 -0
- package/dist/shader-modules/modules/ray-marching.d.ts +1 -0
- package/dist/shader-modules/modules/ray-marching.js +54 -0
- package/dist/shader-modules/modules/sdf/index.d.ts +1 -0
- package/dist/shader-modules/modules/sdf/index.js +183 -0
- package/dist/shader-modules/modules/sdf/operations.d.ts +1 -0
- package/dist/shader-modules/modules/sdf/operations.js +77 -0
- package/dist/shader-modules/modules/utils.d.ts +1 -0
- package/dist/shader-modules/modules/utils.js +115 -0
- package/dist/shader-modules/registry.d.ts +2 -0
- package/dist/shader-modules/registry.js +7 -0
- package/dist/shader-modules/shaders.d.ts +1 -0
- package/dist/shader-modules/shaders.js +109 -0
- package/dist/shader-renderer/helpers.d.ts +10 -0
- package/dist/shader-renderer/helpers.js +19 -0
- package/dist/shader-renderer/index.d.ts +2 -0
- package/dist/shader-renderer/index.js +2 -0
- package/dist/shader-renderer/pixel-shader.d.ts +32 -0
- package/dist/shader-renderer/pixel-shader.js +172 -0
- package/dist/shader-renderer/pixel-vertex-shader.d.ts +1 -0
- package/dist/shader-renderer/pixel-vertex-shader.js +14 -0
- package/dist/shader-renderer/texture.d.ts +28 -0
- package/dist/shader-renderer/texture.js +97 -0
- package/dist/shader-renderer/types.d.ts +17 -0
- package/dist/shader-renderer/types.js +4 -0
- package/dist/shader-renderer/uniform.d.ts +16 -0
- package/dist/shader-renderer/uniform.js +134 -0
- package/dist/tilings/delaunay.d.ts +11 -0
- package/dist/tilings/delaunay.js +28 -0
- package/dist/tilings/grid.d.ts +5 -0
- package/dist/tilings/grid.js +29 -0
- package/dist/tilings/hexagononal.d.ts +4 -0
- package/dist/tilings/hexagononal.js +40 -0
- package/dist/tilings/index.d.ts +12 -0
- package/dist/tilings/index.js +12 -0
- package/dist/tilings/line.d.ts +5 -0
- package/dist/tilings/line.js +19 -0
- package/dist/tilings/mediterranean.d.ts +3 -0
- package/dist/tilings/mediterranean.js +49 -0
- package/dist/tilings/pythagorean.d.ts +3 -0
- package/dist/tilings/pythagorean.js +40 -0
- package/dist/tilings/random-cuts.d.ts +6 -0
- package/dist/tilings/random-cuts.js +16 -0
- package/dist/tilings/recursive-cuts.d.ts +5 -0
- package/dist/tilings/recursive-cuts.js +11 -0
- package/dist/tilings/rhombille.d.ts +3 -0
- package/dist/tilings/rhombille.js +16 -0
- package/dist/tilings/rightangled-triangle.d.ts +3 -0
- package/dist/tilings/rightangled-triangle.js +29 -0
- package/dist/tilings/triangle.d.ts +3 -0
- package/dist/tilings/triangle.js +17 -0
- package/dist/tilings/voronoi.d.ts +11 -0
- package/dist/tilings/voronoi.js +29 -0
- package/dist/tilings/weaving.d.ts +1 -0
- package/dist/tilings/weaving.js +8 -0
- package/dist/transforms/camera/camera.d.ts +32 -0
- package/dist/transforms/camera/camera.js +60 -0
- package/dist/transforms/camera/index.d.ts +3 -0
- package/dist/transforms/camera/index.js +3 -0
- package/dist/transforms/camera/orthographic.d.ts +15 -0
- package/dist/transforms/camera/orthographic.js +31 -0
- package/dist/transforms/camera/perspective.d.ts +16 -0
- package/dist/transforms/camera/perspective.js +40 -0
- package/dist/transforms/index.d.ts +2 -0
- package/dist/transforms/index.js +2 -0
- package/dist/transforms/normalize-transform.d.ts +12 -0
- package/dist/transforms/normalize-transform.js +20 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.js +1 -0
- package/dist/utilities/create-frame.d.ts +2 -0
- package/dist/utilities/create-frame.js +6 -0
- package/dist/utilities/ensure-array.d.ts +1 -0
- package/dist/utilities/ensure-array.js +3 -0
- package/dist/utilities/idw-interpolator.d.ts +9 -0
- package/dist/utilities/idw-interpolator.js +18 -0
- package/dist/utilities/index.d.ts +8 -0
- package/dist/utilities/index.js +8 -0
- package/dist/utilities/marching-squares.d.ts +51 -0
- package/dist/utilities/marching-squares.js +177 -0
- package/dist/utilities/point-sampler.d.ts +12 -0
- package/dist/utilities/point-sampler.js +24 -0
- package/dist/utilities/poisson/grid.d.ts +21 -0
- package/dist/utilities/poisson/grid.js +51 -0
- package/dist/utilities/poisson/poissonnier.d.ts +50 -0
- package/dist/utilities/poisson/poissonnier.js +118 -0
- package/dist/utilities/resolution.d.ts +10 -0
- package/dist/utilities/resolution.js +14 -0
- package/dist/utilities/rng.d.ts +13 -0
- package/dist/utilities/rng.js +80 -0
- package/package.json +28 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as MarchingSquares from "marching-squares";
|
|
2
|
+
import { Vec2, Polygon, AABB } from "../geometry/index.js";
|
|
3
|
+
type ContouringOptions = {
|
|
4
|
+
fn: (v: Vec2) => number;
|
|
5
|
+
extent: AABB;
|
|
6
|
+
resolution?: number | [number, number];
|
|
7
|
+
density?: number | [number, number];
|
|
8
|
+
};
|
|
9
|
+
type Interval = [number, number];
|
|
10
|
+
type IsolineResult = {
|
|
11
|
+
isolines: Array<Polygon>;
|
|
12
|
+
level: number;
|
|
13
|
+
};
|
|
14
|
+
type Isoband = {
|
|
15
|
+
boundary: Polygon;
|
|
16
|
+
holes: Array<Polygon>;
|
|
17
|
+
};
|
|
18
|
+
type IsobandResult = {
|
|
19
|
+
isobands: Array<Isoband>;
|
|
20
|
+
interval: [number, number];
|
|
21
|
+
};
|
|
22
|
+
export declare class Contouring {
|
|
23
|
+
options: ContouringOptions;
|
|
24
|
+
surfaceFunction: (v: Vec2) => number;
|
|
25
|
+
extent: AABB;
|
|
26
|
+
cellSize: Vec2;
|
|
27
|
+
rows: number;
|
|
28
|
+
columns: number;
|
|
29
|
+
valueMatrix?: Array<Array<number>>;
|
|
30
|
+
quadTree?: MarchingSquares.QuadTree;
|
|
31
|
+
valueRange?: Interval;
|
|
32
|
+
constructor(options: ContouringOptions);
|
|
33
|
+
rowToYCoordinate(row: number): number;
|
|
34
|
+
columnToXCoordinate(column: number): number;
|
|
35
|
+
getValueMatrix(): number[][];
|
|
36
|
+
getValueRange(): Interval;
|
|
37
|
+
getQuadTree(): MarchingSquares.QuadTree;
|
|
38
|
+
rawPathToPolygon(rawVertices: Array<[number, number]>): Polygon;
|
|
39
|
+
createIntervals(options: CreateContouringIntervalsOptions): Array<Interval>;
|
|
40
|
+
isoline(levels: Array<number>): IsolineResult[];
|
|
41
|
+
isoband(intervals: Array<Interval>): IsobandResult[];
|
|
42
|
+
}
|
|
43
|
+
export type CreateContouringIntervalsOptions = {
|
|
44
|
+
n?: number;
|
|
45
|
+
density?: number;
|
|
46
|
+
lower?: number;
|
|
47
|
+
upper?: number;
|
|
48
|
+
overlap?: number;
|
|
49
|
+
padding?: number;
|
|
50
|
+
};
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import * as MarchingSquares from "marching-squares";
|
|
2
|
+
import { vec2, Polygon } from "../geometry/index.js";
|
|
3
|
+
import { Resolution } from "./resolution.js";
|
|
4
|
+
export class Contouring {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
const { extent, fn } = options;
|
|
8
|
+
const { width, height } = extent;
|
|
9
|
+
const { columns, rows } = computeResolution(options).toRowsAndColumns();
|
|
10
|
+
this.surfaceFunction = fn;
|
|
11
|
+
this.extent = extent;
|
|
12
|
+
this.columns = columns;
|
|
13
|
+
this.rows = rows;
|
|
14
|
+
this.cellSize = vec2(width / columns, height / rows);
|
|
15
|
+
}
|
|
16
|
+
rowToYCoordinate(row) {
|
|
17
|
+
const { cellSize, extent } = this;
|
|
18
|
+
return extent.yMin + (row + 0.5) * cellSize.y;
|
|
19
|
+
}
|
|
20
|
+
columnToXCoordinate(column) {
|
|
21
|
+
const { cellSize, extent } = this;
|
|
22
|
+
return extent.xMin + (column + 0.5) * cellSize.x;
|
|
23
|
+
}
|
|
24
|
+
getValueMatrix() {
|
|
25
|
+
if (this.valueMatrix)
|
|
26
|
+
return this.valueMatrix;
|
|
27
|
+
let minValue = Infinity, maxValue = -Infinity;
|
|
28
|
+
const { surfaceFunction, rows, columns } = this;
|
|
29
|
+
const values = [];
|
|
30
|
+
for (let row = 0; row < rows; row++) {
|
|
31
|
+
const rowValues = [];
|
|
32
|
+
const y = this.rowToYCoordinate(row);
|
|
33
|
+
for (let column = 0; column < columns; column++) {
|
|
34
|
+
const x = this.columnToXCoordinate(column);
|
|
35
|
+
const value = surfaceFunction(vec2(x, y));
|
|
36
|
+
rowValues.push(value);
|
|
37
|
+
if (value < minValue)
|
|
38
|
+
minValue = value;
|
|
39
|
+
if (value > maxValue)
|
|
40
|
+
maxValue = value;
|
|
41
|
+
}
|
|
42
|
+
values.push(rowValues);
|
|
43
|
+
}
|
|
44
|
+
this.valueMatrix = values;
|
|
45
|
+
this.valueRange = [minValue, maxValue];
|
|
46
|
+
return values;
|
|
47
|
+
}
|
|
48
|
+
getValueRange() {
|
|
49
|
+
if (!this.valueRange)
|
|
50
|
+
this.getValueMatrix();
|
|
51
|
+
return this.valueRange;
|
|
52
|
+
}
|
|
53
|
+
getQuadTree() {
|
|
54
|
+
return this.quadTree ?? (this.quadTree = new MarchingSquares.QuadTree(this.getValueMatrix()));
|
|
55
|
+
}
|
|
56
|
+
rawPathToPolygon(rawVertices) {
|
|
57
|
+
let vertices = [];
|
|
58
|
+
// First and last vertex are the same
|
|
59
|
+
for (let i = 0, n = rawVertices.length - 1; i < n; i++) {
|
|
60
|
+
const [column, row] = rawVertices[i];
|
|
61
|
+
vertices.push(vec2(this.columnToXCoordinate(column), this.rowToYCoordinate(row)));
|
|
62
|
+
}
|
|
63
|
+
return new Polygon(vertices);
|
|
64
|
+
}
|
|
65
|
+
createIntervals(options) {
|
|
66
|
+
const range = this.getValueRange();
|
|
67
|
+
let { n, density, lower = range[0], upper = range[1], overlap = 0, padding = 0 } = options;
|
|
68
|
+
if (n !== undefined && density !== undefined)
|
|
69
|
+
throw new Error("Either n or density must be specified");
|
|
70
|
+
let width = upper - lower;
|
|
71
|
+
if (padding > 0) {
|
|
72
|
+
lower += -0.5 * padding * width;
|
|
73
|
+
upper += 0.5 * padding * width;
|
|
74
|
+
width += padding * width;
|
|
75
|
+
}
|
|
76
|
+
const numThresholds = n ?? Math.round(density * width);
|
|
77
|
+
if (numThresholds < 2)
|
|
78
|
+
return [[lower, upper]];
|
|
79
|
+
const bandwidth = width / ((1 - overlap) * numThresholds + overlap);
|
|
80
|
+
const step = (width - bandwidth) / (numThresholds - 1);
|
|
81
|
+
let intervals = [];
|
|
82
|
+
for (let i = 0; i < numThresholds; i++) {
|
|
83
|
+
const intervalLower = lower + i * step;
|
|
84
|
+
intervals.push([intervalLower, intervalLower + bandwidth]);
|
|
85
|
+
}
|
|
86
|
+
return intervals;
|
|
87
|
+
}
|
|
88
|
+
isoline(levels) {
|
|
89
|
+
const quadTree = this.getQuadTree();
|
|
90
|
+
let results = [];
|
|
91
|
+
for (const level of levels) {
|
|
92
|
+
const [rings] = MarchingSquares.isoLines(quadTree, [level]);
|
|
93
|
+
const isolines = [];
|
|
94
|
+
for (const ring of rings)
|
|
95
|
+
isolines.push(this.rawPathToPolygon(ring));
|
|
96
|
+
results.push({ isolines, level });
|
|
97
|
+
}
|
|
98
|
+
return results;
|
|
99
|
+
}
|
|
100
|
+
isoband(intervals) {
|
|
101
|
+
const quadTree = this.getQuadTree();
|
|
102
|
+
let results = [];
|
|
103
|
+
for (const [lower, upper] of intervals) {
|
|
104
|
+
const [rings] = MarchingSquares.isoBands(quadTree, [lower], [upper - lower]);
|
|
105
|
+
const polygons = [];
|
|
106
|
+
for (const ring of rings)
|
|
107
|
+
polygons.push(this.rawPathToPolygon(ring));
|
|
108
|
+
results.push({ isobands: determineBoundariesAndHoles(polygons), interval: [lower, upper] });
|
|
109
|
+
}
|
|
110
|
+
return results;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function computeResolution(options) {
|
|
114
|
+
const { resolution, density, extent } = options;
|
|
115
|
+
if (resolution !== undefined && density !== undefined)
|
|
116
|
+
throw new Error("Only one of resolution and density must be specified");
|
|
117
|
+
if (resolution === undefined && density === undefined)
|
|
118
|
+
throw new Error("One of resolution and density must be specified");
|
|
119
|
+
const { width, height } = extent;
|
|
120
|
+
const aspectRatio = width / height;
|
|
121
|
+
let columns, rows;
|
|
122
|
+
if (density) {
|
|
123
|
+
const [columnDensity, rowDensity] = Array.isArray(density) ? density : [density, density];
|
|
124
|
+
columns = Math.round(columnDensity * width);
|
|
125
|
+
rows = Math.round(rowDensity * height);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
if (Array.isArray(resolution)) {
|
|
129
|
+
[columns, rows] = resolution;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
[columns, rows] = aspectRatio > 1 ?
|
|
133
|
+
[resolution, Math.round(resolution / aspectRatio)] :
|
|
134
|
+
[Math.round(resolution * aspectRatio), resolution];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return new Resolution(columns, rows);
|
|
138
|
+
}
|
|
139
|
+
function determineBoundariesAndHoles(polygons) {
|
|
140
|
+
const n = polygons.length;
|
|
141
|
+
let isobands = [];
|
|
142
|
+
const isIndexProcessedMap = {};
|
|
143
|
+
polygons.sort((a, b) => b.area() - a.area());
|
|
144
|
+
function helper(activeShape, isHole, index) {
|
|
145
|
+
let holes = [];
|
|
146
|
+
for (let i = index + 1; i < n; i++) {
|
|
147
|
+
if (isIndexProcessedMap[i])
|
|
148
|
+
continue;
|
|
149
|
+
const candidate = polygons[i];
|
|
150
|
+
// Enough to check if a single vertex is inside parent shape
|
|
151
|
+
const isContained = activeShape.aabb().contains(candidate.aabb()) &&
|
|
152
|
+
candidate.vertices.some((v) => activeShape.isPointInside(v));
|
|
153
|
+
if (isContained) {
|
|
154
|
+
helper(candidate, !isHole, i);
|
|
155
|
+
isIndexProcessedMap[i] = true;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (!isHole) {
|
|
161
|
+
candidate.makeClockwise();
|
|
162
|
+
holes.push(candidate);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (isHole)
|
|
166
|
+
return;
|
|
167
|
+
activeShape.makeCounterClockwise();
|
|
168
|
+
isobands.push({ boundary: activeShape, holes });
|
|
169
|
+
}
|
|
170
|
+
for (let i = 0; i < n; i++) {
|
|
171
|
+
const boundary = polygons[i];
|
|
172
|
+
if (isIndexProcessedMap[i])
|
|
173
|
+
continue;
|
|
174
|
+
helper(boundary, false, i);
|
|
175
|
+
}
|
|
176
|
+
return isobands;
|
|
177
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AABB } from "../geometry/aabb.js";
|
|
2
|
+
import { Hollowgon } from "../geometry/hollowgon.js";
|
|
3
|
+
import { Polygon } from "../geometry/polygon.js";
|
|
4
|
+
import { RNG } from "./rng.js";
|
|
5
|
+
export type PointSamplerStrategy = "uniform" | "poisson";
|
|
6
|
+
export type PointSamplerOptions = {
|
|
7
|
+
size: number;
|
|
8
|
+
strategy: PointSamplerStrategy;
|
|
9
|
+
shape: Polygon | Hollowgon | AABB;
|
|
10
|
+
rng: RNG;
|
|
11
|
+
};
|
|
12
|
+
export declare function pointSampler(options: PointSamplerOptions): import("../index.js").Vec2[];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AABB } from "../geometry/aabb.js";
|
|
2
|
+
import { Poissonnier } from "./poisson/poissonnier.js";
|
|
3
|
+
export function pointSampler(options) {
|
|
4
|
+
const { size, strategy, shape, rng } = options;
|
|
5
|
+
const isAABB = shape instanceof AABB;
|
|
6
|
+
if (strategy === "poisson") {
|
|
7
|
+
// TODO polygon is approximate for now
|
|
8
|
+
const aabb = isAABB ? shape : shape.aabb();
|
|
9
|
+
const sampleSize = isAABB ? size : Math.ceil(size * aabb.area / shape.area());
|
|
10
|
+
const aabbSample = Poissonnier.generateFixedSizeSample({
|
|
11
|
+
extent: shape instanceof AABB ? shape : shape.aabb(),
|
|
12
|
+
sampleSize,
|
|
13
|
+
rng
|
|
14
|
+
});
|
|
15
|
+
return isAABB ? aabbSample : aabbSample.filter((v) => shape.isPointInside(v));
|
|
16
|
+
}
|
|
17
|
+
else if (strategy === "uniform") {
|
|
18
|
+
const sampler = isAABB ? () => rng.pointInAABB(shape) : () => rng.pointInPolygon(shape);
|
|
19
|
+
return Array(size).fill(null).map(sampler);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
throw new Error(`Invalid strategy ${strategy}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AABB, Vec2 } from "../../geometry/index.js";
|
|
2
|
+
type GridResolution = {
|
|
3
|
+
columns: number;
|
|
4
|
+
rows: number;
|
|
5
|
+
};
|
|
6
|
+
export declare class LookupGrid {
|
|
7
|
+
extent: AABB;
|
|
8
|
+
minimumDistance: number;
|
|
9
|
+
isVariable: boolean;
|
|
10
|
+
resolution: GridResolution;
|
|
11
|
+
cellSize: Vec2;
|
|
12
|
+
occupied: Array<Vec2>;
|
|
13
|
+
constructor(extent: AABB, minimumDistance: number, isVariable: boolean);
|
|
14
|
+
computeGridPosition(p: Vec2): {
|
|
15
|
+
column: number;
|
|
16
|
+
row: number;
|
|
17
|
+
};
|
|
18
|
+
validatePosition(p: Vec2, radius: number): boolean;
|
|
19
|
+
addToGrid(p: Vec2): void;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { vec2 } from "../../geometry/index.js";
|
|
2
|
+
const { ceil, max, min } = Math;
|
|
3
|
+
export class LookupGrid {
|
|
4
|
+
constructor(extent, minimumDistance, isVariable) {
|
|
5
|
+
this.extent = extent;
|
|
6
|
+
this.isVariable = isVariable;
|
|
7
|
+
this.minimumDistance = minimumDistance;
|
|
8
|
+
const { xMin, xMax, yMin, yMax } = extent;
|
|
9
|
+
const gridSize = minimumDistance / 2 ** 0.5;
|
|
10
|
+
const width = xMax - xMin;
|
|
11
|
+
const height = yMax - yMin;
|
|
12
|
+
const rows = ceil(height / gridSize);
|
|
13
|
+
const columns = ceil(width / gridSize);
|
|
14
|
+
this.resolution = { columns, rows };
|
|
15
|
+
this.occupied = [];
|
|
16
|
+
this.cellSize = vec2(width / columns, height / rows);
|
|
17
|
+
}
|
|
18
|
+
computeGridPosition(p) {
|
|
19
|
+
const { extent, cellSize } = this;
|
|
20
|
+
const column = (p.x - extent.xMin) / cellSize.x | 0;
|
|
21
|
+
const row = (p.y - extent.yMin) / cellSize.y | 0;
|
|
22
|
+
return { column, row };
|
|
23
|
+
}
|
|
24
|
+
validatePosition(p, radius) {
|
|
25
|
+
const { extent, resolution: { columns, rows }, cellSize } = this;
|
|
26
|
+
if (!extent.isPointInside(p))
|
|
27
|
+
return false;
|
|
28
|
+
const radius2 = radius ** 2;
|
|
29
|
+
const { column, row } = this.computeGridPosition(p);
|
|
30
|
+
let dX = 1;
|
|
31
|
+
let dY = 1;
|
|
32
|
+
if (this.isVariable) {
|
|
33
|
+
dX = ((radius / cellSize.x) | 0) + 1;
|
|
34
|
+
dY = ((radius / cellSize.y) | 0) + 1;
|
|
35
|
+
}
|
|
36
|
+
let i0 = max(0, row - dY), i1 = min(rows - 1, row + dY), j0 = max(0, column - dX), j1 = min(columns - 1, column + dX);
|
|
37
|
+
for (let i = i0; i <= i1; i++) {
|
|
38
|
+
for (let j = j0; j <= j1; j++) {
|
|
39
|
+
const existingPoint = this.occupied[i * columns + j];
|
|
40
|
+
if (existingPoint && p.distanceSquared(existingPoint) < radius2)
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
addToGrid(p) {
|
|
47
|
+
const { resolution: { columns } } = this;
|
|
48
|
+
const { column, row } = this.computeGridPosition(p);
|
|
49
|
+
this.occupied[row * columns + column] = p;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { AABB, Vec2 } from "../../geometry/index.js";
|
|
2
|
+
import { RNG } from "../rng.js";
|
|
3
|
+
import { LookupGrid } from "./grid.js";
|
|
4
|
+
export type PoissonnierOptions = {
|
|
5
|
+
extent: AABB;
|
|
6
|
+
minDistance: number;
|
|
7
|
+
distanceFunction?: (v: Vec2) => number;
|
|
8
|
+
maxScale?: number;
|
|
9
|
+
attempts?: number;
|
|
10
|
+
rng: RNG;
|
|
11
|
+
};
|
|
12
|
+
export type PointData = {
|
|
13
|
+
point: Vec2;
|
|
14
|
+
value: number;
|
|
15
|
+
distance: number;
|
|
16
|
+
};
|
|
17
|
+
export declare class Poissonnier {
|
|
18
|
+
extent: AABB;
|
|
19
|
+
minDistance: number;
|
|
20
|
+
distanceRange: number;
|
|
21
|
+
distanceFunction?: (v: Vec2) => number;
|
|
22
|
+
attempts: number;
|
|
23
|
+
rng: RNG;
|
|
24
|
+
grid: LookupGrid;
|
|
25
|
+
sampleData: Array<PointData>;
|
|
26
|
+
activeIndex: number;
|
|
27
|
+
private activeGenerator;
|
|
28
|
+
constructor(options: PoissonnierOptions);
|
|
29
|
+
static generateFixedSizeSample(options: GenerateFixedSizeSampleOptions): Vec2[];
|
|
30
|
+
private initialize;
|
|
31
|
+
addPoint(point: Vec2): {
|
|
32
|
+
point: Vec2;
|
|
33
|
+
value: number;
|
|
34
|
+
distance: number;
|
|
35
|
+
};
|
|
36
|
+
private internalAddPoint;
|
|
37
|
+
private createGenerator;
|
|
38
|
+
getNext(): PointData | undefined;
|
|
39
|
+
getAll(): PointData[];
|
|
40
|
+
private sampleCandidate;
|
|
41
|
+
[Symbol.iterator](): {
|
|
42
|
+
next: () => IteratorResult<PointData, undefined>;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export type GenerateFixedSizeSampleOptions = {
|
|
46
|
+
extent: AABB;
|
|
47
|
+
sampleSize: number;
|
|
48
|
+
rng: RNG;
|
|
49
|
+
padding?: number;
|
|
50
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { EPS } from "../../constants.js";
|
|
2
|
+
import { AABB, Vec2, vec2 } from "../../geometry/index.js";
|
|
3
|
+
import { LookupGrid } from "./grid.js";
|
|
4
|
+
const { sin, cos, PI, sqrt, max } = Math;
|
|
5
|
+
const TWO_PI = 2 * PI;
|
|
6
|
+
// add padding (make extent smaller)
|
|
7
|
+
// periodicity (lets columns and rows wrap around)
|
|
8
|
+
export class Poissonnier {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.sampleData = []; // Stores points accepted into the grid
|
|
11
|
+
this.activeIndex = 0;
|
|
12
|
+
const { extent, minDistance, maxScale = 2, distanceFunction, attempts = 20, rng } = options;
|
|
13
|
+
if (maxScale < 1)
|
|
14
|
+
throw new Error("maxScale can not be less than 1");
|
|
15
|
+
this.extent = extent;
|
|
16
|
+
this.minDistance = minDistance;
|
|
17
|
+
this.distanceRange = distanceFunction ? (maxScale * minDistance - minDistance) : 0;
|
|
18
|
+
this.distanceFunction = distanceFunction;
|
|
19
|
+
this.rng = rng;
|
|
20
|
+
this.grid = new LookupGrid(extent, minDistance, distanceFunction !== undefined);
|
|
21
|
+
this.attempts = attempts;
|
|
22
|
+
this.activeGenerator = this.createGenerator();
|
|
23
|
+
}
|
|
24
|
+
static generateFixedSizeSample(options) {
|
|
25
|
+
return generateFixedSizeSample(options);
|
|
26
|
+
}
|
|
27
|
+
initialize() {
|
|
28
|
+
const { extent, rng, grid, minDistance, distanceRange } = this;
|
|
29
|
+
const paddedExtent = extent.clone().grow(-2 * (minDistance + distanceRange));
|
|
30
|
+
const startPoint = rng.pointInAABB(paddedExtent);
|
|
31
|
+
grid.addToGrid(startPoint);
|
|
32
|
+
return this.internalAddPoint(startPoint);
|
|
33
|
+
}
|
|
34
|
+
addPoint(point) {
|
|
35
|
+
return this.internalAddPoint(point, true);
|
|
36
|
+
}
|
|
37
|
+
internalAddPoint(point, ensureInside = false) {
|
|
38
|
+
const { distanceFunction, distanceRange, minDistance } = this;
|
|
39
|
+
if (ensureInside && !this.extent.isPointInside(point))
|
|
40
|
+
throw new Error(`Added point is outside extent ${this.extent}`);
|
|
41
|
+
this.grid.addToGrid(point);
|
|
42
|
+
const value = distanceFunction?.(point) ?? 0;
|
|
43
|
+
const distance = minDistance + distanceRange * value;
|
|
44
|
+
const data = { point, value, distance };
|
|
45
|
+
this.sampleData.push(data);
|
|
46
|
+
return data;
|
|
47
|
+
}
|
|
48
|
+
*createGenerator() {
|
|
49
|
+
if (this.sampleData.length === 0)
|
|
50
|
+
yield this.initialize();
|
|
51
|
+
while (this.activeIndex < this.sampleData.length) {
|
|
52
|
+
let { point, distance } = this.sampleData[this.activeIndex];
|
|
53
|
+
// Add eps to ensure that distance check only fails when the distance is actually larger than threshold
|
|
54
|
+
const candidateDistance = distance + EPS;
|
|
55
|
+
for (let candidateIndex = 0, n = this.attempts; candidateIndex < n; candidateIndex++) {
|
|
56
|
+
const candidate = this.sampleCandidate(point, candidateDistance);
|
|
57
|
+
const isValid = this.grid.validatePosition(candidate, distance);
|
|
58
|
+
if (!isValid)
|
|
59
|
+
continue;
|
|
60
|
+
yield this.internalAddPoint(candidate);
|
|
61
|
+
}
|
|
62
|
+
this.activeIndex++;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
getNext() {
|
|
67
|
+
const { value } = this.activeGenerator.next();
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
getAll() {
|
|
71
|
+
while (this.getNext()) { }
|
|
72
|
+
return this.sampleData;
|
|
73
|
+
}
|
|
74
|
+
sampleCandidate(origin, distance) {
|
|
75
|
+
const theta = TWO_PI * this.rng();
|
|
76
|
+
return vec2(origin.x + distance * cos(theta), origin.y + distance * sin(theta));
|
|
77
|
+
}
|
|
78
|
+
[Symbol.iterator]() {
|
|
79
|
+
return {
|
|
80
|
+
next: () => this.activeGenerator.next()
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Idea: Generate sample for slightly larger extent,
|
|
85
|
+
// using the heuristic distance = constant * sqrt(area / sampleSize)
|
|
86
|
+
// where constant is roughly 0.82 for typical setups.
|
|
87
|
+
// Then pick out sampleSize points and rescale to fill the original extent
|
|
88
|
+
// Does not work well for extreme aspect ratios
|
|
89
|
+
function generateFixedSizeSample(options) {
|
|
90
|
+
const { extent, sampleSize, rng, padding = 0.5 } = options;
|
|
91
|
+
const { width, height, center, aspectRatio, area } = extent;
|
|
92
|
+
const distance = 0.95 * sqrt(area / sampleSize);
|
|
93
|
+
const paddedExtent = extent.clone().grow(vec2(width, height).times(0.05));
|
|
94
|
+
let sample;
|
|
95
|
+
while (true) {
|
|
96
|
+
sample = new Poissonnier({
|
|
97
|
+
extent: paddedExtent, minDistance: distance, rng
|
|
98
|
+
}).getAll();
|
|
99
|
+
if (sample.length >= sampleSize)
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
const sortData = sample.map(({ point }) => {
|
|
103
|
+
const diff = Vec2.subtract(point, center).absolute();
|
|
104
|
+
const value = max(diff.x, aspectRatio * diff.y);
|
|
105
|
+
return { point, value };
|
|
106
|
+
});
|
|
107
|
+
sortData.sort((a, b) => a.value - b.value);
|
|
108
|
+
let points = [];
|
|
109
|
+
const candidatesAABB = AABB.placeholder();
|
|
110
|
+
for (let i = 0; i < sampleSize; i++) {
|
|
111
|
+
const { point } = sortData[i];
|
|
112
|
+
candidatesAABB.update(point);
|
|
113
|
+
points.push(point);
|
|
114
|
+
}
|
|
115
|
+
const paddingAmount = padding * distance;
|
|
116
|
+
candidatesAABB.grow(rng(-paddingAmount, 0), rng(0, paddingAmount), rng(-paddingAmount, 0), rng(0, paddingAmount));
|
|
117
|
+
return points.map((p) => extent.mapFromUV(candidatesAABB.mapToUV(p)));
|
|
118
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { vec2 } from "../geometry/vec2.js";
|
|
2
|
+
export class Resolution {
|
|
3
|
+
constructor(width, height) {
|
|
4
|
+
this.width = width;
|
|
5
|
+
this.height = height;
|
|
6
|
+
}
|
|
7
|
+
toRowsAndColumns() {
|
|
8
|
+
const { width, height } = this;
|
|
9
|
+
return { columns: width, rows: height };
|
|
10
|
+
}
|
|
11
|
+
toVec2() {
|
|
12
|
+
return vec2(this.width, this.height);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AABB, Hollowgon, Polygon, Vec2 } from "../geometry/index.js";
|
|
2
|
+
export declare function hash(value: number, seed: number): number;
|
|
3
|
+
export type RNG = ReturnType<typeof createRNG>;
|
|
4
|
+
export declare function createRNG(seed: number | (() => number)): ((min?: number, max?: number) => number) & {
|
|
5
|
+
integer: (lower: number, upper?: number) => number;
|
|
6
|
+
sample: <T>(array: Array<T>) => T;
|
|
7
|
+
sampleWeighted: <const E extends readonly (readonly [any, number])[]>(entries: E) => E[number][0];
|
|
8
|
+
shuffle: <T extends Array<any>>(array: T) => T;
|
|
9
|
+
pointInPolygon: (polygon: Polygon | Hollowgon) => Vec2;
|
|
10
|
+
pointInDisk: (center: Vec2, radius: number) => Vec2;
|
|
11
|
+
pointInAABB: (aabb: AABB) => Vec2;
|
|
12
|
+
reset: () => void;
|
|
13
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Vec2, vec2 } from "../geometry/index.js";
|
|
2
|
+
const { imul, floor, sqrt, PI } = Math;
|
|
3
|
+
const MAGIC_NUMBER = 0x9E3779B9;
|
|
4
|
+
export function hash(value, seed) {
|
|
5
|
+
value = (value + MAGIC_NUMBER * seed) | 0;
|
|
6
|
+
value += 0x7ED55D16;
|
|
7
|
+
value ^= value >>> 12;
|
|
8
|
+
value = imul(value, 0xC761C23C);
|
|
9
|
+
value ^= value >>> 19;
|
|
10
|
+
return value >>> 0;
|
|
11
|
+
}
|
|
12
|
+
export function createRNG(seed) {
|
|
13
|
+
const initialState = typeof seed === "function" ? floor(MAGIC_NUMBER * seed()) : seed;
|
|
14
|
+
let state = initialState;
|
|
15
|
+
function next() {
|
|
16
|
+
state = hash(state, 0);
|
|
17
|
+
return state / 4294967296;
|
|
18
|
+
}
|
|
19
|
+
const rng = (min, max) => {
|
|
20
|
+
if (min === undefined || max === undefined)
|
|
21
|
+
[min, max] = [0, 1];
|
|
22
|
+
return min + (max - min) * next();
|
|
23
|
+
};
|
|
24
|
+
const integer = (lower, upper) => {
|
|
25
|
+
[lower, upper] = upper === undefined ? [0, lower] : [lower, upper];
|
|
26
|
+
return lower + floor((upper - lower + 1) * next());
|
|
27
|
+
};
|
|
28
|
+
const sample = (array) => {
|
|
29
|
+
return array[integer(0, array.length - 1)];
|
|
30
|
+
};
|
|
31
|
+
const sampleWeighted = (entries) => {
|
|
32
|
+
let weightSum = 0;
|
|
33
|
+
for (const entry of entries)
|
|
34
|
+
weightSum += entry[1];
|
|
35
|
+
const value = rng() * weightSum;
|
|
36
|
+
let cumulativeWeight = 0;
|
|
37
|
+
for (const [data, weight] of entries) {
|
|
38
|
+
cumulativeWeight += weight;
|
|
39
|
+
if (value < cumulativeWeight)
|
|
40
|
+
return data;
|
|
41
|
+
}
|
|
42
|
+
return entries[entries.length - 1][0];
|
|
43
|
+
};
|
|
44
|
+
const shuffle = (array) => {
|
|
45
|
+
const n = array.length;
|
|
46
|
+
for (let i = n - 1; i > 0; i--) {
|
|
47
|
+
const j = integer(0, i);
|
|
48
|
+
[array[i], array[j]] = [array[j], array[i]];
|
|
49
|
+
}
|
|
50
|
+
return array;
|
|
51
|
+
};
|
|
52
|
+
const pointInDisk = (center, radius) => {
|
|
53
|
+
return Vec2.fromPolar(radius * sqrt(rng()), rng(0, 2 * PI)).plus(center);
|
|
54
|
+
};
|
|
55
|
+
const pointInPolygon = (polygon) => {
|
|
56
|
+
const { xMin, xMax, yMin, yMax } = polygon.aabb();
|
|
57
|
+
while (true) {
|
|
58
|
+
const point = vec2(rng(xMin, xMax), rng(yMin, yMax));
|
|
59
|
+
if (polygon.isPointInside(point))
|
|
60
|
+
return point;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const pointInAABB = (aabb) => {
|
|
64
|
+
const { xMin, xMax, yMin, yMax } = aabb;
|
|
65
|
+
return vec2(rng(xMin, xMax), rng(yMin, yMax));
|
|
66
|
+
};
|
|
67
|
+
const reset = () => {
|
|
68
|
+
state = initialState;
|
|
69
|
+
};
|
|
70
|
+
return Object.assign(rng, {
|
|
71
|
+
integer,
|
|
72
|
+
sample,
|
|
73
|
+
sampleWeighted,
|
|
74
|
+
shuffle,
|
|
75
|
+
pointInPolygon,
|
|
76
|
+
pointInDisk,
|
|
77
|
+
pointInAABB,
|
|
78
|
+
reset
|
|
79
|
+
});
|
|
80
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@matboks/utilities",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist"
|
|
7
|
+
],
|
|
8
|
+
"author": "",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"description": "",
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"d3-delaunay": "^6.0.4",
|
|
14
|
+
"earcut": "^3.0.2",
|
|
15
|
+
"glsl-modules": "^0.5.0",
|
|
16
|
+
"marching-squares": "^1.0.0",
|
|
17
|
+
"polyclip-ts": "^0.16.8"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/d3-delaunay": "^6.0.4",
|
|
21
|
+
"@types/earcut": "^3.0.0",
|
|
22
|
+
"@types/node": "^24.10.1",
|
|
23
|
+
"typescript": "^5.9.3"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc"
|
|
27
|
+
}
|
|
28
|
+
}
|