diff --git a/src/ThreeEditor/Simulation/Figures/FigureManager.ts b/src/ThreeEditor/Simulation/Figures/FigureManager.ts index bd0a3e331..95c33771c 100644 --- a/src/ThreeEditor/Simulation/Figures/FigureManager.ts +++ b/src/ThreeEditor/Simulation/Figures/FigureManager.ts @@ -1,5 +1,6 @@ import { Signal } from 'signals'; import * as THREE from 'three'; +import { Object3D } from 'three'; import { SimulationPropertiesType } from '../../../types/SimulationProperties'; import { YaptideEditor } from '../../js/YaptideEditor'; @@ -72,6 +73,8 @@ export class FigureManager private editor: YaptideEditor; private signals: { + figureAdded: Signal; + figureRemoved: Signal; sceneGraphChanged: Signal; }; @@ -99,13 +102,15 @@ export class FigureManager addFigure(figure: BasicFigure) { this.figureContainer.add(figure); this.editor.select(figure); - this.signals.sceneGraphChanged.dispatch(); + this.editor.signals.figureAdded.dispatch(figure); + this.editor.signals.sceneGraphChanged.dispatch(figure); } removeFigure(figure: BasicFigure) { this.figureContainer.remove(figure); this.editor.deselect(); - this.signals.sceneGraphChanged.dispatch(); + this.signals.figureRemoved.dispatch(figure); + this.signals.sceneGraphChanged.dispatch(figure); } getFigureByUuid(uuid: string) { @@ -168,6 +173,18 @@ export class FigureManager this.name = name; this.figureContainer.fromSerialized(figures); + const dispatchFigureAdded = (object: Object3D) => { + this.signals.figureAdded.dispatch(object); + + for (const child of object.children) { + dispatchFigureAdded(child); + } + }; + + for (const figure of this.figures) { + dispatchFigureAdded(figure); + } + return this; } } diff --git a/src/ThreeEditor/Simulation/Zones/BooleanZone.ts b/src/ThreeEditor/Simulation/Zones/BooleanZone.ts index e58d297e7..f805a9386 100644 --- a/src/ThreeEditor/Simulation/Zones/BooleanZone.ts +++ b/src/ThreeEditor/Simulation/Zones/BooleanZone.ts @@ -28,6 +28,7 @@ export class BooleanZone extends SimulationZone { geometryChanged: Signal; sceneGraphChanged: Signal; zoneGeometryChanged: Signal; + zoneAdded: Signal; zoneChanged: Signal; zoneEmpty: Signal; }; @@ -204,6 +205,9 @@ export class BooleanZone extends SimulationZone { this.subscribedObjects = new CounterMap().fromSerialized(objectsJSON); + // Let the clipped view viewports know that the zone exists + this.signals.zoneAdded.dispatch(this); + return this; } diff --git a/src/ThreeEditor/js/YaptideEditor.js b/src/ThreeEditor/js/YaptideEditor.js index da2d33554..54048e6cf 100644 --- a/src/ThreeEditor/js/YaptideEditor.js +++ b/src/ThreeEditor/js/YaptideEditor.js @@ -28,6 +28,7 @@ export const JSON_VERSION = `0.12`; export function YaptideEditor(container) { this.signals = { editorCleared: new Signal(), + simulatorChanged: new Signal(), savingStarted: new Signal(), savingFinished: new Signal(), @@ -652,6 +653,7 @@ YaptideEditor.prototype = { this.config.setKey('project/title', project.title ?? ''); this.config.setKey('project/description', project.description ?? ''); this.contextManager.currentSimulator = project.simulator ?? SimulatorType.COMMON; + this.signals.simulatorChanged.dispatch(); } else console.warn('Project info was not found in JSON data. Skipping part 1 out of 11'); diff --git a/src/ThreeEditor/js/commands/RemoveFigureCommand.ts b/src/ThreeEditor/js/commands/RemoveFigureCommand.ts new file mode 100644 index 000000000..a33543771 --- /dev/null +++ b/src/ThreeEditor/js/commands/RemoveFigureCommand.ts @@ -0,0 +1,41 @@ +import { Object3D } from 'three'; + +import { RemoveObjectCommand } from './RemoveObjectCommand'; + +class RemoveFigureCommand extends RemoveObjectCommand { + execute() { + super.execute(); + + // Dispatch remove commands from bottom to top so each time the innermost object is called + const dispatchRemove = (o: Object3D) => { + if (o.children.length > 0) { + for (const child of o.children) { + dispatchRemove(child); + } + } + + this.editor.signals.figureRemoved.dispatch(o); + }; + + dispatchRemove(this.object); + } + + undo() { + super.undo(); + + // Dispatch re-adding from top to bottom, so children are called after parent has been called + const dispatchReAdd = (o: Object3D) => { + this.editor.signals.figureAdded.dispatch(o); + + if (o.children.length > 0) { + for (const child of o.children) { + dispatchReAdd(child); + } + } + }; + + dispatchReAdd(this.object); + } +} + +export { RemoveFigureCommand }; diff --git a/src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts b/src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts index a05331f7c..ef3b7095d 100644 --- a/src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts +++ b/src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts @@ -3,6 +3,7 @@ import * as THREE from 'three'; import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js'; import { debounce } from 'throttle-debounce'; +import { SimulatorType } from '../../../types/RequestTypes'; import { CSG } from '../../CSG/CSG'; import { YaptideEditor } from '../YaptideEditor'; import { Viewport } from './Viewport'; @@ -18,6 +19,7 @@ export interface ViewportClippedView { scene: THREE.Scene; gui: GUI; planeHelper: THREE.PlaneHelper; + detachSignals: () => void; reset: () => void; configurationToJson: () => ClippedViewConfigurationJson; fromConfigurationJson: (config: ClippedViewConfigurationJson) => void; @@ -155,6 +157,22 @@ export function ViewportClippedViewCSG< editor.signals.sceneGraphChanged.dispatch(); } + function estimateRenderOrder(object3D: T) { + // estimate render order for rendering of slices of Geant4 geometry that overlap + // figures are nested within each other and the innermost slice should be rendered on top + // by having the highest renderOrder value + // it is sufficient to have the value equal to the depth in the hierarchy + let parent = object3D.parent; + let renderOrder = 1; + + while (parent) { + parent = parent.parent; + renderOrder++; + } + + return renderOrder; + } + function updateMeshIntersection(object3D: T) { const crossSectionObject = clippedObjects.getObjectByName(object3D.uuid); @@ -174,6 +192,12 @@ export function ViewportClippedViewCSG< }); crossSectionMesh = CSG.toMesh(objectMesh, object3D.matrix, crossSectionMaterial) as T; + object3D.getWorldPosition(crossSectionMesh.position); + + if (editor.contextManager.currentSimulator === SimulatorType.GEANT4) { + crossSectionMaterial.depthTest = false; + crossSectionMesh.renderOrder = estimateRenderOrder(object3D); + } } else { crossSectionMesh = new THREE.Mesh() as T; } @@ -183,23 +207,35 @@ export function ViewportClippedViewCSG< crossSectionMesh.visible = object3D.visible; clippedObjects.add(crossSectionMesh); + + editor.signals.sceneGraphChanged.dispatch(); } - signalGeometryChanged.add((object3D: T) => { - updateMeshIntersection(object3D); - }); + function updateMeshIntersectionIfExists(object3D: T) { + if (clippedObjects.getObjectByName(object3D.uuid) === undefined) { + // Don't update objects that do not exist + // This is needed for Geant4 which uses objectChanged signal here that also dispatches detector geometry, + // which we don't want here + return; + } - signalGeometryAdded.add((object3D: T) => { updateMeshIntersection(object3D); - }); + } + + signalGeometryChanged.add(updateMeshIntersectionIfExists); + signalGeometryAdded.add(updateMeshIntersection); - signalGeometryRemoved.add((object3D: T) => { + const removeObjectFromMeshIntersection = (object3D: T) => { const crossSectionObject = clippedObjects.getObjectByName(object3D.uuid); if (crossSectionObject) clippedObjects.remove(crossSectionObject); - }); - editor.signals.objectChanged.add((object3D: T) => { + editor.signals.sceneGraphChanged.dispatch(); + }; + + signalGeometryRemoved.add(removeObjectFromMeshIntersection); + + const updateObjectInMeshIntersection = (object3D: T) => { const crossSectionMesh = clippedObjects.getObjectByName(object3D.uuid) as T; if (crossSectionMesh) { @@ -207,10 +243,22 @@ export function ViewportClippedViewCSG< crossSectionMesh.material.needsUpdate = true; crossSectionMesh.visible = object3D.visible; } - }); + + editor.signals.sceneGraphChanged.dispatch(); + }; + + editor.signals.objectChanged.add(updateObjectInMeshIntersection); + + this.detachSignals = () => { + signalGeometryChanged.remove(updateMeshIntersectionIfExists); + signalGeometryAdded.remove(updateMeshIntersection); + signalGeometryRemoved.remove(removeObjectFromMeshIntersection); + editor.signals.objectChanged.remove(updateObjectInMeshIntersection); + }; this.reset = () => { clippedObjects.clear(); + editor.signals.sceneGraphChanged.dispatch(); }; this.configurationToJson = (): ClippedViewConfigurationJson => { diff --git a/src/ThreeEditor/js/viewport/Viewport.js b/src/ThreeEditor/js/viewport/Viewport.js index d64b5664e..af33c5d2a 100644 --- a/src/ThreeEditor/js/viewport/Viewport.js +++ b/src/ThreeEditor/js/viewport/Viewport.js @@ -1,6 +1,7 @@ import * as THREE from 'three'; import { TransformControls } from 'three/examples/jsm/controls/TransformControls'; +import { SimulatorType } from '../../../types/RequestTypes'; import { SetPositionCommand, SetRotationCommand, @@ -116,24 +117,53 @@ export function Viewport( let viewClipPlane = null; - if (clipPlane) { - viewClipPlane = new ViewportClippedViewCSG( - name, - editor, - this, - planeHelpers, - zoneManager.zoneContainer.children, - signals.zoneGeometryChanged, - signals.zoneAdded, - signals.zoneRemoved, - wrapperDiv.dom, - { - clipPlane, - planeHelperColor, - planePosLabel + const setupViewClipPlane = () => { + if (viewClipPlane) { + viewClipPlane.detachSignals(); + viewClipPlane = null; + } + + if (clipPlane) { + if (editor.contextManager.currentSimulator !== SimulatorType.GEANT4) { + viewClipPlane = new ViewportClippedViewCSG( + name, + editor, + this, + planeHelpers, + zoneManager.zoneContainer.children, + signals.zoneGeometryChanged, + signals.zoneAdded, + signals.zoneRemoved, + wrapperDiv.dom, + { + clipPlane, + planeHelperColor, + planePosLabel + } + ); + } else { + viewClipPlane = new ViewportClippedViewCSG( + name, + editor, + this, + planeHelpers, + zoneManager.zoneContainer.children, + signals.objectChanged, + signals.figureAdded, + signals.figureRemoved, + wrapperDiv.dom, + { + clipPlane, + planeHelperColor, + planePosLabel + } + ); } - ); - } + } + }; + + setupViewClipPlane(); + editor.signals.simulatorChanged.add(setupViewClipPlane); let cachedRenderer = null; @@ -154,7 +184,7 @@ export function Viewport( renderer.clear(); - if (clipPlane) renderer.render(viewClipPlane.scene, camera); + if (clipPlane && viewClipPlane) renderer.render(viewClipPlane.scene, camera); else { renderer.render(detectorManager, camera); diff --git a/src/services/StoreService.tsx b/src/services/StoreService.tsx index 569312f3d..b88b045dd 100644 --- a/src/services/StoreService.tsx +++ b/src/services/StoreService.tsx @@ -72,6 +72,7 @@ const Store = ({ children }: GenericContextProviderProps) => { } editor.contextManager.currentSimulator = simulator; + editor.signals.simulatorChanged.dispatch(); if (changingToOrFromGeant4) { editor.clear(); diff --git a/src/util/hooks/useKeyboardEditorControls.ts b/src/util/hooks/useKeyboardEditorControls.ts index 0220ce7b8..8604a89f6 100644 --- a/src/util/hooks/useKeyboardEditorControls.ts +++ b/src/util/hooks/useKeyboardEditorControls.ts @@ -3,6 +3,7 @@ import { Object3D } from 'three'; import { RemoveDetectGeometryCommand } from '../../ThreeEditor/js/commands/RemoveDetectGeometryCommand'; import { RemoveDifferentialModifierCommand } from '../../ThreeEditor/js/commands/RemoveDifferentialModifierCommand'; +import { RemoveFigureCommand } from '../../ThreeEditor/js/commands/RemoveFigureCommand'; import { RemoveFilterCommand } from '../../ThreeEditor/js/commands/RemoveFilterCommand'; import { RemoveObjectCommand } from '../../ThreeEditor/js/commands/RemoveObjectCommand'; import { RemoveQuantityCommand } from '../../ThreeEditor/js/commands/RemoveQuantityCommand'; @@ -10,6 +11,7 @@ import { RemoveZoneCommand } from '../../ThreeEditor/js/commands/RemoveZoneComma import { SetFilterRuleCommand } from '../../ThreeEditor/js/commands/SetFilterRuleCommand'; import { YaptideEditor } from '../../ThreeEditor/js/YaptideEditor'; import { isDetector } from '../../ThreeEditor/Simulation/Detectors/Detector'; +import { isBasicFigure } from '../../ThreeEditor/Simulation/Figures/BasicFigures'; import { isBeam } from '../../ThreeEditor/Simulation/Physics/Beam'; import { isCustomFilter } from '../../ThreeEditor/Simulation/Scoring/CustomFilter'; import { isOutput } from '../../ThreeEditor/Simulation/Scoring/ScoringOutput'; @@ -43,7 +45,9 @@ export const hasVisibleChildren = (object: Object3D) => { }; export const getRemoveCommand = (editor: YaptideEditor, object: Object3D) => { - if (isDetector(object)) { + if (isBasicFigure(object)) { + return new RemoveFigureCommand(editor, object); + } else if (isDetector(object)) { return new RemoveDetectGeometryCommand(editor, object); } else if (isBooleanZone(object)) { return new RemoveZoneCommand(editor, object);