koota-kit 0.7.1 → 0.8.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/CHANGELOG.md +10 -0
- package/README.md +4 -0
- package/dist/cjs/index.cjs +3 -1
- package/dist/cjs/index.d.cts +2 -0
- package/dist/cjs/index.d.cts.map +1 -1
- package/dist/cjs/spatial.cjs +371 -0
- package/dist/cjs/spatial.d.cts +130 -0
- package/dist/cjs/spatial.d.cts.map +1 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/spatial.d.ts +130 -0
- package/dist/esm/spatial.d.ts.map +1 -0
- package/dist/esm/spatial.js +368 -0
- package/docs/API.md +39 -0
- package/docs/ARCHITECTURE.md +24 -2
- package/package.json +22 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0 (2026-10-14)
|
|
4
|
+
|
|
5
|
+
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
|
6
|
+
|
|
7
|
+
## What's Changed
|
|
8
|
+
* feat(spatial): add renderer-neutral clearance queries by @jbdevprimary in https://github.com/jbcom/koota-kit/pull/55
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
**Full Changelog**: https://github.com/jbcom/koota-kit/compare/v0.7.1...v0.8.0
|
|
12
|
+
|
|
3
13
|
## 0.7.1 (2026-10-14)
|
|
4
14
|
|
|
5
15
|
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
package/README.md
CHANGED
|
@@ -265,6 +265,7 @@ are also available:
|
|
|
265
265
|
| `koota-kit/traits` | Safe trait declaration |
|
|
266
266
|
| `koota-kit/schedule` | Fixed-step system schedule with a step cap |
|
|
267
267
|
| `koota-kit/eventLog` | World-scoped publish/drain logs |
|
|
268
|
+
| `koota-kit/spatial` | Renderer-neutral point-to-open-surface clearance queries |
|
|
268
269
|
| `koota-kit/react` | `SimWorldProvider` and `useSimWorld` (optional React peer) |
|
|
269
270
|
|
|
270
271
|
See the [API reference](./docs/API.md) for signatures and validation behavior,
|
|
@@ -285,6 +286,9 @@ For task-oriented usage, read the Sourcey guides on
|
|
|
285
286
|
- `advanceClock` accepts finite, non-negative `dt`; `0` is a valid paused tick.
|
|
286
287
|
- `createSchedule` needs a finite `step > 0` and a safe-integer `maxSteps >= 1`;
|
|
287
288
|
a negative, `NaN` or infinite `frameDt` counts as `0`.
|
|
289
|
+
- `queryPointClearance` accepts only finite coordinates and a shared Euclidean
|
|
290
|
+
frame/unit. It proves distance to declared open surfaces, never containment
|
|
291
|
+
or solid nonpenetration.
|
|
288
292
|
- `nextInt` requires safe-integer bounds with `maxExclusive > minInclusive` and
|
|
289
293
|
a range no larger than `2**32`.
|
|
290
294
|
- `chance` accepts only finite probabilities in `[0, 1]`.
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// ./eventLog — scratch-backed publish/drain event logs with a peek seam for
|
|
14
14
|
// observers (harnesses, tests, HUDs)
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
-
exports.CoordinationRegistration = exports.CoordinationBinding = exports.bindCoordination = exports.defineTrait = exports.defineEventLog = exports.createSchedule = exports.MASTER_SEED_BYTES = exports.isMasterSeed = exports.deriveSeed = exports.createMasterSeed = exports.substream = exports.snapshotSubstreams = exports.snapshotStream = exports.snapshotLayers = exports.shuffle = exports.restoreSubstreams = exports.restoreStream = exports.restoreLayers = exports.pick = exports.nextU32 = exports.nextInt = exports.nextFloat = exports.getSubstream = exports.createSubstreams = exports.createRng = exports.chance = exports.trait = exports.snapshotWorld = exports.restoreWorldHeader = exports.relation = exports.destroySimWorld = exports.createSimWorld = exports.createActions = exports.advanceClock = void 0;
|
|
16
|
+
exports.CoordinationRegistration = exports.CoordinationBinding = exports.bindCoordination = exports.defineTrait = exports.queryPointClearance = exports.defineEventLog = exports.createSchedule = exports.MASTER_SEED_BYTES = exports.isMasterSeed = exports.deriveSeed = exports.createMasterSeed = exports.substream = exports.snapshotSubstreams = exports.snapshotStream = exports.snapshotLayers = exports.shuffle = exports.restoreSubstreams = exports.restoreStream = exports.restoreLayers = exports.pick = exports.nextU32 = exports.nextInt = exports.nextFloat = exports.getSubstream = exports.createSubstreams = exports.createRng = exports.chance = exports.trait = exports.snapshotWorld = exports.restoreWorldHeader = exports.relation = exports.destroySimWorld = exports.createSimWorld = exports.createActions = exports.advanceClock = void 0;
|
|
17
17
|
var world_js_1 = require("./world.cjs");
|
|
18
18
|
Object.defineProperty(exports, "advanceClock", { enumerable: true, get: function () { return world_js_1.advanceClock; } });
|
|
19
19
|
Object.defineProperty(exports, "createActions", { enumerable: true, get: function () { return world_js_1.createActions; } });
|
|
@@ -49,6 +49,8 @@ var schedule_js_1 = require("./schedule.cjs");
|
|
|
49
49
|
Object.defineProperty(exports, "createSchedule", { enumerable: true, get: function () { return schedule_js_1.createSchedule; } });
|
|
50
50
|
var eventLog_js_1 = require("./eventLog.cjs");
|
|
51
51
|
Object.defineProperty(exports, "defineEventLog", { enumerable: true, get: function () { return eventLog_js_1.defineEventLog; } });
|
|
52
|
+
var spatial_js_1 = require("./spatial.cjs");
|
|
53
|
+
Object.defineProperty(exports, "queryPointClearance", { enumerable: true, get: function () { return spatial_js_1.queryPointClearance; } });
|
|
52
54
|
var index_js_1 = require("./traits/index.cjs");
|
|
53
55
|
Object.defineProperty(exports, "defineTrait", { enumerable: true, get: function () { return index_js_1.defineTrait; } });
|
|
54
56
|
var coordination_js_1 = require("./coordination.cjs");
|
package/dist/cjs/index.d.cts
CHANGED
|
@@ -8,6 +8,8 @@ export { createSchedule } from "./schedule.cjs";
|
|
|
8
8
|
export type { Schedule, ScheduleOptions, System } from "./schedule.cjs";
|
|
9
9
|
export { defineEventLog } from "./eventLog.cjs";
|
|
10
10
|
export type { EventLog } from "./eventLog.cjs";
|
|
11
|
+
export { queryPointClearance } from "./spatial.cjs";
|
|
12
|
+
export type { SpatialAabb, SpatialClearanceQuery, SpatialClearanceResult, SpatialFrame, SpatialIdentity, SpatialNearestSurfaceOracle, SpatialNearestWitness, SpatialOracleReply, SpatialPartRecord, SpatialPoint, SpatialProvider, SpatialQueryExactness, SpatialQueryReason, SpatialQueryStatus, SpatialRecordStamp, SpatialRegistrationIdentity, SpatialRevision, SpatialTopology, SpatialTopologySelection, } from "./spatial.cjs";
|
|
11
13
|
export { defineTrait } from "./traits/index.cjs";
|
|
12
14
|
export type { SafeSchema } from "./traits/index.cjs";
|
|
13
15
|
export { bindCoordination, CoordinationBinding, CoordinationRegistration, } from "./coordination.cjs";
|
package/dist/cjs/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,KAAK,GACN,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,MAAM,EACN,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,OAAO,EACP,OAAO,EACP,IAAI,EACJ,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,SAAS,GACV,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC1F,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvE,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,EACZ,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,KAAK,GACN,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,MAAM,EACN,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,OAAO,EACP,OAAO,EACP,IAAI,EACJ,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,SAAS,GACV,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC1F,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvE,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EACV,WAAW,EACX,qBAAqB,EACrB,sBAAsB,EACtB,YAAY,EACZ,eAAe,EACf,2BAA2B,EAC3B,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,2BAA2B,EAC3B,eAAe,EACf,eAAe,EACf,wBAAwB,GACzB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,EACZ,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Renderer-neutral point-to-surface clearance queries.
|
|
4
|
+
*
|
|
5
|
+
* Producers own their buffers, transforms, and exact geometry implementations.
|
|
6
|
+
* This module only validates a completed provider snapshot, filters certified
|
|
7
|
+
* AABBs, and calls an injected oracle in one declared common metric.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.queryPointClearance = queryPointClearance;
|
|
11
|
+
const finitePoint = (value, label) => {
|
|
12
|
+
if (typeof value !== "object" || value === null)
|
|
13
|
+
throw new TypeError(`spatial: ${label} must be a point.`);
|
|
14
|
+
const point = value;
|
|
15
|
+
for (const axis of ["x", "y", "z"]) {
|
|
16
|
+
if (!Number.isFinite(point[axis]))
|
|
17
|
+
throw new RangeError(`spatial: ${label}.${axis} must be finite.`);
|
|
18
|
+
}
|
|
19
|
+
return Object.freeze({ x: point.x, y: point.y, z: point.z });
|
|
20
|
+
};
|
|
21
|
+
const nonEmpty = (value, label) => {
|
|
22
|
+
if (typeof value !== "string" || value.length === 0)
|
|
23
|
+
throw new TypeError(`spatial: ${label} must be a non-empty string.`);
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
const nonNegativeRevision = (value, label) => {
|
|
27
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
28
|
+
throw new RangeError(`spatial: ${label} must be a non-negative safe integer.`);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
};
|
|
32
|
+
function copyFrame(value, label) {
|
|
33
|
+
if (typeof value !== "object" || value === null)
|
|
34
|
+
throw new TypeError(`spatial: ${label} must be a frame.`);
|
|
35
|
+
const frame = value;
|
|
36
|
+
if (frame.metric !== "euclidean")
|
|
37
|
+
throw new TypeError(`spatial: ${label}.metric must be euclidean.`);
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
id: nonEmpty(frame.id, `${label}.id`),
|
|
40
|
+
unit: nonEmpty(frame.unit, `${label}.unit`),
|
|
41
|
+
metric: "euclidean",
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
function sameFrame(left, right) {
|
|
45
|
+
return left.id === right.id && left.unit === right.unit && left.metric === right.metric;
|
|
46
|
+
}
|
|
47
|
+
function copyIdentity(value, label) {
|
|
48
|
+
if (typeof value !== "object" || value === null)
|
|
49
|
+
throw new TypeError(`spatial: ${label} must be an identity.`);
|
|
50
|
+
const identity = value;
|
|
51
|
+
if (typeof identity.registration !== "object" || identity.registration === null) {
|
|
52
|
+
throw new TypeError(`spatial: ${label}.registration must be an identity.`);
|
|
53
|
+
}
|
|
54
|
+
return Object.freeze({
|
|
55
|
+
sourceId: nonEmpty(identity.sourceId, `${label}.sourceId`),
|
|
56
|
+
instanceId: nonEmpty(identity.instanceId, `${label}.instanceId`),
|
|
57
|
+
partId: nonEmpty(identity.partId, `${label}.partId`),
|
|
58
|
+
registration: Object.freeze({
|
|
59
|
+
id: nonEmpty(identity.registration.id, `${label}.registration.id`),
|
|
60
|
+
generation: nonNegativeRevision(identity.registration.generation, `${label}.registration.generation`),
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function sameIdentity(left, right) {
|
|
65
|
+
return (left.sourceId === right.sourceId &&
|
|
66
|
+
left.instanceId === right.instanceId &&
|
|
67
|
+
left.partId === right.partId &&
|
|
68
|
+
left.registration.id === right.registration.id &&
|
|
69
|
+
left.registration.generation === right.registration.generation);
|
|
70
|
+
}
|
|
71
|
+
function identityKey(identity) {
|
|
72
|
+
return JSON.stringify([
|
|
73
|
+
identity.sourceId,
|
|
74
|
+
identity.instanceId,
|
|
75
|
+
identity.partId,
|
|
76
|
+
identity.registration.id,
|
|
77
|
+
identity.registration.generation,
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
function compareIdentity(left, right) {
|
|
81
|
+
const leftKey = identityKey(left);
|
|
82
|
+
const rightKey = identityKey(right);
|
|
83
|
+
return Number(leftKey > rightKey) - Number(leftKey < rightKey);
|
|
84
|
+
}
|
|
85
|
+
function copyRevisions(value, label) {
|
|
86
|
+
if (typeof value !== "object" || value === null)
|
|
87
|
+
throw new TypeError(`spatial: ${label} must be revisions.`);
|
|
88
|
+
const revisions = value;
|
|
89
|
+
return Object.freeze({
|
|
90
|
+
geometry: nonNegativeRevision(revisions.geometry, `${label}.geometry`),
|
|
91
|
+
topology: nonNegativeRevision(revisions.topology, `${label}.topology`),
|
|
92
|
+
pose: nonNegativeRevision(revisions.pose, `${label}.pose`),
|
|
93
|
+
transform: nonNegativeRevision(revisions.transform, `${label}.transform`),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
function sameRevisions(left, right) {
|
|
97
|
+
return (left.geometry === right.geometry &&
|
|
98
|
+
left.topology === right.topology &&
|
|
99
|
+
left.pose === right.pose &&
|
|
100
|
+
left.transform === right.transform);
|
|
101
|
+
}
|
|
102
|
+
function copyBounds(value, label) {
|
|
103
|
+
if (typeof value !== "object" || value === null)
|
|
104
|
+
throw new TypeError(`spatial: ${label} must be an AABB.`);
|
|
105
|
+
const bounds = value;
|
|
106
|
+
const min = finitePoint(bounds.min, `${label}.min`);
|
|
107
|
+
const max = finitePoint(bounds.max, `${label}.max`);
|
|
108
|
+
for (const axis of ["x", "y", "z"]) {
|
|
109
|
+
if (min[axis] > max[axis])
|
|
110
|
+
throw new RangeError(`spatial: ${label}.min.${axis} must not exceed max.${axis}.`);
|
|
111
|
+
}
|
|
112
|
+
return Object.freeze({ min, max });
|
|
113
|
+
}
|
|
114
|
+
function copyTopology(value, label) {
|
|
115
|
+
if (typeof value !== "object" || value === null)
|
|
116
|
+
throw new TypeError(`spatial: ${label} must be topology.`);
|
|
117
|
+
const topology = value;
|
|
118
|
+
if (topology.enclosesSurface !== true)
|
|
119
|
+
throw new TypeError(`spatial: ${label}.enclosesSurface must certify true.`);
|
|
120
|
+
return Object.freeze({
|
|
121
|
+
id: nonEmpty(topology.id, `${label}.id`),
|
|
122
|
+
bounds: copyBounds(topology.bounds, `${label}.bounds`),
|
|
123
|
+
enclosesSurface: true,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function copyRecord(value, label) {
|
|
127
|
+
if (typeof value !== "object" || value === null)
|
|
128
|
+
throw new TypeError(`spatial: ${label} must be a part record.`);
|
|
129
|
+
const record = value;
|
|
130
|
+
if (typeof record.topology !== "object" || record.topology === null)
|
|
131
|
+
throw new TypeError(`spatial: ${label}.topology must be an object.`);
|
|
132
|
+
if (!Array.isArray(record.capabilities))
|
|
133
|
+
throw new TypeError(`spatial: ${label}.capabilities must be an array.`);
|
|
134
|
+
const capabilities = Array.from(record.capabilities);
|
|
135
|
+
if (capabilities.some((capability) => capability !== "nearest-surface") ||
|
|
136
|
+
new Set(capabilities).size !== capabilities.length) {
|
|
137
|
+
throw new TypeError(`spatial: ${label}.capabilities may only contain nearest-surface.`);
|
|
138
|
+
}
|
|
139
|
+
return Object.freeze({
|
|
140
|
+
identity: copyIdentity(record.identity, `${label}.identity`),
|
|
141
|
+
frame: copyFrame(record.frame, `${label}.frame`),
|
|
142
|
+
revisions: copyRevisions(record.revisions, `${label}.revisions`),
|
|
143
|
+
topology: Object.freeze({
|
|
144
|
+
full: copyTopology(record.topology.full, `${label}.topology.full`),
|
|
145
|
+
visible: copyTopology(record.topology.visible, `${label}.topology.visible`),
|
|
146
|
+
}),
|
|
147
|
+
capabilities: Object.freeze(capabilities),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
function makeStamp(record, selection) {
|
|
151
|
+
return Object.freeze({
|
|
152
|
+
identity: record.identity,
|
|
153
|
+
frame: record.frame,
|
|
154
|
+
topology: Object.freeze({ selection, id: record.topology[selection].id }),
|
|
155
|
+
revisions: record.revisions,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function sameStamp(left, right) {
|
|
159
|
+
return (sameIdentity(left.identity, right.identity) &&
|
|
160
|
+
sameFrame(left.frame, right.frame) &&
|
|
161
|
+
left.topology.selection === right.topology.selection &&
|
|
162
|
+
left.topology.id === right.topology.id &&
|
|
163
|
+
sameRevisions(left.revisions, right.revisions));
|
|
164
|
+
}
|
|
165
|
+
function copyStamp(value, label) {
|
|
166
|
+
if (typeof value !== "object" || value === null)
|
|
167
|
+
throw new TypeError(`spatial: ${label} must be a record stamp.`);
|
|
168
|
+
const stamp = value;
|
|
169
|
+
if (typeof stamp.topology !== "object" ||
|
|
170
|
+
stamp.topology === null ||
|
|
171
|
+
(stamp.topology.selection !== "full" && stamp.topology.selection !== "visible")) {
|
|
172
|
+
throw new TypeError(`spatial: ${label}.topology.selection must be full or visible.`);
|
|
173
|
+
}
|
|
174
|
+
return Object.freeze({
|
|
175
|
+
identity: copyIdentity(stamp.identity, `${label}.identity`),
|
|
176
|
+
frame: copyFrame(stamp.frame, `${label}.frame`),
|
|
177
|
+
topology: Object.freeze({
|
|
178
|
+
selection: stamp.topology.selection,
|
|
179
|
+
id: nonEmpty(stamp.topology.id, `${label}.topology.id`),
|
|
180
|
+
}),
|
|
181
|
+
revisions: copyRevisions(stamp.revisions, `${label}.revisions`),
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function distanceToBounds(point, bounds) {
|
|
185
|
+
const delta = ["x", "y", "z"].map((axis) => Math.max(bounds.min[axis] - point[axis], 0, point[axis] - bounds.max[axis]));
|
|
186
|
+
return Math.hypot(...delta);
|
|
187
|
+
}
|
|
188
|
+
function pointDistance(left, right) {
|
|
189
|
+
return Math.hypot(left.x - right.x, left.y - right.y, left.z - right.z);
|
|
190
|
+
}
|
|
191
|
+
function pointIsInside(point, bounds) {
|
|
192
|
+
return ["x", "y", "z"].every((axis) => point[axis] >= bounds.min[axis] && point[axis] <= bounds.max[axis]);
|
|
193
|
+
}
|
|
194
|
+
function sameDistance(left, right) {
|
|
195
|
+
return Math.abs(left - right) <= Math.max(1e-9, Math.max(Math.abs(left), Math.abs(right)) / 1e9);
|
|
196
|
+
}
|
|
197
|
+
function result(status, reason, exactness, topology, checked, exclusions, witnesses) {
|
|
198
|
+
return Object.freeze({
|
|
199
|
+
status,
|
|
200
|
+
reason,
|
|
201
|
+
exactness,
|
|
202
|
+
topology,
|
|
203
|
+
checked: Object.freeze([...checked]),
|
|
204
|
+
exclusions: Object.freeze([...exclusions]),
|
|
205
|
+
witnesses: Object.freeze([...witnesses]),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Decide a point-to-open-surface clearance predicate in the given common
|
|
210
|
+
* Euclidean frame. It never proves containment or solid nonpenetration.
|
|
211
|
+
*
|
|
212
|
+
* Status precedence is stale, then violated, then unsupported/unknown, then
|
|
213
|
+
* satisfied. An exact close witness therefore proves a violation even if a
|
|
214
|
+
* different selected provider cannot answer.
|
|
215
|
+
*/
|
|
216
|
+
function queryPointClearance(provider, oracle, query) {
|
|
217
|
+
if (typeof provider !== "object" || provider === null || typeof provider.snapshot !== "function")
|
|
218
|
+
throw new TypeError("spatial: provider must expose snapshot().");
|
|
219
|
+
if (typeof oracle !== "object" || oracle === null || typeof oracle.nearest !== "function")
|
|
220
|
+
throw new TypeError("spatial: oracle must expose nearest().");
|
|
221
|
+
if (typeof query !== "object" || query === null)
|
|
222
|
+
throw new TypeError("spatial: query must be an object.");
|
|
223
|
+
const point = finitePoint(query.point, "query.point");
|
|
224
|
+
const frame = copyFrame(query.frame, "query.frame");
|
|
225
|
+
const minimumDistance = query.minimumDistance;
|
|
226
|
+
const searchDistance = query.searchDistance;
|
|
227
|
+
if (!Number.isFinite(minimumDistance) || minimumDistance < 0)
|
|
228
|
+
throw new RangeError("spatial: query.minimumDistance must be finite and non-negative.");
|
|
229
|
+
if (!Number.isFinite(searchDistance) || searchDistance < minimumDistance)
|
|
230
|
+
throw new RangeError("spatial: query.searchDistance must be finite and at least minimumDistance.");
|
|
231
|
+
const topology = query.topology ?? "full";
|
|
232
|
+
if (topology !== "full" && topology !== "visible")
|
|
233
|
+
throw new TypeError("spatial: query.topology must be full or visible.");
|
|
234
|
+
if (query.exclusions !== undefined && !Array.isArray(query.exclusions))
|
|
235
|
+
throw new TypeError("spatial: query.exclusions must be an array.");
|
|
236
|
+
if (query.expected !== undefined && !Array.isArray(query.expected))
|
|
237
|
+
throw new TypeError("spatial: query.expected must be an array.");
|
|
238
|
+
const exclusions = Array.from(query.exclusions ?? [], (value, index) => copyIdentity(value, `query.exclusions[${index}]`)).sort(compareIdentity);
|
|
239
|
+
const exclusionKeys = new Set(exclusions.map(identityKey));
|
|
240
|
+
if (exclusionKeys.size !== exclusions.length)
|
|
241
|
+
throw new TypeError("spatial: query.exclusions must not contain duplicates.");
|
|
242
|
+
const expected = Array.from(query.expected ?? [], (value, index) => copyStamp(value, `query.expected[${index}]`));
|
|
243
|
+
const expectedKeys = new Set(expected.map((stamp) => identityKey(stamp.identity)));
|
|
244
|
+
if (expectedKeys.size !== expected.length)
|
|
245
|
+
throw new TypeError("spatial: query.expected must not contain duplicate identities.");
|
|
246
|
+
const snapshot = provider.snapshot();
|
|
247
|
+
if (!Array.isArray(snapshot))
|
|
248
|
+
throw new TypeError("spatial: provider.snapshot() must return an array.");
|
|
249
|
+
let records;
|
|
250
|
+
try {
|
|
251
|
+
records = Array.from(snapshot, (record, index) => copyRecord(record, `provider.snapshot()[${index}]`));
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return result("unsupported", "provider-contract-invalid", "none", topology, [], exclusions, []);
|
|
255
|
+
}
|
|
256
|
+
const recordKeys = new Set(records.map((record) => identityKey(record.identity)));
|
|
257
|
+
if (recordKeys.size !== records.length) {
|
|
258
|
+
return result("unsupported", "provider-duplicate-record", "none", topology, [], exclusions, []);
|
|
259
|
+
}
|
|
260
|
+
records.sort((left, right) => compareIdentity(left.identity, right.identity));
|
|
261
|
+
const allStamps = records.map((record) => makeStamp(record, topology));
|
|
262
|
+
if (expected.length > 0) {
|
|
263
|
+
const stampsByIdentity = new Map(allStamps.map((stamp) => [identityKey(stamp.identity), stamp]));
|
|
264
|
+
if (expected.some((stamp) => {
|
|
265
|
+
const current = stampsByIdentity.get(identityKey(stamp.identity));
|
|
266
|
+
return !current || !sameStamp(current, stamp);
|
|
267
|
+
})) {
|
|
268
|
+
return result("stale", "revision-mismatch", "none", topology, allStamps, exclusions, []);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
const selected = records.filter((record) => !exclusionKeys.has(identityKey(record.identity)));
|
|
272
|
+
const stamps = selected.map((record) => makeStamp(record, topology));
|
|
273
|
+
let unresolved;
|
|
274
|
+
const prepared = [];
|
|
275
|
+
for (const record of selected) {
|
|
276
|
+
if (!sameFrame(record.frame, frame)) {
|
|
277
|
+
unresolved = "provider-contract-invalid";
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
const stamp = makeStamp(record, topology);
|
|
281
|
+
prepared.push(Object.freeze({
|
|
282
|
+
record,
|
|
283
|
+
stamp,
|
|
284
|
+
boundsDistance: distanceToBounds(point, record.topology[topology].bounds),
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
const candidates = prepared.filter(({ boundsDistance }) => boundsDistance < minimumDistance);
|
|
288
|
+
const witnesses = [];
|
|
289
|
+
for (const candidate of candidates) {
|
|
290
|
+
if (!candidate.record.capabilities.includes("nearest-surface")) {
|
|
291
|
+
if (unresolved !== "provider-contract-invalid")
|
|
292
|
+
unresolved = "nearest-surface-unsupported";
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
const reply = oracle.nearest(Object.freeze({
|
|
296
|
+
record: candidate.record,
|
|
297
|
+
topology,
|
|
298
|
+
point,
|
|
299
|
+
maximumDistance: searchDistance,
|
|
300
|
+
}));
|
|
301
|
+
if (reply === undefined) {
|
|
302
|
+
if (unresolved !== "provider-contract-invalid")
|
|
303
|
+
unresolved = "nearest-surface-unsupported";
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (typeof reply !== "object" ||
|
|
307
|
+
reply === null ||
|
|
308
|
+
(reply.kind !== "nearest" && reply.kind !== "no-hit" && reply.kind !== "unknown")) {
|
|
309
|
+
unresolved = "provider-contract-invalid";
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
let consumed;
|
|
313
|
+
try {
|
|
314
|
+
consumed = copyStamp(reply.kind === "nearest" ? reply.witness?.consumed : reply.consumed, "oracle.consumed");
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
unresolved = "provider-contract-invalid";
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (!sameStamp(consumed, candidate.stamp)) {
|
|
321
|
+
return result("stale", "oracle-stale", "none", topology, stamps, exclusions, witnesses);
|
|
322
|
+
}
|
|
323
|
+
if (reply.kind === "unknown") {
|
|
324
|
+
if (typeof reply.reason !== "string" || reply.reason.length === 0)
|
|
325
|
+
unresolved = "provider-contract-invalid";
|
|
326
|
+
else
|
|
327
|
+
unresolved ?? (unresolved = "oracle-unknown");
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (reply.kind === "no-hit")
|
|
331
|
+
continue;
|
|
332
|
+
let witness;
|
|
333
|
+
try {
|
|
334
|
+
const witnessPoint = finitePoint(reply.witness.point, "oracle.witness.point");
|
|
335
|
+
const distance = reply.witness.distance;
|
|
336
|
+
const actualDistance = pointDistance(point, witnessPoint);
|
|
337
|
+
if (!Number.isFinite(distance) ||
|
|
338
|
+
distance < 0 ||
|
|
339
|
+
distance > searchDistance ||
|
|
340
|
+
!Number.isFinite(actualDistance) ||
|
|
341
|
+
actualDistance > searchDistance ||
|
|
342
|
+
!sameDistance(distance, actualDistance) ||
|
|
343
|
+
!pointIsInside(witnessPoint, candidate.record.topology[topology].bounds))
|
|
344
|
+
throw new RangeError();
|
|
345
|
+
if (!sameFrame(copyFrame(reply.witness.frame, "oracle.witness.frame"), frame))
|
|
346
|
+
throw new TypeError();
|
|
347
|
+
witness = Object.freeze({
|
|
348
|
+
triangleId: nonEmpty(reply.witness.triangleId, "oracle.witness.triangleId"),
|
|
349
|
+
point: witnessPoint,
|
|
350
|
+
distance: actualDistance,
|
|
351
|
+
frame,
|
|
352
|
+
consumed: candidate.stamp,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
unresolved = "provider-contract-invalid";
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
witnesses.push(witness);
|
|
360
|
+
}
|
|
361
|
+
const violation = witnesses.find((witness) => witness.distance < minimumDistance);
|
|
362
|
+
if (violation)
|
|
363
|
+
return result("violated", "clearance-violated", "exact", topology, stamps, exclusions, witnesses);
|
|
364
|
+
if (unresolved === "provider-contract-invalid")
|
|
365
|
+
return result("unsupported", "provider-contract-invalid", "none", topology, stamps, exclusions, witnesses);
|
|
366
|
+
if (unresolved === "nearest-surface-unsupported")
|
|
367
|
+
return result("unsupported", "nearest-surface-unsupported", "none", topology, stamps, exclusions, witnesses);
|
|
368
|
+
if (unresolved === "oracle-unknown")
|
|
369
|
+
return result("unknown", "oracle-unknown", "none", topology, stamps, exclusions, witnesses);
|
|
370
|
+
return result("satisfied", "clearance-satisfied", candidates.length > 0 && candidates.length === selected.length ? "exact" : "conservative", topology, stamps, exclusions, witnesses);
|
|
371
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderer-neutral point-to-surface clearance queries.
|
|
3
|
+
*
|
|
4
|
+
* Producers own their buffers, transforms, and exact geometry implementations.
|
|
5
|
+
* This module only validates a completed provider snapshot, filters certified
|
|
6
|
+
* AABBs, and calls an injected oracle in one declared common metric.
|
|
7
|
+
*/
|
|
8
|
+
export type SpatialPoint = Readonly<{
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
z: number;
|
|
12
|
+
}>;
|
|
13
|
+
export type SpatialAabb = Readonly<{
|
|
14
|
+
min: SpatialPoint;
|
|
15
|
+
max: SpatialPoint;
|
|
16
|
+
}>;
|
|
17
|
+
export type SpatialFrame = Readonly<{
|
|
18
|
+
id: string;
|
|
19
|
+
unit: string;
|
|
20
|
+
metric: "euclidean";
|
|
21
|
+
}>;
|
|
22
|
+
/** A provider-local registration identity. It is deliberately not a coordination token. */
|
|
23
|
+
export type SpatialRegistrationIdentity = Readonly<{
|
|
24
|
+
id: string;
|
|
25
|
+
generation: number;
|
|
26
|
+
}>;
|
|
27
|
+
export type SpatialIdentity = Readonly<{
|
|
28
|
+
sourceId: string;
|
|
29
|
+
instanceId: string;
|
|
30
|
+
partId: string;
|
|
31
|
+
registration: SpatialRegistrationIdentity;
|
|
32
|
+
}>;
|
|
33
|
+
/** Independent completed producer epochs; none is a coordination publication revision. */
|
|
34
|
+
export type SpatialRevision = Readonly<{
|
|
35
|
+
geometry: number;
|
|
36
|
+
topology: number;
|
|
37
|
+
pose: number;
|
|
38
|
+
transform: number;
|
|
39
|
+
}>;
|
|
40
|
+
export type SpatialTopology = Readonly<{
|
|
41
|
+
id: string;
|
|
42
|
+
/** The provider certifies that this AABB encloses the topology it supplies to the oracle. */
|
|
43
|
+
bounds: SpatialAabb;
|
|
44
|
+
enclosesSurface: true;
|
|
45
|
+
}>;
|
|
46
|
+
export type SpatialPartRecord = Readonly<{
|
|
47
|
+
identity: SpatialIdentity;
|
|
48
|
+
frame: SpatialFrame;
|
|
49
|
+
revisions: SpatialRevision;
|
|
50
|
+
/** `full` remains the default. `visible` is only selected explicitly by a query. */
|
|
51
|
+
topology: Readonly<{
|
|
52
|
+
full: SpatialTopology;
|
|
53
|
+
visible: SpatialTopology;
|
|
54
|
+
}>;
|
|
55
|
+
capabilities: readonly "nearest-surface"[];
|
|
56
|
+
}>;
|
|
57
|
+
export type SpatialTopologySelection = "full" | "visible";
|
|
58
|
+
export type SpatialRecordStamp = Readonly<{
|
|
59
|
+
identity: SpatialIdentity;
|
|
60
|
+
frame: SpatialFrame;
|
|
61
|
+
topology: Readonly<{
|
|
62
|
+
selection: SpatialTopologySelection;
|
|
63
|
+
id: string;
|
|
64
|
+
}>;
|
|
65
|
+
revisions: SpatialRevision;
|
|
66
|
+
}>;
|
|
67
|
+
export type SpatialNearestWitness = Readonly<{
|
|
68
|
+
triangleId: string;
|
|
69
|
+
point: SpatialPoint;
|
|
70
|
+
distance: number;
|
|
71
|
+
frame: SpatialFrame;
|
|
72
|
+
consumed: SpatialRecordStamp;
|
|
73
|
+
}>;
|
|
74
|
+
export type SpatialOracleReply = Readonly<{
|
|
75
|
+
kind: "nearest";
|
|
76
|
+
witness: SpatialNearestWitness;
|
|
77
|
+
}> | Readonly<{
|
|
78
|
+
kind: "no-hit";
|
|
79
|
+
consumed: SpatialRecordStamp;
|
|
80
|
+
}> | Readonly<{
|
|
81
|
+
kind: "unknown";
|
|
82
|
+
reason: string;
|
|
83
|
+
consumed: SpatialRecordStamp;
|
|
84
|
+
}>;
|
|
85
|
+
export type SpatialNearestSurfaceOracle = Readonly<{
|
|
86
|
+
nearest: (request: Readonly<{
|
|
87
|
+
record: SpatialPartRecord;
|
|
88
|
+
topology: SpatialTopologySelection;
|
|
89
|
+
point: SpatialPoint;
|
|
90
|
+
maximumDistance: number;
|
|
91
|
+
}>) => SpatialOracleReply | undefined;
|
|
92
|
+
}>;
|
|
93
|
+
/** Provider snapshots must be completed, immutable records for one query call. */
|
|
94
|
+
export type SpatialProvider = Readonly<{
|
|
95
|
+
snapshot: () => readonly SpatialPartRecord[];
|
|
96
|
+
}>;
|
|
97
|
+
export type SpatialClearanceQuery = Readonly<{
|
|
98
|
+
point: SpatialPoint;
|
|
99
|
+
frame: SpatialFrame;
|
|
100
|
+
/** The predicate: every selected open surface must be at least this far away. */
|
|
101
|
+
minimumDistance: number;
|
|
102
|
+
/** Finite oracle search bound; it must be at least `minimumDistance`. */
|
|
103
|
+
searchDistance: number;
|
|
104
|
+
topology?: SpatialTopologySelection;
|
|
105
|
+
exclusions?: readonly SpatialIdentity[];
|
|
106
|
+
/** A mismatch is stale, before an oracle result can be trusted. */
|
|
107
|
+
expected?: readonly SpatialRecordStamp[];
|
|
108
|
+
}>;
|
|
109
|
+
export type SpatialQueryStatus = "satisfied" | "violated" | "unknown" | "unsupported" | "stale";
|
|
110
|
+
export type SpatialQueryExactness = "exact" | "conservative" | "none";
|
|
111
|
+
export type SpatialQueryReason = "clearance-satisfied" | "clearance-violated" | "oracle-unknown" | "nearest-surface-unsupported" | "provider-contract-invalid" | "provider-duplicate-record" | "revision-mismatch" | "oracle-stale";
|
|
112
|
+
export type SpatialClearanceResult = Readonly<{
|
|
113
|
+
status: SpatialQueryStatus;
|
|
114
|
+
reason: SpatialQueryReason;
|
|
115
|
+
exactness: SpatialQueryExactness;
|
|
116
|
+
topology: SpatialTopologySelection;
|
|
117
|
+
checked: readonly SpatialRecordStamp[];
|
|
118
|
+
exclusions: readonly SpatialIdentity[];
|
|
119
|
+
witnesses: readonly SpatialNearestWitness[];
|
|
120
|
+
}>;
|
|
121
|
+
/**
|
|
122
|
+
* Decide a point-to-open-surface clearance predicate in the given common
|
|
123
|
+
* Euclidean frame. It never proves containment or solid nonpenetration.
|
|
124
|
+
*
|
|
125
|
+
* Status precedence is stale, then violated, then unsupported/unknown, then
|
|
126
|
+
* satisfied. An exact close witness therefore proves a violation even if a
|
|
127
|
+
* different selected provider cannot answer.
|
|
128
|
+
*/
|
|
129
|
+
export declare function queryPointClearance(provider: SpatialProvider, oracle: SpatialNearestSurfaceOracle, query: SpatialClearanceQuery): SpatialClearanceResult;
|
|
130
|
+
//# sourceMappingURL=spatial.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spatial.d.ts","sourceRoot":"","sources":["../../src/spatial.ts"],"names":[],"mappings":"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"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export { createSchedule } from "./schedule.js";
|
|
|
8
8
|
export type { Schedule, ScheduleOptions, System } from "./schedule.js";
|
|
9
9
|
export { defineEventLog } from "./eventLog.js";
|
|
10
10
|
export type { EventLog } from "./eventLog.js";
|
|
11
|
+
export { queryPointClearance } from "./spatial.js";
|
|
12
|
+
export type { SpatialAabb, SpatialClearanceQuery, SpatialClearanceResult, SpatialFrame, SpatialIdentity, SpatialNearestSurfaceOracle, SpatialNearestWitness, SpatialOracleReply, SpatialPartRecord, SpatialPoint, SpatialProvider, SpatialQueryExactness, SpatialQueryReason, SpatialQueryStatus, SpatialRecordStamp, SpatialRegistrationIdentity, SpatialRevision, SpatialTopology, SpatialTopologySelection, } from "./spatial.js";
|
|
11
13
|
export { defineTrait } from "./traits/index.js";
|
|
12
14
|
export type { SafeSchema } from "./traits/index.js";
|
|
13
15
|
export { bindCoordination, CoordinationBinding, CoordinationRegistration, } from "./coordination.js";
|
package/dist/esm/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,KAAK,GACN,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,MAAM,EACN,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,OAAO,EACP,OAAO,EACP,IAAI,EACJ,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,SAAS,GACV,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC1F,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvE,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,EACZ,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,YAAY,EACZ,aAAa,EACb,cAAc,EACd,eAAe,EACf,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,KAAK,GACN,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE5E,OAAO,EACL,MAAM,EACN,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,OAAO,EACP,OAAO,EACP,IAAI,EACJ,aAAa,EACb,aAAa,EACb,iBAAiB,EACjB,OAAO,EACP,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,SAAS,GACV,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,SAAS,EACT,iBAAiB,EACjB,QAAQ,EACR,SAAS,EACT,iBAAiB,EACjB,UAAU,EACV,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAC1F,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvE,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAC/C,YAAY,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACnD,YAAY,EACV,WAAW,EACX,qBAAqB,EACrB,sBAAsB,EACtB,YAAY,EACZ,eAAe,EACf,2BAA2B,EAC3B,qBAAqB,EACrB,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,eAAe,EACf,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,2BAA2B,EAC3B,eAAe,EACf,eAAe,EACf,wBAAwB,GACzB,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EACL,gBAAgB,EAChB,mBAAmB,EACnB,wBAAwB,GACzB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,iBAAiB,EACjB,kBAAkB,EAClB,YAAY,EACZ,sBAAsB,EACtB,iBAAiB,EACjB,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,mBAAmB,CAAC"}
|
package/dist/esm/index.js
CHANGED
|
@@ -16,5 +16,6 @@ export { chance, createRng, createSubstreams, getSubstream, nextFloat, nextInt,
|
|
|
16
16
|
export { createMasterSeed, deriveSeed, isMasterSeed, MASTER_SEED_BYTES } from "./seed.js";
|
|
17
17
|
export { createSchedule } from "./schedule.js";
|
|
18
18
|
export { defineEventLog } from "./eventLog.js";
|
|
19
|
+
export { queryPointClearance } from "./spatial.js";
|
|
19
20
|
export { defineTrait } from "./traits/index.js";
|
|
20
21
|
export { bindCoordination, CoordinationBinding, CoordinationRegistration, } from "./coordination.js";
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderer-neutral point-to-surface clearance queries.
|
|
3
|
+
*
|
|
4
|
+
* Producers own their buffers, transforms, and exact geometry implementations.
|
|
5
|
+
* This module only validates a completed provider snapshot, filters certified
|
|
6
|
+
* AABBs, and calls an injected oracle in one declared common metric.
|
|
7
|
+
*/
|
|
8
|
+
export type SpatialPoint = Readonly<{
|
|
9
|
+
x: number;
|
|
10
|
+
y: number;
|
|
11
|
+
z: number;
|
|
12
|
+
}>;
|
|
13
|
+
export type SpatialAabb = Readonly<{
|
|
14
|
+
min: SpatialPoint;
|
|
15
|
+
max: SpatialPoint;
|
|
16
|
+
}>;
|
|
17
|
+
export type SpatialFrame = Readonly<{
|
|
18
|
+
id: string;
|
|
19
|
+
unit: string;
|
|
20
|
+
metric: "euclidean";
|
|
21
|
+
}>;
|
|
22
|
+
/** A provider-local registration identity. It is deliberately not a coordination token. */
|
|
23
|
+
export type SpatialRegistrationIdentity = Readonly<{
|
|
24
|
+
id: string;
|
|
25
|
+
generation: number;
|
|
26
|
+
}>;
|
|
27
|
+
export type SpatialIdentity = Readonly<{
|
|
28
|
+
sourceId: string;
|
|
29
|
+
instanceId: string;
|
|
30
|
+
partId: string;
|
|
31
|
+
registration: SpatialRegistrationIdentity;
|
|
32
|
+
}>;
|
|
33
|
+
/** Independent completed producer epochs; none is a coordination publication revision. */
|
|
34
|
+
export type SpatialRevision = Readonly<{
|
|
35
|
+
geometry: number;
|
|
36
|
+
topology: number;
|
|
37
|
+
pose: number;
|
|
38
|
+
transform: number;
|
|
39
|
+
}>;
|
|
40
|
+
export type SpatialTopology = Readonly<{
|
|
41
|
+
id: string;
|
|
42
|
+
/** The provider certifies that this AABB encloses the topology it supplies to the oracle. */
|
|
43
|
+
bounds: SpatialAabb;
|
|
44
|
+
enclosesSurface: true;
|
|
45
|
+
}>;
|
|
46
|
+
export type SpatialPartRecord = Readonly<{
|
|
47
|
+
identity: SpatialIdentity;
|
|
48
|
+
frame: SpatialFrame;
|
|
49
|
+
revisions: SpatialRevision;
|
|
50
|
+
/** `full` remains the default. `visible` is only selected explicitly by a query. */
|
|
51
|
+
topology: Readonly<{
|
|
52
|
+
full: SpatialTopology;
|
|
53
|
+
visible: SpatialTopology;
|
|
54
|
+
}>;
|
|
55
|
+
capabilities: readonly "nearest-surface"[];
|
|
56
|
+
}>;
|
|
57
|
+
export type SpatialTopologySelection = "full" | "visible";
|
|
58
|
+
export type SpatialRecordStamp = Readonly<{
|
|
59
|
+
identity: SpatialIdentity;
|
|
60
|
+
frame: SpatialFrame;
|
|
61
|
+
topology: Readonly<{
|
|
62
|
+
selection: SpatialTopologySelection;
|
|
63
|
+
id: string;
|
|
64
|
+
}>;
|
|
65
|
+
revisions: SpatialRevision;
|
|
66
|
+
}>;
|
|
67
|
+
export type SpatialNearestWitness = Readonly<{
|
|
68
|
+
triangleId: string;
|
|
69
|
+
point: SpatialPoint;
|
|
70
|
+
distance: number;
|
|
71
|
+
frame: SpatialFrame;
|
|
72
|
+
consumed: SpatialRecordStamp;
|
|
73
|
+
}>;
|
|
74
|
+
export type SpatialOracleReply = Readonly<{
|
|
75
|
+
kind: "nearest";
|
|
76
|
+
witness: SpatialNearestWitness;
|
|
77
|
+
}> | Readonly<{
|
|
78
|
+
kind: "no-hit";
|
|
79
|
+
consumed: SpatialRecordStamp;
|
|
80
|
+
}> | Readonly<{
|
|
81
|
+
kind: "unknown";
|
|
82
|
+
reason: string;
|
|
83
|
+
consumed: SpatialRecordStamp;
|
|
84
|
+
}>;
|
|
85
|
+
export type SpatialNearestSurfaceOracle = Readonly<{
|
|
86
|
+
nearest: (request: Readonly<{
|
|
87
|
+
record: SpatialPartRecord;
|
|
88
|
+
topology: SpatialTopologySelection;
|
|
89
|
+
point: SpatialPoint;
|
|
90
|
+
maximumDistance: number;
|
|
91
|
+
}>) => SpatialOracleReply | undefined;
|
|
92
|
+
}>;
|
|
93
|
+
/** Provider snapshots must be completed, immutable records for one query call. */
|
|
94
|
+
export type SpatialProvider = Readonly<{
|
|
95
|
+
snapshot: () => readonly SpatialPartRecord[];
|
|
96
|
+
}>;
|
|
97
|
+
export type SpatialClearanceQuery = Readonly<{
|
|
98
|
+
point: SpatialPoint;
|
|
99
|
+
frame: SpatialFrame;
|
|
100
|
+
/** The predicate: every selected open surface must be at least this far away. */
|
|
101
|
+
minimumDistance: number;
|
|
102
|
+
/** Finite oracle search bound; it must be at least `minimumDistance`. */
|
|
103
|
+
searchDistance: number;
|
|
104
|
+
topology?: SpatialTopologySelection;
|
|
105
|
+
exclusions?: readonly SpatialIdentity[];
|
|
106
|
+
/** A mismatch is stale, before an oracle result can be trusted. */
|
|
107
|
+
expected?: readonly SpatialRecordStamp[];
|
|
108
|
+
}>;
|
|
109
|
+
export type SpatialQueryStatus = "satisfied" | "violated" | "unknown" | "unsupported" | "stale";
|
|
110
|
+
export type SpatialQueryExactness = "exact" | "conservative" | "none";
|
|
111
|
+
export type SpatialQueryReason = "clearance-satisfied" | "clearance-violated" | "oracle-unknown" | "nearest-surface-unsupported" | "provider-contract-invalid" | "provider-duplicate-record" | "revision-mismatch" | "oracle-stale";
|
|
112
|
+
export type SpatialClearanceResult = Readonly<{
|
|
113
|
+
status: SpatialQueryStatus;
|
|
114
|
+
reason: SpatialQueryReason;
|
|
115
|
+
exactness: SpatialQueryExactness;
|
|
116
|
+
topology: SpatialTopologySelection;
|
|
117
|
+
checked: readonly SpatialRecordStamp[];
|
|
118
|
+
exclusions: readonly SpatialIdentity[];
|
|
119
|
+
witnesses: readonly SpatialNearestWitness[];
|
|
120
|
+
}>;
|
|
121
|
+
/**
|
|
122
|
+
* Decide a point-to-open-surface clearance predicate in the given common
|
|
123
|
+
* Euclidean frame. It never proves containment or solid nonpenetration.
|
|
124
|
+
*
|
|
125
|
+
* Status precedence is stale, then violated, then unsupported/unknown, then
|
|
126
|
+
* satisfied. An exact close witness therefore proves a violation even if a
|
|
127
|
+
* different selected provider cannot answer.
|
|
128
|
+
*/
|
|
129
|
+
export declare function queryPointClearance(provider: SpatialProvider, oracle: SpatialNearestSurfaceOracle, query: SpatialClearanceQuery): SpatialClearanceResult;
|
|
130
|
+
//# sourceMappingURL=spatial.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spatial.d.ts","sourceRoot":"","sources":["../../src/spatial.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renderer-neutral point-to-surface clearance queries.
|
|
3
|
+
*
|
|
4
|
+
* Producers own their buffers, transforms, and exact geometry implementations.
|
|
5
|
+
* This module only validates a completed provider snapshot, filters certified
|
|
6
|
+
* AABBs, and calls an injected oracle in one declared common metric.
|
|
7
|
+
*/
|
|
8
|
+
const finitePoint = (value, label) => {
|
|
9
|
+
if (typeof value !== "object" || value === null)
|
|
10
|
+
throw new TypeError(`spatial: ${label} must be a point.`);
|
|
11
|
+
const point = value;
|
|
12
|
+
for (const axis of ["x", "y", "z"]) {
|
|
13
|
+
if (!Number.isFinite(point[axis]))
|
|
14
|
+
throw new RangeError(`spatial: ${label}.${axis} must be finite.`);
|
|
15
|
+
}
|
|
16
|
+
return Object.freeze({ x: point.x, y: point.y, z: point.z });
|
|
17
|
+
};
|
|
18
|
+
const nonEmpty = (value, label) => {
|
|
19
|
+
if (typeof value !== "string" || value.length === 0)
|
|
20
|
+
throw new TypeError(`spatial: ${label} must be a non-empty string.`);
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
const nonNegativeRevision = (value, label) => {
|
|
24
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
25
|
+
throw new RangeError(`spatial: ${label} must be a non-negative safe integer.`);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
};
|
|
29
|
+
function copyFrame(value, label) {
|
|
30
|
+
if (typeof value !== "object" || value === null)
|
|
31
|
+
throw new TypeError(`spatial: ${label} must be a frame.`);
|
|
32
|
+
const frame = value;
|
|
33
|
+
if (frame.metric !== "euclidean")
|
|
34
|
+
throw new TypeError(`spatial: ${label}.metric must be euclidean.`);
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
id: nonEmpty(frame.id, `${label}.id`),
|
|
37
|
+
unit: nonEmpty(frame.unit, `${label}.unit`),
|
|
38
|
+
metric: "euclidean",
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function sameFrame(left, right) {
|
|
42
|
+
return left.id === right.id && left.unit === right.unit && left.metric === right.metric;
|
|
43
|
+
}
|
|
44
|
+
function copyIdentity(value, label) {
|
|
45
|
+
if (typeof value !== "object" || value === null)
|
|
46
|
+
throw new TypeError(`spatial: ${label} must be an identity.`);
|
|
47
|
+
const identity = value;
|
|
48
|
+
if (typeof identity.registration !== "object" || identity.registration === null) {
|
|
49
|
+
throw new TypeError(`spatial: ${label}.registration must be an identity.`);
|
|
50
|
+
}
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
sourceId: nonEmpty(identity.sourceId, `${label}.sourceId`),
|
|
53
|
+
instanceId: nonEmpty(identity.instanceId, `${label}.instanceId`),
|
|
54
|
+
partId: nonEmpty(identity.partId, `${label}.partId`),
|
|
55
|
+
registration: Object.freeze({
|
|
56
|
+
id: nonEmpty(identity.registration.id, `${label}.registration.id`),
|
|
57
|
+
generation: nonNegativeRevision(identity.registration.generation, `${label}.registration.generation`),
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
function sameIdentity(left, right) {
|
|
62
|
+
return (left.sourceId === right.sourceId &&
|
|
63
|
+
left.instanceId === right.instanceId &&
|
|
64
|
+
left.partId === right.partId &&
|
|
65
|
+
left.registration.id === right.registration.id &&
|
|
66
|
+
left.registration.generation === right.registration.generation);
|
|
67
|
+
}
|
|
68
|
+
function identityKey(identity) {
|
|
69
|
+
return JSON.stringify([
|
|
70
|
+
identity.sourceId,
|
|
71
|
+
identity.instanceId,
|
|
72
|
+
identity.partId,
|
|
73
|
+
identity.registration.id,
|
|
74
|
+
identity.registration.generation,
|
|
75
|
+
]);
|
|
76
|
+
}
|
|
77
|
+
function compareIdentity(left, right) {
|
|
78
|
+
const leftKey = identityKey(left);
|
|
79
|
+
const rightKey = identityKey(right);
|
|
80
|
+
return Number(leftKey > rightKey) - Number(leftKey < rightKey);
|
|
81
|
+
}
|
|
82
|
+
function copyRevisions(value, label) {
|
|
83
|
+
if (typeof value !== "object" || value === null)
|
|
84
|
+
throw new TypeError(`spatial: ${label} must be revisions.`);
|
|
85
|
+
const revisions = value;
|
|
86
|
+
return Object.freeze({
|
|
87
|
+
geometry: nonNegativeRevision(revisions.geometry, `${label}.geometry`),
|
|
88
|
+
topology: nonNegativeRevision(revisions.topology, `${label}.topology`),
|
|
89
|
+
pose: nonNegativeRevision(revisions.pose, `${label}.pose`),
|
|
90
|
+
transform: nonNegativeRevision(revisions.transform, `${label}.transform`),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function sameRevisions(left, right) {
|
|
94
|
+
return (left.geometry === right.geometry &&
|
|
95
|
+
left.topology === right.topology &&
|
|
96
|
+
left.pose === right.pose &&
|
|
97
|
+
left.transform === right.transform);
|
|
98
|
+
}
|
|
99
|
+
function copyBounds(value, label) {
|
|
100
|
+
if (typeof value !== "object" || value === null)
|
|
101
|
+
throw new TypeError(`spatial: ${label} must be an AABB.`);
|
|
102
|
+
const bounds = value;
|
|
103
|
+
const min = finitePoint(bounds.min, `${label}.min`);
|
|
104
|
+
const max = finitePoint(bounds.max, `${label}.max`);
|
|
105
|
+
for (const axis of ["x", "y", "z"]) {
|
|
106
|
+
if (min[axis] > max[axis])
|
|
107
|
+
throw new RangeError(`spatial: ${label}.min.${axis} must not exceed max.${axis}.`);
|
|
108
|
+
}
|
|
109
|
+
return Object.freeze({ min, max });
|
|
110
|
+
}
|
|
111
|
+
function copyTopology(value, label) {
|
|
112
|
+
if (typeof value !== "object" || value === null)
|
|
113
|
+
throw new TypeError(`spatial: ${label} must be topology.`);
|
|
114
|
+
const topology = value;
|
|
115
|
+
if (topology.enclosesSurface !== true)
|
|
116
|
+
throw new TypeError(`spatial: ${label}.enclosesSurface must certify true.`);
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
id: nonEmpty(topology.id, `${label}.id`),
|
|
119
|
+
bounds: copyBounds(topology.bounds, `${label}.bounds`),
|
|
120
|
+
enclosesSurface: true,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function copyRecord(value, label) {
|
|
124
|
+
if (typeof value !== "object" || value === null)
|
|
125
|
+
throw new TypeError(`spatial: ${label} must be a part record.`);
|
|
126
|
+
const record = value;
|
|
127
|
+
if (typeof record.topology !== "object" || record.topology === null)
|
|
128
|
+
throw new TypeError(`spatial: ${label}.topology must be an object.`);
|
|
129
|
+
if (!Array.isArray(record.capabilities))
|
|
130
|
+
throw new TypeError(`spatial: ${label}.capabilities must be an array.`);
|
|
131
|
+
const capabilities = Array.from(record.capabilities);
|
|
132
|
+
if (capabilities.some((capability) => capability !== "nearest-surface") ||
|
|
133
|
+
new Set(capabilities).size !== capabilities.length) {
|
|
134
|
+
throw new TypeError(`spatial: ${label}.capabilities may only contain nearest-surface.`);
|
|
135
|
+
}
|
|
136
|
+
return Object.freeze({
|
|
137
|
+
identity: copyIdentity(record.identity, `${label}.identity`),
|
|
138
|
+
frame: copyFrame(record.frame, `${label}.frame`),
|
|
139
|
+
revisions: copyRevisions(record.revisions, `${label}.revisions`),
|
|
140
|
+
topology: Object.freeze({
|
|
141
|
+
full: copyTopology(record.topology.full, `${label}.topology.full`),
|
|
142
|
+
visible: copyTopology(record.topology.visible, `${label}.topology.visible`),
|
|
143
|
+
}),
|
|
144
|
+
capabilities: Object.freeze(capabilities),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function makeStamp(record, selection) {
|
|
148
|
+
return Object.freeze({
|
|
149
|
+
identity: record.identity,
|
|
150
|
+
frame: record.frame,
|
|
151
|
+
topology: Object.freeze({ selection, id: record.topology[selection].id }),
|
|
152
|
+
revisions: record.revisions,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
function sameStamp(left, right) {
|
|
156
|
+
return (sameIdentity(left.identity, right.identity) &&
|
|
157
|
+
sameFrame(left.frame, right.frame) &&
|
|
158
|
+
left.topology.selection === right.topology.selection &&
|
|
159
|
+
left.topology.id === right.topology.id &&
|
|
160
|
+
sameRevisions(left.revisions, right.revisions));
|
|
161
|
+
}
|
|
162
|
+
function copyStamp(value, label) {
|
|
163
|
+
if (typeof value !== "object" || value === null)
|
|
164
|
+
throw new TypeError(`spatial: ${label} must be a record stamp.`);
|
|
165
|
+
const stamp = value;
|
|
166
|
+
if (typeof stamp.topology !== "object" ||
|
|
167
|
+
stamp.topology === null ||
|
|
168
|
+
(stamp.topology.selection !== "full" && stamp.topology.selection !== "visible")) {
|
|
169
|
+
throw new TypeError(`spatial: ${label}.topology.selection must be full or visible.`);
|
|
170
|
+
}
|
|
171
|
+
return Object.freeze({
|
|
172
|
+
identity: copyIdentity(stamp.identity, `${label}.identity`),
|
|
173
|
+
frame: copyFrame(stamp.frame, `${label}.frame`),
|
|
174
|
+
topology: Object.freeze({
|
|
175
|
+
selection: stamp.topology.selection,
|
|
176
|
+
id: nonEmpty(stamp.topology.id, `${label}.topology.id`),
|
|
177
|
+
}),
|
|
178
|
+
revisions: copyRevisions(stamp.revisions, `${label}.revisions`),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function distanceToBounds(point, bounds) {
|
|
182
|
+
const delta = ["x", "y", "z"].map((axis) => Math.max(bounds.min[axis] - point[axis], 0, point[axis] - bounds.max[axis]));
|
|
183
|
+
return Math.hypot(...delta);
|
|
184
|
+
}
|
|
185
|
+
function pointDistance(left, right) {
|
|
186
|
+
return Math.hypot(left.x - right.x, left.y - right.y, left.z - right.z);
|
|
187
|
+
}
|
|
188
|
+
function pointIsInside(point, bounds) {
|
|
189
|
+
return ["x", "y", "z"].every((axis) => point[axis] >= bounds.min[axis] && point[axis] <= bounds.max[axis]);
|
|
190
|
+
}
|
|
191
|
+
function sameDistance(left, right) {
|
|
192
|
+
return Math.abs(left - right) <= Math.max(1e-9, Math.max(Math.abs(left), Math.abs(right)) / 1e9);
|
|
193
|
+
}
|
|
194
|
+
function result(status, reason, exactness, topology, checked, exclusions, witnesses) {
|
|
195
|
+
return Object.freeze({
|
|
196
|
+
status,
|
|
197
|
+
reason,
|
|
198
|
+
exactness,
|
|
199
|
+
topology,
|
|
200
|
+
checked: Object.freeze([...checked]),
|
|
201
|
+
exclusions: Object.freeze([...exclusions]),
|
|
202
|
+
witnesses: Object.freeze([...witnesses]),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Decide a point-to-open-surface clearance predicate in the given common
|
|
207
|
+
* Euclidean frame. It never proves containment or solid nonpenetration.
|
|
208
|
+
*
|
|
209
|
+
* Status precedence is stale, then violated, then unsupported/unknown, then
|
|
210
|
+
* satisfied. An exact close witness therefore proves a violation even if a
|
|
211
|
+
* different selected provider cannot answer.
|
|
212
|
+
*/
|
|
213
|
+
export function queryPointClearance(provider, oracle, query) {
|
|
214
|
+
if (typeof provider !== "object" || provider === null || typeof provider.snapshot !== "function")
|
|
215
|
+
throw new TypeError("spatial: provider must expose snapshot().");
|
|
216
|
+
if (typeof oracle !== "object" || oracle === null || typeof oracle.nearest !== "function")
|
|
217
|
+
throw new TypeError("spatial: oracle must expose nearest().");
|
|
218
|
+
if (typeof query !== "object" || query === null)
|
|
219
|
+
throw new TypeError("spatial: query must be an object.");
|
|
220
|
+
const point = finitePoint(query.point, "query.point");
|
|
221
|
+
const frame = copyFrame(query.frame, "query.frame");
|
|
222
|
+
const minimumDistance = query.minimumDistance;
|
|
223
|
+
const searchDistance = query.searchDistance;
|
|
224
|
+
if (!Number.isFinite(minimumDistance) || minimumDistance < 0)
|
|
225
|
+
throw new RangeError("spatial: query.minimumDistance must be finite and non-negative.");
|
|
226
|
+
if (!Number.isFinite(searchDistance) || searchDistance < minimumDistance)
|
|
227
|
+
throw new RangeError("spatial: query.searchDistance must be finite and at least minimumDistance.");
|
|
228
|
+
const topology = query.topology ?? "full";
|
|
229
|
+
if (topology !== "full" && topology !== "visible")
|
|
230
|
+
throw new TypeError("spatial: query.topology must be full or visible.");
|
|
231
|
+
if (query.exclusions !== undefined && !Array.isArray(query.exclusions))
|
|
232
|
+
throw new TypeError("spatial: query.exclusions must be an array.");
|
|
233
|
+
if (query.expected !== undefined && !Array.isArray(query.expected))
|
|
234
|
+
throw new TypeError("spatial: query.expected must be an array.");
|
|
235
|
+
const exclusions = Array.from(query.exclusions ?? [], (value, index) => copyIdentity(value, `query.exclusions[${index}]`)).sort(compareIdentity);
|
|
236
|
+
const exclusionKeys = new Set(exclusions.map(identityKey));
|
|
237
|
+
if (exclusionKeys.size !== exclusions.length)
|
|
238
|
+
throw new TypeError("spatial: query.exclusions must not contain duplicates.");
|
|
239
|
+
const expected = Array.from(query.expected ?? [], (value, index) => copyStamp(value, `query.expected[${index}]`));
|
|
240
|
+
const expectedKeys = new Set(expected.map((stamp) => identityKey(stamp.identity)));
|
|
241
|
+
if (expectedKeys.size !== expected.length)
|
|
242
|
+
throw new TypeError("spatial: query.expected must not contain duplicate identities.");
|
|
243
|
+
const snapshot = provider.snapshot();
|
|
244
|
+
if (!Array.isArray(snapshot))
|
|
245
|
+
throw new TypeError("spatial: provider.snapshot() must return an array.");
|
|
246
|
+
let records;
|
|
247
|
+
try {
|
|
248
|
+
records = Array.from(snapshot, (record, index) => copyRecord(record, `provider.snapshot()[${index}]`));
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return result("unsupported", "provider-contract-invalid", "none", topology, [], exclusions, []);
|
|
252
|
+
}
|
|
253
|
+
const recordKeys = new Set(records.map((record) => identityKey(record.identity)));
|
|
254
|
+
if (recordKeys.size !== records.length) {
|
|
255
|
+
return result("unsupported", "provider-duplicate-record", "none", topology, [], exclusions, []);
|
|
256
|
+
}
|
|
257
|
+
records.sort((left, right) => compareIdentity(left.identity, right.identity));
|
|
258
|
+
const allStamps = records.map((record) => makeStamp(record, topology));
|
|
259
|
+
if (expected.length > 0) {
|
|
260
|
+
const stampsByIdentity = new Map(allStamps.map((stamp) => [identityKey(stamp.identity), stamp]));
|
|
261
|
+
if (expected.some((stamp) => {
|
|
262
|
+
const current = stampsByIdentity.get(identityKey(stamp.identity));
|
|
263
|
+
return !current || !sameStamp(current, stamp);
|
|
264
|
+
})) {
|
|
265
|
+
return result("stale", "revision-mismatch", "none", topology, allStamps, exclusions, []);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const selected = records.filter((record) => !exclusionKeys.has(identityKey(record.identity)));
|
|
269
|
+
const stamps = selected.map((record) => makeStamp(record, topology));
|
|
270
|
+
let unresolved;
|
|
271
|
+
const prepared = [];
|
|
272
|
+
for (const record of selected) {
|
|
273
|
+
if (!sameFrame(record.frame, frame)) {
|
|
274
|
+
unresolved = "provider-contract-invalid";
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const stamp = makeStamp(record, topology);
|
|
278
|
+
prepared.push(Object.freeze({
|
|
279
|
+
record,
|
|
280
|
+
stamp,
|
|
281
|
+
boundsDistance: distanceToBounds(point, record.topology[topology].bounds),
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
284
|
+
const candidates = prepared.filter(({ boundsDistance }) => boundsDistance < minimumDistance);
|
|
285
|
+
const witnesses = [];
|
|
286
|
+
for (const candidate of candidates) {
|
|
287
|
+
if (!candidate.record.capabilities.includes("nearest-surface")) {
|
|
288
|
+
if (unresolved !== "provider-contract-invalid")
|
|
289
|
+
unresolved = "nearest-surface-unsupported";
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const reply = oracle.nearest(Object.freeze({
|
|
293
|
+
record: candidate.record,
|
|
294
|
+
topology,
|
|
295
|
+
point,
|
|
296
|
+
maximumDistance: searchDistance,
|
|
297
|
+
}));
|
|
298
|
+
if (reply === undefined) {
|
|
299
|
+
if (unresolved !== "provider-contract-invalid")
|
|
300
|
+
unresolved = "nearest-surface-unsupported";
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (typeof reply !== "object" ||
|
|
304
|
+
reply === null ||
|
|
305
|
+
(reply.kind !== "nearest" && reply.kind !== "no-hit" && reply.kind !== "unknown")) {
|
|
306
|
+
unresolved = "provider-contract-invalid";
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
let consumed;
|
|
310
|
+
try {
|
|
311
|
+
consumed = copyStamp(reply.kind === "nearest" ? reply.witness?.consumed : reply.consumed, "oracle.consumed");
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
unresolved = "provider-contract-invalid";
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
if (!sameStamp(consumed, candidate.stamp)) {
|
|
318
|
+
return result("stale", "oracle-stale", "none", topology, stamps, exclusions, witnesses);
|
|
319
|
+
}
|
|
320
|
+
if (reply.kind === "unknown") {
|
|
321
|
+
if (typeof reply.reason !== "string" || reply.reason.length === 0)
|
|
322
|
+
unresolved = "provider-contract-invalid";
|
|
323
|
+
else
|
|
324
|
+
unresolved ?? (unresolved = "oracle-unknown");
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (reply.kind === "no-hit")
|
|
328
|
+
continue;
|
|
329
|
+
let witness;
|
|
330
|
+
try {
|
|
331
|
+
const witnessPoint = finitePoint(reply.witness.point, "oracle.witness.point");
|
|
332
|
+
const distance = reply.witness.distance;
|
|
333
|
+
const actualDistance = pointDistance(point, witnessPoint);
|
|
334
|
+
if (!Number.isFinite(distance) ||
|
|
335
|
+
distance < 0 ||
|
|
336
|
+
distance > searchDistance ||
|
|
337
|
+
!Number.isFinite(actualDistance) ||
|
|
338
|
+
actualDistance > searchDistance ||
|
|
339
|
+
!sameDistance(distance, actualDistance) ||
|
|
340
|
+
!pointIsInside(witnessPoint, candidate.record.topology[topology].bounds))
|
|
341
|
+
throw new RangeError();
|
|
342
|
+
if (!sameFrame(copyFrame(reply.witness.frame, "oracle.witness.frame"), frame))
|
|
343
|
+
throw new TypeError();
|
|
344
|
+
witness = Object.freeze({
|
|
345
|
+
triangleId: nonEmpty(reply.witness.triangleId, "oracle.witness.triangleId"),
|
|
346
|
+
point: witnessPoint,
|
|
347
|
+
distance: actualDistance,
|
|
348
|
+
frame,
|
|
349
|
+
consumed: candidate.stamp,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
unresolved = "provider-contract-invalid";
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
witnesses.push(witness);
|
|
357
|
+
}
|
|
358
|
+
const violation = witnesses.find((witness) => witness.distance < minimumDistance);
|
|
359
|
+
if (violation)
|
|
360
|
+
return result("violated", "clearance-violated", "exact", topology, stamps, exclusions, witnesses);
|
|
361
|
+
if (unresolved === "provider-contract-invalid")
|
|
362
|
+
return result("unsupported", "provider-contract-invalid", "none", topology, stamps, exclusions, witnesses);
|
|
363
|
+
if (unresolved === "nearest-surface-unsupported")
|
|
364
|
+
return result("unsupported", "nearest-surface-unsupported", "none", topology, stamps, exclusions, witnesses);
|
|
365
|
+
if (unresolved === "oracle-unknown")
|
|
366
|
+
return result("unknown", "oracle-unknown", "none", topology, stamps, exclusions, witnesses);
|
|
367
|
+
return result("satisfied", "clearance-satisfied", candidates.length > 0 && candidates.length === selected.length ? "exact" : "conservative", topology, stamps, exclusions, witnesses);
|
|
368
|
+
}
|
package/docs/API.md
CHANGED
|
@@ -5,6 +5,45 @@ bindings, which are only available from `koota-kit/react` so the root entry
|
|
|
5
5
|
never loads React. The module-specific import paths shown below can reduce
|
|
6
6
|
coupling in larger codebases.
|
|
7
7
|
|
|
8
|
+
## `spatial`
|
|
9
|
+
|
|
10
|
+
Import from `koota-kit/spatial` or the root:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
function queryPointClearance(
|
|
14
|
+
provider: SpatialProvider,
|
|
15
|
+
oracle: SpatialNearestSurfaceOracle,
|
|
16
|
+
query: SpatialClearanceQuery,
|
|
17
|
+
): SpatialClearanceResult;
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`queryPointClearance` decides one explicit predicate: the query point is at
|
|
21
|
+
least `minimumDistance` from every selected **open surface**. The default
|
|
22
|
+
selection is each part's `full` topology; callers must explicitly request
|
|
23
|
+
`visible` topology. A provider record carries stable source, instance, part,
|
|
24
|
+
and provider-local registration identity; a common Euclidean frame and unit;
|
|
25
|
+
independent geometry, topology, pose, and transform revisions; and certified
|
|
26
|
+
enclosing AABBs for both topology selections.
|
|
27
|
+
|
|
28
|
+
The core performs deterministic AABB broadphase only. It invokes the supplied
|
|
29
|
+
nearest-surface oracle for overlapping candidates and validates that its
|
|
30
|
+
witness is in the selected certified bound, in the same frame/unit, and was
|
|
31
|
+
computed from the exact consumed identity/topology/revision tuple. Results
|
|
32
|
+
include those checked tuples, ordered witnesses, exclusions, one of
|
|
33
|
+
`satisfied`, `violated`, `unknown`, `unsupported`, or `stale`, and an
|
|
34
|
+
exactness label. An AABB rejection can conservatively satisfy the threshold;
|
|
35
|
+
an oracle witness or a stamped bounded `no-hit` is exact. Satisfaction that
|
|
36
|
+
combines AABB and oracle evidence is conservative. A stale expected/provider
|
|
37
|
+
tuple wins before the result can be trusted; otherwise an exact violating
|
|
38
|
+
witness wins over an unavailable or foreign-frame peer. Sparse provider
|
|
39
|
+
snapshots and capability lists fail closed. Validated query thresholds are captured before any
|
|
40
|
+
provider or oracle callback and cannot change during evaluation.
|
|
41
|
+
|
|
42
|
+
There is no geometry kernel, BVH, renderer, model, physics, Koota, or React
|
|
43
|
+
dependency in this entry. It does not prove containment or solid
|
|
44
|
+
nonpenetration: an open-surface nearest query has no watertightness/thickness
|
|
45
|
+
semantics. A future solid query needs an injected containment-capable oracle.
|
|
46
|
+
|
|
8
47
|
## `world`
|
|
9
48
|
|
|
10
49
|
### `createSimWorld(seeds)`
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -13,6 +13,7 @@ rng ──────────────────> seedrandom + seed
|
|
|
13
13
|
world ────────────────> koota + rng
|
|
14
14
|
schedule ─────────────> world (advanceClock) + WorldHandle scratch
|
|
15
15
|
eventLog ─────────────> WorldHandle scratch
|
|
16
|
+
spatial ──────────────> (leaf: caller-provided provider + exact oracle)
|
|
16
17
|
coordination ─────────> koota (borrowed World, no sibling runtime dependency)
|
|
17
18
|
react ────────────────> koota/react + react + world (type only)
|
|
18
19
|
index ────────────────> public re-exports only (never react)
|
|
@@ -45,6 +46,20 @@ than carrying it, so one huge dt cannot cause a catch-up storm.
|
|
|
45
46
|
`eventLog` uses the handle's scratch map so event lifetime matches world
|
|
46
47
|
lifetime. It does not use a process-global queue.
|
|
47
48
|
|
|
49
|
+
`spatial` is a renderer-neutral leaf entry. Providers publish immutable,
|
|
50
|
+
completed records with caller-owned source/instance/part/registration identity,
|
|
51
|
+
common frame/unit, separate geometry/topology/pose/transform revisions, full
|
|
52
|
+
and visible topology identities, and certified enclosing AABBs. The module
|
|
53
|
+
sorts records deterministically, does only conservative AABB filtering, and
|
|
54
|
+
delegates exact nearest-surface work to an injected oracle. It retains neither
|
|
55
|
+
provider records nor buffers, so producer-owned buffers cannot be aliased by a
|
|
56
|
+
core cache. Full topology is the default; a visible selection is explicit.
|
|
57
|
+
Sparse provider slots and capability lists fail closed; copied query thresholds stay fixed through
|
|
58
|
+
callbacks. Foreign-frame peers remain unresolved while compatible candidates
|
|
59
|
+
are evaluated, preserving exact violation precedence. Satisfaction involving
|
|
60
|
+
any AABB-only evidence remains conservative.
|
|
61
|
+
Point clearance establishes distance to open surfaces only. It deliberately
|
|
62
|
+
does not infer solid containment or nonpenetration from triangle proximity.
|
|
48
63
|
`coordination` accepts an existing raw Koota `World` and attaches only its
|
|
49
64
|
own ownership and registration traits. Its immutable snapshot is a completed
|
|
50
65
|
metadata projection, not a second World or ECS serializer. The World retains
|
|
@@ -90,9 +105,12 @@ it reaches `react` or `koota/react`.
|
|
|
90
105
|
delivered at any frame rate runs the same steps in the same order, as long
|
|
91
106
|
as no single tick owes more than `maxSteps` (beyond that, the excess is
|
|
92
107
|
dropped by design, so a one-second frame and sixty 1/60 s frames differ).
|
|
93
|
-
11.
|
|
108
|
+
11. Spatial query results are stamped with the provider's full identity,
|
|
109
|
+
selected topology, frame/unit, and independent completed revisions. A
|
|
110
|
+
mismatched expected or oracle-consumed tuple is stale, never exact.
|
|
111
|
+
12. A coordination snapshot publishes only fully validated completed metadata;
|
|
94
112
|
failed batch preparation leaves the prior projection visible.
|
|
95
|
-
|
|
113
|
+
13. Coordination disposal never destroys or resets a caller-owned World,
|
|
96
114
|
entity, caller trait, provider, or geometry buffer.
|
|
97
115
|
|
|
98
116
|
## Performance choices
|
|
@@ -102,6 +120,10 @@ it reaches `react` or `koota/react`.
|
|
|
102
120
|
through JSON.
|
|
103
121
|
- Event logs allocate on first push, leave scratch untouched for empty reads,
|
|
104
122
|
truncate in place on drain, and expose a non-allocating `size` operation.
|
|
123
|
+
- Spatial expected stamps use a per-query map keyed by the complete encoded
|
|
124
|
+
identity tuple. Matching all expectations takes linear lookup work after
|
|
125
|
+
preparing the snapshot, before any exclusions; no map is retained between
|
|
126
|
+
queries. Queries without expected stamps do not allocate this map.
|
|
105
127
|
- Coordination snapshots are rebuilt only after a completed registration,
|
|
106
128
|
publication, removal, or explicit prune. Equal completed revisions retain
|
|
107
129
|
the prior snapshot identity and do not notify subscribers.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koota-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Deterministic Koota simulation conventions: world lifecycle, dual RNG streams, safe object traits, and world-scoped event logs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -83,14 +83,14 @@
|
|
|
83
83
|
"default": "./dist/cjs/eventLog.cjs"
|
|
84
84
|
}
|
|
85
85
|
},
|
|
86
|
-
"./
|
|
86
|
+
"./spatial": {
|
|
87
87
|
"import": {
|
|
88
|
-
"types": "./dist/esm/
|
|
89
|
-
"default": "./dist/esm/
|
|
88
|
+
"types": "./dist/esm/spatial.d.ts",
|
|
89
|
+
"default": "./dist/esm/spatial.js"
|
|
90
90
|
},
|
|
91
91
|
"require": {
|
|
92
|
-
"types": "./dist/cjs/
|
|
93
|
-
"default": "./dist/cjs/
|
|
92
|
+
"types": "./dist/cjs/spatial.d.cts",
|
|
93
|
+
"default": "./dist/cjs/spatial.cjs"
|
|
94
94
|
}
|
|
95
95
|
},
|
|
96
96
|
"./react": {
|
|
@@ -103,7 +103,17 @@
|
|
|
103
103
|
"default": "./dist/cjs/react.cjs"
|
|
104
104
|
}
|
|
105
105
|
},
|
|
106
|
-
"./package.json": "./package.json"
|
|
106
|
+
"./package.json": "./package.json",
|
|
107
|
+
"./coordination": {
|
|
108
|
+
"import": {
|
|
109
|
+
"types": "./dist/esm/coordination.d.ts",
|
|
110
|
+
"default": "./dist/esm/coordination.js"
|
|
111
|
+
},
|
|
112
|
+
"require": {
|
|
113
|
+
"types": "./dist/cjs/coordination.d.cts",
|
|
114
|
+
"default": "./dist/cjs/coordination.cjs"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
107
117
|
},
|
|
108
118
|
"files": [
|
|
109
119
|
"CHANGELOG.md",
|
|
@@ -215,14 +225,17 @@
|
|
|
215
225
|
"eventLog": [
|
|
216
226
|
"./dist/esm/eventLog.d.ts"
|
|
217
227
|
],
|
|
218
|
-
"
|
|
219
|
-
"./dist/esm/
|
|
228
|
+
"spatial": [
|
|
229
|
+
"./dist/esm/spatial.d.ts"
|
|
220
230
|
],
|
|
221
231
|
"react": [
|
|
222
232
|
"./dist/esm/react.d.ts"
|
|
223
233
|
],
|
|
224
234
|
"*": [
|
|
225
235
|
"./dist/esm/index.d.ts"
|
|
236
|
+
],
|
|
237
|
+
"coordination": [
|
|
238
|
+
"./dist/esm/coordination.d.ts"
|
|
226
239
|
]
|
|
227
240
|
}
|
|
228
241
|
},
|