working line endpt drag

master
howard 2021-03-20 12:08:15 -07:00
commit f3f04f9cfb
25 changed files with 9227 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

2788
dist/bundle.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
dist/icon-192.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
dist/icon-512.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

70
dist/index.html vendored Normal file
View File

@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta property="og:title" content="CAD Tool" />
<meta property="og:description" content="Three.js CAD tool" />
<meta property="og:url" content="" />
<meta property="og:image" content="" />
<link rel="apple-touch-icon" href="icon-192.png" />
<link rel="manifest" href="manifest.json" />
<title>CAD Tool</title>
<style>
html,
body {
margin: 0;
height: 100%;
}
#c {
width: 100%;
height: 100%;
display: block;
}
.split {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
#view1 {
width: 100%;
height: 100%;
}
#view2 {
position: absolute;
right: 0;
top: 0;
width: 30%;
height: 30%;
}
</style>
</head>
<body>
<canvas id="c"></canvas>
<div class="split">
<div id="view1"></div>
<div id="view2"></div>
</div>
<script src="bundle.js" type="module"></script>
</script>
</body>
</html>

27
dist/manifest.json vendored Normal file
View File

@ -0,0 +1,27 @@
{
"short_name": "CAD Tool",
"name": "CAD Tool using Three.js",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "icon-192.png",
"type": "image/png",
"sizes": "192x192",
"purpose": "maskable any"
},
{
"src": "icon-512.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable any"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
dist/robots.txt vendored Normal file
View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

19
icon/generate Executable file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
svg=icon
size=(16 24 32 64)
out=""
for i in ${size[@]}; do
inkscape --export-filename="./$svg-$i.png" $svg.svg -w $i -h $i
out+="$svg-$i.png "
done
size=(192 512)
for i in ${size[@]}; do
inkscape --export-filename="./$svg-$i.png" $svg.svg -w $i -h $i
done
convert $out favicon.ico
mv favicon.ico icon-192.png icon-512.png ../dist

BIN
icon/icon-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 B

BIN
icon/icon-24.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
icon/icon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
icon/icon-64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

103
icon/icon.svg Normal file
View File

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
enable-background="new 0 0 24 24"
viewBox="0 0 24 24"
fill="black"
width="24px"
height="24px"
version="1.1"
id="svg26"
sodipodi:docname="icon.svg"
inkscape:version="1.0.2 (1.0.2+r75+1)">
<metadata
id="metadata32">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs30" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1623"
inkscape:window-height="1236"
id="namedview28"
showgrid="false"
inkscape:zoom="21.184428"
inkscape:cx="16.030816"
inkscape:cy="15.296979"
inkscape:window-x="426"
inkscape:window-y="92"
inkscape:window-maximized="0"
inkscape:current-layer="svg26"
inkscape:document-rotation="0"
showguides="true"
inkscape:guide-bbox="true">
<sodipodi:guide
position="-3.6819497,21.6"
orientation="0,1"
id="guide842"
inkscape:label=""
inkscape:locked="false"
inkscape:color="rgb(0,0,255)" />
<sodipodi:guide
position="-1.0857031,2.4"
orientation="0,1"
id="guide846"
inkscape:label=""
inkscape:locked="false"
inkscape:color="rgb(0,0,255)" />
</sodipodi:namedview>
<circle
style="fill:#a6a6a6;stroke-width:0.05;stroke-linecap:round"
id="path927"
cx="12"
cy="12"
r="12" />
<g
id="g875"
transform="matrix(1.2,0,0,1.2,-2.4,-2.4)">
<g
id="g868"
style="fill:#5ccff0;fill-opacity:1">
<polygon
opacity=".3"
points="13,17.17 17,14.87 17,10.24 13,12.57 "
id="polygon6"
style="opacity:1;fill:#5ccff0;fill-opacity:1" />
<polygon
opacity=".3"
points="12,6.25 8.04,8.53 12,10.84 15.96,8.53 "
id="polygon8"
style="opacity:1;fill:#5ccff0;fill-opacity:1" />
<polygon
opacity=".3"
points="7,14.87 11,17.17 11,12.57 7,10.24 "
id="polygon10"
style="opacity:1;fill:#5ccff0;fill-opacity:1" />
</g>
<path
d="M 19,14.87 V 9.13 C 19,8.41 18.62,7.75 18,7.4 L 13,4.52 C 12.69,4.34 12.35,4.25 12,4.25 c -0.35,0 -0.69,0.09 -1,0.27 L 6,7.39 C 5.38,7.75 5,8.41 5,9.13 v 5.74 c 0,0.72 0.38,1.38 1,1.73 l 5,2.88 c 0.31,0.18 0.65,0.27 1,0.27 0.35,0 0.69,-0.09 1,-0.27 l 5,-2.88 c 0.62,-0.35 1,-1.01 1,-1.73 z m -8,2.3 -4,-2.3 v -4.63 l 4,2.33 z M 12,10.84 8.04,8.53 12,6.25 15.96,8.53 Z m 5,4.03 -4,2.3 v -4.6 l 4,-2.33 z"
id="path20" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

3700
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"scripts": {
"start": "webpack --watch --config webpack.dev.js",
"build": "webpack --config webpack.prod.js",
"get-bundle-size": "webpack --profile --json > stats.json --config webpack.prod.js && webpack-bundle-analyzer stats.json dist/"
},
"devDependencies": {
"css-loader": "^5.1.3",
"dat.gui": "^0.7.7",
"redux": "^4.0.5",
"sass": "^1.32.8",
"sass-loader": "^11.0.1",
"stats-js": "^1.0.1",
"style-loader": "^2.0.0",
"three": "^0.126.1",
"webpack": "^5.26.3",
"webpack-bundle-analyzer": "^4.4.0",
"webpack-cli": "^4.5.0",
"webpack-merge": "^5.7.3"
}
}

1230
src/OrbitControls.js Normal file

File diff suppressed because it is too large Load Diff

266
src/Sketcher.js Normal file
View File

@ -0,0 +1,266 @@
import { Matrix4 } from 'three';
import * as THREE from '../node_modules/three/src/Three'
export class Sketcher extends THREE.Group {
constructor(camera, domElement, plane) {
super()
this.camera = camera;
this.domElement = domElement;
this.scene = scene;
this.plane = plane;
this.matrixAutoUpdate = false;
this.sketchNormal = new THREE.Vector3(0, 0, 1)
this.orientSketcher(plane)
this.add(new THREE.PlaneHelper(this.plane, 1, 0xffff00));
this.linesGroup = new THREE.Group()
this.linesArr = this.linesGroup.children
this.pointsGroup = new THREE.Group()
this.ptsArr = this.pointsGroup.children
this.add(this.linesGroup)
this.add(this.pointsGroup)
window.lg = this.linesArr
window.pg = this.ptsArr
this.pickThreshold = 100
this.grabbedObject = null
this.lineMaterial = new THREE.LineBasicMaterial({
color: 0x555,
})
this.pointMaterial = new THREE.PointsMaterial({
color: 0xAAA,
size: 3,
})
this.pointStart = this.pointStart.bind(this);
this.pointEnd = this.pointEnd.bind(this);
this.move = this.move.bind(this);
this.keyHandler = this.keyHandler.bind(this);
this.picker = this.picker.bind(this);
this.grabbedMove = this.grabbedMove.bind(this);
this.grabEnd = this.grabEnd.bind(this);
this.raycaster = new THREE.Raycaster();
window.addEventListener('keydown', this.keyHandler)
domElement.addEventListener('pointerdown', this.picker)
this.mode = ""
this.keyTable = {
'l': this.addLine,
'Escape': this.clear
}
}
orientSketcher() {
const theta = this.sketchNormal.angleTo(this.plane.normal)
const axis = this.sketchNormal.clone().cross(this.plane.normal).normalize()
const rot = new THREE.Matrix4().makeRotationAxis(axis, theta)
const trans = new THREE.Matrix4().makeTranslation(0, 0, this.plane.constant)
this.matrix = rot.multiply(trans) // world matrix will auto update in next render
this.inverse = this.matrix.clone().invert()
}
keyHandler(e) {
switch (e.key) {
case 'Escape':
this.clear()
this.mode = ""
break;
case 'l':
this.addLine()
this.mode = "line"
break;
case '=':
this.plane.applyMatrix4(new Matrix4().makeRotationY(0.1))
this.orientSketcher()
this.dispatchEvent({ type: 'change' })
break;
case '-':
this.plane.applyMatrix4(new Matrix4().makeRotationY(-0.1))
this.orientSketcher()
this.dispatchEvent({ type: 'change' })
break;
}
}
picker(e) {
if (this.mode || e.buttons != 1) return
this.raycaster.setFromCamera(
new THREE.Vector2(
(e.clientX / window.innerWidth) * 2 - 1,
- (e.clientY / window.innerHeight) * 2 + 1
),
this.camera
);
// console.log(this.ptsArr)
const candidates = this.raycaster.intersectObjects(this.ptsArr)
// console.log(candidates)
if (!candidates.length) return;
let minDist = candidates[0].distanceToRay
let idx = 0
for (let i = 1; i < candidates.length; i++) {
if (candidates.distanceToRay < minDist) {
minDist = candidates.distanceToRay
idx = i
}
}
if (minDist < this.pickThreshold) {
this.grabPtIdx = this.ptsArr.indexOf(
candidates[idx].object
)
} else {
return
}
this.domElement.addEventListener('pointermove', this.grabbedMove);
this.domElement.addEventListener('pointerup', this.grabEnd);
}
grabbedMove(e) {
const mouseLoc = this.getLocation(e);
this.moveLinePt(this.grabPtIdx,mouseLoc)
this.dispatchEvent({ type: 'change' })
}
moveLinePt(ptIdx, absPos) {
this.ptsArr[ptIdx].geometry.attributes.position.set(absPos);
this.ptsArr[ptIdx].geometry.attributes.position.needsUpdate = true;
const lineIdx = Math.floor(ptIdx / 2)
const endPtIdx = (ptIdx % 2) * 3
this.linesArr[lineIdx].geometry.attributes.position.set(absPos, endPtIdx)
this.linesArr[lineIdx].geometry.attributes.position.needsUpdate = true;
}
grabEnd() {
this.domElement.removeEventListener('pointermove', this.grabbedMove)
this.domElement.removeEventListener('pointerup', this.grabEnd)
this.ptsArr[this.grabPtIdx].geometry.computeBoundingSphere()
// this.grabbedObject = null
}
addLine() {
this.domElement.addEventListener('pointerdown', this.pointStart)
}
clear() {
if (this.mode == "") return
this.domElement.removeEventListener('pointerdown', this.pointStart)
this.domElement.removeEventListener('pointermove', this.move);
this.domElement.removeEventListener('pointerdown', this.pointEnd);
this.domElement.removeEventListener('pointerdown', this.pointEnd);
const lastLine = this.linesArr[this.linesArr.length - 1]
this.linesGroup.remove(lastLine)
lastLine.geometry.dispose()
const lastPoints = this.ptsArr.slice(this.ptsArr.length - 2)
this.pointsGroup.remove(...lastPoints)
lastPoints.forEach(obj => obj.geometry.dispose())
this.dispatchEvent({ type: 'change' })
}
getLocation(e) {
this.raycaster.setFromCamera(
new THREE.Vector2(
(e.clientX / window.innerWidth) * 2 - 1,
- (e.clientY / window.innerHeight) * 2 + 1
),
this.camera
);
// return this.worldToLocal(this.raycaster.ray.intersectPlane(this.plane)).toArray()
return this.raycaster.ray.intersectPlane(this.plane).applyMatrix4(this.inverse).toArray()
}
pointStart(e) {
if (e.buttons !== 1) return
const mouseLoc = this.getLocation(e);
this.lineGeom = new THREE.BufferGeometry()
this.lineGeom.setAttribute('position',
new THREE.BufferAttribute(
new Float32Array(6), 3
)
);
this.lineGeom.attributes.position.set(mouseLoc)
this.line = new THREE.LineSegments(this.lineGeom, this.lineMaterial);
this.line.frustumCulled = false;
this.linesGroup.add(this.line)
this.p1Geom = new THREE.BufferGeometry()
this.p1Geom.setAttribute('position',
new THREE.BufferAttribute(
new Float32Array(3), 3
)
);
this.p1Geom.attributes.position.set(mouseLoc)
this.p1 = new THREE.Points(this.p1Geom, this.pointMaterial);
this.pointsGroup.add(this.p1)
this.p2Geom = new THREE.BufferGeometry()
this.p2Geom.setAttribute('position',
new THREE.BufferAttribute(
new Float32Array(3), 3
)
);
this.p2 = new THREE.Points(this.p2Geom, this.pointMaterial);
this.pointsGroup.add(this.p2)
this.domElement.removeEventListener('pointerdown', this.pointStart)
this.domElement.addEventListener('pointermove', this.move)
this.domElement.addEventListener('pointerdown', this.pointEnd)
}
move(e) {
const mouseLoc = this.getLocation(e);
this.lineGeom.attributes.position.set(mouseLoc, 3)
this.lineGeom.attributes.position.needsUpdate = true;
this.p2Geom.attributes.position.set(mouseLoc);
this.p2Geom.attributes.position.needsUpdate = true;
this.p2Geom.computeBoundingSphere();
this.dispatchEvent({ type: 'change' })
}
pointEnd(e) {
if (e.buttons !== 1) return;
this.domElement.removeEventListener('pointermove', this.move);
this.domElement.removeEventListener('pointerdown', this.pointEnd);
this.pointStart(e)
}
}

181
src/index.js Normal file
View File

@ -0,0 +1,181 @@
// class MinMaxGUIHelper {
// constructor(obj, minProp, maxProp, minDif) {
// this.obj = obj;
// this.minProp = minProp;
// this.maxProp = maxProp;
// this.minDif = minDif;
// }
// get min() {
// return this.obj[this.minProp];
// }
// set min(v) {
// this.obj[this.minProp] = v;
// this.obj[this.maxProp] = Math.max(this.obj[this.maxProp], v + this.minDif);
// }
// get max() {
// return this.obj[this.maxProp];
// }
// set max(v) {
// this.obj[this.maxProp] = v;
// this.min = this.min; // this will call the min setter
// }
// }
// const gui = new GUI();
// gui.add(camera, 'zoom', 0.01, 1, 0.01).listen();
// const minMaxGUIHelper = new MinMaxGUIHelper(camera, 'near', 'far', 0.1);
// gui.add(minMaxGUIHelper, 'min', 0.1, 50, 0.1).name('near');
// gui.add(minMaxGUIHelper, 'max', 0.1, 50, 0.1).name('far');
import * as THREE from '../node_modules/three/src/Three';
import { OrbitControls } from './OrbitControls'
import { TrackballControls } from './trackball'
import { Sketcher } from './Sketcher'
import GUI from '../node_modules/dat.gui/src/dat/gui/GUI.js'
import Stats from '../node_modules/three/examples/jsm/libs/stats.module.js';
import { KeyboardController } from './utils'
function main() {
const Controller = new KeyboardController()
var stats = new Stats();
stats.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom
document.body.appendChild(stats.dom);
const canvas = document.querySelector('#c');
const view1Elem = document.querySelector('#view1');
const view2Elem = document.querySelector('#view2');
const renderer = new THREE.WebGLRenderer({ canvas });
const scene = new THREE.Scene();
window.scene = scene;
scene.background = new THREE.Color('pink');
const helpersGroup = new THREE.Group();
scene.add(helpersGroup);
const size = 1;
const near = 5;
const far = 50;
const camera = new THREE.OrthographicCamera(-size, size, size, -size, near, far);
camera.zoom = 0.1;
camera.position.set(0, 0, 20);
const controls = new OrbitControls(camera, view1Elem);
// const controls = new TrackballControls(camera, view1Elem);
controls.target.set(0, 0, 0);
controls.update()
const cameraHelper = new THREE.CameraHelper(camera);
helpersGroup.add(cameraHelper);
const camera2 = new THREE.PerspectiveCamera(
60, // fov
2, // aspect
0.1, // near
500, // far
);
camera2.position.set(16, 28, 40);
camera2.lookAt(0, 5, 0);
const controls2 = new OrbitControls(camera2, view2Elem);
controls2.target.set(0, 5, 0);
controls2.update();
const axesHelper = new THREE.AxesHelper(5);
helpersGroup.add(axesHelper);
const sketchPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
const sketcher = new Sketcher(camera, view1Elem, sketchPlane)
scene.add(sketcher)
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(0, 10, 0);
light.target.position.set(-5, 0, 0);
scene.add(light);
scene.add(light.target);
}
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function setScissorForElement(elem) {
const canvasRect = canvas.getBoundingClientRect();
const elemRect = elem.getBoundingClientRect();
// compute a canvas relative rectangle
const right = Math.min(elemRect.right, canvasRect.right) - canvasRect.left;
const left = Math.max(0, elemRect.left - canvasRect.left);
const bottom = Math.min(elemRect.bottom, canvasRect.bottom) - canvasRect.top;
const top = Math.max(0, elemRect.top - canvasRect.top);
const width = Math.min(canvasRect.width, right - left);
const height = Math.min(canvasRect.height, bottom - top);
// setup the scissor to only render to that part of the canvas
const positiveYUpBottom = canvasRect.height - bottom;
renderer.setScissor(left, positiveYUpBottom, width, height);
renderer.setViewport(left, positiveYUpBottom, width, height);
// return the aspect
return width / height;
}
function render() {
stats.begin();
resizeRendererToDisplaySize(renderer);
renderer.setScissorTest(true);
{
const aspect = setScissorForElement(view1Elem);
camera.left = -aspect;
camera.right = aspect;
camera.updateProjectionMatrix();
cameraHelper.update();
cameraHelper.visible = false;
renderer.render(scene, camera);
}
{
const aspect = setScissorForElement(view2Elem);
camera2.aspect = aspect;
camera2.updateProjectionMatrix();
cameraHelper.visible = true;
renderer.render(scene, camera2);
}
stats.end();
// requestAnimationFrame(render);
}
// requestAnimationFrame(render);
controls.addEventListener('change', render);
controls.addEventListener('start', render);
controls2.addEventListener('change', render);
sketcher.addEventListener('change', render);
window.addEventListener('resize', render);
render();
}
main();

754
src/trackball.js Normal file
View File

@ -0,0 +1,754 @@
import {
EventDispatcher,
MOUSE,
Quaternion,
Vector2,
Vector3
} from '../node_modules/three/src/Three';
var TrackballControls = function ( object, domElement ) {
if ( domElement === undefined ) console.warn( 'THREE.TrackballControls: The second parameter "domElement" is now mandatory.' );
if ( domElement === document ) console.error( 'THREE.TrackballControls: "document" should not be used as the target "domElement". Please use "renderer.domElement" instead.' );
var scope = this;
var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_ZOOM_PAN: 4 };
this.object = object;
this.domElement = domElement;
// API
this.enabled = true;
this.screen = { left: 0, top: 0, width: 0, height: 0 };
this.rotateSpeed = 3.0;
this.zoomSpeed = 1.2;
this.panSpeed = 89.5;
this.noRotate = false;
this.noZoom = false;
this.noPan = false;
this.staticMoving = true;
this.dynamicDampingFactor = 0.2;
this.minDistance = 0;
this.maxDistance = Infinity;
this.keys = [ 65 /*A*/, 83 /*S*/, 68 /*D*/ ];
this.mouseButtons = { LEFT: null, MIDDLE: MOUSE.ROTATE, RIGHT: MOUSE.PAN };
// internals
this.target = new Vector3();
var EPS = 0.000001;
var lastPosition = new Vector3();
var lastZoom = 1;
var _state = STATE.NONE,
_keyState = STATE.NONE,
_eye = new Vector3(),
_movePrev = new Vector2(),
_moveCurr = new Vector2(),
_lastAxis = new Vector3(),
_lastAngle = 0,
_zoomStart = new Vector2(),
_zoomEnd = new Vector2(),
_touchZoomDistanceStart = 0,
_touchZoomDistanceEnd = 0,
_panStart = new Vector2(),
_panEnd = new Vector2();
// for reset
this.target0 = this.target.clone();
this.position0 = this.object.position.clone();
this.up0 = this.object.up.clone();
this.zoom0 = this.object.zoom;
// events
var changeEvent = { type: 'change' };
var startEvent = { type: 'start' };
var endEvent = { type: 'end' };
// methods
this.handleResize = function () {
var box = scope.domElement.getBoundingClientRect();
// adjustments come from similar code in the jquery offset() function
var d = scope.domElement.ownerDocument.documentElement;
scope.screen.left = box.left + window.pageXOffset - d.clientLeft;
scope.screen.top = box.top + window.pageYOffset - d.clientTop;
scope.screen.width = box.width;
scope.screen.height = box.height;
};
var getMouseOnScreen = ( function () {
var vector = new Vector2();
return function getMouseOnScreen( pageX, pageY ) {
vector.set(
( pageX - scope.screen.left ) / scope.screen.width,
( pageY - scope.screen.top ) / scope.screen.height
);
return vector;
};
}() );
var getMouseOnCircle = ( function () {
var vector = new Vector2();
return function getMouseOnCircle( pageX, pageY ) {
vector.set(
( ( pageX - scope.screen.width * 0.5 - scope.screen.left ) / ( scope.screen.width * 0.5 ) ),
( ( scope.screen.height + 2 * ( scope.screen.top - pageY ) ) / scope.screen.width ) // screen.width intentional
);
return vector;
};
}() );
this.rotateCamera = ( function () {
var axis = new Vector3(),
quaternion = new Quaternion(),
eyeDirection = new Vector3(),
objectUpDirection = new Vector3(),
objectSidewaysDirection = new Vector3(),
moveDirection = new Vector3(),
angle;
return function rotateCamera() {
moveDirection.set( _moveCurr.x - _movePrev.x, _moveCurr.y - _movePrev.y, 0 );
angle = moveDirection.length();
if ( angle ) {
_eye.copy( scope.object.position ).sub( scope.target );
eyeDirection.copy( _eye ).normalize();
objectUpDirection.copy( scope.object.up ).normalize();
objectSidewaysDirection.crossVectors( objectUpDirection, eyeDirection ).normalize();
objectUpDirection.setLength( _moveCurr.y - _movePrev.y );
objectSidewaysDirection.setLength( _moveCurr.x - _movePrev.x );
moveDirection.copy( objectUpDirection.add( objectSidewaysDirection ) );
axis.crossVectors( moveDirection, _eye ).normalize();
angle *= scope.rotateSpeed;
quaternion.setFromAxisAngle( axis, angle );
_eye.applyQuaternion( quaternion );
scope.object.up.applyQuaternion( quaternion );
_lastAxis.copy( axis );
_lastAngle = angle;
} else if ( ! scope.staticMoving && _lastAngle ) {
_lastAngle *= Math.sqrt( 1.0 - scope.dynamicDampingFactor );
_eye.copy( scope.object.position ).sub( scope.target );
quaternion.setFromAxisAngle( _lastAxis, _lastAngle );
_eye.applyQuaternion( quaternion );
scope.object.up.applyQuaternion( quaternion );
}
_movePrev.copy( _moveCurr );
};
}() );
this.zoomCamera = function () {
var factor;
if ( _state === STATE.TOUCH_ZOOM_PAN ) {
factor = _touchZoomDistanceStart / _touchZoomDistanceEnd;
_touchZoomDistanceStart = _touchZoomDistanceEnd;
if ( scope.object.isPerspectiveCamera ) {
_eye.multiplyScalar( factor );
} else if ( scope.object.isOrthographicCamera ) {
scope.object.zoom *= factor;
scope.object.updateProjectionMatrix();
} else {
console.warn( 'THREE.TrackballControls: Unsupported camera type' );
}
} else {
factor = 1.0 + ( _zoomEnd.y - _zoomStart.y ) * scope.zoomSpeed;
if ( factor !== 1.0 && factor > 0.0 ) {
if ( scope.object.isPerspectiveCamera ) {
_eye.multiplyScalar( factor );
} else if ( scope.object.isOrthographicCamera ) {
scope.object.zoom /= factor;
scope.object.updateProjectionMatrix();
} else {
console.warn( 'THREE.TrackballControls: Unsupported camera type' );
}
}
if ( scope.staticMoving ) {
_zoomStart.copy( _zoomEnd );
} else {
_zoomStart.y += ( _zoomEnd.y - _zoomStart.y ) * this.dynamicDampingFactor;
}
}
};
this.panCamera = ( function () {
var mouseChange = new Vector2(),
objectUp = new Vector3(),
pan = new Vector3();
return function panCamera() {
mouseChange.copy( _panEnd ).sub( _panStart );
if ( mouseChange.lengthSq() ) {
if ( scope.object.isOrthographicCamera ) {
var scale_x = ( scope.object.right - scope.object.left ) / scope.object.zoom / scope.domElement.clientWidth;
var scale_y = ( scope.object.top - scope.object.bottom ) / scope.object.zoom / scope.domElement.clientWidth;
mouseChange.x *= scale_x;
mouseChange.y *= scale_y;
}
mouseChange.multiplyScalar( _eye.length() * scope.panSpeed );
pan.copy( _eye ).cross( scope.object.up ).setLength( mouseChange.x );
pan.add( objectUp.copy( scope.object.up ).setLength( mouseChange.y ) );
scope.object.position.add( pan );
scope.target.add( pan );
if ( scope.staticMoving ) {
_panStart.copy( _panEnd );
} else {
_panStart.add( mouseChange.subVectors( _panEnd, _panStart ).multiplyScalar( scope.dynamicDampingFactor ) );
}
}
};
}() );
this.checkDistances = function () {
if ( ! scope.noZoom || ! scope.noPan ) {
if ( _eye.lengthSq() > scope.maxDistance * scope.maxDistance ) {
scope.object.position.addVectors( scope.target, _eye.setLength( scope.maxDistance ) );
_zoomStart.copy( _zoomEnd );
}
if ( _eye.lengthSq() < scope.minDistance * scope.minDistance ) {
scope.object.position.addVectors( scope.target, _eye.setLength( scope.minDistance ) );
_zoomStart.copy( _zoomEnd );
}
}
};
this.update = function () {
_eye.subVectors( scope.object.position, scope.target );
if ( ! scope.noRotate ) {
scope.rotateCamera();
}
if ( ! scope.noZoom ) {
scope.zoomCamera();
}
if ( ! scope.noPan ) {
scope.panCamera();
}
scope.object.position.addVectors( scope.target, _eye );
if ( scope.object.isPerspectiveCamera ) {
scope.checkDistances();
scope.object.lookAt( scope.target );
if ( lastPosition.distanceToSquared( scope.object.position ) > EPS ) {
scope.dispatchEvent( changeEvent );
lastPosition.copy( scope.object.position );
}
} else if ( scope.object.isOrthographicCamera ) {
scope.object.lookAt( scope.target );
if ( lastPosition.distanceToSquared( scope.object.position ) > EPS || lastZoom !== scope.object.zoom ) {
scope.dispatchEvent( changeEvent );
lastPosition.copy( scope.object.position );
lastZoom = scope.object.zoom;
}
} else {
console.warn( 'THREE.TrackballControls: Unsupported camera type' );
}
};
this.reset = function () {
_state = STATE.NONE;
_keyState = STATE.NONE;
scope.target.copy( scope.target0 );
scope.object.position.copy( scope.position0 );
scope.object.up.copy( scope.up0 );
scope.object.zoom = scope.zoom0;
scope.object.updateProjectionMatrix();
_eye.subVectors( scope.object.position, scope.target );
scope.object.lookAt( scope.target );
scope.dispatchEvent( changeEvent );
lastPosition.copy( scope.object.position );
lastZoom = scope.object.zoom;
};
// listeners
function onPointerDown( event ) {
if ( scope.enabled === false ) return;
switch ( event.pointerType ) {
case 'mouse':
case 'pen':
onMouseDown( event );
break;
// TODO touch
}
}
function onPointerMove( event ) {
if ( scope.enabled === false ) return;
switch ( event.pointerType ) {
case 'mouse':
case 'pen':
onMouseMove( event );
break;
// TODO touch
}
}
function onPointerUp( event ) {
if ( scope.enabled === false ) return;
switch ( event.pointerType ) {
case 'mouse':
case 'pen':
onMouseUp( event );
break;
// TODO touch
}
}
function keydown( event ) {
if ( scope.enabled === false ) return;
window.removeEventListener( 'keydown', keydown );
if ( _keyState !== STATE.NONE ) {
return;
} else if ( event.keyCode === scope.keys[ STATE.ROTATE ] && ! scope.noRotate ) {
_keyState = STATE.ROTATE;
} else if ( event.keyCode === scope.keys[ STATE.ZOOM ] && ! scope.noZoom ) {
_keyState = STATE.ZOOM;
} else if ( event.keyCode === scope.keys[ STATE.PAN ] && ! scope.noPan ) {
_keyState = STATE.PAN;
}
}
function keyup() {
if ( scope.enabled === false ) return;
_keyState = STATE.NONE;
window.addEventListener( 'keydown', keydown );
}
function onMouseDown( event ) {
event.preventDefault();
event.stopPropagation();
if ( _state === STATE.NONE ) {
switch ( event.button ) {
case 0:
_state = scope.mouseButtons.LEFT;
break;
case 1:
_state = scope.mouseButtons.MIDDLE;
break;
case 2:
_state = scope.mouseButtons.RIGHT;
break;
default:
_state = STATE.NONE;
}
}
var state = ( _keyState !== STATE.NONE ) ? _keyState : _state;
if ( state === STATE.ROTATE && ! scope.noRotate ) {
_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
_movePrev.copy( _moveCurr );
} else if ( state === STATE.ZOOM && ! scope.noZoom ) {
_zoomStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
_zoomEnd.copy( _zoomStart );
} else if ( state === STATE.PAN && ! scope.noPan ) {
_panStart.copy( getMouseOnScreen( event.pageX, event.pageY ) );
_panEnd.copy( _panStart );
}
scope.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove );
scope.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp );
scope.dispatchEvent( startEvent );
}
function onMouseMove( event ) {
if ( scope.enabled === false ) return;
event.preventDefault();
event.stopPropagation();
var state = ( _keyState !== STATE.NONE ) ? _keyState : _state;
if ( state === STATE.ROTATE && ! scope.noRotate ) {
_movePrev.copy( _moveCurr );
_moveCurr.copy( getMouseOnCircle( event.pageX, event.pageY ) );
} else if ( state === STATE.ZOOM && ! scope.noZoom ) {
_zoomEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
} else if ( state === STATE.PAN && ! scope.noPan ) {
_panEnd.copy( getMouseOnScreen( event.pageX, event.pageY ) );
}
scope.update()
}
function onMouseUp( event ) {
if ( scope.enabled === false ) return;
event.preventDefault();
event.stopPropagation();
_state = STATE.NONE;
scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove );
scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp );
scope.dispatchEvent( endEvent );
}
function mousewheel( event ) {
if ( scope.enabled === false ) return;
if ( scope.noZoom === true ) return;
event.preventDefault();
event.stopPropagation();
switch ( event.deltaMode ) {
case 2:
// Zoom in pages
_zoomStart.y -= event.deltaY * 0.025;
break;
case 1:
// Zoom in lines
_zoomStart.y -= event.deltaY * 0.01;
break;
default:
// undefined, 0, assume pixels
_zoomStart.y -= event.deltaY * 0.00025;
break;
}
scope.update()
scope.dispatchEvent( startEvent );
scope.dispatchEvent( endEvent );
}
function touchstart( event ) {
if ( scope.enabled === false ) return;
event.preventDefault();
switch ( event.touches.length ) {
case 1:
_state = STATE.TOUCH_ROTATE;
_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
_movePrev.copy( _moveCurr );
break;
default: // 2 or more
_state = STATE.TOUCH_ZOOM_PAN;
var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
_touchZoomDistanceEnd = _touchZoomDistanceStart = Math.sqrt( dx * dx + dy * dy );
var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
_panStart.copy( getMouseOnScreen( x, y ) );
_panEnd.copy( _panStart );
break;
}
scope.dispatchEvent( startEvent );
}
function touchmove( event ) {
if ( scope.enabled === false ) return;
event.preventDefault();
event.stopPropagation();
switch ( event.touches.length ) {
case 1:
_movePrev.copy( _moveCurr );
_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
break;
default: // 2 or more
var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX;
var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY;
_touchZoomDistanceEnd = Math.sqrt( dx * dx + dy * dy );
var x = ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ) / 2;
var y = ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ) / 2;
_panEnd.copy( getMouseOnScreen( x, y ) );
break;
}
}
function touchend( event ) {
if ( scope.enabled === false ) return;
switch ( event.touches.length ) {
case 0:
_state = STATE.NONE;
break;
case 1:
_state = STATE.TOUCH_ROTATE;
_moveCurr.copy( getMouseOnCircle( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ) );
_movePrev.copy( _moveCurr );
break;
}
scope.dispatchEvent( endEvent );
}
function contextmenu( event ) {
if ( scope.enabled === false ) return;
event.preventDefault();
}
this.dispose = function () {
scope.domElement.removeEventListener( 'contextmenu', contextmenu );
scope.domElement.removeEventListener( 'pointerdown', onPointerDown );
scope.domElement.removeEventListener( 'wheel', mousewheel );
scope.domElement.removeEventListener( 'touchstart', touchstart );
scope.domElement.removeEventListener( 'touchend', touchend );
scope.domElement.removeEventListener( 'touchmove', touchmove );
scope.domElement.ownerDocument.removeEventListener( 'pointermove', onPointerMove );
scope.domElement.ownerDocument.removeEventListener( 'pointerup', onPointerUp );
window.removeEventListener( 'keydown', keydown );
window.removeEventListener( 'keyup', keyup );
};
this.domElement.addEventListener( 'contextmenu', contextmenu );
this.domElement.addEventListener( 'pointerdown', onPointerDown );
this.domElement.addEventListener( 'wheel', mousewheel );
this.domElement.addEventListener( 'touchstart', touchstart );
this.domElement.addEventListener( 'touchend', touchend );
this.domElement.addEventListener( 'touchmove', touchmove );
this.domElement.ownerDocument.addEventListener( 'pointermove', onPointerMove );
this.domElement.ownerDocument.addEventListener( 'pointerup', onPointerUp );
window.addEventListener( 'keydown', keydown );
window.addEventListener( 'keyup', keyup );
this.handleResize();
// force an update at start
this.update();
};
TrackballControls.prototype = Object.create( EventDispatcher.prototype );
TrackballControls.prototype.constructor = TrackballControls;
export { TrackballControls };

10
src/utils.js Normal file
View File

@ -0,0 +1,10 @@
export function KeyboardController() {
this.state="";
window.addEventListener('keydown', (e)=> {
if (e.key == "Escape"){
this.state = ""
} else {
this.state = e.key
}
})
}

1
stats.json Normal file

File diff suppressed because one or more lines are too long

31
webpack.common.js Normal file
View File

@ -0,0 +1,31 @@
const path = require('path');
module.exports = {
entry: './src/index.js',
plugins: [
],
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
// clean: true,
},
module: {
rules: [
{
test: /\.s[ac]ss$/i,
use: [
// Creates `style` nodes from JS strings
"style-loader",
// Translates CSS into CommonJS
"css-loader",
// Compiles Sass to CSS
"sass-loader",
],
},
],
},
};

12
webpack.dev.js Normal file
View File

@ -0,0 +1,12 @@
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'development',
// devtool: 'inline-source-map',
});

10
webpack.prod.js Normal file
View File

@ -0,0 +1,10 @@
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production',
});