@pixerate/editor-react 0.68.1 → 0.70.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.
@@ -25,9 +25,11 @@ __export(paint_editor_exports, {
25
25
  PaintCanvas: () => PaintCanvas,
26
26
  PaintEditor: () => PaintEditor,
27
27
  PaintLayersPanel: () => PaintLayersPanel,
28
+ PaintPresenceLayer: () => PaintPresenceLayer,
28
29
  PaintSlider: () => PaintSlider,
29
30
  PaintToolbar: () => PaintToolbar,
30
31
  PaperPanel: () => PaperPanel,
32
+ usePaintDirector: () => usePaintDirector,
31
33
  usePaintEditor: () => usePaintEditor,
32
34
  usePaintEditorState: () => usePaintEditorState
33
35
  });
@@ -48,12 +50,19 @@ function usePaintEditorState(controller) {
48
50
  () => controller.getState()
49
51
  );
50
52
  }
53
+ function usePaintDirector(controller, options = {}) {
54
+ const [value] = (0, import_react.useState)(() => {
55
+ const presence = new import_paint_editor.PaintPresence();
56
+ return { presence, director: new import_paint_editor.PaintDirector(controller, presence, options) };
57
+ });
58
+ return value;
59
+ }
51
60
 
52
61
  // src/paint-editor/PaintCanvas.tsx
53
62
  var import_react2 = require("react");
54
63
  var import_paint_editor2 = require("@pixerate/editor/paint-editor");
55
64
  var import_jsx_runtime = require("react/jsx-runtime");
56
- var PaintCanvas = ({ controller, tool = "brush", showGuides = true, fitOnMount = true, onError, className }) => {
65
+ var PaintCanvas = ({ controller, tool = "brush", showGuides = true, fitOnMount = true, onError, presence, className }) => {
57
66
  const state = usePaintEditorState(controller);
58
67
  const viewport = (0, import_react2.useRef)(null);
59
68
  const canvas = (0, import_react2.useRef)(null);
@@ -168,6 +177,7 @@ var PaintCanvas = ({ controller, tool = "brush", showGuides = true, fitOnMount =
168
177
  predicted.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("polyline", { points: predicted.map((p) => `${p.x},${p.y}`).join(" "), fill: "none", stroke: state.color, strokeOpacity: 0.5, strokeWidth: Math.max(1, state.brush.tip.size / 2), strokeLinecap: "round" })
169
178
  ] }),
170
179
  state.guides.references.filter((r) => r.visible).map((r) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ReferencePanel, { controller, id: r.id }, r.id)),
180
+ presence && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PaintPresenceLayer, { presence, view: state.view, canvas: doc, viewport: size }),
171
181
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "absolute bottom-1 right-2 text-[10px] text-slate-500 pointer-events-none", "data-testid": "paint-view-status", children: [
172
182
  Math.round(state.view.zoom * 100),
173
183
  "% \xB7 ",
@@ -179,6 +189,16 @@ var PaintCanvas = ({ controller, tool = "brush", showGuides = true, fitOnMount =
179
189
  }
180
190
  );
181
191
  };
192
+ var PaintPresenceLayer = ({ presence, view, canvas, viewport }) => {
193
+ const { actors } = (0, import_react2.useSyncExternalStore)(presence.subscribe, presence.getState, presence.getState);
194
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "pointer-events-none absolute inset-0 overflow-hidden", "data-paint-presence": "", "aria-hidden": "true", children: actors.map((a) => {
195
+ const p = (0, import_paint_editor2.documentToScreen)(view, canvas, viewport, a.x, a.y);
196
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "absolute", style: { left: p.x, top: p.y }, "data-actor": a.id, children: [
197
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "18", height: "20", viewBox: "0 0 18 20", className: "drop-shadow-md", style: { transform: a.pressed ? "scale(0.85)" : void 0, transformOrigin: "0 0" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M0 0 L0 15 L4.5 10.8 L7.6 17 L10.2 15.8 L7.2 9.7 L13.5 9.4 Z", fill: a.color, stroke: "white", strokeWidth: "1.4", strokeLinejoin: "round" }) }),
198
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "absolute left-4 top-4 w-max max-w-[240px] rounded-md px-2 py-0.5 text-[11px] font-semibold text-white shadow", style: { background: a.color }, children: a.message ?? a.name })
199
+ ] }, a.id);
200
+ }) });
201
+ };
182
202
  var ReferencePanel = ({ controller, id }) => {
183
203
  const state = usePaintEditorState(controller);
184
204
  const ref = state.guides.references.find((r) => r.id === id);
@@ -501,6 +521,10 @@ var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, clas
501
521
  // eslint-disable-next-line react-hooks/exhaustive-deps
502
522
  [controller, brushes, paper, thumbnailWidth, thumbnailHeight]
503
523
  );
524
+ const engine = (0, import_paint_editor4.brushEngineControls)(brush);
525
+ const mc = brush.behavior.multicolor;
526
+ const carried = (0, import_paint_editor4.brushLoadColor)(state.mixerLoad);
527
+ const setColors = (colors) => controller.setBrush({ behavior: { multicolor: { colors } } });
504
528
  const slug = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "brush";
505
529
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: `${panel} ${className ?? ""}`, "data-testid": "brush-panel", children: [
506
530
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "grid grid-cols-2 gap-1", role: "listbox", "aria-label": "Brushes", children: brushes.map((b) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
@@ -532,6 +556,68 @@ var BrushPanel = ({ controller, thumbnailWidth = 112, thumbnailHeight = 32, clas
532
556
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center gap-2", children: [
533
557
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "checkbox", checked: brush.tip.pressureOpacity, onChange: (e) => controller.setBrush({ tip: { pressureOpacity: e.target.checked } }) }),
534
558
  " Pressure \u2192 opacity"
559
+ ] }),
560
+ brush.engine === "dab" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
561
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Blend" }),
562
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("select", { "aria-label": "Brush blend", value: brush.tip.blend, onChange: (e) => controller.setBrush({ tip: { blend: e.target.value } }), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor4.BLEND_MENU_GROUPS.map((g) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("optgroup", { label: g.label, children: g.modes.map((m) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: m.id, title: m.description, children: m.label }, m.id)) }, g.label)) })
563
+ ] })
564
+ ] }),
565
+ engine && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col gap-1.5", "data-testid": "brush-engine", children: [
566
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "font-medium text-slate-300", children: engine.title }),
567
+ engine.sliders.map((c) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor4.readPath)(brush, c.path), onChange: (v) => controller.setBrush((0, import_paint_editor4.patchPath)(brush, c.path, v)) }, c.path)),
568
+ engine.mixingPath && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
569
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Mixing" }),
570
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
571
+ "select",
572
+ {
573
+ "aria-label": "Brush mixing",
574
+ value: engine.mixingPath === "blender.mixing" ? brush.blender.mixing : engine.mixingPath === "smudge.mixing" ? brush.smudge.mixing : brush.behavior.mixing,
575
+ onChange: (e) => controller.setBrush((0, import_paint_editor4.patchBrushValue)(brush, engine.mixingPath, e.target.value)),
576
+ className: "rounded bg-slate-800 px-1 py-0.5",
577
+ children: [
578
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "pigment", children: "Pigment" }),
579
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "rgb", children: "RGB" })
580
+ ]
581
+ }
582
+ )
583
+ ] }),
584
+ engine.dirty && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center gap-2", title: "Keep the colour the brush picked up for the next stroke (needs Pick-up above 0)", children: [
585
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "checkbox", "aria-label": "Dirty brush", checked: brush.behavior.dirty, onChange: (e) => controller.setBrush({ behavior: { dirty: e.target.checked } }) }),
586
+ " Dirty brush"
587
+ ] }),
588
+ engine.multicolor && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col gap-1.5", "data-testid": "multicolor", children: [
589
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
590
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Multi-colour" }),
591
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("select", { "aria-label": "Multi-colour", value: mc.mode, onChange: (e) => controller.setBrush({ behavior: { multicolor: { mode: e.target.value } } }), className: "rounded bg-slate-800 px-1 py-0.5", children: [
592
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "off", children: "Off" }),
593
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "tip", children: "Across the tip" }),
594
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "dab", children: "Per dab" })
595
+ ] })
596
+ ] }),
597
+ mc.mode !== "off" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
598
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-wrap items-center gap-1", "aria-label": "Multi-colour colours", children: [
599
+ mc.colors.map((col, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { className: "flex items-center", children: [
600
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("input", { type: "color", "aria-label": `Multi-colour ${i + 1}`, value: col, onChange: (e) => setColors(mc.colors.map((x, j) => j === i ? e.target.value : x)), className: "h-5 w-6 rounded bg-transparent" }),
601
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", "aria-label": `Remove multi-colour ${i + 1}`, className: "px-0.5 text-slate-500 hover:text-white", onClick: () => setColors(mc.colors.filter((_, j) => j !== i)), children: "\xD7" })
602
+ ] }, i)),
603
+ mc.colors.length < 8 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: button, "aria-label": "Add multi-colour", onClick: () => setColors([...mc.colors.length ? mc.colors : [state.color, state.secondaryColor], state.color].slice(0, 8)), children: "+" }),
604
+ mc.colors.length < 2 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-[10px] text-slate-500", children: "Uses the paint and second colours" })
605
+ ] }),
606
+ mc.mode === "dab" && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("label", { className: "flex items-center justify-between gap-2", children: [
607
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Order" }),
608
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("select", { "aria-label": "Colour order", value: mc.order, onChange: (e) => controller.setBrush({ behavior: { multicolor: { order: e.target.value } } }), className: "rounded bg-slate-800 px-1 py-0.5", children: [
609
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "sequence", children: "In order" }),
610
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("option", { value: "random", children: "Random" })
611
+ ] })
612
+ ] }),
613
+ (0, import_paint_editor4.multiColorSliders)(mc.mode).map((c) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(PaintSlider, { control: c, value: (0, import_paint_editor4.readPath)(brush, c.path), onChange: (v) => controller.setBrush((0, import_paint_editor4.patchPath)(brush, c.path, v)) }, c.path))
614
+ ] })
615
+ ] }),
616
+ engine.load && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex items-center gap-1", children: [
617
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-slate-400", children: "Carries" }),
618
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { "aria-label": "Brush load", title: carried ?? "Clean", className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: carried ?? "transparent" } }),
619
+ brush.engine === "mixer" && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: button, onClick: () => controller.loadMixerBrush(), children: "Load" }),
620
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("button", { type: "button", className: button, onClick: () => controller.cleanMixerBrush(brush.engine === "mixer"), children: "Clean" })
535
621
  ] })
536
622
  ] }),
537
623
  /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-wrap items-center gap-1", children: [
@@ -611,6 +697,9 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
611
697
  const [wheel, setWheel] = (0, import_react7.useState)("ryb");
612
698
  const [kind, setKind] = (0, import_react7.useState)("complementary");
613
699
  const [padTool, setPadTool] = (0, import_react7.useState)("paint");
700
+ const [sample, setSample] = (0, import_react7.useState)(3);
701
+ const [parts, setParts] = (0, import_react7.useState)([1, 1]);
702
+ const [padName, setPadName] = (0, import_react7.useState)("");
614
703
  const padPoints = (0, import_react7.useRef)(null);
615
704
  const ring = (0, import_react7.useMemo)(() => (0, import_paint_editor6.renderColorWheel)(wheelSize, wheel), [wheelSize, wheel]);
616
705
  const color = state.color;
@@ -722,7 +811,7 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
722
811
  onPointerDown: (e) => {
723
812
  const p = padPoint(e);
724
813
  if (padTool === "pick") {
725
- attempt(() => controller.mixingPadPick(p.x, p.y, { setColor: true }));
814
+ attempt(() => controller.mixingPadPick(p.x, p.y, { setColor: true, radius: (0, import_paint_editor6.padSampleRadius)(sample) }));
726
815
  return;
727
816
  }
728
817
  padPoints.current = [p];
@@ -736,7 +825,44 @@ var ColorPanel = ({ controller, wheelSize = 168, mixingPad = true, className })
736
825
  },
737
826
  children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(PixelCanvas, { pixels: pad, label: "Mixing pad pixels", className: "w-full rounded", style: { aspectRatio: `${import_paint_editor6.MIXING_PAD_WIDTH} / ${import_paint_editor6.MIXING_PAD_HEIGHT}` } })
738
827
  }
739
- )
828
+ ),
829
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex flex-wrap items-center gap-1", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("label", { className: "flex items-center gap-1", children: [
830
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-slate-400", children: "Sample" }),
831
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("select", { "aria-label": "Pick sample size", value: sample, onChange: (e) => setSample(Number(e.target.value)), className: "rounded bg-slate-800 px-1 py-0.5", children: import_paint_editor6.PAD_SAMPLE_SIZES.map((n) => /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("option", { value: n, children: n === 1 ? "1 px" : `${n}\xD7${n}` }, n)) })
832
+ ] }) }),
833
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-wrap items-center gap-1", children: [
834
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: color } }),
835
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "number", "aria-label": "Paint colour parts", min: 0, max: 20, value: parts[0], onChange: (e) => setParts([Number(e.target.value), parts[1]]), className: "w-10 rounded bg-slate-800 px-1 py-0.5" }),
836
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "text-slate-500", children: ":" }),
837
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { type: "number", "aria-label": "Second colour parts", min: 0, max: 20, value: parts[1], onChange: (e) => setParts([parts[0], Number(e.target.value)]), className: "w-10 rounded bg-slate-800 px-1 py-0.5" }),
838
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "h-4 w-4 rounded ring-1 ring-slate-700", style: { background: state.secondaryColor } }),
839
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
840
+ "button",
841
+ {
842
+ type: "button",
843
+ className: button,
844
+ title: "Mix the paint and second colours in these parts on the pad, and use the mix",
845
+ onClick: () => attempt(
846
+ () => controller.mixOnPad(
847
+ [
848
+ { color, parts: parts[0] },
849
+ { color: state.secondaryColor, parts: parts[1] }
850
+ ],
851
+ { setColor: true }
852
+ )
853
+ ),
854
+ children: "Mix"
855
+ }
856
+ )
857
+ ] }),
858
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "flex flex-wrap items-center gap-1", children: [
859
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("input", { value: padName, placeholder: "Pad name", "aria-label": "Pad name", onChange: (e) => setPadName(e.target.value), className: "w-24 rounded bg-slate-800 px-1.5 py-0.5" }),
860
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: button, onClick: () => attempt(() => (controller.saveMixingPad(padName || void 0), setPadName(""))), children: "Keep pad" })
861
+ ] }),
862
+ state.savedMixingPads.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("ul", { className: "flex flex-col gap-0.5", "aria-label": "Kept pads", children: state.savedMixingPads.map((p) => /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("li", { className: "flex items-center gap-1", children: [
863
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: `${button} flex-1 text-left`, onClick: () => attempt(() => controller.loadMixingPad(p.id)), children: p.name }),
864
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("button", { type: "button", className: button, "aria-label": `Remove ${p.name}`, onClick: () => attempt(() => controller.removeMixingPad(p.id)), children: "\xD7" })
865
+ ] }, p.id)) })
740
866
  ] }),
741
867
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ErrorLine, { error })
742
868
  ] });
@@ -856,7 +982,7 @@ var PaintLayersPanel = ({ controller, className }) => {
856
982
  // src/paint-editor/PaintEditor.tsx
857
983
  var import_react9 = require("react");
858
984
  var import_jsx_runtime8 = require("react/jsx-runtime");
859
- var PaintEditor = ({ controller, fileName, height = 720, className }) => {
985
+ var PaintEditor = ({ controller, fileName, height = 720, presence, className }) => {
860
986
  const [tool, setTool] = (0, import_react9.useState)("brush");
861
987
  const [tab, setTab] = (0, import_react9.useState)("color");
862
988
  const [error, setError] = (0, import_react9.useState)(null);
@@ -865,7 +991,7 @@ var PaintEditor = ({ controller, fileName, height = 720, className }) => {
865
991
  /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex min-h-0 flex-1 gap-2", children: [
866
992
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(BrushPanel, { controller, className: "w-60 shrink-0 overflow-y-auto pr-1" }),
867
993
  /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col", children: [
868
- /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PaintCanvas, { controller, tool, onError: setError, className: "flex-1 rounded-lg" }),
994
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(PaintCanvas, { controller, tool, onError: setError, presence, className: "flex-1 rounded-lg" }),
869
995
  error && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("p", { role: "alert", className: "mt-1 text-[11px] text-rose-400", children: error })
870
996
  ] }),
871
997
  /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex w-64 shrink-0 flex-col gap-2 overflow-y-auto pr-1", children: [
@@ -884,9 +1010,11 @@ var PaintEditor = ({ controller, fileName, height = 720, className }) => {
884
1010
  PaintCanvas,
885
1011
  PaintEditor,
886
1012
  PaintLayersPanel,
1013
+ PaintPresenceLayer,
887
1014
  PaintSlider,
888
1015
  PaintToolbar,
889
1016
  PaperPanel,
1017
+ usePaintDirector,
890
1018
  usePaintEditor,
891
1019
  usePaintEditorState
892
1020
  });