@absolutejs/geometry 0.0.0-stage → 0.1.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/LICENSE +88 -0
- package/README.md +97 -2
- package/dist/alpha.d.ts +5 -0
- package/dist/alpha.js +37 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/projection.d.ts +37 -0
- package/dist/projection.js +188 -0
- package/package.json +37 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Business Source License 1.1
|
|
2
|
+
|
|
3
|
+
**Licensor:** Alex Kahn
|
|
4
|
+
|
|
5
|
+
**Licensed Work:** @absolutejs/geometry (https://github.com/absolutejs/geometry)
|
|
6
|
+
|
|
7
|
+
**Change Date:** October 26, 2030
|
|
8
|
+
|
|
9
|
+
**Change License:** Apache License, Version 2.0
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Terms
|
|
14
|
+
|
|
15
|
+
The Licensor hereby grants you the right to copy, modify, create derivative
|
|
16
|
+
works, redistribute, and make non-production use of the Licensed Work. The
|
|
17
|
+
Licensor may make an Additional Use Grant, permitting limited production use.
|
|
18
|
+
|
|
19
|
+
### Additional Use Grant
|
|
20
|
+
|
|
21
|
+
You may use the Licensed Work in production, provided your use does not include
|
|
22
|
+
any of the following:
|
|
23
|
+
|
|
24
|
+
1. **Offering a Competing Service.** You may not offer the Licensed Work, or
|
|
25
|
+
any derivative or substantial portion of it, to third parties as a hosted or
|
|
26
|
+
managed service that competes with a hosted geometry processing or 3D asset analysis service. This includes any
|
|
27
|
+
product whose primary value to its users is the functionality the Licensed
|
|
28
|
+
Work provides.
|
|
29
|
+
|
|
30
|
+
2. **Resale or Redistribution as a Standalone Product.** You may not sell,
|
|
31
|
+
license, or distribute the Licensed Work, or any derivative or fork of it,
|
|
32
|
+
as a standalone commercial product.
|
|
33
|
+
|
|
34
|
+
3. **Removal of Attribution.** Any derivative work, fork, or redistribution of
|
|
35
|
+
the Licensed Work must prominently credit AbsoluteJS and include a link to
|
|
36
|
+
the original project repository (https://github.com/absolutejs/geometry).
|
|
37
|
+
|
|
38
|
+
For clarity, the following uses are expressly permitted:
|
|
39
|
+
|
|
40
|
+
- Using the Licensed Work to build and operate your own applications, websites,
|
|
41
|
+
internal tools, or SaaS products (whether commercial or non-commercial), so
|
|
42
|
+
long as the Licensed Work itself is not the primary product you are selling.
|
|
43
|
+
- Using the Licensed Work as a dependency in commercial software you build and
|
|
44
|
+
sell, as long as the software is not itself a competing managed service of
|
|
45
|
+
the kind described in clause 1.
|
|
46
|
+
- Providing consulting, development, or professional services to clients using
|
|
47
|
+
the Licensed Work.
|
|
48
|
+
- Forking and modifying the Licensed Work for your own internal use, provided
|
|
49
|
+
attribution is maintained.
|
|
50
|
+
|
|
51
|
+
### Change Date and Change License
|
|
52
|
+
|
|
53
|
+
On the Change Date specified above, or on such other date as the Licensor may
|
|
54
|
+
specify by written notice, the Licensed Work will be made available under the
|
|
55
|
+
Change License (Apache License, Version 2.0). Until the Change Date, the terms
|
|
56
|
+
of this Business Source License 1.1 apply.
|
|
57
|
+
|
|
58
|
+
### Trademark
|
|
59
|
+
|
|
60
|
+
This license does not grant you any rights to use the "AbsoluteJS" or
|
|
61
|
+
"@absolutejs" name, logo, or any related trademarks. Forks and derivative works
|
|
62
|
+
must not be named or branded in a manner that suggests endorsement by or
|
|
63
|
+
affiliation with AbsoluteJS or the Licensor.
|
|
64
|
+
|
|
65
|
+
### Notices
|
|
66
|
+
|
|
67
|
+
You must not remove or obscure any licensing, copyright, or other notices
|
|
68
|
+
included in the Licensed Work.
|
|
69
|
+
|
|
70
|
+
### No Warranty
|
|
71
|
+
|
|
72
|
+
THE LICENSED WORK IS PROVIDED "AS IS". THE LICENSOR HEREBY DISCLAIMS ALL
|
|
73
|
+
WARRANTIES, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF
|
|
74
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO
|
|
75
|
+
EVENT SHALL THE LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY,
|
|
76
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR
|
|
77
|
+
IN CONNECTION WITH THE LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE
|
|
78
|
+
LICENSED WORK.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Contact
|
|
83
|
+
|
|
84
|
+
For commercial licensing inquiries or additional permissions, contact:
|
|
85
|
+
|
|
86
|
+
- **Alex Kahn**
|
|
87
|
+
- alexkahndev@gmail.com
|
|
88
|
+
- alexkahndev.github.io
|
package/README.md
CHANGED
|
@@ -1,3 +1,98 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @absolutejs/geometry
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Worker-side measurement of alpha-masked triangle geometry. The first API measures
|
|
4
|
+
orthographic coverage without Three.js, a renderer, or a GPU. This is useful for
|
|
5
|
+
asset inspection, canopy gap measurement, and tools in a 3D website builder.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { projectAlphaCoverage } from '@absolutejs/geometry';
|
|
9
|
+
|
|
10
|
+
const measurement = projectAlphaCoverage({
|
|
11
|
+
positions: new Float64Array([0, 0, 0, 1, 0, 0, 0, 0, 1]),
|
|
12
|
+
uv: new Float32Array([0, 0, 1, 0, 0, 1]),
|
|
13
|
+
indices: new Uint16Array([0, 1, 2]),
|
|
14
|
+
alpha: new Uint8Array([255]),
|
|
15
|
+
textureWidth: 1,
|
|
16
|
+
textureHeight: 1,
|
|
17
|
+
cutoff: 0.4,
|
|
18
|
+
resolution: 256,
|
|
19
|
+
});
|
|
20
|
+
console.log(measurement.footprintArea, measurement.gapFraction);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Coordinate and texture contract
|
|
24
|
+
|
|
25
|
+
Positions are xyz in the caller's projection frame, projected onto xz. Supply
|
|
26
|
+
transformed positions for the actual pose, preferably in local double precision.
|
|
27
|
+
An origin already rounded away in Float32 cannot be recovered. Indexed vertices
|
|
28
|
+
define a convex reference hull; its area includes transparent card extents and
|
|
29
|
+
gaps between disconnected leaves. This is a declared reference footprint, not a
|
|
30
|
+
botanical crown boundary. Units are caller-defined; areas use squared units.
|
|
31
|
+
|
|
32
|
+
UVs must already include any texture transform. Alpha is row-major, one byte per
|
|
33
|
+
texel, with v=0 at the first row: apply any image flip before calling. Bilinear
|
|
34
|
+
sampling uses the base texture level, wrapS/wrapT default to clamp, and alpha at
|
|
35
|
+
or above cutoff is covered. Repeat and mirrored repeat are supported. Faces are
|
|
36
|
+
two-sided; overlapping faces form a union. No color-space conversion applies to
|
|
37
|
+
alpha. Material opacity, vertex alpha, multiple materials and blend materials
|
|
38
|
+
must be resolved by the caller; this API does not silently approximate them.
|
|
39
|
+
|
|
40
|
+
## Results and accuracy
|
|
41
|
+
|
|
42
|
+
`cells` is a resolution-squared mask: 0 outside the reference hull, 1 open,
|
|
43
|
+
2 covered. Rows run from minZ to maxZ; columns from minX to maxX. `bounds` is
|
|
44
|
+
[minX, minZ, maxX, maxZ]. `footprintArea` is the analytic hull area;
|
|
45
|
+
`sampledFootprintArea` and `sampledCoveredArea` expose center-sampling error.
|
|
46
|
+
`coveredArea` normalizes the sampled coverage fraction to the analytic hull.
|
|
47
|
+
`gapFraction` is the open fraction of sampled hull cells.
|
|
48
|
+
|
|
49
|
+
Center sampling can miss thin geometry and small holes. Compare resolutions for
|
|
50
|
+
your asset; convergence is evidence, not a rigorous error bound. The game asset
|
|
51
|
+
audit in docs covers all eight canonical trees plus deformed and fallen examples.
|
|
52
|
+
This does not measure light transmission, rainfall interception, storage capacity,
|
|
53
|
+
wind deformation, mip-filtered screen coverage, or hydrologic history.
|
|
54
|
+
|
|
55
|
+
## Cost and ownership
|
|
56
|
+
|
|
57
|
+
Run this synchronous function in a worker or an offline asset pipeline, never a
|
|
58
|
+
render loop. It does not mutate input buffers. Transfer owned copies when worker
|
|
59
|
+
ownership must change; do not detach a renderer's borrowed geometry buffers.
|
|
60
|
+
|
|
61
|
+
Limits: 300,000 vertices, 200,000 triangles, 4,194,304 texture texels, resolution
|
|
62
|
+
8–1024, finite UV magnitudes at most 1,000,000. Triangle-local bounding rectangles
|
|
63
|
+
are charged against `maxPixelTests` (default 50 million, maximum 100 million)
|
|
64
|
+
before rasterization. The counter is conservative even when union early-outs
|
|
65
|
+
skip already covered cells. Hull construction sorts indexed vertices; scanline
|
|
66
|
+
setup walks convex hull edges. These limits bound work and allocation, not wall
|
|
67
|
+
time. Worker owners should additionally enforce deadlines and cancellation.
|
|
68
|
+
Malformed, unresolved, degenerate and over-budget inputs throw.
|
|
69
|
+
|
|
70
|
+
## Development
|
|
71
|
+
|
|
72
|
+
BVM selects Bun `1.4.4+absolute.1` from `.bun-version`.
|
|
73
|
+
Run `bun install --frozen-lockfile` and `bun run check:package`.
|
|
74
|
+
`bun scripts/audit-game-canopies.ts` additionally requires the Hello World checkout
|
|
75
|
+
and its development asset tooling; see docs/canopy-audit/README.md.
|
|
76
|
+
|
|
77
|
+
Licensed under BSL-1.1; see LICENSE for the additional use grant and change date.
|
|
78
|
+
|
|
79
|
+
## Worker-side alpha preparation (0.1.1)
|
|
80
|
+
|
|
81
|
+
`alphaFromRgba(rgba, width, height)` copies one alpha byte per RGBA8 texel into an
|
|
82
|
+
owned Uint8Array. It accepts Uint8Array or Uint8ClampedArray and validates the
|
|
83
|
+
exact byte length and the same 4,194,304-texel budget before allocation.
|
|
84
|
+
|
|
85
|
+
`alphaFromImageBitmap(bitmap)` reads an already decoded bitmap using a temporary
|
|
86
|
+
OffscreenCanvas 2D context with `willReadFrequently: true`. Call it in a worker;
|
|
87
|
+
it performs synchronous CPU readback wherever it is called. The temporary canvas
|
|
88
|
+
is released afterward. It does not create a WebGL context, close the input bitmap,
|
|
89
|
+
or detach any borrowed renderer resource. The caller owns bitmap cleanup.
|
|
90
|
+
Unsupported runtimes, closed/invalid bitmaps and oversized images fail explicitly.
|
|
91
|
+
There is no hidden main-thread fallback.
|
|
92
|
+
|
|
93
|
+
ImageBitmap supports structured cloning as well as transfer. The browser-worker
|
|
94
|
+
qualification checks exact alpha values and that closing a cloned worker image
|
|
95
|
+
leaves its source usable. Transfer only owned copies if using a transfer list.
|
|
96
|
+
See the [HTML ImageBitmap ownership contract](https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#imagebitmap)
|
|
97
|
+
and [OffscreenCanvas](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas).
|
|
98
|
+
These helpers prepare raster inputs; they do not assign hydrologic meaning to alpha.
|
package/dist/alpha.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Copy one alpha byte per row-major RGBA8 texel. Does not mutate borrowed pixels. */
|
|
2
|
+
export declare function alphaFromRgba(rgba: Uint8Array | Uint8ClampedArray, width: number, height: number): Uint8Array;
|
|
3
|
+
/** Worker/setup-time CPU readback. The caller owns and closes the bitmap.
|
|
4
|
+
* This never closes/detaches the caller's image and never creates a WebGL context. */
|
|
5
|
+
export declare function alphaFromImageBitmap(bitmap: ImageBitmap): Uint8Array;
|
package/dist/alpha.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const MAX_TEXELS = 4_194_304;
|
|
2
|
+
function validateSize(width, height) {
|
|
3
|
+
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width < 1 || height < 1 || width * height > MAX_TEXELS)
|
|
4
|
+
throw Error('Invalid or oversized alpha source');
|
|
5
|
+
}
|
|
6
|
+
/** Copy one alpha byte per row-major RGBA8 texel. Does not mutate borrowed pixels. */
|
|
7
|
+
export function alphaFromRgba(rgba, width, height) {
|
|
8
|
+
validateSize(width, height);
|
|
9
|
+
if (!(rgba instanceof Uint8Array || rgba instanceof Uint8ClampedArray) || rgba.length !== width * height * 4)
|
|
10
|
+
throw Error('Expected tightly packed RGBA8 alpha source');
|
|
11
|
+
const alpha = new Uint8Array(width * height);
|
|
12
|
+
for (let i = 0; i < alpha.length; i++)
|
|
13
|
+
alpha[i] = rgba[i * 4 + 3];
|
|
14
|
+
return alpha;
|
|
15
|
+
}
|
|
16
|
+
/** Worker/setup-time CPU readback. The caller owns and closes the bitmap.
|
|
17
|
+
* This never closes/detaches the caller's image and never creates a WebGL context. */
|
|
18
|
+
export function alphaFromImageBitmap(bitmap) {
|
|
19
|
+
if (typeof ImageBitmap === 'undefined' || !(bitmap instanceof ImageBitmap))
|
|
20
|
+
throw Error('Expected an available ImageBitmap');
|
|
21
|
+
validateSize(bitmap.width, bitmap.height);
|
|
22
|
+
if (typeof OffscreenCanvas === 'undefined')
|
|
23
|
+
throw Error('OffscreenCanvas is unavailable for alpha readback');
|
|
24
|
+
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
25
|
+
try {
|
|
26
|
+
const context = canvas.getContext('2d', { willReadFrequently: true });
|
|
27
|
+
if (!context)
|
|
28
|
+
throw Error('Canvas2D is unavailable for alpha readback');
|
|
29
|
+
context.drawImage(bitmap, 0, 0);
|
|
30
|
+
const pixels = context.getImageData(0, 0, bitmap.width, bitmap.height);
|
|
31
|
+
return alphaFromRgba(pixels.data, pixels.width, pixels.height);
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
canvas.width = 0;
|
|
35
|
+
canvas.height = 0;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type AlphaProjectionInput = {
|
|
2
|
+
/** xyz coordinates in the requested projection frame; projection is onto xz. */
|
|
3
|
+
positions: Float32Array | Float64Array;
|
|
4
|
+
uv: Float32Array | Float64Array;
|
|
5
|
+
indices: Uint16Array | Uint32Array;
|
|
6
|
+
alpha: Uint8Array;
|
|
7
|
+
textureWidth: number;
|
|
8
|
+
textureHeight: number;
|
|
9
|
+
cutoff: number;
|
|
10
|
+
wrapS?: "clamp" | "repeat" | "mirror";
|
|
11
|
+
wrapT?: "clamp" | "repeat" | "mirror";
|
|
12
|
+
resolution?: number;
|
|
13
|
+
maxPixelTests?: number;
|
|
14
|
+
};
|
|
15
|
+
export type AlphaProjection = {
|
|
16
|
+
revision: "alpha-projection-1";
|
|
17
|
+
resolution: number;
|
|
18
|
+
/** 0 outside projected vertex hull, 1 open ray, 2 alpha-covered ray. */
|
|
19
|
+
cells: Uint8Array;
|
|
20
|
+
bounds: [number, number, number, number];
|
|
21
|
+
footprintArea: number;
|
|
22
|
+
sampledFootprintArea: number;
|
|
23
|
+
sampledCoveredArea: number;
|
|
24
|
+
coveredArea: number;
|
|
25
|
+
gapFraction: number;
|
|
26
|
+
coveredCells: number;
|
|
27
|
+
footprintCells: number;
|
|
28
|
+
pixelTests: number;
|
|
29
|
+
alphaTests: number;
|
|
30
|
+
triangles: number;
|
|
31
|
+
cellArea: number;
|
|
32
|
+
};
|
|
33
|
+
/** Deterministic setup/worker-time orthographic coverage. Triangle-local raster
|
|
34
|
+
* bounds and union early-outs avoid a ray/triangle cartesian product. The convex
|
|
35
|
+
* projected vertex hull defines the explicit reference area. Center sampling is
|
|
36
|
+
* approximate; qualify resolution convergence. No water, transmission or wind. */
|
|
37
|
+
export declare function projectAlphaCoverage(input: AlphaProjectionInput): AlphaProjection;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/** Deterministic setup/worker-time orthographic coverage. Triangle-local raster
|
|
2
|
+
* bounds and union early-outs avoid a ray/triangle cartesian product. The convex
|
|
3
|
+
* projected vertex hull defines the explicit reference area. Center sampling is
|
|
4
|
+
* approximate; qualify resolution convergence. No water, transmission or wind. */
|
|
5
|
+
export function projectAlphaCoverage(input) {
|
|
6
|
+
const { positions: p, uv, indices, alpha, textureWidth: w, textureHeight: h, cutoff, } = input;
|
|
7
|
+
const n = input.resolution ?? 256, limit = input.maxPixelTests ?? 50000000;
|
|
8
|
+
const wrapS = input.wrapS ?? "clamp", wrapT = input.wrapT ?? "clamp";
|
|
9
|
+
if (!(p instanceof Float32Array || p instanceof Float64Array) ||
|
|
10
|
+
!(uv instanceof Float32Array || uv instanceof Float64Array) ||
|
|
11
|
+
!(indices instanceof Uint16Array || indices instanceof Uint32Array) ||
|
|
12
|
+
!(alpha instanceof Uint8Array) ||
|
|
13
|
+
p.length % 3 ||
|
|
14
|
+
p.length < 9 ||
|
|
15
|
+
p.length > 900000 ||
|
|
16
|
+
uv.length !== (p.length / 3) * 2 ||
|
|
17
|
+
indices.length % 3 ||
|
|
18
|
+
indices.length < 3 ||
|
|
19
|
+
indices.length > 600000 ||
|
|
20
|
+
!Number.isInteger(w) ||
|
|
21
|
+
!Number.isInteger(h) ||
|
|
22
|
+
w < 1 ||
|
|
23
|
+
h < 1 ||
|
|
24
|
+
w * h > 4194304 ||
|
|
25
|
+
alpha.length !== w * h ||
|
|
26
|
+
!Number.isFinite(cutoff) ||
|
|
27
|
+
cutoff <= 0 ||
|
|
28
|
+
cutoff >= 1 ||
|
|
29
|
+
!Number.isInteger(n) ||
|
|
30
|
+
n < 8 ||
|
|
31
|
+
n > 1024 ||
|
|
32
|
+
!Number.isSafeInteger(limit) ||
|
|
33
|
+
limit < 1 ||
|
|
34
|
+
limit > 100000000 ||
|
|
35
|
+
!["clamp", "repeat", "mirror"].includes(wrapS) ||
|
|
36
|
+
!["clamp", "repeat", "mirror"].includes(wrapT))
|
|
37
|
+
throw Error("Invalid alpha projection input");
|
|
38
|
+
const count = p.length / 3;
|
|
39
|
+
let minX = Infinity, minZ = Infinity, maxX = -Infinity, maxZ = -Infinity;
|
|
40
|
+
const used = new Uint8Array(count);
|
|
41
|
+
for (const id of indices) {
|
|
42
|
+
if (id >= count)
|
|
43
|
+
throw Error("Invalid projection index");
|
|
44
|
+
used[id] = 1;
|
|
45
|
+
}
|
|
46
|
+
for (let i = 0; i < count; i++) {
|
|
47
|
+
if (![p[i * 3], p[i * 3 + 1], p[i * 3 + 2], uv[i * 2], uv[i * 2 + 1]].every(Number.isFinite) ||
|
|
48
|
+
Math.abs(uv[i * 2]) > 1e6 ||
|
|
49
|
+
Math.abs(uv[i * 2 + 1]) > 1e6)
|
|
50
|
+
throw Error("Nonfinite or unbounded projection vertex");
|
|
51
|
+
if (!used[i])
|
|
52
|
+
continue;
|
|
53
|
+
minX = Math.min(minX, p[i * 3]);
|
|
54
|
+
maxX = Math.max(maxX, p[i * 3]);
|
|
55
|
+
minZ = Math.min(minZ, p[i * 3 + 2]);
|
|
56
|
+
maxZ = Math.max(maxZ, p[i * 3 + 2]);
|
|
57
|
+
}
|
|
58
|
+
const spanX = maxX - minX, spanZ = maxZ - minZ;
|
|
59
|
+
if (!Number.isFinite(spanX * spanZ) || spanX <= 0 || spanZ <= 0)
|
|
60
|
+
throw Error("Degenerate projection footprint");
|
|
61
|
+
const coordinates = new Float64Array(count * 2), points = [];
|
|
62
|
+
for (let i = 0; i < count; i++) {
|
|
63
|
+
coordinates[i * 2] = p[i * 3] - minX;
|
|
64
|
+
coordinates[i * 2 + 1] = p[i * 3 + 2] - minZ;
|
|
65
|
+
if (used[i])
|
|
66
|
+
points.push(i);
|
|
67
|
+
}
|
|
68
|
+
points.sort((a, b) => coordinates[a * 2] - coordinates[b * 2] ||
|
|
69
|
+
coordinates[a * 2 + 1] - coordinates[b * 2 + 1]);
|
|
70
|
+
const cross = (a, b, c) => (coordinates[b * 2] - coordinates[a * 2]) *
|
|
71
|
+
(coordinates[c * 2 + 1] - coordinates[a * 2 + 1]) -
|
|
72
|
+
(coordinates[b * 2 + 1] - coordinates[a * 2 + 1]) *
|
|
73
|
+
(coordinates[c * 2] - coordinates[a * 2]);
|
|
74
|
+
const lower = [], upper = [];
|
|
75
|
+
for (const id of points) {
|
|
76
|
+
while (lower.length > 1 && cross(lower.at(-2), lower.at(-1), id) <= 0)
|
|
77
|
+
lower.pop();
|
|
78
|
+
lower.push(id);
|
|
79
|
+
}
|
|
80
|
+
for (let i = points.length - 1; i >= 0; i--) {
|
|
81
|
+
const id = points[i];
|
|
82
|
+
while (upper.length > 1 && cross(upper.at(-2), upper.at(-1), id) <= 0)
|
|
83
|
+
upper.pop();
|
|
84
|
+
upper.push(id);
|
|
85
|
+
}
|
|
86
|
+
const hull = lower.slice(0, -1).concat(upper.slice(0, -1));
|
|
87
|
+
let area = 0;
|
|
88
|
+
for (let i = 0; i < hull.length; i++) {
|
|
89
|
+
const a = hull[i], b = hull[(i + 1) % hull.length];
|
|
90
|
+
area +=
|
|
91
|
+
coordinates[a * 2] * coordinates[b * 2 + 1] -
|
|
92
|
+
coordinates[b * 2] * coordinates[a * 2 + 1];
|
|
93
|
+
}
|
|
94
|
+
area = Math.abs(area) / 2;
|
|
95
|
+
if (!Number.isFinite(area) || !(area > 0))
|
|
96
|
+
throw Error("Degenerate projected vertex hull");
|
|
97
|
+
const dx = spanX / n, dz = spanZ / n, cells = new Uint8Array(n * n);
|
|
98
|
+
let footprintCells = 0;
|
|
99
|
+
// Walk hull edges once. A convex hull has at most two crossings per row,
|
|
100
|
+
// avoiding resolution × hull-vertex work on highly detailed assets.
|
|
101
|
+
const lefts = new Float64Array(n).fill(Infinity);
|
|
102
|
+
const rights = new Float64Array(n).fill(-Infinity);
|
|
103
|
+
for (let i = 0; i < hull.length; i++) {
|
|
104
|
+
const a = hull[i], b = hull[(i + 1) % hull.length];
|
|
105
|
+
const az = coordinates[a * 2 + 1], bz = coordinates[b * 2 + 1];
|
|
106
|
+
if (az === bz)
|
|
107
|
+
continue;
|
|
108
|
+
const firstRow = Math.max(0, Math.ceil(Math.min(az, bz) / dz - 0.5));
|
|
109
|
+
const endRow = Math.min(n, Math.ceil(Math.max(az, bz) / dz - 0.5));
|
|
110
|
+
for (let row = firstRow; row < endRow; row++) {
|
|
111
|
+
const z = (row + 0.5) * dz;
|
|
112
|
+
const x = coordinates[a * 2] +
|
|
113
|
+
((z - az) / (bz - az)) * (coordinates[b * 2] - coordinates[a * 2]);
|
|
114
|
+
lefts[row] = Math.min(lefts[row], x);
|
|
115
|
+
rights[row] = Math.max(rights[row], x);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (let row = 0; row < n; row++) {
|
|
119
|
+
const left = lefts[row], right = rights[row];
|
|
120
|
+
const first = Math.max(0, Math.ceil(left / dx - 0.5)), last = Math.min(n - 1, Math.floor(right / dx - 0.5));
|
|
121
|
+
for (let col = first; col <= last; col++) {
|
|
122
|
+
cells[row * n + col] = 1;
|
|
123
|
+
footprintCells++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (!footprintCells)
|
|
127
|
+
throw Error("Projection footprint is unresolved");
|
|
128
|
+
const wrap = (index, size, mode) => {
|
|
129
|
+
if (mode === "clamp")
|
|
130
|
+
return Math.max(0, Math.min(size - 1, index));
|
|
131
|
+
const period = mode === "mirror" ? size * 2 : size, t = ((index % period) + period) % period;
|
|
132
|
+
return t < size ? t : period - 1 - t;
|
|
133
|
+
};
|
|
134
|
+
const sample = (u, v) => {
|
|
135
|
+
const x = u * w - 0.5, y = v * h - 0.5, ix = Math.floor(x), iy = Math.floor(y), fx = x - ix, fy = y - iy;
|
|
136
|
+
const x0 = wrap(ix, w, wrapS), x1 = wrap(ix + 1, w, wrapS), y0 = wrap(iy, h, wrapT), y1 = wrap(iy + 1, h, wrapT);
|
|
137
|
+
return ((alpha[y0 * w + x0] * (1 - fx) + alpha[y0 * w + x1] * fx) * (1 - fy) +
|
|
138
|
+
(alpha[y1 * w + x0] * (1 - fx) + alpha[y1 * w + x1] * fx) * fy);
|
|
139
|
+
};
|
|
140
|
+
let pixelTests = 0, alphaTests = 0, coveredCells = 0;
|
|
141
|
+
for (let t = 0; t < indices.length; t += 3) {
|
|
142
|
+
const a = indices[t], b = indices[t + 1], c = indices[t + 2];
|
|
143
|
+
const ax = coordinates[a * 2], az = coordinates[a * 2 + 1], bx = coordinates[b * 2], bz = coordinates[b * 2 + 1], cx = coordinates[c * 2], cz = coordinates[c * 2 + 1];
|
|
144
|
+
const den = (bx - ax) * (cz - az) - (bz - az) * (cx - ax);
|
|
145
|
+
if (Math.abs(den) <= Number.EPSILON * (spanX * spanZ))
|
|
146
|
+
continue;
|
|
147
|
+
const firstX = Math.max(0, Math.ceil(Math.min(ax, bx, cx) / dx - 0.5)), lastX = Math.min(n - 1, Math.floor(Math.max(ax, bx, cx) / dx - 0.5));
|
|
148
|
+
const firstZ = Math.max(0, Math.ceil(Math.min(az, bz, cz) / dz - 0.5)), lastZ = Math.min(n - 1, Math.floor(Math.max(az, bz, cz) / dz - 0.5));
|
|
149
|
+
const work = Math.max(0, lastX - firstX + 1) * Math.max(0, lastZ - firstZ + 1);
|
|
150
|
+
if (pixelTests + work > limit)
|
|
151
|
+
throw Error("Alpha projection pixel-test budget exceeded");
|
|
152
|
+
pixelTests += work;
|
|
153
|
+
for (let row = firstZ; row <= lastZ; row++)
|
|
154
|
+
for (let col = firstX; col <= lastX; col++) {
|
|
155
|
+
const cell = row * n + col;
|
|
156
|
+
if (cells[cell] !== 1)
|
|
157
|
+
continue;
|
|
158
|
+
const rx = (col + 0.5) * dx - ax, rz = (row + 0.5) * dz - az;
|
|
159
|
+
const b1 = (rx * (cz - az) - rz * (cx - ax)) / den, b2 = ((bx - ax) * rz - (bz - az) * rx) / den, b0 = 1 - b1 - b2;
|
|
160
|
+
if (b0 < -1e-10 || b1 < -1e-10 || b2 < -1e-10)
|
|
161
|
+
continue;
|
|
162
|
+
alphaTests++;
|
|
163
|
+
if (sample(uv[a * 2] * b0 + uv[b * 2] * b1 + uv[c * 2] * b2, uv[a * 2 + 1] * b0 + uv[b * 2 + 1] * b1 + uv[c * 2 + 1] * b2) >=
|
|
164
|
+
cutoff * 255) {
|
|
165
|
+
cells[cell] = 2;
|
|
166
|
+
coveredCells++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const cellArea = dx * dz;
|
|
171
|
+
return {
|
|
172
|
+
revision: "alpha-projection-1",
|
|
173
|
+
resolution: n,
|
|
174
|
+
cells,
|
|
175
|
+
bounds: [minX, minZ, maxX, maxZ],
|
|
176
|
+
footprintArea: area,
|
|
177
|
+
sampledFootprintArea: footprintCells * cellArea,
|
|
178
|
+
sampledCoveredArea: coveredCells * cellArea,
|
|
179
|
+
coveredArea: (area * coveredCells) / footprintCells,
|
|
180
|
+
gapFraction: 1 - coveredCells / footprintCells,
|
|
181
|
+
coveredCells,
|
|
182
|
+
footprintCells,
|
|
183
|
+
pixelTests,
|
|
184
|
+
alphaTests,
|
|
185
|
+
triangles: indices.length / 3,
|
|
186
|
+
cellArea,
|
|
187
|
+
};
|
|
188
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@absolutejs/geometry",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Bounded alpha-aware geometry projection for worker-side asset measurement and web 3D tooling.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"license": "BSL-1.1",
|
|
22
|
+
"author": "Alex Kahn",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/absolutejs/geometry.git"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -p tsconfig.json",
|
|
32
|
+
"test": "bun test ./tests",
|
|
33
|
+
"check:package": "bun run build && bun test ./tests"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"typescript": "5.9.3",
|
|
37
|
+
"@types/bun": "1.4.2"
|
|
38
|
+
}
|
|
39
|
+
}
|