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 ## 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. * **Export**: obj, stl, ply, gltf, glb, off, 3dm, bim.
## Features ## Features

View File

@ -10,7 +10,7 @@
"keywords": [ "keywords": [
"3d", "viewer", "cad", "3d", "viewer", "cad",
"obj", "3ds", "stl", "ply", "gltf", "glb", "off", "3dm", "fbx", "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": [ "files": [
"build/o3dv.min.js", "build/o3dv.min.js",

View File

@ -16,6 +16,7 @@ import { ImporterBim } from './importerbim.js';
import { ImporterThree3mf, ImporterThreeDae, ImporterThreeFbx, ImporterThreeWrl } from './importerthree.js'; import { ImporterThree3mf, ImporterThreeDae, ImporterThreeFbx, ImporterThreeWrl } from './importerthree.js';
import * as fflate from 'fflate'; import * as fflate from 'fflate';
import { ImporterFcstd } from './importerfcstd.js';
export class ImportSettings export class ImportSettings
{ {
@ -91,6 +92,7 @@ export class Importer
new Importer3dm (), new Importer3dm (),
new ImporterIfc (), new ImporterIfc (),
new ImporterOcct (), new ImporterOcct (),
new ImporterFcstd (),
new ImporterThreeFbx (), new ImporterThreeFbx (),
new ImporterThreeDae (), new ImporterThreeDae (),
new ImporterThreeWrl (), 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 { Importer3ds } from './import/importer3ds.js';
import { ImporterBase } from './import/importerbase.js'; import { ImporterBase } from './import/importerbase.js';
import { ImporterBim } from './import/importerbim.js'; import { ImporterBim } from './import/importerbim.js';
import { ImporterFcstd } from './import/importerfcstd.js';
import { InputFile, ImporterFile, ImporterFileList, InputFilesFromUrls, InputFilesFromFileObjects } from './import/importerfiles.js'; import { InputFile, ImporterFile, ImporterFileList, InputFilesFromUrls, InputFilesFromFileObjects } from './import/importerfiles.js';
import { ImporterGltf } from './import/importergltf.js'; import { ImporterGltf } from './import/importergltf.js';
import { ImporterIfc } from './import/importerifc.js'; import { ImporterIfc } from './import/importerifc.js';
@ -152,6 +153,7 @@ export {
Importer3ds, Importer3ds,
ImporterBase, ImporterBase,
ImporterBim, ImporterBim,
ImporterFcstd,
InputFile, InputFile,
ImporterFile, ImporterFile,
ImporterFileList, ImporterFileList,

View File

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

View File

@ -206,6 +206,14 @@
<td class="center red">&#x2717</td> <td class="center red">&#x2717</td>
<td><a href="https://github.com/kovacsv/occt-import-js">occt-import-js</a></td> <td><a href="https://github.com/kovacsv/occt-import-js">occt-import-js</a></td>
</tr> </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> <tr>
<td>Industry Foundation Classes</td> <td>Industry Foundation Classes</td>
<td>ifc</td> <td>ifc</td>