Merge branch 'feature/cutout'

This commit is contained in:
casperlamboo 2017-11-22 16:41:15 +01:00
commit 171c0445e8
23 changed files with 344 additions and 105 deletions

BIN
img/contextmenu/btnHole.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
img/holepatern.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -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
View File

@ -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",

View File

@ -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",

View File

@ -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;
}

View File

@ -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);

View File

@ -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');

View File

@ -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';

View File

@ -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;

View File

@ -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,

View File

@ -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 = {

View File

@ -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();

View File

@ -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
View File

@ -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;
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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 {

View File

@ -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:

View File

@ -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;
};

View File

@ -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;

View File

@ -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');