mirror of
https://github.com/xibyte/jsketcher
synced 2025-12-07 00:45:08 +01:00
setup debugger UI
This commit is contained in:
parent
d7d7e2e597
commit
1597c5f4b3
17 changed files with 295 additions and 18 deletions
|
|
@ -3,7 +3,8 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "JS.Sketcher is a parametric 2D and 3D CAD modeler written in pure javascript",
|
"description": "JS.Sketcher is a parametric 2D and 3D CAD modeler written in pure javascript",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --config webpack.config.dev.js --content-base web/ --port 3000",
|
"start": "node ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --config webpack.config.js --content-base web/ --port 3000",
|
||||||
|
"start-test": "node ./node_modules/webpack-dev-server/bin/webpack-dev-server.js --config webpack.config.dev.js --content-base web/ --port 3000",
|
||||||
"pack": "node ./node_modules/webpack/bin/webpack.js --config webpack.config.js --progress --profile --colors",
|
"pack": "node ./node_modules/webpack/bin/webpack.js --config webpack.config.js --progress --profile --colors",
|
||||||
"build": "grunt",
|
"build": "grunt",
|
||||||
"lint": "eslint web/app"
|
"lint": "eslint web/app"
|
||||||
|
|
@ -31,6 +32,7 @@
|
||||||
"babel-preset-stage-2": "6.24.1",
|
"babel-preset-stage-2": "6.24.1",
|
||||||
"babel-polyfill": "6.26.0",
|
"babel-polyfill": "6.26.0",
|
||||||
"babel-preset-react": "6.24.1",
|
"babel-preset-react": "6.24.1",
|
||||||
|
"babel-plugin-local-styles-transformer": "git://github.com/xibyte/babel-plugin-local-styles-transformer.git#0.0.1",
|
||||||
|
|
||||||
"css-loader": "0.28.7",
|
"css-loader": "0.28.7",
|
||||||
"less-loader": "4.0.5",
|
"less-loader": "4.0.5",
|
||||||
|
|
@ -58,6 +60,7 @@
|
||||||
"less": "2.7.3",
|
"less": "2.7.3",
|
||||||
"libtess": "1.2.2",
|
"libtess": "1.2.2",
|
||||||
"numeric": "1.2.6",
|
"numeric": "1.2.6",
|
||||||
"sprintf": "0.1.5"
|
"sprintf": "0.1.5",
|
||||||
|
"classnames": "2.2.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,11 @@ import {createSolidMaterial} from './scene/scene-object'
|
||||||
import DPR from '../utils/dpr'
|
import DPR from '../utils/dpr'
|
||||||
import Vector from "../math/vector";
|
import Vector from "../math/vector";
|
||||||
import {NurbsCurve} from "../brep/geom/impl/nurbs";
|
import {NurbsCurve} from "../brep/geom/impl/nurbs";
|
||||||
|
import * as ui from '../ui/ui';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import BrepDebugger from './../brep/debug/debugger/brepDebugger';
|
||||||
|
|
||||||
export const DEBUG = true;
|
export const DEBUG = true;
|
||||||
|
|
||||||
|
|
@ -207,7 +212,7 @@ const DebugMenuConfig = {
|
||||||
label: 'debug',
|
label: 'debug',
|
||||||
cssIcons: ['bug'],
|
cssIcons: ['bug'],
|
||||||
info: 'set of debug actions',
|
info: 'set of debug actions',
|
||||||
actions: [ 'DebugPrintAllSolids', 'DebugPrintFace', 'DebugFaceId', 'DebugFaceSketch']
|
actions: [ 'DebugPrintAllSolids', 'DebugPrintFace', 'DebugFaceId', 'DebugFaceSketch', 'DebugOpenBrepDebugger']
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -274,5 +279,27 @@ const DebugActions = {
|
||||||
};
|
};
|
||||||
console.log(JSON.stringify(squashed));
|
console.log(JSON.stringify(squashed));
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
'DebugOpenBrepDebugger': {
|
||||||
|
cssIcons: ['cubes'],
|
||||||
|
label: 'open BREP debugger',
|
||||||
|
info: 'open the BREP debugger in a window',
|
||||||
|
invoke: (app) => {
|
||||||
|
|
||||||
|
let debuggerWinDom = document.getElementById('brep-debugger');
|
||||||
|
if (!debuggerWinDom) {
|
||||||
|
//Temporary hack until win infrastructure is done for 3d
|
||||||
|
debuggerWinDom = document.createElement('div');
|
||||||
|
debuggerWinDom.setAttribute('id', 'brep-debugger');
|
||||||
|
debuggerWinDom.innerHTML = '<div class="tool-caption" ><i class="fa fa-fw fa-bug"></i>Brep Debugger</div><div class="content"></div>';
|
||||||
|
document.body.appendChild(debuggerWinDom);
|
||||||
|
debuggerWinDom.debuggerWin = new ui.Window($(debuggerWinDom), new ui.WinManager());
|
||||||
|
ReactDOM.render(
|
||||||
|
<BrepDebugger />,
|
||||||
|
debuggerWinDom.getElementsByClassName('content')[0]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
debuggerWinDom.debuggerWin.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
export const keymap = {
|
export const keymap = {
|
||||||
|
|
||||||
'CUT': 'C',
|
'CUT': 'C',
|
||||||
'EXTRUDE': 'E',
|
'EXTRUDE': 'E',
|
||||||
'ZoomIn': '+',
|
'ZoomIn': '+',
|
||||||
|
|
@ -8,5 +7,6 @@ export const keymap = {
|
||||||
'menu.primitives': 'shift+A',
|
'menu.primitives': 'shift+A',
|
||||||
'menu.main': 'space',
|
'menu.main': 'space',
|
||||||
'Save': 'ctrl+S',
|
'Save': 'ctrl+S',
|
||||||
'Info': 'F1'
|
'Info': 'F1',
|
||||||
|
'DebugOpenBrepDebugger': 'ctrl+B'
|
||||||
};
|
};
|
||||||
|
|
|
||||||
34
web/app/brep/debug/brep-debug.js
Normal file
34
web/app/brep/debug/brep-debug.js
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
|
||||||
|
class BRepDebug {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.booleanSessions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
startBooleanSession(a, b, type) {
|
||||||
|
this.currentBooleanSession = new BooleanSession(a, b, type)
|
||||||
|
this.booleanSessions.push(this.currentBooleanSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class BooleanSession {
|
||||||
|
|
||||||
|
constructor(a, b, type) {
|
||||||
|
this.inputOperandA = a;
|
||||||
|
this.inputOperandB = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
setWorkingOperands(a, b) {
|
||||||
|
this.workingOperandA = a;
|
||||||
|
this.workingOperandB = b;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResult(result) {
|
||||||
|
this.result = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (new BRepDebug());
|
||||||
60
web/app/brep/debug/debugger/brepDebugger.jsx
Normal file
60
web/app/brep/debug/debugger/brepDebugger.jsx
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
import React from 'react';
|
||||||
|
import './brepDebugger.less';
|
||||||
|
import BREP_DEBUG from '../brep-debug';
|
||||||
|
import ShellExplorer from './shellExplorer';
|
||||||
|
import Section from './section'
|
||||||
|
|
||||||
|
export default class BrepDebugger extends React.PureComponent {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let {booleanSessions} = BREP_DEBUG;
|
||||||
|
|
||||||
|
return <div className='brep-debugger'>
|
||||||
|
<div className='section'>
|
||||||
|
<i className='fa fa-fw fa-eye-slash button' onClick={() => __DEBUG__.HideSolids()} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='section boolean-sessions'>
|
||||||
|
{booleanSessions.map(session =>
|
||||||
|
<Section name={`boolean session ${session.id}`} closable accent captionStyles={['centered']}>
|
||||||
|
<div className='section'>
|
||||||
|
|
||||||
|
<Section name='input operands' accent>
|
||||||
|
<div className='operands-veiew'>
|
||||||
|
<div>
|
||||||
|
<div className='caption operand-a'>Operand A</div>
|
||||||
|
<ShellExplorer shell={session.inputOperandA}/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className='caption operand-b'>Operand B</div>
|
||||||
|
<ShellExplorer shell={session.inputOperandB}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Section name='working operands' accent>
|
||||||
|
<div className='operands-veiew'>
|
||||||
|
<div>
|
||||||
|
<div className='caption operand-a'>Operand A</div>
|
||||||
|
<ShellExplorer shell={session.workingOperandA}/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className='caption operand-b'>Operand B</div>
|
||||||
|
<ShellExplorer shell={session.workingOperandB}/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className='caption result'>Result</div>
|
||||||
|
<ShellExplorer shell={session.result}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
</Section>)}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
53
web/app/brep/debug/debugger/brepDebugger.less
Normal file
53
web/app/brep/debug/debugger/brepDebugger.less
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
.brep-debugger {
|
||||||
|
|
||||||
|
padding: 0.3em;
|
||||||
|
line-height: 1.5;
|
||||||
|
button {
|
||||||
|
font-size: inherit;
|
||||||
|
padding: 0 0.3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .operands-veiew {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
& > * {
|
||||||
|
flex: 1 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .section {
|
||||||
|
& .caption {
|
||||||
|
&.closable {
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.accent {
|
||||||
|
background-color: rgb(255, 244, 244);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
&.centered {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
&.operand-a {
|
||||||
|
background-color: rgb(239, 244, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
&.operand-b {
|
||||||
|
background-color: rgb(244, 255, 245)
|
||||||
|
}
|
||||||
|
&.result {
|
||||||
|
background-color: rgb(254, 255, 244)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.3em;
|
||||||
|
&:hover {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
0
web/app/brep/debug/debugger/faceMergeExplorer.js
Normal file
0
web/app/brep/debug/debugger/faceMergeExplorer.js
Normal file
0
web/app/brep/debug/debugger/loopDetectionExplorer.jsx
Normal file
0
web/app/brep/debug/debugger/loopDetectionExplorer.jsx
Normal file
31
web/app/brep/debug/debugger/section.jsx
Normal file
31
web/app/brep/debug/debugger/section.jsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react';
|
||||||
|
import cx from 'classnames';
|
||||||
|
|
||||||
|
|
||||||
|
export default class Section extends React.PureComponent {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.state = {
|
||||||
|
closed: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let {name, tabs, closable, defaultClosed, accent, children, captionStyles} = this.props;
|
||||||
|
let closed = closable && (this.state.closed || defaultClosed);
|
||||||
|
return <div className={cx('section', {closable, closed})} style={{paddingLeft: tabs}}>
|
||||||
|
<div className={cx('caption', {accent}, captionStyles)} >
|
||||||
|
{closable && <i className={'fa fa-fw fa-caret-' + (closed ? 'right': 'down')} />} {name}</div>
|
||||||
|
{children}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapIterator(it, fn) {
|
||||||
|
for (let i of it) {
|
||||||
|
fn(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
26
web/app/brep/debug/debugger/shellExplorer.jsx
Normal file
26
web/app/brep/debug/debugger/shellExplorer.jsx
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export default class ShellExplorer extends React.PureComponent {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let {shell} = this.props;
|
||||||
|
return <div className='shell-explorer'>
|
||||||
|
<div className='caption'>faces</div>
|
||||||
|
{shell.faces.map(face => <div>
|
||||||
|
<div className='caption'>face {face.refId}</div>
|
||||||
|
{mapIterator(face.edges, e => <div>
|
||||||
|
edge: {e.refId}
|
||||||
|
</div>)}
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapIterator(it, fn) {
|
||||||
|
for (let i of it) {
|
||||||
|
fn(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -11,15 +11,17 @@ import {Ray} from "../utils/ray";
|
||||||
import pickPointInside2dPolygon from "../utils/pickPointInPolygon";
|
import pickPointInside2dPolygon from "../utils/pickPointInPolygon";
|
||||||
import CadError from "../../utils/errors";
|
import CadError from "../../utils/errors";
|
||||||
import {createBoundingNurbs} from "../brep-builder";
|
import {createBoundingNurbs} from "../brep-builder";
|
||||||
|
import BREP_DEBUG from '../debug/brep-debug';
|
||||||
|
|
||||||
|
|
||||||
const A = 0, B = 1;
|
const A = 0, B = 1;
|
||||||
|
|
||||||
const DEBUG = {
|
const DEBUG = {
|
||||||
OPERANDS_MODE: false,
|
OPERANDS_MODE: false,
|
||||||
LOOP_DETECTION: true,
|
LOOP_DETECTION: false,
|
||||||
FACE_FACE_INTERSECTION: true,
|
FACE_FACE_INTERSECTION: false,
|
||||||
RAY_CAST: false,
|
RAY_CAST: false,
|
||||||
FACE_MERGE: true,
|
FACE_MERGE: false,
|
||||||
NOOP: () => {}
|
NOOP: () => {}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -78,9 +80,13 @@ function checkShellForErrors(shell, code) {
|
||||||
|
|
||||||
export function BooleanAlgorithm( shellA, shellB, type ) {
|
export function BooleanAlgorithm( shellA, shellB, type ) {
|
||||||
|
|
||||||
|
BREP_DEBUG.startBooleanSession(shellA, shellB, type);
|
||||||
|
|
||||||
shellA = prepareWorkingCopy(shellA);
|
shellA = prepareWorkingCopy(shellA);
|
||||||
shellB = prepareWorkingCopy(shellB);
|
shellB = prepareWorkingCopy(shellB);
|
||||||
|
|
||||||
|
BREP_DEBUG.currentBooleanSession.setWorkingOperands(shellA, shellB);
|
||||||
|
|
||||||
let facesData = [];
|
let facesData = [];
|
||||||
|
|
||||||
mergeVertices(shellA, shellB);
|
mergeVertices(shellA, shellB);
|
||||||
|
|
@ -137,6 +143,7 @@ export function BooleanAlgorithm( shellA, shellB, type ) {
|
||||||
|
|
||||||
// __DEBUG__.ClearVolumes();
|
// __DEBUG__.ClearVolumes();
|
||||||
// __DEBUG__.Clear();
|
// __DEBUG__.Clear();
|
||||||
|
BREP_DEBUG.currentBooleanSession.setResult(result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -585,8 +592,8 @@ function filterFacesByNewEdges(faces) {
|
||||||
const validFaces = new Set(faces);
|
const validFaces = new Set(faces);
|
||||||
const result = new Set();
|
const result = new Set();
|
||||||
for (let face of faces) {
|
for (let face of faces) {
|
||||||
__DEBUG__.Clear();
|
// __DEBUG__.Clear();
|
||||||
__DEBUG__.AddFace(face);
|
// __DEBUG__.AddFace(face);
|
||||||
traverseFaces(face, validFaces, (it) => {
|
traverseFaces(face, validFaces, (it) => {
|
||||||
if (result.has(it) || isFaceContainNewEdge(it)) {
|
if (result.has(it) || isFaceContainNewEdge(it)) {
|
||||||
result.add(face);
|
result.add(face);
|
||||||
|
|
@ -866,7 +873,7 @@ function collectNodesOfIntersectionOfFace(curve, face, nodes, operand) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function collectNodesOfIntersection(curve, loop, nodes, operand) {
|
function collectNodesOfIntersection(curve, loop, nodes, operand) {
|
||||||
__DEBUG__.AddCurve(curve, 0xffffff);
|
// __DEBUG__.AddCurve(curve, 0xffffff);
|
||||||
let skippedEnclosures = new Set();
|
let skippedEnclosures = new Set();
|
||||||
|
|
||||||
for (let edge of loop.halfEdges) {
|
for (let edge of loop.halfEdges) {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,11 @@ function Window(el, winManager) {
|
||||||
winManager.registerDrag(this.root, caption);
|
winManager.registerDrag(this.root, caption);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Window.prototype.show = function() {
|
||||||
|
this.root.show();
|
||||||
|
}
|
||||||
|
|
||||||
Window.prototype.toggle = function() {
|
Window.prototype.toggle = function() {
|
||||||
var aboutToShow = !this.root.is(':visible');
|
var aboutToShow = !this.root.is(':visible');
|
||||||
if (aboutToShow) {
|
if (aboutToShow) {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
@import 'brep-debugger.less';
|
||||||
|
|
||||||
@tab-switcher-inner-height: 20px;
|
@tab-switcher-inner-height: 20px;
|
||||||
@tab-switcher-top-border: 1px;
|
@tab-switcher-top-border: 1px;
|
||||||
@tab-switcher-height: @tab-switcher-inner-height + @tab-switcher-top-border;
|
@tab-switcher-height: @tab-switcher-inner-height + @tab-switcher-top-border;
|
||||||
|
|
|
||||||
18
web/css/brep-debugger.less
Normal file
18
web/css/brep-debugger.less
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#brep-debugger {
|
||||||
|
position: absolute;
|
||||||
|
min-width: 100px;
|
||||||
|
min-height: 100px;
|
||||||
|
height: 50%;
|
||||||
|
width: 300px;
|
||||||
|
left:100px;
|
||||||
|
top:300px;
|
||||||
|
background: #eee;
|
||||||
|
border: 5px solid rgb(49, 121, 255);
|
||||||
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
|
||||||
|
& .tool-caption {
|
||||||
|
padding: 0.3em;
|
||||||
|
background-color: rgb(238, 255, 246);
|
||||||
|
cursor: default;
|
||||||
|
user-select: none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
|
|
||||||
|
const WEB_APP = path.join(__dirname, 'web/app');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
entry: {
|
entry: {
|
||||||
|
|
@ -25,21 +27,26 @@ module.exports = {
|
||||||
module: {
|
module: {
|
||||||
rules: [{
|
rules: [{
|
||||||
test: /\.(js|jsx)$/,
|
test: /\.(js|jsx)$/,
|
||||||
use: ['babel-loader'],
|
loader: 'babel-loader',
|
||||||
include: [path.join(__dirname, 'web/app'), path.join(__dirname, 'web/test')]
|
include: [WEB_APP, path.join(__dirname, 'web/test')],
|
||||||
|
options: {
|
||||||
|
plugins: [
|
||||||
|
['local-styles-transformer', {include: WEB_APP}]
|
||||||
|
]
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
test: /\.css$/,
|
test: /\.css$/,
|
||||||
use: [
|
use: [
|
||||||
"style-loader",
|
'style-loader',
|
||||||
"css-loader",
|
'css-loader',
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
test: /\.less$/,
|
test: /\.less$/,
|
||||||
use: [
|
use: [
|
||||||
"style-loader",
|
'style-loader',
|
||||||
"css-loader?-url",
|
'css-loader?-url',
|
||||||
"less-loader"
|
'less-loader'
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -50,5 +57,9 @@ module.exports = {
|
||||||
test: /\.json$/,
|
test: /\.json$/,
|
||||||
use: 'json-loader'
|
use: 'json-loader'
|
||||||
}]
|
}]
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
hot: false,
|
||||||
|
inline: false,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue