Лицензия, редактор, пульт и сборка
- Main: license service, IPC, router; закрытие окон; yauzl закрытие zip (EMFILE), zipRead тест - Editor: стабильный projectState без мигания, логотип и меню, строки UI, LayoutShell overlay - Control: ластик для всех типов эффектов, затухание/нарастание музыки при смене сцены - Сборка: vite, build/dev scripts, obfuscate-main и build-env скрипты с тестами; package.json Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import type { EffectInstance } from './types/effects';
|
||||
|
||||
function distSqPointToSegment(
|
||||
px: number,
|
||||
py: number,
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
): number {
|
||||
const dx = x2 - x1;
|
||||
const dy = y2 - y1;
|
||||
const len2 = dx * dx + dy * dy;
|
||||
if (len2 < 1e-18) {
|
||||
const ex = px - x1;
|
||||
const ey = py - y1;
|
||||
return ex * ex + ey * ey;
|
||||
}
|
||||
let t = ((px - x1) * dx + (py - y1) * dy) / len2;
|
||||
t = Math.max(0, Math.min(1, t));
|
||||
const qx = x1 + t * dx;
|
||||
const qy = y1 + t * dy;
|
||||
const ex = px - qx;
|
||||
const ey = py - qy;
|
||||
return ex * ex + ey * ey;
|
||||
}
|
||||
|
||||
/** Минимальная квадрат дистанции от точки (норм. координаты) до «тела» эффекта — для ластика. */
|
||||
export function minDistSqEffectToPoint(inst: EffectInstance, p: { x: number; y: number }): number {
|
||||
switch (inst.type) {
|
||||
case 'fog':
|
||||
case 'fire':
|
||||
case 'rain': {
|
||||
let best = Number.POSITIVE_INFINITY;
|
||||
for (const q of inst.points) {
|
||||
const dx = q.x - p.x;
|
||||
const dy = q.y - p.y;
|
||||
best = Math.min(best, dx * dx + dy * dy);
|
||||
}
|
||||
return best;
|
||||
}
|
||||
case 'lightning':
|
||||
return distSqPointToSegment(p.x, p.y, inst.start.x, inst.start.y, inst.end.x, inst.end.y);
|
||||
case 'freeze': {
|
||||
const dx = inst.at.x - p.x;
|
||||
const dy = inst.at.y - p.y;
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
case 'scorch':
|
||||
case 'ice': {
|
||||
const dx = inst.at.x - p.x;
|
||||
const dy = inst.at.y - p.y;
|
||||
return dx * dx + dy * dy;
|
||||
}
|
||||
default:
|
||||
return Number.POSITIVE_INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
function eraseHitThresholdSq(inst: EffectInstance, toolRadiusN: number): number {
|
||||
if (inst.type === 'scorch' || inst.type === 'ice') {
|
||||
const r = toolRadiusN + inst.radiusN;
|
||||
return r * r;
|
||||
}
|
||||
return toolRadiusN * toolRadiusN;
|
||||
}
|
||||
|
||||
/** Ближайший эффект в пределах радиуса ластика, иначе `null`. */
|
||||
export function pickEraseTargetId(
|
||||
instances: readonly EffectInstance[],
|
||||
p: { x: number; y: number },
|
||||
toolRadiusN: number,
|
||||
): string | null {
|
||||
let best: { id: string; dd: number } | null = null;
|
||||
for (const inst of instances) {
|
||||
const dd = minDistSqEffectToPoint(inst, p);
|
||||
const th = eraseHitThresholdSq(inst, toolRadiusN);
|
||||
if (dd <= th && (!best || dd < best.dd)) {
|
||||
best = { id: inst.id, dd };
|
||||
}
|
||||
}
|
||||
return best?.id ?? null;
|
||||
}
|
||||
Reference in New Issue
Block a user