Skip to content

Commit

Permalink
Add initial Draw3D tool
Browse files Browse the repository at this point in the history
  • Loading branch information
manisandro committed Jan 9, 2025
1 parent f625903 commit c16d624
Show file tree
Hide file tree
Showing 33 changed files with 1,321 additions and 3 deletions.
171 changes: 171 additions & 0 deletions components/map3d/Draw3D.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* Copyright 2025 Sourcepole AG
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

import React from 'react';

import PropTypes from 'prop-types';
import {Group} from 'three';
import {v4 as uuidv4} from 'uuid';

import LocaleUtils from '../../utils/LocaleUtils';
import Icon from '../Icon';
import TaskBar from '../TaskBar';
import ButtonBar from '../widgets/ButtonBar';
import CreateTool3D from './drawtool/CreateTool3D';
import EditTool3D from './drawtool/EditTool3D';

import './style/Draw3D.css';

export default class Draw3D extends React.Component {
static propTypes = {
sceneContext: PropTypes.object
};
state = {
action: null,
baseSize: 10,
color: [255, 105, 0, 1],
geomType: null,
drawGroupId: "",
selectedObject: null
};
onShow = () => {
// Ensure a draw group is present
const drawGroup = Object.entries(this.props.sceneContext.sceneObjects).find(([key, options]) => {
return options.drawGroup === true;
});
if (drawGroup === undefined) {
this.addDrawGroup(LocaleUtils.tr("draw3d.drawings"));
} else {
this.setState({drawGroupId: drawGroup[0]});
}
this.setState({action: 'Pick'});
};
onHide = () => {
// Remove empty draw groups
Object.entries(this.props.sceneContext.sceneObjects).filter(([objectId, options]) => {
return options.drawGroup === true;
}).forEach(([objectId, options]) => {
const object = this.props.sceneContext.getSceneObject(objectId);
if (object.children.length === 0) {
this.props.sceneContext.removeSceneObject(objectId);
}
});
};
createDrawGroup = () => {
const message = LocaleUtils.tr("draw3d.newgroupprompt");
// eslint-disable-next-line
const name = prompt(message);
if (name) {
this.addDrawGroup(name);
}
};
addDrawGroup = (name) => {
const objectId = uuidv4();
const options = {
drawGroup: true,
layertree: true,
title: name
};
this.props.sceneContext.addSceneObject(objectId, new Group(), options);
this.setState({drawGroupId: objectId});
};
renderBody = () => {
const activeButton = this.state.action === "Create" ? this.state.geomType : this.state.action;
const drawButtons = [
{key: "Cuboid", tooltip: LocaleUtils.tr("draw3d.cuboid"), icon: "cuboid", data: {action: "Create", geomType: "Cuboid"}},
{key: "Wedge", tooltip: LocaleUtils.tr("draw3d.wedge"), icon: "wedge", data: {action: "Create", geomType: "Wedge"}},
{key: "Cylinder", tooltip: LocaleUtils.tr("draw3d.cylinder"), icon: "cylinder", data: {action: "Create", geomType: "Cylinder"}},
[
{key: "Pyramid", tooltip: LocaleUtils.tr("draw3d.pyramid"), icon: "pyramid", data: {action: "Create", geomType: "Pyramid"}},
{key: "Sphere", tooltip: LocaleUtils.tr("draw3d.sphere"), icon: "sphere", data: {action: "Create", geomType: "Sphere"}},
{key: "Cone", tooltip: LocaleUtils.tr("draw3d.cone"), icon: "cone", data: {action: "Create", geomType: "Cone"}}
]
];
const editButtons = [
{key: "Pick", tooltip: LocaleUtils.tr("draw3d.pick"), icon: "nodetool", data: {action: "Pick", geomType: null}},
{key: "Delete", tooltip: LocaleUtils.tr("draw3d.delete"), icon: "trash", data: {action: "Delete", geomType: null}, disabled: !this.state.selectedObject}
];
const drawGroups = Object.entries(this.props.sceneContext.sceneObjects).filter(([key, entry]) => entry.drawGroup === true);
return (
<div>
<div className="redlining-buttongroups">
<div className="redlining-group">
<div>{LocaleUtils.tr("redlining.layer")}</div>
<div className="controlgroup">
<select onChange={(ev) => this.setState({drawGroupId: ev.target.value})} value={this.state.drawGroupId}>
{drawGroups.map(([objectId, options]) => (
<option key={objectId} value={objectId}>{options.title}</option>
))}
</select>
<button className="button" onClick={this.createDrawGroup}><Icon icon="plus" /></button>
</div>
</div>
<div className="redlining-group">
<div>{LocaleUtils.tr("redlining.draw")}</div>
<span>
<ButtonBar active={activeButton} buttons={drawButtons} onClick={(key, data) => this.actionChanged(data)} />
</span>
</div>
<div className="redlining-group">
<div>{LocaleUtils.tr("redlining.edit")}</div>
<ButtonBar active={activeButton} buttons={editButtons} onClick={(key, data) => this.actionChanged(data)} />
</div>
</div>
{this.renderControl()}
</div>
);
};
renderControl = () => {
if (this.state.action === "Create") {
return (
<CreateTool3D
baseSize={this.state.baseSize} baseSizeChanged={baseSize => this.setState({baseSize})}
color={this.state.color} colorChanged={color => this.setState({color})}
drawGroupId={this.state.drawGroupId} geomType={this.state.geomType}
objectCreated={this.objectCreated} sceneContext={this.props.sceneContext} />
);
} else if (this.state.action === "Pick") {
return (
<EditTool3D
color={this.state.color} colorChanged={color => this.setState({color})}
drawGroupId={this.state.drawGroupId} objectPicked={this.objectPicked}
sceneContext={this.props.sceneContext}
selectedObject={this.state.selectedObject} />
);
}
return null;
};
render() {
return (
<TaskBar onHide={this.onHide} onShow={this.onShow} task="Draw3D">
{() => ({
body: this.renderBody()
})}
</TaskBar>
);
}
actionChanged = (data) => {
if (data.action === "Delete") {
this.deleteSelectedObject();
this.setState({action: 'Pick', geomType: null, selectedObject: null});
} else {
this.setState({action: data.action, geomType: data.geomType, selectedObject: null});
}
};
deleteSelectedObject = () => {
this.props.sceneContext.getSceneObject(this.state.drawGroupId).remove(this.state.selectedObject);
this.state.selectedObject.traverse(obj => obj.dispose?.());
this.props.sceneContext.scene.notifyChange();
};
objectCreated = (object) => {
this.setState({action: 'Pick', geomType: null, selectedObject: object});
};
objectPicked = (object) => {
this.setState({selectedObject: object});
};
}
2 changes: 2 additions & 0 deletions components/map3d/Map3D.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import MiscUtils from '../../utils/MiscUtils';
import Icon from '../Icon';
import BottomBar3D from './BottomBar3D';
import Compare3D from './Compare3D';
import Draw3D from './Draw3D';
import LayerTree3D from './LayerTree3D';
import Map3DLight from './Map3DLight';
import Measure3D from './Measure3D';
Expand Down Expand Up @@ -362,6 +363,7 @@ class Map3D extends React.Component {
<Map3DLight sceneContext={this.state.sceneContext} />
<Measure3D sceneContext={this.state.sceneContext} />
<Compare3D sceneContext={this.state.sceneContext} />
<Draw3D sceneContext={this.state.sceneContext} />
</UnloadWrapper>
) : null}
</div>
Expand Down
3 changes: 2 additions & 1 deletion components/map3d/TopBar3D.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class TopBar3D extends React.Component {
render() {
const menuItems = [
{key: "LayerTree3D", icon: "layers"},
{key: "Draw3D", icon: "draw"},
{key: "Measure3D", icon: "measure"},
{key: "Compare3D", icon: "compare"},
{key: "DateTime3D", icon: "clock"}
Expand All @@ -57,6 +58,6 @@ class TopBar3D extends React.Component {
};
}

export default connect(() => {}, {
export default connect(() => ({}), {
setTopbarHeight: setTopbarHeight
})(TopBar3D);
122 changes: 122 additions & 0 deletions components/map3d/drawtool/CreateTool3D.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* Copyright 2025 Sourcepole AG
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/

import React from 'react';

import {default as GiroShape} from '@giro3d/giro3d/entities/Shape';
import PropTypes from 'prop-types';
import {BoxGeometry, Color, ConeGeometry, CylinderGeometry, ExtrudeGeometry, Mesh, MeshStandardMaterial, Shape, SphereGeometry} from 'three';

import LocaleUtils from '../../../utils/LocaleUtils';
import Icon from '../../Icon';
import ColorButton from '../../widgets/ColorButton';
import NumberInput from '../../widgets/NumberInput';


export default class CreateTool3D extends React.Component {
static propTypes = {
baseSize: PropTypes.number,
baseSizeChanged: PropTypes.func,
color: PropTypes.array,
colorChanged: PropTypes.func,
drawGroupId: PropTypes.string,
geomType: PropTypes.string,
objectCreated: PropTypes.func,
sceneContext: PropTypes.object
};
componentDidMount() {
this.drawCursor = new GiroShape({
showVertices: true
});
this.props.sceneContext.addSceneObject("__drawCursor", this.drawCursor);
const renderer = this.props.sceneContext.scene.renderer;
renderer.domElement.addEventListener("pointermove", this.moveDrawCursor);
renderer.domElement.addEventListener("pointerdown", this.drawShapeOnRelease);
}
componentWillUnmount() {
this.props.sceneContext.removeSceneObject("__drawCursor");
const renderer = this.props.sceneContext.scene.renderer;
renderer.domElement.removeEventListener("pointermove", this.moveDrawCursor);
renderer.domElement.removeEventListener("pointerdown", this.drawShapeOnRelease);
}
render() {
return (
<div className="redlining-controlsbar">
<span>
<Icon className="redlining-control-icon" icon="pen" size="large" />
<ColorButton alpha={false} color={this.props.color} onColorChanged={this.props.colorChanged} />
</span>
<span>
<span>{LocaleUtils.tr("redlining.size")}:&nbsp;</span>
<NumberInput max={99} min={1} mobile
onChange={this.props.baseSizeChanged}
value={this.props.baseSize}/>
</span>
</div>
);
}
moveDrawCursor = (ev) => {
const rect = ev.target.getBoundingClientRect();
const x = ((ev.clientX - rect.left) / rect.width) * 2 - 1;
const y = -((ev.clientY - rect.top) / rect.height) * 2 + 1;
const intersection = this.props.sceneContext.getSceneIntersection(x, y);
if (intersection) {
const p = intersection.point;
this.drawCursor.setPoints([p]);
} else {
this.drawCursor.setPoints([]);
}
this.props.sceneContext.scene.notifyChange();
};
drawShapeOnRelease = (ev) => {
if (ev.button === 0) {
const renderer = this.props.sceneContext.scene.renderer;
renderer.domElement.addEventListener("pointerup", this.drawShape, {once: true});
renderer.domElement.addEventListener("pointermove", () => {
renderer.domElement.removeEventListener("pointerup", this.drawShape);
});
}
};
drawShape = () => {
const drawGroup = this.props.sceneContext.getSceneObject(this.props.drawGroupId);
if (this.drawCursor.points.length === 0 || !drawGroup) {
return;
}
let geometry = null;
const s = this.props.baseSize;
if (this.props.geomType === "Cuboid") {
geometry = new BoxGeometry( s, s, s );
} else if (this.props.geomType === "Wedge") {
const shape = new Shape();
shape.moveTo(-0.5 * s, -0.5 * s);
shape.lineTo(0.5 * s, -0.5 * s);
shape.lineTo(0.5 * s, 0.5 * s);
shape.lineTo(-0.5 * s, -0.5 * s);
geometry = new ExtrudeGeometry(shape, {depth: s});
} else if (this.props.geomType === "Cylinder") {
geometry = new CylinderGeometry( 0.5 * s, 0.5 * s, s );
} else if (this.props.geomType === "Pyramid") {
geometry = new ConeGeometry( 0.5 * s * Math.sqrt(2), s, 4, 1, false, Math.PI / 4);
} else if (this.props.geomType === "Sphere") {
geometry = new SphereGeometry( 0.5 * s );
} else if (this.props.geomType === "Cone") {
geometry = new ConeGeometry( 0.5 * s, s );
}
if (geometry) {
geometry.rotateX(Math.PI / 2); // Z-up
const material = new MeshStandardMaterial({color: new Color(...this.props.color.map(c => c / 255))});
const mesh = new Mesh( geometry, material);
drawGroup.add(mesh);
mesh.position.copy(this.drawCursor.points[0]);
mesh.position.z += 0.5 * s;
mesh.updateMatrixWorld();
this.props.sceneContext.scene.notifyChange();
this.props.objectCreated(mesh);
}
};
}
Loading

0 comments on commit c16d624

Please sign in to comment.