mirror of
https://github.com/xibyte/jsketcher
synced 2026-01-27 10:32:44 +01:00
separates mesh operations from BREP
This commit is contained in:
parent
829d8dafae
commit
109e16fdf7
28 changed files with 620 additions and 277 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import * as Operations from '../operations'
|
||||
import * as Operations from '../craft/operations'
|
||||
import * as ActionHelpers from './action-helpers'
|
||||
|
||||
function mergeInfo(opName, action) {
|
||||
|
|
|
|||
50
web/app/3d/craft/brep/cut-extrude.js
Normal file
50
web/app/3d/craft/brep/cut-extrude.js
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import {Matrix3, ORIGIN} from '../../../math/l3space'
|
||||
import * as math from '../../../math/math'
|
||||
import Vector from '../../../math/vector'
|
||||
import {Extruder} from '../../../brep/brep-builder'
|
||||
|
||||
export function Extrude(app, params) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export function Cut(face, params) {
|
||||
|
||||
}
|
||||
|
||||
export class ParametricExtruder extends Extruder {
|
||||
|
||||
constructor(face, params) {
|
||||
super();
|
||||
this.face = face;
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
prepareLidCalculation(baseNormal, lidNormal) {
|
||||
let target;
|
||||
if (this.params.rotation != 0) {
|
||||
const basis = this.face.basis();
|
||||
target = Matrix3.rotateMatrix(this.params.rotation * Math.PI / 180, basis[0], ORIGIN).apply(lidNormal);
|
||||
if (this.params.angle != 0) {
|
||||
target = Matrix3.rotateMatrix(this.params.angle * Math.PI / 180, basis[2], ORIGIN)._apply(target);
|
||||
}
|
||||
target._multiply(Math.abs(this.params.value));
|
||||
} else {
|
||||
target = normal.multiply(Math.abs(this.params.value));
|
||||
}
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
calculateLid(basePoints) {
|
||||
if (this.params.prism != 1) {
|
||||
const scale = this.params.prism < 0.001 ? 0.0001 : this.params.prism;
|
||||
const _3Dtr = this.face.surface.get3DTransformation();
|
||||
const _2Dtr = _3Dtr.invert();
|
||||
const poly2d = basePoints.map(p => _2Dtr.apply(p));
|
||||
basePoints = math.polygonOffset(poly2d, scale).map(p => _2Dtr.apply(p));
|
||||
}
|
||||
return basePoints.map(p => p.plus(this.target));
|
||||
}
|
||||
}
|
||||
9
web/app/3d/craft/brep/sketch-reader.js
Normal file
9
web/app/3d/craft/brep/sketch-reader.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import {sortPolygons, getSketchedPolygons3D} from '../mesh/workbench'
|
||||
|
||||
|
||||
|
||||
// here will be function of conversion sketch objects to brep DS
|
||||
|
||||
export function ReadSketchFromFace(app, faceId) {
|
||||
return getSketchedPolygons3D(app, faceId);
|
||||
}
|
||||
61
web/app/3d/craft/brep/wizards/cut-extrude.js
Normal file
61
web/app/3d/craft/brep/wizards/cut-extrude.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import {CURRENT_SELECTION as S} from './wizard'
|
||||
import {PreviewWizard, SketchBasedPreviewMaker} from './preview-wizard'
|
||||
import {ParametricExtruder} from '../cut-extrude'
|
||||
|
||||
const METADATA = [
|
||||
['value' , 'number', 50, {min: 0}],
|
||||
['prism' , 'number', 1 , {min: 0, step: 0.1, round: 1}],
|
||||
['angle' , 'number', 0 , {}],
|
||||
['rotation', 'number', 0 , {step: 5}],
|
||||
['face' , 'face' , S ]
|
||||
];
|
||||
|
||||
class Cut extends PreviewWizard {
|
||||
constructor(app, initialState) {
|
||||
super(app, 'CUT', METADATA, null, initialState)
|
||||
}
|
||||
|
||||
uiLabel(name) {
|
||||
if ('value' == name) return 'depth';
|
||||
return super.uiLabel(name);
|
||||
}
|
||||
}
|
||||
|
||||
class Extrude extends PreviewWizard {
|
||||
constructor(app, initialState) {
|
||||
super(app, 'EXTRUDE', METADATA, new ExtrudePreviewMaker(), initialState)
|
||||
}
|
||||
|
||||
uiLabel(name) {
|
||||
if ('value' == name) return 'height';
|
||||
return super.uiLabel(name);
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtrudePreviewMaker extends SketchBasedPreviewMaker{
|
||||
|
||||
constructor(cut) {
|
||||
super();
|
||||
this.cut = cut;
|
||||
}
|
||||
|
||||
createImpl(app, params, sketch, face) {
|
||||
const parametricExtruder = new ParametricExtruder(face, params);
|
||||
|
||||
const baseNormal = this.cut ? face.surface.normal : face.surface.normal.negate();
|
||||
const lidNormal = this.cut ? baseNormal.negate() : face.surface.normal;
|
||||
|
||||
parametricExtruder.prepareLidCalculation(baseNormal, lidNormal);
|
||||
|
||||
const triangles = [];
|
||||
for (let base of sketch) {
|
||||
var lid = parametricExtruder.calculateLid(base);
|
||||
const n = base.length;
|
||||
for (let p = n - 1, q = 0; q < n; p = q ++) {
|
||||
triangles.push([ base[p], base[q], lid[q] ]);
|
||||
triangles.push([ lid[q], lid[p], base[p] ]);
|
||||
}
|
||||
}
|
||||
return triangles;
|
||||
}
|
||||
}
|
||||
92
web/app/3d/craft/brep/wizards/preview-wizard.js
Normal file
92
web/app/3d/craft/brep/wizards/preview-wizard.js
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
import {Wizard} from './wizard'
|
||||
import {ReadSketchFromFace} from '../sketch-reader'
|
||||
import {Loop} from '../../../../brep/topo/loop'
|
||||
|
||||
export class PreviewWizard extends Wizard {
|
||||
|
||||
constructor(app, opearation, metadata, previewMaker, initialState) {
|
||||
super(app, opearation, metadata, initialState);
|
||||
this.previewGroup = new THREE.Object3D();
|
||||
this.previewMaker = previewMaker;
|
||||
this.previewObject = null;
|
||||
this.app.viewer.workGroup.add(this.previewGroup);
|
||||
this.updatePreview();
|
||||
}
|
||||
|
||||
updatePreview() {
|
||||
if (this.previewObject != null) {
|
||||
this.destroyPreviewObject();
|
||||
}
|
||||
this.previewObject = this.previewMaker.create(this.app, this.readFormFields());
|
||||
if (this.previewObject != null) {
|
||||
this.previewGroup.add( this.previewObject );
|
||||
}
|
||||
this.app.viewer.render();
|
||||
}
|
||||
|
||||
destroyPreviewObject() {
|
||||
this.previewGroup.parent.remove( this.previewObject );
|
||||
this.previewObject.geometry.dispose();
|
||||
this.previewGroup = null;
|
||||
}
|
||||
|
||||
|
||||
dispose() {
|
||||
this.app.viewer.workGroup.remove(this.previewGroup);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
PreviewWizard.createMesh = function(triangles) {
|
||||
const geometry = new THREE.Geometry();
|
||||
|
||||
for (let tr of triangles) {
|
||||
const a = geometry.vertices.length;
|
||||
const b = a + 1;
|
||||
const c = a + 2;
|
||||
const face = new THREE.Face3(a, b, c);
|
||||
tr.forEach(v => geometry.vertices.push(v.three()));
|
||||
geometry.faces.push(face);
|
||||
}
|
||||
geometry.mergeVertices();
|
||||
geometry.computeFaceNormals();
|
||||
|
||||
return new THREE.Mesh(geometry, IMAGINARY_SURFACE_MATERIAL);
|
||||
};
|
||||
|
||||
export class SketchBasedPreviewMaker {
|
||||
|
||||
constructor() {
|
||||
this.fixToCCW = true;
|
||||
}
|
||||
|
||||
createImpl(app, params, sketch, face) {
|
||||
throw 'not implemented';
|
||||
}
|
||||
|
||||
create(app, params) {
|
||||
const face = app.findFace(params.face);
|
||||
if (!face) return null;
|
||||
const needSketchRead = !this.sketch || params.face != this.face;
|
||||
if (needSketchRead) {
|
||||
this.sketch = ReadSketchFromFace(app, params.face);
|
||||
for (let polygon of this.sketch) {
|
||||
if (!Loop.isPolygonCCWOnSurface(polygon, face.surface) && this.fixToCCW) {
|
||||
polygon.reverse();
|
||||
}
|
||||
}
|
||||
this.face = params.face;
|
||||
}
|
||||
return this.createImpl(app, params, this.sketch, face);
|
||||
}
|
||||
}
|
||||
|
||||
export const IMAGINARY_SURFACE_MATERIAL = new THREE.MeshPhongMaterial({
|
||||
vertexColors: THREE.FaceColors,
|
||||
color: 0xFA8072,
|
||||
transparent: true,
|
||||
opacity: 0.5,
|
||||
shininess: 0,
|
||||
side : THREE.DoubleSide
|
||||
});
|
||||
|
||||
118
web/app/3d/craft/brep/wizards/wizard.js
Normal file
118
web/app/3d/craft/brep/wizards/wizard.js
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import * as tk from '../../../../ui/toolkit'
|
||||
|
||||
export class Wizard {
|
||||
|
||||
constructor(app, opearation, metadata, initialState) {
|
||||
this.app = app;
|
||||
this.metadata = params;
|
||||
this.formFields = {};
|
||||
this.box = this.createUI(opearation, metadata);
|
||||
if (initialState != undefined) {
|
||||
this.setFormFields(initialState);
|
||||
}
|
||||
}
|
||||
|
||||
uiLabel(name) {
|
||||
return name;
|
||||
}
|
||||
|
||||
createUI(operation, metadata) {
|
||||
const box = new tk.Box($('#view-3d'));
|
||||
const folder = new tk.Folder(operation);
|
||||
tk.add(box, folder);
|
||||
for (let def of metadata) {
|
||||
const name = def[0];
|
||||
const type = def[1];
|
||||
const defaultValue = def[1];
|
||||
const params = def[3];
|
||||
const label = this.uiLabel(name);
|
||||
const formItem = createFormField(name, label, type, defaultValue, params);
|
||||
formItem.setter(defaultValue);
|
||||
tk.add(folder, formItem.ui);
|
||||
this.formFields[name] = formItem;
|
||||
}
|
||||
const buttons = new tk.ButtonRow(["Cancel", "OK"], [() => this.cancelClick(), () => this.okClick()]);
|
||||
tk.add(folder, buttons);
|
||||
box.root.keydown((e) => {
|
||||
switch (e.keyCode) {
|
||||
case 27 : this.cancelClick(); break;
|
||||
case 13 : this.okClick(); break;
|
||||
}
|
||||
});
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
cancelClick() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
okClick() {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
onUIChange() {
|
||||
|
||||
}
|
||||
|
||||
readFormFields() {
|
||||
const params = {};
|
||||
for (let field of this.formFields) {
|
||||
params[field.name] = field.getter();
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
setFormFields(params) {
|
||||
const keys = Object.keys(params);
|
||||
for (let key of keys) {
|
||||
const formField = this.formFields[name];
|
||||
if (formField) {
|
||||
formField.setter(params[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposed = true;
|
||||
this.box.close();
|
||||
}
|
||||
|
||||
createFormField(name, label, type, params) {
|
||||
if (type == 'number') {
|
||||
const number = tk.config(tk.Number(label, 0, params.step, params.round), params);
|
||||
number.input.on('t-change', () => this.onUIChange(name));
|
||||
return Field.fromInput(number.input);
|
||||
} else if (type == 'face') {
|
||||
const face = new tk.Text(label, '');
|
||||
face.input.on('change', () => this.onUIChange(name));
|
||||
return Field.fromInput(face.input, undefined, (faceId) => {
|
||||
if (faceId === CURRENT_SELECTION) {
|
||||
let selection = this.app.viewer.selectionMgr.selection[0];
|
||||
return selection ? selection.id : '';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function FaceSelectionListener() {
|
||||
this.callbacks = [];
|
||||
}
|
||||
|
||||
function Field(getter, setter) {
|
||||
this.getter = getter;
|
||||
this.setter = setter;
|
||||
}
|
||||
|
||||
Field.NO_COERCION = (v) => v;
|
||||
|
||||
Field.fromInput = function (input, getterCoercer, setterCoercer) {
|
||||
getterCoercer = getterCoercer || Field.NO_COERCION;
|
||||
setterCoercer = setterCoercer || Field.NO_COERCION;
|
||||
return new Field(() => getterCoercer(input.val()), (value) => input.val(setterCoercer(value)));
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const CURRENT_SELECTION = {};
|
||||
106
web/app/3d/craft/craft.js
Normal file
106
web/app/3d/craft/craft.js
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import Counters from '../counters'
|
||||
|
||||
export function Craft(app) {
|
||||
this.app = app;
|
||||
this.history = [];
|
||||
this.solids = [];
|
||||
this._historyPointer = 0;
|
||||
Object.defineProperty(this, "historyPointer", {
|
||||
get: function() {return this._historyPointer},
|
||||
set: function(value) {
|
||||
if (this._historyPointer === value) return;
|
||||
this._historyPointer = value;
|
||||
this.reset(this.history.slice(0, this._historyPointer));
|
||||
this.app.bus.notify('craft');
|
||||
this.app.bus.notify('historyPointer');
|
||||
this.app.viewer.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Craft.prototype.remove = function(modificationIndex) {
|
||||
const history = this.history;
|
||||
history.splice(modificationIndex, history.length - modificationIndex);
|
||||
|
||||
if (this.historyPointer >= history.length) {
|
||||
this.finishHistoryEditing();
|
||||
} else {
|
||||
this.app.bus.notify('historyShrink');
|
||||
}
|
||||
};
|
||||
|
||||
Craft.prototype.loadHistory = function(history) {
|
||||
this.history = history;
|
||||
this._historyPointer = history.length;
|
||||
this.reset(history);
|
||||
this.app.bus.notify('craft');
|
||||
this.app.bus.notify('historyPointer');
|
||||
this.app.viewer.render();
|
||||
};
|
||||
|
||||
Craft.prototype.reset = function(modifications) {
|
||||
Counters.solid = 0;
|
||||
Counters.shared = 0;
|
||||
this.solids = [];
|
||||
this.app.findAllSolids().forEach(function(s) {s.vanish()});
|
||||
for (var i = 0; i < modifications.length; i++) {
|
||||
const request = modifications[i];
|
||||
this.modifyInternal(request);
|
||||
}
|
||||
};
|
||||
|
||||
Craft.prototype.finishHistoryEditing = function() {
|
||||
this.loadHistory(this.history);
|
||||
};
|
||||
|
||||
Craft.prototype.current = function() {
|
||||
return this.history[this.history.length - 1];
|
||||
};
|
||||
|
||||
|
||||
Craft.prototype.modifyInternal = function(request) {
|
||||
var op = this.operations[request.type];
|
||||
if (!op) return;
|
||||
|
||||
var newSolids = op(this.app, request.params);
|
||||
if (newSolids == null) return;
|
||||
const toUpdate = [];
|
||||
for (let i = 0; i < request.solids.length; i++) {
|
||||
let solid = request.solids[i];
|
||||
var indexToRemove = this.solids.indexOf(solid);
|
||||
if (indexToRemove != -1) {
|
||||
let updatedIdx = newSolids.findIndex((s) => s.id == solid.id);
|
||||
if (updatedIdx != -1) {
|
||||
toUpdate[updatedIdx] = indexToRemove;
|
||||
} else {
|
||||
this.solids.splice(indexToRemove, 1);
|
||||
}
|
||||
}
|
||||
solid.vanish();
|
||||
}
|
||||
for (let i = 0; i < newSolids.length; i++) {
|
||||
let solid = newSolids[i];
|
||||
if (toUpdate[i] !== undefined) {
|
||||
this.solids[toUpdate[i]] = solid;
|
||||
} else {
|
||||
this.solids.push(solid);
|
||||
}
|
||||
this.app.viewer.workGroup.add(solid.cadGroup);
|
||||
}
|
||||
this.app.bus.notify('solid-list', {
|
||||
solids: this.solids,
|
||||
needRefresh: newSolids
|
||||
});
|
||||
};
|
||||
|
||||
Craft.prototype.modify = function(request, overriding) {
|
||||
this.modifyInternal(request);
|
||||
if (!overriding && this._historyPointer != this.history.length) {
|
||||
this.history.splice(this._historyPointer + 1, 0, null);
|
||||
}
|
||||
this.history[this._historyPointer] = request;
|
||||
this._historyPointer ++;
|
||||
this.app.bus.notify('craft');
|
||||
this.app.bus.notify('historyPointer');
|
||||
this.app.viewer.render();
|
||||
};
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import {Matrix3} from '../math/l3space'
|
||||
import Vector from '../math/vector'
|
||||
import * as math from '../math/math'
|
||||
import {createShared} from './cad-utils'
|
||||
import {Matrix3} from '../../../math/l3space'
|
||||
import Vector from '../../../math/vector'
|
||||
import * as math from '../../../math/math'
|
||||
import {createShared} from '../../cad-utils'
|
||||
|
||||
function Group(derivedFrom) {
|
||||
this.polygons = [];
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import {AXIS, IDENTITY_BASIS} from '../../math/l3space'
|
||||
import * as tk from '../../ui/toolkit.js'
|
||||
import {FACE_COLOR} from '../cad-utils'
|
||||
import {AXIS, IDENTITY_BASIS} from '../../../../math/l3space'
|
||||
import * as tk from '../../../../ui/toolkit.js'
|
||||
import {FACE_COLOR} from '../../../cad-utils'
|
||||
import {Wizard} from './wizard-commons'
|
||||
|
||||
export function BoxWizard(viewer, initParams) {
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import * as tk from '../../ui/toolkit.js'
|
||||
import * as tk from '../../../../ui/toolkit.js'
|
||||
import * as workbench from '../workbench'
|
||||
import * as cad_utils from '../cad-utils'
|
||||
import Vector from '../../math/vector'
|
||||
import {Matrix3, ORIGIN} from '../../math/l3space'
|
||||
import * as cad_utils from '../../../cad-utils'
|
||||
import Vector from '../../../../math/vector'
|
||||
import {Matrix3, ORIGIN} from '../../../../math/l3space'
|
||||
import {OpWizard, IMAGINE_MATERIAL, BASE_MATERIAL} from './wizard-commons'
|
||||
|
||||
export function ExtrudeWizard(app, face, invert, initParams) {
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import * as tk from '../../ui/toolkit.js'
|
||||
import * as tk from '../../../../ui/toolkit.js'
|
||||
import * as workbench from '../workbench'
|
||||
import * as cad_utils from '../cad-utils'
|
||||
import Vector from '../../math/vector'
|
||||
import * as cad_utils from '../../../cad-utils'
|
||||
import Vector from '../../../../math/vector'
|
||||
import {Wizard} from './wizard-commons'
|
||||
import {LoadSTLFromURL} from '../io'
|
||||
import {LoadSTLFromURL} from '../../../io'
|
||||
|
||||
export function ImportWizard(viewer, initParams) {
|
||||
Wizard.call(this, viewer, initParams);
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import {AXIS, IDENTITY_BASIS} from '../../math/l3space'
|
||||
import * as tk from '../../ui/toolkit.js'
|
||||
import {FACE_COLOR} from '../cad-utils'
|
||||
import {AXIS, IDENTITY_BASIS} from '../../../../math/l3space'
|
||||
import * as tk from '../../../../ui/toolkit.js'
|
||||
import {FACE_COLOR} from '../../../cad-utils'
|
||||
import {Wizard} from './wizard-commons'
|
||||
import {Matrix3} from '../../math/l3space'
|
||||
import {Matrix3} from '../../../../math/l3space'
|
||||
|
||||
export function PlaneWizard(app, initParams) {
|
||||
Wizard.call(this, app.viewer, initParams);
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import * as tk from '../../ui/toolkit.js'
|
||||
import * as tk from '../../../../ui/toolkit.js'
|
||||
import * as workbench from '../workbench'
|
||||
import * as cad_utils from '../cad-utils'
|
||||
import Vector from '../../math/vector'
|
||||
import {Matrix3, ORIGIN} from '../../math/l3space'
|
||||
import * as cad_utils from '../../../cad-utils'
|
||||
import Vector from '../../../../math/vector'
|
||||
import {Matrix3, ORIGIN} from '../../../../math/l3space'
|
||||
import {revolveToTriangles} from '../revolve'
|
||||
import {OpWizard, IMAGINARY_SURFACE_MATERIAL, } from './wizard-commons'
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import {AXIS, IDENTITY_BASIS} from '../../math/l3space'
|
||||
import * as tk from '../../ui/toolkit.js'
|
||||
import {FACE_COLOR} from '../cad-utils'
|
||||
import {AXIS, IDENTITY_BASIS} from '../../../../math/l3space'
|
||||
import * as tk from '../../../../ui/toolkit.js'
|
||||
import {FACE_COLOR} from '../../../cad-utils'
|
||||
import {Wizard} from './wizard-commons'
|
||||
|
||||
export function SphereWizard(viewer, initParams) {
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import {AXIS, IDENTITY_BASIS} from '../../math/l3space'
|
||||
import * as tk from '../../ui/toolkit.js'
|
||||
import {FACE_COLOR} from '../cad-utils'
|
||||
import {AXIS, IDENTITY_BASIS} from '../../../../math/l3space'
|
||||
import * as tk from '../../../../ui/toolkit.js'
|
||||
import {FACE_COLOR} from '../../../cad-utils'
|
||||
import {Wizard} from './wizard-commons'
|
||||
|
||||
export function TransformWizard(viewer, solid, initParams) {
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import DPR from '../../utils/dpr'
|
||||
import * as tk from '../../ui/toolkit'
|
||||
import DPR from '../../../../utils/dpr'
|
||||
import * as tk from '../../../../ui/toolkit'
|
||||
|
||||
const IMAGINE_MATERIAL = new THREE.LineBasicMaterial({
|
||||
color: 0xFA8072,
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
import Vector from '../math/vector'
|
||||
import * as cad_utils from './cad-utils'
|
||||
import * as math from '../math/math'
|
||||
import {LUT} from '../math/bezier-cubic'
|
||||
import {Matrix3, AXIS, ORIGIN} from '../math/l3space'
|
||||
import {HashTable} from '../utils/hashmap'
|
||||
import Counters from './counters'
|
||||
import {Mesh} from './mesh'
|
||||
import {LoadSTLFromURL} from './io'
|
||||
import Vector from '../../../math/vector'
|
||||
import * as cad_utils from '../../cad-utils'
|
||||
import * as math from '../../../math/math'
|
||||
import {LUT} from '../../../math/bezier-cubic'
|
||||
import {Matrix3, AXIS, ORIGIN} from '../../../math/l3space'
|
||||
import {HashTable} from '../../../utils/hashmap'
|
||||
import {Mesh} from '../../mesh'
|
||||
import {LoadSTLFromURL} from '../../io'
|
||||
import revolve from './revolve'
|
||||
import {Triangulate} from './triangulation'
|
||||
import {Triangulate} from '../../triangulation'
|
||||
|
||||
function SketchConnection(a, b, sketchObject) {
|
||||
this.a = a;
|
||||
|
|
@ -200,7 +199,7 @@ export function getSketchedPolygons3D(app, face) {
|
|||
return sketchedPolygons;
|
||||
}
|
||||
|
||||
function sortPolygons(polygons) {
|
||||
export function sortPolygons(polygons) {
|
||||
function Loop(polygon) {
|
||||
this.polygon = polygon;
|
||||
this.nesting = [];
|
||||
|
|
@ -742,63 +741,6 @@ function recoverySketchInfo(polygons) {
|
|||
}
|
||||
}
|
||||
|
||||
export function Craft(app) {
|
||||
this.app = app;
|
||||
this.history = [];
|
||||
this.solids = [];
|
||||
this._historyPointer = 0;
|
||||
Object.defineProperty(this, "historyPointer", {
|
||||
get: function() {return this._historyPointer},
|
||||
set: function(value) {
|
||||
if (this._historyPointer === value) return;
|
||||
this._historyPointer = value;
|
||||
this.reset(this.history.slice(0, this._historyPointer));
|
||||
this.app.bus.notify('craft');
|
||||
this.app.bus.notify('historyPointer');
|
||||
this.app.viewer.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Craft.prototype.remove = function(modificationIndex) {
|
||||
const history = this.history;
|
||||
history.splice(modificationIndex, history.length - modificationIndex);
|
||||
|
||||
if (this.historyPointer >= history.length) {
|
||||
this.finishHistoryEditing();
|
||||
} else {
|
||||
this.app.bus.notify('historyShrink');
|
||||
}
|
||||
};
|
||||
|
||||
Craft.prototype.loadHistory = function(history) {
|
||||
this.history = history;
|
||||
this._historyPointer = history.length;
|
||||
this.reset(history);
|
||||
this.app.bus.notify('craft');
|
||||
this.app.bus.notify('historyPointer');
|
||||
this.app.viewer.render();
|
||||
};
|
||||
|
||||
Craft.prototype.reset = function(modifications) {
|
||||
Counters.solid = 0;
|
||||
Counters.shared = 0;
|
||||
this.solids = [];
|
||||
this.app.findAllSolids().forEach(function(s) {s.vanish()});
|
||||
for (var i = 0; i < modifications.length; i++) {
|
||||
var request = materialize(this.app.indexEntities(), modifications[i]);
|
||||
this.modifyInternal(request);
|
||||
}
|
||||
};
|
||||
|
||||
Craft.prototype.finishHistoryEditing = function() {
|
||||
this.loadHistory(this.history);
|
||||
};
|
||||
|
||||
Craft.prototype.current = function() {
|
||||
return this.history[this.history.length - 1];
|
||||
};
|
||||
|
||||
function detach(request) {
|
||||
var detachedConfig = {};
|
||||
for (var prop in request) {
|
||||
|
|
@ -849,55 +791,7 @@ function materialize(index, detachedConfig) {
|
|||
return request;
|
||||
}
|
||||
|
||||
Craft.prototype.modifyInternal = function(request) {
|
||||
var op = OPERATIONS[request.type];
|
||||
if (!op) return;
|
||||
|
||||
var newSolids = op(this.app, request);
|
||||
if (newSolids == null) return;
|
||||
const toUpdate = [];
|
||||
for (let i = 0; i < request.solids.length; i++) {
|
||||
let solid = request.solids[i];
|
||||
var indexToRemove = this.solids.indexOf(solid);
|
||||
if (indexToRemove != -1) {
|
||||
let updatedIdx = newSolids.findIndex((s) => s.id == solid.id);
|
||||
if (updatedIdx != -1) {
|
||||
toUpdate[updatedIdx] = indexToRemove;
|
||||
} else {
|
||||
this.solids.splice(indexToRemove, 1);
|
||||
}
|
||||
}
|
||||
solid.vanish();
|
||||
}
|
||||
for (let i = 0; i < newSolids.length; i++) {
|
||||
let solid = newSolids[i];
|
||||
if (toUpdate[i] !== undefined) {
|
||||
this.solids[toUpdate[i]] = solid;
|
||||
} else {
|
||||
this.solids.push(solid);
|
||||
}
|
||||
this.app.viewer.workGroup.add(solid.cadGroup);
|
||||
}
|
||||
this.app.bus.notify('solid-list', {
|
||||
solids: this.solids,
|
||||
needRefresh: newSolids
|
||||
});
|
||||
};
|
||||
|
||||
Craft.prototype.modify = function(request, overriding) {
|
||||
this.modifyInternal(request);
|
||||
var detachedRequest = detach(request);
|
||||
if (!overriding && this._historyPointer != this.history.length) {
|
||||
this.history.splice(this._historyPointer + 1, 0, null);
|
||||
}
|
||||
this.history[this._historyPointer] = detachedRequest;
|
||||
this._historyPointer ++;
|
||||
this.app.bus.notify('craft');
|
||||
this.app.bus.notify('historyPointer');
|
||||
this.app.viewer.render();
|
||||
};
|
||||
|
||||
export const OPERATIONS = {
|
||||
export const MESH_OPERATIONS = {
|
||||
CUT : cut,
|
||||
PAD : extrude,
|
||||
REVOLVE : performRevolve,
|
||||
|
|
@ -1,21 +1,30 @@
|
|||
import * as math from '../math/math'
|
||||
import {MESH_OPERATIONS} from './mesh/workbench'
|
||||
import {Extrude} from './brep/cut-extrude'
|
||||
|
||||
export const CUT = {
|
||||
icon: 'img/3d/cut',
|
||||
label: 'Cut',
|
||||
info: (p) => '(' + r(math.norm2(p.target)) + ')'
|
||||
info: (p) => '(' + r(p.depth) + ')',
|
||||
action: (app, request) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const PAD = {
|
||||
icon: 'img/3d/extrude',
|
||||
label: 'Extrude',
|
||||
info: (p) => '(' + r(math.norm2(p.target)) + ')'
|
||||
info: (p) => '(' + r(p.height) + ')',
|
||||
action: (app, request) => {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
export const REVOLVE = {
|
||||
icon: 'img/3d/revolve',
|
||||
label: 'Revolve',
|
||||
info: (p) => '(' + p.angle + ')'
|
||||
info: (p) => '(' + p.angle + ')',
|
||||
action: (app, request) => {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
export const SHELL = {
|
||||
|
|
@ -27,19 +36,28 @@ export const SHELL = {
|
|||
export const BOX = {
|
||||
icon: 'img/3d/cube',
|
||||
label: 'Box',
|
||||
info: (p) => '(' + p.w + ', ' + p.h + ', ' + p.d + ')'
|
||||
info: (p) => '(' + p.w + ', ' + p.h + ', ' + p.d + ')',
|
||||
action: (app, request) => {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
export const PLANE = {
|
||||
icon: 'img/3d/plane',
|
||||
label: 'Plane',
|
||||
info: (p) => '(' + p.depth + ')'
|
||||
info: (p) => '(' + p.depth + ')',
|
||||
action: (app, request) => {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
export const SPHERE = {
|
||||
icon: 'img/3d/sphere',
|
||||
label: 'Sphere',
|
||||
info: (p) => '(' + p.radius + ')'
|
||||
info: (p) => '(' + p.radius + ')',
|
||||
action: (app, request) => {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
export const INTERSECTION = {
|
||||
|
|
@ -63,7 +81,10 @@ export const UNION = {
|
|||
export const IMPORT_STL = {
|
||||
icon: 'img/3d/stl',
|
||||
label: 'STL Import',
|
||||
info: (p) => '(' + p.url.substring(p.url.lastIndexOf('/') + 1 ) + ')'
|
||||
info: (p) => '(' + p.url.substring(p.url.lastIndexOf('/') + 1 ) + ')',
|
||||
action: (app, request) => {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
function r(value) {
|
||||
|
|
@ -8,7 +8,8 @@ import {ActionManager} from './actions/actions'
|
|||
import * as AllActions from './actions/all-actions'
|
||||
import Vector from '../math/vector'
|
||||
import {Matrix3, AXIS, ORIGIN, IDENTITY_BASIS} from '../math/l3space'
|
||||
import * as workbench from './workbench'
|
||||
import {Craft} from './craft/craft'
|
||||
import * as workbench from './craft/mesh/workbench'
|
||||
import * as cad_utils from './cad-utils'
|
||||
import * as math from '../math/math'
|
||||
import {IO} from '../sketcher/io'
|
||||
|
|
@ -33,7 +34,7 @@ function App() {
|
|||
this.controlBar = new ControlBar(this, $('#control-bar'));
|
||||
|
||||
this.ui = new UI(this);
|
||||
this.craft = new workbench.Craft(this);
|
||||
this.craft = new Craft(this);
|
||||
|
||||
AddDebugSupport(this);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import {HashTable} from '../../utils/hashmap'
|
||||
import Vector from '../../math/vector'
|
||||
import Counters from '../counters'
|
||||
import {findOutline, segmentsToPaths, reconstructSketchBounds} from '../workbench'
|
||||
import {findOutline, segmentsToPaths, reconstructSketchBounds} from '../craft/mesh/workbench'
|
||||
import {Matrix3, AXIS} from '../../math/l3space'
|
||||
import {arrFlatten1L, isCurveClass} from '../cad-utils'
|
||||
import DPR from '../../utils/dpr'
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import {HashTable} from '../../utils/hashmap'
|
||||
import Vector from '../../math/vector'
|
||||
import Counters from '../counters'
|
||||
import {findOutline, segmentsToPaths} from '../workbench'
|
||||
import {Matrix3, AXIS} from '../../math/l3space'
|
||||
import {arrFlatten1L, isCurveClass} from '../cad-utils'
|
||||
import DPR from '../../utils/dpr'
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import * as tk from '../../ui/toolkit'
|
||||
import * as cad_utils from '../cad-utils'
|
||||
import * as math from '../../math/math'
|
||||
import * as workbench from '../workbench'
|
||||
import * as workbench from '../craft/mesh/workbench'
|
||||
import ToolBar from './toolbar'
|
||||
import * as MenuConfig from '../menu/menu-config'
|
||||
import * as Operations from '../operations'
|
||||
import * as Operations from '../craft/operations'
|
||||
import Menu from '../menu/menu'
|
||||
import {ExtrudeWizard} from '../wizards/extrude'
|
||||
import {RevolveWizard} from '../wizards/revolve'
|
||||
import {PlaneWizard} from '../wizards/plane'
|
||||
import {BoxWizard} from '../wizards/box'
|
||||
import {SphereWizard} from '../wizards/sphere'
|
||||
import {TransformWizard} from '../wizards/transform'
|
||||
import {ImportWizard} from '../wizards/import'
|
||||
import {ExtrudeWizard} from '../craft/mesh/wizards/extrude'
|
||||
import {RevolveWizard} from '../craft/mesh/wizards/revolve'
|
||||
import {PlaneWizard} from '../craft/mesh/wizards/plane'
|
||||
import {BoxWizard} from '../craft/mesh/wizards/box'
|
||||
import {SphereWizard} from '../craft/mesh/wizards/sphere'
|
||||
import {TransformWizard} from '../craft/mesh/wizards/transform'
|
||||
import {ImportWizard} from '../craft/mesh/wizards/import'
|
||||
import {LoadTemplate} from './utils'
|
||||
import {BindArray} from './bind'
|
||||
import {SolidList} from './solid-list'
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {LoadTemplate} from './utils'
|
||||
import {Bind} from './bind'
|
||||
import * as Operations from '../operations'
|
||||
import * as Operations from '../craft/operations'
|
||||
|
||||
export function ModificationsPanel(app) {
|
||||
this.app = app;
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
import {ExtrudeWizard} from './extrude'
|
||||
import * as workbench from '../workbench'
|
||||
import * as tk from '../../ui/toolkit.js'
|
||||
|
||||
|
||||
export function ShellWizard(app, face, initParams) {
|
||||
ExtrudeWizard.call(this, app, face, true, initParams);
|
||||
}
|
||||
|
||||
ShellWizard.prototype = Object.create( ExtrudeWizard.prototype );
|
||||
|
||||
ShellWizard.prototype.DEFAULT_PARAMS = [50, 1, 0, 0];
|
||||
|
||||
ShellWizard.prototype.title = function() {
|
||||
return "Create a Shell";
|
||||
};
|
||||
|
||||
ShellWizard.prototype.update = function(d) {
|
||||
ExtrudeWizard.prototype.update.call(this, d, 1, 0, 0);
|
||||
};
|
||||
|
||||
ExtrudeWizard.prototype.updatePolygons = function() {
|
||||
this.polygons = [];//workbench.reconstructOutline(this.face.solid.csg, this.face);
|
||||
};
|
||||
|
||||
ShellWizard.prototype.createUI = function(d) {
|
||||
this.ui.depth = tk.config(new tk.Number("Depth", d), {min : 0});
|
||||
tk.add(this.ui.folder, this.ui.depth);
|
||||
var onChange = tk.methodRef(this, "synch");
|
||||
this.ui.depth.input.on('t-change', onChange);
|
||||
};
|
||||
|
||||
ShellWizard.prototype.getParams = function() {
|
||||
return [Number(this.ui.depth.input.val())];
|
||||
};
|
||||
|
||||
ShellWizard.prototype.createRequest = function(done) {
|
||||
var params = this.getParams();
|
||||
done({
|
||||
type: 'SHELL',
|
||||
solids : [],
|
||||
params : {d : params[0]},
|
||||
protoParams : params
|
||||
});
|
||||
};
|
||||
|
|
@ -10,57 +10,87 @@ import * as cad_utils from '../3d/cad-utils'
|
|||
|
||||
|
||||
export function createPrism(basePoints, height) {
|
||||
const normal = cad_utils.normalOfCCWSeq(basePoints);
|
||||
const baseLoop = createPlaneLoop(basePoints.map(p => new Vertex(p)));
|
||||
const baseFace = createPlaneFace(normal, baseLoop);
|
||||
return new SimpleExtruder(height).extrude(basePoints);
|
||||
}
|
||||
|
||||
const lidNormal = normal.multiply(-1);
|
||||
const offVector = lidNormal.multiply(height);
|
||||
export class Extruder {
|
||||
|
||||
//iterateSegments(basePoints.map(p => new Vertex(p.plus(offVector))), (a, b) => lidSegments.push({a, b}));
|
||||
const lidPoints = basePoints.map(p => p.plus(offVector)).reverse();
|
||||
const lidLoop = createPlaneLoop(lidPoints.map(p => new Vertex(p)));
|
||||
|
||||
const shell = new Shell();
|
||||
|
||||
const n = baseLoop.halfEdges.length;
|
||||
for (let i = 0; i < n; i++) {
|
||||
let lidIdx = n - 2 - i;
|
||||
if (lidIdx == -1) {
|
||||
lidIdx = n - 1;
|
||||
}
|
||||
const baseHalfEdge = baseLoop.halfEdges[i];
|
||||
const lidHalfEdge = lidLoop.halfEdges[lidIdx];
|
||||
const wallPolygon = [baseHalfEdge.vertexB, baseHalfEdge.vertexA, lidHalfEdge.vertexB, lidHalfEdge.vertexA];
|
||||
const wallLoop = createPlaneLoop(wallPolygon);
|
||||
|
||||
const baseEdge = new Edge(Line.fromSegment(baseHalfEdge.vertexA.point, baseHalfEdge.vertexB.point));
|
||||
linkHalfEdges(baseEdge, baseHalfEdge, wallLoop.halfEdges[0]);
|
||||
|
||||
const lidEdge = new Edge(Line.fromSegment(lidHalfEdge.vertexA.point, lidHalfEdge.vertexB.point));
|
||||
linkHalfEdges(lidEdge, lidHalfEdge, wallLoop.halfEdges[2]);
|
||||
|
||||
const wallNormal = cad_utils.normalOfCCWSeq(wallPolygon.map(v => v.point));
|
||||
|
||||
const wallFace = createPlaneFace(wallNormal, wallLoop);
|
||||
wallFace.debugName = 'wall_' + i;
|
||||
|
||||
shell.faces.push(wallFace);
|
||||
prepareLidCalculation(baseNormal, lidNormal) {
|
||||
}
|
||||
const lidFace = createPlaneFace(lidNormal, lidLoop);
|
||||
iterateSegments(shell.faces, (a, b) => {
|
||||
const halfEdgeA = a.outerLoop.halfEdges[3];
|
||||
const halfEdgeB = b.outerLoop.halfEdges[1];
|
||||
const curve = Line.fromSegment(halfEdgeA.vertexA.point, halfEdgeA.vertexB.point);
|
||||
linkHalfEdges(new Edge(curve), halfEdgeA, halfEdgeB);
|
||||
});
|
||||
|
||||
baseFace.debugName = 'base';
|
||||
lidFace.debugName = 'lid';
|
||||
|
||||
shell.faces.push(baseFace, lidFace);
|
||||
shell.faces.forEach(f => f.shell = shell);
|
||||
return shell;
|
||||
calculateLid(basePoints) {
|
||||
throw 'not implemented';
|
||||
}
|
||||
|
||||
extrude(basePoints) {
|
||||
const normal = cad_utils.normalOfCCWSeq(basePoints);
|
||||
const baseLoop = createPlaneLoop(basePoints.map(p => new Vertex(p)));
|
||||
const baseFace = createPlaneFace(normal, baseLoop);
|
||||
const lidNormal = normal.multiply(-1);
|
||||
|
||||
this.prepareLidCalculation(normal, lidNormal);
|
||||
|
||||
//iterateSegments(basePoints.map(p => new Vertex(p.plus(offVector))), (a, b) => lidSegments.push({a, b}));
|
||||
const lidPoints = this.calculateLid(basePoints).reverse();
|
||||
const lidLoop = createPlaneLoop(lidPoints.map(p => new Vertex(p)));
|
||||
|
||||
const shell = new Shell();
|
||||
|
||||
const n = baseLoop.halfEdges.length;
|
||||
for (let i = 0; i < n; i++) {
|
||||
let lidIdx = n - 2 - i;
|
||||
if (lidIdx == -1) {
|
||||
lidIdx = n - 1;
|
||||
}
|
||||
const baseHalfEdge = baseLoop.halfEdges[i];
|
||||
const lidHalfEdge = lidLoop.halfEdges[lidIdx];
|
||||
const wallPolygon = [baseHalfEdge.vertexB, baseHalfEdge.vertexA, lidHalfEdge.vertexB, lidHalfEdge.vertexA];
|
||||
const wallLoop = createPlaneLoop(wallPolygon);
|
||||
|
||||
const baseEdge = new Edge(Line.fromSegment(baseHalfEdge.vertexA.point, baseHalfEdge.vertexB.point));
|
||||
linkHalfEdges(baseEdge, baseHalfEdge, wallLoop.halfEdges[0]);
|
||||
|
||||
const lidEdge = new Edge(Line.fromSegment(lidHalfEdge.vertexA.point, lidHalfEdge.vertexB.point));
|
||||
linkHalfEdges(lidEdge, lidHalfEdge, wallLoop.halfEdges[2]);
|
||||
|
||||
const wallNormal = cad_utils.normalOfCCWSeq(wallPolygon.map(v => v.point));
|
||||
|
||||
const wallFace = createPlaneFace(wallNormal, wallLoop);
|
||||
wallFace.role = 'wall:' + i;
|
||||
|
||||
shell.faces.push(wallFace);
|
||||
}
|
||||
const lidFace = createPlaneFace(lidNormal, lidLoop);
|
||||
iterateSegments(shell.faces, (a, b) => {
|
||||
const halfEdgeA = a.outerLoop.halfEdges[3];
|
||||
const halfEdgeB = b.outerLoop.halfEdges[1];
|
||||
const curve = Line.fromSegment(halfEdgeA.vertexA.point, halfEdgeA.vertexB.point);
|
||||
linkHalfEdges(new Edge(curve), halfEdgeA, halfEdgeB);
|
||||
});
|
||||
|
||||
baseFace.role = 'base';
|
||||
lidFace.role = 'lid';
|
||||
|
||||
shell.faces.push(baseFace, lidFace);
|
||||
shell.faces.forEach(f => f.shell = shell);
|
||||
return shell;
|
||||
}
|
||||
}
|
||||
|
||||
export class SimpleExtruder extends Extruder {
|
||||
|
||||
constructor(height) {
|
||||
super();
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
prepareLidCalculation(baseNormal, lidNormal) {
|
||||
this.extrudeVector = lidNormal.multiply(this.height);
|
||||
}
|
||||
|
||||
calculateLid(basePoints) {
|
||||
return basePoints.map(p => p.plus(this.extrudeVector))
|
||||
}
|
||||
}
|
||||
|
||||
function createPlaneFace(normal, loop) {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ export class Plane extends Surface {
|
|||
}
|
||||
|
||||
get2DTransformation() {
|
||||
return new Matrix3().setBasis(this.calculateBasis()).invert();
|
||||
return this.get3DTransformation().invert();
|
||||
}
|
||||
|
||||
get3DTransformation() {
|
||||
return new Matrix3().setBasis(this.calculateBasis());
|
||||
}
|
||||
}
|
||||
|
|
@ -11,20 +11,23 @@ export class Loop extends TopoObject {
|
|||
}
|
||||
|
||||
isCCW(surface) {
|
||||
const tr = surface.get2DTransformation();
|
||||
const polygon = this.asPolygon();
|
||||
const polygon2d = polygon.map(p => tr.apply(p));
|
||||
const lowestLeftIdx = math.findLowestLeftPoint(polygon2d);
|
||||
const n = polygon.length;
|
||||
const nextIdx = ((lowestLeftIdx + 1) % n);
|
||||
const prevIdx = ((n + lowestLeftIdx - 1) % n);
|
||||
const o = polygon[lowestLeftIdx];
|
||||
const first = polygon[nextIdx].minus(o);
|
||||
const last = o.minus(polygon[prevIdx]);
|
||||
return last.cross(first).dot(surface.normal) >= 0;
|
||||
Loop.isPolygonCCWOnSurface(this.asPolygon(), surface);
|
||||
}
|
||||
|
||||
asPolygon() {
|
||||
return this.halfEdges.map(e => e.vertexA.point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loop.isPolygonCCWOnSurface = function(polygon, surface) {
|
||||
const tr = surface.get2DTransformation();
|
||||
const polygon2d = polygon.map(p => tr.apply(p));
|
||||
const lowestLeftIdx = math.findLowestLeftPoint(polygon2d);
|
||||
const n = polygon.length;
|
||||
const nextIdx = ((lowestLeftIdx + 1) % n);
|
||||
const prevIdx = ((n + lowestLeftIdx - 1) % n);
|
||||
const o = polygon[lowestLeftIdx];
|
||||
const first = polygon[nextIdx].minus(o);
|
||||
const last = o.minus(polygon[prevIdx]);
|
||||
return last.cross(first).dot(surface.normal) >= 0;
|
||||
};
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
export class TopoObject {
|
||||
|
||||
constructor() {
|
||||
this.debugName = '';
|
||||
this.role = '';
|
||||
this.data = {};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue