@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.
- package/package.json +2 -13
- package/src/lib/components/Avatar/Avatar.svelte +82 -0
- package/src/lib/components/Avatar/AvatarFileInput.svelte +85 -0
- package/src/lib/components/Avatar/AvatarPopover.svelte +34 -0
- package/src/lib/components/Avatar/index.ts +4 -0
- package/src/lib/components/Avatar/types.ts +24 -0
- package/src/lib/components/BrushSizeSlider/BrushSizeSlider.svelte +174 -0
- package/src/lib/components/BrushSizeSlider/index.ts +1 -0
- package/src/lib/components/Button/Button.svelte +182 -0
- package/src/lib/components/Button/ConfirmActionButton.svelte +98 -0
- package/src/lib/components/Button/IconButton.svelte +121 -0
- package/src/lib/components/Button/RadioButton.svelte +93 -0
- package/src/lib/components/Button/index.ts +5 -0
- package/src/lib/components/Button/types.ts +54 -0
- package/src/lib/components/CardFan/CardFan.svelte +165 -0
- package/src/lib/components/CardFan/index.ts +2 -0
- package/src/lib/components/CardFan/types.ts +6 -0
- package/src/lib/components/CodeBlock/Code.svelte +7 -0
- package/src/lib/components/CodeBlock/CodeBlock.svelte +102 -0
- package/src/lib/components/CodeBlock/index.ts +3 -0
- package/src/lib/components/CodeBlock/types.ts +10 -0
- package/src/lib/components/ColorMode/ColorMode.svelte +8 -0
- package/src/lib/components/ColorMode/index.ts +2 -0
- package/src/lib/components/ColorMode/types.ts +12 -0
- package/src/lib/components/ColorPicker/ColorPicker.svelte +838 -0
- package/src/lib/components/ColorPicker/ColorPickerSwatch.svelte +32 -0
- package/src/lib/components/ColorPicker/index.ts +3 -0
- package/src/lib/components/ColorPicker/types.ts +51 -0
- package/src/lib/components/ContextMenu/ContextMenu.svelte +86 -0
- package/src/lib/components/ContextMenu/index.ts +2 -0
- package/src/lib/components/ContextMenu/types.ts +15 -0
- package/src/lib/components/DrawingSliders/DrawingSliders.svelte +379 -0
- package/src/lib/components/DrawingSliders/index.ts +1 -0
- package/src/lib/components/Editor/Editor.svelte +825 -0
- package/src/lib/components/Editor/index.ts +1 -0
- package/src/lib/components/FogSliders/FogSliders.svelte +33 -0
- package/src/lib/components/FogSliders/index.ts +1 -0
- package/src/lib/components/Hr/Hr.svelte +15 -0
- package/src/lib/components/Hr/index.ts +1 -0
- package/src/lib/components/Icon/Icon.svelte +6 -0
- package/src/lib/components/Icon/index.ts +2 -0
- package/src/lib/components/Icon/types.ts +20 -0
- package/src/lib/components/Input/DualInputSlider.svelte +126 -0
- package/src/lib/components/Input/FileInput.svelte +176 -0
- package/src/lib/components/Input/FormControl.svelte +150 -0
- package/src/lib/components/Input/FormError.svelte +37 -0
- package/src/lib/components/Input/Input.svelte +56 -0
- package/src/lib/components/Input/InputCheckbox.svelte +99 -0
- package/src/lib/components/Input/InputSlider.svelte +86 -0
- package/src/lib/components/Input/Label.svelte +19 -0
- package/src/lib/components/Input/index.ts +9 -0
- package/src/lib/components/Input/types.ts +39 -0
- package/src/lib/components/Link/Link.svelte +41 -0
- package/src/lib/components/Link/LinkBox.svelte +20 -0
- package/src/lib/components/Link/LinkOverlay.svelte +23 -0
- package/src/lib/components/Link/index.ts +4 -0
- package/src/lib/components/Link/types.ts +17 -0
- package/src/lib/components/Loading/Loader.svelte +60 -0
- package/src/lib/components/Loading/Skeleton.svelte +9 -0
- package/src/lib/components/Loading/index.ts +2 -0
- package/src/lib/components/Logo/Logo.svelte +16 -0
- package/src/lib/components/Logo/index.ts +1 -0
- package/src/lib/components/MarkerTooltip/MarkerTooltip.svelte +435 -0
- package/src/lib/components/MarkerTooltip/index.ts +1 -0
- package/src/lib/components/Menu/SelectorMenu.svelte +280 -0
- package/src/lib/components/Menu/index.ts +2 -0
- package/src/lib/components/Menu/types.ts +17 -0
- package/src/lib/components/MyCounterButton.svelte +11 -0
- package/src/lib/components/Panel/index.ts +2 -0
- package/src/lib/components/Panel/panel.svelte +18 -0
- package/src/lib/components/Panel/types.ts +8 -0
- package/src/lib/components/PersistButton/PersistButton.svelte +100 -0
- package/src/lib/components/PersistButton/index.ts +1 -0
- package/src/lib/components/Popover/Popover.svelte +81 -0
- package/src/lib/components/Popover/index.ts +2 -0
- package/src/lib/components/Popover/types.ts +19 -0
- package/src/lib/components/PropsTable/PropsTable.svelte +107 -0
- package/src/lib/components/RadialMenu/EffectPreview.svelte +36 -0
- package/src/lib/components/RadialMenu/EffectPreviewScene.svelte +194 -0
- package/src/lib/components/RadialMenu/RadialMenu.svelte +503 -0
- package/src/lib/components/RadialMenu/RadialMenuItem.svelte +176 -0
- package/src/lib/components/RadialMenu/index.ts +2 -0
- package/src/lib/components/RadialMenu/types.ts +35 -0
- package/src/lib/components/Select/Select.svelte +342 -0
- package/src/lib/components/Select/index.ts +2 -0
- package/src/lib/components/Select/types.ts +22 -0
- package/src/lib/components/Spacer/Spacer.svelte +14 -0
- package/src/lib/components/Spacer/index.ts +2 -0
- package/src/lib/components/Spacer/types.ts +5 -0
- package/src/lib/components/Stage/components/AnnotationLayer/AnnotationLayer.svelte +445 -0
- package/src/lib/components/Stage/components/AnnotationLayer/AnnotationMaterial.svelte +167 -0
- package/src/lib/components/Stage/components/AnnotationLayer/types.ts +196 -0
- package/src/lib/components/Stage/components/CursorLayer/CursorLayer.svelte +148 -0
- package/src/lib/components/Stage/components/CursorLayer/cursor.svg +26 -0
- package/src/lib/components/Stage/components/CursorLayer/index.ts +2 -0
- package/src/lib/components/Stage/components/CursorLayer/types.ts +23 -0
- package/src/lib/components/Stage/components/DrawingLayer/DrawingMaterial.svelte +364 -0
- package/src/lib/components/Stage/components/DrawingLayer/types.ts +65 -0
- package/src/lib/components/Stage/components/EdgeOverlayLayer/EdgeOverlayLayer.svelte +72 -0
- package/src/lib/components/Stage/components/EdgeOverlayLayer/types.ts +34 -0
- package/src/lib/components/Stage/components/FogLayer/FogLayer.svelte +75 -0
- package/src/lib/components/Stage/components/FogLayer/types.ts +51 -0
- package/src/lib/components/Stage/components/FogOfWarLayer/FogOfWarLayer.svelte +249 -0
- package/src/lib/components/Stage/components/FogOfWarLayer/FogOfWarMaterial.svelte +200 -0
- package/src/lib/components/Stage/components/FogOfWarLayer/types.ts +116 -0
- package/src/lib/components/Stage/components/GridLayer/GridLayer.svelte +20 -0
- package/src/lib/components/Stage/components/GridLayer/GridMaterial.svelte +69 -0
- package/src/lib/components/Stage/components/GridLayer/types.ts +79 -0
- package/src/lib/components/Stage/components/LayerInput/LayerInput.svelte +300 -0
- package/src/lib/components/Stage/components/MapLayer/MapLayer.svelte +196 -0
- package/src/lib/components/Stage/components/MapLayer/dataSources/GifDataSource.ts +265 -0
- package/src/lib/components/Stage/components/MapLayer/dataSources/IMapDataSource.ts +55 -0
- package/src/lib/components/Stage/components/MapLayer/dataSources/ImageDataSource.ts +87 -0
- package/src/lib/components/Stage/components/MapLayer/dataSources/VideoDataSource.ts +150 -0
- package/src/lib/components/Stage/components/MapLayer/dataSources/dataSourceFactory.ts +48 -0
- package/src/lib/components/Stage/components/MapLayer/dataSources/index.ts +16 -0
- package/src/lib/components/Stage/components/MapLayer/types.ts +58 -0
- package/src/lib/components/Stage/components/MarkerLayer/MarkerLayer.svelte +398 -0
- package/src/lib/components/Stage/components/MarkerLayer/MarkerToken.svelte +262 -0
- package/src/lib/components/Stage/components/MarkerLayer/types.ts +126 -0
- package/src/lib/components/Stage/components/MeasurementLayer/MeasurementLayer.svelte +364 -0
- package/src/lib/components/Stage/components/MeasurementLayer/MeasurementManager.svelte +473 -0
- package/src/lib/components/Stage/components/MeasurementLayer/measurements/BaseMeasurement.ts +427 -0
- package/src/lib/components/Stage/components/MeasurementLayer/measurements/BeamMeasurement.ts +105 -0
- package/src/lib/components/Stage/components/MeasurementLayer/measurements/CircleMeasurement.ts +98 -0
- package/src/lib/components/Stage/components/MeasurementLayer/measurements/ConeMeasurement.ts +163 -0
- package/src/lib/components/Stage/components/MeasurementLayer/measurements/LineMeasurement.ts +102 -0
- package/src/lib/components/Stage/components/MeasurementLayer/measurements/RectangleMeasurement.ts +120 -0
- package/src/lib/components/Stage/components/MeasurementLayer/measurements/index.ts +7 -0
- package/src/lib/components/Stage/components/MeasurementLayer/types.ts +94 -0
- package/src/lib/components/Stage/components/MeasurementLayer/utils/canvasDrawing.ts +357 -0
- package/src/lib/components/Stage/components/MeasurementLayer/utils/distanceCalculations.ts +170 -0
- package/src/lib/components/Stage/components/ParticleSystem/ParticleSystem.svelte +220 -0
- package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/ash.png +0 -0
- package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/leaves.png +0 -0
- package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/rain.png +0 -0
- package/src/lib/components/Stage/components/ParticleSystem/particles/atlases/snow.png +0 -0
- package/src/lib/components/Stage/components/ParticleSystem/rng.js +20 -0
- package/src/lib/components/Stage/components/ParticleSystem/types.ts +95 -0
- package/src/lib/components/Stage/components/PerformanceDebugger/PerformanceDebugger.svelte +144 -0
- package/src/lib/components/Stage/components/PerformanceDebugger/index.ts +1 -0
- package/src/lib/components/Stage/components/PerformanceOverlay/PerformanceOverlay.svelte +208 -0
- package/src/lib/components/Stage/components/PerformanceOverlay/index.ts +1 -0
- package/src/lib/components/Stage/components/PointerInputManager/PointerInputManager.svelte +201 -0
- package/src/lib/components/Stage/components/Scene/Scene.svelte +651 -0
- package/src/lib/components/Stage/components/Scene/luts.ts +24 -0
- package/src/lib/components/Stage/components/Scene/types.ts +225 -0
- package/src/lib/components/Stage/components/Stage/Stage.svelte +332 -0
- package/src/lib/components/Stage/components/Stage/types.ts +136 -0
- package/src/lib/components/Stage/components/WeatherLayer/WeatherLayer.svelte +135 -0
- package/src/lib/components/Stage/components/WeatherLayer/presets/AshPreset.ts +71 -0
- package/src/lib/components/Stage/components/WeatherLayer/presets/LeavesPreset.ts +70 -0
- package/src/lib/components/Stage/components/WeatherLayer/presets/RainPreset.ts +68 -0
- package/src/lib/components/Stage/components/WeatherLayer/presets/SnowPreset.ts +70 -0
- package/src/lib/components/Stage/components/WeatherLayer/presets/index.ts +6 -0
- package/src/lib/components/Stage/components/WeatherLayer/types.ts +35 -0
- package/src/lib/components/Stage/helpers/clippingPlaneStore.svelte.ts +28 -0
- package/src/lib/components/Stage/helpers/debugState.svelte.ts +18 -0
- package/src/lib/components/Stage/helpers/grid.ts +548 -0
- package/src/lib/components/Stage/helpers/lazyBrush.ts +171 -0
- package/src/lib/components/Stage/helpers/performanceMetrics.svelte.ts +220 -0
- package/src/lib/components/Stage/helpers/utils.ts +21 -0
- package/src/lib/components/Stage/index.ts +49 -0
- package/src/lib/components/Stage/shaders/AnnotationEffects.frag +1070 -0
- package/src/lib/components/Stage/shaders/Annotations.frag +29 -0
- package/src/lib/components/Stage/shaders/Drawing.frag +83 -0
- package/src/lib/components/Stage/shaders/Drawing.vert +5 -0
- package/src/lib/components/Stage/shaders/Fog.frag +147 -0
- package/src/lib/components/Stage/shaders/FractalNoise.frag +96 -0
- package/src/lib/components/Stage/shaders/GridShader.frag +174 -0
- package/src/lib/components/Stage/shaders/Overlay.frag +23 -0
- package/src/lib/components/Stage/shaders/Overlay.vert +0 -0
- package/src/lib/components/Stage/shaders/Particles.frag +27 -0
- package/src/lib/components/Stage/shaders/Particles.vert +51 -0
- package/src/lib/components/Stage/shaders/ToolOutline.frag +59 -0
- package/src/lib/components/Stage/shaders/default.vert +8 -0
- package/src/lib/components/Stage/types.ts +4 -0
- package/src/lib/components/Table/Table.svelte +16 -0
- package/src/lib/components/Table/Td.svelte +17 -0
- package/src/lib/components/Table/Th.svelte +18 -0
- package/src/lib/components/Table/index.ts +4 -0
- package/src/lib/components/Table/types.ts +14 -0
- package/src/lib/components/Text/Text.svelte +23 -0
- package/src/lib/components/Text/index.ts +2 -0
- package/src/lib/components/Text/types.ts +12 -0
- package/src/lib/components/Title/Title.svelte +54 -0
- package/src/lib/components/Title/index.ts +2 -0
- package/src/lib/components/Title/types.ts +9 -0
- package/src/lib/components/Toast/Toast.svelte +155 -0
- package/src/lib/components/Toast/index.ts +5 -0
- package/src/lib/components/Toast/toastCookie.ts +24 -0
- package/src/lib/components/Toast/types.ts +6 -0
- package/src/lib/components/ToolTip/ToolTip.svelte +70 -0
- package/src/lib/components/ToolTip/index.ts +2 -0
- package/src/lib/components/ToolTip/types.ts +14 -0
- package/src/lib/components/index.ts +32 -0
- package/src/lib/components/types.ts +0 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/styles/globals.css +108 -0
- package/src/lib/styles/normalize.css +9 -0
- package/src/lib/styles/reset.css +133 -0
- package/src/lib/styles/utilities.css +179 -0
- package/src/lib/styles/vars.css +1103 -0
- package/src/lib/types/awareness.ts +17 -0
- 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>
|