diff --git a/src/geo/base.js b/src/geo/base.js index 72f5ac92..21e7d826 100644 --- a/src/geo/base.js +++ b/src/geo/base.js @@ -38,8 +38,8 @@ let count = 0; if (tracker) for (let p of promises) { - p.then(() => { - tracker(count++, promises.length); + p.then(data => { + tracker(count++, promises.length, data); }); } await Promise.all(promises); diff --git a/src/kiri/minion.js b/src/kiri/minion.js index 30195e3b..bd690fc5 100644 --- a/src/kiri/minion.js +++ b/src/kiri/minion.js @@ -92,10 +92,10 @@ const funcs = { realp[p++] = BASE.newPoint(points[i++], points[i++], points[i++]); } let output = []; - for (let slice of slices) { + for (let params of slices) { output.push({ - slice, - tops: KIRI.slicer.sliceZ(slice.z, realp, options).tops + params, + data: { tops: KIRI.slicer.sliceZ(params.z, realp, options, params).tops } }); } reply({ seq, output: CODEC.encode(output) }); diff --git a/src/kiri/slice.js b/src/kiri/slice.js index 2e2ef02c..b4e78f59 100644 --- a/src/kiri/slice.js +++ b/src/kiri/slice.js @@ -176,9 +176,21 @@ } PRO.addTop = function(poly) { - let top = new Top(poly); - this.tops.push(top); - return top; + if (poly.length) { + let top = new Top(poly); + this.tops.push(top); + return top; + } else { + let top = new Top(poly.poly); + top.fill_lines = poly.fill_lines; + top.fill_off = poly.fill_off; + top.last = poly.last; + top.shells = poly.shells; + this.tops.push(top); + return top; + } + console.log('top', poly); + throw "invalid top"; }; PRO.findClosestPointTo = function(target) { diff --git a/src/kiri/slicer.js b/src/kiri/slicer.js index 0af088c7..9940120f 100644 --- a/src/kiri/slicer.js +++ b/src/kiri/slicer.js @@ -2,6 +2,9 @@ "use strict"; +/** + * Slicing engine used by FDM, Laser, and SLA + */ (function() { if (self.kiri.slicer) return; @@ -10,7 +13,8 @@ slice, sliceZ, sliceWidget, - connectLines + connectLines, + createSlice }; let KIRI = self.kiri, @@ -138,8 +142,8 @@ if (concurrent > 1) { if (bucketCount < concurrent) { bucketCount = concurrent; - } else if (bucketCount > 128) { - bucketCount = 128; + } else if (bucketCount > 100) { + bucketCount = 100; } } @@ -302,42 +306,65 @@ function bucketZ(index, z, height, thick) { buckets[Math.floor(z * zScale)].slices.push({ - index, z, height, thick + index, z, height, thick, total: zIndexes.length }); } async function sliceBuckets() { let output = []; - // console.log({buckets}); - if (concurrent < 2) { - sliceBucketsLocal(buckets, output); - } else { - let promises = []; - for (let bucket of buckets) { - promises.push(KIRI.minions.sliceBucket(bucket, options, output)); - } - await tracker(promises, (i,t) => { + if (concurrent) { + let promises = buckets.map( + bucket => KIRI.minions.sliceBucket(bucket, options, output) + ); + await tracker(promises, (i,t,d) => { onupdate(0.1 + (i / t) * 0.9); }); + } else { + let count = 0; + for (let bucket of buckets) { + for (let params of bucket.slices) { + output.push(createSlice( + params, + sliceZ(params.z, bucket.points, options, params), + options + )); + onupdate(0.1 + (count++ / zIndexes.length) * 0.9); + } + } } return output; } - function sliceBucketsLocal(buckets, output) { - let count = 0; - for (let bucket of buckets) { - for (let slice of bucket.slices) { - let { index, z, height, thick } = slice; - let { lines, groups, tops } = sliceZ(z, bucket.points, options); - slice = newSlice(z).addTops(tops); - slice.height = height; - slice.index = index; - slice.thick = thick; - output.push(slice); - onupdate(0.1 + (count++ / zIndexes.length) * 0.9); - } + } + + function createSlice(params, data, options = {}) { + let { index, z, height, thick } = params; + let { lines, groups, tops } = data; + let slice = newSlice(z).addTops(tops); + slice.height = height; + slice.index = index; + slice.thick = thick; + // debugging (non-threaded mode only) + let { debug, xray, view } = options; + if (view && (debug || xray)) { + if ((debug && lines.excessive) || xray) { + const dash = xray || 3; + lines.forEach((line, i) => { + const group = i % dash; + const color = [ 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff ][group]; + slice.output().setLayer(`xl-${group}`, color).addLine(line.p1, line.p2); + }); } + POLY.nest(groups).forEach((poly, i) => { + slice.addTop(poly); + if (xray) { + const group = i % (xray || 3); + const color = [ 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff ][group]; + slice.output().setLayer(`xg-${group}`, color).addPoly(poly); + } + }); } + return slice; } /** ***** SLICING FUNCTIONS ***** */ @@ -421,7 +448,7 @@ * @param {number} z * @param {number} [height] optional real height (fdm) */ - function sliceZ(z, points, options = {}) { + function sliceZ(z, points, options = {}, params = {}) { let phash = {}, lines = [], p1, p2, p3; @@ -477,28 +504,18 @@ groups = POLY.flatten(POLY.union(POLY.nest(groups), null, true), null, true); } - // if ((debug && lines.excessive) || xray) { - // const dash = xray || 3; - // lines.forEach((line, i) => { - // const group = i % dash; - // const color = [ 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff ][group]; - // slice.output().setLayer(`xl-${group}`, color).addLine(line.p1, line.p2); - // }); - // } - let tops = POLY.nest(groups); + let data = { lines, groups, tops }; - // identify and add tops to slice - // POLY.nest(groups).forEach((poly, i) => { - // slice.addTop(poly); - // if (xray) { - // const group = i % (xray || 3); - // const color = [ 0xff0000, 0x00ff00, 0x0000ff, 0xffff00, 0xff00ff ][group]; - // slice.output().setLayer(`xg-${group}`, color).addPoly(poly); - // } - // }); + // look for driver-specific slice post-processor + if (options.mode) { + let fn = KIRI.driver[options.mode].slicePost; + if (fn) { + fn(data, options, params); + } + } - return { lines, groups, tops }; + return data; } /** diff --git a/src/kiri/slicer2.js b/src/kiri/slicer2.js index 8e71be92..ca95920d 100644 --- a/src/kiri/slicer2.js +++ b/src/kiri/slicer2.js @@ -2,6 +2,9 @@ "use strict"; +/** + * Slicing engine used by CAM + */ (function() { let KIRI = self.kiri, diff --git a/src/kiri/worker.js b/src/kiri/worker.js index cc4ac5e8..a4e95e5a 100644 --- a/src/kiri/worker.js +++ b/src/kiri/worker.js @@ -145,16 +145,12 @@ KIRI.minions = { slices, options }, data => { - for (let rec of KIRI.codec.decode(data.output)) { - let { slice, tops } = rec; - let newSlice = KIRI.newSlice(slice.z); - newSlice.index = slice.index; - newSlice.thick = slice.thick; - newSlice.height = slice.height; - newSlice.addTops(tops); - output.push(newSlice); + let recs = KIRI.codec.decode(data.output); + for (let rec of recs) { + let { params, data } = rec; + output.push(KIRI.slicer.createSlice(params, data)); } - resolve(); + resolve(recs); }, [ floatP.buffer ]); }); }, diff --git a/src/mode/fdm/driver.js b/src/mode/fdm/driver.js index b50bb300..652e4081 100644 --- a/src/mode/fdm/driver.js +++ b/src/mode/fdm/driver.js @@ -29,15 +29,15 @@ return settings; } - function getRangeParameters(settings, index) { + function getRangeParameters(process, index) { if (index === undefined || index === null || index < 0) { - return settings.process; + return process; } - let ranges = settings.process.ranges; + let ranges = process.ranges; if (!(ranges && ranges.length)) { - return settings.process; + return process; } - let params = Object.clone(settings.process); + let params = Object.clone(process); for (let range of ranges) { if (index >= range.lo && index <= range.hi) { for (let [key,value] of Object.entries(range.fields)) { diff --git a/src/mode/fdm/export.js b/src/mode/fdm/export.js index b63ee5e4..e237bb1c 100644 --- a/src/mode/fdm/export.js +++ b/src/mode/fdm/export.js @@ -113,7 +113,7 @@ // smallish band-aid. refactor above to remove redundancy function updateParams(layer) { - let process = getRangeParameters(settings, layer); + let process = getRangeParameters(process, layer); zhop = process.zHopDistance || 0; // range retDist = process.outputRetractDist || 0; // range retSpeed = process.outputRetractSpeed * 60 || 1; // range diff --git a/src/mode/fdm/prepare.js b/src/mode/fdm/prepare.js index 45ea341f..f1fdc2f4 100644 --- a/src/mode/fdm/prepare.js +++ b/src/mode/fdm/prepare.js @@ -374,7 +374,7 @@ return a.dst - b.dst; }); let { z, slice, offset } = order[0]; - let params = getRangeParameters(settings, slice.index); + let params = getRangeParameters(process, slice.index); slice.prep = true; // retract between widgets or layers (when set) if (layerout.length && slice.widget !== lastWidget) { @@ -474,7 +474,7 @@ // iterate over layers, find extrusion on belt and // apply corrections and add brim when specified for (let layer of output) { - let params = getRangeParameters(settings, layer.layer || 0); + let params = getRangeParameters(process, layer.layer || 0); let brimHalf = params.firstLayerBrim < 0; let firstLayerBrim = Math.abs(params.firstLayerBrim); let firstLayerBrimIn = params.firstLayerBrimIn; diff --git a/src/mode/fdm/slice.js b/src/mode/fdm/slice.js index 0789507b..6d00eaab 100644 --- a/src/mode/fdm/slice.js +++ b/src/mode/fdm/slice.js @@ -28,6 +28,7 @@ PROTO = Object.clone(COLOR), bwcomp = (1 / Math.cos(Math.PI/4)), getRangeParameters = FDM.getRangeParameters, + slicePost = true, debug = false; let isThin = false; // force line rendering @@ -46,6 +47,49 @@ return opt; } + // console.log('slicing', {debug, slicePost}); + + /** + * may run in minion or worker context. do not create objects + * that will not quickly encode in threaded mode. add to existing + * data object. return is ignored. + */ + FDM.slicePost = slicePost ? function(data, options, params) { + let { lines, groups, tops } = data; + let { z, index, total, height, thick } = params; + let { process, isSynth, isDanger, vaseMode, shellOffset, fillOffset } = options.post; + let range = getRangeParameters(process, index); + let shellFrac = (range.sliceShells - (range.sliceShells | 0)); + let sliceShells = range.sliceShells | 0; + if (shellFrac) { + let v1 = shellFrac > 0.5 ? 1 - shellFrac : shellFrac; + let v2 = 1 - v1; + let parts = Math.round(v2/v1) + 1; + let rem = index % parts; + let trg = shellFrac > 0.5 ? 1 : parts - 1; + sliceShells += rem >= trg ? 1 : 0; + } + let spaceMult = index === 0 ? process.firstLayerLineMult || 1 : 1; + let isBottom = index < process.sliceBottomLayers; + let isTop = index > total - process.sliceTopLayers - 1; + let isDense = range.sliceFillSparse > 0.98; + let isSolid = (isBottom || ((isTop || isDense) && !vaseMode)) && !isSynth; + let solidWidth = isSolid ? range.sliceFillWidth || 1 : 0; + let count = isSynth ? 1 : sliceShells; + let offset = shellOffset * spaceMult; + let fillOff = fillOffset * spaceMult; + + let nutops = []; + for (let top of tops) { + nutops.push(FDM.share.doTopShells(z, top, count, offset/2, offset, fillOff, { + vase: vaseMode, + thin: process.detectThinWalls && !isSynth, + danger: isDanger + })); + } + data.tops = nutops; + } : undefined; + /** * DRIVER SLICE CONTRACT * @@ -62,32 +106,31 @@ FDM.slice = function(settings, widget, onupdate, ondone) { FDM.fixExtruders(settings); let render = settings.render !== false, - ctrl = settings.controller, - spro = settings.process, - sdev = settings.device, - isBelt = sdev.bedBelt, + { process, device, controller } = settings, + isBelt = device.bedBelt, isSynth = widget.track.synth, - minSolid = spro.sliceSolidMinArea, - solidLayers = spro.sliceSolidLayers, - vaseMode = spro.sliceFillType === 'vase' && !isSynth, + isDanger = controller.danger, + minSolid = process.sliceSolidMinArea, + solidLayers = process.sliceSolidLayers, + vaseMode = process.sliceFillType === 'vase' && !isSynth, doSolidLayers = solidLayers && !vaseMode && !isSynth, metadata = settings.widget[widget.id] || {}, extruder = metadata.extruder || 0, - sliceHeight = spro.sliceHeight, - firstSliceHeight = isBelt ? sliceHeight : spro.firstSliceHeight, - nozzleSize = sdev.extruders[extruder].extNozzle, - lineWidth = spro.sliceLineWidth || nozzleSize, - fillOffsetMult = 1.0 - bound(spro.sliceFillOverlap, 0, 0.8), + sliceHeight = process.sliceHeight, + firstSliceHeight = isBelt ? sliceHeight : process.firstSliceHeight, + nozzleSize = device.extruders[extruder].extNozzle, + lineWidth = process.sliceLineWidth || nozzleSize, + fillOffsetMult = 1.0 - bound(process.sliceFillOverlap, 0, 0.8), shellOffset = lineWidth, fillSpacing = lineWidth, fillOffset = lineWidth * fillOffsetMult, - sliceFillAngle = spro.sliceFillAngle, - supportDensity = spro.sliceSupportDensity, + sliceFillAngle = process.sliceFillAngle, + supportDensity = process.sliceSupportDensity, beltfact = Math.cos(Math.PI/4), - doConcurrent = ctrl.threaded && KIRI.minions.concurrent; + doConcurrent = controller.threaded && KIRI.minions.concurrent; - isFlat = ctrl.lineType === "flat"; - isThin = !isFlat && ctrl.lineType === "line"; + isFlat = controller.lineType === "flat"; + isThin = !isFlat && controller.lineType === "line"; offset = lineWidth / 2; if (isFlat) { @@ -117,8 +160,8 @@ firstSliceHeight = sliceHeight; } - const sliceMinHeight = spro.sliceAdaptive && spro.sliceMinHeight > 0 ? - Math.min(spro.sliceMinHeight, sliceHeight) : 0; + const sliceMinHeight = process.sliceAdaptive && process.sliceMinHeight > 0 ? + Math.min(process.sliceMinHeight, sliceHeight) : 0; if (firstSliceHeight < sliceHeight) { DBUG.log("invalid first layer height < slice height"); @@ -127,15 +170,25 @@ } SLICER.sliceWidget(widget, { + mode: 'FDM', height: sliceHeight, minHeight: sliceMinHeight, firstHeight: firstSliceHeight, - union: ctrl.healMesh, + union: controller.healMesh, + indices: process.indices, + concurrent: doConcurrent, + post: slicePost ? { + shellOffset, + fillOffset, + lineWidth, + vaseMode, + isSynth, + process, + isDanger + } : undefined // debug: true, - // xray: 3, // view: view, - indices: spro.indices, - concurrent: doConcurrent + // xray: 3, }, onSliceDone, onSliceUpdate); function onSliceUpdate(update) { @@ -169,8 +222,8 @@ shadow = doConcurrent ? await KIRI.minions.union(alltops, 0.1) : POLY.union(alltops, 0.1, true); - if (spro.sliceSupportExtra) { - shadow = POLY.offset(shadow, spro.sliceSupportExtra); + if (process.sliceSupportExtra) { + shadow = POLY.offset(shadow, process.sliceSupportExtra); } widget.shadow = POLY.setZ(shadow, 0); // slices[0].output() @@ -198,7 +251,7 @@ } // create shadow for non-belt supports - if (!isBelt && (isSynth || (!isSynth && supportDensity && spro.sliceSupportEnable))) { + if (!isBelt && (isSynth || (!isSynth && supportDensity && process.sliceSupportEnable))) { await doShadow(slices); } @@ -214,7 +267,7 @@ slice.addTop(u); } } - let gap = sliceHeight * (isBelt ? 0 : spro.sliceSupportGap); + let gap = sliceHeight * (isBelt ? 0 : process.sliceSupportGap); // clip tops to other widgets in group tops = slice.topPolys(); for (let peer of widget.group) { @@ -226,9 +279,9 @@ if (Math.abs(Math.abs(pslice.z - slice.z) - gap) > 0.1) { continue; } - // offset pslice tops by spro.sliceSupportOffset + // offset pslice tops by process.sliceSupportOffset if (!pslice.synth_off) { - pslice.synth_off = POLY.offset(pslice.topPolys(), spro.sliceSupportOffset); + pslice.synth_off = POLY.offset(pslice.topPolys(), process.sliceSupportOffset); } let ptops = pslice.synth_off; let ntops = []; @@ -276,7 +329,7 @@ } // do not hint polygin fill longer than a max span length - CONF.hint_len_max = UTIL.sqr(spro.sliceBridgeMax); + CONF.hint_len_max = UTIL.sqr(process.sliceBridgeMax); // reset for solids, support projections // and other annotations @@ -286,46 +339,48 @@ slice.solids = []; }); - let promises = doConcurrent ? [] : undefined; // create shells and diff inner fillable areas - forSlices(0.0, promises ? 0.05 : 0.15, slice => { - let params = slice.params = getRangeParameters(settings, slice.index); - let shellFrac = (params.sliceShells - (params.sliceShells | 0)); - let sliceShells = params.sliceShells | 0; - if (shellFrac) { - let v1 = shellFrac > 0.5 ? 1 - shellFrac : shellFrac; - let v2 = 1 - v1; - let parts = Math.round(v2/v1) + 1; - let rem = slice.index % parts; - let trg = shellFrac > 0.5 ? 1 : parts - 1; - sliceShells += rem >= trg ? 1 : 0; + if (!slicePost) { + let promises = doConcurrent ? [] : undefined; + forSlices(0.0, promises ? 0.05 : 0.15, slice => { + let params = slice.params = getRangeParameters(process, slice.index); + let shellFrac = (params.sliceShells - (params.sliceShells | 0)); + let sliceShells = params.sliceShells | 0; + if (shellFrac) { + let v1 = shellFrac > 0.5 ? 1 - shellFrac : shellFrac; + let v2 = 1 - v1; + let parts = Math.round(v2/v1) + 1; + let rem = slice.index % parts; + let trg = shellFrac > 0.5 ? 1 : parts - 1; + sliceShells += rem >= trg ? 1 : 0; + } + let first = slice.index === 0; + let spaceMult = first ? process.firstLayerLineMult || 1 : 1; + let isBottom = slice.index < process.sliceBottomLayers; + let isTop = slice.index > slices.length - process.sliceTopLayers-1; + let isDense = params.sliceFillSparse > 0.98; + let solidWidth = params.sliceFillWidth || 1; + if ((isBottom || ((isTop || isDense) && !vaseMode)) && !isSynth) { + slice.solid = { + solidWidth, + spaceMult + }; + } + let offset = shellOffset * spaceMult; + let fillOff = fillOffset * spaceMult; + let count = isSynth ? 1 : sliceShells; + doShells(slice, count, offset, fillOff, { + vase: vaseMode, + thin: process.detectThinWalls && !isSynth, + danger: controller.danger + }, promises); + }, "shells"); + if (promises) { + // await Promise.all(promises); + await tracker(promises, (i, t) => { + trackupdate(i / t, 0.05, 0.15); + }); } - let first = slice.index === 0; - let spaceMult = first ? spro.firstLayerLineMult || 1 : 1; - let isBottom = slice.index < spro.sliceBottomLayers; - let isTop = slice.index > slices.length - spro.sliceTopLayers-1; - let isDense = params.sliceFillSparse > 0.98; - let solidWidth = params.sliceFillWidth || 1; - if ((isBottom || ((isTop || isDense) && !vaseMode)) && !isSynth) { - slice.solid = { - solidWidth, - spaceMult - }; - } - let offset = shellOffset * spaceMult; - let fillOff = fillOffset * spaceMult; - let count = isSynth ? 1 : sliceShells; - doShells(slice, count, offset, fillOff, { - vase: vaseMode, - thin: spro.detectThinWalls && !isSynth, - danger: ctrl.danger - }, promises); - }, "shells"); - if (promises) { - // await Promise.all(promises); - await tracker(promises, (i, t) => { - trackupdate(i / t, 0.05, 0.15); - }); } // just the top/bottom special solid layers @@ -335,7 +390,7 @@ doSolidLayerFill(slice, fillSpace, sliceFillAngle); } sliceFillAngle += 90.0; - }, "shells"); + }, "solid layers"); // add lead in when specified in belt mode if (!isSynth && isBelt) { @@ -374,13 +429,13 @@ if (!start) start = slice; } } - let offset = spro.firstLayerBeltLead * beltfact; + let offset = process.firstLayerBeltLead * beltfact; // ensure we start against a layer with shells while (start.up && start.topShells().length === 0) { start = start.up; } // if a brim applies, add that width to anchor - let brim = getRangeParameters(settings, 0).firstLayerBrim || 0; + let brim = getRangeParameters(process, 0).firstLayerBrim || 0; if (brim) { minx -= brim; maxx += brim; @@ -414,7 +469,7 @@ offset -= sliceHeight; } // add anchor bump - let bump = spro.firstLayerBeltBump; + let bump = process.firstLayerBeltBump; if (bump) { adds = adds.reverse().slice(1, adds.length - 1); let count = 1; @@ -456,7 +511,7 @@ // console.log('start'); await BASE.util.ptimer(1000); let promises = doConcurrent ? [] : undefined; forSlices(0.35, promises ? 0.4 : 0.5, slice => { - let params = slice.params || spro; + let params = slice.params || process; let first = slice.index === 0; let solidWidth = params.sliceFillWidth || 1; let spaceMult = first ? params.firstLayerLineMult || 1 : 1; @@ -477,16 +532,16 @@ let lastType; let promises = doConcurrent ? [] : undefined; forSlices(0.5, promises ? 0.55 : 0.7, slice => { - let params = slice.params || spro; + let params = slice.params || process; if (vaseMode || !params.sliceFillSparse) { return; } let newType = params.sliceFillType; doSparseLayerFill(slice, { - settings: settings, - process: spro, - device: sdev, - lineWidth: lineWidth, + settings, + process, + device, + lineWidth, spacing: fillOffset, density: params.sliceFillSparse, bounds: widget.getBoundingBox(), @@ -505,7 +560,7 @@ } else if (isSynth) { let promises = doConcurrent ? [] : undefined; forSlices(0.5, promises ? 0.6 : 0.7, slice => { - let params = slice.params || spro; + let params = slice.params || process; let density = params.sliceSupportDensity; if (density) for (let top of slice.tops) { @@ -523,16 +578,16 @@ } // auto support generation - if (!isBelt && !isSynth && supportDensity && spro.sliceSupportEnable) { + if (!isBelt && !isSynth && supportDensity && process.sliceSupportEnable) { doShadow(slices); // console.log('start'); await BASE.util.ptimer(1000); forSlices(0.7, 0.8, slice => { - doSupport(slice, spro, widget.shadow, ctrl.danger); + doSupport(slice, process, widget.shadow, controller.danger); }, "support"); // console.log('stop'); await BASE.util.ptimer(1000); let promises = doConcurrent ? [] : undefined; forSlices(0.8, promises ? 0.88 : 0.9, slice => { - doSupportFill(promises, slice, lineWidth, supportDensity, spro.sliceSupportArea); + doSupportFill(promises, slice, lineWidth, supportDensity, process.sliceSupportArea); }, "support"); if (promises) { await tracker(promises, (i, t) => { @@ -544,8 +599,8 @@ // render if not explicitly disabled if (render) { forSlices(0.9, 1.0, slice => { - let params = slice.params || spro; - doRender(slice, isSynth, params, ctrl.devel); + let params = slice.params || process; + doRender(slice, isSynth, params, controller.devel); }, "render"); } @@ -631,7 +686,7 @@ // console.log(slice.index, slice.render.stats); } - // shared with SLA driver + // shared with SLA driver and minions FDM.share = { doShells, doTopShells, @@ -682,6 +737,11 @@ } function doTopShells(z, top, count, offset1, offsetN, fillOffset, opt = {}) { + // pretend we're a top object in minions + if (!top.poly) { + top = { poly: top }; + } + let top_poly = [ top.poly ]; if (opt.vase) { @@ -801,6 +861,8 @@ // for diffing top.last = last; + + return top; } /**