Fix deleting and modifying tags, tags on timeline

This commit is contained in:
Danielv123 committed 2025-08-10 00:28:48 +02:00
1 parent fc466ec799
commit cf2efb1380
4 files changed
+109 -33

No files matched your search

+52 -21
View File
@@ -35,7 +35,12 @@ export class ControllerPlugin extends BaseControllerPlugin {
private chartTagsPath: string = "";
private chunkSavingQueue = new Map<string, Map<string, { data: ChartData, tick: number }>>();
private chartTagQueues = new Map<string, ChartTagData[]>();
private seenTagNumbers = new Map<string, Set<number>>();
private lastSeenTagContent = new Map<string, Map<number, {
position: [number, number];
text: string;
icon?: any;
last_user?: string;
}>>();
private savingTiles: boolean = false;
private savingChartTags = new Set<string>();
private saveInterval: NodeJS.Timeout | null = null;
@@ -61,7 +66,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
this.setupTileRoutes();
// Load existing tag numbers for deduplication
await this.loadExistingTagNumbers();
await this.loadExistingTagContent();
this.saveInterval = setInterval(() => {
this.saveTiles().catch(err => this.logger.error(`Error saving tiles: ${err}`));
@@ -377,7 +382,7 @@ export class ControllerPlugin extends BaseControllerPlugin {
return Buffer.concat([existingTile, ...appendBuffers]);
}
async loadExistingTagNumbers() {
async loadExistingTagContent() {
try {
if (!await fs.pathExists(this.chartTagsPath)) {
return;
@@ -390,10 +395,10 @@ export class ControllerPlugin extends BaseControllerPlugin {
const fileKey = file.replace('_chart_tags.json', '');
const filePath = path.join(this.chartTagsPath, file);
if (!this.seenTagNumbers.has(fileKey)) {
this.seenTagNumbers.set(fileKey, new Set<number>());
if (!this.lastSeenTagContent.has(fileKey)) {
this.lastSeenTagContent.set(fileKey, new Map());
}
const seenTags = this.seenTagNumbers.get(fileKey)!;
const seenTags = this.lastSeenTagContent.get(fileKey)!;
try {
const content = await fs.readFile(filePath, 'utf-8');
@@ -402,21 +407,27 @@ export class ControllerPlugin extends BaseControllerPlugin {
for (const line of lines) {
try {
const tagData = JSON.parse(line);
if (typeof tagData.tag_number === 'number') {
seenTags.add(tagData.tag_number);
if (typeof tagData.tag_number === 'number' && !tagData.end_tick) {
// Only store the latest content for tags that haven't been deleted
seenTags.set(tagData.tag_number, {
position: tagData.position,
text: tagData.text,
icon: tagData.icon,
last_user: tagData.last_user
});
}
} catch (parseErr) {
this.logger.warn(`Failed to parse chart tag line in ${file}: ${parseErr}`);
}
}
this.logger.info(`Loaded ${seenTags.size} existing tag numbers from ${file}`);
this.logger.info(`Loaded ${seenTags.size} existing tag contents from ${file}`);
} catch (readErr) {
this.logger.warn(`Failed to read chart tag file ${file}: ${readErr}`);
}
}
} catch (err) {
this.logger.error(`Error loading existing tag numbers: ${err}`);
this.logger.error(`Error loading existing tag content: ${err}`);
}
}
@@ -449,12 +460,17 @@ export class ControllerPlugin extends BaseControllerPlugin {
await fs.appendFile(filePath, jsonLines + '\n');
// Mark tags as successfully saved
if (!this.seenTagNumbers.has(fileKey)) {
this.seenTagNumbers.set(fileKey, new Set<number>());
if (!this.lastSeenTagContent.has(fileKey)) {
this.lastSeenTagContent.set(fileKey, new Map());
}
const seenTags = this.seenTagNumbers.get(fileKey)!;
const seenTags = this.lastSeenTagContent.get(fileKey)!;
for (const tag of tagsToSave) {
seenTags.add(tag.tag_number);
seenTags.set(tag.tag_number, {
position: tag.position,
text: tag.text,
icon: tag.icon,
last_user: tag.last_user
});
}
this.logger.info(`Saved ${tagsToSave.length} chart tag entries to ${fileKey}`);
@@ -479,15 +495,30 @@ export class ControllerPlugin extends BaseControllerPlugin {
// Create file key based on instance, surface, and force
const fileKey = `${instance_id}_${tag_data.surface}_${tag_data.force}`;
// Initialize seen tags set if it doesn't exist
if (!this.seenTagNumbers.has(fileKey)) {
this.seenTagNumbers.set(fileKey, new Set<number>());
// Initialize seen tags map if it doesn't exist
if (!this.lastSeenTagContent.has(fileKey)) {
this.lastSeenTagContent.set(fileKey, new Map());
}
// Check for duplicate tag_number
const seenTags = this.seenTagNumbers.get(fileKey)!;
if (seenTags.has(tag_data.tag_number)) {
return;
// Check for duplicate content, but allow deletion events (end_tick set) even if we've seen the tag before
const seenTags = this.lastSeenTagContent.get(fileKey)!;
const lastSeenContent = seenTags.get(tag_data.tag_number);
// For deletion events (end_tick set), always allow through
if (tag_data.end_tick !== undefined) {
// Don't skip deletion events
} else if (lastSeenContent) {
// For creation/modification events, check if content has actually changed
const contentUnchanged =
lastSeenContent.position[0] === tag_data.position[0] &&
lastSeenContent.position[1] === tag_data.position[1] &&
lastSeenContent.text === tag_data.text &&
JSON.stringify(lastSeenContent.icon) === JSON.stringify(tag_data.icon) &&
lastSeenContent.last_user === tag_data.last_user;
if (contentUnchanged) {
return; // Skip if content hasn't changed
}
}
// Add instance_id to the tag data for storage
+2 -2
View File
@@ -28,8 +28,8 @@ export const ChartTagDataSchema = Type.Object({
surface: Type.String(),
position: Type.Tuple([Type.Number(), Type.Number()]),
text: Type.String(),
icon: Type.Union([SignalIDSchema, Type.Null()]),
last_user: Type.Union([Type.String(), Type.Null()]),
icon: Type.Optional(SignalIDSchema),
last_user: Type.Optional(Type.String()),
});
export type ChartTagData = Static<typeof ChartTagDataSchema>;
+4 -4
View File
@@ -102,13 +102,13 @@ local function on_chart_tag_modified(event)
return
end
-- End the old version
-- End the old version at current tick
local end_tick = game.tick
local old_tag_data = create_tag_data(tag, nil, end_tick)
send_chart_tag_data(old_tag_data)
-- Create new tag entry
local start_tick = game.tick
-- Create new tag entry starting at next tick
local start_tick = game.tick + 1
local tag_data = create_tag_data(tag, start_tick, nil)
-- Send to plugin
@@ -121,7 +121,7 @@ local function on_chart_tag_removed(event)
end
local tag = event.tag
if not tag then
if not tag or not tag.valid then
return
end
+51 -6
View File
@@ -27,8 +27,8 @@ interface MergedChartTag {
surface: string;
position: [number, number];
text: string;
icon: SignalID | null;
last_user: string | null;
icon?: SignalID;
last_user?: string;
instance_id: number;
}
@@ -107,6 +107,7 @@ export default function CanvasMinimapPage() {
const [currentTick, setCurrentTick] = useState<number | null>(null);
const [isPlaying, setIsPlaying] = useState(false);
const [playbackSpeed, setPlaybackSpeed] = useState(1);
const [chartTagTimestamps, setChartTagTimestamps] = useState(0);
// Chart tag states
const [showChartTags, setShowChartTags] = useState(true);
@@ -156,6 +157,7 @@ export default function CanvasMinimapPage() {
availableTicks: [] as number[],
showChartTags: true,
mergedChartTags: [] as MergedChartTag[],
chartTagTimestamps: 0,
});
const control = useContext(ControlContext);
@@ -227,8 +229,9 @@ export default function CanvasMinimapPage() {
availableTicks,
showChartTags,
mergedChartTags,
chartTagTimestamps,
};
}, [selectedInstance, selectedSurface, selectedForce, isTimelapseMode, currentTick, availableTicks, showChartTags, mergedChartTags]);
}, [selectedInstance, selectedSurface, selectedForce, isTimelapseMode, currentTick, availableTicks, showChartTags, mergedChartTags, chartTagTimestamps]);
// Define pixel-perfect zoom levels to eliminate seaming issues
const ZOOM_LEVELS = [0.125, 0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10];
@@ -363,6 +366,30 @@ export default function CanvasMinimapPage() {
}
}
// Also include chart tag timestamps
const currentState = currentStateRef.current;
let tagsWithTicks = 0;
if (currentState.mergedChartTags.length > 0) {
for (const tag of currentState.mergedChartTags) {
// Add start_tick if it exists (tag creation)
if (tag.start_tick !== undefined) {
allTicks.add(tag.start_tick);
tagsWithTicks++;
}
// Add end_tick if it exists (tag deletion)
if (tag.end_tick !== undefined) {
allTicks.add(tag.end_tick);
tagsWithTicks++;
}
}
console.log(`Added ${tagsWithTicks} chart tag timestamps to timeline`);
}
setChartTagTimestamps(tagsWithTicks);
const sortedTicks = Array.from(allTicks).sort((a, b) => a - b);
setAvailableTicks(sortedTicks);
@@ -380,9 +407,14 @@ export default function CanvasMinimapPage() {
setIsPlaying(false);
if (enabled) {
// Ensure chart tags are loaded before getting available ticks
if (selectedInstance && selectedSurface && selectedForce) {
await loadExistingChartTags();
}
await loadAvailableTicks();
} else {
setCurrentTick(null);
setChartTagTimestamps(0);
}
// Clear caches when switching modes
@@ -1283,8 +1315,10 @@ export default function CanvasMinimapPage() {
useEffect(() => {
if (isTimelapseMode && selectedInstance && selectedSurface && selectedForce) {
// Debounce the reload to avoid too many requests during navigation
const timeoutId = setTimeout(() => {
loadAvailableTicks();
const timeoutId = setTimeout(async () => {
// Ensure chart tags are loaded before getting available ticks
await loadExistingChartTags();
await loadAvailableTicks();
}, 500);
return () => clearTimeout(timeoutId);
@@ -1332,8 +1366,9 @@ export default function CanvasMinimapPage() {
return false;
}
// In timelapse mode, filter by current tick
// Filter by tick range (both in timelapse and live mode)
if (currentState.isTimelapseMode && currentState.currentTick !== null) {
// In timelapse mode, filter by current tick
// Tag is visible if it started before or at current tick and hasn't ended yet
if (tag.start_tick !== undefined && tag.start_tick > currentState.currentTick) {
return false;
@@ -1341,6 +1376,11 @@ export default function CanvasMinimapPage() {
if (tag.end_tick !== undefined && tag.end_tick <= currentState.currentTick) {
return false;
}
} else {
// In live mode, only hide tags that have been deleted (have an end_tick)
if (tag.end_tick !== undefined) {
return false;
}
}
return true;
@@ -1547,6 +1587,11 @@ export default function CanvasMinimapPage() {
<Text>
{currentTick ? formatTickTime(currentTick) : "No data"}
</Text>
<div style={{ marginTop: "4px", fontSize: "12px", color: "#666" }}>
<Text type="secondary">
{availableTicks.length} total timestamps
</Text>
</div>
<Slider
min={0}
max={availableTicks.length - 1}