@trdrs/quickcharts 1.3.0 → 2.0.1
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 +94 -0
- package/README.md +209 -200
- package/THIRD-PARTY-NOTICES.md +4 -4
- package/dist/adapters/rest.d.ts +3 -3
- package/dist/adapters/rest.js +2 -2
- package/dist/adapters/rest.js.map +1 -1
- package/dist/{ar-G7MVXRE4.js → ar-KSXQ2USS.js} +20 -20
- package/dist/ar-KSXQ2USS.js.map +1 -0
- package/dist/{assets-ySndlUS3.d.ts → assets-DOuBBsOp.d.ts} +50 -50
- package/dist/{ca_ES-EUSRU4VG.js → ca_ES-JG4FM55E.js} +20 -20
- package/dist/ca_ES-JG4FM55E.js.map +1 -0
- package/dist/{chunk-KFBQSPNA.js → chunk-D3S26ARP.js} +2 -2
- package/dist/chunk-D3S26ARP.js.map +1 -0
- package/dist/{chunk-AKXIFHQN.js → chunk-KLPTCWA2.js} +1 -1
- package/dist/{chunk-AKXIFHQN.js.map → chunk-KLPTCWA2.js.map} +1 -1
- package/dist/{chunk-AA7XKXH4.js → chunk-YR7NT5ZW.js} +67 -33
- package/dist/chunk-YR7NT5ZW.js.map +1 -0
- package/dist/{de-2SVTG25S.js → de-VSW2LHNF.js} +20 -20
- package/dist/de-VSW2LHNF.js.map +1 -0
- package/dist/drawings.d.ts +16 -16
- package/dist/drawings.js +12 -12
- package/dist/{es-UDHZM4W7.js → es-5ZXIPVOB.js} +20 -20
- package/dist/es-5ZXIPVOB.js.map +1 -0
- package/dist/feature-manifest.json +3 -3
- package/dist/{fr-35VN4G4S.js → fr-4F6DAWGD.js} +20 -20
- package/dist/fr-4F6DAWGD.js.map +1 -0
- package/dist/{he_IL-JPZMCURP.js → he_IL-B7PBQUEV.js} +20 -20
- package/dist/he_IL-B7PBQUEV.js.map +1 -0
- package/dist/{id_ID-XSTFHHJQ.js → id_ID-CYJRMXPC.js} +20 -20
- package/dist/id_ID-CYJRMXPC.js.map +1 -0
- package/dist/index.d.ts +167 -163
- package/dist/index.js +342 -268
- package/dist/index.js.map +1 -1
- package/dist/{it-EIY6JESQ.js → it-KTX4SG5P.js} +20 -20
- package/dist/it-KTX4SG5P.js.map +1 -0
- package/dist/{ja-42N4XRCW.js → ja-V7OURU5D.js} +20 -20
- package/dist/ja-V7OURU5D.js.map +1 -0
- package/dist/{ko-3VRPTIP3.js → ko-2RNEBYVE.js} +20 -20
- package/dist/ko-2RNEBYVE.js.map +1 -0
- package/dist/{ms_MY-SKN7O6M5.js → ms_MY-EY235E7Q.js} +20 -20
- package/dist/ms_MY-EY235E7Q.js.map +1 -0
- package/dist/{pl-TYARHCLT.js → pl-ADNVRP55.js} +20 -20
- package/dist/pl-ADNVRP55.js.map +1 -0
- package/dist/{pt-72IIFLL4.js → pt-OL2DR3DO.js} +20 -20
- package/dist/pt-OL2DR3DO.js.map +1 -0
- package/dist/quickcharts.css +97 -96
- package/dist/{resources-Ch8eA1Mw.d.ts → resources-Le6flcEn.d.ts} +3 -3
- package/dist/rest-openapi.json +11 -11
- package/dist/{ru-TPH4F7IB.js → ru-V6YFEPAF.js} +20 -20
- package/dist/ru-V6YFEPAF.js.map +1 -0
- package/dist/{sv-DC5RS67Z.js → sv-TORXQVE7.js} +20 -20
- package/dist/sv-TORXQVE7.js.map +1 -0
- package/dist/{th-I6H6AOZ3.js → th-K6SJSYSY.js} +20 -20
- package/dist/th-K6SJSYSY.js.map +1 -0
- package/dist/theme-manifest.json +10 -10
- package/dist/{tr-7FSUVCNW.js → tr-4AEM66KW.js} +20 -20
- package/dist/tr-4AEM66KW.js.map +1 -0
- package/dist/{vi-DLRPHLLH.js → vi-YU5XCEUU.js} +20 -20
- package/dist/vi-YU5XCEUU.js.map +1 -0
- package/dist/{zh-ZXIQBBEP.js → zh-QXKRYHU5.js} +20 -20
- package/dist/zh-QXKRYHU5.js.map +1 -0
- package/dist/{zh_TW-TYNUJU7U.js → zh_TW-AYPDKG4Q.js} +20 -20
- package/dist/zh_TW-AYPDKG4Q.js.map +1 -0
- package/package.json +2 -2
- package/dist/ar-G7MVXRE4.js.map +0 -1
- package/dist/ca_ES-EUSRU4VG.js.map +0 -1
- package/dist/chunk-AA7XKXH4.js.map +0 -1
- package/dist/chunk-KFBQSPNA.js.map +0 -1
- package/dist/de-2SVTG25S.js.map +0 -1
- package/dist/es-UDHZM4W7.js.map +0 -1
- package/dist/fr-35VN4G4S.js.map +0 -1
- package/dist/he_IL-JPZMCURP.js.map +0 -1
- package/dist/id_ID-XSTFHHJQ.js.map +0 -1
- package/dist/it-EIY6JESQ.js.map +0 -1
- package/dist/ja-42N4XRCW.js.map +0 -1
- package/dist/ko-3VRPTIP3.js.map +0 -1
- package/dist/ms_MY-SKN7O6M5.js.map +0 -1
- package/dist/pl-TYARHCLT.js.map +0 -1
- package/dist/pt-72IIFLL4.js.map +0 -1
- package/dist/ru-TPH4F7IB.js.map +0 -1
- package/dist/sv-DC5RS67Z.js.map +0 -1
- package/dist/th-I6H6AOZ3.js.map +0 -1
- package/dist/tr-7FSUVCNW.js.map +0 -1
- package/dist/vi-DLRPHLLH.js.map +0 -1
- package/dist/zh-ZXIQBBEP.js.map +0 -1
- package/dist/zh_TW-TYNUJU7U.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
drawingContextKey
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KLPTCWA2.js";
|
|
4
4
|
|
|
5
5
|
// src/resources.ts
|
|
6
6
|
var ResourceAbortError = class extends Error {
|
|
@@ -104,4 +104,4 @@ export {
|
|
|
104
104
|
ResourceAbortError,
|
|
105
105
|
memorySaveLoadAdapter
|
|
106
106
|
};
|
|
107
|
-
//# sourceMappingURL=chunk-
|
|
107
|
+
//# sourceMappingURL=chunk-D3S26ARP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/resources.ts"],"sourcesContent":["// SAVED RESOURCES — the revisioned, async persistence contract for the entities a chart names and\n// a viewer can lose: saved charts, layouts, drawing documents, and templates.\n//\n// The contract exists because a saved chart is shared, mutable, remote state. Two tabs, two\n// devices, or one slow save and one fast one all end at the same question: whose version wins?\n// A store that answers \"the last write\" silently destroys work. So every read hands back a\n// `ResourceRef` (a stable id plus an opaque revision token), every write states the revision it\n// believes it is replacing, and a write against a stale revision comes back as a typed `conflict`\n// carrying the current ref. The chart never resolves that by overwriting; it surfaces it.\n//\n// Three rules the shape enforces:\n// - The revision is OPAQUE. It is a token the host minted and the chart only ever echoes back.\n// No ordering, no arithmetic, no timestamp parsing; a host is free to use an ETag, a counter,\n// or a content hash.\n// - Every call takes an `AbortSignal`. A symbol switch, a closed dialog, or a fast retype must be\n// able to abandon in-flight work, and a late answer to an abandoned ask must never land.\n// - A successful write RETURNS the stored revision, so the caller's next write is conditional on\n// what the store actually holds rather than on what it hoped it wrote.\n//\n// A chart's, a layout's and a template's body is contractually OPAQUE to the store: no parsing, no\n// per-entity logic, so the chart can evolve those formats freely. The drawings family is the one\n// deliberate exception: its body is the structured document in `drawings/document.ts`, because\n// deletion tombstones, ordered groups and per-drawing ownership have to survive a merge between two\n// surfaces, and a merge cannot be performed over a string. Each drawing's own state stays opaque\n// inside its entry.\n//\n// Flat viewer preferences that need no entity identity are NOT here; they stay on `ChartStorage`,\n// the small settings port beside this one.\nimport type { DrawingResourceContext, DrawingsBody } from './drawings/document'\nimport { drawingContextKey } from './drawings/document'\n\n/** A resource's identity plus the exact version this reader saw. A write quotes it back. */\nexport interface ResourceRef {\n id: string\n /** Opaque to the chart: minted by the store, compared only for equality. */\n revision: string\n}\n\n/** What a write did. `ok` carries the ref the store now holds (and, where the store computes one,\n * the resulting metadata); `conflict` carries the ref that beat this write, so a caller can reload\n * and re-decide; `not-found` means the id is gone. */\nexport type WriteOutcome<T = void> =\n | { kind: 'ok'; ref: ResourceRef; value?: T }\n | { kind: 'conflict'; current: ResourceRef }\n | { kind: 'not-found' }\n\n/** One family of resources. `Meta` is the listing row a picker shows without fetching content\n * (it carries the row's ref, so a picker can delete or open a row without a second read); `Body`\n * is the stored document. */\nexport interface ResourceStore<Meta, Body> {\n /** Listing rows, metadata only. */\n list(signal?: AbortSignal): Promise<Meta[]>\n /** The stored document and the revision it was read at, or null when the id is unknown. */\n load(id: string, signal?: AbortSignal): Promise<{ ref: ResourceRef; body: Body } | null>\n /** Store a new document. The store mints the id; a create conflicts only where the store keys a\n * document by identity and one already stands there (a drawings context that holds a document, a\n * name already taken), and then it answers the ref that stands. */\n create(body: Body, signal?: AbortSignal): Promise<WriteOutcome<Meta>>\n /** Replace the document at `ref.id`, but only while it still stands at `ref.revision`. */\n update(ref: ResourceRef, body: Body, signal?: AbortSignal): Promise<WriteOutcome<Meta>>\n /** Delete the document at `ref.id`, but only while it still stands at `ref.revision`. */\n remove(ref: ResourceRef, signal?: AbortSignal): Promise<WriteOutcome<void>>\n}\n\n/** A saved chart's listing row. */\nexport interface ChartMeta extends ResourceRef {\n name: string\n symbol: string\n timeframe: string\n /** Last save, ms since epoch (server-stamped by real backends). */\n updatedAt: number\n}\n\n/** A saved chart. `content` is the chart's own serialized state, opaque to the store. */\nexport interface ChartBody {\n name: string\n symbol: string\n timeframe: string\n content: string\n}\n\n/** A saved multi-chart layout's listing row. `symbol` is the active chart's market as the chart\n * writes it (`symbolNames(...).mark`, such as `BTCUSDC`) and `timeframe` its timeframe token, both\n * as they stood at the last save, so a listing can say what each layout shows without opening it. A\n * store that does not keep them, or a layout saved before they were stated, leaves them out. */\nexport interface LayoutMeta extends ResourceRef {\n name: string\n symbol?: string\n timeframe?: string\n updatedAt: number\n}\n\n/** A saved multi-chart layout. `content` is the arrangement's serialized state, opaque to the store.\n * The widget states the listing's `symbol` and `timeframe` on every save; they are display text and\n * play no part in restoring the layout. */\nexport interface LayoutBody {\n name: string\n symbol?: string\n timeframe?: string\n content: string\n}\n\n/** A drawings document's listing row. A context holds at most one, so `list` returns zero rows or\n * one, and the ref is the store's handle on it. */\nexport interface DrawingsMeta extends ResourceRef {\n updatedAt: number\n}\n\nexport type TemplateKind = 'indicator' | 'drawing' | 'palette'\n\n/** A named template's listing row. `tool` scopes DRAWING templates to their tool (a trend-line\n * template is meaningless on a rectangle); indicator and palette templates carry no tool. */\nexport interface TemplateMeta extends ResourceRef {\n name: string\n tool?: string\n updatedAt: number\n}\n\nexport interface TemplateBody {\n name: string\n tool?: string\n content: string\n}\n\n/** The adapter a host implements: four resource families over one revisioned contract. Viewer\n * settings are deliberately absent; they belong to `ChartStorage`. */\nexport interface ChartSaveLoadAdapter {\n charts: ResourceStore<ChartMeta, ChartBody>\n layouts: ResourceStore<LayoutMeta, LayoutBody>\n /** The store for one drawing-resource context. Calling with an equal context returns an\n * equivalent store; `drawingContextKey` is the canonical way to compare two. */\n drawings(context: DrawingResourceContext): ResourceStore<DrawingsMeta, DrawingsBody>\n templates(kind: TemplateKind): ResourceStore<TemplateMeta, TemplateBody>\n}\n\n/** The error every store rejects with when its signal aborts. `name` is 'AbortError', which is what\n * callers branch on, and what `DOMException` uses, so a host may reject with either. */\nexport class ResourceAbortError extends Error {\n override readonly name = 'AbortError'\n constructor(message = 'the resource operation was aborted') {\n super(message)\n }\n}\n\n/* ── The in-memory reference implementation ────────────────────────────────────────────────────\n The shape's own proof and the fixture every consumer test mounts: revisions on every write,\n conflicts on a stale ref, not-found on a vanished id, and an abort that lands before any state\n changes. It persists nothing, so it is also the right adapter for SSR, an ephemeral embed, and\n the clean-room consumer. */\n\ninterface Record_<Body> {\n id: string\n revision: string\n body: Body\n updatedAt: number\n}\n\n/** An aborted signal REJECTS the returned promise rather than throwing out of the call, so every\n * caller handles abandonment on one path. */\nconst aborted = (signal: AbortSignal | undefined): Promise<never> | null =>\n signal?.aborted ? Promise.reject(new ResourceAbortError()) : null\n\nconst newId = (): string =>\n typeof crypto !== 'undefined' && crypto.randomUUID ? crypto.randomUUID() : `r${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`\n\n/** Options for {@link memorySaveLoadAdapter}: a clock, so a test can pin `updatedAt`. */\nexport interface MemoryResourcesOptions {\n now?: () => number\n}\n\nfunction memoryStore<Meta, Body>(\n metaOf: (record: Record_<Body>) => Meta,\n now: () => number,\n): ResourceStore<Meta, Body> {\n const rows = new Map<string, Record_<Body>>()\n let counter = 0\n const nextRevision = (): string => `rev-${++counter}`\n const refOf = (record: Record_<Body>): ResourceRef => ({ id: record.id, revision: record.revision })\n\n return {\n list(signal) {\n return aborted(signal) ?? Promise.resolve([...rows.values()].map(metaOf))\n },\n load(id, signal) {\n const stop = aborted(signal)\n if (stop) return stop\n const record = rows.get(id)\n return Promise.resolve(record ? { ref: refOf(record), body: record.body } : null)\n },\n create(body, signal) {\n const stop = aborted(signal)\n if (stop) return stop\n const record: Record_<Body> = { id: newId(), revision: nextRevision(), body, updatedAt: now() }\n rows.set(record.id, record)\n const outcome: WriteOutcome<Meta> = { kind: 'ok', ref: refOf(record), value: metaOf(record) }\n return Promise.resolve(outcome)\n },\n update(ref, body, signal) {\n const stop = aborted(signal)\n if (stop) return stop\n const record = rows.get(ref.id)\n if (!record) return Promise.resolve<WriteOutcome<Meta>>({ kind: 'not-found' })\n if (record.revision !== ref.revision) return Promise.resolve<WriteOutcome<Meta>>({ kind: 'conflict', current: refOf(record) })\n record.body = body\n record.revision = nextRevision()\n record.updatedAt = now()\n const outcome: WriteOutcome<Meta> = { kind: 'ok', ref: refOf(record), value: metaOf(record) }\n return Promise.resolve(outcome)\n },\n remove(ref, signal) {\n const stop = aborted(signal)\n if (stop) return stop\n const record = rows.get(ref.id)\n if (!record) return Promise.resolve<WriteOutcome<void>>({ kind: 'not-found' })\n if (record.revision !== ref.revision) return Promise.resolve<WriteOutcome<void>>({ kind: 'conflict', current: refOf(record) })\n rows.delete(ref.id)\n return Promise.resolve<WriteOutcome<void>>({ kind: 'ok', ref: refOf(record) })\n },\n }\n}\n\n/** An in-memory {@link ChartSaveLoadAdapter}. Each drawing context and each template kind gets its\n * own store, created on first ask and kept, so two calls for one context see the same document. */\nexport function memorySaveLoadAdapter(options?: MemoryResourcesOptions): ChartSaveLoadAdapter {\n const now = options?.now ?? (() => Date.now())\n const drawingStores = new Map<string, ResourceStore<DrawingsMeta, DrawingsBody>>()\n const templateStores = new Map<TemplateKind, ResourceStore<TemplateMeta, TemplateBody>>()\n\n return {\n charts: memoryStore<ChartMeta, ChartBody>(\n (r) => ({ id: r.id, revision: r.revision, name: r.body.name, symbol: r.body.symbol, timeframe: r.body.timeframe, updatedAt: r.updatedAt }),\n now,\n ),\n layouts: memoryStore<LayoutMeta, LayoutBody>(\n (r) => ({\n id: r.id,\n revision: r.revision,\n name: r.body.name,\n ...(r.body.symbol === undefined ? {} : { symbol: r.body.symbol }),\n ...(r.body.timeframe === undefined ? {} : { timeframe: r.body.timeframe }),\n updatedAt: r.updatedAt,\n }),\n now,\n ),\n drawings(context) {\n const key = drawingContextKey(context)\n let store = drawingStores.get(key)\n if (!store) {\n store = memoryStore<DrawingsMeta, DrawingsBody>((r) => ({ id: r.id, revision: r.revision, updatedAt: r.updatedAt }), now)\n drawingStores.set(key, store)\n }\n return store\n },\n templates(kind) {\n let store = templateStores.get(kind)\n if (!store) {\n store = memoryStore<TemplateMeta, TemplateBody>(\n (r) => ({ id: r.id, revision: r.revision, name: r.body.name, ...(r.body.tool === undefined ? {} : { tool: r.body.tool }), updatedAt: r.updatedAt }),\n now,\n )\n templateStores.set(kind, store)\n }\n return store\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/drawings/document.ts"],"sourcesContent":["// The drawings DOCUMENT: the pure rules over the drawing-resource contract's document shape.\n//\n// Nothing here touches a chart, a store or a clock. That is deliberate: what a document means when\n// two surfaces write it, what a deletion does to it, and which of its entries are live are exactly\n// the questions that go wrong in a persistence layer, and they are all decidable from two values.\n//\n// Three rules the module enforces:\n// - A deletion is a TOMBSTONE, never an omission. Two charts hold the same document; if a\n// deletion were just a shorter list, the other chart's copy would put the row back on the next\n// merge, and a drawing the trader deleted would return after a save, a reconnect or a reload.\n// - A merge is not a last-write-wins overwrite. The stored document is the other surface's work\n// and wins by identity and by order; this surface's own additions follow it. Neither side\n// loses a row it made, and neither side resurrects a row the other buried.\n// - Deleting a GROUP deletes the grouping, not the drawings. The members survive, ungrouped, and\n// the group id can never come back.\n//\n// The document's own SHAPE lives here too, beside the rules, because the two only make sense\n// together: the resource contract in `resources.ts` names this the drawings family's body.\n\n/* ── The drawing-resource context ───────────────────────────────────────────────────────────────\n Where a drawings document lives, as a discriminated, versioned context rather than a pair of\n loose fields. The kind IS the sharing rule, so a reader never infers one from which fields\n happen to be set:\n\n chart-local one chart of one layout, on one symbol. Nothing is shared.\n layout-shared every chart of one layout, on one symbol.\n symbol-global every chart the host mounts, on one symbol.\n\n `version` is the context's own format, not the document's: a store keys its rows by the whole\n context, so a later kind or field arrives as a different key rather than as a silently widened\n old one. */\n\nexport const DRAWING_CONTEXT_VERSION = 1\n\nexport type DrawingContextKind = 'chart-local' | 'layout-shared' | 'symbol-global'\n\n/** One chart of one layout, on one symbol. */\nexport interface ChartLocalDrawingContext {\n version: typeof DRAWING_CONTEXT_VERSION\n kind: 'chart-local'\n /** The host's stable identity for the layout the chart is tiled in. */\n layoutId: string\n /** The chart's stable identity WITHIN that layout, so the document is found again after a\n * reload; a per-mount instance id would key a document nothing could ever read back. */\n chartId: string\n symbol: string\n}\n\n/** Every chart of one layout, on one symbol. */\nexport interface LayoutSharedDrawingContext {\n version: typeof DRAWING_CONTEXT_VERSION\n kind: 'layout-shared'\n layoutId: string\n symbol: string\n}\n\n/** Every chart the host mounts, on one symbol. */\nexport interface SymbolGlobalDrawingContext {\n version: typeof DRAWING_CONTEXT_VERSION\n kind: 'symbol-global'\n symbol: string\n}\n\nexport type DrawingResourceContext = ChartLocalDrawingContext | LayoutSharedDrawingContext | SymbolGlobalDrawingContext\n\n/** The drawings document format. Bumping this is the only reason a reader ever branches. */\nexport const DRAWING_DOCUMENT_VERSION = 1\n\n/** One drawing in a document. The ENVELOPE is structured so a restore is validated without parsing\n * the drawing itself: the id is stable for the drawing's life, `source` names what owns it (the\n * main series, or an indicator's own source), `pane` names where it is drawn, and `type` is its\n * tool. `state` is the drawing's own serialized form and is opaque to the document, the store and\n * the transport. */\nexport interface DrawingEntry {\n id: string\n /** The owning source: `main` for the chart's main series, else an indicator instance's id. */\n source: string\n /** The pane the drawing is drawn in: `main` for the price pane, else the pane owner's id. */\n pane: string\n type: string\n /** Opaque: nothing between the layer and the backend reads it. */\n state: unknown\n /** The group this drawing belongs to, when it belongs to one. */\n group?: string\n}\n\n/** A group of drawings. Membership is ORDERED and holds drawing ids, so a group survives a round\n * trip with its order intact rather than being rebuilt from however a document happened to list\n * its entries. */\nexport interface DrawingGroup {\n id: string\n name?: string\n members: readonly string[]\n}\n\n/** A deletion, kept. A document that listed only what survives lets a concurrent save, a reconnect\n * or a reload resurrect what the trader deleted: the other side still holds the row and a merge\n * takes it back. A tombstone says the deletion happened, and at which document revision, so the\n * merge drops it instead. */\nexport interface DrawingTombstone {\n kind: 'drawing' | 'group'\n id: string\n /** The document revision the deletion was stamped at. */\n at: number\n}\n\n/** One context's drawings document. `revision` is the DOCUMENT's own counter, which orders\n * tombstones and detects a stale apply; it is not the store's `ResourceRef.revision`, which stays\n * opaque and is the store's alone. */\nexport interface DrawingsBody {\n version: typeof DRAWING_DOCUMENT_VERSION\n context: DrawingResourceContext\n revision: number\n entries: readonly DrawingEntry[]\n groups: readonly DrawingGroup[]\n tombstones: readonly DrawingTombstone[]\n}\n\n\n/** The canonical key for a drawing-resource context: equal contexts key equally, and no two\n * different contexts collide. The parts join on an escaped null, which no symbol, layout id or\n * chart id carries, so a symbol ending in a separator cannot spell another context's key. The\n * kind and version lead, so a later kind is a different key rather than a widened old one. */\nexport function drawingContextKey(context: DrawingResourceContext): string {\n const parts: string[] = [String(context.version), context.kind]\n if (context.kind === 'chart-local') parts.push(context.layoutId, context.chartId)\n else if (context.kind === 'layout-shared') parts.push(context.layoutId)\n parts.push(context.symbol)\n return parts.join('\\u0000')\n}\n\n/** Whether two contexts name the same document. */\nexport const sameDrawingContext = (a: DrawingResourceContext, b: DrawingResourceContext): boolean => drawingContextKey(a) === drawingContextKey(b)\n\n/** A document with nothing in it, at revision 0: what a context that has never been written reads\n * as, so every caller works over a document rather than over a null. */\nexport function emptyDrawingDocument(context: DrawingResourceContext): DrawingsBody {\n return { version: DRAWING_DOCUMENT_VERSION, context, revision: 0, entries: [], groups: [], tombstones: [] }\n}\n\nconst tombstoneKey = (kind: DrawingTombstone['kind'], id: string): string => `${kind}\\u0000${id}`\n\n/** The tombstones of both documents, one per (kind, id), each at the later revision it was seen at. */\nfunction mergeTombstones(a: readonly DrawingTombstone[], b: readonly DrawingTombstone[]): DrawingTombstone[] {\n const byKey = new Map<string, DrawingTombstone>()\n for (const stone of [...a, ...b]) {\n const key = tombstoneKey(stone.kind, stone.id)\n const held = byKey.get(key)\n if (!held || stone.at > held.at) byKey.set(key, { kind: stone.kind, id: stone.id, at: stone.at })\n }\n return [...byKey.values()]\n}\n\nconst buriedSet = (tombstones: readonly DrawingTombstone[], kind: DrawingTombstone['kind']): Set<string> =>\n new Set(tombstones.filter((stone) => stone.kind === kind).map((stone) => stone.id))\n\n/** Whether a document buries this id. Package-internal, beside the rules it serves: the layer asks\n * it to tell a live entry from one whose group is gone. */\nexport const drawingBuried = (document: DrawingsBody, kind: DrawingTombstone['kind'], id: string): boolean =>\n document.tombstones.some((stone) => stone.kind === kind && stone.id === id)\n\n/** The entries a document actually holds: nothing buried, no duplicate id, and no membership of a\n * group that is gone (the drawing survives, its grouping does not). */\nexport function liveDrawingEntries(document: DrawingsBody): DrawingEntry[] {\n const buriedDrawings = buriedSet(document.tombstones, 'drawing')\n const groups = new Set(liveDrawingGroups(document).map((group) => group.id))\n const out: DrawingEntry[] = []\n const seen = new Set<string>()\n for (const entry of document.entries) {\n if (buriedDrawings.has(entry.id) || seen.has(entry.id)) continue\n seen.add(entry.id)\n out.push(entry.group !== undefined && !groups.has(entry.group) ? omitGroup(entry) : entry)\n }\n return out\n}\n\nconst omitGroup = (entry: DrawingEntry): DrawingEntry => {\n const { group: _buried, ...rest } = entry\n return rest\n}\n\n/** The groups a document actually holds: nothing buried, no duplicate id, and no member that is\n * buried or absent, so a group never names a drawing that is not there. */\nexport function liveDrawingGroups(document: DrawingsBody): DrawingGroup[] {\n const buriedGroups = buriedSet(document.tombstones, 'group')\n const buriedDrawings = buriedSet(document.tombstones, 'drawing')\n const present = new Set(document.entries.filter((entry) => !buriedDrawings.has(entry.id)).map((entry) => entry.id))\n const out: DrawingGroup[] = []\n const seen = new Set<string>()\n for (const group of document.groups) {\n if (buriedGroups.has(group.id) || seen.has(group.id)) continue\n seen.add(group.id)\n out.push({ ...group, members: group.members.filter((id) => present.has(id)) })\n }\n return out\n}\n\n/** Replace what a document holds and stamp the difference. Every id the document held and the next\n * state does not is buried at the new revision; every id that arrives is kept in the order it\n * arrives in. One verb, one revision bump, so a save can never write entries at one revision and\n * groups at another. */\nexport function reviseDrawingDocument(\n document: DrawingsBody,\n next: { entries?: readonly DrawingEntry[]; groups?: readonly DrawingGroup[] },\n): DrawingsBody {\n const revision = document.revision + 1\n const entries = next.entries ?? liveDrawingEntries(document)\n const groups = next.groups ?? liveDrawingGroups(document)\n const keptDrawings = new Set(entries.map((entry) => entry.id))\n const keptGroups = new Set(groups.map((group) => group.id))\n const buried: DrawingTombstone[] = []\n for (const entry of liveDrawingEntries(document)) if (!keptDrawings.has(entry.id)) buried.push({ kind: 'drawing', id: entry.id, at: revision })\n for (const group of liveDrawingGroups(document)) if (!keptGroups.has(group.id)) buried.push({ kind: 'group', id: group.id, at: revision })\n return {\n version: DRAWING_DOCUMENT_VERSION,\n context: document.context,\n revision,\n entries: [...entries],\n groups: [...groups],\n tombstones: mergeTombstones(document.tombstones, buried),\n }\n}\n\n/** Merge the document another surface wrote first with this surface's own. The stored rows win by\n * identity and keep the stored order, because they are what everyone else can already see; rows\n * only this surface holds follow, because they are work the other surface never saw. Every\n * deletion either side made survives the merge. The result's revision is past both, so the write\n * that carries it is the newer document by either side's counter. */\nexport function mergeDrawingDocuments(stored: DrawingsBody, mine: DrawingsBody): DrawingsBody {\n const tombstones = mergeTombstones(stored.tombstones, mine.tombstones)\n const buriedDrawings = buriedSet(tombstones, 'drawing')\n const buriedGroups = buriedSet(tombstones, 'group')\n\n const entries: DrawingEntry[] = []\n const seen = new Set<string>()\n for (const entry of [...stored.entries, ...mine.entries]) {\n if (seen.has(entry.id) || buriedDrawings.has(entry.id)) continue\n seen.add(entry.id)\n entries.push(entry.group !== undefined && buriedGroups.has(entry.group) ? omitGroup(entry) : entry)\n }\n\n const groups: DrawingGroup[] = []\n const byId = new Map<string, DrawingGroup>()\n for (const group of [...stored.groups, ...mine.groups]) {\n if (buriedGroups.has(group.id)) continue\n const held = byId.get(group.id)\n if (!held) {\n const row = { ...group, members: [...group.members] }\n byId.set(group.id, row)\n groups.push(row)\n continue\n }\n // One group written by both surfaces: the stored order stands and the members only this\n // surface knows about are appended, the same rule the entries follow.\n for (const id of group.members) if (!held.members.includes(id)) (held.members as string[]).push(id)\n }\n const present = new Set(entries.map((entry) => entry.id))\n\n return {\n version: DRAWING_DOCUMENT_VERSION,\n context: stored.context,\n revision: Math.max(stored.revision, mine.revision) + 1,\n entries,\n groups: groups.map((group) => ({ ...group, members: group.members.filter((id) => present.has(id)) })),\n tombstones,\n }\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null\n\nconst entryOf = (value: unknown): DrawingEntry | null => {\n if (!isRecord(value)) return null\n const { id, source, pane, type, group } = value\n if (typeof id !== 'string' || !id || typeof source !== 'string' || typeof pane !== 'string' || typeof type !== 'string') return null\n return { id, source, pane, type, state: value.state, ...(typeof group === 'string' ? { group } : {}) }\n}\n\nconst groupOf = (value: unknown): DrawingGroup | null => {\n if (!isRecord(value)) return null\n const { id, name, members } = value\n if (typeof id !== 'string' || !id || !Array.isArray(members)) return null\n return { id, ...(typeof name === 'string' ? { name } : {}), members: members.filter((m): m is string => typeof m === 'string') }\n}\n\n/** The context a payload states, when it states a whole one this build understands. */\nconst contextOf = (value: unknown): DrawingResourceContext | null => {\n if (!isRecord(value) || value.version !== DRAWING_CONTEXT_VERSION) return null\n const { kind, symbol, layoutId, chartId } = value\n if (typeof symbol !== 'string') return null\n if (kind === 'symbol-global') return { version: DRAWING_CONTEXT_VERSION, kind, symbol }\n if (typeof layoutId !== 'string') return null\n if (kind === 'layout-shared') return { version: DRAWING_CONTEXT_VERSION, kind, layoutId, symbol }\n if (kind === 'chart-local' && typeof chartId === 'string') return { version: DRAWING_CONTEXT_VERSION, kind, layoutId, chartId, symbol }\n return null\n}\n\nconst tombstoneOf = (value: unknown): DrawingTombstone | null => {\n if (!isRecord(value)) return null\n const { kind, id, at } = value\n if ((kind !== 'drawing' && kind !== 'group') || typeof id !== 'string' || !id) return null\n return { kind, id, at: typeof at === 'number' ? at : 0 }\n}\n\n/** Read whatever a transport handed back as a document for this context. TOTAL: a null, a string, a\n * wrong version, a junk row or a whole junk body reads as the empty document, never a throw, so a\n * host can hand the chart what its backend returned without guarding it first. The context is the\n * caller's, not the payload's: a document is identified by where it was read from, and a body\n * claiming another context is not merged into this one. */\nexport function parseDrawingDocument(value: unknown, context: DrawingResourceContext): DrawingsBody {\n const raw: unknown = typeof value === 'string' ? safeJson(value) : value\n if (!isRecord(raw) || raw.version !== DRAWING_DOCUMENT_VERSION) return emptyDrawingDocument(context)\n const stated = contextOf(raw.context)\n if (stated && !sameDrawingContext(stated, context)) return emptyDrawingDocument(context)\n return {\n version: DRAWING_DOCUMENT_VERSION,\n context,\n revision: typeof raw.revision === 'number' && Number.isFinite(raw.revision) ? raw.revision : 0,\n entries: Array.isArray(raw.entries) ? raw.entries.map(entryOf).filter((e): e is DrawingEntry => e !== null) : [],\n groups: Array.isArray(raw.groups) ? raw.groups.map(groupOf).filter((g): g is DrawingGroup => g !== null) : [],\n tombstones: Array.isArray(raw.tombstones) ? raw.tombstones.map(tombstoneOf).filter((t): t is DrawingTombstone => t !== null) : [],\n }\n}\n\nconst safeJson = (text: string): unknown => {\n try {\n return JSON.parse(text)\n } catch {\n return null\n }\n}\n"],"mappings":";AAgCO,IAAM,0BAA0B;AAkChC,IAAM,2BAA2B;AAyDjC,SAAS,kBAAkB,SAAyC;AACzE,QAAM,QAAkB,CAAC,OAAO,QAAQ,OAAO,GAAG,QAAQ,IAAI;AAC9D,MAAI,QAAQ,SAAS,cAAe,OAAM,KAAK,QAAQ,UAAU,QAAQ,OAAO;AAAA,WACvE,QAAQ,SAAS,gBAAiB,OAAM,KAAK,QAAQ,QAAQ;AACtE,QAAM,KAAK,QAAQ,MAAM;AACzB,SAAO,MAAM,KAAK,IAAQ;AAC5B;AAGO,IAAM,qBAAqB,CAAC,GAA2B,MAAuC,kBAAkB,CAAC,MAAM,kBAAkB,CAAC;AAI1I,SAAS,qBAAqB,SAA+C;AAClF,SAAO,EAAE,SAAS,0BAA0B,SAAS,UAAU,GAAG,SAAS,CAAC,GAAG,QAAQ,CAAC,GAAG,YAAY,CAAC,EAAE;AAC5G;AAEA,IAAM,eAAe,CAAC,MAAgC,OAAuB,GAAG,IAAI,KAAS,EAAE;AAG/F,SAAS,gBAAgB,GAAgC,GAAoD;AAC3G,QAAM,QAAQ,oBAAI,IAA8B;AAChD,aAAW,SAAS,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG;AAChC,UAAM,MAAM,aAAa,MAAM,MAAM,MAAM,EAAE;AAC7C,UAAM,OAAO,MAAM,IAAI,GAAG;AAC1B,QAAI,CAAC,QAAQ,MAAM,KAAK,KAAK,GAAI,OAAM,IAAI,KAAK,EAAE,MAAM,MAAM,MAAM,IAAI,MAAM,IAAI,IAAI,MAAM,GAAG,CAAC;AAAA,EAClG;AACA,SAAO,CAAC,GAAG,MAAM,OAAO,CAAC;AAC3B;AAEA,IAAM,YAAY,CAAC,YAAyC,SAC1D,IAAI,IAAI,WAAW,OAAO,CAAC,UAAU,MAAM,SAAS,IAAI,EAAE,IAAI,CAAC,UAAU,MAAM,EAAE,CAAC;AAI7E,IAAM,gBAAgB,CAAC,UAAwB,MAAgC,OACpF,SAAS,WAAW,KAAK,CAAC,UAAU,MAAM,SAAS,QAAQ,MAAM,OAAO,EAAE;AAIrE,SAAS,mBAAmB,UAAwC;AACzE,QAAM,iBAAiB,UAAU,SAAS,YAAY,SAAS;AAC/D,QAAM,SAAS,IAAI,IAAI,kBAAkB,QAAQ,EAAE,IAAI,CAAC,UAAU,MAAM,EAAE,CAAC;AAC3E,QAAM,MAAsB,CAAC;AAC7B,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,SAAS,SAAS,SAAS;AACpC,QAAI,eAAe,IAAI,MAAM,EAAE,KAAK,KAAK,IAAI,MAAM,EAAE,EAAG;AACxD,SAAK,IAAI,MAAM,EAAE;AACjB,QAAI,KAAK,MAAM,UAAU,UAAa,CAAC,OAAO,IAAI,MAAM,KAAK,IAAI,UAAU,KAAK,IAAI,KAAK;AAAA,EAC3F;AACA,SAAO;AACT;AAEA,IAAM,YAAY,CAAC,UAAsC;AACvD,QAAM,EAAE,OAAO,SAAS,GAAG,KAAK,IAAI;AACpC,SAAO;AACT;AAIO,SAAS,kBAAkB,UAAwC;AACxE,QAAM,eAAe,UAAU,SAAS,YAAY,OAAO;AAC3D,QAAM,iBAAiB,UAAU,SAAS,YAAY,SAAS;AAC/D,QAAM,UAAU,IAAI,IAAI,SAAS,QAAQ,OAAO,CAAC,UAAU,CAAC,eAAe,IAAI,MAAM,EAAE,CAAC,EAAE,IAAI,CAAC,UAAU,MAAM,EAAE,CAAC;AAClH,QAAM,MAAsB,CAAC;AAC7B,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,SAAS,SAAS,QAAQ;AACnC,QAAI,aAAa,IAAI,MAAM,EAAE,KAAK,KAAK,IAAI,MAAM,EAAE,EAAG;AACtD,SAAK,IAAI,MAAM,EAAE;AACjB,QAAI,KAAK,EAAE,GAAG,OAAO,SAAS,MAAM,QAAQ,OAAO,CAAC,OAAO,QAAQ,IAAI,EAAE,CAAC,EAAE,CAAC;AAAA,EAC/E;AACA,SAAO;AACT;AAMO,SAAS,sBACd,UACA,MACc;AACd,QAAM,WAAW,SAAS,WAAW;AACrC,QAAM,UAAU,KAAK,WAAW,mBAAmB,QAAQ;AAC3D,QAAM,SAAS,KAAK,UAAU,kBAAkB,QAAQ;AACxD,QAAM,eAAe,IAAI,IAAI,QAAQ,IAAI,CAAC,UAAU,MAAM,EAAE,CAAC;AAC7D,QAAM,aAAa,IAAI,IAAI,OAAO,IAAI,CAAC,UAAU,MAAM,EAAE,CAAC;AAC1D,QAAM,SAA6B,CAAC;AACpC,aAAW,SAAS,mBAAmB,QAAQ,EAAG,KAAI,CAAC,aAAa,IAAI,MAAM,EAAE,EAAG,QAAO,KAAK,EAAE,MAAM,WAAW,IAAI,MAAM,IAAI,IAAI,SAAS,CAAC;AAC9I,aAAW,SAAS,kBAAkB,QAAQ,EAAG,KAAI,CAAC,WAAW,IAAI,MAAM,EAAE,EAAG,QAAO,KAAK,EAAE,MAAM,SAAS,IAAI,MAAM,IAAI,IAAI,SAAS,CAAC;AACzI,SAAO;AAAA,IACL,SAAS;AAAA,IACT,SAAS,SAAS;AAAA,IAClB;AAAA,IACA,SAAS,CAAC,GAAG,OAAO;AAAA,IACpB,QAAQ,CAAC,GAAG,MAAM;AAAA,IAClB,YAAY,gBAAgB,SAAS,YAAY,MAAM;AAAA,EACzD;AACF;AAOO,SAAS,sBAAsB,QAAsB,MAAkC;AAC5F,QAAM,aAAa,gBAAgB,OAAO,YAAY,KAAK,UAAU;AACrE,QAAM,iBAAiB,UAAU,YAAY,SAAS;AACtD,QAAM,eAAe,UAAU,YAAY,OAAO;AAElD,QAAM,UAA0B,CAAC;AACjC,QAAM,OAAO,oBAAI,IAAY;AAC7B,aAAW,SAAS,CAAC,GAAG,OAAO,SAAS,GAAG,KAAK,OAAO,GAAG;AACxD,QAAI,KAAK,IAAI,MAAM,EAAE,KAAK,eAAe,IAAI,MAAM,EAAE,EAAG;AACxD,SAAK,IAAI,MAAM,EAAE;AACjB,YAAQ,KAAK,MAAM,UAAU,UAAa,aAAa,IAAI,MAAM,KAAK,IAAI,UAAU,KAAK,IAAI,KAAK;AAAA,EACpG;AAEA,QAAM,SAAyB,CAAC;AAChC,QAAM,OAAO,oBAAI,IAA0B;AAC3C,aAAW,SAAS,CAAC,GAAG,OAAO,QAAQ,GAAG,KAAK,MAAM,GAAG;AACtD,QAAI,aAAa,IAAI,MAAM,EAAE,EAAG;AAChC,UAAM,OAAO,KAAK,IAAI,MAAM,EAAE;AAC9B,QAAI,CAAC,MAAM;AACT,YAAM,MAAM,EAAE,GAAG,OAAO,SAAS,CAAC,GAAG,MAAM,OAAO,EAAE;AACpD,WAAK,IAAI,MAAM,IAAI,GAAG;AACtB,aAAO,KAAK,GAAG;AACf;AAAA,IACF;AAGA,eAAW,MAAM,MAAM,QAAS,KAAI,CAAC,KAAK,QAAQ,SAAS,EAAE,EAAG,CAAC,KAAK,QAAqB,KAAK,EAAE;AAAA,EACpG;AACA,QAAM,UAAU,IAAI,IAAI,QAAQ,IAAI,CAAC,UAAU,MAAM,EAAE,CAAC;AAExD,SAAO;AAAA,IACL,SAAS;AAAA,IACT,SAAS,OAAO;AAAA,IAChB,UAAU,KAAK,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI;AAAA,IACrD;AAAA,IACA,QAAQ,OAAO,IAAI,CAAC,WAAW,EAAE,GAAG,OAAO,SAAS,MAAM,QAAQ,OAAO,CAAC,OAAO,QAAQ,IAAI,EAAE,CAAC,EAAE,EAAE;AAAA,IACpG;AAAA,EACF;AACF;AAEA,IAAM,WAAW,CAAC,UAAqD,OAAO,UAAU,YAAY,UAAU;AAE9G,IAAM,UAAU,CAAC,UAAwC;AACvD,MAAI,CAAC,SAAS,KAAK,EAAG,QAAO;AAC7B,QAAM,EAAE,IAAI,QAAQ,MAAM,MAAM,MAAM,IAAI;AAC1C,MAAI,OAAO,OAAO,YAAY,CAAC,MAAM,OAAO,WAAW,YAAY,OAAO,SAAS,YAAY,OAAO,SAAS,SAAU,QAAO;AAChI,SAAO,EAAE,IAAI,QAAQ,MAAM,MAAM,OAAO,MAAM,OAAO,GAAI,OAAO,UAAU,WAAW,EAAE,MAAM,IAAI,CAAC,EAAG;AACvG;AAEA,IAAM,UAAU,CAAC,UAAwC;AACvD,MAAI,CAAC,SAAS,KAAK,EAAG,QAAO;AAC7B,QAAM,EAAE,IAAI,MAAM,QAAQ,IAAI;AAC9B,MAAI,OAAO,OAAO,YAAY,CAAC,MAAM,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO;AACrE,SAAO,EAAE,IAAI,GAAI,OAAO,SAAS,WAAW,EAAE,KAAK,IAAI,CAAC,GAAI,SAAS,QAAQ,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ,EAAE;AACjI;AAGA,IAAM,YAAY,CAAC,UAAkD;AACnE,MAAI,CAAC,SAAS,KAAK,KAAK,MAAM,YAAY,wBAAyB,QAAO;AAC1E,QAAM,EAAE,MAAM,QAAQ,UAAU,QAAQ,IAAI;AAC5C,MAAI,OAAO,WAAW,SAAU,QAAO;AACvC,MAAI,SAAS,gBAAiB,QAAO,EAAE,SAAS,yBAAyB,MAAM,OAAO;AACtF,MAAI,OAAO,aAAa,SAAU,QAAO;AACzC,MAAI,SAAS,gBAAiB,QAAO,EAAE,SAAS,yBAAyB,MAAM,UAAU,OAAO;AAChG,MAAI,SAAS,iBAAiB,OAAO,YAAY,SAAU,QAAO,EAAE,SAAS,yBAAyB,MAAM,UAAU,SAAS,OAAO;AACtI,SAAO;AACT;AAEA,IAAM,cAAc,CAAC,UAA4C;AAC/D,MAAI,CAAC,SAAS,KAAK,EAAG,QAAO;AAC7B,QAAM,EAAE,MAAM,IAAI,GAAG,IAAI;AACzB,MAAK,SAAS,aAAa,SAAS,WAAY,OAAO,OAAO,YAAY,CAAC,GAAI,QAAO;AACtF,SAAO,EAAE,MAAM,IAAI,IAAI,OAAO,OAAO,WAAW,KAAK,EAAE;AACzD;AAOO,SAAS,qBAAqB,OAAgB,SAA+C;AAClG,QAAM,MAAe,OAAO,UAAU,WAAW,SAAS,KAAK,IAAI;AACnE,MAAI,CAAC,SAAS,GAAG,KAAK,IAAI,YAAY,yBAA0B,QAAO,qBAAqB,OAAO;AACnG,QAAM,SAAS,UAAU,IAAI,OAAO;AACpC,MAAI,UAAU,CAAC,mBAAmB,QAAQ,OAAO,EAAG,QAAO,qBAAqB,OAAO;AACvF,SAAO;AAAA,IACL,SAAS;AAAA,IACT;AAAA,IACA,UAAU,OAAO,IAAI,aAAa,YAAY,OAAO,SAAS,IAAI,QAAQ,IAAI,IAAI,WAAW;AAAA,IAC7F,SAAS,MAAM,QAAQ,IAAI,OAAO,IAAI,IAAI,QAAQ,IAAI,OAAO,EAAE,OAAO,CAAC,MAAyB,MAAM,IAAI,IAAI,CAAC;AAAA,IAC/G,QAAQ,MAAM,QAAQ,IAAI,MAAM,IAAI,IAAI,OAAO,IAAI,OAAO,EAAE,OAAO,CAAC,MAAyB,MAAM,IAAI,IAAI,CAAC;AAAA,IAC5G,YAAY,MAAM,QAAQ,IAAI,UAAU,IAAI,IAAI,WAAW,IAAI,WAAW,EAAE,OAAO,CAAC,MAA6B,MAAM,IAAI,IAAI,CAAC;AAAA,EAClI;AACF;AAEA,IAAM,WAAW,CAAC,SAA0B;AAC1C,MAAI;AACF,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/drawings/document.ts"],"sourcesContent":["// The drawings DOCUMENT: the pure rules over the drawing-resource contract's document shape.\n//\n// Nothing here touches a chart, a store or a clock. That is deliberate: what a document means when\n// two surfaces write it, what a deletion does to it, and which of its entries are live are exactly\n// the questions that go wrong in a persistence layer, and they are all decidable from two values.\n//\n// Three rules the module enforces:\n// - A deletion is a TOMBSTONE, never an omission. Two charts hold the same document; if a\n// deletion were just a shorter list, the other chart's copy would put the row back on the next\n// merge, and a drawing the viewer deleted would return after a save, a reconnect or a reload.\n// - A merge is not a last-write-wins overwrite. The stored document is the other surface's work\n// and wins by identity and by order; this surface's own additions follow it. Neither side\n// loses a row it made, and neither side resurrects a row the other buried.\n// - Deleting a GROUP deletes the grouping, not the drawings. The members survive, ungrouped, and\n// the group id can never come back.\n//\n// The document's own SHAPE lives here too, beside the rules, because the two only make sense\n// together: the resource contract in `resources.ts` names this the drawings family's body.\n\n/* ── The drawing-resource context ───────────────────────────────────────────────────────────────\n Where a drawings document lives, as a discriminated, versioned context rather than a pair of\n loose fields. The kind IS the sharing rule, so a reader never infers one from which fields\n happen to be set:\n\n chart-local one chart of one layout, on one symbol. Nothing is shared.\n layout-shared every chart of one layout, on one symbol.\n symbol-global every chart the host mounts, on one symbol.\n\n `version` is the context's own format, not the document's: a store keys its rows by the whole\n context, so a later kind or field arrives as a different key rather than as a silently widened\n old one. */\n\nexport const DRAWING_CONTEXT_VERSION = 1\n\nexport type DrawingContextKind = 'chart-local' | 'layout-shared' | 'symbol-global'\n\n/** One chart of one layout, on one symbol. */\nexport interface ChartLocalDrawingContext {\n version: typeof DRAWING_CONTEXT_VERSION\n kind: 'chart-local'\n /** The host's stable identity for the layout the chart is tiled in. */\n layoutId: string\n /** The chart's stable identity WITHIN that layout, so the document is found again after a\n * reload; a per-mount instance id would key a document nothing could ever read back. */\n chartId: string\n symbol: string\n}\n\n/** Every chart of one layout, on one symbol. */\nexport interface LayoutSharedDrawingContext {\n version: typeof DRAWING_CONTEXT_VERSION\n kind: 'layout-shared'\n layoutId: string\n symbol: string\n}\n\n/** Every chart the host mounts, on one symbol. */\nexport interface SymbolGlobalDrawingContext {\n version: typeof DRAWING_CONTEXT_VERSION\n kind: 'symbol-global'\n symbol: string\n}\n\nexport type DrawingResourceContext = ChartLocalDrawingContext | LayoutSharedDrawingContext | SymbolGlobalDrawingContext\n\n/** The drawings document format. Bumping this is the only reason a reader ever branches. */\nexport const DRAWING_DOCUMENT_VERSION = 1\n\n/** One drawing in a document. The ENVELOPE is structured so a restore is validated without parsing\n * the drawing itself: the id is stable for the drawing's life, `source` names what owns it (the\n * main series, or an indicator's own source), `pane` names where it is drawn, and `type` is its\n * tool. `state` is the drawing's own serialized form and is opaque to the document, the store and\n * the transport. */\nexport interface DrawingEntry {\n id: string\n /** The owning source: `main` for the chart's main series, else an indicator instance's id. */\n source: string\n /** The pane the drawing is drawn in: `main` for the price pane, else the pane owner's id. */\n pane: string\n type: string\n /** Opaque: nothing between the layer and the backend reads it. */\n state: unknown\n /** The group this drawing belongs to, when it belongs to one. */\n group?: string\n}\n\n/** A group of drawings. Membership is ORDERED and holds drawing ids, so a group survives a round\n * trip with its order intact rather than being rebuilt from however a document happened to list\n * its entries. */\nexport interface DrawingGroup {\n id: string\n name?: string\n members: readonly string[]\n}\n\n/** A deletion, kept. A document that listed only what survives lets a concurrent save, a reconnect\n * or a reload resurrect what the viewer deleted: the other side still holds the row and a merge\n * takes it back. A tombstone says the deletion happened, and at which document revision, so the\n * merge drops it instead. */\nexport interface DrawingTombstone {\n kind: 'drawing' | 'group'\n id: string\n /** The document revision the deletion was stamped at. */\n at: number\n}\n\n/** One context's drawings document. `revision` is the DOCUMENT's own counter, which orders\n * tombstones and detects a stale apply; it is not the store's `ResourceRef.revision`, which stays\n * opaque and is the store's alone. */\nexport interface DrawingsBody {\n version: typeof DRAWING_DOCUMENT_VERSION\n context: DrawingResourceContext\n revision: number\n entries: readonly DrawingEntry[]\n groups: readonly DrawingGroup[]\n tombstones: readonly DrawingTombstone[]\n}\n\n\n/** The canonical key for a drawing-resource context: equal contexts key equally, and no two\n * different contexts collide. The parts join on an escaped null, which no symbol, layout id or\n * chart id carries, so a symbol ending in a separator cannot spell another context's key. The\n * kind and version lead, so a later kind is a different key rather than a widened old one. */\nexport function drawingContextKey(context: DrawingResourceContext): string {\n const parts: string[] = [String(context.version), context.kind]\n if (context.kind === 'chart-local') parts.push(context.layoutId, context.chartId)\n else if (context.kind === 'layout-shared') parts.push(context.layoutId)\n parts.push(context.symbol)\n return parts.join('\\u0000')\n}\n\n/** Whether two contexts name the same document. */\nexport const sameDrawingContext = (a: DrawingResourceContext, b: DrawingResourceContext): boolean => drawingContextKey(a) === drawingContextKey(b)\n\n/** A document with nothing in it, at revision 0: what a context that has never been written reads\n * as, so every caller works over a document rather than over a null. */\nexport function emptyDrawingDocument(context: DrawingResourceContext): DrawingsBody {\n return { version: DRAWING_DOCUMENT_VERSION, context, revision: 0, entries: [], groups: [], tombstones: [] }\n}\n\nconst tombstoneKey = (kind: DrawingTombstone['kind'], id: string): string => `${kind}\\u0000${id}`\n\n/** The tombstones of both documents, one per (kind, id), each at the later revision it was seen at. */\nfunction mergeTombstones(a: readonly DrawingTombstone[], b: readonly DrawingTombstone[]): DrawingTombstone[] {\n const byKey = new Map<string, DrawingTombstone>()\n for (const stone of [...a, ...b]) {\n const key = tombstoneKey(stone.kind, stone.id)\n const held = byKey.get(key)\n if (!held || stone.at > held.at) byKey.set(key, { kind: stone.kind, id: stone.id, at: stone.at })\n }\n return [...byKey.values()]\n}\n\nconst buriedSet = (tombstones: readonly DrawingTombstone[], kind: DrawingTombstone['kind']): Set<string> =>\n new Set(tombstones.filter((stone) => stone.kind === kind).map((stone) => stone.id))\n\n/** Whether a document buries this id. Package-internal, beside the rules it serves: the layer asks\n * it to tell a live entry from one whose group is gone. */\nexport const drawingBuried = (document: DrawingsBody, kind: DrawingTombstone['kind'], id: string): boolean =>\n document.tombstones.some((stone) => stone.kind === kind && stone.id === id)\n\n/** The entries a document actually holds: nothing buried, no duplicate id, and no membership of a\n * group that is gone (the drawing survives, its grouping does not). */\nexport function liveDrawingEntries(document: DrawingsBody): DrawingEntry[] {\n const buriedDrawings = buriedSet(document.tombstones, 'drawing')\n const groups = new Set(liveDrawingGroups(document).map((group) => group.id))\n const out: DrawingEntry[] = []\n const seen = new Set<string>()\n for (const entry of document.entries) {\n if (buriedDrawings.has(entry.id) || seen.has(entry.id)) continue\n seen.add(entry.id)\n out.push(entry.group !== undefined && !groups.has(entry.group) ? omitGroup(entry) : entry)\n }\n return out\n}\n\nconst omitGroup = (entry: DrawingEntry): DrawingEntry => {\n const { group: _buried, ...rest } = entry\n return rest\n}\n\n/** The groups a document actually holds: nothing buried, no duplicate id, and no member that is\n * buried or absent, so a group never names a drawing that is not there. */\nexport function liveDrawingGroups(document: DrawingsBody): DrawingGroup[] {\n const buriedGroups = buriedSet(document.tombstones, 'group')\n const buriedDrawings = buriedSet(document.tombstones, 'drawing')\n const present = new Set(document.entries.filter((entry) => !buriedDrawings.has(entry.id)).map((entry) => entry.id))\n const out: DrawingGroup[] = []\n const seen = new Set<string>()\n for (const group of document.groups) {\n if (buriedGroups.has(group.id) || seen.has(group.id)) continue\n seen.add(group.id)\n out.push({ ...group, members: group.members.filter((id) => present.has(id)) })\n }\n return out\n}\n\n/** Replace what a document holds and stamp the difference. Every id the document held and the next\n * state does not is buried at the new revision; every id that arrives is kept in the order it\n * arrives in. One verb, one revision bump, so a save can never write entries at one revision and\n * groups at another. */\nexport function reviseDrawingDocument(\n document: DrawingsBody,\n next: { entries?: readonly DrawingEntry[]; groups?: readonly DrawingGroup[] },\n): DrawingsBody {\n const revision = document.revision + 1\n const entries = next.entries ?? liveDrawingEntries(document)\n const groups = next.groups ?? liveDrawingGroups(document)\n const keptDrawings = new Set(entries.map((entry) => entry.id))\n const keptGroups = new Set(groups.map((group) => group.id))\n const buried: DrawingTombstone[] = []\n for (const entry of liveDrawingEntries(document)) if (!keptDrawings.has(entry.id)) buried.push({ kind: 'drawing', id: entry.id, at: revision })\n for (const group of liveDrawingGroups(document)) if (!keptGroups.has(group.id)) buried.push({ kind: 'group', id: group.id, at: revision })\n return {\n version: DRAWING_DOCUMENT_VERSION,\n context: document.context,\n revision,\n entries: [...entries],\n groups: [...groups],\n tombstones: mergeTombstones(document.tombstones, buried),\n }\n}\n\n/** Merge the document another surface wrote first with this surface's own. The stored rows win by\n * identity and keep the stored order, because they are what everyone else can already see; rows\n * only this surface holds follow, because they are work the other surface never saw. Every\n * deletion either side made survives the merge. The result's revision is past both, so the write\n * that carries it is the newer document by either side's counter. */\nexport function mergeDrawingDocuments(stored: DrawingsBody, mine: DrawingsBody): DrawingsBody {\n const tombstones = mergeTombstones(stored.tombstones, mine.tombstones)\n const buriedDrawings = buriedSet(tombstones, 'drawing')\n const buriedGroups = buriedSet(tombstones, 'group')\n\n const entries: DrawingEntry[] = []\n const seen = new Set<string>()\n for (const entry of [...stored.entries, ...mine.entries]) {\n if (seen.has(entry.id) || buriedDrawings.has(entry.id)) continue\n seen.add(entry.id)\n entries.push(entry.group !== undefined && buriedGroups.has(entry.group) ? omitGroup(entry) : entry)\n }\n\n const groups: DrawingGroup[] = []\n const byId = new Map<string, DrawingGroup>()\n for (const group of [...stored.groups, ...mine.groups]) {\n if (buriedGroups.has(group.id)) continue\n const held = byId.get(group.id)\n if (!held) {\n const row = { ...group, members: [...group.members] }\n byId.set(group.id, row)\n groups.push(row)\n continue\n }\n // One group written by both surfaces: the stored order stands and the members only this\n // surface knows about are appended, the same rule the entries follow.\n for (const id of group.members) if (!held.members.includes(id)) (held.members as string[]).push(id)\n }\n const present = new Set(entries.map((entry) => entry.id))\n\n return {\n version: DRAWING_DOCUMENT_VERSION,\n context: stored.context,\n revision: Math.max(stored.revision, mine.revision) + 1,\n entries,\n groups: groups.map((group) => ({ ...group, members: group.members.filter((id) => present.has(id)) })),\n tombstones,\n }\n}\n\nconst isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null\n\nconst entryOf = (value: unknown): DrawingEntry | null => {\n if (!isRecord(value)) return null\n const { id, source, pane, type, group } = value\n if (typeof id !== 'string' || !id || typeof source !== 'string' || typeof pane !== 'string' || typeof type !== 'string') return null\n return { id, source, pane, type, state: value.state, ...(typeof group === 'string' ? { group } : {}) }\n}\n\nconst groupOf = (value: unknown): DrawingGroup | null => {\n if (!isRecord(value)) return null\n const { id, name, members } = value\n if (typeof id !== 'string' || !id || !Array.isArray(members)) return null\n return { id, ...(typeof name === 'string' ? { name } : {}), members: members.filter((m): m is string => typeof m === 'string') }\n}\n\n/** The context a payload states, when it states a whole one this build understands. */\nconst contextOf = (value: unknown): DrawingResourceContext | null => {\n if (!isRecord(value) || value.version !== DRAWING_CONTEXT_VERSION) return null\n const { kind, symbol, layoutId, chartId } = value\n if (typeof symbol !== 'string') return null\n if (kind === 'symbol-global') return { version: DRAWING_CONTEXT_VERSION, kind, symbol }\n if (typeof layoutId !== 'string') return null\n if (kind === 'layout-shared') return { version: DRAWING_CONTEXT_VERSION, kind, layoutId, symbol }\n if (kind === 'chart-local' && typeof chartId === 'string') return { version: DRAWING_CONTEXT_VERSION, kind, layoutId, chartId, symbol }\n return null\n}\n\nconst tombstoneOf = (value: unknown): DrawingTombstone | null => {\n if (!isRecord(value)) return null\n const { kind, id, at } = value\n if ((kind !== 'drawing' && kind !== 'group') || typeof id !== 'string' || !id) return null\n return { kind, id, at: typeof at === 'number' ? at : 0 }\n}\n\n/** Read whatever a transport handed back as a document for this context. TOTAL: a null, a string, a\n * wrong version, a junk row or a whole junk body reads as the empty document, never a throw, so a\n * host can hand the chart what its backend returned without guarding it first. The context is the\n * caller's, not the payload's: a document is identified by where it was read from, and a body\n * claiming another context is not merged into this one. */\nexport function parseDrawingDocument(value: unknown, context: DrawingResourceContext): DrawingsBody {\n const raw: unknown = typeof value === 'string' ? safeJson(value) : value\n if (!isRecord(raw) || raw.version !== DRAWING_DOCUMENT_VERSION) return emptyDrawingDocument(context)\n const stated = contextOf(raw.context)\n if (stated && !sameDrawingContext(stated, context)) return emptyDrawingDocument(context)\n return {\n version: DRAWING_DOCUMENT_VERSION,\n context,\n revision: typeof raw.revision === 'number' && Number.isFinite(raw.revision) ? raw.revision : 0,\n entries: Array.isArray(raw.entries) ? raw.entries.map(entryOf).filter((e): e is DrawingEntry => e !== null) : [],\n groups: Array.isArray(raw.groups) ? raw.groups.map(groupOf).filter((g): g is DrawingGroup => g !== null) : [],\n tombstones: Array.isArray(raw.tombstones) ? raw.tombstones.map(tombstoneOf).filter((t): t is DrawingTombstone => t !== null) : [],\n }\n}\n\nconst safeJson = (text: string): unknown => {\n try {\n return JSON.parse(text)\n } catch {\n return null\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -46,7 +46,7 @@ function visibilityPreset(kind, ctx) {
|
|
|
46
46
|
}
|
|
47
47
|
return out;
|
|
48
48
|
}
|
|
49
|
-
function
|
|
49
|
+
function parseTimeframeContext(tf) {
|
|
50
50
|
const match = /^(\d+)(t|s|m|h|d|w|mo)$/.exec(tf.trim().toLowerCase());
|
|
51
51
|
if (!match) return null;
|
|
52
52
|
let value = Number(match[1]);
|
|
@@ -340,9 +340,9 @@ function viewportOf(chart, series) {
|
|
|
340
340
|
const firstTime = data.length > 0 ? numTime(0) : null;
|
|
341
341
|
const lastTime = data.length > 0 ? numTime(lastIndex) : null;
|
|
342
342
|
const prevTime = numTime(lastIndex - 1);
|
|
343
|
-
const
|
|
343
|
+
const trailStep = lastTime !== null && prevTime !== null && lastTime > prevTime ? lastTime - prevTime : null;
|
|
344
344
|
const secondTime = numTime(1);
|
|
345
|
-
const
|
|
345
|
+
const leadStep = firstTime !== null && secondTime !== null && secondTime > firstTime ? secondTime - firstTime : null;
|
|
346
346
|
const logicalOfTime = (time) => {
|
|
347
347
|
const x = ts.timeToCoordinate(time);
|
|
348
348
|
if (x !== null) {
|
|
@@ -350,8 +350,8 @@ function viewportOf(chart, series) {
|
|
|
350
350
|
if (logical !== null) return logical;
|
|
351
351
|
}
|
|
352
352
|
if (typeof time !== "number" || firstTime === null || lastTime === null) return null;
|
|
353
|
-
if (time >= lastTime) return
|
|
354
|
-
if (time <= firstTime) return
|
|
353
|
+
if (time >= lastTime) return trailStep === null ? lastIndex : lastIndex + (time - lastTime) / trailStep;
|
|
354
|
+
if (time <= firstTime) return leadStep === null ? 0 : (time - firstTime) / leadStep;
|
|
355
355
|
let lo = 0;
|
|
356
356
|
let hi = lastIndex;
|
|
357
357
|
while (hi - lo > 1) {
|
|
@@ -368,8 +368,8 @@ function viewportOf(chart, series) {
|
|
|
368
368
|
};
|
|
369
369
|
const timeOfLogicalIndex = (logical) => {
|
|
370
370
|
if (firstTime === null || lastTime === null) return null;
|
|
371
|
-
if (logical >= lastIndex) return
|
|
372
|
-
if (logical <= 0) return
|
|
371
|
+
if (logical >= lastIndex) return trailStep === null ? lastTime : lastTime + (logical - lastIndex) * trailStep;
|
|
372
|
+
if (logical <= 0) return leadStep === null ? firstTime : firstTime + logical * leadStep;
|
|
373
373
|
const i = Math.floor(logical);
|
|
374
374
|
const tLo = numTime(i);
|
|
375
375
|
const tHi = numTime(i + 1);
|
|
@@ -422,7 +422,7 @@ var Drawing = class {
|
|
|
422
422
|
_options;
|
|
423
423
|
_props;
|
|
424
424
|
_state = "normal";
|
|
425
|
-
|
|
425
|
+
_timeframeContext = null;
|
|
426
426
|
_globalHidden = false;
|
|
427
427
|
_barSource = null;
|
|
428
428
|
_chart = null;
|
|
@@ -534,8 +534,8 @@ var Drawing = class {
|
|
|
534
534
|
};
|
|
535
535
|
this.requestUpdate();
|
|
536
536
|
}
|
|
537
|
-
|
|
538
|
-
this.
|
|
537
|
+
setTimeframeContext(context) {
|
|
538
|
+
this._timeframeContext = context;
|
|
539
539
|
this.requestUpdate();
|
|
540
540
|
}
|
|
541
541
|
/** Chart-wide hide-all switch (transient view state — never serialized). */
|
|
@@ -583,7 +583,7 @@ var Drawing = class {
|
|
|
583
583
|
return this._glyphSource ? this._glyphSource(glyph) : null;
|
|
584
584
|
}
|
|
585
585
|
isVisibleNow() {
|
|
586
|
-
return !this._globalHidden && this._options.visible && visibleAt(this._options.visibility, this.
|
|
586
|
+
return !this._globalHidden && this._options.visible && visibleAt(this._options.visibility, this._timeframeContext);
|
|
587
587
|
}
|
|
588
588
|
applyProps(patch) {
|
|
589
589
|
this._props = { ...this._props, ...patch };
|
|
@@ -746,7 +746,7 @@ var DrawingManager = class {
|
|
|
746
746
|
_selectedId = null;
|
|
747
747
|
_chart = null;
|
|
748
748
|
_series = null;
|
|
749
|
-
|
|
749
|
+
_timeframeContext = null;
|
|
750
750
|
_allHidden = false;
|
|
751
751
|
_barSource = null;
|
|
752
752
|
_tickSize = null;
|
|
@@ -775,10 +775,10 @@ var DrawingManager = class {
|
|
|
775
775
|
this._glyphSource = source;
|
|
776
776
|
for (const drawing of this._drawings.values()) drawing.setGlyphSource(source);
|
|
777
777
|
}
|
|
778
|
-
/** Broadcast the chart's
|
|
779
|
-
|
|
780
|
-
this.
|
|
781
|
-
for (const drawing of this._drawings.values()) drawing.
|
|
778
|
+
/** Broadcast the chart's timeframe so per-timeframe visibility rules apply. */
|
|
779
|
+
setTimeframeContext(context) {
|
|
780
|
+
this._timeframeContext = context;
|
|
781
|
+
for (const drawing of this._drawings.values()) drawing.setTimeframeContext(context);
|
|
782
782
|
}
|
|
783
783
|
/** Chart-wide hide-all: every drawing (and its axis pills) disappears until toggled back. */
|
|
784
784
|
setAllHidden(hidden) {
|
|
@@ -816,7 +816,7 @@ var DrawingManager = class {
|
|
|
816
816
|
add(drawing) {
|
|
817
817
|
if (this._drawings.has(drawing.id)) return;
|
|
818
818
|
const concrete = drawing;
|
|
819
|
-
concrete.
|
|
819
|
+
concrete.setTimeframeContext(this._timeframeContext);
|
|
820
820
|
concrete.setGlobalHidden(this._allHidden);
|
|
821
821
|
concrete.setBarSource(this._barSource);
|
|
822
822
|
concrete.setTickSize(this._tickSize);
|
|
@@ -1691,6 +1691,14 @@ var Ellipse = class extends Drawing {
|
|
|
1691
1691
|
ctx.stroke();
|
|
1692
1692
|
if (this.props.text) paintLabel(ctx, this.props.text, geo.center, this.style, { align: "center" });
|
|
1693
1693
|
}
|
|
1694
|
+
/** The third handle stands on the ellipse at its centre's time, where the vertical radius it
|
|
1695
|
+
* drags meets the outline: the anchor's own time paints nothing, so a handle at it would stay
|
|
1696
|
+
* behind as the diameter moves. */
|
|
1697
|
+
getControlPoints(viewport) {
|
|
1698
|
+
const points = super.getControlPoints(viewport);
|
|
1699
|
+
const geo = this.geometry(viewport);
|
|
1700
|
+
return geo ? points.map((point) => point.index === 2 ? { ...point, x: geo.center.x } : point) : points;
|
|
1701
|
+
}
|
|
1694
1702
|
testHit(point, viewport) {
|
|
1695
1703
|
const geo = this.geometry(viewport);
|
|
1696
1704
|
if (!geo) return false;
|
|
@@ -2676,6 +2684,14 @@ var FlatTopBottom = class extends Drawing {
|
|
|
2676
2684
|
strokeSegment(ctx, b.flat.a, b.flat.b);
|
|
2677
2685
|
if (this.props.text) paintLabel(ctx, this.props.text, { x: b.slope.a.x + 6, y: b.slope.a.y - 12 }, this.style);
|
|
2678
2686
|
}
|
|
2687
|
+
/** The flat side's handle stands at the flat side's end under the second anchor: the flat side
|
|
2688
|
+
* spans the sloped side's times at the third anchor's price, and that anchor's own time paints
|
|
2689
|
+
* nothing, so a handle at it would drift off the side as the sloped side moves. */
|
|
2690
|
+
getControlPoints(viewport) {
|
|
2691
|
+
const points = super.getControlPoints(viewport);
|
|
2692
|
+
const end = points.find((point) => point.index === 1);
|
|
2693
|
+
return end ? points.map((point) => point.index === 2 ? { ...point, x: end.x } : point) : points;
|
|
2694
|
+
}
|
|
2679
2695
|
testHit(point, viewport) {
|
|
2680
2696
|
const b = this.boundaries(viewport);
|
|
2681
2697
|
if (!b) return false;
|
|
@@ -3458,6 +3474,16 @@ var FibWedge = class extends Drawing {
|
|
|
3458
3474
|
}
|
|
3459
3475
|
}
|
|
3460
3476
|
}
|
|
3477
|
+
/** The third handle stands at the end of the second ray: that ray runs as long as the first, at
|
|
3478
|
+
* the third anchor's angle, so the anchor's own distance from the apex paints nothing and a
|
|
3479
|
+
* handle at it would float off the ray's end as the first ray changes length. */
|
|
3480
|
+
getControlPoints(viewport) {
|
|
3481
|
+
const points = super.getControlPoints(viewport);
|
|
3482
|
+
const geo = this.geometry(viewport);
|
|
3483
|
+
if (!geo) return points;
|
|
3484
|
+
const end = { x: geo.apex.x + Math.cos(geo.a2) * geo.radius, y: geo.apex.y + Math.sin(geo.a2) * geo.radius };
|
|
3485
|
+
return points.map((point) => point.index === 2 ? { ...point, ...end } : point);
|
|
3486
|
+
}
|
|
3461
3487
|
testHit(point, viewport) {
|
|
3462
3488
|
const geo = this.geometry(viewport);
|
|
3463
3489
|
if (!geo) return false;
|
|
@@ -3755,6 +3781,14 @@ var LongPosition = class extends Drawing {
|
|
|
3755
3781
|
paintLabel(ctx, this.formatPrice(entry.price), { x: z.right + 6, y: z.entryY }, { ...this.style, textColor: this.style.lineColor });
|
|
3756
3782
|
}
|
|
3757
3783
|
}
|
|
3784
|
+
/** The stop's handle stands on the box's far edge beside the target's, where the stop's level is
|
|
3785
|
+
* painted: the box's edges are the entry's time and the target's, and the stop's own time paints
|
|
3786
|
+
* nothing, so a handle at it would stay behind as the box's width changes. */
|
|
3787
|
+
getControlPoints(viewport) {
|
|
3788
|
+
const points = super.getControlPoints(viewport);
|
|
3789
|
+
const edge = points.find((point) => point.index === 1);
|
|
3790
|
+
return edge ? points.map((point) => point.index === 2 ? { ...point, x: edge.x } : point) : points;
|
|
3791
|
+
}
|
|
3758
3792
|
testHit(point, viewport) {
|
|
3759
3793
|
const z = this.zones(viewport);
|
|
3760
3794
|
if (!z) return false;
|
|
@@ -5752,12 +5786,12 @@ function chooseMagnetStrength(mode, strength) {
|
|
|
5752
5786
|
}
|
|
5753
5787
|
var magnetActive = (mode) => mode !== "off";
|
|
5754
5788
|
|
|
5755
|
-
// src/drawings/
|
|
5789
|
+
// src/drawings/drawingToolbarModel.ts
|
|
5756
5790
|
var ARROW_TYPES = ["arrow_marker", "arrow", "arrow_up", "arrow_down"];
|
|
5757
5791
|
var BRUSH_TYPES = ["brush", "highlighter"];
|
|
5758
5792
|
var GLYPH_TYPES = ["emoji", "sticker", "icon"];
|
|
5759
5793
|
var CARD_TYPES = ["image", "content_card"];
|
|
5760
|
-
var
|
|
5794
|
+
var DRAWING_TOOLBAR_PLAN = [
|
|
5761
5795
|
{
|
|
5762
5796
|
id: "trend",
|
|
5763
5797
|
label: "drawing.groupTrend",
|
|
@@ -5818,9 +5852,9 @@ var RAIL_PLAN = [
|
|
|
5818
5852
|
}
|
|
5819
5853
|
];
|
|
5820
5854
|
var isTypeSection = (section) => "types" in section;
|
|
5821
|
-
function
|
|
5855
|
+
function buildDrawingToolbarGroups(catalog2 = drawingTools) {
|
|
5822
5856
|
const toolsOf = (section) => isTypeSection(section) ? section.types.map((type) => catalog2.get(type)).filter((tool2) => !!tool2) : catalog2.byCategory(section.category).filter((tool2) => !(section.exclude ?? []).includes(tool2.type));
|
|
5823
|
-
return
|
|
5857
|
+
return DRAWING_TOOLBAR_PLAN.map((group) => ({
|
|
5824
5858
|
id: group.id,
|
|
5825
5859
|
label: group.label,
|
|
5826
5860
|
sections: group.sections.map((section) => ({ label: section.label, tools: toolsOf(section) })).filter((section) => section.tools.length > 0)
|
|
@@ -5830,12 +5864,12 @@ function groupOfTool(groups, type) {
|
|
|
5830
5864
|
if (!type) return null;
|
|
5831
5865
|
return groups.find((group) => group.sections.some((section) => section.tools.some((tool2) => tool2.type === type)))?.id ?? null;
|
|
5832
5866
|
}
|
|
5833
|
-
function
|
|
5867
|
+
function drawingToolbarFaceOf(group, lastTools) {
|
|
5834
5868
|
const remembered = lastTools[group.id];
|
|
5835
5869
|
if (remembered && group.sections.some((section) => section.tools.some((tool2) => tool2.type === remembered))) return remembered;
|
|
5836
5870
|
return group.sections[0]?.tools[0]?.type ?? null;
|
|
5837
5871
|
}
|
|
5838
|
-
function
|
|
5872
|
+
function rememberDrawingToolbarTool(groups, lastTools, type) {
|
|
5839
5873
|
const group = groupOfTool(groups, type);
|
|
5840
5874
|
if (!group) return { ...lastTools };
|
|
5841
5875
|
return { ...lastTools, [group]: type };
|
|
@@ -6565,7 +6599,7 @@ var DEFAULT_DRAWING_PREFERENCES = {
|
|
|
6565
6599
|
stayInDrawingMode: false,
|
|
6566
6600
|
removeLocked: false,
|
|
6567
6601
|
syncAcrossPanes: true,
|
|
6568
|
-
|
|
6602
|
+
drawingToolbarTools: {},
|
|
6569
6603
|
favorites: DEFAULT_FAVORITES,
|
|
6570
6604
|
settingsBarPosition: null,
|
|
6571
6605
|
recentGlyphs: [],
|
|
@@ -6607,7 +6641,7 @@ function parseDrawingPreferences(raw) {
|
|
|
6607
6641
|
stayInDrawingMode: bool(value.stayInDrawingMode, DEFAULT_DRAWING_PREFERENCES.stayInDrawingMode),
|
|
6608
6642
|
removeLocked: bool(value.removeLocked, DEFAULT_DRAWING_PREFERENCES.removeLocked),
|
|
6609
6643
|
syncAcrossPanes: bool(value.syncAcrossPanes, DEFAULT_DRAWING_PREFERENCES.syncAcrossPanes),
|
|
6610
|
-
|
|
6644
|
+
drawingToolbarTools: stringRecord(value.drawingToolbarTools),
|
|
6611
6645
|
favorites: favoritesOf(value.favorites),
|
|
6612
6646
|
settingsBarPosition: positionOf(value.settingsBarPosition),
|
|
6613
6647
|
recentGlyphs: Array.isArray(value.recentGlyphs) ? value.recentGlyphs.filter((g) => typeof g === "string").slice(0, RECENT_GLYPHS_MAX) : [],
|
|
@@ -6650,7 +6684,7 @@ var DrawingTemplates = class {
|
|
|
6650
6684
|
}
|
|
6651
6685
|
/** Every row of the family, defaults included, each with its preset. A host that has to answer
|
|
6652
6686
|
* synchronously (a placement path, a render) reads this once into a cache of its own rather
|
|
6653
|
-
* fetched: one read per saved setup, once, against a family a
|
|
6687
|
+
* fetched: one read per saved setup, once, against a family a viewer keeps in the dozens. The
|
|
6654
6688
|
* reads run together, `BODY_READS` at a time, and the answer keeps the store's order. */
|
|
6655
6689
|
async listAll(signal) {
|
|
6656
6690
|
const rows = await this.store.list(signal);
|
|
@@ -6701,7 +6735,7 @@ var DrawingTemplates = class {
|
|
|
6701
6735
|
return found ? decodePreset(found.body.content) : {};
|
|
6702
6736
|
}
|
|
6703
6737
|
/** Remember a tool's default. This runs after an ordinary style edit, so a CONFLICT here is not
|
|
6704
|
-
* worth surfacing: another surface already wrote a newer default and the
|
|
6738
|
+
* worth surfacing: another surface already wrote a newer default and the viewer's next edit
|
|
6705
6739
|
* writes again. It is returned rather than swallowed so a caller may still look. */
|
|
6706
6740
|
async rememberDefault(tool2, preset, signal) {
|
|
6707
6741
|
return this.save(tool2, DEFAULT_PRESET_NAME, preset, signal);
|
|
@@ -6849,7 +6883,7 @@ export {
|
|
|
6849
6883
|
parseCssColor,
|
|
6850
6884
|
DEFAULT_VISIBILITY,
|
|
6851
6885
|
visibilityPreset,
|
|
6852
|
-
|
|
6886
|
+
parseTimeframeContext,
|
|
6853
6887
|
normalizeVisibility,
|
|
6854
6888
|
visibleAt,
|
|
6855
6889
|
DEFAULT_STYLE,
|
|
@@ -6894,11 +6928,11 @@ export {
|
|
|
6894
6928
|
BRUSH_TYPES,
|
|
6895
6929
|
GLYPH_TYPES,
|
|
6896
6930
|
CARD_TYPES,
|
|
6897
|
-
|
|
6898
|
-
|
|
6931
|
+
DRAWING_TOOLBAR_PLAN,
|
|
6932
|
+
buildDrawingToolbarGroups,
|
|
6899
6933
|
groupOfTool,
|
|
6900
|
-
|
|
6901
|
-
|
|
6934
|
+
drawingToolbarFaceOf,
|
|
6935
|
+
rememberDrawingToolbarTool,
|
|
6902
6936
|
CHART_HIDE_LAYERS,
|
|
6903
6937
|
hideOrder,
|
|
6904
6938
|
DEFAULT_HIDE_STATE,
|
|
@@ -6960,4 +6994,4 @@ export {
|
|
|
6960
6994
|
NO_DASH,
|
|
6961
6995
|
INPUT_PROPS
|
|
6962
6996
|
};
|
|
6963
|
-
//# sourceMappingURL=chunk-
|
|
6997
|
+
//# sourceMappingURL=chunk-YR7NT5ZW.js.map
|