Add hints
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "vectrace",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "vectrace",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"dependencies": {
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"@types/lodash": "^4.17.24",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "vectrace",
|
||||
"private": true,
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.q",
|
||||
"author": {
|
||||
"name": "Terncode",
|
||||
"email": "[email protected]",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button } from "../styles";
|
||||
import styled from "styled-components";
|
||||
import { createPdf, createSVGDoc, downloadBlob, downloadZip } from "../utils/download";
|
||||
import type { ImageEditor } from "../handler/image-editor";
|
||||
import { Hint } from "./tooltip";
|
||||
|
||||
const Btn = styled(Button)`
|
||||
width: 100%;
|
||||
@@ -39,6 +40,7 @@ export function ImportExportButtons({ handler, selected }: CanvasHandlerProps &
|
||||
<Box $width={boxSize} $height={boxSize}>
|
||||
<Column>
|
||||
<Row>
|
||||
<Hint hint={"Download all project assets as a ZIP archive"}>
|
||||
<Btn
|
||||
onClick={() => {
|
||||
downloadZip(handler);
|
||||
@@ -47,6 +49,8 @@ export function ImportExportButtons({ handler, selected }: CanvasHandlerProps &
|
||||
<FaFileZipper />
|
||||
ZIP
|
||||
</Btn>
|
||||
</Hint>
|
||||
<Hint hint={"Combine all assets into a single PDF"}>
|
||||
<Btn
|
||||
onClick={() => {
|
||||
createPdf(handler, true).save(`${handler.projectName}.pdf`);
|
||||
@@ -55,6 +59,8 @@ export function ImportExportButtons({ handler, selected }: CanvasHandlerProps &
|
||||
<FaFilePdf />
|
||||
PDF
|
||||
</Btn>
|
||||
</Hint>
|
||||
<Hint hint={"Merge all SVG paths into one .svg file"}>
|
||||
<Btn
|
||||
onClick={async () => {
|
||||
downloadBlob(await createSVGDoc(handler), `${handler.projectName}.svg`);
|
||||
@@ -63,8 +69,10 @@ export function ImportExportButtons({ handler, selected }: CanvasHandlerProps &
|
||||
<FaDrawPolygon />
|
||||
SVG
|
||||
</Btn>
|
||||
</Hint>
|
||||
</Row>
|
||||
<Row>
|
||||
<Hint hint={"Download selected layer as PNG"}>
|
||||
<Btn
|
||||
disabled={selected.length !== 1}
|
||||
onClick={() => {
|
||||
@@ -90,6 +98,8 @@ export function ImportExportButtons({ handler, selected }: CanvasHandlerProps &
|
||||
<FaImage />
|
||||
Image
|
||||
</Btn>
|
||||
</Hint>
|
||||
<Hint hint={"Download SVG path for selected layer"}>
|
||||
<Btn
|
||||
disabled={selected.length !== 1}
|
||||
onClick={() => {
|
||||
@@ -104,16 +114,17 @@ export function ImportExportButtons({ handler, selected }: CanvasHandlerProps &
|
||||
}
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
<FaDrawPolygon />
|
||||
SVG
|
||||
</Btn>
|
||||
</Hint>
|
||||
</Row>
|
||||
</Column>
|
||||
</Box>
|
||||
<Box $width={boxSize} $height={boxSize}>
|
||||
<Column>
|
||||
<Row>
|
||||
<Hint hint={"Save entire project to a .ppd file"}>
|
||||
<Btn
|
||||
onClick={() => {
|
||||
handler.exportProject();
|
||||
@@ -122,6 +133,8 @@ export function ImportExportButtons({ handler, selected }: CanvasHandlerProps &
|
||||
<FaFileExport />
|
||||
Export
|
||||
</Btn>
|
||||
</Hint>
|
||||
<Hint hint={"Load a project from a .ppd file"}>
|
||||
<Btn
|
||||
onClick={() => {
|
||||
handler.importProject();
|
||||
@@ -130,7 +143,9 @@ export function ImportExportButtons({ handler, selected }: CanvasHandlerProps &
|
||||
<FaFileImport />
|
||||
Import
|
||||
</Btn>
|
||||
</Hint>
|
||||
</Row>
|
||||
<Hint hint={"Permanently wipe the canvas — cannot be undone"}>
|
||||
<Btn
|
||||
style={{ backgroundColor: "#ff000073" }}
|
||||
disabled={selected.length !== 1}
|
||||
@@ -150,6 +165,7 @@ export function ImportExportButtons({ handler, selected }: CanvasHandlerProps &
|
||||
<FaBroom />
|
||||
Clear
|
||||
</Btn>
|
||||
</Hint>
|
||||
</Column>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { calculateMathExpression, canEvaluateMathExpression } from "../calc";
|
||||
const StyledInput = styled.input<{ $isValid: boolean | null }>`
|
||||
padding: 1px;
|
||||
font-size: 1rem;
|
||||
height: 15px;
|
||||
width: 80px;
|
||||
border: 1px solid ${({ $isValid }) => ($isValid === null ? "#888" : $isValid ? "#ffffff" : "#ef4444")};
|
||||
outline: none;
|
||||
|
||||
@@ -9,9 +9,9 @@ import { VerticalSlider } from "./vertical-slider";
|
||||
import { TRANSPARENCY_THRESHOLD_DEFAULT } from "../svg-tracer";
|
||||
import type { VectraceHandler } from "../handler/vectrace-handler";
|
||||
import { clamp } from "lodash";
|
||||
import { Hint } from "./tooltip";
|
||||
|
||||
const Table = styled.table`
|
||||
margin: 2px;
|
||||
padding-right: 4px;
|
||||
`;
|
||||
|
||||
@@ -31,7 +31,7 @@ const LayerPanel = styled.div`
|
||||
flex-direction: column;
|
||||
width: 100px;
|
||||
overflow: auto;
|
||||
height: 100px;
|
||||
height: 97px;
|
||||
`;
|
||||
|
||||
const ToggleButton = styled.button<{ $enabled: boolean }>`
|
||||
@@ -67,6 +67,12 @@ export function SvgTracerOptions({ selected, handler }: { selected: ImageEditor[
|
||||
|
||||
return (
|
||||
<WrapperRow>
|
||||
<Hint
|
||||
hint={
|
||||
"Transparency cutoff — pixels more transparent than this value are ignored when tracing. " +
|
||||
"Lower = only opaque pixels traced, Higher = transparent pixels included in the path."
|
||||
}
|
||||
>
|
||||
<VerticalSlider
|
||||
max={255}
|
||||
min={0}
|
||||
@@ -88,39 +94,62 @@ export function SvgTracerOptions({ selected, handler }: { selected: ImageEditor[
|
||||
setValue("transparencyThreshold", value);
|
||||
}}
|
||||
/>
|
||||
</Hint>
|
||||
<WrapperColumn>
|
||||
<Button
|
||||
$active={!updated}
|
||||
title="Draw"
|
||||
onClick={() => {
|
||||
redraw();
|
||||
}}
|
||||
>
|
||||
<FaBezierCurve />
|
||||
<span>
|
||||
<span>Redraw</span>
|
||||
<FaBezierCurve style={{ paddingLeft: 10 }} />
|
||||
</span>
|
||||
</Button>
|
||||
<WrapperRow>
|
||||
<Table>
|
||||
<tbody>
|
||||
<Hint
|
||||
hint={
|
||||
"Line threshold — how closely straight lines must follow the original shape. Lower = more accurate, more points."
|
||||
}
|
||||
>
|
||||
<NumericInputWithLabelTable
|
||||
name="Ltres"
|
||||
value={options.ltres}
|
||||
onChange={(value) => setValue("ltres", value)}
|
||||
/>
|
||||
</Hint>
|
||||
<Hint
|
||||
hint={
|
||||
"Curve threshold — how closely curves must follow the original shape. Lower = smoother curves, more points."
|
||||
}
|
||||
>
|
||||
<NumericInputWithLabelTable
|
||||
name="Qtres"
|
||||
value={options.qtres}
|
||||
onChange={(value) => setValue("qtres", value)}
|
||||
/>
|
||||
</Hint>
|
||||
<Hint
|
||||
hint={
|
||||
"Ignore tiny paths smaller than this many pixels. Higher = cleaner output, but small details get dropped."
|
||||
}
|
||||
>
|
||||
<NumericInputWithLabelTable
|
||||
name="Path omit"
|
||||
value={options.pathomit}
|
||||
onChange={(value) => setValue("pathomit", value)}
|
||||
/>
|
||||
</Hint>
|
||||
<Hint hint={"Decimal precision of path coordinates. Lower = smaller file, slightly less accurate."}>
|
||||
<NumericInputWithLabelTable
|
||||
name="Round corners"
|
||||
value={options.roundcoords}
|
||||
onChange={(value) => setValue("roundcoords", value)}
|
||||
/>
|
||||
</Hint>
|
||||
</tbody>
|
||||
</Table>
|
||||
<LayerPanel>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useState, useRef, useEffect, type ReactNode } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
interface HintProps {
|
||||
hint: ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function Hint({ hint, children }: HintProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [pos, setPos] = useState({ top: 0, left: 0 });
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const updatePos = () => {
|
||||
if (!wrapperRef.current?.children[0]) return;
|
||||
const rect = wrapperRef.current.children[0].getBoundingClientRect();
|
||||
setPos({
|
||||
top: rect.bottom,
|
||||
left: rect.left + rect.width / 2,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !tooltipRef.current) return;
|
||||
const tip = tooltipRef.current.getBoundingClientRect();
|
||||
const margin = 8;
|
||||
|
||||
setPos((prev) => ({
|
||||
top: tip.top,
|
||||
left: Math.min(Math.max(prev.left, tip.width / 2 + margin), window.innerWidth - tip.width / 2 - margin),
|
||||
}));
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
style={{ display: "contents" }}
|
||||
onMouseEnter={() => {
|
||||
updatePos();
|
||||
setOpen(true);
|
||||
}}
|
||||
onMouseLeave={() => setOpen(false)}
|
||||
>
|
||||
{children}
|
||||
{open &&
|
||||
createPortal(
|
||||
<div
|
||||
ref={tooltipRef}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: pos.top,
|
||||
left: pos.left,
|
||||
transform: "translateX(-50%)",
|
||||
background: "#141414",
|
||||
border: "1px solid #4af626",
|
||||
color: "#fff",
|
||||
padding: "6px 10px",
|
||||
fontSize: "12px",
|
||||
maxWidth: "260px",
|
||||
pointerEvents: "none",
|
||||
zIndex: 9999,
|
||||
whiteSpace: "pre-wrap",
|
||||
}}
|
||||
>
|
||||
{hint}
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -116,7 +116,7 @@ export function VerticalSlider({ min = 0, max = 100, step = 1, onLabelClick, val
|
||||
onLabelClick?.();
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
{Math.floor((value / max) * 100)}%
|
||||
</ValueLabel>
|
||||
<Track min={min} max={max} step={step} value={value} $fillPercent={fillPercent} onChange={handleChange} />
|
||||
</SliderWrapper>
|
||||
|
||||
Reference in New Issue
Block a user