@mocanvas/editor 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -943,6 +943,24 @@ declare abstract class Geometry2d {
943
943
  ignoreHit(_point: VecLike): boolean;
944
944
  nearestPoint(point: VecLike, _filters?: Geometry2dFilters): Vec;
945
945
  distanceToPoint(point: VecLike, hitInside?: boolean, filters?: Geometry2dFilters): number;
946
+ /**
947
+ * Whether `point` hits this geometry, within `margin`.
948
+ *
949
+ * The interior counts when the shape is **filled**, or when the caller asks
950
+ * for `hitInside` — the two are alternatives, not a requirement and a
951
+ * refinement. `hitInside` means "count the interior of a HOLLOW shape too",
952
+ * which is what a marquee or a drop target wants; it is not the only way the
953
+ * interior is ever reachable.
954
+ *
955
+ * This used to test `hitInside` alone, so a solid rectangle could not be
956
+ * selected by clicking its middle — only its outline answered. Nothing failed
957
+ * and nothing warned: every feature built on hit testing simply returned
958
+ * nothing, which reads as several unrelated bugs rather than one.
959
+ *
960
+ * An unfilled shape still misses in the middle. A hollow rectangle is a frame
961
+ * around empty space, and clicking the space inside it should reach whatever
962
+ * is behind.
963
+ */
946
964
  hitTestPoint(point: VecLike, margin?: number, hitInside?: boolean, _filters?: Geometry2dFilters): boolean;
947
965
  /** Whether `point` lies within the bounding box, which is the cheap pre-test. */
948
966
  isPointInBounds(point: VecLike, margin?: number): boolean;
@@ -8273,10 +8291,6 @@ declare const COLLABORATOR_INACTIVE_TIMEOUT = 60000;
8273
8291
  * all in a raw-ESM browser page, hence the guard rather than a bare read.
8274
8292
  */
8275
8293
  declare function warnOnce(key: string, message: string): void;
8276
- /**
8277
- * The editor: document access, selection, camera, tool dispatch, and the
8278
- * bridge that mirrors the current page into the WASM engine.
8279
- */
8280
8294
  declare class Editor extends EventEmitter<EditorEvents> {
8281
8295
  readonly store: EditorStore;
8282
8296
  readonly engine: EngineBridge;
package/dist/index.js CHANGED
@@ -1696,8 +1696,26 @@ var Geometry2d = class {
1696
1696
  if (hitInside && this.isClosed && this.isFilled && pointInPolygon(point, this.vertices)) return -d;
1697
1697
  return d;
1698
1698
  }
1699
+ /**
1700
+ * Whether `point` hits this geometry, within `margin`.
1701
+ *
1702
+ * The interior counts when the shape is **filled**, or when the caller asks
1703
+ * for `hitInside` — the two are alternatives, not a requirement and a
1704
+ * refinement. `hitInside` means "count the interior of a HOLLOW shape too",
1705
+ * which is what a marquee or a drop target wants; it is not the only way the
1706
+ * interior is ever reachable.
1707
+ *
1708
+ * This used to test `hitInside` alone, so a solid rectangle could not be
1709
+ * selected by clicking its middle — only its outline answered. Nothing failed
1710
+ * and nothing warned: every feature built on hit testing simply returned
1711
+ * nothing, which reads as several unrelated bugs rather than one.
1712
+ *
1713
+ * An unfilled shape still misses in the middle. A hollow rectangle is a frame
1714
+ * around empty space, and clicking the space inside it should reach whatever
1715
+ * is behind.
1716
+ */
1699
1717
  hitTestPoint(point, margin = 0, hitInside = false, _filters) {
1700
- if (hitInside && this.isClosed && pointInPolygon(point, this.vertices)) return true;
1718
+ if ((hitInside || this.isFilled) && this.isClosed && pointInPolygon(point, this.vertices)) return true;
1701
1719
  return Vec.Dist(point, this.nearestPoint(point)) <= margin;
1702
1720
  }
1703
1721
  /** Whether `point` lies within the bounding box, which is the cheap pre-test. */
@@ -1886,7 +1904,7 @@ var TransformedGeometry2d = class extends Geometry2d {
1886
1904
  return d;
1887
1905
  }
1888
1906
  hitTestPoint(point, margin = 0, hitInside = false, filters) {
1889
- if (hitInside && this.isClosed && pointInPolygon(point, this.vertices)) return true;
1907
+ if ((hitInside || this.isFilled) && this.isClosed && pointInPolygon(point, this.vertices)) return true;
1890
1908
  return Vec.Dist(point, this.nearestPoint(point, filters)) <= margin;
1891
1909
  }
1892
1910
  ignoreHit(point) {
@@ -7834,6 +7852,12 @@ function warnOnce(key, message) {
7834
7852
  warnedKeys.add(key);
7835
7853
  console.warn(message);
7836
7854
  }
7855
+ function assertUtilMapPresent(map, name) {
7856
+ if (map) return;
7857
+ throw new Error(
7858
+ `editor.${name} is undefined, which the constructor never leaves it. The usual cause is a subclass redeclaring \`${name}\` without an initializer: at ES2022 that DEFINES the field as undefined after the base constructor ran. Write \`declare readonly ${name}: ...\` instead, or drop the redeclaration.`
7859
+ );
7860
+ }
7837
7861
  var Editor = class extends EventEmitter {
7838
7862
  store;
7839
7863
  engine;
@@ -8415,6 +8439,7 @@ var Editor = class extends EventEmitter {
8415
8439
  */
8416
8440
  getShapeUtil(shape) {
8417
8441
  const type = typeof shape === "string" ? shape : shape.type;
8442
+ assertUtilMapPresent(this.shapeUtils, "shapeUtils");
8418
8443
  const util = this.shapeUtils[type];
8419
8444
  if (!util) throw new Error(`No ShapeUtil registered for type "${type}"`);
8420
8445
  return util;
@@ -8677,29 +8702,34 @@ var Editor = class extends EventEmitter {
8677
8702
  getShapeAtPoint(point, opts = {}) {
8678
8703
  this.flushEngine();
8679
8704
  const margin = (opts.margin ?? this.getHitTestMargin()) / this.getZoomLevel();
8680
- const bits = this.hitFilterBits(opts) | (opts.hitInside ? 0 : 4);
8681
8705
  const filter = this.hitFilter(opts);
8682
- if (!filter) {
8683
- const h = this.engine.hitTest(point.x, point.y, margin, bits);
8706
+ if (!filter && !opts.hitInside) {
8707
+ const h = this.engine.hitTest(point.x, point.y, margin, this.hitFilterBits(opts));
8684
8708
  const id = this.handles.id(h);
8685
8709
  return id ? this.getShape(id) : void 0;
8686
8710
  }
8687
- for (const shape of this.getShapesAtPoint(point, opts)) {
8688
- if (filter(shape)) return shape;
8689
- }
8690
- return void 0;
8711
+ return this.getShapesAtPoint(point, opts)[0];
8691
8712
  }
8692
8713
  /** Shapes under a point, topmost first. */
8693
8714
  getShapesAtPoint(point, opts = {}) {
8694
8715
  this.flushEngine();
8695
8716
  const margin = (opts.margin ?? this.getHitTestMargin()) / this.getZoomLevel();
8696
- const handles = this.engine.queryBox(point.x - margin, point.y - margin, point.x + margin, point.y + margin, 0, this.hitFilterBits(opts));
8697
8717
  const filter = this.hitFilter(opts);
8698
8718
  const out = [];
8699
- for (let i = handles.length - 1; i >= 0; i--) {
8700
- const id = this.handles.id(handles[i]);
8701
- const shape = id ? this.getShape(id) : void 0;
8702
- if (!shape) continue;
8719
+ let candidates;
8720
+ if (opts.hitInside) {
8721
+ candidates = this.getCurrentPageShapesSorted().filter((s) => this.getShapePageBounds(s)?.containsPoint(point, margin));
8722
+ candidates.reverse();
8723
+ } else {
8724
+ const handles = this.engine.queryBox(point.x - margin, point.y - margin, point.x + margin, point.y + margin, 0, this.hitFilterBits(opts));
8725
+ candidates = [];
8726
+ for (let i = handles.length - 1; i >= 0; i--) {
8727
+ const id = this.handles.id(handles[i]);
8728
+ const shape = id ? this.getShape(id) : void 0;
8729
+ if (shape) candidates.push(shape);
8730
+ }
8731
+ }
8732
+ for (const shape of candidates) {
8703
8733
  if (filter && !filter(shape)) continue;
8704
8734
  const local = this.getPointInShapeSpace(shape, point);
8705
8735
  const geo = this.getShapeGeometry(shape);
@@ -8886,6 +8916,7 @@ var Editor = class extends EventEmitter {
8886
8916
  );
8887
8917
  getBindingUtil(binding) {
8888
8918
  const type = typeof binding === "string" ? binding : binding.type;
8919
+ assertUtilMapPresent(this.bindingUtils, "bindingUtils");
8889
8920
  const util = this.bindingUtils[type];
8890
8921
  if (!util) throw new Error(`No BindingUtil registered for type "${type}"`);
8891
8922
  return util;