Support for FCStd format #307

This commit is contained in:
kovacsv 2022-10-08 15:56:23 +02:00
parent b06b0c97a4
commit f9b3d9bc82
7 changed files with 433 additions and 3 deletions

View File

@ -22,7 +22,7 @@ The repository is separated into two parts. See more information in the [Develop
## Supported file formats
* **Import**: obj, 3ds, stl, ply, gltf, glb, off, 3dm, fbx, dae, wrl, 3mf, ifc, brep, step, iges, bim.
* **Import**: obj, 3ds, stl, ply, gltf, glb, off, 3dm, fbx, dae, wrl, 3mf, ifc, brep, step, iges, fcstd, bim.
* **Export**: obj, stl, ply, gltf, glb, off, 3dm, bim.
## Features

View File

@ -10,7 +10,7 @@
"keywords": [
"3d", "viewer", "cad",
"obj", "3ds", "stl", "ply", "gltf", "glb", "off", "3dm", "fbx",
"dae", "wrl", "3mf", "ifc", "brep", "step", "iges", "bim"
"dae", "wrl", "3mf", "ifc", "brep", "step", "iges", "fcstd", "bim"
],
"files": [
"build/o3dv.min.js",

View File

@ -16,6 +16,7 @@ import { ImporterBim } from './importerbim.js';
import { ImporterThree3mf, ImporterThreeDae, ImporterThreeFbx, ImporterThreeWrl } from './importerthree.js';
import * as fflate from 'fflate';
import { ImporterFcstd } from './importerfcstd.js';
export class ImportSettings
{
@ -91,6 +92,7 @@ export class Importer
new Importer3dm (),
new ImporterIfc (),
new ImporterOcct (),
new ImporterFcstd (),
new ImporterThreeFbx (),
new ImporterThreeDae (),
new ImporterThreeWrl (),

View File

@ -0,0 +1,418 @@
import { Direction } from '../geometry/geometry.js';
import { ImporterBase } from './importerbase.js';
import { GetFileExtension } from '../io/fileutils.js';
import { GetExternalLibPath } from '../io/externallibs.js';
import { ConvertThreeGeometryToMesh } from '../threejs/threeutils.js';
import { ArrayBufferToUtf8String } from '../io/bufferutils.js';
import { Node, NodeType } from '../model/node.js';
import { ColorToMaterialConverter } from './importerutils.js';
import { RGBAColor } from '../model/color.js';
import { Property, PropertyGroup, PropertyType } from '../model/property.js';
import * as fflate from 'fflate';
const DocumentInitResult =
{
Success : 0,
NoDocumentXml : 1
};
class FreeCadObject
{
constructor (name, type)
{
this.name = name;
this.type = type;
this.shapeName = null;
this.isVisible = false;
this.color = null;
this.fileName = null;
this.fileContent = null;
this.inLinkCount = 0;
this.properties = null;
}
IsConvertible ()
{
if (this.fileName === null || this.fileContent === null) {
return false;
}
if (!this.isVisible) {
return false;
}
if (this.inLinkCount > 0) {
return false;
}
return true;
}
}
class FreeCadDocument
{
constructor ()
{
this.files = null;
this.properties = null;
this.objectNames = [];
this.objectData = new Map ();
}
Init (fileContent)
{
let fileContentBuffer = new Uint8Array (fileContent);
this.files = fflate.unzipSync (fileContentBuffer);
if (!this.LoadDocumentXml ()) {
return DocumentInitResult.NoDocumentXml;
}
this.LoadGuiDocumentXml ();
return DocumentInitResult.Success;
}
GetObjectListToConvert ()
{
let objectList = [];
for (let objectName of this.objectNames) {
let object = this.objectData.get (objectName);
if (!object.IsConvertible ()) {
continue;
}
objectList.push (object);
}
return objectList;
}
IsSupportedType (type)
{
if (!type.startsWith ('Part::') && !type.startsWith ('PartDesign::')) {
return false;
}
if (type.indexOf ('Part2D') !== -1) {
return false;
}
return true;
}
HasFile (fileName)
{
return (fileName in this.files);
}
LoadDocumentXml ()
{
let documentXml = this.GetXMLContent ('Document.xml');
if (documentXml === null) {
return false;
}
this.properties = new PropertyGroup ('Properties');
let documentElements = documentXml.getElementsByTagName ('Document');
for (let documentElement of documentElements) {
for (let childNode of documentElement.childNodes) {
if (childNode.tagName === 'Properties') {
this.GetPropertiesFromElement (childNode, this.properties);
}
}
}
let objectsElements = documentXml.getElementsByTagName ('Objects');
for (let objectsElement of objectsElements) {
let objectElements = objectsElement.getElementsByTagName ('Object');
for (let objectElement of objectElements) {
let name = objectElement.getAttribute ('name');
let type = objectElement.getAttribute ('type');
if (!this.IsSupportedType (type)) {
continue;
}
let object = new FreeCadObject (name, type);
this.objectNames.push (name);
this.objectData.set (name, object);
}
}
let objectDataElements = documentXml.getElementsByTagName ('ObjectData');
for (let objectDataElement of objectDataElements) {
let objectElements = objectDataElement.getElementsByTagName ('Object');
for (let objectElement of objectElements) {
let name = objectElement.getAttribute ('name');
if (!this.objectData.has (name)) {
continue;
}
let object = this.objectData.get (name);
object.properties = new PropertyGroup ('Properties');
for (let childNode of objectElement.childNodes) {
if (childNode.tagName === 'Properties') {
this.GetPropertiesFromElement (childNode, object.properties);
}
}
let propertyElements = objectElement.getElementsByTagName ('Property');
for (let propertyElement of propertyElements) {
let propertyName = propertyElement.getAttribute ('name');
if (propertyName === 'Label') {
object.shapeName = this.GetFirstChildValue (propertyElement, 'String', 'value');
} else if (propertyName === 'Visibility') {
let isVisibleString = this.GetFirstChildValue (propertyElement, 'Bool', 'value');
object.isVisible = (isVisibleString === 'true');
} else if (propertyName === 'Visible') {
let isVisibleString = this.GetFirstChildValue (propertyElement, 'Bool', 'value');
object.isVisible = (isVisibleString === 'true');
} else if (propertyName === 'Shape') {
let fileName = this.GetFirstChildValue (propertyElement, 'Part', 'file');
if (!this.HasFile (fileName)) {
continue;
}
let extension = GetFileExtension (fileName);
if (extension !== 'brp' && extension !== 'brep') {
continue;
}
object.fileName = fileName;
object.fileContent = this.files[fileName];
}
}
let linkElements = objectElement.getElementsByTagName ('Link');
for (let linkElement of linkElements) {
let linkedName = linkElement.getAttribute ('value');
if (this.objectData.has (linkedName)) {
let linkedObject = this.objectData.get (linkedName);
linkedObject.inLinkCount += 1;
}
}
}
}
return true;
}
LoadGuiDocumentXml ()
{
let documentXml = this.GetXMLContent ('GuiDocument.xml');
if (documentXml === null) {
return false;
}
let viewProviderElements = documentXml.getElementsByTagName ('ViewProvider');
for (let viewProviderElement of viewProviderElements) {
let name = viewProviderElement.getAttribute ('name');
if (!this.objectData.has (name)) {
continue;
}
let object = this.objectData.get (name);
let propertyElements = viewProviderElement.getElementsByTagName ('Property');
for (let propertyElement of propertyElements) {
let propertyName = propertyElement.getAttribute ('name');
if (propertyName === 'Visibility') {
let isVisibleString = this.GetFirstChildValue (propertyElement, 'Bool', 'value');
object.isVisible = (isVisibleString === 'true');
} else if (propertyName === 'ShapeColor') {
let colorString = this.GetFirstChildValue (propertyElement, 'PropertyColor', 'value');
let rgba = parseInt (colorString, 10);
object.color = new RGBAColor (
rgba >> 24 & 0xff,
rgba >> 16 & 0xff,
rgba >> 8 & 0xff,
255
);
}
}
}
return true;
}
GetPropertiesFromElement (propertiesElement, propertyGroup)
{
let propertyElements = propertiesElement.getElementsByTagName ('Property');
for (let propertyElement of propertyElements) {
let propertyName = propertyElement.getAttribute ('name');
let propertyType = propertyElement.getAttribute ('type');
let property = null;
if (propertyType === 'App::PropertyBool') {
let propertyValue = this.GetFirstChildValue (propertyElement, 'String', 'bool');
if (propertyValue !== null && propertyValue.length > 0) {
property = new Property (PropertyType.Boolean, propertyName, propertyValue === 'true');
}
} else if (propertyType === 'App::PropertyInteger') {
let propertyValue = this.GetFirstChildValue (propertyElement, 'Integer', 'value');
if (propertyValue !== null && propertyValue.length > 0) {
property = new Property (PropertyType.Integer, propertyName, parseInt (propertyValue));
}
} else if (propertyType === 'App::PropertyString') {
let propertyValue = this.GetFirstChildValue (propertyElement, 'String', 'value');
if (propertyValue !== null && propertyValue.length > 0) {
property = new Property (PropertyType.Text, propertyName, propertyValue);
}
} else if (propertyType === 'App::PropertyUUID') {
let propertyValue = this.GetFirstChildValue (propertyElement, 'Uuid', 'value');
if (propertyValue !== null && propertyValue.length > 0) {
property = new Property (PropertyType.Text, propertyName, propertyValue);
}
} else if (propertyType === 'App::PropertyFloat' || propertyType === 'App::PropertyLength' || propertyType === 'App::PropertyDistance' || propertyType === 'App::PropertyArea' || propertyType === 'App::PropertyVolume') {
let propertyValue = this.GetFirstChildValue (propertyElement, 'Float', 'value');
if (propertyValue !== null && propertyValue.length > 0) {
property = new Property (PropertyType.Number, propertyName, parseFloat (propertyValue));
}
}
if (property !== null) {
propertyGroup.AddProperty (property);
}
}
}
GetXMLContent (xmlFileName)
{
if (!this.HasFile (xmlFileName)) {
return null;
}
let xmlParser = new DOMParser ();
let xmlString = ArrayBufferToUtf8String (this.files[xmlFileName]);
return xmlParser.parseFromString (xmlString, 'text/xml');
}
GetFirstChildValue (element, childTagName, childAttribute)
{
let childObjects = element.getElementsByTagName (childTagName);
if (childObjects.length === 0) {
return null;
}
return childObjects[0].getAttribute (childAttribute);
}
}
export class ImporterFcstd extends ImporterBase
{
constructor ()
{
super ();
this.worker = null;
this.document = null;
}
CanImportExtension (extension)
{
return extension === 'fcstd';
}
GetUpDirection ()
{
return Direction.Z;
}
ClearContent ()
{
if (this.worker !== null) {
this.worker.terminate ();
this.worker = null;
}
this.document = null;
}
ResetContent ()
{
this.worker = null;
this.document = new FreeCadDocument ();
}
ImportContent (fileContent, onFinish)
{
let result = this.document.Init (fileContent);
if (result === DocumentInitResult.NoDocumentXml) {
this.SetError ('No Document.xml found.');
onFinish ();
return;
}
if (this.document.properties !== null && this.document.properties.PropertyCount () > 0) {
this.model.AddPropertyGroup (this.document.properties);
}
let objectsToConvert = this.document.GetObjectListToConvert ();
this.ConvertObjects (objectsToConvert, onFinish);
}
ConvertObjects (objects, onFinish)
{
let workerPath = GetExternalLibPath ('loaders/occt-import-js-worker.js');
this.worker = new Worker (workerPath);
let convertedObjectCount = 0;
let colorToMaterial = new ColorToMaterialConverter (this.model);
let onFileConverted = (resultContent) => {
if (resultContent !== null) {
let currentObject = objects[convertedObjectCount];
this.OnFileConverted (currentObject, resultContent, colorToMaterial);
}
convertedObjectCount += 1;
if (convertedObjectCount === objects.length) {
onFinish ();
} else {
let currentObject = objects[convertedObjectCount];
this.worker.postMessage ({
format : 'brep',
buffer : currentObject.fileContent
});
}
};
this.worker.addEventListener ('message', (ev) => {
onFileConverted (ev.data);
});
this.worker.addEventListener ('error', (ev) => {
onFileConverted (null);
});
let currentObject = objects[convertedObjectCount];
this.worker.postMessage ({
format : 'brep',
buffer : currentObject.fileContent
});
}
OnFileConverted (object, resultContent, colorToMaterial)
{
if (!resultContent.success || resultContent.meshes.length === 0) {
return;
}
let objectNode = new Node ();
objectNode.SetType (NodeType.GroupNode);
if (object.shapeName !== null) {
objectNode.SetName (object.shapeName);
}
let objectMeshIndex = 1;
for (let resultMesh of resultContent.meshes) {
let materialIndex = null;
if (object.color !== null) {
materialIndex = colorToMaterial.GetMaterialIndex (
object.color.r,
object.color.g,
object.color.b,
object.color.a
);
}
let mesh = ConvertThreeGeometryToMesh (resultMesh, materialIndex);
if (object.shapeName !== null) {
let indexString = objectMeshIndex.toString ().padStart (3, '0');
mesh.SetName (object.shapeName + ' ' + indexString);
}
if (object.properties !== null && object.properties.PropertyCount () > 0) {
mesh.AddPropertyGroup (object.properties);
}
let meshIndex = this.model.AddMesh (mesh);
objectNode.AddMeshIndex (meshIndex);
objectMeshIndex += 1;
}
let rootNode = this.model.GetRootNode ();
rootNode.AddChildNode (objectNode);
}
}

View File

@ -26,6 +26,7 @@ import { Importer3dm } from './import/importer3dm.js';
import { Importer3ds } from './import/importer3ds.js';
import { ImporterBase } from './import/importerbase.js';
import { ImporterBim } from './import/importerbim.js';
import { ImporterFcstd } from './import/importerfcstd.js';
import { InputFile, ImporterFile, ImporterFileList, InputFilesFromUrls, InputFilesFromFileObjects } from './import/importerfiles.js';
import { ImporterGltf } from './import/importergltf.js';
import { ImporterIfc } from './import/importerifc.js';
@ -152,6 +153,7 @@ export {
Importer3ds,
ImporterBase,
ImporterBim,
ImporterFcstd,
InputFile,
ImporterFile,
ImporterFileList,

View File

@ -75,7 +75,7 @@ export class Mesh extends ModelObject3D
return this.normals.length - 1;
}
SetNormal (index, normal)
SetNormal (index, normal)
{
this.normals[index] = normal;
}

View File

@ -206,6 +206,14 @@
<td class="center red">&#x2717</td>
<td><a href="https://github.com/kovacsv/occt-import-js">occt-import-js</a></td>
</tr>
<tr>
<td>FreeCAD Standard file format</td>
<td>FCStd</td>
<td>text</td>
<td class="center green">&#x2713</td>
<td class="center red">&#x2717</td>
<td><a href="https://github.com/kovacsv/occt-import-js">occt-import-js</a></td>
</tr>
<tr>
<td>Industry Foundation Classes</td>
<td>ifc</td>