From fa194d17a71c9b6e020d786a1ff012f37ecbf19f Mon Sep 17 00:00:00 2001 From: Paul Moeller Date: Fri, 1 Nov 2024 16:40:47 -0600 Subject: [PATCH 1/5] fix #7338 drop new element into a scrolled beamline editor (#7339) - the fix was to ensure the drop target element was also the scrollable element, not the parent --- sirepo/package_data/static/js/sirepo-lattice.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sirepo/package_data/static/js/sirepo-lattice.js b/sirepo/package_data/static/js/sirepo-lattice.js index b1dcac2906..d6b86ccbc4 100644 --- a/sirepo/package_data/static/js/sirepo-lattice.js +++ b/sirepo/package_data/static/js/sirepo-lattice.js @@ -618,8 +618,7 @@ SIREPO.app.directive('beamlineEditor', function(appState, latticeService, panelS
-
-
+

drag and drop elements here to define the beamline

@@ -631,7 +630,6 @@ SIREPO.app.directive('beamlineEditor', function(appState, latticeService, panelS
-
Delete item {{ latticeService.selectedItem.name }}?
From cadc5a2c6bb6d2b4a8baf62f8e84c53f7274e885 Mon Sep 17 00:00:00 2001 From: Paul Moeller Date: Mon, 4 Nov 2024 10:03:32 -0700 Subject: [PATCH 2/5] fix #7332 geometry outline compute per slice on server (#7342) --- sirepo/package_data/static/css/openmc.css | 4 - sirepo/package_data/static/css/sirepo.css | 4 + .../static/html/openmc-visualization.html | 123 +- sirepo/package_data/static/js/openmc.js | 442 ++--- sirepo/package_data/static/js/radia.js | 923 ++++++++++- .../static/js/sirepo-components.js | 17 +- .../static/js/sirepo-plotting-vtk.js | 1429 +---------------- .../package_data/static/js/sirepo-plotting.js | 20 +- sirepo/package_data/static/js/sirepo-utils.js | 22 - sirepo/package_data/static/js/warpvnd.js | 178 +- .../static/json/openmc-schema.json | 34 +- sirepo/pkcli/openmc.py | 1 + sirepo/sim_data/openmc.py | 7 +- sirepo/template/openmc.py | 153 +- test.sh | 2 +- 15 files changed, 1557 insertions(+), 1802 deletions(-) diff --git a/sirepo/package_data/static/css/openmc.css b/sirepo/package_data/static/css/openmc.css index 894e189197..d10b3a8b64 100644 --- a/sirepo/package_data/static/css/openmc.css +++ b/sirepo/package_data/static/css/openmc.css @@ -1,8 +1,4 @@ -.sr-range-slider { - background: var(--sr-action) !important; -} - .model-tallyReport-axis .sr-enum-button { min-width: 4em; } diff --git a/sirepo/package_data/static/css/sirepo.css b/sirepo/package_data/static/css/sirepo.css index 32e77d4cb3..741fe49c4f 100644 --- a/sirepo/package_data/static/css/sirepo.css +++ b/sirepo/package_data/static/css/sirepo.css @@ -1056,3 +1056,7 @@ img.sr-disabled-image { .sr-hide-scrollbars { overflow: hidden; } + +.sr-range-slider { + background: var(--sr-action) !important; +} diff --git a/sirepo/package_data/static/html/openmc-visualization.html b/sirepo/package_data/static/html/openmc-visualization.html index 272574ac71..e841f2a196 100644 --- a/sirepo/package_data/static/html/openmc-visualization.html +++ b/sirepo/package_data/static/html/openmc-visualization.html @@ -1,71 +1,74 @@
-
-
-
-
-
-
-
-
- - - - - - - - - - - - - - - - -
Eigenvalues
Batchk<k>σ
{{ e.batch }}{{ e.val[0] }}{{ e.val[1] || 'N/A' }}{{ e.val[2] || 'N/A' }}
-
-
- - - - - - - - - - - - - - -
Results
QuantityValueσ
{{ r[0] }}{{ r[1] }}{{ r[2] }}
-
-
-
- Computed Weight Windows - Apply Weight Windows +
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + +
Eigenvalues
Batchk<k>σ
{{ e.batch }}{{ e.val[0] }}{{ e.val[1] || 'N/A' }}{{ e.val[2] || 'N/A' }}
+
+
+ + + + + + + + + + + + + + +
Results
QuantityValueσ
{{ r[0] }}{{ r[1] }}{{ r[2] }}
+
+
+
+ Computed Weight Windows + Apply Weight Windows +
+
-
-
-
-
-
-
-
-
-
+
+
+
-
-
-
+
+
+
+
+
+
+
+
diff --git a/sirepo/package_data/static/js/openmc.js b/sirepo/package_data/static/js/openmc.js index d8e7e5487f..7d76071989 100644 --- a/sirepo/package_data/static/js/openmc.js +++ b/sirepo/package_data/static/js/openmc.js @@ -162,7 +162,7 @@ SIREPO.app.factory('openmcService', function(appState, panelState, requestSender self.canNormalizeScore = score => ! SIREPO.APP_SCHEMA.constants.unnormalizableScores.includes(score); self.computeModel = (modelKey) => { - if (modelKey === "energyAnimation") { + if (['energyAnimation', 'outlineAnimation'].includes(modelKey)) { return "openmcAnimation"; } return modelKey; @@ -445,7 +445,6 @@ SIREPO.app.controller('VisualizationController', function(appState, errorService openmcService.invalidateRange('thresholds'); openmcService.invalidateRange('colorRange'); r.isEnergySelected = "0"; - panelState.clear('tallyReport'); self.simState.saveAndRunSimulation('openmcAnimation'); }; self.simState.logFileURL = function() { @@ -533,9 +532,21 @@ SIREPO.app.factory('tallyService', function(appState, openmcService, utilities, fieldData: null, minField: 0, maxField: 0, - outlines: null, sourceParticles: [], }; + const invalidPlanePosition = -1e16; + + function initMesh() { + self.mesh = null; + const t = openmcService.findTally(); + for (let k = 1; k <= SIREPO.APP_SCHEMA.constants.maxFilters; k++) { + const f = t[`filter${k}`]; + if (f && f._type === 'meshFilter') { + self.mesh = f; + return; + } + } + } function normalizer(score, numParticles) { if (numParticles === undefined || ! openmcService.canNormalizeScore(score)) { @@ -547,7 +558,6 @@ SIREPO.app.factory('tallyService', function(appState, openmcService, utilities, self.clearMesh = () => { self.mesh = null; self.fieldData = null; - self.outlines = null; }; self.colorScale = modelName => { @@ -587,33 +597,31 @@ SIREPO.app.factory('tallyService', function(appState, openmcService, utilities, ]); }; - self.getOutlines = (volId, dim, index) => { - if (! self.outlines) { - return []; - } - const t = self.outlines[appState.applicationState().openmcAnimation.tally]; - if (t && t[`${volId}`]) { - const o = t[`${volId}`][dim]; - if (o.length) { - return o[index]; - } + self.getSourceParticles = () => self.sourceParticles; + + self.getVisibleAxes = () => { + if (! self.mesh) { + return SIREPO.GEOMETRY.GeometryUtils.BASIS().slice(); } - return []; + const v = {}; + SIREPO.GEOMETRY.GeometryUtils.BASIS().forEach(dim => { + v[dim] = true; + SIREPO.GEOMETRY.GeometryUtils.BASIS_VECTORS()[dim].forEach((bv, bi) => { + if (! bv && self.mesh.dimension[bi] < SIREPO.APP_SCHEMA.constants.minTallyResolution) { + delete v[dim]; + } + }); + }); + return Object.keys(v); }; - self.getSourceParticles = () => self.sourceParticles; + self.invalidatePlanePosition = () => { + //TODO(pjm): only clear planePos if the selected tally domain has changed + appState.models.tallyReport.planePos = invalidPlanePosition; + }; - self.initMesh = () => { - const t = openmcService.findTally(); - for (let k = 1; k <= SIREPO.APP_SCHEMA.constants.maxFilters; k++) { - const f = t[`filter${k}`]; - if (f && f._type === 'meshFilter') { - self.mesh = f; - return true; - } - } - self.mesh = null; - return false; + self.isPlanePositionInvalid = () => { + return appState.models.tallyReport.planePos === invalidPlanePosition; }; self.setFieldData = (fieldData, min, max, numParticles) => { @@ -633,14 +641,8 @@ SIREPO.app.factory('tallyService', function(appState, openmcService, utilities, f.stop, ]; } - }; - - self.setOutlines = (tally, outlines) => { - if (appState.applicationState().openmcAnimation.tally === tally) { - self.outlines = { - [tally]: outlines, - }; - } + self.invalidatePlanePosition(); + initMesh(); }; self.setSourceParticles = particles => { @@ -930,7 +932,6 @@ SIREPO.app.directive('tallyViewer', function(appState, openmcService, plotting, return; } tallyService.setFieldData(json.field_data, json.min_field, json.max_field, json.num_particles); - tallyService.setOutlines(json.summaryData.tally, json.summaryData.outlines || {}); tallyService.setSourceParticles(json.summaryData.sourceParticles || []); }; @@ -947,7 +948,7 @@ SIREPO.app.directive('tallyViewer', function(appState, openmcService, plotting, }; }); -SIREPO.app.directive('geometry2d', function(appState, openmcService, panelState, tallyService) { +SIREPO.app.directive('geometry2d', function(appState, frameCache, openmcService, panelState, tallyService) { return { restrict: 'A', scope: { @@ -959,6 +960,8 @@ SIREPO.app.directive('geometry2d', function(appState, openmcService, panelState, controller: function($scope) { $scope.tallyService = tallyService; const displayRanges = {}; + let geometryOutlines; + let lastTally = [null, null, null]; const sources = openmcService.getSourceVisualizations( { box: space => { @@ -994,21 +997,77 @@ SIREPO.app.directive('geometry2d', function(appState, openmcService, panelState, } function buildTallyReport() { - if (! tallyService.mesh) { + if (! tallyService.fieldData || tallyService.isPlanePositionInvalid()) { + return; + } + if ( + (lastTally[0] != appState.models.openmcAnimation.tally) + || (lastTally[1] != appState.models.tallyReport.axis) + || (lastTally[2] != appState.models.tallyReport.planePos) + ) { + geometryOutlines = null; + const newTally = [ + appState.models.openmcAnimation.tally, + appState.models.tallyReport.axis, + appState.models.tallyReport.planePos, + ]; + lastTally = newTally; + const o = appState.models.outlineAnimation; + o.tally = appState.models.openmcAnimation.tally; + o.axis = appState.models.tallyReport.axis; + appState.saveQuietly('outlineAnimation'); + + const n = SIREPO.GEOMETRY.GeometryUtils.BASIS().indexOf(appState.models.tallyReport.axis); + const range = tallyService.getMeshRanges()[n]; + frameCache.getFrame( + 'outlineAnimation', + fieldIndex(appState.models.tallyReport.planePos, range, n), + false, + (index, data) => { + geometryOutlines = data.outlines || {}; + buildTallyReportsWithOutlines(); + }, + ); return; } + if (geometryOutlines) { + buildTallyReportsWithOutlines(); + } + } + + function buildTallyReportsWithOutlines() { const [z, x, y] = tallyReportAxes(); - const [n, m, l] = tallyReportAxisIndices(); + const [zi, xi, yi] = tallyReportAxisIndices(); const ranges = tallyService.getMeshRanges(); - const pos = appState.models.tallyReport.planePos; - - // for now set the aspect ratio to something reasonable even if it distorts the shape + if (z === 'z' || z === 'x') { + const [xl, xh] = ranges[xi]; + ranges[xi][0] = xh; + ranges[xi][1] = xl; + } + const outlines = []; + for (const volId of openmcService.getNonGraveyardVolumes()) { + const v = openmcService.getVolumeById(volId); + if (! v.isVisibleWithTallies) { + continue; + } + const o = geometryOutlines[volId]; + if (o) { + o.forEach((arr, i) => { + outlines.push({ + name: `${v.name}-${i}`, + color: v.color, + data: arr, + }); + }); + } + } + // set the aspect ratio to something reasonable even if it distorts the shape const arRange = [0.50, 1.25]; let ar = Math.max( arRange[0], Math.min( arRange[1], - Math.abs(ranges[m][1] - ranges[m][0]) / Math.abs(ranges[l][1] - ranges[l][0]) + Math.abs(ranges[yi][1] - ranges[yi][0]) / Math.abs(ranges[xi][1] - ranges[xi][0]), ) ); const r = { @@ -1017,30 +1076,20 @@ SIREPO.app.directive('geometry2d', function(appState, openmcService, panelState, global_min: appState.models.openmcAnimation.colorRange[0], global_max: appState.models.openmcAnimation.colorRange[1], threshold: appState.models.openmcAnimation.thresholds, - title: `Score at ${z} = ${SIREPO.UTILS.roundToPlaces(pos, 6)}m${energySumLabel()}`, + title: `Score at ${z} = ${SIREPO.UTILS.roundToPlaces(appState.models.tallyReport.planePos, 6)}m${energySumLabel()}`, x_label: `${x} [m]`, - x_range: ranges[l], + x_range: ranges[xi], y_label: `${y} [m]`, - y_range: ranges[m], - z_matrix: reorderFieldData(tallyService.mesh.dimension)[fieldIndex(pos, ranges[n], n)], - z_range: ranges[n], - overlayData: getOutlines(pos, ranges[n], n), + y_range: ranges[yi], + z_matrix: sliceFieldData(), + z_range: ranges[zi], + overlayData: outlines.concat(getSourceOutlines()), selectedCoords: $scope.energyFilter ? tallyService.getEnergyReportCoords() : null, }; panelState.setData('tallyReport', r); $scope.$broadcast('tallyReport.reload', r); } - function displayRangeIndices() { - const r = tallyService.getMeshRanges(); - return [ - displayRanges.x, - displayRanges.y, - displayRanges.z, - ] - .map((x, i) => [fieldIndex(x.min, r[i], i), fieldIndex(x.max, r[i], i)]); - } - function energySumLabel() { const sumRange = appState.models.openmcAnimation.energyRangeSum; return $scope.energyFilter ? ` / Energy ∑ ${sumDisplay(sumRange[0])}-${sumDisplay(sumRange[1])} MeV` : ''; @@ -1054,8 +1103,15 @@ SIREPO.app.directive('geometry2d', function(appState, openmcService, panelState, ); } - function getOutlines(pos, range, dimIndex) { - + function getSourceOutlines() { + if (appState.models.openmcAnimation.showSources === '0') { + return []; + } + //TODO(pjm): rework this method + const [n, m, l] = tallyReportAxisIndices(); + const dimIndex = n; + const dim = SIREPO.GEOMETRY.GeometryUtils.BASIS()[dimIndex]; + const outlines = []; const particleColors = SIREPO.UTILS.unique( tallyService.getSourceParticles().map(p => particleColor(p)) ); @@ -1065,6 +1121,21 @@ SIREPO.app.directive('geometry2d', function(appState, openmcService, panelState, return pos[j] < r[j][0] || pos[j] > r[j][1] || pos[k] < r[k][0] || pos[k] > r[k][1]; } + // we cannot set the color of an instance of a marker ref, so we will + // have to create them on the fly + function placeMarkers() { + const ns = 'http://www.w3.org/2000/svg'; + let ds = d3.select('svg.sr-plot defs') + .selectAll('marker') + .data(particleColors); + ds.exit().remove(); + ds.enter() + .append(d => document.createElementNS(ns, 'marker')) + .append('path') + .attr('d', 'M0,0 L0,4 L9,2 z'); + ds.call(updateMarkers); + } + function particleColor(p) { return tallyService.sourceParticleColorScale( appState.models.openmcAnimation.sourceColorMap @@ -1079,19 +1150,14 @@ SIREPO.app.directive('geometry2d', function(appState, openmcService, panelState, return `arrow-${c.slice(1)}`; } - // we cannot set the color of an instance of a marker ref, so we will - // have to create them on the fly - function placeMarkers() { - const ns = 'http://www.w3.org/2000/svg'; - let ds = d3.select('svg.sr-plot defs') - .selectAll('marker') - .data(particleColors); - ds.exit().remove(); - ds.enter() - .append(d => document.createElementNS(ns, 'marker')) - .append('path') - .attr('d', 'M0,0 L0,4 L9,2 z'); - ds.call(updateMarkers); + function toReversed(arr) { + // Array.toReversed() is not available in all active browsers + // not using a polyfill due to array iteration bugs + const r = []; + for (let i = (arr.length - 1); i >= 0; --i) { + r.push(arr[i]); + } + return r; } function updateMarkers(selection) { @@ -1107,32 +1173,6 @@ SIREPO.app.directive('geometry2d', function(appState, openmcService, panelState, .attr('fill', d => sourceColor(d)); } - function toReversed(arr) { - // Array.toReversed() is not available in all active browsers - // not using a polyfill due to array iteration bugs - const r = []; - for (let i = (arr.length - 1); i >= 0; --i) { - r.push(arr[i]); - } - return r; - } - - const outlines = []; - const dim = SIREPO.GEOMETRY.GeometryUtils.BASIS()[dimIndex]; - for (const volId of openmcService.getNonGraveyardVolumes()) { - const v = openmcService.getVolumeById(volId); - if (! v.isVisibleWithTallies) { - continue; - } - const o = tallyService.getOutlines(volId, dim, fieldIndex(pos, range, dimIndex)); - o.forEach((arr, i) => { - outlines.push({ - name: `${v.name}-${i}`, - color: v.color, - data: arr, - }); - }); - } sources.forEach((view, i) => { const s = appState.models.settings.sources[i]; if (view instanceof SIREPO.VTK.SphereViews) { @@ -1168,32 +1208,51 @@ SIREPO.app.directive('geometry2d', function(appState, openmcService, panelState, return outlines; } - function reorderFieldData(dims) { - const [n, m, l] = tallyReportAxisIndices(); - const fd = tallyService.fieldData; - const d = SIREPO.UTILS.reshape(fd, dims.slice().reverse()); - const inds = displayRangeIndices(); - let N = 1; - for (const idx of inds) { - N *= (idx[1] - idx[0] + 1); - } - const ff = SIREPO.UTILS.reshape( - new Array(N), - [(inds[n][1] - inds[n][0] + 1), (inds[m][1] - inds[m][0] + 1), (inds[l][1] - inds[l][0] + 1)] + function sliceFieldData() { + const a = appState.models.tallyReport.axis; + const [nx, ny, nz] = tallyService.getMeshRanges().map(r => r[2]); + const f = tallyService.fieldData; + const v = []; + + //TODO(pjm): change tallyReport.planePos to an index + const n = SIREPO.GEOMETRY.GeometryUtils.BASIS().indexOf(a); + const slice = fieldIndex( + appState.models.tallyReport.planePos, + tallyService.getMeshRanges()[n], + n, ); - for (let k = 0; k <= (inds[n][1] - inds[n][0]); ++k) { - for (let j = 0; j <= (inds[m][1] - inds[m][0]); ++j) { - for (let i = 0; i <= (inds[l][1] - inds[l][0]); ++i) { - const v = [0, 0, 0]; - v[l] = inds[l][0] + i; - v[m] = inds[m][0] + j; - v[n] = inds[n][0] + k; - ff[k][j][i] = d[v[2]][v[1]][v[0]]; + if (a === 'x') { + const x = slice; + for (let z = 0; z < nz; z++) { + const r = []; + for (let y = 0; y < ny; y++) { + r[y] = f[z * nx * ny + y * nx + x]; + } + v.push(r.reverse()); + } + } + else if (a === 'y') { + const y = slice * nx; + for (let z = 0; z < nz; z++) { + const r = []; + for (let x = 0; x < nx; x++) { + r[x] = f[z * nx * ny + y + x]; + } + v.push(r); + } + } + else if (a === 'z') { + const z = slice * nx * ny; + for (let y = 0; y < ny; y++) { + const r = []; + for (let x = 0; x < nx; x++) { + r[nx - x - 1] = f[z + y * nx + x]; } + v.push(r); } } - return ff; + return v; } function sourceColor(color) { @@ -1212,38 +1271,33 @@ SIREPO.app.directive('geometry2d', function(appState, openmcService, panelState, } function tallyReportAxes() { - return [ - appState.models.tallyReport.axis, - ...SIREPO.GEOMETRY.GeometryUtils.nextAxes(appState.models.tallyReport.axis).reverse() - ]; + const a = appState.models.tallyReport.axis; + if (a === 'x') { + return ['x', 'y', 'z']; + } + if (a === 'y') { + return ['y', 'x', 'z']; + } + return ['z', 'x', 'y']; } function tallyReportAxisIndices() { - return SIREPO.GEOMETRY.GeometryUtils.axisIndices(appState.models.tallyReport.axis); + return tallyReportAxes().map((a) => SIREPO.GEOMETRY.GeometryUtils.BASIS().indexOf(a)); } function updateDisplay() { + const axisChanged = appState.models.tallyReport.axis !== appState.applicationState().tallyReport.axis; tallyService.updateTallyDisplay(); + if (axisChanged) { + tallyService.invalidatePlanePosition(); + } buildTallyReport(); } function updateDisplayRange() { - if (! tallyService.initMesh()) { - return; - } SIREPO.GEOMETRY.GeometryUtils.BASIS().forEach(dim => { displayRanges[dim] = tallyService.tallyRange(dim); }); - updateSliceAxis(); - } - - function updateSliceAxis() { - if (! tallyService.fieldData) { - return; - } - if (! tallyService.initMesh()) { - return ; - } buildTallyReport(); } @@ -1271,14 +1325,14 @@ SIREPO.app.directive('geometry2d', function(appState, openmcService, panelState, }); $scope.$on('sr-volume-visibility-toggle-all', buildTallyReport); - appState.watchModelFields($scope, ['tallyReport.axis'], updateSliceAxis); - appState.watchModelFields($scope, ['openmcAnimation.colorMap', 'openmcAnimation.sourceColorMap'], updateDisplay); - appState.watchModelFields($scope, ['tallyReport.planePos', 'openmcAnimation.showSources'], buildTallyReport); - $scope.$watch('tallyService.fieldData', (newValue, oldValue) => { - if (newValue && newValue !== oldValue) { - updateDisplayRange(); - } - }); + appState.watchModelFields($scope, [ + 'openmcAnimation.colorMap', + 'openmcAnimation.showSources', + 'openmcAnimation.sourceColorMap', + 'tallyReport.axis', + 'tallyReport.planePos', + ], updateDisplay); + $scope.$watch('tallyService.fieldData', updateDisplayRange); $scope.$on('sr-plotLinked', () => { if (tallyService.fieldData) { @@ -1299,7 +1353,7 @@ SIREPO.app.directive('geometry3d', function(appState, openmcService, plotting, p
@@ -1386,6 +1440,14 @@ SIREPO.app.directive('geometry3d', function(appState, openmcService, plotting, p buildVoxels(); addSources(); $rootScope.$broadcast('vtk.hideLoader'); + const a = tallyService.getVisibleAxes(); + if (a.includes(appState.models.tallyReport.axis)) { + // default to the 2d axis, if visible + vtkScene.showSide(appState.models.tallyReport.axis); + if (appState.models.tallyReport.axis === vtkScene.resetSide){ + vtkScene.showSide(appState.models.tallyReport.axis); + } + } initAxes(); buildAxes(); vtkScene.renderer.resetCamera(); @@ -1446,9 +1508,6 @@ SIREPO.app.directive('geometry3d', function(appState, openmcService, plotting, p picker.deletePickList(tallyBundle.actor); tallyBundle = null; } - if (! tallyService.initMesh()) { - return; - } const [nx, ny, nz] = tallyService.mesh.dimension; const [wx, wy, wz] = [ (tallyService.mesh.upper_right[0] - tallyService.mesh.lower_left[0]) / tallyService.mesh.dimension[0], @@ -1743,9 +1802,7 @@ SIREPO.app.directive('geometry3d', function(appState, openmcService, plotting, p $scope.init = () => {}; - $scope.resize = () => { - //TODO(pjm): reposition camera? - }; + $scope.resize = () => {}; $scope.sizeStyle = () => { if (hasTallies) { @@ -1802,8 +1859,9 @@ SIREPO.app.directive('geometry3d', function(appState, openmcService, plotting, p if (hasTallies && tallyService.fieldData) { addTally(tallyService.fieldData); } - vtkScene.resetView(); - + if (! hasTallies) { + vtkScene.resetView(); + } plotToPNG.initVTK($element, vtkScene.renderer); }); @@ -2075,6 +2133,9 @@ SIREPO.app.directive('volumeSelector', function(appState, openmcService, panelSt appState.cancelChanges('volumes'); unloadMaterial(); } + if (name == 'volumes') { + loadRows(); + } }); loadRows(); @@ -2839,9 +2900,6 @@ SIREPO.viewLogic('tallyView', function(appState, openmcService, panelState, vali } function validateEnergyFilter(filter) { - if (! filter) { - return; - } const rangeFields = ['start', 'stop']; if (rangeFields.map(x => filter[x]).some(x => x == null)) { return; @@ -2868,8 +2926,12 @@ SIREPO.viewLogic('tallyView', function(appState, openmcService, panelState, vali } function validateFilter(field) { - const f = appState.models[$scope.modelName][field.split('.')[1]]; - if (f._type === 'None') { + const m = appState.models[$scope.modelName]; + if (! m) { + return; + } + const f = m[field.split('.')[1]]; + if (! f || f._type === 'None') { return; } if (f._type === 'energyFilter' || f._type === 'energyoutFilter') { @@ -3069,16 +3131,14 @@ SIREPO.viewLogic('tallySettingsView', function(appState, openmcService, panelSta 'opacity', ! is2D, ]); - panelState.showFields('tallyReport', [ - 'axis', is2D, - 'planePos', is2D && tallyService.tallyRange(appState.models.tallyReport.axis, true).steps > 1, - ]); - panelState.showField('openmcAnimation', 'energyRangeSum', ! ! openmcService.findFilter('energyFilter')); panelState.showField('openmcAnimation', 'sourceNormalization', openmcService.canNormalizeScore(appState.models.openmcAnimation.score)); panelState.showField('openmcAnimation', 'numSampleSourceParticles', showSources); panelState.showField('openmcAnimation', 'sourceColorMap', showSources && appState.models.openmcAnimation.numSampleSourceParticles); - updateVisibleAxes(); + panelState.showFields('tallyReport', [ + 'axis', is2D, + 'planePos', is2D && tallyService.tallyRange(updateVisibleAxes(), true).steps > 1, + ]); } function updateEnergyPlot() { @@ -3087,25 +3147,14 @@ SIREPO.viewLogic('tallySettingsView', function(appState, openmcService, panelSta } function updateVisibleAxes() { - if (! tallyService.mesh) { - return; - } - const v = {}; + const a = tallyService.getVisibleAxes(); SIREPO.GEOMETRY.GeometryUtils.BASIS().forEach(dim => { - v[dim] = true; - SIREPO.GEOMETRY.GeometryUtils.BASIS_VECTORS()[dim].forEach((bv, bi) => { - if (! bv && tallyService.mesh.dimension[bi] < SIREPO.APP_SCHEMA.constants.minTallyResolution) { - delete v[dim]; - } - }); - }); - SIREPO.GEOMETRY.GeometryUtils.BASIS().forEach(dim => { - const s = ! Object.keys(v).length || dim in v; - panelState.showEnum('tallyReport', 'axis', dim, s); - if (! s && appState.models.tallyReport.axis === dim) { - appState.models.tallyReport.axis = Object.keys(v)[0]; - } + panelState.showEnum('tallyReport', 'axis', dim, a.includes(dim)); }); + if (! a.includes(appState.models.tallyReport.axis)) { + appState.models.tallyReport.axis = a[0]; + } + return appState.models.tallyReport.axis; } function validateTally() { @@ -3187,22 +3236,31 @@ SIREPO.app.directive('planePositionSlider', function(appState, panelState, tally dim: '<', }, template: ` -
-
+
+
`, controller: function($scope) { $scope.tallyService = tallyService; function updateRange() { - if ($scope.dim) { - const r = tallyService.tallyRange($scope.dim, true); - $scope.min = r.min; - $scope.max = r.max; - $scope.steps = r.steps; - } + $scope.range = tallyService.tallyRange($scope.dim, true); } + + $scope.hasRange = () => { + if ($scope.range) { + if (tallyService.isPlanePositionInvalid()) { + appState.models.tallyReport.planePos = $scope.range.min; + } + if ($scope.range.steps > 1) { + return true; + } + } + return false; + }; + updateRange(); + $scope.$watch('dim', updateRange); $scope.$watch('tallyService.fieldData', updateRange); }, }; diff --git a/sirepo/package_data/static/js/radia.js b/sirepo/package_data/static/js/radia.js index 11ed7969c2..16e7b6d218 100644 --- a/sirepo/package_data/static/js/radia.js +++ b/sirepo/package_data/static/js/radia.js @@ -2236,7 +2236,7 @@ SIREPO.app.directive('radiaViewerContent', function(appState, geometry, panelSta template: `
-
+
@@ -2974,12 +2974,6 @@ SIREPO.app.directive('radiaViewerContent', function(appState, geometry, panelSta panelState.requestData('geometryReport', setupSceneData, c); } - $scope.eventHandlers = { - keypress: function (evt) { - // do nothing? Stops vtk from changing render based on key presses - }, - }; - appState.whenModelsLoaded($scope, function () { $scope.model = appState.models[$scope.modelName]; appState.watchModelFields($scope, watchFields, updateLayout); @@ -4183,3 +4177,918 @@ SIREPO.viewLogic('simulationView', function(activeSection, appState, panelState, }); }); + +class Elevation { + + static NAMES() { + return { + x: 'side', + y: 'top', + z: 'front', + }; + } + + static PLANES() { + return { + x: 'yz', + y: 'zx', + z: 'xy', + }; + } + + constructor(axis) { + if (! SIREPO.GEOMETRY.GeometryUtils.BASIS().includes(axis)) { + throw new Error('Invalid axis: ' + axis); + } + this.axis = axis; + this.class = `.plot-viewport elevation-${axis}`; + this.coordPlane = Elevation.PLANES()[this.axis]; + this.name = Elevation.NAMES()[axis]; + this.labDimensions = { + x: { + axis: this.coordPlane[0], + axisIndex: SIREPO.GEOMETRY.GeometryUtils.axisIndex(this.coordPlane[0]), + }, + y: { + axis: this.coordPlane[1], + axisIndex: SIREPO.GEOMETRY.GeometryUtils.axisIndex(this.coordPlane[1]), + } + }; + } + + labAxis(dim) { + return this.labDimensions[dim].axis; + } + + labAxes() { + return [this.labAxis('x'), this.labAxis('y')]; + } + + labAxisIndex(dim) { + return this.labDimensions[dim].axisIndex; + } + + labAxisIndices() { + return [this.labAxisIndex('x'), this.labAxisIndex('y')]; + } +} + +// elevations tab + vtk tab (or all in 1 tab?) +// A lot of this is 2d and could be extracted +SIREPO.app.directive('3dBuilder', function(appState, geometry, layoutService, panelState, plotting, utilities) { + return { + restrict: 'A', + scope: { + cfg: '<', + modelName: '@', + source: '=controller', + }, + templateUrl: '/static/html/3d-builder.html' + SIREPO.SOURCE_CACHE_KEY, + controller: function($scope) { + const ASPECT_RATIO = 1.0; + + const ELEVATIONS = {}; + for (const axis of SIREPO.GEOMETRY.GeometryUtils.BASIS().slice().reverse()) { + const e = new Elevation(axis); + ELEVATIONS[e.name] = e; + } + + // svg shapes + const LAYOUT_SHAPES = ['circle', 'ellipse', 'line', 'path', 'polygon', 'polyline', 'rect']; + + const SCREEN_INFO = { + x: { + length: $scope.width / 2 + }, + y: { + length: $scope.height / 2 + }, + }; + + const fitDomainPct = 1.01; + + let screenRect = null; + let selectedObject = null; + const objectScale = SIREPO.APP_SCHEMA.constants.objectScale || 1.0; + const invObjScale = 1.0 / objectScale; + + $scope.alignmentTools = SIREPO.APP_SCHEMA.constants.alignmentTools; + $scope.elevations = ELEVATIONS; + $scope.isClientOnly = true; + $scope.margin = {top: 20, right: 20, bottom: 45, left: 70}; + $scope.settings = appState.models.threeDBuilder; + $scope.snapGridSizes = appState.enumVals('SnapGridSize'); + $scope.width = $scope.height = 0; + + let didDrag = false; + let dragShape, dragInitialShape, zoom; + const dragDelta = {x: 0, y: 0}; + let draggedShape = null; + const axisScale = { + x: 1.0, + y: 1.0, + z: 1.0 + }; + const axes = { + x: layoutService.plotAxis($scope.margin, 'x', 'bottom', refresh), + y: layoutService.plotAxis($scope.margin, 'y', 'left', refresh), + }; + + const snapSettingsFields = [ + 'threeDBuilder.snapToGrid', + 'threeDBuilder.snapGridSize', + ]; + const settingsFields = [ + 'threeDBuilder.autoFit', + 'threeDBuilder.elevation', + ].concat(snapSettingsFields); + + function clearDragShadow() { + d3.selectAll('.vtk-object-layout-drag-shadow').remove(); + } + + function getElevation() { + return ELEVATIONS[$scope.settings.elevation]; + } + + function getLabAxis(dim) { + return getElevation().labAxis(dim); + } + + function resetDrag() { + didDrag = false; + hideShapeLocation(); + dragDelta.x = 0; + dragDelta.y = 0; + draggedShape = null; + selectedObject = null; + } + + function d3DragShapeEnd(shape) { + + function reset() { + resetDrag(); + d3.select(`.plot-viewport ${shapeSelectionId(shape, true)}`).call(updateShapeAttributes); + } + + const dragThreshold = 1e-3; + if (! didDrag || Math.abs(dragDelta.x) < dragThreshold && Math.abs(dragDelta.y) < dragThreshold) { + reset(); + return; + } + $scope.$applyAsync(() => { + if (isShapeInBounds(shape)) { + const o = $scope.source.getObject(shape.id); + if (! o) { + reset(); + return; + } + const e = getElevation(); + for (const dim of SIREPO.SCREEN_DIMS) { + o.center[SIREPO.GEOMETRY.GeometryUtils.axisIndex(e.labAxis(dim))] = invObjScale * shape.center[dim]; + } + $scope.source.saveObject(shape.id, reset); + } + else { + appState.cancelChanges($scope.modelName); + reset(); + } + }); + } + + function canDrag(dim) { + const a = d3.event.sourceEvent.shiftKey ? + (Math.abs(dragDelta.x) > Math.abs(dragDelta.y) ? 'x' : 'y') : + null; + return ! a || a === dim; + } + + function d3DragShape(shape) { + + if (! shape.draggable) { + return; + } + didDrag = true; + draggedShape = shape; + SIREPO.SCREEN_DIMS.forEach(dim => { + if (appState.models.threeDBuilder.snapToGrid) { + dragDelta[dim] = snap(shape, dim); + return; + } + dragDelta[dim] = canDrag(dim) ? d3.event[dim] : 0; + const numPixels = scaledPixels(dim, dragDelta[dim]); + shape[dim] = dragInitialShape[dim] + numPixels; + shape.center[dim] = dragInitialShape.center[dim] + numPixels; + }); + d3.select(shapeSelectionId(shape)).call(updateShapeAttributes); + showShapeLocation(shape); + //TODO(mvk): restore live update of virtual shapes + shape.runLinks().forEach(linkedShape => { + d3.select(shapeSelectionId(linkedShape)).call(updateShapeAttributes); + }); + } + + function shapeSelectionId(shape, includeHash=true) { + return `${(includeHash ? '#' : '')}shape-${shape.id}`; + } + + function d3DragShapeStart(shape) { + d3.event.sourceEvent.stopPropagation(); + dragInitialShape = appState.clone(shape); + showShapeLocation(shape); + } + + function drawObjects(elevation) { + const shapes = $scope.source.getShapes(elevation); + + // need to split the shapes up by type or the data will get mismatched + let layouts = {}; + LAYOUT_SHAPES.forEach(l=> { + layouts[l] = shapes + .filter(s => s.layoutShape === l) + .sort((s1, s2) => s2.z - s1.z) + .sort((s1, s2) => s1.draggable - s2.draggable); + }); + + for (let l in layouts) { + let ds = d3.select('.plot-viewport').selectAll(`${l}.vtk-object-layout-shape`) + .data(layouts[l]); + ds.exit().remove(); + // function must return a DOM object in the SVG namespace + ds.enter() + .append(d => { + return document.createElementNS('http://www.w3.org/2000/svg', d.layoutShape); + }) + .on('dblclick', editObject) + .on('dblclick.zoom', null) + .on('click', null); + ds.call(updateShapeAttributes); + ds.call(dragShape); + } + } + + function drawShapes() { + drawObjects(getElevation()); + } + + function editObject(shape) { + d3.event.stopPropagation(); + if (! shape.draggable) { + return; + } + $scope.$applyAsync(function() { + $scope.source.editObjectWithId(shape.id); + }); + } + + function formatObjectLength(val) { + return utilities.roundToPlaces(invObjScale * val, 4); + } + + function getShape(id) { + return $scope.shapes.filter(x => x.id === id)[0]; + } + + function hideShapeLocation() { + select('.focus-text').text(''); + } + + function isMouseInBounds(evt) { + d3.event = evt.event; + var p = d3.mouse(d3.select('.plot-viewport').node()); + d3.event = null; + return p[0] >= 0 && p[0] < $scope.width && p[1] >= 0 && p[1] < $scope.height + ? p + : null; + } + + function isShapeInBounds(shape) { + if (! $scope.cfg.fixedDomain) { + return true; + } + /* + var vAxis = shape.elev === ELEVATIONS.front ? axes.y : axes.z; + var bounds = { + top: shape.y, + bottom: shape.y - shape.height, + left: shape.x, + right: shape.x + shape.width, + }; + if (bounds.right < axes.x.domain[0] || bounds.left > axes.x.domain[1] + || bounds.top < vAxis.domain[0] || bounds.bottom > vAxis.domain[1]) { + return false; + } + + */ + return true; + } + + function refresh() { + if (! axes.x.domain) { + return; + } + if (layoutService.plotAxis.allowUpdates) { + var elementWidth = parseInt(select('.workspace').style('width')); + if (isNaN(elementWidth)) { + return; + } + [$scope.height, $scope.width] = plotting.constrainFullscreenSize($scope, elementWidth, ASPECT_RATIO); + SCREEN_INFO.x.length = $scope.width; + SCREEN_INFO.y.length = $scope.height; + + select('svg') + .attr('width', $scope.width + $scope.margin.left + $scope.margin.right) + .attr('height', $scope.plotHeight()); + axes.x.scale.range([0, $scope.width]); + axes.y.scale.range([$scope.height, 0]); + axes.x.grid.tickSize(-$scope.height); + axes.y.grid.tickSize(-$scope.width); + } + if (plotting.trimDomain(axes.x.scale, axes.x.domain)) { + select('.overlay').attr('class', 'overlay mouse-zoom'); + axes.y.scale.domain(axes.y.domain); + } + else { + select('.overlay').attr('class', 'overlay mouse-move-ew'); + } + + resetZoom(); + select('.plot-viewport').call(zoom); + $.each(axes, function(dim, axis) { + var d = axes[dim].scale.domain(); + var r = axes[dim].scale.range(); + axisScale[dim] = Math.abs((d[1] - d[0]) / (r[1] - r[0])); + + axis.updateLabelAndTicks({ + width: $scope.width, + height: $scope.height, + }, select); + axis.grid.ticks( + $scope.settings.snapToGrid ? + Math.round(Math.abs(d[1] - d[0]) / ($scope.settings.snapGridSize * objectScale)) : + axis.tickCount + ); + select('.' + dim + '.axis.grid').call(axis.grid); + }); + + screenRect = geometry.rect( + geometry.point(), + geometry.point($scope.width, $scope.height, 0) + ); + + drawShapes(); + } + + function replot(doFit=false) { + const b = $scope.source.shapeBounds(getElevation()); + const newDomain = $scope.cfg.initDomian; + SIREPO.SCREEN_DIMS.forEach(dim => { + const axis = axes[dim]; + const bd = b[dim]; + const nd = newDomain[dim]; + axis.domain = $scope.cfg.fullZoom ? [-Infinity, Infinity] : nd; + if (($scope.settings.autoFit || doFit) && bd[0] !== bd[1]) { + nd[0] = fitDomainPct * bd[0]; + nd[1] = fitDomainPct * bd[1]; + // center + const d = (nd[1] - nd[0]) / 2 - (bd[1] - bd[0]) / 2; + nd[0] -= d; + nd[1] -= d; + } + axis.scale.domain(newDomain[dim]); + }); + $scope.resize(); + } + + function resetZoom() { + zoom = axes.x.createZoom().y(axes.y.scale); + } + + function scaledPixels(dim, pixels) { + const dom = axes[dim].scale.domain(); + return pixels * SIREPO.SCREEN_INFO[dim].direction * (dom[1] - dom[0]) / SCREEN_INFO[dim].length; + } + + function select(selector) { + var e = d3.select($scope.element); + return selector ? e.select(selector) : e; + } + + function selectObject(d) { + //TODO(mvk): allow using shift to select more than one (for alignment etc.) + if (! selectedObject || selectedObject.id !== d.id ) { + selectedObject = d; + } + else { + selectedObject = null; + } + } + + function shapeColor(hexColor, alpha) { + var comp = plotting.colorsFromHexString(hexColor); + return 'rgb(' + comp[0] + ', ' + comp[1] + ', ' + comp[2] + ', ' + (alpha || 1.0) + ')'; + } + + function showShapeLocation(shape) { + select('.focus-text').text( + 'Center: ' + + formatObjectLength(shape.center.x) + ', ' + + formatObjectLength(shape.center.y) + ', ' + + formatObjectLength(shape.center.z) + ); + } + + function snap(shape, dim) { + function roundUnits(val, unit) { + return unit * Math.round(val / unit); + } + + if (! canDrag(dim)) { + return 0; + } + + const g = parseFloat($scope.settings.snapGridSize) * objectScale; + const ctr = dragInitialShape.center[dim]; + const offset = axes[dim].scale(roundUnits(ctr, g)) - axes[dim].scale(ctr); + const gridSpacing = Math.abs(axes[dim].scale(2 * g) - axes[dim].scale(g)); + const gridUnits = roundUnits(d3.event[dim], gridSpacing); + const numPixels = scaledPixels(dim, gridUnits + offset); + shape[dim] = roundUnits(dragInitialShape[dim] + numPixels, g); + shape.center[dim] = roundUnits(ctr + numPixels, g); + return Math.round(gridUnits + offset); + } + + // called when dragging a new object, not an existing object + function updateDragShadow(o, p) { + let r = d3.select('.plot-viewport rect.vtk-object-layout-drag-shadow'); + if (r.empty()) { + const s = $scope.source.viewShadow(o).getView(getElevation()); + r = d3.select('.plot-viewport').append('rect') + .attr('class', 'vtk-object-layout-shape vtk-object-layout-drag-shadow') + .attr('width', shapeSize(s, 'x')) + .attr('height', shapeSize(s, 'y')); + } + //showShapeLocation(shape); + r.attr('x', p[0]).attr('y', p[1]); + } + + function shapeOrigin(shape, dim) { + return axes[dim].scale( + shape.center[dim] - SIREPO.SCREEN_INFO[dim].direction * shape.size[dim] / 2 + ); + } + + function shapePoints(shape) { + //TODO(mvk): apply transforms to dx, dy + const [dx, dy] = shape.id === (draggedShape || {}).id ? [dragDelta.x, dragDelta.y] : [0, 0]; + let pts = ''; + for (const p of shape.points) { + pts += `${dx + axes.x.scale(p.x)},${dy + axes.y.scale(p.y)} `; + } + return pts; + } + + function linePoints(shape) { + if (! shape.line || getElevation().coordPlane !== shape.coordPlane) { + return null; + } + + const lp = shape.line.points; + const labX = getElevation().labAxis('x'); + const labY = getElevation().labAxis('y'); + + // same points in this coord plane + if (lp[0][labX] === lp[1][labX] && lp[0][labY] === lp[1][labY]) { + return null; + } + + var scaledLine = geometry.lineFromArr( + lp.map(function (p) { + var sp = []; + SIREPO.SCREEN_DIMS.forEach(function (dim) { + sp.push(axes[dim].scale(p[getElevation().labAxis(dim)])); + }); + return geometry.pointFromArr(sp); + })); + + var pts = screenRect.boundaryIntersectionsWithLine(scaledLine); + return pts; + } + + function shapeSize(shape, dim) { + let c = shape.center[dim] || 0; + let s = shape.size[dim] || 0; + return Math.abs(axes[dim].scale(c + s / 2) - axes[dim].scale(c - s / 2)); + } + + //TODO(mvk): set only those attributes that pertain to each svg shape + function updateShapeAttributes(selection) { + selection + .attr('class', 'vtk-object-layout-shape') + .classed('vtk-object-layout-shape-selected', d => d.id === (selectedObject || {}).id) + .classed('vtk-object-layout-shape-undraggable', d => ! d.draggable) + .attr('id', d => shapeSelectionId(d, false)) + .attr('href', d => d.href ? `#${d.href}` : '') + .attr('points', d => $.isEmptyObject(d.points || {}) ? null : shapePoints(d)) + .attr('x', d => shapeOrigin(d, 'x') - (d.outlineOffset || 0)) + .attr('y', d => shapeOrigin(d, 'y') - (d.outlineOffset || 0)) + .attr('x1', d => { + const pts = linePoints(d); + return pts ? (pts[0] ? pts[0].coords()[0] : 0) : 0; + }) + .attr('x2', d => { + const pts = linePoints(d); + return pts ? (pts[1] ? pts[1].coords()[0] : 0) : 0; + }) + .attr('y1', d => { + const pts = linePoints(d); + return pts ? (pts[0] ? pts[0].coords()[1] : 0) : 0; + }) + .attr('y2', d => { + const pts = linePoints(d); + return pts ? (pts[1] ? pts[1].coords()[1] : 0) : 0; + }) + .attr('marker-end', d => { + if (d.endMark && d.endMark.length) { + return `url(#${d.endMark})`; + } + }) + .attr('marker-start', d => { + if (d.endMark && d.endMark.length) { + return `url(#${d.endMark})`; + } + }) + .attr('width', d => shapeSize(d, 'x') + 2 * (d.outlineOffset || 0)) + .attr('height', d => shapeSize(d, 'y') + 2 * (d.outlineOffset || 0)) + .attr('style', d => { + if (d.color) { + const a = d.alpha === 0 ? 0 : (d.alpha || 1.0); + const fill = `fill:${(d.fillStyle ? shapeColor(d.color, a) : 'none')}`; + return `${fill}; stroke: ${shapeColor(d.color)}; stroke-width: ${d.strokeWidth || 1.0}`; + } + }) + .attr('stroke-dasharray', d => d.strokeStyle === 'dashed' ? (d.dashes || "5,5") : ""); + let tooltip = selection.select('title'); + if (tooltip.empty()) { + tooltip = selection.append('title'); + } + tooltip.text(function(d) { + const ctr = d.getCenterCoords().map(function (c) { + return utilities.roundToPlaces(c * invObjScale, 2); + }); + const sz = d.getSizeCoords().map(function (c) { + return utilities.roundToPlaces(c * invObjScale, 2); + }); + return `${d.name} center : ${ctr} size: ${sz}`; + }); + } + + $scope.destroy = () => { + if (zoom) { + zoom.on('zoom', null); + } + $('.plot-viewport').off(); + }; + + $scope.dragMove = (o, evt) => { + const p = isMouseInBounds(evt); + if (p) { + d3.select('.sr-drag-clone').attr('class', 'sr-drag-clone sr-drag-clone-hidden'); + updateDragShadow(o, p); + } + else { + clearDragShadow(); + d3.select('.sr-drag-clone').attr('class', 'sr-drag-clone'); + hideShapeLocation(); + } + }; + + // called when dropping new objects, not existing + $scope.dropSuccess = (o, evt) => { + clearDragShadow(); + const p = isMouseInBounds(evt); + if (p) { + const labXIdx = geometry.basis.indexOf(getLabAxis('x')); + const labYIdx = geometry.basis.indexOf(getLabAxis('y')); + const ctr = [0, 0, 0]; + ctr[labXIdx] = axes.x.scale.invert(p[0]); + ctr[labYIdx] = axes.y.scale.invert(p[1]); + o.center = ctr.map(x => x * invObjScale); + $scope.$emit('layout.object.dropped', o); + drawShapes(); + } + }; + + $scope.editObject = $scope.source.editObject; + + $scope.fitToShapes = () => { + replot(true); + }; + + $scope.getElevation = getElevation; + + $scope.getObjects = () => { + return (appState.models[$scope.modelName] || {}).objects; + }; + + $scope.init = () => { + $scope.shapes = $scope.source.getShapes(getElevation()); + + $scope.$on($scope.modelName + '.changed', function(e, name) { + $scope.shapes = $scope.source.getShapes(); + drawShapes(); + replot(); + }); + + select('svg').attr('height', plotting.initialHeight($scope)); + + $.each(axes, function(dim, axis) { + axis.init(); + axis.grid = axis.createAxis(); + }); + resetZoom(); + dragShape = d3.behavior.drag() + .origin(function(d) { return d; }) + .on('drag', d3DragShape) + .on('dragstart', d3DragShapeStart) + .on('dragend', d3DragShapeEnd); + SIREPO.SCREEN_DIMS.forEach(dim => { + axes[dim].parseLabelAndUnits(`${getLabAxis(dim)} [m]`); + }); + replot(); + }; + + $scope.isDropEnabled = () => $scope.source.isDropEnabled(); + + $scope.plotHeight = () => $scope.plotOffset() + $scope.margin.top + $scope.margin.bottom; + + $scope.plotOffset = () => $scope.height; + + $scope.resize = () => { + if (select().empty()) { + return; + } + refresh(); + }; + + $scope.setElevation = elev => { + $scope.settings.elevation = elev; + SIREPO.SCREEN_DIMS.forEach(dim => { + axes[dim].parseLabelAndUnits(`${getLabAxis(dim)} [m]`); + }); + replot(); + }; + + appState.watchModelFields($scope, settingsFields, () => { + appState.saveChanges('threeDBuilder'); + }); + appState.watchModelFields($scope, snapSettingsFields, refresh); + + $scope.$on('shapes.loaded', drawShapes); + + $scope.$on('shape.locked', (e, locks) => { + let doRefresh = false; + for (const l of locks) { + const s = getShape(l.id); + if (s) { + doRefresh = true; + s.draggable = ! l.doLock; + } + } + if (doRefresh) { + refresh(); + } + }); + + }, + link: function link(scope, element) { + plotting.linkPlot(scope, element); + }, + }; +}); + +SIREPO.app.directive('objectTable', function(appState, $rootScope) { + return { + restrict: 'A', + scope: { + elevation: '=', + modelName: '@', + overlayButtons: '=', + source: '=', + }, + template: ` +
+
Objects
+
+
+ + + + + + + + +
+ {{ lockTitle(o) }} + {{ lockTitle(o) }} + + + {{ o.name }} + +
+
+ + + + + + + +
+
+
+
+
+
+ + `, + controller: function($scope) { + $scope.expanded = {}; + $scope.fields = ['objects']; + $scope.locked = {}; + $scope.unlockable = {}; + + const isInGroup = $scope.source.isInGroup; + const getGroup = $scope.source.getGroup; + const getMemberObjects = $scope.source.getMemberObjects; + let areObjectsUnlockable = appState.models.simulation.areObjectsUnlockable; + + function arrange(objects) { + + const arranged = []; + + function addGroup(o) { + const p = getGroup(o); + if (p && ! arranged.includes(p)) { + return; + } + if (! arranged.includes(o)) { + arranged.push(o); + } + for (const m of getMemberObjects(o)) { + if ($scope.isGroup(m)) { + addGroup(m); + } + else { + arranged.push(m); + } + } + } + + for (const o of objects) { + if (arranged.includes(o)) { + continue; + } + if (! isInGroup(o)) { + arranged.push(o); + } + if ($scope.isGroup(o)) { + addGroup(o); + } + } + return arranged; + } + + function init() { + if (areObjectsUnlockable === undefined) { + areObjectsUnlockable = true; + } + for (const o of $scope.getObjects()) { + $scope.expanded[o.id] = true; + $scope.unlockable[o.id] = areObjectsUnlockable; + $scope.locked[o.id] = ! areObjectsUnlockable; + + } + } + + function setLocked(o, doLock) { + $scope.locked[o.id] = doLock; + let ids = [ + { + id: o.id, + doLock: doLock + }, + ]; + if ($scope.isGroup(o)) { + getMemberObjects(o).forEach(x => { + ids = ids.concat(setLocked(x, doLock)); + if (areObjectsUnlockable) { + $scope.unlockable[x.id] = ! doLock; + } + }); + } + return ids; + } + + $scope.align = (o, alignType) => { + $scope.source.align(o, alignType, $scope.elevation.labAxisIndices()); + }; + + $scope.areAllGroupsExpanded = o => { + if (! isInGroup(o)) { + return true; + } + const p = getGroup(o); + if (! $scope.expanded[p.id]) { + return false; + } + return $scope.areAllGroupsExpanded(p); + }; + + $scope.copyObject = $scope.source.copyObject; + + $scope.deleteObject = $scope.source.deleteObject; + + $scope.editObject = $scope.source.editObject; + + $scope.getObjects = () => { + return arrange((appState.models[$scope.modelName] || {}).objects); + }; + + $scope.isAlignDisabled = o => $scope.locked[o.id] || ! $scope.isGroup(o) || getMemberObjects(o).length < 2; + + $scope.isGroup = $scope.source.isGroup; + + $scope.isMoveDisabled = (direction, o) => { + if ($scope.locked[o.id]) { + return true; + } + const objects = isInGroup(o) ? + getMemberObjects(getGroup(o)) : + $scope.getObjects().filter(x => ! isInGroup(x)); + let i = objects.indexOf(o); + return direction === -1 ? i === 0 : i === objects.length - 1; + }; + + $scope.lockTitle = o => { + if (! areObjectsUnlockable) { + return 'designer is read-only for this magnet'; + } + if (! $scope.unlockable[o.id]) { + return 'cannot unlock'; + } + return `click to ${$scope.locked[o.id] ? 'unlock' : 'lock'}`; + }; + + $scope.moveObject = $scope.source.moveObject; + + $scope.nestLevel = o => { + let n = 0; + if (isInGroup(o)) { + n += (1 + $scope.nestLevel(getGroup(o))); + } + return n; + }; + + $scope.toggleExpand = o => { + $scope.expanded[o.id] = ! $scope.expanded[o.id]; + }; + + $scope.toggleLock = o => { + if (! $scope.unlockable[o.id]) { + return; + } + $rootScope.$broadcast('shape.locked', setLocked(o, ! $scope.locked[o.id])); + }; + + init(); + }, + }; +}); + +SIREPO.app.factory('vtkUtils', function() { + var self = {}; + + // Converts vtk colors ranging from 0 -> 255 to 0.0 -> 1.0 + // can't map, because we will still have a UINT8 array + self.floatToRGB = f => { + const rgb = new window.Uint8Array(f.length); + for (let i = 0; i < rgb.length; ++i) { + rgb[i] = Math.floor(255 * f[i]); + } + return rgb; + }; + + return self; +}); diff --git a/sirepo/package_data/static/js/sirepo-components.js b/sirepo/package_data/static/js/sirepo-components.js index 0c5939620b..896a6a4072 100644 --- a/sirepo/package_data/static/js/sirepo-components.js +++ b/sirepo/package_data/static/js/sirepo-components.js @@ -5755,6 +5755,7 @@ SIREPO.app.directive('slider', function(appState, panelState) { let slider = null; // don't show labels for simple cases, ex. opacity $scope.showLabels = !($scope.min === 0 && $scope.max === 1); + function buildSlider() { const s = $($element).find('.' + sliderClass); if (! s.length) { @@ -5792,12 +5793,18 @@ SIREPO.app.directive('slider', function(appState, panelState) { } function didChange(newValues, oldValues) { - if (Array.isArray(newValues)) { - return newValues.some((x, i) => x !== oldValues[i]) && ! newValues.some(x => x == null); - } return newValues != null && newValues !== oldValues; } + function updateRange(newValue, oldValue) { + if (slider && didChange(newValue, oldValue)) { + slider.slider('option', 'min', $scope.min); + slider.slider('option', 'max', $scope.max); + slider.slider('option', 'step', ($scope.max - $scope.min) / ($scope.steps - 1)); + } + } + + $scope.display = (val) => { if (Array.isArray(val)) { if ($scope.space === 'log' && $scope.min > 0) { @@ -5822,6 +5829,10 @@ SIREPO.app.directive('slider', function(appState, panelState) { } ); + $scope.$watch('min', updateRange); + $scope.$watch('max', updateRange); + $scope.$watch('steps', updateRange); + $scope.$on('$destroy', () => { if (slider) { slider.slider('destroy'); diff --git a/sirepo/package_data/static/js/sirepo-plotting-vtk.js b/sirepo/package_data/static/js/sirepo-plotting-vtk.js index 4dc53c9a0c..6b2dba6881 100644 --- a/sirepo/package_data/static/js/sirepo-plotting-vtk.js +++ b/sirepo/package_data/static/js/sirepo-plotting-vtk.js @@ -2,67 +2,6 @@ var srlog = SIREPO.srlog; var srdbg = SIREPO.srdbg; -SIREPO.DEFAULT_COLOR_MAP = 'viridis'; -SIREPO.ZERO_ARR = [0, 0, 0]; -SIREPO.ZERO_STR = '0, 0, 0'; - -/** - * - */ -class Elevation { - - static NAMES() { - return { - x: 'side', - y: 'top', - z: 'front', - }; - } - - static PLANES() { - return { - x: 'yz', - y: 'zx', - z: 'xy', - }; - } - - constructor(axis) { - if (! SIREPO.GEOMETRY.GeometryUtils.BASIS().includes(axis)) { - throw new Error('Invalid axis: ' + axis); - } - this.axis = axis; - this.class = `.plot-viewport elevation-${axis}`; - this.coordPlane = Elevation.PLANES()[this.axis]; - this.name = Elevation.NAMES()[axis]; - this.labDimensions = { - x: { - axis: this.coordPlane[0], - axisIndex: SIREPO.GEOMETRY.GeometryUtils.axisIndex(this.coordPlane[0]), - }, - y: { - axis: this.coordPlane[1], - axisIndex: SIREPO.GEOMETRY.GeometryUtils.axisIndex(this.coordPlane[1]), - } - }; - } - - labAxis(dim) { - return this.labDimensions[dim].axis; - } - - labAxes() { - return [this.labAxis('x'), this.labAxis('y')]; - } - - labAxisIndex(dim) { - return this.labDimensions[dim].axisIndex; - } - - labAxisIndices() { - return [this.labAxisIndex('x'), this.labAxisIndex('y')]; - } -} class ObjectViews { @@ -728,9 +667,13 @@ class VTKScene { * @param {[number]} viewUp */ setCam(position = [1, 0, 0], viewUp = [0, 0, 1]) { + // set focal point outside of the origin initially to avoid a VTK warning: + // "resetting view-up since view plane normal is parallel" + // this happens because the camera is recalculated on each modification + this.cam.setFocalPoint(10, 10, 10); + this.cam.setViewUp(...viewUp); this.cam.setPosition(...position); this.cam.setFocalPoint(0, 0, 0); - this.cam.setViewUp(...viewUp); this.renderer.resetCamera(); this.cam.yaw(0.6); if (this.marker) { @@ -960,80 +903,6 @@ class BoxBundle extends ActorBundle { } -/** - * A bundle for a line source defined by two points - */ -class LineBundle extends ActorBundle { - /** - * @param {[number]} labP1 - 1st point - * @param {[number]} labP2 - 2nd point - * @param {SIREPO.GEOMETRY.Transform} transform - a Transform to translate between "lab" and "local" coordinate systems - * @param {{}} actorProperties - a map of actor properties (e.g. 'color') to values - */ - constructor( - labP1 = [0, 0, 0], - labP2 = [0, 0, 1], - transform = new SIREPO.GEOMETRY.Transform(), - actorProperties = {} - ) { - super( - vtk.Filters.Sources.vtkLineSource.newInstance({ - point1: labP1, - point2: labP2, - resolution: 2 - }), - transform, - actorProperties - ); - } -} - -/** - * A bundle for a plane source defined by three points - */ -class PlaneBundle extends ActorBundle { - /** - * @param {[number]} labOrigin - origin - * @param {[number]} labP1 - 1st point - * @param {[number]} labP2 - 2nd point - * @param {SIREPO.GEOMETRY.Transform} transform - a Transform to translate between "lab" and "local" coordinate systems - * @param {Object} actorProperties - a map of actor properties (e.g. 'color') to values - */ - constructor( - labOrigin = [0, 0, 0], - labP1 = [1, 0, 0], - labP2 = [0, 1, 0], - transform = new SIREPO.GEOMETRY.Transform(), - actorProperties = {} - ) { - super(vtk.Filters.Sources.vtkPlaneSource.newInstance(), transform, actorProperties); - this.setPoints(labOrigin, labP1, labP2); - this.setResolution(); - } - - /** - * Set the defining points of the plane - * @param {[number]} labOrigin - origin - * @param {[number]} labP1 - 1st point - * @param {[number]} labP2 - 2nd point - */ - setPoints(labOrigin, labP1, labP2) { - this.source.setOrigin(...this.transform.apply(new SIREPO.GEOMETRY.Matrix(labOrigin)).val); - this.source.setPoint1(...this.transform.apply(new SIREPO.GEOMETRY.Matrix(labP1)).val); - this.source.setPoint2(...this.transform.apply(new SIREPO.GEOMETRY.Matrix(labP2)).val); - } - - /** - * Set the resolution in each direction - * @param {number} xRes - resolution (number of divisions) in the direction of the origin to p1 - * @param {number} yRes - resolution (number of divisions) in the direction of the origin to p2 - */ - setResolution(xRes = 1, yRes = 1) { - this.source.setXResolution(xRes); - this.source.setYResolution(yRes); - } -} - /** * A bundle for generic polydata */ @@ -1222,29 +1091,6 @@ class CoordMapper { return new BoxBundle(labSize, labCenter, this.transform, actorProperties); } - /** - * Builds a line - * @param {[number]} labP1 - 1st point - * @param {[number]} labP2 - 2nd point - * @param {Object} actorProperties - a map of actor properties (e.g. 'color') to values - * @returns {LineBundle} - */ - buildLine(labP1, labP2, actorProperties) { - return new LineBundle(labP1, labP2, this.transform, actorProperties); - } - - /** - * Builds a plane - * @param {[number]} labOrigin - origin - * @param {[number]} labP1 - 1st point - * @param {[number]} labP2 - 2nd point - * @param {Object} actorProperties - a map of actor properties (e.g. 'color') to values - * @returns {LineBundle} - */ - buildPlane(labOrigin, labP1, labP2, actorProperties) { - return new PlaneBundle(labOrigin, labP1, labP2, this.transform, actorProperties); - } - /** * Creates a Bundle from PolyData * @param {vtk.Common.DataModel.vtkPolyData} polyData @@ -1579,7 +1425,7 @@ class ViewPortBox extends ViewPortObject { } } -SIREPO.app.factory('vtkPlotting', function(appState, errorService, geometry, plotting, panelState, requestSender, utilities, $location, $rootScope, $timeout, $window) { +SIREPO.app.factory('vtkPlotting', function(errorService, geometry, plotting, requestSender, utilities, $rootScope) { let self = {}; let stlReaders = {}; @@ -1673,20 +1519,6 @@ SIREPO.app.factory('vtkPlotting', function(appState, errorService, geometry, plo return actorBundle(src); }, - buildLine: function(labP1, labP2, colorArray) { - var vp1 = this.xform.doTransform(labP1); - var vp2 = this.xform.doTransform(labP2); - var ls = vtk.Filters.Sources.vtkLineSource.newInstance({ - point1: [vp1[0], vp1[1], vp1[2]], - point2: [vp2[0], vp2[1], vp2[2]], - resolution: 2 - }); - - var ab = actorBundle(ls); - ab.actor.getProperty().setColor(colorArray[0], colorArray[1], colorArray[2]); - return ab; - }, - buildPlane: function(labOrigin, labP1, labP2) { var src = vtk.Filters.Sources.vtkPlaneSource.newInstance(); var b = actorBundle(src); @@ -1776,54 +1608,10 @@ SIREPO.app.factory('vtkPlotting', function(appState, errorService, geometry, plo }; }; - self.buildSTL = (coordMapper, file, callback) => { - let r = self.getSTLReader(file); - if (r) { - setSTL(r); - return; - } - - self.loadSTLFile(file).then(function (r) { - r.loadData() - .then(function (res) { - self.addSTLReader(file, r); - setSTL(r); - }, function (reason) { - throw new Error(file + ': Error loading data from .stl file: ' + reason); - } - ).catch(function (e) { - errorService.alertText(e); - }); - }); - - function setSTL(r) { - const b = new ActorBundle(r, coordMapper.transform); - let m = []; - coordMapper.transform.matrix.val.forEach(x => { - m = m.concat(x); - m.push(0); - }); - m = m.concat([0, 0, 0, 1]); - b.actor.setUserMatrix(m); - callback(b); - } - - }; - - self.clearSTLReaders = function() { - stlReaders = {}; - }; - self.getSTLReader = function(file) { return stlReaders[file]; }; - self.isSTLFileValid = function(file) { - return self.loadSTLFile(file).then(function (r) { - return ! ! r; - }); - }; - self.isSTLUrlValid = function(url) { return self.loadSTLURL(url).then(function (r) { return ! ! r; @@ -1858,90 +1646,11 @@ SIREPO.app.factory('vtkPlotting', function(appState, errorService, geometry, plo }); }; - // create a 3d shape - self.plotShape = function(id, name, center, size, color, alpha, fillStyle, strokeStyle, dashes, layoutShape, points) { - var shape = plotting.plotShape(id, name, center, size, color, alpha, fillStyle, strokeStyle, dashes, layoutShape, points); - shape.axes.push('z'); - shape.center.z = center[2]; - shape.size.z = size[2]; - return shape; - }; - self.plotLine = function(id, name, line, color, alpha, strokeStyle, dashes) { var shape = plotting.plotLine(id, name, line, color, alpha, strokeStyle, dashes); return shape; }; - self.removeSTLReader = function(file) { - if (stlReaders[file]) { - delete stlReaders[file]; - } - }; - - self.cylinderSection = function(center, axis, radius, height, planes) { - var startAxis = [0, 0, 1]; - var startOrigin = [0, 0, 0]; - var cylBounds = [-radius, radius, -radius, radius, -height/2.0, height/2.0]; - var cyl = vtk.Common.DataModel.vtkCylinder.newInstance({ - radius: radius, - center: startOrigin, - axis: startAxis - }); - - var pl = planes.map(function (p) { - return vtk.Common.DataModel.vtkPlane.newInstance({ - normal: p.norm || startAxis, - origin: p.origin || startOrigin - }); - }); - - // perform the sectioning - var section = vtk.Common.DataModel.vtkImplicitBoolean.newInstance({ - operation: 'Intersection', - functions: [cyl, pl[0], pl[1], pl[2], pl[3]] - }); - - var sectionSample = vtk.Imaging.Hybrid.vtkSampleFunction.newInstance({ - implicitFunction: section, - modelBounds: cylBounds, - sampleDimensions: [32, 32, 32] - }); - - var sectionSource = vtk.Filters.General.vtkImageMarchingCubes.newInstance(); - sectionSource.setInputConnection(sectionSample.getOutputPort()); - // this transformation adapted from VTK cylinder source - we don't "untranslate" because we want to - // rotate in place, not around the global origin - vtk.Common.Core.vtkMatrixBuilder - .buildFromRadian() - //.translate(...center) - .translate(center[0], center[1], center[2]) - .rotateFromDirections(startAxis, axis) - .apply(sectionSource.getOutputData().getPoints().getData()); - return sectionSource; - }; - - self.setColorScalars = function(data, color) { - var pts = data.getPoints(); - var n = color.length * (pts.getData().length / pts.getNumberOfComponents()); - var pd = data.getPointData(); - var s = pd.getScalars(); - var rgb = s ? s.getData() : new window.Uint8Array(n); - for (var i = 0; i < n; i += color.length) { - for (var j = 0; j < color.length; ++j) { - rgb[i + j] = color[j]; - } - } - pd.setScalars( - vtk.Common.Core.vtkDataArray.newInstance({ - name: 'color', - numberOfComponents: color.length, - values: rgb, - }) - ); - - data.modified(); - }; - self.stlFileType = 'stl-file'; self.addActors = function(renderer, actorArr) { @@ -1994,988 +1703,6 @@ SIREPO.app.factory('vtkPlotting', function(appState, errorService, geometry, plo return self; }); -SIREPO.app.directive('stlFileChooser', function(validationService, vtkPlotting) { - return { - restrict: 'A', - scope: { - description: '=', - url: '=', - inputFile: '=', - model: '=', - require: '<', - title: '@', - }, - template: ` -
-
- `, - controller: function($scope) { - $scope.validate = function (file) { - $scope.url = URL.createObjectURL(file); - return vtkPlotting.isSTLUrlValid($scope.url).then(function (ok) { - return ok; - }); - }; - $scope.validationError = ''; - }, - link: function(scope, element, attrs) { - - }, - }; -}); - -SIREPO.app.directive('stlImportDialog', function(appState, fileManager, fileUpload, vtkPlotting, requestSender) { - return { - restrict: 'A', - scope: { - description: '@', - title: '@', - }, - template: ` - - `, - controller: function($scope) { - $scope.inputFile = null; - $scope.fileURL = null; - $scope.isMissingImportFile = function() { - return ! $scope.inputFile; - }; - $scope.fileUploadError = ''; - $scope.isUploading = false; - $scope.title = $scope.title || 'Import STL File'; - $scope.description = $scope.description || 'Select File'; - - $scope.importStlFile = function(inputFile) { - if (! inputFile) { - return; - } - newSimFromSTL(inputFile); - }; - - function upload(inputFile, data) { - var simId = data.models.simulation.simulationId; - fileUpload.uploadFileToUrl( - inputFile, - $scope.isConfirming - ? { - confirm: $scope.isConfirming, - } - : null, - requestSender.formatUrl( - 'uploadLibFile', - { - '': simId, - '': SIREPO.APP_SCHEMA.simulationType, - '': vtkPlotting.stlFileType, - }), - function(d) { - $('#simulation-import').modal('hide'); - $scope.inputFile = null; - URL.revokeObjectURL($scope.fileURL); - $scope.fileURL = null; - requestSender.localRedirectHome(simId); - }, function (err) { - throw new Error(inputFile + ': Error during upload ' + err); - }); - } - - function newSimFromSTL(inputFile) { - var url = $scope.fileURL; - var model = appState.setModelDefaults(appState.models.simulation, 'simulation'); - model.name = inputFile.name.substring(0, inputFile.name.indexOf('.')); - model.folder = fileManager.getActiveFolderPath(); - model.conductorFile = inputFile.name; - appState.newSimulation( - model, - function (data) { - $scope.isUploading = false; - upload(inputFile, data); - }, - function (err) { - throw new Error(inputFile + ': Error creating simulation ' + err); - } - ); - } - - }, - link: function(scope, element) { - $(element).on('show.bs.modal', function() { - $('#file-import').val(null); - scope.fileUploadError = ''; - scope.isUploading = false; - }); - scope.$on('$destroy', function() { - $(element).off(); - }); - }, - };}); - - -// elevations tab + vtk tab (or all in 1 tab?) -// A lot of this is 2d and could be extracted -SIREPO.app.directive('3dBuilder', function(appState, geometry, layoutService, panelState, plotting, utilities) { - return { - restrict: 'A', - scope: { - cfg: '<', - modelName: '@', - source: '=controller', - }, - templateUrl: '/static/html/3d-builder.html' + SIREPO.SOURCE_CACHE_KEY, - controller: function($scope) { - const ASPECT_RATIO = 1.0; - - const ELEVATIONS = {}; - for (const axis of SIREPO.GEOMETRY.GeometryUtils.BASIS().slice().reverse()) { - const e = new Elevation(axis); - ELEVATIONS[e.name] = e; - } - - // svg shapes - const LAYOUT_SHAPES = ['circle', 'ellipse', 'line', 'path', 'polygon', 'polyline', 'rect']; - - const SCREEN_INFO = { - x: { - length: $scope.width / 2 - }, - y: { - length: $scope.height / 2 - }, - }; - - const fitDomainPct = 1.01; - - let screenRect = null; - let selectedObject = null; - const objectScale = SIREPO.APP_SCHEMA.constants.objectScale || 1.0; - const invObjScale = 1.0 / objectScale; - - $scope.alignmentTools = SIREPO.APP_SCHEMA.constants.alignmentTools; - $scope.elevations = ELEVATIONS; - $scope.isClientOnly = true; - $scope.margin = {top: 20, right: 20, bottom: 45, left: 70}; - $scope.settings = appState.models.threeDBuilder; - $scope.snapGridSizes = appState.enumVals('SnapGridSize'); - $scope.width = $scope.height = 0; - - let didDrag = false; - let dragShape, dragInitialShape, zoom; - const dragDelta = {x: 0, y: 0}; - let draggedShape = null; - const axisScale = { - x: 1.0, - y: 1.0, - z: 1.0 - }; - const axes = { - x: layoutService.plotAxis($scope.margin, 'x', 'bottom', refresh), - y: layoutService.plotAxis($scope.margin, 'y', 'left', refresh), - }; - - const snapSettingsFields = [ - 'threeDBuilder.snapToGrid', - 'threeDBuilder.snapGridSize', - ]; - const settingsFields = [ - 'threeDBuilder.autoFit', - 'threeDBuilder.elevation', - ].concat(snapSettingsFields); - - function clearDragShadow() { - d3.selectAll('.vtk-object-layout-drag-shadow').remove(); - } - - function getElevation() { - return ELEVATIONS[$scope.settings.elevation]; - } - - function getLabAxis(dim) { - return getElevation().labAxis(dim); - } - - function resetDrag() { - didDrag = false; - hideShapeLocation(); - dragDelta.x = 0; - dragDelta.y = 0; - draggedShape = null; - selectedObject = null; - } - - function d3DragShapeEnd(shape) { - - function reset() { - resetDrag(); - d3.select(`.plot-viewport ${shapeSelectionId(shape, true)}`).call(updateShapeAttributes); - } - - const dragThreshold = 1e-3; - if (! didDrag || Math.abs(dragDelta.x) < dragThreshold && Math.abs(dragDelta.y) < dragThreshold) { - reset(); - return; - } - $scope.$applyAsync(() => { - if (isShapeInBounds(shape)) { - const o = $scope.source.getObject(shape.id); - if (! o) { - reset(); - return; - } - const e = getElevation(); - for (const dim of SIREPO.SCREEN_DIMS) { - o.center[SIREPO.GEOMETRY.GeometryUtils.axisIndex(e.labAxis(dim))] = invObjScale * shape.center[dim]; - } - $scope.source.saveObject(shape.id, reset); - } - else { - appState.cancelChanges($scope.modelName); - reset(); - } - }); - } - - function canDrag(dim) { - const a = d3.event.sourceEvent.shiftKey ? - (Math.abs(dragDelta.x) > Math.abs(dragDelta.y) ? 'x' : 'y') : - null; - return ! a || a === dim; - } - - function d3DragShape(shape) { - - if (! shape.draggable) { - return; - } - didDrag = true; - draggedShape = shape; - SIREPO.SCREEN_DIMS.forEach(dim => { - if (appState.models.threeDBuilder.snapToGrid) { - dragDelta[dim] = snap(shape, dim); - return; - } - dragDelta[dim] = canDrag(dim) ? d3.event[dim] : 0; - const numPixels = scaledPixels(dim, dragDelta[dim]); - shape[dim] = dragInitialShape[dim] + numPixels; - shape.center[dim] = dragInitialShape.center[dim] + numPixels; - }); - d3.select(shapeSelectionId(shape)).call(updateShapeAttributes); - showShapeLocation(shape); - //TODO(mvk): restore live update of virtual shapes - shape.runLinks().forEach(linkedShape => { - d3.select(shapeSelectionId(linkedShape)).call(updateShapeAttributes); - }); - } - - function shapeSelectionId(shape, includeHash=true) { - return `${(includeHash ? '#' : '')}shape-${shape.id}`; - } - - function d3DragShapeStart(shape) { - d3.event.sourceEvent.stopPropagation(); - dragInitialShape = appState.clone(shape); - showShapeLocation(shape); - } - - function drawObjects(elevation) { - const shapes = $scope.source.getShapes(elevation); - - // need to split the shapes up by type or the data will get mismatched - let layouts = {}; - LAYOUT_SHAPES.forEach(l=> { - layouts[l] = shapes - .filter(s => s.layoutShape === l) - .sort((s1, s2) => s2.z - s1.z) - .sort((s1, s2) => s1.draggable - s2.draggable); - }); - - for (let l in layouts) { - let ds = d3.select('.plot-viewport').selectAll(`${l}.vtk-object-layout-shape`) - .data(layouts[l]); - ds.exit().remove(); - // function must return a DOM object in the SVG namespace - ds.enter() - .append(d => { - return document.createElementNS('http://www.w3.org/2000/svg', d.layoutShape); - }) - .on('dblclick', editObject) - .on('dblclick.zoom', null) - .on('click', null); - ds.call(updateShapeAttributes); - ds.call(dragShape); - } - } - - function drawShapes() { - drawObjects(getElevation()); - } - - function editObject(shape) { - d3.event.stopPropagation(); - if (! shape.draggable) { - return; - } - $scope.$applyAsync(function() { - $scope.source.editObjectWithId(shape.id); - }); - } - - function formatObjectLength(val) { - return utilities.roundToPlaces(invObjScale * val, 4); - } - - function getShape(id) { - return $scope.shapes.filter(x => x.id === id)[0]; - } - - function hideShapeLocation() { - select('.focus-text').text(''); - } - - function isMouseInBounds(evt) { - d3.event = evt.event; - var p = d3.mouse(d3.select('.plot-viewport').node()); - d3.event = null; - return p[0] >= 0 && p[0] < $scope.width && p[1] >= 0 && p[1] < $scope.height - ? p - : null; - } - - function isShapeInBounds(shape) { - if (! $scope.cfg.fixedDomain) { - return true; - } - /* - var vAxis = shape.elev === ELEVATIONS.front ? axes.y : axes.z; - var bounds = { - top: shape.y, - bottom: shape.y - shape.height, - left: shape.x, - right: shape.x + shape.width, - }; - if (bounds.right < axes.x.domain[0] || bounds.left > axes.x.domain[1] - || bounds.top < vAxis.domain[0] || bounds.bottom > vAxis.domain[1]) { - return false; - } - - */ - return true; - } - - function refresh() { - if (! axes.x.domain) { - return; - } - if (layoutService.plotAxis.allowUpdates) { - var elementWidth = parseInt(select('.workspace').style('width')); - if (isNaN(elementWidth)) { - return; - } - [$scope.height, $scope.width] = plotting.constrainFullscreenSize($scope, elementWidth, ASPECT_RATIO); - SCREEN_INFO.x.length = $scope.width; - SCREEN_INFO.y.length = $scope.height; - - select('svg') - .attr('width', $scope.width + $scope.margin.left + $scope.margin.right) - .attr('height', $scope.plotHeight()); - axes.x.scale.range([0, $scope.width]); - axes.y.scale.range([$scope.height, 0]); - axes.x.grid.tickSize(-$scope.height); - axes.y.grid.tickSize(-$scope.width); - } - if (plotting.trimDomain(axes.x.scale, axes.x.domain)) { - select('.overlay').attr('class', 'overlay mouse-zoom'); - axes.y.scale.domain(axes.y.domain); - } - else { - select('.overlay').attr('class', 'overlay mouse-move-ew'); - } - - resetZoom(); - select('.plot-viewport').call(zoom); - $.each(axes, function(dim, axis) { - var d = axes[dim].scale.domain(); - var r = axes[dim].scale.range(); - axisScale[dim] = Math.abs((d[1] - d[0]) / (r[1] - r[0])); - - axis.updateLabelAndTicks({ - width: $scope.width, - height: $scope.height, - }, select); - axis.grid.ticks( - $scope.settings.snapToGrid ? - Math.round(Math.abs(d[1] - d[0]) / ($scope.settings.snapGridSize * objectScale)) : - axis.tickCount - ); - select('.' + dim + '.axis.grid').call(axis.grid); - }); - - screenRect = geometry.rect( - geometry.point(), - geometry.point($scope.width, $scope.height, 0) - ); - - drawShapes(); - } - - function replot(doFit=false) { - const b = $scope.source.shapeBounds(getElevation()); - const newDomain = $scope.cfg.initDomian; - SIREPO.SCREEN_DIMS.forEach(dim => { - const axis = axes[dim]; - const bd = b[dim]; - const nd = newDomain[dim]; - axis.domain = $scope.cfg.fullZoom ? [-Infinity, Infinity] : nd; - if (($scope.settings.autoFit || doFit) && bd[0] !== bd[1]) { - nd[0] = fitDomainPct * bd[0]; - nd[1] = fitDomainPct * bd[1]; - // center - const d = (nd[1] - nd[0]) / 2 - (bd[1] - bd[0]) / 2; - nd[0] -= d; - nd[1] -= d; - } - axis.scale.domain(newDomain[dim]); - }); - $scope.resize(); - } - - function resetZoom() { - zoom = axes.x.createZoom().y(axes.y.scale); - } - - function scaledPixels(dim, pixels) { - const dom = axes[dim].scale.domain(); - return pixels * SIREPO.SCREEN_INFO[dim].direction * (dom[1] - dom[0]) / SCREEN_INFO[dim].length; - } - - function select(selector) { - var e = d3.select($scope.element); - return selector ? e.select(selector) : e; - } - - function selectObject(d) { - //TODO(mvk): allow using shift to select more than one (for alignment etc.) - if (! selectedObject || selectedObject.id !== d.id ) { - selectedObject = d; - } - else { - selectedObject = null; - } - } - - function shapeColor(hexColor, alpha) { - var comp = plotting.colorsFromHexString(hexColor); - return 'rgb(' + comp[0] + ', ' + comp[1] + ', ' + comp[2] + ', ' + (alpha || 1.0) + ')'; - } - - function showShapeLocation(shape) { - select('.focus-text').text( - 'Center: ' + - formatObjectLength(shape.center.x) + ', ' + - formatObjectLength(shape.center.y) + ', ' + - formatObjectLength(shape.center.z) - ); - } - - function snap(shape, dim) { - function roundUnits(val, unit) { - return unit * Math.round(val / unit); - } - - if (! canDrag(dim)) { - return 0; - } - - const g = parseFloat($scope.settings.snapGridSize) * objectScale; - const ctr = dragInitialShape.center[dim]; - const offset = axes[dim].scale(roundUnits(ctr, g)) - axes[dim].scale(ctr); - const gridSpacing = Math.abs(axes[dim].scale(2 * g) - axes[dim].scale(g)); - const gridUnits = roundUnits(d3.event[dim], gridSpacing); - const numPixels = scaledPixels(dim, gridUnits + offset); - shape[dim] = roundUnits(dragInitialShape[dim] + numPixels, g); - shape.center[dim] = roundUnits(ctr + numPixels, g); - return Math.round(gridUnits + offset); - } - - // called when dragging a new object, not an existing object - function updateDragShadow(o, p) { - let r = d3.select('.plot-viewport rect.vtk-object-layout-drag-shadow'); - if (r.empty()) { - const s = $scope.source.viewShadow(o).getView(getElevation()); - r = d3.select('.plot-viewport').append('rect') - .attr('class', 'vtk-object-layout-shape vtk-object-layout-drag-shadow') - .attr('width', shapeSize(s, 'x')) - .attr('height', shapeSize(s, 'y')); - } - //showShapeLocation(shape); - r.attr('x', p[0]).attr('y', p[1]); - } - - function shapeOrigin(shape, dim) { - return axes[dim].scale( - shape.center[dim] - SIREPO.SCREEN_INFO[dim].direction * shape.size[dim] / 2 - ); - } - - function shapePoints(shape) { - //TODO(mvk): apply transforms to dx, dy - const [dx, dy] = shape.id === (draggedShape || {}).id ? [dragDelta.x, dragDelta.y] : [0, 0]; - let pts = ''; - for (const p of shape.points) { - pts += `${dx + axes.x.scale(p.x)},${dy + axes.y.scale(p.y)} `; - } - return pts; - } - - function linePoints(shape) { - if (! shape.line || getElevation().coordPlane !== shape.coordPlane) { - return null; - } - - const lp = shape.line.points; - const labX = getElevation().labAxis('x'); - const labY = getElevation().labAxis('y'); - - // same points in this coord plane - if (lp[0][labX] === lp[1][labX] && lp[0][labY] === lp[1][labY]) { - return null; - } - - var scaledLine = geometry.lineFromArr( - lp.map(function (p) { - var sp = []; - SIREPO.SCREEN_DIMS.forEach(function (dim) { - sp.push(axes[dim].scale(p[getElevation().labAxis(dim)])); - }); - return geometry.pointFromArr(sp); - })); - - var pts = screenRect.boundaryIntersectionsWithLine(scaledLine); - return pts; - } - - function shapeSize(shape, dim) { - let c = shape.center[dim] || 0; - let s = shape.size[dim] || 0; - return Math.abs(axes[dim].scale(c + s / 2) - axes[dim].scale(c - s / 2)); - } - - //TODO(mvk): set only those attributes that pertain to each svg shape - function updateShapeAttributes(selection) { - selection - .attr('class', 'vtk-object-layout-shape') - .classed('vtk-object-layout-shape-selected', d => d.id === (selectedObject || {}).id) - .classed('vtk-object-layout-shape-undraggable', d => ! d.draggable) - .attr('id', d => shapeSelectionId(d, false)) - .attr('href', d => d.href ? `#${d.href}` : '') - .attr('points', d => $.isEmptyObject(d.points || {}) ? null : shapePoints(d)) - .attr('x', d => shapeOrigin(d, 'x') - (d.outlineOffset || 0)) - .attr('y', d => shapeOrigin(d, 'y') - (d.outlineOffset || 0)) - .attr('x1', d => { - const pts = linePoints(d); - return pts ? (pts[0] ? pts[0].coords()[0] : 0) : 0; - }) - .attr('x2', d => { - const pts = linePoints(d); - return pts ? (pts[1] ? pts[1].coords()[0] : 0) : 0; - }) - .attr('y1', d => { - const pts = linePoints(d); - return pts ? (pts[0] ? pts[0].coords()[1] : 0) : 0; - }) - .attr('y2', d => { - const pts = linePoints(d); - return pts ? (pts[1] ? pts[1].coords()[1] : 0) : 0; - }) - .attr('marker-end', d => { - if (d.endMark && d.endMark.length) { - return `url(#${d.endMark})`; - } - }) - .attr('marker-start', d => { - if (d.endMark && d.endMark.length) { - return `url(#${d.endMark})`; - } - }) - .attr('width', d => shapeSize(d, 'x') + 2 * (d.outlineOffset || 0)) - .attr('height', d => shapeSize(d, 'y') + 2 * (d.outlineOffset || 0)) - .attr('style', d => { - if (d.color) { - const a = d.alpha === 0 ? 0 : (d.alpha || 1.0); - const fill = `fill:${(d.fillStyle ? shapeColor(d.color, a) : 'none')}`; - return `${fill}; stroke: ${shapeColor(d.color)}; stroke-width: ${d.strokeWidth || 1.0}`; - } - }) - .attr('stroke-dasharray', d => d.strokeStyle === 'dashed' ? (d.dashes || "5,5") : ""); - let tooltip = selection.select('title'); - if (tooltip.empty()) { - tooltip = selection.append('title'); - } - tooltip.text(function(d) { - const ctr = d.getCenterCoords().map(function (c) { - return utilities.roundToPlaces(c * invObjScale, 2); - }); - const sz = d.getSizeCoords().map(function (c) { - return utilities.roundToPlaces(c * invObjScale, 2); - }); - return `${d.name} center : ${ctr} size: ${sz}`; - }); - } - - $scope.destroy = () => { - if (zoom) { - zoom.on('zoom', null); - } - $('.plot-viewport').off(); - }; - - $scope.dragMove = (o, evt) => { - const p = isMouseInBounds(evt); - if (p) { - d3.select('.sr-drag-clone').attr('class', 'sr-drag-clone sr-drag-clone-hidden'); - updateDragShadow(o, p); - } - else { - clearDragShadow(); - d3.select('.sr-drag-clone').attr('class', 'sr-drag-clone'); - hideShapeLocation(); - } - }; - - // called when dropping new objects, not existing - $scope.dropSuccess = (o, evt) => { - clearDragShadow(); - const p = isMouseInBounds(evt); - if (p) { - const labXIdx = geometry.basis.indexOf(getLabAxis('x')); - const labYIdx = geometry.basis.indexOf(getLabAxis('y')); - const ctr = [0, 0, 0]; - ctr[labXIdx] = axes.x.scale.invert(p[0]); - ctr[labYIdx] = axes.y.scale.invert(p[1]); - o.center = ctr.map(x => x * invObjScale); - $scope.$emit('layout.object.dropped', o); - drawShapes(); - } - }; - - $scope.editObject = $scope.source.editObject; - - $scope.fitToShapes = () => { - replot(true); - }; - - $scope.getElevation = getElevation; - - $scope.getObjects = () => { - return (appState.models[$scope.modelName] || {}).objects; - }; - - $scope.init = () => { - $scope.shapes = $scope.source.getShapes(getElevation()); - - $scope.$on($scope.modelName + '.changed', function(e, name) { - $scope.shapes = $scope.source.getShapes(); - drawShapes(); - replot(); - }); - - select('svg').attr('height', plotting.initialHeight($scope)); - - $.each(axes, function(dim, axis) { - axis.init(); - axis.grid = axis.createAxis(); - }); - resetZoom(); - dragShape = d3.behavior.drag() - .origin(function(d) { return d; }) - .on('drag', d3DragShape) - .on('dragstart', d3DragShapeStart) - .on('dragend', d3DragShapeEnd); - SIREPO.SCREEN_DIMS.forEach(dim => { - axes[dim].parseLabelAndUnits(`${getLabAxis(dim)} [m]`); - }); - replot(); - }; - - $scope.isDropEnabled = () => $scope.source.isDropEnabled(); - - $scope.plotHeight = () => $scope.plotOffset() + $scope.margin.top + $scope.margin.bottom; - - $scope.plotOffset = () => $scope.height; - - $scope.resize = () => { - if (select().empty()) { - return; - } - refresh(); - }; - - $scope.setElevation = elev => { - $scope.settings.elevation = elev; - SIREPO.SCREEN_DIMS.forEach(dim => { - axes[dim].parseLabelAndUnits(`${getLabAxis(dim)} [m]`); - }); - replot(); - }; - - appState.watchModelFields($scope, settingsFields, () => { - appState.saveChanges('threeDBuilder'); - }); - appState.watchModelFields($scope, snapSettingsFields, refresh); - - $scope.$on('shapes.loaded', drawShapes); - - $scope.$on('shape.locked', (e, locks) => { - let doRefresh = false; - for (const l of locks) { - const s = getShape(l.id); - if (s) { - doRefresh = true; - s.draggable = ! l.doLock; - } - } - if (doRefresh) { - refresh(); - } - }); - - }, - link: function link(scope, element) { - plotting.linkPlot(scope, element); - }, - }; -}); - -SIREPO.app.directive('objectTable', function(appState, $rootScope) { - return { - restrict: 'A', - scope: { - elevation: '=', - modelName: '@', - overlayButtons: '=', - source: '=', - }, - template: ` -
-
Objects
-
-
- - - - - - - - -
- {{ lockTitle(o) }} - {{ lockTitle(o) }} - - - {{ o.name }} - -
-
- - - - - - - -
-
-
-
-
-
- - `, - controller: function($scope) { - $scope.expanded = {}; - $scope.fields = ['objects']; - $scope.locked = {}; - $scope.unlockable = {}; - - const isInGroup = $scope.source.isInGroup; - const getGroup = $scope.source.getGroup; - const getMemberObjects = $scope.source.getMemberObjects; - let areObjectsUnlockable = appState.models.simulation.areObjectsUnlockable; - - function arrange(objects) { - - const arranged = []; - - function addGroup(o) { - const p = getGroup(o); - if (p && ! arranged.includes(p)) { - return; - } - if (! arranged.includes(o)) { - arranged.push(o); - } - for (const m of getMemberObjects(o)) { - if ($scope.isGroup(m)) { - addGroup(m); - } - else { - arranged.push(m); - } - } - } - - for (const o of objects) { - if (arranged.includes(o)) { - continue; - } - if (! isInGroup(o)) { - arranged.push(o); - } - if ($scope.isGroup(o)) { - addGroup(o); - } - } - return arranged; - } - - function init() { - if (areObjectsUnlockable === undefined) { - areObjectsUnlockable = true; - } - for (const o of $scope.getObjects()) { - $scope.expanded[o.id] = true; - $scope.unlockable[o.id] = areObjectsUnlockable; - $scope.locked[o.id] = ! areObjectsUnlockable; - - } - } - - function setLocked(o, doLock) { - $scope.locked[o.id] = doLock; - let ids = [ - { - id: o.id, - doLock: doLock - }, - ]; - if ($scope.isGroup(o)) { - getMemberObjects(o).forEach(x => { - ids = ids.concat(setLocked(x, doLock)); - if (areObjectsUnlockable) { - $scope.unlockable[x.id] = ! doLock; - } - }); - } - return ids; - } - - $scope.align = (o, alignType) => { - $scope.source.align(o, alignType, $scope.elevation.labAxisIndices()); - }; - - $scope.areAllGroupsExpanded = o => { - if (! isInGroup(o)) { - return true; - } - const p = getGroup(o); - if (! $scope.expanded[p.id]) { - return false; - } - return $scope.areAllGroupsExpanded(p); - }; - - $scope.copyObject = $scope.source.copyObject; - - $scope.deleteObject = $scope.source.deleteObject; - - $scope.editObject = $scope.source.editObject; - - $scope.getObjects = () => { - return arrange((appState.models[$scope.modelName] || {}).objects); - }; - - $scope.isAlignDisabled = o => $scope.locked[o.id] || ! $scope.isGroup(o) || getMemberObjects(o).length < 2; - - $scope.isGroup = $scope.source.isGroup; - - $scope.isMoveDisabled = (direction, o) => { - if ($scope.locked[o.id]) { - return true; - } - const objects = isInGroup(o) ? - getMemberObjects(getGroup(o)) : - $scope.getObjects().filter(x => ! isInGroup(x)); - let i = objects.indexOf(o); - return direction === -1 ? i === 0 : i === objects.length - 1; - }; - - $scope.lockTitle = o => { - if (! areObjectsUnlockable) { - return 'designer is read-only for this magnet'; - } - if (! $scope.unlockable[o.id]) { - return 'cannot unlock'; - } - return `click to ${$scope.locked[o.id] ? 'unlock' : 'lock'}`; - }; - - $scope.moveObject = $scope.source.moveObject; - - $scope.nestLevel = o => { - let n = 0; - if (isInGroup(o)) { - n += (1 + $scope.nestLevel(getGroup(o))); - } - return n; - }; - - $scope.toggleExpand = o => { - $scope.expanded[o.id] = ! $scope.expanded[o.id]; - }; - - $scope.toggleLock = o => { - if (! $scope.unlockable[o.id]) { - return; - } - $rootScope.$broadcast('shape.locked', setLocked(o, ! $scope.locked[o.id])); - }; - - init(); - }, - }; -}); - SIREPO.app.directive('vtkAxes', function(geometry, layoutService, plotting) { return { restrict: 'A', @@ -3177,7 +1904,7 @@ SIREPO.app.directive('vtkAxes', function(geometry, layoutService, plotting) { }); // General-purpose vtk display -SIREPO.app.directive('vtkDisplay', function(appState, panelState, utilities, $document, $window) { +SIREPO.app.directive('vtkDisplay', function(appState, utilities, $window) { return { restrict: 'A', @@ -3186,7 +1913,6 @@ SIREPO.app.directive('vtkDisplay', function(appState, panelState, utilities, $do axisObj: '<', enableAxes: '=', enableSelection: '=', - eventHandlers: '<', modelName: '@', resetDirection: '@', resetSide: '@', @@ -3194,41 +1920,25 @@ SIREPO.app.directive('vtkDisplay', function(appState, panelState, utilities, $do }, templateUrl: '/static/html/vtk-display.html' + SIREPO.SOURCE_CACHE_KEY, controller: function($scope, $element) { - $scope.GeometryUtils = SIREPO.GEOMETRY.GeometryUtils; - $scope.VTKUtils = VTKUtils; - $scope.markerState = { - enabled: true, - }; - $scope.modeText = {}; $scope.isOrtho = false; $scope.selection = null; - - let didPan = false; - let hasBodyEvt = false; - let hdlrs = {}; - let isDragging = false; - let isPointerUp = true; - const canvasHolder = $($element).find('.vtk-canvas-holder').eq(0); + let isPointerUp = true; // supplement or override these event handlers - let eventHandlers = { + const eventHandlers = { onpointerdown: function (evt) { - isDragging = false; isPointerUp = false; }, onpointermove: function (evt) { if (isPointerUp) { return; } - isDragging = true; - didPan = didPan || evt.shiftKey; $scope.vtkScene.viewSide = null; utilities.debounce(refresh, 100)(); }, onpointerup: function (evt) { - isDragging = false; isPointerUp = true; refresh(); }, @@ -3245,51 +1955,35 @@ SIREPO.app.directive('vtkDisplay', function(appState, panelState, utilities, $do $scope.$apply(); } - $scope.init = function() { - const rw = angular.element($($element).find('.vtk-canvas-holder'))[0]; - const body = angular.element($($document).find('body'))[0]; - const view = angular.element($($document).find('.sr-view-content'))[0]; - hdlrs = $scope.eventHandlers || {}; - - // vtk adds keypress event listeners to the BODY of the entire document, not the render - // container. - hasBodyEvt = Object.keys(hdlrs).some(function (e) { - return ['keypress', 'keydown', 'keyup'].includes(e); - }); - if (hasBodyEvt) { - const bodyAddEvtLsnr = body.addEventListener; - const bodyRmEvtLsnr = body.removeEventListener; - body.addEventListener = (type, listener, opts) => { - bodyAddEvtLsnr(type, hdlrs[type] ? hdlrs[type] : listener, opts); - }; - // seem to need to do this so listeners get removed correctly - body.removeEventListener = (type, listener, opts) => { - bodyRmEvtLsnr(type, listener, opts); - }; + function refresh() { + if ($scope.axisObj) { + $scope.$broadcast('axes.refresh', $scope.axisObj); } + } - $scope.vtkScene = new VTKScene(rw, $scope.resetSide, $scope.resetDirection); - - // double click handled separately - rw.addEventListener('dblclick', function (evt) { - ondblclick(evt); - if (hdlrs.ondblclick) { - hdlrs.ondblclick(evt); + $scope.canvasGeometry = function() { + return { + pos: canvasHolder.position(), + size: { + width: Math.max(0, canvasHolder.width()), + height: Math.max(0, canvasHolder.height()), } - }); - Object.keys(eventHandlers).forEach(function (k) { - const f = function (evt) { - eventHandlers[k](evt); - if (hdlrs[k]) { - hdlrs[k](evt); - } - }; + }; + }; + + $scope.init = function() { + const rw = canvasHolder[0]; + $scope.vtkScene = new VTKScene(rw, $scope.resetSide, $scope.resetDirection); + // all listeners need to be cleaned up in $destroy + rw.addEventListener('dblclick', ondblclick); + for (const k in eventHandlers) { + const f = eventHandlers[k]; if (k == 'onpointermove') { - view[k] = f; - return; + $('.sr-view-content')[0][k] = f; + continue; } rw[k] = f; - }); + } // remove global VTK key listeners for (const n of ['KeyPress', 'KeyDown', 'KeyUp']) { document.removeEventListener( @@ -3301,16 +1995,6 @@ SIREPO.app.directive('vtkDisplay', function(appState, panelState, utilities, $do refresh(); }; - $scope.canvasGeometry = function() { - return { - pos: $(canvasHolder).position(), - size: { - width: Math.max(0, $(canvasHolder).width()), - height: Math.max(0, $(canvasHolder).height()), - } - }; - }; - $scope.rotate = angle => { $scope.vtkScene.rotate(angle); refresh(); @@ -3329,17 +2013,19 @@ SIREPO.app.directive('vtkDisplay', function(appState, panelState, utilities, $do }; $scope.$on('$destroy', function() { + const rw = canvasHolder[0]; + rw.removeEventListener('dblclick', ondblclick); + for (const k in eventHandlers) { + if (k == 'onpointermove') { + $('.sr-view-content')[0][k] = null; + continue; + } + rw[k] = null; + } $element.off(); $($window).off('resize', asyncRefresh); $scope.vtkScene.teardown(); }); - - function refresh() { - if ($scope.axisObj) { - $scope.$broadcast('axes.refresh', $scope.axisObj); - } - } - $scope.$on('vtk.selected', function (e, d) { $scope.$applyAsync(() => { $scope.selection = d; @@ -3353,51 +2039,26 @@ SIREPO.app.directive('vtkDisplay', function(appState, panelState, utilities, $do $scope.vtkScene.setBgColor(appState.models[$scope.modelName].bgColor || '#ffffff'); $($element).find('.vtk-load-indicator img').css('display', 'none'); }); - $scope.init(); - - // ensure the axes update on each resize event - $($window).resize(asyncRefresh); - $scope.$on('sr-window-resize', () => { // ensure full-screen and exit full-screen resize the renderer $scope.vtkScene.fsRenderer.resize(); refresh(); }); - }, - }; -}); - -SIREPO.app.factory('vtkUtils', function() { - var self = {}; - // Converts vtk colors ranging from 0 -> 255 to 0.0 -> 1.0 - // can't map, because we will still have a UINT8 array - self.floatToRGB = f => { - const rgb = new window.Uint8Array(f.length); - for (let i = 0; i < rgb.length; ++i) { - rgb[i] = Math.floor(255 * f[i]); - } - return rgb; + $scope.init(); + // ensure the axes update on each resize event + $($window).resize(asyncRefresh); + }, }; - - return self; }); SIREPO.VTK = { - ActorBundle: ActorBundle, - BoxBundle: BoxBundle, CoordMapper: CoordMapper, CuboidViews: CuboidViews, CylinderViews: CylinderViews, ExtrudedPolyViews: ExtrudedPolyViews, - LineBundle: LineBundle, - ObjectViews: ObjectViews, - PlaneBundle: PlaneBundle, RacetrackViews: RacetrackViews, - SphereBundle: SphereBundle, SphereViews: SphereViews, - VectorFieldBundle: VectorFieldBundle, ViewPortBox: ViewPortBox, VTKUtils: VTKUtils, - VTKVectorFormula: VTKVectorFormula, }; diff --git a/sirepo/package_data/static/js/sirepo-plotting.js b/sirepo/package_data/static/js/sirepo-plotting.js index aa2d552492..5a22b9b577 100644 --- a/sirepo/package_data/static/js/sirepo-plotting.js +++ b/sirepo/package_data/static/js/sirepo-plotting.js @@ -7,6 +7,7 @@ SIREPO.PLOTTING_YMIN_ZERO = true; SIREPO.DEFAULT_COLOR_MAP = 'viridis'; SIREPO.SCREEN_DIMS = ['x', 'y']; SIREPO.SCREEN_INFO = {x: { direction: 1 }, y: { direction: -1 }}; +SIREPO.ZERO_ARR = [0, 0, 0]; class PlottingUtils { static COLOR_MAP() { @@ -1114,8 +1115,23 @@ SIREPO.app.factory('plotting', function(appState, frameCache, panelState, utilit // ensures the axis domain fits in the fullDomain // returns true if size is reset to full trimDomain: function(axisScale, fullDomain) { - var dom = axisScale.domain(); - var zoomSize = dom[1] - dom[0]; + const dom = axisScale.domain(); + if (fullDomain[0] > fullDomain[1]) { + const zoomSize = dom[0] - dom[1]; + + if (zoomSize >= (fullDomain[0] - fullDomain[1])) { + axisScale.domain(fullDomain); + return true; + } + if (dom[1] < fullDomain[1]) { + axisScale.domain([zoomSize + fullDomain[1], fullDomain[1]]); + } + if (dom[0] > fullDomain[0]) { + axisScale.domain([fullDomain[0], fullDomain[0] - zoomSize]); + } + return false; + } + const zoomSize = dom[1] - dom[0]; if (zoomSize >= (fullDomain[1] - fullDomain[0])) { axisScale.domain(fullDomain); diff --git a/sirepo/package_data/static/js/sirepo-utils.js b/sirepo/package_data/static/js/sirepo-utils.js index 25e7bfea4e..aa4496d501 100644 --- a/sirepo/package_data/static/js/sirepo-utils.js +++ b/sirepo/package_data/static/js/sirepo-utils.js @@ -148,28 +148,6 @@ class SirepoUtils { return SirepoUtils.arrayMin(arr.map(x => x[i])); } - static reshape(arr, dims) { - if (dims.length === 0) { - return arr; - } - const a = Array.from(arr).slice(); - if (dims.length === 1) { - return a; - } - const n = dims.reduce((p, c) => p * c, 1); - if (a.length !== n) { - throw new Error(`Product of shape dimensions must equal array length: ${a.length} != ${n}`); - } - const b = []; - const d = dims[0]; - const m = a.length / d; - for (let i = 0; i < d; ++i) { - const s = a.slice(m * i, m * (i + 1)); - b.push(SirepoUtils.reshape(s, dims.slice(1))); - } - return b; - } - static wordSplits(s) { const wds = s.split(/(\s+)/); return wds.map(function (value, index) { diff --git a/sirepo/package_data/static/js/warpvnd.js b/sirepo/package_data/static/js/warpvnd.js index f5f60091da..34798f27ec 100644 --- a/sirepo/package_data/static/js/warpvnd.js +++ b/sirepo/package_data/static/js/warpvnd.js @@ -3308,44 +3308,6 @@ SIREPO.app.service('warpVTKService', function(vtkPlotting, geometry) { var zeroVoltsColor = [243.0/255.0, 212.0/255.0, 200.0/255.0]; var voltsColor = [105.0/255.0, 146.0/255.0, 255.0/255.0]; - this.initScene = function (coordMapper, renderer) { - - // the emitter plane - startPlaneBundle = coordMapper.buildPlane(); - startPlaneBundle.actor.getProperty().setColor(zeroVoltsColor[0], zeroVoltsColor[1], zeroVoltsColor[2]); - startPlaneBundle.actor.getProperty().setLighting(false); - renderer.addActor(startPlaneBundle.actor); - - // the collector plane - endPlaneBundle = coordMapper.buildPlane(); - endPlaneBundle.actor.getProperty().setColor(voltsColor[0], voltsColor[1], voltsColor[2]); - endPlaneBundle.actor.getProperty().setLighting(false); - renderer.addActor(endPlaneBundle.actor); - - // a box around the elements, for visual clarity - outlineBundle = coordMapper.buildBox(); - outlineBundle.actor.getProperty().setColor(1, 1, 1); - outlineBundle.actor.getProperty().setEdgeVisibility(true); - outlineBundle.actor.getProperty().setEdgeColor(0, 0, 0); - outlineBundle.actor.getProperty().setFrontfaceCulling(true); - outlineBundle.actor.getProperty().setLighting(false); - renderer.addActor(outlineBundle.actor); - - /* - orientationMarker = vtk.Interaction.Widgets.vtkOrientationMarkerWidget.newInstance({ - actor: vtk.Rendering.Core.vtkAxesActor.newInstance(), - interactor: renderWindow.getInteractor() - }); - orientationMarker.setEnabled(true); - orientationMarker.setViewportCorner( - vtk.Interaction.Widgets.vtkOrientationMarkerWidget.Corners.TOP_RIGHT - ); - orientationMarker.setViewportSize(0.08); - orientationMarker.setMinPixelSize(100); - orientationMarker.setMaxPixelSize(300); - */ - }; - this.updateScene = function (coordMapper, axisInfo) { coordMapper.setPlane(startPlaneBundle.source, @@ -3725,8 +3687,6 @@ SIREPO.app.directive('particle3d', function(appState, errorService, frameCache, } }; - //warpVTKService.initScene(coordMapper, renderer); - // the emitter plane startPlaneBundle = coordMapper.buildPlane(); startPlaneBundle.actor.getProperty().setColor(zeroVoltsColor[0], zeroVoltsColor[1], zeroVoltsColor[2]); @@ -4790,3 +4750,141 @@ SIREPO.app.directive('particle3d', function(appState, errorService, frameCache, }, }; }); + +SIREPO.app.directive('stlFileChooser', function(validationService, vtkPlotting) { + return { + restrict: 'A', + scope: { + description: '=', + url: '=', + inputFile: '=', + model: '=', + require: '<', + title: '@', + }, + template: ` +
+
+ `, + controller: function($scope) { + $scope.validate = function (file) { + $scope.url = URL.createObjectURL(file); + return vtkPlotting.isSTLUrlValid($scope.url).then(function (ok) { + return ok; + }); + }; + $scope.validationError = ''; + }, + link: function(scope, element, attrs) { + + }, + }; +}); + +SIREPO.app.directive('stlImportDialog', function(appState, fileManager, fileUpload, vtkPlotting, requestSender) { + return { + restrict: 'A', + scope: { + description: '@', + title: '@', + }, + template: ` + + `, + controller: function($scope) { + $scope.inputFile = null; + $scope.fileURL = null; + $scope.isMissingImportFile = function() { + return ! $scope.inputFile; + }; + $scope.fileUploadError = ''; + $scope.isUploading = false; + $scope.title = $scope.title || 'Import STL File'; + $scope.description = $scope.description || 'Select File'; + + $scope.importStlFile = function(inputFile) { + if (! inputFile) { + return; + } + newSimFromSTL(inputFile); + }; + + function upload(inputFile, data) { + var simId = data.models.simulation.simulationId; + fileUpload.uploadFileToUrl( + inputFile, + $scope.isConfirming + ? { + confirm: $scope.isConfirming, + } + : null, + requestSender.formatUrl( + 'uploadLibFile', + { + '': simId, + '': SIREPO.APP_SCHEMA.simulationType, + '': vtkPlotting.stlFileType, + }), + function(d) { + $('#simulation-import').modal('hide'); + $scope.inputFile = null; + URL.revokeObjectURL($scope.fileURL); + $scope.fileURL = null; + requestSender.localRedirectHome(simId); + }, function (err) { + throw new Error(inputFile + ': Error during upload ' + err); + }); + } + + function newSimFromSTL(inputFile) { + var url = $scope.fileURL; + var model = appState.setModelDefaults(appState.models.simulation, 'simulation'); + model.name = inputFile.name.substring(0, inputFile.name.indexOf('.')); + model.folder = fileManager.getActiveFolderPath(); + model.conductorFile = inputFile.name; + appState.newSimulation( + model, + function (data) { + $scope.isUploading = false; + upload(inputFile, data); + }, + function (err) { + throw new Error(inputFile + ': Error creating simulation ' + err); + } + ); + } + + }, + link: function(scope, element) { + $(element).on('show.bs.modal', function() { + $('#file-import').val(null); + scope.fileUploadError = ''; + scope.isUploading = false; + }); + scope.$on('$destroy', function() { + $(element).off(); + }); + }, + }; +}); diff --git a/sirepo/package_data/static/json/openmc-schema.json b/sirepo/package_data/static/json/openmc-schema.json index 3224936881..a39e46a251 100644 --- a/sirepo/package_data/static/json/openmc-schema.json +++ b/sirepo/package_data/static/json/openmc-schema.json @@ -262,6 +262,10 @@ "x", "y", "z" + ], + "outlineAnimation": [ + "tally", + "axis" ] }, "localRoutes": { @@ -356,7 +360,7 @@ "density": ["Density", "Float"], "density_units": ["Density units", "DensityUnits", "g/cm3"], "depletable": ["Depletable", "Boolean", "0"], - "standardType": ["Standard Material", "StandardMaterial", "None"], + "standardType": ["Standard material", "StandardMaterial", "None"], "volume": ["Volume [$cm^3$] (Optional)", "OptionalFloat"], "components": ["", "MaterialComponents"] }, @@ -913,7 +917,7 @@ } ]], "name": ["Name", "String", "Steel, Stainless 316"], - "standardType": ["Standard Material", "StandardMaterial", "materialStandardSteelStainless316"] + "standardType": ["Standard material", "StandardMaterial", "materialStandardSteelStainless316"] }, "materialValue": { "value": ["Material", "MaterialValue"] @@ -949,21 +953,21 @@ "openmcAnimation": { "aspect": ["Aspect", "TallyAspect", "mean"], "bgColor": ["Background color", "Color", "#fff9ed"], - "colorMap": ["Tally Color Map", "ColorMap", "viridis"], + "colorMap": ["Tally color map", "ColorMap", "viridis"], "energyRangeSum": ["Sum over energies [MeV]", "EnergyRange", [0, 0]], - "numSampleSourceParticles": ["Source Particles to Display", "Integer", 10, "", 0, 100], + "numSampleSourceParticles": ["Source particles to display", "Integer", 10, "", 0, 100], "opacity": ["Global alpha", "Opacity", 1.0], "showEdges": ["Show edges", "Boolean", "0"], "showMarker": ["Show axis marker", "Boolean", "1"], - "showSources": ["Show Sources", "Boolean", "0"], - "sourceColorMap": ["Source Energy Color Map", "ColorMap", "jet"], + "showSources": ["Show sources", "Boolean", "0"], + "sourceColorMap": ["Source energy color map", "ColorMap", "jet"], "tally": ["Tally", "PlotTallyList"], "thresholds": ["Threshold", "Threshold", [0, 0], "Tally cells with a value outside this range will not be displayed"], - "colorRange": ["Color Range", "Threshold", [0, 0]], + "colorRange": ["Color range", "Threshold", [0, 0]], "score": ["Score", "PlotScoreList"], "sourceNormalization": ["Normalize to particle count", "Float", 1e12, "Per-particle tally scores are normalized to this many source particles (a factor of {{ appState.models.openmcAnimation.sourceNormalization / appState.models.settings.particles }})", 1.0], "isEnergySelected": ["", "Boolean", "0"], - "jobRunMode": ["Execution Mode", "JobRunMode", "parallel"], + "jobRunMode": ["Execution mode", "JobRunMode", "parallel"], "tasksPerNode": ["Tasks per node", "Integer", 8, "", 1], "sbatchHours": ["Hours", "Float", 0.4], "sbatchNodes": ["Nodes", "Integer", 32, "", 1], @@ -973,6 +977,10 @@ "ompThreads": ["OMP threads", "Integer", 16, "", 1], "selectedVolumes": ["", "SelectedTallyVolumes", ""] }, + "outlineAnimation": { + "tally": ["", "PlotTallyList"], + "axis": ["", "Axis", "y"] + }, "particle": { "value": ["Particle", "FilterParticle"] }, @@ -1013,7 +1021,7 @@ }, "settings": { "batches": ["Number of batches to simulate", "Integer", 1, "", 1], - "eigenvalueHistory": ["Eigenvalue History", "Integer", 5], + "eigenvalueHistory": ["Eigenvalue history", "Integer", 5], "inactive": ["Number of inactive batches", "Integer", 0, "These batches will be used to converge the eigenvalue before calculations begin"], "particles": ["Number of particles per generation", "Integer", 5000], "photon_transport": ["Photon transport", "Boolean", "0", "Simulate the passage of photons through matter"], @@ -1034,7 +1042,7 @@ "space": ["Spatial distribution", "Spatial", {"_type": "None"}], "strength": ["Strength", "Float", 1], "time": ["Time distribution", "Univariate", {"_type": "None"}], - "type": ["Source Settings", "SourceSettings", "manual"] + "type": ["Source settings", "SourceSettings", "manual"] }, "sources": [], "spatial": { @@ -1049,7 +1057,7 @@ }, "survivalBiasing": { "weight": ["Weight", "Float", 0.25, "Weight cutoff below which particle undergo Russian roulette", 0, 1], - "weight_avg": ["Weight Average", "Float", 1.0, "Weight assigned to particles that are not killed after Russian roulette", 0, 1] + "weight_avg": ["Weight average", "Float", 1.0, "Weight assigned to particles that are not killed after Russian roulette", 0, 1] }, "tabular": { "_super": ["_", "model", "univariate"], @@ -1074,8 +1082,8 @@ "nuclides": ["Nuclides", "SimpleListEditor", [], "List of nuclides to use when scoring results", "nuclide"] }, "tallyReport": { - "axis": ["Slice Axis", "Axis", "y"], - "colorMap": ["Color Map", "ColorMap", "viridis"], + "axis": ["Slice axis", "Axis", "y"], + "colorMap": ["Color map", "ColorMap", "viridis"], "planePos": ["{{ appState.models.tallyReport.axis }} [m]", "PlanePosition", 0], "enableCrosshairs": ["", "Bool", false], "enableSelection": ["", "Bool", false], diff --git a/sirepo/pkcli/openmc.py b/sirepo/pkcli/openmc.py index 1b53ddf57e..945aaa366e 100644 --- a/sirepo/pkcli/openmc.py +++ b/sirepo/pkcli/openmc.py @@ -166,6 +166,7 @@ def __init__(self, collector): self._items.append( _MoabGroupExtractorOp( dagmc_filename=collector.dagmc_filename, + name=g.name, vol_id=g.vol_id, volumes=g.volumes, processor=self, diff --git a/sirepo/sim_data/openmc.py b/sirepo/sim_data/openmc.py index 0d147bc8b1..9e099fe963 100644 --- a/sirepo/sim_data/openmc.py +++ b/sirepo/sim_data/openmc.py @@ -64,6 +64,7 @@ def _fix_val(model, field): "geometry3DReport", "geometryInput", "openmcAnimation", + "outlineAnimation", "reflectivePlanes", "settings", "tallyReport", @@ -134,7 +135,7 @@ def _compute_job_fields(cls, data, *args, **kwargs): def _compute_model(cls, analysis_model, *args, **kwargs): if analysis_model == "geometry3DReport": return "dagmcAnimation" - if analysis_model == "energyAnimation": + if analysis_model in ("energyAnimation", "outlineAnimation"): return "openmcAnimation" return analysis_model @@ -169,8 +170,8 @@ def _lib_file_basenames(cls, data): def _sim_file_basenames(cls, data): res = [] if data.report == "openmcAnimation": - for v in data.models.volumes: - res.append(PKDict(basename=f"{data.models.volumes[v].volId}.ply")) + for v in data.models.volumes.values(): + res.append(PKDict(basename=f"{v.volId}.ply")) d, s = cls.dagmc_and_maybe_step_filename(data) if s: res.append(PKDict(basename=d)) diff --git a/sirepo/template/openmc.py b/sirepo/template/openmc.py index 813f4235de..5ed8cd6787 100644 --- a/sirepo/template/openmc.py +++ b/sirepo/template/openmc.py @@ -19,9 +19,10 @@ import sirepo.sim_data import sirepo.sim_run import subprocess +import h5py _CACHE_DIR = "openmc-cache" -_OUTLINES_FILE = "outlines.json" +_OUTLINES_FILE = "outlines.h5" _PREP_SBATCH_PREFIX = "prep-sbatch" _VOLUME_INFO_FILE = "volumes.json" _SIM_DATA, SIM_TYPE, SCHEMA = sirepo.sim_data.template_globals() @@ -68,20 +69,6 @@ def get_data_file(run_dir, model, frame, options): raise AssertionError(f"invalid model={model} options={options}") -def prepare_for_save(data, qcall): - # materialsFile is used only once to setup initial volume materials. - # it isn't reusable across simulations - if data.models.get("volumes") and data.models.geometryInput.get("materialsFile"): - if _SIM_DATA.lib_file_exists(_SIM_DATA.materials_filename(data), qcall=qcall): - pkio.unchecked_remove( - _SIM_DATA.lib_file_abspath( - _SIM_DATA.materials_filename(data), qcall=qcall - ) - ) - data.models.geometryInput.materialsFile = "" - return data - - def post_execution_processing( compute_model, sim_id, success_exit, is_parallel, run_dir, **kwargs ): @@ -99,6 +86,20 @@ def post_execution_processing( return _parse_openmc_log(run_dir) +def prepare_for_save(data, qcall): + # materialsFile is used only once to setup initial volume materials. + # it isn't reusable across simulations + if data.models.get("volumes") and data.models.geometryInput.get("materialsFile"): + if _SIM_DATA.lib_file_exists(_SIM_DATA.materials_filename(data), qcall=qcall): + pkio.unchecked_remove( + _SIM_DATA.lib_file_abspath( + _SIM_DATA.materials_filename(data), qcall=qcall + ) + ) + data.models.geometryInput.materialsFile = "" + return data + + def python_source_for_model(data, model, qcall, **kwargs): return _generate_parameters_file(data) @@ -113,6 +114,18 @@ def sim_frame(frame_args): return _energy_plot( frame_args.run_dir, frame_args.sim_in, frame_args.frameIndex ) + if frame_args.frameReport == "outlineAnimation": + res = PKDict() + with h5py.File(_OUTLINES_FILE, "r") as f: + s = f[frame_args.tally][frame_args.axis][str(frame_args.frameIndex)] + points = s["points"] + for volId in s["volumes"]: + res[volId] = [] + for idx in s["volumes"][volId]: + res[volId].append(points[idx[0] : idx[0] + idx[1]].tolist()) + return PKDict( + outlines=res, + ) def _sample_sources(filename, num_samples): samples = [] @@ -183,7 +196,6 @@ def _tally_index(frame_args): # volume normalize copied from openmc.UnstructuredMesh.write_data_to_vtk() v /= t.find_filter(openmc.MeshFilter).mesh.volumes.ravel() - o = simulation_db.read_json(frame_args.run_dir.join(_OUTLINES_FILE)) return PKDict( field_data=v.tolist(), min_field=v.min(), @@ -191,7 +203,6 @@ def _tally_index(frame_args): num_particles=frame_args.sim_in.models.settings.particles, summaryData=PKDict( tally=frame_args.tally, - outlines=o[frame_args.tally] if frame_args.tally in o else {}, sourceParticles=_sample_sources( _source_filename(frame_args.sim_in), frame_args.numSampleSourceParticles, @@ -220,7 +231,7 @@ def stateful_compute_download_remote_lib_file(data, **kwargs): return PKDict() -def statefull_compute_save_weight_windows_file_to_lib(data, **kwargs): +def stateful_compute_save_weight_windows_file_to_lib(data, **kwargs): n = _format_weight_windows_file_name(data.args.name) _SIM_DATA.lib_file_write( _SIM_DATA.lib_file_name_with_model_field( @@ -321,8 +332,6 @@ def write_volume_outlines(): import trimesh import dagmc_geometry_slice_plotter - _MIN_RES = SCHEMA.constants.minTallyResolution - def _center_range(mesh, dim): f = ( 0.5 @@ -344,59 +353,61 @@ def _get_meshes(): if t[f]._type == "meshFilter": yield t.name, t[f] - def _is_skip_dimension(tally_range, dim1, dim2): - return len(tally_ranges[dim1]) < _MIN_RES or len(tally_ranges[dim2]) < _MIN_RES - - all_outlines = PKDict() - for tally_name, tally_mesh in _get_meshes(): - tally_ranges = [_center_range(tally_mesh, i) for i in range(3)] - # don't include outlines of low resolution dimensions - skip_dimensions = PKDict( - x=_is_skip_dimension(tally_ranges, 1, 2), - y=_is_skip_dimension(tally_ranges, 0, 2), - z=_is_skip_dimension(tally_ranges, 0, 1), - ) - outlines = PKDict() - all_outlines[tally_name] = outlines - basis_vects = numpy.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) - rots = [ - numpy.array([[1, 0], [0, 1]]), - numpy.array([[0, -1], [1, 0]]), - numpy.array([[0, 1], [-1, 0]]), + def _is_skip_dimension(tally_range, dim): + m = SCHEMA.constants.minTallyResolution + d1 = 1 if dim == "x" else 0 + d2 = 1 if dim == "z" else 2 + return len(tally_range[d1]) < m or len(tally_range[d2]) < m + + basis_vects = numpy.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]]) + rots = numpy.array( + [ + [[0, 1], [1, 0]], + [[0, -1], [1, 0]], + [[-1, 0], [0, 1]], ] - for mf in pkio.sorted_glob("*.ply"): - vol_id = mf.purebasename - vol_mesh = None - outlines[vol_id] = PKDict(x=[], y=[], z=[]) - with open(mf, "rb") as f: - vol_mesh = trimesh.Trimesh(**trimesh.exchange.ply.load_ply(f)) - for i, dim in enumerate(outlines[vol_id].keys()): - if skip_dimensions[dim]: - outlines[vol_id][dim] = [] + ) + scale = SCHEMA.constants.geometryScale + vol_meshes = PKDict() + for mf in pkio.sorted_glob("*.ply"): + vol_id = mf.purebasename + with open(mf, "rb") as f: + vol_meshes[vol_id] = trimesh.Trimesh(**trimesh.exchange.ply.load_ply(f)) + + with h5py.File(_OUTLINES_FILE, "w") as hf: + for tally_name, tally_mesh in _get_meshes(): + tally_grp = hf.create_group(tally_name) + tally_ranges = [_center_range(tally_mesh, i) for i in range(3)] + for i, dim in enumerate(["x", "y", "z"]): + # don't include outlines of low resolution dimensions + if _is_skip_dimension(tally_ranges, dim): continue - n = basis_vects[i] - r = rots[i] - for pos in tally_ranges[i]: - coords = [] - try: - coords = dagmc_geometry_slice_plotter.get_slice_coordinates( - dagmc_file_or_trimesh_object=vol_mesh, - plane_origin=pos * n, - plane_normal=n, - ) - # get_slice_coordinates returns a list of "TrackedArrays", - # arranged for use in matplotlib - ct = [] - for c in [ - (SCHEMA.constants.geometryScale * x.T) for x in coords - ]: - ct.append([numpy.dot(r, x).tolist() for x in c]) - coords = ct - except ValueError: - # no intersection at this plane position - pass - outlines[vol_id][dim].append(coords) - simulation_db.write_json(_OUTLINES_FILE, all_outlines) + dim_grp = tally_grp.create_group(dim) + for sl, pos in enumerate(tally_ranges[i]): + slice_grp = dim_grp.create_group(str(sl)) + vol_group = slice_grp.create_group("volumes") + idx = 0 + points = [] + for vol_id, vol_mesh in vol_meshes.items(): + indices = [] + try: + polys = dagmc_geometry_slice_plotter.get_slice_coordinates( + dagmc_file_or_trimesh_object=vol_mesh, + plane_origin=pos * basis_vects[i], + plane_normal=basis_vects[i], + ) + for poly in [scale * p.T for p in polys]: + pts = [numpy.dot(rots[i], p).tolist() for p in poly] + indices.append([idx, len(pts)]) + idx += len(pts) + for pt in pts: + points.append(pt) + except ValueError: + # no intersection at this plane position + pass + if len(indices): + vol_group.create_dataset(vol_id, data=indices) + slice_grp.create_dataset("points", data=points) def _batch_sequence(settings): diff --git a/test.sh b/test.sh index 91de914e80..772c44300c 100644 --- a/test.sh +++ b/test.sh @@ -44,7 +44,7 @@ _msg() { } _no_h5py() { - local f=( $(find sirepo -name \*.py | egrep -v '/(package_data|activait|flash|omega|opal|radia|silas|warp|server.py|hdf5_util|madx|canvas|elegant)') ) + local f=( $(find sirepo -name \*.py | egrep -v '/(package_data|activait|flash|omega|opal|radia|silas|warp|server.py|hdf5_util|madx|canvas|elegant|openmc)') ) local r=$(grep -l '^import.*h5py' "${f[@]}") if [[ $r ]]; then _err "import h5py found in: $r" From a51a4cc182c156efdf7c1dcceb4880d5d14d8a44 Mon Sep 17 00:00:00 2001 From: Rob Nagler <5495179+robnagler@users.noreply.github.com> Date: Wed, 6 Nov 2024 06:47:44 -0700 Subject: [PATCH 3/5] Fix #7344 require premium for sbatch (#7348) --- sirepo/api_auth.py | 3 + sirepo/api_perm.py | 2 + sirepo/auth/__init__.py | 4 ++ sirepo/job_api.py | 4 +- sirepo/package_data/static/en/plans.html | 69 +++++++++++++++++++ .../static/js/sirepo-components.js | 7 +- sirepo/package_data/static/js/sirepo.js | 8 +++ .../static/json/schema-common.json | 2 +- 8 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 sirepo/package_data/static/en/plans.html diff --git a/sirepo/api_auth.py b/sirepo/api_auth.py index 065446e8f2..ab484d77d9 100644 --- a/sirepo/api_auth.py +++ b/sirepo/api_auth.py @@ -33,6 +33,7 @@ def check_api_call(qcall, func): a.REQUIRE_COOKIE_SENTINEL, a.REQUIRE_USER, a.REQUIRE_ADM, + a.REQUIRE_PREMIUM, ): if not qcall.cookie.has_sentinel(): raise sirepo.util.SRException("missingCookies", None) @@ -42,6 +43,8 @@ def check_api_call(qcall, func): qcall.auth.require_email_user() elif expect == a.REQUIRE_ADM: qcall.auth.require_adm() + elif expect == a.REQUIRE_PREMIUM: + qcall.auth.require_premium() elif expect == a.ALLOW_VISITOR: pass elif expect == a.INTERNAL_TEST: diff --git a/sirepo/api_perm.py b/sirepo/api_perm.py index f5d39e0ed6..1c28eb750b 100644 --- a/sirepo/api_perm.py +++ b/sirepo/api_perm.py @@ -32,6 +32,8 @@ class APIPerm(aenum.Flag): REQUIRE_USER = aenum.auto() #: only usable on internal test systems INTERNAL_TEST = aenum.auto() + #: a user with a a premium subscription is required + REQUIRE_PREMIUM = aenum.auto() #: A user can access APIs decorated with these permissions even if they don't have the role diff --git a/sirepo/auth/__init__.py b/sirepo/auth/__init__.py index d9ecbffba0..a521abcb1d 100644 --- a/sirepo/auth/__init__.py +++ b/sirepo/auth/__init__.py @@ -517,6 +517,10 @@ def require_email_user(self): if m != METHOD_EMAIL: raise sirepo.util.Forbidden(f"method={m} is not email for uid={i}") + def require_premium(self): + if not self.is_premium_user(): + raise sirepo.util.Forbidden(f"not premium user") + def require_user(self): """Asserts whether user is logged in diff --git a/sirepo/job_api.py b/sirepo/job_api.py index 5780758ee6..f103f3441e 100644 --- a/sirepo/job_api.py +++ b/sirepo/job_api.py @@ -211,7 +211,7 @@ async def api_runStatus(self): # runStatus receives models when an animation status if first queried return await self._request_api(_request_content=self._request_content(PKDict())) - @sirepo.quest.Spec("require_user") + @sirepo.quest.Spec("require_premium") async def api_sbatchLogin(self): r = self._request_content( PKDict(computeJobHash="unused", jobRunMode=sirepo.job.SBATCH), @@ -221,7 +221,7 @@ async def api_sbatchLogin(self): r.pkdel("data") return await self._request_api(_request_content=r) - @sirepo.quest.Spec("require_user") + @sirepo.quest.Spec("require_premium") async def api_sbatchLoginStatus(self): return await self._request_api( _request_content=self._request_content( diff --git a/sirepo/package_data/static/en/plans.html b/sirepo/package_data/static/en/plans.html new file mode 100644 index 0000000000..937920ba26 --- /dev/null +++ b/sirepo/package_data/static/en/plans.html @@ -0,0 +1,69 @@ + + + + + + + + + + RadiaSoft + + + + + + +
+
+
+ +
+ +
+
+
+
+

Sirepo Plans

+

+ You are running a private instance of sirepo. +
+ license +

+
+
+
+

Sirepo Plans

+

You should only see this page if you are developing Sirepo.

+
+
+
+
+ +
+
+ +
+
+
+ + diff --git a/sirepo/package_data/static/js/sirepo-components.js b/sirepo/package_data/static/js/sirepo-components.js index 896a6a4072..6d38f0baa9 100644 --- a/sirepo/package_data/static/js/sirepo-components.js +++ b/sirepo/package_data/static/js/sirepo-components.js @@ -4757,7 +4757,11 @@ SIREPO.app.directive('sbatchLoginModal', function() {