Finish explode tool

This commit is contained in:
Fuhrmann 2025-03-24 15:00:38 +01:00
parent f998ecd519
commit 3e78657aef
5 changed files with 149 additions and 51 deletions

View file

@ -22,24 +22,29 @@ function Toggle({
title, title,
onChange, onChange,
defaultChecked, defaultChecked,
disabled = false,
}: { }: {
title: string; title: string;
onChange: (e: ChangeEvent<HTMLInputElement>) => void; onChange: (e: ChangeEvent<HTMLInputElement>) => void;
defaultChecked?: boolean; defaultChecked?: boolean;
disabled?: boolean;
}) { }) {
return ( return (
<label className="inline-flex items-center cursor-pointer"> <label className="group">
<input <div className="inline-flex items-center cursor-pointer group-has-[input:disabled]:cursor-default">
type="checkbox" <input
value="" type="checkbox"
className="sr-only peer" value=""
onChange={onChange} className="sr-only peer"
defaultChecked={defaultChecked ? true : false} onChange={onChange}
/> defaultChecked={defaultChecked ? true : false}
<div className="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-gray-300 rounded-full peer dark:bg-gray-400 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-400 peer-checked:bg-blue-600 dark:peer-checked:bg-blue-400"></div> disabled={disabled}
<span className="ms-3 text-xs xl:text-sm font-medium text-gray-500 dark:text-gray-400"> />
{title} <div className="relative w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-gray-300 rounded-full peer dark:bg-gray-400 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-400 peer-checked:bg-blue-600 dark:peer-checked:bg-blue-400"></div>
</span> <span className="ms-3 text-xs xl:text-sm font-medium text-gray-500 dark:text-gray-400 group-has-[input:disabled]:text-gray-200">
{title}
</span>
</div>
</label> </label>
); );
} }
@ -133,6 +138,7 @@ export function Form() {
const accordionRef0 = useRef<AccordionRef>(null); const accordionRef0 = useRef<AccordionRef>(null);
const [emptyProfile, setEmptyProfile] = useState<boolean>(false); const [emptyProfile, setEmptyProfile] = useState<boolean>(false);
const [exploded, setExploded] = useState<boolean>(false);
const { sceneView } = useContext(SceneViewContext) as SceneViewContextType; const { sceneView } = useContext(SceneViewContext) as SceneViewContextType;
function handleChange() { function handleChange() {
@ -207,6 +213,18 @@ export function Form() {
sceneView.exportOBJ(); sceneView.exportOBJ();
} }
function handleExplode(e: ChangeEvent<HTMLInputElement>) {
if (!sceneView) return;
if (e.target.checked) {
sceneView.explode(true);
// setExploded(true);
} else {
sceneView.explode(false);
// setExploded(false);
}
}
return ( return (
<div className="w-full max-h-full min-h-0 flex flex-col gap-2 dark:bg-gray-700"> <div className="w-full max-h-full min-h-0 flex flex-col gap-2 dark:bg-gray-700">
<div className="w-full flex justify-end"> <div className="w-full flex justify-end">
@ -218,8 +236,16 @@ export function Form() {
</button> </button>
</div> </div>
<div className="border border-gray-200 dark:border-gray-400 rounded grid grid-cols-2 gap-y-2 p-2"> <div className="border border-gray-200 dark:border-gray-400 rounded grid grid-cols-2 gap-y-2 p-2">
<Toggle title="Slicing Box" onChange={handleChange} /> <Toggle
<Toggle title="Virtual Profile" onChange={handleDrilling} /> title="Slicing Box"
onChange={handleChange}
disabled={exploded ? true : false}
/>
<Toggle
title="Virtual Profile"
onChange={handleDrilling}
disabled={exploded ? true : false}
/>
<Toggle title="Coordinate Grid" onChange={handleChangeCG} /> <Toggle title="Coordinate Grid" onChange={handleChangeCG} />
<Toggle title="Wireframe" onChange={handleChangeWireframe} /> <Toggle title="Wireframe" onChange={handleChangeWireframe} />
<Toggle <Toggle
@ -227,6 +253,7 @@ export function Form() {
onChange={handleChangeTopography} onChange={handleChangeTopography}
defaultChecked defaultChecked
/> />
<Toggle title="Explode" onChange={handleExplode} />
</div> </div>
<div className="px-2 pt-2 border border-gray-200 dark:border-gray-400 rounded"> <div className="px-2 pt-2 border border-gray-200 dark:border-gray-400 rounded">
<RangeSlider></RangeSlider> <RangeSlider></RangeSlider>

View file

@ -1,15 +1,17 @@
import { import {
Camera,
Group, Group,
Material,
Mesh, Mesh,
MeshBasicMaterial, MeshBasicMaterial,
MeshStandardMaterial, MeshStandardMaterial,
PerspectiveCamera,
Plane, Plane,
Raycaster, Raycaster,
Scene, Scene,
SphereGeometry, SphereGeometry,
Vector2, Vector2,
Vector3, Vector3,
WebGLRenderer,
} from "three"; } from "three";
import { buildMeshes } from "./utils/build-meshes"; import { buildMeshes } from "./utils/build-meshes";
import { Extent, buildScene } from "./utils/build-scene"; import { Extent, buildScene } from "./utils/build-scene";
@ -40,7 +42,7 @@ export class SceneView extends EventTarget {
private _scene: Scene; private _scene: Scene;
private _dragControls: DragControls; private _dragControls: DragControls;
private _model: Group; private _model: Group;
private _camera: Camera; private _camera: PerspectiveCamera;
private _container: HTMLElement; private _container: HTMLElement;
private _raycaster: Raycaster; private _raycaster: Raycaster;
private _extent: Extent; private _extent: Extent;
@ -50,15 +52,17 @@ export class SceneView extends EventTarget {
private static _DRAG_THRESHOLD = 5; private static _DRAG_THRESHOLD = 5;
private _callback: EventListenerOrEventListenerObject | null = null; private _callback: EventListenerOrEventListenerObject | null = null;
private _orbitControls: OrbitControls; private _orbitControls: OrbitControls;
private _renderer: WebGLRenderer;
constructor( constructor(
scene: Scene, scene: Scene,
model: Group, model: Group,
dragControls: DragControls, dragControls: DragControls,
camera: Camera, camera: PerspectiveCamera,
container: HTMLElement, container: HTMLElement,
extent: Extent, extent: Extent,
orbitControls: OrbitControls orbitControls: OrbitControls,
renderer: WebGLRenderer
) { ) {
super(); super();
this._scene = scene; this._scene = scene;
@ -69,13 +73,12 @@ export class SceneView extends EventTarget {
this._raycaster = new Raycaster(); this._raycaster = new Raycaster();
this._extent = extent; this._extent = extent;
this._orbitControls = orbitControls; this._orbitControls = orbitControls;
this._renderer = renderer;
} }
static async create(container: HTMLElement, modelId: string) { static async create(container: HTMLElement, modelId: string) {
const { scene, model, dragControls, camera, extent, controls } = await init( const { scene, model, dragControls, camera, extent, controls, renderer } =
container, await init(container, modelId);
modelId
);
return new SceneView( return new SceneView(
scene, scene,
@ -84,7 +87,8 @@ export class SceneView extends EventTarget {
camera, camera,
container, container,
extent, extent,
controls controls,
renderer
); );
} }
@ -311,7 +315,7 @@ export class SceneView extends EventTarget {
} }
// Function to export the group as an OBJ file // Function to export the group as an OBJ file
public exportOBJ() { exportOBJ() {
const exporter = new OBJExporter(); const exporter = new OBJExporter();
const objString = exporter.parse(this._model); const objString = exporter.parse(this._model);
@ -326,9 +330,70 @@ export class SceneView extends EventTarget {
} }
// Reset view to initial extent // Reset view to initial extent
public resetView() { resetView() {
this._orbitControls.reset(); this._orbitControls.reset();
} }
explode(explode: boolean) {
const DISPLACEMENT = 2000;
for (let i = 1; i < this._model.children.length; i++) {
const mesh = this._model.children[i];
if (explode) {
const displacement =
(this._model.children.length - i - 1) * DISPLACEMENT;
mesh.userData.originalPosition = mesh.position.clone();
mesh.translateZ(displacement);
if (i === 1) {
this._model.userData.zmax = this._extent.zmax;
this._extent.zmax += displacement;
}
} else {
if (mesh.userData.originalPosition) {
mesh.position.copy(mesh.userData.originalPosition);
}
}
}
// Reset extent
if (!explode && this._model.userData.zmax) {
this._extent.zmax = this._model.userData.zmax;
}
const box = this._scene.getObjectByName("clipping-box");
let visible = false;
if (box) {
visible = box.visible;
this._scene.remove(box);
}
const { planes, dragControls } = buildClippingplanes(
this._renderer,
this._camera,
this._orbitControls,
this._extent,
this._model.children as Mesh[],
this._scene,
visible
);
this._dragControls = dragControls;
// Add clipping planes to the meshes
for (const mesh of this._model.children) {
((mesh as Mesh).material as Material).clippingPlanes = planes;
}
// Remove existing cap meshes
for (const o in Orientation) {
const capMeshGroupName = `cap-mesh-group-${o}`;
let capMeshGroup = this._scene.getObjectByName(capMeshGroupName);
while (capMeshGroup) {
this._scene.remove(capMeshGroup);
capMeshGroup = this._scene.getObjectByName(capMeshGroupName);
}
}
}
} }
async function init(container: HTMLElement, modelId = MODEL_ID) { async function init(container: HTMLElement, modelId = MODEL_ID) {
@ -358,13 +423,15 @@ async function init(container: HTMLElement, modelId = MODEL_ID) {
scene.add(model); scene.add(model);
// Build the clipping planes and add them to the scene // Build the clipping planes and add them to the scene
const visible = false;
const { planes, dragControls } = buildClippingplanes( const { planes, dragControls } = buildClippingplanes(
renderer, renderer,
camera, camera,
controls, controls,
extent, extent,
meshes, meshes,
scene scene,
visible
); );
// Add clipping planes to the meshes // Add clipping planes to the meshes
@ -404,5 +471,5 @@ async function init(container: HTMLElement, modelId = MODEL_ID) {
map.name = "topography"; map.name = "topography";
scene.add(map); scene.add(map);
return { scene, model, dragControls, camera, extent, controls }; return { scene, model, dragControls, camera, extent, controls, renderer };
} }

View file

@ -23,9 +23,9 @@ import { Extent } from "./build-scene";
import earcut from "earcut"; import earcut from "earcut";
export enum Orientation { export enum Orientation {
X = "x", X = "X",
Y = "y", Y = "Y",
Z = "z", Z = "Z",
} }
type PlaneMesh = Mesh<PlaneGeometry, MeshBasicMaterial, Object3DEventMap>; type PlaneMesh = Mesh<PlaneGeometry, MeshBasicMaterial, Object3DEventMap>;
@ -47,7 +47,8 @@ export function buildClippingplanes(
orbitControls: OrbitControls, orbitControls: OrbitControls,
extent: Extent, extent: Extent,
meshes: Mesh[], meshes: Mesh[],
scene: Scene scene: Scene,
visible: boolean
) { ) {
const planesData = [ const planesData = [
{ {
@ -166,7 +167,7 @@ export function buildClippingplanes(
const clippingBox = new Group(); const clippingBox = new Group();
clippingBox.add(planeMeshGroup, edgeMeshGroup); clippingBox.add(planeMeshGroup, edgeMeshGroup);
clippingBox.name = "clipping-box"; clippingBox.name = "clipping-box";
clippingBox.visible = false; clippingBox.visible = visible;
scene.add(clippingBox); scene.add(clippingBox);
// Enable DragControls for the clipping planes // Enable DragControls for the clipping planes
@ -176,7 +177,7 @@ export function buildClippingplanes(
renderer.domElement renderer.domElement
); );
dragControls.enabled = false; dragControls.enabled = visible;
dragControls.addEventListener("dragstart", () => { dragControls.addEventListener("dragstart", () => {
// Disable OrbitControls when dragging starts // Disable OrbitControls when dragging starts
@ -469,8 +470,6 @@ function generateCapMeshes(
// Iterate over the list of geologic meshes // Iterate over the list of geologic meshes
for (const mesh of meshes) { for (const mesh of meshes) {
// Slice visible meshes only
const position = mesh.geometry.attributes.position.array; const position = mesh.geometry.attributes.position.array;
const indices = mesh.geometry.index ? mesh.geometry.index.array : null; const indices = mesh.geometry.index ? mesh.geometry.index.array : null;
const edges: Array<[Vector3, Vector3]> = []; const edges: Array<[Vector3, Vector3]> = [];
@ -484,9 +483,22 @@ function generateCapMeshes(
const i2 = indices ? indices[i + 1] * 3 : (i + 1) * 3; const i2 = indices ? indices[i + 1] * 3 : (i + 1) * 3;
const i3 = indices ? indices[i + 2] * 3 : (i + 2) * 3; const i3 = indices ? indices[i + 2] * 3 : (i + 2) * 3;
const v1 = new Vector3(position[i1], position[i1 + 1], position[i1 + 2]); // Account for local translation of the mesh to its original geometry
const v2 = new Vector3(position[i2], position[i2 + 1], position[i2 + 2]); const v1 = new Vector3(
const v3 = new Vector3(position[i3], position[i3 + 1], position[i3 + 2]); position[i1],
position[i1 + 1],
position[i1 + 2] + mesh.position.z
);
const v2 = new Vector3(
position[i2],
position[i2 + 1],
position[i2 + 2] + mesh.position.z
);
const v3 = new Vector3(
position[i3],
position[i3 + 1],
position[i3 + 2] + mesh.position.z
);
// Check if the triangle is cut by the plane // Check if the triangle is cut by the plane
const d1 = plane.distanceToPoint(v1); const d1 = plane.distanceToPoint(v1);
@ -516,7 +528,7 @@ function generateCapMeshes(
color: (mesh.material as MeshStandardMaterial).color, color: (mesh.material as MeshStandardMaterial).color,
side: DoubleSide, side: DoubleSide,
metalness: 0.0, metalness: 0.0,
roughness: 0.75, roughness: 1.0,
flatShading: true, flatShading: true,
polygonOffset: true, polygonOffset: true,
polygonOffsetFactor: offset, polygonOffsetFactor: offset,

View file

@ -52,15 +52,11 @@ async function buildMesh(layerData: MappedFeature) {
const indices = new BufferAttribute(indexArray, 1); const indices = new BufferAttribute(indexArray, 1);
geometry.setIndex(indices); geometry.setIndex(indices);
geometry.scale(1, 1, 1);
geometry.computeBoundingSphere();
geometry.computeVertexNormals();
geometry.computeBoundingBox();
const material = new MeshStandardMaterial({ const material = new MeshStandardMaterial({
color: color, color: color,
metalness: 0.0, metalness: 0.0,
roughness: 0.75, roughness: 1.0,
flatShading: true, flatShading: true,
side: DoubleSide, side: DoubleSide,
wireframe: false, wireframe: false,

View file

@ -13,6 +13,7 @@ import {
SpriteMaterial, SpriteMaterial,
Sprite, Sprite,
Euler, Euler,
Color,
} from "three"; } from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js"; import { OrbitControls } from "three/addons/controls/OrbitControls.js";
@ -41,12 +42,7 @@ export function buildScene(container: HTMLElement, extent: Extent) {
const width = container.clientWidth; const width = container.clientWidth;
const height = container.clientHeight; const height = container.clientHeight;
camera = new PerspectiveCamera( camera = new PerspectiveCamera(50, width / height, 10, maxSize * 20);
50,
width / height,
maxSize * 0.1,
maxSize * 25
);
camera.position.set(center.x, center.y - 200000, extent.zmax + 100000); camera.position.set(center.x, center.y - 200000, extent.zmax + 100000);
camera.up.set(0, 0, 1); camera.up.set(0, 0, 1);
@ -54,7 +50,6 @@ export function buildScene(container: HTMLElement, extent: Extent) {
// Initialize the renderer // Initialize the renderer
renderer = new WebGLRenderer({ renderer = new WebGLRenderer({
alpha: true,
logarithmicDepthBuffer: true, logarithmicDepthBuffer: true,
}); });
@ -80,6 +75,7 @@ export function buildScene(container: HTMLElement, extent: Extent) {
// Set wireframe to false on initial load // Set wireframe to false on initial load
scene = new Scene(); scene = new Scene();
scene.userData.wireframe = false; scene.userData.wireframe = false;
scene.background = new Color(0xbfd1e5);
// Add lights to the scene // Add lights to the scene
buildDefaultLights(scene, extent); buildDefaultLights(scene, extent);