@tableslayer/ui 0.1.3 → 0.1.4

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.
Files changed (205) hide show
  1. package/package.json +2 -13
  2. package/src/lib/components/Avatar/Avatar.svelte +82 -0
  3. package/src/lib/components/Avatar/AvatarFileInput.svelte +85 -0
  4. package/src/lib/components/Avatar/AvatarPopover.svelte +34 -0
  5. package/src/lib/components/Avatar/index.ts +4 -0
  6. package/src/lib/components/Avatar/types.ts +24 -0
  7. package/src/lib/components/BrushSizeSlider/BrushSizeSlider.svelte +174 -0
  8. package/src/lib/components/BrushSizeSlider/index.ts +1 -0
  9. package/src/lib/components/Button/Button.svelte +182 -0
  10. package/src/lib/components/Button/ConfirmActionButton.svelte +98 -0
  11. package/src/lib/components/Button/IconButton.svelte +121 -0
  12. package/src/lib/components/Button/RadioButton.svelte +93 -0
  13. package/src/lib/components/Button/index.ts +5 -0
  14. package/src/lib/components/Button/types.ts +54 -0
  15. package/src/lib/components/CardFan/CardFan.svelte +165 -0
  16. package/src/lib/components/CardFan/index.ts +2 -0
  17. package/src/lib/components/CardFan/types.ts +6 -0
  18. package/src/lib/components/CodeBlock/Code.svelte +7 -0
  19. package/src/lib/components/CodeBlock/CodeBlock.svelte +102 -0
  20. package/src/lib/components/CodeBlock/index.ts +3 -0
  21. package/src/lib/components/CodeBlock/types.ts +10 -0
  22. package/src/lib/components/ColorMode/ColorMode.svelte +8 -0
  23. package/src/lib/components/ColorMode/index.ts +2 -0
  24. package/src/lib/components/ColorMode/types.ts +12 -0
  25. package/src/lib/components/ColorPicker/ColorPicker.svelte +838 -0
  26. package/src/lib/components/ColorPicker/ColorPickerSwatch.svelte +32 -0
  27. package/src/lib/components/ColorPicker/index.ts +3 -0
  28. package/src/lib/components/ColorPicker/types.ts +51 -0
  29. package/src/lib/components/ContextMenu/ContextMenu.svelte +86 -0
  30. package/src/lib/components/ContextMenu/index.ts +2 -0
  31. package/src/lib/components/ContextMenu/types.ts +15 -0
  32. package/src/lib/components/DrawingSliders/DrawingSliders.svelte +379 -0
  33. package/src/lib/components/DrawingSliders/index.ts +1 -0
  34. package/src/lib/components/Editor/Editor.svelte +825 -0
  35. package/src/lib/components/Editor/index.ts +1 -0
  36. package/src/lib/components/FogSliders/FogSliders.svelte +33 -0
  37. package/src/lib/components/FogSliders/index.ts +1 -0
  38. package/src/lib/components/Hr/Hr.svelte +15 -0
  39. package/src/lib/components/Hr/index.ts +1 -0
  40. package/src/lib/components/Icon/Icon.svelte +6 -0
  41. package/src/lib/components/Icon/index.ts +2 -0
  42. package/src/lib/components/Icon/types.ts +20 -0
  43. package/src/lib/components/Input/DualInputSlider.svelte +126 -0
  44. package/src/lib/components/Input/FileInput.svelte +176 -0
  45. package/src/lib/components/Input/FormControl.svelte +150 -0
  46. package/src/lib/components/Input/FormError.svelte +37 -0
  47. package/src/lib/components/Input/Input.svelte +56 -0
  48. package/src/lib/components/Input/InputCheckbox.svelte +99 -0
  49. package/src/lib/components/Input/InputSlider.svelte +86 -0
  50. package/src/lib/components/Input/Label.svelte +19 -0
  51. package/src/lib/components/Input/index.ts +9 -0
  52. package/src/lib/components/Input/types.ts +39 -0
  53. package/src/lib/components/Link/Link.svelte +41 -0
  54. package/src/lib/components/Link/LinkBox.svelte +20 -0
  55. package/src/lib/components/Link/LinkOverlay.svelte +23 -0
  56. package/src/lib/components/Link/index.ts +4 -0
  57. package/src/lib/components/Link/types.ts +17 -0
  58. package/src/lib/components/Loading/Loader.svelte +60 -0
  59. package/src/lib/components/Loading/Skeleton.svelte +9 -0
  60. package/src/lib/components/Loading/index.ts +2 -0
  61. package/src/lib/components/Logo/Logo.svelte +16 -0
  62. package/src/lib/components/Logo/index.ts +1 -0
  63. package/src/lib/components/MarkerTooltip/MarkerTooltip.svelte +435 -0
  64. package/src/lib/components/MarkerTooltip/index.ts +1 -0
  65. package/src/lib/components/Menu/SelectorMenu.svelte +280 -0
  66. package/src/lib/components/Menu/index.ts +2 -0
  67. package/src/lib/components/Menu/types.ts +17 -0
  68. package/src/lib/components/MyCounterButton.svelte +11 -0
  69. package/src/lib/components/Panel/index.ts +2 -0
  70. package/src/lib/components/Panel/panel.svelte +18 -0
  71. package/src/lib/components/Panel/types.ts +8 -0
  72. package/src/lib/components/PersistButton/PersistButton.svelte +100 -0
  73. package/src/lib/components/PersistButton/index.ts +1 -0
  74. package/src/lib/components/Popover/Popover.svelte +81 -0
  75. package/src/lib/components/Popover/index.ts +2 -0
  76. package/src/lib/components/Popover/types.ts +19 -0
  77. package/src/lib/components/PropsTable/PropsTable.svelte +107 -0
  78. package/src/lib/components/RadialMenu/EffectPreview.svelte +36 -0
  79. package/src/lib/components/RadialMenu/EffectPreviewScene.svelte +194 -0
  80. package/src/lib/components/RadialMenu/RadialMenu.svelte +503 -0
  81. package/src/lib/components/RadialMenu/RadialMenuItem.svelte +176 -0
  82. package/src/lib/components/RadialMenu/index.ts +2 -0
  83. package/src/lib/components/RadialMenu/types.ts +35 -0
  84. package/src/lib/components/Select/Select.svelte +342 -0
  85. package/src/lib/components/Select/index.ts +2 -0
  86. package/src/lib/components/Select/types.ts +22 -0
  87. package/src/lib/components/Spacer/Spacer.svelte +14 -0
  88. package/src/lib/components/Spacer/index.ts +2 -0
  89. package/src/lib/components/Spacer/types.ts +5 -0
  90. package/src/lib/components/Stage/components/AnnotationLayer/AnnotationLayer.svelte +445 -0
  91. package/src/lib/components/Stage/components/AnnotationLayer/AnnotationMaterial.svelte +167 -0
  92. package/src/lib/components/Stage/components/AnnotationLayer/types.ts +196 -0
  93. package/src/lib/components/Stage/components/CursorLayer/CursorLayer.svelte +148 -0
  94. package/src/lib/components/Stage/components/CursorLayer/cursor.svg +26 -0
  95. package/src/lib/components/Stage/components/CursorLayer/index.ts +2 -0
  96. package/src/lib/components/Stage/components/CursorLayer/types.ts +23 -0
  97. package/src/lib/components/Stage/components/DrawingLayer/DrawingMaterial.svelte +364 -0
  98. package/src/lib/components/Stage/components/DrawingLayer/types.ts +65 -0
  99. package/src/lib/components/Stage/components/EdgeOverlayLayer/EdgeOverlayLayer.svelte +72 -0
  100. package/src/lib/components/Stage/components/EdgeOverlayLayer/types.ts +34 -0
  101. package/src/lib/components/Stage/components/FogLayer/FogLayer.svelte +75 -0
  102. package/src/lib/components/Stage/components/FogLayer/types.ts +51 -0
  103. package/src/lib/components/Stage/components/FogOfWarLayer/FogOfWarLayer.svelte +249 -0
  104. package/src/lib/components/Stage/components/FogOfWarLayer/FogOfWarMaterial.svelte +200 -0
  105. package/src/lib/components/Stage/components/FogOfWarLayer/types.ts +116 -0
  106. package/src/lib/components/Stage/components/GridLayer/GridLayer.svelte +20 -0
  107. package/src/lib/components/Stage/components/GridLayer/GridMaterial.svelte +69 -0
  108. package/src/lib/components/Stage/components/GridLayer/types.ts +79 -0
  109. package/src/lib/components/Stage/components/LayerInput/LayerInput.svelte +300 -0
  110. package/src/lib/components/Stage/components/MapLayer/MapLayer.svelte +196 -0
  111. package/src/lib/components/Stage/components/MapLayer/dataSources/GifDataSource.ts +265 -0
  112. package/src/lib/components/Stage/components/MapLayer/dataSources/IMapDataSource.ts +55 -0
  113. package/src/lib/components/Stage/components/MapLayer/dataSources/ImageDataSource.ts +87 -0
  114. package/src/lib/components/Stage/components/MapLayer/dataSources/VideoDataSource.ts +150 -0
  115. package/src/lib/components/Stage/components/MapLayer/dataSources/dataSourceFactory.ts +48 -0
  116. package/src/lib/components/Stage/components/MapLayer/dataSources/index.ts +16 -0
  117. package/src/lib/components/Stage/components/MapLayer/types.ts +58 -0
  118. package/src/lib/components/Stage/components/MarkerLayer/MarkerLayer.svelte +398 -0
  119. package/src/lib/components/Stage/components/MarkerLayer/MarkerToken.svelte +262 -0
  120. package/src/lib/components/Stage/components/MarkerLayer/types.ts +126 -0
  121. package/src/lib/components/Stage/components/MeasurementLayer/MeasurementLayer.svelte +364 -0
  122. package/src/lib/components/Stage/components/MeasurementLayer/MeasurementManager.svelte +473 -0
  123. package/src/lib/components/Stage/components/MeasurementLayer/measurements/BaseMeasurement.ts +427 -0
  124. package/src/lib/components/Stage/components/MeasurementLayer/measurements/BeamMeasurement.ts +105 -0
  125. package/src/lib/components/Stage/components/MeasurementLayer/measurements/CircleMeasurement.ts +98 -0
  126. package/src/lib/components/Stage/components/MeasurementLayer/measurements/ConeMeasurement.ts +163 -0
  127. package/src/lib/components/Stage/components/MeasurementLayer/measurements/LineMeasurement.ts +102 -0
  128. package/src/lib/components/Stage/components/MeasurementLayer/measurements/RectangleMeasurement.ts +120 -0
  129. package/src/lib/components/Stage/components/MeasurementLayer/measurements/index.ts +7 -0
  130. package/src/lib/components/Stage/components/MeasurementLayer/types.ts +94 -0
  131. package/src/lib/components/Stage/components/MeasurementLayer/utils/canvasDrawing.ts +357 -0
  132. package/src/lib/components/Stage/components/MeasurementLayer/utils/distanceCalculations.ts +170 -0
  133. package/src/lib/components/Stage/components/ParticleSystem/ParticleSystem.svelte +220 -0
  134. package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/ash.png +0 -0
  135. package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/leaves.png +0 -0
  136. package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/rain.png +0 -0
  137. package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/snow.png +0 -0
  138. package/src/lib/components/Stage/components/ParticleSystem/rng.js +20 -0
  139. package/src/lib/components/Stage/components/ParticleSystem/types.ts +95 -0
  140. package/src/lib/components/Stage/components/PerformanceDebugger/PerformanceDebugger.svelte +144 -0
  141. package/src/lib/components/Stage/components/PerformanceDebugger/index.ts +1 -0
  142. package/src/lib/components/Stage/components/PerformanceOverlay/PerformanceOverlay.svelte +208 -0
  143. package/src/lib/components/Stage/components/PerformanceOverlay/index.ts +1 -0
  144. package/src/lib/components/Stage/components/PointerInputManager/PointerInputManager.svelte +201 -0
  145. package/src/lib/components/Stage/components/Scene/Scene.svelte +651 -0
  146. package/src/lib/components/Stage/components/Scene/luts.ts +24 -0
  147. package/src/lib/components/Stage/components/Scene/types.ts +225 -0
  148. package/src/lib/components/Stage/components/Stage/Stage.svelte +332 -0
  149. package/src/lib/components/Stage/components/Stage/types.ts +136 -0
  150. package/src/lib/components/Stage/components/WeatherLayer/WeatherLayer.svelte +135 -0
  151. package/src/lib/components/Stage/components/WeatherLayer/presets/AshPreset.ts +71 -0
  152. package/src/lib/components/Stage/components/WeatherLayer/presets/LeavesPreset.ts +70 -0
  153. package/src/lib/components/Stage/components/WeatherLayer/presets/RainPreset.ts +68 -0
  154. package/src/lib/components/Stage/components/WeatherLayer/presets/SnowPreset.ts +70 -0
  155. package/src/lib/components/Stage/components/WeatherLayer/presets/index.ts +6 -0
  156. package/src/lib/components/Stage/components/WeatherLayer/types.ts +35 -0
  157. package/src/lib/components/Stage/helpers/clippingPlaneStore.svelte.ts +28 -0
  158. package/src/lib/components/Stage/helpers/debugState.svelte.ts +18 -0
  159. package/src/lib/components/Stage/helpers/grid.ts +548 -0
  160. package/src/lib/components/Stage/helpers/lazyBrush.ts +171 -0
  161. package/src/lib/components/Stage/helpers/performanceMetrics.svelte.ts +220 -0
  162. package/src/lib/components/Stage/helpers/utils.ts +21 -0
  163. package/src/lib/components/Stage/index.ts +49 -0
  164. package/src/lib/components/Stage/shaders/AnnotationEffects.frag +1070 -0
  165. package/src/lib/components/Stage/shaders/Annotations.frag +29 -0
  166. package/src/lib/components/Stage/shaders/Drawing.frag +83 -0
  167. package/src/lib/components/Stage/shaders/Drawing.vert +5 -0
  168. package/src/lib/components/Stage/shaders/Fog.frag +147 -0
  169. package/src/lib/components/Stage/shaders/FractalNoise.frag +96 -0
  170. package/src/lib/components/Stage/shaders/GridShader.frag +174 -0
  171. package/src/lib/components/Stage/shaders/Overlay.frag +23 -0
  172. package/src/lib/components/Stage/shaders/Overlay.vert +0 -0
  173. package/src/lib/components/Stage/shaders/Particles.frag +27 -0
  174. package/src/lib/components/Stage/shaders/Particles.vert +51 -0
  175. package/src/lib/components/Stage/shaders/ToolOutline.frag +59 -0
  176. package/src/lib/components/Stage/shaders/default.vert +8 -0
  177. package/src/lib/components/Stage/types.ts +4 -0
  178. package/src/lib/components/Table/Table.svelte +16 -0
  179. package/src/lib/components/Table/Td.svelte +17 -0
  180. package/src/lib/components/Table/Th.svelte +18 -0
  181. package/src/lib/components/Table/index.ts +4 -0
  182. package/src/lib/components/Table/types.ts +14 -0
  183. package/src/lib/components/Text/Text.svelte +23 -0
  184. package/src/lib/components/Text/index.ts +2 -0
  185. package/src/lib/components/Text/types.ts +12 -0
  186. package/src/lib/components/Title/Title.svelte +54 -0
  187. package/src/lib/components/Title/index.ts +2 -0
  188. package/src/lib/components/Title/types.ts +9 -0
  189. package/src/lib/components/Toast/Toast.svelte +155 -0
  190. package/src/lib/components/Toast/index.ts +5 -0
  191. package/src/lib/components/Toast/toastCookie.ts +24 -0
  192. package/src/lib/components/Toast/types.ts +6 -0
  193. package/src/lib/components/ToolTip/ToolTip.svelte +70 -0
  194. package/src/lib/components/ToolTip/index.ts +2 -0
  195. package/src/lib/components/ToolTip/types.ts +14 -0
  196. package/src/lib/components/index.ts +32 -0
  197. package/src/lib/components/types.ts +0 -0
  198. package/src/lib/index.ts +2 -0
  199. package/src/lib/styles/globals.css +108 -0
  200. package/src/lib/styles/normalize.css +9 -0
  201. package/src/lib/styles/reset.css +133 -0
  202. package/src/lib/styles/utilities.css +179 -0
  203. package/src/lib/styles/vars.css +1103 -0
  204. package/src/lib/types/awareness.ts +17 -0
  205. package/src/lib/utils/rle.ts +217 -0
@@ -0,0 +1,398 @@
1
+ <script lang="ts">
2
+ import * as THREE from 'three';
3
+ import { T, type Props as ThrelteProps } from '@threlte/core';
4
+ import { MarkerShape, MarkerSize, MarkerVisibility, type Marker } from './types';
5
+ import { getContext } from 'svelte';
6
+ import LayerInput from '../LayerInput/LayerInput.svelte';
7
+ import type { Callbacks } from '../Stage/types';
8
+ import { type StageProps, StageMode } from '../Stage/types';
9
+ import MarkerToken from './MarkerToken.svelte';
10
+ import { getGridCellSize, snapToGrid } from '../../helpers/grid';
11
+ import type { GridLayerProps } from '../GridLayer/types';
12
+ import type { DisplayProps } from '../Stage/types';
13
+ import { SceneLayer } from '../Scene/types';
14
+ import { MapLayerType } from '../MapLayer/types';
15
+ import { v4 as uuidv4 } from 'uuid';
16
+
17
+ interface Props extends ThrelteProps<typeof THREE.Mesh> {
18
+ props: StageProps;
19
+ isActive: boolean;
20
+ grid: GridLayerProps;
21
+ display: DisplayProps;
22
+ }
23
+
24
+ const { props, isActive, display, grid }: Props = $props();
25
+
26
+ const stage = getContext<{ mode: StageMode; hoveredMarkerId: string | null; pinnedMarkerIds: string[] }>('stage');
27
+ const { onMarkerAdded, onMarkerMoved, onMarkerSelected, onMarkerContextMenu, onMarkerHover } =
28
+ getContext<Callbacks>('callbacks');
29
+
30
+ // Quad used for raycasting / mouse input detection
31
+ let inputMesh = $state(new THREE.Mesh());
32
+
33
+ // Track the currently selected marker and dragging state
34
+ let selectedMarker: Marker | null = $state(null);
35
+ let isDragging = $state(false);
36
+ let hoveredMarker: Marker | null = $state(null);
37
+ let hoveredMarkerDelayed: Marker | null = $state(null); // Marker after hover delay
38
+ let hoverTimer: ReturnType<typeof setTimeout> | null = null;
39
+ let hideTimer: ReturnType<typeof setTimeout> | null = null;
40
+ const HOVER_DELAY_MS = 500; // Half second delay before showing tooltip
41
+ const HIDE_DELAY_MS = 300; // Delay before hiding tooltip to allow moving to it
42
+
43
+ const ghostMarker: Marker = $state({
44
+ id: uuidv4(),
45
+ title: '',
46
+ position: new THREE.Vector2(0, 0),
47
+ size: MarkerSize.Small,
48
+ shape: MarkerShape.Circle,
49
+ shapeColor: '#ffffff',
50
+ imageScale: 1.0,
51
+ label: '',
52
+ imageUrl: null,
53
+ visibility: MarkerVisibility.Always,
54
+ note: null
55
+ });
56
+
57
+ function findClosestMarker(gridCoords: THREE.Vector2) {
58
+ // Find the marker that is closest to the mouse down point. The test point
59
+ // must be within the outer radius of the marker for it to be considered.
60
+ // Uses isTokenInteractable to ensure hidden/DM-only markers can't be found in Player mode.
61
+ let closestMarker: Marker | undefined;
62
+ let minDistance = Infinity;
63
+ props.marker.markers.forEach((marker) => {
64
+ if (!isTokenInteractable(marker)) return;
65
+
66
+ const distance = gridCoords.distanceTo(marker.position);
67
+ const markerRadius = getGridCellSize(grid, display) * marker.size;
68
+ if (distance < minDistance && distance <= markerRadius / 2) {
69
+ minDistance = distance;
70
+ closestMarker = marker;
71
+ }
72
+ });
73
+
74
+ return closestMarker;
75
+ }
76
+
77
+ function onMouseDown(e: MouseEvent | TouchEvent, coords: THREE.Vector2 | null) {
78
+ if (!coords) return;
79
+
80
+ // Check if TouchEvent is defined in the browser before using it
81
+ const isTouchEvent = typeof TouchEvent !== 'undefined' && e instanceof TouchEvent;
82
+ const isMouseEvent = e instanceof MouseEvent;
83
+
84
+ // Verify the primary mouse/touch was used
85
+ if (isMouseEvent && e.button !== 0) return;
86
+ if (isTouchEvent && (e as TouchEvent).touches.length !== 1) return;
87
+
88
+ // Only allow marker interaction when activeLayer is None or Marker
89
+ // This prevents marker dragging during fog, drawing, annotation, and measurement modes
90
+ if (props.activeLayer !== MapLayerType.None && props.activeLayer !== MapLayerType.Marker) {
91
+ return;
92
+ }
93
+
94
+ const gridCoords = new THREE.Vector2(coords.x - display.resolution.x / 2, coords.y - display.resolution.y / 2);
95
+
96
+ const closestMarker = findClosestMarker(gridCoords);
97
+
98
+ // Did we click on an existing marker?
99
+ if (closestMarker !== undefined) {
100
+ selectedMarker = closestMarker;
101
+ // Allow dragging in both DM and Player mode, except for pin-shaped markers (locked)
102
+ if (closestMarker.shape !== MarkerShape.Pin) {
103
+ isDragging = true;
104
+ // Clear tooltip when drag starts
105
+ hoveredMarkerDelayed = null;
106
+ clearHoverTimer();
107
+ }
108
+ onMarkerSelected(selectedMarker);
109
+ } else {
110
+ // In player mode, clicking empty space clears selection
111
+ if (stage.mode === StageMode.Player) {
112
+ selectedMarker = null;
113
+ return;
114
+ }
115
+
116
+ // In DM mode, clicking empty space also clears selection (unless we're creating a new marker)
117
+ if (props.activeLayer === MapLayerType.None) {
118
+ selectedMarker = null;
119
+ onMarkerSelected(null); // Notify that selection is cleared
120
+ return;
121
+ }
122
+ const newMarker: Marker = {
123
+ id: uuidv4(),
124
+ title: 'New Marker',
125
+ position: props.marker.snapToGrid ? snapToGrid(gridCoords, grid, display) : gridCoords,
126
+ size: MarkerSize.Small,
127
+ shape: MarkerShape.Circle,
128
+ shapeColor: '#ffffff',
129
+ imageScale: 1.0,
130
+ label: 'A1',
131
+ imageUrl: null,
132
+ visibility: MarkerVisibility.DM,
133
+ note: null
134
+ };
135
+ selectedMarker = newMarker;
136
+ onMarkerAdded(newMarker);
137
+ }
138
+ }
139
+
140
+ function onMouseMove(e: Event, coords: THREE.Vector2 | null) {
141
+ if (!coords) {
142
+ hoveredMarker = null;
143
+ clearHoverTimer();
144
+ return;
145
+ }
146
+
147
+ let position = new THREE.Vector2(coords.x - display.resolution.x / 2, coords.y - display.resolution.y / 2);
148
+ const snapPosition = props.marker.snapToGrid ? snapToGrid(position, grid, display) : position;
149
+
150
+ ghostMarker.position = snapPosition;
151
+
152
+ // Only check for hover when we're not dragging and when activeLayer is None or Marker
153
+ // This prevents hover during fog/drawing/annotation/measurement modes
154
+ if (!isDragging && (props.activeLayer === MapLayerType.None || props.activeLayer === MapLayerType.Marker)) {
155
+ // Check if there are any interactable markers (not just visible ones)
156
+ const hasInteractableMarkers = props.marker.markers.some(isTokenInteractable);
157
+ if (hasInteractableMarkers) {
158
+ const newHoveredMarker = findClosestMarker(position) ?? null;
159
+
160
+ // If we're hovering over a different marker than before
161
+ if (newHoveredMarker?.id !== hoveredMarker?.id) {
162
+ hoveredMarker = newHoveredMarker;
163
+ clearHoverTimer();
164
+
165
+ // Start timer for new hover if we have a marker
166
+ if (newHoveredMarker) {
167
+ cancelHideTimer(); // Cancel any pending hide
168
+ hoverTimer = setTimeout(() => {
169
+ hoveredMarkerDelayed = newHoveredMarker;
170
+ }, HOVER_DELAY_MS);
171
+ }
172
+ }
173
+ } else {
174
+ hoveredMarker = null;
175
+ clearHoverTimer();
176
+ }
177
+ } else {
178
+ hoveredMarker = null;
179
+ clearHoverTimer();
180
+ }
181
+
182
+ if (isDragging && selectedMarker) {
183
+ onMarkerMoved(selectedMarker, snapPosition);
184
+ }
185
+ }
186
+
187
+ function clearHoverTimer() {
188
+ if (hoverTimer) {
189
+ clearTimeout(hoverTimer);
190
+ hoverTimer = null;
191
+ }
192
+ if (hideTimer) {
193
+ clearTimeout(hideTimer);
194
+ }
195
+ hideTimer = setTimeout(() => {
196
+ hoveredMarkerDelayed = null;
197
+ }, HIDE_DELAY_MS);
198
+ }
199
+
200
+ function cancelHideTimer() {
201
+ if (hideTimer) {
202
+ clearTimeout(hideTimer);
203
+ hideTimer = null;
204
+ }
205
+ }
206
+
207
+ function isTokenVisible(marker: Marker) {
208
+ return (
209
+ marker.visibility === MarkerVisibility.Always ||
210
+ (marker.visibility === MarkerVisibility.DM && stage.mode === StageMode.DM) ||
211
+ (marker.visibility === MarkerVisibility.Hover && stage.mode === StageMode.DM) ||
212
+ (marker.visibility === MarkerVisibility.Hover &&
213
+ stage.mode === StageMode.Player &&
214
+ marker.id === stage.hoveredMarkerId) ||
215
+ (marker.visibility === MarkerVisibility.Hover &&
216
+ stage.mode === StageMode.Player &&
217
+ stage.pinnedMarkerIds?.includes(marker.id)) ||
218
+ (marker.visibility === MarkerVisibility.Player && stage.mode === StageMode.Player)
219
+ );
220
+ }
221
+
222
+ /**
223
+ * Checks if a marker can be interacted with (clicked, hovered, dragged) in the current mode.
224
+ * In Player mode, only markers with Always or Player visibility can be interacted with.
225
+ * Hover-revealed markers are visible to players but NOT interactable - they're for viewing only.
226
+ * DM-only markers are never interactable in Player mode.
227
+ */
228
+ function isTokenInteractable(marker: Marker) {
229
+ if (stage.mode === StageMode.DM) {
230
+ // In DM mode, all visible markers are interactable
231
+ return isTokenVisible(marker);
232
+ }
233
+
234
+ // In Player mode, only Always and Player visibility markers are interactable
235
+ // Hover and DM visibility markers are view-only (revealed by DM) or hidden
236
+ return marker.visibility === MarkerVisibility.Always || marker.visibility === MarkerVisibility.Player;
237
+ }
238
+
239
+ function onMouseUp() {
240
+ if (isDragging && selectedMarker) {
241
+ isDragging = false;
242
+ }
243
+ }
244
+
245
+ function onMouseLeave() {
246
+ hoveredMarker = null;
247
+ clearHoverTimer();
248
+ }
249
+
250
+ function onContextMenu(e: MouseEvent | TouchEvent, coords: THREE.Vector2 | null) {
251
+ if (!coords) return;
252
+
253
+ const gridCoords = new THREE.Vector2(coords.x - display.resolution.x / 2, coords.y - display.resolution.y / 2);
254
+ const closestMarker = findClosestMarker(gridCoords);
255
+
256
+ if (closestMarker) {
257
+ onMarkerContextMenu(closestMarker, e);
258
+ }
259
+ }
260
+
261
+ // Track previous hovered marker to detect changes
262
+ let previousHoveredMarkerDelayed: Marker | null = null;
263
+
264
+ // Notify about hover changes when in DM mode (only after delay)
265
+ $effect(() => {
266
+ if (stage.mode === StageMode.DM && hoveredMarkerDelayed !== previousHoveredMarkerDelayed) {
267
+ onMarkerHover?.(hoveredMarkerDelayed);
268
+ previousHoveredMarkerDelayed = hoveredMarkerDelayed;
269
+ }
270
+ });
271
+
272
+ export function maintainHover(maintain: boolean) {
273
+ if (maintain && stage.mode === StageMode.DM) {
274
+ cancelHideTimer();
275
+ } else if (!maintain && stage.mode === StageMode.DM && !hoveredMarker) {
276
+ clearHoverTimer();
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Called when the scene changes to clear all marker interaction state.
282
+ * Resets selection, hover, dragging, and clears any pending timers.
283
+ */
284
+ export function onSceneChange() {
285
+ selectedMarker = null;
286
+ isDragging = false;
287
+ hoveredMarker = null;
288
+ hoveredMarkerDelayed = null;
289
+
290
+ if (hoverTimer) {
291
+ clearTimeout(hoverTimer);
292
+ hoverTimer = null;
293
+ }
294
+ if (hideTimer) {
295
+ clearTimeout(hideTimer);
296
+ hideTimer = null;
297
+ }
298
+ }
299
+
300
+ // Export reactive state for hover and drag
301
+ export const markerState = {
302
+ get isHovering() {
303
+ if (stage.mode === StageMode.DM) {
304
+ return hoveredMarkerDelayed !== null && hoveredMarkerDelayed !== undefined;
305
+ }
306
+ return stage.hoveredMarkerId !== null && stage.hoveredMarkerId !== undefined;
307
+ },
308
+ get isDragging() {
309
+ return isDragging;
310
+ },
311
+ get hoveredMarker() {
312
+ // In DM mode, use the delayed hover
313
+ if (stage.mode === StageMode.DM) {
314
+ return hoveredMarkerDelayed;
315
+ }
316
+ // In Player mode, find the marker that matches the DM's hoveredMarkerId or is pinned
317
+ if (stage.hoveredMarkerId) {
318
+ const hoveredMarker = props.marker.markers.find((m) => m.id === stage.hoveredMarkerId);
319
+ return hoveredMarker || null;
320
+ }
321
+ // Check for pinned markers
322
+ if (stage.pinnedMarkerIds && stage.pinnedMarkerIds.length > 0) {
323
+ const pinnedMarker = props.marker.markers.find((m) => stage.pinnedMarkerIds.includes(m.id));
324
+ return pinnedMarker || null;
325
+ }
326
+ return null;
327
+ },
328
+ get selectedMarker() {
329
+ return selectedMarker;
330
+ }
331
+ };
332
+ </script>
333
+
334
+ <LayerInput
335
+ id="marker"
336
+ isActive={isActive || stage.mode === StageMode.Player}
337
+ target={inputMesh}
338
+ layerSize={{ width: display.resolution.x, height: display.resolution.y }}
339
+ {onMouseDown}
340
+ {onMouseMove}
341
+ {onMouseUp}
342
+ {onMouseLeave}
343
+ {onContextMenu}
344
+ />
345
+
346
+ <!-- This quad is user for raycasting / mouse input detection. It is invisible -->
347
+ <T.Mesh bind:ref={inputMesh} scale={[display.resolution.x, display.resolution.y, 1]} layers={[SceneLayer.Input]}>
348
+ <T.MeshBasicMaterial visible={false} />
349
+ <T.PlaneGeometry />
350
+ </T.Mesh>
351
+
352
+ <!-- This group contains all the markers -->
353
+ <T.Group name="markerLayer" position={[-0.5, -0.5, 0]}>
354
+ {#each props.marker.markers as marker (marker.id)}
355
+ {#if isTokenVisible(marker)}
356
+ <MarkerToken
357
+ {marker}
358
+ {grid}
359
+ {display}
360
+ opacity={1.0}
361
+ textColor={props.marker.text.color}
362
+ textStroke={props.marker.text.strokeWidth}
363
+ textStrokeColor={props.marker.text.strokeColor}
364
+ textSize={props.marker.text.size}
365
+ shadowColor={props.marker.shape.shadowColor}
366
+ shadowBlur={props.marker.shape.shadowBlur}
367
+ shadowOffset={props.marker.shape.shadowOffset}
368
+ strokeColor={props.marker.shape.strokeColor}
369
+ strokeWidth={props.marker.shape.strokeWidth}
370
+ isSelected={selectedMarker?.id === marker.id}
371
+ isHovered={hoveredMarker?.id === marker.id}
372
+ sceneRotation={props.scene.rotation}
373
+ />
374
+ {/if}
375
+ {/each}
376
+
377
+ <!-- Only show the ghost marker when the marker layer is active -->
378
+ {#if isActive && stage.mode === StageMode.DM && props.activeLayer === MapLayerType.Marker}
379
+ <MarkerToken
380
+ marker={ghostMarker}
381
+ {grid}
382
+ {display}
383
+ opacity={0.3}
384
+ textColor={props.marker.text.color}
385
+ textStroke={props.marker.text.strokeWidth}
386
+ textStrokeColor={props.marker.text.strokeColor}
387
+ textSize={props.marker.text.size}
388
+ shadowColor={props.marker.shape.shadowColor}
389
+ shadowBlur={props.marker.shape.shadowBlur}
390
+ shadowOffset={props.marker.shape.shadowOffset}
391
+ strokeColor={props.marker.shape.strokeColor}
392
+ strokeWidth={props.marker.shape.strokeWidth}
393
+ isSelected={false}
394
+ isHovered={false}
395
+ sceneRotation={props.scene.rotation}
396
+ />
397
+ {/if}
398
+ </T.Group>
@@ -0,0 +1,262 @@
1
+ <script lang="ts">
2
+ import * as THREE from 'three';
3
+ import { T, useLoader } from '@threlte/core';
4
+ import { onDestroy } from 'svelte';
5
+ import { MarkerShape, type Marker } from './types';
6
+ import { SceneLayer, SceneLayerOrder } from '../Scene/types';
7
+ import { getGridCellSize } from '../../helpers/grid';
8
+ import type { GridLayerProps } from '../GridLayer/types';
9
+ import type { DisplayProps } from '../Stage/types';
10
+
11
+ interface Props {
12
+ marker: Marker;
13
+ grid: GridLayerProps;
14
+ display: DisplayProps;
15
+ opacity: number;
16
+ strokeColor: string;
17
+ strokeWidth: number;
18
+ shadowColor: string;
19
+ shadowBlur: number;
20
+ shadowOffset: {
21
+ x: number;
22
+ y: number;
23
+ };
24
+ textColor: string;
25
+ textStroke: number;
26
+ textStrokeColor: string;
27
+ textSize: number;
28
+ isSelected: boolean;
29
+ isHovered?: boolean;
30
+ sceneRotation: number;
31
+ }
32
+
33
+ const {
34
+ marker,
35
+ grid,
36
+ display,
37
+ opacity,
38
+ textColor,
39
+ textStroke,
40
+ textStrokeColor,
41
+ textSize,
42
+ strokeColor,
43
+ strokeWidth,
44
+ shadowColor,
45
+ shadowBlur,
46
+ shadowOffset,
47
+ isSelected = false,
48
+ isHovered = false,
49
+ sceneRotation
50
+ }: Props = $props();
51
+
52
+ const loader = useLoader(THREE.TextureLoader);
53
+ const baseMarkerSize = $derived(getGridCellSize(grid, display) * marker.size);
54
+ const markerSize = $derived(isHovered ? baseMarkerSize * 1.15 : baseMarkerSize);
55
+ const sizeMultiplier = 0.9;
56
+
57
+ // Counter-rotate markers to keep them upright relative to the viewport
58
+ const normalizedRotation = $derived(((sceneRotation % 360) + 360) % 360);
59
+ const needsFlip = $derived(
60
+ (normalizedRotation > 85 && normalizedRotation < 95) || (normalizedRotation > 265 && normalizedRotation < 275)
61
+ );
62
+ const counterRotation = $derived(
63
+ needsFlip ? -((sceneRotation + 180) * Math.PI) / 180 : -(sceneRotation * Math.PI) / 180
64
+ );
65
+
66
+ const canvasSize = 1024;
67
+
68
+ let markerCanvas = new OffscreenCanvas(canvasSize, canvasSize);
69
+ let ctx = markerCanvas.getContext('2d')!;
70
+ ctx.globalCompositeOperation = 'source-over';
71
+
72
+ let markerMaterial = new THREE.MeshBasicMaterial({
73
+ transparent: true,
74
+ opacity
75
+ });
76
+ let imageTexture: THREE.Texture | null = $state(null);
77
+
78
+ // Load image if URL is provided
79
+ $effect(() => {
80
+ if (marker.imageUrl) {
81
+ loader
82
+ .load(marker.imageUrl)
83
+ .then((texture) => {
84
+ imageTexture = texture;
85
+ imageTexture.needsUpdate = true;
86
+ })
87
+ .catch((err) => console.error('Error loading image:', err));
88
+ } else {
89
+ imageTexture = null;
90
+ }
91
+ });
92
+
93
+ // Create complete marker canvas with shape, stroke, and text
94
+ function createShape(centerX: number, centerY: number, size: number, clipOnly: boolean = false) {
95
+ ctx.beginPath();
96
+
97
+ switch (marker.shape) {
98
+ case MarkerShape.None:
99
+ break;
100
+ case MarkerShape.Circle:
101
+ const radius = (size * sizeMultiplier) / 2;
102
+ ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
103
+ ctx.closePath();
104
+ break;
105
+
106
+ case MarkerShape.Square:
107
+ const squareSize = size * sizeMultiplier; // Adjust size to match circle's visual weight
108
+ ctx.rect(centerX - squareSize / 2, centerY - squareSize / 2, squareSize, squareSize);
109
+ ctx.closePath();
110
+ break;
111
+
112
+ case MarkerShape.Triangle:
113
+ const height = size * sizeMultiplier;
114
+ ctx.moveTo(centerX, centerY - height / 2); // Top
115
+ ctx.lineTo(centerX - height / 2, centerY + height / 2); // Bottom left
116
+ ctx.lineTo(centerX + height / 2, centerY + height / 2); // Bottom right
117
+ ctx.closePath();
118
+ break;
119
+ }
120
+
121
+ if (!clipOnly) {
122
+ ctx.shadowColor = shadowColor;
123
+ ctx.shadowBlur = shadowBlur;
124
+ ctx.shadowOffsetX = shadowOffset.x;
125
+ ctx.shadowOffsetY = shadowOffset.y;
126
+ ctx.fill();
127
+ if (strokeWidth > 0) {
128
+ ctx.stroke();
129
+ }
130
+ }
131
+
132
+ return ctx;
133
+ }
134
+
135
+ // Create text for the marker
136
+ function createText(centerX: number, centerY: number) {
137
+ if (!marker.label) return;
138
+
139
+ // Reset shadow settings for text
140
+ ctx.shadowColor = 'transparent';
141
+ ctx.shadowBlur = 0;
142
+ ctx.shadowOffsetX = 0;
143
+ ctx.shadowOffsetY = 0;
144
+
145
+ // Set text properties
146
+ ctx.font = `700 ${textSize}px Inter`;
147
+ ctx.textAlign = 'center';
148
+ ctx.textBaseline = 'middle';
149
+
150
+ // Add text stroke if specified
151
+ if (textStroke && textStrokeColor) {
152
+ ctx.strokeStyle = textStrokeColor;
153
+ ctx.lineWidth = textStroke * (textSize / 5);
154
+ ctx.strokeText(marker.label, centerX, centerY);
155
+ }
156
+
157
+ // Fill text
158
+ ctx.fillStyle = textColor || '#ffffff';
159
+ ctx.fillText(marker.label, centerX, centerY);
160
+ }
161
+
162
+ // Create the marker canvas
163
+ function drawMarker() {
164
+ const width = markerCanvas.width;
165
+ const height = markerCanvas.height;
166
+ const centerX = width / 2;
167
+ const centerY = height / 2;
168
+
169
+ // Clear canvas with transparency
170
+ ctx.clearRect(0, 0, width, height);
171
+
172
+ if (marker.shape !== undefined) {
173
+ // Set stroke and fill styles for shape
174
+ // Use --fgPrimary color when hovered or selected, otherwise use the default stroke color
175
+ ctx.strokeStyle = isHovered || isSelected ? getCSSVariable('--fgPrimary') : (strokeColor ?? '#000000');
176
+ ctx.lineWidth = strokeWidth;
177
+ ctx.fillStyle = marker.shapeColor ?? '#ffffff';
178
+
179
+ createShape(centerX, centerY, canvasSize);
180
+
181
+ // Draw image if available
182
+ if (imageTexture && imageTexture.image) {
183
+ // Save the current canvas state
184
+ ctx.save();
185
+
186
+ // Create a smaller shape path for clipping that accounts for stroke width
187
+ const innerSize = canvasSize - strokeWidth; // Reduce by twice the stroke width
188
+ createShape(centerX, centerY, innerSize, true);
189
+
190
+ // Apply clipping to the shape
191
+ ctx.clip();
192
+
193
+ // Set proper compositing for image drawing
194
+ ctx.globalCompositeOperation = 'source-over';
195
+ ctx.globalAlpha = 1.0; // Ensure full opacity for the image itself
196
+
197
+ // Draw the image (will only appear inside the clipped shape)
198
+ ctx.drawImage(
199
+ imageTexture.image as CanvasImageSource,
200
+ centerX - (canvasSize / 2) * marker.imageScale * sizeMultiplier,
201
+ centerY - (canvasSize / 2) * marker.imageScale * sizeMultiplier,
202
+ sizeMultiplier * canvasSize * marker.imageScale,
203
+ sizeMultiplier * canvasSize * marker.imageScale
204
+ );
205
+
206
+ // Restore the canvas state (removes clipping)
207
+ ctx.restore();
208
+ }
209
+ }
210
+
211
+ // // Draw text if enabled
212
+ if (marker.label) {
213
+ createText(centerX, centerY);
214
+ }
215
+
216
+ return markerCanvas;
217
+ }
218
+
219
+ // Create and update marker texture when properties change (including hover state)
220
+ $effect(() => {
221
+ markerCanvas = drawMarker();
222
+
223
+ // Dispose old texture before creating new one
224
+ if (markerMaterial.map) {
225
+ markerMaterial.map.dispose();
226
+ }
227
+
228
+ markerMaterial.map = new THREE.CanvasTexture(markerCanvas);
229
+ markerMaterial.map.colorSpace = THREE.SRGBColorSpace;
230
+ markerMaterial.map.needsUpdate = true;
231
+ });
232
+
233
+ onDestroy(() => {
234
+ if (markerMaterial.map) {
235
+ markerMaterial.map.dispose();
236
+ }
237
+ markerMaterial.dispose();
238
+ if (imageTexture) {
239
+ imageTexture.dispose();
240
+ }
241
+ });
242
+
243
+ // Get CSS variable value for hover color
244
+ function getCSSVariable(varName: string): string {
245
+ if (typeof window !== 'undefined') {
246
+ return getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
247
+ }
248
+ return '#ffffff';
249
+ }
250
+ </script>
251
+
252
+ <T.Group
253
+ position={[marker.position.x, marker.position.y, 0]}
254
+ scale={[markerSize, markerSize, 1]}
255
+ rotation={[0, 0, counterRotation]}
256
+ >
257
+ <!-- Combined shape, stroke and text -->
258
+ <T.Mesh renderOrder={SceneLayerOrder.Marker} layers={[SceneLayer.Main]}>
259
+ <T.MeshBasicMaterial is={markerMaterial} />
260
+ <T.PlaneGeometry args={[1, 1]} />
261
+ </T.Mesh>
262
+ </T.Group>