@mathicsorg/mathics-threejs-backend
Advanced tools
Comparing version 0.1.0 to 0.2.0
{ | ||
"name": "@mathicsorg/mathics-threejs-backend", | ||
"version": "0.1.0", | ||
"description": "Mathics three.js backend for easily build 3d graphics", | ||
"version": "0.2.0", | ||
"description": "Mathics 3D Graphics backend using three.js", | ||
"main": "src/index.js", | ||
@@ -6,0 +6,0 @@ "repository": { |
# mathics-threejs-backend | ||
Example: | ||
A node.js/Javascript library for rendering [Mathics](https://mathics.org) (and eventually Wolfram-Language) [Graphics3D](https://reference.wolfram.com/language/ref/Graphics3D.html) objects. | ||
This can be used in Mathics front ends like [Mathics-Django](https://pypi.org/project/Mathics-Django/) to handle 3D Graphics. The code may also be useful as a guide for other kinds of Mathics/WL frontends to other kinds of Javascript graphics engines. | ||
## Example: | ||
```js | ||
@@ -12,20 +16,14 @@ import drawGraphics3D from 'mathics-threejs-backend/src/index.js'; | ||
{ | ||
type: 'sphere', | ||
coords: [[[0, 0, 0]]], | ||
faceColor: [1, 1, 1] | ||
type: 'Sphere', | ||
Coords: [ | ||
[[0, 0, 0]] | ||
], | ||
RBGColor: [1, 1, 1] | ||
} | ||
], | ||
axes: {}, | ||
extent: { | ||
xmin: -1, | ||
xmax: 1, | ||
ymin: -1, | ||
ymax: 1, | ||
zmin: -1, | ||
zmax: 1 | ||
}, | ||
lighting: [ | ||
{ | ||
type: 'Ambient', | ||
color: [1, 1, 1] | ||
RBGColor: [1, 1, 1] | ||
} | ||
@@ -32,0 +30,0 @@ ], |
649
src/index.js
@@ -1,648 +0,5 @@ | ||
import { | ||
BoxGeometry, | ||
Color, | ||
DirectionalLight, | ||
EdgesGeometry, | ||
Geometry, | ||
Line, | ||
LineBasicMaterial, | ||
LineSegments, | ||
Matrix4, | ||
Mesh, | ||
PerspectiveCamera, | ||
Scene, | ||
Vector2, | ||
Vector3, | ||
WebGLRenderer | ||
} from '../vendors/threejs/three.min.js'; | ||
import drawGraphics3d from './graphics3d.js'; | ||
import primitiveFunctions from './primitives.js'; | ||
import lightFunctions from './lights.js'; | ||
window.drawGraphics3d = drawGraphics3d; | ||
export default function ( | ||
container, | ||
{ axes, elements, extent, lighting, viewpoint }, | ||
maxSize, | ||
innerWidthMultiplier | ||
) { | ||
// TODO: add a mechanism to update the enclosing <mspace> | ||
// TODO: shading, handling of VertexNormals | ||
maxSize ||= 400; | ||
innerWidthMultiplier ||= 0.6; | ||
let isCtrlDown, isShiftDown, onMouseDownFocus, onCtrlDownFov; | ||
let canvasSize = Math.min(maxSize, window.innerWidth * innerWidthMultiplier); | ||
container.style.width = canvasSize + 'px'; | ||
// to avoid overflow when a tick numbers is out of the parent element | ||
container.style.height = canvasSize + 10 + 'px'; | ||
let hasAxes, isMouseDown = false, | ||
theta, onMouseDownTheta, phi, onMouseDownPhi; | ||
// where the camera is looking (initialized on center of the scene) | ||
const focus = new Vector3( | ||
0.5 * (extent.xmin + extent.xmax), | ||
0.5 * (extent.ymin + extent.ymax), | ||
0.5 * (extent.zmin + extent.zmax) | ||
); | ||
const viewPoint = new Vector3(...viewpoint).sub(focus); | ||
const radius = viewPoint.length(); | ||
onMouseDownTheta = theta = Math.acos(viewPoint.z / radius); | ||
onMouseDownPhi = phi = (Math.atan2(viewPoint.y, viewPoint.x) + 2 * Math.PI) % (2 * Math.PI); | ||
const scene = new Scene(); | ||
const camera = new PerspectiveCamera( | ||
35, // field of view | ||
1, // aspect ratio | ||
0.1 * radius, // near plane | ||
1000 * radius // far plane | ||
); | ||
function updateCameraPosition() { | ||
camera.position.set( | ||
radius * Math.sin(theta) * Math.cos(phi), | ||
radius * Math.sin(theta) * Math.sin(phi), | ||
radius * Math.cos(theta) | ||
).add(focus); | ||
camera.lookAt(focus); | ||
} | ||
updateCameraPosition(); | ||
camera.up.copy(new Vector3(0, 0, 1)); | ||
scene.add(camera); | ||
function getInitialLightPosition(element) { | ||
// initial light position in spherical polar coordinates | ||
if (element.position instanceof Array) { | ||
const temporaryPosition = new Vector3(...element.position); | ||
const result = { | ||
radius: radius * temporaryPosition.length(), | ||
phi: 0, | ||
theta: 0 | ||
}; | ||
if (temporaryPosition.lenght !== 0) { | ||
result.phi = (Math.atan2(temporaryPosition.y, temporaryPosition.x) + 2 * Math.PI) % (2 * Math.PI); | ||
result.theta = Math.asin(temporaryPosition.z / result.radius); | ||
} | ||
return result; | ||
} | ||
} | ||
function positionLights() { | ||
lights.forEach((light, i) => { | ||
if (light instanceof DirectionalLight) { | ||
light.position.set( | ||
initialLightPosition[i].radius * Math.sin(theta + initialLightPosition[i].theta) * Math.cos(phi + initialLightPosition[i].phi), | ||
initialLightPosition[i].radius * Math.sin(theta + initialLightPosition[i].theta) * Math.sin(phi + initialLightPosition[i].phi), | ||
initialLightPosition[i].radius * Math.cos(theta + initialLightPosition[i].theta) | ||
).add(focus); | ||
} | ||
}); | ||
} | ||
const lights = new Array(lighting.length); | ||
const initialLightPosition = new Array(lighting.length); | ||
lighting.forEach((light, i) => { | ||
initialLightPosition[i] = getInitialLightPosition(light); | ||
lights[i] = lightFunctions[light.type](light, radius); | ||
scene.add(lights[i]); | ||
}); | ||
const boundingBox = new Mesh(new BoxGeometry( | ||
extent.xmax - extent.xmin, | ||
extent.ymax - extent.ymin, | ||
extent.zmax - extent.zmin | ||
)); | ||
boundingBox.position.copy(focus); | ||
const boundingBoxEdges = new LineSegments( | ||
new EdgesGeometry(boundingBox.geometry), | ||
new LineBasicMaterial({ color: 0x666666 }) | ||
); | ||
boundingBoxEdges.position.copy(focus); | ||
scene.add(boundingBoxEdges); | ||
// draw the axes | ||
if (axes.hasaxes instanceof Array) { | ||
hasAxes = new Array(axes.hasaxes[0], axes.hasaxes[1], axes.hasaxes[2]); | ||
} else if (axes.hasaxes instanceof Boolean) { | ||
if (axes) { | ||
hasAxes = new Array(true, true, true); | ||
} else { | ||
hasAxes = new Array(false, false, false); | ||
} | ||
} else { | ||
hasAxes = new Array(false, false, false); | ||
} | ||
const axesGeometry = []; | ||
const axesIndexes = [ | ||
[[0, 5], [1, 4], [2, 7], [3, 6]], | ||
[[0, 2], [1, 3], [4, 6], [5, 7]], | ||
[[0, 1], [2, 3], [4, 5], [6, 7]] | ||
]; | ||
const axesLines = new Array(3); | ||
for (let i = 0; i < 3; i++) { | ||
if (hasAxes[i]) { | ||
axesGeometry[i] = new Geometry(); | ||
axesGeometry[i].vertices.push(new Vector3().addVectors( | ||
boundingBox.geometry.vertices[axesIndexes[i][0][0]], boundingBox.position | ||
)); | ||
axesGeometry[i].vertices.push(new Vector3().addVectors( | ||
boundingBox.geometry.vertices[axesIndexes[i][0][1]], boundingBox.position | ||
)); | ||
axesLines[i] = new Line( | ||
axesGeometry[i], | ||
new LineBasicMaterial({ | ||
color: 0x000000, | ||
linewidth: 1.5 | ||
}) | ||
); | ||
scene.add(axesLines[i]); | ||
} | ||
} | ||
function positionAxes() { | ||
// automatic axes placement | ||
let nearJ, nearLenght = 10 * radius, farJ, farLenght = 0; | ||
const temporaryVector = new Vector3(); | ||
for (let i = 0; i < 8; i++) { | ||
temporaryVector.addVectors( | ||
boundingBox.geometry.vertices[i], | ||
boundingBox.position | ||
).sub(camera.position); | ||
const temporaryLenght = temporaryVector.length(); | ||
if (temporaryLenght < nearLenght) { | ||
nearLenght = temporaryLenght; | ||
nearJ = i; | ||
} else if (temporaryLenght > farLenght) { | ||
farLenght = temporaryLenght; | ||
farJ = i; | ||
} | ||
} | ||
for (let i = 0; i < 3; i++) { | ||
if (hasAxes[i]) { | ||
let maxJ, maxLenght = 0; | ||
for (let j = 0; j < 4; j++) { | ||
if (axesIndexes[i][j][0] !== nearJ && | ||
axesIndexes[i][j][1] !== nearJ && | ||
axesIndexes[i][j][0] !== farJ && | ||
axesIndexes[i][j][1] !== farJ | ||
) { | ||
const edge = new Vector3().subVectors( | ||
toCanvasCoords(boundingBox.geometry.vertices[axesIndexes[i][j][0]]), | ||
toCanvasCoords(boundingBox.geometry.vertices[axesIndexes[i][j][1]]) | ||
); | ||
edge.z = 0; | ||
if (edge.length() > maxLenght) { | ||
maxLenght = edge.length(); | ||
maxJ = j; | ||
} | ||
} | ||
} | ||
axesLines[i].geometry.vertices[0].addVectors( | ||
boundingBox.geometry.vertices[axesIndexes[i][maxJ][0]], | ||
boundingBox.position | ||
); | ||
axesLines[i].geometry.vertices[1].addVectors( | ||
boundingBox.geometry.vertices[axesIndexes[i][maxJ][1]], | ||
boundingBox.position | ||
); | ||
axesLines[i].geometry.verticesNeedUpdate = true; | ||
} | ||
} | ||
updateAxes(); | ||
} | ||
// axes ticks | ||
const tickMaterial = new LineBasicMaterial({ | ||
color: 0x000000, | ||
linewidth: 1.2 | ||
}); | ||
const ticks = new Array(3), | ||
ticksSmall = new Array(3), | ||
tickLength = 0.005 * radius; | ||
for (let i = 0; i < 3; i++) { | ||
if (hasAxes[i]) { | ||
ticks[i] = []; | ||
for (let j = 0; j < axes.ticks[i][0].length; j++) { | ||
const tickGeometry = new Geometry(); | ||
tickGeometry.vertices.push(new Vector3()); | ||
tickGeometry.vertices.push(new Vector3()); | ||
ticks[i].push(new Line(tickGeometry, tickMaterial)); | ||
scene.add(ticks[i][j]); | ||
} | ||
ticksSmall[i] = []; | ||
for (let j = 0; j < axes.ticks[i][1].length; j++) { | ||
const tickGeometry = new Geometry(); | ||
tickGeometry.vertices.push(new Vector3()); | ||
tickGeometry.vertices.push(new Vector3()); | ||
ticksSmall[i].push(new Line(tickGeometry, tickMaterial)); | ||
scene.add(ticksSmall[i][j]); | ||
} | ||
} | ||
} | ||
function getTickDir(i) { | ||
const tickDir = new Vector3(); | ||
if (i === 0) { | ||
if (0.25 * Math.PI < theta && theta < 0.75 * Math.PI) { | ||
if (axesGeometry[0].vertices[0].z > boundingBox.position.z) { | ||
tickDir.setZ(-tickLength); | ||
} else { | ||
tickDir.setZ(tickLength); | ||
} | ||
} else { | ||
if (axesGeometry[0].vertices[0].y > boundingBox.position.y) { | ||
tickDir.setY(-tickLength); | ||
} else { | ||
tickDir.setY(tickLength); | ||
} | ||
} | ||
} else if (i === 1) { | ||
if (0.25 * Math.PI < theta && theta < 0.75 * Math.PI) { | ||
if (axesGeometry[1].vertices[0].z > boundingBox.position.z) { | ||
tickDir.setZ(-tickLength); | ||
} else { | ||
tickDir.setZ(tickLength); | ||
} | ||
} else { | ||
if (axesGeometry[1].vertices[0].x > boundingBox.position.x) { | ||
tickDir.setX(-tickLength); | ||
} else { | ||
tickDir.setX(tickLength); | ||
} | ||
} | ||
} else if (i === 2) { | ||
if ((0.25 * Math.PI < phi && phi < 0.75 * Math.PI) || (1.25 * Math.PI < phi && phi < 1.75 * Math.PI)) { | ||
if (axesGeometry[2].vertices[0].x > boundingBox.position.x) { | ||
tickDir.setX(-tickLength); | ||
} else { | ||
tickDir.setX(tickLength); | ||
} | ||
} else { | ||
if (axesGeometry[2].vertices[0].y > boundingBox.position.y) { | ||
tickDir.setY(-tickLength); | ||
} else { | ||
tickDir.setY(tickLength); | ||
} | ||
} | ||
} | ||
return tickDir; | ||
} | ||
function updateAxes() { | ||
for (let i = 0; i < 3; i++) { | ||
if (hasAxes[i]) { | ||
let tickDir = getTickDir(i); | ||
for (let j = 0; j < axes.ticks[i][0].length; j++) { | ||
let value = axes.ticks[i][0][j]; | ||
ticks[i][j].geometry.vertices[0].copy(axesGeometry[i].vertices[0]); | ||
ticks[i][j].geometry.vertices[1].addVectors( | ||
axesGeometry[i].vertices[0], | ||
tickDir | ||
); | ||
if (i === 0) { | ||
ticks[i][j].geometry.vertices[0].x = value; | ||
ticks[i][j].geometry.vertices[1].x = value; | ||
} else if (i === 1) { | ||
ticks[i][j].geometry.vertices[0].y = value; | ||
ticks[i][j].geometry.vertices[1].y = value; | ||
} else if (i === 2) { | ||
ticks[i][j].geometry.vertices[0].z = value; | ||
ticks[i][j].geometry.vertices[1].z = value; | ||
} | ||
ticks[i][j].geometry.verticesNeedUpdate = true; | ||
} | ||
for (let j = 0; j < axes.ticks[i][1].length; j++) { | ||
let value = axes.ticks[i][1][j]; | ||
ticksSmall[i][j].geometry.vertices[0].copy(axesGeometry[i].vertices[0]); | ||
ticksSmall[i][j].geometry.vertices[1].addVectors( | ||
axesGeometry[i].vertices[0], | ||
tickDir.clone().multiplyScalar(0.5) | ||
); | ||
if (i === 0) { | ||
ticksSmall[i][j].geometry.vertices[0].x = value; | ||
ticksSmall[i][j].geometry.vertices[1].x = value; | ||
} else if (i === 1) { | ||
ticksSmall[i][j].geometry.vertices[0].y = value; | ||
ticksSmall[i][j].geometry.vertices[1].y = value; | ||
} else if (i === 2) { | ||
ticksSmall[i][j].geometry.vertices[0].z = value; | ||
ticksSmall[i][j].geometry.vertices[1].z = value; | ||
} | ||
ticksSmall[i][j].geometry.verticesNeedUpdate = true; | ||
} | ||
} | ||
} | ||
} | ||
updateAxes(); | ||
// axes numbering using divs | ||
const tickNumbers = new Array(3); | ||
for (let i = 0; i < 3; i++) { | ||
if (hasAxes[i]) { | ||
tickNumbers[i] = new Array(axes.ticks[i][0].length); | ||
for (let j = 0; j < tickNumbers[i].length; j++) { | ||
let color = 'black'; | ||
if (i < axes.ticks_style.length) { | ||
color = new Color(...axes.ticks_style[i]).getStyle(); | ||
} | ||
tickNumbers[i][j] = document.createElement('div'); | ||
tickNumbers[i][j].innerHTML = axes.ticks[i][2][j] | ||
.replace('0.', '.'); | ||
// handle minus signs | ||
if (axes.ticks[i][0][j] >= 0) { | ||
tickNumbers[i][j].style.paddingLeft = '0.5em'; | ||
} else { | ||
tickNumbers[i][j].style.paddingLeft = 0; | ||
} | ||
tickNumbers[i][j].style.position = 'absolute'; | ||
tickNumbers[i][j].style.fontSize = '0.8em'; | ||
tickNumbers[i][j].style.color = color; | ||
container.appendChild(tickNumbers[i][j]); | ||
} | ||
} | ||
} | ||
function toCanvasCoords(position) { | ||
const temporaryPosition = position.clone().applyMatrix4( | ||
new Matrix4().multiplyMatrices( | ||
camera.projectionMatrix, | ||
camera.matrixWorldInverse | ||
) | ||
); | ||
return new Vector3( | ||
(temporaryPosition.x + 1) * 200, | ||
(1 - temporaryPosition.y) * 200, | ||
(temporaryPosition.z + 1) * 200 | ||
); | ||
} | ||
function positionTickNumbers() { | ||
for (let i = 0; i < 3; i++) { | ||
if (hasAxes[i]) { | ||
for (let j = 0; j < tickNumbers[i].length; j++) { | ||
const tickPosition = toCanvasCoords( | ||
ticks[i][j].geometry.vertices[0].clone().add( | ||
new Vector3().subVectors( | ||
ticks[i][j].geometry.vertices[0], | ||
ticks[i][j].geometry.vertices[1] | ||
).multiplyScalar(6) | ||
) | ||
).multiplyScalar(canvasSize / maxSize); | ||
// distance of the bounding box | ||
tickPosition.setX(tickPosition.x - 10); | ||
tickPosition.setY(tickPosition.y + 8); | ||
tickNumbers[i][j].style.position = `absolute`; | ||
tickNumbers[i][j].style.left = `${tickPosition.x}px`; | ||
tickNumbers[i][j].style.top = `${tickPosition.y}px`; | ||
if (tickPosition.x < 5 || tickPosition.x > 395 || tickPosition.y < 5 || tickPosition.y > 395) { | ||
tickNumbers[i][j].style.display = 'none'; | ||
} | ||
else { | ||
tickNumbers[i][j].style.display = ''; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
// plot the primatives | ||
elements.forEach((element) => { | ||
scene.add(primitiveFunctions[element.type](element, canvasSize)); | ||
}); | ||
// renderer (set preserveDrawingBuffer to deal with issue of weird canvas content after switching windows) | ||
const renderer = new WebGLRenderer({ | ||
antialias: true, | ||
preserveDrawingBuffer: true, | ||
alpha: true | ||
}); | ||
renderer.setSize(canvasSize, canvasSize); | ||
renderer.setPixelRatio(window.devicePixelRatio); | ||
container.appendChild(renderer.domElement); | ||
function render() { | ||
positionLights(); | ||
renderer.render(scene, camera); | ||
} | ||
function scaleInView() { | ||
const proj2d = new Vector3(); | ||
let temporaryFOV = 0; | ||
for (let i = 0; i < 8; i++) { | ||
proj2d.addVectors( | ||
boundingBox.geometry.vertices[i], | ||
boundingBox.position | ||
).applyMatrix4(camera.matrixWorldInverse); | ||
temporaryFOV = Math.max( | ||
temporaryFOV, | ||
114.59 * Math.max( | ||
Math.abs(Math.atan(proj2d.x / proj2d.z) / camera.aspect), | ||
Math.abs(Math.atan(proj2d.y / proj2d.z)) | ||
) | ||
); | ||
} | ||
camera.fov = temporaryFOV + 5; | ||
camera.updateProjectionMatrix(); | ||
} | ||
function onDocumentMouseDown(event) { | ||
event.preventDefault(); | ||
isMouseDown = true; | ||
isShiftDown = false; | ||
isCtrlDown = false; | ||
onMouseDownTheta = theta; | ||
onMouseDownPhi = phi; | ||
onMouseDownPosition.x = event.clientX; | ||
onMouseDownPosition.y = event.clientY; | ||
onMouseDownFocus = new Vector3().copy(focus); | ||
} | ||
function onDocumentMouseMove(event) { | ||
event.preventDefault(); | ||
if (isMouseDown) { | ||
positionTickNumbers(); | ||
if (event.shiftKey) { // pan | ||
if (!isShiftDown) { | ||
isShiftDown = true; | ||
onMouseDownPosition.x = event.clientX; | ||
onMouseDownPosition.y = event.clientY; | ||
autoRescale = false; | ||
container.style.cursor = 'move'; | ||
} | ||
const cameraX = new Vector3( | ||
- radius * Math.cos(theta) * Math.sin(phi) * (theta < 0.5 * Math.PI ? 1 : -1), | ||
radius * Math.cos(theta) * Math.cos(phi) * (theta < 0.5 * Math.PI ? 1 : -1), | ||
0 | ||
).normalize(); | ||
const cameraY = new Vector3().crossVectors( | ||
new Vector3() | ||
.subVectors(focus, camera.position) | ||
.normalize(), | ||
cameraX | ||
); | ||
focus.x = onMouseDownFocus.x + (radius / canvasSize) * (cameraX.x * (onMouseDownPosition.x - event.clientX) + cameraY.x * (onMouseDownPosition.y - event.clientY)); | ||
focus.y = onMouseDownFocus.y + (radius / canvasSize) * (cameraX.y * (onMouseDownPosition.x - event.clientX) + cameraY.y * (onMouseDownPosition.y - event.clientY)); | ||
focus.z = onMouseDownFocus.z + (radius / canvasSize) * (cameraY.z * (onMouseDownPosition.y - event.clientY)); | ||
updateCameraPosition(); | ||
} else if (event.ctrlKey) { // zoom | ||
if (!isCtrlDown) { | ||
isCtrlDown = true; | ||
onCtrlDownFov = camera.fov; | ||
onMouseDownPosition.x = event.clientX; | ||
onMouseDownPosition.y = event.clientY; | ||
autoRescale = false; | ||
container.style.cursor = 'crosshair'; | ||
} | ||
camera.fov = Math.max( | ||
1, | ||
Math.min( | ||
onCtrlDownFov + 20 * Math.atan((event.clientY - onMouseDownPosition.y) / 50), | ||
150 | ||
) | ||
); | ||
camera.updateProjectionMatrix(); | ||
} else { // spin | ||
if (isCtrlDown || isShiftDown) { | ||
onMouseDownPosition.x = event.clientX; | ||
onMouseDownPosition.y = event.clientY; | ||
isShiftDown = false; | ||
isCtrlDown = false; | ||
container.style.cursor = 'pointer'; | ||
} | ||
phi = 2 * Math.PI * (onMouseDownPosition.x - event.clientX) / canvasSize + onMouseDownPhi; | ||
phi = (phi + 2 * Math.PI) % (2 * Math.PI); | ||
theta = 2 * Math.PI * (onMouseDownPosition.y - event.clientY) / canvasSize + onMouseDownTheta; | ||
const epsilon = 1e-12; // prevents spinnging from getting stuck | ||
theta = Math.max(Math.min(Math.PI - epsilon, theta), epsilon); | ||
updateCameraPosition(); | ||
} | ||
render(); | ||
} else { | ||
container.style.cursor = 'pointer'; | ||
} | ||
} | ||
function onDocumentMouseUp(event) { | ||
event.preventDefault(); | ||
isMouseDown = false; | ||
container.style.cursor = 'pointer'; | ||
if (autoRescale) { | ||
scaleInView(); | ||
render(); | ||
} | ||
positionAxes(); | ||
render(); | ||
positionTickNumbers(); | ||
} | ||
// bind mouse events | ||
container.addEventListener('mousemove', onDocumentMouseMove); | ||
container.addEventListener('mousedown', onDocumentMouseDown); | ||
container.addEventListener('mouseup', onDocumentMouseUp); | ||
window.addEventListener('resize', () => { | ||
canvasSize = Math.min(maxSize, window.innerWidth * innerWidthMultiplier); | ||
container.style.width = canvasSize + 'px'; | ||
// to avoid overflow when a tick numbers is out of the parent element | ||
container.style.height = canvasSize + 10 + 'px'; | ||
renderer.setSize(canvasSize, canvasSize); | ||
renderer.setPixelRatio(window.devicePixelRatio); | ||
positionTickNumbers(); | ||
}); | ||
const onMouseDownPosition = new Vector2(); | ||
let autoRescale = true; | ||
updateCameraPosition(); | ||
positionAxes(); | ||
render(); // rendering twice updates camera.matrixWorldInverse so that scaleInView works properly | ||
scaleInView(); | ||
render(); | ||
positionTickNumbers(); | ||
} | ||
export default drawGraphics3d; |
@@ -13,29 +13,34 @@ import { | ||
import scaleCoordinate from './scaleCoordinate.js'; | ||
export default { | ||
ambient: ({ color }) => { | ||
return new AmbientLight(new Color(...color).getHex()); | ||
Ambient: ({ RGBColor }) => { | ||
return new AmbientLight(new Color(...RGBColor).getHex()); | ||
}, | ||
directional: ({ color }) => { | ||
return new DirectionalLight(new Color(...color).getHex(), 1); | ||
Directional: ({ RGBColor }) => { | ||
return new DirectionalLight(new Color(...RGBColor).getHex(), 1); | ||
}, | ||
spot: ({ angle, color, position, target }) => { | ||
const group = new Group(); | ||
Spot: ({ Angle, Coords, RGBColor, Target }, extent) => { | ||
const light = new SpotLight(new Color(...RGBColor).getHex()); | ||
light.position.set( | ||
...(Coords[0] ?? scaleCoordinate(Coords[1], extent)) | ||
); | ||
light.angle = Angle; | ||
const light = new SpotLight(new Color(...color).getHex()); | ||
light.position.set(...position); | ||
light.angle = angle; | ||
group.add(light); | ||
light.target.position.set( | ||
...(Target[0] ?? scaleCoordinate(Target[1], extent)) | ||
); | ||
light.target.updateMatrixWorld(); | ||
light.target.position.set(...target); | ||
group.add(light.target); | ||
return group; | ||
return light; | ||
}, | ||
point: ({ color, position }, radius) => { | ||
Point: ({ Coords, RGBColor }, extent, radius) => { | ||
const group = new Group(); | ||
const colorHex = new Color(...color).getHex(); | ||
const color = new Color(...RGBColor).getHex(); | ||
const light = new PointLight(colorHex); | ||
light.position.set(...position); | ||
const light = new PointLight(color); | ||
light.position.set( | ||
...(Coords[0] ?? scaleCoordinate(Coords[1], extent)) | ||
); | ||
group.add(light); | ||
@@ -46,3 +51,3 @@ | ||
new SphereGeometry(0.007 * radius, 16, 8), | ||
new MeshBasicMaterial({ color: colorHex }) | ||
new MeshBasicMaterial({ color }) | ||
); | ||
@@ -49,0 +54,0 @@ lightSphere.position.copy(light.position); |
import { | ||
BoxGeometry, | ||
BoxBufferGeometry, | ||
BufferAttribute, | ||
BufferGeometry, | ||
Color, | ||
CylinderGeometry, | ||
CylinderBufferGeometry, | ||
DoubleSide, | ||
Face3, | ||
Geometry, | ||
Group, | ||
@@ -15,8 +14,10 @@ InstancedMesh, | ||
Mesh, | ||
MeshBasicMaterial, | ||
MeshLambertMaterial, | ||
Points, | ||
Quaternion, | ||
ShaderLib, | ||
ShaderMaterial, | ||
SphereGeometry, | ||
Shape, | ||
ShapeGeometry, | ||
SphereBufferGeometry, | ||
Vector3, | ||
@@ -27,47 +28,68 @@ Vector4 | ||
import earcut from '../vendors/earcut/earcut.min.js'; | ||
import scaleCoordinate from './scaleCoordinate.js'; | ||
// TODO: the one-element arrays should be two-element arrays, where the 2nd element is the "scaled" part of the coordinates that depend on the size of the final graphics (see Mathematica's Scaled) | ||
export default { | ||
arrow: ({ color, coords }) => { | ||
const group = new THREE.Group(); | ||
Arrow: ({ Coords, Opacity, RGBColor }, extent) => { | ||
const group = new Group(); | ||
const colorHex = new THREE.Color(...color).getHex(); | ||
const color = new Color(...RGBColor).getHex(); | ||
const startCoordinate = new THREE.Vector3( | ||
...coords[coords.length - 2][0] | ||
const startCoordinate = new Vector3( | ||
...(Coords[Coords.length - 2][0] ?? scaleCoordinate(Coords[Coords.length - 2][1], extent)) | ||
); | ||
const endCoordinate = new THREE.Vector3( | ||
...coords[coords.length - 1][0] | ||
const endCoordinate = new Vector3( | ||
...(Coords[Coords.length - 1][0] ?? scaleCoordinate(Coords[Coords.length - 1][1], extent)) | ||
); | ||
group.add( | ||
new THREE.ArrowHelper( | ||
endCoordinate.clone().sub(startCoordinate).normalize(), | ||
startCoordinate, | ||
startCoordinate.distanceTo(endCoordinate), | ||
colorHex | ||
) | ||
const arrowHead = new Mesh( | ||
new CylinderBufferGeometry( | ||
0, | ||
0.04 * startCoordinate.distanceTo(endCoordinate), | ||
0.2 * startCoordinate.distanceTo(endCoordinate) | ||
).rotateX(Math.PI / 2), | ||
new MeshBasicMaterial({ | ||
color, | ||
opacity: Opacity ?? 1, | ||
transparent: (Opacity ?? 1) !== 1 | ||
}) | ||
); | ||
const points = new Float32Array(coords.length * 3 - 3); | ||
// set the position to 1/10 far from the end coordinate so lookAt work | ||
arrowHead.position.copy( | ||
endCoordinate.clone() | ||
.multiplyScalar(9) | ||
.add(startCoordinate) | ||
.multiplyScalar(0.1) | ||
); | ||
arrowHead.lookAt(endCoordinate); | ||
group.add(arrowHead); | ||
const points = new Float32Array(Coords.length * 3); | ||
for (let i = 0; i < points.length / 3; i++) { | ||
points[i * 3] = coords[i][0][0]; | ||
points[i * 3 + 1] = coords[i][0][1]; | ||
points[i * 3 + 2] = coords[i][0][2]; | ||
Coords[i][0] ??= scaleCoordinate(Coords[i][1], extent); | ||
points[i * 3] = Coords[i][0][0]; | ||
points[i * 3 + 1] = Coords[i][0][1]; | ||
points[i * 3 + 2] = Coords[i][0][2]; | ||
} | ||
const linesGeometry = new THREE.BufferGeometry(); | ||
const linesGeometry = new BufferGeometry(); | ||
linesGeometry.setAttribute( | ||
'position', | ||
new THREE.BufferAttribute(points, 3) | ||
new BufferAttribute(points, 3) | ||
); | ||
group.add( | ||
new THREE.Line( | ||
new Line( | ||
linesGeometry, | ||
new THREE.LineBasicMaterial({ color: colorHex }) | ||
new LineBasicMaterial({ | ||
color, | ||
opacity: Opacity ?? 1, | ||
transparent: (Opacity ?? 1) !== 1 | ||
}) | ||
) | ||
@@ -78,30 +100,49 @@ ); | ||
}, | ||
cube: ({ color, position, size }) => { | ||
const cube = new Mesh( | ||
new BoxGeometry(...size[0]), | ||
new MeshLambertMaterial({ | ||
color: new Color(...color).getHex() | ||
}) | ||
); | ||
Cuboid: ({ Coords, Opacity, RGBColor }, extent) => { | ||
const group = new Group(); | ||
cube.position.set(...position[0]); | ||
for (let i = 0; i < Coords.length / 2; i++) { | ||
const startCoordinate = new Vector3( | ||
...(Coords[i * 2][0] ?? scaleCoordinate(Coords[i * 2][1], extent)) | ||
); | ||
const endCoordinate = new Vector3( | ||
...(Coords[i * 2 + 1][0] ?? scaleCoordinate(Coords[i * 2 + 1][1], extent)) | ||
); | ||
return cube; | ||
const cuboid = new Mesh( | ||
new BoxBufferGeometry( | ||
...endCoordinate.clone().sub(startCoordinate).toArray() | ||
), | ||
new MeshLambertMaterial({ | ||
color: new Color(...RGBColor).getHex(), | ||
opacity: Opacity ?? 1, | ||
transparent: (Opacity ?? 1) !== 1 | ||
}) | ||
); | ||
// mean of the start and end coordinates, the center of the cuboid | ||
cuboid.position.copy( | ||
startCoordinate.add(endCoordinate).multiplyScalar(0.5) | ||
); | ||
group.add(cuboid); | ||
} | ||
return group; | ||
}, | ||
cylinder: ({ coords, color, radius }) => { | ||
Cylinder: ({ Coords, Opacity, Radius, RGBColor }, extent) => { | ||
const group = new Group(); | ||
for (let i = 0; i < coords.length / 2; i++) { | ||
for (let i = 0; i < Coords.length / 2; i++) { | ||
const startCoordinate = new Vector3( | ||
...coords[i * 2][0] | ||
...(Coords[i * 2][0] ?? scaleCoordinate(Coords[i * 2][1], extent)) | ||
); | ||
const endCoordinate = new Vector3( | ||
...coords[i * 2 + 1][0] | ||
...(Coords[i * 2 + 1][0] ?? scaleCoordinate(Coords[i * 2][1], extent)) | ||
); | ||
const cylinder = new Mesh( | ||
new CylinderGeometry( | ||
radius, | ||
radius, | ||
new CylinderBufferGeometry( | ||
Radius, | ||
Radius, | ||
startCoordinate.distanceTo(endCoordinate), // the height of the cylinder | ||
@@ -114,7 +155,9 @@ 24 | ||
new MeshLambertMaterial({ | ||
color: new Color(...color).getHex() | ||
color: new Color(...RGBColor).getHex(), | ||
opacity: Opacity ?? 1, | ||
transparent: (Opacity ?? 1) !== 1 | ||
}) | ||
); | ||
// mean of the start and end vectors, the center of the cylinder | ||
// mean of the start and end coordinates, the center of the cylinder | ||
cylinder.position.addVectors(startCoordinate, endCoordinate) | ||
@@ -130,21 +173,40 @@ .multiplyScalar(0.5); | ||
}, | ||
line: ({ color, coords }) => { | ||
Line: ({ Coords, Opacity, RGBColor }, extent) => { | ||
const geometry = new BufferGeometry(); | ||
const points = new Float32Array(Coords.length * 3); | ||
Coords.forEach((coordinate, i) => { | ||
coordinate[0] ??= scaleCoordinate(coordinate[1], extent); | ||
points[i * 3] = coordinate[0][0]; | ||
points[i * 3 + 1] = coordinate[0][1]; | ||
points[i * 3 + 2] = coordinate[0][2]; | ||
}); | ||
geometry.setAttribute('position', new BufferAttribute(points, 3)); | ||
return new Line( | ||
new BufferGeometry().setFromPoints( | ||
coords.map( | ||
(coordinate) => new Vector3(...coordinate[0]) | ||
) | ||
), | ||
geometry, | ||
new LineBasicMaterial({ | ||
color: new Color(...color).getHex() | ||
color: new Color(...RGBColor).getHex(), | ||
opacity: Opacity ?? 1, | ||
transparent: (Opacity ?? 1) !== 1 | ||
}) | ||
); | ||
}, | ||
point: ({ color, coords, pointSize }, canvasSize) => { | ||
const geometry = new Geometry(); | ||
Point: ({ Coords, Opacity, PointSize, RGBColor }, extent, canvasSize) => { | ||
const geometry = new BufferGeometry(); | ||
geometry.vertices = coords.map( | ||
(coordinate) => new Vector3(...coordinate[0]) | ||
); | ||
const points = new Float32Array(Coords.length * 3); | ||
Coords.forEach((coordinate, i) => { | ||
coordinate[0] ??= scaleCoordinate(coordinate[1], extent); | ||
points[i * 3] = coordinate[0][0]; | ||
points[i * 3 + 1] = coordinate[0][1]; | ||
points[i * 3 + 2] = coordinate[0][2]; | ||
}); | ||
geometry.setAttribute('position', new BufferAttribute(points, 3)); | ||
return new Points( | ||
@@ -155,6 +217,15 @@ geometry, | ||
uniforms: { | ||
size: { value: pointSize * canvasSize * 0.5 }, | ||
color: { value: new Vector4(...color, 1) }, | ||
size: { value: PointSize * canvasSize * 0.5 }, | ||
color: { value: new Vector4(...RGBColor, Opacity) }, | ||
}, | ||
vertexShader: ShaderLib.points.vertexShader, | ||
vertexShader: ` | ||
uniform float size; | ||
void main() { | ||
#include <begin_vertex> | ||
#include <project_vertex> | ||
gl_PointSize = size; | ||
} | ||
`, | ||
fragmentShader: ` | ||
@@ -172,13 +243,18 @@ uniform vec4 color; | ||
}, | ||
polygon: ({ coords, color }) => { | ||
Polygon: ({ Coords, Opacity, RGBColor }, extent) => { | ||
let geometry; | ||
if (coords.length === 3) { // triangle | ||
geometry = new Geometry(); | ||
if (Coords.length === 3) { // triangle | ||
geometry = new BufferGeometry(); | ||
geometry.vertices = coords.map( | ||
(coordinate) => new Vector3(...coordinate[0]) | ||
geometry.setAttribute( | ||
'position', | ||
new BufferAttribute(new Float32Array([ | ||
...(Coords[0][0] ?? scaleCoordinate(Coords[0][1], extent)), | ||
...(Coords[1][0] ?? scaleCoordinate(Coords[1][1], extent)), | ||
...(Coords[2][0] ?? scaleCoordinate(Coords[2][1], extent)) | ||
]), 3) | ||
); | ||
geometry.faces.push(new Face3(0, 1, 2)); | ||
geometry.computeVertexNormals(); | ||
} else { | ||
@@ -188,10 +264,12 @@ // boolean variables | ||
coords.forEach((coordinate) => { | ||
if (coordinate[0][0] !== coords[0][0][0]) { | ||
Coords.forEach((coordinate) => { | ||
coordinate[0] ??= scaleCoordinate(coordinate[1], extent); | ||
if (coordinate[0][0] !== Coords[0][0][0]) { | ||
isXCoplanar = 0; | ||
} | ||
if (coordinate[0][1] !== coords[0][0][1]) { | ||
if (coordinate[0][1] !== Coords[0][0][1]) { | ||
isYCoplanar = 0; | ||
} | ||
if (coordinate[0][2] !== coords[0][0][2]) { | ||
if (coordinate[0][2] !== Coords[0][0][2]) { | ||
isZCoplanar = 0; | ||
@@ -210,16 +288,15 @@ } | ||
const points = coords.map((coordinate) => | ||
new Vector3(...coordinate[0]) | ||
.applyQuaternion( | ||
new Quaternion().setFromUnitVectors( | ||
normalVector, | ||
normalZVector | ||
) | ||
const points = Coords.map((coordinate) => | ||
new Vector3( | ||
...(coordinate[0] ?? scaleCoordinate(coordinate[1], extent)) | ||
).applyQuaternion( | ||
new Quaternion().setFromUnitVectors( | ||
normalVector, | ||
normalZVector | ||
) | ||
) | ||
); | ||
const polygonShape = new Shape(points); | ||
geometry = new ShapeGeometry(new Shape(points)); | ||
geometry = new ShapeGeometry(polygonShape); | ||
geometry.vertices = geometry.vertices.map( | ||
@@ -233,45 +310,52 @@ (vertex) => vertex.applyQuaternion( | ||
); | ||
geometry.computeFaceNormals(); | ||
} else { | ||
geometry = new Geometry(); | ||
geometry = new BufferGeometry(); | ||
const coordinates = []; | ||
const coordinates = new Float32Array(Coords.length * 3); | ||
coords.forEach((coordinate) => { | ||
coordinates.push(...coordinate[0]); | ||
geometry.vertices.push(new Vector3(...coordinate[0])); | ||
Coords.forEach((coordinate, i) => { | ||
coordinate[0] ??= scaleCoordinate(coordinate[1], extent); | ||
coordinates[i * 3] = coordinate[0][0]; | ||
coordinates[i * 3 + 1] = coordinate[0][1]; | ||
coordinates[i * 3 + 2] = coordinate[0][2]; | ||
}); | ||
const triangles = earcut(coordinates, null, 3); | ||
geometry.setAttribute( | ||
'position', | ||
new BufferAttribute(coordinates, 3) | ||
); | ||
for (let i = 0; i < triangles.length; i += 3) { | ||
geometry.faces.push(new Face3( | ||
triangles[i], | ||
triangles[i + 1], | ||
triangles[i + 2] | ||
)); | ||
} | ||
geometry.setIndex(earcut(coordinates)); | ||
geometry.computeVertexNormals(); | ||
} | ||
}; | ||
geometry.computeFaceNormals(); | ||
return new Mesh(geometry, new MeshLambertMaterial({ | ||
color: new Color(...color).getHex(), | ||
color: new Color(...RGBColor).getHex(), | ||
opacity: Opacity ?? 1, | ||
transparent: (Opacity ?? 1) !== 1, | ||
side: DoubleSide | ||
})); | ||
}, | ||
sphere: ({ coords, color, radius }) => { | ||
Sphere: ({ Coords, Opacity, Radius, RGBColor }, extent) => { | ||
const spheres = new InstancedMesh( | ||
new SphereGeometry(radius, 48, 48), | ||
new SphereBufferGeometry(Radius, 48, 48), | ||
new MeshLambertMaterial({ | ||
color: new Color(...color).getHex() | ||
color: new Color(...RGBColor).getHex(), | ||
opacity: Opacity ?? 1, | ||
transparent: (Opacity ?? 1) !== 1 | ||
}), | ||
coords.length | ||
Coords.length | ||
); | ||
coords.forEach((coordinate, i) => spheres.setMatrixAt( | ||
i, | ||
new Matrix4() | ||
.setPosition(new Vector3(...coordinate[0])) | ||
)); | ||
Coords.forEach((coordinate, i) => | ||
spheres.setMatrixAt( | ||
i, | ||
new Matrix4().setPosition(...(coordinate[0] ?? scaleCoordinate(coordinate[1], extent))) | ||
) | ||
); | ||
@@ -278,0 +362,0 @@ return spheres; |
Sorry, the diff of this file is not supported yet
2097932
39
32879
34