Cancel gestures when their visible ancestor coordinate context changes
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import type {Document,Geometry,Id,Registry} from './types.ts';
|
||||
import {fault} from './values.ts';
|
||||
import {unitScale} from './units.ts';
|
||||
|
||||
type PlacementStep={canvasId:Id;parentCanvasId:Id;entryId:Id;incarnation:number;componentId:string;dataVersion:number;geometry:Geometry;contentScale:number;unitScale:number};
|
||||
|
||||
/** A short-lived reader of the visible owner chain relative to one view, not the project root.
|
||||
* Cache by owner only during this admission/publication, so multi-selection shares ancestry work. */
|
||||
export function createGesturePlacementReader(document:Document,viewCanvasId:Id,registry:Registry){
|
||||
const values=new Map<Id,readonly PlacementStep[]>();
|
||||
return(ownerCanvasId:Id):readonly PlacementStep[]=>{
|
||||
const cached=values.get(ownerCanvasId);if(cached)return cached;
|
||||
if(!document.canvases.has(viewCanvasId))throw fault('canvas_missing',[viewCanvasId]);
|
||||
const chain:PlacementStep[]=[],seen=new Set<Id>();let id=ownerCanvasId;
|
||||
while(id!==viewCanvasId){
|
||||
if(seen.has(id))throw fault('canvas_cycle',[id]);seen.add(id);
|
||||
const parent=document.canvases.get(id)?.parent;
|
||||
if(!parent)throw fault('target_outside_view',[ownerCanvasId]);
|
||||
const entry=document.elements.get(parent.entryElementId);
|
||||
// A future/unknown entry is a collapsed placeholder; never interpret its payload.
|
||||
if(!entry||entry.structure!=='node'||registry.resolve(entry)!=='known')throw fault('target_hidden',[ownerCanvasId]);
|
||||
const raw=entry.data.presentation,presentation=raw&&typeof raw==='object'&&!Array.isArray(raw)?raw:{};
|
||||
if(presentation.collapsed===true)throw fault('target_hidden',[ownerCanvasId]);
|
||||
const contentScale=typeof presentation.contentScale==='number'&&presentation.contentScale>0?presentation.contentScale:1;
|
||||
chain.push({canvasId:id,parentCanvasId:parent.canvasId,entryId:entry.id,incarnation:entry.incarnation,componentId:entry.componentId,dataVersion:entry.dataVersion,geometry:entry.geometry,contentScale,unitScale:unitScale(raw)});
|
||||
id=parent.canvasId;
|
||||
}
|
||||
values.set(ownerCanvasId,chain);return chain;
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Editor } from './editor.ts';
|
||||
import type { Availability, Capability, ElementRecord, Geometry, Id, Json, Result, Target } from './types.ts';
|
||||
import { geometryValid, targetElement } from './document.ts';
|
||||
import {createGesturePlacementReader} from './gesture-placement.ts';
|
||||
import { clone, equal, fault, freeze, readMap, readSet, reject, validJson } from './values.ts';
|
||||
|
||||
export type ViewState = { readonly id: Id; readonly canvasId: Id; readonly selection: ReturnType<typeof readSet<Id>>; readonly viewport: { readonly x: number; readonly y: number; readonly zoom: number }; readonly tool: 'select' | 'pan'; readonly modes: ReadonlyMap<Id, string> };
|
||||
@@ -38,14 +39,21 @@ export function createSessions(editor: Editor) {
|
||||
if (canvasMissing || selection.length !== view.selection.size || modes.size !== view.modes.size) { views.set(id, freeze({ ...view, canvasId: canvasMissing ? [...document.canvases.values()].find(canvas => !canvas.parent)!.id : view.canvasId, selection: readSet(canvasMissing ? [] : selection), modes: readMap(canvasMissing ? new Map() : modes) })); changed = true; }
|
||||
}
|
||||
let cancelReason:string|undefined;
|
||||
if (snapshot.connectionGesture?.phase === 'active') {
|
||||
const active=snapshot.gesture?.phase==='active'?snapshot.gesture:snapshot.connectionGesture?.phase==='active'?snapshot.connectionGesture:undefined;
|
||||
const activeView=active?snapshot.views.get(active.viewId):undefined;
|
||||
if(activeView&&!document.canvases.has(activeView.canvasId))cancelReason='canvas_changed';
|
||||
const oldPlacement=activeView?createGesturePlacementReader(beforeDocument,activeView.canvasId,editor.registry):undefined,newPlacement=activeView?createGesturePlacementReader(document,activeView.canvasId,editor.registry):undefined;
|
||||
const placementChanged=(owner:Id)=>{if(beforeDocument===document)return false;try{return !oldPlacement||!newPlacement||!equal(oldPlacement(owner),newPlacement(owner));}catch{return true;}};
|
||||
if (!cancelReason&&snapshot.connectionGesture?.phase === 'active') {
|
||||
const current = document.elements.get(snapshot.connectionGesture.target.id), before = connectionBaseline;
|
||||
if (!current || current.structure !== 'connection' || before?.structure !== 'connection' || current.incarnation !== before.incarnation || current.ownerCanvasId !== before.ownerCanvasId || !equal(current.path, before.path)) cancelReason='target_changed';
|
||||
if (!current || current.structure !== 'connection' || before?.structure !== 'connection' || current.incarnation !== before.incarnation || current.ownerCanvasId !== before.ownerCanvasId || current.componentId!==before.componentId || current.dataVersion!==before.dataVersion || !equal(current.path, before.path)) cancelReason='target_changed';
|
||||
else if(placementChanged(current.ownerCanvasId))cancelReason='coordinate_context_changed';
|
||||
else if (!editor.can({ type: 'connection.path', target: snapshot.connectionGesture.target, path: current.path }).enabled) cancelReason='permission_changed';
|
||||
}
|
||||
if (snapshot.gesture?.phase === 'active'&&!cancelReason) for (const target of snapshot.gesture.targets) {
|
||||
const before = baseline.get(target.id), current = document.elements.get(target.id);
|
||||
if (!current || current.structure !== 'node' || before?.structure !== 'node' || current.incarnation !== before.incarnation || current.ownerCanvasId !== before.ownerCanvasId || !equal(current.geometry, before.geometry)) { cancelReason='target_changed'; break; }
|
||||
if (!current || current.structure !== 'node' || before?.structure !== 'node' || current.incarnation !== before.incarnation || current.ownerCanvasId !== before.ownerCanvasId || current.componentId!==before.componentId || current.dataVersion!==before.dataVersion || !equal(current.geometry, before.geometry)) { cancelReason='target_changed'; break; }
|
||||
if(placementChanged(current.ownerCanvasId)){cancelReason='coordinate_context_changed';break;}
|
||||
if (!editor.can({ type: 'element.geometry', target, patch: {} }).enabled) { cancelReason='permission_changed'; break; }
|
||||
}
|
||||
if(changed||cancelReason){
|
||||
@@ -110,8 +118,10 @@ export function createSessions(editor: Editor) {
|
||||
try {
|
||||
if(view(viewId).tool!=='select')throw fault('tool_not_select');
|
||||
if (snapshot.gesture || snapshot.connectionGesture || !targets.length || new Set(targets.map(target => target.id)).size !== targets.length) throw fault('gesture_busy');
|
||||
baseline = new Map(targets.map(target => { const element = targetElement(editor.getSnapshot().document, target); if (element.structure !== 'node' || editor.registry.resolve(element) !== 'known') throw fault('unsupported_component', [target.id]); return [target.id, element]; }));
|
||||
for (const target of targets) { const allowed = editor.can({ type: 'element.geometry', target, patch: {} }); if (!allowed.enabled) throw fault(allowed.code, allowed.targets); }
|
||||
const placement=createGesturePlacementReader(editor.getSnapshot().document,view(viewId).canvasId,editor.registry);
|
||||
const plannedBaseline = new Map(targets.map(target => { const element = targetElement(editor.getSnapshot().document, target); if (element.structure !== 'node' || editor.registry.resolve(element) !== 'known') throw fault('unsupported_component', [target.id]); return [target.id, element]; }));
|
||||
for (const target of targets) { placement(plannedBaseline.get(target.id)!.ownerCanvasId);const allowed = editor.can({ type: 'element.geometry', target, patch: {} }); if (!allowed.enabled) throw fault(allowed.code, allowed.targets); }
|
||||
baseline=plannedBaseline;
|
||||
snapshot = Object.freeze({ ...snapshot, lastCancelReason: null, gesture: freeze({ token: ++nextGestureToken, viewId, targets: targets.map(target => ({ ...target })), preview: readMap(new Map<Id, Geometry>()), phase: 'active' as const }) }); notify();
|
||||
return { enabled: true };
|
||||
} catch (error) { const result = reject(error); return { enabled: false, code: result.code, targets: result.targets }; }
|
||||
@@ -122,6 +132,7 @@ export function createSessions(editor: Editor) {
|
||||
if (snapshot.gesture || snapshot.connectionGesture) throw fault('gesture_busy');
|
||||
const element = targetElement(editor.getSnapshot().document, target);
|
||||
if (element.structure !== 'connection') throw fault('unsupported_capability', [target.id]);
|
||||
createGesturePlacementReader(editor.getSnapshot().document,view(viewId).canvasId,editor.registry)(element.ownerCanvasId);
|
||||
const allowed = editor.can({ type: 'connection.path', target, path: element.path });
|
||||
if (!allowed.enabled) throw fault(allowed.code, allowed.targets);
|
||||
connectionBaseline = element;
|
||||
|
||||
Reference in New Issue
Block a user