Support for FCStd format #307
This commit is contained in:
parent
b06b0c97a4
commit
f9b3d9bc82
@ -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
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 (),
|
||||
|
||||
418
source/engine/import/importerfcstd.js
Normal file
418
source/engine/import/importerfcstd.js
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -75,7 +75,7 @@ export class Mesh extends ModelObject3D
|
||||
return this.normals.length - 1;
|
||||
}
|
||||
|
||||
SetNormal (index, normal)
|
||||
SetNormal (index, normal)
|
||||
{
|
||||
this.normals[index] = normal;
|
||||
}
|
||||
|
||||
@ -206,6 +206,14 @@
|
||||
<td class="center red">✗</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">✓</td>
|
||||
<td class="center red">✗</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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user