@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/index.js
CHANGED
|
@@ -151,7 +151,11 @@ function validateStrokes(raw) {
|
|
|
151
151
|
for (let index = 0; index < raw.length; index += 1) {
|
|
152
152
|
const value = raw[index];
|
|
153
153
|
const path = `strokes[${index}]`;
|
|
154
|
-
if (!isRecord(value) || !nonempty(value.id) || !nonempty(value.color) || !positive(value.baseWidth) || !optionalEnum(value.tool, [
|
|
154
|
+
if (!isRecord(value) || !nonempty(value.id) || !nonempty(value.color) || !positive(value.baseWidth) || !optionalEnum(value.tool, [
|
|
155
|
+
"marker",
|
|
156
|
+
"highlighter",
|
|
157
|
+
"shape"
|
|
158
|
+
]) || !validLock(value) || !optionalString(value.clusterId) || !validMatrix(value.matrix) || !Array.isArray(value.points)) return validation("invalid stroke", path);
|
|
155
159
|
const points = [];
|
|
156
160
|
for (let pointIndex = 0; pointIndex < value.points.length; pointIndex += 1) {
|
|
157
161
|
const point = normalizePoint(value.points[pointIndex]);
|
|
@@ -432,9 +436,21 @@ var HIGHLIGHT_COLORS = {
|
|
|
432
436
|
};
|
|
433
437
|
var NOTE_COLORS = {
|
|
434
438
|
yellow: "#FDE68A",
|
|
439
|
+
amber: "#FCD34D",
|
|
440
|
+
orange: "#FDBA74",
|
|
441
|
+
peach: "#FED7AA",
|
|
442
|
+
red: "#FCA5A5",
|
|
443
|
+
rose: "#FECACA",
|
|
435
444
|
pink: "#FBCFE8",
|
|
445
|
+
magenta: "#F9A8D4",
|
|
446
|
+
purple: "#D8B4FE",
|
|
447
|
+
lavender: "#E9D5FF",
|
|
436
448
|
blue: "#BFDBFE",
|
|
437
|
-
|
|
449
|
+
cornflower: "#93C5FD",
|
|
450
|
+
sky: "#7DD3FC",
|
|
451
|
+
cyan: "#BAE6FD",
|
|
452
|
+
green: "#BBF7D0",
|
|
453
|
+
mint: "#86EFAC"
|
|
438
454
|
};
|
|
439
455
|
var TEXT_DEFAULT_SIZE = 1.8;
|
|
440
456
|
function cloneText(block) {
|
|
@@ -36400,13 +36416,26 @@ var INK_Z = .02;
|
|
|
36400
36416
|
var HIGHLIGHT_Z = .016;
|
|
36401
36417
|
/** Ghosts sit under everything so live strokes always read on top. */
|
|
36402
36418
|
var GHOST_Z = .012;
|
|
36419
|
+
/**
|
|
36420
|
+
* A miter join at each centerline point — averaging the incoming and
|
|
36421
|
+
* outgoing segment directions into one width offset — under-covers the
|
|
36422
|
+
* outside of a sharp turn: the two segments' edges diverge there, leaving a
|
|
36423
|
+
* real geometric gap (the board shows through, not just a shader artifact).
|
|
36424
|
+
* Barely visible on opaque ink, but glaring on the highlighter's lower,
|
|
36425
|
+
* multiply-blended opacity, which is why this reads as "goes transparent
|
|
36426
|
+
* at corners" specifically there even though the underlying strip geometry
|
|
36427
|
+
* is shared with ink. Below this turn angle a miter join is a fine
|
|
36428
|
+
* approximation and not worth the extra geometry.
|
|
36429
|
+
*/
|
|
36430
|
+
var JOIN_ANGLE_THRESHOLD = .35;
|
|
36431
|
+
var JOIN_SEGMENTS = 10;
|
|
36403
36432
|
function buildRibbonGeometry(points, baseWidth, z = INK_Z, handDrawn = true) {
|
|
36404
36433
|
const edges = ribbonEdges(points, baseWidth, handDrawn);
|
|
36405
36434
|
const n = edges.length;
|
|
36406
|
-
const positions = new
|
|
36407
|
-
const uvs = new
|
|
36408
|
-
const alphas = new
|
|
36409
|
-
const indices = new
|
|
36435
|
+
const positions = new Array(n * 2 * 3);
|
|
36436
|
+
const uvs = new Array(n * 2 * 2);
|
|
36437
|
+
const alphas = new Array(n * 2);
|
|
36438
|
+
const indices = new Array((n - 1) * 6);
|
|
36410
36439
|
for (let i = 0; i < n; i++) {
|
|
36411
36440
|
const e = edges[i];
|
|
36412
36441
|
const vi = i * 6;
|
|
@@ -36435,11 +36464,45 @@ function buildRibbonGeometry(points, baseWidth, z = INK_Z, handDrawn = true) {
|
|
|
36435
36464
|
indices[ii + 5] = a + 2;
|
|
36436
36465
|
}
|
|
36437
36466
|
}
|
|
36467
|
+
for (let i = 1; i < n - 1; i++) {
|
|
36468
|
+
const prev = points[Math.max(0, i - 1)];
|
|
36469
|
+
const point = points[i];
|
|
36470
|
+
const next = points[Math.min(n - 1, i + 1)];
|
|
36471
|
+
const inX = point.x - prev.x, inY = point.y - prev.y;
|
|
36472
|
+
const outX = next.x - point.x, outY = next.y - point.y;
|
|
36473
|
+
const inLen = Math.hypot(inX, inY) || 1;
|
|
36474
|
+
const outLen = Math.hypot(outX, outY) || 1;
|
|
36475
|
+
const inDx = inX / inLen, inDy = inY / inLen;
|
|
36476
|
+
const outDx = outX / outLen, outDy = outY / outLen;
|
|
36477
|
+
const cos = inDx * outDx + inDy * outDy;
|
|
36478
|
+
if (Math.acos(Math.min(1, Math.max(-1, cos))) < JOIN_ANGLE_THRESHOLD) continue;
|
|
36479
|
+
const radius = (handDrawn ? baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * point.pressure) : baseWidth) / 2;
|
|
36480
|
+
const t = i / (n - 1);
|
|
36481
|
+
const alpha = edges[i].alpha;
|
|
36482
|
+
const side = inDx * outDy - inDy * outDx > 0 ? -1 : 1;
|
|
36483
|
+
const inEdgeX = point.x + side * -inDy * radius, inEdgeY = point.y + side * inDx * radius;
|
|
36484
|
+
const outEdgeX = point.x + side * -outDy * radius, outEdgeY = point.y + side * outDx * radius;
|
|
36485
|
+
const angle1 = Math.atan2(inEdgeY - point.y, inEdgeX - point.x);
|
|
36486
|
+
let sweep = Math.atan2(outEdgeY - point.y, outEdgeX - point.x) - angle1;
|
|
36487
|
+
while (sweep > Math.PI) sweep -= Math.PI * 2;
|
|
36488
|
+
while (sweep < -Math.PI) sweep += Math.PI * 2;
|
|
36489
|
+
const center = positions.length / 3;
|
|
36490
|
+
positions.push(point.x, point.y, z);
|
|
36491
|
+
uvs.push(t, .5);
|
|
36492
|
+
alphas.push(alpha);
|
|
36493
|
+
for (let s = 0; s <= JOIN_SEGMENTS; s++) {
|
|
36494
|
+
const theta = angle1 + sweep * (s / JOIN_SEGMENTS);
|
|
36495
|
+
positions.push(point.x + Math.cos(theta) * radius, point.y + Math.sin(theta) * radius, z);
|
|
36496
|
+
uvs.push(t, .5);
|
|
36497
|
+
alphas.push(alpha);
|
|
36498
|
+
if (s > 0) indices.push(center, center + s, center + s + 1);
|
|
36499
|
+
}
|
|
36500
|
+
}
|
|
36438
36501
|
const geometry = new BufferGeometry();
|
|
36439
|
-
geometry.setAttribute("position", new BufferAttribute(positions, 3));
|
|
36440
|
-
geometry.setAttribute("uv", new BufferAttribute(uvs, 2));
|
|
36441
|
-
geometry.setAttribute("aAlpha", new BufferAttribute(alphas, 1));
|
|
36442
|
-
geometry.setIndex(
|
|
36502
|
+
geometry.setAttribute("position", new BufferAttribute(new Float32Array(positions), 3));
|
|
36503
|
+
geometry.setAttribute("uv", new BufferAttribute(new Float32Array(uvs), 2));
|
|
36504
|
+
geometry.setAttribute("aAlpha", new BufferAttribute(new Float32Array(alphas), 1));
|
|
36505
|
+
geometry.setIndex(indices);
|
|
36443
36506
|
geometry.computeBoundingSphere();
|
|
36444
36507
|
return geometry;
|
|
36445
36508
|
}
|
|
@@ -36550,6 +36613,7 @@ var StrokeRenderer = class {
|
|
|
36550
36613
|
if (mesh) {
|
|
36551
36614
|
mesh.geometry.dispose();
|
|
36552
36615
|
mesh.geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke), handDrawn(stroke));
|
|
36616
|
+
mesh.material = stroke.tool === "highlighter" ? this.materials.highlight(stroke.color) : this.materials.get(stroke.color);
|
|
36553
36617
|
}
|
|
36554
36618
|
}
|
|
36555
36619
|
for (const stroke of change.transformed) {
|
|
@@ -36580,9 +36644,9 @@ function syncMatrix(mesh, stroke) {
|
|
|
36580
36644
|
}
|
|
36581
36645
|
//#endregion
|
|
36582
36646
|
//#region src/renderer/shapes/markerProp.ts
|
|
36583
|
-
var BODY_LENGTH =
|
|
36584
|
-
var BODY_RADIUS = .
|
|
36585
|
-
var TIP_LENGTH = 1.
|
|
36647
|
+
var BODY_LENGTH = 7;
|
|
36648
|
+
var BODY_RADIUS = .55;
|
|
36649
|
+
var TIP_LENGTH = 1.05;
|
|
36586
36650
|
var BASE_TILT = .42;
|
|
36587
36651
|
var VELOCITY_TILT = .22;
|
|
36588
36652
|
var MAX_EXTRA_TILT = .35;
|
|
@@ -36613,12 +36677,12 @@ var MarkerProp = class {
|
|
|
36613
36677
|
const tip = new Mesh(new ConeGeometry(BODY_RADIUS * .55, TIP_LENGTH, 14), tipMaterial);
|
|
36614
36678
|
tip.rotation.x = Math.PI;
|
|
36615
36679
|
tip.position.y = TIP_LENGTH / 2;
|
|
36616
|
-
this.body.position.y =
|
|
36680
|
+
this.body.position.y = 4.55;
|
|
36617
36681
|
const pen = new Group();
|
|
36618
36682
|
pen.add(this.body, tip);
|
|
36619
36683
|
pen.rotation.x = Math.PI / 2;
|
|
36620
36684
|
this.group.add(pen);
|
|
36621
|
-
this.shadow = new Mesh(new CircleGeometry(
|
|
36685
|
+
this.shadow = new Mesh(new CircleGeometry(.9, 24), new MeshBasicMaterial({
|
|
36622
36686
|
color: 0,
|
|
36623
36687
|
transparent: true,
|
|
36624
36688
|
opacity: .13,
|
|
@@ -44729,7 +44793,7 @@ function distToSegmentSq(p, a, b) {
|
|
|
44729
44793
|
}
|
|
44730
44794
|
//#endregion
|
|
44731
44795
|
//#region src/interaction/tools/eraserTool.ts
|
|
44732
|
-
var DECAY_PER_PASS =
|
|
44796
|
+
var DECAY_PER_PASS = 1;
|
|
44733
44797
|
/** A point can take another decay pass after this long — rubbing works. */
|
|
44734
44798
|
var REARM_MS = 250;
|
|
44735
44799
|
var EraserTool = class {
|
|
@@ -44823,19 +44887,11 @@ var EraserTool = class {
|
|
|
44823
44887
|
const runs = partitionByErasure(current.points);
|
|
44824
44888
|
const survives = runs.filter((r) => r.survives && r.points.length >= 2);
|
|
44825
44889
|
const keepId = survives.length === 1 && survives[0].points.length === current.points.length;
|
|
44826
|
-
const m = current.matrix;
|
|
44827
44890
|
for (const run of runs) if (run.survives && run.points.length >= 2) after.push({
|
|
44828
44891
|
...current,
|
|
44829
44892
|
id: keepId ? current.id : crypto.randomUUID(),
|
|
44830
44893
|
points: run.points
|
|
44831
44894
|
});
|
|
44832
|
-
else if (!run.survives) {
|
|
44833
|
-
const world = m ? run.points.map((p) => ({
|
|
44834
|
-
...p,
|
|
44835
|
-
...apply(m, p)
|
|
44836
|
-
})) : run.points;
|
|
44837
|
-
this.ctx.renderer.addGhost(world, current.color, current.baseWidth * (m ? avgScale(m) : 1), current.tool !== "shape");
|
|
44838
|
-
}
|
|
44839
44895
|
}
|
|
44840
44896
|
doc.removeStrokes(before.map((s) => s.id));
|
|
44841
44897
|
doc.addStrokes(after.map(cloneStroke));
|
|
@@ -45043,10 +45099,10 @@ function buildShape(kind, a, b, step, constrain) {
|
|
|
45043
45099
|
case "ellipse": return [ellipse(a, targetB, step)];
|
|
45044
45100
|
case "line": return [sampleSegment(a, constrain ? snappedEnd(a, b) : b, step)];
|
|
45045
45101
|
case "arrow": return arrow(a, constrain ? snappedEnd(a, b) : b, step);
|
|
45046
|
-
case "triangle": return [polygon(3, a, targetB, step,
|
|
45102
|
+
case "triangle": return [polygon(3, a, targetB, step, Math.PI / 2)];
|
|
45047
45103
|
case "diamond": return [polygon(4, a, targetB, step, 0)];
|
|
45048
|
-
case "pentagon": return [polygon(5, a, targetB, step,
|
|
45049
|
-
case "hexagon": return [polygon(6, a, targetB, step,
|
|
45104
|
+
case "pentagon": return [polygon(5, a, targetB, step, Math.PI / 2)];
|
|
45105
|
+
case "hexagon": return [polygon(6, a, targetB, step, Math.PI / 2)];
|
|
45050
45106
|
case "octagon": return [polygon(8, a, targetB, step, Math.PI / 8)];
|
|
45051
45107
|
case "star": return [star(5, a, targetB, step)];
|
|
45052
45108
|
case "heart": return [heart(a, targetB, step)];
|
|
@@ -45245,6 +45301,7 @@ var ShapeTool = class {
|
|
|
45245
45301
|
this.ctx.clusters.assign(strokes[0]);
|
|
45246
45302
|
for (const s of strokes) s.clusterId = strokes[0].clusterId;
|
|
45247
45303
|
this.ctx.history.execute(new AddStrokesCommand(strokes));
|
|
45304
|
+
this.ctx.selection.set(strokes.map((s) => s.id));
|
|
45248
45305
|
this.ctx.setTool?.("select");
|
|
45249
45306
|
}
|
|
45250
45307
|
this.ctx.transition("idle");
|
|
@@ -45826,6 +45883,13 @@ var HANDLE_HIT_PX = 12;
|
|
|
45826
45883
|
var ROTATE_OFFSET_PX = 26;
|
|
45827
45884
|
var ROTATE_HIT_PX = 14;
|
|
45828
45885
|
var COLOR = 2450411;
|
|
45886
|
+
/**
|
|
45887
|
+
* Visual-only outset between the selected object and the gizmo's outline/
|
|
45888
|
+
* handles — the object's actual bounding box (used for resize math, see
|
|
45889
|
+
* `anchorPoint`/`handlePoint`'s callers in `selectTool.ts`) never changes;
|
|
45890
|
+
* only where the outline and handle meshes are *drawn* and hit-tested does.
|
|
45891
|
+
*/
|
|
45892
|
+
var GAP_PX = 8;
|
|
45829
45893
|
var SelectionGizmo = class {
|
|
45830
45894
|
constructor(scene) {
|
|
45831
45895
|
this.group = new Group();
|
|
@@ -45840,7 +45904,7 @@ var SelectionGizmo = class {
|
|
|
45840
45904
|
this.outline = new LineLoop(new BufferGeometry(), this.lineMaterial);
|
|
45841
45905
|
this.outline.position.z = GIZMO_Z;
|
|
45842
45906
|
this.outline.frustumCulled = false;
|
|
45843
|
-
const handleGeometry = new
|
|
45907
|
+
const handleGeometry = new CircleGeometry(.5, 20);
|
|
45844
45908
|
const handleMaterial = new MeshBasicMaterial({ color: COLOR });
|
|
45845
45909
|
const makeHandle = () => new Mesh(handleGeometry, handleMaterial);
|
|
45846
45910
|
this.handles = {
|
|
@@ -45868,18 +45932,19 @@ var SelectionGizmo = class {
|
|
|
45868
45932
|
if (!box) return;
|
|
45869
45933
|
this.lineMaterial.color.setHex(locked ? 16096779 : COLOR);
|
|
45870
45934
|
this.rotateHandle.visible = !locked;
|
|
45935
|
+
const visual = this.visualBox(box, wpp);
|
|
45871
45936
|
const positions = new Float32Array([
|
|
45872
|
-
|
|
45873
|
-
|
|
45937
|
+
visual.minX,
|
|
45938
|
+
visual.minY,
|
|
45874
45939
|
GIZMO_Z,
|
|
45875
|
-
|
|
45876
|
-
|
|
45940
|
+
visual.maxX,
|
|
45941
|
+
visual.minY,
|
|
45877
45942
|
GIZMO_Z,
|
|
45878
|
-
|
|
45879
|
-
|
|
45943
|
+
visual.maxX,
|
|
45944
|
+
visual.maxY,
|
|
45880
45945
|
GIZMO_Z,
|
|
45881
|
-
|
|
45882
|
-
|
|
45946
|
+
visual.minX,
|
|
45947
|
+
visual.maxY,
|
|
45883
45948
|
GIZMO_Z
|
|
45884
45949
|
]);
|
|
45885
45950
|
this.outline.geometry.dispose();
|
|
@@ -45891,17 +45956,27 @@ var SelectionGizmo = class {
|
|
|
45891
45956
|
for (const [handle, mesh] of Object.entries(this.handles)) {
|
|
45892
45957
|
mesh.visible = !locked;
|
|
45893
45958
|
if (!locked) {
|
|
45894
|
-
const p = this.handlePoint(handle,
|
|
45959
|
+
const p = this.handlePoint(handle, visual);
|
|
45895
45960
|
mesh.position.set(p.x, p.y, GIZMO_Z);
|
|
45896
45961
|
mesh.scale.set(size, size, 1);
|
|
45897
45962
|
}
|
|
45898
45963
|
}
|
|
45899
45964
|
if (!locked) {
|
|
45900
|
-
const rotate = this.rotatePoint(
|
|
45965
|
+
const rotate = this.rotatePoint(visual, wpp);
|
|
45901
45966
|
this.rotateHandle.position.set(rotate.x, rotate.y, GIZMO_Z);
|
|
45902
45967
|
this.rotateHandle.scale.set(size, size, 1);
|
|
45903
45968
|
}
|
|
45904
45969
|
}
|
|
45970
|
+
/** 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`). */
|
|
45971
|
+
visualBox(box, wpp) {
|
|
45972
|
+
const gap = GAP_PX * wpp;
|
|
45973
|
+
return {
|
|
45974
|
+
minX: box.minX - gap,
|
|
45975
|
+
minY: box.minY - gap,
|
|
45976
|
+
maxX: box.maxX + gap,
|
|
45977
|
+
maxY: box.maxY + gap
|
|
45978
|
+
};
|
|
45979
|
+
}
|
|
45905
45980
|
handlePoint(handle, box) {
|
|
45906
45981
|
const midX = (box.minX + box.maxX) / 2;
|
|
45907
45982
|
const midY = (box.minY + box.maxY) / 2;
|
|
@@ -45989,7 +46064,8 @@ var SelectionGizmo = class {
|
|
|
45989
46064
|
if (!this.box || this.isLocked) return null;
|
|
45990
46065
|
const handleR = HANDLE_HIT_PX * this.wpp;
|
|
45991
46066
|
const edgeSlop = 6 * this.wpp;
|
|
45992
|
-
const
|
|
46067
|
+
const visual = this.visualBox(this.box, this.wpp);
|
|
46068
|
+
const rotate = this.rotatePoint(visual, this.wpp);
|
|
45993
46069
|
if (Math.hypot(p.x - rotate.x, p.y - rotate.y) < ROTATE_HIT_PX * this.wpp) return { kind: "rotate" };
|
|
45994
46070
|
for (const handle of [
|
|
45995
46071
|
"nw",
|
|
@@ -46001,13 +46077,13 @@ var SelectionGizmo = class {
|
|
|
46001
46077
|
"e",
|
|
46002
46078
|
"w"
|
|
46003
46079
|
]) {
|
|
46004
|
-
const hp = this.handlePoint(handle,
|
|
46080
|
+
const hp = this.handlePoint(handle, visual);
|
|
46005
46081
|
if (Math.abs(p.x - hp.x) < handleR && Math.abs(p.y - hp.y) < handleR) return {
|
|
46006
46082
|
kind: "scale",
|
|
46007
46083
|
handle
|
|
46008
46084
|
};
|
|
46009
46085
|
}
|
|
46010
|
-
const { minX, maxX, minY, maxY } =
|
|
46086
|
+
const { minX, maxX, minY, maxY } = visual;
|
|
46011
46087
|
const inX = p.x >= minX - edgeSlop && p.x <= maxX + edgeSlop;
|
|
46012
46088
|
const inY = p.y >= minY - edgeSlop && p.y <= maxY + edgeSlop;
|
|
46013
46089
|
if (inX && Math.abs(p.y - maxY) < edgeSlop) return {
|
|
@@ -46027,7 +46103,7 @@ var SelectionGizmo = class {
|
|
|
46027
46103
|
handle: "w"
|
|
46028
46104
|
};
|
|
46029
46105
|
const pad = 2 * this.wpp;
|
|
46030
|
-
if (p.x >= minX - pad && p.x <= maxX + pad && p.y >= minY - pad && p.y <= maxY + pad) return { kind: "inside" };
|
|
46106
|
+
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" };
|
|
46031
46107
|
return null;
|
|
46032
46108
|
}
|
|
46033
46109
|
rotatePoint(box, wpp) {
|
|
@@ -48260,6 +48336,112 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48260
48336
|
})),
|
|
48261
48337
|
...[...document.allCustomObjects()].map((value) => clone(toBoardObject(value)))
|
|
48262
48338
|
];
|
|
48339
|
+
const projectToScreen = (point) => engine ? engine.boardToScreen(point) : {
|
|
48340
|
+
x: (point.x - view.x) * view.zoom,
|
|
48341
|
+
y: (view.y - point.y) * view.zoom
|
|
48342
|
+
};
|
|
48343
|
+
const lockFields = (item) => ({
|
|
48344
|
+
locked: !!item.locked,
|
|
48345
|
+
...item.lockedBy ? { lockedBy: item.lockedBy } : {},
|
|
48346
|
+
...item.lockedByName ? { lockedByName: item.lockedByName } : {}
|
|
48347
|
+
});
|
|
48348
|
+
const lockFieldsMany = (items) => {
|
|
48349
|
+
const locked = items.filter((i) => i.locked);
|
|
48350
|
+
if (locked.length === 0) return { locked: false };
|
|
48351
|
+
const lockedBys = [...new Set(locked.map((i) => i.lockedBy).filter((v) => !!v))];
|
|
48352
|
+
const names = [...new Set(locked.map((i) => i.lockedByName).filter((v) => !!v))];
|
|
48353
|
+
return {
|
|
48354
|
+
locked: true,
|
|
48355
|
+
...lockedBys.length === 1 ? { lockedBy: lockedBys[0] } : {},
|
|
48356
|
+
...names.length === 1 ? { lockedByName: names[0] } : {}
|
|
48357
|
+
};
|
|
48358
|
+
};
|
|
48359
|
+
const strokeClusterAnchor = (strokes) => {
|
|
48360
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
48361
|
+
for (const s of strokes) for (const p of s.points) {
|
|
48362
|
+
const world = s.matrix ? apply(s.matrix, p) : p;
|
|
48363
|
+
minX = Math.min(minX, world.x);
|
|
48364
|
+
maxX = Math.max(maxX, world.x);
|
|
48365
|
+
minY = Math.min(minY, world.y);
|
|
48366
|
+
maxY = Math.max(maxY, world.y);
|
|
48367
|
+
}
|
|
48368
|
+
if (!Number.isFinite(minX)) return projectToScreen({
|
|
48369
|
+
x: 0,
|
|
48370
|
+
y: 0
|
|
48371
|
+
});
|
|
48372
|
+
return projectToScreen({
|
|
48373
|
+
x: (minX + maxX) / 2,
|
|
48374
|
+
y: maxY + 1.4
|
|
48375
|
+
});
|
|
48376
|
+
};
|
|
48377
|
+
/** Per-type anchor formulas mirror `ScrawlEngine.getSelectedItemInfo` exactly, so a Host-built toolbar lands in the same spot the SDK's own would. */
|
|
48378
|
+
const anchorFor = (obj) => {
|
|
48379
|
+
switch (obj.type) {
|
|
48380
|
+
case "note": return projectToScreen({
|
|
48381
|
+
x: obj.x,
|
|
48382
|
+
y: obj.y + obj.size / 2 + 1
|
|
48383
|
+
});
|
|
48384
|
+
case "text": {
|
|
48385
|
+
const { width } = measureTextBlock(obj.text, obj.fontSize);
|
|
48386
|
+
return projectToScreen({
|
|
48387
|
+
x: obj.x + width / 2,
|
|
48388
|
+
y: obj.y + 1
|
|
48389
|
+
});
|
|
48390
|
+
}
|
|
48391
|
+
case "table": {
|
|
48392
|
+
const { width } = measureTable(obj);
|
|
48393
|
+
return projectToScreen({
|
|
48394
|
+
x: obj.x + width / 2,
|
|
48395
|
+
y: obj.y + 1.2
|
|
48396
|
+
});
|
|
48397
|
+
}
|
|
48398
|
+
case "image": return projectToScreen({
|
|
48399
|
+
x: obj.x,
|
|
48400
|
+
y: obj.y + obj.height / 2 + 1.2
|
|
48401
|
+
});
|
|
48402
|
+
case "timer": return projectToScreen({
|
|
48403
|
+
x: obj.x,
|
|
48404
|
+
y: obj.y + obj.size / 2 + 1.2
|
|
48405
|
+
});
|
|
48406
|
+
case "custom": {
|
|
48407
|
+
const b = obj.fallback.bounds;
|
|
48408
|
+
return projectToScreen(apply(obj.transform, {
|
|
48409
|
+
x: b.x + b.width / 2,
|
|
48410
|
+
y: b.y + b.height + 1
|
|
48411
|
+
}));
|
|
48412
|
+
}
|
|
48413
|
+
case "stroke": return strokeClusterAnchor([obj]);
|
|
48414
|
+
}
|
|
48415
|
+
};
|
|
48416
|
+
const computeFocusedItem = () => {
|
|
48417
|
+
if (selection.length === 0) return null;
|
|
48418
|
+
if (selection.length === 1) {
|
|
48419
|
+
const obj = readObject(selection[0]);
|
|
48420
|
+
if (!obj) return null;
|
|
48421
|
+
if (obj.type === "custom") return {
|
|
48422
|
+
type: "custom",
|
|
48423
|
+
id: obj.id,
|
|
48424
|
+
locked: false,
|
|
48425
|
+
screenPosition: anchorFor(obj)
|
|
48426
|
+
};
|
|
48427
|
+
return {
|
|
48428
|
+
type: obj.type,
|
|
48429
|
+
id: obj.id,
|
|
48430
|
+
...lockFields(obj),
|
|
48431
|
+
screenPosition: anchorFor(obj)
|
|
48432
|
+
};
|
|
48433
|
+
}
|
|
48434
|
+
const objects = selection.map(readObject);
|
|
48435
|
+
if (!objects.every((o) => o?.type === "stroke")) return null;
|
|
48436
|
+
const clusterId = objects[0].clusterId;
|
|
48437
|
+
if (!clusterId || !objects.every((o) => o.clusterId === clusterId)) return null;
|
|
48438
|
+
return {
|
|
48439
|
+
type: "stroke",
|
|
48440
|
+
id: objects[0].id,
|
|
48441
|
+
...lockFieldsMany(objects),
|
|
48442
|
+
screenPosition: strokeClusterAnchor(objects)
|
|
48443
|
+
};
|
|
48444
|
+
};
|
|
48263
48445
|
const updateObject = (id, patch) => {
|
|
48264
48446
|
assertMutable();
|
|
48265
48447
|
const before = readObject(id);
|
|
@@ -48301,6 +48483,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48301
48483
|
zoom: view.zoom,
|
|
48302
48484
|
readOnly,
|
|
48303
48485
|
selection: Object.freeze([...selection]),
|
|
48486
|
+
focusedItem: computeFocusedItem(),
|
|
48304
48487
|
strokeCount: [...document.all()].length,
|
|
48305
48488
|
objectCount: allObjects().length,
|
|
48306
48489
|
canUndo: history.canUndo,
|
|
@@ -48715,10 +48898,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48715
48898
|
changed();
|
|
48716
48899
|
},
|
|
48717
48900
|
get: () => ({ ...view }),
|
|
48718
|
-
boardToScreen: (point) =>
|
|
48719
|
-
x: (point.x - view.x) * view.zoom,
|
|
48720
|
-
y: (view.y - point.y) * view.zoom
|
|
48721
|
-
},
|
|
48901
|
+
boardToScreen: (point) => projectToScreen(point),
|
|
48722
48902
|
screenToBoard: (point) => engine ? engine.screenToBoard(point.x, point.y) ?? {
|
|
48723
48903
|
x: point.x,
|
|
48724
48904
|
y: point.y
|
|
@@ -48755,6 +48935,81 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48755
48935
|
update(id, patch) {
|
|
48756
48936
|
updateObject(id, patch);
|
|
48757
48937
|
},
|
|
48938
|
+
duplicate(ids) {
|
|
48939
|
+
assertMutable();
|
|
48940
|
+
const objects = [...new Set(ids)].map(readObject).filter((o) => !!o);
|
|
48941
|
+
if (objects.length === 0) return [];
|
|
48942
|
+
const DX = 1.5;
|
|
48943
|
+
const DY = -1.5;
|
|
48944
|
+
const newClusterIds = /* @__PURE__ */ new Map();
|
|
48945
|
+
const clusterIdFor = (oldClusterId) => {
|
|
48946
|
+
let next = newClusterIds.get(oldClusterId);
|
|
48947
|
+
if (!next) {
|
|
48948
|
+
next = crypto.randomUUID();
|
|
48949
|
+
newClusterIds.set(oldClusterId, next);
|
|
48950
|
+
}
|
|
48951
|
+
return next;
|
|
48952
|
+
};
|
|
48953
|
+
const commands = [];
|
|
48954
|
+
const newIds = [];
|
|
48955
|
+
for (const obj of objects) {
|
|
48956
|
+
const id = createId();
|
|
48957
|
+
newIds.push(id);
|
|
48958
|
+
const rest = { ...obj };
|
|
48959
|
+
delete rest.locked;
|
|
48960
|
+
delete rest.lockedBy;
|
|
48961
|
+
delete rest.lockedByName;
|
|
48962
|
+
let duplicated;
|
|
48963
|
+
if (obj.type === "stroke") {
|
|
48964
|
+
const matrix = obj.matrix ?? [
|
|
48965
|
+
1,
|
|
48966
|
+
0,
|
|
48967
|
+
0,
|
|
48968
|
+
1,
|
|
48969
|
+
0,
|
|
48970
|
+
0
|
|
48971
|
+
];
|
|
48972
|
+
duplicated = {
|
|
48973
|
+
...rest,
|
|
48974
|
+
id,
|
|
48975
|
+
matrix: [
|
|
48976
|
+
matrix[0],
|
|
48977
|
+
matrix[1],
|
|
48978
|
+
matrix[2],
|
|
48979
|
+
matrix[3],
|
|
48980
|
+
matrix[4] + DX,
|
|
48981
|
+
matrix[5] + DY
|
|
48982
|
+
],
|
|
48983
|
+
...obj.clusterId ? { clusterId: clusterIdFor(obj.clusterId) } : {}
|
|
48984
|
+
};
|
|
48985
|
+
} else if (obj.type === "custom") {
|
|
48986
|
+
const t = obj.transform;
|
|
48987
|
+
duplicated = {
|
|
48988
|
+
...rest,
|
|
48989
|
+
id,
|
|
48990
|
+
transform: [
|
|
48991
|
+
t[0],
|
|
48992
|
+
t[1],
|
|
48993
|
+
t[2],
|
|
48994
|
+
t[3],
|
|
48995
|
+
t[4] + DX,
|
|
48996
|
+
t[5] + DY
|
|
48997
|
+
]
|
|
48998
|
+
};
|
|
48999
|
+
delete duplicated.lock;
|
|
49000
|
+
} else duplicated = {
|
|
49001
|
+
...rest,
|
|
49002
|
+
id,
|
|
49003
|
+
x: obj.x + DX,
|
|
49004
|
+
y: obj.y + DY
|
|
49005
|
+
};
|
|
49006
|
+
validateBoardObject(duplicated.type, duplicated);
|
|
49007
|
+
commands.push(addCommand(duplicated.type, duplicated));
|
|
49008
|
+
}
|
|
49009
|
+
executeHistory(() => history.execute(new CommandBatch("duplicate content", commands)));
|
|
49010
|
+
changed();
|
|
49011
|
+
return newIds;
|
|
49012
|
+
},
|
|
48758
49013
|
table: {
|
|
48759
49014
|
addRow(tableId) {
|
|
48760
49015
|
const table = document.getTable(tableId);
|
|
@@ -49688,6 +49943,24 @@ function InlineEditors({ controller, renderPortal }) {
|
|
|
49688
49943
|
};
|
|
49689
49944
|
useEffect(() => controller.on("edit-request", (event) => {
|
|
49690
49945
|
const offset = rootOffset();
|
|
49946
|
+
if (event.kind === "text" && event.id === null) {
|
|
49947
|
+
const newId = controller.content.add({
|
|
49948
|
+
type: "text",
|
|
49949
|
+
x: event.boardX,
|
|
49950
|
+
y: event.boardY,
|
|
49951
|
+
text: "",
|
|
49952
|
+
color: event.color,
|
|
49953
|
+
fontSize: TEXT_DEFAULT_SIZE
|
|
49954
|
+
});
|
|
49955
|
+
controller.content.select([newId]);
|
|
49956
|
+
setEditor({
|
|
49957
|
+
...event,
|
|
49958
|
+
id: newId,
|
|
49959
|
+
left: event.screenRect.left - offset.left,
|
|
49960
|
+
top: event.screenRect.top - offset.top
|
|
49961
|
+
});
|
|
49962
|
+
return;
|
|
49963
|
+
}
|
|
49691
49964
|
setEditor({
|
|
49692
49965
|
...event,
|
|
49693
49966
|
left: event.screenRect.left - offset.left,
|
|
@@ -49699,19 +49972,11 @@ function InlineEditors({ controller, renderPortal }) {
|
|
|
49699
49972
|
setEditor(null);
|
|
49700
49973
|
};
|
|
49701
49974
|
const commitText = () => {
|
|
49702
|
-
if (editor?.kind === "text") {
|
|
49975
|
+
if (editor?.kind === "text" && editor.id !== null) {
|
|
49703
49976
|
const text = editor.text;
|
|
49704
|
-
|
|
49705
|
-
|
|
49706
|
-
|
|
49707
|
-
x: editor.boardX,
|
|
49708
|
-
y: editor.boardY,
|
|
49709
|
-
text,
|
|
49710
|
-
color: editor.color,
|
|
49711
|
-
fontSize: TEXT_DEFAULT_SIZE
|
|
49712
|
-
});
|
|
49713
|
-
} else if (!text.trim()) controller.content.remove([editor.id]);
|
|
49714
|
-
else controller.content.update(editor.id, { text });
|
|
49977
|
+
const existing = controller.query.get(editor.id);
|
|
49978
|
+
if (!text.trim()) controller.content.remove([editor.id]);
|
|
49979
|
+
else if (existing?.type !== "text" || existing.text !== text) controller.content.update(editor.id, { text });
|
|
49715
49980
|
}
|
|
49716
49981
|
setEditor(null);
|
|
49717
49982
|
};
|
|
@@ -49904,6 +50169,366 @@ function InlineEditors({ controller, renderPortal }) {
|
|
|
49904
50169
|
}))] });
|
|
49905
50170
|
}
|
|
49906
50171
|
//#endregion
|
|
50172
|
+
//#region src/ui/toolbar/focused-item-toolbar.tsx
|
|
50173
|
+
var NOTE_SIZES = [
|
|
50174
|
+
["Small", 7],
|
|
50175
|
+
["Medium", 10],
|
|
50176
|
+
["Large", 14]
|
|
50177
|
+
];
|
|
50178
|
+
var TEXT_SIZES = [
|
|
50179
|
+
["Small", 1.2],
|
|
50180
|
+
["Medium", 1.8],
|
|
50181
|
+
["Large", 2.6]
|
|
50182
|
+
];
|
|
50183
|
+
var SHAPE_WIDTHS = [
|
|
50184
|
+
["Thin", .1],
|
|
50185
|
+
["Medium", .15],
|
|
50186
|
+
["Thick", .3]
|
|
50187
|
+
];
|
|
50188
|
+
/** Matches the native selection gizmo's own outset (renderer/selection/gizmo.ts's GAP_PX), for a consistent look. */
|
|
50189
|
+
var HANDLE_GAP_PX = 8;
|
|
50190
|
+
var MIN_SIZE = 4;
|
|
50191
|
+
var MAX_SIZE = 30;
|
|
50192
|
+
var HANDLE_CORNERS = [
|
|
50193
|
+
"nw",
|
|
50194
|
+
"ne",
|
|
50195
|
+
"sw",
|
|
50196
|
+
"se"
|
|
50197
|
+
];
|
|
50198
|
+
/**
|
|
50199
|
+
* Floating toolbar above the focused note, shape, or text block — Colour,
|
|
50200
|
+
* Size (note/text) or Width (shape), Lock/Unlock, Duplicate, Delete.
|
|
50201
|
+
* Table/image/timer/custom objects and plain (non-shape) ink strokes never
|
|
50202
|
+
* get a toolbar here — out of scope for this destination.
|
|
50203
|
+
*
|
|
50204
|
+
* Lock/Unlock carries no per-user ownership gating: nothing else in this
|
|
50205
|
+
* SDK enforces lock ownership either (`content.update` never checks
|
|
50206
|
+
* `locked`, and the engine's own internal unlock check has no way for a
|
|
50207
|
+
* Host to ever supply a real user id) — locking is advisory UI state
|
|
50208
|
+
* throughout, and this toolbar matches that rather than inventing an
|
|
50209
|
+
* enforcement story alone.
|
|
50210
|
+
*
|
|
50211
|
+
* A shape can be several strokes sharing one `clusterId` (e.g. an arrow's
|
|
50212
|
+
* shaft + head) — every action here applies to the whole cluster. Resolved
|
|
50213
|
+
* via `query.all()` + `clusterId`, not `snapshot.selection`: a canvas click
|
|
50214
|
+
* on one member selects every member internally, but the public selection
|
|
50215
|
+
* bridge only ever reports one id (`FocusedItem.id` is singular by design),
|
|
50216
|
+
* so reconstructing the cluster from the document is the reliable path
|
|
50217
|
+
* regardless of how the selection was made.
|
|
50218
|
+
*
|
|
50219
|
+
* A focused note also gets drag-to-resize corner handles — notes have no
|
|
50220
|
+
* gizmo of their own (the native selection gizmo is ink-stroke/shape-only),
|
|
50221
|
+
* so this is that capability's default-ui-owned equivalent. Proportional
|
|
50222
|
+
* (always-square) resize from the note's own centre, clamped to
|
|
50223
|
+
* `[MIN_SIZE, MAX_SIZE]`, independent of which corner is grabbed.
|
|
50224
|
+
*/
|
|
50225
|
+
function FocusedItemToolbar({ controller, snapshot }) {
|
|
50226
|
+
const focused = snapshot.focusedItem;
|
|
50227
|
+
if (!focused) return null;
|
|
50228
|
+
const obj = controller.query.get(focused.id);
|
|
50229
|
+
if (!obj) return null;
|
|
50230
|
+
if (obj.type === "note") return /* @__PURE__ */ jsx(NoteToolbar, {
|
|
50231
|
+
controller,
|
|
50232
|
+
snapshot,
|
|
50233
|
+
note: obj,
|
|
50234
|
+
locked: focused.locked,
|
|
50235
|
+
anchor: focused.screenPosition
|
|
50236
|
+
});
|
|
50237
|
+
if (obj.type === "text") return /* @__PURE__ */ jsx(TextToolbar, {
|
|
50238
|
+
controller,
|
|
50239
|
+
snapshot,
|
|
50240
|
+
text: obj,
|
|
50241
|
+
locked: focused.locked,
|
|
50242
|
+
anchor: focused.screenPosition
|
|
50243
|
+
});
|
|
50244
|
+
if (obj.type === "stroke" && obj.tool === "shape") return /* @__PURE__ */ jsx(ShapeToolbar, {
|
|
50245
|
+
controller,
|
|
50246
|
+
snapshot,
|
|
50247
|
+
stroke: obj,
|
|
50248
|
+
locked: focused.locked,
|
|
50249
|
+
anchor: focused.screenPosition
|
|
50250
|
+
});
|
|
50251
|
+
return null;
|
|
50252
|
+
}
|
|
50253
|
+
function ToolbarShell({ anchor, children }) {
|
|
50254
|
+
if (!anchor) return null;
|
|
50255
|
+
return /* @__PURE__ */ jsx("div", {
|
|
50256
|
+
"aria-label": "Selected object",
|
|
50257
|
+
className: "scrawl-board__focus-toolbar",
|
|
50258
|
+
role: "toolbar",
|
|
50259
|
+
style: {
|
|
50260
|
+
left: anchor.x,
|
|
50261
|
+
top: anchor.y
|
|
50262
|
+
},
|
|
50263
|
+
children
|
|
50264
|
+
});
|
|
50265
|
+
}
|
|
50266
|
+
function NoteToolbar({ controller, snapshot, note, locked, anchor }) {
|
|
50267
|
+
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
50268
|
+
const locked_ = !mutable || locked;
|
|
50269
|
+
const half = note.size / 2;
|
|
50270
|
+
const cornerA = controller.view.boardToScreen({
|
|
50271
|
+
x: note.x - half,
|
|
50272
|
+
y: note.y + half
|
|
50273
|
+
});
|
|
50274
|
+
const cornerB = controller.view.boardToScreen({
|
|
50275
|
+
x: note.x + half,
|
|
50276
|
+
y: note.y - half
|
|
50277
|
+
});
|
|
50278
|
+
const outline = {
|
|
50279
|
+
left: Math.min(cornerA.x, cornerB.x) - HANDLE_GAP_PX,
|
|
50280
|
+
top: Math.min(cornerA.y, cornerB.y) - HANDLE_GAP_PX,
|
|
50281
|
+
right: Math.max(cornerA.x, cornerB.x) + HANDLE_GAP_PX,
|
|
50282
|
+
bottom: Math.max(cornerA.y, cornerB.y) + HANDLE_GAP_PX
|
|
50283
|
+
};
|
|
50284
|
+
const handlePoints = {
|
|
50285
|
+
nw: {
|
|
50286
|
+
x: outline.left,
|
|
50287
|
+
y: outline.top
|
|
50288
|
+
},
|
|
50289
|
+
ne: {
|
|
50290
|
+
x: outline.right,
|
|
50291
|
+
y: outline.top
|
|
50292
|
+
},
|
|
50293
|
+
sw: {
|
|
50294
|
+
x: outline.left,
|
|
50295
|
+
y: outline.bottom
|
|
50296
|
+
},
|
|
50297
|
+
se: {
|
|
50298
|
+
x: outline.right,
|
|
50299
|
+
y: outline.bottom
|
|
50300
|
+
}
|
|
50301
|
+
};
|
|
50302
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
50303
|
+
/* @__PURE__ */ jsx("div", {
|
|
50304
|
+
"aria-hidden": "true",
|
|
50305
|
+
className: "scrawl-board__focus-outline",
|
|
50306
|
+
"data-locked": locked,
|
|
50307
|
+
style: {
|
|
50308
|
+
left: outline.left,
|
|
50309
|
+
top: outline.top,
|
|
50310
|
+
width: outline.right - outline.left,
|
|
50311
|
+
height: outline.bottom - outline.top
|
|
50312
|
+
}
|
|
50313
|
+
}),
|
|
50314
|
+
mutable && !locked && HANDLE_CORNERS.map((corner) => /* @__PURE__ */ jsx(ResizeHandle, {
|
|
50315
|
+
controller,
|
|
50316
|
+
note,
|
|
50317
|
+
corner,
|
|
50318
|
+
x: handlePoints[corner].x,
|
|
50319
|
+
y: handlePoints[corner].y
|
|
50320
|
+
}, corner)),
|
|
50321
|
+
/* @__PURE__ */ jsxs(ToolbarShell, {
|
|
50322
|
+
anchor,
|
|
50323
|
+
children: [
|
|
50324
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50325
|
+
className: "scrawl-board__style-group",
|
|
50326
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Size" }), NOTE_SIZES.map(([label, size]) => /* @__PURE__ */ jsx("button", {
|
|
50327
|
+
"aria-label": `${label} note`,
|
|
50328
|
+
"aria-pressed": note.size === size,
|
|
50329
|
+
disabled: locked_,
|
|
50330
|
+
onClick: () => controller.content.update(note.id, { size }),
|
|
50331
|
+
type: "button",
|
|
50332
|
+
children: label[0]
|
|
50333
|
+
}, label))]
|
|
50334
|
+
}),
|
|
50335
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50336
|
+
className: "scrawl-board__style-group",
|
|
50337
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Colour" }), Object.entries(NOTE_COLORS).map(([name, value]) => /* @__PURE__ */ jsx("button", {
|
|
50338
|
+
"aria-label": `${name} note`,
|
|
50339
|
+
"aria-pressed": note.color.toLowerCase() === value.toLowerCase(),
|
|
50340
|
+
disabled: locked_,
|
|
50341
|
+
onClick: () => controller.content.update(note.id, { color: value }),
|
|
50342
|
+
type: "button",
|
|
50343
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
50344
|
+
"aria-hidden": "true",
|
|
50345
|
+
style: { backgroundColor: value }
|
|
50346
|
+
})
|
|
50347
|
+
}, value))]
|
|
50348
|
+
}),
|
|
50349
|
+
/* @__PURE__ */ jsx(LockDuplicateDelete, {
|
|
50350
|
+
controller,
|
|
50351
|
+
mutable,
|
|
50352
|
+
locked,
|
|
50353
|
+
ids: [note.id]
|
|
50354
|
+
})
|
|
50355
|
+
]
|
|
50356
|
+
})
|
|
50357
|
+
] });
|
|
50358
|
+
}
|
|
50359
|
+
/**
|
|
50360
|
+
* One corner handle — drag distance from the note's own centre (its `x`/`y`
|
|
50361
|
+
* already *is* the centre) drives a proportional, always-square resize,
|
|
50362
|
+
* independent of which corner is grabbed. `note` is fresh every render (a
|
|
50363
|
+
* new `content.update` during drag triggers a new snapshot), so the
|
|
50364
|
+
* closure never goes stale.
|
|
50365
|
+
*/
|
|
50366
|
+
function ResizeHandle({ controller, note, corner, x, y }) {
|
|
50367
|
+
const [dragging, setDragging] = useState(false);
|
|
50368
|
+
const nudge = (delta) => {
|
|
50369
|
+
const half = Math.max(MIN_SIZE / 2, Math.min(MAX_SIZE / 2, note.size / 2 + delta));
|
|
50370
|
+
controller.content.update(note.id, { size: half * 2 });
|
|
50371
|
+
};
|
|
50372
|
+
return /* @__PURE__ */ jsx("span", {
|
|
50373
|
+
"aria-label": `Resize note (${corner})`,
|
|
50374
|
+
"aria-valuemax": MAX_SIZE,
|
|
50375
|
+
"aria-valuemin": MIN_SIZE,
|
|
50376
|
+
"aria-valuenow": note.size,
|
|
50377
|
+
role: "slider",
|
|
50378
|
+
tabIndex: 0,
|
|
50379
|
+
className: "scrawl-board__focus-handle",
|
|
50380
|
+
"data-orientation": corner === "nw" || corner === "se" ? "nwse" : "nesw",
|
|
50381
|
+
style: {
|
|
50382
|
+
left: x,
|
|
50383
|
+
top: y
|
|
50384
|
+
},
|
|
50385
|
+
onKeyDown: (event) => {
|
|
50386
|
+
if (event.key === "ArrowUp" || event.key === "ArrowRight") {
|
|
50387
|
+
event.preventDefault();
|
|
50388
|
+
nudge(.5);
|
|
50389
|
+
} else if (event.key === "ArrowDown" || event.key === "ArrowLeft") {
|
|
50390
|
+
event.preventDefault();
|
|
50391
|
+
nudge(-.5);
|
|
50392
|
+
}
|
|
50393
|
+
},
|
|
50394
|
+
onPointerDown: (event) => {
|
|
50395
|
+
event.stopPropagation();
|
|
50396
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
50397
|
+
setDragging(true);
|
|
50398
|
+
},
|
|
50399
|
+
onPointerMove: (event) => {
|
|
50400
|
+
if (!dragging) return;
|
|
50401
|
+
const board = controller.view.screenToBoard({
|
|
50402
|
+
x: event.clientX,
|
|
50403
|
+
y: event.clientY
|
|
50404
|
+
});
|
|
50405
|
+
const half = Math.max(MIN_SIZE / 2, Math.min(MAX_SIZE / 2, Math.max(Math.abs(board.x - note.x), Math.abs(board.y - note.y))));
|
|
50406
|
+
controller.content.update(note.id, { size: half * 2 });
|
|
50407
|
+
},
|
|
50408
|
+
onPointerUp: (event) => {
|
|
50409
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
50410
|
+
setDragging(false);
|
|
50411
|
+
}
|
|
50412
|
+
});
|
|
50413
|
+
}
|
|
50414
|
+
function TextToolbar({ controller, snapshot, text, locked, anchor }) {
|
|
50415
|
+
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
50416
|
+
const locked_ = !mutable || locked;
|
|
50417
|
+
return /* @__PURE__ */ jsxs(ToolbarShell, {
|
|
50418
|
+
anchor,
|
|
50419
|
+
children: [
|
|
50420
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50421
|
+
className: "scrawl-board__style-group",
|
|
50422
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Size" }), TEXT_SIZES.map(([label, fontSize]) => /* @__PURE__ */ jsx("button", {
|
|
50423
|
+
"aria-label": `${label} text`,
|
|
50424
|
+
"aria-pressed": Math.abs(text.fontSize - fontSize) < .001,
|
|
50425
|
+
disabled: locked_,
|
|
50426
|
+
onClick: () => controller.content.update(text.id, { fontSize }),
|
|
50427
|
+
type: "button",
|
|
50428
|
+
children: label[0]
|
|
50429
|
+
}, label))]
|
|
50430
|
+
}),
|
|
50431
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50432
|
+
className: "scrawl-board__style-group",
|
|
50433
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Colour" }), Object.entries(INK_COLORS).map(([name, value]) => /* @__PURE__ */ jsx("button", {
|
|
50434
|
+
"aria-label": `${name} text`,
|
|
50435
|
+
"aria-pressed": text.color.toLowerCase() === value.toLowerCase(),
|
|
50436
|
+
disabled: locked_,
|
|
50437
|
+
onClick: () => controller.content.update(text.id, { color: value }),
|
|
50438
|
+
type: "button",
|
|
50439
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
50440
|
+
"aria-hidden": "true",
|
|
50441
|
+
style: { backgroundColor: value }
|
|
50442
|
+
})
|
|
50443
|
+
}, value))]
|
|
50444
|
+
}),
|
|
50445
|
+
/* @__PURE__ */ jsx(LockDuplicateDelete, {
|
|
50446
|
+
controller,
|
|
50447
|
+
mutable,
|
|
50448
|
+
locked,
|
|
50449
|
+
ids: [text.id]
|
|
50450
|
+
})
|
|
50451
|
+
]
|
|
50452
|
+
});
|
|
50453
|
+
}
|
|
50454
|
+
function ShapeToolbar({ controller, snapshot, stroke, locked, anchor }) {
|
|
50455
|
+
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
50456
|
+
const locked_ = !mutable || locked;
|
|
50457
|
+
const ids = (stroke.clusterId ? controller.query.all().filter((o) => o.type === "stroke" && o.clusterId === stroke.clusterId) : [stroke]).map((m) => m.id);
|
|
50458
|
+
const applyToAll = (patch) => {
|
|
50459
|
+
for (const id of ids) controller.content.update(id, patch);
|
|
50460
|
+
};
|
|
50461
|
+
return /* @__PURE__ */ jsxs(ToolbarShell, {
|
|
50462
|
+
anchor,
|
|
50463
|
+
children: [
|
|
50464
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50465
|
+
className: "scrawl-board__style-group",
|
|
50466
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Width" }), SHAPE_WIDTHS.map(([label, width]) => /* @__PURE__ */ jsx("button", {
|
|
50467
|
+
"aria-label": `${label} stroke`,
|
|
50468
|
+
"aria-pressed": Math.abs(stroke.baseWidth - width) < .001,
|
|
50469
|
+
disabled: locked_,
|
|
50470
|
+
onClick: () => applyToAll({ baseWidth: width }),
|
|
50471
|
+
type: "button",
|
|
50472
|
+
children: label
|
|
50473
|
+
}, label))]
|
|
50474
|
+
}),
|
|
50475
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50476
|
+
className: "scrawl-board__style-group",
|
|
50477
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Colour" }), Object.entries(INK_COLORS).map(([name, value]) => /* @__PURE__ */ jsx("button", {
|
|
50478
|
+
"aria-label": `${name} shape`,
|
|
50479
|
+
"aria-pressed": stroke.color.toLowerCase() === value.toLowerCase(),
|
|
50480
|
+
disabled: locked_,
|
|
50481
|
+
onClick: () => applyToAll({ color: value }),
|
|
50482
|
+
type: "button",
|
|
50483
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
50484
|
+
"aria-hidden": "true",
|
|
50485
|
+
style: { backgroundColor: value }
|
|
50486
|
+
})
|
|
50487
|
+
}, value))]
|
|
50488
|
+
}),
|
|
50489
|
+
/* @__PURE__ */ jsx(LockDuplicateDelete, {
|
|
50490
|
+
controller,
|
|
50491
|
+
mutable,
|
|
50492
|
+
locked,
|
|
50493
|
+
ids
|
|
50494
|
+
})
|
|
50495
|
+
]
|
|
50496
|
+
});
|
|
50497
|
+
}
|
|
50498
|
+
function LockDuplicateDelete({ controller, mutable, locked, ids }) {
|
|
50499
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
50500
|
+
className: "scrawl-board__style-group",
|
|
50501
|
+
children: [
|
|
50502
|
+
/* @__PURE__ */ jsx("button", {
|
|
50503
|
+
"aria-label": locked ? "Unlock" : "Lock",
|
|
50504
|
+
disabled: !mutable,
|
|
50505
|
+
onClick: () => {
|
|
50506
|
+
for (const id of ids) controller.content.update(id, locked ? { locked: false } : { locked: true });
|
|
50507
|
+
},
|
|
50508
|
+
type: "button",
|
|
50509
|
+
children: locked ? "Unlock" : "Lock"
|
|
50510
|
+
}),
|
|
50511
|
+
/* @__PURE__ */ jsx("button", {
|
|
50512
|
+
"aria-label": "Duplicate",
|
|
50513
|
+
disabled: !mutable || locked,
|
|
50514
|
+
onClick: () => {
|
|
50515
|
+
const newIds = controller.content.duplicate(ids);
|
|
50516
|
+
if (newIds.length) controller.content.select(newIds);
|
|
50517
|
+
},
|
|
50518
|
+
type: "button",
|
|
50519
|
+
children: "Duplicate"
|
|
50520
|
+
}),
|
|
50521
|
+
/* @__PURE__ */ jsx("button", {
|
|
50522
|
+
"aria-label": "Delete",
|
|
50523
|
+
disabled: !mutable || locked,
|
|
50524
|
+
onClick: () => controller.content.remove(ids),
|
|
50525
|
+
type: "button",
|
|
50526
|
+
children: "Delete"
|
|
50527
|
+
})
|
|
50528
|
+
]
|
|
50529
|
+
});
|
|
50530
|
+
}
|
|
50531
|
+
//#endregion
|
|
49907
50532
|
//#region src/ui/toolbar/style-shelf.tsx
|
|
49908
50533
|
var ERASER_RADII = [
|
|
49909
50534
|
["Fine", 1],
|
|
@@ -49942,7 +50567,7 @@ var SHELF_TOOLS = /* @__PURE__ */ new Set([
|
|
|
49942
50567
|
"timer"
|
|
49943
50568
|
]);
|
|
49944
50569
|
/** Contextual per-tool style controls — visible while a styleable tool is active. */
|
|
49945
|
-
function StyleShelf({ controller, snapshot }) {
|
|
50570
|
+
function StyleShelf({ controller, snapshot, anchor }) {
|
|
49946
50571
|
if (!SHELF_TOOLS.has(snapshot.tool)) return null;
|
|
49947
50572
|
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
49948
50573
|
const { style } = snapshot;
|
|
@@ -49950,7 +50575,12 @@ function StyleShelf({ controller, snapshot }) {
|
|
|
49950
50575
|
return /* @__PURE__ */ jsxs("div", {
|
|
49951
50576
|
"aria-label": "Board style",
|
|
49952
50577
|
className: "scrawl-board__style-shelf",
|
|
50578
|
+
"data-anchored": !!anchor,
|
|
49953
50579
|
role: "toolbar",
|
|
50580
|
+
style: anchor ? {
|
|
50581
|
+
left: anchor.x,
|
|
50582
|
+
top: anchor.y
|
|
50583
|
+
} : void 0,
|
|
49954
50584
|
children: [
|
|
49955
50585
|
snapshot.tool === "highlighter" && /* @__PURE__ */ jsxs("div", {
|
|
49956
50586
|
className: "scrawl-board__style-group",
|
|
@@ -50180,6 +50810,7 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50180
50810
|
const [panel, setPanel] = useState(null);
|
|
50181
50811
|
const [query, setQuery] = useState("");
|
|
50182
50812
|
const [announcement, setAnnouncement] = useState("Board controls ready");
|
|
50813
|
+
const [styleShelfAnchor, setStyleShelfAnchor] = useState(void 0);
|
|
50183
50814
|
const importRef = useRef(null);
|
|
50184
50815
|
const openerRef = useRef(null);
|
|
50185
50816
|
const searchRef = useRef(null);
|
|
@@ -50190,6 +50821,25 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50190
50821
|
useEffect(() => {
|
|
50191
50822
|
if (panel === "search") searchRef.current?.focus();
|
|
50192
50823
|
}, [panel]);
|
|
50824
|
+
useEffect(() => {
|
|
50825
|
+
const recompute = () => {
|
|
50826
|
+
const root = chromeRef.current;
|
|
50827
|
+
const button = root?.querySelector(".scrawl-board__tools button[aria-pressed=\"true\"]");
|
|
50828
|
+
if (!root || !button) {
|
|
50829
|
+
setStyleShelfAnchor(void 0);
|
|
50830
|
+
return;
|
|
50831
|
+
}
|
|
50832
|
+
const buttonRect = button.getBoundingClientRect();
|
|
50833
|
+
const rootRect = root.getBoundingClientRect();
|
|
50834
|
+
setStyleShelfAnchor({
|
|
50835
|
+
x: buttonRect.left + buttonRect.width / 2 - rootRect.left,
|
|
50836
|
+
y: buttonRect.top - rootRect.top
|
|
50837
|
+
});
|
|
50838
|
+
};
|
|
50839
|
+
recompute();
|
|
50840
|
+
window.addEventListener("resize", recompute);
|
|
50841
|
+
return () => window.removeEventListener("resize", recompute);
|
|
50842
|
+
}, [snapshot.tool]);
|
|
50193
50843
|
const open = (next, opener) => {
|
|
50194
50844
|
openerRef.current = opener;
|
|
50195
50845
|
setPanel(next);
|
|
@@ -50418,9 +51068,14 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50418
51068
|
]
|
|
50419
51069
|
}),
|
|
50420
51070
|
renderSlot(slots?.stylePanel, enabled("styleShelf") && /* @__PURE__ */ jsx(StyleShelf, {
|
|
51071
|
+
anchor: styleShelfAnchor,
|
|
50421
51072
|
controller,
|
|
50422
51073
|
snapshot
|
|
50423
51074
|
})),
|
|
51075
|
+
enabled("focusedItemToolbar") && /* @__PURE__ */ jsx(FocusedItemToolbar, {
|
|
51076
|
+
controller,
|
|
51077
|
+
snapshot
|
|
51078
|
+
}),
|
|
50424
51079
|
ContextMenu && /* @__PURE__ */ jsx(ContextMenu, {
|
|
50425
51080
|
controller,
|
|
50426
51081
|
snapshot
|
|
@@ -51063,4 +51718,4 @@ function ScrawlBoard({ documentId, initialDocument, onReady, className, style })
|
|
|
51063
51718
|
});
|
|
51064
51719
|
}
|
|
51065
51720
|
//#endregion
|
|
51066
|
-
export { ASSET_CACHE_BYTES_DEFAULT, ASSET_CACHE_BYTES_MAX, ASSET_CACHE_BYTES_MIN, ASSET_EXPORT_MAX_DECODED_MEGAPIXELS, ASSET_EXPORT_MAX_ENCODED_BYTES, ASSET_MAX_CONCURRENT_RESOLUTIONS, ASSET_MAX_DECODED_MEGAPIXELS, ASSET_MAX_DIMENSION_PX, ASSET_MAX_ENCODED_BYTES, ASSET_REF_MAX_BYTES, ASSET_REF_PATTERN, AddImageCommand, AddNoteCommand, AddStrokesCommand, AddTableCommand, AddTextCommand, AddTimerCommand, AssetResolutionError, BEACON_INSET, BoardDocument, CURRENT_DOCUMENT_SCHEMA_VERSION, ClusterStore, DefaultBoardChrome, DeleteImageCommand, DeleteNoteCommand, DeleteStrokesCommand, DeleteTableCommand, DeleteTextCommand, DeleteTimerCommand, DocumentRecoveryError, END_TAPER, ERASE_THRESHOLD, EraseCommand, FOG_COLOR, HIGHLIGHT_COLORS, History, IDENTITY, INK_COLORS, InlineEditors, LockItemsCommand, MIN_WIDTH_FACTOR, MultiplayerCursors, NOTE_COLORS, NOTE_DEFAULT_SIZE, NOTE_DEFAULT_Z, NOTE_MAX_Z, NOTE_MIN_Z, NOTE_PEEL_STEP, SDK_DEVELOPMENT_VERSION, SDK_PACKAGE_NAME, STAMPS, STAMP_SIZE, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, SpatialIndex, StyleShelf, TABLE_DEFAULT_CELL_HEIGHT, TABLE_DEFAULT_CELL_WIDTH, TABLE_DEFAULT_FONT_SIZE, TEXT_DEFAULT_SIZE, TIMER_DEFAULT_DURATION_MS, TIMER_DEFAULT_SIZE, TIMER_PRESETS_MS, TransformCommand, UpdateImageCommand, UpdateNoteCommand, UpdateTableCommand, UpdateTextCommand, UpdateTimerCommand, apply, applyItemLock, assetRef, avgScale, canUnlockItem, changeToOps, clampAssetCacheBytes, cloneCustomObject, cloneImage, cloneNote, cloneStroke, cloneTable, cloneText, cloneTimer, createBoardController, createLocalBoard, documentId, documentToSVG, formatTimer, invert, isAssetRef, isIdentity, isStampKind, loadDocumentBytes, measureTable, measureTextBlock, migrateDocument, mul, pauseTimer, placePresenceBeacon, resolveScrawlTheme, ribbonEdges, rotationAbout, scalingAbout, scrawlThemePresets, searchBoard, serializeDocument, serializeLock, serializeStroke, setTimerDuration, stampDataUrl, startTimer, strokeId, timerExpired, timerRemaining, toggleTimer, translation, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
|
|
51721
|
+
export { ASSET_CACHE_BYTES_DEFAULT, ASSET_CACHE_BYTES_MAX, ASSET_CACHE_BYTES_MIN, ASSET_EXPORT_MAX_DECODED_MEGAPIXELS, ASSET_EXPORT_MAX_ENCODED_BYTES, ASSET_MAX_CONCURRENT_RESOLUTIONS, ASSET_MAX_DECODED_MEGAPIXELS, ASSET_MAX_DIMENSION_PX, ASSET_MAX_ENCODED_BYTES, ASSET_REF_MAX_BYTES, ASSET_REF_PATTERN, AddImageCommand, AddNoteCommand, AddStrokesCommand, AddTableCommand, AddTextCommand, AddTimerCommand, AssetResolutionError, BEACON_INSET, BoardDocument, CURRENT_DOCUMENT_SCHEMA_VERSION, ClusterStore, DefaultBoardChrome, DeleteImageCommand, DeleteNoteCommand, DeleteStrokesCommand, DeleteTableCommand, DeleteTextCommand, DeleteTimerCommand, DocumentRecoveryError, END_TAPER, ERASE_THRESHOLD, EraseCommand, FOG_COLOR, FocusedItemToolbar, HIGHLIGHT_COLORS, History, IDENTITY, INK_COLORS, InlineEditors, LockItemsCommand, MIN_WIDTH_FACTOR, MultiplayerCursors, NOTE_COLORS, NOTE_DEFAULT_SIZE, NOTE_DEFAULT_Z, NOTE_MAX_Z, NOTE_MIN_Z, NOTE_PEEL_STEP, SDK_DEVELOPMENT_VERSION, SDK_PACKAGE_NAME, STAMPS, STAMP_SIZE, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, SpatialIndex, StyleShelf, TABLE_DEFAULT_CELL_HEIGHT, TABLE_DEFAULT_CELL_WIDTH, TABLE_DEFAULT_FONT_SIZE, TEXT_DEFAULT_SIZE, TIMER_DEFAULT_DURATION_MS, TIMER_DEFAULT_SIZE, TIMER_PRESETS_MS, TransformCommand, UpdateImageCommand, UpdateNoteCommand, UpdateTableCommand, UpdateTextCommand, UpdateTimerCommand, apply, applyItemLock, assetRef, avgScale, canUnlockItem, changeToOps, clampAssetCacheBytes, cloneCustomObject, cloneImage, cloneNote, cloneStroke, cloneTable, cloneText, cloneTimer, createBoardController, createLocalBoard, documentId, documentToSVG, formatTimer, invert, isAssetRef, isIdentity, isStampKind, loadDocumentBytes, measureTable, measureTextBlock, migrateDocument, mul, pauseTimer, placePresenceBeacon, resolveScrawlTheme, ribbonEdges, rotationAbout, scalingAbout, scrawlThemePresets, searchBoard, serializeDocument, serializeLock, serializeStroke, setTimerDuration, stampDataUrl, startTimer, strokeId, timerExpired, timerRemaining, toggleTimer, translation, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
|