@ifc-lite/bcf 3.0.0 → 4.0.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ifc-lite/bcf
2
2
 
3
- BCF (BIM Collaboration Format) support for IFClite. Reads and writes BCF 2.1 and 3.0 files — the issue-tracking format every BIM tool speaks (Revit, Archicad, Solibri, BIMcollab, etc.).
3
+ BCF (BIM Collaboration Format) support for IFClite. Reads and writes BCF 2.1 and 3.0 files — the topic-tracking format every BIM tool speaks (Revit, Archicad, Solibri, BIMcollab, etc.).
4
4
 
5
5
  ## Installation
6
6
 
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * @ifc-lite/bcf - BIM Collaboration Format support
3
3
  *
4
4
  * This package provides BCF (BIM Collaboration Format) support for IFC-Lite.
5
- * It implements BCF 2.1 and 3.0 specifications for issue tracking in BIM projects.
5
+ * It implements BCF 2.1 and 3.0 specifications for topic tracking in BIM projects.
6
6
  *
7
7
  * @see https://github.com/buildingSMART/BCF-XML
8
8
  * @see https://www.buildingsmart.org/standards/bsi-standards/bim-collaboration-format/
@@ -28,7 +28,7 @@ export declare function createBCFProject(options?: {
28
28
  version?: '2.1' | '3.0';
29
29
  }): BCFProject;
30
30
  /**
31
- * Create a new BCF topic (issue)
31
+ * Create a new BCF topic
32
32
  */
33
33
  export declare function createBCFTopic(options: {
34
34
  title: string;
package/dist/index.js CHANGED
@@ -24,7 +24,7 @@ export function createBCFProject(options) {
24
24
  };
25
25
  }
26
26
  /**
27
- * Create a new BCF topic (issue)
27
+ * Create a new BCF topic
28
28
  */
29
29
  export function createBCFTopic(options) {
30
30
  return {
@@ -0,0 +1,6 @@
1
+ import type { BCFViewpoint } from './types.js';
2
+ /**
3
+ * Parse viewpoint XML content
4
+ */
5
+ export declare function parseViewpointContent(content: string, versionId: '2.1' | '3.0'): BCFViewpoint | null;
6
+ //# sourceMappingURL=reader-viewpoint-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reader-viewpoint-content.d.ts","sourceRoot":"","sources":["../src/reader-viewpoint-content.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACV,YAAY,EAQb,MAAM,YAAY,CAAC;AAIpB;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,GAAG,KAAK,GAAG,YAAY,GAAG,IAAI,CAgCpG"}
@@ -0,0 +1,250 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+ /**
5
+ * Parsing of a single BCFV viewpoint file's XML content: the two camera
6
+ * shapes, the shared point/direction primitive, and the geometry overlays
7
+ * (lines, clipping planes, bitmaps). Split out of reader.ts, which still owns
8
+ * the zip-level orchestration (locating .bcfv files, resolving snapshots) that
9
+ * calls into {@link parseViewpointContent}.
10
+ */
11
+ import { extractElement } from './xml-text.js';
12
+ import { parseFiniteFloat } from './numeric.js';
13
+ import { parseComponents } from './reader-components.js';
14
+ /**
15
+ * Parse viewpoint XML content
16
+ */
17
+ export function parseViewpointContent(content, versionId) {
18
+ // Extract viewpoint GUID from root element (Guid can be anywhere in the tag)
19
+ const guidMatch = content.match(/<VisualizationInfo[^>]+Guid="([^"]+)"/);
20
+ const guid = guidMatch?.[1] || crypto.randomUUID?.() || `vp-${Date.now()}`;
21
+ // Parse perspective camera
22
+ const perspectiveCamera = parsePerspectiveCamera(content);
23
+ // Parse orthogonal camera
24
+ const orthogonalCamera = parseOrthogonalCamera(content);
25
+ // Parse components
26
+ const components = parseComponents(content, versionId);
27
+ // Parse lines
28
+ const lines = parseLines(content);
29
+ // Parse clipping planes
30
+ const clippingPlanes = parseClippingPlanes(content);
31
+ // Parse bitmaps
32
+ const bitmaps = parseBitmaps(content);
33
+ return {
34
+ guid,
35
+ perspectiveCamera,
36
+ orthogonalCamera,
37
+ components,
38
+ lines: lines.length > 0 ? lines : undefined,
39
+ clippingPlanes: clippingPlanes.length > 0 ? clippingPlanes : undefined,
40
+ bitmaps: bitmaps.length > 0 ? bitmaps : undefined,
41
+ };
42
+ }
43
+ /**
44
+ * Parse perspective camera from viewpoint content
45
+ */
46
+ function parsePerspectiveCamera(content) {
47
+ const match = content.match(/<PerspectiveCamera>([\s\S]*?)<\/PerspectiveCamera>/);
48
+ if (!match)
49
+ return undefined;
50
+ const cameraContent = match[1];
51
+ const viewPoint = parsePoint(cameraContent, 'CameraViewPoint');
52
+ const direction = parseDirection(cameraContent, 'CameraDirection');
53
+ const upVector = parseDirection(cameraContent, 'CameraUpVector');
54
+ const fieldOfView = extractElement(cameraContent, 'FieldOfView');
55
+ if (!viewPoint || !direction || !upVector || !fieldOfView) {
56
+ return undefined;
57
+ }
58
+ // Same treatment as the coordinates: an unusable scalar is a missing one.
59
+ // `fieldOfView` is converted to radians and handed to the viewer camera,
60
+ // and the parser already drops the whole camera when the element is absent.
61
+ const fov = parseFiniteFloat(fieldOfView);
62
+ if (fov === undefined)
63
+ return undefined;
64
+ return {
65
+ cameraViewPoint: viewPoint,
66
+ cameraDirection: direction,
67
+ cameraUpVector: upVector,
68
+ fieldOfView: fov,
69
+ ...parseAspectRatio(cameraContent),
70
+ };
71
+ }
72
+ /**
73
+ * Parse the optional `<AspectRatio>` of either camera type.
74
+ *
75
+ * BCF 3.0's visinfo.xsd makes `AspectRatio` (a `PositiveDouble`) a REQUIRED
76
+ * child of both camera types; BCF 2.1 has no such element. It is read here for
77
+ * both, since the element's presence — not the archive's declared version — is
78
+ * what says whether there is a value to keep.
79
+ *
80
+ * Reading it matters beyond fidelity: the writer refuses to emit a 3.0 camera
81
+ * without one, so a 3.0 archive from another tool could otherwise be read and
82
+ * then not written back. Returned as a spread-able partial so an absent or
83
+ * unusable value leaves the property off entirely rather than setting it to
84
+ * `undefined`, and a non-positive value is dropped rather than carried into
85
+ * output the schema would reject.
86
+ */
87
+ function parseAspectRatio(cameraContent) {
88
+ const raw = extractElement(cameraContent, 'AspectRatio');
89
+ if (!raw)
90
+ return {};
91
+ const value = parseFiniteFloat(raw);
92
+ if (value === undefined || !(value > 0))
93
+ return {};
94
+ return { aspectRatio: value };
95
+ }
96
+ /**
97
+ * Parse orthogonal camera from viewpoint content
98
+ */
99
+ function parseOrthogonalCamera(content) {
100
+ const match = content.match(/<OrthogonalCamera>([\s\S]*?)<\/OrthogonalCamera>/);
101
+ if (!match)
102
+ return undefined;
103
+ const cameraContent = match[1];
104
+ const viewPoint = parsePoint(cameraContent, 'CameraViewPoint');
105
+ const direction = parseDirection(cameraContent, 'CameraDirection');
106
+ const upVector = parseDirection(cameraContent, 'CameraUpVector');
107
+ const viewToWorldScale = extractElement(cameraContent, 'ViewToWorldScale');
108
+ if (!viewPoint || !direction || !upVector || !viewToWorldScale) {
109
+ return undefined;
110
+ }
111
+ // `viewToWorldScale` becomes the orthographic half-height, which is the
112
+ // value `getOrthoSize()` hands back into a saved viewpoint — so a
113
+ // non-finite one persists past the session if it is allowed in (#2461).
114
+ const scale = parseFiniteFloat(viewToWorldScale);
115
+ if (scale === undefined)
116
+ return undefined;
117
+ return {
118
+ cameraViewPoint: viewPoint,
119
+ cameraDirection: direction,
120
+ cameraUpVector: upVector,
121
+ viewToWorldScale: scale,
122
+ ...parseAspectRatio(cameraContent),
123
+ };
124
+ }
125
+ /**
126
+ * Parse a 3D point from XML
127
+ */
128
+ function parsePoint(content, elementName) {
129
+ const match = content.match(new RegExp(`<${elementName}>([\\s\\S]*?)<\\/${elementName}>`));
130
+ if (!match)
131
+ return undefined;
132
+ const x = extractElement(match[1], 'X');
133
+ const y = extractElement(match[1], 'Y');
134
+ const z = extractElement(match[1], 'Z');
135
+ if (x === undefined || y === undefined || z === undefined) {
136
+ return undefined;
137
+ }
138
+ // A coordinate that is not a real number is treated as a missing one.
139
+ // `parseFloat` has no out-of-band failure value — `"NaN"` parses to `NaN`
140
+ // and the well-formed literal `"1e999"` parses to `Infinity` — and from here
141
+ // the value reaches `Camera.setPosition`/`setTarget`, which store a pose
142
+ // verbatim by design. Once stored, a single non-finite coordinate spreads
143
+ // across the whole pose on the next gesture. Rejecting at the file boundary
144
+ // means it never gets there, and it costs no new branch: every caller
145
+ // already drops the thing it was parsing when a coordinate is missing
146
+ // (#2466).
147
+ const px = parseFiniteFloat(x);
148
+ const py = parseFiniteFloat(y);
149
+ const pz = parseFiniteFloat(z);
150
+ if (px === undefined || py === undefined || pz === undefined) {
151
+ return undefined;
152
+ }
153
+ return { x: px, y: py, z: pz };
154
+ }
155
+ /**
156
+ * Parse a 3D direction from XML
157
+ */
158
+ function parseDirection(content, elementName) {
159
+ return parsePoint(content, elementName);
160
+ }
161
+ /**
162
+ * Parse lines
163
+ */
164
+ function parseLines(content) {
165
+ const lines = [];
166
+ const linesMatch = content.match(/<Lines>([\s\S]*?)<\/Lines>/);
167
+ if (!linesMatch)
168
+ return lines;
169
+ const lineMatches = linesMatch[1].matchAll(/<Line>([\s\S]*?)<\/Line>/g);
170
+ for (const match of lineMatches) {
171
+ const startPoint = parsePoint(match[1], 'StartPoint');
172
+ const endPoint = parsePoint(match[1], 'EndPoint');
173
+ if (startPoint && endPoint) {
174
+ lines.push({ startPoint, endPoint });
175
+ }
176
+ }
177
+ return lines;
178
+ }
179
+ /**
180
+ * Parse clipping planes
181
+ */
182
+ function parseClippingPlanes(content) {
183
+ const planes = [];
184
+ const planesMatch = content.match(/<ClippingPlanes>([\s\S]*?)<\/ClippingPlanes>/);
185
+ if (!planesMatch)
186
+ return planes;
187
+ const planeMatches = planesMatch[1].matchAll(/<ClippingPlane>([\s\S]*?)<\/ClippingPlane>/g);
188
+ for (const match of planeMatches) {
189
+ const location = parsePoint(match[1], 'Location');
190
+ const direction = parseDirection(match[1], 'Direction');
191
+ if (location && direction) {
192
+ planes.push({ location, direction });
193
+ }
194
+ }
195
+ return planes;
196
+ }
197
+ /**
198
+ * Parse bitmaps
199
+ *
200
+ * The two BCF versions diverge in shape (see writer.ts's writeBitmap/
201
+ * writeViewpointFiles for the write side of this):
202
+ * - BCF 3.0: entries sit inside a `<Bitmaps>` wrapper, and the per-entry
203
+ * format element is named `<Format>`. No tag inside an entry shares the
204
+ * entry's own name, so a plain non-greedy `<Bitmap>...</Bitmap>` match
205
+ * is unambiguous.
206
+ * - BCF 2.1: entries sit DIRECTLY under `<VisualizationInfo>` (no wrapper),
207
+ * and the format element is confusingly also named `<Bitmap>`, nested one
208
+ * level inside the entry (`<Bitmap><Bitmap>PNG</Bitmap><Reference>...`).
209
+ * A naive non-greedy `<Bitmap>...</Bitmap>` match on that shape terminates
210
+ * at the FIRST `</Bitmap>` it sees -- the inner format tag's closing tag,
211
+ * not the entry's -- and silently drops the rest of the entry. It must be
212
+ * matched with an explicit two-level pattern instead.
213
+ */
214
+ function parseBitmaps(content) {
215
+ const bitmaps = [];
216
+ const bitmapsMatch = content.match(/<Bitmaps>([\s\S]*?)<\/Bitmaps>/);
217
+ const pushBitmap = (format, body) => {
218
+ const reference = extractElement(body, 'Reference');
219
+ const location = parsePoint(body, 'Location');
220
+ const normal = parseDirection(body, 'Normal');
221
+ const up = parseDirection(body, 'Up');
222
+ const height = extractElement(body, 'Height');
223
+ const parsedHeight = height === undefined ? undefined : parseFiniteFloat(height);
224
+ if (format && reference && location && normal && up && parsedHeight !== undefined) {
225
+ bitmaps.push({
226
+ format: format.toUpperCase() === 'JPG' ? 'JPG' : 'PNG',
227
+ reference,
228
+ location,
229
+ normal,
230
+ up,
231
+ height: parsedHeight,
232
+ });
233
+ }
234
+ };
235
+ if (bitmapsMatch) {
236
+ // BCF 3.0 shape: <Bitmaps><Bitmap><Format>...</Format>...</Bitmap>...</Bitmaps>
237
+ for (const match of bitmapsMatch[1].matchAll(/<Bitmap>([\s\S]*?)<\/Bitmap>/g)) {
238
+ pushBitmap(extractElement(match[1], 'Format'), match[1]);
239
+ }
240
+ }
241
+ else {
242
+ // BCF 2.1 shape: <Bitmap><Bitmap>PNG</Bitmap>...</Bitmap>, unwrapped,
243
+ // directly under VisualizationInfo.
244
+ for (const match of content.matchAll(/<Bitmap>\s*<Bitmap>([\s\S]*?)<\/Bitmap>([\s\S]*?)<\/Bitmap>/g)) {
245
+ pushBitmap(match[1], match[2]);
246
+ }
247
+ }
248
+ return bitmaps;
249
+ }
250
+ //# sourceMappingURL=reader-viewpoint-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reader-viewpoint-content.js","sourceRoot":"","sources":["../src/reader-viewpoint-content.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../src/reader.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACV,UAAU,EAgBX,MAAM,YAAY,CAAC;AAyIpB;;;;;;GAMG;AACH,wBAAsB,OAAO,CAC3B,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,WAAW,GAAG,UAAU,EAC5C,MAAM,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,GACpF,OAAO,CAAC,UAAU,CAAC,CA4CrB"}
1
+ {"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../src/reader.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,UAAU,EASX,MAAM,YAAY,CAAC;AAyIpB;;;;;;GAMG;AACH,wBAAsB,OAAO,CAC3B,IAAI,EAAE,IAAI,GAAG,IAAI,GAAG,WAAW,GAAG,UAAU,EAC5C,MAAM,CAAC,EAAE;IAAE,eAAe,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,GACpF,OAAO,CAAC,UAAU,CAAC,CA4CrB"}
package/dist/reader.js CHANGED
@@ -7,9 +7,8 @@
7
7
  * Parses .bcfzip files into BCFProject structure
8
8
  */
9
9
  import JSZip from 'jszip';
10
- import { parseComponents } from './reader-components.js';
11
10
  import { extractElement, unescapeXml, parseLabels, parseXsBoolean } from './xml-text.js';
12
- import { parseFiniteFloat } from './numeric.js';
11
+ import { parseViewpointContent } from './reader-viewpoint-content.js';
13
12
  /**
14
13
  * Resource caps guarding against a malicious (zip-bomb) .bcfzip: a tiny
15
14
  * compressed archive that expands to gigabytes, or one with a pathological
@@ -207,6 +206,18 @@ async function readTopics(zip, budget, versionId) {
207
206
  try {
208
207
  const topic = await readTopic(zip, topicGuid, budget, versionId);
209
208
  if (topic) {
209
+ // A second topic folder whose internal Topic/@Guid collides with one
210
+ // already parsed must not silently overwrite it in the map -- that
211
+ // would drop a whole topic with no signal the caller could ever act
212
+ // on (#3960). Keep the first occurrence (folder set iteration order
213
+ // is insertion order, so this is deterministic) and warn, the same
214
+ // way every other "skip this piece, keep going" decision in this
215
+ // function is already reported.
216
+ if (topics.has(topic.guid)) {
217
+ console.warn(`Duplicate topic Guid ${topic.guid}: folder "${topicGuid}" collides with an ` +
218
+ `already-read topic folder and is being dropped. Keeping the first one read.`);
219
+ continue;
220
+ }
210
221
  topics.set(topic.guid, topic);
211
222
  }
212
223
  }
@@ -253,6 +264,7 @@ async function readTopic(zip, topicFolder, budget, versionId) {
253
264
  const description = extractElement(topicContent, 'Description');
254
265
  const priority = extractElement(topicContent, 'Priority');
255
266
  const index = extractElement(topicContent, 'Index');
267
+ const parsedIndex = index ? Number.parseInt(index, 10) : undefined;
256
268
  const creationDate = extractElement(topicContent, 'CreationDate'); // required no-default in markup.xsd; leave undefined, don't fabricate
257
269
  const creationAuthor = extractElement(topicContent, 'CreationAuthor'); // same: required no-default, don't fabricate 'Unknown'
258
270
  const modifiedDate = extractElement(topicContent, 'ModifiedDate');
@@ -285,7 +297,9 @@ async function readTopic(zip, topicFolder, budget, versionId) {
285
297
  topicType,
286
298
  topicStatus,
287
299
  priority,
288
- index: index ? parseInt(index, 10) : undefined,
300
+ // Preserve integer parsing: accepting fractions makes re-export throw.
301
+ // Guard malformed/overflowed tokens instead of storing NaN/Infinity (#3961).
302
+ index: Number.isFinite(parsedIndex) ? parsedIndex : undefined,
289
303
  creationDate,
290
304
  creationAuthor,
291
305
  modifiedDate,
@@ -637,241 +651,6 @@ async function parseViewpoints(zip, topicFolder, markupContent, budget, versionI
637
651
  }
638
652
  return viewpoints;
639
653
  }
640
- /**
641
- * Parse viewpoint XML content
642
- */
643
- function parseViewpointContent(content, versionId) {
644
- // Extract viewpoint GUID from root element (Guid can be anywhere in the tag)
645
- const guidMatch = content.match(/<VisualizationInfo[^>]+Guid="([^"]+)"/);
646
- const guid = guidMatch?.[1] || crypto.randomUUID?.() || `vp-${Date.now()}`;
647
- // Parse perspective camera
648
- const perspectiveCamera = parsePerspectiveCamera(content);
649
- // Parse orthogonal camera
650
- const orthogonalCamera = parseOrthogonalCamera(content);
651
- // Parse components
652
- const components = parseComponents(content, versionId);
653
- // Parse lines
654
- const lines = parseLines(content);
655
- // Parse clipping planes
656
- const clippingPlanes = parseClippingPlanes(content);
657
- // Parse bitmaps
658
- const bitmaps = parseBitmaps(content);
659
- return {
660
- guid,
661
- perspectiveCamera,
662
- orthogonalCamera,
663
- components,
664
- lines: lines.length > 0 ? lines : undefined,
665
- clippingPlanes: clippingPlanes.length > 0 ? clippingPlanes : undefined,
666
- bitmaps: bitmaps.length > 0 ? bitmaps : undefined,
667
- };
668
- }
669
- /**
670
- * Parse perspective camera from viewpoint content
671
- */
672
- function parsePerspectiveCamera(content) {
673
- const match = content.match(/<PerspectiveCamera>([\s\S]*?)<\/PerspectiveCamera>/);
674
- if (!match)
675
- return undefined;
676
- const cameraContent = match[1];
677
- const viewPoint = parsePoint(cameraContent, 'CameraViewPoint');
678
- const direction = parseDirection(cameraContent, 'CameraDirection');
679
- const upVector = parseDirection(cameraContent, 'CameraUpVector');
680
- const fieldOfView = extractElement(cameraContent, 'FieldOfView');
681
- if (!viewPoint || !direction || !upVector || !fieldOfView) {
682
- return undefined;
683
- }
684
- // Same treatment as the coordinates: an unusable scalar is a missing one.
685
- // `fieldOfView` is converted to radians and handed to the viewer camera,
686
- // and the parser already drops the whole camera when the element is absent.
687
- const fov = parseFiniteFloat(fieldOfView);
688
- if (fov === undefined)
689
- return undefined;
690
- return {
691
- cameraViewPoint: viewPoint,
692
- cameraDirection: direction,
693
- cameraUpVector: upVector,
694
- fieldOfView: fov,
695
- ...parseAspectRatio(cameraContent),
696
- };
697
- }
698
- /**
699
- * Parse the optional `<AspectRatio>` of either camera type.
700
- *
701
- * BCF 3.0's visinfo.xsd makes `AspectRatio` (a `PositiveDouble`) a REQUIRED
702
- * child of both camera types; BCF 2.1 has no such element. It is read here for
703
- * both, since the element's presence — not the archive's declared version — is
704
- * what says whether there is a value to keep.
705
- *
706
- * Reading it matters beyond fidelity: the writer refuses to emit a 3.0 camera
707
- * without one, so a 3.0 archive from another tool could otherwise be read and
708
- * then not written back. Returned as a spread-able partial so an absent or
709
- * unusable value leaves the property off entirely rather than setting it to
710
- * `undefined`, and a non-positive value is dropped rather than carried into
711
- * output the schema would reject.
712
- */
713
- function parseAspectRatio(cameraContent) {
714
- const raw = extractElement(cameraContent, 'AspectRatio');
715
- if (!raw)
716
- return {};
717
- const value = parseFiniteFloat(raw);
718
- if (value === undefined || !(value > 0))
719
- return {};
720
- return { aspectRatio: value };
721
- }
722
- /**
723
- * Parse orthogonal camera from viewpoint content
724
- */
725
- function parseOrthogonalCamera(content) {
726
- const match = content.match(/<OrthogonalCamera>([\s\S]*?)<\/OrthogonalCamera>/);
727
- if (!match)
728
- return undefined;
729
- const cameraContent = match[1];
730
- const viewPoint = parsePoint(cameraContent, 'CameraViewPoint');
731
- const direction = parseDirection(cameraContent, 'CameraDirection');
732
- const upVector = parseDirection(cameraContent, 'CameraUpVector');
733
- const viewToWorldScale = extractElement(cameraContent, 'ViewToWorldScale');
734
- if (!viewPoint || !direction || !upVector || !viewToWorldScale) {
735
- return undefined;
736
- }
737
- // `viewToWorldScale` becomes the orthographic half-height, which is the
738
- // value `getOrthoSize()` hands back into a saved viewpoint — so a
739
- // non-finite one persists past the session if it is allowed in (#2461).
740
- const scale = parseFiniteFloat(viewToWorldScale);
741
- if (scale === undefined)
742
- return undefined;
743
- return {
744
- cameraViewPoint: viewPoint,
745
- cameraDirection: direction,
746
- cameraUpVector: upVector,
747
- viewToWorldScale: scale,
748
- ...parseAspectRatio(cameraContent),
749
- };
750
- }
751
- /**
752
- * Parse a 3D point from XML
753
- */
754
- function parsePoint(content, elementName) {
755
- const match = content.match(new RegExp(`<${elementName}>([\\s\\S]*?)<\\/${elementName}>`));
756
- if (!match)
757
- return undefined;
758
- const x = extractElement(match[1], 'X');
759
- const y = extractElement(match[1], 'Y');
760
- const z = extractElement(match[1], 'Z');
761
- if (x === undefined || y === undefined || z === undefined) {
762
- return undefined;
763
- }
764
- // A coordinate that is not a real number is treated as a missing one.
765
- // `parseFloat` has no out-of-band failure value — `"NaN"` parses to `NaN`
766
- // and the well-formed literal `"1e999"` parses to `Infinity` — and from here
767
- // the value reaches `Camera.setPosition`/`setTarget`, which store a pose
768
- // verbatim by design. Once stored, a single non-finite coordinate spreads
769
- // across the whole pose on the next gesture. Rejecting at the file boundary
770
- // means it never gets there, and it costs no new branch: every caller
771
- // already drops the thing it was parsing when a coordinate is missing
772
- // (#2466).
773
- const px = parseFiniteFloat(x);
774
- const py = parseFiniteFloat(y);
775
- const pz = parseFiniteFloat(z);
776
- if (px === undefined || py === undefined || pz === undefined) {
777
- return undefined;
778
- }
779
- return { x: px, y: py, z: pz };
780
- }
781
- /**
782
- * Parse a 3D direction from XML
783
- */
784
- function parseDirection(content, elementName) {
785
- return parsePoint(content, elementName);
786
- }
787
- /**
788
- * Parse lines
789
- */
790
- function parseLines(content) {
791
- const lines = [];
792
- const linesMatch = content.match(/<Lines>([\s\S]*?)<\/Lines>/);
793
- if (!linesMatch)
794
- return lines;
795
- const lineMatches = linesMatch[1].matchAll(/<Line>([\s\S]*?)<\/Line>/g);
796
- for (const match of lineMatches) {
797
- const startPoint = parsePoint(match[1], 'StartPoint');
798
- const endPoint = parsePoint(match[1], 'EndPoint');
799
- if (startPoint && endPoint) {
800
- lines.push({ startPoint, endPoint });
801
- }
802
- }
803
- return lines;
804
- }
805
- /**
806
- * Parse clipping planes
807
- */
808
- function parseClippingPlanes(content) {
809
- const planes = [];
810
- const planesMatch = content.match(/<ClippingPlanes>([\s\S]*?)<\/ClippingPlanes>/);
811
- if (!planesMatch)
812
- return planes;
813
- const planeMatches = planesMatch[1].matchAll(/<ClippingPlane>([\s\S]*?)<\/ClippingPlane>/g);
814
- for (const match of planeMatches) {
815
- const location = parsePoint(match[1], 'Location');
816
- const direction = parseDirection(match[1], 'Direction');
817
- if (location && direction) {
818
- planes.push({ location, direction });
819
- }
820
- }
821
- return planes;
822
- }
823
- /**
824
- * Parse bitmaps
825
- *
826
- * The two BCF versions diverge in shape (see writer.ts's writeBitmap/
827
- * writeViewpointFiles for the write side of this):
828
- * - BCF 3.0: entries sit inside a `<Bitmaps>` wrapper, and the per-entry
829
- * format element is named `<Format>`. No tag inside an entry shares the
830
- * entry's own name, so a plain non-greedy `<Bitmap>...</Bitmap>` match
831
- * is unambiguous.
832
- * - BCF 2.1: entries sit DIRECTLY under `<VisualizationInfo>` (no wrapper),
833
- * and the format element is confusingly also named `<Bitmap>`, nested one
834
- * level inside the entry (`<Bitmap><Bitmap>PNG</Bitmap><Reference>...`).
835
- * A naive non-greedy `<Bitmap>...</Bitmap>` match on that shape terminates
836
- * at the FIRST `</Bitmap>` it sees -- the inner format tag's closing tag,
837
- * not the entry's -- and silently drops the rest of the entry. It must be
838
- * matched with an explicit two-level pattern instead.
839
- */
840
- function parseBitmaps(content) {
841
- const bitmaps = [];
842
- const bitmapsMatch = content.match(/<Bitmaps>([\s\S]*?)<\/Bitmaps>/);
843
- const pushBitmap = (format, body) => {
844
- const reference = extractElement(body, 'Reference');
845
- const location = parsePoint(body, 'Location');
846
- const normal = parseDirection(body, 'Normal');
847
- const up = parseDirection(body, 'Up');
848
- const height = extractElement(body, 'Height');
849
- if (format && reference && location && normal && up && height) {
850
- bitmaps.push({
851
- format: format.toUpperCase() === 'JPG' ? 'JPG' : 'PNG',
852
- reference,
853
- location,
854
- normal,
855
- up,
856
- height: parseFloat(height),
857
- });
858
- }
859
- };
860
- if (bitmapsMatch) {
861
- // BCF 3.0 shape: <Bitmaps><Bitmap><Format>...</Format>...</Bitmap>...</Bitmaps>
862
- for (const match of bitmapsMatch[1].matchAll(/<Bitmap>([\s\S]*?)<\/Bitmap>/g)) {
863
- pushBitmap(extractElement(match[1], 'Format'), match[1]);
864
- }
865
- }
866
- else {
867
- // BCF 2.1 shape: <Bitmap><Bitmap>PNG</Bitmap>...</Bitmap>, unwrapped,
868
- // directly under VisualizationInfo.
869
- for (const match of content.matchAll(/<Bitmap>\s*<Bitmap>([\s\S]*?)<\/Bitmap>([\s\S]*?)<\/Bitmap>/g)) {
870
- pushBitmap(match[1], match[2]);
871
- }
872
- }
873
- return bitmaps;
874
- }
875
654
  /**
876
655
  * Convert Uint8Array to base64 string
877
656
  */