export interface M3DHeader { magic: number; x: number; y: number; z: number; filename: string; subject: string; direction: string; date: number; fov: number; weight: number; ax: number; ay: number; az: number; ox: number; oy: number; oz: number; dlen: number; dth: number; memo: string; } interface M3DImageRenderingContext { a1: number; a2: number; a3: number; b1: number; b2: number; b3: number; c1: number; c2: number; c3: number; d1: number; d2: number; d3: number; ra1: number; ra1_2: number; ra2: number; ra2_2: number; ra3: number; ra3_2: number; rb1: number; rb1_2: number; rb2: number; rb2_2: number; rb3: number; rb3_2: number; rc1: number; rc1_2: number; rc2: number; rc2_2: number; rc3: number; rc3_2: number; rd1: number; rd2: number; rd3: number; } /* MeshLength unit is same as fov unit */ const MeshLength = 10; enum DrawingLineType { VLine = 0, VDottedLine = 1, HLine = 2, HDottedLine = 3, } const LineBrightness = 200; const DottedLineBrightness = 128; const EXACTSWITCH: 0 | 1 = 1; const MaxIntensity = 200; class M3D { private data: ArrayBuffer; private header: M3DHeader | null = null; private xposView: Int32Array | null = null; private zposView: Int32Array | null = null; private yposView: Int32Array | null = null; private img3dView: Uint8ClampedArray | null = null; private imageSize = 0; // imagesize size : max (size_x, size_y, size_z) private centerX = 0; // camera coordinates origin : x private centerY = 0; // camera coordinates origin : y private centerZ = 0; // camera coordinates origin : z private ox = 0; // object coordinates origin : x private oy = 0; // object coordinates origin : y private oz = 0; // object coordinates origin : z constructor(data: ArrayBuffer) { this.data = data; this._initialize(); } isBigEndian(): boolean { return new Uint8Array(new Uint32Array([0x11223344]).buffer)[0] === 0x11; } getHeader(): M3DHeader | null { return this.header; } drawImage( canvas: HTMLCanvasElement, ax: number, ay: number, az: number, cutdepth: number, th: number, is3d: boolean, mesh: boolean, ): boolean { const irc: M3DImageRenderingContext = { a1: 0, a2: 0, a3: 0, b1: 0, b2: 0, b3: 0, c1: 0, c2: 0, c3: 0, d1: 0, d2: 0, d3: 0, ra1: 0, ra1_2: 0, ra2: 0, ra2_2: 0, ra3: 0, ra3_2: 0, rb1: 0, rb1_2: 0, rb2: 0, rb2_2: 0, rb3: 0, rb3_2: 0, rc1: 0, rc1_2: 0, rc2: 0, rc2_2: 0, rc3: 0, rc3_2: 0, rd1: 0, rd2: 0, rd3: 0, }; // console.log(ax, ay, az, cutdepth, th, dim, mesh) if (this.header === null) { return false; } canvas.width = this.imageSize; canvas.height = this.imageSize; const ctx = canvas.getContext('2d'); if (ctx === null) { return false; } ctx.fillStyle = 'rgb(0, 0, 0)'; ctx.fillRect(0, 0, this.imageSize, this.imageSize); const img = ctx.getImageData(0, 0, this.imageSize, this.imageSize); this._setMatrix(ax, ay, az, irc); if (is3d) { const ddimg = [...Array(this.imageSize * this.imageSize)].map(() => 0); this._makeDIMG(img, cutdepth, th, ddimg, irc); this._makeShadingImage(img, ddimg); } else { this._makeSliceImage(img, cutdepth, irc); } if (mesh) { this._drawMesh(img, irc); } ctx.putImageData(img, 0, 0); return true; } private _makeDIMG(img: ImageData, cutdepth: number, th: number, ddimg: number[], irc: M3DImageRenderingContext) { if (this.header === null) { return; } const pdimg = [...Array(this.imageSize * this.imageSize)].map(() => 0); const sw = th > this.header.dth ? 1 : th < this.header.dth ? 2 : EXACTSWITCH; switch (sw) { case 0: { this._transSurface(cutdepth, pdimg, irc); let pos = 0; for (let j = 0; j < this.imageSize; j++) { for (let i = 0; i < this.imageSize; i++, pos++) { ddimg[pos] = pdimg[pos]; const pix = this._getPixel(i, j, cutdepth, irc); if (pix > th) { this._drawPixel(img, pos, pix, pix, pix); ddimg[pos] = cutdepth; } } } break; } case 1: { this._transSurface(cutdepth, pdimg, irc); let pos = 0; for (let y = 0; y < this.imageSize; y++) { for (let x = 0; x < this.imageSize; x++, pos++) { let pdPos = (ddimg[pos] = pdimg[pos]); if (pdPos === 0) { continue; } const pix = this._getPixel(x, y, cutdepth, irc); if (pix > th) { this._drawPixel(img, pos, pix, pix, pix); ddimg[pos] = cutdepth; continue; } if (pdPos < cutdepth) { pdPos = cutdepth; } ddimg[pos] = 0; for (let z = pdPos; z < this.imageSize; z++) { const pix = this._getPixel(x, y, z, irc); if (pix > th) { ddimg[pos] = z; break; } } } } break; } case 2: { let pos = 0; for (let y = 0; y < this.imageSize; y++) { for (let x = 0; x < this.imageSize; x++, pos++) { ddimg[pos] = 0; for (let z = cutdepth; z < this.imageSize; z++) { const pix = this._getPixel(x, y, z, irc); if (pix > th) { if (z === cutdepth) { this._drawPixel(img, pos, pix, pix, pix); } else { ddimg[pos] = z; } break; } } } } break; } } } private _makeShadingImage(img: ImageData, ddimg: number[]) { let pos = 0; for (let x = 0; x < this.imageSize; x++) { for (let y = 0; y < this.imageSize; y++, pos++) { const base = pos * 4; if (img.data[base] === 0) { let timg2d = 0; if (x < this.imageSize - 2 && y < this.imageSize - 2) { if (ddimg[pos] !== 0) { const nx = (ddimg[pos] + ddimg[pos + this.imageSize] - ddimg[pos + 2] - ddimg[pos + 2 + this.imageSize]) / 4.0; const ny = (ddimg[pos] + ddimg[pos + 1] - ddimg[pos + this.imageSize * 2] - ddimg[pos + 1 + this.imageSize * 2]) / 4.0; const n = Math.sqrt(nx * nx + ny * ny + 1); timg2d = Math.round(MaxIntensity / n); } } this._drawPixel(img, pos, timg2d, timg2d, timg2d); } } } } private _makeSliceImage(img: ImageData, cutdepth: number, irc: M3DImageRenderingContext) { let pos = 0; for (let y = 0; y < this.imageSize; y++) { for (let x = 0; x < this.imageSize; x++, pos++) { const pix = this._getPixel(x, y, cutdepth, irc); this._drawPixel(img, pos, pix, pix, pix); } } } private _drawMesh(img: ImageData, irc: M3DImageRenderingContext) { let origx: number; let origy: number; if (this.header === null) { return; } if (this.header.ox !== 0 && this.header.oy !== 0) { origx = this.header.ox * irc.ra1 + this.header.oy * irc.rb1 + this.header.oz * irc.rc1 + irc.rd1; origy = this.header.ox * irc.ra2 + this.header.oy * irc.rb2 + this.header.oz * irc.rc2 + irc.rd2; } else { origx = this.centerX; origy = this.centerY; } origx = this._fround(origx, 5); origy = this._fround(origy, 5); const band = (this.header.x * MeshLength) / this.header.fov; const r = 0; const g = 0; const b = LineBrightness; const sr = 0; const sg = 0; const sb = DottedLineBrightness; this._drawLine(img, Math.round(origx), r, g, b, DrawingLineType.VLine); this._drawLine(img, Math.round(origy), r, g, b, DrawingLineType.HLine); for (let pos = Math.round(origx) + band; pos < this.imageSize; pos += band) { this._drawLine(img, Math.round(pos), sr, sg, sb, DrawingLineType.VDottedLine); } for (let pos = Math.round(origx) - band; pos >= 0; pos -= band) { this._drawLine(img, Math.round(pos), sr, sg, sb, DrawingLineType.VDottedLine); } for (let pos = Math.round(origy) + band; pos < this.imageSize; pos += band) { this._drawLine(img, Math.round(pos), sr, sg, sb, DrawingLineType.HDottedLine); } for (let pos = Math.round(origy) - band; pos >= 0; pos -= band) { this._drawLine(img, Math.round(pos), sr, sg, sb, DrawingLineType.HDottedLine); } } private _drawLine(img: ImageData, value: number, r: number, g: number, b: number, type: DrawingLineType) { switch (type) { case DrawingLineType.VLine: { let pos = value; for (let y = 0; y < this.imageSize; y++, pos += this.imageSize) { this._drawPixel(img, pos, r, g, b); } break; } case DrawingLineType.VDottedLine: { let pos = value; for (let y = 0; y < this.imageSize; y += 2, pos += this.imageSize * 2) { this._drawPixel(img, pos, r, g, b); } break; } case DrawingLineType.HLine: { let pos = value * this.imageSize; for (let x = 0; x < this.imageSize; x++, pos++) { this._drawPixel(img, pos, r, g, b); } break; } case DrawingLineType.HDottedLine: { let pos = value * this.imageSize; for (let x = 0; x < this.imageSize; x += 2, pos += 2) { this._drawPixel(img, pos, r, g, b); } break; } default: break; } } private _drawPixel(img: ImageData, pos: number, r: number, g: number, b: number) { let base = pos * 4; img.data[base++] = r; img.data[base++] = g; img.data[base++] = b; img.data[base] = 255; } private _getPixel(x: number, y: number, z: number, irc: M3DImageRenderingContext): number { const len = [...Array(8)].map(() => 0); const ratio = [...Array(8)].map(() => 0); const tx = x * irc.a1 + y * irc.b1 + z * irc.c1 + irc.d1; const ty = x * irc.a2 + y * irc.b2 + z * irc.c2 + irc.d2; const tz = x * irc.a3 + y * irc.b3 + z * irc.c3 + irc.d3; let ix = Math.floor(tx); let iy = Math.floor(ty); let iz = Math.floor(tz); if (this.header === null || this.img3dView === null) { return 0; } if (ix < 0 || ix + 1 >= this.header.x || iy < 0 || iy + 1 >= this.header.y || iz < 0 || iz + 1 >= this.header.z) { return 0; } const ax1 = Math.abs(tx - ix); const ax2 = Math.abs(1 - ax1); const ay1 = Math.abs(ty - iy); const ay2 = Math.abs(1 - ay1); const az1 = Math.abs(tz - iz); const az2 = Math.abs(1 - az1); let sumlen1 = 0; for (let i = 0; i < 8; i++) { len[i] = i & 1 ? ax2 : ax1; len[i] += i & 2 ? ay2 : ay1; len[i] += i & 4 ? az2 : az1; if (len[i] < 0.001) { ix += i & 1 ? 1 : 0; iy += i & 2 ? 1 : 0; iz += i & 4 ? 1 : 0; return this.img3dView[ix + iy * this.header.x + iz * this.header.x * this.header.y]; } sumlen1 += ratio[i] = 1 / len[i]; } let pos = ix + iy * this.header.x + iz * this.header.x * this.header.y; let sumlen2 = this.img3dView[pos] * ratio[0]; sumlen2 += this.img3dView[pos + 1] * ratio[1]; sumlen2 += this.img3dView[pos + this.header.x] * ratio[2]; sumlen2 += this.img3dView[pos + 1 + this.header.x] * ratio[3]; pos += this.header.x * this.header.y; sumlen2 += this.img3dView[pos] * ratio[4]; sumlen2 += this.img3dView[pos + 1] * ratio[5]; sumlen2 += this.img3dView[pos + this.header.x] * ratio[6]; sumlen2 += this.img3dView[pos + 1 + this.header.x] * ratio[7]; return Math.round(sumlen2 / sumlen1); } private _transSurface(cutdepth: number, pdimg: number[], irc: M3DImageRenderingContext) { if (this.header === null || this.xposView === null || this.yposView === null || this.zposView === null) { return; } const ix = [...Array(8)].map(() => 0); const iy = [...Array(8)].map(() => 0); const iz = [...Array(8)].map(() => 0); for (let i = 0; i < this.header.dlen; i++) { const x = this.xposView[i]; const y = this.yposView[i]; const z = this.zposView[i]; ix[0] = x * irc.ra1 + y * irc.rb1 + z * irc.rc1 + irc.rd1; iy[0] = x * irc.ra2 + y * irc.rb2 + z * irc.rc2 + irc.rd2; iz[0] = x * irc.ra3 + y * irc.rb3 + z * irc.rc3 + irc.rd3; ix[1] = ix[0] + irc.ra1_2; iy[1] = iy[0] + irc.ra2_2; iz[1] = iz[0] + irc.ra3_2; ix[2] = ix[0] + irc.rb1_2; iy[2] = iy[0] + irc.rb2_2; iz[2] = iz[0] + irc.rb3_2; ix[3] = ix[0] + irc.rc1_2; iy[3] = iy[0] + irc.rc2_2; iz[3] = iz[0] + irc.rc3_2; ix[4] = ix[1] + irc.rb1_2; iy[4] = iy[1] + irc.rb2_2; iz[4] = iz[1] + irc.rb3_2; ix[5] = ix[1] + irc.rc1_2; iy[5] = iy[1] + irc.rc2_2; iz[5] = iz[1] + irc.rc3_2; ix[6] = ix[2] + irc.rc1_2; iy[6] = iy[2] + irc.rc2_2; iz[6] = iz[2] + irc.rc3_2; ix[7] = ix[4] + irc.rc1_2; iy[7] = iy[4] + irc.rc2_2; iz[7] = iz[4] + irc.rc3_2; for (let j = 0; j < 8; j++) { const xi = Math.round(ix[j]); const yi = Math.round(iy[j]); if ( xi < 0 || xi >= this.imageSize || yi < 0 || yi >= this.imageSize || iz[j] < cutdepth || iz[j] >= this.imageSize ) { continue; } const pos = xi + yi * this.imageSize; if (pdimg[pos] === 0 || pdimg[pos] > iz[j]) { pdimg[pos] = iz[j]; } } } } private _setMatrix(x: number, y: number, z: number, irc: M3DImageRenderingContext) { const xtr = [...Array(9)].map(() => 0); const ytr = [...Array(9)].map(() => 0); const ztr = [...Array(9)].map(() => 0); const tmp = [...Array(9)].map(() => 0); const tr = [...Array(9)].map(() => 0); this._setXtr(xtr, (x * Math.PI) / 180.0); this._setYtr(ytr, (y * Math.PI) / 180.0); this._setZtr(ztr, (z * Math.PI) / 180.0); this._multi(ytr, xtr, tmp); this._multi(ztr, tmp, tr); irc.a1 = tr[0]; irc.b1 = tr[1]; irc.c1 = tr[2]; irc.a2 = tr[3]; irc.b2 = tr[4]; irc.c2 = tr[5]; irc.a3 = tr[6]; irc.b3 = tr[7]; irc.c3 = tr[8]; irc.d1 = -irc.a1 * this.centerX - irc.b1 * this.centerY - irc.c1 * this.centerZ + this.ox; irc.d2 = -irc.a2 * this.centerX - irc.b2 * this.centerY - irc.c2 * this.centerZ + this.oy; irc.d3 = -irc.a3 * this.centerX - irc.b3 * this.centerY - irc.c3 * this.centerZ + this.oz; this._setXtr(xtr, (-x * Math.PI) / 180.0); this._setYtr(ytr, (-y * Math.PI) / 180.0); this._setZtr(ztr, (-z * Math.PI) / 180.0); this._multi(ytr, ztr, tmp); this._multi(xtr, tmp, tr); irc.ra1 = tr[0]; irc.rb1 = tr[1]; irc.rc1 = tr[2]; irc.ra2 = tr[3]; irc.rb2 = tr[4]; irc.rc2 = tr[5]; irc.ra3 = tr[6]; irc.rb3 = tr[7]; irc.rc3 = tr[8]; irc.rd1 = -irc.ra1 * this.ox - irc.rb1 * this.oy - irc.rc1 * this.oz + this.centerX; irc.rd2 = -irc.ra2 * this.ox - irc.rb2 * this.oy - irc.rc2 * this.oz + this.centerY; irc.rd3 = -irc.ra3 * this.ox - irc.rb3 * this.oy - irc.rc3 * this.oz + this.centerZ; irc.ra1_2 = irc.ra1 / 2; irc.rb1_2 = irc.rb1 / 2; irc.rc1_2 = irc.rc1 / 2; irc.ra2_2 = irc.ra2 / 2; irc.rb2_2 = irc.rb2 / 2; irc.rc2_2 = irc.rc2 / 2; irc.ra3_2 = irc.ra3 / 2; irc.rb3_2 = irc.rb3 / 2; irc.rc3_2 = irc.rc3 / 2; } private _setXtr(xtr: number[], ang: number) { xtr[0] = 1; xtr[1] = 0; xtr[2] = 0; xtr[3] = 0; xtr[4] = Math.cos(ang); xtr[5] = -Math.sin(ang); xtr[6] = 0; xtr[7] = Math.sin(ang); xtr[8] = Math.cos(ang); } private _setYtr(ytr: number[], ang: number) { ytr[0] = Math.cos(ang); ytr[1] = 0; ytr[2] = Math.sin(ang); ytr[3] = 0; ytr[4] = 1; ytr[5] = 0; ytr[6] = -Math.sin(ang); ytr[7] = 0; ytr[8] = Math.cos(ang); } private _setZtr(ztr: number[], ang: number) { ztr[0] = Math.cos(ang); ztr[1] = -Math.sin(ang); ztr[2] = 0; ztr[3] = Math.sin(ang); ztr[4] = Math.cos(ang); ztr[5] = 0; ztr[6] = 0; ztr[7] = 0; ztr[8] = 1; } private _multi(l: number[], r: number[], a: number[]) { a[0] = l[0] * r[0] + l[1] * r[3] + l[2] * r[6]; a[3] = l[3] * r[0] + l[4] * r[3] + l[5] * r[6]; a[6] = l[6] * r[0] + l[7] * r[3] + l[8] * r[6]; a[1] = l[0] * r[1] + l[1] * r[4] + l[2] * r[7]; a[4] = l[3] * r[1] + l[4] * r[4] + l[5] * r[7]; a[7] = l[6] * r[1] + l[7] * r[4] + l[8] * r[7]; a[2] = l[0] * r[2] + l[1] * r[5] + l[2] * r[8]; a[5] = l[3] * r[2] + l[4] * r[5] + l[5] * r[8]; a[8] = l[6] * r[2] + l[7] * r[5] + l[8] * r[8]; } private _fround(x: number, n: number) { return Math.round(x * 10 ** n) / 10 ** n; } private _initialize(): void { const view = new DataView(this.data); const magic = view.getInt32(0, true); const isLittleEndianFile = magic === 0x4d523344; if (!isLittleEndianFile && magic !== 0x4433524d) { console.log('invalid data format, this data is not m3d format'); return; } let offset = 0; const decoder = new TextDecoder('utf-8'); this.header = { magic: view.getInt32(offset, isLittleEndianFile), x: view.getInt32((offset += 4), isLittleEndianFile), y: view.getInt32((offset += 4), isLittleEndianFile), z: view.getInt32((offset += 4), isLittleEndianFile), filename: decoder.decode(new Uint8Array(this.data, (offset += 4), 256)).replace(/\0+$/, ''), subject: decoder.decode(new Uint8Array(this.data, (offset += 256), 256)).replace(/\0+$/, ''), direction: decoder.decode(new Uint8Array(this.data, (offset += 256), 4)).replace(/\0+$/, ''), date: view.getUint32((offset += 4), isLittleEndianFile), fov: view.getFloat64((offset += 4), isLittleEndianFile), weight: view.getFloat64((offset += 8), isLittleEndianFile), ax: view.getFloat64((offset += 8), isLittleEndianFile), ay: view.getFloat64((offset += 8), isLittleEndianFile), az: view.getFloat64((offset += 8), isLittleEndianFile), ox: view.getFloat64((offset += 8), isLittleEndianFile), oy: view.getFloat64((offset += 8), isLittleEndianFile), oz: view.getFloat64((offset += 8), isLittleEndianFile), dlen: view.getInt32((offset += 8), isLittleEndianFile), dth: view.getInt32((offset += 4), isLittleEndianFile), memo: decoder.decode(new Uint8Array(this.data, (offset += 4), 256)).replace(/\0+$/, ''), }; // swap byte order if endian is not matched if ((this.isBigEndian() && isLittleEndianFile) || (!this.isBigEndian() && !isLittleEndianFile)) { const start = 864; const end = start + this.header.dlen * 4 * 3; for (let i = start; i < end; i += 4) { const data = new Uint8Array(this.data, i, 4); data.reverse(); } } this.xposView = new Int32Array(this.data, 864, this.header.dlen); this.yposView = new Int32Array(this.data, 864 + this.header.dlen * 4, this.header.dlen); this.zposView = new Int32Array(this.data, 864 + this.header.dlen * 8, this.header.dlen); this.img3dView = new Uint8ClampedArray( this.data, 864 + this.header.dlen * 12, this.header.x * this.header.y * this.header.z, ); // set image generation offset this.imageSize = Math.ceil(Math.max(this.header.x, this.header.y, this.header.z) * Math.sqrt(3.0)) + 1; this.centerX = (this.imageSize - 1) / 2.0; this.centerY = (this.imageSize - 1) / 2.0; this.centerZ = (this.imageSize - 1) / 2.0; this.ox = (this.header.x - 1) / 2.0; this.oy = (this.header.y - 1) / 2.0; this.oz = (this.header.z - 1) / 2.0; } } export default M3D;