@weasel-js/core 1.8.1 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/README.md +9 -5
- package/dist/{builtins-CMEh6-Oc.d.ts → builtins-CQcvcLXh.d.ts} +98 -31
- package/dist/canvas/DrawCanvas.js +33 -33
- package/dist/canvas/DrawCanvas.js.map +1 -1
- package/dist/canvas/MinimapCanvas.js +29 -29
- package/dist/canvas/MinimapCanvas.js.map +1 -1
- package/dist/canvas/NodeShape.js +63 -54
- package/dist/canvas/NodeShape.js.map +1 -1
- package/dist/canvas/SceneCanvas/features.js +26 -5
- package/dist/canvas/SceneCanvas/features.js.map +1 -1
- package/dist/canvas/SceneCanvas/useSceneSelectTool.js +3 -2
- package/dist/canvas/SceneCanvas/useSceneSelectTool.js.map +1 -1
- package/dist/canvas/SceneCanvas.js +526 -529
- package/dist/canvas/SceneCanvas.js.map +1 -1
- package/dist/canvas/SceneViewCanvas.js +35 -35
- package/dist/canvas/SceneViewCanvas.js.map +1 -1
- package/dist/canvas/defaultDrawOne.js +6 -1
- package/dist/canvas/defaultDrawOne.js.map +1 -1
- package/dist/canvas/pickWalk.js.map +1 -1
- package/dist/canvas/sceneAdapter.js.map +1 -1
- package/dist/canvas/useHitObserver.js +23 -0
- package/dist/canvas/useHitObserver.js.map +1 -0
- package/dist/canvas/useHostAnchor.js.map +1 -1
- package/dist/core/device/useDeviceProfile.js.map +1 -1
- package/dist/core/scene/derivedPath.js.map +1 -1
- package/dist/core/scene/effectivePose.js.map +1 -1
- package/dist/core/scene/kitRegistry.js.map +1 -1
- package/dist/core/scene/scene.js.map +1 -1
- package/dist/core/scene/types.js.map +1 -1
- package/dist/core/scene/useScene.js.map +1 -1
- package/dist/core/viewport/fitViewToBounds.js +3 -3
- package/dist/core/viewport/fitViewToBounds.js.map +1 -1
- package/dist/core/viewport/screenSlop.js +8 -2
- package/dist/core/viewport/screenSlop.js.map +1 -1
- package/dist/features/minimap/actions.js.map +1 -1
- package/dist/features/paths/markerCommands.js +95 -63
- package/dist/features/paths/markerCommands.js.map +1 -1
- package/dist/features/paths/tessellate/dash.js +52 -0
- package/dist/features/paths/tessellate/dash.js.map +1 -0
- package/dist/features/paths/tessellate/insets.js +32 -0
- package/dist/features/paths/tessellate/insets.js.map +1 -0
- package/dist/features/paths/tessellate/metric.js +72 -0
- package/dist/features/paths/tessellate/metric.js.map +1 -0
- package/dist/features/paths/tessellate/stroke.js +74 -134
- package/dist/features/paths/tessellate/stroke.js.map +1 -1
- package/dist/features/paths/tessellate/vertexWidths.js +32 -0
- package/dist/features/paths/tessellate/vertexWidths.js.map +1 -0
- package/dist/features/pointer/PointerContext.js +2 -2
- package/dist/features/pointer/PointerContext.js.map +1 -1
- package/dist/features/poseRun/usePoseRun.js.map +1 -1
- package/dist/features/text/useTextEdit.js +86 -84
- package/dist/features/text/useTextEdit.js.map +1 -1
- package/dist/index.d.ts +78 -39
- package/dist/index.js +148 -148
- package/dist/interactions/actions/defaults/align.js.map +1 -1
- package/dist/interactions/actions/defaults/anchorEditing.js.map +1 -1
- package/dist/interactions/actions/defaults/areaSelect.js.map +1 -1
- package/dist/interactions/actions/defaults/booleans.js.map +1 -1
- package/dist/interactions/actions/defaults/cancelGesture.js.map +1 -1
- package/dist/interactions/actions/defaults/clearSelection.js.map +1 -1
- package/dist/interactions/actions/defaults/clipboard.js.map +1 -1
- package/dist/interactions/actions/defaults/clone.js.map +1 -1
- package/dist/interactions/actions/defaults/createOutlines.js +1 -1
- package/dist/interactions/actions/defaults/createOutlines.js.map +1 -1
- package/dist/interactions/actions/defaults/createPaintAction.js.map +1 -1
- package/dist/interactions/actions/defaults/delete.js.map +1 -1
- package/dist/interactions/actions/defaults/distribute.js.map +1 -1
- package/dist/interactions/actions/defaults/duplicate.js.map +1 -1
- package/dist/interactions/actions/defaults/editAnchors.js.map +1 -1
- package/dist/interactions/actions/defaults/enterPathEdit.js.map +1 -1
- package/dist/interactions/actions/defaults/enterTextEdit.js.map +1 -1
- package/dist/interactions/actions/defaults/escape.js +1 -1
- package/dist/interactions/actions/defaults/escape.js.map +1 -1
- package/dist/interactions/actions/defaults/exitPathEdit.js.map +1 -1
- package/dist/interactions/actions/defaults/flip.js.map +1 -1
- package/dist/interactions/actions/defaults/group.js.map +1 -1
- package/dist/interactions/actions/defaults/ingest.js.map +1 -1
- package/dist/interactions/actions/defaults/insert.js.map +1 -1
- package/dist/interactions/actions/defaults/insertPathAnchor.js +1 -1
- package/dist/interactions/actions/defaults/insertPathAnchor.js.map +1 -1
- package/dist/interactions/actions/defaults/lassoSelect.js.map +1 -1
- package/dist/interactions/actions/defaults/move.js.map +1 -1
- package/dist/interactions/actions/defaults/nudge.js.map +1 -1
- package/dist/interactions/actions/defaults/pinchZoom.js.map +1 -1
- package/dist/interactions/actions/defaults/reorder.js.map +1 -1
- package/dist/interactions/actions/defaults/requiresSelection.js.map +1 -1
- package/dist/interactions/actions/defaults/resize.js.map +1 -1
- package/dist/interactions/actions/defaults/rotate.js.map +1 -1
- package/dist/interactions/actions/defaults/selectAll.js.map +1 -1
- package/dist/interactions/actions/defaults/slice.js.map +1 -1
- package/dist/interactions/actions/defaults/toolActivate.js.map +1 -1
- package/dist/interactions/actions/defaults/toolResetToDefault.js.map +1 -1
- package/dist/interactions/actions/defaults/undoRedo.js +1 -1
- package/dist/interactions/actions/defaults/undoRedo.js.map +1 -1
- package/dist/interactions/actions/defaults/viewportDragPan.js.map +1 -1
- package/dist/interactions/actions/defaults/viewportWheelPan.js.map +1 -1
- package/dist/interactions/actions/defaults/viewportZoom.js.map +1 -1
- package/dist/interactions/actions/planeInput.js.map +1 -1
- package/dist/interactions/gestures/shared/screenTolerance.js.map +1 -1
- package/dist/{registerProgram-ateHDGjw.d.ts → registerProgram-BMZRDgrh.d.ts} +4 -0
- package/dist/renderer/cache/markerCommandCache.js +46 -42
- package/dist/renderer/cache/markerCommandCache.js.map +1 -1
- package/dist/renderer/cache/outlineStrokeMeshCache.js +30 -25
- package/dist/renderer/cache/outlineStrokeMeshCache.js.map +1 -1
- package/dist/renderer/cache/strokeMeshCache.js +42 -31
- package/dist/renderer/cache/strokeMeshCache.js.map +1 -1
- package/dist/renderer/cullDrawCommands.js +32 -31
- package/dist/renderer/cullDrawCommands.js.map +1 -1
- package/dist/renderer/draw.js +315 -362
- package/dist/renderer/draw.js.map +1 -1
- package/dist/renderer/glyphOutlineMesh.js +67 -0
- package/dist/renderer/glyphOutlineMesh.js.map +1 -0
- package/dist/renderer/math/mat3.js +8 -3
- package/dist/renderer/math/mat3.js.map +1 -1
- package/dist/renderer/strokeLengths.js +50 -0
- package/dist/renderer/strokeLengths.js.map +1 -0
- package/dist/renderer.d.ts +2 -2
- package/dist/{test-seams-fYf6m5pu.d.ts → test-seams-CuIxe0xY.d.ts} +1 -1
- package/dist/test-seams.d.ts +2 -2
- package/dist/tools/builtin/eyedropper/useEyedropperTool.js.map +1 -1
- package/dist/tools/builtin/pen/usePenTool.js +1 -1
- package/dist/tools/builtin/pen/usePenTool.js.map +1 -1
- package/dist/tools/builtin/select/selectionContributions.js +43 -23
- package/dist/tools/builtin/select/selectionContributions.js.map +1 -1
- package/dist/tools/builtin/select/useSelectTool.js.map +1 -1
- package/dist/tools/builtin/slice/useSliceTool.js.map +1 -1
- package/dist/version.js +1 -1
- package/package.json +17 -17
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stroke.js","names":["EMPTY_MESH","resolveStrokeWidth","resolveScreenLength","width","scale","tessellateStroke","stroke","join","cap","align","miterLimit","DEFAULT_MITER_LIMIT","varyingThreshold","DEFAULT_VARYING_WIDTH_JOIN_THRESHOLD","workingPath","path","aligned","alignedStrokeRect","polylines","extractPolylines","opts","pl","dash","verts","idx","anchorA","anchorB","anchorT","startInset","endInset","trimmed","trimPolyline","subs","splitForDash","sub","expandPolyline","half","pts","ptCount","segCount","plA","plB","plT","plHalf","w","segs","segSrcIdx","s","ha","hb","seg","makeSeg","closerSrcIdx","last","first","haCloser","hbCloser","closer","segBaseIdx","startSrc","endSrc","base","nxA","nyA","nxB","nyB","joinCount","j","firstBase","lastBase","lastSrc","populatePolylineWidths","aA","aB","aT","widths","read","v","vertexWidths","i","fallbackWidth","a","b","t","wa","wb","ROUND_CAP_STEP_RAD","emitCap","sign","atEnd","dx","dy","cx","cy","nx","ny","ox","oy","lOut","endA","endB","endT","rOut","leftIdx","rightIdx","pivotIdx","startAngle","sweep","steps","stepAngle","prevIdx","ang","fx","fy","newIdx","out","dashIdx","dashRemaining","onPhase","curPts","curA","curB","curT","flushAndAdvance","prevX","prevY","nextIdx","startA","startB","startT","segFullDx","segFullDy","segFullLen","segDx","segDy","segLen","traveled","tConsume","ix","iy","frac","mA","mB","mT","tail","tx","ty","meets","x","y","joinAtSeam","head","bx","ax","by","ay","len","emitJoin","aBase","bBase","adx","ady","bdx","bdy","cross","onPositive","aOuterEnd","bOuterStart","cornerSrc","cornerA","cornerB","cornerT","halfAtCorner","taperA","taperB","aOX","aOY","bOX","bOY","apex","lineLineIntersect","apexIdx","jIdx","ROUND_STEP_RAD","emitRoundJoin","startX","startY","endX","endY","r","emitBevel","denom","bpx","apx","bpy","apy"],"sources":["../../../../src/features/paths/tessellate/stroke.ts"],"sourcesContent":["import type { Path } from '@weasel-js/core';\nimport type { ScreenLength, Stroke } from '@weasel-js/paint';\nimport { alignedStrokeRect, resolveScreenLength } from '@weasel-js/paint';\nimport { type Mesh, extractPolylines, type Polyline, trimPolyline } from '@weasel-js/geom/tessellate';\n\n/** Options for stroke tessellation. */\nexport interface StrokeOptions {\n flattenTolerance?: number;\n /**\n * Shorten each open subpath by this much at its start / end, in the same\n * world units as the path. Stroke markers use this so a filled head is not\n * speared by its own line; the caller resolves the distance, because this\n * layer knows nothing about the marker registry.\n */\n startInset?: number;\n endInset?: number;\n}\n\nconst EMPTY_MESH: Mesh = {\n vertices: new Float32Array(0),\n indices: new Uint32Array(0),\n anchorA: new Uint32Array(0),\n anchorB: new Uint32Array(0),\n anchorT: new Float32Array(0),\n};\n\ntype Cap = 'butt' | 'round' | 'square';\ntype Join = 'miter' | 'round' | 'bevel';\n\n/** Per-segment data the join code needs. */\ninterface Seg {\n ax: number; ay: number;\n bx: number; by: number;\n /** Unit perpendicular (rotated 90° CCW from direction). */\n nxUnit: number; nyUnit: number;\n /** Half-width at the start (a) and end (b) endpoints. Equal for uniform\n * strokes; differ when the polyline carries per-point widths. */\n halfA: number; halfB: number;\n len: number;\n}\n\n/** Resolve a stroke width to world units. A number is already world units;\n * `{ px }` is screen pixels divided by the accumulated scale, so it holds its\n * on-screen thickness as the view zooms. */\nexport function resolveStrokeWidth(width: ScreenLength, scale: number): number {\n return resolveScreenLength(width, scale);\n}\n\n/**\n * Build a triangle-mesh ribbon from a stroked Path.\n *\n * Supports:\n * - cap: 'butt' | 'round' | 'square'\n * - join: 'miter' | 'round' | 'bevel'\n * - center alignment directly; RectPath inner/outer alignment via a\n * pre-shifted rect; PolygonPath inner/outer alignment via stencil at the\n * renderer level\n * - dash splitting\n */\nexport function tessellateStroke(\n path: Path,\n stroke: Stroke,\n opts: StrokeOptions = {},\n): Mesh {\n // The renderer resolves `{ px }` against the accumulated transform before it\n // gets here; a stroke that reaches the tessellator unresolved is read at\n // scale 1, matching what the ribbon cache keys it under.\n const width = resolveStrokeWidth(stroke.width ?? 1, 1);\n if (width <= 0) return EMPTY_MESH;\n const join: Join = stroke.join ?? 'miter';\n const cap: Cap = stroke.cap ?? 'butt';\n const align = stroke.align ?? 'center';\n const miterLimit = stroke.miterLimit ?? DEFAULT_MITER_LIMIT;\n const varyingThreshold = stroke.varyingWidthJoinThreshold ?? DEFAULT_VARYING_WIDTH_JOIN_THRESHOLD;\n\n // RectPath fast path for inner/outer alignment: shift the rect by half-width\n // so the ribbon's center alignment lands the stroke on the desired side of\n // the original geometric edge. Arbitrary paths handle alignment via stencil\n // clipping at the renderer level (not here).\n let workingPath = path;\n if (path.kind === 'rect' && align !== 'center') {\n const aligned = alignedStrokeRect(path, align, width);\n workingPath = {\n kind: 'rect',\n x: aligned.x,\n y: aligned.y,\n width: aligned.width,\n height: aligned.height,\n };\n }\n\n const polylines = extractPolylines(workingPath, opts);\n // Derive per-polyline-point widths by lerping vertexWidths across\n // anchor pairs using each point's anchorT. When vertexWidths is absent\n // or invalid (length mismatch / non-finite values), polyline.widths is\n // left undefined and the uniform-width fast path runs.\n if (stroke.vertexWidths && stroke.vertexWidths.length > 0) {\n for (const pl of polylines) populatePolylineWidths(pl, stroke.vertexWidths, width);\n }\n const dash = stroke.dash ?? [];\n const verts: number[] = [];\n const idx: number[] = [];\n const anchorA: number[] = [];\n const anchorB: number[] = [];\n const anchorT: number[] = [];\n\n // Trim before dashing, so the dash pattern fits the visible line rather than\n // running off under a marker head.\n const startInset = opts.startInset ?? 0;\n const endInset = opts.endInset ?? 0;\n for (const pl of polylines) {\n const trimmed = startInset > 0 || endInset > 0 ? trimPolyline(pl, startInset, endInset) : pl;\n if (trimmed === null) continue;\n const subs = dash.length > 0 ? splitForDash(trimmed, dash) : [trimmed];\n for (const sub of subs) {\n expandPolyline(sub, width, join, cap, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT);\n }\n }\n\n return {\n vertices: new Float32Array(verts),\n indices: new Uint32Array(idx),\n anchorA: new Uint32Array(anchorA),\n anchorB: new Uint32Array(anchorB),\n anchorT: new Float32Array(anchorT),\n };\n}\n\nfunction expandPolyline(\n pl: Polyline,\n width: number,\n join: Join,\n cap: Cap,\n miterLimit: number,\n varyingThreshold: number,\n verts: number[],\n idx: number[],\n anchorA: number[],\n anchorB: number[],\n anchorT: number[],\n): void {\n const half = width / 2;\n const pts = pl.points;\n const ptCount = pts.length / 2;\n const segCount = ptCount - 1;\n if (segCount < 1) return;\n\n // Anchor params per polyline point (set by extractPolylines; splitForDash\n // re-derives them on dash boundaries). Default to A=B=0,t=0 if missing.\n const plA = pl.anchorA ?? new Uint32Array(ptCount);\n const plB = pl.anchorB ?? new Uint32Array(ptCount);\n const plT = pl.anchorT ?? new Float32Array(ptCount);\n // Per-point half-widths: derived from pl.widths if present, else the\n // uniform half. Indexing matches `pts` (one entry per polyline point).\n const plHalf = pl.widths\n ? pl.widths.map((w) => w / 2)\n : null;\n\n const segs: Seg[] = [];\n const segSrcIdx: number[] = []; // For each seg, the polyline-point index of its start.\n for (let s = 0; s < segCount; s++) {\n const ha = plHalf ? plHalf[s] : half;\n const hb = plHalf ? plHalf[s + 1] : half;\n const seg = makeSeg(pts[s * 2], pts[s * 2 + 1], pts[(s + 1) * 2], pts[(s + 1) * 2 + 1], ha, hb);\n if (seg) {\n segs.push(seg);\n segSrcIdx.push(s);\n }\n }\n let closerSrcIdx = -1;\n if (pl.closed && segs.length >= 1) {\n const last = segs[segs.length - 1];\n const first = segs[0];\n // Closer: from last point back to point 0. Half-widths are the\n // already-resolved ones at those source indices.\n const haCloser = plHalf ? plHalf[ptCount - 1] : half;\n const hbCloser = plHalf ? plHalf[0] : half;\n const closer = makeSeg(last.bx, last.by, first.ax, first.ay, haCloser, hbCloser);\n if (closer) {\n segs.push(closer);\n closerSrcIdx = ptCount - 1; // start is the last polyline point.\n segSrcIdx.push(closerSrcIdx);\n }\n }\n if (segs.length === 0) return;\n\n // Emit ribbon quads. For each emitted vertex, record the (A, B, t) of the\n // source polyline point that the geometric vertex sits at. Per-end\n // half-widths produce trapezoidal segments when the stroke tapers.\n const segBaseIdx: number[] = [];\n for (let s = 0; s < segs.length; s++) {\n const seg = segs[s];\n const startSrc = segSrcIdx[s];\n // End-source index for the seg. For closer (closed-path wraparound), end is point 0.\n const endSrc = (s === segs.length - 1 && pl.closed && closerSrcIdx >= 0) ? 0 : startSrc + 1;\n const base = verts.length / 2;\n segBaseIdx.push(base);\n const nxA = seg.nxUnit * seg.halfA, nyA = seg.nyUnit * seg.halfA;\n const nxB = seg.nxUnit * seg.halfB, nyB = seg.nyUnit * seg.halfB;\n // L0, R0 — at seg.ax/ay with start half-width.\n verts.push(seg.ax + nxA, seg.ay + nyA);\n anchorA.push(plA[startSrc]); anchorB.push(plB[startSrc]); anchorT.push(plT[startSrc]);\n verts.push(seg.ax - nxA, seg.ay - nyA);\n anchorA.push(plA[startSrc]); anchorB.push(plB[startSrc]); anchorT.push(plT[startSrc]);\n // L1, R1 — at seg.bx/by with end half-width.\n verts.push(seg.bx + nxB, seg.by + nyB);\n anchorA.push(plA[endSrc]); anchorB.push(plB[endSrc]); anchorT.push(plT[endSrc]);\n verts.push(seg.bx - nxB, seg.by - nyB);\n anchorA.push(plA[endSrc]); anchorB.push(plB[endSrc]); anchorT.push(plT[endSrc]);\n idx.push(base, base + 1, base + 2, base + 1, base + 3, base + 2);\n }\n\n // Joins between consecutive segments.\n const joinCount = pl.closed ? segs.length : segs.length - 1;\n for (let j = 0; j < joinCount; j++) {\n emitJoin(segs, segBaseIdx, j, join, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT, segSrcIdx, plA, plB, plT);\n }\n\n // Caps. Use the local half-width at the cap end (seg.halfA at start, seg.halfB at end).\n if (!pl.closed && cap !== 'butt') {\n const first = segs[0];\n const firstBase = segBaseIdx[0];\n emitCap(first, firstBase + 0, firstBase + 1, false, first.halfA, cap, verts, idx, anchorA, anchorB, anchorT, plA[0], plB[0], plT[0]);\n\n const last = segs[segs.length - 1];\n const lastBase = segBaseIdx[segs.length - 1];\n const lastSrc = segSrcIdx[segs.length - 1] + 1;\n emitCap(last, lastBase + 2, lastBase + 3, true, last.halfB, cap, verts, idx, anchorA, anchorB, anchorT, plA[lastSrc], plB[lastSrc], plT[lastSrc]);\n }\n}\n\n/** Fill `pl.widths` by interpolating `vertexWidths` across each point's\n * (anchorA, anchorB, anchorT). Anchor-aligned points (A === B) read the\n * anchor's value directly. Out-of-range / non-finite anchor entries fall\n * back to `fallbackWidth`. */\nfunction populatePolylineWidths(\n pl: Polyline,\n vertexWidths: number[],\n fallbackWidth: number,\n): void {\n const ptCount = pl.points.length / 2;\n if (ptCount === 0) return;\n const aA = pl.anchorA ?? new Uint32Array(ptCount);\n const aB = pl.anchorB ?? new Uint32Array(ptCount);\n const aT = pl.anchorT ?? new Float32Array(ptCount);\n const widths = new Float32Array(ptCount);\n const read = (i: number): number => {\n const v = vertexWidths[i];\n return typeof v === 'number' && isFinite(v) && v > 0 ? v : fallbackWidth;\n };\n for (let i = 0; i < ptCount; i++) {\n const a = aA[i], b = aB[i], t = aT[i];\n if (a === b) {\n widths[i] = read(a);\n } else {\n const wa = read(a);\n const wb = read(b);\n widths[i] = wa + (wb - wa) * t;\n }\n }\n pl.widths = widths;\n}\n\nconst ROUND_CAP_STEP_RAD = (10 * Math.PI) / 180;\n\n/**\n * Emit a cap at one end of a segment. `leftIdx`/`rightIdx` are the buffer\n * indices of the L (perpendicular +n) and R (perpendicular -n) vertices at\n * the segment endpoint where the cap attaches. `atEnd` chooses which end of\n * the segment to extend from (and which cap-direction sign to use).\n */\nfunction emitCap(\n seg: Seg, leftIdx: number, rightIdx: number, atEnd: boolean,\n half: number, cap: Cap,\n verts: number[], idx: number[],\n anchorA: number[], anchorB: number[], anchorT: number[],\n endA: number, endB: number, endT: number,\n): void {\n // Outward direction = forward at end, backward at start.\n const sign = atEnd ? 1 : -1;\n const dx = (seg.bx - seg.ax) / seg.len * sign;\n const dy = (seg.by - seg.ay) / seg.len * sign;\n const cx = atEnd ? seg.bx : seg.ax;\n const cy = atEnd ? seg.by : seg.ay;\n\n // Perpendicular offset at this cap end uses the local half-width\n // (varies along the polyline when vertexWidths is set).\n const nx = seg.nxUnit * half;\n const ny = seg.nyUnit * half;\n\n if (cap === 'square') {\n const ox = cx + dx * half;\n const oy = cy + dy * half;\n const lOut = verts.length / 2;\n verts.push(ox + nx, oy + ny);\n anchorA.push(endA); anchorB.push(endB); anchorT.push(endT);\n const rOut = verts.length / 2;\n verts.push(ox - nx, oy - ny);\n anchorA.push(endA); anchorB.push(endB); anchorT.push(endT);\n // Two triangles forming the cap rectangle. Wind so triangles are CCW\n // viewed from the front; orientation symmetric for start vs. end caps.\n if (atEnd) {\n idx.push(leftIdx, lOut, rOut, leftIdx, rOut, rightIdx);\n } else {\n idx.push(leftIdx, rOut, lOut, leftIdx, rightIdx, rOut);\n }\n return;\n }\n\n if (cap === 'round') {\n // Pivot vertex at the endpoint center; fan from L through the outward\n // 180° arc to R.\n const pivotIdx = verts.length / 2;\n verts.push(cx, cy);\n anchorA.push(endA); anchorB.push(endB); anchorT.push(endT);\n\n // Starting angle: direction from pivot to L (= +n direction).\n const startAngle = Math.atan2(ny, nx);\n // Sweep 180° toward the outward direction. The outward unit is (dx, dy);\n // we rotate from +n to -n the \"outward\" way. The dot product of (dx, dy)\n // with the perpendicular tells us which way around the arc to go.\n // A start-cap (sign = -1) sweeps backward; end-cap (sign = +1) sweeps forward.\n const sweep = atEnd ? -Math.PI : Math.PI;\n const steps = Math.max(1, Math.ceil(Math.abs(sweep) / ROUND_CAP_STEP_RAD));\n const stepAngle = sweep / steps;\n\n let prevIdx = leftIdx;\n for (let i = 1; i < steps; i++) {\n const ang = startAngle + i * stepAngle;\n const fx = cx + Math.cos(ang) * half;\n const fy = cy + Math.sin(ang) * half;\n const newIdx = verts.length / 2;\n verts.push(fx, fy);\n anchorA.push(endA); anchorB.push(endB); anchorT.push(endT);\n if (atEnd) idx.push(prevIdx, newIdx, pivotIdx);\n else idx.push(prevIdx, pivotIdx, newIdx);\n prevIdx = newIdx;\n }\n if (atEnd) idx.push(prevIdx, rightIdx, pivotIdx);\n else idx.push(prevIdx, pivotIdx, rightIdx);\n }\n}\n\n/**\n * Split a polyline into open sub-polylines for the \"on\" portions of a dash\n * pattern. Each output sub-polyline gets caps from the stroke's `cap` setting.\n * The \"off\" portions become invisible gaps. Anchor params are carried through:\n * sub-polyline endpoints inherit their source polyline-point's anchor params;\n * dash boundaries that land mid-segment (between two polyline points) snap\n * their anchor params to whichever endpoint they're closer to when the\n * segment crosses a path-anchor boundary, otherwise interpolate `t` linearly.\n */\nfunction splitForDash(pl: Polyline, dash: number[]): Polyline[] {\n const out: Polyline[] = [];\n const plA = pl.anchorA ?? new Uint32Array(pl.points.length / 2);\n const plB = pl.anchorB ?? new Uint32Array(pl.points.length / 2);\n const plT = pl.anchorT ?? new Float32Array(pl.points.length / 2);\n\n let dashIdx = 0;\n let dashRemaining = dash[0];\n let onPhase = true;\n let curPts: number[] | null = onPhase ? [pl.points[0], pl.points[1]] : null;\n let curA: number[] | null = onPhase ? [plA[0]] : null;\n let curB: number[] | null = onPhase ? [plB[0]] : null;\n let curT: number[] | null = onPhase ? [plT[0]] : null;\n\n const flushAndAdvance = () => {\n if (curPts && curPts.length >= 4) {\n out.push({\n points: curPts,\n closed: false,\n anchorA: new Uint32Array(curA!),\n anchorB: new Uint32Array(curB!),\n anchorT: new Float32Array(curT!),\n });\n }\n curPts = null; curA = null; curB = null; curT = null;\n dashIdx = (dashIdx + 1) % dash.length;\n dashRemaining = dash[dashIdx];\n onPhase = !onPhase;\n };\n\n let prevX = pl.points[0], prevY = pl.points[1];\n const ptCount = pl.points.length / 2;\n const segCount = pl.closed ? ptCount : ptCount - 1;\n\n for (let i = 0; i < segCount; i++) {\n const nextIdx = (i + 1) % ptCount;\n const cx = pl.points[nextIdx * 2], cy = pl.points[nextIdx * 2 + 1];\n const startA = plA[i], startB = plB[i], startT = plT[i];\n const endA = plA[nextIdx], endB = plB[nextIdx], endT = plT[nextIdx];\n const segFullDx = cx - prevX, segFullDy = cy - prevY;\n const segFullLen = Math.hypot(segFullDx, segFullDy);\n let segDx = segFullDx, segDy = segFullDy;\n let segLen = segFullLen;\n let traveled = 0;\n\n while (segLen > 1e-9) {\n if (segLen <= dashRemaining) {\n if (onPhase && curPts) {\n curPts.push(cx, cy);\n curA!.push(endA); curB!.push(endB); curT!.push(endT);\n }\n dashRemaining -= segLen;\n prevX = cx; prevY = cy;\n traveled = segFullLen;\n segLen = 0;\n if (dashRemaining <= 1e-9) {\n flushAndAdvance();\n if (onPhase) {\n curPts = [prevX, prevY];\n curA = [endA]; curB = [endB]; curT = [endT];\n }\n }\n } else {\n const tConsume = dashRemaining / segLen;\n const ix = prevX + segDx * tConsume;\n const iy = prevY + segDy * tConsume;\n traveled += dashRemaining;\n const frac = traveled / segFullLen;\n let mA: number, mB: number, mT: number;\n if (startA === endA && startB === endB) {\n mA = startA; mB = startB;\n mT = startT + (endT - startT) * frac;\n } else {\n // Cross-anchor segment: pick the nearer endpoint's params.\n if (frac < 0.5) { mA = startA; mB = startB; mT = startT; }\n else { mA = endA; mB = endB; mT = endT; }\n }\n if (onPhase && curPts) {\n curPts.push(ix, iy);\n curA!.push(mA); curB!.push(mB); curT!.push(mT);\n }\n prevX = ix; prevY = iy;\n segDx = cx - prevX; segDy = cy - prevY;\n segLen = Math.hypot(segDx, segDy);\n flushAndAdvance();\n if (onPhase) {\n curPts = [prevX, prevY];\n curA = [mA]; curB = [mB]; curT = [mT];\n }\n }\n }\n }\n\n if (curPts && curPts.length >= 4) {\n const tail: Polyline = {\n points: curPts,\n closed: false,\n anchorA: new Uint32Array(curA!),\n anchorB: new Uint32Array(curB!),\n anchorT: new Float32Array(curT!),\n };\n // A closed subpath's walk ends at the vertex it started from, so the run\n // still open there and the run that started there are one dash. Pushed\n // separately they are two butt-capped ribbons meeting at the seam — the\n // visible artifact whenever the pattern doesn't divide the perimeter.\n const tx = curPts[curPts.length - 2], ty = curPts[curPts.length - 1];\n const meets = (x: number, y: number) => Math.hypot(tx - x, ty - y) <= 1e-9;\n if (pl.closed && out.length > 0 && meets(out[0].points[0], out[0].points[1])) {\n out[0] = joinAtSeam(tail, out[0]);\n } else if (pl.closed && out.length === 0 && meets(curPts[0], curPts[1])) {\n // Every gap fell outside the perimeter: the dash is the whole loop, and\n // a loop stroked as an open run seams at its own start vertex too.\n out.push({\n points: curPts.slice(0, -2),\n closed: true,\n anchorA: tail.anchorA!.slice(0, -1),\n anchorB: tail.anchorB!.slice(0, -1),\n anchorT: tail.anchorT!.slice(0, -1),\n });\n } else {\n out.push(tail);\n }\n }\n return out;\n}\n\n/** `tail` continued by `head`, which starts where `tail` ends — the shared\n * vertex is carried once. */\nfunction joinAtSeam(tail: Polyline, head: Polyline): Polyline {\n return {\n points: [...tail.points, ...head.points.slice(2)],\n closed: false,\n anchorA: Uint32Array.from([...tail.anchorA!, ...head.anchorA!.subarray(1)]),\n anchorB: Uint32Array.from([...tail.anchorB!, ...head.anchorB!.subarray(1)]),\n anchorT: Float32Array.from([...tail.anchorT!, ...head.anchorT!.subarray(1)]),\n };\n}\n\nfunction makeSeg(ax: number, ay: number, bx: number, by: number, halfA: number, halfB: number): Seg | null {\n const dx = bx - ax, dy = by - ay;\n const len = Math.hypot(dx, dy);\n if (len === 0) return null;\n return {\n ax, ay, bx, by,\n nxUnit: -dy / len, nyUnit: dx / len,\n halfA, halfB,\n len,\n };\n}\n\n/**\n * Miter limit for a stroke that does not set one. SVG's default, not\n * Canvas2D's 10, because the serializer omits the attribute when the field is\n * unset — so a kit stroke exported to SVG is read back by every other renderer\n * as 4, and a kit that used 10 would disagree with its own export. 10 also\n * lets an acute corner throw a spike four times the half-width, which is what\n * a glyph outline's sharpest vertices do.\n */\nconst DEFAULT_MITER_LIMIT = 4;\n/** Default `Stroke.varyingWidthJoinThreshold`. */\nconst DEFAULT_VARYING_WIDTH_JOIN_THRESHOLD = 1.5;\n\nfunction emitJoin(\n segs: Seg[], segBaseIdx: number[], j: number, join: Join,\n miterLimit: number,\n varyingThreshold: number,\n verts: number[], idx: number[],\n anchorA: number[], anchorB: number[], anchorT: number[],\n segSrcIdx: number[],\n plA: Uint32Array, plB: Uint32Array, plT: Float32Array,\n): void {\n const a = segs[j];\n const b = segs[(j + 1) % segs.length];\n const aBase = segBaseIdx[j];\n const bBase = segBaseIdx[(j + 1) % segs.length];\n\n const adx = a.bx - a.ax, ady = a.by - a.ay;\n const bdx = b.bx - b.ax, bdy = b.by - b.ay;\n const cross = adx * bdy - ady * bdx;\n if (cross === 0) return; // straight (collinear), no wedge\n const onPositive = cross > 0;\n\n const aOuterEnd = onPositive ? aBase + 3 : aBase + 2;\n const bOuterStart = onPositive ? bBase + 1 : bBase + 0;\n\n // The corner of join `j` is the start of segment `j+1` (which is the END of\n // segment `j` — they share the same polyline point). For the wraparound join\n // on a closed polyline, j+1 wraps to segment 0 whose start is the closer\n // segment's endpoint, which is polyline point 0.\n const nextSeg = (j + 1) % segs.length;\n // The corner anchor is the start of the next segment (shared endpoint).\n // For a closed path's final join, nextSeg wraps to 0, whose start is\n // polyline point 0 — correct.\n const cornerSrc = segSrcIdx[nextSeg];\n const cornerA = plA[cornerSrc];\n const cornerB = plB[cornerSrc];\n const cornerT = plT[cornerSrc];\n // The two adjoining half-widths agree at the shared corner (a.halfB ===\n // b.halfA for a single-contour polyline), so use either for miter-limit\n // sizing. Force bevel when either adjacent segment itself tapers by\n // more than `varyingThreshold` — the miter apex math computes against\n // outer edges parallel to the segment direction, an assumption that\n // breaks on tapered trapezoids (outer edges fan in/out).\n const halfAtCorner = a.halfB;\n const taperA = Math.max(a.halfA, a.halfB) / Math.max(1e-9, Math.min(a.halfA, a.halfB));\n const taperB = Math.max(b.halfA, b.halfB) / Math.max(1e-9, Math.min(b.halfA, b.halfB));\n const forceBevelForVarying = taperA > varyingThreshold || taperB > varyingThreshold;\n\n if (join === 'bevel' || forceBevelForVarying) {\n emitBevel(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);\n return;\n }\n\n if (join === 'miter') {\n // Compute apex: intersection of A's outer edge extended forward and\n // B's outer edge extended backward. If miter length > MITER_LIMIT * half,\n // fall back to bevel.\n const aOX = verts[aOuterEnd * 2], aOY = verts[aOuterEnd * 2 + 1];\n const bOX = verts[bOuterStart * 2], bOY = verts[bOuterStart * 2 + 1];\n const apex = lineLineIntersect(aOX, aOY, adx, ady, bOX, bOY, -bdx, -bdy);\n if (!apex) {\n emitBevel(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);\n return;\n }\n const miterLen = Math.hypot(apex[0] - a.bx, apex[1] - a.by);\n if (miterLen > miterLimit * halfAtCorner) {\n emitBevel(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);\n return;\n }\n // A miter join's outer face is a kite quadrilateral with vertices\n // (R1, apex, R0, J) — covered by TWO triangles. The bevel inner half\n // (R1, R0, J) fills the area between the joint corner and the diagonal\n // hypotenuse R0-R1; the outer extension (R1, apex, R0) fills from the\n // hypotenuse to the apex point. Without the bevel half, an \"inverted\n // triangle\" gap opens at every miter corner.\n const apexIdx = verts.length / 2;\n verts.push(apex[0], apex[1]);\n anchorA.push(cornerA); anchorB.push(cornerB); anchorT.push(cornerT);\n const jIdx = verts.length / 2;\n verts.push(a.bx, a.by);\n anchorA.push(cornerA); anchorB.push(cornerB); anchorT.push(cornerT);\n if (onPositive) {\n idx.push(aOuterEnd, apexIdx, bOuterStart);\n idx.push(aOuterEnd, bOuterStart, jIdx);\n } else {\n idx.push(aOuterEnd, bOuterStart, apexIdx);\n idx.push(aOuterEnd, jIdx, bOuterStart);\n }\n return;\n }\n\n if (join === 'round') {\n emitRoundJoin(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);\n return;\n }\n}\n\nconst ROUND_STEP_RAD = (10 * Math.PI) / 180; // ~10° per fan triangle\n\nfunction emitRoundJoin(\n a: Seg, aOuterEnd: number, bOuterStart: number, onPositive: boolean,\n verts: number[], idx: number[],\n anchorA: number[], anchorB: number[], anchorT: number[],\n cornerA: number, cornerB: number, cornerT: number,\n): void {\n // Pivot = joint center; emit fresh vertex.\n const cx = a.bx, cy = a.by;\n const pivotIdx = verts.length / 2;\n verts.push(cx, cy);\n anchorA.push(cornerA); anchorB.push(cornerB); anchorT.push(cornerT);\n\n const startX = verts[aOuterEnd * 2] - cx;\n const startY = verts[aOuterEnd * 2 + 1] - cy;\n const endX = verts[bOuterStart * 2] - cx;\n const endY = verts[bOuterStart * 2 + 1] - cy;\n const r = Math.hypot(startX, startY);\n\n const startAngle = Math.atan2(startY, startX);\n const endAngle = Math.atan2(endY, endX);\n // Outer-side sweep: for CCW turns (cross > 0, onPositive true) outer is on\n // the -n side and the arc sweeps from R1 (south of joint) clockwise to R0\n // (east of joint) — i.e., negative sweep. For CW turns, sweep is positive.\n // Pick the SHORT arc that crosses the outer wedge. For onPositive=true\n // (CW turn in screen coords with y-down), the outer wedge sits where the\n // natural sweep is positive; for onPositive=false, where it is negative.\n // If the natural sweep has the wrong sign, wrap by ±2π.\n let sweep = endAngle - startAngle;\n if (onPositive && sweep < 0) sweep += 2 * Math.PI;\n else if (!onPositive && sweep > 0) sweep -= 2 * Math.PI;\n\n const steps = Math.max(1, Math.ceil(Math.abs(sweep) / ROUND_STEP_RAD));\n const stepAngle = sweep / steps;\n\n let prevIdx = aOuterEnd;\n for (let i = 1; i < steps; i++) {\n const ang = startAngle + i * stepAngle;\n const fx = cx + Math.cos(ang) * r;\n const fy = cy + Math.sin(ang) * r;\n const newIdx = verts.length / 2;\n verts.push(fx, fy);\n anchorA.push(cornerA); anchorB.push(cornerB); anchorT.push(cornerT);\n if (onPositive) idx.push(prevIdx, pivotIdx, newIdx);\n else idx.push(prevIdx, newIdx, pivotIdx);\n prevIdx = newIdx;\n }\n if (onPositive) idx.push(prevIdx, pivotIdx, bOuterStart);\n else idx.push(prevIdx, bOuterStart, pivotIdx);\n}\n\nfunction emitBevel(\n a: Seg, aOuterEnd: number, bOuterStart: number, onPositive: boolean,\n verts: number[], idx: number[],\n anchorA: number[], anchorB: number[], anchorT: number[],\n cornerA: number, cornerB: number, cornerT: number,\n): void {\n const jIdx = verts.length / 2;\n verts.push(a.bx, a.by);\n anchorA.push(cornerA); anchorB.push(cornerB); anchorT.push(cornerT);\n if (onPositive) idx.push(aOuterEnd, jIdx, bOuterStart);\n else idx.push(aOuterEnd, bOuterStart, jIdx);\n}\n\n/**\n * Solve for the intersection of line A (point ap, direction ad) with line B\n * (point bp, direction bd). Returns null if parallel or near-parallel.\n */\nfunction lineLineIntersect(\n apx: number, apy: number, adx: number, ady: number,\n bpx: number, bpy: number, bdx: number, bdy: number,\n): [number, number] | null {\n const denom = adx * bdy - ady * bdx;\n if (Math.abs(denom) < 1e-9) return null;\n const t = ((bpx - apx) * bdy - (bpy - apy) * bdx) / denom;\n return [apx + t * adx, apy + t * ady];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"stroke.js","names":["EMPTY_MESH","resolveStrokeWidth","resolveScreenLength","width","scale","tessellateStroke","stroke","join","cap","align","miterLimit","DEFAULT_MITER_LIMIT","varyingThreshold","DEFAULT_VARYING_WIDTH_JOIN_THRESHOLD","metric","opts","rectInMetric","path","rectIntoMetric","workingPath","aligned","alignedStrokeRect","polylines","extractPolylines","vertexWidths","worldWidthInMetric","tx","ty","worldWidthInMetricAlongWorld","ux","uy","dash","verts","idx","anchorA","anchorB","anchorT","ribbonSpace","pl","populatePolylineWidths","trimmed","trimInSpace","subs","splitForDash","sub","vertices","coordsOutOfMetric","expandPolyline","half","pts","ptCount","segCount","plA","plB","plT","plHalf","w","segs","segSrcIdx","s","ha","hb","seg","makeSeg","closerSrcIdx","last","first","haCloser","hbCloser","closer","segBaseIdx","startSrc","endSrc","base","nxA","nyA","nxB","nyB","joinCount","j","firstBase","lastBase","lastSrc","ROUND_CAP_STEP_RAD","emitCap","sign","atEnd","dx","dy","cx","cy","nx","ny","ox","oy","lOut","endA","endB","endT","rOut","leftIdx","rightIdx","pivotIdx","startAngle","sweep","steps","stepAngle","prevIdx","i","ang","fx","fy","newIdx","bx","ax","by","ay","len","emitJoin","a","b","aBase","bBase","adx","ady","bdx","bdy","cross","onPositive","aOuterEnd","bOuterStart","cornerSrc","cornerA","cornerB","cornerT","halfAtCorner","taperA","taperB","aOX","aOY","bOX","bOY","apex","lineLineIntersect","apexIdx","jIdx","ROUND_STEP_RAD","emitRoundJoin","startX","startY","endX","endY","r","emitBevel","denom","t","bpx","apx","bpy","apy"],"sources":["../../../../src/features/paths/tessellate/stroke.ts"],"sourcesContent":["import type { Path } from '@weasel-js/core';\nimport type { ScreenLength, Stroke } from '@weasel-js/paint';\nimport { alignedStrokeRect, resolveScreenLength } from '@weasel-js/paint';\nimport { type Mesh, extractPolylines, type Polyline } from '@weasel-js/geom/tessellate';\nimport { splitForDash } from './dash';\nimport { populatePolylineWidths, type WidthScale } from './vertexWidths';\nimport { type InsetLength, trimInSpace } from './insets';\nimport {\n type StrokeMetric, coordsOutOfMetric, rectIntoMetric, worldWidthInMetric, worldWidthInMetricAlongWorld,\n} from './metric';\n\n/** Options for stroke tessellation. */\nexport interface StrokeOptions {\n flattenTolerance?: number;\n /**\n * Shorten each open subpath by this much at its start / end: a length in\n * the ribbon's space, or one measured in a space of its own. Stroke markers\n * use this so a filled head is not speared by its own line; the caller\n * resolves the distance, because this layer knows nothing about the marker\n * registry.\n */\n startInset?: InsetLength;\n endInset?: InsetLength;\n /**\n * Build the ribbon in this metric's space and map it back out, so the\n * transform it was derived from draws it at an exact screen width — see\n * `./metric`. Width and dash are then lengths in that space; `vertexWidths`\n * stay world widths. The path is flattened in world first, so the ribbon\n * follows the same vertices its fill does.\n */\n metric?: StrokeMetric;\n}\n\nconst EMPTY_MESH: Mesh = {\n vertices: new Float32Array(0),\n indices: new Uint32Array(0),\n anchorA: new Uint32Array(0),\n anchorB: new Uint32Array(0),\n anchorT: new Float32Array(0),\n};\n\ntype Cap = 'butt' | 'round' | 'square';\ntype Join = 'miter' | 'round' | 'bevel';\n\n/** Per-segment data the join code needs. */\ninterface Seg {\n ax: number; ay: number;\n bx: number; by: number;\n /** Unit perpendicular (rotated 90° CCW from direction). */\n nxUnit: number; nyUnit: number;\n /** Half-width at the start (a) and end (b) endpoints. Equal for uniform\n * strokes; differ when the polyline carries per-point widths. */\n halfA: number; halfB: number;\n len: number;\n}\n\n/** Resolve a stroke width to world units. A number is already world units;\n * `{ px }` is screen pixels divided by the accumulated scale, so it holds its\n * on-screen thickness as the view zooms. */\nexport function resolveStrokeWidth(width: ScreenLength, scale: number): number {\n return resolveScreenLength(width, scale);\n}\n\n/**\n * Build a triangle-mesh ribbon from a stroked Path.\n *\n * Supports:\n * - cap: 'butt' | 'round' | 'square'\n * - join: 'miter' | 'round' | 'bevel'\n * - center alignment directly; RectPath inner/outer alignment via a\n * pre-shifted rect; PolygonPath inner/outer alignment via stencil at the\n * renderer level\n * - dash splitting\n */\nexport function tessellateStroke(\n path: Path,\n stroke: Stroke,\n opts: StrokeOptions = {},\n): Mesh {\n // The renderer resolves `{ px }` against the accumulated transform before it\n // gets here; a stroke that reaches the tessellator unresolved is read at\n // scale 1, matching what the ribbon cache keys it under.\n const width = resolveStrokeWidth(stroke.width ?? 1, 1);\n if (width <= 0) return EMPTY_MESH;\n const join: Join = stroke.join ?? 'miter';\n const cap: Cap = stroke.cap ?? 'butt';\n const align = stroke.align ?? 'center';\n const miterLimit = stroke.miterLimit ?? DEFAULT_MITER_LIMIT;\n const varyingThreshold = stroke.varyingWidthJoinThreshold ?? DEFAULT_VARYING_WIDTH_JOIN_THRESHOLD;\n\n // RectPath fast path for inner/outer alignment: shift the rect by half-width\n // so the ribbon's center alignment lands the stroke on the desired side of\n // the original geometric edge. Arbitrary paths handle alignment via stencil\n // clipping at the renderer level (not here).\n const metric = opts.metric;\n const rectInMetric = metric && path.kind === 'rect' ? rectIntoMetric(path, metric) : null;\n let workingPath: Path = rectInMetric ?? path;\n if (workingPath.kind === 'rect' && align !== 'center') {\n const aligned = alignedStrokeRect(workingPath, align, width);\n workingPath = {\n kind: 'rect',\n x: aligned.x,\n y: aligned.y,\n width: aligned.width,\n height: aligned.height,\n };\n }\n\n const polylines = extractPolylines(workingPath, opts);\n // Per-point widths come first, in the space the points are in, so a trim\n // interpolates them rather than re-reading an anchor's.\n const vertexWidths = stroke.vertexWidths && stroke.vertexWidths.length > 0 ? stroke.vertexWidths : null;\n const scale: WidthScale | undefined = !metric ? undefined\n : rectInMetric ? (tx, ty) => worldWidthInMetric(metric, tx, ty)\n : (ux, uy) => worldWidthInMetricAlongWorld(metric, ux, uy);\n const dash = stroke.dash ?? [];\n const verts: number[] = [];\n const idx: number[] = [];\n const anchorA: number[] = [];\n const anchorB: number[] = [];\n const anchorT: number[] = [];\n\n // Trim before dashing, so the dash pattern fits the visible line rather than\n // running off under a marker head.\n const ribbonSpace = metric ?? null;\n for (const pl of polylines) {\n if (vertexWidths) populatePolylineWidths(pl, vertexWidths, width, scale);\n const trimmed = trimInSpace(pl, rectInMetric ? ribbonSpace : null, ribbonSpace, opts.startInset, opts.endInset);\n if (trimmed === null) continue;\n const subs = dash.length > 0 ? splitForDash(trimmed, dash) : [trimmed];\n for (const sub of subs) {\n expandPolyline(sub, width, join, cap, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT);\n }\n }\n\n const vertices = new Float32Array(verts);\n if (metric) coordsOutOfMetric(vertices, metric);\n return {\n vertices,\n indices: new Uint32Array(idx),\n anchorA: new Uint32Array(anchorA),\n anchorB: new Uint32Array(anchorB),\n anchorT: new Float32Array(anchorT),\n };\n}\n\nfunction expandPolyline(\n pl: Polyline,\n width: number,\n join: Join,\n cap: Cap,\n miterLimit: number,\n varyingThreshold: number,\n verts: number[],\n idx: number[],\n anchorA: number[],\n anchorB: number[],\n anchorT: number[],\n): void {\n const half = width / 2;\n const pts = pl.points;\n const ptCount = pts.length / 2;\n const segCount = ptCount - 1;\n if (segCount < 1) return;\n\n // Anchor params per polyline point (set by extractPolylines; splitForDash\n // re-derives them on dash boundaries). Default to A=B=0,t=0 if missing.\n const plA = pl.anchorA ?? new Uint32Array(ptCount);\n const plB = pl.anchorB ?? new Uint32Array(ptCount);\n const plT = pl.anchorT ?? new Float32Array(ptCount);\n // Per-point half-widths: derived from pl.widths if present, else the\n // uniform half. Indexing matches `pts` (one entry per polyline point).\n const plHalf = pl.widths\n ? pl.widths.map((w) => w / 2)\n : null;\n\n const segs: Seg[] = [];\n const segSrcIdx: number[] = []; // For each seg, the polyline-point index of its start.\n for (let s = 0; s < segCount; s++) {\n const ha = plHalf ? plHalf[s] : half;\n const hb = plHalf ? plHalf[s + 1] : half;\n const seg = makeSeg(pts[s * 2], pts[s * 2 + 1], pts[(s + 1) * 2], pts[(s + 1) * 2 + 1], ha, hb);\n if (seg) {\n segs.push(seg);\n segSrcIdx.push(s);\n }\n }\n let closerSrcIdx = -1;\n if (pl.closed && segs.length >= 1) {\n const last = segs[segs.length - 1];\n const first = segs[0];\n // Closer: from last point back to point 0. Half-widths are the\n // already-resolved ones at those source indices.\n const haCloser = plHalf ? plHalf[ptCount - 1] : half;\n const hbCloser = plHalf ? plHalf[0] : half;\n const closer = makeSeg(last.bx, last.by, first.ax, first.ay, haCloser, hbCloser);\n if (closer) {\n segs.push(closer);\n closerSrcIdx = ptCount - 1; // start is the last polyline point.\n segSrcIdx.push(closerSrcIdx);\n }\n }\n if (segs.length === 0) return;\n\n // Emit ribbon quads. For each emitted vertex, record the (A, B, t) of the\n // source polyline point that the geometric vertex sits at. Per-end\n // half-widths produce trapezoidal segments when the stroke tapers.\n const segBaseIdx: number[] = [];\n for (let s = 0; s < segs.length; s++) {\n const seg = segs[s];\n const startSrc = segSrcIdx[s];\n // End-source index for the seg. For closer (closed-path wraparound), end is point 0.\n const endSrc = (s === segs.length - 1 && pl.closed && closerSrcIdx >= 0) ? 0 : startSrc + 1;\n const base = verts.length / 2;\n segBaseIdx.push(base);\n const nxA = seg.nxUnit * seg.halfA, nyA = seg.nyUnit * seg.halfA;\n const nxB = seg.nxUnit * seg.halfB, nyB = seg.nyUnit * seg.halfB;\n // L0, R0 — at seg.ax/ay with start half-width.\n verts.push(seg.ax + nxA, seg.ay + nyA);\n anchorA.push(plA[startSrc]); anchorB.push(plB[startSrc]); anchorT.push(plT[startSrc]);\n verts.push(seg.ax - nxA, seg.ay - nyA);\n anchorA.push(plA[startSrc]); anchorB.push(plB[startSrc]); anchorT.push(plT[startSrc]);\n // L1, R1 — at seg.bx/by with end half-width.\n verts.push(seg.bx + nxB, seg.by + nyB);\n anchorA.push(plA[endSrc]); anchorB.push(plB[endSrc]); anchorT.push(plT[endSrc]);\n verts.push(seg.bx - nxB, seg.by - nyB);\n anchorA.push(plA[endSrc]); anchorB.push(plB[endSrc]); anchorT.push(plT[endSrc]);\n idx.push(base, base + 1, base + 2, base + 1, base + 3, base + 2);\n }\n\n // Joins between consecutive segments.\n const joinCount = pl.closed ? segs.length : segs.length - 1;\n for (let j = 0; j < joinCount; j++) {\n emitJoin(segs, segBaseIdx, j, join, miterLimit, varyingThreshold, verts, idx, anchorA, anchorB, anchorT, segSrcIdx, plA, plB, plT);\n }\n\n // Caps. Use the local half-width at the cap end (seg.halfA at start, seg.halfB at end).\n if (!pl.closed && cap !== 'butt') {\n const first = segs[0];\n const firstBase = segBaseIdx[0];\n emitCap(first, firstBase + 0, firstBase + 1, false, first.halfA, cap, verts, idx, anchorA, anchorB, anchorT, plA[0], plB[0], plT[0]);\n\n const last = segs[segs.length - 1];\n const lastBase = segBaseIdx[segs.length - 1];\n const lastSrc = segSrcIdx[segs.length - 1] + 1;\n emitCap(last, lastBase + 2, lastBase + 3, true, last.halfB, cap, verts, idx, anchorA, anchorB, anchorT, plA[lastSrc], plB[lastSrc], plT[lastSrc]);\n }\n}\n\nconst ROUND_CAP_STEP_RAD = (10 * Math.PI) / 180;\n\n/**\n * Emit a cap at one end of a segment. `leftIdx`/`rightIdx` are the buffer\n * indices of the L (perpendicular +n) and R (perpendicular -n) vertices at\n * the segment endpoint where the cap attaches. `atEnd` chooses which end of\n * the segment to extend from (and which cap-direction sign to use).\n */\nfunction emitCap(\n seg: Seg, leftIdx: number, rightIdx: number, atEnd: boolean,\n half: number, cap: Cap,\n verts: number[], idx: number[],\n anchorA: number[], anchorB: number[], anchorT: number[],\n endA: number, endB: number, endT: number,\n): void {\n // Outward direction = forward at end, backward at start.\n const sign = atEnd ? 1 : -1;\n const dx = (seg.bx - seg.ax) / seg.len * sign;\n const dy = (seg.by - seg.ay) / seg.len * sign;\n const cx = atEnd ? seg.bx : seg.ax;\n const cy = atEnd ? seg.by : seg.ay;\n\n // Perpendicular offset at this cap end uses the local half-width\n // (varies along the polyline when vertexWidths is set).\n const nx = seg.nxUnit * half;\n const ny = seg.nyUnit * half;\n\n if (cap === 'square') {\n const ox = cx + dx * half;\n const oy = cy + dy * half;\n const lOut = verts.length / 2;\n verts.push(ox + nx, oy + ny);\n anchorA.push(endA); anchorB.push(endB); anchorT.push(endT);\n const rOut = verts.length / 2;\n verts.push(ox - nx, oy - ny);\n anchorA.push(endA); anchorB.push(endB); anchorT.push(endT);\n // Two triangles forming the cap rectangle. Wind so triangles are CCW\n // viewed from the front; orientation symmetric for start vs. end caps.\n if (atEnd) {\n idx.push(leftIdx, lOut, rOut, leftIdx, rOut, rightIdx);\n } else {\n idx.push(leftIdx, rOut, lOut, leftIdx, rightIdx, rOut);\n }\n return;\n }\n\n if (cap === 'round') {\n // Pivot vertex at the endpoint center; fan from L through the outward\n // 180° arc to R.\n const pivotIdx = verts.length / 2;\n verts.push(cx, cy);\n anchorA.push(endA); anchorB.push(endB); anchorT.push(endT);\n\n // Starting angle: direction from pivot to L (= +n direction).\n const startAngle = Math.atan2(ny, nx);\n // Sweep 180° toward the outward direction. The outward unit is (dx, dy);\n // we rotate from +n to -n the \"outward\" way. The dot product of (dx, dy)\n // with the perpendicular tells us which way around the arc to go.\n // A start-cap (sign = -1) sweeps backward; end-cap (sign = +1) sweeps forward.\n const sweep = atEnd ? -Math.PI : Math.PI;\n const steps = Math.max(1, Math.ceil(Math.abs(sweep) / ROUND_CAP_STEP_RAD));\n const stepAngle = sweep / steps;\n\n let prevIdx = leftIdx;\n for (let i = 1; i < steps; i++) {\n const ang = startAngle + i * stepAngle;\n const fx = cx + Math.cos(ang) * half;\n const fy = cy + Math.sin(ang) * half;\n const newIdx = verts.length / 2;\n verts.push(fx, fy);\n anchorA.push(endA); anchorB.push(endB); anchorT.push(endT);\n if (atEnd) idx.push(prevIdx, newIdx, pivotIdx);\n else idx.push(prevIdx, pivotIdx, newIdx);\n prevIdx = newIdx;\n }\n if (atEnd) idx.push(prevIdx, rightIdx, pivotIdx);\n else idx.push(prevIdx, pivotIdx, rightIdx);\n }\n}\n\nfunction makeSeg(ax: number, ay: number, bx: number, by: number, halfA: number, halfB: number): Seg | null {\n const dx = bx - ax, dy = by - ay;\n const len = Math.hypot(dx, dy);\n if (len === 0) return null;\n return {\n ax, ay, bx, by,\n nxUnit: -dy / len, nyUnit: dx / len,\n halfA, halfB,\n len,\n };\n}\n\n/**\n * Miter limit for a stroke that does not set one. SVG's default, not\n * Canvas2D's 10, because the serializer omits the attribute when the field is\n * unset — so a kit stroke exported to SVG is read back by every other renderer\n * as 4, and a kit that used 10 would disagree with its own export. 10 also\n * lets an acute corner throw a spike four times the half-width, which is what\n * a glyph outline's sharpest vertices do.\n */\nconst DEFAULT_MITER_LIMIT = 4;\n/** Default `Stroke.varyingWidthJoinThreshold`. */\nconst DEFAULT_VARYING_WIDTH_JOIN_THRESHOLD = 1.5;\n\nfunction emitJoin(\n segs: Seg[], segBaseIdx: number[], j: number, join: Join,\n miterLimit: number,\n varyingThreshold: number,\n verts: number[], idx: number[],\n anchorA: number[], anchorB: number[], anchorT: number[],\n segSrcIdx: number[],\n plA: Uint32Array, plB: Uint32Array, plT: Float32Array,\n): void {\n const a = segs[j];\n const b = segs[(j + 1) % segs.length];\n const aBase = segBaseIdx[j];\n const bBase = segBaseIdx[(j + 1) % segs.length];\n\n const adx = a.bx - a.ax, ady = a.by - a.ay;\n const bdx = b.bx - b.ax, bdy = b.by - b.ay;\n const cross = adx * bdy - ady * bdx;\n if (cross === 0) return; // straight (collinear), no wedge\n const onPositive = cross > 0;\n\n const aOuterEnd = onPositive ? aBase + 3 : aBase + 2;\n const bOuterStart = onPositive ? bBase + 1 : bBase + 0;\n\n // The corner of join `j` is the start of segment `j+1` (which is the END of\n // segment `j` — they share the same polyline point). For the wraparound join\n // on a closed polyline, j+1 wraps to segment 0 whose start is the closer\n // segment's endpoint, which is polyline point 0.\n const nextSeg = (j + 1) % segs.length;\n // The corner anchor is the start of the next segment (shared endpoint).\n // For a closed path's final join, nextSeg wraps to 0, whose start is\n // polyline point 0 — correct.\n const cornerSrc = segSrcIdx[nextSeg];\n const cornerA = plA[cornerSrc];\n const cornerB = plB[cornerSrc];\n const cornerT = plT[cornerSrc];\n // The two adjoining half-widths agree at the shared corner (a.halfB ===\n // b.halfA for a single-contour polyline), so use either for miter-limit\n // sizing. Force bevel when either adjacent segment itself tapers by\n // more than `varyingThreshold` — the miter apex math computes against\n // outer edges parallel to the segment direction, an assumption that\n // breaks on tapered trapezoids (outer edges fan in/out).\n const halfAtCorner = a.halfB;\n const taperA = Math.max(a.halfA, a.halfB) / Math.max(1e-9, Math.min(a.halfA, a.halfB));\n const taperB = Math.max(b.halfA, b.halfB) / Math.max(1e-9, Math.min(b.halfA, b.halfB));\n const forceBevelForVarying = taperA > varyingThreshold || taperB > varyingThreshold;\n\n if (join === 'bevel' || forceBevelForVarying) {\n emitBevel(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);\n return;\n }\n\n if (join === 'miter') {\n // Compute apex: intersection of A's outer edge extended forward and\n // B's outer edge extended backward. If miter length > MITER_LIMIT * half,\n // fall back to bevel.\n const aOX = verts[aOuterEnd * 2], aOY = verts[aOuterEnd * 2 + 1];\n const bOX = verts[bOuterStart * 2], bOY = verts[bOuterStart * 2 + 1];\n const apex = lineLineIntersect(aOX, aOY, adx, ady, bOX, bOY, -bdx, -bdy);\n if (!apex) {\n emitBevel(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);\n return;\n }\n const miterLen = Math.hypot(apex[0] - a.bx, apex[1] - a.by);\n if (miterLen > miterLimit * halfAtCorner) {\n emitBevel(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);\n return;\n }\n // A miter join's outer face is a kite quadrilateral with vertices\n // (R1, apex, R0, J) — covered by TWO triangles. The bevel inner half\n // (R1, R0, J) fills the area between the joint corner and the diagonal\n // hypotenuse R0-R1; the outer extension (R1, apex, R0) fills from the\n // hypotenuse to the apex point. Without the bevel half, an \"inverted\n // triangle\" gap opens at every miter corner.\n const apexIdx = verts.length / 2;\n verts.push(apex[0], apex[1]);\n anchorA.push(cornerA); anchorB.push(cornerB); anchorT.push(cornerT);\n const jIdx = verts.length / 2;\n verts.push(a.bx, a.by);\n anchorA.push(cornerA); anchorB.push(cornerB); anchorT.push(cornerT);\n if (onPositive) {\n idx.push(aOuterEnd, apexIdx, bOuterStart);\n idx.push(aOuterEnd, bOuterStart, jIdx);\n } else {\n idx.push(aOuterEnd, bOuterStart, apexIdx);\n idx.push(aOuterEnd, jIdx, bOuterStart);\n }\n return;\n }\n\n if (join === 'round') {\n emitRoundJoin(a, aOuterEnd, bOuterStart, onPositive, verts, idx, anchorA, anchorB, anchorT, cornerA, cornerB, cornerT);\n return;\n }\n}\n\nconst ROUND_STEP_RAD = (10 * Math.PI) / 180; // ~10° per fan triangle\n\nfunction emitRoundJoin(\n a: Seg, aOuterEnd: number, bOuterStart: number, onPositive: boolean,\n verts: number[], idx: number[],\n anchorA: number[], anchorB: number[], anchorT: number[],\n cornerA: number, cornerB: number, cornerT: number,\n): void {\n // Pivot = joint center; emit fresh vertex.\n const cx = a.bx, cy = a.by;\n const pivotIdx = verts.length / 2;\n verts.push(cx, cy);\n anchorA.push(cornerA); anchorB.push(cornerB); anchorT.push(cornerT);\n\n const startX = verts[aOuterEnd * 2] - cx;\n const startY = verts[aOuterEnd * 2 + 1] - cy;\n const endX = verts[bOuterStart * 2] - cx;\n const endY = verts[bOuterStart * 2 + 1] - cy;\n const r = Math.hypot(startX, startY);\n\n const startAngle = Math.atan2(startY, startX);\n const endAngle = Math.atan2(endY, endX);\n // Outer-side sweep: for CCW turns (cross > 0, onPositive true) outer is on\n // the -n side and the arc sweeps from R1 (south of joint) clockwise to R0\n // (east of joint) — i.e., negative sweep. For CW turns, sweep is positive.\n // Pick the SHORT arc that crosses the outer wedge. For onPositive=true\n // (CW turn in screen coords with y-down), the outer wedge sits where the\n // natural sweep is positive; for onPositive=false, where it is negative.\n // If the natural sweep has the wrong sign, wrap by ±2π.\n let sweep = endAngle - startAngle;\n if (onPositive && sweep < 0) sweep += 2 * Math.PI;\n else if (!onPositive && sweep > 0) sweep -= 2 * Math.PI;\n\n const steps = Math.max(1, Math.ceil(Math.abs(sweep) / ROUND_STEP_RAD));\n const stepAngle = sweep / steps;\n\n let prevIdx = aOuterEnd;\n for (let i = 1; i < steps; i++) {\n const ang = startAngle + i * stepAngle;\n const fx = cx + Math.cos(ang) * r;\n const fy = cy + Math.sin(ang) * r;\n const newIdx = verts.length / 2;\n verts.push(fx, fy);\n anchorA.push(cornerA); anchorB.push(cornerB); anchorT.push(cornerT);\n if (onPositive) idx.push(prevIdx, pivotIdx, newIdx);\n else idx.push(prevIdx, newIdx, pivotIdx);\n prevIdx = newIdx;\n }\n if (onPositive) idx.push(prevIdx, pivotIdx, bOuterStart);\n else idx.push(prevIdx, bOuterStart, pivotIdx);\n}\n\nfunction emitBevel(\n a: Seg, aOuterEnd: number, bOuterStart: number, onPositive: boolean,\n verts: number[], idx: number[],\n anchorA: number[], anchorB: number[], anchorT: number[],\n cornerA: number, cornerB: number, cornerT: number,\n): void {\n const jIdx = verts.length / 2;\n verts.push(a.bx, a.by);\n anchorA.push(cornerA); anchorB.push(cornerB); anchorT.push(cornerT);\n if (onPositive) idx.push(aOuterEnd, jIdx, bOuterStart);\n else idx.push(aOuterEnd, bOuterStart, jIdx);\n}\n\n/**\n * Solve for the intersection of line A (point ap, direction ad) with line B\n * (point bp, direction bd). Returns null if parallel or near-parallel.\n */\nfunction lineLineIntersect(\n apx: number, apy: number, adx: number, ady: number,\n bpx: number, bpy: number, bdx: number, bdy: number,\n): [number, number] | null {\n const denom = adx * bdy - ady * bdx;\n if (Math.abs(denom) < 1e-9) return null;\n const t = ((bpx - apx) * bdy - (bpy - apy) * bdx) / denom;\n return [apx + t * adx, apy + t * ady];\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/features/paths/tessellate/vertexWidths.ts
|
|
2
|
+
function e(e, n, r, i) {
|
|
3
|
+
let a = e.points.length / 2;
|
|
4
|
+
if (a === 0) return;
|
|
5
|
+
let o = e.anchorA ?? new Uint32Array(a), s = e.anchorB ?? new Uint32Array(a), c = e.anchorT ?? new Float32Array(a), l = new Float32Array(a), u = 1, d = (e) => {
|
|
6
|
+
let t = n[e];
|
|
7
|
+
return typeof t == "number" && isFinite(t) && t > 0 ? t * u : r;
|
|
8
|
+
};
|
|
9
|
+
for (let n = 0; n < a; n++) {
|
|
10
|
+
i && (u = t(e, n, i));
|
|
11
|
+
let r = o[n], a = s[n], f = c[n];
|
|
12
|
+
if (r === a) l[n] = d(r);
|
|
13
|
+
else {
|
|
14
|
+
let e = d(r), t = d(a);
|
|
15
|
+
l[n] = e + (t - e) * f;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
e.widths = l;
|
|
19
|
+
}
|
|
20
|
+
function t(e, t, n) {
|
|
21
|
+
let r = e.points, i = r.length / 2, a = 0, o = 0, s = (e, t) => {
|
|
22
|
+
let n = r[t * 2] - r[e * 2], i = r[t * 2 + 1] - r[e * 2 + 1], s = Math.hypot(n, i);
|
|
23
|
+
s > 0 && (a += n / s, o += i / s);
|
|
24
|
+
};
|
|
25
|
+
t > 0 ? s(t - 1, t) : e.closed && i > 1 && s(i - 1, 0), t < i - 1 ? s(t, t + 1) : e.closed && i > 1 && s(i - 1, 0);
|
|
26
|
+
let c = Math.hypot(a, o);
|
|
27
|
+
return c > 0 ? n(a / c, o / c) : 1;
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
export { e as populatePolylineWidths };
|
|
31
|
+
|
|
32
|
+
//# sourceMappingURL=vertexWidths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vertexWidths.js","names":["populatePolylineWidths","ptCount","pl","aA","aB","aT","widths","k","read","v","vertexWidths","i","fallbackWidth","scale","scaleAt","a","b","t","wa","wb","pts","n","tx","ty","add","dx","to","from","dy","len"],"sources":["../../../../src/features/paths/tessellate/vertexWidths.ts"],"sourcesContent":["import type { Polyline } from '@weasel-js/geom/tessellate';\n\n/** What one unit of a `vertexWidths` entry measures across the line at a\n * point whose unit tangent, in the polyline's own space, is `(tx, ty)`. */\nexport type WidthScale = (tx: number, ty: number) => number;\n\n/** Fill `pl.widths` by interpolating `vertexWidths` across each point's\n * (anchorA, anchorB, anchorT). Anchor-aligned points (A === B) read the\n * anchor's value directly. Out-of-range / non-finite anchor entries fall\n * back to `fallbackWidth`. Given `scale`, each entry is scaled at the point\n * that reads it, and the fallback is not. */\nexport function populatePolylineWidths(\n pl: Polyline,\n vertexWidths: number[],\n fallbackWidth: number,\n scale?: WidthScale,\n): void {\n const ptCount = pl.points.length / 2;\n if (ptCount === 0) return;\n const aA = pl.anchorA ?? new Uint32Array(ptCount);\n const aB = pl.anchorB ?? new Uint32Array(ptCount);\n const aT = pl.anchorT ?? new Float32Array(ptCount);\n const widths = new Float32Array(ptCount);\n let k = 1;\n const read = (i: number): number => {\n const v = vertexWidths[i];\n return typeof v === 'number' && isFinite(v) && v > 0 ? v * k : fallbackWidth;\n };\n for (let i = 0; i < ptCount; i++) {\n if (scale) k = scaleAt(pl, i, scale);\n const a = aA[i], b = aB[i], t = aT[i];\n if (a === b) {\n widths[i] = read(a);\n } else {\n const wa = read(a);\n const wb = read(b);\n widths[i] = wa + (wb - wa) * t;\n }\n }\n pl.widths = widths;\n}\n\n/** `scale` along the line's direction at point `i`: the bisector of the\n * segments either side, or the one segment at an open end. */\nfunction scaleAt(pl: Polyline, i: number, scale: WidthScale): number {\n const pts = pl.points;\n const n = pts.length / 2;\n let tx = 0, ty = 0;\n const add = (from: number, to: number) => {\n const dx = pts[to * 2] - pts[from * 2], dy = pts[to * 2 + 1] - pts[from * 2 + 1];\n const len = Math.hypot(dx, dy);\n if (len > 0) { tx += dx / len; ty += dy / len; }\n };\n if (i > 0) add(i - 1, i);\n else if (pl.closed && n > 1) add(n - 1, 0);\n if (i < n - 1) add(i, i + 1);\n else if (pl.closed && n > 1) add(n - 1, 0);\n const len = Math.hypot(tx, ty);\n return len > 0 ? scale(tx / len, ty / len) : 1;\n}\n"],"mappings":";AAWA,SAAgBA,EACd,GACA,GACA,GACA,GACM;CACN,IAAMC,IAAUC,EAAG,OAAO,SAAS;CACnC,IAAID,MAAY,GAAG;CACnB,IAAME,IAAKD,EAAG,WAAW,IAAI,YAAYD,CAAO,GAC1CG,IAAKF,EAAG,WAAW,IAAI,YAAYD,CAAO,GAC1CI,IAAKH,EAAG,WAAW,IAAI,aAAaD,CAAO,GAC3CK,IAAS,IAAI,aAAaL,CAAO,GACnCM,IAAI,GACFC,KAAQ,MAAsB;EAClC,IAAMC,IAAIC,EAAaC;EACvB,OAAO,OAAOF,KAAM,YAAY,SAASA,CAAC,KAAKA,IAAI,IAAIA,IAAIF,IAAIK;CACjE;CACA,KAAK,IAAID,IAAI,GAAGA,IAAIV,GAAS,KAAK;EAChC,AAAIY,MAAO,IAAIC,EAAQZ,GAAIS,GAAGE,CAAK;EACnC,IAAME,IAAIZ,EAAGQ,IAAIK,IAAIZ,EAAGO,IAAIM,IAAIZ,EAAGM;EACnC,IAAII,MAAMC,GACR,EAAOL,KAAKH,EAAKO,CAAC;OACb;GACL,IAAMG,IAAKV,EAAKO,CAAC,GACXI,IAAKX,EAAKQ,CAAC;GACjB,EAAOL,KAAKO,KAAMC,IAAKD,KAAMD;EAC/B;CACF;CACA,EAAG,SAASX;AACd;AAIA,SAASQ,EAAQ,GAAc,GAAW,GAA2B;CACnE,IAAMM,IAAMlB,EAAG,QACTmB,IAAID,EAAI,SAAS,GACnBE,IAAK,GAAGC,IAAK,GACXC,KAAO,GAAc,MAAe;EACxC,IAAMC,IAAKL,EAAIM,IAAK,KAAKN,EAAIO,IAAO,IAAIC,IAAKR,EAAIM,IAAK,IAAI,KAAKN,EAAIO,IAAO,IAAI,IACxEE,IAAM,KAAK,MAAMJ,GAAIG,CAAE;EAC7B,AAAIC,IAAM,MAAK,KAAMJ,IAAKI,GAAK,KAAMD,IAAKC;CAC5C;CAGA,AAFIlB,IAAI,IAAGa,EAAIb,IAAI,GAAGA,CAAC,IACdT,EAAG,UAAUmB,IAAI,KAAGG,EAAIH,IAAI,GAAG,CAAC,GACrCV,IAAIU,IAAI,IAAGG,EAAIb,GAAGA,IAAI,CAAC,IAClBT,EAAG,UAAUmB,IAAI,KAAGG,EAAIH,IAAI,GAAG,CAAC;CACzC,IAAMQ,IAAM,KAAK,MAAMP,GAAIC,CAAE;CAC7B,OAAOM,IAAM,IAAIhB,EAAMS,IAAKO,GAAKN,IAAKM,CAAG,IAAI;AAC/C"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext as e, useContext as t,
|
|
1
|
+
import { createContext as e, useContext as t, useState as n, useSyncExternalStore as r } from "react";
|
|
2
2
|
import { jsx as i } from "react/jsx-runtime";
|
|
3
3
|
//#region src/features/pointer/PointerContext.tsx
|
|
4
4
|
function a(e, t) {
|
|
@@ -25,7 +25,7 @@ function o() {
|
|
|
25
25
|
}
|
|
26
26
|
var s = e(null);
|
|
27
27
|
function c({ children: e, store: t }) {
|
|
28
|
-
let r = n(o
|
|
28
|
+
let [r] = n(o);
|
|
29
29
|
return /* @__PURE__ */ i(s.Provider, {
|
|
30
30
|
value: t ?? r,
|
|
31
31
|
children: e
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PointerContext.js","names":["samePos","a","b","createPointerStore","current","version","listeners","get","next","fn","PointerContext","createContext","PointerContextProvider","own","
|
|
1
|
+
{"version":3,"file":"PointerContext.js","names":["samePos","a","b","createPointerStore","current","version","listeners","get","next","fn","PointerContext","createContext","PointerContextProvider","own","useState","store","usePointerContext","useContext","noSubscribe","noPointer","usePointerPosition","useSyncExternalStore"],"sources":["../../../src/features/pointer/PointerContext.tsx"],"sourcesContent":["/**\n * @experimental\n * PointerContext — the world-space position of the canvas pointer, and the\n * view it is over, refreshed on every `pointermove` and cleared on\n * `pointerleave`.\n *\n * It is an external store rather than React state: cursor moves fire dozens of\n * times per second, and a consumer that only reads on demand (paste at the\n * pointer) must not re-render for each one. Readers that do need to follow it\n * — a linked cursor — subscribe, or use `usePointerPosition()`.\n *\n * `<SceneCanvas>` publishes automatically, and defers to a provider already in\n * scope, so several surfaces under one provider share one pointer: a detached\n * minimap and its main canvas each see where the other's pointer is.\n */\nimport {\n createContext, useContext, useState, useSyncExternalStore, type ReactNode,\n} from 'react';\n\n/** @experimental World-space pointer position and the view it is over — a\n * view id, or `null` for a surface's own camera. `null` as a whole when the\n * pointer is over no publishing surface. */\nexport type PointerWorldPos = { worldX: number; worldY: number; viewId: string | null } | null;\n\n/** @experimental */\nexport interface PointerContextValue {\n /** The latest position. */\n get(): PointerWorldPos;\n /** Publish a position. Subscribers hear only a changed value. */\n set(next: PointerWorldPos): void;\n /** Called after every change; returns the unsubscribe. */\n subscribe(fn: () => void): () => void;\n /** Bumps on every change. */\n getVersion(): number;\n /** `get()` as a stable thunk, for hooks taking a drop-point option. */\n readonly getDropPoint: () => PointerWorldPos;\n}\n\nfunction samePos(a: PointerWorldPos, b: PointerWorldPos): boolean {\n if (a === null || b === null) return a === b;\n return a.worldX === b.worldX && a.worldY === b.worldY && a.viewId === b.viewId;\n}\n\n/** @experimental A pointer store with no provider around it. */\nexport function createPointerStore(): PointerContextValue {\n let current: PointerWorldPos = null;\n let version = 0;\n const listeners = new Set<() => void>();\n const get = (): PointerWorldPos => current;\n return {\n get,\n set(next) {\n if (samePos(current, next)) return;\n current = next;\n version++;\n for (const fn of [...listeners]) fn();\n },\n subscribe(fn) {\n listeners.add(fn);\n return () => { listeners.delete(fn); };\n },\n getVersion: () => version,\n getDropPoint: get,\n };\n}\n\nconst PointerContext = createContext<PointerContextValue | null>(null);\n\n/**\n * @experimental\n * Wrap the part of the React tree that should share one pointer. Usually at\n * the app root, alongside `<ActionsProvider>` and `<SelectionContextProvider>`.\n *\n * Most consumers don't need to mount this directly — `<SceneCanvas>` mounts\n * one when none is in scope. Mount it yourself when two surfaces should share\n * a pointer. Pass `store` to publish into one you hold elsewhere\n * (`createPointerStore()`).\n */\nexport function PointerContextProvider(\n { children, store }: { children: ReactNode; store?: PointerContextValue },\n): ReactNode {\n const [own] = useState(createPointerStore);\n return <PointerContext.Provider value={store ?? own}>{children}</PointerContext.Provider>;\n}\n\n/** @experimental The surrounding pointer store, or `null` when no provider is\n * in scope. */\nexport function usePointerContext(): PointerContextValue | null {\n return useContext(PointerContext);\n}\n\nconst noSubscribe = (): (() => void) => () => {};\nconst noPointer = (): PointerWorldPos => null;\n\n/** @experimental The pointer, re-rendering the caller whenever it changes.\n * `null` outside a provider. */\nexport function usePointerPosition(): PointerWorldPos {\n const store = usePointerContext();\n return useSyncExternalStore(\n store ? store.subscribe : noSubscribe,\n store ? store.get : noPointer,\n store ? store.get : noPointer,\n );\n}\n"],"mappings":";;;AAsCA,SAASA,EAAQ,GAAoB,GAA6B;CAEhE,OADIC,MAAM,QAAQC,MAAM,OAAaD,MAAMC,IACpCD,EAAE,WAAWC,EAAE,UAAUD,EAAE,WAAWC,EAAE,UAAUD,EAAE,WAAWC,EAAE;AAC1E;AAGA,SAAgBC,IAA0C;CACxD,IAAIC,IAA2B,MAC3BC,IAAU,GACRC,oBAAY,IAAI,IAAgB,GAChCC,UAA6BH;CACnC,OAAO;EACL;EACA,IAAI,GAAM;GACR,IAAI,GAAQA,GAASI,CAAI,GAEzB;IADA,IAAUA,GACV;IACA,KAAK,IAAMC,KAAM,CAAC,GAAGH,CAAS,GAAG,EAAG;GADpC;EAEF;EACA,UAAU,GAAI;GAEZ,OADAA,EAAU,IAAIG,CAAE,SACH;IAAE,EAAU,OAAOA,CAAE;GAAG;EACvC;EACA,kBAAkBJ;EAClB,cAAcE;CAChB;AACF;AAEA,IAAMG,IAAiBC,EAA0C,IAAI;AAYrE,SAAgBC,EACd,EAAE,aAAU,YACD;CACX,IAAM,CAACC,KAAOC,EAASX,CAAkB;CACzC,OAAO,kBAACO,EAAe,UAAhB;EAAyB,OAAOK,KAASF;EAAM;CAAkC,CAAA;AAC1F;AAIA,SAAgBG,IAAgD;CAC9D,OAAOC,EAAWP,CAAc;AAClC;AAEA,IAAMQ,gBAAwC,CAAC,GACzCC,UAAmC;AAIzC,SAAgBC,IAAsC;CACpD,IAAML,IAAQC,EAAkB;CAChC,OAAOK,EACLN,IAAQA,EAAM,YAAYG,GAC1BH,IAAQA,EAAM,MAAMI,GACpBJ,IAAQA,EAAM,MAAMI,CACtB;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usePoseRun.js","names":["EMPTY","usePoseRun","optsRef","useLatest","opts","state","useRef","running","frame","mounted","loop","useVisibleRaf","syncState","useCallback","pinnedNow","pinned","id","overrides","entry","commit","scene","moved","previews","pose","dropPreviewOverrides","halt","tick","poses","syncPreviewOverrides","done","useEffect","useMemo"],"sources":["../../../src/features/poseRun/usePoseRun.ts"],"sourcesContent":["/**\n * A pose run: poses published per frame, committed once.\n *\n * The frames go to `scene.overrides` — the ephemeral table a drag publishes to,\n * which `effectivePose`, the derived-geometry lookup and the pick source\n * already read. So an edge follows a node the run is moving with nothing added,\n * and the whole run lands as one undo entry when it settles.\n *\n * It knows nothing about layout. Each frame it asks `step` for poses and\n * whether it is finished; a relaxation, an eased transition to a target, and a\n * scrubbed timeline are all the same shape to it.\n *\n * **A node another gesture owns is pinned, not driven.** The run publishes\n * entries it holds by reference, so an override it did not put there belongs to\n * somebody else — a drag, a resize. It never writes that id, never commits it,\n * and hands it to `step` as pinned so the producer can hold its neighbors\n * against it.\n */\nimport { useCallback, useEffect, useMemo, useRef } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { useVisibleRaf } from '../../scheduling/useVisibleRaf';\nimport {\n dropPreviewOverrides,\n syncPreviewOverrides,\n} from '../../interactions/actions/previewOverrides';\nimport type { NodeId, Scene } from '../../core/scene/types';\n\n/** What a producer is told before it is asked for a frame. */\nexport interface PoseRunCtx<TPose> {\n /** Nodes another gesture is moving, and the pose each is drawn at. The run\n * will not write these, so a producer holds them fixed. */\n pinned: ReadonlyMap<NodeId, TPose>;\n /** Frames this run has produced, starting at 0. */\n frame: number;\n}\n\n/** One frame of a run. */\nexport interface PoseRunStep<TPose> {\n /** Where the nodes it drives are this frame — the whole picture, not a\n * delta. An id the frame omits stops being published, which is how a node\n * leaves the run's hands: a producer that drops a grabbed box and its rows\n * from one frame has released them by the next. */\n poses: Iterable<readonly [NodeId, TPose]>;\n /** Whether the run is finished. The frame is published either way, and a\n * finished run commits. */\n done: boolean;\n}\n\n/** Options for `usePoseRun`. Read fresh each frame, so they may change mid-run. */\nexport interface UsePoseRunOptions<TPose> {\n scene: Scene<unknown, string, TPose>;\n /** Called once per frame while running, for that frame's poses. */\n step: (ctx: PoseRunCtx<TPose>) => PoseRunStep<TPose>;\n /** The undo entry's name. Default `'Layout'`. */\n label?: string;\n /** How many nodes the commit wrote. Fires after the ops land. */\n onCommit?: (moved: number) => void;\n /** Clock injection, for tests. Defaults to `requestAnimationFrame`. */\n requestFrame?: (cb: (t: number) => void) => number;\n cancelFrame?: (handle: number) => void;\n}\n\n/** Controls for a pose run. Stable for the life of the component. */\nexport interface PoseRun {\n /** Begin a new run, counting frames from 0. Idempotent while running. */\n start(): void;\n /** Finish now: commit what is published, then drop the overrides. */\n stop(): void;\n /** Abandon: drop the overrides and write nothing, like a canceled drag. */\n cancel(): void;\n isRunning(): boolean;\n}\n\nconst EMPTY: ReadonlyMap<NodeId, unknown> = new Map<NodeId, unknown>();\n\n/**\n * Drive node poses frame by frame through `scene.overrides`, committing them as\n * one undo entry when `step` reports done or `stop` is called. Idle until\n * `start`; unmounting abandons the run without writing.\n */\nexport function usePoseRun<TPose>(opts: UsePoseRunOptions<TPose>): PoseRun {\n const optsRef = useLatest(opts);\n\n /** This frame's poses, and the entries published for them. Held by\n * reference so a frame mutates in place rather than re-notifying per node. */\n const state = useRef({\n previews: new Map<NodeId, unknown>(),\n overrideEntries: new Map<NodeId, { pose: unknown }>(),\n });\n const running = useRef(false);\n const frame = useRef(0);\n const mounted = useRef(true);\n\n // The gate defaults an absent clock to `requestAnimationFrame` itself, so\n // these pass straight through rather than being defaulted twice.\n const loop = useVisibleRaf(() => { tick(); }, {\n requestFrame: opts.requestFrame,\n cancelFrame: opts.cancelFrame,\n });\n\n /** The scene as the previous frame left it, for `syncPreviewOverrides`. */\n const syncState = useCallback((): {\n scene: Scene<unknown, string, unknown>;\n previews: Map<NodeId, unknown>;\n overrideEntries: Map<NodeId, { pose: unknown }>;\n } => ({\n scene: optsRef.current.scene as Scene<unknown, string, unknown>,\n previews: state.current.previews,\n overrideEntries: state.current.overrideEntries,\n }), [optsRef]);\n\n /** Overrides somebody else published, and the pose each carries. */\n const pinnedNow = (): ReadonlyMap<NodeId, TPose> => {\n const { overrides } = optsRef.current.scene;\n let pinned: Map<NodeId, TPose> | undefined;\n for (const id of overrides.ids()) {\n const entry = overrides.get(id);\n if (entry === undefined || entry === state.current.overrideEntries.get(id)) continue;\n if (entry.pose === undefined) continue;\n (pinned ??= new Map()).set(id, entry.pose);\n }\n return pinned ?? (EMPTY as ReadonlyMap<NodeId, TPose>);\n };\n\n const commit = (): void => {\n const { previews } = state.current;\n const scene = optsRef.current.scene;\n let moved = 0;\n if (previews.size > 0) {\n scene.batch(optsRef.current.label ?? 'Layout', () => {\n for (const [id, pose] of previews) {\n if (scene.get(id) === undefined) continue;\n scene.setPose(id, pose as TPose);\n moved++;\n }\n });\n }\n // After the ops land: dropping first leaves a window in which the scene\n // answers with the pre-run pose.\n dropPreviewOverrides(syncState());\n previews.clear();\n optsRef.current.onCommit?.(moved);\n };\n\n const halt = (): void => {\n running.current = false;\n loop.cancel();\n };\n\n const tick = (): void => {\n if (!mounted.current || !running.current) return;\n const pinned = pinnedNow();\n const { poses, done } = optsRef.current.step({ pinned, frame: frame.current });\n frame.current++;\n // Each frame replaces the last: an id this one omits — a node just\n // grabbed, or a row under one — stops being published, rather than being\n // held at a pose from before the press while the gesture carries it\n // somewhere else.\n state.current.previews.clear();\n for (const [id, pose] of poses) {\n if (pinned.has(id)) continue;\n state.current.previews.set(id, pose);\n }\n syncPreviewOverrides(syncState());\n if (done) {\n halt();\n commit();\n return;\n }\n loop.request();\n };\n\n useEffect(() => {\n mounted.current = true;\n const { previews } = state.current;\n return () => {\n mounted.current = false;\n loop.cancel();\n // Teardown is not a decision: an unmounted run abandons its frames\n // rather than writing them.\n dropPreviewOverrides(syncState());\n previews.clear();\n };\n }, [loop, syncState]);\n\n return useMemo<PoseRun>(() => ({\n start() {\n if (running.current || !mounted.current) return;\n running.current = true;\n frame.current = 0;\n loop.request();\n },\n stop() {\n if (!running.current) return;\n halt();\n commit();\n },\n cancel() {\n halt();\n dropPreviewOverrides(syncState());\n state.current.previews.clear();\n },\n isRunning: () => running.current,\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }), [loop]);\n}\n"],"mappings":";;;;;AAyEA,IAAMA,oBAAsC,IAAI,IAAqB;AAOrE,SAAgBC,EAAkB,GAAyC;CACzE,IAAMC,IAAUC,EAAUC,CAAI,GAIxBC,IAAQC,EAAO;EACnB,0BAAU,IAAI,IAAqB;EACnC,iCAAiB,IAAI,IAA+B;CACtD,CAAC,GACKC,IAAUD,EAAO,EAAK,GACtBE,IAAQF,EAAO,CAAC,GAChBG,IAAUH,EAAO,EAAI,GAIrBI,IAAOC,QAAoB;EAAE,EAAK;CAAG,GAAG;EAC5C,cAAcP,EAAK;EACnB,aAAaA,EAAK;CACpB,CAAC,GAGKQ,IAAYC,SAIZ;EACJ,OAAOX,EAAQ,QAAQ;EACvB,UAAUG,EAAM,QAAQ;EACxB,iBAAiBA,EAAM,QAAQ;CACjC,IAAI,CAACH,CAAO,CAAC,GAGPY,UAA8C;EAClD,IAAM,EAAE,iBAAcZ,EAAQ,QAAQ,OAClCa;EACJ,KAAK,IAAMC,KAAMC,EAAU,IAAI,GAAG;GAChC,IAAMC,IAAQD,EAAU,IAAID,CAAE;GAC9B,AAAIE,MAAU,KAAA,KAAaA,MAAUb,EAAM,QAAQ,gBAAgB,IAAIW,CAAE,KACrEE,EAAM,SAAS,KAAA,MAClB,sBAAW,IAAI,IAAI,EAAA,CAAG,IAAIF,GAAIE,EAAM,IAAI;EAC3C;EACA,OAAOH,KAAWf;CACpB,GAEMmB,UAAqB;EACzB,IAAM,EAAE,gBAAad,EAAM,SACrBe,IAAQlB,EAAQ,QAAQ,OAC1BmB,IAAQ;EAcZ,AAbIC,EAAS,OAAO,KAClBF,EAAM,MAAMlB,EAAQ,QAAQ,SAAS,gBAAgB;GACnD,KAAK,IAAM,CAACc,GAAIO,MAASD,GACvB,AAAIF,EAAM,IAAIJ,CAAE,MAAM,KAAA,MACtBI,EAAM,QAAQJ,GAAIO,CAAa,GAC/B;EAEJ,CAAC,GAIHC,EAAqBZ,EAAU,CAAC,GAChCU,EAAS,MAAM,GACfpB,EAAQ,QAAQ,WAAWmB,CAAK;CAClC,GAEMI,UAAmB;EAEvB,AADA,EAAQ,UAAU,IAClBf,EAAK,OAAO;CACd,GAEMgB,UAAmB;EACvB,IAAI,CAACjB,EAAQ,WAAW,CAACF,EAAQ,SAAS;EAC1C,IAAMQ,IAASD,EAAU,GACnB,EAAE,UAAO,YAASZ,EAAQ,QAAQ,KAAK;GAAE;GAAQ,OAAOM,EAAM;EAAQ,CAAC;EAM7E,AALA,EAAM,WAKNH,EAAM,QAAQ,SAAS,MAAM;EAC7B,KAAK,IAAM,CAACW,GAAIO,MAASI,GACvB,AAAIZ,EAAO,IAAIC,CAAE,KACjBX,EAAM,QAAQ,SAAS,IAAIW,GAAIO,CAAI;EAGrC,IADAK,EAAqBhB,EAAU,CAAC,GAC5BiB,GAAM;GAER,AADAJ,EAAK,GACLN,EAAO;GACP;EACF;EACA,EAAK,QAAQ;CACf;CAeA,OAbAW,QAAgB;EACd,EAAQ,UAAU;EAClB,IAAM,EAAE,gBAAazB,EAAM;EAC3B,aAAa;GAMX,AALA,EAAQ,UAAU,IAClBK,EAAK,OAAO,GAGZc,EAAqBZ,EAAU,CAAC,GAChCU,EAAS,MAAM;EACjB;CACF,GAAG,CAACZ,GAAME,CAAS,CAAC,GAEbmB,SAAwB;EAC7B,QAAQ;GACN,AAAI,GAAQ,WAAYtB,EAAQ,YAChC,EAAQ,UAAU,IAClB,EAAM,UAAU,GAChBC,EAAK,QAAQ;EACf;EACA,OAAO;GACL,AAAKH,EAAQ,YACbkB,EAAK,GACLN,EAAO;EACT;EACA,SAAS;GAGP,AAFAM,EAAK,GACLD,EAAqBZ,EAAU,CAAC,GAChCP,EAAM,QAAQ,SAAS,MAAM;EAC/B;EACA,iBAAiBE,EAAQ;CAE3B,IAAI,CAACG,CAAI,CAAC;AACZ"}
|
|
1
|
+
{"version":3,"file":"usePoseRun.js","names":["EMPTY","usePoseRun","optsRef","useLatest","opts","state","useRef","running","frame","mounted","loop","useVisibleRaf","syncState","useCallback","pinnedNow","pinned","id","overrides","entry","commit","scene","moved","previews","pose","dropPreviewOverrides","halt","tick","poses","syncPreviewOverrides","done","useEffect","useMemo"],"sources":["../../../src/features/poseRun/usePoseRun.ts"],"sourcesContent":["/**\n * A pose run: poses published per frame, committed once.\n *\n * The frames go to `scene.overrides` — the ephemeral table a drag publishes to,\n * which `effectivePose`, the derived-geometry lookup and the pick source\n * already read. So an edge follows a node the run is moving with nothing added,\n * and the whole run lands as one undo entry when it settles.\n *\n * It knows nothing about layout. Each frame it asks `step` for poses and\n * whether it is finished; a relaxation, an eased transition to a target, and a\n * scrubbed timeline are all the same shape to it.\n *\n * **A node another gesture owns is pinned, not driven.** The run publishes\n * entries it holds by reference, so an override it did not put there belongs to\n * somebody else — a drag, a resize. It never writes that id, never commits it,\n * and hands it to `step` as pinned so the producer can hold its neighbors\n * against it.\n */\nimport { useCallback, useEffect, useMemo, useRef } from 'react';\nimport { useLatest } from '@weasel-js/react';\nimport { useVisibleRaf } from '../../scheduling/useVisibleRaf';\nimport {\n dropPreviewOverrides,\n syncPreviewOverrides,\n} from '../../interactions/actions/previewOverrides';\nimport type { NodeId, Scene } from '../../core/scene/types';\n\n/** What a producer is told before it is asked for a frame. */\nexport interface PoseRunCtx<TPose> {\n /** Nodes another gesture is moving, and the pose each is drawn at. The run\n * will not write these, so a producer holds them fixed. */\n pinned: ReadonlyMap<NodeId, TPose>;\n /** Frames this run has produced, starting at 0. */\n frame: number;\n}\n\n/** One frame of a run. */\nexport interface PoseRunStep<TPose> {\n /** Where the nodes it drives are this frame — the whole picture, not a\n * delta. An id the frame omits stops being published, which is how a node\n * leaves the run's hands: a producer that drops a grabbed box and its rows\n * from one frame has released them by the next. */\n poses: Iterable<readonly [NodeId, TPose]>;\n /** Whether the run is finished. The frame is published either way, and a\n * finished run commits. */\n done: boolean;\n}\n\n/** Options for `usePoseRun`. Read fresh each frame, so they may change mid-run. */\nexport interface UsePoseRunOptions<TPose> {\n scene: Scene<unknown, string, TPose>;\n /** Called once per frame while running, for that frame's poses. */\n step: (ctx: PoseRunCtx<TPose>) => PoseRunStep<TPose>;\n /** The undo entry's name. Default `'Layout'`. */\n label?: string;\n /** How many nodes the commit wrote. Fires after the ops land. */\n onCommit?: (moved: number) => void;\n /** Clock injection, for tests. Defaults to `requestAnimationFrame`. */\n requestFrame?: (cb: (t: number) => void) => number;\n cancelFrame?: (handle: number) => void;\n}\n\n/** Controls for a pose run. Stable for the life of the component. */\nexport interface PoseRun {\n /** Begin a new run, counting frames from 0. Idempotent while running. */\n start(): void;\n /** Finish now: commit what is published, then drop the overrides. */\n stop(): void;\n /** Abandon: drop the overrides and write nothing, like a canceled drag. */\n cancel(): void;\n isRunning(): boolean;\n}\n\nconst EMPTY: ReadonlyMap<NodeId, unknown> = new Map<NodeId, unknown>();\n\n/**\n * Drive node poses frame by frame through `scene.overrides`, committing them as\n * one undo entry when `step` reports done or `stop` is called. Idle until\n * `start`; unmounting abandons the run without writing.\n */\nexport function usePoseRun<TPose>(opts: UsePoseRunOptions<TPose>): PoseRun {\n const optsRef = useLatest(opts);\n\n /** This frame's poses, and the entries published for them. Held by\n * reference so a frame mutates in place rather than re-notifying per node. */\n const state = useRef({\n previews: new Map<NodeId, unknown>(),\n overrideEntries: new Map<NodeId, { pose: unknown }>(),\n });\n const running = useRef(false);\n const frame = useRef(0);\n const mounted = useRef(true);\n\n // The gate defaults an absent clock to `requestAnimationFrame` itself, so\n // these pass straight through rather than being defaulted twice.\n const loop = useVisibleRaf(() => { tick(); }, {\n requestFrame: opts.requestFrame,\n cancelFrame: opts.cancelFrame,\n });\n\n /** The scene as the previous frame left it, for `syncPreviewOverrides`. */\n const syncState = useCallback((): {\n scene: Scene<unknown, string, unknown>;\n previews: Map<NodeId, unknown>;\n overrideEntries: Map<NodeId, { pose: unknown }>;\n } => ({\n scene: optsRef.current.scene,\n previews: state.current.previews,\n overrideEntries: state.current.overrideEntries,\n }), [optsRef]);\n\n /** Overrides somebody else published, and the pose each carries. */\n const pinnedNow = (): ReadonlyMap<NodeId, TPose> => {\n const { overrides } = optsRef.current.scene;\n let pinned: Map<NodeId, TPose> | undefined;\n for (const id of overrides.ids()) {\n const entry = overrides.get(id);\n if (entry === undefined || entry === state.current.overrideEntries.get(id)) continue;\n if (entry.pose === undefined) continue;\n (pinned ??= new Map()).set(id, entry.pose);\n }\n return pinned ?? (EMPTY as ReadonlyMap<NodeId, TPose>);\n };\n\n const commit = (): void => {\n const { previews } = state.current;\n const scene = optsRef.current.scene;\n let moved = 0;\n if (previews.size > 0) {\n scene.batch(optsRef.current.label ?? 'Layout', () => {\n for (const [id, pose] of previews) {\n if (scene.get(id) === undefined) continue;\n scene.setPose(id, pose as TPose);\n moved++;\n }\n });\n }\n // After the ops land: dropping first leaves a window in which the scene\n // answers with the pre-run pose.\n dropPreviewOverrides(syncState());\n previews.clear();\n optsRef.current.onCommit?.(moved);\n };\n\n const halt = (): void => {\n running.current = false;\n loop.cancel();\n };\n\n const tick = (): void => {\n if (!mounted.current || !running.current) return;\n const pinned = pinnedNow();\n const { poses, done } = optsRef.current.step({ pinned, frame: frame.current });\n frame.current++;\n // Each frame replaces the last: an id this one omits — a node just\n // grabbed, or a row under one — stops being published, rather than being\n // held at a pose from before the press while the gesture carries it\n // somewhere else.\n state.current.previews.clear();\n for (const [id, pose] of poses) {\n if (pinned.has(id)) continue;\n state.current.previews.set(id, pose);\n }\n syncPreviewOverrides(syncState());\n if (done) {\n halt();\n commit();\n return;\n }\n loop.request();\n };\n\n useEffect(() => {\n mounted.current = true;\n const { previews } = state.current;\n return () => {\n mounted.current = false;\n loop.cancel();\n // Teardown is not a decision: an unmounted run abandons its frames\n // rather than writing them.\n dropPreviewOverrides(syncState());\n previews.clear();\n };\n }, [loop, syncState]);\n\n return useMemo<PoseRun>(() => ({\n start() {\n if (running.current || !mounted.current) return;\n running.current = true;\n frame.current = 0;\n loop.request();\n },\n stop() {\n if (!running.current) return;\n halt();\n commit();\n },\n cancel() {\n halt();\n dropPreviewOverrides(syncState());\n state.current.previews.clear();\n },\n isRunning: () => running.current,\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }), [loop]);\n}\n"],"mappings":";;;;;AAyEA,IAAMA,oBAAsC,IAAI,IAAqB;AAOrE,SAAgBC,EAAkB,GAAyC;CACzE,IAAMC,IAAUC,EAAUC,CAAI,GAIxBC,IAAQC,EAAO;EACnB,0BAAU,IAAI,IAAqB;EACnC,iCAAiB,IAAI,IAA+B;CACtD,CAAC,GACKC,IAAUD,EAAO,EAAK,GACtBE,IAAQF,EAAO,CAAC,GAChBG,IAAUH,EAAO,EAAI,GAIrBI,IAAOC,QAAoB;EAAE,EAAK;CAAG,GAAG;EAC5C,cAAcP,EAAK;EACnB,aAAaA,EAAK;CACpB,CAAC,GAGKQ,IAAYC,SAIZ;EACJ,OAAOX,EAAQ,QAAQ;EACvB,UAAUG,EAAM,QAAQ;EACxB,iBAAiBA,EAAM,QAAQ;CACjC,IAAI,CAACH,CAAO,CAAC,GAGPY,UAA8C;EAClD,IAAM,EAAE,iBAAcZ,EAAQ,QAAQ,OAClCa;EACJ,KAAK,IAAMC,KAAMC,EAAU,IAAI,GAAG;GAChC,IAAMC,IAAQD,EAAU,IAAID,CAAE;GAC9B,AAAIE,MAAU,KAAA,KAAaA,MAAUb,EAAM,QAAQ,gBAAgB,IAAIW,CAAE,KACrEE,EAAM,SAAS,KAAA,MAClB,sBAAW,IAAI,IAAI,EAAA,CAAG,IAAIF,GAAIE,EAAM,IAAI;EAC3C;EACA,OAAOH,KAAWf;CACpB,GAEMmB,UAAqB;EACzB,IAAM,EAAE,gBAAad,EAAM,SACrBe,IAAQlB,EAAQ,QAAQ,OAC1BmB,IAAQ;EAcZ,AAbIC,EAAS,OAAO,KAClBF,EAAM,MAAMlB,EAAQ,QAAQ,SAAS,gBAAgB;GACnD,KAAK,IAAM,CAACc,GAAIO,MAASD,GACvB,AAAIF,EAAM,IAAIJ,CAAE,MAAM,KAAA,MACtBI,EAAM,QAAQJ,GAAIO,CAAa,GAC/B;EAEJ,CAAC,GAIHC,EAAqBZ,EAAU,CAAC,GAChCU,EAAS,MAAM,GACfpB,EAAQ,QAAQ,WAAWmB,CAAK;CAClC,GAEMI,UAAmB;EAEvB,AADA,EAAQ,UAAU,IAClBf,EAAK,OAAO;CACd,GAEMgB,UAAmB;EACvB,IAAI,CAACjB,EAAQ,WAAW,CAACF,EAAQ,SAAS;EAC1C,IAAMQ,IAASD,EAAU,GACnB,EAAE,UAAO,YAASZ,EAAQ,QAAQ,KAAK;GAAE;GAAQ,OAAOM,EAAM;EAAQ,CAAC;EAM7E,AALA,EAAM,WAKNH,EAAM,QAAQ,SAAS,MAAM;EAC7B,KAAK,IAAM,CAACW,GAAIO,MAASI,GACvB,AAAIZ,EAAO,IAAIC,CAAE,KACjBX,EAAM,QAAQ,SAAS,IAAIW,GAAIO,CAAI;EAGrC,IADAK,EAAqBhB,EAAU,CAAC,GAC5BiB,GAAM;GAER,AADAJ,EAAK,GACLN,EAAO;GACP;EACF;EACA,EAAK,QAAQ;CACf;CAeA,OAbAW,QAAgB;EACd,EAAQ,UAAU;EAClB,IAAM,EAAE,gBAAazB,EAAM;EAC3B,aAAa;GAMX,AALA,EAAQ,UAAU,IAClBK,EAAK,OAAO,GAGZc,EAAqBZ,EAAU,CAAC,GAChCU,EAAS,MAAM;EACjB;CACF,GAAG,CAACZ,GAAME,CAAS,CAAC,GAEbmB,SAAwB;EAC7B,QAAQ;GACN,AAAI,GAAQ,WAAYtB,EAAQ,YAChC,EAAQ,UAAU,IAClB,EAAM,UAAU,GAChBC,EAAK,QAAQ;EACf;EACA,OAAO;GACL,AAAKH,EAAQ,YACbkB,EAAK,GACLN,EAAO;EACT;EACA,SAAS;GAGP,AAFAM,EAAK,GACLD,EAAqBZ,EAAU,CAAC,GAChCP,EAAM,QAAQ,SAAS,MAAM;EAC/B;EACA,iBAAiBE,EAAQ;CAE3B,IAAI,CAACG,CAAI,CAAC;AACZ"}
|