{ "version": 3, "sources": ["../../../../../src/lib/shapes/shared/freehand/getStrokeOutlinePoints.ts"], "sourcesContent": ["import { Vec2d } from '@tldraw/editor'\nimport type { StrokeOptions, StrokePoint } from './types'\n\nconst { PI } = Math\n\n// Browser strokes seem to be off if PI is regular, a tiny offset seems to fix it\nconst FIXED_PI = PI + 0.0001\n\n/**\n * ## getStrokeOutlinePoints\n *\n * Get an array of points (as `[x, y]`) representing the outline of a stroke.\n *\n * @param points - An array of StrokePoints as returned from `getStrokePoints`.\n * @param options - An object with options.\n * @public\n */\nexport function getStrokeOutlinePoints(\n\tstrokePoints: StrokePoint[],\n\toptions: StrokeOptions = {}\n): Vec2d[] {\n\tconst { size = 16, smoothing = 0.5, start = {}, end = {}, last: isComplete = false } = options\n\n\tconst { cap: capStart = true } = start\n\tconst { cap: capEnd = true } = end\n\n\t// We can't do anything with an empty array or a stroke with negative size.\n\tif (strokePoints.length === 0 || size <= 0) {\n\t\treturn []\n\t}\n\n\tconst firstStrokePoint = strokePoints[0]\n\tconst lastStrokePoint = strokePoints[strokePoints.length - 1]\n\n\t// The total length of the line\n\tconst totalLength = lastStrokePoint.runningLength\n\n\tconst taperStart =\n\t\tstart.taper === false\n\t\t\t? 0\n\t\t\t: start.taper === true\n\t\t\t? Math.max(size, totalLength)\n\t\t\t: (start.taper as number)\n\n\tconst taperEnd =\n\t\tend.taper === false\n\t\t\t? 0\n\t\t\t: end.taper === true\n\t\t\t? Math.max(size, totalLength)\n\t\t\t: (end.taper as number)\n\n\t// The minimum allowed distance between points (squared)\n\tconst minDistance = Math.pow(size * smoothing, 2)\n\n\t// Our collected left and right points\n\tconst leftPts: Vec2d[] = []\n\tconst rightPts: Vec2d[] = []\n\n\t// Previous vector\n\tlet prevVector = strokePoints[0].vector\n\n\t// Previous left and right points\n\tlet pl = strokePoints[0].point\n\tlet pr = pl\n\n\t// Temporary left and right points\n\tlet tl = pl\n\tlet tr = pr\n\n\t// Keep track of whether the previous point is a sharp corner\n\t// ... so that we don't detect the same corner twice\n\tlet isPrevPointSharpCorner = false\n\n\t/*\n Find the outline's left and right points\n\n Iterating through the points and populate the rightPts and leftPts arrays,\n skipping the first and last pointsm, which will get caps later on.\n */\n\n\tlet strokePoint: StrokePoint\n\n\tfor (let i = 0; i < strokePoints.length; i++) {\n\t\tstrokePoint = strokePoints[i]\n\t\tconst { point, vector } = strokePoints[i]\n\n\t\t/*\n Handle sharp corners\n\n Find the difference (dot product) between the current and next vector.\n If the next vector is at more than a right angle to the current vector,\n draw a cap at the current point.\n */\n\n\t\tconst prevDpr = strokePoint.vector.dpr(prevVector)\n\t\tconst nextVector = (i < strokePoints.length - 1 ? strokePoints[i + 1] : strokePoints[i]).vector\n\t\tconst nextDpr = i < strokePoints.length - 1 ? nextVector.dpr(strokePoint.vector) : 1\n\n\t\tconst isPointSharpCorner = prevDpr < 0 && !isPrevPointSharpCorner\n\t\tconst isNextPointSharpCorner = nextDpr !== null && nextDpr < 0.2\n\n\t\tif (isPointSharpCorner || isNextPointSharpCorner) {\n\t\t\t// It's a sharp corner. Draw a rounded cap and move on to the next point\n\t\t\t// Considering saving these and drawing them later? So that we can avoid\n\t\t\t// crossing future points.\n\n\t\t\tif (nextDpr > -0.62 && totalLength - strokePoint.runningLength > strokePoint.radius) {\n\t\t\t\t// Draw a \"soft\" corner\n\t\t\t\tconst offset = prevVector.clone().mul(strokePoint.radius)\n\t\t\t\tconst cpr = prevVector.clone().cpr(nextVector)\n\n\t\t\t\tif (cpr < 0) {\n\t\t\t\t\ttl = Vec2d.Add(point, offset)\n\t\t\t\t\ttr = Vec2d.Sub(point, offset)\n\t\t\t\t} else {\n\t\t\t\t\ttl = Vec2d.Sub(point, offset)\n\t\t\t\t\ttr = Vec2d.Add(point, offset)\n\t\t\t\t}\n\n\t\t\t\tleftPts.push(tl)\n\t\t\t\trightPts.push(tr)\n\t\t\t} else {\n\t\t\t\t// Draw a \"sharp\" corner\n\t\t\t\tconst offset = prevVector.clone().mul(strokePoint.radius).per()\n\t\t\t\tconst start = Vec2d.Sub(strokePoint.input, offset)\n\n\t\t\t\tfor (let step = 1 / 13, t = 0; t < 1; t += step) {\n\t\t\t\t\ttl = Vec2d.RotWith(start, strokePoint.input, FIXED_PI * t)\n\t\t\t\t\tleftPts.push(tl)\n\n\t\t\t\t\ttr = Vec2d.RotWith(start, strokePoint.input, FIXED_PI + FIXED_PI * -t)\n\t\t\t\t\trightPts.push(tr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpl = tl\n\t\t\tpr = tr\n\n\t\t\tif (isNextPointSharpCorner) {\n\t\t\t\tisPrevPointSharpCorner = true\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\tisPrevPointSharpCorner = false\n\n\t\tif (strokePoint === firstStrokePoint || strokePoint === lastStrokePoint) {\n\t\t\tconst offset = Vec2d.Per(vector).mul(strokePoint.radius)\n\t\t\tleftPts.push(Vec2d.Sub(point, offset))\n\t\t\trightPts.push(Vec2d.Add(point, offset))\n\n\t\t\tcontinue\n\t\t}\n\n\t\t/* \n Add regular points\n\n Project points to either side of the current point, using the\n calculated size as a distance. If a point's distance to the \n previous point on that side greater than the minimum distance\n (or if the corner is kinda sharp), add the points to the side's\n points array.\n */\n\n\t\tconst offset = Vec2d.Lrp(nextVector, vector, nextDpr).per().mul(strokePoint.radius)\n\n\t\ttl = Vec2d.Sub(point, offset)\n\n\t\tif (i <= 1 || Vec2d.Dist2(pl, tl) > minDistance) {\n\t\t\tleftPts.push(tl)\n\t\t\tpl = tl\n\t\t}\n\n\t\ttr = Vec2d.Add(point, offset)\n\n\t\tif (i <= 1 || Vec2d.Dist2(pr, tr) > minDistance) {\n\t\t\trightPts.push(tr)\n\t\t\tpr = tr\n\t\t}\n\n\t\t// Set variables for next iteration\n\t\tprevVector = vector\n\n\t\tcontinue\n\t}\n\n\t/*\n Drawing caps\n \n Now that we have our points on either side of the line, we need to\n draw caps at the start and end. Tapered lines don't have caps, but\n may have dots for very short lines.\n */\n\n\tconst firstPoint = firstStrokePoint.point\n\n\tconst lastPoint =\n\t\tstrokePoints.length > 1\n\t\t\t? strokePoints[strokePoints.length - 1].point\n\t\t\t: Vec2d.AddXY(firstStrokePoint.point, 1, 1)\n\n\t/* \n Draw a dot for very short or completed strokes\n \n If the line is too short to gather left or right points and if the line is\n not tapered on either side, draw a dot. If the line is tapered, then only\n draw a dot if the line is both very short and complete. If we draw a dot,\n we can just return those points.\n */\n\n\tif (strokePoints.length === 1) {\n\t\tif (!(taperStart || taperEnd) || isComplete) {\n\t\t\tconst start = Vec2d.Add(\n\t\t\t\tfirstPoint,\n\t\t\t\tVec2d.Sub(firstPoint, lastPoint).uni().per().mul(-firstStrokePoint.radius)\n\t\t\t)\n\t\t\tconst dotPts: Vec2d[] = []\n\t\t\tfor (let step = 1 / 13, t = step; t <= 1; t += step) {\n\t\t\t\tdotPts.push(Vec2d.RotWith(start, firstPoint, FIXED_PI * 2 * t))\n\t\t\t}\n\t\t\treturn dotPts\n\t\t}\n\t}\n\n\t/*\n Draw a start cap\n\n Unless the line has a tapered start, or unless the line has a tapered end\n and the line is very short, draw a start cap around the first point. Use\n the distance between the second left and right point for the cap's radius.\n Finally remove the first left and right points. :psyduck:\n */\n\n\tconst startCap: Vec2d[] = []\n\tif (taperStart || (taperEnd && strokePoints.length === 1)) {\n\t\t// The start point is tapered, noop\n\t} else if (capStart) {\n\t\t// Draw the round cap - add thirteen points rotating the right point around the start point to the left point\n\t\tfor (let step = 1 / 8, t = step; t <= 1; t += step) {\n\t\t\tconst pt = Vec2d.RotWith(rightPts[0], firstPoint, FIXED_PI * t)\n\t\t\tstartCap.push(pt)\n\t\t}\n\t} else {\n\t\t// Draw the flat cap - add a point to the left and right of the start point\n\t\tconst cornersVector = Vec2d.Sub(leftPts[0], rightPts[0])\n\t\tconst offsetA = Vec2d.Mul(cornersVector, 0.5)\n\t\tconst offsetB = Vec2d.Mul(cornersVector, 0.51)\n\n\t\tstartCap.push(\n\t\t\tVec2d.Sub(firstPoint, offsetA),\n\t\t\tVec2d.Sub(firstPoint, offsetB),\n\t\t\tVec2d.Add(firstPoint, offsetB),\n\t\t\tVec2d.Add(firstPoint, offsetA)\n\t\t)\n\t}\n\n\t/*\n Draw an end cap\n\n If the line does not have a tapered end, and unless the line has a tapered\n start and the line is very short, draw a cap around the last point. Finally,\n remove the last left and right points. Otherwise, add the last point. Note\n that This cap is a full-turn-and-a-half: this prevents incorrect caps on\n sharp end turns.\n */\n\n\tconst endCap: Vec2d[] = []\n\tconst direction = lastStrokePoint.vector.clone().per().neg()\n\n\tif (taperEnd || (taperStart && strokePoints.length === 1)) {\n\t\t// Tapered end - push the last point to the line\n\t\tendCap.push(lastPoint)\n\t} else if (capEnd) {\n\t\t// Draw the round end cap\n\t\tconst start = Vec2d.Add(lastPoint, Vec2d.Mul(direction, lastStrokePoint.radius))\n\t\tfor (let step = 1 / 29, t = step; t < 1; t += step) {\n\t\t\tendCap.push(Vec2d.RotWith(start, lastPoint, FIXED_PI * 3 * t))\n\t\t}\n\t} else {\n\t\t// Draw the flat end cap\n\t\tendCap.push(\n\t\t\tVec2d.Add(lastPoint, Vec2d.Mul(direction, lastStrokePoint.radius)),\n\t\t\tVec2d.Add(lastPoint, Vec2d.Mul(direction, lastStrokePoint.radius * 0.99)),\n\t\t\tVec2d.Sub(lastPoint, Vec2d.Mul(direction, lastStrokePoint.radius * 0.99)),\n\t\t\tVec2d.Sub(lastPoint, Vec2d.Mul(direction, lastStrokePoint.radius))\n\t\t)\n\t}\n\n\t/*\n Return the points in the correct winding order: begin on the left side, then \n continue around the end cap, then come back along the right side, and finally \n complete the start cap.\n */\n\n\treturn leftPts.concat(endCap, rightPts.reverse(), startCap)\n}\n"], "mappings": "AAAA,SAAS,aAAa;AAGtB,MAAM,EAAE,GAAG,IAAI;AAGf,MAAM,WAAW,KAAK;AAWf,SAAS,uBACf,cACA,UAAyB,CAAC,GAChB;AACV,QAAM,EAAE,OAAO,IAAI,YAAY,KAAK,QAAQ,CAAC,GAAG,MAAM,CAAC,GAAG,MAAM,aAAa,MAAM,IAAI;AAEvF,QAAM,EAAE,KAAK,WAAW,KAAK,IAAI;AACjC,QAAM,EAAE,KAAK,SAAS,KAAK,IAAI;AAG/B,MAAI,aAAa,WAAW,KAAK,QAAQ,GAAG;AAC3C,WAAO,CAAC;AAAA,EACT;AAEA,QAAM,mBAAmB,aAAa,CAAC;AACvC,QAAM,kBAAkB,aAAa,aAAa,SAAS,CAAC;AAG5D,QAAM,cAAc,gBAAgB;AAEpC,QAAM,aACL,MAAM,UAAU,QACb,IACA,MAAM,UAAU,OAChB,KAAK,IAAI,MAAM,WAAW,IACzB,MAAM;AAEX,QAAM,WACL,IAAI,UAAU,QACX,IACA,IAAI,UAAU,OACd,KAAK,IAAI,MAAM,WAAW,IACzB,IAAI;AAGT,QAAM,cAAc,KAAK,IAAI,OAAO,WAAW,CAAC;AAGhD,QAAM,UAAmB,CAAC;AAC1B,QAAM,WAAoB,CAAC;AAG3B,MAAI,aAAa,aAAa,CAAC,EAAE;AAGjC,MAAI,KAAK,aAAa,CAAC,EAAE;AACzB,MAAI,KAAK;AAGT,MAAI,KAAK;AACT,MAAI,KAAK;AAIT,MAAI,yBAAyB;AAS7B,MAAI;AAEJ,WAAS,IAAI,GAAG,IAAI,aAAa,QAAQ,KAAK;AAC7C,kBAAc,aAAa,CAAC;AAC5B,UAAM,EAAE,OAAO,OAAO,IAAI,aAAa,CAAC;AAUxC,UAAM,UAAU,YAAY,OAAO,IAAI,UAAU;AACjD,UAAM,cAAc,IAAI,aAAa,SAAS,IAAI,aAAa,IAAI,CAAC,IAAI,aAAa,CAAC,GAAG;AACzF,UAAM,UAAU,IAAI,aAAa,SAAS,IAAI,WAAW,IAAI,YAAY,MAAM,IAAI;AAEnF,UAAM,qBAAqB,UAAU,KAAK,CAAC;AAC3C,UAAM,yBAAyB,YAAY,QAAQ,UAAU;AAE7D,QAAI,sBAAsB,wBAAwB;AAKjD,UAAI,UAAU,SAAS,cAAc,YAAY,gBAAgB,YAAY,QAAQ;AAEpF,cAAMA,UAAS,WAAW,MAAM,EAAE,IAAI,YAAY,MAAM;AACxD,cAAM,MAAM,WAAW,MAAM,EAAE,IAAI,UAAU;AAE7C,YAAI,MAAM,GAAG;AACZ,eAAK,MAAM,IAAI,OAAOA,OAAM;AAC5B,eAAK,MAAM,IAAI,OAAOA,OAAM;AAAA,QAC7B,OAAO;AACN,eAAK,MAAM,IAAI,OAAOA,OAAM;AAC5B,eAAK,MAAM,IAAI,OAAOA,OAAM;AAAA,QAC7B;AAEA,gBAAQ,KAAK,EAAE;AACf,iBAAS,KAAK,EAAE;AAAA,MACjB,OAAO;AAEN,cAAMA,UAAS,WAAW,MAAM,EAAE,IAAI,YAAY,MAAM,EAAE,IAAI;AAC9D,cAAMC,SAAQ,MAAM,IAAI,YAAY,OAAOD,OAAM;AAEjD,iBAAS,OAAO,IAAI,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK,MAAM;AAChD,eAAK,MAAM,QAAQC,QAAO,YAAY,OAAO,WAAW,CAAC;AACzD,kBAAQ,KAAK,EAAE;AAEf,eAAK,MAAM,QAAQA,QAAO,YAAY,OAAO,WAAW,WAAW,CAAC,CAAC;AACrE,mBAAS,KAAK,EAAE;AAAA,QACjB;AAAA,MACD;AAEA,WAAK;AACL,WAAK;AAEL,UAAI,wBAAwB;AAC3B,iCAAyB;AAAA,MAC1B;AAEA;AAAA,IACD;AAEA,6BAAyB;AAEzB,QAAI,gBAAgB,oBAAoB,gBAAgB,iBAAiB;AACxE,YAAMD,UAAS,MAAM,IAAI,MAAM,EAAE,IAAI,YAAY,MAAM;AACvD,cAAQ,KAAK,MAAM,IAAI,OAAOA,OAAM,CAAC;AACrC,eAAS,KAAK,MAAM,IAAI,OAAOA,OAAM,CAAC;AAEtC;AAAA,IACD;AAYA,UAAM,SAAS,MAAM,IAAI,YAAY,QAAQ,OAAO,EAAE,IAAI,EAAE,IAAI,YAAY,MAAM;AAElF,SAAK,MAAM,IAAI,OAAO,MAAM;AAE5B,QAAI,KAAK,KAAK,MAAM,MAAM,IAAI,EAAE,IAAI,aAAa;AAChD,cAAQ,KAAK,EAAE;AACf,WAAK;AAAA,IACN;AAEA,SAAK,MAAM,IAAI,OAAO,MAAM;AAE5B,QAAI,KAAK,KAAK,MAAM,MAAM,IAAI,EAAE,IAAI,aAAa;AAChD,eAAS,KAAK,EAAE;AAChB,WAAK;AAAA,IACN;AAGA,iBAAa;AAEb;AAAA,EACD;AAUA,QAAM,aAAa,iBAAiB;AAEpC,QAAM,YACL,aAAa,SAAS,IACnB,aAAa,aAAa,SAAS,CAAC,EAAE,QACtC,MAAM,MAAM,iBAAiB,OAAO,GAAG,CAAC;AAW5C,MAAI,aAAa,WAAW,GAAG;AAC9B,QAAI,EAAE,cAAc,aAAa,YAAY;AAC5C,YAAMC,SAAQ,MAAM;AAAA,QACnB;AAAA,QACA,MAAM,IAAI,YAAY,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,iBAAiB,MAAM;AAAA,MAC1E;AACA,YAAM,SAAkB,CAAC;AACzB,eAAS,OAAO,IAAI,IAAI,IAAI,MAAM,KAAK,GAAG,KAAK,MAAM;AACpD,eAAO,KAAK,MAAM,QAAQA,QAAO,YAAY,WAAW,IAAI,CAAC,CAAC;AAAA,MAC/D;AACA,aAAO;AAAA,IACR;AAAA,EACD;AAWA,QAAM,WAAoB,CAAC;AAC3B,MAAI,cAAe,YAAY,aAAa,WAAW,GAAI;AAAA,EAE3D,WAAW,UAAU;AAEpB,aAAS,OAAO,IAAI,GAAG,IAAI,MAAM,KAAK,GAAG,KAAK,MAAM;AACnD,YAAM,KAAK,MAAM,QAAQ,SAAS,CAAC,GAAG,YAAY,WAAW,CAAC;AAC9D,eAAS,KAAK,EAAE;AAAA,IACjB;AAAA,EACD,OAAO;AAEN,UAAM,gBAAgB,MAAM,IAAI,QAAQ,CAAC,GAAG,SAAS,CAAC,CAAC;AACvD,UAAM,UAAU,MAAM,IAAI,eAAe,GAAG;AAC5C,UAAM,UAAU,MAAM,IAAI,eAAe,IAAI;AAE7C,aAAS;AAAA,MACR,MAAM,IAAI,YAAY,OAAO;AAAA,MAC7B,MAAM,IAAI,YAAY,OAAO;AAAA,MAC7B,MAAM,IAAI,YAAY,OAAO;AAAA,MAC7B,MAAM,IAAI,YAAY,OAAO;AAAA,IAC9B;AAAA,EACD;AAYA,QAAM,SAAkB,CAAC;AACzB,QAAM,YAAY,gBAAgB,OAAO,MAAM,EAAE,IAAI,EAAE,IAAI;AAE3D,MAAI,YAAa,cAAc,aAAa,WAAW,GAAI;AAE1D,WAAO,KAAK,SAAS;AAAA,EACtB,WAAW,QAAQ;AAElB,UAAMA,SAAQ,MAAM,IAAI,WAAW,MAAM,IAAI,WAAW,gBAAgB,MAAM,CAAC;AAC/E,aAAS,OAAO,IAAI,IAAI,IAAI,MAAM,IAAI,GAAG,KAAK,MAAM;AACnD,aAAO,KAAK,MAAM,QAAQA,QAAO,WAAW,WAAW,IAAI,CAAC,CAAC;AAAA,IAC9D;AAAA,EACD,OAAO;AAEN,WAAO;AAAA,MACN,MAAM,IAAI,WAAW,MAAM,IAAI,WAAW,gBAAgB,MAAM,CAAC;AAAA,MACjE,MAAM,IAAI,WAAW,MAAM,IAAI,WAAW,gBAAgB,SAAS,IAAI,CAAC;AAAA,MACxE,MAAM,IAAI,WAAW,MAAM,IAAI,WAAW,gBAAgB,SAAS,IAAI,CAAC;AAAA,MACxE,MAAM,IAAI,WAAW,MAAM,IAAI,WAAW,gBAAgB,MAAM,CAAC;AAAA,IAClE;AAAA,EACD;AAQA,SAAO,QAAQ,OAAO,QAAQ,SAAS,QAAQ,GAAG,QAAQ;AAC3D;", "names": ["offset", "start"] }