@inditextech/weave-sdk 3.9.2 → 3.10.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/dist/sdk.node.js CHANGED
@@ -4,6 +4,7 @@ import pino from "pino";
4
4
  import { WEAVE_ASYNC_STATUS, WEAVE_AWARENESS_LAYER_ID, WEAVE_EXPORT_BACKGROUND_COLOR, WEAVE_EXPORT_FORMATS, WEAVE_EXPORT_RETURN_FORMAT, WEAVE_INSTANCE_STATUS, WEAVE_KONVA_BACKEND, WEAVE_LOG_LEVEL, WEAVE_NODE_CHANGE_TYPE, WEAVE_NODE_CUSTOM_EVENTS, WEAVE_NODE_LAYER_ID, WEAVE_NODE_POSITION, WEAVE_STORE_CONNECTION_STATUS, WEAVE_UTILITY_LAYER_ID } from "@inditextech/weave-types";
5
5
  import { getYjsDoc, getYjsValue, observeDeep, syncedStore } from "@syncedstore/core";
6
6
  import * as Y from "yjs";
7
+ import simplify from "simplify-js";
7
8
  import "konva/lib/types";
8
9
 
9
10
  //#region rolldown:runtime
@@ -17696,7 +17697,8 @@ function moveNodeToContainerNT(instance, node, containerToMove, originalNode, or
17696
17697
  const actualContainerAttrs = nodeActualContainer.getAttrs();
17697
17698
  let layerToMove = void 0;
17698
17699
  if (actualContainerAttrs.id !== containerToMove.getAttrs().id && !invalidOriginsTypes.includes(node.getAttrs().nodeType)) layerToMove = containerToMove;
17699
- if (layerToMove && actualContainerAttrs.id !== layerToMove.getAttrs().id && actualContainerAttrs.id !== layerToMove.getAttrs().containerId) {
17700
+ const nodeHandler = instance.getNodeHandler(node.getAttrs().nodeType);
17701
+ if (layerToMove && actualContainerAttrs.id !== layerToMove.getAttrs().id && actualContainerAttrs.id !== layerToMove.getAttrs().containerId && nodeHandler) {
17700
17702
  const layerToMoveAttrs = layerToMove.getAttrs();
17701
17703
  const nodePos = node.getAbsolutePosition();
17702
17704
  const nodeRotation = node.getAbsoluteRotation();
@@ -17705,7 +17707,6 @@ function moveNodeToContainerNT(instance, node, containerToMove, originalNode, or
17705
17707
  node.rotation(nodeRotation);
17706
17708
  node.x(node.x() - (layerToMoveAttrs.containerOffsetX ?? 0));
17707
17709
  node.y(node.y() - (layerToMoveAttrs.containerOffsetY ?? 0));
17708
- node.destroy();
17709
17710
  const newNode = node.clone();
17710
17711
  instance.emitEvent("onNodeMovedToContainer", {
17711
17712
  node: newNode,
@@ -17713,17 +17714,15 @@ function moveNodeToContainerNT(instance, node, containerToMove, originalNode, or
17713
17714
  originalNode,
17714
17715
  originalContainer
17715
17716
  });
17716
- const nodeHandler = instance.getNodeHandler(node.getAttrs().nodeType);
17717
- if (nodeHandler) {
17718
- const actualNodeState = nodeHandler.serialize(node);
17719
- const newNodeState = nodeHandler.serialize(newNode);
17720
- instance.removeNodeNT(actualNodeState, { emitUserChangeEvent: false });
17721
- instance.addNodeNT(newNodeState, layerToMoveAttrs.id, {
17722
- emitUserChangeEvent: false,
17723
- overrideUserChangeType: WEAVE_NODE_CHANGE_TYPE.UPDATE
17724
- });
17725
- return true;
17726
- }
17717
+ const actualNodeState = nodeHandler.serialize(node);
17718
+ const newNodeState = nodeHandler.serialize(newNode);
17719
+ node.destroy();
17720
+ instance.removeNodeNT(actualNodeState, { emitUserChangeEvent: false });
17721
+ instance.addNodeNT(newNodeState, layerToMoveAttrs.id, {
17722
+ emitUserChangeEvent: false,
17723
+ overrideUserChangeType: WEAVE_NODE_CHANGE_TYPE.UPDATE
17724
+ });
17725
+ return true;
17727
17726
  }
17728
17727
  return false;
17729
17728
  }
@@ -19286,10 +19285,10 @@ var WeaveNodesSelectionPlugin = class extends WeavePlugin {
19286
19285
  }
19287
19286
  const isStage = e.target instanceof Konva.Stage;
19288
19287
  const isTransformer = e.target?.getParent() instanceof Konva.Transformer;
19289
- const isTargetable = e.target.getAttrs().isTargetable !== false;
19288
+ const canBeTargeted = e.target.getAttrs().canBeTargeted !== false;
19290
19289
  const isContainerEmptyArea = e.target.getAttrs().isContainerPrincipal !== void 0 && !e.target.getAttrs().isContainerPrincipal;
19291
19290
  if (isTransformer) return;
19292
- if (!isStage && !isContainerEmptyArea && isTargetable) {
19291
+ if (!isStage && !isContainerEmptyArea && canBeTargeted) {
19293
19292
  this.selecting = false;
19294
19293
  this.stopPanLoop();
19295
19294
  this.hideSelectorArea();
@@ -20401,6 +20400,9 @@ var WeaveNode = class {
20401
20400
  mutexUserId: void 0
20402
20401
  });
20403
20402
  };
20403
+ const isLocked = node.getAttrs().locked ?? false;
20404
+ if (isLocked) node.listening(false);
20405
+ else node.listening(true);
20404
20406
  }
20405
20407
  isNodeSelected(ele) {
20406
20408
  const selectionPlugin = this.instance.getPlugin("nodesSelection");
@@ -20785,27 +20787,27 @@ var WeaveNode = class {
20785
20787
  const activeAction = this.instance.getActiveAction();
20786
20788
  const isNodeSelectionEnabled = this.getSelectionPlugin()?.isEnabled();
20787
20789
  const realNode = this.instance.getInstanceRecursive(node);
20788
- const isTargetable = node.getAttrs().isTargetable !== false;
20789
- const isLocked = node.getAttrs().locked ?? false;
20790
+ const canBeTargeted = realNode.getAttrs().canBeTargeted !== false;
20791
+ const isLocked = realNode.getAttrs().locked ?? false;
20790
20792
  const isMutexLocked = realNode.getAttrs().mutexLocked && realNode.getAttrs().mutexUserId !== user.id;
20791
20793
  if ([MOVE_TOOL_ACTION_NAME].includes(activeAction ?? "")) return false;
20792
20794
  let showHover = false;
20793
20795
  let cancelBubble = false;
20794
- if (isNodeSelectionEnabled && this.isSelecting() && !this.isNodeSelected(realNode) && !this.isPasting() && (isLocked || isMutexLocked)) {
20796
+ if (isNodeSelectionEnabled && this.isSelecting() && !this.isNodeSelected(realNode) && !this.isPasting() && realNode.hasName("node") && (isLocked || isMutexLocked)) {
20795
20797
  stage.container().style.cursor = "default";
20796
20798
  cancelBubble = true;
20797
20799
  }
20798
- if (isNodeSelectionEnabled && this.isSelecting() && !this.isNodeSelected(realNode) && !this.isPasting() && isTargetable && !(isLocked || isMutexLocked) && stage.mode() === WEAVE_STAGE_DEFAULT_MODE) {
20800
+ if (isNodeSelectionEnabled && this.isSelecting() && !this.isNodeSelected(realNode) && !this.isPasting() && canBeTargeted && realNode.hasName("node") && !(isLocked || isMutexLocked) && stage.mode() === WEAVE_STAGE_DEFAULT_MODE) {
20799
20801
  showHover = true;
20800
- stage.container().style.cursor = (typeof node?.defineMousePointer === "function" ? node.defineMousePointer() : null) ?? "pointer";
20802
+ stage.container().style.cursor = (typeof realNode?.defineMousePointer === "function" ? realNode.defineMousePointer() : null) ?? "pointer";
20801
20803
  cancelBubble = true;
20802
20804
  }
20803
- if (isNodeSelectionEnabled && this.isSelecting() && this.isNodeSelected(realNode) && !this.isPasting() && isTargetable && !(isLocked || isMutexLocked) && stage.mode() === WEAVE_STAGE_DEFAULT_MODE) {
20805
+ if (isNodeSelectionEnabled && this.isSelecting() && this.isNodeSelected(realNode) && !this.isPasting() && realNode.hasName("node") && canBeTargeted && !(isLocked || isMutexLocked) && stage.mode() === WEAVE_STAGE_DEFAULT_MODE) {
20804
20806
  showHover = true;
20805
- stage.container().style.cursor = (typeof node?.defineMousePointer === "function" ? node.defineMousePointer() : null) ?? "grab";
20807
+ stage.container().style.cursor = (typeof realNode?.defineMousePointer === "function" ? realNode.defineMousePointer() : null) ?? "grab";
20806
20808
  cancelBubble = true;
20807
20809
  }
20808
- if (!isTargetable) cancelBubble = true;
20810
+ if (!canBeTargeted) cancelBubble = true;
20809
20811
  if (this.isPasting()) {
20810
20812
  stage.container().style.cursor = "crosshair";
20811
20813
  cancelBubble = true;
@@ -20893,7 +20895,12 @@ var WeaveNode = class {
20893
20895
  }
20894
20896
  lock(instance) {
20895
20897
  if (instance.getAttrs().nodeType !== this.getNodeType()) return;
20896
- instance.setAttrs({ locked: true });
20898
+ const isListening = instance.listening();
20899
+ instance.setAttrs({
20900
+ locked: true,
20901
+ listening: false,
20902
+ previousListening: isListening
20903
+ });
20897
20904
  this.instance.updateNode(this.serialize(instance));
20898
20905
  const selectionPlugin = this.getSelectionPlugin();
20899
20906
  if (selectionPlugin) {
@@ -20911,7 +20918,12 @@ var WeaveNode = class {
20911
20918
  let realInstance = instance;
20912
20919
  if (instance.getAttrs().nodeId) realInstance = this.instance.getStage().findOne(`#${instance.getAttrs().nodeId}`);
20913
20920
  if (!realInstance) return;
20914
- realInstance.setAttrs({ locked: false });
20921
+ const previousListening = realInstance.getAttrs().previousListening ?? true;
20922
+ realInstance.setAttrs({
20923
+ locked: false,
20924
+ listening: previousListening,
20925
+ previousListening: void 0
20926
+ });
20915
20927
  this.instance.updateNode(this.serialize(realInstance));
20916
20928
  this.setupDefaultNodeEvents(realInstance);
20917
20929
  const stage = this.instance.getStage();
@@ -22074,7 +22086,7 @@ var WeaveRegisterManager = class {
22074
22086
 
22075
22087
  //#endregion
22076
22088
  //#region package.json
22077
- var version = "3.9.2";
22089
+ var version = "3.10.1";
22078
22090
 
22079
22091
  //#endregion
22080
22092
  //#region src/managers/setup.ts
@@ -24262,6 +24274,7 @@ async function downscaleImageFile(file, ratio) {
24262
24274
  ctx.drawImage(bitmap, 0, 0, width, height);
24263
24275
  return new Promise((resolve) => {
24264
24276
  canvas.toBlob((blob) => resolve(blob), file.type, .9);
24277
+ canvas.remove();
24265
24278
  });
24266
24279
  }
24267
24280
  function getImageSizeFromFile(file) {
@@ -24317,7 +24330,9 @@ const downscaleImageFromURL = (url, options) => {
24317
24330
  canvas.height = height;
24318
24331
  const ctx = canvas.getContext("2d");
24319
24332
  ctx.drawImage(img, 0, 0, width, height);
24320
- resolve(canvas.toDataURL(type));
24333
+ const dataURL = canvas.toDataURL(type);
24334
+ canvas.remove();
24335
+ resolve(dataURL);
24321
24336
  };
24322
24337
  img.onerror = () => {
24323
24338
  reject(new Error("Invalid image", { cause: "InvalidImage" }));
@@ -24379,7 +24394,8 @@ var WeaveStageNode = class extends WeaveNode {
24379
24394
  globalEventsInitialized = false;
24380
24395
  initialize = void 0;
24381
24396
  onRender(props) {
24382
- const stage = new Konva.Stage({
24397
+ let stage = this.instance.getStage();
24398
+ if (!stage) stage = new Konva.Stage({
24383
24399
  ...props,
24384
24400
  mode: "default"
24385
24401
  });
@@ -26429,6 +26445,7 @@ const doPreloadCursors = async (cursorsToPreload, setCursor, getFallbackCursor,
26429
26445
  const ctx = canvas.getContext("2d");
26430
26446
  ctx?.drawImage(img, 0, 0);
26431
26447
  const dataURL = canvas.toDataURL("image/png");
26448
+ canvas.remove();
26432
26449
  const tokens = value.split(" ");
26433
26450
  tokens[0] = `url(${dataURL})`;
26434
26451
  setCursor(state, tokens.join(" "));
@@ -27789,7 +27806,7 @@ var WeaveFrameNode = class extends WeaveNode {
27789
27806
  strokeScaleEnabled: true,
27790
27807
  listening: true,
27791
27808
  draggable: false,
27792
- isTargetable: false
27809
+ canBeTargeted: false
27793
27810
  });
27794
27811
  frameInternal.clip({
27795
27812
  x: -(borderWidth / 2) * frameInternal.scaleX(),
@@ -28050,8 +28067,8 @@ var WeaveStrokeNode = class extends WeaveNode {
28050
28067
  const segLen = Math.hypot(dx, dy) || 1;
28051
28068
  const nx = -dy / segLen;
28052
28069
  const ny = dx / segLen;
28053
- const w0 = baseW * p0.pressure / 2;
28054
- const w1 = baseW * p1.pressure / 2;
28070
+ const w0 = Math.max(baseW * p0.pressure / 2, .5);
28071
+ const w1 = Math.max(baseW * p1.pressure / 2, .5);
28055
28072
  let traveled = 0;
28056
28073
  while (traveled < segLen) {
28057
28074
  const step = Math.min(dashRemaining, segLen - traveled);
@@ -29119,7 +29136,7 @@ var WeaveCommentNode = class extends WeaveNode {
29119
29136
  const commentNode = new Konva.Group({
29120
29137
  ...commentParams,
29121
29138
  name: "comment",
29122
- isTargetable: false,
29139
+ canBeTargeted: false,
29123
29140
  isExpanded: false,
29124
29141
  commentAction: null,
29125
29142
  listening: true,
@@ -29130,7 +29147,7 @@ var WeaveCommentNode = class extends WeaveNode {
29130
29147
  id: `${id}-bg`,
29131
29148
  x: 0,
29132
29149
  y: -heightContracted,
29133
- isTargetable: false,
29150
+ canBeTargeted: false,
29134
29151
  fill: commentParams.userBackgroundColor ?? "#0000FF",
29135
29152
  stroke: this.config.style.stroke,
29136
29153
  strokeWidth: this.config.style.strokeWidth,
@@ -34128,6 +34145,11 @@ const BRUSH_TOOL_DEFAULT_CONFIG = { interpolationSteps: 10 };
34128
34145
  var WeaveBrushToolAction = class extends WeaveAction {
34129
34146
  initialized = false;
34130
34147
  isSpacePressed = false;
34148
+ penActive = false;
34149
+ lastSmoothedPressure = .5;
34150
+ lastPointerPos = null;
34151
+ lastPointerTime = 0;
34152
+ predictedCount = 0;
34131
34153
  onPropsChange = void 0;
34132
34154
  onInit = void 0;
34133
34155
  constructor(params) {
@@ -34157,11 +34179,29 @@ var WeaveBrushToolAction = class extends WeaveAction {
34157
34179
  };
34158
34180
  }
34159
34181
  getEventPressure(e) {
34160
- if (e.evt.pointerType && e.evt.pointerType === "pen") return e.evt.pressure || .5;
34161
- return .5;
34182
+ const now$2 = performance.now();
34183
+ let velocity = 0;
34184
+ if (this.lastPointerPos && now$2 - this.lastPointerTime > 0) {
34185
+ const dx = e.evt.clientX - this.lastPointerPos.x;
34186
+ const dy = e.evt.clientY - this.lastPointerPos.y;
34187
+ velocity = Math.hypot(dx, dy) / (now$2 - this.lastPointerTime) * 1e3;
34188
+ }
34189
+ this.lastPointerPos = {
34190
+ x: e.evt.clientX,
34191
+ y: e.evt.clientY
34192
+ };
34193
+ this.lastPointerTime = now$2;
34194
+ const alpha = Math.min(Math.max(velocity / 1500, .15), .6);
34195
+ let raw;
34196
+ if (e.evt.pointerType === "pen") raw = e.evt.pressure || .5;
34197
+ else raw = .5;
34198
+ this.lastSmoothedPressure = alpha * raw + (1 - alpha) * this.lastSmoothedPressure;
34199
+ return Math.max(this.lastSmoothedPressure, .15);
34162
34200
  }
34163
34201
  setupEvents() {
34164
34202
  const stage = this.instance.getStage();
34203
+ this.prevTouchAction = stage.container().style.touchAction;
34204
+ stage.container().style.touchAction = "none";
34165
34205
  window.addEventListener("keyup", (e) => {
34166
34206
  if (e.code === "Space" && this.instance.getActiveAction() === BRUSH_TOOL_ACTION_NAME) this.isSpacePressed = false;
34167
34207
  }, { signal: this.instance.getEventsController()?.signal });
@@ -34187,6 +34227,8 @@ var WeaveBrushToolAction = class extends WeaveAction {
34187
34227
  if (this.getZoomPlugin()?.isPinching()) return;
34188
34228
  if (this.isSpacePressed) return;
34189
34229
  if (e?.evt?.button !== 0) return;
34230
+ if (e.evt.pointerType === "touch" && this.penActive) return;
34231
+ if (e.evt.pointerType === "pen") this.penActive = true;
34190
34232
  const pointPressure = this.getEventPressure(e);
34191
34233
  this.handleStartStroke(pointPressure);
34192
34234
  e.evt.stopPropagation();
@@ -34197,12 +34239,27 @@ var WeaveBrushToolAction = class extends WeaveAction {
34197
34239
  this.setCursor();
34198
34240
  if (this.state !== BRUSH_TOOL_STATE.DEFINE_STROKE) return;
34199
34241
  if (this.getZoomPlugin()?.isPinching()) return;
34200
- const pointPressure = this.getEventPressure(e);
34201
- this.handleMovement(pointPressure);
34242
+ const coalescedEvents = e.evt.getCoalescedEvents ? e.evt.getCoalescedEvents() : [];
34243
+ if (coalescedEvents.length > 1) {
34244
+ for (const ce of coalescedEvents) {
34245
+ const pointPressure = ce.pointerType === "pen" && typeof ce.pressure === "number" ? ce.pressure : .5;
34246
+ this.handleMovement(pointPressure, void 0, false);
34247
+ }
34248
+ const predictedEvents = e.evt.getPredictedEvents ? e.evt.getPredictedEvents() : [];
34249
+ if (predictedEvents.length > 0) {
34250
+ const last = predictedEvents[predictedEvents.length - 1];
34251
+ const predPressure = last.pointerType === "pen" && typeof last.pressure === "number" ? last.pressure : .5;
34252
+ this.handleMovement(predPressure, last, true);
34253
+ }
34254
+ } else {
34255
+ const pointPressure = this.getEventPressure(e);
34256
+ this.handleMovement(pointPressure, void 0, false);
34257
+ }
34202
34258
  e.evt.stopPropagation();
34203
34259
  };
34204
34260
  stage.on("pointermove", handlePointerMove);
34205
34261
  const handlePointerUp = (e) => {
34262
+ this.penActive = false;
34206
34263
  if (this.state !== BRUSH_TOOL_STATE.DEFINE_STROKE) return;
34207
34264
  if (this.getZoomPlugin()?.isPinching()) return;
34208
34265
  this.handleEndStroke();
@@ -34239,6 +34296,10 @@ var WeaveBrushToolAction = class extends WeaveAction {
34239
34296
  };
34240
34297
  }
34241
34298
  handleStartStroke(pressure) {
34299
+ this.lastSmoothedPressure = .5;
34300
+ this.lastPointerPos = null;
34301
+ this.lastPointerTime = 0;
34302
+ this.predictedCount = 0;
34242
34303
  const { mousePoint, container, measureContainer } = this.instance.getMousePointer();
34243
34304
  this.clickPoint = mousePoint;
34244
34305
  this.container = container;
@@ -34267,17 +34328,25 @@ var WeaveBrushToolAction = class extends WeaveAction {
34267
34328
  }
34268
34329
  this.setState(BRUSH_TOOL_STATE.DEFINE_STROKE);
34269
34330
  }
34270
- handleMovement(pressure) {
34331
+ handleMovement(pressure, predictedEvent, isPredicted = false) {
34271
34332
  if (this.state !== BRUSH_TOOL_STATE.DEFINE_STROKE) return;
34333
+ const stage = this.instance.getStage();
34272
34334
  const tempStroke = this.instance.getStage().findOne(`#${this.strokeId}`);
34273
34335
  if (this.measureContainer && tempStroke) {
34336
+ if (predictedEvent) stage.setPointersPositions(predictedEvent);
34274
34337
  const { mousePoint } = this.instance.getMousePointerRelativeToContainer(this.measureContainer);
34275
34338
  const currentPoint = {
34276
34339
  x: mousePoint.x - tempStroke.x(),
34277
34340
  y: mousePoint.y - tempStroke.y(),
34278
34341
  pressure
34279
34342
  };
34280
- const newStrokeElements = [...tempStroke.getAttrs().strokeElements, currentPoint];
34343
+ let newStrokeElements = [...tempStroke.getAttrs().strokeElements];
34344
+ if (!isPredicted && this.predictedCount > 0) {
34345
+ newStrokeElements = newStrokeElements.slice(0, -1 * this.predictedCount);
34346
+ this.predictedCount = 0;
34347
+ }
34348
+ newStrokeElements.push(currentPoint);
34349
+ if (isPredicted) this.predictedCount++;
34281
34350
  const box = this.getBoundingBox(newStrokeElements);
34282
34351
  tempStroke.setAttrs({
34283
34352
  width: box.width,
@@ -34297,17 +34366,22 @@ var WeaveBrushToolAction = class extends WeaveAction {
34297
34366
  if (nodeHandler) {
34298
34367
  const box = this.getBoundingBox(tempStroke.getAttrs().strokeElements);
34299
34368
  let newStrokeElements = [...tempStroke.getAttrs().strokeElements];
34369
+ if (this.predictedCount > 0) {
34370
+ newStrokeElements = newStrokeElements.slice(0, -1 * this.predictedCount);
34371
+ this.predictedCount = 0;
34372
+ }
34300
34373
  newStrokeElements = newStrokeElements.map((point) => ({
34301
34374
  ...point,
34302
34375
  x: point.x - box.x,
34303
34376
  y: point.y - box.y
34304
34377
  }));
34378
+ const compressedPoints = simplify(newStrokeElements, 1, true);
34305
34379
  tempStroke.setAttrs({
34306
34380
  width: box.width,
34307
34381
  height: box.height,
34308
34382
  x: box.x,
34309
34383
  y: box.y,
34310
- strokeElements: newStrokeElements
34384
+ strokeElements: compressedPoints
34311
34385
  });
34312
34386
  const realNode = this.instance.getStage().findOne(`#${tempStroke.getAttrs().id}`);
34313
34387
  if (realNode) realNode.destroy();
@@ -34343,6 +34417,7 @@ var WeaveBrushToolAction = class extends WeaveAction {
34343
34417
  }
34344
34418
  cleanup() {
34345
34419
  const stage = this.instance.getStage();
34420
+ stage.container().style.touchAction = this.prevTouchAction;
34346
34421
  stage.container().style.cursor = "default";
34347
34422
  this.instance.emitEvent("onAddedBrush");
34348
34423
  const selectionPlugin = this.instance.getPlugin("nodesSelection");
@@ -38612,7 +38687,9 @@ var WeaveStageMinimapPlugin = class extends WeavePlugin {
38612
38687
  skipStroke: true
38613
38688
  });
38614
38689
  if (box.width === 0 || box.height === 0) return;
38615
- const fitScale = Math.min(this.minimapStage.width() / box.width, this.minimapStage.height() / box.height);
38690
+ const width = this.minimapStage?.width();
38691
+ const height = this.minimapStage?.height();
38692
+ const fitScale = Math.min(width / box.width, height / box.height);
38616
38693
  const centerOffset = {
38617
38694
  x: (this.minimapStage.width() - box.width * fitScale) / 2,
38618
38695
  y: (this.minimapStage.height() - box.height * fitScale) / 2