orcasvn-react-diagrams 0.2.8 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -269,6 +269,8 @@ export type ElementData = {
269
269
  style?: Record<string, unknown>;
270
270
  portIds?: string[];
271
271
  textIds?: string[];
272
+ visible?: boolean;
273
+ selectable?: boolean;
272
274
  parentId?: string | null;
273
275
  moveMode?: MoveConstraint;
274
276
  anchorCenter?: boolean;
@@ -0,0 +1,4 @@
1
+ import type { DiagramState } from '../../api';
2
+ import type { DemoConfig } from '../types';
3
+ export declare const createElementVisibilitySelectionState: () => DiagramState;
4
+ export declare const elementVisibilitySelectionDemoConfig: DemoConfig;
@@ -76,6 +76,11 @@ export default class DiagramEngine {
76
76
  setSelection(ids: string[]): void;
77
77
  toggleSelection(id: string): void;
78
78
  getSelection(): string[];
79
+ isElementVisible(id: string): boolean;
80
+ isElementSelectable(id: string): boolean;
81
+ isPortVisible(id: string): boolean;
82
+ isLinkVisible(id: string): boolean;
83
+ isTextVisible(id: string): boolean;
79
84
  getElementWorldPosition(id: string): Point | null;
80
85
  getElementRotation(id: string): number;
81
86
  normalizeElementResize(id: string, proposal: {
@@ -146,6 +151,9 @@ export default class DiagramEngine {
146
151
  private resolveTextPresentation;
147
152
  private resolveAllTextPresentationPatches;
148
153
  private emitSelection;
154
+ private normalizeSelectionIds;
155
+ private isSelectableId;
156
+ private syncSelectionToPolicies;
149
157
  private applyLayoutForParent;
150
158
  private applyLayoutCascade;
151
159
  private applyAllLayouts;
@@ -13,14 +13,19 @@ export default class DiagramModel {
13
13
  load(state: DiagramState): void;
14
14
  toState(): DiagramState;
15
15
  getElement(id: string): ElementModel | undefined;
16
+ isElementVisible(id: string): boolean;
17
+ isElementSelectable(id: string): boolean;
16
18
  setElementLayout(id: string, layout: ElementData['layout']): void;
17
19
  getChildren(parentId: string): ElementModel[];
18
20
  getElementWorldPosition(id: string): Point | null;
19
21
  getPort(id: string): PortModel | undefined;
22
+ isPortVisible(id: string): boolean;
20
23
  getPortWorldPosition(id: string): Point | null;
21
24
  getLink(id: string): LinkModel | undefined;
25
+ isLinkVisible(id: string): boolean;
22
26
  getLinkMidpoint(id: string): Point | null;
23
27
  getText(id: string): TextModel | undefined;
28
+ isTextVisible(id: string): boolean;
24
29
  getTextWorldPosition(id: string): Point | null;
25
30
  addElement(data: ElementData): ElementModel;
26
31
  moveElement(id: string, position: Point): void;
@@ -7,6 +7,8 @@ export default class ElementModel {
7
7
  style?: Record<string, unknown>;
8
8
  portIds: string[];
9
9
  textIds: string[];
10
+ visible?: boolean;
11
+ selectable?: boolean;
10
12
  parentId: string | null;
11
13
  moveMode?: MoveConstraint;
12
14
  anchorCenter?: boolean;
@@ -242,6 +242,8 @@ type ElementData = {
242
242
  style?: Record<string, unknown>;
243
243
  portIds?: string[];
244
244
  textIds?: string[];
245
+ visible?: boolean;
246
+ selectable?: boolean;
245
247
  parentId?: string | null;
246
248
  moveMode?: MoveConstraint;
247
249
  anchorCenter?: boolean;
package/dist/index.d.ts CHANGED
@@ -269,6 +269,8 @@ type ElementData = {
269
269
  style?: Record<string, unknown>;
270
270
  portIds?: string[];
271
271
  textIds?: string[];
272
+ visible?: boolean;
273
+ selectable?: boolean;
272
274
  parentId?: string | null;
273
275
  moveMode?: MoveConstraint;
274
276
  anchorCenter?: boolean;
@@ -543,6 +545,8 @@ declare class ElementModel {
543
545
  style?: Record<string, unknown>;
544
546
  portIds: string[];
545
547
  textIds: string[];
548
+ visible?: boolean;
549
+ selectable?: boolean;
546
550
  parentId: string | null;
547
551
  moveMode?: MoveConstraint;
548
552
  anchorCenter?: boolean;
@@ -640,14 +644,19 @@ declare class DiagramModel {
640
644
  load(state: DiagramState): void;
641
645
  toState(): DiagramState;
642
646
  getElement(id: string): ElementModel | undefined;
647
+ isElementVisible(id: string): boolean;
648
+ isElementSelectable(id: string): boolean;
643
649
  setElementLayout(id: string, layout: ElementData['layout']): void;
644
650
  getChildren(parentId: string): ElementModel[];
645
651
  getElementWorldPosition(id: string): Point | null;
646
652
  getPort(id: string): PortModel | undefined;
653
+ isPortVisible(id: string): boolean;
647
654
  getPortWorldPosition(id: string): Point | null;
648
655
  getLink(id: string): LinkModel | undefined;
656
+ isLinkVisible(id: string): boolean;
649
657
  getLinkMidpoint(id: string): Point | null;
650
658
  getText(id: string): TextModel | undefined;
659
+ isTextVisible(id: string): boolean;
651
660
  getTextWorldPosition(id: string): Point | null;
652
661
  addElement(data: ElementData): ElementModel;
653
662
  moveElement(id: string, position: Point): void;
@@ -62,12 +62,22 @@ Defaults:
62
62
 
63
63
  ### `ElementData`
64
64
  - Required: `id`, `position`, `size`, `shapeId`
65
- - Optional: `style`, `portIds`, `textIds`, `parentId`, `moveMode`, `anchorCenter`, `layout`, `childElementInteraction`, `portMovement`
65
+ - Optional: `style`, `portIds`, `textIds`, `visible`, `selectable`, `parentId`, `moveMode`, `anchorCenter`, `layout`, `childElementInteraction`, `portMovement`
66
66
  - Defaults at runtime/model:
67
67
  - `portIds`: `[]`
68
68
  - `textIds`: `[]`
69
69
  - `parentId`: `null`
70
70
 
71
+ ### `ElementData.visible` and `ElementData.selectable`
72
+ - Optional `visible?: boolean` (default behavior: `true`)
73
+ - Optional `selectable?: boolean` (default behavior: `true`)
74
+ - Semantics:
75
+ - `visible: false` removes the element body from built-in render, hit testing, marquee selection, persisted selection, `zoomToFitElements`, and `exportImage({ fitToContent })`
76
+ - hidden-scene propagation also hides owned ports, owned texts, and links whose source/target ports belong to hidden elements
77
+ - `selectable: false` prevents built-in element-body click selection, marquee selection, and programmatic `setSelection(...)` / `toggleSelection(...)` from retaining that element id
78
+ - `selectable: false` does not hide the element
79
+ - visible ports on a visible-but-unselectable element remain interactable/selectable unless separately constrained
80
+
71
81
  ### `ElementLayout`
72
82
  - Required: `mode: 'manual' | 'horizontal' | 'vertical' | 'grid'`
73
83
  - Optional: `padding?: number | { x: number; y: number }`, `gap?: number`, `align?: 'start' | 'center' | 'end'`
@@ -261,7 +271,9 @@ Defaults:
261
271
  - Missing IDs in most mutators are no-op (non-throwing).
262
272
  - `setViewport` emits a `change` patch with `entity: 'viewport'` and skips immediate render scheduling.
263
273
  - `zoomToFitElements` computes bounds from `elements[]` only, ignores ports/links/texts for fit expansion, and no-ops when no elements exist.
274
+ - `zoomToFitElements` ignores hidden elements when computing fit bounds.
264
275
  - `zoomToFitElements` applies its result through `setViewport`, so hosts observe the standard viewport patch/change path.
276
+ - `exportImage({ fitToContent })` derives crop bounds from visible scene entities only.
265
277
  - Built-in empty-paper panning now starts from plain primary-button drag and no longer requires `panKey`; marquee selection uses `Shift + primary drag`.
266
278
  - `clientToWorld` uses: `world = (client - containerRect - pan) / zoom`, with zoom fallback to `1` when zoom is `0`.
267
279
  - `rerouteLinks(ids)` skips manual links unless `options.includeManual === true`.
@@ -269,6 +281,9 @@ Defaults:
269
281
  - `updateLinkPoints` always marks the link as manual routing.
270
282
  - `linkColorPoolPolicy` is opt-in and applies only to newly created links; explicitly provided non-empty `style.stroke` values are preserved.
271
283
  - `gridLayoutChanged` is additive and does not replace standard `change` / `elementResized` flows.
284
+ - `ElementData.visible = false` hides the element plus owned ports/texts and endpoint-dependent links from built-in renderer sync, hit testing, marquee selection, persisted selection, and fit/export helpers.
285
+ - `ElementData.selectable = false` blocks built-in element-body selection while still allowing visible owned ports to participate in their normal interaction flows.
286
+ - `setSelection(...)` and `toggleSelection(...)` normalize away unknown IDs, hidden scene members, and unselectable elements before emitting `selection`.
272
287
  - `ElementData.childElementInteraction.movable = false` suppresses built-in drag only for direct child elements; programmatic `moveElementTo(...)` and layout/ancestor-driven repositioning remain allowed.
273
288
  - `TextData.interaction.movable = false` blocks built-in drag only; selection still works and programmatic movement remains allowed.
274
289
  - `TextData.interaction.editable = false` blocks built-in textarea editing only; selection still works and programmatic text updates remain allowed.
@@ -17,6 +17,7 @@
17
17
  - Auto-layout parent resize policy (`grow` / `grow-shrink`) and child size constraints.
18
18
  - Optional grid-child width topology editing with `gridLayoutChanged` host events.
19
19
  - Parent-owned direct-child drag suppression through `ElementData.childElementInteraction`.
20
+ - Element-level visibility/selectability policy through `ElementData.visible` and `ElementData.selectable`, including hidden-scene propagation to owned ports/texts and endpoint-dependent links.
20
21
  - Element shape hover controls with edge/vertex/midpoint targets and interaction callbacks.
21
22
  - Optional random link color assignment from configurable pools during link creation.
22
23
  - Per-text interaction policy for suppressing built-in text drag and/or built-in text editing.
@@ -38,10 +38,11 @@
38
38
  - Plus `textDeleted` on removal (including cascade removal)
39
39
  - `setSelection/toggleSelection/deleteSelection`
40
40
  - `selection` plus derived selected entity events
41
+ - hidden scene members and `selectable:false` element IDs are filtered out before selection persists/emits
41
42
  - `setViewport`
42
43
  - `change` with viewport patch (`render: false`)
43
44
  - `zoomToFitElements`
44
- - delegates to `setViewport` when at least one element exists
45
+ - delegates to `setViewport` when at least one visible element exists
45
46
  - emits the same viewport `change` patch path
46
47
  - `setRouting/setSnapping/registerShape`
47
48
  - `config` events
@@ -64,6 +65,7 @@
64
65
  ## Failure Modes
65
66
  - Unknown IDs do not emit entity-specific movement/selection events.
66
67
  - Canceled link creation emits `elementLinkEnded` with `cancelled=true` and no link creation.
68
+ - Built-in element-body selection emits no persisted element selection when the target element is hidden or sets `selectable = false`.
67
69
  - Built-in drag on a direct child element emits no move/drop mutation when its parent sets `childElementInteraction.movable = false`.
68
70
  - `textUpdated` emits only when target text exists; missing IDs remain no-op.
69
71
 
@@ -59,7 +59,7 @@ When resuming later:
59
59
  5. Re-run packaging verification.
60
60
 
61
61
  ## 6. Current Status
62
- - Last updated: 2026-05-24
62
+ - Last updated: 2026-05-27
63
63
  - Last completed step: 10
64
64
  - Next step: 7 (optional additional fixture coverage for deeper nested/manual-route scenarios)
65
65
  - Owner: Codex (with repository maintainers)
@@ -87,3 +87,4 @@ When resuming later:
87
87
  - 2026-05-14: Completed release-doc pass for `v0.2.6`; refreshed release highlights, API contract updates (`zoomToFitElements`, `ViewportFitOptions`, `gridChildWidthResizeEnabled`, `gridLayoutChanged`, `TextInteractionPolicy`), and machine-readable contract metadata.
88
88
  - 2026-05-18: Completed release-doc pass for `v0.2.7`; refreshed release highlights, API contract updates (`ElementChildInteractionPolicy`, `childElementInteraction`, and built-in pan gesture behavior), and machine-readable contract metadata.
89
89
  - 2026-05-24: Completed release-doc pass for `v0.2.8`; refreshed release highlights and publish-facing workflow metadata for the nested grid slot-preservation fix.
90
+ - 2026-05-27: Completed release-doc pass for `v0.2.9`; refreshed release highlights, visibility/selectability API docs, state/event integration notes, and machine-readable contract metadata.
@@ -19,6 +19,7 @@ Embed this library into another diagram host while preserving deterministic stat
19
19
  6. Use `zoomToFitElements(options?)` when the host needs library-owned fit-to-elements navigation instead of recomputing viewport math externally.
20
20
  7. Set `TextData.interaction` when some labels must remain read-only or fixed in place while still using library selection behavior.
21
21
  8. Set `ElementData.childElementInteraction` on parent elements when direct child nodes should stay selectable but not be draggable through built-in interaction.
22
+ 9. Set `ElementData.visible` and `ElementData.selectable` when hosts need hidden-but-retained nodes or visible-but-nonselectable element bodies; hidden elements also suppress owned ports/texts and endpoint-dependent links from built-in view helpers.
22
23
 
23
24
  ## Path B: Engine-Only Adapter
24
25
  1. Implement `Renderer`:
@@ -8,6 +8,7 @@
8
8
  - `PortData`
9
9
  - `LinkData`
10
10
  - `TextData`
11
+ - Confirm host mapping for `ElementData.visible` / `ElementData.selectable` if hidden or non-selectable nodes must retain state.
11
12
  - Confirm owner-relative semantics for ports/texts.
12
13
 
13
14
  ## 2. Match Coordinate Semantics
@@ -12,6 +12,9 @@
12
12
  - Position is local to parent if `parentId` exists; otherwise world.
13
13
  - World position resolves by summing ancestor positions.
14
14
  - If `anchorCenter=true`, world top-left is shifted by half size.
15
+ - `visible` and `selectable` behave as enabled when omitted.
16
+ - `visible=false` excludes the element from the visible scene, and ancestor hidden state propagates to descendant elements.
17
+ - `selectable=false` prevents the element ID from persisting in engine selection state.
15
18
  - Deleting an element cascades to:
16
19
  - child ports
17
20
  - links connected through removed ports
@@ -21,6 +24,7 @@
21
24
  - Port position is local to owner element.
22
25
  - Default `anchorCenter=true` in model constructor.
23
26
  - Default `orientToHostBorder=true` in model constructor.
27
+ - Port visible-state is derived from owning element visibility.
24
28
  - Moving ports can be constrained by:
25
29
  - `moveMode: inside|border`
26
30
  - element-level `portMovement.moveMode: free|inside|border|anchors`
@@ -34,11 +38,13 @@
34
38
  - Auto routing recomputes from router strategy and normalized endpoints.
35
39
  - Manual routing preserves interior bends on endpoint movement.
36
40
  - Links with unresolved source/target world positions are skipped by reroute/update paths.
41
+ - Link visible-state requires both endpoint ports to remain visible.
37
42
 
38
43
  ## Text
39
44
  - `ownerId` can reference element, port, or link.
40
45
  - Owned text position is stored owner-relative.
41
46
  - Text with missing owner behaves as standalone world-position text.
47
+ - Owned text visible-state follows its owner visibility when the owner resolves to an element, port, or link.
42
48
  - Text interaction policy is persisted separately from style/layout metadata.
43
49
  - `interaction.movable = false` suppresses built-in drag without blocking selection or programmatic movement.
44
50
  - `interaction.editable = false` suppresses built-in editing without blocking selection or programmatic content updates.
@@ -49,8 +55,10 @@
49
55
  - `setSelection` emits:
50
56
  - `selection`
51
57
  - plus derived `elementSelected`/`portSelected`/`textSelected`
58
+ - `setSelection` and `toggleSelection` normalize IDs against current visibility/selectability rules before persisting or emitting selection.
52
59
  - `setViewport` emits `change` with `entity=viewport` and does not force render directly.
53
- - `zoomToFitElements` derives viewport fit from `elements[]` bounds only and applies the result through `setViewport`.
60
+ - `zoomToFitElements` derives viewport fit from visible `elements[]` bounds only and applies the result through `setViewport`.
61
+ - `exportImage({ fitToContent })` derives crop bounds from visible scene entities only.
54
62
 
55
63
  ## Failure Modes
56
64
  - Non-existent target IDs: operation is no-op.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orcasvn-react-diagrams",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "dependencies": {
5
5
  "eventemitter3": "^5.0.1",
6
6
  "flatten-js": "^0.6.9",
@@ -0,0 +1,128 @@
1
+ import type { DiagramState } from '../../api';
2
+ import type { DemoConfig } from '../types';
3
+ import { baseElementShapes, basePortShapes } from './shared';
4
+
5
+ export const createElementVisibilitySelectionState = (): DiagramState => ({
6
+ elements: [
7
+ {
8
+ id: 'policy-normal',
9
+ position: { x: 120, y: 160 },
10
+ size: { width: 180, height: 120 },
11
+ shapeId: 'default',
12
+ style: { fill: '#dbeafe', stroke: '#1d4ed8', strokeWidth: 2 },
13
+ },
14
+ {
15
+ id: 'policy-unselectable',
16
+ position: { x: 410, y: 160 },
17
+ size: { width: 180, height: 120 },
18
+ shapeId: 'default',
19
+ selectable: false,
20
+ style: { fill: '#e2e8f0', stroke: '#334155', strokeWidth: 2 },
21
+ },
22
+ {
23
+ id: 'policy-hidden',
24
+ position: { x: 700, y: 140 },
25
+ size: { width: 180, height: 120 },
26
+ shapeId: 'default',
27
+ visible: false,
28
+ style: { fill: '#fee2e2', stroke: '#dc2626', strokeWidth: 2 },
29
+ },
30
+ ],
31
+ ports: [
32
+ {
33
+ id: 'policy-normal-port',
34
+ elementId: 'policy-normal',
35
+ position: { x: 180, y: 60 },
36
+ shapeId: 'port-circle',
37
+ },
38
+ {
39
+ id: 'policy-unselectable-port',
40
+ elementId: 'policy-unselectable',
41
+ position: { x: 0, y: 60 },
42
+ shapeId: 'port-circle',
43
+ },
44
+ {
45
+ id: 'policy-hidden-port',
46
+ elementId: 'policy-hidden',
47
+ position: { x: 0, y: 60 },
48
+ shapeId: 'port-circle',
49
+ },
50
+ ],
51
+ links: [
52
+ {
53
+ id: 'policy-visible-link',
54
+ sourcePortId: 'policy-normal-port',
55
+ targetPortId: 'policy-unselectable-port',
56
+ points: [
57
+ { x: 300, y: 220 },
58
+ { x: 410, y: 220 },
59
+ ],
60
+ style: { stroke: '#2563eb', strokeWidth: 3 },
61
+ },
62
+ {
63
+ id: 'policy-hidden-link',
64
+ sourcePortId: 'policy-normal-port',
65
+ targetPortId: 'policy-hidden-port',
66
+ points: [
67
+ { x: 300, y: 220 },
68
+ { x: 700, y: 200 },
69
+ ],
70
+ style: { stroke: '#dc2626', strokeWidth: 3 },
71
+ },
72
+ ],
73
+ texts: [
74
+ {
75
+ id: 'policy-tip',
76
+ content:
77
+ 'Blue node is normal. Slate node is visible but its body ignores click and marquee. Its port stays interactive. Hidden node, its port, and its attached red link stay in state but are suppressed from scene, selection, zoom-to-fit, and fit-to-content export.',
78
+ position: { x: 80, y: 72 },
79
+ size: { width: 760, height: 44 },
80
+ style: { fontSize: 14, fill: '#0f172a' },
81
+ },
82
+ {
83
+ id: 'policy-normal-label',
84
+ content: 'Normal selectable element',
85
+ position: { x: 16, y: 14 },
86
+ ownerId: 'policy-normal',
87
+ },
88
+ {
89
+ id: 'policy-unselectable-label',
90
+ content: 'Visible but body is unselectable',
91
+ position: { x: 16, y: 14 },
92
+ ownerId: 'policy-unselectable',
93
+ },
94
+ {
95
+ id: 'policy-hidden-label',
96
+ content: 'Hidden in state',
97
+ position: { x: 16, y: 14 },
98
+ ownerId: 'policy-hidden',
99
+ },
100
+ ],
101
+ });
102
+
103
+ export const elementVisibilitySelectionDemoConfig: DemoConfig = {
104
+ id: 'element-visibility-selection',
105
+ title: 'Element Visibility + Selectability',
106
+ description:
107
+ 'QA scene for visible-but-unselectable element bodies, hidden-in-state elements, and port/link propagation. Shift + drag marquee to confirm only eligible items are collected.',
108
+ createState: createElementVisibilitySelectionState,
109
+ elementShapes: baseElementShapes,
110
+ portShapes: basePortShapes,
111
+ defaultElementShapeId: 'default',
112
+ defaultPortShapeId: 'port-circle',
113
+ actions: [
114
+ {
115
+ id: 'policy-filtered-selection',
116
+ label: 'Try filtered API selection',
117
+ run: (editor) => {
118
+ editor.setSelection([
119
+ 'policy-normal',
120
+ 'policy-unselectable',
121
+ 'policy-hidden',
122
+ 'policy-unselectable-port',
123
+ 'policy-hidden-link',
124
+ ]);
125
+ },
126
+ },
127
+ ],
128
+ };
@@ -5,7 +5,8 @@ import { linkPortCreationDemoConfig } from './linkPortCreationDemo';
5
5
  import { nestedDemoConfig } from './nestedDemo';
6
6
  import { customDemoConfig } from './customDemo';
7
7
  import { textDemoConfig } from './textDemo';
8
- import { selectionDemoConfig } from './selectionDemo';
8
+ import { selectionDemoConfig } from './selectionDemo';
9
+ import { elementVisibilitySelectionDemoConfig } from './elementVisibilitySelectionDemo';
9
10
  import { eventHandlersDemoConfig } from './eventHandlersDemo';
10
11
  import { engineEventsDemoConfig } from './engineEventsDemo';
11
12
  import { linkCancelDemoConfig } from './linkCancelDemo';
@@ -94,12 +95,18 @@ export const demoTabs: DemoDefinition[] = [
94
95
  description: textDemoConfig.description,
95
96
  Component: TextLayoutDemo,
96
97
  },
97
- {
98
- id: selectionDemoConfig.id,
99
- title: selectionDemoConfig.title,
100
- description: selectionDemoConfig.description,
101
- Component: wrapSimpleDemo(selectionDemoConfig),
102
- },
98
+ {
99
+ id: selectionDemoConfig.id,
100
+ title: selectionDemoConfig.title,
101
+ description: selectionDemoConfig.description,
102
+ Component: wrapSimpleDemo(selectionDemoConfig),
103
+ },
104
+ {
105
+ id: elementVisibilitySelectionDemoConfig.id,
106
+ title: elementVisibilitySelectionDemoConfig.title,
107
+ description: elementVisibilitySelectionDemoConfig.description,
108
+ Component: wrapSimpleDemo(elementVisibilitySelectionDemoConfig),
109
+ },
103
110
  {
104
111
  id: eventHandlersDemoConfig.id,
105
112
  title: eventHandlersDemoConfig.title,