@scrawl-board/board 0.1.0-beta.3 → 0.1.0-beta.5
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/browser.d.ts +34 -1
- package/dist/browser.js +248 -36
- package/dist/core.d.ts +12 -0
- package/dist/core.js +18 -2
- package/dist/index.d.ts +93 -4
- package/dist/index.js +651 -46
- package/dist/react.d.ts +81 -4
- package/dist/react.js +651 -46
- package/dist/styles.css +62 -0
- package/package.json +1 -1
package/dist/browser.d.ts
CHANGED
|
@@ -591,6 +591,7 @@ interface BoardSnapshot {
|
|
|
591
591
|
readonly zoom: number;
|
|
592
592
|
readonly readOnly: boolean;
|
|
593
593
|
readonly selection: readonly string[];
|
|
594
|
+
readonly focusedItem: FocusedItem | null;
|
|
594
595
|
readonly strokeCount: number;
|
|
595
596
|
readonly objectCount: number;
|
|
596
597
|
readonly canUndo: boolean;
|
|
@@ -601,6 +602,27 @@ interface BoardSnapshot {
|
|
|
601
602
|
readonly collaboration: CollaborationSnapshot;
|
|
602
603
|
};
|
|
603
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* `selection` collapsed into the one thing a selected-object toolbar needs:
|
|
607
|
+
* what's selected, its lock state, and where to anchor above it. `null`
|
|
608
|
+
* when nothing is selected, or when the selection mixes types/objects that
|
|
609
|
+
* don't resolve to a single focus (anything but one object, or several
|
|
610
|
+
* strokes sharing a `clusterId` — a multi-stroke shape).
|
|
611
|
+
*
|
|
612
|
+
* Raw lock fields, not a derived `canUnlock` — this API has no notion of
|
|
613
|
+
* "the local user" to judge that against (see `canUnlockItem` in `../core`,
|
|
614
|
+
* which takes a `userId` the Host already owns). A Custom object's lock
|
|
615
|
+
* shape doesn't carry a holder name, so it always reports `locked: false`
|
|
616
|
+
* here, matching the engine's own internal selection-badge behavior.
|
|
617
|
+
*/
|
|
618
|
+
interface FocusedItem {
|
|
619
|
+
readonly type: "stroke" | "note" | "text" | "table" | "image" | "timer" | "custom";
|
|
620
|
+
readonly id: string;
|
|
621
|
+
readonly locked: boolean;
|
|
622
|
+
readonly lockedBy?: string;
|
|
623
|
+
readonly lockedByName?: string;
|
|
624
|
+
readonly screenPosition?: ScreenPoint;
|
|
625
|
+
}
|
|
604
626
|
interface BoardControllerError {
|
|
605
627
|
source: "controller" | "renderer" | "persistence" | "collaboration";
|
|
606
628
|
code: string;
|
|
@@ -967,6 +989,17 @@ interface BoardController {
|
|
|
967
989
|
add(input: BoardObjectInput): string;
|
|
968
990
|
update(id: string, patch: BoardObjectPatch): void;
|
|
969
991
|
remove(ids: readonly string[]): void;
|
|
992
|
+
/**
|
|
993
|
+
* Clone each given object as a new, unlocked copy offset by a small
|
|
994
|
+
* fixed cascade (matching the Duplicate affordance's established Host
|
|
995
|
+
* convention), as one undoable step. Order-preserving: `result[i]` is
|
|
996
|
+
* the clone of `ids[i]`. Strokes that share a `clusterId` among the
|
|
997
|
+
* given ids get a single fresh shared `clusterId` in the result, so
|
|
998
|
+
* duplicating a whole multi-stroke shape (e.g. an arrow's shaft + head)
|
|
999
|
+
* keeps it one shape — pass every member's id together, not just one.
|
|
1000
|
+
* Unknown ids are silently skipped, matching `remove`'s convention.
|
|
1001
|
+
*/
|
|
1002
|
+
duplicate(ids: readonly string[]): readonly string[];
|
|
970
1003
|
table: {
|
|
971
1004
|
addRow(tableId: string): void;
|
|
972
1005
|
addCol(tableId: string): void;
|
|
@@ -1056,4 +1089,4 @@ type LocalBoard = {
|
|
|
1056
1089
|
declare function createLocalBoard(options: LocalBoardOptions): LocalBoard;
|
|
1057
1090
|
|
|
1058
1091
|
export { AssetResolutionError, STAMPS, SUPPORTED_ASSET_MEDIA_TYPES, assetRef, clampAssetCacheBytes, cloneCustomObject, createBoardController, createLocalBoard, isAssetRef, isStampKind, stampDataUrl };
|
|
1059
|
-
export type { ApplyOpsResult, AssetDiagnostic, AssetIngestRequest, AssetIngestResult, AssetIngestor, AssetKind, AssetPurpose, AssetRef, AssetResolutionErrorCode, AssetResolveRequest, AssetResolveResult, AssetResolver, BoardController, BoardControllerError, BoardEventMap, BoardKeyInput, BoardObject, BoardObjectInput, BoardObjectPatch, BoardPointerInput, BoardScene, BoardSnapshot, BoardStyle, BoardThemeOptions, BoardView, BuiltInTool, CollaborationAdapter, CollaborationReceiver, CollaborationSession, CollaborationSnapshot, CollaboratorIdentity, CommentMarker, ControllerOp, CreateBoardControllerOptions, CustomBoardObject, CustomObjectAddInput, CustomObjectDefinition, CustomTool, CustomToolDefinition, DeepReadonly, DocumentContext, ExtensionCommand, ExtensionDiagnostic, ExtensionHitResult, ExtensionId, ExtensionRequirement, InputModifiers, JsonObject, JsonValue, LoadResult, LocalBoard, LocalBoardOptions, LocalBoardSnapshot, Mat2x3, ObjectDescribeContext, ObjectIntent, ObjectType, PersistenceAdapter, PersistenceDiagnostic, PersistenceSnapshot, PresenceCursor, PresenceUser, PresenceView, QueryableBoardObject, ReadonlyBoardDocument, ReadonlyCustomObject, ReadonlyDocumentChange, SceneEllipse, SceneGroup, SceneImage, ScenePath, SceneRect, SceneText, ScrawlExtension, ScreenPoint, ScreenRect, StampKind, SupportedAssetMediaType, ToolCancelReason, ToolCapabilities, ToolCursor, ToolId };
|
|
1092
|
+
export type { ApplyOpsResult, AssetDiagnostic, AssetIngestRequest, AssetIngestResult, AssetIngestor, AssetKind, AssetPurpose, AssetRef, AssetResolutionErrorCode, AssetResolveRequest, AssetResolveResult, AssetResolver, BoardController, BoardControllerError, BoardEventMap, BoardKeyInput, BoardObject, BoardObjectInput, BoardObjectPatch, BoardPointerInput, BoardScene, BoardSnapshot, BoardStyle, BoardThemeOptions, BoardView, BuiltInTool, CollaborationAdapter, CollaborationReceiver, CollaborationSession, CollaborationSnapshot, CollaboratorIdentity, CommentMarker, ControllerOp, CreateBoardControllerOptions, CustomBoardObject, CustomObjectAddInput, CustomObjectDefinition, CustomTool, CustomToolDefinition, DeepReadonly, DocumentContext, ExtensionCommand, ExtensionDiagnostic, ExtensionHitResult, ExtensionId, ExtensionRequirement, FocusedItem, InputModifiers, JsonObject, JsonValue, LoadResult, LocalBoard, LocalBoardOptions, LocalBoardSnapshot, Mat2x3, ObjectDescribeContext, ObjectIntent, ObjectType, PersistenceAdapter, PersistenceDiagnostic, PersistenceSnapshot, PresenceCursor, PresenceUser, PresenceView, QueryableBoardObject, ReadonlyBoardDocument, ReadonlyCustomObject, ReadonlyDocumentChange, SceneEllipse, SceneGroup, SceneImage, ScenePath, SceneRect, SceneText, ScrawlExtension, ScreenPoint, ScreenRect, StampKind, SupportedAssetMediaType, ToolCancelReason, ToolCapabilities, ToolCursor, ToolId };
|
package/dist/browser.js
CHANGED
|
@@ -123,7 +123,11 @@ function validateStrokes(raw) {
|
|
|
123
123
|
for (let index = 0; index < raw.length; index += 1) {
|
|
124
124
|
const value = raw[index];
|
|
125
125
|
const path = `strokes[${index}]`;
|
|
126
|
-
if (!isRecord(value) || !nonempty(value.id) || !nonempty(value.color) || !positive(value.baseWidth) || !optionalEnum(value.tool, [
|
|
126
|
+
if (!isRecord(value) || !nonempty(value.id) || !nonempty(value.color) || !positive(value.baseWidth) || !optionalEnum(value.tool, [
|
|
127
|
+
"marker",
|
|
128
|
+
"highlighter",
|
|
129
|
+
"shape"
|
|
130
|
+
]) || !validLock(value) || !optionalString(value.clusterId) || !validMatrix(value.matrix) || !Array.isArray(value.points)) return validation("invalid stroke", path);
|
|
127
131
|
const points = [];
|
|
128
132
|
for (let pointIndex = 0; pointIndex < value.points.length; pointIndex += 1) {
|
|
129
133
|
const point = normalizePoint(value.points[pointIndex]);
|
|
@@ -404,9 +408,21 @@ var HIGHLIGHT_COLORS = {
|
|
|
404
408
|
};
|
|
405
409
|
var NOTE_COLORS = {
|
|
406
410
|
yellow: "#FDE68A",
|
|
411
|
+
amber: "#FCD34D",
|
|
412
|
+
orange: "#FDBA74",
|
|
413
|
+
peach: "#FED7AA",
|
|
414
|
+
red: "#FCA5A5",
|
|
415
|
+
rose: "#FECACA",
|
|
407
416
|
pink: "#FBCFE8",
|
|
417
|
+
magenta: "#F9A8D4",
|
|
418
|
+
purple: "#D8B4FE",
|
|
419
|
+
lavender: "#E9D5FF",
|
|
408
420
|
blue: "#BFDBFE",
|
|
409
|
-
|
|
421
|
+
cornflower: "#93C5FD",
|
|
422
|
+
sky: "#7DD3FC",
|
|
423
|
+
cyan: "#BAE6FD",
|
|
424
|
+
green: "#BBF7D0",
|
|
425
|
+
mint: "#86EFAC"
|
|
410
426
|
};
|
|
411
427
|
var TEXT_DEFAULT_SIZE = 1.8;
|
|
412
428
|
function cloneText(block) {
|
|
@@ -35884,12 +35900,16 @@ var FRAGMENT = `
|
|
|
35884
35900
|
return max(lineAlpha2.x, lineAlpha2.y);
|
|
35885
35901
|
}
|
|
35886
35902
|
|
|
35887
|
-
// Distance from p's nearest grid intersection, in world units.
|
|
35888
|
-
//
|
|
35903
|
+
// Distance from p's nearest grid intersection, in world units. A line's
|
|
35904
|
+
// width reads as visible even at 1px because it spans the whole screen;
|
|
35905
|
+
// an isolated dot at that same 1px radius gets almost entirely eaten by
|
|
35906
|
+
// anti-aliasing and all but disappears. Scaling the radius up keeps a
|
|
35907
|
+
// dot's on-screen weight comparable to a line drawn with the same
|
|
35908
|
+
// uGridLineWidthPx, rather than using it literally as a radius.
|
|
35889
35909
|
float gridDotAlpha(vec2 p) {
|
|
35890
35910
|
vec2 cell = mod(p, uGridSpacing) - uGridSpacing * 0.5;
|
|
35891
35911
|
float dist = length(cell);
|
|
35892
|
-
float radius = uGridLineWidthPx * uWorldPerPixel;
|
|
35912
|
+
float radius = uGridLineWidthPx * uWorldPerPixel * 1.75;
|
|
35893
35913
|
float aa = uWorldPerPixel;
|
|
35894
35914
|
return 1.0 - smoothstep(radius - aa * 0.5, radius + aa * 0.5, dist);
|
|
35895
35915
|
}
|
|
@@ -36466,6 +36486,7 @@ var StrokeRenderer = class {
|
|
|
36466
36486
|
if (mesh) {
|
|
36467
36487
|
mesh.geometry.dispose();
|
|
36468
36488
|
mesh.geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke), handDrawn(stroke));
|
|
36489
|
+
mesh.material = stroke.tool === "highlighter" ? this.materials.highlight(stroke.color) : this.materials.get(stroke.color);
|
|
36469
36490
|
}
|
|
36470
36491
|
}
|
|
36471
36492
|
for (const stroke of change.transformed) {
|
|
@@ -44645,7 +44666,7 @@ function distToSegmentSq(p, a, b) {
|
|
|
44645
44666
|
}
|
|
44646
44667
|
//#endregion
|
|
44647
44668
|
//#region src/interaction/tools/eraserTool.ts
|
|
44648
|
-
var DECAY_PER_PASS =
|
|
44669
|
+
var DECAY_PER_PASS = 1;
|
|
44649
44670
|
/** A point can take another decay pass after this long — rubbing works. */
|
|
44650
44671
|
var REARM_MS = 250;
|
|
44651
44672
|
var EraserTool = class {
|
|
@@ -44739,19 +44760,11 @@ var EraserTool = class {
|
|
|
44739
44760
|
const runs = partitionByErasure(current.points);
|
|
44740
44761
|
const survives = runs.filter((r) => r.survives && r.points.length >= 2);
|
|
44741
44762
|
const keepId = survives.length === 1 && survives[0].points.length === current.points.length;
|
|
44742
|
-
const m = current.matrix;
|
|
44743
44763
|
for (const run of runs) if (run.survives && run.points.length >= 2) after.push({
|
|
44744
44764
|
...current,
|
|
44745
44765
|
id: keepId ? current.id : crypto.randomUUID(),
|
|
44746
44766
|
points: run.points
|
|
44747
44767
|
});
|
|
44748
|
-
else if (!run.survives) {
|
|
44749
|
-
const world = m ? run.points.map((p) => ({
|
|
44750
|
-
...p,
|
|
44751
|
-
...apply(m, p)
|
|
44752
|
-
})) : run.points;
|
|
44753
|
-
this.ctx.renderer.addGhost(world, current.color, current.baseWidth * (m ? avgScale(m) : 1), current.tool !== "shape");
|
|
44754
|
-
}
|
|
44755
44768
|
}
|
|
44756
44769
|
doc.removeStrokes(before.map((s) => s.id));
|
|
44757
44770
|
doc.addStrokes(after.map(cloneStroke));
|
|
@@ -44959,10 +44972,10 @@ function buildShape(kind, a, b, step, constrain) {
|
|
|
44959
44972
|
case "ellipse": return [ellipse(a, targetB, step)];
|
|
44960
44973
|
case "line": return [sampleSegment(a, constrain ? snappedEnd(a, b) : b, step)];
|
|
44961
44974
|
case "arrow": return arrow(a, constrain ? snappedEnd(a, b) : b, step);
|
|
44962
|
-
case "triangle": return [polygon(3, a, targetB, step,
|
|
44975
|
+
case "triangle": return [polygon(3, a, targetB, step, Math.PI / 2)];
|
|
44963
44976
|
case "diamond": return [polygon(4, a, targetB, step, 0)];
|
|
44964
|
-
case "pentagon": return [polygon(5, a, targetB, step,
|
|
44965
|
-
case "hexagon": return [polygon(6, a, targetB, step,
|
|
44977
|
+
case "pentagon": return [polygon(5, a, targetB, step, Math.PI / 2)];
|
|
44978
|
+
case "hexagon": return [polygon(6, a, targetB, step, Math.PI / 2)];
|
|
44966
44979
|
case "octagon": return [polygon(8, a, targetB, step, Math.PI / 8)];
|
|
44967
44980
|
case "star": return [star(5, a, targetB, step)];
|
|
44968
44981
|
case "heart": return [heart(a, targetB, step)];
|
|
@@ -45161,6 +45174,7 @@ var ShapeTool = class {
|
|
|
45161
45174
|
this.ctx.clusters.assign(strokes[0]);
|
|
45162
45175
|
for (const s of strokes) s.clusterId = strokes[0].clusterId;
|
|
45163
45176
|
this.ctx.history.execute(new AddStrokesCommand(strokes));
|
|
45177
|
+
this.ctx.selection.set(strokes.map((s) => s.id));
|
|
45164
45178
|
this.ctx.setTool?.("select");
|
|
45165
45179
|
}
|
|
45166
45180
|
this.ctx.transition("idle");
|
|
@@ -45742,6 +45756,13 @@ var HANDLE_HIT_PX = 12;
|
|
|
45742
45756
|
var ROTATE_OFFSET_PX = 26;
|
|
45743
45757
|
var ROTATE_HIT_PX = 14;
|
|
45744
45758
|
var COLOR = 2450411;
|
|
45759
|
+
/**
|
|
45760
|
+
* Visual-only outset between the selected object and the gizmo's outline/
|
|
45761
|
+
* handles — the object's actual bounding box (used for resize math, see
|
|
45762
|
+
* `anchorPoint`/`handlePoint`'s callers in `selectTool.ts`) never changes;
|
|
45763
|
+
* only where the outline and handle meshes are *drawn* and hit-tested does.
|
|
45764
|
+
*/
|
|
45765
|
+
var GAP_PX = 8;
|
|
45745
45766
|
var SelectionGizmo = class {
|
|
45746
45767
|
constructor(scene) {
|
|
45747
45768
|
this.group = new Group();
|
|
@@ -45756,7 +45777,7 @@ var SelectionGizmo = class {
|
|
|
45756
45777
|
this.outline = new LineLoop(new BufferGeometry(), this.lineMaterial);
|
|
45757
45778
|
this.outline.position.z = GIZMO_Z;
|
|
45758
45779
|
this.outline.frustumCulled = false;
|
|
45759
|
-
const handleGeometry = new
|
|
45780
|
+
const handleGeometry = new CircleGeometry(.5, 20);
|
|
45760
45781
|
const handleMaterial = new MeshBasicMaterial({ color: COLOR });
|
|
45761
45782
|
const makeHandle = () => new Mesh(handleGeometry, handleMaterial);
|
|
45762
45783
|
this.handles = {
|
|
@@ -45784,18 +45805,19 @@ var SelectionGizmo = class {
|
|
|
45784
45805
|
if (!box) return;
|
|
45785
45806
|
this.lineMaterial.color.setHex(locked ? 16096779 : COLOR);
|
|
45786
45807
|
this.rotateHandle.visible = !locked;
|
|
45808
|
+
const visual = this.visualBox(box, wpp);
|
|
45787
45809
|
const positions = new Float32Array([
|
|
45788
|
-
|
|
45789
|
-
|
|
45810
|
+
visual.minX,
|
|
45811
|
+
visual.minY,
|
|
45790
45812
|
GIZMO_Z,
|
|
45791
|
-
|
|
45792
|
-
|
|
45813
|
+
visual.maxX,
|
|
45814
|
+
visual.minY,
|
|
45793
45815
|
GIZMO_Z,
|
|
45794
|
-
|
|
45795
|
-
|
|
45816
|
+
visual.maxX,
|
|
45817
|
+
visual.maxY,
|
|
45796
45818
|
GIZMO_Z,
|
|
45797
|
-
|
|
45798
|
-
|
|
45819
|
+
visual.minX,
|
|
45820
|
+
visual.maxY,
|
|
45799
45821
|
GIZMO_Z
|
|
45800
45822
|
]);
|
|
45801
45823
|
this.outline.geometry.dispose();
|
|
@@ -45807,17 +45829,27 @@ var SelectionGizmo = class {
|
|
|
45807
45829
|
for (const [handle, mesh] of Object.entries(this.handles)) {
|
|
45808
45830
|
mesh.visible = !locked;
|
|
45809
45831
|
if (!locked) {
|
|
45810
|
-
const p = this.handlePoint(handle,
|
|
45832
|
+
const p = this.handlePoint(handle, visual);
|
|
45811
45833
|
mesh.position.set(p.x, p.y, GIZMO_Z);
|
|
45812
45834
|
mesh.scale.set(size, size, 1);
|
|
45813
45835
|
}
|
|
45814
45836
|
}
|
|
45815
45837
|
if (!locked) {
|
|
45816
|
-
const rotate = this.rotatePoint(
|
|
45838
|
+
const rotate = this.rotatePoint(visual, wpp);
|
|
45817
45839
|
this.rotateHandle.position.set(rotate.x, rotate.y, GIZMO_Z);
|
|
45818
45840
|
this.rotateHandle.scale.set(size, size, 1);
|
|
45819
45841
|
}
|
|
45820
45842
|
}
|
|
45843
|
+
/** The object's real bbox outset by `GAP_PX`, screen-constant — where the outline/handles are drawn and hit-tested. Never used for resize math (see `anchorPoint`). */
|
|
45844
|
+
visualBox(box, wpp) {
|
|
45845
|
+
const gap = GAP_PX * wpp;
|
|
45846
|
+
return {
|
|
45847
|
+
minX: box.minX - gap,
|
|
45848
|
+
minY: box.minY - gap,
|
|
45849
|
+
maxX: box.maxX + gap,
|
|
45850
|
+
maxY: box.maxY + gap
|
|
45851
|
+
};
|
|
45852
|
+
}
|
|
45821
45853
|
handlePoint(handle, box) {
|
|
45822
45854
|
const midX = (box.minX + box.maxX) / 2;
|
|
45823
45855
|
const midY = (box.minY + box.maxY) / 2;
|
|
@@ -45905,7 +45937,8 @@ var SelectionGizmo = class {
|
|
|
45905
45937
|
if (!this.box || this.isLocked) return null;
|
|
45906
45938
|
const handleR = HANDLE_HIT_PX * this.wpp;
|
|
45907
45939
|
const edgeSlop = 6 * this.wpp;
|
|
45908
|
-
const
|
|
45940
|
+
const visual = this.visualBox(this.box, this.wpp);
|
|
45941
|
+
const rotate = this.rotatePoint(visual, this.wpp);
|
|
45909
45942
|
if (Math.hypot(p.x - rotate.x, p.y - rotate.y) < ROTATE_HIT_PX * this.wpp) return { kind: "rotate" };
|
|
45910
45943
|
for (const handle of [
|
|
45911
45944
|
"nw",
|
|
@@ -45917,13 +45950,13 @@ var SelectionGizmo = class {
|
|
|
45917
45950
|
"e",
|
|
45918
45951
|
"w"
|
|
45919
45952
|
]) {
|
|
45920
|
-
const hp = this.handlePoint(handle,
|
|
45953
|
+
const hp = this.handlePoint(handle, visual);
|
|
45921
45954
|
if (Math.abs(p.x - hp.x) < handleR && Math.abs(p.y - hp.y) < handleR) return {
|
|
45922
45955
|
kind: "scale",
|
|
45923
45956
|
handle
|
|
45924
45957
|
};
|
|
45925
45958
|
}
|
|
45926
|
-
const { minX, maxX, minY, maxY } =
|
|
45959
|
+
const { minX, maxX, minY, maxY } = visual;
|
|
45927
45960
|
const inX = p.x >= minX - edgeSlop && p.x <= maxX + edgeSlop;
|
|
45928
45961
|
const inY = p.y >= minY - edgeSlop && p.y <= maxY + edgeSlop;
|
|
45929
45962
|
if (inX && Math.abs(p.y - maxY) < edgeSlop) return {
|
|
@@ -45943,7 +45976,7 @@ var SelectionGizmo = class {
|
|
|
45943
45976
|
handle: "w"
|
|
45944
45977
|
};
|
|
45945
45978
|
const pad = 2 * this.wpp;
|
|
45946
|
-
if (p.x >= minX - pad && p.x <= maxX + pad && p.y >= minY - pad && p.y <= maxY + pad) return { kind: "inside" };
|
|
45979
|
+
if (p.x >= this.box.minX - pad && p.x <= this.box.maxX + pad && p.y >= this.box.minY - pad && p.y <= this.box.maxY + pad) return { kind: "inside" };
|
|
45947
45980
|
return null;
|
|
45948
45981
|
}
|
|
45949
45982
|
rotatePoint(box, wpp) {
|
|
@@ -48176,6 +48209,112 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48176
48209
|
})),
|
|
48177
48210
|
...[...document.allCustomObjects()].map((value) => clone(toBoardObject(value)))
|
|
48178
48211
|
];
|
|
48212
|
+
const projectToScreen = (point) => engine ? engine.boardToScreen(point) : {
|
|
48213
|
+
x: (point.x - view.x) * view.zoom,
|
|
48214
|
+
y: (view.y - point.y) * view.zoom
|
|
48215
|
+
};
|
|
48216
|
+
const lockFields = (item) => ({
|
|
48217
|
+
locked: !!item.locked,
|
|
48218
|
+
...item.lockedBy ? { lockedBy: item.lockedBy } : {},
|
|
48219
|
+
...item.lockedByName ? { lockedByName: item.lockedByName } : {}
|
|
48220
|
+
});
|
|
48221
|
+
const lockFieldsMany = (items) => {
|
|
48222
|
+
const locked = items.filter((i) => i.locked);
|
|
48223
|
+
if (locked.length === 0) return { locked: false };
|
|
48224
|
+
const lockedBys = [...new Set(locked.map((i) => i.lockedBy).filter((v) => !!v))];
|
|
48225
|
+
const names = [...new Set(locked.map((i) => i.lockedByName).filter((v) => !!v))];
|
|
48226
|
+
return {
|
|
48227
|
+
locked: true,
|
|
48228
|
+
...lockedBys.length === 1 ? { lockedBy: lockedBys[0] } : {},
|
|
48229
|
+
...names.length === 1 ? { lockedByName: names[0] } : {}
|
|
48230
|
+
};
|
|
48231
|
+
};
|
|
48232
|
+
const strokeClusterAnchor = (strokes) => {
|
|
48233
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
48234
|
+
for (const s of strokes) for (const p of s.points) {
|
|
48235
|
+
const world = s.matrix ? apply(s.matrix, p) : p;
|
|
48236
|
+
minX = Math.min(minX, world.x);
|
|
48237
|
+
maxX = Math.max(maxX, world.x);
|
|
48238
|
+
minY = Math.min(minY, world.y);
|
|
48239
|
+
maxY = Math.max(maxY, world.y);
|
|
48240
|
+
}
|
|
48241
|
+
if (!Number.isFinite(minX)) return projectToScreen({
|
|
48242
|
+
x: 0,
|
|
48243
|
+
y: 0
|
|
48244
|
+
});
|
|
48245
|
+
return projectToScreen({
|
|
48246
|
+
x: (minX + maxX) / 2,
|
|
48247
|
+
y: maxY + 1.4
|
|
48248
|
+
});
|
|
48249
|
+
};
|
|
48250
|
+
/** Per-type anchor formulas mirror `ScrawlEngine.getSelectedItemInfo` exactly, so a Host-built toolbar lands in the same spot the SDK's own would. */
|
|
48251
|
+
const anchorFor = (obj) => {
|
|
48252
|
+
switch (obj.type) {
|
|
48253
|
+
case "note": return projectToScreen({
|
|
48254
|
+
x: obj.x,
|
|
48255
|
+
y: obj.y + obj.size / 2 + 1
|
|
48256
|
+
});
|
|
48257
|
+
case "text": {
|
|
48258
|
+
const { width } = measureTextBlock(obj.text, obj.fontSize);
|
|
48259
|
+
return projectToScreen({
|
|
48260
|
+
x: obj.x + width / 2,
|
|
48261
|
+
y: obj.y + 1
|
|
48262
|
+
});
|
|
48263
|
+
}
|
|
48264
|
+
case "table": {
|
|
48265
|
+
const { width } = measureTable(obj);
|
|
48266
|
+
return projectToScreen({
|
|
48267
|
+
x: obj.x + width / 2,
|
|
48268
|
+
y: obj.y + 1.2
|
|
48269
|
+
});
|
|
48270
|
+
}
|
|
48271
|
+
case "image": return projectToScreen({
|
|
48272
|
+
x: obj.x,
|
|
48273
|
+
y: obj.y + obj.height / 2 + 1.2
|
|
48274
|
+
});
|
|
48275
|
+
case "timer": return projectToScreen({
|
|
48276
|
+
x: obj.x,
|
|
48277
|
+
y: obj.y + obj.size / 2 + 1.2
|
|
48278
|
+
});
|
|
48279
|
+
case "custom": {
|
|
48280
|
+
const b = obj.fallback.bounds;
|
|
48281
|
+
return projectToScreen(apply(obj.transform, {
|
|
48282
|
+
x: b.x + b.width / 2,
|
|
48283
|
+
y: b.y + b.height + 1
|
|
48284
|
+
}));
|
|
48285
|
+
}
|
|
48286
|
+
case "stroke": return strokeClusterAnchor([obj]);
|
|
48287
|
+
}
|
|
48288
|
+
};
|
|
48289
|
+
const computeFocusedItem = () => {
|
|
48290
|
+
if (selection.length === 0) return null;
|
|
48291
|
+
if (selection.length === 1) {
|
|
48292
|
+
const obj = readObject(selection[0]);
|
|
48293
|
+
if (!obj) return null;
|
|
48294
|
+
if (obj.type === "custom") return {
|
|
48295
|
+
type: "custom",
|
|
48296
|
+
id: obj.id,
|
|
48297
|
+
locked: false,
|
|
48298
|
+
screenPosition: anchorFor(obj)
|
|
48299
|
+
};
|
|
48300
|
+
return {
|
|
48301
|
+
type: obj.type,
|
|
48302
|
+
id: obj.id,
|
|
48303
|
+
...lockFields(obj),
|
|
48304
|
+
screenPosition: anchorFor(obj)
|
|
48305
|
+
};
|
|
48306
|
+
}
|
|
48307
|
+
const objects = selection.map(readObject);
|
|
48308
|
+
if (!objects.every((o) => o?.type === "stroke")) return null;
|
|
48309
|
+
const clusterId = objects[0].clusterId;
|
|
48310
|
+
if (!clusterId || !objects.every((o) => o.clusterId === clusterId)) return null;
|
|
48311
|
+
return {
|
|
48312
|
+
type: "stroke",
|
|
48313
|
+
id: objects[0].id,
|
|
48314
|
+
...lockFieldsMany(objects),
|
|
48315
|
+
screenPosition: strokeClusterAnchor(objects)
|
|
48316
|
+
};
|
|
48317
|
+
};
|
|
48179
48318
|
const updateObject = (id, patch) => {
|
|
48180
48319
|
assertMutable();
|
|
48181
48320
|
const before = readObject(id);
|
|
@@ -48217,6 +48356,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48217
48356
|
zoom: view.zoom,
|
|
48218
48357
|
readOnly,
|
|
48219
48358
|
selection: Object.freeze([...selection]),
|
|
48359
|
+
focusedItem: computeFocusedItem(),
|
|
48220
48360
|
strokeCount: [...document.all()].length,
|
|
48221
48361
|
objectCount: allObjects().length,
|
|
48222
48362
|
canUndo: history.canUndo,
|
|
@@ -48631,10 +48771,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48631
48771
|
changed();
|
|
48632
48772
|
},
|
|
48633
48773
|
get: () => ({ ...view }),
|
|
48634
|
-
boardToScreen: (point) =>
|
|
48635
|
-
x: (point.x - view.x) * view.zoom,
|
|
48636
|
-
y: (view.y - point.y) * view.zoom
|
|
48637
|
-
},
|
|
48774
|
+
boardToScreen: (point) => projectToScreen(point),
|
|
48638
48775
|
screenToBoard: (point) => engine ? engine.screenToBoard(point.x, point.y) ?? {
|
|
48639
48776
|
x: point.x,
|
|
48640
48777
|
y: point.y
|
|
@@ -48671,6 +48808,81 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48671
48808
|
update(id, patch) {
|
|
48672
48809
|
updateObject(id, patch);
|
|
48673
48810
|
},
|
|
48811
|
+
duplicate(ids) {
|
|
48812
|
+
assertMutable();
|
|
48813
|
+
const objects = [...new Set(ids)].map(readObject).filter((o) => !!o);
|
|
48814
|
+
if (objects.length === 0) return [];
|
|
48815
|
+
const DX = 1.5;
|
|
48816
|
+
const DY = -1.5;
|
|
48817
|
+
const newClusterIds = /* @__PURE__ */ new Map();
|
|
48818
|
+
const clusterIdFor = (oldClusterId) => {
|
|
48819
|
+
let next = newClusterIds.get(oldClusterId);
|
|
48820
|
+
if (!next) {
|
|
48821
|
+
next = crypto.randomUUID();
|
|
48822
|
+
newClusterIds.set(oldClusterId, next);
|
|
48823
|
+
}
|
|
48824
|
+
return next;
|
|
48825
|
+
};
|
|
48826
|
+
const commands = [];
|
|
48827
|
+
const newIds = [];
|
|
48828
|
+
for (const obj of objects) {
|
|
48829
|
+
const id = createId();
|
|
48830
|
+
newIds.push(id);
|
|
48831
|
+
const rest = { ...obj };
|
|
48832
|
+
delete rest.locked;
|
|
48833
|
+
delete rest.lockedBy;
|
|
48834
|
+
delete rest.lockedByName;
|
|
48835
|
+
let duplicated;
|
|
48836
|
+
if (obj.type === "stroke") {
|
|
48837
|
+
const matrix = obj.matrix ?? [
|
|
48838
|
+
1,
|
|
48839
|
+
0,
|
|
48840
|
+
0,
|
|
48841
|
+
1,
|
|
48842
|
+
0,
|
|
48843
|
+
0
|
|
48844
|
+
];
|
|
48845
|
+
duplicated = {
|
|
48846
|
+
...rest,
|
|
48847
|
+
id,
|
|
48848
|
+
matrix: [
|
|
48849
|
+
matrix[0],
|
|
48850
|
+
matrix[1],
|
|
48851
|
+
matrix[2],
|
|
48852
|
+
matrix[3],
|
|
48853
|
+
matrix[4] + DX,
|
|
48854
|
+
matrix[5] + DY
|
|
48855
|
+
],
|
|
48856
|
+
...obj.clusterId ? { clusterId: clusterIdFor(obj.clusterId) } : {}
|
|
48857
|
+
};
|
|
48858
|
+
} else if (obj.type === "custom") {
|
|
48859
|
+
const t = obj.transform;
|
|
48860
|
+
duplicated = {
|
|
48861
|
+
...rest,
|
|
48862
|
+
id,
|
|
48863
|
+
transform: [
|
|
48864
|
+
t[0],
|
|
48865
|
+
t[1],
|
|
48866
|
+
t[2],
|
|
48867
|
+
t[3],
|
|
48868
|
+
t[4] + DX,
|
|
48869
|
+
t[5] + DY
|
|
48870
|
+
]
|
|
48871
|
+
};
|
|
48872
|
+
delete duplicated.lock;
|
|
48873
|
+
} else duplicated = {
|
|
48874
|
+
...rest,
|
|
48875
|
+
id,
|
|
48876
|
+
x: obj.x + DX,
|
|
48877
|
+
y: obj.y + DY
|
|
48878
|
+
};
|
|
48879
|
+
validateBoardObject(duplicated.type, duplicated);
|
|
48880
|
+
commands.push(addCommand(duplicated.type, duplicated));
|
|
48881
|
+
}
|
|
48882
|
+
executeHistory(() => history.execute(new CommandBatch("duplicate content", commands)));
|
|
48883
|
+
changed();
|
|
48884
|
+
return newIds;
|
|
48885
|
+
},
|
|
48674
48886
|
table: {
|
|
48675
48887
|
addRow(tableId) {
|
|
48676
48888
|
const table = document.getTable(tableId);
|
package/dist/core.d.ts
CHANGED
|
@@ -492,9 +492,21 @@ declare const HIGHLIGHT_COLORS: {
|
|
|
492
492
|
};
|
|
493
493
|
declare const NOTE_COLORS: {
|
|
494
494
|
readonly yellow: "#FDE68A";
|
|
495
|
+
readonly amber: "#FCD34D";
|
|
496
|
+
readonly orange: "#FDBA74";
|
|
497
|
+
readonly peach: "#FED7AA";
|
|
498
|
+
readonly red: "#FCA5A5";
|
|
499
|
+
readonly rose: "#FECACA";
|
|
495
500
|
readonly pink: "#FBCFE8";
|
|
501
|
+
readonly magenta: "#F9A8D4";
|
|
502
|
+
readonly purple: "#D8B4FE";
|
|
503
|
+
readonly lavender: "#E9D5FF";
|
|
496
504
|
readonly blue: "#BFDBFE";
|
|
505
|
+
readonly cornflower: "#93C5FD";
|
|
506
|
+
readonly sky: "#7DD3FC";
|
|
507
|
+
readonly cyan: "#BAE6FD";
|
|
497
508
|
readonly green: "#BBF7D0";
|
|
509
|
+
readonly mint: "#86EFAC";
|
|
498
510
|
};
|
|
499
511
|
/**
|
|
500
512
|
* One collaborator's vote on a note. One per person; toggling removes it.
|
package/dist/core.js
CHANGED
|
@@ -148,7 +148,11 @@ function validateStrokes(raw) {
|
|
|
148
148
|
for (let index = 0; index < raw.length; index += 1) {
|
|
149
149
|
const value = raw[index];
|
|
150
150
|
const path = `strokes[${index}]`;
|
|
151
|
-
if (!isRecord(value) || !nonempty(value.id) || !nonempty(value.color) || !positive(value.baseWidth) || !optionalEnum(value.tool, [
|
|
151
|
+
if (!isRecord(value) || !nonempty(value.id) || !nonempty(value.color) || !positive(value.baseWidth) || !optionalEnum(value.tool, [
|
|
152
|
+
"marker",
|
|
153
|
+
"highlighter",
|
|
154
|
+
"shape"
|
|
155
|
+
]) || !validLock(value) || !optionalString(value.clusterId) || !validMatrix(value.matrix) || !Array.isArray(value.points)) return validation("invalid stroke", path);
|
|
152
156
|
const points = [];
|
|
153
157
|
for (let pointIndex = 0; pointIndex < value.points.length; pointIndex += 1) {
|
|
154
158
|
const point = normalizePoint(value.points[pointIndex]);
|
|
@@ -429,9 +433,21 @@ var HIGHLIGHT_COLORS = {
|
|
|
429
433
|
};
|
|
430
434
|
var NOTE_COLORS = {
|
|
431
435
|
yellow: "#FDE68A",
|
|
436
|
+
amber: "#FCD34D",
|
|
437
|
+
orange: "#FDBA74",
|
|
438
|
+
peach: "#FED7AA",
|
|
439
|
+
red: "#FCA5A5",
|
|
440
|
+
rose: "#FECACA",
|
|
432
441
|
pink: "#FBCFE8",
|
|
442
|
+
magenta: "#F9A8D4",
|
|
443
|
+
purple: "#D8B4FE",
|
|
444
|
+
lavender: "#E9D5FF",
|
|
433
445
|
blue: "#BFDBFE",
|
|
434
|
-
|
|
446
|
+
cornflower: "#93C5FD",
|
|
447
|
+
sky: "#7DD3FC",
|
|
448
|
+
cyan: "#BAE6FD",
|
|
449
|
+
green: "#BBF7D0",
|
|
450
|
+
mint: "#86EFAC"
|
|
435
451
|
};
|
|
436
452
|
var TEXT_DEFAULT_SIZE = 1.8;
|
|
437
453
|
function cloneText(block) {
|