feat: boot-экран, стабильность Windows и оптимизация Pixi/пульта
- Экран загрузки (boot.html, bootWindow): статусы, ensureRoots и проверка лицензии, редактор после готовности; закрытие через destroy при closable:false. - Упакованное приложение на Windows: disableHardwareAcceleration, sandbox выкл. вне dev, отложенный показ редактора, ensureWindowBecomesVisible, фокус на splash при second-instance. - Vite: вход boot.html; eslint: игнор release/; тесты boot и maxFPS тикера. - Пульт: позиция курсора кисти через ref/DOM без setState на каждый move; черновик эффекта через rAF; Pixi: maxFPS 32, resolution cap, antialias off, debounce ResizeObserver, меньше частиц poisonCloud, contain на хосте. Made-with: Cursor
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { pickEraseTargetId } from '../../shared/effectEraserHitTest';
|
||||
import { ipcChannels } from '../../shared/ipc/contracts';
|
||||
@@ -62,7 +62,6 @@ export function ControlApp() {
|
||||
points?: { x: number; y: number; tMs: number }[];
|
||||
} | null>(null);
|
||||
const [draftFxTick, setDraftFxTick] = useState(0);
|
||||
const [cursorN, setCursorN] = useState<{ x: number; y: number } | null>(null);
|
||||
const [previewSize, setPreviewSize] = useState<{ w: number; h: number }>({ w: 1, h: 1 });
|
||||
const [previewContentRect, setPreviewContentRect] = useState<{
|
||||
x: number;
|
||||
@@ -70,6 +69,13 @@ export function ControlApp() {
|
||||
w: number;
|
||||
h: number;
|
||||
} | null>(null);
|
||||
const previewContentRectRef = useRef(previewContentRect);
|
||||
previewContentRectRef.current = previewContentRect;
|
||||
const previewSizeRef = useRef(previewSize);
|
||||
previewSizeRef.current = previewSize;
|
||||
const brushCursorElRef = useRef<HTMLDivElement | null>(null);
|
||||
const cursorPosRef = useRef<{ x: number; y: number } | null>(null);
|
||||
const draftPaintRafRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
void api.invoke(ipcChannels.project.get, {}).then((res) => {
|
||||
@@ -351,12 +357,58 @@ export function ControlApp() {
|
||||
}, [currentGraphNodeId, project]);
|
||||
|
||||
const tool = fxState?.tool ?? { tool: 'fog', radiusN: 0.08, intensity: 0.6 };
|
||||
const toolRef = useRef(tool);
|
||||
toolRef.current = tool;
|
||||
|
||||
function layoutBrushCursor(): void {
|
||||
const el = brushCursorElRef.current;
|
||||
const p = cursorPosRef.current;
|
||||
const cr = previewContentRectRef.current;
|
||||
const ps = previewSizeRef.current;
|
||||
const t = toolRef.current;
|
||||
if (!el) return;
|
||||
if (!p) {
|
||||
el.style.visibility = 'hidden';
|
||||
return;
|
||||
}
|
||||
el.style.visibility = 'visible';
|
||||
const ox = cr ? cr.x : 0;
|
||||
const oy = cr ? cr.y : 0;
|
||||
const cw = cr ? cr.w : ps.w;
|
||||
const ch = cr ? cr.h : ps.h;
|
||||
const minDim = Math.min(cw, ch);
|
||||
const size = Math.max(2, t.radiusN * minDim * 2);
|
||||
el.style.left = `${String(ox + p.x * cw)}px`;
|
||||
el.style.top = `${String(oy + p.y * ch)}px`;
|
||||
el.style.width = `${String(size)}px`;
|
||||
el.style.height = `${String(size)}px`;
|
||||
}
|
||||
|
||||
function scheduleDraftRepaint(): void {
|
||||
if (draftPaintRafRef.current !== 0) return;
|
||||
draftPaintRafRef.current = requestAnimationFrame(() => {
|
||||
draftPaintRafRef.current = 0;
|
||||
setDraftFxTick((x) => x + 1);
|
||||
});
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
layoutBrushCursor();
|
||||
}, [tool.radiusN, previewContentRect, previewSize.w, previewSize.h]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (draftPaintRafRef.current !== 0) {
|
||||
cancelAnimationFrame(draftPaintRafRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
function toNPoint(e: React.PointerEvent): { x: number; y: number } | null {
|
||||
const host = previewHostRef.current;
|
||||
if (!host) return null;
|
||||
const r = host.getBoundingClientRect();
|
||||
const cr = previewContentRect;
|
||||
const cr = previewContentRectRef.current;
|
||||
const ox = cr ? cr.x : 0;
|
||||
const oy = cr ? cr.y : 0;
|
||||
const cw = cr ? cr.w : r.width;
|
||||
@@ -914,45 +966,29 @@ export function ControlApp() {
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{cursorN ? (
|
||||
<div
|
||||
className={styles.brushCursor}
|
||||
style={{
|
||||
left:
|
||||
(previewContentRect ? previewContentRect.x : 0) +
|
||||
cursorN.x * (previewContentRect ? previewContentRect.w : previewSize.w),
|
||||
top:
|
||||
(previewContentRect ? previewContentRect.y : 0) +
|
||||
cursorN.y * (previewContentRect ? previewContentRect.h : previewSize.h),
|
||||
width:
|
||||
tool.radiusN *
|
||||
Math.min(
|
||||
previewContentRect ? previewContentRect.w : previewSize.w,
|
||||
previewContentRect ? previewContentRect.h : previewSize.h,
|
||||
) *
|
||||
2,
|
||||
height:
|
||||
tool.radiusN *
|
||||
Math.min(
|
||||
previewContentRect ? previewContentRect.w : previewSize.w,
|
||||
previewContentRect ? previewContentRect.h : previewSize.h,
|
||||
) *
|
||||
2,
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<div
|
||||
ref={brushCursorElRef}
|
||||
className={styles.brushCursor}
|
||||
style={{ visibility: 'hidden' }}
|
||||
aria-hidden
|
||||
/>
|
||||
<div
|
||||
className={styles.brushLayer}
|
||||
onPointerEnter={(e) => {
|
||||
const p = toNPoint(e);
|
||||
if (!p) return;
|
||||
setCursorN(p);
|
||||
cursorPosRef.current = p;
|
||||
layoutBrushCursor();
|
||||
}}
|
||||
onPointerLeave={() => {
|
||||
cursorPosRef.current = null;
|
||||
layoutBrushCursor();
|
||||
}}
|
||||
onPointerLeave={() => setCursorN(null)}
|
||||
onPointerDown={(e) => {
|
||||
const p = toNPoint(e);
|
||||
if (!p) return;
|
||||
setCursorN(p);
|
||||
cursorPosRef.current = p;
|
||||
layoutBrushCursor();
|
||||
(e.currentTarget as HTMLDivElement).setPointerCapture(e.pointerId);
|
||||
if (tool.tool === 'eraser') {
|
||||
const id = pickEraseTargetId(fxState?.instances ?? [], p, tool.radiusN);
|
||||
@@ -969,7 +1005,8 @@ export function ControlApp() {
|
||||
onPointerMove={(e) => {
|
||||
const p = toNPoint(e);
|
||||
if (!p) return;
|
||||
setCursorN(p);
|
||||
cursorPosRef.current = p;
|
||||
layoutBrushCursor();
|
||||
if (tool.tool === 'eraser' && (e.buttons & 1) !== 0) {
|
||||
const id = pickEraseTargetId(fxState?.instances ?? [], p, tool.radiusN);
|
||||
if (id) void fx.dispatch({ kind: 'instance.remove', id });
|
||||
@@ -984,7 +1021,7 @@ export function ControlApp() {
|
||||
const minStep = Math.max(0.004, tool.radiusN * 0.25);
|
||||
if (dx * dx + dy * dy < minStep * minStep) return;
|
||||
b.points.push({ x: p.x, y: p.y, tMs: Date.now() });
|
||||
setDraftFxTick((x) => x + 1);
|
||||
scheduleDraftRepaint();
|
||||
}}
|
||||
onPointerUp={() => {
|
||||
void commitStroke();
|
||||
|
||||
Reference in New Issue
Block a user