dxf-viewer
Version:
JavaScript DXF file viewer
1,091 lines (969 loc) • 36.7 kB
JavaScript
import * as three from "three"
import {BatchingKey} from "./BatchingKey.js"
import {DxfWorker} from "./DxfWorker.js"
import {MaterialKey} from "./MaterialKey.js"
import {ColorCode, DxfScene} from "./DxfScene.js"
import {OrbitControls} from "./OrbitControls.js"
import {RBTree} from "./RBTree.js"
/** Level in "message" events. */
const MessageLevel = Object.freeze({
INFO: "info",
WARN: "warn",
ERROR: "error"
})
/** The representation class for the viewer, based on Three.js WebGL renderer. */
export class DxfViewer {
/**
* @param domContainer Container element to create the canvas in. Usually empty div. Should not
* have padding if auto-resize feature is used.
* @param options Some options can be overridden if specified. See DxfViewer.DefaultOptions.
*/
constructor(domContainer, options = null) {
this.domContainer = domContainer
this.options = Object.create(DxfViewer.DefaultOptions)
if (options) {
Object.assign(this.options, options)
}
options = this.options
this.clearColor = this.options.clearColor.getHex()
this.scene = new three.Scene()
try {
this.renderer = new three.WebGLRenderer({
alpha: options.canvasAlpha,
premultipliedAlpha: options.canvasPremultipliedAlpha,
antialias: options.antialias,
depth: false,
preserveDrawingBuffer: options.preserveDrawingBuffer
})
} catch (e) {
console.log("Failed to create renderer: " + e)
this.renderer = null
return
}
const renderer = this.renderer
/* Prevent bounding spheres calculations which fails due to non-conventional geometry
* buffers layout. Also do not waste CPU on sorting which we do not need anyway.
*/
renderer.sortObjects = false
renderer.setPixelRatio(window.devicePixelRatio)
const camera = this.camera = new three.OrthographicCamera(-1, 1, 1, -1, 0.1, 2);
camera.position.z = 1
camera.position.x = 0
camera.position.y = 0
this.simpleColorMaterial = []
this.simplePointMaterial = []
for (let i = 0; i < InstanceType.MAX; i++) {
this.simpleColorMaterial[i] = this._CreateSimpleColorMaterial(i)
this.simplePointMaterial[i] = this._CreateSimplePointMaterial(i)
}
renderer.setClearColor(options.clearColor, options.clearAlpha)
if (options.autoResize) {
this.canvasWidth = domContainer.clientWidth
this.canvasHeight = domContainer.clientHeight
domContainer.style.position = "relative"
} else {
this.canvasWidth = options.canvasWidth
this.canvasHeight = options.canvasHeight
this.resizeObserver = null
}
renderer.setSize(this.canvasWidth, this.canvasHeight)
this.canvas = renderer.domElement
domContainer.style.display = "block"
if (options.autoResize) {
this.canvas.style.position = "absolute"
this.resizeObserver = new ResizeObserver(entries => this._OnResize(entries[0]))
this.resizeObserver.observe(domContainer)
}
domContainer.appendChild(this.canvas)
this.canvas.addEventListener("pointerdown", this._OnPointerEvent.bind(this))
this.canvas.addEventListener("pointerup", this._OnPointerEvent.bind(this))
this.Render()
/* Indexed by MaterialKey, value is {key, material}. */
this.materials = new RBTree((m1, m2) => m1.key.Compare(m2.key))
/* Indexed by layer name, value is Layer instance. */
this.layers = new Map()
/* Default layer used when no layer specified. */
this.defaultLayer = null
/* Indexed by block name, value is Block instance. */
this.blocks = new Map()
/** Set during data loading. */
this.worker = null
}
/**
* @returns {boolean} True if renderer exists. May be false in case when WebGL context is lost
* (e.g. after wake up from sleep). In such case page should be reloaded.
*/
HasRenderer() {
return Boolean(this.renderer)
}
/**
* @returns {three.WebGLRenderer | null} Returns the created Three.js renderer.
*/
GetRenderer(){
return this.renderer;
}
GetCanvas() {
return this.canvas
}
GetDxf() {
return this.parsedDxf
}
SetSize(width, height) {
this._EnsureRenderer()
const hScale = width / this.canvasWidth
const vScale = height / this.canvasHeight
const cam = this.camera
const centerX = (cam.left + cam.right) / 2
const centerY = (cam.bottom + cam.top) / 2
const camWidth = cam.right - cam.left
const camHeight = cam.top - cam.bottom
cam.left = centerX - hScale * camWidth / 2
cam.right = centerX + hScale * camWidth / 2
cam.bottom = centerY - vScale * camHeight / 2
cam.top = centerY + vScale * camHeight / 2
cam.updateProjectionMatrix()
this.canvasWidth = width
this.canvasHeight = height
this.renderer.setSize(width, height)
if (this.controls) {
this.controls.update()
}
this._Emit("resized", {width, height})
this._Emit("viewChanged")
this.Render()
}
/** Load DXF into the viewer. Old content is discarded, state is reset.
* @param {string} url DXF file URL.
* @param {?string[]} fonts List of font URLs. Files should have typeface.js format. Fonts are
* used in the specified order, each one is checked until necessary glyph is found. Text is not
* rendered if fonts are not specified.
* @param {?Function} progressCbk (phase, processedSize, totalSize)
* Possible phase values:
* * "font"
* * "fetch"
* * "parse"
* * "prepare"
* @param {?Function} workerFactory Factory for worker creation. The worker script should
* invoke DxfViewer.SetupWorker() function.
*/
async Load({url, fonts = null, progressCbk = null, workerFactory = null}) {
if (url === null || url === undefined) {
throw new Error("`url` parameter is not specified")
}
this._EnsureRenderer()
this.Clear()
this.worker = new DxfWorker(workerFactory ? workerFactory() : null)
const {scene, dxf} = await this.worker.Load(url, fonts, this.options, progressCbk)
await this.worker.Destroy()
this.worker = null
this.parsedDxf = dxf
this.origin = scene.origin
this.bounds = scene.bounds
this.hasMissingChars = scene.hasMissingChars
for (const layer of scene.layers) {
this.layers.set(layer.name, new Layer(layer.name, layer.displayName, layer.color))
}
this.defaultLayer = this.layers.get("0") ?? new Layer("0", "0", 0)
/* Load all blocks on the first pass. */
for (const batch of scene.batches) {
if (batch.key.blockName !== null &&
batch.key.geometryType !== BatchingKey.GeometryType.BLOCK_INSTANCE &&
batch.key.geometryType !== BatchingKey.GeometryType.POINT_INSTANCE) {
let block = this.blocks.get(batch.key.blockName)
if (!block) {
block = new Block()
this.blocks.set(batch.key.blockName, block)
}
block.PushBatch(new Batch(this, scene, batch))
}
}
console.log(`DXF scene:
${scene.batches.length} batches,
${this.layers.size} layers,
${this.blocks.size} blocks,
vertices ${scene.vertices.byteLength} B,
indices ${scene.indices.byteLength} B
transforms ${scene.transforms.byteLength} B`)
/* Instantiate all entities. */
for (const batch of scene.batches) {
this._LoadBatch(scene, batch)
}
this._Emit("loaded")
if (scene.bounds) {
this.FitView(scene.bounds.minX - scene.origin.x, scene.bounds.maxX - scene.origin.x,
scene.bounds.minY - scene.origin.y, scene.bounds.maxY - scene.origin.y)
} else {
this._Message("Empty document", MessageLevel.WARN)
}
if (this.hasMissingChars) {
this._Message("Some characters cannot be properly displayed due to missing fonts",
MessageLevel.WARN)
}
this._CreateControls()
this.Render()
}
Render() {
this._EnsureRenderer()
this.renderer.render(this.scene, this.camera)
}
/** @return {Iterable<{name:String, color:number}>} List of layer names. */
GetLayers(nonEmptyOnly = false) {
const result = []
for (const lyr of this.layers.values()) {
if (nonEmptyOnly && lyr.objects.length == 0) {
continue
}
result.push({
name: lyr.name,
displayName: lyr.displayName,
color: this._TransformColor(lyr.color)
})
}
return result
}
ShowLayer(name, show) {
this._EnsureRenderer()
const layer = this.layers.get(name)
if (!layer) {
return
}
for (const obj of layer.objects) {
obj.visible = show
}
this.Render()
}
/** Reset the viewer state. */
Clear() {
this._EnsureRenderer()
if (this.worker) {
this.worker.Destroy(true)
this.worker = null
}
if (this.controls) {
this.controls.dispose()
this.controls = null
}
this.scene.clear()
for (const layer of this.layers.values()) {
layer.Dispose()
}
this.layers.clear()
this.blocks.clear()
this.materials.each(e => e.material.dispose())
this.materials.clear()
this.SetView({x: 0, y: 0}, 2)
this._Emit("cleared")
this.Render()
}
/** Free all resources. The viewer object should not be used after this method was called. */
Destroy() {
if (!this.HasRenderer()) {
return
}
if (this.resizeObserver) {
this.resizeObserver.disconnect()
}
this.Clear()
this._Emit("destroyed")
for (const m of this.simplePointMaterial) {
m.dispose()
}
for (const m of this.simpleColorMaterial) {
m.dispose()
}
this.simplePointMaterial = null
this.simpleColorMaterial = null
this.renderer.dispose()
this.renderer = null
}
SetView(center, width) {
const aspect = this.canvasWidth / this.canvasHeight
const height = width / aspect
const cam = this.camera
cam.left = -width / 2
cam.right = width / 2
cam.top = height / 2
cam.bottom = -height / 2
cam.zoom = 1
cam.position.set(center.x, center.y, 1)
cam.rotation.set(0, 0, 0)
cam.updateMatrix()
cam.updateProjectionMatrix()
if (this.controls) {
this.controls.target.set(cam.position.x, cam.position.y, 0)
this.controls.update()
}
this._Emit("viewChanged")
}
/** Set view to fit the specified bounds. */
FitView(minX, maxX, minY, maxY, padding = 0.1) {
const aspect = this.canvasWidth / this.canvasHeight
let width = maxX - minX
const height = maxY - minY
const center = {x: minX + width / 2, y: minY + height / 2}
if (height * aspect > width) {
width = height * aspect
}
if (width <= Number.MIN_VALUE * 2) {
width = 1
}
this.SetView(center, width * (1 + padding))
}
/** @return {Scene} three.js scene for the viewer. Can be used to add custom entities on the
* scene. Remember to apply scene origin available via GetOrigin() method.
*/
GetScene() {
return this.scene
}
/** @return {OrthographicCamera} three.js camera for the viewer. */
GetCamera() {
return this.camera
}
/** @return {Vector2} Scene origin in global drawing coordinates. */
GetOrigin() {
return this.origin
}
/**
* @return {?{maxX: number, maxY: number, minX: number, minY: number}} Scene bounds in model
* space coordinates. Null if empty scene.
*/
GetBounds() {
return this.bounds
}
/** Subscribe to the specified event. The following events are defined:
* * "loaded" - new scene loaded.
* * "cleared" - current scene cleared.
* * "destroyed" - viewer instance destroyed.
* * "resized" - viewport size changed. Details: {width, height}
* * "pointerdown" - Details: {domEvent, position:{x,y}}, position is in scene coordinates.
* * "pointerup"
* * "viewChanged"
* * "message" - Some message from the viewer. {message: string, level: string}.
*
* @param eventName {string}
* @param eventHandler {function} Accepts event object.
*/
Subscribe(eventName, eventHandler) {
this._EnsureRenderer()
this.canvas.addEventListener(EVENT_NAME_PREFIX + eventName, eventHandler)
}
/** Unsubscribe from previously subscribed event. The arguments should match previous
* Subscribe() call.
*
* @param eventName {string}
* @param eventHandler {function}
*/
Unsubscribe(eventName, eventHandler) {
this._EnsureRenderer()
this.canvas.removeEventListener(EVENT_NAME_PREFIX + eventName, eventHandler)
}
// /////////////////////////////////////////////////////////////////////////////////////////////
_EnsureRenderer() {
if (!this.HasRenderer()) {
throw new Error("WebGL renderer not available. " +
"Probable WebGL context loss, try refreshing the page.")
}
}
_CreateControls() {
if (this.controls) {
this.controls.dispose()
}
const controls = this.controls = new OrbitControls(this.camera, this.canvas)
controls.enableRotate = false
controls.mouseButtons = {
LEFT: three.MOUSE.PAN,
MIDDLE: three.MOUSE.DOLLY
}
controls.touches = {
ONE: three.TOUCH.PAN,
TWO: three.TOUCH.DOLLY_PAN
}
controls.zoomSpeed = 3
controls.mouseZoomSpeedFactor = 0.05
controls.target = new three.Vector3(this.camera.position.x, this.camera.position.y, 0)
controls.addEventListener("change", () => {
this.Render()
this._Emit("viewChanged")
})
controls.update()
}
_Emit(eventName, data = null) {
this.canvas.dispatchEvent(new CustomEvent(EVENT_NAME_PREFIX + eventName, { detail: data }))
}
_Message(message, level = MessageLevel.INFO) {
this._Emit("message", {message, level})
}
_OnPointerEvent(e) {
const canvasRect = e.target.getBoundingClientRect()
const canvasCoord = {x: e.clientX - canvasRect.left, y: e.clientY - canvasRect.top}
this._Emit(e.type, {
domEvent: e,
canvasCoord,
position: this._CanvasToSceneCoord(canvasCoord.x, canvasCoord.y)
})
}
/** @return {{x,y}} Scene coordinate corresponding to the specified canvas pixel coordinates. */
_CanvasToSceneCoord(x, y) {
const v = new three.Vector3(x * 2 / this.canvasWidth - 1,
-y * 2 / this.canvasHeight + 1,
1).unproject(this.camera)
return {x: v.x, y: v.y}
}
_OnResize(entry) {
this.SetSize(Math.floor(entry.contentRect.width), Math.floor(entry.contentRect.height))
}
_LoadBatch(scene, batch) {
if (batch.key.blockName !== null &&
batch.key.geometryType !== BatchingKey.GeometryType.BLOCK_INSTANCE &&
batch.key.geometryType !== BatchingKey.GeometryType.POINT_INSTANCE) {
/* Block definition. */
return
}
const objects = new Batch(this, scene, batch).CreateObjects()
for (const obj of objects) {
this.scene.add(obj)
const layer = obj._dxfViewerLayer ?? this.defaultLayer
layer.PushObject(obj)
}
}
_GetSimpleColorMaterial(color, instanceType = InstanceType.NONE) {
const key = new MaterialKey(instanceType, null, color, 0)
let entry = this.materials.find({key})
if (entry !== null) {
return entry.material
}
entry = {
key,
material: this._CreateSimpleColorMaterialInstance(color, instanceType)
}
this.materials.insert(entry)
return entry.material
}
_CreateSimpleColorMaterial(instanceType = InstanceType.NONE) {
const shaders = this._GenerateShaders(instanceType, false)
return new three.RawShaderMaterial({
uniforms: {
color: {
value: new three.Color(0xff00ff)
}
},
vertexShader: shaders.vertex,
fragmentShader: shaders.fragment,
depthTest: false,
depthWrite: false,
glslVersion: three.GLSL3,
side: three.DoubleSide
})
}
/** @param color {number} Color RGB numeric value.
* @param instanceType {number}
*/
_CreateSimpleColorMaterialInstance(color, instanceType = InstanceType.NONE) {
const src = this.simpleColorMaterial[instanceType]
/* Should reuse compiled shaders. */
const m = src.clone()
m.uniforms.color = { value: new three.Color(color) }
return m
}
_GetSimplePointMaterial(color, instanceType = InstanceType.NONE) {
const key = new MaterialKey(instanceType, BatchingKey.GeometryType.POINTS, color, 0)
let entry = this.materials.find({key})
if (entry !== null) {
return entry.material
}
entry = {
key,
material: this._CreateSimplePointMaterialInstance(color, this.options.pointSize,
instanceType)
}
this.materials.insert(entry)
return entry.material
}
_CreateSimplePointMaterial(instanceType = InstanceType.NONE) {
const shaders = this._GenerateShaders(instanceType, true)
return new three.RawShaderMaterial({
uniforms: {
color: {
value: new three.Color(0xff00ff)
},
pointSize: {
value: 2
}
},
vertexShader: shaders.vertex,
fragmentShader: shaders.fragment,
depthTest: false,
depthWrite: false,
glslVersion: three.GLSL3
})
}
/** @param color {number} Color RGB numeric value.
* @param size {number} Rasterized point size in pixels.
* @param instanceType {number}
*/
_CreateSimplePointMaterialInstance(color, size = 2, instanceType = InstanceType.NONE) {
const src = this.simplePointMaterial[instanceType]
/* Should reuse compiled shaders. */
const m = src.clone()
m.uniforms.color = { value: new three.Color(color) }
m.uniforms.size = { value: size }
return m
}
_GenerateShaders(instanceType, pointSize) {
const fullInstanceAttr = instanceType === InstanceType.FULL ?
`
/* First row. */
in vec3 instanceTransform0;
/* Second row. */
in vec3 instanceTransform1;
` : ""
const fullInstanceTransform = instanceType === InstanceType.FULL ?
`
pos.xy = mat2(instanceTransform0[0], instanceTransform1[0],
instanceTransform0[1], instanceTransform1[1]) * pos.xy +
vec2(instanceTransform0[2], instanceTransform1[2]);
` : ""
const pointInstanceAttr = instanceType === InstanceType.POINT ?
`
in vec2 instanceTransform;
` : ""
const pointInstanceTransform = instanceType === InstanceType.POINT ?
`
pos.xy += instanceTransform;
` : ""
const pointSizeUniform = pointSize ? "uniform float pointSize;" : ""
const pointSizeAssigment = pointSize ? "gl_PointSize = pointSize;" : ""
return {
vertex: `
precision highp float;
precision highp int;
in vec2 position;
${fullInstanceAttr}
${pointInstanceAttr}
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
${pointSizeUniform}
void main() {
vec4 pos = vec4(position, 0.0, 1.0);
${fullInstanceTransform}
${pointInstanceTransform}
gl_Position = projectionMatrix * modelViewMatrix * pos;
${pointSizeAssigment}
}
`,
fragment: `
precision highp float;
precision highp int;
uniform vec3 color;
out vec4 fragColor;
void main() {
fragColor = vec4(color, 1.0);
}
`
}
}
/** Ensure the color is contrast enough with current background color.
* @param color {number} RGB value.
* @return {number} RGB value to use for rendering.
*/
_TransformColor(color) {
if (!this.options.colorCorrection && !this.options.blackWhiteInversion) {
return color
}
/* Just black and white inversion. */
const bkgLum = Luminance(this.clearColor)
if (color === 0xffffff && bkgLum >= 0.8) {
return 0
}
if (color === 0 && bkgLum <= 0.2) {
return 0xffffff
}
if (!this.options.colorCorrection) {
return color
}
const fgLum = Luminance(color)
const MIN_TARGET_RATIO = 1.5
const contrast = ContrastRatio(color, this.clearColor)
const diff = contrast >= 1 ? contrast : 1 / contrast
if (diff < MIN_TARGET_RATIO) {
let targetLum
if (bkgLum > 0.5) {
targetLum = bkgLum / 2
} else {
targetLum = bkgLum * 2
}
if (targetLum > fgLum) {
color = Lighten(color, targetLum / fgLum)
} else {
color = Darken(color, fgLum / targetLum)
}
}
return color
}
}
DxfViewer.MessageLevel = MessageLevel
DxfViewer.DefaultOptions = {
canvasWidth: 400,
canvasHeight: 300,
/** Automatically resize canvas when the container is resized. This options utilizes
* ResizeObserver API which is still not fully standardized. The specified canvas size is
* ignored if the option is enabled.
*/
autoResize: false,
/** Frame buffer clear color. */
clearColor: new three.Color("#000"),
/** Frame buffer clear color alpha value. */
clearAlpha: 1.0,
/** Use alpha channel in a framebuffer. */
canvasAlpha: false,
/** Assume premultiplied alpha in a framebuffer. */
canvasPremultipliedAlpha: true,
/** Use antialiasing. May degrade performance on poor hardware. */
antialias: true,
/** Correct entities colors to ensure that they are always visible with the current background
* color.
*/
colorCorrection: false,
/** Simpler version of colorCorrection - just invert pure white or black entities if they are
* invisible on current background color.
*/
blackWhiteInversion: true,
/** Size in pixels for rasterized points (dot mark). */
pointSize: 2,
/** Scene generation options. */
sceneOptions: DxfScene.DefaultOptions,
/** Retain the simple object representing the parsed DXF - will consume a lot of additional
* memory.
*/
retainParsedDxf: false,
/** Whether to preserve the buffers until manually cleared or overwritten. */
preserveDrawingBuffer: false,
/** Encoding to use for decoding DXF file text content. DXF files newer than DXF R2004 (AC1018)
* use UTF-8 encoding. Older files use some code page which is specified in $DWGCODEPAGE header
* variable. Currently parser is implemented in such a way that encoding must be specified
* before the content is parsed so there is no chance to use this variable dynamically. This may
* be a subject for future changes. The specified value should be suitable for passing as
* `TextDecoder` constructor `label` parameter.
*/
fileEncoding: "utf-8"
}
DxfViewer.SetupWorker = function () {
new DxfWorker(self, true)
}
const InstanceType = Object.freeze({
/** Not instanced. */
NONE: 0,
/** Full affine transform per instance. */
FULL: 1,
/** Point instances, 2D-translation vector per instance. */
POINT: 2,
/** Number of types. */
MAX: 3
})
class Batch {
/**
* @param {DxfViewer} viewer
* @param scene Serialized scene.
* @param batch Serialized scene batch.
*/
constructor(viewer, scene, batch) {
this.viewer = viewer
this.key = batch.key
if (batch.hasOwnProperty("verticesOffset")) {
const verticesArray =
new Float32Array(scene.vertices,
batch.verticesOffset * Float32Array.BYTES_PER_ELEMENT,
batch.verticesSize)
if (this.key.geometryType !== BatchingKey.GeometryType.POINT_INSTANCE ||
scene.pointShapeHasDot) {
this.vertices = new three.BufferAttribute(verticesArray, 2)
}
if (this.key.geometryType === BatchingKey.GeometryType.POINT_INSTANCE) {
this.transforms = new three.InstancedBufferAttribute(verticesArray, 2)
}
}
if (batch.hasOwnProperty("chunks")) {
this.chunks = []
for (const rawChunk of batch.chunks) {
const verticesArray =
new Float32Array(scene.vertices,
rawChunk.verticesOffset * Float32Array.BYTES_PER_ELEMENT,
rawChunk.verticesSize)
const indicesArray =
new Uint16Array(scene.indices,
rawChunk.indicesOffset * Uint16Array.BYTES_PER_ELEMENT,
rawChunk.indicesSize)
this.chunks.push({
vertices: new three.BufferAttribute(verticesArray, 2),
indices: new three.BufferAttribute(indicesArray, 1)
})
}
}
if (batch.hasOwnProperty("transformsOffset")) {
const transformsArray =
new Float32Array(scene.transforms,
batch.transformsOffset * Float32Array.BYTES_PER_ELEMENT,
batch.transformsSize)
/* Each transform is 3x2 matrix which is split into two 3D vectors which will occupy two
* attribute slots.
*/
const buf = new three.InstancedInterleavedBuffer(transformsArray, 6)
this.transforms0 = new three.InterleavedBufferAttribute(buf, 3, 0)
this.transforms1 = new three.InterleavedBufferAttribute(buf, 3, 3)
}
this.layer = this.key.layerName !== null ? this.viewer.layers.get(this.key.layerName) : null
}
GetInstanceType() {
switch (this.key.geometryType) {
case BatchingKey.GeometryType.BLOCK_INSTANCE:
return InstanceType.FULL
case BatchingKey.GeometryType.POINT_INSTANCE:
return InstanceType.POINT
default:
return InstanceType.NONE
}
}
/** Create scene objects corresponding to batch data.
* @param {?Batch} instanceBatch Batch with instance transform. Null for non-instanced object.
*/
*CreateObjects(instanceBatch = null) {
if (this.key.geometryType === BatchingKey.GeometryType.BLOCK_INSTANCE ||
this.key.geometryType === BatchingKey.GeometryType.POINT_INSTANCE) {
if (instanceBatch !== null) {
throw new Error("Unexpected instance batch specified for instance batch")
}
yield* this._CreateBlockInstanceObjects()
return
}
yield* this._CreateObjects(instanceBatch)
}
*_CreateObjects(instanceBatch) {
const color = instanceBatch ?
instanceBatch._GetInstanceColor(this) : this.key.color
/* INSERT layer (if specified) takes precedence over layer specified in block definition. */
const layer = instanceBatch?.layer ?? this.layer
//XXX line type
const materialFactory =
this.key.geometryType === BatchingKey.GeometryType.POINTS ||
this.key.geometryType === BatchingKey.GeometryType.POINT_INSTANCE ?
this.viewer._GetSimplePointMaterial : this.viewer._GetSimpleColorMaterial
const material = materialFactory.call(this.viewer, this.viewer._TransformColor(color),
instanceBatch?.GetInstanceType() ?? InstanceType.NONE)
let objConstructor
switch (this.key.geometryType) {
case BatchingKey.GeometryType.POINTS:
/* This method also called for creating dots for shaped point instances. */
case BatchingKey.GeometryType.POINT_INSTANCE:
objConstructor = three.Points
break
case BatchingKey.GeometryType.LINES:
case BatchingKey.GeometryType.INDEXED_LINES:
objConstructor = three.LineSegments
break
case BatchingKey.GeometryType.TRIANGLES:
case BatchingKey.GeometryType.INDEXED_TRIANGLES:
objConstructor = three.Mesh
break
default:
throw new Error("Unexpected geometry type:" + this.key.geometryType)
}
function CreateObject(vertices, indices) {
const geometry = instanceBatch ?
new three.InstancedBufferGeometry() : new three.BufferGeometry()
geometry.setAttribute("position", vertices)
instanceBatch?._SetInstanceTransformAttribute(geometry)
if (indices) {
geometry.setIndex(indices)
}
const obj = new objConstructor(geometry, material)
obj.frustumCulled = false
obj.matrixAutoUpdate = false
obj._dxfViewerLayer = layer
return obj
}
if (this.chunks) {
for (const chunk of this.chunks) {
yield CreateObject(chunk.vertices, chunk.indices)
}
} else {
yield CreateObject(this.vertices)
}
}
/**
* @param {InstancedBufferGeometry} geometry
*/
_SetInstanceTransformAttribute(geometry) {
if (!geometry.isInstancedBufferGeometry) {
throw new Error("InstancedBufferGeometry expected")
}
if (this.key.geometryType === BatchingKey.GeometryType.POINT_INSTANCE) {
geometry.setAttribute("instanceTransform", this.transforms)
} else {
geometry.setAttribute("instanceTransform0", this.transforms0)
geometry.setAttribute("instanceTransform1", this.transforms1)
}
}
*_CreateBlockInstanceObjects() {
const block = this.viewer.blocks.get(this.key.blockName)
if (!block) {
return
}
for (const batch of block.batches) {
yield* batch.CreateObjects(this)
}
if (this.vertices) {
/* Dots for point shapes. */
yield* this._CreateObjects()
}
}
/**
* @param {Batch} blockBatch Block definition batch.
* @return {number} RGB color value for a block instance.
*/
_GetInstanceColor(blockBatch) {
const defColor = blockBatch.key.color
if (defColor === ColorCode.BY_BLOCK) {
return this.key.color
} else if (defColor === ColorCode.BY_LAYER) {
if (blockBatch.layer) {
return blockBatch.layer.color
}
return this.layer ? this.layer.color : 0
}
return defColor
}
}
class Layer {
constructor(name, displayName, color) {
this.name = name
this.displayName = displayName
this.color = color
this.objects = []
}
PushObject(obj) {
this.objects.push(obj)
}
Dispose() {
for (const obj of this.objects) {
obj.geometry.dispose()
}
this.objects = null
}
}
class Block {
constructor() {
this.batches = []
}
/** @param batch {Batch} */
PushBatch(batch) {
this.batches.push(batch)
}
}
/** Custom viewer event names are prefixed with this string. */
const EVENT_NAME_PREFIX = "__dxf_"
/** Transform sRGB color component to linear color space. */
function LinearColor(c) {
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
}
/** Transform linear color component to sRGB color space. */
function SRgbColor(c) {
return c < 0.003 ? c * 12.92 : Math.pow(c, 1 / 2.4) * 1.055 - 0.055
}
/** Get relative luminance value for a color.
* https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
* @param color {number} RGB color value.
* @return {number} Luminance value in range [0; 1].
*/
function Luminance(color) {
const r = LinearColor(((color & 0xff0000) >>> 16) / 255)
const g = LinearColor(((color & 0xff00) >>> 8) / 255)
const b = LinearColor((color & 0xff) / 255)
return r * 0.2126 + g * 0.7152 + b * 0.0722
}
/**
* Get contrast ratio for a color pair.
* https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
* @param c1
* @param c2
* @return {number} Contrast ratio between the colors. Greater than one if the first color color is
* brighter than the second one.
*/
function ContrastRatio(c1, c2) {
return (Luminance(c1) + 0.05) / (Luminance(c2) + 0.05)
}
function HlsToRgb({h, l, s}) {
let r, g, b
if (s === 0) {
/* Achromatic */
r = g = b = l
} else {
function hue2rgb(p, q, t) {
if (t < 0) {
t += 1
}
if (t > 1) {
t -= 1
}
if (t < 1/6) {
return p + (q - p) * 6 * t
}
if (t < 1/2) {
return q
}
if (t < 2/3) {
return p + (q - p) * (2/3 - t) * 6
}
return p
}
const q = l < 0.5 ? l * (1 + s) : l + s - l * s
const p = 2 * l - q
r = hue2rgb(p, q, h + 1/3)
g = hue2rgb(p, q, h)
b = hue2rgb(p, q, h - 1/3)
}
return (Math.min(Math.floor(SRgbColor(r) * 256), 255) << 16) |
(Math.min(Math.floor(SRgbColor(g) * 256), 255) << 8) |
Math.min(Math.floor(SRgbColor(b) * 256), 255)
}
function RgbToHls(color) {
const r = LinearColor(((color & 0xff0000) >>> 16) / 255)
const g = LinearColor(((color & 0xff00) >>> 8) / 255)
const b = LinearColor((color & 0xff) / 255)
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
let h, s
const l = (max + min) / 2
if (max === min) {
/* Achromatic */
h = s = 0
} else {
const d = max - min
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0)
break;
case g:
h = (b - r) / d + 2
break
case b:
h = (r - g) / d + 4
break
}
h /= 6
}
return {h, l, s}
}
function Lighten(color, factor) {
const hls = RgbToHls(color)
hls.l *= factor
if (hls.l > 1) {
hls.l = 1
}
return HlsToRgb(hls)
}
function Darken(color, factor) {
const hls = RgbToHls(color)
hls.l /= factor
return HlsToRgb(hls)
}