@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/react.js
CHANGED
|
@@ -126,7 +126,11 @@ function validateStrokes(raw) {
|
|
|
126
126
|
for (let index = 0; index < raw.length; index += 1) {
|
|
127
127
|
const value = raw[index];
|
|
128
128
|
const path = `strokes[${index}]`;
|
|
129
|
-
if (!isRecord(value) || !nonempty(value.id) || !nonempty(value.color) || !positive(value.baseWidth) || !optionalEnum(value.tool, [
|
|
129
|
+
if (!isRecord(value) || !nonempty(value.id) || !nonempty(value.color) || !positive(value.baseWidth) || !optionalEnum(value.tool, [
|
|
130
|
+
"marker",
|
|
131
|
+
"highlighter",
|
|
132
|
+
"shape"
|
|
133
|
+
]) || !validLock(value) || !optionalString(value.clusterId) || !validMatrix(value.matrix) || !Array.isArray(value.points)) return validation("invalid stroke", path);
|
|
130
134
|
const points = [];
|
|
131
135
|
for (let pointIndex = 0; pointIndex < value.points.length; pointIndex += 1) {
|
|
132
136
|
const point = normalizePoint(value.points[pointIndex]);
|
|
@@ -407,9 +411,21 @@ var HIGHLIGHT_COLORS = {
|
|
|
407
411
|
};
|
|
408
412
|
var NOTE_COLORS = {
|
|
409
413
|
yellow: "#FDE68A",
|
|
414
|
+
amber: "#FCD34D",
|
|
415
|
+
orange: "#FDBA74",
|
|
416
|
+
peach: "#FED7AA",
|
|
417
|
+
red: "#FCA5A5",
|
|
418
|
+
rose: "#FECACA",
|
|
410
419
|
pink: "#FBCFE8",
|
|
420
|
+
magenta: "#F9A8D4",
|
|
421
|
+
purple: "#D8B4FE",
|
|
422
|
+
lavender: "#E9D5FF",
|
|
411
423
|
blue: "#BFDBFE",
|
|
412
|
-
|
|
424
|
+
cornflower: "#93C5FD",
|
|
425
|
+
sky: "#7DD3FC",
|
|
426
|
+
cyan: "#BAE6FD",
|
|
427
|
+
green: "#BBF7D0",
|
|
428
|
+
mint: "#86EFAC"
|
|
413
429
|
};
|
|
414
430
|
var TEXT_DEFAULT_SIZE = 1.8;
|
|
415
431
|
function cloneText(block) {
|
|
@@ -36362,13 +36378,26 @@ var INK_Z = .02;
|
|
|
36362
36378
|
var HIGHLIGHT_Z = .016;
|
|
36363
36379
|
/** Ghosts sit under everything so live strokes always read on top. */
|
|
36364
36380
|
var GHOST_Z = .012;
|
|
36381
|
+
/**
|
|
36382
|
+
* A miter join at each centerline point — averaging the incoming and
|
|
36383
|
+
* outgoing segment directions into one width offset — under-covers the
|
|
36384
|
+
* outside of a sharp turn: the two segments' edges diverge there, leaving a
|
|
36385
|
+
* real geometric gap (the board shows through, not just a shader artifact).
|
|
36386
|
+
* Barely visible on opaque ink, but glaring on the highlighter's lower,
|
|
36387
|
+
* multiply-blended opacity, which is why this reads as "goes transparent
|
|
36388
|
+
* at corners" specifically there even though the underlying strip geometry
|
|
36389
|
+
* is shared with ink. Below this turn angle a miter join is a fine
|
|
36390
|
+
* approximation and not worth the extra geometry.
|
|
36391
|
+
*/
|
|
36392
|
+
var JOIN_ANGLE_THRESHOLD = .35;
|
|
36393
|
+
var JOIN_SEGMENTS = 10;
|
|
36365
36394
|
function buildRibbonGeometry(points, baseWidth, z = INK_Z, handDrawn = true) {
|
|
36366
36395
|
const edges = ribbonEdges(points, baseWidth, handDrawn);
|
|
36367
36396
|
const n = edges.length;
|
|
36368
|
-
const positions = new
|
|
36369
|
-
const uvs = new
|
|
36370
|
-
const alphas = new
|
|
36371
|
-
const indices = new
|
|
36397
|
+
const positions = new Array(n * 2 * 3);
|
|
36398
|
+
const uvs = new Array(n * 2 * 2);
|
|
36399
|
+
const alphas = new Array(n * 2);
|
|
36400
|
+
const indices = new Array((n - 1) * 6);
|
|
36372
36401
|
for (let i = 0; i < n; i++) {
|
|
36373
36402
|
const e = edges[i];
|
|
36374
36403
|
const vi = i * 6;
|
|
@@ -36397,11 +36426,45 @@ function buildRibbonGeometry(points, baseWidth, z = INK_Z, handDrawn = true) {
|
|
|
36397
36426
|
indices[ii + 5] = a + 2;
|
|
36398
36427
|
}
|
|
36399
36428
|
}
|
|
36429
|
+
for (let i = 1; i < n - 1; i++) {
|
|
36430
|
+
const prev = points[Math.max(0, i - 1)];
|
|
36431
|
+
const point = points[i];
|
|
36432
|
+
const next = points[Math.min(n - 1, i + 1)];
|
|
36433
|
+
const inX = point.x - prev.x, inY = point.y - prev.y;
|
|
36434
|
+
const outX = next.x - point.x, outY = next.y - point.y;
|
|
36435
|
+
const inLen = Math.hypot(inX, inY) || 1;
|
|
36436
|
+
const outLen = Math.hypot(outX, outY) || 1;
|
|
36437
|
+
const inDx = inX / inLen, inDy = inY / inLen;
|
|
36438
|
+
const outDx = outX / outLen, outDy = outY / outLen;
|
|
36439
|
+
const cos = inDx * outDx + inDy * outDy;
|
|
36440
|
+
if (Math.acos(Math.min(1, Math.max(-1, cos))) < JOIN_ANGLE_THRESHOLD) continue;
|
|
36441
|
+
const radius = (handDrawn ? baseWidth * (MIN_WIDTH_FACTOR + (1 - MIN_WIDTH_FACTOR) * point.pressure) : baseWidth) / 2;
|
|
36442
|
+
const t = i / (n - 1);
|
|
36443
|
+
const alpha = edges[i].alpha;
|
|
36444
|
+
const side = inDx * outDy - inDy * outDx > 0 ? -1 : 1;
|
|
36445
|
+
const inEdgeX = point.x + side * -inDy * radius, inEdgeY = point.y + side * inDx * radius;
|
|
36446
|
+
const outEdgeX = point.x + side * -outDy * radius, outEdgeY = point.y + side * outDx * radius;
|
|
36447
|
+
const angle1 = Math.atan2(inEdgeY - point.y, inEdgeX - point.x);
|
|
36448
|
+
let sweep = Math.atan2(outEdgeY - point.y, outEdgeX - point.x) - angle1;
|
|
36449
|
+
while (sweep > Math.PI) sweep -= Math.PI * 2;
|
|
36450
|
+
while (sweep < -Math.PI) sweep += Math.PI * 2;
|
|
36451
|
+
const center = positions.length / 3;
|
|
36452
|
+
positions.push(point.x, point.y, z);
|
|
36453
|
+
uvs.push(t, .5);
|
|
36454
|
+
alphas.push(alpha);
|
|
36455
|
+
for (let s = 0; s <= JOIN_SEGMENTS; s++) {
|
|
36456
|
+
const theta = angle1 + sweep * (s / JOIN_SEGMENTS);
|
|
36457
|
+
positions.push(point.x + Math.cos(theta) * radius, point.y + Math.sin(theta) * radius, z);
|
|
36458
|
+
uvs.push(t, .5);
|
|
36459
|
+
alphas.push(alpha);
|
|
36460
|
+
if (s > 0) indices.push(center, center + s, center + s + 1);
|
|
36461
|
+
}
|
|
36462
|
+
}
|
|
36400
36463
|
const geometry = new BufferGeometry();
|
|
36401
|
-
geometry.setAttribute("position", new BufferAttribute(positions, 3));
|
|
36402
|
-
geometry.setAttribute("uv", new BufferAttribute(uvs, 2));
|
|
36403
|
-
geometry.setAttribute("aAlpha", new BufferAttribute(alphas, 1));
|
|
36404
|
-
geometry.setIndex(
|
|
36464
|
+
geometry.setAttribute("position", new BufferAttribute(new Float32Array(positions), 3));
|
|
36465
|
+
geometry.setAttribute("uv", new BufferAttribute(new Float32Array(uvs), 2));
|
|
36466
|
+
geometry.setAttribute("aAlpha", new BufferAttribute(new Float32Array(alphas), 1));
|
|
36467
|
+
geometry.setIndex(indices);
|
|
36405
36468
|
geometry.computeBoundingSphere();
|
|
36406
36469
|
return geometry;
|
|
36407
36470
|
}
|
|
@@ -36512,6 +36575,7 @@ var StrokeRenderer = class {
|
|
|
36512
36575
|
if (mesh) {
|
|
36513
36576
|
mesh.geometry.dispose();
|
|
36514
36577
|
mesh.geometry = buildRibbonGeometry(stroke.points, stroke.baseWidth, strokeZ(stroke), handDrawn(stroke));
|
|
36578
|
+
mesh.material = stroke.tool === "highlighter" ? this.materials.highlight(stroke.color) : this.materials.get(stroke.color);
|
|
36515
36579
|
}
|
|
36516
36580
|
}
|
|
36517
36581
|
for (const stroke of change.transformed) {
|
|
@@ -36542,9 +36606,9 @@ function syncMatrix(mesh, stroke) {
|
|
|
36542
36606
|
}
|
|
36543
36607
|
//#endregion
|
|
36544
36608
|
//#region src/renderer/shapes/markerProp.ts
|
|
36545
|
-
var BODY_LENGTH =
|
|
36546
|
-
var BODY_RADIUS = .
|
|
36547
|
-
var TIP_LENGTH = 1.
|
|
36609
|
+
var BODY_LENGTH = 7;
|
|
36610
|
+
var BODY_RADIUS = .55;
|
|
36611
|
+
var TIP_LENGTH = 1.05;
|
|
36548
36612
|
var BASE_TILT = .42;
|
|
36549
36613
|
var VELOCITY_TILT = .22;
|
|
36550
36614
|
var MAX_EXTRA_TILT = .35;
|
|
@@ -36575,12 +36639,12 @@ var MarkerProp = class {
|
|
|
36575
36639
|
const tip = new Mesh(new ConeGeometry(BODY_RADIUS * .55, TIP_LENGTH, 14), tipMaterial);
|
|
36576
36640
|
tip.rotation.x = Math.PI;
|
|
36577
36641
|
tip.position.y = TIP_LENGTH / 2;
|
|
36578
|
-
this.body.position.y =
|
|
36642
|
+
this.body.position.y = 4.55;
|
|
36579
36643
|
const pen = new Group();
|
|
36580
36644
|
pen.add(this.body, tip);
|
|
36581
36645
|
pen.rotation.x = Math.PI / 2;
|
|
36582
36646
|
this.group.add(pen);
|
|
36583
|
-
this.shadow = new Mesh(new CircleGeometry(
|
|
36647
|
+
this.shadow = new Mesh(new CircleGeometry(.9, 24), new MeshBasicMaterial({
|
|
36584
36648
|
color: 0,
|
|
36585
36649
|
transparent: true,
|
|
36586
36650
|
opacity: .13,
|
|
@@ -44691,7 +44755,7 @@ function distToSegmentSq(p, a, b) {
|
|
|
44691
44755
|
}
|
|
44692
44756
|
//#endregion
|
|
44693
44757
|
//#region src/interaction/tools/eraserTool.ts
|
|
44694
|
-
var DECAY_PER_PASS =
|
|
44758
|
+
var DECAY_PER_PASS = 1;
|
|
44695
44759
|
/** A point can take another decay pass after this long — rubbing works. */
|
|
44696
44760
|
var REARM_MS = 250;
|
|
44697
44761
|
var EraserTool = class {
|
|
@@ -44785,19 +44849,11 @@ var EraserTool = class {
|
|
|
44785
44849
|
const runs = partitionByErasure(current.points);
|
|
44786
44850
|
const survives = runs.filter((r) => r.survives && r.points.length >= 2);
|
|
44787
44851
|
const keepId = survives.length === 1 && survives[0].points.length === current.points.length;
|
|
44788
|
-
const m = current.matrix;
|
|
44789
44852
|
for (const run of runs) if (run.survives && run.points.length >= 2) after.push({
|
|
44790
44853
|
...current,
|
|
44791
44854
|
id: keepId ? current.id : crypto.randomUUID(),
|
|
44792
44855
|
points: run.points
|
|
44793
44856
|
});
|
|
44794
|
-
else if (!run.survives) {
|
|
44795
|
-
const world = m ? run.points.map((p) => ({
|
|
44796
|
-
...p,
|
|
44797
|
-
...apply(m, p)
|
|
44798
|
-
})) : run.points;
|
|
44799
|
-
this.ctx.renderer.addGhost(world, current.color, current.baseWidth * (m ? avgScale(m) : 1), current.tool !== "shape");
|
|
44800
|
-
}
|
|
44801
44857
|
}
|
|
44802
44858
|
doc.removeStrokes(before.map((s) => s.id));
|
|
44803
44859
|
doc.addStrokes(after.map(cloneStroke));
|
|
@@ -45005,10 +45061,10 @@ function buildShape(kind, a, b, step, constrain) {
|
|
|
45005
45061
|
case "ellipse": return [ellipse(a, targetB, step)];
|
|
45006
45062
|
case "line": return [sampleSegment(a, constrain ? snappedEnd(a, b) : b, step)];
|
|
45007
45063
|
case "arrow": return arrow(a, constrain ? snappedEnd(a, b) : b, step);
|
|
45008
|
-
case "triangle": return [polygon(3, a, targetB, step,
|
|
45064
|
+
case "triangle": return [polygon(3, a, targetB, step, Math.PI / 2)];
|
|
45009
45065
|
case "diamond": return [polygon(4, a, targetB, step, 0)];
|
|
45010
|
-
case "pentagon": return [polygon(5, a, targetB, step,
|
|
45011
|
-
case "hexagon": return [polygon(6, a, targetB, step,
|
|
45066
|
+
case "pentagon": return [polygon(5, a, targetB, step, Math.PI / 2)];
|
|
45067
|
+
case "hexagon": return [polygon(6, a, targetB, step, Math.PI / 2)];
|
|
45012
45068
|
case "octagon": return [polygon(8, a, targetB, step, Math.PI / 8)];
|
|
45013
45069
|
case "star": return [star(5, a, targetB, step)];
|
|
45014
45070
|
case "heart": return [heart(a, targetB, step)];
|
|
@@ -45207,6 +45263,7 @@ var ShapeTool = class {
|
|
|
45207
45263
|
this.ctx.clusters.assign(strokes[0]);
|
|
45208
45264
|
for (const s of strokes) s.clusterId = strokes[0].clusterId;
|
|
45209
45265
|
this.ctx.history.execute(new AddStrokesCommand(strokes));
|
|
45266
|
+
this.ctx.selection.set(strokes.map((s) => s.id));
|
|
45210
45267
|
this.ctx.setTool?.("select");
|
|
45211
45268
|
}
|
|
45212
45269
|
this.ctx.transition("idle");
|
|
@@ -45788,6 +45845,13 @@ var HANDLE_HIT_PX = 12;
|
|
|
45788
45845
|
var ROTATE_OFFSET_PX = 26;
|
|
45789
45846
|
var ROTATE_HIT_PX = 14;
|
|
45790
45847
|
var COLOR = 2450411;
|
|
45848
|
+
/**
|
|
45849
|
+
* Visual-only outset between the selected object and the gizmo's outline/
|
|
45850
|
+
* handles — the object's actual bounding box (used for resize math, see
|
|
45851
|
+
* `anchorPoint`/`handlePoint`'s callers in `selectTool.ts`) never changes;
|
|
45852
|
+
* only where the outline and handle meshes are *drawn* and hit-tested does.
|
|
45853
|
+
*/
|
|
45854
|
+
var GAP_PX = 8;
|
|
45791
45855
|
var SelectionGizmo = class {
|
|
45792
45856
|
constructor(scene) {
|
|
45793
45857
|
this.group = new Group();
|
|
@@ -45802,7 +45866,7 @@ var SelectionGizmo = class {
|
|
|
45802
45866
|
this.outline = new LineLoop(new BufferGeometry(), this.lineMaterial);
|
|
45803
45867
|
this.outline.position.z = GIZMO_Z;
|
|
45804
45868
|
this.outline.frustumCulled = false;
|
|
45805
|
-
const handleGeometry = new
|
|
45869
|
+
const handleGeometry = new CircleGeometry(.5, 20);
|
|
45806
45870
|
const handleMaterial = new MeshBasicMaterial({ color: COLOR });
|
|
45807
45871
|
const makeHandle = () => new Mesh(handleGeometry, handleMaterial);
|
|
45808
45872
|
this.handles = {
|
|
@@ -45830,18 +45894,19 @@ var SelectionGizmo = class {
|
|
|
45830
45894
|
if (!box) return;
|
|
45831
45895
|
this.lineMaterial.color.setHex(locked ? 16096779 : COLOR);
|
|
45832
45896
|
this.rotateHandle.visible = !locked;
|
|
45897
|
+
const visual = this.visualBox(box, wpp);
|
|
45833
45898
|
const positions = new Float32Array([
|
|
45834
|
-
|
|
45835
|
-
|
|
45899
|
+
visual.minX,
|
|
45900
|
+
visual.minY,
|
|
45836
45901
|
GIZMO_Z,
|
|
45837
|
-
|
|
45838
|
-
|
|
45902
|
+
visual.maxX,
|
|
45903
|
+
visual.minY,
|
|
45839
45904
|
GIZMO_Z,
|
|
45840
|
-
|
|
45841
|
-
|
|
45905
|
+
visual.maxX,
|
|
45906
|
+
visual.maxY,
|
|
45842
45907
|
GIZMO_Z,
|
|
45843
|
-
|
|
45844
|
-
|
|
45908
|
+
visual.minX,
|
|
45909
|
+
visual.maxY,
|
|
45845
45910
|
GIZMO_Z
|
|
45846
45911
|
]);
|
|
45847
45912
|
this.outline.geometry.dispose();
|
|
@@ -45853,17 +45918,27 @@ var SelectionGizmo = class {
|
|
|
45853
45918
|
for (const [handle, mesh] of Object.entries(this.handles)) {
|
|
45854
45919
|
mesh.visible = !locked;
|
|
45855
45920
|
if (!locked) {
|
|
45856
|
-
const p = this.handlePoint(handle,
|
|
45921
|
+
const p = this.handlePoint(handle, visual);
|
|
45857
45922
|
mesh.position.set(p.x, p.y, GIZMO_Z);
|
|
45858
45923
|
mesh.scale.set(size, size, 1);
|
|
45859
45924
|
}
|
|
45860
45925
|
}
|
|
45861
45926
|
if (!locked) {
|
|
45862
|
-
const rotate = this.rotatePoint(
|
|
45927
|
+
const rotate = this.rotatePoint(visual, wpp);
|
|
45863
45928
|
this.rotateHandle.position.set(rotate.x, rotate.y, GIZMO_Z);
|
|
45864
45929
|
this.rotateHandle.scale.set(size, size, 1);
|
|
45865
45930
|
}
|
|
45866
45931
|
}
|
|
45932
|
+
/** 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`). */
|
|
45933
|
+
visualBox(box, wpp) {
|
|
45934
|
+
const gap = GAP_PX * wpp;
|
|
45935
|
+
return {
|
|
45936
|
+
minX: box.minX - gap,
|
|
45937
|
+
minY: box.minY - gap,
|
|
45938
|
+
maxX: box.maxX + gap,
|
|
45939
|
+
maxY: box.maxY + gap
|
|
45940
|
+
};
|
|
45941
|
+
}
|
|
45867
45942
|
handlePoint(handle, box) {
|
|
45868
45943
|
const midX = (box.minX + box.maxX) / 2;
|
|
45869
45944
|
const midY = (box.minY + box.maxY) / 2;
|
|
@@ -45951,7 +46026,8 @@ var SelectionGizmo = class {
|
|
|
45951
46026
|
if (!this.box || this.isLocked) return null;
|
|
45952
46027
|
const handleR = HANDLE_HIT_PX * this.wpp;
|
|
45953
46028
|
const edgeSlop = 6 * this.wpp;
|
|
45954
|
-
const
|
|
46029
|
+
const visual = this.visualBox(this.box, this.wpp);
|
|
46030
|
+
const rotate = this.rotatePoint(visual, this.wpp);
|
|
45955
46031
|
if (Math.hypot(p.x - rotate.x, p.y - rotate.y) < ROTATE_HIT_PX * this.wpp) return { kind: "rotate" };
|
|
45956
46032
|
for (const handle of [
|
|
45957
46033
|
"nw",
|
|
@@ -45963,13 +46039,13 @@ var SelectionGizmo = class {
|
|
|
45963
46039
|
"e",
|
|
45964
46040
|
"w"
|
|
45965
46041
|
]) {
|
|
45966
|
-
const hp = this.handlePoint(handle,
|
|
46042
|
+
const hp = this.handlePoint(handle, visual);
|
|
45967
46043
|
if (Math.abs(p.x - hp.x) < handleR && Math.abs(p.y - hp.y) < handleR) return {
|
|
45968
46044
|
kind: "scale",
|
|
45969
46045
|
handle
|
|
45970
46046
|
};
|
|
45971
46047
|
}
|
|
45972
|
-
const { minX, maxX, minY, maxY } =
|
|
46048
|
+
const { minX, maxX, minY, maxY } = visual;
|
|
45973
46049
|
const inX = p.x >= minX - edgeSlop && p.x <= maxX + edgeSlop;
|
|
45974
46050
|
const inY = p.y >= minY - edgeSlop && p.y <= maxY + edgeSlop;
|
|
45975
46051
|
if (inX && Math.abs(p.y - maxY) < edgeSlop) return {
|
|
@@ -45989,7 +46065,7 @@ var SelectionGizmo = class {
|
|
|
45989
46065
|
handle: "w"
|
|
45990
46066
|
};
|
|
45991
46067
|
const pad = 2 * this.wpp;
|
|
45992
|
-
if (p.x >= minX - pad && p.x <= maxX + pad && p.y >= minY - pad && p.y <= maxY + pad) return { kind: "inside" };
|
|
46068
|
+
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" };
|
|
45993
46069
|
return null;
|
|
45994
46070
|
}
|
|
45995
46071
|
rotatePoint(box, wpp) {
|
|
@@ -48222,6 +48298,112 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48222
48298
|
})),
|
|
48223
48299
|
...[...document.allCustomObjects()].map((value) => clone(toBoardObject(value)))
|
|
48224
48300
|
];
|
|
48301
|
+
const projectToScreen = (point) => engine ? engine.boardToScreen(point) : {
|
|
48302
|
+
x: (point.x - view.x) * view.zoom,
|
|
48303
|
+
y: (view.y - point.y) * view.zoom
|
|
48304
|
+
};
|
|
48305
|
+
const lockFields = (item) => ({
|
|
48306
|
+
locked: !!item.locked,
|
|
48307
|
+
...item.lockedBy ? { lockedBy: item.lockedBy } : {},
|
|
48308
|
+
...item.lockedByName ? { lockedByName: item.lockedByName } : {}
|
|
48309
|
+
});
|
|
48310
|
+
const lockFieldsMany = (items) => {
|
|
48311
|
+
const locked = items.filter((i) => i.locked);
|
|
48312
|
+
if (locked.length === 0) return { locked: false };
|
|
48313
|
+
const lockedBys = [...new Set(locked.map((i) => i.lockedBy).filter((v) => !!v))];
|
|
48314
|
+
const names = [...new Set(locked.map((i) => i.lockedByName).filter((v) => !!v))];
|
|
48315
|
+
return {
|
|
48316
|
+
locked: true,
|
|
48317
|
+
...lockedBys.length === 1 ? { lockedBy: lockedBys[0] } : {},
|
|
48318
|
+
...names.length === 1 ? { lockedByName: names[0] } : {}
|
|
48319
|
+
};
|
|
48320
|
+
};
|
|
48321
|
+
const strokeClusterAnchor = (strokes) => {
|
|
48322
|
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
48323
|
+
for (const s of strokes) for (const p of s.points) {
|
|
48324
|
+
const world = s.matrix ? apply(s.matrix, p) : p;
|
|
48325
|
+
minX = Math.min(minX, world.x);
|
|
48326
|
+
maxX = Math.max(maxX, world.x);
|
|
48327
|
+
minY = Math.min(minY, world.y);
|
|
48328
|
+
maxY = Math.max(maxY, world.y);
|
|
48329
|
+
}
|
|
48330
|
+
if (!Number.isFinite(minX)) return projectToScreen({
|
|
48331
|
+
x: 0,
|
|
48332
|
+
y: 0
|
|
48333
|
+
});
|
|
48334
|
+
return projectToScreen({
|
|
48335
|
+
x: (minX + maxX) / 2,
|
|
48336
|
+
y: maxY + 1.4
|
|
48337
|
+
});
|
|
48338
|
+
};
|
|
48339
|
+
/** Per-type anchor formulas mirror `ScrawlEngine.getSelectedItemInfo` exactly, so a Host-built toolbar lands in the same spot the SDK's own would. */
|
|
48340
|
+
const anchorFor = (obj) => {
|
|
48341
|
+
switch (obj.type) {
|
|
48342
|
+
case "note": return projectToScreen({
|
|
48343
|
+
x: obj.x,
|
|
48344
|
+
y: obj.y + obj.size / 2 + 1
|
|
48345
|
+
});
|
|
48346
|
+
case "text": {
|
|
48347
|
+
const { width } = measureTextBlock(obj.text, obj.fontSize);
|
|
48348
|
+
return projectToScreen({
|
|
48349
|
+
x: obj.x + width / 2,
|
|
48350
|
+
y: obj.y + 1
|
|
48351
|
+
});
|
|
48352
|
+
}
|
|
48353
|
+
case "table": {
|
|
48354
|
+
const { width } = measureTable(obj);
|
|
48355
|
+
return projectToScreen({
|
|
48356
|
+
x: obj.x + width / 2,
|
|
48357
|
+
y: obj.y + 1.2
|
|
48358
|
+
});
|
|
48359
|
+
}
|
|
48360
|
+
case "image": return projectToScreen({
|
|
48361
|
+
x: obj.x,
|
|
48362
|
+
y: obj.y + obj.height / 2 + 1.2
|
|
48363
|
+
});
|
|
48364
|
+
case "timer": return projectToScreen({
|
|
48365
|
+
x: obj.x,
|
|
48366
|
+
y: obj.y + obj.size / 2 + 1.2
|
|
48367
|
+
});
|
|
48368
|
+
case "custom": {
|
|
48369
|
+
const b = obj.fallback.bounds;
|
|
48370
|
+
return projectToScreen(apply(obj.transform, {
|
|
48371
|
+
x: b.x + b.width / 2,
|
|
48372
|
+
y: b.y + b.height + 1
|
|
48373
|
+
}));
|
|
48374
|
+
}
|
|
48375
|
+
case "stroke": return strokeClusterAnchor([obj]);
|
|
48376
|
+
}
|
|
48377
|
+
};
|
|
48378
|
+
const computeFocusedItem = () => {
|
|
48379
|
+
if (selection.length === 0) return null;
|
|
48380
|
+
if (selection.length === 1) {
|
|
48381
|
+
const obj = readObject(selection[0]);
|
|
48382
|
+
if (!obj) return null;
|
|
48383
|
+
if (obj.type === "custom") return {
|
|
48384
|
+
type: "custom",
|
|
48385
|
+
id: obj.id,
|
|
48386
|
+
locked: false,
|
|
48387
|
+
screenPosition: anchorFor(obj)
|
|
48388
|
+
};
|
|
48389
|
+
return {
|
|
48390
|
+
type: obj.type,
|
|
48391
|
+
id: obj.id,
|
|
48392
|
+
...lockFields(obj),
|
|
48393
|
+
screenPosition: anchorFor(obj)
|
|
48394
|
+
};
|
|
48395
|
+
}
|
|
48396
|
+
const objects = selection.map(readObject);
|
|
48397
|
+
if (!objects.every((o) => o?.type === "stroke")) return null;
|
|
48398
|
+
const clusterId = objects[0].clusterId;
|
|
48399
|
+
if (!clusterId || !objects.every((o) => o.clusterId === clusterId)) return null;
|
|
48400
|
+
return {
|
|
48401
|
+
type: "stroke",
|
|
48402
|
+
id: objects[0].id,
|
|
48403
|
+
...lockFieldsMany(objects),
|
|
48404
|
+
screenPosition: strokeClusterAnchor(objects)
|
|
48405
|
+
};
|
|
48406
|
+
};
|
|
48225
48407
|
const updateObject = (id, patch) => {
|
|
48226
48408
|
assertMutable();
|
|
48227
48409
|
const before = readObject(id);
|
|
@@ -48263,6 +48445,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48263
48445
|
zoom: view.zoom,
|
|
48264
48446
|
readOnly,
|
|
48265
48447
|
selection: Object.freeze([...selection]),
|
|
48448
|
+
focusedItem: computeFocusedItem(),
|
|
48266
48449
|
strokeCount: [...document.all()].length,
|
|
48267
48450
|
objectCount: allObjects().length,
|
|
48268
48451
|
canUndo: history.canUndo,
|
|
@@ -48677,10 +48860,7 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48677
48860
|
changed();
|
|
48678
48861
|
},
|
|
48679
48862
|
get: () => ({ ...view }),
|
|
48680
|
-
boardToScreen: (point) =>
|
|
48681
|
-
x: (point.x - view.x) * view.zoom,
|
|
48682
|
-
y: (view.y - point.y) * view.zoom
|
|
48683
|
-
},
|
|
48863
|
+
boardToScreen: (point) => projectToScreen(point),
|
|
48684
48864
|
screenToBoard: (point) => engine ? engine.screenToBoard(point.x, point.y) ?? {
|
|
48685
48865
|
x: point.x,
|
|
48686
48866
|
y: point.y
|
|
@@ -48717,6 +48897,81 @@ function createBoardControllerWithEngine(options, existingEngine) {
|
|
|
48717
48897
|
update(id, patch) {
|
|
48718
48898
|
updateObject(id, patch);
|
|
48719
48899
|
},
|
|
48900
|
+
duplicate(ids) {
|
|
48901
|
+
assertMutable();
|
|
48902
|
+
const objects = [...new Set(ids)].map(readObject).filter((o) => !!o);
|
|
48903
|
+
if (objects.length === 0) return [];
|
|
48904
|
+
const DX = 1.5;
|
|
48905
|
+
const DY = -1.5;
|
|
48906
|
+
const newClusterIds = /* @__PURE__ */ new Map();
|
|
48907
|
+
const clusterIdFor = (oldClusterId) => {
|
|
48908
|
+
let next = newClusterIds.get(oldClusterId);
|
|
48909
|
+
if (!next) {
|
|
48910
|
+
next = crypto.randomUUID();
|
|
48911
|
+
newClusterIds.set(oldClusterId, next);
|
|
48912
|
+
}
|
|
48913
|
+
return next;
|
|
48914
|
+
};
|
|
48915
|
+
const commands = [];
|
|
48916
|
+
const newIds = [];
|
|
48917
|
+
for (const obj of objects) {
|
|
48918
|
+
const id = createId();
|
|
48919
|
+
newIds.push(id);
|
|
48920
|
+
const rest = { ...obj };
|
|
48921
|
+
delete rest.locked;
|
|
48922
|
+
delete rest.lockedBy;
|
|
48923
|
+
delete rest.lockedByName;
|
|
48924
|
+
let duplicated;
|
|
48925
|
+
if (obj.type === "stroke") {
|
|
48926
|
+
const matrix = obj.matrix ?? [
|
|
48927
|
+
1,
|
|
48928
|
+
0,
|
|
48929
|
+
0,
|
|
48930
|
+
1,
|
|
48931
|
+
0,
|
|
48932
|
+
0
|
|
48933
|
+
];
|
|
48934
|
+
duplicated = {
|
|
48935
|
+
...rest,
|
|
48936
|
+
id,
|
|
48937
|
+
matrix: [
|
|
48938
|
+
matrix[0],
|
|
48939
|
+
matrix[1],
|
|
48940
|
+
matrix[2],
|
|
48941
|
+
matrix[3],
|
|
48942
|
+
matrix[4] + DX,
|
|
48943
|
+
matrix[5] + DY
|
|
48944
|
+
],
|
|
48945
|
+
...obj.clusterId ? { clusterId: clusterIdFor(obj.clusterId) } : {}
|
|
48946
|
+
};
|
|
48947
|
+
} else if (obj.type === "custom") {
|
|
48948
|
+
const t = obj.transform;
|
|
48949
|
+
duplicated = {
|
|
48950
|
+
...rest,
|
|
48951
|
+
id,
|
|
48952
|
+
transform: [
|
|
48953
|
+
t[0],
|
|
48954
|
+
t[1],
|
|
48955
|
+
t[2],
|
|
48956
|
+
t[3],
|
|
48957
|
+
t[4] + DX,
|
|
48958
|
+
t[5] + DY
|
|
48959
|
+
]
|
|
48960
|
+
};
|
|
48961
|
+
delete duplicated.lock;
|
|
48962
|
+
} else duplicated = {
|
|
48963
|
+
...rest,
|
|
48964
|
+
id,
|
|
48965
|
+
x: obj.x + DX,
|
|
48966
|
+
y: obj.y + DY
|
|
48967
|
+
};
|
|
48968
|
+
validateBoardObject(duplicated.type, duplicated);
|
|
48969
|
+
commands.push(addCommand(duplicated.type, duplicated));
|
|
48970
|
+
}
|
|
48971
|
+
executeHistory(() => history.execute(new CommandBatch("duplicate content", commands)));
|
|
48972
|
+
changed();
|
|
48973
|
+
return newIds;
|
|
48974
|
+
},
|
|
48720
48975
|
table: {
|
|
48721
48976
|
addRow(tableId) {
|
|
48722
48977
|
const table = document.getTable(tableId);
|
|
@@ -49650,6 +49905,24 @@ function InlineEditors({ controller, renderPortal }) {
|
|
|
49650
49905
|
};
|
|
49651
49906
|
useEffect(() => controller.on("edit-request", (event) => {
|
|
49652
49907
|
const offset = rootOffset();
|
|
49908
|
+
if (event.kind === "text" && event.id === null) {
|
|
49909
|
+
const newId = controller.content.add({
|
|
49910
|
+
type: "text",
|
|
49911
|
+
x: event.boardX,
|
|
49912
|
+
y: event.boardY,
|
|
49913
|
+
text: "",
|
|
49914
|
+
color: event.color,
|
|
49915
|
+
fontSize: TEXT_DEFAULT_SIZE
|
|
49916
|
+
});
|
|
49917
|
+
controller.content.select([newId]);
|
|
49918
|
+
setEditor({
|
|
49919
|
+
...event,
|
|
49920
|
+
id: newId,
|
|
49921
|
+
left: event.screenRect.left - offset.left,
|
|
49922
|
+
top: event.screenRect.top - offset.top
|
|
49923
|
+
});
|
|
49924
|
+
return;
|
|
49925
|
+
}
|
|
49653
49926
|
setEditor({
|
|
49654
49927
|
...event,
|
|
49655
49928
|
left: event.screenRect.left - offset.left,
|
|
@@ -49661,19 +49934,11 @@ function InlineEditors({ controller, renderPortal }) {
|
|
|
49661
49934
|
setEditor(null);
|
|
49662
49935
|
};
|
|
49663
49936
|
const commitText = () => {
|
|
49664
|
-
if (editor?.kind === "text") {
|
|
49937
|
+
if (editor?.kind === "text" && editor.id !== null) {
|
|
49665
49938
|
const text = editor.text;
|
|
49666
|
-
|
|
49667
|
-
|
|
49668
|
-
|
|
49669
|
-
x: editor.boardX,
|
|
49670
|
-
y: editor.boardY,
|
|
49671
|
-
text,
|
|
49672
|
-
color: editor.color,
|
|
49673
|
-
fontSize: TEXT_DEFAULT_SIZE
|
|
49674
|
-
});
|
|
49675
|
-
} else if (!text.trim()) controller.content.remove([editor.id]);
|
|
49676
|
-
else controller.content.update(editor.id, { text });
|
|
49939
|
+
const existing = controller.query.get(editor.id);
|
|
49940
|
+
if (!text.trim()) controller.content.remove([editor.id]);
|
|
49941
|
+
else if (existing?.type !== "text" || existing.text !== text) controller.content.update(editor.id, { text });
|
|
49677
49942
|
}
|
|
49678
49943
|
setEditor(null);
|
|
49679
49944
|
};
|
|
@@ -49866,6 +50131,366 @@ function InlineEditors({ controller, renderPortal }) {
|
|
|
49866
50131
|
}))] });
|
|
49867
50132
|
}
|
|
49868
50133
|
//#endregion
|
|
50134
|
+
//#region src/ui/toolbar/focused-item-toolbar.tsx
|
|
50135
|
+
var NOTE_SIZES = [
|
|
50136
|
+
["Small", 7],
|
|
50137
|
+
["Medium", 10],
|
|
50138
|
+
["Large", 14]
|
|
50139
|
+
];
|
|
50140
|
+
var TEXT_SIZES = [
|
|
50141
|
+
["Small", 1.2],
|
|
50142
|
+
["Medium", 1.8],
|
|
50143
|
+
["Large", 2.6]
|
|
50144
|
+
];
|
|
50145
|
+
var SHAPE_WIDTHS = [
|
|
50146
|
+
["Thin", .1],
|
|
50147
|
+
["Medium", .15],
|
|
50148
|
+
["Thick", .3]
|
|
50149
|
+
];
|
|
50150
|
+
/** Matches the native selection gizmo's own outset (renderer/selection/gizmo.ts's GAP_PX), for a consistent look. */
|
|
50151
|
+
var HANDLE_GAP_PX = 8;
|
|
50152
|
+
var MIN_SIZE = 4;
|
|
50153
|
+
var MAX_SIZE = 30;
|
|
50154
|
+
var HANDLE_CORNERS = [
|
|
50155
|
+
"nw",
|
|
50156
|
+
"ne",
|
|
50157
|
+
"sw",
|
|
50158
|
+
"se"
|
|
50159
|
+
];
|
|
50160
|
+
/**
|
|
50161
|
+
* Floating toolbar above the focused note, shape, or text block — Colour,
|
|
50162
|
+
* Size (note/text) or Width (shape), Lock/Unlock, Duplicate, Delete.
|
|
50163
|
+
* Table/image/timer/custom objects and plain (non-shape) ink strokes never
|
|
50164
|
+
* get a toolbar here — out of scope for this destination.
|
|
50165
|
+
*
|
|
50166
|
+
* Lock/Unlock carries no per-user ownership gating: nothing else in this
|
|
50167
|
+
* SDK enforces lock ownership either (`content.update` never checks
|
|
50168
|
+
* `locked`, and the engine's own internal unlock check has no way for a
|
|
50169
|
+
* Host to ever supply a real user id) — locking is advisory UI state
|
|
50170
|
+
* throughout, and this toolbar matches that rather than inventing an
|
|
50171
|
+
* enforcement story alone.
|
|
50172
|
+
*
|
|
50173
|
+
* A shape can be several strokes sharing one `clusterId` (e.g. an arrow's
|
|
50174
|
+
* shaft + head) — every action here applies to the whole cluster. Resolved
|
|
50175
|
+
* via `query.all()` + `clusterId`, not `snapshot.selection`: a canvas click
|
|
50176
|
+
* on one member selects every member internally, but the public selection
|
|
50177
|
+
* bridge only ever reports one id (`FocusedItem.id` is singular by design),
|
|
50178
|
+
* so reconstructing the cluster from the document is the reliable path
|
|
50179
|
+
* regardless of how the selection was made.
|
|
50180
|
+
*
|
|
50181
|
+
* A focused note also gets drag-to-resize corner handles — notes have no
|
|
50182
|
+
* gizmo of their own (the native selection gizmo is ink-stroke/shape-only),
|
|
50183
|
+
* so this is that capability's default-ui-owned equivalent. Proportional
|
|
50184
|
+
* (always-square) resize from the note's own centre, clamped to
|
|
50185
|
+
* `[MIN_SIZE, MAX_SIZE]`, independent of which corner is grabbed.
|
|
50186
|
+
*/
|
|
50187
|
+
function FocusedItemToolbar({ controller, snapshot }) {
|
|
50188
|
+
const focused = snapshot.focusedItem;
|
|
50189
|
+
if (!focused) return null;
|
|
50190
|
+
const obj = controller.query.get(focused.id);
|
|
50191
|
+
if (!obj) return null;
|
|
50192
|
+
if (obj.type === "note") return /* @__PURE__ */ jsx(NoteToolbar, {
|
|
50193
|
+
controller,
|
|
50194
|
+
snapshot,
|
|
50195
|
+
note: obj,
|
|
50196
|
+
locked: focused.locked,
|
|
50197
|
+
anchor: focused.screenPosition
|
|
50198
|
+
});
|
|
50199
|
+
if (obj.type === "text") return /* @__PURE__ */ jsx(TextToolbar, {
|
|
50200
|
+
controller,
|
|
50201
|
+
snapshot,
|
|
50202
|
+
text: obj,
|
|
50203
|
+
locked: focused.locked,
|
|
50204
|
+
anchor: focused.screenPosition
|
|
50205
|
+
});
|
|
50206
|
+
if (obj.type === "stroke" && obj.tool === "shape") return /* @__PURE__ */ jsx(ShapeToolbar, {
|
|
50207
|
+
controller,
|
|
50208
|
+
snapshot,
|
|
50209
|
+
stroke: obj,
|
|
50210
|
+
locked: focused.locked,
|
|
50211
|
+
anchor: focused.screenPosition
|
|
50212
|
+
});
|
|
50213
|
+
return null;
|
|
50214
|
+
}
|
|
50215
|
+
function ToolbarShell({ anchor, children }) {
|
|
50216
|
+
if (!anchor) return null;
|
|
50217
|
+
return /* @__PURE__ */ jsx("div", {
|
|
50218
|
+
"aria-label": "Selected object",
|
|
50219
|
+
className: "scrawl-board__focus-toolbar",
|
|
50220
|
+
role: "toolbar",
|
|
50221
|
+
style: {
|
|
50222
|
+
left: anchor.x,
|
|
50223
|
+
top: anchor.y
|
|
50224
|
+
},
|
|
50225
|
+
children
|
|
50226
|
+
});
|
|
50227
|
+
}
|
|
50228
|
+
function NoteToolbar({ controller, snapshot, note, locked, anchor }) {
|
|
50229
|
+
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
50230
|
+
const locked_ = !mutable || locked;
|
|
50231
|
+
const half = note.size / 2;
|
|
50232
|
+
const cornerA = controller.view.boardToScreen({
|
|
50233
|
+
x: note.x - half,
|
|
50234
|
+
y: note.y + half
|
|
50235
|
+
});
|
|
50236
|
+
const cornerB = controller.view.boardToScreen({
|
|
50237
|
+
x: note.x + half,
|
|
50238
|
+
y: note.y - half
|
|
50239
|
+
});
|
|
50240
|
+
const outline = {
|
|
50241
|
+
left: Math.min(cornerA.x, cornerB.x) - HANDLE_GAP_PX,
|
|
50242
|
+
top: Math.min(cornerA.y, cornerB.y) - HANDLE_GAP_PX,
|
|
50243
|
+
right: Math.max(cornerA.x, cornerB.x) + HANDLE_GAP_PX,
|
|
50244
|
+
bottom: Math.max(cornerA.y, cornerB.y) + HANDLE_GAP_PX
|
|
50245
|
+
};
|
|
50246
|
+
const handlePoints = {
|
|
50247
|
+
nw: {
|
|
50248
|
+
x: outline.left,
|
|
50249
|
+
y: outline.top
|
|
50250
|
+
},
|
|
50251
|
+
ne: {
|
|
50252
|
+
x: outline.right,
|
|
50253
|
+
y: outline.top
|
|
50254
|
+
},
|
|
50255
|
+
sw: {
|
|
50256
|
+
x: outline.left,
|
|
50257
|
+
y: outline.bottom
|
|
50258
|
+
},
|
|
50259
|
+
se: {
|
|
50260
|
+
x: outline.right,
|
|
50261
|
+
y: outline.bottom
|
|
50262
|
+
}
|
|
50263
|
+
};
|
|
50264
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
50265
|
+
/* @__PURE__ */ jsx("div", {
|
|
50266
|
+
"aria-hidden": "true",
|
|
50267
|
+
className: "scrawl-board__focus-outline",
|
|
50268
|
+
"data-locked": locked,
|
|
50269
|
+
style: {
|
|
50270
|
+
left: outline.left,
|
|
50271
|
+
top: outline.top,
|
|
50272
|
+
width: outline.right - outline.left,
|
|
50273
|
+
height: outline.bottom - outline.top
|
|
50274
|
+
}
|
|
50275
|
+
}),
|
|
50276
|
+
mutable && !locked && HANDLE_CORNERS.map((corner) => /* @__PURE__ */ jsx(ResizeHandle, {
|
|
50277
|
+
controller,
|
|
50278
|
+
note,
|
|
50279
|
+
corner,
|
|
50280
|
+
x: handlePoints[corner].x,
|
|
50281
|
+
y: handlePoints[corner].y
|
|
50282
|
+
}, corner)),
|
|
50283
|
+
/* @__PURE__ */ jsxs(ToolbarShell, {
|
|
50284
|
+
anchor,
|
|
50285
|
+
children: [
|
|
50286
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50287
|
+
className: "scrawl-board__style-group",
|
|
50288
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Size" }), NOTE_SIZES.map(([label, size]) => /* @__PURE__ */ jsx("button", {
|
|
50289
|
+
"aria-label": `${label} note`,
|
|
50290
|
+
"aria-pressed": note.size === size,
|
|
50291
|
+
disabled: locked_,
|
|
50292
|
+
onClick: () => controller.content.update(note.id, { size }),
|
|
50293
|
+
type: "button",
|
|
50294
|
+
children: label[0]
|
|
50295
|
+
}, label))]
|
|
50296
|
+
}),
|
|
50297
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50298
|
+
className: "scrawl-board__style-group",
|
|
50299
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Colour" }), Object.entries(NOTE_COLORS).map(([name, value]) => /* @__PURE__ */ jsx("button", {
|
|
50300
|
+
"aria-label": `${name} note`,
|
|
50301
|
+
"aria-pressed": note.color.toLowerCase() === value.toLowerCase(),
|
|
50302
|
+
disabled: locked_,
|
|
50303
|
+
onClick: () => controller.content.update(note.id, { color: value }),
|
|
50304
|
+
type: "button",
|
|
50305
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
50306
|
+
"aria-hidden": "true",
|
|
50307
|
+
style: { backgroundColor: value }
|
|
50308
|
+
})
|
|
50309
|
+
}, value))]
|
|
50310
|
+
}),
|
|
50311
|
+
/* @__PURE__ */ jsx(LockDuplicateDelete, {
|
|
50312
|
+
controller,
|
|
50313
|
+
mutable,
|
|
50314
|
+
locked,
|
|
50315
|
+
ids: [note.id]
|
|
50316
|
+
})
|
|
50317
|
+
]
|
|
50318
|
+
})
|
|
50319
|
+
] });
|
|
50320
|
+
}
|
|
50321
|
+
/**
|
|
50322
|
+
* One corner handle — drag distance from the note's own centre (its `x`/`y`
|
|
50323
|
+
* already *is* the centre) drives a proportional, always-square resize,
|
|
50324
|
+
* independent of which corner is grabbed. `note` is fresh every render (a
|
|
50325
|
+
* new `content.update` during drag triggers a new snapshot), so the
|
|
50326
|
+
* closure never goes stale.
|
|
50327
|
+
*/
|
|
50328
|
+
function ResizeHandle({ controller, note, corner, x, y }) {
|
|
50329
|
+
const [dragging, setDragging] = useState(false);
|
|
50330
|
+
const nudge = (delta) => {
|
|
50331
|
+
const half = Math.max(MIN_SIZE / 2, Math.min(MAX_SIZE / 2, note.size / 2 + delta));
|
|
50332
|
+
controller.content.update(note.id, { size: half * 2 });
|
|
50333
|
+
};
|
|
50334
|
+
return /* @__PURE__ */ jsx("span", {
|
|
50335
|
+
"aria-label": `Resize note (${corner})`,
|
|
50336
|
+
"aria-valuemax": MAX_SIZE,
|
|
50337
|
+
"aria-valuemin": MIN_SIZE,
|
|
50338
|
+
"aria-valuenow": note.size,
|
|
50339
|
+
role: "slider",
|
|
50340
|
+
tabIndex: 0,
|
|
50341
|
+
className: "scrawl-board__focus-handle",
|
|
50342
|
+
"data-orientation": corner === "nw" || corner === "se" ? "nwse" : "nesw",
|
|
50343
|
+
style: {
|
|
50344
|
+
left: x,
|
|
50345
|
+
top: y
|
|
50346
|
+
},
|
|
50347
|
+
onKeyDown: (event) => {
|
|
50348
|
+
if (event.key === "ArrowUp" || event.key === "ArrowRight") {
|
|
50349
|
+
event.preventDefault();
|
|
50350
|
+
nudge(.5);
|
|
50351
|
+
} else if (event.key === "ArrowDown" || event.key === "ArrowLeft") {
|
|
50352
|
+
event.preventDefault();
|
|
50353
|
+
nudge(-.5);
|
|
50354
|
+
}
|
|
50355
|
+
},
|
|
50356
|
+
onPointerDown: (event) => {
|
|
50357
|
+
event.stopPropagation();
|
|
50358
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
50359
|
+
setDragging(true);
|
|
50360
|
+
},
|
|
50361
|
+
onPointerMove: (event) => {
|
|
50362
|
+
if (!dragging) return;
|
|
50363
|
+
const board = controller.view.screenToBoard({
|
|
50364
|
+
x: event.clientX,
|
|
50365
|
+
y: event.clientY
|
|
50366
|
+
});
|
|
50367
|
+
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))));
|
|
50368
|
+
controller.content.update(note.id, { size: half * 2 });
|
|
50369
|
+
},
|
|
50370
|
+
onPointerUp: (event) => {
|
|
50371
|
+
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
50372
|
+
setDragging(false);
|
|
50373
|
+
}
|
|
50374
|
+
});
|
|
50375
|
+
}
|
|
50376
|
+
function TextToolbar({ controller, snapshot, text, locked, anchor }) {
|
|
50377
|
+
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
50378
|
+
const locked_ = !mutable || locked;
|
|
50379
|
+
return /* @__PURE__ */ jsxs(ToolbarShell, {
|
|
50380
|
+
anchor,
|
|
50381
|
+
children: [
|
|
50382
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50383
|
+
className: "scrawl-board__style-group",
|
|
50384
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Size" }), TEXT_SIZES.map(([label, fontSize]) => /* @__PURE__ */ jsx("button", {
|
|
50385
|
+
"aria-label": `${label} text`,
|
|
50386
|
+
"aria-pressed": Math.abs(text.fontSize - fontSize) < .001,
|
|
50387
|
+
disabled: locked_,
|
|
50388
|
+
onClick: () => controller.content.update(text.id, { fontSize }),
|
|
50389
|
+
type: "button",
|
|
50390
|
+
children: label[0]
|
|
50391
|
+
}, label))]
|
|
50392
|
+
}),
|
|
50393
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50394
|
+
className: "scrawl-board__style-group",
|
|
50395
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Colour" }), Object.entries(INK_COLORS).map(([name, value]) => /* @__PURE__ */ jsx("button", {
|
|
50396
|
+
"aria-label": `${name} text`,
|
|
50397
|
+
"aria-pressed": text.color.toLowerCase() === value.toLowerCase(),
|
|
50398
|
+
disabled: locked_,
|
|
50399
|
+
onClick: () => controller.content.update(text.id, { color: value }),
|
|
50400
|
+
type: "button",
|
|
50401
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
50402
|
+
"aria-hidden": "true",
|
|
50403
|
+
style: { backgroundColor: value }
|
|
50404
|
+
})
|
|
50405
|
+
}, value))]
|
|
50406
|
+
}),
|
|
50407
|
+
/* @__PURE__ */ jsx(LockDuplicateDelete, {
|
|
50408
|
+
controller,
|
|
50409
|
+
mutable,
|
|
50410
|
+
locked,
|
|
50411
|
+
ids: [text.id]
|
|
50412
|
+
})
|
|
50413
|
+
]
|
|
50414
|
+
});
|
|
50415
|
+
}
|
|
50416
|
+
function ShapeToolbar({ controller, snapshot, stroke, locked, anchor }) {
|
|
50417
|
+
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
50418
|
+
const locked_ = !mutable || locked;
|
|
50419
|
+
const ids = (stroke.clusterId ? controller.query.all().filter((o) => o.type === "stroke" && o.clusterId === stroke.clusterId) : [stroke]).map((m) => m.id);
|
|
50420
|
+
const applyToAll = (patch) => {
|
|
50421
|
+
for (const id of ids) controller.content.update(id, patch);
|
|
50422
|
+
};
|
|
50423
|
+
return /* @__PURE__ */ jsxs(ToolbarShell, {
|
|
50424
|
+
anchor,
|
|
50425
|
+
children: [
|
|
50426
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50427
|
+
className: "scrawl-board__style-group",
|
|
50428
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Width" }), SHAPE_WIDTHS.map(([label, width]) => /* @__PURE__ */ jsx("button", {
|
|
50429
|
+
"aria-label": `${label} stroke`,
|
|
50430
|
+
"aria-pressed": Math.abs(stroke.baseWidth - width) < .001,
|
|
50431
|
+
disabled: locked_,
|
|
50432
|
+
onClick: () => applyToAll({ baseWidth: width }),
|
|
50433
|
+
type: "button",
|
|
50434
|
+
children: label
|
|
50435
|
+
}, label))]
|
|
50436
|
+
}),
|
|
50437
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50438
|
+
className: "scrawl-board__style-group",
|
|
50439
|
+
children: [/* @__PURE__ */ jsx("span", { children: "Colour" }), Object.entries(INK_COLORS).map(([name, value]) => /* @__PURE__ */ jsx("button", {
|
|
50440
|
+
"aria-label": `${name} shape`,
|
|
50441
|
+
"aria-pressed": stroke.color.toLowerCase() === value.toLowerCase(),
|
|
50442
|
+
disabled: locked_,
|
|
50443
|
+
onClick: () => applyToAll({ color: value }),
|
|
50444
|
+
type: "button",
|
|
50445
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
50446
|
+
"aria-hidden": "true",
|
|
50447
|
+
style: { backgroundColor: value }
|
|
50448
|
+
})
|
|
50449
|
+
}, value))]
|
|
50450
|
+
}),
|
|
50451
|
+
/* @__PURE__ */ jsx(LockDuplicateDelete, {
|
|
50452
|
+
controller,
|
|
50453
|
+
mutable,
|
|
50454
|
+
locked,
|
|
50455
|
+
ids
|
|
50456
|
+
})
|
|
50457
|
+
]
|
|
50458
|
+
});
|
|
50459
|
+
}
|
|
50460
|
+
function LockDuplicateDelete({ controller, mutable, locked, ids }) {
|
|
50461
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
50462
|
+
className: "scrawl-board__style-group",
|
|
50463
|
+
children: [
|
|
50464
|
+
/* @__PURE__ */ jsx("button", {
|
|
50465
|
+
"aria-label": locked ? "Unlock" : "Lock",
|
|
50466
|
+
disabled: !mutable,
|
|
50467
|
+
onClick: () => {
|
|
50468
|
+
for (const id of ids) controller.content.update(id, locked ? { locked: false } : { locked: true });
|
|
50469
|
+
},
|
|
50470
|
+
type: "button",
|
|
50471
|
+
children: locked ? "Unlock" : "Lock"
|
|
50472
|
+
}),
|
|
50473
|
+
/* @__PURE__ */ jsx("button", {
|
|
50474
|
+
"aria-label": "Duplicate",
|
|
50475
|
+
disabled: !mutable || locked,
|
|
50476
|
+
onClick: () => {
|
|
50477
|
+
const newIds = controller.content.duplicate(ids);
|
|
50478
|
+
if (newIds.length) controller.content.select(newIds);
|
|
50479
|
+
},
|
|
50480
|
+
type: "button",
|
|
50481
|
+
children: "Duplicate"
|
|
50482
|
+
}),
|
|
50483
|
+
/* @__PURE__ */ jsx("button", {
|
|
50484
|
+
"aria-label": "Delete",
|
|
50485
|
+
disabled: !mutable || locked,
|
|
50486
|
+
onClick: () => controller.content.remove(ids),
|
|
50487
|
+
type: "button",
|
|
50488
|
+
children: "Delete"
|
|
50489
|
+
})
|
|
50490
|
+
]
|
|
50491
|
+
});
|
|
50492
|
+
}
|
|
50493
|
+
//#endregion
|
|
49869
50494
|
//#region src/ui/toolbar/style-shelf.tsx
|
|
49870
50495
|
var ERASER_RADII = [
|
|
49871
50496
|
["Fine", 1],
|
|
@@ -49904,7 +50529,7 @@ var SHELF_TOOLS = /* @__PURE__ */ new Set([
|
|
|
49904
50529
|
"timer"
|
|
49905
50530
|
]);
|
|
49906
50531
|
/** Contextual per-tool style controls — visible while a styleable tool is active. */
|
|
49907
|
-
function StyleShelf({ controller, snapshot }) {
|
|
50532
|
+
function StyleShelf({ controller, snapshot, anchor }) {
|
|
49908
50533
|
if (!SHELF_TOOLS.has(snapshot.tool)) return null;
|
|
49909
50534
|
const mutable = snapshot.status === "ready" && !snapshot.readOnly;
|
|
49910
50535
|
const { style } = snapshot;
|
|
@@ -49912,7 +50537,12 @@ function StyleShelf({ controller, snapshot }) {
|
|
|
49912
50537
|
return /* @__PURE__ */ jsxs("div", {
|
|
49913
50538
|
"aria-label": "Board style",
|
|
49914
50539
|
className: "scrawl-board__style-shelf",
|
|
50540
|
+
"data-anchored": !!anchor,
|
|
49915
50541
|
role: "toolbar",
|
|
50542
|
+
style: anchor ? {
|
|
50543
|
+
left: anchor.x,
|
|
50544
|
+
top: anchor.y
|
|
50545
|
+
} : void 0,
|
|
49916
50546
|
children: [
|
|
49917
50547
|
snapshot.tool === "highlighter" && /* @__PURE__ */ jsxs("div", {
|
|
49918
50548
|
className: "scrawl-board__style-group",
|
|
@@ -50142,6 +50772,7 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50142
50772
|
const [panel, setPanel] = useState(null);
|
|
50143
50773
|
const [query, setQuery] = useState("");
|
|
50144
50774
|
const [announcement, setAnnouncement] = useState("Board controls ready");
|
|
50775
|
+
const [styleShelfAnchor, setStyleShelfAnchor] = useState(void 0);
|
|
50145
50776
|
const importRef = useRef(null);
|
|
50146
50777
|
const openerRef = useRef(null);
|
|
50147
50778
|
const searchRef = useRef(null);
|
|
@@ -50152,6 +50783,25 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50152
50783
|
useEffect(() => {
|
|
50153
50784
|
if (panel === "search") searchRef.current?.focus();
|
|
50154
50785
|
}, [panel]);
|
|
50786
|
+
useEffect(() => {
|
|
50787
|
+
const recompute = () => {
|
|
50788
|
+
const root = chromeRef.current;
|
|
50789
|
+
const button = root?.querySelector(".scrawl-board__tools button[aria-pressed=\"true\"]");
|
|
50790
|
+
if (!root || !button) {
|
|
50791
|
+
setStyleShelfAnchor(void 0);
|
|
50792
|
+
return;
|
|
50793
|
+
}
|
|
50794
|
+
const buttonRect = button.getBoundingClientRect();
|
|
50795
|
+
const rootRect = root.getBoundingClientRect();
|
|
50796
|
+
setStyleShelfAnchor({
|
|
50797
|
+
x: buttonRect.left + buttonRect.width / 2 - rootRect.left,
|
|
50798
|
+
y: buttonRect.top - rootRect.top
|
|
50799
|
+
});
|
|
50800
|
+
};
|
|
50801
|
+
recompute();
|
|
50802
|
+
window.addEventListener("resize", recompute);
|
|
50803
|
+
return () => window.removeEventListener("resize", recompute);
|
|
50804
|
+
}, [snapshot.tool]);
|
|
50155
50805
|
const open = (next, opener) => {
|
|
50156
50806
|
openerRef.current = opener;
|
|
50157
50807
|
setPanel(next);
|
|
@@ -50380,9 +51030,14 @@ function DefaultBoardChrome({ controller, snapshot, renderPortal, className, sty
|
|
|
50380
51030
|
]
|
|
50381
51031
|
}),
|
|
50382
51032
|
renderSlot(slots?.stylePanel, enabled("styleShelf") && /* @__PURE__ */ jsx(StyleShelf, {
|
|
51033
|
+
anchor: styleShelfAnchor,
|
|
50383
51034
|
controller,
|
|
50384
51035
|
snapshot
|
|
50385
51036
|
})),
|
|
51037
|
+
enabled("focusedItemToolbar") && /* @__PURE__ */ jsx(FocusedItemToolbar, {
|
|
51038
|
+
controller,
|
|
51039
|
+
snapshot
|
|
51040
|
+
}),
|
|
50386
51041
|
ContextMenu && /* @__PURE__ */ jsx(ContextMenu, {
|
|
50387
51042
|
controller,
|
|
50388
51043
|
snapshot
|
|
@@ -51025,4 +51680,4 @@ function ScrawlBoard({ documentId, initialDocument, onReady, className, style })
|
|
|
51025
51680
|
});
|
|
51026
51681
|
}
|
|
51027
51682
|
//#endregion
|
|
51028
|
-
export { AssetResolutionError, DefaultBoardChrome, InlineEditors, MultiplayerCursors, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, StyleShelf, assetRef, clampAssetCacheBytes, cloneCustomObject, isAssetRef, resolveScrawlTheme, scrawlThemePresets, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
|
|
51683
|
+
export { AssetResolutionError, DefaultBoardChrome, FocusedItemToolbar, InlineEditors, MultiplayerCursors, SUPPORTED_ASSET_MEDIA_TYPES, Scrawl, ScrawlBoard, ScrawlCanvas, ScrawlDefaultUI, ScrawlPortal, ScrawlProvider, StyleShelf, assetRef, clampAssetCacheBytes, cloneCustomObject, isAssetRef, resolveScrawlTheme, scrawlThemePresets, useScrawlController, useScrawlSnapshot, useScrawlTheme, validateScrawlTheme };
|