stub in cam pocket op with surface selection
This commit is contained in:
parent
594a1daa07
commit
50dfba4e84
9 changed files with 182 additions and 40 deletions
|
|
@ -177,6 +177,7 @@ CAM.init = function(kiri, api) {
|
||||||
case "register": return func.opAddRegister('X', 2);
|
case "register": return func.opAddRegister('X', 2);
|
||||||
case "drill": return func.opAddDrill();
|
case "drill": return func.opAddDrill();
|
||||||
case "trace": return func.opAddTrace();
|
case "trace": return func.opAddTrace();
|
||||||
|
case "pocket": return func.opAddPocket();
|
||||||
case "flip":
|
case "flip":
|
||||||
// only one flip op permitted
|
// only one flip op permitted
|
||||||
for (let op of current.process.ops) {
|
for (let op of current.process.ops) {
|
||||||
|
|
@ -200,6 +201,10 @@ CAM.init = function(kiri, api) {
|
||||||
func.opAdd(popOp.outline.new());
|
func.opAdd(popOp.outline.new());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
func.opAddPocket = () => {
|
||||||
|
func.opAdd(popOp.pocket.new());
|
||||||
|
};
|
||||||
|
|
||||||
func.opAddContour = (axis) => {
|
func.opAddContour = (axis) => {
|
||||||
let rec = popOp.contour.new();
|
let rec = popOp.contour.new();
|
||||||
rec.axis = axis.toUpperCase();
|
rec.axis = axis.toUpperCase();
|
||||||
|
|
@ -553,15 +558,55 @@ CAM.init = function(kiri, api) {
|
||||||
iw.saveState();
|
iw.saveState();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// SURFACE FUNCS
|
||||||
|
let surfaceOn = false, lastWidget;
|
||||||
|
func.surfaceAdd = (ev) => {
|
||||||
|
func.unpop();
|
||||||
|
func.tabDone();
|
||||||
|
func.traceDone();
|
||||||
|
func.surfaceDone();
|
||||||
|
alert = api.show.alert("analyzing surfaces...", 1000);
|
||||||
|
CAM.surfaces(data => {
|
||||||
|
api.hide.alert(alert);
|
||||||
|
alert = api.show.alert("[esc] cancels surface editing");
|
||||||
|
});
|
||||||
|
surfaceOn = hoveredOp;
|
||||||
|
surfaceOn.classList.add("editing");
|
||||||
|
api.feature.on_mouse_up = (obj, ev) => {
|
||||||
|
let { face } = obj;
|
||||||
|
let min = Math.min(face.a, face.b, face.c);
|
||||||
|
let widget = lastWidget = obj.object.widget;
|
||||||
|
CAM.surface_find(widget.id, min / 3, faces => {
|
||||||
|
if (faces && faces.length) {
|
||||||
|
widget.selectFaces(faces);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
func.surfaceDone = () => {
|
||||||
|
if (!surfaceOn) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lastWidget.selectFaces([]);
|
||||||
|
api.feature.on_mouse_up = undefined;
|
||||||
|
surfaceOn.classList.remove("editing");
|
||||||
|
surfaceOn = false;
|
||||||
|
};
|
||||||
|
|
||||||
// TRACE FUNCS
|
// TRACE FUNCS
|
||||||
let traceOn = false, lastTrace;
|
let traceOn = false, lastTrace;
|
||||||
func.traceAdd = (ev) => {
|
func.traceAdd = (ev) => {
|
||||||
func.unpop();
|
func.unpop();
|
||||||
func.tabDone();
|
func.tabDone();
|
||||||
func.traceDone();
|
func.traceDone();
|
||||||
|
func.surfaceDone();
|
||||||
alert = api.show.alert("analyzing parts...", 1000);
|
alert = api.show.alert("analyzing parts...", 1000);
|
||||||
traceOn = hoveredOp;
|
traceOn = hoveredOp;
|
||||||
traceOn.classList.add("editing");
|
traceOn.classList.add("editing");
|
||||||
|
api.feature.hover = true;
|
||||||
|
api.feature.hoverAdds = true;
|
||||||
|
func.hover = func.traceHover;
|
||||||
|
func.hoverUp = func.traceHoverUp;
|
||||||
CAM.traces((ids) => {
|
CAM.traces((ids) => {
|
||||||
api.hide.alert(alert);
|
api.hide.alert(alert);
|
||||||
alert = api.show.alert("[esc] cancels trace editing");
|
alert = api.show.alert("[esc] cancels trace editing");
|
||||||
|
|
@ -613,10 +658,6 @@ CAM.init = function(kiri, api) {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}, poppedRec.select === 'lines');
|
}, poppedRec.select === 'lines');
|
||||||
api.feature.hover = true;
|
|
||||||
api.feature.hoverAdds = true;
|
|
||||||
func.hover = func.traceHover;
|
|
||||||
func.hoverUp = func.traceHoverUp;
|
|
||||||
};
|
};
|
||||||
func.traceDone = () => {
|
func.traceDone = () => {
|
||||||
if (!traceOn) {
|
if (!traceOn) {
|
||||||
|
|
@ -726,12 +767,14 @@ CAM.init = function(kiri, api) {
|
||||||
if (isCamMode) {
|
if (isCamMode) {
|
||||||
func.tabDone();
|
func.tabDone();
|
||||||
func.traceDone();
|
func.traceDone();
|
||||||
|
func.surfaceDone();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
api.event.on("key.esc", () => {
|
api.event.on("key.esc", () => {
|
||||||
if (isCamMode) {
|
if (isCamMode) {
|
||||||
func.tabDone();
|
func.tabDone();
|
||||||
func.traceDone();
|
func.traceDone();
|
||||||
|
func.surfaceDone();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
api.event.on("selection.scale", () => {
|
api.event.on("selection.scale", () => {
|
||||||
|
|
@ -954,7 +997,6 @@ CAM.init = function(kiri, api) {
|
||||||
down: 'camTraceDown',
|
down: 'camTraceDown',
|
||||||
rate: 'camTraceSpeed',
|
rate: 'camTraceSpeed',
|
||||||
plunge: 'camTracePlunge',
|
plunge: 'camTracePlunge',
|
||||||
// single: 'camTraceLines',
|
|
||||||
select: 'camTraceMode'
|
select: 'camTraceMode'
|
||||||
}).inputs = {
|
}).inputs = {
|
||||||
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
|
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
|
||||||
|
|
@ -967,7 +1009,6 @@ CAM.init = function(kiri, api) {
|
||||||
rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}),
|
rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}),
|
||||||
plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}),
|
plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}),
|
||||||
sep: UC.newBlank({class:"pop-sep"}),
|
sep: UC.newBlank({class:"pop-sep"}),
|
||||||
// single: UC.newBoolean(LANG.cc_sngl_s, undefined, {title:LANG.cc_sngl_l}),
|
|
||||||
select: UC.newSelect(LANG.cc_sele_s, {title:LANG.cc_sele_l}, "select"),
|
select: UC.newSelect(LANG.cc_sele_s, {title:LANG.cc_sele_l}, "select"),
|
||||||
menu: UC.newRow([
|
menu: UC.newRow([
|
||||||
UC.newButton(undefined, func.traceAdd, {icon:'<i class="fas fa-plus"></i>'}),
|
UC.newButton(undefined, func.traceAdd, {icon:'<i class="fas fa-plus"></i>'}),
|
||||||
|
|
@ -975,6 +1016,28 @@ CAM.init = function(kiri, api) {
|
||||||
], {class:"ext-buttons f-row"}),
|
], {class:"ext-buttons f-row"}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
createPopOp('pocket', {
|
||||||
|
spindle: 'camPocketSpindle',
|
||||||
|
tool: 'camPocketTool',
|
||||||
|
step: 'camPocketOver',
|
||||||
|
down: 'camPocketDown',
|
||||||
|
rate: 'camPocketSpeed',
|
||||||
|
plunge: 'camPocketPlunge',
|
||||||
|
}).inputs = {
|
||||||
|
tool: UC.newSelect(LANG.cc_tool, {}, "tools"),
|
||||||
|
sep: UC.newBlank({class:"pop-sep"}),
|
||||||
|
spindle: UC.newInput(LANG.cc_spnd_s, {title:LANG.cc_spnd_l, convert:UC.toInt, show:hasSpindle}),
|
||||||
|
step: UC.newInput(LANG.cc_sovr_s, {title:LANG.cc_sovr_l, convert:UC.toFloat, bound:UC.bound(0.01,1.0)}),
|
||||||
|
down: UC.newInput(LANG.cc_sdwn_s, {title:LANG.cc_sdwn_l, convert:UC.toFloat, units:true}),
|
||||||
|
rate: UC.newInput(LANG.cc_feed_s, {title:LANG.cc_feed_l, convert:UC.toInt, units:true}),
|
||||||
|
plunge: UC.newInput(LANG.cc_plng_s, {title:LANG.cc_plng_l, convert:UC.toInt, units:true}),
|
||||||
|
sep: UC.newBlank({class:"pop-sep"}),
|
||||||
|
menu: UC.newRow([
|
||||||
|
UC.newButton(undefined, func.surfaceAdd, {icon:'<i class="fas fa-plus"></i>'}),
|
||||||
|
UC.newButton(undefined, func.surfaceDone, {icon:'<i class="fas fa-check"></i>'}),
|
||||||
|
], {class:"ext-buttons f-row"}),
|
||||||
|
};
|
||||||
|
|
||||||
createPopOp('drill', {
|
createPopOp('drill', {
|
||||||
tool: 'camDrillTool',
|
tool: 'camDrillTool',
|
||||||
spindle: 'camDrillSpindle',
|
spindle: 'camDrillSpindle',
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
// dep: main.kiri
|
// dep: main.kiri
|
||||||
// use: kiri.codec
|
// use: kiri.codec
|
||||||
|
// use: mesh.tool
|
||||||
gapp.register("kiri-mode.cam.driver", [], (root, exports) => {
|
gapp.register("kiri-mode.cam.driver", [], (root, exports) => {
|
||||||
|
|
||||||
const { kiri } = root;
|
const { kiri } = root;
|
||||||
|
|
@ -49,6 +50,42 @@ kiri.load(api => {
|
||||||
} } )));
|
} } )));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (kiri.client)
|
||||||
|
CAM.surfaces = function(ondone) {
|
||||||
|
kiri.client.sync();
|
||||||
|
const settings = api.conf.get();
|
||||||
|
kiri.client.send("cam_surfaces", { settings }, output => {
|
||||||
|
ondone(output);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (kiri.worker)
|
||||||
|
kiri.worker.cam_surfaces = function(data, send) {
|
||||||
|
const { settings } = data;
|
||||||
|
const widgets = Object.values(kiri.worker.cache);
|
||||||
|
for (let widget of widgets) {
|
||||||
|
if (!widget.tool) {
|
||||||
|
let tool = widget.tool = new mesh.tool();
|
||||||
|
tool.generateFaceMap(widget.getVertices().array);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
send.done({});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (kiri.client)
|
||||||
|
CAM.surface_find = function(id, face, ondone) {
|
||||||
|
kiri.client.send("cam_surface_find", { id, face }, faces => {
|
||||||
|
ondone(faces);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (kiri.worker)
|
||||||
|
kiri.worker.cam_surface_find = function(data, send) {
|
||||||
|
const { id, face } = data;
|
||||||
|
const widget = kiri.worker.cache[id];
|
||||||
|
const faces = widget.tool.findConnectedSurface([face], 0.1, 0.001);
|
||||||
|
send.done(faces);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ const feature = {
|
||||||
on_key: undefined, // function override default key handlers
|
on_key: undefined, // function override default key handlers
|
||||||
on_load: undefined, // function override file drop loads
|
on_load: undefined, // function override file drop loads
|
||||||
on_add_stl: undefined, // legacy override stl drop loads
|
on_add_stl: undefined, // legacy override stl drop loads
|
||||||
|
on_mouse_up: undefined, // function intercepts mouse up select
|
||||||
|
on_mouse_down: undefined, // function intercepts mouse down
|
||||||
work_alerts: true, // allow disabling work progress alerts
|
work_alerts: true, // allow disabling work progress alerts
|
||||||
modes: [ "fdm", "sla", "cam", "laser" ], // enable device modes
|
modes: [ "fdm", "sla", "cam", "laser" ], // enable device modes
|
||||||
pmode: consts.PMODES.SPEED, // preview modes
|
pmode: consts.PMODES.SPEED, // preview modes
|
||||||
|
|
|
||||||
|
|
@ -492,6 +492,12 @@ const conf = exports({
|
||||||
camTraceSpeed: 250,
|
camTraceSpeed: 250,
|
||||||
camTracePlunge: 200,
|
camTracePlunge: 200,
|
||||||
camTraceLines: false,
|
camTraceLines: false,
|
||||||
|
camPocketSpindle: 1000,
|
||||||
|
camPocketTool: 1000,
|
||||||
|
camPocketOver: 0.5,
|
||||||
|
camPocketDown: 1,
|
||||||
|
camPocketSpeed: 250,
|
||||||
|
camPocketPlunge: 200,
|
||||||
camDrillTool: 1000,
|
camDrillTool: 1000,
|
||||||
camDrillSpindle: 1000,
|
camDrillSpindle: 1000,
|
||||||
camDrillDownSpeed: 250,
|
camDrillDownSpeed: 250,
|
||||||
|
|
|
||||||
|
|
@ -2423,13 +2423,19 @@ gapp.register("kiri.init", [], (root, exports) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
space.mouse.downSelect((int,event) => {
|
space.mouse.downSelect((int, event) => {
|
||||||
if (api.feature.hover) {
|
if (api.feature.on_mouse_down) {
|
||||||
if (int) {
|
if (int) {
|
||||||
api.event.emit('mouse.hover.down', {int, point: int.point});
|
api.feature.on_mouse_down(int, event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return;
|
}
|
||||||
|
if (api.feature.hover) {
|
||||||
|
if (int) {
|
||||||
|
return api.event.emit('mouse.hover.down', {int, point: int.point});
|
||||||
|
} else {
|
||||||
|
return api.selection.meshes();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// lay flat with meta or ctrl clicking a selected face
|
// lay flat with meta or ctrl clicking a selected face
|
||||||
if (int && (event.ctrlKey || event.metaKey || api.feature.on_face_select)) {
|
if (int && (event.ctrlKey || event.metaKey || api.feature.on_face_select)) {
|
||||||
|
|
@ -2447,7 +2453,14 @@ gapp.register("kiri.init", [], (root, exports) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
space.mouse.upSelect(function(object, event) {
|
space.mouse.upSelect((object, event) => {
|
||||||
|
if (api.feature.on_mouse_up) {
|
||||||
|
if (event && object) {
|
||||||
|
return api.feature.on_mouse_up(object, event);
|
||||||
|
} else {
|
||||||
|
return api.widgets.meshes();
|
||||||
|
}
|
||||||
|
}
|
||||||
if (event && api.feature.hover) {
|
if (event && api.feature.hover) {
|
||||||
api.event.emit('mouse.hover.up', { object, event });
|
api.event.emit('mouse.hover.up', { object, event });
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,16 @@
|
||||||
// dep: geo.polygons
|
// dep: geo.polygons
|
||||||
// dep: kiri.utils
|
// dep: kiri.utils
|
||||||
// use: kiri.codec
|
// use: kiri.codec
|
||||||
|
// use: mesh.util
|
||||||
gapp.register("kiri.widget", [], (root, exports) => {
|
gapp.register("kiri.widget", [], (root, exports) => {
|
||||||
|
|
||||||
const { base, kiri } = root;
|
const { base, kiri } = root;
|
||||||
const { api, driver, utils } = kiri;
|
const { api, driver, utils } = kiri;
|
||||||
const { config, util, polygons } = base;
|
const { util, polygons } = base;
|
||||||
const { Mesh, newPoint, verticesToPoints } = base;
|
const { Mesh, newPoint, verticesToPoints } = base;
|
||||||
const { inRange, time } = util;
|
const { inRange, time } = util;
|
||||||
const { rgb, avgc } = utils;
|
const { avgc } = utils;
|
||||||
|
|
||||||
const POLY = polygons;
|
|
||||||
const solid_opacity = 1.0;
|
const solid_opacity = 1.0;
|
||||||
const groups = [];
|
const groups = [];
|
||||||
|
|
||||||
|
|
@ -299,15 +299,22 @@ class Widget {
|
||||||
*/
|
*/
|
||||||
loadGeometry(geometry) {
|
loadGeometry(geometry) {
|
||||||
const mesh = new THREE.Mesh(
|
const mesh = new THREE.Mesh(
|
||||||
geometry,
|
geometry, [
|
||||||
[ new THREE.MeshPhongMaterial({
|
new THREE.MeshPhongMaterial({
|
||||||
color: 0xffff00,
|
color: 0xffff00,
|
||||||
specular: 0x202020,
|
specular: 0x202020,
|
||||||
shininess: 125,
|
shininess: 125,
|
||||||
transparent: true,
|
transparent: true,
|
||||||
opacity: solid_opacity
|
opacity: solid_opacity
|
||||||
}) ]
|
}),
|
||||||
);
|
new THREE.MeshPhongMaterial({
|
||||||
|
color: 0x0088ee,
|
||||||
|
specular: 0x202020,
|
||||||
|
shininess: 100,
|
||||||
|
transparent: true,
|
||||||
|
opacity: solid_opacity
|
||||||
|
}),
|
||||||
|
]);
|
||||||
mesh.renderOrder = 1;
|
mesh.renderOrder = 1;
|
||||||
geometry.computeVertexNormals();
|
geometry.computeVertexNormals();
|
||||||
geometry.addGroup(0, Infinity, 0);
|
geometry.addGroup(0, Infinity, 0);
|
||||||
|
|
@ -377,6 +384,15 @@ class Widget {
|
||||||
return this.getMaterial().visible;
|
return this.getMaterial().visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectFaces(faces) {
|
||||||
|
let groups = mesh.util.facesToGroups(faces || []);
|
||||||
|
let geo = this.mesh.geometry;
|
||||||
|
geo.clearGroups();
|
||||||
|
for (let group of groups) {
|
||||||
|
geo.addGroup(group.start*3, group.count*3, group.mat || 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} value
|
* @param {number} value
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -519,28 +519,7 @@ mesh.model = class MeshModel extends mesh.object {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelections() {
|
updateSelections() {
|
||||||
let faces = this.sel.faces;
|
let groups = mesh.util.facesToGroups(this.sel.faces);
|
||||||
let groups = [];
|
|
||||||
if (faces && faces.length) {
|
|
||||||
faces = faces.sort((a,b) => a - b).slice();
|
|
||||||
let first = faces.shift();
|
|
||||||
if (first > 0) {
|
|
||||||
groups.push({ start: 0, count: first, mat: 0 });
|
|
||||||
}
|
|
||||||
let range = { start: first, count: 1, mat: 1 };
|
|
||||||
groups.push(range);
|
|
||||||
for (let face of faces) {
|
|
||||||
if (face === range.start + range.count) {
|
|
||||||
range.count++;
|
|
||||||
} else {
|
|
||||||
groups.push(range = { start: range.start + range.count, count: face - range.start - range.count });
|
|
||||||
groups.push(range = { start: face, count: 1, mat: 1 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
groups.push({ start: range.start + range.count, count: Infinity });
|
|
||||||
} else {
|
|
||||||
groups.push({ start: 0, count: Infinity });
|
|
||||||
}
|
|
||||||
let geo = this.mesh.geometry;
|
let geo = this.mesh.geometry;
|
||||||
geo.clearGroups();
|
geo.clearGroups();
|
||||||
for (let group of groups) {
|
for (let group of groups) {
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,31 @@ const util = exports({
|
||||||
}
|
}
|
||||||
|
|
||||||
return new VertexNormalsHelper(mesh);
|
return new VertexNormalsHelper(mesh);
|
||||||
|
},
|
||||||
|
|
||||||
|
facesToGroups(faces) {
|
||||||
|
let groups = [];
|
||||||
|
if (faces && faces.length) {
|
||||||
|
faces = faces.sort((a,b) => a - b).slice();
|
||||||
|
let first = faces.shift();
|
||||||
|
if (first > 0) {
|
||||||
|
groups.push({ start: 0, count: first, mat: 0 });
|
||||||
|
}
|
||||||
|
let range = { start: first, count: 1, mat: 1 };
|
||||||
|
groups.push(range);
|
||||||
|
for (let face of faces) {
|
||||||
|
if (face === range.start + range.count) {
|
||||||
|
range.count++;
|
||||||
|
} else {
|
||||||
|
groups.push(range = { start: range.start + range.count, count: face - range.start - range.count });
|
||||||
|
groups.push(range = { start: face, count: 1, mat: 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
groups.push({ start: range.start + range.count, count: Infinity });
|
||||||
|
} else {
|
||||||
|
groups.push({ start: 0, count: Infinity });
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -477,6 +477,7 @@
|
||||||
<div title="remove an area of material inside a defined boundary sometimes called pocketing">rough</div>
|
<div title="remove an area of material inside a defined boundary sometimes called pocketing">rough</div>
|
||||||
<div title="clean up after roughing or perform a part cutout">outline</div>
|
<div title="clean up after roughing or perform a part cutout">outline</div>
|
||||||
<div title="2.5D tracing along X or Y axis used for complex part features">contour</div>
|
<div title="2.5D tracing along X or Y axis used for complex part features">contour</div>
|
||||||
|
<div title="clear areas above selected surfaces">pocket</div>
|
||||||
<div title="follow selected features with a specified tool. often used for lettering and engraving">trace</div>
|
<div title="follow selected features with a specified tool. often used for lettering and engraving">trace</div>
|
||||||
<div title="drill any holes matching selected tool diameter">drill</div>
|
<div title="drill any holes matching selected tool diameter">drill</div>
|
||||||
<hr width="100%">
|
<hr width="100%">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue