From 1ee880f54efc0dca70a311f580309dc9ad1d9a7f Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Thu, 11 Dec 2025 19:41:01 +0100 Subject: [PATCH 1/5] Add support for Geant4 geometry in Viewport.ClippedViewCSG --- .../Simulation/Figures/FigureManager.ts | 13 +++- .../Simulation/Zones/BooleanZone.ts | 4 ++ src/ThreeEditor/js/YaptideEditor.js | 2 + .../js/viewport/Viewport.ClippedViewCSG.ts | 65 +++++++++++++++--- src/ThreeEditor/js/viewport/Viewport.js | 66 ++++++++++++++----- src/services/StoreService.tsx | 1 + 6 files changed, 122 insertions(+), 29 deletions(-) diff --git a/src/ThreeEditor/Simulation/Figures/FigureManager.ts b/src/ThreeEditor/Simulation/Figures/FigureManager.ts index bd0a3e331..544156d6f 100644 --- a/src/ThreeEditor/Simulation/Figures/FigureManager.ts +++ b/src/ThreeEditor/Simulation/Figures/FigureManager.ts @@ -72,6 +72,8 @@ export class FigureManager private editor: YaptideEditor; private signals: { + figureAdded: Signal; + figureRemoved: Signal; sceneGraphChanged: Signal; }; @@ -99,13 +101,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 +172,11 @@ export class FigureManager this.name = name; this.figureContainer.fromSerialized(figures); + for (const figure of this.figures) { + // Let the clipped view viewports know that the figure exists + this.signals.figureAdded.dispatch(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/viewport/Viewport.ClippedViewCSG.ts b/src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts index a05331f7c..eb508d52d 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,11 @@ export function ViewportClippedViewCSG< }); crossSectionMesh = CSG.toMesh(objectMesh, object3D.matrix, crossSectionMaterial) as T; + + if (editor.contextManager.currentSimulator === SimulatorType.GEANT4) { + crossSectionMaterial.depthTest = false; + crossSectionMesh.renderOrder = estimateRenderOrder(object3D); + } } else { crossSectionMesh = new THREE.Mesh() as T; } @@ -183,23 +206,38 @@ export function ViewportClippedViewCSG< crossSectionMesh.visible = object3D.visible; clippedObjects.add(crossSectionMesh); + + // Handle Geant4 nested geometry + if (object3D.children.length > 0) { + for (const child of object3D.children) { + updateMeshIntersection(child as T); + } + } } - 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); - }); + }; + + signalGeometryRemoved.add(removeObjectFromMeshIntersection); - editor.signals.objectChanged.add((object3D: T) => { + const updateObjectInMeshIntersection = (object3D: T) => { const crossSectionMesh = clippedObjects.getObjectByName(object3D.uuid) as T; if (crossSectionMesh) { @@ -207,7 +245,16 @@ export function ViewportClippedViewCSG< crossSectionMesh.material.needsUpdate = true; crossSectionMesh.visible = object3D.visible; } - }); + }; + + editor.signals.objectChanged.add(updateObjectInMeshIntersection); + + this.detachSignals = () => { + signalGeometryChanged.remove(updateMeshIntersection); + signalGeometryAdded.remove(updateMeshIntersection); + signalGeometryRemoved.remove(removeObjectFromMeshIntersection); + editor.signals.objectChanged.remove(updateObjectInMeshIntersection); + }; this.reset = () => { clippedObjects.clear(); 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(); From 12e0a3600f2e648f474f6011fcd834136de01544 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Fri, 12 Dec 2025 19:10:24 +0100 Subject: [PATCH 2/5] Handle removing geometry in clipped view --- .../js/commands/RemoveFigureCommand.ts | 15 +++++++++++++++ src/util/hooks/useKeyboardEditorControls.ts | 6 +++++- 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 src/ThreeEditor/js/commands/RemoveFigureCommand.ts diff --git a/src/ThreeEditor/js/commands/RemoveFigureCommand.ts b/src/ThreeEditor/js/commands/RemoveFigureCommand.ts new file mode 100644 index 000000000..251b21645 --- /dev/null +++ b/src/ThreeEditor/js/commands/RemoveFigureCommand.ts @@ -0,0 +1,15 @@ +import { RemoveObjectCommand } from './RemoveObjectCommand'; + +class RemoveFigureCommand extends RemoveObjectCommand { + execute() { + super.execute(); + this.editor.signals.figureRemoved.dispatch(this.object); + } + + undo() { + super.execute(); + this.editor.signals.figureAdded.dispatch(this.object); + } +} + +export { RemoveFigureCommand }; 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); From 34554393ef8b4f361a08e9f8e012a9b9e5c88bb6 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Fri, 12 Dec 2025 19:39:16 +0100 Subject: [PATCH 3/5] Handle removing a part of the tree --- .../js/commands/RemoveFigureCommand.ts | 29 ++++++++++++++++++- .../js/viewport/Viewport.ClippedViewCSG.ts | 7 +++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/ThreeEditor/js/commands/RemoveFigureCommand.ts b/src/ThreeEditor/js/commands/RemoveFigureCommand.ts index 251b21645..b20efd31d 100644 --- a/src/ThreeEditor/js/commands/RemoveFigureCommand.ts +++ b/src/ThreeEditor/js/commands/RemoveFigureCommand.ts @@ -1,14 +1,41 @@ +import { Object3D } from 'three'; + import { RemoveObjectCommand } from './RemoveObjectCommand'; class RemoveFigureCommand extends RemoveObjectCommand { execute() { super.execute(); - this.editor.signals.figureRemoved.dispatch(this.object); + + // 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.execute(); this.editor.signals.figureAdded.dispatch(this.object); + + // 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); } } diff --git a/src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts b/src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts index eb508d52d..0b712e6e0 100644 --- a/src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts +++ b/src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts @@ -213,6 +213,8 @@ export function ViewportClippedViewCSG< updateMeshIntersection(child as T); } } + + editor.signals.sceneGraphChanged.dispatch(); } function updateMeshIntersectionIfExists(object3D: T) { @@ -233,6 +235,8 @@ export function ViewportClippedViewCSG< const crossSectionObject = clippedObjects.getObjectByName(object3D.uuid); if (crossSectionObject) clippedObjects.remove(crossSectionObject); + + editor.signals.sceneGraphChanged.dispatch(); }; signalGeometryRemoved.add(removeObjectFromMeshIntersection); @@ -245,6 +249,8 @@ export function ViewportClippedViewCSG< crossSectionMesh.material.needsUpdate = true; crossSectionMesh.visible = object3D.visible; } + + editor.signals.sceneGraphChanged.dispatch(); }; editor.signals.objectChanged.add(updateObjectInMeshIntersection); @@ -258,6 +264,7 @@ export function ViewportClippedViewCSG< this.reset = () => { clippedObjects.clear(); + editor.signals.sceneGraphChanged.dispatch(); }; this.configurationToJson = (): ClippedViewConfigurationJson => { From a9f4b6eda121d4803e5ee39835389c5b5df6f607 Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Sat, 13 Dec 2025 14:53:47 +0100 Subject: [PATCH 4/5] Fix copilot issues --- src/ThreeEditor/Simulation/Figures/FigureManager.ts | 12 ++++++++++-- src/ThreeEditor/js/commands/RemoveFigureCommand.ts | 3 +-- .../js/viewport/Viewport.ClippedViewCSG.ts | 9 +-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/ThreeEditor/Simulation/Figures/FigureManager.ts b/src/ThreeEditor/Simulation/Figures/FigureManager.ts index 544156d6f..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'; @@ -172,9 +173,16 @@ 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) { - // Let the clipped view viewports know that the figure exists - this.signals.figureAdded.dispatch(figure); + dispatchFigureAdded(figure); } return this; diff --git a/src/ThreeEditor/js/commands/RemoveFigureCommand.ts b/src/ThreeEditor/js/commands/RemoveFigureCommand.ts index b20efd31d..a33543771 100644 --- a/src/ThreeEditor/js/commands/RemoveFigureCommand.ts +++ b/src/ThreeEditor/js/commands/RemoveFigureCommand.ts @@ -21,8 +21,7 @@ class RemoveFigureCommand extends RemoveObjectCommand { } undo() { - super.execute(); - this.editor.signals.figureAdded.dispatch(this.object); + super.undo(); // Dispatch re-adding from top to bottom, so children are called after parent has been called const dispatchReAdd = (o: Object3D) => { diff --git a/src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts b/src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts index 0b712e6e0..4f887359e 100644 --- a/src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts +++ b/src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts @@ -207,13 +207,6 @@ export function ViewportClippedViewCSG< clippedObjects.add(crossSectionMesh); - // Handle Geant4 nested geometry - if (object3D.children.length > 0) { - for (const child of object3D.children) { - updateMeshIntersection(child as T); - } - } - editor.signals.sceneGraphChanged.dispatch(); } @@ -256,7 +249,7 @@ export function ViewportClippedViewCSG< editor.signals.objectChanged.add(updateObjectInMeshIntersection); this.detachSignals = () => { - signalGeometryChanged.remove(updateMeshIntersection); + signalGeometryChanged.remove(updateMeshIntersectionIfExists); signalGeometryAdded.remove(updateMeshIntersection); signalGeometryRemoved.remove(removeObjectFromMeshIntersection); editor.signals.objectChanged.remove(updateObjectInMeshIntersection); From e2f8ff2851c8af4d26a2287b2440d0d39fdcfe6c Mon Sep 17 00:00:00 2001 From: Konrad Michalik Date: Sat, 13 Dec 2025 16:50:46 +0100 Subject: [PATCH 5/5] Fix nested geometry positions --- src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts b/src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts index 4f887359e..ef3b7095d 100644 --- a/src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts +++ b/src/ThreeEditor/js/viewport/Viewport.ClippedViewCSG.ts @@ -192,6 +192,7 @@ 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;