@ifc-lite/bcf 1.18.2 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ids-reporter.d.ts +15 -0
- package/dist/ids-reporter.d.ts.map +1 -1
- package/dist/ids-reporter.js +77 -4
- package/dist/ids-reporter.js.map +1 -1
- package/dist/numeric.d.ts +64 -1
- package/dist/numeric.d.ts.map +1 -1
- package/dist/numeric.js +81 -1
- package/dist/numeric.js.map +1 -1
- package/dist/reader-components.d.ts +15 -0
- package/dist/reader-components.d.ts.map +1 -0
- package/dist/reader-components.js +202 -0
- package/dist/reader-components.js.map +1 -0
- package/dist/reader.d.ts.map +1 -1
- package/dist/reader.js +108 -138
- package/dist/reader.js.map +1 -1
- package/dist/writer-camera.d.ts +46 -0
- package/dist/writer-camera.d.ts.map +1 -0
- package/dist/writer-camera.js +102 -0
- package/dist/writer-camera.js.map +1 -0
- package/dist/writer.d.ts.map +1 -1
- package/dist/writer.js +219 -158
- package/dist/writer.js.map +1 -1
- package/dist/xml-text.d.ts +32 -0
- package/dist/xml-text.d.ts.map +1 -0
- package/dist/xml-text.js +55 -0
- package/dist/xml-text.js.map +1 -0
- package/package.json +4 -3
|
@@ -0,0 +1,202 @@
|
|
|
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
|
+
import { extractElement, unescapeXml } from './xml-text.js';
|
|
5
|
+
/**
|
|
6
|
+
* Every `<Component>` element in an XML fragment.
|
|
7
|
+
*
|
|
8
|
+
* The two branches are spelled out rather than factored behind a common
|
|
9
|
+
* `<Component[^>]*` prefix as
|
|
10
|
+
* `<Component[^>]*(?:\/>|>[\s\S]*?<\/Component>)`, and that is the whole
|
|
11
|
+
* point: `[^>]*` is greedy and eats the `/` of a self-closing tag, so the
|
|
12
|
+
* `\/>` alternative can never fire. A uniform list still parsed, because the
|
|
13
|
+
* engine backtracks and gives the `/` back when no later `</Component>`
|
|
14
|
+
* exists. A MIXED list did not: `<Component .../><Component ...>...</Component>`
|
|
15
|
+
* matched as ONE element spanning both, and the first component silently
|
|
16
|
+
* inherited the second's `authoringToolId` and `originatingSystem`.
|
|
17
|
+
*
|
|
18
|
+
* That is why every fixture passed. Uniform lists are the only shape the suite
|
|
19
|
+
* had, and they are the one shape the defect cannot reach.
|
|
20
|
+
*/
|
|
21
|
+
const COMPONENT_ELEMENT = /<Component[^>]*?\/>|<Component[^>]*>[\s\S]*?<\/Component>/g;
|
|
22
|
+
/** Parse each `<Component>` in `xml`, dropping any that does not parse. */
|
|
23
|
+
function parseComponentElements(xml) {
|
|
24
|
+
const components = [];
|
|
25
|
+
for (const match of xml.matchAll(COMPONENT_ELEMENT)) {
|
|
26
|
+
const component = parseComponent(match[0]);
|
|
27
|
+
if (component)
|
|
28
|
+
components.push(component);
|
|
29
|
+
}
|
|
30
|
+
return components;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Parse components (selection/visibility/coloring)
|
|
34
|
+
*/
|
|
35
|
+
export function parseComponents(content) {
|
|
36
|
+
const componentsMatch = content.match(/<Components>([\s\S]*?)<\/Components>/);
|
|
37
|
+
if (!componentsMatch)
|
|
38
|
+
return undefined;
|
|
39
|
+
const componentsContent = componentsMatch[1];
|
|
40
|
+
// Parse selection
|
|
41
|
+
const selection = parseComponentList(componentsContent, 'Selection');
|
|
42
|
+
// Parse visibility
|
|
43
|
+
let visibility = parseVisibility(componentsContent);
|
|
44
|
+
// Parse coloring
|
|
45
|
+
const coloring = parseColoring(componentsContent);
|
|
46
|
+
// Nothing read `ViewSetupHints` back, so every hint was lost on read —
|
|
47
|
+
// including out of our own archives. It was invisible because no writer
|
|
48
|
+
// fixture set the hints, so the round trip compared `undefined` to
|
|
49
|
+
// `undefined`.
|
|
50
|
+
//
|
|
51
|
+
// Searched across the whole `Components` body on purpose, because the two
|
|
52
|
+
// schema versions disagree on placement: 2.1 puts the element on
|
|
53
|
+
// `Components`, 3.0 nests it inside `Visibility`, and `writer.ts` emits
|
|
54
|
+
// whichever the requested version calls for. Anchoring to either one would
|
|
55
|
+
// read half the files we produce.
|
|
56
|
+
const viewSetupHints = parseViewSetupHints(componentsContent);
|
|
57
|
+
if (viewSetupHints) {
|
|
58
|
+
// Visibility is required by the schema, but tolerate a file that omits it:
|
|
59
|
+
// DefaultVisibility's schema default is true.
|
|
60
|
+
visibility = { ...(visibility ?? { defaultVisibility: true }), viewSetupHints };
|
|
61
|
+
}
|
|
62
|
+
if (!selection && !visibility && !coloring) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
selection: selection?.length ? selection : undefined,
|
|
67
|
+
visibility,
|
|
68
|
+
coloring: coloring?.length ? coloring : undefined,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Parse a list of components
|
|
73
|
+
*/
|
|
74
|
+
function parseComponentList(content, elementName) {
|
|
75
|
+
const match = content.match(new RegExp(`<${elementName}>([\\s\\S]*?)<\\/${elementName}>`));
|
|
76
|
+
if (!match)
|
|
77
|
+
return undefined;
|
|
78
|
+
const components = parseComponentElements(match[1]);
|
|
79
|
+
return components.length > 0 ? components : undefined;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Parse a single component
|
|
83
|
+
*/
|
|
84
|
+
function parseComponent(content) {
|
|
85
|
+
const ifcGuidMatch = content.match(/IfcGuid="([^"]+)"/);
|
|
86
|
+
/** An attribute value, decoded the same way `extractElement` decodes an
|
|
87
|
+
* element's text, so `AuthoringToolId="A & B"` and the element spelling
|
|
88
|
+
* of the same field yield the same string. Deliberately not
|
|
89
|
+
* `reader.ts::extractAttr`, which is contracted to take an already-captured
|
|
90
|
+
* attribute string and does not decode entities. Without this the two spellings of the SAME field
|
|
91
|
+
* disagreed: `AuthoringToolId="A & B"` came back as the literal
|
|
92
|
+
* `A & B` while `<AuthoringToolId>A & B</AuthoringToolId>` came back
|
|
93
|
+
* as `A & B`. Which one a file uses is not supposed to change its value. */
|
|
94
|
+
// Only the OPENING TAG. `content` is the whole element, so an unscoped
|
|
95
|
+
// search reads a CHILD's attribute as the component's own:
|
|
96
|
+
// `<Component IfcGuid="G"><Child OriginatingSystem="x"/></Component>` used to
|
|
97
|
+
// report `x`. `\b` for the same reason `reader.ts::extractAttr` has it, or
|
|
98
|
+
// `XAuthoringToolId="sneaky"` satisfies a search for `AuthoringToolId`.
|
|
99
|
+
const openingTag = content.slice(0, content.indexOf('>') + 1);
|
|
100
|
+
const attribute = (name) => {
|
|
101
|
+
const raw = openingTag.match(new RegExp(`\\b${name}="([^"]*)"`))?.[1];
|
|
102
|
+
return raw ? unescapeXml(raw) : undefined;
|
|
103
|
+
};
|
|
104
|
+
// Per BCF 2.1 §"Component" (unchanged in 3.0) only `IfcGuid` is an
|
|
105
|
+
// attribute; `OriginatingSystem` and `AuthoringToolId` are child
|
|
106
|
+
// ELEMENTS — which is exactly what writeComponent() emits. Reading them
|
|
107
|
+
// as attributes matched nothing, so both fields were dropped from every
|
|
108
|
+
// archive, ifc-lite's own included. The attribute spellings are still
|
|
109
|
+
// accepted as a fallback so files from tools that emit the non-spec form
|
|
110
|
+
// keep working; the element form wins when both are present.
|
|
111
|
+
// `|| undefined`, not `??`: an EMPTY element (`<AuthoringToolId></...>`) has
|
|
112
|
+
// to read the same as an empty attribute and as an absent one. Without it
|
|
113
|
+
// the element form returned `''`, which survived the identity guard below
|
|
114
|
+
// with no identity at all, and `writeComponent` then wrote it back as a bare
|
|
115
|
+
// `<Component/>` that the reader discards. Three spellings of "nothing"
|
|
116
|
+
// disagreeing is the same defect this file exists to fix.
|
|
117
|
+
const authoringToolId = (extractElement(content, 'AuthoringToolId') || undefined) ?? attribute('AuthoringToolId');
|
|
118
|
+
const originatingSystem = (extractElement(content, 'OriginatingSystem') || undefined) ?? attribute('OriginatingSystem');
|
|
119
|
+
// A component needs some identity to be meaningful. `IfcGuid` is optional
|
|
120
|
+
// in the schema, so `AuthoringToolId` alone is a valid identification —
|
|
121
|
+
// and used to be discarded here because it was never found.
|
|
122
|
+
if (!ifcGuidMatch && authoringToolId === undefined) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
ifcGuid: ifcGuidMatch?.[1] === undefined ? undefined : unescapeXml(ifcGuidMatch[1]),
|
|
127
|
+
authoringToolId,
|
|
128
|
+
originatingSystem,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Parse visibility settings
|
|
133
|
+
*/
|
|
134
|
+
function parseVisibility(content) {
|
|
135
|
+
// Both branches, for the same reason as `COMPONENT_ELEMENT`: `<Exceptions>`
|
|
136
|
+
// and `<ViewSetupHints>` are optional, so `<Visibility DefaultVisibility=
|
|
137
|
+
// "false"/>` is schema-legal. Matching only the paired form made the whole
|
|
138
|
+
// `<Components>` block return undefined for such a file, dropping the
|
|
139
|
+
// selection and colouring with it. ifc-lite's own writer always emits the
|
|
140
|
+
// paired form, which is why no fixture reached this.
|
|
141
|
+
const visibilityMatch = content.match(/<Visibility[^>]*?\/>|<Visibility[^>]*>[\s\S]*?<\/Visibility>/);
|
|
142
|
+
if (!visibilityMatch)
|
|
143
|
+
return undefined;
|
|
144
|
+
// Read from the `<Visibility>` element, NOT from the enclosing `<Components>`
|
|
145
|
+
// string: a `DefaultVisibility` attribute on any earlier element (a
|
|
146
|
+
// `<Selection>` component, say) won the match and inverted the answer, so a
|
|
147
|
+
// file saying show-all hid everything.
|
|
148
|
+
const defaultVisMatch = visibilityMatch[0].match(/DefaultVisibility="([^"]+)"/);
|
|
149
|
+
const defaultVisibility = defaultVisMatch?.[1] !== 'false';
|
|
150
|
+
const exceptions = parseComponentList(visibilityMatch[0], 'Exceptions');
|
|
151
|
+
return {
|
|
152
|
+
defaultVisibility,
|
|
153
|
+
exceptions,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Parse coloring settings
|
|
158
|
+
*/
|
|
159
|
+
function parseColoring(content) {
|
|
160
|
+
const coloringMatch = content.match(/<Coloring>([\s\S]*?)<\/Coloring>/);
|
|
161
|
+
if (!coloringMatch)
|
|
162
|
+
return undefined;
|
|
163
|
+
const colorings = [];
|
|
164
|
+
const colorMatches = coloringMatch[1].matchAll(/<Color\s+Color="([^"]+)"[^>]*>([\s\S]*?)<\/Color>/g);
|
|
165
|
+
for (const match of colorMatches) {
|
|
166
|
+
const color = match[1];
|
|
167
|
+
const components = parseComponentElements(match[2]);
|
|
168
|
+
if (components.length > 0) {
|
|
169
|
+
colorings.push({ color, components });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return colorings.length > 0 ? colorings : undefined;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Parse the `<ViewSetupHints>` element that sits directly under `<Components>`.
|
|
176
|
+
*
|
|
177
|
+
* All three attributes are optional xs:boolean; an absent one stays `undefined`
|
|
178
|
+
* rather than collapsing to `false`, because "the author did not say" and "the
|
|
179
|
+
* author said no" mean different things to a viewer applying the hints.
|
|
180
|
+
*/
|
|
181
|
+
function parseViewSetupHints(content) {
|
|
182
|
+
const match = content.match(/<ViewSetupHints\b([^>]*)>/);
|
|
183
|
+
if (!match)
|
|
184
|
+
return undefined;
|
|
185
|
+
const attrs = match[1];
|
|
186
|
+
const flag = (name) => {
|
|
187
|
+
const raw = attrs.match(new RegExp(`\\b${name}="([^"]*)"`))?.[1];
|
|
188
|
+
if (raw === undefined)
|
|
189
|
+
return undefined;
|
|
190
|
+
return raw === 'true' || raw === '1';
|
|
191
|
+
};
|
|
192
|
+
const spacesVisible = flag('SpacesVisible');
|
|
193
|
+
const spaceBoundariesVisible = flag('SpaceBoundariesVisible');
|
|
194
|
+
const openingsVisible = flag('OpeningsVisible');
|
|
195
|
+
if (spacesVisible === undefined
|
|
196
|
+
&& spaceBoundariesVisible === undefined
|
|
197
|
+
&& openingsVisible === undefined) {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
return { spacesVisible, spaceBoundariesVisible, openingsVisible };
|
|
201
|
+
}
|
|
202
|
+
//# sourceMappingURL=reader-components.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reader-components.js","sourceRoot":"","sources":["../src/reader-components.ts"],"names":[],"mappings":"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"}
|
package/dist/reader.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reader.d.ts","sourceRoot":"","sources":["../src/reader.ts"],"names":[],"mappings":"
|
|
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"}
|
package/dist/reader.js
CHANGED
|
@@ -7,6 +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
|
+
import { extractElement, unescapeXml } from './xml-text.js';
|
|
10
12
|
import { parseFiniteFloat } from './numeric.js';
|
|
11
13
|
/**
|
|
12
14
|
* Resource caps guarding against a malicious (zip-bomb) .bcfzip: a tiny
|
|
@@ -177,10 +179,14 @@ async function readProjectFile(zip, budget) {
|
|
|
177
179
|
}
|
|
178
180
|
const content = await readEntryCapped(projectFile, 'string', budget);
|
|
179
181
|
const projectIdMatch = content.match(/ProjectId="([^"]+)"/);
|
|
180
|
-
|
|
182
|
+
// extractElement, not a raw regex: writeProjectFile escapes the name with
|
|
183
|
+
// escapeXml, so a raw match hands back the literal entities (`A & B`) and
|
|
184
|
+
// the next export escapes them again. Every other element in this reader goes
|
|
185
|
+
// through extractElement precisely so the escape has an inverse.
|
|
186
|
+
const name = extractElement(content, 'Name');
|
|
181
187
|
return {
|
|
182
188
|
projectId: projectIdMatch?.[1],
|
|
183
|
-
name
|
|
189
|
+
name,
|
|
184
190
|
};
|
|
185
191
|
}
|
|
186
192
|
/**
|
|
@@ -348,47 +354,31 @@ function parseHeaderFiles(markupContent) {
|
|
|
348
354
|
function extractAttr(attrsString, attrName) {
|
|
349
355
|
return attrsString.match(new RegExp(`\\b${attrName}="([^"]*)"`))?.[1];
|
|
350
356
|
}
|
|
351
|
-
/**
|
|
352
|
-
* Extract a simple element value from XML
|
|
353
|
-
*
|
|
354
|
-
* Values are unescaped so writer.ts's escapeXml() round-trips correctly
|
|
355
|
-
* (see escapeXml/unescapeXml regression: & < > " ' in titles/descriptions/
|
|
356
|
-
* comments must come back exactly as written, not as literal entities).
|
|
357
|
-
*/
|
|
358
|
-
function extractElement(content, elementName) {
|
|
359
|
-
const match = content.match(new RegExp(`<${elementName}>([^<]*)<\\/${elementName}>`));
|
|
360
|
-
return match?.[1] !== undefined ? unescapeXml(match[1]) : undefined;
|
|
361
|
-
}
|
|
362
|
-
/**
|
|
363
|
-
* Unescape XML entities produced by writer.ts's escapeXml()
|
|
364
|
-
*
|
|
365
|
-
* & must be decoded last so a literal "<" written as "&lt;"
|
|
366
|
-
* doesn't get corrupted into "<" by an earlier pass.
|
|
367
|
-
*/
|
|
368
|
-
function unescapeXml(str) {
|
|
369
|
-
return str
|
|
370
|
-
.replace(/</g, '<')
|
|
371
|
-
.replace(/>/g, '>')
|
|
372
|
-
.replace(/"/g, '"')
|
|
373
|
-
.replace(/'/g, "'")
|
|
374
|
-
.replace(/&/g, '&');
|
|
375
|
-
}
|
|
376
357
|
/**
|
|
377
358
|
* Extract BIM snippet from topic content
|
|
378
359
|
*/
|
|
379
360
|
function extractBimSnippet(content) {
|
|
380
|
-
|
|
361
|
+
// Attributes are captured generically and pulled out with extractAttr rather
|
|
362
|
+
// than anchoring SnippetType to first position: our own writer always emits
|
|
363
|
+
// it first, so an anchored regex round-trips our files and silently drops the
|
|
364
|
+
// entire snippet from a foreign tool's file that orders the two attributes
|
|
365
|
+
// the other way (see extractAttr's note on attribute order).
|
|
366
|
+
const match = content.match(/<BimSnippet\b([^>]*)>([\s\S]*?)<\/BimSnippet>/);
|
|
381
367
|
if (!match)
|
|
382
368
|
return undefined;
|
|
369
|
+
const snippetType = extractAttr(match[1], 'SnippetType');
|
|
370
|
+
if (!snippetType)
|
|
371
|
+
return undefined;
|
|
383
372
|
// BCF 2.1 spells this `isExternal`, 3.0 `IsExternal` (same rename as the
|
|
384
373
|
// Header `<File>` attribute in reader.ts's parseHeaderFiles); accept either
|
|
385
|
-
// casing so a spec-correct 3.0 file's flag isn't silently read as false
|
|
386
|
-
|
|
374
|
+
// casing so a spec-correct 3.0 file's flag isn't silently read as false, and
|
|
375
|
+
// the xs:boolean `1`/`0` forms alongside `true`/`false`.
|
|
376
|
+
const isExternalRaw = match[1].match(/\b[Ii]sExternal="([^"]*)"/)?.[1];
|
|
387
377
|
const reference = extractElement(match[2], 'Reference');
|
|
388
378
|
const referenceSchema = extractElement(match[2], 'ReferenceSchema');
|
|
389
379
|
return {
|
|
390
|
-
snippetType
|
|
391
|
-
isExternal:
|
|
380
|
+
snippetType,
|
|
381
|
+
isExternal: isExternalRaw === 'true' || isExternalRaw === '1',
|
|
392
382
|
reference: reference || '',
|
|
393
383
|
referenceSchema,
|
|
394
384
|
};
|
|
@@ -522,6 +512,33 @@ async function parseViewpoints(zip, topicFolder, markupContent, budget) {
|
|
|
522
512
|
snapshotFile: snapshotFileMatch?.[1],
|
|
523
513
|
});
|
|
524
514
|
}
|
|
515
|
+
// BCF 3.0 nests viewpoint entries inside <Topic>, wrapped in a plural
|
|
516
|
+
// <Viewpoints> container that (unlike 2.1's) carries no Guid itself; each
|
|
517
|
+
// entry is a singular <ViewPoint Guid="..."> -- capital P, distinct from
|
|
518
|
+
// the lowercase-p <Viewpoint Guid="..."/> a Comment uses to reference a
|
|
519
|
+
// viewpoint -- per buildingSMART/BCF-XML markup.xsd (release_3_0, Topic's
|
|
520
|
+
// Viewpoints element wraps `ViewPoint` entries) and confirmed against the
|
|
521
|
+
// buildingSMART/BCF-XML Test Cases/v3.0/Visualization/Perspective camera
|
|
522
|
+
// fixture, whose markup.bcf reads
|
|
523
|
+
// `<Viewpoints><ViewPoint Guid="f99eb1ed-...">`. The plural-with-Guid regex
|
|
524
|
+
// above can never match this shape (the wrapper has no Guid attribute), so
|
|
525
|
+
// without this pass every 3.0 file's markup-declared snapshot filename was
|
|
526
|
+
// silently dropped and resolution fell through to guessing our own
|
|
527
|
+
// Snapshot_<guid> naming convention -- which a third-party 3.0 file has no
|
|
528
|
+
// reason to follow.
|
|
529
|
+
const viewPointElementRegex = /<ViewPoint\b([^>]*)>([\s\S]*?)<\/ViewPoint>/g;
|
|
530
|
+
for (const match of markupContent.matchAll(viewPointElementRegex)) {
|
|
531
|
+
const guid = extractAttr(match[1], 'Guid');
|
|
532
|
+
if (!guid || viewpointInfoMap.has(guid))
|
|
533
|
+
continue;
|
|
534
|
+
const content = match[2];
|
|
535
|
+
const viewpointFileMatch = content.match(/<Viewpoint>([^<]+)<\/Viewpoint>/);
|
|
536
|
+
const snapshotFileMatch = content.match(/<Snapshot>([^<]+)<\/Snapshot>/);
|
|
537
|
+
viewpointInfoMap.set(guid, {
|
|
538
|
+
viewpointFile: viewpointFileMatch?.[1],
|
|
539
|
+
snapshotFile: snapshotFileMatch?.[1],
|
|
540
|
+
});
|
|
541
|
+
}
|
|
525
542
|
// Also match self-closing viewpoint references
|
|
526
543
|
const simpleViewpointRefs = markupContent.matchAll(/<Viewpoints\b([^>]*)\/>/g);
|
|
527
544
|
for (const match of simpleViewpointRefs) {
|
|
@@ -677,8 +694,33 @@ function parsePerspectiveCamera(content) {
|
|
|
677
694
|
cameraDirection: direction,
|
|
678
695
|
cameraUpVector: upVector,
|
|
679
696
|
fieldOfView: fov,
|
|
697
|
+
...parseAspectRatio(cameraContent),
|
|
680
698
|
};
|
|
681
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Parse the optional `<AspectRatio>` of either camera type.
|
|
702
|
+
*
|
|
703
|
+
* BCF 3.0's visinfo.xsd makes `AspectRatio` (a `PositiveDouble`) a REQUIRED
|
|
704
|
+
* child of both camera types; BCF 2.1 has no such element. It is read here for
|
|
705
|
+
* both, since the element's presence — not the archive's declared version — is
|
|
706
|
+
* what says whether there is a value to keep.
|
|
707
|
+
*
|
|
708
|
+
* Reading it matters beyond fidelity: the writer refuses to emit a 3.0 camera
|
|
709
|
+
* without one, so a 3.0 archive from another tool could otherwise be read and
|
|
710
|
+
* then not written back. Returned as a spread-able partial so an absent or
|
|
711
|
+
* unusable value leaves the property off entirely rather than setting it to
|
|
712
|
+
* `undefined`, and a non-positive value is dropped rather than carried into
|
|
713
|
+
* output the schema would reject.
|
|
714
|
+
*/
|
|
715
|
+
function parseAspectRatio(cameraContent) {
|
|
716
|
+
const raw = extractElement(cameraContent, 'AspectRatio');
|
|
717
|
+
if (!raw)
|
|
718
|
+
return {};
|
|
719
|
+
const value = parseFiniteFloat(raw);
|
|
720
|
+
if (value === undefined || !(value > 0))
|
|
721
|
+
return {};
|
|
722
|
+
return { aspectRatio: value };
|
|
723
|
+
}
|
|
682
724
|
/**
|
|
683
725
|
* Parse orthogonal camera from viewpoint content
|
|
684
726
|
*/
|
|
@@ -705,6 +747,7 @@ function parseOrthogonalCamera(content) {
|
|
|
705
747
|
cameraDirection: direction,
|
|
706
748
|
cameraUpVector: upVector,
|
|
707
749
|
viewToWorldScale: scale,
|
|
750
|
+
...parseAspectRatio(cameraContent),
|
|
708
751
|
};
|
|
709
752
|
}
|
|
710
753
|
/**
|
|
@@ -743,102 +786,6 @@ function parsePoint(content, elementName) {
|
|
|
743
786
|
function parseDirection(content, elementName) {
|
|
744
787
|
return parsePoint(content, elementName);
|
|
745
788
|
}
|
|
746
|
-
/**
|
|
747
|
-
* Parse components (selection/visibility/coloring)
|
|
748
|
-
*/
|
|
749
|
-
function parseComponents(content) {
|
|
750
|
-
const componentsMatch = content.match(/<Components>([\s\S]*?)<\/Components>/);
|
|
751
|
-
if (!componentsMatch)
|
|
752
|
-
return undefined;
|
|
753
|
-
const componentsContent = componentsMatch[1];
|
|
754
|
-
// Parse selection
|
|
755
|
-
const selection = parseComponentList(componentsContent, 'Selection');
|
|
756
|
-
// Parse visibility
|
|
757
|
-
const visibility = parseVisibility(componentsContent);
|
|
758
|
-
// Parse coloring
|
|
759
|
-
const coloring = parseColoring(componentsContent);
|
|
760
|
-
if (!selection && !visibility && !coloring) {
|
|
761
|
-
return undefined;
|
|
762
|
-
}
|
|
763
|
-
return {
|
|
764
|
-
selection: selection?.length ? selection : undefined,
|
|
765
|
-
visibility,
|
|
766
|
-
coloring: coloring?.length ? coloring : undefined,
|
|
767
|
-
};
|
|
768
|
-
}
|
|
769
|
-
/**
|
|
770
|
-
* Parse a list of components
|
|
771
|
-
*/
|
|
772
|
-
function parseComponentList(content, elementName) {
|
|
773
|
-
const match = content.match(new RegExp(`<${elementName}>([\\s\\S]*?)<\\/${elementName}>`));
|
|
774
|
-
if (!match)
|
|
775
|
-
return undefined;
|
|
776
|
-
const components = [];
|
|
777
|
-
const componentMatches = match[1].matchAll(/<Component[^>]*(?:\/>|>[\s\S]*?<\/Component>)/g);
|
|
778
|
-
for (const compMatch of componentMatches) {
|
|
779
|
-
const component = parseComponent(compMatch[0]);
|
|
780
|
-
if (component) {
|
|
781
|
-
components.push(component);
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
return components.length > 0 ? components : undefined;
|
|
785
|
-
}
|
|
786
|
-
/**
|
|
787
|
-
* Parse a single component
|
|
788
|
-
*/
|
|
789
|
-
function parseComponent(content) {
|
|
790
|
-
const ifcGuidMatch = content.match(/IfcGuid="([^"]+)"/);
|
|
791
|
-
const authoringToolIdMatch = content.match(/AuthoringToolId="([^"]+)"/);
|
|
792
|
-
const originatingSystemMatch = content.match(/OriginatingSystem="([^"]+)"/);
|
|
793
|
-
if (!ifcGuidMatch && !authoringToolIdMatch) {
|
|
794
|
-
return undefined;
|
|
795
|
-
}
|
|
796
|
-
return {
|
|
797
|
-
ifcGuid: ifcGuidMatch?.[1],
|
|
798
|
-
authoringToolId: authoringToolIdMatch?.[1],
|
|
799
|
-
originatingSystem: originatingSystemMatch?.[1],
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
/**
|
|
803
|
-
* Parse visibility settings
|
|
804
|
-
*/
|
|
805
|
-
function parseVisibility(content) {
|
|
806
|
-
const visibilityMatch = content.match(/<Visibility[^>]*>([\s\S]*?)<\/Visibility>/);
|
|
807
|
-
if (!visibilityMatch)
|
|
808
|
-
return undefined;
|
|
809
|
-
const defaultVisMatch = content.match(/DefaultVisibility="([^"]+)"/);
|
|
810
|
-
const defaultVisibility = defaultVisMatch?.[1] !== 'false';
|
|
811
|
-
const exceptions = parseComponentList(visibilityMatch[1], 'Exceptions');
|
|
812
|
-
return {
|
|
813
|
-
defaultVisibility,
|
|
814
|
-
exceptions,
|
|
815
|
-
};
|
|
816
|
-
}
|
|
817
|
-
/**
|
|
818
|
-
* Parse coloring settings
|
|
819
|
-
*/
|
|
820
|
-
function parseColoring(content) {
|
|
821
|
-
const coloringMatch = content.match(/<Coloring>([\s\S]*?)<\/Coloring>/);
|
|
822
|
-
if (!coloringMatch)
|
|
823
|
-
return undefined;
|
|
824
|
-
const colorings = [];
|
|
825
|
-
const colorMatches = coloringMatch[1].matchAll(/<Color\s+Color="([^"]+)"[^>]*>([\s\S]*?)<\/Color>/g);
|
|
826
|
-
for (const match of colorMatches) {
|
|
827
|
-
const color = match[1];
|
|
828
|
-
const components = [];
|
|
829
|
-
const componentMatches = match[2].matchAll(/<Component[^>]*(?:\/>|>[\s\S]*?<\/Component>)/g);
|
|
830
|
-
for (const compMatch of componentMatches) {
|
|
831
|
-
const component = parseComponent(compMatch[0]);
|
|
832
|
-
if (component) {
|
|
833
|
-
components.push(component);
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
if (components.length > 0) {
|
|
837
|
-
colorings.push({ color, components });
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
return colorings.length > 0 ? colorings : undefined;
|
|
841
|
-
}
|
|
842
789
|
/**
|
|
843
790
|
* Parse lines
|
|
844
791
|
*/
|
|
@@ -877,20 +824,30 @@ function parseClippingPlanes(content) {
|
|
|
877
824
|
}
|
|
878
825
|
/**
|
|
879
826
|
* Parse bitmaps
|
|
827
|
+
*
|
|
828
|
+
* The two BCF versions diverge in shape (see writer.ts's writeBitmap/
|
|
829
|
+
* writeViewpointFiles for the write side of this):
|
|
830
|
+
* - BCF 3.0: entries sit inside a `<Bitmaps>` wrapper, and the per-entry
|
|
831
|
+
* format element is named `<Format>`. No tag inside an entry shares the
|
|
832
|
+
* entry's own name, so a plain non-greedy `<Bitmap>...</Bitmap>` match
|
|
833
|
+
* is unambiguous.
|
|
834
|
+
* - BCF 2.1: entries sit DIRECTLY under `<VisualizationInfo>` (no wrapper),
|
|
835
|
+
* and the format element is confusingly also named `<Bitmap>`, nested one
|
|
836
|
+
* level inside the entry (`<Bitmap><Bitmap>PNG</Bitmap><Reference>...`).
|
|
837
|
+
* A naive non-greedy `<Bitmap>...</Bitmap>` match on that shape terminates
|
|
838
|
+
* at the FIRST `</Bitmap>` it sees -- the inner format tag's closing tag,
|
|
839
|
+
* not the entry's -- and silently drops the rest of the entry. It must be
|
|
840
|
+
* matched with an explicit two-level pattern instead.
|
|
880
841
|
*/
|
|
881
842
|
function parseBitmaps(content) {
|
|
882
843
|
const bitmaps = [];
|
|
883
844
|
const bitmapsMatch = content.match(/<Bitmaps>([\s\S]*?)<\/Bitmaps>/);
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
const
|
|
889
|
-
const
|
|
890
|
-
const location = parsePoint(match[1], 'Location');
|
|
891
|
-
const normal = parseDirection(match[1], 'Normal');
|
|
892
|
-
const up = parseDirection(match[1], 'Up');
|
|
893
|
-
const height = extractElement(match[1], 'Height');
|
|
845
|
+
const pushBitmap = (format, body) => {
|
|
846
|
+
const reference = extractElement(body, 'Reference');
|
|
847
|
+
const location = parsePoint(body, 'Location');
|
|
848
|
+
const normal = parseDirection(body, 'Normal');
|
|
849
|
+
const up = parseDirection(body, 'Up');
|
|
850
|
+
const height = extractElement(body, 'Height');
|
|
894
851
|
if (format && reference && location && normal && up && height) {
|
|
895
852
|
bitmaps.push({
|
|
896
853
|
format: format.toUpperCase() === 'JPG' ? 'JPG' : 'PNG',
|
|
@@ -901,6 +858,19 @@ function parseBitmaps(content) {
|
|
|
901
858
|
height: parseFloat(height),
|
|
902
859
|
});
|
|
903
860
|
}
|
|
861
|
+
};
|
|
862
|
+
if (bitmapsMatch) {
|
|
863
|
+
// BCF 3.0 shape: <Bitmaps><Bitmap><Format>...</Format>...</Bitmap>...</Bitmaps>
|
|
864
|
+
for (const match of bitmapsMatch[1].matchAll(/<Bitmap>([\s\S]*?)<\/Bitmap>/g)) {
|
|
865
|
+
pushBitmap(extractElement(match[1], 'Format'), match[1]);
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
else {
|
|
869
|
+
// BCF 2.1 shape: <Bitmap><Bitmap>PNG</Bitmap>...</Bitmap>, unwrapped,
|
|
870
|
+
// directly under VisualizationInfo.
|
|
871
|
+
for (const match of content.matchAll(/<Bitmap>\s*<Bitmap>([\s\S]*?)<\/Bitmap>([\s\S]*?)<\/Bitmap>/g)) {
|
|
872
|
+
pushBitmap(match[1], match[2]);
|
|
873
|
+
}
|
|
904
874
|
}
|
|
905
875
|
return bitmaps;
|
|
906
876
|
}
|