WIP WYSIWYG ~3D SVG editor.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 304 lines 8.0 kB view raw
1class Zoodle { 2 constructor(sceneElem, overlayElem, uiElem, propElem, outlineElem) { 3 // TODO: Calculate appropriate zooms and sizes 4 let zoom = 30; 5 let rotate = { x: -Math.atan(1 / Math.sqrt(2)), y: Zdog.TAU / 8 }; 6 7 this.scene = new Zdog.Illustration({ 8 element: sceneElem, 9 zoom: zoom, 10 resize: 'fullscreen', 11 rotate: rotate, 12 dragRotate: false, 13 }); 14 this.overlay = new Zdog.Illustration({ 15 element: overlayElem, 16 zoom: zoom, 17 resize: 'fullscreen', 18 rotate: rotate, 19 dragRotate: false, 20 }); 21 this.ui = new Zdog.Illustration({ 22 element: uiElem, 23 zoom: zoom, 24 resize: 'fullscreen', 25 rotate: rotate, 26 dragRotate: false, 27 }); 28 29 this.selection = []; 30 this.history = new History(); 31 this.tools = { 32 orbit: new OrbitTool(this), 33 translate: new TranslateTool(this), 34 rotate: new RotateTool(this), 35 }; 36 this.tool = this.tools.translate; 37 38 this.props = new Properties(propElem, this); 39 this.outliner = new Outliner(outlineElem, this); 40 41 this.presets = { 42 solar: new Solar(), 43 } 44 45 this.presets.solar.load(this.scene); 46 47 // TODO: We should capture input when dragging, but this seems to break clicking... 48 this.sceneInput = new Zfetch({ 49 scene: this.scene, 50 click: this.click.bind(this), 51 dragStart: this.dragStart.bind(this), 52 dragMove: this.dragMove.bind(this), 53 dragEnd: this.dragEnd.bind(this), 54 }); 55 56 this.uiInput = new Zfetch({ 57 scene: this.ui, 58 capture: true, 59 click: this.click.bind(this), 60 dragStart: this.dragStart.bind(this), 61 dragMove: this.dragMove.bind(this), 62 dragEnd: this.dragEnd.bind(this), 63 }); 64 65 uiElem.addEventListener("gotpointercapture", _ => uiElem.classList.add("active")); 66 uiElem.addEventListener("lostpointercapture", _ => uiElem.classList.remove("active")); 67 68 // TODO: Maybe we shouldn't listen to these if they're in an input element 69 this.registerShortcut("z", this.history.undo.bind(this.history), true); 70 this.registerShortcut("Z", this.history.redo.bind(this.history), true); 71 this.registerShortcut("y", this.history.redo.bind(this.history), true); 72 73 this.selection = [this.scene.children[0].children[0]]; 74 75 this.props.updatePanel(); 76 this.outliner.updatePanel(); 77 this.updateHighlights(); 78 this.updateUI(); 79 this.update(); 80 } 81 82 registerShortcut(key, callback, preventDefault) { 83 document.body.addEventListener("keydown", e => { 84 if (e.ctrlKey && e.key === key) { 85 if (preventDefault) { 86 e.preventDefault(); 87 } 88 callback(); 89 } 90 }); 91 } 92 93 update() { 94 this.syncLayers(); 95 this.scene.updateRenderGraph(); 96 this.overlay.updateRenderGraph(); 97 this.ui.updateRenderGraph(); 98 //this.scene.element.setAttribute("data-zdog", JSON.stringify(this.scene)); 99 requestAnimationFrame(this.update.bind(this)); 100 } 101 102 set tool(tool) { 103 this._tool = tool; 104 this.updateUI(); 105 } 106 get tool() { return this._tool; } 107 108 // Perform a command and add it to the history. 109 do(command) { 110 this.history.push(command); 111 command.do(); 112 } 113 114 // Store a record of a command that has already been performed. 115 did(command) { 116 this.history.push(command); 117 } 118 119 setSelection(targets) { 120 if (!Array.isArray(targets)) { 121 targets = [targets]; 122 } 123 124 this.selection = targets; 125 } 126 127 toggleSelection(target) { 128 const index = this.selection.indexOf(target); 129 if (index !== -1) { 130 this.selection.splice(index, 1); 131 } else { 132 this.selection.push(target); 133 } 134 } 135 136 clearSelection() { 137 this.selection.length = 0; 138 } 139 140 updateHighlights() { 141 this.outliner.updateHighlights(); 142 143 this.overlay.children = []; 144 this.selection.forEach((selected) => { 145 let highlight = selected.copyGraph({ 146 addTo: this.overlay, 147 color: "#E62", 148 backface: "#E62", 149 ...this.getWorldTransforms(selected), 150 }); 151 152 // Highlight all children 153 highlight.flatGraph.forEach((child) => { 154 child.color = '#E62'; 155 child.backface = '#E62'; 156 }); 157 158 // Apply parent transforms to selected object. 159 // Zdog.extend(highlight, this.getWorldTransforms(selected)); 160 }); 161 } 162 163 updateUI() { 164 this.ui.children = []; 165 166 if (this.selection.length === 0) { 167 return; 168 } 169 170 this.tool.drawWidget(this.selection); 171 } 172 173 syncLayers() { 174 const scene = this.scene; 175 const targets = [this.overlay, this.ui]; 176 177 targets.forEach((target) => { 178 target.translate = scene.translate; 179 target.rotate = scene.rotate; 180 target.scale = scene.scale; 181 target.zoom = scene.zoom; 182 target.updateGraph(); 183 }); 184 } 185 186 getWorldTransforms(target) { 187 let translate = new Zdog.Vector(); 188 let rotate = new Zdog.Vector(); 189 let scale = new Zdog.Vector({x: 1, y: 1, z: 1}); 190 191 let right = new Zdog.Vector({x: 1, y: 0, z: 0}); 192 let down = new Zdog.Vector({x: 0, y: 1, z: 0}); 193 let forward = new Zdog.Vector({x: 0, y: 0, z: 1}); 194 195 // Condense transforms from all ancestors except the root. 196 while (target.addTo) { 197 translate.transform(target.translate, target.rotate, target.scale); 198 forward.rotate(target.rotate); 199 down.rotate(target.rotate); 200 right.rotate(target.rotate); 201 scale.multiply(target.scale); 202 203 target = target.addTo; 204 } 205 206 if (Math.abs(forward.x) > 0.99999) { 207 // Gimbal lock case 208 rotate.x = 0; 209 rotate.y = forward.x > 0 ? -Math.PI/2 : Math.PI/2; 210 rotate.z = Math.atan2(right.y, down.y); 211 } else { 212 rotate.y = -Math.asin(forward.x); 213 rotate.x = Math.atan2(-forward.y, forward.z); 214 rotate.z = Math.atan2(-down.x, right.x); 215 } 216 return {translate: translate, rotate: rotate, scale: scale}; 217 } 218 219 // input 220 click(ptr, target, x, y) { 221 target.layer = this.getLayer(target.element) 222 this.do(new SelectCommand(this, target)); 223 } 224 225 dragStart(ptr, target, x, y) { 226 target.layer = this.getLayer(target.element); 227 if (!target || !target.addTo) { 228 this.tool = new TemporaryTool(this, this.tool, this.tools.orbit, true); 229 } 230 231 this.tool.start(ptr, target, x, y); 232 } 233 234 dragMove(ptr, target, x, y) { 235 target.layer = this.getLayer(target.element); 236 this.tool.move(ptr, target, x, y); 237 } 238 239 dragEnd(ptr, target, x, y) { 240 target.layer = this.getLayer(target.element); 241 this.tool.end(ptr, target, x, y); 242 } 243 244 getLayer(element) { 245 switch (element) { 246 case this.scene.element: 247 return Zoodle.LAYER_CANVAS; 248 case this.overlay.element: 249 return Zoodle.LAYER_OVERLAY; 250 case this.ui.element: 251 return Zoodle.LAYER_UI; 252 default: 253 console.error(`Unsupported element ${element}`); 254 } 255 } 256 257 // returns the distance of a point (x, y) from the origin along the axis defined by the angle. 258 getAxisDistance(x, y, angle) { 259 return x * Math.cos(angle) + y * Math.sin(angle); 260 } 261 262 static get LAYER_CANVAS() { return 1; } 263 static get LAYER_OVERLAY() { return 2; } 264 static get LAYER_UI() { return 3; } 265} 266 267class History { 268 constructor() { 269 this.undoStack = []; 270 this.redoStack = []; 271 } 272 273 push(command) { 274 this.undoStack.push(command); 275 this.redoStack = []; 276 } 277 278 undo() { 279 if (this.undoStack.length === 0) { 280 return; 281 } 282 283 let command = this.undoStack.pop(); 284 command.undo(); 285 this.redoStack.push(command); 286 } 287 288 redo() { 289 if (this.redoStack.length === 0) { 290 return; 291 } 292 293 let command = this.redoStack.pop(); 294 command.do(); 295 this.undoStack.push(command); 296 } 297} 298 299const sceneElem = document.querySelector("#canvas"); 300const overlayElem = document.querySelector("#overlay"); 301const uiElem = document.querySelector("#ui"); 302const propElem = document.querySelector("#properties"); 303const outlineElem = document.querySelector("#outliner"); 304const zoodle = new Zoodle(sceneElem, overlayElem, uiElem, propElem, outlineElem);