diff --git a/README.md b/README.md index 744f60e..77ad187 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package.json b/package.json index b44b650..7f6f9cf 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/source/engine/import/importer.js b/source/engine/import/importer.js index 6659fc9..bd9cdfa 100644 --- a/source/engine/import/importer.js +++ b/source/engine/import/importer.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 (), diff --git a/source/engine/import/importerfcstd.js b/source/engine/import/importerfcstd.js new file mode 100644 index 0000000..1e0abf4 --- /dev/null +++ b/source/engine/import/importerfcstd.js @@ -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); + } +} diff --git a/source/engine/main.js b/source/engine/main.js index 0ab8c13..9ddcce7 100644 --- a/source/engine/main.js +++ b/source/engine/main.js @@ -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, diff --git a/source/engine/model/mesh.js b/source/engine/model/mesh.js index 0a7bd3f..144f2e9 100644 --- a/source/engine/model/mesh.js +++ b/source/engine/model/mesh.js @@ -75,7 +75,7 @@ export class Mesh extends ModelObject3D return this.normals.length - 1; } - SetNormal (index, normal) +SetNormal (index, normal) { this.normals[index] = normal; } diff --git a/website/info/index.html b/website/info/index.html index 5ecadd3..2aabe3b 100644 --- a/website/info/index.html +++ b/website/info/index.html @@ -206,6 +206,14 @@ ✗ occt-import-js + + FreeCAD Standard file format + FCStd + text + ✓ + ✗ + occt-import-js + Industry Foundation Classes ifc