@bldrs-ai/conway 1.502.1421 → 1.509.1435
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/compiled/Dist/ConwayGeomWasmNode.js +0 -0
- package/compiled/Dist/ConwayGeomWasmNodeMT.js +0 -0
- package/compiled/Dist/ConwayGeomWasmWeb.js +0 -0
- package/compiled/Dist/ConwayGeomWasmWebMT.js +1 -1
- package/compiled/Dist/ConwayGeomWasmWebMT.wasm +0 -0
- package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmNode.js +0 -0
- package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmNodeMT.js +0 -0
- package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmWeb.js +0 -0
- package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmWebMT.js +1 -1
- package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmWebMT.wasm +0 -0
- package/compiled/dependencies/conway-geom/interface/conway_geometry.d.ts +19 -0
- package/compiled/dependencies/conway-geom/interface/conway_geometry.d.ts.map +1 -1
- package/compiled/dependencies/conway-geom/interface/conway_geometry.js +17 -0
- package/compiled/examples/browser-bundled.cjs +132 -21
- package/compiled/examples/cli-bundled.cjs +189 -57
- package/compiled/examples/cli-step-bundled.cjs +156 -28
- package/compiled/examples/validator-bundled.cjs +132 -21
- package/compiled/src/compat/web-ifc/coordination_export_order.test.d.ts +2 -0
- package/compiled/src/compat/web-ifc/coordination_export_order.test.d.ts.map +1 -0
- package/compiled/src/compat/web-ifc/coordination_export_order.test.js +110 -0
- package/compiled/src/compat/web-ifc/coordination_f64.d.ts +60 -1
- package/compiled/src/compat/web-ifc/coordination_f64.d.ts.map +1 -1
- package/compiled/src/compat/web-ifc/coordination_f64.js +103 -8
- package/compiled/src/compat/web-ifc/coordination_f64.test.js +115 -30
- package/compiled/src/ifc/ifc_geometry_extraction.d.ts +0 -1
- package/compiled/src/ifc/ifc_geometry_extraction.d.ts.map +1 -1
- package/compiled/src/ifc/ifc_geometry_extraction.js +34 -44
- package/compiled/src/step/parsing/uint32_sink.d.ts +20 -0
- package/compiled/src/step/parsing/uint32_sink.d.ts.map +1 -1
- package/compiled/src/step/parsing/uint32_sink.js +95 -0
- package/compiled/src/step/parsing/uint32_sink.test.js +41 -1
- package/compiled/src/step/step_model_base.d.ts +32 -13
- package/compiled/src/step/step_model_base.d.ts.map +1 -1
- package/compiled/src/step/step_model_base.js +64 -24
- package/compiled/src/version/version.js +1 -1
- package/compiled/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -944,7 +944,7 @@ var EntityTypesIfcCount = 909;
|
|
|
944
944
|
var entity_types_ifc_gen_default = EntityTypesIfc;
|
|
945
945
|
|
|
946
946
|
// compiled/src/version/version.js
|
|
947
|
-
var versionString = "Conway v1.
|
|
947
|
+
var versionString = "Conway v1.509.1435";
|
|
948
948
|
|
|
949
949
|
// compiled/dependencies/conway-geom/interface/conway_geometry.js
|
|
950
950
|
var wasmType = "";
|
|
@@ -4696,7 +4696,18 @@ var StepVtableBuilder = class {
|
|
|
4696
4696
|
};
|
|
4697
4697
|
|
|
4698
4698
|
// compiled/src/step/parsing/uint32_sink.js
|
|
4699
|
+
var OPEN_PAREN3 = parsing_constants_default.OPEN_PAREN;
|
|
4700
|
+
var CLOSE_PAREN3 = parsing_constants_default.CLOSE_PAREN;
|
|
4701
|
+
var COMMA3 = parsing_constants_default.COMMA;
|
|
4702
|
+
var ZERO5 = parsing_constants_default.ZERO;
|
|
4703
|
+
var WHITESPACE4 = parsing_constants_default.WHITE_SPACE_SET;
|
|
4699
4704
|
function extractIntegerArrayAt(buffer, cursor, endCursor, sink) {
|
|
4705
|
+
const marked = sink.length;
|
|
4706
|
+
const fast = extractUnsignedIntegerListAt(buffer, cursor, endCursor, sink);
|
|
4707
|
+
if (fast !== void 0) {
|
|
4708
|
+
return fast;
|
|
4709
|
+
}
|
|
4710
|
+
sink.truncate(marked);
|
|
4700
4711
|
if (stepExtractOptional(buffer, cursor, endCursor) === null) {
|
|
4701
4712
|
return 0;
|
|
4702
4713
|
}
|
|
@@ -4716,6 +4727,68 @@ function extractIntegerArrayAt(buffer, cursor, endCursor, sink) {
|
|
|
4716
4727
|
}
|
|
4717
4728
|
return count;
|
|
4718
4729
|
}
|
|
4730
|
+
function extractUnsignedIntegerListAt(buffer, cursor, endCursor, sink) {
|
|
4731
|
+
const marked = sink.length;
|
|
4732
|
+
while (cursor < endCursor && WHITESPACE4.has(buffer[cursor])) {
|
|
4733
|
+
++cursor;
|
|
4734
|
+
}
|
|
4735
|
+
if (cursor >= endCursor) {
|
|
4736
|
+
return void 0;
|
|
4737
|
+
}
|
|
4738
|
+
if (buffer[cursor] === parsing_constants_default.DOLLAR) {
|
|
4739
|
+
return 0;
|
|
4740
|
+
}
|
|
4741
|
+
if (buffer[cursor] !== OPEN_PAREN3) {
|
|
4742
|
+
return void 0;
|
|
4743
|
+
}
|
|
4744
|
+
++cursor;
|
|
4745
|
+
let count = 0;
|
|
4746
|
+
while (cursor < endCursor) {
|
|
4747
|
+
while (cursor < endCursor && WHITESPACE4.has(buffer[cursor])) {
|
|
4748
|
+
++cursor;
|
|
4749
|
+
}
|
|
4750
|
+
if (cursor >= endCursor) {
|
|
4751
|
+
sink.truncate(marked);
|
|
4752
|
+
return void 0;
|
|
4753
|
+
}
|
|
4754
|
+
if (buffer[cursor] === CLOSE_PAREN3) {
|
|
4755
|
+
return count;
|
|
4756
|
+
}
|
|
4757
|
+
if (count > 0) {
|
|
4758
|
+
if (buffer[cursor] !== COMMA3) {
|
|
4759
|
+
sink.truncate(marked);
|
|
4760
|
+
return void 0;
|
|
4761
|
+
}
|
|
4762
|
+
++cursor;
|
|
4763
|
+
while (cursor < endCursor && WHITESPACE4.has(buffer[cursor])) {
|
|
4764
|
+
++cursor;
|
|
4765
|
+
}
|
|
4766
|
+
if (cursor >= endCursor) {
|
|
4767
|
+
sink.truncate(marked);
|
|
4768
|
+
return void 0;
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
const digit = buffer[cursor] - ZERO5;
|
|
4772
|
+
if (digit < 0 || digit > 9) {
|
|
4773
|
+
sink.truncate(marked);
|
|
4774
|
+
return void 0;
|
|
4775
|
+
}
|
|
4776
|
+
let value = digit;
|
|
4777
|
+
++cursor;
|
|
4778
|
+
while (cursor < endCursor) {
|
|
4779
|
+
const next = buffer[cursor] - ZERO5;
|
|
4780
|
+
if (next < 0 || next > 9) {
|
|
4781
|
+
break;
|
|
4782
|
+
}
|
|
4783
|
+
value = value * 10 + next;
|
|
4784
|
+
++cursor;
|
|
4785
|
+
}
|
|
4786
|
+
sink.push(value);
|
|
4787
|
+
++count;
|
|
4788
|
+
}
|
|
4789
|
+
sink.truncate(marked);
|
|
4790
|
+
return void 0;
|
|
4791
|
+
}
|
|
4719
4792
|
|
|
4720
4793
|
// compiled/src/indexing/index_set_constants.js
|
|
4721
4794
|
var IndexSetConstants = class {
|
|
@@ -5268,43 +5341,63 @@ var StepModelBase = class {
|
|
|
5268
5341
|
}
|
|
5269
5342
|
}
|
|
5270
5343
|
/**
|
|
5271
|
-
*
|
|
5344
|
+
* Resolve an express ID to a local ID, using `hintLocalID + 1` when
|
|
5345
|
+
* the next record is the one we want. Tessellated Faces lists are
|
|
5346
|
+
* sequential (PSB: every faceset), so this turns 9.1M interpolation
|
|
5347
|
+
* searches into one per faceset.
|
|
5272
5348
|
*
|
|
5273
|
-
* @param
|
|
5274
|
-
* @
|
|
5349
|
+
* @param expressID The referenced record.
|
|
5350
|
+
* @param hintLocalID Local ID of the previous resolve, if any.
|
|
5351
|
+
* @return {number | undefined} The local ID, or undefined if unknown.
|
|
5275
5352
|
*/
|
|
5353
|
+
resolveExpressID(expressID, hintLocalID) {
|
|
5354
|
+
if (hintLocalID !== void 0) {
|
|
5355
|
+
const next = hintLocalID + 1;
|
|
5356
|
+
if (next < this.firstInlineElement_ && this.expressID_[next] === expressID) {
|
|
5357
|
+
return next;
|
|
5358
|
+
}
|
|
5359
|
+
}
|
|
5360
|
+
return this.expressIDMap_.get(expressID);
|
|
5361
|
+
}
|
|
5276
5362
|
/**
|
|
5277
|
-
* Append a
|
|
5278
|
-
* without
|
|
5279
|
-
*
|
|
5280
|
-
* This is the reference-level twin of
|
|
5363
|
+
* Append a local record's unsigned-integer list field into `sink`
|
|
5364
|
+
* without an express-ID lookup. The reference-level twin of
|
|
5281
5365
|
* `StepEntityBase.extractIntegerArrayInto`, for hot loops that walk
|
|
5282
5366
|
* millions of small records (tessellated facesets) where constructing
|
|
5283
5367
|
* an entity per record dominates. It deliberately handles only the
|
|
5284
5368
|
* simple case and reports failure otherwise, so callers keep a
|
|
5285
5369
|
* correct fallback rather than this growing subtle special cases:
|
|
5286
|
-
* the
|
|
5287
|
-
*
|
|
5288
|
-
* `offset` as a plain vtable slot equivalent to the generated
|
|
5289
|
-
* getter's `getOffsetCursor( offset, _, _ )` (that only subtracts a
|
|
5290
|
-
* base offset for multi-mapped records).
|
|
5370
|
+
* the record must be single-class (no multi-mapping) and exactly
|
|
5371
|
+
* `expectedTypeID`.
|
|
5291
5372
|
*
|
|
5292
|
-
*
|
|
5293
|
-
*
|
|
5373
|
+
* Field 0 is the data-block start the parser stored in `address_`
|
|
5374
|
+
* (after `TYPE(`). Reading it from the columns skips the descriptor
|
|
5375
|
+
* and vtable `entry()` would retain — on a 9 M-face model those are
|
|
5376
|
+
* the objects the faceset path is trying not to build. Non-zero
|
|
5377
|
+
* offsets still go through a vtable, because only field 0 has that
|
|
5378
|
+
* address identity.
|
|
5379
|
+
*
|
|
5380
|
+
* @param localID The record's local ID.
|
|
5381
|
+
* @param offset The field's vtable offset.
|
|
5294
5382
|
* @param expectedTypeID The type the record must be.
|
|
5295
5383
|
* @param sink Receives the appended values.
|
|
5296
5384
|
* @return {number | undefined} Count appended, or undefined when the
|
|
5297
|
-
* fast path does not apply
|
|
5385
|
+
* fast path does not apply.
|
|
5298
5386
|
*/
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
if (localID === void 0 || localID >= this.count_) {
|
|
5387
|
+
extractIntegerArrayByLocalIDInto(localID, offset, expectedTypeID, sink) {
|
|
5388
|
+
if (localID >= this.count_) {
|
|
5302
5389
|
return void 0;
|
|
5303
5390
|
}
|
|
5304
|
-
|
|
5305
|
-
if (element.multiMapping !== void 0 || element.typeID !== expectedTypeID) {
|
|
5391
|
+
if (this.complexEntries_?.has(localID) || this.typeID_[localID] !== expectedTypeID) {
|
|
5306
5392
|
return void 0;
|
|
5307
5393
|
}
|
|
5394
|
+
if (offset === 0) {
|
|
5395
|
+
const address = this.address_[localID];
|
|
5396
|
+
const acquisition = this.bufferProvider_.acquire(address, this.length_[localID]);
|
|
5397
|
+
const viewAddress = address - acquisition.offset;
|
|
5398
|
+
return extractIntegerArrayAt(acquisition.buffer, viewAddress, acquisition.buffer.length, sink);
|
|
5399
|
+
}
|
|
5400
|
+
const element = this.entry(localID);
|
|
5308
5401
|
if (element.vtableIndex === void 0 && !this.populateVtableEntryRaw(element)) {
|
|
5309
5402
|
return void 0;
|
|
5310
5403
|
}
|
|
@@ -5315,6 +5408,24 @@ var StepModelBase = class {
|
|
|
5315
5408
|
}
|
|
5316
5409
|
return extractIntegerArrayAt(buffer, vtable[element.vtableIndex + offset], buffer.length, sink);
|
|
5317
5410
|
}
|
|
5411
|
+
/**
|
|
5412
|
+
* Append a referenced record's unsigned-integer list field into `sink`
|
|
5413
|
+
* without materialising the referenced entity.
|
|
5414
|
+
*
|
|
5415
|
+
* @param expressID The referenced record's express ID.
|
|
5416
|
+
* @param offset The field's vtable offset within the record.
|
|
5417
|
+
* @param expectedTypeID The type the record must be.
|
|
5418
|
+
* @param sink Receives the appended values.
|
|
5419
|
+
* @return {number | undefined} Count appended, or undefined when the
|
|
5420
|
+
* fast path does not apply.
|
|
5421
|
+
*/
|
|
5422
|
+
extractIntegerArrayByExpressIDInto(expressID, offset, expectedTypeID, sink) {
|
|
5423
|
+
const localID = this.expressIDMap_.get(expressID);
|
|
5424
|
+
if (localID === void 0) {
|
|
5425
|
+
return void 0;
|
|
5426
|
+
}
|
|
5427
|
+
return this.extractIntegerArrayByLocalIDInto(localID, offset, expectedTypeID, sink);
|
|
5428
|
+
}
|
|
5318
5429
|
populateVtableEntryRaw(element) {
|
|
5319
5430
|
if (element.vtableIndex !== void 0 || element.typeID === 0) {
|
|
5320
5431
|
return true;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"coordination_export_order.test.d.ts","sourceRoot":"","sources":["../../../../src/compat/web-ifc/coordination_export_order.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* eslint-disable no-magic-numbers */
|
|
2
|
+
// Export-order independence of the COORDINATE_TO_ORIGIN frame (Share#1749).
|
|
3
|
+
//
|
|
4
|
+
// The recentre anchors on the first geometry the walk reaches, which is
|
|
5
|
+
// whichever element the file happens to declare first. Unsnapped, that
|
|
6
|
+
// makes a model's rendered position a function of its element order:
|
|
7
|
+
// Share shipped the Bldrs logo as both `index.ifc` and `index.step`, and
|
|
8
|
+
// the STEP rendered 76m off the IFC because the two files disagree about
|
|
9
|
+
// which block comes first. Every camera permalink spanning the pair was
|
|
10
|
+
// wrong, and auto-framing hid it — the models look identical until a
|
|
11
|
+
// `#c:` camera pins them.
|
|
12
|
+
//
|
|
13
|
+
// This drives the same surface Share does (the compat IfcAPI, not the
|
|
14
|
+
// CLI's native writer, which has its own recentre convention) and pins
|
|
15
|
+
// the properties the snap has to hold together: a near-origin model
|
|
16
|
+
// keeps the coordinates its file authored — which is what makes two
|
|
17
|
+
// exports of one object coincide — a georeferenced model still
|
|
18
|
+
// recentres, and the classic and streamed opens agree.
|
|
19
|
+
//
|
|
20
|
+
// Not covered here: the AP214 arm. Its placements come back at the
|
|
21
|
+
// origin with the geometry carrying the world transform, so neither
|
|
22
|
+
// placement bounds nor a fixture pair in two element orders reproduces
|
|
23
|
+
// the defect through this surface; the cross-format claim is pinned
|
|
24
|
+
// Share-side instead (`src/Containers/indexStepLogo.spec.ts`, which
|
|
25
|
+
// compares the rendered bounds of `index.ifc` and `index.step`).
|
|
26
|
+
import * as fs from "fs";
|
|
27
|
+
import { beforeAll, describe, expect, test } from "@jest/globals";
|
|
28
|
+
import { LARGE_COORDINATE_BUDGET_M, COORDINATION_SNAP_M } from "./coordination_f64.js";
|
|
29
|
+
import { IfcAPI } from "./ifc_api.js";
|
|
30
|
+
const SETTINGS = { COORDINATE_TO_ORIGIN: true, USE_FAST_BOOLS: true };
|
|
31
|
+
let api;
|
|
32
|
+
let local;
|
|
33
|
+
let georeferenced;
|
|
34
|
+
/**
|
|
35
|
+
* World-space bounds of every placed geometry in a model, as the
|
|
36
|
+
* consumer sees them: the placement translation is what Share stamps
|
|
37
|
+
* onto its instances.
|
|
38
|
+
*
|
|
39
|
+
* @param modelID An open model.
|
|
40
|
+
* @return {{min: number[], max: number[]}} Translation bounds.
|
|
41
|
+
*/
|
|
42
|
+
function placementBounds(modelID) {
|
|
43
|
+
const min = [Infinity, Infinity, Infinity];
|
|
44
|
+
const max = [-Infinity, -Infinity, -Infinity];
|
|
45
|
+
api.StreamAllMeshes(modelID, (mesh) => {
|
|
46
|
+
for (let where = 0; where < mesh.geometries.size(); ++where) {
|
|
47
|
+
const t = mesh.geometries.get(where).flatTransformation;
|
|
48
|
+
for (let axis = 0; axis < 3; ++axis) {
|
|
49
|
+
min[axis] = Math.min(min[axis], t[12 + axis]);
|
|
50
|
+
max[axis] = Math.max(max[axis], t[12 + axis]);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return { min, max };
|
|
55
|
+
}
|
|
56
|
+
beforeAll(async () => {
|
|
57
|
+
api = new IfcAPI();
|
|
58
|
+
await api.Init();
|
|
59
|
+
local = new Uint8Array(fs.readFileSync("data/index.ifc"));
|
|
60
|
+
georeferenced = new Uint8Array(fs.readFileSync("data/index_georeferenced.ifc"));
|
|
61
|
+
}, 120000);
|
|
62
|
+
describe("COORDINATE_TO_ORIGIN export-order independence (Share#1749)", () => {
|
|
63
|
+
test("a near-origin model keeps its authored coordinates", async () => {
|
|
64
|
+
const modelID = await api.OpenModel(local, { ...SETTINGS });
|
|
65
|
+
const { min, max } = placementBounds(modelID);
|
|
66
|
+
expect(Number.isFinite(min[0])).toBe(true);
|
|
67
|
+
// The logo's first-declared block sits at x=76, so the unsnapped
|
|
68
|
+
// frame used to drag the whole model 76m negative. Snapped, the
|
|
69
|
+
// anchor lands in the origin cell and the model stays where the file
|
|
70
|
+
// put it — which is what makes a second export of the same object
|
|
71
|
+
// coincide with it regardless of element order.
|
|
72
|
+
expect(Math.max(...min.map(Math.abs), ...max.map(Math.abs)))
|
|
73
|
+
.toBeLessThan(COORDINATION_SNAP_M);
|
|
74
|
+
expect(min[0]).toBeGreaterThanOrEqual(0);
|
|
75
|
+
api.CloseModel(modelID);
|
|
76
|
+
}, 120000);
|
|
77
|
+
test("a georeferenced model still recentres, inside the budget", async () => {
|
|
78
|
+
const modelID = await api.OpenModel(georeferenced, { ...SETTINGS });
|
|
79
|
+
const { min, max } = placementBounds(modelID);
|
|
80
|
+
// Snapping costs at most half a cell on top of the model's own
|
|
81
|
+
// extent; the budget is the threshold above which float32 jitter
|
|
82
|
+
// becomes visible (Share#1631).
|
|
83
|
+
expect(Math.max(...min.map(Math.abs), ...max.map(Math.abs)))
|
|
84
|
+
.toBeLessThan(LARGE_COORDINATE_BUDGET_M);
|
|
85
|
+
// The applied-frame report (Share#1634) is not asserted here: only
|
|
86
|
+
// the deferred pump records it, and
|
|
87
|
+
// `ifc_api_preview_coordination.test.ts` already covers that path.
|
|
88
|
+
api.CloseModel(modelID);
|
|
89
|
+
}, 120000);
|
|
90
|
+
test("the classic and streamed opens agree on the frame", async () => {
|
|
91
|
+
// Both paths derive through deriveCoordinationF64, so the snap has
|
|
92
|
+
// to leave them identical — otherwise one model renders in two
|
|
93
|
+
// places depending on which open Share happened to take. Compared
|
|
94
|
+
// through the emitted placements rather than
|
|
95
|
+
// GetAppliedCoordinationMatrix, which only the deferred pump fills
|
|
96
|
+
// in: on these two opens it reports the identity contract for both,
|
|
97
|
+
// so comparing it would pass no matter what the frames did.
|
|
98
|
+
const classicID = await api.OpenModel(georeferenced, { ...SETTINGS });
|
|
99
|
+
const classic = placementBounds(classicID);
|
|
100
|
+
api.CloseModel(classicID);
|
|
101
|
+
const streamedID = await api.OpenModelStreamed(georeferenced, { ...SETTINGS });
|
|
102
|
+
const streamed = placementBounds(streamedID);
|
|
103
|
+
api.CloseModel(streamedID);
|
|
104
|
+
expect(Number.isFinite(classic.min[0])).toBe(true);
|
|
105
|
+
for (let axis = 0; axis < 3; ++axis) {
|
|
106
|
+
expect(streamed.min[axis]).toBeCloseTo(classic.min[axis], 6);
|
|
107
|
+
expect(streamed.max[axis]).toBeCloseTo(classic.max[axis], 6);
|
|
108
|
+
}
|
|
109
|
+
}, 120000);
|
|
110
|
+
});
|
|
@@ -45,10 +45,69 @@ export interface Point3Like {
|
|
|
45
45
|
* @return {number[]} The product as a fresh 16-element `number[]`.
|
|
46
46
|
*/
|
|
47
47
|
export declare function mat4MultiplyF64(a: ArrayLike<number>, b: ArrayLike<number>): number[];
|
|
48
|
+
/**
|
|
49
|
+
* Grid (metres) a recentre snaps to once it is needed at all.
|
|
50
|
+
*
|
|
51
|
+
* The anchor a recentre is derived from is the *first geometry* the walk
|
|
52
|
+
* reaches, which is an arbitrary interior element: whichever one the file
|
|
53
|
+
* happens to declare first. Left as-is, that makes a model's world
|
|
54
|
+
* position a function of its element order, with two consequences users
|
|
55
|
+
* see (conway#87, "not a sound basis for a camera in the permalink"):
|
|
56
|
+
*
|
|
57
|
+
* - Two exports of one object — the same logo as IFC and as STEP, a part
|
|
58
|
+
* re-exported by a different CAD kernel — land in different places,
|
|
59
|
+
* because they disagree about which element comes first. Share#1749
|
|
60
|
+
* hit exactly this: `index.step` sat 76m off `index.ifc` because the
|
|
61
|
+
* IFC declares the x=76 block first and the STEP the x=0 one.
|
|
62
|
+
* - Re-exporting a model with the element order shuffled moves it, so
|
|
63
|
+
* every camera permalink saved against it is silently wrong.
|
|
64
|
+
*
|
|
65
|
+
* `quantizeRecentre` answers that in two stages, because the two ranges
|
|
66
|
+
* want different things:
|
|
67
|
+
*
|
|
68
|
+
* - **Inside LARGE_COORDINATE_BUDGET_M, do not recentre at all.** There
|
|
69
|
+
* is no float32 benefit below the budget — that constant *is* the
|
|
70
|
+
* threshold where quantization becomes visible — so a model within
|
|
71
|
+
* 10km of the origin simply keeps the coordinates its file authored.
|
|
72
|
+
* That is model-zero, conway#87's proposal, and it makes the frame
|
|
73
|
+
* exactly order-independent for the overwhelming majority of models
|
|
74
|
+
* rather than order-independent-per-cell.
|
|
75
|
+
* - **Above it, snap to this grid.** A georeferenced model has to come
|
|
76
|
+
* back near the origin, and snapping keeps that repeatable across
|
|
77
|
+
* exports. Half a cell (500m) out of a 1e4 m budget costs ~0.05mm of
|
|
78
|
+
* float32 resolution, and a whole-kilometre translation is exactly
|
|
79
|
+
* representable where the raw anchor was not, so the recentre itself
|
|
80
|
+
* stops contributing rounding.
|
|
81
|
+
*
|
|
82
|
+
* **Quantizing cannot remove the discontinuity, only move it**, and it
|
|
83
|
+
* is worth being precise about where this one sits, because it is not
|
|
84
|
+
* where you would guess. Snapping *everything* to the grid would put a
|
|
85
|
+
* 1km step at every cell edge, including around the near-origin models
|
|
86
|
+
* that make up almost the whole corpus. Staging moves it to the budget:
|
|
87
|
+
* inside, every anchor derives model-zero and there is no edge at all;
|
|
88
|
+
* the single remaining edge is the budget itself, where the step is
|
|
89
|
+
* `LARGE_COORDINATE_BUDGET_M` (1e4), not the grid (1e3) — an anchor at
|
|
90
|
+
* 9900 derives zero and one at 10100 derives -10000.
|
|
91
|
+
*
|
|
92
|
+
* That edge is also invisible to the adopted-preview-frame gate, which
|
|
93
|
+
* re-derives only when a durable placement lands beyond the budget: a
|
|
94
|
+
* preview anchored at 10100 and a durable first placement at 9900 probe
|
|
95
|
+
* at ~100m, so the preview frame is kept and the model renders 10km off
|
|
96
|
+
* a classic open. The trade is deliberate — one edge that a georeferenced
|
|
97
|
+
* model may sit near, rather than an edge every kilometre through the
|
|
98
|
+
* range where nearly every model lives — but it is a real residual, not
|
|
99
|
+
* an eliminated one. design/new/coordination-frame.md tracks it, and
|
|
100
|
+
* conway#87's relative-to-centre direction is what removes it properly.
|
|
101
|
+
*/
|
|
102
|
+
export declare const COORDINATION_SNAP_M = 1000;
|
|
48
103
|
/**
|
|
49
104
|
* Derive the coordination (recentre) matrix in float64, matching the
|
|
50
105
|
* gl-matrix op sequence exactly:
|
|
51
|
-
* `scale * NormalizeMat * translate(-(placement * point))`.
|
|
106
|
+
* `scale * NormalizeMat * translate(-quantize(placement * point))`.
|
|
107
|
+
*
|
|
108
|
+
* The quantization is what keeps the derived frame independent of which
|
|
109
|
+
* element a file declares first — see COORDINATION_SNAP_M for why that
|
|
110
|
+
* matters, and why it is staged around LARGE_COORDINATE_BUDGET_M.
|
|
52
111
|
*
|
|
53
112
|
* @param placement The native placement (16 elements, column-major
|
|
54
113
|
* float64 from `getValues()`); `undefined` is treated as identity.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coordination_f64.d.ts","sourceRoot":"","sources":["../../../../src/compat/web-ifc/coordination_f64.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAiDH;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC3B,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAkBxD;AAED
|
|
1
|
+
{"version":3,"file":"coordination_f64.d.ts","sourceRoot":"","sources":["../../../../src/compat/web-ifc/coordination_f64.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAiDH;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;CACV;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC3B,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAkBxD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,eAAO,MAAM,mBAAmB,OAAM,CAAA;AAoCtC;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,qBAAqB,CACjC,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,SAAS,EACxC,KAAK,EAAE,UAAU,EACjB,YAAY,EAAE,SAAS,CAAC,MAAM,CAAC,EAC/B,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAgCjC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,mBAAmB,CAC/B,YAAY,EAAE,SAAS,CAAC,MAAM,CAAC,EAC/B,SAAS,EAAE,SAAS,CAAC,MAAM,CAAC,GAAG,SAAS,EACxC,UAAU,CAAC,EAAE,UAAU,GAAG,MAAM,EAAE,CAerC;AAGD,eAAO,MAAM,aAAa,KAAK,CAAA;AAC/B,eAAO,MAAM,aAAa,KAAK,CAAA;AAC/B,eAAO,MAAM,aAAa,KAAK,CAAA;AAE/B;;;;;;;GAOG;AACH,eAAO,MAAM,yBAAyB,QAAM,CAAA"}
|
|
@@ -92,10 +92,99 @@ export function mat4MultiplyF64(a, b) {
|
|
|
92
92
|
}
|
|
93
93
|
return out;
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Grid (metres) a recentre snaps to once it is needed at all.
|
|
97
|
+
*
|
|
98
|
+
* The anchor a recentre is derived from is the *first geometry* the walk
|
|
99
|
+
* reaches, which is an arbitrary interior element: whichever one the file
|
|
100
|
+
* happens to declare first. Left as-is, that makes a model's world
|
|
101
|
+
* position a function of its element order, with two consequences users
|
|
102
|
+
* see (conway#87, "not a sound basis for a camera in the permalink"):
|
|
103
|
+
*
|
|
104
|
+
* - Two exports of one object — the same logo as IFC and as STEP, a part
|
|
105
|
+
* re-exported by a different CAD kernel — land in different places,
|
|
106
|
+
* because they disagree about which element comes first. Share#1749
|
|
107
|
+
* hit exactly this: `index.step` sat 76m off `index.ifc` because the
|
|
108
|
+
* IFC declares the x=76 block first and the STEP the x=0 one.
|
|
109
|
+
* - Re-exporting a model with the element order shuffled moves it, so
|
|
110
|
+
* every camera permalink saved against it is silently wrong.
|
|
111
|
+
*
|
|
112
|
+
* `quantizeRecentre` answers that in two stages, because the two ranges
|
|
113
|
+
* want different things:
|
|
114
|
+
*
|
|
115
|
+
* - **Inside LARGE_COORDINATE_BUDGET_M, do not recentre at all.** There
|
|
116
|
+
* is no float32 benefit below the budget — that constant *is* the
|
|
117
|
+
* threshold where quantization becomes visible — so a model within
|
|
118
|
+
* 10km of the origin simply keeps the coordinates its file authored.
|
|
119
|
+
* That is model-zero, conway#87's proposal, and it makes the frame
|
|
120
|
+
* exactly order-independent for the overwhelming majority of models
|
|
121
|
+
* rather than order-independent-per-cell.
|
|
122
|
+
* - **Above it, snap to this grid.** A georeferenced model has to come
|
|
123
|
+
* back near the origin, and snapping keeps that repeatable across
|
|
124
|
+
* exports. Half a cell (500m) out of a 1e4 m budget costs ~0.05mm of
|
|
125
|
+
* float32 resolution, and a whole-kilometre translation is exactly
|
|
126
|
+
* representable where the raw anchor was not, so the recentre itself
|
|
127
|
+
* stops contributing rounding.
|
|
128
|
+
*
|
|
129
|
+
* **Quantizing cannot remove the discontinuity, only move it**, and it
|
|
130
|
+
* is worth being precise about where this one sits, because it is not
|
|
131
|
+
* where you would guess. Snapping *everything* to the grid would put a
|
|
132
|
+
* 1km step at every cell edge, including around the near-origin models
|
|
133
|
+
* that make up almost the whole corpus. Staging moves it to the budget:
|
|
134
|
+
* inside, every anchor derives model-zero and there is no edge at all;
|
|
135
|
+
* the single remaining edge is the budget itself, where the step is
|
|
136
|
+
* `LARGE_COORDINATE_BUDGET_M` (1e4), not the grid (1e3) — an anchor at
|
|
137
|
+
* 9900 derives zero and one at 10100 derives -10000.
|
|
138
|
+
*
|
|
139
|
+
* That edge is also invisible to the adopted-preview-frame gate, which
|
|
140
|
+
* re-derives only when a durable placement lands beyond the budget: a
|
|
141
|
+
* preview anchored at 10100 and a durable first placement at 9900 probe
|
|
142
|
+
* at ~100m, so the preview frame is kept and the model renders 10km off
|
|
143
|
+
* a classic open. The trade is deliberate — one edge that a georeferenced
|
|
144
|
+
* model may sit near, rather than an edge every kilometre through the
|
|
145
|
+
* range where nearly every model lives — but it is a real residual, not
|
|
146
|
+
* an eliminated one. design/new/coordination-frame.md tracks it, and
|
|
147
|
+
* conway#87's relative-to-centre direction is what removes it properly.
|
|
148
|
+
*/
|
|
149
|
+
export const COORDINATION_SNAP_M = 1e3;
|
|
150
|
+
/**
|
|
151
|
+
* The recentre for an anchor: all zeros while the anchor is close enough
|
|
152
|
+
* to the origin not to need one, else each component snapped to
|
|
153
|
+
* COORDINATION_SNAP_M.
|
|
154
|
+
*
|
|
155
|
+
* The stage decision is made **once, on the anchor's distance from the
|
|
156
|
+
* origin**, and applied to all three components together. Deciding per
|
|
157
|
+
* component would leave a model offset diagonally — say (9km, 9km, 9km),
|
|
158
|
+
* 15.6km out and well past the budget — with no recentre at all, since
|
|
159
|
+
* no single component crosses the threshold. That silently regresses the
|
|
160
|
+
* float32 jitter fix (Share#1631) the budget exists to enforce.
|
|
161
|
+
*
|
|
162
|
+
* Works in source units: the values are pre-scale, so both the budget
|
|
163
|
+
* and the grid convert by the same `scaleFactor` the caller is about to
|
|
164
|
+
* apply (a millimetre model snaps every 1e6 source units, a metre model
|
|
165
|
+
* every 1e3).
|
|
166
|
+
*
|
|
167
|
+
* @param values The three translation components, in source units.
|
|
168
|
+
* @param scaleFactor The linear scaling factor to metres; already
|
|
169
|
+
* sanitized by the caller.
|
|
170
|
+
* @return {number[]} The recentre per component, in source units.
|
|
171
|
+
*/
|
|
172
|
+
function quantizeRecentre(values, scaleFactor) {
|
|
173
|
+
const finite = values.map((v) => (Number.isFinite(v) ? v : 0));
|
|
174
|
+
if (Math.hypot(...finite) * scaleFactor <= LARGE_COORDINATE_BUDGET_M) {
|
|
175
|
+
return [0, 0, 0];
|
|
176
|
+
}
|
|
177
|
+
const gridSourceUnits = COORDINATION_SNAP_M / scaleFactor;
|
|
178
|
+
return finite.map((v) => Math.round(v / gridSourceUnits) * gridSourceUnits);
|
|
179
|
+
}
|
|
95
180
|
/**
|
|
96
181
|
* Derive the coordination (recentre) matrix in float64, matching the
|
|
97
182
|
* gl-matrix op sequence exactly:
|
|
98
|
-
* `scale * NormalizeMat * translate(-(placement * point))`.
|
|
183
|
+
* `scale * NormalizeMat * translate(-quantize(placement * point))`.
|
|
184
|
+
*
|
|
185
|
+
* The quantization is what keeps the derived frame independent of which
|
|
186
|
+
* element a file declares first — see COORDINATION_SNAP_M for why that
|
|
187
|
+
* matters, and why it is staged around LARGE_COORDINATE_BUDGET_M.
|
|
99
188
|
*
|
|
100
189
|
* @param placement The native placement (16 elements, column-major
|
|
101
190
|
* float64 from `getValues()`); `undefined` is treated as identity.
|
|
@@ -107,20 +196,26 @@ export function mat4MultiplyF64(a, b) {
|
|
|
107
196
|
export function deriveCoordinationF64(placement, point, normalizeMat, scaleFactor) {
|
|
108
197
|
const p = placement !== undefined ? sanitize16(placement) : IDENTITY;
|
|
109
198
|
const { x, y, z } = point;
|
|
199
|
+
// Sanitized once, here, so the promise the rest of this function makes
|
|
200
|
+
// about degenerate input actually holds: a NaN/Infinite factor from a
|
|
201
|
+
// malformed unit-assignment chain would otherwise pass the recentre
|
|
202
|
+
// guard and then poison every component through the scale matrix below.
|
|
203
|
+
const scale1 = Number.isFinite(scaleFactor) && scaleFactor > 0 ? scaleFactor : 1;
|
|
110
204
|
// transformedPt = placement * (x, y, z, 1)
|
|
111
205
|
const tx = p[0] * x + p[4] * y + p[8] * z + p[12];
|
|
112
206
|
const ty = p[1] * x + p[5] * y + p[9] * z + p[13];
|
|
113
207
|
const tz = p[2] * x + p[6] * y + p[10] * z + p[14];
|
|
114
|
-
// translate(-transformedPt)
|
|
208
|
+
// translate(-quantize(transformedPt))
|
|
209
|
+
const recentre = quantizeRecentre([-tx, -ty, -tz], scale1);
|
|
115
210
|
const translate = IDENTITY.slice();
|
|
116
|
-
translate[12] =
|
|
117
|
-
translate[13] =
|
|
118
|
-
translate[14] =
|
|
211
|
+
translate[12] = recentre[0];
|
|
212
|
+
translate[13] = recentre[1];
|
|
213
|
+
translate[14] = recentre[2];
|
|
119
214
|
// scale(scaleFactor)
|
|
120
215
|
const scale = [
|
|
121
|
-
|
|
122
|
-
0,
|
|
123
|
-
0, 0,
|
|
216
|
+
scale1, 0, 0, 0,
|
|
217
|
+
0, scale1, 0, 0,
|
|
218
|
+
0, 0, scale1, 0,
|
|
124
219
|
0, 0, 0, 1,
|
|
125
220
|
];
|
|
126
221
|
// scale * (NormalizeMat * translate)
|