diff --git a/img/contextmenu/btnHole.png b/img/contextmenu/btnHole.png new file mode 100644 index 0000000..5143479 Binary files /dev/null and b/img/contextmenu/btnHole.png differ diff --git a/img/contextmenu/btnSolid.png b/img/contextmenu/btnSolid.png new file mode 100644 index 0000000..47e33db Binary files /dev/null and b/img/contextmenu/btnSolid.png differ diff --git a/img/holepatern.png b/img/holepatern.png new file mode 100644 index 0000000..5ec8385 Binary files /dev/null and b/img/holepatern.png differ diff --git a/index.js b/index.js index 2e473e0..e51158f 100644 --- a/index.js +++ b/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'; diff --git a/package-lock.json b/package-lock.json index d31e29e..57b954b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 3bec2e4..cab9402 100755 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/D2Panel.js b/src/components/D2Panel.js index 9353f2c..2bc9012 100644 --- a/src/components/D2Panel.js +++ b/src/components/D2Panel.js @@ -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; } diff --git a/src/components/D3Panel.js b/src/components/D3Panel.js index 8e850fc..17e66e9 100644 --- a/src/components/D3Panel.js +++ b/src/components/D3Panel.js @@ -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); diff --git a/src/components/SketcherToolbars.js b/src/components/SketcherToolbars.js index 1733c18..d03936d 100644 --- a/src/components/SketcherToolbars.js +++ b/src/components/SketcherToolbars.js @@ -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'); diff --git a/src/constants/contextTools.js b/src/constants/contextTools.js index a42703d..c846252 100644 --- a/src/constants/contextTools.js +++ b/src/constants/contextTools.js @@ -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'; diff --git a/src/constants/d3Constants.js b/src/constants/d3Constants.js index 2db4ab6..71e749e 100644 --- a/src/constants/d3Constants.js +++ b/src/constants/d3Constants.js @@ -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; diff --git a/src/constants/menu.js b/src/constants/menu.js index 84ffcdf..96d3d6e 100644 --- a/src/constants/menu.js +++ b/src/constants/menu.js @@ -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, diff --git a/src/constants/shapeTypeProperties.js b/src/constants/shapeTypeProperties.js index 8bad5fa..4e2f0a4 100644 --- a/src/constants/shapeTypeProperties.js +++ b/src/constants/shapeTypeProperties.js @@ -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 = { diff --git a/src/d2/Shape.js b/src/d2/Shape.js index 868ed4f..ae092d9 100644 --- a/src/d2/Shape.js +++ b/src/d2/Shape.js @@ -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(); diff --git a/src/d2/ShapesManager.js b/src/d2/ShapesManager.js index dfd100e..b022184 100644 --- a/src/d2/ShapesManager.js +++ b/src/d2/ShapesManager.js @@ -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 { diff --git a/src/d3/ShapeMesh.js b/src/d3/ShapeMesh.js index 96b8eea..5e51566 100644 --- a/src/d3/ShapeMesh.js +++ b/src/d3/ShapeMesh.js @@ -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; } } diff --git a/src/d3/ShapesManager.js b/src/d3/ShapesManager.js index 062eb6e..7427c5b 100644 --- a/src/d3/ShapesManager.js +++ b/src/d3/ShapesManager.js @@ -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); diff --git a/src/d3/transformers/BaseTransformer.js b/src/d3/transformers/BaseTransformer.js index c63b8b6..e4d0415 100644 --- a/src/d3/transformers/BaseTransformer.js +++ b/src/d3/transformers/BaseTransformer.js @@ -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; diff --git a/src/d3/transformers/StampTransformer.js b/src/d3/transformers/StampTransformer.js index ada8e8a..f9abb0e 100644 --- a/src/d3/transformers/StampTransformer.js +++ b/src/d3/transformers/StampTransformer.js @@ -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 { diff --git a/src/reducer/contextReducer.js b/src/reducer/contextReducer.js index 0f7d88e..08f73e9 100644 --- a/src/reducer/contextReducer.js +++ b/src/reducer/contextReducer.js @@ -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: diff --git a/src/shape/shapeDataUtils.js b/src/shape/shapeDataUtils.js index 88a9ea5..a7e6274 100644 --- a/src/shape/shapeDataUtils.js +++ b/src/shape/shapeDataUtils.js @@ -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; +}; diff --git a/src/utils/exportUtils.js b/src/utils/exportUtils.js index 9dfe833..cbdeb6e 100644 --- a/src/utils/exportUtils.js +++ b/src/utils/exportUtils.js @@ -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; diff --git a/src/utils/threeUtils.js b/src/utils/threeUtils.js index 86d5e90..122f484 100644 --- a/src/utils/threeUtils.js +++ b/src/utils/threeUtils.js @@ -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');