web based infinite canvas
1import type { Action } from "../actions";
2import { computeNormalizedAnchor, hitTestPoint } from "../geom";
3import { Vec2 } from "../math";
4import { BindingRecord, createId, ShapeRecord } from "../model";
5import type { EditorState, ToolId } from "../reactivity";
6import { getCurrentPage } from "../reactivity";
7import type { Tool } from "../tools/base";
8
9/**
10 * Internal state for shape creation tools
11 */
12type ShapeCreationToolState = {
13 /** Whether we're currently creating a shape */
14 isCreating: boolean;
15 /** World coordinates where creation started */
16 startWorld: Vec2 | null;
17 /** ID of the shape being created */
18 creatingShapeId: string | null;
19};
20
21/**
22 * Minimum size threshold for shapes (in world units)
23 * Shapes smaller than this on either dimension will be deleted
24 */
25const MIN_SHAPE_SIZE = 5;
26
27/**
28 * Rect tool - creates rectangle shapes by dragging
29 *
30 * Features:
31 * - Drag to create a rectangle from start point to current point
32 * - Click-cancel: shapes too small are deleted on pointer up
33 */
34export class RectTool implements Tool {
35 readonly id: ToolId = "rect";
36 private toolState: ShapeCreationToolState;
37
38 constructor() {
39 this.toolState = { isCreating: false, startWorld: null, creatingShapeId: null };
40 }
41
42 onEnter(state: EditorState): EditorState {
43 this.resetToolState();
44 return state;
45 }
46
47 onExit(state: EditorState): EditorState {
48 let newState = state;
49 if (this.toolState.creatingShapeId) {
50 newState = this.cancelShapeCreation(state);
51 }
52 this.resetToolState();
53 return newState;
54 }
55
56 onAction(state: EditorState, action: Action): EditorState {
57 switch (action.type) {
58 case "pointer-down": {
59 return this.handlePointerDown(state, action);
60 }
61 case "pointer-move": {
62 return this.handlePointerMove(state, action);
63 }
64 case "pointer-up": {
65 return this.handlePointerUp(state, action);
66 }
67 case "key-down": {
68 return this.handleKeyDown(state, action);
69 }
70 default: {
71 return state;
72 }
73 }
74 }
75
76 private handlePointerDown(state: EditorState, action: Action): EditorState {
77 if (action.type !== "pointer-down") return state;
78
79 const currentPage = getCurrentPage(state);
80 if (!currentPage) return state;
81
82 const shapeId = createId("shape");
83
84 const shape = ShapeRecord.createRect(currentPage.id, action.world.x, action.world.y, {
85 w: 0,
86 h: 0,
87 fill: "#4a90e2",
88 stroke: "#2e5c8a",
89 radius: 4,
90 }, shapeId);
91
92 this.toolState.isCreating = true;
93 this.toolState.startWorld = action.world;
94 this.toolState.creatingShapeId = shapeId;
95
96 const newPage = { ...currentPage, shapeIds: [...currentPage.shapeIds, shapeId] };
97
98 return {
99 ...state,
100 doc: {
101 ...state.doc,
102 shapes: { ...state.doc.shapes, [shapeId]: shape },
103 pages: { ...state.doc.pages, [currentPage.id]: newPage },
104 },
105 ui: { ...state.ui, selectionIds: [shapeId] },
106 };
107 }
108
109 private handlePointerMove(state: EditorState, action: Action): EditorState {
110 if (action.type !== "pointer-move" || !this.toolState.isCreating || !this.toolState.startWorld) return state;
111 if (!this.toolState.creatingShapeId) return state;
112
113 const shape = state.doc.shapes[this.toolState.creatingShapeId];
114 if (!shape || shape.type !== "rect") return state;
115
116 const delta = Vec2.sub(action.world, this.toolState.startWorld);
117 const w = Math.abs(delta.x);
118 const h = Math.abs(delta.y);
119
120 const x = delta.x < 0 ? this.toolState.startWorld.x - w : this.toolState.startWorld.x;
121 const y = delta.y < 0 ? this.toolState.startWorld.y - h : this.toolState.startWorld.y;
122
123 const updatedShape = { ...shape, x, y, props: { ...shape.props, w, h } };
124
125 return {
126 ...state,
127 doc: { ...state.doc, shapes: { ...state.doc.shapes, [this.toolState.creatingShapeId]: updatedShape } },
128 };
129 }
130
131 private handlePointerUp(state: EditorState, action: Action): EditorState {
132 if (action.type !== "pointer-up" || !this.toolState.creatingShapeId) return state;
133
134 const shape = state.doc.shapes[this.toolState.creatingShapeId];
135 if (!shape || shape.type !== "rect") return state;
136
137 let newState = state;
138
139 if (shape.props.w < MIN_SHAPE_SIZE || shape.props.h < MIN_SHAPE_SIZE) {
140 newState = this.cancelShapeCreation(state);
141 }
142
143 this.resetToolState();
144 return newState;
145 }
146
147 private handleKeyDown(state: EditorState, action: Action): EditorState {
148 if (action.type !== "key-down") return state;
149
150 if (action.key === "Escape" && this.toolState.creatingShapeId) {
151 const newState = this.cancelShapeCreation(state);
152 this.resetToolState();
153 return newState;
154 }
155
156 return state;
157 }
158
159 private cancelShapeCreation(state: EditorState): EditorState {
160 if (!this.toolState.creatingShapeId) return state;
161
162 const shape = state.doc.shapes[this.toolState.creatingShapeId];
163 if (!shape) return state;
164
165 const newShapes = { ...state.doc.shapes };
166 delete newShapes[this.toolState.creatingShapeId];
167
168 const currentPage = getCurrentPage(state);
169 if (!currentPage) return state;
170
171 const newPage = {
172 ...currentPage,
173 shapeIds: currentPage.shapeIds.filter((id) => id !== this.toolState.creatingShapeId),
174 };
175
176 return {
177 ...state,
178 doc: { ...state.doc, shapes: newShapes, pages: { ...state.doc.pages, [currentPage.id]: newPage } },
179 ui: { ...state.ui, selectionIds: [] },
180 };
181 }
182
183 private resetToolState(): void {
184 this.toolState = { isCreating: false, startWorld: null, creatingShapeId: null };
185 }
186}
187
188/**
189 * Ellipse tool - creates ellipse shapes by dragging
190 *
191 * Features:
192 * - Drag to create an ellipse from start point to current point
193 * - Click-cancel: shapes too small are deleted on pointer up
194 */
195export class EllipseTool implements Tool {
196 readonly id: ToolId = "ellipse";
197 private toolState: ShapeCreationToolState;
198
199 constructor() {
200 this.toolState = { isCreating: false, startWorld: null, creatingShapeId: null };
201 }
202
203 onEnter(state: EditorState): EditorState {
204 this.resetToolState();
205 return state;
206 }
207
208 onExit(state: EditorState): EditorState {
209 let newState = state;
210 if (this.toolState.creatingShapeId) {
211 newState = this.cancelShapeCreation(state);
212 }
213 this.resetToolState();
214 return newState;
215 }
216
217 onAction(state: EditorState, action: Action): EditorState {
218 switch (action.type) {
219 case "pointer-down": {
220 return this.handlePointerDown(state, action);
221 }
222 case "pointer-move": {
223 return this.handlePointerMove(state, action);
224 }
225 case "pointer-up": {
226 return this.handlePointerUp(state, action);
227 }
228 case "key-down": {
229 return this.handleKeyDown(state, action);
230 }
231 default: {
232 return state;
233 }
234 }
235 }
236
237 private handlePointerDown(state: EditorState, action: Action): EditorState {
238 if (action.type !== "pointer-down") return state;
239
240 const currentPage = getCurrentPage(state);
241 if (!currentPage) return state;
242
243 const shapeId = createId("shape");
244
245 const shape = ShapeRecord.createEllipse(currentPage.id, action.world.x, action.world.y, {
246 w: 0,
247 h: 0,
248 fill: "#51cf66",
249 stroke: "#2f9e44",
250 }, shapeId);
251
252 this.toolState.isCreating = true;
253 this.toolState.startWorld = action.world;
254 this.toolState.creatingShapeId = shapeId;
255
256 const newPage = { ...currentPage, shapeIds: [...currentPage.shapeIds, shapeId] };
257
258 return {
259 ...state,
260 doc: {
261 ...state.doc,
262 shapes: { ...state.doc.shapes, [shapeId]: shape },
263 pages: { ...state.doc.pages, [currentPage.id]: newPage },
264 },
265 ui: { ...state.ui, selectionIds: [shapeId] },
266 };
267 }
268
269 private handlePointerMove(state: EditorState, action: Action): EditorState {
270 if (action.type !== "pointer-move" || !this.toolState.isCreating || !this.toolState.startWorld) return state;
271 if (!this.toolState.creatingShapeId) return state;
272
273 const shape = state.doc.shapes[this.toolState.creatingShapeId];
274 if (!shape || shape.type !== "ellipse") return state;
275
276 const delta = Vec2.sub(action.world, this.toolState.startWorld);
277 const w = Math.abs(delta.x);
278 const h = Math.abs(delta.y);
279
280 const x = delta.x < 0 ? this.toolState.startWorld.x - w : this.toolState.startWorld.x;
281 const y = delta.y < 0 ? this.toolState.startWorld.y - h : this.toolState.startWorld.y;
282
283 const updatedShape = { ...shape, x, y, props: { ...shape.props, w, h } };
284
285 return {
286 ...state,
287 doc: { ...state.doc, shapes: { ...state.doc.shapes, [this.toolState.creatingShapeId]: updatedShape } },
288 };
289 }
290
291 private handlePointerUp(state: EditorState, action: Action): EditorState {
292 if (action.type !== "pointer-up" || !this.toolState.creatingShapeId) return state;
293
294 const shape = state.doc.shapes[this.toolState.creatingShapeId];
295 if (!shape || shape.type !== "ellipse") return state;
296
297 let newState = state;
298
299 if (shape.props.w < MIN_SHAPE_SIZE || shape.props.h < MIN_SHAPE_SIZE) {
300 newState = this.cancelShapeCreation(state);
301 }
302
303 this.resetToolState();
304 return newState;
305 }
306
307 private handleKeyDown(state: EditorState, action: Action): EditorState {
308 if (action.type !== "key-down") return state;
309
310 if (action.key === "Escape" && this.toolState.creatingShapeId) {
311 const newState = this.cancelShapeCreation(state);
312 this.resetToolState();
313 return newState;
314 }
315
316 return state;
317 }
318
319 private cancelShapeCreation(state: EditorState): EditorState {
320 if (!this.toolState.creatingShapeId) return state;
321
322 const shape = state.doc.shapes[this.toolState.creatingShapeId];
323 if (!shape) return state;
324
325 const newShapes = { ...state.doc.shapes };
326 delete newShapes[this.toolState.creatingShapeId];
327
328 const currentPage = getCurrentPage(state);
329 if (!currentPage) return state;
330
331 const newPage = {
332 ...currentPage,
333 shapeIds: currentPage.shapeIds.filter((id) => id !== this.toolState.creatingShapeId),
334 };
335
336 return {
337 ...state,
338 doc: { ...state.doc, shapes: newShapes, pages: { ...state.doc.pages, [currentPage.id]: newPage } },
339 ui: { ...state.ui, selectionIds: [] },
340 };
341 }
342
343 private resetToolState(): void {
344 this.toolState = { isCreating: false, startWorld: null, creatingShapeId: null };
345 }
346}
347
348/**
349 * Line tool - creates line shapes by dragging
350 *
351 * Features:
352 * - Drag to create a line from start point (a) to current point (b)
353 * - Click-cancel: very short lines are deleted on pointer up
354 */
355export class LineTool implements Tool {
356 readonly id: ToolId = "line";
357 private toolState: ShapeCreationToolState;
358
359 constructor() {
360 this.toolState = { isCreating: false, startWorld: null, creatingShapeId: null };
361 }
362
363 onEnter(state: EditorState): EditorState {
364 this.resetToolState();
365 return state;
366 }
367
368 onExit(state: EditorState): EditorState {
369 let newState = state;
370 if (this.toolState.creatingShapeId) {
371 newState = this.cancelShapeCreation(state);
372 }
373 this.resetToolState();
374 return newState;
375 }
376
377 onAction(state: EditorState, action: Action): EditorState {
378 switch (action.type) {
379 case "pointer-down": {
380 return this.handlePointerDown(state, action);
381 }
382 case "pointer-move": {
383 return this.handlePointerMove(state, action);
384 }
385 case "pointer-up": {
386 return this.handlePointerUp(state, action);
387 }
388 case "key-down": {
389 return this.handleKeyDown(state, action);
390 }
391 default: {
392 return state;
393 }
394 }
395 }
396
397 private handlePointerDown(state: EditorState, action: Action): EditorState {
398 if (action.type !== "pointer-down") return state;
399
400 const currentPage = getCurrentPage(state);
401 if (!currentPage) return state;
402
403 const shapeId = createId("shape");
404
405 const shape = ShapeRecord.createLine(currentPage.id, action.world.x, action.world.y, {
406 a: { x: 0, y: 0 },
407 b: { x: 0, y: 0 },
408 stroke: "#495057",
409 width: 2,
410 }, shapeId);
411
412 this.toolState.isCreating = true;
413 this.toolState.startWorld = action.world;
414 this.toolState.creatingShapeId = shapeId;
415
416 const newPage = { ...currentPage, shapeIds: [...currentPage.shapeIds, shapeId] };
417
418 return {
419 ...state,
420 doc: {
421 ...state.doc,
422 shapes: { ...state.doc.shapes, [shapeId]: shape },
423 pages: { ...state.doc.pages, [currentPage.id]: newPage },
424 },
425 ui: { ...state.ui, selectionIds: [shapeId] },
426 };
427 }
428
429 private handlePointerMove(state: EditorState, action: Action): EditorState {
430 if (action.type !== "pointer-move" || !this.toolState.isCreating || !this.toolState.startWorld) return state;
431 if (!this.toolState.creatingShapeId) return state;
432
433 const shape = state.doc.shapes[this.toolState.creatingShapeId];
434 if (!shape || shape.type !== "line") return state;
435
436 const b = Vec2.sub(action.world, this.toolState.startWorld);
437 const updatedShape = { ...shape, props: { ...shape.props, b } };
438
439 return {
440 ...state,
441 doc: { ...state.doc, shapes: { ...state.doc.shapes, [this.toolState.creatingShapeId]: updatedShape } },
442 };
443 }
444
445 private handlePointerUp(state: EditorState, action: Action): EditorState {
446 if (action.type !== "pointer-up" || !this.toolState.creatingShapeId) return state;
447
448 const shape = state.doc.shapes[this.toolState.creatingShapeId];
449 if (!shape || shape.type !== "line") return state;
450
451 let newState = state;
452
453 const lineLength = Vec2.len(shape.props.b);
454 if (lineLength < MIN_SHAPE_SIZE) {
455 newState = this.cancelShapeCreation(state);
456 }
457
458 this.resetToolState();
459 return newState;
460 }
461
462 private handleKeyDown(state: EditorState, action: Action): EditorState {
463 if (action.type !== "key-down") return state;
464
465 if (action.key === "Escape" && this.toolState.creatingShapeId) {
466 const newState = this.cancelShapeCreation(state);
467 this.resetToolState();
468 return newState;
469 }
470
471 return state;
472 }
473
474 private cancelShapeCreation(state: EditorState): EditorState {
475 if (!this.toolState.creatingShapeId) return state;
476
477 const shape = state.doc.shapes[this.toolState.creatingShapeId];
478 if (!shape) return state;
479
480 const newShapes = { ...state.doc.shapes };
481 delete newShapes[this.toolState.creatingShapeId];
482
483 const currentPage = getCurrentPage(state);
484 if (!currentPage) return state;
485
486 const newPage = {
487 ...currentPage,
488 shapeIds: currentPage.shapeIds.filter((id) => id !== this.toolState.creatingShapeId),
489 };
490
491 return {
492 ...state,
493 doc: { ...state.doc, shapes: newShapes, pages: { ...state.doc.pages, [currentPage.id]: newPage } },
494 ui: { ...state.ui, selectionIds: [] },
495 };
496 }
497
498 private resetToolState(): void {
499 this.toolState = { isCreating: false, startWorld: null, creatingShapeId: null };
500 }
501}
502
503/**
504 * Arrow tool - creates arrow shapes by dragging
505 *
506 * Features:
507 * - Drag to create an arrow from start point (a) to current point (b)
508 * - Click-cancel: very short arrows are deleted on pointer up
509 */
510export class ArrowTool implements Tool {
511 readonly id: ToolId = "arrow";
512 private toolState: ShapeCreationToolState;
513
514 constructor() {
515 this.toolState = { isCreating: false, startWorld: null, creatingShapeId: null };
516 }
517
518 onEnter(state: EditorState): EditorState {
519 this.resetToolState();
520 return state;
521 }
522
523 onExit(state: EditorState): EditorState {
524 let newState = state;
525 if (this.toolState.creatingShapeId) {
526 newState = this.cancelShapeCreation(state);
527 }
528 this.resetToolState();
529 return newState;
530 }
531
532 onAction(state: EditorState, action: Action): EditorState {
533 switch (action.type) {
534 case "pointer-down": {
535 return this.handlePointerDown(state, action);
536 }
537 case "pointer-move": {
538 return this.handlePointerMove(state, action);
539 }
540 case "pointer-up": {
541 return this.handlePointerUp(state, action);
542 }
543 case "key-down": {
544 return this.handleKeyDown(state, action);
545 }
546 default: {
547 return state;
548 }
549 }
550 }
551
552 private handlePointerDown(state: EditorState, action: Action): EditorState {
553 if (action.type !== "pointer-down") return state;
554
555 const currentPage = getCurrentPage(state);
556 if (!currentPage) return state;
557
558 const shapeId = createId("shape");
559
560 const shape = ShapeRecord.createArrow(currentPage.id, action.world.x, action.world.y, {
561 points: [{ x: 0, y: 0 }, { x: 0, y: 0 }],
562 start: { kind: "free" },
563 end: { kind: "free" },
564 style: { stroke: "#2563eb", width: 2, headEnd: true },
565 routing: { kind: "straight" },
566 }, shapeId);
567
568 this.toolState.isCreating = true;
569 this.toolState.startWorld = action.world;
570 this.toolState.creatingShapeId = shapeId;
571
572 const newPage = { ...currentPage, shapeIds: [...currentPage.shapeIds, shapeId] };
573
574 return {
575 ...state,
576 doc: {
577 ...state.doc,
578 shapes: { ...state.doc.shapes, [shapeId]: shape },
579 pages: { ...state.doc.pages, [currentPage.id]: newPage },
580 },
581 ui: { ...state.ui, selectionIds: [shapeId] },
582 };
583 }
584
585 private handlePointerMove(state: EditorState, action: Action): EditorState {
586 if (action.type !== "pointer-move" || !this.toolState.isCreating || !this.toolState.startWorld) return state;
587 if (!this.toolState.creatingShapeId) return state;
588
589 const shape = state.doc.shapes[this.toolState.creatingShapeId];
590 if (!shape || shape.type !== "arrow") return state;
591
592 const b = Vec2.sub(action.world, this.toolState.startWorld);
593 const updatedPoints = [{ x: 0, y: 0 }, b];
594 const updatedShape = { ...shape, props: { ...shape.props, points: updatedPoints } };
595
596 let newState = {
597 ...state,
598 doc: { ...state.doc, shapes: { ...state.doc.shapes, [this.toolState.creatingShapeId]: updatedShape } },
599 };
600
601 const stateWithoutArrow = {
602 ...newState,
603 doc: {
604 ...newState.doc,
605 shapes: Object.fromEntries(
606 Object.entries(newState.doc.shapes).filter(([id]) => id !== this.toolState.creatingShapeId),
607 ),
608 },
609 };
610
611 const hitShapeId = hitTestPoint(stateWithoutArrow, action.world);
612
613 if (hitShapeId) {
614 newState = {
615 ...newState,
616 ui: {
617 ...newState.ui,
618 bindingPreview: { arrowId: this.toolState.creatingShapeId, targetShapeId: hitShapeId, handle: "end" },
619 },
620 };
621 } else {
622 newState = { ...newState, ui: { ...newState.ui, bindingPreview: undefined } };
623 }
624
625 return newState;
626 }
627
628 private handlePointerUp(state: EditorState, action: Action): EditorState {
629 if (action.type !== "pointer-up" || !this.toolState.creatingShapeId) return state;
630
631 const shape = state.doc.shapes[this.toolState.creatingShapeId];
632 if (!shape || shape.type !== "arrow") return state;
633
634 let newState = state;
635
636 const points = shape.props.points;
637 if (!points || points.length < 2) {
638 newState = this.cancelShapeCreation(state);
639 this.resetToolState();
640 return newState;
641 }
642
643 const endPoint = points[points.length - 1];
644 const arrowLength = Vec2.len(endPoint);
645 if (arrowLength < MIN_SHAPE_SIZE) {
646 newState = this.cancelShapeCreation(state);
647 } else {
648 newState = this.createBindingsForArrow(state, this.toolState.creatingShapeId);
649 }
650
651 if (newState.ui.bindingPreview) {
652 newState = { ...newState, ui: { ...newState.ui, bindingPreview: undefined } };
653 }
654
655 this.resetToolState();
656 return newState;
657 }
658
659 /**
660 * Create bindings for arrow endpoints that hit other shapes
661 */
662 private createBindingsForArrow(state: EditorState, arrowId: string): EditorState {
663 const arrow = state.doc.shapes[arrowId];
664 if (!arrow || arrow.type !== "arrow") return state;
665
666 const points = arrow.props.points;
667 if (!points || points.length < 2) return state;
668
669 const startPoint = points[0];
670 const endPoint = points[points.length - 1];
671
672 const startWorld = { x: arrow.x + startPoint.x, y: arrow.y + startPoint.y };
673 const endWorld = { x: arrow.x + endPoint.x, y: arrow.y + endPoint.y };
674
675 const newBindings = { ...state.doc.bindings };
676 let updatedArrow = arrow;
677
678 const stateWithoutArrow = {
679 ...state,
680 doc: {
681 ...state.doc,
682 shapes: Object.fromEntries(Object.entries(state.doc.shapes).filter(([id]) => id !== arrowId)),
683 },
684 };
685
686 const startHitId = hitTestPoint(stateWithoutArrow, startWorld);
687 if (startHitId) {
688 const targetShape = state.doc.shapes[startHitId];
689 if (targetShape) {
690 const anchor = computeNormalizedAnchor(startWorld, targetShape);
691 const binding = BindingRecord.create(arrowId, startHitId, "start", {
692 kind: "edge",
693 nx: anchor.nx,
694 ny: anchor.ny,
695 });
696 newBindings[binding.id] = binding;
697 updatedArrow = {
698 ...updatedArrow,
699 props: { ...updatedArrow.props, start: { kind: "bound", bindingId: binding.id } },
700 };
701 }
702 }
703
704 const endHitId = hitTestPoint(stateWithoutArrow, endWorld);
705 if (endHitId) {
706 const targetShape = state.doc.shapes[endHitId];
707 if (targetShape) {
708 const anchor = computeNormalizedAnchor(endWorld, targetShape);
709 const binding = BindingRecord.create(arrowId, endHitId, "end", { kind: "edge", nx: anchor.nx, ny: anchor.ny });
710 newBindings[binding.id] = binding;
711 updatedArrow = {
712 ...updatedArrow,
713 props: { ...updatedArrow.props, end: { kind: "bound", bindingId: binding.id } },
714 };
715 }
716 }
717
718 return {
719 ...state,
720 doc: { ...state.doc, bindings: newBindings, shapes: { ...state.doc.shapes, [arrowId]: updatedArrow } },
721 };
722 }
723
724 private handleKeyDown(state: EditorState, action: Action): EditorState {
725 if (action.type !== "key-down") return state;
726
727 if (action.key === "Escape" && this.toolState.creatingShapeId) {
728 const newState = this.cancelShapeCreation(state);
729 this.resetToolState();
730 return newState;
731 }
732
733 return state;
734 }
735
736 private cancelShapeCreation(state: EditorState): EditorState {
737 if (!this.toolState.creatingShapeId) return state;
738
739 const shape = state.doc.shapes[this.toolState.creatingShapeId];
740 if (!shape) return state;
741
742 const newShapes = { ...state.doc.shapes };
743 delete newShapes[this.toolState.creatingShapeId];
744
745 const currentPage = getCurrentPage(state);
746 if (!currentPage) return state;
747
748 const newPage = {
749 ...currentPage,
750 shapeIds: currentPage.shapeIds.filter((id) => id !== this.toolState.creatingShapeId),
751 };
752
753 return {
754 ...state,
755 doc: { ...state.doc, shapes: newShapes, pages: { ...state.doc.pages, [currentPage.id]: newPage } },
756 ui: { ...state.ui, selectionIds: [] },
757 };
758 }
759
760 private resetToolState(): void {
761 this.toolState = { isCreating: false, startWorld: null, creatingShapeId: null };
762 }
763}