@mocanvas/editor 4.3.0 → 4.4.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/index.d.ts +28 -5
- package/dist/index.js +56 -16
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
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;
|
|
@@ -8725,7 +8739,16 @@ declare class Editor extends EventEmitter<EditorEvents> {
|
|
|
8725
8739
|
getSharedStyles(): SharedStyleMap;
|
|
8726
8740
|
/** Move shapes by a page-space offset. */
|
|
8727
8741
|
nudgeShapes(ids: readonly ShapeId[], offset: VecLike): this;
|
|
8728
|
-
/**
|
|
8742
|
+
/**
|
|
8743
|
+
* Rotate shapes by `delta` radians around the center of their common page
|
|
8744
|
+
* bounds.
|
|
8745
|
+
*
|
|
8746
|
+
* The resulting rotation is canonicalized into `[0, 2π)`. Without that, an
|
|
8747
|
+
* angle that only ever accumulates leaves two documents that look identical
|
|
8748
|
+
* carrying different numbers — and `rotation` is persisted and sent over the
|
|
8749
|
+
* wire, so a diff, a parity check and a sync merge all see a difference that
|
|
8750
|
+
* is not on screen.
|
|
8751
|
+
*/
|
|
8729
8752
|
rotateShapesBy(ids: readonly ShapeId[], delta: number, center?: VecLike): this;
|
|
8730
8753
|
/** Mirror shapes across the center of their common bounds. Positions flip; geometry is not mirrored. */
|
|
8731
8754
|
flipShapes(ids: readonly ShapeId[], operation: "horizontal" | "vertical"): this;
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
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;
|
|
@@ -9203,7 +9234,16 @@ var Editor = class extends EventEmitter {
|
|
|
9203
9234
|
}
|
|
9204
9235
|
return this.updateShapes(updates);
|
|
9205
9236
|
}
|
|
9206
|
-
/**
|
|
9237
|
+
/**
|
|
9238
|
+
* Rotate shapes by `delta` radians around the center of their common page
|
|
9239
|
+
* bounds.
|
|
9240
|
+
*
|
|
9241
|
+
* The resulting rotation is canonicalized into `[0, 2π)`. Without that, an
|
|
9242
|
+
* angle that only ever accumulates leaves two documents that look identical
|
|
9243
|
+
* carrying different numbers — and `rotation` is persisted and sent over the
|
|
9244
|
+
* wire, so a diff, a parity check and a sync merge all see a difference that
|
|
9245
|
+
* is not on screen.
|
|
9246
|
+
*/
|
|
9207
9247
|
rotateShapesBy(ids, delta, center) {
|
|
9208
9248
|
const shapes = ids.map((id) => this.getShape(id)).filter((s) => !!s && !s.isLocked);
|
|
9209
9249
|
if (shapes.length === 0) return this;
|
|
@@ -9214,7 +9254,7 @@ var Editor = class extends EventEmitter {
|
|
|
9214
9254
|
const m = this.getShapePageTransform(shape);
|
|
9215
9255
|
const newPagePos = Vec.RotWith(new Vec(m.e, m.f), c, delta);
|
|
9216
9256
|
const parentPoint = this.getPointInParentSpace(shape, newPagePos);
|
|
9217
|
-
updates.push({ id: shape.id, type: shape.type, x: parentPoint.x, y: parentPoint.y, rotation: shape.rotation + delta });
|
|
9257
|
+
updates.push({ id: shape.id, type: shape.type, x: parentPoint.x, y: parentPoint.y, rotation: canonicalizeRotation(shape.rotation + delta) });
|
|
9218
9258
|
}
|
|
9219
9259
|
return this.updateShapes(updates);
|
|
9220
9260
|
}
|