feat(editor): F4.6a simulation mode and a computer's desktop
Java's mode buttons start and stop the room, now in a worker of the editor's page (LocalSimHost), with Java's speed slider. While the simulation runs the design is locked as in Java: no palette, no sheet, nothing moves, no undo. Cables that carry a frame light up in Java's green, and a tap on a computer opens its desktop: Java's title, the software installation first, then the installed programs in the order of Java's list for the language, a taskbar with the network icon and the network as the simulation runs it. The room sends that over a new topic, node:<id>. Desktops stay over a return to the design and show again at the next start. The programs' windows follow from F4.6b. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -56,8 +56,19 @@ export interface CanvasProps {
|
||||
readonly onMenu?: (target: Selection, at: Point) => void;
|
||||
/** A cable drawn active, as Java draws the cable of the card whose tab is open (Kabel.setAktiv). */
|
||||
readonly highlightedLink?: string | null;
|
||||
/**
|
||||
* The simulation: a tap on a device activates it (a computer's desktop),
|
||||
* a long press opens its menu; nothing moves, nothing is selected, as in
|
||||
* Java's simulation mode.
|
||||
*/
|
||||
readonly mode?: 'design' | 'simulation';
|
||||
/** The cables that carry a frame now, drawn as Java draws them then (JCablePanel.farbeBlinken). */
|
||||
readonly activeLinks?: ReadonlySet<string>;
|
||||
readonly onActivate?: (id: string) => void;
|
||||
}
|
||||
|
||||
const NO_LINKS: ReadonlySet<string> = new Set();
|
||||
|
||||
/** The label's width in Java's font, measured once per text. */
|
||||
function useTextWidth(): (text: string) => number {
|
||||
const cache = useRef(new Map<string, number>());
|
||||
@@ -141,7 +152,7 @@ type Gesture =
|
||||
| { readonly kind: 'link'; readonly id: string; readonly start: Point; moved: boolean; longPressed: boolean };
|
||||
|
||||
export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
{ view, tool = 'select', selection = null, cableFrom = null, onSelect, onMoveDevice, onMoveEnd, onCableStart, onCable, onMenu, highlightedLink = null },
|
||||
{ view, tool = 'select', selection = null, cableFrom = null, onSelect, onMoveDevice, onMoveEnd, onCableStart, onCable, onMenu, highlightedLink = null, mode = 'design', activeLinks = NO_LINKS, onActivate },
|
||||
handle,
|
||||
) {
|
||||
const t = useTranslate();
|
||||
@@ -222,6 +233,7 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
[kinds, t],
|
||||
);
|
||||
|
||||
const simulating = mode === 'simulation';
|
||||
const pointers = useRef(new Map<number, Point>());
|
||||
const gesture = useRef<Gesture | null>(null);
|
||||
const longPress = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -258,7 +270,7 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
now.longPressed = true;
|
||||
onMenu?.({ kind: 'device', id: now.id }, now.start);
|
||||
}, LONG_PRESS_MILLISECONDS);
|
||||
} else if (linkId !== undefined && linkId !== null) {
|
||||
} else if (linkId !== undefined && linkId !== null && !simulating) {
|
||||
gesture.current = { kind: 'link', id: linkId, start: point, moved: false, longPressed: false };
|
||||
longPress.current = setTimeout(() => {
|
||||
const now = gesture.current;
|
||||
@@ -302,7 +314,7 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
const step = { x: point.x - now.last.x, y: point.y - now.last.y };
|
||||
now.last = point;
|
||||
setViewport((before) => shifted(before ?? currentRef.current, step));
|
||||
} else if (now.kind === 'device' && !now.longPressed) {
|
||||
} else if (now.kind === 'device' && !now.longPressed && !simulating) {
|
||||
if (tool === 'cable') {
|
||||
const from = boxes.get(now.id)!.center;
|
||||
const target = deviceAt(point);
|
||||
@@ -328,7 +340,7 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
return;
|
||||
}
|
||||
if (now.kind === 'background') {
|
||||
if (!now.moved) {
|
||||
if (!now.moved && !simulating) {
|
||||
onSelect?.(null);
|
||||
if (cableFrom !== null) onCableStart?.(null);
|
||||
}
|
||||
@@ -339,6 +351,10 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
return;
|
||||
}
|
||||
if (now.longPressed) return;
|
||||
if (simulating) {
|
||||
if (!now.moved) onActivate?.(now.id);
|
||||
return;
|
||||
}
|
||||
if (tool === 'cable') {
|
||||
setBand(null);
|
||||
const from = { id: now.id, side: now.side };
|
||||
@@ -399,6 +415,7 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
ref={surface}
|
||||
className="filius-canvas"
|
||||
data-tool={tool}
|
||||
data-mode={mode}
|
||||
role="application"
|
||||
aria-label={t('canvas.label')}
|
||||
style={{ ...style, cursor: tool === 'cable' ? 'crosshair' : 'default' }}
|
||||
@@ -421,7 +438,7 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
if (source === undefined || target === undefined) return null;
|
||||
const d = cablePath(source.center, target.center);
|
||||
const selected = selection?.kind === 'link' && selection.id === link.id;
|
||||
const active = highlightedLink === link.id;
|
||||
const active = simulating ? activeLinks.has(link.id) : highlightedLink === link.id;
|
||||
return (
|
||||
<g key={link.id}>
|
||||
<path
|
||||
@@ -463,7 +480,7 @@ export const Canvas = forwardRef<CanvasHandle, CanvasProps>(function Canvas(
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
{menuButton === null || selection === null ? null : (
|
||||
{menuButton === null || selection === null || simulating ? null : (
|
||||
<button
|
||||
type="button"
|
||||
className="device-menu-button"
|
||||
|
||||
Reference in New Issue
Block a user