`,
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: `
+
+
+
+ `,
+ 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: `
-
-
-
- `,
- 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"