Refactoring round 5 covers the code since round 4 without changing behaviour: four identical copies of dot() and three of "nearest point on a segment" stood in the collision math, in the hit area of the collider outlines and in the polygon handles, so they moved into the new module src/core/util/vec.ts; the warning line, the busy state, info() and the caret reset were written out line by line in both the sign-in screen and the password dialog and are now FormStatus; the closing row "Cancel plus a confirming button, optionally one on the left" was built by hand in FormDialog, FilePicker, OpenProjectDialog and PasswordDialog and is now dialogButtons(); the header bar was drawn by the toolbar, the TabHeader and the Breadcrumb each on its own and is now drawHeaderBar(); AddColliderCommand and AddRigidbodyCommand share a base, each subclass only builds its own asset; Editor.ts derives the panes from assetEditors instead of repeating eight entries and clamps its splitters with clamp(); jscpd over src falls from 0.19 % to 0.14 %; dead code is gone - listScenes() has been orphaned since the Scene picker fell away (#183), DEFAULT_FILTER (#152) was never used, the two test hooks projectPickerRowAt/rowPosition and particleCompiles are read by no tool, and four functions lost an export they did not need; seven comments that told history ("used to", "no longer", "since #...") describe the state of things now, and that this changed nothing was proved by a rebuild - the bundle is md5 identical, b4ad0dfe... before and after; the README said Logout sits at the far right of the toolbar, which has been the Account menu since #182, and wiki/Architecture.md was missing src/core/util/ altogether; 4 new unit tests cover vec.ts, and switching the clamp on the segment off makes exactly one of them fail while none of the 76 neighbouring tests notice, so that clamping was covered nowhere before (#85)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
da35f10d1a
commit
ae3aa7d3bd
32 files changed
+240
-246
No files matched your search
+9
-16
@@ -318,20 +318,6 @@ export class Editor implements PlayHost {
|
||||
};
|
||||
this.tilemapPanel.onTargetChange = () => this.syncTilemapPanel();
|
||||
this.syncTilemapPanel();
|
||||
this.panes = {
|
||||
scene: this.sceneRow,
|
||||
game: this.gameView,
|
||||
graph: this.graphEditor,
|
||||
statemachine: this.stateMachineEditor,
|
||||
shader: this.shaderEditor,
|
||||
particles: this.particlesEditor,
|
||||
texture: this.textureEditor,
|
||||
animation: this.animationEditor,
|
||||
tile: this.tileEditor,
|
||||
container: this.containerEditor,
|
||||
list: this.listEditor,
|
||||
plugins: this.pluginsPane,
|
||||
};
|
||||
this.assetEditors = {
|
||||
graph: this.graphEditor,
|
||||
statemachine: this.stateMachineEditor,
|
||||
@@ -342,6 +328,13 @@ export class Editor implements PlayHost {
|
||||
tile: this.tileEditor,
|
||||
container: this.containerEditor,
|
||||
};
|
||||
this.panes = {
|
||||
scene: this.sceneRow,
|
||||
game: this.gameView,
|
||||
...this.assetEditors,
|
||||
list: this.listEditor,
|
||||
plugins: this.pluginsPane,
|
||||
};
|
||||
// Following a call goes on in the same tab: the tab changes its document, not its place.
|
||||
this.graphEditor.onRetarget = (from, to) => {
|
||||
const next = replaceDocument<DocumentId>(this.documents, `asset:${from}`, `asset:${to}`);
|
||||
@@ -369,12 +362,12 @@ export class Editor implements PlayHost {
|
||||
this.leftColumn.minWidth = this.leftWidth;
|
||||
this.inspector.minWidth = this.rightWidth;
|
||||
this.leftSplitter.onDrag = (delta) => {
|
||||
this.leftWidth = Math.max(MIN_LEFT_WIDTH, Math.min(MAX_LEFT_WIDTH, this.leftWidth + delta));
|
||||
this.leftWidth = clamp(this.leftWidth + delta, MIN_LEFT_WIDTH, MAX_LEFT_WIDTH);
|
||||
this.leftColumn.minWidth = this.leftWidth;
|
||||
this.workspace.relayout();
|
||||
};
|
||||
this.rightSplitter.onDrag = (delta) => {
|
||||
this.rightWidth = Math.max(MIN_RIGHT_WIDTH, Math.min(MAX_RIGHT_WIDTH, this.rightWidth - delta));
|
||||
this.rightWidth = clamp(this.rightWidth - delta, MIN_RIGHT_WIDTH, MAX_RIGHT_WIDTH);
|
||||
this.inspector.minWidth = this.rightWidth;
|
||||
this.workspace.relayout();
|
||||
};
|
||||
|
||||
@@ -15,16 +15,15 @@ import {
|
||||
Button,
|
||||
drawControlBox,
|
||||
formatBytes,
|
||||
HStack,
|
||||
Label,
|
||||
Popup,
|
||||
pressCount,
|
||||
ScrollContainer,
|
||||
Spacer,
|
||||
Theme,
|
||||
textStyle,
|
||||
Widget,
|
||||
} from '../ui';
|
||||
import { dialogButtons } from './FormDialog';
|
||||
|
||||
/** The dialog keeps this size whatever the list turns out to be, so it does not jump on opening. */
|
||||
export const PICKER_WIDTH = 560;
|
||||
@@ -255,19 +254,12 @@ export abstract class FilePicker<Item extends PickerItem> extends Popup {
|
||||
this.detail = new Label(words.idle ?? '', { style: 'muted' });
|
||||
this.content.add(this.detail);
|
||||
|
||||
const buttons = new HStack({ gap: Theme.gap, align: 'center' });
|
||||
const uploadButton = new Button('Upload…');
|
||||
uploadButton.onClick = this.closeThen(upload);
|
||||
buttons.add(uploadButton);
|
||||
buttons.add(new Spacer(), 1);
|
||||
const cancel = new Button('Cancel');
|
||||
cancel.onClick = this.closeThen();
|
||||
buttons.add(cancel);
|
||||
this.use = new Button(words.use, { variant: 'primary' });
|
||||
this.use.onClick = () => this.confirm();
|
||||
this.use.setEnabled(false);
|
||||
buttons.add(this.use);
|
||||
this.content.add(buttons);
|
||||
this.content.add(dialogButtons(this.closeThen(), this.use, uploadButton));
|
||||
}
|
||||
|
||||
/** What the dialog offers, which of it the project already wears, and what is picked (test hook). */
|
||||
|
||||
@@ -31,19 +31,25 @@ export abstract class FormDialog extends Popup {
|
||||
|
||||
/** Cancel and the confirming button, right-aligned; the dialog closes before `onConfirm` runs. */
|
||||
protected addButtons(text: string, variant: ButtonOptions['variant'], onConfirm: () => void): Button {
|
||||
const buttons = new HStack({ gap: Theme.gap, align: 'center' });
|
||||
buttons.add(new Spacer(), 1);
|
||||
const cancel = new Button('Cancel');
|
||||
cancel.onClick = this.closeThen();
|
||||
buttons.add(cancel);
|
||||
const confirm = new Button(text, { variant });
|
||||
confirm.onClick = this.closeThen(onConfirm);
|
||||
buttons.add(confirm);
|
||||
this.content.add(buttons);
|
||||
this.content.add(dialogButtons(this.closeThen(), confirm));
|
||||
return confirm;
|
||||
}
|
||||
}
|
||||
|
||||
/** The row a dialog ends on: Cancel and `confirm` on the right, `leading` – if any – on the left. */
|
||||
export function dialogButtons(onCancel: () => void, confirm: Button, leading?: Button): HStack {
|
||||
const buttons = new HStack({ gap: Theme.gap, align: 'center' });
|
||||
if (leading) buttons.add(leading);
|
||||
buttons.add(new Spacer(), 1);
|
||||
const cancel = new Button('Cancel');
|
||||
cancel.onClick = onCancel;
|
||||
buttons.add(cancel);
|
||||
buttons.add(confirm);
|
||||
return buttons;
|
||||
}
|
||||
|
||||
/** One open dialog of a kind at a time: opening the next closes the last. `current` is for the check scripts. */
|
||||
export function dialogSlot<T extends Popup>(): { current(): T | null; open(dialog: T): T } {
|
||||
let current: T | null = null;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Button, HStack, Label, ListView, Popup, ScrollContainer, Spacer, Theme } from '../ui';
|
||||
import { Button, Label, ListView, Popup, ScrollContainer, Theme } from '../ui';
|
||||
import { dialogButtons } from './FormDialog';
|
||||
|
||||
/** One project as the list shows it. */
|
||||
export interface ProjectChoice {
|
||||
@@ -54,18 +55,11 @@ export class OpenProjectDialog extends Popup {
|
||||
this.scroll.setContent(this.list);
|
||||
this.content.add(this.scroll);
|
||||
|
||||
const buttons = new HStack({ gap: Theme.gap, align: 'center' });
|
||||
this.deleteButton = new Button('Delete', { variant: 'danger' });
|
||||
this.deleteButton.onClick = () => this.withPicked(actions.onDelete);
|
||||
buttons.add(this.deleteButton);
|
||||
buttons.add(new Spacer(), 1);
|
||||
const cancel = new Button('Cancel');
|
||||
cancel.onClick = this.closeThen();
|
||||
buttons.add(cancel);
|
||||
this.openButton = new Button('Open', { variant: 'primary' });
|
||||
this.openButton.onClick = () => this.withPicked(actions.onOpen);
|
||||
buttons.add(this.openButton);
|
||||
this.content.add(buttons);
|
||||
this.content.add(dialogButtons(this.closeThen(), this.openButton, this.deleteButton));
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
@@ -74,11 +68,6 @@ export class OpenProjectDialog extends Popup {
|
||||
return { names: [...this.names], picked: this.picked };
|
||||
}
|
||||
|
||||
/** Global centre of a row, so a check can click it. */
|
||||
rowPosition(id: string): { x: number; y: number } | null {
|
||||
return this.list.rowGlobal(id);
|
||||
}
|
||||
|
||||
private withPicked(action: (id: string) => void): void {
|
||||
const id = this.picked;
|
||||
if (id) this.closeThen(() => action(id))();
|
||||
|
||||
@@ -39,8 +39,8 @@ const PLAY_FROM_LABEL: Record<PlayFrom, string> = {
|
||||
|
||||
/**
|
||||
* Everything about the project that is not an asset (#183): its name, which scene Play starts, and
|
||||
* the collision layers (#152). The three used to sit in the top bar and in a menu entry of their
|
||||
* own; the bar carries icons now, so this is the one place they are edited.
|
||||
* the collision layers (#152). The top bar carries icons only, so this is the one place the three
|
||||
* are edited.
|
||||
*
|
||||
* Colliders name a layer by its *number*, so a layer taken out of the middle would silently move
|
||||
* every collider below it onto another one; only the last layer can be removed, and the list is
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Graphics } from 'pixi.js';
|
||||
import { HStack, Theme } from '../ui';
|
||||
import { drawHeaderBar, HStack, Theme } from '../ui';
|
||||
|
||||
/** The bar across the top of a tab: its controls in a row on the header colour, a line below. */
|
||||
export class TabHeader extends HStack {
|
||||
@@ -12,7 +12,6 @@ export class TabHeader extends HStack {
|
||||
}
|
||||
|
||||
override redraw(): void {
|
||||
this.bg.clear().rect(0, 0, this.w, this.h).fill(Theme.colors.panelHeader);
|
||||
this.bg.rect(0, this.h - 1, this.w, 1).fill(Theme.colors.border);
|
||||
drawHeaderBar(this.bg, this.w, this.h);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Label, requestRender, type TextField, ui } from '../../ui';
|
||||
|
||||
/**
|
||||
* What a form that can be refused shows about its last attempt – the sign-in screen and the
|
||||
* password dialog: the warning line under the fields, and whether an answer is still awaited.
|
||||
*/
|
||||
export class FormStatus {
|
||||
readonly message = new Label('', { style: 'warning' });
|
||||
private waiting = false;
|
||||
|
||||
/** `onChange` redoes what the form shows of it: its button while waiting, its layout after a message. */
|
||||
constructor(private readonly onChange: (busy: boolean) => void) {}
|
||||
|
||||
get busy(): boolean {
|
||||
return this.waiting;
|
||||
}
|
||||
|
||||
/** What the check scripts read. */
|
||||
info(): { message: string; busy: boolean } {
|
||||
return { message: this.message.text, busy: this.waiting };
|
||||
}
|
||||
|
||||
say(message: string): void {
|
||||
this.message.setText(message);
|
||||
this.changed();
|
||||
}
|
||||
|
||||
setBusy(busy: boolean): void {
|
||||
this.waiting = busy;
|
||||
this.changed();
|
||||
}
|
||||
|
||||
private changed(): void {
|
||||
this.onChange(this.waiting);
|
||||
requestRender();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the caret back into a field after a refused attempt. A field blurs itself once its Enter
|
||||
* has been handled, and that happens after this call returns – so the focus has to be set again
|
||||
* behind it, or the next thing typed goes nowhere.
|
||||
*/
|
||||
export function refocus(field: TextField): void {
|
||||
queueMicrotask(() => ui().focus.focus(field));
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Graphics } from 'pixi.js';
|
||||
import { type AuthStore, WrongCredentialsError } from '../../core/store/AuthStore';
|
||||
import { Button, drawControlBox, Label, requestRender, Spacer, TextField, Theme, ui, VStack } from '../../ui';
|
||||
import { Button, drawControlBox, Label, Spacer, TextField, Theme, ui, VStack } from '../../ui';
|
||||
import { FormStatus, refocus } from './FormStatus';
|
||||
import { loginError, loginProblem } from './loginRules';
|
||||
|
||||
const CARD_WIDTH = 280;
|
||||
@@ -31,9 +32,11 @@ class Card extends VStack {
|
||||
export class LoginScreen extends VStack {
|
||||
private readonly user: TextField;
|
||||
private readonly password: TextField;
|
||||
private readonly message: Label;
|
||||
private readonly submit: Button;
|
||||
private busy = false;
|
||||
private readonly status = new FormStatus((busy) => {
|
||||
this.submit.setText(busy ? 'Signing in…' : 'Sign in');
|
||||
this.submit.setEnabled(!busy);
|
||||
});
|
||||
|
||||
constructor(
|
||||
private readonly auth: AuthStore,
|
||||
@@ -64,8 +67,7 @@ export class LoginScreen extends VStack {
|
||||
});
|
||||
card.add(this.password);
|
||||
|
||||
this.message = new Label('', { style: 'warning' });
|
||||
card.add(this.message);
|
||||
card.add(this.status.message);
|
||||
this.submit = new Button('Sign in', {
|
||||
variant: 'primary',
|
||||
width: CARD_WIDTH - Theme.padding * 4,
|
||||
@@ -84,49 +86,28 @@ export class LoginScreen extends VStack {
|
||||
|
||||
/** What the check scripts read: the message under the form and whether it is waiting. */
|
||||
info(): { message: string; busy: boolean } {
|
||||
return { message: this.message.text, busy: this.busy };
|
||||
return this.status.info();
|
||||
}
|
||||
|
||||
private async signIn(): Promise<void> {
|
||||
if (this.busy) return;
|
||||
if (this.status.busy) return;
|
||||
const username = this.user.value.trim();
|
||||
const password = this.password.value;
|
||||
const problem = loginProblem(username, password);
|
||||
if (problem) {
|
||||
this.say(problem);
|
||||
this.refocus(username === '' ? this.user : this.password);
|
||||
this.status.say(problem);
|
||||
refocus(username === '' ? this.user : this.password);
|
||||
return;
|
||||
}
|
||||
this.setBusy(true);
|
||||
this.status.setBusy(true);
|
||||
try {
|
||||
const user = await this.auth.logIn(username, password);
|
||||
this.onSignedIn(user);
|
||||
} catch (error) {
|
||||
this.say(loginError(error, error instanceof WrongCredentialsError));
|
||||
this.setBusy(false);
|
||||
this.status.say(loginError(error, error instanceof WrongCredentialsError));
|
||||
this.status.setBusy(false);
|
||||
// A second attempt starts where the first one went wrong, not in an empty form.
|
||||
this.refocus(this.password);
|
||||
refocus(this.password);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the caret back into a field after a refused attempt. A field blurs itself once its Enter
|
||||
* has been handled, and that happens after this call returns – so the focus has to be set again
|
||||
* behind it, or the next thing typed goes nowhere.
|
||||
*/
|
||||
private refocus(field: TextField): void {
|
||||
queueMicrotask(() => ui().focus.focus(field));
|
||||
}
|
||||
|
||||
private setBusy(busy: boolean): void {
|
||||
this.busy = busy;
|
||||
this.submit.setText(busy ? 'Signing in…' : 'Sign in');
|
||||
this.submit.setEnabled(!busy);
|
||||
requestRender();
|
||||
}
|
||||
|
||||
private say(message: string): void {
|
||||
this.message.setText(message);
|
||||
requestRender();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { WrongCredentialsError } from '../../core/store/AuthStore';
|
||||
import { Button, HStack, Label, requestRender, Spacer, TextField, Theme, ui } from '../../ui';
|
||||
import { FormDialog } from '../FormDialog';
|
||||
import { Button, Label, TextField, ui } from '../../ui';
|
||||
import { dialogButtons, FormDialog } from '../FormDialog';
|
||||
import { FormStatus, refocus } from './FormStatus';
|
||||
import { MIN_PASSWORD_LENGTH, passwordError, passwordProblem } from './loginRules';
|
||||
|
||||
const FIELD_WIDTH = 200;
|
||||
@@ -16,9 +17,11 @@ export class PasswordDialog extends FormDialog {
|
||||
private readonly current: TextField;
|
||||
private readonly next: TextField;
|
||||
private readonly repeat: TextField;
|
||||
private readonly message: Label;
|
||||
private readonly confirm: Button;
|
||||
private busy = false;
|
||||
private readonly status = new FormStatus((busy) => {
|
||||
this.confirm.setEnabled(!busy);
|
||||
this.refreshLayout();
|
||||
});
|
||||
|
||||
constructor(
|
||||
private readonly change: (current: string, next: string) => Promise<void>,
|
||||
@@ -38,17 +41,10 @@ export class PasswordDialog extends FormDialog {
|
||||
this.content.add(this.formRow(this.fieldLabel('New'), this.next));
|
||||
this.content.add(this.formRow(this.fieldLabel('Repeat'), this.repeat));
|
||||
|
||||
this.message = new Label('', { style: 'warning' });
|
||||
this.content.add(this.message);
|
||||
this.content.add(this.status.message);
|
||||
|
||||
const buttons = new HStack({ gap: Theme.gap, align: 'center' });
|
||||
buttons.add(new Spacer(), 1);
|
||||
const cancel = new Button('Cancel');
|
||||
cancel.onClick = this.closeThen();
|
||||
buttons.add(cancel);
|
||||
this.confirm = new Button('Change password', { variant: 'primary', onClick: () => void this.submit() });
|
||||
buttons.add(this.confirm);
|
||||
this.content.add(buttons);
|
||||
this.content.add(dialogButtons(this.closeThen(), this.confirm));
|
||||
}
|
||||
|
||||
/** Puts the caret in the first field, so a keyboard alone can go through the form. */
|
||||
@@ -58,7 +54,7 @@ export class PasswordDialog extends FormDialog {
|
||||
|
||||
/** What the check scripts read: the message under the form and whether it is waiting. */
|
||||
info(): { message: string; busy: boolean } {
|
||||
return { message: this.message.text, busy: this.busy };
|
||||
return this.status.info();
|
||||
}
|
||||
|
||||
private secretField(onSubmit: () => void): TextField {
|
||||
@@ -66,44 +62,24 @@ export class PasswordDialog extends FormDialog {
|
||||
}
|
||||
|
||||
private async submit(): Promise<void> {
|
||||
if (this.busy) return;
|
||||
if (this.status.busy) return;
|
||||
const current = this.current.value;
|
||||
const next = this.next.value;
|
||||
const problem = passwordProblem(current, next, this.repeat.value);
|
||||
if (problem) {
|
||||
this.say(problem);
|
||||
this.refocus(current === '' ? this.current : this.next);
|
||||
this.status.say(problem);
|
||||
refocus(current === '' ? this.current : this.next);
|
||||
return;
|
||||
}
|
||||
this.setBusy(true);
|
||||
this.status.setBusy(true);
|
||||
try {
|
||||
await this.change(current, next);
|
||||
this.close();
|
||||
this.onChanged();
|
||||
} catch (error) {
|
||||
this.say(passwordError(error, error instanceof WrongCredentialsError));
|
||||
this.setBusy(false);
|
||||
this.refocus(this.current);
|
||||
this.status.say(passwordError(error, error instanceof WrongCredentialsError));
|
||||
this.status.setBusy(false);
|
||||
refocus(this.current);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the caret back after a refused attempt. A field blurs itself once its Enter has been
|
||||
* handled, and that happens after this call returns (see `LoginScreen.refocus`).
|
||||
*/
|
||||
private refocus(field: TextField): void {
|
||||
queueMicrotask(() => ui().focus.focus(field));
|
||||
}
|
||||
|
||||
private setBusy(busy: boolean): void {
|
||||
this.busy = busy;
|
||||
this.confirm.setEnabled(!busy);
|
||||
requestRender();
|
||||
}
|
||||
|
||||
private say(message: string): void {
|
||||
this.message.setText(message);
|
||||
this.refreshLayout();
|
||||
requestRender();
|
||||
}
|
||||
}
|
||||
@@ -220,8 +220,6 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
},
|
||||
/** The "Open project" dialog (#183): the projects it lists and the row picked. */
|
||||
projectPicker: () => editor.projectPickerDialog()?.info() ?? null,
|
||||
/** Where to click a project's row in that dialog. */
|
||||
projectPickerRowAt: (id: string) => editor.projectPickerDialog()?.rowPosition(id) ?? null,
|
||||
/** The dividers left and right of the centre column – dragging them narrows the tab row. */
|
||||
panelSplitters: () => ({ left: centreOf(editor.leftSplitter), right: centreOf(editor.rightSplitter) }),
|
||||
/** The left column: asset panel on top, scene objects below, and the divider between them. */
|
||||
@@ -725,7 +723,6 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
shaderTime: (at: number | null) => shaderLibrary.freezeTime(at),
|
||||
/** The same for particle time, kept apart so one can be held without the other. */
|
||||
particleTime: (at: number | null) => particleLibrary.freezeTime(at),
|
||||
particleCompiles: () => particleLibrary.compiles,
|
||||
/** Every source's clock back to zero on the current (usually frozen) second. */
|
||||
particlesRestart: () => {
|
||||
viewport.restartParticles(particleLibrary.now());
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Graphics } from 'pixi.js';
|
||||
import { Button, HStack, Label, Theme, type Widget } from '../../ui';
|
||||
import { Button, drawHeaderBar, HStack, Label, Theme, type Widget } from '../../ui';
|
||||
|
||||
/** Past this a crumb cuts its name short, so a deep trail of long names still fits the tab. */
|
||||
const CRUMB_MAX_WIDTH = 160;
|
||||
@@ -56,9 +56,7 @@ export class Breadcrumb extends HStack {
|
||||
|
||||
override redraw(): void {
|
||||
super.redraw();
|
||||
this.bg.clear();
|
||||
this.bg.rect(0, 0, this.w, this.h).fill(Theme.colors.panelHeader);
|
||||
this.bg.rect(0, this.h - 1, this.w, 1).fill(Theme.colors.border);
|
||||
drawHeaderBar(this.bg, this.w, this.h);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -311,7 +311,7 @@ export function bezier(g: Graphics, x1: number, y1: number, x2: number, y2: numb
|
||||
* The same curve as dashes. Pixi has no dash style, and unlike a straight line a bezier cannot be
|
||||
* stepped by length, so it is sampled and the walked distance decides which steps are drawn.
|
||||
*/
|
||||
export function dashedBezier(
|
||||
function dashedBezier(
|
||||
g: Graphics,
|
||||
x1: number,
|
||||
y1: number,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Graphics } from 'pixi.js';
|
||||
import { Button, Dropdown, HStack, Label, Spacer, Theme, type Widget } from '../../ui';
|
||||
import { Button, drawHeaderBar, Dropdown, HStack, Label, Spacer, Theme, type Widget } from '../../ui';
|
||||
import type { EditorState, SaveStatus } from '../state';
|
||||
import { TOOLBAR_ICON_SVG } from './toolbarIcons';
|
||||
|
||||
@@ -38,9 +38,9 @@ function iconButton(name: IconName, onClick: () => void): Button {
|
||||
|
||||
/**
|
||||
* Top bar (#183): menu, undo and redo on the left, play and pause in the middle, the account on
|
||||
* the right – icons, no captions. What it used to spell out lives in the menu now: the project
|
||||
* name and the start-scene switch in Project settings, the project list in Open project, and the
|
||||
* scene is picked where the scenes are, in the Assets panel.
|
||||
* the right – icons, no captions. Everything with words is behind the menu: the project name and
|
||||
* the start-scene switch in Project settings, the project list in Open project. The scene is
|
||||
* picked where the scenes are, in the Assets panel.
|
||||
*/
|
||||
export class Toolbar extends HStack {
|
||||
private readonly bg = new Graphics();
|
||||
@@ -80,8 +80,8 @@ export class Toolbar extends HStack {
|
||||
this.add(this.pauseButton);
|
||||
this.add(new Spacer(), 1);
|
||||
|
||||
// Flexible, not a fixed strip: it is the one thing here that may shrink, and since the account
|
||||
// button (#177) the row no longer fits a narrow window otherwise.
|
||||
// Flexible, not a fixed strip: it is the one thing here that may shrink, and with the account
|
||||
// button (#177) the row does not fit a narrow window otherwise.
|
||||
this.notice = new Label('', { style: 'small', align: 'right' });
|
||||
this.add(this.notice, 1);
|
||||
this.status = new Label('', { style: 'small', align: 'right' });
|
||||
@@ -113,7 +113,7 @@ export class Toolbar extends HStack {
|
||||
};
|
||||
}
|
||||
|
||||
/** The five icon buttons, so a check can click what no longer carries a caption. */
|
||||
/** The five icon buttons, so a check can click what carries no caption. */
|
||||
get controls(): Record<'menu' | 'undo' | 'redo' | 'play' | 'pause', Button> {
|
||||
return {
|
||||
menu: this.menuButton,
|
||||
@@ -167,8 +167,6 @@ export class Toolbar extends HStack {
|
||||
}
|
||||
|
||||
override redraw(): void {
|
||||
this.bg.clear();
|
||||
this.bg.rect(0, 0, this.w, this.h).fill(Theme.colors.panelHeader);
|
||||
this.bg.rect(0, this.h - 1, this.w, 1).fill(Theme.colors.border);
|
||||
drawHeaderBar(this.bg, this.w, this.h);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
* corner there, double-click a corner to take it away. Pure, so the rules are testable without a view.
|
||||
*/
|
||||
import type { Vec2 } from '../core/model/Properties';
|
||||
import { closestOnSegment } from '../core/util/vec';
|
||||
|
||||
/** What lies under the pointer: a corner, or the edge from corner `index` to the next one. */
|
||||
export type PolygonGrip = { kind: 'corner'; index: number } | { kind: 'edge'; index: number; at: Vec2 };
|
||||
@@ -11,13 +12,6 @@ export type PolygonGrip = { kind: 'corner'; index: number } | { kind: 'edge'; in
|
||||
/** Fewer corners than this are no polygon; the last three cannot be taken away. */
|
||||
export const MIN_CORNERS = 3;
|
||||
|
||||
function closestOnSegment(p: Vec2, a: Vec2, b: Vec2): Vec2 {
|
||||
const ab = { x: b.x - a.x, y: b.y - a.y };
|
||||
const length = ab.x * ab.x + ab.y * ab.y;
|
||||
const t = length > 0 ? Math.min(1, Math.max(0, ((p.x - a.x) * ab.x + (p.y - a.y) * ab.y) / length)) : 0;
|
||||
return { x: a.x + ab.x * t, y: a.y + ab.y * t };
|
||||
}
|
||||
|
||||
/**
|
||||
* The corner or edge within `tolerance` of `p`, the nearest one; a corner wins over an edge, since
|
||||
* every corner lies on two edges. An outline of one or two corners has no edge to press on yet.
|
||||
|
||||
Reference in new issue
Block a user