@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.
Files changed (37) hide show
  1. package/compiled/Dist/ConwayGeomWasmNode.js +0 -0
  2. package/compiled/Dist/ConwayGeomWasmNodeMT.js +0 -0
  3. package/compiled/Dist/ConwayGeomWasmWeb.js +0 -0
  4. package/compiled/Dist/ConwayGeomWasmWebMT.js +1 -1
  5. package/compiled/Dist/ConwayGeomWasmWebMT.wasm +0 -0
  6. package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmNode.js +0 -0
  7. package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmNodeMT.js +0 -0
  8. package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmWeb.js +0 -0
  9. package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmWebMT.js +1 -1
  10. package/compiled/dependencies/conway-geom/Dist/ConwayGeomWasmWebMT.wasm +0 -0
  11. package/compiled/dependencies/conway-geom/interface/conway_geometry.d.ts +19 -0
  12. package/compiled/dependencies/conway-geom/interface/conway_geometry.d.ts.map +1 -1
  13. package/compiled/dependencies/conway-geom/interface/conway_geometry.js +17 -0
  14. package/compiled/examples/browser-bundled.cjs +132 -21
  15. package/compiled/examples/cli-bundled.cjs +189 -57
  16. package/compiled/examples/cli-step-bundled.cjs +156 -28
  17. package/compiled/examples/validator-bundled.cjs +132 -21
  18. package/compiled/src/compat/web-ifc/coordination_export_order.test.d.ts +2 -0
  19. package/compiled/src/compat/web-ifc/coordination_export_order.test.d.ts.map +1 -0
  20. package/compiled/src/compat/web-ifc/coordination_export_order.test.js +110 -0
  21. package/compiled/src/compat/web-ifc/coordination_f64.d.ts +60 -1
  22. package/compiled/src/compat/web-ifc/coordination_f64.d.ts.map +1 -1
  23. package/compiled/src/compat/web-ifc/coordination_f64.js +103 -8
  24. package/compiled/src/compat/web-ifc/coordination_f64.test.js +115 -30
  25. package/compiled/src/ifc/ifc_geometry_extraction.d.ts +0 -1
  26. package/compiled/src/ifc/ifc_geometry_extraction.d.ts.map +1 -1
  27. package/compiled/src/ifc/ifc_geometry_extraction.js +34 -44
  28. package/compiled/src/step/parsing/uint32_sink.d.ts +20 -0
  29. package/compiled/src/step/parsing/uint32_sink.d.ts.map +1 -1
  30. package/compiled/src/step/parsing/uint32_sink.js +95 -0
  31. package/compiled/src/step/parsing/uint32_sink.test.js +41 -1
  32. package/compiled/src/step/step_model_base.d.ts +32 -13
  33. package/compiled/src/step/step_model_base.d.ts.map +1 -1
  34. package/compiled/src/step/step_model_base.js +64 -24
  35. package/compiled/src/version/version.js +1 -1
  36. package/compiled/tsconfig.tsbuildinfo +1 -1
  37. 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.502.1421";
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
- * Populate a raw vtable entry for a particular element, extra
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 element The raw elment to populate the vtable entry for.
5274
- * @return {boolean} Did the vtable entry populate correctly?
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 referenced record's unsigned-integer list field into `sink`
5278
- * without materialising the referenced entity.
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 reference must resolve, be single-class (no multi-mapping),
5287
- * and be exactly `expectedTypeID` — which is what makes reading
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
- * @param expressID The referenced record's express ID.
5293
- * @param offset The field's vtable offset within the record.
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 and the caller must fall back.
5385
+ * fast path does not apply.
5298
5386
  */
5299
- extractIntegerArrayByExpressIDInto(expressID, offset, expectedTypeID, sink) {
5300
- const localID = this.expressIDMap_.get(expressID);
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
- const element = this.entry(localID);
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=coordination_export_order.test.d.ts.map
@@ -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;;;;;;;;;;;GAWG;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,CA0BjC;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"}
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] = -tx;
117
- translate[13] = -ty;
118
- translate[14] = -tz;
211
+ translate[12] = recentre[0];
212
+ translate[13] = recentre[1];
213
+ translate[14] = recentre[2];
119
214
  // scale(scaleFactor)
120
215
  const scale = [
121
- scaleFactor, 0, 0, 0,
122
- 0, scaleFactor, 0, 0,
123
- 0, 0, scaleFactor, 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)