@peter.prib/renderengine 0.2.4 → 0.2.5

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.
@@ -1,119 +1,119 @@
1
- import { render } from '../workspace/dom.js';
2
- import { CreateTemplateCommand } from './DiagramEditorCommands.js';
3
-
4
- /**
5
- * Palette — category picker plus a grid of draggable shapes.
6
- *
7
- * Previews are drawn by the templates themselves (Template#renderPreview), so a
8
- * palette tile always matches what lands on the canvas.
9
- */
10
- export class Palette {
11
- /**
12
- * @param {Object} options
13
- * @param {HTMLElement} options.container - Element the palette renders into (contents replaced)
14
- * @param {TemplateCatalog} options.catalog - Shapes offered in the current category
15
- * @param {DiagramEditor} options.editor - Editor receiving drops
16
- * @param {Array<{group: string, types: Array<{key: string, name: string, description?: string}>}>} [options.categories]
17
- * Grouped categories for the picker; omit to hide the picker
18
- * @param {string} [options.category] - Selected category key
19
- * @param {(key: string) => void} [options.onCategoryChange]
20
- */
21
- constructor(options = {}) {
22
- this.container = options.container;
23
- this.catalog = options.catalog;
24
- this.editor = options.editor;
25
- this.categories = options.categories ?? [];
26
- this.category = options.category ?? null;
27
- this.onCategoryChange = options.onCategoryChange ?? (() => {});
28
- }
29
-
30
- render() {
31
- if (!this.container) return;
32
- const doc = this.container.ownerDocument;
33
- render(this.container, [
34
- this.categories.length ? this._pickerSpec() : '',
35
- {
36
- style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '6px', overflowY: 'auto', flex: '1 1 auto', alignContent: 'start' },
37
- children: this.catalog.getAll().map(entry => this._tileSpec(entry, doc)),
38
- },
39
- ], doc);
40
- Object.assign(this.container.style, { display: 'flex', flexDirection: 'column', gap: '8px', minHeight: '0' });
41
- }
42
-
43
- _pickerSpec() {
44
- const selected = this.categories.flatMap(g => g.types).find(t => t.key === this.category);
45
- return {
46
- tag: 'select',
47
- attrs: { 'aria-label': 'Diagram type', title: selected?.description },
48
- style: { width: '100%', padding: '6px', fontSize: '12px', flex: '0 0 auto' },
49
- on: { change: (e) => this.onCategoryChange(e.target.value) },
50
- children: this.categories.map(({ group, types }) => ({
51
- tag: 'optgroup',
52
- attrs: { label: group },
53
- children: types.map(t => ({
54
- tag: 'option',
55
- attrs: { value: t.key, selected: t.key === this.category, title: t.description },
56
- text: t.name,
57
- })),
58
- })),
59
- };
60
- }
61
-
62
- _tileSpec(entry, doc) {
63
- return {
64
- attrs: { draggable: 'true', title: `Drag to add ${entry.name}` },
65
- style: {
66
- display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px',
67
- padding: '6px 4px', background: '#fff', border: '1px solid #ddd', borderRadius: '4px',
68
- cursor: 'grab', userSelect: 'none',
69
- },
70
- on: { dragstart: (e) => this._startDrag(e, entry) },
71
- children: [
72
- entry.factory('__preview', 0, 0).renderPreview(40, doc),
73
- {
74
- text: entry.name,
75
- style: { fontSize: '11px', color: '#333', textAlign: 'center', lineHeight: '1.2', overflowWrap: 'anywhere' },
76
- },
77
- ],
78
- };
79
- }
80
-
81
- _startDrag(e, entry) {
82
- if (!e.dataTransfer) return;
83
- e.dataTransfer.effectAllowed = 'copy';
84
- e.dataTransfer.setData('templateType', entry.type);
85
- }
86
-
87
- /**
88
- * Create the dragged shape at a drop point given in SVG viewBox coordinates,
89
- * snapping to the grid when it is shown.
90
- */
91
- handleDrop(e, x, y) {
92
- const templateType = e.dataTransfer.getData('templateType');
93
- if (!templateType) return false;
94
-
95
- let snapX = x, snapY = y;
96
- if (this.editor.showGrid && this.editor.gridSize) {
97
- const gridSize = this.editor.gridSize;
98
- snapX = Math.round(x / gridSize) * gridSize;
99
- snapY = Math.round(y / gridSize) * gridSize;
100
- }
101
-
102
- const template = this.catalog.create(templateType, this._generateId(templateType), snapX, snapY);
103
- if (!template) return false;
104
-
105
- if (this.editor.history) {
106
- this.editor.history.execute(new CreateTemplateCommand(this.editor, template));
107
- } else {
108
- this.editor.addTemplate(template);
109
- }
110
- this.editor.render();
111
- return true;
112
- }
113
-
114
- _generateId(templateType) {
115
- let counter = 1;
116
- while (this.editor.getTemplate(`${templateType}_${counter}`)) counter++;
117
- return `${templateType}_${counter}`;
118
- }
119
- }
1
+ import { render } from '../workspace/dom.js';
2
+ import { CreateTemplateCommand } from './DiagramEditorCommands.js';
3
+
4
+ /**
5
+ * Palette — category picker plus a grid of draggable shapes.
6
+ *
7
+ * Previews are drawn by the templates themselves (Template#renderPreview), so a
8
+ * palette tile always matches what lands on the canvas.
9
+ */
10
+ export class Palette {
11
+ /**
12
+ * @param {Object} options
13
+ * @param {HTMLElement} options.container - Element the palette renders into (contents replaced)
14
+ * @param {TemplateCatalog} options.catalog - Shapes offered in the current category
15
+ * @param {DiagramEditor} options.editor - Editor receiving drops
16
+ * @param {Array<{group: string, types: Array<{key: string, name: string, description?: string}>}>} [options.categories]
17
+ * Grouped categories for the picker; omit to hide the picker
18
+ * @param {string} [options.category] - Selected category key
19
+ * @param {(key: string) => void} [options.onCategoryChange]
20
+ */
21
+ constructor(options = {}) {
22
+ this.container = options.container;
23
+ this.catalog = options.catalog;
24
+ this.editor = options.editor;
25
+ this.categories = options.categories ?? [];
26
+ this.category = options.category ?? null;
27
+ this.onCategoryChange = options.onCategoryChange ?? (() => {});
28
+ }
29
+
30
+ render() {
31
+ if (!this.container) return;
32
+ const doc = this.container.ownerDocument;
33
+ render(this.container, [
34
+ this.categories.length ? this._pickerSpec() : '',
35
+ {
36
+ style: { display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: '6px', overflowY: 'auto', flex: '1 1 auto', alignContent: 'start' },
37
+ children: this.catalog.getAll().map(entry => this._tileSpec(entry, doc)),
38
+ },
39
+ ], doc);
40
+ Object.assign(this.container.style, { display: 'flex', flexDirection: 'column', gap: '8px', minHeight: '0' });
41
+ }
42
+
43
+ _pickerSpec() {
44
+ const selected = this.categories.flatMap(g => g.types).find(t => t.key === this.category);
45
+ return {
46
+ tag: 'select',
47
+ attrs: { 'aria-label': 'Diagram type', title: selected?.description },
48
+ style: { width: '100%', padding: '6px', fontSize: '12px', flex: '0 0 auto' },
49
+ on: { change: (e) => this.onCategoryChange(e.target.value) },
50
+ children: this.categories.map(({ group, types }) => ({
51
+ tag: 'optgroup',
52
+ attrs: { label: group },
53
+ children: types.map(t => ({
54
+ tag: 'option',
55
+ attrs: { value: t.key, selected: t.key === this.category, title: t.description },
56
+ text: t.name,
57
+ })),
58
+ })),
59
+ };
60
+ }
61
+
62
+ _tileSpec(entry, doc) {
63
+ return {
64
+ attrs: { draggable: 'true', title: entry.description ? `${entry.name}: ${entry.description}\nDrag to add` : `Drag to add ${entry.name}` },
65
+ style: {
66
+ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '4px',
67
+ padding: '6px 4px', background: '#fff', border: '1px solid #ddd', borderRadius: '4px',
68
+ cursor: 'grab', userSelect: 'none',
69
+ },
70
+ on: { dragstart: (e) => this._startDrag(e, entry) },
71
+ children: [
72
+ entry.factory('__preview', 0, 0).renderPreview(40, doc),
73
+ {
74
+ text: entry.name,
75
+ style: { fontSize: '11px', color: '#333', textAlign: 'center', lineHeight: '1.2', overflowWrap: 'anywhere' },
76
+ },
77
+ ],
78
+ };
79
+ }
80
+
81
+ _startDrag(e, entry) {
82
+ if (!e.dataTransfer) return;
83
+ e.dataTransfer.effectAllowed = 'copy';
84
+ e.dataTransfer.setData('templateType', entry.type);
85
+ }
86
+
87
+ /**
88
+ * Create the dragged shape at a drop point given in SVG viewBox coordinates,
89
+ * snapping to the grid when it is shown.
90
+ */
91
+ handleDrop(e, x, y) {
92
+ const templateType = e.dataTransfer.getData('templateType');
93
+ if (!templateType) return false;
94
+
95
+ let snapX = x, snapY = y;
96
+ if (this.editor.showGrid && this.editor.gridSize) {
97
+ const gridSize = this.editor.gridSize;
98
+ snapX = Math.round(x / gridSize) * gridSize;
99
+ snapY = Math.round(y / gridSize) * gridSize;
100
+ }
101
+
102
+ const template = this.catalog.create(templateType, this._generateId(templateType), snapX, snapY);
103
+ if (!template) return false;
104
+
105
+ if (this.editor.history) {
106
+ this.editor.history.execute(new CreateTemplateCommand(this.editor, template));
107
+ } else {
108
+ this.editor.addTemplate(template);
109
+ }
110
+ this.editor.render();
111
+ return true;
112
+ }
113
+
114
+ _generateId(templateType) {
115
+ let counter = 1;
116
+ while (this.editor.getTemplate(`${templateType}_${counter}`)) counter++;
117
+ return `${templateType}_${counter}`;
118
+ }
119
+ }
@@ -1,59 +1,61 @@
1
- /**
2
- * TemplateCatalog — registry of template types that can be added to a diagram.
3
- * Each entry describes a template type and how to create instances.
4
- */
5
- export class TemplateCatalog {
6
- constructor() {
7
- this.entries = new Map(); // type -> { name, icon, factory, properties }
8
- }
9
-
10
- /**
11
- * Register a template type.
12
- * @param {string} type - Unique type identifier
13
- * @param {Object} spec - Template specification
14
- * @param {string} spec.name - Display name
15
- * @param {string} spec.icon - Icon/emoji (e.g., "📦")
16
- * @param {Function} spec.factory - Factory function: (id, x, y) => Template instance
17
- * @param {Object} spec.properties - Default properties for this type
18
- */
19
- register(type, spec) {
20
- if (!spec.factory) {
21
- throw new Error(`TemplateCatalog.register: factory required for type '${type}'`);
22
- }
23
- this.entries.set(type, {
24
- type,
25
- name: spec.name || type,
26
- icon: spec.icon || '■',
27
- factory: spec.factory,
28
- properties: spec.properties || {},
29
- });
30
- }
31
-
32
- /**
33
- * Get a template type entry.
34
- */
35
- get(type) {
36
- return this.entries.get(type);
37
- }
38
-
39
- /**
40
- * Get all entries.
41
- */
42
- getAll() {
43
- return Array.from(this.entries.values());
44
- }
45
-
46
- /**
47
- * Create a template instance of given type.
48
- * @param {string} type
49
- * @param {string} id - Instance id
50
- * @param {number} x - X position
51
- * @param {number} y - Y position
52
- * @returns {Template|null}
53
- */
54
- create(type, id, x, y) {
55
- const entry = this.get(type);
56
- if (!entry) return null;
57
- return entry.factory(id, x, y);
58
- }
59
- }
1
+ /**
2
+ * TemplateCatalog — registry of template types that can be added to a diagram.
3
+ * Each entry describes a template type and how to create instances.
4
+ */
5
+ export class TemplateCatalog {
6
+ constructor() {
7
+ this.entries = new Map(); // type -> { name, icon, factory, properties }
8
+ }
9
+
10
+ /**
11
+ * Register a template type.
12
+ * @param {string} type - Unique type identifier
13
+ * @param {Object} spec - Template specification
14
+ * @param {string} spec.name - Display name
15
+ * @param {string} spec.icon - Icon/emoji (e.g., "📦")
16
+ * @param {Function} spec.factory - Factory function: (id, x, y) => Template instance
17
+ * @param {Object} spec.properties - Default properties for this type
18
+ * @param {string} [spec.description] - What it is (the palette shows it on hover)
19
+ */
20
+ register(type, spec) {
21
+ if (!spec.factory) {
22
+ throw new Error(`TemplateCatalog.register: factory required for type '${type}'`);
23
+ }
24
+ this.entries.set(type, {
25
+ type,
26
+ name: spec.name || type,
27
+ icon: spec.icon || '■',
28
+ factory: spec.factory,
29
+ properties: spec.properties || {},
30
+ ...(spec.description && { description: spec.description }),
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Get a template type entry.
36
+ */
37
+ get(type) {
38
+ return this.entries.get(type);
39
+ }
40
+
41
+ /**
42
+ * Get all entries.
43
+ */
44
+ getAll() {
45
+ return Array.from(this.entries.values());
46
+ }
47
+
48
+ /**
49
+ * Create a template instance of given type.
50
+ * @param {string} type
51
+ * @param {string} id - Instance id
52
+ * @param {number} x - X position
53
+ * @param {number} y - Y position
54
+ * @returns {Template|null}
55
+ */
56
+ create(type, id, x, y) {
57
+ const entry = this.get(type);
58
+ if (!entry) return null;
59
+ return entry.factory(id, x, y);
60
+ }
61
+ }