mirror of
https://github.com/Doodle3D/Doodle3D-Core.git
synced 2024-12-22 11:03:48 +01:00
Merge branch 'feature/cutout'
This commit is contained in:
commit
171c0445e8
BIN
img/contextmenu/btnHole.png
Normal file
BIN
img/contextmenu/btnHole.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
img/contextmenu/btnSolid.png
Normal file
BIN
img/contextmenu/btnSolid.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
img/holepatern.png
Normal file
BIN
img/holepatern.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
15
index.js
15
index.js
@ -57,6 +57,21 @@ window.addEventListener('dragover', (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
import * as CAL from 'cal';
|
||||
store.dispatch(actions.addObject({
|
||||
type: 'STAR',
|
||||
fill: true,
|
||||
solid: false,
|
||||
star: { innerRadius: 10, outerRadius: 20, rays: 5 },
|
||||
transform: new CAL.Matrix({ x: -20, y: 0 })
|
||||
}));
|
||||
store.dispatch(actions.addObject({
|
||||
type: 'RECT',
|
||||
fill: true,
|
||||
rectSize: new CAL.Vector(20, 20),
|
||||
height: 40,
|
||||
transform: new CAL.Matrix({ x: -10, y: -10 })
|
||||
}));
|
||||
|
||||
// render dom
|
||||
import React from 'react';
|
||||
|
23
package-lock.json
generated
23
package-lock.json
generated
@ -36,19 +36,18 @@
|
||||
"integrity": "sha512-w1+sG3ClsSaQwo3ks5wl6QLe4aWEHBe8QePq0IeAcj+lypqo770sUcWVfEZWUFBumAhSlCJg3GRc8MsycHy8LA=="
|
||||
},
|
||||
"@doodle3d/threejs-export-obj": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@doodle3d/threejs-export-obj/-/threejs-export-obj-0.0.4.tgz",
|
||||
"integrity": "sha512-7wF302lO77y7bt/pdPPoTS7wAW8TNMavW7ps60LqOCa/KmNDe0hYvgNXwsn61kGW7aKorlx22Y5stCjHQs5GRA==",
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@doodle3d/threejs-export-obj/-/threejs-export-obj-0.0.5.tgz",
|
||||
"integrity": "sha512-kU3xpA77DjRQz27UBX3/PsdX6GecEJ274msqvzSrRiYZXQSs7HFIFsS0a4lcyzJd2Q8SkzMIOHvOA9h85dxWTQ==",
|
||||
"requires": {
|
||||
"babel-preset-env": "1.6.1",
|
||||
"jszip": "3.1.4",
|
||||
"three": "0.87.1"
|
||||
"jszip": "3.1.5",
|
||||
"three": "0.88.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"three": {
|
||||
"version": "0.87.1",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.87.1.tgz",
|
||||
"integrity": "sha1-Rmo07cRUNFnO2bnX0na2Uhb+K6g="
|
||||
"version": "0.88.0",
|
||||
"resolved": "https://registry.npmjs.org/three/-/three-0.88.0.tgz",
|
||||
"integrity": "sha1-QlbC/Djk+yOg0j66K2zOTfjkZtU="
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -5167,9 +5166,9 @@
|
||||
}
|
||||
},
|
||||
"jszip": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.4.tgz",
|
||||
"integrity": "sha512-z6w8iYIxZ/fcgul0j/OerkYnkomH8BZigvzbxVmr2h5HkZUrPtk2kjYtLkqR9wwQxEP6ecKNoKLsbhd18jfnGA==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.1.5.tgz",
|
||||
"integrity": "sha512-5W8NUaFRFRqTOL7ZDDrx5qWHJyBXy6velVudIzQUSoqAAYqzSh2Z7/m0Rf1QbmQJccegD0r+YZxBjzqoBiEeJQ==",
|
||||
"requires": {
|
||||
"core-js": "2.3.0",
|
||||
"es6-promise": "3.0.2",
|
||||
|
@ -17,7 +17,7 @@
|
||||
"@doodle3d/clipper-js": "^1.0.7",
|
||||
"@doodle3d/fill-path": "^1.0.7",
|
||||
"@doodle3d/potrace-js": "0.0.6",
|
||||
"@doodle3d/threejs-export-obj": "0.0.4",
|
||||
"@doodle3d/threejs-export-obj": "0.0.5",
|
||||
"@doodle3d/threejs-export-stl": "0.0.3",
|
||||
"@doodle3d/touch-events": "0.0.7",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
|
@ -20,6 +20,7 @@ import { PIXEL_RATIO } from '../constants/general';
|
||||
import ShapesManager from '../d2/ShapesManager.js';
|
||||
import EventGroup from '../d2/EventGroup.js';
|
||||
import ReactResizeDetector from 'react-resize-detector';
|
||||
import { load as loadPattern } from '../d2/Shape.js';
|
||||
// import createDebug from 'debug';
|
||||
// const debug = createDebug('d3d:d2');
|
||||
|
||||
@ -91,6 +92,11 @@ class D2Panel extends React.Component {
|
||||
this.objectContainerInactive.add(new Grid(new CAL.Color(0xdddddd)));
|
||||
|
||||
this.shapesManager = new ShapesManager(this.objectContainerActive, this.objectContainerInactive);
|
||||
loadPattern.then(() => {
|
||||
this.activeNeedRender = true;
|
||||
this.inactiveNeedRender = true;
|
||||
this.renderRequest();
|
||||
});
|
||||
|
||||
this.DOM = null;
|
||||
}
|
||||
|
@ -117,6 +117,7 @@ class D3Panel extends React.Component {
|
||||
this.plane.rotation.x = Math.PI / 2;
|
||||
this.plane.position.y = -0.01;
|
||||
this.plane.name = 'bed-plane';
|
||||
this.plane.isBedPlane = true;
|
||||
this.scene.add(this.plane);
|
||||
|
||||
const directionalLight = new THREE.PointLight(0xffffff, 0.6);
|
||||
|
@ -148,8 +148,8 @@ function renderChildren(children) {
|
||||
return components;
|
||||
}
|
||||
|
||||
function filterMenus(numSelectedObjects, allObjectsAreFilled, activeTool, menus) {
|
||||
const showUnion = activeTool === d2Tools.TRANSFORM && allObjectsAreFilled && numSelectedObjects >= 2;
|
||||
function filterMenus(activeTool, numSelectedObjects, numFilledObjects, numSolidObjects, menus) {
|
||||
const showUnion = activeTool === d2Tools.TRANSFORM && numFilledObjects && numSelectedObjects >= 2;
|
||||
const showIntersect = activeTool === d2Tools.TRANSFORM && numSelectedObjects >= 1;
|
||||
return {
|
||||
...menus,
|
||||
@ -166,9 +166,11 @@ function filterMenus(numSelectedObjects, allObjectsAreFilled, activeTool, menus)
|
||||
|
||||
case contextTools.DUPLICATE:
|
||||
case contextTools.DELETE:
|
||||
case contextTools.FILL_TOGGLE:
|
||||
return numSelectedObjects > 0;
|
||||
|
||||
case contextTools.FILL_TOGGLE:
|
||||
return numSelectedObjects > 0 && numSolidObjects === numSelectedObjects;
|
||||
|
||||
case contextTools.ALIGN:
|
||||
return numSelectedObjects > 1;
|
||||
|
||||
@ -183,11 +185,14 @@ function filterMenus(numSelectedObjects, allObjectsAreFilled, activeTool, menus)
|
||||
case contextTools.BRUSH_SIZE:
|
||||
return activeTool === d2Tools.BRUSH;
|
||||
|
||||
case contextTools.HOLE_TOGGLE:
|
||||
return numSelectedObjects > 0 && numFilledObjects === numSelectedObjects;
|
||||
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}).map(child => {
|
||||
return filterMenus(numSelectedObjects, allObjectsAreFilled, activeTool, child);
|
||||
return filterMenus(activeTool, numSelectedObjects, numFilledObjects, numSolidObjects, child);
|
||||
})
|
||||
};
|
||||
}
|
||||
@ -206,13 +211,14 @@ function nestChildren(menus, target) {
|
||||
|
||||
const getMenus = createSelector([
|
||||
state => state.sketcher.present.menus,
|
||||
state => state.sketcher.present.d2.tool,
|
||||
state => state.sketcher.present.selection.objects.length,
|
||||
state => state.sketcher.present.selection.objects.every(({ id }) => state.sketcher.present.objectsById[id].fill),
|
||||
state => state.sketcher.present.d2.tool
|
||||
], (menus, numSelectedObjects, allObjectsAreFilled, activeTool) => ({
|
||||
toolbar2d: filterMenus(numSelectedObjects, allObjectsAreFilled, activeTool, nestChildren(menus, menus[TOOLBAR2D])),
|
||||
toolbar3d: filterMenus(numSelectedObjects, allObjectsAreFilled, activeTool, nestChildren(menus, menus[TOOLBAR3D])),
|
||||
context: filterMenus(numSelectedObjects, allObjectsAreFilled, activeTool, nestChildren(menus, menus[CONTEXT]))
|
||||
state => state.sketcher.present.selection.objects.filter(({ id }) => state.sketcher.present.objectsById[id].fill).length,
|
||||
state => state.sketcher.present.selection.objects.filter(({ id }) => state.sketcher.present.objectsById[id].solid).length
|
||||
], (menus, activeTool, numSelectedObjects, numFilledObjects, numSolidObjects) => ({
|
||||
toolbar2d: filterMenus(activeTool, numSelectedObjects, numFilledObjects, numSolidObjects, nestChildren(menus, menus[TOOLBAR2D])),
|
||||
toolbar3d: filterMenus(activeTool, numSelectedObjects, numFilledObjects, numSolidObjects, nestChildren(menus, menus[TOOLBAR3D])),
|
||||
context: filterMenus(activeTool, numSelectedObjects, numFilledObjects, numSolidObjects, nestChildren(menus, menus[CONTEXT]))
|
||||
}));
|
||||
|
||||
const style = document.createElement('style');
|
||||
@ -403,6 +409,18 @@ style.innerHTML = `
|
||||
width: 33px;
|
||||
height: 41px;
|
||||
}
|
||||
#hole-toggle-solid, #hole-toggle-solid-menu {
|
||||
background-image: url('../img/contextmenu/btnSolid.png');
|
||||
background-size: 33px auto;
|
||||
width: 33px;
|
||||
height: 41px;
|
||||
}
|
||||
#hole-toggle-hole, #hole-toggle-hole-menu {
|
||||
background-image: url('../img/contextmenu/btnHole.png');
|
||||
background-size: 33px auto;
|
||||
width: 33px;
|
||||
height: 41px;
|
||||
}
|
||||
#align-right-menu, #align-horizontal-menu, #align-left-menu,
|
||||
#align-top-menu, #align-vertical-menu, #align-bottom-menu {
|
||||
background-image: url('../img/contextmenu/btnAlignHorizontal.png');
|
||||
|
@ -4,6 +4,7 @@ export const COLOR_PICKER = 'color-picker-tool';
|
||||
export const ERASER_SIZE = 'eraser-size-tool';
|
||||
export const BRUSH_SIZE = 'brush-size-tool';
|
||||
export const FILL_TOGGLE = 'fill-toggle-tool';
|
||||
export const HOLE_TOGGLE = 'hole-toggle-tool';
|
||||
export const ALIGN = 'align-tool';
|
||||
export const ADVANCED = 'advanced-tool';
|
||||
|
||||
@ -63,6 +64,14 @@ export const FILL_TOGGLE_TOOLS = [
|
||||
FILL_TOGGLE_OUTLINE
|
||||
];
|
||||
|
||||
export const HOLE_TOGGLE_HOLE = 'hole-toggle-hole';
|
||||
export const HOLE_TOGGLE_SOLID = 'hole-toggle-solid';
|
||||
|
||||
export const HOLE_TOGGLE_TOOLS = [
|
||||
HOLE_TOGGLE_HOLE,
|
||||
HOLE_TOGGLE_SOLID
|
||||
];
|
||||
|
||||
export const ALIGN_LEFT = 'align-left';
|
||||
export const ALIGN_HORIZONTAL = 'align-horizontal';
|
||||
export const ALIGN_RIGHT = 'align-right';
|
||||
|
2
src/constants/d3Constants.js
vendored
2
src/constants/d3Constants.js
vendored
@ -1,5 +1,5 @@
|
||||
export const SCULPT_LIMIT = 0.6;
|
||||
export const DESELECT_TRANSPARENCY = 0.8;
|
||||
export const DESELECT_TRANSPARENCY = 0.2;
|
||||
export const MIN_CAMERA_ZOOM = 10;
|
||||
export const MAX_CAMERA_ZOOM = 600;
|
||||
export const MAX_CAMERA_PAN = 150;
|
||||
|
@ -80,6 +80,11 @@ const context = {
|
||||
selected: contextTools.FILL_TOGGLE_FILL,
|
||||
children: contextTools.FILL_TOGGLE_TOOLS.map(value => ({ value })),
|
||||
...toggleBehavior
|
||||
}, {
|
||||
value: contextTools.HOLE_TOGGLE,
|
||||
selected: contextTools.HOLE_TOGGLE_SOLID,
|
||||
children: contextTools.HOLE_TOGGLE_TOOLS.map(value => ({ value })),
|
||||
...toggleBehavior
|
||||
}, {
|
||||
value: contextTools.ALIGN,
|
||||
selected: contextTools.ALIGN_HORIZONTAL,
|
||||
|
@ -20,7 +20,8 @@ const defaultProperties = {
|
||||
z: 0.0,
|
||||
sculpt: [{ pos: 0.0, scale: 1.0 }, { pos: 1.0, scale: 1.0 }],
|
||||
twist: 0.0,
|
||||
fill : false
|
||||
fill: false,
|
||||
solid: true
|
||||
};
|
||||
|
||||
export const SHAPE_TYPE_PROPERTIES = {
|
||||
|
@ -5,6 +5,13 @@ import { LINE_WIDTH } from '../constants/d2Constants.js';
|
||||
import { hexToStyle } from '../utils/colorUtils.js';
|
||||
import { DESELECT_TRANSPARENCY, FILL_TRANSPARENCY, LINE_TRANSPARENCY } from '../constants/d2Constants.js';
|
||||
import { PIXEL_RATIO } from '../constants/general.js';
|
||||
import holePaternUrl from '../../img/holepatern.png';
|
||||
import { loadImage } from '../utils/imageUtils.js';
|
||||
|
||||
let holePatern;
|
||||
export const load = loadImage(holePaternUrl).then(image => {
|
||||
holePatern = document.createElement('canvas').getContext('2d').createPattern(image, 'repeat');
|
||||
});
|
||||
|
||||
export default class Shape extends Matrix {
|
||||
constructor(shapeData) {
|
||||
@ -50,6 +57,10 @@ export default class Shape extends Matrix {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!this._shapeData || this._shapeData.solid !== shapeData.solid) {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
this._shapeData = shapeData;
|
||||
return changed;
|
||||
}
|
||||
@ -94,7 +105,10 @@ export default class Shape extends Matrix {
|
||||
const lineWidth = PIXEL_RATIO * LINE_WIDTH;
|
||||
|
||||
context.globalAlpha = this.alpha;
|
||||
if (this._shapeData.fill) {
|
||||
if (!this._shapeData.solid) {
|
||||
context.fillStyle = holePatern;
|
||||
context.fill();
|
||||
} else if (this._shapeData.fill) {
|
||||
context.fillStyle = this.color;
|
||||
context.fill();
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { shapeDataToShape } from '../shape/shapeDataUtils.js';
|
||||
import { shapeDataToShape, determineActiveShape2d } from '../shape/shapeDataUtils.js';
|
||||
// import R from 'ramda';
|
||||
|
||||
export default class ShapesManager {
|
||||
@ -14,10 +14,8 @@ export default class ShapesManager {
|
||||
update(state) {
|
||||
const needRender = { active: false, inactive: false };
|
||||
|
||||
const selectedObjects = state.selection.objects.map(({ id }) => id);
|
||||
// determine if shape is "active", meaning it will be updated frequently
|
||||
const activeShapes = Object.keys(state.objectsById)
|
||||
.filter(id => state.d2.activeShape === id || selectedObjects.indexOf(id) !== -1);
|
||||
const activeShapes = determineActiveShape2d(state);
|
||||
|
||||
const { objectsById } = state;
|
||||
|
||||
@ -45,7 +43,7 @@ export default class ShapesManager {
|
||||
const newInactiveObjectUIDs = [];
|
||||
|
||||
for (const UID of spaceObjectIds) {
|
||||
const active = activeShapes.indexOf(UID) !== -1;
|
||||
const active = activeShapes[UID];
|
||||
if (active) {
|
||||
newActiveObjectUIDs.push(UID);
|
||||
} else {
|
||||
|
111
src/d3/ShapeMesh.js
vendored
111
src/d3/ShapeMesh.js
vendored
@ -4,6 +4,9 @@ import { shapeToPointsCornered } from '../shape/shapeToPoints.js';
|
||||
import * as THREE from 'three';
|
||||
import { getPointsBounds, shapeChanged } from '../shape/shapeDataUtils.js';
|
||||
import { DESELECT_TRANSPARENCY, LEGACY_HEIGHT_STEP } from '../constants/d3Constants.js';
|
||||
import ThreeBSP from 'three-js-csg';
|
||||
|
||||
const THREE_BSP = ThreeBSP(THREE);
|
||||
|
||||
const MAX_HEIGHT_BASE = 5;
|
||||
// Legacy compensation. Compensating for the fact that we
|
||||
@ -12,9 +15,12 @@ const MAX_HEIGHT_BASE = 5;
|
||||
// and converting old files on open once
|
||||
const isValidNumber = (num) => typeof num === 'number' && !isNaN(num);
|
||||
|
||||
class ShapeMesh extends THREE.Mesh {
|
||||
constructor(shapeData, toonShader) {
|
||||
const { sculpt, rotate, twist, height, type, transform, z, color, fill } = shapeData;
|
||||
class ShapeMesh extends THREE.Object3D {
|
||||
constructor(shapeData, active, toonShader) {
|
||||
super();
|
||||
this.name = shapeData.UID;
|
||||
|
||||
const { sculpt, rotate, twist, height, type, transform, z, color, fill, solid } = shapeData;
|
||||
|
||||
let material;
|
||||
if (toonShader) {
|
||||
@ -30,12 +36,12 @@ class ShapeMesh extends THREE.Mesh {
|
||||
});
|
||||
}
|
||||
|
||||
super(new THREE.BufferGeometry(), material);
|
||||
this._mesh = new THREE.Mesh(new THREE.BufferGeometry(), material.clone());
|
||||
this._mesh.name = shapeData.UID;
|
||||
this._mesh.isShapeMesh = true;
|
||||
|
||||
this._toonShader = toonShader;
|
||||
|
||||
this.name = shapeData.UID;
|
||||
|
||||
this._shapes = [];
|
||||
this._shapesMap = [];
|
||||
|
||||
@ -51,11 +57,45 @@ class ShapeMesh extends THREE.Mesh {
|
||||
this._shapeData = shapeData;
|
||||
this._color = color;
|
||||
this._fill = fill;
|
||||
|
||||
this.updatePoints(shapeData);
|
||||
|
||||
this._holeMesh = new THREE.Mesh(new THREE.Geometry().fromBufferGeometry(this._mesh.geometry), material.clone());
|
||||
this._holeMesh.name = shapeData.UID;
|
||||
this._holeMesh.isShapeMesh = true;
|
||||
|
||||
this.updateSolid(solid, active);
|
||||
}
|
||||
|
||||
update(shapeData) {
|
||||
add(object) {
|
||||
if (!this.children.includes(object)) super.add(object);
|
||||
}
|
||||
remove(object) {
|
||||
if (this.children.includes(object)) super.remove(object);
|
||||
}
|
||||
|
||||
updateHoleGeometry(holes) {
|
||||
if (holes === this._holes && !this._changedGeometry) return false;
|
||||
if (!this._solid) return false;
|
||||
|
||||
this._holeMesh.geometry.dispose();
|
||||
|
||||
if (holes === null || !this._fill) {
|
||||
this._holeMesh.geometry = new THREE.Geometry().fromBufferGeometry(this._mesh.geometry);
|
||||
return true;
|
||||
}
|
||||
|
||||
const objectGeometry = new THREE.Geometry().fromBufferGeometry(this._mesh.geometry);
|
||||
let objectBSP = new THREE_BSP(objectGeometry);
|
||||
objectGeometry.dispose();
|
||||
objectBSP = objectBSP.subtract(holes);
|
||||
this._holeMesh.geometry = objectBSP.toMesh().geometry;
|
||||
|
||||
this._holes = holes;
|
||||
this._changedGeometry = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
update(shapeData, active) {
|
||||
let changed = false;
|
||||
|
||||
if (shapeChanged(this._shapeData, shapeData)) {
|
||||
@ -88,17 +128,25 @@ class ShapeMesh extends THREE.Mesh {
|
||||
changed = true;
|
||||
}
|
||||
|
||||
let solidChanged = false;
|
||||
if (shapeData.solid !== this._solid || active !== this._active) {
|
||||
this.updateSolid(shapeData.solid, active);
|
||||
changed = true;
|
||||
solidChanged = true;
|
||||
}
|
||||
|
||||
this._shapeData = shapeData;
|
||||
return changed;
|
||||
}
|
||||
|
||||
setOpaque(opaque) {
|
||||
this.material.opacity = opaque ? 1.0 : 1.0 - DESELECT_TRANSPARENCY;
|
||||
this.material.transparent = !opaque;
|
||||
this._holeMesh.material.opacity = opaque ? 1.0 : DESELECT_TRANSPARENCY;
|
||||
this._holeMesh.material.transparent = !opaque;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.geometry.dispose();
|
||||
this._mesh.geometry.dispose();
|
||||
this._holeMesh.geometry.dispose();
|
||||
}
|
||||
|
||||
updatePoints(shapeData) {
|
||||
@ -173,10 +221,27 @@ class ShapeMesh extends THREE.Mesh {
|
||||
throw new Error(`Cannot update object ${this.name}: color is an invalid value.`);
|
||||
}
|
||||
|
||||
this.material.color.setHex(color);
|
||||
this._holeMesh.material.color.setHex(color);
|
||||
this._color = color;
|
||||
}
|
||||
|
||||
updateSolid(solid, active) {
|
||||
this._mesh.material.opacity = solid ? 1.0 : 0.0;
|
||||
this._mesh.material.transparent = !solid;
|
||||
this.visible = solid || active;
|
||||
|
||||
if (active || !solid) {
|
||||
this.add(this._mesh);
|
||||
this.remove(this._holeMesh);
|
||||
} else {
|
||||
this.add(this._holeMesh);
|
||||
this.remove(this._mesh);
|
||||
}
|
||||
|
||||
this._solid = solid;
|
||||
this._active = active;
|
||||
}
|
||||
|
||||
_getPoint(point, heightStep, center) {
|
||||
const { scale, pos: y } = this._heightSteps[heightStep];
|
||||
|
||||
@ -247,10 +312,12 @@ class ShapeMesh extends THREE.Mesh {
|
||||
|
||||
this._vertexBuffer.needsUpdate = true;
|
||||
|
||||
this.geometry.boundingBox = null;
|
||||
this.geometry.boundingSphere = null;
|
||||
this.geometry.computeFaceNormals();
|
||||
this.geometry.computeVertexNormals();
|
||||
this._mesh.geometry.boundingBox = null;
|
||||
this._mesh.geometry.boundingSphere = null;
|
||||
this._mesh.geometry.computeFaceNormals();
|
||||
this._mesh.geometry.computeVertexNormals();
|
||||
|
||||
this._changedGeometry = true;
|
||||
}
|
||||
_updateSide() {
|
||||
// TODO use higher precision for export mesh
|
||||
@ -287,6 +354,8 @@ class ShapeMesh extends THREE.Mesh {
|
||||
this._heightSteps = heightSteps;
|
||||
|
||||
if (heightStepsChanged) this._updateFaces();
|
||||
|
||||
this._changedGeometry = true;
|
||||
}
|
||||
_updateFaces() {
|
||||
// TODO
|
||||
@ -295,8 +364,8 @@ class ShapeMesh extends THREE.Mesh {
|
||||
|
||||
const numHeightSteps = this._heightSteps.length;
|
||||
|
||||
this.geometry.dispose();
|
||||
this.geometry = new THREE.BufferGeometry();
|
||||
this._mesh.geometry.dispose();
|
||||
this._mesh.geometry = new THREE.BufferGeometry();
|
||||
|
||||
// store total number of indexes and vertices needed
|
||||
let indexBufferLength = 0;
|
||||
@ -370,7 +439,7 @@ class ShapeMesh extends THREE.Mesh {
|
||||
|
||||
const indexes = new Uint32Array(indexBufferLength);
|
||||
const indexBuffer = new THREE.BufferAttribute(indexes, 1);
|
||||
this.geometry.setIndex(indexBuffer);
|
||||
this._mesh.geometry.setIndex(indexBuffer);
|
||||
|
||||
let indexCounter = 0;
|
||||
for (let i = 0; i < this._shapes.length; i ++) {
|
||||
@ -407,7 +476,9 @@ class ShapeMesh extends THREE.Mesh {
|
||||
|
||||
this._vertices = new Float32Array(vertexBufferLength);
|
||||
this._vertexBuffer = new THREE.BufferAttribute(this._vertices, 3);
|
||||
this.geometry.addAttribute('position', this._vertexBuffer);
|
||||
this._mesh.geometry.addAttribute('position', this._vertexBuffer);
|
||||
|
||||
this._changedGeometry = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
62
src/d3/ShapesManager.js
vendored
62
src/d3/ShapesManager.js
vendored
@ -1,6 +1,10 @@
|
||||
import { determineActiveShape3d } from '../shape/shapeDataUtils.js';
|
||||
import { SHAPE_TYPE_PROPERTIES } from '../constants/shapeTypeProperties.js';
|
||||
import * as THREE from 'three';
|
||||
import ShapeMesh from './ShapeMesh.js';
|
||||
import ThreeBSP from 'three-js-csg';
|
||||
|
||||
const THREE_BSP = ThreeBSP(THREE);
|
||||
|
||||
export default class ShapesManager extends THREE.Object3D {
|
||||
constructor({ toonShader }) {
|
||||
@ -11,6 +15,9 @@ export default class ShapesManager extends THREE.Object3D {
|
||||
this._meshes = {};
|
||||
this._spaces = {};
|
||||
this.name = 'shapes-manager';
|
||||
|
||||
this._holes = null;
|
||||
|
||||
// this._edges = {};
|
||||
}
|
||||
|
||||
@ -35,35 +42,72 @@ export default class ShapesManager extends THREE.Object3D {
|
||||
}
|
||||
}
|
||||
|
||||
let holesChanged = false;
|
||||
|
||||
// Remove removed shapes
|
||||
if (this._state) {
|
||||
for (const id in this._state.objectsById) {
|
||||
if (!state.objectsById[id]) {
|
||||
if (!this._meshes[id].mesh._shapeData.solid) holesChanged = true;
|
||||
this._handleShapeRemove(id);
|
||||
render = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const id in state.objectsById) {
|
||||
// const shapeData = this._state.objectsById[id];
|
||||
const ids = Object.keys(state.objectsById);
|
||||
const activeShapes = determineActiveShape3d(state);
|
||||
|
||||
for (let i = 0; i < ids.length; i ++) {
|
||||
const id = ids[i];
|
||||
const newShapeData = state.objectsById[id];
|
||||
const active = activeShapes[id];
|
||||
|
||||
if (!SHAPE_TYPE_PROPERTIES[newShapeData.type].D3Visible) continue;
|
||||
// add new shapes
|
||||
if (!this._state || !this._state.objectsById[id]) {
|
||||
this._handleShapeAdded(newShapeData);
|
||||
this._handleShapeAdded(newShapeData, active);
|
||||
render = true;
|
||||
if (!newShapeData.solid) holesChanged = true;
|
||||
} else {
|
||||
const { mesh } = this._meshes[id];
|
||||
if (mesh.update(newShapeData)) {
|
||||
if (mesh.update(newShapeData, active)) {
|
||||
render = true;
|
||||
if (!newShapeData.solid || !this._state.objectsById[id].solid) holesChanged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (holesChanged) {
|
||||
this._holes = null;
|
||||
for (let i = 0; i < ids.length; i ++) {
|
||||
const id = ids[i];
|
||||
if (!state.objectsById[id].solid) {
|
||||
const hole = this._meshes[id].mesh._mesh;
|
||||
const holeGeometry = new THREE.Geometry().fromBufferGeometry(hole.geometry);
|
||||
const holeBSP = new THREE_BSP(holeGeometry);
|
||||
if (!this._holes) {
|
||||
this._holes = holeBSP;
|
||||
} else {
|
||||
this._holes = this._holes.union(holeBSP);
|
||||
}
|
||||
holeGeometry.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < ids.length; i ++) {
|
||||
const id = ids[i];
|
||||
const active = activeShapes[id];
|
||||
if (!active && state.objectsById[id].solid) {
|
||||
const shape = this._meshes[id].mesh;
|
||||
if (shape.updateHoleGeometry(this._holes)) {
|
||||
render = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._state = state;
|
||||
|
||||
return render;
|
||||
}
|
||||
|
||||
@ -77,10 +121,6 @@ export default class ShapesManager extends THREE.Object3D {
|
||||
}
|
||||
}
|
||||
|
||||
getMesh(id) {
|
||||
return this._meshes[id].mesh;
|
||||
}
|
||||
|
||||
_handleShapeRemove(id) {
|
||||
if (this._meshes[id] === undefined) return;
|
||||
const { mesh, space } = this._meshes[id];
|
||||
@ -90,10 +130,10 @@ export default class ShapesManager extends THREE.Object3D {
|
||||
this._spaces[space].remove(mesh);
|
||||
}
|
||||
|
||||
_handleShapeAdded(shapeData) {
|
||||
_handleShapeAdded(shapeData, active) {
|
||||
if (!SHAPE_TYPE_PROPERTIES[shapeData.type].D3Visible) return;
|
||||
const { space } = shapeData;
|
||||
const mesh = new ShapeMesh(shapeData, this._toonShader);
|
||||
const mesh = new ShapeMesh(shapeData, active, this._toonShader);
|
||||
this._meshes[shapeData.UID] = { mesh, space };
|
||||
|
||||
this._spaces[space].add(mesh);
|
||||
|
6
src/d3/transformers/BaseTransformer.js
vendored
6
src/d3/transformers/BaseTransformer.js
vendored
@ -59,8 +59,8 @@ export default class BaseTransformer extends EventObject3D {
|
||||
this.dispatch(actions.d3MultitouchEnd(positions));
|
||||
}
|
||||
select(intersections) {
|
||||
const mesh = intersections.find(({ object }) => object instanceof ShapeMesh);
|
||||
const bed = intersections.find(({ object }) => object.name === 'bed-plane');
|
||||
const mesh = intersections.find(({ object }) => object.isShapeMesh);
|
||||
const bed = intersections.find(({ object }) => object.isBedPlane);
|
||||
|
||||
if (mesh) {
|
||||
this.dispatch(actions.toggleSelect(mesh.object.name));
|
||||
@ -132,7 +132,7 @@ export default class BaseTransformer extends EventObject3D {
|
||||
}
|
||||
updateSpriteScale() {
|
||||
for (const sprite of this.children) {
|
||||
if (!(sprite instanceof SpriteHandle) || !sprite.material.map) continue;
|
||||
if (!(sprite.isUIHandle) || !sprite.material.map) continue;
|
||||
|
||||
const scale = sprite.position.distanceTo(this.camera.getWorldPosition()) / 2000.0;
|
||||
const { width, height } = sprite.material.map.image;
|
||||
|
2
src/d3/transformers/StampTransformer.js
vendored
2
src/d3/transformers/StampTransformer.js
vendored
@ -42,7 +42,7 @@ export default class SculptTransformer extends BaseTransformer {
|
||||
}
|
||||
|
||||
tap(event) {
|
||||
const intersection = event.intersections.find(({ object }) => object instanceof ShapeMesh);
|
||||
const intersection = event.intersections.find(({ object }) => object.isShapeMesh);
|
||||
if (this.hasSelection && intersection) {
|
||||
this.dispatch(actions.stamp(intersection));
|
||||
} else {
|
||||
|
@ -22,6 +22,10 @@ export default function (state, action) {
|
||||
const fill = fillBool ? contextTools.FILL_TOGGLE_FILL : contextTools.FILL_TOGGLE_OUTLINE;
|
||||
menus = select(menus, fill);
|
||||
|
||||
const solidBool = firstSelected && state.objectsById[firstSelected.id].solid;
|
||||
const solid = solidBool ? contextTools.HOLE_TOGGLE_SOLID : contextTools.HOLE_TOGGLE_HOLE;
|
||||
menus = select(menus, solid);
|
||||
|
||||
return update(state, { menus: { $set: menus } });
|
||||
}
|
||||
|
||||
@ -104,6 +108,19 @@ export default function (state, action) {
|
||||
});
|
||||
}
|
||||
|
||||
case contextTools.HOLE_TOGGLE_HOLE:
|
||||
case contextTools.HOLE_TOGGLE_SOLID: {
|
||||
const solid = action.tool === contextTools.HOLE_TOGGLE_SOLID;
|
||||
|
||||
return update(state, {
|
||||
objectsById: state.selection.objects.reduce((updateObject, { id }) => {
|
||||
const { fill } = state.objectsById[id];
|
||||
if (fill) updateObject[id] = { solid: { $set: solid } };
|
||||
return updateObject;
|
||||
}, {})
|
||||
});
|
||||
}
|
||||
|
||||
case contextTools.ALIGN_LEFT:
|
||||
case contextTools.ALIGN_HORIZONTAL:
|
||||
case contextTools.ALIGN_RIGHT:
|
||||
|
@ -69,3 +69,36 @@ function shapeDataToShapeRaw(shapeData) {
|
||||
return new Shape(shapeData);
|
||||
}
|
||||
}
|
||||
|
||||
export const determineActiveShape2d = (state) => {
|
||||
const selectedObjects = state.selection.objects.map(({ id }) => id);
|
||||
|
||||
const activeShapes = {};
|
||||
for (const id in state.objectsById) {
|
||||
activeShapes[id] = state.d2.activeShape === id || selectedObjects.includes(id);
|
||||
}
|
||||
return activeShapes;
|
||||
};
|
||||
|
||||
export const determineActiveShape3d = (state) => {
|
||||
if (!state.d2 || !state.d3) {
|
||||
const activeShapes = {};
|
||||
for (const id in state.objectsById) {
|
||||
activeShapes[id] = false;
|
||||
}
|
||||
return activeShapes;
|
||||
}
|
||||
|
||||
const activeTransformer = state.d2.eraser.active ||
|
||||
(state.d2.transform.active && state.d2.transform.handle !== 'dragselect') ||
|
||||
state.d3.height.active ||
|
||||
state.d3.sculpt.activeHandle !== null ||
|
||||
state.d3.twist.active;
|
||||
|
||||
const selectedObjects = state.selection.objects.map(({ id }) => id);
|
||||
const activeShapes = {};
|
||||
for (const id in state.objectsById) {
|
||||
activeShapes[id] = activeTransformer;
|
||||
}
|
||||
return activeShapes;
|
||||
};
|
||||
|
@ -4,7 +4,7 @@ import * as exportOBJ from '@doodle3d/threejs-export-obj';
|
||||
import * as THREE from 'three';
|
||||
import ThreeBSP from 'three-js-csg';
|
||||
import ClipperShape from '@doodle3d/clipper-js';
|
||||
import ShapeMesh from '../d3/ShapeMesh.js';
|
||||
import ShapesManager from '../d3/ShapesManager.js';
|
||||
import { applyMatrixOnShape, pathToVectorPath } from '../utils/vectorUtils.js';
|
||||
import { shapeToPoints } from '../shape/shapeToPoints.js';
|
||||
import { SHAPE_TYPE_PROPERTIES } from '../constants/shapeTypeProperties.js';
|
||||
@ -17,7 +17,7 @@ const THREE_BSP = ThreeBSP(THREE);
|
||||
const ROTATION_MATRIX = new THREE.Matrix4().makeRotationX(Math.PI / 2);
|
||||
const SCALE = 10.0;
|
||||
|
||||
function createExportGeometry(shapeData, offsetSingleWalls, lineWidth) {
|
||||
function createExportShapeData(shapeData, offsetSingleWalls, lineWidth) {
|
||||
let shapes = shapeToPoints(shapeData).map(({ points, holes }) => {
|
||||
const shape = applyMatrixOnShape([points, ...holes], shapeData.transform);
|
||||
return new ClipperShape(shape, shapeData.fill, true, false);
|
||||
@ -53,58 +53,71 @@ function createExportGeometry(shapeData, offsetSingleWalls, lineWidth) {
|
||||
}))
|
||||
.map(([points, ...holes]) => ({ points, holes }));
|
||||
|
||||
const objectMesh = new ShapeMesh({
|
||||
return {
|
||||
...shapeData,
|
||||
transform: new Matrix(),
|
||||
type: 'EXPORT_SHAPE',
|
||||
fill,
|
||||
shapes
|
||||
});
|
||||
|
||||
return objectMesh;
|
||||
};
|
||||
}
|
||||
|
||||
export function generateExportMesh(state, options = {}) {
|
||||
const {
|
||||
unionGeometry = false,
|
||||
exportLineWidth = LINE_WIDTH,
|
||||
lineWidth = LINE_WIDTH,
|
||||
offsetSingleWalls = true,
|
||||
matrix = ROTATION_MATRIX
|
||||
} = options;
|
||||
|
||||
const materials = [];
|
||||
let exportGeometry;
|
||||
const objectMatrix = new THREE.Matrix4();
|
||||
const exportState = {
|
||||
spaces: state.spaces,
|
||||
objectsById: {}
|
||||
};
|
||||
|
||||
for (const id in state.objectsById) {
|
||||
const shapeData = state.objectsById[id];
|
||||
|
||||
if (!SHAPE_TYPE_PROPERTIES[shapeData.type].D3Visible) continue;
|
||||
|
||||
const { geometry, material } = createExportGeometry(shapeData, offsetSingleWalls || unionGeometry, exportLineWidth);
|
||||
let objectGeometry = new THREE.Geometry().fromBufferGeometry(geometry);
|
||||
objectGeometry.mergeVertices();
|
||||
objectGeometry.applyMatrix(objectMatrix.multiplyMatrices(state.spaces[shapeData.space].matrix, matrix));
|
||||
|
||||
const colorHex = material.color.getHex();
|
||||
let materialIndex = materials.findIndex(exportMaterial => exportMaterial.color.getHex() === colorHex);
|
||||
if (materialIndex === -1) {
|
||||
materialIndex = materials.length;
|
||||
materials.push(material);
|
||||
}
|
||||
|
||||
if (unionGeometry) objectGeometry = new THREE_BSP(objectGeometry, materials.length);
|
||||
|
||||
if (exportGeometry) {
|
||||
if (unionGeometry) {
|
||||
exportGeometry = exportGeometry.union(objectGeometry);
|
||||
} else {
|
||||
exportGeometry = exportGeometry.merge(objectGeometry, undefined, materials.length);
|
||||
}
|
||||
} else {
|
||||
exportGeometry = objectGeometry;
|
||||
}
|
||||
exportState.objectsById[id] = createExportShapeData(state.objectsById[id], offsetSingleWalls || unionGeometry, lineWidth);
|
||||
}
|
||||
|
||||
const shapesManager = new ShapesManager({ toonShader: false });
|
||||
shapesManager.update(exportState);
|
||||
|
||||
const materials = [];
|
||||
const objectMatrix = new THREE.Matrix4();
|
||||
let exportGeometry;
|
||||
shapesManager.traverse(mesh => {
|
||||
const shapeData = exportState.objectsById[mesh.name];
|
||||
if (mesh instanceof THREE.Mesh && shapeData.solid) {
|
||||
const { geometry, material } = mesh;
|
||||
|
||||
console.log('mesh: ', mesh);
|
||||
|
||||
let objectGeometry = geometry.clone();
|
||||
objectGeometry.mergeVertices();
|
||||
objectGeometry.applyMatrix(objectMatrix.multiplyMatrices(state.spaces[shapeData.space].matrix, matrix));
|
||||
|
||||
const colorHex = material.color.getHex();
|
||||
let materialIndex = materials.findIndex(exportMaterial => exportMaterial.color.getHex() === colorHex);
|
||||
if (materialIndex === -1) {
|
||||
materialIndex = materials.length;
|
||||
materials.push(material);
|
||||
}
|
||||
|
||||
if (unionGeometry) objectGeometry = new THREE_BSP(objectGeometry, materialIndex);
|
||||
|
||||
if (unionGeometry) {
|
||||
if (!exportGeometry) {
|
||||
exportGeometry = objectGeometry;
|
||||
} else {
|
||||
exportGeometry = exportGeometry.union(objectGeometry);
|
||||
}
|
||||
} else {
|
||||
if (!exportGeometry) exportGeometry = new THREE.Geometry();
|
||||
exportGeometry.merge(objectGeometry, undefined, materialIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (unionGeometry) {
|
||||
return exportGeometry.toMesh(materials);
|
||||
} else {
|
||||
@ -115,8 +128,6 @@ export function generateExportMesh(state, options = {}) {
|
||||
export async function createFile(state, type, options) {
|
||||
const exportMesh = generateExportMesh(state, options);
|
||||
|
||||
console.log('exportMesh: ', exportMesh);
|
||||
|
||||
switch (type) {
|
||||
case 'json-string': {
|
||||
const object = exportMesh.geometry.toJSON().data;
|
||||
|
@ -41,6 +41,7 @@ export function createTextureFromBlob(blob) {
|
||||
}
|
||||
|
||||
export class SpriteHandle extends THREE.Sprite {
|
||||
isUIHandle = true;
|
||||
constructor(texture, scale) {
|
||||
if (!texture.image) {
|
||||
debug('Error: Texture not loaded');
|
||||
|
Loading…
Reference in New Issue
Block a user