Skip to content

Implement map resizing, image editing tools, and import/export functionality#3

Merged
wernerbihl merged 12 commits intomainfrom
feature/2026-04-20
Apr 20, 2026
Merged

Implement map resizing, image editing tools, and import/export functionality#3
wernerbihl merged 12 commits intomainfrom
feature/2026-04-20

Conversation

@wernerbihl
Copy link
Copy Markdown
Collaborator

Introduce map resizing functionality and enhance image editing capabilities with new shape tools. Improve import/export features with support for multiple asset types and a refined user interface. Refactor code for better readability and consistency throughout the project.

wernerbihl and others added 12 commits April 19, 2026 23:41
- Implemented line, rectangle, and contour drawing tools in `image-editor-shape-tools.ts`.
- Added Bresenham's line algorithm and pixel manipulation functions in `image-editor-tools-shared.ts`.
- Created utility functions for property cloning and remapping layer IDs in `project-import.ts`.
- Defined application shell props and image editor hook internals types.
- Introduced crop state and rectangle types for image editing.
- Developed layers panel types for managing layer interactions and state.
- Expanded map panel types for enhanced map editing capabilities, including clipboard actions and history controls.

Co-authored-by: Copilot <copilot@github.com>
- Cleaned up import statements across multiple files for better organization.
- Reformatted state management and function calls to enhance clarity.
- Adjusted line breaks and indentation for improved code style.
- Ensured consistent use of arrow functions and callback structures.
- Removed unnecessary comments and streamlined code logic.
…ctions

Co-authored-by: Copilot <copilot@github.com>
…t for multiple asset types

Co-authored-by: Copilot <copilot@github.com>
…nctionality

Co-authored-by: Copilot <copilot@github.com>
- Implemented functions to handle importing raster images from files, including support for various formats (PNG, JPG, WEBP, BMP, GIF).
- Added functionality to render tilesets and maps to canvas, supporting multiple layer types and visibility settings.
- Introduced encoding methods for exporting canvas content as raster images in specified formats, including handling transparency and quality settings.
- Created utility functions for managing raster file types, MIME types, and image loading.

Co-authored-by: Copilot <copilot@github.com>
Copilot AI review requested due to automatic review settings April 20, 2026 19:08
@wernerbihl wernerbihl merged commit eee5375 into main Apr 20, 2026
3 checks passed
@wernerbihl wernerbihl deleted the feature/2026-04-20 branch April 20, 2026 19:10
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR introduces raster import/export utilities, adds new image editor tools (shape/selection/crop), and refactors UI/editor structure to support map resizing and a cleaner import/export flow.

Changes:

  • Added raster rendering/encoding utilities (PNG/JPG/WebP/GIF/BMP) and ZIP helpers for import/export.
  • Implemented new image editor tooling (shape drawing, selection/crop manipulation) plus palette/layer/frame action hooks.
  • Refactored editor UI: extracted resize controls, introduced reusable MapPanel components, and moved the main editor shell into a dedicated AppShell.

Reviewed changes

Copilot reviewed 39 out of 55 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/lib/import-export-raster.ts New raster import/render/export helpers for maps/tilesets and canvas encoding.
src/lib/image-editor-tools-shared.ts Shared low-level pixel + geometry utilities for image editor tools.
src/lib/image-editor-shape-tools.ts New line/rectangle/contour drawing tools built on shared pixel helpers.
src/lib/image-editor-selection-tools.ts New selection/floating transform logic (move/resize/copy/paste).
src/lib/image-editor-document.ts Centralized image editor document state, compositing, and undo/redo stacks.
src/lib/image-editor-crop-tools.ts New crop interaction implementation with resize handles.
src/lib/format.ts Added filename sanitization and ZIP archive creation helper.
src/hooks/use-image-editor-palette-actions.ts Added palette import/export and palette management actions.
src/hooks/use-image-editor-layer-actions.ts Added layer/image-layer/group CRUD + reorder actions for image editor.
src/hooks/use-image-editor-frame-actions.ts Added frame CRUD + undo/redo operations for frames.
src/components/tools/ImageEditor/ImageCanvasResizeControls.tsx Extracted canvas resize UI controls into a dedicated component.
src/components/tools/ImageEditor/ImageCanvas.tsx Refactored to use extracted resize controls component.
src/components/layout/Toolbar.tsx Simplified import/export menu into new dialog entry points + updated help text.
src/components/editor/TilesetPanel.tsx Added drag-and-drop image import to create tilesets + minor input attributes.
src/components/editor/MapPanel/use-map-panel-canvas-actions.ts Extracted MapPanel canvas action handlers into a reusable hook.
src/components/editor/MapPanel/MapPanelWorkspace.tsx New workspace wrapper wiring MapCanvas + context menu actions.
src/components/editor/MapPanel/MapPanelToolbar.tsx New MapPanel toolbar with tool/brush/fill/orientation/zoom controls.
src/components/editor/MapPanel/MapPanelTabs.tsx New map group selector + map tabs UI with rename/duplicate/delete intents.
src/components/editor/MapPanel/MapPanelDialogs.tsx New MapPanel dialog composition (options, new map/group, delete confirm).
src/components/editor/MapCanvas/use-map-resize.ts New hook encapsulating map resize pointer interaction and preview state.
src/components/editor/MapCanvas/scene-interaction-handlers.ts Expanded scene interactions (object placement, polygon, image layer transforms).
src/components/editor/MapCanvas/index.tsx Refactored MapCanvas to use useMapResize + extracted resize controls.
src/components/editor/MapCanvas/MapResizeControls.tsx Extracted map resize UI controls into a dedicated component.
src/components/editor/LayersPanel/index.tsx Refactored LayersPanel rendering into LayersTree + extracted delete dialog.
src/components/editor/LayersPanel/LayersTree.tsx New component encapsulating layer/group tree rendering.
src/components/editor/LayersPanel/DeleteLayerDialog.tsx New extracted delete-confirm dialog for layers/groups.
src/components/dialogs/ProjectDialog.tsx Refactored project open to route through openProjectInEditor.
src/components/dialogs/BugReportDialog.tsx Updated copy to include feature requests and adjusted textarea placeholder usage.
src/components/app/AppShell.tsx New top-level editor shell coordinating layout, dialogs, and import/export.
src/App.tsx Slimmed down by moving editor shell logic into components/app/AppShell.
components.json Removed external registry configuration.
TODO.txt Updated TODOs related to import/export and UX polish.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +233 to +239
const file = Array.from(e.dataTransfer.files).find((candidate) =>
candidate.type.startsWith("image/"),
);
if (!file) return;

e.preventDefault();
setIsDropTargetActive(false);
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleCanvasDrop returns early without calling preventDefault() when the drop payload contains no image. This can cause the browser to navigate/open the dropped file, and it can also leave isDropTargetActive stuck true (overlay remains visible). Move e.preventDefault() (and setIsDropTargetActive(false)) before the if (!file) return, and consider also calling e.stopPropagation() to avoid unintended parent drop handlers.

Suggested change
const file = Array.from(e.dataTransfer.files).find((candidate) =>
candidate.type.startsWith("image/"),
);
if (!file) return;
e.preventDefault();
setIsDropTargetActive(false);
e.preventDefault();
e.stopPropagation();
setIsDropTargetActive(false);
const file = Array.from(e.dataTransfer.files).find((candidate) =>
candidate.type.startsWith("image/"),
);
if (!file) return;

Copilot uses AI. Check for mistakes.
Comment on lines +110 to +111
anchor.click();
URL.revokeObjectURL(url);
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URL.revokeObjectURL(url) is called immediately after anchor.click(). In some browsers this can revoke the URL before the download is initiated, leading to flaky/failed downloads. Prefer appending the anchor to the DOM and revoking the URL asynchronously (e.g., setTimeout(..., 0) or requestAnimationFrame) after the click.

Suggested change
anchor.click();
URL.revokeObjectURL(url);
document.body.appendChild(anchor);
anchor.click();
setTimeout(() => {
anchor.remove();
URL.revokeObjectURL(url);
}, 0);

Copilot uses AI. Check for mistakes.
Comment thread src/App.tsx
} from "@/lib/ids";
import { findLastLayerId, getAllGroupIds, getAllLayerIds } from "@/lib/layers";
import { clearTileEditorContext } from "@/lib/tile-editor-context";
import {} from "@/lib/db";
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import {} from "@/lib/db"; is a no-op import and is likely unintended. It adds noise and may trip lint rules; it should be removed.

Suggested change
import {} from "@/lib/db";

Copilot uses AI. Check for mistakes.
Comment on lines +162 to +193
const baseName = activePalette.name || "palette";
const colors = activePalette.colors;

let blob: Blob;
let filename: string;

if (format === "ase") {
const buffer = writePhotoshopAse(colors);
blob = new Blob([buffer], { type: "application/octet-stream" });
filename = `${baseName}.ase`;
} else if (format === "aseprite") {
const buffer = writeAsePalette(colors);
blob = new Blob([buffer], { type: "application/octet-stream" });
filename = `${baseName}.aseprite`;
} else if (format === "gpl") {
blob = new Blob([writeGpl(colors, baseName)], { type: "text/plain" });
filename = `${baseName}.gpl`;
} else if (format === "pal") {
blob = new Blob([writeJascPal(colors)], { type: "text/plain" });
filename = `${baseName}.pal`;
} else if (format === "txt") {
blob = new Blob([writePaintNetTxt(colors, baseName)], {
type: "text/plain",
});
filename = `${baseName}.txt`;
} else if (format === "hex") {
blob = new Blob([writeHex(colors)], { type: "text/plain" });
filename = `${baseName}.hex`;
} else {
blob = await writePng(colors, swatchSize);
filename = `${baseName}-${swatchSize}px.png`;
}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Export filenames are built directly from activePalette.name, which may contain characters invalid for downloads on some platforms (e.g., /, \, :). Since sanitizeDownloadSegment/buildDownloadFilename were added in src/lib/format.ts, consider using them here (or otherwise sanitizing baseName) before calling downloadBlob.

Copilot uses AI. Check for mistakes.
Comment thread src/lib/format.ts
Comment on lines +129 to +139
export function createZipArchive(
entries: ImportExportArchiveEntry[],
): Uint8Array {
const archiveEntries: Record<string, Uint8Array> = {};

for (const entry of entries) {
archiveEntries[entry.path.replace(/\\/g, "/")] = entry.data;
}

return zipSync(archiveEntries, { level: 6 });
}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

entry.path is normalized for slashes but not validated for traversal segments (e.g., ../, leading /, or drive letters). If any entries paths can be influenced by user-controlled names, the produced ZIP could contain “zip-slip” style paths when extracted. Consider sanitizing paths to strip traversal components and enforce a safe, relative path structure before passing to zipSync.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +67
const sourceCanvas = document.createElement("canvas");
sourceCanvas.width = state.floatingPixels.width;
sourceCanvas.height = state.floatingPixels.height;
const sourceContext = sourceCanvas.getContext("2d");
if (!sourceContext) return null;
sourceContext.putImageData(state.floatingPixels, 0, 0);

const destinationCanvas = document.createElement("canvas");
destinationCanvas.width = displayWidth;
destinationCanvas.height = displayHeight;
const destinationContext = destinationCanvas.getContext("2d");
if (!destinationContext) return null;
destinationContext.imageSmoothingEnabled = false;
destinationContext.drawImage(sourceCanvas, 0, 0, displayWidth, displayHeight);

return destinationContext.getImageData(0, 0, displayWidth, displayHeight);
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getScaledFloating() allocates two canvases (and contexts) each time it needs to scale the floating selection. This is likely to be called frequently during resize/drag operations and can cause GC churn and jank for larger selections. Consider caching/reusing a single pair of offscreen canvases/contexts across calls (module-level or captured in closure), resizing them as needed.

Copilot uses AI. Check for mistakes.
Comment on lines +253 to +264
for (const layerId of visibleIds) {
const data = moduleLayerFrameData.get(layerDataKey(frameId, layerId));
if (!data) continue;

const tempCanvas = document.createElement("canvas");
tempCanvas.width = width;
tempCanvas.height = height;
const tempContext = tempCanvas.getContext("2d");
if (!tempContext) continue;
tempContext.putImageData(data, 0, 0);
context.drawImage(tempCanvas, 0, 0);
}
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

computeComposite* creates a new temporary canvas for every layer on every call. For multi-layer or animated documents this can be a significant overhead. Consider reusing a single tempCanvas/tempContext per composite call (create once outside the loop and just putImageData each iteration), or caching reusable offscreen resources to reduce allocations.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants