online-3d-viewer
Version:
Online 3D Viewer
1,994 lines (1,743 loc) • 469 kB
JavaScript
import * as THREE from 'three';
import { TGALoader } from 'three/examples/jsm/loaders/TGALoader.js';
import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';
import { ColladaLoader } from 'three/examples/jsm/loaders/ColladaLoader.js';
import { VRMLLoader } from 'three/examples/jsm/loaders/VRMLLoader.js';
import { ThreeMFLoader } from 'three/examples/jsm/loaders/3MFLoader.js';
import { AMFLoader } from 'three/examples/jsm/loaders/AMFLoader.js';
import * as fflate from 'fflate';
import { SVGLoader } from 'three/examples/jsm/loaders/SVGLoader.js';
function IsDefined (val)
{
return val !== undefined && val !== null;
}
function ValueOrDefault (val, def)
{
if (val === undefined || val === null) {
return def;
}
return val;
}
function CopyObjectAttributes (src, dest)
{
if (!IsDefined (src)) {
return;
}
for (let attribute of Object.keys (src)) {
if (IsDefined (src[attribute])) {
dest[attribute] = src[attribute];
}
}
}
function IsObjectEmpty (obj)
{
return Object.keys (obj).length === 0;
}
function FormatString (template, ...args)
{
return template.replace (/{([0-9]+)}/g, (match, index) => {
return args[index] === undefined ? match : args[index];
});
}
function EscapeHtmlChars (str)
{
return str.replace (/</g, '<').replace (/>/g, '>');
}
class EventNotifier
{
constructor ()
{
this.eventListeners = new Map ();
}
AddEventListener (eventId, listener)
{
if (!this.eventListeners.has (eventId)) {
this.eventListeners.set (eventId, []);
}
let listeners = this.eventListeners.get (eventId);
listeners.push (listener);
}
HasEventListener (eventId)
{
return this.eventListeners.has (eventId);
}
GetEventNotifier (eventId)
{
return () => {
this.NotifyEventListeners (eventId);
};
}
NotifyEventListeners (eventId, ...args)
{
if (!this.eventListeners.has (eventId)) {
return;
}
let listeners = this.eventListeners.get (eventId);
for (let listener of listeners) {
listener (...args);
}
}
}
let gLocalizedStrings = null;
let gLanguageCode = null;
function SetLocalizedStrings (localizedStrings)
{
gLocalizedStrings = localizedStrings;
}
function SetLanguageCode (languageCode)
{
gLanguageCode = languageCode;
}
function Loc (str)
{
if (gLocalizedStrings === null || gLanguageCode === null) {
return str;
}
if (!gLocalizedStrings[str] || !gLocalizedStrings[str][gLanguageCode]) {
return str;
}
return gLocalizedStrings[str][gLanguageCode];
}
function FLoc (str, ...args)
{
return FormatString (Loc (str), ...args);
}
class TaskRunner
{
constructor ()
{
this.count = null;
this.current = null;
this.callbacks = null;
}
Run (count, callbacks)
{
this.count = count;
this.current = 0;
this.callbacks = callbacks;
if (count === 0) {
this.TaskReady ();
} else {
this.RunOnce ();
}
}
RunBatch (count, batchCount, callbacks)
{
let stepCount = 0;
if (count > 0) {
stepCount = parseInt ((count - 1) / batchCount, 10) + 1;
}
this.Run (stepCount, {
runTask : (index, ready) => {
const firstIndex = index * batchCount;
const lastIndex = Math.min ((index + 1) * batchCount, count) - 1;
callbacks.runTask (firstIndex, lastIndex, ready);
},
onReady : callbacks.onReady
});
}
RunOnce ()
{
setTimeout (() => {
this.callbacks.runTask (this.current, this.TaskReady.bind (this));
}, 0);
}
TaskReady ()
{
this.current += 1;
if (this.current < this.count) {
this.RunOnce ();
} else {
if (this.callbacks.onReady) {
this.callbacks.onReady ();
}
}
}
}
function RunTaskAsync (task)
{
setTimeout (() => {
task ();
}, 10);
}
function RunTasks (count, callbacks)
{
let taskRunner = new TaskRunner ();
taskRunner.Run (count, callbacks);
}
function RunTasksBatch (count, batchCount, callbacks)
{
let taskRunner = new TaskRunner ();
taskRunner.RunBatch (count, batchCount, callbacks);
}
function WaitWhile (expression)
{
function Waiter (expression)
{
if (expression ()) {
setTimeout (() => {
Waiter (expression);
}, 10);
}
}
Waiter (expression);
}
const Eps = 0.00000001;
const BigEps = 0.0001;
const RadDeg = 57.29577951308232;
const DegRad = 0.017453292519943;
function IsZero (a)
{
return Math.abs (a) < Eps;
}
function IsLower (a, b)
{
return b - a > Eps;
}
function IsGreater (a, b)
{
return a - b > Eps;
}
function IsLowerOrEqual (a, b)
{
return b - a > -1e-8;
}
function IsGreaterOrEqual (a, b)
{
return a - b > -1e-8;
}
function IsEqual (a, b)
{
return Math.abs (b - a) < Eps;
}
function IsEqualEps (a, b, eps)
{
return Math.abs (b - a) < eps;
}
function IsPositive (a)
{
return a > Eps;
}
function IsNegative (a)
{
return a < -1e-8;
}
const Direction =
{
X : 1,
Y : 2,
Z : 3
};
class Coord2D
{
constructor (x, y)
{
this.x = x;
this.y = y;
}
Clone ()
{
return new Coord2D (this.x, this.y);
}
}
function CoordIsEqual2D (a, b)
{
return IsEqual (a.x, b.x) && IsEqual (a.y, b.y);
}
function AddCoord2D (a, b)
{
return new Coord2D (a.x + b.x, a.y + b.y);
}
function SubCoord2D (a, b)
{
return new Coord2D (a.x - b.x, a.y - b.y);
}
function CoordDistance2D (a, b)
{
return Math.sqrt ((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}
function DotVector2D (a, b)
{
return a.x * b.x + a.y * b.y;
}
/**
* RGB color object. Components are numbers in the range of 0..255.
*/
class RGBColor
{
/**
* @param {number} r Red component.
* @param {number} g Green component.
* @param {number} b Blue component.
*/
constructor (r, g, b)
{
this.r = r;
this.g = g;
this.b = b;
}
/**
* Sets the value of all components.
* @param {number} r Red component.
* @param {number} g Green component.
* @param {number} b Blue component.
*/
Set (r, g, b)
{
this.r = r;
this.g = g;
this.b = b;
}
/**
* Creates a clone of the object.
* @returns {RGBColor}
*/
Clone ()
{
return new RGBColor (this.r, this.g, this.b);
}
}
/**
* RGBA color object. Components are numbers in the range of 0..255.
*/
class RGBAColor
{
/**
* @param {number} r Red component.
* @param {number} g Green component.
* @param {number} b Blue component.
* @param {number} a Alpha component.
*/
constructor (r, g, b, a)
{
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
/**
* Sets the value of all components.
* @param {number} r Red component.
* @param {number} g Green component.
* @param {number} b Blue component.
* @param {number} a Alpha component.
*/
Set (r, g, b, a)
{
this.r = r;
this.g = g;
this.b = b;
this.a = a;
}
/**
* Creates a clone of the object.
* @returns {RGBAColor}
*/
Clone ()
{
return new RGBAColor (this.r, this.g, this.b, this.a);
}
}
function ColorComponentFromFloat (component)
{
return parseInt (Math.round (component * 255.0), 10);
}
function ColorComponentToFloat (component)
{
return component / 255.0;
}
function RGBColorFromFloatComponents (r, g, b)
{
return new RGBColor (
ColorComponentFromFloat (r),
ColorComponentFromFloat (g),
ColorComponentFromFloat (b)
);
}
function SRGBToLinear (component)
{
if (component < 0.04045) {
return component * 0.0773993808;
} else {
return Math.pow (component * 0.9478672986 + 0.0521327014, 2.4);
}
}
function LinearToSRGB (component)
{
if (component < 0.0031308) {
return component * 12.92;
} else {
return 1.055 * (Math.pow (component, 0.41666)) - 0.055;
}
}
function IntegerToHexString (intVal)
{
let result = parseInt (intVal, 10).toString (16);
while (result.length < 2) {
result = '0' + result;
}
return result;
}
function RGBColorToHexString (color)
{
let r = IntegerToHexString (color.r);
let g = IntegerToHexString (color.g);
let b = IntegerToHexString (color.b);
return r + g + b;
}
function RGBAColorToHexString (color)
{
let r = IntegerToHexString (color.r);
let g = IntegerToHexString (color.g);
let b = IntegerToHexString (color.b);
let a = IntegerToHexString (color.a);
return r + g + b + a;
}
function HexStringToRGBColor (hexString)
{
if (hexString.length !== 6) {
return null;
}
let r = parseInt (hexString.substring (0, 2), 16);
let g = parseInt (hexString.substring (2, 4), 16);
let b = parseInt (hexString.substring (4, 6), 16);
return new RGBColor (r, g, b);
}
function HexStringToRGBAColor (hexString)
{
if (hexString.length !== 6 && hexString.length !== 8) {
return null;
}
let r = parseInt (hexString.substring (0, 2), 16);
let g = parseInt (hexString.substring (2, 4), 16);
let b = parseInt (hexString.substring (4, 6), 16);
let a = 255;
if (hexString.length === 8) {
a = parseInt (hexString.substring (6, 8), 16);
}
return new RGBAColor (r, g, b, a);
}
function ArrayToRGBColor (arr)
{
return new RGBColor (arr[0], arr[1], arr[2]);
}
function RGBColorIsEqual (a, b)
{
return a.r === b.r && a.g === b.g && a.b === b.b;
}
class TextureMap
{
constructor ()
{
this.name = null;
this.mimeType = null;
this.buffer = null;
this.offset = new Coord2D (0.0, 0.0);
this.scale = new Coord2D (1.0, 1.0);
this.rotation = 0.0; // radians
}
IsValid ()
{
return this.name !== null && this.buffer !== null;
}
HasTransformation ()
{
if (!CoordIsEqual2D (this.offset, new Coord2D (0.0, 0.0))) {
return true;
}
if (!CoordIsEqual2D (this.scale, new Coord2D (1.0, 1.0))) {
return true;
}
if (!IsEqual (this.rotation, 0.0)) {
return true;
}
return false;
}
IsEqual (rhs)
{
if (this.name !== rhs.name) {
return false;
}
if (this.mimeType !== rhs.mimeType) {
return false;
}
if (!CoordIsEqual2D (this.offset, rhs.offset)) {
return false;
}
if (!CoordIsEqual2D (this.scale, rhs.scale)) {
return false;
}
if (!IsEqual (this.rotation, rhs.rotation)) {
return false;
}
return true;
}
}
function TextureMapIsEqual (aTex, bTex)
{
if (aTex === null && bTex === null) {
return true;
} else if (aTex === null || bTex === null) {
return false;
}
return aTex.IsEqual (bTex);
}
const MaterialType =
{
Phong : 1,
Physical : 2
};
const MaterialSource =
{
Model : 1,
DefaultFace : 2,
DefaultLine : 3
};
class MaterialBase
{
constructor (type)
{
this.type = type;
this.source = MaterialSource.Model;
this.name = '';
this.color = new RGBColor (0, 0, 0);
this.vertexColors = false;
}
IsEqual (rhs)
{
if (this.type !== rhs.type) {
return false;
}
if (this.source !== rhs.source) {
return false;
}
if (this.name !== rhs.name) {
return false;
}
if (!RGBColorIsEqual (this.color, rhs.color)) {
return false;
}
if (this.vertexColors !== rhs.vertexColors) {
return false;
}
return true;
}
}
class FaceMaterial extends MaterialBase
{
constructor (type)
{
super (type);
this.emissive = new RGBColor (0, 0, 0);
this.opacity = 1.0; // 0.0 .. 1.0
this.transparent = false;
this.diffuseMap = null;
this.bumpMap = null;
this.normalMap = null;
this.emissiveMap = null;
this.alphaTest = 0.0; // 0.0 .. 1.0
this.multiplyDiffuseMap = false;
}
IsEqual (rhs)
{
if (!super.IsEqual (rhs)) {
return false;
}
if (!RGBColorIsEqual (this.emissive, rhs.emissive)) {
return false;
}
if (!IsEqual (this.opacity, rhs.opacity)) {
return false;
}
if (this.transparent !== rhs.transparent) {
return false;
}
if (!TextureMapIsEqual (this.diffuseMap, rhs.diffuseMap)) {
return false;
}
if (!TextureMapIsEqual (this.bumpMap, rhs.bumpMap)) {
return false;
}
if (!TextureMapIsEqual (this.normalMap, rhs.normalMap)) {
return false;
}
if (!TextureMapIsEqual (this.emissiveMap, rhs.emissiveMap)) {
return false;
}
if (!IsEqual (this.alphaTest, rhs.alphaTest)) {
return false;
}
if (this.multiplyDiffuseMap !== rhs.multiplyDiffuseMap) {
return false;
}
return true;
}
}
class PhongMaterial extends FaceMaterial
{
constructor ()
{
super (MaterialType.Phong);
this.ambient = new RGBColor (0, 0, 0);
this.specular = new RGBColor (0, 0, 0);
this.shininess = 0.0; // 0.0 .. 1.0
this.specularMap = null;
}
IsEqual (rhs)
{
if (!super.IsEqual (rhs)) {
return false;
}
if (!RGBColorIsEqual (this.ambient, rhs.ambient)) {
return false;
}
if (!RGBColorIsEqual (this.specular, rhs.specular)) {
return false;
}
if (!IsEqual (this.shininess, rhs.shininess)) {
return false;
}
if (!TextureMapIsEqual (this.specularMap, rhs.specularMap)) {
return false;
}
return true;
}
}
class PhysicalMaterial extends FaceMaterial
{
constructor ()
{
super (MaterialType.Physical);
this.metalness = 0.0; // 0.0 .. 1.0
this.roughness = 1.0; // 0.0 .. 1.0
this.metalnessMap = null;
}
IsEqual (rhs)
{
if (!super.IsEqual (rhs)) {
return false;
}
if (!IsEqual (this.metalness, rhs.metalness)) {
return false;
}
if (!IsEqual (this.roughness, rhs.roughness)) {
return false;
}
if (!TextureMapIsEqual (this.metalnessMap, rhs.metalnessMap)) {
return false;
}
return true;
}
}
function TextureIsEqual (a, b)
{
if (a.name !== b.name) {
return false;
}
if (a.mimeType !== b.mimeType) {
return false;
}
if (!CoordIsEqual2D (a.offset, b.offset)) {
return false;
}
if (!CoordIsEqual2D (a.scale, b.scale)) {
return false;
}
if (!IsEqual (a.rotation, b.rotation)) {
return false;
}
return true;
}
let loadedExternalLibUrls = new Set ();
function LoadExternalLibraryFromUrl (libraryUrl)
{
return new Promise ((resolve, reject) => {
if (loadedExternalLibUrls.has (libraryUrl)) {
resolve ();
return;
}
let scriptElement = document.createElement ('script');
scriptElement.type = 'text/javascript';
scriptElement.src = libraryUrl;
scriptElement.onload = () => {
loadedExternalLibUrls.add (libraryUrl);
resolve ();
};
scriptElement.onerror = () => {
reject ();
};
document.head.appendChild (scriptElement);
});
}
function NameFromLine (line, startIndex, commentChar)
{
let name = line.substring (startIndex);
let commentStart = name.indexOf (commentChar);
if (commentStart !== -1) {
name = name.substring (0, commentStart);
}
return name.trim ();
}
function ParametersFromLine (line, commentChar)
{
if (commentChar !== null) {
let commentStart = line.indexOf (commentChar);
if (commentStart !== -1) {
line = line.substring (0, commentStart).trim ();
}
}
return line.split (/\s+/u);
}
function ReadLines (str, onLine)
{
function LineFound (line, onLine)
{
let trimmed = line.trim ();
if (trimmed.length > 0) {
onLine (trimmed);
}
}
let cursor = 0;
let next = str.indexOf ('\n', cursor);
while (next !== -1) {
LineFound (str.substring (cursor, next), onLine);
cursor = next + 1;
next = str.indexOf ('\n', cursor);
}
LineFound (str.substring (cursor), onLine);
}
function IsPowerOfTwo (x)
{
return (x & (x - 1)) === 0;
}
function NextPowerOfTwo (x)
{
if (IsPowerOfTwo (x)) {
return x;
}
let npot = Math.pow (2, Math.ceil (Math.log (x) / Math.log (2)));
return parseInt (npot, 10);
}
function UpdateMaterialTransparency (material)
{
material.transparent = false;
if (IsLower (material.opacity, 1.0)) {
material.transparent = true;
}
}
class ColorToMaterialConverter
{
constructor (model)
{
this.model = model;
this.colorToMaterialIndex = new Map ();
}
GetMaterialIndex (r, g, b, a)
{
let colorKey =
IntegerToHexString (r) +
IntegerToHexString (g) +
IntegerToHexString (b);
let hasAlpha = (a !== undefined && a !== null);
if (hasAlpha) {
colorKey += IntegerToHexString (a);
}
if (this.colorToMaterialIndex.has (colorKey)) {
return this.colorToMaterialIndex.get (colorKey);
} else {
let material = new PhongMaterial ();
material.name = colorKey.toUpperCase ();
material.color = new RGBColor (r, g, b);
if (hasAlpha && a < 255) {
material.opacity = a / 255.0;
UpdateMaterialTransparency (material);
}
let materialIndex = this.model.AddMaterial (material);
this.colorToMaterialIndex.set (colorKey, materialIndex);
return materialIndex;
}
}
}
let occtWorkerUrl = null;
function CreateOcctWorker (worker)
{
return new Promise ((resolve, reject) => {
if (occtWorkerUrl !== null) {
resolve (new Worker (occtWorkerUrl));
return;
}
let baseUrl = 'https://cdn.jsdelivr.net/npm/occt-import-js@0.0.22/dist/';
fetch (baseUrl + 'occt-import-js-worker.js')
.then ((response) => {
if (!response.ok) {
return reject ();
}
return response.text ();
})
.then ((workerScript) => {
workerScript = workerScript.replace ('occt-import-js.js', baseUrl + 'occt-import-js.js');
workerScript = workerScript.replace ('return path', 'return \'' + baseUrl + 'occt-import-js.wasm\'');
let blob = new Blob ([workerScript], { type : 'text/javascript' });
occtWorkerUrl = URL.createObjectURL (blob);
return resolve (new Worker (occtWorkerUrl));
})
.catch (reject);
});
}
function LoadExternalLibrary (libraryName)
{
if (libraryName === 'rhino3dm') {
return LoadExternalLibraryFromUrl ('https://cdn.jsdelivr.net/npm/rhino3dm@8.17.0/rhino3dm.min.js');
} else if (libraryName === 'webifc') {
return LoadExternalLibraryFromUrl ('https://cdn.jsdelivr.net/npm/web-ifc@0.0.68/web-ifc-api-iife.js');
} else if (libraryName === 'draco3d') {
return LoadExternalLibraryFromUrl ('https://cdn.jsdelivr.net/npm/draco3d@1.5.7/draco_decoder_nodejs.min.js');
} else {
return null;
}
}
/**
* File source identifier for import.
* @enum
*/
const FileSource =
{
/** The file is provided by a URL. */
Url : 1,
/** The file is provided by a {@link File} object. */
File : 2,
/** Used internally if a file is originated by a compressed archive. */
Decompressed : 3
};
const FileFormat =
{
Text : 1,
Binary : 2
};
function GetFileName (filePath)
{
let fileName = filePath;
let firstParamIndex = fileName.indexOf ('?');
if (firstParamIndex !== -1) {
fileName = fileName.substring (0, firstParamIndex);
}
let firstSeparator = fileName.lastIndexOf ('/');
if (firstSeparator === -1) {
firstSeparator = fileName.lastIndexOf ('\\');
}
if (firstSeparator !== -1) {
fileName = fileName.substring (firstSeparator + 1);
}
return decodeURI (fileName);
}
function GetFileExtension (filePath)
{
let fileName = GetFileName (filePath);
let firstPoint = fileName.lastIndexOf ('.');
if (firstPoint === -1) {
return '';
}
let extension = fileName.substring (firstPoint + 1);
return extension.toLowerCase ();
}
function RequestUrl (url, onProgress)
{
return new Promise ((resolve, reject) => {
let request = new XMLHttpRequest ();
request.open ('GET', url, true);
request.onprogress = (event) => {
onProgress (event.loaded, event.total);
};
request.onload = () => {
if (request.status === 200) {
resolve (request.response);
} else {
reject ();
}
};
request.onerror = () => {
reject ();
};
request.responseType = 'arraybuffer';
request.send (null);
});
}
function ReadFile (file, onProgress)
{
return new Promise ((resolve, reject) => {
let reader = new FileReader ();
reader.onprogress = (event) => {
onProgress (event.loaded, event.total);
};
reader.onloadend = (event) => {
if (event.target.readyState === FileReader.DONE) {
resolve (event.target.result);
}
};
reader.onerror = () => {
reject ();
};
reader.readAsArrayBuffer (file);
});
}
function TransformFileHostUrls (urls)
{
for (let i = 0; i < urls.length; i++) {
let url = urls[i];
if (url.indexOf ('www.dropbox.com') !== -1) {
url = url.replace ('www.dropbox.com', 'dl.dropbox.com');
urls[i] = url;
} else if (url.indexOf ('github.com') !== -1) {
url = url.replace ('github.com', 'raw.githubusercontent.com');
url = url.replace ('/blob', '');
urls[i] = url;
}
}
}
function IsUrl (str)
{
const regex = /^https?:\/\/\S+$/g;
const match = str.match (regex);
return match !== null;
}
class Coord3D
{
constructor (x, y, z)
{
this.x = x;
this.y = y;
this.z = z;
}
Length ()
{
return Math.sqrt (this.x * this.x + this.y * this.y + this.z * this.z);
}
MultiplyScalar (scalar)
{
this.x *= scalar;
this.y *= scalar;
this.z *= scalar;
return this;
}
Normalize ()
{
let length = this.Length ();
if (length > 0.0) {
this.MultiplyScalar (1.0 / length);
}
return this;
}
Offset (direction, distance)
{
let normal = direction.Clone ().Normalize ();
this.x += normal.x * distance;
this.y += normal.y * distance;
this.z += normal.z * distance;
return this;
}
Rotate (axis, angle, origo)
{
let normal = axis.Clone ().Normalize ();
let u = normal.x;
let v = normal.y;
let w = normal.z;
let x = this.x - origo.x;
let y = this.y - origo.y;
let z = this.z - origo.z;
let si = Math.sin (angle);
let co = Math.cos (angle);
this.x = - u * (- u * x - v * y - w * z) * (1.0 - co) + x * co + (- w * y + v * z) * si;
this.y = - v * (- u * x - v * y - w * z) * (1.0 - co) + y * co + (w * x - u * z) * si;
this.z = - w * (- u * x - v * y - w * z) * (1.0 - co) + z * co + (- v * x + u * y) * si;
this.x += origo.x;
this.y += origo.y;
this.z += origo.z;
return this;
}
Clone ()
{
return new Coord3D (this.x, this.y, this.z);
}
}
function CoordIsEqual3D (a, b)
{
return IsEqual (a.x, b.x) && IsEqual (a.y, b.y) && IsEqual (a.z, b.z);
}
function AddCoord3D (a, b)
{
return new Coord3D (a.x + b.x, a.y + b.y, a.z + b.z);
}
function SubCoord3D (a, b)
{
return new Coord3D (a.x - b.x, a.y - b.y, a.z - b.z);
}
function CoordDistance3D (a, b)
{
return Math.sqrt ((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y) + (a.z - b.z) * (a.z - b.z));
}
function DotVector3D (a, b)
{
return a.x * b.x + a.y * b.y + a.z * b.z;
}
function VectorAngle3D (a, b)
{
let aDirection = a.Clone ().Normalize ();
let bDirection = b.Clone ().Normalize ();
if (CoordIsEqual3D (aDirection, bDirection)) {
return 0.0;
}
let product = DotVector3D (aDirection, bDirection);
return Math.acos (product);
}
function CrossVector3D (a, b)
{
let result = new Coord3D (0.0, 0.0, 0.0);
result.x = a.y * b.z - a.z * b.y;
result.y = a.z * b.x - a.x * b.z;
result.z = a.x * b.y - a.y * b.x;
return result;
}
function VectorLength3D (x, y, z)
{
return Math.sqrt (x * x + y * y + z * z);
}
function ArrayToCoord3D (arr)
{
return new Coord3D (arr[0], arr[1], arr[2]);
}
class MeshPrimitiveBuffer
{
constructor ()
{
this.indices = [];
this.vertices = [];
this.colors = [];
this.normals = [];
this.uvs = [];
this.material = null;
}
GetBounds ()
{
let min = [Infinity, Infinity, Infinity];
let max = [-Infinity, -Infinity, -Infinity];
for (let i = 0; i < this.vertices.length / 3; i++) {
for (let j = 0; j < 3; j++) {
min[j] = Math.min (min[j], this.vertices[i * 3 + j]);
max[j] = Math.max (max[j], this.vertices[i * 3 + j]);
}
}
return {
min : min,
max : max
};
}
GetByteLength (indexTypeSize, numberTypeSize)
{
let indexCount = this.indices.length;
let numberCount = this.vertices.length + this.colors.length + this.normals.length + this.uvs.length;
return indexCount * indexTypeSize + numberCount * numberTypeSize;
}
}
class MeshBuffer
{
constructor ()
{
this.primitives = [];
}
PrimitiveCount ()
{
return this.primitives.length;
}
GetPrimitive (index)
{
return this.primitives[index];
}
GetByteLength (indexTypeSize, numberTypeSize)
{
let byteLength = 0;
for (let i = 0; i < this.primitives.length; i++) {
let primitive = this.primitives[i];
byteLength += primitive.GetByteLength (indexTypeSize, numberTypeSize);
}
return byteLength;
}
}
function ConvertMeshToMeshBuffer (mesh)
{
function AddVertexToPrimitiveBuffer (mesh, indices, primitiveBuffer, meshVertexToPrimitiveVertices)
{
function GetColorOrDefault (mesh, colorIndex, forceColors)
{
if (colorIndex !== null) {
return mesh.GetVertexColor (colorIndex);
} else if (forceColors) {
return new RGBColor (0, 0, 0);
} else {
return null;
}
}
function GetUVOrDefault (mesh, uvIndex, forceUVs)
{
if (uvIndex !== null) {
return mesh.GetTextureUV (uvIndex);
} else if (forceUVs) {
return new Coord2D (0.0, 0.0);
} else {
return null;
}
}
function AddVertex (mesh, indices, primitiveBuffer)
{
let forceColors = mesh.VertexColorCount () > 0;
let forceUVs = mesh.TextureUVCount () > 0;
let vertex = mesh.GetVertex (indices.vertex);
let normal = mesh.GetNormal (indices.normal);
let primitiveVertexIndex = primitiveBuffer.vertices.length / 3;
primitiveBuffer.indices.push (primitiveVertexIndex);
primitiveBuffer.vertices.push (vertex.x, vertex.y, vertex.z);
let color = GetColorOrDefault (mesh, indices.color, forceColors);
if (color !== null) {
primitiveBuffer.colors.push (color.r / 255.0, color.g / 255.0, color.b / 255.0);
}
primitiveBuffer.normals.push (normal.x, normal.y, normal.z);
let uv = GetUVOrDefault (mesh, indices.uv, forceUVs);
if (uv !== null) {
primitiveBuffer.uvs.push (uv.x, uv.y);
}
return {
index : primitiveVertexIndex,
color : color,
normal : normal,
uv : uv
};
}
function FindMatchingPrimitiveVertex (mesh, primitiveVertices, indices)
{
function IsEqualColor (mesh, colorIndex, existingColor)
{
if (existingColor === null && colorIndex === null) {
return true;
}
let color = GetColorOrDefault (mesh, colorIndex, true);
return RGBColorIsEqual (existingColor, color);
}
function IsEqualNormal (mesh, normalIndex, existingNormal)
{
let normal = mesh.GetNormal (normalIndex);
return CoordIsEqual3D (existingNormal, normal);
}
function IsEqualUV (mesh, uvIndex, existingUv)
{
if (existingUv === null && uvIndex === null) {
return true;
}
let uv = GetUVOrDefault (mesh, uvIndex, true);
return CoordIsEqual2D (existingUv, uv);
}
for (let i = 0; i < primitiveVertices.length; i++) {
let primitiveVertex = primitiveVertices[i];
let equalColor = IsEqualColor (mesh, indices.color, primitiveVertex.color);
let equalNormal = IsEqualNormal (mesh, indices.normal, primitiveVertex.normal);
let equalUv = IsEqualUV (mesh, indices.uv, primitiveVertex.uv);
if (equalColor && equalNormal && equalUv) {
return primitiveVertex;
}
}
return null;
}
if (meshVertexToPrimitiveVertices.has (indices.vertex)) {
let primitiveVertices = meshVertexToPrimitiveVertices.get (indices.vertex);
let existingPrimitiveVertex = FindMatchingPrimitiveVertex (mesh, primitiveVertices, indices);
if (existingPrimitiveVertex !== null) {
primitiveBuffer.indices.push (existingPrimitiveVertex.index);
} else {
let primitiveVertex = AddVertex (mesh, indices, primitiveBuffer);
primitiveVertices.push (primitiveVertex);
}
} else {
let primitiveVertex = AddVertex (mesh, indices, primitiveBuffer);
meshVertexToPrimitiveVertices.set (indices.vertex, [primitiveVertex]);
}
}
let meshBuffer = new MeshBuffer ();
let triangleCount = mesh.TriangleCount ();
if (triangleCount === 0) {
return null;
}
let triangleIndices = [];
for (let i = 0; i < triangleCount; i++) {
triangleIndices.push (i);
}
triangleIndices.sort ((a, b) => {
let aTriangle = mesh.GetTriangle (a);
let bTriangle = mesh.GetTriangle (b);
return aTriangle.mat - bTriangle.mat;
});
let primitiveBuffer = null;
let meshVertexToPrimitiveVertices = null;
for (let i = 0; i < triangleIndices.length; i++) {
let triangleIndex = triangleIndices[i];
let triangle = mesh.GetTriangle (triangleIndex);
if (primitiveBuffer === null || primitiveBuffer.material !== triangle.mat) {
primitiveBuffer = new MeshPrimitiveBuffer ();
primitiveBuffer.material = triangle.mat;
meshVertexToPrimitiveVertices = new Map ();
meshBuffer.primitives.push (primitiveBuffer);
}
let v0Indices = {
vertex : triangle.v0,
color : triangle.c0,
normal : triangle.n0,
uv : triangle.u0
};
let v1Indices = {
vertex : triangle.v1,
color : triangle.c1,
normal : triangle.n1,
uv : triangle.u1
};
let v2Indices = {
vertex : triangle.v2,
color : triangle.c2,
normal : triangle.n2,
uv : triangle.u2
};
AddVertexToPrimitiveBuffer (mesh, v0Indices, primitiveBuffer, meshVertexToPrimitiveVertices);
AddVertexToPrimitiveBuffer (mesh, v1Indices, primitiveBuffer, meshVertexToPrimitiveVertices);
AddVertexToPrimitiveBuffer (mesh, v2Indices, primitiveBuffer, meshVertexToPrimitiveVertices);
}
return meshBuffer;
}
function ArrayBufferToUtf8String (buffer)
{
let decoder = new TextDecoder ('utf-8');
return decoder.decode (buffer);
}
function ArrayBufferToAsciiString (buffer)
{
let text = '';
let bufferView = new Uint8Array (buffer);
for (let i = 0; i < bufferView.byteLength; i++) {
text += String.fromCharCode (bufferView[i]);
}
return text;
}
function AsciiStringToArrayBuffer (str)
{
let buffer = new ArrayBuffer (str.length);
let bufferView = new Uint8Array (buffer);
for (let i = 0; i < str.length; i++) {
bufferView[i] = str.charCodeAt (i);
}
return buffer;
}
function Utf8StringToArrayBuffer (str)
{
let encoder = new TextEncoder ();
let uint8Array = encoder.encode (str);
return uint8Array.buffer;
}
function Base64DataURIToArrayBuffer (uri)
{
let dataPrefix = 'data:';
if (!uri.startsWith (dataPrefix)) {
return null;
}
let mimeSeparator = uri.indexOf (';');
if (mimeSeparator === -1) {
return null;
}
let bufferSeparator = uri.indexOf (',');
if (bufferSeparator === -1) {
return null;
}
let mimeType = uri.substring (dataPrefix.length, dataPrefix.length + mimeSeparator - 5);
let base64String = atob (uri.substring (bufferSeparator + 1));
let buffer = new ArrayBuffer (base64String.length);
let bufferView = new Uint8Array (buffer);
for (let i = 0; i < base64String.length; i++) {
bufferView[i] = base64String.charCodeAt (i);
}
return {
mimeType : mimeType,
buffer : buffer
};
}
function GetFileExtensionFromMimeType (mimeType)
{
if (mimeType === undefined || mimeType === null) {
return '';
}
let mimeParts = mimeType.split ('/');
if (mimeParts.length === 0) {
return '';
}
return mimeParts[mimeParts.length - 1];
}
function CreateObjectUrl (content)
{
let blob = new Blob ([content]);
let url = URL.createObjectURL (blob);
return url;
}
function CreateObjectUrlWithMimeType (content, mimeType)
{
let blob = new Blob ([content], { type : mimeType });
let url = URL.createObjectURL (blob);
return url;
}
function RevokeObjectUrl (url)
{
URL.revokeObjectURL (url);
}
class ExportedFile
{
constructor (name)
{
this.name = name;
this.content = null;
}
GetName ()
{
return this.name;
}
SetName (name)
{
this.name = name;
}
GetTextContent ()
{
let text = ArrayBufferToUtf8String (this.content);
return text;
}
GetBufferContent ()
{
return this.content;
}
SetTextContent (content)
{
let buffer = Utf8StringToArrayBuffer (content);
this.content = buffer;
}
SetBufferContent (content)
{
this.content = content;
}
}
class ExporterBase
{
constructor ()
{
}
CanExport (format, extension)
{
return false;
}
Export (exporterModel, format, onFinish)
{
let files = [];
this.ExportContent (exporterModel, format, files, () => {
onFinish (files);
});
}
ExportContent (exporterModel, format, files, onFinish)
{
}
GetExportedMaterialName (originalName)
{
return this.GetExportedName (originalName, Loc ('Material'));
}
GetExportedMeshName (originalName)
{
return this.GetExportedName (originalName, Loc ('Mesh'));
}
GetExportedName (originalName, defaultName)
{
if (originalName.length === 0) {
return defaultName;
}
return originalName;
}
}
class Exporter3dm extends ExporterBase
{
constructor ()
{
super ();
this.rhino = null;
}
CanExport (format, extension)
{
return format === FileFormat.Binary && extension === '3dm';
}
ExportContent (exporterModel, format, files, onFinish)
{
if (this.rhino === null) {
LoadExternalLibrary ('rhino3dm').then (() => {
rhino3dm ().then ((rhino) => {
this.rhino = rhino;
this.ExportRhinoContent (exporterModel, files, onFinish);
});
}).catch (() => {
onFinish ();
});
} else {
this.ExportRhinoContent (exporterModel, files, onFinish);
}
}
ExportRhinoContent (exporterModel, files, onFinish)
{
function ColorToRhinoColor (color)
{
return {
r : color.r,
g : color.g,
b : color.b,
a : 255
};
}
let rhinoFile = new ExportedFile ('model.3dm');
files.push (rhinoFile);
let rhinoDoc = new this.rhino.File3dm ();
exporterModel.EnumerateTransformedMeshInstances ((mesh) => {
let meshBuffer = ConvertMeshToMeshBuffer (mesh);
for (let primitiveIndex = 0; primitiveIndex < meshBuffer.PrimitiveCount (); primitiveIndex++) {
let primitive = meshBuffer.GetPrimitive (primitiveIndex);
let threeJson = {
data : {
attributes : {
position : {
itemSize : 3,
type : 'Float32Array',
array : primitive.vertices
},
normal : {
itemSize : 3,
type : 'Float32Array',
array : primitive.normals
}
},
index : {
type : 'Uint16Array',
array : primitive.indices
}
}
};
let material = exporterModel.GetMaterial (primitive.material);
let rhinoMaterial = new this.rhino.Material ();
rhinoMaterial.name = this.GetExportedMaterialName (material.name);
if (material.type === MaterialType.Phong) {
rhinoMaterial.ambientColor = ColorToRhinoColor (material.ambient);
rhinoMaterial.specularColor = ColorToRhinoColor (material.specular);
}
rhinoMaterial.diffuseColor = ColorToRhinoColor (material.color);
rhinoMaterial.transparency = 1.0 - material.opacity;
let rhinoMaterialIndex = rhinoDoc.materials ().count;
rhinoDoc.materials ().add (rhinoMaterial);
let rhinoMesh = new this.rhino.Mesh.createFromThreejsJSON (threeJson);
let rhinoAttributes = new this.rhino.ObjectAttributes ();
rhinoAttributes.name = this.GetExportedMeshName (mesh.GetName ());
rhinoAttributes.materialSource = this.rhino.ObjectMaterialSource.MaterialFromObject;
rhinoAttributes.materialIndex = rhinoMaterialIndex;
rhinoDoc.objects ().add (rhinoMesh, rhinoAttributes);
}
});
let writeOptions = new this.rhino.File3dmWriteOptions ();
writeOptions.version = 6;
let rhinoDocBuffer = rhinoDoc.toByteArrayOptions (writeOptions);
rhinoFile.SetBufferContent (rhinoDocBuffer);
onFinish ();
}
}
const PropertyType =
{
Text : 1,
Integer : 2,
Number : 3,
Boolean : 4,
Percent : 5,
Color : 6
};
class Property
{
constructor (type, name, value)
{
this.type = type;
this.name = name;
this.value = value;
}
Clone ()
{
const clonable = (this.type === PropertyType.Color);
if (clonable) {
return new Property (this.type, this.name, this.value.Clone ());
} else {
return new Property (this.type, this.name, this.value);
}
}
}
class PropertyGroup
{
constructor (name)
{
this.name = name;
this.properties = [];
}
PropertyCount ()
{
return this.properties.length;
}
AddProperty (property)
{
this.properties.push (property);
}
GetProperty (index)
{
return this.properties[index];
}
Clone ()
{
let cloned = new PropertyGroup (this.name);
for (let property of this.properties) {
cloned.AddProperty (property.Clone ());
}
return cloned;
}
}
function PropertyToString (property)
{
if (property.type === PropertyType.Text) {
return EscapeHtmlChars (property.value);
} else if (property.type === PropertyType.Integer) {
return property.value.toLocaleString ();
} else if (property.type === PropertyType.Number) {
return property.value.toLocaleString (undefined, {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
} else if (property.type === PropertyType.Boolean) {
return property.value ? Loc ('True') : Loc ('False');
} else if (property.type === PropertyType.Percent) {
return parseInt (property.value * 100, 10).toString () + '%';
} else if (property.type === PropertyType.Color) {
return '#' + RGBColorToHexString (property.value);
}
return null;
}
function GenerateGuid ()
{
// https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid
let template = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
return template.replace (/[xy]/g, (c) => {
let r = Math.random () * 16 | 0;
let v = (c === 'x') ? r : (r & 0x3 | 0x8);
return v.toString (16);
});
}
class ExporterBim extends ExporterBase
{
constructor ()
{
super ();
}
CanExport (format, extension)
{
return format === FileFormat.Text && extension === 'bim';
}
ExportContent (exporterModel, format, files, onFinish)
{
let bimContent = {
schema_version : '1.1.0',
meshes : [],
elements : [],
info : {}
};
this.ExportProperties (exporterModel.GetModel (), bimContent.info);
let meshId = 0;
exporterModel.EnumerateTransformedMeshInstances ((mesh) => {
let bimMesh = {
mesh_id : meshId,
coordinates : [],
indices : []
};
mesh.EnumerateVertices ((vertex) => {
bimMesh.coordinates.push (vertex.x, vertex.y, vertex.z);
});
mesh.EnumerateTriangleVertexIndices ((v0, v1, v2) => {
bimMesh.indices.push (v0, v1, v2);
});
let bimElement = {
mesh_id : meshId,
type : 'Other',
color : {
r : 200,
g : 200,
b : 200,
a : 255
},
vector : {
x : 0.0,
y : 0.0,
z : 0.0
},
rotation : {
qx: 0.0,
qy: 0.0,
qz: 0.0,
qw: 1.0
},
guid : GenerateGuid (),
info : {}
};
let defaultColor = null;
let hasOnlyOneColor = true;
let faceColors = [];
for (let i = 0; i < mesh.TriangleCount (); i++) {
let triangle = mesh.GetTriangle (i);
let material = exporterModel.GetMaterial (triangle.mat);
let faceColor = {
r : Math.round (material.color.r),
g : Math.round (material.color.g),
b : Math.round (material.color.b),
a : ColorComponentFromFloat (material.opacity),
};
faceColors.push (faceColor.r, faceColor.g, faceColor.b, faceColor.a);
if (hasOnlyOneColor) {
if (defaultColor === null) {
defaultColor = faceColor;
} else {
if (defaultColor.r !== faceColor.r || defaultColor.g !== faceColor.g || defaultColor.b !== faceColor.b || defaultColor.a !== faceColor.a) {
hasOnlyOneColor = false;
defaultColor = null;
}
}
}
}
if (hasOnlyOneColor) {
bimElement.color = defaultColor;
} else {
bimElement.face_colors = faceColors;
}
bimElement.info['Name'] = mesh.GetName ();
this.ExportProperties (mesh, bimElement.info);
bimContent.meshes.push (bimMesh);
bimContent.elements.push (bimElement);
meshId += 1;
});
let bimFile = new ExportedFile ('model.bim');
bimFile.SetTextContent (JSON.stringify (bimContent, null, 4));
files.push (bimFile);
onFinish ();
}
ExportProperties (element, targetObject)
{
for (let groupIndex = 0; groupIndex < element.PropertyGroupCount (); groupIndex++) {
let group = element.GetPropertyGroup (groupIndex);
for (let propertyIndex = 0; propertyIndex < group.PropertyCount (); propertyIndex++) {
let property = group.GetProperty (propertyIndex);
targetObject[property.name] = PropertyToString (property);
}
}
}
}
class BinaryWriter
{
constructor (byteLength, isLittleEndian)
{
this.arrayBuffer = new ArrayBuffer (byteLength);
this.dataView = new DataView (this.arrayBuffer);
this.isLittleEndian = isLittleEndian;
this.position = 0;
}
GetPosition ()
{
return this.position;
}
SetPosition (position)
{
this.position = position;
}
End ()
{
return this.position >= this.arrayBuffer.byteLength;
}
GetBuffer ()
{
return this.arrayBuffer;
}
WriteArrayBuffer (arrayBuffer)
{
let bufferView = new Uint8Array (arrayBuffer);
let thisBufferView = new Uint8Array (this.arrayBuffer);
thisBufferView.set (bufferView, this.position);
this.position += arrayBuffer.byteLength;
}
WriteBoolean8 (val)
{
this.dataView.setInt8 (this.position, val ? 1 : 0);
this.position = this.position + 1;
}
WriteCharacter8 (val)
{
this.dataView.setInt8 (this.position, val);
this.position = this.position + 1;
}
WriteUnsignedCharacter8 (val)
{
this.dataView.setUint8 (this.position, val);
this.position = this.position + 1;
}
WriteInteger16 (val)
{
this.dataView.setInt16 (this.position, val, this.isLittleEndian);
this.position = this.position + 2;
}
WriteUnsignedInteger16 (val)
{
this.dataView.setUint16 (this.position, val, this.isLittleEndian);
this.position = this.position + 2;
}
WriteInteger32 (val)
{
this.dataView.setInt32 (this.position, val, this.isLittleEndian);
this.position = this.position + 4;
}
WriteUnsignedInteger32 (val)
{
this.dataView.setUint32 (this.position, val, this.isLittleEndian);
this.position = this.position + 4;
}
WriteFloat32 (val)
{
this.dataView.setFloat32 (this.position, val, this.isLittleEndian);
this.position = this.position + 4;
}
WriteDouble64 (val)
{
this.dataView.setFloat64 (this.position, val, this.isLittleEndian);
this.position = this.position + 8;
}
}
class Coord4D
{
constructor (x, y, z, w)
{
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}
Clone ()
{
return new Coord4D (this.x, this.y, this.z, this.w);
}
}
class Quaternion
{
constructor (x, y, z, w)
{
this.x = x;
this.y = y;
this.z = z;
this.w = w;
}