@scrawl-board/board 0.1.0-beta.4 → 0.1.0-beta.6
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 +301 -46
- package/dist/core.d.ts +12 -0
- package/dist/core.js +18 -2
- package/dist/index.d.ts +93 -4
- package/dist/index.js +715 -60
- package/dist/react.d.ts +81 -4
- package/dist/react.js +715 -60
- 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) {
|
|
@@ -36320,13 +36336,26 @@ var INK_Z = .02;
|
|
|
36320
36336
|
var HIGHLIGHT_Z = .016;
|
|
36321
36337
|
/** Ghosts sit under everything so live strokes always read on top. */
|
|
36322
36338
|
var GHOST_Z = .012;
|
|
36339
|
+
/**
|
|
36340
|
+
* A miter join at each centerline point — averaging the incoming and
|
|
36341
|
+
* outgoing segment directions into one width offset — under-covers the
|
|
36342
|
+
* outside of a sharp turn: the two segments' edges diverge there, leaving a
|
|
36343
|
+
* real geometric gap (the board shows through, not just a shader artifact).
|
|
36344
|
+
* Barely visible on opaque ink, but glaring on the highlighter's lower,
|
|
36345
|
+
* multiply-blended opacity, which is why this reads as "goes transparent
|
|
36346
|
+
* at corners" specifically there even though the underlying strip geometry
|
|
36347
|
+
* is shared with ink. Below this turn angle a miter join is a fine
|
|
36348
|
+
* approximation and not worth the extra geometry.
|
|
36349
|
+
*/
|
|
36350
|
+
var JOIN_ANGLE_THRESHOLD = .35;
|
|
36351
|
+
var JOIN_SEGMENTS = 10;
|
|
36323
36352
|
function buildRibbonGeometry(points, baseWidth, z = INK_Z, handDrawn = true) {
|
|
36324
36353
|
const edges = ribbonEdges(points, baseWidth, handDrawn);
|
|
36325
36354
|
const n = edges.length;
|
|
36326
|
-
const positions = new
|
|
36327
|
-
const uvs = new
|
|
36328
|
-
const alphas = new
|
|
36329
|
-
const indices = new
|
|
36355
|
+
const positions = new Array(n * 2 * 3);
|
|
36356
|
+
const uvs = new Array(n * 2 * 2);
|
|
36357
|
+
const alphas = new Array(n * 2);
|
|
36358
|
+
const indices = new Array((n - 1) * 6);
|
|
36330
36359
|
for (let i = 0; i < n; i++) {
|
|
36331
36360
|
const e = edges[i];
|
|
36332
36361
|
const vi = i * 6;
|
|
@@ -36355,11 +36384,45 @@ function buildRibbonGeometry(points, baseWidth, z = INK_Z, handDrawn = true) {
|
|
|
36355
36384
|
indices[ii + 5] = a + 2;
|
|
36356
36385
|
}
|
|
36357
36386
|
}
|
|
36387
|
+
for (let i = 1; i < n - 1; i++) {
|
|
36388
|
+
const prev = points[Math.max(0, i - 1)];
|
|
36389
|
+
const point = points[i];
|
|
36390
|
+
const next = points[Math.min(n - 1, i + 1)];
|
|
36391
|
+
const inX = point.x - prev.x, inY = point.y - prev.y;
|
|
36392
|
+
const outX = next.x - point.x, outY = next.y - point.y;
|
|
36393
|
+
const inLen = Math.hypot(inX, inY) || 1;
|
|
36394
|
+
const outLen = Math.hypot(outX, outY) || 1;
|
|
36395
|
+
const inDx = inX / inLen, inDy = inY / inLen;
|
|
36396
|
+
const outDx = outX / outLen, outDy = outY / outLen;
|
|
36397
|
+
const cos = inDx * outDx + inDy * outDy;
|
|
36398
|
+
if (Math.acos(Math.min(1, Math.max(-1, cos))) < JOIN_ANGLE_THRESHOLD) continue;
|
|
36399
|
+
const radius = (handDrawn ? baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * point.pressure) : baseWidth) / 2;
|
|
36400
|
+
const t = i / (n - 1);
|
|
36401
|
+
const alpha = edges[i].alpha;
|
|
36402
|
+
const side = inDx * outDy - inDy * outDx > 0 ? -1 : 1;
|
|
36403
|
+
const inEdgeX = point.x + side * -inDy * radius, inEdgeY = point.y + side * inDx * radius;
|
|
36404
|
+
const outEdgeX = point.x + side * -outDy * radius, outEdgeY = point.y + side * outDx * radius;
|
|
36405
|
+
const angle1 = Math.atan2(inEdgeY - point.y, inEdgeX - point.x);
|
|
36406
|
+
let sweep = Math.atan2(outEdgeY - point.y, outEdgeX - point.x) - angle1;
|
|
36407
|
+
while (sweep > Math.PI) sweep -= Math.PI * 2;
|
|
36408
|
+
while (sweep < -Math.PI) sweep += Math.PI * 2;
|
|
36409
|
+
const center = positions.length / 3;
|
|
36410
|
+
positions.push(point.x, point.y, z);
|
|
36411
|
+
uvs.push(t, .5);
|
|
36412
|
+
alphas.push(alpha);
|
|
36413
|
+
for (let s = 0; s <= JOIN_SEGMENTS; s++) {
|
|
36414
|
+
const theta = angle1 + sweep * (s / JOIN_SEGMENTS);
|
|
36415
|
+
positions.push(point.x + Math.cos(theta) * radius, point.y + Math.sin(theta) * radius, z);
|
|
36416
|
+
uvs.push(t, .5);
|
|
36417
|
+
alphas.push(alpha);
|
|
36418
|
+
if (s > 0) indices.push(center, center + s, center + s + 1);
|
|
36419
|
+
}
|
|
36420
|
+
}
|
|
36358
36421
|
const geometry = new BufferGeometry();
|
|
36359
|
-
geometry.setAttribute("position", new BufferAttribute(positions, 3));
|
|
36360
|
-
geometry.setAttribute("uv", new BufferAttribute(uvs, 2));
|
|
36361
|
-
geometry.setAttribute("aAlpha", new BufferAttribute(alphas, 1));
|
|
36362
|
-
geometry.setIndex(
|
|
36422
|
+
geometry.setAttribute("position", new BufferAttribute(new Float32Array(positions), 3));
|
|
36423
|
+
geometry.setAttribute("uv", new BufferAttribute(new Float32Array(uvs), 2));
|
|
36424
|
+
geometry.setAttribute("aAlpha", new BufferAttribute(new Float32Array(alphas), 1));
|
|
36425
|
+
geometry.setIndex(indices);
|
|
36363
36426
|
geometry.computeBoundingSphere();
|
|
36364
36427
|
return geometry;
|
|
36365
36428
|
}
|
|
@@ -36470,6 +36533,7 @@ var StrokeRenderer = class {
|
|
|
36470
36533
|
if (mesh) {
|
|
36471
36534
|
mesh.geometry.dispose();
|
|
36472
36535
|
mesh.geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke), handDrawn(stroke));
|
|
36536
|
+
mesh.material = stroke.tool === "highlighter" ? this.materials.highlight(stroke.color) : this.materials.get(stroke.color);
|
|
36473
36537
|
}
|
|
36474
36538
|
}
|
|
36475
36539
|
for (const stroke of change.transformed) {
|
|
@@ -36500,9 +36564,9 @@ function syncMatrix(mesh, stroke) {
|
|
|
36500
36564
|
}
|
|
36501
36565
|
//#endregion
|
|
36502
36566
|
//#region src/renderer/shapes/markerProp.ts
|
|
36503
|
-
var BODY_LENGTH =
|
|
36504
|
-
var BODY_RADIUS = .
|
|
36505
|
-
var TIP_LENGTH = 1.
|
|
36567
|
+
var BODY_LENGTH = 7;
|
|
36568
|
+
var BODY_RADIUS = .55;
|
|
36569
|
+
var TIP_LENGTH = 1.05;
|
|
36506
36570
|
var BASE_TILT = .42;
|
|
36507
36571
|
var VELOCITY_TILT = .22;
|
|
36508
36572
|
var MAX_EXTRA_TILT = .35;
|
|
@@ -36533,12 +36597,12 @@ var MarkerProp = class {
|
|
|
36533
36597
|
const tip = new Mesh(new ConeGeometry(BODY_RADIUS * .55, TIP_LENGTH, 14), tipMaterial);
|
|
36534
36598
|
tip.rotation.x = Math.PI;
|
|
36535
36599
|
tip.position.y = TIP_LENGTH / 2;
|
|
36536
|
-
this.body.position.y =
|
|
36600
|
+
this.body.position.y = 4.55;
|
|
36537
36601
|
const pen = new Group();
|
|
36538
36602
|
pen.add(this.body, tip);
|
|
36539
36603
|
pen.rotation.x = Math.PI / 2;
|
|
36540
36604
|
this.group.add(pen);
|
|
36541
|
-
this.shadow = new Mesh(new CircleGeometry(
|
|
36605
|
+
this.shadow = new Mesh(new CircleGeometry(.9, 24), new MeshBasicMaterial({
|
|
36542
36606
|
color: 0,
|
|
36543
36607
|
transparent: true,
|
|
36544
36608
|
opacity: .13,
|
|
@@ -44649,7 +44713,7 @@ function distToSegmentSq(p, a, b) {
|
|
|
44649
44713
|
}
|
|
44650
44714
|
//#endregion
|
|
44651
44715
|
//#region src/interaction/tools/eraserTool.ts
|
|
44652
|
-
var DECAY_PER_PASS =
|
|
44716
|
+
var DECAY_PER_PASS = 1;
|
|
44653
44717
|
/** A point can take another decay pass after this long — rubbing works. */
|
|
44654
44718
|
var REARM_MS = 250;
|
|
44655
44719
|
var EraserTool = class {
|
|
@@ -44743,19 +44807,11 @@ var EraserTool = class {
|
|
|
44743
44807
|
const runs = partitionByErasure(current.points);
|
|
44744
44808
|
const survives = runs.filter((r) => r.survives && r.points.length >= 2);
|
|
44745
44809
|
const keepId = survives.length === 1 && survives[0].points.length === current.points.length;
|
|
44746
|
-
const m = current.matrix;
|
|
44747
44810
|
for (const run of runs) if (run.survives && run.points.length >= 2) after.push({
|
|
44748
44811
|
...current,
|
|
44749
44812
|
id: keepId ? current.id : crypto.randomUUID(),
|
|
44750
44813
|
points: run.points
|
|
44751
44814
|
});
|
|
44752
|
-
else if (!run.survives) {
|
|
44753
|
-
const world = m ? run.points.map((p) => ({
|
|
44754
|
-
...p,
|
|
44755
|
-
...apply(m, p)
|
|
44756
|
-
})) : run.points;
|
|
44757
|
-
this.ctx.renderer.addGhost(world, current.color, current.baseWidth * (m ? avgScale(m) : 1), current.tool !== "shape");
|
|
44758
|
-
}
|
|
44759
44815
|
}
|
|
44760
44816
|
doc.removeStrokes(before.map((s) => s.id));
|
|
44761
44817
|
doc.addStrokes(after.map(cloneStroke));
|
|
@@ -44963,10 +45019,10 @@ function buildShape(kind, a, b, step, constrain) {
|
|
|
44963
45019
|
case "ellipse": return [ellipse(a, targetB, step)];
|
|
44964
45020
|
case "line": return [sampleSegment(a, constrain ? snappedEnd(a, b) : b, step)];
|
|
44965
45021
|
case "arrow": return arrow(a, constrain ? snappedEnd(a, b) : b, step);
|
|
44966
|
-
case "triangle": return [polygon(3, a, targetB, step,
|
|
45022
|
+
case "triangle": return [polygon(3, a, targetB, step, Math.PI / 2)];
|
|
44967
45023
|
case "diamond": return [polygon(4, a, targetB, step, 0)];
|
|
44968
|
-
case "pentagon": return [polygon(5, a, targetB, step,
|
|
44969
|
-
case "hexagon": return [polygon(6, a, targetB, step,
|
|
45024
|
+
case "pentagon": return [polygon(5, a, targetB, step, Math.PI / 2)];
|
|
45025
|
+
case "hexagon": return [polygon(6, a, targetB, step, Math.PI / 2)];
|
|
44970
45026
|
case "octagon": return [polygon(8, a, targetB, step, Math.PI / 8)];
|
|
44971
45027
|
case "star": return [star(5, a, targetB, step)];
|
|
44972
45028
|
case "heart": return [heart(a, targetB, step)];
|
|
@@ -45165,6 +45221,7 @@ var ShapeTool = class {
|
|
|
45165
45221
|
this.ctx.clusters.assign(strokes[0]);
|
|
45166
45222
|
for (const s of strokes) s.clusterId = strokes[0].clusterId;
|
|
45167
45223
|
this.ctx.history.execute(new AddStrokesCommand(strokes));
|
|
45224
|
+
this.ctx.selection.set(strokes.map((s) => s.id));
|
|
45168
45225
|
this.ctx.setTool?.("select");
|
|
45169
45226
|
}
|
|
45170
45227
|
this.ctx.transition("idle");
|
|
@@ -45746,6 +45803,13 @@ var HANDLE_HIT_PX = 12;
|
|
|
45746
45803
|
var ROTATE_OFFSET_PX = 26;
|
|
45747
45804
|
var ROTATE_HIT_PX = 14;
|
|
45748
45805
|
var COLOR = 2450411;
|
|
45806
|
+
/**
|
|
45807
|
+
* Visual-only outset between the selected object and the gizmo's outline/
|
|
45808
|
+
* handles — the object's actual bounding box (used for resize math, see
|
|
45809
|
+
* `anchorPoint`/`handlePoint`'s callers in `selectTool.ts`) never changes;
|
|
45810
|
+
* only where the outline and handle meshes are *drawn* and hit-tested does.
|
|
45811
|
+
*/
|
|
45812
|
+
var GAP_PX = 8;
|
|
45749
45813
|
var SelectionGizmo = class {
|
|
45750
45814
|
constructor(scene) {
|
|
45751
45815
|
this.group = new Group();
|
|
@@ -45760,7 +45824,7 @@ var SelectionGizmo = class {
|
|
|
45760
45824
|
this.outline = new LineLoop(new BufferGeometry(), this.lineMaterial);
|
|
45761
45825
|
this.outline.position.z = GIZMO_Z;
|
|
45762
45826
|
this.outline.frustumCulled = false;
|
|
45763
|
-
const handleGeometry = new
|
|
45827
|
+
const handleGeometry = new CircleGeometry(.5, 20);
|
|
45764
45828
|
const handleMaterial = new MeshBasicMaterial({ color: COLOR });
|
|
45765
45829
|
const makeHandle = () => new Mesh(handleGeometry, handleMaterial);
|
|
45766
45830
|
this.handles = {
|
|
@@ -45788,18 +45852,19 @@ var SelectionGizmo = class {
|
|
|
45788
45852
|
if (!box) return;
|
|
45789
45853
|
this.lineMaterial.color.setHex(locked ? 16096779 : COLOR);
|
|
45790
45854
|
this.rotateHandle.visible = !locked;
|
|
45855
|
+
const visual = this.visualBox(box, wpp);
|
|
45791
45856
|
const positions = new Float32Array([
|
|
45792
|
-
|
|
45793
|
-
|
|
45857
|
+
visual.minX,
|
|
45858
|
+
visual.minY,
|
|
45794
45859
|
GIZMO_Z,
|
|
45795
|
-
|
|
45796
|
-
|
|
45860
|
+
visual.maxX,
|
|
45861
|
+
visual.minY,
|
|
45797
45862
|
GIZMO_Z,
|
|
45798
|
-
|
|
45799
|
-
|
|
45863
|
+
visual.maxX,
|
|
45864
|
+
visual.maxY,
|
|
45800
45865
|
GIZMO_Z,
|
|
45801
|
-
|
|
45802
|
-
|
|
45866
|
+
visual.minX,
|
|
45867
|
+
visual.maxY,
|
|
45803
45868
|
GIZMO_Z
|
|
45804
45869
|
]);
|
|
45805
45870
|
this.outline.geometry.dispose();
|
|
@@ -45811,17 +45876,27 @@ var SelectionGizmo = class {
|
|
|
45811
45876
|
for (const [handle, mesh] of Object.entries(this.handles)) {
|
|
45812
45877
|
mesh.visible = !locked;
|
|
45813
45878
|
if (!locked) {
|
|
45814
|
-
const p = this.handlePoint(handle,
|
|
45879
|
+
const p = this.handlePoint(handle, visual);
|
|
45815
45880
|
mesh.position.set(p.x, p.y, GIZMO_Z);
|
|
45816
45881
|
mesh.scale.set(size, size, 1);
|
|
45817
45882
|
}
|
|
45818
45883
|
}
|
|
45819
45884
|
if (!locked) {
|
|
45820
|
-
const rotate = this.rotatePoint(
|
|
45885
|
+
const rotate = this.rotatePoint(visual, wpp);
|
|
45821
45886
|
this.rotateHandle.position.set(rotate.x, rotate.y, GIZMO_Z);
|
|
45822
45887
|
this.rotateHandle.scale.set(size, size, 1);
|
|
45823
45888
|
}
|
|
45824
45889
|
}
|
|
45890
|
+
/** 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`). */
|
|
45891
|
+
visualBox(box, wpp) {
|
|
45892
|
+
const gap = GAP_PX * wpp;
|
|
45893
|
+
return {
|
|
45894
|
+
minX: box.minX - gap,
|
|
45895
|
+
minY: box.minY - gap,
|
|
45896
|
+
maxX: box.maxX + gap,
|
|
45897
|
+
maxY: box.maxY + gap
|
|
45898
|
+
};
|
|
45899
|
+
}
|
|
45825
45900
|
handlePoint(handle, box) {
|
|
45826
45901
|
const midX = (box.minX + box.maxX) / 2;
|
|
45827
45902
|
const midY = (box.minY + box.maxY) / 2;
|
|
@@ -45909,7 +45984,8 @@ var SelectionGizmo = class {
|
|
|
45909
45984
|
if (!this.box || this.isLocked) return null;
|
|
45910
45985
|
const handleR = HANDLE_HIT_PX * this.wpp;
|
|
45911
45986
|
const edgeSlop = 6 * this.wpp;
|
|
45912
|
-
const
|
|
45987
|
+
const visual = this.visualBox(this.box, this.wpp);
|
|
45988
|
+
const rotate = this.rotatePoint(visual, this.wpp);
|
|
45913
45989
|
if (Math.hypot(p.x - rotate.x, p.y - rotate.y) < ROTATE_HIT_PX * this.wpp) return { kind: "rotate" };
|
|
45914
45990
|
for (const handle of [
|
|
45915
45991
|
"nw",
|
|
@@ -45921,13 +45997,13 @@ var SelectionGizmo = class {
|
|
|
45921
45997
|
"e",
|
|
45922
45998
|
"w"
|
|
45923
45999
|
]) {
|
|
45924
|
-
const hp = this.handlePoint(handle,
|
|
46000
|
+
const hp = this.handlePoint(handle, visual);
|
|
45925
46001
|
if (Math.abs(p.x - hp.x) < handleR && Math.abs(p.y - hp.y) < handleR) return {
|
|
45926
46002
|
kind: "scale",
|
|
45927
46003
|
handle
|
|
45928
46004
|
};
|
|
45929
46005
|
}
|
|
45930
|
-
const { minX, maxX, minY, maxY } =
|
|
46006
|
+
const { minX, maxX, minY, maxY } = visual;
|
|
45931
46007
|
const inX = p.x >= minX - edgeSlop && p.x <= maxX + edgeSlop;
|
|
45932
46008
|
const inY = p.y >= minY - edgeSlop && p.y <= maxY + edgeSlop;
|
|
45933
46009
|
if (inX && Math.abs(p.y - maxY) < edgeSlop) return {
|
|
@@ -45947,7 +46023,7 @@ var SelectionGizmo = class {
|
|
|
45947
46023
|
handle: "w"
|
|
45948
46024
|
};
|
|
45949
46025
|
const pad = 2 * this.wpp;
|
|
45950
|
-
if (p.x >= minX - pad && p.x <= maxX + pad && p.y >= minY - pad && p.y <= maxY + pad) return { kind: "inside" };
|
|
46026
|
+
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" };
|
|
45951
46027
|
return null;
|
|
45952
46028
|
}
|
|
45953
46029
|
rotatePoint(box, wpp) {
|
|
@@ -48180,6 +48256,112 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48180
48256
|
})),
|
|
48181
48257
|
...[...document.allCustomObjects()].map((value) => clone(toBoardObject(value)))
|
|
48182
48258
|
];
|
|
48259
|
+
const projectToScreen = (point) => engine ? engine.boardToScreen(point) : {
|
|
48260
|
+
x: (point.x - view.x) * view.zoom,
|
|
48261
|
+
y: (view.y - point.y) * view.zoom
|
|
48262
|
+
};
|
|
48263
|
+
const lockFields = (item) => ({
|
|
48264
|
+
locked: !!item.locked,
|
|
48265
|
+
...item.lockedBy ? { lockedBy: item.lockedBy } : {},
|
|
48266
|
+
...item.lockedByName ? { lockedByName: item.lockedByName } : {}
|
|
48267
|
+
});
|
|
48268
|
+
const lockFieldsMany = (items) => {
|
|
48269
|
+
const locked = items.filter((i) => i.locked);
|
|
48270
|
+
if (locked.length === 0) return { locked: false };
|
|
48271
|
+
const lockedBys = [...new Set(locked.map((i) => i.lockedBy).filter((v) => !!v))];
|
|
48272
|
+
const names = [...new Set(locked.map((i) => i.lockedByName).filter((v) => !!v))];
|
|
48273
|
+
return {
|
|
48274
|
+
locked: true,
|
|
48275
|
+
...lockedBys.length === 1 ? { lockedBy: lockedBys[0] } : {},
|
|
48276
|
+
...names.length === 1 ? { lockedByName: names[0] } : {}
|
|
48277
|
+
};
|
|
48278
|
+
};
|
|
48279
|
+
const strokeClusterAnchor = (strokes) => {
|
|
48280
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
48281
|
+
for (const s of strokes) for (const p of s.points) {
|
|
48282
|
+
const world = s.matrix ? apply(s.matrix, p) : p;
|
|
48283
|
+
minX = Math.min(minX, world.x);
|
|
48284
|
+
maxX = Math.max(maxX, world.x);
|
|
48285
|
+
minY = Math.min(minY, world.y);
|
|
48286
|
+
maxY = Math.max(maxY, world.y);
|
|
48287
|
+
}
|
|
48288
|
+
if (!Number.isFinite(minX)) return projectToScreen({
|
|
48289
|
+
x: 0,
|
|
48290
|
+
y: 0
|
|
48291
|
+
});
|
|
48292
|
+
return projectToScreen({
|
|
48293
|
+
x: (minX + maxX) / 2,
|
|
48294
|
+
y: maxY + 1.4
|
|
48295
|
+
});
|
|
48296
|
+
};
|
|
48297
|
+
/** Per-type anchor formulas mirror `ScrawlEngine.getSelectedItemInfo` exactly, so a Host-built toolbar lands in the same spot the SDK's own would. */
|
|
48298
|
+
const anchorFor = (obj) => {
|
|
48299
|
+
switch (obj.type) {
|
|
48300
|
+
case "note": return projectToScreen({
|
|
48301
|
+
x: obj.x,
|
|
48302
|
+
y: obj.y + obj.size / 2 + 1
|
|
48303
|
+
});
|
|
48304
|
+
case "text": {
|
|
48305
|
+
const { width } = measureTextBlock(obj.text, obj.fontSize);
|
|
48306
|
+
return projectToScreen({
|
|
48307
|
+
x: obj.x + width / 2,
|
|
48308
|
+
y: obj.y + 1
|
|
48309
|
+
});
|
|
48310
|
+
}
|
|
48311
|
+
case "table": {
|
|
48312
|
+
const { width } = measureTable(obj);
|
|
48313
|
+
return projectToScreen({
|
|
48314
|
+
x: obj.x + width / 2,
|
|
48315
|
+
y: obj.y + 1.2
|
|
48316
|
+
});
|
|
48317
|
+
}
|
|
48318
|
+
case "image": return projectToScreen({
|
|
48319
|
+
x: obj.x,
|
|
48320
|
+
y: obj.y + obj.height / 2 + 1.2
|
|
48321
|
+
});
|
|
48322
|
+
case "timer": return projectToScreen({
|
|
48323
|
+
x: obj.x,
|
|
48324
|
+
y: obj.y + obj.size / 2 + 1.2
|
|
48325
|
+
});
|
|
48326
|
+
case "custom": {
|
|
48327
|
+
const b = obj.fallback.bounds;
|
|
48328
|
+
return projectToScreen(apply(obj.transform, {
|
|
48329
|
+
x: b.x + b.width / 2,
|
|
48330
|
+
y: b.y + b.height + 1
|
|
48331
|
+
}));
|
|
48332
|
+
}
|
|
48333
|
+
case "stroke": return strokeClusterAnchor([obj]);
|
|
48334
|
+
}
|
|
48335
|
+
};
|
|
48336
|
+
const computeFocusedItem = () => {
|
|
48337
|
+
if (selection.length === 0) return null;
|
|
48338
|
+
if (selection.length === 1) {
|
|
48339
|
+
const obj = readObject(selection[0]);
|
|
48340
|
+
if (!obj) return null;
|
|
48341
|
+
if (obj.type === "custom") return {
|
|
48342
|
+
type: "custom",
|
|
48343
|
+
id: obj.id,
|
|
48344
|
+
locked: false,
|
|
48345
|
+
screenPosition: anchorFor(obj)
|
|
48346
|
+
};
|
|
48347
|
+
return {
|
|
48348
|
+
type: obj.type,
|
|
48349
|
+
id: obj.id,
|
|
48350
|
+
...lockFields(obj),
|
|
48351
|
+
screenPosition: anchorFor(obj)
|
|
48352
|
+
};
|
|
48353
|
+
}
|
|
48354
|
+
const objects = selection.map(readObject);
|
|
48355
|
+
if (!objects.every((o) => o?.type === "stroke")) return null;
|
|
48356
|
+
const clusterId = objects[0].clusterId;
|
|
48357
|
+
if (!clusterId || !objects.every((o) => o.clusterId === clusterId)) return null;
|
|
48358
|
+
return {
|
|
48359
|
+
type: "stroke",
|
|
48360
|
+
id: objects[0].id,
|
|
48361
|
+
...lockFieldsMany(objects),
|
|
48362
|
+
screenPosition: strokeClusterAnchor(objects)
|
|
48363
|
+
};
|
|
48364
|
+
};
|
|
48183
48365
|
const updateObject = (id, patch) => {
|
|
48184
48366
|
assertMutable();
|
|
48185
48367
|
const before = readObject(id);
|
|
@@ -48221,6 +48403,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48221
48403
|
zoom: view.zoom,
|
|
48222
48404
|
readOnly,
|
|
48223
48405
|
selection: Object.freeze([...selection]),
|
|
48406
|
+
focusedItem: computeFocusedItem(),
|
|
48224
48407
|
strokeCount: [...document.all()].length,
|
|
48225
48408
|
objectCount: allObjects().length,
|
|
48226
48409
|
canUndo: history.canUndo,
|
|
@@ -48635,10 +48818,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48635
48818
|
changed();
|
|
48636
48819
|
},
|
|
48637
48820
|
get: () => ({ ...view }),
|
|
48638
|
-
boardToScreen: (point) =>
|
|
48639
|
-
x: (point.x - view.x) * view.zoom,
|
|
48640
|
-
y: (view.y - point.y) * view.zoom
|
|
48641
|
-
},
|
|
48821
|
+
boardToScreen: (point) => projectToScreen(point),
|
|
48642
48822
|
screenToBoard: (point) => engine ? engine.screenToBoard(point.x, point.y) ?? {
|
|
48643
48823
|
x: point.x,
|
|
48644
48824
|
y: point.y
|
|
@@ -48675,6 +48855,81 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48675
48855
|
update(id, patch) {
|
|
48676
48856
|
updateObject(id, patch);
|
|
48677
48857
|
},
|
|
48858
|
+
duplicate(ids) {
|
|
48859
|
+
assertMutable();
|
|
48860
|
+
const objects = [...new Set(ids)].map(readObject).filter((o) => !!o);
|
|
48861
|
+
if (objects.length === 0) return [];
|
|
48862
|
+
const DX = 1.5;
|
|
48863
|
+
const DY = -1.5;
|
|
48864
|
+
const newClusterIds = /* @__PURE__ */ new Map();
|
|
48865
|
+
const clusterIdFor = (oldClusterId) => {
|
|
48866
|
+
let next = newClusterIds.get(oldClusterId);
|
|
48867
|
+
if (!next) {
|
|
48868
|
+
next = crypto.randomUUID();
|
|
48869
|
+
newClusterIds.set(oldClusterId, next);
|
|
48870
|
+
}
|
|
48871
|
+
return next;
|
|
48872
|
+
};
|
|
48873
|
+
const commands = [];
|
|
48874
|
+
const newIds = [];
|
|
48875
|
+
for (const obj of objects) {
|
|
48876
|
+
const id = createId();
|
|
48877
|
+
newIds.push(id);
|
|
48878
|
+
const rest = { ...obj };
|
|
48879
|
+
delete rest.locked;
|
|
48880
|
+
delete rest.lockedBy;
|
|
48881
|
+
delete rest.lockedByName;
|
|
48882
|
+
let duplicated;
|
|
48883
|
+
if (obj.type === "stroke") {
|
|
48884
|
+
const matrix = obj.matrix ?? [
|
|
48885
|
+
1,
|
|
48886
|
+
0,
|
|
48887
|
+
0,
|
|
48888
|
+
1,
|
|
48889
|
+
0,
|
|
48890
|
+
0
|
|
48891
|
+
];
|
|
48892
|
+
duplicated = {
|
|
48893
|
+
...rest,
|
|
48894
|
+
id,
|
|
48895
|
+
matrix: [
|
|
48896
|
+
matrix[0],
|
|
48897
|
+
matrix[1],
|
|
48898
|
+
matrix[2],
|
|
48899
|
+
matrix[3],
|
|
48900
|
+
matrix[4] + DX,
|
|
48901
|
+
matrix[5] + DY
|
|
48902
|
+
],
|
|
48903
|
+
...obj.clusterId ? { clusterId: clusterIdFor(obj.clusterId) } : {}
|
|
48904
|
+
};
|
|
48905
|
+
} else if (obj.type === "custom") {
|
|
48906
|
+
const t = obj.transform;
|
|
48907
|
+
duplicated = {
|
|
48908
|
+
...rest,
|
|
48909
|
+
id,
|
|
48910
|
+
transform: [
|
|
48911
|
+
t[0],
|
|
48912
|
+
t[1],
|
|
48913
|
+
t[2],
|
|
48914
|
+
t[3],
|
|
48915
|
+
t[4] + DX,
|
|
48916
|
+
t[5] + DY
|
|
48917
|
+
]
|
|
48918
|
+
};
|
|
48919
|
+
delete duplicated.lock;
|
|
48920
|
+
} else duplicated = {
|
|
48921
|
+
...rest,
|
|
48922
|
+
id,
|
|
48923
|
+
x: obj.x + DX,
|
|
48924
|
+
y: obj.y + DY
|
|
48925
|
+
};
|
|
48926
|
+
validateBoardObject(duplicated.type, duplicated);
|
|
48927
|
+
commands.push(addCommand(duplicated.type, duplicated));
|
|
48928
|
+
}
|
|
48929
|
+
executeHistory(() => history.execute(new CommandBatch("duplicate content", commands)));
|
|
48930
|
+
changed();
|
|
48931
|
+
return newIds;
|
|
48932
|
+
},
|
|
48678
48933
|
table: {
|
|
48679
48934
|
addRow(tableId) {
|
|
48680
48935
|
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) {
|