@pixerate/editor 0.277.0 → 0.279.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.
@@ -473,6 +473,10 @@ function createAutosaveController(options) {
473
473
  return rev;
474
474
  };
475
475
  const docName = () => typeof options.name === "function" ? options.name() : options.name;
476
+ const readBaseVersion = () => {
477
+ const v = typeof options.baseVersion === "function" ? options.baseVersion() : options.baseVersion;
478
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
479
+ };
476
480
  const run = (action, rev) => {
477
481
  if (action.type === "writeSnapshot") {
478
482
  setInfo({ status: "saving", nextAttemptAt: null });
@@ -482,11 +486,13 @@ function createAutosaveController(options) {
482
486
  try {
483
487
  const text = serialize(getSnapshot());
484
488
  await storage.set(key, text);
489
+ const baseVersion = readBaseVersion();
485
490
  const meta = {
486
491
  dirty: true,
487
492
  revision: action.revision,
488
493
  time: wallClock(),
489
- name: docName()
494
+ name: docName(),
495
+ ...baseVersion !== void 0 ? { baseVersion } : {}
490
496
  };
491
497
  await storage.set(metaKey, JSON.stringify(meta));
492
498
  ok = true;
@@ -607,7 +613,7 @@ function createAutosaveController(options) {
607
613
  tick();
608
614
  await whenIdle();
609
615
  },
610
- async checkRecovery() {
616
+ async checkRecovery(checkOptions) {
611
617
  if (policy.ownsSnapshot) return null;
612
618
  let meta;
613
619
  try {
@@ -618,6 +624,12 @@ function createAutosaveController(options) {
618
624
  return null;
619
625
  }
620
626
  if (!meta || meta.dirty !== true) return null;
627
+ const required = checkOptions?.minimumBaseVersion ?? readBaseVersion();
628
+ if (required !== void 0 && (typeof meta.baseVersion === "number" ? meta.baseVersion : 0) < required) {
629
+ await storage.remove(metaKey).catch(() => void 0);
630
+ await storage.remove(key).catch(() => void 0);
631
+ return null;
632
+ }
621
633
  const text = await storage.get(key).catch(() => null);
622
634
  if (text === null) return null;
623
635
  try {
@@ -726,10 +738,36 @@ function createEditorAutosave(doc, options) {
726
738
  savedRevision: revisions.savedRevision
727
739
  });
728
740
  const unsubscribe = revisions.subscribe(() => controller.notify());
741
+ const retireRecovery = async () => {
742
+ await controller.discardRecovery();
743
+ controller.ready();
744
+ await collect();
745
+ };
729
746
  return {
730
747
  controller,
731
748
  revisions,
732
749
  checkRecovery: () => controller.checkRecovery(),
750
+ async recoverIfNewer(minimumBaseVersion) {
751
+ const recovery = await controller.checkRecovery(
752
+ minimumBaseVersion !== void 0 ? { minimumBaseVersion } : void 0
753
+ );
754
+ if (!recovery) {
755
+ controller.ready();
756
+ return null;
757
+ }
758
+ if (!recovery.snapshot) {
759
+ await retireRecovery();
760
+ return null;
761
+ }
762
+ try {
763
+ const report = doc.loadDocument(recovery.snapshot, { history: "reset" });
764
+ controller.ready();
765
+ return report;
766
+ } catch {
767
+ await retireRecovery();
768
+ return null;
769
+ }
770
+ },
733
771
  restore(recovery) {
734
772
  if (!recovery.snapshot) throw recovery.error ?? new Error("The recovered snapshot could not be read.");
735
773
  const report = doc.loadDocument(recovery.snapshot, { history: "reset" });
@@ -737,9 +775,7 @@ function createEditorAutosave(doc, options) {
737
775
  return report;
738
776
  },
739
777
  async discard() {
740
- await controller.discardRecovery();
741
- controller.ready();
742
- await collect();
778
+ await retireRecovery();
743
779
  },
744
780
  async markClean() {
745
781
  revisions.markSaved();
@@ -754,6 +790,289 @@ function createEditorAutosave(doc, options) {
754
790
  };
755
791
  }
756
792
 
793
+ // src/persistence/remoteSync.ts
794
+ var EditorRemoteConflictError = class extends Error {
795
+ constructor(serverVersion) {
796
+ super(
797
+ serverVersion === void 0 ? "The document changed on the server since it was loaded; local edits were not saved." : `The document is at version ${serverVersion} on the server; local edits were not saved.`
798
+ );
799
+ this.serverVersion = serverVersion;
800
+ this.name = "EditorRemoteConflictError";
801
+ }
802
+ serverVersion;
803
+ };
804
+ var cloneSnapshot = (value) => {
805
+ try {
806
+ return JSON.parse(JSON.stringify(value));
807
+ } catch {
808
+ return value;
809
+ }
810
+ };
811
+ function createEditorRemoteSync(doc, options) {
812
+ const debounceMs = options.debounceMs ?? 500;
813
+ const now = options.now ?? Date.now;
814
+ let localVersion = options.initialVersion ?? 0;
815
+ let confirmedVersion = localVersion;
816
+ let localSequence = 0;
817
+ let savedSequence = 0;
818
+ let baselineSnapshot = cloneSnapshot(doc.getDocument());
819
+ let savedSnapshot = baselineSnapshot;
820
+ let state = "idle";
821
+ let lastSavedAt = null;
822
+ let lastError = void 0;
823
+ let isApplyingRemote = false;
824
+ let conflictRetries = 0;
825
+ let destroyed = false;
826
+ let debounceTimer = null;
827
+ let inFlightPromise = null;
828
+ const listeners = /* @__PURE__ */ new Set();
829
+ const getStatus = () => ({
830
+ status: state,
831
+ version: localVersion,
832
+ savedVersion: confirmedVersion,
833
+ dirty: localSequence > savedSequence,
834
+ lastSavedAt,
835
+ ...lastError !== void 0 ? { error: lastError } : {}
836
+ });
837
+ const emit = () => {
838
+ const currentStatus = getStatus();
839
+ listeners.forEach((listener) => {
840
+ try {
841
+ listener(currentStatus);
842
+ } catch (error) {
843
+ console.error("[remoteSync] Error in status listener:", error);
844
+ }
845
+ });
846
+ };
847
+ const clearTimer = () => {
848
+ if (debounceTimer !== null) {
849
+ clearTimeout(debounceTimer);
850
+ debounceTimer = null;
851
+ }
852
+ };
853
+ const hasChanged = (current, baseline) => {
854
+ if (options.diff) {
855
+ const diff = options.diff(current, baseline);
856
+ const empty = options.isEmptyDiff ? options.isEmptyDiff(diff, current, baseline) : JSON.stringify(current) === JSON.stringify(baseline);
857
+ return { changed: !empty, diff };
858
+ }
859
+ return { changed: JSON.stringify(current) !== JSON.stringify(baseline) };
860
+ };
861
+ const suppressed = (fn) => {
862
+ const wasApplying = isApplyingRemote;
863
+ isApplyingRemote = true;
864
+ try {
865
+ return fn();
866
+ } finally {
867
+ isApplyingRemote = wasApplying;
868
+ }
869
+ };
870
+ const executeSave = async () => {
871
+ if (inFlightPromise) {
872
+ await inFlightPromise;
873
+ if (localSequence > savedSequence) {
874
+ return executeSave();
875
+ }
876
+ return true;
877
+ }
878
+ if (localSequence <= savedSequence) {
879
+ return true;
880
+ }
881
+ clearTimer();
882
+ const targetSequence = localSequence;
883
+ const snapshotToSave = cloneSnapshot(baselineSnapshot);
884
+ const diffToSave = options.diff ? options.diff(snapshotToSave, savedSnapshot) : void 0;
885
+ const versionToSave = Math.max(localVersion, confirmedVersion + 1);
886
+ localVersion = versionToSave;
887
+ state = "saving";
888
+ lastError = void 0;
889
+ emit();
890
+ const run = (async () => {
891
+ try {
892
+ const result = await options.save({
893
+ document: snapshotToSave,
894
+ version: versionToSave,
895
+ baseVersion: confirmedVersion,
896
+ diff: diffToSave
897
+ });
898
+ if (result && typeof result === "object" && result.conflict) {
899
+ const serverVersion = typeof result.version === "number" ? result.version : void 0;
900
+ if (!options.onConflict || conflictRetries >= (options.maxConflictRetries ?? 3)) {
901
+ conflictRetries = 0;
902
+ throw new EditorRemoteConflictError(serverVersion);
903
+ }
904
+ conflictRetries++;
905
+ const resolution = await options.onConflict({
906
+ document: snapshotToSave,
907
+ base: savedSnapshot,
908
+ serverVersion,
909
+ apply: (fn) => suppressed(fn)
910
+ });
911
+ confirmedVersion = resolution.serverVersion;
912
+ localVersion = Math.max(localVersion, confirmedVersion);
913
+ if (resolution.serverDocument !== void 0) savedSnapshot = cloneSnapshot(resolution.serverDocument);
914
+ baselineSnapshot = cloneSnapshot(doc.getDocument());
915
+ localSequence++;
916
+ state = "pending";
917
+ emit();
918
+ return true;
919
+ }
920
+ conflictRetries = 0;
921
+ const returnedVersion = typeof result === "number" ? result : result && typeof result === "object" && typeof result.version === "number" ? result.version : versionToSave;
922
+ confirmedVersion = Math.max(confirmedVersion, versionToSave, returnedVersion);
923
+ if (returnedVersion > localVersion) {
924
+ localVersion = returnedVersion;
925
+ }
926
+ savedSequence = Math.max(savedSequence, targetSequence);
927
+ savedSnapshot = snapshotToSave;
928
+ lastSavedAt = now();
929
+ lastError = void 0;
930
+ if (localSequence <= savedSequence) {
931
+ state = "saved";
932
+ try {
933
+ await options.autosave?.markClean();
934
+ } catch (error) {
935
+ console.error("[remoteSync] autosave.markClean failed:", error);
936
+ }
937
+ } else {
938
+ state = "pending";
939
+ }
940
+ options.onSaved?.({ document: snapshotToSave, version: confirmedVersion });
941
+ emit();
942
+ return true;
943
+ } catch (error) {
944
+ lastError = error;
945
+ state = "error";
946
+ options.onError?.(error);
947
+ emit();
948
+ return false;
949
+ }
950
+ })();
951
+ inFlightPromise = run;
952
+ let ok;
953
+ try {
954
+ ok = await run;
955
+ } finally {
956
+ if (inFlightPromise === run) inFlightPromise = null;
957
+ }
958
+ if (ok && localSequence > savedSequence && debounceTimer === null) {
959
+ return executeSave();
960
+ }
961
+ return ok;
962
+ };
963
+ const scheduleSave = () => {
964
+ clearTimer();
965
+ if (debounceMs <= 0) {
966
+ void executeSave();
967
+ return;
968
+ }
969
+ debounceTimer = setTimeout(() => {
970
+ debounceTimer = null;
971
+ if (!destroyed) {
972
+ void executeSave();
973
+ }
974
+ }, debounceMs);
975
+ };
976
+ const notify = () => {
977
+ if (isApplyingRemote || destroyed) return false;
978
+ const current = cloneSnapshot(doc.getDocument());
979
+ const { changed, diff } = hasChanged(current, baselineSnapshot);
980
+ if (!changed) return false;
981
+ const previousBaseline = baselineSnapshot;
982
+ baselineSnapshot = current;
983
+ localSequence++;
984
+ localVersion = Math.max(localVersion, confirmedVersion) + 1;
985
+ if (state !== "saving") {
986
+ state = "pending";
987
+ }
988
+ emit();
989
+ options.onLocalChange?.({
990
+ document: current,
991
+ baseline: previousBaseline,
992
+ diff,
993
+ version: localVersion
994
+ });
995
+ scheduleSave();
996
+ return true;
997
+ };
998
+ const flush = async () => {
999
+ clearTimer();
1000
+ notify();
1001
+ if (localSequence <= savedSequence && !inFlightPromise) {
1002
+ return true;
1003
+ }
1004
+ return executeSave();
1005
+ };
1006
+ const unsubscribeDoc = doc.subscribe(() => {
1007
+ notify();
1008
+ });
1009
+ return {
1010
+ version: () => localVersion,
1011
+ savedVersion: () => confirmedVersion,
1012
+ dirty: () => localSequence > savedSequence,
1013
+ status: getStatus,
1014
+ baseline: () => baselineSnapshot,
1015
+ notify,
1016
+ applyRemote(fn, remoteVersion) {
1017
+ const wasApplying = isApplyingRemote;
1018
+ const wasDirty = localSequence > savedSequence;
1019
+ isApplyingRemote = true;
1020
+ try {
1021
+ const result = fn();
1022
+ baselineSnapshot = cloneSnapshot(doc.getDocument());
1023
+ if (typeof remoteVersion === "number" && Number.isFinite(remoteVersion)) {
1024
+ confirmedVersion = Math.max(confirmedVersion, remoteVersion);
1025
+ localVersion = Math.max(localVersion, remoteVersion);
1026
+ }
1027
+ if (!wasDirty) {
1028
+ savedSequence = localSequence;
1029
+ savedSnapshot = baselineSnapshot;
1030
+ }
1031
+ emit();
1032
+ return result;
1033
+ } finally {
1034
+ isApplyingRemote = wasApplying;
1035
+ }
1036
+ },
1037
+ resetBaseline(remoteVersion) {
1038
+ clearTimer();
1039
+ baselineSnapshot = cloneSnapshot(doc.getDocument());
1040
+ savedSnapshot = baselineSnapshot;
1041
+ if (typeof remoteVersion === "number" && Number.isFinite(remoteVersion)) {
1042
+ localVersion = remoteVersion;
1043
+ confirmedVersion = remoteVersion;
1044
+ }
1045
+ savedSequence = localSequence;
1046
+ lastError = void 0;
1047
+ state = "idle";
1048
+ emit();
1049
+ },
1050
+ flush,
1051
+ async whenIdle() {
1052
+ while (debounceTimer !== null || inFlightPromise) {
1053
+ if (debounceTimer !== null) {
1054
+ await flush();
1055
+ } else if (inFlightPromise) {
1056
+ await inFlightPromise.catch(() => false);
1057
+ }
1058
+ }
1059
+ },
1060
+ subscribe(listener) {
1061
+ listeners.add(listener);
1062
+ listener(getStatus());
1063
+ return () => {
1064
+ listeners.delete(listener);
1065
+ };
1066
+ },
1067
+ destroy() {
1068
+ destroyed = true;
1069
+ clearTimer();
1070
+ unsubscribeDoc();
1071
+ listeners.clear();
1072
+ }
1073
+ };
1074
+ }
1075
+
757
1076
  // src/persistence/documentStore.ts
758
1077
  function slugifyDocumentName(name) {
759
1078
  const slug = name.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
@@ -1067,6 +1386,8 @@ export {
1067
1386
  createAutosaveController,
1068
1387
  createRevisionTracker,
1069
1388
  createEditorAutosave,
1389
+ EditorRemoteConflictError,
1390
+ createEditorRemoteSync,
1070
1391
  slugifyDocumentName,
1071
1392
  createEditorDocumentStore,
1072
1393
  createDocumentToolProvider
@@ -23,7 +23,7 @@ import { type PaintBrushInput, type PaintBrush } from './brushes.cjs';
23
23
  import { type BrushLibrary, type BrushLibraryFile } from '../brushes/library.cjs';
24
24
  import { type BrushPatch, type UnsettableBrushFacet } from '../brushes/patch.cjs';
25
25
  import type { Brush } from '../brushes/types.cjs';
26
- import type { ColorScheme } from '../color/dual.cjs';
26
+ import { type ColorScheme, type ColorValue } from '../color/dual.cjs';
27
27
  import { type PaintCanvas, type PaintDocumentState, type PaintLayer } from './document.cjs';
28
28
  import { type ImageEncoder, type ImageFormat } from './encode.cjs';
29
29
  import type { PenPoint } from './engine.cjs';
@@ -206,8 +206,8 @@ export interface LiveStrokeUpdate {
206
206
  brushId?: string;
207
207
  /** Brush settings to change (as `setBrush` takes), on top of `brushId` or the stroke's brush. */
208
208
  brush?: Omit<PaintBrushInput, 'id'>;
209
- /** A new colour #rrggbb (replaces gradient or effect paint). */
210
- color?: string;
209
+ /** A new colour #rrggbb, or a light / dark pair (replaces gradient or effect paint). */
210
+ color?: ColorValue;
211
211
  /** Paint: a colour, gradient paint or effect paint; `null` back to the plain colour. */
212
212
  paint?: unknown;
213
213
  /** Another jitter seed (particles, scatter and texture fall differently). */
@@ -275,8 +275,12 @@ export interface PaintPreviewPlacement {
275
275
  export interface PaintStrokeInit {
276
276
  /** Default the active layer. */
277
277
  layerId?: string;
278
- /** Default the editor colour. */
279
- color?: string;
278
+ /**
279
+ * Default the editor colour. A light / dark pair (`DualColor`) is recorded with the stroke
280
+ * (`RecordedStroke.dualColor`): on a stroke layer the document draws its light side and the view
281
+ * the viewer's (`setColorScheme`); on a pixel layer it paints the scheme's side.
282
+ */
283
+ color?: ColorValue;
280
284
  /**
281
285
  * What this stroke paints with, instead of the editor's paint: a colour, gradient paint
282
286
  * (`@pixerate/editor/paint` `PaintSource`), or effect paint (render it first with
@@ -1062,9 +1066,29 @@ export declare class PaintEditorController {
1062
1066
  private brushColour;
1063
1067
  /**
1064
1068
  * The host's colour scheme: a paint colour that came from a brush's light / dark default
1065
- * resolves to the new side. Editor state, not an undo step.
1069
+ * resolves to the new side, and stroke layers show their dual-colour strokes in it. Only the
1070
+ * view changes: the document, history and change listeners don't hear of it.
1066
1071
  */
1067
1072
  setColorScheme(scheme: ColorScheme): void;
1073
+ /**
1074
+ * Stroke layers in the colour scheme: the document keeps every stroke layer drawn with each
1075
+ * stroke's `color` (a dual-colour stroke's light side), so saved and shared documents never
1076
+ * depend on who is looking. The view (display, image exports, the base a pointer stroke paints
1077
+ * on) draws dual-colour strokes in the scheme's side instead. One cached surface per layer,
1078
+ * brought up to date from the strokes that changed since it was drawn.
1079
+ */
1080
+ private schemeViews;
1081
+ private schemeCopies;
1082
+ /** Set while a stroke is painted for the document rather than the view (`runStroke` on a stroke layer): dual colours take this side. */
1083
+ private paintScheme;
1084
+ /** A stroke as the scheme shows it: itself, or (a dual-colour stroke) a copy in the scheme's side. */
1085
+ private schemeStroke;
1086
+ /** A stroke layer's surface in the scheme, or `null` when the document's pixels already show it. */
1087
+ private schemeSurface;
1088
+ /** `layers` with each stroke layer that holds dual-colour strokes shown in the scheme. */
1089
+ private schemeLayers;
1090
+ /** The paint colour's light / dark pair for a stroke with `brushId`, when the colour came from one (a brush's own or default colour, or `setColor`). */
1091
+ private dualPaint;
1068
1092
  /** Locks the paint colour: choosing a brush with a default colour keeps it. Editor state, not an undo step. */
1069
1093
  setColorLocked(locked: boolean): void;
1070
1094
  /**
@@ -1075,8 +1099,12 @@ export declare class PaintEditorController {
1075
1099
  private takeBrushColour;
1076
1100
  /** A brush that always paints its own colour (`colour.own`): that colour, resolved for the scheme now; else `null`. */
1077
1101
  private ownColour;
1078
- /** Sets the paint colour (and goes back to one colour from gradient paint). */
1079
- setColor(color: string): string;
1102
+ /**
1103
+ * Sets the paint colour (and goes back to one colour from gradient paint). A light / dark pair
1104
+ * paints the colour scheme's side, follows `setColorScheme`, and is recorded with each stroke
1105
+ * painted in it. Returns the colour as #rrggbb.
1106
+ */
1107
+ setColor(color: ColorValue): string;
1080
1108
  /**
1081
1109
  * Sets what strokes paint with: one colour (`{ type: 'color', color }` or `#rrggbb`), or gradient
1082
1110
  * paint (`{ type: 'gradient', stops, space?, map?, softness? }`, see `@pixerate/editor/paint`),
@@ -1775,6 +1803,10 @@ export declare class PaintEditorController {
1775
1803
  liveStrokeArea(id: string): RasterRect | null;
1776
1804
  /** Commits a layer's new stroke list with its redrawn pixels, recorded for replays (one undo step). */
1777
1805
  private commitLiveStrokes;
1806
+ /**
1807
+ * The document with a layer's strokes set and its pixels redrawn. With `record` false (the same
1808
+ * strokes drawn for the colour scheme) the pixel lock doesn't apply and no event is recorded for replays.
1809
+ */
1778
1810
  private withLiveStrokes;
1779
1811
  /** Where a stroke painted (document pixels): as learnt when it was painted or drawn, else an estimate from its points and brush. */
1780
1812
  private strokeArea;
@@ -23,7 +23,7 @@ import { type PaintBrushInput, type PaintBrush } from './brushes.js';
23
23
  import { type BrushLibrary, type BrushLibraryFile } from '../brushes/library.js';
24
24
  import { type BrushPatch, type UnsettableBrushFacet } from '../brushes/patch.js';
25
25
  import type { Brush } from '../brushes/types.js';
26
- import type { ColorScheme } from '../color/dual.js';
26
+ import { type ColorScheme, type ColorValue } from '../color/dual.js';
27
27
  import { type PaintCanvas, type PaintDocumentState, type PaintLayer } from './document.js';
28
28
  import { type ImageEncoder, type ImageFormat } from './encode.js';
29
29
  import type { PenPoint } from './engine.js';
@@ -206,8 +206,8 @@ export interface LiveStrokeUpdate {
206
206
  brushId?: string;
207
207
  /** Brush settings to change (as `setBrush` takes), on top of `brushId` or the stroke's brush. */
208
208
  brush?: Omit<PaintBrushInput, 'id'>;
209
- /** A new colour #rrggbb (replaces gradient or effect paint). */
210
- color?: string;
209
+ /** A new colour #rrggbb, or a light / dark pair (replaces gradient or effect paint). */
210
+ color?: ColorValue;
211
211
  /** Paint: a colour, gradient paint or effect paint; `null` back to the plain colour. */
212
212
  paint?: unknown;
213
213
  /** Another jitter seed (particles, scatter and texture fall differently). */
@@ -275,8 +275,12 @@ export interface PaintPreviewPlacement {
275
275
  export interface PaintStrokeInit {
276
276
  /** Default the active layer. */
277
277
  layerId?: string;
278
- /** Default the editor colour. */
279
- color?: string;
278
+ /**
279
+ * Default the editor colour. A light / dark pair (`DualColor`) is recorded with the stroke
280
+ * (`RecordedStroke.dualColor`): on a stroke layer the document draws its light side and the view
281
+ * the viewer's (`setColorScheme`); on a pixel layer it paints the scheme's side.
282
+ */
283
+ color?: ColorValue;
280
284
  /**
281
285
  * What this stroke paints with, instead of the editor's paint: a colour, gradient paint
282
286
  * (`@pixerate/editor/paint` `PaintSource`), or effect paint (render it first with
@@ -1062,9 +1066,29 @@ export declare class PaintEditorController {
1062
1066
  private brushColour;
1063
1067
  /**
1064
1068
  * The host's colour scheme: a paint colour that came from a brush's light / dark default
1065
- * resolves to the new side. Editor state, not an undo step.
1069
+ * resolves to the new side, and stroke layers show their dual-colour strokes in it. Only the
1070
+ * view changes: the document, history and change listeners don't hear of it.
1066
1071
  */
1067
1072
  setColorScheme(scheme: ColorScheme): void;
1073
+ /**
1074
+ * Stroke layers in the colour scheme: the document keeps every stroke layer drawn with each
1075
+ * stroke's `color` (a dual-colour stroke's light side), so saved and shared documents never
1076
+ * depend on who is looking. The view (display, image exports, the base a pointer stroke paints
1077
+ * on) draws dual-colour strokes in the scheme's side instead. One cached surface per layer,
1078
+ * brought up to date from the strokes that changed since it was drawn.
1079
+ */
1080
+ private schemeViews;
1081
+ private schemeCopies;
1082
+ /** Set while a stroke is painted for the document rather than the view (`runStroke` on a stroke layer): dual colours take this side. */
1083
+ private paintScheme;
1084
+ /** A stroke as the scheme shows it: itself, or (a dual-colour stroke) a copy in the scheme's side. */
1085
+ private schemeStroke;
1086
+ /** A stroke layer's surface in the scheme, or `null` when the document's pixels already show it. */
1087
+ private schemeSurface;
1088
+ /** `layers` with each stroke layer that holds dual-colour strokes shown in the scheme. */
1089
+ private schemeLayers;
1090
+ /** The paint colour's light / dark pair for a stroke with `brushId`, when the colour came from one (a brush's own or default colour, or `setColor`). */
1091
+ private dualPaint;
1068
1092
  /** Locks the paint colour: choosing a brush with a default colour keeps it. Editor state, not an undo step. */
1069
1093
  setColorLocked(locked: boolean): void;
1070
1094
  /**
@@ -1075,8 +1099,12 @@ export declare class PaintEditorController {
1075
1099
  private takeBrushColour;
1076
1100
  /** A brush that always paints its own colour (`colour.own`): that colour, resolved for the scheme now; else `null`. */
1077
1101
  private ownColour;
1078
- /** Sets the paint colour (and goes back to one colour from gradient paint). */
1079
- setColor(color: string): string;
1102
+ /**
1103
+ * Sets the paint colour (and goes back to one colour from gradient paint). A light / dark pair
1104
+ * paints the colour scheme's side, follows `setColorScheme`, and is recorded with each stroke
1105
+ * painted in it. Returns the colour as #rrggbb.
1106
+ */
1107
+ setColor(color: ColorValue): string;
1080
1108
  /**
1081
1109
  * Sets what strokes paint with: one colour (`{ type: 'color', color }` or `#rrggbb`), or gradient
1082
1110
  * paint (`{ type: 'gradient', stops, space?, map?, softness? }`, see `@pixerate/editor/paint`),
@@ -1775,6 +1803,10 @@ export declare class PaintEditorController {
1775
1803
  liveStrokeArea(id: string): RasterRect | null;
1776
1804
  /** Commits a layer's new stroke list with its redrawn pixels, recorded for replays (one undo step). */
1777
1805
  private commitLiveStrokes;
1806
+ /**
1807
+ * The document with a layer's strokes set and its pixels redrawn. With `record` false (the same
1808
+ * strokes drawn for the colour scheme) the pixel lock doesn't apply and no event is recorded for replays.
1809
+ */
1778
1810
  private withLiveStrokes;
1779
1811
  /** Where a stroke painted (document pixels): as learnt when it was painted or drawn, else an estimate from its points and brush. */
1780
1812
  private strokeArea;